feat: rename design_task to research_task and update related documentation
This commit is contained in:
@@ -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 │
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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/` |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
4
index.ts
4
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"] });
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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"), "🚫");
|
||||
});
|
||||
@@ -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,
|
||||
@@ -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"));
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user