Adds the Architect role for design/architecture investigations with persistent sessions and structured design proposals. ## New Features - **Architect role** with opus (complex) and sonnet (standard) levels - **design_task tool** — Creates To Design issues and dispatches architect - **Workflow states:** To Design → Designing → Planning - **Completion rules:** architect:done → Planning, architect:blocked → Refining - **Auto-level selection** based on complexity keywords ## Files Changed (22 files, 546 additions) ### New Files - lib/tools/design-task.ts — design_task tool implementation - lib/tools/design-task.test.ts — 16 tests for architect functionality ### Core Changes - lib/tiers.ts — ARCHITECT_LEVELS, WorkerRole type, models, emoji - lib/workflow.ts — toDesign/designing states, completion rules - lib/projects.ts — architect WorkerState on Project type - lib/dispatch.ts — architect role support in dispatch pipeline - lib/services/pipeline.ts — architect completion rules - lib/model-selector.ts — architect level selection heuristic ### Integration - index.ts — Register design_task tool, architect config schema - lib/notify.ts — architect role in notifications - lib/bootstrap-hook.ts — architect session key parsing - lib/services/tick.ts — architect in queue processing - lib/services/heartbeat.ts — architect in health checks - lib/tools/health.ts — architect in health scans - lib/tools/status.ts — architect in status dashboard - lib/tools/work-start.ts — architect role option - lib/tools/work-finish.ts — architect validation - lib/tools/project-register.ts — architect labels + role scaffolding - lib/templates.ts — architect instructions + AGENTS.md updates - lib/setup/workspace.ts — architect role file scaffolding - lib/setup/smart-model-selector.ts — architect in model assignment - lib/setup/llm-model-selector.ts — architect in LLM prompt
118 lines
4.0 KiB
TypeScript
118 lines
4.0 KiB
TypeScript
/**
|
|
* bootstrap-hook.ts — Agent bootstrap hook for injecting role instructions.
|
|
*
|
|
* Registers an `agent:bootstrap` hook that intercepts DevClaw worker session
|
|
* startup and injects role-specific instructions as a virtual workspace file.
|
|
*
|
|
* This eliminates the file-read-network-send pattern in dispatch.ts that
|
|
* triggered the security auditor's potential-exfiltration warning.
|
|
*/
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
|
|
/**
|
|
* Parse a DevClaw subagent session key to extract project name and role.
|
|
*
|
|
* Session key format: `agent:{agentId}:subagent:{projectName}-{role}-{level}`
|
|
* Examples:
|
|
* - `agent:devclaw:subagent:my-project-dev-medior` → { projectName: "my-project", role: "dev" }
|
|
* - `agent:devclaw:subagent:webapp-qa-reviewer` → { projectName: "webapp", role: "qa" }
|
|
*
|
|
* Note: projectName may contain hyphens, so we match role from the end.
|
|
*/
|
|
export function parseDevClawSessionKey(
|
|
sessionKey: string,
|
|
): { projectName: string; role: "dev" | "qa" | "architect" } | null {
|
|
// Match `:subagent:` prefix, then capture everything up to the last `-dev-`, `-qa-`, or `-architect-`
|
|
const match = sessionKey.match(/:subagent:(.+)-(dev|qa|architect)-[^-]+$/);
|
|
if (!match) return null;
|
|
return { projectName: match[1], role: match[2] as "dev" | "qa" | "architect" };
|
|
}
|
|
|
|
/**
|
|
* Load role-specific instructions from workspace.
|
|
* Tries project-specific file first, then falls back to default.
|
|
*
|
|
* This is the same logic previously in dispatch.ts loadRoleInstructions(),
|
|
* now called from the bootstrap hook instead of during dispatch.
|
|
*/
|
|
export async function loadRoleInstructions(
|
|
workspaceDir: string,
|
|
projectName: string,
|
|
role: "dev" | "qa" | "architect",
|
|
): Promise<string> {
|
|
const projectFile = path.join(workspaceDir, "projects", "roles", projectName, `${role}.md`);
|
|
try {
|
|
return await fs.readFile(projectFile, "utf-8");
|
|
} catch {
|
|
/* not found — try default */
|
|
}
|
|
const defaultFile = path.join(workspaceDir, "projects", "roles", "default", `${role}.md`);
|
|
try {
|
|
return await fs.readFile(defaultFile, "utf-8");
|
|
} catch {
|
|
/* not found */
|
|
}
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* Register the agent:bootstrap hook for DevClaw worker instruction injection.
|
|
*
|
|
* When a DevClaw worker session starts, this hook:
|
|
* 1. Detects it's a DevClaw subagent via session key pattern
|
|
* 2. Extracts project name and role
|
|
* 3. Loads role-specific instructions from workspace
|
|
* 4. Injects them as a virtual workspace file (WORKER_INSTRUCTIONS.md)
|
|
*
|
|
* OpenClaw automatically includes bootstrap files in the agent's system prompt,
|
|
* so workers receive their instructions without any file-read in dispatch.ts.
|
|
*/
|
|
export function registerBootstrapHook(api: OpenClawPluginApi): void {
|
|
api.registerHook("agent:bootstrap", async (event) => {
|
|
const sessionKey = event.sessionKey;
|
|
if (!sessionKey) return;
|
|
|
|
const parsed = parseDevClawSessionKey(sessionKey);
|
|
if (!parsed) return;
|
|
|
|
const context = event.context as {
|
|
workspaceDir?: string;
|
|
bootstrapFiles?: Array<{
|
|
name: string;
|
|
path: string;
|
|
content?: string;
|
|
missing: boolean;
|
|
}>;
|
|
};
|
|
|
|
const workspaceDir = context.workspaceDir;
|
|
if (!workspaceDir || typeof workspaceDir !== "string") return;
|
|
|
|
const bootstrapFiles = context.bootstrapFiles;
|
|
if (!Array.isArray(bootstrapFiles)) return;
|
|
|
|
const instructions = await loadRoleInstructions(
|
|
workspaceDir,
|
|
parsed.projectName,
|
|
parsed.role,
|
|
);
|
|
|
|
if (!instructions) return;
|
|
|
|
// Inject as a virtual bootstrap file. OpenClaw includes these in the
|
|
// agent's system prompt automatically (via buildBootstrapContextFiles).
|
|
bootstrapFiles.push({
|
|
name: "WORKER_INSTRUCTIONS.md" as any,
|
|
path: `<devclaw:${parsed.projectName}:${parsed.role}>`,
|
|
content: instructions.trim(),
|
|
missing: false,
|
|
});
|
|
|
|
api.logger.info(
|
|
`Bootstrap hook: injected ${parsed.role} instructions for project "${parsed.projectName}"`,
|
|
);
|
|
});
|
|
}
|