@wrongstack/tools 0.306.4 → 0.307.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) 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 +11469 -9304
  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 +42 -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 +4239 -1492
  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 +819 -933
  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 +790 -914
  26. package/dist/codebase-index/writer-graph-helpers.d.ts +1 -1
  27. package/dist/codebase-index/writer-helpers.d.ts +12 -0
  28. package/dist/codebase-index/writer-mutations.d.ts +26 -0
  29. package/dist/codebase-index/writer-refs.d.ts +50 -0
  30. package/dist/codebase-index/writer-search.d.ts +30 -0
  31. package/dist/codebase-index/writer.d.ts +2 -320
  32. package/dist/edit.js +8602 -349
  33. package/dist/exec.js +73 -20
  34. package/dist/fetch.js +11 -9
  35. package/dist/format.js +14 -8
  36. package/dist/glob.js +5 -1
  37. package/dist/grep.js +19 -8
  38. package/dist/index.d.ts +3 -1
  39. package/dist/index.js +6178 -4037
  40. package/dist/install.js +14 -8
  41. package/dist/json.js +88 -15
  42. package/dist/kanban-board-actions.d.ts +4 -0
  43. package/dist/kanban-lifecycle-actions.d.ts +4 -0
  44. package/dist/kanban-serializer.d.ts +21 -0
  45. package/dist/kanban.js +985 -1034
  46. package/dist/languages/index.js +14 -8
  47. package/dist/lint.js +14 -8
  48. package/dist/logs.js +13 -3
  49. package/dist/next-steps.d.ts +8 -0
  50. package/dist/next-steps.js +18 -0
  51. package/dist/outdated.js +14 -8
  52. package/dist/pack.js +11461 -9304
  53. package/dist/patch.js +8345 -78
  54. package/dist/plan.js +1231 -1281
  55. package/dist/process-registry.js +14 -8
  56. package/dist/ps-slash.js +65 -30
  57. package/dist/read.js +822 -937
  58. package/dist/replace.js +8494 -224
  59. package/dist/search.js +21 -15
  60. package/dist/security-ast-scan-tool.d.ts +43 -0
  61. package/dist/session-kanban-graph.d.ts +9 -0
  62. package/dist/session-kanban-sync.d.ts +31 -0
  63. package/dist/session-kanban.d.ts +5 -141
  64. package/dist/session-kanban.js +367 -360
  65. package/dist/task.js +1157 -1207
  66. package/dist/test.js +14 -8
  67. package/dist/todo.js +2187 -2237
  68. package/dist/tool-diff.js +6 -1
  69. package/dist/tool-summary.js +4 -2
  70. package/dist/tool-tier.js +11469 -9304
  71. package/dist/typecheck.js +14 -8
  72. package/dist/write.js +8425 -160
  73. package/package.json +7 -7
package/dist/todo.js CHANGED
@@ -6,54 +6,34 @@ import {
6
6
  saveTasks,
7
7
  setPlanItemStatus
8
8
  } from "@wrongstack/core/storage";
9
- import { addTask as addTask2, getBoard as getBoard4 } from "@wrongstack/kanban";
9
+ import { addTask as addTask3, getBoard as getBoard5 } from "@wrongstack/kanban";
10
10
 
11
11
  // src/kanban.ts
12
- import { randomUUID as randomUUID2 } from "node:crypto";
12
+ import { stripLifecycleIssues as stripLifecycleIssues2 } from "@wrongstack/kanban";
13
+
14
+ // src/kanban-board-actions.ts
13
15
  import { loadTasks as loadTasks2 } from "@wrongstack/core/storage";
14
16
  import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
15
17
  import {
16
18
  addTask,
17
19
  adoptManagedLifecycle,
18
- assignTask,
19
- claimReadyTask,
20
- copyTaskToBoard,
21
20
  createBoard as createBoard2,
22
21
  createBoardFromTaskGraph,
23
22
  createBoardFromText,
24
23
  duplicateBoard,
25
- evaluateContractGraphReadiness,
26
24
  exportBoardAsMarkdown,
27
25
  exportBoardToTaskGraph,
28
- finalizeTaskCompletion,
29
26
  getBoard as getBoard3,
30
27
  getKanbanOrchestrationSnapshot as getKanbanOrchestrationSnapshot2,
31
28
  getKanbanQueueHealth,
32
- getTask,
33
- getTaskChain,
34
- heartbeatTaskAssignment,
35
29
  listBoards as listBoards2,
36
30
  listKanbanEvents,
37
31
  listReadyTasks,
38
- mergeTasks,
39
- moveTask,
40
32
  parseLinesIntoTasks,
41
- recoverStaleTaskAssignments,
42
- releaseTaskClaim,
43
33
  removeBoard as removeBoard2,
44
- removeTask,
45
- repairManagedTaskProjection,
46
- resolveAutoAccept,
47
34
  searchKanban,
48
- setTaskChain,
49
- stripLifecycleIssues,
50
35
  syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
51
- transferTaskToBoard,
52
- transitionTask,
53
- updateBoard as updateBoard2,
54
- updateTask as updateTask2,
55
- updateTaskAssignment,
56
- verifyTaskCompletion as verifyTaskCompletion2
36
+ updateBoard as updateBoard2
57
37
  } from "@wrongstack/kanban";
58
38
 
59
39
  // src/kanban-board-inputs.ts
@@ -100,16 +80,8 @@ function duplicateBoardOptions(input) {
100
80
  };
101
81
  }
102
82
 
103
- // src/kanban-contract-actions.ts
104
- import {
105
- addContractEdge,
106
- configureContractGraph,
107
- evaluateTaskContractGraph,
108
- getContractGraph,
109
- removeContractEdge,
110
- removeContractNode,
111
- upsertContractNode
112
- } from "@wrongstack/kanban";
83
+ // src/kanban-split-task-handler.ts
84
+ import { getBoard, splitTask } from "@wrongstack/kanban";
113
85
 
114
86
  // src/kanban-tool-results.ts
115
87
  function atomicityNudge(task) {
@@ -131,1404 +103,2228 @@ function okTask(board, task, message) {
131
103
  return { ok: true, message, board, task };
132
104
  }
133
105
 
134
- // src/kanban-contract-actions.ts
135
- async function handleKanbanContractAction(projectRoot, input, actor) {
136
- switch (input.action) {
137
- case "get_contract_graph": {
138
- if (!input.boardId) return fail("get_contract_graph requires boardId.");
139
- const found = await getContractGraph(projectRoot, input.boardId);
140
- if (!found) return fail("Board not found.");
141
- const evaluated = input.taskId ? await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId) : null;
142
- if (input.taskId && !evaluated) return fail("Task not found on this board.");
143
- return {
144
- ok: true,
145
- message: found.graph ? `Contract map: ${found.graph.nodes.length} node(s), ${found.graph.edges.length} edge(s), enforcement ${found.graph.enforcement}.` : "No contract map on this board yet. Call configure_contract_graph to start one.",
146
- board: found.board,
147
- contractGraph: found.graph,
148
- ...evaluated ? { contractEvaluation: evaluated.evaluation } : {}
149
- };
150
- }
151
- case "configure_contract_graph": {
152
- if (!input.boardId) return fail("configure_contract_graph requires boardId.");
153
- const enforcement = input.contractEnforcement ?? "advisory";
154
- const board = await configureContractGraph(projectRoot, input.boardId, enforcement);
155
- return board ? okBoard(board, `Contract map enforcement set to ${enforcement}.`) : fail("Board not found.");
156
- }
157
- case "upsert_contract_node": {
158
- if (!input.boardId || !input.taskId) {
159
- return fail("upsert_contract_node requires boardId and taskId.");
160
- }
161
- if (!input.contractNodeKind || !input.contractNodeTitle) {
162
- return fail("upsert_contract_node requires contractNodeKind and contractNodeTitle.");
163
- }
164
- const waiver = input.contractNodeState === "waived" ? {
165
- actor: actor ?? "agent",
166
- reason: input.contractWaiverReason ?? "",
167
- at: (/* @__PURE__ */ new Date()).toISOString()
168
- } : void 0;
169
- if (waiver && !waiver.reason.trim()) {
170
- return fail("A waived contract node requires contractWaiverReason.");
171
- }
172
- const result = await upsertContractNode(projectRoot, input.boardId, {
173
- taskId: input.taskId,
174
- kind: input.contractNodeKind,
175
- title: input.contractNodeTitle,
176
- ...input.contractNodeId !== void 0 ? { id: input.contractNodeId } : {},
177
- ...input.contractNodeDescription !== void 0 ? { description: input.contractNodeDescription } : {},
178
- ...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
179
- ...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
180
- ...input.contractCheckId !== void 0 ? { checkId: input.contractCheckId } : {},
181
- ...input.contractMetricId !== void 0 ? { metricId: input.contractMetricId } : {},
182
- ...waiver ? { waiver } : {},
183
- ...actor !== void 0 ? { createdBy: actor } : {}
184
- });
185
- if (!result) return fail("Board or task not found.");
186
- return {
187
- ok: true,
188
- message: `Contract node ${result.node.kind} "${result.node.title}" saved (${result.node.id}).`,
189
- board: result.board,
190
- contractGraph: result.board.contractGraph ?? null
191
- };
192
- }
193
- case "remove_contract_node": {
194
- if (!input.boardId || !input.contractNodeId) {
195
- return fail("remove_contract_node requires boardId and contractNodeId.");
196
- }
197
- const board = await removeContractNode(projectRoot, input.boardId, input.contractNodeId);
198
- return board ? okBoard(board, "Contract node removed, along with every edge that touched it.") : fail("Contract node not found.");
199
- }
200
- case "add_contract_edge": {
201
- if (!input.boardId || !input.contractEdgeFrom || !input.contractEdgeTo) {
202
- return fail("add_contract_edge requires boardId, contractEdgeFrom, and contractEdgeTo.");
203
- }
204
- if (!input.contractEdgeType) return fail("add_contract_edge requires contractEdgeType.");
205
- const result = await addContractEdge(projectRoot, input.boardId, {
206
- from: input.contractEdgeFrom,
207
- to: input.contractEdgeTo,
208
- type: input.contractEdgeType,
209
- ...input.contractEdgeId !== void 0 ? { id: input.contractEdgeId } : {},
210
- ...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
211
- ...input.contractEdgeRationale !== void 0 ? { rationale: input.contractEdgeRationale } : {},
212
- ...actor !== void 0 ? { createdBy: actor } : {}
213
- });
214
- if (!result) return fail("Board not found.");
215
- return {
216
- ok: true,
217
- message: `Contract edge ${result.edge.type}: ${result.edge.from} \u2192 ${result.edge.to}.`,
218
- board: result.board,
219
- contractGraph: result.board.contractGraph ?? null
220
- };
221
- }
222
- case "remove_contract_edge": {
223
- if (!input.boardId || !input.contractEdgeId) {
224
- return fail("remove_contract_edge requires boardId and contractEdgeId.");
225
- }
226
- const board = await removeContractEdge(projectRoot, input.boardId, input.contractEdgeId);
227
- return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
228
- }
229
- default:
230
- return void 0;
106
+ // src/kanban-split-task-handler.ts
107
+ async function handleSplitTask(projectRoot, input, extraSplitOptions) {
108
+ const boardId = input.boardId;
109
+ const taskId = input.taskId;
110
+ const childTitles = input.childTitles;
111
+ if (!boardId || !taskId || !childTitles?.length) {
112
+ return fail("split requires boardId, taskId, and at least one childTitles.");
113
+ }
114
+ const {
115
+ targetColumnId,
116
+ inheritAssignment,
117
+ inheritLabels,
118
+ inheritSuccessCriteria,
119
+ inheritGoalMetrics,
120
+ inheritDependencies,
121
+ chainChildren,
122
+ rewireDependents
123
+ } = input;
124
+ const result = await splitTask(projectRoot, boardId, taskId, {
125
+ titles: childTitles,
126
+ ...extraSplitOptions,
127
+ ...targetColumnId !== void 0 ? { columnId: targetColumnId } : {},
128
+ ...inheritAssignment !== void 0 ? { inheritAssignment } : {},
129
+ ...inheritLabels !== void 0 ? { inheritLabels } : {},
130
+ ...inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria } : {},
131
+ ...inheritGoalMetrics !== void 0 ? { inheritGoalMetrics } : {},
132
+ ...inheritDependencies !== void 0 ? { inheritDependencies } : {},
133
+ ...chainChildren !== void 0 ? { chainChildren } : {},
134
+ ...rewireDependents !== void 0 ? { rewireDependents } : {}
135
+ });
136
+ if (!result) return fail("Task not found.");
137
+ const freshParent = result.board.tasks?.find((t) => t.id === taskId);
138
+ if (!freshParent) {
139
+ return fail(
140
+ `Split succeeded but parent ${taskId} not found in returned board. Children: [${result.children.map((c) => c.id).join(", ")}].`
141
+ );
231
142
  }
143
+ return {
144
+ ok: true,
145
+ message: `${result.children.length} child task(s) created.`,
146
+ board: result.board,
147
+ task: freshParent,
148
+ children: result.children
149
+ };
150
+ }
151
+ async function requireBoard(projectRoot, boardId) {
152
+ return boardId ? getBoard(projectRoot, boardId) : null;
232
153
  }
233
154
 
234
- // src/kanban-decomposition-actions.ts
155
+ // src/session-kanban.ts
156
+ import { loadPlan, loadTasks } from "@wrongstack/core/storage";
157
+ import { deserializeTaskGraph } from "@wrongstack/core/tasking";
235
158
  import {
236
- assessTaskAtomicity,
237
- proposeTaskDecomposition,
238
- updateTask,
239
- verifyTaskCompletion
159
+ bridgeKanbanSupervisor,
160
+ compactSessionMirrorBoard,
161
+ createBoard,
162
+ DEFAULT_COLUMNS,
163
+ getBoard as getBoard2,
164
+ getKanbanOrchestrationSnapshot,
165
+ listBoards,
166
+ pruneSessionBoards,
167
+ removeBoard,
168
+ syncBoardFromTaskGraph,
169
+ touchKanbanPresence,
170
+ updateBoard
240
171
  } from "@wrongstack/kanban";
241
172
 
242
- // src/kanban-evidence-bridge.ts
243
- import { recordCompletedWorkEvidence } from "@wrongstack/core/utils";
244
- function kanbanEvidenceKey(boardId, taskId) {
245
- return `kanban:${boardId}:${taskId}`;
246
- }
247
- function kanbanEvidencePointer(boardId, taskId) {
248
- return `kanban://${boardId}/${taskId}#verificationReport`;
249
- }
250
- function recordKanbanVerificationEvidence(ctx, report) {
251
- try {
252
- const passed = report.checks.filter((check) => check.status === "passed").length;
253
- const completedAt = Date.parse(report.completedAt);
254
- recordCompletedWorkEvidence(ctx, {
255
- key: kanbanEvidenceKey(report.boardId, report.taskId),
256
- source: "verification",
257
- summary: `${report.taskTitle} \u2014 verification ${report.verdict} (${passed}/${report.checks.length} checks)`,
258
- ...Number.isFinite(completedAt) ? { completedAt } : {},
259
- evidence: kanbanEvidencePointer(report.boardId, report.taskId)
260
- });
261
- } catch {
262
- }
173
+ // src/session-kanban-graph.ts
174
+ function todoListToSerializedGraph(todos, sessionId) {
175
+ const graphId = `todo:${sessionId}`;
176
+ const nodes = todos.map((todo, index) => ({
177
+ id: todo.id,
178
+ title: todo.content,
179
+ description: todo.activeForm ?? "",
180
+ type: "chore",
181
+ priority: "medium",
182
+ status: todo.status,
183
+ specRequirementId: `${graphId}:${todo.id}`,
184
+ createdAt: index,
185
+ updatedAt: index
186
+ }));
187
+ return {
188
+ id: graphId,
189
+ specId: graphId,
190
+ requiredRequirementIds: nodes.map((node) => node.specRequirementId),
191
+ title: "Session todos",
192
+ nodes,
193
+ edges: [],
194
+ rootNodes: nodes.map((node) => node.id),
195
+ createdAt: 0,
196
+ updatedAt: 0
197
+ };
263
198
  }
264
-
265
- // src/kanban-decomposition-actions.ts
266
- async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
267
- switch (input.action) {
268
- case "assess_atomicity": {
269
- if (!input.boardId || !input.taskId) {
270
- return fail("assess_atomicity requires boardId and taskId.");
271
- }
272
- const result = await assessTaskAtomicity(projectRoot, input.boardId, input.taskId, {
273
- assessedBy: "agent",
274
- ...ctx.agentId !== void 0 ? { eventContext: { actor: ctx.agentId } } : {}
275
- });
276
- if (!result) return fail("Task not found.");
277
- const failing = result.assessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason);
278
- const guidance = result.assessment.verdict === "needs_decomposition" ? ` This task should be split before dispatch \u2014 call propose_decomposition with 2+ subtasks (each with one verifiable success criterion). Reasons: ${failing.join(" | ")}` : result.assessment.verdict === "composite" ? " Container task: work happens in its children; it is verified via subtask aggregation." : "";
279
- return okTask(
280
- result.board,
281
- result.task,
282
- `Atomicity verdict: ${result.assessment.verdict} (score ${result.assessment.score}).${guidance}`
283
- );
284
- }
285
- case "propose_decomposition": {
286
- if (!input.boardId || !input.taskId || !input.subtasks?.length) {
287
- return fail("propose_decomposition requires boardId, taskId, and subtasks (2+).");
288
- }
289
- if (input.subtasks.length < 2) {
290
- return fail("propose_decomposition requires at least two subtasks.");
291
- }
292
- const invalid = input.subtasks.find(
293
- (subtask) => typeof subtask?.title !== "string" || !subtask.title.trim()
294
- );
295
- if (invalid) return fail("Every proposed subtask needs a non-blank title.");
296
- const result = await proposeTaskDecomposition(
297
- projectRoot,
298
- input.boardId,
299
- input.taskId,
300
- {
301
- subtasks: input.subtasks,
302
- ...input.note !== void 0 ? { rationale: input.note } : {},
303
- ...ctx.agentId !== void 0 ? { proposedBy: ctx.agentId } : {}
304
- },
305
- ctx.agentId !== void 0 ? { actor: ctx.agentId } : {}
306
- );
307
- if (!result) return fail("Task not found.");
308
- const message = result.proposal.status === "applied" ? `Decomposition applied: ${result.proposal.appliedChildTaskIds?.length ?? 0} child tasks created (parent marked atomic).` : 'Decomposition proposal recorded \u2014 awaiting approval (board policy is "propose"). It can be approved from the WebUI or via update_task.';
309
- return okTask(result.board, result.task, message);
310
- }
311
- case "verify_completion": {
312
- if (!input.boardId || !input.taskId) {
313
- return fail("verify_completion requires boardId and taskId.");
314
- }
315
- const verResult = await verifyTaskCompletion(projectRoot, input.boardId, input.taskId);
316
- const persistedBoard = await updateTask(projectRoot, input.boardId, input.taskId, {
317
- verificationReport: verResult.report,
318
- successCriteria: verResult.task.successCriteria
319
- });
320
- if (!persistedBoard) {
321
- return {
322
- ok: false,
323
- verdict: verResult.report.verdict,
324
- message: `Verification succeeded but persist failed: ${verResult.report.markdownSummary}. Board may be stale \u2014 re-run verify_completion.`,
325
- board: verResult.board
326
- };
327
- }
328
- recordKanbanVerificationEvidence(ctx, verResult.report);
329
- const freshTask = persistedBoard.tasks?.find((t) => t.id === input.taskId);
330
- const deterministicVerdicts = ["passed", "failed", "needs_human", "incomplete"];
331
- return {
332
- ok: deterministicVerdicts.includes(
333
- verResult.report.verdict
334
- ),
335
- verdict: verResult.report.verdict,
336
- message: verResult.report.markdownSummary,
337
- board: persistedBoard,
338
- task: freshTask ?? verResult.task
339
- };
340
- }
341
- default:
342
- return void 0;
343
- }
199
+ function taskFileToSerializedGraph(tasks, sessionId) {
200
+ const graphId = `session:${sessionId}`;
201
+ const ids = new Set(tasks.map((task) => task.id));
202
+ const nodes = tasks.map((task, index) => ({
203
+ id: task.id,
204
+ title: task.title,
205
+ description: task.description ?? "",
206
+ type: task.type,
207
+ priority: task.priority,
208
+ status: task.status,
209
+ specRequirementId: `${graphId}:${task.id}`,
210
+ ...task.assignee ? { assignee: task.assignee } : {},
211
+ ...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
212
+ createdAt: index,
213
+ updatedAt: index
214
+ }));
215
+ const edges = tasks.flatMap(
216
+ (task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
217
+ id: `${dependency}->${task.id}`,
218
+ from: dependency,
219
+ to: task.id,
220
+ type: "depends_on"
221
+ }))
222
+ );
223
+ const hasIncoming = new Set(edges.map((edge) => edge.to));
224
+ const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
225
+ return {
226
+ // Keep the historical graph id so existing mirrored task cards are reused.
227
+ id: graphId,
228
+ specId: graphId,
229
+ requiredRequirementIds: nodes.map((node) => node.specRequirementId),
230
+ title: "Session tasks",
231
+ nodes,
232
+ edges,
233
+ rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
234
+ createdAt: 0,
235
+ updatedAt: 0
236
+ };
344
237
  }
345
238
 
346
- // src/kanban-detail-actions.ts
239
+ // src/session-kanban-sync.ts
240
+ import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
241
+ import { mutatePlan, mutateTasks } from "@wrongstack/core/storage";
242
+ import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
347
243
  import {
348
- addCheckToTask,
349
- addDependency,
350
- addGoalMetricToTask,
351
- addLinkToTask,
352
- addNoteToTask,
353
- getKanbanWorkbench,
354
- removeCheckFromTask,
355
- updateCheckOnTask,
356
- updateGoalMetricOnTask
244
+ getDependencyReadinessIssues
357
245
  } from "@wrongstack/kanban";
358
-
359
- // src/kanban-split-task-handler.ts
360
- import { getBoard, splitTask } from "@wrongstack/kanban";
361
- async function handleSplitTask(projectRoot, input, extraSplitOptions) {
362
- const boardId = input.boardId;
363
- const taskId = input.taskId;
364
- const childTitles = input.childTitles;
365
- if (!boardId || !taskId || !childTitles?.length) {
366
- return fail("split requires boardId, taskId, and at least one childTitles.");
367
- }
368
- const {
369
- targetColumnId,
370
- inheritAssignment,
371
- inheritLabels,
372
- inheritSuccessCriteria,
373
- inheritGoalMetrics,
374
- inheritDependencies,
375
- chainChildren,
376
- rewireDependents
377
- } = input;
378
- const result = await splitTask(projectRoot, boardId, taskId, {
379
- titles: childTitles,
380
- ...extraSplitOptions,
381
- ...targetColumnId !== void 0 ? { columnId: targetColumnId } : {},
382
- ...inheritAssignment !== void 0 ? { inheritAssignment } : {},
383
- ...inheritLabels !== void 0 ? { inheritLabels } : {},
384
- ...inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria } : {},
385
- ...inheritGoalMetrics !== void 0 ? { inheritGoalMetrics } : {},
386
- ...inheritDependencies !== void 0 ? { inheritDependencies } : {},
387
- ...chainChildren !== void 0 ? { chainChildren } : {},
388
- ...rewireDependents !== void 0 ? { rewireDependents } : {}
389
- });
390
- if (!result) return fail("Task not found.");
391
- const freshParent = result.board.tasks?.find((t) => t.id === taskId);
392
- if (!freshParent) {
393
- return fail(
394
- `Split succeeded but parent ${taskId} not found in returned board. Children: [${result.children.map((c) => c.id).join(", ")}].`
395
- );
396
- }
246
+ function sourceStatus(task) {
247
+ if (task.status === "completed") return "completed";
248
+ if (task.status === "in_progress") return "in_progress";
249
+ if (task.status === "review") return "review";
250
+ if (task.status === "blocked") return "blocked";
251
+ if (task.status === "failed") return "failed";
252
+ return "pending";
253
+ }
254
+ function todoStatus(task) {
255
+ const status = sourceStatus(task);
256
+ if (status === "completed") return "completed";
257
+ if (status === "review" && task.assignment?.status === "completed") return "completed";
258
+ if (status === "in_progress" || status === "review") return "in_progress";
259
+ return "pending";
260
+ }
261
+ function sessionTodoFromTask(task, board) {
262
+ const blockedBy = board ? blockingTitles(board, task) : [];
397
263
  return {
398
- ok: true,
399
- message: `${result.children.length} child task(s) created.`,
400
- board: result.board,
401
- task: freshParent,
402
- children: result.children
264
+ id: task.origin?.taskId ?? task.id,
265
+ content: task.title,
266
+ status: todoStatus(task),
267
+ ...task.description ? { activeForm: task.description } : {},
268
+ ...blockedBy.length ? { blockedBy } : {}
403
269
  };
404
270
  }
405
- async function requireBoard(projectRoot, boardId) {
406
- return boardId ? getBoard(projectRoot, boardId) : null;
271
+ function managedTodoFromTask(task, board) {
272
+ return {
273
+ ...sessionTodoFromTask(task, board),
274
+ kanbanBoardId: board.id,
275
+ kanbanTaskId: task.id
276
+ };
407
277
  }
408
-
409
- // src/kanban-detail-actions.ts
410
- async function handleKanbanDetailAction(projectRoot, input) {
411
- switch (input.action) {
412
- case "workbench": {
413
- const workbench = await getKanbanWorkbench(projectRoot, {
414
- ...input.limit !== void 0 ? { limitPerLane: input.limit, alertLimit: input.limit } : {}
415
- });
416
- return {
417
- ok: true,
418
- message: `${workbench.totals.now} now, ${workbench.totals.next} next, ${workbench.totals.blocked} blocked, ${workbench.totals.review} review; ${workbench.alertTotal} alert(s).`,
419
- workbench
420
- };
421
- }
422
- case "add_dependency": {
423
- if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
424
- return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
425
- }
426
- const board = await addDependency(
427
- projectRoot,
428
- input.boardId,
429
- input.taskId,
430
- input.dependencyTaskId
431
- );
432
- return board ? okBoard(board, "Dependency added.") : fail("Task not found.");
278
+ function blockingTitles(board, task) {
279
+ return getDependencyReadinessIssues(board, task).map((issue) => {
280
+ const dependency = board.tasks.find((candidate) => candidate.id === issue.dependencyId);
281
+ if (!dependency) return `${issue.dependencyId} (missing)`;
282
+ return dependency.title;
283
+ });
284
+ }
285
+ var PRIORITY_ORDER = {
286
+ critical: 0,
287
+ high: 1,
288
+ medium: 2,
289
+ low: 3
290
+ };
291
+ function orderTasksForTodos(board, tasks) {
292
+ const columnOrder = new Map(board.columns.map((column) => [column.id, column.order]));
293
+ const baseline = [...tasks].sort(
294
+ (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)
295
+ );
296
+ const included = new Set(baseline.map((task) => task.id));
297
+ const remaining = new Map(baseline.map((task) => [task.id, task]));
298
+ const emitted = [];
299
+ const done = /* @__PURE__ */ new Set();
300
+ while (remaining.size > 0) {
301
+ const ready = baseline.filter(
302
+ (task) => remaining.has(task.id) && (task.dependsOn ?? []).every(
303
+ (dependencyId) => !included.has(dependencyId) || done.has(dependencyId)
304
+ )
305
+ );
306
+ if (ready.length === 0) break;
307
+ for (const task of ready) {
308
+ remaining.delete(task.id);
309
+ done.add(task.id);
310
+ emitted.push(task);
433
311
  }
434
- case "add_goal_metric": {
435
- if (!input.boardId || !input.taskId || !input.metricName) {
436
- return fail("add_goal_metric requires boardId, taskId, and metricName.");
437
- }
438
- const board = await addGoalMetricToTask(projectRoot, input.boardId, input.taskId, {
439
- name: input.metricName,
440
- ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
441
- ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
442
- ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
443
- ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
444
- ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
445
- });
446
- return board ? okBoard(board, "Goal metric added.") : fail("Task not found.");
447
- }
448
- case "update_goal_metric": {
449
- if (!input.boardId || !input.taskId || !input.metricId) {
450
- return fail("update_goal_metric requires boardId, taskId, and metricId.");
451
- }
452
- const board = await updateGoalMetricOnTask(
453
- projectRoot,
454
- input.boardId,
455
- input.taskId,
456
- input.metricId,
457
- {
458
- ...input.metricName !== void 0 ? { name: input.metricName } : {},
459
- ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
460
- ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
461
- ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
462
- ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
463
- ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
464
- }
465
- );
466
- return board ? okBoard(board, "Goal metric updated.") : fail("Metric not found.");
467
- }
468
- case "add_check": {
469
- if (!input.boardId || !input.taskId || !input.checkDescription) {
470
- return fail("add_check requires boardId, taskId, and checkDescription.");
471
- }
472
- const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
473
- description: input.checkDescription,
474
- type: input.checkType ?? "manual",
475
- status: input.checkStatus,
476
- ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
477
- });
478
- return board ? okBoard(board, "Check added.") : fail("Task not found.");
479
- }
480
- case "update_check": {
481
- if (!input.boardId || !input.taskId || !input.checkId) {
482
- return fail("update_check requires boardId, taskId, and checkId.");
483
- }
484
- const board = await updateCheckOnTask(
485
- projectRoot,
486
- input.boardId,
487
- input.taskId,
488
- input.checkId,
489
- {
490
- ...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
491
- ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {},
492
- // Promoting an existing manual criterion to an executable one is the
493
- // common repair: the card was written before anyone knew the command.
494
- ...input.checkType !== void 0 ? { type: input.checkType } : {},
495
- ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
496
- }
497
- );
498
- return board ? okBoard(board, "Check updated.") : fail("Check not found.");
499
- }
500
- case "remove_check": {
501
- if (!input.boardId || !input.taskId || !input.checkId) {
502
- return fail("remove_check requires boardId, taskId, and checkId.");
312
+ }
313
+ for (const task of baseline) if (remaining.has(task.id)) emitted.push(task);
314
+ return emitted;
315
+ }
316
+ function sameTodos(left, right) {
317
+ return left.length === right.length && left.every((todo, index) => {
318
+ const candidate = right[index];
319
+ 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");
320
+ });
321
+ }
322
+ function broadcastTodoUpdate(context, todos) {
323
+ const sessionId = context.session?.id ?? "";
324
+ if (!context.agentId || !sessionId) return;
325
+ const statusCounts = { pending: 0, inProgress: 0, completed: 0 };
326
+ for (const todo of todos) {
327
+ if (todo.status === "completed") statusCounts.completed++;
328
+ else if (todo.status === "in_progress") statusCounts.inProgress++;
329
+ else statusCounts.pending++;
330
+ }
331
+ const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;
332
+ const mailbox = getSharedProjectMailbox(projectDir);
333
+ void mailbox.send({
334
+ from: context.agentId,
335
+ to: "*",
336
+ type: "status",
337
+ subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
338
+ body: JSON.stringify({
339
+ kind: "kanban.todos.updated",
340
+ sessionId,
341
+ revision: context.state.revision,
342
+ todoCount: todos.length,
343
+ statusCounts
344
+ }),
345
+ priority: "normal",
346
+ senderSessionId: sessionId
347
+ }).catch(() => {
348
+ });
349
+ }
350
+ function notifyTodoUpdate(context, todos) {
351
+ const summary = formatTodosForModel(todos);
352
+ const text = `[KANBAN TODO UPDATE]
353
+ Another Kanban agent reassessed the shared board. The canonical todo list is now:
354
+ ${summary}
355
+ 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.`;
356
+ const state = context.state;
357
+ if (typeof state.appendBlockToLastUserMessage === "function") {
358
+ if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
359
+ }
360
+ if (typeof state.appendMessage === "function") {
361
+ state.appendMessage({ role: "user", content: [{ type: "text", text }] });
362
+ }
363
+ }
364
+ function applyManagedKanbanBoardToTodos(context, board, suppressedTodoMirrors2) {
365
+ const metaKanban = context.meta["kanban"];
366
+ const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
367
+ const activeBoardId2 = context.currentKanbanBoardId ?? (typeof metaBoardId === "string" ? metaBoardId : void 0);
368
+ if (!activeBoardId2 || board.id !== activeBoardId2 || board.lifecycle?.mode !== "managed") {
369
+ return [...context.todos];
370
+ }
371
+ const projectedTodos = orderTasksForTodos(
372
+ board,
373
+ board.tasks.filter(
374
+ (task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
375
+ )
376
+ ).map((task) => managedTodoFromTask(task, board));
377
+ if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
378
+ suppressedTodoMirrors2.add(context);
379
+ try {
380
+ context.state.replaceTodos(projectedTodos);
381
+ } finally {
382
+ suppressedTodoMirrors2.delete(context);
383
+ }
384
+ notifyTodoUpdate(context, context.todos);
385
+ broadcastTodoUpdate(context, context.todos);
386
+ return [...context.todos];
387
+ }
388
+
389
+ // src/session-kanban.ts
390
+ var SESSION_BOARD_TAG = "session-work";
391
+ var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
392
+ var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
393
+ ...column
394
+ }));
395
+ var boardQueue = /* @__PURE__ */ new Map();
396
+ var boardEnsures = /* @__PURE__ */ new Map();
397
+ var pendingMirrors = /* @__PURE__ */ new Map();
398
+ var activeMirrors = /* @__PURE__ */ new Set();
399
+ var mirrorFailures = /* @__PURE__ */ new Map();
400
+ var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
401
+ function boardKey(projectRoot, sessionId) {
402
+ return `${projectRoot}\0${sessionId}`;
403
+ }
404
+ function mirrorKey(projectRoot, sessionId, sourceSystem) {
405
+ return `${boardKey(projectRoot, sessionId)}\0${sourceSystem}`;
406
+ }
407
+ function completedReconciliationGraph(latest, candidates) {
408
+ const latestNodeIds = new Set(latest.nodes.map((node) => node.id));
409
+ const carriedNodeIds = /* @__PURE__ */ new Set();
410
+ const completedNodes = candidates.flatMap(
411
+ (candidate) => candidate.nodes.filter((node) => {
412
+ if (node.status !== "completed" || latestNodeIds.has(node.id) || carriedNodeIds.has(node.id)) {
413
+ return false;
503
414
  }
504
- const board = await removeCheckFromTask(
505
- projectRoot,
506
- input.boardId,
507
- input.taskId,
508
- input.checkId
509
- );
510
- return board ? okBoard(board, "Acceptance criterion removed.") : fail("Check not found on this task.");
511
- }
512
- case "add_note": {
513
- if (!input.boardId || !input.taskId || !input.note)
514
- return fail("add_note requires boardId, taskId, and note.");
515
- const board = await addNoteToTask(projectRoot, input.boardId, input.taskId, {
516
- author: input.author ?? "agent",
517
- content: input.note
518
- });
519
- return board ? okBoard(board, "Note added.") : fail("Task not found.");
520
- }
521
- case "add_link": {
522
- if (!input.boardId || !input.taskId || !input.url)
523
- return fail("add_link requires boardId, taskId, and url.");
524
- const board = await addLinkToTask(projectRoot, input.boardId, input.taskId, {
525
- url: input.url,
526
- type: input.linkType ?? "url",
527
- ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
415
+ carriedNodeIds.add(node.id);
416
+ return true;
417
+ })
418
+ );
419
+ if (completedNodes.length === 0) return void 0;
420
+ const carriedRequirements = completedNodes.flatMap(
421
+ (node) => node.specRequirementId ? [node.specRequirementId] : []
422
+ );
423
+ return {
424
+ ...latest,
425
+ nodes: [...latest.nodes, ...completedNodes],
426
+ rootNodes: [.../* @__PURE__ */ new Set([...latest.rootNodes, ...completedNodes.map((node) => node.id)])],
427
+ ...latest.requiredRequirementIds ? {
428
+ requiredRequirementIds: [
429
+ .../* @__PURE__ */ new Set([...latest.requiredRequirementIds, ...carriedRequirements])
430
+ ]
431
+ } : {}
432
+ };
433
+ }
434
+ function sessionTag(sessionId) {
435
+ return `session:${sessionId}`;
436
+ }
437
+ function sessionBoardTitle(sessionId) {
438
+ const leaf = sessionId.split(/[\\/]/).filter(Boolean).pop() ?? sessionId;
439
+ return `Session ${leaf.slice(0, 12)}`;
440
+ }
441
+ function sessionBoardTags(sessionId) {
442
+ return ["session", SESSION_BOARD_TAG, sessionTag(sessionId)];
443
+ }
444
+ function sameColumns(columns) {
445
+ return columns.length === SESSION_KANBAN_COLUMNS.length && columns.every((column, index) => column.id === SESSION_KANBAN_COLUMNS[index]?.id);
446
+ }
447
+ async function ensureSessionKanbanBoard(projectRoot, sessionId) {
448
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
449
+ const key = boardKey(projectRoot, sessionId);
450
+ const inFlight = boardEnsures.get(key);
451
+ if (inFlight) return inFlight;
452
+ const promise = (async () => {
453
+ const summary = (await listBoards(projectRoot)).find(
454
+ (board2) => board2.tags?.includes(sessionTag(sessionId))
455
+ );
456
+ let board = summary ? await getBoard2(projectRoot, summary.id) : null;
457
+ if (!board) {
458
+ return createBoard(projectRoot, {
459
+ title: sessionBoardTitle(sessionId),
460
+ description: "Live session work: todos, tasks, and plan items.",
461
+ tags: sessionBoardTags(sessionId),
462
+ columns: SESSION_KANBAN_COLUMNS,
463
+ generatedBy: `session-kanban:${sessionId}`,
464
+ kind: "session_mirror",
465
+ retention: { mode: "archive_after_ttl", ttlMs: 7 * 24 * 60 * 60 * 1e3 }
528
466
  });
529
- return board ? okBoard(board, "Link added.") : fail("Task not found.");
530
467
  }
531
- case "split_atomic": {
532
- if (!input.boardId || !input.taskId || !input.childTitles?.length) {
533
- return fail("split_atomic requires boardId, taskId, and childTitles (at least one).");
534
- }
535
- return handleSplitTask(projectRoot, input, { atomic: true });
468
+ if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
469
+ board = await updateBoard(projectRoot, board.id, {
470
+ title: sessionBoardTitle(sessionId),
471
+ description: "Live session work: todos, tasks, and plan items.",
472
+ tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
473
+ columns: SESSION_KANBAN_COLUMNS
474
+ }) ?? board;
536
475
  }
537
- default:
538
- return void 0;
476
+ return board;
477
+ })();
478
+ boardEnsures.set(key, promise);
479
+ try {
480
+ return await promise;
481
+ } finally {
482
+ boardEnsures.delete(key);
539
483
  }
540
484
  }
541
-
542
- // src/kanban-presence.ts
543
- import { touchKanbanPresence } from "@wrongstack/kanban";
544
- function createKanbanPresenceWrapper(projectRoot, input, ctx) {
545
- return async (result) => {
546
- const boardId = result.board?.id ?? input.boardId;
547
- if (!result.ok || !boardId || !ctx.session?.id || !ctx.agentId) return result;
548
- try {
549
- const board = await touchKanbanPresence(projectRoot, boardId, {
550
- sessionId: ctx.session.id,
551
- agentId: ctx.agentId,
552
- agentName: ctx.agentName,
553
- taskId: input.taskId ?? result.task?.id,
554
- runTaskId: input.runTaskId
555
- });
556
- return board ? { ...result, board } : result;
557
- } catch {
558
- return result;
485
+ function enqueueBoardWork(projectRoot, sessionId, work) {
486
+ const key = boardKey(projectRoot, sessionId);
487
+ const previous = boardQueue.get(key) ?? Promise.resolve();
488
+ const result = previous.catch(() => void 0).then(work);
489
+ const tail = result.then(
490
+ () => void 0,
491
+ () => void 0
492
+ );
493
+ boardQueue.set(key, tail);
494
+ void tail.then(() => {
495
+ if (boardQueue.get(key) === tail) boardQueue.delete(key);
496
+ });
497
+ return result;
498
+ }
499
+ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
500
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
501
+ return enqueueBoardWork(projectRoot, sessionId, async () => {
502
+ const board = await ensureSessionKanbanBoard(projectRoot, sessionId);
503
+ if (!board) return null;
504
+ const result = await syncBoardFromTaskGraph(
505
+ projectRoot,
506
+ board.id,
507
+ deserializeTaskGraph(graph),
508
+ {
509
+ sourceSystem,
510
+ tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
511
+ archiveMissingTasks: true,
512
+ includeCompletedTasks: true,
513
+ allowRequirementScopeShrink: true
514
+ }
515
+ );
516
+ if (!result) return null;
517
+ const compacted = await compactSessionMirrorBoard(projectRoot, board.id);
518
+ if (compacted?.removedTaskIds.length) {
519
+ return await getBoard2(projectRoot, board.id) ?? result.board;
559
520
  }
560
- };
521
+ return result.board;
522
+ });
561
523
  }
562
-
563
- // src/kanban-task-inputs.ts
564
- import { randomUUID } from "node:crypto";
565
- import { clampSubagentCapabilities } from "@wrongstack/core/security";
566
- function taskInput(input) {
567
- const assignment = hasAssignmentInput(input) ? assignmentForTaskCreate(input) : void 0;
568
- return {
569
- title: input.title ?? "",
570
- columnId: input.columnId,
571
- description: input.description,
572
- dueDate: input.dueDate,
573
- priority: input.priority,
574
- ...input.taskType !== void 0 ? { type: input.taskType } : {},
575
- status: input.status,
576
- labels: input.labels,
577
- ...assignment?.agentId ?? assignment?.role ?? assignment?.name ? { assignedAgent: assignment.agentId ?? assignment.role ?? assignment.name } : {},
578
- ...input.assignee ?? assignment?.name ?? assignment?.agentId ? { assignee: input.assignee ?? assignment?.name ?? assignment?.agentId } : {},
579
- ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
580
- ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
581
- ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {},
582
- ...assignment ? { assignment } : {},
583
- ...input.order !== void 0 ? { order: input.order } : {},
584
- ...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
585
- ...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
586
- // The system prompt has always told the model it may "set atomic: true"
587
- // when creating a composite parent. It could not: the field reached
588
- // neither the create input nor the patch, so the instruction described a
589
- // capability that did not exist and the attempt was silently dropped.
590
- ...input.atomic !== void 0 ? { atomic: input.atomic } : {},
591
- ...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
592
- ...input.checkDescription !== void 0 ? {
593
- successCriteria: [
594
- {
595
- id: randomUUID(),
596
- description: input.checkDescription,
597
- // `manual` only as the fallback. Hard-coding it here meant every
598
- // agent-authored criterion was unverifiable by construction: the
599
- // deterministic plugins never matched, the registry passed the
600
- // hand-set status straight through, and "verified" collapsed into
601
- // "the author ticked its own box".
602
- type: input.checkType ?? "manual",
603
- status: input.checkStatus ?? "pending",
604
- ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
605
- }
606
- ]
607
- } : {},
608
- ...input.metricName !== void 0 ? {
609
- goalMetrics: [
610
- {
611
- id: randomUUID(),
612
- name: input.metricName,
613
- status: input.metricStatus ?? "pending",
614
- ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
615
- ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
616
- ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
617
- ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
618
- }
619
- ]
620
- } : {},
621
- ...input.url !== void 0 ? {
622
- links: [
623
- {
624
- url: input.url,
625
- type: input.linkType ?? "url",
626
- ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
627
- }
628
- ]
629
- } : {},
630
- ...input.note !== void 0 ? {
631
- notes: [
632
- {
633
- id: randomUUID(),
634
- author: input.author ?? "agent",
635
- content: input.note,
636
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
524
+ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
525
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return;
526
+ const key = mirrorKey(projectRoot, sessionId, sourceSystem);
527
+ const previous = pendingMirrors.get(key);
528
+ const reconciliationGraph = previous ? completedReconciliationGraph(
529
+ graph,
530
+ [previous.reconciliationGraph, previous.graph].filter(
531
+ (candidate) => candidate !== void 0
532
+ )
533
+ ) : void 0;
534
+ pendingMirrors.set(key, {
535
+ projectRoot,
536
+ sessionId,
537
+ graph,
538
+ ...reconciliationGraph ? { reconciliationGraph } : {},
539
+ sourceSystem
540
+ });
541
+ if (activeMirrors.has(key)) return;
542
+ activeMirrors.add(key);
543
+ void (async () => {
544
+ try {
545
+ for (; ; ) {
546
+ const pending = pendingMirrors.get(key);
547
+ if (!pending) break;
548
+ pendingMirrors.delete(key);
549
+ try {
550
+ if (pending.reconciliationGraph) {
551
+ await projectGraph(
552
+ pending.projectRoot,
553
+ pending.sessionId,
554
+ pending.reconciliationGraph,
555
+ pending.sourceSystem
556
+ );
557
+ }
558
+ await projectGraph(
559
+ pending.projectRoot,
560
+ pending.sessionId,
561
+ pending.graph,
562
+ pending.sourceSystem
563
+ );
564
+ mirrorFailures.delete(boardKey(pending.projectRoot, pending.sessionId));
565
+ } catch (error) {
566
+ const message = error instanceof Error ? error.message : String(error);
567
+ mirrorFailures.set(boardKey(pending.projectRoot, pending.sessionId), {
568
+ message,
569
+ sourceSystem: pending.sourceSystem
570
+ });
571
+ console.warn(
572
+ JSON.stringify({
573
+ level: "warn",
574
+ event: "session-kanban.mirror-failed",
575
+ sessionId: pending.sessionId,
576
+ sourceSystem: pending.sourceSystem,
577
+ message,
578
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
579
+ })
580
+ );
637
581
  }
638
- ]
639
- } : {},
640
- ...[input.graphId, input.specId, input.specRequirementId].some((value) => value !== void 0) ? {
641
- origin: {
642
- system: input.sourceSystem ?? "kanban-tool",
643
- ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
644
- ...input.specId !== void 0 ? { specId: input.specId } : {},
645
- ...input.specRequirementId !== void 0 ? { specRequirementId: input.specRequirementId } : {},
646
- ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {}
647
582
  }
648
- } : {}
649
- };
583
+ } finally {
584
+ activeMirrors.delete(key);
585
+ const pending = pendingMirrors.get(key);
586
+ if (pending) {
587
+ pendingMirrors.delete(key);
588
+ queueLatestMirror(
589
+ pending.projectRoot,
590
+ pending.sessionId,
591
+ pending.graph,
592
+ pending.sourceSystem
593
+ );
594
+ }
595
+ }
596
+ })();
650
597
  }
651
- function mergedDependsOn(input) {
652
- if (input.dependsOn === void 0 && input.dependencyTaskId === void 0) return void 0;
653
- return [
654
- ...input.dependsOn ?? [],
655
- ...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
656
- ].filter((id, i, arr) => id && arr.indexOf(id) === i);
598
+ function takeSessionMirrorFailure(projectRoot, sessionId) {
599
+ if (!projectRoot || !sessionId) return void 0;
600
+ const key = boardKey(projectRoot, sessionId);
601
+ const failure = mirrorFailures.get(key);
602
+ if (!failure) return void 0;
603
+ mirrorFailures.delete(key);
604
+ return `Kanban mirror (${failure.sourceSystem}) failed and the board may be stale: ${failure.message}`;
657
605
  }
658
- function taskPatch(input) {
659
- return {
660
- title: input.title,
661
- description: input.description,
662
- dueDate: input.dueDate,
663
- columnId: input.columnId,
664
- order: input.order,
665
- priority: input.priority,
666
- ...input.taskType !== void 0 ? { type: input.taskType } : {},
667
- status: input.status,
668
- labels: input.labels,
669
- assignedAgent: input.agentId,
670
- ...mergedDependsOn(input) !== void 0 ? { dependsOn: mergedDependsOn(input) } : {},
671
- // `atomic` and `childTaskIds` are the composite-parent contract, and the
672
- // managed gate reads both: an `atomic` parent may not move forward without
673
- // children, and may not reach Done until every child is completed. The
674
- // manager has always accepted both on a patch; only this surface withheld
675
- // them, so `split_atomic` was a one-way door — delete the children and the
676
- // parent was stranded with no way to declare itself a leaf again.
677
- ...input.atomic !== void 0 ? { atomic: input.atomic } : {},
678
- ...input.childTaskIds !== void 0 ? { childTaskIds: input.childTaskIds } : {},
679
- ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
680
- ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
681
- };
606
+ function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
607
+ queueLatestMirror(
608
+ projectRoot,
609
+ sessionId,
610
+ todoListToSerializedGraph(todos, sessionId),
611
+ "session-todo"
612
+ );
682
613
  }
683
- function clampRequestedCapabilities(requested) {
684
- if (requested === void 0) return void 0;
685
- return clampSubagentCapabilities(requested).granted;
614
+ function applyManagedKanbanBoardToTodos2(context, board) {
615
+ return applyManagedKanbanBoardToTodos(context, board, suppressedTodoMirrors);
686
616
  }
687
- function assignmentInput(input) {
688
- return {
689
- agentId: input.agentId,
690
- name: input.name,
691
- role: input.role,
692
- provider: input.provider,
693
- model: input.model,
694
- fallbackProfile: input.fallbackProfile,
695
- fallbackModels: input.fallbackModels,
696
- tools: input.tools,
697
- allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities),
698
- assignee: input.assignee,
699
- leaseId: input.leaseId,
700
- claimedAt: input.claimedAt,
701
- heartbeatAt: input.heartbeatAt,
702
- leaseExpiresAt: input.leaseExpiresAt,
703
- attempt: input.attempt,
704
- maxAttempts: input.maxAttempts,
705
- costCeilingUsd: input.costCeilingUsd,
706
- retryPolicy: input.retryPolicy,
707
- lastFailureKind: input.lastFailureKind
708
- };
617
+
618
+ // src/kanban-board-actions.ts
619
+ async function handleKanbanBoardAction(projectRoot, input, ctx) {
620
+ switch (input.action) {
621
+ case "list_boards": {
622
+ const boards = await listBoards2(projectRoot);
623
+ return { ok: true, message: `${boards.length} board(s).`, boards };
624
+ }
625
+ case "get_board": {
626
+ const board = await requireBoard(projectRoot, input.boardId);
627
+ return board ? okBoard(board) : fail("Board not found.");
628
+ }
629
+ case "create_board": {
630
+ if (!input.title) return fail("create_board requires title.");
631
+ const existing = (await listBoards2(projectRoot)).filter(
632
+ (candidate) => (candidate.kind ?? "project") === "project"
633
+ );
634
+ const board = await createBoard2(projectRoot, boardCreateInput(input, input.title));
635
+ 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(
636
+ ", "
637
+ )}${existing.length > 3 ? ", \u2026" : ""}. If this work belongs to one of them, add_task there instead and delete this board.` : "";
638
+ return { ok: true, message: `Board created: ${board.title}.${note}`, board };
639
+ }
640
+ case "update_board": {
641
+ if (!input.boardId) return fail("update_board requires boardId.");
642
+ const board = await updateBoard2(projectRoot, input.boardId, boardUpdatePatch(input));
643
+ return board ? okBoard(board, "Board updated.") : fail("Board not found.");
644
+ }
645
+ case "adopt_managed_lifecycle": {
646
+ if (!input.boardId || !input.author || !input.transitionComment) {
647
+ return fail(
648
+ "adopt_managed_lifecycle requires boardId, author, transitionComment, and five ordered columns."
649
+ );
650
+ }
651
+ if (input.columns?.length !== 5) {
652
+ return fail(
653
+ "adopt_managed_lifecycle columns must be ordered as backlog, todo, running, review, done."
654
+ );
655
+ }
656
+ const [backlog, todo, running, review, done] = input.columns;
657
+ if (!backlog || !todo || !running || !review || !done) {
658
+ return fail("adopt_managed_lifecycle columns must contain five nonblank ids.");
659
+ }
660
+ const board = await adoptManagedLifecycle(projectRoot, input.boardId, {
661
+ columns: { backlog, todo, running, review, done },
662
+ actor: input.author,
663
+ comment: input.transitionComment
664
+ });
665
+ return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
666
+ }
667
+ case "release_managed_lifecycle": {
668
+ if (!input.boardId) return fail("release_managed_lifecycle requires boardId.");
669
+ const board = await updateBoard2(projectRoot, input.boardId, { lifecycle: null });
670
+ return board ? okBoard(
671
+ board,
672
+ "Managed lifecycle released; the board now tracks work without strict gates."
673
+ ) : fail("Board not found.");
674
+ }
675
+ case "duplicate_board": {
676
+ if (!input.boardId) return fail("duplicate_board requires boardId.");
677
+ const board = await duplicateBoard(projectRoot, input.boardId, duplicateBoardOptions(input));
678
+ return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
679
+ }
680
+ case "delete_board": {
681
+ if (!input.boardId) return fail("delete_board requires boardId.");
682
+ const removed = await removeBoard2(projectRoot, input.boardId);
683
+ return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
684
+ }
685
+ case "generate_board": {
686
+ if (!input.description) return fail("generate_board requires description.");
687
+ const boardInput = createBoardFromText({
688
+ description: input.description,
689
+ ...input.title !== void 0 ? { title: input.title } : {},
690
+ ...input.context !== void 0 ? { context: input.context } : {}
691
+ });
692
+ const board = await createBoard2(projectRoot, boardInput);
693
+ for (const tInput of parseLinesIntoTasks(
694
+ input.description,
695
+ board.columns[0]?.id ?? "backlog"
696
+ )) {
697
+ await addTask(projectRoot, board.id, tInput);
698
+ }
699
+ return okBoard(await getBoard3(projectRoot, board.id) ?? board, "Board generated.");
700
+ }
701
+ case "export_markdown": {
702
+ const board = await requireBoard(projectRoot, input.boardId);
703
+ if (!board) return fail("Board not found.");
704
+ return {
705
+ ok: true,
706
+ message: "Board exported.",
707
+ board,
708
+ markdown: exportBoardAsMarkdown(board)
709
+ };
710
+ }
711
+ case "export_task_graph": {
712
+ if (!input.boardId) return fail("export_task_graph requires boardId.");
713
+ const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
714
+ ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
715
+ ...input.specId !== void 0 ? { specId: input.specId } : {},
716
+ ...input.title !== void 0 ? { title: input.title } : {},
717
+ ...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
718
+ ...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
719
+ });
720
+ if (!exported) return fail("Board not found.");
721
+ return {
722
+ ok: true,
723
+ message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
724
+ board: exported.board,
725
+ taskGraph: serializeTaskGraph(exported.graph)
726
+ };
727
+ }
728
+ case "sync_task_graph": {
729
+ if (!input.boardId || !input.taskGraph) {
730
+ return fail("sync_task_graph requires boardId and taskGraph.");
731
+ }
732
+ const graph = deserializeTaskGraph2(input.taskGraph);
733
+ const result = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
734
+ ...input.title !== void 0 ? { title: input.title } : {},
735
+ ...input.description !== void 0 ? { description: input.description } : {},
736
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
737
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
738
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
739
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
740
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
741
+ ...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
742
+ ...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
743
+ });
744
+ return result ? {
745
+ ok: true,
746
+ message: `Task graph synced: ${result.createdTaskIds.length} created, ${result.updatedTaskIds.length} updated, ${result.archivedTaskIds.length} archived.`,
747
+ board: result.board
748
+ } : fail("Board not found.");
749
+ }
750
+ case "create_from_graph": {
751
+ if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
752
+ const graph = deserializeTaskGraph2(input.taskGraph);
753
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
754
+ ...input.title !== void 0 ? { title: input.title } : {},
755
+ ...input.description !== void 0 ? { description: input.description } : {},
756
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
757
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
758
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
759
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
760
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
761
+ });
762
+ return {
763
+ ok: true,
764
+ message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
765
+ board
766
+ };
767
+ }
768
+ case "import_session_tasks": {
769
+ const taskPath = ctx.meta?.["task.path"];
770
+ if (!taskPath) return fail("No session task file for this session.");
771
+ const file = await loadTasks2(taskPath);
772
+ if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
773
+ const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
774
+ const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
775
+ const tags = ["session", `session:${sessionId}`];
776
+ const existing = (await listBoards2(projectRoot)).find(
777
+ (b) => b.tags?.includes(`session:${sessionId}`)
778
+ );
779
+ if (existing) {
780
+ const result = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
781
+ sourceSystem: "session",
782
+ tags,
783
+ archiveMissingTasks: true,
784
+ includeCompletedTasks: true
785
+ });
786
+ return result ? {
787
+ ok: true,
788
+ message: `Synced ${file.tasks.length} session tasks into board "${result.board.title}".`,
789
+ board: result.board
790
+ } : fail("Session board vanished mid-sync.");
791
+ }
792
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
793
+ title: `Session tasks (${sessionId.slice(0, 8)})`,
794
+ sourceSystem: "session",
795
+ tags
796
+ });
797
+ return {
798
+ ok: true,
799
+ message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
800
+ board
801
+ };
802
+ }
803
+ case "search_tasks": {
804
+ const tasks = await searchKanban(projectRoot, {
805
+ query: input.query,
806
+ boardId: input.boardId,
807
+ assignedAgent: input.agentId,
808
+ status: input.status,
809
+ priority: input.priority,
810
+ label: input.labels?.[0],
811
+ chainId: input.chainId
812
+ });
813
+ return { ok: true, message: `${tasks.length} task(s) matched.`, tasks };
814
+ }
815
+ case "ready_tasks": {
816
+ const tasks = await listReadyTasks(projectRoot, {
817
+ query: input.query,
818
+ boardId: input.boardId,
819
+ assignedAgent: input.agentId,
820
+ priority: input.priority,
821
+ label: input.labels?.[0],
822
+ chainId: input.chainId,
823
+ limit: input.limit
824
+ });
825
+ return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
826
+ }
827
+ case "snapshot": {
828
+ const snapshot = await getKanbanOrchestrationSnapshot2(projectRoot, {
829
+ query: input.query,
830
+ boardId: input.boardId,
831
+ assignedAgent: input.agentId,
832
+ status: input.status,
833
+ priority: input.priority,
834
+ label: input.labels?.[0],
835
+ chainId: input.chainId
836
+ });
837
+ return {
838
+ ok: true,
839
+ message: `${snapshot.ready.length} ready, ${snapshot.running.length} running, ${snapshot.blocked.length} blocked.`,
840
+ snapshot
841
+ };
842
+ }
843
+ case "events": {
844
+ if (!input.boardId) return fail("events requires boardId.");
845
+ const eventList = await listKanbanEvents(projectRoot, input.boardId);
846
+ return {
847
+ ok: true,
848
+ message: `${eventList.length} event(s).`,
849
+ events: eventList
850
+ };
851
+ }
852
+ case "queue_health": {
853
+ const health = await getKanbanQueueHealth(projectRoot, {
854
+ ...input.boardId !== void 0 ? { boardId: input.boardId } : {}
855
+ });
856
+ return {
857
+ ok: true,
858
+ message: `Counts: startable=${health.counts.startable}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
859
+ queueHealth: health
860
+ };
861
+ }
862
+ default:
863
+ return void 0;
864
+ }
709
865
  }
710
- function hasAssignmentInput(input) {
711
- 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;
866
+
867
+ // src/kanban-contract-actions.ts
868
+ import {
869
+ addContractEdge,
870
+ configureContractGraph,
871
+ evaluateTaskContractGraph,
872
+ getContractGraph,
873
+ removeContractEdge,
874
+ removeContractNode,
875
+ upsertContractNode
876
+ } from "@wrongstack/kanban";
877
+ async function handleKanbanContractAction(projectRoot, input, actor) {
878
+ switch (input.action) {
879
+ case "get_contract_graph": {
880
+ if (!input.boardId) return fail("get_contract_graph requires boardId.");
881
+ const found = await getContractGraph(projectRoot, input.boardId);
882
+ if (!found) return fail("Board not found.");
883
+ const evaluated = input.taskId ? await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId) : null;
884
+ if (input.taskId && !evaluated) return fail("Task not found on this board.");
885
+ return {
886
+ ok: true,
887
+ message: found.graph ? `Contract map: ${found.graph.nodes.length} node(s), ${found.graph.edges.length} edge(s), enforcement ${found.graph.enforcement}.` : "No contract map on this board yet. Call configure_contract_graph to start one.",
888
+ board: found.board,
889
+ contractGraph: found.graph,
890
+ ...evaluated ? { contractEvaluation: evaluated.evaluation } : {}
891
+ };
892
+ }
893
+ case "configure_contract_graph": {
894
+ if (!input.boardId) return fail("configure_contract_graph requires boardId.");
895
+ const enforcement = input.contractEnforcement ?? "advisory";
896
+ const board = await configureContractGraph(projectRoot, input.boardId, enforcement);
897
+ return board ? okBoard(board, `Contract map enforcement set to ${enforcement}.`) : fail("Board not found.");
898
+ }
899
+ case "upsert_contract_node": {
900
+ if (!input.boardId || !input.taskId) {
901
+ return fail("upsert_contract_node requires boardId and taskId.");
902
+ }
903
+ if (!input.contractNodeKind || !input.contractNodeTitle) {
904
+ return fail("upsert_contract_node requires contractNodeKind and contractNodeTitle.");
905
+ }
906
+ const waiver = input.contractNodeState === "waived" ? {
907
+ actor: actor ?? "agent",
908
+ reason: input.contractWaiverReason ?? "",
909
+ at: (/* @__PURE__ */ new Date()).toISOString()
910
+ } : void 0;
911
+ if (waiver && !waiver.reason.trim()) {
912
+ return fail("A waived contract node requires contractWaiverReason.");
913
+ }
914
+ const result = await upsertContractNode(projectRoot, input.boardId, {
915
+ taskId: input.taskId,
916
+ kind: input.contractNodeKind,
917
+ title: input.contractNodeTitle,
918
+ ...input.contractNodeId !== void 0 ? { id: input.contractNodeId } : {},
919
+ ...input.contractNodeDescription !== void 0 ? { description: input.contractNodeDescription } : {},
920
+ ...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
921
+ ...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
922
+ ...input.contractCheckId !== void 0 ? { checkId: input.contractCheckId } : {},
923
+ ...input.contractMetricId !== void 0 ? { metricId: input.contractMetricId } : {},
924
+ ...waiver ? { waiver } : {},
925
+ ...actor !== void 0 ? { createdBy: actor } : {}
926
+ });
927
+ if (!result) return fail("Board or task not found.");
928
+ return {
929
+ ok: true,
930
+ message: `Contract node ${result.node.kind} "${result.node.title}" saved (${result.node.id}).`,
931
+ board: result.board,
932
+ contractGraph: result.board.contractGraph ?? null
933
+ };
934
+ }
935
+ case "remove_contract_node": {
936
+ if (!input.boardId || !input.contractNodeId) {
937
+ return fail("remove_contract_node requires boardId and contractNodeId.");
938
+ }
939
+ const board = await removeContractNode(projectRoot, input.boardId, input.contractNodeId);
940
+ return board ? okBoard(board, "Contract node removed, along with every edge that touched it.") : fail("Contract node not found.");
941
+ }
942
+ case "add_contract_edge": {
943
+ if (!input.boardId || !input.contractEdgeFrom || !input.contractEdgeTo) {
944
+ return fail("add_contract_edge requires boardId, contractEdgeFrom, and contractEdgeTo.");
945
+ }
946
+ if (!input.contractEdgeType) return fail("add_contract_edge requires contractEdgeType.");
947
+ const result = await addContractEdge(projectRoot, input.boardId, {
948
+ from: input.contractEdgeFrom,
949
+ to: input.contractEdgeTo,
950
+ type: input.contractEdgeType,
951
+ ...input.contractEdgeId !== void 0 ? { id: input.contractEdgeId } : {},
952
+ ...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
953
+ ...input.contractEdgeRationale !== void 0 ? { rationale: input.contractEdgeRationale } : {},
954
+ ...actor !== void 0 ? { createdBy: actor } : {}
955
+ });
956
+ if (!result) return fail("Board not found.");
957
+ return {
958
+ ok: true,
959
+ message: `Contract edge ${result.edge.type}: ${result.edge.from} \u2192 ${result.edge.to}.`,
960
+ board: result.board,
961
+ contractGraph: result.board.contractGraph ?? null
962
+ };
963
+ }
964
+ case "remove_contract_edge": {
965
+ if (!input.boardId || !input.contractEdgeId) {
966
+ return fail("remove_contract_edge requires boardId and contractEdgeId.");
967
+ }
968
+ const board = await removeContractEdge(projectRoot, input.boardId, input.contractEdgeId);
969
+ return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
970
+ }
971
+ default:
972
+ return void 0;
973
+ }
712
974
  }
713
- function assignmentForTaskCreate(input) {
714
- return {
715
- status: input.assignmentStatus ?? "assigned",
716
- ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
717
- ...input.name !== void 0 ? { name: input.name } : {},
718
- ...input.role !== void 0 ? { role: input.role } : {},
719
- ...input.provider !== void 0 ? { provider: input.provider } : {},
720
- ...input.model !== void 0 ? { model: input.model } : {},
721
- ...input.fallbackProfile !== void 0 ? { fallbackProfile: input.fallbackProfile } : {},
722
- ...input.fallbackModels !== void 0 ? { fallbackModels: input.fallbackModels } : {},
723
- ...input.tools !== void 0 ? { tools: input.tools } : {},
724
- ...input.allowedCapabilities !== void 0 ? { allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities) } : {},
725
- ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
726
- ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
727
- ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
728
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
729
- ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
730
- ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {},
731
- ...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
732
- ...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
733
- ...input.lastFailureKind !== void 0 ? { lastFailureKind: input.lastFailureKind } : {}
734
- };
975
+
976
+ // src/kanban-decomposition-actions.ts
977
+ import {
978
+ assessTaskAtomicity,
979
+ proposeTaskDecomposition,
980
+ updateTask,
981
+ verifyTaskCompletion
982
+ } from "@wrongstack/kanban";
983
+
984
+ // src/kanban-evidence-bridge.ts
985
+ import { recordCompletedWorkEvidence } from "@wrongstack/core/utils";
986
+ function kanbanEvidenceKey(boardId, taskId) {
987
+ return `kanban:${boardId}:${taskId}`;
988
+ }
989
+ function kanbanEvidencePointer(boardId, taskId) {
990
+ return `kanban://${boardId}/${taskId}#verificationReport`;
991
+ }
992
+ function recordKanbanVerificationEvidence(ctx, report) {
993
+ try {
994
+ const passed = report.checks.filter((check) => check.status === "passed").length;
995
+ const completedAt = Date.parse(report.completedAt);
996
+ recordCompletedWorkEvidence(ctx, {
997
+ key: kanbanEvidenceKey(report.boardId, report.taskId),
998
+ source: "verification",
999
+ summary: `${report.taskTitle} \u2014 verification ${report.verdict} (${passed}/${report.checks.length} checks)`,
1000
+ ...Number.isFinite(completedAt) ? { completedAt } : {},
1001
+ evidence: kanbanEvidencePointer(report.boardId, report.taskId)
1002
+ });
1003
+ } catch {
1004
+ }
735
1005
  }
736
1006
 
737
- // src/kanban-tool-schema.ts
738
- var KANBAN_TOOL_DESCRIPTION = "Durable project task boards: create and move cards, record checks, notes, links and assignments. The board is a record of the work, not a permit for it \u2014 nothing here gates other tools. Managed boards additionally enforce ordered Backlog \u2192 Todo \u2192 Running \u2192 Review \u2192 Done transitions; release_managed_lifecycle turns that off.";
739
- var KANBAN_TOOL_USAGE_HINT = 'Track substantial or multi-step work so it survives the session; a trivial edit or a question needs no card. Work stays on ONE board: call list_boards first and add_task to the board this project already uses. create_board is for a genuinely separate line of work, not for each new piece of it \u2014 a second board splits the same effort in two, and a board holding a single card is the usual sign. Common flow: list_boards or search_tasks to orient, add_task to record work, start_task when you begin, update_check with checkStatus "passed" to tick acceptance criteria (read their ids from get_task), then transition_task. On a managed board a refused transition names the field it wants \u2014 supply it and retry. When the acceptance criterion is something a machine can run, say so: set checkType ("command", "test", "file_exists", "file_matches", "git_diff", "metric") and put the command, pattern or path in checkNotes, then verify_completion executes it and the result is real evidence. Leave checkType off (or "manual") only for criteria that genuinely need a human eye \u2014 a manual check records your assertion, it does not test anything.';
740
- var KANBAN_INPUT_SCHEMA = {
741
- type: "object",
742
- properties: {
743
- action: {
744
- type: "string",
745
- enum: [
746
- "list_boards",
747
- "get_board",
748
- "create_board",
749
- "duplicate_board",
750
- "update_board",
751
- "adopt_managed_lifecycle",
752
- "release_managed_lifecycle",
753
- "delete_board",
754
- "generate_board",
755
- "export_markdown",
756
- "export_task_graph",
757
- "sync_task_graph",
758
- "create_from_graph",
759
- "import_session_tasks",
760
- "search_tasks",
761
- "ready_tasks",
762
- "snapshot",
763
- "workbench",
764
- "add_task",
765
- "split_task",
766
- "merge_tasks",
767
- "copy_task",
768
- "transfer_task",
769
- "get_task",
770
- "start_task",
771
- "update_task",
772
- "transition_task",
773
- "repair_managed_projection",
774
- "move_task",
775
- "delete_task",
776
- "set_chain",
777
- "get_chain",
778
- "claim_task",
779
- "release_task",
780
- "assign_task",
781
- "mark_assignment",
782
- "heartbeat_assignment",
783
- "recover_stale",
784
- "events",
785
- "queue_health",
786
- "add_dependency",
787
- "add_goal_metric",
788
- "update_goal_metric",
789
- "add_check",
790
- "update_check",
791
- "remove_check",
792
- "add_note",
793
- "add_link",
794
- "verify_completion",
795
- "split_atomic",
796
- "assess_atomicity",
797
- "propose_decomposition",
798
- "get_contract_graph",
799
- "configure_contract_graph",
800
- "upsert_contract_node",
801
- "remove_contract_node",
802
- "add_contract_edge",
803
- "remove_contract_edge"
804
- ]
805
- },
806
- boardId: { type: "string" },
807
- taskId: { type: "string" },
808
- taskIds: { type: "array", items: { type: "string" } },
809
- chainId: { type: "string" },
810
- fromNodeId: { type: "string" },
811
- toNodeId: { type: "string" },
812
- baseline: { oneOf: [{ type: "string" }, { type: "number" }] },
813
- threshold: { oneOf: [{ type: "string" }, { type: "number" }] },
814
- columnId: { type: "string" },
815
- targetBoardId: { type: "string" },
816
- targetColumnId: { type: "string" },
817
- title: { type: "string" },
818
- description: { type: "string" },
819
- dueDate: { type: "string" },
820
- tags: { type: "array", items: { type: "string" } },
821
- labels: { type: "array", items: { type: "string" } },
822
- priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
823
- taskType: {
824
- type: "string",
825
- enum: ["feature", "bugfix", "refactor", "docs", "test", "chore"]
826
- },
827
- status: {
828
- type: "string",
829
- enum: [
830
- "pending",
831
- "ready",
832
- "in_progress",
833
- "blocked",
834
- "review",
835
- "completed",
836
- "failed",
837
- "archived"
838
- ]
839
- },
840
- order: { type: "number" },
841
- query: { type: "string" },
842
- limit: { type: "number" },
843
- agentId: { type: "string" },
844
- name: { type: "string" },
845
- role: { type: "string" },
846
- provider: { type: "string" },
847
- model: { type: "string" },
848
- fallbackProfile: { type: "string" },
849
- fallbackModels: { type: "array", items: { type: "string" } },
850
- tools: { type: "array", items: { type: "string" } },
851
- allowedCapabilities: { type: "array", items: { type: "string" } },
852
- leaseId: { type: "string" },
853
- claimedAt: { type: "string" },
854
- heartbeatAt: { type: "string" },
855
- leaseExpiresAt: { type: "string" },
856
- attempt: { type: "number" },
857
- maxAttempts: { type: "number" },
858
- subagentId: { type: "string" },
859
- runTaskId: { type: "string" },
860
- lastResult: { type: "string" },
861
- error: { type: "string" },
862
- expectedLeaseId: { type: "string" },
863
- assignmentStatus: {
864
- type: "string",
865
- enum: ["assigned", "queued", "running", "completed", "failed", "cancelled"]
866
- },
867
- lifecycleStage: {
868
- type: "string",
869
- enum: ["backlog", "todo", "running", "review", "done"]
870
- },
871
- transitionAction: { type: "string" },
872
- transitionComment: { type: "string" },
873
- tickChecks: {
874
- type: "array",
875
- items: {
876
- type: "object",
877
- properties: {
878
- checkId: { type: "string" },
879
- checkStatus: { type: "string", enum: ["passed", "failed", "skipped"] }
1007
+ // src/kanban-decomposition-actions.ts
1008
+ async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
1009
+ switch (input.action) {
1010
+ case "assess_atomicity": {
1011
+ if (!input.boardId || !input.taskId) {
1012
+ return fail("assess_atomicity requires boardId and taskId.");
1013
+ }
1014
+ const result = await assessTaskAtomicity(projectRoot, input.boardId, input.taskId, {
1015
+ assessedBy: "agent",
1016
+ ...ctx.agentId !== void 0 ? { eventContext: { actor: ctx.agentId } } : {}
1017
+ });
1018
+ if (!result) return fail("Task not found.");
1019
+ const failing = result.assessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason);
1020
+ const guidance = result.assessment.verdict === "needs_decomposition" ? ` This task should be split before dispatch \u2014 call propose_decomposition with 2+ subtasks (each with one verifiable success criterion). Reasons: ${failing.join(" | ")}` : result.assessment.verdict === "composite" ? " Container task: work happens in its children; it is verified via subtask aggregation." : "";
1021
+ return okTask(
1022
+ result.board,
1023
+ result.task,
1024
+ `Atomicity verdict: ${result.assessment.verdict} (score ${result.assessment.score}).${guidance}`
1025
+ );
1026
+ }
1027
+ case "propose_decomposition": {
1028
+ if (!input.boardId || !input.taskId || !input.subtasks?.length) {
1029
+ return fail("propose_decomposition requires boardId, taskId, and subtasks (2+).");
1030
+ }
1031
+ if (input.subtasks.length < 2) {
1032
+ return fail("propose_decomposition requires at least two subtasks.");
1033
+ }
1034
+ const invalid = input.subtasks.find(
1035
+ (subtask) => typeof subtask?.title !== "string" || !subtask.title.trim()
1036
+ );
1037
+ if (invalid) return fail("Every proposed subtask needs a non-blank title.");
1038
+ const result = await proposeTaskDecomposition(
1039
+ projectRoot,
1040
+ input.boardId,
1041
+ input.taskId,
1042
+ {
1043
+ subtasks: input.subtasks,
1044
+ ...input.note !== void 0 ? { rationale: input.note } : {},
1045
+ ...ctx.agentId !== void 0 ? { proposedBy: ctx.agentId } : {}
880
1046
  },
881
- required: ["checkId", "checkStatus"]
882
- },
883
- description: "`transition_task` (to=done only): flip one or more manual criteria to `passed` before the gate fires. Read ids from kanban get_task. Non-manual criteria are refused."
884
- },
885
- attachmentUrl: { type: "string" },
886
- attachmentTitle: { type: "string" },
887
- attachmentType: {
888
- type: "string",
889
- enum: ["issue", "pr", "doc", "commit", "design", "file", "url", "other"]
890
- },
891
- releaseStatus: { type: "string", enum: ["pending", "ready", "blocked"] },
892
- releaseReason: { type: "string" },
893
- clearAssignee: { type: "boolean" },
894
- recoveryMode: { type: "string", enum: ["auto", "release", "retry", "fail"] },
895
- recoveryNow: { type: "string" },
896
- recoveryPolicyFailOnCostCeiling: { type: "boolean" },
897
- recoveryPolicyReleaseOnFailureKinds: { type: "array", items: { type: "string" } },
898
- recoveryPolicyReleaseOnHeartbeatDue: { type: "boolean" },
899
- recoveryPolicyRetryPolicyOverride: {
900
- type: "string",
901
- enum: ["off", "incremental", "exponential"]
902
- },
903
- assignee: { type: "string" },
904
- costCeilingUsd: { type: "number" },
905
- retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
906
- lastFailureKind: { type: "string" },
907
- dependsOn: {
908
- type: "array",
909
- items: { type: "string" },
910
- description: "Task ids this card waits on. On update_task an explicit empty array clears them \u2014 use it when a dependency was recorded in error rather than completing work nobody wants."
911
- },
912
- atomic: {
913
- type: "boolean",
914
- description: "Composite parent (true) or executable leaf (false). Set false to make a stranded parent a leaf again after its children were dropped."
915
- },
916
- childTaskIds: {
917
- type: "array",
918
- items: { type: "string" },
919
- description: "Children of a composite parent. On update_task an explicit empty array detaches them all."
920
- },
921
- estimatedHours: { type: "number" },
922
- actualHours: { type: "number" },
923
- taskGraph: { type: "object" },
924
- graphId: { type: "string" },
925
- specId: { type: "string" },
926
- specRequirementId: { type: "string" },
927
- sourceSystem: { type: "string" },
928
- phaseId: { type: "string" },
929
- preserveOriginTaskIds: { type: "boolean" },
930
- includeArchived: { type: "boolean" },
931
- archiveMissingTasks: { type: "boolean" },
932
- preserveManualDependencies: { type: "boolean" },
933
- dependencyTaskId: { type: "string" },
934
- enforceDependencies: { type: "boolean" },
935
- childTitles: { type: "array", items: { type: "string" } },
936
- inheritAssignment: { type: "boolean" },
937
- inheritLabels: { type: "boolean" },
938
- inheritSuccessCriteria: { type: "boolean" },
939
- inheritGoalMetrics: { type: "boolean" },
940
- inheritDependencies: { type: "boolean" },
941
- chainChildren: { type: "boolean" },
942
- rewireDependents: { type: "boolean" },
943
- closeSourceTasks: { type: "boolean" },
944
- metricId: { type: "string" },
945
- metricName: { type: "string" },
946
- metricTarget: { oneOf: [{ type: "string" }, { type: "number" }] },
947
- metricCurrent: { oneOf: [{ type: "string" }, { type: "number" }] },
948
- metricUnit: { type: "string" },
949
- metricStatus: { type: "string", enum: ["pending", "met", "missed", "waived"] },
950
- metricNotes: { type: "string" },
951
- checkId: { type: "string" },
952
- checkDescription: { type: "string" },
953
- checkStatus: { type: "string", enum: ["pending", "passed", "failed", "skipped"] },
954
- checkType: {
955
- type: "string",
956
- // Only types a verifier can actually execute. `manual` is the default and
957
- // means a human or agent asserts the status by hand. The rest are run by
958
- // `verify_completion` against the default deterministic registry. Types
959
- // with no plugin in that registry (`auto`, `review`, `agent`, `council`)
960
- // are deliberately omitted: offering them would produce criteria that
961
- // silently report `skipped — no verifier plugin registered`.
962
- enum: ["manual", "command", "test", "file_exists", "file_matches", "git_diff", "metric"],
963
- description: 'How this acceptance criterion is verified. Default "manual" (status set by hand). Any other value makes verify_completion execute it, so the criterion becomes real evidence rather than a self-assertion. Pair with checkNotes.'
964
- },
965
- checkNotes: {
966
- type: "string",
967
- description: 'The executable body for a non-manual checkType, read in preference to checkDescription. command/test: the shell command or test pattern. file_exists: the path. file_matches: JSON {"file","pattern","flags"}. git_diff: JSON {"expectedFiles","minChanges","maxChanges"}.'
968
- },
969
- // ── Contract map ───────────────────────────────────────────────────
970
- // The card contract: what this work targets, what it must not break, what
971
- // it risks, and what verifies it. Advisory by default — the readiness gate
972
- // deliberately does not require map structure, so a map is an operator
973
- // review aid, not work the model must complete before implementing.
974
- contractEnforcement: {
975
- type: "string",
976
- enum: ["off", "advisory", "strict"],
977
- description: "Board-level contract map enforcement. Default when first configured: advisory."
978
- },
979
- contractNodeId: { type: "string" },
980
- contractNodeKind: {
981
- type: "string",
982
- enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"],
983
- description: "objective = what this card is for; guardrail = what must keep working; risk = what could go wrong; component/artifact = what it touches; verification = what settles it."
984
- },
985
- contractNodeTitle: { type: "string" },
986
- contractNodeDescription: { type: "string" },
987
- contractNodeState: {
988
- type: "string",
989
- enum: ["unknown", "active", "satisfied", "violated", "waived", "resolved"]
990
- },
991
- contractNodeEnforcement: {
992
- type: "string",
993
- enum: ["blocking", "advisory", "informational"]
994
- },
995
- /** Bind a node to an acceptance criterion or goal metric already on the task. */
996
- contractCheckId: { type: "string" },
997
- contractMetricId: { type: "string" },
998
- contractWaiverReason: {
999
- type: "string",
1000
- description: 'Required, with an actor, when contractNodeState is "waived".'
1001
- },
1002
- contractEdgeId: { type: "string" },
1003
- contractEdgeFrom: {
1004
- type: "string",
1005
- description: 'A contract node id, or a task id (bare or "task:<id>") for the card endpoint.'
1006
- },
1007
- contractEdgeTo: { type: "string" },
1008
- contractEdgeType: {
1009
- type: "string",
1010
- enum: [
1011
- "targets",
1012
- "affects",
1013
- "must_preserve",
1014
- "exposes",
1015
- "verified_by",
1016
- "conflicts_with",
1017
- "derived_from",
1018
- "relates_to"
1019
- ]
1020
- },
1021
- contractEdgeRationale: { type: "string" },
1022
- note: { type: "string" },
1023
- author: { type: "string" },
1024
- url: { type: "string" },
1025
- linkTitle: { type: "string" },
1026
- linkType: {
1027
- type: "string",
1028
- enum: ["issue", "pr", "doc", "commit", "design", "file", "url", "other"]
1029
- },
1030
- context: { type: "string" },
1031
- columns: { type: "array", items: { type: "string" } },
1032
- generatedBy: { type: "string" },
1033
- includeTasks: { type: "boolean" },
1034
- includeCompletedTasks: { type: "boolean" },
1035
- preserveAssignment: { type: "boolean" },
1036
- preserveDependencies: { type: "boolean" },
1037
- moveTasksToColumnId: { type: "string" },
1038
- atomicityMode: { type: "string", enum: ["off", "assess", "enforce"] },
1039
- atomicityDecomposition: { type: "string", enum: ["auto", "propose"] },
1040
- gateEnforcement: {
1041
- type: "string",
1042
- // WS-023: `'off'` is deliberately absent. The agent whose work this gate
1043
- // checks must not be able to switch it off; it may only tighten. Turning
1044
- // a gate off stays a human decision, made through board config.
1045
- enum: ["strict", "soft"]
1046
- },
1047
- subtasks: {
1048
- type: "array",
1049
- minItems: 2,
1050
- items: {
1051
- type: "object",
1052
- properties: {
1053
- title: { type: "string" },
1054
- description: { type: "string" },
1055
- successCriteria: { type: "array", items: { type: "string" } },
1056
- dependsOnIndex: { type: "array", items: { type: "number" } }
1057
- },
1058
- required: ["title"]
1059
- }
1047
+ ctx.agentId !== void 0 ? { actor: ctx.agentId } : {}
1048
+ );
1049
+ if (!result) return fail("Task not found.");
1050
+ const message = result.proposal.status === "applied" ? `Decomposition applied: ${result.proposal.appliedChildTaskIds?.length ?? 0} child tasks created (parent marked atomic).` : 'Decomposition proposal recorded \u2014 awaiting approval (board policy is "propose"). It can be approved from the WebUI or via update_task.';
1051
+ return okTask(result.board, result.task, message);
1060
1052
  }
1061
- },
1062
- required: ["action"]
1063
- };
1064
-
1065
- // src/session-kanban.ts
1066
- import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
1067
- import {
1068
- loadPlan,
1069
- loadTasks,
1070
- mutatePlan,
1071
- mutateTasks
1072
- } from "@wrongstack/core/storage";
1073
- import { deserializeTaskGraph } from "@wrongstack/core/tasking";
1074
- import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
1075
- import {
1076
- bridgeKanbanSupervisor,
1077
- compactSessionMirrorBoard,
1078
- createBoard,
1079
- DEFAULT_COLUMNS,
1080
- getBoard as getBoard2,
1081
- getDependencyReadinessIssues,
1082
- getKanbanOrchestrationSnapshot,
1083
- listBoards,
1084
- pruneSessionBoards,
1085
- removeBoard,
1086
- syncBoardFromTaskGraph,
1087
- touchKanbanPresence as touchKanbanPresence2,
1088
- updateBoard
1089
- } from "@wrongstack/kanban";
1090
- var SESSION_BOARD_TAG = "session-work";
1091
- var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
1092
- var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
1093
- ...column
1094
- }));
1095
- var boardQueue = /* @__PURE__ */ new Map();
1096
- var boardEnsures = /* @__PURE__ */ new Map();
1097
- var pendingMirrors = /* @__PURE__ */ new Map();
1098
- var activeMirrors = /* @__PURE__ */ new Set();
1099
- var mirrorFailures = /* @__PURE__ */ new Map();
1100
- var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
1101
- function boardKey(projectRoot, sessionId) {
1102
- return `${projectRoot}\0${sessionId}`;
1103
- }
1104
- function mirrorKey(projectRoot, sessionId, sourceSystem) {
1105
- return `${boardKey(projectRoot, sessionId)}\0${sourceSystem}`;
1106
- }
1107
- function completedReconciliationGraph(latest, candidates) {
1108
- const latestNodeIds = new Set(latest.nodes.map((node) => node.id));
1109
- const carriedNodeIds = /* @__PURE__ */ new Set();
1110
- const completedNodes = candidates.flatMap(
1111
- (candidate) => candidate.nodes.filter((node) => {
1112
- if (node.status !== "completed" || latestNodeIds.has(node.id) || carriedNodeIds.has(node.id)) {
1113
- return false;
1053
+ case "verify_completion": {
1054
+ if (!input.boardId || !input.taskId) {
1055
+ return fail("verify_completion requires boardId and taskId.");
1114
1056
  }
1115
- carriedNodeIds.add(node.id);
1116
- return true;
1117
- })
1118
- );
1119
- if (completedNodes.length === 0) return void 0;
1120
- const carriedRequirements = completedNodes.flatMap(
1121
- (node) => node.specRequirementId ? [node.specRequirementId] : []
1122
- );
1123
- return {
1124
- ...latest,
1125
- nodes: [...latest.nodes, ...completedNodes],
1126
- rootNodes: [.../* @__PURE__ */ new Set([...latest.rootNodes, ...completedNodes.map((node) => node.id)])],
1127
- ...latest.requiredRequirementIds ? {
1128
- requiredRequirementIds: [
1129
- .../* @__PURE__ */ new Set([...latest.requiredRequirementIds, ...carriedRequirements])
1130
- ]
1131
- } : {}
1132
- };
1133
- }
1134
- function sessionTag(sessionId) {
1135
- return `session:${sessionId}`;
1136
- }
1137
- function sessionBoardTitle(sessionId) {
1138
- const leaf = sessionId.split(/[\\/]/).filter(Boolean).pop() ?? sessionId;
1139
- return `Session ${leaf.slice(0, 12)}`;
1140
- }
1141
- function sessionBoardTags(sessionId) {
1142
- return ["session", SESSION_BOARD_TAG, sessionTag(sessionId)];
1143
- }
1144
- function sameColumns(columns) {
1145
- return columns.length === SESSION_KANBAN_COLUMNS.length && columns.every((column, index) => column.id === SESSION_KANBAN_COLUMNS[index]?.id);
1146
- }
1147
- async function ensureSessionKanbanBoard(projectRoot, sessionId) {
1148
- if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
1149
- const key = boardKey(projectRoot, sessionId);
1150
- const inFlight = boardEnsures.get(key);
1151
- if (inFlight) return inFlight;
1152
- const promise = (async () => {
1153
- const summary = (await listBoards(projectRoot)).find(
1154
- (board2) => board2.tags?.includes(sessionTag(sessionId))
1155
- );
1156
- let board = summary ? await getBoard2(projectRoot, summary.id) : null;
1157
- if (!board) {
1158
- return createBoard(projectRoot, {
1159
- title: sessionBoardTitle(sessionId),
1160
- description: "Live session work: todos, tasks, and plan items.",
1161
- tags: sessionBoardTags(sessionId),
1162
- columns: SESSION_KANBAN_COLUMNS,
1163
- generatedBy: `session-kanban:${sessionId}`,
1164
- kind: "session_mirror",
1165
- retention: { mode: "archive_after_ttl", ttlMs: 7 * 24 * 60 * 60 * 1e3 }
1057
+ const verResult = await verifyTaskCompletion(projectRoot, input.boardId, input.taskId);
1058
+ const persistedBoard = await updateTask(projectRoot, input.boardId, input.taskId, {
1059
+ verificationReport: verResult.report,
1060
+ successCriteria: verResult.task.successCriteria
1166
1061
  });
1062
+ if (!persistedBoard) {
1063
+ return {
1064
+ ok: false,
1065
+ verdict: verResult.report.verdict,
1066
+ message: `Verification succeeded but persist failed: ${verResult.report.markdownSummary}. Board may be stale \u2014 re-run verify_completion.`,
1067
+ board: verResult.board
1068
+ };
1069
+ }
1070
+ recordKanbanVerificationEvidence(ctx, verResult.report);
1071
+ const freshTask = persistedBoard.tasks?.find((t) => t.id === input.taskId);
1072
+ const deterministicVerdicts = ["passed", "failed", "needs_human", "incomplete"];
1073
+ return {
1074
+ ok: deterministicVerdicts.includes(
1075
+ verResult.report.verdict
1076
+ ),
1077
+ verdict: verResult.report.verdict,
1078
+ message: verResult.report.markdownSummary,
1079
+ board: persistedBoard,
1080
+ task: freshTask ?? verResult.task
1081
+ };
1167
1082
  }
1168
- if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
1169
- board = await updateBoard(projectRoot, board.id, {
1170
- title: sessionBoardTitle(sessionId),
1171
- description: "Live session work: todos, tasks, and plan items.",
1172
- tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
1173
- columns: SESSION_KANBAN_COLUMNS
1174
- }) ?? board;
1175
- }
1176
- return board;
1177
- })();
1178
- boardEnsures.set(key, promise);
1179
- try {
1180
- return await promise;
1181
- } finally {
1182
- boardEnsures.delete(key);
1083
+ default:
1084
+ return void 0;
1183
1085
  }
1184
1086
  }
1185
- function enqueueBoardWork(projectRoot, sessionId, work) {
1186
- const key = boardKey(projectRoot, sessionId);
1187
- const previous = boardQueue.get(key) ?? Promise.resolve();
1188
- const result = previous.catch(() => void 0).then(work);
1189
- const tail = result.then(
1190
- () => void 0,
1191
- () => void 0
1192
- );
1193
- boardQueue.set(key, tail);
1194
- void tail.then(() => {
1195
- if (boardQueue.get(key) === tail) boardQueue.delete(key);
1196
- });
1197
- return result;
1198
- }
1199
- async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
1200
- if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
1201
- return enqueueBoardWork(projectRoot, sessionId, async () => {
1202
- const board = await ensureSessionKanbanBoard(projectRoot, sessionId);
1203
- if (!board) return null;
1204
- const result = await syncBoardFromTaskGraph(
1205
- projectRoot,
1206
- board.id,
1207
- deserializeTaskGraph(graph),
1208
- {
1209
- sourceSystem,
1210
- tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
1211
- archiveMissingTasks: true,
1212
- includeCompletedTasks: true,
1213
- // The scope ledger stays declared and accurate, but it may not veto a
1214
- // projection. A session mirror reflects a tactical list that shrinks by
1215
- // design, and refusing the sync never protected the removed row — it
1216
- // froze the entire board, permanently, because the stored scope then
1217
- // outlived every later snapshot (`session-kanban.mirror-failed`).
1218
- // Nothing is lost by shrinking here: `archiveMissingTasks` keeps the
1219
- // removed card on the board as `archived`, the reconciliation pass
1220
- // first walks vanished completed rows to Done, and the session journal
1221
- // remains the durable record.
1222
- allowRequirementScopeShrink: true
1087
+
1088
+ // src/kanban-detail-actions.ts
1089
+ import {
1090
+ addCheckToTask,
1091
+ addDependency,
1092
+ addGoalMetricToTask,
1093
+ addLinkToTask,
1094
+ addNoteToTask,
1095
+ getKanbanWorkbench,
1096
+ removeCheckFromTask,
1097
+ updateCheckOnTask,
1098
+ updateGoalMetricOnTask
1099
+ } from "@wrongstack/kanban";
1100
+ async function handleKanbanDetailAction(projectRoot, input) {
1101
+ switch (input.action) {
1102
+ case "workbench": {
1103
+ const workbench = await getKanbanWorkbench(projectRoot, {
1104
+ ...input.limit !== void 0 ? { limitPerLane: input.limit, alertLimit: input.limit } : {}
1105
+ });
1106
+ return {
1107
+ ok: true,
1108
+ message: `${workbench.totals.now} now, ${workbench.totals.next} next, ${workbench.totals.blocked} blocked, ${workbench.totals.review} review; ${workbench.alertTotal} alert(s).`,
1109
+ workbench
1110
+ };
1111
+ }
1112
+ case "add_dependency": {
1113
+ if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
1114
+ return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
1223
1115
  }
1224
- );
1225
- if (!result) return null;
1226
- const compacted = await compactSessionMirrorBoard(projectRoot, board.id);
1227
- if (compacted?.removedTaskIds.length) {
1228
- return await getBoard2(projectRoot, board.id) ?? result.board;
1116
+ const board = await addDependency(
1117
+ projectRoot,
1118
+ input.boardId,
1119
+ input.taskId,
1120
+ input.dependencyTaskId
1121
+ );
1122
+ return board ? okBoard(board, "Dependency added.") : fail("Task not found.");
1229
1123
  }
1230
- return result.board;
1231
- });
1232
- }
1233
- function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
1234
- if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return;
1235
- const key = mirrorKey(projectRoot, sessionId, sourceSystem);
1236
- const previous = pendingMirrors.get(key);
1237
- const reconciliationGraph = previous ? completedReconciliationGraph(
1238
- graph,
1239
- [previous.reconciliationGraph, previous.graph].filter(
1240
- (candidate) => candidate !== void 0
1241
- )
1242
- ) : void 0;
1243
- pendingMirrors.set(key, {
1244
- projectRoot,
1245
- sessionId,
1246
- graph,
1247
- ...reconciliationGraph ? { reconciliationGraph } : {},
1248
- sourceSystem
1249
- });
1250
- if (activeMirrors.has(key)) return;
1251
- activeMirrors.add(key);
1252
- void (async () => {
1253
- try {
1254
- for (; ; ) {
1255
- const pending = pendingMirrors.get(key);
1256
- if (!pending) break;
1257
- pendingMirrors.delete(key);
1258
- try {
1259
- if (pending.reconciliationGraph) {
1260
- await projectGraph(
1261
- pending.projectRoot,
1262
- pending.sessionId,
1263
- pending.reconciliationGraph,
1264
- pending.sourceSystem
1265
- );
1266
- }
1267
- await projectGraph(
1268
- pending.projectRoot,
1269
- pending.sessionId,
1270
- pending.graph,
1271
- pending.sourceSystem
1272
- );
1273
- mirrorFailures.delete(boardKey(pending.projectRoot, pending.sessionId));
1274
- } catch (error) {
1275
- const message = error instanceof Error ? error.message : String(error);
1276
- mirrorFailures.set(boardKey(pending.projectRoot, pending.sessionId), {
1277
- message,
1278
- sourceSystem: pending.sourceSystem
1279
- });
1280
- console.warn(
1281
- JSON.stringify({
1282
- level: "warn",
1283
- event: "session-kanban.mirror-failed",
1284
- sessionId: pending.sessionId,
1285
- sourceSystem: pending.sourceSystem,
1286
- message,
1287
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1288
- })
1289
- );
1124
+ case "add_goal_metric": {
1125
+ if (!input.boardId || !input.taskId || !input.metricName) {
1126
+ return fail("add_goal_metric requires boardId, taskId, and metricName.");
1127
+ }
1128
+ const board = await addGoalMetricToTask(projectRoot, input.boardId, input.taskId, {
1129
+ name: input.metricName,
1130
+ ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
1131
+ ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
1132
+ ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
1133
+ ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
1134
+ ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
1135
+ });
1136
+ return board ? okBoard(board, "Goal metric added.") : fail("Task not found.");
1137
+ }
1138
+ case "update_goal_metric": {
1139
+ if (!input.boardId || !input.taskId || !input.metricId) {
1140
+ return fail("update_goal_metric requires boardId, taskId, and metricId.");
1141
+ }
1142
+ const board = await updateGoalMetricOnTask(
1143
+ projectRoot,
1144
+ input.boardId,
1145
+ input.taskId,
1146
+ input.metricId,
1147
+ {
1148
+ ...input.metricName !== void 0 ? { name: input.metricName } : {},
1149
+ ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
1150
+ ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
1151
+ ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
1152
+ ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
1153
+ ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
1290
1154
  }
1155
+ );
1156
+ return board ? okBoard(board, "Goal metric updated.") : fail("Metric not found.");
1157
+ }
1158
+ case "add_check": {
1159
+ if (!input.boardId || !input.taskId || !input.checkDescription) {
1160
+ return fail("add_check requires boardId, taskId, and checkDescription.");
1291
1161
  }
1292
- } finally {
1293
- activeMirrors.delete(key);
1294
- const pending = pendingMirrors.get(key);
1295
- if (pending) {
1296
- pendingMirrors.delete(key);
1297
- queueLatestMirror(
1298
- pending.projectRoot,
1299
- pending.sessionId,
1300
- pending.graph,
1301
- pending.sourceSystem
1302
- );
1162
+ const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
1163
+ description: input.checkDescription,
1164
+ type: input.checkType ?? "manual",
1165
+ status: input.checkStatus,
1166
+ ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
1167
+ });
1168
+ return board ? okBoard(board, "Check added.") : fail("Task not found.");
1169
+ }
1170
+ case "update_check": {
1171
+ if (!input.boardId || !input.taskId || !input.checkId) {
1172
+ return fail("update_check requires boardId, taskId, and checkId.");
1173
+ }
1174
+ const board = await updateCheckOnTask(
1175
+ projectRoot,
1176
+ input.boardId,
1177
+ input.taskId,
1178
+ input.checkId,
1179
+ {
1180
+ ...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
1181
+ ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {},
1182
+ // Promoting an existing manual criterion to an executable one is the
1183
+ // common repair: the card was written before anyone knew the command.
1184
+ ...input.checkType !== void 0 ? { type: input.checkType } : {},
1185
+ ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
1186
+ }
1187
+ );
1188
+ return board ? okBoard(board, "Check updated.") : fail("Check not found.");
1189
+ }
1190
+ case "remove_check": {
1191
+ if (!input.boardId || !input.taskId || !input.checkId) {
1192
+ return fail("remove_check requires boardId, taskId, and checkId.");
1303
1193
  }
1194
+ const board = await removeCheckFromTask(
1195
+ projectRoot,
1196
+ input.boardId,
1197
+ input.taskId,
1198
+ input.checkId
1199
+ );
1200
+ return board ? okBoard(board, "Acceptance criterion removed.") : fail("Check not found on this task.");
1304
1201
  }
1305
- })();
1306
- }
1307
- function takeSessionMirrorFailure(projectRoot, sessionId) {
1308
- if (!projectRoot || !sessionId) return void 0;
1309
- const key = boardKey(projectRoot, sessionId);
1310
- const failure = mirrorFailures.get(key);
1311
- if (!failure) return void 0;
1312
- mirrorFailures.delete(key);
1313
- return `Kanban mirror (${failure.sourceSystem}) failed and the board may be stale: ${failure.message}`;
1202
+ case "add_note": {
1203
+ if (!input.boardId || !input.taskId || !input.note)
1204
+ return fail("add_note requires boardId, taskId, and note.");
1205
+ const board = await addNoteToTask(projectRoot, input.boardId, input.taskId, {
1206
+ author: input.author ?? "agent",
1207
+ content: input.note
1208
+ });
1209
+ return board ? okBoard(board, "Note added.") : fail("Task not found.");
1210
+ }
1211
+ case "add_link": {
1212
+ if (!input.boardId || !input.taskId || !input.url)
1213
+ return fail("add_link requires boardId, taskId, and url.");
1214
+ const board = await addLinkToTask(projectRoot, input.boardId, input.taskId, {
1215
+ url: input.url,
1216
+ type: input.linkType ?? "url",
1217
+ ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
1218
+ });
1219
+ return board ? okBoard(board, "Link added.") : fail("Task not found.");
1220
+ }
1221
+ case "split_atomic": {
1222
+ if (!input.boardId || !input.taskId || !input.childTitles?.length) {
1223
+ return fail("split_atomic requires boardId, taskId, and childTitles (at least one).");
1224
+ }
1225
+ return handleSplitTask(projectRoot, input, { atomic: true });
1226
+ }
1227
+ default:
1228
+ return void 0;
1229
+ }
1314
1230
  }
1315
- function todoListToSerializedGraph(todos, sessionId) {
1316
- const graphId = `todo:${sessionId}`;
1317
- const nodes = todos.map((todo, index) => ({
1318
- id: todo.id,
1319
- title: todo.content,
1320
- description: todo.activeForm ?? "",
1321
- type: "chore",
1322
- priority: "medium",
1323
- status: todo.status,
1324
- specRequirementId: `${graphId}:${todo.id}`,
1325
- createdAt: index,
1326
- updatedAt: index
1327
- }));
1231
+
1232
+ // src/kanban-lifecycle-actions.ts
1233
+ import { randomUUID as randomUUID2 } from "node:crypto";
1234
+ import {
1235
+ addTask as addTask2,
1236
+ assignTask,
1237
+ claimReadyTask,
1238
+ copyTaskToBoard,
1239
+ evaluateContractGraphReadiness,
1240
+ finalizeTaskCompletion,
1241
+ getBoard as getBoard4,
1242
+ getTask,
1243
+ getTaskChain,
1244
+ heartbeatTaskAssignment,
1245
+ mergeTasks,
1246
+ moveTask,
1247
+ recoverStaleTaskAssignments,
1248
+ releaseTaskClaim,
1249
+ removeTask,
1250
+ repairManagedTaskProjection,
1251
+ resolveAutoAccept,
1252
+ setTaskChain,
1253
+ stripLifecycleIssues,
1254
+ transferTaskToBoard,
1255
+ transitionTask,
1256
+ updateTask as updateTask2,
1257
+ updateTaskAssignment,
1258
+ verifyTaskCompletion as verifyTaskCompletion2
1259
+ } from "@wrongstack/kanban";
1260
+
1261
+ // src/kanban-task-inputs.ts
1262
+ import { randomUUID } from "node:crypto";
1263
+ import { clampSubagentCapabilities } from "@wrongstack/core/security";
1264
+ function taskInput(input) {
1265
+ const assignment = hasAssignmentInput(input) ? assignmentForTaskCreate(input) : void 0;
1328
1266
  return {
1329
- id: graphId,
1330
- specId: graphId,
1331
- requiredRequirementIds: nodes.map((node) => node.specRequirementId),
1332
- title: "Session todos",
1333
- nodes,
1334
- edges: [],
1335
- rootNodes: nodes.map((node) => node.id),
1336
- createdAt: 0,
1337
- updatedAt: 0
1267
+ title: input.title ?? "",
1268
+ columnId: input.columnId,
1269
+ description: input.description,
1270
+ dueDate: input.dueDate,
1271
+ priority: input.priority,
1272
+ ...input.taskType !== void 0 ? { type: input.taskType } : {},
1273
+ status: input.status,
1274
+ labels: input.labels,
1275
+ ...assignment?.agentId ?? assignment?.role ?? assignment?.name ? { assignedAgent: assignment.agentId ?? assignment.role ?? assignment.name } : {},
1276
+ ...input.assignee ?? assignment?.name ?? assignment?.agentId ? { assignee: input.assignee ?? assignment?.name ?? assignment?.agentId } : {},
1277
+ ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
1278
+ ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
1279
+ ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {},
1280
+ ...assignment ? { assignment } : {},
1281
+ ...input.order !== void 0 ? { order: input.order } : {},
1282
+ ...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
1283
+ ...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
1284
+ // The system prompt has always told the model it may "set atomic: true"
1285
+ // when creating a composite parent. It could not: the field reached
1286
+ // neither the create input nor the patch, so the instruction described a
1287
+ // capability that did not exist and the attempt was silently dropped.
1288
+ ...input.atomic !== void 0 ? { atomic: input.atomic } : {},
1289
+ ...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
1290
+ ...input.checkDescription !== void 0 ? {
1291
+ successCriteria: [
1292
+ {
1293
+ id: randomUUID(),
1294
+ description: input.checkDescription,
1295
+ // `manual` only as the fallback. Hard-coding it here meant every
1296
+ // agent-authored criterion was unverifiable by construction: the
1297
+ // deterministic plugins never matched, the registry passed the
1298
+ // hand-set status straight through, and "verified" collapsed into
1299
+ // "the author ticked its own box".
1300
+ type: input.checkType ?? "manual",
1301
+ status: input.checkStatus ?? "pending",
1302
+ ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
1303
+ }
1304
+ ]
1305
+ } : {},
1306
+ ...input.metricName !== void 0 ? {
1307
+ goalMetrics: [
1308
+ {
1309
+ id: randomUUID(),
1310
+ name: input.metricName,
1311
+ status: input.metricStatus ?? "pending",
1312
+ ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
1313
+ ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
1314
+ ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
1315
+ ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
1316
+ }
1317
+ ]
1318
+ } : {},
1319
+ ...input.url !== void 0 ? {
1320
+ links: [
1321
+ {
1322
+ url: input.url,
1323
+ type: input.linkType ?? "url",
1324
+ ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
1325
+ }
1326
+ ]
1327
+ } : {},
1328
+ ...input.note !== void 0 ? {
1329
+ notes: [
1330
+ {
1331
+ id: randomUUID(),
1332
+ author: input.author ?? "agent",
1333
+ content: input.note,
1334
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1335
+ }
1336
+ ]
1337
+ } : {},
1338
+ ...[input.graphId, input.specId, input.specRequirementId].some((value) => value !== void 0) ? {
1339
+ origin: {
1340
+ system: input.sourceSystem ?? "kanban-tool",
1341
+ ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
1342
+ ...input.specId !== void 0 ? { specId: input.specId } : {},
1343
+ ...input.specRequirementId !== void 0 ? { specRequirementId: input.specRequirementId } : {},
1344
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {}
1345
+ }
1346
+ } : {}
1338
1347
  };
1339
1348
  }
1340
- function taskFileToSerializedGraph(tasks, sessionId) {
1341
- const graphId = `session:${sessionId}`;
1342
- const ids = new Set(tasks.map((task) => task.id));
1343
- const nodes = tasks.map((task, index) => ({
1344
- id: task.id,
1345
- title: task.title,
1346
- description: task.description ?? "",
1347
- type: task.type,
1348
- priority: task.priority,
1349
- status: task.status,
1350
- specRequirementId: `${graphId}:${task.id}`,
1351
- ...task.assignee ? { assignee: task.assignee } : {},
1352
- ...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
1353
- createdAt: index,
1354
- updatedAt: index
1355
- }));
1356
- const edges = tasks.flatMap(
1357
- (task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
1358
- id: `${dependency}->${task.id}`,
1359
- from: dependency,
1360
- to: task.id,
1361
- type: "depends_on"
1362
- }))
1363
- );
1364
- const hasIncoming = new Set(edges.map((edge) => edge.to));
1365
- const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
1349
+ function mergedDependsOn(input) {
1350
+ if (input.dependsOn === void 0 && input.dependencyTaskId === void 0) return void 0;
1351
+ return [
1352
+ ...input.dependsOn ?? [],
1353
+ ...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
1354
+ ].filter((id, i, arr) => id && arr.indexOf(id) === i);
1355
+ }
1356
+ function taskPatch(input) {
1366
1357
  return {
1367
- // Keep the historical graph id so existing mirrored task cards are reused.
1368
- id: graphId,
1369
- specId: graphId,
1370
- requiredRequirementIds: nodes.map((node) => node.specRequirementId),
1371
- title: "Session tasks",
1372
- nodes,
1373
- edges,
1374
- rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
1375
- createdAt: 0,
1376
- updatedAt: 0
1358
+ title: input.title,
1359
+ description: input.description,
1360
+ dueDate: input.dueDate,
1361
+ columnId: input.columnId,
1362
+ order: input.order,
1363
+ priority: input.priority,
1364
+ ...input.taskType !== void 0 ? { type: input.taskType } : {},
1365
+ status: input.status,
1366
+ labels: input.labels,
1367
+ assignedAgent: input.agentId,
1368
+ ...mergedDependsOn(input) !== void 0 ? { dependsOn: mergedDependsOn(input) } : {},
1369
+ // `atomic` and `childTaskIds` are the composite-parent contract, and the
1370
+ // managed gate reads both: an `atomic` parent may not move forward without
1371
+ // children, and may not reach Done until every child is completed. The
1372
+ // manager has always accepted both on a patch; only this surface withheld
1373
+ // them, so `split_atomic` was a one-way door — delete the children and the
1374
+ // parent was stranded with no way to declare itself a leaf again.
1375
+ ...input.atomic !== void 0 ? { atomic: input.atomic } : {},
1376
+ ...input.childTaskIds !== void 0 ? { childTaskIds: input.childTaskIds } : {},
1377
+ ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
1378
+ ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
1377
1379
  };
1378
1380
  }
1379
- function broadcastTodoUpdate(context, todos) {
1380
- const sessionId = context.session?.id ?? "";
1381
- if (!context.agentId || !sessionId) return;
1382
- const statusCounts = { pending: 0, inProgress: 0, completed: 0 };
1383
- for (const todo of todos) {
1384
- if (todo.status === "completed") statusCounts.completed++;
1385
- else if (todo.status === "in_progress") statusCounts.inProgress++;
1386
- else statusCounts.pending++;
1387
- }
1388
- const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;
1389
- const mailbox = getSharedProjectMailbox(projectDir);
1390
- void mailbox.send({
1391
- from: context.agentId,
1392
- to: "*",
1393
- type: "status",
1394
- subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
1395
- body: JSON.stringify({
1396
- kind: "kanban.todos.updated",
1397
- sessionId,
1398
- revision: context.state.revision,
1399
- todoCount: todos.length,
1400
- statusCounts
1401
- }),
1402
- priority: "normal",
1403
- senderSessionId: sessionId
1404
- }).catch(() => {
1405
- });
1381
+ function clampRequestedCapabilities(requested) {
1382
+ if (requested === void 0) return void 0;
1383
+ return clampSubagentCapabilities(requested).granted;
1406
1384
  }
1407
- function notifyTodoUpdate(context, todos) {
1408
- const summary = formatTodosForModel(todos);
1409
- const text = `[KANBAN TODO UPDATE]
1410
- Another Kanban agent reassessed the shared board. The canonical todo list is now:
1411
- ${summary}
1412
- 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.`;
1413
- const state = context.state;
1414
- if (typeof state.appendBlockToLastUserMessage === "function") {
1415
- if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
1416
- }
1417
- if (typeof state.appendMessage === "function") {
1418
- state.appendMessage({ role: "user", content: [{ type: "text", text }] });
1419
- }
1385
+ function assignmentInput(input) {
1386
+ return {
1387
+ agentId: input.agentId,
1388
+ name: input.name,
1389
+ role: input.role,
1390
+ provider: input.provider,
1391
+ model: input.model,
1392
+ fallbackProfile: input.fallbackProfile,
1393
+ fallbackModels: input.fallbackModels,
1394
+ tools: input.tools,
1395
+ allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities),
1396
+ assignee: input.assignee,
1397
+ leaseId: input.leaseId,
1398
+ claimedAt: input.claimedAt,
1399
+ heartbeatAt: input.heartbeatAt,
1400
+ leaseExpiresAt: input.leaseExpiresAt,
1401
+ attempt: input.attempt,
1402
+ maxAttempts: input.maxAttempts,
1403
+ costCeilingUsd: input.costCeilingUsd,
1404
+ retryPolicy: input.retryPolicy,
1405
+ lastFailureKind: input.lastFailureKind
1406
+ };
1420
1407
  }
1421
- function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
1422
- queueLatestMirror(
1423
- projectRoot,
1424
- sessionId,
1425
- todoListToSerializedGraph(todos, sessionId),
1426
- "session-todo"
1427
- );
1428
- }
1429
- function sourceStatus(task) {
1430
- if (task.status === "completed") return "completed";
1431
- if (task.status === "in_progress") return "in_progress";
1432
- if (task.status === "review") return "review";
1433
- if (task.status === "blocked") return "blocked";
1434
- if (task.status === "failed") return "failed";
1435
- return "pending";
1436
- }
1437
- function todoStatus(task) {
1438
- const status = sourceStatus(task);
1439
- if (status === "completed") return "completed";
1440
- if (status === "review" && task.assignment?.status === "completed") return "completed";
1441
- if (status === "in_progress" || status === "review") return "in_progress";
1442
- return "pending";
1408
+ function hasAssignmentInput(input) {
1409
+ 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;
1443
1410
  }
1444
- function sessionTodoFromTask(task, board) {
1445
- const blockedBy = board ? blockingTitles(board, task) : [];
1411
+ function assignmentForTaskCreate(input) {
1446
1412
  return {
1447
- id: task.origin?.taskId ?? task.id,
1448
- content: task.title,
1449
- status: todoStatus(task),
1450
- ...task.description ? { activeForm: task.description } : {},
1451
- ...blockedBy.length ? { blockedBy } : {}
1413
+ status: input.assignmentStatus ?? "assigned",
1414
+ ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
1415
+ ...input.name !== void 0 ? { name: input.name } : {},
1416
+ ...input.role !== void 0 ? { role: input.role } : {},
1417
+ ...input.provider !== void 0 ? { provider: input.provider } : {},
1418
+ ...input.model !== void 0 ? { model: input.model } : {},
1419
+ ...input.fallbackProfile !== void 0 ? { fallbackProfile: input.fallbackProfile } : {},
1420
+ ...input.fallbackModels !== void 0 ? { fallbackModels: input.fallbackModels } : {},
1421
+ ...input.tools !== void 0 ? { tools: input.tools } : {},
1422
+ ...input.allowedCapabilities !== void 0 ? { allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities) } : {},
1423
+ ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
1424
+ ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
1425
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
1426
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
1427
+ ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
1428
+ ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {},
1429
+ ...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
1430
+ ...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
1431
+ ...input.lastFailureKind !== void 0 ? { lastFailureKind: input.lastFailureKind } : {}
1452
1432
  };
1453
1433
  }
1454
- function managedTodoFromTask(task, board) {
1455
- return {
1456
- ...sessionTodoFromTask(task, board),
1457
- kanbanBoardId: board.id,
1458
- kanbanTaskId: task.id
1459
- };
1434
+
1435
+ // src/kanban-lifecycle-actions.ts
1436
+ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1437
+ switch (input.action) {
1438
+ case "add_task": {
1439
+ if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
1440
+ const result = await addTask2(projectRoot, input.boardId, taskInput(input));
1441
+ if (!result) return fail("Board not found.");
1442
+ return okTask(result.board, result.task, `Task added.${atomicityNudge(result.task)}`);
1443
+ }
1444
+ case "split_task": {
1445
+ if (!input.boardId || !input.taskId || !input.childTitles?.length) {
1446
+ return fail("split_task requires boardId, taskId, and childTitles.");
1447
+ }
1448
+ return handleSplitTask(projectRoot, input, {});
1449
+ }
1450
+ case "merge_tasks": {
1451
+ if (!input.boardId || !input.taskIds?.length || !input.title) {
1452
+ return fail("merge_tasks requires boardId, taskIds, and title.");
1453
+ }
1454
+ const result = await mergeTasks(projectRoot, input.boardId, {
1455
+ taskIds: input.taskIds,
1456
+ title: input.title,
1457
+ ...input.description !== void 0 ? { description: input.description } : {},
1458
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
1459
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
1460
+ ...input.closeSourceTasks !== void 0 ? { closeSourceTasks: input.closeSourceTasks } : {}
1461
+ });
1462
+ return result ? okTask(result.board, result.task, "Tasks merged.") : fail("Board or task not found.");
1463
+ }
1464
+ case "copy_task": {
1465
+ if (!input.boardId || !input.taskId || !input.targetBoardId) {
1466
+ return fail("copy_task requires boardId, taskId, and targetBoardId.");
1467
+ }
1468
+ const result = await copyTaskToBoard(
1469
+ projectRoot,
1470
+ input.boardId,
1471
+ input.taskId,
1472
+ input.targetBoardId,
1473
+ {
1474
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
1475
+ ...input.order !== void 0 ? { targetOrder: input.order } : {},
1476
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
1477
+ ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
1478
+ }
1479
+ );
1480
+ return result ? okTask(result.targetBoard, result.task, "Task copied to target board.") : fail("Board or task not found.");
1481
+ }
1482
+ case "transfer_task": {
1483
+ if (!input.boardId || !input.taskId || !input.targetBoardId) {
1484
+ return fail("transfer_task requires boardId, taskId, and targetBoardId.");
1485
+ }
1486
+ const result = await transferTaskToBoard(
1487
+ projectRoot,
1488
+ input.boardId,
1489
+ input.taskId,
1490
+ input.targetBoardId,
1491
+ {
1492
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
1493
+ ...input.order !== void 0 ? { targetOrder: input.order } : {},
1494
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
1495
+ ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
1496
+ }
1497
+ );
1498
+ return result ? okTask(result.targetBoard, result.task, "Task transferred to target board.") : fail("Board or task not found.");
1499
+ }
1500
+ case "get_task": {
1501
+ if (!input.boardId || !input.taskId) return fail("get_task requires boardId and taskId.");
1502
+ const task = await getTask(projectRoot, input.boardId, input.taskId);
1503
+ return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
1504
+ }
1505
+ case "start_task": {
1506
+ if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
1507
+ return fail("start_task requires boardId, taskId, author, and transitionComment.");
1508
+ }
1509
+ let board = await getBoard4(projectRoot, input.boardId);
1510
+ let task = board?.tasks.find((candidate) => candidate.id === input.taskId);
1511
+ if (!board || !task) return fail("Board or task not found.");
1512
+ const readiness = evaluateContractGraphReadiness(board, task.id);
1513
+ if (!readiness.ready) {
1514
+ return fail(
1515
+ `Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
1516
+ );
1517
+ }
1518
+ if (board.lifecycle?.mode !== "managed") {
1519
+ const now = /* @__PURE__ */ new Date();
1520
+ const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
1521
+ status: "running",
1522
+ agentId: input.agentId ?? input.author,
1523
+ leaseId: input.leaseId ?? randomUUID2(),
1524
+ claimedAt: input.claimedAt ?? now.toISOString(),
1525
+ heartbeatAt: input.heartbeatAt ?? now.toISOString(),
1526
+ leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
1527
+ attempt: input.attempt ?? 1,
1528
+ maxAttempts: input.maxAttempts ?? 3
1529
+ });
1530
+ if (!assigned) return fail("Task assignment could not be started.");
1531
+ const started = await updateTask2(projectRoot, board.id, task.id, {
1532
+ status: "in_progress"
1533
+ });
1534
+ const current = started ?? assigned;
1535
+ const claimed = task;
1536
+ const currentTask = current.tasks.find((candidate) => candidate.id === claimed.id) ?? claimed;
1537
+ ctx.setCurrentKanbanTask?.(currentTask.id, current.id);
1538
+ return okTask(
1539
+ current,
1540
+ currentTask,
1541
+ "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."
1542
+ );
1543
+ }
1544
+ let stage = task.lifecycle?.currentStage;
1545
+ if (stage === "backlog") {
1546
+ const moved = await transitionTask(projectRoot, board.id, task.id, {
1547
+ to: "todo",
1548
+ actor: input.author,
1549
+ comment: input.transitionComment
1550
+ });
1551
+ if (!moved) return fail("Task could not enter Todo.");
1552
+ board = moved.board;
1553
+ task = moved.task;
1554
+ stage = task.lifecycle?.currentStage;
1555
+ }
1556
+ if (stage === "todo" || stage === "review") {
1557
+ const now = /* @__PURE__ */ new Date();
1558
+ const leaseId = input.leaseId ?? randomUUID2();
1559
+ const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
1560
+ status: "running",
1561
+ agentId: input.agentId ?? input.author,
1562
+ leaseId,
1563
+ claimedAt: input.claimedAt ?? now.toISOString(),
1564
+ heartbeatAt: input.heartbeatAt ?? now.toISOString(),
1565
+ leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
1566
+ attempt: input.attempt ?? 1,
1567
+ maxAttempts: input.maxAttempts ?? 3
1568
+ });
1569
+ if (!assigned) return fail("Task assignment could not be started.");
1570
+ const moved = await transitionTask(projectRoot, board.id, task.id, {
1571
+ to: "running",
1572
+ actor: input.author,
1573
+ comment: input.transitionComment
1574
+ });
1575
+ if (!moved) return fail("Task could not enter Running.");
1576
+ board = moved.board;
1577
+ task = moved.task;
1578
+ stage = task.lifecycle?.currentStage;
1579
+ }
1580
+ if (stage !== "running" || task.assignment?.status !== "running") {
1581
+ return fail(
1582
+ `start_task only accepts Backlog, Todo, Review repair, or live Running cards (current: ${stage ?? "unknown"}).`
1583
+ );
1584
+ }
1585
+ ctx.setCurrentKanbanTask(task.id, board.id);
1586
+ return okTask(
1587
+ board,
1588
+ task,
1589
+ "Task is active; runtime Kanban governance is now bound to this run."
1590
+ );
1591
+ }
1592
+ case "update_task": {
1593
+ if (!input.boardId || !input.taskId) return fail("update_task requires boardId and taskId.");
1594
+ const board = await updateTask2(projectRoot, input.boardId, input.taskId, taskPatch(input));
1595
+ return board ? okBoard(board, "Task updated.") : fail("Task not found.");
1596
+ }
1597
+ case "transition_task": {
1598
+ if (!input.boardId || !input.taskId || !input.lifecycleStage || !input.author || !input.transitionComment) {
1599
+ return fail(
1600
+ "transition_task requires boardId, taskId, lifecycleStage, author, and transitionComment."
1601
+ );
1602
+ }
1603
+ if (input.lifecycleStage === "done") {
1604
+ const boardBefore = await getBoard4(projectRoot, input.boardId);
1605
+ const taskBefore = boardBefore ? await getTask(projectRoot, input.boardId, input.taskId) : null;
1606
+ if (boardBefore && taskBefore && !taskBefore.verificationReport && (taskBefore.atomic || Boolean(taskBefore.successCriteria?.length))) {
1607
+ const preGate = await verifyTaskCompletion2(projectRoot, input.boardId, taskBefore.id, {
1608
+ persist: false
1609
+ });
1610
+ await updateTask2(projectRoot, input.boardId, taskBefore.id, {
1611
+ verificationReport: preGate.report,
1612
+ successCriteria: preGate.task.successCriteria
1613
+ });
1614
+ }
1615
+ }
1616
+ const result = await transitionTask(projectRoot, input.boardId, input.taskId, {
1617
+ to: input.lifecycleStage,
1618
+ actor: input.author,
1619
+ comment: input.transitionComment,
1620
+ ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
1621
+ ...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
1622
+ ...input.attachmentUrl !== void 0 ? {
1623
+ attachment: {
1624
+ url: input.attachmentUrl,
1625
+ type: input.attachmentType ?? "url",
1626
+ ...input.attachmentTitle !== void 0 ? { title: input.attachmentTitle } : {}
1627
+ }
1628
+ } : {},
1629
+ patch: taskPatch(input)
1630
+ });
1631
+ if (result && input.lifecycleStage === "done" && result.task.verificationReport) {
1632
+ recordKanbanVerificationEvidence(ctx, result.task.verificationReport);
1633
+ }
1634
+ return result ? okTask(result.board, result.task, `Task advanced to ${result.transition.to}.`) : fail("Board or task not found.");
1635
+ }
1636
+ case "repair_managed_projection": {
1637
+ if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
1638
+ return fail(
1639
+ "repair_managed_projection requires boardId, taskId, author, and transitionComment."
1640
+ );
1641
+ }
1642
+ const result = await repairManagedTaskProjection(projectRoot, input.boardId, input.taskId, {
1643
+ actor: input.author,
1644
+ comment: input.transitionComment
1645
+ });
1646
+ return result ? okTask(
1647
+ result.board,
1648
+ result.task,
1649
+ "Managed card projection repaired from lifecycle history."
1650
+ ) : fail("Board or task not found.");
1651
+ }
1652
+ case "move_task": {
1653
+ if (!input.boardId || !input.taskId || !input.targetColumnId) {
1654
+ return fail("move_task requires boardId, taskId, and targetColumnId.");
1655
+ }
1656
+ const board = await moveTask(
1657
+ projectRoot,
1658
+ input.boardId,
1659
+ input.taskId,
1660
+ input.targetColumnId,
1661
+ input.order
1662
+ );
1663
+ return board ? okBoard(board, "Task moved.") : fail("Move failed.");
1664
+ }
1665
+ case "delete_task": {
1666
+ if (!input.boardId || !input.taskId) return fail("delete_task requires boardId and taskId.");
1667
+ const board = await removeTask(projectRoot, input.boardId, input.taskId);
1668
+ if (board && ctx.currentKanbanTaskId === input.taskId) {
1669
+ ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
1670
+ }
1671
+ return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
1672
+ }
1673
+ case "set_chain": {
1674
+ if (!input.boardId || !input.taskIds?.length) {
1675
+ return fail("set_chain requires boardId and taskIds.");
1676
+ }
1677
+ const result = await setTaskChain(projectRoot, input.boardId, {
1678
+ taskIds: input.taskIds,
1679
+ ...input.chainId !== void 0 ? { chainId: input.chainId } : {},
1680
+ ...input.enforceDependencies !== void 0 ? { enforceDependencies: input.enforceDependencies } : {}
1681
+ });
1682
+ return result ? {
1683
+ ok: true,
1684
+ message: `Chain set: ${result.chainId}`,
1685
+ board: result.board,
1686
+ chain: result.tasks
1687
+ } : fail("Board or task not found.");
1688
+ }
1689
+ case "get_chain": {
1690
+ if (!input.boardId || !(input.taskId || input.chainId)) {
1691
+ return fail("get_chain requires boardId and taskId or chainId.");
1692
+ }
1693
+ const result = await getTaskChain(
1694
+ projectRoot,
1695
+ input.boardId,
1696
+ input.taskId ?? input.chainId ?? ""
1697
+ );
1698
+ return result ? {
1699
+ ok: true,
1700
+ message: `Chain loaded: ${result.chainId}`,
1701
+ board: result.board,
1702
+ chain: result.tasks
1703
+ } : fail("Chain not found.");
1704
+ }
1705
+ case "claim_task": {
1706
+ const result = await claimReadyTask(projectRoot, {
1707
+ ...input.boardId !== void 0 ? { boardId: input.boardId } : {},
1708
+ ...input.taskId !== void 0 ? { taskId: input.taskId } : {},
1709
+ ...assignmentInput(input),
1710
+ status: input.assignmentStatus ?? "queued"
1711
+ });
1712
+ return result ? okTask(result.board, result.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
1713
+ }
1714
+ case "release_task": {
1715
+ if (!input.boardId || !input.taskId) {
1716
+ return fail("release_task requires boardId and taskId.");
1717
+ }
1718
+ const board = await releaseTaskClaim(projectRoot, input.boardId, input.taskId, {
1719
+ ...input.releaseStatus !== void 0 ? { status: input.releaseStatus } : {},
1720
+ ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
1721
+ ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {}
1722
+ });
1723
+ return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
1724
+ }
1725
+ case "assign_task": {
1726
+ if (!input.boardId || !input.taskId) return fail("assign_task requires boardId and taskId.");
1727
+ const board = await assignTask(
1728
+ projectRoot,
1729
+ input.boardId,
1730
+ input.taskId,
1731
+ assignmentInput(input)
1732
+ );
1733
+ return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
1734
+ }
1735
+ case "mark_assignment": {
1736
+ if (!input.boardId || !input.taskId)
1737
+ return fail("mark_assignment requires boardId and taskId.");
1738
+ const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
1739
+ const board = await updateTaskAssignment(
1740
+ projectRoot,
1741
+ input.boardId,
1742
+ input.taskId,
1743
+ {
1744
+ ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
1745
+ ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
1746
+ ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
1747
+ ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
1748
+ ...input.error !== void 0 ? { error: input.error } : {},
1749
+ ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
1750
+ ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
1751
+ ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
1752
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
1753
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
1754
+ ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
1755
+ ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
1756
+ },
1757
+ input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
1758
+ );
1759
+ if (!board) return fail("Task not found.");
1760
+ if (assignmentStatus === "completed" && board.lifecycle?.mode !== "managed") {
1761
+ const envGate = readEnvGateEnforcement();
1762
+ const finalized = await finalizeTaskCompletion(projectRoot, board.id, input.taskId, {
1763
+ ...board.completionGate === void 0 && envGate !== void 0 ? { enforcement: envGate } : {},
1764
+ ...ctx.agentId !== void 0 ? { eventContext: { actor: ctx.agentId } } : {}
1765
+ });
1766
+ if (finalized) {
1767
+ if (finalized.gate.report) {
1768
+ recordKanbanVerificationEvidence(ctx, finalized.gate.report);
1769
+ }
1770
+ const gateSummary = {
1771
+ enforcement: finalized.gate.enforcement,
1772
+ allowed: finalized.gate.allowed,
1773
+ verdict: finalized.gate.verdict,
1774
+ issues: finalized.gate.issues.map((issue) => issue.message)
1775
+ };
1776
+ 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(" | ")}`;
1777
+ return {
1778
+ ...okTask(finalized.board, finalized.task, `Assignment updated. ${gateMessage}`),
1779
+ gate: gateSummary
1780
+ };
1781
+ }
1782
+ } else if (board.lifecycle?.mode === "managed") {
1783
+ const managedTask = board.tasks.find((candidate) => candidate.id === input.taskId);
1784
+ const stage = managedTask?.lifecycle?.currentStage;
1785
+ const actor = ctx.agentId ?? "kanban-agent";
1786
+ let transitionResult = null;
1787
+ const lifecycleWarnings = [];
1788
+ if (assignmentStatus === "running" && stage === "todo") {
1789
+ try {
1790
+ transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
1791
+ to: "running",
1792
+ actor,
1793
+ comment: "Work started."
1794
+ });
1795
+ } catch (err) {
1796
+ lifecycleWarnings.push(
1797
+ `Lifecycle transition to Running deferred: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
1798
+ );
1799
+ }
1800
+ }
1801
+ if (assignmentStatus === "completed" && stage === "running") {
1802
+ const comment = typeof input.lastResult === "string" && input.lastResult.trim().length > 0 ? input.lastResult.trim().slice(0, 1e3) : "Work completed.";
1803
+ try {
1804
+ transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
1805
+ to: "review",
1806
+ actor,
1807
+ comment,
1808
+ attachment: {
1809
+ url: `kanban://task/${input.taskId}/result`,
1810
+ title: "Worker completion result",
1811
+ type: "file"
1812
+ },
1813
+ patch: {
1814
+ ...input.agentId !== void 0 ? { assignedAgent: input.agentId } : {}
1815
+ }
1816
+ });
1817
+ } catch (err) {
1818
+ lifecycleWarnings.push(
1819
+ `Lifecycle transition to Review failed: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
1820
+ );
1821
+ }
1822
+ if (transitionResult) {
1823
+ const hasCriteria = (transitionResult.task.successCriteria?.length ?? 0) > 0 || transitionResult.task.atomic === true;
1824
+ if (hasCriteria) {
1825
+ try {
1826
+ const verResult = await verifyTaskCompletion2(projectRoot, board.id, input.taskId);
1827
+ if (verResult.report) {
1828
+ recordKanbanVerificationEvidence(ctx, verResult.report);
1829
+ }
1830
+ await updateTask2(projectRoot, board.id, input.taskId, {
1831
+ verificationReport: verResult.report,
1832
+ successCriteria: verResult.task.successCriteria
1833
+ });
1834
+ const verdict = verResult.report.verdict;
1835
+ if (verdict === "passed" && !resolveAutoAccept(board)) {
1836
+ lifecycleWarnings.push(
1837
+ "Verification passed, but this board does not auto-accept. The card is in Review awaiting an explicit transition_task to done."
1838
+ );
1839
+ } else if (verdict === "passed") {
1840
+ try {
1841
+ const doneResult = await transitionTask(projectRoot, board.id, input.taskId, {
1842
+ to: "done",
1843
+ actor,
1844
+ action: "Automated acceptance after verification",
1845
+ comment: "Auto-accepted: verification passed.",
1846
+ attachment: {
1847
+ url: `kanban://task/${input.taskId}/verification`,
1848
+ title: "Auto-verification result",
1849
+ type: "file"
1850
+ }
1851
+ });
1852
+ transitionResult = doneResult;
1853
+ } catch (acceptErr) {
1854
+ lifecycleWarnings.push(
1855
+ `Auto-accept to Done deferred: ${acceptErr instanceof Error ? acceptErr.message : String(acceptErr)}`
1856
+ );
1857
+ }
1858
+ } else {
1859
+ lifecycleWarnings.push(
1860
+ `Verification verdict: ${verdict} \u2014 card left in Review for manual acceptance.`
1861
+ );
1862
+ }
1863
+ } catch (verifyErr) {
1864
+ lifecycleWarnings.push(
1865
+ `Auto-verification error: ${verifyErr instanceof Error ? verifyErr.message : String(verifyErr)}`
1866
+ );
1867
+ }
1868
+ } else {
1869
+ lifecycleWarnings.push(
1870
+ "No automatic success criteria \u2014 card left in Review for manual verification."
1871
+ );
1872
+ }
1873
+ }
1874
+ }
1875
+ const responseBoard = transitionResult?.board ?? board;
1876
+ const responseTask = transitionResult?.task ?? managedTask;
1877
+ const msgParts = ["Assignment updated."];
1878
+ if (transitionResult) {
1879
+ msgParts.push(`Card advanced to ${transitionResult.transition.to}.`);
1880
+ }
1881
+ for (const w of lifecycleWarnings) msgParts.push(`Warning: ${w}`);
1882
+ return okTask(responseBoard, responseTask, msgParts.join(" "));
1883
+ }
1884
+ return okBoard(board, "Assignment updated.");
1885
+ }
1886
+ case "heartbeat_assignment": {
1887
+ if (!input.boardId || !input.taskId) {
1888
+ return fail("heartbeat_assignment requires boardId and taskId.");
1889
+ }
1890
+ const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
1891
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
1892
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
1893
+ ...input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
1894
+ });
1895
+ return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
1896
+ }
1897
+ case "recover_stale": {
1898
+ if (!input.boardId) return fail("recover_stale requires boardId.");
1899
+ const policyFields = [
1900
+ input.recoveryPolicyFailOnCostCeiling !== void 0,
1901
+ input.recoveryPolicyReleaseOnFailureKinds !== void 0,
1902
+ input.recoveryPolicyReleaseOnHeartbeatDue !== void 0,
1903
+ input.recoveryPolicyRetryPolicyOverride !== void 0
1904
+ ].some(Boolean);
1905
+ const result = await recoverStaleTaskAssignments(projectRoot, input.boardId, {
1906
+ ...input.recoveryMode !== void 0 ? { mode: input.recoveryMode } : {},
1907
+ ...input.recoveryNow !== void 0 ? { now: input.recoveryNow } : {},
1908
+ ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
1909
+ ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {},
1910
+ ...policyFields ? {
1911
+ policy: {
1912
+ ...input.recoveryPolicyFailOnCostCeiling !== void 0 ? { failWhenCostCeilingSet: input.recoveryPolicyFailOnCostCeiling } : {},
1913
+ ...input.recoveryPolicyReleaseOnFailureKinds !== void 0 ? {
1914
+ releaseOnFailureKinds: input.recoveryPolicyReleaseOnFailureKinds
1915
+ } : {},
1916
+ ...input.recoveryPolicyReleaseOnHeartbeatDue !== void 0 ? {
1917
+ releaseOnHeartbeatDue: input.recoveryPolicyReleaseOnHeartbeatDue
1918
+ } : {},
1919
+ ...input.recoveryPolicyRetryPolicyOverride !== void 0 ? {
1920
+ retryPolicyOverride: input.recoveryPolicyRetryPolicyOverride
1921
+ } : {}
1922
+ }
1923
+ } : {}
1924
+ });
1925
+ return result ? {
1926
+ ok: true,
1927
+ message: `Recovered ${result.tasks.length} stale assignment(s).`,
1928
+ board: result.board,
1929
+ recoveredTasks: result.tasks
1930
+ } : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
1931
+ }
1932
+ default:
1933
+ return void 0;
1934
+ }
1460
1935
  }
1461
- function blockingTitles(board, task) {
1462
- return getDependencyReadinessIssues(board, task).map((issue) => {
1463
- const dependency = board.tasks.find((candidate) => candidate.id === issue.dependencyId);
1464
- if (!dependency) return `${issue.dependencyId} (missing)`;
1465
- return dependency.title;
1466
- });
1936
+
1937
+ // src/kanban-presence.ts
1938
+ import { touchKanbanPresence as touchKanbanPresence2 } from "@wrongstack/kanban";
1939
+ function createKanbanPresenceWrapper(projectRoot, input, ctx) {
1940
+ return async (result) => {
1941
+ const boardId = result.board?.id ?? input.boardId;
1942
+ if (!result.ok || !boardId || !ctx.session?.id || !ctx.agentId) return result;
1943
+ try {
1944
+ const board = await touchKanbanPresence2(projectRoot, boardId, {
1945
+ sessionId: ctx.session.id,
1946
+ agentId: ctx.agentId,
1947
+ agentName: ctx.agentName,
1948
+ taskId: input.taskId ?? result.task?.id,
1949
+ runTaskId: input.runTaskId
1950
+ });
1951
+ return board ? { ...result, board } : result;
1952
+ } catch {
1953
+ return result;
1954
+ }
1955
+ };
1467
1956
  }
1468
- var PRIORITY_ORDER = {
1469
- critical: 0,
1470
- high: 1,
1471
- medium: 2,
1472
- low: 3
1473
- };
1474
- function orderTasksForTodos(board, tasks) {
1475
- const columnOrder = new Map(board.columns.map((column) => [column.id, column.order]));
1476
- const baseline = [...tasks].sort(
1477
- (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)
1478
- );
1479
- const included = new Set(baseline.map((task) => task.id));
1480
- const remaining = new Map(baseline.map((task) => [task.id, task]));
1481
- const emitted = [];
1482
- const done = /* @__PURE__ */ new Set();
1483
- while (remaining.size > 0) {
1484
- const ready = baseline.filter(
1485
- (task) => remaining.has(task.id) && (task.dependsOn ?? []).every(
1486
- (dependencyId) => !included.has(dependencyId) || done.has(dependencyId)
1487
- )
1488
- );
1489
- if (ready.length === 0) break;
1490
- for (const task of ready) {
1491
- remaining.delete(task.id);
1492
- done.add(task.id);
1493
- emitted.push(task);
1957
+
1958
+ // src/kanban-serializer.ts
1959
+ var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
1960
+ var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
1961
+ "get_board",
1962
+ "export_markdown",
1963
+ "export_task_graph"
1964
+ ]);
1965
+ function serializeKanbanOutput(output, input) {
1966
+ const action = input && typeof input === "object" ? input.action : void 0;
1967
+ const board = output.board;
1968
+ if (board) {
1969
+ const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
1970
+ let boardBytes = 0;
1971
+ if (!keepFull) {
1972
+ try {
1973
+ boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
1974
+ } catch {
1975
+ boardBytes = 0;
1976
+ }
1977
+ }
1978
+ if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
1979
+ const columns = {};
1980
+ for (const column of board.columns) {
1981
+ columns[column.title || column.id] = board.tasks.filter(
1982
+ (task) => task.columnId === column.id
1983
+ ).length;
1984
+ }
1985
+ const compact = {
1986
+ ...output,
1987
+ board: {
1988
+ id: board.id,
1989
+ title: board.title,
1990
+ columns,
1991
+ totalTasks: board.tasks.length,
1992
+ note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
1993
+ }
1994
+ };
1995
+ return JSON.stringify(compact, null, 2);
1996
+ }
1997
+ }
1998
+ return JSON.stringify(output, null, 2);
1999
+ }
2000
+
2001
+ // src/kanban-tool-schema.ts
2002
+ var KANBAN_TOOL_DESCRIPTION = "Durable project task boards: create and move cards, record checks, notes, links and assignments. The board is a record of the work, not a permit for it \u2014 nothing here gates other tools. Managed boards additionally enforce ordered Backlog \u2192 Todo \u2192 Running \u2192 Review \u2192 Done transitions; release_managed_lifecycle turns that off.";
2003
+ var KANBAN_TOOL_USAGE_HINT = 'Track substantial or multi-step work so it survives the session; a trivial edit or a question needs no card. Work stays on ONE board: call list_boards first and add_task to the board this project already uses. create_board is for a genuinely separate line of work, not for each new piece of it \u2014 a second board splits the same effort in two, and a board holding a single card is the usual sign. Common flow: list_boards or search_tasks to orient, add_task to record work, start_task when you begin, update_check with checkStatus "passed" to tick acceptance criteria (read their ids from get_task), then transition_task. On a managed board a refused transition names the field it wants \u2014 supply it and retry. When the acceptance criterion is something a machine can run, say so: set checkType ("command", "test", "file_exists", "file_matches", "git_diff", "metric") and put the command, pattern or path in checkNotes, then verify_completion executes it and the result is real evidence. Leave checkType off (or "manual") only for criteria that genuinely need a human eye \u2014 a manual check records your assertion, it does not test anything.';
2004
+ var KANBAN_INPUT_SCHEMA = {
2005
+ type: "object",
2006
+ properties: {
2007
+ action: {
2008
+ type: "string",
2009
+ enum: [
2010
+ "list_boards",
2011
+ "get_board",
2012
+ "create_board",
2013
+ "duplicate_board",
2014
+ "update_board",
2015
+ "adopt_managed_lifecycle",
2016
+ "release_managed_lifecycle",
2017
+ "delete_board",
2018
+ "generate_board",
2019
+ "export_markdown",
2020
+ "export_task_graph",
2021
+ "sync_task_graph",
2022
+ "create_from_graph",
2023
+ "import_session_tasks",
2024
+ "search_tasks",
2025
+ "ready_tasks",
2026
+ "snapshot",
2027
+ "workbench",
2028
+ "add_task",
2029
+ "split_task",
2030
+ "merge_tasks",
2031
+ "copy_task",
2032
+ "transfer_task",
2033
+ "get_task",
2034
+ "start_task",
2035
+ "update_task",
2036
+ "transition_task",
2037
+ "repair_managed_projection",
2038
+ "move_task",
2039
+ "delete_task",
2040
+ "set_chain",
2041
+ "get_chain",
2042
+ "claim_task",
2043
+ "release_task",
2044
+ "assign_task",
2045
+ "mark_assignment",
2046
+ "heartbeat_assignment",
2047
+ "recover_stale",
2048
+ "events",
2049
+ "queue_health",
2050
+ "add_dependency",
2051
+ "add_goal_metric",
2052
+ "update_goal_metric",
2053
+ "add_check",
2054
+ "update_check",
2055
+ "remove_check",
2056
+ "add_note",
2057
+ "add_link",
2058
+ "verify_completion",
2059
+ "split_atomic",
2060
+ "assess_atomicity",
2061
+ "propose_decomposition",
2062
+ "get_contract_graph",
2063
+ "configure_contract_graph",
2064
+ "upsert_contract_node",
2065
+ "remove_contract_node",
2066
+ "add_contract_edge",
2067
+ "remove_contract_edge"
2068
+ ]
2069
+ },
2070
+ boardId: { type: "string" },
2071
+ taskId: { type: "string" },
2072
+ taskIds: { type: "array", items: { type: "string" } },
2073
+ chainId: { type: "string" },
2074
+ fromNodeId: { type: "string" },
2075
+ toNodeId: { type: "string" },
2076
+ baseline: { oneOf: [{ type: "string" }, { type: "number" }] },
2077
+ threshold: { oneOf: [{ type: "string" }, { type: "number" }] },
2078
+ columnId: { type: "string" },
2079
+ targetBoardId: { type: "string" },
2080
+ targetColumnId: { type: "string" },
2081
+ title: { type: "string" },
2082
+ description: { type: "string" },
2083
+ dueDate: { type: "string" },
2084
+ tags: { type: "array", items: { type: "string" } },
2085
+ labels: { type: "array", items: { type: "string" } },
2086
+ priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
2087
+ taskType: {
2088
+ type: "string",
2089
+ enum: ["feature", "bugfix", "refactor", "docs", "test", "chore"]
2090
+ },
2091
+ status: {
2092
+ type: "string",
2093
+ enum: [
2094
+ "pending",
2095
+ "ready",
2096
+ "in_progress",
2097
+ "blocked",
2098
+ "review",
2099
+ "completed",
2100
+ "failed",
2101
+ "archived"
2102
+ ]
2103
+ },
2104
+ order: { type: "number" },
2105
+ query: { type: "string" },
2106
+ limit: { type: "number" },
2107
+ agentId: { type: "string" },
2108
+ name: { type: "string" },
2109
+ role: { type: "string" },
2110
+ provider: { type: "string" },
2111
+ model: { type: "string" },
2112
+ fallbackProfile: { type: "string" },
2113
+ fallbackModels: { type: "array", items: { type: "string" } },
2114
+ tools: { type: "array", items: { type: "string" } },
2115
+ allowedCapabilities: { type: "array", items: { type: "string" } },
2116
+ leaseId: { type: "string" },
2117
+ claimedAt: { type: "string" },
2118
+ heartbeatAt: { type: "string" },
2119
+ leaseExpiresAt: { type: "string" },
2120
+ attempt: { type: "number" },
2121
+ maxAttempts: { type: "number" },
2122
+ subagentId: { type: "string" },
2123
+ runTaskId: { type: "string" },
2124
+ lastResult: { type: "string" },
2125
+ error: { type: "string" },
2126
+ expectedLeaseId: { type: "string" },
2127
+ assignmentStatus: {
2128
+ type: "string",
2129
+ enum: ["assigned", "queued", "running", "completed", "failed", "cancelled"]
2130
+ },
2131
+ lifecycleStage: {
2132
+ type: "string",
2133
+ enum: ["backlog", "todo", "running", "review", "done"]
2134
+ },
2135
+ transitionAction: { type: "string" },
2136
+ transitionComment: { type: "string" },
2137
+ tickChecks: {
2138
+ type: "array",
2139
+ items: {
2140
+ type: "object",
2141
+ properties: {
2142
+ checkId: { type: "string" },
2143
+ checkStatus: { type: "string", enum: ["passed", "failed", "skipped"] }
2144
+ },
2145
+ required: ["checkId", "checkStatus"]
2146
+ },
2147
+ description: "`transition_task` (to=done only): flip one or more manual criteria to `passed` before the gate fires. Read ids from kanban get_task. Non-manual criteria are refused."
2148
+ },
2149
+ attachmentUrl: { type: "string" },
2150
+ attachmentTitle: { type: "string" },
2151
+ attachmentType: {
2152
+ type: "string",
2153
+ enum: ["issue", "pr", "doc", "commit", "design", "file", "url", "other"]
2154
+ },
2155
+ releaseStatus: { type: "string", enum: ["pending", "ready", "blocked"] },
2156
+ releaseReason: { type: "string" },
2157
+ clearAssignee: { type: "boolean" },
2158
+ recoveryMode: { type: "string", enum: ["auto", "release", "retry", "fail"] },
2159
+ recoveryNow: { type: "string" },
2160
+ recoveryPolicyFailOnCostCeiling: { type: "boolean" },
2161
+ recoveryPolicyReleaseOnFailureKinds: { type: "array", items: { type: "string" } },
2162
+ recoveryPolicyReleaseOnHeartbeatDue: { type: "boolean" },
2163
+ recoveryPolicyRetryPolicyOverride: {
2164
+ type: "string",
2165
+ enum: ["off", "incremental", "exponential"]
2166
+ },
2167
+ assignee: { type: "string" },
2168
+ costCeilingUsd: { type: "number" },
2169
+ retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
2170
+ lastFailureKind: { type: "string" },
2171
+ dependsOn: {
2172
+ type: "array",
2173
+ items: { type: "string" },
2174
+ description: "Task ids this card waits on. On update_task an explicit empty array clears them \u2014 use it when a dependency was recorded in error rather than completing work nobody wants."
2175
+ },
2176
+ atomic: {
2177
+ type: "boolean",
2178
+ description: "Composite parent (true) or executable leaf (false). Set false to make a stranded parent a leaf again after its children were dropped."
2179
+ },
2180
+ childTaskIds: {
2181
+ type: "array",
2182
+ items: { type: "string" },
2183
+ description: "Children of a composite parent. On update_task an explicit empty array detaches them all."
2184
+ },
2185
+ estimatedHours: { type: "number" },
2186
+ actualHours: { type: "number" },
2187
+ taskGraph: { type: "object" },
2188
+ graphId: { type: "string" },
2189
+ specId: { type: "string" },
2190
+ specRequirementId: { type: "string" },
2191
+ sourceSystem: { type: "string" },
2192
+ phaseId: { type: "string" },
2193
+ preserveOriginTaskIds: { type: "boolean" },
2194
+ includeArchived: { type: "boolean" },
2195
+ archiveMissingTasks: { type: "boolean" },
2196
+ preserveManualDependencies: { type: "boolean" },
2197
+ dependencyTaskId: { type: "string" },
2198
+ enforceDependencies: { type: "boolean" },
2199
+ childTitles: { type: "array", items: { type: "string" } },
2200
+ inheritAssignment: { type: "boolean" },
2201
+ inheritLabels: { type: "boolean" },
2202
+ inheritSuccessCriteria: { type: "boolean" },
2203
+ inheritGoalMetrics: { type: "boolean" },
2204
+ inheritDependencies: { type: "boolean" },
2205
+ chainChildren: { type: "boolean" },
2206
+ rewireDependents: { type: "boolean" },
2207
+ closeSourceTasks: { type: "boolean" },
2208
+ metricId: { type: "string" },
2209
+ metricName: { type: "string" },
2210
+ metricTarget: { oneOf: [{ type: "string" }, { type: "number" }] },
2211
+ metricCurrent: { oneOf: [{ type: "string" }, { type: "number" }] },
2212
+ metricUnit: { type: "string" },
2213
+ metricStatus: { type: "string", enum: ["pending", "met", "missed", "waived"] },
2214
+ metricNotes: { type: "string" },
2215
+ checkId: { type: "string" },
2216
+ checkDescription: { type: "string" },
2217
+ checkStatus: { type: "string", enum: ["pending", "passed", "failed", "skipped"] },
2218
+ checkType: {
2219
+ type: "string",
2220
+ // Only types a verifier can actually execute. `manual` is the default and
2221
+ // means a human or agent asserts the status by hand. The rest are run by
2222
+ // `verify_completion` against the default deterministic registry. Types
2223
+ // with no plugin in that registry (`auto`, `review`, `agent`, `council`)
2224
+ // are deliberately omitted: offering them would produce criteria that
2225
+ // silently report `skipped — no verifier plugin registered`.
2226
+ enum: ["manual", "command", "test", "file_exists", "file_matches", "git_diff", "metric"],
2227
+ description: 'How this acceptance criterion is verified. Default "manual" (status set by hand). Any other value makes verify_completion execute it, so the criterion becomes real evidence rather than a self-assertion. Pair with checkNotes.'
2228
+ },
2229
+ checkNotes: {
2230
+ type: "string",
2231
+ description: 'The executable body for a non-manual checkType, read in preference to checkDescription. command/test: the shell command or test pattern. file_exists: the path. file_matches: JSON {"file","pattern","flags"}. git_diff: JSON {"expectedFiles","minChanges","maxChanges"}.'
2232
+ },
2233
+ // ── Contract map ───────────────────────────────────────────────────
2234
+ // The card contract: what this work targets, what it must not break, what
2235
+ // it risks, and what verifies it. Advisory by default — the readiness gate
2236
+ // deliberately does not require map structure, so a map is an operator
2237
+ // review aid, not work the model must complete before implementing.
2238
+ contractEnforcement: {
2239
+ type: "string",
2240
+ enum: ["off", "advisory", "strict"],
2241
+ description: "Board-level contract map enforcement. Default when first configured: advisory."
2242
+ },
2243
+ contractNodeId: { type: "string" },
2244
+ contractNodeKind: {
2245
+ type: "string",
2246
+ enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"],
2247
+ description: "objective = what this card is for; guardrail = what must keep working; risk = what could go wrong; component/artifact = what it touches; verification = what settles it."
2248
+ },
2249
+ contractNodeTitle: { type: "string" },
2250
+ contractNodeDescription: { type: "string" },
2251
+ contractNodeState: {
2252
+ type: "string",
2253
+ enum: ["unknown", "active", "satisfied", "violated", "waived", "resolved"]
2254
+ },
2255
+ contractNodeEnforcement: {
2256
+ type: "string",
2257
+ enum: ["blocking", "advisory", "informational"]
2258
+ },
2259
+ /** Bind a node to an acceptance criterion or goal metric already on the task. */
2260
+ contractCheckId: { type: "string" },
2261
+ contractMetricId: { type: "string" },
2262
+ contractWaiverReason: {
2263
+ type: "string",
2264
+ description: 'Required, with an actor, when contractNodeState is "waived".'
2265
+ },
2266
+ contractEdgeId: { type: "string" },
2267
+ contractEdgeFrom: {
2268
+ type: "string",
2269
+ description: 'A contract node id, or a task id (bare or "task:<id>") for the card endpoint.'
2270
+ },
2271
+ contractEdgeTo: { type: "string" },
2272
+ contractEdgeType: {
2273
+ type: "string",
2274
+ enum: [
2275
+ "targets",
2276
+ "affects",
2277
+ "must_preserve",
2278
+ "exposes",
2279
+ "verified_by",
2280
+ "conflicts_with",
2281
+ "derived_from",
2282
+ "relates_to"
2283
+ ]
2284
+ },
2285
+ contractEdgeRationale: { type: "string" },
2286
+ note: { type: "string" },
2287
+ author: { type: "string" },
2288
+ url: { type: "string" },
2289
+ linkTitle: { type: "string" },
2290
+ linkType: {
2291
+ type: "string",
2292
+ enum: ["issue", "pr", "doc", "commit", "design", "file", "url", "other"]
2293
+ },
2294
+ context: { type: "string" },
2295
+ columns: { type: "array", items: { type: "string" } },
2296
+ generatedBy: { type: "string" },
2297
+ includeTasks: { type: "boolean" },
2298
+ includeCompletedTasks: { type: "boolean" },
2299
+ preserveAssignment: { type: "boolean" },
2300
+ preserveDependencies: { type: "boolean" },
2301
+ moveTasksToColumnId: { type: "string" },
2302
+ atomicityMode: { type: "string", enum: ["off", "assess", "enforce"] },
2303
+ atomicityDecomposition: { type: "string", enum: ["auto", "propose"] },
2304
+ gateEnforcement: {
2305
+ type: "string",
2306
+ // WS-023: `'off'` is deliberately absent. The agent whose work this gate
2307
+ // checks must not be able to switch it off; it may only tighten. Turning
2308
+ // a gate off stays a human decision, made through board config.
2309
+ enum: ["strict", "soft"]
2310
+ },
2311
+ subtasks: {
2312
+ type: "array",
2313
+ minItems: 2,
2314
+ items: {
2315
+ type: "object",
2316
+ properties: {
2317
+ title: { type: "string" },
2318
+ description: { type: "string" },
2319
+ successCriteria: { type: "array", items: { type: "string" } },
2320
+ dependsOnIndex: { type: "array", items: { type: "number" } }
2321
+ },
2322
+ required: ["title"]
2323
+ }
1494
2324
  }
1495
- }
1496
- for (const task of baseline) if (remaining.has(task.id)) emitted.push(task);
1497
- return emitted;
1498
- }
1499
- function sameTodos(left, right) {
1500
- return left.length === right.length && left.every((todo, index) => {
1501
- const candidate = right[index];
1502
- 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,
1503
- // the rows are otherwise identical and the unblocking would never
1504
- // reach the model.
1505
- (candidate.blockedBy ?? []).join("\0") === (todo.blockedBy ?? []).join("\0");
1506
- });
1507
- }
1508
- function applyManagedKanbanBoardToTodos(context, board) {
1509
- const metaKanban = context.meta["kanban"];
1510
- const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
1511
- const activeBoardId2 = context.currentKanbanBoardId ?? (typeof metaBoardId === "string" ? metaBoardId : void 0);
1512
- if (!activeBoardId2 || board.id !== activeBoardId2 || board.lifecycle?.mode !== "managed") {
1513
- return [...context.todos];
1514
- }
1515
- const projectedTodos = orderTasksForTodos(
1516
- board,
1517
- board.tasks.filter(
1518
- (task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
1519
- )
1520
- ).map((task) => managedTodoFromTask(task, board));
1521
- if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
1522
- suppressedTodoMirrors.add(context);
1523
- try {
1524
- context.state.replaceTodos(projectedTodos);
1525
- } finally {
1526
- suppressedTodoMirrors.delete(context);
1527
- }
1528
- notifyTodoUpdate(context, context.todos);
1529
- broadcastTodoUpdate(context, context.todos);
1530
- return [...context.todos];
1531
- }
2325
+ },
2326
+ required: ["action"]
2327
+ };
1532
2328
 
1533
2329
  // src/kanban.ts
1534
2330
  var kanbanTool = {
@@ -1537,8 +2333,6 @@ var kanbanTool = {
1537
2333
  description: KANBAN_TOOL_DESCRIPTION,
1538
2334
  usageHint: KANBAN_TOOL_USAGE_HINT,
1539
2335
  permission: "confirm",
1540
- // WS-046: gives permission decisions something to key on.
1541
- // The action performed; kanban has no single file or path subject.
1542
2336
  subjectKey: "action",
1543
2337
  mutating: true,
1544
2338
  capabilities: ["fs.write"],
@@ -1553,873 +2347,29 @@ var kanbanTool = {
1553
2347
  const result = await (async () => {
1554
2348
  const decompositionResult = await handleKanbanDecompositionAction(projectRoot, input, ctx);
1555
2349
  if (decompositionResult !== void 0) return decompositionResult;
1556
- switch (input.action) {
1557
- case "list_boards": {
1558
- const boards = await listBoards2(projectRoot);
1559
- return { ok: true, message: `${boards.length} board(s).`, boards };
1560
- }
1561
- case "get_board": {
1562
- const board = await requireBoard(projectRoot, input.boardId);
1563
- return board ? okBoard(board) : fail("Board not found.");
1564
- }
1565
- case "create_board": {
1566
- if (!input.title) return fail("create_board requires title.");
1567
- const existing = (await listBoards2(projectRoot)).filter(
1568
- (candidate) => (candidate.kind ?? "project") === "project"
1569
- );
1570
- const board = await createBoard2(projectRoot, boardCreateInput(input, input.title));
1571
- 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(
1572
- ", "
1573
- )}${existing.length > 3 ? ", \u2026" : ""}. If this work belongs to one of them, add_task there instead and delete this board.` : "";
1574
- return { ok: true, message: `Board created: ${board.title}.${note}`, board };
1575
- }
1576
- case "update_board": {
1577
- if (!input.boardId) return fail("update_board requires boardId.");
1578
- const board = await updateBoard2(projectRoot, input.boardId, boardUpdatePatch(input));
1579
- return board ? okBoard(board, "Board updated.") : fail("Board not found.");
1580
- }
1581
- case "adopt_managed_lifecycle": {
1582
- if (!input.boardId || !input.author || !input.transitionComment) {
1583
- return fail(
1584
- "adopt_managed_lifecycle requires boardId, author, transitionComment, and five ordered columns."
1585
- );
1586
- }
1587
- if (input.columns?.length !== 5) {
1588
- return fail(
1589
- "adopt_managed_lifecycle columns must be ordered as backlog, todo, running, review, done."
1590
- );
1591
- }
1592
- const [backlog, todo, running, review, done] = input.columns;
1593
- if (!backlog || !todo || !running || !review || !done) {
1594
- return fail("adopt_managed_lifecycle columns must contain five nonblank ids.");
1595
- }
1596
- const board = await adoptManagedLifecycle(projectRoot, input.boardId, {
1597
- columns: { backlog, todo, running, review, done },
1598
- actor: input.author,
1599
- comment: input.transitionComment
1600
- });
1601
- return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
1602
- }
1603
- // Adoption used to be a one-way door: the strict lifecycle carries
1604
- // acceptance-criteria, verification-report, review-evidence and
1605
- // one-stage-at-a-time gates, and nothing on the tool surface could
1606
- // undo it, so a board adopted once kept its ceremony forever. The
1607
- // gates are worth having where a fleet is supervised; they are not
1608
- // worth being unable to leave. Cards and columns are untouched.
1609
- case "release_managed_lifecycle": {
1610
- if (!input.boardId) return fail("release_managed_lifecycle requires boardId.");
1611
- const board = await updateBoard2(projectRoot, input.boardId, { lifecycle: null });
1612
- return board ? okBoard(
1613
- board,
1614
- "Managed lifecycle released; the board now tracks work without strict gates."
1615
- ) : fail("Board not found.");
1616
- }
1617
- case "duplicate_board": {
1618
- if (!input.boardId) return fail("duplicate_board requires boardId.");
1619
- const board = await duplicateBoard(
1620
- projectRoot,
1621
- input.boardId,
1622
- duplicateBoardOptions(input)
1623
- );
1624
- return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
1625
- }
1626
- case "delete_board": {
1627
- if (!input.boardId) return fail("delete_board requires boardId.");
1628
- const removed = await removeBoard2(projectRoot, input.boardId);
1629
- return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
1630
- }
1631
- case "generate_board": {
1632
- if (!input.description) return fail("generate_board requires description.");
1633
- const boardInput = createBoardFromText({
1634
- description: input.description,
1635
- ...input.title !== void 0 ? { title: input.title } : {},
1636
- ...input.context !== void 0 ? { context: input.context } : {}
1637
- });
1638
- const board = await createBoard2(projectRoot, boardInput);
1639
- for (const taskInput2 of parseLinesIntoTasks(
1640
- input.description,
1641
- board.columns[0]?.id ?? "backlog"
1642
- )) {
1643
- await addTask(projectRoot, board.id, taskInput2);
1644
- }
1645
- return okBoard(await getBoard3(projectRoot, board.id) ?? board, "Board generated.");
1646
- }
1647
- case "export_markdown": {
1648
- const board = await requireBoard(projectRoot, input.boardId);
1649
- if (!board) return fail("Board not found.");
1650
- return {
1651
- ok: true,
1652
- message: "Board exported.",
1653
- board,
1654
- markdown: exportBoardAsMarkdown(board)
1655
- };
1656
- }
1657
- case "export_task_graph": {
1658
- if (!input.boardId) return fail("export_task_graph requires boardId.");
1659
- const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
1660
- ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
1661
- ...input.specId !== void 0 ? { specId: input.specId } : {},
1662
- ...input.title !== void 0 ? { title: input.title } : {},
1663
- ...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
1664
- ...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
1665
- });
1666
- if (!exported) return fail("Board not found.");
1667
- return {
1668
- ok: true,
1669
- message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
1670
- board: exported.board,
1671
- taskGraph: serializeTaskGraph(exported.graph)
1672
- };
1673
- }
1674
- case "sync_task_graph": {
1675
- if (!input.boardId || !input.taskGraph) {
1676
- return fail("sync_task_graph requires boardId and taskGraph.");
1677
- }
1678
- const graph = deserializeTaskGraph2(input.taskGraph);
1679
- const result2 = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
1680
- ...input.title !== void 0 ? { title: input.title } : {},
1681
- ...input.description !== void 0 ? { description: input.description } : {},
1682
- ...input.tags !== void 0 ? { tags: input.tags } : {},
1683
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
1684
- ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
1685
- ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
1686
- ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
1687
- ...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
1688
- ...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
1689
- });
1690
- return result2 ? {
1691
- ok: true,
1692
- message: `Task graph synced: ${result2.createdTaskIds.length} created, ${result2.updatedTaskIds.length} updated, ${result2.archivedTaskIds.length} archived.`,
1693
- board: result2.board
1694
- } : fail("Board not found.");
1695
- }
1696
- case "create_from_graph": {
1697
- if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
1698
- const graph = deserializeTaskGraph2(input.taskGraph);
1699
- const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
1700
- ...input.title !== void 0 ? { title: input.title } : {},
1701
- ...input.description !== void 0 ? { description: input.description } : {},
1702
- ...input.tags !== void 0 ? { tags: input.tags } : {},
1703
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
1704
- ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
1705
- ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
1706
- ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
1707
- });
1708
- return {
1709
- ok: true,
1710
- message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
1711
- board
1712
- };
1713
- }
1714
- case "import_session_tasks": {
1715
- const taskPath = ctx.meta?.["task.path"];
1716
- if (!taskPath) return fail("No session task file for this session.");
1717
- const file = await loadTasks2(taskPath);
1718
- if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
1719
- const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
1720
- const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
1721
- const tags = ["session", `session:${sessionId}`];
1722
- const existing = (await listBoards2(projectRoot)).find(
1723
- (b) => b.tags?.includes(`session:${sessionId}`)
1724
- );
1725
- if (existing) {
1726
- const result2 = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
1727
- sourceSystem: "session",
1728
- tags,
1729
- archiveMissingTasks: true,
1730
- includeCompletedTasks: true
1731
- });
1732
- return result2 ? {
1733
- ok: true,
1734
- message: `Synced ${file.tasks.length} session tasks into board "${result2.board.title}".`,
1735
- board: result2.board
1736
- } : fail("Session board vanished mid-sync.");
1737
- }
1738
- const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
1739
- title: `Session tasks (${sessionId.slice(0, 8)})`,
1740
- sourceSystem: "session",
1741
- tags
1742
- });
1743
- return {
1744
- ok: true,
1745
- message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
1746
- board
1747
- };
1748
- }
1749
- case "search_tasks": {
1750
- const tasks = await searchKanban(projectRoot, {
1751
- query: input.query,
1752
- boardId: input.boardId,
1753
- assignedAgent: input.agentId,
1754
- status: input.status,
1755
- priority: input.priority,
1756
- label: input.labels?.[0],
1757
- chainId: input.chainId
1758
- });
1759
- return { ok: true, message: `${tasks.length} task(s) matched.`, tasks };
1760
- }
1761
- case "ready_tasks": {
1762
- const tasks = await listReadyTasks(projectRoot, {
1763
- query: input.query,
1764
- boardId: input.boardId,
1765
- assignedAgent: input.agentId,
1766
- priority: input.priority,
1767
- label: input.labels?.[0],
1768
- chainId: input.chainId,
1769
- limit: input.limit
1770
- });
1771
- return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
1772
- }
1773
- case "snapshot": {
1774
- const snapshot = await getKanbanOrchestrationSnapshot2(projectRoot, {
1775
- query: input.query,
1776
- boardId: input.boardId,
1777
- assignedAgent: input.agentId,
1778
- status: input.status,
1779
- priority: input.priority,
1780
- label: input.labels?.[0],
1781
- chainId: input.chainId
1782
- });
1783
- return {
1784
- ok: true,
1785
- message: `${snapshot.ready.length} ready, ${snapshot.running.length} running, ${snapshot.blocked.length} blocked.`,
1786
- snapshot
1787
- };
1788
- }
1789
- case "add_task": {
1790
- if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
1791
- const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
1792
- if (!result2) return fail("Board not found.");
1793
- return okTask(result2.board, result2.task, `Task added.${atomicityNudge(result2.task)}`);
1794
- }
1795
- case "split_task": {
1796
- if (!input.boardId || !input.taskId || !input.childTitles?.length) {
1797
- return fail("split_task requires boardId, taskId, and childTitles.");
1798
- }
1799
- return handleSplitTask(projectRoot, input, {});
1800
- }
1801
- case "merge_tasks": {
1802
- if (!input.boardId || !input.taskIds?.length || !input.title) {
1803
- return fail("merge_tasks requires boardId, taskIds, and title.");
1804
- }
1805
- const result2 = await mergeTasks(projectRoot, input.boardId, {
1806
- taskIds: input.taskIds,
1807
- title: input.title,
1808
- ...input.description !== void 0 ? { description: input.description } : {},
1809
- ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
1810
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
1811
- ...input.closeSourceTasks !== void 0 ? { closeSourceTasks: input.closeSourceTasks } : {}
1812
- });
1813
- return result2 ? okTask(result2.board, result2.task, "Tasks merged.") : fail("Board or task not found.");
1814
- }
1815
- case "copy_task": {
1816
- if (!input.boardId || !input.taskId || !input.targetBoardId) {
1817
- return fail("copy_task requires boardId, taskId, and targetBoardId.");
1818
- }
1819
- const result2 = await copyTaskToBoard(
1820
- projectRoot,
1821
- input.boardId,
1822
- input.taskId,
1823
- input.targetBoardId,
1824
- {
1825
- ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
1826
- ...input.order !== void 0 ? { targetOrder: input.order } : {},
1827
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
1828
- ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
1829
- }
1830
- );
1831
- return result2 ? okTask(result2.targetBoard, result2.task, "Task copied to target board.") : fail("Board or task not found.");
1832
- }
1833
- case "transfer_task": {
1834
- if (!input.boardId || !input.taskId || !input.targetBoardId) {
1835
- return fail("transfer_task requires boardId, taskId, and targetBoardId.");
1836
- }
1837
- const result2 = await transferTaskToBoard(
1838
- projectRoot,
1839
- input.boardId,
1840
- input.taskId,
1841
- input.targetBoardId,
1842
- {
1843
- ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
1844
- ...input.order !== void 0 ? { targetOrder: input.order } : {},
1845
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
1846
- ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
1847
- }
1848
- );
1849
- return result2 ? okTask(result2.targetBoard, result2.task, "Task transferred to target board.") : fail("Board or task not found.");
1850
- }
1851
- case "get_task": {
1852
- if (!input.boardId || !input.taskId)
1853
- return fail("get_task requires boardId and taskId.");
1854
- const task = await getTask(projectRoot, input.boardId, input.taskId);
1855
- return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
1856
- }
1857
- case "start_task": {
1858
- if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
1859
- return fail("start_task requires boardId, taskId, author, and transitionComment.");
1860
- }
1861
- let board = await getBoard3(projectRoot, input.boardId);
1862
- let task = board?.tasks.find((candidate) => candidate.id === input.taskId);
1863
- if (!board || !task) return fail("Board or task not found.");
1864
- const readiness = evaluateContractGraphReadiness(board, task.id);
1865
- if (!readiness.ready) {
1866
- return fail(
1867
- `Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
1868
- );
1869
- }
1870
- if (board.lifecycle?.mode !== "managed") {
1871
- const now = /* @__PURE__ */ new Date();
1872
- const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
1873
- status: "running",
1874
- agentId: input.agentId ?? input.author,
1875
- leaseId: input.leaseId ?? randomUUID2(),
1876
- claimedAt: input.claimedAt ?? now.toISOString(),
1877
- heartbeatAt: input.heartbeatAt ?? now.toISOString(),
1878
- leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
1879
- attempt: input.attempt ?? 1,
1880
- maxAttempts: input.maxAttempts ?? 3
1881
- });
1882
- if (!assigned) return fail("Task assignment could not be started.");
1883
- const started = await updateTask2(projectRoot, board.id, task.id, {
1884
- status: "in_progress"
1885
- });
1886
- const current = started ?? assigned;
1887
- const claimed = task;
1888
- const currentTask = current.tasks.find((candidate) => candidate.id === claimed.id) ?? claimed;
1889
- ctx.setCurrentKanbanTask?.(currentTask.id, current.id);
1890
- return okTask(
1891
- current,
1892
- currentTask,
1893
- "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."
1894
- );
1895
- }
1896
- let stage = task.lifecycle?.currentStage;
1897
- if (stage === "backlog") {
1898
- const moved = await transitionTask(projectRoot, board.id, task.id, {
1899
- to: "todo",
1900
- actor: input.author,
1901
- comment: input.transitionComment
1902
- });
1903
- if (!moved) return fail("Task could not enter Todo.");
1904
- board = moved.board;
1905
- task = moved.task;
1906
- stage = task.lifecycle?.currentStage;
1907
- }
1908
- if (stage === "todo" || stage === "review") {
1909
- const now = /* @__PURE__ */ new Date();
1910
- const leaseId = input.leaseId ?? randomUUID2();
1911
- const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
1912
- status: "running",
1913
- agentId: input.agentId ?? input.author,
1914
- leaseId,
1915
- claimedAt: input.claimedAt ?? now.toISOString(),
1916
- heartbeatAt: input.heartbeatAt ?? now.toISOString(),
1917
- leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
1918
- attempt: input.attempt ?? 1,
1919
- maxAttempts: input.maxAttempts ?? 3
1920
- });
1921
- if (!assigned) return fail("Task assignment could not be started.");
1922
- const moved = await transitionTask(projectRoot, board.id, task.id, {
1923
- to: "running",
1924
- actor: input.author,
1925
- comment: input.transitionComment
1926
- });
1927
- if (!moved) return fail("Task could not enter Running.");
1928
- board = moved.board;
1929
- task = moved.task;
1930
- stage = task.lifecycle?.currentStage;
1931
- }
1932
- if (stage !== "running" || task.assignment?.status !== "running") {
1933
- return fail(
1934
- `start_task only accepts Backlog, Todo, Review repair, or live Running cards (current: ${stage ?? "unknown"}).`
1935
- );
1936
- }
1937
- ctx.setCurrentKanbanTask(task.id, board.id);
1938
- return okTask(
1939
- board,
1940
- task,
1941
- "Task is active; runtime Kanban governance is now bound to this run."
1942
- );
1943
- }
1944
- case "update_task": {
1945
- if (!input.boardId || !input.taskId)
1946
- return fail("update_task requires boardId and taskId.");
1947
- const board = await updateTask2(
1948
- projectRoot,
1949
- input.boardId,
1950
- input.taskId,
1951
- taskPatch(input)
1952
- );
1953
- return board ? okBoard(board, "Task updated.") : fail("Task not found.");
1954
- }
1955
- case "transition_task": {
1956
- if (!input.boardId || !input.taskId || !input.lifecycleStage || !input.author || !input.transitionComment) {
1957
- return fail(
1958
- "transition_task requires boardId, taskId, lifecycleStage, author, and transitionComment."
1959
- );
1960
- }
1961
- if (input.lifecycleStage === "done") {
1962
- const boardBefore = await getBoard3(projectRoot, input.boardId);
1963
- const taskBefore = boardBefore ? await getTask(projectRoot, input.boardId, input.taskId) : null;
1964
- if (boardBefore && taskBefore && !taskBefore.verificationReport && (taskBefore.atomic || Boolean(taskBefore.successCriteria?.length))) {
1965
- const preGate = await verifyTaskCompletion2(
1966
- projectRoot,
1967
- input.boardId,
1968
- taskBefore.id,
1969
- {
1970
- persist: false
1971
- }
1972
- );
1973
- await updateTask2(projectRoot, input.boardId, taskBefore.id, {
1974
- verificationReport: preGate.report,
1975
- successCriteria: preGate.task.successCriteria
1976
- });
1977
- }
1978
- }
1979
- const result2 = await transitionTask(projectRoot, input.boardId, input.taskId, {
1980
- to: input.lifecycleStage,
1981
- actor: input.author,
1982
- comment: input.transitionComment,
1983
- ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
1984
- ...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
1985
- ...input.attachmentUrl !== void 0 ? {
1986
- attachment: {
1987
- url: input.attachmentUrl,
1988
- type: input.attachmentType ?? "url",
1989
- ...input.attachmentTitle !== void 0 ? { title: input.attachmentTitle } : {}
1990
- }
1991
- } : {},
1992
- patch: taskPatch(input)
1993
- });
1994
- if (result2 && input.lifecycleStage === "done" && result2.task.verificationReport) {
1995
- recordKanbanVerificationEvidence(ctx, result2.task.verificationReport);
1996
- }
1997
- return result2 ? okTask(result2.board, result2.task, `Task advanced to ${result2.transition.to}.`) : fail("Board or task not found.");
1998
- }
1999
- case "repair_managed_projection": {
2000
- if (!input.boardId || !input.taskId || !input.author || !input.transitionComment) {
2001
- return fail(
2002
- "repair_managed_projection requires boardId, taskId, author, and transitionComment."
2003
- );
2004
- }
2005
- const result2 = await repairManagedTaskProjection(
2006
- projectRoot,
2007
- input.boardId,
2008
- input.taskId,
2009
- {
2010
- actor: input.author,
2011
- comment: input.transitionComment
2012
- }
2013
- );
2014
- return result2 ? okTask(
2015
- result2.board,
2016
- result2.task,
2017
- "Managed card projection repaired from lifecycle history."
2018
- ) : fail("Board or task not found.");
2019
- }
2020
- case "move_task": {
2021
- if (!input.boardId || !input.taskId || !input.targetColumnId) {
2022
- return fail("move_task requires boardId, taskId, and targetColumnId.");
2023
- }
2024
- const board = await moveTask(
2025
- projectRoot,
2026
- input.boardId,
2027
- input.taskId,
2028
- input.targetColumnId,
2029
- input.order
2030
- );
2031
- return board ? okBoard(board, "Task moved.") : fail("Move failed.");
2032
- }
2033
- case "delete_task": {
2034
- if (!input.boardId || !input.taskId)
2035
- return fail("delete_task requires boardId and taskId.");
2036
- const board = await removeTask(projectRoot, input.boardId, input.taskId);
2037
- if (board && ctx.currentKanbanTaskId === input.taskId) {
2038
- ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
2039
- }
2040
- return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
2041
- }
2042
- case "set_chain": {
2043
- if (!input.boardId || !input.taskIds?.length) {
2044
- return fail("set_chain requires boardId and taskIds.");
2045
- }
2046
- const result2 = await setTaskChain(projectRoot, input.boardId, {
2047
- taskIds: input.taskIds,
2048
- ...input.chainId !== void 0 ? { chainId: input.chainId } : {},
2049
- ...input.enforceDependencies !== void 0 ? { enforceDependencies: input.enforceDependencies } : {}
2050
- });
2051
- return result2 ? {
2052
- ok: true,
2053
- message: `Chain set: ${result2.chainId}`,
2054
- board: result2.board,
2055
- chain: result2.tasks
2056
- } : fail("Board or task not found.");
2057
- }
2058
- case "get_chain": {
2059
- if (!input.boardId || !(input.taskId || input.chainId)) {
2060
- return fail("get_chain requires boardId and taskId or chainId.");
2061
- }
2062
- const result2 = await getTaskChain(
2063
- projectRoot,
2064
- input.boardId,
2065
- input.taskId ?? input.chainId ?? ""
2066
- );
2067
- return result2 ? {
2068
- ok: true,
2069
- message: `Chain loaded: ${result2.chainId}`,
2070
- board: result2.board,
2071
- chain: result2.tasks
2072
- } : fail("Chain not found.");
2073
- }
2074
- case "claim_task": {
2075
- const result2 = await claimReadyTask(projectRoot, {
2076
- ...input.boardId !== void 0 ? { boardId: input.boardId } : {},
2077
- ...input.taskId !== void 0 ? { taskId: input.taskId } : {},
2078
- ...assignmentInput(input),
2079
- status: input.assignmentStatus ?? "queued"
2080
- });
2081
- return result2 ? okTask(result2.board, result2.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
2082
- }
2083
- case "release_task": {
2084
- if (!input.boardId || !input.taskId) {
2085
- return fail("release_task requires boardId and taskId.");
2086
- }
2087
- const board = await releaseTaskClaim(projectRoot, input.boardId, input.taskId, {
2088
- ...input.releaseStatus !== void 0 ? { status: input.releaseStatus } : {},
2089
- ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
2090
- ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {}
2091
- });
2092
- return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
2093
- }
2094
- case "assign_task": {
2095
- if (!input.boardId || !input.taskId)
2096
- return fail("assign_task requires boardId and taskId.");
2097
- const board = await assignTask(
2098
- projectRoot,
2099
- input.boardId,
2100
- input.taskId,
2101
- assignmentInput(input)
2102
- );
2103
- return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
2104
- }
2105
- case "mark_assignment": {
2106
- if (!input.boardId || !input.taskId)
2107
- return fail("mark_assignment requires boardId and taskId.");
2108
- const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
2109
- const board = await updateTaskAssignment(
2110
- projectRoot,
2111
- input.boardId,
2112
- input.taskId,
2113
- {
2114
- ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
2115
- ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
2116
- ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
2117
- ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
2118
- ...input.error !== void 0 ? { error: input.error } : {},
2119
- ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
2120
- ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
2121
- ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
2122
- ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
2123
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
2124
- ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
2125
- ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
2126
- },
2127
- // Ownership fence: when expectedLeaseId is supplied, the write is
2128
- // applied only if the current assignment still holds this lease.
2129
- // This prevents a recovered+reassigned stale worker's terminal
2130
- // mark_assignment from overwriting the successor's state. The check
2131
- // is atomic inside updateTaskAssignment's mutateBoard lock.
2132
- input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
2133
- );
2134
- if (!board) return fail("Task not found.");
2135
- if (assignmentStatus === "completed" && board.lifecycle?.mode !== "managed") {
2136
- const envGate = readEnvGateEnforcement();
2137
- const finalized = await finalizeTaskCompletion(projectRoot, board.id, input.taskId, {
2138
- ...board.completionGate === void 0 && envGate !== void 0 ? { enforcement: envGate } : {},
2139
- ...ctx.agentId !== void 0 ? { eventContext: { actor: ctx.agentId } } : {}
2140
- });
2141
- if (finalized) {
2142
- if (finalized.gate.report) {
2143
- recordKanbanVerificationEvidence(ctx, finalized.gate.report);
2144
- }
2145
- const gateSummary = {
2146
- enforcement: finalized.gate.enforcement,
2147
- allowed: finalized.gate.allowed,
2148
- verdict: finalized.gate.verdict,
2149
- issues: finalized.gate.issues.map((issue) => issue.message)
2150
- };
2151
- 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(" | ")}`;
2152
- return {
2153
- ...okTask(finalized.board, finalized.task, `Assignment updated. ${gateMessage}`),
2154
- gate: gateSummary
2155
- };
2156
- }
2157
- } else if (board.lifecycle?.mode === "managed") {
2158
- const managedTask = board.tasks.find((candidate) => candidate.id === input.taskId);
2159
- const stage = managedTask?.lifecycle?.currentStage;
2160
- const actor = ctx.agentId ?? "kanban-agent";
2161
- let transitionResult = null;
2162
- const lifecycleWarnings = [];
2163
- if (assignmentStatus === "running" && stage === "todo") {
2164
- try {
2165
- transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
2166
- to: "running",
2167
- actor,
2168
- comment: "Work started."
2169
- });
2170
- } catch (err) {
2171
- lifecycleWarnings.push(
2172
- `Lifecycle transition to Running deferred: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
2173
- );
2174
- }
2175
- }
2176
- if (assignmentStatus === "completed" && stage === "running") {
2177
- const comment = typeof input.lastResult === "string" && input.lastResult.trim().length > 0 ? input.lastResult.trim().slice(0, 1e3) : "Work completed.";
2178
- try {
2179
- transitionResult = await transitionTask(projectRoot, board.id, input.taskId, {
2180
- to: "review",
2181
- actor,
2182
- comment,
2183
- attachment: {
2184
- url: `kanban://task/${input.taskId}/result`,
2185
- title: "Worker completion result",
2186
- type: "file"
2187
- },
2188
- patch: {
2189
- // Only patch non-description fields so the
2190
- // original card description is preserved.
2191
- ...input.agentId !== void 0 ? { assignedAgent: input.agentId } : {}
2192
- }
2193
- });
2194
- } catch (err) {
2195
- lifecycleWarnings.push(
2196
- `Lifecycle transition to Review failed: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
2197
- );
2198
- }
2199
- if (transitionResult) {
2200
- const hasCriteria = (transitionResult.task.successCriteria?.length ?? 0) > 0 || transitionResult.task.atomic === true;
2201
- if (hasCriteria) {
2202
- try {
2203
- const verResult = await verifyTaskCompletion2(
2204
- projectRoot,
2205
- board.id,
2206
- input.taskId
2207
- );
2208
- if (verResult.report) {
2209
- recordKanbanVerificationEvidence(ctx, verResult.report);
2210
- }
2211
- await updateTask2(projectRoot, board.id, input.taskId, {
2212
- verificationReport: verResult.report,
2213
- successCriteria: verResult.task.successCriteria
2214
- });
2215
- const verdict = verResult.report.verdict;
2216
- if (verdict === "passed" && !resolveAutoAccept(board)) {
2217
- lifecycleWarnings.push(
2218
- "Verification passed, but this board does not auto-accept. The card is in Review awaiting an explicit transition_task to done."
2219
- );
2220
- } else if (verdict === "passed") {
2221
- try {
2222
- const doneResult = await transitionTask(
2223
- projectRoot,
2224
- board.id,
2225
- input.taskId,
2226
- {
2227
- to: "done",
2228
- actor,
2229
- action: "Automated acceptance after verification",
2230
- comment: "Auto-accepted: verification passed.",
2231
- attachment: {
2232
- url: `kanban://task/${input.taskId}/verification`,
2233
- title: "Auto-verification result",
2234
- type: "file"
2235
- }
2236
- }
2237
- );
2238
- transitionResult = doneResult;
2239
- } catch (acceptErr) {
2240
- lifecycleWarnings.push(
2241
- `Auto-accept to Done deferred: ${acceptErr instanceof Error ? acceptErr.message : String(acceptErr)}`
2242
- );
2243
- }
2244
- } else {
2245
- lifecycleWarnings.push(
2246
- `Verification verdict: ${verdict} \u2014 card left in Review for manual acceptance.`
2247
- );
2248
- }
2249
- } catch (verifyErr) {
2250
- lifecycleWarnings.push(
2251
- `Auto-verification error: ${verifyErr instanceof Error ? verifyErr.message : String(verifyErr)}`
2252
- );
2253
- }
2254
- } else {
2255
- lifecycleWarnings.push(
2256
- "No automatic success criteria \u2014 card left in Review for manual verification."
2257
- );
2258
- }
2259
- }
2260
- }
2261
- const responseBoard = transitionResult?.board ?? board;
2262
- const responseTask = transitionResult?.task ?? managedTask;
2263
- const msgParts = ["Assignment updated."];
2264
- if (transitionResult) {
2265
- msgParts.push(`Card advanced to ${transitionResult.transition.to}.`);
2266
- }
2267
- for (const w of lifecycleWarnings) msgParts.push(`Warning: ${w}`);
2268
- return okTask(responseBoard, responseTask, msgParts.join(" "));
2269
- }
2270
- return okBoard(board, "Assignment updated.");
2271
- }
2272
- case "heartbeat_assignment": {
2273
- if (!input.boardId || !input.taskId) {
2274
- return fail("heartbeat_assignment requires boardId and taskId.");
2275
- }
2276
- const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
2277
- ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
2278
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
2279
- // Ownership fence: when expectedLeaseId is supplied, the renewal
2280
- // is applied only if the current assignment still holds this lease.
2281
- // This prevents a recovered+reassigned stale worker's heartbeat
2282
- // from renewing the successor's lease. The check is atomic inside
2283
- // heartbeatTaskAssignment's mutateBoard lock.
2284
- ...input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
2285
- });
2286
- return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
2287
- }
2288
- case "recover_stale": {
2289
- if (!input.boardId) return fail("recover_stale requires boardId.");
2290
- const policyFields = [
2291
- input.recoveryPolicyFailOnCostCeiling !== void 0,
2292
- input.recoveryPolicyReleaseOnFailureKinds !== void 0,
2293
- input.recoveryPolicyReleaseOnHeartbeatDue !== void 0,
2294
- input.recoveryPolicyRetryPolicyOverride !== void 0
2295
- ].some(Boolean);
2296
- const result2 = await recoverStaleTaskAssignments(projectRoot, input.boardId, {
2297
- ...input.recoveryMode !== void 0 ? { mode: input.recoveryMode } : {},
2298
- ...input.recoveryNow !== void 0 ? { now: input.recoveryNow } : {},
2299
- ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
2300
- ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {},
2301
- ...policyFields ? {
2302
- policy: {
2303
- ...input.recoveryPolicyFailOnCostCeiling !== void 0 ? { failWhenCostCeilingSet: input.recoveryPolicyFailOnCostCeiling } : {},
2304
- ...input.recoveryPolicyReleaseOnFailureKinds !== void 0 ? {
2305
- releaseOnFailureKinds: input.recoveryPolicyReleaseOnFailureKinds
2306
- } : {},
2307
- ...input.recoveryPolicyReleaseOnHeartbeatDue !== void 0 ? {
2308
- releaseOnHeartbeatDue: input.recoveryPolicyReleaseOnHeartbeatDue
2309
- } : {},
2310
- ...input.recoveryPolicyRetryPolicyOverride !== void 0 ? {
2311
- retryPolicyOverride: input.recoveryPolicyRetryPolicyOverride
2312
- } : {}
2313
- }
2314
- } : {}
2315
- });
2316
- return result2 ? {
2317
- ok: true,
2318
- message: `Recovered ${result2.tasks.length} stale assignment(s).`,
2319
- board: result2.board,
2320
- recoveredTasks: result2.tasks
2321
- } : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
2322
- }
2323
- case "events": {
2324
- if (!input.boardId) return fail("events requires boardId.");
2325
- const eventList = await listKanbanEvents(projectRoot, input.boardId);
2326
- return {
2327
- ok: true,
2328
- message: `${eventList.length} event(s).`,
2329
- events: eventList
2330
- };
2331
- }
2332
- case "queue_health": {
2333
- const health = await getKanbanQueueHealth(projectRoot, {
2334
- ...input.boardId !== void 0 ? { boardId: input.boardId } : {}
2335
- });
2336
- return {
2337
- ok: true,
2338
- message: `Counts: startable=${health.counts.startable}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
2339
- queueHealth: health
2340
- };
2341
- }
2342
- // Not every action is handled above. These are dispatched from here,
2343
- // and the split has already cost real time: an agent that read this
2344
- // file concluded `add_check` / `update_check` did not exist, wrote
2345
- // that on a card, and spent a session trying to satisfy a gate it
2346
- // already had the tool to clear. Keep this index in step with the
2347
- // handlers.
2348
- //
2349
- // kanban-detail-actions.ts workbench · add_dependency ·
2350
- // add_goal_metric · update_goal_metric · add_check ·
2351
- // update_check · add_note · add_link · split_atomic
2352
- // kanban-decomposition-actions.ts verify_completion ·
2353
- // assess_atomicity · propose_decomposition
2354
- // kanban-contract-actions.ts get_contract_graph ·
2355
- // configure_contract_graph · upsert_contract_node ·
2356
- // remove_contract_node · add_contract_edge · remove_contract_edge
2357
- default:
2358
- {
2359
- const contractResult = await handleKanbanContractAction(
2360
- projectRoot,
2361
- input,
2362
- input.author ?? input.agentId
2363
- );
2364
- if (contractResult !== void 0) return contractResult;
2365
- }
2366
- {
2367
- const detailResult = await handleKanbanDetailAction(projectRoot, input);
2368
- if (detailResult !== void 0) return detailResult;
2369
- }
2370
- return fail(`Unknown kanban action: ${input.action}`);
2371
- }
2350
+ const boardResult = await handleKanbanBoardAction(projectRoot, input, ctx);
2351
+ if (boardResult !== void 0) return boardResult;
2352
+ const lifecycleResult = await handleKanbanLifecycleAction(projectRoot, input, ctx);
2353
+ if (lifecycleResult !== void 0) return lifecycleResult;
2354
+ const contractResult = await handleKanbanContractAction(
2355
+ projectRoot,
2356
+ input,
2357
+ input.author ?? input.agentId
2358
+ );
2359
+ if (contractResult !== void 0) return contractResult;
2360
+ const detailResult = await handleKanbanDetailAction(projectRoot, input);
2361
+ if (detailResult !== void 0) return detailResult;
2362
+ return fail(`Unknown kanban action: ${input.action}`);
2372
2363
  })();
2373
2364
  return withPresence(result);
2374
2365
  } catch (err) {
2375
- return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
2366
+ return fail(stripLifecycleIssues2(err instanceof Error ? err.message : String(err)));
2376
2367
  }
2377
2368
  },
2378
2369
  serialize(output, input) {
2379
2370
  return serializeKanbanOutput(output, input);
2380
2371
  }
2381
2372
  };
2382
- var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
2383
- var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
2384
- "get_board",
2385
- "export_markdown",
2386
- "export_task_graph"
2387
- ]);
2388
- function serializeKanbanOutput(output, input) {
2389
- const action = input && typeof input === "object" ? input.action : void 0;
2390
- const board = output.board;
2391
- if (board) {
2392
- const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
2393
- let boardBytes = 0;
2394
- if (!keepFull) {
2395
- try {
2396
- boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
2397
- } catch {
2398
- boardBytes = 0;
2399
- }
2400
- }
2401
- if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
2402
- const columns = {};
2403
- for (const column of board.columns) {
2404
- columns[column.title || column.id] = board.tasks.filter(
2405
- (task) => task.columnId === column.id
2406
- ).length;
2407
- }
2408
- const compact = {
2409
- ...output,
2410
- board: {
2411
- id: board.id,
2412
- title: board.title,
2413
- columns,
2414
- totalTasks: board.tasks.length,
2415
- note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
2416
- }
2417
- };
2418
- return JSON.stringify(compact, null, 2);
2419
- }
2420
- }
2421
- return JSON.stringify(output, null, 2);
2422
- }
2423
2373
 
2424
2374
  // src/todo.ts
2425
2375
  function normalizedTitle(value) {
@@ -2477,7 +2427,7 @@ async function createMissingManagedCards(items, board, ctx, warnings) {
2477
2427
  for (const item of items) {
2478
2428
  if (item.kanbanBoardId === board.id && item.kanbanTaskId) continue;
2479
2429
  try {
2480
- const result = await addTask2(ctx.projectRoot, board.id, {
2430
+ const result = await addTask3(ctx.projectRoot, board.id, {
2481
2431
  title: item.content,
2482
2432
  description: item.activeForm?.trim() || `Added from the session todo list: ${item.content}`
2483
2433
  });
@@ -2571,7 +2521,7 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
2571
2521
  });
2572
2522
  }
2573
2523
  const attemptedParents = /* @__PURE__ */ new Set();
2574
- let afterCompletions = await getBoard4(ctx.projectRoot, board.id);
2524
+ let afterCompletions = await getBoard5(ctx.projectRoot, board.id);
2575
2525
  while (afterCompletions) {
2576
2526
  const parent = afterCompletions.tasks.find(
2577
2527
  (task) => task.atomic === true && task.status !== "completed" && Boolean(task.childTaskIds?.length) && !attemptedParents.has(task.id) && task.childTaskIds?.every(
@@ -2598,7 +2548,7 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
2598
2548
  lastResult: "All child tasks completed; composite result ready for verification."
2599
2549
  });
2600
2550
  }
2601
- afterCompletions = await getBoard4(ctx.projectRoot, board.id);
2551
+ afterCompletions = await getBoard5(ctx.projectRoot, board.id);
2602
2552
  }
2603
2553
  const completionPending = items.some(
2604
2554
  (item) => item.status === "completed" && item.kanbanBoardId === board.id && Boolean(item.kanbanTaskId) && afterCompletions?.tasks.find((task) => task.id === item.kanbanTaskId)?.status !== "completed"
@@ -2711,7 +2661,7 @@ var todoTool = {
2711
2661
  }
2712
2662
  }
2713
2663
  const boardId = activeBoardId(items, ctx);
2714
- let board = boardId ? await getBoard4(ctx.projectRoot, boardId) : null;
2664
+ let board = boardId ? await getBoard5(ctx.projectRoot, boardId) : null;
2715
2665
  const managed = board?.lifecycle?.mode === "managed";
2716
2666
  let boundItems = managed && board ? bindTodosToBoard(items, ctx.todos ?? [], board) : items;
2717
2667
  const creationWarnings = [];
@@ -2723,7 +2673,7 @@ var todoTool = {
2723
2673
  const taskId = created.get(item.id);
2724
2674
  return taskId ? { ...item, kanbanBoardId: managedBoardId, kanbanTaskId: taskId } : item;
2725
2675
  });
2726
- board = await getBoard4(ctx.projectRoot, managedBoardId) ?? board;
2676
+ board = await getBoard5(ctx.projectRoot, managedBoardId) ?? board;
2727
2677
  boundItems = bindTodosToBoard(boundItems, ctx.todos ?? [], board);
2728
2678
  }
2729
2679
  boundItems = demoteBlockedInProgress(boundItems, creationWarnings);
@@ -2745,10 +2695,10 @@ var todoTool = {
2745
2695
  if (mirrorFailure) kanbanSync.warnings.push(mirrorFailure);
2746
2696
  let projectedBoard = board;
2747
2697
  if (managed && board) {
2748
- const refreshed = await getBoard4(ctx.projectRoot, board.id);
2698
+ const refreshed = await getBoard5(ctx.projectRoot, board.id);
2749
2699
  if (refreshed) {
2750
2700
  projectedBoard = refreshed;
2751
- applyManagedKanbanBoardToTodos(ctx, refreshed);
2701
+ applyManagedKanbanBoardToTodos2(ctx, refreshed);
2752
2702
  }
2753
2703
  }
2754
2704
  if (!managed) {