feat(KB-142): add confirm gate and abort flow to task_add_dep tool

- Add confirm parameter to task_add_dep requiring explicit opt-in before destructive action
- On confirm, abort execution, discard worktree/branch, and move task to triage
- Add triage as valid transition from in-progress for dependency-triggered re-specification
- Auto-move todo tasks to triage when dependencies are added
- Update EXECUTOR_SYSTEM_PROMPT with task_add_dep confirm behavior documentation
This commit is contained in:
Dustin Byrne
2026-03-28 00:41:01 -04:00
parent 43aada5ebf
commit e0f33fb32c
5 changed files with 550 additions and 4 deletions

View File

@@ -401,6 +401,66 @@ describe("TaskStore", () => {
});
});
describe("updateTask — auto-move todo to triage on new deps", () => {
it("moves a todo task to triage when a new dependency is added", async () => {
const task = await store.createTask({ description: "Todo task", column: "todo" });
expect(task.column).toBe("todo");
const updated = await store.updateTask(task.id, { dependencies: ["KB-999"] });
expect(updated.column).toBe("triage");
expect(updated.status).toBeUndefined();
// Verify log entry
expect(updated.log.some((l: any) => l.action.includes("Moved to triage for re-specification"))).toBe(true);
// Verify persistence
const fetched = await store.getTask(task.id);
expect(fetched.column).toBe("triage");
});
it("emits task:moved event with { from: 'todo', to: 'triage' }", async () => {
const task = await store.createTask({ description: "Todo task", column: "todo" });
const events: any[] = [];
store.on("task:moved", (data: any) => events.push(data));
await store.updateTask(task.id, { dependencies: ["KB-999"] });
expect(events).toHaveLength(1);
expect(events[0].from).toBe("todo");
expect(events[0].to).toBe("triage");
});
it("does NOT move when dependencies are removed from a todo task", async () => {
const task = await store.createTask({ description: "Todo task", column: "todo", dependencies: ["KB-001"] });
const updated = await store.updateTask(task.id, { dependencies: [] });
expect(updated.column).toBe("todo");
});
it("does NOT move when dependencies are replaced with same set", async () => {
const task = await store.createTask({ description: "Todo task", column: "todo", dependencies: ["KB-001"] });
const updated = await store.updateTask(task.id, { dependencies: ["KB-001"] });
expect(updated.column).toBe("todo");
});
it("does NOT move a triage task when dependencies are added", async () => {
const task = await store.createTask({ description: "Triage task" });
expect(task.column).toBe("triage");
const updated = await store.updateTask(task.id, { dependencies: ["KB-999"] });
expect(updated.column).toBe("triage");
});
it("does NOT move an in-progress task when dependencies are added (handled by executor)", async () => {
const task = await store.createTask({ description: "IP task", column: "todo" });
await store.moveTask(task.id, "in-progress");
const updated = await store.updateTask(task.id, { dependencies: ["KB-999"] });
expect(updated.column).toBe("in-progress");
});
});
describe("updateTask — blockedBy", () => {
it("sets blockedBy to a string value", async () => {
const task = await store.createTask({ title: "Blocked task", description: "A task" });
@@ -866,6 +926,17 @@ describe("TaskStore", () => {
});
});
describe("moveTask — in-progress to triage", () => {
it("allows moving an in-progress task to triage", async () => {
const task = await store.createTask({ description: "test in-progress to triage" });
await store.moveTask(task.id, "todo");
await store.moveTask(task.id, "in-progress");
const moved = await store.moveTask(task.id, "triage");
expect(moved.column).toBe("triage");
});
});
describe("columnMovedAt", () => {
it("createTask sets columnMovedAt", async () => {
const before = new Date().toISOString();

View File

@@ -296,7 +296,25 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
if (updates.title !== undefined) task.title = updates.title;
if (updates.description !== undefined) task.description = updates.description;
if (updates.worktree !== undefined) task.worktree = updates.worktree;
if (updates.dependencies !== undefined) task.dependencies = updates.dependencies;
// Detect new dependencies being added to a todo task → auto-move to triage
let movedToTriage = false;
if (updates.dependencies !== undefined) {
const oldDeps = new Set(task.dependencies);
const hasNewDeps = updates.dependencies.some((d) => !oldDeps.has(d));
task.dependencies = updates.dependencies;
if (hasNewDeps && task.column === "todo") {
const fromColumn = task.column;
task.column = "triage";
task.status = undefined;
task.columnMovedAt = new Date().toISOString();
task.log.push({
timestamp: new Date().toISOString(),
action: "Moved to triage for re-specification — new dependency added",
});
movedToTriage = true;
}
}
if (updates.status === null) {
task.status = undefined;
} else if (updates.status !== undefined) {
@@ -322,6 +340,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
await writeFile(join(dir, "PROMPT.md"), updates.prompt);
}
if (movedToTriage) {
this.emit("task:moved", { task, from: "todo" as Column, to: "triage" as Column });
}
this.emit("task:updated", task);
return task;
});

View File

@@ -184,7 +184,7 @@ export const COLUMN_DESCRIPTIONS: Record<Column, string> = {
export const VALID_TRANSITIONS: Record<Column, Column[]> = {
triage: ["todo"],
todo: ["in-progress", "triage"],
"in-progress": ["in-review", "todo"],
"in-progress": ["in-review", "todo", "triage"],
"in-review": ["done", "in-progress"],
done: [],
};