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:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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: [],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user