@wrongstack/tools 0.287.0 → 0.291.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 (102) hide show
  1. package/dist/_fetch-guard.d.ts.map +1 -1
  2. package/dist/_redact-command.d.ts.map +1 -1
  3. package/dist/_spawn-stream.d.ts.map +1 -1
  4. package/dist/audit.js +61 -14
  5. package/dist/audit.js.map +3 -3
  6. package/dist/auto-proceed-loop-guard.d.ts +128 -0
  7. package/dist/auto-proceed-loop-guard.d.ts.map +1 -0
  8. package/dist/auto-proceed-loop-guard.js +46 -0
  9. package/dist/auto-proceed-loop-guard.js.map +7 -0
  10. package/dist/bash.d.ts +1 -1
  11. package/dist/bash.d.ts.map +1 -1
  12. package/dist/bash.js +97 -19
  13. package/dist/bash.js.map +3 -3
  14. package/dist/builtin.d.ts +12 -10
  15. package/dist/builtin.d.ts.map +1 -1
  16. package/dist/builtin.js +1766 -864
  17. package/dist/builtin.js.map +3 -3
  18. package/dist/codebase-index/background-indexer.d.ts +1 -1
  19. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  20. package/dist/codebase-index/bm25.d.ts +1 -2
  21. package/dist/codebase-index/bm25.d.ts.map +1 -1
  22. package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
  23. package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
  24. package/dist/codebase-index/codebase-stats-tool.d.ts +7 -0
  25. package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
  26. package/dist/codebase-index/index-service.d.ts +11 -1
  27. package/dist/codebase-index/index-service.d.ts.map +1 -1
  28. package/dist/codebase-index/index.d.ts +2 -1
  29. package/dist/codebase-index/index.d.ts.map +1 -1
  30. package/dist/codebase-index/index.js +888 -254
  31. package/dist/codebase-index/index.js.map +3 -3
  32. package/dist/codebase-index/indexer.d.ts.map +1 -1
  33. package/dist/codebase-index/refs-extractor.d.ts +2 -17
  34. package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
  35. package/dist/codebase-index/schema.d.ts +43 -1
  36. package/dist/codebase-index/schema.d.ts.map +1 -1
  37. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  38. package/dist/codebase-index/worker-protocol.d.ts +1 -0
  39. package/dist/codebase-index/worker-protocol.d.ts.map +1 -1
  40. package/dist/codebase-index/worker.js +738 -212
  41. package/dist/codebase-index/worker.js.map +3 -3
  42. package/dist/codebase-index/writer.d.ts +60 -1
  43. package/dist/codebase-index/writer.d.ts.map +1 -1
  44. package/dist/e2e.js +477 -0
  45. package/dist/e2e.js.map +7 -0
  46. package/dist/exec.d.ts +1 -1
  47. package/dist/exec.d.ts.map +1 -1
  48. package/dist/exec.js +60 -5
  49. package/dist/exec.js.map +3 -3
  50. package/dist/fetch.js +2 -2
  51. package/dist/fetch.js.map +2 -2
  52. package/dist/format.js +61 -14
  53. package/dist/format.js.map +3 -3
  54. package/dist/glob.js +2 -2
  55. package/dist/glob.js.map +1 -1
  56. package/dist/grep.d.ts.map +1 -1
  57. package/dist/grep.js +2 -2
  58. package/dist/grep.js.map +2 -2
  59. package/dist/index.d.ts +3 -2
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +2001 -873
  62. package/dist/index.js.map +3 -3
  63. package/dist/install.js +61 -14
  64. package/dist/install.js.map +3 -3
  65. package/dist/kanban.d.ts +21 -2
  66. package/dist/kanban.d.ts.map +1 -1
  67. package/dist/kanban.js +713 -572
  68. package/dist/kanban.js.map +3 -3
  69. package/dist/languages/detect.d.ts.map +1 -1
  70. package/dist/languages/index.js +61 -14
  71. package/dist/languages/index.js.map +3 -3
  72. package/dist/languages/types.d.ts +7 -0
  73. package/dist/languages/types.d.ts.map +1 -1
  74. package/dist/lint.js +61 -14
  75. package/dist/lint.js.map +3 -3
  76. package/dist/outdated.js +61 -14
  77. package/dist/outdated.js.map +3 -3
  78. package/dist/pack.js +1763 -861
  79. package/dist/pack.js.map +3 -3
  80. package/dist/plan.js +4 -0
  81. package/dist/plan.js.map +2 -2
  82. package/dist/process-registry.js +12 -5
  83. package/dist/process-registry.js.map +2 -2
  84. package/dist/read.d.ts.map +1 -1
  85. package/dist/read.js.map +2 -2
  86. package/dist/search.js +2 -2
  87. package/dist/search.js.map +2 -2
  88. package/dist/session-kanban.d.ts +9 -0
  89. package/dist/session-kanban.d.ts.map +1 -1
  90. package/dist/session-kanban.js +197 -3
  91. package/dist/session-kanban.js.map +2 -2
  92. package/dist/task.js +4 -0
  93. package/dist/task.js.map +2 -2
  94. package/dist/test.js +61 -14
  95. package/dist/test.js.map +3 -3
  96. package/dist/todo.js +4 -0
  97. package/dist/todo.js.map +2 -2
  98. package/dist/tree.js +2 -2
  99. package/dist/tree.js.map +1 -1
  100. package/dist/typecheck.js +61 -14
  101. package/dist/typecheck.js.map +3 -3
  102. package/package.json +13 -5
@@ -3,17 +3,21 @@ import { watch } from "node:fs";
3
3
  import { basename, dirname } from "node:path";
4
4
  import {
5
5
  deserializeTaskGraph,
6
+ GlobalMailbox,
6
7
  loadPlan,
7
8
  loadTasks,
8
9
  mutatePlan,
9
10
  mutateTasks
10
11
  } from "@wrongstack/core";
12
+ import { resolveWstackPaths } from "@wrongstack/core/utils";
11
13
  import {
12
14
  createBoard,
13
15
  getBoard,
16
+ getKanbanDir,
14
17
  listBoards,
15
18
  removeBoard,
16
19
  syncBoardFromTaskGraph,
20
+ touchKanbanPresence,
17
21
  updateBoard
18
22
  } from "@wrongstack/kanban";
19
23
  var SESSION_BOARD_TAG = "session-work";
@@ -26,12 +30,17 @@ var SESSION_KANBAN_COLUMNS = [
26
30
  ];
27
31
  var boardQueue = /* @__PURE__ */ new Map();
28
32
  var boardEnsures = /* @__PURE__ */ new Map();
33
+ var pendingMirrors = /* @__PURE__ */ new Map();
34
+ var activeMirrors = /* @__PURE__ */ new Set();
29
35
  var bindings = /* @__PURE__ */ new WeakMap();
30
36
  var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
31
37
  var activeSessionBoards = /* @__PURE__ */ new Map();
32
38
  function boardKey(projectRoot, sessionId) {
33
39
  return `${projectRoot}\0${sessionId}`;
34
40
  }
41
+ function mirrorKey(projectRoot, sessionId, sourceSystem) {
42
+ return `${boardKey(projectRoot, sessionId)}\0${sourceSystem}`;
43
+ }
35
44
  function sessionTag(sessionId) {
36
45
  return `session:${sessionId}`;
37
46
  }
@@ -167,6 +176,43 @@ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
167
176
  return result?.board ?? null;
168
177
  });
169
178
  }
179
+ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
180
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return;
181
+ const key = mirrorKey(projectRoot, sessionId, sourceSystem);
182
+ pendingMirrors.set(key, { projectRoot, sessionId, graph, sourceSystem });
183
+ if (activeMirrors.has(key)) return;
184
+ activeMirrors.add(key);
185
+ void (async () => {
186
+ try {
187
+ for (; ; ) {
188
+ const pending = pendingMirrors.get(key);
189
+ if (!pending) break;
190
+ pendingMirrors.delete(key);
191
+ try {
192
+ await projectGraph(
193
+ pending.projectRoot,
194
+ pending.sessionId,
195
+ pending.graph,
196
+ pending.sourceSystem
197
+ );
198
+ } catch {
199
+ }
200
+ }
201
+ } finally {
202
+ activeMirrors.delete(key);
203
+ const pending = pendingMirrors.get(key);
204
+ if (pending) {
205
+ pendingMirrors.delete(key);
206
+ queueLatestMirror(
207
+ pending.projectRoot,
208
+ pending.sessionId,
209
+ pending.graph,
210
+ pending.sourceSystem
211
+ );
212
+ }
213
+ }
214
+ })();
215
+ }
170
216
  function todoListToSerializedGraph(todos, sessionId) {
171
217
  const nodes = todos.map((todo, index) => ({
172
218
  id: todo.id,
@@ -280,14 +326,65 @@ function fireAndForget(work) {
280
326
  void work.catch(() => {
281
327
  });
282
328
  }
329
+ function broadcastTodoUpdate(context, todos) {
330
+ const sessionId = context.session?.id ?? "";
331
+ if (!context.agentId || !sessionId) return;
332
+ const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;
333
+ const mailbox = new GlobalMailbox(projectDir);
334
+ void mailbox.send({
335
+ from: context.agentId,
336
+ to: "*",
337
+ type: "status",
338
+ subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
339
+ body: JSON.stringify({
340
+ kind: "kanban.todos.updated",
341
+ sessionId,
342
+ revision: context.state.revision,
343
+ todos
344
+ }),
345
+ priority: "normal",
346
+ senderSessionId: sessionId,
347
+ ttlMs: 6 * 60 * 60 * 1e3
348
+ }).catch(() => {
349
+ });
350
+ }
351
+ function notifyTodoUpdate(context, todos) {
352
+ const summary = todos.length ? todos.map((todo) => `- [${todo.status}] ${todo.content} (${todo.id})`).join("\n") : "- No active todos remain.";
353
+ const text = `[KANBAN TODO UPDATE]
354
+ Another Kanban agent reassessed the shared board. The canonical todo list is now:
355
+ ${summary}
356
+ Reassess your current plan before continuing; do not rely on the initial todo snapshot.`;
357
+ const state = context.state;
358
+ if (typeof state.appendBlockToLastUserMessage === "function") {
359
+ if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
360
+ }
361
+ if (typeof state.appendMessage === "function") {
362
+ state.appendMessage({ role: "user", content: [{ type: "text", text }] });
363
+ }
364
+ }
283
365
  function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
284
- fireAndForget(projectSessionTodosToKanban(projectRoot, todos, sessionId));
366
+ queueLatestMirror(
367
+ projectRoot,
368
+ sessionId,
369
+ todoListToSerializedGraph(todos, sessionId),
370
+ "session-todo"
371
+ );
285
372
  }
286
373
  function mirrorSessionTasksToKanban(projectRoot, tasks, sessionId) {
287
- fireAndForget(projectSessionTasksToKanban(projectRoot, tasks, sessionId));
374
+ queueLatestMirror(
375
+ projectRoot,
376
+ sessionId,
377
+ taskFileToSerializedGraph(tasks, sessionId),
378
+ "session-task"
379
+ );
288
380
  }
289
381
  function mirrorSessionPlanToKanban(projectRoot, items, sessionId) {
290
- fireAndForget(projectSessionPlanToKanban(projectRoot, items, sessionId));
382
+ queueLatestMirror(
383
+ projectRoot,
384
+ sessionId,
385
+ planFileToSerializedGraph(items, sessionId),
386
+ "session-plan"
387
+ );
291
388
  }
292
389
  function attachSessionKanbanMirror(context) {
293
390
  const existing = bindings.get(context);
@@ -311,6 +408,10 @@ function attachSessionKanbanMirror(context) {
311
408
  let watcher = null;
312
409
  let watchedDir = "";
313
410
  let timer = null;
411
+ let boardWatcher = null;
412
+ let watchedBoardId = "";
413
+ let boardTimer = null;
414
+ let presenceTimer = null;
314
415
  const sessionId = () => context.session?.id ?? "";
315
416
  const refreshFiles = async () => {
316
417
  const id = sessionId();
@@ -326,6 +427,48 @@ function attachSessionKanbanMirror(context) {
326
427
  if (tasks) await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);
327
428
  }
328
429
  };
430
+ const refreshBoard = async () => {
431
+ if (!watchedBoardId) return;
432
+ const board = await getBoard(context.projectRoot, watchedBoardId);
433
+ if (board) applySessionKanbanBoardToTodos(context, board);
434
+ };
435
+ const configureBoardWatcher = async () => {
436
+ const id = sessionId();
437
+ const board = id ? await ensureSessionKanbanBoard(context.projectRoot, id) : null;
438
+ if (!board || board.id === watchedBoardId) return;
439
+ boardWatcher?.close();
440
+ boardWatcher = null;
441
+ watchedBoardId = board.id;
442
+ try {
443
+ const boardFileName = `${board.id}.json`;
444
+ boardWatcher = watch(
445
+ getKanbanDir(context.projectRoot),
446
+ { persistent: false },
447
+ (_event, filename) => {
448
+ if (filename?.toString() !== boardFileName) return;
449
+ if (boardTimer) clearTimeout(boardTimer);
450
+ boardTimer = setTimeout(() => fireAndForget(refreshBoard()), 60);
451
+ }
452
+ );
453
+ const touchPresence = () => touchKanbanPresence(context.projectRoot, board.id, {
454
+ sessionId: id,
455
+ agentId: context.agentId,
456
+ agentName: context.agentName
457
+ });
458
+ fireAndForget(touchPresence());
459
+ if (presenceTimer) clearInterval(presenceTimer);
460
+ presenceTimer = setInterval(() => fireAndForget(touchPresence()), 6e4);
461
+ presenceTimer.unref?.();
462
+ boardWatcher.on("error", () => {
463
+ boardWatcher?.close();
464
+ boardWatcher = null;
465
+ watchedBoardId = "";
466
+ });
467
+ } catch {
468
+ boardWatcher = null;
469
+ watchedBoardId = "";
470
+ }
471
+ };
329
472
  const configureWatcher = () => {
330
473
  const planPath = context.meta["plan.path"];
331
474
  const taskPath = context.meta["task.path"];
@@ -364,14 +507,19 @@ function attachSessionKanbanMirror(context) {
364
507
  syncActiveSessionRegistration();
365
508
  configureWatcher();
366
509
  fireAndForget(ensureSessionKanbanBoard(context.projectRoot, sessionId()));
510
+ fireAndForget(configureBoardWatcher());
367
511
  fireAndForget(refreshFiles());
368
512
  }
369
513
  });
370
514
  configureWatcher();
515
+ fireAndForget(configureBoardWatcher());
371
516
  const detach = () => {
372
517
  unsubscribe();
373
518
  if (timer) clearTimeout(timer);
519
+ if (boardTimer) clearTimeout(boardTimer);
520
+ if (presenceTimer) clearInterval(presenceTimer);
374
521
  watcher?.close();
522
+ boardWatcher?.close();
375
523
  bindings.delete(context);
376
524
  if (attachedProjectRoot && registeredSessionId) {
377
525
  releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
@@ -410,6 +558,51 @@ function sourceStatus(task) {
410
558
  if (task.status === "failed") return "failed";
411
559
  return "pending";
412
560
  }
561
+ function todoStatus(task) {
562
+ const status = sourceStatus(task);
563
+ if (status === "completed") return "completed";
564
+ if (status === "in_progress" || status === "review") return "in_progress";
565
+ return "pending";
566
+ }
567
+ function sessionTodoFromTask(task) {
568
+ return {
569
+ id: task.origin?.taskId ?? task.id,
570
+ content: task.title,
571
+ status: todoStatus(task),
572
+ ...task.description ? { activeForm: task.description } : {}
573
+ };
574
+ }
575
+ function sameTodos(left, right) {
576
+ return left.length === right.length && left.every((todo, index) => {
577
+ const candidate = right[index];
578
+ 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;
579
+ });
580
+ }
581
+ function applySessionKanbanBoardToTodos(context, board) {
582
+ const sessionId = context.session?.id ?? "";
583
+ if (!sessionId || sessionIdFromTags(board.tags) !== sessionId || !isOwnedSessionBoard(board.tags)) {
584
+ return [...context.todos];
585
+ }
586
+ const projectedTodos = board.tasks.filter(
587
+ (task) => task.status !== "archived" && (!task.origin || task.origin.system === "session-todo" || (task.origin.graphId ?? "").startsWith("todo:"))
588
+ ).sort((left, right) => {
589
+ const leftColumn = board.columns.find((column) => column.id === left.columnId)?.order ?? 0;
590
+ const rightColumn = board.columns.find((column) => column.id === right.columnId)?.order ?? 0;
591
+ return leftColumn - rightColumn || left.order - right.order || left.createdAt.localeCompare(right.createdAt);
592
+ }).map(sessionTodoFromTask);
593
+ const allCompleted = projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === "completed");
594
+ const effectiveTodos = allCompleted ? [] : projectedTodos;
595
+ if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];
596
+ suppressedTodoMirrors.add(context);
597
+ try {
598
+ context.state.replaceTodos(projectedTodos);
599
+ } finally {
600
+ suppressedTodoMirrors.delete(context);
601
+ }
602
+ notifyTodoUpdate(context, context.todos);
603
+ broadcastTodoUpdate(context, context.todos);
604
+ return [...context.todos];
605
+ }
413
606
  async function applySessionKanbanTaskToSource(context, task, options = {}) {
414
607
  const originId = task.origin?.taskId;
415
608
  const graphId = task.origin?.graphId ?? "";
@@ -470,6 +663,7 @@ async function applySessionKanbanTaskToSource(context, task, options = {}) {
470
663
  }
471
664
  export {
472
665
  SESSION_KANBAN_COLUMNS,
666
+ applySessionKanbanBoardToTodos,
473
667
  applySessionKanbanTaskToSource,
474
668
  attachSessionKanbanMirror,
475
669
  cleanupEmptySessionKanbanBoards,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/session-kanban.ts"],
4
- "sourcesContent": ["import { type FSWatcher, watch } from 'node:fs';\nimport { basename, dirname } from 'node:path';\nimport {\n type Context,\n deserializeTaskGraph,\n loadPlan,\n loadTasks,\n mutatePlan,\n mutateTasks,\n type PlanFile,\n type PlanItem,\n type SerializedTaskGraph,\n type TaskFile,\n type TaskItem,\n type TaskStatus,\n type TodoItem,\n} from '@wrongstack/core';\nimport {\n createBoard,\n getBoard,\n type KanbanBoard,\n type KanbanColumn,\n type KanbanTask,\n listBoards,\n removeBoard,\n syncBoardFromTaskGraph,\n updateBoard,\n} from '@wrongstack/kanban';\n\nconst SESSION_BOARD_TAG = 'session-work';\nconst MIRROR_DISABLED_ENV = 'WRONGSTACK_KANBAN_TASK_MIRROR';\n\n/** The canonical workflow shared by WebUI and TUI session boards. */\nexport const SESSION_KANBAN_COLUMNS: KanbanColumn[] = [\n { id: 'todo', title: 'Todo', order: 0, wipLimit: 0, color: '#2563eb' },\n { id: 'in-progress', title: 'Running', order: 1, wipLimit: 1, color: '#d97706' },\n { id: 'review', title: 'Preview', order: 2, wipLimit: 0, color: '#7c3aed' },\n { id: 'done', title: 'Done', order: 3, wipLimit: 0, color: '#16a34a' },\n];\n\nconst boardQueue = new Map<string, Promise<void>>();\nconst boardEnsures = new Map<string, Promise<KanbanBoard>>();\nconst bindings = new WeakMap<Context, () => void>();\nconst suppressedTodoMirrors = new WeakSet<Context>();\nconst activeSessionBoards = new Map<string, number>();\n\nfunction boardKey(projectRoot: string, sessionId: string): string {\n return `${projectRoot}\\0${sessionId}`;\n}\n\nfunction sessionTag(sessionId: string): string {\n return `session:${sessionId}`;\n}\n\nfunction sessionBoardTitle(sessionId: string): string {\n const leaf = sessionId.split(/[\\\\/]/).filter(Boolean).pop() ?? sessionId;\n return `Session ${leaf.slice(0, 12)}`;\n}\n\nfunction sessionBoardTags(sessionId: string): string[] {\n return ['session', SESSION_BOARD_TAG, sessionTag(sessionId)];\n}\n\nfunction sessionIdFromTags(tags: readonly string[] | undefined): string | null {\n const tag = tags?.find((candidate) => candidate.startsWith('session:'));\n return tag?.slice('session:'.length) || null;\n}\n\nfunction isOwnedSessionBoard(tags: readonly string[] | undefined): boolean {\n return Boolean(tags?.includes(SESSION_BOARD_TAG) && sessionIdFromTags(tags));\n}\n\nfunction retainActiveSessionBoard(projectRoot: string, sessionId: string): void {\n const key = boardKey(projectRoot, sessionId);\n activeSessionBoards.set(key, (activeSessionBoards.get(key) ?? 0) + 1);\n}\n\nfunction releaseActiveSessionBoard(projectRoot: string, sessionId: string): void {\n const key = boardKey(projectRoot, sessionId);\n const remaining = (activeSessionBoards.get(key) ?? 0) - 1;\n if (remaining > 0) activeSessionBoards.set(key, remaining);\n else activeSessionBoards.delete(key);\n}\n\nfunction isSessionBoardActive(projectRoot: string, sessionId: string): boolean {\n return (activeSessionBoards.get(boardKey(projectRoot, sessionId)) ?? 0) > 0;\n}\n\nfunction sameColumns(columns: readonly KanbanColumn[]): boolean {\n return (\n columns.length === SESSION_KANBAN_COLUMNS.length &&\n columns.every((column, index) => column.id === SESSION_KANBAN_COLUMNS[index]?.id)\n );\n}\n\n/** Create (or migrate) the single Kanban board owned by a session. */\nexport async function ensureSessionKanbanBoard(\n projectRoot: string | undefined,\n sessionId: string,\n): Promise<KanbanBoard | null> {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return null;\n const key = boardKey(projectRoot, sessionId);\n const inFlight = boardEnsures.get(key);\n if (inFlight) return inFlight;\n\n const promise = (async () => {\n const summary = (await listBoards(projectRoot)).find((board) =>\n board.tags?.includes(sessionTag(sessionId)),\n );\n let board = summary ? await getBoard(projectRoot, summary.id) : null;\n if (!board) {\n return createBoard(projectRoot, {\n title: sessionBoardTitle(sessionId),\n description: 'Live session work: todos, tasks, and plan items.',\n tags: sessionBoardTags(sessionId),\n columns: SESSION_KANBAN_COLUMNS,\n generatedBy: `session-kanban:${sessionId}`,\n });\n }\n\n // Boards produced by the older task/plan mirrors used the generic five\n // columns. Migrate only session-owned boards; updateBoard reconciles cards\n // from Backlog into Todo while preserving Running/Preview/Done cards.\n if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {\n board =\n (await updateBoard(projectRoot, board.id, {\n title: sessionBoardTitle(sessionId),\n description: 'Live session work: todos, tasks, and plan items.',\n tags: [...new Set([...(board.tags ?? []), ...sessionBoardTags(sessionId)])],\n columns: SESSION_KANBAN_COLUMNS,\n })) ?? board;\n }\n return board;\n })();\n\n boardEnsures.set(key, promise);\n try {\n return await promise;\n } finally {\n boardEnsures.delete(key);\n }\n}\n\nfunction enqueueBoardWork<T>(\n projectRoot: string,\n sessionId: string,\n work: () => Promise<T>,\n): Promise<T> {\n const key = boardKey(projectRoot, sessionId);\n const previous = boardQueue.get(key) ?? Promise.resolve();\n const result = previous.catch(() => undefined).then(work);\n const tail = result.then(\n () => undefined,\n () => undefined,\n );\n boardQueue.set(key, tail);\n void tail.then(() => {\n if (boardQueue.get(key) === tail) boardQueue.delete(key);\n });\n return result;\n}\n\nasync function removeEmptySessionBoard(\n projectRoot: string,\n boardId: string,\n sessionId: string,\n): Promise<string | null> {\n return enqueueBoardWork(projectRoot, sessionId, async () => {\n if (isSessionBoardActive(projectRoot, sessionId)) return null;\n const board = await getBoard(projectRoot, boardId);\n if (!board || board.tasks.length > 0 || !isOwnedSessionBoard(board.tags)) return null;\n if (sessionIdFromTags(board.tags) !== sessionId) return null;\n return (await removeBoard(projectRoot, board.id)) ? board.id : null;\n });\n}\n\n/** Remove a particular inactive session's system-owned board when it has no cards. */\nexport async function cleanupSessionKanbanBoardIfEmpty(\n projectRoot: string | undefined,\n sessionId: string,\n): Promise<string[]> {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return [];\n if (isSessionBoardActive(projectRoot, sessionId)) return [];\n const candidates = (await listBoards(projectRoot)).filter(\n (board) =>\n board.taskCount === 0 &&\n isOwnedSessionBoard(board.tags) &&\n sessionIdFromTags(board.tags) === sessionId,\n );\n const removed = await Promise.all(\n candidates.map((board) => removeEmptySessionBoard(projectRoot, board.id, sessionId)),\n );\n return removed.filter((boardId): boardId is string => Boolean(boardId));\n}\n\n/** Prune stale empty session boards while preserving manual and live boards. */\nexport async function cleanupEmptySessionKanbanBoards(\n projectRoot: string | undefined,\n activeSessionId = '',\n): Promise<string[]> {\n if (!projectRoot || process.env[MIRROR_DISABLED_ENV] === '0') return [];\n const candidates = (await listBoards(projectRoot)).flatMap((board) => {\n const ownerSessionId = sessionIdFromTags(board.tags);\n return board.taskCount === 0 &&\n isOwnedSessionBoard(board.tags) &&\n ownerSessionId &&\n ownerSessionId !== activeSessionId &&\n !isSessionBoardActive(projectRoot, ownerSessionId)\n ? [{ boardId: board.id, sessionId: ownerSessionId }]\n : [];\n });\n const removed = await Promise.all(\n candidates.map(({ boardId, sessionId }) =>\n removeEmptySessionBoard(projectRoot, boardId, sessionId),\n ),\n );\n return removed.filter((boardId): boardId is string => Boolean(boardId));\n}\n\nasync function projectGraph(\n projectRoot: string | undefined,\n sessionId: string,\n graph: SerializedTaskGraph,\n sourceSystem: 'session-todo' | 'session-task' | 'session-plan',\n): Promise<KanbanBoard | null> {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return null;\n return enqueueBoardWork(projectRoot, sessionId, async () => {\n const board = await ensureSessionKanbanBoard(projectRoot, sessionId);\n if (!board) return null;\n const result = await syncBoardFromTaskGraph(\n projectRoot,\n board.id,\n deserializeTaskGraph(graph),\n {\n sourceSystem,\n tags: [...new Set([...(board.tags ?? []), ...sessionBoardTags(sessionId)])],\n archiveMissingTasks: true,\n includeCompletedTasks: true,\n },\n );\n return result?.board ?? null;\n });\n}\n\nexport function todoListToSerializedGraph(\n todos: readonly TodoItem[],\n sessionId: string,\n): SerializedTaskGraph {\n const nodes = todos.map((todo, index) => ({\n id: todo.id,\n title: todo.content,\n description: todo.activeForm ?? '',\n type: 'chore' as const,\n priority: 'medium' as const,\n status: todo.status,\n createdAt: index,\n updatedAt: index,\n }));\n return {\n id: `todo:${sessionId}`,\n specId: `todo:${sessionId}`,\n title: 'Session todos',\n nodes,\n edges: [],\n rootNodes: nodes.map((node) => node.id),\n createdAt: 0,\n updatedAt: 0,\n };\n}\n\nexport function taskFileToSerializedGraph(\n tasks: readonly TaskItem[],\n sessionId: string,\n): SerializedTaskGraph {\n const ids = new Set(tasks.map((task) => task.id));\n const nodes = tasks.map((task, index) => ({\n id: task.id,\n title: task.title,\n description: task.description ?? '',\n type: task.type,\n priority: task.priority,\n status: task.status,\n ...(task.assignee ? { assignee: task.assignee } : {}),\n ...(task.estimateHours !== undefined ? { estimateHours: task.estimateHours } : {}),\n createdAt: index,\n updatedAt: index,\n }));\n const edges = tasks.flatMap((task) =>\n (task.dependsOn ?? [])\n .filter((dependency) => ids.has(dependency))\n .map((dependency) => ({\n id: `${dependency}->${task.id}`,\n from: dependency,\n to: task.id,\n type: 'depends_on' as const,\n })),\n );\n const hasIncoming = new Set(edges.map((edge) => edge.to));\n const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);\n return {\n // Keep the historical graph id so existing mirrored task cards are reused.\n id: `session:${sessionId}`,\n specId: `session:${sessionId}`,\n title: 'Session tasks',\n nodes,\n edges,\n rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],\n createdAt: 0,\n updatedAt: 0,\n };\n}\n\nconst PLAN_STATUS_TO_TASK: Record<PlanItem['status'], TaskStatus> = {\n open: 'pending',\n in_progress: 'in_progress',\n done: 'completed',\n};\n\nexport function planFileToSerializedGraph(\n items: readonly PlanItem[],\n sessionId: string,\n): SerializedTaskGraph {\n const nodes = items.map((item, index) => ({\n id: item.id,\n title: item.title,\n description: item.details ?? '',\n type: 'chore' as const,\n priority: 'medium' as const,\n status: PLAN_STATUS_TO_TASK[item.status],\n createdAt: index,\n updatedAt: index,\n }));\n return {\n id: `plan:${sessionId}`,\n specId: `plan:${sessionId}`,\n title: 'Session plan',\n nodes,\n edges: [],\n rootNodes: nodes.map((node) => node.id),\n createdAt: 0,\n updatedAt: 0,\n };\n}\n\nexport function projectSessionTodosToKanban(\n projectRoot: string | undefined,\n todos: readonly TodoItem[],\n sessionId: string,\n): Promise<KanbanBoard | null> {\n return projectGraph(\n projectRoot,\n sessionId,\n todoListToSerializedGraph(todos, sessionId),\n 'session-todo',\n );\n}\n\nexport function projectSessionTasksToKanban(\n projectRoot: string | undefined,\n tasks: readonly TaskItem[],\n sessionId: string,\n): Promise<KanbanBoard | null> {\n return projectGraph(\n projectRoot,\n sessionId,\n taskFileToSerializedGraph(tasks, sessionId),\n 'session-task',\n );\n}\n\nexport function projectSessionPlanToKanban(\n projectRoot: string | undefined,\n items: readonly PlanItem[],\n sessionId: string,\n): Promise<KanbanBoard | null> {\n return projectGraph(\n projectRoot,\n sessionId,\n planFileToSerializedGraph(items, sessionId),\n 'session-plan',\n );\n}\n\nfunction fireAndForget(work: Promise<unknown>): void {\n void work.catch(() => {\n // The session files/state remain recoverable if the observational board\n // cannot be written; the next mutation or file watcher retries the mirror.\n });\n}\n\nexport function mirrorSessionTodosToKanban(\n projectRoot: string | undefined,\n todos: readonly TodoItem[],\n sessionId: string,\n): void {\n fireAndForget(projectSessionTodosToKanban(projectRoot, todos, sessionId));\n}\n\nexport function mirrorSessionTasksToKanban(\n projectRoot: string | undefined,\n tasks: readonly TaskItem[],\n sessionId: string,\n): void {\n fireAndForget(projectSessionTasksToKanban(projectRoot, tasks, sessionId));\n}\n\nexport function mirrorSessionPlanToKanban(\n projectRoot: string | undefined,\n items: readonly PlanItem[],\n sessionId: string,\n): void {\n fireAndForget(projectSessionPlanToKanban(projectRoot, items, sessionId));\n}\n\n/**\n * Bind all live session work paths to Kanban. Todo changes are observed from\n * ConversationState; plan/task sidecars are watched so slash commands, WebUI,\n * plugins, and tools all pass through the same board.\n */\nexport function attachSessionKanbanMirror(context: Context): () => void {\n const existing = bindings.get(context);\n if (existing) return existing;\n\n const attachedProjectRoot = context.projectRoot ?? '';\n let registeredSessionId = '';\n const syncActiveSessionRegistration = () => {\n if (!attachedProjectRoot) return;\n const currentSessionId = context.session?.id ?? '';\n if (currentSessionId === registeredSessionId) return;\n if (registeredSessionId) {\n releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);\n fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));\n }\n registeredSessionId = currentSessionId;\n if (registeredSessionId) {\n retainActiveSessionBoard(attachedProjectRoot, registeredSessionId);\n }\n };\n syncActiveSessionRegistration();\n\n let watcher: FSWatcher | null = null;\n let watchedDir = '';\n let timer: NodeJS.Timeout | null = null;\n\n const sessionId = () => context.session?.id ?? '';\n const refreshFiles = async () => {\n const id = sessionId();\n if (!id) return;\n const planPath = context.meta['plan.path'];\n if (typeof planPath === 'string' && planPath) {\n const plan = await loadPlan(planPath);\n if (plan) await projectSessionPlanToKanban(context.projectRoot, plan.items, id);\n }\n const taskPath = context.meta['task.path'];\n if (typeof taskPath === 'string' && taskPath) {\n const tasks = await loadTasks(taskPath);\n if (tasks) await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);\n }\n };\n\n const configureWatcher = () => {\n const planPath = context.meta['plan.path'];\n const taskPath = context.meta['task.path'];\n const candidate =\n typeof planPath === 'string' && planPath\n ? dirname(planPath)\n : typeof taskPath === 'string' && taskPath\n ? dirname(taskPath)\n : '';\n if (!candidate || candidate === watchedDir) return;\n watcher?.close();\n watcher = null;\n watchedDir = candidate;\n try {\n watcher = watch(candidate, { persistent: false }, (_event, filename) => {\n const name = filename?.toString();\n const currentPlanPath = context.meta['plan.path'];\n const currentTaskPath = context.meta['task.path'];\n const planName = typeof currentPlanPath === 'string' ? basename(currentPlanPath) : '';\n const taskName = typeof currentTaskPath === 'string' ? basename(currentTaskPath) : '';\n if (name && name !== planName && name !== taskName) return;\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => fireAndForget(refreshFiles()), 60);\n });\n watcher.on('error', () => watcher?.close());\n } catch {\n // The session directory can briefly disappear during project/session\n // switches; the next meta update re-attempts the binding.\n watcher = null;\n watchedDir = '';\n }\n };\n\n const unsubscribe = context.state.onChange((change) => {\n if (change.kind === 'todos_replaced' && !suppressedTodoMirrors.has(context)) {\n // ConversationState auto-clears an all-done tactical list. Project the\n // pre-clear completion snapshot so every card reaches Done atomically.\n mirrorSessionTodosToKanban(\n context.projectRoot,\n change.completedSnapshot ?? change.todos,\n sessionId(),\n );\n return;\n }\n if (change.kind === 'meta_set' && (change.key === 'plan.path' || change.key === 'task.path')) {\n syncActiveSessionRegistration();\n configureWatcher();\n fireAndForget(ensureSessionKanbanBoard(context.projectRoot, sessionId()));\n fireAndForget(refreshFiles());\n }\n });\n\n configureWatcher();\n\n const detach = () => {\n unsubscribe();\n if (timer) clearTimeout(timer);\n watcher?.close();\n bindings.delete(context);\n if (attachedProjectRoot && registeredSessionId) {\n releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);\n fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));\n registeredSessionId = '';\n }\n };\n bindings.set(context, detach);\n return detach;\n}\n\n/** Fully hydrate a session board before a host announces the session as ready. */\nexport async function hydrateSessionKanban(context: Context): Promise<KanbanBoard | null> {\n const id = context.session?.id ?? '';\n if (!id) return null;\n await cleanupEmptySessionKanbanBoards(context.projectRoot, id);\n let board = await ensureSessionKanbanBoard(context.projectRoot, id);\n if (context.todos.length) {\n board = await projectSessionTodosToKanban(context.projectRoot, context.todos, id);\n }\n const planPath = context.meta['plan.path'];\n if (typeof planPath === 'string' && planPath) {\n const plan = await loadPlan(planPath);\n if (plan) board = await projectSessionPlanToKanban(context.projectRoot, plan.items, id);\n }\n const taskPath = context.meta['task.path'];\n if (typeof taskPath === 'string' && taskPath) {\n const tasks = await loadTasks(taskPath);\n if (tasks) board = await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);\n }\n return board;\n}\n\nexport interface SessionKanbanSourceUpdate {\n source: 'todo' | 'task' | 'plan' | null;\n todos?: TodoItem[] | undefined;\n tasks?: TaskFile | undefined;\n plan?: PlanFile | undefined;\n}\n\nfunction sourceStatus(task: KanbanTask): TaskStatus {\n if (task.status === 'completed') return 'completed';\n if (task.status === 'in_progress') return 'in_progress';\n if (task.status === 'review') return 'review';\n if (task.status === 'blocked') return 'blocked';\n if (task.status === 'failed') return 'failed';\n return 'pending';\n}\n\n/** Reflect a TUI/WebUI Kanban card edit back to its originating work list. */\nexport async function applySessionKanbanTaskToSource(\n context: Context,\n task: KanbanTask,\n options: { remove?: boolean | undefined } = {},\n): Promise<SessionKanbanSourceUpdate> {\n const originId = task.origin?.taskId;\n const graphId = task.origin?.graphId ?? '';\n if (!originId) return { source: null };\n\n if (task.origin?.system === 'session-todo' || graphId.startsWith('todo:')) {\n const next = options.remove\n ? context.todos.filter((todo) => todo.id !== originId)\n : context.todos.map((todo) =>\n todo.id === originId\n ? {\n ...todo,\n content: task.title,\n status:\n sourceStatus(task) === 'completed'\n ? ('completed' as const)\n : sourceStatus(task) === 'in_progress' || sourceStatus(task) === 'review'\n ? ('in_progress' as const)\n : ('pending' as const),\n }\n : todo,\n );\n suppressedTodoMirrors.add(context);\n try {\n context.state.replaceTodos(next);\n } finally {\n suppressedTodoMirrors.delete(context);\n }\n return { source: 'todo', todos: [...context.todos] };\n }\n\n const id = context.session?.id ?? '';\n if (task.origin?.system === 'session-plan' || graphId.startsWith('plan:')) {\n const planPath = context.meta['plan.path'];\n if (typeof planPath !== 'string' || !planPath) return { source: 'plan' };\n const plan = await mutatePlan(planPath, id, (file) => ({\n ...file,\n updatedAt: new Date().toISOString(),\n items: options.remove\n ? file.items.filter((item) => item.id !== originId)\n : file.items.map((item) =>\n item.id === originId\n ? {\n ...item,\n title: task.title,\n details: task.description,\n status:\n task.status === 'completed'\n ? ('done' as const)\n : task.status === 'in_progress' || task.status === 'review'\n ? ('in_progress' as const)\n : ('open' as const),\n updatedAt: new Date().toISOString(),\n }\n : item,\n ),\n }));\n return { source: 'plan', plan };\n }\n\n if (\n task.origin?.system === 'session-task' ||\n task.origin?.system === 'session' ||\n graphId.startsWith('session:')\n ) {\n const taskPath = context.meta['task.path'];\n if (typeof taskPath !== 'string' || !taskPath) return { source: 'task' };\n const tasks = await mutateTasks(taskPath, id, (file) => ({\n ...file,\n tasks: options.remove\n ? file.tasks.filter((item) => item.id !== originId)\n : file.tasks.map((item) =>\n item.id === originId\n ? {\n ...item,\n title: task.title,\n description: task.description,\n status: sourceStatus(task),\n updatedAt: new Date().toISOString(),\n }\n : item,\n ),\n }));\n return { source: 'task', tasks };\n }\n\n return { source: null };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { type FSWatcher, watch } from 'node:fs';\nimport { basename, dirname } from 'node:path';\nimport {\n type Context,\n deserializeTaskGraph,\n GlobalMailbox,\n loadPlan,\n loadTasks,\n mutatePlan,\n mutateTasks,\n type PlanFile,\n type PlanItem,\n type SerializedTaskGraph,\n type TaskFile,\n type TaskItem,\n type TaskStatus,\n type TodoItem,\n} from '@wrongstack/core';\nimport { resolveWstackPaths } from '@wrongstack/core/utils';\nimport {\n createBoard,\n getBoard,\n getKanbanDir,\n type KanbanBoard,\n type KanbanColumn,\n type KanbanTask,\n listBoards,\n removeBoard,\n syncBoardFromTaskGraph,\n touchKanbanPresence,\n updateBoard,\n} from '@wrongstack/kanban';\n\nconst SESSION_BOARD_TAG = 'session-work';\nconst MIRROR_DISABLED_ENV = 'WRONGSTACK_KANBAN_TASK_MIRROR';\n\n/** The canonical workflow shared by WebUI and TUI session boards. */\nexport const SESSION_KANBAN_COLUMNS: KanbanColumn[] = [\n { id: 'todo', title: 'Todo', order: 0, wipLimit: 0, color: '#2563eb' },\n { id: 'in-progress', title: 'Running', order: 1, wipLimit: 1, color: '#d97706' },\n { id: 'review', title: 'Preview', order: 2, wipLimit: 0, color: '#7c3aed' },\n { id: 'done', title: 'Done', order: 3, wipLimit: 0, color: '#16a34a' },\n];\n\nconst boardQueue = new Map<string, Promise<void>>();\nconst boardEnsures = new Map<string, Promise<KanbanBoard>>();\ntype PendingMirror = {\n projectRoot: string;\n sessionId: string;\n graph: SerializedTaskGraph;\n sourceSystem: 'session-todo' | 'session-task' | 'session-plan';\n};\nconst pendingMirrors = new Map<string, PendingMirror>();\nconst activeMirrors = new Set<string>();\nconst bindings = new WeakMap<Context, () => void>();\nconst suppressedTodoMirrors = new WeakSet<Context>();\nconst activeSessionBoards = new Map<string, number>();\n\nfunction boardKey(projectRoot: string, sessionId: string): string {\n return `${projectRoot}\\0${sessionId}`;\n}\n\nfunction mirrorKey(\n projectRoot: string,\n sessionId: string,\n sourceSystem: PendingMirror['sourceSystem'],\n): string {\n return `${boardKey(projectRoot, sessionId)}\\0${sourceSystem}`;\n}\n\nfunction sessionTag(sessionId: string): string {\n return `session:${sessionId}`;\n}\n\nfunction sessionBoardTitle(sessionId: string): string {\n const leaf = sessionId.split(/[\\\\/]/).filter(Boolean).pop() ?? sessionId;\n return `Session ${leaf.slice(0, 12)}`;\n}\n\nfunction sessionBoardTags(sessionId: string): string[] {\n return ['session', SESSION_BOARD_TAG, sessionTag(sessionId)];\n}\n\nfunction sessionIdFromTags(tags: readonly string[] | undefined): string | null {\n const tag = tags?.find((candidate) => candidate.startsWith('session:'));\n return tag?.slice('session:'.length) || null;\n}\n\nfunction isOwnedSessionBoard(tags: readonly string[] | undefined): boolean {\n return Boolean(tags?.includes(SESSION_BOARD_TAG) && sessionIdFromTags(tags));\n}\n\nfunction retainActiveSessionBoard(projectRoot: string, sessionId: string): void {\n const key = boardKey(projectRoot, sessionId);\n activeSessionBoards.set(key, (activeSessionBoards.get(key) ?? 0) + 1);\n}\n\nfunction releaseActiveSessionBoard(projectRoot: string, sessionId: string): void {\n const key = boardKey(projectRoot, sessionId);\n const remaining = (activeSessionBoards.get(key) ?? 0) - 1;\n if (remaining > 0) activeSessionBoards.set(key, remaining);\n else activeSessionBoards.delete(key);\n}\n\nfunction isSessionBoardActive(projectRoot: string, sessionId: string): boolean {\n return (activeSessionBoards.get(boardKey(projectRoot, sessionId)) ?? 0) > 0;\n}\n\nfunction sameColumns(columns: readonly KanbanColumn[]): boolean {\n return (\n columns.length === SESSION_KANBAN_COLUMNS.length &&\n columns.every((column, index) => column.id === SESSION_KANBAN_COLUMNS[index]?.id)\n );\n}\n\n/** Create (or migrate) the single Kanban board owned by a session. */\nexport async function ensureSessionKanbanBoard(\n projectRoot: string | undefined,\n sessionId: string,\n): Promise<KanbanBoard | null> {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return null;\n const key = boardKey(projectRoot, sessionId);\n const inFlight = boardEnsures.get(key);\n if (inFlight) return inFlight;\n\n const promise = (async () => {\n const summary = (await listBoards(projectRoot)).find((board) =>\n board.tags?.includes(sessionTag(sessionId)),\n );\n let board = summary ? await getBoard(projectRoot, summary.id) : null;\n if (!board) {\n return createBoard(projectRoot, {\n title: sessionBoardTitle(sessionId),\n description: 'Live session work: todos, tasks, and plan items.',\n tags: sessionBoardTags(sessionId),\n columns: SESSION_KANBAN_COLUMNS,\n generatedBy: `session-kanban:${sessionId}`,\n });\n }\n\n // Boards produced by the older task/plan mirrors used the generic five\n // columns. Migrate only session-owned boards; updateBoard reconciles cards\n // from Backlog into Todo while preserving Running/Preview/Done cards.\n if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {\n board =\n (await updateBoard(projectRoot, board.id, {\n title: sessionBoardTitle(sessionId),\n description: 'Live session work: todos, tasks, and plan items.',\n tags: [...new Set([...(board.tags ?? []), ...sessionBoardTags(sessionId)])],\n columns: SESSION_KANBAN_COLUMNS,\n })) ?? board;\n }\n return board;\n })();\n\n boardEnsures.set(key, promise);\n try {\n return await promise;\n } finally {\n boardEnsures.delete(key);\n }\n}\n\nfunction enqueueBoardWork<T>(\n projectRoot: string,\n sessionId: string,\n work: () => Promise<T>,\n): Promise<T> {\n const key = boardKey(projectRoot, sessionId);\n const previous = boardQueue.get(key) ?? Promise.resolve();\n const result = previous.catch(() => undefined).then(work);\n const tail = result.then(\n () => undefined,\n () => undefined,\n );\n boardQueue.set(key, tail);\n void tail.then(() => {\n if (boardQueue.get(key) === tail) boardQueue.delete(key);\n });\n return result;\n}\n\nasync function removeEmptySessionBoard(\n projectRoot: string,\n boardId: string,\n sessionId: string,\n): Promise<string | null> {\n return enqueueBoardWork(projectRoot, sessionId, async () => {\n if (isSessionBoardActive(projectRoot, sessionId)) return null;\n const board = await getBoard(projectRoot, boardId);\n if (!board || board.tasks.length > 0 || !isOwnedSessionBoard(board.tags)) return null;\n if (sessionIdFromTags(board.tags) !== sessionId) return null;\n return (await removeBoard(projectRoot, board.id)) ? board.id : null;\n });\n}\n\n/** Remove a particular inactive session's system-owned board when it has no cards. */\nexport async function cleanupSessionKanbanBoardIfEmpty(\n projectRoot: string | undefined,\n sessionId: string,\n): Promise<string[]> {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return [];\n if (isSessionBoardActive(projectRoot, sessionId)) return [];\n const candidates = (await listBoards(projectRoot)).filter(\n (board) =>\n board.taskCount === 0 &&\n isOwnedSessionBoard(board.tags) &&\n sessionIdFromTags(board.tags) === sessionId,\n );\n const removed = await Promise.all(\n candidates.map((board) => removeEmptySessionBoard(projectRoot, board.id, sessionId)),\n );\n return removed.filter((boardId): boardId is string => Boolean(boardId));\n}\n\n/** Prune stale empty session boards while preserving manual and live boards. */\nexport async function cleanupEmptySessionKanbanBoards(\n projectRoot: string | undefined,\n activeSessionId = '',\n): Promise<string[]> {\n if (!projectRoot || process.env[MIRROR_DISABLED_ENV] === '0') return [];\n const candidates = (await listBoards(projectRoot)).flatMap((board) => {\n const ownerSessionId = sessionIdFromTags(board.tags);\n return board.taskCount === 0 &&\n isOwnedSessionBoard(board.tags) &&\n ownerSessionId &&\n ownerSessionId !== activeSessionId &&\n !isSessionBoardActive(projectRoot, ownerSessionId)\n ? [{ boardId: board.id, sessionId: ownerSessionId }]\n : [];\n });\n const removed = await Promise.all(\n candidates.map(({ boardId, sessionId }) =>\n removeEmptySessionBoard(projectRoot, boardId, sessionId),\n ),\n );\n return removed.filter((boardId): boardId is string => Boolean(boardId));\n}\n\nasync function projectGraph(\n projectRoot: string | undefined,\n sessionId: string,\n graph: SerializedTaskGraph,\n sourceSystem: 'session-todo' | 'session-task' | 'session-plan',\n): Promise<KanbanBoard | null> {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return null;\n return enqueueBoardWork(projectRoot, sessionId, async () => {\n const board = await ensureSessionKanbanBoard(projectRoot, sessionId);\n if (!board) return null;\n const result = await syncBoardFromTaskGraph(\n projectRoot,\n board.id,\n deserializeTaskGraph(graph),\n {\n sourceSystem,\n tags: [...new Set([...(board.tags ?? []), ...sessionBoardTags(sessionId)])],\n archiveMissingTasks: true,\n includeCompletedTasks: true,\n },\n );\n return result?.board ?? null;\n });\n}\n\n/**\n * Queue an observational mirror without retaining every intermediate state.\n * Todo/task/plan streams are independent, but within each stream only the\n * newest pending graph matters. This bounds a stalled file lock to one active\n * and one pending projection instead of an arbitrarily long Promise chain.\n */\nfunction queueLatestMirror(\n projectRoot: string | undefined,\n sessionId: string,\n graph: SerializedTaskGraph,\n sourceSystem: PendingMirror['sourceSystem'],\n): void {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return;\n const key = mirrorKey(projectRoot, sessionId, sourceSystem);\n pendingMirrors.set(key, { projectRoot, sessionId, graph, sourceSystem });\n if (activeMirrors.has(key)) return;\n activeMirrors.add(key);\n void (async () => {\n try {\n for (;;) {\n const pending = pendingMirrors.get(key);\n if (!pending) break;\n pendingMirrors.delete(key);\n try {\n await projectGraph(\n pending.projectRoot,\n pending.sessionId,\n pending.graph,\n pending.sourceSystem,\n );\n } catch {\n // Mirrors are observational. A newer pending snapshot, if present,\n // still gets a chance after a transient lock or filesystem failure.\n }\n }\n } finally {\n activeMirrors.delete(key);\n // A mirror can arrive between the last Map read and deleting the active\n // marker. Re-arm once so that snapshot cannot be stranded.\n const pending = pendingMirrors.get(key);\n if (pending) {\n pendingMirrors.delete(key);\n queueLatestMirror(\n pending.projectRoot,\n pending.sessionId,\n pending.graph,\n pending.sourceSystem,\n );\n }\n }\n })();\n}\n\nexport function todoListToSerializedGraph(\n todos: readonly TodoItem[],\n sessionId: string,\n): SerializedTaskGraph {\n const nodes = todos.map((todo, index) => ({\n id: todo.id,\n title: todo.content,\n description: todo.activeForm ?? '',\n type: 'chore' as const,\n priority: 'medium' as const,\n status: todo.status,\n createdAt: index,\n updatedAt: index,\n }));\n return {\n id: `todo:${sessionId}`,\n specId: `todo:${sessionId}`,\n title: 'Session todos',\n nodes,\n edges: [],\n rootNodes: nodes.map((node) => node.id),\n createdAt: 0,\n updatedAt: 0,\n };\n}\n\nexport function taskFileToSerializedGraph(\n tasks: readonly TaskItem[],\n sessionId: string,\n): SerializedTaskGraph {\n const ids = new Set(tasks.map((task) => task.id));\n const nodes = tasks.map((task, index) => ({\n id: task.id,\n title: task.title,\n description: task.description ?? '',\n type: task.type,\n priority: task.priority,\n status: task.status,\n ...(task.assignee ? { assignee: task.assignee } : {}),\n ...(task.estimateHours !== undefined ? { estimateHours: task.estimateHours } : {}),\n createdAt: index,\n updatedAt: index,\n }));\n const edges = tasks.flatMap((task) =>\n (task.dependsOn ?? [])\n .filter((dependency) => ids.has(dependency))\n .map((dependency) => ({\n id: `${dependency}->${task.id}`,\n from: dependency,\n to: task.id,\n type: 'depends_on' as const,\n })),\n );\n const hasIncoming = new Set(edges.map((edge) => edge.to));\n const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);\n return {\n // Keep the historical graph id so existing mirrored task cards are reused.\n id: `session:${sessionId}`,\n specId: `session:${sessionId}`,\n title: 'Session tasks',\n nodes,\n edges,\n rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],\n createdAt: 0,\n updatedAt: 0,\n };\n}\n\nconst PLAN_STATUS_TO_TASK: Record<PlanItem['status'], TaskStatus> = {\n open: 'pending',\n in_progress: 'in_progress',\n done: 'completed',\n};\n\nexport function planFileToSerializedGraph(\n items: readonly PlanItem[],\n sessionId: string,\n): SerializedTaskGraph {\n const nodes = items.map((item, index) => ({\n id: item.id,\n title: item.title,\n description: item.details ?? '',\n type: 'chore' as const,\n priority: 'medium' as const,\n status: PLAN_STATUS_TO_TASK[item.status],\n createdAt: index,\n updatedAt: index,\n }));\n return {\n id: `plan:${sessionId}`,\n specId: `plan:${sessionId}`,\n title: 'Session plan',\n nodes,\n edges: [],\n rootNodes: nodes.map((node) => node.id),\n createdAt: 0,\n updatedAt: 0,\n };\n}\n\nexport function projectSessionTodosToKanban(\n projectRoot: string | undefined,\n todos: readonly TodoItem[],\n sessionId: string,\n): Promise<KanbanBoard | null> {\n return projectGraph(\n projectRoot,\n sessionId,\n todoListToSerializedGraph(todos, sessionId),\n 'session-todo',\n );\n}\n\nexport function projectSessionTasksToKanban(\n projectRoot: string | undefined,\n tasks: readonly TaskItem[],\n sessionId: string,\n): Promise<KanbanBoard | null> {\n return projectGraph(\n projectRoot,\n sessionId,\n taskFileToSerializedGraph(tasks, sessionId),\n 'session-task',\n );\n}\n\nexport function projectSessionPlanToKanban(\n projectRoot: string | undefined,\n items: readonly PlanItem[],\n sessionId: string,\n): Promise<KanbanBoard | null> {\n return projectGraph(\n projectRoot,\n sessionId,\n planFileToSerializedGraph(items, sessionId),\n 'session-plan',\n );\n}\n\nfunction fireAndForget(work: Promise<unknown>): void {\n void work.catch(() => {\n // The session files/state remain recoverable if the observational board\n // cannot be written; the next mutation or file watcher retries the mirror.\n });\n}\n\nfunction broadcastTodoUpdate(context: Context, todos: readonly TodoItem[]): void {\n const sessionId = context.session?.id ?? '';\n if (!context.agentId || !sessionId) return;\n const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;\n const mailbox = new GlobalMailbox(projectDir);\n void mailbox\n .send({\n from: context.agentId,\n to: '*',\n type: 'status',\n subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? '' : 's'})`,\n body: JSON.stringify({\n kind: 'kanban.todos.updated',\n sessionId,\n revision: context.state.revision,\n todos,\n }),\n priority: 'normal',\n senderSessionId: sessionId,\n ttlMs: 6 * 60 * 60 * 1000,\n })\n .catch(() => {\n // Mailbox awareness is best-effort; canonical state is already updated.\n });\n}\n\nfunction notifyTodoUpdate(context: Context, todos: readonly TodoItem[]): void {\n const summary = todos.length\n ? todos\n .map((todo) => `- [${todo.status}] ${todo.content} (${todo.id})`)\n .join('\\n')\n : '- No active todos remain.';\n const text = `[KANBAN TODO UPDATE]\\nAnother Kanban agent reassessed the shared board. The canonical todo list is now:\\n${summary}\\nReassess your current plan before continuing; do not rely on the initial todo snapshot.`;\n const state = context.state as Partial<Context['state']>;\n if (typeof state.appendBlockToLastUserMessage === 'function') {\n if (state.appendBlockToLastUserMessage({ type: 'text', text })) return;\n }\n if (typeof state.appendMessage === 'function') {\n state.appendMessage({ role: 'user', content: [{ type: 'text', text }] });\n }\n}\n\nexport function mirrorSessionTodosToKanban(\n projectRoot: string | undefined,\n todos: readonly TodoItem[],\n sessionId: string,\n): void {\n queueLatestMirror(\n projectRoot,\n sessionId,\n todoListToSerializedGraph(todos, sessionId),\n 'session-todo',\n );\n}\n\nexport function mirrorSessionTasksToKanban(\n projectRoot: string | undefined,\n tasks: readonly TaskItem[],\n sessionId: string,\n): void {\n queueLatestMirror(\n projectRoot,\n sessionId,\n taskFileToSerializedGraph(tasks, sessionId),\n 'session-task',\n );\n}\n\nexport function mirrorSessionPlanToKanban(\n projectRoot: string | undefined,\n items: readonly PlanItem[],\n sessionId: string,\n): void {\n queueLatestMirror(\n projectRoot,\n sessionId,\n planFileToSerializedGraph(items, sessionId),\n 'session-plan',\n );\n}\n\n/**\n * Bind all live session work paths to Kanban. Todo changes are observed from\n * ConversationState; plan/task sidecars are watched so slash commands, WebUI,\n * plugins, and tools all pass through the same board.\n */\nexport function attachSessionKanbanMirror(context: Context): () => void {\n const existing = bindings.get(context);\n if (existing) return existing;\n\n const attachedProjectRoot = context.projectRoot ?? '';\n let registeredSessionId = '';\n const syncActiveSessionRegistration = () => {\n if (!attachedProjectRoot) return;\n const currentSessionId = context.session?.id ?? '';\n if (currentSessionId === registeredSessionId) return;\n if (registeredSessionId) {\n releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);\n fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));\n }\n registeredSessionId = currentSessionId;\n if (registeredSessionId) {\n retainActiveSessionBoard(attachedProjectRoot, registeredSessionId);\n }\n };\n syncActiveSessionRegistration();\n\n let watcher: FSWatcher | null = null;\n let watchedDir = '';\n let timer: NodeJS.Timeout | null = null;\n let boardWatcher: FSWatcher | null = null;\n let watchedBoardId = '';\n let boardTimer: NodeJS.Timeout | null = null;\n let presenceTimer: NodeJS.Timeout | null = null;\n\n const sessionId = () => context.session?.id ?? '';\n const refreshFiles = async () => {\n const id = sessionId();\n if (!id) return;\n const planPath = context.meta['plan.path'];\n if (typeof planPath === 'string' && planPath) {\n const plan = await loadPlan(planPath);\n if (plan) await projectSessionPlanToKanban(context.projectRoot, plan.items, id);\n }\n const taskPath = context.meta['task.path'];\n if (typeof taskPath === 'string' && taskPath) {\n const tasks = await loadTasks(taskPath);\n if (tasks) await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);\n }\n };\n\n const refreshBoard = async () => {\n if (!watchedBoardId) return;\n const board = await getBoard(context.projectRoot, watchedBoardId);\n if (board) applySessionKanbanBoardToTodos(context, board);\n };\n\n const configureBoardWatcher = async () => {\n const id = sessionId();\n const board = id ? await ensureSessionKanbanBoard(context.projectRoot, id) : null;\n if (!board || board.id === watchedBoardId) return;\n boardWatcher?.close();\n boardWatcher = null;\n watchedBoardId = board.id;\n try {\n const boardFileName = `${board.id}.json`;\n boardWatcher = watch(\n getKanbanDir(context.projectRoot),\n { persistent: false },\n (_event, filename) => {\n if (filename?.toString() !== boardFileName) return;\n if (boardTimer) clearTimeout(boardTimer);\n boardTimer = setTimeout(() => fireAndForget(refreshBoard()), 60);\n },\n );\n const touchPresence = () =>\n touchKanbanPresence(context.projectRoot, board.id, {\n sessionId: id,\n agentId: context.agentId,\n agentName: context.agentName,\n });\n fireAndForget(touchPresence());\n if (presenceTimer) clearInterval(presenceTimer);\n presenceTimer = setInterval(() => fireAndForget(touchPresence()), 60_000);\n presenceTimer.unref?.();\n boardWatcher.on('error', () => {\n boardWatcher?.close();\n boardWatcher = null;\n watchedBoardId = '';\n });\n } catch {\n boardWatcher = null;\n watchedBoardId = '';\n }\n };\n\n const configureWatcher = () => {\n const planPath = context.meta['plan.path'];\n const taskPath = context.meta['task.path'];\n const candidate =\n typeof planPath === 'string' && planPath\n ? dirname(planPath)\n : typeof taskPath === 'string' && taskPath\n ? dirname(taskPath)\n : '';\n if (!candidate || candidate === watchedDir) return;\n watcher?.close();\n watcher = null;\n watchedDir = candidate;\n try {\n watcher = watch(candidate, { persistent: false }, (_event, filename) => {\n const name = filename?.toString();\n const currentPlanPath = context.meta['plan.path'];\n const currentTaskPath = context.meta['task.path'];\n const planName = typeof currentPlanPath === 'string' ? basename(currentPlanPath) : '';\n const taskName = typeof currentTaskPath === 'string' ? basename(currentTaskPath) : '';\n if (name && name !== planName && name !== taskName) return;\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => fireAndForget(refreshFiles()), 60);\n });\n watcher.on('error', () => watcher?.close());\n } catch {\n // The session directory can briefly disappear during project/session\n // switches; the next meta update re-attempts the binding.\n watcher = null;\n watchedDir = '';\n }\n };\n\n const unsubscribe = context.state.onChange((change) => {\n if (change.kind === 'todos_replaced' && !suppressedTodoMirrors.has(context)) {\n // ConversationState auto-clears an all-done tactical list. Project the\n // pre-clear completion snapshot so every card reaches Done atomically.\n mirrorSessionTodosToKanban(\n context.projectRoot,\n change.completedSnapshot ?? change.todos,\n sessionId(),\n );\n return;\n }\n if (change.kind === 'meta_set' && (change.key === 'plan.path' || change.key === 'task.path')) {\n syncActiveSessionRegistration();\n configureWatcher();\n fireAndForget(ensureSessionKanbanBoard(context.projectRoot, sessionId()));\n fireAndForget(configureBoardWatcher());\n fireAndForget(refreshFiles());\n }\n });\n\n configureWatcher();\n fireAndForget(configureBoardWatcher());\n\n const detach = () => {\n unsubscribe();\n if (timer) clearTimeout(timer);\n if (boardTimer) clearTimeout(boardTimer);\n if (presenceTimer) clearInterval(presenceTimer);\n watcher?.close();\n boardWatcher?.close();\n bindings.delete(context);\n if (attachedProjectRoot && registeredSessionId) {\n releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);\n fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));\n registeredSessionId = '';\n }\n };\n bindings.set(context, detach);\n return detach;\n}\n\n/** Fully hydrate a session board before a host announces the session as ready. */\nexport async function hydrateSessionKanban(context: Context): Promise<KanbanBoard | null> {\n const id = context.session?.id ?? '';\n if (!id) return null;\n await cleanupEmptySessionKanbanBoards(context.projectRoot, id);\n let board = await ensureSessionKanbanBoard(context.projectRoot, id);\n if (context.todos.length) {\n board = await projectSessionTodosToKanban(context.projectRoot, context.todos, id);\n }\n const planPath = context.meta['plan.path'];\n if (typeof planPath === 'string' && planPath) {\n const plan = await loadPlan(planPath);\n if (plan) board = await projectSessionPlanToKanban(context.projectRoot, plan.items, id);\n }\n const taskPath = context.meta['task.path'];\n if (typeof taskPath === 'string' && taskPath) {\n const tasks = await loadTasks(taskPath);\n if (tasks) board = await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);\n }\n return board;\n}\n\nexport interface SessionKanbanSourceUpdate {\n source: 'todo' | 'task' | 'plan' | null;\n todos?: TodoItem[] | undefined;\n tasks?: TaskFile | undefined;\n plan?: PlanFile | undefined;\n}\n\nfunction sourceStatus(task: KanbanTask): TaskStatus {\n if (task.status === 'completed') return 'completed';\n if (task.status === 'in_progress') return 'in_progress';\n if (task.status === 'review') return 'review';\n if (task.status === 'blocked') return 'blocked';\n if (task.status === 'failed') return 'failed';\n return 'pending';\n}\n\nfunction todoStatus(task: KanbanTask): TodoItem['status'] {\n const status = sourceStatus(task);\n if (status === 'completed') return 'completed';\n if (status === 'in_progress' || status === 'review') return 'in_progress';\n return 'pending';\n}\n\nfunction sessionTodoFromTask(task: KanbanTask): TodoItem {\n return {\n id: task.origin?.taskId ?? task.id,\n content: task.title,\n status: todoStatus(task),\n ...(task.description ? { activeForm: task.description } : {}),\n };\n}\n\nfunction sameTodos(left: readonly TodoItem[], right: readonly TodoItem[]): boolean {\n return (\n left.length === right.length &&\n left.every((todo, index) => {\n const candidate = right[index];\n return (\n candidate?.id === todo.id &&\n candidate.content === todo.content &&\n candidate.status === todo.status &&\n candidate.activeForm === todo.activeForm &&\n candidate.promotedFromPlan === todo.promotedFromPlan &&\n candidate.promotedFromTask === todo.promotedFromTask\n );\n })\n );\n}\n\n/**\n * Replace the tactical todo list from the current cards on a session-owned board.\n *\n * Existing mirrored todo cards retain their stable source ids. New cards created\n * by a Kanban worker become todos under their card ids, so reassessment can add,\n * split, merge, reprioritize, or remove work without being overwritten by the\n * todo list that happened to exist when the run started.\n */\nexport function applySessionKanbanBoardToTodos(context: Context, board: KanbanBoard): TodoItem[] {\n const sessionId = context.session?.id ?? '';\n if (!sessionId || sessionIdFromTags(board.tags) !== sessionId || !isOwnedSessionBoard(board.tags)) {\n return [...context.todos];\n }\n\n const projectedTodos = board.tasks\n .filter(\n (task) =>\n task.status !== 'archived' &&\n (!task.origin ||\n task.origin.system === 'session-todo' ||\n (task.origin.graphId ?? '').startsWith('todo:')),\n )\n .sort((left, right) => {\n const leftColumn = board.columns.find((column) => column.id === left.columnId)?.order ?? 0;\n const rightColumn = board.columns.find((column) => column.id === right.columnId)?.order ?? 0;\n return leftColumn - rightColumn || left.order - right.order || left.createdAt.localeCompare(right.createdAt);\n })\n .map(sessionTodoFromTask);\n\n const allCompleted =\n projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === 'completed');\n const effectiveTodos = allCompleted ? [] : projectedTodos;\n if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];\n suppressedTodoMirrors.add(context);\n try {\n context.state.replaceTodos(projectedTodos);\n } finally {\n suppressedTodoMirrors.delete(context);\n }\n notifyTodoUpdate(context, context.todos);\n broadcastTodoUpdate(context, context.todos);\n return [...context.todos];\n}\n\n/** Reflect a TUI/WebUI Kanban card edit back to its originating work list. */\nexport async function applySessionKanbanTaskToSource(\n context: Context,\n task: KanbanTask,\n options: { remove?: boolean | undefined } = {},\n): Promise<SessionKanbanSourceUpdate> {\n const originId = task.origin?.taskId;\n const graphId = task.origin?.graphId ?? '';\n if (!originId) return { source: null };\n\n if (task.origin?.system === 'session-todo' || graphId.startsWith('todo:')) {\n const next = options.remove\n ? context.todos.filter((todo) => todo.id !== originId)\n : context.todos.map((todo) =>\n todo.id === originId\n ? {\n ...todo,\n content: task.title,\n status:\n sourceStatus(task) === 'completed'\n ? ('completed' as const)\n : sourceStatus(task) === 'in_progress' || sourceStatus(task) === 'review'\n ? ('in_progress' as const)\n : ('pending' as const),\n }\n : todo,\n );\n suppressedTodoMirrors.add(context);\n try {\n context.state.replaceTodos(next);\n } finally {\n suppressedTodoMirrors.delete(context);\n }\n return { source: 'todo', todos: [...context.todos] };\n }\n\n const id = context.session?.id ?? '';\n if (task.origin?.system === 'session-plan' || graphId.startsWith('plan:')) {\n const planPath = context.meta['plan.path'];\n if (typeof planPath !== 'string' || !planPath) return { source: 'plan' };\n const plan = await mutatePlan(planPath, id, (file) => ({\n ...file,\n updatedAt: new Date().toISOString(),\n items: options.remove\n ? file.items.filter((item) => item.id !== originId)\n : file.items.map((item) =>\n item.id === originId\n ? {\n ...item,\n title: task.title,\n details: task.description,\n status:\n task.status === 'completed'\n ? ('done' as const)\n : task.status === 'in_progress' || task.status === 'review'\n ? ('in_progress' as const)\n : ('open' as const),\n updatedAt: new Date().toISOString(),\n }\n : item,\n ),\n }));\n return { source: 'plan', plan };\n }\n\n if (\n task.origin?.system === 'session-task' ||\n task.origin?.system === 'session' ||\n graphId.startsWith('session:')\n ) {\n const taskPath = context.meta['task.path'];\n if (typeof taskPath !== 'string' || !taskPath) return { source: 'task' };\n const tasks = await mutateTasks(taskPath, id, (file) => ({\n ...file,\n tasks: options.remove\n ? file.tasks.filter((item) => item.id !== originId)\n : file.tasks.map((item) =>\n item.id === originId\n ? {\n ...item,\n title: task.title,\n description: task.description,\n status: sourceStatus(task),\n updatedAt: new Date().toISOString(),\n }\n : item,\n ),\n }));\n return { source: 'task', tasks };\n }\n\n return { source: null };\n}\n"],
5
+ "mappings": ";AAAA,SAAyB,aAAa;AACtC,SAAS,UAAU,eAAe;AAClC;AAAA,EAEE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAQK;AACP,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EAIA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,IAAM,oBAAoB;AAC1B,IAAM,sBAAsB;AAGrB,IAAM,yBAAyC;AAAA,EACpD,EAAE,IAAI,QAAQ,OAAO,QAAQ,OAAO,GAAG,UAAU,GAAG,OAAO,UAAU;AAAA,EACrE,EAAE,IAAI,eAAe,OAAO,WAAW,OAAO,GAAG,UAAU,GAAG,OAAO,UAAU;AAAA,EAC/E,EAAE,IAAI,UAAU,OAAO,WAAW,OAAO,GAAG,UAAU,GAAG,OAAO,UAAU;AAAA,EAC1E,EAAE,IAAI,QAAQ,OAAO,QAAQ,OAAO,GAAG,UAAU,GAAG,OAAO,UAAU;AACvE;AAEA,IAAM,aAAa,oBAAI,IAA2B;AAClD,IAAM,eAAe,oBAAI,IAAkC;AAO3D,IAAM,iBAAiB,oBAAI,IAA2B;AACtD,IAAM,gBAAgB,oBAAI,IAAY;AACtC,IAAM,WAAW,oBAAI,QAA6B;AAClD,IAAM,wBAAwB,oBAAI,QAAiB;AACnD,IAAM,sBAAsB,oBAAI,IAAoB;AAEpD,SAAS,SAAS,aAAqB,WAA2B;AAChE,SAAO,GAAG,WAAW,KAAK,SAAS;AACrC;AAEA,SAAS,UACP,aACA,WACA,cACQ;AACR,SAAO,GAAG,SAAS,aAAa,SAAS,CAAC,KAAK,YAAY;AAC7D;AAEA,SAAS,WAAW,WAA2B;AAC7C,SAAO,WAAW,SAAS;AAC7B;AAEA,SAAS,kBAAkB,WAA2B;AACpD,QAAM,OAAO,UAAU,MAAM,OAAO,EAAE,OAAO,OAAO,EAAE,IAAI,KAAK;AAC/D,SAAO,WAAW,KAAK,MAAM,GAAG,EAAE,CAAC;AACrC;AAEA,SAAS,iBAAiB,WAA6B;AACrD,SAAO,CAAC,WAAW,mBAAmB,WAAW,SAAS,CAAC;AAC7D;AAEA,SAAS,kBAAkB,MAAoD;AAC7E,QAAM,MAAM,MAAM,KAAK,CAAC,cAAc,UAAU,WAAW,UAAU,CAAC;AACtE,SAAO,KAAK,MAAM,WAAW,MAAM,KAAK;AAC1C;AAEA,SAAS,oBAAoB,MAA8C;AACzE,SAAO,QAAQ,MAAM,SAAS,iBAAiB,KAAK,kBAAkB,IAAI,CAAC;AAC7E;AAEA,SAAS,yBAAyB,aAAqB,WAAyB;AAC9E,QAAM,MAAM,SAAS,aAAa,SAAS;AAC3C,sBAAoB,IAAI,MAAM,oBAAoB,IAAI,GAAG,KAAK,KAAK,CAAC;AACtE;AAEA,SAAS,0BAA0B,aAAqB,WAAyB;AAC/E,QAAM,MAAM,SAAS,aAAa,SAAS;AAC3C,QAAM,aAAa,oBAAoB,IAAI,GAAG,KAAK,KAAK;AACxD,MAAI,YAAY,EAAG,qBAAoB,IAAI,KAAK,SAAS;AAAA,MACpD,qBAAoB,OAAO,GAAG;AACrC;AAEA,SAAS,qBAAqB,aAAqB,WAA4B;AAC7E,UAAQ,oBAAoB,IAAI,SAAS,aAAa,SAAS,CAAC,KAAK,KAAK;AAC5E;AAEA,SAAS,YAAY,SAA2C;AAC9D,SACE,QAAQ,WAAW,uBAAuB,UAC1C,QAAQ,MAAM,CAAC,QAAQ,UAAU,OAAO,OAAO,uBAAuB,KAAK,GAAG,EAAE;AAEpF;AAGA,eAAsB,yBACpB,aACA,WAC6B;AAC7B,MAAI,CAAC,eAAe,CAAC,aAAa,QAAQ,IAAI,mBAAmB,MAAM,IAAK,QAAO;AACnF,QAAM,MAAM,SAAS,aAAa,SAAS;AAC3C,QAAM,WAAW,aAAa,IAAI,GAAG;AACrC,MAAI,SAAU,QAAO;AAErB,QAAM,WAAW,YAAY;AAC3B,UAAM,WAAW,MAAM,WAAW,WAAW,GAAG;AAAA,MAAK,CAACA,WACpDA,OAAM,MAAM,SAAS,WAAW,SAAS,CAAC;AAAA,IAC5C;AACA,QAAI,QAAQ,UAAU,MAAM,SAAS,aAAa,QAAQ,EAAE,IAAI;AAChE,QAAI,CAAC,OAAO;AACV,aAAO,YAAY,aAAa;AAAA,QAC9B,OAAO,kBAAkB,SAAS;AAAA,QAClC,aAAa;AAAA,QACb,MAAM,iBAAiB,SAAS;AAAA,QAChC,SAAS;AAAA,QACT,aAAa,kBAAkB,SAAS;AAAA,MAC1C,CAAC;AAAA,IACH;AAKA,QAAI,CAAC,YAAY,MAAM,OAAO,KAAK,CAAC,MAAM,MAAM,SAAS,iBAAiB,GAAG;AAC3E,cACG,MAAM,YAAY,aAAa,MAAM,IAAI;AAAA,QACxC,OAAO,kBAAkB,SAAS;AAAA,QAClC,aAAa;AAAA,QACb,MAAM,CAAC,GAAG,oBAAI,IAAI,CAAC,GAAI,MAAM,QAAQ,CAAC,GAAI,GAAG,iBAAiB,SAAS,CAAC,CAAC,CAAC;AAAA,QAC1E,SAAS;AAAA,MACX,CAAC,KAAM;AAAA,IACX;AACA,WAAO;AAAA,EACT,GAAG;AAEH,eAAa,IAAI,KAAK,OAAO;AAC7B,MAAI;AACF,WAAO,MAAM;AAAA,EACf,UAAE;AACA,iBAAa,OAAO,GAAG;AAAA,EACzB;AACF;AAEA,SAAS,iBACP,aACA,WACA,MACY;AACZ,QAAM,MAAM,SAAS,aAAa,SAAS;AAC3C,QAAM,WAAW,WAAW,IAAI,GAAG,KAAK,QAAQ,QAAQ;AACxD,QAAM,SAAS,SAAS,MAAM,MAAM,MAAS,EAAE,KAAK,IAAI;AACxD,QAAM,OAAO,OAAO;AAAA,IAClB,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AACA,aAAW,IAAI,KAAK,IAAI;AACxB,OAAK,KAAK,KAAK,MAAM;AACnB,QAAI,WAAW,IAAI,GAAG,MAAM,KAAM,YAAW,OAAO,GAAG;AAAA,EACzD,CAAC;AACD,SAAO;AACT;AAEA,eAAe,wBACb,aACA,SACA,WACwB;AACxB,SAAO,iBAAiB,aAAa,WAAW,YAAY;AAC1D,QAAI,qBAAqB,aAAa,SAAS,EAAG,QAAO;AACzD,UAAM,QAAQ,MAAM,SAAS,aAAa,OAAO;AACjD,QAAI,CAAC,SAAS,MAAM,MAAM,SAAS,KAAK,CAAC,oBAAoB,MAAM,IAAI,EAAG,QAAO;AACjF,QAAI,kBAAkB,MAAM,IAAI,MAAM,UAAW,QAAO;AACxD,WAAQ,MAAM,YAAY,aAAa,MAAM,EAAE,IAAK,MAAM,KAAK;AAAA,EACjE,CAAC;AACH;AAGA,eAAsB,iCACpB,aACA,WACmB;AACnB,MAAI,CAAC,eAAe,CAAC,aAAa,QAAQ,IAAI,mBAAmB,MAAM,IAAK,QAAO,CAAC;AACpF,MAAI,qBAAqB,aAAa,SAAS,EAAG,QAAO,CAAC;AAC1D,QAAM,cAAc,MAAM,WAAW,WAAW,GAAG;AAAA,IACjD,CAAC,UACC,MAAM,cAAc,KACpB,oBAAoB,MAAM,IAAI,KAC9B,kBAAkB,MAAM,IAAI,MAAM;AAAA,EACtC;AACA,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC5B,WAAW,IAAI,CAAC,UAAU,wBAAwB,aAAa,MAAM,IAAI,SAAS,CAAC;AAAA,EACrF;AACA,SAAO,QAAQ,OAAO,CAAC,YAA+B,QAAQ,OAAO,CAAC;AACxE;AAGA,eAAsB,gCACpB,aACA,kBAAkB,IACC;AACnB,MAAI,CAAC,eAAe,QAAQ,IAAI,mBAAmB,MAAM,IAAK,QAAO,CAAC;AACtE,QAAM,cAAc,MAAM,WAAW,WAAW,GAAG,QAAQ,CAAC,UAAU;AACpE,UAAM,iBAAiB,kBAAkB,MAAM,IAAI;AACnD,WAAO,MAAM,cAAc,KACzB,oBAAoB,MAAM,IAAI,KAC9B,kBACA,mBAAmB,mBACnB,CAAC,qBAAqB,aAAa,cAAc,IAC/C,CAAC,EAAE,SAAS,MAAM,IAAI,WAAW,eAAe,CAAC,IACjD,CAAC;AAAA,EACP,CAAC;AACD,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC5B,WAAW;AAAA,MAAI,CAAC,EAAE,SAAS,UAAU,MACnC,wBAAwB,aAAa,SAAS,SAAS;AAAA,IACzD;AAAA,EACF;AACA,SAAO,QAAQ,OAAO,CAAC,YAA+B,QAAQ,OAAO,CAAC;AACxE;AAEA,eAAe,aACb,aACA,WACA,OACA,cAC6B;AAC7B,MAAI,CAAC,eAAe,CAAC,aAAa,QAAQ,IAAI,mBAAmB,MAAM,IAAK,QAAO;AACnF,SAAO,iBAAiB,aAAa,WAAW,YAAY;AAC1D,UAAM,QAAQ,MAAM,yBAAyB,aAAa,SAAS;AACnE,QAAI,CAAC,MAAO,QAAO;AACnB,UAAM,SAAS,MAAM;AAAA,MACnB;AAAA,MACA,MAAM;AAAA,MACN,qBAAqB,KAAK;AAAA,MAC1B;AAAA,QACE;AAAA,QACA,MAAM,CAAC,GAAG,oBAAI,IAAI,CAAC,GAAI,MAAM,QAAQ,CAAC,GAAI,GAAG,iBAAiB,SAAS,CAAC,CAAC,CAAC;AAAA,QAC1E,qBAAqB;AAAA,QACrB,uBAAuB;AAAA,MACzB;AAAA,IACF;AACA,WAAO,QAAQ,SAAS;AAAA,EAC1B,CAAC;AACH;AAQA,SAAS,kBACP,aACA,WACA,OACA,cACM;AACN,MAAI,CAAC,eAAe,CAAC,aAAa,QAAQ,IAAI,mBAAmB,MAAM,IAAK;AAC5E,QAAM,MAAM,UAAU,aAAa,WAAW,YAAY;AAC1D,iBAAe,IAAI,KAAK,EAAE,aAAa,WAAW,OAAO,aAAa,CAAC;AACvE,MAAI,cAAc,IAAI,GAAG,EAAG;AAC5B,gBAAc,IAAI,GAAG;AACrB,QAAM,YAAY;AAChB,QAAI;AACF,iBAAS;AACP,cAAM,UAAU,eAAe,IAAI,GAAG;AACtC,YAAI,CAAC,QAAS;AACd,uBAAe,OAAO,GAAG;AACzB,YAAI;AACF,gBAAM;AAAA,YACJ,QAAQ;AAAA,YACR,QAAQ;AAAA,YACR,QAAQ;AAAA,YACR,QAAQ;AAAA,UACV;AAAA,QACF,QAAQ;AAAA,QAGR;AAAA,MACF;AAAA,IACF,UAAE;AACA,oBAAc,OAAO,GAAG;AAGxB,YAAM,UAAU,eAAe,IAAI,GAAG;AACtC,UAAI,SAAS;AACX,uBAAe,OAAO,GAAG;AACzB;AAAA,UACE,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAAA,EACF,GAAG;AACL;AAEO,SAAS,0BACd,OACA,WACqB;AACrB,QAAM,QAAQ,MAAM,IAAI,CAAC,MAAM,WAAW;AAAA,IACxC,IAAI,KAAK;AAAA,IACT,OAAO,KAAK;AAAA,IACZ,aAAa,KAAK,cAAc;AAAA,IAChC,MAAM;AAAA,IACN,UAAU;AAAA,IACV,QAAQ,KAAK;AAAA,IACb,WAAW;AAAA,IACX,WAAW;AAAA,EACb,EAAE;AACF,SAAO;AAAA,IACL,IAAI,QAAQ,SAAS;AAAA,IACrB,QAAQ,QAAQ,SAAS;AAAA,IACzB,OAAO;AAAA,IACP;AAAA,IACA,OAAO,CAAC;AAAA,IACR,WAAW,MAAM,IAAI,CAAC,SAAS,KAAK,EAAE;AAAA,IACtC,WAAW;AAAA,IACX,WAAW;AAAA,EACb;AACF;AAEO,SAAS,0BACd,OACA,WACqB;AACrB,QAAM,MAAM,IAAI,IAAI,MAAM,IAAI,CAAC,SAAS,KAAK,EAAE,CAAC;AAChD,QAAM,QAAQ,MAAM,IAAI,CAAC,MAAM,WAAW;AAAA,IACxC,IAAI,KAAK;AAAA,IACT,OAAO,KAAK;AAAA,IACZ,aAAa,KAAK,eAAe;AAAA,IACjC,MAAM,KAAK;AAAA,IACX,UAAU,KAAK;AAAA,IACf,QAAQ,KAAK;AAAA,IACb,GAAI,KAAK,WAAW,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC;AAAA,IACnD,GAAI,KAAK,kBAAkB,SAAY,EAAE,eAAe,KAAK,cAAc,IAAI,CAAC;AAAA,IAChF,WAAW;AAAA,IACX,WAAW;AAAA,EACb,EAAE;AACF,QAAM,QAAQ,MAAM;AAAA,IAAQ,CAAC,UAC1B,KAAK,aAAa,CAAC,GACjB,OAAO,CAAC,eAAe,IAAI,IAAI,UAAU,CAAC,EAC1C,IAAI,CAAC,gBAAgB;AAAA,MACpB,IAAI,GAAG,UAAU,KAAK,KAAK,EAAE;AAAA,MAC7B,MAAM;AAAA,MACN,IAAI,KAAK;AAAA,MACT,MAAM;AAAA,IACR,EAAE;AAAA,EACN;AACA,QAAM,cAAc,IAAI,IAAI,MAAM,IAAI,CAAC,SAAS,KAAK,EAAE,CAAC;AACxD,QAAM,YAAY,MAAM,OAAO,CAAC,SAAS,CAAC,YAAY,IAAI,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,SAAS,KAAK,EAAE;AACzF,SAAO;AAAA;AAAA,IAEL,IAAI,WAAW,SAAS;AAAA,IACxB,QAAQ,WAAW,SAAS;AAAA,IAC5B,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,WAAW,UAAU,SAAS,YAAY,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC;AAAA,IACtE,WAAW;AAAA,IACX,WAAW;AAAA,EACb;AACF;AAEA,IAAM,sBAA8D;AAAA,EAClE,MAAM;AAAA,EACN,aAAa;AAAA,EACb,MAAM;AACR;AAEO,SAAS,0BACd,OACA,WACqB;AACrB,QAAM,QAAQ,MAAM,IAAI,CAAC,MAAM,WAAW;AAAA,IACxC,IAAI,KAAK;AAAA,IACT,OAAO,KAAK;AAAA,IACZ,aAAa,KAAK,WAAW;AAAA,IAC7B,MAAM;AAAA,IACN,UAAU;AAAA,IACV,QAAQ,oBAAoB,KAAK,MAAM;AAAA,IACvC,WAAW;AAAA,IACX,WAAW;AAAA,EACb,EAAE;AACF,SAAO;AAAA,IACL,IAAI,QAAQ,SAAS;AAAA,IACrB,QAAQ,QAAQ,SAAS;AAAA,IACzB,OAAO;AAAA,IACP;AAAA,IACA,OAAO,CAAC;AAAA,IACR,WAAW,MAAM,IAAI,CAAC,SAAS,KAAK,EAAE;AAAA,IACtC,WAAW;AAAA,IACX,WAAW;AAAA,EACb;AACF;AAEO,SAAS,4BACd,aACA,OACA,WAC6B;AAC7B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,0BAA0B,OAAO,SAAS;AAAA,IAC1C;AAAA,EACF;AACF;AAEO,SAAS,4BACd,aACA,OACA,WAC6B;AAC7B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,0BAA0B,OAAO,SAAS;AAAA,IAC1C;AAAA,EACF;AACF;AAEO,SAAS,2BACd,aACA,OACA,WAC6B;AAC7B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,0BAA0B,OAAO,SAAS;AAAA,IAC1C;AAAA,EACF;AACF;AAEA,SAAS,cAAc,MAA8B;AACnD,OAAK,KAAK,MAAM,MAAM;AAAA,EAGtB,CAAC;AACH;AAEA,SAAS,oBAAoB,SAAkB,OAAkC;AAC/E,QAAM,YAAY,QAAQ,SAAS,MAAM;AACzC,MAAI,CAAC,QAAQ,WAAW,CAAC,UAAW;AACpC,QAAM,aAAa,mBAAmB,EAAE,aAAa,QAAQ,YAAY,CAAC,EAAE;AAC5E,QAAM,UAAU,IAAI,cAAc,UAAU;AAC5C,OAAK,QACF,KAAK;AAAA,IACJ,MAAM,QAAQ;AAAA,IACd,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,SAAS,6BAA6B,MAAM,MAAM,QAAQ,MAAM,WAAW,IAAI,KAAK,GAAG;AAAA,IACvF,MAAM,KAAK,UAAU;AAAA,MACnB,MAAM;AAAA,MACN;AAAA,MACA,UAAU,QAAQ,MAAM;AAAA,MACxB;AAAA,IACF,CAAC;AAAA,IACD,UAAU;AAAA,IACV,iBAAiB;AAAA,IACjB,OAAO,IAAI,KAAK,KAAK;AAAA,EACvB,CAAC,EACA,MAAM,MAAM;AAAA,EAEb,CAAC;AACL;AAEA,SAAS,iBAAiB,SAAkB,OAAkC;AAC5E,QAAM,UAAU,MAAM,SAClB,MACG,IAAI,CAAC,SAAS,MAAM,KAAK,MAAM,KAAK,KAAK,OAAO,KAAK,KAAK,EAAE,GAAG,EAC/D,KAAK,IAAI,IACZ;AACJ,QAAM,OAAO;AAAA;AAAA,EAA4G,OAAO;AAAA;AAChI,QAAM,QAAQ,QAAQ;AACtB,MAAI,OAAO,MAAM,iCAAiC,YAAY;AAC5D,QAAI,MAAM,6BAA6B,EAAE,MAAM,QAAQ,KAAK,CAAC,EAAG;AAAA,EAClE;AACA,MAAI,OAAO,MAAM,kBAAkB,YAAY;AAC7C,UAAM,cAAc,EAAE,MAAM,QAAQ,SAAS,CAAC,EAAE,MAAM,QAAQ,KAAK,CAAC,EAAE,CAAC;AAAA,EACzE;AACF;AAEO,SAAS,2BACd,aACA,OACA,WACM;AACN;AAAA,IACE;AAAA,IACA;AAAA,IACA,0BAA0B,OAAO,SAAS;AAAA,IAC1C;AAAA,EACF;AACF;AAEO,SAAS,2BACd,aACA,OACA,WACM;AACN;AAAA,IACE;AAAA,IACA;AAAA,IACA,0BAA0B,OAAO,SAAS;AAAA,IAC1C;AAAA,EACF;AACF;AAEO,SAAS,0BACd,aACA,OACA,WACM;AACN;AAAA,IACE;AAAA,IACA;AAAA,IACA,0BAA0B,OAAO,SAAS;AAAA,IAC1C;AAAA,EACF;AACF;AAOO,SAAS,0BAA0B,SAA8B;AACtE,QAAM,WAAW,SAAS,IAAI,OAAO;AACrC,MAAI,SAAU,QAAO;AAErB,QAAM,sBAAsB,QAAQ,eAAe;AACnD,MAAI,sBAAsB;AAC1B,QAAM,gCAAgC,MAAM;AAC1C,QAAI,CAAC,oBAAqB;AAC1B,UAAM,mBAAmB,QAAQ,SAAS,MAAM;AAChD,QAAI,qBAAqB,oBAAqB;AAC9C,QAAI,qBAAqB;AACvB,gCAA0B,qBAAqB,mBAAmB;AAClE,oBAAc,iCAAiC,qBAAqB,mBAAmB,CAAC;AAAA,IAC1F;AACA,0BAAsB;AACtB,QAAI,qBAAqB;AACvB,+BAAyB,qBAAqB,mBAAmB;AAAA,IACnE;AAAA,EACF;AACA,gCAA8B;AAE9B,MAAI,UAA4B;AAChC,MAAI,aAAa;AACjB,MAAI,QAA+B;AACnC,MAAI,eAAiC;AACrC,MAAI,iBAAiB;AACrB,MAAI,aAAoC;AACxC,MAAI,gBAAuC;AAE3C,QAAM,YAAY,MAAM,QAAQ,SAAS,MAAM;AAC/C,QAAM,eAAe,YAAY;AAC/B,UAAM,KAAK,UAAU;AACrB,QAAI,CAAC,GAAI;AACT,UAAM,WAAW,QAAQ,KAAK,WAAW;AACzC,QAAI,OAAO,aAAa,YAAY,UAAU;AAC5C,YAAM,OAAO,MAAM,SAAS,QAAQ;AACpC,UAAI,KAAM,OAAM,2BAA2B,QAAQ,aAAa,KAAK,OAAO,EAAE;AAAA,IAChF;AACA,UAAM,WAAW,QAAQ,KAAK,WAAW;AACzC,QAAI,OAAO,aAAa,YAAY,UAAU;AAC5C,YAAM,QAAQ,MAAM,UAAU,QAAQ;AACtC,UAAI,MAAO,OAAM,4BAA4B,QAAQ,aAAa,MAAM,OAAO,EAAE;AAAA,IACnF;AAAA,EACF;AAEA,QAAM,eAAe,YAAY;AAC/B,QAAI,CAAC,eAAgB;AACrB,UAAM,QAAQ,MAAM,SAAS,QAAQ,aAAa,cAAc;AAChE,QAAI,MAAO,gCAA+B,SAAS,KAAK;AAAA,EAC1D;AAEA,QAAM,wBAAwB,YAAY;AACxC,UAAM,KAAK,UAAU;AACrB,UAAM,QAAQ,KAAK,MAAM,yBAAyB,QAAQ,aAAa,EAAE,IAAI;AAC7E,QAAI,CAAC,SAAS,MAAM,OAAO,eAAgB;AAC3C,kBAAc,MAAM;AACpB,mBAAe;AACf,qBAAiB,MAAM;AACvB,QAAI;AACF,YAAM,gBAAgB,GAAG,MAAM,EAAE;AACjC,qBAAe;AAAA,QACb,aAAa,QAAQ,WAAW;AAAA,QAChC,EAAE,YAAY,MAAM;AAAA,QACpB,CAAC,QAAQ,aAAa;AACpB,cAAI,UAAU,SAAS,MAAM,cAAe;AAC5C,cAAI,WAAY,cAAa,UAAU;AACvC,uBAAa,WAAW,MAAM,cAAc,aAAa,CAAC,GAAG,EAAE;AAAA,QACjE;AAAA,MACF;AACA,YAAM,gBAAgB,MACpB,oBAAoB,QAAQ,aAAa,MAAM,IAAI;AAAA,QACjD,WAAW;AAAA,QACX,SAAS,QAAQ;AAAA,QACjB,WAAW,QAAQ;AAAA,MACrB,CAAC;AACH,oBAAc,cAAc,CAAC;AAC7B,UAAI,cAAe,eAAc,aAAa;AAC9C,sBAAgB,YAAY,MAAM,cAAc,cAAc,CAAC,GAAG,GAAM;AACxE,oBAAc,QAAQ;AACtB,mBAAa,GAAG,SAAS,MAAM;AAC7B,sBAAc,MAAM;AACpB,uBAAe;AACf,yBAAiB;AAAA,MACnB,CAAC;AAAA,IACH,QAAQ;AACN,qBAAe;AACf,uBAAiB;AAAA,IACnB;AAAA,EACF;AAEA,QAAM,mBAAmB,MAAM;AAC7B,UAAM,WAAW,QAAQ,KAAK,WAAW;AACzC,UAAM,WAAW,QAAQ,KAAK,WAAW;AACzC,UAAM,YACJ,OAAO,aAAa,YAAY,WAC5B,QAAQ,QAAQ,IAChB,OAAO,aAAa,YAAY,WAC9B,QAAQ,QAAQ,IAChB;AACR,QAAI,CAAC,aAAa,cAAc,WAAY;AAC5C,aAAS,MAAM;AACf,cAAU;AACV,iBAAa;AACb,QAAI;AACF,gBAAU,MAAM,WAAW,EAAE,YAAY,MAAM,GAAG,CAAC,QAAQ,aAAa;AACtE,cAAM,OAAO,UAAU,SAAS;AAChC,cAAM,kBAAkB,QAAQ,KAAK,WAAW;AAChD,cAAM,kBAAkB,QAAQ,KAAK,WAAW;AAChD,cAAM,WAAW,OAAO,oBAAoB,WAAW,SAAS,eAAe,IAAI;AACnF,cAAM,WAAW,OAAO,oBAAoB,WAAW,SAAS,eAAe,IAAI;AACnF,YAAI,QAAQ,SAAS,YAAY,SAAS,SAAU;AACpD,YAAI,MAAO,cAAa,KAAK;AAC7B,gBAAQ,WAAW,MAAM,cAAc,aAAa,CAAC,GAAG,EAAE;AAAA,MAC5D,CAAC;AACD,cAAQ,GAAG,SAAS,MAAM,SAAS,MAAM,CAAC;AAAA,IAC5C,QAAQ;AAGN,gBAAU;AACV,mBAAa;AAAA,IACf;AAAA,EACF;AAEA,QAAM,cAAc,QAAQ,MAAM,SAAS,CAAC,WAAW;AACrD,QAAI,OAAO,SAAS,oBAAoB,CAAC,sBAAsB,IAAI,OAAO,GAAG;AAG3E;AAAA,QACE,QAAQ;AAAA,QACR,OAAO,qBAAqB,OAAO;AAAA,QACnC,UAAU;AAAA,MACZ;AACA;AAAA,IACF;AACA,QAAI,OAAO,SAAS,eAAe,OAAO,QAAQ,eAAe,OAAO,QAAQ,cAAc;AAC5F,oCAA8B;AAC9B,uBAAiB;AACjB,oBAAc,yBAAyB,QAAQ,aAAa,UAAU,CAAC,CAAC;AACxE,oBAAc,sBAAsB,CAAC;AACrC,oBAAc,aAAa,CAAC;AAAA,IAC9B;AAAA,EACF,CAAC;AAED,mBAAiB;AACjB,gBAAc,sBAAsB,CAAC;AAErC,QAAM,SAAS,MAAM;AACnB,gBAAY;AACZ,QAAI,MAAO,cAAa,KAAK;AAC7B,QAAI,WAAY,cAAa,UAAU;AACvC,QAAI,cAAe,eAAc,aAAa;AAC9C,aAAS,MAAM;AACf,kBAAc,MAAM;AACpB,aAAS,OAAO,OAAO;AACvB,QAAI,uBAAuB,qBAAqB;AAC9C,gCAA0B,qBAAqB,mBAAmB;AAClE,oBAAc,iCAAiC,qBAAqB,mBAAmB,CAAC;AACxF,4BAAsB;AAAA,IACxB;AAAA,EACF;AACA,WAAS,IAAI,SAAS,MAAM;AAC5B,SAAO;AACT;AAGA,eAAsB,qBAAqB,SAA+C;AACxF,QAAM,KAAK,QAAQ,SAAS,MAAM;AAClC,MAAI,CAAC,GAAI,QAAO;AAChB,QAAM,gCAAgC,QAAQ,aAAa,EAAE;AAC7D,MAAI,QAAQ,MAAM,yBAAyB,QAAQ,aAAa,EAAE;AAClE,MAAI,QAAQ,MAAM,QAAQ;AACxB,YAAQ,MAAM,4BAA4B,QAAQ,aAAa,QAAQ,OAAO,EAAE;AAAA,EAClF;AACA,QAAM,WAAW,QAAQ,KAAK,WAAW;AACzC,MAAI,OAAO,aAAa,YAAY,UAAU;AAC5C,UAAM,OAAO,MAAM,SAAS,QAAQ;AACpC,QAAI,KAAM,SAAQ,MAAM,2BAA2B,QAAQ,aAAa,KAAK,OAAO,EAAE;AAAA,EACxF;AACA,QAAM,WAAW,QAAQ,KAAK,WAAW;AACzC,MAAI,OAAO,aAAa,YAAY,UAAU;AAC5C,UAAM,QAAQ,MAAM,UAAU,QAAQ;AACtC,QAAI,MAAO,SAAQ,MAAM,4BAA4B,QAAQ,aAAa,MAAM,OAAO,EAAE;AAAA,EAC3F;AACA,SAAO;AACT;AASA,SAAS,aAAa,MAA8B;AAClD,MAAI,KAAK,WAAW,YAAa,QAAO;AACxC,MAAI,KAAK,WAAW,cAAe,QAAO;AAC1C,MAAI,KAAK,WAAW,SAAU,QAAO;AACrC,MAAI,KAAK,WAAW,UAAW,QAAO;AACtC,MAAI,KAAK,WAAW,SAAU,QAAO;AACrC,SAAO;AACT;AAEA,SAAS,WAAW,MAAsC;AACxD,QAAM,SAAS,aAAa,IAAI;AAChC,MAAI,WAAW,YAAa,QAAO;AACnC,MAAI,WAAW,iBAAiB,WAAW,SAAU,QAAO;AAC5D,SAAO;AACT;AAEA,SAAS,oBAAoB,MAA4B;AACvD,SAAO;AAAA,IACL,IAAI,KAAK,QAAQ,UAAU,KAAK;AAAA,IAChC,SAAS,KAAK;AAAA,IACd,QAAQ,WAAW,IAAI;AAAA,IACvB,GAAI,KAAK,cAAc,EAAE,YAAY,KAAK,YAAY,IAAI,CAAC;AAAA,EAC7D;AACF;AAEA,SAAS,UAAU,MAA2B,OAAqC;AACjF,SACE,KAAK,WAAW,MAAM,UACtB,KAAK,MAAM,CAAC,MAAM,UAAU;AAC1B,UAAM,YAAY,MAAM,KAAK;AAC7B,WACE,WAAW,OAAO,KAAK,MACvB,UAAU,YAAY,KAAK,WAC3B,UAAU,WAAW,KAAK,UAC1B,UAAU,eAAe,KAAK,cAC9B,UAAU,qBAAqB,KAAK,oBACpC,UAAU,qBAAqB,KAAK;AAAA,EAExC,CAAC;AAEL;AAUO,SAAS,+BAA+B,SAAkB,OAAgC;AAC/F,QAAM,YAAY,QAAQ,SAAS,MAAM;AACzC,MAAI,CAAC,aAAa,kBAAkB,MAAM,IAAI,MAAM,aAAa,CAAC,oBAAoB,MAAM,IAAI,GAAG;AACjG,WAAO,CAAC,GAAG,QAAQ,KAAK;AAAA,EAC1B;AAEA,QAAM,iBAAiB,MAAM,MAC1B;AAAA,IACC,CAAC,SACC,KAAK,WAAW,eACf,CAAC,KAAK,UACL,KAAK,OAAO,WAAW,mBACtB,KAAK,OAAO,WAAW,IAAI,WAAW,OAAO;AAAA,EACpD,EACC,KAAK,CAAC,MAAM,UAAU;AACrB,UAAM,aAAa,MAAM,QAAQ,KAAK,CAAC,WAAW,OAAO,OAAO,KAAK,QAAQ,GAAG,SAAS;AACzF,UAAM,cAAc,MAAM,QAAQ,KAAK,CAAC,WAAW,OAAO,OAAO,MAAM,QAAQ,GAAG,SAAS;AAC3F,WAAO,aAAa,eAAe,KAAK,QAAQ,MAAM,SAAS,KAAK,UAAU,cAAc,MAAM,SAAS;AAAA,EAC7G,CAAC,EACA,IAAI,mBAAmB;AAE1B,QAAM,eACJ,eAAe,SAAS,KAAK,eAAe,MAAM,CAAC,SAAS,KAAK,WAAW,WAAW;AACzF,QAAM,iBAAiB,eAAe,CAAC,IAAI;AAC3C,MAAI,UAAU,QAAQ,OAAO,cAAc,EAAG,QAAO,CAAC,GAAG,QAAQ,KAAK;AACtE,wBAAsB,IAAI,OAAO;AACjC,MAAI;AACF,YAAQ,MAAM,aAAa,cAAc;AAAA,EAC3C,UAAE;AACA,0BAAsB,OAAO,OAAO;AAAA,EACtC;AACA,mBAAiB,SAAS,QAAQ,KAAK;AACvC,sBAAoB,SAAS,QAAQ,KAAK;AAC1C,SAAO,CAAC,GAAG,QAAQ,KAAK;AAC1B;AAGA,eAAsB,+BACpB,SACA,MACA,UAA4C,CAAC,GACT;AACpC,QAAM,WAAW,KAAK,QAAQ;AAC9B,QAAM,UAAU,KAAK,QAAQ,WAAW;AACxC,MAAI,CAAC,SAAU,QAAO,EAAE,QAAQ,KAAK;AAErC,MAAI,KAAK,QAAQ,WAAW,kBAAkB,QAAQ,WAAW,OAAO,GAAG;AACzE,UAAM,OAAO,QAAQ,SACjB,QAAQ,MAAM,OAAO,CAAC,SAAS,KAAK,OAAO,QAAQ,IACnD,QAAQ,MAAM;AAAA,MAAI,CAAC,SACjB,KAAK,OAAO,WACR;AAAA,QACE,GAAG;AAAA,QACH,SAAS,KAAK;AAAA,QACd,QACE,aAAa,IAAI,MAAM,cAClB,cACD,aAAa,IAAI,MAAM,iBAAiB,aAAa,IAAI,MAAM,WAC5D,gBACA;AAAA,MACX,IACA;AAAA,IACN;AACJ,0BAAsB,IAAI,OAAO;AACjC,QAAI;AACF,cAAQ,MAAM,aAAa,IAAI;AAAA,IACjC,UAAE;AACA,4BAAsB,OAAO,OAAO;AAAA,IACtC;AACA,WAAO,EAAE,QAAQ,QAAQ,OAAO,CAAC,GAAG,QAAQ,KAAK,EAAE;AAAA,EACrD;AAEA,QAAM,KAAK,QAAQ,SAAS,MAAM;AAClC,MAAI,KAAK,QAAQ,WAAW,kBAAkB,QAAQ,WAAW,OAAO,GAAG;AACzE,UAAM,WAAW,QAAQ,KAAK,WAAW;AACzC,QAAI,OAAO,aAAa,YAAY,CAAC,SAAU,QAAO,EAAE,QAAQ,OAAO;AACvE,UAAM,OAAO,MAAM,WAAW,UAAU,IAAI,CAAC,UAAU;AAAA,MACrD,GAAG;AAAA,MACH,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,OAAO,QAAQ,SACX,KAAK,MAAM,OAAO,CAAC,SAAS,KAAK,OAAO,QAAQ,IAChD,KAAK,MAAM;AAAA,QAAI,CAAC,SACd,KAAK,OAAO,WACR;AAAA,UACE,GAAG;AAAA,UACH,OAAO,KAAK;AAAA,UACZ,SAAS,KAAK;AAAA,UACd,QACE,KAAK,WAAW,cACX,SACD,KAAK,WAAW,iBAAiB,KAAK,WAAW,WAC9C,gBACA;AAAA,UACT,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC,IACA;AAAA,MACN;AAAA,IACN,EAAE;AACF,WAAO,EAAE,QAAQ,QAAQ,KAAK;AAAA,EAChC;AAEA,MACE,KAAK,QAAQ,WAAW,kBACxB,KAAK,QAAQ,WAAW,aACxB,QAAQ,WAAW,UAAU,GAC7B;AACA,UAAM,WAAW,QAAQ,KAAK,WAAW;AACzC,QAAI,OAAO,aAAa,YAAY,CAAC,SAAU,QAAO,EAAE,QAAQ,OAAO;AACvE,UAAM,QAAQ,MAAM,YAAY,UAAU,IAAI,CAAC,UAAU;AAAA,MACvD,GAAG;AAAA,MACH,OAAO,QAAQ,SACX,KAAK,MAAM,OAAO,CAAC,SAAS,KAAK,OAAO,QAAQ,IAChD,KAAK,MAAM;AAAA,QAAI,CAAC,SACd,KAAK,OAAO,WACR;AAAA,UACE,GAAG;AAAA,UACH,OAAO,KAAK;AAAA,UACZ,aAAa,KAAK;AAAA,UAClB,QAAQ,aAAa,IAAI;AAAA,UACzB,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC,IACA;AAAA,MACN;AAAA,IACN,EAAE;AACF,WAAO,EAAE,QAAQ,QAAQ,MAAM;AAAA,EACjC;AAEA,SAAO,EAAE,QAAQ,KAAK;AACxB;",
6
6
  "names": ["board"]
7
7
  }
package/dist/task.js CHANGED
@@ -16,17 +16,21 @@ import { randomUUID } from "node:crypto";
16
16
  // src/session-kanban.ts
17
17
  import {
18
18
  deserializeTaskGraph,
19
+ GlobalMailbox,
19
20
  loadPlan,
20
21
  loadTasks,
21
22
  mutatePlan,
22
23
  mutateTasks
23
24
  } from "@wrongstack/core";
25
+ import { resolveWstackPaths } from "@wrongstack/core/utils";
24
26
  import {
25
27
  createBoard,
26
28
  getBoard,
29
+ getKanbanDir,
27
30
  listBoards,
28
31
  removeBoard,
29
32
  syncBoardFromTaskGraph,
33
+ touchKanbanPresence,
30
34
  updateBoard
31
35
  } from "@wrongstack/kanban";
32
36
  var SESSION_BOARD_TAG = "session-work";