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