@wrongstack/tools 0.293.0 → 0.295.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) 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/bash.d.ts +1 -1
  13. package/dist/bash.d.ts.map +1 -1
  14. package/dist/bash.js +66 -52
  15. package/dist/bash.js.map +3 -3
  16. package/dist/batch-tool-use.d.ts +1 -1
  17. package/dist/batch-tool-use.d.ts.map +1 -1
  18. package/dist/batch-tool-use.js +1 -1
  19. package/dist/batch-tool-use.js.map +1 -1
  20. package/dist/browser/index.js +10 -1
  21. package/dist/browser/index.js.map +2 -2
  22. package/dist/browser/manager.d.ts +2 -0
  23. package/dist/browser/manager.d.ts.map +1 -1
  24. package/dist/browser/tools.d.ts +1 -1
  25. package/dist/browser/tools.d.ts.map +1 -1
  26. package/dist/builtin.d.ts +1 -1
  27. package/dist/builtin.d.ts.map +1 -1
  28. package/dist/builtin.js +1394 -553
  29. package/dist/builtin.js.map +4 -4
  30. package/dist/codebase-index/background-indexer.d.ts +2 -2
  31. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  32. package/dist/codebase-index/bm25.d.ts +3 -1
  33. package/dist/codebase-index/bm25.d.ts.map +1 -1
  34. package/dist/codebase-index/codebase-index-tool.d.ts +1 -1
  35. package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
  36. package/dist/codebase-index/codebase-search-tool.d.ts +1 -1
  37. package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
  38. package/dist/codebase-index/codebase-stats-tool.d.ts +1 -1
  39. package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
  40. package/dist/codebase-index/generic-parser.d.ts +34 -0
  41. package/dist/codebase-index/generic-parser.d.ts.map +1 -0
  42. package/dist/codebase-index/go-parser.d.ts +1 -1
  43. package/dist/codebase-index/go-parser.d.ts.map +1 -1
  44. package/dist/codebase-index/index.d.ts +4 -2
  45. package/dist/codebase-index/index.d.ts.map +1 -1
  46. package/dist/codebase-index/index.js +1036 -341
  47. package/dist/codebase-index/index.js.map +4 -4
  48. package/dist/codebase-index/indexer.d.ts +6 -1
  49. package/dist/codebase-index/indexer.d.ts.map +1 -1
  50. package/dist/codebase-index/json-parser.d.ts +1 -1
  51. package/dist/codebase-index/languages.d.ts +25 -0
  52. package/dist/codebase-index/languages.d.ts.map +1 -0
  53. package/dist/codebase-index/py-parser.d.ts +9 -1
  54. package/dist/codebase-index/py-parser.d.ts.map +1 -1
  55. package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
  56. package/dist/codebase-index/rs-parser.d.ts +1 -1
  57. package/dist/codebase-index/rs-parser.d.ts.map +1 -1
  58. package/dist/codebase-index/schema.d.ts +9 -2
  59. package/dist/codebase-index/schema.d.ts.map +1 -1
  60. package/dist/codebase-index/spawn-gate.d.ts +15 -0
  61. package/dist/codebase-index/spawn-gate.d.ts.map +1 -0
  62. package/dist/codebase-index/ts-parser.d.ts +2 -2
  63. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  64. package/dist/codebase-index/worker.js +961 -317
  65. package/dist/codebase-index/worker.js.map +4 -4
  66. package/dist/codebase-index/writer.d.ts +83 -4
  67. package/dist/codebase-index/writer.d.ts.map +1 -1
  68. package/dist/codebase-index/yaml-parser.d.ts +1 -1
  69. package/dist/design.d.ts +5 -2
  70. package/dist/design.d.ts.map +1 -1
  71. package/dist/design.js +36 -3
  72. package/dist/design.js.map +2 -2
  73. package/dist/diff.d.ts +1 -1
  74. package/dist/diff.d.ts.map +1 -1
  75. package/dist/diff.js +2 -2
  76. package/dist/diff.js.map +1 -1
  77. package/dist/document.d.ts +1 -1
  78. package/dist/document.d.ts.map +1 -1
  79. package/dist/document.js +1 -1
  80. package/dist/document.js.map +1 -1
  81. package/dist/e2e.d.ts +1 -1
  82. package/dist/e2e.d.ts.map +1 -1
  83. package/dist/e2e.js +1 -1
  84. package/dist/e2e.js.map +1 -1
  85. package/dist/edit.d.ts +1 -1
  86. package/dist/edit.d.ts.map +1 -1
  87. package/dist/edit.js +3 -3
  88. package/dist/edit.js.map +2 -2
  89. package/dist/exec.d.ts +1 -1
  90. package/dist/exec.d.ts.map +1 -1
  91. package/dist/exec.js +60 -21
  92. package/dist/exec.js.map +3 -3
  93. package/dist/fetch.d.ts +1 -1
  94. package/dist/fetch.d.ts.map +1 -1
  95. package/dist/fetch.js +4 -5
  96. package/dist/fetch.js.map +2 -2
  97. package/dist/format.d.ts +1 -1
  98. package/dist/format.d.ts.map +1 -1
  99. package/dist/format.js +81 -16
  100. package/dist/format.js.map +2 -2
  101. package/dist/git.d.ts +1 -1
  102. package/dist/git.d.ts.map +1 -1
  103. package/dist/git.js +3 -2
  104. package/dist/git.js.map +2 -2
  105. package/dist/glob.d.ts +1 -1
  106. package/dist/glob.d.ts.map +1 -1
  107. package/dist/glob.js +2 -2
  108. package/dist/glob.js.map +1 -1
  109. package/dist/grep.d.ts +1 -1
  110. package/dist/grep.d.ts.map +1 -1
  111. package/dist/grep.js +4 -3
  112. package/dist/grep.js.map +2 -2
  113. package/dist/index.d.ts +4 -4
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +1657 -705
  116. package/dist/index.js.map +4 -4
  117. package/dist/install.d.ts +1 -1
  118. package/dist/install.d.ts.map +1 -1
  119. package/dist/install.js +85 -17
  120. package/dist/install.js.map +2 -2
  121. package/dist/json.d.ts +1 -1
  122. package/dist/json.d.ts.map +1 -1
  123. package/dist/json.js +2 -2
  124. package/dist/json.js.map +2 -2
  125. package/dist/kanban.d.ts +4 -3
  126. package/dist/kanban.d.ts.map +1 -1
  127. package/dist/kanban.js +94 -27
  128. package/dist/kanban.js.map +2 -2
  129. package/dist/languages/execute-tool.d.ts +1 -1
  130. package/dist/languages/execute-tool.d.ts.map +1 -1
  131. package/dist/languages/execute.d.ts +1 -1
  132. package/dist/languages/execute.d.ts.map +1 -1
  133. package/dist/languages/index.d.ts +1 -1
  134. package/dist/languages/index.d.ts.map +1 -1
  135. package/dist/languages/index.js +99 -17
  136. package/dist/languages/index.js.map +2 -2
  137. package/dist/languages/package-tool.d.ts +3 -4
  138. package/dist/languages/package-tool.d.ts.map +1 -1
  139. package/dist/languages/plan.d.ts.map +1 -1
  140. package/dist/languages/profiles/additional.d.ts.map +1 -1
  141. package/dist/languages/profiles/primary.d.ts.map +1 -1
  142. package/dist/languages/tool.d.ts +1 -1
  143. package/dist/languages/tool.d.ts.map +1 -1
  144. package/dist/lint.d.ts +1 -1
  145. package/dist/lint.d.ts.map +1 -1
  146. package/dist/lint.js +81 -16
  147. package/dist/lint.js.map +2 -2
  148. package/dist/logs.d.ts +1 -1
  149. package/dist/logs.d.ts.map +1 -1
  150. package/dist/logs.js +2 -2
  151. package/dist/logs.js.map +1 -1
  152. package/dist/memory.d.ts +1 -1
  153. package/dist/memory.d.ts.map +1 -1
  154. package/dist/memory.js +1 -1
  155. package/dist/memory.js.map +1 -1
  156. package/dist/mode.d.ts +1 -1
  157. package/dist/mode.d.ts.map +1 -1
  158. package/dist/mode.js.map +1 -1
  159. package/dist/outdated.d.ts +1 -1
  160. package/dist/outdated.d.ts.map +1 -1
  161. package/dist/outdated.js +101 -19
  162. package/dist/outdated.js.map +2 -2
  163. package/dist/pack.d.ts +1 -1
  164. package/dist/pack.js +1394 -553
  165. package/dist/pack.js.map +4 -4
  166. package/dist/patch.d.ts +1 -1
  167. package/dist/patch.d.ts.map +1 -1
  168. package/dist/patch.js +2 -2
  169. package/dist/patch.js.map +1 -1
  170. package/dist/plan.d.ts +1 -1
  171. package/dist/plan.d.ts.map +1 -1
  172. package/dist/plan.js +7 -7
  173. package/dist/plan.js.map +2 -2
  174. package/dist/process-guardian.d.ts +7 -0
  175. package/dist/process-guardian.d.ts.map +1 -1
  176. package/dist/process-registry-persistent.d.ts +5 -0
  177. package/dist/process-registry-persistent.d.ts.map +1 -1
  178. package/dist/process-registry.d.ts +9 -1
  179. package/dist/process-registry.d.ts.map +1 -1
  180. package/dist/process-registry.js +18 -3
  181. package/dist/process-registry.js.map +2 -2
  182. package/dist/ps-slash.js +54 -15
  183. package/dist/ps-slash.js.map +2 -2
  184. package/dist/read.d.ts +1 -1
  185. package/dist/read.d.ts.map +1 -1
  186. package/dist/read.js +3 -2
  187. package/dist/read.js.map +2 -2
  188. package/dist/replace.d.ts +1 -1
  189. package/dist/replace.d.ts.map +1 -1
  190. package/dist/replace.js +4 -4
  191. package/dist/replace.js.map +2 -2
  192. package/dist/scaffold.d.ts +1 -1
  193. package/dist/scaffold.d.ts.map +1 -1
  194. package/dist/scaffold.js +2 -2
  195. package/dist/scaffold.js.map +1 -1
  196. package/dist/search.d.ts +1 -1
  197. package/dist/search.d.ts.map +1 -1
  198. package/dist/search.js +4 -4
  199. package/dist/search.js.map +2 -2
  200. package/dist/session-kanban.d.ts +4 -1
  201. package/dist/session-kanban.d.ts.map +1 -1
  202. package/dist/session-kanban.js +23 -15
  203. package/dist/session-kanban.js.map +2 -2
  204. package/dist/set-working-dir.d.ts +1 -1
  205. package/dist/set-working-dir.d.ts.map +1 -1
  206. package/dist/skill.d.ts +1 -1
  207. package/dist/skill.d.ts.map +1 -1
  208. package/dist/skill.js +2 -5
  209. package/dist/skill.js.map +2 -2
  210. package/dist/spawn-background.d.ts.map +1 -1
  211. package/dist/task.d.ts +3 -2
  212. package/dist/task.d.ts.map +1 -1
  213. package/dist/task.js +6 -8
  214. package/dist/task.js.map +2 -2
  215. package/dist/test.d.ts +1 -1
  216. package/dist/test.d.ts.map +1 -1
  217. package/dist/test.js +81 -16
  218. package/dist/test.js.map +2 -2
  219. package/dist/todo.d.ts +2 -1
  220. package/dist/todo.d.ts.map +1 -1
  221. package/dist/todo.js +4 -4
  222. package/dist/todo.js.map +2 -2
  223. package/dist/tool-help.d.ts +1 -1
  224. package/dist/tool-help.d.ts.map +1 -1
  225. package/dist/tool-help.js.map +1 -1
  226. package/dist/tool-search.d.ts +1 -1
  227. package/dist/tool-search.d.ts.map +1 -1
  228. package/dist/tool-search.js.map +1 -1
  229. package/dist/tool-tier.d.ts +16 -0
  230. package/dist/tool-tier.d.ts.map +1 -0
  231. package/dist/tool-tier.js +22233 -0
  232. package/dist/tool-tier.js.map +7 -0
  233. package/dist/tool-use.d.ts +1 -1
  234. package/dist/tool-use.d.ts.map +1 -1
  235. package/dist/tool-use.js.map +1 -1
  236. package/dist/tree.d.ts +1 -1
  237. package/dist/tree.d.ts.map +1 -1
  238. package/dist/tree.js +2 -2
  239. package/dist/tree.js.map +1 -1
  240. package/dist/typecheck.d.ts +1 -1
  241. package/dist/typecheck.d.ts.map +1 -1
  242. package/dist/typecheck.js +81 -16
  243. package/dist/typecheck.js.map +2 -2
  244. package/dist/write.d.ts +1 -1
  245. package/dist/write.d.ts.map +1 -1
  246. package/dist/write.js +3 -2
  247. package/dist/write.js.map +2 -2
  248. package/package.json +7 -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,7 @@ 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 LOCKFILE = ".process-registry.lock";
5301
5384
  function generateInstanceId() {
5302
5385
  const hostname4 = os2.hostname();
5303
5386
  const pid = process.pid;
@@ -5386,12 +5469,18 @@ var PersistentProcessRegistry = class {
5386
5469
  lockPath;
5387
5470
  baseRegistry;
5388
5471
  heartbeatInterval = null;
5472
+ cleanupInterval = null;
5473
+ heartbeatRunning = false;
5474
+ cleanupRunning = false;
5389
5475
  isShuttingDown = false;
5476
+ onProcessExit = () => {
5477
+ void this.syncToPersistent();
5478
+ };
5390
5479
  constructor(baseRegistry) {
5391
5480
  this.instanceId = generateInstanceId();
5392
- const homeDir = os2.homedir();
5393
- this.registryPath = path9.join(homeDir, REGISTRY_FILE);
5394
- this.lockPath = path9.join(homeDir, LOCKFILE);
5481
+ const globalRoot = wstackGlobalRoot3();
5482
+ this.registryPath = path9.join(globalRoot, REGISTRY_FILE);
5483
+ this.lockPath = path9.join(globalRoot, LOCKFILE);
5395
5484
  this.baseRegistry = baseRegistry ?? getProcessRegistry();
5396
5485
  this.ensureDirectory().catch((err) => {
5397
5486
  emitStructuredLog("warn", "process_registry.dir_create_failed", "PersistentProcessRegistry: failed to create .wrongstack directory", err);
@@ -5410,16 +5499,18 @@ var PersistentProcessRegistry = class {
5410
5499
  */
5411
5500
  start() {
5412
5501
  if (this.heartbeatInterval) return;
5413
- this.syncToPersistent();
5502
+ this.isShuttingDown = false;
5503
+ this.heartbeat();
5414
5504
  this.heartbeatInterval = setInterval(() => {
5415
5505
  this.heartbeat();
5416
5506
  }, HEARTBEAT_INTERVAL_MS);
5417
5507
  this.heartbeatInterval.unref?.();
5418
- setInterval(() => {
5419
- this.cleanupStaleEntries();
5420
- }, STALE_THRESHOLD_MS).unref?.();
5508
+ this.cleanupInterval = setInterval(() => {
5509
+ this.cleanup();
5510
+ }, STALE_THRESHOLD_MS);
5511
+ this.cleanupInterval.unref?.();
5421
5512
  this.registerMainProcess();
5422
- process.on("exit", () => this.syncToPersistent());
5513
+ process.on("exit", this.onProcessExit);
5423
5514
  }
5424
5515
  /**
5425
5516
  * Stop the heartbeat and clean up.
@@ -5430,7 +5521,12 @@ var PersistentProcessRegistry = class {
5430
5521
  clearInterval(this.heartbeatInterval);
5431
5522
  this.heartbeatInterval = null;
5432
5523
  }
5433
- this.syncToPersistent();
5524
+ if (this.cleanupInterval) {
5525
+ clearInterval(this.cleanupInterval);
5526
+ this.cleanupInterval = null;
5527
+ }
5528
+ process.off("exit", this.onProcessExit);
5529
+ void this.syncToPersistent();
5434
5530
  }
5435
5531
  /**
5436
5532
  * Register the main WrongStack process as protected.
@@ -5514,8 +5610,18 @@ var PersistentProcessRegistry = class {
5514
5610
  * Send heartbeat to mark all this instance's processes as alive.
5515
5611
  */
5516
5612
  heartbeat() {
5517
- if (this.isShuttingDown) return;
5518
- this.syncToPersistent();
5613
+ if (this.isShuttingDown || this.heartbeatRunning) return;
5614
+ this.heartbeatRunning = true;
5615
+ void this.syncToPersistent().finally(() => {
5616
+ this.heartbeatRunning = false;
5617
+ });
5618
+ }
5619
+ cleanup() {
5620
+ if (this.isShuttingDown || this.cleanupRunning) return;
5621
+ this.cleanupRunning = true;
5622
+ void this.cleanupStaleEntries().finally(() => {
5623
+ this.cleanupRunning = false;
5624
+ });
5519
5625
  }
5520
5626
  /**
5521
5627
  * Sync this instance's processes to the persistent registry.
@@ -6237,14 +6343,12 @@ var bashTool = {
6237
6343
  const detached = !isWin5;
6238
6344
  const startedAt = Date.now();
6239
6345
  if (input.background) {
6240
- let buf2 = "";
6241
- let truncated = false;
6242
6346
  const child2 = spawn3(shell, args, {
6243
6347
  cwd: ctx.projectRoot,
6244
6348
  env,
6245
6349
  // PowerShell takes the script on stdin (no argv quoting); cmd.exe
6246
6350
  // and POSIX shells ignore stdin when given the command inline.
6247
- stdio: [plan.useStdin ? "pipe" : "ignore", "pipe", "pipe"],
6351
+ stdio: [plan.useStdin ? "pipe" : "ignore", "ignore", "ignore"],
6248
6352
  // win32: CreateProcess IGNORES CREATE_NO_WINDOW (windowsHide) when
6249
6353
  // DETACHED_PROCESS (detached: true) is set, so the console-less
6250
6354
  // cmd.exe's grandchildren (node, dev servers) each allocate a fresh
@@ -6263,8 +6367,8 @@ var bashTool = {
6263
6367
  }
6264
6368
  }
6265
6369
  const pid2 = child2.pid;
6266
- let stdoutBytes2 = 0;
6267
- let stderrBytes2 = 0;
6370
+ const stdoutBytes2 = 0;
6371
+ const stderrBytes2 = 0;
6268
6372
  let telemetryCompleted2 = false;
6269
6373
  emitProcessStarted2({
6270
6374
  ...pid2 !== void 0 ? { pid: pid2 } : {},
@@ -6297,41 +6401,17 @@ var bashTool = {
6297
6401
  startedAt: Date.now(),
6298
6402
  sessionId: ctx.session?.id,
6299
6403
  child: child2,
6300
- processGroupLeader: detached && child2.pid === pid2
6404
+ processGroupLeader: detached && child2.pid === pid2,
6405
+ background: true
6301
6406
  });
6302
6407
  child2.on("close", () => registry.unregister(pid2));
6303
6408
  }
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
6409
  child2.on("error", () => {
6328
- cleanupBackground();
6329
6410
  if (typeof pid2 === "number") registry.unregister(pid2);
6330
6411
  registry.afterCall(Date.now() - startedAt, true, bypassBreaker);
6331
6412
  completeBackground(1);
6332
6413
  });
6333
6414
  child2.on("close", (code, signal) => {
6334
- cleanupBackground();
6335
6415
  registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
6336
6416
  completeBackground(code ?? (signal ? 1 : 0), signal ?? void 0);
6337
6417
  });
@@ -6339,7 +6419,7 @@ var bashTool = {
6339
6419
  yield {
6340
6420
  type: "final",
6341
6421
  output: {
6342
- output: normalizeCommandOutput(buf2),
6422
+ output: "",
6343
6423
  exit_code: null,
6344
6424
  timed_out: false,
6345
6425
  pid: pid2
@@ -6610,6 +6690,7 @@ ${hint}` : ""),
6610
6690
  for (const entry of registry.bySession(sessionId)) {
6611
6691
  if (entry.name !== "bash") continue;
6612
6692
  if (entry.child.exitCode !== null) continue;
6693
+ if (entry.background) continue;
6613
6694
  if (entry.protected) continue;
6614
6695
  registry.kill(entry.pid, { force: true });
6615
6696
  }
@@ -6619,7 +6700,7 @@ ${hint}` : ""),
6619
6700
  // src/batch-tool-use.ts
6620
6701
  import {
6621
6702
  GOVERNED_TOOL_EXECUTOR_META_KEY
6622
- } from "@wrongstack/core";
6703
+ } from "@wrongstack/core/types";
6623
6704
  var batchToolUseTool = {
6624
6705
  name: "batch_tool_use",
6625
6706
  category: "Meta",
@@ -7399,6 +7480,10 @@ var BrowserSessionManager = class {
7399
7480
  const ids = [...this.sessions.values()].filter((session) => session.ownerId === ownerId).map((session) => session.id);
7400
7481
  for (const id of ids) await this.close(id, ownerId).catch(() => void 0);
7401
7482
  }
7483
+ /** True when the manager owns no live browser contexts. */
7484
+ isIdle() {
7485
+ return this.sessions.size === 0;
7486
+ }
7402
7487
  async dispose() {
7403
7488
  for (const session of [...this.sessions.values()]) {
7404
7489
  await session.context.close().catch(() => void 0);
@@ -7562,7 +7647,7 @@ async function abortable(signal, operation, onAbort) {
7562
7647
 
7563
7648
  // src/browser/tools.ts
7564
7649
  import * as path12 from "node:path";
7565
- import { ToolCapabilities } from "@wrongstack/core";
7650
+ import { ToolCapabilities } from "@wrongstack/core/security";
7566
7651
  import { resolveWstackPaths } from "@wrongstack/core/utils";
7567
7652
  var managers = /* @__PURE__ */ new Map();
7568
7653
  var cleanupSignalByContext = /* @__PURE__ */ new WeakMap();
@@ -7583,12 +7668,17 @@ function managerFor(ctx) {
7583
7668
  if (cleanupSignalByContext.get(ctx) !== runSignal) {
7584
7669
  cleanupSignalByContext.set(ctx, runSignal);
7585
7670
  const managerForRun = manager;
7671
+ const rootForRun = root;
7586
7672
  const ownerForRun = owner(ctx);
7587
7673
  ctx.registerAbortHook?.(async () => {
7588
7674
  if (cleanupSignalByContext.get(ctx) === runSignal) {
7589
7675
  cleanupSignalByContext.delete(ctx);
7590
7676
  }
7591
7677
  await managerForRun.closeOwner(ownerForRun);
7678
+ if (managerForRun.isIdle() && managers.get(rootForRun) === managerForRun) {
7679
+ managers.delete(rootForRun);
7680
+ await managerForRun.dispose();
7681
+ }
7592
7682
  });
7593
7683
  }
7594
7684
  return manager;
@@ -7976,7 +8066,7 @@ async function shutdownBrowserTools() {
7976
8066
  }
7977
8067
 
7978
8068
  // src/codebase-index/writer.ts
7979
- import { expectDefined as expectDefined2 } from "@wrongstack/core";
8069
+ import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
7980
8070
 
7981
8071
  // src/codebase-index/circuit-breaker.ts
7982
8072
  var CircuitOpenError = class extends Error {
@@ -8066,7 +8156,7 @@ import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
8066
8156
  import { createRequire } from "node:module";
8067
8157
  import * as fs9 from "node:fs";
8068
8158
  import * as path13 from "node:path";
8069
- import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core";
8159
+ import { resolveWstackPaths as resolveWstackPaths2, sqliteCachePragmas } from "@wrongstack/core/utils";
8070
8160
 
8071
8161
  // src/codebase-index/schema.ts
8072
8162
  var SCHEMA_VERSION = 3;
@@ -8122,23 +8212,13 @@ function buildBm25Index(docs) {
8122
8212
  const tokens = tokenise(d.text);
8123
8213
  return { id: d.id, tokens, raw: d.text, len: tokens.length };
8124
8214
  });
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
8215
  const N = documents.length;
8136
8216
  const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
8137
8217
  const avgLen = N === 0 ? 0 : totalLen / N;
8138
- return new Bm25Index(documents, df, N, avgLen);
8218
+ return new Bm25Index(documents, N, avgLen);
8139
8219
  }
8140
8220
  var Bm25Index = class {
8141
- constructor(documents, _df, N, avgLen) {
8221
+ constructor(documents, N, avgLen) {
8142
8222
  this.documents = documents;
8143
8223
  this.N = N;
8144
8224
  this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
@@ -8146,9 +8226,19 @@ var Bm25Index = class {
8146
8226
  documents;
8147
8227
  N;
8148
8228
  safeAvgLen;
8229
+ /** Lazily-built id→doc index so getDoc is O(1) instead of an O(D) linear find. */
8230
+ _byId;
8149
8231
  score(query, filter) {
8150
8232
  const qTokens = tokenise(query);
8151
8233
  if (qTokens.length === 0) return [];
8234
+ const dfByTerm = /* @__PURE__ */ new Map();
8235
+ for (const qTerm of qTokens) {
8236
+ let dfVal = 0;
8237
+ for (const candidate of this.documents) {
8238
+ if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
8239
+ }
8240
+ dfByTerm.set(qTerm, dfVal);
8241
+ }
8152
8242
  const results = [];
8153
8243
  for (const doc of this.documents) {
8154
8244
  if (filter && !filter(doc.id)) continue;
@@ -8159,10 +8249,7 @@ var Bm25Index = class {
8159
8249
  if (t.startsWith(qTerm)) tf++;
8160
8250
  }
8161
8251
  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
- }
8252
+ const dfVal = dfByTerm.get(qTerm) ?? 0;
8166
8253
  if (dfVal === 0) continue;
8167
8254
  const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
8168
8255
  const lenRatio = B * (doc.len / this.safeAvgLen);
@@ -8174,7 +8261,10 @@ var Bm25Index = class {
8174
8261
  return results;
8175
8262
  }
8176
8263
  getDoc(id) {
8177
- return this.documents.find((d) => d.id === id);
8264
+ if (!this._byId) {
8265
+ this._byId = new Map(this.documents.map((d) => [d.id, d]));
8266
+ }
8267
+ return this._byId.get(id);
8178
8268
  }
8179
8269
  extractSnippet(docId, queryTokens, radius = 40) {
8180
8270
  const doc = this.getDoc(docId);
@@ -8336,6 +8426,25 @@ var IndexStore = class _IndexStore {
8336
8426
  * and reuse it. Cleared in {@link close} when the connection is torn down.
8337
8427
  */
8338
8428
  stmtCache = /* @__PURE__ */ new Map();
8429
+ /**
8430
+ * Cached full-corpus BM25 index for the FTS5-unavailable fallback path.
8431
+ * Built lazily on the first `searchRankedFallback` call and invalidated
8432
+ * (via `bm25Dirty`) whenever the `symbols` table is mutated. Computing
8433
+ * IDF over the full corpus is also more correct than the old per-query
8434
+ * candidate-subset IDF.
8435
+ *
8436
+ * Cache-lifecycle invariants (single source of truth lives at the
8437
+ * `invalidateBm25()` helper — see its docblock for the "every mutation
8438
+ * MUST call this" contract):
8439
+ * - declaration: this field + `bm25Dirty` (here)
8440
+ * - invalidation: `invalidateBm25()` flips the flag and nulls the cache
8441
+ * - build: `getOrBuildBm25()` rebuilds against current `symbols` rows
8442
+ * - teardown: `close()` resets the flag and nulls the cache
8443
+ */
8444
+ bm25Cache = null;
8445
+ // Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
8446
+ // an empty or pre-existing corpus makes a stale IDF table meaningless.
8447
+ bm25Dirty = true;
8339
8448
  /** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
8340
8449
  stmt(sql) {
8341
8450
  let s = this.stmtCache.get(sql);
@@ -8383,7 +8492,15 @@ var IndexStore = class _IndexStore {
8383
8492
  this.db = new Database(path13.join(this.indexDir, DB_FILE));
8384
8493
  try {
8385
8494
  this.db.exec("PRAGMA journal_mode = WAL");
8386
- this.db.exec("PRAGMA busy_timeout = 5000");
8495
+ this.db.exec("PRAGMA synchronous = NORMAL");
8496
+ this.db.exec("PRAGMA busy_timeout = 15000");
8497
+ this.db.exec("PRAGMA temp_store = MEMORY");
8498
+ const cache2 = sqliteCachePragmas();
8499
+ this.db.exec(`PRAGMA cache_size = -${cache2.cacheSizeKiB}`);
8500
+ this.db.exec(`PRAGMA mmap_size = ${cache2.mmapBytes}`);
8501
+ this.db.exec("PRAGMA foreign_keys = ON");
8502
+ this.db.exec("PRAGMA journal_size_limit = 67108864");
8503
+ this.db.exec("PRAGMA wal_autocheckpoint = 1000");
8387
8504
  } catch {
8388
8505
  }
8389
8506
  this.initSchema();
@@ -8395,7 +8512,7 @@ var IndexStore = class _IndexStore {
8395
8512
  value TEXT NOT NULL
8396
8513
  );
8397
8514
  `);
8398
- const storedRows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all("version");
8515
+ const storedRows = this.stmt("SELECT value FROM metadata WHERE key = ?").all("version");
8399
8516
  const storedVersion = storedRows.length ? Number(storedRows[0]?.value) : null;
8400
8517
  if (storedVersion !== null && storedVersion !== SCHEMA_VERSION) {
8401
8518
  this.db.exec(`
@@ -8404,9 +8521,9 @@ var IndexStore = class _IndexStore {
8404
8521
  DROP TABLE IF EXISTS refs;
8405
8522
  `);
8406
8523
  this.db.exec("DROP TABLE IF EXISTS symbols_fts");
8407
- this.db.prepare("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
8524
+ this.stmt("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
8408
8525
  } else if (storedVersion === null) {
8409
- this.db.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
8526
+ this.stmt("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
8410
8527
  }
8411
8528
  this.db.exec(`
8412
8529
  CREATE TABLE IF NOT EXISTS files (
@@ -8437,6 +8554,7 @@ var IndexStore = class _IndexStore {
8437
8554
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_file ON symbols(file)");
8438
8555
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_lang_kind ON symbols(lang, kind)");
8439
8556
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_file_fk ON symbols(file_fk)");
8557
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_name_id ON symbols(name, id)");
8440
8558
  this.db.exec(`
8441
8559
  CREATE TABLE IF NOT EXISTS refs (
8442
8560
  id INTEGER PRIMARY KEY,
@@ -8457,65 +8575,167 @@ var IndexStore = class _IndexStore {
8457
8575
  );
8458
8576
  this.ftsAvailable = true;
8459
8577
  const symbolCount = Number(
8460
- this.db.prepare("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
8578
+ this.stmt("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
8461
8579
  );
8462
8580
  const ftsCount = Number(
8463
- this.db.prepare("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
8581
+ this.stmt("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
8464
8582
  );
8465
8583
  if (symbolCount !== ftsCount) {
8466
8584
  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
- }
8585
+ const rows = this.stmt("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
8586
+ this.bulkInsertFts(
8587
+ rows.map((row) => ({
8588
+ id: row.id,
8589
+ text: buildIndexableText(row.name, row.signature, row.doc_comment)
8590
+ }))
8591
+ );
8592
+ this.invalidateBm25();
8472
8593
  }
8473
8594
  } catch {
8474
8595
  this.ftsAvailable = false;
8475
8596
  }
8597
+ this.ensureNextSymbolIdSeeded();
8598
+ }
8599
+ // ─── ID allocation & bulk helpers ────────────────────────────────────────────
8600
+ static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
8601
+ /** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
8602
+ static MAX_SQL_VARS = 900;
8603
+ /**
8604
+ * Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
8605
+ * transaction on open; the first concurrent writer under BEGIN IMMEDIATE
8606
+ * re-reads and advances the counter atomically.
8607
+ */
8608
+ ensureNextSymbolIdSeeded() {
8609
+ const existing = this.stmt("SELECT value FROM metadata WHERE key = ?").get(
8610
+ _IndexStore.NEXT_SYMBOL_ID_KEY
8611
+ );
8612
+ if (existing?.value !== void 0) return;
8613
+ const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8614
+ const next = (maxRows[0]?.m ?? 0) + 1;
8615
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)").run(
8616
+ _IndexStore.NEXT_SYMBOL_ID_KEY,
8617
+ String(next)
8618
+ );
8619
+ }
8620
+ /**
8621
+ * Reserve `count` consecutive symbol ids. MUST run inside BEGIN IMMEDIATE
8622
+ * so concurrent indexers cannot hand out overlapping ranges.
8623
+ */
8624
+ allocateSymbolIds(count) {
8625
+ if (count <= 0) return this.getMaxSymbolId() + 1;
8626
+ this.ensureNextSymbolIdSeeded();
8627
+ const row = this.stmt("SELECT value FROM metadata WHERE key = ?").get(
8628
+ _IndexStore.NEXT_SYMBOL_ID_KEY
8629
+ );
8630
+ const start = Math.max(1, Number(row?.value ?? 1) || 1);
8631
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)").run(
8632
+ _IndexStore.NEXT_SYMBOL_ID_KEY,
8633
+ String(start + count)
8634
+ );
8635
+ return start;
8636
+ }
8637
+ /** Multi-row INSERT for symbols — amortizes prepare/bind overhead in large batches. */
8638
+ bulkInsertSymbols(rows) {
8639
+ if (rows.length === 0) return;
8640
+ const cols = 12;
8641
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / cols));
8642
+ for (let i = 0; i < rows.length; i += chunkSize) {
8643
+ const chunk = rows.slice(i, i + chunkSize);
8644
+ const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
8645
+ const stmt = this.stmt(
8646
+ `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
8647
+ VALUES ${placeholders}`
8648
+ );
8649
+ const binds = [];
8650
+ for (const r of chunk) {
8651
+ binds.push(
8652
+ r.id,
8653
+ r.lang,
8654
+ r.kind,
8655
+ r.name,
8656
+ r.file,
8657
+ r.line,
8658
+ r.col,
8659
+ r.signature,
8660
+ r.docComment,
8661
+ r.scope,
8662
+ r.text,
8663
+ r.file
8664
+ );
8665
+ }
8666
+ stmt.run(...binds);
8667
+ }
8668
+ }
8669
+ bulkInsertFts(rows) {
8670
+ if (!this.ftsAvailable || rows.length === 0) return;
8671
+ const cols = 2;
8672
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / cols));
8673
+ for (let i = 0; i < rows.length; i += chunkSize) {
8674
+ const chunk = rows.slice(i, i + chunkSize);
8675
+ const placeholders = chunk.map(() => "(?, ?)").join(", ");
8676
+ const stmt = this.stmt(`INSERT INTO symbols_fts(rowid, text) VALUES ${placeholders}`);
8677
+ const binds = [];
8678
+ for (const r of chunk) binds.push(r.id, r.text);
8679
+ stmt.run(...binds);
8680
+ }
8681
+ }
8682
+ bulkInsertRefs(refs) {
8683
+ if (refs.length === 0) return;
8684
+ const cols = 5;
8685
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / cols));
8686
+ for (let i = 0; i < refs.length; i += chunkSize) {
8687
+ const chunk = refs.slice(i, i + chunkSize);
8688
+ const placeholders = chunk.map(() => "(?, ?, ?, ?, ?)").join(", ");
8689
+ const stmt = this.stmt(
8690
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line) VALUES ${placeholders}`
8691
+ );
8692
+ const binds = [];
8693
+ for (const ref of chunk) {
8694
+ binds.push(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
8695
+ }
8696
+ stmt.run(...binds);
8697
+ }
8476
8698
  }
8477
8699
  // ─── Symbol CRUD ─────────────────────────────────────────────────────────────
8478
8700
  /**
8479
8701
  * 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).
8702
+ * `COMMIT`. Id ranges come from the `next_symbol_id` metadata counter
8703
+ * (O(1)); multi-row INSERT amortizes bind overhead for large files.
8484
8704
  *
8485
8705
  * @returns The symbols array with `id` fields populated so the caller can
8486
8706
  * use them for refs without re-reading from the DB.
8487
8707
  */
8488
8708
  insertSymbols(symbols) {
8709
+ this.invalidateBm25();
8489
8710
  return this.runWithRetry(() => {
8490
8711
  this.db.exec("BEGIN IMMEDIATE");
8491
8712
  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;
8713
+ let nextId = this.allocateSymbolIds(symbols.length);
8499
8714
  const result = [];
8715
+ const bulk = [];
8716
+ const ftsRows = [];
8500
8717
  for (const s of symbols) {
8501
8718
  const id = nextId++;
8502
- stmt.run(
8719
+ bulk.push({
8503
8720
  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));
8721
+ lang: s.lang,
8722
+ kind: s.kind,
8723
+ name: s.name,
8724
+ file: s.file,
8725
+ line: s.line,
8726
+ col: s.col,
8727
+ signature: s.signature,
8728
+ docComment: s.docComment,
8729
+ scope: s.scope,
8730
+ text: s.text
8731
+ });
8732
+ if (this.ftsAvailable) {
8733
+ ftsRows.push({ id, text: buildIndexableText(s.name, s.signature, s.docComment) });
8734
+ }
8517
8735
  result.push({ ...s, id });
8518
8736
  }
8737
+ this.bulkInsertSymbols(bulk);
8738
+ this.bulkInsertFts(ftsRows);
8519
8739
  this.db.exec("COMMIT");
8520
8740
  return result;
8521
8741
  } catch (err) {
@@ -8525,6 +8745,7 @@ var IndexStore = class _IndexStore {
8525
8745
  });
8526
8746
  }
8527
8747
  deleteSymbolsForFile(file) {
8748
+ this.invalidateBm25();
8528
8749
  this.runWithRetry(() => {
8529
8750
  if (this.ftsAvailable) {
8530
8751
  this.stmt(
@@ -8540,17 +8761,20 @@ var IndexStore = class _IndexStore {
8540
8761
  * dropped the `files` row, leaving its symbols orphaned but still searchable.
8541
8762
  */
8542
8763
  deleteFile(file) {
8764
+ this.invalidateBm25();
8543
8765
  this.runWithRetry(() => {
8544
8766
  this.db.exec("BEGIN IMMEDIATE");
8545
8767
  try {
8546
8768
  if (this.ftsAvailable) {
8547
- this.db.prepare(
8769
+ this.stmt(
8548
8770
  "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
8549
8771
  ).run(file);
8550
8772
  }
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);
8773
+ this.stmt(
8774
+ "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
8775
+ ).run(file);
8776
+ this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
8777
+ this.stmt("DELETE FROM files WHERE file = ?").run(file);
8554
8778
  this.db.exec("COMMIT");
8555
8779
  } catch (err) {
8556
8780
  this.db.exec("ROLLBACK");
@@ -8587,7 +8811,7 @@ var IndexStore = class _IndexStore {
8587
8811
  };
8588
8812
  }
8589
8813
  getAllFileMetas() {
8590
- return this.db.prepare("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
8814
+ return this.stmt("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
8591
8815
  file: r.file,
8592
8816
  lang: r.lang,
8593
8817
  mtimeMs: r.mtime_ms,
@@ -8596,7 +8820,32 @@ var IndexStore = class _IndexStore {
8596
8820
  }));
8597
8821
  }
8598
8822
  // ─── Search ──────────────────────────────────────────────────────────────────
8599
- search(query, filter) {
8823
+ search(query, filter, opts) {
8824
+ const built = this.buildSearchWhere(query, filter);
8825
+ if (built === null) return [];
8826
+ const { where, values } = built;
8827
+ const limit = typeof opts?.limit === "number" && Number.isFinite(opts.limit) ? Math.max(0, Math.trunc(opts.limit)) : void 0;
8828
+ const limitSql = limit !== void 0 ? " LIMIT ?" : "";
8829
+ const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}${limitSql}`;
8830
+ const binds = limit !== void 0 ? [...values, limit] : values;
8831
+ const rows = this.stmt(sql).all(...binds);
8832
+ return rows.map((r) => ({
8833
+ id: r.id,
8834
+ lang: r.lang,
8835
+ kind: r.kind,
8836
+ name: r.name,
8837
+ file: r.file,
8838
+ line: r.line,
8839
+ col: r.col,
8840
+ signature: r.signature,
8841
+ docComment: r.doc_comment,
8842
+ score: 0,
8843
+ snippet: "",
8844
+ lspKind: filter?.lspKind
8845
+ }));
8846
+ }
8847
+ /** Shared WHERE builder for {@link search} / empty-query ranked totals. */
8848
+ buildSearchWhere(query, filter) {
8600
8849
  const conditions = [];
8601
8850
  const values = [];
8602
8851
  let effectiveKind = filter?.kind;
@@ -8605,7 +8854,7 @@ var IndexStore = class _IndexStore {
8605
8854
  if (mapped !== null) {
8606
8855
  effectiveKind = mapped;
8607
8856
  } else {
8608
- return [];
8857
+ return null;
8609
8858
  }
8610
8859
  }
8611
8860
  if (effectiveKind) {
@@ -8627,23 +8876,15 @@ var IndexStore = class _IndexStore {
8627
8876
  for (const t of tokens) values.push(`%${t}%`);
8628
8877
  }
8629
8878
  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
- }));
8879
+ return { where, values };
8880
+ }
8881
+ countSearch(query, filter) {
8882
+ const built = this.buildSearchWhere(query, filter);
8883
+ if (built === null) return 0;
8884
+ const row = this.stmt(`SELECT COUNT(*) AS n FROM symbols ${built.where}`).get(
8885
+ ...built.values
8886
+ );
8887
+ return Number(row?.n ?? 0);
8647
8888
  }
8648
8889
  /**
8649
8890
  * Ranked search — the one-stop query the codebase-search tool and plug-lsp
@@ -8684,12 +8925,12 @@ var IndexStore = class _IndexStore {
8684
8925
  values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
8685
8926
  }
8686
8927
  const where = conditions.join(" AND ");
8687
- const countRows = this.db.prepare(
8928
+ const countRows = this.stmt(
8688
8929
  `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
8689
8930
  ).all(...values);
8690
8931
  const total = countRows[0] ? Number(countRows[0].n) : 0;
8691
8932
  if (total === 0) return { results: [], total: 0 };
8692
- const rows = this.db.prepare(
8933
+ const rows = this.stmt(
8693
8934
  `SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
8694
8935
  -bm25(symbols_fts) AS score,
8695
8936
  snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
@@ -8722,20 +8963,60 @@ var IndexStore = class _IndexStore {
8722
8963
  total
8723
8964
  };
8724
8965
  }
8966
+ /**
8967
+ * Invalidate the cached BM25 index.
8968
+ *
8969
+ * **Contract: every method that mutates `symbols` MUST call this before
8970
+ * returning.** (`refs` mutations do not affect the BM25 fallback because
8971
+ * the corpus is built from `symbols.text` via `getAllIndexable()` and the
8972
+ * BM25 score is filtered by the LIKE-selected candidate set in
8973
+ * `searchRankedFallback`.) Today the call sites are `repairDrift`,
8974
+ * `insertSymbols`, `deleteSymbolsForFile`, `deleteFile`, `clearAll`, and
8975
+ * `commitBatch`. A future mutation that adds a new write path (e.g.
8976
+ * `renameFile`, `updateSignature`) MUST also call this — otherwise the
8977
+ * FTS5-unavailable fallback will serve stale search results. The
8978
+ * `close()` reset at L1820-1821 tears the cache down on store shutdown,
8979
+ * which is the only legitimate place that flips the flag outside this
8980
+ * helper.
8981
+ *
8982
+ * Called *before* `runWithRetry` on purpose: if the write fails all
8983
+ * retries the flag stays set, forcing a rebuild on the next search rather
8984
+ * than trusting a cache that may not reflect the intended mutation.
8985
+ * Do not move this inside the retry closure.
8986
+ */
8987
+ invalidateBm25() {
8988
+ this.bm25Dirty = true;
8989
+ this.bm25Cache = null;
8990
+ }
8991
+ /**
8992
+ * Return the cached full-corpus BM25 index, rebuilding it only when the
8993
+ * symbols table has been mutated since the last build. The full-corpus IDF
8994
+ * is more correct than the old per-query candidate-subset IDF, and the
8995
+ * amortized build cost drops from O(symbols × tokens) per search to once
8996
+ * per write batch.
8997
+ *
8998
+ * Note: the first call after a long idle (or on a freshly opened store)
8999
+ * pays the full corpus rebuild synchronously on the search path. For a
9000
+ * 5 500+ symbol corpus this is a visible one-time latency spike.
9001
+ */
9002
+ getOrBuildBm25() {
9003
+ if (this.bm25Cache && !this.bm25Dirty) return this.bm25Cache;
9004
+ const docs = this.getAllIndexable();
9005
+ this.bm25Cache = buildBm25Index(docs);
9006
+ this.bm25Dirty = false;
9007
+ return this.bm25Cache;
9008
+ }
8725
9009
  /** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
8726
9010
  searchRankedFallback(query, filter, limit) {
8727
- const candidates = this.search(query, filter);
8728
- if (candidates.length === 0) return { results: [], total: 0 };
8729
9011
  if (!query.trim()) {
8730
- return { results: candidates.slice(0, limit), total: candidates.length };
9012
+ const total = this.countSearch(query, filter);
9013
+ if (total === 0) return { results: [], total: 0 };
9014
+ return { results: this.search(query, filter, { limit }), total };
8731
9015
  }
9016
+ const candidates = this.search(query, filter);
9017
+ if (candidates.length === 0) return { results: [], total: 0 };
8732
9018
  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
- );
9019
+ const bm25 = this.getOrBuildBm25();
8739
9020
  const scored = bm25.score(query, (id) => candidateById.has(id));
8740
9021
  const q = query.trim().toLowerCase();
8741
9022
  const rank = (id) => {
@@ -8777,7 +9058,7 @@ var IndexStore = class _IndexStore {
8777
9058
  // ─── Stats ───────────────────────────────────────────────────────────────────
8778
9059
  getStats() {
8779
9060
  const sizeBytes = this.sizeBytes();
8780
- const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
9061
+ const lastRows = this.stmt("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
8781
9062
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
8782
9063
  const totalRows = this.stmt("SELECT COUNT(*) FROM symbols").all();
8783
9064
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
@@ -8802,19 +9083,20 @@ var IndexStore = class _IndexStore {
8802
9083
  }
8803
9084
  setLastIndexed(ts2) {
8804
9085
  this.runWithRetry(() => {
8805
- this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
9086
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
8806
9087
  });
8807
9088
  }
8808
9089
  getMetadata(key) {
8809
- const rows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all(key);
9090
+ const rows = this.stmt("SELECT value FROM metadata WHERE key = ?").all(key);
8810
9091
  return rows[0]?.value;
8811
9092
  }
8812
9093
  setMetadata(key, value) {
8813
9094
  this.runWithRetry(() => {
8814
- this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
9095
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
8815
9096
  });
8816
9097
  }
8817
9098
  clearAll() {
9099
+ this.invalidateBm25();
8818
9100
  this.runWithRetry(() => {
8819
9101
  this.db.exec("BEGIN IMMEDIATE");
8820
9102
  try {
@@ -8841,13 +9123,7 @@ var IndexStore = class _IndexStore {
8841
9123
  this.runWithRetry(() => {
8842
9124
  this.stmt("DELETE FROM refs WHERE from_id = ?").run(fromId);
8843
9125
  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
- }
9126
+ this.bulkInsertRefs(refs.map((ref) => ({ ...ref, fromId })));
8851
9127
  });
8852
9128
  }
8853
9129
  /**
@@ -8864,13 +9140,7 @@ var IndexStore = class _IndexStore {
8864
9140
  insertRefsBatch(refs) {
8865
9141
  if (refs.length === 0) return;
8866
9142
  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
- }
9143
+ this.bulkInsertRefs(refs);
8874
9144
  });
8875
9145
  }
8876
9146
  /**
@@ -8895,65 +9165,61 @@ var IndexStore = class _IndexStore {
8895
9165
  if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
8896
9166
  return [];
8897
9167
  }
9168
+ this.invalidateBm25();
8898
9169
  return this.runWithRetry(() => {
8899
9170
  this.db.exec("BEGIN IMMEDIATE");
8900
9171
  try {
8901
9172
  if (options.deleteForFiles && options.deleteForFiles.length > 0) {
8902
9173
  const placeholders = options.deleteForFiles.map(() => "?").join(",");
8903
9174
  if (this.ftsAvailable) {
8904
- this.db.prepare(
9175
+ this.stmt(
8905
9176
  `DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
8906
9177
  ).run(...options.deleteForFiles);
8907
9178
  }
8908
- this.db.prepare(
9179
+ this.stmt(
8909
9180
  `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
8910
9181
  ).run(...options.deleteForFiles);
8911
- this.db.prepare(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
9182
+ this.stmt(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
8912
9183
  }
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;
9184
+ const totalSymbols = entries.reduce((n, e) => n + e.symbols.length, 0);
9185
+ let nextId = this.allocateSymbolIds(totalSymbols);
8920
9186
  const allInserted = [];
8921
9187
  const refsToInsert = [];
9188
+ const bulkSyms = [];
9189
+ const ftsRows = [];
8922
9190
  for (const entry of entries) {
8923
9191
  const insertedForEntry = [];
8924
9192
  for (const s of entry.symbols) {
8925
9193
  const id = nextId++;
8926
- symStmt.run(
9194
+ bulkSyms.push({
8927
9195
  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));
9196
+ lang: s.lang,
9197
+ kind: s.kind,
9198
+ name: s.name,
9199
+ file: s.file,
9200
+ line: s.line,
9201
+ col: s.col,
9202
+ signature: s.signature,
9203
+ docComment: s.docComment,
9204
+ scope: s.scope,
9205
+ text: s.text
9206
+ });
9207
+ if (this.ftsAvailable) {
9208
+ ftsRows.push({
9209
+ id,
9210
+ text: buildIndexableText(s.name, s.signature, s.docComment)
9211
+ });
9212
+ }
8941
9213
  const inserted = { ...s, id };
8942
9214
  allInserted.push(inserted);
8943
9215
  insertedForEntry.push(inserted);
8944
9216
  }
8945
9217
  refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
8946
9218
  }
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(
9219
+ this.bulkInsertSymbols(bulkSyms);
9220
+ this.bulkInsertFts(ftsRows);
9221
+ this.bulkInsertRefs(refsToInsert);
9222
+ const upsertStmt = this.stmt(
8957
9223
  `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
8958
9224
  VALUES (?, ?, ?, ?, ?)
8959
9225
  ON CONFLICT(file) DO UPDATE SET
@@ -8980,7 +9246,7 @@ var IndexStore = class _IndexStore {
8980
9246
  */
8981
9247
  deleteRefsForFile(file) {
8982
9248
  this.runWithRetry(() => {
8983
- this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
9249
+ this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
8984
9250
  });
8985
9251
  }
8986
9252
  /**
@@ -8993,20 +9259,75 @@ var IndexStore = class _IndexStore {
8993
9259
  */
8994
9260
  resolveRefs() {
8995
9261
  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;
9262
+ try {
9263
+ const result = this.stmt(
9264
+ `UPDATE refs
9265
+ SET to_id = s.id
9266
+ FROM (
9267
+ SELECT name, MIN(id) AS id FROM symbols GROUP BY name
9268
+ ) AS s
9269
+ WHERE refs.to_id IS NULL
9270
+ AND refs.to_name IS NOT NULL
9271
+ AND refs.to_name = s.name`
9272
+ ).run();
9273
+ return result.changes ?? 0;
9274
+ } catch {
9275
+ const result = this.stmt(
9276
+ `UPDATE refs SET to_id = (
9277
+ SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
9278
+ ) WHERE to_id IS NULL AND to_name IS NOT NULL
9279
+ AND to_name IN (SELECT name FROM symbols)`
9280
+ ).run();
9281
+ return result.changes ?? 0;
9282
+ }
9003
9283
  });
9004
9284
  }
9285
+ /**
9286
+ * Clear symbols/refs for a file and mark it as indexed with zero symbols.
9287
+ * Used by the indexer for empty-parse results so three writes share one txn.
9288
+ */
9289
+ replaceEmptyFile(meta) {
9290
+ this.invalidateBm25();
9291
+ this.runWithRetry(() => {
9292
+ this.db.exec("BEGIN IMMEDIATE");
9293
+ try {
9294
+ if (this.ftsAvailable) {
9295
+ this.stmt(
9296
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
9297
+ ).run(meta.file);
9298
+ }
9299
+ this.stmt(
9300
+ "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
9301
+ ).run(meta.file);
9302
+ this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(meta.file);
9303
+ this.stmt(
9304
+ `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
9305
+ VALUES (?, ?, ?, ?, ?)
9306
+ ON CONFLICT(file) DO UPDATE SET
9307
+ lang = excluded.lang,
9308
+ mtime_ms = excluded.mtime_ms,
9309
+ symbol_count = excluded.symbol_count,
9310
+ last_indexed = excluded.last_indexed`
9311
+ ).run(meta.file, meta.lang, meta.mtimeMs, meta.symbolCount, meta.lastIndexed);
9312
+ this.db.exec("COMMIT");
9313
+ } catch (err) {
9314
+ this.db.exec("ROLLBACK");
9315
+ throw err;
9316
+ }
9317
+ });
9318
+ }
9319
+ /** Best-effort query planner refresh after a large reindex. */
9320
+ optimize() {
9321
+ try {
9322
+ this.db.exec("PRAGMA optimize");
9323
+ } catch {
9324
+ }
9325
+ }
9005
9326
  /**
9006
9327
  * Find all references TO a given symbol (who calls / uses this symbol?).
9007
9328
  */
9008
9329
  findRefsTo(symbolId) {
9009
- return this.db.prepare(
9330
+ return this.stmt(
9010
9331
  "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
9332
  ).all(symbolId, symbolId).map((r) => ({
9012
9333
  id: r.id,
@@ -9021,7 +9342,7 @@ var IndexStore = class _IndexStore {
9021
9342
  * Find all references FROM a given symbol (what does this symbol call/use?).
9022
9343
  */
9023
9344
  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) => ({
9345
+ return this.stmt("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
9025
9346
  id: r.id,
9026
9347
  fromId: r.from_id,
9027
9348
  toName: r.to_name,
@@ -9094,45 +9415,57 @@ var IndexStore = class _IndexStore {
9094
9415
  * symbol resolved in package B). Node metadata includes symbol/file counts.
9095
9416
  */
9096
9417
  getPackageGraph() {
9097
- const symbols = this.db.prepare("SELECT file, id FROM symbols ORDER BY id").all();
9418
+ const fileCounts = this.stmt(
9419
+ "SELECT file, COUNT(*) AS n FROM symbols GROUP BY file"
9420
+ ).all();
9098
9421
  const pkgNodes = /* @__PURE__ */ new Map();
9099
9422
  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);
9423
+ for (const { file, n } of fileCounts) {
9424
+ const pkg = _IndexStore.derivePackage(file) ?? "(root)";
9425
+ fileToPkg.set(file, pkg);
9105
9426
  const node = pkgNodes.get(pkg);
9106
9427
  if (node) {
9107
- node.symbolCount = (node.symbolCount ?? 0) + 1;
9428
+ node.symbolCount = (node.symbolCount ?? 0) + n;
9108
9429
  } else {
9109
9430
  pkgNodes.set(pkg, {
9110
9431
  id: `pkg:${pkg}`,
9111
9432
  label: pkg,
9112
9433
  kind: "package",
9113
9434
  package: pkg,
9114
- symbolCount: 1,
9435
+ symbolCount: n,
9115
9436
  fileCount: 0
9116
9437
  });
9117
9438
  }
9118
9439
  }
9119
- const files = this.db.prepare("SELECT DISTINCT file FROM files").all();
9440
+ const files = this.stmt("SELECT DISTINCT file FROM files").all();
9120
9441
  for (const { file } of files) {
9121
9442
  const pkg = _IndexStore.derivePackage(file) ?? "(root)";
9443
+ fileToPkg.set(file, pkg);
9122
9444
  const node = pkgNodes.get(pkg);
9123
9445
  if (node) node.fileCount = (node.fileCount ?? 0) + 1;
9446
+ else {
9447
+ pkgNodes.set(pkg, {
9448
+ id: `pkg:${pkg}`,
9449
+ label: pkg,
9450
+ kind: "package",
9451
+ package: pkg,
9452
+ symbolCount: 0,
9453
+ fileCount: 1
9454
+ });
9455
+ }
9124
9456
  }
9125
- const refRows = this.db.prepare(
9126
- `SELECT r.from_id, r.to_id, r.call_type
9457
+ const refRows = this.stmt(
9458
+ `SELECT r.call_type, sf.file AS from_file, st.file AS to_file
9127
9459
  FROM refs r
9128
- WHERE r.to_id IS NOT NULL`
9460
+ JOIN symbols sf ON sf.id = r.from_id
9461
+ JOIN symbols st ON st.id = r.to_id
9462
+ WHERE r.to_id IS NOT NULL AND r.call_type != 'import'`
9129
9463
  ).all();
9130
9464
  const edgeMap = /* @__PURE__ */ new Map();
9131
9465
  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;
9466
+ const fromPkg = fileToPkg.get(r.from_file) ?? _IndexStore.derivePackage(r.from_file) ?? "(root)";
9467
+ const toPkg = fileToPkg.get(r.to_file) ?? _IndexStore.derivePackage(r.to_file) ?? "(root)";
9468
+ if (fromPkg === toPkg) continue;
9136
9469
  const key = `${fromPkg}\0${toPkg}`;
9137
9470
  let e = edgeMap.get(key);
9138
9471
  if (!e) {
@@ -9142,13 +9475,14 @@ var IndexStore = class _IndexStore {
9142
9475
  e.weight++;
9143
9476
  e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
9144
9477
  }
9145
- const importRows = this.db.prepare(
9146
- `SELECT r.from_id, r.to_name
9478
+ const importRows = this.stmt(
9479
+ `SELECT r.to_name, s.file AS from_file
9147
9480
  FROM refs r
9481
+ JOIN symbols s ON s.id = r.from_id
9148
9482
  WHERE r.call_type = 'import'`
9149
9483
  ).all();
9150
9484
  for (const r of importRows) {
9151
- const fromPkg = symbolToPkg.get(r.from_id);
9485
+ const fromPkg = fileToPkg.get(r.from_file) ?? _IndexStore.derivePackage(r.from_file) ?? "(root)";
9152
9486
  const toPkg = _IndexStore.packageFromImport(r.to_name);
9153
9487
  if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
9154
9488
  const key = `${fromPkg}\0${toPkg}`;
@@ -9185,12 +9519,12 @@ var IndexStore = class _IndexStore {
9185
9519
  * derived from cross-file symbol references within the package.
9186
9520
  */
9187
9521
  getFileGraph(packageFilter) {
9188
- const allFiles = this.db.prepare("SELECT DISTINCT file FROM symbols").all();
9522
+ const allFiles = this.stmt("SELECT DISTINCT file FROM symbols").all();
9189
9523
  const pkgFilePaths = allFiles.filter((f) => (_IndexStore.derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
9190
9524
  const localFiles = new Set(pkgFilePaths);
9191
9525
  if (localFiles.size === 0) return { nodes: [], edges: [] };
9192
9526
  const filePlaceholders = [...localFiles].map(() => "?").join(",");
9193
- const pkgSyms = this.db.prepare(
9527
+ const pkgSyms = this.stmt(
9194
9528
  `SELECT file, id, name, kind, lang, line FROM symbols WHERE file IN (${filePlaceholders}) ORDER BY id`
9195
9529
  ).all(...pkgFilePaths);
9196
9530
  const fileNodes = /* @__PURE__ */ new Map();
@@ -9222,7 +9556,7 @@ var IndexStore = class _IndexStore {
9222
9556
  ensureFileNode(file);
9223
9557
  }
9224
9558
  const indexedFiles = new Set(allFiles.map((f) => f.file));
9225
- const refRows = this.db.prepare(
9559
+ const refRows = this.stmt(
9226
9560
  `SELECT r.from_id, r.to_id, r.call_type
9227
9561
  FROM refs r
9228
9562
  WHERE (r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
@@ -9237,7 +9571,7 @@ var IndexStore = class _IndexStore {
9237
9571
  }
9238
9572
  if (crossRefIds.size > 0) {
9239
9573
  const crossPlaceholders = [...crossRefIds].map(() => "?").join(",");
9240
- const extras = this.db.prepare(
9574
+ const extras = this.stmt(
9241
9575
  `SELECT id, file FROM symbols WHERE id IN (${crossPlaceholders})`
9242
9576
  ).all(...crossRefIds);
9243
9577
  for (const x of extras) {
@@ -9265,7 +9599,7 @@ var IndexStore = class _IndexStore {
9265
9599
  e.weight++;
9266
9600
  e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
9267
9601
  }
9268
- const importRows = this.db.prepare(
9602
+ const importRows = this.stmt(
9269
9603
  `SELECT r.from_id, r.to_name
9270
9604
  FROM refs r
9271
9605
  WHERE r.call_type = 'import'
@@ -9312,7 +9646,7 @@ var IndexStore = class _IndexStore {
9312
9646
  * derived from intra-file and cross-file symbol references (who calls whom).
9313
9647
  */
9314
9648
  getSymbolGraph(fileFilter) {
9315
- const syms = this.db.prepare(
9649
+ const syms = this.stmt(
9316
9650
  "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE file = ? ORDER BY line, id"
9317
9651
  ).all(fileFilter);
9318
9652
  if (syms.length === 0) return { nodes: [], edges: [] };
@@ -9333,11 +9667,16 @@ var IndexStore = class _IndexStore {
9333
9667
  external: s.file !== fileFilter
9334
9668
  });
9335
9669
  const symIds = new Set(syms.map((s) => s.id));
9336
- const refRows = this.db.prepare(
9670
+ const refRows = this.stmt(
9337
9671
  `SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
9338
9672
  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 = ?)`
9673
+ JOIN symbols s ON s.id = r.from_id
9674
+ WHERE s.file = ?
9675
+ UNION
9676
+ SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
9677
+ FROM refs r
9678
+ JOIN symbols s ON s.id = r.to_id
9679
+ WHERE s.file = ?`
9341
9680
  ).all(fileFilter, fileFilter);
9342
9681
  const edgeMap = /* @__PURE__ */ new Map();
9343
9682
  for (const r of refRows) {
@@ -9376,7 +9715,7 @@ var IndexStore = class _IndexStore {
9376
9715
  const missingIds = [...relatedIds].filter((id) => !loadedIds.has(id));
9377
9716
  if (missingIds.length > 0) {
9378
9717
  const placeholders = missingIds.map(() => "?").join(",");
9379
- const extras = this.db.prepare(
9718
+ const extras = this.stmt(
9380
9719
  `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders})`
9381
9720
  ).all(...missingIds);
9382
9721
  for (const s of extras) symById.set(s.id, s);
@@ -9390,6 +9729,8 @@ var IndexStore = class _IndexStore {
9390
9729
  }
9391
9730
  close() {
9392
9731
  this.stmtCache.clear();
9732
+ this.bm25Dirty = true;
9733
+ this.bm25Cache = null;
9393
9734
  try {
9394
9735
  this.db.close();
9395
9736
  } catch {
@@ -9403,14 +9744,121 @@ import { fileURLToPath } from "node:url";
9403
9744
  import { Worker } from "node:worker_threads";
9404
9745
 
9405
9746
  // src/codebase-index/indexer.ts
9406
- import { expectDefined as expectDefined6 } from "@wrongstack/core";
9747
+ import { expectDefined as expectDefined6 } from "@wrongstack/core/utils";
9407
9748
  import * as fs14 from "node:fs/promises";
9408
- import * as path19 from "node:path";
9749
+ import * as path20 from "node:path";
9409
9750
  import { availableParallelism } from "node:os";
9410
- import { compileGlob as compileGlob2 } from "@wrongstack/core";
9751
+ import { indexParallelBatchSize, isFrugalPerf } from "@wrongstack/core/utils";
9411
9752
 
9412
9753
  // src/codebase-index/ts-parser.ts
9413
9754
  import * as ts from "@typescript/typescript6";
9755
+
9756
+ // src/codebase-index/languages.ts
9757
+ import * as path14 from "node:path";
9758
+ var EXT_TO_LANG = {
9759
+ // TypeScript / JavaScript (first-class TS compiler API)
9760
+ ".ts": "ts",
9761
+ ".mts": "ts",
9762
+ ".cts": "ts",
9763
+ ".tsx": "tsx",
9764
+ ".js": "js",
9765
+ ".mjs": "js",
9766
+ ".cjs": "js",
9767
+ ".jsx": "jsx",
9768
+ // First-class native/spawn parsers (+ regex fallback)
9769
+ ".go": "go",
9770
+ ".py": "py",
9771
+ ".pyi": "py",
9772
+ ".pyw": "py",
9773
+ ".rs": "rs",
9774
+ ".json": "json",
9775
+ ".jsonc": "json",
9776
+ ".yaml": "yaml",
9777
+ ".yml": "yaml",
9778
+ // C family
9779
+ ".c": "c",
9780
+ ".h": "c",
9781
+ ".cc": "cpp",
9782
+ ".cpp": "cpp",
9783
+ ".cxx": "cpp",
9784
+ ".hh": "cpp",
9785
+ ".hpp": "cpp",
9786
+ ".hxx": "cpp",
9787
+ // JVM / .NET
9788
+ ".java": "java",
9789
+ ".cs": "csharp",
9790
+ ".kt": "kotlin",
9791
+ ".kts": "kotlin",
9792
+ ".scala": "scala",
9793
+ ".sc": "scala",
9794
+ // Scripting
9795
+ ".php": "php",
9796
+ ".rb": "ruby",
9797
+ ".swift": "swift",
9798
+ ".dart": "dart",
9799
+ ".lua": "lua",
9800
+ ".r": "r",
9801
+ ".R": "r",
9802
+ ".pl": "other",
9803
+ ".pm": "other",
9804
+ // Systems / functional
9805
+ ".zig": "zig",
9806
+ ".ex": "elixir",
9807
+ ".exs": "elixir",
9808
+ ".hs": "haskell",
9809
+ ".lhs": "haskell",
9810
+ // Shell / data / docs / web
9811
+ ".sh": "shell",
9812
+ ".bash": "shell",
9813
+ ".zsh": "shell",
9814
+ ".ps1": "shell",
9815
+ ".sql": "sql",
9816
+ ".md": "md",
9817
+ ".mdx": "md",
9818
+ ".toml": "toml",
9819
+ ".html": "html",
9820
+ ".htm": "html",
9821
+ ".css": "css",
9822
+ ".scss": "css",
9823
+ ".less": "css",
9824
+ ".vue": "vue",
9825
+ ".svelte": "svelte",
9826
+ ".proto": "proto",
9827
+ ".graphql": "graphql",
9828
+ ".gql": "graphql"
9829
+ };
9830
+ var INDEXABLE_EXTENSIONS = Object.freeze(
9831
+ [...new Set(Object.keys(EXT_TO_LANG).map((e) => e.toLowerCase()))].sort()
9832
+ );
9833
+ var SPECIAL_FILENAMES = {
9834
+ makefile: "other",
9835
+ gnumakefile: "other",
9836
+ dockerfile: "other",
9837
+ "docker-compose.yml": "yaml",
9838
+ "docker-compose.yaml": "yaml",
9839
+ "cmakelists.txt": "other",
9840
+ gemfile: "ruby",
9841
+ rakefile: "ruby",
9842
+ procfile: "other",
9843
+ justfile: "other"
9844
+ };
9845
+ function detectLang(file) {
9846
+ const base = path14.basename(file);
9847
+ const lowerBase = base.toLowerCase();
9848
+ if (lowerBase.endsWith(".d.ts") || lowerBase.endsWith(".d.mts") || lowerBase.endsWith(".d.cts")) {
9849
+ return "ts";
9850
+ }
9851
+ const special = SPECIAL_FILENAMES[lowerBase];
9852
+ if (special) return special;
9853
+ const ext = path14.extname(base).toLowerCase();
9854
+ if (!ext) return null;
9855
+ return EXT_TO_LANG[ext] ?? null;
9856
+ }
9857
+ function isIndexablePath(file) {
9858
+ return detectLang(file) !== null;
9859
+ }
9860
+
9861
+ // src/codebase-index/ts-parser.ts
9414
9862
  var KIND_MAP = {
9415
9863
  [ts.SyntaxKind.ClassDeclaration]: "class",
9416
9864
  [ts.SyntaxKind.InterfaceDeclaration]: "interface",
@@ -9437,32 +9885,6 @@ function kindOf(node) {
9437
9885
  if (ts.isModuleDeclaration(node)) return "namespace";
9438
9886
  return KIND_MAP[node.kind] ?? null;
9439
9887
  }
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
9888
  function getSignature(printer, node, sourceFile) {
9467
9889
  const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
9468
9890
  return raw.replace(/\s+/g, " ").slice(0, 500);
@@ -9585,22 +10007,30 @@ function deduplicateRefs(refs) {
9585
10007
  return true;
9586
10008
  });
9587
10009
  }
9588
- function detectLang(file) {
9589
- const idx = file.lastIndexOf(".");
9590
- if (idx < 0) return null;
9591
- return extToLang(file.slice(idx));
9592
- }
9593
10010
 
9594
10011
  // src/codebase-index/go-parser.ts
9595
10012
  init_win32_resolve();
9596
10013
  import { spawn as spawn4 } from "node:child_process";
9597
10014
  import * as os5 from "node:os";
9598
- import * as path14 from "node:path";
10015
+ import * as path15 from "node:path";
9599
10016
  import * as fs10 from "node:fs/promises";
10017
+
10018
+ // src/codebase-index/spawn-gate.ts
10019
+ var chain = Promise.resolve();
10020
+ function withSpawnGate(fn) {
10021
+ const run = chain.then(fn, fn);
10022
+ chain = run.then(
10023
+ () => void 0,
10024
+ () => void 0
10025
+ );
10026
+ return run;
10027
+ }
10028
+
10029
+ // src/codebase-index/go-parser.ts
9600
10030
  async function parseSymbols2(opts) {
9601
10031
  const { file, content, lang } = opts;
9602
10032
  try {
9603
- const parsed = await syncGoParse(file, content, lang);
10033
+ const parsed = await withSpawnGate(() => syncGoParse(file, content, lang));
9604
10034
  if (parsed.symbols.length > 0) {
9605
10035
  return parsed;
9606
10036
  }
@@ -9896,11 +10326,16 @@ func formatType(t ast.Expr) string {
9896
10326
  }
9897
10327
  }
9898
10328
  `;
10329
+ var _cachedGoScriptPath = null;
9899
10330
  async function syncGoParse(filePath, content, lang) {
9900
- const tmpDir = await fs10.mkdtemp(path14.join(os5.tmpdir(), "ws-go-parse-"));
9901
10331
  try {
9902
- const scriptPath = path14.join(tmpDir, "parse.go");
9903
- await fs10.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
10332
+ let scriptPath = _cachedGoScriptPath;
10333
+ if (!scriptPath) {
10334
+ const tmpDir = await fs10.mkdtemp(path15.join(os5.tmpdir(), "ws-go-parse-"));
10335
+ scriptPath = path15.join(tmpDir, "parse.go");
10336
+ await fs10.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
10337
+ _cachedGoScriptPath = scriptPath;
10338
+ }
9904
10339
  const goBinary = resolveWin32Command("go");
9905
10340
  const goResult = await new Promise(
9906
10341
  (resolve15, reject) => {
@@ -9940,41 +10375,330 @@ async function syncGoParse(filePath, content, lang) {
9940
10375
  if (code !== 0 || !stdout.trim()) {
9941
10376
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
9942
10377
  }
9943
- const raw = JSON.parse(stdout.trim());
9944
- const symbols = raw.map((s) => ({
9945
- id: 0,
9946
- lang,
9947
- kind: s.kind,
9948
- name: s.name,
9949
- file: filePath,
9950
- line: s.line,
9951
- col: s.col,
9952
- signature: s.signature ?? "",
9953
- docComment: "",
9954
- scope: s.scope ?? "",
9955
- text: `${s.name} ${s.signature ?? ""}`.trim()
9956
- }));
9957
- return { file: filePath, lang, symbols, mtimeMs: Date.now() };
9958
- } catch {
9959
- return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
9960
- } finally {
9961
- await fs10.rm(tmpDir, { recursive: true, force: true });
10378
+ const raw = JSON.parse(stdout.trim());
10379
+ const symbols = raw.map((s) => ({
10380
+ id: 0,
10381
+ lang,
10382
+ kind: s.kind,
10383
+ name: s.name,
10384
+ file: filePath,
10385
+ line: s.line,
10386
+ col: s.col,
10387
+ signature: s.signature ?? "",
10388
+ docComment: "",
10389
+ scope: s.scope ?? "",
10390
+ text: `${s.name} ${s.signature ?? ""}`.trim()
10391
+ }));
10392
+ return { file: filePath, lang, symbols, mtimeMs: Date.now() };
10393
+ } catch {
10394
+ return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
10395
+ }
10396
+ }
10397
+
10398
+ // src/codebase-index/py-parser.ts
10399
+ init_win32_resolve();
10400
+ import { spawn as spawn5 } from "node:child_process";
10401
+ import * as fs11 from "node:fs/promises";
10402
+ import * as os6 from "node:os";
10403
+ import * as path16 from "node:path";
10404
+
10405
+ // src/codebase-index/generic-parser.ts
10406
+ var C_LIKE = [
10407
+ { re: /\b(?:class|struct|enum|interface|union)\s+([A-Za-z_]\w*)/g, kind: "class" },
10408
+ {
10409
+ 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,
10410
+ kind: "function"
10411
+ },
10412
+ { re: /\b(?:namespace)\s+([A-Za-z_]\w*)/g, kind: "namespace" }
10413
+ ];
10414
+ var LANG_PATTERNS = {
10415
+ py: [
10416
+ { re: /^(?:async\s+)?def\s+([A-Za-z_]\w*)/gm, kind: "function" },
10417
+ { re: /^class\s+([A-Za-z_]\w*)/gm, kind: "class" },
10418
+ { re: /^([A-Za-z_]\w*)\s*=/gm, kind: "var" }
10419
+ ],
10420
+ go: [
10421
+ { re: /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(/gm, kind: "function" },
10422
+ { re: /^type\s+([A-Za-z_]\w*)\b/gm, kind: "type" },
10423
+ { re: /^(?:const|var)\s+([A-Za-z_]\w*)\b/gm, kind: "const" },
10424
+ { re: /^package\s+([A-Za-z_]\w*)/gm, kind: "namespace" }
10425
+ ],
10426
+ rs: [
10427
+ { re: /\bfn\s+([A-Za-z_]\w*)/g, kind: "function" },
10428
+ { re: /\bstruct\s+([A-Za-z_]\w*)/g, kind: "struct" },
10429
+ { re: /\benum\s+([A-Za-z_]\w*)/g, kind: "enum" },
10430
+ { re: /\btrait\s+([A-Za-z_]\w*)/g, kind: "trait" },
10431
+ { re: /\bimpl(?:\s*<[^>]+>)?\s+([A-Za-z_]\w*)/g, kind: "impl" },
10432
+ { re: /\b(?:const|static)\s+([A-Za-z_]\w*)/g, kind: "const" },
10433
+ { re: /\bmod\s+([A-Za-z_]\w*)/g, kind: "mod" }
10434
+ ],
10435
+ c: C_LIKE,
10436
+ cpp: C_LIKE,
10437
+ java: [
10438
+ { re: /\b(?:class|interface|enum|record)\s+([A-Za-z_]\w*)/g, kind: "class" },
10439
+ {
10440
+ re: /\b(?:public|private|protected|static|final|abstract|synchronized|native|default|\s)+\s*[\w.<>,\[\]\s]+\s+([A-Za-z_]\w*)\s*\(/g,
10441
+ kind: "method"
10442
+ }
10443
+ ],
10444
+ csharp: [
10445
+ { re: /\b(?:class|interface|struct|enum|record)\s+([A-Za-z_]\w*)/g, kind: "class" },
10446
+ { re: /\bnamespace\s+([A-Za-z_.\w]+)/g, kind: "namespace" },
10447
+ {
10448
+ re: /\b(?:public|private|protected|internal|static|async|override|virtual|\s)+\s*[\w.<>,\[\]\s]+\s+([A-Za-z_]\w*)\s*\(/g,
10449
+ kind: "method"
10450
+ }
10451
+ ],
10452
+ php: [
10453
+ { re: /\bfunction\s+([A-Za-z_]\w*)/g, kind: "function" },
10454
+ { re: /\bclass\s+([A-Za-z_]\w*)/g, kind: "class" },
10455
+ { re: /\binterface\s+([A-Za-z_]\w*)/g, kind: "interface" },
10456
+ { re: /\bnamespace\s+([A-Za-z_\\]+)/g, kind: "namespace" }
10457
+ ],
10458
+ ruby: [
10459
+ { re: /^\s*def\s+(?:self\.)?([A-Za-z_]\w*[!?]?)/gm, kind: "function" },
10460
+ { re: /^\s*class\s+([A-Za-z_]\w*)/gm, kind: "class" },
10461
+ { re: /^\s*module\s+([A-Za-z_]\w*)/gm, kind: "namespace" }
10462
+ ],
10463
+ swift: [
10464
+ { re: /\b(?:func)\s+([A-Za-z_]\w*)/g, kind: "function" },
10465
+ { re: /\b(?:class|struct|enum|protocol|actor)\s+([A-Za-z_]\w*)/g, kind: "class" }
10466
+ ],
10467
+ kotlin: [
10468
+ { re: /\b(?:fun)\s+([A-Za-z_]\w*)/g, kind: "function" },
10469
+ { re: /\b(?:class|interface|object|enum\s+class|data\s+class)\s+([A-Za-z_]\w*)/g, kind: "class" }
10470
+ ],
10471
+ scala: [
10472
+ { re: /\b(?:def)\s+([A-Za-z_]\w*)/g, kind: "function" },
10473
+ { re: /\b(?:class|object|trait|enum)\s+([A-Za-z_]\w*)/g, kind: "class" }
10474
+ ],
10475
+ shell: [
10476
+ { re: /^(?:function\s+)?([A-Za-z_]\w*)\s*\(\)\s*\{/gm, kind: "function" },
10477
+ { re: /^([A-Za-z_][\w]*)\s*\(\)\s*\{/gm, kind: "function" }
10478
+ ],
10479
+ sql: [
10480
+ { 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" }
10481
+ ],
10482
+ md: [
10483
+ { re: /^(#{1,6})\s+(.+)$/gm, kind: "namespace" }
10484
+ ],
10485
+ toml: [
10486
+ { re: /^\[([^\]]+)\]/gm, kind: "namespace" }
10487
+ ],
10488
+ html: [
10489
+ { re: /\bid\s*=\s*["']([^"']+)["']/gi, kind: "property" },
10490
+ { re: /<(?:script|template|style)\b/gi, kind: "namespace" }
10491
+ ],
10492
+ css: [
10493
+ { re: /^\s*([.#]?[A-Za-z_][\w-]*)\s*\{/gm, kind: "type" },
10494
+ { re: /@(?:keyframes|media|supports)\s+([^{\s]+)/g, kind: "namespace" }
10495
+ ],
10496
+ vue: [
10497
+ { re: /\b(?:function|const|let|var|class|export\s+(?:default\s+)?(?:function|class|const))\s+([A-Za-z_]\w*)/g, kind: "function" },
10498
+ { re: /<(?:script|template|style)\b/gi, kind: "namespace" }
10499
+ ],
10500
+ svelte: [
10501
+ { re: /\b(?:function|const|let|var|class|export\s+(?:default\s+)?(?:function|class|const))\s+([A-Za-z_]\w*)/g, kind: "function" }
10502
+ ],
10503
+ dart: [
10504
+ { re: /\b(?:class|enum|mixin|extension)\s+([A-Za-z_]\w*)/g, kind: "class" },
10505
+ { re: /\b([A-Za-z_]\w*)\s*\([^;]*\)\s*(?:async\s*)?\{/g, kind: "function" }
10506
+ ],
10507
+ lua: [
10508
+ { re: /\bfunction\s+([A-Za-z_.:]\w*)/g, kind: "function" },
10509
+ { re: /\blocal\s+function\s+([A-Za-z_]\w*)/g, kind: "function" }
10510
+ ],
10511
+ r: [
10512
+ { re: /([A-Za-z.]\w*)\s*<-\s*function\s*\(/g, kind: "function" },
10513
+ { re: /([A-Za-z.]\w*)\s*=\s*function\s*\(/g, kind: "function" }
10514
+ ],
10515
+ proto: [
10516
+ { re: /\b(?:message|service|enum)\s+([A-Za-z_]\w*)/g, kind: "type" },
10517
+ { re: /\brpc\s+([A-Za-z_]\w*)/g, kind: "function" }
10518
+ ],
10519
+ graphql: [
10520
+ { re: /\b(?:type|interface|enum|input|union|scalar)\s+([A-Za-z_]\w*)/g, kind: "type" },
10521
+ { re: /\b(?:query|mutation|subscription)\s+([A-Za-z_]\w*)/g, kind: "function" }
10522
+ ],
10523
+ zig: [
10524
+ { re: /\b(?:fn|pub\s+fn)\s+([A-Za-z_]\w*)/g, kind: "function" },
10525
+ { re: /\b(?:const|var)\s+([A-Za-z_]\w*)/g, kind: "const" },
10526
+ { re: /\b(?:struct|enum|union)\s*\{/g, kind: "type" }
10527
+ ],
10528
+ elixir: [
10529
+ { re: /\bdef(?:p|macro|macrop)?\s+([A-Za-z_]\w*[!?]?)/g, kind: "function" },
10530
+ { re: /\bdefmodule\s+([A-Za-z_.]\w*)/g, kind: "namespace" }
10531
+ ],
10532
+ haskell: [
10533
+ { re: /^([A-Za-z_]\w*)\s*::/gm, kind: "function" },
10534
+ { re: /\bdata\s+([A-Za-z_]\w*)/g, kind: "type" },
10535
+ { re: /\btype\s+(?:family\s+)?([A-Za-z_]\w*)/g, kind: "type" },
10536
+ { re: /\bclass\s+([A-Za-z_]\w*)/g, kind: "class" }
10537
+ ],
10538
+ other: [
10539
+ { re: /^(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_]\w*)/gm, kind: "function" },
10540
+ { re: /^(?:export\s+)?class\s+([A-Za-z_]\w*)/gm, kind: "class" },
10541
+ { re: /^(?:export\s+)?(?:const|let|var)\s+([A-Za-z_]\w*)/gm, kind: "const" },
10542
+ { re: /^(?:async\s+)?def\s+([A-Za-z_]\w*)/gm, kind: "function" },
10543
+ { re: /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)/gm, kind: "function" },
10544
+ { re: /\bfn\s+([A-Za-z_]\w*)/g, kind: "function" },
10545
+ { re: /^(?:target|PHONY)\s*:/gm, kind: "namespace" }
10546
+ ]
10547
+ };
10548
+ var KEYWORDS = /* @__PURE__ */ new Set([
10549
+ "if",
10550
+ "else",
10551
+ "for",
10552
+ "while",
10553
+ "switch",
10554
+ "case",
10555
+ "return",
10556
+ "break",
10557
+ "continue",
10558
+ "new",
10559
+ "delete",
10560
+ "typeof",
10561
+ "instanceof",
10562
+ "void",
10563
+ "null",
10564
+ "true",
10565
+ "false",
10566
+ "this",
10567
+ "super",
10568
+ "import",
10569
+ "export",
10570
+ "from",
10571
+ "as",
10572
+ "default",
10573
+ "public",
10574
+ "private",
10575
+ "protected",
10576
+ "static",
10577
+ "final",
10578
+ "class",
10579
+ "struct",
10580
+ "enum",
10581
+ "interface",
10582
+ "function",
10583
+ "def",
10584
+ "fn",
10585
+ "func",
10586
+ "var",
10587
+ "let",
10588
+ "const",
10589
+ "package",
10590
+ "namespace",
10591
+ "module",
10592
+ "using",
10593
+ "include",
10594
+ "require",
10595
+ "select",
10596
+ "from",
10597
+ "where",
10598
+ "and",
10599
+ "or",
10600
+ "not",
10601
+ "in",
10602
+ "is",
10603
+ "try",
10604
+ "catch",
10605
+ "finally",
10606
+ "throw",
10607
+ "async",
10608
+ "await",
10609
+ "yield"
10610
+ ]);
10611
+ function patternsFor(lang) {
10612
+ return LANG_PATTERNS[lang] ?? LANG_PATTERNS.other ?? [];
10613
+ }
10614
+ function looksBinary(content) {
10615
+ if (content.includes("\0")) return true;
10616
+ const sample = content.slice(0, 2048);
10617
+ if (sample.length === 0) return false;
10618
+ let bad = 0;
10619
+ for (let i = 0; i < sample.length; i++) {
10620
+ const c = sample.charCodeAt(i);
10621
+ if (c === 9 || c === 10 || c === 13) continue;
10622
+ if (c < 32 || c === 127) bad++;
10623
+ }
10624
+ return bad / sample.length > 0.1;
10625
+ }
10626
+ function lineColAt(content, index) {
10627
+ let line = 1;
10628
+ let lastNl = -1;
10629
+ for (let i = 0; i < index && i < content.length; i++) {
10630
+ if (content.charCodeAt(i) === 10) {
10631
+ line++;
10632
+ lastNl = i;
10633
+ }
10634
+ }
10635
+ return { line, col: index - lastNl };
10636
+ }
10637
+ var GENERIC_MAX_SYMBOLS_DEFAULT = 500;
10638
+ var GENERIC_MAX_FILE_CHARS = 512 * 1024;
10639
+ function parseGeneric2(opts) {
10640
+ const { file, lang } = opts;
10641
+ const maxSymbols = opts.maxSymbols ?? GENERIC_MAX_SYMBOLS_DEFAULT;
10642
+ const mtimeMs = Date.now();
10643
+ if (!opts.content || looksBinary(opts.content)) {
10644
+ return { file, lang, symbols: [], mtimeMs };
10645
+ }
10646
+ const content = opts.content.length > GENERIC_MAX_FILE_CHARS ? opts.content.slice(0, GENERIC_MAX_FILE_CHARS) : opts.content;
10647
+ const patterns = patternsFor(lang);
10648
+ const symbols = [];
10649
+ const seen = /* @__PURE__ */ new Set();
10650
+ for (const pattern of patterns) {
10651
+ const re = new RegExp(pattern.re.source, pattern.re.flags.includes("g") ? pattern.re.flags : `${pattern.re.flags}g`);
10652
+ re.lastIndex = 0;
10653
+ let match;
10654
+ while ((match = re.exec(content)) !== null) {
10655
+ if (symbols.length >= maxSymbols) break;
10656
+ let name = (match[1] ?? match[2] ?? "").trim();
10657
+ if (lang === "md" && match[2]) name = match[2].trim();
10658
+ if (!name || name.length > 200) continue;
10659
+ name = name.replace(/^#+\s*/, "").replace(/["'`]/g, "");
10660
+ if (!name || KEYWORDS.has(name.toLowerCase())) continue;
10661
+ if (!/^[A-Za-z_#.@/\w][\w.\-:/#!?]*$/.test(name) && lang !== "md" && lang !== "toml") {
10662
+ continue;
10663
+ }
10664
+ const { line, col } = lineColAt(content, match.index);
10665
+ const key = `${name}\0${line}\0${pattern.kind}`;
10666
+ if (seen.has(key)) continue;
10667
+ seen.add(key);
10668
+ const nl = content.indexOf("\n", match.index);
10669
+ const lineText = content.slice(match.index, nl === -1 ? content.length : nl);
10670
+ const signature = (lineText || name).trim().slice(0, 500);
10671
+ symbols.push({
10672
+ id: 0,
10673
+ lang,
10674
+ kind: lang === "md" ? "namespace" : pattern.kind,
10675
+ name: name.slice(0, 200),
10676
+ file,
10677
+ line,
10678
+ col,
10679
+ signature,
10680
+ docComment: "",
10681
+ scope: "",
10682
+ text: `${name} ${signature}`.trim().slice(0, 1e3)
10683
+ });
10684
+ }
10685
+ if (symbols.length >= maxSymbols) break;
9962
10686
  }
10687
+ return { file, lang, symbols, mtimeMs };
10688
+ }
10689
+ async function parseSymbols3(opts) {
10690
+ return parseGeneric2(opts);
9963
10691
  }
9964
10692
 
9965
10693
  // src/codebase-index/py-parser.ts
9966
- init_win32_resolve();
9967
- import { spawn as spawn5, spawnSync } from "node:child_process";
9968
- import * as fs11 from "node:fs/promises";
9969
- import * as os6 from "node:os";
9970
- import * as path15 from "node:path";
9971
- async function parseSymbols3(opts) {
10694
+ async function parseSymbols4(opts) {
9972
10695
  const { file, content, lang } = opts;
9973
10696
  try {
9974
- return await syncPyParse(file, content, lang);
10697
+ const native = await withSpawnGate(() => syncPyParse(file, content, lang));
10698
+ if (native !== null) return native;
9975
10699
  } catch {
9976
- return { file, lang, symbols: [], mtimeMs: Date.now() };
9977
10700
  }
10701
+ return parseGeneric2({ file, content, lang: lang === "py" ? "py" : lang });
9978
10702
  }
9979
10703
  var PY_PARSE_SCRIPT = `import ast, json, sys, os
9980
10704
 
@@ -10179,20 +10903,37 @@ visitor.visit(tree)
10179
10903
 
10180
10904
  print(json.dumps([s.to_dict() for s in syms]))
10181
10905
  `;
10182
- function resolvePython() {
10906
+ async function resolvePython() {
10183
10907
  const candidates = process.platform === "win32" ? ["python3", "python", "py"] : ["python3", "python"];
10184
10908
  for (const name of candidates) {
10185
10909
  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;
10910
+ if (!await commandIsAvailable(resolved)) continue;
10192
10911
  return resolved;
10193
10912
  }
10194
10913
  return null;
10195
10914
  }
10915
+ function commandIsAvailable(command) {
10916
+ return new Promise((resolve15) => {
10917
+ let settled = false;
10918
+ const proc = spawn5(command, ["--version"], {
10919
+ stdio: "ignore",
10920
+ windowsHide: true
10921
+ });
10922
+ const finish = (available) => {
10923
+ if (settled) return;
10924
+ settled = true;
10925
+ clearTimeout(timer);
10926
+ resolve15(available);
10927
+ };
10928
+ const timer = setTimeout(() => {
10929
+ proc.kill("SIGKILL");
10930
+ finish(false);
10931
+ }, 5e3);
10932
+ timer.unref?.();
10933
+ proc.once("error", () => finish(false));
10934
+ proc.once("close", (code) => finish(code === 0));
10935
+ });
10936
+ }
10196
10937
  function spawnPyParser(pyBinary, scriptPath, filePath, content) {
10197
10938
  return new Promise((resolve15, reject) => {
10198
10939
  let settled = false;
@@ -10228,21 +10969,20 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
10228
10969
  });
10229
10970
  }
10230
10971
  var _cachedScriptPath = null;
10231
- var _cachedPyBinary = null;
10972
+ var cachedPyBinary;
10232
10973
  async function syncPyParse(filePath, content, lang) {
10233
10974
  try {
10234
10975
  if (!_cachedScriptPath) {
10235
- const tmpDir = path15.join(os6.tmpdir(), "ws-py-parse");
10976
+ const tmpDir = path16.join(os6.tmpdir(), "ws-py-parse");
10236
10977
  await fs11.mkdir(tmpDir, { recursive: true });
10237
- _cachedScriptPath = path15.join(tmpDir, "parse.py");
10978
+ _cachedScriptPath = path16.join(tmpDir, "parse.py");
10238
10979
  await fs11.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
10239
10980
  }
10240
- if (!_cachedPyBinary) {
10241
- _cachedPyBinary = resolvePython();
10242
- if (!_cachedPyBinary) return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
10243
- }
10981
+ cachedPyBinary ??= resolvePython();
10982
+ const pyBinary = await cachedPyBinary;
10983
+ if (!pyBinary) return null;
10244
10984
  const { code, stdout } = await spawnPyParser(
10245
- _cachedPyBinary,
10985
+ pyBinary,
10246
10986
  _cachedScriptPath,
10247
10987
  filePath,
10248
10988
  content
@@ -10266,31 +11006,40 @@ async function syncPyParse(filePath, content, lang) {
10266
11006
  }));
10267
11007
  return { file: filePath, lang, symbols, mtimeMs: Date.now() };
10268
11008
  } catch {
10269
- return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
11009
+ return null;
10270
11010
  }
10271
11011
  }
10272
11012
 
10273
11013
  // src/codebase-index/rs-parser.ts
10274
11014
  init_win32_resolve();
10275
- import { expectDefined as expectDefined3 } from "@wrongstack/core";
10276
- import { execFileSync, spawn as spawn6 } from "node:child_process";
11015
+ import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
11016
+ import { execFile, spawn as spawn6 } from "node:child_process";
10277
11017
  import * as fs12 from "node:fs/promises";
10278
- import * as path16 from "node:path";
10279
- async function parseSymbols4(opts) {
11018
+ import * as path17 from "node:path";
11019
+ async function parseSymbols5(opts) {
10280
11020
  const { file, content, lang } = opts;
10281
- const nativeAvailable = checkNativeParser();
11021
+ const nativeAvailable = await checkNativeParser();
10282
11022
  if (nativeAvailable) {
10283
- const result = await tryNativeParse(file, content);
11023
+ const result = await withSpawnGate(() => tryNativeParse(file, content));
10284
11024
  if (result) return result;
10285
11025
  }
10286
11026
  return regexParse({ file, content, lang });
10287
11027
  }
11028
+ var nativeParserAvailability;
11029
+ function probe(command, args) {
11030
+ return new Promise((resolve15, reject) => {
11031
+ execFile(command, args, { timeout: 1e4, windowsHide: true }, (error) => {
11032
+ if (error) reject(error);
11033
+ else resolve15();
11034
+ });
11035
+ });
11036
+ }
10288
11037
  function checkNativeParser() {
10289
- try {
10290
- execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
10291
- const toolsDir = path16.join(process.cwd(), "tools");
11038
+ nativeParserAvailability ??= (async () => {
10292
11039
  try {
10293
- execFileSync(
11040
+ await probe("rustc", ["--version"]);
11041
+ const toolsDir = path17.join(process.cwd(), "tools");
11042
+ await probe(
10294
11043
  "cargo",
10295
11044
  [
10296
11045
  "metadata",
@@ -10298,23 +11047,21 @@ function checkNativeParser() {
10298
11047
  "--format-version",
10299
11048
  "1",
10300
11049
  "--manifest-path",
10301
- path16.join(toolsDir, "Cargo.toml")
10302
- ],
10303
- { stdio: "pipe", windowsHide: true }
11050
+ path17.join(toolsDir, "Cargo.toml")
11051
+ ]
10304
11052
  );
10305
11053
  return true;
10306
11054
  } catch {
10307
11055
  return false;
10308
11056
  }
10309
- } catch {
10310
- return false;
10311
- }
11057
+ })();
11058
+ return nativeParserAvailability;
10312
11059
  }
10313
11060
  async function tryNativeParse(file, content) {
10314
11061
  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");
11062
+ const toolsDir = path17.join(process.cwd(), "tools");
11063
+ const crateDir = path17.join(toolsDir, "syn-parser");
11064
+ const tmpFile = path17.join(crateDir, "src", "input.rs");
10318
11065
  await fs12.writeFile(tmpFile, content, "utf8");
10319
11066
  const cargoBinary = resolveWin32Command("cargo");
10320
11067
  const result = await new Promise(
@@ -10322,7 +11069,7 @@ async function tryNativeParse(file, content) {
10322
11069
  let settled = false;
10323
11070
  const proc = spawn6(
10324
11071
  cargoBinary,
10325
- ["run", "--manifest-path", path16.join(toolsDir, "Cargo.toml")],
11072
+ ["run", "--manifest-path", path17.join(toolsDir, "Cargo.toml")],
10326
11073
  {
10327
11074
  cwd: process.cwd(),
10328
11075
  stdio: ["pipe", "pipe", "pipe"],
@@ -10436,9 +11183,9 @@ function regexParse(opts) {
10436
11183
  }
10437
11184
 
10438
11185
  // 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) {
11186
+ import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
11187
+ import * as path18 from "node:path";
11188
+ function parseSymbols6(opts) {
10442
11189
  const { file, content, lang } = opts;
10443
11190
  try {
10444
11191
  return regexParse2({ file, content, lang });
@@ -10449,9 +11196,9 @@ function parseSymbols5(opts) {
10449
11196
  function regexParse2(opts) {
10450
11197
  const { file, content, lang } = opts;
10451
11198
  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";
11199
+ const basename12 = path18.basename(file).toLowerCase();
11200
+ const isPackageJson = basename12 === "package.json";
11201
+ const isTsconfig = basename12 === "tsconfig.json" || basename12 === "tsconfig.build.json";
10455
11202
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
10456
11203
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
10457
11204
  const lines = content.split("\n");
@@ -10475,11 +11222,11 @@ function regexParse2(opts) {
10475
11222
  const line = lineFromOffset(offset);
10476
11223
  symbols.push(
10477
11224
  makeSymbol({
10478
- name: path17.basename(file),
11225
+ name: path18.basename(file),
10479
11226
  kind: "object",
10480
11227
  line,
10481
11228
  col: 0,
10482
- signature: `"${path17.basename(file)}" = { ... }`,
11229
+ signature: `"${path18.basename(file)}" = { ... }`,
10483
11230
  file,
10484
11231
  lang
10485
11232
  })
@@ -10641,8 +11388,8 @@ function makeSymbol(opts) {
10641
11388
  }
10642
11389
 
10643
11390
  // src/codebase-index/yaml-parser.ts
10644
- import { expectDefined as expectDefined5, truncate } from "@wrongstack/core";
10645
- function parseSymbols6(opts) {
11391
+ import { expectDefined as expectDefined5, truncate } from "@wrongstack/core/utils";
11392
+ function parseSymbols7(opts) {
10646
11393
  const { file, content, lang } = opts;
10647
11394
  try {
10648
11395
  return regexParse3({ file, content, lang });
@@ -10791,8 +11538,8 @@ function makeSymbol2(opts) {
10791
11538
 
10792
11539
  // src/codebase-index/gitignore.ts
10793
11540
  import * as fs13 from "node:fs/promises";
10794
- import * as path18 from "node:path";
10795
- import { compileGlob } from "@wrongstack/core";
11541
+ import * as path19 from "node:path";
11542
+ import { compileGlob } from "@wrongstack/core/utils";
10796
11543
  function globBody(glob) {
10797
11544
  return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
10798
11545
  }
@@ -10837,7 +11584,7 @@ function compileGitignore(lines) {
10837
11584
  async function loadGitignoreMatcher(projectRoot) {
10838
11585
  let lines = [];
10839
11586
  try {
10840
- const raw = await fs13.readFile(path18.join(projectRoot, ".gitignore"), "utf8");
11587
+ const raw = await fs13.readFile(path19.join(projectRoot, ".gitignore"), "utf8");
10841
11588
  lines = raw.split("\n");
10842
11589
  } catch {
10843
11590
  }
@@ -10846,7 +11593,9 @@ async function loadGitignoreMatcher(projectRoot) {
10846
11593
 
10847
11594
  // src/codebase-index/indexer.ts
10848
11595
  var YIELD_EVERY_N = 50;
10849
- var PARALLEL_BATCH = Math.min(availableParallelism() * 4, 40);
11596
+ function resolveParallelBatch() {
11597
+ return indexParallelBatchSize(availableParallelism());
11598
+ }
10850
11599
  function yieldEventLoop() {
10851
11600
  return new Promise((resolve15) => setImmediate(resolve15));
10852
11601
  }
@@ -10872,8 +11621,8 @@ var DEFAULT_IGNORE = [
10872
11621
  var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
10873
11622
  var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
10874
11623
  function isWithinProject(projectRoot, file) {
10875
- const rel = path19.relative(projectRoot, file);
10876
- return rel !== "" && !rel.startsWith(`..${path19.sep}`) && rel !== ".." && !path19.isAbsolute(rel);
11624
+ const rel = path20.relative(projectRoot, file);
11625
+ return rel !== "" && !rel.startsWith(`..${path20.sep}`) && rel !== ".." && !path20.isAbsolute(rel);
10877
11626
  }
10878
11627
  function isMissingPathError(err) {
10879
11628
  const code = err?.code;
@@ -10884,18 +11633,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10884
11633
  const errors = [];
10885
11634
  let complete = true;
10886
11635
  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
- ];
11636
+ const indexableExts = new Set(INDEXABLE_EXTENSIONS);
10899
11637
  let dirCount = 0;
10900
11638
  const walk2 = async (dir) => {
10901
11639
  throwIfAborted(signal);
@@ -10914,19 +11652,16 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10914
11652
  dirCount++;
10915
11653
  for (const e of entries) {
10916
11654
  if (ignoreSet.has(e.name)) continue;
10917
- const full = path19.join(dir, e.name);
10918
- const rel = path19.relative(projectRoot, full).replace(/\\/g, "/");
11655
+ const full = path20.join(dir, e.name);
11656
+ const rel = path20.relative(projectRoot, full).replace(/\\/g, "/");
10919
11657
  if (e.isDirectory()) {
10920
11658
  if (isGitIgnored(rel, true)) continue;
10921
11659
  await walk2(full);
10922
11660
  } else if (e.isFile()) {
10923
11661
  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
- }
11662
+ const ext = path20.extname(e.name).toLowerCase();
11663
+ if (indexableExts.has(ext) || detectLang(full) !== null) {
11664
+ results.push(full);
10930
11665
  }
10931
11666
  }
10932
11667
  }
@@ -10935,24 +11670,25 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10935
11670
  return { files: results, complete, errors };
10936
11671
  }
10937
11672
  async function parseFile(file, content, lang) {
10938
- switch (lang) {
11673
+ const symbolLang = lang;
11674
+ switch (symbolLang) {
10939
11675
  case "ts":
10940
11676
  case "tsx":
10941
11677
  case "js":
10942
11678
  case "jsx":
10943
- return parseSymbols({ file, content, lang });
11679
+ return parseSymbols({ file, content, lang: symbolLang });
10944
11680
  case "go":
10945
11681
  return parseSymbols2({ file, content, lang: "go" });
10946
11682
  case "py":
10947
- return parseSymbols3({ file, content, lang: "py" });
11683
+ return parseSymbols4({ file, content, lang: "py" });
10948
11684
  case "rs":
10949
- return parseSymbols4({ file, content, lang: "rs" });
11685
+ return parseSymbols5({ file, content, lang: "rs" });
10950
11686
  case "json":
10951
- return parseSymbols5({ file, content, lang: "json" });
11687
+ return parseSymbols6({ file, content, lang: "json" });
10952
11688
  case "yaml":
10953
- return parseSymbols6({ file, content, lang: "yaml" });
11689
+ return parseSymbols7({ file, content, lang: "yaml" });
10954
11690
  default:
10955
- return { file, lang, symbols: [], mtimeMs: Date.now() };
11691
+ return parseSymbols3({ file, content, lang: symbolLang });
10956
11692
  }
10957
11693
  }
10958
11694
  function assignRefsToSymbols2(refs, symbols) {
@@ -11000,10 +11736,10 @@ async function runIndexerWithStore(store, opts) {
11000
11736
  let discoveredFiles = null;
11001
11737
  let discoveryComplete = true;
11002
11738
  if (opts.files && opts.files.length > 0) {
11003
- files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => {
11739
+ files = opts.files.map((f) => path20.resolve(projectRoot, f)).filter((f) => {
11004
11740
  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);
11741
+ const rel = path20.relative(projectRoot, f).replace(/\\/g, "/");
11742
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path20.basename(f)) && !isGitIgnored(rel, false);
11007
11743
  });
11008
11744
  } else {
11009
11745
  const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
@@ -11024,15 +11760,19 @@ async function runIndexerWithStore(store, opts) {
11024
11760
  if (!force) {
11025
11761
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
11026
11762
  }
11763
+ const parallelBatch = resolveParallelBatch();
11027
11764
  let filesSinceLastYield = 0;
11028
- for (let batchStart = 0; batchStart < files.length; batchStart += PARALLEL_BATCH) {
11029
- const batchEnd = Math.min(batchStart + PARALLEL_BATCH, files.length);
11765
+ for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
11766
+ const batchEnd = Math.min(batchStart + parallelBatch, files.length);
11030
11767
  const batchFiles = files.slice(batchStart, batchEnd);
11031
11768
  opts.onProgress?.(batchEnd, files.length);
11032
11769
  filesSinceLastYield += batchFiles.length;
11033
11770
  if (filesSinceLastYield >= YIELD_EVERY_N) {
11034
11771
  filesSinceLastYield = 0;
11035
11772
  await yieldEventLoop();
11773
+ if (isFrugalPerf()) {
11774
+ await new Promise((r) => setTimeout(r, 8));
11775
+ }
11036
11776
  throwIfAborted(signal);
11037
11777
  }
11038
11778
  const statOpts = signal ? { signal } : {};
@@ -11136,9 +11876,7 @@ async function runIndexerWithStore(store, opts) {
11136
11876
  continue;
11137
11877
  }
11138
11878
  if (parsed.symbols.length === 0) {
11139
- store.deleteRefsForFile(file);
11140
- store.deleteSymbolsForFile(file);
11141
- store.upsertFile({
11879
+ store.replaceEmptyFile({
11142
11880
  file,
11143
11881
  lang,
11144
11882
  mtimeMs: Math.floor(stat18.mtimeMs),
@@ -11207,6 +11945,7 @@ async function runIndexerWithStore(store, opts) {
11207
11945
  }
11208
11946
  store.resolveRefs();
11209
11947
  store.setMetadata("relation_graph_version", relationGraphVersion);
11948
+ store.optimize();
11210
11949
  const durationMs = Date.now() - startMs;
11211
11950
  store.setLastIndexed(Date.now());
11212
11951
  return {
@@ -11497,10 +12236,10 @@ async function callInline(op, args, opts) {
11497
12236
  opts.signal?.removeEventListener("abort", onOuterAbort);
11498
12237
  }
11499
12238
  }
11500
- var chain = Promise.resolve();
12239
+ var chain2 = Promise.resolve();
11501
12240
  function withMutex(job) {
11502
- const run = chain.then(job, job);
11503
- chain = run.then(
12241
+ const run = chain2.then(job, job);
12242
+ chain2 = run.then(
11504
12243
  () => void 0,
11505
12244
  () => void 0
11506
12245
  );
@@ -11514,11 +12253,62 @@ function circuitOpenError() {
11514
12253
  }
11515
12254
  var DEFAULT_DEBOUNCE_MS = 400;
11516
12255
  var debounceTimers = /* @__PURE__ */ new Map();
11517
- function debounceKey(indexDir, file) {
11518
- return `${indexDir ?? ""}|${file}`;
12256
+ var readyReindexBatches = /* @__PURE__ */ new Map();
12257
+ function debounceKey(projectRoot, indexDir, file) {
12258
+ return JSON.stringify([projectRoot, indexDir ?? "", file]);
12259
+ }
12260
+ function reindexBatchKey(projectRoot, indexDir) {
12261
+ return JSON.stringify([projectRoot, indexDir ?? ""]);
12262
+ }
12263
+ function flushReadyReindexBatch(key) {
12264
+ const batch = readyReindexBatches.get(key);
12265
+ if (!batch) return;
12266
+ readyReindexBatches.delete(key);
12267
+ if (!indexCircuitBreaker.allowRequest()) {
12268
+ const error = circuitOpenError();
12269
+ for (const onError of batch.onErrors) onError(error);
12270
+ return;
12271
+ }
12272
+ void withMutex(
12273
+ () => callIndexOp(
12274
+ "index",
12275
+ {
12276
+ projectRoot: batch.projectRoot,
12277
+ files: [...batch.files].sort(),
12278
+ indexDir: batch.indexDir
12279
+ },
12280
+ { timeoutMs: batch.timeoutMs }
12281
+ )
12282
+ ).then(
12283
+ () => indexCircuitBreaker.recordSuccess(),
12284
+ (err) => {
12285
+ indexCircuitBreaker.recordFailure(err);
12286
+ for (const onError of batch.onErrors) onError(err);
12287
+ }
12288
+ );
12289
+ }
12290
+ function addReadyReindex(opts) {
12291
+ const key = reindexBatchKey(opts.projectRoot, opts.indexDir);
12292
+ const existing = readyReindexBatches.get(key);
12293
+ if (existing) {
12294
+ existing.files.add(opts.file);
12295
+ existing.timeoutMs = Math.max(existing.timeoutMs, opts.timeoutMs);
12296
+ if (opts.onError) existing.onErrors.add(opts.onError);
12297
+ return;
12298
+ }
12299
+ const batch = {
12300
+ projectRoot: opts.projectRoot,
12301
+ indexDir: opts.indexDir,
12302
+ files: /* @__PURE__ */ new Set([opts.file]),
12303
+ timeoutMs: opts.timeoutMs,
12304
+ onErrors: new Set(opts.onError ? [opts.onError] : [])
12305
+ };
12306
+ batch.flush = setImmediate(() => flushReadyReindexBatch(key));
12307
+ batch.flush.unref?.();
12308
+ readyReindexBatches.set(key, batch);
11519
12309
  }
11520
12310
  function isIndexableFile(filePath) {
11521
- return detectLang(filePath) !== null;
12311
+ return isIndexablePath(filePath);
11522
12312
  }
11523
12313
  function isRecoverableConstraintError(err) {
11524
12314
  if (err instanceof Error) {
@@ -11578,28 +12368,18 @@ function enqueueReindex(opts) {
11578
12368
  if (files.length === 0) return;
11579
12369
  const ms = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
11580
12370
  for (const file of files) {
11581
- const key = debounceKey(opts.indexDir, file);
12371
+ const key = debounceKey(opts.projectRoot, opts.indexDir, file);
11582
12372
  const existing = debounceTimers.get(key);
11583
12373
  if (existing) clearTimeout(existing);
11584
12374
  const timer = setTimeout(() => {
11585
12375
  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
- );
12376
+ addReadyReindex({
12377
+ projectRoot: opts.projectRoot,
12378
+ file,
12379
+ indexDir: opts.indexDir,
12380
+ timeoutMs: opts.timeoutMs ?? DEFAULT_INCREMENTAL_TIMEOUT_MS,
12381
+ onError: opts.onError
12382
+ });
11603
12383
  }, ms);
11604
12384
  timer.unref?.();
11605
12385
  debounceTimers.set(key, timer);
@@ -11608,6 +12388,10 @@ function enqueueReindex(opts) {
11608
12388
  function cancelPendingReindexes() {
11609
12389
  for (const t of debounceTimers.values()) clearTimeout(t);
11610
12390
  debounceTimers.clear();
12391
+ for (const batch of readyReindexBatches.values()) {
12392
+ if (batch.flush) clearImmediate(batch.flush);
12393
+ }
12394
+ readyReindexBatches.clear();
11611
12395
  }
11612
12396
  async function searchCodebaseIndex(args, opts = {}) {
11613
12397
  return callIndexOp("search", args, {
@@ -11893,7 +12677,7 @@ var codebaseStatsTool = {
11893
12677
 
11894
12678
  // src/design.ts
11895
12679
  import * as fs16 from "node:fs/promises";
11896
- import * as path20 from "node:path";
12680
+ import * as path21 from "node:path";
11897
12681
  import {
11898
12682
  applyTokenOverrides,
11899
12683
  getDesignKitLoader,
@@ -11902,23 +12686,24 @@ import {
11902
12686
  materializeTokens,
11903
12687
  recordKitChoice,
11904
12688
  recordOverrides,
12689
+ resolveSemanticTune,
11905
12690
  runDesignVerify,
11906
12691
  setActiveKit,
11907
12692
  setDesignOverrides
11908
- } from "@wrongstack/core";
12693
+ } from "@wrongstack/core/design";
11909
12694
  async function resolveReal(p) {
11910
- const resolved = path20.resolve(p);
11911
- let probe = resolved;
12695
+ const resolved = path21.resolve(p);
12696
+ let probe2 = resolved;
11912
12697
  const missing = [];
11913
12698
  for (; ; ) {
11914
12699
  try {
11915
- return path20.resolve(await fs16.realpath(probe), ...missing);
12700
+ return path21.resolve(await fs16.realpath(probe2), ...missing);
11916
12701
  } catch (err) {
11917
12702
  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;
12703
+ const parent = path21.dirname(probe2);
12704
+ if (parent === probe2) return resolved;
12705
+ missing.unshift(path21.basename(probe2));
12706
+ probe2 = parent;
11922
12707
  continue;
11923
12708
  }
11924
12709
  return resolved;
@@ -11948,8 +12733,8 @@ var designTool = {
11948
12733
  properties: {
11949
12734
  action: {
11950
12735
  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."
12736
+ enum: ["list", "use", "foundations", "set", "tune", "materialize", "verify"],
12737
+ 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
12738
  },
11954
12739
  kit: {
11955
12740
  type: "string",
@@ -11965,6 +12750,16 @@ var designTool = {
11965
12750
  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
12751
  additionalProperties: { type: "string" }
11967
12752
  },
12753
+ tune: {
12754
+ type: "object",
12755
+ 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.',
12756
+ properties: {
12757
+ radius: { type: "string" },
12758
+ density: { type: "string" },
12759
+ font: { type: "string" },
12760
+ motion: { type: "string" }
12761
+ }
12762
+ },
11968
12763
  out: {
11969
12764
  type: "string",
11970
12765
  description: "Materialize output path (project-relative). Defaults to a per-stack convention."
@@ -12062,6 +12857,28 @@ ${body}`
12062
12857
  These win over kit tokens. Run \`design {action:"materialize"}\` to write them to a theme file.`
12063
12858
  };
12064
12859
  }
12860
+ if (action === "tune") {
12861
+ const patch = resolveSemanticTune(input.tune ?? {});
12862
+ if (Object.keys(patch).length === 0) {
12863
+ return {
12864
+ action,
12865
+ output: 'No recognized knobs. Pass tune:{ radius:"lg", density:"compact", font:"\u2026", motion:"snappy" }.'
12866
+ };
12867
+ }
12868
+ const merged = await recordOverrides(ctx.projectRoot, patch, (/* @__PURE__ */ new Date()).toISOString());
12869
+ if (!merged) {
12870
+ return {
12871
+ action,
12872
+ output: 'No active kit. Pick one first: `design {action:"use", kit:"<id>"}`.'
12873
+ };
12874
+ }
12875
+ setDesignOverrides(ctx, merged);
12876
+ return {
12877
+ action,
12878
+ output: `Tuned. Resolved ${Object.keys(patch).length} token override(s): ${Object.entries(patch).map(([k, v]) => `${k}=${v}`).join(", ")}
12879
+ Run \`design {action:"materialize"}\` to write the tuned tokens to your theme file.`
12880
+ };
12881
+ }
12065
12882
  if (action === "materialize") {
12066
12883
  const active = await loadActiveKit(ctx.projectRoot);
12067
12884
  if (!active) {
@@ -12083,11 +12900,11 @@ These win over kit tokens. Run \`design {action:"materialize"}\` to write them t
12083
12900
  outPath: input.out
12084
12901
  });
12085
12902
  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)) {
12903
+ const absResolved = path21.isAbsolute(result.path) ? path21.resolve(result.path) : path21.resolve(path21.join(ctx.projectRoot, result.path));
12904
+ const absParent = await resolveReal(path21.dirname(absResolved));
12905
+ const abs = path21.join(absParent, path21.basename(absResolved));
12906
+ const rel = path21.relative(root, abs);
12907
+ if (rel.startsWith("..") || path21.isAbsolute(rel)) {
12091
12908
  throw new Error(
12092
12909
  `design: materialize path "${result.path}" would escape the project root`
12093
12910
  );
@@ -12111,7 +12928,7 @@ ${result.content}
12111
12928
  \`\`\``
12112
12929
  };
12113
12930
  }
12114
- await fs16.mkdir(path20.dirname(abs), { recursive: true });
12931
+ await fs16.mkdir(path21.dirname(abs), { recursive: true });
12115
12932
  await fs16.writeFile(abs, result.content);
12116
12933
  return {
12117
12934
  action,
@@ -12163,8 +12980,8 @@ init_util();
12163
12980
  import { spawn as spawn7 } from "node:child_process";
12164
12981
  import { statSync as statSync2 } from "node:fs";
12165
12982
  import * as fs17 from "node:fs/promises";
12166
- import * as path21 from "node:path";
12167
- import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core";
12983
+ import * as path22 from "node:path";
12984
+ import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core/utils";
12168
12985
  var diffTool = {
12169
12986
  name: "diff",
12170
12987
  category: "Filesystem",
@@ -12247,11 +13064,11 @@ function findGitDir(cwd) {
12247
13064
  let dir = cwd;
12248
13065
  for (let i = 0; i < 20; i++) {
12249
13066
  try {
12250
- const stat18 = statSync2(path21.join(dir, ".git"));
13067
+ const stat18 = statSync2(path22.join(dir, ".git"));
12251
13068
  if (stat18.isDirectory()) return dir;
12252
13069
  } catch {
12253
13070
  }
12254
- const parent = path21.dirname(dir);
13071
+ const parent = path22.dirname(dir);
12255
13072
  if (parent === dir) break;
12256
13073
  dir = parent;
12257
13074
  }
@@ -12465,7 +13282,7 @@ function processFile(content, absPath, _style, _overwrite, target) {
12465
13282
  // src/e2e.ts
12466
13283
  init_util();
12467
13284
  import { open, readdir as readdir5 } from "node:fs/promises";
12468
- import * as path22 from "node:path";
13285
+ import * as path23 from "node:path";
12469
13286
  async function readBoundedText(filePath, maxBytes) {
12470
13287
  let handle;
12471
13288
  try {
@@ -12515,8 +13332,8 @@ var MAX_PACKAGE_BYTES = 512 * 1024;
12515
13332
  var MAX_CONFIG_BYTES = 512 * 1024;
12516
13333
  var MAX_SPEC_SAMPLES = 20;
12517
13334
  function relativePath(root, target) {
12518
- const value = path22.relative(root, target) || ".";
12519
- return value.split(path22.sep).join("/");
13335
+ const value = path23.relative(root, target) || ".";
13336
+ return value.split(path23.sep).join("/");
12520
13337
  }
12521
13338
  function escapeRegExp(value) {
12522
13339
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -12588,7 +13405,7 @@ async function scanWorkspace(root, maxDepth, signal) {
12588
13405
  }
12589
13406
  for (const entry of entries) {
12590
13407
  signal.throwIfAborted();
12591
- const absolutePath = path22.join(current.directory, entry.name);
13408
+ const absolutePath = path23.join(current.directory, entry.name);
12592
13409
  if (entry.isFile()) {
12593
13410
  if (entry.name === "package.json") result.packageFiles.push(absolutePath);
12594
13411
  const framework = CONFIG_NAMES.get(entry.name);
@@ -12649,9 +13466,9 @@ async function detectPackageManager3(projectRoot, scanRoot, declared) {
12649
13466
  if (names.has("bun.lock") || names.has("bun.lockb")) return "bun";
12650
13467
  if (names.has("package-lock.json") || names.has("npm-shrinkwrap.json")) return "npm";
12651
13468
  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)) {
13469
+ const parent = path23.dirname(directory);
13470
+ const relativeParent = path23.relative(scanRoot, parent);
13471
+ if (parent === directory || relativeParent.startsWith("..") || path23.isAbsolute(relativeParent)) {
12655
13472
  break;
12656
13473
  }
12657
13474
  directory = parent;
@@ -12696,7 +13513,7 @@ function isSpec(framework, filename) {
12696
13513
  return /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/i.test(filename);
12697
13514
  }
12698
13515
  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")];
13516
+ const roots = testDirectory ? [path23.resolve(root, testDirectory)] : framework === "cypress" ? [path23.join(root, "cypress", "e2e"), path23.join(root, "cypress", "integration")] : [path23.join(root, "tests")];
12700
13517
  const samples = [];
12701
13518
  let count = 0;
12702
13519
  let scanned = 0;
@@ -12715,7 +13532,7 @@ async function collectSpecs(root, framework, testDirectory, signal) {
12715
13532
  }
12716
13533
  for (const entry of entries) {
12717
13534
  signal.throwIfAborted();
12718
- const target = path22.join(directory, entry.name);
13535
+ const target = path23.join(directory, entry.name);
12719
13536
  if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) queue.push(target);
12720
13537
  else if (entry.isFile() && isSpec(framework, entry.name)) {
12721
13538
  count += 1;
@@ -12735,7 +13552,7 @@ function nearestPackage(projectRoot, packagesByDirectory, scanRoot) {
12735
13552
  const found = packagesByDirectory.get(directory);
12736
13553
  if (found) return found;
12737
13554
  if (directory === scanRoot) return void 0;
12738
- const parent = path22.dirname(directory);
13555
+ const parent = path23.dirname(directory);
12739
13556
  if (parent === directory || relativePath(scanRoot, parent).startsWith("..")) return void 0;
12740
13557
  directory = parent;
12741
13558
  }
@@ -12746,13 +13563,13 @@ async function discoverE2EProjects(root, options) {
12746
13563
  const packages = (await Promise.all(scan.packageFiles.map(readPackageInfo))).filter(
12747
13564
  (info) => Boolean(info)
12748
13565
  );
12749
- const packagesByDirectory = new Map(packages.map((info) => [path22.dirname(info.path), info]));
13566
+ const packagesByDirectory = new Map(packages.map((info) => [path23.dirname(info.path), info]));
12750
13567
  const candidates = /* @__PURE__ */ new Map();
12751
13568
  for (const config of scan.configs) {
12752
13569
  if (options.framework && options.framework !== "all" && config.framework !== options.framework) {
12753
13570
  continue;
12754
13571
  }
12755
- const projectRoot = path22.dirname(config.absolutePath);
13572
+ const projectRoot = path23.dirname(config.absolutePath);
12756
13573
  candidates.set(`${config.framework}:${projectRoot}`, {
12757
13574
  framework: config.framework,
12758
13575
  root: projectRoot,
@@ -12760,7 +13577,7 @@ async function discoverE2EProjects(root, options) {
12760
13577
  });
12761
13578
  }
12762
13579
  for (const info of packages) {
12763
- const projectRoot = path22.dirname(info.path);
13580
+ const projectRoot = path23.dirname(info.path);
12764
13581
  for (const framework of frameworkFromPackage(info)) {
12765
13582
  if (options.framework && options.framework !== "all" && framework !== options.framework)
12766
13583
  continue;
@@ -12776,10 +13593,10 @@ async function discoverE2EProjects(root, options) {
12776
13593
  const scripts = matchingScripts(info, candidate.framework);
12777
13594
  const manager = await detectPackageManager3(candidate.root, root, info?.packageManager);
12778
13595
  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;
13596
+ const resolvedTestDirectory = testDirectory ? path23.resolve(candidate.root, testDirectory) : void 0;
13597
+ const relativeTestDirectory = resolvedTestDirectory ? path23.relative(root, resolvedTestDirectory) : void 0;
12781
13598
  const unsafeTestDirectory = Boolean(
12782
- relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path22.isAbsolute(relativeTestDirectory))
13599
+ relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path23.isAbsolute(relativeTestDirectory))
12783
13600
  );
12784
13601
  const specs = options.includeSpecs === false || unsafeTestDirectory ? { count: 0, samples: [], truncated: false } : await collectSpecs(candidate.root, candidate.framework, testDirectory, options.signal);
12785
13602
  const warnings = [];
@@ -12884,10 +13701,10 @@ import {
12884
13701
  atomicWrite as atomicWrite2,
12885
13702
  detectNewlineStyle,
12886
13703
  normalizeToLf,
12887
- ToolValidationError,
12888
13704
  toStyle,
12889
13705
  unifiedDiff
12890
- } from "@wrongstack/core";
13706
+ } from "@wrongstack/core/utils";
13707
+ import { ToolValidationError } from "@wrongstack/core/types";
12891
13708
 
12892
13709
  // src/_edit-match.ts
12893
13710
  var TIER_LABEL = {
@@ -13115,7 +13932,7 @@ function prefixSimilarity(a, b) {
13115
13932
  }
13116
13933
 
13117
13934
  // src/_syntax-check.ts
13118
- import * as path23 from "node:path";
13935
+ import * as path24 from "node:path";
13119
13936
  var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
13120
13937
  var MAX_CHECK_CHARS = 15e5;
13121
13938
  var MAX_ERRORS = 5;
@@ -13139,15 +13956,15 @@ async function checkSyntax(filePath, content, previousContent) {
13139
13956
  return { errors, preExisting };
13140
13957
  }
13141
13958
  function isJsoncFile(filePath) {
13142
- const base = path23.basename(filePath).toLowerCase();
13959
+ const base = path24.basename(filePath).toLowerCase();
13143
13960
  if (base.endsWith(".jsonc")) return true;
13144
13961
  if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
13145
- const dir = path23.basename(path23.dirname(filePath)).toLowerCase();
13962
+ const dir = path24.basename(path24.dirname(filePath)).toLowerCase();
13146
13963
  return dir === ".vscode";
13147
13964
  }
13148
13965
  async function parseErrors(filePath, content) {
13149
13966
  if (content.length > MAX_CHECK_CHARS) return void 0;
13150
- const ext = path23.extname(filePath).toLowerCase();
13967
+ const ext = path24.extname(filePath).toLowerCase();
13151
13968
  if (ext === ".json" || ext === ".jsonc") {
13152
13969
  try {
13153
13970
  JSON.parse(content);
@@ -13174,7 +13991,7 @@ async function parseErrors(filePath, content) {
13174
13991
  ts2.ScriptKind.JSX
13175
13992
  );
13176
13993
  const sourceFile = ts2.createSourceFile(
13177
- path23.basename(filePath),
13994
+ path24.basename(filePath),
13178
13995
  content,
13179
13996
  ts2.ScriptTarget.Latest,
13180
13997
  /* setParentNodes */
@@ -13409,7 +14226,7 @@ import {
13409
14226
  emitProcessCompleted as emitProcessCompleted3,
13410
14227
  emitProcessOutput as emitProcessOutput3,
13411
14228
  emitProcessStarted as emitProcessStarted3
13412
- } from "@wrongstack/core";
14229
+ } from "@wrongstack/core/observability";
13413
14230
  import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils/error";
13414
14231
  init_output_spool();
13415
14232
  init_util();
@@ -13418,7 +14235,7 @@ init_win32_resolve();
13418
14235
 
13419
14236
  // src/exec-kill-guard.ts
13420
14237
  import * as os7 from "node:os";
13421
- import * as path24 from "node:path";
14238
+ import * as path25 from "node:path";
13422
14239
  var isWin3 = os7.platform() === "win32";
13423
14240
  async function checkExecKillCommand(cmd, args) {
13424
14241
  if (!cmd) return { blocked: false };
@@ -13631,7 +14448,7 @@ async function checkKillTarget(target) {
13631
14448
  reason: `Blocked: kill ${target.signal} '${target.name}' targets a WrongStack process name.`
13632
14449
  };
13633
14450
  }
13634
- const currentImage = path24.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
14451
+ const currentImage = path25.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
13635
14452
  const targetsNodeRuntime = nameLower === "node" || nameLower.startsWith("node");
13636
14453
  if (targetsNodeRuntime && currentImage === "node") {
13637
14454
  return {
@@ -14447,7 +15264,7 @@ var execTool = {
14447
15264
  command: cmd,
14448
15265
  args: input.args ?? [],
14449
15266
  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.`,
15267
+ 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
15268
  exitCode: 1,
14452
15269
  truncated: false,
14453
15270
  allowed: false,
@@ -14659,18 +15476,17 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
14659
15476
  }
14660
15477
 
14661
15478
  // src/fetch.ts
14662
- import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValidationError3 } from "@wrongstack/core";
15479
+ import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValidationError3 } from "@wrongstack/core/types";
14663
15480
  import TurndownService from "turndown";
14664
15481
 
14665
15482
  // src/_fetch-guard.ts
14666
15483
  import * as dns2 from "node:dns/promises";
14667
15484
  import * as net3 from "node:net";
14668
15485
  import {
14669
- FetchError,
14670
- ToolValidationError as ToolValidationError2,
14671
15486
  isPrivateIPv4 as isPrivateIPv42,
14672
15487
  isPrivateIPv6 as isPrivateIPv62
14673
- } from "@wrongstack/core";
15488
+ } from "@wrongstack/core/utils";
15489
+ import { FetchError, ToolValidationError as ToolValidationError2 } from "@wrongstack/core/types";
14674
15490
  import { Agent, fetch as undiciFetch } from "undici";
14675
15491
  var nativeGlobalFetch = globalThis.fetch;
14676
15492
  var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
@@ -15168,7 +15984,8 @@ init_util();
15168
15984
  import { spawn as spawn9 } from "node:child_process";
15169
15985
  import { statSync as statSync3 } from "node:fs";
15170
15986
  import { dirname as dirname12, resolve as resolve11, sep as sep5 } from "node:path";
15171
- import { assessCommitSafety, buildChildEnv as buildChildEnv4 } from "@wrongstack/core";
15987
+ import { assessCommitSafety } from "@wrongstack/core/coordination";
15988
+ import { buildChildEnv as buildChildEnv4 } from "@wrongstack/core/utils";
15172
15989
  var TIMEOUT_MS2 = 3e4;
15173
15990
  var MAX_OUTPUT3 = 1e5;
15174
15991
  var gitTool = {
@@ -15448,8 +16265,8 @@ function runGit2(args, cwd, signal) {
15448
16265
 
15449
16266
  // src/glob.ts
15450
16267
  import * as fs20 from "node:fs/promises";
15451
- import * as path25 from "node:path";
15452
- import { compileGlob as compileGlob3 } from "@wrongstack/core";
16268
+ import * as path26 from "node:path";
16269
+ import { compileGlob as compileGlob2 } from "@wrongstack/core/utils";
15453
16270
 
15454
16271
  // src/_concurrency.ts
15455
16272
  async function mapWithConcurrency(items, limit, fn) {
@@ -15511,7 +16328,7 @@ var globTool = {
15511
16328
  const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
15512
16329
  const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
15513
16330
  const ignored = await readGitignore(base);
15514
- const re = compileGlob3(input.pattern);
16331
+ const re = compileGlob2(input.pattern);
15515
16332
  const results = [];
15516
16333
  let truncated = false;
15517
16334
  const pushResult = async (full) => {
@@ -15552,7 +16369,7 @@ var globTool = {
15552
16369
  if (DEFAULT_IGNORE2.includes(name)) continue;
15553
16370
  if (ignored.includes(name)) continue;
15554
16371
  const rel = relPrefix ? `${relPrefix}/${name}` : name;
15555
- const full = path25.join(dir, name);
16372
+ const full = path26.join(dir, name);
15556
16373
  if (e.isDirectory()) {
15557
16374
  subdirs.push({ full, rel });
15558
16375
  } else if (e.isFile()) {
@@ -15596,7 +16413,7 @@ var globTool = {
15596
16413
  };
15597
16414
  async function readGitignore(dir) {
15598
16415
  try {
15599
- const raw = await fs20.readFile(path25.join(dir, ".gitignore"), "utf8");
16416
+ const raw = await fs20.readFile(path26.join(dir, ".gitignore"), "utf8");
15600
16417
  return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
15601
16418
  } catch {
15602
16419
  return [];
@@ -15604,11 +16421,12 @@ async function readGitignore(dir) {
15604
16421
  }
15605
16422
 
15606
16423
  // src/grep.ts
15607
- import { expectDefined as expectDefined7 } from "@wrongstack/core";
16424
+ import { expectDefined as expectDefined7 } from "@wrongstack/core/utils";
15608
16425
  import { spawn as spawn10 } from "node:child_process";
15609
16426
  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";
16427
+ import * as path27 from "node:path";
16428
+ import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
16429
+ import { buildChildEnv as buildChildEnv5, compileGlob as compileGlob3 } from "@wrongstack/core/utils";
15612
16430
 
15613
16431
  // src/_regex.ts
15614
16432
  var MAX_PATTERN_LEN = 256;
@@ -15898,7 +16716,7 @@ async function runNative(input, base, mode, limit, signal) {
15898
16716
  });
15899
16717
  }
15900
16718
  const re = compiled.regex;
15901
- const globRe = input.glob ? compileGlob4(input.glob) : null;
16719
+ const globRe = input.glob ? compileGlob3(input.glob) : null;
15902
16720
  const matches = [];
15903
16721
  const countOnlyFirstHit = mode === "count" && limit === 1;
15904
16722
  const maxBytes = mode === "content" ? NATIVE_MAX_FILE_BYTES : Math.min(NATIVE_MAX_FILE_BYTES, 256 * 1024);
@@ -16000,7 +16818,7 @@ async function runNative(input, base, mode, limit, signal) {
16000
16818
  if (stopped) return;
16001
16819
  if (DEFAULT_IGNORE3.includes(e.name)) continue;
16002
16820
  if (e.isSymbolicLink()) continue;
16003
- const full = path26.join(dir, e.name);
16821
+ const full = path27.join(dir, e.name);
16004
16822
  if (e.isDirectory()) {
16005
16823
  subdirs.push(full);
16006
16824
  } else if (e.isFile()) {
@@ -16024,7 +16842,10 @@ init_spawn_stream();
16024
16842
  init_util();
16025
16843
  init_legacy_bridge();
16026
16844
  import { join as join21 } from "node:path";
16027
- import { detectPackageEcosystem, recordPackageAction } from "@wrongstack/core";
16845
+ import {
16846
+ detectEcosystem as detectPackageEcosystem,
16847
+ recordPackageAction
16848
+ } from "@wrongstack/core/coordination";
16028
16849
  var installTool = {
16029
16850
  name: "install",
16030
16851
  category: "Package Management",
@@ -16210,7 +17031,7 @@ function resolveManifestPath(cwd, pkgManager) {
16210
17031
  // src/json.ts
16211
17032
  init_util();
16212
17033
  import * as fs22 from "node:fs/promises";
16213
- import { deepMerge as deepMergeCore } from "@wrongstack/core";
17034
+ import { deepMerge as deepMergeCore } from "@wrongstack/core/utils";
16214
17035
  var jsonTool = {
16215
17036
  name: "json",
16216
17037
  category: "Data",
@@ -16588,56 +17409,56 @@ function jmespathSearch(data, query) {
16588
17409
  }
16589
17410
  function validateJsonSchema(data, schema) {
16590
17411
  const errors = [];
16591
- function check(value, s, path34) {
17412
+ function check(value, s, path35) {
16592
17413
  if (s["type"]) {
16593
17414
  const expectedType = s["type"];
16594
17415
  const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
16595
17416
  if (expectedType === "integer") {
16596
- if (!Number.isInteger(value)) errors.push(`${path34}: expected integer, got ${actualType}`);
17417
+ if (!Number.isInteger(value)) errors.push(`${path35}: expected integer, got ${actualType}`);
16597
17418
  } else if (expectedType !== actualType) {
16598
- errors.push(`${path34}: expected ${expectedType}, got ${actualType}`);
17419
+ errors.push(`${path35}: expected ${expectedType}, got ${actualType}`);
16599
17420
  }
16600
17421
  }
16601
17422
  if (typeof value === "string" && s["format"] === "uri" && value) {
16602
17423
  try {
16603
17424
  new URL(value);
16604
17425
  } catch {
16605
- errors.push(`${path34}: not a valid URI`);
17426
+ errors.push(`${path35}: not a valid URI`);
16606
17427
  }
16607
17428
  }
16608
17429
  if (typeof value === "string" && s["pattern"]) {
16609
17430
  const re = new RegExp(s["pattern"]);
16610
- if (!re.test(value)) errors.push(`${path34}: does not match pattern ${s["pattern"]}`);
17431
+ if (!re.test(value)) errors.push(`${path35}: does not match pattern ${s["pattern"]}`);
16611
17432
  }
16612
17433
  if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
16613
- errors.push(`${path34}: string too short (min ${s["minLength"]})`);
17434
+ errors.push(`${path35}: string too short (min ${s["minLength"]})`);
16614
17435
  }
16615
17436
  if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
16616
- errors.push(`${path34}: string too long (max ${s["maxLength"]})`);
17437
+ errors.push(`${path35}: string too long (max ${s["maxLength"]})`);
16617
17438
  }
16618
17439
  if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
16619
- errors.push(`${path34}: below minimum ${s["minimum"]}`);
17440
+ errors.push(`${path35}: below minimum ${s["minimum"]}`);
16620
17441
  }
16621
17442
  if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
16622
- errors.push(`${path34}: above maximum ${s["maximum"]}`);
17443
+ errors.push(`${path35}: above maximum ${s["maximum"]}`);
16623
17444
  }
16624
17445
  if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
16625
17446
  for (let i = 0; i < value.length; i++) {
16626
- check(value[i], s["items"], `${path34}[${i}]`);
17447
+ check(value[i], s["items"], `${path35}[${i}]`);
16627
17448
  }
16628
17449
  }
16629
17450
  if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
16630
17451
  const props = s["properties"];
16631
17452
  for (const [k, propSchema] of Object.entries(props)) {
16632
- check(value[k], propSchema, `${path34}.${k}`);
17453
+ check(value[k], propSchema, `${path35}.${k}`);
16633
17454
  }
16634
17455
  }
16635
17456
  }
16636
17457
  check(data, schema, "$");
16637
17458
  return { valid: errors.length === 0, errors };
16638
17459
  }
16639
- function simpleQuery(data, path34) {
16640
- const parts = path34.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
17460
+ function simpleQuery(data, path35) {
17461
+ const parts = path35.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
16641
17462
  let current = data;
16642
17463
  for (const part of parts) {
16643
17464
  if (current === null || current === void 0) return void 0;
@@ -16688,7 +17509,8 @@ function toYaml(data, indent = 0) {
16688
17509
 
16689
17510
  // src/kanban.ts
16690
17511
  import { randomUUID } from "node:crypto";
16691
- import { deserializeTaskGraph as deserializeTaskGraph2, loadTasks as loadTasks2, serializeTaskGraph } from "@wrongstack/core";
17512
+ import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
17513
+ import { loadTasks as loadTasks2 } from "@wrongstack/core/storage";
16692
17514
  import {
16693
17515
  addCheckToTask,
16694
17516
  addColumn,
@@ -16735,20 +17557,21 @@ import {
16735
17557
  updateColumn,
16736
17558
  updateGoalMetricOnTask,
16737
17559
  updateTask,
16738
- updateTaskAssignment
17560
+ updateTaskAssignment,
17561
+ verifyTaskCompletion
16739
17562
  } from "@wrongstack/kanban";
16740
17563
 
16741
17564
  // src/session-kanban.ts
16742
17565
  import { watch } from "node:fs";
16743
- import { basename as basename10, dirname as dirname13 } from "node:path";
17566
+ import { basename as basename11, dirname as dirname13 } from "node:path";
17567
+ import { GlobalMailbox } from "@wrongstack/core/coordination";
16744
17568
  import {
16745
- deserializeTaskGraph,
16746
- GlobalMailbox,
16747
17569
  loadPlan,
16748
17570
  loadTasks,
16749
17571
  mutatePlan,
16750
17572
  mutateTasks
16751
- } from "@wrongstack/core";
17573
+ } from "@wrongstack/core/storage";
17574
+ import { deserializeTaskGraph } from "@wrongstack/core/tasking";
16752
17575
  import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
16753
17576
  import {
16754
17577
  createBoard,
@@ -17062,8 +17885,10 @@ function projectSessionPlanToKanban(projectRoot, items, sessionId) {
17062
17885
  "session-plan"
17063
17886
  );
17064
17887
  }
17065
- function fireAndForget(work) {
17066
- void work.catch(() => {
17888
+ function fireAndForget(context, work) {
17889
+ void work.catch((err) => {
17890
+ const message = err instanceof Error ? err.message : String(err);
17891
+ console.warn(JSON.stringify({ level: "warn", event: "session-kanban", context, message, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
17067
17892
  });
17068
17893
  }
17069
17894
  function broadcastTodoUpdate(context, todos) {
@@ -17137,7 +17962,10 @@ function attachSessionKanbanMirror(context) {
17137
17962
  if (currentSessionId === registeredSessionId) return;
17138
17963
  if (registeredSessionId) {
17139
17964
  releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
17140
- fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));
17965
+ fireAndForget(
17966
+ "cleanup-board",
17967
+ cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId)
17968
+ );
17141
17969
  }
17142
17970
  registeredSessionId = currentSessionId;
17143
17971
  if (registeredSessionId) {
@@ -17187,7 +18015,7 @@ function attachSessionKanbanMirror(context) {
17187
18015
  (_event, filename) => {
17188
18016
  if (filename?.toString() !== boardFileName) return;
17189
18017
  if (boardTimer) clearTimeout(boardTimer);
17190
- boardTimer = setTimeout(() => fireAndForget(refreshBoard()), 60);
18018
+ boardTimer = setTimeout(() => fireAndForget("refresh-board", refreshBoard()), 60);
17191
18019
  }
17192
18020
  );
17193
18021
  const touchPresence = () => touchKanbanPresence(context.projectRoot, board.id, {
@@ -17195,9 +18023,9 @@ function attachSessionKanbanMirror(context) {
17195
18023
  agentId: context.agentId,
17196
18024
  agentName: context.agentName
17197
18025
  });
17198
- fireAndForget(touchPresence());
18026
+ fireAndForget("touch-presence", touchPresence());
17199
18027
  if (presenceTimer) clearInterval(presenceTimer);
17200
- presenceTimer = setInterval(() => fireAndForget(touchPresence()), 6e4);
18028
+ presenceTimer = setInterval(() => fireAndForget("touch-presence", touchPresence()), 6e4);
17201
18029
  presenceTimer.unref?.();
17202
18030
  boardWatcher.on("error", () => {
17203
18031
  boardWatcher?.close();
@@ -17222,11 +18050,11 @@ function attachSessionKanbanMirror(context) {
17222
18050
  const name = filename?.toString();
17223
18051
  const currentPlanPath = context.meta["plan.path"];
17224
18052
  const currentTaskPath = context.meta["task.path"];
17225
- const planName = typeof currentPlanPath === "string" ? basename10(currentPlanPath) : "";
17226
- const taskName = typeof currentTaskPath === "string" ? basename10(currentTaskPath) : "";
18053
+ const planName = typeof currentPlanPath === "string" ? basename11(currentPlanPath) : "";
18054
+ const taskName = typeof currentTaskPath === "string" ? basename11(currentTaskPath) : "";
17227
18055
  if (name && name !== planName && name !== taskName) return;
17228
18056
  if (timer) clearTimeout(timer);
17229
- timer = setTimeout(() => fireAndForget(refreshFiles()), 60);
18057
+ timer = setTimeout(() => fireAndForget("refresh-files", refreshFiles()), 60);
17230
18058
  });
17231
18059
  watcher.on("error", () => watcher?.close());
17232
18060
  } catch {
@@ -17246,13 +18074,13 @@ function attachSessionKanbanMirror(context) {
17246
18074
  if (change.kind === "meta_set" && (change.key === "plan.path" || change.key === "task.path")) {
17247
18075
  syncActiveSessionRegistration();
17248
18076
  configureWatcher();
17249
- fireAndForget(ensureSessionKanbanBoard(context.projectRoot, sessionId()));
17250
- fireAndForget(configureBoardWatcher());
17251
- fireAndForget(refreshFiles());
18077
+ fireAndForget("ensure-board", ensureSessionKanbanBoard(context.projectRoot, sessionId()));
18078
+ fireAndForget("configure-watcher", configureBoardWatcher());
18079
+ fireAndForget("refresh-files", refreshFiles());
17252
18080
  }
17253
18081
  });
17254
18082
  configureWatcher();
17255
- fireAndForget(configureBoardWatcher());
18083
+ fireAndForget("configure-watcher", configureBoardWatcher());
17256
18084
  const detach = () => {
17257
18085
  unsubscribe();
17258
18086
  if (timer) clearTimeout(timer);
@@ -17263,7 +18091,10 @@ function attachSessionKanbanMirror(context) {
17263
18091
  bindings.delete(context);
17264
18092
  if (attachedProjectRoot && registeredSessionId) {
17265
18093
  releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
17266
- fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));
18094
+ fireAndForget(
18095
+ "cleanup-board",
18096
+ cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId)
18097
+ );
17267
18098
  registeredSessionId = "";
17268
18099
  }
17269
18100
  };
@@ -17406,8 +18237,8 @@ async function applySessionKanbanTaskToSource(context, task, options = {}) {
17406
18237
  var kanbanTool = {
17407
18238
  name: "kanban",
17408
18239
  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.",
18240
+ 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.",
18241
+ 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
18242
  permission: "confirm",
17412
18243
  mutating: true,
17413
18244
  capabilities: ["fs.write"],
@@ -17463,7 +18294,9 @@ var kanbanTool = {
17463
18294
  "add_check",
17464
18295
  "update_check",
17465
18296
  "add_note",
17466
- "add_link"
18297
+ "add_link",
18298
+ "verify_completion",
18299
+ "split_atomic"
17467
18300
  ]
17468
18301
  },
17469
18302
  boardId: { type: "string" },
@@ -17518,6 +18351,7 @@ var kanbanTool = {
17518
18351
  runTaskId: { type: "string" },
17519
18352
  lastResult: { type: "string" },
17520
18353
  error: { type: "string" },
18354
+ expectedLeaseId: { type: "string" },
17521
18355
  assignmentStatus: {
17522
18356
  type: "string",
17523
18357
  enum: ["assigned", "queued", "running", "completed", "failed", "cancelled"]
@@ -17552,6 +18386,7 @@ var kanbanTool = {
17552
18386
  lastFailureKind: { type: "string" },
17553
18387
  dependsOn: { type: "array", items: { type: "string" } },
17554
18388
  estimatedHours: { type: "number" },
18389
+ actualHours: { type: "number" },
17555
18390
  taskGraph: { type: "object" },
17556
18391
  graphId: { type: "string" },
17557
18392
  specId: { type: "string" },
@@ -17860,24 +18695,7 @@ var kanbanTool = {
17860
18695
  if (!input.boardId || !input.taskId || !input.childTitles?.length) {
17861
18696
  return fail("split_task requires boardId, taskId, and childTitles.");
17862
18697
  }
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.");
18698
+ return handleSplitTask(projectRoot, input, {});
17881
18699
  }
17882
18700
  case "merge_tasks": {
17883
18701
  if (!input.boardId || !input.taskIds?.length || !input.title) {
@@ -18242,6 +19060,39 @@ var kanbanTool = {
18242
19060
  });
18243
19061
  return board ? okBoard(board, "Link added.") : fail("Task not found.");
18244
19062
  }
19063
+ case "verify_completion": {
19064
+ if (!input.boardId || !input.taskId) {
19065
+ return fail("verify_completion requires boardId and taskId.");
19066
+ }
19067
+ const verResult = await verifyTaskCompletion(projectRoot, input.boardId, input.taskId);
19068
+ const persistedBoard = await updateTask(projectRoot, input.boardId, input.taskId, {
19069
+ verificationReport: verResult.report,
19070
+ successCriteria: verResult.task.successCriteria
19071
+ });
19072
+ if (!persistedBoard) {
19073
+ return {
19074
+ ok: false,
19075
+ verdict: verResult.report.verdict,
19076
+ message: `Verification succeeded but persist failed: ${verResult.report.markdownSummary}. Board may be stale \u2014 re-run verify_completion.`,
19077
+ board: verResult.board
19078
+ };
19079
+ }
19080
+ const freshTask = persistedBoard.tasks?.find((t) => t.id === input.taskId);
19081
+ const deterministicVerdicts = ["passed", "failed", "needs_human", "incomplete"];
19082
+ return {
19083
+ ok: deterministicVerdicts.includes(verResult.report.verdict),
19084
+ verdict: verResult.report.verdict,
19085
+ message: verResult.report.markdownSummary,
19086
+ board: persistedBoard,
19087
+ task: freshTask ?? verResult.task
19088
+ };
19089
+ }
19090
+ case "split_atomic": {
19091
+ if (!input.boardId || !input.taskId || !input.childTitles?.length) {
19092
+ return fail("split_atomic requires boardId, taskId, and childTitles (at least one).");
19093
+ }
19094
+ return handleSplitTask(projectRoot, input, { atomic: true });
19095
+ }
18245
19096
  default:
18246
19097
  return fail(`Unknown kanban action: ${input.action}`);
18247
19098
  }
@@ -18261,6 +19112,50 @@ function okBoard(board, message = "Board loaded.") {
18261
19112
  function okTask(board, task, message) {
18262
19113
  return { ok: true, message, board, task };
18263
19114
  }
19115
+ async function handleSplitTask(projectRoot, input, extraSplitOptions) {
19116
+ const boardId = input.boardId;
19117
+ const taskId = input.taskId;
19118
+ const childTitles = input.childTitles;
19119
+ if (!boardId || !taskId || !childTitles?.length) {
19120
+ return fail("split requires boardId, taskId, and at least one childTitles.");
19121
+ }
19122
+ const {
19123
+ targetColumnId,
19124
+ inheritAssignment,
19125
+ inheritLabels,
19126
+ inheritSuccessCriteria,
19127
+ inheritGoalMetrics,
19128
+ inheritDependencies,
19129
+ chainChildren,
19130
+ rewireDependents
19131
+ } = input;
19132
+ const result = await splitTask(projectRoot, boardId, taskId, {
19133
+ titles: childTitles,
19134
+ ...extraSplitOptions,
19135
+ ...targetColumnId !== void 0 ? { columnId: targetColumnId } : {},
19136
+ ...inheritAssignment !== void 0 ? { inheritAssignment } : {},
19137
+ ...inheritLabels !== void 0 ? { inheritLabels } : {},
19138
+ ...inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria } : {},
19139
+ ...inheritGoalMetrics !== void 0 ? { inheritGoalMetrics } : {},
19140
+ ...inheritDependencies !== void 0 ? { inheritDependencies } : {},
19141
+ ...chainChildren !== void 0 ? { chainChildren } : {},
19142
+ ...rewireDependents !== void 0 ? { rewireDependents } : {}
19143
+ });
19144
+ if (!result) return fail("Task not found.");
19145
+ const freshParent = result.board.tasks?.find((t) => t.id === taskId);
19146
+ if (!freshParent) {
19147
+ return fail(
19148
+ `Split succeeded but parent ${taskId} not found in returned board. Children: [${result.children.map((c) => c.id).join(", ")}].`
19149
+ );
19150
+ }
19151
+ return {
19152
+ ok: true,
19153
+ message: `${result.children.length} child task(s) created.`,
19154
+ board: result.board,
19155
+ task: freshParent,
19156
+ children: result.children
19157
+ };
19158
+ }
18264
19159
  async function requireBoard(projectRoot, boardId) {
18265
19160
  return boardId ? getBoard2(projectRoot, boardId) : null;
18266
19161
  }
@@ -18362,7 +19257,8 @@ function taskPatch(input) {
18362
19257
  labels: input.labels,
18363
19258
  assignedAgent: input.agentId,
18364
19259
  ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
18365
- ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {}
19260
+ ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
19261
+ ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
18366
19262
  };
18367
19263
  }
18368
19264
  function assignmentInput(input) {
@@ -18544,7 +19440,7 @@ async function detectLinter(cwd) {
18544
19440
 
18545
19441
  // src/logs.ts
18546
19442
  import { spawn as spawn11 } from "node:child_process";
18547
- import { buildChildEnv as buildChildEnv6 } from "@wrongstack/core";
19443
+ import { buildChildEnv as buildChildEnv6 } from "@wrongstack/core/utils";
18548
19444
  init_util();
18549
19445
  var logsTool = {
18550
19446
  name: "logs",
@@ -18683,7 +19579,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
18683
19579
  }
18684
19580
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
18685
19581
  var MAX_TAIL_LINES = 1e5;
18686
- async function fileLogs(path34, lines, filterRe, stream) {
19582
+ async function fileLogs(path35, lines, filterRe, stream) {
18687
19583
  const { createInterface } = await import("node:readline");
18688
19584
  const { createReadStream: createReadStream2 } = await import("node:fs");
18689
19585
  const entries = [];
@@ -18692,7 +19588,7 @@ async function fileLogs(path34, lines, filterRe, stream) {
18692
19588
  let writeIdx = 0;
18693
19589
  let totalLines = 0;
18694
19590
  const rl = createInterface({
18695
- input: createReadStream2(path34),
19591
+ input: createReadStream2(path35),
18696
19592
  crlfDelay: Number.POSITIVE_INFINITY
18697
19593
  });
18698
19594
  for await (const line of rl) {
@@ -18713,7 +19609,7 @@ async function fileLogs(path34, lines, filterRe, stream) {
18713
19609
  if (parsed) entries.push(parsed);
18714
19610
  }
18715
19611
  return {
18716
- source: path34,
19612
+ source: path35,
18717
19613
  entries,
18718
19614
  total: entries.length,
18719
19615
  truncated: totalLines > effLines,
@@ -18760,7 +19656,7 @@ function parseLine(line) {
18760
19656
  init_util();
18761
19657
  init_win32_resolve();
18762
19658
  import { spawn as spawn12 } from "node:child_process";
18763
- import { buildChildEnv as buildChildEnv7 } from "@wrongstack/core";
19659
+ import { buildChildEnv as buildChildEnv7 } from "@wrongstack/core/utils";
18764
19660
  var outdatedTool = {
18765
19661
  name: "outdated",
18766
19662
  category: "Package Management",
@@ -18945,8 +19841,8 @@ init_util();
18945
19841
  import { spawn as spawn13 } from "node:child_process";
18946
19842
  import * as fs23 from "node:fs/promises";
18947
19843
  import * as os8 from "node:os";
18948
- import * as path27 from "node:path";
18949
- import { buildChildEnv as buildChildEnv8 } from "@wrongstack/core";
19844
+ import * as path28 from "node:path";
19845
+ import { buildChildEnv as buildChildEnv8 } from "@wrongstack/core/utils";
18950
19846
  var patchTool = {
18951
19847
  name: "patch",
18952
19848
  category: "Filesystem",
@@ -18981,9 +19877,9 @@ var patchTool = {
18981
19877
  for (const t of targets) {
18982
19878
  const stripped = stripPathComponents(t, strip);
18983
19879
  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)) {
19880
+ const candidate = path28.resolve(dir, stripped);
19881
+ const rel = path28.relative(ctx.projectRoot, candidate);
19882
+ if (rel.startsWith("..") || path28.isAbsolute(rel)) {
18987
19883
  return {
18988
19884
  applied: 0,
18989
19885
  rejected: 1,
@@ -19000,11 +19896,11 @@ var patchTool = {
19000
19896
  beforeContents.set(target, await readTextForTracking(target));
19001
19897
  }
19002
19898
  }
19003
- const tmpDir = await fs23.mkdtemp(path27.join(os8.tmpdir(), ".wstack_patch_"));
19899
+ const tmpDir = await fs23.mkdtemp(path28.join(os8.tmpdir(), ".wstack_patch_"));
19004
19900
  try {
19005
19901
  await fs23.chmod(tmpDir, 448).catch(() => {
19006
19902
  });
19007
- const patchFile = path27.join(tmpDir, "in.diff");
19903
+ const patchFile = path28.join(tmpDir, "in.diff");
19008
19904
  await fs23.writeFile(patchFile, input.patch, { mode: 384 });
19009
19905
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
19010
19906
  const result = await runPatch(args, dir, opts.signal);
@@ -19110,11 +20006,11 @@ import {
19110
20006
  mutatePlan as mutatePlan2,
19111
20007
  removePlanItem,
19112
20008
  setPlanItemStatus
19113
- } from "@wrongstack/core";
20009
+ } from "@wrongstack/core/storage";
19114
20010
  import {
19115
- mutateTasks as mutateTasks2,
19116
- formatTaskList
19117
- } from "@wrongstack/core";
20011
+ mutateTasks as mutateTasks2
20012
+ } from "@wrongstack/core/storage";
20013
+ import { formatTaskList } from "@wrongstack/core/utils";
19118
20014
  import { randomUUID as randomUUID2 } from "node:crypto";
19119
20015
  var planTool = {
19120
20016
  name: "plan",
@@ -19381,7 +20277,8 @@ function mkResult(plan, ok, message, todos) {
19381
20277
  // src/read.ts
19382
20278
  init_util();
19383
20279
  import * as fs24 from "node:fs/promises";
19384
- import { FsError, toErrorMessage as toErrorMessage4, ToolValidationError as ToolValidationError5 } from "@wrongstack/core";
20280
+ import { FsError, ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
20281
+ import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
19385
20282
  var MAX_BYTES2 = 5 * 1024 * 1024;
19386
20283
  var readTool = {
19387
20284
  name: "read",
@@ -19587,20 +20484,20 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
19587
20484
  }
19588
20485
 
19589
20486
  // src/replace.ts
19590
- import { expectDefined as expectDefined8 } from "@wrongstack/core";
20487
+ import { expectDefined as expectDefined8 } from "@wrongstack/core/utils";
19591
20488
  import { spawn as spawn14 } from "node:child_process";
19592
20489
  import * as fs25 from "node:fs/promises";
19593
- import * as path28 from "node:path";
20490
+ import * as path29 from "node:path";
19594
20491
  import {
19595
20492
  atomicWrite as atomicWrite3,
19596
20493
  buildChildEnv as buildChildEnv9,
19597
- compileGlob as compileGlob5,
20494
+ compileGlob as compileGlob4,
19598
20495
  detectNewlineStyle as detectNewlineStyle2,
19599
20496
  normalizeToLf as normalizeToLf2,
19600
20497
  toStyle as toStyle2,
19601
- ToolValidationError as ToolValidationError6,
19602
20498
  unifiedDiff as unifiedDiff2
19603
- } from "@wrongstack/core";
20499
+ } from "@wrongstack/core/utils";
20500
+ import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
19604
20501
  init_util();
19605
20502
  var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
19606
20503
  var replaceTool = {
@@ -19659,7 +20556,7 @@ var replaceTool = {
19659
20556
  });
19660
20557
  }
19661
20558
  const re = compiled.regex;
19662
- const globRe = input.glob ? compileGlob5(input.glob) : null;
20559
+ const globRe = input.glob ? compileGlob4(input.glob) : null;
19663
20560
  const dryRun = input.dry_run ?? true;
19664
20561
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
19665
20562
  const fileList = await resolveFiles2(filesInput, ctx, globRe);
@@ -19679,8 +20576,8 @@ var replaceTool = {
19679
20576
  } catch {
19680
20577
  continue;
19681
20578
  }
19682
- const rel = path28.relative(realRoot, realPath);
19683
- if (rel.startsWith("..") || path28.isAbsolute(rel)) continue;
20579
+ const rel = path29.relative(realRoot, realPath);
20580
+ if (rel.startsWith("..") || path29.isAbsolute(rel)) continue;
19684
20581
  const stat18 = await fs25.stat(realPath).catch(() => null);
19685
20582
  if (!stat18?.isFile()) continue;
19686
20583
  let content;
@@ -19799,7 +20696,7 @@ function spawnRgFind(pattern, base) {
19799
20696
  }
19800
20697
  async function globNative(pattern, base, extraGlob) {
19801
20698
  const results = [];
19802
- const globRe = compileGlob5(pattern);
20699
+ const globRe = compileGlob4(pattern);
19803
20700
  const walk2 = async (dir) => {
19804
20701
  let entries;
19805
20702
  try {
@@ -19809,7 +20706,7 @@ async function globNative(pattern, base, extraGlob) {
19809
20706
  }
19810
20707
  for (const e of entries) {
19811
20708
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
19812
- const full = path28.join(dir, e.name);
20709
+ const full = path29.join(dir, e.name);
19813
20710
  try {
19814
20711
  const stat18 = await fs25.lstat(full);
19815
20712
  if (stat18.isSymbolicLink()) continue;
@@ -19836,8 +20733,8 @@ async function globNative(pattern, base, extraGlob) {
19836
20733
  // src/scaffold.ts
19837
20734
  init_util();
19838
20735
  import * as fs26 from "node:fs/promises";
19839
- import * as path29 from "node:path";
19840
- import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
20736
+ import * as path30 from "node:path";
20737
+ import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
19841
20738
  var BUILT_IN_TEMPLATES = {
19842
20739
  "npm-package": {
19843
20740
  description: "Basic npm package with ESM",
@@ -19986,16 +20883,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
19986
20883
  let filesCreated = 0;
19987
20884
  for (const [filePath, content] of Object.entries(templateFiles)) {
19988
20885
  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)) {
20886
+ const joinedPath = path30.join(cwd, resolvedPath);
20887
+ const root = path30.resolve(ctx.projectRoot);
20888
+ const target = path30.resolve(joinedPath);
20889
+ const rel = path30.relative(root, target);
20890
+ if (rel.startsWith("..") || path30.isAbsolute(rel)) {
19994
20891
  throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
19995
20892
  }
19996
20893
  const fullPath = target;
19997
20894
  if (!dryRun) {
19998
- await fs26.mkdir(path29.dirname(fullPath), { recursive: true });
20895
+ await fs26.mkdir(path30.dirname(fullPath), { recursive: true });
19999
20896
  await atomicWrite4(fullPath, substituteVars(content, name, vars));
20000
20897
  }
20001
20898
  files.push(resolvedPath);
@@ -20024,7 +20921,8 @@ function substituteVars(content, name, vars) {
20024
20921
  }
20025
20922
 
20026
20923
  // src/search.ts
20027
- import { expectDefined as expectDefined9, FetchError as FetchError3, ToolValidationError as ToolValidationError7 } from "@wrongstack/core";
20924
+ import { FetchError as FetchError3, ToolValidationError as ToolValidationError7 } from "@wrongstack/core/types";
20925
+ import { expectDefined as expectDefined9 } from "@wrongstack/core/utils";
20028
20926
  import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
20029
20927
  var DEFAULT_NUM = 10;
20030
20928
  var MAX_RESULTS = 50;
@@ -20474,15 +21372,13 @@ var setWorkingDirTool = {
20474
21372
  import {
20475
21373
  computeTaskItemProgress,
20476
21374
  formatTaskList as formatTaskList2
20477
- } from "@wrongstack/core";
20478
- import {
20479
- mutateTasks as mutateTasks3
20480
- } from "@wrongstack/core";
21375
+ } from "@wrongstack/core/utils";
21376
+ import { mutateTasks as mutateTasks3 } from "@wrongstack/core/storage";
20481
21377
  import {
20482
21378
  addPlanItem as addPlanItem2,
20483
21379
  mutatePlan as mutatePlan3,
20484
21380
  formatPlan as formatPlan2
20485
- } from "@wrongstack/core";
21381
+ } from "@wrongstack/core/storage";
20486
21382
  import { randomUUID as randomUUID3 } from "node:crypto";
20487
21383
  function findTaskIndex(tasks, query) {
20488
21384
  const asNum = Number.parseInt(query, 10);
@@ -20852,7 +21748,7 @@ ${formatTaskList2(file.tasks)}` : file.tasks.length > 0 ? formatTaskList2(file.t
20852
21748
  init_spawn_stream();
20853
21749
  init_util();
20854
21750
  init_legacy_bridge();
20855
- import * as path30 from "node:path";
21751
+ import * as path31 from "node:path";
20856
21752
  var testTool = {
20857
21753
  name: "test",
20858
21754
  category: "Code Quality",
@@ -20959,7 +21855,7 @@ async function detectRunner(cwd) {
20959
21855
  const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
20960
21856
  for (const f of candidates) {
20961
21857
  try {
20962
- await stat18(path30.join(cwd, f));
21858
+ await stat18(path31.join(cwd, f));
20963
21859
  if (f.includes("vitest")) return "vitest";
20964
21860
  if (f.includes("jest")) return "jest";
20965
21861
  if (f.includes("mocha")) return "mocha";
@@ -21036,7 +21932,7 @@ function parseResult(runner, result, duration) {
21036
21932
  }
21037
21933
 
21038
21934
  // src/todo.ts
21039
- import { loadPlan as loadPlan2, loadTasks as loadTasks3, savePlan, saveTasks, setPlanItemStatus as setPlanItemStatus2 } from "@wrongstack/core";
21935
+ import { loadPlan as loadPlan2, loadTasks as loadTasks3, savePlan, saveTasks, setPlanItemStatus as setPlanItemStatus2 } from "@wrongstack/core/storage";
21040
21936
  var todoTool = {
21041
21937
  name: "todo",
21042
21938
  category: "Session",
@@ -21428,9 +22324,9 @@ var toolUseTool = {
21428
22324
 
21429
22325
  // src/tree.ts
21430
22326
  init_util();
21431
- import { expectDefined as expectDefined10 } from "@wrongstack/core";
22327
+ import { expectDefined as expectDefined10 } from "@wrongstack/core/utils";
21432
22328
  import * as fs28 from "node:fs/promises";
21433
- import * as path31 from "node:path";
22329
+ import * as path32 from "node:path";
21434
22330
  var DEFAULT_IGNORE5 = [
21435
22331
  "node_modules",
21436
22332
  ".git",
@@ -21601,7 +22497,7 @@ async function walkDir(dir, depth, opts) {
21601
22497
  opts.lines.push(opts.prefix + branch + displayName);
21602
22498
  if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
21603
22499
  const childPrefix = opts.prefix + connector;
21604
- await walkDir(path31.join(dir, entry.name), depth + 1, {
22500
+ await walkDir(path32.join(dir, entry.name), depth + 1, {
21605
22501
  ...opts,
21606
22502
  prefix: childPrefix,
21607
22503
  isLast
@@ -21614,7 +22510,7 @@ async function walkDir(dir, depth, opts) {
21614
22510
  init_spawn_stream();
21615
22511
  init_util();
21616
22512
  init_legacy_bridge();
21617
- import * as path32 from "node:path";
22513
+ import * as path33 from "node:path";
21618
22514
  var typecheckTool = {
21619
22515
  name: "typecheck",
21620
22516
  category: "Code Quality",
@@ -21717,8 +22613,8 @@ async function findTsConfig(cwd) {
21717
22613
  const candidates = ["tsconfig.json", "tsconfig.base.json"];
21718
22614
  for (const f of candidates) {
21719
22615
  try {
21720
- const s = await stat18(path32.join(cwd, f));
21721
- if (s.isFile()) return path32.join(cwd, f);
22616
+ const s = await stat18(path33.join(cwd, f));
22617
+ if (s.isFile()) return path33.join(cwd, f);
21722
22618
  } catch {
21723
22619
  }
21724
22620
  }
@@ -21727,7 +22623,8 @@ async function findTsConfig(cwd) {
21727
22623
 
21728
22624
  // src/write.ts
21729
22625
  import * as fs29 from "node:fs/promises";
21730
- import { atomicWrite as atomicWrite5, ToolValidationError as ToolValidationError8, unifiedDiff as unifiedDiff3 } from "@wrongstack/core";
22626
+ import { ToolValidationError as ToolValidationError8 } from "@wrongstack/core/types";
22627
+ import { atomicWrite as atomicWrite5, unifiedDiff as unifiedDiff3 } from "@wrongstack/core/utils";
21731
22628
  init_util();
21732
22629
  var writeTool = {
21733
22630
  name: "write",
@@ -21951,7 +22848,7 @@ init_circuit_breaker();
21951
22848
  init_languages();
21952
22849
 
21953
22850
  // src/memory.ts
21954
- import { ToolValidationError as ToolValidationError9 } from "@wrongstack/core";
22851
+ import { ToolValidationError as ToolValidationError9 } from "@wrongstack/core/types";
21955
22852
  function rememberTool(memory) {
21956
22853
  return {
21957
22854
  name: "remember",
@@ -22260,6 +23157,88 @@ var ProcessGuardian = class {
22260
23157
  isRunning = false;
22261
23158
  instanceId;
22262
23159
  sigtermCount = 0;
23160
+ processHandlersInstalled = false;
23161
+ onProcessExit = (code) => {
23162
+ console.log(JSON.stringify({
23163
+ level: "info",
23164
+ event: "process_guardian.process_exiting",
23165
+ pid: process.pid,
23166
+ code,
23167
+ instanceId: this.instanceId
23168
+ }));
23169
+ this.stop();
23170
+ };
23171
+ onUncaughtException = (err) => {
23172
+ console.error(JSON.stringify({
23173
+ level: "error",
23174
+ event: "process_guardian.uncaught_exception",
23175
+ error: err.message,
23176
+ stack: err.stack,
23177
+ instanceId: this.instanceId,
23178
+ fatal: true
23179
+ }));
23180
+ this.stop();
23181
+ process.exit(1);
23182
+ };
23183
+ onUnhandledRejection = (reason) => {
23184
+ const err = reason instanceof Error ? { message: reason.message, stack: reason.stack } : { value: String(reason) };
23185
+ console.error(JSON.stringify({
23186
+ level: "error",
23187
+ event: "process_guardian.unhandled_rejection",
23188
+ ...err,
23189
+ instanceId: this.instanceId,
23190
+ fatal: true
23191
+ }));
23192
+ this.stop();
23193
+ process.exit(1);
23194
+ };
23195
+ onSigterm = () => {
23196
+ this.sigtermCount++;
23197
+ const threshold = this.config.sigtermThreshold;
23198
+ if (threshold === Infinity) {
23199
+ console.log(JSON.stringify({
23200
+ level: "warn",
23201
+ event: "process_guardian.sigterm_ignored",
23202
+ pid: process.pid,
23203
+ instanceId: this.instanceId,
23204
+ sigtermCount: this.sigtermCount,
23205
+ message: "SIGTERM received but sigtermThreshold is Infinity \u2014 ignoring (use graceful shutdown instead)"
23206
+ }));
23207
+ return;
23208
+ }
23209
+ if (this.sigtermCount < threshold) {
23210
+ console.log(JSON.stringify({
23211
+ level: "warn",
23212
+ event: "process_guardian.sigterm_deferred",
23213
+ pid: process.pid,
23214
+ instanceId: this.instanceId,
23215
+ sigtermCount: this.sigtermCount,
23216
+ threshold,
23217
+ message: `SIGTERM received (${this.sigtermCount}/${threshold}) \u2014 ignoring until threshold is reached`
23218
+ }));
23219
+ return;
23220
+ }
23221
+ console.log(JSON.stringify({
23222
+ level: "warn",
23223
+ event: "process_guardian.sigterm_exiting",
23224
+ pid: process.pid,
23225
+ instanceId: this.instanceId,
23226
+ sigtermCount: this.sigtermCount,
23227
+ threshold,
23228
+ message: `SIGTERM threshold (${threshold}) reached \u2014 shutting down gracefully`
23229
+ }));
23230
+ this.stop();
23231
+ process.exit(0);
23232
+ };
23233
+ onSighup = () => {
23234
+ console.log(JSON.stringify({
23235
+ level: "warn",
23236
+ event: "process_guardian.sighup_received",
23237
+ pid: process.pid,
23238
+ instanceId: this.instanceId,
23239
+ message: "SIGHUP received but ignored - WrongStack continues running"
23240
+ }));
23241
+ };
22263
23242
  constructor(config = {}) {
22264
23243
  this.registry = getPersistentProcessRegistry();
22265
23244
  this.config = {
@@ -22298,6 +23277,7 @@ var ProcessGuardian = class {
22298
23277
  * Stop the guardian gracefully.
22299
23278
  */
22300
23279
  stop() {
23280
+ this.removeProcessHandlers();
22301
23281
  if (!this.isRunning) return;
22302
23282
  this.isRunning = false;
22303
23283
  if (this.heartbeatTimer) {
@@ -22377,87 +23357,22 @@ var ProcessGuardian = class {
22377
23357
  * Set up process-level event handlers.
22378
23358
  */
22379
23359
  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
- });
23360
+ if (this.processHandlersInstalled) return;
23361
+ this.processHandlersInstalled = true;
23362
+ process.on("exit", this.onProcessExit);
23363
+ process.on("uncaughtException", this.onUncaughtException);
23364
+ process.on("unhandledRejection", this.onUnhandledRejection);
23365
+ process.on("SIGTERM", this.onSigterm);
23366
+ process.on("SIGHUP", this.onSighup);
23367
+ }
23368
+ removeProcessHandlers() {
23369
+ if (!this.processHandlersInstalled) return;
23370
+ this.processHandlersInstalled = false;
23371
+ process.off("exit", this.onProcessExit);
23372
+ process.off("uncaughtException", this.onUncaughtException);
23373
+ process.off("unhandledRejection", this.onUnhandledRejection);
23374
+ process.off("SIGTERM", this.onSigterm);
23375
+ process.off("SIGHUP", this.onSighup);
22461
23376
  }
22462
23377
  /**
22463
23378
  * Check if a PID is protected by this guardian.
@@ -22493,6 +23408,7 @@ function getProcessGuardian() {
22493
23408
  return _guardian;
22494
23409
  }
22495
23410
  function startProcessGuardian(config) {
23411
+ _guardian?.stop();
22496
23412
  const guardian = new ProcessGuardian(config);
22497
23413
  guardian.start();
22498
23414
  _guardian = guardian;
@@ -22823,12 +23739,9 @@ function createGlobalPsSlashCommand() {
22823
23739
 
22824
23740
  // src/skill.ts
22825
23741
  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";
23742
+ import * as path34 from "node:path";
23743
+ import { SKILL_LIMITS, stripFrontmatter } from "@wrongstack/core/skills";
23744
+ import { ToolValidationError as ToolValidationError10 } from "@wrongstack/core/types";
22832
23745
  var MAX_BODY_CHARS = SKILL_LIMITS.MAX_SKILL_BODY_CHARS;
22833
23746
  var MAX_RESOURCE_CHARS = SKILL_LIMITS.MAX_RESOURCE_CHARS;
22834
23747
  var MAX_LISTED_RESOURCES = SKILL_LIMITS.MAX_LISTED_RESOURCES;
@@ -22870,7 +23783,7 @@ function makeSkillTool(skillLoader) {
22870
23783
  field: "name"
22871
23784
  });
22872
23785
  }
22873
- const dir = path33.dirname(manifest.path);
23786
+ const dir = path34.dirname(manifest.path);
22874
23787
  let loadedResource;
22875
23788
  if (input.resource?.trim()) {
22876
23789
  loadedResource = await loadResource(dir, input.resource.trim());
@@ -22920,15 +23833,15 @@ ${listing}`;
22920
23833
  }
22921
23834
  async function loadResource(skillDir, rel) {
22922
23835
  const norm = rel.replace(/\\/g, "/");
22923
- if (path33.isAbsolute(rel) || norm.split("/").some((seg) => seg === "..")) {
23836
+ if (path34.isAbsolute(rel) || norm.split("/").some((seg) => seg === "..")) {
22924
23837
  throw new ToolValidationError10({
22925
23838
  message: `skill: invalid resource path "${rel}"`,
22926
23839
  field: "resource"
22927
23840
  });
22928
23841
  }
22929
- const absPath = path33.resolve(skillDir, rel);
22930
- const root = path33.resolve(skillDir);
22931
- if (absPath !== root && !absPath.startsWith(root + path33.sep)) {
23842
+ const absPath = path34.resolve(skillDir, rel);
23843
+ const root = path34.resolve(skillDir);
23844
+ if (absPath !== root && !absPath.startsWith(root + path34.sep)) {
22932
23845
  throw new ToolValidationError10({
22933
23846
  message: `skill: resource "${rel}" escapes the skill directory`,
22934
23847
  field: "resource"
@@ -22969,7 +23882,7 @@ async function walk(root, dir, out) {
22969
23882
  }
22970
23883
  for (const e of entries) {
22971
23884
  if (out.length >= MAX_LISTED_RESOURCES) return;
22972
- const fullPath = path33.join(dir, e.name);
23885
+ const fullPath = path34.join(dir, e.name);
22973
23886
  let isDir = e.isDirectory();
22974
23887
  if (e.isSymbolicLink()) {
22975
23888
  try {
@@ -22985,7 +23898,7 @@ async function walk(root, dir, out) {
22985
23898
  if (e.name === "SKILL.md" || e.name === "SKILL.save.md") continue;
22986
23899
  try {
22987
23900
  const stat18 = await fs30.stat(fullPath);
22988
- const rel = path33.relative(root, fullPath).split(path33.sep).join("/");
23901
+ const rel = path34.relative(root, fullPath).split(path34.sep).join("/");
22989
23902
  out.push({ path: rel, bytes: stat18.size });
22990
23903
  } catch {
22991
23904
  }
@@ -23160,6 +24073,43 @@ var TOOL_ICON_CONFIG = {
23160
24073
  // gray-light
23161
24074
  };
23162
24075
  var FALLBACK_ICON = "fallback";
24076
+
24077
+ // src/tool-tier.ts
24078
+ function toolNameSet(tools) {
24079
+ return new Set(tools.flatMap((tool) => tool ? [tool.name] : []));
24080
+ }
24081
+ function selectBuiltinToolsForTier(tier, allTools) {
24082
+ switch (tier) {
24083
+ case "off":
24084
+ return [...allTools];
24085
+ case "minimal":
24086
+ case "light": {
24087
+ const tier1Names = toolNameSet(TIER1_TOOLS);
24088
+ return allTools.filter((tool) => tier1Names.has(tool.name));
24089
+ }
24090
+ case "medium": {
24091
+ const tier1Names = toolNameSet(TIER1_TOOLS);
24092
+ const tier2Names = toolNameSet(TIER2_TOOLS);
24093
+ return allTools.filter((tool) => tier1Names.has(tool.name) || tier2Names.has(tool.name));
24094
+ }
24095
+ case "aggressive": {
24096
+ const tier1Names = toolNameSet(TIER1_TOOLS);
24097
+ const tier2Names = toolNameSet(TIER2_TOOLS);
24098
+ const tier3Names = toolNameSet(TIER3_TOOLS);
24099
+ return allTools.filter(
24100
+ (tool) => tier1Names.has(tool.name) || tier2Names.has(tool.name) && tool.name !== "task" || tier3Names.has(tool.name) && tool.name !== "setWorkingDir"
24101
+ );
24102
+ }
24103
+ }
24104
+ }
24105
+ function registerBuiltinToolTier(options) {
24106
+ const tools = selectBuiltinToolsForTier(
24107
+ options.tier,
24108
+ options.tools ?? builtinToolsPack.tools ?? []
24109
+ );
24110
+ options.registry.registerAllOrThrow(tools, options.owner ?? builtinToolsPack.name);
24111
+ return tools;
24112
+ }
23163
24113
  export {
23164
24114
  ADDITIONAL_LANGUAGE_PROFILES,
23165
24115
  BrowserArtifactStore,
@@ -23285,6 +24235,7 @@ export {
23285
24235
  projectSessionTodosToKanban,
23286
24236
  readTool,
23287
24237
  redactBrowserText,
24238
+ registerBuiltinToolTier,
23288
24239
  relatedMemoryTool,
23289
24240
  rememberTool,
23290
24241
  replaceTool,
@@ -23300,6 +24251,7 @@ export {
23300
24251
  searchCodebaseIndex,
23301
24252
  searchMemoryTool,
23302
24253
  searchTool,
24254
+ selectBuiltinToolsForTier,
23303
24255
  shutdownBrowserTools,
23304
24256
  shutdownCodebaseIndexHost,
23305
24257
  startProcessGuardian,