@wrongstack/tools 0.313.0 → 0.313.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 (134) hide show
  1. package/dist/audit.js +10 -4055
  2. package/dist/bash.js +9 -2308
  3. package/dist/batch-tool-use.js +2 -187
  4. package/dist/browser/index.js +25 -1339
  5. package/dist/builtin.js +69 -33949
  6. package/dist/chunk-2CTB3UK5.js +197 -0
  7. package/dist/chunk-2ME7ZWDD.js +1272 -0
  8. package/dist/chunk-2TFQDEPU.js +113 -0
  9. package/dist/chunk-2ZCJNKFE.js +315 -0
  10. package/dist/chunk-3PVYQFRQ.js +472 -0
  11. package/dist/chunk-3QMQBO5Z.js +89 -0
  12. package/dist/chunk-42AKKX7O.js +385 -0
  13. package/dist/chunk-42SKEETB.js +113 -0
  14. package/dist/chunk-4BHUSMNL.js +104 -0
  15. package/dist/chunk-4I4LWFOO.js +431 -0
  16. package/dist/chunk-553IQUMO.js +478 -0
  17. package/dist/chunk-55KF77OK.js +1944 -0
  18. package/dist/chunk-6HLZK37X.js +359 -0
  19. package/dist/chunk-6RNIFLG4.js +305 -0
  20. package/dist/chunk-A6BRWIXN.js +317 -0
  21. package/dist/chunk-AHHDWNXC.js +200 -0
  22. package/dist/chunk-B2CIOKWP.js +899 -0
  23. package/dist/chunk-BTSFTGIL.js +7 -0
  24. package/dist/chunk-D33S4RQG.js +222 -0
  25. package/dist/chunk-DIJKHSUO.js +134 -0
  26. package/dist/chunk-DPL47QGS.js +3515 -0
  27. package/dist/chunk-EMRGN4KM.js +170 -0
  28. package/dist/chunk-FGOABE4Z.js +955 -0
  29. package/dist/chunk-FW65TP4G.js +520 -0
  30. package/dist/chunk-GOIZNNOA.js +221 -0
  31. package/dist/chunk-H5MJUAA6.js +295 -0
  32. package/dist/chunk-H5OHH6F4.js +223 -0
  33. package/dist/chunk-HC5RM4WY.js +199 -0
  34. package/dist/chunk-HEQFMLZ7.js +190 -0
  35. package/dist/chunk-HFXSNKCV.js +196 -0
  36. package/dist/chunk-HXSBQ2SE.js +1723 -0
  37. package/dist/chunk-ILESNGNM.js +163 -0
  38. package/dist/chunk-INF5CGAO.js +137 -0
  39. package/dist/chunk-IWDFZ6XR.js +337 -0
  40. package/dist/chunk-IXMNTNW5.js +238 -0
  41. package/dist/chunk-J4SRJKBW.js +428 -0
  42. package/dist/chunk-J5SKD6LN.js +235 -0
  43. package/dist/chunk-JRJJX3L5.js +424 -0
  44. package/dist/chunk-KXNDHMUB.js +366 -0
  45. package/dist/chunk-LIJQMYYX.js +59 -0
  46. package/dist/chunk-MAN26PZ5.js +260 -0
  47. package/dist/chunk-MKRQMLZA.js +474 -0
  48. package/dist/chunk-NBDZMMG2.js +169 -0
  49. package/dist/chunk-NHA6BEIH.js +101 -0
  50. package/dist/chunk-OA6YJIWL.js +4412 -0
  51. package/dist/chunk-OZC255IC.js +2830 -0
  52. package/dist/chunk-PUR6WU2B.js +154 -0
  53. package/dist/chunk-Q5JPPCZC.js +12 -0
  54. package/dist/chunk-QBV725YG.js +128 -0
  55. package/dist/chunk-QHVW4AAM.js +892 -0
  56. package/dist/chunk-QNJ7AQ4B.js +1000 -0
  57. package/dist/chunk-RGISBZBD.js +231 -0
  58. package/dist/chunk-RJZQWEMS.js +84 -0
  59. package/dist/chunk-RP2ZA47V.js +15 -0
  60. package/dist/chunk-RXISBFR5.js +315 -0
  61. package/dist/chunk-S5CCLZL3.js +99 -0
  62. package/dist/chunk-TVTCSBJF.js +122 -0
  63. package/dist/chunk-U4D5S3VQ.js +21 -0
  64. package/dist/chunk-U4VAATKP.js +193 -0
  65. package/dist/chunk-UKU2ZX4C.js +203 -0
  66. package/dist/chunk-UNSKG6DC.js +159 -0
  67. package/dist/chunk-URFKEIHM.js +1091 -0
  68. package/dist/chunk-UTV7YAW7.js +178 -0
  69. package/dist/chunk-V43EQ6VR.js +68 -0
  70. package/dist/chunk-VB3MJGCV.js +610 -0
  71. package/dist/chunk-WHIIIREL.js +223 -0
  72. package/dist/chunk-XMIFYKUT.js +1371 -0
  73. package/dist/chunk-YF2RSV34.js +54 -0
  74. package/dist/chunk-Z6DAKQ3U.js +1 -0
  75. package/dist/chunk-ZIGAZFWE.js +522 -0
  76. package/dist/circuit-breaker.js +3 -191
  77. package/dist/codebase-index/index.js +84 -13688
  78. package/dist/codebase-index/parser-worker-script.js +8 -3995
  79. package/dist/codebase-index/project-server.js +60 -8548
  80. package/dist/codebase-index/worker.js +20 -8416
  81. package/dist/design.js +2 -299
  82. package/dist/diff.js +4 -256
  83. package/dist/document.js +4 -222
  84. package/dist/e2e.js +5 -476
  85. package/dist/edit.js +14 -10200
  86. package/dist/exec.js +16 -3131
  87. package/dist/fetch.js +8 -398
  88. package/dist/format.js +10 -3750
  89. package/dist/generic-parser-VUXDIAOZ.js +13 -0
  90. package/dist/git.js +4 -380
  91. package/dist/glob.js +6 -280
  92. package/dist/go-parser-6CA77WZ4.js +469 -0
  93. package/dist/grep.js +7 -604
  94. package/dist/index.js +318 -35387
  95. package/dist/install.js +10 -4091
  96. package/dist/json-parser-7YMDJSBJ.js +213 -0
  97. package/dist/json.js +4 -665
  98. package/dist/kanban.js +3 -2096
  99. package/dist/languages/index.js +23 -4664
  100. package/dist/legacy-bridge-7UKZT46G.js +18 -0
  101. package/dist/lint.js +10 -3706
  102. package/dist/logs.js +4 -287
  103. package/dist/memory.js +6 -215
  104. package/dist/mode.js +3 -99
  105. package/dist/outdated.js +4 -5170
  106. package/dist/pack.js +65 -33897
  107. package/dist/parser-dispatch-VPOFDO2U.js +14 -0
  108. package/dist/patch.js +13 -9935
  109. package/dist/plan.js +5 -3286
  110. package/dist/process-registry.js +8 -651
  111. package/dist/ps-slash.js +12 -1439
  112. package/dist/py-parser-GPM5VYRG.js +16 -0
  113. package/dist/read.js +13 -9816
  114. package/dist/replace.js +15 -10014
  115. package/dist/rs-parser-WVQ4KYCX.js +81 -0
  116. package/dist/scaffold.js +4 -253
  117. package/dist/search.js +5 -585
  118. package/dist/session-kanban.js +30 -972
  119. package/dist/skill.js +2 -215
  120. package/dist/task.js +5 -3410
  121. package/dist/test.js +10 -3773
  122. package/dist/todo.js +4 -2891
  123. package/dist/tool-help.js +3 -123
  124. package/dist/tool-search.js +3 -79
  125. package/dist/tool-tier.js +68 -33984
  126. package/dist/tool-use.js +2 -93
  127. package/dist/tree-sitter-parser-YCILHGZD.js +17 -0
  128. package/dist/tree.js +4 -288
  129. package/dist/ts-parser-YH4ZMWMH.js +232 -0
  130. package/dist/typecheck.js +10 -3753
  131. package/dist/win32.js +6 -60
  132. package/dist/write.js +14 -9882
  133. package/dist/yaml-parser-GFAOKV53.js +157 -0
  134. package/package.json +5 -5
@@ -0,0 +1,1000 @@
1
+ // src/session-kanban.ts
2
+ import { watch } from "node:fs";
3
+ import { basename, dirname } from "node:path";
4
+ import { loadPlan, loadTasks } from "@wrongstack/core/storage";
5
+ import { deserializeTaskGraph } from "@wrongstack/core/tasking";
6
+ import {
7
+ bridgeKanbanSupervisor,
8
+ compactSessionMirrorBoard,
9
+ createBoard,
10
+ DEFAULT_COLUMNS,
11
+ getBoard,
12
+ getKanbanOrchestrationSnapshot,
13
+ listBoards,
14
+ pruneSessionBoards,
15
+ removeBoard,
16
+ syncBoardFromTaskGraph,
17
+ touchKanbanPresence,
18
+ updateBoard
19
+ } from "@wrongstack/kanban";
20
+
21
+ // src/session-kanban-graph.ts
22
+ var PLAN_STATUS_TO_TASK = {
23
+ open: "pending",
24
+ in_progress: "in_progress",
25
+ done: "completed"
26
+ };
27
+ function todoListToSerializedGraph(todos, sessionId) {
28
+ const graphId = `todo:${sessionId}`;
29
+ const nodes = todos.map((todo, index) => ({
30
+ id: todo.id,
31
+ title: todo.content,
32
+ description: todo.activeForm ?? "",
33
+ type: "chore",
34
+ priority: "medium",
35
+ status: todo.status,
36
+ specRequirementId: `${graphId}:${todo.id}`,
37
+ createdAt: index,
38
+ updatedAt: index
39
+ }));
40
+ return {
41
+ id: graphId,
42
+ specId: graphId,
43
+ requiredRequirementIds: nodes.map((node) => node.specRequirementId),
44
+ title: "Session todos",
45
+ nodes,
46
+ edges: [],
47
+ rootNodes: nodes.map((node) => node.id),
48
+ createdAt: 0,
49
+ updatedAt: 0
50
+ };
51
+ }
52
+ function taskFileToSerializedGraph(tasks, sessionId) {
53
+ const graphId = `session:${sessionId}`;
54
+ const ids = new Set(tasks.map((task) => task.id));
55
+ const nodes = tasks.map((task, index) => ({
56
+ id: task.id,
57
+ title: task.title,
58
+ description: task.description ?? "",
59
+ type: task.type,
60
+ priority: task.priority,
61
+ status: task.status,
62
+ specRequirementId: `${graphId}:${task.id}`,
63
+ ...task.assignee ? { assignee: task.assignee } : {},
64
+ ...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
65
+ createdAt: index,
66
+ updatedAt: index
67
+ }));
68
+ const edges = tasks.flatMap(
69
+ (task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
70
+ id: `${dependency}->${task.id}`,
71
+ from: dependency,
72
+ to: task.id,
73
+ type: "depends_on"
74
+ }))
75
+ );
76
+ const hasIncoming = new Set(edges.map((edge) => edge.to));
77
+ const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
78
+ return {
79
+ // Keep the historical graph id so existing mirrored task cards are reused.
80
+ id: graphId,
81
+ specId: graphId,
82
+ requiredRequirementIds: nodes.map((node) => node.specRequirementId),
83
+ title: "Session tasks",
84
+ nodes,
85
+ edges,
86
+ rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
87
+ createdAt: 0,
88
+ updatedAt: 0
89
+ };
90
+ }
91
+ function planFileToSerializedGraph(items, sessionId) {
92
+ const graphId = `plan:${sessionId}`;
93
+ const nodes = items.map((item, index) => ({
94
+ id: item.id,
95
+ title: item.title,
96
+ description: item.details ?? "",
97
+ type: "chore",
98
+ priority: "medium",
99
+ status: PLAN_STATUS_TO_TASK[item.status],
100
+ specRequirementId: `${graphId}:${item.id}`,
101
+ createdAt: index,
102
+ updatedAt: index
103
+ }));
104
+ return {
105
+ id: graphId,
106
+ specId: graphId,
107
+ requiredRequirementIds: nodes.map((node) => node.specRequirementId),
108
+ title: "Session plan",
109
+ nodes,
110
+ edges: [],
111
+ rootNodes: nodes.map((node) => node.id),
112
+ createdAt: 0,
113
+ updatedAt: 0
114
+ };
115
+ }
116
+
117
+ // src/session-kanban-sync.ts
118
+ import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
119
+ import { mutatePlan, mutateTasks } from "@wrongstack/core/storage";
120
+ import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
121
+ import {
122
+ getDependencyReadinessIssues
123
+ } from "@wrongstack/kanban";
124
+ function sourceStatus(task) {
125
+ if (task.status === "completed") return "completed";
126
+ if (task.status === "in_progress") return "in_progress";
127
+ if (task.status === "review") return "review";
128
+ if (task.status === "blocked") return "blocked";
129
+ if (task.status === "failed") return "failed";
130
+ return "pending";
131
+ }
132
+ function todoStatus(task) {
133
+ const status = sourceStatus(task);
134
+ if (status === "completed") return "completed";
135
+ if (status === "review" && task.assignment?.status === "completed") return "completed";
136
+ if (status === "in_progress" || status === "review") return "in_progress";
137
+ return "pending";
138
+ }
139
+ function sessionTodoFromTask(task, board) {
140
+ const blockedBy = board ? blockingTitles(board, task) : [];
141
+ return {
142
+ id: task.origin?.taskId ?? task.id,
143
+ content: task.title,
144
+ status: todoStatus(task),
145
+ ...task.description ? { activeForm: task.description } : {},
146
+ ...blockedBy.length ? { blockedBy } : {}
147
+ };
148
+ }
149
+ function managedTodoFromTask(task, board) {
150
+ return {
151
+ ...sessionTodoFromTask(task, board),
152
+ kanbanBoardId: board.id,
153
+ kanbanTaskId: task.id
154
+ };
155
+ }
156
+ function blockingTitles(board, task) {
157
+ return getDependencyReadinessIssues(board, task).map((issue) => {
158
+ const dependency = board.tasks.find((candidate) => candidate.id === issue.dependencyId);
159
+ if (!dependency) return `${issue.dependencyId} (missing)`;
160
+ return dependency.title;
161
+ });
162
+ }
163
+ var PRIORITY_ORDER = {
164
+ critical: 0,
165
+ high: 1,
166
+ medium: 2,
167
+ low: 3
168
+ };
169
+ function orderTasksForTodos(board, tasks) {
170
+ const columnOrder = new Map(board.columns.map((column) => [column.id, column.order]));
171
+ const baseline = [...tasks].sort(
172
+ (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)
173
+ );
174
+ const included = new Set(baseline.map((task) => task.id));
175
+ const remaining = new Map(baseline.map((task) => [task.id, task]));
176
+ const emitted = [];
177
+ const done = /* @__PURE__ */ new Set();
178
+ while (remaining.size > 0) {
179
+ const ready = baseline.filter(
180
+ (task) => remaining.has(task.id) && (task.dependsOn ?? []).every(
181
+ (dependencyId) => !included.has(dependencyId) || done.has(dependencyId)
182
+ )
183
+ );
184
+ if (ready.length === 0) break;
185
+ for (const task of ready) {
186
+ remaining.delete(task.id);
187
+ done.add(task.id);
188
+ emitted.push(task);
189
+ }
190
+ }
191
+ for (const task of baseline) if (remaining.has(task.id)) emitted.push(task);
192
+ return emitted;
193
+ }
194
+ function sameTodos(left, right) {
195
+ return left.length === right.length && left.every((todo, index) => {
196
+ const candidate = right[index];
197
+ 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");
198
+ });
199
+ }
200
+ function broadcastTodoUpdate(context, todos) {
201
+ const sessionId = context.session?.id ?? "";
202
+ if (!context.agentId || !sessionId) return;
203
+ const statusCounts = { pending: 0, inProgress: 0, completed: 0 };
204
+ for (const todo of todos) {
205
+ if (todo.status === "completed") statusCounts.completed++;
206
+ else if (todo.status === "in_progress") statusCounts.inProgress++;
207
+ else statusCounts.pending++;
208
+ }
209
+ const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;
210
+ const mailbox = getSharedProjectMailbox(projectDir);
211
+ void mailbox.send({
212
+ from: context.agentId,
213
+ to: `@session:${sessionId}`,
214
+ type: "status",
215
+ subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
216
+ body: `Shared Kanban board synced this session's todo list: ${todos.length} item${todos.length === 1 ? "" : "s"} \u2014 ${statusCounts.completed} completed, ${statusCounts.inProgress} in progress, ${statusCounts.pending} pending.`,
217
+ priority: "normal",
218
+ senderSessionId: sessionId
219
+ }).catch(() => {
220
+ });
221
+ }
222
+ function notifyTodoUpdate(context, todos) {
223
+ const summary = formatTodosForModel(todos);
224
+ const text = `[KANBAN TODO UPDATE]
225
+ Another Kanban agent reassessed the shared board. The canonical todo list is now:
226
+ ${summary}
227
+ 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.`;
228
+ const state = context.state;
229
+ if (typeof state.appendBlockToLastUserMessage === "function") {
230
+ if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
231
+ }
232
+ if (typeof state.appendMessage === "function") {
233
+ state.appendMessage({ role: "user", content: [{ type: "text", text }] });
234
+ }
235
+ }
236
+ function todosNeedingSessionMirror(todos, activeBoardId) {
237
+ if (!activeBoardId) return todos;
238
+ return todos.filter((todo) => todo.kanbanBoardId !== activeBoardId || !todo.kanbanTaskId);
239
+ }
240
+ function applySessionKanbanBoardToTodos(context, board, options) {
241
+ const sessionId = context.session?.id ?? "";
242
+ if (!sessionId || options.sessionIdFromTags(board.tags) !== sessionId || !options.isOwnedSessionBoard(board.tags)) {
243
+ return [...context.todos];
244
+ }
245
+ if (options.hasInFlightTodoMirror(context.projectRoot, sessionId)) return [...context.todos];
246
+ const projectedTodos = orderTasksForTodos(
247
+ board,
248
+ board.tasks.filter(
249
+ (task) => task.status !== "archived" && (!task.origin || task.origin.system === "session-todo" || (task.origin.graphId ?? "").startsWith("todo:"))
250
+ )
251
+ ).map((task) => sessionTodoFromTask(task, board));
252
+ const allCompleted = projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === "completed");
253
+ const effectiveTodos = allCompleted ? [] : projectedTodos;
254
+ if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];
255
+ options.suppressedTodoMirrors.add(context);
256
+ try {
257
+ context.state.replaceTodos(projectedTodos);
258
+ } finally {
259
+ options.suppressedTodoMirrors.delete(context);
260
+ }
261
+ notifyTodoUpdate(context, context.todos);
262
+ broadcastTodoUpdate(context, context.todos);
263
+ return [...context.todos];
264
+ }
265
+ function applyManagedKanbanBoardToTodos(context, board, suppressedTodoMirrors2, options = {}) {
266
+ const metaKanban = context.meta["kanban"];
267
+ const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
268
+ const activeBoardId = context.currentKanbanBoardId ?? (typeof metaBoardId === "string" ? metaBoardId : void 0);
269
+ if (!activeBoardId || board.id !== activeBoardId || board.lifecycle?.mode !== "managed") {
270
+ return [...context.todos];
271
+ }
272
+ const ownerSessionId = options.sessionOwnerFromTags?.(board.tags);
273
+ if (ownerSessionId && ownerSessionId !== (context.session?.id ?? "")) {
274
+ return [...context.todos];
275
+ }
276
+ const projectedTodos = orderTasksForTodos(
277
+ board,
278
+ board.tasks.filter(
279
+ (task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
280
+ )
281
+ ).map((task) => managedTodoFromTask(task, board));
282
+ if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
283
+ suppressedTodoMirrors2.add(context);
284
+ try {
285
+ context.state.replaceTodos(projectedTodos);
286
+ } finally {
287
+ suppressedTodoMirrors2.delete(context);
288
+ }
289
+ notifyTodoUpdate(context, context.todos);
290
+ broadcastTodoUpdate(context, context.todos);
291
+ return [...context.todos];
292
+ }
293
+ async function applySessionKanbanTaskToSource(context, task, suppressedTodoMirrors2, options = {}) {
294
+ const originId = task.origin?.taskId;
295
+ const graphId = task.origin?.graphId ?? "";
296
+ const isTodoOrigin = task.origin?.system === "session-todo" || graphId.startsWith("todo:");
297
+ const targetTodo = context.todos?.find(
298
+ (todo) => originId !== void 0 && todo.id === originId || todo.kanbanTaskId === task.id || todo.id === task.id && !todo.promotedFromPlan && !todo.promotedFromTask
299
+ );
300
+ if (isTodoOrigin || targetTodo) {
301
+ const matchedId = targetTodo?.id ?? originId;
302
+ if (matchedId) {
303
+ const mappedStatus = todoStatus(task);
304
+ const next = options.remove ? context.todos.filter((todo) => todo.id !== matchedId) : context.todos.map(
305
+ (todo) => todo.id === matchedId ? {
306
+ ...todo,
307
+ content: task.title,
308
+ status: mappedStatus,
309
+ kanbanTaskId: task.id
310
+ } : todo
311
+ );
312
+ suppressedTodoMirrors2.add(context);
313
+ try {
314
+ context.state.replaceTodos(next);
315
+ } finally {
316
+ suppressedTodoMirrors2.delete(context);
317
+ }
318
+ return { source: "todo", todos: [...context.todos] };
319
+ }
320
+ }
321
+ if (!originId) return { source: null };
322
+ const id = context.session?.id ?? "";
323
+ if (task.origin?.system === "session-plan" || graphId.startsWith("plan:")) {
324
+ const planPath = context.meta["plan.path"];
325
+ if (typeof planPath !== "string" || !planPath) {
326
+ throw new Error(
327
+ "Cannot reflect this Kanban edit back to its plan source: the session has no plan.path configured. The board mutation already succeeded; the plan file is now out of sync."
328
+ );
329
+ }
330
+ const plan = await mutatePlan(planPath, id, (file) => ({
331
+ ...file,
332
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
333
+ items: options.remove ? file.items.filter((item) => item.id !== originId) : file.items.map(
334
+ (item) => item.id === originId ? {
335
+ ...item,
336
+ title: task.title,
337
+ details: task.description,
338
+ status: task.status === "completed" ? "done" : task.status === "in_progress" || task.status === "review" ? "in_progress" : "open",
339
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
340
+ } : item
341
+ )
342
+ }));
343
+ return { source: "plan", plan };
344
+ }
345
+ if (task.origin?.system === "session-task" || task.origin?.system === "session" || graphId.startsWith("session:")) {
346
+ const taskPath = context.meta["task.path"];
347
+ if (typeof taskPath !== "string" || !taskPath) {
348
+ throw new Error(
349
+ "Cannot reflect this Kanban edit back to its task source: the session has no task.path configured. The board mutation already succeeded; the task file is now out of sync."
350
+ );
351
+ }
352
+ const tasks = await mutateTasks(taskPath, id, (file) => ({
353
+ ...file,
354
+ tasks: options.remove ? file.tasks.filter((item) => item.id !== originId) : file.tasks.map(
355
+ (item) => item.id === originId ? {
356
+ ...item,
357
+ title: task.title,
358
+ description: task.description,
359
+ status: sourceStatus(task),
360
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
361
+ } : item
362
+ )
363
+ }));
364
+ return { source: "task", tasks };
365
+ }
366
+ return { source: null };
367
+ }
368
+
369
+ // src/session-kanban.ts
370
+ var SESSION_BOARD_TAG = "session-work";
371
+ var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
372
+ var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
373
+ ...column
374
+ }));
375
+ var boardQueue = /* @__PURE__ */ new Map();
376
+ var boardEnsures = /* @__PURE__ */ new Map();
377
+ var pendingMirrors = /* @__PURE__ */ new Map();
378
+ var activeMirrors = /* @__PURE__ */ new Set();
379
+ var mirrorFailures = /* @__PURE__ */ new Map();
380
+ var bindings = /* @__PURE__ */ new WeakMap();
381
+ var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
382
+ var activeSessionBoards = /* @__PURE__ */ new Map();
383
+ function boardKey(projectRoot, sessionId) {
384
+ return `${projectRoot}\0${sessionId}`;
385
+ }
386
+ function mirrorKey(projectRoot, sessionId, sourceSystem) {
387
+ return `${boardKey(projectRoot, sessionId)}\0${sourceSystem}`;
388
+ }
389
+ function completedReconciliationGraph(latest, candidates) {
390
+ const latestNodeIds = new Set(latest.nodes.map((node) => node.id));
391
+ const carriedNodeIds = /* @__PURE__ */ new Set();
392
+ const completedNodes = candidates.flatMap(
393
+ (candidate) => candidate.nodes.filter((node) => {
394
+ if (node.status !== "completed" || latestNodeIds.has(node.id) || carriedNodeIds.has(node.id)) {
395
+ return false;
396
+ }
397
+ carriedNodeIds.add(node.id);
398
+ return true;
399
+ })
400
+ );
401
+ if (completedNodes.length === 0) return void 0;
402
+ const carriedRequirements = completedNodes.flatMap(
403
+ (node) => node.specRequirementId ? [node.specRequirementId] : []
404
+ );
405
+ return {
406
+ ...latest,
407
+ nodes: [...latest.nodes, ...completedNodes],
408
+ rootNodes: [.../* @__PURE__ */ new Set([...latest.rootNodes, ...completedNodes.map((node) => node.id)])],
409
+ ...latest.requiredRequirementIds ? {
410
+ requiredRequirementIds: [
411
+ .../* @__PURE__ */ new Set([...latest.requiredRequirementIds, ...carriedRequirements])
412
+ ]
413
+ } : {}
414
+ };
415
+ }
416
+ function sessionTag(sessionId) {
417
+ return `session:${sessionId}`;
418
+ }
419
+ function sessionBoardTitle(sessionId) {
420
+ const leaf = sessionId.split(/[\\/]/).filter(Boolean).pop() ?? sessionId;
421
+ return `Session ${leaf.slice(0, 12)}`;
422
+ }
423
+ function sessionBoardTags(sessionId) {
424
+ return ["session", SESSION_BOARD_TAG, sessionTag(sessionId)];
425
+ }
426
+ function sessionIdFromTags(tags) {
427
+ const tag = tags?.find((candidate) => candidate.startsWith("session:"));
428
+ return tag?.slice("session:".length) || null;
429
+ }
430
+ function isOwnedSessionBoard(tags) {
431
+ return Boolean(tags?.includes(SESSION_BOARD_TAG) && sessionIdFromTags(tags));
432
+ }
433
+ function retainActiveSessionBoard(projectRoot, sessionId) {
434
+ const key = boardKey(projectRoot, sessionId);
435
+ activeSessionBoards.set(key, (activeSessionBoards.get(key) ?? 0) + 1);
436
+ }
437
+ function releaseActiveSessionBoard(projectRoot, sessionId) {
438
+ const key = boardKey(projectRoot, sessionId);
439
+ const remaining = (activeSessionBoards.get(key) ?? 0) - 1;
440
+ if (remaining > 0) activeSessionBoards.set(key, remaining);
441
+ else activeSessionBoards.delete(key);
442
+ }
443
+ function isSessionBoardActive(projectRoot, sessionId) {
444
+ return (activeSessionBoards.get(boardKey(projectRoot, sessionId)) ?? 0) > 0;
445
+ }
446
+ function sameColumns(columns) {
447
+ return columns.length === SESSION_KANBAN_COLUMNS.length && columns.every((column, index) => column.id === SESSION_KANBAN_COLUMNS[index]?.id);
448
+ }
449
+ async function ensureSessionKanbanBoard(projectRoot, sessionId) {
450
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
451
+ const key = boardKey(projectRoot, sessionId);
452
+ const inFlight = boardEnsures.get(key);
453
+ if (inFlight) return inFlight;
454
+ const promise = (async () => {
455
+ const summary = (await listBoards(projectRoot)).find(
456
+ (board2) => board2.tags?.includes(sessionTag(sessionId))
457
+ );
458
+ let board = summary ? await getBoard(projectRoot, summary.id) : null;
459
+ if (!board) {
460
+ return createBoard(projectRoot, {
461
+ title: sessionBoardTitle(sessionId),
462
+ description: "Live session work: todos, tasks, and plan items.",
463
+ tags: sessionBoardTags(sessionId),
464
+ columns: SESSION_KANBAN_COLUMNS,
465
+ generatedBy: `session-kanban:${sessionId}`,
466
+ kind: "session_mirror",
467
+ retention: { mode: "archive_after_ttl", ttlMs: 7 * 24 * 60 * 60 * 1e3 }
468
+ });
469
+ }
470
+ if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
471
+ board = await updateBoard(projectRoot, board.id, {
472
+ title: sessionBoardTitle(sessionId),
473
+ description: "Live session work: todos, tasks, and plan items.",
474
+ tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
475
+ columns: SESSION_KANBAN_COLUMNS
476
+ }) ?? board;
477
+ }
478
+ return board;
479
+ })();
480
+ boardEnsures.set(key, promise);
481
+ try {
482
+ return await promise;
483
+ } finally {
484
+ boardEnsures.delete(key);
485
+ }
486
+ }
487
+ function enqueueBoardWork(projectRoot, sessionId, work) {
488
+ const key = boardKey(projectRoot, sessionId);
489
+ const previous = boardQueue.get(key) ?? Promise.resolve();
490
+ const result = previous.catch(() => void 0).then(work);
491
+ const tail = result.then(
492
+ () => void 0,
493
+ () => void 0
494
+ );
495
+ boardQueue.set(key, tail);
496
+ void tail.then(() => {
497
+ if (boardQueue.get(key) === tail) boardQueue.delete(key);
498
+ });
499
+ return result;
500
+ }
501
+ async function removeEmptySessionBoard(projectRoot, boardId, sessionId) {
502
+ return enqueueBoardWork(projectRoot, sessionId, async () => {
503
+ if (isSessionBoardActive(projectRoot, sessionId)) return null;
504
+ const board = await getBoard(projectRoot, boardId);
505
+ if (!board || board.tasks.length > 0 || !isOwnedSessionBoard(board.tags)) return null;
506
+ if (sessionIdFromTags(board.tags) !== sessionId) return null;
507
+ return await removeBoard(projectRoot, board.id) ? board.id : null;
508
+ });
509
+ }
510
+ async function removeOwnedSessionBoard(projectRoot, boardId, sessionId) {
511
+ return enqueueBoardWork(projectRoot, sessionId, async () => {
512
+ if (isSessionBoardActive(projectRoot, sessionId)) return null;
513
+ const board = await getBoard(projectRoot, boardId);
514
+ if (!board || !isOwnedSessionBoard(board.tags)) return null;
515
+ if (sessionIdFromTags(board.tags) !== sessionId) return null;
516
+ return await removeBoard(projectRoot, board.id) ? board.id : null;
517
+ });
518
+ }
519
+ async function cleanupSessionKanbanBoard(projectRoot, sessionId) {
520
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return [];
521
+ if (isSessionBoardActive(projectRoot, sessionId)) return [];
522
+ const candidates = (await listBoards(projectRoot)).filter(
523
+ (board) => isOwnedSessionBoard(board.tags) && sessionIdFromTags(board.tags) === sessionId
524
+ );
525
+ const removed = await Promise.all(
526
+ candidates.map((board) => removeOwnedSessionBoard(projectRoot, board.id, sessionId))
527
+ );
528
+ return removed.filter((boardId) => Boolean(boardId));
529
+ }
530
+ async function cleanupSessionKanbanBoardIfEmpty(projectRoot, sessionId) {
531
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return [];
532
+ if (isSessionBoardActive(projectRoot, sessionId)) return [];
533
+ const candidates = (await listBoards(projectRoot)).filter(
534
+ (board) => board.taskCount === 0 && isOwnedSessionBoard(board.tags) && sessionIdFromTags(board.tags) === sessionId
535
+ );
536
+ const removed = await Promise.all(
537
+ candidates.map((board) => removeEmptySessionBoard(projectRoot, board.id, sessionId))
538
+ );
539
+ return removed.filter((boardId) => Boolean(boardId));
540
+ }
541
+ async function cleanupEmptySessionKanbanBoards(projectRoot, activeSessionId = "") {
542
+ if (!projectRoot || process.env[MIRROR_DISABLED_ENV] === "0") return [];
543
+ const candidates = (await listBoards(projectRoot)).flatMap((board) => {
544
+ const ownerSessionId = sessionIdFromTags(board.tags);
545
+ return board.taskCount === 0 && isOwnedSessionBoard(board.tags) && ownerSessionId && ownerSessionId !== activeSessionId && !isSessionBoardActive(projectRoot, ownerSessionId) ? [{ boardId: board.id, sessionId: ownerSessionId }] : [];
546
+ });
547
+ const removed = await Promise.all(
548
+ candidates.map(
549
+ ({ boardId, sessionId }) => removeEmptySessionBoard(projectRoot, boardId, sessionId)
550
+ )
551
+ );
552
+ return removed.filter((boardId) => Boolean(boardId));
553
+ }
554
+ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
555
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
556
+ return enqueueBoardWork(projectRoot, sessionId, async () => {
557
+ const board = await ensureSessionKanbanBoard(projectRoot, sessionId);
558
+ if (!board) return null;
559
+ const result = await syncBoardFromTaskGraph(
560
+ projectRoot,
561
+ board.id,
562
+ deserializeTaskGraph(graph),
563
+ {
564
+ sourceSystem,
565
+ tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
566
+ archiveMissingTasks: true,
567
+ includeCompletedTasks: true,
568
+ allowRequirementScopeShrink: true
569
+ }
570
+ );
571
+ if (!result) return null;
572
+ const compacted = await compactSessionMirrorBoard(projectRoot, board.id);
573
+ if (compacted?.removedTaskIds.length) {
574
+ return await getBoard(projectRoot, board.id) ?? result.board;
575
+ }
576
+ return result.board;
577
+ });
578
+ }
579
+ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
580
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return;
581
+ const key = mirrorKey(projectRoot, sessionId, sourceSystem);
582
+ const previous = pendingMirrors.get(key);
583
+ const reconciliationGraph = previous ? completedReconciliationGraph(
584
+ graph,
585
+ [previous.reconciliationGraph, previous.graph].filter(
586
+ (candidate) => candidate !== void 0
587
+ )
588
+ ) : void 0;
589
+ pendingMirrors.set(key, {
590
+ projectRoot,
591
+ sessionId,
592
+ graph,
593
+ ...reconciliationGraph ? { reconciliationGraph } : {},
594
+ sourceSystem
595
+ });
596
+ if (activeMirrors.has(key)) return;
597
+ activeMirrors.add(key);
598
+ void (async () => {
599
+ try {
600
+ for (; ; ) {
601
+ const pending = pendingMirrors.get(key);
602
+ if (!pending) break;
603
+ pendingMirrors.delete(key);
604
+ try {
605
+ if (pending.reconciliationGraph) {
606
+ await projectGraph(
607
+ pending.projectRoot,
608
+ pending.sessionId,
609
+ pending.reconciliationGraph,
610
+ pending.sourceSystem
611
+ );
612
+ }
613
+ await projectGraph(
614
+ pending.projectRoot,
615
+ pending.sessionId,
616
+ pending.graph,
617
+ pending.sourceSystem
618
+ );
619
+ mirrorFailures.delete(boardKey(pending.projectRoot, pending.sessionId));
620
+ } catch (error) {
621
+ const message = error instanceof Error ? error.message : String(error);
622
+ mirrorFailures.set(boardKey(pending.projectRoot, pending.sessionId), {
623
+ message,
624
+ sourceSystem: pending.sourceSystem
625
+ });
626
+ console.warn(
627
+ JSON.stringify({
628
+ level: "warn",
629
+ event: "session-kanban.mirror-failed",
630
+ sessionId: pending.sessionId,
631
+ sourceSystem: pending.sourceSystem,
632
+ message,
633
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
634
+ })
635
+ );
636
+ }
637
+ }
638
+ } finally {
639
+ activeMirrors.delete(key);
640
+ const pending = pendingMirrors.get(key);
641
+ if (pending) {
642
+ pendingMirrors.delete(key);
643
+ queueLatestMirror(
644
+ pending.projectRoot,
645
+ pending.sessionId,
646
+ pending.graph,
647
+ pending.sourceSystem
648
+ );
649
+ }
650
+ }
651
+ })();
652
+ }
653
+ function takeSessionMirrorFailure(projectRoot, sessionId) {
654
+ if (!projectRoot || !sessionId) return void 0;
655
+ const key = boardKey(projectRoot, sessionId);
656
+ const failure = mirrorFailures.get(key);
657
+ if (!failure) return void 0;
658
+ mirrorFailures.delete(key);
659
+ return `Kanban mirror (${failure.sourceSystem}) failed and the board may be stale: ${failure.message}`;
660
+ }
661
+ function hasInFlightTodoMirror(projectRoot, sessionId) {
662
+ if (!projectRoot || !sessionId) return false;
663
+ const key = mirrorKey(projectRoot, sessionId, "session-todo");
664
+ return pendingMirrors.has(key) || activeMirrors.has(key);
665
+ }
666
+ function projectSessionTodosToKanban(projectRoot, todos, sessionId) {
667
+ return projectGraph(
668
+ projectRoot,
669
+ sessionId,
670
+ todoListToSerializedGraph(todos, sessionId),
671
+ "session-todo"
672
+ );
673
+ }
674
+ function projectSessionTasksToKanban(projectRoot, tasks, sessionId) {
675
+ return projectGraph(
676
+ projectRoot,
677
+ sessionId,
678
+ taskFileToSerializedGraph(tasks, sessionId),
679
+ "session-task"
680
+ );
681
+ }
682
+ function projectSessionPlanToKanban(projectRoot, items, sessionId) {
683
+ return projectGraph(
684
+ projectRoot,
685
+ sessionId,
686
+ planFileToSerializedGraph(items, sessionId),
687
+ "session-plan"
688
+ );
689
+ }
690
+ function fireAndForget(context, work) {
691
+ void work.catch((err) => {
692
+ const message = err instanceof Error ? err.message : String(err);
693
+ console.warn(
694
+ JSON.stringify({
695
+ level: "warn",
696
+ event: "session-kanban",
697
+ context,
698
+ message,
699
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
700
+ })
701
+ );
702
+ });
703
+ }
704
+ function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
705
+ queueLatestMirror(
706
+ projectRoot,
707
+ sessionId,
708
+ todoListToSerializedGraph(todos, sessionId),
709
+ "session-todo"
710
+ );
711
+ }
712
+ function mirrorSessionTasksToKanban(projectRoot, tasks, sessionId) {
713
+ queueLatestMirror(
714
+ projectRoot,
715
+ sessionId,
716
+ taskFileToSerializedGraph(tasks, sessionId),
717
+ "session-task"
718
+ );
719
+ }
720
+ function mirrorSessionPlanToKanban(projectRoot, items, sessionId) {
721
+ queueLatestMirror(
722
+ projectRoot,
723
+ sessionId,
724
+ planFileToSerializedGraph(items, sessionId),
725
+ "session-plan"
726
+ );
727
+ }
728
+ function attachSessionKanbanMirror(context) {
729
+ const existing = bindings.get(context);
730
+ if (existing) return existing;
731
+ const attachedProjectRoot = context.projectRoot ?? "";
732
+ let registeredSessionId = "";
733
+ const syncActiveSessionRegistration = () => {
734
+ if (!attachedProjectRoot) return;
735
+ const currentSessionId = context.session?.id ?? "";
736
+ if (currentSessionId === registeredSessionId) return;
737
+ if (registeredSessionId) {
738
+ releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
739
+ fireAndForget(
740
+ "cleanup-board",
741
+ cleanupSessionKanbanBoard(attachedProjectRoot, registeredSessionId)
742
+ );
743
+ }
744
+ registeredSessionId = currentSessionId;
745
+ if (registeredSessionId) {
746
+ retainActiveSessionBoard(attachedProjectRoot, registeredSessionId);
747
+ }
748
+ };
749
+ syncActiveSessionRegistration();
750
+ let watcher = null;
751
+ let watchedDir = "";
752
+ let timer = null;
753
+ let unsubscribeBoardEvents = null;
754
+ let watchedBoardId = "";
755
+ let boardTimer = null;
756
+ let presenceTimer = null;
757
+ const sessionId = () => context.session?.id ?? "";
758
+ const activeManagedBoardId = () => {
759
+ const metaKanban = context.meta["kanban"];
760
+ const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
761
+ return context.currentKanbanBoardId ?? (typeof metaBoardId === "string" && metaBoardId ? metaBoardId : "");
762
+ };
763
+ const refreshFiles = async () => {
764
+ const id = sessionId();
765
+ if (!id) return;
766
+ const planPath = context.meta["plan.path"];
767
+ if (typeof planPath === "string" && planPath) {
768
+ const plan = await loadPlan(planPath);
769
+ if (plan) await projectSessionPlanToKanban(context.projectRoot, plan.items, id);
770
+ }
771
+ const taskPath = context.meta["task.path"];
772
+ if (typeof taskPath === "string" && taskPath) {
773
+ const tasks = await loadTasks(taskPath);
774
+ if (tasks) await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);
775
+ }
776
+ };
777
+ const refreshBoard = async () => {
778
+ if (!watchedBoardId) return;
779
+ const board = await getBoard(context.projectRoot, watchedBoardId);
780
+ if (!board) return;
781
+ if (board.lifecycle?.mode === "managed") applyManagedKanbanBoardToTodos2(context, board);
782
+ else applySessionKanbanBoardToTodos2(context, board);
783
+ };
784
+ const configureBoardWatcher = async () => {
785
+ const id = sessionId();
786
+ const managedBoardId = activeManagedBoardId();
787
+ const board = managedBoardId ? await getBoard(context.projectRoot, managedBoardId) : id ? await ensureSessionKanbanBoard(context.projectRoot, id) : null;
788
+ if (!board) return;
789
+ if (board.id === watchedBoardId) {
790
+ await refreshBoard();
791
+ return;
792
+ }
793
+ unsubscribeBoardEvents?.();
794
+ unsubscribeBoardEvents = null;
795
+ watchedBoardId = board.id;
796
+ try {
797
+ unsubscribeBoardEvents = bridgeKanbanSupervisor(
798
+ context.projectRoot,
799
+ (event) => {
800
+ const data = event.data;
801
+ if (data?.boardId !== board.id) return;
802
+ if (boardTimer) clearTimeout(boardTimer);
803
+ boardTimer = setTimeout(() => fireAndForget("refresh-board", refreshBoard()), 60);
804
+ },
805
+ { autoReconnect: true, reconnectDelayMs: 1e3 }
806
+ );
807
+ const touchPresence = () => touchKanbanPresence(context.projectRoot, board.id, {
808
+ sessionId: id,
809
+ agentId: context.agentId,
810
+ agentName: context.agentName
811
+ });
812
+ fireAndForget("touch-presence", touchPresence());
813
+ if (presenceTimer) clearInterval(presenceTimer);
814
+ presenceTimer = setInterval(() => fireAndForget("touch-presence", touchPresence()), 6e4);
815
+ presenceTimer.unref?.();
816
+ if (board.lifecycle?.mode === "managed") applyManagedKanbanBoardToTodos2(context, board);
817
+ } catch {
818
+ unsubscribeBoardEvents = null;
819
+ watchedBoardId = "";
820
+ }
821
+ };
822
+ const configureWatcher = () => {
823
+ const planPath = context.meta["plan.path"];
824
+ const taskPath = context.meta["task.path"];
825
+ const candidate = typeof planPath === "string" && planPath ? dirname(planPath) : typeof taskPath === "string" && taskPath ? dirname(taskPath) : "";
826
+ if (!candidate || candidate === watchedDir) return;
827
+ watcher?.close();
828
+ watcher = null;
829
+ watchedDir = candidate;
830
+ try {
831
+ watcher = watch(candidate, { persistent: false }, (_event, filename) => {
832
+ const name = filename?.toString();
833
+ const currentPlanPath = context.meta["plan.path"];
834
+ const currentTaskPath = context.meta["task.path"];
835
+ const planName = typeof currentPlanPath === "string" ? basename(currentPlanPath) : "";
836
+ const taskName = typeof currentTaskPath === "string" ? basename(currentTaskPath) : "";
837
+ if (name && name !== planName && name !== taskName) return;
838
+ if (timer) clearTimeout(timer);
839
+ timer = setTimeout(() => fireAndForget("refresh-files", refreshFiles()), 60);
840
+ });
841
+ watcher.on("error", () => watcher?.close());
842
+ } catch {
843
+ watcher = null;
844
+ watchedDir = "";
845
+ }
846
+ };
847
+ const unsubscribe = context.state.onChange((change) => {
848
+ if (change.kind === "todos_replaced" && !suppressedTodoMirrors.has(context)) {
849
+ const snapshot = change.completedSnapshot ?? change.todos;
850
+ const unbound = todosNeedingSessionMirror(snapshot, activeManagedBoardId());
851
+ if (snapshot.length > 0 && unbound.length === 0) return;
852
+ mirrorSessionTodosToKanban(context.projectRoot, unbound, sessionId());
853
+ return;
854
+ }
855
+ if (change.kind === "meta_set" && (change.key === "plan.path" || change.key === "task.path" || change.key === "kanban")) {
856
+ syncActiveSessionRegistration();
857
+ configureWatcher();
858
+ if (!activeManagedBoardId()) {
859
+ fireAndForget("ensure-board", ensureSessionKanbanBoard(context.projectRoot, sessionId()));
860
+ }
861
+ fireAndForget("configure-watcher", configureBoardWatcher());
862
+ fireAndForget("refresh-board", refreshBoard());
863
+ fireAndForget("refresh-files", refreshFiles());
864
+ }
865
+ });
866
+ configureWatcher();
867
+ fireAndForget("configure-watcher", configureBoardWatcher());
868
+ const detach = () => {
869
+ unsubscribe();
870
+ if (timer) clearTimeout(timer);
871
+ if (boardTimer) clearTimeout(boardTimer);
872
+ if (presenceTimer) clearInterval(presenceTimer);
873
+ watcher?.close();
874
+ unsubscribeBoardEvents?.();
875
+ bindings.delete(context);
876
+ if (attachedProjectRoot && registeredSessionId) {
877
+ releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
878
+ fireAndForget(
879
+ "cleanup-board",
880
+ cleanupSessionKanbanBoard(attachedProjectRoot, registeredSessionId)
881
+ );
882
+ registeredSessionId = "";
883
+ }
884
+ };
885
+ bindings.set(context, detach);
886
+ return detach;
887
+ }
888
+ async function rebindSessionKanbanTask(context) {
889
+ const sessionId = context.session?.id;
890
+ if (!sessionId || !context.projectRoot) return null;
891
+ if (context.currentKanbanTaskId) return null;
892
+ let best;
893
+ try {
894
+ const snapshot = await getKanbanOrchestrationSnapshot(context.projectRoot);
895
+ const nowMs = Date.now();
896
+ for (const result of snapshot.running) {
897
+ const assignment = result.task.assignment;
898
+ if (assignment?.status !== "running") continue;
899
+ const expiresAt = assignment.leaseExpiresAt ? Date.parse(assignment.leaseExpiresAt) : Number.NaN;
900
+ if (Number.isFinite(expiresAt) && expiresAt <= nowMs) continue;
901
+ const entry = result.board.presence?.find(
902
+ (candidate) => candidate.sessionId === sessionId && candidate.taskId === result.task.id
903
+ );
904
+ if (!entry) continue;
905
+ if (!best || entry.lastSeenAt > best.lastSeenAt) {
906
+ best = { boardId: result.board.id, taskId: result.task.id, lastSeenAt: entry.lastSeenAt };
907
+ }
908
+ }
909
+ } catch {
910
+ return null;
911
+ }
912
+ if (!best) return null;
913
+ context.setCurrentKanbanTask(best.taskId, best.boardId);
914
+ return { boardId: best.boardId, taskId: best.taskId };
915
+ }
916
+ var degradationReason;
917
+ function sessionKanbanDegradation() {
918
+ return degradationReason;
919
+ }
920
+ async function hydrateSessionKanban(context) {
921
+ const id = context.session?.id ?? "";
922
+ if (!id) return null;
923
+ try {
924
+ const board = await hydrateSessionKanbanBoard(context, id);
925
+ degradationReason = void 0;
926
+ return board;
927
+ } catch (error) {
928
+ degradationReason = error instanceof Error ? error.message : String(error);
929
+ fireAndForget("hydrate", Promise.reject(error));
930
+ return null;
931
+ }
932
+ }
933
+ async function hydrateSessionKanbanBoard(context, id) {
934
+ await rebindSessionKanbanTask(context);
935
+ await cleanupEmptySessionKanbanBoards(context.projectRoot, id);
936
+ if (context.projectRoot) {
937
+ fireAndForget("prune-session-boards", pruneSessionBoards(context.projectRoot));
938
+ }
939
+ let board = await ensureSessionKanbanBoard(context.projectRoot, id);
940
+ if (context.todos.length) {
941
+ board = await projectSessionTodosToKanban(context.projectRoot, context.todos, id);
942
+ }
943
+ const planPath = context.meta["plan.path"];
944
+ if (typeof planPath === "string" && planPath) {
945
+ const plan = await loadPlan(planPath);
946
+ if (plan) board = await projectSessionPlanToKanban(context.projectRoot, plan.items, id);
947
+ }
948
+ const taskPath = context.meta["task.path"];
949
+ if (typeof taskPath === "string" && taskPath) {
950
+ const tasks = await loadTasks(taskPath);
951
+ if (tasks) board = await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);
952
+ }
953
+ return board;
954
+ }
955
+ function applySessionKanbanBoardToTodos2(context, board) {
956
+ return applySessionKanbanBoardToTodos(context, board, {
957
+ sessionIdFromTags,
958
+ isOwnedSessionBoard,
959
+ hasInFlightTodoMirror,
960
+ suppressedTodoMirrors
961
+ });
962
+ }
963
+ function applyManagedKanbanBoardToTodos2(context, board) {
964
+ return applyManagedKanbanBoardToTodos(context, board, suppressedTodoMirrors, {
965
+ sessionOwnerFromTags: sessionIdFromTags
966
+ });
967
+ }
968
+ function applySessionKanbanTaskToSource2(context, task, options = {}) {
969
+ return applySessionKanbanTaskToSource(context, task, suppressedTodoMirrors, options);
970
+ }
971
+
972
+ export {
973
+ PLAN_STATUS_TO_TASK,
974
+ todoListToSerializedGraph,
975
+ taskFileToSerializedGraph,
976
+ planFileToSerializedGraph,
977
+ blockingTitles,
978
+ orderTasksForTodos,
979
+ todosNeedingSessionMirror,
980
+ SESSION_KANBAN_COLUMNS,
981
+ ensureSessionKanbanBoard,
982
+ cleanupSessionKanbanBoard,
983
+ cleanupSessionKanbanBoardIfEmpty,
984
+ cleanupEmptySessionKanbanBoards,
985
+ takeSessionMirrorFailure,
986
+ projectSessionTodosToKanban,
987
+ projectSessionTasksToKanban,
988
+ projectSessionPlanToKanban,
989
+ mirrorSessionTodosToKanban,
990
+ mirrorSessionTasksToKanban,
991
+ mirrorSessionPlanToKanban,
992
+ attachSessionKanbanMirror,
993
+ rebindSessionKanbanTask,
994
+ sessionKanbanDegradation,
995
+ hydrateSessionKanban,
996
+ applySessionKanbanBoardToTodos2 as applySessionKanbanBoardToTodos,
997
+ applyManagedKanbanBoardToTodos2 as applyManagedKanbanBoardToTodos,
998
+ applySessionKanbanTaskToSource2 as applySessionKanbanTaskToSource
999
+ };
1000
+ //# sourceMappingURL=chunk-QNJ7AQ4B.js.map