@wrongstack/tools 0.297.0 → 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 (253) hide show
  1. package/dist/_spawn-stream.d.ts +2 -0
  2. package/dist/_util.d.ts +29 -1
  3. package/dist/audit.js +113 -65
  4. package/dist/bash.js +5 -4
  5. package/dist/batch-tool-use.js +70 -7
  6. package/dist/browser/index.js +31 -1
  7. package/dist/builtin.js +841 -351
  8. package/dist/codebase-index/index.js +68 -16
  9. package/dist/codebase-index/project-server-protocol.d.ts +26 -0
  10. package/dist/codebase-index/project-server.js +60 -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 +560 -529
  20. package/dist/format.js +116 -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 +792 -290
  25. package/dist/install.js +120 -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 +128 -71
  30. package/dist/lint.js +113 -65
  31. package/dist/outdated.js +86 -26
  32. package/dist/pack.js +841 -351
  33. package/dist/patch.js +11 -1
  34. package/dist/plan.js +32 -10
  35. package/dist/read.js +77 -23
  36. package/dist/replace.js +11 -3
  37. package/dist/scaffold.js +9 -6
  38. package/dist/session-kanban.js +21 -5
  39. package/dist/task.js +3 -1
  40. package/dist/test.js +113 -65
  41. package/dist/todo.js +3 -1
  42. package/dist/tool-tier.js +841 -351
  43. package/dist/tool-use.d.ts +1 -1
  44. package/dist/tool-use.js +28 -4
  45. package/dist/tree.d.ts +1 -0
  46. package/dist/tree.js +44 -8
  47. package/dist/typecheck.js +113 -65
  48. package/dist/write.js +9 -6
  49. package/package.json +5 -4
  50. package/dist/_concurrency.d.ts.map +0 -1
  51. package/dist/_danger-detect.d.ts.map +0 -1
  52. package/dist/_edit-match.d.ts.map +0 -1
  53. package/dist/_env.d.ts.map +0 -1
  54. package/dist/_fetch-guard.d.ts.map +0 -1
  55. package/dist/_output-spool.d.ts.map +0 -1
  56. package/dist/_redact-command.d.ts.map +0 -1
  57. package/dist/_regex.d.ts.map +0 -1
  58. package/dist/_session-shell.d.ts.map +0 -1
  59. package/dist/_shell-pick.d.ts.map +0 -1
  60. package/dist/_spawn-stream.d.ts.map +0 -1
  61. package/dist/_syntax-check.d.ts.map +0 -1
  62. package/dist/_util.d.ts.map +0 -1
  63. package/dist/_win32-resolve.d.ts.map +0 -1
  64. package/dist/audit.d.ts.map +0 -1
  65. package/dist/audit.js.map +0 -7
  66. package/dist/auto-proceed-loop-guard.d.ts.map +0 -1
  67. package/dist/auto-proceed-loop-guard.js.map +0 -7
  68. package/dist/bash-kill-guard.d.ts.map +0 -1
  69. package/dist/bash.d.ts.map +0 -1
  70. package/dist/bash.js.map +0 -7
  71. package/dist/batch-tool-use.d.ts.map +0 -1
  72. package/dist/batch-tool-use.js.map +0 -7
  73. package/dist/browser/artifacts.d.ts.map +0 -1
  74. package/dist/browser/index.d.ts.map +0 -1
  75. package/dist/browser/index.js.map +0 -7
  76. package/dist/browser/manager.d.ts.map +0 -1
  77. package/dist/browser/network-guard-proxy.d.ts.map +0 -1
  78. package/dist/browser/security.d.ts.map +0 -1
  79. package/dist/browser/tools.d.ts.map +0 -1
  80. package/dist/browser/types.d.ts.map +0 -1
  81. package/dist/builtin.d.ts.map +0 -1
  82. package/dist/builtin.js.map +0 -7
  83. package/dist/circuit-breaker.d.ts.map +0 -1
  84. package/dist/circuit-breaker.js.map +0 -7
  85. package/dist/codebase-index/background-indexer.d.ts.map +0 -1
  86. package/dist/codebase-index/bm25.d.ts.map +0 -1
  87. package/dist/codebase-index/circuit-breaker.d.ts.map +0 -1
  88. package/dist/codebase-index/codebase-index-tool.d.ts.map +0 -1
  89. package/dist/codebase-index/codebase-search-tool.d.ts.map +0 -1
  90. package/dist/codebase-index/codebase-stats-tool.d.ts.map +0 -1
  91. package/dist/codebase-index/dead-code-scan.d.ts.map +0 -1
  92. package/dist/codebase-index/generic-parser.d.ts.map +0 -1
  93. package/dist/codebase-index/gitignore.d.ts.map +0 -1
  94. package/dist/codebase-index/go-parser.d.ts.map +0 -1
  95. package/dist/codebase-index/index-service.d.ts.map +0 -1
  96. package/dist/codebase-index/index.d.ts.map +0 -1
  97. package/dist/codebase-index/index.js.map +0 -7
  98. package/dist/codebase-index/indexer.d.ts.map +0 -1
  99. package/dist/codebase-index/json-parser.d.ts.map +0 -1
  100. package/dist/codebase-index/languages.d.ts.map +0 -1
  101. package/dist/codebase-index/lsp-kind.d.ts.map +0 -1
  102. package/dist/codebase-index/parser-dispatch.d.ts.map +0 -1
  103. package/dist/codebase-index/project-server-cache.d.ts.map +0 -1
  104. package/dist/codebase-index/project-server-client.d.ts.map +0 -1
  105. package/dist/codebase-index/project-server-endpoint.d.ts.map +0 -1
  106. package/dist/codebase-index/project-server-protocol.d.ts.map +0 -1
  107. package/dist/codebase-index/project-server.d.ts.map +0 -1
  108. package/dist/codebase-index/project-server.js.map +0 -7
  109. package/dist/codebase-index/py-parser.d.ts.map +0 -1
  110. package/dist/codebase-index/refs-extractor.d.ts.map +0 -1
  111. package/dist/codebase-index/rs-parser.d.ts.map +0 -1
  112. package/dist/codebase-index/schema.d.ts.map +0 -1
  113. package/dist/codebase-index/spawn-gate.d.ts.map +0 -1
  114. package/dist/codebase-index/sqlite-runtime.d.ts.map +0 -1
  115. package/dist/codebase-index/ts-parser.d.ts.map +0 -1
  116. package/dist/codebase-index/worker-protocol.d.ts.map +0 -1
  117. package/dist/codebase-index/worker.d.ts.map +0 -1
  118. package/dist/codebase-index/worker.js.map +0 -7
  119. package/dist/codebase-index/writer-admin.d.ts.map +0 -1
  120. package/dist/codebase-index/writer-bulk-insert.d.ts.map +0 -1
  121. package/dist/codebase-index/writer-graph-helpers.d.ts.map +0 -1
  122. package/dist/codebase-index/writer-graph-reader.d.ts.map +0 -1
  123. package/dist/codebase-index/writer-helpers.d.ts.map +0 -1
  124. package/dist/codebase-index/writer-pragmas.d.ts.map +0 -1
  125. package/dist/codebase-index/writer-ref-mapper.d.ts.map +0 -1
  126. package/dist/codebase-index/writer-schema.d.ts.map +0 -1
  127. package/dist/codebase-index/writer-search-helpers.d.ts.map +0 -1
  128. package/dist/codebase-index/writer-store-pool.d.ts.map +0 -1
  129. package/dist/codebase-index/writer.d.ts.map +0 -1
  130. package/dist/codebase-index/yaml-parser.d.ts.map +0 -1
  131. package/dist/design.d.ts.map +0 -1
  132. package/dist/design.js.map +0 -7
  133. package/dist/diff.d.ts.map +0 -1
  134. package/dist/diff.js.map +0 -7
  135. package/dist/document.d.ts.map +0 -1
  136. package/dist/document.js.map +0 -7
  137. package/dist/e2e.d.ts.map +0 -1
  138. package/dist/e2e.js.map +0 -7
  139. package/dist/edit.d.ts.map +0 -1
  140. package/dist/edit.js.map +0 -7
  141. package/dist/exec-kill-guard.d.ts.map +0 -1
  142. package/dist/exec.d.ts.map +0 -1
  143. package/dist/exec.js.map +0 -7
  144. package/dist/fetch.d.ts.map +0 -1
  145. package/dist/fetch.js.map +0 -7
  146. package/dist/format.d.ts.map +0 -1
  147. package/dist/format.js.map +0 -7
  148. package/dist/git.d.ts.map +0 -1
  149. package/dist/git.js.map +0 -7
  150. package/dist/glob.d.ts.map +0 -1
  151. package/dist/glob.js.map +0 -7
  152. package/dist/grep.d.ts.map +0 -1
  153. package/dist/grep.js.map +0 -7
  154. package/dist/index.d.ts.map +0 -1
  155. package/dist/index.js.map +0 -7
  156. package/dist/install.d.ts.map +0 -1
  157. package/dist/install.js.map +0 -7
  158. package/dist/json.d.ts.map +0 -1
  159. package/dist/json.js.map +0 -7
  160. package/dist/kanban-board-inputs.d.ts.map +0 -1
  161. package/dist/kanban-decomposition-actions.d.ts.map +0 -1
  162. package/dist/kanban-detail-actions.d.ts.map +0 -1
  163. package/dist/kanban-evidence-bridge.d.ts.map +0 -1
  164. package/dist/kanban-presence.d.ts.map +0 -1
  165. package/dist/kanban-split-task-handler.d.ts.map +0 -1
  166. package/dist/kanban-task-inputs.d.ts.map +0 -1
  167. package/dist/kanban-tool-results.d.ts.map +0 -1
  168. package/dist/kanban-tool-schema.d.ts.map +0 -1
  169. package/dist/kanban-tool-types.d.ts.map +0 -1
  170. package/dist/kanban.d.ts.map +0 -1
  171. package/dist/kanban.js.map +0 -7
  172. package/dist/languages/detect.d.ts.map +0 -1
  173. package/dist/languages/diagnostics.d.ts.map +0 -1
  174. package/dist/languages/execute-tool.d.ts.map +0 -1
  175. package/dist/languages/execute.d.ts.map +0 -1
  176. package/dist/languages/index.d.ts.map +0 -1
  177. package/dist/languages/index.js.map +0 -7
  178. package/dist/languages/legacy-bridge.d.ts.map +0 -1
  179. package/dist/languages/package-tool.d.ts.map +0 -1
  180. package/dist/languages/plan.d.ts.map +0 -1
  181. package/dist/languages/profile-helpers.d.ts.map +0 -1
  182. package/dist/languages/profiles/additional.d.ts.map +0 -1
  183. package/dist/languages/profiles/primary.d.ts.map +0 -1
  184. package/dist/languages/registry.d.ts.map +0 -1
  185. package/dist/languages/tool.d.ts.map +0 -1
  186. package/dist/languages/types.d.ts.map +0 -1
  187. package/dist/lint.d.ts.map +0 -1
  188. package/dist/lint.js.map +0 -7
  189. package/dist/logs.d.ts.map +0 -1
  190. package/dist/logs.js.map +0 -7
  191. package/dist/memory.d.ts.map +0 -1
  192. package/dist/memory.js.map +0 -7
  193. package/dist/mode.d.ts.map +0 -1
  194. package/dist/mode.js.map +0 -7
  195. package/dist/next-steps.d.ts.map +0 -1
  196. package/dist/next-steps.js.map +0 -7
  197. package/dist/outdated.d.ts.map +0 -1
  198. package/dist/outdated.js.map +0 -7
  199. package/dist/pack.d.ts.map +0 -1
  200. package/dist/pack.js.map +0 -7
  201. package/dist/patch.d.ts.map +0 -1
  202. package/dist/patch.js.map +0 -7
  203. package/dist/plan.d.ts.map +0 -1
  204. package/dist/plan.js.map +0 -7
  205. package/dist/process-guardian.d.ts.map +0 -1
  206. package/dist/process-registry-persistent.d.ts.map +0 -1
  207. package/dist/process-registry.d.ts.map +0 -1
  208. package/dist/process-registry.js.map +0 -7
  209. package/dist/ps-slash.d.ts.map +0 -1
  210. package/dist/ps-slash.js.map +0 -7
  211. package/dist/read.d.ts.map +0 -1
  212. package/dist/read.js.map +0 -7
  213. package/dist/replace.d.ts.map +0 -1
  214. package/dist/replace.js.map +0 -7
  215. package/dist/scaffold.d.ts.map +0 -1
  216. package/dist/scaffold.js.map +0 -7
  217. package/dist/search.d.ts.map +0 -1
  218. package/dist/search.js.map +0 -7
  219. package/dist/session-kanban.d.ts.map +0 -1
  220. package/dist/session-kanban.js.map +0 -7
  221. package/dist/set-working-dir.d.ts.map +0 -1
  222. package/dist/skill.d.ts.map +0 -1
  223. package/dist/skill.js.map +0 -7
  224. package/dist/spawn-background.d.ts.map +0 -1
  225. package/dist/task.d.ts.map +0 -1
  226. package/dist/task.js.map +0 -7
  227. package/dist/test.d.ts.map +0 -1
  228. package/dist/test.js.map +0 -7
  229. package/dist/todo.d.ts.map +0 -1
  230. package/dist/todo.js.map +0 -7
  231. package/dist/tool-diff.d.ts.map +0 -1
  232. package/dist/tool-diff.js.map +0 -7
  233. package/dist/tool-help.d.ts.map +0 -1
  234. package/dist/tool-help.js.map +0 -7
  235. package/dist/tool-icon-map.d.ts.map +0 -1
  236. package/dist/tool-icons.d.ts.map +0 -1
  237. package/dist/tool-icons.js.map +0 -7
  238. package/dist/tool-search.d.ts.map +0 -1
  239. package/dist/tool-search.js.map +0 -7
  240. package/dist/tool-summary.d.ts.map +0 -1
  241. package/dist/tool-summary.js.map +0 -7
  242. package/dist/tool-tier.d.ts.map +0 -1
  243. package/dist/tool-tier.js.map +0 -7
  244. package/dist/tool-use.d.ts.map +0 -1
  245. package/dist/tool-use.js.map +0 -7
  246. package/dist/tree.d.ts.map +0 -1
  247. package/dist/tree.js.map +0 -7
  248. package/dist/typecheck.d.ts.map +0 -1
  249. package/dist/typecheck.js.map +0 -7
  250. package/dist/win32.d.ts.map +0 -1
  251. package/dist/win32.js.map +0 -7
  252. package/dist/write.d.ts.map +0 -1
  253. 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.`);
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);
@@ -4862,6 +4890,31 @@ var ProjectServerConnection = class {
4862
4890
  this.transition("offline");
4863
4891
  maybeStopHeartbeatLoop();
4864
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
+ }
4865
4918
  request(message, options) {
4866
4919
  const socket = this.socket;
4867
4920
  if (!socket || socket.destroyed) {
@@ -4905,7 +4958,7 @@ var ProjectServerConnection = class {
4905
4958
  return;
4906
4959
  }
4907
4960
  }
4908
- this.write({ ...message, id });
4961
+ this.write({ ...message, id, authToken: this.currentAuthToken() });
4909
4962
  });
4910
4963
  }
4911
4964
  async ensureConnected(spawnIfMissing) {
@@ -4952,6 +5005,7 @@ var ProjectServerConnection = class {
4952
5005
  connectOnce() {
4953
5006
  this.socket?.destroy();
4954
5007
  this.socket = null;
5008
+ this.authToken = void 0;
4955
5009
  this.info = null;
4956
5010
  this.activity = null;
4957
5011
  this.health = null;
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)) {