From b57ede0863d3cafa757c27280d9ccf6f5038f59d Mon Sep 17 00:00:00 2001 From: Lauren ten Hoor Date: Mon, 16 Feb 2026 18:47:01 +0800 Subject: [PATCH] feat: rename design_task to research_task and update related documentation --- docs/ARCHITECTURE.md | 2 +- docs/CONFIGURATION.md | 20 +-- docs/MANAGEMENT.md | 2 +- docs/ROADMAP.md | 4 +- docs/TESTING.md | 2 +- docs/TOOLS.md | 21 +-- index.ts | 4 +- lib/services/bootstrap.e2e.test.ts | 6 +- lib/services/health.ts | 9 ++ lib/templates.ts | 54 ++++---- ...ign-task.test.ts => research-task.test.ts} | 48 ++++--- .../{design-task.ts => research-task.ts} | 100 +++++++-------- lib/tools/task-update.test.ts | 2 +- lib/tools/work-finish.ts | 121 ++++++++++++++++-- lib/workflow.ts | 27 ++-- 15 files changed, 249 insertions(+), 173 deletions(-) rename lib/tools/{design-task.test.ts => research-task.test.ts} (67%) rename lib/tools/{design-task.ts => research-task.ts} (62%) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7cb3558..78dae0e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -583,7 +583,7 @@ Every piece of data and where it lives: │ status → read labels + read state │ │ health → check sessions + fix zombies │ │ project_register → labels + prompts + state init (one-time) │ -│ design_task → architect dispatch │ +│ research_task → architect dispatch │ │ │ │ Bootstrap hook → injects role instructions into worker sessions│ │ Review pass → polls PR status, auto-merges approved PRs │ diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index fd16ad4..6faae71 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -164,24 +164,10 @@ workflow: type: terminal label: Done color: "#5cb85c" - toDesign: - type: queue - role: architect - label: To Design - color: "#0075ca" - priority: 1 - on: - PICKUP: designing - designing: - type: active - role: architect - label: Designing - color: "#d4c5f9" - on: - COMPLETE: planning - BLOCKED: refining ``` +Note: The architect role has no dedicated workflow states. Design tasks are triggered via `research_task` tool only — issues go directly to Planning. + **State types:** | Type | Description | @@ -340,7 +326,7 @@ Restrict DevClaw tools to your orchestrator agent: "project_register", "setup", "onboard", - "design_task" + "research_task" ] } } diff --git a/docs/MANAGEMENT.md b/docs/MANAGEMENT.md index c4d541e..bec85ad 100644 --- a/docs/MANAGEMENT.md +++ b/docs/MANAGEMENT.md @@ -53,7 +53,7 @@ Planning → To Do → Doing → To Test → Testing → Done ↘ Refining → (human decision) ``` -The ARCHITECT role adds a parallel track: `To Design → Designing → Planning`. +The ARCHITECT role is tool-triggered only via `research_task` — no queue states. Issues go directly to Planning, the architect researches and posts findings, then the issue stays in Planning for human review. Every label transition, state update, and audit log entry happens atomically inside the plugin. The orchestrator agent cannot skip a step, forget a label, or corrupt session state — because those operations are deterministic code, not instructions an LLM follows imperfectly. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index c252e45..4a6d21c 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -17,7 +17,7 @@ Planning → To Do → Doing → To Test → Testing → Done ↘ In Review → (PR approved → auto-merge) → To Test ↘ To Improve → Doing (merge conflict / fix cycle) ↘ Refining → (human decision) -To Design → Designing → Planning +research_task → Planning (architect researches, stays in Planning) ``` States have types (`queue`, `active`, `hold`, `review`, `terminal`), transitions with actions (`gitPull`, `detectPr`, `mergePr`, `closeIssue`, `reopenIssue`), and review checks (`prMerged`, `prApproved`). @@ -46,7 +46,7 @@ DEVELOPER can submit a PR for human review (`result: "review"`), which transitio ### Architect Role -The architect role enables design investigations. `design_task` creates a `To Design` issue and dispatches an architect worker. The architect completes with `done` (→ Planning) or `blocked` (→ Refining). +The architect role enables design investigations. `research_task` creates a Planning issue with rich context and dispatches an architect worker directly (no queue states). The architect posts findings as comments, then completes with `done` (stays in Planning for human review) or `blocked` (→ Refining). ### Workspace Layout Migration diff --git a/docs/TESTING.md b/docs/TESTING.md index b19c16d..90b6344 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -28,7 +28,7 @@ npm run build | [lib/projects.test.ts](../lib/projects.test.ts) | Project state: read/write, worker state, atomic file operations | | [lib/bootstrap-hook.test.ts](../lib/bootstrap-hook.test.ts) | Bootstrap hook: role instruction loading, source tracking, overloads | | [lib/tools/task-update.test.ts](../lib/tools/task-update.test.ts) | Task update tool: label transitions, validation | -| [lib/tools/design-task.test.ts](../lib/tools/design-task.test.ts) | Design task tool: architect dispatch | +| [lib/tools/research-task.test.ts](../lib/tools/research-task.test.ts) | Research task tool: architect dispatch | | [lib/tools/queue-status.test.ts](../lib/tools/queue-status.test.ts) | Queue status formatting | | [lib/setup/migrate-layout.test.ts](../lib/setup/migrate-layout.test.ts) | Workspace layout migration: `projects/` → `devclaw/` | diff --git a/docs/TOOLS.md b/docs/TOOLS.md index 7e3a162..813a422 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -79,8 +79,8 @@ Complete a task with a result. Called by workers (DEVELOPER/TESTER/ARCHITECT sub | tester | `"fail"` | Testing → To Improve | Issue reopened | | tester | `"refine"` | Testing → Refining | Awaits human decision | | tester | `"blocked"` | Testing → Refining | Awaits human decision | -| architect | `"done"` | Designing → Planning | Design complete | -| architect | `"blocked"` | Designing → Refining | Awaits human decision | +| architect | `"done"` | stays in Planning | Design complete, ready for human review | +| architect | `"blocked"` | Planning → Refining | Awaits human decision | **What it does atomically:** @@ -140,7 +140,7 @@ Change an issue's state label manually without going through the full pickup/com | `state` | StateLabel | Yes | New state label | | `reason` | string | No | Audit log reason for the change | -**Valid states:** `Planning`, `To Do`, `Doing`, `To Test`, `Testing`, `Done`, `To Improve`, `Refining`, `In Review`, `To Design`, `Designing` +**Valid states:** `Planning`, `To Do`, `Doing`, `To Test`, `Testing`, `Done`, `To Improve`, `Refining`, `In Review` **Use cases:** @@ -331,11 +331,11 @@ Conversational onboarding guide. Returns step-by-step instructions for the agent --- -### `design_task` +### `research_task` -Spawn an architect for a design investigation. Creates a "To Design" issue and dispatches an architect worker. +Spawn an architect for a design investigation. Creates a Planning issue with rich context and dispatches an architect worker. No queue states — tool-triggered only. -**Source:** [`lib/tools/design-task.ts`](../lib/tools/design-task.ts) +**Source:** [`lib/tools/research-task.ts`](../lib/tools/research-task.ts) **Parameters:** @@ -343,8 +343,9 @@ Spawn an architect for a design investigation. Creates a "To Design" issue and d |---|---|---|---| | `projectGroupId` | string | Yes | Project group ID | | `title` | string | Yes | Design task title | -| `description` | string | No | Design problem description | -| `level` | `"junior"` \| `"senior"` | No | Architect level. Default: `"junior"`. | +| `description` | string | Yes | Detailed background context for the architect | +| `focusAreas` | string[] | No | Specific areas to investigate | +| `complexity` | `"simple"` \| `"medium"` \| `"complex"` | No | Guides level selection. Default: `"medium"`. | --- @@ -360,8 +361,8 @@ tester:pass → Testing → Done (close issue) tester:fail → Testing → To Improve (reopen issue) tester:refine → Testing → Refining (awaits human decision) tester:blocked → Testing → Refining (awaits human decision) -architect:done → Designing → Planning (design complete) -architect:blocked → Designing → Refining (awaits human decision) +architect:done → stays in Planning (design complete, ready for human review) +architect:blocked → Planning → Refining (awaits human decision) ``` ## Issue Priority Order diff --git a/index.ts b/index.ts index d646a1f..6ba7d6b 100644 --- a/index.ts +++ b/index.ts @@ -10,7 +10,7 @@ import { createProjectRegisterTool } from "./lib/tools/project-register.js"; import { createSetupTool } from "./lib/tools/setup.js"; import { createOnboardTool } from "./lib/tools/onboard.js"; import { createAutoConfigureModelsTool } from "./lib/tools/autoconfigure-models.js"; -import { createDesignTaskTool } from "./lib/tools/design-task.js"; +import { createResearchTaskTool } from "./lib/tools/research-task.js"; import { registerCli } from "./lib/cli.js"; import { registerHeartbeatService } from "./lib/services/heartbeat.js"; import { registerBootstrapHook } from "./lib/bootstrap-hook.js"; @@ -78,7 +78,7 @@ const plugin = { api.registerTool(createTaskCommentTool(api), { names: ["task_comment"] }); // Architect - api.registerTool(createDesignTaskTool(api), { names: ["design_task"] }); + api.registerTool(createResearchTaskTool(api), { names: ["research_task"] }); // Operations api.registerTool(createStatusTool(api), { names: ["status"] }); diff --git a/lib/services/bootstrap.e2e.test.ts b/lib/services/bootstrap.e2e.test.ts index 433ac2e..9872c8b 100644 --- a/lib/services/bootstrap.e2e.test.ts +++ b/lib/services/bootstrap.e2e.test.ts @@ -213,7 +213,7 @@ describe("E2E bootstrap — hook injection", () => { it("should resolve architect instructions with project override", async () => { h = await createTestHarness({ projectName: "arch-proj" }); - h.provider.seedIssue({ iid: 6, title: "Design API", labels: ["To Design"] }); + h.provider.seedIssue({ iid: 6, title: "Design API", labels: ["Planning"] }); await h.writePrompt("architect", "# Default Architect\nGeneral design guidelines."); await h.writePrompt("architect", "# Arch Proj Architect\nUse event-driven architecture.", "arch-proj"); @@ -229,8 +229,8 @@ describe("E2E bootstrap — hook injection", () => { issueUrl: "https://example.com/issues/6", role: "architect", level: "senior", - fromLabel: "To Design", - toLabel: "Designing", + fromLabel: "Planning", + toLabel: "Planning", transitionLabel: (id, from, to) => h.provider.transitionLabel(id, from, to), provider: h.provider, }); diff --git a/lib/services/health.ts b/lib/services/health.ts index 68167ba..5e2ed8e 100644 --- a/lib/services/health.ts +++ b/lib/services/health.ts @@ -28,6 +28,7 @@ import { DEFAULT_WORKFLOW, getActiveLabel, getRevertLabel, + hasWorkflowStates, type WorkflowConfig, type Role, } from "../workflow.js"; @@ -161,6 +162,10 @@ export async function checkWorkerHealth(opts: { } = opts; const fixes: HealthFix[] = []; + + // Skip roles without workflow states (e.g. architect — tool-triggered only) + if (!hasWorkflowStates(workflow, role)) return fixes; + const worker = getWorker(project, role); const sessionKey = worker.level ? getSessionForLevel(worker, worker.level) : null; @@ -430,6 +435,10 @@ export async function scanOrphanedLabels(opts: { } = opts; const fixes: HealthFix[] = []; + + // Skip roles without workflow states (e.g. architect — tool-triggered only) + if (!hasWorkflowStates(workflow, role)) return fixes; + const worker = getWorker(project, role); // Get labels from workflow config diff --git a/lib/templates.ts b/lib/templates.ts index 2ac34c0..a85ac40 100644 --- a/lib/templates.ts +++ b/lib/templates.ts @@ -48,43 +48,42 @@ export const DEFAULT_QA_INSTRUCTIONS = `# TESTER Worker Instructions export const DEFAULT_ARCHITECT_INSTRUCTIONS = `# Architect Worker Instructions -You design and investigate architecture/design questions systematically. +You research design/architecture questions and produce detailed, development-ready findings. ## Your Job -Investigate the design problem thoroughly: -1. **Understand the problem** — Read the issue, comments, and codebase -2. **Research alternatives** — Explore >= 3 viable approaches -3. **Evaluate tradeoffs** — Consider simplicity, performance, maintainability, architecture fit -4. **Recommend** — Pick the best option with clear reasoning -5. **Outline implementation** — Break down into developer tasks +The issue contains background context and constraints. Your goal is to produce findings detailed enough that a developer can start implementation immediately — no further research needed. + +1. **Understand the problem** — Read the issue body carefully. It contains the background context, constraints, and focus areas. +2. **Research thoroughly** — Explore the codebase, read docs, search the web. Understand the current state deeply. +3. **Investigate alternatives** — Research >= 3 viable approaches with concrete pros/cons and effort estimates. +4. **Recommend** — Pick the best option with clear, evidence-based reasoning. +5. **Outline implementation** — Break down into specific, actionable developer tasks with enough detail to start coding. ## Output Format -Structure your findings as: +Post your findings as issue comments. Structure them as: ### Problem Statement -Why is this design decision important? +Why is this design decision important? What breaks if we get it wrong? ### Current State -What exists today? Current limitations? +What exists today? Current limitations? Relevant code paths. ### Alternatives Investigated **Option A: [Name]** +- Approach: [Concrete description of what this looks like] - Pros: ... - Cons: ... - Effort estimate: X hours +- Key code paths affected: [files/modules] **Option B: [Name]** -- Pros: ... -- Cons: ... -- Effort estimate: X hours +(same structure) **Option C: [Name]** -- Pros: ... -- Cons: ... -- Effort estimate: X hours +(same structure) ### Recommendation **Option X** is recommended because: @@ -93,24 +92,26 @@ What exists today? Current limitations? - [Long-term implications] ### Implementation Outline -- [ ] Task 1: [Description] +Detailed enough for a developer to start immediately: +- [ ] Task 1: [Description — what to change, where, how] - [ ] Task 2: [Description] - [ ] Task 3: [Description] ### References -- [Code examples, prior art, related issues] +- [Code paths, docs, prior art, related issues] -## Available Tools +## Important -- web_search, web_fetch (research patterns) -- Read files (explore codebase) -- exec (run commands, search code) +- **Be thorough** — Your output becomes the spec for development. Missing detail = blocked developer. +- **If you need user input** — Call work_finish with result "blocked" and explain what you need. Do NOT guess on ambiguous requirements. +- **Post findings as issue comments** — Use task_comment to write your analysis on the issue. ## Completion When done, call work_finish with: - role: "architect" -- result: "done" +- result: "done" — findings posted, ready for human review +- result: "blocked" — you need human input to proceed (goes to Refining) - summary: Brief summary of your recommendation Your session is persistent — you may be called back for refinements. @@ -261,7 +262,7 @@ All orchestration goes through these tools. You do NOT manually manage sessions, | \`health\` | Scan worker health: zombies, stale workers, orphaned state. Pass fix=true to auto-fix | | \`work_start\` | End-to-end: label transition, level assignment, session create/reuse, dispatch with role instructions | | \`work_finish\` | End-to-end: label transition, state update, issue close/reopen | -| \`design_task\` | Spawn an architect for design investigation. Creates To Design issue and dispatches architect | +| \`research_task\` | Spawn an architect for design investigation. Creates Planning issue with rich context and dispatches architect | ### First Thing on Session Start @@ -277,7 +278,7 @@ Planning → To Do → Doing → To Review ──┬── [agent] → Reviewing To Improve → Doing (fix cycle) Refining (human decision) -To Design → Designing → Planning (design complete) +research_task → Planning (architect researches, posts findings, stays in Planning) \`\`\` Review policy (configurable per project in workflow.yaml): @@ -317,7 +318,8 @@ Workers call \`work_finish\` themselves — the label transition, state update, - Tester "fail" → "To Improve" → scheduler dispatches Developer - Tester "pass" → Done, no further dispatch - Tester "refine" / blocked → needs human input -- Architect "done" → "Planning" → ready for tech lead review +- Architect "done" → stays in "Planning" → ready for tech lead review +- Architect "blocked" → "Refining" → needs human input **Always include issue URLs** in your response — these are in the \`announcement\` fields. diff --git a/lib/tools/design-task.test.ts b/lib/tools/research-task.test.ts similarity index 67% rename from lib/tools/design-task.test.ts rename to lib/tools/research-task.test.ts index be6491b..e05ebdd 100644 --- a/lib/tools/design-task.test.ts +++ b/lib/tools/research-task.test.ts @@ -1,6 +1,6 @@ /** - * Tests for architect role, design_task tool, and workflow integration. - * Run with: npx tsx --test lib/tools/design-task.test.ts + * Tests for architect role, research_task tool, and workflow integration. + * Run with: npx tsx --test lib/tools/research-task.test.ts */ import { describe, it } from "node:test"; import assert from "node:assert"; @@ -8,8 +8,8 @@ import { parseDevClawSessionKey } from "../bootstrap-hook.js"; import { isLevelForRole, roleForLevel, resolveModel, getDefaultModel, getEmoji } from "../roles/index.js"; import { selectLevel } from "../model-selector.js"; import { - DEFAULT_WORKFLOW, getQueueLabels, getActiveLabel, getCompletionRule, - getCompletionEmoji, detectRoleFromLabel, getStateLabels, + DEFAULT_WORKFLOW, getQueueLabels, getCompletionRule, + getCompletionEmoji, getStateLabels, hasWorkflowStates, } from "../workflow.js"; describe("architect tiers", () => { @@ -42,42 +42,38 @@ describe("architect tiers", () => { }); }); -describe("architect workflow states", () => { - it("should include To Design and Designing in state labels", () => { +describe("architect workflow — no dedicated states", () => { + it("should NOT have To Design or Designing in state labels", () => { const labels = getStateLabels(DEFAULT_WORKFLOW); - assert.ok(labels.includes("To Design")); - assert.ok(labels.includes("Designing")); + assert.ok(!labels.includes("To Design"), "To Design should not exist"); + assert.ok(!labels.includes("Designing"), "Designing should not exist"); }); - it("should have To Design as architect queue label", () => { + it("should have no queue labels for architect", () => { const queues = getQueueLabels(DEFAULT_WORKFLOW, "architect"); - assert.deepStrictEqual(queues, ["To Design"]); + assert.deepStrictEqual(queues, []); }); - it("should have Designing as architect active label", () => { - assert.strictEqual(getActiveLabel(DEFAULT_WORKFLOW, "architect"), "Designing"); + it("should report architect has no workflow states", () => { + assert.strictEqual(hasWorkflowStates(DEFAULT_WORKFLOW, "architect"), false); }); - it("should detect architect role from To Design label", () => { - assert.strictEqual(detectRoleFromLabel(DEFAULT_WORKFLOW, "To Design"), "architect"); + it("should report developer has workflow states", () => { + assert.strictEqual(hasWorkflowStates(DEFAULT_WORKFLOW, "developer"), true); }); - it("should have architect:done completion rule", () => { - const rule = getCompletionRule(DEFAULT_WORKFLOW, "architect", "done"); - assert.ok(rule); - assert.strictEqual(rule!.from, "Designing"); - assert.strictEqual(rule!.to, "Planning"); + it("should report tester has workflow states", () => { + assert.strictEqual(hasWorkflowStates(DEFAULT_WORKFLOW, "tester"), true); }); - it("should have architect:blocked completion rule", () => { - const rule = getCompletionRule(DEFAULT_WORKFLOW, "architect", "blocked"); - assert.ok(rule); - assert.strictEqual(rule!.from, "Designing"); - assert.strictEqual(rule!.to, "Refining"); + it("should have no completion rules for architect (no active state)", () => { + const doneRule = getCompletionRule(DEFAULT_WORKFLOW, "architect", "done"); + assert.strictEqual(doneRule, null); + const blockedRule = getCompletionRule(DEFAULT_WORKFLOW, "architect", "blocked"); + assert.strictEqual(blockedRule, null); }); - it("should have completion emoji by result type", () => { - // Emoji is now keyed by result, not role:result + it("should still have completion emoji for architect results", () => { assert.strictEqual(getCompletionEmoji("architect", "done"), "✅"); assert.strictEqual(getCompletionEmoji("architect", "blocked"), "🚫"); }); diff --git a/lib/tools/design-task.ts b/lib/tools/research-task.ts similarity index 62% rename from lib/tools/design-task.ts rename to lib/tools/research-task.ts index 37ec7ca..4f72d8a 100644 --- a/lib/tools/design-task.ts +++ b/lib/tools/research-task.ts @@ -1,9 +1,11 @@ /** - * design_task — Spawn an architect to investigate a design problem. + * research_task — Spawn an architect to research a design/architecture problem. * - * Creates a "To Design" issue and optionally dispatches an architect worker. - * The architect investigates systematically, then produces structured findings - * as a GitHub issue in Planning state. + * Creates a Planning issue with rich context and dispatches an architect worker. + * The architect researches the problem and produces detailed findings as issue comments. + * The issue stays in Planning — ready for human review when the architect completes. + * + * No queue states — tool-triggered only. */ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { jsonResult } from "openclaw/plugin-sdk"; @@ -13,33 +15,41 @@ import { getWorker } from "../projects.js"; import { dispatchTask } from "../dispatch.js"; import { log as auditLog } from "../audit.js"; import { requireWorkspaceDir, resolveProject, resolveProvider, getPluginConfig } from "../tool-helpers.js"; -import { loadWorkflow, getActiveLabel, getQueueLabels } from "../workflow.js"; import { loadConfig } from "../config/index.js"; import { selectLevel } from "../model-selector.js"; import { resolveModel } from "../roles/index.js"; -export function createDesignTaskTool(api: OpenClawPluginApi) { +/** Planning label — architect issues go directly here. */ +const PLANNING_LABEL = "Planning"; + +export function createResearchTaskTool(api: OpenClawPluginApi) { return (ctx: ToolContext) => ({ - name: "design_task", - label: "Design Task", - description: `Spawn an architect to investigate a design/architecture problem. Creates a "To Design" issue and dispatches an architect worker with persistent session. + name: "research_task", + label: "Research Task", + description: `Spawn an architect to research a design/architecture problem. Creates a Planning issue and dispatches an architect worker. + +IMPORTANT: Provide a detailed description with enough background context for the architect +to produce actionable, development-ready findings. Include: current state, constraints, +requirements, relevant code paths, and any prior decisions. The output should be detailed +enough for a developer to start implementation immediately. The architect will: -1. Investigate the problem systematically -2. Research alternatives (>= 3 options) -3. Produce structured findings with recommendation -4. Complete with work_finish, moving the issue to Planning +1. Research the problem systematically (codebase, docs, web) +2. Investigate >= 3 alternatives with tradeoffs +3. Produce a recommendation with implementation outline +4. Post findings as issue comments, then complete with work_finish Example: - design_task({ + research_task({ projectGroupId: "-5176490302", - title: "Design: Session persistence strategy", - description: "How should sessions be persisted across restarts?", + title: "Research: Session persistence strategy", + description: "Sessions are lost on restart. Current impl uses in-memory Map in session-store.ts. Constraints: must work with SQLite (already a dep), max 50ms latency on read. Prior discussion in #42 ruled out Redis.", + focusAreas: ["SQLite vs file-based", "migration path", "cache invalidation"], complexity: "complex" })`, parameters: { type: "object", - required: ["projectGroupId", "title"], + required: ["projectGroupId", "title", "description"], properties: { projectGroupId: { type: "string", @@ -47,11 +57,11 @@ Example: }, title: { type: "string", - description: "Design title (e.g., 'Design: Session persistence')", + description: "Research title (e.g., 'Research: Session persistence strategy')", }, description: { type: "string", - description: "What are we designing & why? Include context and constraints.", + description: "Detailed background context: what exists today, why this needs investigation, constraints, relevant code paths, prior decisions. Must be detailed enough for the architect to produce development-ready findings.", }, focusAreas: { type: "array", @@ -81,41 +91,28 @@ Example: if (!groupId) throw new Error("projectGroupId is required"); if (!title) throw new Error("title is required"); + if (!description) throw new Error("description is required — provide detailed background context for the architect"); const { project } = await resolveProject(workspaceDir, groupId); const { provider } = await resolveProvider(project); const pluginConfig = getPluginConfig(api); - - // Derive labels from workflow config - const workflow = await loadWorkflow(workspaceDir, project.name); const role = "architect"; - const queueLabels = getQueueLabels(workflow, role); - const queueLabel = queueLabels[0]; - if (!queueLabel) throw new Error(`No queue state found for role "${role}" in workflow`); - // Build issue body with focus areas - const bodyParts = [description]; + // Build issue body with rich context + const bodyParts = [ + "## Background", + "", + description, + ]; if (focusAreas.length > 0) { bodyParts.push("", "## Focus Areas", ...focusAreas.map(a => `- ${a}`)); } - bodyParts.push( - "", "---", - "", "## Architect Output Template", - "", - "When complete, the architect will produce findings covering:", - "1. **Problem Statement** — Why is this design decision important?", - "2. **Current State** — What exists today? Limitations?", - "3. **Alternatives** (>= 3 options with pros/cons and effort estimates)", - "4. **Recommendation** — Which option and why?", - "5. **Implementation Outline** — What dev tasks are needed?", - "6. **References** — Code, docs, prior art", - ); const issueBody = bodyParts.join("\n"); - // Create issue in queue state - const issue = await provider.createIssue(title, issueBody, queueLabel as StateLabel); + // Create issue directly in Planning state (no queue — tool-triggered only) + const issue = await provider.createIssue(title, issueBody, PLANNING_LABEL as StateLabel); - await auditLog(workspaceDir, "design_task", { + await auditLog(workspaceDir, "research_task", { project: project.name, groupId, issueId: issue.iid, title, complexity, focusAreas, dryRun, }); @@ -132,7 +129,7 @@ Example: return jsonResult({ success: true, dryRun: true, - issue: { id: issue.iid, title: issue.title, url: issue.web_url, label: queueLabel }, + issue: { id: issue.iid, title: issue.title, url: issue.web_url, label: PLANNING_LABEL }, design: { level, model, status: "dry_run" }, announcement: `📐 [DRY RUN] Would spawn ${role} (${level}) for #${issue.iid}: ${title}\n🔗 ${issue.web_url}`, }); @@ -141,22 +138,19 @@ Example: // Check worker availability const worker = getWorker(project, role); if (worker.active) { - // Issue created but can't dispatch yet — will be picked up by heartbeat return jsonResult({ success: true, - issue: { id: issue.iid, title: issue.title, url: issue.web_url, label: queueLabel }, + issue: { id: issue.iid, title: issue.title, url: issue.web_url, label: PLANNING_LABEL }, design: { level, status: "queued", - reason: `${role.toUpperCase()} already active on #${worker.issueId}. Issue queued for pickup.`, + reason: `${role.toUpperCase()} already active on #${worker.issueId}. Issue created in Planning — dispatch manually when architect is free.`, }, - announcement: `📐 Created design task #${issue.iid}: ${title} (queued — ${role} busy)\n🔗 ${issue.web_url}`, + announcement: `📐 Created research task #${issue.iid}: ${title} (architect busy — issue in Planning)\n🔗 ${issue.web_url}`, }); } - // Dispatch worker - const targetLabel = getActiveLabel(workflow, role); - + // Dispatch architect directly — issue stays in Planning (no state transition) const dr = await dispatchTask({ workspaceDir, agentId: ctx.agentId, @@ -168,8 +162,8 @@ Example: issueUrl: issue.web_url, role, level, - fromLabel: queueLabel, - toLabel: targetLabel, + fromLabel: PLANNING_LABEL, + toLabel: PLANNING_LABEL, transitionLabel: (id, from, to) => provider.transitionLabel(id, from as StateLabel, to as StateLabel), provider, pluginConfig, @@ -180,7 +174,7 @@ Example: return jsonResult({ success: true, - issue: { id: issue.iid, title: issue.title, url: issue.web_url, label: targetLabel }, + issue: { id: issue.iid, title: issue.title, url: issue.web_url, label: PLANNING_LABEL }, design: { sessionKey: dr.sessionKey, level: dr.level, diff --git a/lib/tools/task-update.test.ts b/lib/tools/task-update.test.ts index 4572158..979a13f 100644 --- a/lib/tools/task-update.test.ts +++ b/lib/tools/task-update.test.ts @@ -18,7 +18,7 @@ describe("task_update tool", () => { it("supports all state labels", () => { const labels = getStateLabels(DEFAULT_WORKFLOW); - assert.strictEqual(labels.length, 12); + assert.strictEqual(labels.length, 10); assert.ok(labels.includes("Planning")); assert.ok(labels.includes("Done")); assert.ok(labels.includes("To Review")); diff --git a/lib/tools/work-finish.ts b/lib/tools/work-finish.ts index 9686020..870c4ec 100644 --- a/lib/tools/work-finish.ts +++ b/lib/tools/work-finish.ts @@ -3,22 +3,27 @@ * * Delegates side-effects to pipeline service: label transition, state update, * issue close/reopen, notifications, and audit logging. + * + * Roles without workflow states (e.g. architect) are handled inline — + * deactivate worker, optionally transition label, and notify. */ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { jsonResult } from "openclaw/plugin-sdk"; import type { ToolContext } from "../types.js"; -import { getWorker, resolveRepoPath } from "../projects.js"; +import type { StateLabel } from "../providers/provider.js"; +import { deactivateWorker, getWorker, resolveRepoPath } from "../projects.js"; import { executeCompletion, getRule } from "../services/pipeline.js"; import { log as auditLog } from "../audit.js"; import { requireWorkspaceDir, resolveProject, resolveProvider, getPluginConfig } from "../tool-helpers.js"; import { getAllRoleIds, isValidResult, getCompletionResults } from "../roles/index.js"; -import { loadWorkflow } from "../workflow.js"; +import { loadWorkflow, hasWorkflowStates, getCompletionEmoji } from "../workflow.js"; +import { notify, getNotificationConfig } from "../notify.js"; export function createWorkFinishTool(api: OpenClawPluginApi) { return (ctx: ToolContext) => ({ name: "work_finish", label: "Work Finish", - description: `Complete a task: Developer done (PR created, goes to review) or blocked. Tester pass/fail/refine/blocked. Reviewer approve/reject/blocked. Handles label transition, state update, issue close/reopen, notifications, and audit logging.`, + description: `Complete a task: Developer done (PR created, goes to review) or blocked. Tester pass/fail/refine/blocked. Reviewer approve/reject/blocked. Architect done/blocked. Handles label transition, state update, issue close/reopen, notifications, and audit logging.`, parameters: { type: "object", required: ["role", "result", "projectGroupId"], @@ -44,8 +49,6 @@ export function createWorkFinishTool(api: OpenClawPluginApi) { const valid = getCompletionResults(role); throw new Error(`${role.toUpperCase()} cannot complete with "${result}". Valid results: ${valid.join(", ")}`); } - if (!getRule(role, result)) - throw new Error(`Invalid completion: ${role}:${result}`); // Resolve project + worker const { project } = await resolveProject(workspaceDir, groupId); @@ -56,13 +59,24 @@ export function createWorkFinishTool(api: OpenClawPluginApi) { if (!issueId) throw new Error(`No issueId for active ${role.toUpperCase()} on ${project.name}`); const { provider } = await resolveProvider(project); - const repoPath = resolveRepoPath(project.repo); - const issue = await provider.getIssue(issueId); - - const pluginConfig = getPluginConfig(api); const workflow = await loadWorkflow(workspaceDir, project.name); - // Execute completion (pipeline service handles notification with runtime) + // Roles without workflow states (e.g. architect) — handle inline + if (!hasWorkflowStates(workflow, role)) { + return handleStatelessCompletion({ + workspaceDir, groupId, role, result, issueId, summary, + provider, projectName: project.name, channel: project.channel, + pluginConfig: getPluginConfig(api), runtime: api.runtime, + }); + } + + // Standard pipeline completion for roles with workflow states + if (!getRule(role, result)) + throw new Error(`Invalid completion: ${role}:${result}`); + + const repoPath = resolveRepoPath(project.repo); + const pluginConfig = getPluginConfig(api); + const completion = await executeCompletion({ workspaceDir, groupId, role, result, issueId, summary, prUrl, provider, repoPath, projectName: project.name, @@ -77,7 +91,6 @@ export function createWorkFinishTool(api: OpenClawPluginApi) { ...completion, }; - // Audit await auditLog(workspaceDir, "work_finish", { project: project.name, groupId, issue: issueId, role, result, summary: summary ?? null, labelTransition: completion.labelTransition, @@ -87,3 +100,89 @@ export function createWorkFinishTool(api: OpenClawPluginApi) { }, }); } + +/** + * Handle completion for roles without workflow states (e.g. architect). + * + * - done: deactivate worker, issue stays in current state (Planning) + * - blocked: deactivate worker, transition issue to Refining + */ +async function handleStatelessCompletion(opts: { + workspaceDir: string; + groupId: string; + role: string; + result: string; + issueId: number; + summary?: string; + provider: import("../providers/provider.js").IssueProvider; + projectName: string; + channel?: string; + pluginConfig?: Record; + runtime?: import("openclaw/plugin-sdk").PluginRuntime; +}): Promise> { + const { + workspaceDir, groupId, role, result, issueId, summary, + provider, projectName, channel, pluginConfig, runtime, + } = opts; + + const issue = await provider.getIssue(issueId); + + // Deactivate worker + await deactivateWorker(workspaceDir, groupId, role); + + // If blocked, transition to Refining + let labelTransition = "none"; + if (result === "blocked") { + const currentLabel = provider.getCurrentStateLabel(issue) ?? "Planning"; + await provider.transitionLabel(issueId, currentLabel as StateLabel, "Refining" as StateLabel); + labelTransition = `${currentLabel} → Refining`; + } + + // Notification + const nextState = result === "blocked" ? "awaiting human decision" : "awaiting human decision"; + const notifyConfig = getNotificationConfig(pluginConfig); + notify( + { + type: "workerComplete", + project: projectName, + groupId, + issueId, + issueUrl: issue.web_url, + role, + result: result as "done" | "blocked", + summary, + nextState, + }, + { + workspaceDir, + config: notifyConfig, + groupId, + channel: channel ?? "telegram", + runtime, + }, + ).catch((err) => { + auditLog(workspaceDir, "pipeline_warning", { step: "notify", issue: issueId, role, error: (err as Error).message ?? String(err) }).catch(() => {}); + }); + + // Build announcement + const emoji = getCompletionEmoji(role, result); + const label = `${role} ${result}`.toUpperCase(); + let announcement = `${emoji} ${label} #${issueId}`; + if (summary) announcement += ` — ${summary}`; + announcement += `\n📋 Issue: ${issue.web_url}`; + if (result === "blocked") announcement += `\nawaiting human decision.`; + + // Audit + await auditLog(workspaceDir, "work_finish", { + project: projectName, groupId, issue: issueId, role, result, + summary: summary ?? null, labelTransition, + }); + + return jsonResult({ + success: true, project: projectName, groupId, issueId, role, result, + labelTransition, + announcement, + nextState, + issueUrl: issue.web_url, + }); +} diff --git a/lib/workflow.ts b/lib/workflow.ts index cda6e47..119a0b7 100644 --- a/lib/workflow.ts +++ b/lib/workflow.ts @@ -201,25 +201,6 @@ export const DEFAULT_WORKFLOW: WorkflowConfig = { on: { [WorkflowEvent.APPROVE]: "todo" }, }, - // ── Architect track ───────────────────────────────────────── - toDesign: { - type: StateType.QUEUE, - role: "architect", - label: "To Design", - color: "#0075ca", - priority: 1, - on: { [WorkflowEvent.PICKUP]: "designing" }, - }, - designing: { - type: StateType.ACTIVE, - role: "architect", - label: "Designing", - color: "#d4c5f9", - on: { - [WorkflowEvent.COMPLETE]: "planning", - [WorkflowEvent.BLOCKED]: "refining", - }, - }, }, }; @@ -437,6 +418,14 @@ export function findStateKeyByLabel(workflow: WorkflowConfig, label: string): st return Object.entries(workflow.states).find(([, s]) => s.label === label)?.[0] ?? null; } +/** + * Check if a role has any workflow states (queue, active, etc.). + * Roles without workflow states (e.g. architect) are dispatched by tool only. + */ +export function hasWorkflowStates(workflow: WorkflowConfig, role: Role): boolean { + return Object.values(workflow.states).some((s) => s.role === role); +} + // --------------------------------------------------------------------------- // Completion rules — derived from transitions // ---------------------------------------------------------------------------