@wrongstack/tools 0.309.0 → 0.310.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 (67) hide show
  1. package/dist/_regex.d.ts +6 -34
  2. package/dist/audit.js +3 -3
  3. package/dist/bash.js +4 -4
  4. package/dist/builtin.js +3020 -1680
  5. package/dist/codebase-index/binary-frame.d.ts +57 -8
  6. package/dist/codebase-index/codebase-incoming-calls-tool.d.ts +6 -0
  7. package/dist/codebase-index/codebase-outgoing-calls-tool.d.ts +6 -0
  8. package/dist/codebase-index/codebase-search-tool.d.ts +15 -5
  9. package/dist/codebase-index/index-service.d.ts +3 -19
  10. package/dist/codebase-index/index.js +2784 -1464
  11. package/dist/codebase-index/indexer.d.ts +3 -0
  12. package/dist/codebase-index/parser-batch.d.ts +53 -0
  13. package/dist/codebase-index/parser-dispatch.d.ts +32 -0
  14. package/dist/codebase-index/parser-output.d.ts +14 -0
  15. package/dist/codebase-index/parser-worker-pool.d.ts +57 -4
  16. package/dist/codebase-index/parser-worker-script.d.ts +5 -2
  17. package/dist/codebase-index/parser-worker-script.js +4042 -0
  18. package/dist/codebase-index/project-server-cache.d.ts +16 -0
  19. package/dist/codebase-index/project-server-client.d.ts +2 -2
  20. package/dist/codebase-index/project-server-query-cache.d.ts +88 -0
  21. package/dist/codebase-index/project-server.js +2846 -1308
  22. package/dist/codebase-index/py-parser.d.ts +5 -0
  23. package/dist/codebase-index/schema.d.ts +14 -1
  24. package/dist/codebase-index/sqlite-runtime.d.ts +2 -2
  25. package/dist/codebase-index/tree-sitter/queries.d.ts +30 -3
  26. package/dist/codebase-index/tree-sitter/visitor.d.ts +2 -1
  27. package/dist/codebase-index/vector-search.d.ts +12 -0
  28. package/dist/codebase-index/wal-maintenance.d.ts +58 -0
  29. package/dist/codebase-index/worker-protocol/contracts.d.ts +44 -0
  30. package/dist/codebase-index/worker-protocol.d.ts +17 -1
  31. package/dist/codebase-index/worker.js +2300 -1020
  32. package/dist/codebase-index/writer-admin.d.ts +11 -0
  33. package/dist/codebase-index/writer-helpers.d.ts +31 -1
  34. package/dist/codebase-index/writer-mutations.d.ts +0 -6
  35. package/dist/codebase-index/writer-schema.d.ts +2 -2
  36. package/dist/codebase-index/writer.d.ts +15 -0
  37. package/dist/edit.js +2511 -1203
  38. package/dist/exec.js +8 -6
  39. package/dist/format.js +3 -3
  40. package/dist/git.js +6 -0
  41. package/dist/grep.js +5 -124
  42. package/dist/index.js +3016 -1739
  43. package/dist/install.js +3 -3
  44. package/dist/json.js +5 -124
  45. package/dist/kanban.js +130 -0
  46. package/dist/languages/index.js +3 -3
  47. package/dist/lint.js +3 -3
  48. package/dist/logs.js +5 -121
  49. package/dist/outdated.js +3 -3
  50. package/dist/pack.js +3020 -1680
  51. package/dist/patch.js +2524 -1216
  52. package/dist/plan.js +106 -0
  53. package/dist/process-registry.js +1 -1
  54. package/dist/read.js +2506 -1198
  55. package/dist/replace.js +2487 -1295
  56. package/dist/search.js +6 -2
  57. package/dist/session-kanban.js +24 -16
  58. package/dist/task.js +106 -0
  59. package/dist/test.js +3 -3
  60. package/dist/todo.js +106 -0
  61. package/dist/tool-tier.d.ts +11 -0
  62. package/dist/tool-tier.js +3028 -1680
  63. package/dist/tree.js +14 -3
  64. package/dist/typecheck.js +3 -3
  65. package/dist/win32.js +5 -5
  66. package/dist/write.js +2513 -1205
  67. package/package.json +5 -4
package/dist/search.js CHANGED
@@ -421,10 +421,14 @@ function parseDuckDuckGo(html, num) {
421
421
  }
422
422
  return results;
423
423
  }
424
+ function escapeRegex(s) {
425
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
426
+ }
424
427
  function getHtmlAttr(attrs, name) {
425
- const quoted = new RegExp(`\\b${name}\\s*=\\s*(["'])(.*?)\\1`, "i").exec(attrs);
428
+ const escaped = escapeRegex(name);
429
+ const quoted = new RegExp(`\\b${escaped}\\s*=\\s*(["'])(.*?)\\1`, "i").exec(attrs);
426
430
  if (quoted?.[2]) return decodeHtmlEntities(quoted[2]);
427
- const unquoted = new RegExp(`\\b${name}\\s*=\\s*([^\\s>]+)`, "i").exec(attrs);
431
+ const unquoted = new RegExp(`\\b${escaped}\\s*=\\s*([^\\s>]+)`, "i").exec(attrs);
428
432
  return unquoted?.[1] ? decodeHtmlEntities(unquoted[1]) : void 0;
429
433
  }
430
434
  function normalizeDuckDuckGoUrl(raw) {
@@ -293,24 +293,32 @@ function applyManagedKanbanBoardToTodos(context, board, suppressedTodoMirrors2,
293
293
  async function applySessionKanbanTaskToSource(context, task, suppressedTodoMirrors2, options = {}) {
294
294
  const originId = task.origin?.taskId;
295
295
  const graphId = task.origin?.graphId ?? "";
296
- if (!originId) return { source: null };
297
- if (task.origin?.system === "session-todo" || graphId.startsWith("todo:")) {
298
- const mappedStatus = todoStatus(task);
299
- const next = options.remove ? context.todos.filter((todo) => todo.id !== originId) : context.todos.map(
300
- (todo) => todo.id === originId ? {
301
- ...todo,
302
- content: task.title,
303
- status: mappedStatus
304
- } : todo
305
- );
306
- suppressedTodoMirrors2.add(context);
307
- try {
308
- context.state.replaceTodos(next);
309
- } finally {
310
- suppressedTodoMirrors2.delete(context);
296
+ const isTodoOrigin = task.origin?.system === "session-todo" || graphId.startsWith("todo:");
297
+ const targetTodo = context.todos?.find(
298
+ (todo) => originId !== void 0 && todo.id === originId || todo.kanbanTaskId === task.id || todo.id === task.id && !todo.promotedFromPlan && !todo.promotedFromTask
299
+ );
300
+ if (isTodoOrigin || targetTodo) {
301
+ const matchedId = targetTodo?.id ?? originId;
302
+ if (matchedId) {
303
+ const mappedStatus = todoStatus(task);
304
+ const next = options.remove ? context.todos.filter((todo) => todo.id !== matchedId) : context.todos.map(
305
+ (todo) => todo.id === matchedId ? {
306
+ ...todo,
307
+ content: task.title,
308
+ status: mappedStatus,
309
+ kanbanTaskId: task.id
310
+ } : todo
311
+ );
312
+ suppressedTodoMirrors2.add(context);
313
+ try {
314
+ context.state.replaceTodos(next);
315
+ } finally {
316
+ suppressedTodoMirrors2.delete(context);
317
+ }
318
+ return { source: "todo", todos: [...context.todos] };
311
319
  }
312
- return { source: "todo", todos: [...context.todos] };
313
320
  }
321
+ if (!originId) return { source: null };
314
322
  const id = context.session?.id ?? "";
315
323
  if (task.origin?.system === "session-plan" || graphId.startsWith("plan:")) {
316
324
  const planPath = context.meta["plan.path"];
package/dist/task.js CHANGED
@@ -234,6 +234,81 @@ function applyManagedKanbanBoardToTodos(context, board, suppressedTodoMirrors2,
234
234
  broadcastTodoUpdate(context, context.todos);
235
235
  return [...context.todos];
236
236
  }
237
+ async function applySessionKanbanTaskToSource(context, task, suppressedTodoMirrors2, options = {}) {
238
+ const originId = task.origin?.taskId;
239
+ const graphId = task.origin?.graphId ?? "";
240
+ const isTodoOrigin = task.origin?.system === "session-todo" || graphId.startsWith("todo:");
241
+ const targetTodo = context.todos?.find(
242
+ (todo) => originId !== void 0 && todo.id === originId || todo.kanbanTaskId === task.id || todo.id === task.id && !todo.promotedFromPlan && !todo.promotedFromTask
243
+ );
244
+ if (isTodoOrigin || targetTodo) {
245
+ const matchedId = targetTodo?.id ?? originId;
246
+ if (matchedId) {
247
+ const mappedStatus = todoStatus(task);
248
+ const next = options.remove ? context.todos.filter((todo) => todo.id !== matchedId) : context.todos.map(
249
+ (todo) => todo.id === matchedId ? {
250
+ ...todo,
251
+ content: task.title,
252
+ status: mappedStatus,
253
+ kanbanTaskId: task.id
254
+ } : todo
255
+ );
256
+ suppressedTodoMirrors2.add(context);
257
+ try {
258
+ context.state.replaceTodos(next);
259
+ } finally {
260
+ suppressedTodoMirrors2.delete(context);
261
+ }
262
+ return { source: "todo", todos: [...context.todos] };
263
+ }
264
+ }
265
+ if (!originId) return { source: null };
266
+ const id = context.session?.id ?? "";
267
+ if (task.origin?.system === "session-plan" || graphId.startsWith("plan:")) {
268
+ const planPath = context.meta["plan.path"];
269
+ if (typeof planPath !== "string" || !planPath) {
270
+ throw new Error(
271
+ "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."
272
+ );
273
+ }
274
+ const plan = await mutatePlan(planPath, id, (file) => ({
275
+ ...file,
276
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
277
+ items: options.remove ? file.items.filter((item) => item.id !== originId) : file.items.map(
278
+ (item) => item.id === originId ? {
279
+ ...item,
280
+ title: task.title,
281
+ details: task.description,
282
+ status: task.status === "completed" ? "done" : task.status === "in_progress" || task.status === "review" ? "in_progress" : "open",
283
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
284
+ } : item
285
+ )
286
+ }));
287
+ return { source: "plan", plan };
288
+ }
289
+ if (task.origin?.system === "session-task" || task.origin?.system === "session" || graphId.startsWith("session:")) {
290
+ const taskPath = context.meta["task.path"];
291
+ if (typeof taskPath !== "string" || !taskPath) {
292
+ throw new Error(
293
+ "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."
294
+ );
295
+ }
296
+ const tasks = await mutateTasks(taskPath, id, (file) => ({
297
+ ...file,
298
+ tasks: options.remove ? file.tasks.filter((item) => item.id !== originId) : file.tasks.map(
299
+ (item) => item.id === originId ? {
300
+ ...item,
301
+ title: task.title,
302
+ description: task.description,
303
+ status: sourceStatus(task),
304
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
305
+ } : item
306
+ )
307
+ }));
308
+ return { source: "task", tasks };
309
+ }
310
+ return { source: null };
311
+ }
237
312
 
238
313
  // src/session-kanban.ts
239
314
  var SESSION_BOARD_TAG = "session-work";
@@ -477,6 +552,9 @@ function applyManagedKanbanBoardToTodos2(context, board) {
477
552
  sessionOwnerFromTags: sessionIdFromTags
478
553
  });
479
554
  }
555
+ function applySessionKanbanTaskToSource2(context, task, options = {}) {
556
+ return applySessionKanbanTaskToSource(context, task, suppressedTodoMirrors, options);
557
+ }
480
558
 
481
559
  // src/todo.ts
482
560
  import {
@@ -1453,12 +1531,20 @@ function assignmentForTaskCreate(input) {
1453
1531
  }
1454
1532
 
1455
1533
  // src/kanban-lifecycle-actions.ts
1534
+ async function syncContextTask(ctx, task, options = {}) {
1535
+ if (!ctx?.state || !task) return;
1536
+ try {
1537
+ await applySessionKanbanTaskToSource2(ctx, task, options);
1538
+ } catch {
1539
+ }
1540
+ }
1456
1541
  async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1457
1542
  switch (input.action) {
1458
1543
  case "add_task": {
1459
1544
  if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
1460
1545
  const result = await addTask2(projectRoot, input.boardId, taskInput(input));
1461
1546
  if (!result) return fail("Board not found.");
1547
+ await syncContextTask(ctx, result.task);
1462
1548
  return okTask(result.board, result.task, `Task added.${atomicityNudge(result.task)}`);
1463
1549
  }
1464
1550
  case "split_task": {
@@ -1603,6 +1689,7 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1603
1689
  );
1604
1690
  }
1605
1691
  ctx.setCurrentKanbanTask(task.id, board.id);
1692
+ await syncContextTask(ctx, task);
1606
1693
  return okTask(
1607
1694
  board,
1608
1695
  task,
@@ -1612,6 +1699,9 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1612
1699
  case "update_task": {
1613
1700
  if (!input.boardId || !input.taskId) return fail("update_task requires boardId and taskId.");
1614
1701
  const board = await updateTask2(projectRoot, input.boardId, input.taskId, taskPatch(input));
1702
+ if (board) {
1703
+ await syncContextTask(ctx, board.tasks.find((t) => t.id === input.taskId));
1704
+ }
1615
1705
  return board ? okBoard(board, "Task updated.") : fail("Task not found.");
1616
1706
  }
1617
1707
  case "transition_task": {
@@ -1651,6 +1741,9 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1651
1741
  if (result && input.lifecycleStage === "done" && result.task.verificationReport) {
1652
1742
  recordKanbanVerificationEvidence(ctx, result.task.verificationReport);
1653
1743
  }
1744
+ if (result?.task) {
1745
+ await syncContextTask(ctx, result.task);
1746
+ }
1654
1747
  return result ? okTask(result.board, result.task, `Task advanced to ${result.transition.to}.`) : fail("Board or task not found.");
1655
1748
  }
1656
1749
  case "repair_managed_projection": {
@@ -1663,6 +1756,9 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1663
1756
  actor: input.author,
1664
1757
  comment: input.transitionComment
1665
1758
  });
1759
+ if (result?.task) {
1760
+ await syncContextTask(ctx, result.task);
1761
+ }
1666
1762
  return result ? okTask(
1667
1763
  result.board,
1668
1764
  result.task,
@@ -1680,14 +1776,22 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1680
1776
  input.targetColumnId,
1681
1777
  input.order
1682
1778
  );
1779
+ if (board) {
1780
+ await syncContextTask(ctx, board.tasks.find((t) => t.id === input.taskId));
1781
+ }
1683
1782
  return board ? okBoard(board, "Task moved.") : fail("Move failed.");
1684
1783
  }
1685
1784
  case "delete_task": {
1686
1785
  if (!input.boardId || !input.taskId) return fail("delete_task requires boardId and taskId.");
1786
+ const boardBefore = await getBoard4(projectRoot, input.boardId);
1787
+ const taskToDelete = boardBefore?.tasks.find((t) => t.id === input.taskId);
1687
1788
  const board = await removeTask(projectRoot, input.boardId, input.taskId);
1688
1789
  if (board && ctx.currentKanbanTaskId === input.taskId) {
1689
1790
  ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
1690
1791
  }
1792
+ if (board && taskToDelete) {
1793
+ await syncContextTask(ctx, taskToDelete, { remove: true });
1794
+ }
1691
1795
  return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
1692
1796
  }
1693
1797
  case "set_chain": {
@@ -1794,6 +1898,7 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1794
1898
  issues: finalized.gate.issues.map((issue) => issue.message)
1795
1899
  };
1796
1900
  const gateMessage = finalized.gate.allowed ? `Completion gate ${finalized.gate.verdict === "skipped" ? "passed" : "passed"}; task completed.` : finalized.gate.enforcement === "strict" ? `Completion gate BLOCKED (verdict: ${finalized.gate.verdict}); task parked in review. Issues: ${gateSummary.issues.join(" | ")}` : `Completion gate failed softly (verdict: ${finalized.gate.verdict}); task completed with warnings. Issues: ${gateSummary.issues.join(" | ")}`;
1901
+ await syncContextTask(ctx, finalized.task);
1797
1902
  return {
1798
1903
  ...okTask(finalized.board, finalized.task, `Assignment updated. ${gateMessage}`),
1799
1904
  gate: gateSummary
@@ -1899,6 +2004,7 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1899
2004
  msgParts.push(`Card advanced to ${transitionResult.transition.to}.`);
1900
2005
  }
1901
2006
  for (const w of lifecycleWarnings) msgParts.push(`Warning: ${w}`);
2007
+ await syncContextTask(ctx, responseTask);
1902
2008
  return okTask(responseBoard, responseTask, msgParts.join(" "));
1903
2009
  }
1904
2010
  return okBoard(board, "Assignment updated.");
package/dist/test.js CHANGED
@@ -141,12 +141,12 @@ function resolveWin32Command(cmd) {
141
141
  }
142
142
  return cmd;
143
143
  }
144
- var WIN32_SHELL_META = /[&|<>"\r\n\0]/;
144
+ var WIN32_SHELL_META = /[&|<>"%\r\n\0]/;
145
145
  function assertSafeWin32ShellArgs(args) {
146
146
  for (const arg of args) {
147
147
  if (typeof arg === "string" && WIN32_SHELL_META.test(arg)) {
148
148
  throw new Error(
149
- 'win32 cmd shim spawn: argument contains a shell metacharacter (one of & | < > ", or a newline) that could enable command injection through the .cmd/.bat wrapper - refusing to run. Offending argument: ' + JSON.stringify(arg)
149
+ 'win32 cmd shim spawn: argument contains a shell metacharacter (one of & | < > " %, or a newline) that could enable command injection through the .cmd/.bat wrapper - refusing to run. Offending argument: ' + JSON.stringify(arg)
150
150
  );
151
151
  }
152
152
  }
@@ -377,7 +377,7 @@ var SENSITIVE_FLAG_PATTERNS = [
377
377
  // redaction function. Synced with core copy.
378
378
  /(?<![-\w])-(?:password|p|a)(?:[=\s]+)?[^\s,-]+/gi,
379
379
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
380
- /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
380
+ /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD|PASSPHRASE)\s*[=:]\s*[^\s,]+/gi,
381
381
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
382
382
  // when preceded by a flag name (e.g. --github-token=EyJ...). Global flag so
383
383
  // every such flag in the command line is redacted, not just the first.
package/dist/todo.js CHANGED
@@ -383,6 +383,81 @@ function applyManagedKanbanBoardToTodos(context, board, suppressedTodoMirrors2,
383
383
  broadcastTodoUpdate(context, context.todos);
384
384
  return [...context.todos];
385
385
  }
386
+ async function applySessionKanbanTaskToSource(context, task, suppressedTodoMirrors2, options = {}) {
387
+ const originId = task.origin?.taskId;
388
+ const graphId = task.origin?.graphId ?? "";
389
+ const isTodoOrigin = task.origin?.system === "session-todo" || graphId.startsWith("todo:");
390
+ const targetTodo = context.todos?.find(
391
+ (todo) => originId !== void 0 && todo.id === originId || todo.kanbanTaskId === task.id || todo.id === task.id && !todo.promotedFromPlan && !todo.promotedFromTask
392
+ );
393
+ if (isTodoOrigin || targetTodo) {
394
+ const matchedId = targetTodo?.id ?? originId;
395
+ if (matchedId) {
396
+ const mappedStatus = todoStatus(task);
397
+ const next = options.remove ? context.todos.filter((todo) => todo.id !== matchedId) : context.todos.map(
398
+ (todo) => todo.id === matchedId ? {
399
+ ...todo,
400
+ content: task.title,
401
+ status: mappedStatus,
402
+ kanbanTaskId: task.id
403
+ } : todo
404
+ );
405
+ suppressedTodoMirrors2.add(context);
406
+ try {
407
+ context.state.replaceTodos(next);
408
+ } finally {
409
+ suppressedTodoMirrors2.delete(context);
410
+ }
411
+ return { source: "todo", todos: [...context.todos] };
412
+ }
413
+ }
414
+ if (!originId) return { source: null };
415
+ const id = context.session?.id ?? "";
416
+ if (task.origin?.system === "session-plan" || graphId.startsWith("plan:")) {
417
+ const planPath = context.meta["plan.path"];
418
+ if (typeof planPath !== "string" || !planPath) {
419
+ throw new Error(
420
+ "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."
421
+ );
422
+ }
423
+ const plan = await mutatePlan(planPath, id, (file) => ({
424
+ ...file,
425
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
426
+ items: options.remove ? file.items.filter((item) => item.id !== originId) : file.items.map(
427
+ (item) => item.id === originId ? {
428
+ ...item,
429
+ title: task.title,
430
+ details: task.description,
431
+ status: task.status === "completed" ? "done" : task.status === "in_progress" || task.status === "review" ? "in_progress" : "open",
432
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
433
+ } : item
434
+ )
435
+ }));
436
+ return { source: "plan", plan };
437
+ }
438
+ if (task.origin?.system === "session-task" || task.origin?.system === "session" || graphId.startsWith("session:")) {
439
+ const taskPath = context.meta["task.path"];
440
+ if (typeof taskPath !== "string" || !taskPath) {
441
+ throw new Error(
442
+ "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."
443
+ );
444
+ }
445
+ const tasks = await mutateTasks(taskPath, id, (file) => ({
446
+ ...file,
447
+ tasks: options.remove ? file.tasks.filter((item) => item.id !== originId) : file.tasks.map(
448
+ (item) => item.id === originId ? {
449
+ ...item,
450
+ title: task.title,
451
+ description: task.description,
452
+ status: sourceStatus(task),
453
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
454
+ } : item
455
+ )
456
+ }));
457
+ return { source: "task", tasks };
458
+ }
459
+ return { source: null };
460
+ }
386
461
 
387
462
  // src/session-kanban.ts
388
463
  var SESSION_BOARD_TAG = "session-work";
@@ -618,6 +693,9 @@ function applyManagedKanbanBoardToTodos2(context, board) {
618
693
  sessionOwnerFromTags: sessionIdFromTags
619
694
  });
620
695
  }
696
+ function applySessionKanbanTaskToSource2(context, task, options = {}) {
697
+ return applySessionKanbanTaskToSource(context, task, suppressedTodoMirrors, options);
698
+ }
621
699
 
622
700
  // src/kanban-board-actions.ts
623
701
  async function handleKanbanBoardAction(projectRoot, input, ctx) {
@@ -1440,12 +1518,20 @@ function assignmentForTaskCreate(input) {
1440
1518
  }
1441
1519
 
1442
1520
  // src/kanban-lifecycle-actions.ts
1521
+ async function syncContextTask(ctx, task, options = {}) {
1522
+ if (!ctx?.state || !task) return;
1523
+ try {
1524
+ await applySessionKanbanTaskToSource2(ctx, task, options);
1525
+ } catch {
1526
+ }
1527
+ }
1443
1528
  async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1444
1529
  switch (input.action) {
1445
1530
  case "add_task": {
1446
1531
  if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
1447
1532
  const result = await addTask2(projectRoot, input.boardId, taskInput(input));
1448
1533
  if (!result) return fail("Board not found.");
1534
+ await syncContextTask(ctx, result.task);
1449
1535
  return okTask(result.board, result.task, `Task added.${atomicityNudge(result.task)}`);
1450
1536
  }
1451
1537
  case "split_task": {
@@ -1590,6 +1676,7 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1590
1676
  );
1591
1677
  }
1592
1678
  ctx.setCurrentKanbanTask(task.id, board.id);
1679
+ await syncContextTask(ctx, task);
1593
1680
  return okTask(
1594
1681
  board,
1595
1682
  task,
@@ -1599,6 +1686,9 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1599
1686
  case "update_task": {
1600
1687
  if (!input.boardId || !input.taskId) return fail("update_task requires boardId and taskId.");
1601
1688
  const board = await updateTask2(projectRoot, input.boardId, input.taskId, taskPatch(input));
1689
+ if (board) {
1690
+ await syncContextTask(ctx, board.tasks.find((t) => t.id === input.taskId));
1691
+ }
1602
1692
  return board ? okBoard(board, "Task updated.") : fail("Task not found.");
1603
1693
  }
1604
1694
  case "transition_task": {
@@ -1638,6 +1728,9 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1638
1728
  if (result && input.lifecycleStage === "done" && result.task.verificationReport) {
1639
1729
  recordKanbanVerificationEvidence(ctx, result.task.verificationReport);
1640
1730
  }
1731
+ if (result?.task) {
1732
+ await syncContextTask(ctx, result.task);
1733
+ }
1641
1734
  return result ? okTask(result.board, result.task, `Task advanced to ${result.transition.to}.`) : fail("Board or task not found.");
1642
1735
  }
1643
1736
  case "repair_managed_projection": {
@@ -1650,6 +1743,9 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1650
1743
  actor: input.author,
1651
1744
  comment: input.transitionComment
1652
1745
  });
1746
+ if (result?.task) {
1747
+ await syncContextTask(ctx, result.task);
1748
+ }
1653
1749
  return result ? okTask(
1654
1750
  result.board,
1655
1751
  result.task,
@@ -1667,14 +1763,22 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1667
1763
  input.targetColumnId,
1668
1764
  input.order
1669
1765
  );
1766
+ if (board) {
1767
+ await syncContextTask(ctx, board.tasks.find((t) => t.id === input.taskId));
1768
+ }
1670
1769
  return board ? okBoard(board, "Task moved.") : fail("Move failed.");
1671
1770
  }
1672
1771
  case "delete_task": {
1673
1772
  if (!input.boardId || !input.taskId) return fail("delete_task requires boardId and taskId.");
1773
+ const boardBefore = await getBoard4(projectRoot, input.boardId);
1774
+ const taskToDelete = boardBefore?.tasks.find((t) => t.id === input.taskId);
1674
1775
  const board = await removeTask(projectRoot, input.boardId, input.taskId);
1675
1776
  if (board && ctx.currentKanbanTaskId === input.taskId) {
1676
1777
  ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
1677
1778
  }
1779
+ if (board && taskToDelete) {
1780
+ await syncContextTask(ctx, taskToDelete, { remove: true });
1781
+ }
1678
1782
  return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
1679
1783
  }
1680
1784
  case "set_chain": {
@@ -1781,6 +1885,7 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1781
1885
  issues: finalized.gate.issues.map((issue) => issue.message)
1782
1886
  };
1783
1887
  const gateMessage = finalized.gate.allowed ? `Completion gate ${finalized.gate.verdict === "skipped" ? "passed" : "passed"}; task completed.` : finalized.gate.enforcement === "strict" ? `Completion gate BLOCKED (verdict: ${finalized.gate.verdict}); task parked in review. Issues: ${gateSummary.issues.join(" | ")}` : `Completion gate failed softly (verdict: ${finalized.gate.verdict}); task completed with warnings. Issues: ${gateSummary.issues.join(" | ")}`;
1888
+ await syncContextTask(ctx, finalized.task);
1784
1889
  return {
1785
1890
  ...okTask(finalized.board, finalized.task, `Assignment updated. ${gateMessage}`),
1786
1891
  gate: gateSummary
@@ -1886,6 +1991,7 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
1886
1991
  msgParts.push(`Card advanced to ${transitionResult.transition.to}.`);
1887
1992
  }
1888
1993
  for (const w of lifecycleWarnings) msgParts.push(`Warning: ${w}`);
1994
+ await syncContextTask(ctx, responseTask);
1889
1995
  return okTask(responseBoard, responseTask, msgParts.join(" "));
1890
1996
  }
1891
1997
  return okBoard(board, "Assignment updated.");
@@ -1,5 +1,16 @@
1
1
  import type { ToolRegistry } from '@wrongstack/core/registry';
2
2
  import type { ConcreteTokenSavingTier, Tool } from '@wrongstack/core/types';
3
+ /**
4
+ * Live count of tools each concrete token-saving tier exposes to the provider.
5
+ * This is the runtime source of truth — `off` counts every registered tool,
6
+ * the other tiers count the result of `selectBuiltinToolsForTier(tier, …)`.
7
+ *
8
+ * Website (`website/src/pages/ToolsPage.tsx`) and any other consumer that
9
+ * wants to render tier counts without actually loading the catalog should
10
+ * mirror this map. When `TIER1_TOOLS` / `TIER2_TOOLS` / `TIER3_TOOLS` change,
11
+ * regenerate these numbers from a smoke test (or update both together).
12
+ */
13
+ export declare const BUILTIN_TIER_COUNTS: Readonly<Record<ConcreteTokenSavingTier, number>>;
3
14
  /**
4
15
  * Select built-in tools for a concrete token-saving tier while preserving the
5
16
  * order and instances supplied by the caller.