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 │
│ 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 │

View File

@@ -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"
]
}
}

View File

@@ -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.

View File

@@ -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

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/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/` |

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 | `"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

View File

@@ -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"] });

View File

@@ -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,
});

View File

@@ -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

View File

@@ -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.

View File

@@ -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"), "🚫");
});

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.
* 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,

View File

@@ -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"));

View File

@@ -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<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" },
},
// ── 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
// ---------------------------------------------------------------------------