@wrongstack/tools 0.302.2 → 0.305.0

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.
Files changed (44) hide show
  1. package/dist/builtin.d.ts +6 -0
  2. package/dist/builtin.js +3213 -686
  3. package/dist/codebase-index/binary-frame.d.ts +43 -0
  4. package/dist/codebase-index/codebase-incoming-calls-tool.d.ts +1 -0
  5. package/dist/codebase-index/codebase-index-tool.d.ts +6 -0
  6. package/dist/codebase-index/codebase-outgoing-calls-tool.d.ts +1 -0
  7. package/dist/codebase-index/content-hash.d.ts +66 -0
  8. package/dist/codebase-index/index.js +1597 -153
  9. package/dist/codebase-index/indexer.d.ts +6 -0
  10. package/dist/codebase-index/parser-worker-pool.d.ts +63 -0
  11. package/dist/codebase-index/parser-worker-script.d.ts +42 -0
  12. package/dist/codebase-index/project-server-protocol.d.ts +2 -0
  13. package/dist/codebase-index/project-server.js +1483 -104
  14. package/dist/codebase-index/schema.d.ts +18 -0
  15. package/dist/codebase-index/tree-sitter/queries.d.ts +48 -0
  16. package/dist/codebase-index/tree-sitter/util.d.ts +31 -0
  17. package/dist/codebase-index/tree-sitter/visitor.d.ts +47 -0
  18. package/dist/codebase-index/tree-sitter-parser.d.ts +58 -0
  19. package/dist/codebase-index/vector-search.d.ts +62 -0
  20. package/dist/codebase-index/worker-protocol.d.ts +2 -0
  21. package/dist/codebase-index/worker.js +1452 -73
  22. package/dist/codebase-index/writer-bulk-insert.d.ts +5 -0
  23. package/dist/codebase-index/writer-graph-reader.d.ts +39 -0
  24. package/dist/codebase-index/writer-schema.d.ts +9 -2
  25. package/dist/codebase-index/writer.d.ts +36 -0
  26. package/dist/index.d.ts +4 -3
  27. package/dist/index.js +3258 -727
  28. package/dist/kanban-contract-actions.d.ts +7 -0
  29. package/dist/kanban-task-inputs.d.ts +16 -2
  30. package/dist/kanban-tool-schema.d.ts +2 -2
  31. package/dist/kanban-tool-types.d.ts +39 -2
  32. package/dist/kanban.js +580 -193
  33. package/dist/pack.js +3212 -686
  34. package/dist/plan.d.ts +4 -1
  35. package/dist/plan.js +2701 -18
  36. package/dist/read.js +1559 -103
  37. package/dist/session-kanban.d.ts +94 -1
  38. package/dist/session-kanban.js +308 -44
  39. package/dist/task.d.ts +5 -4
  40. package/dist/task.js +2825 -138
  41. package/dist/todo.d.ts +10 -1
  42. package/dist/todo.js +2474 -30
  43. package/dist/tool-tier.js +3212 -686
  44. package/package.json +8 -4
package/dist/kanban.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // src/kanban.ts
2
- import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
2
+ import { randomUUID as randomUUID2 } from "node:crypto";
3
3
  import { loadTasks as loadTasks2 } from "@wrongstack/core/storage";
4
+ import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
4
5
  import {
5
- addColumn,
6
6
  addTask,
7
7
  adoptManagedLifecycle,
8
8
  assignTask,
@@ -10,12 +10,14 @@ import {
10
10
  copyTaskToBoard,
11
11
  createBoard as createBoard2,
12
12
  createBoardFromTaskGraph,
13
+ createBoardFromText,
13
14
  duplicateBoard,
15
+ evaluateContractGraphReadiness,
14
16
  exportBoardAsMarkdown,
15
17
  exportBoardToTaskGraph,
16
- createBoardFromText,
18
+ finalizeTaskCompletion,
17
19
  getBoard as getBoard3,
18
- getKanbanOrchestrationSnapshot,
20
+ getKanbanOrchestrationSnapshot as getKanbanOrchestrationSnapshot2,
19
21
  getKanbanQueueHealth,
20
22
  getTask,
21
23
  getTaskChain,
@@ -27,53 +29,76 @@ import {
27
29
  moveTask,
28
30
  parseLinesIntoTasks,
29
31
  recoverStaleTaskAssignments,
30
- repairManagedTaskProjection,
31
32
  releaseTaskClaim,
32
33
  removeBoard as removeBoard2,
33
- removeColumn,
34
34
  removeTask,
35
+ repairManagedTaskProjection,
36
+ resolveAutoAccept,
35
37
  searchKanban,
36
38
  setTaskChain,
39
+ stripLifecycleIssues,
37
40
  syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
38
- transitionTask,
39
41
  transferTaskToBoard,
42
+ transitionTask,
40
43
  updateBoard as updateBoard2,
41
- updateColumn,
42
44
  updateTask as updateTask2,
43
45
  updateTaskAssignment,
44
- verifyTaskCompletion as verifyTaskCompletion2,
45
- finalizeTaskCompletion
46
+ verifyTaskCompletion as verifyTaskCompletion2
46
47
  } from "@wrongstack/kanban";
47
48
 
48
- // src/kanban-evidence-bridge.ts
49
- import { recordCompletedWorkEvidence } from "@wrongstack/core/utils";
50
- function kanbanEvidenceKey(boardId, taskId) {
51
- return `kanban:${boardId}:${taskId}`;
49
+ // src/kanban-board-inputs.ts
50
+ function agentSettableGate(enforcement) {
51
+ if (enforcement === void 0 || enforcement === "off") return {};
52
+ return { completionGate: { enforcement } };
52
53
  }
53
- function kanbanEvidencePointer(boardId, taskId) {
54
- return `kanban://${boardId}/${taskId}#verificationReport`;
54
+ function boardCreateInput(input, title) {
55
+ return {
56
+ title,
57
+ ...input.description !== void 0 ? { description: input.description } : {},
58
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
59
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
60
+ ...input.atomicityMode !== void 0 ? {
61
+ atomicity: {
62
+ mode: input.atomicityMode,
63
+ decomposition: input.atomicityDecomposition ?? "propose"
64
+ }
65
+ } : {},
66
+ ...agentSettableGate(input.gateEnforcement)
67
+ };
55
68
  }
56
- function recordKanbanVerificationEvidence(ctx, report) {
57
- try {
58
- const passed = report.checks.filter((check) => check.status === "passed").length;
59
- const completedAt = Date.parse(report.completedAt);
60
- recordCompletedWorkEvidence(ctx, {
61
- key: kanbanEvidenceKey(report.boardId, report.taskId),
62
- source: "verification",
63
- summary: `${report.taskTitle} \u2014 verification ${report.verdict} (${passed}/${report.checks.length} checks)`,
64
- ...Number.isFinite(completedAt) ? { completedAt } : {},
65
- evidence: kanbanEvidencePointer(report.boardId, report.taskId)
66
- });
67
- } catch {
68
- }
69
+ function boardUpdatePatch(input) {
70
+ return {
71
+ ...input.title !== void 0 ? { title: input.title } : {},
72
+ ...input.description !== void 0 ? { description: input.description } : {},
73
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
74
+ ...input.atomicityMode !== void 0 ? {
75
+ atomicity: {
76
+ mode: input.atomicityMode,
77
+ decomposition: input.atomicityDecomposition ?? "propose"
78
+ }
79
+ } : {},
80
+ ...agentSettableGate(input.gateEnforcement)
81
+ };
82
+ }
83
+ function duplicateBoardOptions(input) {
84
+ return {
85
+ ...input.title !== void 0 ? { title: input.title } : {},
86
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
87
+ ...input.includeTasks !== void 0 ? { includeTasks: input.includeTasks } : {},
88
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
89
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {}
90
+ };
69
91
  }
70
92
 
71
- // src/kanban-decomposition-actions.ts
93
+ // src/kanban-contract-actions.ts
72
94
  import {
73
- assessTaskAtomicity,
74
- proposeTaskDecomposition,
75
- updateTask,
76
- verifyTaskCompletion
95
+ addContractEdge,
96
+ configureContractGraph,
97
+ evaluateTaskContractGraph,
98
+ getContractGraph,
99
+ removeContractEdge,
100
+ removeContractNode,
101
+ upsertContractNode
77
102
  } from "@wrongstack/kanban";
78
103
 
79
104
  // src/kanban-tool-results.ts
@@ -96,6 +121,137 @@ function okTask(board, task, message) {
96
121
  return { ok: true, message, board, task };
97
122
  }
98
123
 
124
+ // src/kanban-contract-actions.ts
125
+ async function handleKanbanContractAction(projectRoot, input, actor) {
126
+ switch (input.action) {
127
+ case "get_contract_graph": {
128
+ if (!input.boardId) return fail("get_contract_graph requires boardId.");
129
+ const found = await getContractGraph(projectRoot, input.boardId);
130
+ if (!found) return fail("Board not found.");
131
+ const evaluated = input.taskId ? await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId) : null;
132
+ if (input.taskId && !evaluated) return fail("Task not found on this board.");
133
+ return {
134
+ ok: true,
135
+ 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.",
136
+ board: found.board,
137
+ contractGraph: found.graph,
138
+ ...evaluated ? { contractEvaluation: evaluated.evaluation } : {}
139
+ };
140
+ }
141
+ case "configure_contract_graph": {
142
+ if (!input.boardId) return fail("configure_contract_graph requires boardId.");
143
+ const enforcement = input.contractEnforcement ?? "advisory";
144
+ const board = await configureContractGraph(projectRoot, input.boardId, enforcement);
145
+ return board ? okBoard(board, `Contract map enforcement set to ${enforcement}.`) : fail("Board not found.");
146
+ }
147
+ case "upsert_contract_node": {
148
+ if (!input.boardId || !input.taskId) {
149
+ return fail("upsert_contract_node requires boardId and taskId.");
150
+ }
151
+ if (!input.contractNodeKind || !input.contractNodeTitle) {
152
+ return fail("upsert_contract_node requires contractNodeKind and contractNodeTitle.");
153
+ }
154
+ const waiver = input.contractNodeState === "waived" ? {
155
+ actor: actor ?? "agent",
156
+ reason: input.contractWaiverReason ?? "",
157
+ at: (/* @__PURE__ */ new Date()).toISOString()
158
+ } : void 0;
159
+ if (waiver && !waiver.reason.trim()) {
160
+ return fail("A waived contract node requires contractWaiverReason.");
161
+ }
162
+ const result = await upsertContractNode(projectRoot, input.boardId, {
163
+ taskId: input.taskId,
164
+ kind: input.contractNodeKind,
165
+ title: input.contractNodeTitle,
166
+ ...input.contractNodeId !== void 0 ? { id: input.contractNodeId } : {},
167
+ ...input.contractNodeDescription !== void 0 ? { description: input.contractNodeDescription } : {},
168
+ ...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
169
+ ...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
170
+ ...input.contractCheckId !== void 0 ? { checkId: input.contractCheckId } : {},
171
+ ...input.contractMetricId !== void 0 ? { metricId: input.contractMetricId } : {},
172
+ ...waiver ? { waiver } : {},
173
+ ...actor !== void 0 ? { createdBy: actor } : {}
174
+ });
175
+ if (!result) return fail("Board or task not found.");
176
+ return {
177
+ ok: true,
178
+ message: `Contract node ${result.node.kind} "${result.node.title}" saved (${result.node.id}).`,
179
+ board: result.board,
180
+ contractGraph: result.board.contractGraph ?? null
181
+ };
182
+ }
183
+ case "remove_contract_node": {
184
+ if (!input.boardId || !input.contractNodeId) {
185
+ return fail("remove_contract_node requires boardId and contractNodeId.");
186
+ }
187
+ const board = await removeContractNode(projectRoot, input.boardId, input.contractNodeId);
188
+ return board ? okBoard(board, "Contract node removed, along with every edge that touched it.") : fail("Contract node not found.");
189
+ }
190
+ case "add_contract_edge": {
191
+ if (!input.boardId || !input.contractEdgeFrom || !input.contractEdgeTo) {
192
+ return fail("add_contract_edge requires boardId, contractEdgeFrom, and contractEdgeTo.");
193
+ }
194
+ if (!input.contractEdgeType) return fail("add_contract_edge requires contractEdgeType.");
195
+ const result = await addContractEdge(projectRoot, input.boardId, {
196
+ from: input.contractEdgeFrom,
197
+ to: input.contractEdgeTo,
198
+ type: input.contractEdgeType,
199
+ ...input.contractEdgeId !== void 0 ? { id: input.contractEdgeId } : {},
200
+ ...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
201
+ ...input.contractEdgeRationale !== void 0 ? { rationale: input.contractEdgeRationale } : {},
202
+ ...actor !== void 0 ? { createdBy: actor } : {}
203
+ });
204
+ if (!result) return fail("Board not found.");
205
+ return {
206
+ ok: true,
207
+ message: `Contract edge ${result.edge.type}: ${result.edge.from} \u2192 ${result.edge.to}.`,
208
+ board: result.board,
209
+ contractGraph: result.board.contractGraph ?? null
210
+ };
211
+ }
212
+ case "remove_contract_edge": {
213
+ if (!input.boardId || !input.contractEdgeId) {
214
+ return fail("remove_contract_edge requires boardId and contractEdgeId.");
215
+ }
216
+ const board = await removeContractEdge(projectRoot, input.boardId, input.contractEdgeId);
217
+ return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
218
+ }
219
+ default:
220
+ return void 0;
221
+ }
222
+ }
223
+
224
+ // src/kanban-decomposition-actions.ts
225
+ import {
226
+ assessTaskAtomicity,
227
+ proposeTaskDecomposition,
228
+ updateTask,
229
+ verifyTaskCompletion
230
+ } from "@wrongstack/kanban";
231
+
232
+ // src/kanban-evidence-bridge.ts
233
+ import { recordCompletedWorkEvidence } from "@wrongstack/core/utils";
234
+ function kanbanEvidenceKey(boardId, taskId) {
235
+ return `kanban:${boardId}:${taskId}`;
236
+ }
237
+ function kanbanEvidencePointer(boardId, taskId) {
238
+ return `kanban://${boardId}/${taskId}#verificationReport`;
239
+ }
240
+ function recordKanbanVerificationEvidence(ctx, report) {
241
+ try {
242
+ const passed = report.checks.filter((check) => check.status === "passed").length;
243
+ const completedAt = Date.parse(report.completedAt);
244
+ recordCompletedWorkEvidence(ctx, {
245
+ key: kanbanEvidenceKey(report.boardId, report.taskId),
246
+ source: "verification",
247
+ summary: `${report.taskTitle} \u2014 verification ${report.verdict} (${passed}/${report.checks.length} checks)`,
248
+ ...Number.isFinite(completedAt) ? { completedAt } : {},
249
+ evidence: kanbanEvidencePointer(report.boardId, report.taskId)
250
+ });
251
+ } catch {
252
+ }
253
+ }
254
+
99
255
  // src/kanban-decomposition-actions.ts
100
256
  async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
101
257
  switch (input.action) {
@@ -184,6 +340,8 @@ import {
184
340
  addGoalMetricToTask,
185
341
  addLinkToTask,
186
342
  addNoteToTask,
343
+ getKanbanWorkbench,
344
+ removeCheckFromTask,
187
345
  updateCheckOnTask,
188
346
  updateGoalMetricOnTask
189
347
  } from "@wrongstack/kanban";
@@ -241,6 +399,16 @@ async function requireBoard(projectRoot, boardId) {
241
399
  // src/kanban-detail-actions.ts
242
400
  async function handleKanbanDetailAction(projectRoot, input) {
243
401
  switch (input.action) {
402
+ case "workbench": {
403
+ const workbench = await getKanbanWorkbench(projectRoot, {
404
+ ...input.limit !== void 0 ? { limitPerLane: input.limit, alertLimit: input.limit } : {}
405
+ });
406
+ return {
407
+ ok: true,
408
+ message: `${workbench.totals.now} now, ${workbench.totals.next} next, ${workbench.totals.blocked} blocked, ${workbench.totals.review} review; ${workbench.alertTotal} alert(s).`,
409
+ workbench
410
+ };
411
+ }
244
412
  case "add_dependency": {
245
413
  if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
246
414
  return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
@@ -293,8 +461,9 @@ async function handleKanbanDetailAction(projectRoot, input) {
293
461
  }
294
462
  const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
295
463
  description: input.checkDescription,
296
- type: "manual",
297
- status: input.checkStatus
464
+ type: input.checkType ?? "manual",
465
+ status: input.checkStatus,
466
+ ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
298
467
  });
299
468
  return board ? okBoard(board, "Check added.") : fail("Task not found.");
300
469
  }
@@ -309,11 +478,27 @@ async function handleKanbanDetailAction(projectRoot, input) {
309
478
  input.checkId,
310
479
  {
311
480
  ...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
312
- ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
481
+ ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {},
482
+ // Promoting an existing manual criterion to an executable one is the
483
+ // common repair: the card was written before anyone knew the command.
484
+ ...input.checkType !== void 0 ? { type: input.checkType } : {},
485
+ ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
313
486
  }
314
487
  );
315
488
  return board ? okBoard(board, "Check updated.") : fail("Check not found.");
316
489
  }
490
+ case "remove_check": {
491
+ if (!input.boardId || !input.taskId || !input.checkId) {
492
+ return fail("remove_check requires boardId, taskId, and checkId.");
493
+ }
494
+ const board = await removeCheckFromTask(
495
+ projectRoot,
496
+ input.boardId,
497
+ input.taskId,
498
+ input.checkId
499
+ );
500
+ return board ? okBoard(board, "Acceptance criterion removed.") : fail("Check not found on this task.");
501
+ }
317
502
  case "add_note": {
318
503
  if (!input.boardId || !input.taskId || !input.note)
319
504
  return fail("add_note requires boardId, taskId, and note.");
@@ -344,47 +529,24 @@ async function handleKanbanDetailAction(projectRoot, input) {
344
529
  }
345
530
  }
346
531
 
347
- // src/kanban-board-inputs.ts
348
- function agentSettableGate(enforcement) {
349
- if (enforcement === void 0 || enforcement === "off") return {};
350
- return { completionGate: { enforcement } };
351
- }
352
- function boardCreateInput(input, title) {
353
- return {
354
- title,
355
- ...input.description !== void 0 ? { description: input.description } : {},
356
- ...input.tags !== void 0 ? { tags: input.tags } : {},
357
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
358
- ...input.atomicityMode !== void 0 ? {
359
- atomicity: {
360
- mode: input.atomicityMode,
361
- decomposition: input.atomicityDecomposition ?? "propose"
362
- }
363
- } : {},
364
- ...agentSettableGate(input.gateEnforcement)
365
- };
366
- }
367
- function boardUpdatePatch(input) {
368
- return {
369
- ...input.title !== void 0 ? { title: input.title } : {},
370
- ...input.description !== void 0 ? { description: input.description } : {},
371
- ...input.tags !== void 0 ? { tags: input.tags } : {},
372
- ...input.atomicityMode !== void 0 ? {
373
- atomicity: {
374
- mode: input.atomicityMode,
375
- decomposition: input.atomicityDecomposition ?? "propose"
376
- }
377
- } : {},
378
- ...agentSettableGate(input.gateEnforcement)
379
- };
380
- }
381
- function duplicateBoardOptions(input) {
382
- return {
383
- ...input.title !== void 0 ? { title: input.title } : {},
384
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
385
- ...input.includeTasks !== void 0 ? { includeTasks: input.includeTasks } : {},
386
- ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
387
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {}
532
+ // src/kanban-presence.ts
533
+ import { touchKanbanPresence } from "@wrongstack/kanban";
534
+ function createKanbanPresenceWrapper(projectRoot, input, ctx) {
535
+ return async (result) => {
536
+ const boardId = result.board?.id ?? input.boardId;
537
+ if (!result.ok || !boardId || !ctx.session?.id || !ctx.agentId) return result;
538
+ try {
539
+ const board = await touchKanbanPresence(projectRoot, boardId, {
540
+ sessionId: ctx.session.id,
541
+ agentId: ctx.agentId,
542
+ agentName: ctx.agentName,
543
+ taskId: input.taskId ?? result.task?.id,
544
+ runTaskId: input.runTaskId
545
+ });
546
+ return board ? { ...result, board } : result;
547
+ } catch {
548
+ return result;
549
+ }
388
550
  };
389
551
  }
390
552
 
@@ -411,14 +573,25 @@ function taskInput(input) {
411
573
  ...input.order !== void 0 ? { order: input.order } : {},
412
574
  ...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
413
575
  ...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
576
+ // The system prompt has always told the model it may "set atomic: true"
577
+ // when creating a composite parent. It could not: the field reached
578
+ // neither the create input nor the patch, so the instruction described a
579
+ // capability that did not exist and the attempt was silently dropped.
580
+ ...input.atomic !== void 0 ? { atomic: input.atomic } : {},
414
581
  ...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
415
582
  ...input.checkDescription !== void 0 ? {
416
583
  successCriteria: [
417
584
  {
418
585
  id: randomUUID(),
419
586
  description: input.checkDescription,
420
- type: "manual",
421
- status: input.checkStatus ?? "pending"
587
+ // `manual` only as the fallback. Hard-coding it here meant every
588
+ // agent-authored criterion was unverifiable by construction: the
589
+ // deterministic plugins never matched, the registry passed the
590
+ // hand-set status straight through, and "verified" collapsed into
591
+ // "the author ticked its own box".
592
+ type: input.checkType ?? "manual",
593
+ status: input.checkStatus ?? "pending",
594
+ ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
422
595
  }
423
596
  ]
424
597
  } : {},
@@ -454,22 +627,23 @@ function taskInput(input) {
454
627
  }
455
628
  ]
456
629
  } : {},
457
- ...input.graphId !== void 0 ? {
630
+ ...[input.graphId, input.specId, input.specRequirementId].some((value) => value !== void 0) ? {
458
631
  origin: {
459
632
  system: input.sourceSystem ?? "kanban-tool",
460
633
  ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
461
634
  ...input.specId !== void 0 ? { specId: input.specId } : {},
635
+ ...input.specRequirementId !== void 0 ? { specRequirementId: input.specRequirementId } : {},
462
636
  ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {}
463
637
  }
464
638
  } : {}
465
639
  };
466
640
  }
467
641
  function mergedDependsOn(input) {
468
- const ids = [
642
+ if (input.dependsOn === void 0 && input.dependencyTaskId === void 0) return void 0;
643
+ return [
469
644
  ...input.dependsOn ?? [],
470
645
  ...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
471
646
  ].filter((id, i, arr) => id && arr.indexOf(id) === i);
472
- return ids.length > 0 ? ids : void 0;
473
647
  }
474
648
  function taskPatch(input) {
475
649
  return {
@@ -483,7 +657,15 @@ function taskPatch(input) {
483
657
  status: input.status,
484
658
  labels: input.labels,
485
659
  assignedAgent: input.agentId,
486
- ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
660
+ ...mergedDependsOn(input) !== void 0 ? { dependsOn: mergedDependsOn(input) } : {},
661
+ // `atomic` and `childTaskIds` are the composite-parent contract, and the
662
+ // managed gate reads both: an `atomic` parent may not move forward without
663
+ // children, and may not reach Done until every child is completed. The
664
+ // manager has always accepted both on a patch; only this surface withheld
665
+ // them, so `split_atomic` was a one-way door — delete the children and the
666
+ // parent was stranded with no way to declare itself a leaf again.
667
+ ...input.atomic !== void 0 ? { atomic: input.atomic } : {},
668
+ ...input.childTaskIds !== void 0 ? { childTaskIds: input.childTaskIds } : {},
487
669
  ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
488
670
  ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
489
671
  };
@@ -543,8 +725,8 @@ function assignmentForTaskCreate(input) {
543
725
  }
544
726
 
545
727
  // src/kanban-tool-schema.ts
546
- var KANBAN_TOOL_DESCRIPTION = "Manage and audit project-scoped multi-kanban boards through the shared IPC Kanban server and its SQLite store. Managed cards enforce fully specified details and adjacent Backlog \u2192 Todo \u2192 Running \u2192 Review \u2192 Done transitions with persistent comments and evidence. Successful board access records live agent/session presence. Use verify_completion to validate a task against its success criteria and persist the verification report. Use split_atomic to split a task into child tasks with parent.atomic=true in a single atomic board mutation, enforcing subtree verification before the parent can complete.";
547
- var KANBAN_TOOL_USAGE_HINT = "Use this for durable project kanban state. Reassess with get_board whenever evidence changes; agents may add, update, split, merge, reprioritize, or remove tasks so the board remains a live plan. Presence includes active/last-seen session and agent metadata. For managed boards, fully fill card details, use transition_task after every material step, attach truthful evidence, and never use update_task/move_task to bypass lifecycle guards. Worker completion enters Review; only passed acceptance criteria plus review evidence allow Done. Use verify_completion to generate a verification report (persisted automatically) and split_atomic to atomically create child subtasks with the atomic flag pre-set.";
728
+ 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.";
729
+ 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.';
548
730
  var KANBAN_INPUT_SCHEMA = {
549
731
  type: "object",
550
732
  properties: {
@@ -557,6 +739,7 @@ var KANBAN_INPUT_SCHEMA = {
557
739
  "duplicate_board",
558
740
  "update_board",
559
741
  "adopt_managed_lifecycle",
742
+ "release_managed_lifecycle",
560
743
  "delete_board",
561
744
  "generate_board",
562
745
  "export_markdown",
@@ -567,15 +750,14 @@ var KANBAN_INPUT_SCHEMA = {
567
750
  "search_tasks",
568
751
  "ready_tasks",
569
752
  "snapshot",
570
- "add_column",
571
- "update_column",
572
- "delete_column",
753
+ "workbench",
573
754
  "add_task",
574
755
  "split_task",
575
756
  "merge_tasks",
576
757
  "copy_task",
577
758
  "transfer_task",
578
759
  "get_task",
760
+ "start_task",
579
761
  "update_task",
580
762
  "transition_task",
581
763
  "repair_managed_projection",
@@ -596,18 +778,29 @@ var KANBAN_INPUT_SCHEMA = {
596
778
  "update_goal_metric",
597
779
  "add_check",
598
780
  "update_check",
781
+ "remove_check",
599
782
  "add_note",
600
783
  "add_link",
601
784
  "verify_completion",
602
785
  "split_atomic",
603
786
  "assess_atomicity",
604
- "propose_decomposition"
787
+ "propose_decomposition",
788
+ "get_contract_graph",
789
+ "configure_contract_graph",
790
+ "upsert_contract_node",
791
+ "remove_contract_node",
792
+ "add_contract_edge",
793
+ "remove_contract_edge"
605
794
  ]
606
795
  },
607
796
  boardId: { type: "string" },
608
797
  taskId: { type: "string" },
609
798
  taskIds: { type: "array", items: { type: "string" } },
610
799
  chainId: { type: "string" },
800
+ fromNodeId: { type: "string" },
801
+ toNodeId: { type: "string" },
802
+ baseline: { oneOf: [{ type: "string" }, { type: "number" }] },
803
+ threshold: { oneOf: [{ type: "string" }, { type: "number" }] },
611
804
  columnId: { type: "string" },
612
805
  targetBoardId: { type: "string" },
613
806
  targetColumnId: { type: "string" },
@@ -689,12 +882,26 @@ var KANBAN_INPUT_SCHEMA = {
689
882
  costCeilingUsd: { type: "number" },
690
883
  retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
691
884
  lastFailureKind: { type: "string" },
692
- dependsOn: { type: "array", items: { type: "string" } },
885
+ dependsOn: {
886
+ type: "array",
887
+ items: { type: "string" },
888
+ 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."
889
+ },
890
+ atomic: {
891
+ type: "boolean",
892
+ description: "Composite parent (true) or executable leaf (false). Set false to make a stranded parent a leaf again after its children were dropped."
893
+ },
894
+ childTaskIds: {
895
+ type: "array",
896
+ items: { type: "string" },
897
+ description: "Children of a composite parent. On update_task an explicit empty array detaches them all."
898
+ },
693
899
  estimatedHours: { type: "number" },
694
900
  actualHours: { type: "number" },
695
901
  taskGraph: { type: "object" },
696
902
  graphId: { type: "string" },
697
903
  specId: { type: "string" },
904
+ specRequirementId: { type: "string" },
698
905
  sourceSystem: { type: "string" },
699
906
  phaseId: { type: "string" },
700
907
  preserveOriginTaskIds: { type: "boolean" },
@@ -722,6 +929,74 @@ var KANBAN_INPUT_SCHEMA = {
722
929
  checkId: { type: "string" },
723
930
  checkDescription: { type: "string" },
724
931
  checkStatus: { type: "string", enum: ["pending", "passed", "failed", "skipped"] },
932
+ checkType: {
933
+ type: "string",
934
+ // Only types a verifier can actually execute. `manual` is the default and
935
+ // means a human or agent asserts the status by hand. The rest are run by
936
+ // `verify_completion` against the default deterministic registry. Types
937
+ // with no plugin in that registry (`auto`, `review`, `agent`, `council`)
938
+ // are deliberately omitted: offering them would produce criteria that
939
+ // silently report `skipped — no verifier plugin registered`.
940
+ enum: ["manual", "command", "test", "file_exists", "file_matches", "git_diff", "metric"],
941
+ 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.'
942
+ },
943
+ checkNotes: {
944
+ type: "string",
945
+ 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"}.'
946
+ },
947
+ // ── Contract map ───────────────────────────────────────────────────
948
+ // The card contract: what this work targets, what it must not break, what
949
+ // it risks, and what verifies it. Advisory by default — the readiness gate
950
+ // deliberately does not require map structure, so a map is an operator
951
+ // review aid, not work the model must complete before implementing.
952
+ contractEnforcement: {
953
+ type: "string",
954
+ enum: ["off", "advisory", "strict"],
955
+ description: "Board-level contract map enforcement. Default when first configured: advisory."
956
+ },
957
+ contractNodeId: { type: "string" },
958
+ contractNodeKind: {
959
+ type: "string",
960
+ enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"],
961
+ 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."
962
+ },
963
+ contractNodeTitle: { type: "string" },
964
+ contractNodeDescription: { type: "string" },
965
+ contractNodeState: {
966
+ type: "string",
967
+ enum: ["unknown", "active", "satisfied", "violated", "waived", "resolved"]
968
+ },
969
+ contractNodeEnforcement: {
970
+ type: "string",
971
+ enum: ["blocking", "advisory", "informational"]
972
+ },
973
+ /** Bind a node to an acceptance criterion or goal metric already on the task. */
974
+ contractCheckId: { type: "string" },
975
+ contractMetricId: { type: "string" },
976
+ contractWaiverReason: {
977
+ type: "string",
978
+ description: 'Required, with an actor, when contractNodeState is "waived".'
979
+ },
980
+ contractEdgeId: { type: "string" },
981
+ contractEdgeFrom: {
982
+ type: "string",
983
+ description: 'A contract node id, or a task id (bare or "task:<id>") for the card endpoint.'
984
+ },
985
+ contractEdgeTo: { type: "string" },
986
+ contractEdgeType: {
987
+ type: "string",
988
+ enum: [
989
+ "targets",
990
+ "affects",
991
+ "must_preserve",
992
+ "exposes",
993
+ "verified_by",
994
+ "conflicts_with",
995
+ "derived_from",
996
+ "relates_to"
997
+ ]
998
+ },
999
+ contractEdgeRationale: { type: "string" },
725
1000
  note: { type: "string" },
726
1001
  author: { type: "string" },
727
1002
  url: { type: "string" },
@@ -765,27 +1040,6 @@ var KANBAN_INPUT_SCHEMA = {
765
1040
  required: ["action"]
766
1041
  };
767
1042
 
768
- // src/kanban-presence.ts
769
- import { touchKanbanPresence } from "@wrongstack/kanban";
770
- function createKanbanPresenceWrapper(projectRoot, input, ctx) {
771
- return async (result) => {
772
- const boardId = result.board?.id ?? input.boardId;
773
- if (!result.ok || !boardId || !ctx.session?.id || !ctx.agentId) return result;
774
- try {
775
- const board = await touchKanbanPresence(projectRoot, boardId, {
776
- sessionId: ctx.session.id,
777
- agentId: ctx.agentId,
778
- agentName: ctx.agentName,
779
- taskId: input.taskId ?? result.task?.id,
780
- runTaskId: input.runTaskId
781
- });
782
- return board ? { ...result, board } : result;
783
- } catch {
784
- return result;
785
- }
786
- };
787
- }
788
-
789
1043
  // src/session-kanban.ts
790
1044
  import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
791
1045
  import {
@@ -795,18 +1049,27 @@ import {
795
1049
  mutateTasks
796
1050
  } from "@wrongstack/core/storage";
797
1051
  import { deserializeTaskGraph } from "@wrongstack/core/tasking";
798
- import { resolveWstackPaths } from "@wrongstack/core/utils";
1052
+ import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
799
1053
  import {
800
1054
  bridgeKanbanSupervisor,
1055
+ compactSessionMirrorBoard,
801
1056
  createBoard,
1057
+ DEFAULT_COLUMNS,
802
1058
  getBoard as getBoard2,
1059
+ getDependencyReadinessIssues,
1060
+ getKanbanOrchestrationSnapshot,
803
1061
  listBoards,
1062
+ pruneSessionBoards,
804
1063
  removeBoard,
805
1064
  syncBoardFromTaskGraph,
806
1065
  touchKanbanPresence as touchKanbanPresence2,
807
1066
  updateBoard
808
1067
  } from "@wrongstack/kanban";
1068
+ var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
1069
+ ...column
1070
+ }));
809
1071
  function taskFileToSerializedGraph(tasks, sessionId) {
1072
+ const graphId = `session:${sessionId}`;
810
1073
  const ids = new Set(tasks.map((task) => task.id));
811
1074
  const nodes = tasks.map((task, index) => ({
812
1075
  id: task.id,
@@ -815,6 +1078,7 @@ function taskFileToSerializedGraph(tasks, sessionId) {
815
1078
  type: task.type,
816
1079
  priority: task.priority,
817
1080
  status: task.status,
1081
+ specRequirementId: `${graphId}:${task.id}`,
818
1082
  ...task.assignee ? { assignee: task.assignee } : {},
819
1083
  ...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
820
1084
  createdAt: index,
@@ -832,8 +1096,9 @@ function taskFileToSerializedGraph(tasks, sessionId) {
832
1096
  const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
833
1097
  return {
834
1098
  // Keep the historical graph id so existing mirrored task cards are reused.
835
- id: `session:${sessionId}`,
836
- specId: `session:${sessionId}`,
1099
+ id: graphId,
1100
+ specId: graphId,
1101
+ requiredRequirementIds: nodes.map((node) => node.specRequirementId),
837
1102
  title: "Session tasks",
838
1103
  nodes,
839
1104
  edges,
@@ -874,8 +1139,14 @@ var kanbanTool = {
874
1139
  }
875
1140
  case "create_board": {
876
1141
  if (!input.title) return fail("create_board requires title.");
1142
+ const existing = (await listBoards2(projectRoot)).filter(
1143
+ (candidate) => (candidate.kind ?? "project") === "project"
1144
+ );
877
1145
  const board = await createBoard2(projectRoot, boardCreateInput(input, input.title));
878
- return { ok: true, message: `Board created: ${board.title}`, board };
1146
+ 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(
1147
+ ", "
1148
+ )}${existing.length > 3 ? ", \u2026" : ""}. If this work belongs to one of them, add_task there instead and delete this board.` : "";
1149
+ return { ok: true, message: `Board created: ${board.title}.${note}`, board };
879
1150
  }
880
1151
  case "update_board": {
881
1152
  if (!input.boardId) return fail("update_board requires boardId.");
@@ -904,9 +1175,27 @@ var kanbanTool = {
904
1175
  });
905
1176
  return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
906
1177
  }
1178
+ // Adoption used to be a one-way door: the strict lifecycle carries
1179
+ // acceptance-criteria, verification-report, review-evidence and
1180
+ // one-stage-at-a-time gates, and nothing on the tool surface could
1181
+ // undo it, so a board adopted once kept its ceremony forever. The
1182
+ // gates are worth having where a fleet is supervised; they are not
1183
+ // worth being unable to leave. Cards and columns are untouched.
1184
+ case "release_managed_lifecycle": {
1185
+ if (!input.boardId) return fail("release_managed_lifecycle requires boardId.");
1186
+ const board = await updateBoard2(projectRoot, input.boardId, { lifecycle: null });
1187
+ return board ? okBoard(
1188
+ board,
1189
+ "Managed lifecycle released; the board now tracks work without strict gates."
1190
+ ) : fail("Board not found.");
1191
+ }
907
1192
  case "duplicate_board": {
908
1193
  if (!input.boardId) return fail("duplicate_board requires boardId.");
909
- const board = await duplicateBoard(projectRoot, input.boardId, duplicateBoardOptions(input));
1194
+ const board = await duplicateBoard(
1195
+ projectRoot,
1196
+ input.boardId,
1197
+ duplicateBoardOptions(input)
1198
+ );
910
1199
  return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
911
1200
  }
912
1201
  case "delete_board": {
@@ -919,8 +1208,7 @@ var kanbanTool = {
919
1208
  const boardInput = createBoardFromText({
920
1209
  description: input.description,
921
1210
  ...input.title !== void 0 ? { title: input.title } : {},
922
- ...input.context !== void 0 ? { context: input.context } : {},
923
- ...input.columns !== void 0 ? { columns: input.columns } : {}
1211
+ ...input.context !== void 0 ? { context: input.context } : {}
924
1212
  });
925
1213
  const board = await createBoard2(projectRoot, boardInput);
926
1214
  for (const taskInput2 of parseLinesIntoTasks(
@@ -1058,7 +1346,7 @@ var kanbanTool = {
1058
1346
  return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
1059
1347
  }
1060
1348
  case "snapshot": {
1061
- const snapshot = await getKanbanOrchestrationSnapshot(projectRoot, {
1349
+ const snapshot = await getKanbanOrchestrationSnapshot2(projectRoot, {
1062
1350
  query: input.query,
1063
1351
  boardId: input.boardId,
1064
1352
  assignedAgent: input.agentId,
@@ -1073,41 +1361,11 @@ var kanbanTool = {
1073
1361
  snapshot
1074
1362
  };
1075
1363
  }
1076
- case "add_column": {
1077
- if (!input.boardId || !input.title) return fail("add_column requires boardId and title.");
1078
- const result2 = await addColumn(projectRoot, input.boardId, {
1079
- title: input.title,
1080
- ...input.description !== void 0 ? { description: input.description } : {}
1081
- });
1082
- return result2 ? okBoard(result2.board, "Column added.") : fail("Board not found.");
1083
- }
1084
- case "update_column": {
1085
- if (!input.boardId || !input.columnId)
1086
- return fail("update_column requires boardId and columnId.");
1087
- const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
1088
- ...input.title !== void 0 ? { title: input.title } : {},
1089
- ...input.description !== void 0 ? { description: input.description } : {},
1090
- ...input.order !== void 0 ? { order: input.order } : {}
1091
- });
1092
- return board ? okBoard(board, "Column updated.") : fail("Column not found.");
1093
- }
1094
- case "delete_column": {
1095
- if (!input.boardId || !input.columnId)
1096
- return fail("delete_column requires boardId and columnId.");
1097
- const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
1098
- moveTasksToColumnId: input.moveTasksToColumnId
1099
- });
1100
- return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
1101
- }
1102
1364
  case "add_task": {
1103
1365
  if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
1104
1366
  const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
1105
1367
  if (!result2) return fail("Board not found.");
1106
- return okTask(
1107
- result2.board,
1108
- result2.task,
1109
- `Task added.${atomicityNudge(result2.task)}`
1110
- );
1368
+ return okTask(result2.board, result2.task, `Task added.${atomicityNudge(result2.task)}`);
1111
1369
  }
1112
1370
  case "split_task": {
1113
1371
  if (!input.boardId || !input.taskId || !input.childTitles?.length) {
@@ -1166,10 +1424,98 @@ var kanbanTool = {
1166
1424
  return result2 ? okTask(result2.targetBoard, result2.task, "Task transferred to target board.") : fail("Board or task not found.");
1167
1425
  }
1168
1426
  case "get_task": {
1169
- if (!input.boardId || !input.taskId) return fail("get_task requires boardId and taskId.");
1427
+ if (!input.boardId || !input.taskId)
1428
+ return fail("get_task requires boardId and taskId.");
1170
1429
  const task = await getTask(projectRoot, input.boardId, input.taskId);
1171
1430
  return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
1172
1431
  }
1432
+ case "start_task": {
1433
+ if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
1434
+ return fail("start_task requires boardId, taskId, author, and transitionComment.");
1435
+ }
1436
+ let board = await getBoard3(projectRoot, input.boardId);
1437
+ let task = board?.tasks.find((candidate) => candidate.id === input.taskId);
1438
+ if (!board || !task) return fail("Board or task not found.");
1439
+ const readiness = evaluateContractGraphReadiness(board, task.id);
1440
+ if (!readiness.ready) {
1441
+ return fail(
1442
+ `Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
1443
+ );
1444
+ }
1445
+ if (board.lifecycle?.mode !== "managed") {
1446
+ const now = /* @__PURE__ */ new Date();
1447
+ const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
1448
+ status: "running",
1449
+ agentId: input.agentId ?? input.author,
1450
+ leaseId: input.leaseId ?? randomUUID2(),
1451
+ claimedAt: input.claimedAt ?? now.toISOString(),
1452
+ heartbeatAt: input.heartbeatAt ?? now.toISOString(),
1453
+ leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
1454
+ attempt: input.attempt ?? 1,
1455
+ maxAttempts: input.maxAttempts ?? 3
1456
+ });
1457
+ if (!assigned) return fail("Task assignment could not be started.");
1458
+ const started = await updateTask2(projectRoot, board.id, task.id, {
1459
+ status: "in_progress"
1460
+ });
1461
+ const current = started ?? assigned;
1462
+ const claimed = task;
1463
+ const currentTask = current.tasks.find((candidate) => candidate.id === claimed.id) ?? claimed;
1464
+ ctx.setCurrentKanbanTask?.(currentTask.id, current.id);
1465
+ return okTask(
1466
+ current,
1467
+ currentTask,
1468
+ "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."
1469
+ );
1470
+ }
1471
+ let stage = task.lifecycle?.currentStage;
1472
+ if (stage === "backlog") {
1473
+ const moved = await transitionTask(projectRoot, board.id, task.id, {
1474
+ to: "todo",
1475
+ actor: input.author,
1476
+ comment: input.transitionComment
1477
+ });
1478
+ if (!moved) return fail("Task could not enter Todo.");
1479
+ board = moved.board;
1480
+ task = moved.task;
1481
+ stage = task.lifecycle?.currentStage;
1482
+ }
1483
+ if (stage === "todo" || stage === "review") {
1484
+ const now = /* @__PURE__ */ new Date();
1485
+ const leaseId = input.leaseId ?? randomUUID2();
1486
+ const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
1487
+ status: "running",
1488
+ agentId: input.agentId ?? input.author,
1489
+ leaseId,
1490
+ claimedAt: input.claimedAt ?? now.toISOString(),
1491
+ heartbeatAt: input.heartbeatAt ?? now.toISOString(),
1492
+ leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
1493
+ attempt: input.attempt ?? 1,
1494
+ maxAttempts: input.maxAttempts ?? 3
1495
+ });
1496
+ if (!assigned) return fail("Task assignment could not be started.");
1497
+ const moved = await transitionTask(projectRoot, board.id, task.id, {
1498
+ to: "running",
1499
+ actor: input.author,
1500
+ comment: input.transitionComment
1501
+ });
1502
+ if (!moved) return fail("Task could not enter Running.");
1503
+ board = moved.board;
1504
+ task = moved.task;
1505
+ stage = task.lifecycle?.currentStage;
1506
+ }
1507
+ if (stage !== "running" || task.assignment?.status !== "running") {
1508
+ return fail(
1509
+ `start_task only accepts Backlog, Todo, Review repair, or live Running cards (current: ${stage ?? "unknown"}).`
1510
+ );
1511
+ }
1512
+ ctx.setCurrentKanbanTask(task.id, board.id);
1513
+ return okTask(
1514
+ board,
1515
+ task,
1516
+ "Task is active; runtime Kanban governance is now bound to this run."
1517
+ );
1518
+ }
1173
1519
  case "update_task": {
1174
1520
  if (!input.boardId || !input.taskId)
1175
1521
  return fail("update_task requires boardId and taskId.");
@@ -1191,9 +1537,14 @@ var kanbanTool = {
1191
1537
  const boardBefore = await getBoard3(projectRoot, input.boardId);
1192
1538
  const taskBefore = boardBefore ? await getTask(projectRoot, input.boardId, input.taskId) : null;
1193
1539
  if (boardBefore && taskBefore && !taskBefore.verificationReport && (taskBefore.atomic || Boolean(taskBefore.successCriteria?.length))) {
1194
- const preGate = await verifyTaskCompletion2(projectRoot, input.boardId, taskBefore.id, {
1195
- persist: false
1196
- });
1540
+ const preGate = await verifyTaskCompletion2(
1541
+ projectRoot,
1542
+ input.boardId,
1543
+ taskBefore.id,
1544
+ {
1545
+ persist: false
1546
+ }
1547
+ );
1197
1548
  await updateTask2(projectRoot, input.boardId, taskBefore.id, {
1198
1549
  verificationReport: preGate.report,
1199
1550
  successCriteria: preGate.task.successCriteria
@@ -1225,11 +1576,20 @@ var kanbanTool = {
1225
1576
  "repair_managed_projection requires boardId, taskId, author, and transitionComment."
1226
1577
  );
1227
1578
  }
1228
- const result2 = await repairManagedTaskProjection(projectRoot, input.boardId, input.taskId, {
1229
- actor: input.author,
1230
- comment: input.transitionComment
1231
- });
1232
- return result2 ? okTask(result2.board, result2.task, "Managed card projection repaired from lifecycle history.") : fail("Board or task not found.");
1579
+ const result2 = await repairManagedTaskProjection(
1580
+ projectRoot,
1581
+ input.boardId,
1582
+ input.taskId,
1583
+ {
1584
+ actor: input.author,
1585
+ comment: input.transitionComment
1586
+ }
1587
+ );
1588
+ return result2 ? okTask(
1589
+ result2.board,
1590
+ result2.task,
1591
+ "Managed card projection repaired from lifecycle history."
1592
+ ) : fail("Board or task not found.");
1233
1593
  }
1234
1594
  case "move_task": {
1235
1595
  if (!input.boardId || !input.taskId || !input.targetColumnId) {
@@ -1248,6 +1608,9 @@ var kanbanTool = {
1248
1608
  if (!input.boardId || !input.taskId)
1249
1609
  return fail("delete_task requires boardId and taskId.");
1250
1610
  const board = await removeTask(projectRoot, input.boardId, input.taskId);
1611
+ if (board && ctx.currentKanbanTaskId === input.taskId) {
1612
+ ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
1613
+ }
1251
1614
  return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
1252
1615
  }
1253
1616
  case "set_chain": {
@@ -1366,53 +1729,45 @@ var kanbanTool = {
1366
1729
  };
1367
1730
  }
1368
1731
  } else if (board.lifecycle?.mode === "managed") {
1369
- const managedTask = board.tasks.find(
1370
- (candidate) => candidate.id === input.taskId
1371
- );
1732
+ const managedTask = board.tasks.find((candidate) => candidate.id === input.taskId);
1372
1733
  const stage = managedTask?.lifecycle?.currentStage;
1373
1734
  const actor = ctx.agentId ?? "kanban-agent";
1374
1735
  let transitionResult = null;
1375
1736
  const lifecycleWarnings = [];
1376
1737
  if (assignmentStatus === "running" && stage === "todo") {
1377
1738
  try {
1378
- transitionResult = await transitionTask(
1379
- projectRoot,
1380
- board.id,
1381
- input.taskId,
1382
- { to: "running", actor, comment: "Work started." }
1383
- );
1739
+ transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
1740
+ to: "running",
1741
+ actor,
1742
+ comment: "Work started."
1743
+ });
1384
1744
  } catch (err) {
1385
1745
  lifecycleWarnings.push(
1386
- `Lifecycle transition to Running deferred: ${err instanceof Error ? err.message : String(err)}`
1746
+ `Lifecycle transition to Running deferred: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
1387
1747
  );
1388
1748
  }
1389
1749
  }
1390
1750
  if (assignmentStatus === "completed" && stage === "running") {
1391
1751
  const comment = typeof input.lastResult === "string" && input.lastResult.trim().length > 0 ? input.lastResult.trim().slice(0, 1e3) : "Work completed.";
1392
1752
  try {
1393
- transitionResult = await transitionTask(
1394
- projectRoot,
1395
- board.id,
1396
- input.taskId,
1397
- {
1398
- to: "review",
1399
- actor,
1400
- comment,
1401
- attachment: {
1402
- url: `kanban://task/${input.taskId}/result`,
1403
- title: "Worker completion result",
1404
- type: "file"
1405
- },
1406
- patch: {
1407
- // Only patch non-description fields so the
1408
- // original card description is preserved.
1409
- ...input.agentId !== void 0 ? { assignedAgent: input.agentId } : {}
1410
- }
1753
+ transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
1754
+ to: "review",
1755
+ actor,
1756
+ comment,
1757
+ attachment: {
1758
+ url: `kanban://task/${input.taskId}/result`,
1759
+ title: "Worker completion result",
1760
+ type: "file"
1761
+ },
1762
+ patch: {
1763
+ // Only patch non-description fields so the
1764
+ // original card description is preserved.
1765
+ ...input.agentId !== void 0 ? { assignedAgent: input.agentId } : {}
1411
1766
  }
1412
- );
1767
+ });
1413
1768
  } catch (err) {
1414
1769
  lifecycleWarnings.push(
1415
- `Lifecycle transition to Review failed: ${err instanceof Error ? err.message : String(err)}`
1770
+ `Lifecycle transition to Review failed: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
1416
1771
  );
1417
1772
  }
1418
1773
  if (transitionResult) {
@@ -1427,8 +1782,16 @@ var kanbanTool = {
1427
1782
  if (verResult.report) {
1428
1783
  recordKanbanVerificationEvidence(ctx, verResult.report);
1429
1784
  }
1785
+ await updateTask2(projectRoot, board.id, input.taskId, {
1786
+ verificationReport: verResult.report,
1787
+ successCriteria: verResult.task.successCriteria
1788
+ });
1430
1789
  const verdict = verResult.report.verdict;
1431
- if (verdict === "passed") {
1790
+ if (verdict === "passed" && !resolveAutoAccept(board)) {
1791
+ lifecycleWarnings.push(
1792
+ "Verification passed, but this board does not auto-accept. The card is in Review awaiting an explicit transition_task to done."
1793
+ );
1794
+ } else if (verdict === "passed") {
1432
1795
  try {
1433
1796
  const doneResult = await transitionTask(
1434
1797
  projectRoot,
@@ -1437,6 +1800,7 @@ var kanbanTool = {
1437
1800
  {
1438
1801
  to: "done",
1439
1802
  actor,
1803
+ action: "Automated acceptance after verification",
1440
1804
  comment: "Auto-accepted: verification passed.",
1441
1805
  attachment: {
1442
1806
  url: `kanban://task/${input.taskId}/verification`,
@@ -1545,11 +1909,34 @@ var kanbanTool = {
1545
1909
  });
1546
1910
  return {
1547
1911
  ok: true,
1548
- message: `Counts: ready=${health.counts.ready}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
1912
+ message: `Counts: startable=${health.counts.startable}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
1549
1913
  queueHealth: health
1550
1914
  };
1551
1915
  }
1916
+ // Not every action is handled above. These are dispatched from here,
1917
+ // and the split has already cost real time: an agent that read this
1918
+ // file concluded `add_check` / `update_check` did not exist, wrote
1919
+ // that on a card, and spent a session trying to satisfy a gate it
1920
+ // already had the tool to clear. Keep this index in step with the
1921
+ // handlers.
1922
+ //
1923
+ // kanban-detail-actions.ts workbench · add_dependency ·
1924
+ // add_goal_metric · update_goal_metric · add_check ·
1925
+ // update_check · add_note · add_link · split_atomic
1926
+ // kanban-decomposition-actions.ts verify_completion ·
1927
+ // assess_atomicity · propose_decomposition
1928
+ // kanban-contract-actions.ts get_contract_graph ·
1929
+ // configure_contract_graph · upsert_contract_node ·
1930
+ // remove_contract_node · add_contract_edge · remove_contract_edge
1552
1931
  default:
1932
+ {
1933
+ const contractResult = await handleKanbanContractAction(
1934
+ projectRoot,
1935
+ input,
1936
+ input.author ?? input.agentId
1937
+ );
1938
+ if (contractResult !== void 0) return contractResult;
1939
+ }
1553
1940
  {
1554
1941
  const detailResult = await handleKanbanDetailAction(projectRoot, input);
1555
1942
  if (detailResult !== void 0) return detailResult;
@@ -1559,7 +1946,7 @@ var kanbanTool = {
1559
1946
  })();
1560
1947
  return withPresence(result);
1561
1948
  } catch (err) {
1562
- return fail(err instanceof Error ? err.message : String(err));
1949
+ return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
1563
1950
  }
1564
1951
  }
1565
1952
  };