@wrongstack/tools 0.306.4 → 0.307.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/audit.js +14 -8
  2. package/dist/auto-proceed-loop-guard.js +8 -2
  3. package/dist/bash.js +57 -22
  4. package/dist/builtin.d.ts +6 -6
  5. package/dist/builtin.js +11302 -9305
  6. package/dist/clarify.d.ts +32 -0
  7. package/dist/codebase-index/ast-invariant-engine.d.ts +102 -0
  8. package/dist/codebase-index/ast-symbol-mutator.d.ts +28 -0
  9. package/dist/codebase-index/background-indexer.d.ts +9 -0
  10. package/dist/codebase-index/codebase-ast-replace-tool.d.ts +31 -0
  11. package/dist/codebase-index/codebase-impact-analysis-tool.d.ts +40 -0
  12. package/dist/codebase-index/codebase-invariant-check-tool.d.ts +23 -0
  13. package/dist/codebase-index/codebase-repo-map-tool.d.ts +22 -0
  14. package/dist/codebase-index/codebase-skeleton-tool.d.ts +36 -0
  15. package/dist/codebase-index/codebase-targeted-test-tool.d.ts +30 -0
  16. package/dist/codebase-index/index.d.ts +11 -1
  17. package/dist/codebase-index/index.js +4141 -1479
  18. package/dist/codebase-index/project-server-client-state.d.ts +79 -0
  19. package/dist/codebase-index/project-server-client.d.ts +3 -71
  20. package/dist/codebase-index/project-server-protocol.d.ts +1 -0
  21. package/dist/codebase-index/project-server.js +751 -909
  22. package/dist/codebase-index/repo-map.d.ts +20 -0
  23. package/dist/codebase-index/skeleton-extractor.d.ts +64 -0
  24. package/dist/codebase-index/tree-sitter-parser.d.ts +11 -0
  25. package/dist/codebase-index/worker.js +723 -891
  26. package/dist/codebase-index/writer-mutations.d.ts +26 -0
  27. package/dist/codebase-index/writer-refs.d.ts +50 -0
  28. package/dist/codebase-index/writer-search.d.ts +30 -0
  29. package/dist/codebase-index/writer.d.ts +2 -320
  30. package/dist/edit.js +8558 -349
  31. package/dist/exec.js +73 -20
  32. package/dist/fetch.js +11 -9
  33. package/dist/format.js +14 -8
  34. package/dist/glob.js +5 -1
  35. package/dist/grep.js +19 -8
  36. package/dist/index.d.ts +3 -1
  37. package/dist/index.js +11386 -9416
  38. package/dist/install.js +14 -8
  39. package/dist/json.js +88 -15
  40. package/dist/kanban-board-actions.d.ts +4 -0
  41. package/dist/kanban-lifecycle-actions.d.ts +4 -0
  42. package/dist/kanban-serializer.d.ts +21 -0
  43. package/dist/kanban.js +985 -1034
  44. package/dist/languages/index.js +14 -8
  45. package/dist/lint.js +14 -8
  46. package/dist/logs.js +13 -3
  47. package/dist/next-steps.d.ts +8 -0
  48. package/dist/next-steps.js +18 -0
  49. package/dist/outdated.js +14 -8
  50. package/dist/pack.js +11295 -9305
  51. package/dist/patch.js +8301 -78
  52. package/dist/plan.js +1231 -1281
  53. package/dist/process-registry.js +14 -8
  54. package/dist/ps-slash.js +65 -30
  55. package/dist/read.js +751 -910
  56. package/dist/replace.js +8450 -224
  57. package/dist/search.js +21 -15
  58. package/dist/security-ast-scan-tool.d.ts +43 -0
  59. package/dist/session-kanban-graph.d.ts +9 -0
  60. package/dist/session-kanban-sync.d.ts +31 -0
  61. package/dist/session-kanban.d.ts +5 -141
  62. package/dist/session-kanban.js +367 -360
  63. package/dist/task.js +1157 -1207
  64. package/dist/test.js +14 -8
  65. package/dist/todo.js +2187 -2237
  66. package/dist/tool-diff.js +6 -1
  67. package/dist/tool-summary.js +4 -2
  68. package/dist/tool-tier.js +11302 -9305
  69. package/dist/typecheck.js +14 -8
  70. package/dist/write.js +8374 -153
  71. package/package.json +7 -7
package/dist/task.js CHANGED
@@ -4,22 +4,14 @@ import { addPlanItem, formatPlan, mutatePlan as mutatePlan2, mutateTasks as muta
4
4
  import { computeTaskItemProgress, formatTaskList } from "@wrongstack/core/utils";
5
5
 
6
6
  // src/session-kanban.ts
7
- import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
8
- import {
9
- loadPlan,
10
- loadTasks,
11
- mutatePlan,
12
- mutateTasks
13
- } from "@wrongstack/core/storage";
7
+ import { loadPlan, loadTasks } from "@wrongstack/core/storage";
14
8
  import { deserializeTaskGraph } from "@wrongstack/core/tasking";
15
- import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
16
9
  import {
17
10
  bridgeKanbanSupervisor,
18
11
  compactSessionMirrorBoard,
19
12
  createBoard,
20
13
  DEFAULT_COLUMNS,
21
14
  getBoard,
22
- getDependencyReadinessIssues,
23
15
  getKanbanOrchestrationSnapshot,
24
16
  listBoards,
25
17
  pruneSessionBoards,
@@ -28,6 +20,224 @@ import {
28
20
  touchKanbanPresence,
29
21
  updateBoard
30
22
  } from "@wrongstack/kanban";
23
+
24
+ // src/session-kanban-graph.ts
25
+ function todoListToSerializedGraph(todos, sessionId) {
26
+ const graphId = `todo:${sessionId}`;
27
+ const nodes = todos.map((todo, index) => ({
28
+ id: todo.id,
29
+ title: todo.content,
30
+ description: todo.activeForm ?? "",
31
+ type: "chore",
32
+ priority: "medium",
33
+ status: todo.status,
34
+ specRequirementId: `${graphId}:${todo.id}`,
35
+ createdAt: index,
36
+ updatedAt: index
37
+ }));
38
+ return {
39
+ id: graphId,
40
+ specId: graphId,
41
+ requiredRequirementIds: nodes.map((node) => node.specRequirementId),
42
+ title: "Session todos",
43
+ nodes,
44
+ edges: [],
45
+ rootNodes: nodes.map((node) => node.id),
46
+ createdAt: 0,
47
+ updatedAt: 0
48
+ };
49
+ }
50
+ function taskFileToSerializedGraph(tasks, sessionId) {
51
+ const graphId = `session:${sessionId}`;
52
+ const ids = new Set(tasks.map((task) => task.id));
53
+ const nodes = tasks.map((task, index) => ({
54
+ id: task.id,
55
+ title: task.title,
56
+ description: task.description ?? "",
57
+ type: task.type,
58
+ priority: task.priority,
59
+ status: task.status,
60
+ specRequirementId: `${graphId}:${task.id}`,
61
+ ...task.assignee ? { assignee: task.assignee } : {},
62
+ ...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
63
+ createdAt: index,
64
+ updatedAt: index
65
+ }));
66
+ const edges = tasks.flatMap(
67
+ (task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
68
+ id: `${dependency}->${task.id}`,
69
+ from: dependency,
70
+ to: task.id,
71
+ type: "depends_on"
72
+ }))
73
+ );
74
+ const hasIncoming = new Set(edges.map((edge) => edge.to));
75
+ const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
76
+ return {
77
+ // Keep the historical graph id so existing mirrored task cards are reused.
78
+ id: graphId,
79
+ specId: graphId,
80
+ requiredRequirementIds: nodes.map((node) => node.specRequirementId),
81
+ title: "Session tasks",
82
+ nodes,
83
+ edges,
84
+ rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
85
+ createdAt: 0,
86
+ updatedAt: 0
87
+ };
88
+ }
89
+
90
+ // src/session-kanban-sync.ts
91
+ import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
92
+ import { mutatePlan, mutateTasks } from "@wrongstack/core/storage";
93
+ import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
94
+ import {
95
+ getDependencyReadinessIssues
96
+ } from "@wrongstack/kanban";
97
+ function sourceStatus(task) {
98
+ if (task.status === "completed") return "completed";
99
+ if (task.status === "in_progress") return "in_progress";
100
+ if (task.status === "review") return "review";
101
+ if (task.status === "blocked") return "blocked";
102
+ if (task.status === "failed") return "failed";
103
+ return "pending";
104
+ }
105
+ function todoStatus(task) {
106
+ const status = sourceStatus(task);
107
+ if (status === "completed") return "completed";
108
+ if (status === "review" && task.assignment?.status === "completed") return "completed";
109
+ if (status === "in_progress" || status === "review") return "in_progress";
110
+ return "pending";
111
+ }
112
+ function sessionTodoFromTask(task, board) {
113
+ const blockedBy = board ? blockingTitles(board, task) : [];
114
+ return {
115
+ id: task.origin?.taskId ?? task.id,
116
+ content: task.title,
117
+ status: todoStatus(task),
118
+ ...task.description ? { activeForm: task.description } : {},
119
+ ...blockedBy.length ? { blockedBy } : {}
120
+ };
121
+ }
122
+ function managedTodoFromTask(task, board) {
123
+ return {
124
+ ...sessionTodoFromTask(task, board),
125
+ kanbanBoardId: board.id,
126
+ kanbanTaskId: task.id
127
+ };
128
+ }
129
+ function blockingTitles(board, task) {
130
+ return getDependencyReadinessIssues(board, task).map((issue) => {
131
+ const dependency = board.tasks.find((candidate) => candidate.id === issue.dependencyId);
132
+ if (!dependency) return `${issue.dependencyId} (missing)`;
133
+ return dependency.title;
134
+ });
135
+ }
136
+ var PRIORITY_ORDER = {
137
+ critical: 0,
138
+ high: 1,
139
+ medium: 2,
140
+ low: 3
141
+ };
142
+ function orderTasksForTodos(board, tasks) {
143
+ const columnOrder = new Map(board.columns.map((column) => [column.id, column.order]));
144
+ const baseline = [...tasks].sort(
145
+ (left, right) => (columnOrder.get(left.columnId) ?? 0) - (columnOrder.get(right.columnId) ?? 0) || (PRIORITY_ORDER[left.priority] ?? 2) - (PRIORITY_ORDER[right.priority] ?? 2) || left.order - right.order || left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id)
146
+ );
147
+ const included = new Set(baseline.map((task) => task.id));
148
+ const remaining = new Map(baseline.map((task) => [task.id, task]));
149
+ const emitted = [];
150
+ const done = /* @__PURE__ */ new Set();
151
+ while (remaining.size > 0) {
152
+ const ready = baseline.filter(
153
+ (task) => remaining.has(task.id) && (task.dependsOn ?? []).every(
154
+ (dependencyId) => !included.has(dependencyId) || done.has(dependencyId)
155
+ )
156
+ );
157
+ if (ready.length === 0) break;
158
+ for (const task of ready) {
159
+ remaining.delete(task.id);
160
+ done.add(task.id);
161
+ emitted.push(task);
162
+ }
163
+ }
164
+ for (const task of baseline) if (remaining.has(task.id)) emitted.push(task);
165
+ return emitted;
166
+ }
167
+ function sameTodos(left, right) {
168
+ return left.length === right.length && left.every((todo, index) => {
169
+ const candidate = right[index];
170
+ return candidate?.id === todo.id && candidate.content === todo.content && candidate.status === todo.status && candidate.activeForm === todo.activeForm && candidate.promotedFromPlan === todo.promotedFromPlan && candidate.promotedFromTask === todo.promotedFromTask && candidate.kanbanBoardId === todo.kanbanBoardId && candidate.kanbanTaskId === todo.kanbanTaskId && (candidate.blockedBy ?? []).join("\0") === (todo.blockedBy ?? []).join("\0");
171
+ });
172
+ }
173
+ function broadcastTodoUpdate(context, todos) {
174
+ const sessionId = context.session?.id ?? "";
175
+ if (!context.agentId || !sessionId) return;
176
+ const statusCounts = { pending: 0, inProgress: 0, completed: 0 };
177
+ for (const todo of todos) {
178
+ if (todo.status === "completed") statusCounts.completed++;
179
+ else if (todo.status === "in_progress") statusCounts.inProgress++;
180
+ else statusCounts.pending++;
181
+ }
182
+ const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;
183
+ const mailbox = getSharedProjectMailbox(projectDir);
184
+ void mailbox.send({
185
+ from: context.agentId,
186
+ to: "*",
187
+ type: "status",
188
+ subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
189
+ body: JSON.stringify({
190
+ kind: "kanban.todos.updated",
191
+ sessionId,
192
+ revision: context.state.revision,
193
+ todoCount: todos.length,
194
+ statusCounts
195
+ }),
196
+ priority: "normal",
197
+ senderSessionId: sessionId
198
+ }).catch(() => {
199
+ });
200
+ }
201
+ function notifyTodoUpdate(context, todos) {
202
+ const summary = formatTodosForModel(todos);
203
+ const text = `[KANBAN TODO UPDATE]
204
+ Another Kanban agent reassessed the shared board. The canonical todo list is now:
205
+ ${summary}
206
+ Reassess your current plan before continuing; do not rely on the initial todo snapshot. Preserve each row's <kanban board/task> binding verbatim on your next \`todo\` call \u2014 a row that loses it stops advancing its card.`;
207
+ const state = context.state;
208
+ if (typeof state.appendBlockToLastUserMessage === "function") {
209
+ if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
210
+ }
211
+ if (typeof state.appendMessage === "function") {
212
+ state.appendMessage({ role: "user", content: [{ type: "text", text }] });
213
+ }
214
+ }
215
+ function applyManagedKanbanBoardToTodos(context, board, suppressedTodoMirrors2) {
216
+ const metaKanban = context.meta["kanban"];
217
+ const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
218
+ const activeBoardId2 = context.currentKanbanBoardId ?? (typeof metaBoardId === "string" ? metaBoardId : void 0);
219
+ if (!activeBoardId2 || board.id !== activeBoardId2 || board.lifecycle?.mode !== "managed") {
220
+ return [...context.todos];
221
+ }
222
+ const projectedTodos = orderTasksForTodos(
223
+ board,
224
+ board.tasks.filter(
225
+ (task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
226
+ )
227
+ ).map((task) => managedTodoFromTask(task, board));
228
+ if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
229
+ suppressedTodoMirrors2.add(context);
230
+ try {
231
+ context.state.replaceTodos(projectedTodos);
232
+ } finally {
233
+ suppressedTodoMirrors2.delete(context);
234
+ }
235
+ notifyTodoUpdate(context, context.todos);
236
+ broadcastTodoUpdate(context, context.todos);
237
+ return [...context.todos];
238
+ }
239
+
240
+ // src/session-kanban.ts
31
241
  var SESSION_BOARD_TAG = "session-work";
32
242
  var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
33
243
  var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
@@ -151,15 +361,6 @@ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
151
361
  tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
152
362
  archiveMissingTasks: true,
153
363
  includeCompletedTasks: true,
154
- // The scope ledger stays declared and accurate, but it may not veto a
155
- // projection. A session mirror reflects a tactical list that shrinks by
156
- // design, and refusing the sync never protected the removed row — it
157
- // froze the entire board, permanently, because the stored scope then
158
- // outlived every later snapshot (`session-kanban.mirror-failed`).
159
- // Nothing is lost by shrinking here: `archiveMissingTasks` keeps the
160
- // removed card on the board as `archived`, the reconciliation pass
161
- // first walks vanished completed rows to Done, and the session journal
162
- // remains the durable record.
163
364
  allowRequirementScopeShrink: true
164
365
  }
165
366
  );
@@ -253,119 +454,13 @@ function takeSessionMirrorFailure(projectRoot, sessionId) {
253
454
  mirrorFailures.delete(key);
254
455
  return `Kanban mirror (${failure.sourceSystem}) failed and the board may be stale: ${failure.message}`;
255
456
  }
256
- function todoListToSerializedGraph(todos, sessionId) {
257
- const graphId = `todo:${sessionId}`;
258
- const nodes = todos.map((todo, index) => ({
259
- id: todo.id,
260
- title: todo.content,
261
- description: todo.activeForm ?? "",
262
- type: "chore",
263
- priority: "medium",
264
- status: todo.status,
265
- specRequirementId: `${graphId}:${todo.id}`,
266
- createdAt: index,
267
- updatedAt: index
268
- }));
269
- return {
270
- id: graphId,
271
- specId: graphId,
272
- requiredRequirementIds: nodes.map((node) => node.specRequirementId),
273
- title: "Session todos",
274
- nodes,
275
- edges: [],
276
- rootNodes: nodes.map((node) => node.id),
277
- createdAt: 0,
278
- updatedAt: 0
279
- };
280
- }
281
- function taskFileToSerializedGraph(tasks, sessionId) {
282
- const graphId = `session:${sessionId}`;
283
- const ids = new Set(tasks.map((task) => task.id));
284
- const nodes = tasks.map((task, index) => ({
285
- id: task.id,
286
- title: task.title,
287
- description: task.description ?? "",
288
- type: task.type,
289
- priority: task.priority,
290
- status: task.status,
291
- specRequirementId: `${graphId}:${task.id}`,
292
- ...task.assignee ? { assignee: task.assignee } : {},
293
- ...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
294
- createdAt: index,
295
- updatedAt: index
296
- }));
297
- const edges = tasks.flatMap(
298
- (task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
299
- id: `${dependency}->${task.id}`,
300
- from: dependency,
301
- to: task.id,
302
- type: "depends_on"
303
- }))
304
- );
305
- const hasIncoming = new Set(edges.map((edge) => edge.to));
306
- const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
307
- return {
308
- // Keep the historical graph id so existing mirrored task cards are reused.
309
- id: graphId,
310
- specId: graphId,
311
- requiredRequirementIds: nodes.map((node) => node.specRequirementId),
312
- title: "Session tasks",
313
- nodes,
314
- edges,
315
- rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
316
- createdAt: 0,
317
- updatedAt: 0
318
- };
319
- }
320
- function broadcastTodoUpdate(context, todos) {
321
- const sessionId = context.session?.id ?? "";
322
- if (!context.agentId || !sessionId) return;
323
- const statusCounts = { pending: 0, inProgress: 0, completed: 0 };
324
- for (const todo of todos) {
325
- if (todo.status === "completed") statusCounts.completed++;
326
- else if (todo.status === "in_progress") statusCounts.inProgress++;
327
- else statusCounts.pending++;
328
- }
329
- const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;
330
- const mailbox = getSharedProjectMailbox(projectDir);
331
- void mailbox.send({
332
- from: context.agentId,
333
- to: "*",
334
- type: "status",
335
- subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
336
- body: JSON.stringify({
337
- kind: "kanban.todos.updated",
338
- sessionId,
339
- revision: context.state.revision,
340
- todoCount: todos.length,
341
- statusCounts
342
- }),
343
- priority: "normal",
344
- senderSessionId: sessionId
345
- }).catch(() => {
346
- });
347
- }
348
- function notifyTodoUpdate(context, todos) {
349
- const summary = formatTodosForModel(todos);
350
- const text = `[KANBAN TODO UPDATE]
351
- Another Kanban agent reassessed the shared board. The canonical todo list is now:
352
- ${summary}
353
- Reassess your current plan before continuing; do not rely on the initial todo snapshot. Preserve each row's <kanban board/task> binding verbatim on your next \`todo\` call \u2014 a row that loses it stops advancing its card.`;
354
- const state = context.state;
355
- if (typeof state.appendBlockToLastUserMessage === "function") {
356
- if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
357
- }
358
- if (typeof state.appendMessage === "function") {
359
- state.appendMessage({ role: "user", content: [{ type: "text", text }] });
360
- }
361
- }
362
- function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
363
- queueLatestMirror(
364
- projectRoot,
365
- sessionId,
366
- todoListToSerializedGraph(todos, sessionId),
367
- "session-todo"
368
- );
457
+ function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
458
+ queueLatestMirror(
459
+ projectRoot,
460
+ sessionId,
461
+ todoListToSerializedGraph(todos, sessionId),
462
+ "session-todo"
463
+ );
369
464
  }
370
465
  function mirrorSessionTasksToKanban(projectRoot, tasks, sessionId) {
371
466
  queueLatestMirror(
@@ -375,108 +470,8 @@ function mirrorSessionTasksToKanban(projectRoot, tasks, sessionId) {
375
470
  "session-task"
376
471
  );
377
472
  }
378
- function sourceStatus(task) {
379
- if (task.status === "completed") return "completed";
380
- if (task.status === "in_progress") return "in_progress";
381
- if (task.status === "review") return "review";
382
- if (task.status === "blocked") return "blocked";
383
- if (task.status === "failed") return "failed";
384
- return "pending";
385
- }
386
- function todoStatus(task) {
387
- const status = sourceStatus(task);
388
- if (status === "completed") return "completed";
389
- if (status === "review" && task.assignment?.status === "completed") return "completed";
390
- if (status === "in_progress" || status === "review") return "in_progress";
391
- return "pending";
392
- }
393
- function sessionTodoFromTask(task, board) {
394
- const blockedBy = board ? blockingTitles(board, task) : [];
395
- return {
396
- id: task.origin?.taskId ?? task.id,
397
- content: task.title,
398
- status: todoStatus(task),
399
- ...task.description ? { activeForm: task.description } : {},
400
- ...blockedBy.length ? { blockedBy } : {}
401
- };
402
- }
403
- function managedTodoFromTask(task, board) {
404
- return {
405
- ...sessionTodoFromTask(task, board),
406
- kanbanBoardId: board.id,
407
- kanbanTaskId: task.id
408
- };
409
- }
410
- function blockingTitles(board, task) {
411
- return getDependencyReadinessIssues(board, task).map((issue) => {
412
- const dependency = board.tasks.find((candidate) => candidate.id === issue.dependencyId);
413
- if (!dependency) return `${issue.dependencyId} (missing)`;
414
- return dependency.title;
415
- });
416
- }
417
- var PRIORITY_ORDER = {
418
- critical: 0,
419
- high: 1,
420
- medium: 2,
421
- low: 3
422
- };
423
- function orderTasksForTodos(board, tasks) {
424
- const columnOrder = new Map(board.columns.map((column) => [column.id, column.order]));
425
- const baseline = [...tasks].sort(
426
- (left, right) => (columnOrder.get(left.columnId) ?? 0) - (columnOrder.get(right.columnId) ?? 0) || (PRIORITY_ORDER[left.priority] ?? 2) - (PRIORITY_ORDER[right.priority] ?? 2) || left.order - right.order || left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id)
427
- );
428
- const included = new Set(baseline.map((task) => task.id));
429
- const remaining = new Map(baseline.map((task) => [task.id, task]));
430
- const emitted = [];
431
- const done = /* @__PURE__ */ new Set();
432
- while (remaining.size > 0) {
433
- const ready = baseline.filter(
434
- (task) => remaining.has(task.id) && (task.dependsOn ?? []).every(
435
- (dependencyId) => !included.has(dependencyId) || done.has(dependencyId)
436
- )
437
- );
438
- if (ready.length === 0) break;
439
- for (const task of ready) {
440
- remaining.delete(task.id);
441
- done.add(task.id);
442
- emitted.push(task);
443
- }
444
- }
445
- for (const task of baseline) if (remaining.has(task.id)) emitted.push(task);
446
- return emitted;
447
- }
448
- function sameTodos(left, right) {
449
- return left.length === right.length && left.every((todo, index) => {
450
- const candidate = right[index];
451
- return candidate?.id === todo.id && candidate.content === todo.content && candidate.status === todo.status && candidate.activeForm === todo.activeForm && candidate.promotedFromPlan === todo.promotedFromPlan && candidate.promotedFromTask === todo.promotedFromTask && candidate.kanbanBoardId === todo.kanbanBoardId && candidate.kanbanTaskId === todo.kanbanTaskId && // Readiness is part of the projection: when a dependency completes,
452
- // the rows are otherwise identical and the unblocking would never
453
- // reach the model.
454
- (candidate.blockedBy ?? []).join("\0") === (todo.blockedBy ?? []).join("\0");
455
- });
456
- }
457
- function applyManagedKanbanBoardToTodos(context, board) {
458
- const metaKanban = context.meta["kanban"];
459
- const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
460
- const activeBoardId2 = context.currentKanbanBoardId ?? (typeof metaBoardId === "string" ? metaBoardId : void 0);
461
- if (!activeBoardId2 || board.id !== activeBoardId2 || board.lifecycle?.mode !== "managed") {
462
- return [...context.todos];
463
- }
464
- const projectedTodos = orderTasksForTodos(
465
- board,
466
- board.tasks.filter(
467
- (task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
468
- )
469
- ).map((task) => managedTodoFromTask(task, board));
470
- if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
471
- suppressedTodoMirrors.add(context);
472
- try {
473
- context.state.replaceTodos(projectedTodos);
474
- } finally {
475
- suppressedTodoMirrors.delete(context);
476
- }
477
- notifyTodoUpdate(context, context.todos);
478
- broadcastTodoUpdate(context, context.todos);
479
- return [...context.todos];
473
+ function applyManagedKanbanBoardToTodos2(context, board) {
474
+ return applyManagedKanbanBoardToTodos(context, board, suppressedTodoMirrors);
480
475
  }
481
476
 
482
477
  // src/todo.ts
@@ -487,54 +482,34 @@ import {
487
482
  saveTasks,
488
483
  setPlanItemStatus
489
484
  } from "@wrongstack/core/storage";
490
- import { addTask as addTask2, getBoard as getBoard4 } from "@wrongstack/kanban";
485
+ import { addTask as addTask3, getBoard as getBoard5 } from "@wrongstack/kanban";
491
486
 
492
487
  // src/kanban.ts
493
- import { randomUUID as randomUUID2 } from "node:crypto";
488
+ import { stripLifecycleIssues as stripLifecycleIssues2 } from "@wrongstack/kanban";
489
+
490
+ // src/kanban-board-actions.ts
494
491
  import { loadTasks as loadTasks2 } from "@wrongstack/core/storage";
495
492
  import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
496
493
  import {
497
494
  addTask,
498
495
  adoptManagedLifecycle,
499
- assignTask,
500
- claimReadyTask,
501
- copyTaskToBoard,
502
496
  createBoard as createBoard2,
503
497
  createBoardFromTaskGraph,
504
498
  createBoardFromText,
505
499
  duplicateBoard,
506
- evaluateContractGraphReadiness,
507
500
  exportBoardAsMarkdown,
508
501
  exportBoardToTaskGraph,
509
- finalizeTaskCompletion,
510
502
  getBoard as getBoard3,
511
503
  getKanbanOrchestrationSnapshot as getKanbanOrchestrationSnapshot2,
512
504
  getKanbanQueueHealth,
513
- getTask,
514
- getTaskChain,
515
- heartbeatTaskAssignment,
516
505
  listBoards as listBoards2,
517
506
  listKanbanEvents,
518
507
  listReadyTasks,
519
- mergeTasks,
520
- moveTask,
521
508
  parseLinesIntoTasks,
522
- recoverStaleTaskAssignments,
523
- releaseTaskClaim,
524
509
  removeBoard as removeBoard2,
525
- removeTask,
526
- repairManagedTaskProjection,
527
- resolveAutoAccept,
528
510
  searchKanban,
529
- setTaskChain,
530
- stripLifecycleIssues,
531
511
  syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
532
- transferTaskToBoard,
533
- transitionTask,
534
- updateBoard as updateBoard2,
535
- updateTask as updateTask2,
536
- updateTaskAssignment,
537
- verifyTaskCompletion as verifyTaskCompletion2
512
+ updateBoard as updateBoard2
538
513
  } from "@wrongstack/kanban";
539
514
 
540
515
  // src/kanban-board-inputs.ts
@@ -581,16 +556,8 @@ function duplicateBoardOptions(input) {
581
556
  };
582
557
  }
583
558
 
584
- // src/kanban-contract-actions.ts
585
- import {
586
- addContractEdge,
587
- configureContractGraph,
588
- evaluateTaskContractGraph,
589
- getContractGraph,
590
- removeContractEdge,
591
- removeContractNode,
592
- upsertContractNode
593
- } from "@wrongstack/kanban";
559
+ // src/kanban-split-task-handler.ts
560
+ import { getBoard as getBoard2, splitTask } from "@wrongstack/kanban";
594
561
 
595
562
  // src/kanban-tool-results.ts
596
563
  function atomicityNudge(task) {
@@ -612,7 +579,314 @@ function okTask(board, task, message) {
612
579
  return { ok: true, message, board, task };
613
580
  }
614
581
 
582
+ // src/kanban-split-task-handler.ts
583
+ async function handleSplitTask(projectRoot, input, extraSplitOptions) {
584
+ const boardId = input.boardId;
585
+ const taskId = input.taskId;
586
+ const childTitles = input.childTitles;
587
+ if (!boardId || !taskId || !childTitles?.length) {
588
+ return fail("split requires boardId, taskId, and at least one childTitles.");
589
+ }
590
+ const {
591
+ targetColumnId,
592
+ inheritAssignment,
593
+ inheritLabels,
594
+ inheritSuccessCriteria,
595
+ inheritGoalMetrics,
596
+ inheritDependencies,
597
+ chainChildren,
598
+ rewireDependents
599
+ } = input;
600
+ const result = await splitTask(projectRoot, boardId, taskId, {
601
+ titles: childTitles,
602
+ ...extraSplitOptions,
603
+ ...targetColumnId !== void 0 ? { columnId: targetColumnId } : {},
604
+ ...inheritAssignment !== void 0 ? { inheritAssignment } : {},
605
+ ...inheritLabels !== void 0 ? { inheritLabels } : {},
606
+ ...inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria } : {},
607
+ ...inheritGoalMetrics !== void 0 ? { inheritGoalMetrics } : {},
608
+ ...inheritDependencies !== void 0 ? { inheritDependencies } : {},
609
+ ...chainChildren !== void 0 ? { chainChildren } : {},
610
+ ...rewireDependents !== void 0 ? { rewireDependents } : {}
611
+ });
612
+ if (!result) return fail("Task not found.");
613
+ const freshParent = result.board.tasks?.find((t) => t.id === taskId);
614
+ if (!freshParent) {
615
+ return fail(
616
+ `Split succeeded but parent ${taskId} not found in returned board. Children: [${result.children.map((c) => c.id).join(", ")}].`
617
+ );
618
+ }
619
+ return {
620
+ ok: true,
621
+ message: `${result.children.length} child task(s) created.`,
622
+ board: result.board,
623
+ task: freshParent,
624
+ children: result.children
625
+ };
626
+ }
627
+ async function requireBoard(projectRoot, boardId) {
628
+ return boardId ? getBoard2(projectRoot, boardId) : null;
629
+ }
630
+
631
+ // src/kanban-board-actions.ts
632
+ async function handleKanbanBoardAction(projectRoot, input, ctx) {
633
+ switch (input.action) {
634
+ case "list_boards": {
635
+ const boards = await listBoards2(projectRoot);
636
+ return { ok: true, message: `${boards.length} board(s).`, boards };
637
+ }
638
+ case "get_board": {
639
+ const board = await requireBoard(projectRoot, input.boardId);
640
+ return board ? okBoard(board) : fail("Board not found.");
641
+ }
642
+ case "create_board": {
643
+ if (!input.title) return fail("create_board requires title.");
644
+ const existing = (await listBoards2(projectRoot)).filter(
645
+ (candidate) => (candidate.kind ?? "project") === "project"
646
+ );
647
+ const board = await createBoard2(projectRoot, boardCreateInput(input, input.title));
648
+ 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(
649
+ ", "
650
+ )}${existing.length > 3 ? ", \u2026" : ""}. If this work belongs to one of them, add_task there instead and delete this board.` : "";
651
+ return { ok: true, message: `Board created: ${board.title}.${note}`, board };
652
+ }
653
+ case "update_board": {
654
+ if (!input.boardId) return fail("update_board requires boardId.");
655
+ const board = await updateBoard2(projectRoot, input.boardId, boardUpdatePatch(input));
656
+ return board ? okBoard(board, "Board updated.") : fail("Board not found.");
657
+ }
658
+ case "adopt_managed_lifecycle": {
659
+ if (!input.boardId || !input.author || !input.transitionComment) {
660
+ return fail(
661
+ "adopt_managed_lifecycle requires boardId, author, transitionComment, and five ordered columns."
662
+ );
663
+ }
664
+ if (input.columns?.length !== 5) {
665
+ return fail(
666
+ "adopt_managed_lifecycle columns must be ordered as backlog, todo, running, review, done."
667
+ );
668
+ }
669
+ const [backlog, todo, running, review, done] = input.columns;
670
+ if (!backlog || !todo || !running || !review || !done) {
671
+ return fail("adopt_managed_lifecycle columns must contain five nonblank ids.");
672
+ }
673
+ const board = await adoptManagedLifecycle(projectRoot, input.boardId, {
674
+ columns: { backlog, todo, running, review, done },
675
+ actor: input.author,
676
+ comment: input.transitionComment
677
+ });
678
+ return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
679
+ }
680
+ case "release_managed_lifecycle": {
681
+ if (!input.boardId) return fail("release_managed_lifecycle requires boardId.");
682
+ const board = await updateBoard2(projectRoot, input.boardId, { lifecycle: null });
683
+ return board ? okBoard(
684
+ board,
685
+ "Managed lifecycle released; the board now tracks work without strict gates."
686
+ ) : fail("Board not found.");
687
+ }
688
+ case "duplicate_board": {
689
+ if (!input.boardId) return fail("duplicate_board requires boardId.");
690
+ const board = await duplicateBoard(projectRoot, input.boardId, duplicateBoardOptions(input));
691
+ return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
692
+ }
693
+ case "delete_board": {
694
+ if (!input.boardId) return fail("delete_board requires boardId.");
695
+ const removed = await removeBoard2(projectRoot, input.boardId);
696
+ return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
697
+ }
698
+ case "generate_board": {
699
+ if (!input.description) return fail("generate_board requires description.");
700
+ const boardInput = createBoardFromText({
701
+ description: input.description,
702
+ ...input.title !== void 0 ? { title: input.title } : {},
703
+ ...input.context !== void 0 ? { context: input.context } : {}
704
+ });
705
+ const board = await createBoard2(projectRoot, boardInput);
706
+ for (const tInput of parseLinesIntoTasks(
707
+ input.description,
708
+ board.columns[0]?.id ?? "backlog"
709
+ )) {
710
+ await addTask(projectRoot, board.id, tInput);
711
+ }
712
+ return okBoard(await getBoard3(projectRoot, board.id) ?? board, "Board generated.");
713
+ }
714
+ case "export_markdown": {
715
+ const board = await requireBoard(projectRoot, input.boardId);
716
+ if (!board) return fail("Board not found.");
717
+ return {
718
+ ok: true,
719
+ message: "Board exported.",
720
+ board,
721
+ markdown: exportBoardAsMarkdown(board)
722
+ };
723
+ }
724
+ case "export_task_graph": {
725
+ if (!input.boardId) return fail("export_task_graph requires boardId.");
726
+ const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
727
+ ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
728
+ ...input.specId !== void 0 ? { specId: input.specId } : {},
729
+ ...input.title !== void 0 ? { title: input.title } : {},
730
+ ...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
731
+ ...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
732
+ });
733
+ if (!exported) return fail("Board not found.");
734
+ return {
735
+ ok: true,
736
+ message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
737
+ board: exported.board,
738
+ taskGraph: serializeTaskGraph(exported.graph)
739
+ };
740
+ }
741
+ case "sync_task_graph": {
742
+ if (!input.boardId || !input.taskGraph) {
743
+ return fail("sync_task_graph requires boardId and taskGraph.");
744
+ }
745
+ const graph = deserializeTaskGraph2(input.taskGraph);
746
+ const result = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
747
+ ...input.title !== void 0 ? { title: input.title } : {},
748
+ ...input.description !== void 0 ? { description: input.description } : {},
749
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
750
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
751
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
752
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
753
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
754
+ ...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
755
+ ...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
756
+ });
757
+ return result ? {
758
+ ok: true,
759
+ message: `Task graph synced: ${result.createdTaskIds.length} created, ${result.updatedTaskIds.length} updated, ${result.archivedTaskIds.length} archived.`,
760
+ board: result.board
761
+ } : fail("Board not found.");
762
+ }
763
+ case "create_from_graph": {
764
+ if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
765
+ const graph = deserializeTaskGraph2(input.taskGraph);
766
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
767
+ ...input.title !== void 0 ? { title: input.title } : {},
768
+ ...input.description !== void 0 ? { description: input.description } : {},
769
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
770
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
771
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
772
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
773
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
774
+ });
775
+ return {
776
+ ok: true,
777
+ message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
778
+ board
779
+ };
780
+ }
781
+ case "import_session_tasks": {
782
+ const taskPath = ctx.meta?.["task.path"];
783
+ if (!taskPath) return fail("No session task file for this session.");
784
+ const file = await loadTasks2(taskPath);
785
+ if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
786
+ const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
787
+ const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
788
+ const tags = ["session", `session:${sessionId}`];
789
+ const existing = (await listBoards2(projectRoot)).find(
790
+ (b) => b.tags?.includes(`session:${sessionId}`)
791
+ );
792
+ if (existing) {
793
+ const result = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
794
+ sourceSystem: "session",
795
+ tags,
796
+ archiveMissingTasks: true,
797
+ includeCompletedTasks: true
798
+ });
799
+ return result ? {
800
+ ok: true,
801
+ message: `Synced ${file.tasks.length} session tasks into board "${result.board.title}".`,
802
+ board: result.board
803
+ } : fail("Session board vanished mid-sync.");
804
+ }
805
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
806
+ title: `Session tasks (${sessionId.slice(0, 8)})`,
807
+ sourceSystem: "session",
808
+ tags
809
+ });
810
+ return {
811
+ ok: true,
812
+ message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
813
+ board
814
+ };
815
+ }
816
+ case "search_tasks": {
817
+ const tasks = await searchKanban(projectRoot, {
818
+ query: input.query,
819
+ boardId: input.boardId,
820
+ assignedAgent: input.agentId,
821
+ status: input.status,
822
+ priority: input.priority,
823
+ label: input.labels?.[0],
824
+ chainId: input.chainId
825
+ });
826
+ return { ok: true, message: `${tasks.length} task(s) matched.`, tasks };
827
+ }
828
+ case "ready_tasks": {
829
+ const tasks = await listReadyTasks(projectRoot, {
830
+ query: input.query,
831
+ boardId: input.boardId,
832
+ assignedAgent: input.agentId,
833
+ priority: input.priority,
834
+ label: input.labels?.[0],
835
+ chainId: input.chainId,
836
+ limit: input.limit
837
+ });
838
+ return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
839
+ }
840
+ case "snapshot": {
841
+ const snapshot = await getKanbanOrchestrationSnapshot2(projectRoot, {
842
+ query: input.query,
843
+ boardId: input.boardId,
844
+ assignedAgent: input.agentId,
845
+ status: input.status,
846
+ priority: input.priority,
847
+ label: input.labels?.[0],
848
+ chainId: input.chainId
849
+ });
850
+ return {
851
+ ok: true,
852
+ message: `${snapshot.ready.length} ready, ${snapshot.running.length} running, ${snapshot.blocked.length} blocked.`,
853
+ snapshot
854
+ };
855
+ }
856
+ case "events": {
857
+ if (!input.boardId) return fail("events requires boardId.");
858
+ const eventList = await listKanbanEvents(projectRoot, input.boardId);
859
+ return {
860
+ ok: true,
861
+ message: `${eventList.length} event(s).`,
862
+ events: eventList
863
+ };
864
+ }
865
+ case "queue_health": {
866
+ const health = await getKanbanQueueHealth(projectRoot, {
867
+ ...input.boardId !== void 0 ? { boardId: input.boardId } : {}
868
+ });
869
+ return {
870
+ ok: true,
871
+ message: `Counts: startable=${health.counts.startable}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
872
+ queueHealth: health
873
+ };
874
+ }
875
+ default:
876
+ return void 0;
877
+ }
878
+ }
879
+
615
880
  // src/kanban-contract-actions.ts
881
+ import {
882
+ addContractEdge,
883
+ configureContractGraph,
884
+ evaluateTaskContractGraph,
885
+ getContractGraph,
886
+ removeContractEdge,
887
+ removeContractNode,
888
+ upsertContractNode
889
+ } from "@wrongstack/kanban";
616
890
  async function handleKanbanContractAction(projectRoot, input, actor) {
617
891
  switch (input.action) {
618
892
  case "get_contract_graph": {
@@ -836,58 +1110,6 @@ import {
836
1110
  updateCheckOnTask,
837
1111
  updateGoalMetricOnTask
838
1112
  } from "@wrongstack/kanban";
839
-
840
- // src/kanban-split-task-handler.ts
841
- import { getBoard as getBoard2, splitTask } from "@wrongstack/kanban";
842
- async function handleSplitTask(projectRoot, input, extraSplitOptions) {
843
- const boardId = input.boardId;
844
- const taskId = input.taskId;
845
- const childTitles = input.childTitles;
846
- if (!boardId || !taskId || !childTitles?.length) {
847
- return fail("split requires boardId, taskId, and at least one childTitles.");
848
- }
849
- const {
850
- targetColumnId,
851
- inheritAssignment,
852
- inheritLabels,
853
- inheritSuccessCriteria,
854
- inheritGoalMetrics,
855
- inheritDependencies,
856
- chainChildren,
857
- rewireDependents
858
- } = input;
859
- const result = await splitTask(projectRoot, boardId, taskId, {
860
- titles: childTitles,
861
- ...extraSplitOptions,
862
- ...targetColumnId !== void 0 ? { columnId: targetColumnId } : {},
863
- ...inheritAssignment !== void 0 ? { inheritAssignment } : {},
864
- ...inheritLabels !== void 0 ? { inheritLabels } : {},
865
- ...inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria } : {},
866
- ...inheritGoalMetrics !== void 0 ? { inheritGoalMetrics } : {},
867
- ...inheritDependencies !== void 0 ? { inheritDependencies } : {},
868
- ...chainChildren !== void 0 ? { chainChildren } : {},
869
- ...rewireDependents !== void 0 ? { rewireDependents } : {}
870
- });
871
- if (!result) return fail("Task not found.");
872
- const freshParent = result.board.tasks?.find((t) => t.id === taskId);
873
- if (!freshParent) {
874
- return fail(
875
- `Split succeeded but parent ${taskId} not found in returned board. Children: [${result.children.map((c) => c.id).join(", ")}].`
876
- );
877
- }
878
- return {
879
- ok: true,
880
- message: `${result.children.length} child task(s) created.`,
881
- board: result.board,
882
- task: freshParent,
883
- children: result.children
884
- };
885
- }
886
- async function requireBoard(projectRoot, boardId) {
887
- return boardId ? getBoard2(projectRoot, boardId) : null;
888
- }
889
-
890
- // src/kanban-detail-actions.ts
891
1113
  async function handleKanbanDetailAction(projectRoot, input) {
892
1114
  switch (input.action) {
893
1115
  case "workbench": {
@@ -1020,26 +1242,34 @@ async function handleKanbanDetailAction(projectRoot, input) {
1020
1242
  }
1021
1243
  }
1022
1244
 
1023
- // src/kanban-presence.ts
1024
- import { touchKanbanPresence as touchKanbanPresence2 } from "@wrongstack/kanban";
1025
- function createKanbanPresenceWrapper(projectRoot, input, ctx) {
1026
- return async (result) => {
1027
- const boardId = result.board?.id ?? input.boardId;
1028
- if (!result.ok || !boardId || !ctx.session?.id || !ctx.agentId) return result;
1029
- try {
1030
- const board = await touchKanbanPresence2(projectRoot, boardId, {
1031
- sessionId: ctx.session.id,
1032
- agentId: ctx.agentId,
1033
- agentName: ctx.agentName,
1034
- taskId: input.taskId ?? result.task?.id,
1035
- runTaskId: input.runTaskId
1036
- });
1037
- return board ? { ...result, board } : result;
1038
- } catch {
1039
- return result;
1040
- }
1041
- };
1042
- }
1245
+ // src/kanban-lifecycle-actions.ts
1246
+ import { randomUUID as randomUUID2 } from "node:crypto";
1247
+ import {
1248
+ addTask as addTask2,
1249
+ assignTask,
1250
+ claimReadyTask,
1251
+ copyTaskToBoard,
1252
+ evaluateContractGraphReadiness,
1253
+ finalizeTaskCompletion,
1254
+ getBoard as getBoard4,
1255
+ getTask,
1256
+ getTaskChain,
1257
+ heartbeatTaskAssignment,
1258
+ mergeTasks,
1259
+ moveTask,
1260
+ recoverStaleTaskAssignments,
1261
+ releaseTaskClaim,
1262
+ removeTask,
1263
+ repairManagedTaskProjection,
1264
+ resolveAutoAccept,
1265
+ setTaskChain,
1266
+ stripLifecycleIssues,
1267
+ transferTaskToBoard,
1268
+ transitionTask,
1269
+ updateTask as updateTask2,
1270
+ updateTaskAssignment,
1271
+ verifyTaskCompletion as verifyTaskCompletion2
1272
+ } from "@wrongstack/kanban";
1043
1273
 
1044
1274
  // src/kanban-task-inputs.ts
1045
1275
  import { randomUUID } from "node:crypto";
@@ -1215,6 +1445,572 @@ function assignmentForTaskCreate(input) {
1215
1445
  };
1216
1446
  }
1217
1447
 
1448
+ // src/kanban-lifecycle-actions.ts
1449
+ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1450
+ switch (input.action) {
1451
+ case "add_task": {
1452
+ if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
1453
+ const result = await addTask2(projectRoot, input.boardId, taskInput(input));
1454
+ if (!result) return fail("Board not found.");
1455
+ return okTask(result.board, result.task, `Task added.${atomicityNudge(result.task)}`);
1456
+ }
1457
+ case "split_task": {
1458
+ if (!input.boardId || !input.taskId || !input.childTitles?.length) {
1459
+ return fail("split_task requires boardId, taskId, and childTitles.");
1460
+ }
1461
+ return handleSplitTask(projectRoot, input, {});
1462
+ }
1463
+ case "merge_tasks": {
1464
+ if (!input.boardId || !input.taskIds?.length || !input.title) {
1465
+ return fail("merge_tasks requires boardId, taskIds, and title.");
1466
+ }
1467
+ const result = await mergeTasks(projectRoot, input.boardId, {
1468
+ taskIds: input.taskIds,
1469
+ title: input.title,
1470
+ ...input.description !== void 0 ? { description: input.description } : {},
1471
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
1472
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
1473
+ ...input.closeSourceTasks !== void 0 ? { closeSourceTasks: input.closeSourceTasks } : {}
1474
+ });
1475
+ return result ? okTask(result.board, result.task, "Tasks merged.") : fail("Board or task not found.");
1476
+ }
1477
+ case "copy_task": {
1478
+ if (!input.boardId || !input.taskId || !input.targetBoardId) {
1479
+ return fail("copy_task requires boardId, taskId, and targetBoardId.");
1480
+ }
1481
+ const result = await copyTaskToBoard(
1482
+ projectRoot,
1483
+ input.boardId,
1484
+ input.taskId,
1485
+ input.targetBoardId,
1486
+ {
1487
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
1488
+ ...input.order !== void 0 ? { targetOrder: input.order } : {},
1489
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
1490
+ ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
1491
+ }
1492
+ );
1493
+ return result ? okTask(result.targetBoard, result.task, "Task copied to target board.") : fail("Board or task not found.");
1494
+ }
1495
+ case "transfer_task": {
1496
+ if (!input.boardId || !input.taskId || !input.targetBoardId) {
1497
+ return fail("transfer_task requires boardId, taskId, and targetBoardId.");
1498
+ }
1499
+ const result = await transferTaskToBoard(
1500
+ projectRoot,
1501
+ input.boardId,
1502
+ input.taskId,
1503
+ input.targetBoardId,
1504
+ {
1505
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
1506
+ ...input.order !== void 0 ? { targetOrder: input.order } : {},
1507
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
1508
+ ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
1509
+ }
1510
+ );
1511
+ return result ? okTask(result.targetBoard, result.task, "Task transferred to target board.") : fail("Board or task not found.");
1512
+ }
1513
+ case "get_task": {
1514
+ if (!input.boardId || !input.taskId) return fail("get_task requires boardId and taskId.");
1515
+ const task = await getTask(projectRoot, input.boardId, input.taskId);
1516
+ return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
1517
+ }
1518
+ case "start_task": {
1519
+ if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
1520
+ return fail("start_task requires boardId, taskId, author, and transitionComment.");
1521
+ }
1522
+ let board = await getBoard4(projectRoot, input.boardId);
1523
+ let task = board?.tasks.find((candidate) => candidate.id === input.taskId);
1524
+ if (!board || !task) return fail("Board or task not found.");
1525
+ const readiness = evaluateContractGraphReadiness(board, task.id);
1526
+ if (!readiness.ready) {
1527
+ return fail(
1528
+ `Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
1529
+ );
1530
+ }
1531
+ if (board.lifecycle?.mode !== "managed") {
1532
+ const now = /* @__PURE__ */ new Date();
1533
+ const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
1534
+ status: "running",
1535
+ agentId: input.agentId ?? input.author,
1536
+ leaseId: input.leaseId ?? randomUUID2(),
1537
+ claimedAt: input.claimedAt ?? now.toISOString(),
1538
+ heartbeatAt: input.heartbeatAt ?? now.toISOString(),
1539
+ leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
1540
+ attempt: input.attempt ?? 1,
1541
+ maxAttempts: input.maxAttempts ?? 3
1542
+ });
1543
+ if (!assigned) return fail("Task assignment could not be started.");
1544
+ const started = await updateTask2(projectRoot, board.id, task.id, {
1545
+ status: "in_progress"
1546
+ });
1547
+ const current = started ?? assigned;
1548
+ const claimed = task;
1549
+ const currentTask = current.tasks.find((candidate) => candidate.id === claimed.id) ?? claimed;
1550
+ ctx.setCurrentKanbanTask?.(currentTask.id, current.id);
1551
+ return okTask(
1552
+ current,
1553
+ currentTask,
1554
+ "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."
1555
+ );
1556
+ }
1557
+ let stage = task.lifecycle?.currentStage;
1558
+ if (stage === "backlog") {
1559
+ const moved = await transitionTask(projectRoot, board.id, task.id, {
1560
+ to: "todo",
1561
+ actor: input.author,
1562
+ comment: input.transitionComment
1563
+ });
1564
+ if (!moved) return fail("Task could not enter Todo.");
1565
+ board = moved.board;
1566
+ task = moved.task;
1567
+ stage = task.lifecycle?.currentStage;
1568
+ }
1569
+ if (stage === "todo" || stage === "review") {
1570
+ const now = /* @__PURE__ */ new Date();
1571
+ const leaseId = input.leaseId ?? randomUUID2();
1572
+ const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
1573
+ status: "running",
1574
+ agentId: input.agentId ?? input.author,
1575
+ leaseId,
1576
+ claimedAt: input.claimedAt ?? now.toISOString(),
1577
+ heartbeatAt: input.heartbeatAt ?? now.toISOString(),
1578
+ leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
1579
+ attempt: input.attempt ?? 1,
1580
+ maxAttempts: input.maxAttempts ?? 3
1581
+ });
1582
+ if (!assigned) return fail("Task assignment could not be started.");
1583
+ const moved = await transitionTask(projectRoot, board.id, task.id, {
1584
+ to: "running",
1585
+ actor: input.author,
1586
+ comment: input.transitionComment
1587
+ });
1588
+ if (!moved) return fail("Task could not enter Running.");
1589
+ board = moved.board;
1590
+ task = moved.task;
1591
+ stage = task.lifecycle?.currentStage;
1592
+ }
1593
+ if (stage !== "running" || task.assignment?.status !== "running") {
1594
+ return fail(
1595
+ `start_task only accepts Backlog, Todo, Review repair, or live Running cards (current: ${stage ?? "unknown"}).`
1596
+ );
1597
+ }
1598
+ ctx.setCurrentKanbanTask(task.id, board.id);
1599
+ return okTask(
1600
+ board,
1601
+ task,
1602
+ "Task is active; runtime Kanban governance is now bound to this run."
1603
+ );
1604
+ }
1605
+ case "update_task": {
1606
+ if (!input.boardId || !input.taskId) return fail("update_task requires boardId and taskId.");
1607
+ const board = await updateTask2(projectRoot, input.boardId, input.taskId, taskPatch(input));
1608
+ return board ? okBoard(board, "Task updated.") : fail("Task not found.");
1609
+ }
1610
+ case "transition_task": {
1611
+ if (!input.boardId || !input.taskId || !input.lifecycleStage || !input.author || !input.transitionComment) {
1612
+ return fail(
1613
+ "transition_task requires boardId, taskId, lifecycleStage, author, and transitionComment."
1614
+ );
1615
+ }
1616
+ if (input.lifecycleStage === "done") {
1617
+ const boardBefore = await getBoard4(projectRoot, input.boardId);
1618
+ const taskBefore = boardBefore ? await getTask(projectRoot, input.boardId, input.taskId) : null;
1619
+ if (boardBefore && taskBefore && !taskBefore.verificationReport && (taskBefore.atomic || Boolean(taskBefore.successCriteria?.length))) {
1620
+ const preGate = await verifyTaskCompletion2(projectRoot, input.boardId, taskBefore.id, {
1621
+ persist: false
1622
+ });
1623
+ await updateTask2(projectRoot, input.boardId, taskBefore.id, {
1624
+ verificationReport: preGate.report,
1625
+ successCriteria: preGate.task.successCriteria
1626
+ });
1627
+ }
1628
+ }
1629
+ const result = await transitionTask(projectRoot, input.boardId, input.taskId, {
1630
+ to: input.lifecycleStage,
1631
+ actor: input.author,
1632
+ comment: input.transitionComment,
1633
+ ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
1634
+ ...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
1635
+ ...input.attachmentUrl !== void 0 ? {
1636
+ attachment: {
1637
+ url: input.attachmentUrl,
1638
+ type: input.attachmentType ?? "url",
1639
+ ...input.attachmentTitle !== void 0 ? { title: input.attachmentTitle } : {}
1640
+ }
1641
+ } : {},
1642
+ patch: taskPatch(input)
1643
+ });
1644
+ if (result && input.lifecycleStage === "done" && result.task.verificationReport) {
1645
+ recordKanbanVerificationEvidence(ctx, result.task.verificationReport);
1646
+ }
1647
+ return result ? okTask(result.board, result.task, `Task advanced to ${result.transition.to}.`) : fail("Board or task not found.");
1648
+ }
1649
+ case "repair_managed_projection": {
1650
+ if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
1651
+ return fail(
1652
+ "repair_managed_projection requires boardId, taskId, author, and transitionComment."
1653
+ );
1654
+ }
1655
+ const result = await repairManagedTaskProjection(projectRoot, input.boardId, input.taskId, {
1656
+ actor: input.author,
1657
+ comment: input.transitionComment
1658
+ });
1659
+ return result ? okTask(
1660
+ result.board,
1661
+ result.task,
1662
+ "Managed card projection repaired from lifecycle history."
1663
+ ) : fail("Board or task not found.");
1664
+ }
1665
+ case "move_task": {
1666
+ if (!input.boardId || !input.taskId || !input.targetColumnId) {
1667
+ return fail("move_task requires boardId, taskId, and targetColumnId.");
1668
+ }
1669
+ const board = await moveTask(
1670
+ projectRoot,
1671
+ input.boardId,
1672
+ input.taskId,
1673
+ input.targetColumnId,
1674
+ input.order
1675
+ );
1676
+ return board ? okBoard(board, "Task moved.") : fail("Move failed.");
1677
+ }
1678
+ case "delete_task": {
1679
+ if (!input.boardId || !input.taskId) return fail("delete_task requires boardId and taskId.");
1680
+ const board = await removeTask(projectRoot, input.boardId, input.taskId);
1681
+ if (board && ctx.currentKanbanTaskId === input.taskId) {
1682
+ ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
1683
+ }
1684
+ return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
1685
+ }
1686
+ case "set_chain": {
1687
+ if (!input.boardId || !input.taskIds?.length) {
1688
+ return fail("set_chain requires boardId and taskIds.");
1689
+ }
1690
+ const result = await setTaskChain(projectRoot, input.boardId, {
1691
+ taskIds: input.taskIds,
1692
+ ...input.chainId !== void 0 ? { chainId: input.chainId } : {},
1693
+ ...input.enforceDependencies !== void 0 ? { enforceDependencies: input.enforceDependencies } : {}
1694
+ });
1695
+ return result ? {
1696
+ ok: true,
1697
+ message: `Chain set: ${result.chainId}`,
1698
+ board: result.board,
1699
+ chain: result.tasks
1700
+ } : fail("Board or task not found.");
1701
+ }
1702
+ case "get_chain": {
1703
+ if (!input.boardId || !(input.taskId || input.chainId)) {
1704
+ return fail("get_chain requires boardId and taskId or chainId.");
1705
+ }
1706
+ const result = await getTaskChain(
1707
+ projectRoot,
1708
+ input.boardId,
1709
+ input.taskId ?? input.chainId ?? ""
1710
+ );
1711
+ return result ? {
1712
+ ok: true,
1713
+ message: `Chain loaded: ${result.chainId}`,
1714
+ board: result.board,
1715
+ chain: result.tasks
1716
+ } : fail("Chain not found.");
1717
+ }
1718
+ case "claim_task": {
1719
+ const result = await claimReadyTask(projectRoot, {
1720
+ ...input.boardId !== void 0 ? { boardId: input.boardId } : {},
1721
+ ...input.taskId !== void 0 ? { taskId: input.taskId } : {},
1722
+ ...assignmentInput(input),
1723
+ status: input.assignmentStatus ?? "queued"
1724
+ });
1725
+ return result ? okTask(result.board, result.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
1726
+ }
1727
+ case "release_task": {
1728
+ if (!input.boardId || !input.taskId) {
1729
+ return fail("release_task requires boardId and taskId.");
1730
+ }
1731
+ const board = await releaseTaskClaim(projectRoot, input.boardId, input.taskId, {
1732
+ ...input.releaseStatus !== void 0 ? { status: input.releaseStatus } : {},
1733
+ ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
1734
+ ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {}
1735
+ });
1736
+ return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
1737
+ }
1738
+ case "assign_task": {
1739
+ if (!input.boardId || !input.taskId) return fail("assign_task requires boardId and taskId.");
1740
+ const board = await assignTask(
1741
+ projectRoot,
1742
+ input.boardId,
1743
+ input.taskId,
1744
+ assignmentInput(input)
1745
+ );
1746
+ return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
1747
+ }
1748
+ case "mark_assignment": {
1749
+ if (!input.boardId || !input.taskId)
1750
+ return fail("mark_assignment requires boardId and taskId.");
1751
+ const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
1752
+ const board = await updateTaskAssignment(
1753
+ projectRoot,
1754
+ input.boardId,
1755
+ input.taskId,
1756
+ {
1757
+ ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
1758
+ ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
1759
+ ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
1760
+ ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
1761
+ ...input.error !== void 0 ? { error: input.error } : {},
1762
+ ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
1763
+ ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
1764
+ ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
1765
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
1766
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
1767
+ ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
1768
+ ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
1769
+ },
1770
+ input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
1771
+ );
1772
+ if (!board) return fail("Task not found.");
1773
+ if (assignmentStatus === "completed" && board.lifecycle?.mode !== "managed") {
1774
+ const envGate = readEnvGateEnforcement();
1775
+ const finalized = await finalizeTaskCompletion(projectRoot, board.id, input.taskId, {
1776
+ ...board.completionGate === void 0 && envGate !== void 0 ? { enforcement: envGate } : {},
1777
+ ...ctx.agentId !== void 0 ? { eventContext: { actor: ctx.agentId } } : {}
1778
+ });
1779
+ if (finalized) {
1780
+ if (finalized.gate.report) {
1781
+ recordKanbanVerificationEvidence(ctx, finalized.gate.report);
1782
+ }
1783
+ const gateSummary = {
1784
+ enforcement: finalized.gate.enforcement,
1785
+ allowed: finalized.gate.allowed,
1786
+ verdict: finalized.gate.verdict,
1787
+ issues: finalized.gate.issues.map((issue) => issue.message)
1788
+ };
1789
+ 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(" | ")}`;
1790
+ return {
1791
+ ...okTask(finalized.board, finalized.task, `Assignment updated. ${gateMessage}`),
1792
+ gate: gateSummary
1793
+ };
1794
+ }
1795
+ } else if (board.lifecycle?.mode === "managed") {
1796
+ const managedTask = board.tasks.find((candidate) => candidate.id === input.taskId);
1797
+ const stage = managedTask?.lifecycle?.currentStage;
1798
+ const actor = ctx.agentId ?? "kanban-agent";
1799
+ let transitionResult = null;
1800
+ const lifecycleWarnings = [];
1801
+ if (assignmentStatus === "running" && stage === "todo") {
1802
+ try {
1803
+ transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
1804
+ to: "running",
1805
+ actor,
1806
+ comment: "Work started."
1807
+ });
1808
+ } catch (err) {
1809
+ lifecycleWarnings.push(
1810
+ `Lifecycle transition to Running deferred: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
1811
+ );
1812
+ }
1813
+ }
1814
+ if (assignmentStatus === "completed" && stage === "running") {
1815
+ const comment = typeof input.lastResult === "string" && input.lastResult.trim().length > 0 ? input.lastResult.trim().slice(0, 1e3) : "Work completed.";
1816
+ try {
1817
+ transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
1818
+ to: "review",
1819
+ actor,
1820
+ comment,
1821
+ attachment: {
1822
+ url: `kanban://task/${input.taskId}/result`,
1823
+ title: "Worker completion result",
1824
+ type: "file"
1825
+ },
1826
+ patch: {
1827
+ ...input.agentId !== void 0 ? { assignedAgent: input.agentId } : {}
1828
+ }
1829
+ });
1830
+ } catch (err) {
1831
+ lifecycleWarnings.push(
1832
+ `Lifecycle transition to Review failed: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
1833
+ );
1834
+ }
1835
+ if (transitionResult) {
1836
+ const hasCriteria = (transitionResult.task.successCriteria?.length ?? 0) > 0 || transitionResult.task.atomic === true;
1837
+ if (hasCriteria) {
1838
+ try {
1839
+ const verResult = await verifyTaskCompletion2(projectRoot, board.id, input.taskId);
1840
+ if (verResult.report) {
1841
+ recordKanbanVerificationEvidence(ctx, verResult.report);
1842
+ }
1843
+ await updateTask2(projectRoot, board.id, input.taskId, {
1844
+ verificationReport: verResult.report,
1845
+ successCriteria: verResult.task.successCriteria
1846
+ });
1847
+ const verdict = verResult.report.verdict;
1848
+ if (verdict === "passed" && !resolveAutoAccept(board)) {
1849
+ lifecycleWarnings.push(
1850
+ "Verification passed, but this board does not auto-accept. The card is in Review awaiting an explicit transition_task to done."
1851
+ );
1852
+ } else if (verdict === "passed") {
1853
+ try {
1854
+ const doneResult = await transitionTask(projectRoot, board.id, input.taskId, {
1855
+ to: "done",
1856
+ actor,
1857
+ action: "Automated acceptance after verification",
1858
+ comment: "Auto-accepted: verification passed.",
1859
+ attachment: {
1860
+ url: `kanban://task/${input.taskId}/verification`,
1861
+ title: "Auto-verification result",
1862
+ type: "file"
1863
+ }
1864
+ });
1865
+ transitionResult = doneResult;
1866
+ } catch (acceptErr) {
1867
+ lifecycleWarnings.push(
1868
+ `Auto-accept to Done deferred: ${acceptErr instanceof Error ? acceptErr.message : String(acceptErr)}`
1869
+ );
1870
+ }
1871
+ } else {
1872
+ lifecycleWarnings.push(
1873
+ `Verification verdict: ${verdict} \u2014 card left in Review for manual acceptance.`
1874
+ );
1875
+ }
1876
+ } catch (verifyErr) {
1877
+ lifecycleWarnings.push(
1878
+ `Auto-verification error: ${verifyErr instanceof Error ? verifyErr.message : String(verifyErr)}`
1879
+ );
1880
+ }
1881
+ } else {
1882
+ lifecycleWarnings.push(
1883
+ "No automatic success criteria \u2014 card left in Review for manual verification."
1884
+ );
1885
+ }
1886
+ }
1887
+ }
1888
+ const responseBoard = transitionResult?.board ?? board;
1889
+ const responseTask = transitionResult?.task ?? managedTask;
1890
+ const msgParts = ["Assignment updated."];
1891
+ if (transitionResult) {
1892
+ msgParts.push(`Card advanced to ${transitionResult.transition.to}.`);
1893
+ }
1894
+ for (const w of lifecycleWarnings) msgParts.push(`Warning: ${w}`);
1895
+ return okTask(responseBoard, responseTask, msgParts.join(" "));
1896
+ }
1897
+ return okBoard(board, "Assignment updated.");
1898
+ }
1899
+ case "heartbeat_assignment": {
1900
+ if (!input.boardId || !input.taskId) {
1901
+ return fail("heartbeat_assignment requires boardId and taskId.");
1902
+ }
1903
+ const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
1904
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
1905
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
1906
+ ...input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
1907
+ });
1908
+ return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
1909
+ }
1910
+ case "recover_stale": {
1911
+ if (!input.boardId) return fail("recover_stale requires boardId.");
1912
+ const policyFields = [
1913
+ input.recoveryPolicyFailOnCostCeiling !== void 0,
1914
+ input.recoveryPolicyReleaseOnFailureKinds !== void 0,
1915
+ input.recoveryPolicyReleaseOnHeartbeatDue !== void 0,
1916
+ input.recoveryPolicyRetryPolicyOverride !== void 0
1917
+ ].some(Boolean);
1918
+ const result = await recoverStaleTaskAssignments(projectRoot, input.boardId, {
1919
+ ...input.recoveryMode !== void 0 ? { mode: input.recoveryMode } : {},
1920
+ ...input.recoveryNow !== void 0 ? { now: input.recoveryNow } : {},
1921
+ ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
1922
+ ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {},
1923
+ ...policyFields ? {
1924
+ policy: {
1925
+ ...input.recoveryPolicyFailOnCostCeiling !== void 0 ? { failWhenCostCeilingSet: input.recoveryPolicyFailOnCostCeiling } : {},
1926
+ ...input.recoveryPolicyReleaseOnFailureKinds !== void 0 ? {
1927
+ releaseOnFailureKinds: input.recoveryPolicyReleaseOnFailureKinds
1928
+ } : {},
1929
+ ...input.recoveryPolicyReleaseOnHeartbeatDue !== void 0 ? {
1930
+ releaseOnHeartbeatDue: input.recoveryPolicyReleaseOnHeartbeatDue
1931
+ } : {},
1932
+ ...input.recoveryPolicyRetryPolicyOverride !== void 0 ? {
1933
+ retryPolicyOverride: input.recoveryPolicyRetryPolicyOverride
1934
+ } : {}
1935
+ }
1936
+ } : {}
1937
+ });
1938
+ return result ? {
1939
+ ok: true,
1940
+ message: `Recovered ${result.tasks.length} stale assignment(s).`,
1941
+ board: result.board,
1942
+ recoveredTasks: result.tasks
1943
+ } : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
1944
+ }
1945
+ default:
1946
+ return void 0;
1947
+ }
1948
+ }
1949
+
1950
+ // src/kanban-presence.ts
1951
+ import { touchKanbanPresence as touchKanbanPresence2 } from "@wrongstack/kanban";
1952
+ function createKanbanPresenceWrapper(projectRoot, input, ctx) {
1953
+ return async (result) => {
1954
+ const boardId = result.board?.id ?? input.boardId;
1955
+ if (!result.ok || !boardId || !ctx.session?.id || !ctx.agentId) return result;
1956
+ try {
1957
+ const board = await touchKanbanPresence2(projectRoot, boardId, {
1958
+ sessionId: ctx.session.id,
1959
+ agentId: ctx.agentId,
1960
+ agentName: ctx.agentName,
1961
+ taskId: input.taskId ?? result.task?.id,
1962
+ runTaskId: input.runTaskId
1963
+ });
1964
+ return board ? { ...result, board } : result;
1965
+ } catch {
1966
+ return result;
1967
+ }
1968
+ };
1969
+ }
1970
+
1971
+ // src/kanban-serializer.ts
1972
+ var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
1973
+ var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
1974
+ "get_board",
1975
+ "export_markdown",
1976
+ "export_task_graph"
1977
+ ]);
1978
+ function serializeKanbanOutput(output, input) {
1979
+ const action = input && typeof input === "object" ? input.action : void 0;
1980
+ const board = output.board;
1981
+ if (board) {
1982
+ const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
1983
+ let boardBytes = 0;
1984
+ if (!keepFull) {
1985
+ try {
1986
+ boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
1987
+ } catch {
1988
+ boardBytes = 0;
1989
+ }
1990
+ }
1991
+ if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
1992
+ const columns = {};
1993
+ for (const column of board.columns) {
1994
+ columns[column.title || column.id] = board.tasks.filter(
1995
+ (task) => task.columnId === column.id
1996
+ ).length;
1997
+ }
1998
+ const compact = {
1999
+ ...output,
2000
+ board: {
2001
+ id: board.id,
2002
+ title: board.title,
2003
+ columns,
2004
+ totalTasks: board.tasks.length,
2005
+ note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
2006
+ }
2007
+ };
2008
+ return JSON.stringify(compact, null, 2);
2009
+ }
2010
+ }
2011
+ return JSON.stringify(output, null, 2);
2012
+ }
2013
+
1218
2014
  // src/kanban-tool-schema.ts
1219
2015
  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.";
1220
2016
  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.';
@@ -1550,8 +2346,6 @@ var kanbanTool = {
1550
2346
  description: KANBAN_TOOL_DESCRIPTION,
1551
2347
  usageHint: KANBAN_TOOL_USAGE_HINT,
1552
2348
  permission: "confirm",
1553
- // WS-046: gives permission decisions something to key on.
1554
- // The action performed; kanban has no single file or path subject.
1555
2349
  subjectKey: "action",
1556
2350
  mutating: true,
1557
2351
  capabilities: ["fs.write"],
@@ -1566,873 +2360,29 @@ var kanbanTool = {
1566
2360
  const result = await (async () => {
1567
2361
  const decompositionResult = await handleKanbanDecompositionAction(projectRoot, input, ctx);
1568
2362
  if (decompositionResult !== void 0) return decompositionResult;
1569
- switch (input.action) {
1570
- case "list_boards": {
1571
- const boards = await listBoards2(projectRoot);
1572
- return { ok: true, message: `${boards.length} board(s).`, boards };
1573
- }
1574
- case "get_board": {
1575
- const board = await requireBoard(projectRoot, input.boardId);
1576
- return board ? okBoard(board) : fail("Board not found.");
1577
- }
1578
- case "create_board": {
1579
- if (!input.title) return fail("create_board requires title.");
1580
- const existing = (await listBoards2(projectRoot)).filter(
1581
- (candidate) => (candidate.kind ?? "project") === "project"
1582
- );
1583
- const board = await createBoard2(projectRoot, boardCreateInput(input, input.title));
1584
- 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(
1585
- ", "
1586
- )}${existing.length > 3 ? ", \u2026" : ""}. If this work belongs to one of them, add_task there instead and delete this board.` : "";
1587
- return { ok: true, message: `Board created: ${board.title}.${note}`, board };
1588
- }
1589
- case "update_board": {
1590
- if (!input.boardId) return fail("update_board requires boardId.");
1591
- const board = await updateBoard2(projectRoot, input.boardId, boardUpdatePatch(input));
1592
- return board ? okBoard(board, "Board updated.") : fail("Board not found.");
1593
- }
1594
- case "adopt_managed_lifecycle": {
1595
- if (!input.boardId || !input.author || !input.transitionComment) {
1596
- return fail(
1597
- "adopt_managed_lifecycle requires boardId, author, transitionComment, and five ordered columns."
1598
- );
1599
- }
1600
- if (input.columns?.length !== 5) {
1601
- return fail(
1602
- "adopt_managed_lifecycle columns must be ordered as backlog, todo, running, review, done."
1603
- );
1604
- }
1605
- const [backlog, todo, running, review, done] = input.columns;
1606
- if (!backlog || !todo || !running || !review || !done) {
1607
- return fail("adopt_managed_lifecycle columns must contain five nonblank ids.");
1608
- }
1609
- const board = await adoptManagedLifecycle(projectRoot, input.boardId, {
1610
- columns: { backlog, todo, running, review, done },
1611
- actor: input.author,
1612
- comment: input.transitionComment
1613
- });
1614
- return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
1615
- }
1616
- // Adoption used to be a one-way door: the strict lifecycle carries
1617
- // acceptance-criteria, verification-report, review-evidence and
1618
- // one-stage-at-a-time gates, and nothing on the tool surface could
1619
- // undo it, so a board adopted once kept its ceremony forever. The
1620
- // gates are worth having where a fleet is supervised; they are not
1621
- // worth being unable to leave. Cards and columns are untouched.
1622
- case "release_managed_lifecycle": {
1623
- if (!input.boardId) return fail("release_managed_lifecycle requires boardId.");
1624
- const board = await updateBoard2(projectRoot, input.boardId, { lifecycle: null });
1625
- return board ? okBoard(
1626
- board,
1627
- "Managed lifecycle released; the board now tracks work without strict gates."
1628
- ) : fail("Board not found.");
1629
- }
1630
- case "duplicate_board": {
1631
- if (!input.boardId) return fail("duplicate_board requires boardId.");
1632
- const board = await duplicateBoard(
1633
- projectRoot,
1634
- input.boardId,
1635
- duplicateBoardOptions(input)
1636
- );
1637
- return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
1638
- }
1639
- case "delete_board": {
1640
- if (!input.boardId) return fail("delete_board requires boardId.");
1641
- const removed = await removeBoard2(projectRoot, input.boardId);
1642
- return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
1643
- }
1644
- case "generate_board": {
1645
- if (!input.description) return fail("generate_board requires description.");
1646
- const boardInput = createBoardFromText({
1647
- description: input.description,
1648
- ...input.title !== void 0 ? { title: input.title } : {},
1649
- ...input.context !== void 0 ? { context: input.context } : {}
1650
- });
1651
- const board = await createBoard2(projectRoot, boardInput);
1652
- for (const taskInput2 of parseLinesIntoTasks(
1653
- input.description,
1654
- board.columns[0]?.id ?? "backlog"
1655
- )) {
1656
- await addTask(projectRoot, board.id, taskInput2);
1657
- }
1658
- return okBoard(await getBoard3(projectRoot, board.id) ?? board, "Board generated.");
1659
- }
1660
- case "export_markdown": {
1661
- const board = await requireBoard(projectRoot, input.boardId);
1662
- if (!board) return fail("Board not found.");
1663
- return {
1664
- ok: true,
1665
- message: "Board exported.",
1666
- board,
1667
- markdown: exportBoardAsMarkdown(board)
1668
- };
1669
- }
1670
- case "export_task_graph": {
1671
- if (!input.boardId) return fail("export_task_graph requires boardId.");
1672
- const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
1673
- ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
1674
- ...input.specId !== void 0 ? { specId: input.specId } : {},
1675
- ...input.title !== void 0 ? { title: input.title } : {},
1676
- ...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
1677
- ...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
1678
- });
1679
- if (!exported) return fail("Board not found.");
1680
- return {
1681
- ok: true,
1682
- message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
1683
- board: exported.board,
1684
- taskGraph: serializeTaskGraph(exported.graph)
1685
- };
1686
- }
1687
- case "sync_task_graph": {
1688
- if (!input.boardId || !input.taskGraph) {
1689
- return fail("sync_task_graph requires boardId and taskGraph.");
1690
- }
1691
- const graph = deserializeTaskGraph2(input.taskGraph);
1692
- const result2 = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
1693
- ...input.title !== void 0 ? { title: input.title } : {},
1694
- ...input.description !== void 0 ? { description: input.description } : {},
1695
- ...input.tags !== void 0 ? { tags: input.tags } : {},
1696
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
1697
- ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
1698
- ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
1699
- ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
1700
- ...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
1701
- ...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
1702
- });
1703
- return result2 ? {
1704
- ok: true,
1705
- message: `Task graph synced: ${result2.createdTaskIds.length} created, ${result2.updatedTaskIds.length} updated, ${result2.archivedTaskIds.length} archived.`,
1706
- board: result2.board
1707
- } : fail("Board not found.");
1708
- }
1709
- case "create_from_graph": {
1710
- if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
1711
- const graph = deserializeTaskGraph2(input.taskGraph);
1712
- const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
1713
- ...input.title !== void 0 ? { title: input.title } : {},
1714
- ...input.description !== void 0 ? { description: input.description } : {},
1715
- ...input.tags !== void 0 ? { tags: input.tags } : {},
1716
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
1717
- ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
1718
- ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
1719
- ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
1720
- });
1721
- return {
1722
- ok: true,
1723
- message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
1724
- board
1725
- };
1726
- }
1727
- case "import_session_tasks": {
1728
- const taskPath = ctx.meta?.["task.path"];
1729
- if (!taskPath) return fail("No session task file for this session.");
1730
- const file = await loadTasks2(taskPath);
1731
- if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
1732
- const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
1733
- const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
1734
- const tags = ["session", `session:${sessionId}`];
1735
- const existing = (await listBoards2(projectRoot)).find(
1736
- (b) => b.tags?.includes(`session:${sessionId}`)
1737
- );
1738
- if (existing) {
1739
- const result2 = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
1740
- sourceSystem: "session",
1741
- tags,
1742
- archiveMissingTasks: true,
1743
- includeCompletedTasks: true
1744
- });
1745
- return result2 ? {
1746
- ok: true,
1747
- message: `Synced ${file.tasks.length} session tasks into board "${result2.board.title}".`,
1748
- board: result2.board
1749
- } : fail("Session board vanished mid-sync.");
1750
- }
1751
- const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
1752
- title: `Session tasks (${sessionId.slice(0, 8)})`,
1753
- sourceSystem: "session",
1754
- tags
1755
- });
1756
- return {
1757
- ok: true,
1758
- message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
1759
- board
1760
- };
1761
- }
1762
- case "search_tasks": {
1763
- const tasks = await searchKanban(projectRoot, {
1764
- query: input.query,
1765
- boardId: input.boardId,
1766
- assignedAgent: input.agentId,
1767
- status: input.status,
1768
- priority: input.priority,
1769
- label: input.labels?.[0],
1770
- chainId: input.chainId
1771
- });
1772
- return { ok: true, message: `${tasks.length} task(s) matched.`, tasks };
1773
- }
1774
- case "ready_tasks": {
1775
- const tasks = await listReadyTasks(projectRoot, {
1776
- query: input.query,
1777
- boardId: input.boardId,
1778
- assignedAgent: input.agentId,
1779
- priority: input.priority,
1780
- label: input.labels?.[0],
1781
- chainId: input.chainId,
1782
- limit: input.limit
1783
- });
1784
- return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
1785
- }
1786
- case "snapshot": {
1787
- const snapshot = await getKanbanOrchestrationSnapshot2(projectRoot, {
1788
- query: input.query,
1789
- boardId: input.boardId,
1790
- assignedAgent: input.agentId,
1791
- status: input.status,
1792
- priority: input.priority,
1793
- label: input.labels?.[0],
1794
- chainId: input.chainId
1795
- });
1796
- return {
1797
- ok: true,
1798
- message: `${snapshot.ready.length} ready, ${snapshot.running.length} running, ${snapshot.blocked.length} blocked.`,
1799
- snapshot
1800
- };
1801
- }
1802
- case "add_task": {
1803
- if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
1804
- const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
1805
- if (!result2) return fail("Board not found.");
1806
- return okTask(result2.board, result2.task, `Task added.${atomicityNudge(result2.task)}`);
1807
- }
1808
- case "split_task": {
1809
- if (!input.boardId || !input.taskId || !input.childTitles?.length) {
1810
- return fail("split_task requires boardId, taskId, and childTitles.");
1811
- }
1812
- return handleSplitTask(projectRoot, input, {});
1813
- }
1814
- case "merge_tasks": {
1815
- if (!input.boardId || !input.taskIds?.length || !input.title) {
1816
- return fail("merge_tasks requires boardId, taskIds, and title.");
1817
- }
1818
- const result2 = await mergeTasks(projectRoot, input.boardId, {
1819
- taskIds: input.taskIds,
1820
- title: input.title,
1821
- ...input.description !== void 0 ? { description: input.description } : {},
1822
- ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
1823
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
1824
- ...input.closeSourceTasks !== void 0 ? { closeSourceTasks: input.closeSourceTasks } : {}
1825
- });
1826
- return result2 ? okTask(result2.board, result2.task, "Tasks merged.") : fail("Board or task not found.");
1827
- }
1828
- case "copy_task": {
1829
- if (!input.boardId || !input.taskId || !input.targetBoardId) {
1830
- return fail("copy_task requires boardId, taskId, and targetBoardId.");
1831
- }
1832
- const result2 = await copyTaskToBoard(
1833
- projectRoot,
1834
- input.boardId,
1835
- input.taskId,
1836
- input.targetBoardId,
1837
- {
1838
- ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
1839
- ...input.order !== void 0 ? { targetOrder: input.order } : {},
1840
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
1841
- ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
1842
- }
1843
- );
1844
- return result2 ? okTask(result2.targetBoard, result2.task, "Task copied to target board.") : fail("Board or task not found.");
1845
- }
1846
- case "transfer_task": {
1847
- if (!input.boardId || !input.taskId || !input.targetBoardId) {
1848
- return fail("transfer_task requires boardId, taskId, and targetBoardId.");
1849
- }
1850
- const result2 = await transferTaskToBoard(
1851
- projectRoot,
1852
- input.boardId,
1853
- input.taskId,
1854
- input.targetBoardId,
1855
- {
1856
- ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
1857
- ...input.order !== void 0 ? { targetOrder: input.order } : {},
1858
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
1859
- ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
1860
- }
1861
- );
1862
- return result2 ? okTask(result2.targetBoard, result2.task, "Task transferred to target board.") : fail("Board or task not found.");
1863
- }
1864
- case "get_task": {
1865
- if (!input.boardId || !input.taskId)
1866
- return fail("get_task requires boardId and taskId.");
1867
- const task = await getTask(projectRoot, input.boardId, input.taskId);
1868
- return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
1869
- }
1870
- case "start_task": {
1871
- if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
1872
- return fail("start_task requires boardId, taskId, author, and transitionComment.");
1873
- }
1874
- let board = await getBoard3(projectRoot, input.boardId);
1875
- let task = board?.tasks.find((candidate) => candidate.id === input.taskId);
1876
- if (!board || !task) return fail("Board or task not found.");
1877
- const readiness = evaluateContractGraphReadiness(board, task.id);
1878
- if (!readiness.ready) {
1879
- return fail(
1880
- `Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
1881
- );
1882
- }
1883
- if (board.lifecycle?.mode !== "managed") {
1884
- const now = /* @__PURE__ */ new Date();
1885
- const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
1886
- status: "running",
1887
- agentId: input.agentId ?? input.author,
1888
- leaseId: input.leaseId ?? randomUUID2(),
1889
- claimedAt: input.claimedAt ?? now.toISOString(),
1890
- heartbeatAt: input.heartbeatAt ?? now.toISOString(),
1891
- leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
1892
- attempt: input.attempt ?? 1,
1893
- maxAttempts: input.maxAttempts ?? 3
1894
- });
1895
- if (!assigned) return fail("Task assignment could not be started.");
1896
- const started = await updateTask2(projectRoot, board.id, task.id, {
1897
- status: "in_progress"
1898
- });
1899
- const current = started ?? assigned;
1900
- const claimed = task;
1901
- const currentTask = current.tasks.find((candidate) => candidate.id === claimed.id) ?? claimed;
1902
- ctx.setCurrentKanbanTask?.(currentTask.id, current.id);
1903
- return okTask(
1904
- current,
1905
- currentTask,
1906
- "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."
1907
- );
1908
- }
1909
- let stage = task.lifecycle?.currentStage;
1910
- if (stage === "backlog") {
1911
- const moved = await transitionTask(projectRoot, board.id, task.id, {
1912
- to: "todo",
1913
- actor: input.author,
1914
- comment: input.transitionComment
1915
- });
1916
- if (!moved) return fail("Task could not enter Todo.");
1917
- board = moved.board;
1918
- task = moved.task;
1919
- stage = task.lifecycle?.currentStage;
1920
- }
1921
- if (stage === "todo" || stage === "review") {
1922
- const now = /* @__PURE__ */ new Date();
1923
- const leaseId = input.leaseId ?? randomUUID2();
1924
- const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
1925
- status: "running",
1926
- agentId: input.agentId ?? input.author,
1927
- leaseId,
1928
- claimedAt: input.claimedAt ?? now.toISOString(),
1929
- heartbeatAt: input.heartbeatAt ?? now.toISOString(),
1930
- leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
1931
- attempt: input.attempt ?? 1,
1932
- maxAttempts: input.maxAttempts ?? 3
1933
- });
1934
- if (!assigned) return fail("Task assignment could not be started.");
1935
- const moved = await transitionTask(projectRoot, board.id, task.id, {
1936
- to: "running",
1937
- actor: input.author,
1938
- comment: input.transitionComment
1939
- });
1940
- if (!moved) return fail("Task could not enter Running.");
1941
- board = moved.board;
1942
- task = moved.task;
1943
- stage = task.lifecycle?.currentStage;
1944
- }
1945
- if (stage !== "running" || task.assignment?.status !== "running") {
1946
- return fail(
1947
- `start_task only accepts Backlog, Todo, Review repair, or live Running cards (current: ${stage ?? "unknown"}).`
1948
- );
1949
- }
1950
- ctx.setCurrentKanbanTask(task.id, board.id);
1951
- return okTask(
1952
- board,
1953
- task,
1954
- "Task is active; runtime Kanban governance is now bound to this run."
1955
- );
1956
- }
1957
- case "update_task": {
1958
- if (!input.boardId || !input.taskId)
1959
- return fail("update_task requires boardId and taskId.");
1960
- const board = await updateTask2(
1961
- projectRoot,
1962
- input.boardId,
1963
- input.taskId,
1964
- taskPatch(input)
1965
- );
1966
- return board ? okBoard(board, "Task updated.") : fail("Task not found.");
1967
- }
1968
- case "transition_task": {
1969
- if (!input.boardId || !input.taskId || !input.lifecycleStage || !input.author || !input.transitionComment) {
1970
- return fail(
1971
- "transition_task requires boardId, taskId, lifecycleStage, author, and transitionComment."
1972
- );
1973
- }
1974
- if (input.lifecycleStage === "done") {
1975
- const boardBefore = await getBoard3(projectRoot, input.boardId);
1976
- const taskBefore = boardBefore ? await getTask(projectRoot, input.boardId, input.taskId) : null;
1977
- if (boardBefore && taskBefore && !taskBefore.verificationReport && (taskBefore.atomic || Boolean(taskBefore.successCriteria?.length))) {
1978
- const preGate = await verifyTaskCompletion2(
1979
- projectRoot,
1980
- input.boardId,
1981
- taskBefore.id,
1982
- {
1983
- persist: false
1984
- }
1985
- );
1986
- await updateTask2(projectRoot, input.boardId, taskBefore.id, {
1987
- verificationReport: preGate.report,
1988
- successCriteria: preGate.task.successCriteria
1989
- });
1990
- }
1991
- }
1992
- const result2 = await transitionTask(projectRoot, input.boardId, input.taskId, {
1993
- to: input.lifecycleStage,
1994
- actor: input.author,
1995
- comment: input.transitionComment,
1996
- ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
1997
- ...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
1998
- ...input.attachmentUrl !== void 0 ? {
1999
- attachment: {
2000
- url: input.attachmentUrl,
2001
- type: input.attachmentType ?? "url",
2002
- ...input.attachmentTitle !== void 0 ? { title: input.attachmentTitle } : {}
2003
- }
2004
- } : {},
2005
- patch: taskPatch(input)
2006
- });
2007
- if (result2 && input.lifecycleStage === "done" && result2.task.verificationReport) {
2008
- recordKanbanVerificationEvidence(ctx, result2.task.verificationReport);
2009
- }
2010
- return result2 ? okTask(result2.board, result2.task, `Task advanced to ${result2.transition.to}.`) : fail("Board or task not found.");
2011
- }
2012
- case "repair_managed_projection": {
2013
- if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
2014
- return fail(
2015
- "repair_managed_projection requires boardId, taskId, author, and transitionComment."
2016
- );
2017
- }
2018
- const result2 = await repairManagedTaskProjection(
2019
- projectRoot,
2020
- input.boardId,
2021
- input.taskId,
2022
- {
2023
- actor: input.author,
2024
- comment: input.transitionComment
2025
- }
2026
- );
2027
- return result2 ? okTask(
2028
- result2.board,
2029
- result2.task,
2030
- "Managed card projection repaired from lifecycle history."
2031
- ) : fail("Board or task not found.");
2032
- }
2033
- case "move_task": {
2034
- if (!input.boardId || !input.taskId || !input.targetColumnId) {
2035
- return fail("move_task requires boardId, taskId, and targetColumnId.");
2036
- }
2037
- const board = await moveTask(
2038
- projectRoot,
2039
- input.boardId,
2040
- input.taskId,
2041
- input.targetColumnId,
2042
- input.order
2043
- );
2044
- return board ? okBoard(board, "Task moved.") : fail("Move failed.");
2045
- }
2046
- case "delete_task": {
2047
- if (!input.boardId || !input.taskId)
2048
- return fail("delete_task requires boardId and taskId.");
2049
- const board = await removeTask(projectRoot, input.boardId, input.taskId);
2050
- if (board && ctx.currentKanbanTaskId === input.taskId) {
2051
- ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
2052
- }
2053
- return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
2054
- }
2055
- case "set_chain": {
2056
- if (!input.boardId || !input.taskIds?.length) {
2057
- return fail("set_chain requires boardId and taskIds.");
2058
- }
2059
- const result2 = await setTaskChain(projectRoot, input.boardId, {
2060
- taskIds: input.taskIds,
2061
- ...input.chainId !== void 0 ? { chainId: input.chainId } : {},
2062
- ...input.enforceDependencies !== void 0 ? { enforceDependencies: input.enforceDependencies } : {}
2063
- });
2064
- return result2 ? {
2065
- ok: true,
2066
- message: `Chain set: ${result2.chainId}`,
2067
- board: result2.board,
2068
- chain: result2.tasks
2069
- } : fail("Board or task not found.");
2070
- }
2071
- case "get_chain": {
2072
- if (!input.boardId || !(input.taskId || input.chainId)) {
2073
- return fail("get_chain requires boardId and taskId or chainId.");
2074
- }
2075
- const result2 = await getTaskChain(
2076
- projectRoot,
2077
- input.boardId,
2078
- input.taskId ?? input.chainId ?? ""
2079
- );
2080
- return result2 ? {
2081
- ok: true,
2082
- message: `Chain loaded: ${result2.chainId}`,
2083
- board: result2.board,
2084
- chain: result2.tasks
2085
- } : fail("Chain not found.");
2086
- }
2087
- case "claim_task": {
2088
- const result2 = await claimReadyTask(projectRoot, {
2089
- ...input.boardId !== void 0 ? { boardId: input.boardId } : {},
2090
- ...input.taskId !== void 0 ? { taskId: input.taskId } : {},
2091
- ...assignmentInput(input),
2092
- status: input.assignmentStatus ?? "queued"
2093
- });
2094
- return result2 ? okTask(result2.board, result2.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
2095
- }
2096
- case "release_task": {
2097
- if (!input.boardId || !input.taskId) {
2098
- return fail("release_task requires boardId and taskId.");
2099
- }
2100
- const board = await releaseTaskClaim(projectRoot, input.boardId, input.taskId, {
2101
- ...input.releaseStatus !== void 0 ? { status: input.releaseStatus } : {},
2102
- ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
2103
- ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {}
2104
- });
2105
- return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
2106
- }
2107
- case "assign_task": {
2108
- if (!input.boardId || !input.taskId)
2109
- return fail("assign_task requires boardId and taskId.");
2110
- const board = await assignTask(
2111
- projectRoot,
2112
- input.boardId,
2113
- input.taskId,
2114
- assignmentInput(input)
2115
- );
2116
- return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
2117
- }
2118
- case "mark_assignment": {
2119
- if (!input.boardId || !input.taskId)
2120
- return fail("mark_assignment requires boardId and taskId.");
2121
- const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
2122
- const board = await updateTaskAssignment(
2123
- projectRoot,
2124
- input.boardId,
2125
- input.taskId,
2126
- {
2127
- ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
2128
- ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
2129
- ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
2130
- ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
2131
- ...input.error !== void 0 ? { error: input.error } : {},
2132
- ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
2133
- ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
2134
- ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
2135
- ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
2136
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
2137
- ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
2138
- ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
2139
- },
2140
- // Ownership fence: when expectedLeaseId is supplied, the write is
2141
- // applied only if the current assignment still holds this lease.
2142
- // This prevents a recovered+reassigned stale worker's terminal
2143
- // mark_assignment from overwriting the successor's state. The check
2144
- // is atomic inside updateTaskAssignment's mutateBoard lock.
2145
- input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
2146
- );
2147
- if (!board) return fail("Task not found.");
2148
- if (assignmentStatus === "completed" && board.lifecycle?.mode !== "managed") {
2149
- const envGate = readEnvGateEnforcement();
2150
- const finalized = await finalizeTaskCompletion(projectRoot, board.id, input.taskId, {
2151
- ...board.completionGate === void 0 && envGate !== void 0 ? { enforcement: envGate } : {},
2152
- ...ctx.agentId !== void 0 ? { eventContext: { actor: ctx.agentId } } : {}
2153
- });
2154
- if (finalized) {
2155
- if (finalized.gate.report) {
2156
- recordKanbanVerificationEvidence(ctx, finalized.gate.report);
2157
- }
2158
- const gateSummary = {
2159
- enforcement: finalized.gate.enforcement,
2160
- allowed: finalized.gate.allowed,
2161
- verdict: finalized.gate.verdict,
2162
- issues: finalized.gate.issues.map((issue) => issue.message)
2163
- };
2164
- const gateMessage = finalized.gate.allowed ? `Completion gate ${finalized.gate.verdict === "skipped" ? "skipped" : "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(" | ")}`;
2165
- return {
2166
- ...okTask(finalized.board, finalized.task, `Assignment updated. ${gateMessage}`),
2167
- gate: gateSummary
2168
- };
2169
- }
2170
- } else if (board.lifecycle?.mode === "managed") {
2171
- const managedTask = board.tasks.find((candidate) => candidate.id === input.taskId);
2172
- const stage = managedTask?.lifecycle?.currentStage;
2173
- const actor = ctx.agentId ?? "kanban-agent";
2174
- let transitionResult = null;
2175
- const lifecycleWarnings = [];
2176
- if (assignmentStatus === "running" && stage === "todo") {
2177
- try {
2178
- transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
2179
- to: "running",
2180
- actor,
2181
- comment: "Work started."
2182
- });
2183
- } catch (err) {
2184
- lifecycleWarnings.push(
2185
- `Lifecycle transition to Running deferred: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
2186
- );
2187
- }
2188
- }
2189
- if (assignmentStatus === "completed" && stage === "running") {
2190
- const comment = typeof input.lastResult === "string" && input.lastResult.trim().length > 0 ? input.lastResult.trim().slice(0, 1e3) : "Work completed.";
2191
- try {
2192
- transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
2193
- to: "review",
2194
- actor,
2195
- comment,
2196
- attachment: {
2197
- url: `kanban://task/${input.taskId}/result`,
2198
- title: "Worker completion result",
2199
- type: "file"
2200
- },
2201
- patch: {
2202
- // Only patch non-description fields so the
2203
- // original card description is preserved.
2204
- ...input.agentId !== void 0 ? { assignedAgent: input.agentId } : {}
2205
- }
2206
- });
2207
- } catch (err) {
2208
- lifecycleWarnings.push(
2209
- `Lifecycle transition to Review failed: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
2210
- );
2211
- }
2212
- if (transitionResult) {
2213
- const hasCriteria = (transitionResult.task.successCriteria?.length ?? 0) > 0 || transitionResult.task.atomic === true;
2214
- if (hasCriteria) {
2215
- try {
2216
- const verResult = await verifyTaskCompletion2(
2217
- projectRoot,
2218
- board.id,
2219
- input.taskId
2220
- );
2221
- if (verResult.report) {
2222
- recordKanbanVerificationEvidence(ctx, verResult.report);
2223
- }
2224
- await updateTask2(projectRoot, board.id, input.taskId, {
2225
- verificationReport: verResult.report,
2226
- successCriteria: verResult.task.successCriteria
2227
- });
2228
- const verdict = verResult.report.verdict;
2229
- if (verdict === "passed" && !resolveAutoAccept(board)) {
2230
- lifecycleWarnings.push(
2231
- "Verification passed, but this board does not auto-accept. The card is in Review awaiting an explicit transition_task to done."
2232
- );
2233
- } else if (verdict === "passed") {
2234
- try {
2235
- const doneResult = await transitionTask(
2236
- projectRoot,
2237
- board.id,
2238
- input.taskId,
2239
- {
2240
- to: "done",
2241
- actor,
2242
- action: "Automated acceptance after verification",
2243
- comment: "Auto-accepted: verification passed.",
2244
- attachment: {
2245
- url: `kanban://task/${input.taskId}/verification`,
2246
- title: "Auto-verification result",
2247
- type: "file"
2248
- }
2249
- }
2250
- );
2251
- transitionResult = doneResult;
2252
- } catch (acceptErr) {
2253
- lifecycleWarnings.push(
2254
- `Auto-accept to Done deferred: ${acceptErr instanceof Error ? acceptErr.message : String(acceptErr)}`
2255
- );
2256
- }
2257
- } else {
2258
- lifecycleWarnings.push(
2259
- `Verification verdict: ${verdict} \u2014 card left in Review for manual acceptance.`
2260
- );
2261
- }
2262
- } catch (verifyErr) {
2263
- lifecycleWarnings.push(
2264
- `Auto-verification error: ${verifyErr instanceof Error ? verifyErr.message : String(verifyErr)}`
2265
- );
2266
- }
2267
- } else {
2268
- lifecycleWarnings.push(
2269
- "No automatic success criteria \u2014 card left in Review for manual verification."
2270
- );
2271
- }
2272
- }
2273
- }
2274
- const responseBoard = transitionResult?.board ?? board;
2275
- const responseTask = transitionResult?.task ?? managedTask;
2276
- const msgParts = ["Assignment updated."];
2277
- if (transitionResult) {
2278
- msgParts.push(`Card advanced to ${transitionResult.transition.to}.`);
2279
- }
2280
- for (const w of lifecycleWarnings) msgParts.push(`Warning: ${w}`);
2281
- return okTask(responseBoard, responseTask, msgParts.join(" "));
2282
- }
2283
- return okBoard(board, "Assignment updated.");
2284
- }
2285
- case "heartbeat_assignment": {
2286
- if (!input.boardId || !input.taskId) {
2287
- return fail("heartbeat_assignment requires boardId and taskId.");
2288
- }
2289
- const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
2290
- ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
2291
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
2292
- // Ownership fence: when expectedLeaseId is supplied, the renewal
2293
- // is applied only if the current assignment still holds this lease.
2294
- // This prevents a recovered+reassigned stale worker's heartbeat
2295
- // from renewing the successor's lease. The check is atomic inside
2296
- // heartbeatTaskAssignment's mutateBoard lock.
2297
- ...input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
2298
- });
2299
- return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
2300
- }
2301
- case "recover_stale": {
2302
- if (!input.boardId) return fail("recover_stale requires boardId.");
2303
- const policyFields = [
2304
- input.recoveryPolicyFailOnCostCeiling !== void 0,
2305
- input.recoveryPolicyReleaseOnFailureKinds !== void 0,
2306
- input.recoveryPolicyReleaseOnHeartbeatDue !== void 0,
2307
- input.recoveryPolicyRetryPolicyOverride !== void 0
2308
- ].some(Boolean);
2309
- const result2 = await recoverStaleTaskAssignments(projectRoot, input.boardId, {
2310
- ...input.recoveryMode !== void 0 ? { mode: input.recoveryMode } : {},
2311
- ...input.recoveryNow !== void 0 ? { now: input.recoveryNow } : {},
2312
- ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
2313
- ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {},
2314
- ...policyFields ? {
2315
- policy: {
2316
- ...input.recoveryPolicyFailOnCostCeiling !== void 0 ? { failWhenCostCeilingSet: input.recoveryPolicyFailOnCostCeiling } : {},
2317
- ...input.recoveryPolicyReleaseOnFailureKinds !== void 0 ? {
2318
- releaseOnFailureKinds: input.recoveryPolicyReleaseOnFailureKinds
2319
- } : {},
2320
- ...input.recoveryPolicyReleaseOnHeartbeatDue !== void 0 ? {
2321
- releaseOnHeartbeatDue: input.recoveryPolicyReleaseOnHeartbeatDue
2322
- } : {},
2323
- ...input.recoveryPolicyRetryPolicyOverride !== void 0 ? {
2324
- retryPolicyOverride: input.recoveryPolicyRetryPolicyOverride
2325
- } : {}
2326
- }
2327
- } : {}
2328
- });
2329
- return result2 ? {
2330
- ok: true,
2331
- message: `Recovered ${result2.tasks.length} stale assignment(s).`,
2332
- board: result2.board,
2333
- recoveredTasks: result2.tasks
2334
- } : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
2335
- }
2336
- case "events": {
2337
- if (!input.boardId) return fail("events requires boardId.");
2338
- const eventList = await listKanbanEvents(projectRoot, input.boardId);
2339
- return {
2340
- ok: true,
2341
- message: `${eventList.length} event(s).`,
2342
- events: eventList
2343
- };
2344
- }
2345
- case "queue_health": {
2346
- const health = await getKanbanQueueHealth(projectRoot, {
2347
- ...input.boardId !== void 0 ? { boardId: input.boardId } : {}
2348
- });
2349
- return {
2350
- ok: true,
2351
- message: `Counts: startable=${health.counts.startable}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
2352
- queueHealth: health
2353
- };
2354
- }
2355
- // Not every action is handled above. These are dispatched from here,
2356
- // and the split has already cost real time: an agent that read this
2357
- // file concluded `add_check` / `update_check` did not exist, wrote
2358
- // that on a card, and spent a session trying to satisfy a gate it
2359
- // already had the tool to clear. Keep this index in step with the
2360
- // handlers.
2361
- //
2362
- // kanban-detail-actions.ts workbench · add_dependency ·
2363
- // add_goal_metric · update_goal_metric · add_check ·
2364
- // update_check · add_note · add_link · split_atomic
2365
- // kanban-decomposition-actions.ts verify_completion ·
2366
- // assess_atomicity · propose_decomposition
2367
- // kanban-contract-actions.ts get_contract_graph ·
2368
- // configure_contract_graph · upsert_contract_node ·
2369
- // remove_contract_node · add_contract_edge · remove_contract_edge
2370
- default:
2371
- {
2372
- const contractResult = await handleKanbanContractAction(
2373
- projectRoot,
2374
- input,
2375
- input.author ?? input.agentId
2376
- );
2377
- if (contractResult !== void 0) return contractResult;
2378
- }
2379
- {
2380
- const detailResult = await handleKanbanDetailAction(projectRoot, input);
2381
- if (detailResult !== void 0) return detailResult;
2382
- }
2383
- return fail(`Unknown kanban action: ${input.action}`);
2384
- }
2363
+ const boardResult = await handleKanbanBoardAction(projectRoot, input, ctx);
2364
+ if (boardResult !== void 0) return boardResult;
2365
+ const lifecycleResult = await handleKanbanLifecycleAction(projectRoot, input, ctx);
2366
+ if (lifecycleResult !== void 0) return lifecycleResult;
2367
+ const contractResult = await handleKanbanContractAction(
2368
+ projectRoot,
2369
+ input,
2370
+ input.author ?? input.agentId
2371
+ );
2372
+ if (contractResult !== void 0) return contractResult;
2373
+ const detailResult = await handleKanbanDetailAction(projectRoot, input);
2374
+ if (detailResult !== void 0) return detailResult;
2375
+ return fail(`Unknown kanban action: ${input.action}`);
2385
2376
  })();
2386
2377
  return withPresence(result);
2387
2378
  } catch (err) {
2388
- return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
2379
+ return fail(stripLifecycleIssues2(err instanceof Error ? err.message : String(err)));
2389
2380
  }
2390
2381
  },
2391
2382
  serialize(output, input) {
2392
2383
  return serializeKanbanOutput(output, input);
2393
2384
  }
2394
2385
  };
2395
- var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
2396
- var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
2397
- "get_board",
2398
- "export_markdown",
2399
- "export_task_graph"
2400
- ]);
2401
- function serializeKanbanOutput(output, input) {
2402
- const action = input && typeof input === "object" ? input.action : void 0;
2403
- const board = output.board;
2404
- if (board) {
2405
- const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
2406
- let boardBytes = 0;
2407
- if (!keepFull) {
2408
- try {
2409
- boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
2410
- } catch {
2411
- boardBytes = 0;
2412
- }
2413
- }
2414
- if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
2415
- const columns = {};
2416
- for (const column of board.columns) {
2417
- columns[column.title || column.id] = board.tasks.filter(
2418
- (task) => task.columnId === column.id
2419
- ).length;
2420
- }
2421
- const compact = {
2422
- ...output,
2423
- board: {
2424
- id: board.id,
2425
- title: board.title,
2426
- columns,
2427
- totalTasks: board.tasks.length,
2428
- note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
2429
- }
2430
- };
2431
- return JSON.stringify(compact, null, 2);
2432
- }
2433
- }
2434
- return JSON.stringify(output, null, 2);
2435
- }
2436
2386
 
2437
2387
  // src/todo.ts
2438
2388
  function normalizedTitle(value) {
@@ -2490,7 +2440,7 @@ async function createMissingManagedCards(items, board, ctx, warnings) {
2490
2440
  for (const item of items) {
2491
2441
  if (item.kanbanBoardId === board.id && item.kanbanTaskId) continue;
2492
2442
  try {
2493
- const result = await addTask2(ctx.projectRoot, board.id, {
2443
+ const result = await addTask3(ctx.projectRoot, board.id, {
2494
2444
  title: item.content,
2495
2445
  description: item.activeForm?.trim() || `Added from the session todo list: ${item.content}`
2496
2446
  });
@@ -2584,7 +2534,7 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
2584
2534
  });
2585
2535
  }
2586
2536
  const attemptedParents = /* @__PURE__ */ new Set();
2587
- let afterCompletions = await getBoard4(ctx.projectRoot, board.id);
2537
+ let afterCompletions = await getBoard5(ctx.projectRoot, board.id);
2588
2538
  while (afterCompletions) {
2589
2539
  const parent = afterCompletions.tasks.find(
2590
2540
  (task) => task.atomic === true && task.status !== "completed" && Boolean(task.childTaskIds?.length) && !attemptedParents.has(task.id) && task.childTaskIds?.every(
@@ -2611,7 +2561,7 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
2611
2561
  lastResult: "All child tasks completed; composite result ready for verification."
2612
2562
  });
2613
2563
  }
2614
- afterCompletions = await getBoard4(ctx.projectRoot, board.id);
2564
+ afterCompletions = await getBoard5(ctx.projectRoot, board.id);
2615
2565
  }
2616
2566
  const completionPending = items.some(
2617
2567
  (item) => item.status === "completed" && item.kanbanBoardId === board.id && Boolean(item.kanbanTaskId) && afterCompletions?.tasks.find((task) => task.id === item.kanbanTaskId)?.status !== "completed"
@@ -2724,7 +2674,7 @@ var todoTool = {
2724
2674
  }
2725
2675
  }
2726
2676
  const boardId = activeBoardId(items, ctx);
2727
- let board = boardId ? await getBoard4(ctx.projectRoot, boardId) : null;
2677
+ let board = boardId ? await getBoard5(ctx.projectRoot, boardId) : null;
2728
2678
  const managed = board?.lifecycle?.mode === "managed";
2729
2679
  let boundItems = managed && board ? bindTodosToBoard(items, ctx.todos ?? [], board) : items;
2730
2680
  const creationWarnings = [];
@@ -2736,7 +2686,7 @@ var todoTool = {
2736
2686
  const taskId = created.get(item.id);
2737
2687
  return taskId ? { ...item, kanbanBoardId: managedBoardId, kanbanTaskId: taskId } : item;
2738
2688
  });
2739
- board = await getBoard4(ctx.projectRoot, managedBoardId) ?? board;
2689
+ board = await getBoard5(ctx.projectRoot, managedBoardId) ?? board;
2740
2690
  boundItems = bindTodosToBoard(boundItems, ctx.todos ?? [], board);
2741
2691
  }
2742
2692
  boundItems = demoteBlockedInProgress(boundItems, creationWarnings);
@@ -2758,10 +2708,10 @@ var todoTool = {
2758
2708
  if (mirrorFailure) kanbanSync.warnings.push(mirrorFailure);
2759
2709
  let projectedBoard = board;
2760
2710
  if (managed && board) {
2761
- const refreshed = await getBoard4(ctx.projectRoot, board.id);
2711
+ const refreshed = await getBoard5(ctx.projectRoot, board.id);
2762
2712
  if (refreshed) {
2763
2713
  projectedBoard = refreshed;
2764
- applyManagedKanbanBoardToTodos(ctx, refreshed);
2714
+ applyManagedKanbanBoardToTodos2(ctx, refreshed);
2765
2715
  }
2766
2716
  }
2767
2717
  if (!managed) {