@wrongstack/tools 1.0.8 → 1.0.10

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 (136) hide show
  1. package/dist/_win32-resolve.d.ts +5 -0
  2. package/dist/audit.js +5 -5
  3. package/dist/bash-kill-guard.d.ts +6 -4
  4. package/dist/bash.js +2 -2
  5. package/dist/browser/index.js +1 -1
  6. package/dist/builtin.js +41 -41
  7. package/dist/{chunk-N3OTTQNC.js → chunk-2OCAAFAT.js} +44 -25
  8. package/dist/{chunk-5MP76C72.js → chunk-2UVAIS5O.js} +24 -29
  9. package/dist/{chunk-SIIOHBNY.js → chunk-3RRK33YM.js} +2 -2
  10. package/dist/{chunk-M2W36JT7.js → chunk-4CE3RKEP.js} +6 -3
  11. package/dist/{chunk-QJARSK2G.js → chunk-4SUGLJLO.js} +11 -3
  12. package/dist/{chunk-AH7J5HWL.js → chunk-5RBUYFRV.js} +15 -7
  13. package/dist/{chunk-GNLHBDFO.js → chunk-6I6RXQFG.js} +127 -196
  14. package/dist/{chunk-WKLUOXR4.js → chunk-7GOE2ZTZ.js} +2 -2
  15. package/dist/{chunk-O6AGFJBO.js → chunk-7V33PHT6.js} +49 -77
  16. package/dist/{chunk-CAF4L6KN.js → chunk-C3WP6ZOT.js} +36 -48
  17. package/dist/{chunk-KMXPP32M.js → chunk-C6ZOXNXV.js} +13 -3
  18. package/dist/{chunk-EGT5QLCB.js → chunk-D6RQZMZS.js} +123 -255
  19. package/dist/{chunk-S6CGSFLX.js → chunk-DBG7UQGK.js} +10 -3
  20. package/dist/{chunk-FI6SOIVE.js → chunk-DLZCOEVG.js} +21 -3
  21. package/dist/{chunk-NL45OWEO.js → chunk-DOAHONOS.js} +7 -12
  22. package/dist/{chunk-3LQJXJDM.js → chunk-E7A4SLKR.js} +2 -2
  23. package/dist/{chunk-JB5JLY7V.js → chunk-ENE22I5W.js} +387 -394
  24. package/dist/{chunk-5KRKL3WD.js → chunk-FCSUBTUJ.js} +112 -119
  25. package/dist/{chunk-YUK3WLHJ.js → chunk-FVSMTEO3.js} +2 -2
  26. package/dist/{chunk-SEONDNLN.js → chunk-G7CYW5EP.js} +2 -2
  27. package/dist/{chunk-TU6UVATU.js → chunk-HYHE4VNA.js} +3 -3
  28. package/dist/{chunk-HIRVCXLP.js → chunk-IE4KDAOB.js} +3 -3
  29. package/dist/{chunk-YA2ZYCJY.js → chunk-JPK2HHBK.js} +1 -1
  30. package/dist/{chunk-7E55DAFW.js → chunk-KHUCKLOR.js} +12 -7
  31. package/dist/{chunk-R3HIZVFS.js → chunk-KPDCKSN5.js} +23 -3
  32. package/dist/{chunk-B3MGJBFI.js → chunk-KS2OEZWV.js} +23 -47
  33. package/dist/{chunk-UI3ZJIG3.js → chunk-LKLIQUCW.js} +14 -8
  34. package/dist/{chunk-62OAECEQ.js → chunk-LLSFOUZF.js} +8 -8
  35. package/dist/{chunk-YZQSUL5G.js → chunk-M4ZHHCCW.js} +14 -6
  36. package/dist/{chunk-IXIS7RSG.js → chunk-MRXWOOOR.js} +29 -47
  37. package/dist/{chunk-C4DSQBTT.js → chunk-NEYUPZL6.js} +3 -3
  38. package/dist/{chunk-6XYFYLN7.js → chunk-O2MHULBO.js} +13 -8
  39. package/dist/{chunk-B62LKMWQ.js → chunk-OKZJJDOG.js} +86 -188
  40. package/dist/{chunk-HOFVYIPI.js → chunk-ON6ET5ER.js} +16 -1
  41. package/dist/{chunk-KF7GMVNU.js → chunk-OUERRFEM.js} +32 -11
  42. package/dist/{chunk-GF4JHLCO.js → chunk-PWZUHZP7.js} +2 -2
  43. package/dist/{chunk-OGQMPX3J.js → chunk-QAWI3RSK.js} +37 -81
  44. package/dist/{chunk-ZMCV3XOV.js → chunk-SPNXDTCJ.js} +29 -63
  45. package/dist/{chunk-H2VB7GCR.js → chunk-ULI2J6TX.js} +27 -14
  46. package/dist/{chunk-EMA2XENR.js → chunk-UVYRQKPZ.js} +4 -3
  47. package/dist/{chunk-MF6ZUBI7.js → chunk-V25Q6TQ7.js} +3 -3
  48. package/dist/{chunk-2NTHMZV3.js → chunk-VMJY6FYJ.js} +693 -276
  49. package/dist/{chunk-ZOUG3KKS.js → chunk-XPVYZ2S6.js} +2 -2
  50. package/dist/{chunk-5U5ULQX2.js → chunk-XZLVJD56.js} +5 -1
  51. package/dist/{chunk-HCPSVK7J.js → chunk-YKSOSI2N.js} +47 -31
  52. package/dist/codebase-index/codebase-ast-replace-tool.d.ts +5 -6
  53. package/dist/codebase-index/codebase-context-tool.d.ts +4 -6
  54. package/dist/codebase-index/codebase-impact-analysis-tool.d.ts +1 -2
  55. package/dist/codebase-index/codebase-incoming-calls-tool.d.ts +0 -2
  56. package/dist/codebase-index/codebase-index-tool.d.ts +0 -2
  57. package/dist/codebase-index/codebase-invariant-check-tool.d.ts +1 -2
  58. package/dist/codebase-index/codebase-outgoing-calls-tool.d.ts +0 -2
  59. package/dist/codebase-index/codebase-repo-map-tool.d.ts +1 -2
  60. package/dist/codebase-index/codebase-search-tool.d.ts +1 -1
  61. package/dist/codebase-index/codebase-targeted-test-tool.d.ts +1 -2
  62. package/dist/codebase-index/index.js +8 -8
  63. package/dist/codebase-index/parser-worker-script.js +3 -3
  64. package/dist/codebase-index/project-server.js +5 -5
  65. package/dist/codebase-index/worker.js +4 -4
  66. package/dist/design.js +1 -1
  67. package/dist/diff.js +1 -1
  68. package/dist/edit.js +7 -7
  69. package/dist/exec.d.ts +7 -7
  70. package/dist/exec.js +2 -2
  71. package/dist/format.js +5 -5
  72. package/dist/git.js +1 -1
  73. package/dist/glob.js +1 -1
  74. package/dist/{go-parser-VDPRQEGM.js → go-parser-Z4H6SR3R.js} +6 -3
  75. package/dist/grep.js +1 -1
  76. package/dist/index.js +45 -45
  77. package/dist/install.js +5 -5
  78. package/dist/json.d.ts +0 -1
  79. package/dist/json.js +1 -1
  80. package/dist/kanban-presence.d.ts +6 -0
  81. package/dist/kanban-serializer.d.ts +9 -1
  82. package/dist/kanban-split-task-handler.d.ts +1 -1
  83. package/dist/kanban-tool-results.d.ts +57 -2
  84. package/dist/kanban-tool-types.d.ts +16 -5
  85. package/dist/kanban.d.ts +14 -0
  86. package/dist/kanban.js +9 -1
  87. package/dist/languages/index.js +3 -3
  88. package/dist/{legacy-bridge-4T4OTSQP.js → legacy-bridge-BPT2S2F4.js} +5 -5
  89. package/dist/lint.js +5 -5
  90. package/dist/logs.js +1 -1
  91. package/dist/mode.d.ts +3 -2
  92. package/dist/mode.js +1 -1
  93. package/dist/outdated.js +2 -2
  94. package/dist/pack.js +42 -42
  95. package/dist/{parser-dispatch-4E2SDQUO.js → parser-dispatch-WG4DNBHH.js} +4 -4
  96. package/dist/patch.js +7 -7
  97. package/dist/plan.d.ts +2 -1
  98. package/dist/plan.js +3 -3
  99. package/dist/{py-parser-MAW6DHFE.js → py-parser-4PFZYAJ7.js} +3 -3
  100. package/dist/read.js +7 -7
  101. package/dist/replace.js +7 -7
  102. package/dist/search.d.ts +0 -6
  103. package/dist/search.js +1 -1
  104. package/dist/security-ast-scan-tool.d.ts +1 -2
  105. package/dist/session-shell.js +2 -2
  106. package/dist/task.d.ts +2 -1
  107. package/dist/task.js +3 -3
  108. package/dist/test.d.ts +8 -1
  109. package/dist/test.js +5 -5
  110. package/dist/todo.js +2 -2
  111. package/dist/tool-search.d.ts +2 -0
  112. package/dist/tool-search.js +1 -1
  113. package/dist/tool-tier.js +43 -43
  114. package/dist/tool-use.d.ts +2 -2
  115. package/dist/tool-use.js +1 -1
  116. package/dist/tree.js +1 -1
  117. package/dist/typecheck.js +5 -5
  118. package/dist/wasm/bash/tree-sitter-bash.wasm +0 -0
  119. package/dist/wasm/c/tree-sitter-c.wasm +0 -0
  120. package/dist/wasm/c_sharp/tree-sitter-c_sharp.wasm +0 -0
  121. package/dist/wasm/checksums.json +16 -0
  122. package/dist/wasm/cpp/tree-sitter-cpp.wasm +0 -0
  123. package/dist/wasm/elixir/tree-sitter-elixir.wasm +0 -0
  124. package/dist/wasm/go/tree-sitter-go.wasm +0 -0
  125. package/dist/wasm/java/tree-sitter-java.wasm +0 -0
  126. package/dist/wasm/kotlin/tree-sitter-kotlin.wasm +0 -0
  127. package/dist/wasm/php/tree-sitter-php.wasm +0 -0
  128. package/dist/wasm/python/tree-sitter-python.wasm +0 -0
  129. package/dist/wasm/ruby/tree-sitter-ruby.wasm +0 -0
  130. package/dist/wasm/rust/tree-sitter-rust.wasm +0 -0
  131. package/dist/wasm/swift/tree-sitter-swift.wasm +0 -0
  132. package/dist/wasm/tree-sitter-runtime.wasm +0 -0
  133. package/dist/win32.d.ts +1 -1
  134. package/dist/win32.js +3 -1
  135. package/dist/write.js +7 -7
  136. package/package.json +10 -10
@@ -3,9 +3,6 @@ import {
3
3
  taskFileToSerializedGraph
4
4
  } from "./chunk-YZECCW3F.js";
5
5
 
6
- // src/kanban.ts
7
- import { stripLifecycleIssues as stripLifecycleIssues2 } from "@wrongstack/kanban";
8
-
9
6
  // src/kanban-board-actions.ts
10
7
  import { loadTasks } from "@wrongstack/core/storage";
11
8
  import { deserializeTaskGraph, serializeTaskGraph } from "@wrongstack/core/tasking";
@@ -31,9 +28,163 @@ import {
31
28
  updateBoard
32
29
  } from "@wrongstack/kanban";
33
30
 
31
+ // src/kanban-tool-results.ts
32
+ import { ToolValidationError } from "@wrongstack/core/types";
33
+ import {
34
+ decodeLifecycleIssues,
35
+ stripLifecycleIssues
36
+ } from "@wrongstack/kanban";
37
+ function atomicityNudge(task) {
38
+ if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
39
+ const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
40
+ return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
41
+ }
42
+ function readEnvGateEnforcement() {
43
+ const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
44
+ return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
45
+ }
46
+ var CLASSIFIER_HINT = {
47
+ NOT_FOUND: "ENOENT",
48
+ CONFLICT: "EBUSY",
49
+ UNAVAILABLE: "ECONNREFUSED"
50
+ };
51
+ function formatKanbanErrorMessage(code, message, opts) {
52
+ let text = `[${code}] ${message}`;
53
+ if (opts.committed) text += ` Already committed: ${opts.committed}`;
54
+ const extra = (opts.issues ?? []).filter((issue) => issue.message.trim() !== message.trim());
55
+ if (extra.length > 0) {
56
+ text += ` Issues: ${extra.map((issue) => issue.field ? `${issue.field}: ${issue.message}` : issue.message).join(" | ")}`;
57
+ }
58
+ return text;
59
+ }
60
+ var KanbanToolError = class extends Error {
61
+ kanbanCode;
62
+ /** Classifier hint (see CLASSIFIER_HINT); not the Kanban code. */
63
+ code;
64
+ retryable;
65
+ issues;
66
+ committed;
67
+ /** The bare message, without the code prefix, committed note, or issues tail. */
68
+ detail;
69
+ constructor(code, message, opts = {}) {
70
+ super(
71
+ formatKanbanErrorMessage(code, message, opts),
72
+ opts.cause !== void 0 ? { cause: opts.cause } : void 0
73
+ );
74
+ this.detail = message;
75
+ this.name = code === "ABORTED" ? "AbortError" : "KanbanToolError";
76
+ this.kanbanCode = code;
77
+ this.retryable = opts.retryable ?? (code === "CONFLICT" || code === "UNAVAILABLE");
78
+ const hint = CLASSIFIER_HINT[code];
79
+ this.code = hint === "ENOENT" || this.retryable ? hint : void 0;
80
+ this.issues = opts.issues;
81
+ this.committed = opts.committed;
82
+ }
83
+ };
84
+ var KanbanInputError = class extends ToolValidationError {
85
+ kanbanCode = "INVALID_INPUT";
86
+ retryable = false;
87
+ issues = void 0;
88
+ constructor(message, field) {
89
+ super({ message: `[INVALID_INPUT] ${message}`, ...field !== void 0 ? { field } : {} });
90
+ }
91
+ };
92
+ function invalidInput(message, field) {
93
+ return new KanbanInputError(message, field);
94
+ }
95
+ function notFound(message, opts = {}) {
96
+ return new KanbanToolError("NOT_FOUND", message, opts);
97
+ }
98
+ function refused(message, opts = {}) {
99
+ return new KanbanToolError("REFUSED", message, opts);
100
+ }
101
+ function conflict(message, opts = {}) {
102
+ return new KanbanToolError("CONFLICT", message, opts);
103
+ }
104
+ function isKanbanToolFailure(err) {
105
+ return err instanceof Error && typeof err.kanbanCode === "string" && typeof err.retryable === "boolean";
106
+ }
107
+ function errorCode(err) {
108
+ const code = err.code;
109
+ return typeof code === "string" ? code : void 0;
110
+ }
111
+ function lifecycleIssuesOf(err) {
112
+ const own = err.issues;
113
+ if (Array.isArray(own)) return own;
114
+ return decodeLifecycleIssues(err);
115
+ }
116
+ var UNAVAILABLE_MESSAGE_PATTERNS = [
117
+ /^Kanban project server is disabled/,
118
+ /^Failed to connect to kanban project server/,
119
+ /^Kanban server .+: /,
120
+ /^Connection closed$/,
121
+ /^connect-timeout$/,
122
+ /^Request \S+ timed out$/
123
+ ];
124
+ var UNAVAILABLE_ERRNO = /* @__PURE__ */ new Set(["ECONNREFUSED", "ECONNRESET", "EPIPE", "ETIMEDOUT"]);
125
+ function toKanbanToolError(err, committed) {
126
+ if (!(err instanceof Error)) return err;
127
+ if (isKanbanToolFailure(err)) {
128
+ if (!committed) return err;
129
+ return err instanceof KanbanToolError ? new KanbanToolError(err.kanbanCode, err.detail, {
130
+ issues: err.issues,
131
+ retryable: err.retryable,
132
+ committed: err.committed ? `${err.committed} ${committed}` : committed,
133
+ cause: err
134
+ }) : err;
135
+ }
136
+ const code = errorCode(err);
137
+ if (err.name === "KanbanLifecycleError" || code === "LIFECYCLE") {
138
+ return refused(stripLifecycleIssues(err.message), {
139
+ issues: lifecycleIssuesOf(err),
140
+ committed,
141
+ cause: err
142
+ });
143
+ }
144
+ if (err.name === "StaleWriteError" || code === "STALE_WRITE") {
145
+ return conflict(err.message, { retryable: true, committed, cause: err });
146
+ }
147
+ if (code !== void 0 && UNAVAILABLE_ERRNO.has(code) || UNAVAILABLE_MESSAGE_PATTERNS.some((pattern) => pattern.test(err.message))) {
148
+ return new KanbanToolError("UNAVAILABLE", err.message, {
149
+ retryable: true,
150
+ committed,
151
+ cause: err
152
+ });
153
+ }
154
+ if (/^Ambiguous kanban (task|board) id /.test(err.message)) {
155
+ return invalidInput(err.message, "taskId");
156
+ }
157
+ return err;
158
+ }
159
+ function okBoard(board, message = "Board loaded.") {
160
+ return { ok: true, message, board };
161
+ }
162
+ function okTask(board, task, message) {
163
+ return { ok: true, message, board, task };
164
+ }
165
+ function resolveTaskRef(board, taskRef) {
166
+ if (!taskRef?.trim()) return void 0;
167
+ const exact = board.tasks.find((task) => task.id === taskRef);
168
+ if (exact) return exact;
169
+ const matches = board.tasks.filter((task) => task.id.startsWith(taskRef));
170
+ if (matches.length > 1) {
171
+ throw invalidInput(
172
+ `Ambiguous kanban task id "${taskRef}": ${matches.slice(0, 5).map((task) => task.id).join(", ")}`,
173
+ "taskId"
174
+ );
175
+ }
176
+ return matches[0];
177
+ }
178
+
34
179
  // src/kanban-board-inputs.ts
35
180
  function agentSettableGate(enforcement) {
36
- if (enforcement === void 0 || enforcement === "off") return {};
181
+ if (enforcement === void 0) return {};
182
+ if (enforcement !== "strict" && enforcement !== "soft") {
183
+ throw invalidInput(
184
+ `gateEnforcement "${String(enforcement)}" is not settable from the kanban tool; only "strict" or "soft" (switching the completion gate off is a human decision made through board config).`,
185
+ "gateEnforcement"
186
+ );
187
+ }
37
188
  return { completionGate: { enforcement } };
38
189
  }
39
190
  function boardCreateInput(input, title) {
@@ -77,34 +228,12 @@ function duplicateBoardOptions(input) {
77
228
 
78
229
  // src/kanban-split-task-handler.ts
79
230
  import { getBoard, splitTask } from "@wrongstack/kanban";
80
-
81
- // src/kanban-tool-results.ts
82
- function atomicityNudge(task) {
83
- if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
84
- const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
85
- return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
86
- }
87
- function readEnvGateEnforcement() {
88
- const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
89
- return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
90
- }
91
- function fail(message) {
92
- return { ok: false, message };
93
- }
94
- function okBoard(board, message = "Board loaded.") {
95
- return { ok: true, message, board };
96
- }
97
- function okTask(board, task, message) {
98
- return { ok: true, message, board, task };
99
- }
100
-
101
- // src/kanban-split-task-handler.ts
102
231
  async function handleSplitTask(projectRoot, input, extraSplitOptions, eventContext) {
103
232
  const boardId = input.boardId;
104
233
  const taskId = input.taskId;
105
234
  const childTitles = input.childTitles;
106
235
  if (!boardId || !taskId || !childTitles?.length) {
107
- return fail("split requires boardId, taskId, and at least one childTitles.");
236
+ throw invalidInput("split requires boardId, taskId, and at least one childTitles.");
108
237
  }
109
238
  const {
110
239
  targetColumnId,
@@ -134,12 +263,12 @@ async function handleSplitTask(projectRoot, input, extraSplitOptions, eventConte
134
263
  },
135
264
  eventContext
136
265
  );
137
- if (!result) return fail("Task not found.");
138
- const freshParent = result.board.tasks?.find((t) => t.id === taskId);
266
+ if (!result) throw notFound("Task not found.");
267
+ const freshParent = result.board.tasks?.find((t) => t.id === taskId) ?? result.board.tasks?.find((t) => t.id === result.parent?.id);
139
268
  if (!freshParent) {
140
- return fail(
141
- `Split succeeded but parent ${taskId} not found in returned board. Children: [${result.children.map((c) => c.id).join(", ")}].`
142
- );
269
+ throw conflict(`Parent ${taskId} not found in the board returned by the split.`, {
270
+ committed: `the split was written \u2014 child task(s) [${result.children.map((c) => c.id).join(", ")}] were created. Re-read the board with get_board.`
271
+ });
143
272
  }
144
273
  return {
145
274
  ok: true,
@@ -165,11 +294,13 @@ async function handleKanbanBoardAction(projectRoot, input, ctx) {
165
294
  return { ok: true, message: `${boards.length} board(s).`, boards };
166
295
  }
167
296
  case "get_board": {
297
+ if (!input.boardId) throw invalidInput("get_board requires boardId.", "boardId");
168
298
  const board = await requireBoard(projectRoot, input.boardId);
169
- return board ? okBoard(board) : fail("Board not found.");
299
+ if (!board) throw notFound("Board not found.");
300
+ return okBoard(board);
170
301
  }
171
302
  case "create_board": {
172
- if (!input.title) return fail("create_board requires title.");
303
+ if (!input.title) throw invalidInput("create_board requires title.", "title");
173
304
  const existing = (await listBoards(projectRoot)).filter(
174
305
  (candidate) => (candidate.kind ?? "project") === "project"
175
306
  );
@@ -180,52 +311,65 @@ async function handleKanbanBoardAction(projectRoot, input, ctx) {
180
311
  return { ok: true, message: `Board created: ${board.title}.${note}`, board };
181
312
  }
182
313
  case "update_board": {
183
- if (!input.boardId) return fail("update_board requires boardId.");
314
+ if (!input.boardId) throw invalidInput("update_board requires boardId.", "boardId");
184
315
  const board = await updateBoard(projectRoot, input.boardId, boardUpdatePatch(input));
185
- return board ? okBoard(board, "Board updated.") : fail("Board not found.");
316
+ if (!board) throw notFound("Board not found.");
317
+ return okBoard(board, "Board updated.");
186
318
  }
187
319
  case "adopt_managed_lifecycle": {
188
320
  if (!input.boardId || !input.author || !input.transitionComment) {
189
- return fail(
321
+ throw invalidInput(
190
322
  "adopt_managed_lifecycle requires boardId, author, transitionComment, and five ordered columns."
191
323
  );
192
324
  }
193
325
  if (input.columns?.length !== 5) {
194
- return fail(
195
- "adopt_managed_lifecycle columns must be ordered as backlog, todo, running, review, done."
326
+ throw invalidInput(
327
+ "adopt_managed_lifecycle columns must be ordered as backlog, todo, running, review, done.",
328
+ "columns"
196
329
  );
197
330
  }
198
331
  const [backlog, todo, running, review, done] = input.columns;
199
332
  if (!backlog || !todo || !running || !review || !done) {
200
- return fail("adopt_managed_lifecycle columns must contain five nonblank ids.");
333
+ throw invalidInput(
334
+ "adopt_managed_lifecycle columns must contain five nonblank ids.",
335
+ "columns"
336
+ );
201
337
  }
202
338
  const board = await adoptManagedLifecycle(projectRoot, input.boardId, {
203
339
  columns: { backlog, todo, running, review, done },
204
340
  actor: input.author,
205
341
  comment: input.transitionComment
206
342
  });
207
- return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
343
+ if (!board) throw notFound("Board not found.");
344
+ return okBoard(board, "Managed lifecycle adopted without moving existing cards.");
208
345
  }
209
346
  case "release_managed_lifecycle": {
210
- if (!input.boardId) return fail("release_managed_lifecycle requires boardId.");
347
+ if (!input.boardId) {
348
+ throw invalidInput("release_managed_lifecycle requires boardId.", "boardId");
349
+ }
211
350
  const board = await updateBoard(projectRoot, input.boardId, { lifecycle: null });
212
- return board ? okBoard(
351
+ if (!board) throw notFound("Board not found.");
352
+ return okBoard(
213
353
  board,
214
354
  "Managed lifecycle released; the board now tracks work without strict gates."
215
- ) : fail("Board not found.");
355
+ );
216
356
  }
217
357
  case "duplicate_board": {
218
- if (!input.boardId) return fail("duplicate_board requires boardId.");
358
+ if (!input.boardId) throw invalidInput("duplicate_board requires boardId.", "boardId");
219
359
  const board = await duplicateBoard(projectRoot, input.boardId, duplicateBoardOptions(input));
220
- return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
360
+ if (!board) throw notFound("Board not found.");
361
+ return okBoard(board, "Board duplicated.");
221
362
  }
222
363
  case "delete_board": {
223
- if (!input.boardId) return fail("delete_board requires boardId.");
364
+ if (!input.boardId) throw invalidInput("delete_board requires boardId.", "boardId");
224
365
  const removed = await removeBoard(projectRoot, input.boardId);
225
- return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
366
+ if (!removed) throw notFound("Board not found.");
367
+ return { ok: true, message: "Board deleted." };
226
368
  }
227
369
  case "generate_board": {
228
- if (!input.description) return fail("generate_board requires description.");
370
+ if (!input.description) {
371
+ throw invalidInput("generate_board requires description.", "description");
372
+ }
229
373
  const boardInput = createBoardFromText({
230
374
  description: input.description,
231
375
  ...input.title !== void 0 ? { title: input.title } : {},
@@ -241,17 +385,17 @@ async function handleKanbanBoardAction(projectRoot, input, ctx) {
241
385
  return okBoard(await getBoard2(projectRoot, board.id) ?? board, "Board generated.");
242
386
  }
243
387
  case "export_markdown": {
388
+ if (!input.boardId) throw invalidInput("export_markdown requires boardId.", "boardId");
244
389
  const board = await requireBoard(projectRoot, input.boardId);
245
- if (!board) return fail("Board not found.");
390
+ if (!board) throw notFound("Board not found.");
246
391
  return {
247
392
  ok: true,
248
393
  message: "Board exported.",
249
- board,
250
394
  markdown: exportBoardAsMarkdown(board)
251
395
  };
252
396
  }
253
397
  case "export_task_graph": {
254
- if (!input.boardId) return fail("export_task_graph requires boardId.");
398
+ if (!input.boardId) throw invalidInput("export_task_graph requires boardId.", "boardId");
255
399
  const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
256
400
  ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
257
401
  ...input.specId !== void 0 ? { specId: input.specId } : {},
@@ -259,7 +403,7 @@ async function handleKanbanBoardAction(projectRoot, input, ctx) {
259
403
  ...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
260
404
  ...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
261
405
  });
262
- if (!exported) return fail("Board not found.");
406
+ if (!exported) throw notFound("Board not found.");
263
407
  return {
264
408
  ok: true,
265
409
  message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
@@ -269,7 +413,7 @@ async function handleKanbanBoardAction(projectRoot, input, ctx) {
269
413
  }
270
414
  case "sync_task_graph": {
271
415
  if (!input.boardId || !input.taskGraph) {
272
- return fail("sync_task_graph requires boardId and taskGraph.");
416
+ throw invalidInput("sync_task_graph requires boardId and taskGraph.");
273
417
  }
274
418
  const graph = deserializeTaskGraph(input.taskGraph);
275
419
  const result = await syncBoardFromTaskGraph(projectRoot, input.boardId, graph, {
@@ -283,14 +427,16 @@ async function handleKanbanBoardAction(projectRoot, input, ctx) {
283
427
  ...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
284
428
  ...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
285
429
  });
286
- return result ? {
430
+ if (!result) throw notFound("Board not found.");
431
+ return {
287
432
  ok: true,
288
433
  message: `Task graph synced: ${result.createdTaskIds.length} created, ${result.updatedTaskIds.length} updated, ${result.archivedTaskIds.length} archived.`,
289
434
  board: result.board
290
- } : fail("Board not found.");
435
+ };
291
436
  }
292
437
  case "create_from_graph": {
293
- if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
438
+ if (!input.taskGraph)
439
+ throw invalidInput("create_from_graph requires taskGraph.", "taskGraph");
294
440
  const graph = deserializeTaskGraph(input.taskGraph);
295
441
  const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
296
442
  ...input.title !== void 0 ? { title: input.title } : {},
@@ -309,9 +455,13 @@ async function handleKanbanBoardAction(projectRoot, input, ctx) {
309
455
  }
310
456
  case "import_session_tasks": {
311
457
  const taskPath = ctx.meta?.["task.path"];
312
- if (!taskPath) return fail("No session task file for this session.");
458
+ if (!taskPath) {
459
+ return { ok: true, imported: 0, message: "No session task file for this session." };
460
+ }
313
461
  const file = await loadTasks(taskPath);
314
- if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
462
+ if (!file || file.tasks.length === 0) {
463
+ return { ok: true, imported: 0, message: "No session tasks to import." };
464
+ }
315
465
  const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
316
466
  const graph = deserializeTaskGraph(taskFileToSerializedGraph(file.tasks, sessionId));
317
467
  const tags = ["session", `session:${sessionId}`];
@@ -325,11 +475,17 @@ async function handleKanbanBoardAction(projectRoot, input, ctx) {
325
475
  archiveMissingTasks: true,
326
476
  includeCompletedTasks: true
327
477
  });
328
- return result ? {
478
+ if (!result) {
479
+ throw conflict("Session board vanished mid-sync; nothing was imported. Retry.", {
480
+ retryable: true
481
+ });
482
+ }
483
+ return {
329
484
  ok: true,
485
+ imported: file.tasks.length,
330
486
  message: `Synced ${file.tasks.length} session tasks into board "${result.board.title}".`,
331
487
  board: result.board
332
- } : fail("Session board vanished mid-sync.");
488
+ };
333
489
  }
334
490
  const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
335
491
  title: `Session tasks (${sessionId.slice(0, 8)})`,
@@ -338,6 +494,7 @@ async function handleKanbanBoardAction(projectRoot, input, ctx) {
338
494
  });
339
495
  return {
340
496
  ok: true,
497
+ imported: file.tasks.length,
341
498
  message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
342
499
  board
343
500
  };
@@ -383,12 +540,14 @@ async function handleKanbanBoardAction(projectRoot, input, ctx) {
383
540
  };
384
541
  }
385
542
  case "events": {
386
- if (!input.boardId) return fail("events requires boardId.");
543
+ if (!input.boardId) throw invalidInput("events requires boardId.", "boardId");
387
544
  const eventList = await listKanbanEvents(projectRoot, input.boardId);
545
+ const limit = typeof input.limit === "number" && Number.isFinite(input.limit) && input.limit > 0 ? Math.floor(input.limit) : void 0;
546
+ const events = limit !== void 0 ? eventList.slice(-limit) : eventList;
388
547
  return {
389
548
  ok: true,
390
- message: `${eventList.length} event(s).`,
391
- events: eventList
549
+ message: events.length === eventList.length ? `${eventList.length} event(s).` : `${events.length} most recent of ${eventList.length} event(s).`,
550
+ events
392
551
  };
393
552
  }
394
553
  case "queue_health": {
@@ -420,11 +579,11 @@ async function handleKanbanContractAction(projectRoot, input, actor, sessionId)
420
579
  const eventContext = { sessionId, ...actor !== void 0 ? { actor } : {} };
421
580
  switch (input.action) {
422
581
  case "get_contract_graph": {
423
- if (!input.boardId) return fail("get_contract_graph requires boardId.");
582
+ if (!input.boardId) throw invalidInput("get_contract_graph requires boardId.");
424
583
  const found = await getContractGraph(projectRoot, input.boardId);
425
- if (!found) return fail("Board not found.");
584
+ if (!found) throw notFound("Board not found.");
426
585
  const evaluated = input.taskId ? await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId) : null;
427
- if (input.taskId && !evaluated) return fail("Task not found on this board.");
586
+ if (input.taskId && !evaluated) throw notFound("Task not found on this board.");
428
587
  return {
429
588
  ok: true,
430
589
  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.",
@@ -434,7 +593,7 @@ async function handleKanbanContractAction(projectRoot, input, actor, sessionId)
434
593
  };
435
594
  }
436
595
  case "configure_contract_graph": {
437
- if (!input.boardId) return fail("configure_contract_graph requires boardId.");
596
+ if (!input.boardId) throw invalidInput("configure_contract_graph requires boardId.");
438
597
  const enforcement = input.contractEnforcement ?? "advisory";
439
598
  const board = await configureContractGraph(
440
599
  projectRoot,
@@ -442,14 +601,15 @@ async function handleKanbanContractAction(projectRoot, input, actor, sessionId)
442
601
  enforcement,
443
602
  eventContext
444
603
  );
445
- return board ? okBoard(board, `Contract map enforcement set to ${enforcement}.`) : fail("Board not found.");
604
+ if (!board) throw notFound("Board not found.");
605
+ return okBoard(board, `Contract map enforcement set to ${enforcement}.`);
446
606
  }
447
607
  case "upsert_contract_node": {
448
608
  if (!input.boardId || !input.taskId) {
449
- return fail("upsert_contract_node requires boardId and taskId.");
609
+ throw invalidInput("upsert_contract_node requires boardId and taskId.");
450
610
  }
451
611
  if (!input.contractNodeKind || !input.contractNodeTitle) {
452
- return fail("upsert_contract_node requires contractNodeKind and contractNodeTitle.");
612
+ throw invalidInput("upsert_contract_node requires contractNodeKind and contractNodeTitle.");
453
613
  }
454
614
  const waiver = input.contractNodeState === "waived" ? {
455
615
  actor: actor ?? "agent",
@@ -457,7 +617,7 @@ async function handleKanbanContractAction(projectRoot, input, actor, sessionId)
457
617
  at: (/* @__PURE__ */ new Date()).toISOString()
458
618
  } : void 0;
459
619
  if (waiver && !waiver.reason.trim()) {
460
- return fail("A waived contract node requires contractWaiverReason.");
620
+ throw invalidInput("A waived contract node requires contractWaiverReason.");
461
621
  }
462
622
  const result = await upsertContractNode(
463
623
  projectRoot,
@@ -477,7 +637,7 @@ async function handleKanbanContractAction(projectRoot, input, actor, sessionId)
477
637
  },
478
638
  eventContext
479
639
  );
480
- if (!result) return fail("Board or task not found.");
640
+ if (!result) throw notFound("Board or task not found.");
481
641
  return {
482
642
  ok: true,
483
643
  message: `Contract node ${result.node.kind} "${result.node.title}" saved (${result.node.id}).`,
@@ -487,7 +647,7 @@ async function handleKanbanContractAction(projectRoot, input, actor, sessionId)
487
647
  }
488
648
  case "remove_contract_node": {
489
649
  if (!input.boardId || !input.contractNodeId) {
490
- return fail("remove_contract_node requires boardId and contractNodeId.");
650
+ throw invalidInput("remove_contract_node requires boardId and contractNodeId.");
491
651
  }
492
652
  const board = await removeContractNode(
493
653
  projectRoot,
@@ -495,13 +655,17 @@ async function handleKanbanContractAction(projectRoot, input, actor, sessionId)
495
655
  input.contractNodeId,
496
656
  eventContext
497
657
  );
498
- return board ? okBoard(board, "Contract node removed, along with every edge that touched it.") : fail("Contract node not found.");
658
+ if (!board) throw notFound("Contract node not found.");
659
+ return okBoard(board, "Contract node removed, along with every edge that touched it.");
499
660
  }
500
661
  case "add_contract_edge": {
501
662
  if (!input.boardId || !input.contractEdgeFrom || !input.contractEdgeTo) {
502
- return fail("add_contract_edge requires boardId, contractEdgeFrom, and contractEdgeTo.");
663
+ throw invalidInput(
664
+ "add_contract_edge requires boardId, contractEdgeFrom, and contractEdgeTo."
665
+ );
503
666
  }
504
- if (!input.contractEdgeType) return fail("add_contract_edge requires contractEdgeType.");
667
+ if (!input.contractEdgeType)
668
+ throw invalidInput("add_contract_edge requires contractEdgeType.");
505
669
  const result = await addContractEdge(
506
670
  projectRoot,
507
671
  input.boardId,
@@ -516,7 +680,7 @@ async function handleKanbanContractAction(projectRoot, input, actor, sessionId)
516
680
  },
517
681
  eventContext
518
682
  );
519
- if (!result) return fail("Board not found.");
683
+ if (!result) throw notFound("Board not found.");
520
684
  return {
521
685
  ok: true,
522
686
  message: `Contract edge ${result.edge.type}: ${result.edge.from} \u2192 ${result.edge.to}.`,
@@ -526,7 +690,7 @@ async function handleKanbanContractAction(projectRoot, input, actor, sessionId)
526
690
  }
527
691
  case "remove_contract_edge": {
528
692
  if (!input.boardId || !input.contractEdgeId) {
529
- return fail("remove_contract_edge requires boardId and contractEdgeId.");
693
+ throw invalidInput("remove_contract_edge requires boardId and contractEdgeId.");
530
694
  }
531
695
  const board = await removeContractEdge(
532
696
  projectRoot,
@@ -534,7 +698,8 @@ async function handleKanbanContractAction(projectRoot, input, actor, sessionId)
534
698
  input.contractEdgeId,
535
699
  eventContext
536
700
  );
537
- return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
701
+ if (!board) throw notFound("Contract edge not found.");
702
+ return okBoard(board, "Contract edge removed.");
538
703
  }
539
704
  default:
540
705
  return void 0;
@@ -581,13 +746,13 @@ async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
581
746
  switch (input.action) {
582
747
  case "assess_atomicity": {
583
748
  if (!input.boardId || !input.taskId) {
584
- return fail("assess_atomicity requires boardId and taskId.");
749
+ throw invalidInput("assess_atomicity requires boardId and taskId.");
585
750
  }
586
751
  const result = await assessTaskAtomicity(projectRoot, input.boardId, input.taskId, {
587
752
  assessedBy: "agent",
588
753
  eventContext
589
754
  });
590
- if (!result) return fail("Task not found.");
755
+ if (!result) throw notFound("Task not found.");
591
756
  const failing = result.assessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason);
592
757
  const guidance = result.assessment.verdict === "needs_decomposition" ? ` This task should be split before dispatch \u2014 call propose_decomposition with 2+ subtasks (each with one verifiable success criterion). Reasons: ${failing.join(" | ")}` : result.assessment.verdict === "composite" ? " Container task: work happens in its children; it is verified via subtask aggregation." : "";
593
758
  return okTask(
@@ -598,15 +763,16 @@ async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
598
763
  }
599
764
  case "propose_decomposition": {
600
765
  if (!input.boardId || !input.taskId || !input.subtasks?.length) {
601
- return fail("propose_decomposition requires boardId, taskId, and subtasks (2+).");
766
+ throw invalidInput("propose_decomposition requires boardId, taskId, and subtasks (2+).");
602
767
  }
603
768
  if (input.subtasks.length < 2) {
604
- return fail("propose_decomposition requires at least two subtasks.");
769
+ throw invalidInput("propose_decomposition requires at least two subtasks.", "subtasks");
605
770
  }
606
771
  const invalid = input.subtasks.find(
607
772
  (subtask) => typeof subtask?.title !== "string" || !subtask.title.trim()
608
773
  );
609
- if (invalid) return fail("Every proposed subtask needs a non-blank title.");
774
+ if (invalid)
775
+ throw invalidInput("Every proposed subtask needs a non-blank title.", "subtasks");
610
776
  const result = await proposeTaskDecomposition(
611
777
  projectRoot,
612
778
  input.boardId,
@@ -618,15 +784,25 @@ async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
618
784
  },
619
785
  eventContext
620
786
  );
621
- if (!result) return fail("Task not found.");
787
+ if (!result) throw notFound("Task not found.");
622
788
  const message = result.proposal.status === "applied" ? `Decomposition applied: ${result.proposal.appliedChildTaskIds?.length ?? 0} child tasks created (parent marked atomic).` : 'Decomposition proposal recorded \u2014 awaiting approval (board policy is "propose"). It can be approved from the WebUI or via update_task.';
623
789
  return okTask(result.board, result.task, message);
624
790
  }
625
791
  case "verify_completion": {
626
792
  if (!input.boardId || !input.taskId) {
627
- return fail("verify_completion requires boardId and taskId.");
793
+ throw invalidInput("verify_completion requires boardId and taskId.");
794
+ }
795
+ let verResult;
796
+ try {
797
+ verResult = await verifyTaskCompletion(projectRoot, input.boardId, input.taskId, {
798
+ persist: false
799
+ });
800
+ } catch (err) {
801
+ if (err instanceof Error && /^(Board|Task) not found\b/.test(err.message)) {
802
+ throw notFound(err.message, { cause: err });
803
+ }
804
+ throw err;
628
805
  }
629
- const verResult = await verifyTaskCompletion(projectRoot, input.boardId, input.taskId);
630
806
  const persistedBoard = await updateTask(
631
807
  projectRoot,
632
808
  input.boardId,
@@ -638,20 +814,15 @@ async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
638
814
  eventContext
639
815
  );
640
816
  if (!persistedBoard) {
641
- return {
642
- ok: false,
643
- verdict: verResult.report.verdict,
644
- message: `Verification succeeded but persist failed: ${verResult.report.markdownSummary}. Board may be stale \u2014 re-run verify_completion.`,
645
- board: verResult.board
646
- };
817
+ throw conflict(
818
+ `Verification ran (verdict: ${verResult.report.verdict}) but its report could not be saved: the task is no longer on the board. Nothing was written; re-run verify_completion.`,
819
+ { retryable: true }
820
+ );
647
821
  }
648
822
  recordKanbanVerificationEvidence(ctx, verResult.report);
649
- const freshTask = persistedBoard.tasks?.find((t) => t.id === input.taskId);
650
- const deterministicVerdicts = ["passed", "failed", "needs_human", "incomplete"];
823
+ const freshTask = persistedBoard.tasks?.find((t) => t.id === verResult.task.id);
651
824
  return {
652
- ok: deterministicVerdicts.includes(
653
- verResult.report.verdict
654
- ),
825
+ ok: true,
655
826
  verdict: verResult.report.verdict,
656
827
  message: verResult.report.markdownSummary,
657
828
  board: persistedBoard,
@@ -693,7 +864,7 @@ async function handleKanbanDetailAction(projectRoot, input, ctx) {
693
864
  }
694
865
  case "add_dependency": {
695
866
  if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
696
- return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
867
+ throw invalidInput("add_dependency requires boardId, taskId, and dependencyTaskId.");
697
868
  }
698
869
  const board = await addDependency(
699
870
  projectRoot,
@@ -702,11 +873,12 @@ async function handleKanbanDetailAction(projectRoot, input, ctx) {
702
873
  input.dependencyTaskId,
703
874
  eventContext
704
875
  );
705
- return board ? okBoard(board, "Dependency added.") : fail("Task not found.");
876
+ if (!board) throw notFound("Task not found.");
877
+ return okBoard(board, "Dependency added.");
706
878
  }
707
879
  case "add_goal_metric": {
708
880
  if (!input.boardId || !input.taskId || !input.metricName) {
709
- return fail("add_goal_metric requires boardId, taskId, and metricName.");
881
+ throw invalidInput("add_goal_metric requires boardId, taskId, and metricName.");
710
882
  }
711
883
  const board = await addGoalMetricToTask(
712
884
  projectRoot,
@@ -723,11 +895,12 @@ async function handleKanbanDetailAction(projectRoot, input, ctx) {
723
895
  },
724
896
  eventContext
725
897
  );
726
- return board ? okBoard(board, "Goal metric added.") : fail("Task not found.");
898
+ if (!board) throw notFound("Task not found.");
899
+ return okBoard(board, "Goal metric added.");
727
900
  }
728
901
  case "update_goal_metric": {
729
902
  if (!input.boardId || !input.taskId || !input.metricId) {
730
- return fail("update_goal_metric requires boardId, taskId, and metricId.");
903
+ throw invalidInput("update_goal_metric requires boardId, taskId, and metricId.");
731
904
  }
732
905
  const board = await updateGoalMetricOnTask(
733
906
  projectRoot,
@@ -745,11 +918,12 @@ async function handleKanbanDetailAction(projectRoot, input, ctx) {
745
918
  },
746
919
  eventContext
747
920
  );
748
- return board ? okBoard(board, "Goal metric updated.") : fail("Metric not found.");
921
+ if (!board) throw notFound("Metric not found.");
922
+ return okBoard(board, "Goal metric updated.");
749
923
  }
750
924
  case "add_check": {
751
925
  if (!input.boardId || !input.taskId || !input.checkDescription) {
752
- return fail("add_check requires boardId, taskId, and checkDescription.");
926
+ throw invalidInput("add_check requires boardId, taskId, and checkDescription.");
753
927
  }
754
928
  const board = await addCheckToTask(
755
929
  projectRoot,
@@ -763,11 +937,12 @@ async function handleKanbanDetailAction(projectRoot, input, ctx) {
763
937
  },
764
938
  eventContext
765
939
  );
766
- return board ? okBoard(board, "Check added.") : fail("Task not found.");
940
+ if (!board) throw notFound("Task not found.");
941
+ return okBoard(board, "Check added.");
767
942
  }
768
943
  case "update_check": {
769
944
  if (!input.boardId || !input.taskId || !input.checkId) {
770
- return fail("update_check requires boardId, taskId, and checkId.");
945
+ throw invalidInput("update_check requires boardId, taskId, and checkId.");
771
946
  }
772
947
  const board = await updateCheckOnTask(
773
948
  projectRoot,
@@ -784,11 +959,12 @@ async function handleKanbanDetailAction(projectRoot, input, ctx) {
784
959
  },
785
960
  eventContext
786
961
  );
787
- return board ? okBoard(board, "Check updated.") : fail("Check not found.");
962
+ if (!board) throw notFound("Check not found.");
963
+ return okBoard(board, "Check updated.");
788
964
  }
789
965
  case "remove_check": {
790
966
  if (!input.boardId || !input.taskId || !input.checkId) {
791
- return fail("remove_check requires boardId, taskId, and checkId.");
967
+ throw invalidInput("remove_check requires boardId, taskId, and checkId.");
792
968
  }
793
969
  const board = await removeCheckFromTask(
794
970
  projectRoot,
@@ -797,11 +973,12 @@ async function handleKanbanDetailAction(projectRoot, input, ctx) {
797
973
  input.checkId,
798
974
  eventContext
799
975
  );
800
- return board ? okBoard(board, "Acceptance criterion removed.") : fail("Check not found on this task.");
976
+ if (!board) throw notFound("Check not found on this task.");
977
+ return okBoard(board, "Acceptance criterion removed.");
801
978
  }
802
979
  case "add_note": {
803
980
  if (!input.boardId || !input.taskId || !input.note)
804
- return fail("add_note requires boardId, taskId, and note.");
981
+ throw invalidInput("add_note requires boardId, taskId, and note.");
805
982
  const board = await addNoteToTask(
806
983
  projectRoot,
807
984
  input.boardId,
@@ -812,11 +989,12 @@ async function handleKanbanDetailAction(projectRoot, input, ctx) {
812
989
  },
813
990
  eventContext
814
991
  );
815
- return board ? okBoard(board, "Note added.") : fail("Task not found.");
992
+ if (!board) throw notFound("Task not found.");
993
+ return okBoard(board, "Note added.");
816
994
  }
817
995
  case "add_link": {
818
996
  if (!input.boardId || !input.taskId || !input.url)
819
- return fail("add_link requires boardId, taskId, and url.");
997
+ throw invalidInput("add_link requires boardId, taskId, and url.");
820
998
  const board = await addLinkToTask(
821
999
  projectRoot,
822
1000
  input.boardId,
@@ -828,11 +1006,14 @@ async function handleKanbanDetailAction(projectRoot, input, ctx) {
828
1006
  },
829
1007
  eventContext
830
1008
  );
831
- return board ? okBoard(board, "Link added.") : fail("Task not found.");
1009
+ if (!board) throw notFound("Task not found.");
1010
+ return okBoard(board, "Link added.");
832
1011
  }
833
1012
  case "split_atomic": {
834
1013
  if (!input.boardId || !input.taskId || !input.childTitles?.length) {
835
- return fail("split_atomic requires boardId, taskId, and childTitles (at least one).");
1014
+ throw invalidInput(
1015
+ "split_atomic requires boardId, taskId, and childTitles (at least one)."
1016
+ );
836
1017
  }
837
1018
  return handleSplitTask(projectRoot, input, { atomic: true }, eventContext);
838
1019
  }
@@ -862,7 +1043,7 @@ import {
862
1043
  repairManagedTaskProjection,
863
1044
  resolveAutoAccept,
864
1045
  setTaskChain,
865
- stripLifecycleIssues,
1046
+ stripLifecycleIssues as stripLifecycleIssues2,
866
1047
  transferTaskToBoard,
867
1048
  transitionTask,
868
1049
  updateTask as updateTask2,
@@ -898,7 +1079,12 @@ function taskInput(input) {
898
1079
  // neither the create input nor the patch, so the instruction described a
899
1080
  // capability that did not exist and the attempt was silently dropped.
900
1081
  ...input.atomic !== void 0 ? { atomic: input.atomic } : {},
901
- ...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
1082
+ // `childTitles` are TITLES of children to create, never ids. They used to
1083
+ // be written straight into `childTaskIds`, recording dangling "ids" such as
1084
+ // "Child 1". add_task creates those children after the parent exists (via
1085
+ // splitTask, the one path that creates child cards); explicit ids of
1086
+ // existing children go through `childTaskIds`.
1087
+ ...input.childTaskIds !== void 0 ? { childTaskIds: input.childTaskIds } : {},
902
1088
  ...input.checkDescription !== void 0 ? {
903
1089
  successCriteria: [
904
1090
  {
@@ -1053,6 +1239,37 @@ async function syncContextTask(ctx, task, options = {}) {
1053
1239
  } catch {
1054
1240
  }
1055
1241
  }
1242
+ async function rollbackStartedAssignment(projectRoot, boardId, taskId, prior, leaseId, eventContext) {
1243
+ try {
1244
+ const restored = prior ? await updateTaskAssignment(projectRoot, boardId, taskId, prior, {
1245
+ ...eventContext,
1246
+ expectedLeaseId: leaseId
1247
+ }) : await releaseTaskClaim(
1248
+ projectRoot,
1249
+ boardId,
1250
+ taskId,
1251
+ { clearAssignee: false, expectedLeaseId: leaseId },
1252
+ eventContext
1253
+ );
1254
+ if (restored) return void 0;
1255
+ return `the running assignment (lease ${leaseId}) could not be rolled back \u2014 the lease changed or the task is gone; release it with release_task.`;
1256
+ } catch (err) {
1257
+ return `the running assignment (lease ${leaseId}) could not be rolled back (${err instanceof Error ? err.message : String(err)}); release it with release_task.`;
1258
+ }
1259
+ }
1260
+ function joinCommitted(...notes) {
1261
+ const present = notes.filter((note) => Boolean(note));
1262
+ return present.length ? present.join(" ") : void 0;
1263
+ }
1264
+ async function missingOrFenced(projectRoot, boardId, taskId, expectedLeaseId, what) {
1265
+ if (expectedLeaseId !== void 0 && await getTask(projectRoot, boardId, taskId)) {
1266
+ throw conflict(
1267
+ `${what}: the task's current lease no longer matches expectedLeaseId "${expectedLeaseId}" (it was recovered or reassigned). Nothing was written.`,
1268
+ { retryable: false }
1269
+ );
1270
+ }
1271
+ throw notFound(`${what}: task not found.`);
1272
+ }
1056
1273
  async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1057
1274
  const eventContext = {
1058
1275
  sessionId: ctx.eventSessionId?.() ?? ctx.session?.id ?? "default-session",
@@ -1060,21 +1277,57 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1060
1277
  };
1061
1278
  switch (input.action) {
1062
1279
  case "add_task": {
1063
- if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
1280
+ if (!input.boardId || !input.title)
1281
+ throw invalidInput("add_task requires boardId and title.");
1282
+ const childTitles = input.childTitles;
1283
+ if (childTitles !== void 0 && (!Array.isArray(childTitles) || childTitles.some((title) => typeof title !== "string" || !title.trim()))) {
1284
+ throw invalidInput("add_task childTitles must be non-blank strings.", "childTitles");
1285
+ }
1064
1286
  const result = await addTask2(projectRoot, input.boardId, taskInput(input), eventContext);
1065
- if (!result) return fail("Board not found.");
1287
+ if (!result) throw notFound("Board not found.");
1288
+ if (childTitles?.length) {
1289
+ let split;
1290
+ try {
1291
+ split = await handleSplitTask(
1292
+ projectRoot,
1293
+ { action: "split_task", boardId: result.board.id, taskId: result.task.id, childTitles },
1294
+ {},
1295
+ eventContext
1296
+ );
1297
+ } catch (err) {
1298
+ const removed = await removeTask(
1299
+ projectRoot,
1300
+ result.board.id,
1301
+ result.task.id,
1302
+ eventContext
1303
+ ).catch(() => null);
1304
+ throw toKanbanToolError(
1305
+ err,
1306
+ removed ? void 0 : `task ${result.task.id} was created but its children were not, and removing it failed; delete it with delete_task.`
1307
+ );
1308
+ }
1309
+ const parent = split.task ?? result.task;
1310
+ await syncContextTask(ctx, parent);
1311
+ return {
1312
+ ok: true,
1313
+ message: `Task added with ${split.children?.length ?? 0} child task(s).${atomicityNudge(parent)}`,
1314
+ board: split.board ?? result.board,
1315
+ task: parent,
1316
+ children: split.children
1317
+ };
1318
+ }
1066
1319
  await syncContextTask(ctx, result.task);
1067
1320
  return okTask(result.board, result.task, `Task added.${atomicityNudge(result.task)}`);
1068
1321
  }
1069
1322
  case "split_task": {
1070
1323
  if (!input.boardId || !input.taskId || !input.childTitles?.length) {
1071
- return fail("split_task requires boardId, taskId, and childTitles.");
1324
+ throw invalidInput("split_task requires boardId, taskId, and childTitles.");
1072
1325
  }
1073
1326
  return handleSplitTask(projectRoot, input, {}, eventContext);
1074
1327
  }
1075
1328
  case "merge_tasks": {
1076
1329
  if (!input.boardId || !input.taskIds?.length || !input.title) {
1077
- return fail("merge_tasks requires boardId, taskIds, and title.");
1330
+ throw invalidInput("merge_tasks requires boardId, taskIds, and title.");
1078
1331
  }
1079
1332
  const result = await mergeTasks(
1080
1333
  projectRoot,
@@ -1089,11 +1342,12 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1089
1342
  },
1090
1343
  eventContext
1091
1344
  );
1092
- return result ? okTask(result.board, result.task, "Tasks merged.") : fail("Board or task not found.");
1345
+ if (!result) throw notFound("Board or task not found.");
1346
+ return okTask(result.board, result.task, "Tasks merged.");
1093
1347
  }
1094
1348
  case "copy_task": {
1095
1349
  if (!input.boardId || !input.taskId || !input.targetBoardId) {
1096
- return fail("copy_task requires boardId, taskId, and targetBoardId.");
1350
+ throw invalidInput("copy_task requires boardId, taskId, and targetBoardId.");
1097
1351
  }
1098
1352
  const result = await copyTaskToBoard(
1099
1353
  projectRoot,
@@ -1108,11 +1362,12 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1108
1362
  eventContext
1109
1363
  }
1110
1364
  );
1111
- return result ? okTask(result.targetBoard, result.task, "Task copied to target board.") : fail("Board or task not found.");
1365
+ if (!result) throw notFound("Board or task not found.");
1366
+ return okTask(result.targetBoard, result.task, "Task copied to target board.");
1112
1367
  }
1113
1368
  case "transfer_task": {
1114
1369
  if (!input.boardId || !input.taskId || !input.targetBoardId) {
1115
- return fail("transfer_task requires boardId, taskId, and targetBoardId.");
1370
+ throw invalidInput("transfer_task requires boardId, taskId, and targetBoardId.");
1116
1371
  }
1117
1372
  const result = await transferTaskToBoard(
1118
1373
  projectRoot,
@@ -1127,23 +1382,27 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1127
1382
  eventContext
1128
1383
  }
1129
1384
  );
1130
- return result ? okTask(result.targetBoard, result.task, "Task transferred to target board.") : fail("Board or task not found.");
1385
+ if (!result) throw notFound("Board or task not found.");
1386
+ return okTask(result.targetBoard, result.task, "Task transferred to target board.");
1131
1387
  }
1132
1388
  case "get_task": {
1133
- if (!input.boardId || !input.taskId) return fail("get_task requires boardId and taskId.");
1389
+ if (!input.boardId || !input.taskId)
1390
+ throw invalidInput("get_task requires boardId and taskId.");
1134
1391
  const task = await getTask(projectRoot, input.boardId, input.taskId);
1135
- return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
1392
+ if (!task) throw notFound("Task not found.");
1393
+ return { ok: true, message: "Task loaded.", task };
1136
1394
  }
1137
1395
  case "start_task": {
1138
1396
  if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
1139
- return fail("start_task requires boardId, taskId, author, and transitionComment.");
1397
+ throw invalidInput("start_task requires boardId, taskId, author, and transitionComment.");
1140
1398
  }
1141
1399
  let board = await getBoard3(projectRoot, input.boardId);
1142
- let task = board?.tasks.find((candidate) => candidate.id === input.taskId);
1143
- if (!board || !task) return fail("Board or task not found.");
1400
+ if (!board) throw notFound("Board not found.");
1401
+ let task = resolveTaskRef(board, input.taskId);
1402
+ if (!task) throw notFound("Task not found.");
1144
1403
  const readiness = evaluateContractGraphReadiness(board, task.id);
1145
1404
  if (!readiness.ready) {
1146
- return fail(
1405
+ throw refused(
1147
1406
  `Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
1148
1407
  );
1149
1408
  }
@@ -1165,7 +1424,7 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1165
1424
  },
1166
1425
  eventContext
1167
1426
  );
1168
- if (!assigned) return fail("Task assignment could not be started.");
1427
+ if (!assigned) throw notFound("Task assignment could not be started: task not found.");
1169
1428
  const started = await updateTask2(
1170
1429
  projectRoot,
1171
1430
  board.id,
@@ -1184,6 +1443,7 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1184
1443
  );
1185
1444
  }
1186
1445
  let stage = task.lifecycle?.currentStage;
1446
+ let movedToTodo = false;
1187
1447
  if (stage === "backlog") {
1188
1448
  const moved = await transitionTask(projectRoot, board.id, task.id, {
1189
1449
  to: "todo",
@@ -1191,7 +1451,8 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1191
1451
  actor: input.author,
1192
1452
  comment: input.transitionComment
1193
1453
  });
1194
- if (!moved) return fail("Task could not enter Todo.");
1454
+ if (!moved) throw notFound("Task could not enter Todo: board or task not found.");
1455
+ movedToTodo = true;
1195
1456
  board = moved.board;
1196
1457
  task = moved.task;
1197
1458
  stage = task.lifecycle?.currentStage;
@@ -1199,6 +1460,8 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1199
1460
  if (stage === "todo" || stage === "review") {
1200
1461
  const now = /* @__PURE__ */ new Date();
1201
1462
  const leaseId = input.leaseId ?? randomUUID2();
1463
+ const priorAssignment = task.assignment ? { ...task.assignment } : void 0;
1464
+ const todoNote = movedToTodo ? "the card was moved Backlog \u2192 Todo." : void 0;
1202
1465
  const assigned = await updateTaskAssignment(
1203
1466
  projectRoot,
1204
1467
  board.id,
@@ -1215,24 +1478,56 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1215
1478
  },
1216
1479
  eventContext
1217
1480
  );
1218
- if (!assigned) return fail("Task assignment could not be started.");
1219
- const moved = await transitionTask(projectRoot, board.id, task.id, {
1220
- to: "running",
1221
- sessionId: eventContext.sessionId,
1222
- actor: input.author,
1223
- comment: input.transitionComment
1224
- });
1225
- if (!moved) return fail("Task could not enter Running.");
1481
+ if (!assigned) {
1482
+ throw notFound("Task assignment could not be started: task not found.", {
1483
+ committed: todoNote
1484
+ });
1485
+ }
1486
+ const startedTaskId = task.id;
1487
+ const startedBoardId = board.id;
1488
+ let moved;
1489
+ try {
1490
+ moved = await transitionTask(projectRoot, startedBoardId, startedTaskId, {
1491
+ to: "running",
1492
+ sessionId: eventContext.sessionId,
1493
+ actor: input.author,
1494
+ comment: input.transitionComment
1495
+ });
1496
+ } catch (err) {
1497
+ const rollback = await rollbackStartedAssignment(
1498
+ projectRoot,
1499
+ startedBoardId,
1500
+ startedTaskId,
1501
+ priorAssignment,
1502
+ leaseId,
1503
+ eventContext
1504
+ );
1505
+ throw toKanbanToolError(err, joinCommitted(todoNote, rollback));
1506
+ }
1507
+ if (!moved) {
1508
+ const rollback = await rollbackStartedAssignment(
1509
+ projectRoot,
1510
+ startedBoardId,
1511
+ startedTaskId,
1512
+ priorAssignment,
1513
+ leaseId,
1514
+ eventContext
1515
+ );
1516
+ throw conflict("Task could not enter Running: the board or task disappeared.", {
1517
+ committed: joinCommitted(todoNote, rollback)
1518
+ });
1519
+ }
1226
1520
  board = moved.board;
1227
1521
  task = moved.task;
1228
1522
  stage = task.lifecycle?.currentStage;
1229
1523
  }
1230
1524
  if (stage !== "running" || task.assignment?.status !== "running") {
1231
- return fail(
1232
- `start_task only accepts Backlog, Todo, Review repair, or live Running cards (current: ${stage ?? "unknown"}).`
1525
+ throw refused(
1526
+ `start_task only accepts Backlog, Todo, Review repair, or live Running cards (current: ${stage ?? "unknown"}).`,
1527
+ { committed: movedToTodo ? "the card was moved Backlog \u2192 Todo." : void 0 }
1233
1528
  );
1234
1529
  }
1235
- ctx.setCurrentKanbanTask(task.id, board.id);
1530
+ ctx.setCurrentKanbanTask?.(task.id, board.id);
1236
1531
  await syncContextTask(ctx, task);
1237
1532
  return okTask(
1238
1533
  board,
@@ -1241,7 +1536,8 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1241
1536
  );
1242
1537
  }
1243
1538
  case "update_task": {
1244
- if (!input.boardId || !input.taskId) return fail("update_task requires boardId and taskId.");
1539
+ if (!input.boardId || !input.taskId)
1540
+ throw invalidInput("update_task requires boardId and taskId.");
1245
1541
  const board = await updateTask2(
1246
1542
  projectRoot,
1247
1543
  input.boardId,
@@ -1249,20 +1545,23 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1249
1545
  taskPatch(input),
1250
1546
  eventContext
1251
1547
  );
1252
- if (board) {
1253
- await syncContextTask(
1254
- ctx,
1255
- board.tasks.find((t) => t.id === input.taskId)
1256
- );
1257
- }
1258
- return board ? okBoard(board, "Task updated.") : fail("Task not found.");
1548
+ if (!board) throw notFound("Task not found.");
1549
+ await syncContextTask(ctx, resolveTaskRef(board, input.taskId));
1550
+ return okBoard(board, "Task updated.");
1259
1551
  }
1260
1552
  case "transition_task": {
1261
1553
  if (!input.boardId || !input.taskId || !input.lifecycleStage || !input.author || !input.transitionComment) {
1262
- return fail(
1554
+ throw invalidInput(
1263
1555
  "transition_task requires boardId, taskId, lifecycleStage, author, and transitionComment."
1264
1556
  );
1265
1557
  }
1558
+ if (input.tickChecks?.length && input.lifecycleStage !== "done") {
1559
+ throw invalidInput(
1560
+ `tickChecks only applies to transition_task with lifecycleStage "done" (got "${input.lifecycleStage}"). Tick criteria with update_check instead.`,
1561
+ "tickChecks"
1562
+ );
1563
+ }
1564
+ let preGateSaved = false;
1266
1565
  if (input.lifecycleStage === "done") {
1267
1566
  const boardBefore = await getBoard3(projectRoot, input.boardId);
1268
1567
  const taskBefore = boardBefore ? await getTask(projectRoot, input.boardId, input.taskId) : null;
@@ -1270,45 +1569,52 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1270
1569
  const preGate = await verifyTaskCompletion2(projectRoot, input.boardId, taskBefore.id, {
1271
1570
  persist: false
1272
1571
  });
1273
- await updateTask2(
1274
- projectRoot,
1275
- input.boardId,
1276
- taskBefore.id,
1277
- {
1278
- verificationReport: preGate.report,
1279
- successCriteria: preGate.task.successCriteria
1280
- },
1281
- eventContext
1572
+ preGateSaved = Boolean(
1573
+ await updateTask2(
1574
+ projectRoot,
1575
+ input.boardId,
1576
+ taskBefore.id,
1577
+ {
1578
+ verificationReport: preGate.report,
1579
+ successCriteria: preGate.task.successCriteria
1580
+ },
1581
+ eventContext
1582
+ )
1282
1583
  );
1283
1584
  }
1284
1585
  }
1285
- const result = await transitionTask(projectRoot, input.boardId, input.taskId, {
1286
- to: input.lifecycleStage,
1287
- sessionId: eventContext.sessionId,
1288
- actor: input.author,
1289
- comment: input.transitionComment,
1290
- ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
1291
- ...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
1292
- ...input.attachmentUrl !== void 0 ? {
1293
- attachment: {
1294
- url: input.attachmentUrl,
1295
- type: input.attachmentType ?? "url",
1296
- ...input.attachmentTitle !== void 0 ? { title: input.attachmentTitle } : {}
1297
- }
1298
- } : {},
1299
- patch: taskPatch(input)
1300
- });
1301
- if (result && input.lifecycleStage === "done" && result.task.verificationReport) {
1302
- recordKanbanVerificationEvidence(ctx, result.task.verificationReport);
1586
+ const preGateNote = preGateSaved ? "the pre-gate verification report was saved on the card (evidence only; the card did not move)." : void 0;
1587
+ let result;
1588
+ try {
1589
+ result = await transitionTask(projectRoot, input.boardId, input.taskId, {
1590
+ to: input.lifecycleStage,
1591
+ sessionId: eventContext.sessionId,
1592
+ actor: input.author,
1593
+ comment: input.transitionComment,
1594
+ ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
1595
+ ...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
1596
+ ...input.attachmentUrl !== void 0 ? {
1597
+ attachment: {
1598
+ url: input.attachmentUrl,
1599
+ type: input.attachmentType ?? "url",
1600
+ ...input.attachmentTitle !== void 0 ? { title: input.attachmentTitle } : {}
1601
+ }
1602
+ } : {},
1603
+ patch: taskPatch(input)
1604
+ });
1605
+ } catch (err) {
1606
+ throw toKanbanToolError(err, preGateNote);
1303
1607
  }
1304
- if (result?.task) {
1305
- await syncContextTask(ctx, result.task);
1608
+ if (!result) throw notFound("Board or task not found.", { committed: preGateNote });
1609
+ if (input.lifecycleStage === "done" && result.task.verificationReport) {
1610
+ recordKanbanVerificationEvidence(ctx, result.task.verificationReport);
1306
1611
  }
1307
- return result ? okTask(result.board, result.task, `Task advanced to ${result.transition.to}.`) : fail("Board or task not found.");
1612
+ await syncContextTask(ctx, result.task);
1613
+ return okTask(result.board, result.task, `Task advanced to ${result.transition.to}.`);
1308
1614
  }
1309
1615
  case "repair_managed_projection": {
1310
1616
  if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
1311
- return fail(
1617
+ throw invalidInput(
1312
1618
  "repair_managed_projection requires boardId, taskId, author, and transitionComment."
1313
1619
  );
1314
1620
  }
@@ -1319,15 +1625,16 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1319
1625
  if (result?.task) {
1320
1626
  await syncContextTask(ctx, result.task);
1321
1627
  }
1322
- return result ? okTask(
1628
+ if (!result) throw notFound("Board or task not found.");
1629
+ return okTask(
1323
1630
  result.board,
1324
1631
  result.task,
1325
1632
  "Managed card projection repaired from lifecycle history."
1326
- ) : fail("Board or task not found.");
1633
+ );
1327
1634
  }
1328
1635
  case "move_task": {
1329
1636
  if (!input.boardId || !input.taskId || !input.targetColumnId) {
1330
- return fail("move_task requires boardId, taskId, and targetColumnId.");
1637
+ throw invalidInput("move_task requires boardId, taskId, and targetColumnId.");
1331
1638
  }
1332
1639
  const board = await moveTask(
1333
1640
  projectRoot,
@@ -1337,30 +1644,28 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1337
1644
  input.order,
1338
1645
  eventContext
1339
1646
  );
1340
- if (board) {
1341
- await syncContextTask(
1342
- ctx,
1343
- board.tasks.find((t) => t.id === input.taskId)
1344
- );
1345
- }
1346
- return board ? okBoard(board, "Task moved.") : fail("Move failed.");
1647
+ if (!board) throw notFound("Move failed: board or task not found.");
1648
+ await syncContextTask(ctx, resolveTaskRef(board, input.taskId));
1649
+ return okBoard(board, "Task moved.");
1347
1650
  }
1348
1651
  case "delete_task": {
1349
- if (!input.boardId || !input.taskId) return fail("delete_task requires boardId and taskId.");
1652
+ if (!input.boardId || !input.taskId)
1653
+ throw invalidInput("delete_task requires boardId and taskId.");
1350
1654
  const boardBefore = await getBoard3(projectRoot, input.boardId);
1351
- const taskToDelete = boardBefore?.tasks.find((t) => t.id === input.taskId);
1655
+ const taskToDelete = boardBefore ? resolveTaskRef(boardBefore, input.taskId) : void 0;
1352
1656
  const board = await removeTask(projectRoot, input.boardId, input.taskId, eventContext);
1353
- if (board && ctx.currentKanbanTaskId === input.taskId) {
1657
+ if (board && taskToDelete && ctx.currentKanbanTaskId === taskToDelete.id) {
1354
1658
  ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
1355
1659
  }
1356
1660
  if (board && taskToDelete) {
1357
1661
  await syncContextTask(ctx, taskToDelete, { remove: true });
1358
1662
  }
1359
- return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
1663
+ if (!board) throw notFound("Task not found.");
1664
+ return okBoard(board, "Task deleted.");
1360
1665
  }
1361
1666
  case "set_chain": {
1362
1667
  if (!input.boardId || !input.taskIds?.length) {
1363
- return fail("set_chain requires boardId and taskIds.");
1668
+ throw invalidInput("set_chain requires boardId and taskIds.");
1364
1669
  }
1365
1670
  const result = await setTaskChain(
1366
1671
  projectRoot,
@@ -1372,28 +1677,30 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1372
1677
  },
1373
1678
  eventContext
1374
1679
  );
1375
- return result ? {
1680
+ if (!result) throw notFound("Board or task not found.");
1681
+ return {
1376
1682
  ok: true,
1377
1683
  message: `Chain set: ${result.chainId}`,
1378
1684
  board: result.board,
1379
1685
  chain: result.tasks
1380
- } : fail("Board or task not found.");
1686
+ };
1381
1687
  }
1382
1688
  case "get_chain": {
1383
1689
  if (!input.boardId || !(input.taskId || input.chainId)) {
1384
- return fail("get_chain requires boardId and taskId or chainId.");
1690
+ throw invalidInput("get_chain requires boardId and taskId or chainId.");
1385
1691
  }
1386
1692
  const result = await getTaskChain(
1387
1693
  projectRoot,
1388
1694
  input.boardId,
1389
1695
  input.taskId ?? input.chainId ?? ""
1390
1696
  );
1391
- return result ? {
1697
+ if (!result) throw notFound("Chain not found.");
1698
+ return {
1392
1699
  ok: true,
1393
1700
  message: `Chain loaded: ${result.chainId}`,
1394
1701
  board: result.board,
1395
1702
  chain: result.tasks
1396
- } : fail("Chain not found.");
1703
+ };
1397
1704
  }
1398
1705
  case "claim_task": {
1399
1706
  const result = await claimReadyTask(
@@ -1406,11 +1713,14 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1406
1713
  },
1407
1714
  eventContext
1408
1715
  );
1409
- return result ? okTask(result.board, result.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
1716
+ if (!result) {
1717
+ return { ok: true, claimed: false, message: "No ready kanban task matched the claim." };
1718
+ }
1719
+ return { ...okTask(result.board, result.task, "Task claimed."), claimed: true };
1410
1720
  }
1411
1721
  case "release_task": {
1412
1722
  if (!input.boardId || !input.taskId) {
1413
- return fail("release_task requires boardId and taskId.");
1723
+ throw invalidInput("release_task requires boardId and taskId.");
1414
1724
  }
1415
1725
  const board = await releaseTaskClaim(
1416
1726
  projectRoot,
@@ -1423,10 +1733,12 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1423
1733
  },
1424
1734
  eventContext
1425
1735
  );
1426
- return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
1736
+ if (!board) throw notFound("Task not found.");
1737
+ return okBoard(board, "Task claim released.");
1427
1738
  }
1428
1739
  case "assign_task": {
1429
- if (!input.boardId || !input.taskId) return fail("assign_task requires boardId and taskId.");
1740
+ if (!input.boardId || !input.taskId)
1741
+ throw invalidInput("assign_task requires boardId and taskId.");
1430
1742
  const board = await assignTask(
1431
1743
  projectRoot,
1432
1744
  input.boardId,
@@ -1434,11 +1746,12 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1434
1746
  assignmentInput(input),
1435
1747
  eventContext
1436
1748
  );
1437
- return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
1749
+ if (!board) throw notFound("Task not found.");
1750
+ return okBoard(board, "Task assigned.");
1438
1751
  }
1439
1752
  case "mark_assignment": {
1440
1753
  if (!input.boardId || !input.taskId)
1441
- return fail("mark_assignment requires boardId and taskId.");
1754
+ throw invalidInput("mark_assignment requires boardId and taskId.");
1442
1755
  const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
1443
1756
  const board = await updateTaskAssignment(
1444
1757
  projectRoot,
@@ -1463,7 +1776,15 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1463
1776
  ...input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
1464
1777
  }
1465
1778
  );
1466
- if (!board) return fail("Task not found.");
1779
+ if (!board) {
1780
+ return missingOrFenced(
1781
+ projectRoot,
1782
+ input.boardId,
1783
+ input.taskId,
1784
+ input.expectedLeaseId,
1785
+ "Assignment not updated"
1786
+ );
1787
+ }
1467
1788
  if (assignmentStatus === "completed" && board.lifecycle?.mode !== "managed") {
1468
1789
  const envGate = readEnvGateEnforcement();
1469
1790
  const finalized = await finalizeTaskCompletion(projectRoot, board.id, input.taskId, {
@@ -1503,7 +1824,7 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1503
1824
  });
1504
1825
  } catch (err) {
1505
1826
  lifecycleWarnings.push(
1506
- `Lifecycle transition to Running deferred: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
1827
+ `Lifecycle transition to Running deferred: ${stripLifecycleIssues2(err instanceof Error ? err.message : String(err))}`
1507
1828
  );
1508
1829
  }
1509
1830
  }
@@ -1526,14 +1847,16 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1526
1847
  });
1527
1848
  } catch (err) {
1528
1849
  lifecycleWarnings.push(
1529
- `Lifecycle transition to Review failed: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
1850
+ `Lifecycle transition to Review failed: ${stripLifecycleIssues2(err instanceof Error ? err.message : String(err))}`
1530
1851
  );
1531
1852
  }
1532
1853
  if (transitionResult) {
1533
1854
  const hasCriteria = (transitionResult.task.successCriteria?.length ?? 0) > 0 || transitionResult.task.atomic === true;
1534
1855
  if (hasCriteria) {
1535
1856
  try {
1536
- const verResult = await verifyTaskCompletion2(projectRoot, board.id, input.taskId);
1857
+ const verResult = await verifyTaskCompletion2(projectRoot, board.id, input.taskId, {
1858
+ persist: false
1859
+ });
1537
1860
  if (verResult.report) {
1538
1861
  recordKanbanVerificationEvidence(ctx, verResult.report);
1539
1862
  }
@@ -1603,7 +1926,7 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1603
1926
  }
1604
1927
  case "heartbeat_assignment": {
1605
1928
  if (!input.boardId || !input.taskId) {
1606
- return fail("heartbeat_assignment requires boardId and taskId.");
1929
+ throw invalidInput("heartbeat_assignment requires boardId and taskId.");
1607
1930
  }
1608
1931
  const board = await heartbeatTaskAssignment(
1609
1932
  projectRoot,
@@ -1616,10 +1939,19 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1616
1939
  },
1617
1940
  eventContext
1618
1941
  );
1619
- return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
1942
+ if (!board) {
1943
+ return missingOrFenced(
1944
+ projectRoot,
1945
+ input.boardId,
1946
+ input.taskId,
1947
+ input.expectedLeaseId,
1948
+ "Heartbeat not recorded"
1949
+ );
1950
+ }
1951
+ return okBoard(board, "Assignment heartbeat updated.");
1620
1952
  }
1621
1953
  case "recover_stale": {
1622
- if (!input.boardId) return fail("recover_stale requires boardId.");
1954
+ if (!input.boardId) throw invalidInput("recover_stale requires boardId.");
1623
1955
  const policyFields = [
1624
1956
  input.recoveryPolicyFailOnCostCeiling !== void 0,
1625
1957
  input.recoveryPolicyReleaseOnFailureKinds !== void 0,
@@ -1651,12 +1983,16 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1651
1983
  },
1652
1984
  eventContext
1653
1985
  );
1654
- return result ? {
1986
+ if (!result) {
1987
+ if (!await getBoard3(projectRoot, input.boardId)) throw notFound("Board not found.");
1988
+ return { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
1989
+ }
1990
+ return {
1655
1991
  ok: true,
1656
1992
  message: `Recovered ${result.tasks.length} stale assignment(s).`,
1657
1993
  board: result.board,
1658
1994
  recoveredTasks: result.tasks
1659
- } : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
1995
+ };
1660
1996
  }
1661
1997
  default:
1662
1998
  return void 0;
@@ -1665,8 +2001,29 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1665
2001
 
1666
2002
  // src/kanban-presence.ts
1667
2003
  import { touchKanbanPresence } from "@wrongstack/kanban";
2004
+
2005
+ // src/kanban-tool-types.ts
2006
+ var KANBAN_READ_ONLY_ACTIONS = [
2007
+ "list_boards",
2008
+ "get_board",
2009
+ "export_markdown",
2010
+ "export_task_graph",
2011
+ "search_tasks",
2012
+ "ready_tasks",
2013
+ "snapshot",
2014
+ "workbench",
2015
+ "get_task",
2016
+ "get_chain",
2017
+ "events",
2018
+ "queue_health",
2019
+ "get_contract_graph"
2020
+ ];
2021
+
2022
+ // src/kanban-presence.ts
2023
+ var READ_ONLY = new Set(KANBAN_READ_ONLY_ACTIONS);
1668
2024
  function createKanbanPresenceWrapper(projectRoot, input, ctx) {
1669
2025
  return async (result) => {
2026
+ if (READ_ONLY.has(input.action)) return result;
1670
2027
  const boardId = result.board?.id ?? input.boardId;
1671
2028
  if (!result.ok || !boardId || !ctx.session?.id || !ctx.agentId) return result;
1672
2029
  try {
@@ -1677,7 +2034,7 @@ function createKanbanPresenceWrapper(projectRoot, input, ctx) {
1677
2034
  taskId: input.taskId ?? result.task?.id,
1678
2035
  runTaskId: input.runTaskId
1679
2036
  });
1680
- return board ? { ...result, board } : result;
2037
+ return board && result.board ? { ...result, board } : result;
1681
2038
  } catch {
1682
2039
  return result;
1683
2040
  }
@@ -1691,40 +2048,86 @@ var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
1691
2048
  "export_markdown",
1692
2049
  "export_task_graph"
1693
2050
  ]);
2051
+ var KANBAN_TRANSCRIPT_ITEM_CAP = 100;
2052
+ var KANBAN_LIST_FIELDS = [
2053
+ { field: "events", tail: true },
2054
+ { field: "tasks" },
2055
+ { field: "boards" },
2056
+ { field: "recoveredTasks" },
2057
+ { field: "chain" },
2058
+ { field: "children" }
2059
+ ];
2060
+ var KANBAN_OBJECT_TRANSCRIPT_BYTE_CAP = 65536;
2061
+ var KANBAN_PRIMARY_OBJECT_TRANSCRIPT_BYTE_CAP = 262144;
2062
+ var KANBAN_OBJECT_FIELDS = [
2063
+ { field: "snapshot", primaryFor: "snapshot" },
2064
+ { field: "workbench", primaryFor: "workbench" },
2065
+ { field: "taskGraph", primaryFor: "export_task_graph" }
2066
+ ];
2067
+ function byteLength(value) {
2068
+ try {
2069
+ return Buffer.byteLength(JSON.stringify(value) ?? "", "utf8");
2070
+ } catch {
2071
+ return 0;
2072
+ }
2073
+ }
2074
+ function objectSummary(field, value) {
2075
+ const summary = {};
2076
+ if (value && typeof value === "object") {
2077
+ for (const [key, entry] of Object.entries(value)) {
2078
+ if (Array.isArray(entry)) summary[`${key}Count`] = entry.length;
2079
+ else if (typeof entry === "number" || typeof entry === "string") summary[key] = entry;
2080
+ }
2081
+ if (field === "workbench") {
2082
+ const totals = value.totals;
2083
+ if (totals && typeof totals === "object") summary["totals"] = totals;
2084
+ }
2085
+ }
2086
+ return summary;
2087
+ }
1694
2088
  function serializeKanbanOutput(output, input) {
1695
2089
  const action = input && typeof input === "object" ? input.action : void 0;
2090
+ const actionName = typeof action === "string" ? action : "";
2091
+ const compact = { ...output };
2092
+ const truncated = { ...output.truncated };
1696
2093
  const board = output.board;
1697
- if (board) {
1698
- const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
1699
- let boardBytes = 0;
1700
- if (!keepFull) {
1701
- try {
1702
- boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
1703
- } catch {
1704
- boardBytes = 0;
1705
- }
1706
- }
1707
- if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
2094
+ if (board && !KANBAN_FULL_BOARD_ACTIONS.has(actionName)) {
2095
+ const boardBytes = byteLength(board);
2096
+ if (boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
1708
2097
  const columns = {};
1709
2098
  for (const column of board.columns) {
1710
2099
  columns[column.title || column.id] = board.tasks.filter(
1711
2100
  (task) => task.columnId === column.id
1712
2101
  ).length;
1713
2102
  }
1714
- const compact = {
1715
- ...output,
1716
- board: {
1717
- id: board.id,
1718
- title: board.title,
1719
- columns,
1720
- totalTasks: board.tasks.length,
1721
- note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
1722
- }
2103
+ compact["board"] = {
2104
+ id: board.id,
2105
+ title: board.title,
2106
+ columns,
2107
+ totalTasks: board.tasks.length,
2108
+ note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
1723
2109
  };
1724
- return JSON.stringify(compact, null, 2);
1725
2110
  }
1726
2111
  }
1727
- return JSON.stringify(output, null, 2);
2112
+ for (const { field, tail } of KANBAN_LIST_FIELDS) {
2113
+ const list = output[field];
2114
+ if (!Array.isArray(list) || list.length <= KANBAN_TRANSCRIPT_ITEM_CAP) continue;
2115
+ compact[field] = tail ? list.slice(-KANBAN_TRANSCRIPT_ITEM_CAP) : list.slice(0, KANBAN_TRANSCRIPT_ITEM_CAP);
2116
+ truncated[field] = { shown: KANBAN_TRANSCRIPT_ITEM_CAP, total: list.length };
2117
+ }
2118
+ for (const { field, primaryFor } of KANBAN_OBJECT_FIELDS) {
2119
+ const value = output[field];
2120
+ if (value === void 0 || value === null) continue;
2121
+ const cap = actionName === primaryFor ? KANBAN_PRIMARY_OBJECT_TRANSCRIPT_BYTE_CAP : KANBAN_OBJECT_TRANSCRIPT_BYTE_CAP;
2122
+ const bytes = byteLength(value);
2123
+ if (bytes <= cap) continue;
2124
+ compact[field] = {
2125
+ ...objectSummary(field, value),
2126
+ note: `${field} (${bytes} bytes) omitted from the transcript; narrow it with boardId, query, or limit.`
2127
+ };
2128
+ }
2129
+ if (Object.keys(truncated).length > 0) compact["truncated"] = truncated;
2130
+ return JSON.stringify(compact, null, 2);
1728
2131
  }
1729
2132
 
1730
2133
  // src/kanban-tool-schema.ts
@@ -1800,10 +2203,6 @@ var KANBAN_INPUT_SCHEMA = {
1800
2203
  taskId: { type: "string" },
1801
2204
  taskIds: { type: "array", items: { type: "string" } },
1802
2205
  chainId: { type: "string" },
1803
- fromNodeId: { type: "string" },
1804
- toNodeId: { type: "string" },
1805
- baseline: { oneOf: [{ type: "string" }, { type: "number" }] },
1806
- threshold: { oneOf: [{ type: "string" }, { type: "number" }] },
1807
2206
  columnId: { type: "string" },
1808
2207
  targetBoardId: { type: "string" },
1809
2208
  targetColumnId: { type: "string" },
@@ -1812,6 +2211,10 @@ var KANBAN_INPUT_SCHEMA = {
1812
2211
  dueDate: { type: "string" },
1813
2212
  tags: { type: "array", items: { type: "string" } },
1814
2213
  labels: { type: "array", items: { type: "string" } },
2214
+ label: {
2215
+ type: "string",
2216
+ description: "search_tasks / ready_tasks / snapshot: filter by a single label (takes precedence over labels[0])."
2217
+ },
1815
2218
  priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
1816
2219
  taskType: {
1817
2220
  type: "string",
@@ -1832,7 +2235,10 @@ var KANBAN_INPUT_SCHEMA = {
1832
2235
  },
1833
2236
  order: { type: "number" },
1834
2237
  query: { type: "string" },
1835
- limit: { type: "number" },
2238
+ limit: {
2239
+ type: "number",
2240
+ description: "ready_tasks: max results. workbench: per-lane and alert limit. events: return only the most recent N events."
2241
+ },
1836
2242
  agentId: { type: "string" },
1837
2243
  name: { type: "string" },
1838
2244
  role: { type: "string" },
@@ -1873,7 +2279,7 @@ var KANBAN_INPUT_SCHEMA = {
1873
2279
  },
1874
2280
  required: ["checkId", "checkStatus"]
1875
2281
  },
1876
- description: "`transition_task` (to=done only): flip one or more manual criteria to `passed` before the gate fires. Read ids from kanban get_task. Non-manual criteria are refused."
2282
+ description: '`transition_task` with lifecycleStage "done" only (rejected for any other stage): flip one or more manual criteria before the gate fires. Read ids from kanban get_task. Non-manual criteria are refused.'
1877
2283
  },
1878
2284
  attachmentUrl: { type: "string" },
1879
2285
  attachmentTitle: { type: "string" },
@@ -2028,7 +2434,6 @@ var KANBAN_INPUT_SCHEMA = {
2028
2434
  includeCompletedTasks: { type: "boolean" },
2029
2435
  preserveAssignment: { type: "boolean" },
2030
2436
  preserveDependencies: { type: "boolean" },
2031
- moveTasksToColumnId: { type: "string" },
2032
2437
  atomicityMode: { type: "string", enum: ["off", "assess", "enforce"] },
2033
2438
  atomicityDecomposition: { type: "string", enum: ["auto", "propose"] },
2034
2439
  gateEnforcement: {
@@ -2071,51 +2476,63 @@ var kanbanTool = {
2071
2476
  inputSchema: KANBAN_INPUT_SCHEMA,
2072
2477
  async execute(input, ctx, _opts) {
2073
2478
  const signal = _opts?.signal ?? ctx.signal ?? new AbortController().signal;
2074
- if (signal.aborted) return fail("Operation aborted.");
2479
+ if (signal.aborted) {
2480
+ throw new KanbanToolError("ABORTED", "Operation aborted before any Kanban work ran.");
2481
+ }
2075
2482
  if (!input || typeof input !== "object" || typeof input.action !== "string" || !input.action.trim()) {
2076
- return fail("kanban: action is required and must be a non-empty string.");
2483
+ throw invalidInput("kanban: action is required and must be a non-empty string.", "action");
2077
2484
  }
2078
2485
  const normalizedInput = {
2079
2486
  ...input,
2080
2487
  action: input.action.trim()
2081
2488
  };
2082
2489
  const projectRoot = ctx.projectRoot;
2083
- if (!projectRoot) return fail("No project root is available.");
2490
+ if (!projectRoot) {
2491
+ throw new KanbanToolError("UNAVAILABLE", "No project root is available.", {
2492
+ retryable: false
2493
+ });
2494
+ }
2084
2495
  const withPresence = createKanbanPresenceWrapper(projectRoot, normalizedInput, ctx);
2496
+ let result;
2085
2497
  try {
2086
- const result = await (async () => {
2087
- const decompositionResult = await handleKanbanDecompositionAction(projectRoot, normalizedInput, ctx);
2088
- if (decompositionResult !== void 0) return decompositionResult;
2089
- const boardResult = await handleKanbanBoardAction(projectRoot, normalizedInput, ctx);
2090
- if (boardResult !== void 0) return boardResult;
2091
- const lifecycleResult = await handleKanbanLifecycleAction(projectRoot, normalizedInput, ctx);
2092
- if (lifecycleResult !== void 0) return lifecycleResult;
2093
- const contractResult = await handleKanbanContractAction(
2094
- projectRoot,
2095
- normalizedInput,
2096
- normalizedInput.author ?? normalizedInput.agentId,
2097
- ctx.eventSessionId?.() ?? ctx.session?.id ?? "default-session"
2098
- );
2099
- if (contractResult !== void 0) return contractResult;
2100
- const detailResult = await handleKanbanDetailAction(projectRoot, normalizedInput, ctx);
2101
- if (detailResult !== void 0) return detailResult;
2102
- return fail(`Unknown kanban action: ${normalizedInput.action}`);
2103
- })();
2104
- if (signal.aborted) return fail("Operation aborted.");
2105
- return await withPresence(result);
2498
+ result = await dispatchKanbanAction(projectRoot, normalizedInput, ctx);
2106
2499
  } catch (err) {
2107
- return fail(stripLifecycleIssues2(err instanceof Error ? err.message : String(err)));
2500
+ throw toKanbanToolError(err);
2108
2501
  }
2502
+ if (signal.aborted) return result;
2503
+ return await withPresence(result);
2109
2504
  },
2110
2505
  serialize(output, input) {
2111
2506
  return serializeKanbanOutput(output, input);
2112
2507
  }
2113
2508
  };
2509
+ async function dispatchKanbanAction(projectRoot, input, ctx) {
2510
+ const decompositionResult = await handleKanbanDecompositionAction(projectRoot, input, ctx);
2511
+ if (decompositionResult !== void 0) return decompositionResult;
2512
+ const boardResult = await handleKanbanBoardAction(projectRoot, input, ctx);
2513
+ if (boardResult !== void 0) return boardResult;
2514
+ const lifecycleResult = await handleKanbanLifecycleAction(projectRoot, input, ctx);
2515
+ if (lifecycleResult !== void 0) return lifecycleResult;
2516
+ const contractResult = await handleKanbanContractAction(
2517
+ projectRoot,
2518
+ input,
2519
+ input.author ?? input.agentId,
2520
+ ctx.eventSessionId?.() ?? ctx.session?.id ?? "default-session"
2521
+ );
2522
+ if (contractResult !== void 0) return contractResult;
2523
+ const detailResult = await handleKanbanDetailAction(projectRoot, input, ctx);
2524
+ if (detailResult !== void 0) return detailResult;
2525
+ throw invalidInput(`Unknown kanban action: ${input.action}`, "action");
2526
+ }
2114
2527
 
2115
2528
  export {
2529
+ KanbanToolError,
2530
+ KanbanInputError,
2531
+ isKanbanToolFailure,
2116
2532
  kanbanEvidenceKey,
2117
2533
  kanbanEvidencePointer,
2118
2534
  recordKanbanVerificationEvidence,
2535
+ KANBAN_READ_ONLY_ACTIONS,
2119
2536
  kanbanTool
2120
2537
  };
2121
- //# sourceMappingURL=chunk-2NTHMZV3.js.map
2538
+ //# sourceMappingURL=chunk-VMJY6FYJ.js.map