@wrongstack/tools 0.296.4 → 0.298.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 (258) hide show
  1. package/dist/_spawn-stream.d.ts +2 -0
  2. package/dist/_util.d.ts +29 -1
  3. package/dist/audit.js +128 -65
  4. package/dist/bash.js +20 -4
  5. package/dist/batch-tool-use.js +70 -7
  6. package/dist/browser/index.js +31 -1
  7. package/dist/builtin.js +883 -351
  8. package/dist/codebase-index/index.js +95 -16
  9. package/dist/codebase-index/project-server-protocol.d.ts +26 -0
  10. package/dist/codebase-index/project-server.js +76 -16
  11. package/dist/codebase-index/worker.js +36 -13
  12. package/dist/codebase-index/writer-search-helpers.d.ts +16 -0
  13. package/dist/codebase-index/writer.d.ts +13 -1
  14. package/dist/design.d.ts +1 -1
  15. package/dist/design.js +4 -3
  16. package/dist/diff.js +11 -1
  17. package/dist/e2e.js +9 -6
  18. package/dist/edit.js +9 -6
  19. package/dist/exec.js +562 -516
  20. package/dist/format.js +131 -65
  21. package/dist/git.js +22 -2
  22. package/dist/glob.js +11 -5
  23. package/dist/grep.js +160 -104
  24. package/dist/index.js +834 -290
  25. package/dist/install.js +135 -68
  26. package/dist/json.js +64 -18
  27. package/dist/kanban-board-inputs.d.ts +21 -4
  28. package/dist/kanban.js +20 -5
  29. package/dist/languages/index.js +143 -71
  30. package/dist/lint.js +128 -65
  31. package/dist/next-steps.d.ts +23 -0
  32. package/dist/next-steps.js +4 -0
  33. package/dist/outdated.js +101 -26
  34. package/dist/pack.js +883 -351
  35. package/dist/patch.js +11 -1
  36. package/dist/plan.js +32 -10
  37. package/dist/process-registry.d.ts +9 -0
  38. package/dist/process-registry.js +15 -0
  39. package/dist/ps-slash.js +15 -0
  40. package/dist/read.js +104 -23
  41. package/dist/replace.js +11 -3
  42. package/dist/scaffold.js +9 -6
  43. package/dist/session-kanban.js +21 -5
  44. package/dist/task.js +3 -1
  45. package/dist/test.js +128 -65
  46. package/dist/todo.js +3 -1
  47. package/dist/tool-tier.js +883 -351
  48. package/dist/tool-use.d.ts +1 -1
  49. package/dist/tool-use.js +28 -4
  50. package/dist/tree.d.ts +1 -0
  51. package/dist/tree.js +44 -8
  52. package/dist/typecheck.js +128 -65
  53. package/dist/write.js +9 -6
  54. package/package.json +5 -4
  55. package/dist/_concurrency.d.ts.map +0 -1
  56. package/dist/_danger-detect.d.ts.map +0 -1
  57. package/dist/_edit-match.d.ts.map +0 -1
  58. package/dist/_env.d.ts.map +0 -1
  59. package/dist/_fetch-guard.d.ts.map +0 -1
  60. package/dist/_output-spool.d.ts.map +0 -1
  61. package/dist/_redact-command.d.ts.map +0 -1
  62. package/dist/_regex.d.ts.map +0 -1
  63. package/dist/_session-shell.d.ts.map +0 -1
  64. package/dist/_shell-pick.d.ts.map +0 -1
  65. package/dist/_spawn-stream.d.ts.map +0 -1
  66. package/dist/_syntax-check.d.ts.map +0 -1
  67. package/dist/_util.d.ts.map +0 -1
  68. package/dist/_win32-resolve.d.ts.map +0 -1
  69. package/dist/audit.d.ts.map +0 -1
  70. package/dist/audit.js.map +0 -7
  71. package/dist/auto-proceed-loop-guard.d.ts.map +0 -1
  72. package/dist/auto-proceed-loop-guard.js.map +0 -7
  73. package/dist/bash-kill-guard.d.ts.map +0 -1
  74. package/dist/bash.d.ts.map +0 -1
  75. package/dist/bash.js.map +0 -7
  76. package/dist/batch-tool-use.d.ts.map +0 -1
  77. package/dist/batch-tool-use.js.map +0 -7
  78. package/dist/browser/artifacts.d.ts.map +0 -1
  79. package/dist/browser/index.d.ts.map +0 -1
  80. package/dist/browser/index.js.map +0 -7
  81. package/dist/browser/manager.d.ts.map +0 -1
  82. package/dist/browser/network-guard-proxy.d.ts.map +0 -1
  83. package/dist/browser/security.d.ts.map +0 -1
  84. package/dist/browser/tools.d.ts.map +0 -1
  85. package/dist/browser/types.d.ts.map +0 -1
  86. package/dist/builtin.d.ts.map +0 -1
  87. package/dist/builtin.js.map +0 -7
  88. package/dist/circuit-breaker.d.ts.map +0 -1
  89. package/dist/circuit-breaker.js.map +0 -7
  90. package/dist/codebase-index/background-indexer.d.ts.map +0 -1
  91. package/dist/codebase-index/bm25.d.ts.map +0 -1
  92. package/dist/codebase-index/circuit-breaker.d.ts.map +0 -1
  93. package/dist/codebase-index/codebase-index-tool.d.ts.map +0 -1
  94. package/dist/codebase-index/codebase-search-tool.d.ts.map +0 -1
  95. package/dist/codebase-index/codebase-stats-tool.d.ts.map +0 -1
  96. package/dist/codebase-index/dead-code-scan.d.ts.map +0 -1
  97. package/dist/codebase-index/generic-parser.d.ts.map +0 -1
  98. package/dist/codebase-index/gitignore.d.ts.map +0 -1
  99. package/dist/codebase-index/go-parser.d.ts.map +0 -1
  100. package/dist/codebase-index/index-service.d.ts.map +0 -1
  101. package/dist/codebase-index/index.d.ts.map +0 -1
  102. package/dist/codebase-index/index.js.map +0 -7
  103. package/dist/codebase-index/indexer.d.ts.map +0 -1
  104. package/dist/codebase-index/json-parser.d.ts.map +0 -1
  105. package/dist/codebase-index/languages.d.ts.map +0 -1
  106. package/dist/codebase-index/lsp-kind.d.ts.map +0 -1
  107. package/dist/codebase-index/parser-dispatch.d.ts.map +0 -1
  108. package/dist/codebase-index/project-server-cache.d.ts.map +0 -1
  109. package/dist/codebase-index/project-server-client.d.ts.map +0 -1
  110. package/dist/codebase-index/project-server-endpoint.d.ts.map +0 -1
  111. package/dist/codebase-index/project-server-protocol.d.ts.map +0 -1
  112. package/dist/codebase-index/project-server.d.ts.map +0 -1
  113. package/dist/codebase-index/project-server.js.map +0 -7
  114. package/dist/codebase-index/py-parser.d.ts.map +0 -1
  115. package/dist/codebase-index/refs-extractor.d.ts.map +0 -1
  116. package/dist/codebase-index/rs-parser.d.ts.map +0 -1
  117. package/dist/codebase-index/schema.d.ts.map +0 -1
  118. package/dist/codebase-index/spawn-gate.d.ts.map +0 -1
  119. package/dist/codebase-index/sqlite-runtime.d.ts.map +0 -1
  120. package/dist/codebase-index/ts-parser.d.ts.map +0 -1
  121. package/dist/codebase-index/worker-protocol.d.ts.map +0 -1
  122. package/dist/codebase-index/worker.d.ts.map +0 -1
  123. package/dist/codebase-index/worker.js.map +0 -7
  124. package/dist/codebase-index/writer-admin.d.ts.map +0 -1
  125. package/dist/codebase-index/writer-bulk-insert.d.ts.map +0 -1
  126. package/dist/codebase-index/writer-graph-helpers.d.ts.map +0 -1
  127. package/dist/codebase-index/writer-graph-reader.d.ts.map +0 -1
  128. package/dist/codebase-index/writer-helpers.d.ts.map +0 -1
  129. package/dist/codebase-index/writer-pragmas.d.ts.map +0 -1
  130. package/dist/codebase-index/writer-ref-mapper.d.ts.map +0 -1
  131. package/dist/codebase-index/writer-schema.d.ts.map +0 -1
  132. package/dist/codebase-index/writer-search-helpers.d.ts.map +0 -1
  133. package/dist/codebase-index/writer-store-pool.d.ts.map +0 -1
  134. package/dist/codebase-index/writer.d.ts.map +0 -1
  135. package/dist/codebase-index/yaml-parser.d.ts.map +0 -1
  136. package/dist/design.d.ts.map +0 -1
  137. package/dist/design.js.map +0 -7
  138. package/dist/diff.d.ts.map +0 -1
  139. package/dist/diff.js.map +0 -7
  140. package/dist/document.d.ts.map +0 -1
  141. package/dist/document.js.map +0 -7
  142. package/dist/e2e.d.ts.map +0 -1
  143. package/dist/e2e.js.map +0 -7
  144. package/dist/edit.d.ts.map +0 -1
  145. package/dist/edit.js.map +0 -7
  146. package/dist/exec-kill-guard.d.ts.map +0 -1
  147. package/dist/exec.d.ts.map +0 -1
  148. package/dist/exec.js.map +0 -7
  149. package/dist/fetch.d.ts.map +0 -1
  150. package/dist/fetch.js.map +0 -7
  151. package/dist/format.d.ts.map +0 -1
  152. package/dist/format.js.map +0 -7
  153. package/dist/git.d.ts.map +0 -1
  154. package/dist/git.js.map +0 -7
  155. package/dist/glob.d.ts.map +0 -1
  156. package/dist/glob.js.map +0 -7
  157. package/dist/grep.d.ts.map +0 -1
  158. package/dist/grep.js.map +0 -7
  159. package/dist/index.d.ts.map +0 -1
  160. package/dist/index.js.map +0 -7
  161. package/dist/install.d.ts.map +0 -1
  162. package/dist/install.js.map +0 -7
  163. package/dist/json.d.ts.map +0 -1
  164. package/dist/json.js.map +0 -7
  165. package/dist/kanban-board-inputs.d.ts.map +0 -1
  166. package/dist/kanban-decomposition-actions.d.ts.map +0 -1
  167. package/dist/kanban-detail-actions.d.ts.map +0 -1
  168. package/dist/kanban-evidence-bridge.d.ts.map +0 -1
  169. package/dist/kanban-presence.d.ts.map +0 -1
  170. package/dist/kanban-split-task-handler.d.ts.map +0 -1
  171. package/dist/kanban-task-inputs.d.ts.map +0 -1
  172. package/dist/kanban-tool-results.d.ts.map +0 -1
  173. package/dist/kanban-tool-schema.d.ts.map +0 -1
  174. package/dist/kanban-tool-types.d.ts.map +0 -1
  175. package/dist/kanban.d.ts.map +0 -1
  176. package/dist/kanban.js.map +0 -7
  177. package/dist/languages/detect.d.ts.map +0 -1
  178. package/dist/languages/diagnostics.d.ts.map +0 -1
  179. package/dist/languages/execute-tool.d.ts.map +0 -1
  180. package/dist/languages/execute.d.ts.map +0 -1
  181. package/dist/languages/index.d.ts.map +0 -1
  182. package/dist/languages/index.js.map +0 -7
  183. package/dist/languages/legacy-bridge.d.ts.map +0 -1
  184. package/dist/languages/package-tool.d.ts.map +0 -1
  185. package/dist/languages/plan.d.ts.map +0 -1
  186. package/dist/languages/profile-helpers.d.ts.map +0 -1
  187. package/dist/languages/profiles/additional.d.ts.map +0 -1
  188. package/dist/languages/profiles/primary.d.ts.map +0 -1
  189. package/dist/languages/registry.d.ts.map +0 -1
  190. package/dist/languages/tool.d.ts.map +0 -1
  191. package/dist/languages/types.d.ts.map +0 -1
  192. package/dist/lint.d.ts.map +0 -1
  193. package/dist/lint.js.map +0 -7
  194. package/dist/logs.d.ts.map +0 -1
  195. package/dist/logs.js.map +0 -7
  196. package/dist/memory.d.ts.map +0 -1
  197. package/dist/memory.js.map +0 -7
  198. package/dist/mode.d.ts.map +0 -1
  199. package/dist/mode.js.map +0 -7
  200. package/dist/next-steps.d.ts.map +0 -1
  201. package/dist/next-steps.js.map +0 -7
  202. package/dist/outdated.d.ts.map +0 -1
  203. package/dist/outdated.js.map +0 -7
  204. package/dist/pack.d.ts.map +0 -1
  205. package/dist/pack.js.map +0 -7
  206. package/dist/patch.d.ts.map +0 -1
  207. package/dist/patch.js.map +0 -7
  208. package/dist/plan.d.ts.map +0 -1
  209. package/dist/plan.js.map +0 -7
  210. package/dist/process-guardian.d.ts.map +0 -1
  211. package/dist/process-registry-persistent.d.ts.map +0 -1
  212. package/dist/process-registry.d.ts.map +0 -1
  213. package/dist/process-registry.js.map +0 -7
  214. package/dist/ps-slash.d.ts.map +0 -1
  215. package/dist/ps-slash.js.map +0 -7
  216. package/dist/read.d.ts.map +0 -1
  217. package/dist/read.js.map +0 -7
  218. package/dist/replace.d.ts.map +0 -1
  219. package/dist/replace.js.map +0 -7
  220. package/dist/scaffold.d.ts.map +0 -1
  221. package/dist/scaffold.js.map +0 -7
  222. package/dist/search.d.ts.map +0 -1
  223. package/dist/search.js.map +0 -7
  224. package/dist/session-kanban.d.ts.map +0 -1
  225. package/dist/session-kanban.js.map +0 -7
  226. package/dist/set-working-dir.d.ts.map +0 -1
  227. package/dist/skill.d.ts.map +0 -1
  228. package/dist/skill.js.map +0 -7
  229. package/dist/spawn-background.d.ts.map +0 -1
  230. package/dist/task.d.ts.map +0 -1
  231. package/dist/task.js.map +0 -7
  232. package/dist/test.d.ts.map +0 -1
  233. package/dist/test.js.map +0 -7
  234. package/dist/todo.d.ts.map +0 -1
  235. package/dist/todo.js.map +0 -7
  236. package/dist/tool-diff.d.ts.map +0 -1
  237. package/dist/tool-diff.js.map +0 -7
  238. package/dist/tool-help.d.ts.map +0 -1
  239. package/dist/tool-help.js.map +0 -7
  240. package/dist/tool-icon-map.d.ts.map +0 -1
  241. package/dist/tool-icons.d.ts.map +0 -1
  242. package/dist/tool-icons.js.map +0 -7
  243. package/dist/tool-search.d.ts.map +0 -1
  244. package/dist/tool-search.js.map +0 -7
  245. package/dist/tool-summary.d.ts.map +0 -1
  246. package/dist/tool-summary.js.map +0 -7
  247. package/dist/tool-tier.d.ts.map +0 -1
  248. package/dist/tool-tier.js.map +0 -7
  249. package/dist/tool-use.d.ts.map +0 -1
  250. package/dist/tool-use.js.map +0 -7
  251. package/dist/tree.d.ts.map +0 -1
  252. package/dist/tree.js.map +0 -7
  253. package/dist/typecheck.d.ts.map +0 -1
  254. package/dist/typecheck.js.map +0 -7
  255. package/dist/win32.d.ts.map +0 -1
  256. package/dist/win32.js.map +0 -7
  257. package/dist/write.d.ts.map +0 -1
  258. package/dist/write.js.map +0 -7
package/dist/patch.js CHANGED
@@ -45,6 +45,10 @@ var patchTool = {
45
45
  useInstead: ["edit"]
46
46
  },
47
47
  permission: "confirm",
48
+ // WS-046: gives permission decisions something to key on.
49
+ // The target directory. The patch body itself is unstable (every diff
50
+ // differs), so it could never re-match its own stored rule.
51
+ subjectKey: "directory",
48
52
  mutating: true,
49
53
  capabilities: ["fs.write"],
50
54
  icon: "edit",
@@ -168,7 +172,13 @@ function runPatch(args, cwd, signal) {
168
172
  let stdout = "";
169
173
  let stderr = "";
170
174
  const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
171
- const child = spawn("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
175
+ const child = spawn("patch", args, {
176
+ cwd,
177
+ signal,
178
+ env,
179
+ stdio: ["pipe", "pipe", "pipe"],
180
+ windowsHide: true
181
+ });
172
182
  child.stdout?.on("data", (c) => {
173
183
  stdout += c.toString();
174
184
  });
package/dist/plan.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/plan.ts
2
+ import { randomUUID } from "node:crypto";
2
3
  import {
3
4
  addPlanItem,
4
5
  clearPlan,
@@ -6,14 +7,11 @@ import {
6
7
  formatPlan,
7
8
  getPlanTemplate,
8
9
  mutatePlan as mutatePlan2,
10
+ mutateTasks as mutateTasks2,
9
11
  removePlanItem,
10
12
  setPlanItemStatus
11
13
  } from "@wrongstack/core/storage";
12
- import {
13
- mutateTasks as mutateTasks2
14
- } from "@wrongstack/core/storage";
15
14
  import { formatTaskList } from "@wrongstack/core/utils";
16
- import { randomUUID } from "node:crypto";
17
15
 
18
16
  // src/session-kanban.ts
19
17
  import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
@@ -77,7 +75,9 @@ async function ensureSessionKanbanBoard(projectRoot, sessionId) {
77
75
  description: "Live session work: todos, tasks, and plan items.",
78
76
  tags: sessionBoardTags(sessionId),
79
77
  columns: SESSION_KANBAN_COLUMNS,
80
- generatedBy: `session-kanban:${sessionId}`
78
+ generatedBy: `session-kanban:${sessionId}`,
79
+ kind: "session_mirror",
80
+ retention: { mode: "archive_after_ttl", ttlMs: 7 * 24 * 60 * 60 * 1e3 }
81
81
  });
82
82
  }
83
83
  if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
@@ -173,6 +173,9 @@ var planTool = {
173
173
  description: 'Manage a session-persistent strategic plan. The plan is written to disk and survives conversation resumptions within the same session, but is isolated to this session \u2014 other sessions have their own separate plans. Unlike todos (which are per-turn and lost on restart), a plan tracks high-level progress across multiple turns. Use this to outline big-picture work, then promote concrete items into the todo list when ready to execute. By default plans are isolated to this session; use `scope: "project"` to store the plan in a shared project-level file visible to all sessions.',
174
174
  usageHint: 'RECOMMENDED FOR COMPLEX, MULTI-PHASE WORK:\n\n- Start by creating a high-level plan with `action: "add"` or using templates (`template_use`).\n- Use `promote` to turn a plan item into actionable todos.\n- Use `taskify` to convert a plan item into a structured task (with type/priority/deps).\n- Keep plans at the "why and what" level, and todos at the "how and next step" level.\n- Common templates: "new-feature", "bug-fix", "refactor", "release", "security-audit".\n\nThis tool is excellent for maintaining long-term direction across many turns within a session. Plans survive resume but are not shared across separate sessions.\nUse `scope: "project"` to use a shared project-level plan file.',
175
175
  permission: "confirm",
176
+ // WS-046: gives permission decisions something to key on.
177
+ // The action performed; plan has no single file or path subject.
178
+ subjectKey: "action",
176
179
  mutating: true,
177
180
  capabilities: ["fs.write"],
178
181
  icon: "plan",
@@ -229,7 +232,10 @@ var planTool = {
229
232
  let planPath;
230
233
  if (input.scope === "project") {
231
234
  if (typeof sessionPlanPath === "string") {
232
- const lastSep = Math.max(sessionPlanPath.lastIndexOf("/"), sessionPlanPath.lastIndexOf("\\"));
235
+ const lastSep = Math.max(
236
+ sessionPlanPath.lastIndexOf("/"),
237
+ sessionPlanPath.lastIndexOf("\\")
238
+ );
233
239
  planPath = lastSep >= 0 ? sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
234
240
  }
235
241
  } else {
@@ -266,7 +272,11 @@ var planTool = {
266
272
  case "start":
267
273
  case "done": {
268
274
  if (!input.target) {
269
- early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
275
+ early = mkResult(
276
+ p,
277
+ false,
278
+ `${input.action} requires \`target\` (id|index|substring).`
279
+ );
270
280
  return p;
271
281
  }
272
282
  const next = setPlanItemStatus(
@@ -294,7 +304,11 @@ var planTool = {
294
304
  }
295
305
  case "promote": {
296
306
  if (!input.target) {
297
- early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
307
+ early = mkResult(
308
+ p,
309
+ false,
310
+ `${input.action} requires \`target\` (id|index|substring).`
311
+ );
298
312
  return p;
299
313
  }
300
314
  const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
@@ -337,7 +351,11 @@ var planTool = {
337
351
  return clearPlan(p);
338
352
  case "taskify": {
339
353
  if (!input.target) {
340
- early = mkResult(p, false, "taskify requires `target` (plan item id|index|substring).");
354
+ early = mkResult(
355
+ p,
356
+ false,
357
+ "taskify requires `target` (plan item id|index|substring)."
358
+ );
341
359
  return p;
342
360
  }
343
361
  let itemIdx = -1;
@@ -410,7 +428,11 @@ var planTool = {
410
428
  ${formatTaskList(taskFile.tasks)}`
411
429
  );
412
430
  } catch (err) {
413
- return mkResult(plan, false, `taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`);
431
+ return mkResult(
432
+ plan,
433
+ false,
434
+ `taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`
435
+ );
414
436
  }
415
437
  }
416
438
  return mkResult(plan, true, `Plan ${input.action} ok.`);
@@ -223,6 +223,15 @@ export declare class ProcessRegistryImpl {
223
223
  * process that has been replaced by a new one with the same PID.
224
224
  */
225
225
  private _pruneStale;
226
+ /**
227
+ * Remove every stale entry, not just one PID. `list()`/`stats()` — the
228
+ * surfaces the TUI status bar and `/ps` poll — must prune too: a child
229
+ * whose 'close' event never fires (e.g. Windows grandchildren holding stdio
230
+ * open) would otherwise linger in the registry until someone looks up its
231
+ * exact PID, and PID reuse meanwhile makes `get()`/`kill()` target the
232
+ * wrong process. RAM-leak audit 2026-07-31, MEDIUM.
233
+ */
234
+ private _pruneAllStale;
226
235
  }
227
236
  export declare function getProcessRegistry(): ProcessRegistryImpl;
228
237
  /** Reset for tests. */
@@ -329,6 +329,7 @@ var ProcessRegistryImpl = class {
329
329
  }
330
330
  /** Get all tracked processes. */
331
331
  list() {
332
+ this._pruneAllStale();
332
333
  return Array.from(this.processes.values());
333
334
  }
334
335
  /** Get processes filtered by name (e.g. 'bash', 'exec'). */
@@ -359,6 +360,7 @@ var ProcessRegistryImpl = class {
359
360
  * Combined stats for observability — used by /ps and the TUI status bar.
360
361
  */
361
362
  stats() {
363
+ this._pruneAllStale();
362
364
  return {
363
365
  activeCount: this.activeCount,
364
366
  backgroundCount: this.activeBackgroundCount,
@@ -598,6 +600,19 @@ var ProcessRegistryImpl = class {
598
600
  this.processes.delete(pid);
599
601
  }
600
602
  }
603
+ /**
604
+ * Remove every stale entry, not just one PID. `list()`/`stats()` — the
605
+ * surfaces the TUI status bar and `/ps` poll — must prune too: a child
606
+ * whose 'close' event never fires (e.g. Windows grandchildren holding stdio
607
+ * open) would otherwise linger in the registry until someone looks up its
608
+ * exact PID, and PID reuse meanwhile makes `get()`/`kill()` target the
609
+ * wrong process. RAM-leak audit 2026-07-31, MEDIUM.
610
+ */
611
+ _pruneAllStale() {
612
+ for (const [pid, entry] of this.processes) {
613
+ if (this._isStaleEntry(entry)) this.processes.delete(pid);
614
+ }
615
+ }
601
616
  };
602
617
  var _registry;
603
618
  function getProcessRegistry() {
package/dist/ps-slash.js CHANGED
@@ -299,6 +299,7 @@ var ProcessRegistryImpl = class {
299
299
  }
300
300
  /** Get all tracked processes. */
301
301
  list() {
302
+ this._pruneAllStale();
302
303
  return Array.from(this.processes.values());
303
304
  }
304
305
  /** Get processes filtered by name (e.g. 'bash', 'exec'). */
@@ -329,6 +330,7 @@ var ProcessRegistryImpl = class {
329
330
  * Combined stats for observability — used by /ps and the TUI status bar.
330
331
  */
331
332
  stats() {
333
+ this._pruneAllStale();
332
334
  return {
333
335
  activeCount: this.activeCount,
334
336
  backgroundCount: this.activeBackgroundCount,
@@ -568,6 +570,19 @@ var ProcessRegistryImpl = class {
568
570
  this.processes.delete(pid);
569
571
  }
570
572
  }
573
+ /**
574
+ * Remove every stale entry, not just one PID. `list()`/`stats()` — the
575
+ * surfaces the TUI status bar and `/ps` poll — must prune too: a child
576
+ * whose 'close' event never fires (e.g. Windows grandchildren holding stdio
577
+ * open) would otherwise linger in the registry until someone looks up its
578
+ * exact PID, and PID reuse meanwhile makes `get()`/`kill()` target the
579
+ * wrong process. RAM-leak audit 2026-07-31, MEDIUM.
580
+ */
581
+ _pruneAllStale() {
582
+ for (const [pid, entry] of this.processes) {
583
+ if (this._isStaleEntry(entry)) this.processes.delete(pid);
584
+ }
585
+ }
571
586
  };
572
587
  var _registry;
573
588
  function getProcessRegistry() {
package/dist/read.js CHANGED
@@ -1610,9 +1610,9 @@ function parseSymbols6(opts) {
1610
1610
  function regexParse2(opts) {
1611
1611
  const { file, content, lang } = opts;
1612
1612
  const symbols = [];
1613
- const basename5 = path8.basename(file).toLowerCase();
1614
- const isPackageJson = basename5 === "package.json";
1615
- const isTsconfig = basename5 === "tsconfig.json" || basename5 === "tsconfig.build.json";
1613
+ const basename6 = path8.basename(file).toLowerCase();
1614
+ const isPackageJson = basename6 === "package.json";
1615
+ const isTsconfig = basename6 === "tsconfig.json" || basename6 === "tsconfig.build.json";
1616
1616
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
1617
1617
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
1618
1618
  const lines = content.split("\n");
@@ -2001,12 +2001,13 @@ function ensureInsideRoot(absPath, ctx) {
2001
2001
  function safeResolve(input, ctx) {
2002
2002
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
2003
2003
  }
2004
- async function assertRealInsideRoot(absPath, ctx) {
2005
- if (ctx.allowOutsideProjectRoot) return;
2004
+ async function resolveRealInsideRoot(absPath, ctx) {
2005
+ if (ctx.allowOutsideProjectRoot) return absPath;
2006
2006
  const realRoots = await Promise.all(
2007
2007
  allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r)))
2008
2008
  );
2009
2009
  let probe2 = absPath;
2010
+ const pendingTail = [];
2010
2011
  for (; ; ) {
2011
2012
  let real;
2012
2013
  try {
@@ -2014,13 +2015,16 @@ async function assertRealInsideRoot(absPath, ctx) {
2014
2015
  } catch (err) {
2015
2016
  if (err.code === "ENOENT") {
2016
2017
  const parent = path.dirname(probe2);
2017
- if (parent === probe2) return;
2018
+ if (parent === probe2) return absPath;
2019
+ pendingTail.unshift(path.basename(probe2));
2018
2020
  probe2 = parent;
2019
2021
  continue;
2020
2022
  }
2021
2023
  throw err;
2022
2024
  }
2023
- if (isInsideAny(real, realRoots)) return;
2025
+ if (isInsideAny(real, realRoots)) {
2026
+ return pendingTail.length > 0 ? path.join(real, ...pendingTail) : real;
2027
+ }
2024
2028
  throw new Error(
2025
2029
  `Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
2026
2030
  );
@@ -2028,8 +2032,7 @@ async function assertRealInsideRoot(absPath, ctx) {
2028
2032
  }
2029
2033
  async function safeResolveReal(input, ctx) {
2030
2034
  const abs = safeResolve(input, ctx);
2031
- await assertRealInsideRoot(abs, ctx);
2032
- return abs;
2035
+ return await resolveRealInsideRoot(abs, ctx);
2033
2036
  }
2034
2037
  function isBinaryBuffer(buf) {
2035
2038
  const len = Math.min(buf.length, 8192);
@@ -3006,6 +3009,7 @@ var REFS_INDEX_SQL = [
3006
3009
  var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
3007
3010
 
3008
3011
  // src/codebase-index/writer-search-helpers.ts
3012
+ var SEARCH_CANDIDATE_SCAN_CAP = 5e3;
3009
3013
  function normalizeSearchLimit(limit) {
3010
3014
  return typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.trunc(limit)) : void 0;
3011
3015
  }
@@ -3035,8 +3039,8 @@ function buildWriterSearchWhere(query, filter) {
3035
3039
  }
3036
3040
  if (query.trim()) {
3037
3041
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
3038
- conditions.push(`(${tokens.map(() => "text LIKE ?").join(" OR ")})`);
3039
- for (const token of tokens) values.push(`%${token}%`);
3042
+ conditions.push(`(${tokens.map(() => "text LIKE ? ESCAPE '\\'").join(" OR ")})`);
3043
+ for (const token of tokens) values.push(`%${escapeLike(token)}%`);
3040
3044
  }
3041
3045
  return { where: conditions.length ? `WHERE ${conditions.join(" AND ")}` : "", values };
3042
3046
  }
@@ -3145,6 +3149,7 @@ var StorePool = class {
3145
3149
 
3146
3150
  // src/codebase-index/writer.ts
3147
3151
  var DB_FILE2 = "index.db";
3152
+ var MAX_STATEMENT_CACHE = 128;
3148
3153
  var IndexStore = class _IndexStore {
3149
3154
  db;
3150
3155
  /** Absolute path to this project's index directory. */
@@ -3182,12 +3187,31 @@ var IndexStore = class _IndexStore {
3182
3187
  // Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
3183
3188
  // an empty or pre-existing corpus makes a stale IDF table meaningless.
3184
3189
  bm25Dirty = true;
3185
- /** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
3190
+ /**
3191
+ * Prepare-once helper: compile `sql` on first use, reuse thereafter.
3192
+ *
3193
+ * Bounded LRU rather than an open Map. The cache is keyed by SQL TEXT, and
3194
+ * the fallback search builder emits one `text LIKE ?` clause per query token
3195
+ * — so the SQL varies with the token count and a stream of differently-sized
3196
+ * queries grew the cache without limit. Sage's store already bounds its
3197
+ * equivalent at 128 (WS-096).
3198
+ *
3199
+ * Re-inserting on a hit keeps the hot fixed-SQL statements (upsertFile,
3200
+ * insertRefs, …) at the young end, so a burst of one-off search SQL evicts
3201
+ * itself rather than the reindex hot path.
3202
+ */
3186
3203
  stmt(sql) {
3187
- let s = this.stmtCache.get(sql);
3188
- if (s === void 0) {
3189
- s = this.db.prepare(sql);
3190
- this.stmtCache.set(sql, s);
3204
+ const cached = this.stmtCache.get(sql);
3205
+ if (cached !== void 0) {
3206
+ this.stmtCache.delete(sql);
3207
+ this.stmtCache.set(sql, cached);
3208
+ return cached;
3209
+ }
3210
+ const s = this.db.prepare(sql);
3211
+ this.stmtCache.set(sql, s);
3212
+ if (this.stmtCache.size > MAX_STATEMENT_CACHE) {
3213
+ const oldest = this.stmtCache.keys().next();
3214
+ if (!oldest.done) this.stmtCache.delete(oldest.value);
3191
3215
  }
3192
3216
  return s;
3193
3217
  }
@@ -3460,7 +3484,7 @@ var IndexStore = class _IndexStore {
3460
3484
  const { where, values } = built;
3461
3485
  const limit = normalizeSearchLimit(opts?.limit);
3462
3486
  const limitSql = limit !== void 0 ? " LIMIT ?" : "";
3463
- const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}${limitSql}`;
3487
+ const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment FROM symbols ${where}${limitSql}`;
3464
3488
  const binds = limit !== void 0 ? [...values, limit] : values;
3465
3489
  const rows = this.stmt(sql).all(
3466
3490
  ...binds
@@ -3589,11 +3613,13 @@ var IndexStore = class _IndexStore {
3589
3613
  /** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
3590
3614
  searchRankedFallback(query, filter, limit) {
3591
3615
  if (!query.trim()) {
3592
- const total = this.countSearch(query, filter);
3593
- if (total === 0) return { results: [], total: 0 };
3594
- return { results: this.search(query, filter, { limit }), total };
3616
+ const total2 = this.countSearch(query, filter);
3617
+ if (total2 === 0) return { results: [], total: 0 };
3618
+ return { results: this.search(query, filter, { limit }), total: total2 };
3595
3619
  }
3596
- const candidates = this.search(query, filter);
3620
+ const total = this.countSearch(query, filter);
3621
+ if (total === 0) return { results: [], total: 0 };
3622
+ const candidates = this.search(query, filter, { limit: SEARCH_CANDIDATE_SCAN_CAP });
3597
3623
  if (candidates.length === 0) return { results: [], total: 0 };
3598
3624
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
3599
3625
  const bm25 = this.getOrBuildBm25();
@@ -3619,7 +3645,7 @@ var IndexStore = class _IndexStore {
3619
3645
  const c = expectDefined4(candidateById.get(id));
3620
3646
  return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
3621
3647
  });
3622
- return { results, total: candidates.length };
3648
+ return { results, total };
3623
3649
  }
3624
3650
  getAllIndexable() {
3625
3651
  return getAllIndexableWithStatement((sql) => this.stmt(sql));
@@ -4726,6 +4752,8 @@ var ProjectServerConnection = class {
4726
4752
  connectResolve = null;
4727
4753
  connectReject = null;
4728
4754
  nextId = 1;
4755
+ /** WS-027: read from the owner-only metadata file — see currentAuthToken. */
4756
+ authToken;
4729
4757
  pending = /* @__PURE__ */ new Map();
4730
4758
  transition(status, options = {}) {
4731
4759
  const previous = connectionStates.get(this.endpoint);
@@ -4746,6 +4774,10 @@ var ProjectServerConnection = class {
4746
4774
  isConnected() {
4747
4775
  return this.socket !== null && !this.socket.destroyed && this.info !== null;
4748
4776
  }
4777
+ /** Safe LRU candidate: no request, connect, or health probe is in flight. */
4778
+ isEvictable() {
4779
+ return this.pending.size === 0 && this.connecting === null && this.healthCheck === null;
4780
+ }
4749
4781
  async checkHealth(spawnIfMissing = false, timeoutMs = SERVER_HEALTH_TIMEOUT_MS) {
4750
4782
  await this.ensureConnected(spawnIfMissing);
4751
4783
  if (this.healthCheck) return this.healthCheck;
@@ -4858,6 +4890,31 @@ var ProjectServerConnection = class {
4858
4890
  this.transition("offline");
4859
4891
  maybeStopHeartbeatLoop();
4860
4892
  }
4893
+ /**
4894
+ * WS-027: the per-process token, read from the daemon's owner-only metadata
4895
+ * file — never from the `hello` frame, which the daemon sends to every
4896
+ * socket that connects (the mistake WS-028 found in the SAGE daemon).
4897
+ *
4898
+ * Read lazily and re-read while unknown: the daemon starts listening before
4899
+ * it writes metadata (endpoint ownership has to be won first), and a
4900
+ * respawned daemon mints a new token, so a cached wrong one would be sticky.
4901
+ */
4902
+ currentAuthToken() {
4903
+ if (this.authToken === void 0) {
4904
+ try {
4905
+ const raw = fs10.readFileSync(
4906
+ projectIndexServerMetadataPath(this.projectRoot, this.indexDir),
4907
+ "utf8"
4908
+ );
4909
+ const parsed = JSON.parse(raw);
4910
+ if (typeof parsed.authToken === "string" && parsed.authToken.length > 0) {
4911
+ this.authToken = parsed.authToken;
4912
+ }
4913
+ } catch {
4914
+ }
4915
+ }
4916
+ return this.authToken;
4917
+ }
4861
4918
  request(message, options) {
4862
4919
  const socket = this.socket;
4863
4920
  if (!socket || socket.destroyed) {
@@ -4901,7 +4958,7 @@ var ProjectServerConnection = class {
4901
4958
  return;
4902
4959
  }
4903
4960
  }
4904
- this.write({ ...message, id });
4961
+ this.write({ ...message, id, authToken: this.currentAuthToken() });
4905
4962
  });
4906
4963
  }
4907
4964
  async ensureConnected(spawnIfMissing) {
@@ -4948,6 +5005,7 @@ var ProjectServerConnection = class {
4948
5005
  connectOnce() {
4949
5006
  this.socket?.destroy();
4950
5007
  this.socket = null;
5008
+ this.authToken = void 0;
4951
5009
  this.info = null;
4952
5010
  this.activity = null;
4953
5011
  this.health = null;
@@ -5144,7 +5202,26 @@ var ProjectServerConnection = class {
5144
5202
  }
5145
5203
  };
5146
5204
  var connections = /* @__PURE__ */ new Map();
5205
+ var MAX_CACHED_CONNECTIONS = 8;
5147
5206
  var heartbeatTimer;
5207
+ function forgetConnection(endpoint, connection) {
5208
+ if (connections.get(endpoint) === connection) connections.delete(endpoint);
5209
+ connection.close();
5210
+ connectionStates.delete(endpoint);
5211
+ if (latestConnectionState.endpoint !== endpoint) return;
5212
+ latestConnectionState = [...connectionStates.values()].at(-1) ?? {
5213
+ status: isProjectIndexServerAvailable() ? "offline" : "unavailable",
5214
+ connected: false
5215
+ };
5216
+ }
5217
+ function trimConnectionCache(protectedConnection) {
5218
+ if (connections.size <= MAX_CACHED_CONNECTIONS) return;
5219
+ for (const [endpoint, connection] of connections) {
5220
+ if (connections.size <= MAX_CACHED_CONNECTIONS) break;
5221
+ if (connection === protectedConnection || !connection.isEvictable()) continue;
5222
+ forgetConnection(endpoint, connection);
5223
+ }
5224
+ }
5148
5225
  function ensureHeartbeatLoop() {
5149
5226
  if (heartbeatTimer) return;
5150
5227
  heartbeatTimer = setInterval(() => {
@@ -5167,7 +5244,11 @@ function connectionFor(projectRoot, indexDir) {
5167
5244
  if (!connection) {
5168
5245
  connection = new ProjectServerConnection(projectRoot, indexDir, endpoint);
5169
5246
  connections.set(endpoint, connection);
5247
+ } else {
5248
+ connections.delete(endpoint);
5249
+ connections.set(endpoint, connection);
5170
5250
  }
5251
+ trimConnectionCache(connection);
5171
5252
  return connection;
5172
5253
  }
5173
5254
  function callProjectIndexServer(op, args, options) {
package/dist/replace.js CHANGED
@@ -1,18 +1,18 @@
1
1
  // src/replace.ts
2
- import { expectDefined } from "@wrongstack/core/utils";
3
2
  import { spawn } from "node:child_process";
4
3
  import * as fs from "node:fs/promises";
5
4
  import * as path2 from "node:path";
5
+ import { ToolValidationError } from "@wrongstack/core/types";
6
6
  import {
7
7
  atomicWrite,
8
8
  buildChildEnv,
9
9
  compileGlob,
10
10
  detectNewlineStyle,
11
+ expectDefined,
11
12
  normalizeToLf,
12
13
  toStyle,
13
14
  unifiedDiff
14
15
  } from "@wrongstack/core/utils";
15
- import { ToolValidationError } from "@wrongstack/core/types";
16
16
 
17
17
  // src/_regex.ts
18
18
  var MAX_PATTERN_LEN = 256;
@@ -100,6 +100,10 @@ var replaceTool = {
100
100
  description: "Perform a search-and-replace across multiple files using a regex pattern. This is a powerful bulk transformation tool. Dry-run is ON by default \u2014 set `dry_run: false` to apply changes.",
101
101
  usageHint: "DANGEROUS IF USED CARELESSLY \u2014 review the diff output carefully.\n\nRecommended workflow:\n1. Run without `dry_run: false` first to see exactly what would change (dry-run is the default).\n2. Review the diff output, then re-run with `dry_run: false` to apply.\n3. Use a specific enough `pattern` (and `glob` / `files`) to avoid accidental broad changes.\n4. `replace_all` controls whether only the first match per file or all matches are replaced.\nThis tool is excellent for large-scale refactors (renaming, import updates, etc.) but must be used with caution.",
102
102
  permission: "confirm",
103
+ // WS-046: gives permission decisions something to key on.
104
+ // The file scope being rewritten, not the pattern: a trust rule should say
105
+ // where a bulk replace may run.
106
+ subjectKey: "files",
103
107
  mutating: true,
104
108
  capabilities: ["fs.write"],
105
109
  icon: "edit",
@@ -259,7 +263,11 @@ async function globFiles(pattern, base, extraGlob) {
259
263
  function checkRg() {
260
264
  return new Promise((resolve2) => {
261
265
  try {
262
- const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", windowsHide: true });
266
+ const p = spawn("rg", ["--version"], {
267
+ env: buildChildEnv(),
268
+ stdio: "ignore",
269
+ windowsHide: true
270
+ });
263
271
  p.on("error", () => resolve2(false));
264
272
  p.on("close", (code) => resolve2(code === 0));
265
273
  } catch {
package/dist/scaffold.js CHANGED
@@ -127,6 +127,7 @@ var scaffoldTool = {
127
127
  usageHint: "PREFERRED FOR SCAFFOLDING:\n\n- Use built-in templates when they match your needs (e.g. react-component, npm-package).\n- Supports `dry_run` so you can preview exactly what will be created.\n- Has the powerful `fs.write.outside-project` capability \u2014 review paths carefully.\nMuch cleaner and safer than manually writing multiple files.",
128
128
  permission: "confirm",
129
129
  mutating: true,
130
+ subjectKey: "name",
130
131
  capabilities: ["fs.write.outside-project", "fs.write"],
131
132
  icon: "scaffold",
132
133
  timeoutMs: 3e4,
@@ -201,15 +202,17 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
201
202
  output: dryRun ? `Would create ${filesCreated} files: ${files.join(", ")}` : `Created ${filesCreated} files: ${files.join(", ")}`
202
203
  };
203
204
  }
205
+ function escapeRegexLiteral(value) {
206
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
207
+ }
204
208
  function substituteVars(content, name, vars) {
209
+ const kebab = name.toLowerCase().replace(/\s+/g, "-");
210
+ const pascal = name.replace(/(?:^|[-_\s]+)([a-z])/g, (_, c) => c.toUpperCase());
205
211
  let result = content;
206
- result = result.replace(/\{\{name\}\}/g, name.toLowerCase().replace(/\s+/g, "-"));
207
- result = result.replace(
208
- /\{\{Name\}\}/g,
209
- name.replace(/(?:^|[-_\s]+)([a-z])/g, (_, c) => c.toUpperCase())
210
- );
212
+ result = result.replace(/\{\{name\}\}/g, () => kebab);
213
+ result = result.replace(/\{\{Name\}\}/g, () => pascal);
211
214
  for (const [k, v] of Object.entries(vars)) {
212
- result = result.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v);
215
+ result = result.replace(new RegExp(`\\{\\{${escapeRegexLiteral(k)}\\}\\}`, "g"), () => v);
213
216
  }
214
217
  return result;
215
218
  }
@@ -90,7 +90,9 @@ async function ensureSessionKanbanBoard(projectRoot, sessionId) {
90
90
  description: "Live session work: todos, tasks, and plan items.",
91
91
  tags: sessionBoardTags(sessionId),
92
92
  columns: SESSION_KANBAN_COLUMNS,
93
- generatedBy: `session-kanban:${sessionId}`
93
+ generatedBy: `session-kanban:${sessionId}`,
94
+ kind: "session_mirror",
95
+ retention: { mode: "archive_after_ttl", ttlMs: 7 * 24 * 60 * 60 * 1e3 }
94
96
  });
95
97
  }
96
98
  if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
@@ -325,12 +327,26 @@ function projectSessionPlanToKanban(projectRoot, items, sessionId) {
325
327
  function fireAndForget(context, work) {
326
328
  void work.catch((err) => {
327
329
  const message = err instanceof Error ? err.message : String(err);
328
- console.warn(JSON.stringify({ level: "warn", event: "session-kanban", context, message, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
330
+ console.warn(
331
+ JSON.stringify({
332
+ level: "warn",
333
+ event: "session-kanban",
334
+ context,
335
+ message,
336
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
337
+ })
338
+ );
329
339
  });
330
340
  }
331
341
  function broadcastTodoUpdate(context, todos) {
332
342
  const sessionId = context.session?.id ?? "";
333
343
  if (!context.agentId || !sessionId) return;
344
+ const statusCounts = { pending: 0, inProgress: 0, completed: 0 };
345
+ for (const todo of todos) {
346
+ if (todo.status === "completed") statusCounts.completed++;
347
+ else if (todo.status === "in_progress") statusCounts.inProgress++;
348
+ else statusCounts.pending++;
349
+ }
334
350
  const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;
335
351
  const mailbox = getSharedProjectMailbox(projectDir);
336
352
  void mailbox.send({
@@ -342,11 +358,11 @@ function broadcastTodoUpdate(context, todos) {
342
358
  kind: "kanban.todos.updated",
343
359
  sessionId,
344
360
  revision: context.state.revision,
345
- todos
361
+ todoCount: todos.length,
362
+ statusCounts
346
363
  }),
347
364
  priority: "normal",
348
- senderSessionId: sessionId,
349
- ttlMs: 6 * 60 * 60 * 1e3
365
+ senderSessionId: sessionId
350
366
  }).catch(() => {
351
367
  });
352
368
  }
package/dist/task.js CHANGED
@@ -78,7 +78,9 @@ async function ensureSessionKanbanBoard(projectRoot, sessionId) {
78
78
  description: "Live session work: todos, tasks, and plan items.",
79
79
  tags: sessionBoardTags(sessionId),
80
80
  columns: SESSION_KANBAN_COLUMNS,
81
- generatedBy: `session-kanban:${sessionId}`
81
+ generatedBy: `session-kanban:${sessionId}`,
82
+ kind: "session_mirror",
83
+ retention: { mode: "archive_after_ttl", ttlMs: 7 * 24 * 60 * 60 * 1e3 }
82
84
  });
83
85
  }
84
86
  if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {