Creates a single source of truth for all worker roles via lib/roles/.
## New: lib/roles/
- **registry.ts** — All role definitions (dev, qa, architect) with
levels, models, emoji, completion results, session key patterns
- **types.ts** — RoleConfig interface
- **selectors.ts** — Query helpers: getRole(), getLevelsForRole(),
resolveModel(), isValidResult(), roleForLevel(), etc.
- **index.ts** — Barrel exports
## Migrated Files
- **lib/tiers.ts** — Now delegates to registry (backward compat kept)
- **lib/dispatch.ts** — Uses registry for emoji resolution
- **lib/bootstrap-hook.ts** — Uses registry for session key pattern
- **lib/services/tick.ts** — Uses registry for level detection
- **lib/services/heartbeat.ts** — Uses registry for role iteration
- **lib/tools/health.ts** — Uses registry for role iteration
- **lib/tools/work-start.ts** — Uses registry for role enum
- **lib/tools/work-finish.ts** — Uses registry for result validation
- **lib/tools/project-register.ts** — Uses registry for level lists
## Key Benefits
- Adding a new role = add entry to registry.ts (single file)
- No more scattered role unions ("dev" | "qa" | "architect")
- Type-safe role/level/result validation from registry
- Session key pattern auto-generated from registry
- All 64 tests passing (22 new registry tests + 42 existing)
87 lines
3.8 KiB
TypeScript
87 lines
3.8 KiB
TypeScript
/**
|
|
* work_finish — Complete a task (DEV done, QA pass/fail/refine/blocked).
|
|
*
|
|
* Delegates side-effects to pipeline service: label transition, state update,
|
|
* issue close/reopen, notifications, and audit logging.
|
|
*/
|
|
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 { executeCompletion, getRule, NEXT_STATE } 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";
|
|
|
|
export function createWorkFinishTool(api: OpenClawPluginApi) {
|
|
return (ctx: ToolContext) => ({
|
|
name: "work_finish",
|
|
label: "Work Finish",
|
|
description: `Complete a task: DEV done/blocked, QA pass/fail/refine/blocked. Handles label transition, state update, issue close/reopen, notifications, and audit logging.`,
|
|
parameters: {
|
|
type: "object",
|
|
required: ["role", "result", "projectGroupId"],
|
|
properties: {
|
|
role: { type: "string", enum: getAllRoleIds(), description: "Worker role" },
|
|
result: { type: "string", enum: ["done", "pass", "fail", "refine", "blocked"], description: "Completion result" },
|
|
projectGroupId: { type: "string", description: "Project group ID" },
|
|
summary: { type: "string", description: "Brief summary" },
|
|
prUrl: { type: "string", description: "PR/MR URL (auto-detected if omitted)" },
|
|
},
|
|
},
|
|
|
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
const role = params.role as "dev" | "qa" | "architect";
|
|
const result = params.result as string;
|
|
const groupId = params.projectGroupId as string;
|
|
const summary = params.summary as string | undefined;
|
|
const prUrl = params.prUrl as string | undefined;
|
|
const workspaceDir = requireWorkspaceDir(ctx);
|
|
|
|
// Validate role:result using registry
|
|
if (!isValidResult(role, result)) {
|
|
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);
|
|
const worker = getWorker(project, role);
|
|
if (!worker.active) throw new Error(`${role.toUpperCase()} worker not active on ${project.name}`);
|
|
|
|
const issueId = worker.issueId ? Number(worker.issueId.split(",")[0]) : null;
|
|
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);
|
|
|
|
// Execute completion (pipeline service handles notification with runtime)
|
|
const completion = await executeCompletion({
|
|
workspaceDir, groupId, role, result, issueId, summary, prUrl, provider, repoPath,
|
|
projectName: project.name,
|
|
channel: project.channel,
|
|
pluginConfig,
|
|
runtime: api.runtime,
|
|
});
|
|
|
|
const output: Record<string, unknown> = {
|
|
success: true, project: project.name, groupId, issueId, role, result,
|
|
...completion,
|
|
};
|
|
|
|
// Audit
|
|
await auditLog(workspaceDir, "work_finish", {
|
|
project: project.name, groupId, issue: issueId, role, result,
|
|
summary: summary ?? null, labelTransition: completion.labelTransition,
|
|
});
|
|
|
|
return jsonResult(output);
|
|
},
|
|
});
|
|
}
|