@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/task.js
CHANGED
|
@@ -12,12 +12,17 @@ import {
|
|
|
12
12
|
mutateTasks
|
|
13
13
|
} from "@wrongstack/core/storage";
|
|
14
14
|
import { deserializeTaskGraph } from "@wrongstack/core/tasking";
|
|
15
|
-
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
15
|
+
import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
|
|
16
16
|
import {
|
|
17
17
|
bridgeKanbanSupervisor,
|
|
18
|
+
compactSessionMirrorBoard,
|
|
18
19
|
createBoard,
|
|
20
|
+
DEFAULT_COLUMNS,
|
|
19
21
|
getBoard,
|
|
22
|
+
getDependencyReadinessIssues,
|
|
23
|
+
getKanbanOrchestrationSnapshot,
|
|
20
24
|
listBoards,
|
|
25
|
+
pruneSessionBoards,
|
|
21
26
|
removeBoard,
|
|
22
27
|
syncBoardFromTaskGraph,
|
|
23
28
|
touchKanbanPresence,
|
|
@@ -25,16 +30,14 @@ import {
|
|
|
25
30
|
} from "@wrongstack/kanban";
|
|
26
31
|
var SESSION_BOARD_TAG = "session-work";
|
|
27
32
|
var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
|
|
28
|
-
var SESSION_KANBAN_COLUMNS =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{ id: "review", title: "Preview", order: 2, wipLimit: 0, color: "#7c3aed" },
|
|
32
|
-
{ id: "done", title: "Done", order: 3, wipLimit: 0, color: "#16a34a" }
|
|
33
|
-
];
|
|
33
|
+
var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
|
|
34
|
+
...column
|
|
35
|
+
}));
|
|
34
36
|
var boardQueue = /* @__PURE__ */ new Map();
|
|
35
37
|
var boardEnsures = /* @__PURE__ */ new Map();
|
|
36
38
|
var pendingMirrors = /* @__PURE__ */ new Map();
|
|
37
39
|
var activeMirrors = /* @__PURE__ */ new Set();
|
|
40
|
+
var mirrorFailures = /* @__PURE__ */ new Map();
|
|
38
41
|
var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
|
|
39
42
|
function boardKey(projectRoot, sessionId) {
|
|
40
43
|
return `${projectRoot}\0${sessionId}`;
|
|
@@ -42,6 +45,33 @@ function boardKey(projectRoot, sessionId) {
|
|
|
42
45
|
function mirrorKey(projectRoot, sessionId, sourceSystem) {
|
|
43
46
|
return `${boardKey(projectRoot, sessionId)}\0${sourceSystem}`;
|
|
44
47
|
}
|
|
48
|
+
function completedReconciliationGraph(latest, candidates) {
|
|
49
|
+
const latestNodeIds = new Set(latest.nodes.map((node) => node.id));
|
|
50
|
+
const carriedNodeIds = /* @__PURE__ */ new Set();
|
|
51
|
+
const completedNodes = candidates.flatMap(
|
|
52
|
+
(candidate) => candidate.nodes.filter((node) => {
|
|
53
|
+
if (node.status !== "completed" || latestNodeIds.has(node.id) || carriedNodeIds.has(node.id)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
carriedNodeIds.add(node.id);
|
|
57
|
+
return true;
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
if (completedNodes.length === 0) return void 0;
|
|
61
|
+
const carriedRequirements = completedNodes.flatMap(
|
|
62
|
+
(node) => node.specRequirementId ? [node.specRequirementId] : []
|
|
63
|
+
);
|
|
64
|
+
return {
|
|
65
|
+
...latest,
|
|
66
|
+
nodes: [...latest.nodes, ...completedNodes],
|
|
67
|
+
rootNodes: [.../* @__PURE__ */ new Set([...latest.rootNodes, ...completedNodes.map((node) => node.id)])],
|
|
68
|
+
...latest.requiredRequirementIds ? {
|
|
69
|
+
requiredRequirementIds: [
|
|
70
|
+
.../* @__PURE__ */ new Set([...latest.requiredRequirementIds, ...carriedRequirements])
|
|
71
|
+
]
|
|
72
|
+
} : {}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
45
75
|
function sessionTag(sessionId) {
|
|
46
76
|
return `session:${sessionId}`;
|
|
47
77
|
}
|
|
@@ -120,16 +150,44 @@ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
|
|
|
120
150
|
sourceSystem,
|
|
121
151
|
tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
|
|
122
152
|
archiveMissingTasks: true,
|
|
123
|
-
includeCompletedTasks: true
|
|
153
|
+
includeCompletedTasks: true,
|
|
154
|
+
// The scope ledger stays declared and accurate, but it may not veto a
|
|
155
|
+
// projection. A session mirror reflects a tactical list that shrinks by
|
|
156
|
+
// design, and refusing the sync never protected the removed row — it
|
|
157
|
+
// froze the entire board, permanently, because the stored scope then
|
|
158
|
+
// outlived every later snapshot (`session-kanban.mirror-failed`).
|
|
159
|
+
// Nothing is lost by shrinking here: `archiveMissingTasks` keeps the
|
|
160
|
+
// removed card on the board as `archived`, the reconciliation pass
|
|
161
|
+
// first walks vanished completed rows to Done, and the session journal
|
|
162
|
+
// remains the durable record.
|
|
163
|
+
allowRequirementScopeShrink: true
|
|
124
164
|
}
|
|
125
165
|
);
|
|
126
|
-
|
|
166
|
+
if (!result) return null;
|
|
167
|
+
const compacted = await compactSessionMirrorBoard(projectRoot, board.id);
|
|
168
|
+
if (compacted?.removedTaskIds.length) {
|
|
169
|
+
return await getBoard(projectRoot, board.id) ?? result.board;
|
|
170
|
+
}
|
|
171
|
+
return result.board;
|
|
127
172
|
});
|
|
128
173
|
}
|
|
129
174
|
function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
|
|
130
175
|
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return;
|
|
131
176
|
const key = mirrorKey(projectRoot, sessionId, sourceSystem);
|
|
132
|
-
pendingMirrors.
|
|
177
|
+
const previous = pendingMirrors.get(key);
|
|
178
|
+
const reconciliationGraph = previous ? completedReconciliationGraph(
|
|
179
|
+
graph,
|
|
180
|
+
[previous.reconciliationGraph, previous.graph].filter(
|
|
181
|
+
(candidate) => candidate !== void 0
|
|
182
|
+
)
|
|
183
|
+
) : void 0;
|
|
184
|
+
pendingMirrors.set(key, {
|
|
185
|
+
projectRoot,
|
|
186
|
+
sessionId,
|
|
187
|
+
graph,
|
|
188
|
+
...reconciliationGraph ? { reconciliationGraph } : {},
|
|
189
|
+
sourceSystem
|
|
190
|
+
});
|
|
133
191
|
if (activeMirrors.has(key)) return;
|
|
134
192
|
activeMirrors.add(key);
|
|
135
193
|
void (async () => {
|
|
@@ -139,20 +197,34 @@ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
|
|
|
139
197
|
if (!pending) break;
|
|
140
198
|
pendingMirrors.delete(key);
|
|
141
199
|
try {
|
|
200
|
+
if (pending.reconciliationGraph) {
|
|
201
|
+
await projectGraph(
|
|
202
|
+
pending.projectRoot,
|
|
203
|
+
pending.sessionId,
|
|
204
|
+
pending.reconciliationGraph,
|
|
205
|
+
pending.sourceSystem
|
|
206
|
+
);
|
|
207
|
+
}
|
|
142
208
|
await projectGraph(
|
|
143
209
|
pending.projectRoot,
|
|
144
210
|
pending.sessionId,
|
|
145
211
|
pending.graph,
|
|
146
212
|
pending.sourceSystem
|
|
147
213
|
);
|
|
214
|
+
mirrorFailures.delete(boardKey(pending.projectRoot, pending.sessionId));
|
|
148
215
|
} catch (error) {
|
|
216
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
217
|
+
mirrorFailures.set(boardKey(pending.projectRoot, pending.sessionId), {
|
|
218
|
+
message,
|
|
219
|
+
sourceSystem: pending.sourceSystem
|
|
220
|
+
});
|
|
149
221
|
console.warn(
|
|
150
222
|
JSON.stringify({
|
|
151
223
|
level: "warn",
|
|
152
224
|
event: "session-kanban.mirror-failed",
|
|
153
225
|
sessionId: pending.sessionId,
|
|
154
226
|
sourceSystem: pending.sourceSystem,
|
|
155
|
-
message
|
|
227
|
+
message,
|
|
156
228
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
157
229
|
})
|
|
158
230
|
);
|
|
@@ -173,6 +245,14 @@ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
|
|
|
173
245
|
}
|
|
174
246
|
})();
|
|
175
247
|
}
|
|
248
|
+
function takeSessionMirrorFailure(projectRoot, sessionId) {
|
|
249
|
+
if (!projectRoot || !sessionId) return void 0;
|
|
250
|
+
const key = boardKey(projectRoot, sessionId);
|
|
251
|
+
const failure = mirrorFailures.get(key);
|
|
252
|
+
if (!failure) return void 0;
|
|
253
|
+
mirrorFailures.delete(key);
|
|
254
|
+
return `Kanban mirror (${failure.sourceSystem}) failed and the board may be stale: ${failure.message}`;
|
|
255
|
+
}
|
|
176
256
|
function todoListToSerializedGraph(todos, sessionId) {
|
|
177
257
|
const graphId = `todo:${sessionId}`;
|
|
178
258
|
const nodes = todos.map((todo, index) => ({
|
|
@@ -266,11 +346,11 @@ function broadcastTodoUpdate(context, todos) {
|
|
|
266
346
|
});
|
|
267
347
|
}
|
|
268
348
|
function notifyTodoUpdate(context, todos) {
|
|
269
|
-
const summary = todos
|
|
349
|
+
const summary = formatTodosForModel(todos);
|
|
270
350
|
const text = `[KANBAN TODO UPDATE]
|
|
271
351
|
Another Kanban agent reassessed the shared board. The canonical todo list is now:
|
|
272
352
|
${summary}
|
|
273
|
-
Reassess your current plan before continuing; do not rely on the initial todo snapshot.`;
|
|
353
|
+
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.`;
|
|
274
354
|
const state = context.state;
|
|
275
355
|
if (typeof state.appendBlockToLastUserMessage === "function") {
|
|
276
356
|
if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
|
|
@@ -309,26 +389,68 @@ function todoStatus(task) {
|
|
|
309
389
|
if (status === "in_progress" || status === "review") return "in_progress";
|
|
310
390
|
return "pending";
|
|
311
391
|
}
|
|
312
|
-
function sessionTodoFromTask(task,
|
|
392
|
+
function sessionTodoFromTask(task, board) {
|
|
393
|
+
const blockedBy = board ? blockingTitles(board, task) : [];
|
|
313
394
|
return {
|
|
314
395
|
id: task.origin?.taskId ?? task.id,
|
|
315
396
|
content: task.title,
|
|
316
397
|
status: todoStatus(task),
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
...task.description ? { activeForm: task.description } : {}
|
|
398
|
+
...task.description ? { activeForm: task.description } : {},
|
|
399
|
+
...blockedBy.length ? { blockedBy } : {}
|
|
320
400
|
};
|
|
321
401
|
}
|
|
322
|
-
function managedTodoFromTask(task,
|
|
402
|
+
function managedTodoFromTask(task, board) {
|
|
323
403
|
return {
|
|
324
|
-
...sessionTodoFromTask(task,
|
|
325
|
-
|
|
404
|
+
...sessionTodoFromTask(task, board),
|
|
405
|
+
kanbanBoardId: board.id,
|
|
406
|
+
kanbanTaskId: task.id
|
|
326
407
|
};
|
|
327
408
|
}
|
|
409
|
+
function blockingTitles(board, task) {
|
|
410
|
+
return getDependencyReadinessIssues(board, task).map((issue) => {
|
|
411
|
+
const dependency = board.tasks.find((candidate) => candidate.id === issue.dependencyId);
|
|
412
|
+
if (!dependency) return `${issue.dependencyId} (missing)`;
|
|
413
|
+
return dependency.title;
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
var PRIORITY_ORDER = {
|
|
417
|
+
critical: 0,
|
|
418
|
+
high: 1,
|
|
419
|
+
medium: 2,
|
|
420
|
+
low: 3
|
|
421
|
+
};
|
|
422
|
+
function orderTasksForTodos(board, tasks) {
|
|
423
|
+
const columnOrder = new Map(board.columns.map((column) => [column.id, column.order]));
|
|
424
|
+
const baseline = [...tasks].sort(
|
|
425
|
+
(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)
|
|
426
|
+
);
|
|
427
|
+
const included = new Set(baseline.map((task) => task.id));
|
|
428
|
+
const remaining = new Map(baseline.map((task) => [task.id, task]));
|
|
429
|
+
const emitted = [];
|
|
430
|
+
const done = /* @__PURE__ */ new Set();
|
|
431
|
+
while (remaining.size > 0) {
|
|
432
|
+
const ready = baseline.filter(
|
|
433
|
+
(task) => remaining.has(task.id) && (task.dependsOn ?? []).every(
|
|
434
|
+
(dependencyId) => !included.has(dependencyId) || done.has(dependencyId)
|
|
435
|
+
)
|
|
436
|
+
);
|
|
437
|
+
if (ready.length === 0) break;
|
|
438
|
+
for (const task of ready) {
|
|
439
|
+
remaining.delete(task.id);
|
|
440
|
+
done.add(task.id);
|
|
441
|
+
emitted.push(task);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
for (const task of baseline) if (remaining.has(task.id)) emitted.push(task);
|
|
445
|
+
return emitted;
|
|
446
|
+
}
|
|
328
447
|
function sameTodos(left, right) {
|
|
329
448
|
return left.length === right.length && left.every((todo, index) => {
|
|
330
449
|
const candidate = right[index];
|
|
331
|
-
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
|
|
450
|
+
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,
|
|
451
|
+
// the rows are otherwise identical and the unblocking would never
|
|
452
|
+
// reach the model.
|
|
453
|
+
(candidate.blockedBy ?? []).join("\0") === (todo.blockedBy ?? []).join("\0");
|
|
332
454
|
});
|
|
333
455
|
}
|
|
334
456
|
function applyManagedKanbanBoardToTodos(context, board) {
|
|
@@ -338,11 +460,12 @@ function applyManagedKanbanBoardToTodos(context, board) {
|
|
|
338
460
|
if (!activeBoardId2 || board.id !== activeBoardId2 || board.lifecycle?.mode !== "managed") {
|
|
339
461
|
return [...context.todos];
|
|
340
462
|
}
|
|
341
|
-
const projectedTodos =
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
463
|
+
const projectedTodos = orderTasksForTodos(
|
|
464
|
+
board,
|
|
465
|
+
board.tasks.filter(
|
|
466
|
+
(task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
|
|
467
|
+
)
|
|
468
|
+
).map((task) => managedTodoFromTask(task, board));
|
|
346
469
|
if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
|
|
347
470
|
suppressedTodoMirrors.add(context);
|
|
348
471
|
try {
|
|
@@ -363,14 +486,13 @@ import {
|
|
|
363
486
|
saveTasks,
|
|
364
487
|
setPlanItemStatus
|
|
365
488
|
} from "@wrongstack/core/storage";
|
|
366
|
-
import { getBoard as getBoard4 } from "@wrongstack/kanban";
|
|
489
|
+
import { addTask as addTask2, getBoard as getBoard4 } from "@wrongstack/kanban";
|
|
367
490
|
|
|
368
491
|
// src/kanban.ts
|
|
369
492
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
370
493
|
import { loadTasks as loadTasks2 } from "@wrongstack/core/storage";
|
|
371
494
|
import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
|
|
372
495
|
import {
|
|
373
|
-
addColumn,
|
|
374
496
|
addTask,
|
|
375
497
|
adoptManagedLifecycle,
|
|
376
498
|
assignTask,
|
|
@@ -385,7 +507,7 @@ import {
|
|
|
385
507
|
exportBoardToTaskGraph,
|
|
386
508
|
finalizeTaskCompletion,
|
|
387
509
|
getBoard as getBoard3,
|
|
388
|
-
getKanbanOrchestrationSnapshot,
|
|
510
|
+
getKanbanOrchestrationSnapshot as getKanbanOrchestrationSnapshot2,
|
|
389
511
|
getKanbanQueueHealth,
|
|
390
512
|
getTask,
|
|
391
513
|
getTaskChain,
|
|
@@ -399,16 +521,16 @@ import {
|
|
|
399
521
|
recoverStaleTaskAssignments,
|
|
400
522
|
releaseTaskClaim,
|
|
401
523
|
removeBoard as removeBoard2,
|
|
402
|
-
removeColumn,
|
|
403
524
|
removeTask,
|
|
404
525
|
repairManagedTaskProjection,
|
|
526
|
+
resolveAutoAccept,
|
|
405
527
|
searchKanban,
|
|
406
528
|
setTaskChain,
|
|
529
|
+
stripLifecycleIssues,
|
|
407
530
|
syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
|
|
408
531
|
transferTaskToBoard,
|
|
409
532
|
transitionTask,
|
|
410
533
|
updateBoard as updateBoard2,
|
|
411
|
-
updateColumn,
|
|
412
534
|
updateTask as updateTask2,
|
|
413
535
|
updateTaskAssignment,
|
|
414
536
|
verifyTaskCompletion as verifyTaskCompletion2
|
|
@@ -458,6 +580,137 @@ function duplicateBoardOptions(input) {
|
|
|
458
580
|
};
|
|
459
581
|
}
|
|
460
582
|
|
|
583
|
+
// src/kanban-contract-actions.ts
|
|
584
|
+
import {
|
|
585
|
+
addContractEdge,
|
|
586
|
+
configureContractGraph,
|
|
587
|
+
evaluateTaskContractGraph,
|
|
588
|
+
getContractGraph,
|
|
589
|
+
removeContractEdge,
|
|
590
|
+
removeContractNode,
|
|
591
|
+
upsertContractNode
|
|
592
|
+
} from "@wrongstack/kanban";
|
|
593
|
+
|
|
594
|
+
// src/kanban-tool-results.ts
|
|
595
|
+
function atomicityNudge(task) {
|
|
596
|
+
if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
|
|
597
|
+
const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
|
|
598
|
+
return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
|
|
599
|
+
}
|
|
600
|
+
function readEnvGateEnforcement() {
|
|
601
|
+
const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
|
|
602
|
+
return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
|
|
603
|
+
}
|
|
604
|
+
function fail(message) {
|
|
605
|
+
return { ok: false, message };
|
|
606
|
+
}
|
|
607
|
+
function okBoard(board, message = "Board loaded.") {
|
|
608
|
+
return { ok: true, message, board };
|
|
609
|
+
}
|
|
610
|
+
function okTask(board, task, message) {
|
|
611
|
+
return { ok: true, message, board, task };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// src/kanban-contract-actions.ts
|
|
615
|
+
async function handleKanbanContractAction(projectRoot, input, actor) {
|
|
616
|
+
switch (input.action) {
|
|
617
|
+
case "get_contract_graph": {
|
|
618
|
+
if (!input.boardId) return fail("get_contract_graph requires boardId.");
|
|
619
|
+
const found = await getContractGraph(projectRoot, input.boardId);
|
|
620
|
+
if (!found) return fail("Board not found.");
|
|
621
|
+
const evaluated = input.taskId ? await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId) : null;
|
|
622
|
+
if (input.taskId && !evaluated) return fail("Task not found on this board.");
|
|
623
|
+
return {
|
|
624
|
+
ok: true,
|
|
625
|
+
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.",
|
|
626
|
+
board: found.board,
|
|
627
|
+
contractGraph: found.graph,
|
|
628
|
+
...evaluated ? { contractEvaluation: evaluated.evaluation } : {}
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
case "configure_contract_graph": {
|
|
632
|
+
if (!input.boardId) return fail("configure_contract_graph requires boardId.");
|
|
633
|
+
const enforcement = input.contractEnforcement ?? "advisory";
|
|
634
|
+
const board = await configureContractGraph(projectRoot, input.boardId, enforcement);
|
|
635
|
+
return board ? okBoard(board, `Contract map enforcement set to ${enforcement}.`) : fail("Board not found.");
|
|
636
|
+
}
|
|
637
|
+
case "upsert_contract_node": {
|
|
638
|
+
if (!input.boardId || !input.taskId) {
|
|
639
|
+
return fail("upsert_contract_node requires boardId and taskId.");
|
|
640
|
+
}
|
|
641
|
+
if (!input.contractNodeKind || !input.contractNodeTitle) {
|
|
642
|
+
return fail("upsert_contract_node requires contractNodeKind and contractNodeTitle.");
|
|
643
|
+
}
|
|
644
|
+
const waiver = input.contractNodeState === "waived" ? {
|
|
645
|
+
actor: actor ?? "agent",
|
|
646
|
+
reason: input.contractWaiverReason ?? "",
|
|
647
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
648
|
+
} : void 0;
|
|
649
|
+
if (waiver && !waiver.reason.trim()) {
|
|
650
|
+
return fail("A waived contract node requires contractWaiverReason.");
|
|
651
|
+
}
|
|
652
|
+
const result = await upsertContractNode(projectRoot, input.boardId, {
|
|
653
|
+
taskId: input.taskId,
|
|
654
|
+
kind: input.contractNodeKind,
|
|
655
|
+
title: input.contractNodeTitle,
|
|
656
|
+
...input.contractNodeId !== void 0 ? { id: input.contractNodeId } : {},
|
|
657
|
+
...input.contractNodeDescription !== void 0 ? { description: input.contractNodeDescription } : {},
|
|
658
|
+
...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
|
|
659
|
+
...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
|
|
660
|
+
...input.contractCheckId !== void 0 ? { checkId: input.contractCheckId } : {},
|
|
661
|
+
...input.contractMetricId !== void 0 ? { metricId: input.contractMetricId } : {},
|
|
662
|
+
...waiver ? { waiver } : {},
|
|
663
|
+
...actor !== void 0 ? { createdBy: actor } : {}
|
|
664
|
+
});
|
|
665
|
+
if (!result) return fail("Board or task not found.");
|
|
666
|
+
return {
|
|
667
|
+
ok: true,
|
|
668
|
+
message: `Contract node ${result.node.kind} "${result.node.title}" saved (${result.node.id}).`,
|
|
669
|
+
board: result.board,
|
|
670
|
+
contractGraph: result.board.contractGraph ?? null
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
case "remove_contract_node": {
|
|
674
|
+
if (!input.boardId || !input.contractNodeId) {
|
|
675
|
+
return fail("remove_contract_node requires boardId and contractNodeId.");
|
|
676
|
+
}
|
|
677
|
+
const board = await removeContractNode(projectRoot, input.boardId, input.contractNodeId);
|
|
678
|
+
return board ? okBoard(board, "Contract node removed, along with every edge that touched it.") : fail("Contract node not found.");
|
|
679
|
+
}
|
|
680
|
+
case "add_contract_edge": {
|
|
681
|
+
if (!input.boardId || !input.contractEdgeFrom || !input.contractEdgeTo) {
|
|
682
|
+
return fail("add_contract_edge requires boardId, contractEdgeFrom, and contractEdgeTo.");
|
|
683
|
+
}
|
|
684
|
+
if (!input.contractEdgeType) return fail("add_contract_edge requires contractEdgeType.");
|
|
685
|
+
const result = await addContractEdge(projectRoot, input.boardId, {
|
|
686
|
+
from: input.contractEdgeFrom,
|
|
687
|
+
to: input.contractEdgeTo,
|
|
688
|
+
type: input.contractEdgeType,
|
|
689
|
+
...input.contractEdgeId !== void 0 ? { id: input.contractEdgeId } : {},
|
|
690
|
+
...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
|
|
691
|
+
...input.contractEdgeRationale !== void 0 ? { rationale: input.contractEdgeRationale } : {},
|
|
692
|
+
...actor !== void 0 ? { createdBy: actor } : {}
|
|
693
|
+
});
|
|
694
|
+
if (!result) return fail("Board not found.");
|
|
695
|
+
return {
|
|
696
|
+
ok: true,
|
|
697
|
+
message: `Contract edge ${result.edge.type}: ${result.edge.from} \u2192 ${result.edge.to}.`,
|
|
698
|
+
board: result.board,
|
|
699
|
+
contractGraph: result.board.contractGraph ?? null
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
case "remove_contract_edge": {
|
|
703
|
+
if (!input.boardId || !input.contractEdgeId) {
|
|
704
|
+
return fail("remove_contract_edge requires boardId and contractEdgeId.");
|
|
705
|
+
}
|
|
706
|
+
const board = await removeContractEdge(projectRoot, input.boardId, input.contractEdgeId);
|
|
707
|
+
return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
|
|
708
|
+
}
|
|
709
|
+
default:
|
|
710
|
+
return void 0;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
461
714
|
// src/kanban-decomposition-actions.ts
|
|
462
715
|
import {
|
|
463
716
|
assessTaskAtomicity,
|
|
@@ -489,26 +742,6 @@ function recordKanbanVerificationEvidence(ctx, report) {
|
|
|
489
742
|
}
|
|
490
743
|
}
|
|
491
744
|
|
|
492
|
-
// src/kanban-tool-results.ts
|
|
493
|
-
function atomicityNudge(task) {
|
|
494
|
-
if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
|
|
495
|
-
const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
|
|
496
|
-
return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
|
|
497
|
-
}
|
|
498
|
-
function readEnvGateEnforcement() {
|
|
499
|
-
const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
|
|
500
|
-
return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
|
|
501
|
-
}
|
|
502
|
-
function fail(message) {
|
|
503
|
-
return { ok: false, message };
|
|
504
|
-
}
|
|
505
|
-
function okBoard(board, message = "Board loaded.") {
|
|
506
|
-
return { ok: true, message, board };
|
|
507
|
-
}
|
|
508
|
-
function okTask(board, task, message) {
|
|
509
|
-
return { ok: true, message, board, task };
|
|
510
|
-
}
|
|
511
|
-
|
|
512
745
|
// src/kanban-decomposition-actions.ts
|
|
513
746
|
async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
|
|
514
747
|
switch (input.action) {
|
|
@@ -593,20 +826,14 @@ async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
|
|
|
593
826
|
// src/kanban-detail-actions.ts
|
|
594
827
|
import {
|
|
595
828
|
addCheckToTask,
|
|
596
|
-
addContractEdge,
|
|
597
829
|
addDependency,
|
|
598
830
|
addGoalMetricToTask,
|
|
599
831
|
addLinkToTask,
|
|
600
832
|
addNoteToTask,
|
|
601
|
-
configureContractGraph,
|
|
602
|
-
evaluateTaskContractGraph,
|
|
603
|
-
getContractGraph,
|
|
604
833
|
getKanbanWorkbench,
|
|
605
|
-
|
|
606
|
-
removeContractNode,
|
|
834
|
+
removeCheckFromTask,
|
|
607
835
|
updateCheckOnTask,
|
|
608
|
-
updateGoalMetricOnTask
|
|
609
|
-
upsertContractNode
|
|
836
|
+
updateGoalMetricOnTask
|
|
610
837
|
} from "@wrongstack/kanban";
|
|
611
838
|
|
|
612
839
|
// src/kanban-split-task-handler.ts
|
|
@@ -672,131 +899,6 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
672
899
|
workbench
|
|
673
900
|
};
|
|
674
901
|
}
|
|
675
|
-
case "get_contract_graph": {
|
|
676
|
-
if (!input.boardId) return fail("get_contract_graph requires boardId.");
|
|
677
|
-
const result = await getContractGraph(projectRoot, input.boardId);
|
|
678
|
-
return result ? {
|
|
679
|
-
ok: true,
|
|
680
|
-
message: result.graph ? `${result.graph.nodes.length} contract node(s), ${result.graph.edges.length} edge(s).` : "Contract graph is not configured.",
|
|
681
|
-
board: result.board,
|
|
682
|
-
...result.graph ? { contractGraph: result.graph } : {}
|
|
683
|
-
} : fail("Board not found.");
|
|
684
|
-
}
|
|
685
|
-
case "configure_contract_graph": {
|
|
686
|
-
if (!input.boardId || !input.contractGraphEnforcement) {
|
|
687
|
-
return fail("configure_contract_graph requires boardId and contractGraphEnforcement.");
|
|
688
|
-
}
|
|
689
|
-
const current = await getContractGraph(projectRoot, input.boardId);
|
|
690
|
-
if (!current) return fail("Board not found.");
|
|
691
|
-
if (input.contractGraphEnforcement === "strict" && current.graph?.enforcement !== "strict") {
|
|
692
|
-
return fail(
|
|
693
|
-
"Strict Contract Map enforcement is operator-owned. Autonomous agents may use advisory maps but may not turn them into an execution gate."
|
|
694
|
-
);
|
|
695
|
-
}
|
|
696
|
-
if (current.graph?.enforcement === "strict" && input.contractGraphEnforcement !== "strict") {
|
|
697
|
-
return fail("An autonomous agent may not loosen a strict contract graph.");
|
|
698
|
-
}
|
|
699
|
-
const board = await configureContractGraph(
|
|
700
|
-
projectRoot,
|
|
701
|
-
input.boardId,
|
|
702
|
-
input.contractGraphEnforcement
|
|
703
|
-
);
|
|
704
|
-
return board ? okBoard(board, "Contract graph configured.") : fail("Board not found.");
|
|
705
|
-
}
|
|
706
|
-
case "upsert_contract_node": {
|
|
707
|
-
if (!input.boardId || !input.taskId || !input.contractNodeKind || !input.title) {
|
|
708
|
-
return fail("upsert_contract_node requires boardId, taskId, contractNodeKind, and title.");
|
|
709
|
-
}
|
|
710
|
-
if (input.contractNodeState === "waived") {
|
|
711
|
-
return fail(
|
|
712
|
-
"The autonomous kanban tool may not waive contract nodes; a human-owned review surface must record that exception."
|
|
713
|
-
);
|
|
714
|
-
}
|
|
715
|
-
if (input.contractNodeId) {
|
|
716
|
-
const current = await getContractGraph(projectRoot, input.boardId);
|
|
717
|
-
const existing = current?.graph?.nodes.find((node) => node.id === input.contractNodeId);
|
|
718
|
-
if (current?.graph?.enforcement === "strict" && existing && (existing.kind !== input.contractNodeKind || input.contractEnforcement !== void 0 && input.contractEnforcement !== existing.enforcement)) {
|
|
719
|
-
return fail(
|
|
720
|
-
"The autonomous kanban tool may not change the kind or enforcement of an existing strict contract node."
|
|
721
|
-
);
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
const result = await upsertContractNode(projectRoot, input.boardId, {
|
|
725
|
-
...input.contractNodeId ? { id: input.contractNodeId } : {},
|
|
726
|
-
taskId: input.taskId,
|
|
727
|
-
kind: input.contractNodeKind,
|
|
728
|
-
title: input.title,
|
|
729
|
-
...input.description !== void 0 ? { description: input.description } : {},
|
|
730
|
-
...input.contractEnforcement !== void 0 ? { enforcement: input.contractEnforcement } : {},
|
|
731
|
-
...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
|
|
732
|
-
...input.checkId !== void 0 ? { checkId: input.checkId } : {},
|
|
733
|
-
...input.metricId !== void 0 ? { metricId: input.metricId } : {},
|
|
734
|
-
...input.baseline !== void 0 ? { baseline: input.baseline } : {},
|
|
735
|
-
...input.threshold !== void 0 ? { threshold: input.threshold } : {},
|
|
736
|
-
...input.author !== void 0 ? { createdBy: input.author } : {}
|
|
737
|
-
});
|
|
738
|
-
return result ? {
|
|
739
|
-
...okBoard(result.board, "Contract node saved."),
|
|
740
|
-
contractGraph: result.board.contractGraph
|
|
741
|
-
} : fail("Task not found.");
|
|
742
|
-
}
|
|
743
|
-
case "link_contract_nodes": {
|
|
744
|
-
if (!input.boardId || !input.fromNodeId || !input.toNodeId || !input.contractEdgeType) {
|
|
745
|
-
return fail(
|
|
746
|
-
"link_contract_nodes requires boardId, fromNodeId, toNodeId, and contractEdgeType."
|
|
747
|
-
);
|
|
748
|
-
}
|
|
749
|
-
const result = await addContractEdge(projectRoot, input.boardId, {
|
|
750
|
-
from: input.fromNodeId,
|
|
751
|
-
to: input.toNodeId,
|
|
752
|
-
type: input.contractEdgeType,
|
|
753
|
-
...input.contractEdgeId ? { id: input.contractEdgeId } : {},
|
|
754
|
-
...input.contractEnforcement ? { enforcement: input.contractEnforcement } : {},
|
|
755
|
-
...input.contractRationale ? { rationale: input.contractRationale } : {},
|
|
756
|
-
...input.author ? { createdBy: input.author } : {}
|
|
757
|
-
});
|
|
758
|
-
return result ? {
|
|
759
|
-
...okBoard(result.board, "Contract edge added."),
|
|
760
|
-
contractGraph: result.board.contractGraph
|
|
761
|
-
} : fail("Board not found.");
|
|
762
|
-
}
|
|
763
|
-
case "remove_contract_node": {
|
|
764
|
-
if (!input.boardId || !input.contractNodeId) {
|
|
765
|
-
return fail("remove_contract_node requires boardId and contractNodeId.");
|
|
766
|
-
}
|
|
767
|
-
const current = await getContractGraph(projectRoot, input.boardId);
|
|
768
|
-
const node = current?.graph?.nodes.find((candidate) => candidate.id === input.contractNodeId);
|
|
769
|
-
if (current?.graph?.enforcement === "strict" && node?.enforcement === "blocking") {
|
|
770
|
-
return fail("The autonomous kanban tool may not remove a blocking strict contract node.");
|
|
771
|
-
}
|
|
772
|
-
const board = await removeContractNode(projectRoot, input.boardId, input.contractNodeId);
|
|
773
|
-
return board ? okBoard(board, "Contract node removed.") : fail("Contract node not found.");
|
|
774
|
-
}
|
|
775
|
-
case "remove_contract_edge": {
|
|
776
|
-
if (!input.boardId || !input.contractEdgeId) {
|
|
777
|
-
return fail("remove_contract_edge requires boardId and contractEdgeId.");
|
|
778
|
-
}
|
|
779
|
-
const current = await getContractGraph(projectRoot, input.boardId);
|
|
780
|
-
const edge = current?.graph?.edges.find((candidate) => candidate.id === input.contractEdgeId);
|
|
781
|
-
if (current?.graph?.enforcement === "strict" && edge?.enforcement === "blocking") {
|
|
782
|
-
return fail("The autonomous kanban tool may not remove a blocking strict contract edge.");
|
|
783
|
-
}
|
|
784
|
-
const board = await removeContractEdge(projectRoot, input.boardId, input.contractEdgeId);
|
|
785
|
-
return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
|
|
786
|
-
}
|
|
787
|
-
case "evaluate_contract_graph": {
|
|
788
|
-
if (!input.boardId || !input.taskId) {
|
|
789
|
-
return fail("evaluate_contract_graph requires boardId and taskId.");
|
|
790
|
-
}
|
|
791
|
-
const result = await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId);
|
|
792
|
-
return result ? {
|
|
793
|
-
ok: result.evaluation.allowed,
|
|
794
|
-
message: result.evaluation.allowed ? "Contract graph is closed." : `Contract graph has ${result.evaluation.issues.length} unresolved issue(s).`,
|
|
795
|
-
board: result.board,
|
|
796
|
-
contractGraph: result.board.contractGraph,
|
|
797
|
-
contractEvaluation: result.evaluation
|
|
798
|
-
} : fail("Task not found.");
|
|
799
|
-
}
|
|
800
902
|
case "add_dependency": {
|
|
801
903
|
if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
|
|
802
904
|
return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
|
|
@@ -849,8 +951,9 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
849
951
|
}
|
|
850
952
|
const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
|
|
851
953
|
description: input.checkDescription,
|
|
852
|
-
type: "manual",
|
|
853
|
-
status: input.checkStatus
|
|
954
|
+
type: input.checkType ?? "manual",
|
|
955
|
+
status: input.checkStatus,
|
|
956
|
+
...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
|
|
854
957
|
});
|
|
855
958
|
return board ? okBoard(board, "Check added.") : fail("Task not found.");
|
|
856
959
|
}
|
|
@@ -865,11 +968,27 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
865
968
|
input.checkId,
|
|
866
969
|
{
|
|
867
970
|
...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
|
|
868
|
-
...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
|
|
971
|
+
...input.checkStatus !== void 0 ? { status: input.checkStatus } : {},
|
|
972
|
+
// Promoting an existing manual criterion to an executable one is the
|
|
973
|
+
// common repair: the card was written before anyone knew the command.
|
|
974
|
+
...input.checkType !== void 0 ? { type: input.checkType } : {},
|
|
975
|
+
...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
|
|
869
976
|
}
|
|
870
977
|
);
|
|
871
978
|
return board ? okBoard(board, "Check updated.") : fail("Check not found.");
|
|
872
979
|
}
|
|
980
|
+
case "remove_check": {
|
|
981
|
+
if (!input.boardId || !input.taskId || !input.checkId) {
|
|
982
|
+
return fail("remove_check requires boardId, taskId, and checkId.");
|
|
983
|
+
}
|
|
984
|
+
const board = await removeCheckFromTask(
|
|
985
|
+
projectRoot,
|
|
986
|
+
input.boardId,
|
|
987
|
+
input.taskId,
|
|
988
|
+
input.checkId
|
|
989
|
+
);
|
|
990
|
+
return board ? okBoard(board, "Acceptance criterion removed.") : fail("Check not found on this task.");
|
|
991
|
+
}
|
|
873
992
|
case "add_note": {
|
|
874
993
|
if (!input.boardId || !input.taskId || !input.note)
|
|
875
994
|
return fail("add_note requires boardId, taskId, and note.");
|
|
@@ -944,14 +1063,25 @@ function taskInput(input) {
|
|
|
944
1063
|
...input.order !== void 0 ? { order: input.order } : {},
|
|
945
1064
|
...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
|
|
946
1065
|
...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
|
|
1066
|
+
// The system prompt has always told the model it may "set atomic: true"
|
|
1067
|
+
// when creating a composite parent. It could not: the field reached
|
|
1068
|
+
// neither the create input nor the patch, so the instruction described a
|
|
1069
|
+
// capability that did not exist and the attempt was silently dropped.
|
|
1070
|
+
...input.atomic !== void 0 ? { atomic: input.atomic } : {},
|
|
947
1071
|
...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
|
|
948
1072
|
...input.checkDescription !== void 0 ? {
|
|
949
1073
|
successCriteria: [
|
|
950
1074
|
{
|
|
951
1075
|
id: randomUUID(),
|
|
952
1076
|
description: input.checkDescription,
|
|
953
|
-
|
|
954
|
-
|
|
1077
|
+
// `manual` only as the fallback. Hard-coding it here meant every
|
|
1078
|
+
// agent-authored criterion was unverifiable by construction: the
|
|
1079
|
+
// deterministic plugins never matched, the registry passed the
|
|
1080
|
+
// hand-set status straight through, and "verified" collapsed into
|
|
1081
|
+
// "the author ticked its own box".
|
|
1082
|
+
type: input.checkType ?? "manual",
|
|
1083
|
+
status: input.checkStatus ?? "pending",
|
|
1084
|
+
...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
|
|
955
1085
|
}
|
|
956
1086
|
]
|
|
957
1087
|
} : {},
|
|
@@ -999,11 +1129,11 @@ function taskInput(input) {
|
|
|
999
1129
|
};
|
|
1000
1130
|
}
|
|
1001
1131
|
function mergedDependsOn(input) {
|
|
1002
|
-
|
|
1132
|
+
if (input.dependsOn === void 0 && input.dependencyTaskId === void 0) return void 0;
|
|
1133
|
+
return [
|
|
1003
1134
|
...input.dependsOn ?? [],
|
|
1004
1135
|
...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
|
|
1005
1136
|
].filter((id, i, arr) => id && arr.indexOf(id) === i);
|
|
1006
|
-
return ids.length > 0 ? ids : void 0;
|
|
1007
1137
|
}
|
|
1008
1138
|
function taskPatch(input) {
|
|
1009
1139
|
return {
|
|
@@ -1017,7 +1147,15 @@ function taskPatch(input) {
|
|
|
1017
1147
|
status: input.status,
|
|
1018
1148
|
labels: input.labels,
|
|
1019
1149
|
assignedAgent: input.agentId,
|
|
1020
|
-
...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
|
|
1150
|
+
...mergedDependsOn(input) !== void 0 ? { dependsOn: mergedDependsOn(input) } : {},
|
|
1151
|
+
// `atomic` and `childTaskIds` are the composite-parent contract, and the
|
|
1152
|
+
// managed gate reads both: an `atomic` parent may not move forward without
|
|
1153
|
+
// children, and may not reach Done until every child is completed. The
|
|
1154
|
+
// manager has always accepted both on a patch; only this surface withheld
|
|
1155
|
+
// them, so `split_atomic` was a one-way door — delete the children and the
|
|
1156
|
+
// parent was stranded with no way to declare itself a leaf again.
|
|
1157
|
+
...input.atomic !== void 0 ? { atomic: input.atomic } : {},
|
|
1158
|
+
...input.childTaskIds !== void 0 ? { childTaskIds: input.childTaskIds } : {},
|
|
1021
1159
|
...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
|
|
1022
1160
|
...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
|
|
1023
1161
|
};
|
|
@@ -1077,8 +1215,8 @@ function assignmentForTaskCreate(input) {
|
|
|
1077
1215
|
}
|
|
1078
1216
|
|
|
1079
1217
|
// src/kanban-tool-schema.ts
|
|
1080
|
-
var KANBAN_TOOL_DESCRIPTION = "
|
|
1081
|
-
var KANBAN_TOOL_USAGE_HINT =
|
|
1218
|
+
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.";
|
|
1219
|
+
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.';
|
|
1082
1220
|
var KANBAN_INPUT_SCHEMA = {
|
|
1083
1221
|
type: "object",
|
|
1084
1222
|
properties: {
|
|
@@ -1091,6 +1229,7 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
1091
1229
|
"duplicate_board",
|
|
1092
1230
|
"update_board",
|
|
1093
1231
|
"adopt_managed_lifecycle",
|
|
1232
|
+
"release_managed_lifecycle",
|
|
1094
1233
|
"delete_board",
|
|
1095
1234
|
"generate_board",
|
|
1096
1235
|
"export_markdown",
|
|
@@ -1102,9 +1241,6 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
1102
1241
|
"ready_tasks",
|
|
1103
1242
|
"snapshot",
|
|
1104
1243
|
"workbench",
|
|
1105
|
-
"add_column",
|
|
1106
|
-
"update_column",
|
|
1107
|
-
"delete_column",
|
|
1108
1244
|
"add_task",
|
|
1109
1245
|
"split_task",
|
|
1110
1246
|
"merge_tasks",
|
|
@@ -1119,13 +1255,6 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
1119
1255
|
"delete_task",
|
|
1120
1256
|
"set_chain",
|
|
1121
1257
|
"get_chain",
|
|
1122
|
-
"get_contract_graph",
|
|
1123
|
-
"configure_contract_graph",
|
|
1124
|
-
"upsert_contract_node",
|
|
1125
|
-
"link_contract_nodes",
|
|
1126
|
-
"remove_contract_node",
|
|
1127
|
-
"remove_contract_edge",
|
|
1128
|
-
"evaluate_contract_graph",
|
|
1129
1258
|
"claim_task",
|
|
1130
1259
|
"release_task",
|
|
1131
1260
|
"assign_task",
|
|
@@ -1139,49 +1268,27 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
1139
1268
|
"update_goal_metric",
|
|
1140
1269
|
"add_check",
|
|
1141
1270
|
"update_check",
|
|
1271
|
+
"remove_check",
|
|
1142
1272
|
"add_note",
|
|
1143
1273
|
"add_link",
|
|
1144
1274
|
"verify_completion",
|
|
1145
1275
|
"split_atomic",
|
|
1146
1276
|
"assess_atomicity",
|
|
1147
|
-
"propose_decomposition"
|
|
1277
|
+
"propose_decomposition",
|
|
1278
|
+
"get_contract_graph",
|
|
1279
|
+
"configure_contract_graph",
|
|
1280
|
+
"upsert_contract_node",
|
|
1281
|
+
"remove_contract_node",
|
|
1282
|
+
"add_contract_edge",
|
|
1283
|
+
"remove_contract_edge"
|
|
1148
1284
|
]
|
|
1149
1285
|
},
|
|
1150
1286
|
boardId: { type: "string" },
|
|
1151
1287
|
taskId: { type: "string" },
|
|
1152
1288
|
taskIds: { type: "array", items: { type: "string" } },
|
|
1153
1289
|
chainId: { type: "string" },
|
|
1154
|
-
contractNodeId: { type: "string" },
|
|
1155
|
-
contractNodeKind: {
|
|
1156
|
-
type: "string",
|
|
1157
|
-
enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"]
|
|
1158
|
-
},
|
|
1159
|
-
contractNodeState: {
|
|
1160
|
-
type: "string",
|
|
1161
|
-
enum: ["unknown", "active", "satisfied", "violated", "resolved"]
|
|
1162
|
-
},
|
|
1163
|
-
contractEnforcement: {
|
|
1164
|
-
type: "string",
|
|
1165
|
-
enum: ["blocking", "advisory", "informational"]
|
|
1166
|
-
},
|
|
1167
|
-
contractGraphEnforcement: { type: "string", enum: ["off", "advisory", "strict"] },
|
|
1168
|
-
contractEdgeId: { type: "string" },
|
|
1169
|
-
contractEdgeType: {
|
|
1170
|
-
type: "string",
|
|
1171
|
-
enum: [
|
|
1172
|
-
"targets",
|
|
1173
|
-
"affects",
|
|
1174
|
-
"must_preserve",
|
|
1175
|
-
"exposes",
|
|
1176
|
-
"verified_by",
|
|
1177
|
-
"conflicts_with",
|
|
1178
|
-
"derived_from",
|
|
1179
|
-
"relates_to"
|
|
1180
|
-
]
|
|
1181
|
-
},
|
|
1182
1290
|
fromNodeId: { type: "string" },
|
|
1183
1291
|
toNodeId: { type: "string" },
|
|
1184
|
-
contractRationale: { type: "string" },
|
|
1185
1292
|
baseline: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
1186
1293
|
threshold: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
1187
1294
|
columnId: { type: "string" },
|
|
@@ -1265,7 +1372,20 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
1265
1372
|
costCeilingUsd: { type: "number" },
|
|
1266
1373
|
retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
|
|
1267
1374
|
lastFailureKind: { type: "string" },
|
|
1268
|
-
dependsOn: {
|
|
1375
|
+
dependsOn: {
|
|
1376
|
+
type: "array",
|
|
1377
|
+
items: { type: "string" },
|
|
1378
|
+
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."
|
|
1379
|
+
},
|
|
1380
|
+
atomic: {
|
|
1381
|
+
type: "boolean",
|
|
1382
|
+
description: "Composite parent (true) or executable leaf (false). Set false to make a stranded parent a leaf again after its children were dropped."
|
|
1383
|
+
},
|
|
1384
|
+
childTaskIds: {
|
|
1385
|
+
type: "array",
|
|
1386
|
+
items: { type: "string" },
|
|
1387
|
+
description: "Children of a composite parent. On update_task an explicit empty array detaches them all."
|
|
1388
|
+
},
|
|
1269
1389
|
estimatedHours: { type: "number" },
|
|
1270
1390
|
actualHours: { type: "number" },
|
|
1271
1391
|
taskGraph: { type: "object" },
|
|
@@ -1299,6 +1419,74 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
1299
1419
|
checkId: { type: "string" },
|
|
1300
1420
|
checkDescription: { type: "string" },
|
|
1301
1421
|
checkStatus: { type: "string", enum: ["pending", "passed", "failed", "skipped"] },
|
|
1422
|
+
checkType: {
|
|
1423
|
+
type: "string",
|
|
1424
|
+
// Only types a verifier can actually execute. `manual` is the default and
|
|
1425
|
+
// means a human or agent asserts the status by hand. The rest are run by
|
|
1426
|
+
// `verify_completion` against the default deterministic registry. Types
|
|
1427
|
+
// with no plugin in that registry (`auto`, `review`, `agent`, `council`)
|
|
1428
|
+
// are deliberately omitted: offering them would produce criteria that
|
|
1429
|
+
// silently report `skipped — no verifier plugin registered`.
|
|
1430
|
+
enum: ["manual", "command", "test", "file_exists", "file_matches", "git_diff", "metric"],
|
|
1431
|
+
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.'
|
|
1432
|
+
},
|
|
1433
|
+
checkNotes: {
|
|
1434
|
+
type: "string",
|
|
1435
|
+
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"}.'
|
|
1436
|
+
},
|
|
1437
|
+
// ── Contract map ───────────────────────────────────────────────────
|
|
1438
|
+
// The card contract: what this work targets, what it must not break, what
|
|
1439
|
+
// it risks, and what verifies it. Advisory by default — the readiness gate
|
|
1440
|
+
// deliberately does not require map structure, so a map is an operator
|
|
1441
|
+
// review aid, not work the model must complete before implementing.
|
|
1442
|
+
contractEnforcement: {
|
|
1443
|
+
type: "string",
|
|
1444
|
+
enum: ["off", "advisory", "strict"],
|
|
1445
|
+
description: "Board-level contract map enforcement. Default when first configured: advisory."
|
|
1446
|
+
},
|
|
1447
|
+
contractNodeId: { type: "string" },
|
|
1448
|
+
contractNodeKind: {
|
|
1449
|
+
type: "string",
|
|
1450
|
+
enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"],
|
|
1451
|
+
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."
|
|
1452
|
+
},
|
|
1453
|
+
contractNodeTitle: { type: "string" },
|
|
1454
|
+
contractNodeDescription: { type: "string" },
|
|
1455
|
+
contractNodeState: {
|
|
1456
|
+
type: "string",
|
|
1457
|
+
enum: ["unknown", "active", "satisfied", "violated", "waived", "resolved"]
|
|
1458
|
+
},
|
|
1459
|
+
contractNodeEnforcement: {
|
|
1460
|
+
type: "string",
|
|
1461
|
+
enum: ["blocking", "advisory", "informational"]
|
|
1462
|
+
},
|
|
1463
|
+
/** Bind a node to an acceptance criterion or goal metric already on the task. */
|
|
1464
|
+
contractCheckId: { type: "string" },
|
|
1465
|
+
contractMetricId: { type: "string" },
|
|
1466
|
+
contractWaiverReason: {
|
|
1467
|
+
type: "string",
|
|
1468
|
+
description: 'Required, with an actor, when contractNodeState is "waived".'
|
|
1469
|
+
},
|
|
1470
|
+
contractEdgeId: { type: "string" },
|
|
1471
|
+
contractEdgeFrom: {
|
|
1472
|
+
type: "string",
|
|
1473
|
+
description: 'A contract node id, or a task id (bare or "task:<id>") for the card endpoint.'
|
|
1474
|
+
},
|
|
1475
|
+
contractEdgeTo: { type: "string" },
|
|
1476
|
+
contractEdgeType: {
|
|
1477
|
+
type: "string",
|
|
1478
|
+
enum: [
|
|
1479
|
+
"targets",
|
|
1480
|
+
"affects",
|
|
1481
|
+
"must_preserve",
|
|
1482
|
+
"exposes",
|
|
1483
|
+
"verified_by",
|
|
1484
|
+
"conflicts_with",
|
|
1485
|
+
"derived_from",
|
|
1486
|
+
"relates_to"
|
|
1487
|
+
]
|
|
1488
|
+
},
|
|
1489
|
+
contractEdgeRationale: { type: "string" },
|
|
1302
1490
|
note: { type: "string" },
|
|
1303
1491
|
author: { type: "string" },
|
|
1304
1492
|
url: { type: "string" },
|
|
@@ -1373,8 +1561,14 @@ var kanbanTool = {
|
|
|
1373
1561
|
}
|
|
1374
1562
|
case "create_board": {
|
|
1375
1563
|
if (!input.title) return fail("create_board requires title.");
|
|
1564
|
+
const existing = (await listBoards2(projectRoot)).filter(
|
|
1565
|
+
(candidate) => (candidate.kind ?? "project") === "project"
|
|
1566
|
+
);
|
|
1376
1567
|
const board = await createBoard2(projectRoot, boardCreateInput(input, input.title));
|
|
1377
|
-
|
|
1568
|
+
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(
|
|
1569
|
+
", "
|
|
1570
|
+
)}${existing.length > 3 ? ", \u2026" : ""}. If this work belongs to one of them, add_task there instead and delete this board.` : "";
|
|
1571
|
+
return { ok: true, message: `Board created: ${board.title}.${note}`, board };
|
|
1378
1572
|
}
|
|
1379
1573
|
case "update_board": {
|
|
1380
1574
|
if (!input.boardId) return fail("update_board requires boardId.");
|
|
@@ -1403,6 +1597,20 @@ var kanbanTool = {
|
|
|
1403
1597
|
});
|
|
1404
1598
|
return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
|
|
1405
1599
|
}
|
|
1600
|
+
// Adoption used to be a one-way door: the strict lifecycle carries
|
|
1601
|
+
// acceptance-criteria, verification-report, review-evidence and
|
|
1602
|
+
// one-stage-at-a-time gates, and nothing on the tool surface could
|
|
1603
|
+
// undo it, so a board adopted once kept its ceremony forever. The
|
|
1604
|
+
// gates are worth having where a fleet is supervised; they are not
|
|
1605
|
+
// worth being unable to leave. Cards and columns are untouched.
|
|
1606
|
+
case "release_managed_lifecycle": {
|
|
1607
|
+
if (!input.boardId) return fail("release_managed_lifecycle requires boardId.");
|
|
1608
|
+
const board = await updateBoard2(projectRoot, input.boardId, { lifecycle: null });
|
|
1609
|
+
return board ? okBoard(
|
|
1610
|
+
board,
|
|
1611
|
+
"Managed lifecycle released; the board now tracks work without strict gates."
|
|
1612
|
+
) : fail("Board not found.");
|
|
1613
|
+
}
|
|
1406
1614
|
case "duplicate_board": {
|
|
1407
1615
|
if (!input.boardId) return fail("duplicate_board requires boardId.");
|
|
1408
1616
|
const board = await duplicateBoard(
|
|
@@ -1422,8 +1630,7 @@ var kanbanTool = {
|
|
|
1422
1630
|
const boardInput = createBoardFromText({
|
|
1423
1631
|
description: input.description,
|
|
1424
1632
|
...input.title !== void 0 ? { title: input.title } : {},
|
|
1425
|
-
...input.context !== void 0 ? { context: input.context } : {}
|
|
1426
|
-
...input.columns !== void 0 ? { columns: input.columns } : {}
|
|
1633
|
+
...input.context !== void 0 ? { context: input.context } : {}
|
|
1427
1634
|
});
|
|
1428
1635
|
const board = await createBoard2(projectRoot, boardInput);
|
|
1429
1636
|
for (const taskInput2 of parseLinesIntoTasks(
|
|
@@ -1561,7 +1768,7 @@ var kanbanTool = {
|
|
|
1561
1768
|
return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
|
|
1562
1769
|
}
|
|
1563
1770
|
case "snapshot": {
|
|
1564
|
-
const snapshot = await
|
|
1771
|
+
const snapshot = await getKanbanOrchestrationSnapshot2(projectRoot, {
|
|
1565
1772
|
query: input.query,
|
|
1566
1773
|
boardId: input.boardId,
|
|
1567
1774
|
assignedAgent: input.agentId,
|
|
@@ -1576,33 +1783,6 @@ var kanbanTool = {
|
|
|
1576
1783
|
snapshot
|
|
1577
1784
|
};
|
|
1578
1785
|
}
|
|
1579
|
-
case "add_column": {
|
|
1580
|
-
if (!input.boardId || !input.title)
|
|
1581
|
-
return fail("add_column requires boardId and title.");
|
|
1582
|
-
const result2 = await addColumn(projectRoot, input.boardId, {
|
|
1583
|
-
title: input.title,
|
|
1584
|
-
...input.description !== void 0 ? { description: input.description } : {}
|
|
1585
|
-
});
|
|
1586
|
-
return result2 ? okBoard(result2.board, "Column added.") : fail("Board not found.");
|
|
1587
|
-
}
|
|
1588
|
-
case "update_column": {
|
|
1589
|
-
if (!input.boardId || !input.columnId)
|
|
1590
|
-
return fail("update_column requires boardId and columnId.");
|
|
1591
|
-
const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
|
|
1592
|
-
...input.title !== void 0 ? { title: input.title } : {},
|
|
1593
|
-
...input.description !== void 0 ? { description: input.description } : {},
|
|
1594
|
-
...input.order !== void 0 ? { order: input.order } : {}
|
|
1595
|
-
});
|
|
1596
|
-
return board ? okBoard(board, "Column updated.") : fail("Column not found.");
|
|
1597
|
-
}
|
|
1598
|
-
case "delete_column": {
|
|
1599
|
-
if (!input.boardId || !input.columnId)
|
|
1600
|
-
return fail("delete_column requires boardId and columnId.");
|
|
1601
|
-
const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
|
|
1602
|
-
moveTasksToColumnId: input.moveTasksToColumnId
|
|
1603
|
-
});
|
|
1604
|
-
return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
|
|
1605
|
-
}
|
|
1606
1786
|
case "add_task": {
|
|
1607
1787
|
if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
|
|
1608
1788
|
const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
|
|
@@ -1684,6 +1864,32 @@ var kanbanTool = {
|
|
|
1684
1864
|
`Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
|
|
1685
1865
|
);
|
|
1686
1866
|
}
|
|
1867
|
+
if (board.lifecycle?.mode !== "managed") {
|
|
1868
|
+
const now = /* @__PURE__ */ new Date();
|
|
1869
|
+
const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
|
|
1870
|
+
status: "running",
|
|
1871
|
+
agentId: input.agentId ?? input.author,
|
|
1872
|
+
leaseId: input.leaseId ?? randomUUID2(),
|
|
1873
|
+
claimedAt: input.claimedAt ?? now.toISOString(),
|
|
1874
|
+
heartbeatAt: input.heartbeatAt ?? now.toISOString(),
|
|
1875
|
+
leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
|
|
1876
|
+
attempt: input.attempt ?? 1,
|
|
1877
|
+
maxAttempts: input.maxAttempts ?? 3
|
|
1878
|
+
});
|
|
1879
|
+
if (!assigned) return fail("Task assignment could not be started.");
|
|
1880
|
+
const started = await updateTask2(projectRoot, board.id, task.id, {
|
|
1881
|
+
status: "in_progress"
|
|
1882
|
+
});
|
|
1883
|
+
const current = started ?? assigned;
|
|
1884
|
+
const claimed = task;
|
|
1885
|
+
const currentTask = current.tasks.find((candidate) => candidate.id === claimed.id) ?? claimed;
|
|
1886
|
+
ctx.setCurrentKanbanTask?.(currentTask.id, current.id);
|
|
1887
|
+
return okTask(
|
|
1888
|
+
current,
|
|
1889
|
+
currentTask,
|
|
1890
|
+
"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."
|
|
1891
|
+
);
|
|
1892
|
+
}
|
|
1687
1893
|
let stage = task.lifecycle?.currentStage;
|
|
1688
1894
|
if (stage === "backlog") {
|
|
1689
1895
|
const moved = await transitionTask(projectRoot, board.id, task.id, {
|
|
@@ -1824,6 +2030,9 @@ var kanbanTool = {
|
|
|
1824
2030
|
if (!input.boardId || !input.taskId)
|
|
1825
2031
|
return fail("delete_task requires boardId and taskId.");
|
|
1826
2032
|
const board = await removeTask(projectRoot, input.boardId, input.taskId);
|
|
2033
|
+
if (board && ctx.currentKanbanTaskId === input.taskId) {
|
|
2034
|
+
ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
|
|
2035
|
+
}
|
|
1827
2036
|
return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
|
|
1828
2037
|
}
|
|
1829
2038
|
case "set_chain": {
|
|
@@ -1956,7 +2165,7 @@ var kanbanTool = {
|
|
|
1956
2165
|
});
|
|
1957
2166
|
} catch (err) {
|
|
1958
2167
|
lifecycleWarnings.push(
|
|
1959
|
-
`Lifecycle transition to Running deferred: ${err instanceof Error ? err.message : String(err)}`
|
|
2168
|
+
`Lifecycle transition to Running deferred: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
|
|
1960
2169
|
);
|
|
1961
2170
|
}
|
|
1962
2171
|
}
|
|
@@ -1980,7 +2189,7 @@ var kanbanTool = {
|
|
|
1980
2189
|
});
|
|
1981
2190
|
} catch (err) {
|
|
1982
2191
|
lifecycleWarnings.push(
|
|
1983
|
-
`Lifecycle transition to Review failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2192
|
+
`Lifecycle transition to Review failed: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
|
|
1984
2193
|
);
|
|
1985
2194
|
}
|
|
1986
2195
|
if (transitionResult) {
|
|
@@ -2000,7 +2209,11 @@ var kanbanTool = {
|
|
|
2000
2209
|
successCriteria: verResult.task.successCriteria
|
|
2001
2210
|
});
|
|
2002
2211
|
const verdict = verResult.report.verdict;
|
|
2003
|
-
if (verdict === "passed") {
|
|
2212
|
+
if (verdict === "passed" && !resolveAutoAccept(board)) {
|
|
2213
|
+
lifecycleWarnings.push(
|
|
2214
|
+
"Verification passed, but this board does not auto-accept. The card is in Review awaiting an explicit transition_task to done."
|
|
2215
|
+
);
|
|
2216
|
+
} else if (verdict === "passed") {
|
|
2004
2217
|
try {
|
|
2005
2218
|
const doneResult = await transitionTask(
|
|
2006
2219
|
projectRoot,
|
|
@@ -2118,11 +2331,34 @@ var kanbanTool = {
|
|
|
2118
2331
|
});
|
|
2119
2332
|
return {
|
|
2120
2333
|
ok: true,
|
|
2121
|
-
message: `Counts:
|
|
2334
|
+
message: `Counts: startable=${health.counts.startable}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
|
|
2122
2335
|
queueHealth: health
|
|
2123
2336
|
};
|
|
2124
2337
|
}
|
|
2338
|
+
// Not every action is handled above. These are dispatched from here,
|
|
2339
|
+
// and the split has already cost real time: an agent that read this
|
|
2340
|
+
// file concluded `add_check` / `update_check` did not exist, wrote
|
|
2341
|
+
// that on a card, and spent a session trying to satisfy a gate it
|
|
2342
|
+
// already had the tool to clear. Keep this index in step with the
|
|
2343
|
+
// handlers.
|
|
2344
|
+
//
|
|
2345
|
+
// kanban-detail-actions.ts workbench · add_dependency ·
|
|
2346
|
+
// add_goal_metric · update_goal_metric · add_check ·
|
|
2347
|
+
// update_check · add_note · add_link · split_atomic
|
|
2348
|
+
// kanban-decomposition-actions.ts verify_completion ·
|
|
2349
|
+
// assess_atomicity · propose_decomposition
|
|
2350
|
+
// kanban-contract-actions.ts get_contract_graph ·
|
|
2351
|
+
// configure_contract_graph · upsert_contract_node ·
|
|
2352
|
+
// remove_contract_node · add_contract_edge · remove_contract_edge
|
|
2125
2353
|
default:
|
|
2354
|
+
{
|
|
2355
|
+
const contractResult = await handleKanbanContractAction(
|
|
2356
|
+
projectRoot,
|
|
2357
|
+
input,
|
|
2358
|
+
input.author ?? input.agentId
|
|
2359
|
+
);
|
|
2360
|
+
if (contractResult !== void 0) return contractResult;
|
|
2361
|
+
}
|
|
2126
2362
|
{
|
|
2127
2363
|
const detailResult = await handleKanbanDetailAction(projectRoot, input);
|
|
2128
2364
|
if (detailResult !== void 0) return detailResult;
|
|
@@ -2132,7 +2368,7 @@ var kanbanTool = {
|
|
|
2132
2368
|
})();
|
|
2133
2369
|
return withPresence(result);
|
|
2134
2370
|
} catch (err) {
|
|
2135
|
-
return fail(err instanceof Error ? err.message : String(err));
|
|
2371
|
+
return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
|
|
2136
2372
|
}
|
|
2137
2373
|
}
|
|
2138
2374
|
};
|
|
@@ -2165,11 +2401,51 @@ function bindTodosToBoard(items, previous, board) {
|
|
|
2165
2401
|
available.find((task2) => !used.has(task2.id) && normalizedTitle(task2.title) === title)
|
|
2166
2402
|
];
|
|
2167
2403
|
const task = candidates.find((candidate) => candidate && !used.has(candidate.id));
|
|
2168
|
-
if (!task)
|
|
2404
|
+
if (!task) {
|
|
2405
|
+
const { blockedBy: _discarded, ...rest } = item;
|
|
2406
|
+
return { ...rest };
|
|
2407
|
+
}
|
|
2169
2408
|
used.add(task.id);
|
|
2170
|
-
|
|
2409
|
+
const blockedBy = blockingTitles(board, task);
|
|
2410
|
+
return {
|
|
2411
|
+
...item,
|
|
2412
|
+
kanbanBoardId: board.id,
|
|
2413
|
+
kanbanTaskId: task.id,
|
|
2414
|
+
...blockedBy.length ? { blockedBy } : { blockedBy: void 0 }
|
|
2415
|
+
};
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
function demoteBlockedInProgress(items, warnings) {
|
|
2419
|
+
return items.map((item) => {
|
|
2420
|
+
if (item.status !== "in_progress" || !item.blockedBy?.length) return item;
|
|
2421
|
+
warnings.push(
|
|
2422
|
+
`"${item.content}" cannot start yet \u2014 it waits on: ${item.blockedBy.join("; ")}. Kept as pending; complete the blocking work first.`
|
|
2423
|
+
);
|
|
2424
|
+
return { ...item, status: "pending" };
|
|
2171
2425
|
});
|
|
2172
2426
|
}
|
|
2427
|
+
async function createMissingManagedCards(items, board, ctx, warnings) {
|
|
2428
|
+
const created = /* @__PURE__ */ new Map();
|
|
2429
|
+
for (const item of items) {
|
|
2430
|
+
if (item.kanbanBoardId === board.id && item.kanbanTaskId) continue;
|
|
2431
|
+
try {
|
|
2432
|
+
const result = await addTask2(ctx.projectRoot, board.id, {
|
|
2433
|
+
title: item.content,
|
|
2434
|
+
description: item.activeForm?.trim() || `Added from the session todo list: ${item.content}`
|
|
2435
|
+
});
|
|
2436
|
+
if (!result) {
|
|
2437
|
+
warnings.push(`Could not open a Kanban card for "${item.content}": board not found.`);
|
|
2438
|
+
continue;
|
|
2439
|
+
}
|
|
2440
|
+
created.set(item.id, result.task.id);
|
|
2441
|
+
} catch (error) {
|
|
2442
|
+
warnings.push(
|
|
2443
|
+
`Could not open a Kanban card for "${item.content}": ${error instanceof Error ? error.message : String(error)}`
|
|
2444
|
+
);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
return created;
|
|
2448
|
+
}
|
|
2173
2449
|
async function synchronizeManagedKanban(items, board, ctx, signal) {
|
|
2174
2450
|
let synced = 0;
|
|
2175
2451
|
const warnings = [];
|
|
@@ -2207,6 +2483,16 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
|
|
|
2207
2483
|
transitionComment: `Todo returned to queue: ${item.content}`
|
|
2208
2484
|
});
|
|
2209
2485
|
}
|
|
2486
|
+
for (const item of items) {
|
|
2487
|
+
if (item.status === "completed" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
|
|
2488
|
+
continue;
|
|
2489
|
+
}
|
|
2490
|
+
const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
|
|
2491
|
+
if (task?.status !== "completed") continue;
|
|
2492
|
+
warnings.push(
|
|
2493
|
+
`"${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.`
|
|
2494
|
+
);
|
|
2495
|
+
}
|
|
2210
2496
|
for (const item of items) {
|
|
2211
2497
|
if (item.status !== "completed" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
|
|
2212
2498
|
continue;
|
|
@@ -2258,17 +2544,25 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
|
|
|
2258
2544
|
const active = items.find(
|
|
2259
2545
|
(item) => item.status === "in_progress" && item.kanbanBoardId === board.id && Boolean(item.kanbanTaskId)
|
|
2260
2546
|
);
|
|
2547
|
+
const activeStage = active?.kanbanTaskId ? afterCompletions?.tasks.find((task) => task.id === active.kanbanTaskId)?.lifecycle?.currentStage : void 0;
|
|
2261
2548
|
if (active?.kanbanTaskId) {
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2549
|
+
if (activeStage === "review" || activeStage === "done") {
|
|
2550
|
+
warnings.push(
|
|
2551
|
+
`"${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.")
|
|
2552
|
+
);
|
|
2553
|
+
} else {
|
|
2554
|
+
await execute({
|
|
2555
|
+
action: "start_task",
|
|
2556
|
+
boardId: board.id,
|
|
2557
|
+
taskId: active.kanbanTaskId,
|
|
2558
|
+
author: actor,
|
|
2559
|
+
agentId: actor,
|
|
2560
|
+
transitionComment: `Todo activated: ${active.content}`
|
|
2561
|
+
});
|
|
2562
|
+
}
|
|
2270
2563
|
}
|
|
2271
|
-
if (active?.kanbanTaskId &&
|
|
2564
|
+
if (active?.kanbanTaskId && activeStage === "review") {
|
|
2565
|
+
} else if (active?.kanbanTaskId && completionPending) {
|
|
2272
2566
|
warnings.push(
|
|
2273
2567
|
"A completed todo is still awaiting acceptance; the next independent Kanban task was started."
|
|
2274
2568
|
);
|
|
@@ -2355,29 +2649,47 @@ var todoTool = {
|
|
|
2355
2649
|
}
|
|
2356
2650
|
}
|
|
2357
2651
|
const boardId = activeBoardId(items, ctx);
|
|
2358
|
-
|
|
2359
|
-
const
|
|
2652
|
+
let board = boardId ? await getBoard4(ctx.projectRoot, boardId) : null;
|
|
2653
|
+
const managed = board?.lifecycle?.mode === "managed";
|
|
2654
|
+
let boundItems = managed && board ? bindTodosToBoard(items, ctx.todos ?? [], board) : items;
|
|
2655
|
+
const creationWarnings = [];
|
|
2656
|
+
if (managed && board) {
|
|
2657
|
+
const managedBoardId = board.id;
|
|
2658
|
+
const created = await createMissingManagedCards(boundItems, board, ctx, creationWarnings);
|
|
2659
|
+
if (created.size > 0) {
|
|
2660
|
+
boundItems = boundItems.map((item) => {
|
|
2661
|
+
const taskId = created.get(item.id);
|
|
2662
|
+
return taskId ? { ...item, kanbanBoardId: managedBoardId, kanbanTaskId: taskId } : item;
|
|
2663
|
+
});
|
|
2664
|
+
board = await getBoard4(ctx.projectRoot, managedBoardId) ?? board;
|
|
2665
|
+
boundItems = bindTodosToBoard(boundItems, ctx.todos ?? [], board);
|
|
2666
|
+
}
|
|
2667
|
+
boundItems = demoteBlockedInProgress(boundItems, creationWarnings);
|
|
2668
|
+
}
|
|
2360
2669
|
ctx.state.replaceTodos(boundItems);
|
|
2361
|
-
const kanbanSync =
|
|
2362
|
-
|
|
2670
|
+
const kanbanSync = managed && board ? await synchronizeManagedKanban(boundItems, board, ctx, call.signal) : { synced: 0, warnings: [] };
|
|
2671
|
+
kanbanSync.warnings.unshift(...creationWarnings);
|
|
2672
|
+
if (managed && board) {
|
|
2363
2673
|
const unresolved = boundItems.filter(
|
|
2364
2674
|
(item) => item.kanbanBoardId !== board.id || !item.kanbanTaskId
|
|
2365
2675
|
);
|
|
2366
2676
|
if (unresolved.length > 0) {
|
|
2367
2677
|
kanbanSync.warnings.push(
|
|
2368
|
-
`${unresolved.length} Todo row(s)
|
|
2678
|
+
`${unresolved.length} Todo row(s) could not be bound to a Kanban task and were not applied. Preserve kanbanBoardId/kanbanTaskId when updating the projection.`
|
|
2369
2679
|
);
|
|
2370
2680
|
}
|
|
2371
2681
|
}
|
|
2682
|
+
const mirrorFailure = takeSessionMirrorFailure(ctx.projectRoot, ctx.session?.id ?? "");
|
|
2683
|
+
if (mirrorFailure) kanbanSync.warnings.push(mirrorFailure);
|
|
2372
2684
|
let projectedBoard = board;
|
|
2373
|
-
if (
|
|
2685
|
+
if (managed && board) {
|
|
2374
2686
|
const refreshed = await getBoard4(ctx.projectRoot, board.id);
|
|
2375
2687
|
if (refreshed) {
|
|
2376
2688
|
projectedBoard = refreshed;
|
|
2377
2689
|
applyManagedKanbanBoardToTodos(ctx, refreshed);
|
|
2378
2690
|
}
|
|
2379
2691
|
}
|
|
2380
|
-
if (
|
|
2692
|
+
if (!managed) {
|
|
2381
2693
|
mirrorSessionTodosToKanban(ctx.projectRoot, items, ctx.session?.id ?? "session");
|
|
2382
2694
|
}
|
|
2383
2695
|
const completedPlanIds = /* @__PURE__ */ new Set();
|