@wrongstack/tools 0.303.0 → 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.
package/dist/kanban.js CHANGED
@@ -3,7 +3,6 @@ import { randomUUID as randomUUID2 } from "node:crypto";
3
3
  import { loadTasks as loadTasks2 } from "@wrongstack/core/storage";
4
4
  import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
5
5
  import {
6
- addColumn,
7
6
  addTask,
8
7
  adoptManagedLifecycle,
9
8
  assignTask,
@@ -18,7 +17,7 @@ import {
18
17
  exportBoardToTaskGraph,
19
18
  finalizeTaskCompletion,
20
19
  getBoard as getBoard3,
21
- getKanbanOrchestrationSnapshot,
20
+ getKanbanOrchestrationSnapshot as getKanbanOrchestrationSnapshot2,
22
21
  getKanbanQueueHealth,
23
22
  getTask,
24
23
  getTaskChain,
@@ -32,16 +31,16 @@ import {
32
31
  recoverStaleTaskAssignments,
33
32
  releaseTaskClaim,
34
33
  removeBoard as removeBoard2,
35
- removeColumn,
36
34
  removeTask,
37
35
  repairManagedTaskProjection,
36
+ resolveAutoAccept,
38
37
  searchKanban,
39
38
  setTaskChain,
39
+ stripLifecycleIssues,
40
40
  syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
41
41
  transferTaskToBoard,
42
42
  transitionTask,
43
43
  updateBoard as updateBoard2,
44
- updateColumn,
45
44
  updateTask as updateTask2,
46
45
  updateTaskAssignment,
47
46
  verifyTaskCompletion as verifyTaskCompletion2
@@ -91,6 +90,137 @@ function duplicateBoardOptions(input) {
91
90
  };
92
91
  }
93
92
 
93
+ // src/kanban-contract-actions.ts
94
+ import {
95
+ addContractEdge,
96
+ configureContractGraph,
97
+ evaluateTaskContractGraph,
98
+ getContractGraph,
99
+ removeContractEdge,
100
+ removeContractNode,
101
+ upsertContractNode
102
+ } from "@wrongstack/kanban";
103
+
104
+ // src/kanban-tool-results.ts
105
+ function atomicityNudge(task) {
106
+ if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
107
+ const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
108
+ return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
109
+ }
110
+ function readEnvGateEnforcement() {
111
+ const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
112
+ return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
113
+ }
114
+ function fail(message) {
115
+ return { ok: false, message };
116
+ }
117
+ function okBoard(board, message = "Board loaded.") {
118
+ return { ok: true, message, board };
119
+ }
120
+ function okTask(board, task, message) {
121
+ return { ok: true, message, board, task };
122
+ }
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
+
94
224
  // src/kanban-decomposition-actions.ts
95
225
  import {
96
226
  assessTaskAtomicity,
@@ -122,26 +252,6 @@ function recordKanbanVerificationEvidence(ctx, report) {
122
252
  }
123
253
  }
124
254
 
125
- // src/kanban-tool-results.ts
126
- function atomicityNudge(task) {
127
- if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
128
- const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
129
- return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
130
- }
131
- function readEnvGateEnforcement() {
132
- const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
133
- return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
134
- }
135
- function fail(message) {
136
- return { ok: false, message };
137
- }
138
- function okBoard(board, message = "Board loaded.") {
139
- return { ok: true, message, board };
140
- }
141
- function okTask(board, task, message) {
142
- return { ok: true, message, board, task };
143
- }
144
-
145
255
  // src/kanban-decomposition-actions.ts
146
256
  async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
147
257
  switch (input.action) {
@@ -226,20 +336,14 @@ async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
226
336
  // src/kanban-detail-actions.ts
227
337
  import {
228
338
  addCheckToTask,
229
- addContractEdge,
230
339
  addDependency,
231
340
  addGoalMetricToTask,
232
341
  addLinkToTask,
233
342
  addNoteToTask,
234
- configureContractGraph,
235
- evaluateTaskContractGraph,
236
- getContractGraph,
237
343
  getKanbanWorkbench,
238
- removeContractEdge,
239
- removeContractNode,
344
+ removeCheckFromTask,
240
345
  updateCheckOnTask,
241
- updateGoalMetricOnTask,
242
- upsertContractNode
346
+ updateGoalMetricOnTask
243
347
  } from "@wrongstack/kanban";
244
348
 
245
349
  // src/kanban-split-task-handler.ts
@@ -305,131 +409,6 @@ async function handleKanbanDetailAction(projectRoot, input) {
305
409
  workbench
306
410
  };
307
411
  }
308
- case "get_contract_graph": {
309
- if (!input.boardId) return fail("get_contract_graph requires boardId.");
310
- const result = await getContractGraph(projectRoot, input.boardId);
311
- return result ? {
312
- ok: true,
313
- message: result.graph ? `${result.graph.nodes.length} contract node(s), ${result.graph.edges.length} edge(s).` : "Contract graph is not configured.",
314
- board: result.board,
315
- ...result.graph ? { contractGraph: result.graph } : {}
316
- } : fail("Board not found.");
317
- }
318
- case "configure_contract_graph": {
319
- if (!input.boardId || !input.contractGraphEnforcement) {
320
- return fail("configure_contract_graph requires boardId and contractGraphEnforcement.");
321
- }
322
- const current = await getContractGraph(projectRoot, input.boardId);
323
- if (!current) return fail("Board not found.");
324
- if (input.contractGraphEnforcement === "strict" && current.graph?.enforcement !== "strict") {
325
- return fail(
326
- "Strict Contract Map enforcement is operator-owned. Autonomous agents may use advisory maps but may not turn them into an execution gate."
327
- );
328
- }
329
- if (current.graph?.enforcement === "strict" && input.contractGraphEnforcement !== "strict") {
330
- return fail("An autonomous agent may not loosen a strict contract graph.");
331
- }
332
- const board = await configureContractGraph(
333
- projectRoot,
334
- input.boardId,
335
- input.contractGraphEnforcement
336
- );
337
- return board ? okBoard(board, "Contract graph configured.") : fail("Board not found.");
338
- }
339
- case "upsert_contract_node": {
340
- if (!input.boardId || !input.taskId || !input.contractNodeKind || !input.title) {
341
- return fail("upsert_contract_node requires boardId, taskId, contractNodeKind, and title.");
342
- }
343
- if (input.contractNodeState === "waived") {
344
- return fail(
345
- "The autonomous kanban tool may not waive contract nodes; a human-owned review surface must record that exception."
346
- );
347
- }
348
- if (input.contractNodeId) {
349
- const current = await getContractGraph(projectRoot, input.boardId);
350
- const existing = current?.graph?.nodes.find((node) => node.id === input.contractNodeId);
351
- if (current?.graph?.enforcement === "strict" && existing && (existing.kind !== input.contractNodeKind || input.contractEnforcement !== void 0 && input.contractEnforcement !== existing.enforcement)) {
352
- return fail(
353
- "The autonomous kanban tool may not change the kind or enforcement of an existing strict contract node."
354
- );
355
- }
356
- }
357
- const result = await upsertContractNode(projectRoot, input.boardId, {
358
- ...input.contractNodeId ? { id: input.contractNodeId } : {},
359
- taskId: input.taskId,
360
- kind: input.contractNodeKind,
361
- title: input.title,
362
- ...input.description !== void 0 ? { description: input.description } : {},
363
- ...input.contractEnforcement !== void 0 ? { enforcement: input.contractEnforcement } : {},
364
- ...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
365
- ...input.checkId !== void 0 ? { checkId: input.checkId } : {},
366
- ...input.metricId !== void 0 ? { metricId: input.metricId } : {},
367
- ...input.baseline !== void 0 ? { baseline: input.baseline } : {},
368
- ...input.threshold !== void 0 ? { threshold: input.threshold } : {},
369
- ...input.author !== void 0 ? { createdBy: input.author } : {}
370
- });
371
- return result ? {
372
- ...okBoard(result.board, "Contract node saved."),
373
- contractGraph: result.board.contractGraph
374
- } : fail("Task not found.");
375
- }
376
- case "link_contract_nodes": {
377
- if (!input.boardId || !input.fromNodeId || !input.toNodeId || !input.contractEdgeType) {
378
- return fail(
379
- "link_contract_nodes requires boardId, fromNodeId, toNodeId, and contractEdgeType."
380
- );
381
- }
382
- const result = await addContractEdge(projectRoot, input.boardId, {
383
- from: input.fromNodeId,
384
- to: input.toNodeId,
385
- type: input.contractEdgeType,
386
- ...input.contractEdgeId ? { id: input.contractEdgeId } : {},
387
- ...input.contractEnforcement ? { enforcement: input.contractEnforcement } : {},
388
- ...input.contractRationale ? { rationale: input.contractRationale } : {},
389
- ...input.author ? { createdBy: input.author } : {}
390
- });
391
- return result ? {
392
- ...okBoard(result.board, "Contract edge added."),
393
- contractGraph: result.board.contractGraph
394
- } : fail("Board not found.");
395
- }
396
- case "remove_contract_node": {
397
- if (!input.boardId || !input.contractNodeId) {
398
- return fail("remove_contract_node requires boardId and contractNodeId.");
399
- }
400
- const current = await getContractGraph(projectRoot, input.boardId);
401
- const node = current?.graph?.nodes.find((candidate) => candidate.id === input.contractNodeId);
402
- if (current?.graph?.enforcement === "strict" && node?.enforcement === "blocking") {
403
- return fail("The autonomous kanban tool may not remove a blocking strict contract node.");
404
- }
405
- const board = await removeContractNode(projectRoot, input.boardId, input.contractNodeId);
406
- return board ? okBoard(board, "Contract node removed.") : fail("Contract node not found.");
407
- }
408
- case "remove_contract_edge": {
409
- if (!input.boardId || !input.contractEdgeId) {
410
- return fail("remove_contract_edge requires boardId and contractEdgeId.");
411
- }
412
- const current = await getContractGraph(projectRoot, input.boardId);
413
- const edge = current?.graph?.edges.find((candidate) => candidate.id === input.contractEdgeId);
414
- if (current?.graph?.enforcement === "strict" && edge?.enforcement === "blocking") {
415
- return fail("The autonomous kanban tool may not remove a blocking strict contract edge.");
416
- }
417
- const board = await removeContractEdge(projectRoot, input.boardId, input.contractEdgeId);
418
- return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
419
- }
420
- case "evaluate_contract_graph": {
421
- if (!input.boardId || !input.taskId) {
422
- return fail("evaluate_contract_graph requires boardId and taskId.");
423
- }
424
- const result = await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId);
425
- return result ? {
426
- ok: result.evaluation.allowed,
427
- message: result.evaluation.allowed ? "Contract graph is closed." : `Contract graph has ${result.evaluation.issues.length} unresolved issue(s).`,
428
- board: result.board,
429
- contractGraph: result.board.contractGraph,
430
- contractEvaluation: result.evaluation
431
- } : fail("Task not found.");
432
- }
433
412
  case "add_dependency": {
434
413
  if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
435
414
  return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
@@ -482,8 +461,9 @@ async function handleKanbanDetailAction(projectRoot, input) {
482
461
  }
483
462
  const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
484
463
  description: input.checkDescription,
485
- type: "manual",
486
- status: input.checkStatus
464
+ type: input.checkType ?? "manual",
465
+ status: input.checkStatus,
466
+ ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
487
467
  });
488
468
  return board ? okBoard(board, "Check added.") : fail("Task not found.");
489
469
  }
@@ -498,11 +478,27 @@ async function handleKanbanDetailAction(projectRoot, input) {
498
478
  input.checkId,
499
479
  {
500
480
  ...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
501
- ...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 } : {}
502
486
  }
503
487
  );
504
488
  return board ? okBoard(board, "Check updated.") : fail("Check not found.");
505
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
+ }
506
502
  case "add_note": {
507
503
  if (!input.boardId || !input.taskId || !input.note)
508
504
  return fail("add_note requires boardId, taskId, and note.");
@@ -577,14 +573,25 @@ function taskInput(input) {
577
573
  ...input.order !== void 0 ? { order: input.order } : {},
578
574
  ...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
579
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 } : {},
580
581
  ...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
581
582
  ...input.checkDescription !== void 0 ? {
582
583
  successCriteria: [
583
584
  {
584
585
  id: randomUUID(),
585
586
  description: input.checkDescription,
586
- type: "manual",
587
- 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 } : {}
588
595
  }
589
596
  ]
590
597
  } : {},
@@ -632,11 +639,11 @@ function taskInput(input) {
632
639
  };
633
640
  }
634
641
  function mergedDependsOn(input) {
635
- const ids = [
642
+ if (input.dependsOn === void 0 && input.dependencyTaskId === void 0) return void 0;
643
+ return [
636
644
  ...input.dependsOn ?? [],
637
645
  ...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
638
646
  ].filter((id, i, arr) => id && arr.indexOf(id) === i);
639
- return ids.length > 0 ? ids : void 0;
640
647
  }
641
648
  function taskPatch(input) {
642
649
  return {
@@ -650,7 +657,15 @@ function taskPatch(input) {
650
657
  status: input.status,
651
658
  labels: input.labels,
652
659
  assignedAgent: input.agentId,
653
- ...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 } : {},
654
669
  ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
655
670
  ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
656
671
  };
@@ -710,8 +725,8 @@ function assignmentForTaskCreate(input) {
710
725
  }
711
726
 
712
727
  // src/kanban-tool-schema.ts
713
- 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. Contract Map actions are optional advisory metadata unless an operator explicitly enabled strict enforcement. Use verify_completion to validate executable success criteria before Done.";
714
- var KANBAN_TOOL_USAGE_HINT = "Use this for durable project kanban state. Read workbench when orienting across boards; it returns bounded Now, Next, Blocked, Review lanes and operational alerts. Before coding mutation, create a fully detailed card on a managed board with executable acceptance criteria, then call start_task. The runtime blocks product mutations until start_task binds a ready Running card. Do not create, inspect, repair, or enable a Contract Map during ordinary work; every map mode stays off the execution and completion path. Worker completion enters Review; Done requires passed acceptance criteria and review evidence. Surface existing strict-map findings for operator review without stopping work to repair them.";
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.';
715
730
  var KANBAN_INPUT_SCHEMA = {
716
731
  type: "object",
717
732
  properties: {
@@ -724,6 +739,7 @@ var KANBAN_INPUT_SCHEMA = {
724
739
  "duplicate_board",
725
740
  "update_board",
726
741
  "adopt_managed_lifecycle",
742
+ "release_managed_lifecycle",
727
743
  "delete_board",
728
744
  "generate_board",
729
745
  "export_markdown",
@@ -735,9 +751,6 @@ var KANBAN_INPUT_SCHEMA = {
735
751
  "ready_tasks",
736
752
  "snapshot",
737
753
  "workbench",
738
- "add_column",
739
- "update_column",
740
- "delete_column",
741
754
  "add_task",
742
755
  "split_task",
743
756
  "merge_tasks",
@@ -752,13 +765,6 @@ var KANBAN_INPUT_SCHEMA = {
752
765
  "delete_task",
753
766
  "set_chain",
754
767
  "get_chain",
755
- "get_contract_graph",
756
- "configure_contract_graph",
757
- "upsert_contract_node",
758
- "link_contract_nodes",
759
- "remove_contract_node",
760
- "remove_contract_edge",
761
- "evaluate_contract_graph",
762
768
  "claim_task",
763
769
  "release_task",
764
770
  "assign_task",
@@ -772,49 +778,27 @@ var KANBAN_INPUT_SCHEMA = {
772
778
  "update_goal_metric",
773
779
  "add_check",
774
780
  "update_check",
781
+ "remove_check",
775
782
  "add_note",
776
783
  "add_link",
777
784
  "verify_completion",
778
785
  "split_atomic",
779
786
  "assess_atomicity",
780
- "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"
781
794
  ]
782
795
  },
783
796
  boardId: { type: "string" },
784
797
  taskId: { type: "string" },
785
798
  taskIds: { type: "array", items: { type: "string" } },
786
799
  chainId: { type: "string" },
787
- contractNodeId: { type: "string" },
788
- contractNodeKind: {
789
- type: "string",
790
- enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"]
791
- },
792
- contractNodeState: {
793
- type: "string",
794
- enum: ["unknown", "active", "satisfied", "violated", "resolved"]
795
- },
796
- contractEnforcement: {
797
- type: "string",
798
- enum: ["blocking", "advisory", "informational"]
799
- },
800
- contractGraphEnforcement: { type: "string", enum: ["off", "advisory", "strict"] },
801
- contractEdgeId: { type: "string" },
802
- contractEdgeType: {
803
- type: "string",
804
- enum: [
805
- "targets",
806
- "affects",
807
- "must_preserve",
808
- "exposes",
809
- "verified_by",
810
- "conflicts_with",
811
- "derived_from",
812
- "relates_to"
813
- ]
814
- },
815
800
  fromNodeId: { type: "string" },
816
801
  toNodeId: { type: "string" },
817
- contractRationale: { type: "string" },
818
802
  baseline: { oneOf: [{ type: "string" }, { type: "number" }] },
819
803
  threshold: { oneOf: [{ type: "string" }, { type: "number" }] },
820
804
  columnId: { type: "string" },
@@ -898,7 +882,20 @@ var KANBAN_INPUT_SCHEMA = {
898
882
  costCeilingUsd: { type: "number" },
899
883
  retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
900
884
  lastFailureKind: { type: "string" },
901
- 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
+ },
902
899
  estimatedHours: { type: "number" },
903
900
  actualHours: { type: "number" },
904
901
  taskGraph: { type: "object" },
@@ -932,6 +929,74 @@ var KANBAN_INPUT_SCHEMA = {
932
929
  checkId: { type: "string" },
933
930
  checkDescription: { type: "string" },
934
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" },
935
1000
  note: { type: "string" },
936
1001
  author: { type: "string" },
937
1002
  url: { type: "string" },
@@ -984,17 +1049,25 @@ import {
984
1049
  mutateTasks
985
1050
  } from "@wrongstack/core/storage";
986
1051
  import { deserializeTaskGraph } from "@wrongstack/core/tasking";
987
- import { resolveWstackPaths } from "@wrongstack/core/utils";
1052
+ import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
988
1053
  import {
989
1054
  bridgeKanbanSupervisor,
1055
+ compactSessionMirrorBoard,
990
1056
  createBoard,
1057
+ DEFAULT_COLUMNS,
991
1058
  getBoard as getBoard2,
1059
+ getDependencyReadinessIssues,
1060
+ getKanbanOrchestrationSnapshot,
992
1061
  listBoards,
1062
+ pruneSessionBoards,
993
1063
  removeBoard,
994
1064
  syncBoardFromTaskGraph,
995
1065
  touchKanbanPresence as touchKanbanPresence2,
996
1066
  updateBoard
997
1067
  } from "@wrongstack/kanban";
1068
+ var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
1069
+ ...column
1070
+ }));
998
1071
  function taskFileToSerializedGraph(tasks, sessionId) {
999
1072
  const graphId = `session:${sessionId}`;
1000
1073
  const ids = new Set(tasks.map((task) => task.id));
@@ -1066,8 +1139,14 @@ var kanbanTool = {
1066
1139
  }
1067
1140
  case "create_board": {
1068
1141
  if (!input.title) return fail("create_board requires title.");
1142
+ const existing = (await listBoards2(projectRoot)).filter(
1143
+ (candidate) => (candidate.kind ?? "project") === "project"
1144
+ );
1069
1145
  const board = await createBoard2(projectRoot, boardCreateInput(input, input.title));
1070
- 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 };
1071
1150
  }
1072
1151
  case "update_board": {
1073
1152
  if (!input.boardId) return fail("update_board requires boardId.");
@@ -1096,6 +1175,20 @@ var kanbanTool = {
1096
1175
  });
1097
1176
  return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
1098
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
+ }
1099
1192
  case "duplicate_board": {
1100
1193
  if (!input.boardId) return fail("duplicate_board requires boardId.");
1101
1194
  const board = await duplicateBoard(
@@ -1115,8 +1208,7 @@ var kanbanTool = {
1115
1208
  const boardInput = createBoardFromText({
1116
1209
  description: input.description,
1117
1210
  ...input.title !== void 0 ? { title: input.title } : {},
1118
- ...input.context !== void 0 ? { context: input.context } : {},
1119
- ...input.columns !== void 0 ? { columns: input.columns } : {}
1211
+ ...input.context !== void 0 ? { context: input.context } : {}
1120
1212
  });
1121
1213
  const board = await createBoard2(projectRoot, boardInput);
1122
1214
  for (const taskInput2 of parseLinesIntoTasks(
@@ -1254,7 +1346,7 @@ var kanbanTool = {
1254
1346
  return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
1255
1347
  }
1256
1348
  case "snapshot": {
1257
- const snapshot = await getKanbanOrchestrationSnapshot(projectRoot, {
1349
+ const snapshot = await getKanbanOrchestrationSnapshot2(projectRoot, {
1258
1350
  query: input.query,
1259
1351
  boardId: input.boardId,
1260
1352
  assignedAgent: input.agentId,
@@ -1269,33 +1361,6 @@ var kanbanTool = {
1269
1361
  snapshot
1270
1362
  };
1271
1363
  }
1272
- case "add_column": {
1273
- if (!input.boardId || !input.title)
1274
- return fail("add_column requires boardId and title.");
1275
- const result2 = await addColumn(projectRoot, input.boardId, {
1276
- title: input.title,
1277
- ...input.description !== void 0 ? { description: input.description } : {}
1278
- });
1279
- return result2 ? okBoard(result2.board, "Column added.") : fail("Board not found.");
1280
- }
1281
- case "update_column": {
1282
- if (!input.boardId || !input.columnId)
1283
- return fail("update_column requires boardId and columnId.");
1284
- const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
1285
- ...input.title !== void 0 ? { title: input.title } : {},
1286
- ...input.description !== void 0 ? { description: input.description } : {},
1287
- ...input.order !== void 0 ? { order: input.order } : {}
1288
- });
1289
- return board ? okBoard(board, "Column updated.") : fail("Column not found.");
1290
- }
1291
- case "delete_column": {
1292
- if (!input.boardId || !input.columnId)
1293
- return fail("delete_column requires boardId and columnId.");
1294
- const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
1295
- moveTasksToColumnId: input.moveTasksToColumnId
1296
- });
1297
- return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
1298
- }
1299
1364
  case "add_task": {
1300
1365
  if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
1301
1366
  const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
@@ -1377,6 +1442,32 @@ var kanbanTool = {
1377
1442
  `Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
1378
1443
  );
1379
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
+ }
1380
1471
  let stage = task.lifecycle?.currentStage;
1381
1472
  if (stage === "backlog") {
1382
1473
  const moved = await transitionTask(projectRoot, board.id, task.id, {
@@ -1517,6 +1608,9 @@ var kanbanTool = {
1517
1608
  if (!input.boardId || !input.taskId)
1518
1609
  return fail("delete_task requires boardId and taskId.");
1519
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
+ }
1520
1614
  return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
1521
1615
  }
1522
1616
  case "set_chain": {
@@ -1649,7 +1743,7 @@ var kanbanTool = {
1649
1743
  });
1650
1744
  } catch (err) {
1651
1745
  lifecycleWarnings.push(
1652
- `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))}`
1653
1747
  );
1654
1748
  }
1655
1749
  }
@@ -1673,7 +1767,7 @@ var kanbanTool = {
1673
1767
  });
1674
1768
  } catch (err) {
1675
1769
  lifecycleWarnings.push(
1676
- `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))}`
1677
1771
  );
1678
1772
  }
1679
1773
  if (transitionResult) {
@@ -1693,7 +1787,11 @@ var kanbanTool = {
1693
1787
  successCriteria: verResult.task.successCriteria
1694
1788
  });
1695
1789
  const verdict = verResult.report.verdict;
1696
- 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") {
1697
1795
  try {
1698
1796
  const doneResult = await transitionTask(
1699
1797
  projectRoot,
@@ -1811,11 +1909,34 @@ var kanbanTool = {
1811
1909
  });
1812
1910
  return {
1813
1911
  ok: true,
1814
- 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}.`,
1815
1913
  queueHealth: health
1816
1914
  };
1817
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
1818
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
+ }
1819
1940
  {
1820
1941
  const detailResult = await handleKanbanDetailAction(projectRoot, input);
1821
1942
  if (detailResult !== void 0) return detailResult;
@@ -1825,7 +1946,7 @@ var kanbanTool = {
1825
1946
  })();
1826
1947
  return withPresence(result);
1827
1948
  } catch (err) {
1828
- return fail(err instanceof Error ? err.message : String(err));
1949
+ return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
1829
1950
  }
1830
1951
  }
1831
1952
  };