@wrongstack/tools 0.306.3 → 0.307.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/audit.js +50 -9
  2. package/dist/auto-proceed-loop-guard.js +8 -2
  3. package/dist/bash.js +57 -22
  4. package/dist/builtin.d.ts +6 -6
  5. package/dist/builtin.js +11315 -9317
  6. package/dist/clarify.d.ts +32 -0
  7. package/dist/codebase-index/ast-invariant-engine.d.ts +102 -0
  8. package/dist/codebase-index/ast-symbol-mutator.d.ts +28 -0
  9. package/dist/codebase-index/background-indexer.d.ts +9 -0
  10. package/dist/codebase-index/codebase-ast-replace-tool.d.ts +31 -0
  11. package/dist/codebase-index/codebase-impact-analysis-tool.d.ts +40 -0
  12. package/dist/codebase-index/codebase-invariant-check-tool.d.ts +23 -0
  13. package/dist/codebase-index/codebase-repo-map-tool.d.ts +22 -0
  14. package/dist/codebase-index/codebase-skeleton-tool.d.ts +36 -0
  15. package/dist/codebase-index/codebase-targeted-test-tool.d.ts +30 -0
  16. package/dist/codebase-index/index.d.ts +11 -1
  17. package/dist/codebase-index/index.js +4141 -1479
  18. package/dist/codebase-index/project-server-client-state.d.ts +79 -0
  19. package/dist/codebase-index/project-server-client.d.ts +3 -71
  20. package/dist/codebase-index/project-server-protocol.d.ts +1 -0
  21. package/dist/codebase-index/project-server.js +751 -909
  22. package/dist/codebase-index/repo-map.d.ts +20 -0
  23. package/dist/codebase-index/skeleton-extractor.d.ts +64 -0
  24. package/dist/codebase-index/tree-sitter-parser.d.ts +11 -0
  25. package/dist/codebase-index/worker.js +723 -891
  26. package/dist/codebase-index/writer-mutations.d.ts +26 -0
  27. package/dist/codebase-index/writer-refs.d.ts +50 -0
  28. package/dist/codebase-index/writer-search.d.ts +30 -0
  29. package/dist/codebase-index/writer.d.ts +2 -320
  30. package/dist/diff.js +3 -2
  31. package/dist/document.js +37 -2
  32. package/dist/e2e.js +3 -2
  33. package/dist/edit.js +8558 -348
  34. package/dist/exec.js +76 -22
  35. package/dist/fetch.js +11 -9
  36. package/dist/format.js +50 -9
  37. package/dist/glob.js +8 -3
  38. package/dist/grep.js +22 -10
  39. package/dist/index.d.ts +3 -1
  40. package/dist/index.js +11411 -9440
  41. package/dist/install.js +50 -9
  42. package/dist/json.js +91 -17
  43. package/dist/kanban-board-actions.d.ts +4 -0
  44. package/dist/kanban-lifecycle-actions.d.ts +4 -0
  45. package/dist/kanban-serializer.d.ts +21 -0
  46. package/dist/kanban.js +985 -1034
  47. package/dist/languages/index.js +17 -10
  48. package/dist/lint.js +50 -9
  49. package/dist/logs.js +17 -6
  50. package/dist/next-steps.d.ts +8 -0
  51. package/dist/next-steps.js +18 -0
  52. package/dist/outdated.js +18 -11
  53. package/dist/pack.js +11308 -9317
  54. package/dist/patch.js +8301 -77
  55. package/dist/plan.js +1231 -1281
  56. package/dist/process-registry.js +14 -8
  57. package/dist/ps-slash.js +65 -30
  58. package/dist/read.js +754 -912
  59. package/dist/replace.js +8456 -195
  60. package/dist/scaffold.js +36 -1
  61. package/dist/search.js +21 -15
  62. package/dist/security-ast-scan-tool.d.ts +43 -0
  63. package/dist/session-kanban-graph.d.ts +9 -0
  64. package/dist/session-kanban-sync.d.ts +31 -0
  65. package/dist/session-kanban.d.ts +5 -141
  66. package/dist/session-kanban.js +367 -360
  67. package/dist/task.js +1157 -1207
  68. package/dist/test.js +50 -9
  69. package/dist/todo.js +2187 -2237
  70. package/dist/tool-diff.js +6 -1
  71. package/dist/tool-summary.js +4 -2
  72. package/dist/tool-tier.js +11315 -9317
  73. package/dist/tree.js +36 -1
  74. package/dist/typecheck.js +51 -10
  75. package/dist/write.js +8374 -152
  76. package/package.json +7 -7
@@ -1,22 +1,14 @@
1
1
  // src/session-kanban.ts
2
2
  import { watch } from "node:fs";
3
3
  import { basename, dirname } from "node:path";
4
- import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
5
- import {
6
- loadPlan,
7
- loadTasks,
8
- mutatePlan,
9
- mutateTasks
10
- } from "@wrongstack/core/storage";
4
+ import { loadPlan, loadTasks } from "@wrongstack/core/storage";
11
5
  import { deserializeTaskGraph } from "@wrongstack/core/tasking";
12
- import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
13
6
  import {
14
7
  bridgeKanbanSupervisor,
15
8
  compactSessionMirrorBoard,
16
9
  createBoard,
17
10
  DEFAULT_COLUMNS,
18
11
  getBoard,
19
- getDependencyReadinessIssues,
20
12
  getKanbanOrchestrationSnapshot,
21
13
  listBoards,
22
14
  pruneSessionBoards,
@@ -25,6 +17,350 @@ import {
25
17
  touchKanbanPresence,
26
18
  updateBoard
27
19
  } from "@wrongstack/kanban";
20
+
21
+ // src/session-kanban-graph.ts
22
+ var PLAN_STATUS_TO_TASK = {
23
+ open: "pending",
24
+ in_progress: "in_progress",
25
+ done: "completed"
26
+ };
27
+ function todoListToSerializedGraph(todos, sessionId) {
28
+ const graphId = `todo:${sessionId}`;
29
+ const nodes = todos.map((todo, index) => ({
30
+ id: todo.id,
31
+ title: todo.content,
32
+ description: todo.activeForm ?? "",
33
+ type: "chore",
34
+ priority: "medium",
35
+ status: todo.status,
36
+ specRequirementId: `${graphId}:${todo.id}`,
37
+ createdAt: index,
38
+ updatedAt: index
39
+ }));
40
+ return {
41
+ id: graphId,
42
+ specId: graphId,
43
+ requiredRequirementIds: nodes.map((node) => node.specRequirementId),
44
+ title: "Session todos",
45
+ nodes,
46
+ edges: [],
47
+ rootNodes: nodes.map((node) => node.id),
48
+ createdAt: 0,
49
+ updatedAt: 0
50
+ };
51
+ }
52
+ function taskFileToSerializedGraph(tasks, sessionId) {
53
+ const graphId = `session:${sessionId}`;
54
+ const ids = new Set(tasks.map((task) => task.id));
55
+ const nodes = tasks.map((task, index) => ({
56
+ id: task.id,
57
+ title: task.title,
58
+ description: task.description ?? "",
59
+ type: task.type,
60
+ priority: task.priority,
61
+ status: task.status,
62
+ specRequirementId: `${graphId}:${task.id}`,
63
+ ...task.assignee ? { assignee: task.assignee } : {},
64
+ ...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
65
+ createdAt: index,
66
+ updatedAt: index
67
+ }));
68
+ const edges = tasks.flatMap(
69
+ (task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
70
+ id: `${dependency}->${task.id}`,
71
+ from: dependency,
72
+ to: task.id,
73
+ type: "depends_on"
74
+ }))
75
+ );
76
+ const hasIncoming = new Set(edges.map((edge) => edge.to));
77
+ const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
78
+ return {
79
+ // Keep the historical graph id so existing mirrored task cards are reused.
80
+ id: graphId,
81
+ specId: graphId,
82
+ requiredRequirementIds: nodes.map((node) => node.specRequirementId),
83
+ title: "Session tasks",
84
+ nodes,
85
+ edges,
86
+ rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
87
+ createdAt: 0,
88
+ updatedAt: 0
89
+ };
90
+ }
91
+ function planFileToSerializedGraph(items, sessionId) {
92
+ const graphId = `plan:${sessionId}`;
93
+ const nodes = items.map((item, index) => ({
94
+ id: item.id,
95
+ title: item.title,
96
+ description: item.details ?? "",
97
+ type: "chore",
98
+ priority: "medium",
99
+ status: PLAN_STATUS_TO_TASK[item.status],
100
+ specRequirementId: `${graphId}:${item.id}`,
101
+ createdAt: index,
102
+ updatedAt: index
103
+ }));
104
+ return {
105
+ id: graphId,
106
+ specId: graphId,
107
+ requiredRequirementIds: nodes.map((node) => node.specRequirementId),
108
+ title: "Session plan",
109
+ nodes,
110
+ edges: [],
111
+ rootNodes: nodes.map((node) => node.id),
112
+ createdAt: 0,
113
+ updatedAt: 0
114
+ };
115
+ }
116
+
117
+ // src/session-kanban-sync.ts
118
+ import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
119
+ import { mutatePlan, mutateTasks } from "@wrongstack/core/storage";
120
+ import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
121
+ import {
122
+ getDependencyReadinessIssues
123
+ } from "@wrongstack/kanban";
124
+ function sourceStatus(task) {
125
+ if (task.status === "completed") return "completed";
126
+ if (task.status === "in_progress") return "in_progress";
127
+ if (task.status === "review") return "review";
128
+ if (task.status === "blocked") return "blocked";
129
+ if (task.status === "failed") return "failed";
130
+ return "pending";
131
+ }
132
+ function todoStatus(task) {
133
+ const status = sourceStatus(task);
134
+ if (status === "completed") return "completed";
135
+ if (status === "review" && task.assignment?.status === "completed") return "completed";
136
+ if (status === "in_progress" || status === "review") return "in_progress";
137
+ return "pending";
138
+ }
139
+ function sessionTodoFromTask(task, board) {
140
+ const blockedBy = board ? blockingTitles(board, task) : [];
141
+ return {
142
+ id: task.origin?.taskId ?? task.id,
143
+ content: task.title,
144
+ status: todoStatus(task),
145
+ ...task.description ? { activeForm: task.description } : {},
146
+ ...blockedBy.length ? { blockedBy } : {}
147
+ };
148
+ }
149
+ function managedTodoFromTask(task, board) {
150
+ return {
151
+ ...sessionTodoFromTask(task, board),
152
+ kanbanBoardId: board.id,
153
+ kanbanTaskId: task.id
154
+ };
155
+ }
156
+ function blockingTitles(board, task) {
157
+ return getDependencyReadinessIssues(board, task).map((issue) => {
158
+ const dependency = board.tasks.find((candidate) => candidate.id === issue.dependencyId);
159
+ if (!dependency) return `${issue.dependencyId} (missing)`;
160
+ return dependency.title;
161
+ });
162
+ }
163
+ var PRIORITY_ORDER = {
164
+ critical: 0,
165
+ high: 1,
166
+ medium: 2,
167
+ low: 3
168
+ };
169
+ function orderTasksForTodos(board, tasks) {
170
+ const columnOrder = new Map(board.columns.map((column) => [column.id, column.order]));
171
+ const baseline = [...tasks].sort(
172
+ (left, right) => (columnOrder.get(left.columnId) ?? 0) - (columnOrder.get(right.columnId) ?? 0) || (PRIORITY_ORDER[left.priority] ?? 2) - (PRIORITY_ORDER[right.priority] ?? 2) || left.order - right.order || left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id)
173
+ );
174
+ const included = new Set(baseline.map((task) => task.id));
175
+ const remaining = new Map(baseline.map((task) => [task.id, task]));
176
+ const emitted = [];
177
+ const done = /* @__PURE__ */ new Set();
178
+ while (remaining.size > 0) {
179
+ const ready = baseline.filter(
180
+ (task) => remaining.has(task.id) && (task.dependsOn ?? []).every(
181
+ (dependencyId) => !included.has(dependencyId) || done.has(dependencyId)
182
+ )
183
+ );
184
+ if (ready.length === 0) break;
185
+ for (const task of ready) {
186
+ remaining.delete(task.id);
187
+ done.add(task.id);
188
+ emitted.push(task);
189
+ }
190
+ }
191
+ for (const task of baseline) if (remaining.has(task.id)) emitted.push(task);
192
+ return emitted;
193
+ }
194
+ function sameTodos(left, right) {
195
+ return left.length === right.length && left.every((todo, index) => {
196
+ const candidate = right[index];
197
+ return candidate?.id === todo.id && candidate.content === todo.content && candidate.status === todo.status && candidate.activeForm === todo.activeForm && candidate.promotedFromPlan === todo.promotedFromPlan && candidate.promotedFromTask === todo.promotedFromTask && candidate.kanbanBoardId === todo.kanbanBoardId && candidate.kanbanTaskId === todo.kanbanTaskId && (candidate.blockedBy ?? []).join("\0") === (todo.blockedBy ?? []).join("\0");
198
+ });
199
+ }
200
+ function broadcastTodoUpdate(context, todos) {
201
+ const sessionId = context.session?.id ?? "";
202
+ if (!context.agentId || !sessionId) return;
203
+ const statusCounts = { pending: 0, inProgress: 0, completed: 0 };
204
+ for (const todo of todos) {
205
+ if (todo.status === "completed") statusCounts.completed++;
206
+ else if (todo.status === "in_progress") statusCounts.inProgress++;
207
+ else statusCounts.pending++;
208
+ }
209
+ const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;
210
+ const mailbox = getSharedProjectMailbox(projectDir);
211
+ void mailbox.send({
212
+ from: context.agentId,
213
+ to: "*",
214
+ type: "status",
215
+ subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
216
+ body: JSON.stringify({
217
+ kind: "kanban.todos.updated",
218
+ sessionId,
219
+ revision: context.state.revision,
220
+ todoCount: todos.length,
221
+ statusCounts
222
+ }),
223
+ priority: "normal",
224
+ senderSessionId: sessionId
225
+ }).catch(() => {
226
+ });
227
+ }
228
+ function notifyTodoUpdate(context, todos) {
229
+ const summary = formatTodosForModel(todos);
230
+ const text = `[KANBAN TODO UPDATE]
231
+ Another Kanban agent reassessed the shared board. The canonical todo list is now:
232
+ ${summary}
233
+ Reassess your current plan before continuing; do not rely on the initial todo snapshot. Preserve each row's <kanban board/task> binding verbatim on your next \`todo\` call \u2014 a row that loses it stops advancing its card.`;
234
+ const state = context.state;
235
+ if (typeof state.appendBlockToLastUserMessage === "function") {
236
+ if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
237
+ }
238
+ if (typeof state.appendMessage === "function") {
239
+ state.appendMessage({ role: "user", content: [{ type: "text", text }] });
240
+ }
241
+ }
242
+ function todosNeedingSessionMirror(todos, activeBoardId) {
243
+ if (!activeBoardId) return todos;
244
+ return todos.filter((todo) => todo.kanbanBoardId !== activeBoardId || !todo.kanbanTaskId);
245
+ }
246
+ function applySessionKanbanBoardToTodos(context, board, options) {
247
+ const sessionId = context.session?.id ?? "";
248
+ if (!sessionId || options.sessionIdFromTags(board.tags) !== sessionId || !options.isOwnedSessionBoard(board.tags)) {
249
+ return [...context.todos];
250
+ }
251
+ if (options.hasInFlightTodoMirror(context.projectRoot, sessionId)) return [...context.todos];
252
+ const projectedTodos = orderTasksForTodos(
253
+ board,
254
+ board.tasks.filter(
255
+ (task) => task.status !== "archived" && (!task.origin || task.origin.system === "session-todo" || (task.origin.graphId ?? "").startsWith("todo:"))
256
+ )
257
+ ).map((task) => sessionTodoFromTask(task, board));
258
+ const allCompleted = projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === "completed");
259
+ const effectiveTodos = allCompleted ? [] : projectedTodos;
260
+ if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];
261
+ options.suppressedTodoMirrors.add(context);
262
+ try {
263
+ context.state.replaceTodos(projectedTodos);
264
+ } finally {
265
+ options.suppressedTodoMirrors.delete(context);
266
+ }
267
+ notifyTodoUpdate(context, context.todos);
268
+ broadcastTodoUpdate(context, context.todos);
269
+ return [...context.todos];
270
+ }
271
+ function applyManagedKanbanBoardToTodos(context, board, suppressedTodoMirrors2) {
272
+ const metaKanban = context.meta["kanban"];
273
+ const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
274
+ const activeBoardId = context.currentKanbanBoardId ?? (typeof metaBoardId === "string" ? metaBoardId : void 0);
275
+ if (!activeBoardId || board.id !== activeBoardId || board.lifecycle?.mode !== "managed") {
276
+ return [...context.todos];
277
+ }
278
+ const projectedTodos = orderTasksForTodos(
279
+ board,
280
+ board.tasks.filter(
281
+ (task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
282
+ )
283
+ ).map((task) => managedTodoFromTask(task, board));
284
+ if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
285
+ suppressedTodoMirrors2.add(context);
286
+ try {
287
+ context.state.replaceTodos(projectedTodos);
288
+ } finally {
289
+ suppressedTodoMirrors2.delete(context);
290
+ }
291
+ notifyTodoUpdate(context, context.todos);
292
+ broadcastTodoUpdate(context, context.todos);
293
+ return [...context.todos];
294
+ }
295
+ async function applySessionKanbanTaskToSource(context, task, suppressedTodoMirrors2, options = {}) {
296
+ const originId = task.origin?.taskId;
297
+ const graphId = task.origin?.graphId ?? "";
298
+ if (!originId) return { source: null };
299
+ if (task.origin?.system === "session-todo" || graphId.startsWith("todo:")) {
300
+ const mappedStatus = todoStatus(task);
301
+ const next = options.remove ? context.todos.filter((todo) => todo.id !== originId) : context.todos.map(
302
+ (todo) => todo.id === originId ? {
303
+ ...todo,
304
+ content: task.title,
305
+ status: mappedStatus
306
+ } : todo
307
+ );
308
+ suppressedTodoMirrors2.add(context);
309
+ try {
310
+ context.state.replaceTodos(next);
311
+ } finally {
312
+ suppressedTodoMirrors2.delete(context);
313
+ }
314
+ return { source: "todo", todos: [...context.todos] };
315
+ }
316
+ const id = context.session?.id ?? "";
317
+ if (task.origin?.system === "session-plan" || graphId.startsWith("plan:")) {
318
+ const planPath = context.meta["plan.path"];
319
+ if (typeof planPath !== "string" || !planPath) {
320
+ throw new Error(
321
+ "Cannot reflect this Kanban edit back to its plan source: the session has no plan.path configured. The board mutation already succeeded; the plan file is now out of sync."
322
+ );
323
+ }
324
+ const plan = await mutatePlan(planPath, id, (file) => ({
325
+ ...file,
326
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
327
+ items: options.remove ? file.items.filter((item) => item.id !== originId) : file.items.map(
328
+ (item) => item.id === originId ? {
329
+ ...item,
330
+ title: task.title,
331
+ details: task.description,
332
+ status: task.status === "completed" ? "done" : task.status === "in_progress" || task.status === "review" ? "in_progress" : "open",
333
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
334
+ } : item
335
+ )
336
+ }));
337
+ return { source: "plan", plan };
338
+ }
339
+ if (task.origin?.system === "session-task" || task.origin?.system === "session" || graphId.startsWith("session:")) {
340
+ const taskPath = context.meta["task.path"];
341
+ if (typeof taskPath !== "string" || !taskPath) {
342
+ throw new Error(
343
+ "Cannot reflect this Kanban edit back to its task source: the session has no task.path configured. The board mutation already succeeded; the task file is now out of sync."
344
+ );
345
+ }
346
+ const tasks = await mutateTasks(taskPath, id, (file) => ({
347
+ ...file,
348
+ tasks: options.remove ? file.tasks.filter((item) => item.id !== originId) : file.tasks.map(
349
+ (item) => item.id === originId ? {
350
+ ...item,
351
+ title: task.title,
352
+ description: task.description,
353
+ status: sourceStatus(task),
354
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
355
+ } : item
356
+ )
357
+ }));
358
+ return { source: "task", tasks };
359
+ }
360
+ return { source: null };
361
+ }
362
+
363
+ // src/session-kanban.ts
28
364
  var SESSION_BOARD_TAG = "session-work";
29
365
  var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
30
366
  var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
@@ -223,15 +559,6 @@ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
223
559
  tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
224
560
  archiveMissingTasks: true,
225
561
  includeCompletedTasks: true,
226
- // The scope ledger stays declared and accurate, but it may not veto a
227
- // projection. A session mirror reflects a tactical list that shrinks by
228
- // design, and refusing the sync never protected the removed row — it
229
- // froze the entire board, permanently, because the stored scope then
230
- // outlived every later snapshot (`session-kanban.mirror-failed`).
231
- // Nothing is lost by shrinking here: `archiveMissingTasks` keeps the
232
- // removed card on the board as `archived`, the reconciliation pass
233
- // first walks vanished completed rows to Done, and the session journal
234
- // remains the durable record.
235
562
  allowRequirementScopeShrink: true
236
563
  }
237
564
  );
@@ -323,106 +650,12 @@ function takeSessionMirrorFailure(projectRoot, sessionId) {
323
650
  const failure = mirrorFailures.get(key);
324
651
  if (!failure) return void 0;
325
652
  mirrorFailures.delete(key);
326
- return `Kanban mirror (${failure.sourceSystem}) failed and the board may be stale: ${failure.message}`;
327
- }
328
- function hasInFlightTodoMirror(projectRoot, sessionId) {
329
- if (!projectRoot || !sessionId) return false;
330
- const key = mirrorKey(projectRoot, sessionId, "session-todo");
331
- return pendingMirrors.has(key) || activeMirrors.has(key);
332
- }
333
- function todoListToSerializedGraph(todos, sessionId) {
334
- const graphId = `todo:${sessionId}`;
335
- const nodes = todos.map((todo, index) => ({
336
- id: todo.id,
337
- title: todo.content,
338
- description: todo.activeForm ?? "",
339
- type: "chore",
340
- priority: "medium",
341
- status: todo.status,
342
- specRequirementId: `${graphId}:${todo.id}`,
343
- createdAt: index,
344
- updatedAt: index
345
- }));
346
- return {
347
- id: graphId,
348
- specId: graphId,
349
- requiredRequirementIds: nodes.map((node) => node.specRequirementId),
350
- title: "Session todos",
351
- nodes,
352
- edges: [],
353
- rootNodes: nodes.map((node) => node.id),
354
- createdAt: 0,
355
- updatedAt: 0
356
- };
357
- }
358
- function taskFileToSerializedGraph(tasks, sessionId) {
359
- const graphId = `session:${sessionId}`;
360
- const ids = new Set(tasks.map((task) => task.id));
361
- const nodes = tasks.map((task, index) => ({
362
- id: task.id,
363
- title: task.title,
364
- description: task.description ?? "",
365
- type: task.type,
366
- priority: task.priority,
367
- status: task.status,
368
- specRequirementId: `${graphId}:${task.id}`,
369
- ...task.assignee ? { assignee: task.assignee } : {},
370
- ...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
371
- createdAt: index,
372
- updatedAt: index
373
- }));
374
- const edges = tasks.flatMap(
375
- (task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
376
- id: `${dependency}->${task.id}`,
377
- from: dependency,
378
- to: task.id,
379
- type: "depends_on"
380
- }))
381
- );
382
- const hasIncoming = new Set(edges.map((edge) => edge.to));
383
- const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
384
- return {
385
- // Keep the historical graph id so existing mirrored task cards are reused.
386
- id: graphId,
387
- specId: graphId,
388
- requiredRequirementIds: nodes.map((node) => node.specRequirementId),
389
- title: "Session tasks",
390
- nodes,
391
- edges,
392
- rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
393
- createdAt: 0,
394
- updatedAt: 0
395
- };
653
+ return `Kanban mirror (${failure.sourceSystem}) failed and the board may be stale: ${failure.message}`;
396
654
  }
397
- var PLAN_STATUS_TO_TASK = {
398
- open: "pending",
399
- in_progress: "in_progress",
400
- done: "completed"
401
- };
402
- function planFileToSerializedGraph(items, sessionId) {
403
- const graphId = `plan:${sessionId}`;
404
- const nodes = items.map((item, index) => ({
405
- id: item.id,
406
- title: item.title,
407
- description: item.details ?? "",
408
- type: "chore",
409
- priority: "medium",
410
- status: PLAN_STATUS_TO_TASK[item.status],
411
- specRequirementId: `${graphId}:${item.id}`,
412
- createdAt: index,
413
- updatedAt: index
414
- }));
415
- return {
416
- id: graphId,
417
- specId: graphId,
418
- requiredRequirementIds: nodes.map((node) => node.specRequirementId),
419
- title: "Session plan",
420
- nodes,
421
- edges: [],
422
- rootNodes: nodes.map((node) => node.id),
423
- createdAt: 0,
424
- updatedAt: 0
425
- };
655
+ function hasInFlightTodoMirror(projectRoot, sessionId) {
656
+ if (!projectRoot || !sessionId) return false;
657
+ const key = mirrorKey(projectRoot, sessionId, "session-todo");
658
+ return pendingMirrors.has(key) || activeMirrors.has(key);
426
659
  }
427
660
  function projectSessionTodosToKanban(projectRoot, todos, sessionId) {
428
661
  return projectGraph(
@@ -462,52 +695,6 @@ function fireAndForget(context, work) {
462
695
  );
463
696
  });
464
697
  }
465
- function broadcastTodoUpdate(context, todos) {
466
- const sessionId = context.session?.id ?? "";
467
- if (!context.agentId || !sessionId) return;
468
- const statusCounts = { pending: 0, inProgress: 0, completed: 0 };
469
- for (const todo of todos) {
470
- if (todo.status === "completed") statusCounts.completed++;
471
- else if (todo.status === "in_progress") statusCounts.inProgress++;
472
- else statusCounts.pending++;
473
- }
474
- const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;
475
- const mailbox = getSharedProjectMailbox(projectDir);
476
- void mailbox.send({
477
- from: context.agentId,
478
- to: "*",
479
- type: "status",
480
- subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
481
- body: JSON.stringify({
482
- kind: "kanban.todos.updated",
483
- sessionId,
484
- revision: context.state.revision,
485
- todoCount: todos.length,
486
- statusCounts
487
- }),
488
- priority: "normal",
489
- senderSessionId: sessionId
490
- }).catch(() => {
491
- });
492
- }
493
- function notifyTodoUpdate(context, todos) {
494
- const summary = formatTodosForModel(todos);
495
- const text = `[KANBAN TODO UPDATE]
496
- Another Kanban agent reassessed the shared board. The canonical todo list is now:
497
- ${summary}
498
- Reassess your current plan before continuing; do not rely on the initial todo snapshot. Preserve each row's <kanban board/task> binding verbatim on your next \`todo\` call \u2014 a row that loses it stops advancing its card.`;
499
- const state = context.state;
500
- if (typeof state.appendBlockToLastUserMessage === "function") {
501
- if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
502
- }
503
- if (typeof state.appendMessage === "function") {
504
- state.appendMessage({ role: "user", content: [{ type: "text", text }] });
505
- }
506
- }
507
- function todosNeedingSessionMirror(todos, activeBoardId) {
508
- if (!activeBoardId) return todos;
509
- return todos.filter((todo) => todo.kanbanBoardId !== activeBoardId || !todo.kanbanTaskId);
510
- }
511
698
  function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
512
699
  queueLatestMirror(
513
700
  projectRoot,
@@ -585,8 +772,8 @@ function attachSessionKanbanMirror(context) {
585
772
  if (!watchedBoardId) return;
586
773
  const board = await getBoard(context.projectRoot, watchedBoardId);
587
774
  if (!board) return;
588
- if (board.lifecycle?.mode === "managed") applyManagedKanbanBoardToTodos(context, board);
589
- else applySessionKanbanBoardToTodos(context, board);
775
+ if (board.lifecycle?.mode === "managed") applyManagedKanbanBoardToTodos2(context, board);
776
+ else applySessionKanbanBoardToTodos2(context, board);
590
777
  };
591
778
  const configureBoardWatcher = async () => {
592
779
  const id = sessionId();
@@ -620,7 +807,7 @@ function attachSessionKanbanMirror(context) {
620
807
  if (presenceTimer) clearInterval(presenceTimer);
621
808
  presenceTimer = setInterval(() => fireAndForget("touch-presence", touchPresence()), 6e4);
622
809
  presenceTimer.unref?.();
623
- if (board.lifecycle?.mode === "managed") applyManagedKanbanBoardToTodos(context, board);
810
+ if (board.lifecycle?.mode === "managed") applyManagedKanbanBoardToTodos2(context, board);
624
811
  } catch {
625
812
  unsubscribeBoardEvents = null;
626
813
  watchedBoardId = "";
@@ -759,206 +946,26 @@ async function hydrateSessionKanbanBoard(context, id) {
759
946
  }
760
947
  return board;
761
948
  }
762
- function sourceStatus(task) {
763
- if (task.status === "completed") return "completed";
764
- if (task.status === "in_progress") return "in_progress";
765
- if (task.status === "review") return "review";
766
- if (task.status === "blocked") return "blocked";
767
- if (task.status === "failed") return "failed";
768
- return "pending";
769
- }
770
- function todoStatus(task) {
771
- const status = sourceStatus(task);
772
- if (status === "completed") return "completed";
773
- if (status === "review" && task.assignment?.status === "completed") return "completed";
774
- if (status === "in_progress" || status === "review") return "in_progress";
775
- return "pending";
776
- }
777
- function sessionTodoFromTask(task, board) {
778
- const blockedBy = board ? blockingTitles(board, task) : [];
779
- return {
780
- id: task.origin?.taskId ?? task.id,
781
- content: task.title,
782
- status: todoStatus(task),
783
- ...task.description ? { activeForm: task.description } : {},
784
- ...blockedBy.length ? { blockedBy } : {}
785
- };
786
- }
787
- function managedTodoFromTask(task, board) {
788
- return {
789
- ...sessionTodoFromTask(task, board),
790
- kanbanBoardId: board.id,
791
- kanbanTaskId: task.id
792
- };
793
- }
794
- function blockingTitles(board, task) {
795
- return getDependencyReadinessIssues(board, task).map((issue) => {
796
- const dependency = board.tasks.find((candidate) => candidate.id === issue.dependencyId);
797
- if (!dependency) return `${issue.dependencyId} (missing)`;
798
- return dependency.title;
799
- });
800
- }
801
- var PRIORITY_ORDER = {
802
- critical: 0,
803
- high: 1,
804
- medium: 2,
805
- low: 3
806
- };
807
- function orderTasksForTodos(board, tasks) {
808
- const columnOrder = new Map(board.columns.map((column) => [column.id, column.order]));
809
- const baseline = [...tasks].sort(
810
- (left, right) => (columnOrder.get(left.columnId) ?? 0) - (columnOrder.get(right.columnId) ?? 0) || (PRIORITY_ORDER[left.priority] ?? 2) - (PRIORITY_ORDER[right.priority] ?? 2) || left.order - right.order || left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id)
811
- );
812
- const included = new Set(baseline.map((task) => task.id));
813
- const remaining = new Map(baseline.map((task) => [task.id, task]));
814
- const emitted = [];
815
- const done = /* @__PURE__ */ new Set();
816
- while (remaining.size > 0) {
817
- const ready = baseline.filter(
818
- (task) => remaining.has(task.id) && (task.dependsOn ?? []).every(
819
- (dependencyId) => !included.has(dependencyId) || done.has(dependencyId)
820
- )
821
- );
822
- if (ready.length === 0) break;
823
- for (const task of ready) {
824
- remaining.delete(task.id);
825
- done.add(task.id);
826
- emitted.push(task);
827
- }
828
- }
829
- for (const task of baseline) if (remaining.has(task.id)) emitted.push(task);
830
- return emitted;
831
- }
832
- function sameTodos(left, right) {
833
- return left.length === right.length && left.every((todo, index) => {
834
- const candidate = right[index];
835
- return candidate?.id === todo.id && candidate.content === todo.content && candidate.status === todo.status && candidate.activeForm === todo.activeForm && candidate.promotedFromPlan === todo.promotedFromPlan && candidate.promotedFromTask === todo.promotedFromTask && candidate.kanbanBoardId === todo.kanbanBoardId && candidate.kanbanTaskId === todo.kanbanTaskId && // Readiness is part of the projection: when a dependency completes,
836
- // the rows are otherwise identical and the unblocking would never
837
- // reach the model.
838
- (candidate.blockedBy ?? []).join("\0") === (todo.blockedBy ?? []).join("\0");
949
+ function applySessionKanbanBoardToTodos2(context, board) {
950
+ return applySessionKanbanBoardToTodos(context, board, {
951
+ sessionIdFromTags,
952
+ isOwnedSessionBoard,
953
+ hasInFlightTodoMirror,
954
+ suppressedTodoMirrors
839
955
  });
840
956
  }
841
- function applySessionKanbanBoardToTodos(context, board) {
842
- const sessionId = context.session?.id ?? "";
843
- if (!sessionId || sessionIdFromTags(board.tags) !== sessionId || !isOwnedSessionBoard(board.tags)) {
844
- return [...context.todos];
845
- }
846
- if (hasInFlightTodoMirror(context.projectRoot, sessionId)) return [...context.todos];
847
- const projectedTodos = orderTasksForTodos(
848
- board,
849
- board.tasks.filter(
850
- (task) => task.status !== "archived" && (!task.origin || task.origin.system === "session-todo" || (task.origin.graphId ?? "").startsWith("todo:"))
851
- )
852
- ).map((task) => sessionTodoFromTask(task, board));
853
- const allCompleted = projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === "completed");
854
- const effectiveTodos = allCompleted ? [] : projectedTodos;
855
- if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];
856
- suppressedTodoMirrors.add(context);
857
- try {
858
- context.state.replaceTodos(projectedTodos);
859
- } finally {
860
- suppressedTodoMirrors.delete(context);
861
- }
862
- notifyTodoUpdate(context, context.todos);
863
- broadcastTodoUpdate(context, context.todos);
864
- return [...context.todos];
865
- }
866
- function applyManagedKanbanBoardToTodos(context, board) {
867
- const metaKanban = context.meta["kanban"];
868
- const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
869
- const activeBoardId = context.currentKanbanBoardId ?? (typeof metaBoardId === "string" ? metaBoardId : void 0);
870
- if (!activeBoardId || board.id !== activeBoardId || board.lifecycle?.mode !== "managed") {
871
- return [...context.todos];
872
- }
873
- const projectedTodos = orderTasksForTodos(
874
- board,
875
- board.tasks.filter(
876
- (task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
877
- )
878
- ).map((task) => managedTodoFromTask(task, board));
879
- if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
880
- suppressedTodoMirrors.add(context);
881
- try {
882
- context.state.replaceTodos(projectedTodos);
883
- } finally {
884
- suppressedTodoMirrors.delete(context);
885
- }
886
- notifyTodoUpdate(context, context.todos);
887
- broadcastTodoUpdate(context, context.todos);
888
- return [...context.todos];
957
+ function applyManagedKanbanBoardToTodos2(context, board) {
958
+ return applyManagedKanbanBoardToTodos(context, board, suppressedTodoMirrors);
889
959
  }
890
- async function applySessionKanbanTaskToSource(context, task, options = {}) {
891
- const originId = task.origin?.taskId;
892
- const graphId = task.origin?.graphId ?? "";
893
- if (!originId) return { source: null };
894
- if (task.origin?.system === "session-todo" || graphId.startsWith("todo:")) {
895
- const mappedStatus = todoStatus(task);
896
- const next = options.remove ? context.todos.filter((todo) => todo.id !== originId) : context.todos.map(
897
- (todo) => todo.id === originId ? {
898
- ...todo,
899
- content: task.title,
900
- status: mappedStatus
901
- } : todo
902
- );
903
- suppressedTodoMirrors.add(context);
904
- try {
905
- context.state.replaceTodos(next);
906
- } finally {
907
- suppressedTodoMirrors.delete(context);
908
- }
909
- return { source: "todo", todos: [...context.todos] };
910
- }
911
- const id = context.session?.id ?? "";
912
- if (task.origin?.system === "session-plan" || graphId.startsWith("plan:")) {
913
- const planPath = context.meta["plan.path"];
914
- if (typeof planPath !== "string" || !planPath) {
915
- throw new Error(
916
- "Cannot reflect this Kanban edit back to its plan source: the session has no plan.path configured. The board mutation already succeeded; the plan file is now out of sync."
917
- );
918
- }
919
- const plan = await mutatePlan(planPath, id, (file) => ({
920
- ...file,
921
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
922
- items: options.remove ? file.items.filter((item) => item.id !== originId) : file.items.map(
923
- (item) => item.id === originId ? {
924
- ...item,
925
- title: task.title,
926
- details: task.description,
927
- status: task.status === "completed" ? "done" : task.status === "in_progress" || task.status === "review" ? "in_progress" : "open",
928
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
929
- } : item
930
- )
931
- }));
932
- return { source: "plan", plan };
933
- }
934
- if (task.origin?.system === "session-task" || task.origin?.system === "session" || graphId.startsWith("session:")) {
935
- const taskPath = context.meta["task.path"];
936
- if (typeof taskPath !== "string" || !taskPath) {
937
- throw new Error(
938
- "Cannot reflect this Kanban edit back to its task source: the session has no task.path configured. The board mutation already succeeded; the task file is now out of sync."
939
- );
940
- }
941
- const tasks = await mutateTasks(taskPath, id, (file) => ({
942
- ...file,
943
- tasks: options.remove ? file.tasks.filter((item) => item.id !== originId) : file.tasks.map(
944
- (item) => item.id === originId ? {
945
- ...item,
946
- title: task.title,
947
- description: task.description,
948
- status: sourceStatus(task),
949
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
950
- } : item
951
- )
952
- }));
953
- return { source: "task", tasks };
954
- }
955
- return { source: null };
960
+ function applySessionKanbanTaskToSource2(context, task, options = {}) {
961
+ return applySessionKanbanTaskToSource(context, task, suppressedTodoMirrors, options);
956
962
  }
957
963
  export {
964
+ PLAN_STATUS_TO_TASK,
958
965
  SESSION_KANBAN_COLUMNS,
959
- applyManagedKanbanBoardToTodos,
960
- applySessionKanbanBoardToTodos,
961
- applySessionKanbanTaskToSource,
966
+ applyManagedKanbanBoardToTodos2 as applyManagedKanbanBoardToTodos,
967
+ applySessionKanbanBoardToTodos2 as applySessionKanbanBoardToTodos,
968
+ applySessionKanbanTaskToSource2 as applySessionKanbanTaskToSource,
962
969
  attachSessionKanbanMirror,
963
970
  blockingTitles,
964
971
  cleanupEmptySessionKanbanBoards,