feat: rename design_task to research_task and update related documentation

This commit is contained in:
Lauren ten Hoor
2026-02-16 18:47:01 +08:00
parent d87b9f68a2
commit b57ede0863
15 changed files with 249 additions and 173 deletions

View File

@@ -583,7 +583,7 @@ Every piece of data and where it lives:
│ status → read labels + read state │ │ status → read labels + read state │
│ health → check sessions + fix zombies │ │ health → check sessions + fix zombies │
│ project_register → labels + prompts + state init (one-time) │ │ project_register → labels + prompts + state init (one-time) │
design_task → architect dispatch │ research_task → architect dispatch │
│ │ │ │
│ Bootstrap hook → injects role instructions into worker sessions│ │ Bootstrap hook → injects role instructions into worker sessions│
│ Review pass → polls PR status, auto-merges approved PRs │ │ Review pass → polls PR status, auto-merges approved PRs │

View File

@@ -164,24 +164,10 @@ workflow:
type: terminal type: terminal
label: Done label: Done
color: "#5cb85c" 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:** **State types:**
| Type | Description | | Type | Description |
@@ -340,7 +326,7 @@ Restrict DevClaw tools to your orchestrator agent:
"project_register", "project_register",
"setup", "setup",
"onboard", "onboard",
"design_task" "research_task"
] ]
} }
} }

View File

@@ -53,7 +53,7 @@ Planning → To Do → Doing → To Test → Testing → Done
↘ Refining → (human decision) ↘ 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. 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.

View File

@@ -17,7 +17,7 @@ Planning → To Do → Doing → To Test → Testing → Done
↘ In Review → (PR approved → auto-merge) → To Test ↘ In Review → (PR approved → auto-merge) → To Test
↘ To Improve → Doing (merge conflict / fix cycle) ↘ To Improve → Doing (merge conflict / fix cycle)
↘ Refining → (human decision) ↘ 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`). 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 ### 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 ### Workspace Layout Migration

View File

@@ -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/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/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/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/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/` | | [lib/setup/migrate-layout.test.ts](../lib/setup/migrate-layout.test.ts) | Workspace layout migration: `projects/``devclaw/` |

View File

@@ -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 | `"fail"` | Testing → To Improve | Issue reopened |
| tester | `"refine"` | Testing → Refining | Awaits human decision | | tester | `"refine"` | Testing → Refining | Awaits human decision |
| tester | `"blocked"` | Testing → Refining | Awaits human decision | | tester | `"blocked"` | Testing → Refining | Awaits human decision |
| architect | `"done"` | Designing → Planning | Design complete | | architect | `"done"` | stays in Planning | Design complete, ready for human review |
| architect | `"blocked"` | Designing → Refining | Awaits human decision | | architect | `"blocked"` | Planning → Refining | Awaits human decision |
**What it does atomically:** **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 | | `state` | StateLabel | Yes | New state label |
| `reason` | string | No | Audit log reason for the change | | `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:** **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:** **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 | | `projectGroupId` | string | Yes | Project group ID |
| `title` | string | Yes | Design task title | | `title` | string | Yes | Design task title |
| `description` | string | No | Design problem description | | `description` | string | Yes | Detailed background context for the architect |
| `level` | `"junior"` \| `"senior"` | No | Architect level. Default: `"junior"`. | | `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:fail → Testing → To Improve (reopen issue)
tester:refine → Testing → Refining (awaits human decision) tester:refine → Testing → Refining (awaits human decision)
tester:blocked → Testing → Refining (awaits human decision) tester:blocked → Testing → Refining (awaits human decision)
architect:done → Designing → Planning (design complete) architect:done → stays in Planning (design complete, ready for human review)
architect:blocked → Designing → Refining (awaits human decision) architect:blocked → Planning → Refining (awaits human decision)
``` ```
## Issue Priority Order ## Issue Priority Order

View File

@@ -10,7 +10,7 @@ import { createProjectRegisterTool } from "./lib/tools/project-register.js";
import { createSetupTool } from "./lib/tools/setup.js"; import { createSetupTool } from "./lib/tools/setup.js";
import { createOnboardTool } from "./lib/tools/onboard.js"; import { createOnboardTool } from "./lib/tools/onboard.js";
import { createAutoConfigureModelsTool } from "./lib/tools/autoconfigure-models.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 { registerCli } from "./lib/cli.js";
import { registerHeartbeatService } from "./lib/services/heartbeat.js"; import { registerHeartbeatService } from "./lib/services/heartbeat.js";
import { registerBootstrapHook } from "./lib/bootstrap-hook.js"; import { registerBootstrapHook } from "./lib/bootstrap-hook.js";
@@ -78,7 +78,7 @@ const plugin = {
api.registerTool(createTaskCommentTool(api), { names: ["task_comment"] }); api.registerTool(createTaskCommentTool(api), { names: ["task_comment"] });
// Architect // Architect
api.registerTool(createDesignTaskTool(api), { names: ["design_task"] }); api.registerTool(createResearchTaskTool(api), { names: ["research_task"] });
// Operations // Operations
api.registerTool(createStatusTool(api), { names: ["status"] }); api.registerTool(createStatusTool(api), { names: ["status"] });

View File

@@ -213,7 +213,7 @@ describe("E2E bootstrap — hook injection", () => {
it("should resolve architect instructions with project override", async () => { it("should resolve architect instructions with project override", async () => {
h = await createTestHarness({ projectName: "arch-proj" }); 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", "# Default Architect\nGeneral design guidelines.");
await h.writePrompt("architect", "# Arch Proj Architect\nUse event-driven architecture.", "arch-proj"); 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", issueUrl: "https://example.com/issues/6",
role: "architect", role: "architect",
level: "senior", level: "senior",
fromLabel: "To Design", fromLabel: "Planning",
toLabel: "Designing", toLabel: "Planning",
transitionLabel: (id, from, to) => h.provider.transitionLabel(id, from, to), transitionLabel: (id, from, to) => h.provider.transitionLabel(id, from, to),
provider: h.provider, provider: h.provider,
}); });

View File

@@ -28,6 +28,7 @@ import {
DEFAULT_WORKFLOW, DEFAULT_WORKFLOW,
getActiveLabel, getActiveLabel,
getRevertLabel, getRevertLabel,
hasWorkflowStates,
type WorkflowConfig, type WorkflowConfig,
type Role, type Role,
} from "../workflow.js"; } from "../workflow.js";
@@ -161,6 +162,10 @@ export async function checkWorkerHealth(opts: {
} = opts; } = opts;
const fixes: HealthFix[] = []; 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 worker = getWorker(project, role);
const sessionKey = worker.level ? getSessionForLevel(worker, worker.level) : null; const sessionKey = worker.level ? getSessionForLevel(worker, worker.level) : null;
@@ -430,6 +435,10 @@ export async function scanOrphanedLabels(opts: {
} = opts; } = opts;
const fixes: HealthFix[] = []; 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 worker = getWorker(project, role);
// Get labels from workflow config // Get labels from workflow config

View File

@@ -48,43 +48,42 @@ export const DEFAULT_QA_INSTRUCTIONS = `# TESTER Worker Instructions
export const DEFAULT_ARCHITECT_INSTRUCTIONS = `# Architect 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 ## Your Job
Investigate the design problem thoroughly: 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, comments, and codebase
2. **Research alternatives** — Explore >= 3 viable approaches 1. **Understand the problem** — Read the issue body carefully. It contains the background context, constraints, and focus areas.
3. **Evaluate tradeoffs** — Consider simplicity, performance, maintainability, architecture fit 2. **Research thoroughly** — Explore the codebase, read docs, search the web. Understand the current state deeply.
4. **Recommend** — Pick the best option with clear reasoning 3. **Investigate alternatives** — Research >= 3 viable approaches with concrete pros/cons and effort estimates.
5. **Outline implementation** — Break down into developer tasks 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 ## Output Format
Structure your findings as: Post your findings as issue comments. Structure them as:
### Problem Statement ### Problem Statement
Why is this design decision important? Why is this design decision important? What breaks if we get it wrong?
### Current State ### Current State
What exists today? Current limitations? What exists today? Current limitations? Relevant code paths.
### Alternatives Investigated ### Alternatives Investigated
**Option A: [Name]** **Option A: [Name]**
- Approach: [Concrete description of what this looks like]
- Pros: ... - Pros: ...
- Cons: ... - Cons: ...
- Effort estimate: X hours - Effort estimate: X hours
- Key code paths affected: [files/modules]
**Option B: [Name]** **Option B: [Name]**
- Pros: ... (same structure)
- Cons: ...
- Effort estimate: X hours
**Option C: [Name]** **Option C: [Name]**
- Pros: ... (same structure)
- Cons: ...
- Effort estimate: X hours
### Recommendation ### Recommendation
**Option X** is recommended because: **Option X** is recommended because:
@@ -93,24 +92,26 @@ What exists today? Current limitations?
- [Long-term implications] - [Long-term implications]
### Implementation Outline ### 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 2: [Description]
- [ ] Task 3: [Description] - [ ] Task 3: [Description]
### References ### References
- [Code examples, prior art, related issues] - [Code paths, docs, prior art, related issues]
## Available Tools ## Important
- web_search, web_fetch (research patterns) - **Be thorough** — Your output becomes the spec for development. Missing detail = blocked developer.
- Read files (explore codebase) - **If you need user input** — Call work_finish with result "blocked" and explain what you need. Do NOT guess on ambiguous requirements.
- exec (run commands, search code) - **Post findings as issue comments** — Use task_comment to write your analysis on the issue.
## Completion ## Completion
When done, call work_finish with: When done, call work_finish with:
- role: "architect" - 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 - summary: Brief summary of your recommendation
Your session is persistent — you may be called back for refinements. 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 | | \`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_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 | | \`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 ### First Thing on Session Start
@@ -277,7 +278,7 @@ Planning → To Do → Doing → To Review ──┬── [agent] → Reviewing
To Improve → Doing (fix cycle) To Improve → Doing (fix cycle)
Refining (human decision) 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): 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 "fail" → "To Improve" → scheduler dispatches Developer
- Tester "pass" → Done, no further dispatch - Tester "pass" → Done, no further dispatch
- Tester "refine" / blocked → needs human input - 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. **Always include issue URLs** in your response — these are in the \`announcement\` fields.

View File

@@ -1,6 +1,6 @@
/** /**
* Tests for architect role, design_task tool, and workflow integration. * Tests for architect role, research_task tool, and workflow integration.
* Run with: npx tsx --test lib/tools/design-task.test.ts * Run with: npx tsx --test lib/tools/research-task.test.ts
*/ */
import { describe, it } from "node:test"; import { describe, it } from "node:test";
import assert from "node:assert"; 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 { isLevelForRole, roleForLevel, resolveModel, getDefaultModel, getEmoji } from "../roles/index.js";
import { selectLevel } from "../model-selector.js"; import { selectLevel } from "../model-selector.js";
import { import {
DEFAULT_WORKFLOW, getQueueLabels, getActiveLabel, getCompletionRule, DEFAULT_WORKFLOW, getQueueLabels, getCompletionRule,
getCompletionEmoji, detectRoleFromLabel, getStateLabels, getCompletionEmoji, getStateLabels, hasWorkflowStates,
} from "../workflow.js"; } from "../workflow.js";
describe("architect tiers", () => { describe("architect tiers", () => {
@@ -42,42 +42,38 @@ describe("architect tiers", () => {
}); });
}); });
describe("architect workflow states", () => { describe("architect workflow — no dedicated states", () => {
it("should include To Design and Designing in state labels", () => { it("should NOT have To Design or Designing in state labels", () => {
const labels = getStateLabels(DEFAULT_WORKFLOW); const labels = getStateLabels(DEFAULT_WORKFLOW);
assert.ok(labels.includes("To Design")); assert.ok(!labels.includes("To Design"), "To Design should not exist");
assert.ok(labels.includes("Designing")); 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"); const queues = getQueueLabels(DEFAULT_WORKFLOW, "architect");
assert.deepStrictEqual(queues, ["To Design"]); assert.deepStrictEqual(queues, []);
}); });
it("should have Designing as architect active label", () => { it("should report architect has no workflow states", () => {
assert.strictEqual(getActiveLabel(DEFAULT_WORKFLOW, "architect"), "Designing"); assert.strictEqual(hasWorkflowStates(DEFAULT_WORKFLOW, "architect"), false);
}); });
it("should detect architect role from To Design label", () => { it("should report developer has workflow states", () => {
assert.strictEqual(detectRoleFromLabel(DEFAULT_WORKFLOW, "To Design"), "architect"); assert.strictEqual(hasWorkflowStates(DEFAULT_WORKFLOW, "developer"), true);
}); });
it("should have architect:done completion rule", () => { it("should report tester has workflow states", () => {
const rule = getCompletionRule(DEFAULT_WORKFLOW, "architect", "done"); assert.strictEqual(hasWorkflowStates(DEFAULT_WORKFLOW, "tester"), true);
assert.ok(rule);
assert.strictEqual(rule!.from, "Designing");
assert.strictEqual(rule!.to, "Planning");
}); });
it("should have architect:blocked completion rule", () => { it("should have no completion rules for architect (no active state)", () => {
const rule = getCompletionRule(DEFAULT_WORKFLOW, "architect", "blocked"); const doneRule = getCompletionRule(DEFAULT_WORKFLOW, "architect", "done");
assert.ok(rule); assert.strictEqual(doneRule, null);
assert.strictEqual(rule!.from, "Designing"); const blockedRule = getCompletionRule(DEFAULT_WORKFLOW, "architect", "blocked");
assert.strictEqual(rule!.to, "Refining"); assert.strictEqual(blockedRule, null);
}); });
it("should have completion emoji by result type", () => { it("should still have completion emoji for architect results", () => {
// Emoji is now keyed by result, not role:result
assert.strictEqual(getCompletionEmoji("architect", "done"), "✅"); assert.strictEqual(getCompletionEmoji("architect", "done"), "✅");
assert.strictEqual(getCompletionEmoji("architect", "blocked"), "🚫"); assert.strictEqual(getCompletionEmoji("architect", "blocked"), "🚫");
}); });

View File

@@ -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. * Creates a Planning issue with rich context and dispatches an architect worker.
* The architect investigates systematically, then produces structured findings * The architect researches the problem and produces detailed findings as issue comments.
* as a GitHub issue in Planning state. * 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 type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { jsonResult } 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 { dispatchTask } from "../dispatch.js";
import { log as auditLog } from "../audit.js"; import { log as auditLog } from "../audit.js";
import { requireWorkspaceDir, resolveProject, resolveProvider, getPluginConfig } from "../tool-helpers.js"; import { requireWorkspaceDir, resolveProject, resolveProvider, getPluginConfig } from "../tool-helpers.js";
import { loadWorkflow, getActiveLabel, getQueueLabels } from "../workflow.js";
import { loadConfig } from "../config/index.js"; import { loadConfig } from "../config/index.js";
import { selectLevel } from "../model-selector.js"; import { selectLevel } from "../model-selector.js";
import { resolveModel } from "../roles/index.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) => ({ return (ctx: ToolContext) => ({
name: "design_task", name: "research_task",
label: "Design Task", label: "Research Task",
description: `Spawn an architect to investigate a design/architecture problem. Creates a "To Design" issue and dispatches an architect worker with persistent session. 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: The architect will:
1. Investigate the problem systematically 1. Research the problem systematically (codebase, docs, web)
2. Research alternatives (>= 3 options) 2. Investigate >= 3 alternatives with tradeoffs
3. Produce structured findings with recommendation 3. Produce a recommendation with implementation outline
4. Complete with work_finish, moving the issue to Planning 4. Post findings as issue comments, then complete with work_finish
Example: Example:
design_task({ research_task({
projectGroupId: "-5176490302", projectGroupId: "-5176490302",
title: "Design: Session persistence strategy", title: "Research: Session persistence strategy",
description: "How should sessions be persisted across restarts?", 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" complexity: "complex"
})`, })`,
parameters: { parameters: {
type: "object", type: "object",
required: ["projectGroupId", "title"], required: ["projectGroupId", "title", "description"],
properties: { properties: {
projectGroupId: { projectGroupId: {
type: "string", type: "string",
@@ -47,11 +57,11 @@ Example:
}, },
title: { title: {
type: "string", type: "string",
description: "Design title (e.g., 'Design: Session persistence')", description: "Research title (e.g., 'Research: Session persistence strategy')",
}, },
description: { description: {
type: "string", 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: { focusAreas: {
type: "array", type: "array",
@@ -81,41 +91,28 @@ Example:
if (!groupId) throw new Error("projectGroupId is required"); if (!groupId) throw new Error("projectGroupId is required");
if (!title) throw new Error("title 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 { project } = await resolveProject(workspaceDir, groupId);
const { provider } = await resolveProvider(project); const { provider } = await resolveProvider(project);
const pluginConfig = getPluginConfig(api); const pluginConfig = getPluginConfig(api);
// Derive labels from workflow config
const workflow = await loadWorkflow(workspaceDir, project.name);
const role = "architect"; 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 // Build issue body with rich context
const bodyParts = [description]; const bodyParts = [
"## Background",
"",
description,
];
if (focusAreas.length > 0) { if (focusAreas.length > 0) {
bodyParts.push("", "## Focus Areas", ...focusAreas.map(a => `- ${a}`)); 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"); const issueBody = bodyParts.join("\n");
// Create issue in queue state // Create issue directly in Planning state (no queue — tool-triggered only)
const issue = await provider.createIssue(title, issueBody, queueLabel as StateLabel); 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, project: project.name, groupId, issueId: issue.iid,
title, complexity, focusAreas, dryRun, title, complexity, focusAreas, dryRun,
}); });
@@ -132,7 +129,7 @@ Example:
return jsonResult({ return jsonResult({
success: true, success: true,
dryRun: 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" }, design: { level, model, status: "dry_run" },
announcement: `📐 [DRY RUN] Would spawn ${role} (${level}) for #${issue.iid}: ${title}\n🔗 ${issue.web_url}`, announcement: `📐 [DRY RUN] Would spawn ${role} (${level}) for #${issue.iid}: ${title}\n🔗 ${issue.web_url}`,
}); });
@@ -141,22 +138,19 @@ Example:
// Check worker availability // Check worker availability
const worker = getWorker(project, role); const worker = getWorker(project, role);
if (worker.active) { if (worker.active) {
// Issue created but can't dispatch yet — will be picked up by heartbeat
return jsonResult({ return jsonResult({
success: true, 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: { design: {
level, level,
status: "queued", 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 // Dispatch architect directly — issue stays in Planning (no state transition)
const targetLabel = getActiveLabel(workflow, role);
const dr = await dispatchTask({ const dr = await dispatchTask({
workspaceDir, workspaceDir,
agentId: ctx.agentId, agentId: ctx.agentId,
@@ -168,8 +162,8 @@ Example:
issueUrl: issue.web_url, issueUrl: issue.web_url,
role, role,
level, level,
fromLabel: queueLabel, fromLabel: PLANNING_LABEL,
toLabel: targetLabel, toLabel: PLANNING_LABEL,
transitionLabel: (id, from, to) => provider.transitionLabel(id, from as StateLabel, to as StateLabel), transitionLabel: (id, from, to) => provider.transitionLabel(id, from as StateLabel, to as StateLabel),
provider, provider,
pluginConfig, pluginConfig,
@@ -180,7 +174,7 @@ Example:
return jsonResult({ return jsonResult({
success: true, 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: { design: {
sessionKey: dr.sessionKey, sessionKey: dr.sessionKey,
level: dr.level, level: dr.level,

View File

@@ -18,7 +18,7 @@ describe("task_update tool", () => {
it("supports all state labels", () => { it("supports all state labels", () => {
const labels = getStateLabels(DEFAULT_WORKFLOW); const labels = getStateLabels(DEFAULT_WORKFLOW);
assert.strictEqual(labels.length, 12); assert.strictEqual(labels.length, 10);
assert.ok(labels.includes("Planning")); assert.ok(labels.includes("Planning"));
assert.ok(labels.includes("Done")); assert.ok(labels.includes("Done"));
assert.ok(labels.includes("To Review")); assert.ok(labels.includes("To Review"));

View File

@@ -3,22 +3,27 @@
* *
* Delegates side-effects to pipeline service: label transition, state update, * Delegates side-effects to pipeline service: label transition, state update,
* issue close/reopen, notifications, and audit logging. * 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 type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { jsonResult } from "openclaw/plugin-sdk"; import { jsonResult } from "openclaw/plugin-sdk";
import type { ToolContext } from "../types.js"; 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 { executeCompletion, getRule } from "../services/pipeline.js";
import { log as auditLog } from "../audit.js"; import { log as auditLog } from "../audit.js";
import { requireWorkspaceDir, resolveProject, resolveProvider, getPluginConfig } from "../tool-helpers.js"; import { requireWorkspaceDir, resolveProject, resolveProvider, getPluginConfig } from "../tool-helpers.js";
import { getAllRoleIds, isValidResult, getCompletionResults } from "../roles/index.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) { export function createWorkFinishTool(api: OpenClawPluginApi) {
return (ctx: ToolContext) => ({ return (ctx: ToolContext) => ({
name: "work_finish", name: "work_finish",
label: "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: { parameters: {
type: "object", type: "object",
required: ["role", "result", "projectGroupId"], required: ["role", "result", "projectGroupId"],
@@ -44,8 +49,6 @@ export function createWorkFinishTool(api: OpenClawPluginApi) {
const valid = getCompletionResults(role); const valid = getCompletionResults(role);
throw new Error(`${role.toUpperCase()} cannot complete with "${result}". Valid results: ${valid.join(", ")}`); 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 // Resolve project + worker
const { project } = await resolveProject(workspaceDir, groupId); 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}`); if (!issueId) throw new Error(`No issueId for active ${role.toUpperCase()} on ${project.name}`);
const { provider } = await resolveProvider(project); 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); 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({ const completion = await executeCompletion({
workspaceDir, groupId, role, result, issueId, summary, prUrl, provider, repoPath, workspaceDir, groupId, role, result, issueId, summary, prUrl, provider, repoPath,
projectName: project.name, projectName: project.name,
@@ -77,7 +91,6 @@ export function createWorkFinishTool(api: OpenClawPluginApi) {
...completion, ...completion,
}; };
// Audit
await auditLog(workspaceDir, "work_finish", { await auditLog(workspaceDir, "work_finish", {
project: project.name, groupId, issue: issueId, role, result, project: project.name, groupId, issue: issueId, role, result,
summary: summary ?? null, labelTransition: completion.labelTransition, 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<string, unknown>;
runtime?: import("openclaw/plugin-sdk").PluginRuntime;
}): Promise<ReturnType<typeof jsonResult>> {
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,
});
}

View File

@@ -201,25 +201,6 @@ export const DEFAULT_WORKFLOW: WorkflowConfig = {
on: { [WorkflowEvent.APPROVE]: "todo" }, 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; 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 // Completion rules — derived from transitions
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------