feat(KB-218): add workflow steps for post-implementation review

- Add core data model for workflow step definitions with AI-assisted prompt refinement
- Create API routes for CRUD operations and prompt refinement via /api/workflow-steps
- Add WorkflowStepManager dashboard UI for defining and managing workflow steps
- Integrate workflow step selection into NewTaskModal for per-task enablement
- Execute workflow steps sequentially in executor after task_done() with readonly tools
- Run workflow step agents before moving tasks to in-review, failing on step errors
- Add comprehensive tests for store, API routes, components, and executor integration
This commit is contained in:
gsxdsm
2026-03-31 03:55:01 -07:00
parent 609c06f96e
commit 23964bf0c0
18 changed files with 2141 additions and 8 deletions

View File

@@ -389,6 +389,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
column: input.column || "triage",
dependencies: input.dependencies || [],
breakIntoSubtasks: input.breakIntoSubtasks === true ? true : undefined,
enabledWorkflowSteps: input.enabledWorkflowSteps?.length ? input.enabledWorkflowSteps : undefined,
modelPresetId: input.modelPresetId,
modelProvider: input.modelProvider,
modelId: input.modelId,
@@ -1948,6 +1949,125 @@ ${deps}
${stepsSection}`;
}
// ── Workflow Step CRUD Methods ─────────────────────────────────────
/**
* Create a new workflow step definition.
* Generates a unique ID (WS-001, WS-002, etc.) and stores in config.json.
*/
async createWorkflowStep(input: import("./types.js").WorkflowStepInput): Promise<import("./types.js").WorkflowStep> {
return this.withConfigLock(async () => {
const config = await this.readConfig();
const nextWsId = config.nextWorkflowStepId || 1;
const id = `WS-${String(nextWsId).padStart(3, "0")}`;
const now = new Date().toISOString();
const step: import("./types.js").WorkflowStep = {
id,
name: input.name,
description: input.description,
prompt: input.prompt || "",
enabled: input.enabled !== undefined ? input.enabled : true,
createdAt: now,
updatedAt: now,
};
if (!config.workflowSteps) {
config.workflowSteps = [];
}
config.workflowSteps.push(step);
config.nextWorkflowStepId = nextWsId + 1;
await this.writeConfig(config);
return step;
});
}
/**
* List all workflow step definitions from config.json.
*/
async listWorkflowSteps(): Promise<import("./types.js").WorkflowStep[]> {
const config = await this.readConfig();
return config.workflowSteps || [];
}
/**
* Get a single workflow step by ID.
*/
async getWorkflowStep(id: string): Promise<import("./types.js").WorkflowStep | undefined> {
const config = await this.readConfig();
return (config.workflowSteps || []).find((ws) => ws.id === id);
}
/**
* Update a workflow step definition.
* @throws Error if the workflow step is not found
*/
async updateWorkflowStep(id: string, updates: Partial<import("./types.js").WorkflowStepInput>): Promise<import("./types.js").WorkflowStep> {
return this.withConfigLock(async () => {
const config = await this.readConfig();
const steps = config.workflowSteps || [];
const index = steps.findIndex((ws) => ws.id === id);
if (index === -1) {
throw new Error(`Workflow step '${id}' not found`);
}
const step = steps[index];
if (updates.name !== undefined) step.name = updates.name;
if (updates.description !== undefined) step.description = updates.description;
if (updates.prompt !== undefined) step.prompt = updates.prompt;
if (updates.enabled !== undefined) step.enabled = updates.enabled;
step.updatedAt = new Date().toISOString();
config.workflowSteps = steps;
await this.writeConfig(config);
return step;
});
}
/**
* Delete a workflow step definition.
* Also removes the ID from any tasks that reference it in enabledWorkflowSteps.
* @throws Error if the workflow step is not found
*/
async deleteWorkflowStep(id: string): Promise<void> {
await this.withConfigLock(async () => {
const config = await this.readConfig();
const steps = config.workflowSteps || [];
const index = steps.findIndex((ws) => ws.id === id);
if (index === -1) {
throw new Error(`Workflow step '${id}' not found`);
}
steps.splice(index, 1);
config.workflowSteps = steps;
await this.writeConfig(config);
});
// Clean up references from existing tasks (best-effort, outside config lock)
try {
const tasks = await this.listTasks();
for (const task of tasks) {
if (task.enabledWorkflowSteps?.includes(id)) {
const updated = task.enabledWorkflowSteps.filter((wsId) => wsId !== id);
// Direct task.json mutation for enabledWorkflowSteps cleanup
await this.withTaskLock(task.id, async () => {
const dir = this.taskDir(task.id);
const t = await this.readTaskJson(dir);
t.enabledWorkflowSteps = updated.length > 0 ? updated : undefined;
t.updatedAt = new Date().toISOString();
await this.atomicWriteTaskJson(dir, t);
});
}
}
} catch {
// Best-effort: task cleanup is non-critical
}
}
getRootDir(): string {
return this.rootDir;
}