@wrongstack/tools 0.292.1 → 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/builtin.js CHANGED
@@ -17,7 +17,7 @@ var __export = (target, all) => {
17
17
  import { createWriteStream, mkdirSync } from "node:fs";
18
18
  import * as fsp from "node:fs/promises";
19
19
  import * as path from "node:path";
20
- import { wstackGlobalRoot } from "@wrongstack/core";
20
+ import { wstackGlobalRoot } from "@wrongstack/core/utils";
21
21
  function toolOutputDir() {
22
22
  return path.join(wstackGlobalRoot(), "tool-output");
23
23
  }
@@ -325,7 +325,7 @@ var init_circuit_breaker = __esm({
325
325
  });
326
326
 
327
327
  // src/_redact-command.ts
328
- import { expectDefined } from "@wrongstack/core";
328
+ import { expectDefined } from "@wrongstack/core/utils";
329
329
  function redactCommand(cmd) {
330
330
  let result = cmd;
331
331
  for (const pattern of SENSITIVE_FLAG_PATTERNS) {
@@ -439,7 +439,12 @@ var init_process_registry = __esm({
439
439
  this.breaker.setEnabled(false);
440
440
  }
441
441
  register(info) {
442
- this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
442
+ this.processes.set(info.pid, {
443
+ ...info,
444
+ killed: false,
445
+ protected: info.protected ?? false,
446
+ background: info.background ?? false
447
+ });
443
448
  }
444
449
  _isSafeSignalPid(pid) {
445
450
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
@@ -492,12 +497,21 @@ var init_process_registry = __esm({
492
497
  }
493
498
  return n;
494
499
  }
500
+ /** Count of active jobs explicitly launched in background mode. */
501
+ get activeBackgroundCount() {
502
+ let n = 0;
503
+ for (const p of this.processes.values()) {
504
+ if (p.background && !p.killed) n++;
505
+ }
506
+ return n;
507
+ }
495
508
  /**
496
509
  * Combined stats for observability — used by /ps and the TUI status bar.
497
510
  */
498
511
  stats() {
499
512
  return {
500
513
  activeCount: this.activeCount,
514
+ backgroundCount: this.activeBackgroundCount,
501
515
  totalCount: this.processes.size,
502
516
  breaker: this.breaker.snapshot()
503
517
  };
@@ -598,7 +612,7 @@ var init_process_registry = __esm({
598
612
  this.autoKillTimer = setTimeout(() => {
599
613
  this.autoKillTimer = null;
600
614
  this.autoKillArmedAt = null;
601
- this.killAll({ force: false });
615
+ this.killAll({ force: false, preserveBackground: true });
602
616
  this.breaker.forceReset();
603
617
  this._emitBreakerCountdown();
604
618
  }, this.autoKillResetMs);
@@ -638,6 +652,7 @@ var init_process_registry = __esm({
638
652
  if (!p) return false;
639
653
  if (p.killed) return true;
640
654
  if (p.protected) return false;
655
+ if (opts.preserveBackground && p.background) return false;
641
656
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
642
657
  const isWin5 = os.platform() === "win32";
643
658
  if (isWin5) {
@@ -807,11 +822,11 @@ var init_win32_resolve = __esm({
807
822
  // src/_spawn-stream.ts
808
823
  import { spawn as spawn2 } from "node:child_process";
809
824
  import {
810
- buildChildEnv,
811
825
  emitProcessCompleted,
812
826
  emitProcessOutput,
813
827
  emitProcessStarted
814
- } from "@wrongstack/core";
828
+ } from "@wrongstack/core/observability";
829
+ import { buildChildEnv } from "@wrongstack/core/utils";
815
830
  async function* spawnStream(opts) {
816
831
  const max = opts.maxBytes ?? 2e5;
817
832
  const flushAt = opts.flushBytes ?? 4 * 1024;
@@ -1023,7 +1038,7 @@ var init_spawn_stream = __esm({
1023
1038
  import { createHash } from "node:crypto";
1024
1039
  import * as fsp2 from "node:fs/promises";
1025
1040
  import * as path3 from "node:path";
1026
- import * as Core from "@wrongstack/core";
1041
+ import * as Core from "@wrongstack/core/utils";
1027
1042
  function sha256hex(content) {
1028
1043
  return createHash("sha256").update(content, "utf8").digest("hex");
1029
1044
  }
@@ -1067,16 +1082,16 @@ async function assertRealInsideRoot(absPath, ctx) {
1067
1082
  const realRoots = await Promise.all(
1068
1083
  allowedRoots(ctx).map((r) => fsp2.realpath(r).catch(() => path3.resolve(r)))
1069
1084
  );
1070
- let probe = absPath;
1085
+ let probe2 = absPath;
1071
1086
  for (; ; ) {
1072
1087
  let real;
1073
1088
  try {
1074
- real = await fsp2.realpath(probe);
1089
+ real = await fsp2.realpath(probe2);
1075
1090
  } catch (err) {
1076
1091
  if (err.code === "ENOENT") {
1077
- const parent = path3.dirname(probe);
1078
- if (parent === probe) return;
1079
- probe = parent;
1092
+ const parent = path3.dirname(probe2);
1093
+ if (parent === probe2) return;
1094
+ probe2 = parent;
1080
1095
  continue;
1081
1096
  }
1082
1097
  throw err;
@@ -1264,7 +1279,20 @@ function pythonProfile() {
1264
1279
  ]),
1265
1280
  ignoredDirectories: IGNORES,
1266
1281
  packageManagers: Object.freeze(["pip", "poetry", "pipenv", "uv"]),
1267
- executables: Object.freeze(["python", "python3", "ruff", "black", "pytest", "mypy", "pyright"]),
1282
+ executables: Object.freeze([
1283
+ "python",
1284
+ "python3",
1285
+ "pip",
1286
+ "pip3",
1287
+ "poetry",
1288
+ "pipenv",
1289
+ "uv",
1290
+ "ruff",
1291
+ "black",
1292
+ "pytest",
1293
+ "mypy",
1294
+ "pyright"
1295
+ ]),
1268
1296
  operations: Object.freeze({
1269
1297
  syntax: async (ctx) => {
1270
1298
  const py = ctx.options.target ? "python3" : "python3";
@@ -1324,6 +1352,14 @@ function pythonProfile() {
1324
1352
  network: true
1325
1353
  });
1326
1354
  },
1355
+ "package-remove": async (ctx) => {
1356
+ const names = packageNames(ctx);
1357
+ return names.length === 0 ? unavailable(ctx, "package-remove", "At least one package name is required.") : processPlan(ctx, "package-remove", "pip", ["uninstall", "--yes", ...names], {
1358
+ parser: "package-text",
1359
+ reason: "Remove validated Python packages.",
1360
+ mutating: true
1361
+ });
1362
+ },
1327
1363
  "package-audit": async (ctx) => processPlan(ctx, "package-audit", "pip", ["audit"], {
1328
1364
  parser: "pip-audit",
1329
1365
  reason: "Audit Python dependencies for vulnerabilities.",
@@ -1935,6 +1971,19 @@ function typescriptProfile() {
1935
1971
  network: true
1936
1972
  });
1937
1973
  },
1974
+ "package-remove": async (ctx) => {
1975
+ const names = packageNames(ctx);
1976
+ if (names.length === 0)
1977
+ return unavailable(ctx, "package-remove", "At least one package name is required.");
1978
+ const manager = ctx.workspace.packageManager ?? "npm";
1979
+ const args = manager === "npm" ? ["uninstall", "--ignore-scripts", ...names] : ["remove", "--ignore-scripts", ...names];
1980
+ return processPlan(ctx, "package-remove", manager, args, {
1981
+ parser: "package-text",
1982
+ reason: `Remove validated packages with ${manager} and lifecycle scripts disabled.`,
1983
+ mutating: true,
1984
+ network: true
1985
+ });
1986
+ },
1938
1987
  "package-audit": async (ctx) => {
1939
1988
  const manager = ctx.workspace.packageManager ?? "npm";
1940
1989
  return processPlan(ctx, "package-audit", manager, ["audit", "--json"], {
@@ -2387,13 +2436,28 @@ var init_primary = __esm({
2387
2436
  }),
2388
2437
  "package-add": async (ctx) => {
2389
2438
  const names = packageNames(ctx);
2390
- return names.length === 0 ? unavailable(ctx, "package-add", "At least one NuGet package is required.") : processPlan(ctx, "package-add", "dotnet", ["add", "package", ...names], {
2391
- parser: "dotnet-package",
2392
- reason: "Add validated NuGet packages.",
2393
- mutating: true,
2394
- network: true,
2395
- executesProjectCode: true
2396
- });
2439
+ const [spec] = names;
2440
+ const versionAt = spec?.lastIndexOf("@") ?? -1;
2441
+ const packageName = versionAt > 0 ? spec?.slice(0, versionAt) : spec;
2442
+ const packageVersion = versionAt > 0 ? spec?.slice(versionAt + 1) : void 0;
2443
+ 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(
2444
+ ctx,
2445
+ "package-add",
2446
+ "dotnet",
2447
+ [
2448
+ "add",
2449
+ "package",
2450
+ packageName ?? "",
2451
+ ...packageVersion ? ["--version", packageVersion] : []
2452
+ ],
2453
+ {
2454
+ parser: "dotnet-package",
2455
+ reason: "Add validated NuGet packages.",
2456
+ mutating: true,
2457
+ network: true,
2458
+ executesProjectCode: true
2459
+ }
2460
+ );
2397
2461
  },
2398
2462
  "package-remove": async (ctx) => {
2399
2463
  const names = packageNames(ctx);
@@ -2605,8 +2669,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, extraIgn
2605
2669
  collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2606
2670
  }
2607
2671
  }
2608
- function collectFileEvidence(directory, fullPath, basename10, profiles, state) {
2609
- const lower = basename10.toLowerCase();
2672
+ function collectFileEvidence(directory, fullPath, basename11, profiles, state) {
2673
+ const lower = basename11.toLowerCase();
2610
2674
  const extension = path4.extname(lower);
2611
2675
  for (const profile of profiles) {
2612
2676
  const detector = profile.detectors.find(
@@ -2617,7 +2681,7 @@ function collectFileEvidence(directory, fullPath, basename10, profiles, state) {
2617
2681
  candidate.evidence.push({
2618
2682
  kind: detector.kind,
2619
2683
  path: fullPath,
2620
- value: basename10,
2684
+ value: basename11,
2621
2685
  weight: detector.weight
2622
2686
  });
2623
2687
  if (detector.kind === "manifest" || detector.kind === "config") {
@@ -3483,7 +3547,7 @@ function validateOperationOptions(operation, packages) {
3483
3547
  if (value.startsWith(".") || value.startsWith("/") || /^[A-Za-z]:/.test(value) || value.includes("://")) {
3484
3548
  throw new Error(`Package paths and URLs are not supported: "${value}"`);
3485
3549
  }
3486
- if (!PACKAGE_NAME_RE.test(value) && !COMPOSER_PACKAGE_RE.test(value) && !GO_MODULE_RE.test(value)) {
3550
+ if (!PACKAGE_NAME_RE.test(value) && !COMPOSER_PACKAGE_RE.test(value) && !GO_MODULE_RE.test(value) && !PYTHON_PACKAGE_RE.test(value)) {
3487
3551
  throw new Error(`Invalid package identifier "${value}"`);
3488
3552
  }
3489
3553
  }
@@ -3505,7 +3569,7 @@ function isInside2(candidate, root) {
3505
3569
  const relative12 = path6.relative(path6.resolve(root), path6.resolve(candidate));
3506
3570
  return relative12 === "" || !relative12.startsWith("..") && !path6.isAbsolute(relative12);
3507
3571
  }
3508
- var MAX_ARGUMENTS, MAX_ARGUMENT_LENGTH, PACKAGE_NAME_RE, COMPOSER_PACKAGE_RE, GO_MODULE_RE;
3572
+ var MAX_ARGUMENTS, MAX_ARGUMENT_LENGTH, PACKAGE_NAME_RE, COMPOSER_PACKAGE_RE, GO_MODULE_RE, PYTHON_PACKAGE_RE;
3509
3573
  var init_plan = __esm({
3510
3574
  "src/languages/plan.ts"() {
3511
3575
  "use strict";
@@ -3516,6 +3580,7 @@ var init_plan = __esm({
3516
3580
  PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9*+._~^<>=|-]+)?$/i;
3517
3581
  COMPOSER_PACKAGE_RE = /^[a-z0-9_.-]+\/[a-z0-9_.-]+(?::[a-z0-9*+._~^<>=|-]+)?$/i;
3518
3582
  GO_MODULE_RE = /^(?:[a-z0-9.-]+\.)+[a-z0-9.-]+\/[a-z0-9._~+/@-]+$/i;
3583
+ PYTHON_PACKAGE_RE = /^[a-z0-9._-]+(?:\[[a-z0-9._,-]+\])?==[a-z0-9*+._!~-]+$/i;
3519
3584
  }
3520
3585
  });
3521
3586
 
@@ -3934,15 +3999,15 @@ async function changedPaths(before, after, beforeSizes, afterSizes) {
3934
3999
  const beforeSet = new Set(before);
3935
4000
  const afterSet = new Set(after);
3936
4001
  const changed = /* @__PURE__ */ new Set();
3937
- for (const path33 of after) {
3938
- if (!beforeSet.has(path33)) changed.add(path33);
4002
+ for (const path34 of after) {
4003
+ if (!beforeSet.has(path34)) changed.add(path34);
3939
4004
  }
3940
- for (const path33 of before) {
3941
- if (!afterSet.has(path33)) changed.add(path33);
4005
+ for (const path34 of before) {
4006
+ if (!afterSet.has(path34)) changed.add(path34);
3942
4007
  }
3943
4008
  if (beforeSizes && afterSizes) {
3944
- for (const path33 of after) {
3945
- if (beforeSizes.get(path33) !== afterSizes.get(path33)) changed.add(path33);
4009
+ for (const path34 of after) {
4010
+ if (beforeSizes.get(path34) !== afterSizes.get(path34)) changed.add(path34);
3946
4011
  }
3947
4012
  }
3948
4013
  return [...changed].sort();
@@ -4417,7 +4482,6 @@ var init_package_tool = __esm({
4417
4482
  const operationOptions2 = {
4418
4483
  ...input.names ? { packages: input.names } : {},
4419
4484
  ...input.scope ? { packageScope: input.scope } : {},
4420
- ...input.dryRun !== void 0 ? { allowScripts: input.dryRun } : { allowScripts: false },
4421
4485
  ...input.allowScripts !== void 0 ? { allowScripts: input.allowScripts } : {}
4422
4486
  };
4423
4487
  const planResult = await planLanguageOperation({
@@ -4446,6 +4510,24 @@ var init_package_tool = __esm({
4446
4510
  };
4447
4511
  return;
4448
4512
  }
4513
+ if (input.dryRun) {
4514
+ yield {
4515
+ type: "final",
4516
+ output: {
4517
+ status: "passed",
4518
+ language: planResult.workspace.language,
4519
+ workspace: planResult.workspace.root,
4520
+ operation: planResult.plan.operation,
4521
+ mutations: [],
4522
+ vulnerabilities: [],
4523
+ outdated: [],
4524
+ manifestsChanged: [],
4525
+ lockfilesChanged: [],
4526
+ output: `Dry run: ${planResult.plan.command ?? "internal"} ${planResult.plan.args.join(" ")}`.trim()
4527
+ }
4528
+ };
4529
+ return;
4530
+ }
4449
4531
  const runner = executePackagePlan({
4450
4532
  projectRoot: ctx.projectRoot,
4451
4533
  workspace: planResult.workspace,
@@ -4948,10 +5030,10 @@ import {
4948
5030
  emitProcessCompleted as emitProcessCompleted2,
4949
5031
  emitProcessOutput as emitProcessOutput2,
4950
5032
  emitProcessStarted as emitProcessStarted2
4951
- } from "@wrongstack/core";
5033
+ } from "@wrongstack/core/observability";
4952
5034
 
4953
5035
  // src/_env.ts
4954
- import { buildChildEnv as buildChildEnv2 } from "@wrongstack/core";
5036
+ import { buildChildEnv as buildChildEnv2 } from "@wrongstack/core/utils";
4955
5037
 
4956
5038
  // src/bash.ts
4957
5039
  init_output_spool();
@@ -4965,8 +5047,9 @@ import * as os3 from "node:os";
4965
5047
  init_process_registry();
4966
5048
  import * as fs6 from "node:fs/promises";
4967
5049
  import * as os2 from "node:os";
5050
+ import { wstackGlobalRoot as wstackGlobalRoot3 } from "@wrongstack/core/utils";
4968
5051
  import * as path9 from "node:path";
4969
- var REGISTRY_FILE = ".wrongstack/process-registry.json";
5052
+ var REGISTRY_FILE = "process-registry.json";
4970
5053
  function toErrorMessage(err) {
4971
5054
  return err instanceof Error ? err.message : String(err);
4972
5055
  }
@@ -4984,7 +5067,7 @@ function emitStructuredLog(level, event, message, error) {
4984
5067
  }
4985
5068
  var HEARTBEAT_INTERVAL_MS = 5e3;
4986
5069
  var STALE_THRESHOLD_MS = 3e4;
4987
- var LOCKFILE = ".wrongstack/.process-registry.lock";
5070
+ var LOCKFILE = ".process-registry.lock";
4988
5071
  function generateInstanceId() {
4989
5072
  const hostname2 = os2.hostname();
4990
5073
  const pid = process.pid;
@@ -5073,12 +5156,18 @@ var PersistentProcessRegistry = class {
5073
5156
  lockPath;
5074
5157
  baseRegistry;
5075
5158
  heartbeatInterval = null;
5159
+ cleanupInterval = null;
5160
+ heartbeatRunning = false;
5161
+ cleanupRunning = false;
5076
5162
  isShuttingDown = false;
5163
+ onProcessExit = () => {
5164
+ void this.syncToPersistent();
5165
+ };
5077
5166
  constructor(baseRegistry) {
5078
5167
  this.instanceId = generateInstanceId();
5079
- const homeDir = os2.homedir();
5080
- this.registryPath = path9.join(homeDir, REGISTRY_FILE);
5081
- this.lockPath = path9.join(homeDir, LOCKFILE);
5168
+ const globalRoot = wstackGlobalRoot3();
5169
+ this.registryPath = path9.join(globalRoot, REGISTRY_FILE);
5170
+ this.lockPath = path9.join(globalRoot, LOCKFILE);
5082
5171
  this.baseRegistry = baseRegistry ?? getProcessRegistry();
5083
5172
  this.ensureDirectory().catch((err) => {
5084
5173
  emitStructuredLog("warn", "process_registry.dir_create_failed", "PersistentProcessRegistry: failed to create .wrongstack directory", err);
@@ -5097,16 +5186,18 @@ var PersistentProcessRegistry = class {
5097
5186
  */
5098
5187
  start() {
5099
5188
  if (this.heartbeatInterval) return;
5100
- this.syncToPersistent();
5189
+ this.isShuttingDown = false;
5190
+ this.heartbeat();
5101
5191
  this.heartbeatInterval = setInterval(() => {
5102
5192
  this.heartbeat();
5103
5193
  }, HEARTBEAT_INTERVAL_MS);
5104
5194
  this.heartbeatInterval.unref?.();
5105
- setInterval(() => {
5106
- this.cleanupStaleEntries();
5107
- }, STALE_THRESHOLD_MS).unref?.();
5195
+ this.cleanupInterval = setInterval(() => {
5196
+ this.cleanup();
5197
+ }, STALE_THRESHOLD_MS);
5198
+ this.cleanupInterval.unref?.();
5108
5199
  this.registerMainProcess();
5109
- process.on("exit", () => this.syncToPersistent());
5200
+ process.on("exit", this.onProcessExit);
5110
5201
  }
5111
5202
  /**
5112
5203
  * Stop the heartbeat and clean up.
@@ -5117,7 +5208,12 @@ var PersistentProcessRegistry = class {
5117
5208
  clearInterval(this.heartbeatInterval);
5118
5209
  this.heartbeatInterval = null;
5119
5210
  }
5120
- this.syncToPersistent();
5211
+ if (this.cleanupInterval) {
5212
+ clearInterval(this.cleanupInterval);
5213
+ this.cleanupInterval = null;
5214
+ }
5215
+ process.off("exit", this.onProcessExit);
5216
+ void this.syncToPersistent();
5121
5217
  }
5122
5218
  /**
5123
5219
  * Register the main WrongStack process as protected.
@@ -5201,8 +5297,18 @@ var PersistentProcessRegistry = class {
5201
5297
  * Send heartbeat to mark all this instance's processes as alive.
5202
5298
  */
5203
5299
  heartbeat() {
5204
- if (this.isShuttingDown) return;
5205
- this.syncToPersistent();
5300
+ if (this.isShuttingDown || this.heartbeatRunning) return;
5301
+ this.heartbeatRunning = true;
5302
+ void this.syncToPersistent().finally(() => {
5303
+ this.heartbeatRunning = false;
5304
+ });
5305
+ }
5306
+ cleanup() {
5307
+ if (this.isShuttingDown || this.cleanupRunning) return;
5308
+ this.cleanupRunning = true;
5309
+ void this.cleanupStaleEntries().finally(() => {
5310
+ this.cleanupRunning = false;
5311
+ });
5206
5312
  }
5207
5313
  /**
5208
5314
  * Sync this instance's processes to the persistent registry.
@@ -5918,14 +6024,12 @@ var bashTool = {
5918
6024
  const detached = !isWin5;
5919
6025
  const startedAt = Date.now();
5920
6026
  if (input.background) {
5921
- let buf2 = "";
5922
- let truncated = false;
5923
6027
  const child2 = spawn3(shell, args, {
5924
6028
  cwd: ctx.projectRoot,
5925
6029
  env,
5926
6030
  // PowerShell takes the script on stdin (no argv quoting); cmd.exe
5927
6031
  // and POSIX shells ignore stdin when given the command inline.
5928
- stdio: [plan.useStdin ? "pipe" : "ignore", "pipe", "pipe"],
6032
+ stdio: [plan.useStdin ? "pipe" : "ignore", "ignore", "ignore"],
5929
6033
  // win32: CreateProcess IGNORES CREATE_NO_WINDOW (windowsHide) when
5930
6034
  // DETACHED_PROCESS (detached: true) is set, so the console-less
5931
6035
  // cmd.exe's grandchildren (node, dev servers) each allocate a fresh
@@ -5944,8 +6048,8 @@ var bashTool = {
5944
6048
  }
5945
6049
  }
5946
6050
  const pid2 = child2.pid;
5947
- let stdoutBytes2 = 0;
5948
- let stderrBytes2 = 0;
6051
+ const stdoutBytes2 = 0;
6052
+ const stderrBytes2 = 0;
5949
6053
  let telemetryCompleted2 = false;
5950
6054
  emitProcessStarted2({
5951
6055
  ...pid2 !== void 0 ? { pid: pid2 } : {},
@@ -5978,41 +6082,17 @@ var bashTool = {
5978
6082
  startedAt: Date.now(),
5979
6083
  sessionId: ctx.session?.id,
5980
6084
  child: child2,
5981
- processGroupLeader: detached && child2.pid === pid2
6085
+ processGroupLeader: detached && child2.pid === pid2,
6086
+ background: true
5982
6087
  });
5983
6088
  child2.on("close", () => registry.unregister(pid2));
5984
6089
  }
5985
- const onBgData = (chunk, stream) => {
5986
- if (stream === "stdout") stdoutBytes2 += chunk.byteLength;
5987
- else stderrBytes2 += chunk.byteLength;
5988
- emitProcessOutput2({ pid: pid2, stream, chunk });
5989
- if (truncated) return;
5990
- const remain = MAX_OUTPUT - buf2.length;
5991
- if (remain > 0) {
5992
- buf2 += chunk.toString().slice(0, remain);
5993
- }
5994
- if (buf2.length >= MAX_OUTPUT) {
5995
- truncated = true;
5996
- child2.stdout?.off("data", onBgStdout);
5997
- child2.stderr?.off("data", onBgStderr);
5998
- }
5999
- };
6000
- const onBgStdout = (chunk) => onBgData(chunk, "stdout");
6001
- const onBgStderr = (chunk) => onBgData(chunk, "stderr");
6002
- child2.stdout?.on("data", onBgStdout);
6003
- child2.stderr?.on("data", onBgStderr);
6004
- const cleanupBackground = () => {
6005
- child2.stdout?.off("data", onBgStdout);
6006
- child2.stderr?.off("data", onBgStderr);
6007
- };
6008
6090
  child2.on("error", () => {
6009
- cleanupBackground();
6010
6091
  if (typeof pid2 === "number") registry.unregister(pid2);
6011
6092
  registry.afterCall(Date.now() - startedAt, true, bypassBreaker);
6012
6093
  completeBackground(1);
6013
6094
  });
6014
6095
  child2.on("close", (code, signal) => {
6015
- cleanupBackground();
6016
6096
  registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
6017
6097
  completeBackground(code ?? (signal ? 1 : 0), signal ?? void 0);
6018
6098
  });
@@ -6020,7 +6100,7 @@ var bashTool = {
6020
6100
  yield {
6021
6101
  type: "final",
6022
6102
  output: {
6023
- output: normalizeCommandOutput(buf2),
6103
+ output: "",
6024
6104
  exit_code: null,
6025
6105
  timed_out: false,
6026
6106
  pid: pid2
@@ -6291,6 +6371,7 @@ ${hint}` : ""),
6291
6371
  for (const entry of registry.bySession(sessionId)) {
6292
6372
  if (entry.name !== "bash") continue;
6293
6373
  if (entry.child.exitCode !== null) continue;
6374
+ if (entry.background) continue;
6294
6375
  if (entry.protected) continue;
6295
6376
  registry.kill(entry.pid, { force: true });
6296
6377
  }
@@ -6300,7 +6381,7 @@ ${hint}` : ""),
6300
6381
  // src/batch-tool-use.ts
6301
6382
  import {
6302
6383
  GOVERNED_TOOL_EXECUTOR_META_KEY
6303
- } from "@wrongstack/core";
6384
+ } from "@wrongstack/core/types";
6304
6385
  var batchToolUseTool = {
6305
6386
  name: "batch_tool_use",
6306
6387
  category: "Meta",
@@ -6423,7 +6504,7 @@ async function executeSingle(call, ctx, governedExecute) {
6423
6504
 
6424
6505
  // src/browser/tools.ts
6425
6506
  import * as path12 from "node:path";
6426
- import { ToolCapabilities } from "@wrongstack/core";
6507
+ import { ToolCapabilities } from "@wrongstack/core/security";
6427
6508
  import { resolveWstackPaths } from "@wrongstack/core/utils";
6428
6509
 
6429
6510
  // src/browser/manager.ts
@@ -7085,6 +7166,10 @@ var BrowserSessionManager = class {
7085
7166
  const ids = [...this.sessions.values()].filter((session) => session.ownerId === ownerId).map((session) => session.id);
7086
7167
  for (const id of ids) await this.close(id, ownerId).catch(() => void 0);
7087
7168
  }
7169
+ /** True when the manager owns no live browser contexts. */
7170
+ isIdle() {
7171
+ return this.sessions.size === 0;
7172
+ }
7088
7173
  async dispose() {
7089
7174
  for (const session of [...this.sessions.values()]) {
7090
7175
  await session.context.close().catch(() => void 0);
@@ -7266,12 +7351,17 @@ function managerFor(ctx) {
7266
7351
  if (cleanupSignalByContext.get(ctx) !== runSignal) {
7267
7352
  cleanupSignalByContext.set(ctx, runSignal);
7268
7353
  const managerForRun = manager;
7354
+ const rootForRun = root;
7269
7355
  const ownerForRun = owner(ctx);
7270
7356
  ctx.registerAbortHook?.(async () => {
7271
7357
  if (cleanupSignalByContext.get(ctx) === runSignal) {
7272
7358
  cleanupSignalByContext.delete(ctx);
7273
7359
  }
7274
7360
  await managerForRun.closeOwner(ownerForRun);
7361
+ if (managerForRun.isIdle() && managers.get(rootForRun) === managerForRun) {
7362
+ managers.delete(rootForRun);
7363
+ await managerForRun.dispose();
7364
+ }
7275
7365
  });
7276
7366
  }
7277
7367
  return manager;
@@ -7654,7 +7744,7 @@ for (const tool of browserTools) tool.icon = "web";
7654
7744
  for (const tool of browserTools) tool.timeoutMs ??= 6e4;
7655
7745
 
7656
7746
  // src/codebase-index/writer.ts
7657
- import { expectDefined as expectDefined2 } from "@wrongstack/core";
7747
+ import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
7658
7748
 
7659
7749
  // src/codebase-index/circuit-breaker.ts
7660
7750
  var CircuitOpenError = class extends Error {
@@ -7741,7 +7831,7 @@ import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
7741
7831
  import { createRequire } from "node:module";
7742
7832
  import * as fs9 from "node:fs";
7743
7833
  import * as path13 from "node:path";
7744
- import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core";
7834
+ import { resolveWstackPaths as resolveWstackPaths2, sqliteCachePragmas } from "@wrongstack/core/utils";
7745
7835
 
7746
7836
  // src/codebase-index/schema.ts
7747
7837
  var SCHEMA_VERSION = 3;
@@ -7797,23 +7887,13 @@ function buildBm25Index(docs) {
7797
7887
  const tokens = tokenise(d.text);
7798
7888
  return { id: d.id, tokens, raw: d.text, len: tokens.length };
7799
7889
  });
7800
- const df = {};
7801
- for (const doc of documents) {
7802
- const seen = /* @__PURE__ */ new Set();
7803
- for (const t of doc.tokens) {
7804
- if (!seen.has(t)) {
7805
- df[t] = (df[t] ?? 0) + 1;
7806
- seen.add(t);
7807
- }
7808
- }
7809
- }
7810
7890
  const N = documents.length;
7811
7891
  const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
7812
7892
  const avgLen = N === 0 ? 0 : totalLen / N;
7813
- return new Bm25Index(documents, df, N, avgLen);
7893
+ return new Bm25Index(documents, N, avgLen);
7814
7894
  }
7815
7895
  var Bm25Index = class {
7816
- constructor(documents, _df, N, avgLen) {
7896
+ constructor(documents, N, avgLen) {
7817
7897
  this.documents = documents;
7818
7898
  this.N = N;
7819
7899
  this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
@@ -7821,9 +7901,19 @@ var Bm25Index = class {
7821
7901
  documents;
7822
7902
  N;
7823
7903
  safeAvgLen;
7904
+ /** Lazily-built id→doc index so getDoc is O(1) instead of an O(D) linear find. */
7905
+ _byId;
7824
7906
  score(query, filter) {
7825
7907
  const qTokens = tokenise(query);
7826
7908
  if (qTokens.length === 0) return [];
7909
+ const dfByTerm = /* @__PURE__ */ new Map();
7910
+ for (const qTerm of qTokens) {
7911
+ let dfVal = 0;
7912
+ for (const candidate of this.documents) {
7913
+ if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
7914
+ }
7915
+ dfByTerm.set(qTerm, dfVal);
7916
+ }
7827
7917
  const results = [];
7828
7918
  for (const doc of this.documents) {
7829
7919
  if (filter && !filter(doc.id)) continue;
@@ -7834,10 +7924,7 @@ var Bm25Index = class {
7834
7924
  if (t.startsWith(qTerm)) tf++;
7835
7925
  }
7836
7926
  if (tf === 0) continue;
7837
- let dfVal = 0;
7838
- for (const candidate of this.documents) {
7839
- if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
7840
- }
7927
+ const dfVal = dfByTerm.get(qTerm) ?? 0;
7841
7928
  if (dfVal === 0) continue;
7842
7929
  const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
7843
7930
  const lenRatio = B * (doc.len / this.safeAvgLen);
@@ -7849,7 +7936,10 @@ var Bm25Index = class {
7849
7936
  return results;
7850
7937
  }
7851
7938
  getDoc(id) {
7852
- return this.documents.find((d) => d.id === id);
7939
+ if (!this._byId) {
7940
+ this._byId = new Map(this.documents.map((d) => [d.id, d]));
7941
+ }
7942
+ return this._byId.get(id);
7853
7943
  }
7854
7944
  extractSnippet(docId, queryTokens, radius = 40) {
7855
7945
  const doc = this.getDoc(docId);
@@ -8011,6 +8101,25 @@ var IndexStore = class _IndexStore {
8011
8101
  * and reuse it. Cleared in {@link close} when the connection is torn down.
8012
8102
  */
8013
8103
  stmtCache = /* @__PURE__ */ new Map();
8104
+ /**
8105
+ * Cached full-corpus BM25 index for the FTS5-unavailable fallback path.
8106
+ * Built lazily on the first `searchRankedFallback` call and invalidated
8107
+ * (via `bm25Dirty`) whenever the `symbols` table is mutated. Computing
8108
+ * IDF over the full corpus is also more correct than the old per-query
8109
+ * candidate-subset IDF.
8110
+ *
8111
+ * Cache-lifecycle invariants (single source of truth lives at the
8112
+ * `invalidateBm25()` helper — see its docblock for the "every mutation
8113
+ * MUST call this" contract):
8114
+ * - declaration: this field + `bm25Dirty` (here)
8115
+ * - invalidation: `invalidateBm25()` flips the flag and nulls the cache
8116
+ * - build: `getOrBuildBm25()` rebuilds against current `symbols` rows
8117
+ * - teardown: `close()` resets the flag and nulls the cache
8118
+ */
8119
+ bm25Cache = null;
8120
+ // Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
8121
+ // an empty or pre-existing corpus makes a stale IDF table meaningless.
8122
+ bm25Dirty = true;
8014
8123
  /** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
8015
8124
  stmt(sql) {
8016
8125
  let s = this.stmtCache.get(sql);
@@ -8058,7 +8167,15 @@ var IndexStore = class _IndexStore {
8058
8167
  this.db = new Database(path13.join(this.indexDir, DB_FILE));
8059
8168
  try {
8060
8169
  this.db.exec("PRAGMA journal_mode = WAL");
8061
- this.db.exec("PRAGMA busy_timeout = 5000");
8170
+ this.db.exec("PRAGMA synchronous = NORMAL");
8171
+ this.db.exec("PRAGMA busy_timeout = 15000");
8172
+ this.db.exec("PRAGMA temp_store = MEMORY");
8173
+ const cache2 = sqliteCachePragmas();
8174
+ this.db.exec(`PRAGMA cache_size = -${cache2.cacheSizeKiB}`);
8175
+ this.db.exec(`PRAGMA mmap_size = ${cache2.mmapBytes}`);
8176
+ this.db.exec("PRAGMA foreign_keys = ON");
8177
+ this.db.exec("PRAGMA journal_size_limit = 67108864");
8178
+ this.db.exec("PRAGMA wal_autocheckpoint = 1000");
8062
8179
  } catch {
8063
8180
  }
8064
8181
  this.initSchema();
@@ -8070,7 +8187,7 @@ var IndexStore = class _IndexStore {
8070
8187
  value TEXT NOT NULL
8071
8188
  );
8072
8189
  `);
8073
- const storedRows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all("version");
8190
+ const storedRows = this.stmt("SELECT value FROM metadata WHERE key = ?").all("version");
8074
8191
  const storedVersion = storedRows.length ? Number(storedRows[0]?.value) : null;
8075
8192
  if (storedVersion !== null && storedVersion !== SCHEMA_VERSION) {
8076
8193
  this.db.exec(`
@@ -8079,9 +8196,9 @@ var IndexStore = class _IndexStore {
8079
8196
  DROP TABLE IF EXISTS refs;
8080
8197
  `);
8081
8198
  this.db.exec("DROP TABLE IF EXISTS symbols_fts");
8082
- this.db.prepare("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
8199
+ this.stmt("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
8083
8200
  } else if (storedVersion === null) {
8084
- this.db.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
8201
+ this.stmt("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
8085
8202
  }
8086
8203
  this.db.exec(`
8087
8204
  CREATE TABLE IF NOT EXISTS files (
@@ -8112,6 +8229,7 @@ var IndexStore = class _IndexStore {
8112
8229
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_file ON symbols(file)");
8113
8230
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_lang_kind ON symbols(lang, kind)");
8114
8231
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_file_fk ON symbols(file_fk)");
8232
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_name_id ON symbols(name, id)");
8115
8233
  this.db.exec(`
8116
8234
  CREATE TABLE IF NOT EXISTS refs (
8117
8235
  id INTEGER PRIMARY KEY,
@@ -8132,65 +8250,167 @@ var IndexStore = class _IndexStore {
8132
8250
  );
8133
8251
  this.ftsAvailable = true;
8134
8252
  const symbolCount = Number(
8135
- this.db.prepare("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
8253
+ this.stmt("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
8136
8254
  );
8137
8255
  const ftsCount = Number(
8138
- this.db.prepare("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
8256
+ this.stmt("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
8139
8257
  );
8140
8258
  if (symbolCount !== ftsCount) {
8141
8259
  this.db.exec("DELETE FROM symbols_fts");
8142
- const insert = this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)");
8143
- const rows = this.db.prepare("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
8144
- for (const row of rows) {
8145
- insert.run(row.id, buildIndexableText(row.name, row.signature, row.doc_comment));
8146
- }
8260
+ const rows = this.stmt("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
8261
+ this.bulkInsertFts(
8262
+ rows.map((row) => ({
8263
+ id: row.id,
8264
+ text: buildIndexableText(row.name, row.signature, row.doc_comment)
8265
+ }))
8266
+ );
8267
+ this.invalidateBm25();
8147
8268
  }
8148
8269
  } catch {
8149
8270
  this.ftsAvailable = false;
8150
8271
  }
8272
+ this.ensureNextSymbolIdSeeded();
8273
+ }
8274
+ // ─── ID allocation & bulk helpers ────────────────────────────────────────────
8275
+ static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
8276
+ /** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
8277
+ static MAX_SQL_VARS = 900;
8278
+ /**
8279
+ * Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
8280
+ * transaction on open; the first concurrent writer under BEGIN IMMEDIATE
8281
+ * re-reads and advances the counter atomically.
8282
+ */
8283
+ ensureNextSymbolIdSeeded() {
8284
+ const existing = this.stmt("SELECT value FROM metadata WHERE key = ?").get(
8285
+ _IndexStore.NEXT_SYMBOL_ID_KEY
8286
+ );
8287
+ if (existing?.value !== void 0) return;
8288
+ const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8289
+ const next = (maxRows[0]?.m ?? 0) + 1;
8290
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)").run(
8291
+ _IndexStore.NEXT_SYMBOL_ID_KEY,
8292
+ String(next)
8293
+ );
8294
+ }
8295
+ /**
8296
+ * Reserve `count` consecutive symbol ids. MUST run inside BEGIN IMMEDIATE
8297
+ * so concurrent indexers cannot hand out overlapping ranges.
8298
+ */
8299
+ allocateSymbolIds(count) {
8300
+ if (count <= 0) return this.getMaxSymbolId() + 1;
8301
+ this.ensureNextSymbolIdSeeded();
8302
+ const row = this.stmt("SELECT value FROM metadata WHERE key = ?").get(
8303
+ _IndexStore.NEXT_SYMBOL_ID_KEY
8304
+ );
8305
+ const start = Math.max(1, Number(row?.value ?? 1) || 1);
8306
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)").run(
8307
+ _IndexStore.NEXT_SYMBOL_ID_KEY,
8308
+ String(start + count)
8309
+ );
8310
+ return start;
8311
+ }
8312
+ /** Multi-row INSERT for symbols — amortizes prepare/bind overhead in large batches. */
8313
+ bulkInsertSymbols(rows) {
8314
+ if (rows.length === 0) return;
8315
+ const cols = 12;
8316
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / cols));
8317
+ for (let i = 0; i < rows.length; i += chunkSize) {
8318
+ const chunk = rows.slice(i, i + chunkSize);
8319
+ const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
8320
+ const stmt = this.stmt(
8321
+ `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
8322
+ VALUES ${placeholders}`
8323
+ );
8324
+ const binds = [];
8325
+ for (const r of chunk) {
8326
+ binds.push(
8327
+ r.id,
8328
+ r.lang,
8329
+ r.kind,
8330
+ r.name,
8331
+ r.file,
8332
+ r.line,
8333
+ r.col,
8334
+ r.signature,
8335
+ r.docComment,
8336
+ r.scope,
8337
+ r.text,
8338
+ r.file
8339
+ );
8340
+ }
8341
+ stmt.run(...binds);
8342
+ }
8343
+ }
8344
+ bulkInsertFts(rows) {
8345
+ if (!this.ftsAvailable || rows.length === 0) return;
8346
+ const cols = 2;
8347
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / cols));
8348
+ for (let i = 0; i < rows.length; i += chunkSize) {
8349
+ const chunk = rows.slice(i, i + chunkSize);
8350
+ const placeholders = chunk.map(() => "(?, ?)").join(", ");
8351
+ const stmt = this.stmt(`INSERT INTO symbols_fts(rowid, text) VALUES ${placeholders}`);
8352
+ const binds = [];
8353
+ for (const r of chunk) binds.push(r.id, r.text);
8354
+ stmt.run(...binds);
8355
+ }
8356
+ }
8357
+ bulkInsertRefs(refs) {
8358
+ if (refs.length === 0) return;
8359
+ const cols = 5;
8360
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / cols));
8361
+ for (let i = 0; i < refs.length; i += chunkSize) {
8362
+ const chunk = refs.slice(i, i + chunkSize);
8363
+ const placeholders = chunk.map(() => "(?, ?, ?, ?, ?)").join(", ");
8364
+ const stmt = this.stmt(
8365
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line) VALUES ${placeholders}`
8366
+ );
8367
+ const binds = [];
8368
+ for (const ref of chunk) {
8369
+ binds.push(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
8370
+ }
8371
+ stmt.run(...binds);
8372
+ }
8151
8373
  }
8152
8374
  // ─── Symbol CRUD ─────────────────────────────────────────────────────────────
8153
8375
  /**
8154
8376
  * Insert symbols, assigning IDs atomically inside `BEGIN IMMEDIATE` /
8155
- * `COMMIT`. The ID allocation (`SELECT MAX(id)`) and all `INSERT`s share
8156
- * the same transaction, preventing UNIQUE constraint violations when two
8157
- * processes index concurrently (each would see a different `MAX(id)` and
8158
- * neither can insert with the other's IDs).
8377
+ * `COMMIT`. Id ranges come from the `next_symbol_id` metadata counter
8378
+ * (O(1)); multi-row INSERT amortizes bind overhead for large files.
8159
8379
  *
8160
8380
  * @returns The symbols array with `id` fields populated so the caller can
8161
8381
  * use them for refs without re-reading from the DB.
8162
8382
  */
8163
8383
  insertSymbols(symbols) {
8384
+ this.invalidateBm25();
8164
8385
  return this.runWithRetry(() => {
8165
8386
  this.db.exec("BEGIN IMMEDIATE");
8166
8387
  try {
8167
- const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8168
- let nextId = (maxRows[0]?.m ?? 0) + 1;
8169
- const stmt = this.db.prepare(
8170
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
8171
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
8172
- );
8173
- const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
8388
+ let nextId = this.allocateSymbolIds(symbols.length);
8174
8389
  const result = [];
8390
+ const bulk = [];
8391
+ const ftsRows = [];
8175
8392
  for (const s of symbols) {
8176
8393
  const id = nextId++;
8177
- stmt.run(
8394
+ bulk.push({
8178
8395
  id,
8179
- s.lang,
8180
- s.kind,
8181
- s.name,
8182
- s.file,
8183
- s.line,
8184
- s.col,
8185
- s.signature,
8186
- s.docComment,
8187
- s.scope,
8188
- s.text,
8189
- s.file
8190
- );
8191
- ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
8396
+ lang: s.lang,
8397
+ kind: s.kind,
8398
+ name: s.name,
8399
+ file: s.file,
8400
+ line: s.line,
8401
+ col: s.col,
8402
+ signature: s.signature,
8403
+ docComment: s.docComment,
8404
+ scope: s.scope,
8405
+ text: s.text
8406
+ });
8407
+ if (this.ftsAvailable) {
8408
+ ftsRows.push({ id, text: buildIndexableText(s.name, s.signature, s.docComment) });
8409
+ }
8192
8410
  result.push({ ...s, id });
8193
8411
  }
8412
+ this.bulkInsertSymbols(bulk);
8413
+ this.bulkInsertFts(ftsRows);
8194
8414
  this.db.exec("COMMIT");
8195
8415
  return result;
8196
8416
  } catch (err) {
@@ -8200,6 +8420,7 @@ var IndexStore = class _IndexStore {
8200
8420
  });
8201
8421
  }
8202
8422
  deleteSymbolsForFile(file) {
8423
+ this.invalidateBm25();
8203
8424
  this.runWithRetry(() => {
8204
8425
  if (this.ftsAvailable) {
8205
8426
  this.stmt(
@@ -8215,17 +8436,20 @@ var IndexStore = class _IndexStore {
8215
8436
  * dropped the `files` row, leaving its symbols orphaned but still searchable.
8216
8437
  */
8217
8438
  deleteFile(file) {
8439
+ this.invalidateBm25();
8218
8440
  this.runWithRetry(() => {
8219
8441
  this.db.exec("BEGIN IMMEDIATE");
8220
8442
  try {
8221
8443
  if (this.ftsAvailable) {
8222
- this.db.prepare(
8444
+ this.stmt(
8223
8445
  "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
8224
8446
  ).run(file);
8225
8447
  }
8226
- this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
8227
- this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
8228
- this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
8448
+ this.stmt(
8449
+ "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
8450
+ ).run(file);
8451
+ this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
8452
+ this.stmt("DELETE FROM files WHERE file = ?").run(file);
8229
8453
  this.db.exec("COMMIT");
8230
8454
  } catch (err) {
8231
8455
  this.db.exec("ROLLBACK");
@@ -8262,7 +8486,7 @@ var IndexStore = class _IndexStore {
8262
8486
  };
8263
8487
  }
8264
8488
  getAllFileMetas() {
8265
- return this.db.prepare("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
8489
+ return this.stmt("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
8266
8490
  file: r.file,
8267
8491
  lang: r.lang,
8268
8492
  mtimeMs: r.mtime_ms,
@@ -8271,7 +8495,32 @@ var IndexStore = class _IndexStore {
8271
8495
  }));
8272
8496
  }
8273
8497
  // ─── Search ──────────────────────────────────────────────────────────────────
8274
- search(query, filter) {
8498
+ search(query, filter, opts) {
8499
+ const built = this.buildSearchWhere(query, filter);
8500
+ if (built === null) return [];
8501
+ const { where, values } = built;
8502
+ const limit = typeof opts?.limit === "number" && Number.isFinite(opts.limit) ? Math.max(0, Math.trunc(opts.limit)) : void 0;
8503
+ const limitSql = limit !== void 0 ? " LIMIT ?" : "";
8504
+ const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}${limitSql}`;
8505
+ const binds = limit !== void 0 ? [...values, limit] : values;
8506
+ const rows = this.stmt(sql).all(...binds);
8507
+ return rows.map((r) => ({
8508
+ id: r.id,
8509
+ lang: r.lang,
8510
+ kind: r.kind,
8511
+ name: r.name,
8512
+ file: r.file,
8513
+ line: r.line,
8514
+ col: r.col,
8515
+ signature: r.signature,
8516
+ docComment: r.doc_comment,
8517
+ score: 0,
8518
+ snippet: "",
8519
+ lspKind: filter?.lspKind
8520
+ }));
8521
+ }
8522
+ /** Shared WHERE builder for {@link search} / empty-query ranked totals. */
8523
+ buildSearchWhere(query, filter) {
8275
8524
  const conditions = [];
8276
8525
  const values = [];
8277
8526
  let effectiveKind = filter?.kind;
@@ -8280,7 +8529,7 @@ var IndexStore = class _IndexStore {
8280
8529
  if (mapped !== null) {
8281
8530
  effectiveKind = mapped;
8282
8531
  } else {
8283
- return [];
8532
+ return null;
8284
8533
  }
8285
8534
  }
8286
8535
  if (effectiveKind) {
@@ -8302,23 +8551,15 @@ var IndexStore = class _IndexStore {
8302
8551
  for (const t of tokens) values.push(`%${t}%`);
8303
8552
  }
8304
8553
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
8305
- const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}`;
8306
- const stmt = this.db.prepare(sql);
8307
- const rows = stmt.all(...values);
8308
- return rows.map((r) => ({
8309
- id: r.id,
8310
- lang: r.lang,
8311
- kind: r.kind,
8312
- name: r.name,
8313
- file: r.file,
8314
- line: r.line,
8315
- col: r.col,
8316
- signature: r.signature,
8317
- docComment: r.doc_comment,
8318
- score: 0,
8319
- snippet: "",
8320
- lspKind: filter?.lspKind
8321
- }));
8554
+ return { where, values };
8555
+ }
8556
+ countSearch(query, filter) {
8557
+ const built = this.buildSearchWhere(query, filter);
8558
+ if (built === null) return 0;
8559
+ const row = this.stmt(`SELECT COUNT(*) AS n FROM symbols ${built.where}`).get(
8560
+ ...built.values
8561
+ );
8562
+ return Number(row?.n ?? 0);
8322
8563
  }
8323
8564
  /**
8324
8565
  * Ranked search — the one-stop query the codebase-search tool and plug-lsp
@@ -8359,12 +8600,12 @@ var IndexStore = class _IndexStore {
8359
8600
  values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
8360
8601
  }
8361
8602
  const where = conditions.join(" AND ");
8362
- const countRows = this.db.prepare(
8603
+ const countRows = this.stmt(
8363
8604
  `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
8364
8605
  ).all(...values);
8365
8606
  const total = countRows[0] ? Number(countRows[0].n) : 0;
8366
8607
  if (total === 0) return { results: [], total: 0 };
8367
- const rows = this.db.prepare(
8608
+ const rows = this.stmt(
8368
8609
  `SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
8369
8610
  -bm25(symbols_fts) AS score,
8370
8611
  snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
@@ -8397,20 +8638,60 @@ var IndexStore = class _IndexStore {
8397
8638
  total
8398
8639
  };
8399
8640
  }
8641
+ /**
8642
+ * Invalidate the cached BM25 index.
8643
+ *
8644
+ * **Contract: every method that mutates `symbols` MUST call this before
8645
+ * returning.** (`refs` mutations do not affect the BM25 fallback because
8646
+ * the corpus is built from `symbols.text` via `getAllIndexable()` and the
8647
+ * BM25 score is filtered by the LIKE-selected candidate set in
8648
+ * `searchRankedFallback`.) Today the call sites are `repairDrift`,
8649
+ * `insertSymbols`, `deleteSymbolsForFile`, `deleteFile`, `clearAll`, and
8650
+ * `commitBatch`. A future mutation that adds a new write path (e.g.
8651
+ * `renameFile`, `updateSignature`) MUST also call this — otherwise the
8652
+ * FTS5-unavailable fallback will serve stale search results. The
8653
+ * `close()` reset at L1820-1821 tears the cache down on store shutdown,
8654
+ * which is the only legitimate place that flips the flag outside this
8655
+ * helper.
8656
+ *
8657
+ * Called *before* `runWithRetry` on purpose: if the write fails all
8658
+ * retries the flag stays set, forcing a rebuild on the next search rather
8659
+ * than trusting a cache that may not reflect the intended mutation.
8660
+ * Do not move this inside the retry closure.
8661
+ */
8662
+ invalidateBm25() {
8663
+ this.bm25Dirty = true;
8664
+ this.bm25Cache = null;
8665
+ }
8666
+ /**
8667
+ * Return the cached full-corpus BM25 index, rebuilding it only when the
8668
+ * symbols table has been mutated since the last build. The full-corpus IDF
8669
+ * is more correct than the old per-query candidate-subset IDF, and the
8670
+ * amortized build cost drops from O(symbols × tokens) per search to once
8671
+ * per write batch.
8672
+ *
8673
+ * Note: the first call after a long idle (or on a freshly opened store)
8674
+ * pays the full corpus rebuild synchronously on the search path. For a
8675
+ * 5 500+ symbol corpus this is a visible one-time latency spike.
8676
+ */
8677
+ getOrBuildBm25() {
8678
+ if (this.bm25Cache && !this.bm25Dirty) return this.bm25Cache;
8679
+ const docs = this.getAllIndexable();
8680
+ this.bm25Cache = buildBm25Index(docs);
8681
+ this.bm25Dirty = false;
8682
+ return this.bm25Cache;
8683
+ }
8400
8684
  /** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
8401
8685
  searchRankedFallback(query, filter, limit) {
8402
- const candidates = this.search(query, filter);
8403
- if (candidates.length === 0) return { results: [], total: 0 };
8404
8686
  if (!query.trim()) {
8405
- return { results: candidates.slice(0, limit), total: candidates.length };
8687
+ const total = this.countSearch(query, filter);
8688
+ if (total === 0) return { results: [], total: 0 };
8689
+ return { results: this.search(query, filter, { limit }), total };
8406
8690
  }
8691
+ const candidates = this.search(query, filter);
8692
+ if (candidates.length === 0) return { results: [], total: 0 };
8407
8693
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
8408
- const bm25 = buildBm25Index(
8409
- candidates.map((c) => ({
8410
- id: c.id,
8411
- text: buildIndexableText(c.name, c.signature, c.docComment)
8412
- }))
8413
- );
8694
+ const bm25 = this.getOrBuildBm25();
8414
8695
  const scored = bm25.score(query, (id) => candidateById.has(id));
8415
8696
  const q = query.trim().toLowerCase();
8416
8697
  const rank = (id) => {
@@ -8452,7 +8733,7 @@ var IndexStore = class _IndexStore {
8452
8733
  // ─── Stats ───────────────────────────────────────────────────────────────────
8453
8734
  getStats() {
8454
8735
  const sizeBytes = this.sizeBytes();
8455
- const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
8736
+ const lastRows = this.stmt("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
8456
8737
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
8457
8738
  const totalRows = this.stmt("SELECT COUNT(*) FROM symbols").all();
8458
8739
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
@@ -8477,19 +8758,20 @@ var IndexStore = class _IndexStore {
8477
8758
  }
8478
8759
  setLastIndexed(ts2) {
8479
8760
  this.runWithRetry(() => {
8480
- this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
8761
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
8481
8762
  });
8482
8763
  }
8483
8764
  getMetadata(key) {
8484
- const rows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all(key);
8765
+ const rows = this.stmt("SELECT value FROM metadata WHERE key = ?").all(key);
8485
8766
  return rows[0]?.value;
8486
8767
  }
8487
8768
  setMetadata(key, value) {
8488
8769
  this.runWithRetry(() => {
8489
- this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
8770
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
8490
8771
  });
8491
8772
  }
8492
8773
  clearAll() {
8774
+ this.invalidateBm25();
8493
8775
  this.runWithRetry(() => {
8494
8776
  this.db.exec("BEGIN IMMEDIATE");
8495
8777
  try {
@@ -8516,13 +8798,7 @@ var IndexStore = class _IndexStore {
8516
8798
  this.runWithRetry(() => {
8517
8799
  this.stmt("DELETE FROM refs WHERE from_id = ?").run(fromId);
8518
8800
  if (refs.length === 0) return;
8519
- const stmt = this.db.prepare(
8520
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
8521
- VALUES (?, ?, ?, ?, ?)`
8522
- );
8523
- for (const ref of refs) {
8524
- stmt.run(fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
8525
- }
8801
+ this.bulkInsertRefs(refs.map((ref) => ({ ...ref, fromId })));
8526
8802
  });
8527
8803
  }
8528
8804
  /**
@@ -8539,13 +8815,7 @@ var IndexStore = class _IndexStore {
8539
8815
  insertRefsBatch(refs) {
8540
8816
  if (refs.length === 0) return;
8541
8817
  this.runWithRetry(() => {
8542
- const stmt = this.db.prepare(
8543
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
8544
- VALUES (?, ?, ?, ?, ?)`
8545
- );
8546
- for (const ref of refs) {
8547
- stmt.run(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
8548
- }
8818
+ this.bulkInsertRefs(refs);
8549
8819
  });
8550
8820
  }
8551
8821
  /**
@@ -8570,65 +8840,61 @@ var IndexStore = class _IndexStore {
8570
8840
  if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
8571
8841
  return [];
8572
8842
  }
8843
+ this.invalidateBm25();
8573
8844
  return this.runWithRetry(() => {
8574
8845
  this.db.exec("BEGIN IMMEDIATE");
8575
8846
  try {
8576
8847
  if (options.deleteForFiles && options.deleteForFiles.length > 0) {
8577
8848
  const placeholders = options.deleteForFiles.map(() => "?").join(",");
8578
8849
  if (this.ftsAvailable) {
8579
- this.db.prepare(
8850
+ this.stmt(
8580
8851
  `DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
8581
8852
  ).run(...options.deleteForFiles);
8582
8853
  }
8583
- this.db.prepare(
8854
+ this.stmt(
8584
8855
  `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
8585
8856
  ).run(...options.deleteForFiles);
8586
- this.db.prepare(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
8857
+ this.stmt(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
8587
8858
  }
8588
- const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8589
- let nextId = (maxRows[0]?.m ?? 0) + 1;
8590
- const symStmt = this.db.prepare(
8591
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
8592
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
8593
- );
8594
- const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
8859
+ const totalSymbols = entries.reduce((n, e) => n + e.symbols.length, 0);
8860
+ let nextId = this.allocateSymbolIds(totalSymbols);
8595
8861
  const allInserted = [];
8596
8862
  const refsToInsert = [];
8863
+ const bulkSyms = [];
8864
+ const ftsRows = [];
8597
8865
  for (const entry of entries) {
8598
8866
  const insertedForEntry = [];
8599
8867
  for (const s of entry.symbols) {
8600
8868
  const id = nextId++;
8601
- symStmt.run(
8869
+ bulkSyms.push({
8602
8870
  id,
8603
- s.lang,
8604
- s.kind,
8605
- s.name,
8606
- s.file,
8607
- s.line,
8608
- s.col,
8609
- s.signature,
8610
- s.docComment,
8611
- s.scope,
8612
- s.text,
8613
- s.file
8614
- );
8615
- ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
8871
+ lang: s.lang,
8872
+ kind: s.kind,
8873
+ name: s.name,
8874
+ file: s.file,
8875
+ line: s.line,
8876
+ col: s.col,
8877
+ signature: s.signature,
8878
+ docComment: s.docComment,
8879
+ scope: s.scope,
8880
+ text: s.text
8881
+ });
8882
+ if (this.ftsAvailable) {
8883
+ ftsRows.push({
8884
+ id,
8885
+ text: buildIndexableText(s.name, s.signature, s.docComment)
8886
+ });
8887
+ }
8616
8888
  const inserted = { ...s, id };
8617
8889
  allInserted.push(inserted);
8618
8890
  insertedForEntry.push(inserted);
8619
8891
  }
8620
8892
  refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
8621
8893
  }
8622
- if (refsToInsert.length > 0) {
8623
- const refStmt = this.db.prepare(
8624
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
8625
- VALUES (?, ?, ?, ?, ?)`
8626
- );
8627
- for (const ref of refsToInsert) {
8628
- refStmt.run(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
8629
- }
8630
- }
8631
- const upsertStmt = this.db.prepare(
8894
+ this.bulkInsertSymbols(bulkSyms);
8895
+ this.bulkInsertFts(ftsRows);
8896
+ this.bulkInsertRefs(refsToInsert);
8897
+ const upsertStmt = this.stmt(
8632
8898
  `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
8633
8899
  VALUES (?, ?, ?, ?, ?)
8634
8900
  ON CONFLICT(file) DO UPDATE SET
@@ -8655,7 +8921,7 @@ var IndexStore = class _IndexStore {
8655
8921
  */
8656
8922
  deleteRefsForFile(file) {
8657
8923
  this.runWithRetry(() => {
8658
- this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
8924
+ this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
8659
8925
  });
8660
8926
  }
8661
8927
  /**
@@ -8668,20 +8934,75 @@ var IndexStore = class _IndexStore {
8668
8934
  */
8669
8935
  resolveRefs() {
8670
8936
  return this.runWithRetry(() => {
8671
- const result = this.db.prepare(
8672
- `UPDATE refs SET to_id = (
8673
- SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
8674
- ) WHERE to_id IS NULL AND to_name IS NOT NULL
8675
- AND to_name IN (SELECT name FROM symbols)`
8676
- ).run();
8677
- return result.changes ?? 0;
8937
+ try {
8938
+ const result = this.stmt(
8939
+ `UPDATE refs
8940
+ SET to_id = s.id
8941
+ FROM (
8942
+ SELECT name, MIN(id) AS id FROM symbols GROUP BY name
8943
+ ) AS s
8944
+ WHERE refs.to_id IS NULL
8945
+ AND refs.to_name IS NOT NULL
8946
+ AND refs.to_name = s.name`
8947
+ ).run();
8948
+ return result.changes ?? 0;
8949
+ } catch {
8950
+ const result = this.stmt(
8951
+ `UPDATE refs SET to_id = (
8952
+ SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
8953
+ ) WHERE to_id IS NULL AND to_name IS NOT NULL
8954
+ AND to_name IN (SELECT name FROM symbols)`
8955
+ ).run();
8956
+ return result.changes ?? 0;
8957
+ }
8958
+ });
8959
+ }
8960
+ /**
8961
+ * Clear symbols/refs for a file and mark it as indexed with zero symbols.
8962
+ * Used by the indexer for empty-parse results so three writes share one txn.
8963
+ */
8964
+ replaceEmptyFile(meta) {
8965
+ this.invalidateBm25();
8966
+ this.runWithRetry(() => {
8967
+ this.db.exec("BEGIN IMMEDIATE");
8968
+ try {
8969
+ if (this.ftsAvailable) {
8970
+ this.stmt(
8971
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
8972
+ ).run(meta.file);
8973
+ }
8974
+ this.stmt(
8975
+ "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
8976
+ ).run(meta.file);
8977
+ this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(meta.file);
8978
+ this.stmt(
8979
+ `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
8980
+ VALUES (?, ?, ?, ?, ?)
8981
+ ON CONFLICT(file) DO UPDATE SET
8982
+ lang = excluded.lang,
8983
+ mtime_ms = excluded.mtime_ms,
8984
+ symbol_count = excluded.symbol_count,
8985
+ last_indexed = excluded.last_indexed`
8986
+ ).run(meta.file, meta.lang, meta.mtimeMs, meta.symbolCount, meta.lastIndexed);
8987
+ this.db.exec("COMMIT");
8988
+ } catch (err) {
8989
+ this.db.exec("ROLLBACK");
8990
+ throw err;
8991
+ }
8678
8992
  });
8679
8993
  }
8994
+ /** Best-effort query planner refresh after a large reindex. */
8995
+ optimize() {
8996
+ try {
8997
+ this.db.exec("PRAGMA optimize");
8998
+ } catch {
8999
+ }
9000
+ }
8680
9001
  /**
8681
9002
  * Find all references TO a given symbol (who calls / uses this symbol?).
8682
9003
  */
8683
9004
  findRefsTo(symbolId) {
8684
- return this.db.prepare(
9005
+ return this.stmt(
8685
9006
  "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 = ?)"
8686
9007
  ).all(symbolId, symbolId).map((r) => ({
8687
9008
  id: r.id,
@@ -8696,7 +9017,7 @@ var IndexStore = class _IndexStore {
8696
9017
  * Find all references FROM a given symbol (what does this symbol call/use?).
8697
9018
  */
8698
9019
  findRefsFrom(symbolId) {
8699
- return this.db.prepare("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
9020
+ return this.stmt("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
8700
9021
  id: r.id,
8701
9022
  fromId: r.from_id,
8702
9023
  toName: r.to_name,
@@ -8769,45 +9090,57 @@ var IndexStore = class _IndexStore {
8769
9090
  * symbol resolved in package B). Node metadata includes symbol/file counts.
8770
9091
  */
8771
9092
  getPackageGraph() {
8772
- const symbols = this.db.prepare("SELECT file, id FROM symbols ORDER BY id").all();
9093
+ const fileCounts = this.stmt(
9094
+ "SELECT file, COUNT(*) AS n FROM symbols GROUP BY file"
9095
+ ).all();
8773
9096
  const pkgNodes = /* @__PURE__ */ new Map();
8774
9097
  const fileToPkg = /* @__PURE__ */ new Map();
8775
- const symbolToPkg = /* @__PURE__ */ new Map();
8776
- for (const s of symbols) {
8777
- const pkg = _IndexStore.derivePackage(s.file) ?? "(root)";
8778
- fileToPkg.set(s.file, pkg);
8779
- symbolToPkg.set(s.id, pkg);
9098
+ for (const { file, n } of fileCounts) {
9099
+ const pkg = _IndexStore.derivePackage(file) ?? "(root)";
9100
+ fileToPkg.set(file, pkg);
8780
9101
  const node = pkgNodes.get(pkg);
8781
9102
  if (node) {
8782
- node.symbolCount = (node.symbolCount ?? 0) + 1;
9103
+ node.symbolCount = (node.symbolCount ?? 0) + n;
8783
9104
  } else {
8784
9105
  pkgNodes.set(pkg, {
8785
9106
  id: `pkg:${pkg}`,
8786
9107
  label: pkg,
8787
9108
  kind: "package",
8788
9109
  package: pkg,
8789
- symbolCount: 1,
9110
+ symbolCount: n,
8790
9111
  fileCount: 0
8791
9112
  });
8792
9113
  }
8793
9114
  }
8794
- const files = this.db.prepare("SELECT DISTINCT file FROM files").all();
9115
+ const files = this.stmt("SELECT DISTINCT file FROM files").all();
8795
9116
  for (const { file } of files) {
8796
9117
  const pkg = _IndexStore.derivePackage(file) ?? "(root)";
9118
+ fileToPkg.set(file, pkg);
8797
9119
  const node = pkgNodes.get(pkg);
8798
9120
  if (node) node.fileCount = (node.fileCount ?? 0) + 1;
9121
+ else {
9122
+ pkgNodes.set(pkg, {
9123
+ id: `pkg:${pkg}`,
9124
+ label: pkg,
9125
+ kind: "package",
9126
+ package: pkg,
9127
+ symbolCount: 0,
9128
+ fileCount: 1
9129
+ });
9130
+ }
8799
9131
  }
8800
- const refRows = this.db.prepare(
8801
- `SELECT r.from_id, r.to_id, r.call_type
9132
+ const refRows = this.stmt(
9133
+ `SELECT r.call_type, sf.file AS from_file, st.file AS to_file
8802
9134
  FROM refs r
8803
- WHERE r.to_id IS NOT NULL`
9135
+ JOIN symbols sf ON sf.id = r.from_id
9136
+ JOIN symbols st ON st.id = r.to_id
9137
+ WHERE r.to_id IS NOT NULL AND r.call_type != 'import'`
8804
9138
  ).all();
8805
9139
  const edgeMap = /* @__PURE__ */ new Map();
8806
9140
  for (const r of refRows) {
8807
- if (r.call_type === "import") continue;
8808
- const fromPkg = symbolToPkg.get(r.from_id);
8809
- const toPkg = symbolToPkg.get(r.to_id);
8810
- if (!fromPkg || !toPkg || fromPkg === toPkg) continue;
9141
+ const fromPkg = fileToPkg.get(r.from_file) ?? _IndexStore.derivePackage(r.from_file) ?? "(root)";
9142
+ const toPkg = fileToPkg.get(r.to_file) ?? _IndexStore.derivePackage(r.to_file) ?? "(root)";
9143
+ if (fromPkg === toPkg) continue;
8811
9144
  const key = `${fromPkg}\0${toPkg}`;
8812
9145
  let e = edgeMap.get(key);
8813
9146
  if (!e) {
@@ -8817,13 +9150,14 @@ var IndexStore = class _IndexStore {
8817
9150
  e.weight++;
8818
9151
  e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
8819
9152
  }
8820
- const importRows = this.db.prepare(
8821
- `SELECT r.from_id, r.to_name
9153
+ const importRows = this.stmt(
9154
+ `SELECT r.to_name, s.file AS from_file
8822
9155
  FROM refs r
9156
+ JOIN symbols s ON s.id = r.from_id
8823
9157
  WHERE r.call_type = 'import'`
8824
9158
  ).all();
8825
9159
  for (const r of importRows) {
8826
- const fromPkg = symbolToPkg.get(r.from_id);
9160
+ const fromPkg = fileToPkg.get(r.from_file) ?? _IndexStore.derivePackage(r.from_file) ?? "(root)";
8827
9161
  const toPkg = _IndexStore.packageFromImport(r.to_name);
8828
9162
  if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
8829
9163
  const key = `${fromPkg}\0${toPkg}`;
@@ -8860,12 +9194,12 @@ var IndexStore = class _IndexStore {
8860
9194
  * derived from cross-file symbol references within the package.
8861
9195
  */
8862
9196
  getFileGraph(packageFilter) {
8863
- const allFiles = this.db.prepare("SELECT DISTINCT file FROM symbols").all();
9197
+ const allFiles = this.stmt("SELECT DISTINCT file FROM symbols").all();
8864
9198
  const pkgFilePaths = allFiles.filter((f) => (_IndexStore.derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
8865
9199
  const localFiles = new Set(pkgFilePaths);
8866
9200
  if (localFiles.size === 0) return { nodes: [], edges: [] };
8867
9201
  const filePlaceholders = [...localFiles].map(() => "?").join(",");
8868
- const pkgSyms = this.db.prepare(
9202
+ const pkgSyms = this.stmt(
8869
9203
  `SELECT file, id, name, kind, lang, line FROM symbols WHERE file IN (${filePlaceholders}) ORDER BY id`
8870
9204
  ).all(...pkgFilePaths);
8871
9205
  const fileNodes = /* @__PURE__ */ new Map();
@@ -8897,7 +9231,7 @@ var IndexStore = class _IndexStore {
8897
9231
  ensureFileNode(file);
8898
9232
  }
8899
9233
  const indexedFiles = new Set(allFiles.map((f) => f.file));
8900
- const refRows = this.db.prepare(
9234
+ const refRows = this.stmt(
8901
9235
  `SELECT r.from_id, r.to_id, r.call_type
8902
9236
  FROM refs r
8903
9237
  WHERE (r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
@@ -8912,7 +9246,7 @@ var IndexStore = class _IndexStore {
8912
9246
  }
8913
9247
  if (crossRefIds.size > 0) {
8914
9248
  const crossPlaceholders = [...crossRefIds].map(() => "?").join(",");
8915
- const extras = this.db.prepare(
9249
+ const extras = this.stmt(
8916
9250
  `SELECT id, file FROM symbols WHERE id IN (${crossPlaceholders})`
8917
9251
  ).all(...crossRefIds);
8918
9252
  for (const x of extras) {
@@ -8940,7 +9274,7 @@ var IndexStore = class _IndexStore {
8940
9274
  e.weight++;
8941
9275
  e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
8942
9276
  }
8943
- const importRows = this.db.prepare(
9277
+ const importRows = this.stmt(
8944
9278
  `SELECT r.from_id, r.to_name
8945
9279
  FROM refs r
8946
9280
  WHERE r.call_type = 'import'
@@ -8987,7 +9321,7 @@ var IndexStore = class _IndexStore {
8987
9321
  * derived from intra-file and cross-file symbol references (who calls whom).
8988
9322
  */
8989
9323
  getSymbolGraph(fileFilter) {
8990
- const syms = this.db.prepare(
9324
+ const syms = this.stmt(
8991
9325
  "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE file = ? ORDER BY line, id"
8992
9326
  ).all(fileFilter);
8993
9327
  if (syms.length === 0) return { nodes: [], edges: [] };
@@ -9008,11 +9342,16 @@ var IndexStore = class _IndexStore {
9008
9342
  external: s.file !== fileFilter
9009
9343
  });
9010
9344
  const symIds = new Set(syms.map((s) => s.id));
9011
- const refRows = this.db.prepare(
9345
+ const refRows = this.stmt(
9012
9346
  `SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
9013
9347
  FROM refs r
9014
- WHERE r.from_id IN (SELECT id FROM symbols WHERE file = ?)
9015
- OR r.to_id IN (SELECT id FROM symbols WHERE file = ?)`
9348
+ JOIN symbols s ON s.id = r.from_id
9349
+ WHERE s.file = ?
9350
+ UNION
9351
+ SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
9352
+ FROM refs r
9353
+ JOIN symbols s ON s.id = r.to_id
9354
+ WHERE s.file = ?`
9016
9355
  ).all(fileFilter, fileFilter);
9017
9356
  const edgeMap = /* @__PURE__ */ new Map();
9018
9357
  for (const r of refRows) {
@@ -9051,7 +9390,7 @@ var IndexStore = class _IndexStore {
9051
9390
  const missingIds = [...relatedIds].filter((id) => !loadedIds.has(id));
9052
9391
  if (missingIds.length > 0) {
9053
9392
  const placeholders = missingIds.map(() => "?").join(",");
9054
- const extras = this.db.prepare(
9393
+ const extras = this.stmt(
9055
9394
  `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders})`
9056
9395
  ).all(...missingIds);
9057
9396
  for (const s of extras) symById.set(s.id, s);
@@ -9065,6 +9404,8 @@ var IndexStore = class _IndexStore {
9065
9404
  }
9066
9405
  close() {
9067
9406
  this.stmtCache.clear();
9407
+ this.bm25Dirty = true;
9408
+ this.bm25Cache = null;
9068
9409
  try {
9069
9410
  this.db.close();
9070
9411
  } catch {
@@ -9078,14 +9419,118 @@ import { fileURLToPath } from "node:url";
9078
9419
  import { Worker } from "node:worker_threads";
9079
9420
 
9080
9421
  // src/codebase-index/indexer.ts
9081
- import { expectDefined as expectDefined6 } from "@wrongstack/core";
9422
+ import { expectDefined as expectDefined6 } from "@wrongstack/core/utils";
9082
9423
  import * as fs14 from "node:fs/promises";
9083
- import * as path19 from "node:path";
9424
+ import * as path20 from "node:path";
9084
9425
  import { availableParallelism } from "node:os";
9085
- import { compileGlob as compileGlob2 } from "@wrongstack/core";
9426
+ import { indexParallelBatchSize, isFrugalPerf } from "@wrongstack/core/utils";
9086
9427
 
9087
9428
  // src/codebase-index/ts-parser.ts
9088
9429
  import * as ts from "@typescript/typescript6";
9430
+
9431
+ // src/codebase-index/languages.ts
9432
+ import * as path14 from "node:path";
9433
+ var EXT_TO_LANG = {
9434
+ // TypeScript / JavaScript (first-class TS compiler API)
9435
+ ".ts": "ts",
9436
+ ".mts": "ts",
9437
+ ".cts": "ts",
9438
+ ".tsx": "tsx",
9439
+ ".js": "js",
9440
+ ".mjs": "js",
9441
+ ".cjs": "js",
9442
+ ".jsx": "jsx",
9443
+ // First-class native/spawn parsers (+ regex fallback)
9444
+ ".go": "go",
9445
+ ".py": "py",
9446
+ ".pyi": "py",
9447
+ ".pyw": "py",
9448
+ ".rs": "rs",
9449
+ ".json": "json",
9450
+ ".jsonc": "json",
9451
+ ".yaml": "yaml",
9452
+ ".yml": "yaml",
9453
+ // C family
9454
+ ".c": "c",
9455
+ ".h": "c",
9456
+ ".cc": "cpp",
9457
+ ".cpp": "cpp",
9458
+ ".cxx": "cpp",
9459
+ ".hh": "cpp",
9460
+ ".hpp": "cpp",
9461
+ ".hxx": "cpp",
9462
+ // JVM / .NET
9463
+ ".java": "java",
9464
+ ".cs": "csharp",
9465
+ ".kt": "kotlin",
9466
+ ".kts": "kotlin",
9467
+ ".scala": "scala",
9468
+ ".sc": "scala",
9469
+ // Scripting
9470
+ ".php": "php",
9471
+ ".rb": "ruby",
9472
+ ".swift": "swift",
9473
+ ".dart": "dart",
9474
+ ".lua": "lua",
9475
+ ".r": "r",
9476
+ ".R": "r",
9477
+ ".pl": "other",
9478
+ ".pm": "other",
9479
+ // Systems / functional
9480
+ ".zig": "zig",
9481
+ ".ex": "elixir",
9482
+ ".exs": "elixir",
9483
+ ".hs": "haskell",
9484
+ ".lhs": "haskell",
9485
+ // Shell / data / docs / web
9486
+ ".sh": "shell",
9487
+ ".bash": "shell",
9488
+ ".zsh": "shell",
9489
+ ".ps1": "shell",
9490
+ ".sql": "sql",
9491
+ ".md": "md",
9492
+ ".mdx": "md",
9493
+ ".toml": "toml",
9494
+ ".html": "html",
9495
+ ".htm": "html",
9496
+ ".css": "css",
9497
+ ".scss": "css",
9498
+ ".less": "css",
9499
+ ".vue": "vue",
9500
+ ".svelte": "svelte",
9501
+ ".proto": "proto",
9502
+ ".graphql": "graphql",
9503
+ ".gql": "graphql"
9504
+ };
9505
+ var INDEXABLE_EXTENSIONS = Object.freeze(
9506
+ [...new Set(Object.keys(EXT_TO_LANG).map((e) => e.toLowerCase()))].sort()
9507
+ );
9508
+ var SPECIAL_FILENAMES = {
9509
+ makefile: "other",
9510
+ gnumakefile: "other",
9511
+ dockerfile: "other",
9512
+ "docker-compose.yml": "yaml",
9513
+ "docker-compose.yaml": "yaml",
9514
+ "cmakelists.txt": "other",
9515
+ gemfile: "ruby",
9516
+ rakefile: "ruby",
9517
+ procfile: "other",
9518
+ justfile: "other"
9519
+ };
9520
+ function detectLang(file) {
9521
+ const base = path14.basename(file);
9522
+ const lowerBase = base.toLowerCase();
9523
+ if (lowerBase.endsWith(".d.ts") || lowerBase.endsWith(".d.mts") || lowerBase.endsWith(".d.cts")) {
9524
+ return "ts";
9525
+ }
9526
+ const special = SPECIAL_FILENAMES[lowerBase];
9527
+ if (special) return special;
9528
+ const ext = path14.extname(base).toLowerCase();
9529
+ if (!ext) return null;
9530
+ return EXT_TO_LANG[ext] ?? null;
9531
+ }
9532
+
9533
+ // src/codebase-index/ts-parser.ts
9089
9534
  var KIND_MAP = {
9090
9535
  [ts.SyntaxKind.ClassDeclaration]: "class",
9091
9536
  [ts.SyntaxKind.InterfaceDeclaration]: "interface",
@@ -9112,32 +9557,6 @@ function kindOf(node) {
9112
9557
  if (ts.isModuleDeclaration(node)) return "namespace";
9113
9558
  return KIND_MAP[node.kind] ?? null;
9114
9559
  }
9115
- function extToLang(ext) {
9116
- switch (ext) {
9117
- case ".ts":
9118
- return "ts";
9119
- case ".tsx":
9120
- return "tsx";
9121
- case ".js":
9122
- return "js";
9123
- case ".jsx":
9124
- return "jsx";
9125
- case ".go":
9126
- return "go";
9127
- case ".py":
9128
- return "py";
9129
- case ".rs":
9130
- return "rs";
9131
- case ".json":
9132
- return "json";
9133
- case ".yaml":
9134
- return "yaml";
9135
- case ".yml":
9136
- return "yaml";
9137
- default:
9138
- return null;
9139
- }
9140
- }
9141
9560
  function getSignature(printer, node, sourceFile) {
9142
9561
  const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
9143
9562
  return raw.replace(/\s+/g, " ").slice(0, 500);
@@ -9260,22 +9679,30 @@ function deduplicateRefs(refs) {
9260
9679
  return true;
9261
9680
  });
9262
9681
  }
9263
- function detectLang(file) {
9264
- const idx = file.lastIndexOf(".");
9265
- if (idx < 0) return null;
9266
- return extToLang(file.slice(idx));
9267
- }
9268
9682
 
9269
9683
  // src/codebase-index/go-parser.ts
9270
9684
  init_win32_resolve();
9271
9685
  import { spawn as spawn4 } from "node:child_process";
9272
9686
  import * as os5 from "node:os";
9273
- import * as path14 from "node:path";
9687
+ import * as path15 from "node:path";
9274
9688
  import * as fs10 from "node:fs/promises";
9689
+
9690
+ // src/codebase-index/spawn-gate.ts
9691
+ var chain = Promise.resolve();
9692
+ function withSpawnGate(fn) {
9693
+ const run = chain.then(fn, fn);
9694
+ chain = run.then(
9695
+ () => void 0,
9696
+ () => void 0
9697
+ );
9698
+ return run;
9699
+ }
9700
+
9701
+ // src/codebase-index/go-parser.ts
9275
9702
  async function parseSymbols2(opts) {
9276
9703
  const { file, content, lang } = opts;
9277
9704
  try {
9278
- const parsed = await syncGoParse(file, content, lang);
9705
+ const parsed = await withSpawnGate(() => syncGoParse(file, content, lang));
9279
9706
  if (parsed.symbols.length > 0) {
9280
9707
  return parsed;
9281
9708
  }
@@ -9571,11 +9998,16 @@ func formatType(t ast.Expr) string {
9571
9998
  }
9572
9999
  }
9573
10000
  `;
10001
+ var _cachedGoScriptPath = null;
9574
10002
  async function syncGoParse(filePath, content, lang) {
9575
- const tmpDir = await fs10.mkdtemp(path14.join(os5.tmpdir(), "ws-go-parse-"));
9576
10003
  try {
9577
- const scriptPath = path14.join(tmpDir, "parse.go");
9578
- await fs10.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
10004
+ let scriptPath = _cachedGoScriptPath;
10005
+ if (!scriptPath) {
10006
+ const tmpDir = await fs10.mkdtemp(path15.join(os5.tmpdir(), "ws-go-parse-"));
10007
+ scriptPath = path15.join(tmpDir, "parse.go");
10008
+ await fs10.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
10009
+ _cachedGoScriptPath = scriptPath;
10010
+ }
9579
10011
  const goBinary = resolveWin32Command("go");
9580
10012
  const goResult = await new Promise(
9581
10013
  (resolve14, reject) => {
@@ -9632,24 +10064,313 @@ async function syncGoParse(filePath, content, lang) {
9632
10064
  return { file: filePath, lang, symbols, mtimeMs: Date.now() };
9633
10065
  } catch {
9634
10066
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
9635
- } finally {
9636
- await fs10.rm(tmpDir, { recursive: true, force: true });
9637
10067
  }
9638
10068
  }
9639
10069
 
9640
10070
  // src/codebase-index/py-parser.ts
9641
10071
  init_win32_resolve();
9642
- import { spawn as spawn5, spawnSync } from "node:child_process";
10072
+ import { spawn as spawn5 } from "node:child_process";
9643
10073
  import * as fs11 from "node:fs/promises";
9644
10074
  import * as os6 from "node:os";
9645
- import * as path15 from "node:path";
10075
+ import * as path16 from "node:path";
10076
+
10077
+ // src/codebase-index/generic-parser.ts
10078
+ var C_LIKE = [
10079
+ { re: /\b(?:class|struct|enum|interface|union)\s+([A-Za-z_]\w*)/g, kind: "class" },
10080
+ {
10081
+ 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,
10082
+ kind: "function"
10083
+ },
10084
+ { re: /\b(?:namespace)\s+([A-Za-z_]\w*)/g, kind: "namespace" }
10085
+ ];
10086
+ var LANG_PATTERNS = {
10087
+ py: [
10088
+ { re: /^(?:async\s+)?def\s+([A-Za-z_]\w*)/gm, kind: "function" },
10089
+ { re: /^class\s+([A-Za-z_]\w*)/gm, kind: "class" },
10090
+ { re: /^([A-Za-z_]\w*)\s*=/gm, kind: "var" }
10091
+ ],
10092
+ go: [
10093
+ { re: /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(/gm, kind: "function" },
10094
+ { re: /^type\s+([A-Za-z_]\w*)\b/gm, kind: "type" },
10095
+ { re: /^(?:const|var)\s+([A-Za-z_]\w*)\b/gm, kind: "const" },
10096
+ { re: /^package\s+([A-Za-z_]\w*)/gm, kind: "namespace" }
10097
+ ],
10098
+ rs: [
10099
+ { re: /\bfn\s+([A-Za-z_]\w*)/g, kind: "function" },
10100
+ { re: /\bstruct\s+([A-Za-z_]\w*)/g, kind: "struct" },
10101
+ { re: /\benum\s+([A-Za-z_]\w*)/g, kind: "enum" },
10102
+ { re: /\btrait\s+([A-Za-z_]\w*)/g, kind: "trait" },
10103
+ { re: /\bimpl(?:\s*<[^>]+>)?\s+([A-Za-z_]\w*)/g, kind: "impl" },
10104
+ { re: /\b(?:const|static)\s+([A-Za-z_]\w*)/g, kind: "const" },
10105
+ { re: /\bmod\s+([A-Za-z_]\w*)/g, kind: "mod" }
10106
+ ],
10107
+ c: C_LIKE,
10108
+ cpp: C_LIKE,
10109
+ java: [
10110
+ { re: /\b(?:class|interface|enum|record)\s+([A-Za-z_]\w*)/g, kind: "class" },
10111
+ {
10112
+ re: /\b(?:public|private|protected|static|final|abstract|synchronized|native|default|\s)+\s*[\w.<>,\[\]\s]+\s+([A-Za-z_]\w*)\s*\(/g,
10113
+ kind: "method"
10114
+ }
10115
+ ],
10116
+ csharp: [
10117
+ { re: /\b(?:class|interface|struct|enum|record)\s+([A-Za-z_]\w*)/g, kind: "class" },
10118
+ { re: /\bnamespace\s+([A-Za-z_.\w]+)/g, kind: "namespace" },
10119
+ {
10120
+ re: /\b(?:public|private|protected|internal|static|async|override|virtual|\s)+\s*[\w.<>,\[\]\s]+\s+([A-Za-z_]\w*)\s*\(/g,
10121
+ kind: "method"
10122
+ }
10123
+ ],
10124
+ php: [
10125
+ { re: /\bfunction\s+([A-Za-z_]\w*)/g, kind: "function" },
10126
+ { re: /\bclass\s+([A-Za-z_]\w*)/g, kind: "class" },
10127
+ { re: /\binterface\s+([A-Za-z_]\w*)/g, kind: "interface" },
10128
+ { re: /\bnamespace\s+([A-Za-z_\\]+)/g, kind: "namespace" }
10129
+ ],
10130
+ ruby: [
10131
+ { re: /^\s*def\s+(?:self\.)?([A-Za-z_]\w*[!?]?)/gm, kind: "function" },
10132
+ { re: /^\s*class\s+([A-Za-z_]\w*)/gm, kind: "class" },
10133
+ { re: /^\s*module\s+([A-Za-z_]\w*)/gm, kind: "namespace" }
10134
+ ],
10135
+ swift: [
10136
+ { re: /\b(?:func)\s+([A-Za-z_]\w*)/g, kind: "function" },
10137
+ { re: /\b(?:class|struct|enum|protocol|actor)\s+([A-Za-z_]\w*)/g, kind: "class" }
10138
+ ],
10139
+ kotlin: [
10140
+ { re: /\b(?:fun)\s+([A-Za-z_]\w*)/g, kind: "function" },
10141
+ { re: /\b(?:class|interface|object|enum\s+class|data\s+class)\s+([A-Za-z_]\w*)/g, kind: "class" }
10142
+ ],
10143
+ scala: [
10144
+ { re: /\b(?:def)\s+([A-Za-z_]\w*)/g, kind: "function" },
10145
+ { re: /\b(?:class|object|trait|enum)\s+([A-Za-z_]\w*)/g, kind: "class" }
10146
+ ],
10147
+ shell: [
10148
+ { re: /^(?:function\s+)?([A-Za-z_]\w*)\s*\(\)\s*\{/gm, kind: "function" },
10149
+ { re: /^([A-Za-z_][\w]*)\s*\(\)\s*\{/gm, kind: "function" }
10150
+ ],
10151
+ sql: [
10152
+ { 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" }
10153
+ ],
10154
+ md: [
10155
+ { re: /^(#{1,6})\s+(.+)$/gm, kind: "namespace" }
10156
+ ],
10157
+ toml: [
10158
+ { re: /^\[([^\]]+)\]/gm, kind: "namespace" }
10159
+ ],
10160
+ html: [
10161
+ { re: /\bid\s*=\s*["']([^"']+)["']/gi, kind: "property" },
10162
+ { re: /<(?:script|template|style)\b/gi, kind: "namespace" }
10163
+ ],
10164
+ css: [
10165
+ { re: /^\s*([.#]?[A-Za-z_][\w-]*)\s*\{/gm, kind: "type" },
10166
+ { re: /@(?:keyframes|media|supports)\s+([^{\s]+)/g, kind: "namespace" }
10167
+ ],
10168
+ vue: [
10169
+ { re: /\b(?:function|const|let|var|class|export\s+(?:default\s+)?(?:function|class|const))\s+([A-Za-z_]\w*)/g, kind: "function" },
10170
+ { re: /<(?:script|template|style)\b/gi, kind: "namespace" }
10171
+ ],
10172
+ svelte: [
10173
+ { re: /\b(?:function|const|let|var|class|export\s+(?:default\s+)?(?:function|class|const))\s+([A-Za-z_]\w*)/g, kind: "function" }
10174
+ ],
10175
+ dart: [
10176
+ { re: /\b(?:class|enum|mixin|extension)\s+([A-Za-z_]\w*)/g, kind: "class" },
10177
+ { re: /\b([A-Za-z_]\w*)\s*\([^;]*\)\s*(?:async\s*)?\{/g, kind: "function" }
10178
+ ],
10179
+ lua: [
10180
+ { re: /\bfunction\s+([A-Za-z_.:]\w*)/g, kind: "function" },
10181
+ { re: /\blocal\s+function\s+([A-Za-z_]\w*)/g, kind: "function" }
10182
+ ],
10183
+ r: [
10184
+ { re: /([A-Za-z.]\w*)\s*<-\s*function\s*\(/g, kind: "function" },
10185
+ { re: /([A-Za-z.]\w*)\s*=\s*function\s*\(/g, kind: "function" }
10186
+ ],
10187
+ proto: [
10188
+ { re: /\b(?:message|service|enum)\s+([A-Za-z_]\w*)/g, kind: "type" },
10189
+ { re: /\brpc\s+([A-Za-z_]\w*)/g, kind: "function" }
10190
+ ],
10191
+ graphql: [
10192
+ { re: /\b(?:type|interface|enum|input|union|scalar)\s+([A-Za-z_]\w*)/g, kind: "type" },
10193
+ { re: /\b(?:query|mutation|subscription)\s+([A-Za-z_]\w*)/g, kind: "function" }
10194
+ ],
10195
+ zig: [
10196
+ { re: /\b(?:fn|pub\s+fn)\s+([A-Za-z_]\w*)/g, kind: "function" },
10197
+ { re: /\b(?:const|var)\s+([A-Za-z_]\w*)/g, kind: "const" },
10198
+ { re: /\b(?:struct|enum|union)\s*\{/g, kind: "type" }
10199
+ ],
10200
+ elixir: [
10201
+ { re: /\bdef(?:p|macro|macrop)?\s+([A-Za-z_]\w*[!?]?)/g, kind: "function" },
10202
+ { re: /\bdefmodule\s+([A-Za-z_.]\w*)/g, kind: "namespace" }
10203
+ ],
10204
+ haskell: [
10205
+ { re: /^([A-Za-z_]\w*)\s*::/gm, kind: "function" },
10206
+ { re: /\bdata\s+([A-Za-z_]\w*)/g, kind: "type" },
10207
+ { re: /\btype\s+(?:family\s+)?([A-Za-z_]\w*)/g, kind: "type" },
10208
+ { re: /\bclass\s+([A-Za-z_]\w*)/g, kind: "class" }
10209
+ ],
10210
+ other: [
10211
+ { re: /^(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_]\w*)/gm, kind: "function" },
10212
+ { re: /^(?:export\s+)?class\s+([A-Za-z_]\w*)/gm, kind: "class" },
10213
+ { re: /^(?:export\s+)?(?:const|let|var)\s+([A-Za-z_]\w*)/gm, kind: "const" },
10214
+ { re: /^(?:async\s+)?def\s+([A-Za-z_]\w*)/gm, kind: "function" },
10215
+ { re: /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)/gm, kind: "function" },
10216
+ { re: /\bfn\s+([A-Za-z_]\w*)/g, kind: "function" },
10217
+ { re: /^(?:target|PHONY)\s*:/gm, kind: "namespace" }
10218
+ ]
10219
+ };
10220
+ var KEYWORDS = /* @__PURE__ */ new Set([
10221
+ "if",
10222
+ "else",
10223
+ "for",
10224
+ "while",
10225
+ "switch",
10226
+ "case",
10227
+ "return",
10228
+ "break",
10229
+ "continue",
10230
+ "new",
10231
+ "delete",
10232
+ "typeof",
10233
+ "instanceof",
10234
+ "void",
10235
+ "null",
10236
+ "true",
10237
+ "false",
10238
+ "this",
10239
+ "super",
10240
+ "import",
10241
+ "export",
10242
+ "from",
10243
+ "as",
10244
+ "default",
10245
+ "public",
10246
+ "private",
10247
+ "protected",
10248
+ "static",
10249
+ "final",
10250
+ "class",
10251
+ "struct",
10252
+ "enum",
10253
+ "interface",
10254
+ "function",
10255
+ "def",
10256
+ "fn",
10257
+ "func",
10258
+ "var",
10259
+ "let",
10260
+ "const",
10261
+ "package",
10262
+ "namespace",
10263
+ "module",
10264
+ "using",
10265
+ "include",
10266
+ "require",
10267
+ "select",
10268
+ "from",
10269
+ "where",
10270
+ "and",
10271
+ "or",
10272
+ "not",
10273
+ "in",
10274
+ "is",
10275
+ "try",
10276
+ "catch",
10277
+ "finally",
10278
+ "throw",
10279
+ "async",
10280
+ "await",
10281
+ "yield"
10282
+ ]);
10283
+ function patternsFor(lang) {
10284
+ return LANG_PATTERNS[lang] ?? LANG_PATTERNS.other ?? [];
10285
+ }
10286
+ function looksBinary(content) {
10287
+ if (content.includes("\0")) return true;
10288
+ const sample = content.slice(0, 2048);
10289
+ if (sample.length === 0) return false;
10290
+ let bad = 0;
10291
+ for (let i = 0; i < sample.length; i++) {
10292
+ const c = sample.charCodeAt(i);
10293
+ if (c === 9 || c === 10 || c === 13) continue;
10294
+ if (c < 32 || c === 127) bad++;
10295
+ }
10296
+ return bad / sample.length > 0.1;
10297
+ }
10298
+ function lineColAt(content, index) {
10299
+ let line = 1;
10300
+ let lastNl = -1;
10301
+ for (let i = 0; i < index && i < content.length; i++) {
10302
+ if (content.charCodeAt(i) === 10) {
10303
+ line++;
10304
+ lastNl = i;
10305
+ }
10306
+ }
10307
+ return { line, col: index - lastNl };
10308
+ }
10309
+ var GENERIC_MAX_SYMBOLS_DEFAULT = 500;
10310
+ var GENERIC_MAX_FILE_CHARS = 512 * 1024;
10311
+ function parseGeneric2(opts) {
10312
+ const { file, lang } = opts;
10313
+ const maxSymbols = opts.maxSymbols ?? GENERIC_MAX_SYMBOLS_DEFAULT;
10314
+ const mtimeMs = Date.now();
10315
+ if (!opts.content || looksBinary(opts.content)) {
10316
+ return { file, lang, symbols: [], mtimeMs };
10317
+ }
10318
+ const content = opts.content.length > GENERIC_MAX_FILE_CHARS ? opts.content.slice(0, GENERIC_MAX_FILE_CHARS) : opts.content;
10319
+ const patterns = patternsFor(lang);
10320
+ const symbols = [];
10321
+ const seen = /* @__PURE__ */ new Set();
10322
+ for (const pattern of patterns) {
10323
+ const re = new RegExp(pattern.re.source, pattern.re.flags.includes("g") ? pattern.re.flags : `${pattern.re.flags}g`);
10324
+ re.lastIndex = 0;
10325
+ let match;
10326
+ while ((match = re.exec(content)) !== null) {
10327
+ if (symbols.length >= maxSymbols) break;
10328
+ let name = (match[1] ?? match[2] ?? "").trim();
10329
+ if (lang === "md" && match[2]) name = match[2].trim();
10330
+ if (!name || name.length > 200) continue;
10331
+ name = name.replace(/^#+\s*/, "").replace(/["'`]/g, "");
10332
+ if (!name || KEYWORDS.has(name.toLowerCase())) continue;
10333
+ if (!/^[A-Za-z_#.@/\w][\w.\-:/#!?]*$/.test(name) && lang !== "md" && lang !== "toml") {
10334
+ continue;
10335
+ }
10336
+ const { line, col } = lineColAt(content, match.index);
10337
+ const key = `${name}\0${line}\0${pattern.kind}`;
10338
+ if (seen.has(key)) continue;
10339
+ seen.add(key);
10340
+ const nl = content.indexOf("\n", match.index);
10341
+ const lineText = content.slice(match.index, nl === -1 ? content.length : nl);
10342
+ const signature = (lineText || name).trim().slice(0, 500);
10343
+ symbols.push({
10344
+ id: 0,
10345
+ lang,
10346
+ kind: lang === "md" ? "namespace" : pattern.kind,
10347
+ name: name.slice(0, 200),
10348
+ file,
10349
+ line,
10350
+ col,
10351
+ signature,
10352
+ docComment: "",
10353
+ scope: "",
10354
+ text: `${name} ${signature}`.trim().slice(0, 1e3)
10355
+ });
10356
+ }
10357
+ if (symbols.length >= maxSymbols) break;
10358
+ }
10359
+ return { file, lang, symbols, mtimeMs };
10360
+ }
9646
10361
  async function parseSymbols3(opts) {
10362
+ return parseGeneric2(opts);
10363
+ }
10364
+
10365
+ // src/codebase-index/py-parser.ts
10366
+ async function parseSymbols4(opts) {
9647
10367
  const { file, content, lang } = opts;
9648
10368
  try {
9649
- return await syncPyParse(file, content, lang);
10369
+ const native = await withSpawnGate(() => syncPyParse(file, content, lang));
10370
+ if (native !== null) return native;
9650
10371
  } catch {
9651
- return { file, lang, symbols: [], mtimeMs: Date.now() };
9652
10372
  }
10373
+ return parseGeneric2({ file, content, lang: lang === "py" ? "py" : lang });
9653
10374
  }
9654
10375
  var PY_PARSE_SCRIPT = `import ast, json, sys, os
9655
10376
 
@@ -9854,20 +10575,37 @@ visitor.visit(tree)
9854
10575
 
9855
10576
  print(json.dumps([s.to_dict() for s in syms]))
9856
10577
  `;
9857
- function resolvePython() {
10578
+ async function resolvePython() {
9858
10579
  const candidates = process.platform === "win32" ? ["python3", "python", "py"] : ["python3", "python"];
9859
10580
  for (const name of candidates) {
9860
10581
  const resolved = resolveWin32Command(name);
9861
- const result = spawnSync(resolved, ["--version"], {
9862
- stdio: "pipe",
9863
- timeout: 5e3
9864
- });
9865
- if (result.error) continue;
9866
- if (result.status !== 0) continue;
10582
+ if (!await commandIsAvailable(resolved)) continue;
9867
10583
  return resolved;
9868
10584
  }
9869
10585
  return null;
9870
10586
  }
10587
+ function commandIsAvailable(command) {
10588
+ return new Promise((resolve14) => {
10589
+ let settled = false;
10590
+ const proc = spawn5(command, ["--version"], {
10591
+ stdio: "ignore",
10592
+ windowsHide: true
10593
+ });
10594
+ const finish = (available) => {
10595
+ if (settled) return;
10596
+ settled = true;
10597
+ clearTimeout(timer);
10598
+ resolve14(available);
10599
+ };
10600
+ const timer = setTimeout(() => {
10601
+ proc.kill("SIGKILL");
10602
+ finish(false);
10603
+ }, 5e3);
10604
+ timer.unref?.();
10605
+ proc.once("error", () => finish(false));
10606
+ proc.once("close", (code) => finish(code === 0));
10607
+ });
10608
+ }
9871
10609
  function spawnPyParser(pyBinary, scriptPath, filePath, content) {
9872
10610
  return new Promise((resolve14, reject) => {
9873
10611
  let settled = false;
@@ -9903,21 +10641,20 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
9903
10641
  });
9904
10642
  }
9905
10643
  var _cachedScriptPath = null;
9906
- var _cachedPyBinary = null;
10644
+ var cachedPyBinary;
9907
10645
  async function syncPyParse(filePath, content, lang) {
9908
10646
  try {
9909
10647
  if (!_cachedScriptPath) {
9910
- const tmpDir = path15.join(os6.tmpdir(), "ws-py-parse");
10648
+ const tmpDir = path16.join(os6.tmpdir(), "ws-py-parse");
9911
10649
  await fs11.mkdir(tmpDir, { recursive: true });
9912
- _cachedScriptPath = path15.join(tmpDir, "parse.py");
10650
+ _cachedScriptPath = path16.join(tmpDir, "parse.py");
9913
10651
  await fs11.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
9914
10652
  }
9915
- if (!_cachedPyBinary) {
9916
- _cachedPyBinary = resolvePython();
9917
- if (!_cachedPyBinary) return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
9918
- }
10653
+ cachedPyBinary ??= resolvePython();
10654
+ const pyBinary = await cachedPyBinary;
10655
+ if (!pyBinary) return null;
9919
10656
  const { code, stdout } = await spawnPyParser(
9920
- _cachedPyBinary,
10657
+ pyBinary,
9921
10658
  _cachedScriptPath,
9922
10659
  filePath,
9923
10660
  content
@@ -9941,31 +10678,40 @@ async function syncPyParse(filePath, content, lang) {
9941
10678
  }));
9942
10679
  return { file: filePath, lang, symbols, mtimeMs: Date.now() };
9943
10680
  } catch {
9944
- return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
10681
+ return null;
9945
10682
  }
9946
10683
  }
9947
10684
 
9948
10685
  // src/codebase-index/rs-parser.ts
9949
10686
  init_win32_resolve();
9950
- import { expectDefined as expectDefined3 } from "@wrongstack/core";
9951
- import { execFileSync, spawn as spawn6 } from "node:child_process";
10687
+ import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
10688
+ import { execFile, spawn as spawn6 } from "node:child_process";
9952
10689
  import * as fs12 from "node:fs/promises";
9953
- import * as path16 from "node:path";
9954
- async function parseSymbols4(opts) {
10690
+ import * as path17 from "node:path";
10691
+ async function parseSymbols5(opts) {
9955
10692
  const { file, content, lang } = opts;
9956
- const nativeAvailable = checkNativeParser();
10693
+ const nativeAvailable = await checkNativeParser();
9957
10694
  if (nativeAvailable) {
9958
- const result = await tryNativeParse(file, content);
10695
+ const result = await withSpawnGate(() => tryNativeParse(file, content));
9959
10696
  if (result) return result;
9960
10697
  }
9961
10698
  return regexParse({ file, content, lang });
9962
10699
  }
10700
+ var nativeParserAvailability;
10701
+ function probe(command, args) {
10702
+ return new Promise((resolve14, reject) => {
10703
+ execFile(command, args, { timeout: 1e4, windowsHide: true }, (error) => {
10704
+ if (error) reject(error);
10705
+ else resolve14();
10706
+ });
10707
+ });
10708
+ }
9963
10709
  function checkNativeParser() {
9964
- try {
9965
- execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
9966
- const toolsDir = path16.join(process.cwd(), "tools");
10710
+ nativeParserAvailability ??= (async () => {
9967
10711
  try {
9968
- execFileSync(
10712
+ await probe("rustc", ["--version"]);
10713
+ const toolsDir = path17.join(process.cwd(), "tools");
10714
+ await probe(
9969
10715
  "cargo",
9970
10716
  [
9971
10717
  "metadata",
@@ -9973,23 +10719,21 @@ function checkNativeParser() {
9973
10719
  "--format-version",
9974
10720
  "1",
9975
10721
  "--manifest-path",
9976
- path16.join(toolsDir, "Cargo.toml")
9977
- ],
9978
- { stdio: "pipe", windowsHide: true }
10722
+ path17.join(toolsDir, "Cargo.toml")
10723
+ ]
9979
10724
  );
9980
10725
  return true;
9981
10726
  } catch {
9982
10727
  return false;
9983
10728
  }
9984
- } catch {
9985
- return false;
9986
- }
10729
+ })();
10730
+ return nativeParserAvailability;
9987
10731
  }
9988
10732
  async function tryNativeParse(file, content) {
9989
10733
  try {
9990
- const toolsDir = path16.join(process.cwd(), "tools");
9991
- const crateDir = path16.join(toolsDir, "syn-parser");
9992
- const tmpFile = path16.join(crateDir, "src", "input.rs");
10734
+ const toolsDir = path17.join(process.cwd(), "tools");
10735
+ const crateDir = path17.join(toolsDir, "syn-parser");
10736
+ const tmpFile = path17.join(crateDir, "src", "input.rs");
9993
10737
  await fs12.writeFile(tmpFile, content, "utf8");
9994
10738
  const cargoBinary = resolveWin32Command("cargo");
9995
10739
  const result = await new Promise(
@@ -9997,7 +10741,7 @@ async function tryNativeParse(file, content) {
9997
10741
  let settled = false;
9998
10742
  const proc = spawn6(
9999
10743
  cargoBinary,
10000
- ["run", "--manifest-path", path16.join(toolsDir, "Cargo.toml")],
10744
+ ["run", "--manifest-path", path17.join(toolsDir, "Cargo.toml")],
10001
10745
  {
10002
10746
  cwd: process.cwd(),
10003
10747
  stdio: ["pipe", "pipe", "pipe"],
@@ -10111,9 +10855,9 @@ function regexParse(opts) {
10111
10855
  }
10112
10856
 
10113
10857
  // src/codebase-index/json-parser.ts
10114
- import { expectDefined as expectDefined4 } from "@wrongstack/core";
10115
- import * as path17 from "node:path";
10116
- function parseSymbols5(opts) {
10858
+ import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
10859
+ import * as path18 from "node:path";
10860
+ function parseSymbols6(opts) {
10117
10861
  const { file, content, lang } = opts;
10118
10862
  try {
10119
10863
  return regexParse2({ file, content, lang });
@@ -10124,9 +10868,9 @@ function parseSymbols5(opts) {
10124
10868
  function regexParse2(opts) {
10125
10869
  const { file, content, lang } = opts;
10126
10870
  const symbols = [];
10127
- const basename10 = path17.basename(file).toLowerCase();
10128
- const isPackageJson = basename10 === "package.json";
10129
- const isTsconfig = basename10 === "tsconfig.json" || basename10 === "tsconfig.build.json";
10871
+ const basename11 = path18.basename(file).toLowerCase();
10872
+ const isPackageJson = basename11 === "package.json";
10873
+ const isTsconfig = basename11 === "tsconfig.json" || basename11 === "tsconfig.build.json";
10130
10874
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
10131
10875
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
10132
10876
  const lines = content.split("\n");
@@ -10150,11 +10894,11 @@ function regexParse2(opts) {
10150
10894
  const line = lineFromOffset(offset);
10151
10895
  symbols.push(
10152
10896
  makeSymbol({
10153
- name: path17.basename(file),
10897
+ name: path18.basename(file),
10154
10898
  kind: "object",
10155
10899
  line,
10156
10900
  col: 0,
10157
- signature: `"${path17.basename(file)}" = { ... }`,
10901
+ signature: `"${path18.basename(file)}" = { ... }`,
10158
10902
  file,
10159
10903
  lang
10160
10904
  })
@@ -10316,8 +11060,8 @@ function makeSymbol(opts) {
10316
11060
  }
10317
11061
 
10318
11062
  // src/codebase-index/yaml-parser.ts
10319
- import { expectDefined as expectDefined5, truncate } from "@wrongstack/core";
10320
- function parseSymbols6(opts) {
11063
+ import { expectDefined as expectDefined5, truncate } from "@wrongstack/core/utils";
11064
+ function parseSymbols7(opts) {
10321
11065
  const { file, content, lang } = opts;
10322
11066
  try {
10323
11067
  return regexParse3({ file, content, lang });
@@ -10466,8 +11210,8 @@ function makeSymbol2(opts) {
10466
11210
 
10467
11211
  // src/codebase-index/gitignore.ts
10468
11212
  import * as fs13 from "node:fs/promises";
10469
- import * as path18 from "node:path";
10470
- import { compileGlob } from "@wrongstack/core";
11213
+ import * as path19 from "node:path";
11214
+ import { compileGlob } from "@wrongstack/core/utils";
10471
11215
  function globBody(glob) {
10472
11216
  return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
10473
11217
  }
@@ -10512,7 +11256,7 @@ function compileGitignore(lines) {
10512
11256
  async function loadGitignoreMatcher(projectRoot) {
10513
11257
  let lines = [];
10514
11258
  try {
10515
- const raw = await fs13.readFile(path18.join(projectRoot, ".gitignore"), "utf8");
11259
+ const raw = await fs13.readFile(path19.join(projectRoot, ".gitignore"), "utf8");
10516
11260
  lines = raw.split("\n");
10517
11261
  } catch {
10518
11262
  }
@@ -10521,7 +11265,9 @@ async function loadGitignoreMatcher(projectRoot) {
10521
11265
 
10522
11266
  // src/codebase-index/indexer.ts
10523
11267
  var YIELD_EVERY_N = 50;
10524
- var PARALLEL_BATCH = Math.min(availableParallelism() * 4, 40);
11268
+ function resolveParallelBatch() {
11269
+ return indexParallelBatchSize(availableParallelism());
11270
+ }
10525
11271
  function yieldEventLoop() {
10526
11272
  return new Promise((resolve14) => setImmediate(resolve14));
10527
11273
  }
@@ -10547,8 +11293,8 @@ var DEFAULT_IGNORE = [
10547
11293
  var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
10548
11294
  var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
10549
11295
  function isWithinProject(projectRoot, file) {
10550
- const rel = path19.relative(projectRoot, file);
10551
- return rel !== "" && !rel.startsWith(`..${path19.sep}`) && rel !== ".." && !path19.isAbsolute(rel);
11296
+ const rel = path20.relative(projectRoot, file);
11297
+ return rel !== "" && !rel.startsWith(`..${path20.sep}`) && rel !== ".." && !path20.isAbsolute(rel);
10552
11298
  }
10553
11299
  function isMissingPathError(err) {
10554
11300
  const code = err?.code;
@@ -10559,18 +11305,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10559
11305
  const errors = [];
10560
11306
  let complete = true;
10561
11307
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
10562
- const globs = [
10563
- { ext: ".ts", pat: compileGlob2("**/*.ts") },
10564
- { ext: ".tsx", pat: compileGlob2("**/*.tsx") },
10565
- { ext: ".js", pat: compileGlob2("**/*.js") },
10566
- { ext: ".jsx", pat: compileGlob2("**/*.jsx") },
10567
- { ext: ".go", pat: compileGlob2("**/*.go") },
10568
- { ext: ".py", pat: compileGlob2("**/*.py") },
10569
- { ext: ".rs", pat: compileGlob2("**/*.rs") },
10570
- { ext: ".json", pat: compileGlob2("**/*.json") },
10571
- { ext: ".yaml", pat: compileGlob2("**/*.yaml") },
10572
- { ext: ".yml", pat: compileGlob2("**/*.yml") }
10573
- ];
11308
+ const indexableExts = new Set(INDEXABLE_EXTENSIONS);
10574
11309
  let dirCount = 0;
10575
11310
  const walk = async (dir) => {
10576
11311
  throwIfAborted(signal);
@@ -10589,19 +11324,16 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10589
11324
  dirCount++;
10590
11325
  for (const e of entries) {
10591
11326
  if (ignoreSet.has(e.name)) continue;
10592
- const full = path19.join(dir, e.name);
10593
- const rel = path19.relative(projectRoot, full).replace(/\\/g, "/");
11327
+ const full = path20.join(dir, e.name);
11328
+ const rel = path20.relative(projectRoot, full).replace(/\\/g, "/");
10594
11329
  if (e.isDirectory()) {
10595
11330
  if (isGitIgnored(rel, true)) continue;
10596
11331
  await walk(full);
10597
11332
  } else if (e.isFile()) {
10598
11333
  if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
10599
- const ext = path19.extname(e.name).toLowerCase();
10600
- for (const { ext: extName, pat } of globs) {
10601
- if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
10602
- results.push(full);
10603
- break;
10604
- }
11334
+ const ext = path20.extname(e.name).toLowerCase();
11335
+ if (indexableExts.has(ext) || detectLang(full) !== null) {
11336
+ results.push(full);
10605
11337
  }
10606
11338
  }
10607
11339
  }
@@ -10610,24 +11342,25 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10610
11342
  return { files: results, complete, errors };
10611
11343
  }
10612
11344
  async function parseFile(file, content, lang) {
10613
- switch (lang) {
11345
+ const symbolLang = lang;
11346
+ switch (symbolLang) {
10614
11347
  case "ts":
10615
11348
  case "tsx":
10616
11349
  case "js":
10617
11350
  case "jsx":
10618
- return parseSymbols({ file, content, lang });
11351
+ return parseSymbols({ file, content, lang: symbolLang });
10619
11352
  case "go":
10620
11353
  return parseSymbols2({ file, content, lang: "go" });
10621
11354
  case "py":
10622
- return parseSymbols3({ file, content, lang: "py" });
11355
+ return parseSymbols4({ file, content, lang: "py" });
10623
11356
  case "rs":
10624
- return parseSymbols4({ file, content, lang: "rs" });
11357
+ return parseSymbols5({ file, content, lang: "rs" });
10625
11358
  case "json":
10626
- return parseSymbols5({ file, content, lang: "json" });
11359
+ return parseSymbols6({ file, content, lang: "json" });
10627
11360
  case "yaml":
10628
- return parseSymbols6({ file, content, lang: "yaml" });
11361
+ return parseSymbols7({ file, content, lang: "yaml" });
10629
11362
  default:
10630
- return { file, lang, symbols: [], mtimeMs: Date.now() };
11363
+ return parseSymbols3({ file, content, lang: symbolLang });
10631
11364
  }
10632
11365
  }
10633
11366
  function assignRefsToSymbols2(refs, symbols) {
@@ -10675,10 +11408,10 @@ async function runIndexerWithStore(store, opts) {
10675
11408
  let discoveredFiles = null;
10676
11409
  let discoveryComplete = true;
10677
11410
  if (opts.files && opts.files.length > 0) {
10678
- files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => {
11411
+ files = opts.files.map((f) => path20.resolve(projectRoot, f)).filter((f) => {
10679
11412
  if (!isWithinProject(projectRoot, f)) return false;
10680
- const rel = path19.relative(projectRoot, f).replace(/\\/g, "/");
10681
- return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path19.basename(f)) && !isGitIgnored(rel, false);
11413
+ const rel = path20.relative(projectRoot, f).replace(/\\/g, "/");
11414
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path20.basename(f)) && !isGitIgnored(rel, false);
10682
11415
  });
10683
11416
  } else {
10684
11417
  const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
@@ -10699,15 +11432,19 @@ async function runIndexerWithStore(store, opts) {
10699
11432
  if (!force) {
10700
11433
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
10701
11434
  }
11435
+ const parallelBatch = resolveParallelBatch();
10702
11436
  let filesSinceLastYield = 0;
10703
- for (let batchStart = 0; batchStart < files.length; batchStart += PARALLEL_BATCH) {
10704
- const batchEnd = Math.min(batchStart + PARALLEL_BATCH, files.length);
11437
+ for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
11438
+ const batchEnd = Math.min(batchStart + parallelBatch, files.length);
10705
11439
  const batchFiles = files.slice(batchStart, batchEnd);
10706
11440
  opts.onProgress?.(batchEnd, files.length);
10707
11441
  filesSinceLastYield += batchFiles.length;
10708
11442
  if (filesSinceLastYield >= YIELD_EVERY_N) {
10709
11443
  filesSinceLastYield = 0;
10710
11444
  await yieldEventLoop();
11445
+ if (isFrugalPerf()) {
11446
+ await new Promise((r) => setTimeout(r, 8));
11447
+ }
10711
11448
  throwIfAborted(signal);
10712
11449
  }
10713
11450
  const statOpts = signal ? { signal } : {};
@@ -10811,9 +11548,7 @@ async function runIndexerWithStore(store, opts) {
10811
11548
  continue;
10812
11549
  }
10813
11550
  if (parsed.symbols.length === 0) {
10814
- store.deleteRefsForFile(file);
10815
- store.deleteSymbolsForFile(file);
10816
- store.upsertFile({
11551
+ store.replaceEmptyFile({
10817
11552
  file,
10818
11553
  lang,
10819
11554
  mtimeMs: Math.floor(stat17.mtimeMs),
@@ -10882,6 +11617,7 @@ async function runIndexerWithStore(store, opts) {
10882
11617
  }
10883
11618
  store.resolveRefs();
10884
11619
  store.setMetadata("relation_graph_version", relationGraphVersion);
11620
+ store.optimize();
10885
11621
  const durationMs = Date.now() - startMs;
10886
11622
  store.setLastIndexed(Date.now());
10887
11623
  return {
@@ -11124,10 +11860,10 @@ async function callInline(op, args, opts) {
11124
11860
  opts.signal?.removeEventListener("abort", onOuterAbort);
11125
11861
  }
11126
11862
  }
11127
- var chain = Promise.resolve();
11863
+ var chain2 = Promise.resolve();
11128
11864
  function withMutex(job) {
11129
- const run = chain.then(job, job);
11130
- chain = run.then(
11865
+ const run = chain2.then(job, job);
11866
+ chain2 = run.then(
11131
11867
  () => void 0,
11132
11868
  () => void 0
11133
11869
  );
@@ -11476,7 +12212,7 @@ var codebaseStatsTool = {
11476
12212
 
11477
12213
  // src/design.ts
11478
12214
  import * as fs16 from "node:fs/promises";
11479
- import * as path20 from "node:path";
12215
+ import * as path21 from "node:path";
11480
12216
  import {
11481
12217
  applyTokenOverrides,
11482
12218
  getDesignKitLoader,
@@ -11485,23 +12221,24 @@ import {
11485
12221
  materializeTokens,
11486
12222
  recordKitChoice,
11487
12223
  recordOverrides,
12224
+ resolveSemanticTune,
11488
12225
  runDesignVerify,
11489
12226
  setActiveKit,
11490
12227
  setDesignOverrides
11491
- } from "@wrongstack/core";
12228
+ } from "@wrongstack/core/design";
11492
12229
  async function resolveReal(p) {
11493
- const resolved = path20.resolve(p);
11494
- let probe = resolved;
12230
+ const resolved = path21.resolve(p);
12231
+ let probe2 = resolved;
11495
12232
  const missing = [];
11496
12233
  for (; ; ) {
11497
12234
  try {
11498
- return path20.resolve(await fs16.realpath(probe), ...missing);
12235
+ return path21.resolve(await fs16.realpath(probe2), ...missing);
11499
12236
  } catch (err) {
11500
12237
  if (err.code === "ENOENT") {
11501
- const parent = path20.dirname(probe);
11502
- if (parent === probe) return resolved;
11503
- missing.unshift(path20.basename(probe));
11504
- probe = parent;
12238
+ const parent = path21.dirname(probe2);
12239
+ if (parent === probe2) return resolved;
12240
+ missing.unshift(path21.basename(probe2));
12241
+ probe2 = parent;
11505
12242
  continue;
11506
12243
  }
11507
12244
  return resolved;
@@ -11531,8 +12268,8 @@ var designTool = {
11531
12268
  properties: {
11532
12269
  action: {
11533
12270
  type: "string",
11534
- enum: ["list", "use", "foundations", "set", "materialize", "verify"],
11535
- 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."
12271
+ enum: ["list", "use", "foundations", "set", "tune", "materialize", "verify"],
12272
+ 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."
11536
12273
  },
11537
12274
  kit: {
11538
12275
  type: "string",
@@ -11548,6 +12285,16 @@ var designTool = {
11548
12285
  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.',
11549
12286
  additionalProperties: { type: "string" }
11550
12287
  },
12288
+ tune: {
12289
+ type: "object",
12290
+ 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.',
12291
+ properties: {
12292
+ radius: { type: "string" },
12293
+ density: { type: "string" },
12294
+ font: { type: "string" },
12295
+ motion: { type: "string" }
12296
+ }
12297
+ },
11551
12298
  out: {
11552
12299
  type: "string",
11553
12300
  description: "Materialize output path (project-relative). Defaults to a per-stack convention."
@@ -11645,6 +12392,28 @@ ${body}`
11645
12392
  These win over kit tokens. Run \`design {action:"materialize"}\` to write them to a theme file.`
11646
12393
  };
11647
12394
  }
12395
+ if (action === "tune") {
12396
+ const patch = resolveSemanticTune(input.tune ?? {});
12397
+ if (Object.keys(patch).length === 0) {
12398
+ return {
12399
+ action,
12400
+ output: 'No recognized knobs. Pass tune:{ radius:"lg", density:"compact", font:"\u2026", motion:"snappy" }.'
12401
+ };
12402
+ }
12403
+ const merged = await recordOverrides(ctx.projectRoot, patch, (/* @__PURE__ */ new Date()).toISOString());
12404
+ if (!merged) {
12405
+ return {
12406
+ action,
12407
+ output: 'No active kit. Pick one first: `design {action:"use", kit:"<id>"}`.'
12408
+ };
12409
+ }
12410
+ setDesignOverrides(ctx, merged);
12411
+ return {
12412
+ action,
12413
+ output: `Tuned. Resolved ${Object.keys(patch).length} token override(s): ${Object.entries(patch).map(([k, v]) => `${k}=${v}`).join(", ")}
12414
+ Run \`design {action:"materialize"}\` to write the tuned tokens to your theme file.`
12415
+ };
12416
+ }
11648
12417
  if (action === "materialize") {
11649
12418
  const active = await loadActiveKit(ctx.projectRoot);
11650
12419
  if (!active) {
@@ -11666,11 +12435,11 @@ These win over kit tokens. Run \`design {action:"materialize"}\` to write them t
11666
12435
  outPath: input.out
11667
12436
  });
11668
12437
  const root = await resolveReal(ctx.projectRoot);
11669
- const absResolved = path20.isAbsolute(result.path) ? path20.resolve(result.path) : path20.resolve(path20.join(ctx.projectRoot, result.path));
11670
- const absParent = await resolveReal(path20.dirname(absResolved));
11671
- const abs = path20.join(absParent, path20.basename(absResolved));
11672
- const rel = path20.relative(root, abs);
11673
- if (rel.startsWith("..") || path20.isAbsolute(rel)) {
12438
+ const absResolved = path21.isAbsolute(result.path) ? path21.resolve(result.path) : path21.resolve(path21.join(ctx.projectRoot, result.path));
12439
+ const absParent = await resolveReal(path21.dirname(absResolved));
12440
+ const abs = path21.join(absParent, path21.basename(absResolved));
12441
+ const rel = path21.relative(root, abs);
12442
+ if (rel.startsWith("..") || path21.isAbsolute(rel)) {
11674
12443
  throw new Error(
11675
12444
  `design: materialize path "${result.path}" would escape the project root`
11676
12445
  );
@@ -11694,7 +12463,7 @@ ${result.content}
11694
12463
  \`\`\``
11695
12464
  };
11696
12465
  }
11697
- await fs16.mkdir(path20.dirname(abs), { recursive: true });
12466
+ await fs16.mkdir(path21.dirname(abs), { recursive: true });
11698
12467
  await fs16.writeFile(abs, result.content);
11699
12468
  return {
11700
12469
  action,
@@ -11746,8 +12515,8 @@ init_util();
11746
12515
  import { spawn as spawn7 } from "node:child_process";
11747
12516
  import { statSync as statSync2 } from "node:fs";
11748
12517
  import * as fs17 from "node:fs/promises";
11749
- import * as path21 from "node:path";
11750
- import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core";
12518
+ import * as path22 from "node:path";
12519
+ import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core/utils";
11751
12520
  var diffTool = {
11752
12521
  name: "diff",
11753
12522
  category: "Filesystem",
@@ -11830,11 +12599,11 @@ function findGitDir(cwd) {
11830
12599
  let dir = cwd;
11831
12600
  for (let i = 0; i < 20; i++) {
11832
12601
  try {
11833
- const stat17 = statSync2(path21.join(dir, ".git"));
12602
+ const stat17 = statSync2(path22.join(dir, ".git"));
11834
12603
  if (stat17.isDirectory()) return dir;
11835
12604
  } catch {
11836
12605
  }
11837
- const parent = path21.dirname(dir);
12606
+ const parent = path22.dirname(dir);
11838
12607
  if (parent === dir) break;
11839
12608
  dir = parent;
11840
12609
  }
@@ -12048,7 +12817,7 @@ function processFile(content, absPath, _style, _overwrite, target) {
12048
12817
  // src/e2e.ts
12049
12818
  init_util();
12050
12819
  import { open, readdir as readdir5 } from "node:fs/promises";
12051
- import * as path22 from "node:path";
12820
+ import * as path23 from "node:path";
12052
12821
  async function readBoundedText(filePath, maxBytes) {
12053
12822
  let handle;
12054
12823
  try {
@@ -12098,8 +12867,8 @@ var MAX_PACKAGE_BYTES = 512 * 1024;
12098
12867
  var MAX_CONFIG_BYTES = 512 * 1024;
12099
12868
  var MAX_SPEC_SAMPLES = 20;
12100
12869
  function relativePath(root, target) {
12101
- const value = path22.relative(root, target) || ".";
12102
- return value.split(path22.sep).join("/");
12870
+ const value = path23.relative(root, target) || ".";
12871
+ return value.split(path23.sep).join("/");
12103
12872
  }
12104
12873
  function escapeRegExp(value) {
12105
12874
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -12171,7 +12940,7 @@ async function scanWorkspace(root, maxDepth, signal) {
12171
12940
  }
12172
12941
  for (const entry of entries) {
12173
12942
  signal.throwIfAborted();
12174
- const absolutePath = path22.join(current.directory, entry.name);
12943
+ const absolutePath = path23.join(current.directory, entry.name);
12175
12944
  if (entry.isFile()) {
12176
12945
  if (entry.name === "package.json") result.packageFiles.push(absolutePath);
12177
12946
  const framework = CONFIG_NAMES.get(entry.name);
@@ -12232,9 +13001,9 @@ async function detectPackageManager3(projectRoot, scanRoot, declared) {
12232
13001
  if (names.has("bun.lock") || names.has("bun.lockb")) return "bun";
12233
13002
  if (names.has("package-lock.json") || names.has("npm-shrinkwrap.json")) return "npm";
12234
13003
  if (directory === scanRoot) break;
12235
- const parent = path22.dirname(directory);
12236
- const relativeParent = path22.relative(scanRoot, parent);
12237
- if (parent === directory || relativeParent.startsWith("..") || path22.isAbsolute(relativeParent)) {
13004
+ const parent = path23.dirname(directory);
13005
+ const relativeParent = path23.relative(scanRoot, parent);
13006
+ if (parent === directory || relativeParent.startsWith("..") || path23.isAbsolute(relativeParent)) {
12238
13007
  break;
12239
13008
  }
12240
13009
  directory = parent;
@@ -12279,7 +13048,7 @@ function isSpec(framework, filename) {
12279
13048
  return /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/i.test(filename);
12280
13049
  }
12281
13050
  async function collectSpecs(root, framework, testDirectory, signal) {
12282
- const roots = testDirectory ? [path22.resolve(root, testDirectory)] : framework === "cypress" ? [path22.join(root, "cypress", "e2e"), path22.join(root, "cypress", "integration")] : [path22.join(root, "tests")];
13051
+ const roots = testDirectory ? [path23.resolve(root, testDirectory)] : framework === "cypress" ? [path23.join(root, "cypress", "e2e"), path23.join(root, "cypress", "integration")] : [path23.join(root, "tests")];
12283
13052
  const samples = [];
12284
13053
  let count = 0;
12285
13054
  let scanned = 0;
@@ -12298,7 +13067,7 @@ async function collectSpecs(root, framework, testDirectory, signal) {
12298
13067
  }
12299
13068
  for (const entry of entries) {
12300
13069
  signal.throwIfAborted();
12301
- const target = path22.join(directory, entry.name);
13070
+ const target = path23.join(directory, entry.name);
12302
13071
  if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) queue.push(target);
12303
13072
  else if (entry.isFile() && isSpec(framework, entry.name)) {
12304
13073
  count += 1;
@@ -12318,7 +13087,7 @@ function nearestPackage(projectRoot, packagesByDirectory, scanRoot) {
12318
13087
  const found = packagesByDirectory.get(directory);
12319
13088
  if (found) return found;
12320
13089
  if (directory === scanRoot) return void 0;
12321
- const parent = path22.dirname(directory);
13090
+ const parent = path23.dirname(directory);
12322
13091
  if (parent === directory || relativePath(scanRoot, parent).startsWith("..")) return void 0;
12323
13092
  directory = parent;
12324
13093
  }
@@ -12329,13 +13098,13 @@ async function discoverE2EProjects(root, options) {
12329
13098
  const packages = (await Promise.all(scan.packageFiles.map(readPackageInfo))).filter(
12330
13099
  (info) => Boolean(info)
12331
13100
  );
12332
- const packagesByDirectory = new Map(packages.map((info) => [path22.dirname(info.path), info]));
13101
+ const packagesByDirectory = new Map(packages.map((info) => [path23.dirname(info.path), info]));
12333
13102
  const candidates = /* @__PURE__ */ new Map();
12334
13103
  for (const config of scan.configs) {
12335
13104
  if (options.framework && options.framework !== "all" && config.framework !== options.framework) {
12336
13105
  continue;
12337
13106
  }
12338
- const projectRoot = path22.dirname(config.absolutePath);
13107
+ const projectRoot = path23.dirname(config.absolutePath);
12339
13108
  candidates.set(`${config.framework}:${projectRoot}`, {
12340
13109
  framework: config.framework,
12341
13110
  root: projectRoot,
@@ -12343,7 +13112,7 @@ async function discoverE2EProjects(root, options) {
12343
13112
  });
12344
13113
  }
12345
13114
  for (const info of packages) {
12346
- const projectRoot = path22.dirname(info.path);
13115
+ const projectRoot = path23.dirname(info.path);
12347
13116
  for (const framework of frameworkFromPackage(info)) {
12348
13117
  if (options.framework && options.framework !== "all" && framework !== options.framework)
12349
13118
  continue;
@@ -12359,10 +13128,10 @@ async function discoverE2EProjects(root, options) {
12359
13128
  const scripts = matchingScripts(info, candidate.framework);
12360
13129
  const manager = await detectPackageManager3(candidate.root, root, info?.packageManager);
12361
13130
  const testDirectory = candidate.framework === "playwright" ? staticString(source, "testDir") : void 0;
12362
- const resolvedTestDirectory = testDirectory ? path22.resolve(candidate.root, testDirectory) : void 0;
12363
- const relativeTestDirectory = resolvedTestDirectory ? path22.relative(root, resolvedTestDirectory) : void 0;
13131
+ const resolvedTestDirectory = testDirectory ? path23.resolve(candidate.root, testDirectory) : void 0;
13132
+ const relativeTestDirectory = resolvedTestDirectory ? path23.relative(root, resolvedTestDirectory) : void 0;
12364
13133
  const unsafeTestDirectory = Boolean(
12365
- relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path22.isAbsolute(relativeTestDirectory))
13134
+ relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path23.isAbsolute(relativeTestDirectory))
12366
13135
  );
12367
13136
  const specs = options.includeSpecs === false || unsafeTestDirectory ? { count: 0, samples: [], truncated: false } : await collectSpecs(candidate.root, candidate.framework, testDirectory, options.signal);
12368
13137
  const warnings = [];
@@ -12467,10 +13236,10 @@ import {
12467
13236
  atomicWrite as atomicWrite2,
12468
13237
  detectNewlineStyle,
12469
13238
  normalizeToLf,
12470
- ToolValidationError,
12471
13239
  toStyle,
12472
13240
  unifiedDiff
12473
- } from "@wrongstack/core";
13241
+ } from "@wrongstack/core/utils";
13242
+ import { ToolValidationError } from "@wrongstack/core/types";
12474
13243
 
12475
13244
  // src/_edit-match.ts
12476
13245
  var TIER_LABEL = {
@@ -12698,7 +13467,7 @@ function prefixSimilarity(a, b) {
12698
13467
  }
12699
13468
 
12700
13469
  // src/_syntax-check.ts
12701
- import * as path23 from "node:path";
13470
+ import * as path24 from "node:path";
12702
13471
  var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
12703
13472
  var MAX_CHECK_CHARS = 15e5;
12704
13473
  var MAX_ERRORS = 5;
@@ -12722,15 +13491,15 @@ async function checkSyntax(filePath, content, previousContent) {
12722
13491
  return { errors, preExisting };
12723
13492
  }
12724
13493
  function isJsoncFile(filePath) {
12725
- const base = path23.basename(filePath).toLowerCase();
13494
+ const base = path24.basename(filePath).toLowerCase();
12726
13495
  if (base.endsWith(".jsonc")) return true;
12727
13496
  if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
12728
- const dir = path23.basename(path23.dirname(filePath)).toLowerCase();
13497
+ const dir = path24.basename(path24.dirname(filePath)).toLowerCase();
12729
13498
  return dir === ".vscode";
12730
13499
  }
12731
13500
  async function parseErrors(filePath, content) {
12732
13501
  if (content.length > MAX_CHECK_CHARS) return void 0;
12733
- const ext = path23.extname(filePath).toLowerCase();
13502
+ const ext = path24.extname(filePath).toLowerCase();
12734
13503
  if (ext === ".json" || ext === ".jsonc") {
12735
13504
  try {
12736
13505
  JSON.parse(content);
@@ -12757,7 +13526,7 @@ async function parseErrors(filePath, content) {
12757
13526
  ts2.ScriptKind.JSX
12758
13527
  );
12759
13528
  const sourceFile = ts2.createSourceFile(
12760
- path23.basename(filePath),
13529
+ path24.basename(filePath),
12761
13530
  content,
12762
13531
  ts2.ScriptTarget.Latest,
12763
13532
  /* setParentNodes */
@@ -12992,7 +13761,7 @@ import {
12992
13761
  emitProcessCompleted as emitProcessCompleted3,
12993
13762
  emitProcessOutput as emitProcessOutput3,
12994
13763
  emitProcessStarted as emitProcessStarted3
12995
- } from "@wrongstack/core";
13764
+ } from "@wrongstack/core/observability";
12996
13765
  import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils/error";
12997
13766
  init_output_spool();
12998
13767
  init_util();
@@ -13282,7 +14051,7 @@ function levelRank(level) {
13282
14051
 
13283
14052
  // src/exec-kill-guard.ts
13284
14053
  import * as os7 from "node:os";
13285
- import * as path24 from "node:path";
14054
+ import * as path25 from "node:path";
13286
14055
  var isWin3 = os7.platform() === "win32";
13287
14056
  async function checkExecKillCommand(cmd, args) {
13288
14057
  if (!cmd) return { blocked: false };
@@ -13495,7 +14264,7 @@ async function checkKillTarget(target) {
13495
14264
  reason: `Blocked: kill ${target.signal} '${target.name}' targets a WrongStack process name.`
13496
14265
  };
13497
14266
  }
13498
- const currentImage = path24.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
14267
+ const currentImage = path25.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
13499
14268
  const targetsNodeRuntime = nameLower === "node" || nameLower.startsWith("node");
13500
14269
  if (targetsNodeRuntime && currentImage === "node") {
13501
14270
  return {
@@ -14282,7 +15051,7 @@ var execTool = {
14282
15051
  command: cmd,
14283
15052
  args: input.args ?? [],
14284
15053
  stdout: "",
14285
- 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.`,
15054
+ 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.`,
14286
15055
  exitCode: 1,
14287
15056
  truncated: false,
14288
15057
  allowed: false,
@@ -14494,18 +15263,17 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
14494
15263
  }
14495
15264
 
14496
15265
  // src/fetch.ts
14497
- import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValidationError3 } from "@wrongstack/core";
15266
+ import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValidationError3 } from "@wrongstack/core/types";
14498
15267
  import TurndownService from "turndown";
14499
15268
 
14500
15269
  // src/_fetch-guard.ts
14501
15270
  import * as dns2 from "node:dns/promises";
14502
15271
  import * as net3 from "node:net";
14503
15272
  import {
14504
- FetchError,
14505
- ToolValidationError as ToolValidationError2,
14506
15273
  isPrivateIPv4 as isPrivateIPv42,
14507
15274
  isPrivateIPv6 as isPrivateIPv62
14508
- } from "@wrongstack/core";
15275
+ } from "@wrongstack/core/utils";
15276
+ import { FetchError, ToolValidationError as ToolValidationError2 } from "@wrongstack/core/types";
14509
15277
  import { Agent, fetch as undiciFetch } from "undici";
14510
15278
  var nativeGlobalFetch = globalThis.fetch;
14511
15279
  var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
@@ -15003,7 +15771,8 @@ init_util();
15003
15771
  import { spawn as spawn9 } from "node:child_process";
15004
15772
  import { statSync as statSync3 } from "node:fs";
15005
15773
  import { dirname as dirname12, resolve as resolve11, sep as sep5 } from "node:path";
15006
- import { assessCommitSafety, buildChildEnv as buildChildEnv4 } from "@wrongstack/core";
15774
+ import { assessCommitSafety } from "@wrongstack/core/coordination";
15775
+ import { buildChildEnv as buildChildEnv4 } from "@wrongstack/core/utils";
15007
15776
  var TIMEOUT_MS2 = 3e4;
15008
15777
  var MAX_OUTPUT3 = 1e5;
15009
15778
  var gitTool = {
@@ -15283,8 +16052,8 @@ function runGit2(args, cwd, signal) {
15283
16052
 
15284
16053
  // src/glob.ts
15285
16054
  import * as fs20 from "node:fs/promises";
15286
- import * as path25 from "node:path";
15287
- import { compileGlob as compileGlob3 } from "@wrongstack/core";
16055
+ import * as path26 from "node:path";
16056
+ import { compileGlob as compileGlob2 } from "@wrongstack/core/utils";
15288
16057
 
15289
16058
  // src/_concurrency.ts
15290
16059
  async function mapWithConcurrency(items, limit, fn) {
@@ -15346,7 +16115,7 @@ var globTool = {
15346
16115
  const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
15347
16116
  const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
15348
16117
  const ignored = await readGitignore(base);
15349
- const re = compileGlob3(input.pattern);
16118
+ const re = compileGlob2(input.pattern);
15350
16119
  const results = [];
15351
16120
  let truncated = false;
15352
16121
  const pushResult = async (full) => {
@@ -15387,7 +16156,7 @@ var globTool = {
15387
16156
  if (DEFAULT_IGNORE2.includes(name)) continue;
15388
16157
  if (ignored.includes(name)) continue;
15389
16158
  const rel = relPrefix ? `${relPrefix}/${name}` : name;
15390
- const full = path25.join(dir, name);
16159
+ const full = path26.join(dir, name);
15391
16160
  if (e.isDirectory()) {
15392
16161
  subdirs.push({ full, rel });
15393
16162
  } else if (e.isFile()) {
@@ -15431,7 +16200,7 @@ var globTool = {
15431
16200
  };
15432
16201
  async function readGitignore(dir) {
15433
16202
  try {
15434
- const raw = await fs20.readFile(path25.join(dir, ".gitignore"), "utf8");
16203
+ const raw = await fs20.readFile(path26.join(dir, ".gitignore"), "utf8");
15435
16204
  return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
15436
16205
  } catch {
15437
16206
  return [];
@@ -15439,11 +16208,12 @@ async function readGitignore(dir) {
15439
16208
  }
15440
16209
 
15441
16210
  // src/grep.ts
15442
- import { expectDefined as expectDefined7 } from "@wrongstack/core";
16211
+ import { expectDefined as expectDefined7 } from "@wrongstack/core/utils";
15443
16212
  import { spawn as spawn10 } from "node:child_process";
15444
16213
  import * as fs21 from "node:fs/promises";
15445
- import * as path26 from "node:path";
15446
- import { buildChildEnv as buildChildEnv5, compileGlob as compileGlob4, ToolValidationError as ToolValidationError4 } from "@wrongstack/core";
16214
+ import * as path27 from "node:path";
16215
+ import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
16216
+ import { buildChildEnv as buildChildEnv5, compileGlob as compileGlob3 } from "@wrongstack/core/utils";
15447
16217
 
15448
16218
  // src/_regex.ts
15449
16219
  var MAX_PATTERN_LEN = 256;
@@ -15733,7 +16503,7 @@ async function runNative(input, base, mode, limit, signal) {
15733
16503
  });
15734
16504
  }
15735
16505
  const re = compiled.regex;
15736
- const globRe = input.glob ? compileGlob4(input.glob) : null;
16506
+ const globRe = input.glob ? compileGlob3(input.glob) : null;
15737
16507
  const matches = [];
15738
16508
  const countOnlyFirstHit = mode === "count" && limit === 1;
15739
16509
  const maxBytes = mode === "content" ? NATIVE_MAX_FILE_BYTES : Math.min(NATIVE_MAX_FILE_BYTES, 256 * 1024);
@@ -15835,7 +16605,7 @@ async function runNative(input, base, mode, limit, signal) {
15835
16605
  if (stopped) return;
15836
16606
  if (DEFAULT_IGNORE3.includes(e.name)) continue;
15837
16607
  if (e.isSymbolicLink()) continue;
15838
- const full = path26.join(dir, e.name);
16608
+ const full = path27.join(dir, e.name);
15839
16609
  if (e.isDirectory()) {
15840
16610
  subdirs.push(full);
15841
16611
  } else if (e.isFile()) {
@@ -15859,7 +16629,10 @@ init_spawn_stream();
15859
16629
  init_util();
15860
16630
  init_legacy_bridge();
15861
16631
  import { join as join21 } from "node:path";
15862
- import { detectPackageEcosystem, recordPackageAction } from "@wrongstack/core";
16632
+ import {
16633
+ detectEcosystem as detectPackageEcosystem,
16634
+ recordPackageAction
16635
+ } from "@wrongstack/core/coordination";
15863
16636
  var installTool = {
15864
16637
  name: "install",
15865
16638
  category: "Package Management",
@@ -16045,7 +16818,7 @@ function resolveManifestPath(cwd, pkgManager) {
16045
16818
  // src/json.ts
16046
16819
  init_util();
16047
16820
  import * as fs22 from "node:fs/promises";
16048
- import { deepMerge as deepMergeCore } from "@wrongstack/core";
16821
+ import { deepMerge as deepMergeCore } from "@wrongstack/core/utils";
16049
16822
  var jsonTool = {
16050
16823
  name: "json",
16051
16824
  category: "Data",
@@ -16423,56 +17196,56 @@ function jmespathSearch(data, query) {
16423
17196
  }
16424
17197
  function validateJsonSchema(data, schema) {
16425
17198
  const errors = [];
16426
- function check(value, s, path33) {
17199
+ function check(value, s, path34) {
16427
17200
  if (s["type"]) {
16428
17201
  const expectedType = s["type"];
16429
17202
  const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
16430
17203
  if (expectedType === "integer") {
16431
- if (!Number.isInteger(value)) errors.push(`${path33}: expected integer, got ${actualType}`);
17204
+ if (!Number.isInteger(value)) errors.push(`${path34}: expected integer, got ${actualType}`);
16432
17205
  } else if (expectedType !== actualType) {
16433
- errors.push(`${path33}: expected ${expectedType}, got ${actualType}`);
17206
+ errors.push(`${path34}: expected ${expectedType}, got ${actualType}`);
16434
17207
  }
16435
17208
  }
16436
17209
  if (typeof value === "string" && s["format"] === "uri" && value) {
16437
17210
  try {
16438
17211
  new URL(value);
16439
17212
  } catch {
16440
- errors.push(`${path33}: not a valid URI`);
17213
+ errors.push(`${path34}: not a valid URI`);
16441
17214
  }
16442
17215
  }
16443
17216
  if (typeof value === "string" && s["pattern"]) {
16444
17217
  const re = new RegExp(s["pattern"]);
16445
- if (!re.test(value)) errors.push(`${path33}: does not match pattern ${s["pattern"]}`);
17218
+ if (!re.test(value)) errors.push(`${path34}: does not match pattern ${s["pattern"]}`);
16446
17219
  }
16447
17220
  if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
16448
- errors.push(`${path33}: string too short (min ${s["minLength"]})`);
17221
+ errors.push(`${path34}: string too short (min ${s["minLength"]})`);
16449
17222
  }
16450
17223
  if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
16451
- errors.push(`${path33}: string too long (max ${s["maxLength"]})`);
17224
+ errors.push(`${path34}: string too long (max ${s["maxLength"]})`);
16452
17225
  }
16453
17226
  if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
16454
- errors.push(`${path33}: below minimum ${s["minimum"]}`);
17227
+ errors.push(`${path34}: below minimum ${s["minimum"]}`);
16455
17228
  }
16456
17229
  if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
16457
- errors.push(`${path33}: above maximum ${s["maximum"]}`);
17230
+ errors.push(`${path34}: above maximum ${s["maximum"]}`);
16458
17231
  }
16459
17232
  if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
16460
17233
  for (let i = 0; i < value.length; i++) {
16461
- check(value[i], s["items"], `${path33}[${i}]`);
17234
+ check(value[i], s["items"], `${path34}[${i}]`);
16462
17235
  }
16463
17236
  }
16464
17237
  if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
16465
17238
  const props = s["properties"];
16466
17239
  for (const [k, propSchema] of Object.entries(props)) {
16467
- check(value[k], propSchema, `${path33}.${k}`);
17240
+ check(value[k], propSchema, `${path34}.${k}`);
16468
17241
  }
16469
17242
  }
16470
17243
  }
16471
17244
  check(data, schema, "$");
16472
17245
  return { valid: errors.length === 0, errors };
16473
17246
  }
16474
- function simpleQuery(data, path33) {
16475
- const parts = path33.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
17247
+ function simpleQuery(data, path34) {
17248
+ const parts = path34.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
16476
17249
  let current = data;
16477
17250
  for (const part of parts) {
16478
17251
  if (current === null || current === void 0) return void 0;
@@ -16523,7 +17296,8 @@ function toYaml(data, indent = 0) {
16523
17296
 
16524
17297
  // src/kanban.ts
16525
17298
  import { randomUUID } from "node:crypto";
16526
- import { deserializeTaskGraph as deserializeTaskGraph2, loadTasks as loadTasks2, serializeTaskGraph } from "@wrongstack/core";
17299
+ import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
17300
+ import { loadTasks as loadTasks2 } from "@wrongstack/core/storage";
16527
17301
  import {
16528
17302
  addCheckToTask,
16529
17303
  addColumn,
@@ -16570,18 +17344,19 @@ import {
16570
17344
  updateColumn,
16571
17345
  updateGoalMetricOnTask,
16572
17346
  updateTask,
16573
- updateTaskAssignment
17347
+ updateTaskAssignment,
17348
+ verifyTaskCompletion
16574
17349
  } from "@wrongstack/kanban";
16575
17350
 
16576
17351
  // src/session-kanban.ts
17352
+ import { GlobalMailbox } from "@wrongstack/core/coordination";
16577
17353
  import {
16578
- deserializeTaskGraph,
16579
- GlobalMailbox,
16580
17354
  loadPlan,
16581
17355
  loadTasks,
16582
17356
  mutatePlan,
16583
17357
  mutateTasks
16584
- } from "@wrongstack/core";
17358
+ } from "@wrongstack/core/storage";
17359
+ import { deserializeTaskGraph } from "@wrongstack/core/tasking";
16585
17360
  import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
16586
17361
  import {
16587
17362
  createBoard,
@@ -16802,8 +17577,8 @@ function projectSessionPlanToKanban(projectRoot, items, sessionId) {
16802
17577
  var kanbanTool = {
16803
17578
  name: "kanban",
16804
17579
  category: "Project",
16805
- 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.",
16806
- 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.",
17580
+ 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.",
17581
+ 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.",
16807
17582
  permission: "confirm",
16808
17583
  mutating: true,
16809
17584
  capabilities: ["fs.write"],
@@ -16859,7 +17634,9 @@ var kanbanTool = {
16859
17634
  "add_check",
16860
17635
  "update_check",
16861
17636
  "add_note",
16862
- "add_link"
17637
+ "add_link",
17638
+ "verify_completion",
17639
+ "split_atomic"
16863
17640
  ]
16864
17641
  },
16865
17642
  boardId: { type: "string" },
@@ -16914,6 +17691,7 @@ var kanbanTool = {
16914
17691
  runTaskId: { type: "string" },
16915
17692
  lastResult: { type: "string" },
16916
17693
  error: { type: "string" },
17694
+ expectedLeaseId: { type: "string" },
16917
17695
  assignmentStatus: {
16918
17696
  type: "string",
16919
17697
  enum: ["assigned", "queued", "running", "completed", "failed", "cancelled"]
@@ -16948,6 +17726,7 @@ var kanbanTool = {
16948
17726
  lastFailureKind: { type: "string" },
16949
17727
  dependsOn: { type: "array", items: { type: "string" } },
16950
17728
  estimatedHours: { type: "number" },
17729
+ actualHours: { type: "number" },
16951
17730
  taskGraph: { type: "object" },
16952
17731
  graphId: { type: "string" },
16953
17732
  specId: { type: "string" },
@@ -17256,24 +18035,7 @@ var kanbanTool = {
17256
18035
  if (!input.boardId || !input.taskId || !input.childTitles?.length) {
17257
18036
  return fail("split_task requires boardId, taskId, and childTitles.");
17258
18037
  }
17259
- const result2 = await splitTask(projectRoot, input.boardId, input.taskId, {
17260
- titles: input.childTitles,
17261
- ...input.targetColumnId !== void 0 ? { columnId: input.targetColumnId } : {},
17262
- ...input.inheritAssignment !== void 0 ? { inheritAssignment: input.inheritAssignment } : {},
17263
- ...input.inheritLabels !== void 0 ? { inheritLabels: input.inheritLabels } : {},
17264
- ...input.inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria: input.inheritSuccessCriteria } : {},
17265
- ...input.inheritGoalMetrics !== void 0 ? { inheritGoalMetrics: input.inheritGoalMetrics } : {},
17266
- ...input.inheritDependencies !== void 0 ? { inheritDependencies: input.inheritDependencies } : {},
17267
- ...input.chainChildren !== void 0 ? { chainChildren: input.chainChildren } : {},
17268
- ...input.rewireDependents !== void 0 ? { rewireDependents: input.rewireDependents } : {}
17269
- });
17270
- return result2 ? {
17271
- ok: true,
17272
- message: `${result2.children.length} child task(s) created.`,
17273
- board: result2.board,
17274
- task: result2.parent,
17275
- children: result2.children
17276
- } : fail("Task not found.");
18038
+ return handleSplitTask(projectRoot, input, {});
17277
18039
  }
17278
18040
  case "merge_tasks": {
17279
18041
  if (!input.boardId || !input.taskIds?.length || !input.title) {
@@ -17638,6 +18400,39 @@ var kanbanTool = {
17638
18400
  });
17639
18401
  return board ? okBoard(board, "Link added.") : fail("Task not found.");
17640
18402
  }
18403
+ case "verify_completion": {
18404
+ if (!input.boardId || !input.taskId) {
18405
+ return fail("verify_completion requires boardId and taskId.");
18406
+ }
18407
+ const verResult = await verifyTaskCompletion(projectRoot, input.boardId, input.taskId);
18408
+ const persistedBoard = await updateTask(projectRoot, input.boardId, input.taskId, {
18409
+ verificationReport: verResult.report,
18410
+ successCriteria: verResult.task.successCriteria
18411
+ });
18412
+ if (!persistedBoard) {
18413
+ return {
18414
+ ok: false,
18415
+ verdict: verResult.report.verdict,
18416
+ message: `Verification succeeded but persist failed: ${verResult.report.markdownSummary}. Board may be stale \u2014 re-run verify_completion.`,
18417
+ board: verResult.board
18418
+ };
18419
+ }
18420
+ const freshTask = persistedBoard.tasks?.find((t) => t.id === input.taskId);
18421
+ const deterministicVerdicts = ["passed", "failed", "needs_human", "incomplete"];
18422
+ return {
18423
+ ok: deterministicVerdicts.includes(verResult.report.verdict),
18424
+ verdict: verResult.report.verdict,
18425
+ message: verResult.report.markdownSummary,
18426
+ board: persistedBoard,
18427
+ task: freshTask ?? verResult.task
18428
+ };
18429
+ }
18430
+ case "split_atomic": {
18431
+ if (!input.boardId || !input.taskId || !input.childTitles?.length) {
18432
+ return fail("split_atomic requires boardId, taskId, and childTitles (at least one).");
18433
+ }
18434
+ return handleSplitTask(projectRoot, input, { atomic: true });
18435
+ }
17641
18436
  default:
17642
18437
  return fail(`Unknown kanban action: ${input.action}`);
17643
18438
  }
@@ -17657,6 +18452,50 @@ function okBoard(board, message = "Board loaded.") {
17657
18452
  function okTask(board, task, message) {
17658
18453
  return { ok: true, message, board, task };
17659
18454
  }
18455
+ async function handleSplitTask(projectRoot, input, extraSplitOptions) {
18456
+ const boardId = input.boardId;
18457
+ const taskId = input.taskId;
18458
+ const childTitles = input.childTitles;
18459
+ if (!boardId || !taskId || !childTitles?.length) {
18460
+ return fail("split requires boardId, taskId, and at least one childTitles.");
18461
+ }
18462
+ const {
18463
+ targetColumnId,
18464
+ inheritAssignment,
18465
+ inheritLabels,
18466
+ inheritSuccessCriteria,
18467
+ inheritGoalMetrics,
18468
+ inheritDependencies,
18469
+ chainChildren,
18470
+ rewireDependents
18471
+ } = input;
18472
+ const result = await splitTask(projectRoot, boardId, taskId, {
18473
+ titles: childTitles,
18474
+ ...extraSplitOptions,
18475
+ ...targetColumnId !== void 0 ? { columnId: targetColumnId } : {},
18476
+ ...inheritAssignment !== void 0 ? { inheritAssignment } : {},
18477
+ ...inheritLabels !== void 0 ? { inheritLabels } : {},
18478
+ ...inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria } : {},
18479
+ ...inheritGoalMetrics !== void 0 ? { inheritGoalMetrics } : {},
18480
+ ...inheritDependencies !== void 0 ? { inheritDependencies } : {},
18481
+ ...chainChildren !== void 0 ? { chainChildren } : {},
18482
+ ...rewireDependents !== void 0 ? { rewireDependents } : {}
18483
+ });
18484
+ if (!result) return fail("Task not found.");
18485
+ const freshParent = result.board.tasks?.find((t) => t.id === taskId);
18486
+ if (!freshParent) {
18487
+ return fail(
18488
+ `Split succeeded but parent ${taskId} not found in returned board. Children: [${result.children.map((c) => c.id).join(", ")}].`
18489
+ );
18490
+ }
18491
+ return {
18492
+ ok: true,
18493
+ message: `${result.children.length} child task(s) created.`,
18494
+ board: result.board,
18495
+ task: freshParent,
18496
+ children: result.children
18497
+ };
18498
+ }
17660
18499
  async function requireBoard(projectRoot, boardId) {
17661
18500
  return boardId ? getBoard2(projectRoot, boardId) : null;
17662
18501
  }
@@ -17758,7 +18597,8 @@ function taskPatch(input) {
17758
18597
  labels: input.labels,
17759
18598
  assignedAgent: input.agentId,
17760
18599
  ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
17761
- ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {}
18600
+ ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
18601
+ ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
17762
18602
  };
17763
18603
  }
17764
18604
  function assignmentInput(input) {
@@ -17940,7 +18780,7 @@ async function detectLinter(cwd) {
17940
18780
 
17941
18781
  // src/logs.ts
17942
18782
  import { spawn as spawn11 } from "node:child_process";
17943
- import { buildChildEnv as buildChildEnv6 } from "@wrongstack/core";
18783
+ import { buildChildEnv as buildChildEnv6 } from "@wrongstack/core/utils";
17944
18784
  init_util();
17945
18785
  var logsTool = {
17946
18786
  name: "logs",
@@ -18079,7 +18919,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
18079
18919
  }
18080
18920
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
18081
18921
  var MAX_TAIL_LINES = 1e5;
18082
- async function fileLogs(path33, lines, filterRe, stream) {
18922
+ async function fileLogs(path34, lines, filterRe, stream) {
18083
18923
  const { createInterface } = await import("node:readline");
18084
18924
  const { createReadStream: createReadStream2 } = await import("node:fs");
18085
18925
  const entries = [];
@@ -18088,7 +18928,7 @@ async function fileLogs(path33, lines, filterRe, stream) {
18088
18928
  let writeIdx = 0;
18089
18929
  let totalLines = 0;
18090
18930
  const rl = createInterface({
18091
- input: createReadStream2(path33),
18931
+ input: createReadStream2(path34),
18092
18932
  crlfDelay: Number.POSITIVE_INFINITY
18093
18933
  });
18094
18934
  for await (const line of rl) {
@@ -18109,7 +18949,7 @@ async function fileLogs(path33, lines, filterRe, stream) {
18109
18949
  if (parsed) entries.push(parsed);
18110
18950
  }
18111
18951
  return {
18112
- source: path33,
18952
+ source: path34,
18113
18953
  entries,
18114
18954
  total: entries.length,
18115
18955
  truncated: totalLines > effLines,
@@ -18156,7 +18996,7 @@ function parseLine(line) {
18156
18996
  init_util();
18157
18997
  init_win32_resolve();
18158
18998
  import { spawn as spawn12 } from "node:child_process";
18159
- import { buildChildEnv as buildChildEnv7 } from "@wrongstack/core";
18999
+ import { buildChildEnv as buildChildEnv7 } from "@wrongstack/core/utils";
18160
19000
  var outdatedTool = {
18161
19001
  name: "outdated",
18162
19002
  category: "Package Management",
@@ -18341,8 +19181,8 @@ init_util();
18341
19181
  import { spawn as spawn13 } from "node:child_process";
18342
19182
  import * as fs23 from "node:fs/promises";
18343
19183
  import * as os8 from "node:os";
18344
- import * as path27 from "node:path";
18345
- import { buildChildEnv as buildChildEnv8 } from "@wrongstack/core";
19184
+ import * as path28 from "node:path";
19185
+ import { buildChildEnv as buildChildEnv8 } from "@wrongstack/core/utils";
18346
19186
  var patchTool = {
18347
19187
  name: "patch",
18348
19188
  category: "Filesystem",
@@ -18377,9 +19217,9 @@ var patchTool = {
18377
19217
  for (const t of targets) {
18378
19218
  const stripped = stripPathComponents(t, strip);
18379
19219
  if (!stripped) continue;
18380
- const candidate = path27.resolve(dir, stripped);
18381
- const rel = path27.relative(ctx.projectRoot, candidate);
18382
- if (rel.startsWith("..") || path27.isAbsolute(rel)) {
19220
+ const candidate = path28.resolve(dir, stripped);
19221
+ const rel = path28.relative(ctx.projectRoot, candidate);
19222
+ if (rel.startsWith("..") || path28.isAbsolute(rel)) {
18383
19223
  return {
18384
19224
  applied: 0,
18385
19225
  rejected: 1,
@@ -18396,11 +19236,11 @@ var patchTool = {
18396
19236
  beforeContents.set(target, await readTextForTracking(target));
18397
19237
  }
18398
19238
  }
18399
- const tmpDir = await fs23.mkdtemp(path27.join(os8.tmpdir(), ".wstack_patch_"));
19239
+ const tmpDir = await fs23.mkdtemp(path28.join(os8.tmpdir(), ".wstack_patch_"));
18400
19240
  try {
18401
19241
  await fs23.chmod(tmpDir, 448).catch(() => {
18402
19242
  });
18403
- const patchFile = path27.join(tmpDir, "in.diff");
19243
+ const patchFile = path28.join(tmpDir, "in.diff");
18404
19244
  await fs23.writeFile(patchFile, input.patch, { mode: 384 });
18405
19245
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
18406
19246
  const result = await runPatch(args, dir, opts.signal);
@@ -18506,11 +19346,11 @@ import {
18506
19346
  mutatePlan as mutatePlan2,
18507
19347
  removePlanItem,
18508
19348
  setPlanItemStatus
18509
- } from "@wrongstack/core";
19349
+ } from "@wrongstack/core/storage";
18510
19350
  import {
18511
- mutateTasks as mutateTasks2,
18512
- formatTaskList
18513
- } from "@wrongstack/core";
19351
+ mutateTasks as mutateTasks2
19352
+ } from "@wrongstack/core/storage";
19353
+ import { formatTaskList } from "@wrongstack/core/utils";
18514
19354
  import { randomUUID as randomUUID2 } from "node:crypto";
18515
19355
  var planTool = {
18516
19356
  name: "plan",
@@ -18777,7 +19617,8 @@ function mkResult(plan, ok, message, todos) {
18777
19617
  // src/read.ts
18778
19618
  init_util();
18779
19619
  import * as fs24 from "node:fs/promises";
18780
- import { FsError, toErrorMessage as toErrorMessage4, ToolValidationError as ToolValidationError5 } from "@wrongstack/core";
19620
+ import { FsError, ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
19621
+ import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
18781
19622
  var MAX_BYTES2 = 5 * 1024 * 1024;
18782
19623
  var readTool = {
18783
19624
  name: "read",
@@ -18983,20 +19824,20 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
18983
19824
  }
18984
19825
 
18985
19826
  // src/replace.ts
18986
- import { expectDefined as expectDefined8 } from "@wrongstack/core";
19827
+ import { expectDefined as expectDefined8 } from "@wrongstack/core/utils";
18987
19828
  import { spawn as spawn14 } from "node:child_process";
18988
19829
  import * as fs25 from "node:fs/promises";
18989
- import * as path28 from "node:path";
19830
+ import * as path29 from "node:path";
18990
19831
  import {
18991
19832
  atomicWrite as atomicWrite3,
18992
19833
  buildChildEnv as buildChildEnv9,
18993
- compileGlob as compileGlob5,
19834
+ compileGlob as compileGlob4,
18994
19835
  detectNewlineStyle as detectNewlineStyle2,
18995
19836
  normalizeToLf as normalizeToLf2,
18996
19837
  toStyle as toStyle2,
18997
- ToolValidationError as ToolValidationError6,
18998
19838
  unifiedDiff as unifiedDiff2
18999
- } from "@wrongstack/core";
19839
+ } from "@wrongstack/core/utils";
19840
+ import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
19000
19841
  init_util();
19001
19842
  var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
19002
19843
  var replaceTool = {
@@ -19055,7 +19896,7 @@ var replaceTool = {
19055
19896
  });
19056
19897
  }
19057
19898
  const re = compiled.regex;
19058
- const globRe = input.glob ? compileGlob5(input.glob) : null;
19899
+ const globRe = input.glob ? compileGlob4(input.glob) : null;
19059
19900
  const dryRun = input.dry_run ?? true;
19060
19901
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
19061
19902
  const fileList = await resolveFiles2(filesInput, ctx, globRe);
@@ -19075,8 +19916,8 @@ var replaceTool = {
19075
19916
  } catch {
19076
19917
  continue;
19077
19918
  }
19078
- const rel = path28.relative(realRoot, realPath);
19079
- if (rel.startsWith("..") || path28.isAbsolute(rel)) continue;
19919
+ const rel = path29.relative(realRoot, realPath);
19920
+ if (rel.startsWith("..") || path29.isAbsolute(rel)) continue;
19080
19921
  const stat17 = await fs25.stat(realPath).catch(() => null);
19081
19922
  if (!stat17?.isFile()) continue;
19082
19923
  let content;
@@ -19195,7 +20036,7 @@ function spawnRgFind(pattern, base) {
19195
20036
  }
19196
20037
  async function globNative(pattern, base, extraGlob) {
19197
20038
  const results = [];
19198
- const globRe = compileGlob5(pattern);
20039
+ const globRe = compileGlob4(pattern);
19199
20040
  const walk = async (dir) => {
19200
20041
  let entries;
19201
20042
  try {
@@ -19205,7 +20046,7 @@ async function globNative(pattern, base, extraGlob) {
19205
20046
  }
19206
20047
  for (const e of entries) {
19207
20048
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
19208
- const full = path28.join(dir, e.name);
20049
+ const full = path29.join(dir, e.name);
19209
20050
  try {
19210
20051
  const stat17 = await fs25.lstat(full);
19211
20052
  if (stat17.isSymbolicLink()) continue;
@@ -19232,8 +20073,8 @@ async function globNative(pattern, base, extraGlob) {
19232
20073
  // src/scaffold.ts
19233
20074
  init_util();
19234
20075
  import * as fs26 from "node:fs/promises";
19235
- import * as path29 from "node:path";
19236
- import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
20076
+ import * as path30 from "node:path";
20077
+ import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
19237
20078
  var BUILT_IN_TEMPLATES = {
19238
20079
  "npm-package": {
19239
20080
  description: "Basic npm package with ESM",
@@ -19382,16 +20223,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
19382
20223
  let filesCreated = 0;
19383
20224
  for (const [filePath, content] of Object.entries(templateFiles)) {
19384
20225
  const resolvedPath = substituteVars(filePath, name, vars);
19385
- const joinedPath = path29.join(cwd, resolvedPath);
19386
- const root = path29.resolve(ctx.projectRoot);
19387
- const target = path29.resolve(joinedPath);
19388
- const rel = path29.relative(root, target);
19389
- if (rel.startsWith("..") || path29.isAbsolute(rel)) {
20226
+ const joinedPath = path30.join(cwd, resolvedPath);
20227
+ const root = path30.resolve(ctx.projectRoot);
20228
+ const target = path30.resolve(joinedPath);
20229
+ const rel = path30.relative(root, target);
20230
+ if (rel.startsWith("..") || path30.isAbsolute(rel)) {
19390
20231
  throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
19391
20232
  }
19392
20233
  const fullPath = target;
19393
20234
  if (!dryRun) {
19394
- await fs26.mkdir(path29.dirname(fullPath), { recursive: true });
20235
+ await fs26.mkdir(path30.dirname(fullPath), { recursive: true });
19395
20236
  await atomicWrite4(fullPath, substituteVars(content, name, vars));
19396
20237
  }
19397
20238
  files.push(resolvedPath);
@@ -19420,7 +20261,8 @@ function substituteVars(content, name, vars) {
19420
20261
  }
19421
20262
 
19422
20263
  // src/search.ts
19423
- import { expectDefined as expectDefined9, FetchError as FetchError3, ToolValidationError as ToolValidationError7 } from "@wrongstack/core";
20264
+ import { FetchError as FetchError3, ToolValidationError as ToolValidationError7 } from "@wrongstack/core/types";
20265
+ import { expectDefined as expectDefined9 } from "@wrongstack/core/utils";
19424
20266
  import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
19425
20267
  var DEFAULT_NUM = 10;
19426
20268
  var MAX_RESULTS = 50;
@@ -19870,15 +20712,13 @@ var setWorkingDirTool = {
19870
20712
  import {
19871
20713
  computeTaskItemProgress,
19872
20714
  formatTaskList as formatTaskList2
19873
- } from "@wrongstack/core";
19874
- import {
19875
- mutateTasks as mutateTasks3
19876
- } from "@wrongstack/core";
20715
+ } from "@wrongstack/core/utils";
20716
+ import { mutateTasks as mutateTasks3 } from "@wrongstack/core/storage";
19877
20717
  import {
19878
20718
  addPlanItem as addPlanItem2,
19879
20719
  mutatePlan as mutatePlan3,
19880
20720
  formatPlan as formatPlan2
19881
- } from "@wrongstack/core";
20721
+ } from "@wrongstack/core/storage";
19882
20722
  import { randomUUID as randomUUID3 } from "node:crypto";
19883
20723
  function findTaskIndex(tasks, query) {
19884
20724
  const asNum = Number.parseInt(query, 10);
@@ -20248,7 +21088,7 @@ ${formatTaskList2(file.tasks)}` : file.tasks.length > 0 ? formatTaskList2(file.t
20248
21088
  init_spawn_stream();
20249
21089
  init_util();
20250
21090
  init_legacy_bridge();
20251
- import * as path30 from "node:path";
21091
+ import * as path31 from "node:path";
20252
21092
  var testTool = {
20253
21093
  name: "test",
20254
21094
  category: "Code Quality",
@@ -20355,7 +21195,7 @@ async function detectRunner(cwd) {
20355
21195
  const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
20356
21196
  for (const f of candidates) {
20357
21197
  try {
20358
- await stat17(path30.join(cwd, f));
21198
+ await stat17(path31.join(cwd, f));
20359
21199
  if (f.includes("vitest")) return "vitest";
20360
21200
  if (f.includes("jest")) return "jest";
20361
21201
  if (f.includes("mocha")) return "mocha";
@@ -20432,7 +21272,7 @@ function parseResult(runner, result, duration) {
20432
21272
  }
20433
21273
 
20434
21274
  // src/todo.ts
20435
- import { loadPlan as loadPlan2, loadTasks as loadTasks3, savePlan, saveTasks, setPlanItemStatus as setPlanItemStatus2 } from "@wrongstack/core";
21275
+ import { loadPlan as loadPlan2, loadTasks as loadTasks3, savePlan, saveTasks, setPlanItemStatus as setPlanItemStatus2 } from "@wrongstack/core/storage";
20436
21276
  var todoTool = {
20437
21277
  name: "todo",
20438
21278
  category: "Session",
@@ -20824,9 +21664,9 @@ var toolUseTool = {
20824
21664
 
20825
21665
  // src/tree.ts
20826
21666
  init_util();
20827
- import { expectDefined as expectDefined10 } from "@wrongstack/core";
21667
+ import { expectDefined as expectDefined10 } from "@wrongstack/core/utils";
20828
21668
  import * as fs28 from "node:fs/promises";
20829
- import * as path31 from "node:path";
21669
+ import * as path32 from "node:path";
20830
21670
  var DEFAULT_IGNORE5 = [
20831
21671
  "node_modules",
20832
21672
  ".git",
@@ -20997,7 +21837,7 @@ async function walkDir(dir, depth, opts) {
20997
21837
  opts.lines.push(opts.prefix + branch + displayName);
20998
21838
  if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
20999
21839
  const childPrefix = opts.prefix + connector;
21000
- await walkDir(path31.join(dir, entry.name), depth + 1, {
21840
+ await walkDir(path32.join(dir, entry.name), depth + 1, {
21001
21841
  ...opts,
21002
21842
  prefix: childPrefix,
21003
21843
  isLast
@@ -21010,7 +21850,7 @@ async function walkDir(dir, depth, opts) {
21010
21850
  init_spawn_stream();
21011
21851
  init_util();
21012
21852
  init_legacy_bridge();
21013
- import * as path32 from "node:path";
21853
+ import * as path33 from "node:path";
21014
21854
  var typecheckTool = {
21015
21855
  name: "typecheck",
21016
21856
  category: "Code Quality",
@@ -21113,8 +21953,8 @@ async function findTsConfig(cwd) {
21113
21953
  const candidates = ["tsconfig.json", "tsconfig.base.json"];
21114
21954
  for (const f of candidates) {
21115
21955
  try {
21116
- const s = await stat17(path32.join(cwd, f));
21117
- if (s.isFile()) return path32.join(cwd, f);
21956
+ const s = await stat17(path33.join(cwd, f));
21957
+ if (s.isFile()) return path33.join(cwd, f);
21118
21958
  } catch {
21119
21959
  }
21120
21960
  }
@@ -21123,7 +21963,8 @@ async function findTsConfig(cwd) {
21123
21963
 
21124
21964
  // src/write.ts
21125
21965
  import * as fs29 from "node:fs/promises";
21126
- import { atomicWrite as atomicWrite5, ToolValidationError as ToolValidationError8, unifiedDiff as unifiedDiff3 } from "@wrongstack/core";
21966
+ import { ToolValidationError as ToolValidationError8 } from "@wrongstack/core/types";
21967
+ import { atomicWrite as atomicWrite5, unifiedDiff as unifiedDiff3 } from "@wrongstack/core/utils";
21127
21968
  init_util();
21128
21969
  var writeTool = {
21129
21970
  name: "write",