@wrongstack/tools 0.286.0 → 0.289.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +2 -2
  2. package/dist/_edit-match.d.ts.map +1 -1
  3. package/dist/_fetch-guard.d.ts.map +1 -1
  4. package/dist/_redact-command.d.ts.map +1 -1
  5. package/dist/_spawn-stream.d.ts.map +1 -1
  6. package/dist/audit.js +61 -14
  7. package/dist/audit.js.map +3 -3
  8. package/dist/bash.d.ts +1 -1
  9. package/dist/bash.d.ts.map +1 -1
  10. package/dist/bash.js +97 -19
  11. package/dist/bash.js.map +3 -3
  12. package/dist/batch-tool-use.d.ts +1 -1
  13. package/dist/batch-tool-use.d.ts.map +1 -1
  14. package/dist/batch-tool-use.js +26 -8
  15. package/dist/batch-tool-use.js.map +2 -2
  16. package/dist/browser/index.js +1 -1
  17. package/dist/browser/index.js.map +1 -1
  18. package/dist/browser/types.d.ts +1 -1
  19. package/dist/browser/types.d.ts.map +1 -1
  20. package/dist/builtin.d.ts +12 -10
  21. package/dist/builtin.d.ts.map +1 -1
  22. package/dist/builtin.js +2107 -864
  23. package/dist/builtin.js.map +4 -4
  24. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  25. package/dist/codebase-index/bm25.d.ts +1 -2
  26. package/dist/codebase-index/bm25.d.ts.map +1 -1
  27. package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
  28. package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
  29. package/dist/codebase-index/codebase-stats-tool.d.ts +7 -0
  30. package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
  31. package/dist/codebase-index/go-parser.d.ts.map +1 -1
  32. package/dist/codebase-index/index-service.d.ts +11 -1
  33. package/dist/codebase-index/index-service.d.ts.map +1 -1
  34. package/dist/codebase-index/index.d.ts +2 -1
  35. package/dist/codebase-index/index.d.ts.map +1 -1
  36. package/dist/codebase-index/index.js +934 -283
  37. package/dist/codebase-index/index.js.map +4 -4
  38. package/dist/codebase-index/indexer.d.ts.map +1 -1
  39. package/dist/codebase-index/py-parser.d.ts.map +1 -1
  40. package/dist/codebase-index/rs-parser.d.ts.map +1 -1
  41. package/dist/codebase-index/schema.d.ts +43 -1
  42. package/dist/codebase-index/schema.d.ts.map +1 -1
  43. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  44. package/dist/codebase-index/worker-protocol.d.ts +1 -0
  45. package/dist/codebase-index/worker-protocol.d.ts.map +1 -1
  46. package/dist/codebase-index/worker.js +793 -241
  47. package/dist/codebase-index/worker.js.map +4 -4
  48. package/dist/codebase-index/writer.d.ts +27 -1
  49. package/dist/codebase-index/writer.d.ts.map +1 -1
  50. package/dist/e2e.js +477 -0
  51. package/dist/e2e.js.map +7 -0
  52. package/dist/edit.d.ts.map +1 -1
  53. package/dist/edit.js +36 -12
  54. package/dist/edit.js.map +3 -3
  55. package/dist/exec.d.ts +1 -1
  56. package/dist/exec.d.ts.map +1 -1
  57. package/dist/exec.js +60 -5
  58. package/dist/exec.js.map +3 -3
  59. package/dist/fetch.js +2 -2
  60. package/dist/fetch.js.map +2 -2
  61. package/dist/format.js +61 -14
  62. package/dist/format.js.map +3 -3
  63. package/dist/glob.js +2 -2
  64. package/dist/glob.js.map +1 -1
  65. package/dist/grep.d.ts.map +1 -1
  66. package/dist/grep.js +2 -2
  67. package/dist/grep.js.map +2 -2
  68. package/dist/index.d.ts +3 -1
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +2535 -865
  71. package/dist/index.js.map +4 -4
  72. package/dist/install.js +61 -14
  73. package/dist/install.js.map +3 -3
  74. package/dist/kanban.d.ts +16 -2
  75. package/dist/kanban.d.ts.map +1 -1
  76. package/dist/kanban.js +795 -532
  77. package/dist/kanban.js.map +4 -4
  78. package/dist/languages/detect.d.ts.map +1 -1
  79. package/dist/languages/index.js +61 -14
  80. package/dist/languages/index.js.map +3 -3
  81. package/dist/languages/types.d.ts +7 -0
  82. package/dist/languages/types.d.ts.map +1 -1
  83. package/dist/lint.js +61 -14
  84. package/dist/lint.js.map +3 -3
  85. package/dist/next-steps.d.ts +6 -2
  86. package/dist/next-steps.d.ts.map +1 -1
  87. package/dist/next-steps.js +3 -3
  88. package/dist/next-steps.js.map +2 -2
  89. package/dist/outdated.js +61 -14
  90. package/dist/outdated.js.map +3 -3
  91. package/dist/pack.js +2104 -861
  92. package/dist/pack.js.map +4 -4
  93. package/dist/plan.d.ts.map +1 -1
  94. package/dist/plan.js +158 -4
  95. package/dist/plan.js.map +4 -4
  96. package/dist/process-registry.js +12 -5
  97. package/dist/process-registry.js.map +2 -2
  98. package/dist/search.js +2 -2
  99. package/dist/search.js.map +2 -2
  100. package/dist/session-kanban.d.ts +47 -0
  101. package/dist/session-kanban.d.ts.map +1 -0
  102. package/dist/session-kanban.js +626 -0
  103. package/dist/session-kanban.js.map +7 -0
  104. package/dist/task.d.ts.map +1 -1
  105. package/dist/task.js +167 -4
  106. package/dist/task.js.map +4 -4
  107. package/dist/test.js +61 -14
  108. package/dist/test.js.map +3 -3
  109. package/dist/todo.d.ts.map +1 -1
  110. package/dist/todo.js +159 -7
  111. package/dist/todo.js.map +4 -4
  112. package/dist/tool-icon-map.d.ts.map +1 -1
  113. package/dist/tool-icons.d.ts.map +1 -1
  114. package/dist/tool-icons.js +7 -1
  115. package/dist/tool-icons.js.map +2 -2
  116. package/dist/tree.js +2 -2
  117. package/dist/tree.js.map +1 -1
  118. package/dist/typecheck.js +61 -14
  119. package/dist/typecheck.js.map +3 -3
  120. package/package.json +11 -3
@@ -0,0 +1,626 @@
1
+ // src/session-kanban.ts
2
+ import { watch } from "node:fs";
3
+ import { basename, dirname } from "node:path";
4
+ import {
5
+ deserializeTaskGraph,
6
+ GlobalMailbox,
7
+ loadPlan,
8
+ loadTasks,
9
+ mutatePlan,
10
+ mutateTasks
11
+ } from "@wrongstack/core";
12
+ import { resolveWstackPaths } from "@wrongstack/core/utils";
13
+ import {
14
+ createBoard,
15
+ getBoard,
16
+ getKanbanDir,
17
+ listBoards,
18
+ removeBoard,
19
+ syncBoardFromTaskGraph,
20
+ touchKanbanPresence,
21
+ updateBoard
22
+ } from "@wrongstack/kanban";
23
+ var SESSION_BOARD_TAG = "session-work";
24
+ var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
25
+ var SESSION_KANBAN_COLUMNS = [
26
+ { id: "todo", title: "Todo", order: 0, wipLimit: 0, color: "#2563eb" },
27
+ { id: "in-progress", title: "Running", order: 1, wipLimit: 1, color: "#d97706" },
28
+ { id: "review", title: "Preview", order: 2, wipLimit: 0, color: "#7c3aed" },
29
+ { id: "done", title: "Done", order: 3, wipLimit: 0, color: "#16a34a" }
30
+ ];
31
+ var boardQueue = /* @__PURE__ */ new Map();
32
+ var boardEnsures = /* @__PURE__ */ new Map();
33
+ var bindings = /* @__PURE__ */ new WeakMap();
34
+ var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
35
+ var activeSessionBoards = /* @__PURE__ */ new Map();
36
+ function boardKey(projectRoot, sessionId) {
37
+ return `${projectRoot}\0${sessionId}`;
38
+ }
39
+ function sessionTag(sessionId) {
40
+ return `session:${sessionId}`;
41
+ }
42
+ function sessionBoardTitle(sessionId) {
43
+ const leaf = sessionId.split(/[\\/]/).filter(Boolean).pop() ?? sessionId;
44
+ return `Session ${leaf.slice(0, 12)}`;
45
+ }
46
+ function sessionBoardTags(sessionId) {
47
+ return ["session", SESSION_BOARD_TAG, sessionTag(sessionId)];
48
+ }
49
+ function sessionIdFromTags(tags) {
50
+ const tag = tags?.find((candidate) => candidate.startsWith("session:"));
51
+ return tag?.slice("session:".length) || null;
52
+ }
53
+ function isOwnedSessionBoard(tags) {
54
+ return Boolean(tags?.includes(SESSION_BOARD_TAG) && sessionIdFromTags(tags));
55
+ }
56
+ function retainActiveSessionBoard(projectRoot, sessionId) {
57
+ const key = boardKey(projectRoot, sessionId);
58
+ activeSessionBoards.set(key, (activeSessionBoards.get(key) ?? 0) + 1);
59
+ }
60
+ function releaseActiveSessionBoard(projectRoot, sessionId) {
61
+ const key = boardKey(projectRoot, sessionId);
62
+ const remaining = (activeSessionBoards.get(key) ?? 0) - 1;
63
+ if (remaining > 0) activeSessionBoards.set(key, remaining);
64
+ else activeSessionBoards.delete(key);
65
+ }
66
+ function isSessionBoardActive(projectRoot, sessionId) {
67
+ return (activeSessionBoards.get(boardKey(projectRoot, sessionId)) ?? 0) > 0;
68
+ }
69
+ function sameColumns(columns) {
70
+ return columns.length === SESSION_KANBAN_COLUMNS.length && columns.every((column, index) => column.id === SESSION_KANBAN_COLUMNS[index]?.id);
71
+ }
72
+ async function ensureSessionKanbanBoard(projectRoot, sessionId) {
73
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
74
+ const key = boardKey(projectRoot, sessionId);
75
+ const inFlight = boardEnsures.get(key);
76
+ if (inFlight) return inFlight;
77
+ const promise = (async () => {
78
+ const summary = (await listBoards(projectRoot)).find(
79
+ (board2) => board2.tags?.includes(sessionTag(sessionId))
80
+ );
81
+ let board = summary ? await getBoard(projectRoot, summary.id) : null;
82
+ if (!board) {
83
+ return createBoard(projectRoot, {
84
+ title: sessionBoardTitle(sessionId),
85
+ description: "Live session work: todos, tasks, and plan items.",
86
+ tags: sessionBoardTags(sessionId),
87
+ columns: SESSION_KANBAN_COLUMNS,
88
+ generatedBy: `session-kanban:${sessionId}`
89
+ });
90
+ }
91
+ if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
92
+ board = await updateBoard(projectRoot, board.id, {
93
+ title: sessionBoardTitle(sessionId),
94
+ description: "Live session work: todos, tasks, and plan items.",
95
+ tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
96
+ columns: SESSION_KANBAN_COLUMNS
97
+ }) ?? board;
98
+ }
99
+ return board;
100
+ })();
101
+ boardEnsures.set(key, promise);
102
+ try {
103
+ return await promise;
104
+ } finally {
105
+ boardEnsures.delete(key);
106
+ }
107
+ }
108
+ function enqueueBoardWork(projectRoot, sessionId, work) {
109
+ const key = boardKey(projectRoot, sessionId);
110
+ const previous = boardQueue.get(key) ?? Promise.resolve();
111
+ const result = previous.catch(() => void 0).then(work);
112
+ const tail = result.then(
113
+ () => void 0,
114
+ () => void 0
115
+ );
116
+ boardQueue.set(key, tail);
117
+ void tail.then(() => {
118
+ if (boardQueue.get(key) === tail) boardQueue.delete(key);
119
+ });
120
+ return result;
121
+ }
122
+ async function removeEmptySessionBoard(projectRoot, boardId, sessionId) {
123
+ return enqueueBoardWork(projectRoot, sessionId, async () => {
124
+ if (isSessionBoardActive(projectRoot, sessionId)) return null;
125
+ const board = await getBoard(projectRoot, boardId);
126
+ if (!board || board.tasks.length > 0 || !isOwnedSessionBoard(board.tags)) return null;
127
+ if (sessionIdFromTags(board.tags) !== sessionId) return null;
128
+ return await removeBoard(projectRoot, board.id) ? board.id : null;
129
+ });
130
+ }
131
+ async function cleanupSessionKanbanBoardIfEmpty(projectRoot, sessionId) {
132
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return [];
133
+ if (isSessionBoardActive(projectRoot, sessionId)) return [];
134
+ const candidates = (await listBoards(projectRoot)).filter(
135
+ (board) => board.taskCount === 0 && isOwnedSessionBoard(board.tags) && sessionIdFromTags(board.tags) === sessionId
136
+ );
137
+ const removed = await Promise.all(
138
+ candidates.map((board) => removeEmptySessionBoard(projectRoot, board.id, sessionId))
139
+ );
140
+ return removed.filter((boardId) => Boolean(boardId));
141
+ }
142
+ async function cleanupEmptySessionKanbanBoards(projectRoot, activeSessionId = "") {
143
+ if (!projectRoot || process.env[MIRROR_DISABLED_ENV] === "0") return [];
144
+ const candidates = (await listBoards(projectRoot)).flatMap((board) => {
145
+ const ownerSessionId = sessionIdFromTags(board.tags);
146
+ return board.taskCount === 0 && isOwnedSessionBoard(board.tags) && ownerSessionId && ownerSessionId !== activeSessionId && !isSessionBoardActive(projectRoot, ownerSessionId) ? [{ boardId: board.id, sessionId: ownerSessionId }] : [];
147
+ });
148
+ const removed = await Promise.all(
149
+ candidates.map(
150
+ ({ boardId, sessionId }) => removeEmptySessionBoard(projectRoot, boardId, sessionId)
151
+ )
152
+ );
153
+ return removed.filter((boardId) => Boolean(boardId));
154
+ }
155
+ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
156
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
157
+ return enqueueBoardWork(projectRoot, sessionId, async () => {
158
+ const board = await ensureSessionKanbanBoard(projectRoot, sessionId);
159
+ if (!board) return null;
160
+ const result = await syncBoardFromTaskGraph(
161
+ projectRoot,
162
+ board.id,
163
+ deserializeTaskGraph(graph),
164
+ {
165
+ sourceSystem,
166
+ tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
167
+ archiveMissingTasks: true,
168
+ includeCompletedTasks: true
169
+ }
170
+ );
171
+ return result?.board ?? null;
172
+ });
173
+ }
174
+ function todoListToSerializedGraph(todos, sessionId) {
175
+ const nodes = todos.map((todo, index) => ({
176
+ id: todo.id,
177
+ title: todo.content,
178
+ description: todo.activeForm ?? "",
179
+ type: "chore",
180
+ priority: "medium",
181
+ status: todo.status,
182
+ createdAt: index,
183
+ updatedAt: index
184
+ }));
185
+ return {
186
+ id: `todo:${sessionId}`,
187
+ specId: `todo:${sessionId}`,
188
+ title: "Session todos",
189
+ nodes,
190
+ edges: [],
191
+ rootNodes: nodes.map((node) => node.id),
192
+ createdAt: 0,
193
+ updatedAt: 0
194
+ };
195
+ }
196
+ function taskFileToSerializedGraph(tasks, sessionId) {
197
+ const ids = new Set(tasks.map((task) => task.id));
198
+ const nodes = tasks.map((task, index) => ({
199
+ id: task.id,
200
+ title: task.title,
201
+ description: task.description ?? "",
202
+ type: task.type,
203
+ priority: task.priority,
204
+ status: task.status,
205
+ ...task.assignee ? { assignee: task.assignee } : {},
206
+ ...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
207
+ createdAt: index,
208
+ updatedAt: index
209
+ }));
210
+ const edges = tasks.flatMap(
211
+ (task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
212
+ id: `${dependency}->${task.id}`,
213
+ from: dependency,
214
+ to: task.id,
215
+ type: "depends_on"
216
+ }))
217
+ );
218
+ const hasIncoming = new Set(edges.map((edge) => edge.to));
219
+ const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
220
+ return {
221
+ // Keep the historical graph id so existing mirrored task cards are reused.
222
+ id: `session:${sessionId}`,
223
+ specId: `session:${sessionId}`,
224
+ title: "Session tasks",
225
+ nodes,
226
+ edges,
227
+ rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
228
+ createdAt: 0,
229
+ updatedAt: 0
230
+ };
231
+ }
232
+ var PLAN_STATUS_TO_TASK = {
233
+ open: "pending",
234
+ in_progress: "in_progress",
235
+ done: "completed"
236
+ };
237
+ function planFileToSerializedGraph(items, sessionId) {
238
+ const nodes = items.map((item, index) => ({
239
+ id: item.id,
240
+ title: item.title,
241
+ description: item.details ?? "",
242
+ type: "chore",
243
+ priority: "medium",
244
+ status: PLAN_STATUS_TO_TASK[item.status],
245
+ createdAt: index,
246
+ updatedAt: index
247
+ }));
248
+ return {
249
+ id: `plan:${sessionId}`,
250
+ specId: `plan:${sessionId}`,
251
+ title: "Session plan",
252
+ nodes,
253
+ edges: [],
254
+ rootNodes: nodes.map((node) => node.id),
255
+ createdAt: 0,
256
+ updatedAt: 0
257
+ };
258
+ }
259
+ function projectSessionTodosToKanban(projectRoot, todos, sessionId) {
260
+ return projectGraph(
261
+ projectRoot,
262
+ sessionId,
263
+ todoListToSerializedGraph(todos, sessionId),
264
+ "session-todo"
265
+ );
266
+ }
267
+ function projectSessionTasksToKanban(projectRoot, tasks, sessionId) {
268
+ return projectGraph(
269
+ projectRoot,
270
+ sessionId,
271
+ taskFileToSerializedGraph(tasks, sessionId),
272
+ "session-task"
273
+ );
274
+ }
275
+ function projectSessionPlanToKanban(projectRoot, items, sessionId) {
276
+ return projectGraph(
277
+ projectRoot,
278
+ sessionId,
279
+ planFileToSerializedGraph(items, sessionId),
280
+ "session-plan"
281
+ );
282
+ }
283
+ function fireAndForget(work) {
284
+ void work.catch(() => {
285
+ });
286
+ }
287
+ function broadcastTodoUpdate(context, todos) {
288
+ const sessionId = context.session?.id ?? "";
289
+ if (!context.agentId || !sessionId) return;
290
+ const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;
291
+ const mailbox = new GlobalMailbox(projectDir);
292
+ void mailbox.send({
293
+ from: context.agentId,
294
+ to: "*",
295
+ type: "status",
296
+ subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
297
+ body: JSON.stringify({
298
+ kind: "kanban.todos.updated",
299
+ sessionId,
300
+ revision: context.state.revision,
301
+ todos
302
+ }),
303
+ priority: "normal",
304
+ senderSessionId: sessionId,
305
+ ttlMs: 6 * 60 * 60 * 1e3
306
+ }).catch(() => {
307
+ });
308
+ }
309
+ function notifyTodoUpdate(context, todos) {
310
+ const summary = todos.length ? todos.map((todo) => `- [${todo.status}] ${todo.content} (${todo.id})`).join("\n") : "- No active todos remain.";
311
+ const text = `[KANBAN TODO UPDATE]
312
+ Another Kanban agent reassessed the shared board. The canonical todo list is now:
313
+ ${summary}
314
+ Reassess your current plan before continuing; do not rely on the initial todo snapshot.`;
315
+ const state = context.state;
316
+ if (typeof state.appendBlockToLastUserMessage === "function") {
317
+ if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
318
+ }
319
+ if (typeof state.appendMessage === "function") {
320
+ state.appendMessage({ role: "user", content: [{ type: "text", text }] });
321
+ }
322
+ }
323
+ function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
324
+ fireAndForget(projectSessionTodosToKanban(projectRoot, todos, sessionId));
325
+ }
326
+ function mirrorSessionTasksToKanban(projectRoot, tasks, sessionId) {
327
+ fireAndForget(projectSessionTasksToKanban(projectRoot, tasks, sessionId));
328
+ }
329
+ function mirrorSessionPlanToKanban(projectRoot, items, sessionId) {
330
+ fireAndForget(projectSessionPlanToKanban(projectRoot, items, sessionId));
331
+ }
332
+ function attachSessionKanbanMirror(context) {
333
+ const existing = bindings.get(context);
334
+ if (existing) return existing;
335
+ const attachedProjectRoot = context.projectRoot ?? "";
336
+ let registeredSessionId = "";
337
+ const syncActiveSessionRegistration = () => {
338
+ if (!attachedProjectRoot) return;
339
+ const currentSessionId = context.session?.id ?? "";
340
+ if (currentSessionId === registeredSessionId) return;
341
+ if (registeredSessionId) {
342
+ releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
343
+ fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));
344
+ }
345
+ registeredSessionId = currentSessionId;
346
+ if (registeredSessionId) {
347
+ retainActiveSessionBoard(attachedProjectRoot, registeredSessionId);
348
+ }
349
+ };
350
+ syncActiveSessionRegistration();
351
+ let watcher = null;
352
+ let watchedDir = "";
353
+ let timer = null;
354
+ let boardWatcher = null;
355
+ let watchedBoardId = "";
356
+ let boardTimer = null;
357
+ let presenceTimer = null;
358
+ const sessionId = () => context.session?.id ?? "";
359
+ const refreshFiles = async () => {
360
+ const id = sessionId();
361
+ if (!id) return;
362
+ const planPath = context.meta["plan.path"];
363
+ if (typeof planPath === "string" && planPath) {
364
+ const plan = await loadPlan(planPath);
365
+ if (plan) await projectSessionPlanToKanban(context.projectRoot, plan.items, id);
366
+ }
367
+ const taskPath = context.meta["task.path"];
368
+ if (typeof taskPath === "string" && taskPath) {
369
+ const tasks = await loadTasks(taskPath);
370
+ if (tasks) await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);
371
+ }
372
+ };
373
+ const refreshBoard = async () => {
374
+ if (!watchedBoardId) return;
375
+ const board = await getBoard(context.projectRoot, watchedBoardId);
376
+ if (board) applySessionKanbanBoardToTodos(context, board);
377
+ };
378
+ const configureBoardWatcher = async () => {
379
+ const id = sessionId();
380
+ const board = id ? await ensureSessionKanbanBoard(context.projectRoot, id) : null;
381
+ if (!board || board.id === watchedBoardId) return;
382
+ boardWatcher?.close();
383
+ boardWatcher = null;
384
+ watchedBoardId = board.id;
385
+ try {
386
+ const boardFileName = `${board.id}.json`;
387
+ boardWatcher = watch(
388
+ getKanbanDir(context.projectRoot),
389
+ { persistent: false },
390
+ (_event, filename) => {
391
+ if (filename?.toString() !== boardFileName) return;
392
+ if (boardTimer) clearTimeout(boardTimer);
393
+ boardTimer = setTimeout(() => fireAndForget(refreshBoard()), 60);
394
+ }
395
+ );
396
+ const touchPresence = () => touchKanbanPresence(context.projectRoot, board.id, {
397
+ sessionId: id,
398
+ agentId: context.agentId,
399
+ agentName: context.agentName
400
+ });
401
+ fireAndForget(touchPresence());
402
+ if (presenceTimer) clearInterval(presenceTimer);
403
+ presenceTimer = setInterval(() => fireAndForget(touchPresence()), 6e4);
404
+ presenceTimer.unref?.();
405
+ boardWatcher.on("error", () => {
406
+ boardWatcher?.close();
407
+ boardWatcher = null;
408
+ watchedBoardId = "";
409
+ });
410
+ } catch {
411
+ boardWatcher = null;
412
+ watchedBoardId = "";
413
+ }
414
+ };
415
+ const configureWatcher = () => {
416
+ const planPath = context.meta["plan.path"];
417
+ const taskPath = context.meta["task.path"];
418
+ const candidate = typeof planPath === "string" && planPath ? dirname(planPath) : typeof taskPath === "string" && taskPath ? dirname(taskPath) : "";
419
+ if (!candidate || candidate === watchedDir) return;
420
+ watcher?.close();
421
+ watcher = null;
422
+ watchedDir = candidate;
423
+ try {
424
+ watcher = watch(candidate, { persistent: false }, (_event, filename) => {
425
+ const name = filename?.toString();
426
+ const currentPlanPath = context.meta["plan.path"];
427
+ const currentTaskPath = context.meta["task.path"];
428
+ const planName = typeof currentPlanPath === "string" ? basename(currentPlanPath) : "";
429
+ const taskName = typeof currentTaskPath === "string" ? basename(currentTaskPath) : "";
430
+ if (name && name !== planName && name !== taskName) return;
431
+ if (timer) clearTimeout(timer);
432
+ timer = setTimeout(() => fireAndForget(refreshFiles()), 60);
433
+ });
434
+ watcher.on("error", () => watcher?.close());
435
+ } catch {
436
+ watcher = null;
437
+ watchedDir = "";
438
+ }
439
+ };
440
+ const unsubscribe = context.state.onChange((change) => {
441
+ if (change.kind === "todos_replaced" && !suppressedTodoMirrors.has(context)) {
442
+ mirrorSessionTodosToKanban(
443
+ context.projectRoot,
444
+ change.completedSnapshot ?? change.todos,
445
+ sessionId()
446
+ );
447
+ return;
448
+ }
449
+ if (change.kind === "meta_set" && (change.key === "plan.path" || change.key === "task.path")) {
450
+ syncActiveSessionRegistration();
451
+ configureWatcher();
452
+ fireAndForget(ensureSessionKanbanBoard(context.projectRoot, sessionId()));
453
+ fireAndForget(configureBoardWatcher());
454
+ fireAndForget(refreshFiles());
455
+ }
456
+ });
457
+ configureWatcher();
458
+ fireAndForget(configureBoardWatcher());
459
+ const detach = () => {
460
+ unsubscribe();
461
+ if (timer) clearTimeout(timer);
462
+ if (boardTimer) clearTimeout(boardTimer);
463
+ if (presenceTimer) clearInterval(presenceTimer);
464
+ watcher?.close();
465
+ boardWatcher?.close();
466
+ bindings.delete(context);
467
+ if (attachedProjectRoot && registeredSessionId) {
468
+ releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
469
+ fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));
470
+ registeredSessionId = "";
471
+ }
472
+ };
473
+ bindings.set(context, detach);
474
+ return detach;
475
+ }
476
+ async function hydrateSessionKanban(context) {
477
+ const id = context.session?.id ?? "";
478
+ if (!id) return null;
479
+ await cleanupEmptySessionKanbanBoards(context.projectRoot, id);
480
+ let board = await ensureSessionKanbanBoard(context.projectRoot, id);
481
+ if (context.todos.length) {
482
+ board = await projectSessionTodosToKanban(context.projectRoot, context.todos, id);
483
+ }
484
+ const planPath = context.meta["plan.path"];
485
+ if (typeof planPath === "string" && planPath) {
486
+ const plan = await loadPlan(planPath);
487
+ if (plan) board = await projectSessionPlanToKanban(context.projectRoot, plan.items, id);
488
+ }
489
+ const taskPath = context.meta["task.path"];
490
+ if (typeof taskPath === "string" && taskPath) {
491
+ const tasks = await loadTasks(taskPath);
492
+ if (tasks) board = await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);
493
+ }
494
+ return board;
495
+ }
496
+ function sourceStatus(task) {
497
+ if (task.status === "completed") return "completed";
498
+ if (task.status === "in_progress") return "in_progress";
499
+ if (task.status === "review") return "review";
500
+ if (task.status === "blocked") return "blocked";
501
+ if (task.status === "failed") return "failed";
502
+ return "pending";
503
+ }
504
+ function todoStatus(task) {
505
+ const status = sourceStatus(task);
506
+ if (status === "completed") return "completed";
507
+ if (status === "in_progress" || status === "review") return "in_progress";
508
+ return "pending";
509
+ }
510
+ function sessionTodoFromTask(task) {
511
+ return {
512
+ id: task.origin?.taskId ?? task.id,
513
+ content: task.title,
514
+ status: todoStatus(task),
515
+ ...task.description ? { activeForm: task.description } : {}
516
+ };
517
+ }
518
+ function sameTodos(left, right) {
519
+ return left.length === right.length && left.every((todo, index) => {
520
+ const candidate = right[index];
521
+ 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;
522
+ });
523
+ }
524
+ function applySessionKanbanBoardToTodos(context, board) {
525
+ const sessionId = context.session?.id ?? "";
526
+ if (!sessionId || sessionIdFromTags(board.tags) !== sessionId || !isOwnedSessionBoard(board.tags)) {
527
+ return [...context.todos];
528
+ }
529
+ const projectedTodos = board.tasks.filter(
530
+ (task) => task.status !== "archived" && (!task.origin || task.origin.system === "session-todo" || (task.origin.graphId ?? "").startsWith("todo:"))
531
+ ).sort((left, right) => {
532
+ const leftColumn = board.columns.find((column) => column.id === left.columnId)?.order ?? 0;
533
+ const rightColumn = board.columns.find((column) => column.id === right.columnId)?.order ?? 0;
534
+ return leftColumn - rightColumn || left.order - right.order || left.createdAt.localeCompare(right.createdAt);
535
+ }).map(sessionTodoFromTask);
536
+ const allCompleted = projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === "completed");
537
+ const effectiveTodos = allCompleted ? [] : projectedTodos;
538
+ if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];
539
+ suppressedTodoMirrors.add(context);
540
+ try {
541
+ context.state.replaceTodos(projectedTodos);
542
+ } finally {
543
+ suppressedTodoMirrors.delete(context);
544
+ }
545
+ notifyTodoUpdate(context, context.todos);
546
+ broadcastTodoUpdate(context, context.todos);
547
+ return [...context.todos];
548
+ }
549
+ async function applySessionKanbanTaskToSource(context, task, options = {}) {
550
+ const originId = task.origin?.taskId;
551
+ const graphId = task.origin?.graphId ?? "";
552
+ if (!originId) return { source: null };
553
+ if (task.origin?.system === "session-todo" || graphId.startsWith("todo:")) {
554
+ const next = options.remove ? context.todos.filter((todo) => todo.id !== originId) : context.todos.map(
555
+ (todo) => todo.id === originId ? {
556
+ ...todo,
557
+ content: task.title,
558
+ status: sourceStatus(task) === "completed" ? "completed" : sourceStatus(task) === "in_progress" || sourceStatus(task) === "review" ? "in_progress" : "pending"
559
+ } : todo
560
+ );
561
+ suppressedTodoMirrors.add(context);
562
+ try {
563
+ context.state.replaceTodos(next);
564
+ } finally {
565
+ suppressedTodoMirrors.delete(context);
566
+ }
567
+ return { source: "todo", todos: [...context.todos] };
568
+ }
569
+ const id = context.session?.id ?? "";
570
+ if (task.origin?.system === "session-plan" || graphId.startsWith("plan:")) {
571
+ const planPath = context.meta["plan.path"];
572
+ if (typeof planPath !== "string" || !planPath) return { source: "plan" };
573
+ const plan = await mutatePlan(planPath, id, (file) => ({
574
+ ...file,
575
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
576
+ items: options.remove ? file.items.filter((item) => item.id !== originId) : file.items.map(
577
+ (item) => item.id === originId ? {
578
+ ...item,
579
+ title: task.title,
580
+ details: task.description,
581
+ status: task.status === "completed" ? "done" : task.status === "in_progress" || task.status === "review" ? "in_progress" : "open",
582
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
583
+ } : item
584
+ )
585
+ }));
586
+ return { source: "plan", plan };
587
+ }
588
+ if (task.origin?.system === "session-task" || task.origin?.system === "session" || graphId.startsWith("session:")) {
589
+ const taskPath = context.meta["task.path"];
590
+ if (typeof taskPath !== "string" || !taskPath) return { source: "task" };
591
+ const tasks = await mutateTasks(taskPath, id, (file) => ({
592
+ ...file,
593
+ tasks: options.remove ? file.tasks.filter((item) => item.id !== originId) : file.tasks.map(
594
+ (item) => item.id === originId ? {
595
+ ...item,
596
+ title: task.title,
597
+ description: task.description,
598
+ status: sourceStatus(task),
599
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
600
+ } : item
601
+ )
602
+ }));
603
+ return { source: "task", tasks };
604
+ }
605
+ return { source: null };
606
+ }
607
+ export {
608
+ SESSION_KANBAN_COLUMNS,
609
+ applySessionKanbanBoardToTodos,
610
+ applySessionKanbanTaskToSource,
611
+ attachSessionKanbanMirror,
612
+ cleanupEmptySessionKanbanBoards,
613
+ cleanupSessionKanbanBoardIfEmpty,
614
+ ensureSessionKanbanBoard,
615
+ hydrateSessionKanban,
616
+ mirrorSessionPlanToKanban,
617
+ mirrorSessionTasksToKanban,
618
+ mirrorSessionTodosToKanban,
619
+ planFileToSerializedGraph,
620
+ projectSessionPlanToKanban,
621
+ projectSessionTasksToKanban,
622
+ projectSessionTodosToKanban,
623
+ taskFileToSerializedGraph,
624
+ todoListToSerializedGraph
625
+ };
626
+ //# sourceMappingURL=session-kanban.js.map