diff --git a/lib/services/queue.ts b/lib/services/queue.ts index 9d8e0d3..f5f1455 100644 --- a/lib/services/queue.ts +++ b/lib/services/queue.ts @@ -2,48 +2,141 @@ * Queue service — issue queue fetching. * * Fetches issue queues per project from the issue provider. - * Pure functions, no tool registration or state mutation. + * Uses workflow config for queue labels — no hardcoded state names. */ import type { Issue } from "../providers/provider.js"; import { createProvider } from "../providers/index.js"; import type { Project } from "../projects.js"; +import { + DEFAULT_WORKFLOW, + type WorkflowConfig, + type Role, +} from "../workflow.js"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- +/** + * @deprecated Use string labels from workflow config instead. + * Kept for backward compatibility. + */ export type QueueLabel = "To Improve" | "To Test" | "To Do"; -export const QUEUE_PRIORITY: Record = { +/** + * @deprecated Use getQueuePriority() instead. + * Kept for backward compatibility. + */ +export const QUEUE_PRIORITY: Record = { "To Improve": 3, "To Test": 2, "To Do": 1, }; -export function getTaskPriority(label: QueueLabel, issue: Issue): number { - return QUEUE_PRIORITY[label] * 10000 - issue.iid; +// --------------------------------------------------------------------------- +// Workflow-driven helpers +// --------------------------------------------------------------------------- + +/** + * Get queue labels with their priorities from workflow config. + * Returns labels sorted by priority (highest first). + */ +export function getQueueLabelsWithPriority( + workflow: WorkflowConfig = DEFAULT_WORKFLOW, +): Array<{ label: string; priority: number; role?: Role }> { + const labels: Array<{ label: string; priority: number; role?: Role }> = []; + + for (const state of Object.values(workflow.states)) { + if (state.type === "queue") { + labels.push({ + label: state.label, + priority: state.priority ?? 0, + role: state.role, + }); + } + } + + return labels.sort((a, b) => b.priority - a.priority); } -export function getRoleForLabel(label: QueueLabel): "dev" | "qa" { - return label === "To Test" ? "qa" : "dev"; +/** + * Get the priority for a queue label from workflow config. + */ +export function getQueuePriority( + label: string, + workflow: WorkflowConfig = DEFAULT_WORKFLOW, +): number { + const state = Object.values(workflow.states).find( + (s) => s.label === label && s.type === "queue", + ); + return state?.priority ?? 0; +} + +/** + * Get task priority for sorting (higher = more urgent). + * Priority = queue_priority * 10000 - issue_id (older issues first within same queue). + */ +export function getTaskPriority( + label: string, + issue: Issue, + workflow: WorkflowConfig = DEFAULT_WORKFLOW, +): number { + const priority = getQueuePriority(label, workflow); + return priority * 10000 - issue.iid; +} + +/** + * Get the role assigned to a queue label. + */ +export function getRoleForLabel( + label: string, + workflow: WorkflowConfig = DEFAULT_WORKFLOW, +): Role | null { + const state = Object.values(workflow.states).find( + (s) => s.label === label && s.type === "queue", + ); + return state?.role ?? null; } // --------------------------------------------------------------------------- // Fetching // --------------------------------------------------------------------------- -export async function fetchProjectQueues(project: Project): Promise> { +/** + * Fetch all queued issues for a project, grouped by queue label. + * Uses workflow config for queue labels. + */ +export async function fetchProjectQueues( + project: Project, + workflow: WorkflowConfig = DEFAULT_WORKFLOW, +): Promise> { const { provider } = await createProvider({ repo: project.repo }); - const labels: QueueLabel[] = ["To Improve", "To Test", "To Do"]; - const queues: Record = { "To Improve": [], "To Test": [], "To Do": [] }; + const queueLabels = getQueueLabelsWithPriority(workflow); + const queues: Record = {}; - for (const label of labels) { + // Initialize all queue labels with empty arrays + for (const { label } of queueLabels) { + queues[label] = []; + } + + // Fetch issues for each queue + for (const { label } of queueLabels) { try { const issues = await provider.listIssuesByLabel(label); - queues[label] = issues.sort((a, b) => getTaskPriority(label, b) - getTaskPriority(label, a)); + queues[label] = issues.sort( + (a, b) => getTaskPriority(label, b, workflow) - getTaskPriority(label, a, workflow), + ); } catch { queues[label] = []; } } + return queues; } + +/** + * Get total count of queued issues across all queues. + */ +export function getTotalQueuedCount(queues: Record): number { + return Object.values(queues).reduce((sum, issues) => sum + issues.length, 0); +} diff --git a/lib/tools/status.ts b/lib/tools/status.ts index 85d38cc..1a5443d 100644 --- a/lib/tools/status.ts +++ b/lib/tools/status.ts @@ -9,8 +9,9 @@ import { jsonResult } from "openclaw/plugin-sdk"; import type { ToolContext } from "../types.js"; import { readProjects, getProject } from "../projects.js"; import { log as auditLog } from "../audit.js"; -import { fetchProjectQueues, type QueueLabel } from "../services/queue.js"; +import { fetchProjectQueues, getTotalQueuedCount, getQueueLabelsWithPriority } from "../services/queue.js"; import { requireWorkspaceDir, getPluginConfig } from "../tool-helpers.js"; +import { DEFAULT_WORKFLOW } from "../workflow.js"; export function createStatusTool(api: OpenClawPluginApi) { return (ctx: ToolContext) => ({ @@ -31,6 +32,9 @@ export function createStatusTool(api: OpenClawPluginApi) { const pluginConfig = getPluginConfig(api); const projectExecution = (pluginConfig?.projectExecution as string) ?? "parallel"; + // TODO: Load per-project workflow when supported + const workflow = DEFAULT_WORKFLOW; + const data = await readProjects(workspaceDir); const projectIds = groupId ? [groupId] : Object.keys(data.projects); @@ -40,30 +44,59 @@ export function createStatusTool(api: OpenClawPluginApi) { const project = getProject(data, pid); if (!project) return null; - const queues = await fetchProjectQueues(project); - const count = (label: QueueLabel) => queues[label].length; + const queues = await fetchProjectQueues(project, workflow); + + // Build dynamic queue object with counts + const queueCounts: Record = {}; + for (const [label, issues] of Object.entries(queues)) { + queueCounts[label] = issues.length; + } return { name: project.name, groupId: pid, roleExecution: project.roleExecution ?? "parallel", - dev: { active: project.dev.active, issueId: project.dev.issueId, level: project.dev.level, startTime: project.dev.startTime }, - qa: { active: project.qa.active, issueId: project.qa.issueId, level: project.qa.level, startTime: project.qa.startTime }, - queue: { toImprove: count("To Improve"), toTest: count("To Test"), toDo: count("To Do") }, + dev: { + active: project.dev.active, + issueId: project.dev.issueId, + level: project.dev.level, + startTime: project.dev.startTime, + }, + qa: { + active: project.qa.active, + issueId: project.qa.issueId, + level: project.qa.level, + startTime: project.qa.startTime, + }, + queue: queueCounts, }; }), ); - const filtered = projects.filter(Boolean); + const filtered = projects.filter(Boolean) as NonNullable[]; + + // Calculate total queued across all projects + const totalQueued = filtered.reduce( + (sum, p) => sum + Object.values(p.queue).reduce((s, c) => s + c, 0), + 0, + ); await auditLog(workspaceDir, "status", { projectCount: filtered.length, - totalQueued: filtered.reduce((s, p) => s + p!.queue.toImprove + p!.queue.toTest + p!.queue.toDo, 0), + totalQueued, }); + // Include queue labels in response for context + const queueLabels = getQueueLabelsWithPriority(workflow).map((q) => ({ + label: q.label, + role: q.role, + priority: q.priority, + })); + return jsonResult({ success: true, execution: { projectExecution }, + queueLabels, projects: filtered, }); },