@wrongstack/tools 0.313.0 → 0.316.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/dist/_edit-match.d.ts +3 -2
  2. package/dist/_output-spool.d.ts +4 -3
  3. package/dist/_shell-pick.d.ts +2 -1
  4. package/dist/_spawn-stream.d.ts +2 -1
  5. package/dist/_syntax-check.d.ts +2 -1
  6. package/dist/audit.js +10 -4055
  7. package/dist/bash-kill-guard.d.ts +3 -6
  8. package/dist/bash.js +9 -2308
  9. package/dist/batch-tool-use.js +2 -187
  10. package/dist/browser/index.js +25 -1339
  11. package/dist/browser/network-guard-proxy.d.ts +2 -1
  12. package/dist/builtin.js +69 -33949
  13. package/dist/chunk-22B7GE5T.js +260 -0
  14. package/dist/chunk-2VSZC6QM.js +113 -0
  15. package/dist/chunk-3PVYQFRQ.js +472 -0
  16. package/dist/chunk-42BHI7TN.js +337 -0
  17. package/dist/chunk-4BHUSMNL.js +104 -0
  18. package/dist/chunk-4I4LWFOO.js +431 -0
  19. package/dist/chunk-4UESMQDP.js +122 -0
  20. package/dist/chunk-553IQUMO.js +478 -0
  21. package/dist/chunk-5H4GSJAI.js +223 -0
  22. package/dist/chunk-6RNIFLG4.js +305 -0
  23. package/dist/chunk-7EY5DCRO.js +434 -0
  24. package/dist/chunk-A6BRWIXN.js +317 -0
  25. package/dist/chunk-AHHDWNXC.js +200 -0
  26. package/dist/chunk-BSLVR56S.js +199 -0
  27. package/dist/chunk-BTSFTGIL.js +7 -0
  28. package/dist/chunk-D33S4RQG.js +222 -0
  29. package/dist/chunk-DA4KNE2H.js +4412 -0
  30. package/dist/chunk-DR7JOQTC.js +520 -0
  31. package/dist/chunk-EEOFQDT6.js +898 -0
  32. package/dist/chunk-EJWSCQ6D.js +137 -0
  33. package/dist/chunk-EMRGN4KM.js +170 -0
  34. package/dist/chunk-FQZOHLG3.js +89 -0
  35. package/dist/chunk-GOIZNNOA.js +221 -0
  36. package/dist/chunk-HFXSNKCV.js +196 -0
  37. package/dist/chunk-HYX2TZ7O.js +1272 -0
  38. package/dist/chunk-I64BS3VC.js +15 -0
  39. package/dist/chunk-IVWV2UM6.js +955 -0
  40. package/dist/chunk-IXMNTNW5.js +238 -0
  41. package/dist/chunk-IYK7XGDZ.js +892 -0
  42. package/dist/chunk-J5SKD6LN.js +235 -0
  43. package/dist/chunk-JNNVRHMV.js +2111 -0
  44. package/dist/chunk-JOHXDF3D.js +295 -0
  45. package/dist/chunk-JRJJX3L5.js +424 -0
  46. package/dist/chunk-K3WNSHUF.js +1371 -0
  47. package/dist/chunk-L25CVUVR.js +2830 -0
  48. package/dist/chunk-LIJQMYYX.js +59 -0
  49. package/dist/chunk-LWKUTMD5.js +197 -0
  50. package/dist/chunk-MQX2KXVW.js +54 -0
  51. package/dist/chunk-MZZN7VCD.js +359 -0
  52. package/dist/chunk-NBDZMMG2.js +169 -0
  53. package/dist/chunk-NKJW5GFO.js +366 -0
  54. package/dist/chunk-NKNOBUBU.js +522 -0
  55. package/dist/chunk-O4MBV4AX.js +134 -0
  56. package/dist/chunk-OUBDL3WH.js +3515 -0
  57. package/dist/chunk-PQKK5JH7.js +101 -0
  58. package/dist/chunk-PY2IOS7C.js +1000 -0
  59. package/dist/chunk-Q5JPPCZC.js +12 -0
  60. package/dist/chunk-QBV725YG.js +128 -0
  61. package/dist/chunk-QZQSPUBF.js +315 -0
  62. package/dist/chunk-RGISBZBD.js +231 -0
  63. package/dist/chunk-RJZQWEMS.js +84 -0
  64. package/dist/chunk-RXISBFR5.js +315 -0
  65. package/dist/chunk-S5CCLZL3.js +99 -0
  66. package/dist/chunk-SDLVH5RZ.js +178 -0
  67. package/dist/chunk-T2F4S7U4.js +1723 -0
  68. package/dist/chunk-TEBQ4VPZ.js +163 -0
  69. package/dist/chunk-U4D5S3VQ.js +21 -0
  70. package/dist/chunk-U4VAATKP.js +193 -0
  71. package/dist/chunk-UCYQTZ7F.js +113 -0
  72. package/dist/chunk-UKU2ZX4C.js +203 -0
  73. package/dist/chunk-UNSKG6DC.js +159 -0
  74. package/dist/chunk-V43EQ6VR.js +68 -0
  75. package/dist/chunk-VB3MJGCV.js +610 -0
  76. package/dist/chunk-VFKPWSOX.js +154 -0
  77. package/dist/chunk-VJNHFZ7J.js +190 -0
  78. package/dist/chunk-VXECAJJ7.js +1091 -0
  79. package/dist/chunk-WHIIIREL.js +223 -0
  80. package/dist/chunk-Z6DAKQ3U.js +1 -0
  81. package/dist/chunk-ZJQ4IGXK.js +474 -0
  82. package/dist/chunk-ZLTFLC25.js +385 -0
  83. package/dist/circuit-breaker.js +3 -191
  84. package/dist/codebase-index/ast-invariant-engine.d.ts +5 -4
  85. package/dist/codebase-index/binary-frame.d.ts +0 -2
  86. package/dist/codebase-index/bm25.d.ts +1 -1
  87. package/dist/codebase-index/circuit-breaker.d.ts +2 -1
  88. package/dist/codebase-index/codebase-ast-replace-tool.d.ts +3 -2
  89. package/dist/codebase-index/codebase-impact-analysis-tool.d.ts +4 -3
  90. package/dist/codebase-index/codebase-invariant-check-tool.d.ts +3 -2
  91. package/dist/codebase-index/codebase-repo-map-tool.d.ts +3 -2
  92. package/dist/codebase-index/codebase-skeleton-tool.d.ts +3 -2
  93. package/dist/codebase-index/codebase-targeted-test-tool.d.ts +3 -2
  94. package/dist/codebase-index/content-hash.d.ts +0 -2
  95. package/dist/codebase-index/import-extractor.d.ts +0 -13
  96. package/dist/codebase-index/index-service.d.ts +1 -1
  97. package/dist/codebase-index/index.js +84 -13688
  98. package/dist/codebase-index/languages.d.ts +2 -1
  99. package/dist/codebase-index/module-roots.d.ts +3 -2
  100. package/dist/codebase-index/parser-batch.d.ts +0 -5
  101. package/dist/codebase-index/parser-dispatch.d.ts +2 -1
  102. package/dist/codebase-index/parser-output.d.ts +4 -3
  103. package/dist/codebase-index/parser-worker-pool.d.ts +0 -2
  104. package/dist/codebase-index/parser-worker-script.d.ts +1 -16
  105. package/dist/codebase-index/parser-worker-script.js +8 -3995
  106. package/dist/codebase-index/project-server-client-state.d.ts +2 -2
  107. package/dist/codebase-index/project-server-endpoint.d.ts +0 -15
  108. package/dist/codebase-index/project-server.js +60 -8548
  109. package/dist/codebase-index/tree-sitter/visitor.d.ts +2 -1
  110. package/dist/codebase-index/tree-sitter-parser.d.ts +0 -2
  111. package/dist/codebase-index/ts-parser.d.ts +1 -1
  112. package/dist/codebase-index/vector-search.d.ts +2 -1
  113. package/dist/codebase-index/wal-maintenance.d.ts +3 -2
  114. package/dist/codebase-index/worker.js +20 -8416
  115. package/dist/codebase-index/writer-admin.d.ts +0 -1
  116. package/dist/codebase-index/writer-search.d.ts +0 -9
  117. package/dist/design.js +2 -299
  118. package/dist/diff.js +4 -256
  119. package/dist/document.js +4 -222
  120. package/dist/e2e.js +5 -476
  121. package/dist/edit.js +14 -10200
  122. package/dist/exec.js +16 -3131
  123. package/dist/fetch.js +8 -398
  124. package/dist/format.js +10 -3750
  125. package/dist/generic-parser-VUXDIAOZ.js +13 -0
  126. package/dist/git.js +4 -380
  127. package/dist/glob.js +6 -280
  128. package/dist/go-parser-JXYKJHRC.js +469 -0
  129. package/dist/grep.js +7 -604
  130. package/dist/index.js +318 -35387
  131. package/dist/install.js +10 -4091
  132. package/dist/json-parser-IBHZ4BZC.js +213 -0
  133. package/dist/json.js +4 -665
  134. package/dist/kanban-contract-actions.d.ts +1 -1
  135. package/dist/kanban-detail-actions.d.ts +2 -1
  136. package/dist/kanban-presence.d.ts +2 -1
  137. package/dist/kanban-serializer.d.ts +0 -8
  138. package/dist/kanban-split-task-handler.d.ts +2 -2
  139. package/dist/kanban-tool-types.d.ts +2 -1
  140. package/dist/kanban.js +3 -2096
  141. package/dist/languages/diagnostics.d.ts +2 -1
  142. package/dist/languages/index.js +23 -4664
  143. package/dist/languages/legacy-bridge.d.ts +3 -2
  144. package/dist/languages/types.d.ts +2 -1
  145. package/dist/legacy-bridge-7M6QLUQN.js +18 -0
  146. package/dist/lint.js +10 -3706
  147. package/dist/logs.js +4 -287
  148. package/dist/memory.js +6 -215
  149. package/dist/mode.js +3 -99
  150. package/dist/outdated.js +4 -5170
  151. package/dist/pack.js +65 -33897
  152. package/dist/parser-dispatch-QH4XPGG6.js +14 -0
  153. package/dist/patch.js +13 -9935
  154. package/dist/plan.js +5 -3286
  155. package/dist/process-registry.js +8 -651
  156. package/dist/ps-slash.js +12 -1439
  157. package/dist/pwsh.d.ts +0 -1
  158. package/dist/py-parser-IS4HKYMF.js +16 -0
  159. package/dist/read.js +13 -9816
  160. package/dist/replace.js +15 -10014
  161. package/dist/rs-parser-XHUUO2KZ.js +81 -0
  162. package/dist/scaffold.js +4 -253
  163. package/dist/search.js +5 -585
  164. package/dist/session-kanban-sync.d.ts +0 -6
  165. package/dist/session-kanban.js +30 -972
  166. package/dist/skill.js +2 -215
  167. package/dist/spawn-background.d.ts +1 -1
  168. package/dist/task.js +5 -3410
  169. package/dist/test.js +10 -3773
  170. package/dist/todo.js +4 -2891
  171. package/dist/tool-help.js +3 -123
  172. package/dist/tool-search.js +3 -79
  173. package/dist/tool-tier.js +68 -33984
  174. package/dist/tool-use.js +2 -93
  175. package/dist/tree-sitter-parser-AIERW3YI.js +15 -0
  176. package/dist/tree.js +4 -288
  177. package/dist/ts-parser-RCTYKSVI.js +232 -0
  178. package/dist/typecheck.js +10 -3753
  179. package/dist/win32.js +6 -60
  180. package/dist/write.js +14 -9882
  181. package/dist/yaml-parser-4SAXV3PL.js +157 -0
  182. package/package.json +5 -5
@@ -0,0 +1,2111 @@
1
+ import {
2
+ applySessionKanbanTaskToSource,
3
+ taskFileToSerializedGraph
4
+ } from "./chunk-PY2IOS7C.js";
5
+
6
+ // src/kanban.ts
7
+ import { stripLifecycleIssues as stripLifecycleIssues2 } from "@wrongstack/kanban";
8
+
9
+ // src/kanban-board-actions.ts
10
+ import { loadTasks } from "@wrongstack/core/storage";
11
+ import { deserializeTaskGraph, serializeTaskGraph } from "@wrongstack/core/tasking";
12
+ import {
13
+ addTask,
14
+ adoptManagedLifecycle,
15
+ createBoard,
16
+ createBoardFromTaskGraph,
17
+ createBoardFromText,
18
+ duplicateBoard,
19
+ exportBoardAsMarkdown,
20
+ exportBoardToTaskGraph,
21
+ getBoard as getBoard2,
22
+ getKanbanOrchestrationSnapshot,
23
+ getKanbanQueueHealth,
24
+ listBoards,
25
+ listKanbanEvents,
26
+ listReadyTasks,
27
+ parseLinesIntoTasks,
28
+ removeBoard,
29
+ searchKanban,
30
+ syncBoardFromTaskGraph,
31
+ updateBoard
32
+ } from "@wrongstack/kanban";
33
+
34
+ // src/kanban-board-inputs.ts
35
+ function agentSettableGate(enforcement) {
36
+ if (enforcement === void 0 || enforcement === "off") return {};
37
+ return { completionGate: { enforcement } };
38
+ }
39
+ function boardCreateInput(input, title) {
40
+ return {
41
+ title,
42
+ ...input.description !== void 0 ? { description: input.description } : {},
43
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
44
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
45
+ ...input.atomicityMode !== void 0 ? {
46
+ atomicity: {
47
+ mode: input.atomicityMode,
48
+ decomposition: input.atomicityDecomposition ?? "propose"
49
+ }
50
+ } : {},
51
+ ...agentSettableGate(input.gateEnforcement)
52
+ };
53
+ }
54
+ function boardUpdatePatch(input) {
55
+ return {
56
+ ...input.title !== void 0 ? { title: input.title } : {},
57
+ ...input.description !== void 0 ? { description: input.description } : {},
58
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
59
+ ...input.atomicityMode !== void 0 ? {
60
+ atomicity: {
61
+ mode: input.atomicityMode,
62
+ decomposition: input.atomicityDecomposition ?? "propose"
63
+ }
64
+ } : {},
65
+ ...agentSettableGate(input.gateEnforcement)
66
+ };
67
+ }
68
+ function duplicateBoardOptions(input) {
69
+ return {
70
+ ...input.title !== void 0 ? { title: input.title } : {},
71
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
72
+ ...input.includeTasks !== void 0 ? { includeTasks: input.includeTasks } : {},
73
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
74
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {}
75
+ };
76
+ }
77
+
78
+ // src/kanban-split-task-handler.ts
79
+ 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
+ async function handleSplitTask(projectRoot, input, extraSplitOptions, eventContext) {
103
+ const boardId = input.boardId;
104
+ const taskId = input.taskId;
105
+ const childTitles = input.childTitles;
106
+ if (!boardId || !taskId || !childTitles?.length) {
107
+ return fail("split requires boardId, taskId, and at least one childTitles.");
108
+ }
109
+ const {
110
+ targetColumnId,
111
+ inheritAssignment,
112
+ inheritLabels,
113
+ inheritSuccessCriteria,
114
+ inheritGoalMetrics,
115
+ inheritDependencies,
116
+ chainChildren,
117
+ rewireDependents
118
+ } = input;
119
+ const result = await splitTask(
120
+ projectRoot,
121
+ boardId,
122
+ taskId,
123
+ {
124
+ titles: childTitles,
125
+ ...extraSplitOptions,
126
+ ...targetColumnId !== void 0 ? { columnId: targetColumnId } : {},
127
+ ...inheritAssignment !== void 0 ? { inheritAssignment } : {},
128
+ ...inheritLabels !== void 0 ? { inheritLabels } : {},
129
+ ...inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria } : {},
130
+ ...inheritGoalMetrics !== void 0 ? { inheritGoalMetrics } : {},
131
+ ...inheritDependencies !== void 0 ? { inheritDependencies } : {},
132
+ ...chainChildren !== void 0 ? { chainChildren } : {},
133
+ ...rewireDependents !== void 0 ? { rewireDependents } : {}
134
+ },
135
+ eventContext
136
+ );
137
+ if (!result) return fail("Task not found.");
138
+ const freshParent = result.board.tasks?.find((t) => t.id === taskId);
139
+ 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
+ );
143
+ }
144
+ return {
145
+ ok: true,
146
+ message: `${result.children.length} child task(s) created.`,
147
+ board: result.board,
148
+ task: freshParent,
149
+ children: result.children
150
+ };
151
+ }
152
+ async function requireBoard(projectRoot, boardId) {
153
+ return boardId ? getBoard(projectRoot, boardId) : null;
154
+ }
155
+
156
+ // src/kanban-board-actions.ts
157
+ async function handleKanbanBoardAction(projectRoot, input, ctx) {
158
+ const eventContext = {
159
+ sessionId: ctx.eventSessionId(),
160
+ ...ctx.agentId !== void 0 ? { actor: ctx.agentId } : {}
161
+ };
162
+ switch (input.action) {
163
+ case "list_boards": {
164
+ const boards = await listBoards(projectRoot);
165
+ return { ok: true, message: `${boards.length} board(s).`, boards };
166
+ }
167
+ case "get_board": {
168
+ const board = await requireBoard(projectRoot, input.boardId);
169
+ return board ? okBoard(board) : fail("Board not found.");
170
+ }
171
+ case "create_board": {
172
+ if (!input.title) return fail("create_board requires title.");
173
+ const existing = (await listBoards(projectRoot)).filter(
174
+ (candidate) => (candidate.kind ?? "project") === "project"
175
+ );
176
+ const board = await createBoard(projectRoot, boardCreateInput(input, input.title));
177
+ 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(
178
+ ", "
179
+ )}${existing.length > 3 ? ", \u2026" : ""}. If this work belongs to one of them, add_task there instead and delete this board.` : "";
180
+ return { ok: true, message: `Board created: ${board.title}.${note}`, board };
181
+ }
182
+ case "update_board": {
183
+ if (!input.boardId) return fail("update_board requires boardId.");
184
+ const board = await updateBoard(projectRoot, input.boardId, boardUpdatePatch(input));
185
+ return board ? okBoard(board, "Board updated.") : fail("Board not found.");
186
+ }
187
+ case "adopt_managed_lifecycle": {
188
+ if (!input.boardId || !input.author || !input.transitionComment) {
189
+ return fail(
190
+ "adopt_managed_lifecycle requires boardId, author, transitionComment, and five ordered columns."
191
+ );
192
+ }
193
+ if (input.columns?.length !== 5) {
194
+ return fail(
195
+ "adopt_managed_lifecycle columns must be ordered as backlog, todo, running, review, done."
196
+ );
197
+ }
198
+ const [backlog, todo, running, review, done] = input.columns;
199
+ if (!backlog || !todo || !running || !review || !done) {
200
+ return fail("adopt_managed_lifecycle columns must contain five nonblank ids.");
201
+ }
202
+ const board = await adoptManagedLifecycle(projectRoot, input.boardId, {
203
+ columns: { backlog, todo, running, review, done },
204
+ actor: input.author,
205
+ comment: input.transitionComment
206
+ });
207
+ return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
208
+ }
209
+ case "release_managed_lifecycle": {
210
+ if (!input.boardId) return fail("release_managed_lifecycle requires boardId.");
211
+ const board = await updateBoard(projectRoot, input.boardId, { lifecycle: null });
212
+ return board ? okBoard(
213
+ board,
214
+ "Managed lifecycle released; the board now tracks work without strict gates."
215
+ ) : fail("Board not found.");
216
+ }
217
+ case "duplicate_board": {
218
+ if (!input.boardId) return fail("duplicate_board requires boardId.");
219
+ const board = await duplicateBoard(projectRoot, input.boardId, duplicateBoardOptions(input));
220
+ return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
221
+ }
222
+ case "delete_board": {
223
+ if (!input.boardId) return fail("delete_board requires boardId.");
224
+ const removed = await removeBoard(projectRoot, input.boardId);
225
+ return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
226
+ }
227
+ case "generate_board": {
228
+ if (!input.description) return fail("generate_board requires description.");
229
+ const boardInput = createBoardFromText({
230
+ description: input.description,
231
+ ...input.title !== void 0 ? { title: input.title } : {},
232
+ ...input.context !== void 0 ? { context: input.context } : {}
233
+ });
234
+ const board = await createBoard(projectRoot, boardInput);
235
+ for (const tInput of parseLinesIntoTasks(
236
+ input.description,
237
+ board.columns[0]?.id ?? "backlog"
238
+ )) {
239
+ await addTask(projectRoot, board.id, tInput, eventContext);
240
+ }
241
+ return okBoard(await getBoard2(projectRoot, board.id) ?? board, "Board generated.");
242
+ }
243
+ case "export_markdown": {
244
+ const board = await requireBoard(projectRoot, input.boardId);
245
+ if (!board) return fail("Board not found.");
246
+ return {
247
+ ok: true,
248
+ message: "Board exported.",
249
+ board,
250
+ markdown: exportBoardAsMarkdown(board)
251
+ };
252
+ }
253
+ case "export_task_graph": {
254
+ if (!input.boardId) return fail("export_task_graph requires boardId.");
255
+ const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
256
+ ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
257
+ ...input.specId !== void 0 ? { specId: input.specId } : {},
258
+ ...input.title !== void 0 ? { title: input.title } : {},
259
+ ...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
260
+ ...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
261
+ });
262
+ if (!exported) return fail("Board not found.");
263
+ return {
264
+ ok: true,
265
+ message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
266
+ board: exported.board,
267
+ taskGraph: serializeTaskGraph(exported.graph)
268
+ };
269
+ }
270
+ case "sync_task_graph": {
271
+ if (!input.boardId || !input.taskGraph) {
272
+ return fail("sync_task_graph requires boardId and taskGraph.");
273
+ }
274
+ const graph = deserializeTaskGraph(input.taskGraph);
275
+ const result = await syncBoardFromTaskGraph(projectRoot, input.boardId, graph, {
276
+ ...input.title !== void 0 ? { title: input.title } : {},
277
+ ...input.description !== void 0 ? { description: input.description } : {},
278
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
279
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
280
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
281
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
282
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
283
+ ...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
284
+ ...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
285
+ });
286
+ return result ? {
287
+ ok: true,
288
+ message: `Task graph synced: ${result.createdTaskIds.length} created, ${result.updatedTaskIds.length} updated, ${result.archivedTaskIds.length} archived.`,
289
+ board: result.board
290
+ } : fail("Board not found.");
291
+ }
292
+ case "create_from_graph": {
293
+ if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
294
+ const graph = deserializeTaskGraph(input.taskGraph);
295
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
296
+ ...input.title !== void 0 ? { title: input.title } : {},
297
+ ...input.description !== void 0 ? { description: input.description } : {},
298
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
299
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
300
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
301
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
302
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
303
+ });
304
+ return {
305
+ ok: true,
306
+ message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
307
+ board
308
+ };
309
+ }
310
+ case "import_session_tasks": {
311
+ const taskPath = ctx.meta?.["task.path"];
312
+ if (!taskPath) return fail("No session task file for this session.");
313
+ const file = await loadTasks(taskPath);
314
+ if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
315
+ const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
316
+ const graph = deserializeTaskGraph(taskFileToSerializedGraph(file.tasks, sessionId));
317
+ const tags = ["session", `session:${sessionId}`];
318
+ const existing = (await listBoards(projectRoot)).find(
319
+ (b) => b.tags?.includes(`session:${sessionId}`)
320
+ );
321
+ if (existing) {
322
+ const result = await syncBoardFromTaskGraph(projectRoot, existing.id, graph, {
323
+ sourceSystem: "session",
324
+ tags,
325
+ archiveMissingTasks: true,
326
+ includeCompletedTasks: true
327
+ });
328
+ return result ? {
329
+ ok: true,
330
+ message: `Synced ${file.tasks.length} session tasks into board "${result.board.title}".`,
331
+ board: result.board
332
+ } : fail("Session board vanished mid-sync.");
333
+ }
334
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
335
+ title: `Session tasks (${sessionId.slice(0, 8)})`,
336
+ sourceSystem: "session",
337
+ tags
338
+ });
339
+ return {
340
+ ok: true,
341
+ message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
342
+ board
343
+ };
344
+ }
345
+ case "search_tasks": {
346
+ const tasks = await searchKanban(projectRoot, {
347
+ query: input.query,
348
+ boardId: input.boardId,
349
+ assignedAgent: input.agentId,
350
+ status: input.status,
351
+ priority: input.priority,
352
+ label: input.labels?.[0],
353
+ chainId: input.chainId
354
+ });
355
+ return { ok: true, message: `${tasks.length} task(s) matched.`, tasks };
356
+ }
357
+ case "ready_tasks": {
358
+ const tasks = await listReadyTasks(projectRoot, {
359
+ query: input.query,
360
+ boardId: input.boardId,
361
+ assignedAgent: input.agentId,
362
+ priority: input.priority,
363
+ label: input.labels?.[0],
364
+ chainId: input.chainId,
365
+ limit: input.limit
366
+ });
367
+ return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
368
+ }
369
+ case "snapshot": {
370
+ const snapshot = await getKanbanOrchestrationSnapshot(projectRoot, {
371
+ query: input.query,
372
+ boardId: input.boardId,
373
+ assignedAgent: input.agentId,
374
+ status: input.status,
375
+ priority: input.priority,
376
+ label: input.labels?.[0],
377
+ chainId: input.chainId
378
+ });
379
+ return {
380
+ ok: true,
381
+ message: `${snapshot.ready.length} ready, ${snapshot.running.length} running, ${snapshot.blocked.length} blocked.`,
382
+ snapshot
383
+ };
384
+ }
385
+ case "events": {
386
+ if (!input.boardId) return fail("events requires boardId.");
387
+ const eventList = await listKanbanEvents(projectRoot, input.boardId);
388
+ return {
389
+ ok: true,
390
+ message: `${eventList.length} event(s).`,
391
+ events: eventList
392
+ };
393
+ }
394
+ case "queue_health": {
395
+ const health = await getKanbanQueueHealth(projectRoot, {
396
+ ...input.boardId !== void 0 ? { boardId: input.boardId } : {}
397
+ });
398
+ return {
399
+ ok: true,
400
+ message: `Counts: startable=${health.counts.startable}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
401
+ queueHealth: health
402
+ };
403
+ }
404
+ default:
405
+ return void 0;
406
+ }
407
+ }
408
+
409
+ // src/kanban-contract-actions.ts
410
+ import {
411
+ addContractEdge,
412
+ configureContractGraph,
413
+ evaluateTaskContractGraph,
414
+ getContractGraph,
415
+ removeContractEdge,
416
+ removeContractNode,
417
+ upsertContractNode
418
+ } from "@wrongstack/kanban";
419
+ async function handleKanbanContractAction(projectRoot, input, actor, sessionId) {
420
+ const eventContext = { sessionId, ...actor !== void 0 ? { actor } : {} };
421
+ switch (input.action) {
422
+ case "get_contract_graph": {
423
+ if (!input.boardId) return fail("get_contract_graph requires boardId.");
424
+ const found = await getContractGraph(projectRoot, input.boardId);
425
+ if (!found) return fail("Board not found.");
426
+ 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.");
428
+ return {
429
+ ok: true,
430
+ 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.",
431
+ board: found.board,
432
+ contractGraph: found.graph,
433
+ ...evaluated ? { contractEvaluation: evaluated.evaluation } : {}
434
+ };
435
+ }
436
+ case "configure_contract_graph": {
437
+ if (!input.boardId) return fail("configure_contract_graph requires boardId.");
438
+ const enforcement = input.contractEnforcement ?? "advisory";
439
+ const board = await configureContractGraph(
440
+ projectRoot,
441
+ input.boardId,
442
+ enforcement,
443
+ eventContext
444
+ );
445
+ return board ? okBoard(board, `Contract map enforcement set to ${enforcement}.`) : fail("Board not found.");
446
+ }
447
+ case "upsert_contract_node": {
448
+ if (!input.boardId || !input.taskId) {
449
+ return fail("upsert_contract_node requires boardId and taskId.");
450
+ }
451
+ if (!input.contractNodeKind || !input.contractNodeTitle) {
452
+ return fail("upsert_contract_node requires contractNodeKind and contractNodeTitle.");
453
+ }
454
+ const waiver = input.contractNodeState === "waived" ? {
455
+ actor: actor ?? "agent",
456
+ reason: input.contractWaiverReason ?? "",
457
+ at: (/* @__PURE__ */ new Date()).toISOString()
458
+ } : void 0;
459
+ if (waiver && !waiver.reason.trim()) {
460
+ return fail("A waived contract node requires contractWaiverReason.");
461
+ }
462
+ const result = await upsertContractNode(
463
+ projectRoot,
464
+ input.boardId,
465
+ {
466
+ taskId: input.taskId,
467
+ kind: input.contractNodeKind,
468
+ title: input.contractNodeTitle,
469
+ ...input.contractNodeId !== void 0 ? { id: input.contractNodeId } : {},
470
+ ...input.contractNodeDescription !== void 0 ? { description: input.contractNodeDescription } : {},
471
+ ...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
472
+ ...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
473
+ ...input.contractCheckId !== void 0 ? { checkId: input.contractCheckId } : {},
474
+ ...input.contractMetricId !== void 0 ? { metricId: input.contractMetricId } : {},
475
+ ...waiver ? { waiver } : {},
476
+ ...actor !== void 0 ? { createdBy: actor } : {}
477
+ },
478
+ eventContext
479
+ );
480
+ if (!result) return fail("Board or task not found.");
481
+ return {
482
+ ok: true,
483
+ message: `Contract node ${result.node.kind} "${result.node.title}" saved (${result.node.id}).`,
484
+ board: result.board,
485
+ contractGraph: result.board.contractGraph ?? null
486
+ };
487
+ }
488
+ case "remove_contract_node": {
489
+ if (!input.boardId || !input.contractNodeId) {
490
+ return fail("remove_contract_node requires boardId and contractNodeId.");
491
+ }
492
+ const board = await removeContractNode(
493
+ projectRoot,
494
+ input.boardId,
495
+ input.contractNodeId,
496
+ eventContext
497
+ );
498
+ return board ? okBoard(board, "Contract node removed, along with every edge that touched it.") : fail("Contract node not found.");
499
+ }
500
+ case "add_contract_edge": {
501
+ if (!input.boardId || !input.contractEdgeFrom || !input.contractEdgeTo) {
502
+ return fail("add_contract_edge requires boardId, contractEdgeFrom, and contractEdgeTo.");
503
+ }
504
+ if (!input.contractEdgeType) return fail("add_contract_edge requires contractEdgeType.");
505
+ const result = await addContractEdge(
506
+ projectRoot,
507
+ input.boardId,
508
+ {
509
+ from: input.contractEdgeFrom,
510
+ to: input.contractEdgeTo,
511
+ type: input.contractEdgeType,
512
+ ...input.contractEdgeId !== void 0 ? { id: input.contractEdgeId } : {},
513
+ ...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
514
+ ...input.contractEdgeRationale !== void 0 ? { rationale: input.contractEdgeRationale } : {},
515
+ ...actor !== void 0 ? { createdBy: actor } : {}
516
+ },
517
+ eventContext
518
+ );
519
+ if (!result) return fail("Board not found.");
520
+ return {
521
+ ok: true,
522
+ message: `Contract edge ${result.edge.type}: ${result.edge.from} \u2192 ${result.edge.to}.`,
523
+ board: result.board,
524
+ contractGraph: result.board.contractGraph ?? null
525
+ };
526
+ }
527
+ case "remove_contract_edge": {
528
+ if (!input.boardId || !input.contractEdgeId) {
529
+ return fail("remove_contract_edge requires boardId and contractEdgeId.");
530
+ }
531
+ const board = await removeContractEdge(
532
+ projectRoot,
533
+ input.boardId,
534
+ input.contractEdgeId,
535
+ eventContext
536
+ );
537
+ return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
538
+ }
539
+ default:
540
+ return void 0;
541
+ }
542
+ }
543
+
544
+ // src/kanban-decomposition-actions.ts
545
+ import {
546
+ assessTaskAtomicity,
547
+ proposeTaskDecomposition,
548
+ updateTask,
549
+ verifyTaskCompletion
550
+ } from "@wrongstack/kanban";
551
+
552
+ // src/kanban-evidence-bridge.ts
553
+ import { recordCompletedWorkEvidence } from "@wrongstack/core/utils";
554
+ function kanbanEvidenceKey(boardId, taskId) {
555
+ return `kanban:${boardId}:${taskId}`;
556
+ }
557
+ function kanbanEvidencePointer(boardId, taskId) {
558
+ return `kanban://${boardId}/${taskId}#verificationReport`;
559
+ }
560
+ function recordKanbanVerificationEvidence(ctx, report) {
561
+ try {
562
+ const passed = report.checks.filter((check) => check.status === "passed").length;
563
+ const completedAt = Date.parse(report.completedAt);
564
+ recordCompletedWorkEvidence(ctx, {
565
+ key: kanbanEvidenceKey(report.boardId, report.taskId),
566
+ source: "verification",
567
+ summary: `${report.taskTitle} \u2014 verification ${report.verdict} (${passed}/${report.checks.length} checks)`,
568
+ ...Number.isFinite(completedAt) ? { completedAt } : {},
569
+ evidence: kanbanEvidencePointer(report.boardId, report.taskId)
570
+ });
571
+ } catch {
572
+ }
573
+ }
574
+
575
+ // src/kanban-decomposition-actions.ts
576
+ async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
577
+ const eventContext = {
578
+ sessionId: ctx.eventSessionId(),
579
+ ...ctx.agentId !== void 0 ? { actor: ctx.agentId } : {}
580
+ };
581
+ switch (input.action) {
582
+ case "assess_atomicity": {
583
+ if (!input.boardId || !input.taskId) {
584
+ return fail("assess_atomicity requires boardId and taskId.");
585
+ }
586
+ const result = await assessTaskAtomicity(projectRoot, input.boardId, input.taskId, {
587
+ assessedBy: "agent",
588
+ eventContext
589
+ });
590
+ if (!result) return fail("Task not found.");
591
+ const failing = result.assessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason);
592
+ 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
+ return okTask(
594
+ result.board,
595
+ result.task,
596
+ `Atomicity verdict: ${result.assessment.verdict} (score ${result.assessment.score}).${guidance}`
597
+ );
598
+ }
599
+ case "propose_decomposition": {
600
+ if (!input.boardId || !input.taskId || !input.subtasks?.length) {
601
+ return fail("propose_decomposition requires boardId, taskId, and subtasks (2+).");
602
+ }
603
+ if (input.subtasks.length < 2) {
604
+ return fail("propose_decomposition requires at least two subtasks.");
605
+ }
606
+ const invalid = input.subtasks.find(
607
+ (subtask) => typeof subtask?.title !== "string" || !subtask.title.trim()
608
+ );
609
+ if (invalid) return fail("Every proposed subtask needs a non-blank title.");
610
+ const result = await proposeTaskDecomposition(
611
+ projectRoot,
612
+ input.boardId,
613
+ input.taskId,
614
+ {
615
+ subtasks: input.subtasks,
616
+ ...input.note !== void 0 ? { rationale: input.note } : {},
617
+ ...ctx.agentId !== void 0 ? { proposedBy: ctx.agentId } : {}
618
+ },
619
+ eventContext
620
+ );
621
+ if (!result) return fail("Task not found.");
622
+ 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
+ return okTask(result.board, result.task, message);
624
+ }
625
+ case "verify_completion": {
626
+ if (!input.boardId || !input.taskId) {
627
+ return fail("verify_completion requires boardId and taskId.");
628
+ }
629
+ const verResult = await verifyTaskCompletion(projectRoot, input.boardId, input.taskId);
630
+ const persistedBoard = await updateTask(
631
+ projectRoot,
632
+ input.boardId,
633
+ input.taskId,
634
+ {
635
+ verificationReport: verResult.report,
636
+ successCriteria: verResult.task.successCriteria
637
+ },
638
+ eventContext
639
+ );
640
+ 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
+ };
647
+ }
648
+ recordKanbanVerificationEvidence(ctx, verResult.report);
649
+ const freshTask = persistedBoard.tasks?.find((t) => t.id === input.taskId);
650
+ const deterministicVerdicts = ["passed", "failed", "needs_human", "incomplete"];
651
+ return {
652
+ ok: deterministicVerdicts.includes(
653
+ verResult.report.verdict
654
+ ),
655
+ verdict: verResult.report.verdict,
656
+ message: verResult.report.markdownSummary,
657
+ board: persistedBoard,
658
+ task: freshTask ?? verResult.task
659
+ };
660
+ }
661
+ default:
662
+ return void 0;
663
+ }
664
+ }
665
+
666
+ // src/kanban-detail-actions.ts
667
+ import {
668
+ addCheckToTask,
669
+ addDependency,
670
+ addGoalMetricToTask,
671
+ addLinkToTask,
672
+ addNoteToTask,
673
+ getKanbanWorkbench,
674
+ removeCheckFromTask,
675
+ updateCheckOnTask,
676
+ updateGoalMetricOnTask
677
+ } from "@wrongstack/kanban";
678
+ async function handleKanbanDetailAction(projectRoot, input, ctx) {
679
+ const eventContext = {
680
+ sessionId: ctx.eventSessionId(),
681
+ ...ctx.agentId !== void 0 ? { actor: ctx.agentId } : {}
682
+ };
683
+ switch (input.action) {
684
+ case "workbench": {
685
+ const workbench = await getKanbanWorkbench(projectRoot, {
686
+ ...input.limit !== void 0 ? { limitPerLane: input.limit, alertLimit: input.limit } : {}
687
+ });
688
+ return {
689
+ ok: true,
690
+ message: `${workbench.totals.now} now, ${workbench.totals.next} next, ${workbench.totals.blocked} blocked, ${workbench.totals.review} review; ${workbench.alertTotal} alert(s).`,
691
+ workbench
692
+ };
693
+ }
694
+ case "add_dependency": {
695
+ if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
696
+ return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
697
+ }
698
+ const board = await addDependency(
699
+ projectRoot,
700
+ input.boardId,
701
+ input.taskId,
702
+ input.dependencyTaskId,
703
+ eventContext
704
+ );
705
+ return board ? okBoard(board, "Dependency added.") : fail("Task not found.");
706
+ }
707
+ case "add_goal_metric": {
708
+ if (!input.boardId || !input.taskId || !input.metricName) {
709
+ return fail("add_goal_metric requires boardId, taskId, and metricName.");
710
+ }
711
+ const board = await addGoalMetricToTask(
712
+ projectRoot,
713
+ input.boardId,
714
+ input.taskId,
715
+ {
716
+ name: input.metricName,
717
+ ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
718
+ ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
719
+ ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
720
+ ...input.metricDirection !== void 0 ? { direction: input.metricDirection } : {},
721
+ ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
722
+ ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
723
+ },
724
+ eventContext
725
+ );
726
+ return board ? okBoard(board, "Goal metric added.") : fail("Task not found.");
727
+ }
728
+ case "update_goal_metric": {
729
+ if (!input.boardId || !input.taskId || !input.metricId) {
730
+ return fail("update_goal_metric requires boardId, taskId, and metricId.");
731
+ }
732
+ const board = await updateGoalMetricOnTask(
733
+ projectRoot,
734
+ input.boardId,
735
+ input.taskId,
736
+ input.metricId,
737
+ {
738
+ ...input.metricName !== void 0 ? { name: input.metricName } : {},
739
+ ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
740
+ ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
741
+ ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
742
+ ...input.metricDirection !== void 0 ? { direction: input.metricDirection } : {},
743
+ ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
744
+ ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
745
+ },
746
+ eventContext
747
+ );
748
+ return board ? okBoard(board, "Goal metric updated.") : fail("Metric not found.");
749
+ }
750
+ case "add_check": {
751
+ if (!input.boardId || !input.taskId || !input.checkDescription) {
752
+ return fail("add_check requires boardId, taskId, and checkDescription.");
753
+ }
754
+ const board = await addCheckToTask(
755
+ projectRoot,
756
+ input.boardId,
757
+ input.taskId,
758
+ {
759
+ description: input.checkDescription,
760
+ type: input.checkType ?? "manual",
761
+ status: input.checkStatus,
762
+ ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
763
+ },
764
+ eventContext
765
+ );
766
+ return board ? okBoard(board, "Check added.") : fail("Task not found.");
767
+ }
768
+ case "update_check": {
769
+ if (!input.boardId || !input.taskId || !input.checkId) {
770
+ return fail("update_check requires boardId, taskId, and checkId.");
771
+ }
772
+ const board = await updateCheckOnTask(
773
+ projectRoot,
774
+ input.boardId,
775
+ input.taskId,
776
+ input.checkId,
777
+ {
778
+ ...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
779
+ ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {},
780
+ // Promoting an existing manual criterion to an executable one is the
781
+ // common repair: the card was written before anyone knew the command.
782
+ ...input.checkType !== void 0 ? { type: input.checkType } : {},
783
+ ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
784
+ },
785
+ eventContext
786
+ );
787
+ return board ? okBoard(board, "Check updated.") : fail("Check not found.");
788
+ }
789
+ case "remove_check": {
790
+ if (!input.boardId || !input.taskId || !input.checkId) {
791
+ return fail("remove_check requires boardId, taskId, and checkId.");
792
+ }
793
+ const board = await removeCheckFromTask(
794
+ projectRoot,
795
+ input.boardId,
796
+ input.taskId,
797
+ input.checkId,
798
+ eventContext
799
+ );
800
+ return board ? okBoard(board, "Acceptance criterion removed.") : fail("Check not found on this task.");
801
+ }
802
+ case "add_note": {
803
+ if (!input.boardId || !input.taskId || !input.note)
804
+ return fail("add_note requires boardId, taskId, and note.");
805
+ const board = await addNoteToTask(
806
+ projectRoot,
807
+ input.boardId,
808
+ input.taskId,
809
+ {
810
+ author: input.author ?? "agent",
811
+ content: input.note
812
+ },
813
+ eventContext
814
+ );
815
+ return board ? okBoard(board, "Note added.") : fail("Task not found.");
816
+ }
817
+ case "add_link": {
818
+ if (!input.boardId || !input.taskId || !input.url)
819
+ return fail("add_link requires boardId, taskId, and url.");
820
+ const board = await addLinkToTask(
821
+ projectRoot,
822
+ input.boardId,
823
+ input.taskId,
824
+ {
825
+ url: input.url,
826
+ type: input.linkType ?? "url",
827
+ ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
828
+ },
829
+ eventContext
830
+ );
831
+ return board ? okBoard(board, "Link added.") : fail("Task not found.");
832
+ }
833
+ case "split_atomic": {
834
+ if (!input.boardId || !input.taskId || !input.childTitles?.length) {
835
+ return fail("split_atomic requires boardId, taskId, and childTitles (at least one).");
836
+ }
837
+ return handleSplitTask(projectRoot, input, { atomic: true }, eventContext);
838
+ }
839
+ default:
840
+ return void 0;
841
+ }
842
+ }
843
+
844
+ // src/kanban-lifecycle-actions.ts
845
+ import { randomUUID as randomUUID2 } from "node:crypto";
846
+ import {
847
+ addTask as addTask2,
848
+ assignTask,
849
+ claimReadyTask,
850
+ copyTaskToBoard,
851
+ evaluateContractGraphReadiness,
852
+ finalizeTaskCompletion,
853
+ getBoard as getBoard3,
854
+ getTask,
855
+ getTaskChain,
856
+ heartbeatTaskAssignment,
857
+ mergeTasks,
858
+ moveTask,
859
+ recoverStaleTaskAssignments,
860
+ releaseTaskClaim,
861
+ removeTask,
862
+ repairManagedTaskProjection,
863
+ resolveAutoAccept,
864
+ setTaskChain,
865
+ stripLifecycleIssues,
866
+ transferTaskToBoard,
867
+ transitionTask,
868
+ updateTask as updateTask2,
869
+ updateTaskAssignment,
870
+ verifyTaskCompletion as verifyTaskCompletion2
871
+ } from "@wrongstack/kanban";
872
+
873
+ // src/kanban-task-inputs.ts
874
+ import { randomUUID } from "node:crypto";
875
+ import { clampSubagentCapabilities } from "@wrongstack/core/security";
876
+ function taskInput(input) {
877
+ const assignment = hasAssignmentInput(input) ? assignmentForTaskCreate(input) : void 0;
878
+ return {
879
+ title: input.title ?? "",
880
+ columnId: input.columnId,
881
+ description: input.description,
882
+ dueDate: input.dueDate,
883
+ priority: input.priority,
884
+ ...input.taskType !== void 0 ? { type: input.taskType } : {},
885
+ status: input.status,
886
+ labels: input.labels,
887
+ ...assignment?.agentId ?? assignment?.role ?? assignment?.name ? { assignedAgent: assignment.agentId ?? assignment.role ?? assignment.name } : {},
888
+ ...input.assignee ?? assignment?.name ?? assignment?.agentId ? { assignee: input.assignee ?? assignment?.name ?? assignment?.agentId } : {},
889
+ ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
890
+ ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
891
+ ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {},
892
+ ...assignment ? { assignment } : {},
893
+ ...input.order !== void 0 ? { order: input.order } : {},
894
+ ...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
895
+ ...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
896
+ // The system prompt has always told the model it may "set atomic: true"
897
+ // when creating a composite parent. It could not: the field reached
898
+ // neither the create input nor the patch, so the instruction described a
899
+ // capability that did not exist and the attempt was silently dropped.
900
+ ...input.atomic !== void 0 ? { atomic: input.atomic } : {},
901
+ ...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
902
+ ...input.checkDescription !== void 0 ? {
903
+ successCriteria: [
904
+ {
905
+ id: randomUUID(),
906
+ description: input.checkDescription,
907
+ // `manual` only as the fallback. Hard-coding it here meant every
908
+ // agent-authored criterion was unverifiable by construction: the
909
+ // deterministic plugins never matched, the registry passed the
910
+ // hand-set status straight through, and "verified" collapsed into
911
+ // "the author ticked its own box".
912
+ type: input.checkType ?? "manual",
913
+ status: input.checkStatus ?? "pending",
914
+ ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
915
+ }
916
+ ]
917
+ } : {},
918
+ ...input.metricName !== void 0 ? {
919
+ goalMetrics: [
920
+ {
921
+ id: randomUUID(),
922
+ name: input.metricName,
923
+ status: input.metricStatus ?? "pending",
924
+ ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
925
+ ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
926
+ ...input.metricDirection !== void 0 ? { direction: input.metricDirection } : {},
927
+ ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
928
+ ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
929
+ }
930
+ ]
931
+ } : {},
932
+ ...input.url !== void 0 ? {
933
+ links: [
934
+ {
935
+ url: input.url,
936
+ type: input.linkType ?? "url",
937
+ ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
938
+ }
939
+ ]
940
+ } : {},
941
+ ...input.note !== void 0 ? {
942
+ notes: [
943
+ {
944
+ id: randomUUID(),
945
+ author: input.author ?? "agent",
946
+ content: input.note,
947
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
948
+ }
949
+ ]
950
+ } : {},
951
+ ...[input.graphId, input.specId, input.specRequirementId].some((value) => value !== void 0) ? {
952
+ origin: {
953
+ system: input.sourceSystem ?? "kanban-tool",
954
+ ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
955
+ ...input.specId !== void 0 ? { specId: input.specId } : {},
956
+ ...input.specRequirementId !== void 0 ? { specRequirementId: input.specRequirementId } : {},
957
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {}
958
+ }
959
+ } : {}
960
+ };
961
+ }
962
+ function mergedDependsOn(input) {
963
+ if (input.dependsOn === void 0 && input.dependencyTaskId === void 0) return void 0;
964
+ return [
965
+ ...input.dependsOn ?? [],
966
+ ...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
967
+ ].filter((id, i, arr) => id && arr.indexOf(id) === i);
968
+ }
969
+ function taskPatch(input) {
970
+ return {
971
+ title: input.title,
972
+ description: input.description,
973
+ dueDate: input.dueDate,
974
+ columnId: input.columnId,
975
+ order: input.order,
976
+ priority: input.priority,
977
+ ...input.taskType !== void 0 ? { type: input.taskType } : {},
978
+ status: input.status,
979
+ labels: input.labels,
980
+ assignedAgent: input.agentId,
981
+ ...mergedDependsOn(input) !== void 0 ? { dependsOn: mergedDependsOn(input) } : {},
982
+ // `atomic` and `childTaskIds` are the composite-parent contract, and the
983
+ // managed gate reads both: an `atomic` parent may not move forward without
984
+ // children, and may not reach Done until every child is completed. The
985
+ // manager has always accepted both on a patch; only this surface withheld
986
+ // them, so `split_atomic` was a one-way door — delete the children and the
987
+ // parent was stranded with no way to declare itself a leaf again.
988
+ ...input.atomic !== void 0 ? { atomic: input.atomic } : {},
989
+ ...input.childTaskIds !== void 0 ? { childTaskIds: input.childTaskIds } : {},
990
+ ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
991
+ ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
992
+ };
993
+ }
994
+ function clampRequestedCapabilities(requested) {
995
+ if (requested === void 0) return void 0;
996
+ return clampSubagentCapabilities(requested).granted;
997
+ }
998
+ function assignmentInput(input) {
999
+ return {
1000
+ agentId: input.agentId,
1001
+ name: input.name,
1002
+ role: input.role,
1003
+ provider: input.provider,
1004
+ model: input.model,
1005
+ fallbackProfile: input.fallbackProfile,
1006
+ fallbackModels: input.fallbackModels,
1007
+ tools: input.tools,
1008
+ allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities),
1009
+ assignee: input.assignee,
1010
+ leaseId: input.leaseId,
1011
+ claimedAt: input.claimedAt,
1012
+ heartbeatAt: input.heartbeatAt,
1013
+ leaseExpiresAt: input.leaseExpiresAt,
1014
+ attempt: input.attempt,
1015
+ maxAttempts: input.maxAttempts,
1016
+ costCeilingUsd: input.costCeilingUsd,
1017
+ retryPolicy: input.retryPolicy,
1018
+ lastFailureKind: input.lastFailureKind
1019
+ };
1020
+ }
1021
+ function hasAssignmentInput(input) {
1022
+ return input.agentId !== void 0 || input.name !== void 0 || input.role !== void 0 || input.provider !== void 0 || input.model !== void 0 || input.fallbackProfile !== void 0 || input.fallbackModels !== void 0 || input.tools !== void 0 || input.allowedCapabilities !== void 0 || input.assignee !== void 0 || input.leaseId !== void 0 || input.claimedAt !== void 0 || input.heartbeatAt !== void 0 || input.leaseExpiresAt !== void 0 || input.attempt !== void 0 || input.maxAttempts !== void 0 || input.costCeilingUsd !== void 0 || input.retryPolicy !== void 0 || input.lastFailureKind !== void 0 || input.assignmentStatus !== void 0;
1023
+ }
1024
+ function assignmentForTaskCreate(input) {
1025
+ return {
1026
+ status: input.assignmentStatus ?? "assigned",
1027
+ ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
1028
+ ...input.name !== void 0 ? { name: input.name } : {},
1029
+ ...input.role !== void 0 ? { role: input.role } : {},
1030
+ ...input.provider !== void 0 ? { provider: input.provider } : {},
1031
+ ...input.model !== void 0 ? { model: input.model } : {},
1032
+ ...input.fallbackProfile !== void 0 ? { fallbackProfile: input.fallbackProfile } : {},
1033
+ ...input.fallbackModels !== void 0 ? { fallbackModels: input.fallbackModels } : {},
1034
+ ...input.tools !== void 0 ? { tools: input.tools } : {},
1035
+ ...input.allowedCapabilities !== void 0 ? { allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities) } : {},
1036
+ ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
1037
+ ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
1038
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
1039
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
1040
+ ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
1041
+ ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {},
1042
+ ...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
1043
+ ...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
1044
+ ...input.lastFailureKind !== void 0 ? { lastFailureKind: input.lastFailureKind } : {}
1045
+ };
1046
+ }
1047
+
1048
+ // src/kanban-lifecycle-actions.ts
1049
+ async function syncContextTask(ctx, task, options = {}) {
1050
+ if (!ctx?.state || !task) return;
1051
+ try {
1052
+ await applySessionKanbanTaskToSource(ctx, task, options);
1053
+ } catch {
1054
+ }
1055
+ }
1056
+ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1057
+ const eventContext = {
1058
+ sessionId: ctx.eventSessionId(),
1059
+ ...ctx.agentId !== void 0 ? { actor: ctx.agentId } : {}
1060
+ };
1061
+ switch (input.action) {
1062
+ case "add_task": {
1063
+ if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
1064
+ const result = await addTask2(projectRoot, input.boardId, taskInput(input), eventContext);
1065
+ if (!result) return fail("Board not found.");
1066
+ await syncContextTask(ctx, result.task);
1067
+ return okTask(result.board, result.task, `Task added.${atomicityNudge(result.task)}`);
1068
+ }
1069
+ case "split_task": {
1070
+ if (!input.boardId || !input.taskId || !input.childTitles?.length) {
1071
+ return fail("split_task requires boardId, taskId, and childTitles.");
1072
+ }
1073
+ return handleSplitTask(projectRoot, input, {}, eventContext);
1074
+ }
1075
+ case "merge_tasks": {
1076
+ if (!input.boardId || !input.taskIds?.length || !input.title) {
1077
+ return fail("merge_tasks requires boardId, taskIds, and title.");
1078
+ }
1079
+ const result = await mergeTasks(
1080
+ projectRoot,
1081
+ input.boardId,
1082
+ {
1083
+ taskIds: input.taskIds,
1084
+ title: input.title,
1085
+ ...input.description !== void 0 ? { description: input.description } : {},
1086
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
1087
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
1088
+ ...input.closeSourceTasks !== void 0 ? { closeSourceTasks: input.closeSourceTasks } : {}
1089
+ },
1090
+ eventContext
1091
+ );
1092
+ return result ? okTask(result.board, result.task, "Tasks merged.") : fail("Board or task not found.");
1093
+ }
1094
+ case "copy_task": {
1095
+ if (!input.boardId || !input.taskId || !input.targetBoardId) {
1096
+ return fail("copy_task requires boardId, taskId, and targetBoardId.");
1097
+ }
1098
+ const result = await copyTaskToBoard(
1099
+ projectRoot,
1100
+ input.boardId,
1101
+ input.taskId,
1102
+ input.targetBoardId,
1103
+ {
1104
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
1105
+ ...input.order !== void 0 ? { targetOrder: input.order } : {},
1106
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
1107
+ ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {},
1108
+ eventContext
1109
+ }
1110
+ );
1111
+ return result ? okTask(result.targetBoard, result.task, "Task copied to target board.") : fail("Board or task not found.");
1112
+ }
1113
+ case "transfer_task": {
1114
+ if (!input.boardId || !input.taskId || !input.targetBoardId) {
1115
+ return fail("transfer_task requires boardId, taskId, and targetBoardId.");
1116
+ }
1117
+ const result = await transferTaskToBoard(
1118
+ projectRoot,
1119
+ input.boardId,
1120
+ input.taskId,
1121
+ input.targetBoardId,
1122
+ {
1123
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
1124
+ ...input.order !== void 0 ? { targetOrder: input.order } : {},
1125
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
1126
+ ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {},
1127
+ eventContext
1128
+ }
1129
+ );
1130
+ return result ? okTask(result.targetBoard, result.task, "Task transferred to target board.") : fail("Board or task not found.");
1131
+ }
1132
+ case "get_task": {
1133
+ if (!input.boardId || !input.taskId) return fail("get_task requires boardId and taskId.");
1134
+ const task = await getTask(projectRoot, input.boardId, input.taskId);
1135
+ return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
1136
+ }
1137
+ case "start_task": {
1138
+ if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
1139
+ return fail("start_task requires boardId, taskId, author, and transitionComment.");
1140
+ }
1141
+ 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.");
1144
+ const readiness = evaluateContractGraphReadiness(board, task.id);
1145
+ if (!readiness.ready) {
1146
+ return fail(
1147
+ `Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
1148
+ );
1149
+ }
1150
+ if (board.lifecycle?.mode !== "managed") {
1151
+ const now = /* @__PURE__ */ new Date();
1152
+ const assigned = await updateTaskAssignment(
1153
+ projectRoot,
1154
+ board.id,
1155
+ task.id,
1156
+ {
1157
+ status: "running",
1158
+ agentId: input.agentId ?? input.author,
1159
+ leaseId: input.leaseId ?? randomUUID2(),
1160
+ claimedAt: input.claimedAt ?? now.toISOString(),
1161
+ heartbeatAt: input.heartbeatAt ?? now.toISOString(),
1162
+ leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
1163
+ attempt: input.attempt ?? 1,
1164
+ maxAttempts: input.maxAttempts ?? 3
1165
+ },
1166
+ eventContext
1167
+ );
1168
+ if (!assigned) return fail("Task assignment could not be started.");
1169
+ const started = await updateTask2(
1170
+ projectRoot,
1171
+ board.id,
1172
+ task.id,
1173
+ { status: "in_progress" },
1174
+ eventContext
1175
+ );
1176
+ const current = started ?? assigned;
1177
+ const claimed = task;
1178
+ const currentTask = current.tasks.find((candidate) => candidate.id === claimed.id) ?? claimed;
1179
+ ctx.setCurrentKanbanTask?.(currentTask.id, current.id);
1180
+ return okTask(
1181
+ current,
1182
+ currentTask,
1183
+ "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."
1184
+ );
1185
+ }
1186
+ let stage = task.lifecycle?.currentStage;
1187
+ if (stage === "backlog") {
1188
+ const moved = await transitionTask(projectRoot, board.id, task.id, {
1189
+ to: "todo",
1190
+ sessionId: eventContext.sessionId,
1191
+ actor: input.author,
1192
+ comment: input.transitionComment
1193
+ });
1194
+ if (!moved) return fail("Task could not enter Todo.");
1195
+ board = moved.board;
1196
+ task = moved.task;
1197
+ stage = task.lifecycle?.currentStage;
1198
+ }
1199
+ if (stage === "todo" || stage === "review") {
1200
+ const now = /* @__PURE__ */ new Date();
1201
+ const leaseId = input.leaseId ?? randomUUID2();
1202
+ const assigned = await updateTaskAssignment(
1203
+ projectRoot,
1204
+ board.id,
1205
+ task.id,
1206
+ {
1207
+ status: "running",
1208
+ agentId: input.agentId ?? input.author,
1209
+ leaseId,
1210
+ claimedAt: input.claimedAt ?? now.toISOString(),
1211
+ heartbeatAt: input.heartbeatAt ?? now.toISOString(),
1212
+ leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
1213
+ attempt: input.attempt ?? 1,
1214
+ maxAttempts: input.maxAttempts ?? 3
1215
+ },
1216
+ eventContext
1217
+ );
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.");
1226
+ board = moved.board;
1227
+ task = moved.task;
1228
+ stage = task.lifecycle?.currentStage;
1229
+ }
1230
+ 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"}).`
1233
+ );
1234
+ }
1235
+ ctx.setCurrentKanbanTask(task.id, board.id);
1236
+ await syncContextTask(ctx, task);
1237
+ return okTask(
1238
+ board,
1239
+ task,
1240
+ "Task is active; runtime Kanban governance is now bound to this run."
1241
+ );
1242
+ }
1243
+ case "update_task": {
1244
+ if (!input.boardId || !input.taskId) return fail("update_task requires boardId and taskId.");
1245
+ const board = await updateTask2(
1246
+ projectRoot,
1247
+ input.boardId,
1248
+ input.taskId,
1249
+ taskPatch(input),
1250
+ eventContext
1251
+ );
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.");
1259
+ }
1260
+ case "transition_task": {
1261
+ if (!input.boardId || !input.taskId || !input.lifecycleStage || !input.author || !input.transitionComment) {
1262
+ return fail(
1263
+ "transition_task requires boardId, taskId, lifecycleStage, author, and transitionComment."
1264
+ );
1265
+ }
1266
+ if (input.lifecycleStage === "done") {
1267
+ const boardBefore = await getBoard3(projectRoot, input.boardId);
1268
+ const taskBefore = boardBefore ? await getTask(projectRoot, input.boardId, input.taskId) : null;
1269
+ if (boardBefore && taskBefore && !taskBefore.verificationReport && (taskBefore.atomic || Boolean(taskBefore.successCriteria?.length))) {
1270
+ const preGate = await verifyTaskCompletion2(projectRoot, input.boardId, taskBefore.id, {
1271
+ persist: false
1272
+ });
1273
+ await updateTask2(
1274
+ projectRoot,
1275
+ input.boardId,
1276
+ taskBefore.id,
1277
+ {
1278
+ verificationReport: preGate.report,
1279
+ successCriteria: preGate.task.successCriteria
1280
+ },
1281
+ eventContext
1282
+ );
1283
+ }
1284
+ }
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);
1303
+ }
1304
+ if (result?.task) {
1305
+ await syncContextTask(ctx, result.task);
1306
+ }
1307
+ return result ? okTask(result.board, result.task, `Task advanced to ${result.transition.to}.`) : fail("Board or task not found.");
1308
+ }
1309
+ case "repair_managed_projection": {
1310
+ if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
1311
+ return fail(
1312
+ "repair_managed_projection requires boardId, taskId, author, and transitionComment."
1313
+ );
1314
+ }
1315
+ const result = await repairManagedTaskProjection(projectRoot, input.boardId, input.taskId, {
1316
+ actor: input.author,
1317
+ comment: input.transitionComment
1318
+ });
1319
+ if (result?.task) {
1320
+ await syncContextTask(ctx, result.task);
1321
+ }
1322
+ return result ? okTask(
1323
+ result.board,
1324
+ result.task,
1325
+ "Managed card projection repaired from lifecycle history."
1326
+ ) : fail("Board or task not found.");
1327
+ }
1328
+ case "move_task": {
1329
+ if (!input.boardId || !input.taskId || !input.targetColumnId) {
1330
+ return fail("move_task requires boardId, taskId, and targetColumnId.");
1331
+ }
1332
+ const board = await moveTask(
1333
+ projectRoot,
1334
+ input.boardId,
1335
+ input.taskId,
1336
+ input.targetColumnId,
1337
+ input.order,
1338
+ eventContext
1339
+ );
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.");
1347
+ }
1348
+ case "delete_task": {
1349
+ if (!input.boardId || !input.taskId) return fail("delete_task requires boardId and taskId.");
1350
+ const boardBefore = await getBoard3(projectRoot, input.boardId);
1351
+ const taskToDelete = boardBefore?.tasks.find((t) => t.id === input.taskId);
1352
+ const board = await removeTask(projectRoot, input.boardId, input.taskId, eventContext);
1353
+ if (board && ctx.currentKanbanTaskId === input.taskId) {
1354
+ ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
1355
+ }
1356
+ if (board && taskToDelete) {
1357
+ await syncContextTask(ctx, taskToDelete, { remove: true });
1358
+ }
1359
+ return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
1360
+ }
1361
+ case "set_chain": {
1362
+ if (!input.boardId || !input.taskIds?.length) {
1363
+ return fail("set_chain requires boardId and taskIds.");
1364
+ }
1365
+ const result = await setTaskChain(
1366
+ projectRoot,
1367
+ input.boardId,
1368
+ {
1369
+ taskIds: input.taskIds,
1370
+ ...input.chainId !== void 0 ? { chainId: input.chainId } : {},
1371
+ ...input.enforceDependencies !== void 0 ? { enforceDependencies: input.enforceDependencies } : {}
1372
+ },
1373
+ eventContext
1374
+ );
1375
+ return result ? {
1376
+ ok: true,
1377
+ message: `Chain set: ${result.chainId}`,
1378
+ board: result.board,
1379
+ chain: result.tasks
1380
+ } : fail("Board or task not found.");
1381
+ }
1382
+ case "get_chain": {
1383
+ if (!input.boardId || !(input.taskId || input.chainId)) {
1384
+ return fail("get_chain requires boardId and taskId or chainId.");
1385
+ }
1386
+ const result = await getTaskChain(
1387
+ projectRoot,
1388
+ input.boardId,
1389
+ input.taskId ?? input.chainId ?? ""
1390
+ );
1391
+ return result ? {
1392
+ ok: true,
1393
+ message: `Chain loaded: ${result.chainId}`,
1394
+ board: result.board,
1395
+ chain: result.tasks
1396
+ } : fail("Chain not found.");
1397
+ }
1398
+ case "claim_task": {
1399
+ const result = await claimReadyTask(
1400
+ projectRoot,
1401
+ {
1402
+ ...input.boardId !== void 0 ? { boardId: input.boardId } : {},
1403
+ ...input.taskId !== void 0 ? { taskId: input.taskId } : {},
1404
+ ...assignmentInput(input),
1405
+ status: input.assignmentStatus ?? "queued"
1406
+ },
1407
+ eventContext
1408
+ );
1409
+ return result ? okTask(result.board, result.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
1410
+ }
1411
+ case "release_task": {
1412
+ if (!input.boardId || !input.taskId) {
1413
+ return fail("release_task requires boardId and taskId.");
1414
+ }
1415
+ const board = await releaseTaskClaim(
1416
+ projectRoot,
1417
+ input.boardId,
1418
+ input.taskId,
1419
+ {
1420
+ ...input.releaseStatus !== void 0 ? { status: input.releaseStatus } : {},
1421
+ ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
1422
+ ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {}
1423
+ },
1424
+ eventContext
1425
+ );
1426
+ return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
1427
+ }
1428
+ case "assign_task": {
1429
+ if (!input.boardId || !input.taskId) return fail("assign_task requires boardId and taskId.");
1430
+ const board = await assignTask(
1431
+ projectRoot,
1432
+ input.boardId,
1433
+ input.taskId,
1434
+ assignmentInput(input),
1435
+ eventContext
1436
+ );
1437
+ return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
1438
+ }
1439
+ case "mark_assignment": {
1440
+ if (!input.boardId || !input.taskId)
1441
+ return fail("mark_assignment requires boardId and taskId.");
1442
+ const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
1443
+ const board = await updateTaskAssignment(
1444
+ projectRoot,
1445
+ input.boardId,
1446
+ input.taskId,
1447
+ {
1448
+ ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
1449
+ ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
1450
+ ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
1451
+ ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
1452
+ ...input.error !== void 0 ? { error: input.error } : {},
1453
+ ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
1454
+ ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
1455
+ ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
1456
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
1457
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
1458
+ ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
1459
+ ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
1460
+ },
1461
+ {
1462
+ ...eventContext,
1463
+ ...input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
1464
+ }
1465
+ );
1466
+ if (!board) return fail("Task not found.");
1467
+ if (assignmentStatus === "completed" && board.lifecycle?.mode !== "managed") {
1468
+ const envGate = readEnvGateEnforcement();
1469
+ const finalized = await finalizeTaskCompletion(projectRoot, board.id, input.taskId, {
1470
+ ...board.completionGate === void 0 && envGate !== void 0 ? { enforcement: envGate } : {},
1471
+ eventContext
1472
+ });
1473
+ if (finalized) {
1474
+ if (finalized.gate.report) {
1475
+ recordKanbanVerificationEvidence(ctx, finalized.gate.report);
1476
+ }
1477
+ const gateSummary = {
1478
+ enforcement: finalized.gate.enforcement,
1479
+ allowed: finalized.gate.allowed,
1480
+ verdict: finalized.gate.verdict,
1481
+ issues: finalized.gate.issues.map((issue) => issue.message)
1482
+ };
1483
+ const gateMessage = finalized.gate.allowed ? `Completion gate ${finalized.gate.verdict === "skipped" ? "passed" : "passed"}; task completed.` : finalized.gate.enforcement === "strict" ? `Completion gate BLOCKED (verdict: ${finalized.gate.verdict}); task parked in review. Issues: ${gateSummary.issues.join(" | ")}` : `Completion gate failed softly (verdict: ${finalized.gate.verdict}); task completed with warnings. Issues: ${gateSummary.issues.join(" | ")}`;
1484
+ await syncContextTask(ctx, finalized.task);
1485
+ return {
1486
+ ...okTask(finalized.board, finalized.task, `Assignment updated. ${gateMessage}`),
1487
+ gate: gateSummary
1488
+ };
1489
+ }
1490
+ } else if (board.lifecycle?.mode === "managed") {
1491
+ const managedTask = board.tasks.find((candidate) => candidate.id === input.taskId);
1492
+ const stage = managedTask?.lifecycle?.currentStage;
1493
+ const actor = ctx.agentId ?? "kanban-agent";
1494
+ let transitionResult = null;
1495
+ const lifecycleWarnings = [];
1496
+ if (assignmentStatus === "running" && stage === "todo") {
1497
+ try {
1498
+ transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
1499
+ to: "running",
1500
+ sessionId: eventContext.sessionId,
1501
+ actor,
1502
+ comment: "Work started."
1503
+ });
1504
+ } catch (err) {
1505
+ lifecycleWarnings.push(
1506
+ `Lifecycle transition to Running deferred: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
1507
+ );
1508
+ }
1509
+ }
1510
+ if (assignmentStatus === "completed" && stage === "running") {
1511
+ const comment = typeof input.lastResult === "string" && input.lastResult.trim().length > 0 ? input.lastResult.trim().slice(0, 1e3) : "Work completed.";
1512
+ try {
1513
+ transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
1514
+ to: "review",
1515
+ sessionId: eventContext.sessionId,
1516
+ actor,
1517
+ comment,
1518
+ attachment: {
1519
+ url: `kanban://task/${input.taskId}/result`,
1520
+ title: "Worker completion result",
1521
+ type: "file"
1522
+ },
1523
+ patch: {
1524
+ ...input.agentId !== void 0 ? { assignedAgent: input.agentId } : {}
1525
+ }
1526
+ });
1527
+ } catch (err) {
1528
+ lifecycleWarnings.push(
1529
+ `Lifecycle transition to Review failed: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
1530
+ );
1531
+ }
1532
+ if (transitionResult) {
1533
+ const hasCriteria = (transitionResult.task.successCriteria?.length ?? 0) > 0 || transitionResult.task.atomic === true;
1534
+ if (hasCriteria) {
1535
+ try {
1536
+ const verResult = await verifyTaskCompletion2(projectRoot, board.id, input.taskId);
1537
+ if (verResult.report) {
1538
+ recordKanbanVerificationEvidence(ctx, verResult.report);
1539
+ }
1540
+ await updateTask2(
1541
+ projectRoot,
1542
+ board.id,
1543
+ input.taskId,
1544
+ {
1545
+ verificationReport: verResult.report,
1546
+ successCriteria: verResult.task.successCriteria
1547
+ },
1548
+ eventContext
1549
+ );
1550
+ const verdict = verResult.report.verdict;
1551
+ if (verdict === "passed" && !resolveAutoAccept(board)) {
1552
+ lifecycleWarnings.push(
1553
+ "Verification passed, but this board does not auto-accept. The card is in Review awaiting an explicit transition_task to done."
1554
+ );
1555
+ } else if (verdict === "passed") {
1556
+ try {
1557
+ const doneResult = await transitionTask(projectRoot, board.id, input.taskId, {
1558
+ to: "done",
1559
+ sessionId: eventContext.sessionId,
1560
+ actor,
1561
+ action: "Automated acceptance after verification",
1562
+ comment: "Auto-accepted: verification passed.",
1563
+ attachment: {
1564
+ url: `kanban://task/${input.taskId}/verification`,
1565
+ title: "Auto-verification result",
1566
+ type: "file"
1567
+ }
1568
+ });
1569
+ transitionResult = doneResult;
1570
+ } catch (acceptErr) {
1571
+ lifecycleWarnings.push(
1572
+ `Auto-accept to Done deferred: ${acceptErr instanceof Error ? acceptErr.message : String(acceptErr)}`
1573
+ );
1574
+ }
1575
+ } else {
1576
+ lifecycleWarnings.push(
1577
+ `Verification verdict: ${verdict} \u2014 card left in Review for manual acceptance.`
1578
+ );
1579
+ }
1580
+ } catch (verifyErr) {
1581
+ lifecycleWarnings.push(
1582
+ `Auto-verification error: ${verifyErr instanceof Error ? verifyErr.message : String(verifyErr)}`
1583
+ );
1584
+ }
1585
+ } else {
1586
+ lifecycleWarnings.push(
1587
+ "No automatic success criteria \u2014 card left in Review for manual verification."
1588
+ );
1589
+ }
1590
+ }
1591
+ }
1592
+ const responseBoard = transitionResult?.board ?? board;
1593
+ const responseTask = transitionResult?.task ?? managedTask;
1594
+ const msgParts = ["Assignment updated."];
1595
+ if (transitionResult) {
1596
+ msgParts.push(`Card advanced to ${transitionResult.transition.to}.`);
1597
+ }
1598
+ for (const w of lifecycleWarnings) msgParts.push(`Warning: ${w}`);
1599
+ await syncContextTask(ctx, responseTask);
1600
+ return okTask(responseBoard, responseTask, msgParts.join(" "));
1601
+ }
1602
+ return okBoard(board, "Assignment updated.");
1603
+ }
1604
+ case "heartbeat_assignment": {
1605
+ if (!input.boardId || !input.taskId) {
1606
+ return fail("heartbeat_assignment requires boardId and taskId.");
1607
+ }
1608
+ const board = await heartbeatTaskAssignment(
1609
+ projectRoot,
1610
+ input.boardId,
1611
+ input.taskId,
1612
+ {
1613
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
1614
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
1615
+ ...input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
1616
+ },
1617
+ eventContext
1618
+ );
1619
+ return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
1620
+ }
1621
+ case "recover_stale": {
1622
+ if (!input.boardId) return fail("recover_stale requires boardId.");
1623
+ const policyFields = [
1624
+ input.recoveryPolicyFailOnCostCeiling !== void 0,
1625
+ input.recoveryPolicyReleaseOnFailureKinds !== void 0,
1626
+ input.recoveryPolicyReleaseOnHeartbeatDue !== void 0,
1627
+ input.recoveryPolicyRetryPolicyOverride !== void 0
1628
+ ].some(Boolean);
1629
+ const result = await recoverStaleTaskAssignments(
1630
+ projectRoot,
1631
+ input.boardId,
1632
+ {
1633
+ ...input.recoveryMode !== void 0 ? { mode: input.recoveryMode } : {},
1634
+ ...input.recoveryNow !== void 0 ? { now: input.recoveryNow } : {},
1635
+ ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
1636
+ ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {},
1637
+ ...policyFields ? {
1638
+ policy: {
1639
+ ...input.recoveryPolicyFailOnCostCeiling !== void 0 ? { failWhenCostCeilingSet: input.recoveryPolicyFailOnCostCeiling } : {},
1640
+ ...input.recoveryPolicyReleaseOnFailureKinds !== void 0 ? {
1641
+ releaseOnFailureKinds: input.recoveryPolicyReleaseOnFailureKinds
1642
+ } : {},
1643
+ ...input.recoveryPolicyReleaseOnHeartbeatDue !== void 0 ? {
1644
+ releaseOnHeartbeatDue: input.recoveryPolicyReleaseOnHeartbeatDue
1645
+ } : {},
1646
+ ...input.recoveryPolicyRetryPolicyOverride !== void 0 ? {
1647
+ retryPolicyOverride: input.recoveryPolicyRetryPolicyOverride
1648
+ } : {}
1649
+ }
1650
+ } : {}
1651
+ },
1652
+ eventContext
1653
+ );
1654
+ return result ? {
1655
+ ok: true,
1656
+ message: `Recovered ${result.tasks.length} stale assignment(s).`,
1657
+ board: result.board,
1658
+ recoveredTasks: result.tasks
1659
+ } : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
1660
+ }
1661
+ default:
1662
+ return void 0;
1663
+ }
1664
+ }
1665
+
1666
+ // src/kanban-presence.ts
1667
+ import { touchKanbanPresence } from "@wrongstack/kanban";
1668
+ function createKanbanPresenceWrapper(projectRoot, input, ctx) {
1669
+ return async (result) => {
1670
+ const boardId = result.board?.id ?? input.boardId;
1671
+ if (!result.ok || !boardId || !ctx.session?.id || !ctx.agentId) return result;
1672
+ try {
1673
+ const board = await touchKanbanPresence(projectRoot, boardId, {
1674
+ sessionId: ctx.session.id,
1675
+ agentId: ctx.agentId,
1676
+ agentName: ctx.agentName,
1677
+ taskId: input.taskId ?? result.task?.id,
1678
+ runTaskId: input.runTaskId
1679
+ });
1680
+ return board ? { ...result, board } : result;
1681
+ } catch {
1682
+ return result;
1683
+ }
1684
+ };
1685
+ }
1686
+
1687
+ // src/kanban-serializer.ts
1688
+ var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
1689
+ var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
1690
+ "get_board",
1691
+ "export_markdown",
1692
+ "export_task_graph"
1693
+ ]);
1694
+ function serializeKanbanOutput(output, input) {
1695
+ const action = input && typeof input === "object" ? input.action : void 0;
1696
+ 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) {
1708
+ const columns = {};
1709
+ for (const column of board.columns) {
1710
+ columns[column.title || column.id] = board.tasks.filter(
1711
+ (task) => task.columnId === column.id
1712
+ ).length;
1713
+ }
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
+ }
1723
+ };
1724
+ return JSON.stringify(compact, null, 2);
1725
+ }
1726
+ }
1727
+ return JSON.stringify(output, null, 2);
1728
+ }
1729
+
1730
+ // src/kanban-tool-schema.ts
1731
+ 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.";
1732
+ 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. Goal metrics pass when current >= target by default; when lower is better (error rate, cost ceiling, latency, open-bug count), pass metricDirection "at_most" with add_goal_metric / update_goal_metric so verification compares current <= target instead.';
1733
+ var KANBAN_INPUT_SCHEMA = {
1734
+ type: "object",
1735
+ properties: {
1736
+ action: {
1737
+ type: "string",
1738
+ enum: [
1739
+ "list_boards",
1740
+ "get_board",
1741
+ "create_board",
1742
+ "duplicate_board",
1743
+ "update_board",
1744
+ "adopt_managed_lifecycle",
1745
+ "release_managed_lifecycle",
1746
+ "delete_board",
1747
+ "generate_board",
1748
+ "export_markdown",
1749
+ "export_task_graph",
1750
+ "sync_task_graph",
1751
+ "create_from_graph",
1752
+ "import_session_tasks",
1753
+ "search_tasks",
1754
+ "ready_tasks",
1755
+ "snapshot",
1756
+ "workbench",
1757
+ "add_task",
1758
+ "split_task",
1759
+ "merge_tasks",
1760
+ "copy_task",
1761
+ "transfer_task",
1762
+ "get_task",
1763
+ "start_task",
1764
+ "update_task",
1765
+ "transition_task",
1766
+ "repair_managed_projection",
1767
+ "move_task",
1768
+ "delete_task",
1769
+ "set_chain",
1770
+ "get_chain",
1771
+ "claim_task",
1772
+ "release_task",
1773
+ "assign_task",
1774
+ "mark_assignment",
1775
+ "heartbeat_assignment",
1776
+ "recover_stale",
1777
+ "events",
1778
+ "queue_health",
1779
+ "add_dependency",
1780
+ "add_goal_metric",
1781
+ "update_goal_metric",
1782
+ "add_check",
1783
+ "update_check",
1784
+ "remove_check",
1785
+ "add_note",
1786
+ "add_link",
1787
+ "verify_completion",
1788
+ "split_atomic",
1789
+ "assess_atomicity",
1790
+ "propose_decomposition",
1791
+ "get_contract_graph",
1792
+ "configure_contract_graph",
1793
+ "upsert_contract_node",
1794
+ "remove_contract_node",
1795
+ "add_contract_edge",
1796
+ "remove_contract_edge"
1797
+ ]
1798
+ },
1799
+ boardId: { type: "string" },
1800
+ taskId: { type: "string" },
1801
+ taskIds: { type: "array", items: { type: "string" } },
1802
+ 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
+ columnId: { type: "string" },
1808
+ targetBoardId: { type: "string" },
1809
+ targetColumnId: { type: "string" },
1810
+ title: { type: "string" },
1811
+ description: { type: "string" },
1812
+ dueDate: { type: "string" },
1813
+ tags: { type: "array", items: { type: "string" } },
1814
+ labels: { type: "array", items: { type: "string" } },
1815
+ priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
1816
+ taskType: {
1817
+ type: "string",
1818
+ enum: ["feature", "bugfix", "refactor", "docs", "test", "chore"]
1819
+ },
1820
+ status: {
1821
+ type: "string",
1822
+ enum: [
1823
+ "pending",
1824
+ "ready",
1825
+ "in_progress",
1826
+ "blocked",
1827
+ "review",
1828
+ "completed",
1829
+ "failed",
1830
+ "archived"
1831
+ ]
1832
+ },
1833
+ order: { type: "number" },
1834
+ query: { type: "string" },
1835
+ limit: { type: "number" },
1836
+ agentId: { type: "string" },
1837
+ name: { type: "string" },
1838
+ role: { type: "string" },
1839
+ provider: { type: "string" },
1840
+ model: { type: "string" },
1841
+ fallbackProfile: { type: "string" },
1842
+ fallbackModels: { type: "array", items: { type: "string" } },
1843
+ tools: { type: "array", items: { type: "string" } },
1844
+ allowedCapabilities: { type: "array", items: { type: "string" } },
1845
+ leaseId: { type: "string" },
1846
+ claimedAt: { type: "string" },
1847
+ heartbeatAt: { type: "string" },
1848
+ leaseExpiresAt: { type: "string" },
1849
+ attempt: { type: "number" },
1850
+ maxAttempts: { type: "number" },
1851
+ subagentId: { type: "string" },
1852
+ runTaskId: { type: "string" },
1853
+ lastResult: { type: "string" },
1854
+ error: { type: "string" },
1855
+ expectedLeaseId: { type: "string" },
1856
+ assignmentStatus: {
1857
+ type: "string",
1858
+ enum: ["assigned", "queued", "running", "completed", "failed", "cancelled"]
1859
+ },
1860
+ lifecycleStage: {
1861
+ type: "string",
1862
+ enum: ["backlog", "todo", "running", "review", "done"]
1863
+ },
1864
+ transitionAction: { type: "string" },
1865
+ transitionComment: { type: "string" },
1866
+ tickChecks: {
1867
+ type: "array",
1868
+ items: {
1869
+ type: "object",
1870
+ properties: {
1871
+ checkId: { type: "string" },
1872
+ checkStatus: { type: "string", enum: ["passed", "failed", "skipped"] }
1873
+ },
1874
+ required: ["checkId", "checkStatus"]
1875
+ },
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."
1877
+ },
1878
+ attachmentUrl: { type: "string" },
1879
+ attachmentTitle: { type: "string" },
1880
+ attachmentType: {
1881
+ type: "string",
1882
+ enum: ["issue", "pr", "doc", "commit", "design", "file", "url", "other"]
1883
+ },
1884
+ releaseStatus: { type: "string", enum: ["pending", "ready", "blocked"] },
1885
+ releaseReason: { type: "string" },
1886
+ clearAssignee: { type: "boolean" },
1887
+ recoveryMode: { type: "string", enum: ["auto", "release", "retry", "fail"] },
1888
+ recoveryNow: { type: "string" },
1889
+ recoveryPolicyFailOnCostCeiling: { type: "boolean" },
1890
+ recoveryPolicyReleaseOnFailureKinds: { type: "array", items: { type: "string" } },
1891
+ recoveryPolicyReleaseOnHeartbeatDue: { type: "boolean" },
1892
+ recoveryPolicyRetryPolicyOverride: {
1893
+ type: "string",
1894
+ enum: ["off", "incremental", "exponential"]
1895
+ },
1896
+ assignee: { type: "string" },
1897
+ costCeilingUsd: { type: "number" },
1898
+ retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
1899
+ lastFailureKind: { type: "string" },
1900
+ dependsOn: {
1901
+ type: "array",
1902
+ items: { type: "string" },
1903
+ 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."
1904
+ },
1905
+ atomic: {
1906
+ type: "boolean",
1907
+ description: "Composite parent (true) or executable leaf (false). Set false to make a stranded parent a leaf again after its children were dropped."
1908
+ },
1909
+ childTaskIds: {
1910
+ type: "array",
1911
+ items: { type: "string" },
1912
+ description: "Children of a composite parent. On update_task an explicit empty array detaches them all."
1913
+ },
1914
+ estimatedHours: { type: "number" },
1915
+ actualHours: { type: "number" },
1916
+ taskGraph: { type: "object" },
1917
+ graphId: { type: "string" },
1918
+ specId: { type: "string" },
1919
+ specRequirementId: { type: "string" },
1920
+ sourceSystem: { type: "string" },
1921
+ phaseId: { type: "string" },
1922
+ preserveOriginTaskIds: { type: "boolean" },
1923
+ includeArchived: { type: "boolean" },
1924
+ archiveMissingTasks: { type: "boolean" },
1925
+ preserveManualDependencies: { type: "boolean" },
1926
+ dependencyTaskId: { type: "string" },
1927
+ enforceDependencies: { type: "boolean" },
1928
+ childTitles: { type: "array", items: { type: "string" } },
1929
+ inheritAssignment: { type: "boolean" },
1930
+ inheritLabels: { type: "boolean" },
1931
+ inheritSuccessCriteria: { type: "boolean" },
1932
+ inheritGoalMetrics: { type: "boolean" },
1933
+ inheritDependencies: { type: "boolean" },
1934
+ chainChildren: { type: "boolean" },
1935
+ rewireDependents: { type: "boolean" },
1936
+ closeSourceTasks: { type: "boolean" },
1937
+ metricId: { type: "string" },
1938
+ metricName: { type: "string" },
1939
+ metricTarget: { oneOf: [{ type: "string" }, { type: "number" }] },
1940
+ metricCurrent: { oneOf: [{ type: "string" }, { type: "number" }] },
1941
+ metricDirection: { type: "string", enum: ["at_least", "at_most"] },
1942
+ metricUnit: { type: "string" },
1943
+ metricStatus: { type: "string", enum: ["pending", "met", "missed", "waived"] },
1944
+ metricNotes: { type: "string" },
1945
+ checkId: { type: "string" },
1946
+ checkDescription: { type: "string" },
1947
+ checkStatus: { type: "string", enum: ["pending", "passed", "failed", "skipped"] },
1948
+ checkType: {
1949
+ type: "string",
1950
+ // Only types a verifier can actually execute. `manual` is the default and
1951
+ // means a human or agent asserts the status by hand. The rest are run by
1952
+ // `verify_completion` against the default deterministic registry. Types
1953
+ // with no plugin in that registry (`auto`, `review`, `agent`, `council`)
1954
+ // are deliberately omitted: offering them would produce criteria that
1955
+ // silently report `skipped — no verifier plugin registered`.
1956
+ enum: ["manual", "command", "test", "file_exists", "file_matches", "git_diff", "metric"],
1957
+ 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.'
1958
+ },
1959
+ checkNotes: {
1960
+ type: "string",
1961
+ 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"}.'
1962
+ },
1963
+ // ── Contract map ───────────────────────────────────────────────────
1964
+ // The card contract: what this work targets, what it must not break, what
1965
+ // it risks, and what verifies it. Advisory by default — the readiness gate
1966
+ // deliberately does not require map structure, so a map is an operator
1967
+ // review aid, not work the model must complete before implementing.
1968
+ contractEnforcement: {
1969
+ type: "string",
1970
+ enum: ["off", "advisory", "strict"],
1971
+ description: "Board-level contract map enforcement. Default when first configured: advisory."
1972
+ },
1973
+ contractNodeId: { type: "string" },
1974
+ contractNodeKind: {
1975
+ type: "string",
1976
+ enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"],
1977
+ 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."
1978
+ },
1979
+ contractNodeTitle: { type: "string" },
1980
+ contractNodeDescription: { type: "string" },
1981
+ contractNodeState: {
1982
+ type: "string",
1983
+ enum: ["unknown", "active", "satisfied", "violated", "waived", "resolved"]
1984
+ },
1985
+ contractNodeEnforcement: {
1986
+ type: "string",
1987
+ enum: ["blocking", "advisory", "informational"]
1988
+ },
1989
+ /** Bind a node to an acceptance criterion or goal metric already on the task. */
1990
+ contractCheckId: { type: "string" },
1991
+ contractMetricId: { type: "string" },
1992
+ contractWaiverReason: {
1993
+ type: "string",
1994
+ description: 'Required, with an actor, when contractNodeState is "waived".'
1995
+ },
1996
+ contractEdgeId: { type: "string" },
1997
+ contractEdgeFrom: {
1998
+ type: "string",
1999
+ description: 'A contract node id, or a task id (bare or "task:<id>") for the card endpoint.'
2000
+ },
2001
+ contractEdgeTo: { type: "string" },
2002
+ contractEdgeType: {
2003
+ type: "string",
2004
+ enum: [
2005
+ "targets",
2006
+ "affects",
2007
+ "must_preserve",
2008
+ "exposes",
2009
+ "verified_by",
2010
+ "conflicts_with",
2011
+ "derived_from",
2012
+ "relates_to"
2013
+ ]
2014
+ },
2015
+ contractEdgeRationale: { type: "string" },
2016
+ note: { type: "string" },
2017
+ author: { type: "string" },
2018
+ url: { type: "string" },
2019
+ linkTitle: { type: "string" },
2020
+ linkType: {
2021
+ type: "string",
2022
+ enum: ["issue", "pr", "doc", "commit", "design", "file", "url", "other"]
2023
+ },
2024
+ context: { type: "string" },
2025
+ columns: { type: "array", items: { type: "string" } },
2026
+ generatedBy: { type: "string" },
2027
+ includeTasks: { type: "boolean" },
2028
+ includeCompletedTasks: { type: "boolean" },
2029
+ preserveAssignment: { type: "boolean" },
2030
+ preserveDependencies: { type: "boolean" },
2031
+ moveTasksToColumnId: { type: "string" },
2032
+ atomicityMode: { type: "string", enum: ["off", "assess", "enforce"] },
2033
+ atomicityDecomposition: { type: "string", enum: ["auto", "propose"] },
2034
+ gateEnforcement: {
2035
+ type: "string",
2036
+ // WS-023: `'off'` is deliberately absent. The agent whose work this gate
2037
+ // checks must not be able to switch it off; it may only tighten. Turning
2038
+ // a gate off stays a human decision, made through board config.
2039
+ enum: ["strict", "soft"]
2040
+ },
2041
+ subtasks: {
2042
+ type: "array",
2043
+ minItems: 2,
2044
+ items: {
2045
+ type: "object",
2046
+ properties: {
2047
+ title: { type: "string" },
2048
+ description: { type: "string" },
2049
+ successCriteria: { type: "array", items: { type: "string" } },
2050
+ dependsOnIndex: { type: "array", items: { type: "number" } }
2051
+ },
2052
+ required: ["title"]
2053
+ }
2054
+ }
2055
+ },
2056
+ required: ["action"]
2057
+ };
2058
+
2059
+ // src/kanban.ts
2060
+ var kanbanTool = {
2061
+ name: "kanban",
2062
+ category: "Project",
2063
+ description: KANBAN_TOOL_DESCRIPTION,
2064
+ usageHint: KANBAN_TOOL_USAGE_HINT,
2065
+ permission: "confirm",
2066
+ subjectKey: "action",
2067
+ mutating: true,
2068
+ capabilities: ["fs.write"],
2069
+ icon: "task",
2070
+ timeoutMs: 3e4,
2071
+ inputSchema: KANBAN_INPUT_SCHEMA,
2072
+ async execute(input, ctx) {
2073
+ const projectRoot = ctx.projectRoot;
2074
+ if (!projectRoot) return fail("No project root is available.");
2075
+ const withPresence = createKanbanPresenceWrapper(projectRoot, input, ctx);
2076
+ try {
2077
+ const result = await (async () => {
2078
+ const decompositionResult = await handleKanbanDecompositionAction(projectRoot, input, ctx);
2079
+ if (decompositionResult !== void 0) return decompositionResult;
2080
+ const boardResult = await handleKanbanBoardAction(projectRoot, input, ctx);
2081
+ if (boardResult !== void 0) return boardResult;
2082
+ const lifecycleResult = await handleKanbanLifecycleAction(projectRoot, input, ctx);
2083
+ if (lifecycleResult !== void 0) return lifecycleResult;
2084
+ const contractResult = await handleKanbanContractAction(
2085
+ projectRoot,
2086
+ input,
2087
+ input.author ?? input.agentId,
2088
+ ctx.eventSessionId()
2089
+ );
2090
+ if (contractResult !== void 0) return contractResult;
2091
+ const detailResult = await handleKanbanDetailAction(projectRoot, input, ctx);
2092
+ if (detailResult !== void 0) return detailResult;
2093
+ return fail(`Unknown kanban action: ${input.action}`);
2094
+ })();
2095
+ return withPresence(result);
2096
+ } catch (err) {
2097
+ return fail(stripLifecycleIssues2(err instanceof Error ? err.message : String(err)));
2098
+ }
2099
+ },
2100
+ serialize(output, input) {
2101
+ return serializeKanbanOutput(output, input);
2102
+ }
2103
+ };
2104
+
2105
+ export {
2106
+ kanbanEvidenceKey,
2107
+ kanbanEvidencePointer,
2108
+ recordKanbanVerificationEvidence,
2109
+ kanbanTool
2110
+ };
2111
+ //# sourceMappingURL=chunk-JNNVRHMV.js.map