@wrongstack/tools 0.293.0 → 0.295.1

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/_env.d.ts +1 -1
  2. package/dist/_env.d.ts.map +1 -1
  3. package/dist/_fetch-guard.d.ts.map +1 -1
  4. package/dist/_spawn-stream.d.ts +1 -1
  5. package/dist/_spawn-stream.d.ts.map +1 -1
  6. package/dist/_util.d.ts +1 -1
  7. package/dist/_util.d.ts.map +1 -1
  8. package/dist/audit.d.ts +1 -1
  9. package/dist/audit.d.ts.map +1 -1
  10. package/dist/audit.js +81 -16
  11. package/dist/audit.js.map +2 -2
  12. package/dist/auto-proceed-loop-guard.d.ts +31 -0
  13. package/dist/auto-proceed-loop-guard.d.ts.map +1 -1
  14. package/dist/auto-proceed-loop-guard.js +24 -1
  15. package/dist/auto-proceed-loop-guard.js.map +2 -2
  16. package/dist/bash.d.ts +1 -1
  17. package/dist/bash.d.ts.map +1 -1
  18. package/dist/bash.js +105 -75
  19. package/dist/bash.js.map +3 -3
  20. package/dist/batch-tool-use.d.ts +1 -1
  21. package/dist/batch-tool-use.d.ts.map +1 -1
  22. package/dist/batch-tool-use.js +1 -1
  23. package/dist/batch-tool-use.js.map +1 -1
  24. package/dist/browser/index.js +10 -1
  25. package/dist/browser/index.js.map +2 -2
  26. package/dist/browser/manager.d.ts +2 -0
  27. package/dist/browser/manager.d.ts.map +1 -1
  28. package/dist/browser/tools.d.ts +1 -1
  29. package/dist/browser/tools.d.ts.map +1 -1
  30. package/dist/builtin.d.ts +1 -1
  31. package/dist/builtin.d.ts.map +1 -1
  32. package/dist/builtin.js +1650 -640
  33. package/dist/builtin.js.map +4 -4
  34. package/dist/codebase-index/background-indexer.d.ts +2 -2
  35. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  36. package/dist/codebase-index/bm25.d.ts +3 -1
  37. package/dist/codebase-index/bm25.d.ts.map +1 -1
  38. package/dist/codebase-index/codebase-index-tool.d.ts +1 -1
  39. package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
  40. package/dist/codebase-index/codebase-search-tool.d.ts +1 -1
  41. package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
  42. package/dist/codebase-index/codebase-stats-tool.d.ts +1 -1
  43. package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
  44. package/dist/codebase-index/generic-parser.d.ts +34 -0
  45. package/dist/codebase-index/generic-parser.d.ts.map +1 -0
  46. package/dist/codebase-index/go-parser.d.ts +1 -1
  47. package/dist/codebase-index/go-parser.d.ts.map +1 -1
  48. package/dist/codebase-index/index.d.ts +4 -2
  49. package/dist/codebase-index/index.d.ts.map +1 -1
  50. package/dist/codebase-index/index.js +1068 -372
  51. package/dist/codebase-index/index.js.map +4 -4
  52. package/dist/codebase-index/indexer.d.ts +6 -1
  53. package/dist/codebase-index/indexer.d.ts.map +1 -1
  54. package/dist/codebase-index/json-parser.d.ts +1 -1
  55. package/dist/codebase-index/languages.d.ts +25 -0
  56. package/dist/codebase-index/languages.d.ts.map +1 -0
  57. package/dist/codebase-index/py-parser.d.ts +9 -1
  58. package/dist/codebase-index/py-parser.d.ts.map +1 -1
  59. package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
  60. package/dist/codebase-index/rs-parser.d.ts +1 -1
  61. package/dist/codebase-index/rs-parser.d.ts.map +1 -1
  62. package/dist/codebase-index/schema.d.ts +9 -2
  63. package/dist/codebase-index/schema.d.ts.map +1 -1
  64. package/dist/codebase-index/spawn-gate.d.ts +15 -0
  65. package/dist/codebase-index/spawn-gate.d.ts.map +1 -0
  66. package/dist/codebase-index/ts-parser.d.ts +2 -2
  67. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  68. package/dist/codebase-index/worker.js +993 -348
  69. package/dist/codebase-index/worker.js.map +4 -4
  70. package/dist/codebase-index/writer.d.ts +83 -4
  71. package/dist/codebase-index/writer.d.ts.map +1 -1
  72. package/dist/codebase-index/yaml-parser.d.ts +1 -1
  73. package/dist/design.d.ts +5 -2
  74. package/dist/design.d.ts.map +1 -1
  75. package/dist/design.js +36 -3
  76. package/dist/design.js.map +2 -2
  77. package/dist/diff.d.ts +1 -1
  78. package/dist/diff.d.ts.map +1 -1
  79. package/dist/diff.js +2 -2
  80. package/dist/diff.js.map +1 -1
  81. package/dist/document.d.ts +1 -1
  82. package/dist/document.d.ts.map +1 -1
  83. package/dist/document.js +1 -1
  84. package/dist/document.js.map +1 -1
  85. package/dist/e2e.d.ts +1 -1
  86. package/dist/e2e.d.ts.map +1 -1
  87. package/dist/e2e.js +1 -1
  88. package/dist/e2e.js.map +1 -1
  89. package/dist/edit.d.ts +1 -1
  90. package/dist/edit.d.ts.map +1 -1
  91. package/dist/edit.js +3 -3
  92. package/dist/edit.js.map +2 -2
  93. package/dist/exec.d.ts +1 -1
  94. package/dist/exec.d.ts.map +1 -1
  95. package/dist/exec.js +99 -44
  96. package/dist/exec.js.map +3 -3
  97. package/dist/fetch.d.ts +1 -1
  98. package/dist/fetch.d.ts.map +1 -1
  99. package/dist/fetch.js +4 -5
  100. package/dist/fetch.js.map +2 -2
  101. package/dist/format.d.ts +1 -1
  102. package/dist/format.d.ts.map +1 -1
  103. package/dist/format.js +81 -16
  104. package/dist/format.js.map +2 -2
  105. package/dist/git.d.ts +1 -1
  106. package/dist/git.d.ts.map +1 -1
  107. package/dist/git.js +3 -2
  108. package/dist/git.js.map +2 -2
  109. package/dist/glob.d.ts +1 -1
  110. package/dist/glob.d.ts.map +1 -1
  111. package/dist/glob.js +81 -31
  112. package/dist/glob.js.map +4 -4
  113. package/dist/grep.d.ts +1 -1
  114. package/dist/grep.d.ts.map +1 -1
  115. package/dist/grep.js +90 -22
  116. package/dist/grep.js.map +4 -4
  117. package/dist/index.d.ts +5 -4
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +1895 -771
  120. package/dist/index.js.map +4 -4
  121. package/dist/install.d.ts +1 -1
  122. package/dist/install.d.ts.map +1 -1
  123. package/dist/install.js +85 -17
  124. package/dist/install.js.map +2 -2
  125. package/dist/json.d.ts +1 -1
  126. package/dist/json.d.ts.map +1 -1
  127. package/dist/json.js +2 -2
  128. package/dist/json.js.map +2 -2
  129. package/dist/kanban-evidence-bridge.d.ts +21 -0
  130. package/dist/kanban-evidence-bridge.d.ts.map +1 -0
  131. package/dist/kanban.d.ts +20 -3
  132. package/dist/kanban.d.ts.map +1 -1
  133. package/dist/kanban.js +256 -32
  134. package/dist/kanban.js.map +3 -3
  135. package/dist/languages/execute-tool.d.ts +1 -1
  136. package/dist/languages/execute-tool.d.ts.map +1 -1
  137. package/dist/languages/execute.d.ts +1 -1
  138. package/dist/languages/execute.d.ts.map +1 -1
  139. package/dist/languages/index.d.ts +1 -1
  140. package/dist/languages/index.d.ts.map +1 -1
  141. package/dist/languages/index.js +99 -17
  142. package/dist/languages/index.js.map +2 -2
  143. package/dist/languages/package-tool.d.ts +3 -4
  144. package/dist/languages/package-tool.d.ts.map +1 -1
  145. package/dist/languages/plan.d.ts.map +1 -1
  146. package/dist/languages/profiles/additional.d.ts.map +1 -1
  147. package/dist/languages/profiles/primary.d.ts.map +1 -1
  148. package/dist/languages/tool.d.ts +1 -1
  149. package/dist/languages/tool.d.ts.map +1 -1
  150. package/dist/lint.d.ts +1 -1
  151. package/dist/lint.d.ts.map +1 -1
  152. package/dist/lint.js +81 -16
  153. package/dist/lint.js.map +2 -2
  154. package/dist/logs.d.ts +1 -1
  155. package/dist/logs.d.ts.map +1 -1
  156. package/dist/logs.js +2 -2
  157. package/dist/logs.js.map +1 -1
  158. package/dist/memory.d.ts +1 -1
  159. package/dist/memory.d.ts.map +1 -1
  160. package/dist/memory.js +1 -1
  161. package/dist/memory.js.map +1 -1
  162. package/dist/mode.d.ts +1 -1
  163. package/dist/mode.d.ts.map +1 -1
  164. package/dist/mode.js.map +1 -1
  165. package/dist/outdated.d.ts +1 -1
  166. package/dist/outdated.d.ts.map +1 -1
  167. package/dist/outdated.js +101 -19
  168. package/dist/outdated.js.map +2 -2
  169. package/dist/pack.d.ts +1 -1
  170. package/dist/pack.js +1650 -640
  171. package/dist/pack.js.map +4 -4
  172. package/dist/patch.d.ts +1 -1
  173. package/dist/patch.d.ts.map +1 -1
  174. package/dist/patch.js +2 -2
  175. package/dist/patch.js.map +1 -1
  176. package/dist/plan.d.ts +1 -1
  177. package/dist/plan.d.ts.map +1 -1
  178. package/dist/plan.js +7 -7
  179. package/dist/plan.js.map +2 -2
  180. package/dist/process-guardian.d.ts +7 -0
  181. package/dist/process-guardian.d.ts.map +1 -1
  182. package/dist/process-registry-persistent.d.ts +5 -0
  183. package/dist/process-registry-persistent.d.ts.map +1 -1
  184. package/dist/process-registry.d.ts +9 -1
  185. package/dist/process-registry.d.ts.map +1 -1
  186. package/dist/process-registry.js +18 -3
  187. package/dist/process-registry.js.map +2 -2
  188. package/dist/ps-slash.js +93 -38
  189. package/dist/ps-slash.js.map +2 -2
  190. package/dist/read.d.ts +1 -1
  191. package/dist/read.d.ts.map +1 -1
  192. package/dist/read.js +3 -2
  193. package/dist/read.js.map +2 -2
  194. package/dist/replace.d.ts +1 -1
  195. package/dist/replace.d.ts.map +1 -1
  196. package/dist/replace.js +4 -4
  197. package/dist/replace.js.map +2 -2
  198. package/dist/scaffold.d.ts +1 -1
  199. package/dist/scaffold.d.ts.map +1 -1
  200. package/dist/scaffold.js +2 -2
  201. package/dist/scaffold.js.map +1 -1
  202. package/dist/search.d.ts +1 -1
  203. package/dist/search.d.ts.map +1 -1
  204. package/dist/search.js +4 -4
  205. package/dist/search.js.map +2 -2
  206. package/dist/session-kanban.d.ts +4 -1
  207. package/dist/session-kanban.d.ts.map +1 -1
  208. package/dist/session-kanban.js +23 -15
  209. package/dist/session-kanban.js.map +2 -2
  210. package/dist/set-working-dir.d.ts +1 -1
  211. package/dist/set-working-dir.d.ts.map +1 -1
  212. package/dist/skill.d.ts +1 -1
  213. package/dist/skill.d.ts.map +1 -1
  214. package/dist/skill.js +2 -5
  215. package/dist/skill.js.map +2 -2
  216. package/dist/spawn-background.d.ts.map +1 -1
  217. package/dist/task.d.ts +3 -2
  218. package/dist/task.d.ts.map +1 -1
  219. package/dist/task.js +6 -8
  220. package/dist/task.js.map +2 -2
  221. package/dist/test.d.ts +1 -1
  222. package/dist/test.d.ts.map +1 -1
  223. package/dist/test.js +81 -16
  224. package/dist/test.js.map +2 -2
  225. package/dist/todo.d.ts +2 -1
  226. package/dist/todo.d.ts.map +1 -1
  227. package/dist/todo.js +4 -4
  228. package/dist/todo.js.map +2 -2
  229. package/dist/tool-help.d.ts +1 -1
  230. package/dist/tool-help.d.ts.map +1 -1
  231. package/dist/tool-help.js.map +1 -1
  232. package/dist/tool-search.d.ts +1 -1
  233. package/dist/tool-search.d.ts.map +1 -1
  234. package/dist/tool-search.js.map +1 -1
  235. package/dist/tool-tier.d.ts +16 -0
  236. package/dist/tool-tier.d.ts.map +1 -0
  237. package/dist/tool-tier.js +22402 -0
  238. package/dist/tool-tier.js.map +7 -0
  239. package/dist/tool-use.d.ts +1 -1
  240. package/dist/tool-use.d.ts.map +1 -1
  241. package/dist/tool-use.js.map +1 -1
  242. package/dist/tree.d.ts +1 -1
  243. package/dist/tree.d.ts.map +1 -1
  244. package/dist/tree.js +3 -15
  245. package/dist/tree.js.map +2 -2
  246. package/dist/typecheck.d.ts +1 -1
  247. package/dist/typecheck.d.ts.map +1 -1
  248. package/dist/typecheck.js +81 -16
  249. package/dist/typecheck.js.map +2 -2
  250. package/dist/win32.d.ts +10 -0
  251. package/dist/win32.d.ts.map +1 -0
  252. package/dist/win32.js +67 -0
  253. package/dist/win32.js.map +7 -0
  254. package/dist/write.d.ts +1 -1
  255. package/dist/write.d.ts.map +1 -1
  256. package/dist/write.js +3 -2
  257. package/dist/write.js.map +2 -2
  258. package/package.json +11 -3
package/dist/index.js CHANGED
@@ -84,7 +84,7 @@ var init_win32_resolve = __esm({
84
84
  import { createWriteStream, mkdirSync } from "node:fs";
85
85
  import * as fsp from "node:fs/promises";
86
86
  import * as path2 from "node:path";
87
- import { wstackGlobalRoot } from "@wrongstack/core";
87
+ import { wstackGlobalRoot } from "@wrongstack/core/utils";
88
88
  function toolOutputDir() {
89
89
  return path2.join(wstackGlobalRoot(), "tool-output");
90
90
  }
@@ -392,7 +392,7 @@ var init_circuit_breaker = __esm({
392
392
  });
393
393
 
394
394
  // src/_redact-command.ts
395
- import { expectDefined } from "@wrongstack/core";
395
+ import { expectDefined } from "@wrongstack/core/utils";
396
396
  function redactCommand(cmd) {
397
397
  let result = cmd;
398
398
  for (const pattern of SENSITIVE_FLAG_PATTERNS) {
@@ -509,7 +509,12 @@ var init_process_registry = __esm({
509
509
  this.breaker.setEnabled(false);
510
510
  }
511
511
  register(info) {
512
- this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
512
+ this.processes.set(info.pid, {
513
+ ...info,
514
+ killed: false,
515
+ protected: info.protected ?? false,
516
+ background: info.background ?? false
517
+ });
513
518
  }
514
519
  _isSafeSignalPid(pid) {
515
520
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
@@ -562,12 +567,21 @@ var init_process_registry = __esm({
562
567
  }
563
568
  return n;
564
569
  }
570
+ /** Count of active jobs explicitly launched in background mode. */
571
+ get activeBackgroundCount() {
572
+ let n = 0;
573
+ for (const p of this.processes.values()) {
574
+ if (p.background && !p.killed) n++;
575
+ }
576
+ return n;
577
+ }
565
578
  /**
566
579
  * Combined stats for observability — used by /ps and the TUI status bar.
567
580
  */
568
581
  stats() {
569
582
  return {
570
583
  activeCount: this.activeCount,
584
+ backgroundCount: this.activeBackgroundCount,
571
585
  totalCount: this.processes.size,
572
586
  breaker: this.breaker.snapshot()
573
587
  };
@@ -668,7 +682,7 @@ var init_process_registry = __esm({
668
682
  this.autoKillTimer = setTimeout(() => {
669
683
  this.autoKillTimer = null;
670
684
  this.autoKillArmedAt = null;
671
- this.killAll({ force: false });
685
+ this.killAll({ force: false, preserveBackground: true });
672
686
  this.breaker.forceReset();
673
687
  this._emitBreakerCountdown();
674
688
  }, this.autoKillResetMs);
@@ -708,6 +722,7 @@ var init_process_registry = __esm({
708
722
  if (!p) return false;
709
723
  if (p.killed) return true;
710
724
  if (p.protected) return false;
725
+ if (opts.preserveBackground && p.background) return false;
711
726
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
712
727
  const isWin5 = os.platform() === "win32";
713
728
  if (isWin5) {
@@ -810,11 +825,11 @@ var init_process_registry = __esm({
810
825
  // src/_spawn-stream.ts
811
826
  import { spawn as spawn2 } from "node:child_process";
812
827
  import {
813
- buildChildEnv,
814
828
  emitProcessCompleted,
815
829
  emitProcessOutput,
816
830
  emitProcessStarted
817
- } from "@wrongstack/core";
831
+ } from "@wrongstack/core/observability";
832
+ import { buildChildEnv } from "@wrongstack/core/utils";
818
833
  async function* spawnStream(opts) {
819
834
  const max = opts.maxBytes ?? 2e5;
820
835
  const flushAt = opts.flushBytes ?? 4 * 1024;
@@ -1026,7 +1041,7 @@ var init_spawn_stream = __esm({
1026
1041
  import { createHash } from "node:crypto";
1027
1042
  import * as fsp2 from "node:fs/promises";
1028
1043
  import * as path3 from "node:path";
1029
- import * as Core from "@wrongstack/core";
1044
+ import * as Core from "@wrongstack/core/utils";
1030
1045
  function sha256hex(content) {
1031
1046
  return createHash("sha256").update(content, "utf8").digest("hex");
1032
1047
  }
@@ -1070,16 +1085,16 @@ async function assertRealInsideRoot(absPath, ctx) {
1070
1085
  const realRoots = await Promise.all(
1071
1086
  allowedRoots(ctx).map((r) => fsp2.realpath(r).catch(() => path3.resolve(r)))
1072
1087
  );
1073
- let probe = absPath;
1088
+ let probe2 = absPath;
1074
1089
  for (; ; ) {
1075
1090
  let real;
1076
1091
  try {
1077
- real = await fsp2.realpath(probe);
1092
+ real = await fsp2.realpath(probe2);
1078
1093
  } catch (err) {
1079
1094
  if (err.code === "ENOENT") {
1080
- const parent = path3.dirname(probe);
1081
- if (parent === probe) return;
1082
- probe = parent;
1095
+ const parent = path3.dirname(probe2);
1096
+ if (parent === probe2) return;
1097
+ probe2 = parent;
1083
1098
  continue;
1084
1099
  }
1085
1100
  throw err;
@@ -1267,7 +1282,20 @@ function pythonProfile() {
1267
1282
  ]),
1268
1283
  ignoredDirectories: IGNORES,
1269
1284
  packageManagers: Object.freeze(["pip", "poetry", "pipenv", "uv"]),
1270
- executables: Object.freeze(["python", "python3", "ruff", "black", "pytest", "mypy", "pyright"]),
1285
+ executables: Object.freeze([
1286
+ "python",
1287
+ "python3",
1288
+ "pip",
1289
+ "pip3",
1290
+ "poetry",
1291
+ "pipenv",
1292
+ "uv",
1293
+ "ruff",
1294
+ "black",
1295
+ "pytest",
1296
+ "mypy",
1297
+ "pyright"
1298
+ ]),
1271
1299
  operations: Object.freeze({
1272
1300
  syntax: async (ctx) => {
1273
1301
  const py = ctx.options.target ? "python3" : "python3";
@@ -1327,6 +1355,14 @@ function pythonProfile() {
1327
1355
  network: true
1328
1356
  });
1329
1357
  },
1358
+ "package-remove": async (ctx) => {
1359
+ const names = packageNames(ctx);
1360
+ return names.length === 0 ? unavailable(ctx, "package-remove", "At least one package name is required.") : processPlan(ctx, "package-remove", "pip", ["uninstall", "--yes", ...names], {
1361
+ parser: "package-text",
1362
+ reason: "Remove validated Python packages.",
1363
+ mutating: true
1364
+ });
1365
+ },
1330
1366
  "package-audit": async (ctx) => processPlan(ctx, "package-audit", "pip", ["audit"], {
1331
1367
  parser: "pip-audit",
1332
1368
  reason: "Audit Python dependencies for vulnerabilities.",
@@ -1938,6 +1974,19 @@ function typescriptProfile() {
1938
1974
  network: true
1939
1975
  });
1940
1976
  },
1977
+ "package-remove": async (ctx) => {
1978
+ const names = packageNames(ctx);
1979
+ if (names.length === 0)
1980
+ return unavailable(ctx, "package-remove", "At least one package name is required.");
1981
+ const manager = ctx.workspace.packageManager ?? "npm";
1982
+ const args = manager === "npm" ? ["uninstall", "--ignore-scripts", ...names] : ["remove", "--ignore-scripts", ...names];
1983
+ return processPlan(ctx, "package-remove", manager, args, {
1984
+ parser: "package-text",
1985
+ reason: `Remove validated packages with ${manager} and lifecycle scripts disabled.`,
1986
+ mutating: true,
1987
+ network: true
1988
+ });
1989
+ },
1941
1990
  "package-audit": async (ctx) => {
1942
1991
  const manager = ctx.workspace.packageManager ?? "npm";
1943
1992
  return processPlan(ctx, "package-audit", manager, ["audit", "--json"], {
@@ -2390,13 +2439,28 @@ var init_primary = __esm({
2390
2439
  }),
2391
2440
  "package-add": async (ctx) => {
2392
2441
  const names = packageNames(ctx);
2393
- return names.length === 0 ? unavailable(ctx, "package-add", "At least one NuGet package is required.") : processPlan(ctx, "package-add", "dotnet", ["add", "package", ...names], {
2394
- parser: "dotnet-package",
2395
- reason: "Add validated NuGet packages.",
2396
- mutating: true,
2397
- network: true,
2398
- executesProjectCode: true
2399
- });
2442
+ const [spec] = names;
2443
+ const versionAt = spec?.lastIndexOf("@") ?? -1;
2444
+ const packageName = versionAt > 0 ? spec?.slice(0, versionAt) : spec;
2445
+ const packageVersion = versionAt > 0 ? spec?.slice(versionAt + 1) : void 0;
2446
+ return names.length === 0 ? unavailable(ctx, "package-add", "At least one NuGet package is required.") : names.length > 1 ? unavailable(ctx, "package-add", "NuGet package changes run one package at a time.") : processPlan(
2447
+ ctx,
2448
+ "package-add",
2449
+ "dotnet",
2450
+ [
2451
+ "add",
2452
+ "package",
2453
+ packageName ?? "",
2454
+ ...packageVersion ? ["--version", packageVersion] : []
2455
+ ],
2456
+ {
2457
+ parser: "dotnet-package",
2458
+ reason: "Add validated NuGet packages.",
2459
+ mutating: true,
2460
+ network: true,
2461
+ executesProjectCode: true
2462
+ }
2463
+ );
2400
2464
  },
2401
2465
  "package-remove": async (ctx) => {
2402
2466
  const names = packageNames(ctx);
@@ -2608,8 +2672,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, extraIgn
2608
2672
  collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2609
2673
  }
2610
2674
  }
2611
- function collectFileEvidence(directory, fullPath, basename11, profiles, state) {
2612
- const lower = basename11.toLowerCase();
2675
+ function collectFileEvidence(directory, fullPath, basename12, profiles, state) {
2676
+ const lower = basename12.toLowerCase();
2613
2677
  const extension = path4.extname(lower);
2614
2678
  for (const profile of profiles) {
2615
2679
  const detector = profile.detectors.find(
@@ -2620,7 +2684,7 @@ function collectFileEvidence(directory, fullPath, basename11, profiles, state) {
2620
2684
  candidate.evidence.push({
2621
2685
  kind: detector.kind,
2622
2686
  path: fullPath,
2623
- value: basename11,
2687
+ value: basename12,
2624
2688
  weight: detector.weight
2625
2689
  });
2626
2690
  if (detector.kind === "manifest" || detector.kind === "config") {
@@ -3486,7 +3550,7 @@ function validateOperationOptions(operation, packages) {
3486
3550
  if (value.startsWith(".") || value.startsWith("/") || /^[A-Za-z]:/.test(value) || value.includes("://")) {
3487
3551
  throw new Error(`Package paths and URLs are not supported: "${value}"`);
3488
3552
  }
3489
- if (!PACKAGE_NAME_RE.test(value) && !COMPOSER_PACKAGE_RE.test(value) && !GO_MODULE_RE.test(value)) {
3553
+ if (!PACKAGE_NAME_RE.test(value) && !COMPOSER_PACKAGE_RE.test(value) && !GO_MODULE_RE.test(value) && !PYTHON_PACKAGE_RE.test(value)) {
3490
3554
  throw new Error(`Invalid package identifier "${value}"`);
3491
3555
  }
3492
3556
  }
@@ -3508,7 +3572,7 @@ function isInside2(candidate, root) {
3508
3572
  const relative13 = path6.relative(path6.resolve(root), path6.resolve(candidate));
3509
3573
  return relative13 === "" || !relative13.startsWith("..") && !path6.isAbsolute(relative13);
3510
3574
  }
3511
- var MAX_ARGUMENTS, MAX_ARGUMENT_LENGTH, PACKAGE_NAME_RE, COMPOSER_PACKAGE_RE, GO_MODULE_RE;
3575
+ var MAX_ARGUMENTS, MAX_ARGUMENT_LENGTH, PACKAGE_NAME_RE, COMPOSER_PACKAGE_RE, GO_MODULE_RE, PYTHON_PACKAGE_RE;
3512
3576
  var init_plan = __esm({
3513
3577
  "src/languages/plan.ts"() {
3514
3578
  "use strict";
@@ -3519,6 +3583,7 @@ var init_plan = __esm({
3519
3583
  PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9*+._~^<>=|-]+)?$/i;
3520
3584
  COMPOSER_PACKAGE_RE = /^[a-z0-9_.-]+\/[a-z0-9_.-]+(?::[a-z0-9*+._~^<>=|-]+)?$/i;
3521
3585
  GO_MODULE_RE = /^(?:[a-z0-9.-]+\.)+[a-z0-9.-]+\/[a-z0-9._~+/@-]+$/i;
3586
+ PYTHON_PACKAGE_RE = /^[a-z0-9._-]+(?:\[[a-z0-9._,-]+\])?==[a-z0-9*+._!~-]+$/i;
3522
3587
  }
3523
3588
  });
3524
3589
 
@@ -3937,15 +4002,15 @@ async function changedPaths(before, after, beforeSizes, afterSizes) {
3937
4002
  const beforeSet = new Set(before);
3938
4003
  const afterSet = new Set(after);
3939
4004
  const changed = /* @__PURE__ */ new Set();
3940
- for (const path34 of after) {
3941
- if (!beforeSet.has(path34)) changed.add(path34);
4005
+ for (const path35 of after) {
4006
+ if (!beforeSet.has(path35)) changed.add(path35);
3942
4007
  }
3943
- for (const path34 of before) {
3944
- if (!afterSet.has(path34)) changed.add(path34);
4008
+ for (const path35 of before) {
4009
+ if (!afterSet.has(path35)) changed.add(path35);
3945
4010
  }
3946
4011
  if (beforeSizes && afterSizes) {
3947
- for (const path34 of after) {
3948
- if (beforeSizes.get(path34) !== afterSizes.get(path34)) changed.add(path34);
4012
+ for (const path35 of after) {
4013
+ if (beforeSizes.get(path35) !== afterSizes.get(path35)) changed.add(path35);
3949
4014
  }
3950
4015
  }
3951
4016
  return [...changed].sort();
@@ -4420,7 +4485,6 @@ var init_package_tool = __esm({
4420
4485
  const operationOptions2 = {
4421
4486
  ...input.names ? { packages: input.names } : {},
4422
4487
  ...input.scope ? { packageScope: input.scope } : {},
4423
- ...input.dryRun !== void 0 ? { allowScripts: input.dryRun } : { allowScripts: false },
4424
4488
  ...input.allowScripts !== void 0 ? { allowScripts: input.allowScripts } : {}
4425
4489
  };
4426
4490
  const planResult = await planLanguageOperation({
@@ -4449,6 +4513,24 @@ var init_package_tool = __esm({
4449
4513
  };
4450
4514
  return;
4451
4515
  }
4516
+ if (input.dryRun) {
4517
+ yield {
4518
+ type: "final",
4519
+ output: {
4520
+ status: "passed",
4521
+ language: planResult.workspace.language,
4522
+ workspace: planResult.workspace.root,
4523
+ operation: planResult.plan.operation,
4524
+ mutations: [],
4525
+ vulnerabilities: [],
4526
+ outdated: [],
4527
+ manifestsChanged: [],
4528
+ lockfilesChanged: [],
4529
+ output: `Dry run: ${planResult.plan.command ?? "internal"} ${planResult.plan.args.join(" ")}`.trim()
4530
+ }
4531
+ };
4532
+ return;
4533
+ }
4452
4534
  const runner = executePackagePlan({
4453
4535
  projectRoot: ctx.projectRoot,
4454
4536
  workspace: planResult.workspace,
@@ -5261,10 +5343,10 @@ import {
5261
5343
  emitProcessCompleted as emitProcessCompleted2,
5262
5344
  emitProcessOutput as emitProcessOutput2,
5263
5345
  emitProcessStarted as emitProcessStarted2
5264
- } from "@wrongstack/core";
5346
+ } from "@wrongstack/core/observability";
5265
5347
 
5266
5348
  // src/_env.ts
5267
- import { buildChildEnv as buildChildEnv2 } from "@wrongstack/core";
5349
+ import { buildChildEnv as buildChildEnv2 } from "@wrongstack/core/utils";
5268
5350
 
5269
5351
  // src/bash.ts
5270
5352
  init_output_spool();
@@ -5278,8 +5360,9 @@ import * as os3 from "node:os";
5278
5360
  init_process_registry();
5279
5361
  import * as fs6 from "node:fs/promises";
5280
5362
  import * as os2 from "node:os";
5363
+ import { wstackGlobalRoot as wstackGlobalRoot3 } from "@wrongstack/core/utils";
5281
5364
  import * as path9 from "node:path";
5282
- var REGISTRY_FILE = ".wrongstack/process-registry.json";
5365
+ var REGISTRY_FILE = "process-registry.json";
5283
5366
  function toErrorMessage(err) {
5284
5367
  return err instanceof Error ? err.message : String(err);
5285
5368
  }
@@ -5297,7 +5380,8 @@ function emitStructuredLog(level, event, message, error) {
5297
5380
  }
5298
5381
  var HEARTBEAT_INTERVAL_MS = 5e3;
5299
5382
  var STALE_THRESHOLD_MS = 3e4;
5300
- var LOCKFILE = ".wrongstack/.process-registry.lock";
5383
+ var LOCK_STALE_MS = 3e4;
5384
+ var LOCKFILE = ".process-registry.lock";
5301
5385
  function generateInstanceId() {
5302
5386
  const hostname4 = os2.hostname();
5303
5387
  const pid = process.pid;
@@ -5325,21 +5409,26 @@ async function acquireLock(lockfilePath, timeoutMs = 5e3) {
5325
5409
  try {
5326
5410
  const content = await fs6.readFile(lockfilePath, "utf-8");
5327
5411
  const parts = content.split(":");
5328
- const lockPidStr = parts[0] ?? "0";
5329
- const lockPid = parseInt(lockPidStr, 10);
5330
- if (process.platform !== "win32") {
5412
+ const lockPid = parseInt(parts[0] ?? "0", 10);
5413
+ const lockTs = Number(parts[parts.length - 1]);
5414
+ const staleByAge = Number.isFinite(lockTs) && Date.now() - lockTs > LOCK_STALE_MS;
5415
+ let holderDead = false;
5416
+ if (process.platform !== "win32" && Number.isFinite(lockPid) && lockPid > 0) {
5331
5417
  try {
5332
5418
  process.kill(lockPid, 0);
5333
5419
  } catch {
5334
- await fs6.unlink(lockfilePath);
5335
- continue;
5420
+ holderDead = true;
5336
5421
  }
5337
5422
  }
5338
- } catch {
5339
- try {
5340
- await fs6.unlink(lockfilePath);
5341
- } catch {
5423
+ if (holderDead || staleByAge) {
5424
+ await fs6.unlink(lockfilePath).catch(() => {
5425
+ });
5426
+ continue;
5342
5427
  }
5428
+ } catch {
5429
+ await fs6.unlink(lockfilePath).catch(() => {
5430
+ });
5431
+ continue;
5343
5432
  }
5344
5433
  await new Promise((r) => setTimeout(r, 100));
5345
5434
  continue;
@@ -5349,25 +5438,35 @@ async function acquireLock(lockfilePath, timeoutMs = 5e3) {
5349
5438
  }
5350
5439
  throw new Error(`Failed to acquire lock after ${timeoutMs}ms`);
5351
5440
  }
5441
+ function freshRegistryData() {
5442
+ return {
5443
+ version: 1,
5444
+ instances: /* @__PURE__ */ new Map(),
5445
+ protectedPatterns: ["wrongstack", "node"],
5446
+ lastCleanup: Date.now()
5447
+ };
5448
+ }
5352
5449
  async function readRegistryFile(filePath) {
5450
+ let content;
5353
5451
  try {
5354
- const content = await fs6.readFile(filePath, "utf-8");
5355
- const parsed = JSON.parse(content);
5356
- if (parsed.instances && Array.isArray(parsed.instances)) {
5357
- parsed.instances = new Map(parsed.instances);
5358
- }
5359
- return parsed;
5452
+ content = await fs6.readFile(filePath, "utf-8");
5360
5453
  } catch (err) {
5361
- if (isNodeError(err) && err.code === "ENOENT") {
5362
- return {
5363
- version: 1,
5364
- instances: /* @__PURE__ */ new Map(),
5365
- protectedPatterns: ["wrongstack", "node"],
5366
- lastCleanup: Date.now()
5367
- };
5368
- }
5454
+ if (isNodeError(err) && err.code === "ENOENT") return freshRegistryData();
5369
5455
  throw err;
5370
5456
  }
5457
+ try {
5458
+ const parsed = JSON.parse(content);
5459
+ if (!parsed || typeof parsed !== "object") return freshRegistryData();
5460
+ const base = freshRegistryData();
5461
+ return {
5462
+ version: 1,
5463
+ instances: Array.isArray(parsed.instances) ? new Map(parsed.instances) : base.instances,
5464
+ protectedPatterns: Array.isArray(parsed.protectedPatterns) ? parsed.protectedPatterns : base.protectedPatterns,
5465
+ lastCleanup: typeof parsed.lastCleanup === "number" ? parsed.lastCleanup : base.lastCleanup
5466
+ };
5467
+ } catch {
5468
+ return freshRegistryData();
5469
+ }
5371
5470
  }
5372
5471
  async function writeRegistryFile(filePath, data) {
5373
5472
  const tmpPath = `${filePath}.tmp.${process.pid}`;
@@ -5386,12 +5485,18 @@ var PersistentProcessRegistry = class {
5386
5485
  lockPath;
5387
5486
  baseRegistry;
5388
5487
  heartbeatInterval = null;
5488
+ cleanupInterval = null;
5489
+ heartbeatRunning = false;
5490
+ cleanupRunning = false;
5389
5491
  isShuttingDown = false;
5492
+ onProcessExit = () => {
5493
+ void this.syncToPersistent();
5494
+ };
5390
5495
  constructor(baseRegistry) {
5391
5496
  this.instanceId = generateInstanceId();
5392
- const homeDir = os2.homedir();
5393
- this.registryPath = path9.join(homeDir, REGISTRY_FILE);
5394
- this.lockPath = path9.join(homeDir, LOCKFILE);
5497
+ const globalRoot = wstackGlobalRoot3();
5498
+ this.registryPath = path9.join(globalRoot, REGISTRY_FILE);
5499
+ this.lockPath = path9.join(globalRoot, LOCKFILE);
5395
5500
  this.baseRegistry = baseRegistry ?? getProcessRegistry();
5396
5501
  this.ensureDirectory().catch((err) => {
5397
5502
  emitStructuredLog("warn", "process_registry.dir_create_failed", "PersistentProcessRegistry: failed to create .wrongstack directory", err);
@@ -5410,16 +5515,18 @@ var PersistentProcessRegistry = class {
5410
5515
  */
5411
5516
  start() {
5412
5517
  if (this.heartbeatInterval) return;
5413
- this.syncToPersistent();
5518
+ this.isShuttingDown = false;
5519
+ this.heartbeat();
5414
5520
  this.heartbeatInterval = setInterval(() => {
5415
5521
  this.heartbeat();
5416
5522
  }, HEARTBEAT_INTERVAL_MS);
5417
5523
  this.heartbeatInterval.unref?.();
5418
- setInterval(() => {
5419
- this.cleanupStaleEntries();
5420
- }, STALE_THRESHOLD_MS).unref?.();
5524
+ this.cleanupInterval = setInterval(() => {
5525
+ this.cleanup();
5526
+ }, STALE_THRESHOLD_MS);
5527
+ this.cleanupInterval.unref?.();
5421
5528
  this.registerMainProcess();
5422
- process.on("exit", () => this.syncToPersistent());
5529
+ process.on("exit", this.onProcessExit);
5423
5530
  }
5424
5531
  /**
5425
5532
  * Stop the heartbeat and clean up.
@@ -5430,7 +5537,12 @@ var PersistentProcessRegistry = class {
5430
5537
  clearInterval(this.heartbeatInterval);
5431
5538
  this.heartbeatInterval = null;
5432
5539
  }
5433
- this.syncToPersistent();
5540
+ if (this.cleanupInterval) {
5541
+ clearInterval(this.cleanupInterval);
5542
+ this.cleanupInterval = null;
5543
+ }
5544
+ process.off("exit", this.onProcessExit);
5545
+ void this.syncToPersistent();
5434
5546
  }
5435
5547
  /**
5436
5548
  * Register the main WrongStack process as protected.
@@ -5514,8 +5626,18 @@ var PersistentProcessRegistry = class {
5514
5626
  * Send heartbeat to mark all this instance's processes as alive.
5515
5627
  */
5516
5628
  heartbeat() {
5517
- if (this.isShuttingDown) return;
5518
- this.syncToPersistent();
5629
+ if (this.isShuttingDown || this.heartbeatRunning) return;
5630
+ this.heartbeatRunning = true;
5631
+ void this.syncToPersistent().finally(() => {
5632
+ this.heartbeatRunning = false;
5633
+ });
5634
+ }
5635
+ cleanup() {
5636
+ if (this.isShuttingDown || this.cleanupRunning) return;
5637
+ this.cleanupRunning = true;
5638
+ void this.cleanupStaleEntries().finally(() => {
5639
+ this.cleanupRunning = false;
5640
+ });
5519
5641
  }
5520
5642
  /**
5521
5643
  * Sync this instance's processes to the persistent registry.
@@ -6237,14 +6359,12 @@ var bashTool = {
6237
6359
  const detached = !isWin5;
6238
6360
  const startedAt = Date.now();
6239
6361
  if (input.background) {
6240
- let buf2 = "";
6241
- let truncated = false;
6242
6362
  const child2 = spawn3(shell, args, {
6243
6363
  cwd: ctx.projectRoot,
6244
6364
  env,
6245
6365
  // PowerShell takes the script on stdin (no argv quoting); cmd.exe
6246
6366
  // and POSIX shells ignore stdin when given the command inline.
6247
- stdio: [plan.useStdin ? "pipe" : "ignore", "pipe", "pipe"],
6367
+ stdio: [plan.useStdin ? "pipe" : "ignore", "ignore", "ignore"],
6248
6368
  // win32: CreateProcess IGNORES CREATE_NO_WINDOW (windowsHide) when
6249
6369
  // DETACHED_PROCESS (detached: true) is set, so the console-less
6250
6370
  // cmd.exe's grandchildren (node, dev servers) each allocate a fresh
@@ -6263,8 +6383,8 @@ var bashTool = {
6263
6383
  }
6264
6384
  }
6265
6385
  const pid2 = child2.pid;
6266
- let stdoutBytes2 = 0;
6267
- let stderrBytes2 = 0;
6386
+ const stdoutBytes2 = 0;
6387
+ const stderrBytes2 = 0;
6268
6388
  let telemetryCompleted2 = false;
6269
6389
  emitProcessStarted2({
6270
6390
  ...pid2 !== void 0 ? { pid: pid2 } : {},
@@ -6297,41 +6417,17 @@ var bashTool = {
6297
6417
  startedAt: Date.now(),
6298
6418
  sessionId: ctx.session?.id,
6299
6419
  child: child2,
6300
- processGroupLeader: detached && child2.pid === pid2
6420
+ processGroupLeader: detached && child2.pid === pid2,
6421
+ background: true
6301
6422
  });
6302
6423
  child2.on("close", () => registry.unregister(pid2));
6303
6424
  }
6304
- const onBgData = (chunk, stream) => {
6305
- if (stream === "stdout") stdoutBytes2 += chunk.byteLength;
6306
- else stderrBytes2 += chunk.byteLength;
6307
- emitProcessOutput2({ pid: pid2, stream, chunk });
6308
- if (truncated) return;
6309
- const remain = MAX_OUTPUT - buf2.length;
6310
- if (remain > 0) {
6311
- buf2 += chunk.toString().slice(0, remain);
6312
- }
6313
- if (buf2.length >= MAX_OUTPUT) {
6314
- truncated = true;
6315
- child2.stdout?.off("data", onBgStdout);
6316
- child2.stderr?.off("data", onBgStderr);
6317
- }
6318
- };
6319
- const onBgStdout = (chunk) => onBgData(chunk, "stdout");
6320
- const onBgStderr = (chunk) => onBgData(chunk, "stderr");
6321
- child2.stdout?.on("data", onBgStdout);
6322
- child2.stderr?.on("data", onBgStderr);
6323
- const cleanupBackground = () => {
6324
- child2.stdout?.off("data", onBgStdout);
6325
- child2.stderr?.off("data", onBgStderr);
6326
- };
6327
6425
  child2.on("error", () => {
6328
- cleanupBackground();
6329
6426
  if (typeof pid2 === "number") registry.unregister(pid2);
6330
6427
  registry.afterCall(Date.now() - startedAt, true, bypassBreaker);
6331
6428
  completeBackground(1);
6332
6429
  });
6333
6430
  child2.on("close", (code, signal) => {
6334
- cleanupBackground();
6335
6431
  registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
6336
6432
  completeBackground(code ?? (signal ? 1 : 0), signal ?? void 0);
6337
6433
  });
@@ -6339,7 +6435,7 @@ var bashTool = {
6339
6435
  yield {
6340
6436
  type: "final",
6341
6437
  output: {
6342
- output: normalizeCommandOutput(buf2),
6438
+ output: "",
6343
6439
  exit_code: null,
6344
6440
  timed_out: false,
6345
6441
  pid: pid2
@@ -6610,6 +6706,7 @@ ${hint}` : ""),
6610
6706
  for (const entry of registry.bySession(sessionId)) {
6611
6707
  if (entry.name !== "bash") continue;
6612
6708
  if (entry.child.exitCode !== null) continue;
6709
+ if (entry.background) continue;
6613
6710
  if (entry.protected) continue;
6614
6711
  registry.kill(entry.pid, { force: true });
6615
6712
  }
@@ -6619,7 +6716,7 @@ ${hint}` : ""),
6619
6716
  // src/batch-tool-use.ts
6620
6717
  import {
6621
6718
  GOVERNED_TOOL_EXECUTOR_META_KEY
6622
- } from "@wrongstack/core";
6719
+ } from "@wrongstack/core/types";
6623
6720
  var batchToolUseTool = {
6624
6721
  name: "batch_tool_use",
6625
6722
  category: "Meta",
@@ -7399,6 +7496,10 @@ var BrowserSessionManager = class {
7399
7496
  const ids = [...this.sessions.values()].filter((session) => session.ownerId === ownerId).map((session) => session.id);
7400
7497
  for (const id of ids) await this.close(id, ownerId).catch(() => void 0);
7401
7498
  }
7499
+ /** True when the manager owns no live browser contexts. */
7500
+ isIdle() {
7501
+ return this.sessions.size === 0;
7502
+ }
7402
7503
  async dispose() {
7403
7504
  for (const session of [...this.sessions.values()]) {
7404
7505
  await session.context.close().catch(() => void 0);
@@ -7562,7 +7663,7 @@ async function abortable(signal, operation, onAbort) {
7562
7663
 
7563
7664
  // src/browser/tools.ts
7564
7665
  import * as path12 from "node:path";
7565
- import { ToolCapabilities } from "@wrongstack/core";
7666
+ import { ToolCapabilities } from "@wrongstack/core/security";
7566
7667
  import { resolveWstackPaths } from "@wrongstack/core/utils";
7567
7668
  var managers = /* @__PURE__ */ new Map();
7568
7669
  var cleanupSignalByContext = /* @__PURE__ */ new WeakMap();
@@ -7583,12 +7684,17 @@ function managerFor(ctx) {
7583
7684
  if (cleanupSignalByContext.get(ctx) !== runSignal) {
7584
7685
  cleanupSignalByContext.set(ctx, runSignal);
7585
7686
  const managerForRun = manager;
7687
+ const rootForRun = root;
7586
7688
  const ownerForRun = owner(ctx);
7587
7689
  ctx.registerAbortHook?.(async () => {
7588
7690
  if (cleanupSignalByContext.get(ctx) === runSignal) {
7589
7691
  cleanupSignalByContext.delete(ctx);
7590
7692
  }
7591
7693
  await managerForRun.closeOwner(ownerForRun);
7694
+ if (managerForRun.isIdle() && managers.get(rootForRun) === managerForRun) {
7695
+ managers.delete(rootForRun);
7696
+ await managerForRun.dispose();
7697
+ }
7592
7698
  });
7593
7699
  }
7594
7700
  return manager;
@@ -7976,7 +8082,7 @@ async function shutdownBrowserTools() {
7976
8082
  }
7977
8083
 
7978
8084
  // src/codebase-index/writer.ts
7979
- import { expectDefined as expectDefined2 } from "@wrongstack/core";
8085
+ import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
7980
8086
 
7981
8087
  // src/codebase-index/circuit-breaker.ts
7982
8088
  var CircuitOpenError = class extends Error {
@@ -8066,7 +8172,7 @@ import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
8066
8172
  import { createRequire } from "node:module";
8067
8173
  import * as fs9 from "node:fs";
8068
8174
  import * as path13 from "node:path";
8069
- import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core";
8175
+ import { resolveWstackPaths as resolveWstackPaths2, sqliteCachePragmas } from "@wrongstack/core/utils";
8070
8176
 
8071
8177
  // src/codebase-index/schema.ts
8072
8178
  var SCHEMA_VERSION = 3;
@@ -8122,23 +8228,13 @@ function buildBm25Index(docs) {
8122
8228
  const tokens = tokenise(d.text);
8123
8229
  return { id: d.id, tokens, raw: d.text, len: tokens.length };
8124
8230
  });
8125
- const df = {};
8126
- for (const doc of documents) {
8127
- const seen = /* @__PURE__ */ new Set();
8128
- for (const t of doc.tokens) {
8129
- if (!seen.has(t)) {
8130
- df[t] = (df[t] ?? 0) + 1;
8131
- seen.add(t);
8132
- }
8133
- }
8134
- }
8135
8231
  const N = documents.length;
8136
8232
  const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
8137
8233
  const avgLen = N === 0 ? 0 : totalLen / N;
8138
- return new Bm25Index(documents, df, N, avgLen);
8234
+ return new Bm25Index(documents, N, avgLen);
8139
8235
  }
8140
8236
  var Bm25Index = class {
8141
- constructor(documents, _df, N, avgLen) {
8237
+ constructor(documents, N, avgLen) {
8142
8238
  this.documents = documents;
8143
8239
  this.N = N;
8144
8240
  this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
@@ -8146,9 +8242,19 @@ var Bm25Index = class {
8146
8242
  documents;
8147
8243
  N;
8148
8244
  safeAvgLen;
8245
+ /** Lazily-built id→doc index so getDoc is O(1) instead of an O(D) linear find. */
8246
+ _byId;
8149
8247
  score(query, filter) {
8150
8248
  const qTokens = tokenise(query);
8151
8249
  if (qTokens.length === 0) return [];
8250
+ const dfByTerm = /* @__PURE__ */ new Map();
8251
+ for (const qTerm of qTokens) {
8252
+ let dfVal = 0;
8253
+ for (const candidate of this.documents) {
8254
+ if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
8255
+ }
8256
+ dfByTerm.set(qTerm, dfVal);
8257
+ }
8152
8258
  const results = [];
8153
8259
  for (const doc of this.documents) {
8154
8260
  if (filter && !filter(doc.id)) continue;
@@ -8159,10 +8265,7 @@ var Bm25Index = class {
8159
8265
  if (t.startsWith(qTerm)) tf++;
8160
8266
  }
8161
8267
  if (tf === 0) continue;
8162
- let dfVal = 0;
8163
- for (const candidate of this.documents) {
8164
- if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
8165
- }
8268
+ const dfVal = dfByTerm.get(qTerm) ?? 0;
8166
8269
  if (dfVal === 0) continue;
8167
8270
  const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
8168
8271
  const lenRatio = B * (doc.len / this.safeAvgLen);
@@ -8174,7 +8277,10 @@ var Bm25Index = class {
8174
8277
  return results;
8175
8278
  }
8176
8279
  getDoc(id) {
8177
- return this.documents.find((d) => d.id === id);
8280
+ if (!this._byId) {
8281
+ this._byId = new Map(this.documents.map((d) => [d.id, d]));
8282
+ }
8283
+ return this._byId.get(id);
8178
8284
  }
8179
8285
  extractSnippet(docId, queryTokens, radius = 40) {
8180
8286
  const doc = this.getDoc(docId);
@@ -8336,6 +8442,25 @@ var IndexStore = class _IndexStore {
8336
8442
  * and reuse it. Cleared in {@link close} when the connection is torn down.
8337
8443
  */
8338
8444
  stmtCache = /* @__PURE__ */ new Map();
8445
+ /**
8446
+ * Cached full-corpus BM25 index for the FTS5-unavailable fallback path.
8447
+ * Built lazily on the first `searchRankedFallback` call and invalidated
8448
+ * (via `bm25Dirty`) whenever the `symbols` table is mutated. Computing
8449
+ * IDF over the full corpus is also more correct than the old per-query
8450
+ * candidate-subset IDF.
8451
+ *
8452
+ * Cache-lifecycle invariants (single source of truth lives at the
8453
+ * `invalidateBm25()` helper — see its docblock for the "every mutation
8454
+ * MUST call this" contract):
8455
+ * - declaration: this field + `bm25Dirty` (here)
8456
+ * - invalidation: `invalidateBm25()` flips the flag and nulls the cache
8457
+ * - build: `getOrBuildBm25()` rebuilds against current `symbols` rows
8458
+ * - teardown: `close()` resets the flag and nulls the cache
8459
+ */
8460
+ bm25Cache = null;
8461
+ // Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
8462
+ // an empty or pre-existing corpus makes a stale IDF table meaningless.
8463
+ bm25Dirty = true;
8339
8464
  /** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
8340
8465
  stmt(sql) {
8341
8466
  let s = this.stmtCache.get(sql);
@@ -8383,7 +8508,15 @@ var IndexStore = class _IndexStore {
8383
8508
  this.db = new Database(path13.join(this.indexDir, DB_FILE));
8384
8509
  try {
8385
8510
  this.db.exec("PRAGMA journal_mode = WAL");
8386
- this.db.exec("PRAGMA busy_timeout = 5000");
8511
+ this.db.exec("PRAGMA synchronous = NORMAL");
8512
+ this.db.exec("PRAGMA busy_timeout = 15000");
8513
+ this.db.exec("PRAGMA temp_store = MEMORY");
8514
+ const cache2 = sqliteCachePragmas();
8515
+ this.db.exec(`PRAGMA cache_size = -${cache2.cacheSizeKiB}`);
8516
+ this.db.exec(`PRAGMA mmap_size = ${cache2.mmapBytes}`);
8517
+ this.db.exec("PRAGMA foreign_keys = ON");
8518
+ this.db.exec("PRAGMA journal_size_limit = 67108864");
8519
+ this.db.exec("PRAGMA wal_autocheckpoint = 1000");
8387
8520
  } catch {
8388
8521
  }
8389
8522
  this.initSchema();
@@ -8395,7 +8528,7 @@ var IndexStore = class _IndexStore {
8395
8528
  value TEXT NOT NULL
8396
8529
  );
8397
8530
  `);
8398
- const storedRows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all("version");
8531
+ const storedRows = this.stmt("SELECT value FROM metadata WHERE key = ?").all("version");
8399
8532
  const storedVersion = storedRows.length ? Number(storedRows[0]?.value) : null;
8400
8533
  if (storedVersion !== null && storedVersion !== SCHEMA_VERSION) {
8401
8534
  this.db.exec(`
@@ -8404,9 +8537,9 @@ var IndexStore = class _IndexStore {
8404
8537
  DROP TABLE IF EXISTS refs;
8405
8538
  `);
8406
8539
  this.db.exec("DROP TABLE IF EXISTS symbols_fts");
8407
- this.db.prepare("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
8540
+ this.stmt("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
8408
8541
  } else if (storedVersion === null) {
8409
- this.db.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
8542
+ this.stmt("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
8410
8543
  }
8411
8544
  this.db.exec(`
8412
8545
  CREATE TABLE IF NOT EXISTS files (
@@ -8437,6 +8570,7 @@ var IndexStore = class _IndexStore {
8437
8570
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_file ON symbols(file)");
8438
8571
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_lang_kind ON symbols(lang, kind)");
8439
8572
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_file_fk ON symbols(file_fk)");
8573
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_name_id ON symbols(name, id)");
8440
8574
  this.db.exec(`
8441
8575
  CREATE TABLE IF NOT EXISTS refs (
8442
8576
  id INTEGER PRIMARY KEY,
@@ -8457,65 +8591,167 @@ var IndexStore = class _IndexStore {
8457
8591
  );
8458
8592
  this.ftsAvailable = true;
8459
8593
  const symbolCount = Number(
8460
- this.db.prepare("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
8594
+ this.stmt("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
8461
8595
  );
8462
8596
  const ftsCount = Number(
8463
- this.db.prepare("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
8597
+ this.stmt("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
8464
8598
  );
8465
8599
  if (symbolCount !== ftsCount) {
8466
8600
  this.db.exec("DELETE FROM symbols_fts");
8467
- const insert = this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)");
8468
- const rows = this.db.prepare("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
8469
- for (const row of rows) {
8470
- insert.run(row.id, buildIndexableText(row.name, row.signature, row.doc_comment));
8471
- }
8601
+ const rows = this.stmt("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
8602
+ this.bulkInsertFts(
8603
+ rows.map((row) => ({
8604
+ id: row.id,
8605
+ text: buildIndexableText(row.name, row.signature, row.doc_comment)
8606
+ }))
8607
+ );
8608
+ this.invalidateBm25();
8472
8609
  }
8473
8610
  } catch {
8474
8611
  this.ftsAvailable = false;
8475
8612
  }
8613
+ this.ensureNextSymbolIdSeeded();
8614
+ }
8615
+ // ─── ID allocation & bulk helpers ────────────────────────────────────────────
8616
+ static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
8617
+ /** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
8618
+ static MAX_SQL_VARS = 900;
8619
+ /**
8620
+ * Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
8621
+ * transaction on open; the first concurrent writer under BEGIN IMMEDIATE
8622
+ * re-reads and advances the counter atomically.
8623
+ */
8624
+ ensureNextSymbolIdSeeded() {
8625
+ const existing = this.stmt("SELECT value FROM metadata WHERE key = ?").get(
8626
+ _IndexStore.NEXT_SYMBOL_ID_KEY
8627
+ );
8628
+ if (existing?.value !== void 0) return;
8629
+ const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8630
+ const next = (maxRows[0]?.m ?? 0) + 1;
8631
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)").run(
8632
+ _IndexStore.NEXT_SYMBOL_ID_KEY,
8633
+ String(next)
8634
+ );
8635
+ }
8636
+ /**
8637
+ * Reserve `count` consecutive symbol ids. MUST run inside BEGIN IMMEDIATE
8638
+ * so concurrent indexers cannot hand out overlapping ranges.
8639
+ */
8640
+ allocateSymbolIds(count) {
8641
+ if (count <= 0) return this.getMaxSymbolId() + 1;
8642
+ this.ensureNextSymbolIdSeeded();
8643
+ const row = this.stmt("SELECT value FROM metadata WHERE key = ?").get(
8644
+ _IndexStore.NEXT_SYMBOL_ID_KEY
8645
+ );
8646
+ const start = Math.max(1, Number(row?.value ?? 1) || 1);
8647
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)").run(
8648
+ _IndexStore.NEXT_SYMBOL_ID_KEY,
8649
+ String(start + count)
8650
+ );
8651
+ return start;
8652
+ }
8653
+ /** Multi-row INSERT for symbols — amortizes prepare/bind overhead in large batches. */
8654
+ bulkInsertSymbols(rows) {
8655
+ if (rows.length === 0) return;
8656
+ const cols = 12;
8657
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / cols));
8658
+ for (let i = 0; i < rows.length; i += chunkSize) {
8659
+ const chunk = rows.slice(i, i + chunkSize);
8660
+ const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
8661
+ const stmt = this.stmt(
8662
+ `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
8663
+ VALUES ${placeholders}`
8664
+ );
8665
+ const binds = [];
8666
+ for (const r of chunk) {
8667
+ binds.push(
8668
+ r.id,
8669
+ r.lang,
8670
+ r.kind,
8671
+ r.name,
8672
+ r.file,
8673
+ r.line,
8674
+ r.col,
8675
+ r.signature,
8676
+ r.docComment,
8677
+ r.scope,
8678
+ r.text,
8679
+ r.file
8680
+ );
8681
+ }
8682
+ stmt.run(...binds);
8683
+ }
8684
+ }
8685
+ bulkInsertFts(rows) {
8686
+ if (!this.ftsAvailable || rows.length === 0) return;
8687
+ const cols = 2;
8688
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / cols));
8689
+ for (let i = 0; i < rows.length; i += chunkSize) {
8690
+ const chunk = rows.slice(i, i + chunkSize);
8691
+ const placeholders = chunk.map(() => "(?, ?)").join(", ");
8692
+ const stmt = this.stmt(`INSERT INTO symbols_fts(rowid, text) VALUES ${placeholders}`);
8693
+ const binds = [];
8694
+ for (const r of chunk) binds.push(r.id, r.text);
8695
+ stmt.run(...binds);
8696
+ }
8697
+ }
8698
+ bulkInsertRefs(refs) {
8699
+ if (refs.length === 0) return;
8700
+ const cols = 5;
8701
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / cols));
8702
+ for (let i = 0; i < refs.length; i += chunkSize) {
8703
+ const chunk = refs.slice(i, i + chunkSize);
8704
+ const placeholders = chunk.map(() => "(?, ?, ?, ?, ?)").join(", ");
8705
+ const stmt = this.stmt(
8706
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line) VALUES ${placeholders}`
8707
+ );
8708
+ const binds = [];
8709
+ for (const ref of chunk) {
8710
+ binds.push(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
8711
+ }
8712
+ stmt.run(...binds);
8713
+ }
8476
8714
  }
8477
8715
  // ─── Symbol CRUD ─────────────────────────────────────────────────────────────
8478
8716
  /**
8479
8717
  * Insert symbols, assigning IDs atomically inside `BEGIN IMMEDIATE` /
8480
- * `COMMIT`. The ID allocation (`SELECT MAX(id)`) and all `INSERT`s share
8481
- * the same transaction, preventing UNIQUE constraint violations when two
8482
- * processes index concurrently (each would see a different `MAX(id)` and
8483
- * neither can insert with the other's IDs).
8718
+ * `COMMIT`. Id ranges come from the `next_symbol_id` metadata counter
8719
+ * (O(1)); multi-row INSERT amortizes bind overhead for large files.
8484
8720
  *
8485
8721
  * @returns The symbols array with `id` fields populated so the caller can
8486
8722
  * use them for refs without re-reading from the DB.
8487
8723
  */
8488
8724
  insertSymbols(symbols) {
8725
+ this.invalidateBm25();
8489
8726
  return this.runWithRetry(() => {
8490
8727
  this.db.exec("BEGIN IMMEDIATE");
8491
8728
  try {
8492
- const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8493
- let nextId = (maxRows[0]?.m ?? 0) + 1;
8494
- const stmt = this.db.prepare(
8495
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
8496
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
8497
- );
8498
- const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
8729
+ let nextId = this.allocateSymbolIds(symbols.length);
8499
8730
  const result = [];
8731
+ const bulk = [];
8732
+ const ftsRows = [];
8500
8733
  for (const s of symbols) {
8501
8734
  const id = nextId++;
8502
- stmt.run(
8735
+ bulk.push({
8503
8736
  id,
8504
- s.lang,
8505
- s.kind,
8506
- s.name,
8507
- s.file,
8508
- s.line,
8509
- s.col,
8510
- s.signature,
8511
- s.docComment,
8512
- s.scope,
8513
- s.text,
8514
- s.file
8515
- );
8516
- ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
8737
+ lang: s.lang,
8738
+ kind: s.kind,
8739
+ name: s.name,
8740
+ file: s.file,
8741
+ line: s.line,
8742
+ col: s.col,
8743
+ signature: s.signature,
8744
+ docComment: s.docComment,
8745
+ scope: s.scope,
8746
+ text: s.text
8747
+ });
8748
+ if (this.ftsAvailable) {
8749
+ ftsRows.push({ id, text: buildIndexableText(s.name, s.signature, s.docComment) });
8750
+ }
8517
8751
  result.push({ ...s, id });
8518
8752
  }
8753
+ this.bulkInsertSymbols(bulk);
8754
+ this.bulkInsertFts(ftsRows);
8519
8755
  this.db.exec("COMMIT");
8520
8756
  return result;
8521
8757
  } catch (err) {
@@ -8525,6 +8761,7 @@ var IndexStore = class _IndexStore {
8525
8761
  });
8526
8762
  }
8527
8763
  deleteSymbolsForFile(file) {
8764
+ this.invalidateBm25();
8528
8765
  this.runWithRetry(() => {
8529
8766
  if (this.ftsAvailable) {
8530
8767
  this.stmt(
@@ -8540,17 +8777,20 @@ var IndexStore = class _IndexStore {
8540
8777
  * dropped the `files` row, leaving its symbols orphaned but still searchable.
8541
8778
  */
8542
8779
  deleteFile(file) {
8780
+ this.invalidateBm25();
8543
8781
  this.runWithRetry(() => {
8544
8782
  this.db.exec("BEGIN IMMEDIATE");
8545
8783
  try {
8546
8784
  if (this.ftsAvailable) {
8547
- this.db.prepare(
8785
+ this.stmt(
8548
8786
  "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
8549
8787
  ).run(file);
8550
8788
  }
8551
- this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
8552
- this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
8553
- this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
8789
+ this.stmt(
8790
+ "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
8791
+ ).run(file);
8792
+ this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
8793
+ this.stmt("DELETE FROM files WHERE file = ?").run(file);
8554
8794
  this.db.exec("COMMIT");
8555
8795
  } catch (err) {
8556
8796
  this.db.exec("ROLLBACK");
@@ -8587,7 +8827,7 @@ var IndexStore = class _IndexStore {
8587
8827
  };
8588
8828
  }
8589
8829
  getAllFileMetas() {
8590
- return this.db.prepare("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
8830
+ return this.stmt("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
8591
8831
  file: r.file,
8592
8832
  lang: r.lang,
8593
8833
  mtimeMs: r.mtime_ms,
@@ -8596,7 +8836,32 @@ var IndexStore = class _IndexStore {
8596
8836
  }));
8597
8837
  }
8598
8838
  // ─── Search ──────────────────────────────────────────────────────────────────
8599
- search(query, filter) {
8839
+ search(query, filter, opts) {
8840
+ const built = this.buildSearchWhere(query, filter);
8841
+ if (built === null) return [];
8842
+ const { where, values } = built;
8843
+ const limit = typeof opts?.limit === "number" && Number.isFinite(opts.limit) ? Math.max(0, Math.trunc(opts.limit)) : void 0;
8844
+ const limitSql = limit !== void 0 ? " LIMIT ?" : "";
8845
+ const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}${limitSql}`;
8846
+ const binds = limit !== void 0 ? [...values, limit] : values;
8847
+ const rows = this.stmt(sql).all(...binds);
8848
+ return rows.map((r) => ({
8849
+ id: r.id,
8850
+ lang: r.lang,
8851
+ kind: r.kind,
8852
+ name: r.name,
8853
+ file: r.file,
8854
+ line: r.line,
8855
+ col: r.col,
8856
+ signature: r.signature,
8857
+ docComment: r.doc_comment,
8858
+ score: 0,
8859
+ snippet: "",
8860
+ lspKind: filter?.lspKind
8861
+ }));
8862
+ }
8863
+ /** Shared WHERE builder for {@link search} / empty-query ranked totals. */
8864
+ buildSearchWhere(query, filter) {
8600
8865
  const conditions = [];
8601
8866
  const values = [];
8602
8867
  let effectiveKind = filter?.kind;
@@ -8605,7 +8870,7 @@ var IndexStore = class _IndexStore {
8605
8870
  if (mapped !== null) {
8606
8871
  effectiveKind = mapped;
8607
8872
  } else {
8608
- return [];
8873
+ return null;
8609
8874
  }
8610
8875
  }
8611
8876
  if (effectiveKind) {
@@ -8627,23 +8892,15 @@ var IndexStore = class _IndexStore {
8627
8892
  for (const t of tokens) values.push(`%${t}%`);
8628
8893
  }
8629
8894
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
8630
- const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}`;
8631
- const stmt = this.db.prepare(sql);
8632
- const rows = stmt.all(...values);
8633
- return rows.map((r) => ({
8634
- id: r.id,
8635
- lang: r.lang,
8636
- kind: r.kind,
8637
- name: r.name,
8638
- file: r.file,
8639
- line: r.line,
8640
- col: r.col,
8641
- signature: r.signature,
8642
- docComment: r.doc_comment,
8643
- score: 0,
8644
- snippet: "",
8645
- lspKind: filter?.lspKind
8646
- }));
8895
+ return { where, values };
8896
+ }
8897
+ countSearch(query, filter) {
8898
+ const built = this.buildSearchWhere(query, filter);
8899
+ if (built === null) return 0;
8900
+ const row = this.stmt(`SELECT COUNT(*) AS n FROM symbols ${built.where}`).get(
8901
+ ...built.values
8902
+ );
8903
+ return Number(row?.n ?? 0);
8647
8904
  }
8648
8905
  /**
8649
8906
  * Ranked search — the one-stop query the codebase-search tool and plug-lsp
@@ -8684,12 +8941,12 @@ var IndexStore = class _IndexStore {
8684
8941
  values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
8685
8942
  }
8686
8943
  const where = conditions.join(" AND ");
8687
- const countRows = this.db.prepare(
8944
+ const countRows = this.stmt(
8688
8945
  `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
8689
8946
  ).all(...values);
8690
8947
  const total = countRows[0] ? Number(countRows[0].n) : 0;
8691
8948
  if (total === 0) return { results: [], total: 0 };
8692
- const rows = this.db.prepare(
8949
+ const rows = this.stmt(
8693
8950
  `SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
8694
8951
  -bm25(symbols_fts) AS score,
8695
8952
  snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
@@ -8722,20 +8979,60 @@ var IndexStore = class _IndexStore {
8722
8979
  total
8723
8980
  };
8724
8981
  }
8982
+ /**
8983
+ * Invalidate the cached BM25 index.
8984
+ *
8985
+ * **Contract: every method that mutates `symbols` MUST call this before
8986
+ * returning.** (`refs` mutations do not affect the BM25 fallback because
8987
+ * the corpus is built from `symbols.text` via `getAllIndexable()` and the
8988
+ * BM25 score is filtered by the LIKE-selected candidate set in
8989
+ * `searchRankedFallback`.) Today the call sites are `repairDrift`,
8990
+ * `insertSymbols`, `deleteSymbolsForFile`, `deleteFile`, `clearAll`, and
8991
+ * `commitBatch`. A future mutation that adds a new write path (e.g.
8992
+ * `renameFile`, `updateSignature`) MUST also call this — otherwise the
8993
+ * FTS5-unavailable fallback will serve stale search results. The
8994
+ * `close()` reset at L1820-1821 tears the cache down on store shutdown,
8995
+ * which is the only legitimate place that flips the flag outside this
8996
+ * helper.
8997
+ *
8998
+ * Called *before* `runWithRetry` on purpose: if the write fails all
8999
+ * retries the flag stays set, forcing a rebuild on the next search rather
9000
+ * than trusting a cache that may not reflect the intended mutation.
9001
+ * Do not move this inside the retry closure.
9002
+ */
9003
+ invalidateBm25() {
9004
+ this.bm25Dirty = true;
9005
+ this.bm25Cache = null;
9006
+ }
9007
+ /**
9008
+ * Return the cached full-corpus BM25 index, rebuilding it only when the
9009
+ * symbols table has been mutated since the last build. The full-corpus IDF
9010
+ * is more correct than the old per-query candidate-subset IDF, and the
9011
+ * amortized build cost drops from O(symbols × tokens) per search to once
9012
+ * per write batch.
9013
+ *
9014
+ * Note: the first call after a long idle (or on a freshly opened store)
9015
+ * pays the full corpus rebuild synchronously on the search path. For a
9016
+ * 5 500+ symbol corpus this is a visible one-time latency spike.
9017
+ */
9018
+ getOrBuildBm25() {
9019
+ if (this.bm25Cache && !this.bm25Dirty) return this.bm25Cache;
9020
+ const docs = this.getAllIndexable();
9021
+ this.bm25Cache = buildBm25Index(docs);
9022
+ this.bm25Dirty = false;
9023
+ return this.bm25Cache;
9024
+ }
8725
9025
  /** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
8726
9026
  searchRankedFallback(query, filter, limit) {
8727
- const candidates = this.search(query, filter);
8728
- if (candidates.length === 0) return { results: [], total: 0 };
8729
9027
  if (!query.trim()) {
8730
- return { results: candidates.slice(0, limit), total: candidates.length };
9028
+ const total = this.countSearch(query, filter);
9029
+ if (total === 0) return { results: [], total: 0 };
9030
+ return { results: this.search(query, filter, { limit }), total };
8731
9031
  }
9032
+ const candidates = this.search(query, filter);
9033
+ if (candidates.length === 0) return { results: [], total: 0 };
8732
9034
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
8733
- const bm25 = buildBm25Index(
8734
- candidates.map((c) => ({
8735
- id: c.id,
8736
- text: buildIndexableText(c.name, c.signature, c.docComment)
8737
- }))
8738
- );
9035
+ const bm25 = this.getOrBuildBm25();
8739
9036
  const scored = bm25.score(query, (id) => candidateById.has(id));
8740
9037
  const q = query.trim().toLowerCase();
8741
9038
  const rank = (id) => {
@@ -8777,7 +9074,7 @@ var IndexStore = class _IndexStore {
8777
9074
  // ─── Stats ───────────────────────────────────────────────────────────────────
8778
9075
  getStats() {
8779
9076
  const sizeBytes = this.sizeBytes();
8780
- const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
9077
+ const lastRows = this.stmt("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
8781
9078
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
8782
9079
  const totalRows = this.stmt("SELECT COUNT(*) FROM symbols").all();
8783
9080
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
@@ -8802,19 +9099,20 @@ var IndexStore = class _IndexStore {
8802
9099
  }
8803
9100
  setLastIndexed(ts2) {
8804
9101
  this.runWithRetry(() => {
8805
- this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
9102
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
8806
9103
  });
8807
9104
  }
8808
9105
  getMetadata(key) {
8809
- const rows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all(key);
9106
+ const rows = this.stmt("SELECT value FROM metadata WHERE key = ?").all(key);
8810
9107
  return rows[0]?.value;
8811
9108
  }
8812
9109
  setMetadata(key, value) {
8813
9110
  this.runWithRetry(() => {
8814
- this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
9111
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
8815
9112
  });
8816
9113
  }
8817
9114
  clearAll() {
9115
+ this.invalidateBm25();
8818
9116
  this.runWithRetry(() => {
8819
9117
  this.db.exec("BEGIN IMMEDIATE");
8820
9118
  try {
@@ -8841,13 +9139,7 @@ var IndexStore = class _IndexStore {
8841
9139
  this.runWithRetry(() => {
8842
9140
  this.stmt("DELETE FROM refs WHERE from_id = ?").run(fromId);
8843
9141
  if (refs.length === 0) return;
8844
- const stmt = this.db.prepare(
8845
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
8846
- VALUES (?, ?, ?, ?, ?)`
8847
- );
8848
- for (const ref of refs) {
8849
- stmt.run(fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
8850
- }
9142
+ this.bulkInsertRefs(refs.map((ref) => ({ ...ref, fromId })));
8851
9143
  });
8852
9144
  }
8853
9145
  /**
@@ -8864,13 +9156,7 @@ var IndexStore = class _IndexStore {
8864
9156
  insertRefsBatch(refs) {
8865
9157
  if (refs.length === 0) return;
8866
9158
  this.runWithRetry(() => {
8867
- const stmt = this.db.prepare(
8868
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
8869
- VALUES (?, ?, ?, ?, ?)`
8870
- );
8871
- for (const ref of refs) {
8872
- stmt.run(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
8873
- }
9159
+ this.bulkInsertRefs(refs);
8874
9160
  });
8875
9161
  }
8876
9162
  /**
@@ -8895,65 +9181,61 @@ var IndexStore = class _IndexStore {
8895
9181
  if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
8896
9182
  return [];
8897
9183
  }
9184
+ this.invalidateBm25();
8898
9185
  return this.runWithRetry(() => {
8899
9186
  this.db.exec("BEGIN IMMEDIATE");
8900
9187
  try {
8901
9188
  if (options.deleteForFiles && options.deleteForFiles.length > 0) {
8902
9189
  const placeholders = options.deleteForFiles.map(() => "?").join(",");
8903
9190
  if (this.ftsAvailable) {
8904
- this.db.prepare(
9191
+ this.stmt(
8905
9192
  `DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
8906
9193
  ).run(...options.deleteForFiles);
8907
9194
  }
8908
- this.db.prepare(
9195
+ this.stmt(
8909
9196
  `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
8910
9197
  ).run(...options.deleteForFiles);
8911
- this.db.prepare(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
9198
+ this.stmt(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
8912
9199
  }
8913
- const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8914
- let nextId = (maxRows[0]?.m ?? 0) + 1;
8915
- const symStmt = this.db.prepare(
8916
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
8917
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
8918
- );
8919
- const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
9200
+ const totalSymbols = entries.reduce((n, e) => n + e.symbols.length, 0);
9201
+ let nextId = this.allocateSymbolIds(totalSymbols);
8920
9202
  const allInserted = [];
8921
9203
  const refsToInsert = [];
9204
+ const bulkSyms = [];
9205
+ const ftsRows = [];
8922
9206
  for (const entry of entries) {
8923
9207
  const insertedForEntry = [];
8924
9208
  for (const s of entry.symbols) {
8925
9209
  const id = nextId++;
8926
- symStmt.run(
9210
+ bulkSyms.push({
8927
9211
  id,
8928
- s.lang,
8929
- s.kind,
8930
- s.name,
8931
- s.file,
8932
- s.line,
8933
- s.col,
8934
- s.signature,
8935
- s.docComment,
8936
- s.scope,
8937
- s.text,
8938
- s.file
8939
- );
8940
- ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
9212
+ lang: s.lang,
9213
+ kind: s.kind,
9214
+ name: s.name,
9215
+ file: s.file,
9216
+ line: s.line,
9217
+ col: s.col,
9218
+ signature: s.signature,
9219
+ docComment: s.docComment,
9220
+ scope: s.scope,
9221
+ text: s.text
9222
+ });
9223
+ if (this.ftsAvailable) {
9224
+ ftsRows.push({
9225
+ id,
9226
+ text: buildIndexableText(s.name, s.signature, s.docComment)
9227
+ });
9228
+ }
8941
9229
  const inserted = { ...s, id };
8942
9230
  allInserted.push(inserted);
8943
9231
  insertedForEntry.push(inserted);
8944
9232
  }
8945
9233
  refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
8946
9234
  }
8947
- if (refsToInsert.length > 0) {
8948
- const refStmt = this.db.prepare(
8949
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
8950
- VALUES (?, ?, ?, ?, ?)`
8951
- );
8952
- for (const ref of refsToInsert) {
8953
- refStmt.run(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
8954
- }
8955
- }
8956
- const upsertStmt = this.db.prepare(
9235
+ this.bulkInsertSymbols(bulkSyms);
9236
+ this.bulkInsertFts(ftsRows);
9237
+ this.bulkInsertRefs(refsToInsert);
9238
+ const upsertStmt = this.stmt(
8957
9239
  `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
8958
9240
  VALUES (?, ?, ?, ?, ?)
8959
9241
  ON CONFLICT(file) DO UPDATE SET
@@ -8980,7 +9262,7 @@ var IndexStore = class _IndexStore {
8980
9262
  */
8981
9263
  deleteRefsForFile(file) {
8982
9264
  this.runWithRetry(() => {
8983
- this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
9265
+ this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
8984
9266
  });
8985
9267
  }
8986
9268
  /**
@@ -8993,20 +9275,75 @@ var IndexStore = class _IndexStore {
8993
9275
  */
8994
9276
  resolveRefs() {
8995
9277
  return this.runWithRetry(() => {
8996
- const result = this.db.prepare(
8997
- `UPDATE refs SET to_id = (
8998
- SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
8999
- ) WHERE to_id IS NULL AND to_name IS NOT NULL
9000
- AND to_name IN (SELECT name FROM symbols)`
9001
- ).run();
9002
- return result.changes ?? 0;
9278
+ try {
9279
+ const result = this.stmt(
9280
+ `UPDATE refs
9281
+ SET to_id = s.id
9282
+ FROM (
9283
+ SELECT name, MIN(id) AS id FROM symbols GROUP BY name
9284
+ ) AS s
9285
+ WHERE refs.to_id IS NULL
9286
+ AND refs.to_name IS NOT NULL
9287
+ AND refs.to_name = s.name`
9288
+ ).run();
9289
+ return result.changes ?? 0;
9290
+ } catch {
9291
+ const result = this.stmt(
9292
+ `UPDATE refs SET to_id = (
9293
+ SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
9294
+ ) WHERE to_id IS NULL AND to_name IS NOT NULL
9295
+ AND to_name IN (SELECT name FROM symbols)`
9296
+ ).run();
9297
+ return result.changes ?? 0;
9298
+ }
9003
9299
  });
9004
9300
  }
9301
+ /**
9302
+ * Clear symbols/refs for a file and mark it as indexed with zero symbols.
9303
+ * Used by the indexer for empty-parse results so three writes share one txn.
9304
+ */
9305
+ replaceEmptyFile(meta) {
9306
+ this.invalidateBm25();
9307
+ this.runWithRetry(() => {
9308
+ this.db.exec("BEGIN IMMEDIATE");
9309
+ try {
9310
+ if (this.ftsAvailable) {
9311
+ this.stmt(
9312
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
9313
+ ).run(meta.file);
9314
+ }
9315
+ this.stmt(
9316
+ "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
9317
+ ).run(meta.file);
9318
+ this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(meta.file);
9319
+ this.stmt(
9320
+ `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
9321
+ VALUES (?, ?, ?, ?, ?)
9322
+ ON CONFLICT(file) DO UPDATE SET
9323
+ lang = excluded.lang,
9324
+ mtime_ms = excluded.mtime_ms,
9325
+ symbol_count = excluded.symbol_count,
9326
+ last_indexed = excluded.last_indexed`
9327
+ ).run(meta.file, meta.lang, meta.mtimeMs, meta.symbolCount, meta.lastIndexed);
9328
+ this.db.exec("COMMIT");
9329
+ } catch (err) {
9330
+ this.db.exec("ROLLBACK");
9331
+ throw err;
9332
+ }
9333
+ });
9334
+ }
9335
+ /** Best-effort query planner refresh after a large reindex. */
9336
+ optimize() {
9337
+ try {
9338
+ this.db.exec("PRAGMA optimize");
9339
+ } catch {
9340
+ }
9341
+ }
9005
9342
  /**
9006
9343
  * Find all references TO a given symbol (who calls / uses this symbol?).
9007
9344
  */
9008
9345
  findRefsTo(symbolId) {
9009
- return this.db.prepare(
9346
+ return this.stmt(
9010
9347
  "SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE to_id = ? OR to_name = (SELECT name FROM symbols WHERE id = ?)"
9011
9348
  ).all(symbolId, symbolId).map((r) => ({
9012
9349
  id: r.id,
@@ -9021,7 +9358,7 @@ var IndexStore = class _IndexStore {
9021
9358
  * Find all references FROM a given symbol (what does this symbol call/use?).
9022
9359
  */
9023
9360
  findRefsFrom(symbolId) {
9024
- return this.db.prepare("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
9361
+ return this.stmt("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
9025
9362
  id: r.id,
9026
9363
  fromId: r.from_id,
9027
9364
  toName: r.to_name,
@@ -9094,61 +9431,77 @@ var IndexStore = class _IndexStore {
9094
9431
  * symbol resolved in package B). Node metadata includes symbol/file counts.
9095
9432
  */
9096
9433
  getPackageGraph() {
9097
- const symbols = this.db.prepare("SELECT file, id FROM symbols ORDER BY id").all();
9434
+ const fileCounts = this.stmt(
9435
+ "SELECT file, COUNT(*) AS n FROM symbols GROUP BY file"
9436
+ ).all();
9098
9437
  const pkgNodes = /* @__PURE__ */ new Map();
9099
9438
  const fileToPkg = /* @__PURE__ */ new Map();
9100
- const symbolToPkg = /* @__PURE__ */ new Map();
9101
- for (const s of symbols) {
9102
- const pkg = _IndexStore.derivePackage(s.file) ?? "(root)";
9103
- fileToPkg.set(s.file, pkg);
9104
- symbolToPkg.set(s.id, pkg);
9439
+ for (const { file, n } of fileCounts) {
9440
+ const pkg = _IndexStore.derivePackage(file) ?? "(root)";
9441
+ fileToPkg.set(file, pkg);
9105
9442
  const node = pkgNodes.get(pkg);
9106
9443
  if (node) {
9107
- node.symbolCount = (node.symbolCount ?? 0) + 1;
9444
+ node.symbolCount = (node.symbolCount ?? 0) + n;
9108
9445
  } else {
9109
9446
  pkgNodes.set(pkg, {
9110
9447
  id: `pkg:${pkg}`,
9111
9448
  label: pkg,
9112
9449
  kind: "package",
9113
9450
  package: pkg,
9114
- symbolCount: 1,
9451
+ symbolCount: n,
9115
9452
  fileCount: 0
9116
9453
  });
9117
9454
  }
9118
9455
  }
9119
- const files = this.db.prepare("SELECT DISTINCT file FROM files").all();
9456
+ const files = this.stmt("SELECT DISTINCT file FROM files").all();
9120
9457
  for (const { file } of files) {
9121
9458
  const pkg = _IndexStore.derivePackage(file) ?? "(root)";
9459
+ fileToPkg.set(file, pkg);
9122
9460
  const node = pkgNodes.get(pkg);
9123
9461
  if (node) node.fileCount = (node.fileCount ?? 0) + 1;
9462
+ else {
9463
+ pkgNodes.set(pkg, {
9464
+ id: `pkg:${pkg}`,
9465
+ label: pkg,
9466
+ kind: "package",
9467
+ package: pkg,
9468
+ symbolCount: 0,
9469
+ fileCount: 1
9470
+ });
9471
+ }
9124
9472
  }
9125
- const refRows = this.db.prepare(
9126
- `SELECT r.from_id, r.to_id, r.call_type
9473
+ const refRows = this.stmt(
9474
+ `SELECT r.call_type, sf.file AS from_file, st.file AS to_file, COUNT(*) AS n
9127
9475
  FROM refs r
9128
- WHERE r.to_id IS NOT NULL`
9476
+ JOIN symbols sf ON sf.id = r.from_id
9477
+ JOIN symbols st ON st.id = r.to_id
9478
+ WHERE r.to_id IS NOT NULL AND r.call_type != 'import'
9479
+ GROUP BY r.call_type, sf.file, st.file`
9129
9480
  ).all();
9130
9481
  const edgeMap = /* @__PURE__ */ new Map();
9131
9482
  for (const r of refRows) {
9132
- if (r.call_type === "import") continue;
9133
- const fromPkg = symbolToPkg.get(r.from_id);
9134
- const toPkg = symbolToPkg.get(r.to_id);
9135
- if (!fromPkg || !toPkg || fromPkg === toPkg) continue;
9483
+ const fromPkg = fileToPkg.get(r.from_file) ?? _IndexStore.derivePackage(r.from_file) ?? "(root)";
9484
+ const toPkg = fileToPkg.get(r.to_file) ?? _IndexStore.derivePackage(r.to_file) ?? "(root)";
9485
+ if (fromPkg === toPkg) continue;
9136
9486
  const key = `${fromPkg}\0${toPkg}`;
9137
9487
  let e = edgeMap.get(key);
9138
9488
  if (!e) {
9139
9489
  e = { weight: 0, types: /* @__PURE__ */ new Map() };
9140
9490
  edgeMap.set(key, e);
9141
9491
  }
9142
- e.weight++;
9143
- e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
9492
+ const n = Number(r.n) || 0;
9493
+ e.weight += n;
9494
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + n);
9144
9495
  }
9145
- const importRows = this.db.prepare(
9146
- `SELECT r.from_id, r.to_name
9496
+ const importRows = this.stmt(
9497
+ `SELECT r.to_name, s.file AS from_file, COUNT(*) AS n
9147
9498
  FROM refs r
9148
- WHERE r.call_type = 'import'`
9499
+ JOIN symbols s ON s.id = r.from_id
9500
+ WHERE r.call_type = 'import'
9501
+ GROUP BY r.to_name, s.file`
9149
9502
  ).all();
9150
9503
  for (const r of importRows) {
9151
- const fromPkg = symbolToPkg.get(r.from_id);
9504
+ const fromPkg = fileToPkg.get(r.from_file) ?? _IndexStore.derivePackage(r.from_file) ?? "(root)";
9152
9505
  const toPkg = _IndexStore.packageFromImport(r.to_name);
9153
9506
  if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
9154
9507
  const key = `${fromPkg}\0${toPkg}`;
@@ -9157,8 +9510,9 @@ var IndexStore = class _IndexStore {
9157
9510
  edge = { weight: 0, types: /* @__PURE__ */ new Map() };
9158
9511
  edgeMap.set(key, edge);
9159
9512
  }
9160
- edge.weight++;
9161
- edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
9513
+ const n = Number(r.n) || 0;
9514
+ edge.weight += n;
9515
+ edge.types.set("import", (edge.types.get("import") ?? 0) + n);
9162
9516
  }
9163
9517
  const edges = [];
9164
9518
  for (const [key, e] of edgeMap) {
@@ -9185,12 +9539,12 @@ var IndexStore = class _IndexStore {
9185
9539
  * derived from cross-file symbol references within the package.
9186
9540
  */
9187
9541
  getFileGraph(packageFilter) {
9188
- const allFiles = this.db.prepare("SELECT DISTINCT file FROM symbols").all();
9542
+ const allFiles = this.stmt("SELECT DISTINCT file FROM symbols").all();
9189
9543
  const pkgFilePaths = allFiles.filter((f) => (_IndexStore.derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
9190
9544
  const localFiles = new Set(pkgFilePaths);
9191
9545
  if (localFiles.size === 0) return { nodes: [], edges: [] };
9192
9546
  const filePlaceholders = [...localFiles].map(() => "?").join(",");
9193
- const pkgSyms = this.db.prepare(
9547
+ const pkgSyms = this.stmt(
9194
9548
  `SELECT file, id, name, kind, lang, line FROM symbols WHERE file IN (${filePlaceholders}) ORDER BY id`
9195
9549
  ).all(...pkgFilePaths);
9196
9550
  const fileNodes = /* @__PURE__ */ new Map();
@@ -9222,12 +9576,13 @@ var IndexStore = class _IndexStore {
9222
9576
  ensureFileNode(file);
9223
9577
  }
9224
9578
  const indexedFiles = new Set(allFiles.map((f) => f.file));
9225
- const refRows = this.db.prepare(
9226
- `SELECT r.from_id, r.to_id, r.call_type
9579
+ const refRows = this.stmt(
9580
+ `SELECT r.from_id, r.to_id, r.call_type, COUNT(*) AS n
9227
9581
  FROM refs r
9228
9582
  WHERE (r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
9229
9583
  OR r.to_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders})))
9230
- AND r.to_id IS NOT NULL`
9584
+ AND r.to_id IS NOT NULL
9585
+ GROUP BY r.from_id, r.to_id, r.call_type`
9231
9586
  ).all(...pkgFilePaths, ...pkgFilePaths);
9232
9587
  const knownSymIds = new Set(pkgSyms.map((s) => s.id));
9233
9588
  const crossRefIds = /* @__PURE__ */ new Set();
@@ -9237,7 +9592,7 @@ var IndexStore = class _IndexStore {
9237
9592
  }
9238
9593
  if (crossRefIds.size > 0) {
9239
9594
  const crossPlaceholders = [...crossRefIds].map(() => "?").join(",");
9240
- const extras = this.db.prepare(
9595
+ const extras = this.stmt(
9241
9596
  `SELECT id, file FROM symbols WHERE id IN (${crossPlaceholders})`
9242
9597
  ).all(...crossRefIds);
9243
9598
  for (const x of extras) {
@@ -9262,14 +9617,16 @@ var IndexStore = class _IndexStore {
9262
9617
  e = { weight: 0, types: /* @__PURE__ */ new Map() };
9263
9618
  edgeMap.set(key, e);
9264
9619
  }
9265
- e.weight++;
9266
- e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
9620
+ const n = Number(r.n) || 0;
9621
+ e.weight += n;
9622
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + n);
9267
9623
  }
9268
- const importRows = this.db.prepare(
9269
- `SELECT r.from_id, r.to_name
9624
+ const importRows = this.stmt(
9625
+ `SELECT r.from_id, r.to_name, COUNT(*) AS n
9270
9626
  FROM refs r
9271
9627
  WHERE r.call_type = 'import'
9272
- AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))`
9628
+ AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
9629
+ GROUP BY r.from_id, r.to_name`
9273
9630
  ).all(...pkgFilePaths);
9274
9631
  for (const r of importRows) {
9275
9632
  const fromFile = symToFile.get(r.from_id);
@@ -9284,8 +9641,9 @@ var IndexStore = class _IndexStore {
9284
9641
  edge = { weight: 0, types: /* @__PURE__ */ new Map() };
9285
9642
  edgeMap.set(key, edge);
9286
9643
  }
9287
- edge.weight++;
9288
- edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
9644
+ const n = Number(r.n) || 0;
9645
+ edge.weight += n;
9646
+ edge.types.set("import", (edge.types.get("import") ?? 0) + n);
9289
9647
  }
9290
9648
  const edges = [];
9291
9649
  for (const [key, e] of edgeMap) {
@@ -9312,7 +9670,7 @@ var IndexStore = class _IndexStore {
9312
9670
  * derived from intra-file and cross-file symbol references (who calls whom).
9313
9671
  */
9314
9672
  getSymbolGraph(fileFilter) {
9315
- const syms = this.db.prepare(
9673
+ const syms = this.stmt(
9316
9674
  "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE file = ? ORDER BY line, id"
9317
9675
  ).all(fileFilter);
9318
9676
  if (syms.length === 0) return { nodes: [], edges: [] };
@@ -9332,17 +9690,25 @@ var IndexStore = class _IndexStore {
9332
9690
  scope: s.scope,
9333
9691
  external: s.file !== fileFilter
9334
9692
  });
9335
- const symIds = new Set(syms.map((s) => s.id));
9336
- const refRows = this.db.prepare(
9337
- `SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
9338
- FROM refs r
9339
- WHERE r.from_id IN (SELECT id FROM symbols WHERE file = ?)
9340
- OR r.to_id IN (SELECT id FROM symbols WHERE file = ?)`
9693
+ const refRows = this.stmt(
9694
+ `SELECT from_id, to_id, call_type, COUNT(*) AS n
9695
+ FROM (
9696
+ SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
9697
+ FROM refs r
9698
+ JOIN symbols s ON s.id = r.from_id
9699
+ WHERE s.file = ?
9700
+ UNION
9701
+ SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
9702
+ FROM refs r
9703
+ JOIN symbols s ON s.id = r.to_id
9704
+ WHERE s.file = ?
9705
+ )
9706
+ WHERE to_id IS NOT NULL
9707
+ GROUP BY from_id, to_id, call_type`
9341
9708
  ).all(fileFilter, fileFilter);
9342
9709
  const edgeMap = /* @__PURE__ */ new Map();
9343
9710
  for (const r of refRows) {
9344
- if (r.to_id === null) continue;
9345
- if (!symIds.has(r.from_id) && !symIds.has(r.to_id)) continue;
9711
+ if (r.to_id == null) continue;
9346
9712
  relatedIds.add(r.from_id);
9347
9713
  relatedIds.add(r.to_id);
9348
9714
  const key = `${r.from_id}\0${r.to_id}`;
@@ -9351,8 +9717,9 @@ var IndexStore = class _IndexStore {
9351
9717
  e = { weight: 0, types: /* @__PURE__ */ new Map() };
9352
9718
  edgeMap.set(key, e);
9353
9719
  }
9354
- e.weight++;
9355
- e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
9720
+ const n = Number(r.n) || 0;
9721
+ e.weight += n;
9722
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + n);
9356
9723
  }
9357
9724
  const edges = [];
9358
9725
  for (const [key, e] of edgeMap) {
@@ -9376,7 +9743,7 @@ var IndexStore = class _IndexStore {
9376
9743
  const missingIds = [...relatedIds].filter((id) => !loadedIds.has(id));
9377
9744
  if (missingIds.length > 0) {
9378
9745
  const placeholders = missingIds.map(() => "?").join(",");
9379
- const extras = this.db.prepare(
9746
+ const extras = this.stmt(
9380
9747
  `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders})`
9381
9748
  ).all(...missingIds);
9382
9749
  for (const s of extras) symById.set(s.id, s);
@@ -9390,6 +9757,8 @@ var IndexStore = class _IndexStore {
9390
9757
  }
9391
9758
  close() {
9392
9759
  this.stmtCache.clear();
9760
+ this.bm25Dirty = true;
9761
+ this.bm25Cache = null;
9393
9762
  try {
9394
9763
  this.db.close();
9395
9764
  } catch {
@@ -9403,14 +9772,121 @@ import { fileURLToPath } from "node:url";
9403
9772
  import { Worker } from "node:worker_threads";
9404
9773
 
9405
9774
  // src/codebase-index/indexer.ts
9406
- import { expectDefined as expectDefined6 } from "@wrongstack/core";
9775
+ import { expectDefined as expectDefined6 } from "@wrongstack/core/utils";
9407
9776
  import * as fs14 from "node:fs/promises";
9408
- import * as path19 from "node:path";
9777
+ import * as path20 from "node:path";
9409
9778
  import { availableParallelism } from "node:os";
9410
- import { compileGlob as compileGlob2 } from "@wrongstack/core";
9779
+ import { DEFAULT_WALK_IGNORE_DIRS, indexParallelBatchSize, isFrugalPerf } from "@wrongstack/core/utils";
9411
9780
 
9412
9781
  // src/codebase-index/ts-parser.ts
9413
9782
  import * as ts from "@typescript/typescript6";
9783
+
9784
+ // src/codebase-index/languages.ts
9785
+ import * as path14 from "node:path";
9786
+ var EXT_TO_LANG = {
9787
+ // TypeScript / JavaScript (first-class TS compiler API)
9788
+ ".ts": "ts",
9789
+ ".mts": "ts",
9790
+ ".cts": "ts",
9791
+ ".tsx": "tsx",
9792
+ ".js": "js",
9793
+ ".mjs": "js",
9794
+ ".cjs": "js",
9795
+ ".jsx": "jsx",
9796
+ // First-class native/spawn parsers (+ regex fallback)
9797
+ ".go": "go",
9798
+ ".py": "py",
9799
+ ".pyi": "py",
9800
+ ".pyw": "py",
9801
+ ".rs": "rs",
9802
+ ".json": "json",
9803
+ ".jsonc": "json",
9804
+ ".yaml": "yaml",
9805
+ ".yml": "yaml",
9806
+ // C family
9807
+ ".c": "c",
9808
+ ".h": "c",
9809
+ ".cc": "cpp",
9810
+ ".cpp": "cpp",
9811
+ ".cxx": "cpp",
9812
+ ".hh": "cpp",
9813
+ ".hpp": "cpp",
9814
+ ".hxx": "cpp",
9815
+ // JVM / .NET
9816
+ ".java": "java",
9817
+ ".cs": "csharp",
9818
+ ".kt": "kotlin",
9819
+ ".kts": "kotlin",
9820
+ ".scala": "scala",
9821
+ ".sc": "scala",
9822
+ // Scripting
9823
+ ".php": "php",
9824
+ ".rb": "ruby",
9825
+ ".swift": "swift",
9826
+ ".dart": "dart",
9827
+ ".lua": "lua",
9828
+ ".r": "r",
9829
+ ".R": "r",
9830
+ ".pl": "other",
9831
+ ".pm": "other",
9832
+ // Systems / functional
9833
+ ".zig": "zig",
9834
+ ".ex": "elixir",
9835
+ ".exs": "elixir",
9836
+ ".hs": "haskell",
9837
+ ".lhs": "haskell",
9838
+ // Shell / data / docs / web
9839
+ ".sh": "shell",
9840
+ ".bash": "shell",
9841
+ ".zsh": "shell",
9842
+ ".ps1": "shell",
9843
+ ".sql": "sql",
9844
+ ".md": "md",
9845
+ ".mdx": "md",
9846
+ ".toml": "toml",
9847
+ ".html": "html",
9848
+ ".htm": "html",
9849
+ ".css": "css",
9850
+ ".scss": "css",
9851
+ ".less": "css",
9852
+ ".vue": "vue",
9853
+ ".svelte": "svelte",
9854
+ ".proto": "proto",
9855
+ ".graphql": "graphql",
9856
+ ".gql": "graphql"
9857
+ };
9858
+ var INDEXABLE_EXTENSIONS = Object.freeze(
9859
+ [...new Set(Object.keys(EXT_TO_LANG).map((e) => e.toLowerCase()))].sort()
9860
+ );
9861
+ var SPECIAL_FILENAMES = {
9862
+ makefile: "other",
9863
+ gnumakefile: "other",
9864
+ dockerfile: "other",
9865
+ "docker-compose.yml": "yaml",
9866
+ "docker-compose.yaml": "yaml",
9867
+ "cmakelists.txt": "other",
9868
+ gemfile: "ruby",
9869
+ rakefile: "ruby",
9870
+ procfile: "other",
9871
+ justfile: "other"
9872
+ };
9873
+ function detectLang(file) {
9874
+ const base = path14.basename(file);
9875
+ const lowerBase = base.toLowerCase();
9876
+ if (lowerBase.endsWith(".d.ts") || lowerBase.endsWith(".d.mts") || lowerBase.endsWith(".d.cts")) {
9877
+ return "ts";
9878
+ }
9879
+ const special = SPECIAL_FILENAMES[lowerBase];
9880
+ if (special) return special;
9881
+ const ext = path14.extname(base).toLowerCase();
9882
+ if (!ext) return null;
9883
+ return EXT_TO_LANG[ext] ?? null;
9884
+ }
9885
+ function isIndexablePath(file) {
9886
+ return detectLang(file) !== null;
9887
+ }
9888
+
9889
+ // src/codebase-index/ts-parser.ts
9414
9890
  var KIND_MAP = {
9415
9891
  [ts.SyntaxKind.ClassDeclaration]: "class",
9416
9892
  [ts.SyntaxKind.InterfaceDeclaration]: "interface",
@@ -9437,32 +9913,6 @@ function kindOf(node) {
9437
9913
  if (ts.isModuleDeclaration(node)) return "namespace";
9438
9914
  return KIND_MAP[node.kind] ?? null;
9439
9915
  }
9440
- function extToLang(ext) {
9441
- switch (ext) {
9442
- case ".ts":
9443
- return "ts";
9444
- case ".tsx":
9445
- return "tsx";
9446
- case ".js":
9447
- return "js";
9448
- case ".jsx":
9449
- return "jsx";
9450
- case ".go":
9451
- return "go";
9452
- case ".py":
9453
- return "py";
9454
- case ".rs":
9455
- return "rs";
9456
- case ".json":
9457
- return "json";
9458
- case ".yaml":
9459
- return "yaml";
9460
- case ".yml":
9461
- return "yaml";
9462
- default:
9463
- return null;
9464
- }
9465
- }
9466
9916
  function getSignature(printer, node, sourceFile) {
9467
9917
  const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
9468
9918
  return raw.replace(/\s+/g, " ").slice(0, 500);
@@ -9585,22 +10035,30 @@ function deduplicateRefs(refs) {
9585
10035
  return true;
9586
10036
  });
9587
10037
  }
9588
- function detectLang(file) {
9589
- const idx = file.lastIndexOf(".");
9590
- if (idx < 0) return null;
9591
- return extToLang(file.slice(idx));
9592
- }
9593
10038
 
9594
10039
  // src/codebase-index/go-parser.ts
9595
10040
  init_win32_resolve();
9596
10041
  import { spawn as spawn4 } from "node:child_process";
9597
10042
  import * as os5 from "node:os";
9598
- import * as path14 from "node:path";
10043
+ import * as path15 from "node:path";
9599
10044
  import * as fs10 from "node:fs/promises";
10045
+
10046
+ // src/codebase-index/spawn-gate.ts
10047
+ var chain = Promise.resolve();
10048
+ function withSpawnGate(fn) {
10049
+ const run = chain.then(fn, fn);
10050
+ chain = run.then(
10051
+ () => void 0,
10052
+ () => void 0
10053
+ );
10054
+ return run;
10055
+ }
10056
+
10057
+ // src/codebase-index/go-parser.ts
9600
10058
  async function parseSymbols2(opts) {
9601
10059
  const { file, content, lang } = opts;
9602
10060
  try {
9603
- const parsed = await syncGoParse(file, content, lang);
10061
+ const parsed = await withSpawnGate(() => syncGoParse(file, content, lang));
9604
10062
  if (parsed.symbols.length > 0) {
9605
10063
  return parsed;
9606
10064
  }
@@ -9896,11 +10354,16 @@ func formatType(t ast.Expr) string {
9896
10354
  }
9897
10355
  }
9898
10356
  `;
10357
+ var _cachedGoScriptPath = null;
9899
10358
  async function syncGoParse(filePath, content, lang) {
9900
- const tmpDir = await fs10.mkdtemp(path14.join(os5.tmpdir(), "ws-go-parse-"));
9901
10359
  try {
9902
- const scriptPath = path14.join(tmpDir, "parse.go");
9903
- await fs10.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
10360
+ let scriptPath = _cachedGoScriptPath;
10361
+ if (!scriptPath) {
10362
+ const tmpDir = await fs10.mkdtemp(path15.join(os5.tmpdir(), "ws-go-parse-"));
10363
+ scriptPath = path15.join(tmpDir, "parse.go");
10364
+ await fs10.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
10365
+ _cachedGoScriptPath = scriptPath;
10366
+ }
9904
10367
  const goBinary = resolveWin32Command("go");
9905
10368
  const goResult = await new Promise(
9906
10369
  (resolve15, reject) => {
@@ -9957,24 +10420,312 @@ async function syncGoParse(filePath, content, lang) {
9957
10420
  return { file: filePath, lang, symbols, mtimeMs: Date.now() };
9958
10421
  } catch {
9959
10422
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
9960
- } finally {
9961
- await fs10.rm(tmpDir, { recursive: true, force: true });
9962
10423
  }
9963
10424
  }
9964
10425
 
9965
10426
  // src/codebase-index/py-parser.ts
9966
10427
  init_win32_resolve();
9967
- import { spawn as spawn5, spawnSync } from "node:child_process";
10428
+ import { spawn as spawn5 } from "node:child_process";
9968
10429
  import * as fs11 from "node:fs/promises";
9969
10430
  import * as os6 from "node:os";
9970
- import * as path15 from "node:path";
10431
+ import * as path16 from "node:path";
10432
+
10433
+ // src/codebase-index/generic-parser.ts
10434
+ var C_LIKE = [
10435
+ { re: /\b(?:class|struct|enum|interface|union)\s+([A-Za-z_]\w*)/g, kind: "class" },
10436
+ {
10437
+ re: /\b(?:public|private|protected|static|final|async|override|virtual|inline|export)?\s*(?:[\w:<>[\]\s*&]+)\s+([A-Za-z_]\w*)\s*\([^;{]*\)\s*(?:const)?\s*[{;]/g,
10438
+ kind: "function"
10439
+ },
10440
+ { re: /\b(?:namespace)\s+([A-Za-z_]\w*)/g, kind: "namespace" }
10441
+ ];
10442
+ var LANG_PATTERNS = {
10443
+ py: [
10444
+ { re: /^(?:async\s+)?def\s+([A-Za-z_]\w*)/gm, kind: "function" },
10445
+ { re: /^class\s+([A-Za-z_]\w*)/gm, kind: "class" },
10446
+ { re: /^([A-Za-z_]\w*)\s*=/gm, kind: "var" }
10447
+ ],
10448
+ go: [
10449
+ { re: /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(/gm, kind: "function" },
10450
+ { re: /^type\s+([A-Za-z_]\w*)\b/gm, kind: "type" },
10451
+ { re: /^(?:const|var)\s+([A-Za-z_]\w*)\b/gm, kind: "const" },
10452
+ { re: /^package\s+([A-Za-z_]\w*)/gm, kind: "namespace" }
10453
+ ],
10454
+ rs: [
10455
+ { re: /\bfn\s+([A-Za-z_]\w*)/g, kind: "function" },
10456
+ { re: /\bstruct\s+([A-Za-z_]\w*)/g, kind: "struct" },
10457
+ { re: /\benum\s+([A-Za-z_]\w*)/g, kind: "enum" },
10458
+ { re: /\btrait\s+([A-Za-z_]\w*)/g, kind: "trait" },
10459
+ { re: /\bimpl(?:\s*<[^>]+>)?\s+([A-Za-z_]\w*)/g, kind: "impl" },
10460
+ { re: /\b(?:const|static)\s+([A-Za-z_]\w*)/g, kind: "const" },
10461
+ { re: /\bmod\s+([A-Za-z_]\w*)/g, kind: "mod" }
10462
+ ],
10463
+ c: C_LIKE,
10464
+ cpp: C_LIKE,
10465
+ java: [
10466
+ { re: /\b(?:class|interface|enum|record)\s+([A-Za-z_]\w*)/g, kind: "class" },
10467
+ {
10468
+ re: /\b(?:public|private|protected|static|final|abstract|synchronized|native|default|\s)+\s*[\w.<>,[\]\s]+\s+([A-Za-z_]\w*)\s*\(/g,
10469
+ kind: "method"
10470
+ }
10471
+ ],
10472
+ csharp: [
10473
+ { re: /\b(?:class|interface|struct|enum|record)\s+([A-Za-z_]\w*)/g, kind: "class" },
10474
+ { re: /\bnamespace\s+([A-Za-z_.\w]+)/g, kind: "namespace" },
10475
+ {
10476
+ re: /\b(?:public|private|protected|internal|static|async|override|virtual|\s)+\s*[\w.<>,[\]\s]+\s+([A-Za-z_]\w*)\s*\(/g,
10477
+ kind: "method"
10478
+ }
10479
+ ],
10480
+ php: [
10481
+ { re: /\bfunction\s+([A-Za-z_]\w*)/g, kind: "function" },
10482
+ { re: /\bclass\s+([A-Za-z_]\w*)/g, kind: "class" },
10483
+ { re: /\binterface\s+([A-Za-z_]\w*)/g, kind: "interface" },
10484
+ { re: /\bnamespace\s+([A-Za-z_\\]+)/g, kind: "namespace" }
10485
+ ],
10486
+ ruby: [
10487
+ { re: /^\s*def\s+(?:self\.)?([A-Za-z_]\w*[!?]?)/gm, kind: "function" },
10488
+ { re: /^\s*class\s+([A-Za-z_]\w*)/gm, kind: "class" },
10489
+ { re: /^\s*module\s+([A-Za-z_]\w*)/gm, kind: "namespace" }
10490
+ ],
10491
+ swift: [
10492
+ { re: /\b(?:func)\s+([A-Za-z_]\w*)/g, kind: "function" },
10493
+ { re: /\b(?:class|struct|enum|protocol|actor)\s+([A-Za-z_]\w*)/g, kind: "class" }
10494
+ ],
10495
+ kotlin: [
10496
+ { re: /\b(?:fun)\s+([A-Za-z_]\w*)/g, kind: "function" },
10497
+ { re: /\b(?:class|interface|object|enum\s+class|data\s+class)\s+([A-Za-z_]\w*)/g, kind: "class" }
10498
+ ],
10499
+ scala: [
10500
+ { re: /\b(?:def)\s+([A-Za-z_]\w*)/g, kind: "function" },
10501
+ { re: /\b(?:class|object|trait|enum)\s+([A-Za-z_]\w*)/g, kind: "class" }
10502
+ ],
10503
+ shell: [
10504
+ { re: /^(?:function\s+)?([A-Za-z_]\w*)\s*\(\)\s*\{/gm, kind: "function" },
10505
+ { re: /^([A-Za-z_][\w]*)\s*\(\)\s*\{/gm, kind: "function" }
10506
+ ],
10507
+ sql: [
10508
+ { re: /\bCREATE\s+(?:OR\s+REPLACE\s+)?(?:TABLE|VIEW|INDEX|FUNCTION|PROCEDURE|TRIGGER)\s+(?:IF\s+NOT\s+EXISTS\s+)?([A-Za-z_"][\w."]*)/gi, kind: "type" }
10509
+ ],
10510
+ md: [
10511
+ { re: /^(#{1,6})\s+(.+)$/gm, kind: "namespace" }
10512
+ ],
10513
+ toml: [
10514
+ { re: /^\[([^\]]+)\]/gm, kind: "namespace" }
10515
+ ],
10516
+ html: [
10517
+ { re: /\bid\s*=\s*["']([^"']+)["']/gi, kind: "property" },
10518
+ { re: /<(?:script|template|style)\b/gi, kind: "namespace" }
10519
+ ],
10520
+ css: [
10521
+ { re: /^\s*([.#]?[A-Za-z_][\w-]*)\s*\{/gm, kind: "type" },
10522
+ { re: /@(?:keyframes|media|supports)\s+([^{\s]+)/g, kind: "namespace" }
10523
+ ],
10524
+ vue: [
10525
+ { re: /\b(?:function|const|let|var|class|export\s+(?:default\s+)?(?:function|class|const))\s+([A-Za-z_]\w*)/g, kind: "function" },
10526
+ { re: /<(?:script|template|style)\b/gi, kind: "namespace" }
10527
+ ],
10528
+ svelte: [
10529
+ { re: /\b(?:function|const|let|var|class|export\s+(?:default\s+)?(?:function|class|const))\s+([A-Za-z_]\w*)/g, kind: "function" }
10530
+ ],
10531
+ dart: [
10532
+ { re: /\b(?:class|enum|mixin|extension)\s+([A-Za-z_]\w*)/g, kind: "class" },
10533
+ { re: /\b([A-Za-z_]\w*)\s*\([^;]*\)\s*(?:async\s*)?\{/g, kind: "function" }
10534
+ ],
10535
+ lua: [
10536
+ { re: /\bfunction\s+([A-Za-z_.:]\w*)/g, kind: "function" },
10537
+ { re: /\blocal\s+function\s+([A-Za-z_]\w*)/g, kind: "function" }
10538
+ ],
10539
+ r: [
10540
+ { re: /([A-Za-z.]\w*)\s*<-\s*function\s*\(/g, kind: "function" },
10541
+ { re: /([A-Za-z.]\w*)\s*=\s*function\s*\(/g, kind: "function" }
10542
+ ],
10543
+ proto: [
10544
+ { re: /\b(?:message|service|enum)\s+([A-Za-z_]\w*)/g, kind: "type" },
10545
+ { re: /\brpc\s+([A-Za-z_]\w*)/g, kind: "function" }
10546
+ ],
10547
+ graphql: [
10548
+ { re: /\b(?:type|interface|enum|input|union|scalar)\s+([A-Za-z_]\w*)/g, kind: "type" },
10549
+ { re: /\b(?:query|mutation|subscription)\s+([A-Za-z_]\w*)/g, kind: "function" }
10550
+ ],
10551
+ zig: [
10552
+ { re: /\b(?:fn|pub\s+fn)\s+([A-Za-z_]\w*)/g, kind: "function" },
10553
+ { re: /\b(?:const|var)\s+([A-Za-z_]\w*)/g, kind: "const" },
10554
+ { re: /\b(?:struct|enum|union)\s*\{/g, kind: "type" }
10555
+ ],
10556
+ elixir: [
10557
+ { re: /\bdef(?:p|macro|macrop)?\s+([A-Za-z_]\w*[!?]?)/g, kind: "function" },
10558
+ { re: /\bdefmodule\s+([A-Za-z_.]\w*)/g, kind: "namespace" }
10559
+ ],
10560
+ haskell: [
10561
+ { re: /^([A-Za-z_]\w*)\s*::/gm, kind: "function" },
10562
+ { re: /\bdata\s+([A-Za-z_]\w*)/g, kind: "type" },
10563
+ { re: /\btype\s+(?:family\s+)?([A-Za-z_]\w*)/g, kind: "type" },
10564
+ { re: /\bclass\s+([A-Za-z_]\w*)/g, kind: "class" }
10565
+ ],
10566
+ other: [
10567
+ { re: /^(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_]\w*)/gm, kind: "function" },
10568
+ { re: /^(?:export\s+)?class\s+([A-Za-z_]\w*)/gm, kind: "class" },
10569
+ { re: /^(?:export\s+)?(?:const|let|var)\s+([A-Za-z_]\w*)/gm, kind: "const" },
10570
+ { re: /^(?:async\s+)?def\s+([A-Za-z_]\w*)/gm, kind: "function" },
10571
+ { re: /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)/gm, kind: "function" },
10572
+ { re: /\bfn\s+([A-Za-z_]\w*)/g, kind: "function" },
10573
+ { re: /^(?:target|PHONY)\s*:/gm, kind: "namespace" }
10574
+ ]
10575
+ };
10576
+ var KEYWORDS = /* @__PURE__ */ new Set([
10577
+ "if",
10578
+ "else",
10579
+ "for",
10580
+ "while",
10581
+ "switch",
10582
+ "case",
10583
+ "return",
10584
+ "break",
10585
+ "continue",
10586
+ "new",
10587
+ "delete",
10588
+ "typeof",
10589
+ "instanceof",
10590
+ "void",
10591
+ "null",
10592
+ "true",
10593
+ "false",
10594
+ "this",
10595
+ "super",
10596
+ "import",
10597
+ "export",
10598
+ "from",
10599
+ "as",
10600
+ "default",
10601
+ "public",
10602
+ "private",
10603
+ "protected",
10604
+ "static",
10605
+ "final",
10606
+ "class",
10607
+ "struct",
10608
+ "enum",
10609
+ "interface",
10610
+ "function",
10611
+ "def",
10612
+ "fn",
10613
+ "func",
10614
+ "var",
10615
+ "let",
10616
+ "const",
10617
+ "package",
10618
+ "namespace",
10619
+ "module",
10620
+ "using",
10621
+ "include",
10622
+ "require",
10623
+ "select",
10624
+ "from",
10625
+ "where",
10626
+ "and",
10627
+ "or",
10628
+ "not",
10629
+ "in",
10630
+ "is",
10631
+ "try",
10632
+ "catch",
10633
+ "finally",
10634
+ "throw",
10635
+ "async",
10636
+ "await",
10637
+ "yield"
10638
+ ]);
10639
+ function patternsFor(lang) {
10640
+ return LANG_PATTERNS[lang] ?? LANG_PATTERNS.other ?? [];
10641
+ }
10642
+ function looksBinary(content) {
10643
+ if (content.includes("\0")) return true;
10644
+ const sample = content.slice(0, 2048);
10645
+ if (sample.length === 0) return false;
10646
+ let bad = 0;
10647
+ for (let i = 0; i < sample.length; i++) {
10648
+ const c = sample.charCodeAt(i);
10649
+ if (c === 9 || c === 10 || c === 13) continue;
10650
+ if (c < 32 || c === 127) bad++;
10651
+ }
10652
+ return bad / sample.length > 0.1;
10653
+ }
10654
+ function lineColAt(content, index) {
10655
+ let line = 1;
10656
+ let lastNl = -1;
10657
+ for (let i = 0; i < index && i < content.length; i++) {
10658
+ if (content.charCodeAt(i) === 10) {
10659
+ line++;
10660
+ lastNl = i;
10661
+ }
10662
+ }
10663
+ return { line, col: index - lastNl };
10664
+ }
10665
+ var GENERIC_MAX_SYMBOLS_DEFAULT = 500;
10666
+ var GENERIC_MAX_FILE_CHARS = 512 * 1024;
10667
+ function parseGeneric2(opts) {
10668
+ const { file, lang } = opts;
10669
+ const maxSymbols = opts.maxSymbols ?? GENERIC_MAX_SYMBOLS_DEFAULT;
10670
+ const mtimeMs = Date.now();
10671
+ if (!opts.content || looksBinary(opts.content)) {
10672
+ return { file, lang, symbols: [], mtimeMs };
10673
+ }
10674
+ const content = opts.content.length > GENERIC_MAX_FILE_CHARS ? opts.content.slice(0, GENERIC_MAX_FILE_CHARS) : opts.content;
10675
+ const patterns = patternsFor(lang);
10676
+ const symbols = [];
10677
+ const seen = /* @__PURE__ */ new Set();
10678
+ for (const pattern of patterns) {
10679
+ const re = new RegExp(pattern.re.source, pattern.re.flags.includes("g") ? pattern.re.flags : `${pattern.re.flags}g`);
10680
+ re.lastIndex = 0;
10681
+ for (const match of content.matchAll(re)) {
10682
+ if (symbols.length >= maxSymbols) break;
10683
+ let name = (match[1] ?? match[2] ?? "").trim();
10684
+ if (lang === "md" && match[2]) name = match[2].trim();
10685
+ if (!name || name.length > 200) continue;
10686
+ name = name.replace(/^#+\s*/, "").replace(/["'`]/g, "");
10687
+ if (!name || KEYWORDS.has(name.toLowerCase())) continue;
10688
+ if (!/^[A-Za-z_#.@/\w][\w.\-:/#!?]*$/.test(name) && lang !== "md" && lang !== "toml") {
10689
+ continue;
10690
+ }
10691
+ const { line, col } = lineColAt(content, match.index);
10692
+ const key = `${name}\0${line}\0${pattern.kind}`;
10693
+ if (seen.has(key)) continue;
10694
+ seen.add(key);
10695
+ const nl = content.indexOf("\n", match.index);
10696
+ const lineText = content.slice(match.index, nl === -1 ? content.length : nl);
10697
+ const signature = (lineText || name).trim().slice(0, 500);
10698
+ symbols.push({
10699
+ id: 0,
10700
+ lang,
10701
+ kind: lang === "md" ? "namespace" : pattern.kind,
10702
+ name: name.slice(0, 200),
10703
+ file,
10704
+ line,
10705
+ col,
10706
+ signature,
10707
+ docComment: "",
10708
+ scope: "",
10709
+ text: `${name} ${signature}`.trim().slice(0, 1e3)
10710
+ });
10711
+ }
10712
+ if (symbols.length >= maxSymbols) break;
10713
+ }
10714
+ return { file, lang, symbols, mtimeMs };
10715
+ }
9971
10716
  async function parseSymbols3(opts) {
10717
+ return parseGeneric2(opts);
10718
+ }
10719
+
10720
+ // src/codebase-index/py-parser.ts
10721
+ async function parseSymbols4(opts) {
9972
10722
  const { file, content, lang } = opts;
9973
10723
  try {
9974
- return await syncPyParse(file, content, lang);
10724
+ const native = await withSpawnGate(() => syncPyParse(file, content, lang));
10725
+ if (native !== null) return native;
9975
10726
  } catch {
9976
- return { file, lang, symbols: [], mtimeMs: Date.now() };
9977
10727
  }
10728
+ return parseGeneric2({ file, content, lang: lang === "py" ? "py" : lang });
9978
10729
  }
9979
10730
  var PY_PARSE_SCRIPT = `import ast, json, sys, os
9980
10731
 
@@ -10179,20 +10930,37 @@ visitor.visit(tree)
10179
10930
 
10180
10931
  print(json.dumps([s.to_dict() for s in syms]))
10181
10932
  `;
10182
- function resolvePython() {
10933
+ async function resolvePython() {
10183
10934
  const candidates = process.platform === "win32" ? ["python3", "python", "py"] : ["python3", "python"];
10184
10935
  for (const name of candidates) {
10185
10936
  const resolved = resolveWin32Command(name);
10186
- const result = spawnSync(resolved, ["--version"], {
10187
- stdio: "pipe",
10188
- timeout: 5e3
10189
- });
10190
- if (result.error) continue;
10191
- if (result.status !== 0) continue;
10937
+ if (!await commandIsAvailable(resolved)) continue;
10192
10938
  return resolved;
10193
10939
  }
10194
10940
  return null;
10195
10941
  }
10942
+ function commandIsAvailable(command) {
10943
+ return new Promise((resolve15) => {
10944
+ let settled = false;
10945
+ const proc = spawn5(command, ["--version"], {
10946
+ stdio: "ignore",
10947
+ windowsHide: true
10948
+ });
10949
+ const finish = (available) => {
10950
+ if (settled) return;
10951
+ settled = true;
10952
+ clearTimeout(timer);
10953
+ resolve15(available);
10954
+ };
10955
+ const timer = setTimeout(() => {
10956
+ proc.kill("SIGKILL");
10957
+ finish(false);
10958
+ }, 5e3);
10959
+ timer.unref?.();
10960
+ proc.once("error", () => finish(false));
10961
+ proc.once("close", (code) => finish(code === 0));
10962
+ });
10963
+ }
10196
10964
  function spawnPyParser(pyBinary, scriptPath, filePath, content) {
10197
10965
  return new Promise((resolve15, reject) => {
10198
10966
  let settled = false;
@@ -10228,21 +10996,20 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
10228
10996
  });
10229
10997
  }
10230
10998
  var _cachedScriptPath = null;
10231
- var _cachedPyBinary = null;
10999
+ var cachedPyBinary;
10232
11000
  async function syncPyParse(filePath, content, lang) {
10233
11001
  try {
10234
11002
  if (!_cachedScriptPath) {
10235
- const tmpDir = path15.join(os6.tmpdir(), "ws-py-parse");
11003
+ const tmpDir = path16.join(os6.tmpdir(), "ws-py-parse");
10236
11004
  await fs11.mkdir(tmpDir, { recursive: true });
10237
- _cachedScriptPath = path15.join(tmpDir, "parse.py");
11005
+ _cachedScriptPath = path16.join(tmpDir, "parse.py");
10238
11006
  await fs11.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
10239
11007
  }
10240
- if (!_cachedPyBinary) {
10241
- _cachedPyBinary = resolvePython();
10242
- if (!_cachedPyBinary) return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
10243
- }
11008
+ cachedPyBinary ??= resolvePython();
11009
+ const pyBinary = await cachedPyBinary;
11010
+ if (!pyBinary) return null;
10244
11011
  const { code, stdout } = await spawnPyParser(
10245
- _cachedPyBinary,
11012
+ pyBinary,
10246
11013
  _cachedScriptPath,
10247
11014
  filePath,
10248
11015
  content
@@ -10266,31 +11033,40 @@ async function syncPyParse(filePath, content, lang) {
10266
11033
  }));
10267
11034
  return { file: filePath, lang, symbols, mtimeMs: Date.now() };
10268
11035
  } catch {
10269
- return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
11036
+ return null;
10270
11037
  }
10271
11038
  }
10272
11039
 
10273
11040
  // src/codebase-index/rs-parser.ts
10274
11041
  init_win32_resolve();
10275
- import { expectDefined as expectDefined3 } from "@wrongstack/core";
10276
- import { execFileSync, spawn as spawn6 } from "node:child_process";
11042
+ import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
11043
+ import { execFile, spawn as spawn6 } from "node:child_process";
10277
11044
  import * as fs12 from "node:fs/promises";
10278
- import * as path16 from "node:path";
10279
- async function parseSymbols4(opts) {
11045
+ import * as path17 from "node:path";
11046
+ async function parseSymbols5(opts) {
10280
11047
  const { file, content, lang } = opts;
10281
- const nativeAvailable = checkNativeParser();
11048
+ const nativeAvailable = await checkNativeParser();
10282
11049
  if (nativeAvailable) {
10283
- const result = await tryNativeParse(file, content);
11050
+ const result = await withSpawnGate(() => tryNativeParse(file, content));
10284
11051
  if (result) return result;
10285
11052
  }
10286
11053
  return regexParse({ file, content, lang });
10287
11054
  }
11055
+ var nativeParserAvailability;
11056
+ function probe(command, args) {
11057
+ return new Promise((resolve15, reject) => {
11058
+ execFile(command, args, { timeout: 1e4, windowsHide: true }, (error) => {
11059
+ if (error) reject(error);
11060
+ else resolve15();
11061
+ });
11062
+ });
11063
+ }
10288
11064
  function checkNativeParser() {
10289
- try {
10290
- execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
10291
- const toolsDir = path16.join(process.cwd(), "tools");
11065
+ nativeParserAvailability ??= (async () => {
10292
11066
  try {
10293
- execFileSync(
11067
+ await probe("rustc", ["--version"]);
11068
+ const toolsDir = path17.join(process.cwd(), "tools");
11069
+ await probe(
10294
11070
  "cargo",
10295
11071
  [
10296
11072
  "metadata",
@@ -10298,23 +11074,21 @@ function checkNativeParser() {
10298
11074
  "--format-version",
10299
11075
  "1",
10300
11076
  "--manifest-path",
10301
- path16.join(toolsDir, "Cargo.toml")
10302
- ],
10303
- { stdio: "pipe", windowsHide: true }
11077
+ path17.join(toolsDir, "Cargo.toml")
11078
+ ]
10304
11079
  );
10305
11080
  return true;
10306
11081
  } catch {
10307
11082
  return false;
10308
11083
  }
10309
- } catch {
10310
- return false;
10311
- }
11084
+ })();
11085
+ return nativeParserAvailability;
10312
11086
  }
10313
11087
  async function tryNativeParse(file, content) {
10314
11088
  try {
10315
- const toolsDir = path16.join(process.cwd(), "tools");
10316
- const crateDir = path16.join(toolsDir, "syn-parser");
10317
- const tmpFile = path16.join(crateDir, "src", "input.rs");
11089
+ const toolsDir = path17.join(process.cwd(), "tools");
11090
+ const crateDir = path17.join(toolsDir, "syn-parser");
11091
+ const tmpFile = path17.join(crateDir, "src", "input.rs");
10318
11092
  await fs12.writeFile(tmpFile, content, "utf8");
10319
11093
  const cargoBinary = resolveWin32Command("cargo");
10320
11094
  const result = await new Promise(
@@ -10322,7 +11096,7 @@ async function tryNativeParse(file, content) {
10322
11096
  let settled = false;
10323
11097
  const proc = spawn6(
10324
11098
  cargoBinary,
10325
- ["run", "--manifest-path", path16.join(toolsDir, "Cargo.toml")],
11099
+ ["run", "--manifest-path", path17.join(toolsDir, "Cargo.toml")],
10326
11100
  {
10327
11101
  cwd: process.cwd(),
10328
11102
  stdio: ["pipe", "pipe", "pipe"],
@@ -10436,9 +11210,9 @@ function regexParse(opts) {
10436
11210
  }
10437
11211
 
10438
11212
  // src/codebase-index/json-parser.ts
10439
- import { expectDefined as expectDefined4 } from "@wrongstack/core";
10440
- import * as path17 from "node:path";
10441
- function parseSymbols5(opts) {
11213
+ import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
11214
+ import * as path18 from "node:path";
11215
+ function parseSymbols6(opts) {
10442
11216
  const { file, content, lang } = opts;
10443
11217
  try {
10444
11218
  return regexParse2({ file, content, lang });
@@ -10449,9 +11223,9 @@ function parseSymbols5(opts) {
10449
11223
  function regexParse2(opts) {
10450
11224
  const { file, content, lang } = opts;
10451
11225
  const symbols = [];
10452
- const basename11 = path17.basename(file).toLowerCase();
10453
- const isPackageJson = basename11 === "package.json";
10454
- const isTsconfig = basename11 === "tsconfig.json" || basename11 === "tsconfig.build.json";
11226
+ const basename12 = path18.basename(file).toLowerCase();
11227
+ const isPackageJson = basename12 === "package.json";
11228
+ const isTsconfig = basename12 === "tsconfig.json" || basename12 === "tsconfig.build.json";
10455
11229
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
10456
11230
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
10457
11231
  const lines = content.split("\n");
@@ -10475,11 +11249,11 @@ function regexParse2(opts) {
10475
11249
  const line = lineFromOffset(offset);
10476
11250
  symbols.push(
10477
11251
  makeSymbol({
10478
- name: path17.basename(file),
11252
+ name: path18.basename(file),
10479
11253
  kind: "object",
10480
11254
  line,
10481
11255
  col: 0,
10482
- signature: `"${path17.basename(file)}" = { ... }`,
11256
+ signature: `"${path18.basename(file)}" = { ... }`,
10483
11257
  file,
10484
11258
  lang
10485
11259
  })
@@ -10641,8 +11415,8 @@ function makeSymbol(opts) {
10641
11415
  }
10642
11416
 
10643
11417
  // src/codebase-index/yaml-parser.ts
10644
- import { expectDefined as expectDefined5, truncate } from "@wrongstack/core";
10645
- function parseSymbols6(opts) {
11418
+ import { expectDefined as expectDefined5, truncate } from "@wrongstack/core/utils";
11419
+ function parseSymbols7(opts) {
10646
11420
  const { file, content, lang } = opts;
10647
11421
  try {
10648
11422
  return regexParse3({ file, content, lang });
@@ -10791,8 +11565,8 @@ function makeSymbol2(opts) {
10791
11565
 
10792
11566
  // src/codebase-index/gitignore.ts
10793
11567
  import * as fs13 from "node:fs/promises";
10794
- import * as path18 from "node:path";
10795
- import { compileGlob } from "@wrongstack/core";
11568
+ import * as path19 from "node:path";
11569
+ import { compileGlob } from "@wrongstack/core/utils";
10796
11570
  function globBody(glob) {
10797
11571
  return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
10798
11572
  }
@@ -10837,7 +11611,7 @@ function compileGitignore(lines) {
10837
11611
  async function loadGitignoreMatcher(projectRoot) {
10838
11612
  let lines = [];
10839
11613
  try {
10840
- const raw = await fs13.readFile(path18.join(projectRoot, ".gitignore"), "utf8");
11614
+ const raw = await fs13.readFile(path19.join(projectRoot, ".gitignore"), "utf8");
10841
11615
  lines = raw.split("\n");
10842
11616
  } catch {
10843
11617
  }
@@ -10846,7 +11620,9 @@ async function loadGitignoreMatcher(projectRoot) {
10846
11620
 
10847
11621
  // src/codebase-index/indexer.ts
10848
11622
  var YIELD_EVERY_N = 50;
10849
- var PARALLEL_BATCH = Math.min(availableParallelism() * 4, 40);
11623
+ function resolveParallelBatch() {
11624
+ return indexParallelBatchSize(availableParallelism());
11625
+ }
10850
11626
  function yieldEventLoop() {
10851
11627
  return new Promise((resolve15) => setImmediate(resolve15));
10852
11628
  }
@@ -10858,22 +11634,12 @@ function throwIfAborted(signal) {
10858
11634
  function isAbortError(err) {
10859
11635
  return err instanceof DOMException && err.name === "AbortError";
10860
11636
  }
10861
- var DEFAULT_IGNORE = [
10862
- "node_modules",
10863
- ".git",
10864
- "dist",
10865
- "build",
10866
- ".next",
10867
- "coverage",
10868
- ".turbo",
10869
- "__snapshots__",
10870
- ".nyc_output"
10871
- ];
11637
+ var DEFAULT_IGNORE = DEFAULT_WALK_IGNORE_DIRS;
10872
11638
  var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
10873
11639
  var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
10874
11640
  function isWithinProject(projectRoot, file) {
10875
- const rel = path19.relative(projectRoot, file);
10876
- return rel !== "" && !rel.startsWith(`..${path19.sep}`) && rel !== ".." && !path19.isAbsolute(rel);
11641
+ const rel = path20.relative(projectRoot, file);
11642
+ return rel !== "" && !rel.startsWith(`..${path20.sep}`) && rel !== ".." && !path20.isAbsolute(rel);
10877
11643
  }
10878
11644
  function isMissingPathError(err) {
10879
11645
  const code = err?.code;
@@ -10884,18 +11650,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10884
11650
  const errors = [];
10885
11651
  let complete = true;
10886
11652
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
10887
- const globs = [
10888
- { ext: ".ts", pat: compileGlob2("**/*.ts") },
10889
- { ext: ".tsx", pat: compileGlob2("**/*.tsx") },
10890
- { ext: ".js", pat: compileGlob2("**/*.js") },
10891
- { ext: ".jsx", pat: compileGlob2("**/*.jsx") },
10892
- { ext: ".go", pat: compileGlob2("**/*.go") },
10893
- { ext: ".py", pat: compileGlob2("**/*.py") },
10894
- { ext: ".rs", pat: compileGlob2("**/*.rs") },
10895
- { ext: ".json", pat: compileGlob2("**/*.json") },
10896
- { ext: ".yaml", pat: compileGlob2("**/*.yaml") },
10897
- { ext: ".yml", pat: compileGlob2("**/*.yml") }
10898
- ];
11653
+ const indexableExts = new Set(INDEXABLE_EXTENSIONS);
10899
11654
  let dirCount = 0;
10900
11655
  const walk2 = async (dir) => {
10901
11656
  throwIfAborted(signal);
@@ -10914,19 +11669,16 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10914
11669
  dirCount++;
10915
11670
  for (const e of entries) {
10916
11671
  if (ignoreSet.has(e.name)) continue;
10917
- const full = path19.join(dir, e.name);
10918
- const rel = path19.relative(projectRoot, full).replace(/\\/g, "/");
11672
+ const full = path20.join(dir, e.name);
11673
+ const rel = path20.relative(projectRoot, full).replace(/\\/g, "/");
10919
11674
  if (e.isDirectory()) {
10920
11675
  if (isGitIgnored(rel, true)) continue;
10921
11676
  await walk2(full);
10922
11677
  } else if (e.isFile()) {
10923
11678
  if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
10924
- const ext = path19.extname(e.name).toLowerCase();
10925
- for (const { ext: extName, pat } of globs) {
10926
- if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
10927
- results.push(full);
10928
- break;
10929
- }
11679
+ const ext = path20.extname(e.name).toLowerCase();
11680
+ if (indexableExts.has(ext) || detectLang(full) !== null) {
11681
+ results.push(full);
10930
11682
  }
10931
11683
  }
10932
11684
  }
@@ -10935,24 +11687,25 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10935
11687
  return { files: results, complete, errors };
10936
11688
  }
10937
11689
  async function parseFile(file, content, lang) {
10938
- switch (lang) {
11690
+ const symbolLang = lang;
11691
+ switch (symbolLang) {
10939
11692
  case "ts":
10940
11693
  case "tsx":
10941
11694
  case "js":
10942
11695
  case "jsx":
10943
- return parseSymbols({ file, content, lang });
11696
+ return parseSymbols({ file, content, lang: symbolLang });
10944
11697
  case "go":
10945
11698
  return parseSymbols2({ file, content, lang: "go" });
10946
11699
  case "py":
10947
- return parseSymbols3({ file, content, lang: "py" });
11700
+ return parseSymbols4({ file, content, lang: "py" });
10948
11701
  case "rs":
10949
- return parseSymbols4({ file, content, lang: "rs" });
11702
+ return parseSymbols5({ file, content, lang: "rs" });
10950
11703
  case "json":
10951
- return parseSymbols5({ file, content, lang: "json" });
11704
+ return parseSymbols6({ file, content, lang: "json" });
10952
11705
  case "yaml":
10953
- return parseSymbols6({ file, content, lang: "yaml" });
11706
+ return parseSymbols7({ file, content, lang: "yaml" });
10954
11707
  default:
10955
- return { file, lang, symbols: [], mtimeMs: Date.now() };
11708
+ return parseSymbols3({ file, content, lang: symbolLang });
10956
11709
  }
10957
11710
  }
10958
11711
  function assignRefsToSymbols2(refs, symbols) {
@@ -11000,10 +11753,10 @@ async function runIndexerWithStore(store, opts) {
11000
11753
  let discoveredFiles = null;
11001
11754
  let discoveryComplete = true;
11002
11755
  if (opts.files && opts.files.length > 0) {
11003
- files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => {
11756
+ files = opts.files.map((f) => path20.resolve(projectRoot, f)).filter((f) => {
11004
11757
  if (!isWithinProject(projectRoot, f)) return false;
11005
- const rel = path19.relative(projectRoot, f).replace(/\\/g, "/");
11006
- return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path19.basename(f)) && !isGitIgnored(rel, false);
11758
+ const rel = path20.relative(projectRoot, f).replace(/\\/g, "/");
11759
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path20.basename(f)) && !isGitIgnored(rel, false);
11007
11760
  });
11008
11761
  } else {
11009
11762
  const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
@@ -11024,15 +11777,19 @@ async function runIndexerWithStore(store, opts) {
11024
11777
  if (!force) {
11025
11778
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
11026
11779
  }
11780
+ const parallelBatch = resolveParallelBatch();
11027
11781
  let filesSinceLastYield = 0;
11028
- for (let batchStart = 0; batchStart < files.length; batchStart += PARALLEL_BATCH) {
11029
- const batchEnd = Math.min(batchStart + PARALLEL_BATCH, files.length);
11782
+ for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
11783
+ const batchEnd = Math.min(batchStart + parallelBatch, files.length);
11030
11784
  const batchFiles = files.slice(batchStart, batchEnd);
11031
11785
  opts.onProgress?.(batchEnd, files.length);
11032
11786
  filesSinceLastYield += batchFiles.length;
11033
11787
  if (filesSinceLastYield >= YIELD_EVERY_N) {
11034
11788
  filesSinceLastYield = 0;
11035
11789
  await yieldEventLoop();
11790
+ if (isFrugalPerf()) {
11791
+ await new Promise((r) => setTimeout(r, 8));
11792
+ }
11036
11793
  throwIfAborted(signal);
11037
11794
  }
11038
11795
  const statOpts = signal ? { signal } : {};
@@ -11136,9 +11893,7 @@ async function runIndexerWithStore(store, opts) {
11136
11893
  continue;
11137
11894
  }
11138
11895
  if (parsed.symbols.length === 0) {
11139
- store.deleteRefsForFile(file);
11140
- store.deleteSymbolsForFile(file);
11141
- store.upsertFile({
11896
+ store.replaceEmptyFile({
11142
11897
  file,
11143
11898
  lang,
11144
11899
  mtimeMs: Math.floor(stat18.mtimeMs),
@@ -11207,6 +11962,7 @@ async function runIndexerWithStore(store, opts) {
11207
11962
  }
11208
11963
  store.resolveRefs();
11209
11964
  store.setMetadata("relation_graph_version", relationGraphVersion);
11965
+ store.optimize();
11210
11966
  const durationMs = Date.now() - startMs;
11211
11967
  store.setLastIndexed(Date.now());
11212
11968
  return {
@@ -11497,10 +12253,10 @@ async function callInline(op, args, opts) {
11497
12253
  opts.signal?.removeEventListener("abort", onOuterAbort);
11498
12254
  }
11499
12255
  }
11500
- var chain = Promise.resolve();
12256
+ var chain2 = Promise.resolve();
11501
12257
  function withMutex(job) {
11502
- const run = chain.then(job, job);
11503
- chain = run.then(
12258
+ const run = chain2.then(job, job);
12259
+ chain2 = run.then(
11504
12260
  () => void 0,
11505
12261
  () => void 0
11506
12262
  );
@@ -11514,11 +12270,62 @@ function circuitOpenError() {
11514
12270
  }
11515
12271
  var DEFAULT_DEBOUNCE_MS = 400;
11516
12272
  var debounceTimers = /* @__PURE__ */ new Map();
11517
- function debounceKey(indexDir, file) {
11518
- return `${indexDir ?? ""}|${file}`;
12273
+ var readyReindexBatches = /* @__PURE__ */ new Map();
12274
+ function debounceKey(projectRoot, indexDir, file) {
12275
+ return JSON.stringify([projectRoot, indexDir ?? "", file]);
12276
+ }
12277
+ function reindexBatchKey(projectRoot, indexDir) {
12278
+ return JSON.stringify([projectRoot, indexDir ?? ""]);
12279
+ }
12280
+ function flushReadyReindexBatch(key) {
12281
+ const batch = readyReindexBatches.get(key);
12282
+ if (!batch) return;
12283
+ readyReindexBatches.delete(key);
12284
+ if (!indexCircuitBreaker.allowRequest()) {
12285
+ const error = circuitOpenError();
12286
+ for (const onError of batch.onErrors) onError(error);
12287
+ return;
12288
+ }
12289
+ void withMutex(
12290
+ () => callIndexOp(
12291
+ "index",
12292
+ {
12293
+ projectRoot: batch.projectRoot,
12294
+ files: [...batch.files].sort(),
12295
+ indexDir: batch.indexDir
12296
+ },
12297
+ { timeoutMs: batch.timeoutMs }
12298
+ )
12299
+ ).then(
12300
+ () => indexCircuitBreaker.recordSuccess(),
12301
+ (err) => {
12302
+ indexCircuitBreaker.recordFailure(err);
12303
+ for (const onError of batch.onErrors) onError(err);
12304
+ }
12305
+ );
12306
+ }
12307
+ function addReadyReindex(opts) {
12308
+ const key = reindexBatchKey(opts.projectRoot, opts.indexDir);
12309
+ const existing = readyReindexBatches.get(key);
12310
+ if (existing) {
12311
+ existing.files.add(opts.file);
12312
+ existing.timeoutMs = Math.max(existing.timeoutMs, opts.timeoutMs);
12313
+ if (opts.onError) existing.onErrors.add(opts.onError);
12314
+ return;
12315
+ }
12316
+ const batch = {
12317
+ projectRoot: opts.projectRoot,
12318
+ indexDir: opts.indexDir,
12319
+ files: /* @__PURE__ */ new Set([opts.file]),
12320
+ timeoutMs: opts.timeoutMs,
12321
+ onErrors: new Set(opts.onError ? [opts.onError] : [])
12322
+ };
12323
+ batch.flush = setImmediate(() => flushReadyReindexBatch(key));
12324
+ batch.flush.unref?.();
12325
+ readyReindexBatches.set(key, batch);
11519
12326
  }
11520
12327
  function isIndexableFile(filePath) {
11521
- return detectLang(filePath) !== null;
12328
+ return isIndexablePath(filePath);
11522
12329
  }
11523
12330
  function isRecoverableConstraintError(err) {
11524
12331
  if (err instanceof Error) {
@@ -11578,28 +12385,18 @@ function enqueueReindex(opts) {
11578
12385
  if (files.length === 0) return;
11579
12386
  const ms = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
11580
12387
  for (const file of files) {
11581
- const key = debounceKey(opts.indexDir, file);
12388
+ const key = debounceKey(opts.projectRoot, opts.indexDir, file);
11582
12389
  const existing = debounceTimers.get(key);
11583
12390
  if (existing) clearTimeout(existing);
11584
12391
  const timer = setTimeout(() => {
11585
12392
  debounceTimers.delete(key);
11586
- if (!indexCircuitBreaker.allowRequest()) {
11587
- opts.onError?.(circuitOpenError());
11588
- return;
11589
- }
11590
- void withMutex(
11591
- () => callIndexOp(
11592
- "index",
11593
- { projectRoot: opts.projectRoot, files: [file], indexDir: opts.indexDir },
11594
- { timeoutMs: opts.timeoutMs ?? DEFAULT_INCREMENTAL_TIMEOUT_MS }
11595
- )
11596
- ).then(
11597
- () => indexCircuitBreaker.recordSuccess(),
11598
- (err) => {
11599
- indexCircuitBreaker.recordFailure(err);
11600
- opts.onError?.(err);
11601
- }
11602
- );
12393
+ addReadyReindex({
12394
+ projectRoot: opts.projectRoot,
12395
+ file,
12396
+ indexDir: opts.indexDir,
12397
+ timeoutMs: opts.timeoutMs ?? DEFAULT_INCREMENTAL_TIMEOUT_MS,
12398
+ onError: opts.onError
12399
+ });
11603
12400
  }, ms);
11604
12401
  timer.unref?.();
11605
12402
  debounceTimers.set(key, timer);
@@ -11608,6 +12405,10 @@ function enqueueReindex(opts) {
11608
12405
  function cancelPendingReindexes() {
11609
12406
  for (const t of debounceTimers.values()) clearTimeout(t);
11610
12407
  debounceTimers.clear();
12408
+ for (const batch of readyReindexBatches.values()) {
12409
+ if (batch.flush) clearImmediate(batch.flush);
12410
+ }
12411
+ readyReindexBatches.clear();
11611
12412
  }
11612
12413
  async function searchCodebaseIndex(args, opts = {}) {
11613
12414
  return callIndexOp("search", args, {
@@ -11893,7 +12694,7 @@ var codebaseStatsTool = {
11893
12694
 
11894
12695
  // src/design.ts
11895
12696
  import * as fs16 from "node:fs/promises";
11896
- import * as path20 from "node:path";
12697
+ import * as path21 from "node:path";
11897
12698
  import {
11898
12699
  applyTokenOverrides,
11899
12700
  getDesignKitLoader,
@@ -11902,23 +12703,24 @@ import {
11902
12703
  materializeTokens,
11903
12704
  recordKitChoice,
11904
12705
  recordOverrides,
12706
+ resolveSemanticTune,
11905
12707
  runDesignVerify,
11906
12708
  setActiveKit,
11907
12709
  setDesignOverrides
11908
- } from "@wrongstack/core";
12710
+ } from "@wrongstack/core/design";
11909
12711
  async function resolveReal(p) {
11910
- const resolved = path20.resolve(p);
11911
- let probe = resolved;
12712
+ const resolved = path21.resolve(p);
12713
+ let probe2 = resolved;
11912
12714
  const missing = [];
11913
12715
  for (; ; ) {
11914
12716
  try {
11915
- return path20.resolve(await fs16.realpath(probe), ...missing);
12717
+ return path21.resolve(await fs16.realpath(probe2), ...missing);
11916
12718
  } catch (err) {
11917
12719
  if (err.code === "ENOENT") {
11918
- const parent = path20.dirname(probe);
11919
- if (parent === probe) return resolved;
11920
- missing.unshift(path20.basename(probe));
11921
- probe = parent;
12720
+ const parent = path21.dirname(probe2);
12721
+ if (parent === probe2) return resolved;
12722
+ missing.unshift(path21.basename(probe2));
12723
+ probe2 = parent;
11922
12724
  continue;
11923
12725
  }
11924
12726
  return resolved;
@@ -11948,8 +12750,8 @@ var designTool = {
11948
12750
  properties: {
11949
12751
  action: {
11950
12752
  type: "string",
11951
- enum: ["list", "use", "foundations", "set", "materialize", "verify"],
11952
- description: "list = menu; use = load+pin a kit; foundations = baseline; set = override colors/tokens; materialize = write tokens to a theme file; verify = scan UI for off-palette colors. Default: list."
12753
+ enum: ["list", "use", "foundations", "set", "tune", "materialize", "verify"],
12754
+ description: "list = menu; use = load+pin a kit; foundations = baseline; set = override colors/tokens; tune = high-level knobs (radius/density/font/motion); materialize = write tokens to a theme file; verify = scan UI for token drift. Default: list."
11953
12755
  },
11954
12756
  kit: {
11955
12757
  type: "string",
@@ -11965,6 +12767,16 @@ var designTool = {
11965
12767
  description: 'Token overrides for "set"/"use": { "primary": "oklch(\u2026)", "dark.bg": "#111" }. Bare key = both themes; "light."/"dark." prefix = that theme only. Empty value clears an override.',
11966
12768
  additionalProperties: { type: "string" }
11967
12769
  },
12770
+ tune: {
12771
+ type: "object",
12772
+ description: 'High-level knobs for "tune": { "radius": "lg", "density": "compact", "font": "Space Grotesk", "motion": "snappy" }. radius = none|sm|md|lg|xl|full or a base length; density = compact|cozy|comfortable; motion = snappy|smooth|none. Resolved to concrete token overrides.',
12773
+ properties: {
12774
+ radius: { type: "string" },
12775
+ density: { type: "string" },
12776
+ font: { type: "string" },
12777
+ motion: { type: "string" }
12778
+ }
12779
+ },
11968
12780
  out: {
11969
12781
  type: "string",
11970
12782
  description: "Materialize output path (project-relative). Defaults to a per-stack convention."
@@ -12062,6 +12874,28 @@ ${body}`
12062
12874
  These win over kit tokens. Run \`design {action:"materialize"}\` to write them to a theme file.`
12063
12875
  };
12064
12876
  }
12877
+ if (action === "tune") {
12878
+ const patch = resolveSemanticTune(input.tune ?? {});
12879
+ if (Object.keys(patch).length === 0) {
12880
+ return {
12881
+ action,
12882
+ output: 'No recognized knobs. Pass tune:{ radius:"lg", density:"compact", font:"\u2026", motion:"snappy" }.'
12883
+ };
12884
+ }
12885
+ const merged = await recordOverrides(ctx.projectRoot, patch, (/* @__PURE__ */ new Date()).toISOString());
12886
+ if (!merged) {
12887
+ return {
12888
+ action,
12889
+ output: 'No active kit. Pick one first: `design {action:"use", kit:"<id>"}`.'
12890
+ };
12891
+ }
12892
+ setDesignOverrides(ctx, merged);
12893
+ return {
12894
+ action,
12895
+ output: `Tuned. Resolved ${Object.keys(patch).length} token override(s): ${Object.entries(patch).map(([k, v]) => `${k}=${v}`).join(", ")}
12896
+ Run \`design {action:"materialize"}\` to write the tuned tokens to your theme file.`
12897
+ };
12898
+ }
12065
12899
  if (action === "materialize") {
12066
12900
  const active = await loadActiveKit(ctx.projectRoot);
12067
12901
  if (!active) {
@@ -12083,11 +12917,11 @@ These win over kit tokens. Run \`design {action:"materialize"}\` to write them t
12083
12917
  outPath: input.out
12084
12918
  });
12085
12919
  const root = await resolveReal(ctx.projectRoot);
12086
- const absResolved = path20.isAbsolute(result.path) ? path20.resolve(result.path) : path20.resolve(path20.join(ctx.projectRoot, result.path));
12087
- const absParent = await resolveReal(path20.dirname(absResolved));
12088
- const abs = path20.join(absParent, path20.basename(absResolved));
12089
- const rel = path20.relative(root, abs);
12090
- if (rel.startsWith("..") || path20.isAbsolute(rel)) {
12920
+ const absResolved = path21.isAbsolute(result.path) ? path21.resolve(result.path) : path21.resolve(path21.join(ctx.projectRoot, result.path));
12921
+ const absParent = await resolveReal(path21.dirname(absResolved));
12922
+ const abs = path21.join(absParent, path21.basename(absResolved));
12923
+ const rel = path21.relative(root, abs);
12924
+ if (rel.startsWith("..") || path21.isAbsolute(rel)) {
12091
12925
  throw new Error(
12092
12926
  `design: materialize path "${result.path}" would escape the project root`
12093
12927
  );
@@ -12111,7 +12945,7 @@ ${result.content}
12111
12945
  \`\`\``
12112
12946
  };
12113
12947
  }
12114
- await fs16.mkdir(path20.dirname(abs), { recursive: true });
12948
+ await fs16.mkdir(path21.dirname(abs), { recursive: true });
12115
12949
  await fs16.writeFile(abs, result.content);
12116
12950
  return {
12117
12951
  action,
@@ -12163,8 +12997,8 @@ init_util();
12163
12997
  import { spawn as spawn7 } from "node:child_process";
12164
12998
  import { statSync as statSync2 } from "node:fs";
12165
12999
  import * as fs17 from "node:fs/promises";
12166
- import * as path21 from "node:path";
12167
- import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core";
13000
+ import * as path22 from "node:path";
13001
+ import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core/utils";
12168
13002
  var diffTool = {
12169
13003
  name: "diff",
12170
13004
  category: "Filesystem",
@@ -12247,11 +13081,11 @@ function findGitDir(cwd) {
12247
13081
  let dir = cwd;
12248
13082
  for (let i = 0; i < 20; i++) {
12249
13083
  try {
12250
- const stat18 = statSync2(path21.join(dir, ".git"));
13084
+ const stat18 = statSync2(path22.join(dir, ".git"));
12251
13085
  if (stat18.isDirectory()) return dir;
12252
13086
  } catch {
12253
13087
  }
12254
- const parent = path21.dirname(dir);
13088
+ const parent = path22.dirname(dir);
12255
13089
  if (parent === dir) break;
12256
13090
  dir = parent;
12257
13091
  }
@@ -12465,7 +13299,7 @@ function processFile(content, absPath, _style, _overwrite, target) {
12465
13299
  // src/e2e.ts
12466
13300
  init_util();
12467
13301
  import { open, readdir as readdir5 } from "node:fs/promises";
12468
- import * as path22 from "node:path";
13302
+ import * as path23 from "node:path";
12469
13303
  async function readBoundedText(filePath, maxBytes) {
12470
13304
  let handle;
12471
13305
  try {
@@ -12515,8 +13349,8 @@ var MAX_PACKAGE_BYTES = 512 * 1024;
12515
13349
  var MAX_CONFIG_BYTES = 512 * 1024;
12516
13350
  var MAX_SPEC_SAMPLES = 20;
12517
13351
  function relativePath(root, target) {
12518
- const value = path22.relative(root, target) || ".";
12519
- return value.split(path22.sep).join("/");
13352
+ const value = path23.relative(root, target) || ".";
13353
+ return value.split(path23.sep).join("/");
12520
13354
  }
12521
13355
  function escapeRegExp(value) {
12522
13356
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -12588,7 +13422,7 @@ async function scanWorkspace(root, maxDepth, signal) {
12588
13422
  }
12589
13423
  for (const entry of entries) {
12590
13424
  signal.throwIfAborted();
12591
- const absolutePath = path22.join(current.directory, entry.name);
13425
+ const absolutePath = path23.join(current.directory, entry.name);
12592
13426
  if (entry.isFile()) {
12593
13427
  if (entry.name === "package.json") result.packageFiles.push(absolutePath);
12594
13428
  const framework = CONFIG_NAMES.get(entry.name);
@@ -12649,9 +13483,9 @@ async function detectPackageManager3(projectRoot, scanRoot, declared) {
12649
13483
  if (names.has("bun.lock") || names.has("bun.lockb")) return "bun";
12650
13484
  if (names.has("package-lock.json") || names.has("npm-shrinkwrap.json")) return "npm";
12651
13485
  if (directory === scanRoot) break;
12652
- const parent = path22.dirname(directory);
12653
- const relativeParent = path22.relative(scanRoot, parent);
12654
- if (parent === directory || relativeParent.startsWith("..") || path22.isAbsolute(relativeParent)) {
13486
+ const parent = path23.dirname(directory);
13487
+ const relativeParent = path23.relative(scanRoot, parent);
13488
+ if (parent === directory || relativeParent.startsWith("..") || path23.isAbsolute(relativeParent)) {
12655
13489
  break;
12656
13490
  }
12657
13491
  directory = parent;
@@ -12696,7 +13530,7 @@ function isSpec(framework, filename) {
12696
13530
  return /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/i.test(filename);
12697
13531
  }
12698
13532
  async function collectSpecs(root, framework, testDirectory, signal) {
12699
- const roots = testDirectory ? [path22.resolve(root, testDirectory)] : framework === "cypress" ? [path22.join(root, "cypress", "e2e"), path22.join(root, "cypress", "integration")] : [path22.join(root, "tests")];
13533
+ const roots = testDirectory ? [path23.resolve(root, testDirectory)] : framework === "cypress" ? [path23.join(root, "cypress", "e2e"), path23.join(root, "cypress", "integration")] : [path23.join(root, "tests")];
12700
13534
  const samples = [];
12701
13535
  let count = 0;
12702
13536
  let scanned = 0;
@@ -12715,7 +13549,7 @@ async function collectSpecs(root, framework, testDirectory, signal) {
12715
13549
  }
12716
13550
  for (const entry of entries) {
12717
13551
  signal.throwIfAborted();
12718
- const target = path22.join(directory, entry.name);
13552
+ const target = path23.join(directory, entry.name);
12719
13553
  if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) queue.push(target);
12720
13554
  else if (entry.isFile() && isSpec(framework, entry.name)) {
12721
13555
  count += 1;
@@ -12735,7 +13569,7 @@ function nearestPackage(projectRoot, packagesByDirectory, scanRoot) {
12735
13569
  const found = packagesByDirectory.get(directory);
12736
13570
  if (found) return found;
12737
13571
  if (directory === scanRoot) return void 0;
12738
- const parent = path22.dirname(directory);
13572
+ const parent = path23.dirname(directory);
12739
13573
  if (parent === directory || relativePath(scanRoot, parent).startsWith("..")) return void 0;
12740
13574
  directory = parent;
12741
13575
  }
@@ -12746,13 +13580,13 @@ async function discoverE2EProjects(root, options) {
12746
13580
  const packages = (await Promise.all(scan.packageFiles.map(readPackageInfo))).filter(
12747
13581
  (info) => Boolean(info)
12748
13582
  );
12749
- const packagesByDirectory = new Map(packages.map((info) => [path22.dirname(info.path), info]));
13583
+ const packagesByDirectory = new Map(packages.map((info) => [path23.dirname(info.path), info]));
12750
13584
  const candidates = /* @__PURE__ */ new Map();
12751
13585
  for (const config of scan.configs) {
12752
13586
  if (options.framework && options.framework !== "all" && config.framework !== options.framework) {
12753
13587
  continue;
12754
13588
  }
12755
- const projectRoot = path22.dirname(config.absolutePath);
13589
+ const projectRoot = path23.dirname(config.absolutePath);
12756
13590
  candidates.set(`${config.framework}:${projectRoot}`, {
12757
13591
  framework: config.framework,
12758
13592
  root: projectRoot,
@@ -12760,7 +13594,7 @@ async function discoverE2EProjects(root, options) {
12760
13594
  });
12761
13595
  }
12762
13596
  for (const info of packages) {
12763
- const projectRoot = path22.dirname(info.path);
13597
+ const projectRoot = path23.dirname(info.path);
12764
13598
  for (const framework of frameworkFromPackage(info)) {
12765
13599
  if (options.framework && options.framework !== "all" && framework !== options.framework)
12766
13600
  continue;
@@ -12776,10 +13610,10 @@ async function discoverE2EProjects(root, options) {
12776
13610
  const scripts = matchingScripts(info, candidate.framework);
12777
13611
  const manager = await detectPackageManager3(candidate.root, root, info?.packageManager);
12778
13612
  const testDirectory = candidate.framework === "playwright" ? staticString(source, "testDir") : void 0;
12779
- const resolvedTestDirectory = testDirectory ? path22.resolve(candidate.root, testDirectory) : void 0;
12780
- const relativeTestDirectory = resolvedTestDirectory ? path22.relative(root, resolvedTestDirectory) : void 0;
13613
+ const resolvedTestDirectory = testDirectory ? path23.resolve(candidate.root, testDirectory) : void 0;
13614
+ const relativeTestDirectory = resolvedTestDirectory ? path23.relative(root, resolvedTestDirectory) : void 0;
12781
13615
  const unsafeTestDirectory = Boolean(
12782
- relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path22.isAbsolute(relativeTestDirectory))
13616
+ relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path23.isAbsolute(relativeTestDirectory))
12783
13617
  );
12784
13618
  const specs = options.includeSpecs === false || unsafeTestDirectory ? { count: 0, samples: [], truncated: false } : await collectSpecs(candidate.root, candidate.framework, testDirectory, options.signal);
12785
13619
  const warnings = [];
@@ -12884,10 +13718,10 @@ import {
12884
13718
  atomicWrite as atomicWrite2,
12885
13719
  detectNewlineStyle,
12886
13720
  normalizeToLf,
12887
- ToolValidationError,
12888
13721
  toStyle,
12889
13722
  unifiedDiff
12890
- } from "@wrongstack/core";
13723
+ } from "@wrongstack/core/utils";
13724
+ import { ToolValidationError } from "@wrongstack/core/types";
12891
13725
 
12892
13726
  // src/_edit-match.ts
12893
13727
  var TIER_LABEL = {
@@ -13115,7 +13949,7 @@ function prefixSimilarity(a, b) {
13115
13949
  }
13116
13950
 
13117
13951
  // src/_syntax-check.ts
13118
- import * as path23 from "node:path";
13952
+ import * as path24 from "node:path";
13119
13953
  var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
13120
13954
  var MAX_CHECK_CHARS = 15e5;
13121
13955
  var MAX_ERRORS = 5;
@@ -13139,15 +13973,15 @@ async function checkSyntax(filePath, content, previousContent) {
13139
13973
  return { errors, preExisting };
13140
13974
  }
13141
13975
  function isJsoncFile(filePath) {
13142
- const base = path23.basename(filePath).toLowerCase();
13976
+ const base = path24.basename(filePath).toLowerCase();
13143
13977
  if (base.endsWith(".jsonc")) return true;
13144
13978
  if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
13145
- const dir = path23.basename(path23.dirname(filePath)).toLowerCase();
13979
+ const dir = path24.basename(path24.dirname(filePath)).toLowerCase();
13146
13980
  return dir === ".vscode";
13147
13981
  }
13148
13982
  async function parseErrors(filePath, content) {
13149
13983
  if (content.length > MAX_CHECK_CHARS) return void 0;
13150
- const ext = path23.extname(filePath).toLowerCase();
13984
+ const ext = path24.extname(filePath).toLowerCase();
13151
13985
  if (ext === ".json" || ext === ".jsonc") {
13152
13986
  try {
13153
13987
  JSON.parse(content);
@@ -13174,7 +14008,7 @@ async function parseErrors(filePath, content) {
13174
14008
  ts2.ScriptKind.JSX
13175
14009
  );
13176
14010
  const sourceFile = ts2.createSourceFile(
13177
- path23.basename(filePath),
14011
+ path24.basename(filePath),
13178
14012
  content,
13179
14013
  ts2.ScriptTarget.Latest,
13180
14014
  /* setParentNodes */
@@ -13409,7 +14243,7 @@ import {
13409
14243
  emitProcessCompleted as emitProcessCompleted3,
13410
14244
  emitProcessOutput as emitProcessOutput3,
13411
14245
  emitProcessStarted as emitProcessStarted3
13412
- } from "@wrongstack/core";
14246
+ } from "@wrongstack/core/observability";
13413
14247
  import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils/error";
13414
14248
  init_output_spool();
13415
14249
  init_util();
@@ -13418,7 +14252,7 @@ init_win32_resolve();
13418
14252
 
13419
14253
  // src/exec-kill-guard.ts
13420
14254
  import * as os7 from "node:os";
13421
- import * as path24 from "node:path";
14255
+ import * as path25 from "node:path";
13422
14256
  var isWin3 = os7.platform() === "win32";
13423
14257
  async function checkExecKillCommand(cmd, args) {
13424
14258
  if (!cmd) return { blocked: false };
@@ -13631,7 +14465,7 @@ async function checkKillTarget(target) {
13631
14465
  reason: `Blocked: kill ${target.signal} '${target.name}' targets a WrongStack process name.`
13632
14466
  };
13633
14467
  }
13634
- const currentImage = path24.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
14468
+ const currentImage = path25.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
13635
14469
  const targetsNodeRuntime = nameLower === "node" || nameLower.startsWith("node");
13636
14470
  if (targetsNodeRuntime && currentImage === "node") {
13637
14471
  return {
@@ -14447,7 +15281,7 @@ var execTool = {
14447
15281
  command: cmd,
14448
15282
  args: input.args ?? [],
14449
15283
  stdout: "",
14450
- stderr: `Command "${cmd}" not in allowlist. Add it to your ~/.wrongstack/config.json under "tools": { "exec": { "allow": ["${cmd}"] } }, or use the bash tool for one-off arbitrary commands.`,
15284
+ stderr: `Command "${cmd}" not in allowlist. Add it to your active profile config (~/.wrongstack/profiles/<name>/config.json) under "tools": { "exec": { "allow": ["${cmd}"] } }, or use the bash tool for one-off arbitrary commands.`,
14451
15285
  exitCode: 1,
14452
15286
  truncated: false,
14453
15287
  allowed: false,
@@ -14659,18 +15493,17 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
14659
15493
  }
14660
15494
 
14661
15495
  // src/fetch.ts
14662
- import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValidationError3 } from "@wrongstack/core";
15496
+ import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValidationError3 } from "@wrongstack/core/types";
14663
15497
  import TurndownService from "turndown";
14664
15498
 
14665
15499
  // src/_fetch-guard.ts
14666
15500
  import * as dns2 from "node:dns/promises";
14667
15501
  import * as net3 from "node:net";
14668
15502
  import {
14669
- FetchError,
14670
- ToolValidationError as ToolValidationError2,
14671
15503
  isPrivateIPv4 as isPrivateIPv42,
14672
15504
  isPrivateIPv6 as isPrivateIPv62
14673
- } from "@wrongstack/core";
15505
+ } from "@wrongstack/core/utils";
15506
+ import { FetchError, ToolValidationError as ToolValidationError2 } from "@wrongstack/core/types";
14674
15507
  import { Agent, fetch as undiciFetch } from "undici";
14675
15508
  var nativeGlobalFetch = globalThis.fetch;
14676
15509
  var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
@@ -15168,7 +16001,8 @@ init_util();
15168
16001
  import { spawn as spawn9 } from "node:child_process";
15169
16002
  import { statSync as statSync3 } from "node:fs";
15170
16003
  import { dirname as dirname12, resolve as resolve11, sep as sep5 } from "node:path";
15171
- import { assessCommitSafety, buildChildEnv as buildChildEnv4 } from "@wrongstack/core";
16004
+ import { assessCommitSafety } from "@wrongstack/core/coordination";
16005
+ import { buildChildEnv as buildChildEnv4 } from "@wrongstack/core/utils";
15172
16006
  var TIMEOUT_MS2 = 3e4;
15173
16007
  var MAX_OUTPUT3 = 1e5;
15174
16008
  var gitTool = {
@@ -15448,8 +16282,8 @@ function runGit2(args, cwd, signal) {
15448
16282
 
15449
16283
  // src/glob.ts
15450
16284
  import * as fs20 from "node:fs/promises";
15451
- import * as path25 from "node:path";
15452
- import { compileGlob as compileGlob3 } from "@wrongstack/core";
16285
+ import * as path26 from "node:path";
16286
+ import { compileGlob as compileGlob2, DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS2 } from "@wrongstack/core/utils";
15453
16287
 
15454
16288
  // src/_concurrency.ts
15455
16289
  async function mapWithConcurrency(items, limit, fn) {
@@ -15470,7 +16304,7 @@ async function mapWithConcurrency(items, limit, fn) {
15470
16304
 
15471
16305
  // src/glob.ts
15472
16306
  init_util();
15473
- var DEFAULT_IGNORE2 = ["node_modules", ".git", "dist", "build", ".next", "coverage", ".turbo"];
16307
+ var DEFAULT_IGNORE2 = DEFAULT_WALK_IGNORE_DIRS2;
15474
16308
  var WALK_CONCURRENCY = 16;
15475
16309
  var globTool = {
15476
16310
  name: "glob",
@@ -15510,8 +16344,8 @@ var globTool = {
15510
16344
  const signal = opts?.signal;
15511
16345
  const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
15512
16346
  const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
15513
- const ignored = await readGitignore(base);
15514
- const re = compileGlob3(input.pattern);
16347
+ const isGitIgnored = await loadGitignoreMatcher(base);
16348
+ const re = compileGlob2(input.pattern);
15515
16349
  const results = [];
15516
16350
  let truncated = false;
15517
16351
  const pushResult = async (full) => {
@@ -15550,12 +16384,13 @@ var globTool = {
15550
16384
  for (const e of entries) {
15551
16385
  const name = e.name;
15552
16386
  if (DEFAULT_IGNORE2.includes(name)) continue;
15553
- if (ignored.includes(name)) continue;
15554
16387
  const rel = relPrefix ? `${relPrefix}/${name}` : name;
15555
- const full = path25.join(dir, name);
16388
+ const full = path26.join(dir, name);
15556
16389
  if (e.isDirectory()) {
16390
+ if (isGitIgnored(rel, true)) continue;
15557
16391
  subdirs.push({ full, rel });
15558
16392
  } else if (e.isFile()) {
16393
+ if (isGitIgnored(rel, false)) continue;
15559
16394
  re.lastIndex = 0;
15560
16395
  const relMatch = re.test(rel);
15561
16396
  re.lastIndex = 0;
@@ -15567,10 +16402,12 @@ var globTool = {
15567
16402
  try {
15568
16403
  const st = await fs20.stat(full);
15569
16404
  if (st.isDirectory()) {
16405
+ if (isGitIgnored(rel, true)) continue;
15570
16406
  const real = await fs20.realpath(full);
15571
16407
  await assertRealInsideRoot(real, ctx);
15572
16408
  subdirs.push({ full, rel });
15573
16409
  } else if (st.isFile()) {
16410
+ if (isGitIgnored(rel, false)) continue;
15574
16411
  const real = await fs20.realpath(full);
15575
16412
  await assertRealInsideRoot(real, ctx);
15576
16413
  re.lastIndex = 0;
@@ -15594,21 +16431,14 @@ var globTool = {
15594
16431
  return { files: results.map((r) => r.rel), truncated };
15595
16432
  }
15596
16433
  };
15597
- async function readGitignore(dir) {
15598
- try {
15599
- const raw = await fs20.readFile(path25.join(dir, ".gitignore"), "utf8");
15600
- return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
15601
- } catch {
15602
- return [];
15603
- }
15604
- }
15605
16434
 
15606
16435
  // src/grep.ts
15607
- import { expectDefined as expectDefined7 } from "@wrongstack/core";
16436
+ import { expectDefined as expectDefined7 } from "@wrongstack/core/utils";
15608
16437
  import { spawn as spawn10 } from "node:child_process";
15609
16438
  import * as fs21 from "node:fs/promises";
15610
- import * as path26 from "node:path";
15611
- import { buildChildEnv as buildChildEnv5, compileGlob as compileGlob4, ToolValidationError as ToolValidationError4 } from "@wrongstack/core";
16439
+ import * as path27 from "node:path";
16440
+ import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
16441
+ import { buildChildEnv as buildChildEnv5, compileGlob as compileGlob3, DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS3 } from "@wrongstack/core/utils";
15612
16442
 
15613
16443
  // src/_regex.ts
15614
16444
  var MAX_PATTERN_LEN = 256;
@@ -15657,7 +16487,7 @@ function capSubject(line) {
15657
16487
 
15658
16488
  // src/grep.ts
15659
16489
  init_util();
15660
- var DEFAULT_IGNORE3 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
16490
+ var DEFAULT_IGNORE3 = DEFAULT_WALK_IGNORE_DIRS3;
15661
16491
  var NATIVE_SCAN_CONCURRENCY = 32;
15662
16492
  var NATIVE_READ_CHUNK_BYTES = 64 * 1024;
15663
16493
  var NATIVE_MAX_FILE_BYTES = 1e6;
@@ -15774,6 +16604,10 @@ async function* runRgStream(input, base, mode, limit, signal) {
15774
16604
  for (const ignored of DEFAULT_IGNORE3) {
15775
16605
  args.push("--glob", `!${ignored}/**`, "--glob", `!**/${ignored}/**`);
15776
16606
  }
16607
+ const gitignorePath = path27.join(base, ".gitignore");
16608
+ if (await fs21.access(gitignorePath).then(() => true, () => false)) {
16609
+ args.push("--ignore-file", gitignorePath);
16610
+ }
15777
16611
  if (input.glob) args.push("--glob", input.glob);
15778
16612
  args.push("--", input.pattern, base);
15779
16613
  const matches = [];
@@ -15898,7 +16732,8 @@ async function runNative(input, base, mode, limit, signal) {
15898
16732
  });
15899
16733
  }
15900
16734
  const re = compiled.regex;
15901
- const globRe = input.glob ? compileGlob4(input.glob) : null;
16735
+ const globRe = input.glob ? compileGlob3(input.glob) : null;
16736
+ const isGitIgnored = await loadGitignoreMatcher(base);
15902
16737
  const matches = [];
15903
16738
  const countOnlyFirstHit = mode === "count" && limit === 1;
15904
16739
  const maxBytes = mode === "content" ? NATIVE_MAX_FILE_BYTES : Math.min(NATIVE_MAX_FILE_BYTES, 256 * 1024);
@@ -15986,7 +16821,7 @@ async function runNative(input, base, mode, limit, signal) {
15986
16821
  } catch {
15987
16822
  }
15988
16823
  };
15989
- const walk2 = async (dir) => {
16824
+ const walk2 = async (dir, relPrefix) => {
15990
16825
  if (stopped || signal.aborted) return;
15991
16826
  let entries;
15992
16827
  try {
@@ -16000,17 +16835,24 @@ async function runNative(input, base, mode, limit, signal) {
16000
16835
  if (stopped) return;
16001
16836
  if (DEFAULT_IGNORE3.includes(e.name)) continue;
16002
16837
  if (e.isSymbolicLink()) continue;
16003
- const full = path26.join(dir, e.name);
16838
+ const rel = relPrefix ? `${relPrefix}/${e.name}` : e.name;
16839
+ const full = path27.join(dir, e.name);
16004
16840
  if (e.isDirectory()) {
16005
- subdirs.push(full);
16841
+ if (isGitIgnored(rel, true)) continue;
16842
+ subdirs.push({ full, rel });
16006
16843
  } else if (e.isFile()) {
16844
+ if (isGitIgnored(rel, false)) continue;
16007
16845
  files.push({ full, name: e.name });
16008
16846
  }
16009
16847
  }
16010
16848
  await mapWithConcurrency(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
16011
- await mapWithConcurrency(subdirs, Math.min(16, NATIVE_SCAN_CONCURRENCY), walk2);
16849
+ await mapWithConcurrency(
16850
+ subdirs,
16851
+ Math.min(16, NATIVE_SCAN_CONCURRENCY),
16852
+ ({ full, rel }) => walk2(full, rel)
16853
+ );
16012
16854
  };
16013
- await walk2(base);
16855
+ await walk2(base, "");
16014
16856
  return {
16015
16857
  matches,
16016
16858
  count: total,
@@ -16024,7 +16866,10 @@ init_spawn_stream();
16024
16866
  init_util();
16025
16867
  init_legacy_bridge();
16026
16868
  import { join as join21 } from "node:path";
16027
- import { detectPackageEcosystem, recordPackageAction } from "@wrongstack/core";
16869
+ import {
16870
+ detectEcosystem as detectPackageEcosystem,
16871
+ recordPackageAction
16872
+ } from "@wrongstack/core/coordination";
16028
16873
  var installTool = {
16029
16874
  name: "install",
16030
16875
  category: "Package Management",
@@ -16210,7 +17055,7 @@ function resolveManifestPath(cwd, pkgManager) {
16210
17055
  // src/json.ts
16211
17056
  init_util();
16212
17057
  import * as fs22 from "node:fs/promises";
16213
- import { deepMerge as deepMergeCore } from "@wrongstack/core";
17058
+ import { deepMerge as deepMergeCore } from "@wrongstack/core/utils";
16214
17059
  var jsonTool = {
16215
17060
  name: "json",
16216
17061
  category: "Data",
@@ -16588,56 +17433,56 @@ function jmespathSearch(data, query) {
16588
17433
  }
16589
17434
  function validateJsonSchema(data, schema) {
16590
17435
  const errors = [];
16591
- function check(value, s, path34) {
17436
+ function check(value, s, path35) {
16592
17437
  if (s["type"]) {
16593
17438
  const expectedType = s["type"];
16594
17439
  const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
16595
17440
  if (expectedType === "integer") {
16596
- if (!Number.isInteger(value)) errors.push(`${path34}: expected integer, got ${actualType}`);
17441
+ if (!Number.isInteger(value)) errors.push(`${path35}: expected integer, got ${actualType}`);
16597
17442
  } else if (expectedType !== actualType) {
16598
- errors.push(`${path34}: expected ${expectedType}, got ${actualType}`);
17443
+ errors.push(`${path35}: expected ${expectedType}, got ${actualType}`);
16599
17444
  }
16600
17445
  }
16601
17446
  if (typeof value === "string" && s["format"] === "uri" && value) {
16602
17447
  try {
16603
17448
  new URL(value);
16604
17449
  } catch {
16605
- errors.push(`${path34}: not a valid URI`);
17450
+ errors.push(`${path35}: not a valid URI`);
16606
17451
  }
16607
17452
  }
16608
17453
  if (typeof value === "string" && s["pattern"]) {
16609
17454
  const re = new RegExp(s["pattern"]);
16610
- if (!re.test(value)) errors.push(`${path34}: does not match pattern ${s["pattern"]}`);
17455
+ if (!re.test(value)) errors.push(`${path35}: does not match pattern ${s["pattern"]}`);
16611
17456
  }
16612
17457
  if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
16613
- errors.push(`${path34}: string too short (min ${s["minLength"]})`);
17458
+ errors.push(`${path35}: string too short (min ${s["minLength"]})`);
16614
17459
  }
16615
17460
  if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
16616
- errors.push(`${path34}: string too long (max ${s["maxLength"]})`);
17461
+ errors.push(`${path35}: string too long (max ${s["maxLength"]})`);
16617
17462
  }
16618
17463
  if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
16619
- errors.push(`${path34}: below minimum ${s["minimum"]}`);
17464
+ errors.push(`${path35}: below minimum ${s["minimum"]}`);
16620
17465
  }
16621
17466
  if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
16622
- errors.push(`${path34}: above maximum ${s["maximum"]}`);
17467
+ errors.push(`${path35}: above maximum ${s["maximum"]}`);
16623
17468
  }
16624
17469
  if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
16625
17470
  for (let i = 0; i < value.length; i++) {
16626
- check(value[i], s["items"], `${path34}[${i}]`);
17471
+ check(value[i], s["items"], `${path35}[${i}]`);
16627
17472
  }
16628
17473
  }
16629
17474
  if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
16630
17475
  const props = s["properties"];
16631
17476
  for (const [k, propSchema] of Object.entries(props)) {
16632
- check(value[k], propSchema, `${path34}.${k}`);
17477
+ check(value[k], propSchema, `${path35}.${k}`);
16633
17478
  }
16634
17479
  }
16635
17480
  }
16636
17481
  check(data, schema, "$");
16637
17482
  return { valid: errors.length === 0, errors };
16638
17483
  }
16639
- function simpleQuery(data, path34) {
16640
- const parts = path34.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
17484
+ function simpleQuery(data, path35) {
17485
+ const parts = path35.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
16641
17486
  let current = data;
16642
17487
  for (const part of parts) {
16643
17488
  if (current === null || current === void 0) return void 0;
@@ -16688,7 +17533,8 @@ function toYaml(data, indent = 0) {
16688
17533
 
16689
17534
  // src/kanban.ts
16690
17535
  import { randomUUID } from "node:crypto";
16691
- import { deserializeTaskGraph as deserializeTaskGraph2, loadTasks as loadTasks2, serializeTaskGraph } from "@wrongstack/core";
17536
+ import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
17537
+ import { loadTasks as loadTasks2 } from "@wrongstack/core/storage";
16692
17538
  import {
16693
17539
  addCheckToTask,
16694
17540
  addColumn,
@@ -16735,20 +17581,47 @@ import {
16735
17581
  updateColumn,
16736
17582
  updateGoalMetricOnTask,
16737
17583
  updateTask,
16738
- updateTaskAssignment
17584
+ updateTaskAssignment,
17585
+ verifyTaskCompletion,
17586
+ finalizeTaskCompletion,
17587
+ assessTaskAtomicity,
17588
+ proposeTaskDecomposition
16739
17589
  } from "@wrongstack/kanban";
16740
17590
 
17591
+ // src/kanban-evidence-bridge.ts
17592
+ import { recordCompletedWorkEvidence } from "@wrongstack/core/utils";
17593
+ function kanbanEvidenceKey(boardId, taskId) {
17594
+ return `kanban:${boardId}:${taskId}`;
17595
+ }
17596
+ function kanbanEvidencePointer(boardId, taskId) {
17597
+ return `kanban://${boardId}/${taskId}#verificationReport`;
17598
+ }
17599
+ function recordKanbanVerificationEvidence(ctx, report) {
17600
+ try {
17601
+ const passed = report.checks.filter((check) => check.status === "passed").length;
17602
+ const completedAt = Date.parse(report.completedAt);
17603
+ recordCompletedWorkEvidence(ctx, {
17604
+ key: kanbanEvidenceKey(report.boardId, report.taskId),
17605
+ source: "verification",
17606
+ summary: `${report.taskTitle} \u2014 verification ${report.verdict} (${passed}/${report.checks.length} checks)`,
17607
+ ...Number.isFinite(completedAt) ? { completedAt } : {},
17608
+ evidence: kanbanEvidencePointer(report.boardId, report.taskId)
17609
+ });
17610
+ } catch {
17611
+ }
17612
+ }
17613
+
16741
17614
  // src/session-kanban.ts
16742
17615
  import { watch } from "node:fs";
16743
- import { basename as basename10, dirname as dirname13 } from "node:path";
17616
+ import { basename as basename11, dirname as dirname13 } from "node:path";
17617
+ import { GlobalMailbox } from "@wrongstack/core/coordination";
16744
17618
  import {
16745
- deserializeTaskGraph,
16746
- GlobalMailbox,
16747
17619
  loadPlan,
16748
17620
  loadTasks,
16749
17621
  mutatePlan,
16750
17622
  mutateTasks
16751
- } from "@wrongstack/core";
17623
+ } from "@wrongstack/core/storage";
17624
+ import { deserializeTaskGraph } from "@wrongstack/core/tasking";
16752
17625
  import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
16753
17626
  import {
16754
17627
  createBoard,
@@ -17062,8 +17935,10 @@ function projectSessionPlanToKanban(projectRoot, items, sessionId) {
17062
17935
  "session-plan"
17063
17936
  );
17064
17937
  }
17065
- function fireAndForget(work) {
17066
- void work.catch(() => {
17938
+ function fireAndForget(context, work) {
17939
+ void work.catch((err) => {
17940
+ const message = err instanceof Error ? err.message : String(err);
17941
+ console.warn(JSON.stringify({ level: "warn", event: "session-kanban", context, message, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
17067
17942
  });
17068
17943
  }
17069
17944
  function broadcastTodoUpdate(context, todos) {
@@ -17137,7 +18012,10 @@ function attachSessionKanbanMirror(context) {
17137
18012
  if (currentSessionId === registeredSessionId) return;
17138
18013
  if (registeredSessionId) {
17139
18014
  releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
17140
- fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));
18015
+ fireAndForget(
18016
+ "cleanup-board",
18017
+ cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId)
18018
+ );
17141
18019
  }
17142
18020
  registeredSessionId = currentSessionId;
17143
18021
  if (registeredSessionId) {
@@ -17187,7 +18065,7 @@ function attachSessionKanbanMirror(context) {
17187
18065
  (_event, filename) => {
17188
18066
  if (filename?.toString() !== boardFileName) return;
17189
18067
  if (boardTimer) clearTimeout(boardTimer);
17190
- boardTimer = setTimeout(() => fireAndForget(refreshBoard()), 60);
18068
+ boardTimer = setTimeout(() => fireAndForget("refresh-board", refreshBoard()), 60);
17191
18069
  }
17192
18070
  );
17193
18071
  const touchPresence = () => touchKanbanPresence(context.projectRoot, board.id, {
@@ -17195,9 +18073,9 @@ function attachSessionKanbanMirror(context) {
17195
18073
  agentId: context.agentId,
17196
18074
  agentName: context.agentName
17197
18075
  });
17198
- fireAndForget(touchPresence());
18076
+ fireAndForget("touch-presence", touchPresence());
17199
18077
  if (presenceTimer) clearInterval(presenceTimer);
17200
- presenceTimer = setInterval(() => fireAndForget(touchPresence()), 6e4);
18078
+ presenceTimer = setInterval(() => fireAndForget("touch-presence", touchPresence()), 6e4);
17201
18079
  presenceTimer.unref?.();
17202
18080
  boardWatcher.on("error", () => {
17203
18081
  boardWatcher?.close();
@@ -17222,11 +18100,11 @@ function attachSessionKanbanMirror(context) {
17222
18100
  const name = filename?.toString();
17223
18101
  const currentPlanPath = context.meta["plan.path"];
17224
18102
  const currentTaskPath = context.meta["task.path"];
17225
- const planName = typeof currentPlanPath === "string" ? basename10(currentPlanPath) : "";
17226
- const taskName = typeof currentTaskPath === "string" ? basename10(currentTaskPath) : "";
18103
+ const planName = typeof currentPlanPath === "string" ? basename11(currentPlanPath) : "";
18104
+ const taskName = typeof currentTaskPath === "string" ? basename11(currentTaskPath) : "";
17227
18105
  if (name && name !== planName && name !== taskName) return;
17228
18106
  if (timer) clearTimeout(timer);
17229
- timer = setTimeout(() => fireAndForget(refreshFiles()), 60);
18107
+ timer = setTimeout(() => fireAndForget("refresh-files", refreshFiles()), 60);
17230
18108
  });
17231
18109
  watcher.on("error", () => watcher?.close());
17232
18110
  } catch {
@@ -17246,13 +18124,13 @@ function attachSessionKanbanMirror(context) {
17246
18124
  if (change.kind === "meta_set" && (change.key === "plan.path" || change.key === "task.path")) {
17247
18125
  syncActiveSessionRegistration();
17248
18126
  configureWatcher();
17249
- fireAndForget(ensureSessionKanbanBoard(context.projectRoot, sessionId()));
17250
- fireAndForget(configureBoardWatcher());
17251
- fireAndForget(refreshFiles());
18127
+ fireAndForget("ensure-board", ensureSessionKanbanBoard(context.projectRoot, sessionId()));
18128
+ fireAndForget("configure-watcher", configureBoardWatcher());
18129
+ fireAndForget("refresh-files", refreshFiles());
17252
18130
  }
17253
18131
  });
17254
18132
  configureWatcher();
17255
- fireAndForget(configureBoardWatcher());
18133
+ fireAndForget("configure-watcher", configureBoardWatcher());
17256
18134
  const detach = () => {
17257
18135
  unsubscribe();
17258
18136
  if (timer) clearTimeout(timer);
@@ -17263,7 +18141,10 @@ function attachSessionKanbanMirror(context) {
17263
18141
  bindings.delete(context);
17264
18142
  if (attachedProjectRoot && registeredSessionId) {
17265
18143
  releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
17266
- fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));
18144
+ fireAndForget(
18145
+ "cleanup-board",
18146
+ cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId)
18147
+ );
17267
18148
  registeredSessionId = "";
17268
18149
  }
17269
18150
  };
@@ -17406,8 +18287,8 @@ async function applySessionKanbanTaskToSource(context, task, options = {}) {
17406
18287
  var kanbanTool = {
17407
18288
  name: "kanban",
17408
18289
  category: "Project",
17409
- description: "Manage and audit project-scoped multi-kanban boards stored under .wrongstack/kanbans. Managed cards enforce fully specified details and adjacent Backlog \u2192 Todo \u2192 Running \u2192 Review \u2192 Done transitions with persistent comments and evidence. Successful board access records live agent/session presence.",
17410
- usageHint: "Use this for durable project kanban state. Reassess with get_board whenever evidence changes; agents may add, update, split, merge, reprioritize, or remove tasks so the board remains a live plan. Presence includes active/last-seen session and agent metadata. For managed boards, fully fill card details, use transition_task after every material step, attach truthful evidence, and never use update_task/move_task to bypass lifecycle guards. Worker completion enters Review; only passed acceptance criteria plus review evidence allow Done.",
18290
+ description: "Manage and audit project-scoped multi-kanban boards stored under .wrongstack/kanbans. Managed cards enforce fully specified details and adjacent Backlog \u2192 Todo \u2192 Running \u2192 Review \u2192 Done transitions with persistent comments and evidence. Successful board access records live agent/session presence. Use verify_completion to validate a task against its success criteria and persist the verification report. Use split_atomic to split a task into child tasks with parent.atomic=true in a single atomic board mutation, enforcing subtree verification before the parent can complete.",
18291
+ usageHint: "Use this for durable project kanban state. Reassess with get_board whenever evidence changes; agents may add, update, split, merge, reprioritize, or remove tasks so the board remains a live plan. Presence includes active/last-seen session and agent metadata. For managed boards, fully fill card details, use transition_task after every material step, attach truthful evidence, and never use update_task/move_task to bypass lifecycle guards. Worker completion enters Review; only passed acceptance criteria plus review evidence allow Done. Use verify_completion to generate a verification report (persisted automatically) and split_atomic to atomically create child subtasks with the atomic flag pre-set.",
17411
18292
  permission: "confirm",
17412
18293
  mutating: true,
17413
18294
  capabilities: ["fs.write"],
@@ -17463,7 +18344,11 @@ var kanbanTool = {
17463
18344
  "add_check",
17464
18345
  "update_check",
17465
18346
  "add_note",
17466
- "add_link"
18347
+ "add_link",
18348
+ "verify_completion",
18349
+ "split_atomic",
18350
+ "assess_atomicity",
18351
+ "propose_decomposition"
17467
18352
  ]
17468
18353
  },
17469
18354
  boardId: { type: "string" },
@@ -17518,6 +18403,7 @@ var kanbanTool = {
17518
18403
  runTaskId: { type: "string" },
17519
18404
  lastResult: { type: "string" },
17520
18405
  error: { type: "string" },
18406
+ expectedLeaseId: { type: "string" },
17521
18407
  assignmentStatus: {
17522
18408
  type: "string",
17523
18409
  enum: ["assigned", "queued", "running", "completed", "failed", "cancelled"]
@@ -17552,6 +18438,7 @@ var kanbanTool = {
17552
18438
  lastFailureKind: { type: "string" },
17553
18439
  dependsOn: { type: "array", items: { type: "string" } },
17554
18440
  estimatedHours: { type: "number" },
18441
+ actualHours: { type: "number" },
17555
18442
  taskGraph: { type: "object" },
17556
18443
  graphId: { type: "string" },
17557
18444
  specId: { type: "string" },
@@ -17597,7 +18484,24 @@ var kanbanTool = {
17597
18484
  includeCompletedTasks: { type: "boolean" },
17598
18485
  preserveAssignment: { type: "boolean" },
17599
18486
  preserveDependencies: { type: "boolean" },
17600
- moveTasksToColumnId: { type: "string" }
18487
+ moveTasksToColumnId: { type: "string" },
18488
+ atomicityMode: { type: "string", enum: ["off", "assess", "enforce"] },
18489
+ atomicityDecomposition: { type: "string", enum: ["auto", "propose"] },
18490
+ gateEnforcement: { type: "string", enum: ["strict", "soft", "off"] },
18491
+ subtasks: {
18492
+ type: "array",
18493
+ minItems: 2,
18494
+ items: {
18495
+ type: "object",
18496
+ properties: {
18497
+ title: { type: "string" },
18498
+ description: { type: "string" },
18499
+ successCriteria: { type: "array", items: { type: "string" } },
18500
+ dependsOnIndex: { type: "array", items: { type: "number" } }
18501
+ },
18502
+ required: ["title"]
18503
+ }
18504
+ }
17601
18505
  },
17602
18506
  required: ["action"]
17603
18507
  },
@@ -17637,7 +18541,14 @@ var kanbanTool = {
17637
18541
  title: input.title,
17638
18542
  ...input.description !== void 0 ? { description: input.description } : {},
17639
18543
  ...input.tags !== void 0 ? { tags: input.tags } : {},
17640
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {}
18544
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
18545
+ ...input.atomicityMode !== void 0 ? {
18546
+ atomicity: {
18547
+ mode: input.atomicityMode,
18548
+ decomposition: input.atomicityDecomposition ?? "propose"
18549
+ }
18550
+ } : {},
18551
+ ...input.gateEnforcement !== void 0 ? { completionGate: { enforcement: input.gateEnforcement } } : {}
17641
18552
  });
17642
18553
  return { ok: true, message: `Board created: ${board.title}`, board };
17643
18554
  }
@@ -17646,7 +18557,14 @@ var kanbanTool = {
17646
18557
  const board = await updateBoard2(projectRoot, input.boardId, {
17647
18558
  ...input.title !== void 0 ? { title: input.title } : {},
17648
18559
  ...input.description !== void 0 ? { description: input.description } : {},
17649
- ...input.tags !== void 0 ? { tags: input.tags } : {}
18560
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
18561
+ ...input.atomicityMode !== void 0 ? {
18562
+ atomicity: {
18563
+ mode: input.atomicityMode,
18564
+ decomposition: input.atomicityDecomposition ?? "propose"
18565
+ }
18566
+ } : {},
18567
+ ...input.gateEnforcement !== void 0 ? { completionGate: { enforcement: input.gateEnforcement } } : {}
17650
18568
  });
17651
18569
  return board ? okBoard(board, "Board updated.") : fail("Board not found.");
17652
18570
  }
@@ -17854,30 +18772,18 @@ var kanbanTool = {
17854
18772
  case "add_task": {
17855
18773
  if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
17856
18774
  const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
17857
- return result2 ? okTask(result2.board, result2.task, "Task added.") : fail("Board not found.");
18775
+ if (!result2) return fail("Board not found.");
18776
+ return okTask(
18777
+ result2.board,
18778
+ result2.task,
18779
+ `Task added.${atomicityNudge(result2.task)}`
18780
+ );
17858
18781
  }
17859
18782
  case "split_task": {
17860
18783
  if (!input.boardId || !input.taskId || !input.childTitles?.length) {
17861
18784
  return fail("split_task requires boardId, taskId, and childTitles.");
17862
18785
  }
17863
- const result2 = await splitTask(projectRoot, input.boardId, input.taskId, {
17864
- titles: input.childTitles,
17865
- ...input.targetColumnId !== void 0 ? { columnId: input.targetColumnId } : {},
17866
- ...input.inheritAssignment !== void 0 ? { inheritAssignment: input.inheritAssignment } : {},
17867
- ...input.inheritLabels !== void 0 ? { inheritLabels: input.inheritLabels } : {},
17868
- ...input.inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria: input.inheritSuccessCriteria } : {},
17869
- ...input.inheritGoalMetrics !== void 0 ? { inheritGoalMetrics: input.inheritGoalMetrics } : {},
17870
- ...input.inheritDependencies !== void 0 ? { inheritDependencies: input.inheritDependencies } : {},
17871
- ...input.chainChildren !== void 0 ? { chainChildren: input.chainChildren } : {},
17872
- ...input.rewireDependents !== void 0 ? { rewireDependents: input.rewireDependents } : {}
17873
- });
17874
- return result2 ? {
17875
- ok: true,
17876
- message: `${result2.children.length} child task(s) created.`,
17877
- board: result2.board,
17878
- task: result2.parent,
17879
- children: result2.children
17880
- } : fail("Task not found.");
18786
+ return handleSplitTask(projectRoot, input, {});
17881
18787
  }
17882
18788
  case "merge_tasks": {
17883
18789
  if (!input.boardId || !input.taskIds?.length || !input.title) {
@@ -17951,6 +18857,19 @@ var kanbanTool = {
17951
18857
  "transition_task requires boardId, taskId, lifecycleStage, author, and transitionComment."
17952
18858
  );
17953
18859
  }
18860
+ if (input.lifecycleStage === "done") {
18861
+ const boardBefore = await getBoard2(projectRoot, input.boardId);
18862
+ const taskBefore = boardBefore ? await getTask(projectRoot, input.boardId, input.taskId) : null;
18863
+ if (boardBefore && taskBefore && !taskBefore.verificationReport && (taskBefore.atomic || Boolean(taskBefore.successCriteria?.length))) {
18864
+ const preGate = await verifyTaskCompletion(projectRoot, input.boardId, taskBefore.id, {
18865
+ persist: false
18866
+ });
18867
+ await updateTask(projectRoot, input.boardId, taskBefore.id, {
18868
+ verificationReport: preGate.report,
18869
+ successCriteria: preGate.task.successCriteria
18870
+ });
18871
+ }
18872
+ }
17954
18873
  const result2 = await transitionTask(projectRoot, input.boardId, input.taskId, {
17955
18874
  to: input.lifecycleStage,
17956
18875
  actor: input.author,
@@ -17965,6 +18884,9 @@ var kanbanTool = {
17965
18884
  } : {},
17966
18885
  patch: taskPatch(input)
17967
18886
  });
18887
+ if (result2 && input.lifecycleStage === "done" && result2.task.verificationReport) {
18888
+ recordKanbanVerificationEvidence(ctx, result2.task.verificationReport);
18889
+ }
17968
18890
  return result2 ? okTask(result2.board, result2.task, `Task advanced to ${result2.transition.to}.`) : fail("Board or task not found.");
17969
18891
  }
17970
18892
  case "move_task": {
@@ -18078,7 +19000,31 @@ var kanbanTool = {
18078
19000
  // is atomic inside updateTaskAssignment's mutateBoard lock.
18079
19001
  input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
18080
19002
  );
18081
- return board ? okBoard(board, "Assignment updated.") : fail("Task not found.");
19003
+ if (!board) return fail("Task not found.");
19004
+ if (assignmentStatus === "completed") {
19005
+ const envGate = readEnvGateEnforcement();
19006
+ const finalized = await finalizeTaskCompletion(projectRoot, board.id, input.taskId, {
19007
+ ...board.completionGate === void 0 && envGate !== void 0 ? { enforcement: envGate } : {},
19008
+ ...ctx.agentId !== void 0 ? { eventContext: { actor: ctx.agentId } } : {}
19009
+ });
19010
+ if (finalized) {
19011
+ if (finalized.gate.report) {
19012
+ recordKanbanVerificationEvidence(ctx, finalized.gate.report);
19013
+ }
19014
+ const gateSummary = {
19015
+ enforcement: finalized.gate.enforcement,
19016
+ allowed: finalized.gate.allowed,
19017
+ verdict: finalized.gate.verdict,
19018
+ issues: finalized.gate.issues.map((issue) => issue.message)
19019
+ };
19020
+ const gateMessage = finalized.gate.allowed ? `Completion gate ${finalized.gate.verdict === "skipped" ? "skipped" : "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(" | ")}`;
19021
+ return {
19022
+ ...okTask(finalized.board, finalized.task, `Assignment updated. ${gateMessage}`),
19023
+ gate: gateSummary
19024
+ };
19025
+ }
19026
+ }
19027
+ return okBoard(board, "Assignment updated.");
18082
19028
  }
18083
19029
  case "heartbeat_assignment": {
18084
19030
  if (!input.boardId || !input.taskId) {
@@ -18242,6 +19188,83 @@ var kanbanTool = {
18242
19188
  });
18243
19189
  return board ? okBoard(board, "Link added.") : fail("Task not found.");
18244
19190
  }
19191
+ case "assess_atomicity": {
19192
+ if (!input.boardId || !input.taskId) {
19193
+ return fail("assess_atomicity requires boardId and taskId.");
19194
+ }
19195
+ const result2 = await assessTaskAtomicity(projectRoot, input.boardId, input.taskId, {
19196
+ assessedBy: "agent",
19197
+ ...ctx.agentId !== void 0 ? { eventContext: { actor: ctx.agentId } } : {}
19198
+ });
19199
+ if (!result2) return fail("Task not found.");
19200
+ const failing = result2.assessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason);
19201
+ const guidance = result2.assessment.verdict === "needs_decomposition" ? ` This task should be split before dispatch \u2014 call propose_decomposition with 2+ subtasks (each with one verifiable success criterion). Reasons: ${failing.join(" | ")}` : result2.assessment.verdict === "composite" ? " Container task: work happens in its children; it is verified via subtask aggregation." : "";
19202
+ return okTask(
19203
+ result2.board,
19204
+ result2.task,
19205
+ `Atomicity verdict: ${result2.assessment.verdict} (score ${result2.assessment.score}).${guidance}`
19206
+ );
19207
+ }
19208
+ case "propose_decomposition": {
19209
+ if (!input.boardId || !input.taskId || !input.subtasks?.length) {
19210
+ return fail("propose_decomposition requires boardId, taskId, and subtasks (2+).");
19211
+ }
19212
+ if (input.subtasks.length < 2) {
19213
+ return fail("propose_decomposition requires at least two subtasks.");
19214
+ }
19215
+ const invalid = input.subtasks.find(
19216
+ (subtask) => typeof subtask?.title !== "string" || !subtask.title.trim()
19217
+ );
19218
+ if (invalid) return fail("Every proposed subtask needs a non-blank title.");
19219
+ const result2 = await proposeTaskDecomposition(
19220
+ projectRoot,
19221
+ input.boardId,
19222
+ input.taskId,
19223
+ {
19224
+ subtasks: input.subtasks,
19225
+ ...input.note !== void 0 ? { rationale: input.note } : {},
19226
+ ...ctx.agentId !== void 0 ? { proposedBy: ctx.agentId } : {}
19227
+ },
19228
+ ctx.agentId !== void 0 ? { actor: ctx.agentId } : {}
19229
+ );
19230
+ if (!result2) return fail("Task not found.");
19231
+ const message = result2.proposal.status === "applied" ? `Decomposition applied: ${result2.proposal.appliedChildTaskIds?.length ?? 0} child tasks created (parent marked atomic).` : 'Decomposition proposal recorded \u2014 awaiting approval (board policy is "propose"). It can be approved from the WebUI or via update_task.';
19232
+ return okTask(result2.board, result2.task, message);
19233
+ }
19234
+ case "verify_completion": {
19235
+ if (!input.boardId || !input.taskId) {
19236
+ return fail("verify_completion requires boardId and taskId.");
19237
+ }
19238
+ const verResult = await verifyTaskCompletion(projectRoot, input.boardId, input.taskId);
19239
+ const persistedBoard = await updateTask(projectRoot, input.boardId, input.taskId, {
19240
+ verificationReport: verResult.report,
19241
+ successCriteria: verResult.task.successCriteria
19242
+ });
19243
+ if (!persistedBoard) {
19244
+ return {
19245
+ ok: false,
19246
+ verdict: verResult.report.verdict,
19247
+ message: `Verification succeeded but persist failed: ${verResult.report.markdownSummary}. Board may be stale \u2014 re-run verify_completion.`,
19248
+ board: verResult.board
19249
+ };
19250
+ }
19251
+ recordKanbanVerificationEvidence(ctx, verResult.report);
19252
+ const freshTask = persistedBoard.tasks?.find((t) => t.id === input.taskId);
19253
+ const deterministicVerdicts = ["passed", "failed", "needs_human", "incomplete"];
19254
+ return {
19255
+ ok: deterministicVerdicts.includes(verResult.report.verdict),
19256
+ verdict: verResult.report.verdict,
19257
+ message: verResult.report.markdownSummary,
19258
+ board: persistedBoard,
19259
+ task: freshTask ?? verResult.task
19260
+ };
19261
+ }
19262
+ case "split_atomic": {
19263
+ if (!input.boardId || !input.taskId || !input.childTitles?.length) {
19264
+ return fail("split_atomic requires boardId, taskId, and childTitles (at least one).");
19265
+ }
19266
+ return handleSplitTask(projectRoot, input, { atomic: true });
19267
+ }
18245
19268
  default:
18246
19269
  return fail(`Unknown kanban action: ${input.action}`);
18247
19270
  }
@@ -18252,6 +19275,15 @@ var kanbanTool = {
18252
19275
  }
18253
19276
  }
18254
19277
  };
19278
+ function atomicityNudge(task) {
19279
+ if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
19280
+ const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
19281
+ return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
19282
+ }
19283
+ function readEnvGateEnforcement() {
19284
+ const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
19285
+ return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
19286
+ }
18255
19287
  function fail(message) {
18256
19288
  return { ok: false, message };
18257
19289
  }
@@ -18261,6 +19293,50 @@ function okBoard(board, message = "Board loaded.") {
18261
19293
  function okTask(board, task, message) {
18262
19294
  return { ok: true, message, board, task };
18263
19295
  }
19296
+ async function handleSplitTask(projectRoot, input, extraSplitOptions) {
19297
+ const boardId = input.boardId;
19298
+ const taskId = input.taskId;
19299
+ const childTitles = input.childTitles;
19300
+ if (!boardId || !taskId || !childTitles?.length) {
19301
+ return fail("split requires boardId, taskId, and at least one childTitles.");
19302
+ }
19303
+ const {
19304
+ targetColumnId,
19305
+ inheritAssignment,
19306
+ inheritLabels,
19307
+ inheritSuccessCriteria,
19308
+ inheritGoalMetrics,
19309
+ inheritDependencies,
19310
+ chainChildren,
19311
+ rewireDependents
19312
+ } = input;
19313
+ const result = await splitTask(projectRoot, boardId, taskId, {
19314
+ titles: childTitles,
19315
+ ...extraSplitOptions,
19316
+ ...targetColumnId !== void 0 ? { columnId: targetColumnId } : {},
19317
+ ...inheritAssignment !== void 0 ? { inheritAssignment } : {},
19318
+ ...inheritLabels !== void 0 ? { inheritLabels } : {},
19319
+ ...inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria } : {},
19320
+ ...inheritGoalMetrics !== void 0 ? { inheritGoalMetrics } : {},
19321
+ ...inheritDependencies !== void 0 ? { inheritDependencies } : {},
19322
+ ...chainChildren !== void 0 ? { chainChildren } : {},
19323
+ ...rewireDependents !== void 0 ? { rewireDependents } : {}
19324
+ });
19325
+ if (!result) return fail("Task not found.");
19326
+ const freshParent = result.board.tasks?.find((t) => t.id === taskId);
19327
+ if (!freshParent) {
19328
+ return fail(
19329
+ `Split succeeded but parent ${taskId} not found in returned board. Children: [${result.children.map((c) => c.id).join(", ")}].`
19330
+ );
19331
+ }
19332
+ return {
19333
+ ok: true,
19334
+ message: `${result.children.length} child task(s) created.`,
19335
+ board: result.board,
19336
+ task: freshParent,
19337
+ children: result.children
19338
+ };
19339
+ }
18264
19340
  async function requireBoard(projectRoot, boardId) {
18265
19341
  return boardId ? getBoard2(projectRoot, boardId) : null;
18266
19342
  }
@@ -18362,7 +19438,8 @@ function taskPatch(input) {
18362
19438
  labels: input.labels,
18363
19439
  assignedAgent: input.agentId,
18364
19440
  ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
18365
- ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {}
19441
+ ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
19442
+ ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
18366
19443
  };
18367
19444
  }
18368
19445
  function assignmentInput(input) {
@@ -18544,7 +19621,7 @@ async function detectLinter(cwd) {
18544
19621
 
18545
19622
  // src/logs.ts
18546
19623
  import { spawn as spawn11 } from "node:child_process";
18547
- import { buildChildEnv as buildChildEnv6 } from "@wrongstack/core";
19624
+ import { buildChildEnv as buildChildEnv6 } from "@wrongstack/core/utils";
18548
19625
  init_util();
18549
19626
  var logsTool = {
18550
19627
  name: "logs",
@@ -18683,7 +19760,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
18683
19760
  }
18684
19761
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
18685
19762
  var MAX_TAIL_LINES = 1e5;
18686
- async function fileLogs(path34, lines, filterRe, stream) {
19763
+ async function fileLogs(path35, lines, filterRe, stream) {
18687
19764
  const { createInterface } = await import("node:readline");
18688
19765
  const { createReadStream: createReadStream2 } = await import("node:fs");
18689
19766
  const entries = [];
@@ -18692,7 +19769,7 @@ async function fileLogs(path34, lines, filterRe, stream) {
18692
19769
  let writeIdx = 0;
18693
19770
  let totalLines = 0;
18694
19771
  const rl = createInterface({
18695
- input: createReadStream2(path34),
19772
+ input: createReadStream2(path35),
18696
19773
  crlfDelay: Number.POSITIVE_INFINITY
18697
19774
  });
18698
19775
  for await (const line of rl) {
@@ -18713,7 +19790,7 @@ async function fileLogs(path34, lines, filterRe, stream) {
18713
19790
  if (parsed) entries.push(parsed);
18714
19791
  }
18715
19792
  return {
18716
- source: path34,
19793
+ source: path35,
18717
19794
  entries,
18718
19795
  total: entries.length,
18719
19796
  truncated: totalLines > effLines,
@@ -18760,7 +19837,7 @@ function parseLine(line) {
18760
19837
  init_util();
18761
19838
  init_win32_resolve();
18762
19839
  import { spawn as spawn12 } from "node:child_process";
18763
- import { buildChildEnv as buildChildEnv7 } from "@wrongstack/core";
19840
+ import { buildChildEnv as buildChildEnv7 } from "@wrongstack/core/utils";
18764
19841
  var outdatedTool = {
18765
19842
  name: "outdated",
18766
19843
  category: "Package Management",
@@ -18945,8 +20022,8 @@ init_util();
18945
20022
  import { spawn as spawn13 } from "node:child_process";
18946
20023
  import * as fs23 from "node:fs/promises";
18947
20024
  import * as os8 from "node:os";
18948
- import * as path27 from "node:path";
18949
- import { buildChildEnv as buildChildEnv8 } from "@wrongstack/core";
20025
+ import * as path28 from "node:path";
20026
+ import { buildChildEnv as buildChildEnv8 } from "@wrongstack/core/utils";
18950
20027
  var patchTool = {
18951
20028
  name: "patch",
18952
20029
  category: "Filesystem",
@@ -18981,9 +20058,9 @@ var patchTool = {
18981
20058
  for (const t of targets) {
18982
20059
  const stripped = stripPathComponents(t, strip);
18983
20060
  if (!stripped) continue;
18984
- const candidate = path27.resolve(dir, stripped);
18985
- const rel = path27.relative(ctx.projectRoot, candidate);
18986
- if (rel.startsWith("..") || path27.isAbsolute(rel)) {
20061
+ const candidate = path28.resolve(dir, stripped);
20062
+ const rel = path28.relative(ctx.projectRoot, candidate);
20063
+ if (rel.startsWith("..") || path28.isAbsolute(rel)) {
18987
20064
  return {
18988
20065
  applied: 0,
18989
20066
  rejected: 1,
@@ -19000,11 +20077,11 @@ var patchTool = {
19000
20077
  beforeContents.set(target, await readTextForTracking(target));
19001
20078
  }
19002
20079
  }
19003
- const tmpDir = await fs23.mkdtemp(path27.join(os8.tmpdir(), ".wstack_patch_"));
20080
+ const tmpDir = await fs23.mkdtemp(path28.join(os8.tmpdir(), ".wstack_patch_"));
19004
20081
  try {
19005
20082
  await fs23.chmod(tmpDir, 448).catch(() => {
19006
20083
  });
19007
- const patchFile = path27.join(tmpDir, "in.diff");
20084
+ const patchFile = path28.join(tmpDir, "in.diff");
19008
20085
  await fs23.writeFile(patchFile, input.patch, { mode: 384 });
19009
20086
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
19010
20087
  const result = await runPatch(args, dir, opts.signal);
@@ -19110,11 +20187,11 @@ import {
19110
20187
  mutatePlan as mutatePlan2,
19111
20188
  removePlanItem,
19112
20189
  setPlanItemStatus
19113
- } from "@wrongstack/core";
20190
+ } from "@wrongstack/core/storage";
19114
20191
  import {
19115
- mutateTasks as mutateTasks2,
19116
- formatTaskList
19117
- } from "@wrongstack/core";
20192
+ mutateTasks as mutateTasks2
20193
+ } from "@wrongstack/core/storage";
20194
+ import { formatTaskList } from "@wrongstack/core/utils";
19118
20195
  import { randomUUID as randomUUID2 } from "node:crypto";
19119
20196
  var planTool = {
19120
20197
  name: "plan",
@@ -19381,7 +20458,8 @@ function mkResult(plan, ok, message, todos) {
19381
20458
  // src/read.ts
19382
20459
  init_util();
19383
20460
  import * as fs24 from "node:fs/promises";
19384
- import { FsError, toErrorMessage as toErrorMessage4, ToolValidationError as ToolValidationError5 } from "@wrongstack/core";
20461
+ import { FsError, ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
20462
+ import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
19385
20463
  var MAX_BYTES2 = 5 * 1024 * 1024;
19386
20464
  var readTool = {
19387
20465
  name: "read",
@@ -19587,20 +20665,20 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
19587
20665
  }
19588
20666
 
19589
20667
  // src/replace.ts
19590
- import { expectDefined as expectDefined8 } from "@wrongstack/core";
20668
+ import { expectDefined as expectDefined8 } from "@wrongstack/core/utils";
19591
20669
  import { spawn as spawn14 } from "node:child_process";
19592
20670
  import * as fs25 from "node:fs/promises";
19593
- import * as path28 from "node:path";
20671
+ import * as path29 from "node:path";
19594
20672
  import {
19595
20673
  atomicWrite as atomicWrite3,
19596
20674
  buildChildEnv as buildChildEnv9,
19597
- compileGlob as compileGlob5,
20675
+ compileGlob as compileGlob4,
19598
20676
  detectNewlineStyle as detectNewlineStyle2,
19599
20677
  normalizeToLf as normalizeToLf2,
19600
20678
  toStyle as toStyle2,
19601
- ToolValidationError as ToolValidationError6,
19602
20679
  unifiedDiff as unifiedDiff2
19603
- } from "@wrongstack/core";
20680
+ } from "@wrongstack/core/utils";
20681
+ import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
19604
20682
  init_util();
19605
20683
  var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
19606
20684
  var replaceTool = {
@@ -19659,7 +20737,7 @@ var replaceTool = {
19659
20737
  });
19660
20738
  }
19661
20739
  const re = compiled.regex;
19662
- const globRe = input.glob ? compileGlob5(input.glob) : null;
20740
+ const globRe = input.glob ? compileGlob4(input.glob) : null;
19663
20741
  const dryRun = input.dry_run ?? true;
19664
20742
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
19665
20743
  const fileList = await resolveFiles2(filesInput, ctx, globRe);
@@ -19679,8 +20757,8 @@ var replaceTool = {
19679
20757
  } catch {
19680
20758
  continue;
19681
20759
  }
19682
- const rel = path28.relative(realRoot, realPath);
19683
- if (rel.startsWith("..") || path28.isAbsolute(rel)) continue;
20760
+ const rel = path29.relative(realRoot, realPath);
20761
+ if (rel.startsWith("..") || path29.isAbsolute(rel)) continue;
19684
20762
  const stat18 = await fs25.stat(realPath).catch(() => null);
19685
20763
  if (!stat18?.isFile()) continue;
19686
20764
  let content;
@@ -19799,7 +20877,7 @@ function spawnRgFind(pattern, base) {
19799
20877
  }
19800
20878
  async function globNative(pattern, base, extraGlob) {
19801
20879
  const results = [];
19802
- const globRe = compileGlob5(pattern);
20880
+ const globRe = compileGlob4(pattern);
19803
20881
  const walk2 = async (dir) => {
19804
20882
  let entries;
19805
20883
  try {
@@ -19809,7 +20887,7 @@ async function globNative(pattern, base, extraGlob) {
19809
20887
  }
19810
20888
  for (const e of entries) {
19811
20889
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
19812
- const full = path28.join(dir, e.name);
20890
+ const full = path29.join(dir, e.name);
19813
20891
  try {
19814
20892
  const stat18 = await fs25.lstat(full);
19815
20893
  if (stat18.isSymbolicLink()) continue;
@@ -19836,8 +20914,8 @@ async function globNative(pattern, base, extraGlob) {
19836
20914
  // src/scaffold.ts
19837
20915
  init_util();
19838
20916
  import * as fs26 from "node:fs/promises";
19839
- import * as path29 from "node:path";
19840
- import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
20917
+ import * as path30 from "node:path";
20918
+ import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
19841
20919
  var BUILT_IN_TEMPLATES = {
19842
20920
  "npm-package": {
19843
20921
  description: "Basic npm package with ESM",
@@ -19986,16 +21064,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
19986
21064
  let filesCreated = 0;
19987
21065
  for (const [filePath, content] of Object.entries(templateFiles)) {
19988
21066
  const resolvedPath = substituteVars(filePath, name, vars);
19989
- const joinedPath = path29.join(cwd, resolvedPath);
19990
- const root = path29.resolve(ctx.projectRoot);
19991
- const target = path29.resolve(joinedPath);
19992
- const rel = path29.relative(root, target);
19993
- if (rel.startsWith("..") || path29.isAbsolute(rel)) {
21067
+ const joinedPath = path30.join(cwd, resolvedPath);
21068
+ const root = path30.resolve(ctx.projectRoot);
21069
+ const target = path30.resolve(joinedPath);
21070
+ const rel = path30.relative(root, target);
21071
+ if (rel.startsWith("..") || path30.isAbsolute(rel)) {
19994
21072
  throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
19995
21073
  }
19996
21074
  const fullPath = target;
19997
21075
  if (!dryRun) {
19998
- await fs26.mkdir(path29.dirname(fullPath), { recursive: true });
21076
+ await fs26.mkdir(path30.dirname(fullPath), { recursive: true });
19999
21077
  await atomicWrite4(fullPath, substituteVars(content, name, vars));
20000
21078
  }
20001
21079
  files.push(resolvedPath);
@@ -20024,7 +21102,8 @@ function substituteVars(content, name, vars) {
20024
21102
  }
20025
21103
 
20026
21104
  // src/search.ts
20027
- import { expectDefined as expectDefined9, FetchError as FetchError3, ToolValidationError as ToolValidationError7 } from "@wrongstack/core";
21105
+ import { FetchError as FetchError3, ToolValidationError as ToolValidationError7 } from "@wrongstack/core/types";
21106
+ import { expectDefined as expectDefined9 } from "@wrongstack/core/utils";
20028
21107
  import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
20029
21108
  var DEFAULT_NUM = 10;
20030
21109
  var MAX_RESULTS = 50;
@@ -20474,15 +21553,13 @@ var setWorkingDirTool = {
20474
21553
  import {
20475
21554
  computeTaskItemProgress,
20476
21555
  formatTaskList as formatTaskList2
20477
- } from "@wrongstack/core";
20478
- import {
20479
- mutateTasks as mutateTasks3
20480
- } from "@wrongstack/core";
21556
+ } from "@wrongstack/core/utils";
21557
+ import { mutateTasks as mutateTasks3 } from "@wrongstack/core/storage";
20481
21558
  import {
20482
21559
  addPlanItem as addPlanItem2,
20483
21560
  mutatePlan as mutatePlan3,
20484
21561
  formatPlan as formatPlan2
20485
- } from "@wrongstack/core";
21562
+ } from "@wrongstack/core/storage";
20486
21563
  import { randomUUID as randomUUID3 } from "node:crypto";
20487
21564
  function findTaskIndex(tasks, query) {
20488
21565
  const asNum = Number.parseInt(query, 10);
@@ -20852,7 +21929,7 @@ ${formatTaskList2(file.tasks)}` : file.tasks.length > 0 ? formatTaskList2(file.t
20852
21929
  init_spawn_stream();
20853
21930
  init_util();
20854
21931
  init_legacy_bridge();
20855
- import * as path30 from "node:path";
21932
+ import * as path31 from "node:path";
20856
21933
  var testTool = {
20857
21934
  name: "test",
20858
21935
  category: "Code Quality",
@@ -20959,7 +22036,7 @@ async function detectRunner(cwd) {
20959
22036
  const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
20960
22037
  for (const f of candidates) {
20961
22038
  try {
20962
- await stat18(path30.join(cwd, f));
22039
+ await stat18(path31.join(cwd, f));
20963
22040
  if (f.includes("vitest")) return "vitest";
20964
22041
  if (f.includes("jest")) return "jest";
20965
22042
  if (f.includes("mocha")) return "mocha";
@@ -21036,7 +22113,7 @@ function parseResult(runner, result, duration) {
21036
22113
  }
21037
22114
 
21038
22115
  // src/todo.ts
21039
- import { loadPlan as loadPlan2, loadTasks as loadTasks3, savePlan, saveTasks, setPlanItemStatus as setPlanItemStatus2 } from "@wrongstack/core";
22116
+ import { loadPlan as loadPlan2, loadTasks as loadTasks3, savePlan, saveTasks, setPlanItemStatus as setPlanItemStatus2 } from "@wrongstack/core/storage";
21040
22117
  var todoTool = {
21041
22118
  name: "todo",
21042
22119
  category: "Session",
@@ -21428,22 +22505,10 @@ var toolUseTool = {
21428
22505
 
21429
22506
  // src/tree.ts
21430
22507
  init_util();
21431
- import { expectDefined as expectDefined10 } from "@wrongstack/core";
22508
+ import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
21432
22509
  import * as fs28 from "node:fs/promises";
21433
- import * as path31 from "node:path";
21434
- var DEFAULT_IGNORE5 = [
21435
- "node_modules",
21436
- ".git",
21437
- "dist",
21438
- "build",
21439
- ".next",
21440
- "coverage",
21441
- "__pycache__",
21442
- ".wrongstack",
21443
- ".ssh",
21444
- ".gnupg",
21445
- ".aws"
21446
- ];
22510
+ import * as path32 from "node:path";
22511
+ var DEFAULT_IGNORE5 = [...DEFAULT_WALK_IGNORE_DIRS4, ".wrongstack", ".ssh", ".gnupg", ".aws"];
21447
22512
  var treeTool = {
21448
22513
  name: "tree",
21449
22514
  category: "Filesystem",
@@ -21601,7 +22666,7 @@ async function walkDir(dir, depth, opts) {
21601
22666
  opts.lines.push(opts.prefix + branch + displayName);
21602
22667
  if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
21603
22668
  const childPrefix = opts.prefix + connector;
21604
- await walkDir(path31.join(dir, entry.name), depth + 1, {
22669
+ await walkDir(path32.join(dir, entry.name), depth + 1, {
21605
22670
  ...opts,
21606
22671
  prefix: childPrefix,
21607
22672
  isLast
@@ -21614,7 +22679,7 @@ async function walkDir(dir, depth, opts) {
21614
22679
  init_spawn_stream();
21615
22680
  init_util();
21616
22681
  init_legacy_bridge();
21617
- import * as path32 from "node:path";
22682
+ import * as path33 from "node:path";
21618
22683
  var typecheckTool = {
21619
22684
  name: "typecheck",
21620
22685
  category: "Code Quality",
@@ -21717,8 +22782,8 @@ async function findTsConfig(cwd) {
21717
22782
  const candidates = ["tsconfig.json", "tsconfig.base.json"];
21718
22783
  for (const f of candidates) {
21719
22784
  try {
21720
- const s = await stat18(path32.join(cwd, f));
21721
- if (s.isFile()) return path32.join(cwd, f);
22785
+ const s = await stat18(path33.join(cwd, f));
22786
+ if (s.isFile()) return path33.join(cwd, f);
21722
22787
  } catch {
21723
22788
  }
21724
22789
  }
@@ -21727,7 +22792,8 @@ async function findTsConfig(cwd) {
21727
22792
 
21728
22793
  // src/write.ts
21729
22794
  import * as fs29 from "node:fs/promises";
21730
- import { atomicWrite as atomicWrite5, ToolValidationError as ToolValidationError8, unifiedDiff as unifiedDiff3 } from "@wrongstack/core";
22795
+ import { ToolValidationError as ToolValidationError8 } from "@wrongstack/core/types";
22796
+ import { atomicWrite as atomicWrite5, unifiedDiff as unifiedDiff3 } from "@wrongstack/core/utils";
21731
22797
  init_util();
21732
22798
  var writeTool = {
21733
22799
  name: "write",
@@ -21951,7 +23017,7 @@ init_circuit_breaker();
21951
23017
  init_languages();
21952
23018
 
21953
23019
  // src/memory.ts
21954
- import { ToolValidationError as ToolValidationError9 } from "@wrongstack/core";
23020
+ import { ToolValidationError as ToolValidationError9 } from "@wrongstack/core/types";
21955
23021
  function rememberTool(memory) {
21956
23022
  return {
21957
23023
  name: "remember",
@@ -22260,6 +23326,88 @@ var ProcessGuardian = class {
22260
23326
  isRunning = false;
22261
23327
  instanceId;
22262
23328
  sigtermCount = 0;
23329
+ processHandlersInstalled = false;
23330
+ onProcessExit = (code) => {
23331
+ console.log(JSON.stringify({
23332
+ level: "info",
23333
+ event: "process_guardian.process_exiting",
23334
+ pid: process.pid,
23335
+ code,
23336
+ instanceId: this.instanceId
23337
+ }));
23338
+ this.stop();
23339
+ };
23340
+ onUncaughtException = (err) => {
23341
+ console.error(JSON.stringify({
23342
+ level: "error",
23343
+ event: "process_guardian.uncaught_exception",
23344
+ error: err.message,
23345
+ stack: err.stack,
23346
+ instanceId: this.instanceId,
23347
+ fatal: true
23348
+ }));
23349
+ this.stop();
23350
+ process.exit(1);
23351
+ };
23352
+ onUnhandledRejection = (reason) => {
23353
+ const err = reason instanceof Error ? { message: reason.message, stack: reason.stack } : { value: String(reason) };
23354
+ console.error(JSON.stringify({
23355
+ level: "error",
23356
+ event: "process_guardian.unhandled_rejection",
23357
+ ...err,
23358
+ instanceId: this.instanceId,
23359
+ fatal: true
23360
+ }));
23361
+ this.stop();
23362
+ process.exit(1);
23363
+ };
23364
+ onSigterm = () => {
23365
+ this.sigtermCount++;
23366
+ const threshold = this.config.sigtermThreshold;
23367
+ if (threshold === Infinity) {
23368
+ console.log(JSON.stringify({
23369
+ level: "warn",
23370
+ event: "process_guardian.sigterm_ignored",
23371
+ pid: process.pid,
23372
+ instanceId: this.instanceId,
23373
+ sigtermCount: this.sigtermCount,
23374
+ message: "SIGTERM received but sigtermThreshold is Infinity \u2014 ignoring (use graceful shutdown instead)"
23375
+ }));
23376
+ return;
23377
+ }
23378
+ if (this.sigtermCount < threshold) {
23379
+ console.log(JSON.stringify({
23380
+ level: "warn",
23381
+ event: "process_guardian.sigterm_deferred",
23382
+ pid: process.pid,
23383
+ instanceId: this.instanceId,
23384
+ sigtermCount: this.sigtermCount,
23385
+ threshold,
23386
+ message: `SIGTERM received (${this.sigtermCount}/${threshold}) \u2014 ignoring until threshold is reached`
23387
+ }));
23388
+ return;
23389
+ }
23390
+ console.log(JSON.stringify({
23391
+ level: "warn",
23392
+ event: "process_guardian.sigterm_exiting",
23393
+ pid: process.pid,
23394
+ instanceId: this.instanceId,
23395
+ sigtermCount: this.sigtermCount,
23396
+ threshold,
23397
+ message: `SIGTERM threshold (${threshold}) reached \u2014 shutting down gracefully`
23398
+ }));
23399
+ this.stop();
23400
+ process.exit(0);
23401
+ };
23402
+ onSighup = () => {
23403
+ console.log(JSON.stringify({
23404
+ level: "warn",
23405
+ event: "process_guardian.sighup_received",
23406
+ pid: process.pid,
23407
+ instanceId: this.instanceId,
23408
+ message: "SIGHUP received but ignored - WrongStack continues running"
23409
+ }));
23410
+ };
22263
23411
  constructor(config = {}) {
22264
23412
  this.registry = getPersistentProcessRegistry();
22265
23413
  this.config = {
@@ -22298,6 +23446,7 @@ var ProcessGuardian = class {
22298
23446
  * Stop the guardian gracefully.
22299
23447
  */
22300
23448
  stop() {
23449
+ this.removeProcessHandlers();
22301
23450
  if (!this.isRunning) return;
22302
23451
  this.isRunning = false;
22303
23452
  if (this.heartbeatTimer) {
@@ -22377,87 +23526,22 @@ var ProcessGuardian = class {
22377
23526
  * Set up process-level event handlers.
22378
23527
  */
22379
23528
  setupProcessHandlers() {
22380
- process.on("exit", (code) => {
22381
- console.log(JSON.stringify({
22382
- level: "info",
22383
- event: "process_guardian.process_exiting",
22384
- pid: process.pid,
22385
- code,
22386
- instanceId: this.instanceId
22387
- }));
22388
- this.stop();
22389
- });
22390
- process.on("uncaughtException", (err) => {
22391
- console.error(JSON.stringify({
22392
- level: "error",
22393
- event: "process_guardian.uncaught_exception",
22394
- error: err.message,
22395
- stack: err.stack,
22396
- instanceId: this.instanceId,
22397
- fatal: true
22398
- }));
22399
- this.stop();
22400
- process.exit(1);
22401
- });
22402
- process.on("unhandledRejection", (reason) => {
22403
- const err = reason instanceof Error ? { message: reason.message, stack: reason.stack } : { value: String(reason) };
22404
- console.error(JSON.stringify({
22405
- level: "error",
22406
- event: "process_guardian.unhandled_rejection",
22407
- ...err,
22408
- instanceId: this.instanceId,
22409
- fatal: true
22410
- }));
22411
- this.stop();
22412
- process.exit(1);
22413
- });
22414
- process.on("SIGTERM", () => {
22415
- this.sigtermCount++;
22416
- const threshold = this.config.sigtermThreshold;
22417
- if (threshold === Infinity) {
22418
- console.log(JSON.stringify({
22419
- level: "warn",
22420
- event: "process_guardian.sigterm_ignored",
22421
- pid: process.pid,
22422
- instanceId: this.instanceId,
22423
- sigtermCount: this.sigtermCount,
22424
- message: "SIGTERM received but sigtermThreshold is Infinity \u2014 ignoring (use graceful shutdown instead)"
22425
- }));
22426
- return;
22427
- }
22428
- if (this.sigtermCount < threshold) {
22429
- console.log(JSON.stringify({
22430
- level: "warn",
22431
- event: "process_guardian.sigterm_deferred",
22432
- pid: process.pid,
22433
- instanceId: this.instanceId,
22434
- sigtermCount: this.sigtermCount,
22435
- threshold,
22436
- message: `SIGTERM received (${this.sigtermCount}/${threshold}) \u2014 ignoring until threshold is reached`
22437
- }));
22438
- return;
22439
- }
22440
- console.log(JSON.stringify({
22441
- level: "warn",
22442
- event: "process_guardian.sigterm_exiting",
22443
- pid: process.pid,
22444
- instanceId: this.instanceId,
22445
- sigtermCount: this.sigtermCount,
22446
- threshold,
22447
- message: `SIGTERM threshold (${threshold}) reached \u2014 shutting down gracefully`
22448
- }));
22449
- this.stop();
22450
- process.exit(0);
22451
- });
22452
- process.on("SIGHUP", () => {
22453
- console.log(JSON.stringify({
22454
- level: "warn",
22455
- event: "process_guardian.sighup_received",
22456
- pid: process.pid,
22457
- instanceId: this.instanceId,
22458
- message: "SIGHUP received but ignored - WrongStack continues running"
22459
- }));
22460
- });
23529
+ if (this.processHandlersInstalled) return;
23530
+ this.processHandlersInstalled = true;
23531
+ process.on("exit", this.onProcessExit);
23532
+ process.on("uncaughtException", this.onUncaughtException);
23533
+ process.on("unhandledRejection", this.onUnhandledRejection);
23534
+ process.on("SIGTERM", this.onSigterm);
23535
+ process.on("SIGHUP", this.onSighup);
23536
+ }
23537
+ removeProcessHandlers() {
23538
+ if (!this.processHandlersInstalled) return;
23539
+ this.processHandlersInstalled = false;
23540
+ process.off("exit", this.onProcessExit);
23541
+ process.off("uncaughtException", this.onUncaughtException);
23542
+ process.off("unhandledRejection", this.onUnhandledRejection);
23543
+ process.off("SIGTERM", this.onSigterm);
23544
+ process.off("SIGHUP", this.onSighup);
22461
23545
  }
22462
23546
  /**
22463
23547
  * Check if a PID is protected by this guardian.
@@ -22493,6 +23577,7 @@ function getProcessGuardian() {
22493
23577
  return _guardian;
22494
23578
  }
22495
23579
  function startProcessGuardian(config) {
23580
+ _guardian?.stop();
22496
23581
  const guardian = new ProcessGuardian(config);
22497
23582
  guardian.start();
22498
23583
  _guardian = guardian;
@@ -22823,12 +23908,9 @@ function createGlobalPsSlashCommand() {
22823
23908
 
22824
23909
  // src/skill.ts
22825
23910
  import * as fs30 from "node:fs/promises";
22826
- import * as path33 from "node:path";
22827
- import {
22828
- SKILL_LIMITS,
22829
- stripFrontmatter,
22830
- ToolValidationError as ToolValidationError10
22831
- } from "@wrongstack/core";
23911
+ import * as path34 from "node:path";
23912
+ import { SKILL_LIMITS, stripFrontmatter } from "@wrongstack/core/skills";
23913
+ import { ToolValidationError as ToolValidationError10 } from "@wrongstack/core/types";
22832
23914
  var MAX_BODY_CHARS = SKILL_LIMITS.MAX_SKILL_BODY_CHARS;
22833
23915
  var MAX_RESOURCE_CHARS = SKILL_LIMITS.MAX_RESOURCE_CHARS;
22834
23916
  var MAX_LISTED_RESOURCES = SKILL_LIMITS.MAX_LISTED_RESOURCES;
@@ -22870,7 +23952,7 @@ function makeSkillTool(skillLoader) {
22870
23952
  field: "name"
22871
23953
  });
22872
23954
  }
22873
- const dir = path33.dirname(manifest.path);
23955
+ const dir = path34.dirname(manifest.path);
22874
23956
  let loadedResource;
22875
23957
  if (input.resource?.trim()) {
22876
23958
  loadedResource = await loadResource(dir, input.resource.trim());
@@ -22920,15 +24002,15 @@ ${listing}`;
22920
24002
  }
22921
24003
  async function loadResource(skillDir, rel) {
22922
24004
  const norm = rel.replace(/\\/g, "/");
22923
- if (path33.isAbsolute(rel) || norm.split("/").some((seg) => seg === "..")) {
24005
+ if (path34.isAbsolute(rel) || norm.split("/").some((seg) => seg === "..")) {
22924
24006
  throw new ToolValidationError10({
22925
24007
  message: `skill: invalid resource path "${rel}"`,
22926
24008
  field: "resource"
22927
24009
  });
22928
24010
  }
22929
- const absPath = path33.resolve(skillDir, rel);
22930
- const root = path33.resolve(skillDir);
22931
- if (absPath !== root && !absPath.startsWith(root + path33.sep)) {
24011
+ const absPath = path34.resolve(skillDir, rel);
24012
+ const root = path34.resolve(skillDir);
24013
+ if (absPath !== root && !absPath.startsWith(root + path34.sep)) {
22932
24014
  throw new ToolValidationError10({
22933
24015
  message: `skill: resource "${rel}" escapes the skill directory`,
22934
24016
  field: "resource"
@@ -22969,7 +24051,7 @@ async function walk(root, dir, out) {
22969
24051
  }
22970
24052
  for (const e of entries) {
22971
24053
  if (out.length >= MAX_LISTED_RESOURCES) return;
22972
- const fullPath = path33.join(dir, e.name);
24054
+ const fullPath = path34.join(dir, e.name);
22973
24055
  let isDir = e.isDirectory();
22974
24056
  if (e.isSymbolicLink()) {
22975
24057
  try {
@@ -22985,7 +24067,7 @@ async function walk(root, dir, out) {
22985
24067
  if (e.name === "SKILL.md" || e.name === "SKILL.save.md") continue;
22986
24068
  try {
22987
24069
  const stat18 = await fs30.stat(fullPath);
22988
- const rel = path33.relative(root, fullPath).split(path33.sep).join("/");
24070
+ const rel = path34.relative(root, fullPath).split(path34.sep).join("/");
22989
24071
  out.push({ path: rel, bytes: stat18.size });
22990
24072
  } catch {
22991
24073
  }
@@ -23160,6 +24242,43 @@ var TOOL_ICON_CONFIG = {
23160
24242
  // gray-light
23161
24243
  };
23162
24244
  var FALLBACK_ICON = "fallback";
24245
+
24246
+ // src/tool-tier.ts
24247
+ function toolNameSet(tools) {
24248
+ return new Set(tools.flatMap((tool) => tool ? [tool.name] : []));
24249
+ }
24250
+ function selectBuiltinToolsForTier(tier, allTools) {
24251
+ switch (tier) {
24252
+ case "off":
24253
+ return [...allTools];
24254
+ case "minimal":
24255
+ case "light": {
24256
+ const tier1Names = toolNameSet(TIER1_TOOLS);
24257
+ return allTools.filter((tool) => tier1Names.has(tool.name));
24258
+ }
24259
+ case "medium": {
24260
+ const tier1Names = toolNameSet(TIER1_TOOLS);
24261
+ const tier2Names = toolNameSet(TIER2_TOOLS);
24262
+ return allTools.filter((tool) => tier1Names.has(tool.name) || tier2Names.has(tool.name));
24263
+ }
24264
+ case "aggressive": {
24265
+ const tier1Names = toolNameSet(TIER1_TOOLS);
24266
+ const tier2Names = toolNameSet(TIER2_TOOLS);
24267
+ const tier3Names = toolNameSet(TIER3_TOOLS);
24268
+ return allTools.filter(
24269
+ (tool) => tier1Names.has(tool.name) || tier2Names.has(tool.name) && tool.name !== "task" || tier3Names.has(tool.name) && tool.name !== "setWorkingDir"
24270
+ );
24271
+ }
24272
+ }
24273
+ }
24274
+ function registerBuiltinToolTier(options) {
24275
+ const tools = selectBuiltinToolsForTier(
24276
+ options.tier,
24277
+ options.tools ?? builtinToolsPack.tools ?? []
24278
+ );
24279
+ options.registry.registerAllOrThrow(tools, options.owner ?? builtinToolsPack.name);
24280
+ return tools;
24281
+ }
23163
24282
  export {
23164
24283
  ADDITIONAL_LANGUAGE_PROFILES,
23165
24284
  BrowserArtifactStore,
@@ -23258,6 +24377,8 @@ export {
23258
24377
  isIndexableFile,
23259
24378
  isIndexing,
23260
24379
  jsonTool,
24380
+ kanbanEvidenceKey,
24381
+ kanbanEvidencePointer,
23261
24382
  kanbanTool,
23262
24383
  languageInfoTool,
23263
24384
  languagePackageTool,
@@ -23284,7 +24405,9 @@ export {
23284
24405
  projectSessionTasksToKanban,
23285
24406
  projectSessionTodosToKanban,
23286
24407
  readTool,
24408
+ recordKanbanVerificationEvidence,
23287
24409
  redactBrowserText,
24410
+ registerBuiltinToolTier,
23288
24411
  relatedMemoryTool,
23289
24412
  rememberTool,
23290
24413
  replaceTool,
@@ -23300,6 +24423,7 @@ export {
23300
24423
  searchCodebaseIndex,
23301
24424
  searchMemoryTool,
23302
24425
  searchTool,
24426
+ selectBuiltinToolsForTier,
23303
24427
  shutdownBrowserTools,
23304
24428
  shutdownCodebaseIndexHost,
23305
24429
  startProcessGuardian,