feat: replace .DONE file with task_done tool

- task_done() marks all remaining steps as done and signals completion
- No more filesystem side channel for completion detection
- Removes all .DONE references from executor and prompts
This commit is contained in:
Dustin Byrne
2026-03-25 21:51:34 -04:00
parent b5da595f3b
commit a6d18e1b53

View File

@@ -114,8 +114,7 @@ model, read-only access) to independently assess your work.
## Completion
After all steps are done, tests pass, and docs are updated:
\`\`\`bash
echo "done" > .DONE
\`\`\``;
Call \`task_done()\` to signal completion.`;
export interface TaskExecutorOptions {
semaphore?: AgentSemaphore;
@@ -210,10 +209,12 @@ export class TaskExecutor {
}
// Build custom tools for the worker
let taskDone = false;
const customTools = [
this.createTaskUpdateTool(task.id),
this.createTaskLogTool(task.id),
this.createTaskCreateTool(),
this.createTaskDoneTool(task.id, () => { taskDone = true; }),
this.createReviewStepTool(task.id, worktreePath, detail.prompt),
];
@@ -231,16 +232,14 @@ export class TaskExecutor {
const agentPrompt = buildExecutionPrompt(detail);
await session.prompt(agentPrompt);
const doneCwd = join(worktreePath, ".DONE");
if (existsSync(doneCwd)) {
await this.store.logEntry(task.id, "Execution complete — .DONE created");
if (taskDone) {
await this.store.moveTask(task.id, "in-review");
console.log(`[executor] ✓ ${task.id} completed → in-review`);
this.options.onComplete?.(task);
} else {
await this.store.logEntry(task.id, "Agent finished without .DONE — moved to in-review for inspection");
await this.store.logEntry(task.id, "Agent finished without calling task_done — moved to in-review for inspection");
await this.store.moveTask(task.id, "in-review");
console.log(`[executor] ⚠ ${task.id} agent finished without .DONE → in-review`);
console.log(`[executor] ⚠ ${task.id} finished without task_done → in-review`);
this.options.onComplete?.(task);
}
} finally {
@@ -337,6 +336,34 @@ export class TaskExecutor {
};
}
private createTaskDoneTool(taskId: string, onDone: () => void): ToolDefinition {
const store = this.store;
return {
name: "task_done",
label: "Mark Task Done",
description:
"Signal that all steps are complete, tests pass, and documentation is updated. " +
"Call this as the final action after finishing all work. " +
"Automatically marks all remaining steps as done.",
parameters: Type.Object({}),
execute: async () => {
onDone();
// Mark all pending/in-progress steps as done
const task = await store.getTask(taskId);
for (let i = 0; i < task.steps.length; i++) {
if (task.steps[i].status !== "done" && task.steps[i].status !== "skipped") {
await store.updateStep(taskId, i, "done");
}
}
await store.logEntry(taskId, "Task marked done by agent");
return {
content: [{ type: "text" as const, text: "Task marked complete. All steps done. Moving to in-review." }],
details: {},
};
},
};
}
private createReviewStepTool(
taskId: string,
worktreePath: string,
@@ -491,5 +518,5 @@ Use \`task_update\` to report progress on every step transition.
Use \`task_log\` for important actions and decisions.
Use \`task_create\` if you find out-of-scope work that needs doing.
Commit at step boundaries: \`git commit -m "feat(${task.id}): complete Step N — description"\`
When done: \`echo "done" > .DONE\``;
When all steps are complete: call \`task_done()\``;
}