@wrongstack/tools 0.303.0 → 0.305.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtin.d.ts +6 -0
- package/dist/builtin.js +717 -381
- package/dist/codebase-index/codebase-index-tool.d.ts +6 -0
- package/dist/codebase-index/index.d.ts +1 -0
- package/dist/codebase-index/index.js +114 -90
- package/dist/codebase-index/indexer.d.ts +6 -0
- package/dist/codebase-index/project-server-endpoint.d.ts +1 -2
- package/dist/codebase-index/project-server.js +131 -109
- package/dist/codebase-index/schema.d.ts +11 -0
- package/dist/codebase-index/worker.js +112 -89
- package/dist/index.d.ts +3 -3
- package/dist/index.js +793 -402
- package/dist/kanban-contract-actions.d.ts +7 -0
- package/dist/kanban-task-inputs.d.ts +15 -2
- package/dist/kanban-tool-schema.d.ts +2 -2
- package/dist/kanban-tool-types.d.ts +32 -11
- package/dist/kanban.js +366 -245
- package/dist/pack.js +716 -381
- package/dist/plan.js +601 -289
- package/dist/read.js +112 -90
- package/dist/session-kanban.d.ts +111 -1
- package/dist/session-kanban.js +232 -46
- package/dist/task.js +601 -289
- package/dist/todo.js +601 -289
- package/dist/tool-tier.js +716 -381
- package/package.json +4 -3
package/dist/plan.js
CHANGED
|
@@ -22,12 +22,17 @@ import {
|
|
|
22
22
|
mutateTasks
|
|
23
23
|
} from "@wrongstack/core/storage";
|
|
24
24
|
import { deserializeTaskGraph } from "@wrongstack/core/tasking";
|
|
25
|
-
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
25
|
+
import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
|
|
26
26
|
import {
|
|
27
27
|
bridgeKanbanSupervisor,
|
|
28
|
+
compactSessionMirrorBoard,
|
|
28
29
|
createBoard,
|
|
30
|
+
DEFAULT_COLUMNS,
|
|
29
31
|
getBoard,
|
|
32
|
+
getDependencyReadinessIssues,
|
|
33
|
+
getKanbanOrchestrationSnapshot,
|
|
30
34
|
listBoards,
|
|
35
|
+
pruneSessionBoards,
|
|
31
36
|
removeBoard,
|
|
32
37
|
syncBoardFromTaskGraph,
|
|
33
38
|
touchKanbanPresence,
|
|
@@ -35,16 +40,14 @@ import {
|
|
|
35
40
|
} from "@wrongstack/kanban";
|
|
36
41
|
var SESSION_BOARD_TAG = "session-work";
|
|
37
42
|
var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
|
|
38
|
-
var SESSION_KANBAN_COLUMNS =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
{ id: "review", title: "Preview", order: 2, wipLimit: 0, color: "#7c3aed" },
|
|
42
|
-
{ id: "done", title: "Done", order: 3, wipLimit: 0, color: "#16a34a" }
|
|
43
|
-
];
|
|
43
|
+
var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
|
|
44
|
+
...column
|
|
45
|
+
}));
|
|
44
46
|
var boardQueue = /* @__PURE__ */ new Map();
|
|
45
47
|
var boardEnsures = /* @__PURE__ */ new Map();
|
|
46
48
|
var pendingMirrors = /* @__PURE__ */ new Map();
|
|
47
49
|
var activeMirrors = /* @__PURE__ */ new Set();
|
|
50
|
+
var mirrorFailures = /* @__PURE__ */ new Map();
|
|
48
51
|
var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
|
|
49
52
|
function boardKey(projectRoot, sessionId) {
|
|
50
53
|
return `${projectRoot}\0${sessionId}`;
|
|
@@ -52,6 +55,33 @@ function boardKey(projectRoot, sessionId) {
|
|
|
52
55
|
function mirrorKey(projectRoot, sessionId, sourceSystem) {
|
|
53
56
|
return `${boardKey(projectRoot, sessionId)}\0${sourceSystem}`;
|
|
54
57
|
}
|
|
58
|
+
function completedReconciliationGraph(latest, candidates) {
|
|
59
|
+
const latestNodeIds = new Set(latest.nodes.map((node) => node.id));
|
|
60
|
+
const carriedNodeIds = /* @__PURE__ */ new Set();
|
|
61
|
+
const completedNodes = candidates.flatMap(
|
|
62
|
+
(candidate) => candidate.nodes.filter((node) => {
|
|
63
|
+
if (node.status !== "completed" || latestNodeIds.has(node.id) || carriedNodeIds.has(node.id)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
carriedNodeIds.add(node.id);
|
|
67
|
+
return true;
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
if (completedNodes.length === 0) return void 0;
|
|
71
|
+
const carriedRequirements = completedNodes.flatMap(
|
|
72
|
+
(node) => node.specRequirementId ? [node.specRequirementId] : []
|
|
73
|
+
);
|
|
74
|
+
return {
|
|
75
|
+
...latest,
|
|
76
|
+
nodes: [...latest.nodes, ...completedNodes],
|
|
77
|
+
rootNodes: [.../* @__PURE__ */ new Set([...latest.rootNodes, ...completedNodes.map((node) => node.id)])],
|
|
78
|
+
...latest.requiredRequirementIds ? {
|
|
79
|
+
requiredRequirementIds: [
|
|
80
|
+
.../* @__PURE__ */ new Set([...latest.requiredRequirementIds, ...carriedRequirements])
|
|
81
|
+
]
|
|
82
|
+
} : {}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
55
85
|
function sessionTag(sessionId) {
|
|
56
86
|
return `session:${sessionId}`;
|
|
57
87
|
}
|
|
@@ -130,16 +160,44 @@ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
|
|
|
130
160
|
sourceSystem,
|
|
131
161
|
tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
|
|
132
162
|
archiveMissingTasks: true,
|
|
133
|
-
includeCompletedTasks: true
|
|
163
|
+
includeCompletedTasks: true,
|
|
164
|
+
// The scope ledger stays declared and accurate, but it may not veto a
|
|
165
|
+
// projection. A session mirror reflects a tactical list that shrinks by
|
|
166
|
+
// design, and refusing the sync never protected the removed row — it
|
|
167
|
+
// froze the entire board, permanently, because the stored scope then
|
|
168
|
+
// outlived every later snapshot (`session-kanban.mirror-failed`).
|
|
169
|
+
// Nothing is lost by shrinking here: `archiveMissingTasks` keeps the
|
|
170
|
+
// removed card on the board as `archived`, the reconciliation pass
|
|
171
|
+
// first walks vanished completed rows to Done, and the session journal
|
|
172
|
+
// remains the durable record.
|
|
173
|
+
allowRequirementScopeShrink: true
|
|
134
174
|
}
|
|
135
175
|
);
|
|
136
|
-
|
|
176
|
+
if (!result) return null;
|
|
177
|
+
const compacted = await compactSessionMirrorBoard(projectRoot, board.id);
|
|
178
|
+
if (compacted?.removedTaskIds.length) {
|
|
179
|
+
return await getBoard(projectRoot, board.id) ?? result.board;
|
|
180
|
+
}
|
|
181
|
+
return result.board;
|
|
137
182
|
});
|
|
138
183
|
}
|
|
139
184
|
function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
|
|
140
185
|
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return;
|
|
141
186
|
const key = mirrorKey(projectRoot, sessionId, sourceSystem);
|
|
142
|
-
pendingMirrors.
|
|
187
|
+
const previous = pendingMirrors.get(key);
|
|
188
|
+
const reconciliationGraph = previous ? completedReconciliationGraph(
|
|
189
|
+
graph,
|
|
190
|
+
[previous.reconciliationGraph, previous.graph].filter(
|
|
191
|
+
(candidate) => candidate !== void 0
|
|
192
|
+
)
|
|
193
|
+
) : void 0;
|
|
194
|
+
pendingMirrors.set(key, {
|
|
195
|
+
projectRoot,
|
|
196
|
+
sessionId,
|
|
197
|
+
graph,
|
|
198
|
+
...reconciliationGraph ? { reconciliationGraph } : {},
|
|
199
|
+
sourceSystem
|
|
200
|
+
});
|
|
143
201
|
if (activeMirrors.has(key)) return;
|
|
144
202
|
activeMirrors.add(key);
|
|
145
203
|
void (async () => {
|
|
@@ -149,20 +207,34 @@ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
|
|
|
149
207
|
if (!pending) break;
|
|
150
208
|
pendingMirrors.delete(key);
|
|
151
209
|
try {
|
|
210
|
+
if (pending.reconciliationGraph) {
|
|
211
|
+
await projectGraph(
|
|
212
|
+
pending.projectRoot,
|
|
213
|
+
pending.sessionId,
|
|
214
|
+
pending.reconciliationGraph,
|
|
215
|
+
pending.sourceSystem
|
|
216
|
+
);
|
|
217
|
+
}
|
|
152
218
|
await projectGraph(
|
|
153
219
|
pending.projectRoot,
|
|
154
220
|
pending.sessionId,
|
|
155
221
|
pending.graph,
|
|
156
222
|
pending.sourceSystem
|
|
157
223
|
);
|
|
224
|
+
mirrorFailures.delete(boardKey(pending.projectRoot, pending.sessionId));
|
|
158
225
|
} catch (error) {
|
|
226
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
227
|
+
mirrorFailures.set(boardKey(pending.projectRoot, pending.sessionId), {
|
|
228
|
+
message,
|
|
229
|
+
sourceSystem: pending.sourceSystem
|
|
230
|
+
});
|
|
159
231
|
console.warn(
|
|
160
232
|
JSON.stringify({
|
|
161
233
|
level: "warn",
|
|
162
234
|
event: "session-kanban.mirror-failed",
|
|
163
235
|
sessionId: pending.sessionId,
|
|
164
236
|
sourceSystem: pending.sourceSystem,
|
|
165
|
-
message
|
|
237
|
+
message,
|
|
166
238
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
167
239
|
})
|
|
168
240
|
);
|
|
@@ -183,6 +255,14 @@ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
|
|
|
183
255
|
}
|
|
184
256
|
})();
|
|
185
257
|
}
|
|
258
|
+
function takeSessionMirrorFailure(projectRoot, sessionId) {
|
|
259
|
+
if (!projectRoot || !sessionId) return void 0;
|
|
260
|
+
const key = boardKey(projectRoot, sessionId);
|
|
261
|
+
const failure = mirrorFailures.get(key);
|
|
262
|
+
if (!failure) return void 0;
|
|
263
|
+
mirrorFailures.delete(key);
|
|
264
|
+
return `Kanban mirror (${failure.sourceSystem}) failed and the board may be stale: ${failure.message}`;
|
|
265
|
+
}
|
|
186
266
|
function todoListToSerializedGraph(todos, sessionId) {
|
|
187
267
|
const graphId = `todo:${sessionId}`;
|
|
188
268
|
const nodes = todos.map((todo, index) => ({
|
|
@@ -314,11 +394,11 @@ function broadcastTodoUpdate(context, todos) {
|
|
|
314
394
|
});
|
|
315
395
|
}
|
|
316
396
|
function notifyTodoUpdate(context, todos) {
|
|
317
|
-
const summary = todos
|
|
397
|
+
const summary = formatTodosForModel(todos);
|
|
318
398
|
const text = `[KANBAN TODO UPDATE]
|
|
319
399
|
Another Kanban agent reassessed the shared board. The canonical todo list is now:
|
|
320
400
|
${summary}
|
|
321
|
-
Reassess your current plan before continuing; do not rely on the initial todo snapshot.`;
|
|
401
|
+
Reassess your current plan before continuing; do not rely on the initial todo snapshot. Preserve each row's <kanban board/task> binding verbatim on your next \`todo\` call \u2014 a row that loses it stops advancing its card.`;
|
|
322
402
|
const state = context.state;
|
|
323
403
|
if (typeof state.appendBlockToLastUserMessage === "function") {
|
|
324
404
|
if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
|
|
@@ -349,26 +429,68 @@ function todoStatus(task) {
|
|
|
349
429
|
if (status === "in_progress" || status === "review") return "in_progress";
|
|
350
430
|
return "pending";
|
|
351
431
|
}
|
|
352
|
-
function sessionTodoFromTask(task,
|
|
432
|
+
function sessionTodoFromTask(task, board) {
|
|
433
|
+
const blockedBy = board ? blockingTitles(board, task) : [];
|
|
353
434
|
return {
|
|
354
435
|
id: task.origin?.taskId ?? task.id,
|
|
355
436
|
content: task.title,
|
|
356
437
|
status: todoStatus(task),
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
...task.description ? { activeForm: task.description } : {}
|
|
438
|
+
...task.description ? { activeForm: task.description } : {},
|
|
439
|
+
...blockedBy.length ? { blockedBy } : {}
|
|
360
440
|
};
|
|
361
441
|
}
|
|
362
|
-
function managedTodoFromTask(task,
|
|
442
|
+
function managedTodoFromTask(task, board) {
|
|
363
443
|
return {
|
|
364
|
-
...sessionTodoFromTask(task,
|
|
365
|
-
|
|
444
|
+
...sessionTodoFromTask(task, board),
|
|
445
|
+
kanbanBoardId: board.id,
|
|
446
|
+
kanbanTaskId: task.id
|
|
366
447
|
};
|
|
367
448
|
}
|
|
449
|
+
function blockingTitles(board, task) {
|
|
450
|
+
return getDependencyReadinessIssues(board, task).map((issue) => {
|
|
451
|
+
const dependency = board.tasks.find((candidate) => candidate.id === issue.dependencyId);
|
|
452
|
+
if (!dependency) return `${issue.dependencyId} (missing)`;
|
|
453
|
+
return dependency.title;
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
var PRIORITY_ORDER = {
|
|
457
|
+
critical: 0,
|
|
458
|
+
high: 1,
|
|
459
|
+
medium: 2,
|
|
460
|
+
low: 3
|
|
461
|
+
};
|
|
462
|
+
function orderTasksForTodos(board, tasks) {
|
|
463
|
+
const columnOrder = new Map(board.columns.map((column) => [column.id, column.order]));
|
|
464
|
+
const baseline = [...tasks].sort(
|
|
465
|
+
(left, right) => (columnOrder.get(left.columnId) ?? 0) - (columnOrder.get(right.columnId) ?? 0) || (PRIORITY_ORDER[left.priority] ?? 2) - (PRIORITY_ORDER[right.priority] ?? 2) || left.order - right.order || left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id)
|
|
466
|
+
);
|
|
467
|
+
const included = new Set(baseline.map((task) => task.id));
|
|
468
|
+
const remaining = new Map(baseline.map((task) => [task.id, task]));
|
|
469
|
+
const emitted = [];
|
|
470
|
+
const done = /* @__PURE__ */ new Set();
|
|
471
|
+
while (remaining.size > 0) {
|
|
472
|
+
const ready = baseline.filter(
|
|
473
|
+
(task) => remaining.has(task.id) && (task.dependsOn ?? []).every(
|
|
474
|
+
(dependencyId) => !included.has(dependencyId) || done.has(dependencyId)
|
|
475
|
+
)
|
|
476
|
+
);
|
|
477
|
+
if (ready.length === 0) break;
|
|
478
|
+
for (const task of ready) {
|
|
479
|
+
remaining.delete(task.id);
|
|
480
|
+
done.add(task.id);
|
|
481
|
+
emitted.push(task);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
for (const task of baseline) if (remaining.has(task.id)) emitted.push(task);
|
|
485
|
+
return emitted;
|
|
486
|
+
}
|
|
368
487
|
function sameTodos(left, right) {
|
|
369
488
|
return left.length === right.length && left.every((todo, index) => {
|
|
370
489
|
const candidate = right[index];
|
|
371
|
-
return candidate?.id === todo.id && candidate.content === todo.content && candidate.status === todo.status && candidate.activeForm === todo.activeForm && candidate.promotedFromPlan === todo.promotedFromPlan && candidate.promotedFromTask === todo.promotedFromTask && candidate.kanbanBoardId === todo.kanbanBoardId && candidate.kanbanTaskId === todo.kanbanTaskId
|
|
490
|
+
return candidate?.id === todo.id && candidate.content === todo.content && candidate.status === todo.status && candidate.activeForm === todo.activeForm && candidate.promotedFromPlan === todo.promotedFromPlan && candidate.promotedFromTask === todo.promotedFromTask && candidate.kanbanBoardId === todo.kanbanBoardId && candidate.kanbanTaskId === todo.kanbanTaskId && // Readiness is part of the projection: when a dependency completes,
|
|
491
|
+
// the rows are otherwise identical and the unblocking would never
|
|
492
|
+
// reach the model.
|
|
493
|
+
(candidate.blockedBy ?? []).join("\0") === (todo.blockedBy ?? []).join("\0");
|
|
372
494
|
});
|
|
373
495
|
}
|
|
374
496
|
function applyManagedKanbanBoardToTodos(context, board) {
|
|
@@ -378,11 +500,12 @@ function applyManagedKanbanBoardToTodos(context, board) {
|
|
|
378
500
|
if (!activeBoardId2 || board.id !== activeBoardId2 || board.lifecycle?.mode !== "managed") {
|
|
379
501
|
return [...context.todos];
|
|
380
502
|
}
|
|
381
|
-
const projectedTodos =
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
503
|
+
const projectedTodos = orderTasksForTodos(
|
|
504
|
+
board,
|
|
505
|
+
board.tasks.filter(
|
|
506
|
+
(task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
|
|
507
|
+
)
|
|
508
|
+
).map((task) => managedTodoFromTask(task, board));
|
|
386
509
|
if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
|
|
387
510
|
suppressedTodoMirrors.add(context);
|
|
388
511
|
try {
|
|
@@ -403,14 +526,13 @@ import {
|
|
|
403
526
|
saveTasks,
|
|
404
527
|
setPlanItemStatus
|
|
405
528
|
} from "@wrongstack/core/storage";
|
|
406
|
-
import { getBoard as getBoard4 } from "@wrongstack/kanban";
|
|
529
|
+
import { addTask as addTask2, getBoard as getBoard4 } from "@wrongstack/kanban";
|
|
407
530
|
|
|
408
531
|
// src/kanban.ts
|
|
409
532
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
410
533
|
import { loadTasks as loadTasks2 } from "@wrongstack/core/storage";
|
|
411
534
|
import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
|
|
412
535
|
import {
|
|
413
|
-
addColumn,
|
|
414
536
|
addTask,
|
|
415
537
|
adoptManagedLifecycle,
|
|
416
538
|
assignTask,
|
|
@@ -425,7 +547,7 @@ import {
|
|
|
425
547
|
exportBoardToTaskGraph,
|
|
426
548
|
finalizeTaskCompletion,
|
|
427
549
|
getBoard as getBoard3,
|
|
428
|
-
getKanbanOrchestrationSnapshot,
|
|
550
|
+
getKanbanOrchestrationSnapshot as getKanbanOrchestrationSnapshot2,
|
|
429
551
|
getKanbanQueueHealth,
|
|
430
552
|
getTask,
|
|
431
553
|
getTaskChain,
|
|
@@ -439,16 +561,16 @@ import {
|
|
|
439
561
|
recoverStaleTaskAssignments,
|
|
440
562
|
releaseTaskClaim,
|
|
441
563
|
removeBoard as removeBoard2,
|
|
442
|
-
removeColumn,
|
|
443
564
|
removeTask,
|
|
444
565
|
repairManagedTaskProjection,
|
|
566
|
+
resolveAutoAccept,
|
|
445
567
|
searchKanban,
|
|
446
568
|
setTaskChain,
|
|
569
|
+
stripLifecycleIssues,
|
|
447
570
|
syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
|
|
448
571
|
transferTaskToBoard,
|
|
449
572
|
transitionTask,
|
|
450
573
|
updateBoard as updateBoard2,
|
|
451
|
-
updateColumn,
|
|
452
574
|
updateTask as updateTask2,
|
|
453
575
|
updateTaskAssignment,
|
|
454
576
|
verifyTaskCompletion as verifyTaskCompletion2
|
|
@@ -498,6 +620,137 @@ function duplicateBoardOptions(input) {
|
|
|
498
620
|
};
|
|
499
621
|
}
|
|
500
622
|
|
|
623
|
+
// src/kanban-contract-actions.ts
|
|
624
|
+
import {
|
|
625
|
+
addContractEdge,
|
|
626
|
+
configureContractGraph,
|
|
627
|
+
evaluateTaskContractGraph,
|
|
628
|
+
getContractGraph,
|
|
629
|
+
removeContractEdge,
|
|
630
|
+
removeContractNode,
|
|
631
|
+
upsertContractNode
|
|
632
|
+
} from "@wrongstack/kanban";
|
|
633
|
+
|
|
634
|
+
// src/kanban-tool-results.ts
|
|
635
|
+
function atomicityNudge(task) {
|
|
636
|
+
if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
|
|
637
|
+
const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
|
|
638
|
+
return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
|
|
639
|
+
}
|
|
640
|
+
function readEnvGateEnforcement() {
|
|
641
|
+
const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
|
|
642
|
+
return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
|
|
643
|
+
}
|
|
644
|
+
function fail(message) {
|
|
645
|
+
return { ok: false, message };
|
|
646
|
+
}
|
|
647
|
+
function okBoard(board, message = "Board loaded.") {
|
|
648
|
+
return { ok: true, message, board };
|
|
649
|
+
}
|
|
650
|
+
function okTask(board, task, message) {
|
|
651
|
+
return { ok: true, message, board, task };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/kanban-contract-actions.ts
|
|
655
|
+
async function handleKanbanContractAction(projectRoot, input, actor) {
|
|
656
|
+
switch (input.action) {
|
|
657
|
+
case "get_contract_graph": {
|
|
658
|
+
if (!input.boardId) return fail("get_contract_graph requires boardId.");
|
|
659
|
+
const found = await getContractGraph(projectRoot, input.boardId);
|
|
660
|
+
if (!found) return fail("Board not found.");
|
|
661
|
+
const evaluated = input.taskId ? await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId) : null;
|
|
662
|
+
if (input.taskId && !evaluated) return fail("Task not found on this board.");
|
|
663
|
+
return {
|
|
664
|
+
ok: true,
|
|
665
|
+
message: found.graph ? `Contract map: ${found.graph.nodes.length} node(s), ${found.graph.edges.length} edge(s), enforcement ${found.graph.enforcement}.` : "No contract map on this board yet. Call configure_contract_graph to start one.",
|
|
666
|
+
board: found.board,
|
|
667
|
+
contractGraph: found.graph,
|
|
668
|
+
...evaluated ? { contractEvaluation: evaluated.evaluation } : {}
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
case "configure_contract_graph": {
|
|
672
|
+
if (!input.boardId) return fail("configure_contract_graph requires boardId.");
|
|
673
|
+
const enforcement = input.contractEnforcement ?? "advisory";
|
|
674
|
+
const board = await configureContractGraph(projectRoot, input.boardId, enforcement);
|
|
675
|
+
return board ? okBoard(board, `Contract map enforcement set to ${enforcement}.`) : fail("Board not found.");
|
|
676
|
+
}
|
|
677
|
+
case "upsert_contract_node": {
|
|
678
|
+
if (!input.boardId || !input.taskId) {
|
|
679
|
+
return fail("upsert_contract_node requires boardId and taskId.");
|
|
680
|
+
}
|
|
681
|
+
if (!input.contractNodeKind || !input.contractNodeTitle) {
|
|
682
|
+
return fail("upsert_contract_node requires contractNodeKind and contractNodeTitle.");
|
|
683
|
+
}
|
|
684
|
+
const waiver = input.contractNodeState === "waived" ? {
|
|
685
|
+
actor: actor ?? "agent",
|
|
686
|
+
reason: input.contractWaiverReason ?? "",
|
|
687
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
688
|
+
} : void 0;
|
|
689
|
+
if (waiver && !waiver.reason.trim()) {
|
|
690
|
+
return fail("A waived contract node requires contractWaiverReason.");
|
|
691
|
+
}
|
|
692
|
+
const result = await upsertContractNode(projectRoot, input.boardId, {
|
|
693
|
+
taskId: input.taskId,
|
|
694
|
+
kind: input.contractNodeKind,
|
|
695
|
+
title: input.contractNodeTitle,
|
|
696
|
+
...input.contractNodeId !== void 0 ? { id: input.contractNodeId } : {},
|
|
697
|
+
...input.contractNodeDescription !== void 0 ? { description: input.contractNodeDescription } : {},
|
|
698
|
+
...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
|
|
699
|
+
...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
|
|
700
|
+
...input.contractCheckId !== void 0 ? { checkId: input.contractCheckId } : {},
|
|
701
|
+
...input.contractMetricId !== void 0 ? { metricId: input.contractMetricId } : {},
|
|
702
|
+
...waiver ? { waiver } : {},
|
|
703
|
+
...actor !== void 0 ? { createdBy: actor } : {}
|
|
704
|
+
});
|
|
705
|
+
if (!result) return fail("Board or task not found.");
|
|
706
|
+
return {
|
|
707
|
+
ok: true,
|
|
708
|
+
message: `Contract node ${result.node.kind} "${result.node.title}" saved (${result.node.id}).`,
|
|
709
|
+
board: result.board,
|
|
710
|
+
contractGraph: result.board.contractGraph ?? null
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
case "remove_contract_node": {
|
|
714
|
+
if (!input.boardId || !input.contractNodeId) {
|
|
715
|
+
return fail("remove_contract_node requires boardId and contractNodeId.");
|
|
716
|
+
}
|
|
717
|
+
const board = await removeContractNode(projectRoot, input.boardId, input.contractNodeId);
|
|
718
|
+
return board ? okBoard(board, "Contract node removed, along with every edge that touched it.") : fail("Contract node not found.");
|
|
719
|
+
}
|
|
720
|
+
case "add_contract_edge": {
|
|
721
|
+
if (!input.boardId || !input.contractEdgeFrom || !input.contractEdgeTo) {
|
|
722
|
+
return fail("add_contract_edge requires boardId, contractEdgeFrom, and contractEdgeTo.");
|
|
723
|
+
}
|
|
724
|
+
if (!input.contractEdgeType) return fail("add_contract_edge requires contractEdgeType.");
|
|
725
|
+
const result = await addContractEdge(projectRoot, input.boardId, {
|
|
726
|
+
from: input.contractEdgeFrom,
|
|
727
|
+
to: input.contractEdgeTo,
|
|
728
|
+
type: input.contractEdgeType,
|
|
729
|
+
...input.contractEdgeId !== void 0 ? { id: input.contractEdgeId } : {},
|
|
730
|
+
...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
|
|
731
|
+
...input.contractEdgeRationale !== void 0 ? { rationale: input.contractEdgeRationale } : {},
|
|
732
|
+
...actor !== void 0 ? { createdBy: actor } : {}
|
|
733
|
+
});
|
|
734
|
+
if (!result) return fail("Board not found.");
|
|
735
|
+
return {
|
|
736
|
+
ok: true,
|
|
737
|
+
message: `Contract edge ${result.edge.type}: ${result.edge.from} \u2192 ${result.edge.to}.`,
|
|
738
|
+
board: result.board,
|
|
739
|
+
contractGraph: result.board.contractGraph ?? null
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
case "remove_contract_edge": {
|
|
743
|
+
if (!input.boardId || !input.contractEdgeId) {
|
|
744
|
+
return fail("remove_contract_edge requires boardId and contractEdgeId.");
|
|
745
|
+
}
|
|
746
|
+
const board = await removeContractEdge(projectRoot, input.boardId, input.contractEdgeId);
|
|
747
|
+
return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
|
|
748
|
+
}
|
|
749
|
+
default:
|
|
750
|
+
return void 0;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
501
754
|
// src/kanban-decomposition-actions.ts
|
|
502
755
|
import {
|
|
503
756
|
assessTaskAtomicity,
|
|
@@ -529,26 +782,6 @@ function recordKanbanVerificationEvidence(ctx, report) {
|
|
|
529
782
|
}
|
|
530
783
|
}
|
|
531
784
|
|
|
532
|
-
// src/kanban-tool-results.ts
|
|
533
|
-
function atomicityNudge(task) {
|
|
534
|
-
if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
|
|
535
|
-
const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
|
|
536
|
-
return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
|
|
537
|
-
}
|
|
538
|
-
function readEnvGateEnforcement() {
|
|
539
|
-
const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
|
|
540
|
-
return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
|
|
541
|
-
}
|
|
542
|
-
function fail(message) {
|
|
543
|
-
return { ok: false, message };
|
|
544
|
-
}
|
|
545
|
-
function okBoard(board, message = "Board loaded.") {
|
|
546
|
-
return { ok: true, message, board };
|
|
547
|
-
}
|
|
548
|
-
function okTask(board, task, message) {
|
|
549
|
-
return { ok: true, message, board, task };
|
|
550
|
-
}
|
|
551
|
-
|
|
552
785
|
// src/kanban-decomposition-actions.ts
|
|
553
786
|
async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
|
|
554
787
|
switch (input.action) {
|
|
@@ -633,20 +866,14 @@ async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
|
|
|
633
866
|
// src/kanban-detail-actions.ts
|
|
634
867
|
import {
|
|
635
868
|
addCheckToTask,
|
|
636
|
-
addContractEdge,
|
|
637
869
|
addDependency,
|
|
638
870
|
addGoalMetricToTask,
|
|
639
871
|
addLinkToTask,
|
|
640
872
|
addNoteToTask,
|
|
641
|
-
configureContractGraph,
|
|
642
|
-
evaluateTaskContractGraph,
|
|
643
|
-
getContractGraph,
|
|
644
873
|
getKanbanWorkbench,
|
|
645
|
-
|
|
646
|
-
removeContractNode,
|
|
874
|
+
removeCheckFromTask,
|
|
647
875
|
updateCheckOnTask,
|
|
648
|
-
updateGoalMetricOnTask
|
|
649
|
-
upsertContractNode
|
|
876
|
+
updateGoalMetricOnTask
|
|
650
877
|
} from "@wrongstack/kanban";
|
|
651
878
|
|
|
652
879
|
// src/kanban-split-task-handler.ts
|
|
@@ -712,131 +939,6 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
712
939
|
workbench
|
|
713
940
|
};
|
|
714
941
|
}
|
|
715
|
-
case "get_contract_graph": {
|
|
716
|
-
if (!input.boardId) return fail("get_contract_graph requires boardId.");
|
|
717
|
-
const result = await getContractGraph(projectRoot, input.boardId);
|
|
718
|
-
return result ? {
|
|
719
|
-
ok: true,
|
|
720
|
-
message: result.graph ? `${result.graph.nodes.length} contract node(s), ${result.graph.edges.length} edge(s).` : "Contract graph is not configured.",
|
|
721
|
-
board: result.board,
|
|
722
|
-
...result.graph ? { contractGraph: result.graph } : {}
|
|
723
|
-
} : fail("Board not found.");
|
|
724
|
-
}
|
|
725
|
-
case "configure_contract_graph": {
|
|
726
|
-
if (!input.boardId || !input.contractGraphEnforcement) {
|
|
727
|
-
return fail("configure_contract_graph requires boardId and contractGraphEnforcement.");
|
|
728
|
-
}
|
|
729
|
-
const current = await getContractGraph(projectRoot, input.boardId);
|
|
730
|
-
if (!current) return fail("Board not found.");
|
|
731
|
-
if (input.contractGraphEnforcement === "strict" && current.graph?.enforcement !== "strict") {
|
|
732
|
-
return fail(
|
|
733
|
-
"Strict Contract Map enforcement is operator-owned. Autonomous agents may use advisory maps but may not turn them into an execution gate."
|
|
734
|
-
);
|
|
735
|
-
}
|
|
736
|
-
if (current.graph?.enforcement === "strict" && input.contractGraphEnforcement !== "strict") {
|
|
737
|
-
return fail("An autonomous agent may not loosen a strict contract graph.");
|
|
738
|
-
}
|
|
739
|
-
const board = await configureContractGraph(
|
|
740
|
-
projectRoot,
|
|
741
|
-
input.boardId,
|
|
742
|
-
input.contractGraphEnforcement
|
|
743
|
-
);
|
|
744
|
-
return board ? okBoard(board, "Contract graph configured.") : fail("Board not found.");
|
|
745
|
-
}
|
|
746
|
-
case "upsert_contract_node": {
|
|
747
|
-
if (!input.boardId || !input.taskId || !input.contractNodeKind || !input.title) {
|
|
748
|
-
return fail("upsert_contract_node requires boardId, taskId, contractNodeKind, and title.");
|
|
749
|
-
}
|
|
750
|
-
if (input.contractNodeState === "waived") {
|
|
751
|
-
return fail(
|
|
752
|
-
"The autonomous kanban tool may not waive contract nodes; a human-owned review surface must record that exception."
|
|
753
|
-
);
|
|
754
|
-
}
|
|
755
|
-
if (input.contractNodeId) {
|
|
756
|
-
const current = await getContractGraph(projectRoot, input.boardId);
|
|
757
|
-
const existing = current?.graph?.nodes.find((node) => node.id === input.contractNodeId);
|
|
758
|
-
if (current?.graph?.enforcement === "strict" && existing && (existing.kind !== input.contractNodeKind || input.contractEnforcement !== void 0 && input.contractEnforcement !== existing.enforcement)) {
|
|
759
|
-
return fail(
|
|
760
|
-
"The autonomous kanban tool may not change the kind or enforcement of an existing strict contract node."
|
|
761
|
-
);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
const result = await upsertContractNode(projectRoot, input.boardId, {
|
|
765
|
-
...input.contractNodeId ? { id: input.contractNodeId } : {},
|
|
766
|
-
taskId: input.taskId,
|
|
767
|
-
kind: input.contractNodeKind,
|
|
768
|
-
title: input.title,
|
|
769
|
-
...input.description !== void 0 ? { description: input.description } : {},
|
|
770
|
-
...input.contractEnforcement !== void 0 ? { enforcement: input.contractEnforcement } : {},
|
|
771
|
-
...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
|
|
772
|
-
...input.checkId !== void 0 ? { checkId: input.checkId } : {},
|
|
773
|
-
...input.metricId !== void 0 ? { metricId: input.metricId } : {},
|
|
774
|
-
...input.baseline !== void 0 ? { baseline: input.baseline } : {},
|
|
775
|
-
...input.threshold !== void 0 ? { threshold: input.threshold } : {},
|
|
776
|
-
...input.author !== void 0 ? { createdBy: input.author } : {}
|
|
777
|
-
});
|
|
778
|
-
return result ? {
|
|
779
|
-
...okBoard(result.board, "Contract node saved."),
|
|
780
|
-
contractGraph: result.board.contractGraph
|
|
781
|
-
} : fail("Task not found.");
|
|
782
|
-
}
|
|
783
|
-
case "link_contract_nodes": {
|
|
784
|
-
if (!input.boardId || !input.fromNodeId || !input.toNodeId || !input.contractEdgeType) {
|
|
785
|
-
return fail(
|
|
786
|
-
"link_contract_nodes requires boardId, fromNodeId, toNodeId, and contractEdgeType."
|
|
787
|
-
);
|
|
788
|
-
}
|
|
789
|
-
const result = await addContractEdge(projectRoot, input.boardId, {
|
|
790
|
-
from: input.fromNodeId,
|
|
791
|
-
to: input.toNodeId,
|
|
792
|
-
type: input.contractEdgeType,
|
|
793
|
-
...input.contractEdgeId ? { id: input.contractEdgeId } : {},
|
|
794
|
-
...input.contractEnforcement ? { enforcement: input.contractEnforcement } : {},
|
|
795
|
-
...input.contractRationale ? { rationale: input.contractRationale } : {},
|
|
796
|
-
...input.author ? { createdBy: input.author } : {}
|
|
797
|
-
});
|
|
798
|
-
return result ? {
|
|
799
|
-
...okBoard(result.board, "Contract edge added."),
|
|
800
|
-
contractGraph: result.board.contractGraph
|
|
801
|
-
} : fail("Board not found.");
|
|
802
|
-
}
|
|
803
|
-
case "remove_contract_node": {
|
|
804
|
-
if (!input.boardId || !input.contractNodeId) {
|
|
805
|
-
return fail("remove_contract_node requires boardId and contractNodeId.");
|
|
806
|
-
}
|
|
807
|
-
const current = await getContractGraph(projectRoot, input.boardId);
|
|
808
|
-
const node = current?.graph?.nodes.find((candidate) => candidate.id === input.contractNodeId);
|
|
809
|
-
if (current?.graph?.enforcement === "strict" && node?.enforcement === "blocking") {
|
|
810
|
-
return fail("The autonomous kanban tool may not remove a blocking strict contract node.");
|
|
811
|
-
}
|
|
812
|
-
const board = await removeContractNode(projectRoot, input.boardId, input.contractNodeId);
|
|
813
|
-
return board ? okBoard(board, "Contract node removed.") : fail("Contract node not found.");
|
|
814
|
-
}
|
|
815
|
-
case "remove_contract_edge": {
|
|
816
|
-
if (!input.boardId || !input.contractEdgeId) {
|
|
817
|
-
return fail("remove_contract_edge requires boardId and contractEdgeId.");
|
|
818
|
-
}
|
|
819
|
-
const current = await getContractGraph(projectRoot, input.boardId);
|
|
820
|
-
const edge = current?.graph?.edges.find((candidate) => candidate.id === input.contractEdgeId);
|
|
821
|
-
if (current?.graph?.enforcement === "strict" && edge?.enforcement === "blocking") {
|
|
822
|
-
return fail("The autonomous kanban tool may not remove a blocking strict contract edge.");
|
|
823
|
-
}
|
|
824
|
-
const board = await removeContractEdge(projectRoot, input.boardId, input.contractEdgeId);
|
|
825
|
-
return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
|
|
826
|
-
}
|
|
827
|
-
case "evaluate_contract_graph": {
|
|
828
|
-
if (!input.boardId || !input.taskId) {
|
|
829
|
-
return fail("evaluate_contract_graph requires boardId and taskId.");
|
|
830
|
-
}
|
|
831
|
-
const result = await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId);
|
|
832
|
-
return result ? {
|
|
833
|
-
ok: result.evaluation.allowed,
|
|
834
|
-
message: result.evaluation.allowed ? "Contract graph is closed." : `Contract graph has ${result.evaluation.issues.length} unresolved issue(s).`,
|
|
835
|
-
board: result.board,
|
|
836
|
-
contractGraph: result.board.contractGraph,
|
|
837
|
-
contractEvaluation: result.evaluation
|
|
838
|
-
} : fail("Task not found.");
|
|
839
|
-
}
|
|
840
942
|
case "add_dependency": {
|
|
841
943
|
if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
|
|
842
944
|
return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
|
|
@@ -889,8 +991,9 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
889
991
|
}
|
|
890
992
|
const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
|
|
891
993
|
description: input.checkDescription,
|
|
892
|
-
type: "manual",
|
|
893
|
-
status: input.checkStatus
|
|
994
|
+
type: input.checkType ?? "manual",
|
|
995
|
+
status: input.checkStatus,
|
|
996
|
+
...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
|
|
894
997
|
});
|
|
895
998
|
return board ? okBoard(board, "Check added.") : fail("Task not found.");
|
|
896
999
|
}
|
|
@@ -905,11 +1008,27 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
905
1008
|
input.checkId,
|
|
906
1009
|
{
|
|
907
1010
|
...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
|
|
908
|
-
...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
|
|
1011
|
+
...input.checkStatus !== void 0 ? { status: input.checkStatus } : {},
|
|
1012
|
+
// Promoting an existing manual criterion to an executable one is the
|
|
1013
|
+
// common repair: the card was written before anyone knew the command.
|
|
1014
|
+
...input.checkType !== void 0 ? { type: input.checkType } : {},
|
|
1015
|
+
...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
|
|
909
1016
|
}
|
|
910
1017
|
);
|
|
911
1018
|
return board ? okBoard(board, "Check updated.") : fail("Check not found.");
|
|
912
1019
|
}
|
|
1020
|
+
case "remove_check": {
|
|
1021
|
+
if (!input.boardId || !input.taskId || !input.checkId) {
|
|
1022
|
+
return fail("remove_check requires boardId, taskId, and checkId.");
|
|
1023
|
+
}
|
|
1024
|
+
const board = await removeCheckFromTask(
|
|
1025
|
+
projectRoot,
|
|
1026
|
+
input.boardId,
|
|
1027
|
+
input.taskId,
|
|
1028
|
+
input.checkId
|
|
1029
|
+
);
|
|
1030
|
+
return board ? okBoard(board, "Acceptance criterion removed.") : fail("Check not found on this task.");
|
|
1031
|
+
}
|
|
913
1032
|
case "add_note": {
|
|
914
1033
|
if (!input.boardId || !input.taskId || !input.note)
|
|
915
1034
|
return fail("add_note requires boardId, taskId, and note.");
|
|
@@ -984,14 +1103,25 @@ function taskInput(input) {
|
|
|
984
1103
|
...input.order !== void 0 ? { order: input.order } : {},
|
|
985
1104
|
...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
|
|
986
1105
|
...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
|
|
1106
|
+
// The system prompt has always told the model it may "set atomic: true"
|
|
1107
|
+
// when creating a composite parent. It could not: the field reached
|
|
1108
|
+
// neither the create input nor the patch, so the instruction described a
|
|
1109
|
+
// capability that did not exist and the attempt was silently dropped.
|
|
1110
|
+
...input.atomic !== void 0 ? { atomic: input.atomic } : {},
|
|
987
1111
|
...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
|
|
988
1112
|
...input.checkDescription !== void 0 ? {
|
|
989
1113
|
successCriteria: [
|
|
990
1114
|
{
|
|
991
1115
|
id: randomUUID(),
|
|
992
1116
|
description: input.checkDescription,
|
|
993
|
-
|
|
994
|
-
|
|
1117
|
+
// `manual` only as the fallback. Hard-coding it here meant every
|
|
1118
|
+
// agent-authored criterion was unverifiable by construction: the
|
|
1119
|
+
// deterministic plugins never matched, the registry passed the
|
|
1120
|
+
// hand-set status straight through, and "verified" collapsed into
|
|
1121
|
+
// "the author ticked its own box".
|
|
1122
|
+
type: input.checkType ?? "manual",
|
|
1123
|
+
status: input.checkStatus ?? "pending",
|
|
1124
|
+
...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
|
|
995
1125
|
}
|
|
996
1126
|
]
|
|
997
1127
|
} : {},
|
|
@@ -1039,11 +1169,11 @@ function taskInput(input) {
|
|
|
1039
1169
|
};
|
|
1040
1170
|
}
|
|
1041
1171
|
function mergedDependsOn(input) {
|
|
1042
|
-
|
|
1172
|
+
if (input.dependsOn === void 0 && input.dependencyTaskId === void 0) return void 0;
|
|
1173
|
+
return [
|
|
1043
1174
|
...input.dependsOn ?? [],
|
|
1044
1175
|
...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
|
|
1045
1176
|
].filter((id, i, arr) => id && arr.indexOf(id) === i);
|
|
1046
|
-
return ids.length > 0 ? ids : void 0;
|
|
1047
1177
|
}
|
|
1048
1178
|
function taskPatch(input) {
|
|
1049
1179
|
return {
|
|
@@ -1057,7 +1187,15 @@ function taskPatch(input) {
|
|
|
1057
1187
|
status: input.status,
|
|
1058
1188
|
labels: input.labels,
|
|
1059
1189
|
assignedAgent: input.agentId,
|
|
1060
|
-
...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
|
|
1190
|
+
...mergedDependsOn(input) !== void 0 ? { dependsOn: mergedDependsOn(input) } : {},
|
|
1191
|
+
// `atomic` and `childTaskIds` are the composite-parent contract, and the
|
|
1192
|
+
// managed gate reads both: an `atomic` parent may not move forward without
|
|
1193
|
+
// children, and may not reach Done until every child is completed. The
|
|
1194
|
+
// manager has always accepted both on a patch; only this surface withheld
|
|
1195
|
+
// them, so `split_atomic` was a one-way door — delete the children and the
|
|
1196
|
+
// parent was stranded with no way to declare itself a leaf again.
|
|
1197
|
+
...input.atomic !== void 0 ? { atomic: input.atomic } : {},
|
|
1198
|
+
...input.childTaskIds !== void 0 ? { childTaskIds: input.childTaskIds } : {},
|
|
1061
1199
|
...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
|
|
1062
1200
|
...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
|
|
1063
1201
|
};
|
|
@@ -1117,8 +1255,8 @@ function assignmentForTaskCreate(input) {
|
|
|
1117
1255
|
}
|
|
1118
1256
|
|
|
1119
1257
|
// src/kanban-tool-schema.ts
|
|
1120
|
-
var KANBAN_TOOL_DESCRIPTION = "
|
|
1121
|
-
var KANBAN_TOOL_USAGE_HINT =
|
|
1258
|
+
var KANBAN_TOOL_DESCRIPTION = "Durable project task boards: create and move cards, record checks, notes, links and assignments. The board is a record of the work, not a permit for it \u2014 nothing here gates other tools. Managed boards additionally enforce ordered Backlog \u2192 Todo \u2192 Running \u2192 Review \u2192 Done transitions; release_managed_lifecycle turns that off.";
|
|
1259
|
+
var KANBAN_TOOL_USAGE_HINT = 'Track substantial or multi-step work so it survives the session; a trivial edit or a question needs no card. Work stays on ONE board: call list_boards first and add_task to the board this project already uses. create_board is for a genuinely separate line of work, not for each new piece of it \u2014 a second board splits the same effort in two, and a board holding a single card is the usual sign. Common flow: list_boards or search_tasks to orient, add_task to record work, start_task when you begin, update_check with checkStatus "passed" to tick acceptance criteria (read their ids from get_task), then transition_task. On a managed board a refused transition names the field it wants \u2014 supply it and retry. When the acceptance criterion is something a machine can run, say so: set checkType ("command", "test", "file_exists", "file_matches", "git_diff", "metric") and put the command, pattern or path in checkNotes, then verify_completion executes it and the result is real evidence. Leave checkType off (or "manual") only for criteria that genuinely need a human eye \u2014 a manual check records your assertion, it does not test anything.';
|
|
1122
1260
|
var KANBAN_INPUT_SCHEMA = {
|
|
1123
1261
|
type: "object",
|
|
1124
1262
|
properties: {
|
|
@@ -1131,6 +1269,7 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
1131
1269
|
"duplicate_board",
|
|
1132
1270
|
"update_board",
|
|
1133
1271
|
"adopt_managed_lifecycle",
|
|
1272
|
+
"release_managed_lifecycle",
|
|
1134
1273
|
"delete_board",
|
|
1135
1274
|
"generate_board",
|
|
1136
1275
|
"export_markdown",
|
|
@@ -1142,9 +1281,6 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
1142
1281
|
"ready_tasks",
|
|
1143
1282
|
"snapshot",
|
|
1144
1283
|
"workbench",
|
|
1145
|
-
"add_column",
|
|
1146
|
-
"update_column",
|
|
1147
|
-
"delete_column",
|
|
1148
1284
|
"add_task",
|
|
1149
1285
|
"split_task",
|
|
1150
1286
|
"merge_tasks",
|
|
@@ -1159,13 +1295,6 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
1159
1295
|
"delete_task",
|
|
1160
1296
|
"set_chain",
|
|
1161
1297
|
"get_chain",
|
|
1162
|
-
"get_contract_graph",
|
|
1163
|
-
"configure_contract_graph",
|
|
1164
|
-
"upsert_contract_node",
|
|
1165
|
-
"link_contract_nodes",
|
|
1166
|
-
"remove_contract_node",
|
|
1167
|
-
"remove_contract_edge",
|
|
1168
|
-
"evaluate_contract_graph",
|
|
1169
1298
|
"claim_task",
|
|
1170
1299
|
"release_task",
|
|
1171
1300
|
"assign_task",
|
|
@@ -1179,49 +1308,27 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
1179
1308
|
"update_goal_metric",
|
|
1180
1309
|
"add_check",
|
|
1181
1310
|
"update_check",
|
|
1311
|
+
"remove_check",
|
|
1182
1312
|
"add_note",
|
|
1183
1313
|
"add_link",
|
|
1184
1314
|
"verify_completion",
|
|
1185
1315
|
"split_atomic",
|
|
1186
1316
|
"assess_atomicity",
|
|
1187
|
-
"propose_decomposition"
|
|
1317
|
+
"propose_decomposition",
|
|
1318
|
+
"get_contract_graph",
|
|
1319
|
+
"configure_contract_graph",
|
|
1320
|
+
"upsert_contract_node",
|
|
1321
|
+
"remove_contract_node",
|
|
1322
|
+
"add_contract_edge",
|
|
1323
|
+
"remove_contract_edge"
|
|
1188
1324
|
]
|
|
1189
1325
|
},
|
|
1190
1326
|
boardId: { type: "string" },
|
|
1191
1327
|
taskId: { type: "string" },
|
|
1192
1328
|
taskIds: { type: "array", items: { type: "string" } },
|
|
1193
1329
|
chainId: { type: "string" },
|
|
1194
|
-
contractNodeId: { type: "string" },
|
|
1195
|
-
contractNodeKind: {
|
|
1196
|
-
type: "string",
|
|
1197
|
-
enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"]
|
|
1198
|
-
},
|
|
1199
|
-
contractNodeState: {
|
|
1200
|
-
type: "string",
|
|
1201
|
-
enum: ["unknown", "active", "satisfied", "violated", "resolved"]
|
|
1202
|
-
},
|
|
1203
|
-
contractEnforcement: {
|
|
1204
|
-
type: "string",
|
|
1205
|
-
enum: ["blocking", "advisory", "informational"]
|
|
1206
|
-
},
|
|
1207
|
-
contractGraphEnforcement: { type: "string", enum: ["off", "advisory", "strict"] },
|
|
1208
|
-
contractEdgeId: { type: "string" },
|
|
1209
|
-
contractEdgeType: {
|
|
1210
|
-
type: "string",
|
|
1211
|
-
enum: [
|
|
1212
|
-
"targets",
|
|
1213
|
-
"affects",
|
|
1214
|
-
"must_preserve",
|
|
1215
|
-
"exposes",
|
|
1216
|
-
"verified_by",
|
|
1217
|
-
"conflicts_with",
|
|
1218
|
-
"derived_from",
|
|
1219
|
-
"relates_to"
|
|
1220
|
-
]
|
|
1221
|
-
},
|
|
1222
1330
|
fromNodeId: { type: "string" },
|
|
1223
1331
|
toNodeId: { type: "string" },
|
|
1224
|
-
contractRationale: { type: "string" },
|
|
1225
1332
|
baseline: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
1226
1333
|
threshold: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
1227
1334
|
columnId: { type: "string" },
|
|
@@ -1305,7 +1412,20 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
1305
1412
|
costCeilingUsd: { type: "number" },
|
|
1306
1413
|
retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
|
|
1307
1414
|
lastFailureKind: { type: "string" },
|
|
1308
|
-
dependsOn: {
|
|
1415
|
+
dependsOn: {
|
|
1416
|
+
type: "array",
|
|
1417
|
+
items: { type: "string" },
|
|
1418
|
+
description: "Task ids this card waits on. On update_task an explicit empty array clears them \u2014 use it when a dependency was recorded in error rather than completing work nobody wants."
|
|
1419
|
+
},
|
|
1420
|
+
atomic: {
|
|
1421
|
+
type: "boolean",
|
|
1422
|
+
description: "Composite parent (true) or executable leaf (false). Set false to make a stranded parent a leaf again after its children were dropped."
|
|
1423
|
+
},
|
|
1424
|
+
childTaskIds: {
|
|
1425
|
+
type: "array",
|
|
1426
|
+
items: { type: "string" },
|
|
1427
|
+
description: "Children of a composite parent. On update_task an explicit empty array detaches them all."
|
|
1428
|
+
},
|
|
1309
1429
|
estimatedHours: { type: "number" },
|
|
1310
1430
|
actualHours: { type: "number" },
|
|
1311
1431
|
taskGraph: { type: "object" },
|
|
@@ -1339,6 +1459,74 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
1339
1459
|
checkId: { type: "string" },
|
|
1340
1460
|
checkDescription: { type: "string" },
|
|
1341
1461
|
checkStatus: { type: "string", enum: ["pending", "passed", "failed", "skipped"] },
|
|
1462
|
+
checkType: {
|
|
1463
|
+
type: "string",
|
|
1464
|
+
// Only types a verifier can actually execute. `manual` is the default and
|
|
1465
|
+
// means a human or agent asserts the status by hand. The rest are run by
|
|
1466
|
+
// `verify_completion` against the default deterministic registry. Types
|
|
1467
|
+
// with no plugin in that registry (`auto`, `review`, `agent`, `council`)
|
|
1468
|
+
// are deliberately omitted: offering them would produce criteria that
|
|
1469
|
+
// silently report `skipped — no verifier plugin registered`.
|
|
1470
|
+
enum: ["manual", "command", "test", "file_exists", "file_matches", "git_diff", "metric"],
|
|
1471
|
+
description: 'How this acceptance criterion is verified. Default "manual" (status set by hand). Any other value makes verify_completion execute it, so the criterion becomes real evidence rather than a self-assertion. Pair with checkNotes.'
|
|
1472
|
+
},
|
|
1473
|
+
checkNotes: {
|
|
1474
|
+
type: "string",
|
|
1475
|
+
description: 'The executable body for a non-manual checkType, read in preference to checkDescription. command/test: the shell command or test pattern. file_exists: the path. file_matches: JSON {"file","pattern","flags"}. git_diff: JSON {"expectedFiles","minChanges","maxChanges"}.'
|
|
1476
|
+
},
|
|
1477
|
+
// ── Contract map ───────────────────────────────────────────────────
|
|
1478
|
+
// The card contract: what this work targets, what it must not break, what
|
|
1479
|
+
// it risks, and what verifies it. Advisory by default — the readiness gate
|
|
1480
|
+
// deliberately does not require map structure, so a map is an operator
|
|
1481
|
+
// review aid, not work the model must complete before implementing.
|
|
1482
|
+
contractEnforcement: {
|
|
1483
|
+
type: "string",
|
|
1484
|
+
enum: ["off", "advisory", "strict"],
|
|
1485
|
+
description: "Board-level contract map enforcement. Default when first configured: advisory."
|
|
1486
|
+
},
|
|
1487
|
+
contractNodeId: { type: "string" },
|
|
1488
|
+
contractNodeKind: {
|
|
1489
|
+
type: "string",
|
|
1490
|
+
enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"],
|
|
1491
|
+
description: "objective = what this card is for; guardrail = what must keep working; risk = what could go wrong; component/artifact = what it touches; verification = what settles it."
|
|
1492
|
+
},
|
|
1493
|
+
contractNodeTitle: { type: "string" },
|
|
1494
|
+
contractNodeDescription: { type: "string" },
|
|
1495
|
+
contractNodeState: {
|
|
1496
|
+
type: "string",
|
|
1497
|
+
enum: ["unknown", "active", "satisfied", "violated", "waived", "resolved"]
|
|
1498
|
+
},
|
|
1499
|
+
contractNodeEnforcement: {
|
|
1500
|
+
type: "string",
|
|
1501
|
+
enum: ["blocking", "advisory", "informational"]
|
|
1502
|
+
},
|
|
1503
|
+
/** Bind a node to an acceptance criterion or goal metric already on the task. */
|
|
1504
|
+
contractCheckId: { type: "string" },
|
|
1505
|
+
contractMetricId: { type: "string" },
|
|
1506
|
+
contractWaiverReason: {
|
|
1507
|
+
type: "string",
|
|
1508
|
+
description: 'Required, with an actor, when contractNodeState is "waived".'
|
|
1509
|
+
},
|
|
1510
|
+
contractEdgeId: { type: "string" },
|
|
1511
|
+
contractEdgeFrom: {
|
|
1512
|
+
type: "string",
|
|
1513
|
+
description: 'A contract node id, or a task id (bare or "task:<id>") for the card endpoint.'
|
|
1514
|
+
},
|
|
1515
|
+
contractEdgeTo: { type: "string" },
|
|
1516
|
+
contractEdgeType: {
|
|
1517
|
+
type: "string",
|
|
1518
|
+
enum: [
|
|
1519
|
+
"targets",
|
|
1520
|
+
"affects",
|
|
1521
|
+
"must_preserve",
|
|
1522
|
+
"exposes",
|
|
1523
|
+
"verified_by",
|
|
1524
|
+
"conflicts_with",
|
|
1525
|
+
"derived_from",
|
|
1526
|
+
"relates_to"
|
|
1527
|
+
]
|
|
1528
|
+
},
|
|
1529
|
+
contractEdgeRationale: { type: "string" },
|
|
1342
1530
|
note: { type: "string" },
|
|
1343
1531
|
author: { type: "string" },
|
|
1344
1532
|
url: { type: "string" },
|
|
@@ -1413,8 +1601,14 @@ var kanbanTool = {
|
|
|
1413
1601
|
}
|
|
1414
1602
|
case "create_board": {
|
|
1415
1603
|
if (!input.title) return fail("create_board requires title.");
|
|
1604
|
+
const existing = (await listBoards2(projectRoot)).filter(
|
|
1605
|
+
(candidate) => (candidate.kind ?? "project") === "project"
|
|
1606
|
+
);
|
|
1416
1607
|
const board = await createBoard2(projectRoot, boardCreateInput(input, input.title));
|
|
1417
|
-
|
|
1608
|
+
const note = existing.length ? ` ${existing.length} other project board(s) already exist: ${existing.slice(0, 3).map((candidate) => `"${candidate.title}" (${candidate.taskCount} task(s))`).join(
|
|
1609
|
+
", "
|
|
1610
|
+
)}${existing.length > 3 ? ", \u2026" : ""}. If this work belongs to one of them, add_task there instead and delete this board.` : "";
|
|
1611
|
+
return { ok: true, message: `Board created: ${board.title}.${note}`, board };
|
|
1418
1612
|
}
|
|
1419
1613
|
case "update_board": {
|
|
1420
1614
|
if (!input.boardId) return fail("update_board requires boardId.");
|
|
@@ -1443,6 +1637,20 @@ var kanbanTool = {
|
|
|
1443
1637
|
});
|
|
1444
1638
|
return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
|
|
1445
1639
|
}
|
|
1640
|
+
// Adoption used to be a one-way door: the strict lifecycle carries
|
|
1641
|
+
// acceptance-criteria, verification-report, review-evidence and
|
|
1642
|
+
// one-stage-at-a-time gates, and nothing on the tool surface could
|
|
1643
|
+
// undo it, so a board adopted once kept its ceremony forever. The
|
|
1644
|
+
// gates are worth having where a fleet is supervised; they are not
|
|
1645
|
+
// worth being unable to leave. Cards and columns are untouched.
|
|
1646
|
+
case "release_managed_lifecycle": {
|
|
1647
|
+
if (!input.boardId) return fail("release_managed_lifecycle requires boardId.");
|
|
1648
|
+
const board = await updateBoard2(projectRoot, input.boardId, { lifecycle: null });
|
|
1649
|
+
return board ? okBoard(
|
|
1650
|
+
board,
|
|
1651
|
+
"Managed lifecycle released; the board now tracks work without strict gates."
|
|
1652
|
+
) : fail("Board not found.");
|
|
1653
|
+
}
|
|
1446
1654
|
case "duplicate_board": {
|
|
1447
1655
|
if (!input.boardId) return fail("duplicate_board requires boardId.");
|
|
1448
1656
|
const board = await duplicateBoard(
|
|
@@ -1462,8 +1670,7 @@ var kanbanTool = {
|
|
|
1462
1670
|
const boardInput = createBoardFromText({
|
|
1463
1671
|
description: input.description,
|
|
1464
1672
|
...input.title !== void 0 ? { title: input.title } : {},
|
|
1465
|
-
...input.context !== void 0 ? { context: input.context } : {}
|
|
1466
|
-
...input.columns !== void 0 ? { columns: input.columns } : {}
|
|
1673
|
+
...input.context !== void 0 ? { context: input.context } : {}
|
|
1467
1674
|
});
|
|
1468
1675
|
const board = await createBoard2(projectRoot, boardInput);
|
|
1469
1676
|
for (const taskInput2 of parseLinesIntoTasks(
|
|
@@ -1601,7 +1808,7 @@ var kanbanTool = {
|
|
|
1601
1808
|
return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
|
|
1602
1809
|
}
|
|
1603
1810
|
case "snapshot": {
|
|
1604
|
-
const snapshot = await
|
|
1811
|
+
const snapshot = await getKanbanOrchestrationSnapshot2(projectRoot, {
|
|
1605
1812
|
query: input.query,
|
|
1606
1813
|
boardId: input.boardId,
|
|
1607
1814
|
assignedAgent: input.agentId,
|
|
@@ -1616,33 +1823,6 @@ var kanbanTool = {
|
|
|
1616
1823
|
snapshot
|
|
1617
1824
|
};
|
|
1618
1825
|
}
|
|
1619
|
-
case "add_column": {
|
|
1620
|
-
if (!input.boardId || !input.title)
|
|
1621
|
-
return fail("add_column requires boardId and title.");
|
|
1622
|
-
const result2 = await addColumn(projectRoot, input.boardId, {
|
|
1623
|
-
title: input.title,
|
|
1624
|
-
...input.description !== void 0 ? { description: input.description } : {}
|
|
1625
|
-
});
|
|
1626
|
-
return result2 ? okBoard(result2.board, "Column added.") : fail("Board not found.");
|
|
1627
|
-
}
|
|
1628
|
-
case "update_column": {
|
|
1629
|
-
if (!input.boardId || !input.columnId)
|
|
1630
|
-
return fail("update_column requires boardId and columnId.");
|
|
1631
|
-
const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
|
|
1632
|
-
...input.title !== void 0 ? { title: input.title } : {},
|
|
1633
|
-
...input.description !== void 0 ? { description: input.description } : {},
|
|
1634
|
-
...input.order !== void 0 ? { order: input.order } : {}
|
|
1635
|
-
});
|
|
1636
|
-
return board ? okBoard(board, "Column updated.") : fail("Column not found.");
|
|
1637
|
-
}
|
|
1638
|
-
case "delete_column": {
|
|
1639
|
-
if (!input.boardId || !input.columnId)
|
|
1640
|
-
return fail("delete_column requires boardId and columnId.");
|
|
1641
|
-
const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
|
|
1642
|
-
moveTasksToColumnId: input.moveTasksToColumnId
|
|
1643
|
-
});
|
|
1644
|
-
return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
|
|
1645
|
-
}
|
|
1646
1826
|
case "add_task": {
|
|
1647
1827
|
if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
|
|
1648
1828
|
const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
|
|
@@ -1724,6 +1904,32 @@ var kanbanTool = {
|
|
|
1724
1904
|
`Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
|
|
1725
1905
|
);
|
|
1726
1906
|
}
|
|
1907
|
+
if (board.lifecycle?.mode !== "managed") {
|
|
1908
|
+
const now = /* @__PURE__ */ new Date();
|
|
1909
|
+
const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
|
|
1910
|
+
status: "running",
|
|
1911
|
+
agentId: input.agentId ?? input.author,
|
|
1912
|
+
leaseId: input.leaseId ?? randomUUID2(),
|
|
1913
|
+
claimedAt: input.claimedAt ?? now.toISOString(),
|
|
1914
|
+
heartbeatAt: input.heartbeatAt ?? now.toISOString(),
|
|
1915
|
+
leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
|
|
1916
|
+
attempt: input.attempt ?? 1,
|
|
1917
|
+
maxAttempts: input.maxAttempts ?? 3
|
|
1918
|
+
});
|
|
1919
|
+
if (!assigned) return fail("Task assignment could not be started.");
|
|
1920
|
+
const started = await updateTask2(projectRoot, board.id, task.id, {
|
|
1921
|
+
status: "in_progress"
|
|
1922
|
+
});
|
|
1923
|
+
const current = started ?? assigned;
|
|
1924
|
+
const claimed = task;
|
|
1925
|
+
const currentTask = current.tasks.find((candidate) => candidate.id === claimed.id) ?? claimed;
|
|
1926
|
+
ctx.setCurrentKanbanTask?.(currentTask.id, current.id);
|
|
1927
|
+
return okTask(
|
|
1928
|
+
current,
|
|
1929
|
+
currentTask,
|
|
1930
|
+
"Task is active and bound to this run for attribution. This board is not in managed lifecycle mode, so runtime Kanban governance was not bound to it."
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1727
1933
|
let stage = task.lifecycle?.currentStage;
|
|
1728
1934
|
if (stage === "backlog") {
|
|
1729
1935
|
const moved = await transitionTask(projectRoot, board.id, task.id, {
|
|
@@ -1864,6 +2070,9 @@ var kanbanTool = {
|
|
|
1864
2070
|
if (!input.boardId || !input.taskId)
|
|
1865
2071
|
return fail("delete_task requires boardId and taskId.");
|
|
1866
2072
|
const board = await removeTask(projectRoot, input.boardId, input.taskId);
|
|
2073
|
+
if (board && ctx.currentKanbanTaskId === input.taskId) {
|
|
2074
|
+
ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
|
|
2075
|
+
}
|
|
1867
2076
|
return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
|
|
1868
2077
|
}
|
|
1869
2078
|
case "set_chain": {
|
|
@@ -1996,7 +2205,7 @@ var kanbanTool = {
|
|
|
1996
2205
|
});
|
|
1997
2206
|
} catch (err) {
|
|
1998
2207
|
lifecycleWarnings.push(
|
|
1999
|
-
`Lifecycle transition to Running deferred: ${err instanceof Error ? err.message : String(err)}`
|
|
2208
|
+
`Lifecycle transition to Running deferred: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
|
|
2000
2209
|
);
|
|
2001
2210
|
}
|
|
2002
2211
|
}
|
|
@@ -2020,7 +2229,7 @@ var kanbanTool = {
|
|
|
2020
2229
|
});
|
|
2021
2230
|
} catch (err) {
|
|
2022
2231
|
lifecycleWarnings.push(
|
|
2023
|
-
`Lifecycle transition to Review failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2232
|
+
`Lifecycle transition to Review failed: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
|
|
2024
2233
|
);
|
|
2025
2234
|
}
|
|
2026
2235
|
if (transitionResult) {
|
|
@@ -2040,7 +2249,11 @@ var kanbanTool = {
|
|
|
2040
2249
|
successCriteria: verResult.task.successCriteria
|
|
2041
2250
|
});
|
|
2042
2251
|
const verdict = verResult.report.verdict;
|
|
2043
|
-
if (verdict === "passed") {
|
|
2252
|
+
if (verdict === "passed" && !resolveAutoAccept(board)) {
|
|
2253
|
+
lifecycleWarnings.push(
|
|
2254
|
+
"Verification passed, but this board does not auto-accept. The card is in Review awaiting an explicit transition_task to done."
|
|
2255
|
+
);
|
|
2256
|
+
} else if (verdict === "passed") {
|
|
2044
2257
|
try {
|
|
2045
2258
|
const doneResult = await transitionTask(
|
|
2046
2259
|
projectRoot,
|
|
@@ -2158,11 +2371,34 @@ var kanbanTool = {
|
|
|
2158
2371
|
});
|
|
2159
2372
|
return {
|
|
2160
2373
|
ok: true,
|
|
2161
|
-
message: `Counts:
|
|
2374
|
+
message: `Counts: startable=${health.counts.startable}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
|
|
2162
2375
|
queueHealth: health
|
|
2163
2376
|
};
|
|
2164
2377
|
}
|
|
2378
|
+
// Not every action is handled above. These are dispatched from here,
|
|
2379
|
+
// and the split has already cost real time: an agent that read this
|
|
2380
|
+
// file concluded `add_check` / `update_check` did not exist, wrote
|
|
2381
|
+
// that on a card, and spent a session trying to satisfy a gate it
|
|
2382
|
+
// already had the tool to clear. Keep this index in step with the
|
|
2383
|
+
// handlers.
|
|
2384
|
+
//
|
|
2385
|
+
// kanban-detail-actions.ts workbench · add_dependency ·
|
|
2386
|
+
// add_goal_metric · update_goal_metric · add_check ·
|
|
2387
|
+
// update_check · add_note · add_link · split_atomic
|
|
2388
|
+
// kanban-decomposition-actions.ts verify_completion ·
|
|
2389
|
+
// assess_atomicity · propose_decomposition
|
|
2390
|
+
// kanban-contract-actions.ts get_contract_graph ·
|
|
2391
|
+
// configure_contract_graph · upsert_contract_node ·
|
|
2392
|
+
// remove_contract_node · add_contract_edge · remove_contract_edge
|
|
2165
2393
|
default:
|
|
2394
|
+
{
|
|
2395
|
+
const contractResult = await handleKanbanContractAction(
|
|
2396
|
+
projectRoot,
|
|
2397
|
+
input,
|
|
2398
|
+
input.author ?? input.agentId
|
|
2399
|
+
);
|
|
2400
|
+
if (contractResult !== void 0) return contractResult;
|
|
2401
|
+
}
|
|
2166
2402
|
{
|
|
2167
2403
|
const detailResult = await handleKanbanDetailAction(projectRoot, input);
|
|
2168
2404
|
if (detailResult !== void 0) return detailResult;
|
|
@@ -2172,7 +2408,7 @@ var kanbanTool = {
|
|
|
2172
2408
|
})();
|
|
2173
2409
|
return withPresence(result);
|
|
2174
2410
|
} catch (err) {
|
|
2175
|
-
return fail(err instanceof Error ? err.message : String(err));
|
|
2411
|
+
return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
|
|
2176
2412
|
}
|
|
2177
2413
|
}
|
|
2178
2414
|
};
|
|
@@ -2205,11 +2441,51 @@ function bindTodosToBoard(items, previous, board) {
|
|
|
2205
2441
|
available.find((task2) => !used.has(task2.id) && normalizedTitle(task2.title) === title)
|
|
2206
2442
|
];
|
|
2207
2443
|
const task = candidates.find((candidate) => candidate && !used.has(candidate.id));
|
|
2208
|
-
if (!task)
|
|
2444
|
+
if (!task) {
|
|
2445
|
+
const { blockedBy: _discarded, ...rest } = item;
|
|
2446
|
+
return { ...rest };
|
|
2447
|
+
}
|
|
2209
2448
|
used.add(task.id);
|
|
2210
|
-
|
|
2449
|
+
const blockedBy = blockingTitles(board, task);
|
|
2450
|
+
return {
|
|
2451
|
+
...item,
|
|
2452
|
+
kanbanBoardId: board.id,
|
|
2453
|
+
kanbanTaskId: task.id,
|
|
2454
|
+
...blockedBy.length ? { blockedBy } : { blockedBy: void 0 }
|
|
2455
|
+
};
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
function demoteBlockedInProgress(items, warnings) {
|
|
2459
|
+
return items.map((item) => {
|
|
2460
|
+
if (item.status !== "in_progress" || !item.blockedBy?.length) return item;
|
|
2461
|
+
warnings.push(
|
|
2462
|
+
`"${item.content}" cannot start yet \u2014 it waits on: ${item.blockedBy.join("; ")}. Kept as pending; complete the blocking work first.`
|
|
2463
|
+
);
|
|
2464
|
+
return { ...item, status: "pending" };
|
|
2211
2465
|
});
|
|
2212
2466
|
}
|
|
2467
|
+
async function createMissingManagedCards(items, board, ctx, warnings) {
|
|
2468
|
+
const created = /* @__PURE__ */ new Map();
|
|
2469
|
+
for (const item of items) {
|
|
2470
|
+
if (item.kanbanBoardId === board.id && item.kanbanTaskId) continue;
|
|
2471
|
+
try {
|
|
2472
|
+
const result = await addTask2(ctx.projectRoot, board.id, {
|
|
2473
|
+
title: item.content,
|
|
2474
|
+
description: item.activeForm?.trim() || `Added from the session todo list: ${item.content}`
|
|
2475
|
+
});
|
|
2476
|
+
if (!result) {
|
|
2477
|
+
warnings.push(`Could not open a Kanban card for "${item.content}": board not found.`);
|
|
2478
|
+
continue;
|
|
2479
|
+
}
|
|
2480
|
+
created.set(item.id, result.task.id);
|
|
2481
|
+
} catch (error) {
|
|
2482
|
+
warnings.push(
|
|
2483
|
+
`Could not open a Kanban card for "${item.content}": ${error instanceof Error ? error.message : String(error)}`
|
|
2484
|
+
);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
return created;
|
|
2488
|
+
}
|
|
2213
2489
|
async function synchronizeManagedKanban(items, board, ctx, signal) {
|
|
2214
2490
|
let synced = 0;
|
|
2215
2491
|
const warnings = [];
|
|
@@ -2247,6 +2523,16 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
|
|
|
2247
2523
|
transitionComment: `Todo returned to queue: ${item.content}`
|
|
2248
2524
|
});
|
|
2249
2525
|
}
|
|
2526
|
+
for (const item of items) {
|
|
2527
|
+
if (item.status === "completed" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
|
|
2528
|
+
continue;
|
|
2529
|
+
}
|
|
2530
|
+
const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
|
|
2531
|
+
if (task?.status !== "completed") continue;
|
|
2532
|
+
warnings.push(
|
|
2533
|
+
`"${item.content}" is already Done on the Kanban board and a completed card cannot be reopened; the row stays completed. Create a follow-up card for any remaining work.`
|
|
2534
|
+
);
|
|
2535
|
+
}
|
|
2250
2536
|
for (const item of items) {
|
|
2251
2537
|
if (item.status !== "completed" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
|
|
2252
2538
|
continue;
|
|
@@ -2298,17 +2584,25 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
|
|
|
2298
2584
|
const active = items.find(
|
|
2299
2585
|
(item) => item.status === "in_progress" && item.kanbanBoardId === board.id && Boolean(item.kanbanTaskId)
|
|
2300
2586
|
);
|
|
2587
|
+
const activeStage = active?.kanbanTaskId ? afterCompletions?.tasks.find((task) => task.id === active.kanbanTaskId)?.lifecycle?.currentStage : void 0;
|
|
2301
2588
|
if (active?.kanbanTaskId) {
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2589
|
+
if (activeStage === "review" || activeStage === "done") {
|
|
2590
|
+
warnings.push(
|
|
2591
|
+
`"${active.content}" is in ${activeStage === "review" ? "Review" : "Done"} awaiting acceptance; not re-activating it from the todo list. ` + (activeStage === "review" ? "Call kanban start_task explicitly to reopen it as a repair." : "Done is terminal; reopen only by creating a follow-up card.")
|
|
2592
|
+
);
|
|
2593
|
+
} else {
|
|
2594
|
+
await execute({
|
|
2595
|
+
action: "start_task",
|
|
2596
|
+
boardId: board.id,
|
|
2597
|
+
taskId: active.kanbanTaskId,
|
|
2598
|
+
author: actor,
|
|
2599
|
+
agentId: actor,
|
|
2600
|
+
transitionComment: `Todo activated: ${active.content}`
|
|
2601
|
+
});
|
|
2602
|
+
}
|
|
2310
2603
|
}
|
|
2311
|
-
if (active?.kanbanTaskId &&
|
|
2604
|
+
if (active?.kanbanTaskId && activeStage === "review") {
|
|
2605
|
+
} else if (active?.kanbanTaskId && completionPending) {
|
|
2312
2606
|
warnings.push(
|
|
2313
2607
|
"A completed todo is still awaiting acceptance; the next independent Kanban task was started."
|
|
2314
2608
|
);
|
|
@@ -2395,29 +2689,47 @@ var todoTool = {
|
|
|
2395
2689
|
}
|
|
2396
2690
|
}
|
|
2397
2691
|
const boardId = activeBoardId(items, ctx);
|
|
2398
|
-
|
|
2399
|
-
const
|
|
2692
|
+
let board = boardId ? await getBoard4(ctx.projectRoot, boardId) : null;
|
|
2693
|
+
const managed = board?.lifecycle?.mode === "managed";
|
|
2694
|
+
let boundItems = managed && board ? bindTodosToBoard(items, ctx.todos ?? [], board) : items;
|
|
2695
|
+
const creationWarnings = [];
|
|
2696
|
+
if (managed && board) {
|
|
2697
|
+
const managedBoardId = board.id;
|
|
2698
|
+
const created = await createMissingManagedCards(boundItems, board, ctx, creationWarnings);
|
|
2699
|
+
if (created.size > 0) {
|
|
2700
|
+
boundItems = boundItems.map((item) => {
|
|
2701
|
+
const taskId = created.get(item.id);
|
|
2702
|
+
return taskId ? { ...item, kanbanBoardId: managedBoardId, kanbanTaskId: taskId } : item;
|
|
2703
|
+
});
|
|
2704
|
+
board = await getBoard4(ctx.projectRoot, managedBoardId) ?? board;
|
|
2705
|
+
boundItems = bindTodosToBoard(boundItems, ctx.todos ?? [], board);
|
|
2706
|
+
}
|
|
2707
|
+
boundItems = demoteBlockedInProgress(boundItems, creationWarnings);
|
|
2708
|
+
}
|
|
2400
2709
|
ctx.state.replaceTodos(boundItems);
|
|
2401
|
-
const kanbanSync =
|
|
2402
|
-
|
|
2710
|
+
const kanbanSync = managed && board ? await synchronizeManagedKanban(boundItems, board, ctx, call.signal) : { synced: 0, warnings: [] };
|
|
2711
|
+
kanbanSync.warnings.unshift(...creationWarnings);
|
|
2712
|
+
if (managed && board) {
|
|
2403
2713
|
const unresolved = boundItems.filter(
|
|
2404
2714
|
(item) => item.kanbanBoardId !== board.id || !item.kanbanTaskId
|
|
2405
2715
|
);
|
|
2406
2716
|
if (unresolved.length > 0) {
|
|
2407
2717
|
kanbanSync.warnings.push(
|
|
2408
|
-
`${unresolved.length} Todo row(s)
|
|
2718
|
+
`${unresolved.length} Todo row(s) could not be bound to a Kanban task and were not applied. Preserve kanbanBoardId/kanbanTaskId when updating the projection.`
|
|
2409
2719
|
);
|
|
2410
2720
|
}
|
|
2411
2721
|
}
|
|
2722
|
+
const mirrorFailure = takeSessionMirrorFailure(ctx.projectRoot, ctx.session?.id ?? "");
|
|
2723
|
+
if (mirrorFailure) kanbanSync.warnings.push(mirrorFailure);
|
|
2412
2724
|
let projectedBoard = board;
|
|
2413
|
-
if (
|
|
2725
|
+
if (managed && board) {
|
|
2414
2726
|
const refreshed = await getBoard4(ctx.projectRoot, board.id);
|
|
2415
2727
|
if (refreshed) {
|
|
2416
2728
|
projectedBoard = refreshed;
|
|
2417
2729
|
applyManagedKanbanBoardToTodos(ctx, refreshed);
|
|
2418
2730
|
}
|
|
2419
2731
|
}
|
|
2420
|
-
if (
|
|
2732
|
+
if (!managed) {
|
|
2421
2733
|
mirrorSessionTodosToKanban(ctx.projectRoot, items, ctx.session?.id ?? "session");
|
|
2422
2734
|
}
|
|
2423
2735
|
const completedPlanIds = /* @__PURE__ */ new Set();
|