@wrongstack/tools 0.305.0 → 0.306.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 (66) hide show
  1. package/dist/_shell-pick.d.ts +4 -5
  2. package/dist/_util.d.ts +22 -5
  3. package/dist/audit.d.ts +0 -1
  4. package/dist/audit.js +135 -46
  5. package/dist/bash.js +61 -37
  6. package/dist/browser/index.js +29 -9
  7. package/dist/browser/types.d.ts +7 -1
  8. package/dist/builtin.js +1280 -728
  9. package/dist/codebase-index/codebase-search-tool.d.ts +5 -0
  10. package/dist/codebase-index/index.d.ts +1 -0
  11. package/dist/codebase-index/index.js +225 -153
  12. package/dist/codebase-index/project-server-endpoint.d.ts +1 -2
  13. package/dist/codebase-index/project-server.js +19 -20
  14. package/dist/diff.d.ts +5 -0
  15. package/dist/diff.js +78 -12
  16. package/dist/document.js +18 -6
  17. package/dist/edit.js +69 -16
  18. package/dist/exec.js +44 -22
  19. package/dist/fetch.js +13 -1
  20. package/dist/format.d.ts +4 -2
  21. package/dist/format.js +81 -31
  22. package/dist/glob.js +12 -4
  23. package/dist/grep.d.ts +2 -0
  24. package/dist/grep.js +15 -4
  25. package/dist/index.js +1357 -765
  26. package/dist/install.js +96 -37
  27. package/dist/kanban-tool-types.d.ts +6 -1
  28. package/dist/kanban.js +60 -0
  29. package/dist/languages/index.js +28 -13
  30. package/dist/lint.js +28 -13
  31. package/dist/logs.d.ts +0 -1
  32. package/dist/logs.js +44 -13
  33. package/dist/memory.d.ts +8 -0
  34. package/dist/memory.js +23 -3
  35. package/dist/mode.d.ts +1 -1
  36. package/dist/mode.js +3 -0
  37. package/dist/next-steps.d.ts +2 -3
  38. package/dist/next-steps.js +3 -3
  39. package/dist/outdated.d.ts +0 -3
  40. package/dist/outdated.js +89 -48
  41. package/dist/pack.js +1280 -728
  42. package/dist/plan.js +76 -3
  43. package/dist/process-registry.d.ts +8 -2
  44. package/dist/process-registry.js +28 -13
  45. package/dist/ps-slash.js +22 -12
  46. package/dist/read.js +10 -4
  47. package/dist/replace.d.ts +4 -0
  48. package/dist/replace.js +104 -7
  49. package/dist/search.d.ts +6 -0
  50. package/dist/search.js +47 -26
  51. package/dist/session-kanban.d.ts +25 -0
  52. package/dist/session-kanban.js +16 -0
  53. package/dist/skill.d.ts +6 -0
  54. package/dist/skill.js +9 -10
  55. package/dist/task.js +66 -2
  56. package/dist/test.js +28 -13
  57. package/dist/todo.js +64 -2
  58. package/dist/tool-icons.js +4 -2
  59. package/dist/tool-summary.d.ts +1 -1
  60. package/dist/tool-summary.js +76 -1
  61. package/dist/tool-tier.js +1280 -728
  62. package/dist/tree.js +9 -10
  63. package/dist/typecheck.d.ts +0 -2
  64. package/dist/typecheck.js +98 -31
  65. package/dist/write.js +58 -10
  66. package/package.json +4 -3
package/dist/index.js CHANGED
@@ -422,8 +422,11 @@ var init_redact_command = __esm({
422
422
  /--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\s,][^\s]*)?/gi,
423
423
  // -t short flag (token): attached (-tVALUE), separated (-t VALUE), or -t=VALUE.
424
424
  // (?<![-\w]) anchors to a token start so we don't match the `-t` inside `--token`.
425
+ // The value must be token-like (>= 8 chars) so ordinary combined flags such
426
+ // as `tar -tf` / `ssh -tt` are not eaten. Global flag: EVERY occurrence is
427
+ // redacted, not just the first.
425
428
  // NOTE: synced with @wrongstack/core observability/redact-command.ts.
426
- /(?<![-\w])-t(?:[=\s]+)?[^\s,-]+/,
429
+ /(?<![-\w])-t(?:[=\s]+)?[^\s,-]{8,}/g,
427
430
  // -p|-password|-a (redis auth) short flags: attached + separated + =value.
428
431
  // Same token-start anchor; over-redaction is an accepted tradeoff for a
429
432
  // redaction function. Synced with core copy.
@@ -431,8 +434,9 @@ var init_redact_command = __esm({
431
434
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
432
435
  /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
433
436
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
434
- // when preceded by a flag name (e.g. --github-token=EyJ...).
435
- /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/
437
+ // when preceded by a flag name (e.g. --github-token=EyJ...). Global flag so
438
+ // every such flag in the command line is redacted, not just the first.
439
+ /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/g
436
440
  ];
437
441
  }
438
442
  });
@@ -521,11 +525,15 @@ var init_process_registry = __esm({
521
525
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
522
526
  }
523
527
  _canSignalProcessGroup(p) {
524
- return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
528
+ return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && p.child !== null && typeof p.child.pid === "number" && p.child.pid === p.pid;
525
529
  }
526
530
  _killChildDirect(p, signal) {
527
531
  try {
528
- p.child.kill(signal);
532
+ if (p.child) {
533
+ p.child.kill(signal);
534
+ return;
535
+ }
536
+ if (this._isSafeSignalPid(p.pid)) process.kill(p.pid, signal);
529
537
  } catch {
530
538
  }
531
539
  }
@@ -723,15 +731,15 @@ var init_process_registry = __esm({
723
731
  this._pruneStale(pid);
724
732
  const p = this.processes.get(pid);
725
733
  if (!p) return false;
726
- if (p.killed) return true;
734
+ if (p.killed && opts.force !== true) return true;
727
735
  if (p.protected && opts.includeProtected !== true) return false;
728
736
  if (opts.preserveBackground && p.background) return false;
729
737
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
730
738
  const isWin5 = os.platform() === "win32";
731
739
  if (isWin5) {
732
- const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
740
+ const liveRealChild = p.child === null || p.child.exitCode === null && typeof p.child.pid === "number";
733
741
  const directFallback = () => {
734
- if (p.child.exitCode === null) {
742
+ if (p.child && p.child.exitCode === null) {
735
743
  try {
736
744
  p.child.kill("SIGKILL");
737
745
  } catch {
@@ -743,10 +751,7 @@ var init_process_registry = __esm({
743
751
  onSettled: directFallback
744
752
  })) {
745
753
  } else {
746
- try {
747
- p.child.kill(force ? "SIGKILL" : "SIGTERM");
748
- } catch {
749
- }
754
+ this._killChildDirect(p, force ? "SIGKILL" : "SIGTERM");
750
755
  }
751
756
  p.killed = true;
752
757
  return true;
@@ -757,7 +762,7 @@ var init_process_registry = __esm({
757
762
  } else {
758
763
  this._killPosix(p, "SIGTERM");
759
764
  const timer = setTimeout(() => {
760
- if (this.processes.has(pid) && !p.child.killed) {
765
+ if (this.processes.has(pid) && !p.child?.killed) {
761
766
  this._killPosix(p, "SIGKILL");
762
767
  }
763
768
  }, graceMs);
@@ -812,6 +817,16 @@ var init_process_registry = __esm({
812
817
  * before reusing a PID, but we want to clean up before that becomes a risk.
813
818
  */
814
819
  _isStaleEntry(entry) {
820
+ if (entry.child === null) {
821
+ if (Date.now() - entry.startedAt <= 6e4) return false;
822
+ if (os.platform() === "win32") return false;
823
+ try {
824
+ process.kill(entry.pid, 0);
825
+ return false;
826
+ } catch (err) {
827
+ return err.code !== "EPERM";
828
+ }
829
+ }
815
830
  return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
816
831
  }
817
832
  /**
@@ -1013,8 +1028,8 @@ async function* spawnStream(opts) {
1013
1028
  try {
1014
1029
  for (; ; ) {
1015
1030
  while (queue.length === 0) {
1016
- await new Promise((resolve17) => {
1017
- waiter = resolve17;
1031
+ await new Promise((resolve18) => {
1032
+ waiter = resolve18;
1018
1033
  });
1019
1034
  }
1020
1035
  const chunk = queue.shift();
@@ -1102,19 +1117,48 @@ import * as Core from "@wrongstack/core/utils";
1102
1117
  function sha256hex(content) {
1103
1118
  return createHash("sha256").update(content, "utf8").digest("hex");
1104
1119
  }
1105
- async function detectPackageManager(cwd) {
1106
- const { stat: stat19 } = await import("node:fs/promises");
1107
- try {
1108
- await stat19(`${cwd}/pnpm-lock.yaml`);
1109
- return "pnpm";
1110
- } catch {
1120
+ async function detectPackageManager(cwd, stopAt) {
1121
+ let dir = path3.resolve(cwd);
1122
+ const stop = stopAt ? path3.resolve(stopAt) : dir;
1123
+ for (; ; ) {
1124
+ const found = await detectPackageManagerInDir(dir);
1125
+ if (found) return found;
1126
+ if (dir === stop) break;
1127
+ const parent = path3.dirname(dir);
1128
+ const relParent = path3.relative(stop, parent);
1129
+ if (parent === dir || relParent.startsWith("..") || path3.isAbsolute(relParent)) break;
1130
+ dir = parent;
1111
1131
  }
1132
+ return "npm";
1133
+ }
1134
+ async function detectPackageManagerInDir(dir) {
1135
+ const fs36 = await import("node:fs/promises");
1112
1136
  try {
1113
- await stat19(`${cwd}/yarn.lock`);
1114
- return "yarn";
1137
+ const raw = await fs36.readFile(path3.join(dir, "package.json"), "utf8");
1138
+ const declared = JSON.parse(raw).packageManager;
1139
+ if (typeof declared === "string") {
1140
+ const name = declared.split("@")[0] ?? "";
1141
+ if (name === "pnpm" || name === "yarn") return name;
1142
+ if (name === "npm" || name === "bun") return "npm";
1143
+ }
1115
1144
  } catch {
1116
1145
  }
1117
- return "npm";
1146
+ const lockfiles = [
1147
+ ["pnpm-lock.yaml", "pnpm"],
1148
+ ["yarn.lock", "yarn"],
1149
+ ["bun.lockb", "npm"],
1150
+ ["bun.lock", "npm"],
1151
+ ["package-lock.json", "npm"],
1152
+ ["npm-shrinkwrap.json", "npm"]
1153
+ ];
1154
+ for (const [file, manager] of lockfiles) {
1155
+ try {
1156
+ await fs36.stat(`${dir}/${file}`);
1157
+ return manager;
1158
+ } catch {
1159
+ }
1160
+ }
1161
+ return null;
1118
1162
  }
1119
1163
  function resolvePath(input, ctx) {
1120
1164
  return path3.isAbsolute(input) ? path3.normalize(input) : path3.resolve(ctx.workingDir ?? ctx.cwd, input);
@@ -1173,6 +1217,20 @@ async function safeResolveReal(input, ctx) {
1173
1217
  const abs = safeResolve(input, ctx);
1174
1218
  return await resolveRealInsideRoot(abs, ctx);
1175
1219
  }
1220
+ function truncateDiffPayload(diff, maxBytes) {
1221
+ const total = Buffer.byteLength(diff, "utf8");
1222
+ if (total <= maxBytes) return { text: diff, truncated: false };
1223
+ const MARKER_RESERVE = 96;
1224
+ let head = takeHeadBytes(diff, Math.max(0, maxBytes - MARKER_RESERVE));
1225
+ const nl = head.lastIndexOf("\n");
1226
+ if (nl > 0) head = head.slice(0, nl);
1227
+ const kept = Buffer.byteLength(head, "utf8");
1228
+ return {
1229
+ text: `${head}
1230
+ \u2026[diff truncated: ${total - kept} of ${total} bytes omitted]`,
1231
+ truncated: true
1232
+ };
1233
+ }
1176
1234
  function truncateMiddle(s, max) {
1177
1235
  if (Buffer.byteLength(s, "utf8") <= max) return s;
1178
1236
  const half = Math.floor(max / 2);
@@ -2735,8 +2793,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, extraIgn
2735
2793
  collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2736
2794
  }
2737
2795
  }
2738
- function collectFileEvidence(directory, fullPath, basename14, profiles, state) {
2739
- const lower = basename14.toLowerCase();
2796
+ function collectFileEvidence(directory, fullPath, basename15, profiles, state) {
2797
+ const lower = basename15.toLowerCase();
2740
2798
  const extension = path4.extname(lower);
2741
2799
  for (const profile of profiles) {
2742
2800
  const detector = profile.detectors.find(
@@ -2747,7 +2805,7 @@ function collectFileEvidence(directory, fullPath, basename14, profiles, state) {
2747
2805
  candidate.evidence.push({
2748
2806
  kind: detector.kind,
2749
2807
  path: fullPath,
2750
- value: basename14,
2808
+ value: basename15,
2751
2809
  weight: detector.weight
2752
2810
  });
2753
2811
  if (detector.kind === "manifest" || detector.kind === "config") {
@@ -2875,8 +2933,8 @@ function normalizeLimits(input) {
2875
2933
  async function canonicalDirectory(input) {
2876
2934
  const resolved = path4.resolve(input);
2877
2935
  const real = await fs2.realpath(resolved);
2878
- const stat19 = await fs2.stat(real);
2879
- if (!stat19.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
2936
+ const stat20 = await fs2.stat(real);
2937
+ if (!stat20.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
2880
2938
  return real;
2881
2939
  }
2882
2940
  async function canonicalInside(input, root, label) {
@@ -3775,8 +3833,8 @@ async function executeInternal(options, startedAt) {
3775
3833
  const target = options.plan.evidence.find((item) => item.kind === "target")?.path;
3776
3834
  if (!target) return unavailableResult(options, "Internal syntax plan has no target evidence.");
3777
3835
  const safeTarget = await assertContainedFile(target, options.projectRoot);
3778
- const stat19 = await fs4.stat(safeTarget);
3779
- if (stat19.size > MAX_INTERNAL_SOURCE_BYTES) {
3836
+ const stat20 = await fs4.stat(safeTarget);
3837
+ if (stat20.size > MAX_INTERNAL_SOURCE_BYTES) {
3780
3838
  return unavailableResult(
3781
3839
  options,
3782
3840
  `Internal syntax target exceeds ${MAX_INTERNAL_SOURCE_BYTES} bytes.`
@@ -4063,8 +4121,8 @@ async function snapshotPaths(paths) {
4063
4121
  const existing = [];
4064
4122
  for (const candidate of paths) {
4065
4123
  try {
4066
- const stat19 = await fs4.stat(candidate);
4067
- if (!stat19.isFile()) continue;
4124
+ const stat20 = await fs4.stat(candidate);
4125
+ if (!stat20.isFile()) continue;
4068
4126
  existing.push(candidate);
4069
4127
  } catch {
4070
4128
  }
@@ -4075,15 +4133,15 @@ async function changedPaths(before, after, beforeSizes, afterSizes) {
4075
4133
  const beforeSet = new Set(before);
4076
4134
  const afterSet = new Set(after);
4077
4135
  const changed = /* @__PURE__ */ new Set();
4078
- for (const path40 of after) {
4079
- if (!beforeSet.has(path40)) changed.add(path40);
4136
+ for (const path41 of after) {
4137
+ if (!beforeSet.has(path41)) changed.add(path41);
4080
4138
  }
4081
- for (const path40 of before) {
4082
- if (!afterSet.has(path40)) changed.add(path40);
4139
+ for (const path41 of before) {
4140
+ if (!afterSet.has(path41)) changed.add(path41);
4083
4141
  }
4084
4142
  if (beforeSizes && afterSizes) {
4085
- for (const path40 of after) {
4086
- if (beforeSizes.get(path40) !== afterSizes.get(path40)) changed.add(path40);
4143
+ for (const path41 of after) {
4144
+ if (beforeSizes.get(path41) !== afterSizes.get(path41)) changed.add(path41);
4087
4145
  }
4088
4146
  }
4089
4147
  return [...changed].sort();
@@ -4092,8 +4150,8 @@ async function snapshotSizes(paths) {
4092
4150
  const sizes = /* @__PURE__ */ new Map();
4093
4151
  for (const candidate of paths) {
4094
4152
  try {
4095
- const stat19 = await fs4.stat(candidate);
4096
- if (stat19.isFile()) sizes.set(candidate, stat19.size);
4153
+ const stat20 = await fs4.stat(candidate);
4154
+ if (stat20.isFile()) sizes.set(candidate, stat20.size);
4097
4155
  } catch {
4098
4156
  }
4099
4157
  }
@@ -5551,7 +5609,7 @@ async function syncGoParse(filePath, content, lang) {
5551
5609
  }
5552
5610
  const goBinary = resolveWin32Command("go");
5553
5611
  const goResult = await new Promise(
5554
- (resolve17, reject) => {
5612
+ (resolve18, reject) => {
5555
5613
  let settled = false;
5556
5614
  const proc = spawn5(goBinary, ["run", scriptPath], {
5557
5615
  stdio: ["pipe", "pipe", "pipe"],
@@ -5580,7 +5638,7 @@ async function syncGoParse(filePath, content, lang) {
5580
5638
  if (settled) return;
5581
5639
  settled = true;
5582
5640
  clearTimeout(timer);
5583
- resolve17({ code: code2, stdout: stdout2 });
5641
+ resolve18({ code: code2, stdout: stdout2 });
5584
5642
  });
5585
5643
  }
5586
5644
  );
@@ -6245,7 +6303,7 @@ async function resolvePython() {
6245
6303
  return null;
6246
6304
  }
6247
6305
  function commandIsAvailable(command) {
6248
- return new Promise((resolve17) => {
6306
+ return new Promise((resolve18) => {
6249
6307
  let settled = false;
6250
6308
  const proc = spawn6(command, ["--version"], {
6251
6309
  stdio: "ignore",
@@ -6255,7 +6313,7 @@ function commandIsAvailable(command) {
6255
6313
  if (settled) return;
6256
6314
  settled = true;
6257
6315
  clearTimeout(timer);
6258
- resolve17(available);
6316
+ resolve18(available);
6259
6317
  };
6260
6318
  const timer = setTimeout(() => {
6261
6319
  proc.kill("SIGKILL");
@@ -6267,7 +6325,7 @@ function commandIsAvailable(command) {
6267
6325
  });
6268
6326
  }
6269
6327
  function spawnPyParser(pyBinary, scriptPath, filePath, content) {
6270
- return new Promise((resolve17, reject) => {
6328
+ return new Promise((resolve18, reject) => {
6271
6329
  let settled = false;
6272
6330
  const proc = spawn6(pyBinary, [scriptPath, filePath], {
6273
6331
  stdio: ["pipe", "pipe", "pipe"],
@@ -6296,7 +6354,7 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
6296
6354
  if (settled) return;
6297
6355
  settled = true;
6298
6356
  clearTimeout(timer);
6299
- resolve17({ code, stdout });
6357
+ resolve18({ code, stdout });
6300
6358
  });
6301
6359
  });
6302
6360
  }
@@ -6705,9 +6763,9 @@ function parseSymbols6(opts) {
6705
6763
  function regexParse2(opts) {
6706
6764
  const { file, content, lang } = opts;
6707
6765
  const symbols = [];
6708
- const basename14 = path22.basename(file).toLowerCase();
6709
- const isPackageJson = basename14 === "package.json";
6710
- const isTsconfig = basename14 === "tsconfig.json" || basename14 === "tsconfig.build.json";
6766
+ const basename15 = path22.basename(file).toLowerCase();
6767
+ const isPackageJson = basename15 === "package.json";
6768
+ const isTsconfig = basename15 === "tsconfig.json" || basename15 === "tsconfig.build.json";
6711
6769
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
6712
6770
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
6713
6771
  const lines = content.split("\n");
@@ -7989,11 +8047,18 @@ function ensureSessionShell(opts = {}) {
7989
8047
  init_spawn_stream();
7990
8048
  init_util();
7991
8049
  init_legacy_bridge();
8050
+ var SEVERITY_RANK = {
8051
+ info: 0,
8052
+ low: 1,
8053
+ moderate: 2,
8054
+ high: 3,
8055
+ critical: 4
8056
+ };
7992
8057
  var auditTool = {
7993
8058
  name: "audit",
7994
8059
  category: "Package Management",
7995
8060
  description: "Run a security audit against project dependencies (using pnpm/npm audit). Reports known vulnerabilities with severity.",
7996
- usageHint: "CRITICAL SECURITY TOOL:\n\n- Run regularly and especially before any release.\n- Use `level` to focus on high/critical issues.\n- `fix` can attempt automatic remediation for some vulnerabilities.\nThis is one of the most important tools for supply chain security.",
8061
+ usageHint: "CRITICAL SECURITY TOOL:\n\n- Run regularly and especially before any release.\n- Use `level` to focus on high/critical issues.\n- This tool is read-only: to remediate, use `install` (or `language_package`) to upgrade the affected packages.\nThis is one of the most important tools for supply chain security.",
7997
8062
  permission: "confirm",
7998
8063
  mutating: false,
7999
8064
  capabilities: ["shell.restricted"],
@@ -8008,8 +8073,10 @@ var auditTool = {
8008
8073
  enum: ["low", "moderate", "high", "critical"],
8009
8074
  description: "Minimum severity level to report"
8010
8075
  },
8011
- fix: { type: "boolean", description: "Attempt to fix vulnerabilities (default: false)" },
8012
- packages: { type: "string", description: "Specific package(s) to audit (comma-separated)" }
8076
+ fix: {
8077
+ type: "boolean",
8078
+ description: "Deprecated and rejected \u2014 this tool is read-only and never modifies dependencies. Use `install` (or `language_package`) to remediate vulnerabilities."
8079
+ }
8013
8080
  }
8014
8081
  },
8015
8082
  async execute(input, ctx, opts) {
@@ -8023,6 +8090,11 @@ var auditTool = {
8023
8090
  return final;
8024
8091
  },
8025
8092
  async *executeStream(input, ctx, opts) {
8093
+ if (input.fix === true) {
8094
+ throw new Error(
8095
+ "audit: `fix: true` is not supported \u2014 this tool is read-only (mutating: false). To remediate vulnerabilities, upgrade the affected packages with the `install` tool (or `language_package` for non-JS ecosystems)."
8096
+ );
8097
+ }
8026
8098
  const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
8027
8099
  const bridge = await tryLegacyPackageOperation("package-audit", {
8028
8100
  cwd,
@@ -8052,13 +8124,11 @@ var auditTool = {
8052
8124
  };
8053
8125
  return;
8054
8126
  }
8055
- const manager = await detectPackageManager(cwd);
8127
+ const manager = await detectPackageManager(cwd, ctx.projectRoot);
8056
8128
  yield { type: "log", text: `Auditing with ${manager}\u2026`, data: { manager } };
8057
8129
  const args = ["audit", "--json"];
8058
- if (input.fix) args.push("--fix");
8059
- if (input.packages) {
8060
- const pkgs = Array.isArray(input.packages) ? input.packages : input.packages.split(",");
8061
- args.push(...pkgs.map((p) => p.trim()));
8130
+ if (input.level && (manager === "npm" || manager === "pnpm")) {
8131
+ args.push(`--audit-level=${input.level}`);
8062
8132
  }
8063
8133
  const result = yield* spawnStream({
8064
8134
  cmd: manager,
@@ -8067,10 +8137,16 @@ var auditTool = {
8067
8137
  signal: opts.signal,
8068
8138
  maxBytes: 1e5
8069
8139
  });
8070
- yield { type: "final", output: parseAuditOutput(result.stdout, result.exitCode) };
8140
+ yield {
8141
+ type: "final",
8142
+ output: parseAuditOutput(result.stdout, result.exitCode, {
8143
+ level: input.level,
8144
+ spawnTruncated: result.truncated
8145
+ })
8146
+ };
8071
8147
  }
8072
8148
  };
8073
- function parseAuditOutput(json2, exitCode) {
8149
+ function parseAuditOutput(json2, exitCode, opts = {}) {
8074
8150
  if (!json2) {
8075
8151
  return {
8076
8152
  exit_code: exitCode,
@@ -8081,18 +8157,14 @@ function parseAuditOutput(json2, exitCode) {
8081
8157
  truncated: false
8082
8158
  };
8083
8159
  }
8160
+ const cappedOutput = normalizeCommandOutput(json2);
8161
+ const truncated = opts.spawnTruncated === true || Buffer.byteLength(json2, "utf8") > COMMAND_OUTPUT_MAX_BYTES;
8084
8162
  try {
8085
8163
  const data = JSON.parse(json2);
8086
- const advisories = [];
8087
- const ads = data.advisories ?? {};
8088
- for (const id of Object.keys(ads)) {
8089
- const adv = ads[id];
8090
- advisories.push({
8091
- severity: adv.severity ?? "unknown",
8092
- package: adv.module_name ?? id,
8093
- title: adv.title ?? "Unknown vulnerability",
8094
- url: adv.url ?? ""
8095
- });
8164
+ let advisories = extractAdvisories(data);
8165
+ const minRank = opts.level ? SEVERITY_RANK[opts.level] ?? 0 : 0;
8166
+ if (minRank > 0) {
8167
+ advisories = advisories.filter((a) => (SEVERITY_RANK[a.severity] ?? 0) >= minRank);
8096
8168
  }
8097
8169
  const total = advisories.length;
8098
8170
  const summary = total === 0 ? "No vulnerabilities found" : `Found ${total} vulnerabilities: ${advisories.filter((a) => a.severity === "critical").length} critical, ${advisories.filter((a) => a.severity === "high").length} high`;
@@ -8101,8 +8173,8 @@ function parseAuditOutput(json2, exitCode) {
8101
8173
  vulnerabilities: advisories,
8102
8174
  total,
8103
8175
  summary,
8104
- output: json2,
8105
- truncated: json2.length > 1e5
8176
+ output: cappedOutput,
8177
+ truncated
8106
8178
  };
8107
8179
  } catch {
8108
8180
  return {
@@ -8110,15 +8182,47 @@ function parseAuditOutput(json2, exitCode) {
8110
8182
  vulnerabilities: [],
8111
8183
  total: 0,
8112
8184
  summary: "Could not parse audit output",
8113
- output: json2,
8114
- truncated: false
8185
+ output: cappedOutput,
8186
+ truncated
8115
8187
  };
8116
8188
  }
8117
8189
  }
8190
+ function extractAdvisories(data) {
8191
+ const advisories = [];
8192
+ const ads = data["advisories"];
8193
+ if (ads && typeof ads === "object") {
8194
+ for (const [id, value] of Object.entries(ads)) {
8195
+ const adv = value ?? {};
8196
+ advisories.push({
8197
+ severity: typeof adv["severity"] === "string" ? adv["severity"] : "unknown",
8198
+ package: typeof adv["module_name"] === "string" ? adv["module_name"] : id,
8199
+ title: typeof adv["title"] === "string" ? adv["title"] : "Unknown vulnerability",
8200
+ url: typeof adv["url"] === "string" ? adv["url"] : ""
8201
+ });
8202
+ }
8203
+ return advisories;
8204
+ }
8205
+ const vulns = data["vulnerabilities"];
8206
+ if (vulns && typeof vulns === "object") {
8207
+ for (const [pkg, value] of Object.entries(vulns)) {
8208
+ const vuln = value ?? {};
8209
+ const via = Array.isArray(vuln["via"]) ? vuln["via"] : [];
8210
+ const detail = via.find((v) => !!v && typeof v === "object");
8211
+ advisories.push({
8212
+ severity: typeof vuln["severity"] === "string" ? vuln["severity"] : "unknown",
8213
+ package: pkg,
8214
+ title: detail && typeof detail["title"] === "string" ? detail["title"] : "Unknown vulnerability",
8215
+ url: detail && typeof detail["url"] === "string" ? detail["url"] : ""
8216
+ });
8217
+ }
8218
+ }
8219
+ return advisories;
8220
+ }
8118
8221
 
8119
8222
  // src/bash.ts
8120
8223
  import { spawn as spawn3 } from "node:child_process";
8121
8224
  import * as os4 from "node:os";
8225
+ import { StringDecoder } from "node:string_decoder";
8122
8226
  import {
8123
8227
  emitProcessCompleted as emitProcessCompleted2,
8124
8228
  emitProcessOutput as emitProcessOutput2,
@@ -8399,7 +8503,6 @@ var PersistentProcessRegistry = class {
8399
8503
  try {
8400
8504
  const data = await readRegistryFile(this.registryPath);
8401
8505
  data.instances.set(String(entry.pid), entry);
8402
- const child = null;
8403
8506
  this.baseRegistry.register({
8404
8507
  pid: entry.pid,
8405
8508
  name: entry.name,
@@ -8407,7 +8510,7 @@ var PersistentProcessRegistry = class {
8407
8510
  startedAt: entry.startedAt,
8408
8511
  sessionId: entry.sessionId,
8409
8512
  protected: entry.protected,
8410
- child
8513
+ child: null
8411
8514
  });
8412
8515
  await writeRegistryFile(this.registryPath, data);
8413
8516
  } finally {
@@ -8973,7 +9076,7 @@ function looksLikePowerShell(command) {
8973
9076
  return true;
8974
9077
  }
8975
9078
  if (PS_VERB_RE.test(trimmed)) return true;
8976
- if (/(?:^|[\s;&|])(gci|gi|gp|gcm|gps|sl|rm|cat|cp|mv)\b/i.test(trimmed)) {
9079
+ if (/(?:^|[\s;&|])(gci|gi|gp|gcm|gps)\b/i.test(trimmed)) {
8977
9080
  return true;
8978
9081
  }
8979
9082
  if (looksLikePowerShellExtended(command)) return true;
@@ -9065,7 +9168,7 @@ var bashTool = {
9065
9168
  name: "bash",
9066
9169
  category: "Shell",
9067
9170
  description: "Execute an arbitrary command in the user's default shell (bash/zsh/pwsh/cmd). stdout and stderr are merged into one stream. This is the most powerful and dangerous tool \u2014 it gives the model full access to the developer's machine. Prefer specialized tools whenever possible.",
9068
- usageHint: "SECURITY WARNING: This tool runs with the full privileges of the current user.\n\nBest practices for the model:\n- Strongly prefer `exec` for known safe commands (node, npm, pnpm, tsc, git, etc.).\n- Use bash only when you genuinely need shell features (pipes, redirection, complex one-liners).\n- Prefer single focused commands over huge `&&` chains.\n- Use `background: true` only for long-running processes (dev servers, watchers).\n- The working directory is the project root.\n- Output may be truncated in the middle for very large results.",
9171
+ usageHint: "SECURITY WARNING: This tool runs with the full privileges of the current user.\n\nBest practices for the model:\n- Strongly prefer `exec` for known safe commands (node, npm, pnpm, tsc, git, etc.).\n- Use bash only when you genuinely need shell features (pipes, redirection, complex one-liners).\n- Prefer single focused commands over huge `&&` chains.\n- Use `background: true` only for long-running processes (dev servers, watchers).\n- The working directory is the session working dir (changed via `set_working_dir`), defaulting to the project root.\n- Output may be truncated in the middle for very large results.",
9069
9172
  selection: {
9070
9173
  doNotUseWhen: "the command is allowlisted and does not require pipes, redirection, or shell expansion.",
9071
9174
  useInstead: ["exec"]
@@ -9079,7 +9182,14 @@ var bashTool = {
9079
9182
  // explicitly removes the implicit cross-tool aliasing.
9080
9183
  subjectKey: "command",
9081
9184
  capabilities: ["shell.arbitrary"],
9082
- timeoutMs: 3e5,
9185
+ // Executor-level abort ceiling. Must sit ABOVE the per-call `timeout_ms`
9186
+ // ceiling (600_000): the tool's own timer tree-kills and returns a
9187
+ // structured `timed_out: true` result, while the executor's
9188
+ // AbortSignal.timeout is a blunt abort. The old value (300_000) meant any
9189
+ // timeout_ms > 5min was silently cut short by the executor. The 10s margin
9190
+ // covers the kill/teardown window. (The executor additionally clamps to
9191
+ // config `tools.maxToolTimeoutMs`.)
9192
+ timeoutMs: 61e4,
9083
9193
  maxOutputBytes: MAX_OUTPUT,
9084
9194
  estimatedDurationMs: 3e4,
9085
9195
  inputSchema: {
@@ -9091,7 +9201,7 @@ var bashTool = {
9091
9201
  },
9092
9202
  timeout_ms: {
9093
9203
  type: "integer",
9094
- description: "Optional timeout for this specific command in milliseconds."
9204
+ description: "Optional timeout for this specific command in milliseconds (default 300000, max 600000)."
9095
9205
  },
9096
9206
  background: {
9097
9207
  type: "boolean",
@@ -9142,16 +9252,7 @@ var bashTool = {
9142
9252
  return;
9143
9253
  }
9144
9254
  const PIPE_TO_SHELL_PATTERN = /\|\s*(sh|bash|ksh|zsh|fish|cmd|powershell|pwsh)/i;
9145
- if (PIPE_TO_SHELL_PATTERN.test(input.command)) {
9146
- console.warn(JSON.stringify({
9147
- level: "warn",
9148
- event: "bash.pipe_to_shell_detected",
9149
- message: "Detected pipe-to-shell pattern. Consider reviewing the full command before confirming.",
9150
- command_prefix: input.command.slice(0, 100),
9151
- // Log first 100 chars for review
9152
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
9153
- }));
9154
- }
9255
+ const pipeToShellNote = PIPE_TO_SHELL_PATTERN.test(input.command) ? "\n\n[wrongstack] Caution: this command pipes output into a shell interpreter (pipe-to-shell). Piped content executes as arbitrary code \u2014 review the source before trusting the result, and prefer downloading to a file and inspecting it first." : "";
9155
9256
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS2, 6e5));
9156
9257
  const isWin5 = os4.platform() === "win32";
9157
9258
  let plan;
@@ -9185,11 +9286,12 @@ var bashTool = {
9185
9286
  const shell = plan.bin;
9186
9287
  const args = plan.useStdin ? [...plan.argv] : [...plan.argv, input.command];
9187
9288
  const env = buildChildEnv2(ctx.session?.id);
9289
+ const spawnCwd = ctx.workingDir ?? ctx.projectRoot;
9188
9290
  const detached = !isWin5;
9189
9291
  const startedAt = Date.now();
9190
9292
  if (input.background) {
9191
9293
  const child2 = spawn3(shell, args, {
9192
- cwd: ctx.projectRoot,
9294
+ cwd: spawnCwd,
9193
9295
  env,
9194
9296
  // PowerShell takes the script on stdin (no argv quoting); cmd.exe
9195
9297
  // and POSIX shells ignore stdin when given the command inline.
@@ -9220,7 +9322,7 @@ var bashTool = {
9220
9322
  parentPid: process.pid,
9221
9323
  command: redactCommand(`${shell} ${args.join(" ")}`),
9222
9324
  args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
9223
- cwd: ctx.projectRoot,
9325
+ cwd: spawnCwd,
9224
9326
  background: true,
9225
9327
  startedAt: new Date(startedAt).toISOString()
9226
9328
  });
@@ -9264,7 +9366,9 @@ var bashTool = {
9264
9366
  yield {
9265
9367
  type: "final",
9266
9368
  output: {
9267
- output: "",
9369
+ // Background runs have no captured output; the pipe-to-shell caution
9370
+ // (when present) is the only thing worth surfacing.
9371
+ output: pipeToShellNote.trim(),
9268
9372
  exit_code: null,
9269
9373
  timed_out: false,
9270
9374
  pid: pid2
@@ -9281,7 +9385,7 @@ var bashTool = {
9281
9385
  return;
9282
9386
  }
9283
9387
  const child = spawn3(shell, args, {
9284
- cwd: ctx.projectRoot,
9388
+ cwd: spawnCwd,
9285
9389
  env,
9286
9390
  // PowerShell takes the script on stdin (no argv quoting); cmd.exe
9287
9391
  // and POSIX shells ignore stdin when given the command inline.
@@ -9306,7 +9410,7 @@ var bashTool = {
9306
9410
  parentPid: process.pid,
9307
9411
  command: redactCommand(`${shell} ${args.join(" ")}`),
9308
9412
  args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
9309
- cwd: ctx.projectRoot,
9413
+ cwd: spawnCwd,
9310
9414
  background: false,
9311
9415
  startedAt: new Date(startedAt).toISOString()
9312
9416
  });
@@ -9397,10 +9501,10 @@ var bashTool = {
9397
9501
  queue.push(c);
9398
9502
  }
9399
9503
  };
9400
- const next = () => new Promise((resolve17) => {
9504
+ const next = () => new Promise((resolve18) => {
9401
9505
  const c = queue.shift();
9402
- if (c) resolve17(c);
9403
- else resolveNext = resolve17;
9506
+ if (c) resolve18(c);
9507
+ else resolveNext = resolve18;
9404
9508
  });
9405
9509
  let lastFlush = Date.now();
9406
9510
  const flush = () => {
@@ -9425,8 +9529,10 @@ var bashTool = {
9425
9529
  child.stderr?.resume();
9426
9530
  }
9427
9531
  };
9532
+ const stdoutDecoder = new StringDecoder("utf8");
9533
+ const stderrDecoder = new StringDecoder("utf8");
9428
9534
  const onData = (chunk, stream) => {
9429
- const text = chunk.toString();
9535
+ const text = (stream === "stdout" ? stdoutDecoder : stderrDecoder).write(chunk);
9430
9536
  if (stream === "stdout") stdoutBytes += chunk.byteLength;
9431
9537
  else stderrBytes += chunk.byteLength;
9432
9538
  emitProcessOutput2({ pid, stream, chunk });
@@ -9453,6 +9559,12 @@ var bashTool = {
9453
9559
  if (typeof pid === "number") registry.unregister(pid);
9454
9560
  registry.afterCall(Date.now() - startedAt, code !== 0 && code !== null);
9455
9561
  completeForeground(timedOut ? 124 : code ?? (signal ? 1 : 0), signal ?? void 0);
9562
+ const tail = stdoutDecoder.end() + stderrDecoder.end();
9563
+ if (tail) {
9564
+ if (buf.length < MAX_OUTPUT) buf += tail.slice(0, MAX_OUTPUT - buf.length);
9565
+ spool.write(tail);
9566
+ pending2 += tail;
9567
+ }
9456
9568
  push({ kind: "end", code });
9457
9569
  });
9458
9570
  try {
@@ -9472,7 +9584,7 @@ var bashTool = {
9472
9584
  output: {
9473
9585
  output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : "") + (hint ? `
9474
9586
 
9475
- ${hint}` : ""),
9587
+ ${hint}` : "") + pipeToShellNote,
9476
9588
  exit_code: c.code,
9477
9589
  timed_out: timedOut
9478
9590
  }
@@ -9534,7 +9646,7 @@ ${hint}` : ""),
9534
9646
  if (!sessionId) return;
9535
9647
  for (const entry of registry.bySession(sessionId)) {
9536
9648
  if (entry.name !== "bash") continue;
9537
- if (entry.child.exitCode !== null) continue;
9649
+ if (entry.child && entry.child.exitCode !== null) continue;
9538
9650
  if (entry.background) continue;
9539
9651
  if (entry.protected) continue;
9540
9652
  registry.kill(entry.pid, { force: true });
@@ -9769,8 +9881,8 @@ function sweepOldArtifacts(root) {
9769
9881
  for (const name of names) {
9770
9882
  const target = path10.join(dir, name);
9771
9883
  try {
9772
- const stat19 = await fs7.stat(target);
9773
- if (stat19.isFile() && stat19.mtimeMs < cutoff) {
9884
+ const stat20 = await fs7.stat(target);
9885
+ if (stat20.isFile() && stat20.mtimeMs < cutoff) {
9774
9886
  await fs7.rm(target, { force: true });
9775
9887
  removed++;
9776
9888
  }
@@ -9798,14 +9910,14 @@ var BrowserArtifactStore = class {
9798
9910
  }
9799
9911
  async record(id, sessionId, kind, target, mimeType) {
9800
9912
  await fs7.chmod(target, 384).catch(() => void 0);
9801
- const [stat19, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
9913
+ const [stat20, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
9802
9914
  const artifact = {
9803
9915
  id,
9804
9916
  kind,
9805
9917
  sensitivity: "sensitive",
9806
9918
  path: target,
9807
9919
  mimeType,
9808
- sizeBytes: stat19.size,
9920
+ sizeBytes: stat20.size,
9809
9921
  sha256,
9810
9922
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
9811
9923
  };
@@ -9845,11 +9957,11 @@ var BrowserArtifactStore = class {
9845
9957
  };
9846
9958
  async function hashFile(target) {
9847
9959
  const hash = createHash3("sha256");
9848
- await new Promise((resolve17, reject) => {
9960
+ await new Promise((resolve18, reject) => {
9849
9961
  const stream = createReadStream(target);
9850
9962
  stream.on("data", (chunk) => hash.update(chunk));
9851
9963
  stream.once("error", reject);
9852
- stream.once("end", resolve17);
9964
+ stream.once("end", resolve18);
9853
9965
  });
9854
9966
  return hash.digest("hex");
9855
9967
  }
@@ -10004,7 +10116,7 @@ var BrowserNetworkGuardProxy = class {
10004
10116
  async start() {
10005
10117
  if (this.url) return this.url;
10006
10118
  if (this.startPromise) return this.startPromise;
10007
- this.startPromise = new Promise((resolve17, reject) => {
10119
+ this.startPromise = new Promise((resolve18, reject) => {
10008
10120
  const onError = (error) => {
10009
10121
  this.server.off("listening", onListening);
10010
10122
  reject(error);
@@ -10017,7 +10129,7 @@ var BrowserNetworkGuardProxy = class {
10017
10129
  return;
10018
10130
  }
10019
10131
  this.url = `http://127.0.0.1:${address.port}`;
10020
- resolve17(this.url);
10132
+ resolve18(this.url);
10021
10133
  };
10022
10134
  this.server.once("error", onError);
10023
10135
  this.server.once("listening", onListening);
@@ -10032,7 +10144,7 @@ var BrowserNetworkGuardProxy = class {
10032
10144
  for (const socket of this.sockets) socket.destroy();
10033
10145
  this.sockets.clear();
10034
10146
  if (!this.server.listening) return;
10035
- await new Promise((resolve17) => this.server.close(() => resolve17()));
10147
+ await new Promise((resolve18) => this.server.close(() => resolve18()));
10036
10148
  }
10037
10149
  async forwardHttp(request2, response) {
10038
10150
  try {
@@ -10062,8 +10174,8 @@ var BrowserNetworkGuardProxy = class {
10062
10174
  upstream.on("error", () => writeProxyError(response, 502, "Bad Gateway"));
10063
10175
  request2.on("aborted", () => upstream.destroy());
10064
10176
  request2.pipe(upstream);
10065
- } catch {
10066
- writeProxyError(response, 403, "Blocked by browser network policy");
10177
+ } catch (error) {
10178
+ writeProxyError(response, 403, policyBlockMessage(error));
10067
10179
  }
10068
10180
  }
10069
10181
  async forwardConnect(request2, client, head) {
@@ -10087,8 +10199,8 @@ var BrowserNetworkGuardProxy = class {
10087
10199
  pipeDuplexPair(upstream, client);
10088
10200
  });
10089
10201
  client.once("close", () => upstream.destroy());
10090
- } catch {
10091
- client.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
10202
+ } catch (error) {
10203
+ client.end(rawForbiddenResponse(policyBlockMessage(error)));
10092
10204
  }
10093
10205
  }
10094
10206
  async forwardUpgrade(request2, client, head) {
@@ -10129,9 +10241,9 @@ var BrowserNetworkGuardProxy = class {
10129
10241
  });
10130
10242
  client.once("close", () => upstream?.destroy());
10131
10243
  upstream.end();
10132
- } catch {
10244
+ } catch (error) {
10133
10245
  upstream?.destroy();
10134
- client.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
10246
+ client.end(rawForbiddenResponse(policyBlockMessage(error)));
10135
10247
  }
10136
10248
  }
10137
10249
  resolve(rawUrl) {
@@ -10171,6 +10283,18 @@ function writeProxyError(response, status, message) {
10171
10283
  response.writeHead(status, { "content-type": "text/plain", connection: "close" });
10172
10284
  response.end(message);
10173
10285
  }
10286
+ function policyBlockMessage(error) {
10287
+ const reason = error instanceof Error ? error.message : String(error);
10288
+ return reason ? `Blocked by browser network policy: ${reason}` : "Blocked by browser network policy";
10289
+ }
10290
+ function rawForbiddenResponse(message) {
10291
+ return `HTTP/1.1 403 Forbidden\r
10292
+ Content-Type: text/plain\r
10293
+ Content-Length: ${Buffer.byteLength(message)}\r
10294
+ Connection: close\r
10295
+ \r
10296
+ ` + message;
10297
+ }
10174
10298
 
10175
10299
  // src/browser/manager.ts
10176
10300
  var DEFAULT_OPERATION_TIMEOUT_MS = 3e4;
@@ -10398,13 +10522,21 @@ var BrowserSessionManager = class {
10398
10522
  if (relative13.startsWith("..") || path11.isAbsolute(relative13)) {
10399
10523
  throw new Error("browser: upload files must stay inside the project root");
10400
10524
  }
10401
- const realFile = await fs8.realpath(absolute);
10525
+ let realFile;
10526
+ try {
10527
+ realFile = await fs8.realpath(absolute);
10528
+ } catch (error) {
10529
+ if (error?.code === "ENOENT") {
10530
+ throw new Error(`browser: upload file not found: ${file}`);
10531
+ }
10532
+ throw error;
10533
+ }
10402
10534
  const realRelative = path11.relative(realRoot, realFile);
10403
10535
  if (realRelative.startsWith("..") || path11.isAbsolute(realRelative)) {
10404
10536
  throw new Error("browser: upload files must not escape the project root through a symlink");
10405
10537
  }
10406
- const stat19 = await fs8.stat(realFile);
10407
- if (!stat19.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
10538
+ const stat20 = await fs8.stat(realFile);
10539
+ if (!stat20.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
10408
10540
  resolved.push(realFile);
10409
10541
  }
10410
10542
  await this.runPageOperation(
@@ -10585,7 +10717,7 @@ function pushBounded(target, value, limit) {
10585
10717
  }
10586
10718
  async function abortable(signal, operation, onAbort) {
10587
10719
  signal.throwIfAborted();
10588
- return new Promise((resolve17, reject) => {
10720
+ return new Promise((resolve18, reject) => {
10589
10721
  let settled = false;
10590
10722
  let aborting = false;
10591
10723
  const finish = (fn) => {
@@ -10603,7 +10735,7 @@ async function abortable(signal, operation, onAbort) {
10603
10735
  signal.addEventListener("abort", abort, { once: true });
10604
10736
  operation().then(
10605
10737
  (value) => {
10606
- if (!aborting) finish(() => resolve17(value));
10738
+ if (!aborting) finish(() => resolve18(value));
10607
10739
  },
10608
10740
  (err) => {
10609
10741
  if (!aborting) finish(() => reject(err));
@@ -10663,7 +10795,7 @@ var sessionIdSchema = {
10663
10795
  };
10664
10796
  var browserOpenTool = {
10665
10797
  name: "browser_open",
10666
- description: "Open an isolated first-party Playwright browser session, optionally navigating to a URL.",
10798
+ description: "Open an isolated first-party Playwright browser session, optionally navigating to a URL. Private/localhost origins are blocked by default; enable specific origins via the WRONGSTACK_BROWSER_PRIVATE_ORIGINS env allowlist.",
10667
10799
  usageHint: "browser_open({ url?, width?, height?, trace? })",
10668
10800
  permission: "confirm",
10669
10801
  mutating: true,
@@ -10716,7 +10848,7 @@ var browserStatusTool = {
10716
10848
  };
10717
10849
  var browserNavigateTool = {
10718
10850
  name: "browser_navigate",
10719
- description: "Navigate an owned browser session to an approved http(s) URL.",
10851
+ description: "Navigate an owned browser session to an approved http(s) URL. Private/localhost origins are blocked by default; enable specific origins via the WRONGSTACK_BROWSER_PRIVATE_ORIGINS env allowlist.",
10720
10852
  usageHint: "browser_navigate({ sessionId, url })",
10721
10853
  permission: "confirm",
10722
10854
  mutating: true,
@@ -11060,7 +11192,7 @@ async function shutdownBrowserTools() {
11060
11192
  import { spawn as spawn4 } from "node:child_process";
11061
11193
  import * as fs13 from "node:fs";
11062
11194
  import * as net3 from "node:net";
11063
- import { StringDecoder } from "node:string_decoder";
11195
+ import { StringDecoder as StringDecoder2 } from "node:string_decoder";
11064
11196
  import { fileURLToPath as fileURLToPath2 } from "node:url";
11065
11197
  import { checkUnixSocketPath } from "@wrongstack/core/utils";
11066
11198
 
@@ -11170,7 +11302,6 @@ import * as fs12 from "node:fs";
11170
11302
  import * as os5 from "node:os";
11171
11303
  import * as path17 from "node:path";
11172
11304
  import { fileURLToPath } from "node:url";
11173
- import { assertUnixSocketPathWithinLimit } from "@wrongstack/core/utils";
11174
11305
 
11175
11306
  // src/codebase-index/writer.ts
11176
11307
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
@@ -14105,12 +14236,12 @@ function projectIndexServerBuildId(entrypoint) {
14105
14236
  const cleanHref = href.split(/[?#]/, 1)[0] ?? href;
14106
14237
  const file = cleanHref.startsWith("file:") ? fileURLToPath(cleanHref) : path17.resolve(cleanHref);
14107
14238
  try {
14108
- const stat19 = fs12.statSync(file);
14109
- if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat19.mtimeMs && buildIdCache.size === stat19.size) {
14239
+ const stat20 = fs12.statSync(file);
14240
+ if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat20.mtimeMs && buildIdCache.size === stat20.size) {
14110
14241
  return buildIdCache.buildId;
14111
14242
  }
14112
14243
  const buildId = createHash4("sha256").update(fs12.readFileSync(file)).digest("hex").slice(0, 24);
14113
- buildIdCache = { file, mtimeMs: stat19.mtimeMs, size: stat19.size, buildId };
14244
+ buildIdCache = { file, mtimeMs: stat20.mtimeMs, size: stat20.size, buildId };
14114
14245
  return buildId;
14115
14246
  } catch {
14116
14247
  return `unreadable:${path17.basename(file)}`;
@@ -14252,8 +14383,8 @@ function isProjectIndexServerHealth(value) {
14252
14383
  return typeof health.checkedAt === "number" && typeof health.uptimeMs === "number" && typeof memory?.rss === "number" && typeof memory.heapUsed === "number" && typeof memory.heapTotal === "number" && typeof memory.external === "number" && typeof health.clients === "number" && typeof health.activeRequests === "number" && typeof health.activeWrites === "number" && typeof health.queuedWrites === "number" && typeof health.pendingExternalFiles === "number" && typeof health.watchingExternal === "boolean" && typeof activity?.indexing === "boolean" && typeof activity.currentFile === "number" && typeof activity.totalFiles === "number" && typeof activity.generation === "number";
14253
14384
  }
14254
14385
  function delay(ms) {
14255
- return new Promise((resolve17) => {
14256
- const timer = setTimeout(resolve17, ms);
14386
+ return new Promise((resolve18) => {
14387
+ const timer = setTimeout(resolve18, ms);
14257
14388
  timer.unref?.();
14258
14389
  });
14259
14390
  }
@@ -14457,7 +14588,7 @@ var ProjectServerConnection = class {
14457
14588
  return Promise.reject(new Error("codebase-index server connection is not available"));
14458
14589
  }
14459
14590
  const id = this.nextId++;
14460
- return new Promise((resolve17, reject) => {
14591
+ return new Promise((resolve18, reject) => {
14461
14592
  const timer = setTimeout(() => {
14462
14593
  const entry = this.pending.get(id);
14463
14594
  if (!entry) return;
@@ -14480,7 +14611,7 @@ var ProjectServerConnection = class {
14480
14611
  entry.reject(cancellationError(signal));
14481
14612
  } : void 0;
14482
14613
  this.pending.set(id, {
14483
- resolve: resolve17,
14614
+ resolve: resolve18,
14484
14615
  reject,
14485
14616
  timer,
14486
14617
  signal,
@@ -14549,7 +14680,7 @@ var ProjectServerConnection = class {
14549
14680
  this.binaryBuffer = [];
14550
14681
  this.useBinary = false;
14551
14682
  this.textDecoder = null;
14552
- return new Promise((resolve17, reject) => {
14683
+ return new Promise((resolve18, reject) => {
14553
14684
  const socket = net3.createConnection(this.endpoint);
14554
14685
  this.socket = socket;
14555
14686
  const timer = setTimeout(() => {
@@ -14561,7 +14692,7 @@ var ProjectServerConnection = class {
14561
14692
  clearTimeout(timer);
14562
14693
  this.connectResolve = null;
14563
14694
  this.connectReject = null;
14564
- resolve17();
14695
+ resolve18();
14565
14696
  };
14566
14697
  const finishReject = (error) => {
14567
14698
  clearTimeout(timer);
@@ -14584,7 +14715,7 @@ var ProjectServerConnection = class {
14584
14715
  this.onBinaryData(socket, chunk);
14585
14716
  return;
14586
14717
  }
14587
- if (!this.textDecoder) this.textDecoder = new StringDecoder("utf8");
14718
+ if (!this.textDecoder) this.textDecoder = new StringDecoder2("utf8");
14588
14719
  this.buffer += this.textDecoder.write(chunk);
14589
14720
  while (true) {
14590
14721
  const newline = this.buffer.indexOf("\n");
@@ -15609,9 +15740,9 @@ var ParserWorkerPool = class {
15609
15740
  for (let i = 0; i < files.length; i++) {
15610
15741
  chunks[i % workerCount].push(files[i]);
15611
15742
  }
15612
- return new Promise((resolve17, reject) => {
15743
+ return new Promise((resolve18, reject) => {
15613
15744
  this.pending.set(batchId, {
15614
- resolve: resolve17,
15745
+ resolve: resolve18,
15615
15746
  reject,
15616
15747
  accumulated: [],
15617
15748
  expectedWorkers: workerCount,
@@ -15642,10 +15773,10 @@ var ParserWorkerPool = class {
15642
15773
  await Promise.allSettled(
15643
15774
  workers.map(
15644
15775
  (w) => Promise.race([
15645
- new Promise((resolve17) => {
15646
- w.once("exit", () => resolve17());
15776
+ new Promise((resolve18) => {
15777
+ w.once("exit", () => resolve18());
15647
15778
  }),
15648
- new Promise((resolve17) => setTimeout(() => resolve17(), 2e3))
15779
+ new Promise((resolve18) => setTimeout(() => resolve18(), 2e3))
15649
15780
  ]).then(() => {
15650
15781
  if (!w.threadId) return;
15651
15782
  return w.terminate().catch(() => {
@@ -15709,7 +15840,7 @@ function shouldUseParserWorkerPool(candidateFileCount, parseBatchCount) {
15709
15840
  return !isFrugalPerf() && candidateFileCount >= WORKER_POOL_THRESHOLD && parseBatchCount > 1;
15710
15841
  }
15711
15842
  function yieldEventLoop() {
15712
- return new Promise((resolve17) => setImmediate(resolve17));
15843
+ return new Promise((resolve18) => setImmediate(resolve18));
15713
15844
  }
15714
15845
  function throwIfAborted(signal) {
15715
15846
  if (!signal?.aborted) return;
@@ -15737,7 +15868,7 @@ function normalizeComparablePath(value) {
15737
15868
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
15738
15869
  }
15739
15870
  function gitOutput(projectRoot, args) {
15740
- return new Promise((resolve17, reject) => {
15871
+ return new Promise((resolve18, reject) => {
15741
15872
  execFile(
15742
15873
  "git",
15743
15874
  ["-C", projectRoot, ...args],
@@ -15748,7 +15879,7 @@ function gitOutput(projectRoot, args) {
15748
15879
  },
15749
15880
  (error, stdout) => {
15750
15881
  if (error) reject(error);
15751
- else resolve17(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
15882
+ else resolve18(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
15752
15883
  }
15753
15884
  );
15754
15885
  });
@@ -15980,9 +16111,9 @@ async function runIndexerWithStore(store, opts) {
15980
16111
  const statReadParse = await Promise.allSettled(
15981
16112
  batchFiles.map(
15982
16113
  async (file) => {
15983
- let stat19;
16114
+ let stat20;
15984
16115
  try {
15985
- stat19 = await fs18.stat(file, statOpts);
16116
+ stat20 = await fs18.stat(file, statOpts);
15986
16117
  } catch (e) {
15987
16118
  if (isAbortError(e)) throw e;
15988
16119
  return {
@@ -15994,21 +16125,21 @@ async function runIndexerWithStore(store, opts) {
15994
16125
  missing: isMissingPathError(e)
15995
16126
  };
15996
16127
  }
15997
- if (!stat19.isFile()) return { file, stat: stat19, lang: "", parsed: null };
16128
+ if (!stat20.isFile()) return { file, stat: stat20, lang: "", parsed: null };
15998
16129
  const lang = detectLang(file);
15999
- if (!lang) return { file, stat: stat19, lang: "", parsed: null };
16000
- if (stat19.size > MAX_INDEX_FILE_BYTES) {
16130
+ if (!lang) return { file, stat: stat20, lang: "", parsed: null };
16131
+ if (stat20.size > MAX_INDEX_FILE_BYTES) {
16001
16132
  return {
16002
16133
  file,
16003
- stat: stat19,
16134
+ stat: stat20,
16004
16135
  lang,
16005
16136
  parsed: null,
16006
- error: `file too large (${stat19.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
16137
+ error: `file too large (${stat20.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
16007
16138
  };
16008
16139
  }
16009
16140
  const meta = existingMeta.get(file);
16010
- if (!force && meta && meta.mtimeMs === Math.floor(stat19.mtimeMs)) {
16011
- return { file, stat: stat19, lang, parsed: null, skippedMeta: meta };
16141
+ if (!force && meta && meta.mtimeMs === Math.floor(stat20.mtimeMs)) {
16142
+ return { file, stat: stat20, lang, parsed: null, skippedMeta: meta };
16012
16143
  }
16013
16144
  let content;
16014
16145
  try {
@@ -16017,7 +16148,7 @@ async function runIndexerWithStore(store, opts) {
16017
16148
  if (isAbortError(e)) throw e;
16018
16149
  return {
16019
16150
  file,
16020
- stat: stat19,
16151
+ stat: stat20,
16021
16152
  lang,
16022
16153
  parsed: null,
16023
16154
  error: `read error: ${e instanceof Error ? e.message : String(e)}`
@@ -16027,15 +16158,15 @@ async function runIndexerWithStore(store, opts) {
16027
16158
  if (!force && meta && meta.contentHash && contentHash === meta.contentHash) {
16028
16159
  return {
16029
16160
  file,
16030
- stat: stat19,
16161
+ stat: stat20,
16031
16162
  lang,
16032
16163
  parsed: null,
16033
16164
  content,
16034
16165
  contentHash,
16035
- skippedMeta: { ...meta, mtimeMs: Math.floor(stat19.mtimeMs) }
16166
+ skippedMeta: { ...meta, mtimeMs: Math.floor(stat20.mtimeMs) }
16036
16167
  };
16037
16168
  }
16038
- return { file, stat: stat19, lang, parsed: null, content, contentHash };
16169
+ return { file, stat: stat20, lang, parsed: null, content, contentHash };
16039
16170
  }
16040
16171
  )
16041
16172
  );
@@ -16114,7 +16245,7 @@ async function runIndexerWithStore(store, opts) {
16114
16245
  filesFailed++;
16115
16246
  continue;
16116
16247
  }
16117
- const { stat: stat19, lang, parsed } = result;
16248
+ const { stat: stat20, lang, parsed } = result;
16118
16249
  if (result.skippedMeta) {
16119
16250
  langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
16120
16251
  symbolsIndexed += result.skippedMeta.symbolCount;
@@ -16138,7 +16269,7 @@ async function runIndexerWithStore(store, opts) {
16138
16269
  store.upsertFile({
16139
16270
  file,
16140
16271
  lang,
16141
- mtimeMs: Math.floor(stat19.mtimeMs),
16272
+ mtimeMs: Math.floor(stat20.mtimeMs),
16142
16273
  symbolCount: 0,
16143
16274
  lastIndexed: Date.now(),
16144
16275
  contentHash: result.contentHash ?? ""
@@ -16152,7 +16283,7 @@ async function runIndexerWithStore(store, opts) {
16152
16283
  store.replaceEmptyFile({
16153
16284
  file,
16154
16285
  lang,
16155
- mtimeMs: Math.floor(stat19.mtimeMs),
16286
+ mtimeMs: Math.floor(stat20.mtimeMs),
16156
16287
  symbolCount: 0,
16157
16288
  lastIndexed: Date.now(),
16158
16289
  contentHash: result.contentHash ?? ""
@@ -16166,7 +16297,7 @@ async function runIndexerWithStore(store, opts) {
16166
16297
  lang,
16167
16298
  symbols: parsed.symbols,
16168
16299
  refs: parsed.refs ?? [],
16169
- mtimeMs: Math.floor(stat19.mtimeMs),
16300
+ mtimeMs: Math.floor(stat20.mtimeMs),
16170
16301
  symbolCount: parsed.symbols.length,
16171
16302
  contentHash: result.contentHash ?? ""
16172
16303
  });
@@ -16517,7 +16648,7 @@ function callIndexOp(op, args, opts) {
16517
16648
  opts.signal.reason instanceof Error ? opts.signal.reason : new Error("Indexing cancelled")
16518
16649
  );
16519
16650
  }
16520
- return new Promise((resolve17, reject) => {
16651
+ return new Promise((resolve18, reject) => {
16521
16652
  const id = nextRpcId++;
16522
16653
  const timer = setTimeout(() => {
16523
16654
  pending.delete(id);
@@ -16539,7 +16670,7 @@ function callIndexOp(op, args, opts) {
16539
16670
  pending.set(id, {
16540
16671
  resolve: (v) => {
16541
16672
  cleanup();
16542
- resolve17(v);
16673
+ resolve18(v);
16543
16674
  },
16544
16675
  reject: (e) => {
16545
16676
  cleanup();
@@ -16807,6 +16938,160 @@ function ensureCodebaseIndexServer(options) {
16807
16938
  }
16808
16939
 
16809
16940
  // src/codebase-index/codebase-index-tool.ts
16941
+ import { ToolValidationError } from "@wrongstack/core/types";
16942
+
16943
+ // src/codebase-index/codebase-search-tool.ts
16944
+ import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
16945
+ var INDEXABLE_LANG_IDS = [
16946
+ "ts",
16947
+ "tsx",
16948
+ "js",
16949
+ "jsx",
16950
+ "go",
16951
+ "py",
16952
+ "rs",
16953
+ "json",
16954
+ "yaml"
16955
+ ];
16956
+ var codebaseSearchTool = {
16957
+ name: "codebase-search",
16958
+ category: "Project",
16959
+ icon: "index",
16960
+ description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback. Prefer this before broad `tree`, `glob`, or `grep` exploration when finding code by name or concept. Use `grep` instead for exact text, regexes, unsupported content, or concrete usage sites. Set `preferLsp: true` for live precision when the LSP plugin is active (supersedes codebase-lsp-search).",
16961
+ usageHint: "FIRST CHOICE FOR INDEXABLE CODE UNDERSTANDING:\n\n- Call before broad `tree`, `glob`, or `grep` exploration when locating symbols, concepts, definitions, or candidate modules.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\n- If `indexStatus` reports no persisted data, run `codebase-index` and retry.",
16962
+ permission: "auto",
16963
+ mutating: false,
16964
+ capabilities: ["fs.read"],
16965
+ // The index host has its own 30s read watchdog. Leave enough headroom for
16966
+ // worker teardown and structured timeout reporting.
16967
+ timeoutMs: 35e3,
16968
+ inputSchema: {
16969
+ type: "object",
16970
+ properties: {
16971
+ query: {
16972
+ type: "string",
16973
+ description: "Search query \u2014 searches symbol names, signatures, and doc comments"
16974
+ },
16975
+ kind: {
16976
+ type: "string",
16977
+ enum: [
16978
+ "class",
16979
+ "interface",
16980
+ "enum",
16981
+ "type",
16982
+ "function",
16983
+ "method",
16984
+ "var",
16985
+ "const",
16986
+ "let",
16987
+ "property",
16988
+ "parameter",
16989
+ "namespace",
16990
+ "object",
16991
+ "literal",
16992
+ "schema",
16993
+ "struct",
16994
+ "trait",
16995
+ "impl",
16996
+ "static",
16997
+ "mod"
16998
+ ],
16999
+ description: "Filter by indexed symbol kind"
17000
+ },
17001
+ lang: {
17002
+ type: "string",
17003
+ enum: [...INDEXABLE_LANG_IDS],
17004
+ description: "Filter by indexed language"
17005
+ },
17006
+ lspKind: {
17007
+ type: "integer",
17008
+ description: "Filter by LSP SymbolKind number (e.g. 5=Class, 12=Function, 11=Interface, 10=Enum)"
17009
+ },
17010
+ file: {
17011
+ type: "string",
17012
+ description: "Filter to files matching this path substring"
17013
+ },
17014
+ limit: {
17015
+ type: "integer",
17016
+ description: "Maximum results to return (default 20, max 100)",
17017
+ minimum: 1,
17018
+ maximum: 100
17019
+ },
17020
+ preferLsp: {
17021
+ type: "boolean",
17022
+ description: "Prefer live LSP results over the index. Ignored unless the LSP plugin is active; when it is active and this is true, results come from live workspaceSymbol queries."
17023
+ }
17024
+ },
17025
+ required: ["query"]
17026
+ },
17027
+ async execute(input, ctx, execOpts) {
17028
+ const state = getIndexState();
17029
+ if (state.indexing && !state.ready) {
17030
+ return {
17031
+ results: [],
17032
+ total: 0,
17033
+ query: input.query,
17034
+ indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
17035
+ };
17036
+ }
17037
+ if (state.lastError) {
17038
+ const circuit = state.circuit;
17039
+ const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s); the user can run /codebase-reindex to retry now.` : "Try /codebase-reindex.";
17040
+ return {
17041
+ results: [],
17042
+ total: 0,
17043
+ query: input.query,
17044
+ indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
17045
+ };
17046
+ }
17047
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
17048
+ let searched;
17049
+ try {
17050
+ searched = await searchCodebaseIndex(
17051
+ {
17052
+ projectRoot: ctx.projectRoot,
17053
+ indexDir: codebaseIndexDirOverride(ctx),
17054
+ query: input.query,
17055
+ kind: input.kind?.toLowerCase(),
17056
+ lang: input.lang?.toLowerCase(),
17057
+ file: input.file,
17058
+ lspKind: input.lspKind,
17059
+ limit
17060
+ },
17061
+ { signal: execOpts?.signal }
17062
+ );
17063
+ } catch (err) {
17064
+ if (execOpts?.signal?.aborted) throw err;
17065
+ return {
17066
+ results: [],
17067
+ total: 0,
17068
+ query: input.query,
17069
+ indexStatus: `Index query failed: ${toErrorMessage3(err)}. Fall back to grep/glob for this lookup.`
17070
+ };
17071
+ }
17072
+ const { results, total } = searched;
17073
+ let hasPersistedIndex = state.ready || total > 0;
17074
+ if (!hasPersistedIndex) {
17075
+ try {
17076
+ const stats = await codebaseIndexStats(
17077
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
17078
+ { signal: execOpts?.signal }
17079
+ );
17080
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
17081
+ } catch {
17082
+ }
17083
+ }
17084
+ return {
17085
+ results,
17086
+ total,
17087
+ query: input.query,
17088
+ ...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
17089
+ };
17090
+ }
17091
+ };
17092
+
17093
+ // src/codebase-index/codebase-index-tool.ts
17094
+ var MAX_REPORTED_ERRORS = 20;
16810
17095
  var codebaseIndexTool = {
16811
17096
  name: "codebase-index",
16812
17097
  category: "Project",
@@ -16832,12 +17117,23 @@ var codebaseIndexTool = {
16832
17117
  },
16833
17118
  langs: {
16834
17119
  type: "array",
16835
- items: { type: "string" },
16836
- description: "Limit reindex to specific languages: ts, tsx, js, jsx, go, py, rs"
17120
+ items: { type: "string", enum: [...INDEXABLE_LANG_IDS] },
17121
+ description: `Limit reindex to specific languages: ${INDEXABLE_LANG_IDS.join(", ")}`
16837
17122
  }
16838
17123
  }
16839
17124
  },
16840
17125
  async execute(input, ctx, execOpts) {
17126
+ if (input.langs) {
17127
+ const unknown = input.langs.filter(
17128
+ (lang) => !INDEXABLE_LANG_IDS.includes(lang)
17129
+ );
17130
+ if (unknown.length > 0) {
17131
+ throw new ToolValidationError({
17132
+ message: `codebase-index: unknown lang(s) ${unknown.map((l) => `"${l}"`).join(", ")}. Valid ids: ${INDEXABLE_LANG_IDS.join(", ")}.`,
17133
+ field: "langs"
17134
+ });
17135
+ }
17136
+ }
16841
17137
  if (isIndexing()) {
16842
17138
  return {
16843
17139
  filesIndexed: 0,
@@ -16859,23 +17155,32 @@ var codebaseIndexTool = {
16859
17155
  note: `Codebase indexing is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}). Auto-retry possible in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s; the user can run /codebase-reindex to retry immediately.`
16860
17156
  };
16861
17157
  }
16862
- return await runStartupIndex({
17158
+ const result = await runStartupIndex({
16863
17159
  projectRoot: ctx.projectRoot,
16864
17160
  force: input.force ?? false,
16865
17161
  langs: input.langs,
16866
17162
  indexDir: codebaseIndexDirOverride(ctx),
16867
17163
  signal: execOpts?.signal
16868
17164
  });
17165
+ if (result.errors.length > MAX_REPORTED_ERRORS) {
17166
+ const hidden = result.errors.length - MAX_REPORTED_ERRORS;
17167
+ return {
17168
+ ...result,
17169
+ errors: [...result.errors.slice(0, MAX_REPORTED_ERRORS), `+${hidden} more`]
17170
+ };
17171
+ }
17172
+ return result;
16869
17173
  }
16870
17174
  };
16871
17175
 
16872
17176
  // src/codebase-index/codebase-incoming-calls-tool.ts
17177
+ import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
16873
17178
  var codebaseIncomingCallsTool = {
16874
17179
  name: "codebase-incoming-calls",
16875
17180
  category: "Project",
16876
17181
  icon: "index",
16877
- description: "Find all callers of a function, method, or symbol \u2014 who invokes or references it. Uses the codebase index ref graph for instant, exact results. Always use this instead of grep when checking impact of a change.",
16878
- usageHint: 'CALL THIS BEFORE REFACTORING OR CHANGING ANY FUNCTION:\n\n- NEVER use grep or manual line reading to check where a function is called.\n- ALWAYS call codebase-incoming-calls({ symbol: "funcName" }) first.\n- Returns exact files, line numbers, caller signatures, and call types in milliseconds.\n- Use `file` to disambiguate when multiple symbols share a name.\n- Combine with codebase-outgoing-calls to see what the symbol itself calls.\nIf the index is not built, run codebase-index first.',
17182
+ description: "Find all callers of a function, method, or symbol \u2014 who invokes or references it. Uses the codebase index ref graph for instant, exact results. Prefer this over grep for change-impact checks when the index is available.",
17183
+ usageHint: 'CALL THIS BEFORE REFACTORING OR CHANGING ANY FUNCTION:\n\n- Prefer this over grep when the index is available; fall back to grep when the index is cold/unavailable or for dynamic dispatch the ref graph cannot see.\n- Call codebase-incoming-calls({ symbol: "funcName" }) before editing the symbol.\n- Returns exact files, line numbers, caller signatures, and call types in milliseconds.\n- Use `file` to disambiguate when multiple symbols share a name.\n- Combine with codebase-outgoing-calls to see what the symbol itself calls.\nIf the index is not built, run codebase-index first.',
16879
17184
  permission: "auto",
16880
17185
  mutating: false,
16881
17186
  capabilities: ["fs.read"],
@@ -16927,16 +17232,27 @@ var codebaseIncomingCallsTool = {
16927
17232
  }
16928
17233
  const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
16929
17234
  const transitive = input.transitive === true;
16930
- const { calls, symbolFound, ambiguous, totalMatches } = await incomingCallsService2(
16931
- {
16932
- projectRoot: ctx.projectRoot,
16933
- indexDir: codebaseIndexDirOverride(ctx),
17235
+ let serviced;
17236
+ try {
17237
+ serviced = await incomingCallsService2(
17238
+ {
17239
+ projectRoot: ctx.projectRoot,
17240
+ indexDir: codebaseIndexDirOverride(ctx),
17241
+ symbol: input.symbol,
17242
+ file: input.file,
17243
+ limit,
17244
+ transitive
17245
+ }
17246
+ );
17247
+ } catch (err) {
17248
+ return {
16934
17249
  symbol: input.symbol,
16935
- file: input.file,
16936
- limit,
16937
- transitive
16938
- }
16939
- );
17250
+ calls: [],
17251
+ total: 0,
17252
+ indexStatus: `Index query failed: ${toErrorMessage4(err)}. Fall back to grep for this lookup.`
17253
+ };
17254
+ }
17255
+ const { calls, symbolFound, ambiguous, totalMatches } = serviced;
16940
17256
  if (!symbolFound) {
16941
17257
  let hasPersistedIndex = state.ready;
16942
17258
  if (!hasPersistedIndex) {
@@ -16981,12 +17297,13 @@ var codebaseIncomingCallsTool = {
16981
17297
  };
16982
17298
 
16983
17299
  // src/codebase-index/codebase-outgoing-calls-tool.ts
17300
+ import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
16984
17301
  var codebaseOutgoingCallsTool = {
16985
17302
  name: "codebase-outgoing-calls",
16986
17303
  category: "Project",
16987
17304
  icon: "index",
16988
17305
  description: "Find all functions/methods/symbols that a given symbol calls or depends on \u2014 its callees. Uses the codebase index ref graph for instant, exact results. Use this to understand a function's dependencies before modifying it.",
16989
- usageHint: 'USE THIS TO UNDERSTAND A FUNCTION\'S DEPENDENCIES:\n\n- Call codebase-outgoing-calls({ symbol: "funcName" }) to see everything it calls.\n- Returns exact files, line numbers, callee signatures, and call types in milliseconds.\n- Use `file` to disambiguate when multiple symbols share a name.\n- Pair with codebase-incoming-calls for a complete impact picture: incoming = who calls you, outgoing = what you call.\nIf the index is not built, run codebase-index first.',
17306
+ usageHint: 'USE THIS TO UNDERSTAND A FUNCTION\'S DEPENDENCIES:\n\n- Prefer this over grep when the index is available; fall back to grep when the index is cold/unavailable or for dynamic dispatch the ref graph cannot see.\n- Call codebase-outgoing-calls({ symbol: "funcName" }) to see everything it calls.\n- Returns exact files, line numbers, callee signatures, and call types in milliseconds.\n- Use `file` to disambiguate when multiple symbols share a name.\n- Pair with codebase-incoming-calls for a complete impact picture: incoming = who calls you, outgoing = what you call.\nIf the index is not built, run codebase-index first.',
16990
17307
  permission: "auto",
16991
17308
  mutating: false,
16992
17309
  capabilities: ["fs.read"],
@@ -17038,16 +17355,27 @@ var codebaseOutgoingCallsTool = {
17038
17355
  }
17039
17356
  const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
17040
17357
  const transitive = input.transitive === true;
17041
- const { calls, symbolFound, unresolvedCount, totalMatches } = await outgoingCallsService2(
17042
- {
17043
- projectRoot: ctx.projectRoot,
17044
- indexDir: codebaseIndexDirOverride(ctx),
17358
+ let serviced;
17359
+ try {
17360
+ serviced = await outgoingCallsService2(
17361
+ {
17362
+ projectRoot: ctx.projectRoot,
17363
+ indexDir: codebaseIndexDirOverride(ctx),
17364
+ symbol: input.symbol,
17365
+ file: input.file,
17366
+ limit,
17367
+ transitive
17368
+ }
17369
+ );
17370
+ } catch (err) {
17371
+ return {
17045
17372
  symbol: input.symbol,
17046
- file: input.file,
17047
- limit,
17048
- transitive
17049
- }
17050
- );
17373
+ calls: [],
17374
+ total: 0,
17375
+ indexStatus: `Index query failed: ${toErrorMessage5(err)}. Fall back to grep for this lookup.`
17376
+ };
17377
+ }
17378
+ const { calls, symbolFound, unresolvedCount, totalMatches } = serviced;
17051
17379
  if (!symbolFound) {
17052
17380
  let hasPersistedIndex = state.ready;
17053
17381
  if (!hasPersistedIndex) {
@@ -17091,132 +17419,6 @@ var codebaseOutgoingCallsTool = {
17091
17419
  }
17092
17420
  };
17093
17421
 
17094
- // src/codebase-index/codebase-search-tool.ts
17095
- var codebaseSearchTool = {
17096
- name: "codebase-search",
17097
- category: "Project",
17098
- icon: "index",
17099
- description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback. Prefer this before broad `tree`, `glob`, or `grep` exploration when finding code by name or concept. Set `preferLsp: true` for live precision when the LSP plugin is active (supersedes codebase-lsp-search).",
17100
- usageHint: "FIRST CHOICE FOR INDEXABLE CODE UNDERSTANDING:\n\n- Call before broad `tree`, `glob`, or `grep` exploration when locating symbols, concepts, definitions, or candidate modules.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\n- If `indexStatus` reports no persisted data, run `codebase-index` and retry.\nUse `grep` afterwards for exact text, regexes, unsupported content, or concrete usage sites.",
17101
- permission: "auto",
17102
- mutating: false,
17103
- capabilities: ["fs.read"],
17104
- // The index host has its own 30s read watchdog. Leave enough headroom for
17105
- // worker teardown and structured timeout reporting.
17106
- timeoutMs: 35e3,
17107
- inputSchema: {
17108
- type: "object",
17109
- properties: {
17110
- query: {
17111
- type: "string",
17112
- description: "Search query \u2014 searches symbol names, signatures, and doc comments"
17113
- },
17114
- kind: {
17115
- type: "string",
17116
- enum: [
17117
- "class",
17118
- "interface",
17119
- "enum",
17120
- "type",
17121
- "function",
17122
- "method",
17123
- "var",
17124
- "const",
17125
- "let",
17126
- "property",
17127
- "parameter",
17128
- "namespace",
17129
- "object",
17130
- "literal",
17131
- "schema",
17132
- "struct",
17133
- "trait",
17134
- "impl",
17135
- "static",
17136
- "mod"
17137
- ],
17138
- description: "Filter by indexed symbol kind"
17139
- },
17140
- lang: {
17141
- type: "string",
17142
- enum: ["ts", "tsx", "js", "jsx", "go", "py", "rs", "json", "yaml"],
17143
- description: "Filter by indexed language"
17144
- },
17145
- lspKind: {
17146
- type: "integer",
17147
- description: "Filter by LSP SymbolKind number (e.g. 5=Class, 12=Function, 11=Interface, 10=Enum)"
17148
- },
17149
- file: {
17150
- type: "string",
17151
- description: "Filter to files matching this path substring"
17152
- },
17153
- limit: {
17154
- type: "integer",
17155
- description: "Maximum results to return (default 20, max 100)",
17156
- minimum: 1,
17157
- maximum: 100
17158
- },
17159
- preferLsp: {
17160
- type: "boolean",
17161
- description: "Prefer live LSP results over the index. Index-only when the LSP plugin is not active. When the LSP plugin is active and this is true, results come from live workspaceSymbol queries."
17162
- }
17163
- },
17164
- required: ["query"]
17165
- },
17166
- async execute(input, ctx, execOpts) {
17167
- const state = getIndexState();
17168
- if (state.indexing && !state.ready) {
17169
- return {
17170
- results: [],
17171
- total: 0,
17172
- query: input.query,
17173
- indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
17174
- };
17175
- }
17176
- if (state.lastError) {
17177
- const circuit = state.circuit;
17178
- const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s); the user can run /codebase-reindex to retry now.` : "Try /codebase-reindex.";
17179
- return {
17180
- results: [],
17181
- total: 0,
17182
- query: input.query,
17183
- indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
17184
- };
17185
- }
17186
- const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
17187
- const { results, total } = await searchCodebaseIndex(
17188
- {
17189
- projectRoot: ctx.projectRoot,
17190
- indexDir: codebaseIndexDirOverride(ctx),
17191
- query: input.query,
17192
- kind: input.kind?.toLowerCase(),
17193
- lang: input.lang?.toLowerCase(),
17194
- file: input.file,
17195
- lspKind: input.lspKind,
17196
- limit
17197
- },
17198
- { signal: execOpts?.signal }
17199
- );
17200
- let hasPersistedIndex = state.ready || total > 0;
17201
- if (!hasPersistedIndex) {
17202
- try {
17203
- const stats = await codebaseIndexStats(
17204
- { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
17205
- { signal: execOpts?.signal }
17206
- );
17207
- hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
17208
- } catch {
17209
- }
17210
- }
17211
- return {
17212
- results,
17213
- total,
17214
- query: input.query,
17215
- ...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
17216
- };
17217
- }
17218
- };
17219
-
17220
17422
  // src/codebase-index/codebase-stats-tool.ts
17221
17423
  var codebaseStatsTool = {
17222
17424
  name: "codebase-stats",
@@ -17310,9 +17512,9 @@ import * as path25 from "node:path";
17310
17512
  var deadCodeScanTool = {
17311
17513
  name: "dead-code-scan",
17312
17514
  category: "Project",
17313
- icon: "search",
17515
+ icon: "index",
17314
17516
  description: "Scan TypeScript/JavaScript source files for exported symbols that appear unused anywhere in the project. Uses the codebase-index reference graph (import/call/type-ref edges) to compute transitive reachability from package.json entry points. Requires a built codebase-index (run `codebase-index` first if you get no results).",
17315
- usageHint: 'PASS `path` TO SCOPE THE SCAN:\n\n- Defaults to the project root (all indexed files).\n- `path` can be a directory to scope the scan (e.g. "src/util/").\n- `entryPoints` overrides auto-detected entry points (comma-separated file paths).\n\nThe scan runs against the existing index; results are best-effort.',
17517
+ usageHint: "SCANS ALL INDEXED FILES UNDER THE PROJECT ROOT:\n\n- `projectRoot` defaults to the current project root; `indexDir` overrides the resolved index location.\n- `entryPoints` is an array of file paths that AUGMENTS the auto-discovered entry points (package.json bin/main/exports/types plus conventional src/index.ts-style files) \u2014 it does not replace them.\n\nThe scan runs against the existing index; results are best-effort (dynamic imports, external consumers, and config-driven registration are invisible).",
17316
17518
  permission: "auto",
17317
17519
  mutating: false,
17318
17520
  capabilities: ["fs.read"],
@@ -18075,12 +18277,14 @@ import { statSync as statSync3 } from "node:fs";
18075
18277
  import * as fs22 from "node:fs/promises";
18076
18278
  import * as path27 from "node:path";
18077
18279
  import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core/utils";
18280
+ import { ToolValidationError as ToolValidationError2 } from "@wrongstack/core/types";
18078
18281
  var MAX_FILE_DUMP_BYTES = 5 * 1024 * 1024;
18282
+ var MAX_GIT_DIFF_CHARS = 1e5;
18079
18283
  var diffTool = {
18080
18284
  name: "diff",
18081
18285
  category: "Filesystem",
18082
18286
  description: "Show file content with line numbers, staged/working-tree diffs via git, or commit/branch diffs. A safer and more structured alternative to raw `git diff` via shell.",
18083
- usageHint: 'USE FOR CODE REVIEW AND CHANGE INSPECTION:\n\n- `files` + no `a`/`b` \u2192 show file content with line numbers (NOT a unified diff; no +/- prefixes).\n- `a` and/or `b` \u2192 git-style commit/branch diff (unified format, real +/- prefixes).\n- `staged: true` \u2192 only show staged changes.\n- `mode` can be "unified", "stat", or "side-by-side" (only affects the git-diff path).\n\nNOTE: For a true file-vs-file unified diff, supply `a` and `b` so the tool delegates to `git diff`. The `files`-only path is a line-numbered dump, not a diff.\n\nThis tool has important safety guards against flag injection (see previous security findings).',
18287
+ usageHint: 'USE FOR CODE REVIEW AND CHANGE INSPECTION:\n\n- `files` + no `a`/`b` \u2192 show file content with line numbers (NOT a unified diff; no +/- prefixes). Result `mode` is "dump".\n- `a` and/or `b` \u2192 git-style commit/branch diff (unified format, real +/- prefixes).\n- `staged: true` \u2192 only show staged changes.\n- `mode` only affects the git-diff path: "stat" runs `git diff --stat`; "side-by-side" is not supported and falls back to unified (result `mode` reports what was produced).\n- `context` sets the unified-diff context line count on the git path (`-U<n>`); the dump path has no context notion.\n\nNOTE: For a true file-vs-file unified diff, supply `a` and `b` so the tool delegates to `git diff`. The `files`-only path is a line-numbered dump, not a diff.\n\nThis tool has important safety guards against flag injection (see previous security findings).',
18084
18288
  permission: "auto",
18085
18289
  mutating: false,
18086
18290
  maxOutputBytes: 262144,
@@ -18113,11 +18317,12 @@ var diffTool = {
18113
18317
  mode: {
18114
18318
  type: "string",
18115
18319
  enum: ["unified", "side-by-side", "stat"],
18116
- description: 'Output format. "unified" is default, "stat" shows summary only.'
18320
+ description: 'Output format for the git-diff path. "unified" is default; "stat" shows a summary only; "side-by-side" is not supported and falls back to unified. The `files`-only dump path ignores this.'
18117
18321
  },
18118
18322
  context: {
18119
18323
  type: "integer",
18120
- description: "Number of context lines for unified diffs (default: 3)."
18324
+ minimum: 0,
18325
+ description: "Number of context lines for git unified diffs (default: 3, passed as -U<n>). Ignored by the `files`-only dump path."
18121
18326
  }
18122
18327
  }
18123
18328
  },
@@ -18130,16 +18335,31 @@ var diffTool = {
18130
18335
  };
18131
18336
  async function gitDiff(input, ctx, signal) {
18132
18337
  if (input.a?.startsWith("-")) {
18133
- throw new Error(`diff: unsafe ref "${input.a}" \u2014 refs may not begin with '-' (flag injection)`);
18338
+ throw new ToolValidationError2({
18339
+ message: `diff: unsafe ref "${input.a}" \u2014 refs may not begin with '-' (flag injection)`,
18340
+ field: "a"
18341
+ });
18134
18342
  }
18135
18343
  if (input.b?.startsWith("-")) {
18136
- throw new Error(`diff: unsafe ref "${input.b}" \u2014 refs may not begin with '-' (flag injection)`);
18344
+ throw new ToolValidationError2({
18345
+ message: `diff: unsafe ref "${input.b}" \u2014 refs may not begin with '-' (flag injection)`,
18346
+ field: "b"
18347
+ });
18137
18348
  }
18349
+ const requestedMode = input.mode ?? "unified";
18350
+ const statMode = requestedMode === "stat";
18351
+ const effectiveMode = statMode ? "stat" : "unified";
18352
+ const sideBySideNote = requestedMode === "side-by-side" ? "side-by-side output is not supported; a unified diff was produced instead." : void 0;
18138
18353
  const gitDir = findGitDir(ctx.cwd);
18139
18354
  if (!gitDir) {
18140
- return { diff: "", files: [], truncated: false, mode: "unified" };
18355
+ return { diff: "", files: [], truncated: false, mode: effectiveMode };
18141
18356
  }
18142
18357
  const args = ["diff", "--no-color"];
18358
+ if (statMode) args.push("--stat");
18359
+ if (!statMode && input.context !== void 0) {
18360
+ const contextLines = Math.max(0, Math.floor(input.context));
18361
+ if (Number.isFinite(contextLines)) args.push(`-U${contextLines}`);
18362
+ }
18143
18363
  if (input.staged) args.push("--staged");
18144
18364
  if (input.a) args.push(input.a);
18145
18365
  if (input.b) args.push(input.b);
@@ -18148,19 +18368,30 @@ async function gitDiff(input, ctx, signal) {
18148
18368
  args.push("--", ...files.map((f) => f.trim()));
18149
18369
  }
18150
18370
  const result = await runGit(args, gitDir, signal);
18371
+ let diff = result.stdout;
18372
+ let truncated = false;
18373
+ if (diff.length > MAX_GIT_DIFF_CHARS) {
18374
+ let clipped = diff.slice(0, MAX_GIT_DIFF_CHARS);
18375
+ const nl = clipped.lastIndexOf("\n");
18376
+ if (nl > 0) clipped = clipped.slice(0, nl);
18377
+ diff = `${clipped}
18378
+ \u2026[git diff truncated: ${result.stdout.length - clipped.length} of ${result.stdout.length} characters omitted]`;
18379
+ truncated = true;
18380
+ }
18151
18381
  return {
18152
- diff: result.stdout,
18382
+ diff,
18153
18383
  files: [],
18154
- truncated: result.stdout.length > 1e5,
18155
- mode: "unified"
18384
+ truncated,
18385
+ mode: effectiveMode,
18386
+ note: sideBySideNote
18156
18387
  };
18157
18388
  }
18158
18389
  function findGitDir(cwd) {
18159
18390
  let dir = cwd;
18160
18391
  for (let i = 0; i < 20; i++) {
18161
18392
  try {
18162
- const stat19 = statSync3(path27.join(dir, ".git"));
18163
- if (stat19.isDirectory()) return dir;
18393
+ const stat20 = statSync3(path27.join(dir, ".git"));
18394
+ if (stat20.isDirectory()) return dir;
18164
18395
  } catch {
18165
18396
  }
18166
18397
  const parent = path27.dirname(dir);
@@ -18170,7 +18401,7 @@ function findGitDir(cwd) {
18170
18401
  return null;
18171
18402
  }
18172
18403
  function runGit(args, cwd, signal) {
18173
- return new Promise((resolve17) => {
18404
+ return new Promise((resolve18) => {
18174
18405
  let stdout = "";
18175
18406
  let stderr = "";
18176
18407
  const child = spawn7("git", args, {
@@ -18186,8 +18417,8 @@ function runGit(args, cwd, signal) {
18186
18417
  child.stderr?.on("data", (c) => {
18187
18418
  stderr += c.toString();
18188
18419
  });
18189
- child.on("close", (code) => resolve17({ stdout, stderr, exitCode: code ?? 0 }));
18190
- child.on("error", (e) => resolve17({ stdout: "", stderr: e.message, exitCode: 1 }));
18420
+ child.on("close", (code) => resolve18({ stdout, stderr, exitCode: code ?? 0 }));
18421
+ child.on("error", (e) => resolve18({ stdout: "", stderr: e.message, exitCode: 1 }));
18191
18422
  });
18192
18423
  }
18193
18424
  async function fileDiff(input, ctx, _signal) {
@@ -18198,19 +18429,19 @@ async function fileDiff(input, ctx, _signal) {
18198
18429
  diff: "No files specified",
18199
18430
  files: [],
18200
18431
  truncated: false,
18201
- mode: input.mode ?? "unified"
18432
+ mode: "dump"
18202
18433
  };
18203
18434
  }
18204
18435
  const results = [];
18205
18436
  let truncated = false;
18206
18437
  for (const file of files) {
18207
- const absPath = safeResolve(file, ctx);
18208
- const stat19 = await fs22.stat(absPath).catch(() => null);
18209
- if (!stat19?.isFile()) continue;
18210
- if (stat19.size > MAX_FILE_DUMP_BYTES) {
18438
+ const absPath = await safeResolveReal(file, ctx);
18439
+ const stat20 = await fs22.stat(absPath).catch(() => null);
18440
+ if (!stat20?.isFile()) continue;
18441
+ if (stat20.size > MAX_FILE_DUMP_BYTES) {
18211
18442
  truncated = true;
18212
18443
  results.push(
18213
- `--- ${file} (skipped: ${stat19.size} bytes exceeds the ${MAX_FILE_DUMP_BYTES} limit; use the read tool with offset/limit) ---`
18444
+ `--- ${file} (skipped: ${stat20.size} bytes exceeds the ${MAX_FILE_DUMP_BYTES} limit; use the read tool with offset/limit) ---`
18214
18445
  );
18215
18446
  continue;
18216
18447
  }
@@ -18222,7 +18453,10 @@ async function fileDiff(input, ctx, _signal) {
18222
18453
  diff: results.join("\n\n"),
18223
18454
  files,
18224
18455
  truncated,
18225
- mode: input.mode ?? "unified"
18456
+ // Honest mode: this path always produces a line-numbered dump — it never
18457
+ // honors `mode`, so it must not echo the requested value back.
18458
+ mode: "dump",
18459
+ note: input.mode !== void 0 ? "The `files`-only path is a line-numbered dump; `mode` only affects the git-diff path (`a`/`b`)." : void 0
18226
18460
  };
18227
18461
  }
18228
18462
  function formatWithLineNumbers(file, lines) {
@@ -18235,11 +18469,12 @@ ${numbered}`;
18235
18469
  // src/document.ts
18236
18470
  init_util();
18237
18471
  import * as fs23 from "node:fs/promises";
18472
+ import * as path28 from "node:path";
18238
18473
  var documentTool = {
18239
18474
  name: "document",
18240
18475
  category: "Project",
18241
- description: "DEPRECATED \u2014 use the `auto_doc` tool with `dryRun: true` instead. This tool is a read-only preview stub that returns `skipped` candidates without generating real docstrings.",
18242
- usageHint: "Deprecated: prefer `auto_doc` with `dryRun: true` for previewing, or `auto_doc` without dryRun for writing. This tool only lists undocumented symbols with placeholder comments \u2014 it does not generate real JSDoc/TSDoc.",
18476
+ description: "DEPRECATED \u2014 read-only preview stub that lists undocumented symbols as `skipped` candidates. It never writes files and does not generate real docstrings. If the auto-doc plugin is enabled, use its `auto_doc` tool (with `dry_run: true` to preview) instead.",
18477
+ usageHint: "Deprecated: this tool only lists undocumented symbols with placeholder comments \u2014 it does not generate real JSDoc/TSDoc and writes nothing. When the auto-doc plugin is enabled, prefer its `auto_doc` tool (`dry_run: true` for previewing, without it for writing).",
18243
18478
  permission: "auto",
18244
18479
  mutating: false,
18245
18480
  timeoutMs: 3e4,
@@ -18275,7 +18510,11 @@ var documentTool = {
18275
18510
  const results = [];
18276
18511
  let filesProcessed = 0;
18277
18512
  let itemsDocumented = 0;
18278
- const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
18513
+ const fileList = input.files ? await resolveFiles(
18514
+ Array.isArray(input.files) ? input.files.join(",") : input.files,
18515
+ cwd,
18516
+ ctx
18517
+ ) : input.path ? [safeResolve(input.path, ctx)] : [];
18279
18518
  for (const absPath of fileList) {
18280
18519
  try {
18281
18520
  const content = await fs23.readFile(absPath, "utf8");
@@ -18308,14 +18547,21 @@ var documentTool = {
18308
18547
  };
18309
18548
  }
18310
18549
  };
18311
- async function resolveFiles(filesInput, cwd) {
18312
- const files = Array.isArray(filesInput) ? filesInput : filesInput.split(",");
18550
+ async function resolveFiles(filesInput, cwd, ctx) {
18551
+ const files = filesInput.split(",");
18313
18552
  const resolved = [];
18314
18553
  for (const f of files) {
18315
- const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
18554
+ const entry = f.trim();
18555
+ if (!entry) continue;
18556
+ let absPath;
18316
18557
  try {
18317
- const stat19 = await fs23.stat(absPath);
18318
- if (stat19.isFile()) resolved.push(absPath);
18558
+ absPath = ensureInsideRoot(path28.resolve(cwd, entry), ctx);
18559
+ } catch {
18560
+ continue;
18561
+ }
18562
+ try {
18563
+ const stat20 = await fs23.stat(absPath);
18564
+ if (stat20.isFile()) resolved.push(absPath);
18319
18565
  } catch {
18320
18566
  }
18321
18567
  }
@@ -18385,7 +18631,7 @@ function processFile(content, absPath, _style, _overwrite, target) {
18385
18631
  // src/e2e.ts
18386
18632
  init_util();
18387
18633
  import { open, readdir as readdir7 } from "node:fs/promises";
18388
- import * as path28 from "node:path";
18634
+ import * as path29 from "node:path";
18389
18635
  async function readBoundedText(filePath, maxBytes) {
18390
18636
  let handle;
18391
18637
  try {
@@ -18435,8 +18681,8 @@ var MAX_PACKAGE_BYTES = 512 * 1024;
18435
18681
  var MAX_CONFIG_BYTES = 512 * 1024;
18436
18682
  var MAX_SPEC_SAMPLES = 20;
18437
18683
  function relativePath(root, target) {
18438
- const value = path28.relative(root, target) || ".";
18439
- return value.split(path28.sep).join("/");
18684
+ const value = path29.relative(root, target) || ".";
18685
+ return value.split(path29.sep).join("/");
18440
18686
  }
18441
18687
  function escapeRegExp(value) {
18442
18688
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -18508,7 +18754,7 @@ async function scanWorkspace(root, maxDepth, signal) {
18508
18754
  }
18509
18755
  for (const entry of entries) {
18510
18756
  signal.throwIfAborted();
18511
- const absolutePath = path28.join(current.directory, entry.name);
18757
+ const absolutePath = path29.join(current.directory, entry.name);
18512
18758
  if (entry.isFile()) {
18513
18759
  if (entry.name === "package.json") result.packageFiles.push(absolutePath);
18514
18760
  const framework = CONFIG_NAMES.get(entry.name);
@@ -18569,9 +18815,9 @@ async function detectPackageManager3(projectRoot, scanRoot, declared) {
18569
18815
  if (names.has("bun.lock") || names.has("bun.lockb")) return "bun";
18570
18816
  if (names.has("package-lock.json") || names.has("npm-shrinkwrap.json")) return "npm";
18571
18817
  if (directory === scanRoot) break;
18572
- const parent = path28.dirname(directory);
18573
- const relativeParent = path28.relative(scanRoot, parent);
18574
- if (parent === directory || relativeParent.startsWith("..") || path28.isAbsolute(relativeParent)) {
18818
+ const parent = path29.dirname(directory);
18819
+ const relativeParent = path29.relative(scanRoot, parent);
18820
+ if (parent === directory || relativeParent.startsWith("..") || path29.isAbsolute(relativeParent)) {
18575
18821
  break;
18576
18822
  }
18577
18823
  directory = parent;
@@ -18616,7 +18862,7 @@ function isSpec(framework, filename) {
18616
18862
  return /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/i.test(filename);
18617
18863
  }
18618
18864
  async function collectSpecs(root, framework, testDirectory, signal) {
18619
- const roots = testDirectory ? [path28.resolve(root, testDirectory)] : framework === "cypress" ? [path28.join(root, "cypress", "e2e"), path28.join(root, "cypress", "integration")] : [path28.join(root, "tests")];
18865
+ const roots = testDirectory ? [path29.resolve(root, testDirectory)] : framework === "cypress" ? [path29.join(root, "cypress", "e2e"), path29.join(root, "cypress", "integration")] : [path29.join(root, "tests")];
18620
18866
  const samples = [];
18621
18867
  let count = 0;
18622
18868
  let scanned = 0;
@@ -18635,7 +18881,7 @@ async function collectSpecs(root, framework, testDirectory, signal) {
18635
18881
  }
18636
18882
  for (const entry of entries) {
18637
18883
  signal.throwIfAborted();
18638
- const target = path28.join(directory, entry.name);
18884
+ const target = path29.join(directory, entry.name);
18639
18885
  if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) queue.push(target);
18640
18886
  else if (entry.isFile() && isSpec(framework, entry.name)) {
18641
18887
  count += 1;
@@ -18655,7 +18901,7 @@ function nearestPackage(projectRoot, packagesByDirectory, scanRoot) {
18655
18901
  const found = packagesByDirectory.get(directory);
18656
18902
  if (found) return found;
18657
18903
  if (directory === scanRoot) return void 0;
18658
- const parent = path28.dirname(directory);
18904
+ const parent = path29.dirname(directory);
18659
18905
  if (parent === directory || relativePath(scanRoot, parent).startsWith("..")) return void 0;
18660
18906
  directory = parent;
18661
18907
  }
@@ -18666,13 +18912,13 @@ async function discoverE2EProjects(root, options) {
18666
18912
  const packages = (await Promise.all(scan.packageFiles.map(readPackageInfo))).filter(
18667
18913
  (info) => Boolean(info)
18668
18914
  );
18669
- const packagesByDirectory = new Map(packages.map((info) => [path28.dirname(info.path), info]));
18915
+ const packagesByDirectory = new Map(packages.map((info) => [path29.dirname(info.path), info]));
18670
18916
  const candidates = /* @__PURE__ */ new Map();
18671
18917
  for (const config of scan.configs) {
18672
18918
  if (options.framework && options.framework !== "all" && config.framework !== options.framework) {
18673
18919
  continue;
18674
18920
  }
18675
- const projectRoot = path28.dirname(config.absolutePath);
18921
+ const projectRoot = path29.dirname(config.absolutePath);
18676
18922
  candidates.set(`${config.framework}:${projectRoot}`, {
18677
18923
  framework: config.framework,
18678
18924
  root: projectRoot,
@@ -18680,7 +18926,7 @@ async function discoverE2EProjects(root, options) {
18680
18926
  });
18681
18927
  }
18682
18928
  for (const info of packages) {
18683
- const projectRoot = path28.dirname(info.path);
18929
+ const projectRoot = path29.dirname(info.path);
18684
18930
  for (const framework of frameworkFromPackage(info)) {
18685
18931
  if (options.framework && options.framework !== "all" && framework !== options.framework)
18686
18932
  continue;
@@ -18696,10 +18942,10 @@ async function discoverE2EProjects(root, options) {
18696
18942
  const scripts = matchingScripts(info, candidate.framework);
18697
18943
  const manager = await detectPackageManager3(candidate.root, root, info?.packageManager);
18698
18944
  const testDirectory = candidate.framework === "playwright" ? staticString(source, "testDir") : void 0;
18699
- const resolvedTestDirectory = testDirectory ? path28.resolve(candidate.root, testDirectory) : void 0;
18700
- const relativeTestDirectory = resolvedTestDirectory ? path28.relative(root, resolvedTestDirectory) : void 0;
18945
+ const resolvedTestDirectory = testDirectory ? path29.resolve(candidate.root, testDirectory) : void 0;
18946
+ const relativeTestDirectory = resolvedTestDirectory ? path29.relative(root, resolvedTestDirectory) : void 0;
18701
18947
  const unsafeTestDirectory = Boolean(
18702
- relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path28.isAbsolute(relativeTestDirectory))
18948
+ relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path29.isAbsolute(relativeTestDirectory))
18703
18949
  );
18704
18950
  const specs = options.includeSpecs === false || unsafeTestDirectory ? { count: 0, samples: [], truncated: false } : await collectSpecs(candidate.root, candidate.framework, testDirectory, options.signal);
18705
18951
  const warnings = [];
@@ -18807,7 +19053,7 @@ import {
18807
19053
  toStyle,
18808
19054
  unifiedDiff
18809
19055
  } from "@wrongstack/core/utils";
18810
- import { ToolValidationError } from "@wrongstack/core/types";
19056
+ import { ToolValidationError as ToolValidationError3 } from "@wrongstack/core/types";
18811
19057
 
18812
19058
  // src/_edit-match.ts
18813
19059
  var TIER_LABEL = {
@@ -19035,7 +19281,7 @@ function prefixSimilarity(a, b) {
19035
19281
  }
19036
19282
 
19037
19283
  // src/_syntax-check.ts
19038
- import * as path29 from "node:path";
19284
+ import * as path30 from "node:path";
19039
19285
  var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
19040
19286
  var MAX_CHECK_CHARS = 15e5;
19041
19287
  var MAX_ERRORS = 5;
@@ -19059,15 +19305,15 @@ async function checkSyntax(filePath, content, previousContent) {
19059
19305
  return { errors, preExisting };
19060
19306
  }
19061
19307
  function isJsoncFile(filePath) {
19062
- const base = path29.basename(filePath).toLowerCase();
19308
+ const base = path30.basename(filePath).toLowerCase();
19063
19309
  if (base.endsWith(".jsonc")) return true;
19064
19310
  if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
19065
- const dir = path29.basename(path29.dirname(filePath)).toLowerCase();
19311
+ const dir = path30.basename(path30.dirname(filePath)).toLowerCase();
19066
19312
  return dir === ".vscode";
19067
19313
  }
19068
19314
  async function parseErrors(filePath, content) {
19069
19315
  if (content.length > MAX_CHECK_CHARS) return void 0;
19070
- const ext = path29.extname(filePath).toLowerCase();
19316
+ const ext = path30.extname(filePath).toLowerCase();
19071
19317
  if (ext === ".json" || ext === ".jsonc") {
19072
19318
  try {
19073
19319
  JSON.parse(content);
@@ -19094,7 +19340,7 @@ async function parseErrors(filePath, content) {
19094
19340
  ts2.ScriptKind.JSX
19095
19341
  );
19096
19342
  const sourceFile = ts2.createSourceFile(
19097
- path29.basename(filePath),
19343
+ path30.basename(filePath),
19098
19344
  content,
19099
19345
  ts2.ScriptTarget.Latest,
19100
19346
  /* setParentNodes */
@@ -19121,6 +19367,7 @@ function formatDiag(ts2, diag, content, sourceFile) {
19121
19367
 
19122
19368
  // src/edit.ts
19123
19369
  init_util();
19370
+ var MAX_DIFF_BYTES = 262144;
19124
19371
  var editTool = {
19125
19372
  name: "edit",
19126
19373
  category: "Filesystem",
@@ -19131,46 +19378,62 @@ var editTool = {
19131
19378
  useInstead: ["write", "patch"]
19132
19379
  },
19133
19380
  permission: "confirm",
19381
+ // WS-046: gives permission decisions something to key on — the file being
19382
+ // edited, so trust rules can scope by path.
19383
+ subjectKey: "path",
19134
19384
  mutating: true,
19135
19385
  capabilities: ["fs.write"],
19136
19386
  icon: "edit",
19137
19387
  timeoutMs: 5e3,
19388
+ maxOutputBytes: 262144,
19138
19389
  inputSchema: {
19139
19390
  type: "object",
19140
19391
  properties: {
19141
- path: { type: "string" },
19142
- old_string: { type: "string" },
19143
- new_string: { type: "string" },
19144
- replace_all: { type: "boolean" }
19392
+ path: {
19393
+ type: "string",
19394
+ description: "Path to the file to edit \u2014 relative to the project root, or absolute inside it."
19395
+ },
19396
+ old_string: {
19397
+ type: "string",
19398
+ description: "The exact text to replace, including whitespace and indentation. Must be unique in the file unless `replace_all` is set \u2014 add surrounding lines to disambiguate."
19399
+ },
19400
+ new_string: {
19401
+ type: "string",
19402
+ description: "The exact replacement text (may be empty to delete `old_string`)."
19403
+ },
19404
+ replace_all: {
19405
+ type: "boolean",
19406
+ description: "Replace every occurrence instead of requiring a unique match. Only allowed when `old_string` matches exactly (or up to trailing whitespace) \u2014 fuzzy matches stay single-target."
19407
+ }
19145
19408
  },
19146
19409
  required: ["path", "old_string", "new_string"]
19147
19410
  },
19148
19411
  async execute(input, ctx, opts) {
19149
19412
  if (!input?.path) {
19150
- throw new ToolValidationError({ message: "edit: path is required", field: "path" });
19413
+ throw new ToolValidationError3({ message: "edit: path is required", field: "path" });
19151
19414
  }
19152
19415
  if (input.old_string === void 0) {
19153
- throw new ToolValidationError({
19416
+ throw new ToolValidationError3({
19154
19417
  message: "edit: old_string is required",
19155
19418
  field: "old_string"
19156
19419
  });
19157
19420
  }
19158
19421
  if (input.new_string === void 0) {
19159
- throw new ToolValidationError({
19422
+ throw new ToolValidationError3({
19160
19423
  message: "edit: new_string is required",
19161
19424
  field: "new_string"
19162
19425
  });
19163
19426
  }
19164
19427
  if (input.old_string === "") {
19165
- throw new ToolValidationError({
19428
+ throw new ToolValidationError3({
19166
19429
  message: "edit: old_string cannot be empty",
19167
19430
  field: "old_string"
19168
19431
  });
19169
19432
  }
19170
19433
  const absPath = await safeResolveReal(input.path, ctx);
19171
- const stat19 = await fs24.stat(absPath).catch((err) => {
19434
+ const stat20 = await fs24.stat(absPath).catch((err) => {
19172
19435
  if (err.code === "ENOENT") {
19173
- throw new ToolValidationError({
19436
+ throw new ToolValidationError3({
19174
19437
  message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
19175
19438
  field: "path",
19176
19439
  context: { exists: false }
@@ -19178,8 +19441,8 @@ var editTool = {
19178
19441
  }
19179
19442
  throw err;
19180
19443
  });
19181
- if (!stat19.isFile()) {
19182
- throw new ToolValidationError({
19444
+ if (!stat20.isFile()) {
19445
+ throw new ToolValidationError3({
19183
19446
  message: `edit: "${input.path}" is not a regular file`,
19184
19447
  field: "path"
19185
19448
  });
@@ -19192,7 +19455,7 @@ var editTool = {
19192
19455
  const lastReadHash = ctx.lastReadHash?.(absPath);
19193
19456
  if (lastReadHash !== void 0) {
19194
19457
  if (lastReadHash !== originalHash) {
19195
- throw new ToolValidationError({
19458
+ throw new ToolValidationError3({
19196
19459
  message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
19197
19460
  field: "path",
19198
19461
  context: { reason: "external_modification" }
@@ -19201,15 +19464,15 @@ var editTool = {
19201
19464
  } else {
19202
19465
  const lastReadMtime = ctx.lastReadMtime(absPath);
19203
19466
  if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
19204
- throw new ToolValidationError({
19467
+ throw new ToolValidationError3({
19205
19468
  message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
19206
19469
  field: "path",
19207
19470
  context: { reason: "external_modification" }
19208
19471
  });
19209
19472
  }
19210
19473
  }
19211
- if (autoRead && updated.mtimeMs > stat19.mtimeMs + mtimeTolerance) {
19212
- throw new ToolValidationError({
19474
+ if (autoRead && updated.mtimeMs > stat20.mtimeMs + mtimeTolerance) {
19475
+ throw new ToolValidationError3({
19213
19476
  message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
19214
19477
  field: "path",
19215
19478
  context: { reason: "auto_read_race" }
@@ -19221,6 +19484,9 @@ var editTool = {
19221
19484
  const oldLf = normalizeToLf(input.old_string);
19222
19485
  const newLf = normalizeToLf(input.new_string);
19223
19486
  if (oldLf === newLf) {
19487
+ if (!fileLf.includes(oldLf)) {
19488
+ throw noMatchError(input.path, fileLf, oldLf);
19489
+ }
19224
19490
  if (autoRead) ctx.recordRead(absPath, updated.mtimeMs, "user", originalHash);
19225
19491
  return {
19226
19492
  path: absPath,
@@ -19234,26 +19500,20 @@ var editTool = {
19234
19500
  const ladder = findLadderMatches(fileLf, oldLf);
19235
19501
  if (!ladder) {
19236
19502
  opts?.signal?.throwIfAborted();
19237
- const hint = nearestMatchHint(fileLf, oldLf);
19238
- throw new ToolValidationError({
19239
- message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint.line}:
19240
- ${hint.snippet}
19241
- Compare this against your old_string and retry with the file's actual text.` : ""}`,
19242
- field: "old_string"
19243
- });
19503
+ throw noMatchError(input.path, fileLf, oldLf);
19244
19504
  }
19245
19505
  const { tier, matches } = ladder;
19246
19506
  const count = matches.length;
19247
19507
  if (ladder.ambiguous) {
19248
19508
  const lines = matches.map((m) => m.startLine);
19249
- throw new ToolValidationError({
19509
+ throw new ToolValidationError3({
19250
19510
  message: `edit: old_string only matched fuzzily and ${count} candidate blocks scored too close to distinguish (lines: ${lines.join(", ")}) in "${input.path}". Re-read the file and use the exact text of the intended block.`,
19251
19511
  field: "old_string",
19252
19512
  context: { occurrences: count, matchTier: tier }
19253
19513
  });
19254
19514
  }
19255
19515
  if (input.replace_all && tier !== "exact" && tier !== "trailing-whitespace") {
19256
- throw new ToolValidationError({
19516
+ throw new ToolValidationError3({
19257
19517
  message: `edit: old_string only matched via ${TIER_LABEL[tier]} in "${input.path}", but replace_all requires an exact (or trailing-whitespace) match. Re-read the file and use its exact text.`,
19258
19518
  field: "old_string",
19259
19519
  context: { matchTier: tier }
@@ -19261,7 +19521,7 @@ Compare this against your old_string and retry with the file's actual text.` : "
19261
19521
  }
19262
19522
  if (count > 1 && !input.replace_all) {
19263
19523
  const lines = matches.map((m) => m.startLine);
19264
- throw new ToolValidationError({
19524
+ throw new ToolValidationError3({
19265
19525
  message: `edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")})${tier === "exact" ? "" : ` via ${TIER_LABEL[tier]}`}. Add more context to make it unique, or set replace_all: true.`,
19266
19526
  field: "old_string",
19267
19527
  context: { occurrences: count, matchTier: tier }
@@ -19302,16 +19562,20 @@ Compare this against your old_string and retry with the file's actual text.` : "
19302
19562
  after: newFile
19303
19563
  });
19304
19564
  opts?.signal?.throwIfAborted();
19305
- const diff = unifiedDiff(original, newFile, {
19306
- fromFile: input.path,
19307
- toFile: input.path
19308
- });
19565
+ const { text: diff, truncated: diffTruncated } = truncateDiffPayload(
19566
+ unifiedDiff(original, newFile, {
19567
+ fromFile: input.path,
19568
+ toFile: input.path
19569
+ }),
19570
+ MAX_DIFF_BYTES
19571
+ );
19572
+ const diffNote = diffTruncated ? "Diff truncated to the 256 KiB output budget \u2014 the full edit is on disk." : void 0;
19309
19573
  const syntax = await checkSyntax(absPath, newFile, original).catch(() => void 0);
19310
19574
  let syntaxNote;
19311
19575
  if (syntax && syntax.errors.length > 0) {
19312
19576
  syntaxNote = syntax.preExisting ? `Syntax check: the file still has parse errors (they pre-date this edit) \u2014 see syntax_errors.` : `Syntax check: this edit introduced ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.`;
19313
19577
  }
19314
- const notes = [autoReadNote, tierNote, syntaxNote].filter(Boolean);
19578
+ const notes = [autoReadNote, tierNote, diffNote, syntaxNote].filter(Boolean);
19315
19579
  return {
19316
19580
  path: absPath,
19317
19581
  replacements: input.replace_all ? count : 1,
@@ -19322,6 +19586,15 @@ Compare this against your old_string and retry with the file's actual text.` : "
19322
19586
  };
19323
19587
  }
19324
19588
  };
19589
+ function noMatchError(inputPath, fileLf, oldLf) {
19590
+ const hint = nearestMatchHint(fileLf, oldLf);
19591
+ return new ToolValidationError3({
19592
+ message: `edit: no match for old_string in "${inputPath}".${hint ? ` Nearest match near line ${hint.line}:
19593
+ ${hint.snippet}
19594
+ Compare this against your old_string and retry with the file's actual text.` : ""}`,
19595
+ field: "old_string"
19596
+ });
19597
+ }
19325
19598
 
19326
19599
  // src/exec.ts
19327
19600
  import { spawn as spawn8 } from "node:child_process";
@@ -19330,14 +19603,14 @@ import {
19330
19603
  emitProcessOutput as emitProcessOutput3,
19331
19604
  emitProcessStarted as emitProcessStarted3
19332
19605
  } from "@wrongstack/core/observability";
19333
- import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils/error";
19606
+ import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils/error";
19334
19607
  init_output_spool();
19335
19608
  init_util();
19336
19609
  init_win32_resolve();
19337
19610
 
19338
19611
  // src/exec-kill-guard.ts
19339
19612
  import * as os8 from "node:os";
19340
- import * as path30 from "node:path";
19613
+ import * as path31 from "node:path";
19341
19614
  var isWin3 = os8.platform() === "win32";
19342
19615
  async function checkExecKillCommand(cmd, args) {
19343
19616
  if (!cmd) return { blocked: false };
@@ -19550,7 +19823,7 @@ async function checkKillTarget(target) {
19550
19823
  reason: `Blocked: kill ${target.signal} '${target.name}' targets a WrongStack process name.`
19551
19824
  };
19552
19825
  }
19553
- const currentImage = path30.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
19826
+ const currentImage = path31.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
19554
19827
  const targetsNodeRuntime = nameLower === "node" || nameLower.startsWith("node");
19555
19828
  if (targetsNodeRuntime && currentImage === "node") {
19556
19829
  return {
@@ -20201,6 +20474,7 @@ function getExecAllowlist() {
20201
20474
  var MAX_ARGS = 20;
20202
20475
  var MAX_OUTPUT2 = 2e5;
20203
20476
  var DEFAULT_TIMEOUT_MS3 = 3e4;
20477
+ var MAX_TIMEOUT_MS = 6e5;
20204
20478
  var BLOCKED_ARG_PATTERNS = {
20205
20479
  python: [],
20206
20480
  // git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack;
@@ -20326,8 +20600,8 @@ var SAFE_DANGER = { level: "safe", reasons: [] };
20326
20600
  var execTool = {
20327
20601
  name: "exec",
20328
20602
  category: "Shell",
20329
- description: "Execute a **whitelisted, restricted set of commands** with strict argument validation. This is the **preferred and safer** alternative to the `bash` tool for running development tools (node, npm, pnpm, tsc, git, tests, linters, etc.). It prevents arbitrary command injection and limits what the model can do.",
20330
- usageHint: "PREFERRED SHELL TOOL for most cases.\n\nUse this instead of `bash` whenever possible.\n- `command` must be in the allowlist. Defaults cover JS (node/npm/pnpm/yarn/bun/deno/tsc/vitest/eslint/biome), Go (`go build`/`go test`), Rust (cargo), Python (python/pip), Ruby (gem/bundle), JVM (java/mvn/gradle), .NET (dotnet), native (make/cmake), and git. Users can extend it via `tools.exec.allow` in config.\n- Arguments are passed as a clean array (no shell interpretation).\n- `cwd` is validated to stay inside the project.\n- If a command is not allowlisted, the error explains how to add it; for one-off arbitrary commands, fall back to `bash` (with strong justification).\nThis tool significantly reduces the risk compared to full shell access.",
20603
+ description: "Execute a command from a **curated command roster** with argument validation and confirm gating. This is the **preferred** alternative to the `bash` tool for running development tools (node, npm, pnpm, tsc, git, tests, linters, etc.). It is NOT a sandbox \u2014 several rostered commands (node, python, powershell, \u2026) can run arbitrary code \u2014 so prefer least-privilege commands.",
20604
+ usageHint: "PREFERRED SHELL TOOL for most cases.\n\nUse this instead of `bash` whenever possible.\n- `command` must be in the allowlist. Defaults cover JS (node/npm/pnpm/yarn/bun/deno/tsc/vitest/eslint/biome), Go (`go build`/`go test`), Rust (cargo), Python (python/pip), Ruby (gem/bundle), JVM (java/mvn/gradle), .NET (dotnet), native (make/cmake), and git. Users can extend it via `tools.exec.allow` in config.\n- Arguments are passed as a clean array (no shell interpretation).\n- `cwd` is validated to stay inside the project.\n- If a command is not allowlisted, the error explains how to add it; for one-off arbitrary commands, fall back to `bash` (with strong justification).\nThe curated roster + confirm gating narrows the surface compared to full shell access, but this is not a sandbox \u2014 prefer least-privilege commands.",
20331
20605
  selection: {
20332
20606
  doNotUseWhen: "the operation requires pipes, redirection, shell expansion, or a non-allowlisted command.",
20333
20607
  useInstead: ["bash"]
@@ -20343,7 +20617,13 @@ var execTool = {
20343
20617
  subjectKey: "command",
20344
20618
  mutating: true,
20345
20619
  riskTier: "standard",
20346
- timeoutMs: DEFAULT_TIMEOUT_MS3,
20620
+ // Executor-level abort ceiling. Must sit ABOVE the per-call timeout ceiling
20621
+ // (MAX_TIMEOUT_MS): the tool's own timer resolves with exit 124 + registry
20622
+ // tree-kill; the executor's AbortSignal.timeout is a blunt abort that would
20623
+ // otherwise fire first and discard the structured timeout result. The 10s
20624
+ // margin covers the kill/teardown window. (The executor additionally clamps
20625
+ // to config `tools.maxToolTimeoutMs`.)
20626
+ timeoutMs: MAX_TIMEOUT_MS + 1e4,
20347
20627
  capabilities: ["shell.restricted"],
20348
20628
  icon: "terminal",
20349
20629
  inputSchema: {
@@ -20364,7 +20644,7 @@ var execTool = {
20364
20644
  },
20365
20645
  timeout: {
20366
20646
  type: "integer",
20367
- description: "Per-command timeout in milliseconds."
20647
+ description: "Per-command timeout in milliseconds (default 30000, max 600000)."
20368
20648
  }
20369
20649
  },
20370
20650
  required: ["command"]
@@ -20408,7 +20688,7 @@ var execTool = {
20408
20688
  };
20409
20689
  }
20410
20690
  const args = (input.args ?? []).slice(0, MAX_ARGS);
20411
- const timeout = Math.max(1, Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS3, DEFAULT_TIMEOUT_MS3));
20691
+ const timeout = Math.max(1, Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS3, MAX_TIMEOUT_MS));
20412
20692
  const danger = detectDanger(cmd, args, dangerBypass);
20413
20693
  const killCheck = await checkExecKillCommand(cmd, args);
20414
20694
  if (killCheck.blocked) {
@@ -20436,15 +20716,16 @@ var execTool = {
20436
20716
  danger
20437
20717
  };
20438
20718
  }
20719
+ const defaultCwd = ctx.workingDir ?? ctx.cwd;
20439
20720
  let cwd;
20440
20721
  try {
20441
- cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : await safeResolveReal(ctx.cwd, ctx);
20722
+ cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : await safeResolveReal(defaultCwd, ctx);
20442
20723
  } catch {
20443
20724
  return {
20444
20725
  command: cmd,
20445
20726
  args,
20446
20727
  stdout: "",
20447
- stderr: `cwd "${input.cwd ?? ctx.cwd}" resolves outside project root`,
20728
+ stderr: `cwd "${input.cwd ?? defaultCwd}" resolves outside project root`,
20448
20729
  exitCode: 1,
20449
20730
  truncated: false,
20450
20731
  allowed: false,
@@ -20456,7 +20737,7 @@ var execTool = {
20456
20737
  }
20457
20738
  };
20458
20739
  function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
20459
- return new Promise((resolve17) => {
20740
+ return new Promise((resolve18) => {
20460
20741
  let stdout = "";
20461
20742
  let stderr = "";
20462
20743
  let killed = false;
@@ -20464,7 +20745,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
20464
20745
  const finish = (result) => {
20465
20746
  if (resolvedOnce.value) return;
20466
20747
  resolvedOnce.value = true;
20467
- resolve17(result);
20748
+ resolve18(result);
20468
20749
  };
20469
20750
  const startedAt = Date.now();
20470
20751
  let stdoutBytes = 0;
@@ -20516,7 +20797,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
20516
20797
  command: cmd,
20517
20798
  args,
20518
20799
  stdout: "",
20519
- stderr: `spawn failed: ${toErrorMessage3(err)}`,
20800
+ stderr: `spawn failed: ${toErrorMessage6(err)}`,
20520
20801
  exitCode: 1,
20521
20802
  truncated: false,
20522
20803
  allowed: true,
@@ -20619,7 +20900,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
20619
20900
  }
20620
20901
 
20621
20902
  // src/fetch.ts
20622
- import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValidationError3 } from "@wrongstack/core/types";
20903
+ import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
20623
20904
  import TurndownService from "turndown";
20624
20905
 
20625
20906
  // src/_fetch-guard.ts
@@ -20629,7 +20910,7 @@ import {
20629
20910
  isPrivateIPv4 as isPrivateIPv42,
20630
20911
  isPrivateIPv6 as isPrivateIPv62
20631
20912
  } from "@wrongstack/core/utils";
20632
- import { FetchError, ToolValidationError as ToolValidationError2 } from "@wrongstack/core/types";
20913
+ import { FetchError, ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
20633
20914
  import { Agent, fetch as undiciFetch } from "undici";
20634
20915
  var nativeGlobalFetch = globalThis.fetch;
20635
20916
  var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
@@ -20700,13 +20981,13 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
20700
20981
  for (; ; ) {
20701
20982
  const parsed = new URL(currentUrl);
20702
20983
  if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
20703
- throw new ToolValidationError2({
20984
+ throw new ToolValidationError4({
20704
20985
  message: `fetch: redirect to unsupported protocol "${parsed.protocol}"`,
20705
20986
  field: "url"
20706
20987
  });
20707
20988
  }
20708
20989
  if (parsed.protocol === "http:" && !ALLOW_PRIVATE) {
20709
- throw new ToolValidationError2({
20990
+ throw new ToolValidationError4({
20710
20991
  message: "fetch: redirect to http:// blocked (HTTPS required by default)",
20711
20992
  field: "url"
20712
20993
  });
@@ -20722,6 +21003,10 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
20722
21003
  if (res.status < 300 || res.status > 399) {
20723
21004
  return res;
20724
21005
  }
21006
+ try {
21007
+ await res.body?.cancel();
21008
+ } catch {
21009
+ }
20725
21010
  redirectCount++;
20726
21011
  if (redirectCount > maxRedirects) {
20727
21012
  throw new FetchError({
@@ -20745,7 +21030,7 @@ async function assertNotPrivate(hostname4) {
20745
21030
  if (ALLOW_PRIVATE) return;
20746
21031
  const host = hostname4.startsWith("[") && hostname4.endsWith("]") ? hostname4.slice(1, -1) : hostname4;
20747
21032
  if (host === "localhost" || host.endsWith(".localhost")) {
20748
- throw new ToolValidationError2({
21033
+ throw new ToolValidationError4({
20749
21034
  message: "fetch: blocked localhost target",
20750
21035
  field: "url"
20751
21036
  });
@@ -20753,14 +21038,14 @@ async function assertNotPrivate(hostname4) {
20753
21038
  const ipVersion = net4.isIP(host);
20754
21039
  if (ipVersion === 4) {
20755
21040
  if (isPrivateIPv42(host)) {
20756
- throw new ToolValidationError2({
21041
+ throw new ToolValidationError4({
20757
21042
  message: `fetch: blocked private/loopback address "${host}"`,
20758
21043
  field: "url"
20759
21044
  });
20760
21045
  }
20761
21046
  } else if (ipVersion === 6) {
20762
21047
  if (isPrivateIPv62(host)) {
20763
- throw new ToolValidationError2({
21048
+ throw new ToolValidationError4({
20764
21049
  message: `fetch: blocked private/loopback address "${host}"`,
20765
21050
  field: "url"
20766
21051
  });
@@ -20771,14 +21056,14 @@ async function assertNotPrivate(hostname4) {
20771
21056
  for (const r of records) {
20772
21057
  const bad = r.family === 4 ? isPrivateIPv42(r.address) : isPrivateIPv62(r.address);
20773
21058
  if (bad) {
20774
- throw new ToolValidationError2({
21059
+ throw new ToolValidationError4({
20775
21060
  message: `fetch: resolved to private address ${r.address}`,
20776
21061
  field: "url"
20777
21062
  });
20778
21063
  }
20779
21064
  }
20780
21065
  } catch (err) {
20781
- if (err instanceof ToolValidationError2) throw err;
21066
+ if (err instanceof ToolValidationError4) throw err;
20782
21067
  }
20783
21068
  }
20784
21069
  }
@@ -20795,6 +21080,8 @@ TD.addRule("stripDangerousElements", {
20795
21080
  filter: ["script", "style", "noscript"],
20796
21081
  replacement: () => ""
20797
21082
  });
21083
+ var PRUNED_BOILERPLATE_TAGS = /* @__PURE__ */ new Set(["nav", "header", "footer", "aside", "svg", "iframe"]);
21084
+ TD.remove((node) => PRUNED_BOILERPLATE_TAGS.has(node.nodeName.toLowerCase()));
20798
21085
  var MAX_BYTES = 131072;
20799
21086
  var TIMEOUT_MS = 2e4;
20800
21087
  var combineSignals = (signals) => AbortSignal.any(signals);
@@ -20824,7 +21111,7 @@ var fetchTool = {
20824
21111
  format: {
20825
21112
  type: "string",
20826
21113
  enum: ["markdown", "text", "raw"],
20827
- description: 'Output format. "markdown" is recommended for HTML pages.'
21114
+ description: 'Output format. "markdown" is recommended for HTML pages; for non-HTML content types it falls back to plain text (JSON is pretty-printed).'
20828
21115
  }
20829
21116
  },
20830
21117
  required: ["url"]
@@ -20853,20 +21140,26 @@ var fetchTool = {
20853
21140
  },
20854
21141
  async *executeStream(input, ctx, opts) {
20855
21142
  if (!input?.url) {
20856
- throw new ToolValidationError3({
21143
+ throw new ToolValidationError5({
20857
21144
  message: "fetch: url is required",
20858
21145
  field: "url"
20859
21146
  });
20860
21147
  }
20861
21148
  const u = new URL(input.url);
21149
+ if (u.username || u.password) {
21150
+ throw new ToolValidationError5({
21151
+ message: "fetch: URLs with embedded credentials (user:pass@host) are not allowed",
21152
+ field: "url"
21153
+ });
21154
+ }
20862
21155
  if (u.protocol !== "https:" && u.protocol !== "http:") {
20863
- throw new ToolValidationError3({
21156
+ throw new ToolValidationError5({
20864
21157
  message: `fetch: unsupported protocol "${u.protocol}"`,
20865
21158
  field: "url"
20866
21159
  });
20867
21160
  }
20868
21161
  if (u.protocol === "http:" && !ALLOW_PRIVATE) {
20869
- throw new ToolValidationError3({
21162
+ throw new ToolValidationError5({
20870
21163
  message: "fetch: http:// blocked (HTTPS required by default)",
20871
21164
  field: "url"
20872
21165
  });
@@ -21056,8 +21349,9 @@ var formatTool = {
21056
21349
  type: "final",
21057
21350
  output: {
21058
21351
  fixer: bridge.language,
21059
- files_checked: 0,
21060
- files_changed: run.summary.errors > 0 ? 0 : 1,
21352
+ // Language-bridge runs don't report per-file counts.
21353
+ files_checked: void 0,
21354
+ files_changed: void 0,
21061
21355
  output: normalizeCommandOutput(run.output || run.error || ""),
21062
21356
  truncated: run.truncated
21063
21357
  }
@@ -21084,11 +21378,14 @@ var formatTool = {
21084
21378
  text: `Running ${detected}\u2026`,
21085
21379
  data: { fixer: detected, check: !!input.check }
21086
21380
  };
21087
- const args = ["format", "--write"];
21088
- if (input.check) args[args.length - 1] = "--check";
21089
- if (input.files) {
21090
- const files = Array.isArray(input.files) ? input.files : input.files.split(",");
21091
- args.push("--", ...files.map((f) => f.trim()));
21381
+ const fileList = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((f) => f.trim()) : [];
21382
+ let args;
21383
+ if (detected === "prettier") {
21384
+ args = [input.check ? "--check" : "--write"];
21385
+ args.push(...fileList.length > 0 ? fileList : ["."]);
21386
+ } else {
21387
+ args = ["format", input.check ? "--check" : "--write"];
21388
+ if (fileList.length > 0) args.push("--", ...fileList);
21092
21389
  }
21093
21390
  const result = yield* spawnStream({
21094
21391
  cmd: detected,
@@ -21097,39 +21394,70 @@ var formatTool = {
21097
21394
  signal: opts.signal,
21098
21395
  maxBytes: 1e5
21099
21396
  });
21100
- const changed = [...result.stdout.matchAll(/\bchanged\b/gi)].length;
21397
+ const combinedOut = `${result.stdout}
21398
+ ${result.stderr}`;
21399
+ const counts = parseFormatterCounts(detected, combinedOut);
21101
21400
  yield {
21102
21401
  type: "final",
21103
21402
  output: {
21104
21403
  fixer: detected,
21105
- files_checked: 0,
21106
- files_changed: changed,
21404
+ files_checked: counts.checked,
21405
+ files_changed: counts.changed,
21107
21406
  output: normalizeCommandOutput(result.stdout || result.stderr || result.error || ""),
21108
21407
  truncated: result.truncated
21109
21408
  }
21110
21409
  };
21111
21410
  }
21112
21411
  };
21412
+ function parseFormatterCounts(fixer, output) {
21413
+ if (fixer !== "biome") return { checked: void 0, changed: void 0 };
21414
+ const checkedMatch = /\b(?:Checked|Formatted)\s+(\d+)\s+files?\b/i.exec(output);
21415
+ const changedMatch = /\bFixed\s+(\d+)\s+files?\b/i.exec(output);
21416
+ return {
21417
+ checked: checkedMatch?.[1] !== void 0 ? Number(checkedMatch[1]) : void 0,
21418
+ changed: changedMatch?.[1] !== void 0 ? Number(changedMatch[1]) : void 0
21419
+ };
21420
+ }
21113
21421
  async function detectFixer(cwd) {
21114
- const { stat: stat19 } = await import("node:fs/promises");
21115
- try {
21116
- await stat19(`${cwd}/biome.json`);
21117
- return "biome";
21118
- } catch {
21422
+ const fs36 = await import("node:fs/promises");
21423
+ const exists = async (file) => {
21119
21424
  try {
21120
- await stat19(`${cwd}/.prettierrc`);
21121
- return "prettier";
21425
+ await fs36.stat(`${cwd}/${file}`);
21426
+ return true;
21122
21427
  } catch {
21123
- return "biome";
21428
+ return false;
21124
21429
  }
21430
+ };
21431
+ if (await exists("biome.json") || await exists("biome.jsonc")) return "biome";
21432
+ const PRETTIER_CONFIGS = [
21433
+ ".prettierrc",
21434
+ ".prettierrc.json",
21435
+ ".prettierrc.yml",
21436
+ ".prettierrc.yaml",
21437
+ ".prettierrc.js",
21438
+ ".prettierrc.cjs",
21439
+ ".prettierrc.mjs",
21440
+ "prettier.config.js",
21441
+ "prettier.config.cjs",
21442
+ "prettier.config.mjs"
21443
+ ];
21444
+ for (const cfg of PRETTIER_CONFIGS) {
21445
+ if (await exists(cfg)) return "prettier";
21446
+ }
21447
+ try {
21448
+ const raw = await fs36.readFile(`${cwd}/package.json`, "utf8");
21449
+ const pkg = JSON.parse(raw);
21450
+ if (pkg["prettier"] !== void 0) return "prettier";
21451
+ } catch {
21125
21452
  }
21453
+ return "biome";
21126
21454
  }
21127
21455
 
21128
21456
  // src/git.ts
21129
21457
  init_util();
21130
21458
  import { spawn as spawn9 } from "node:child_process";
21131
21459
  import { statSync as statSync4 } from "node:fs";
21132
- import { dirname as dirname14, resolve as resolve13, sep as sep6 } from "node:path";
21460
+ import { dirname as dirname13, resolve as resolve14, sep as sep6 } from "node:path";
21133
21461
  import { assessCommitSafety } from "@wrongstack/core/coordination";
21134
21462
  import { buildChildEnv as buildChildEnv4 } from "@wrongstack/core/utils";
21135
21463
  var TIMEOUT_MS2 = 3e4;
@@ -21290,8 +21618,8 @@ function validateWorktreeInput(input, projectRoot) {
21290
21618
  return reject(`unsafe worktree path: ${input.worktreePath}`);
21291
21619
  }
21292
21620
  if ((input.worktreeAction === "add" || input.worktreeAction === "remove") && input.worktreePath) {
21293
- const root = resolve13(projectRoot);
21294
- const abs = resolve13(root, input.worktreePath);
21621
+ const root = resolve14(projectRoot);
21622
+ const abs = resolve14(root, input.worktreePath);
21295
21623
  if (abs !== root && !abs.startsWith(root + sep6)) {
21296
21624
  return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);
21297
21625
  }
@@ -21303,12 +21631,12 @@ function findGitDir2(cwd, projectRoot) {
21303
21631
  let dir = cwd;
21304
21632
  for (let i = 0; i < 20; i++) {
21305
21633
  try {
21306
- const stat19 = statSync4(`${dir}/.git`);
21307
- if (stat19.isDirectory() || stat19.isFile()) return dir;
21634
+ const stat20 = statSync4(`${dir}/.git`);
21635
+ if (stat20.isDirectory() || stat20.isFile()) return dir;
21308
21636
  } catch {
21309
21637
  }
21310
21638
  if (dir === root) break;
21311
- const parent = dirname14(dir);
21639
+ const parent = dirname13(dir);
21312
21640
  if (parent === dir) break;
21313
21641
  dir = parent;
21314
21642
  }
@@ -21385,7 +21713,7 @@ function buildArgs(input) {
21385
21713
  }
21386
21714
  }
21387
21715
  function runGit2(args, cwd, signal) {
21388
- return new Promise((resolve17) => {
21716
+ return new Promise((resolve18) => {
21389
21717
  let stdout = "";
21390
21718
  let stderr = "";
21391
21719
  const child = spawn9("git", args, {
@@ -21406,7 +21734,7 @@ function runGit2(args, cwd, signal) {
21406
21734
  }
21407
21735
  });
21408
21736
  child.on("error", (err) => {
21409
- resolve17({
21737
+ resolve18({
21410
21738
  command: args[0],
21411
21739
  stdout: normalizeCommandOutput(stdout),
21412
21740
  stderr: err.message,
@@ -21415,7 +21743,7 @@ function runGit2(args, cwd, signal) {
21415
21743
  });
21416
21744
  });
21417
21745
  child.on("close", (code) => {
21418
- resolve17({
21746
+ resolve18({
21419
21747
  command: args[0],
21420
21748
  stdout: normalizeCommandOutput(stdout),
21421
21749
  stderr: normalizeCommandOutput(stderr),
@@ -21428,8 +21756,9 @@ function runGit2(args, cwd, signal) {
21428
21756
 
21429
21757
  // src/glob.ts
21430
21758
  import * as fs25 from "node:fs/promises";
21431
- import * as path31 from "node:path";
21759
+ import * as path32 from "node:path";
21432
21760
  import { compileGlob as compileGlob2, DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS2 } from "@wrongstack/core/utils";
21761
+ import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
21433
21762
 
21434
21763
  // src/_concurrency.ts
21435
21764
  async function mapWithConcurrency2(items, limit, fn) {
@@ -21455,7 +21784,7 @@ var WALK_CONCURRENCY = 16;
21455
21784
  var globTool = {
21456
21785
  name: "glob",
21457
21786
  category: "Filesystem",
21458
- description: "Find files by path pattern. Use index-backed `codebase-search` first for code symbols or concepts when it is live.",
21787
+ description: "Find files by path pattern. Results are sorted by modification time, newest first, so when the list is truncated at `limit` it is the oldest files that are dropped (recency-biased). Use index-backed `codebase-search` first for code symbols or concepts when it is live.",
21459
21788
  usageHint: "PATH DISCOVERY AND SEARCH SCOPING:\n\n- When `codebase-search` is live, use it first for code concepts; use `glob` for filenames, path patterns, and non-indexed files.\n- Combine with `path` and `limit`.\n- Default ignores common build/dependency directories.\nMuch more efficient than shell `find` for most use cases inside the agent.",
21460
21789
  selection: {
21461
21790
  doNotUseWhen: "you need to search inside file contents.",
@@ -21466,7 +21795,7 @@ var globTool = {
21466
21795
  capabilities: ["fs.read"],
21467
21796
  icon: "folder",
21468
21797
  maxOutputBytes: 65536,
21469
- timeoutMs: 5e3,
21798
+ timeoutMs: 15e3,
21470
21799
  inputSchema: {
21471
21800
  type: "object",
21472
21801
  properties: {
@@ -21480,13 +21809,20 @@ var globTool = {
21480
21809
  },
21481
21810
  limit: {
21482
21811
  type: "integer",
21483
- description: "Maximum number of results to return (default 1000, max 5000)."
21812
+ minimum: 1,
21813
+ maximum: 5e3,
21814
+ description: "Maximum number of results to return (default 1000, max 5000). Results are sorted by mtime descending, so truncation keeps the most recently modified files."
21484
21815
  }
21485
21816
  },
21486
21817
  required: ["pattern"]
21487
21818
  },
21488
21819
  async execute(input, ctx, opts) {
21489
- if (!input?.pattern) throw new Error("glob: pattern is required");
21820
+ if (!input?.pattern) {
21821
+ throw new ToolValidationError6({
21822
+ message: "glob: pattern is required",
21823
+ field: "pattern"
21824
+ });
21825
+ }
21490
21826
  const signal = opts?.signal;
21491
21827
  const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
21492
21828
  const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
@@ -21531,7 +21867,7 @@ var globTool = {
21531
21867
  const name = e.name;
21532
21868
  if (DEFAULT_IGNORE2.has(name)) continue;
21533
21869
  const rel = relPrefix ? `${relPrefix}/${name}` : name;
21534
- const full = path31.join(dir, name);
21870
+ const full = path32.join(dir, name);
21535
21871
  if (e.isDirectory()) {
21536
21872
  if (isGitIgnored(rel, true)) continue;
21537
21873
  subdirs.push({ full, rel });
@@ -21581,8 +21917,8 @@ var globTool = {
21581
21917
  // src/grep.ts
21582
21918
  import { spawn as spawn10 } from "node:child_process";
21583
21919
  import * as fs26 from "node:fs/promises";
21584
- import * as path32 from "node:path";
21585
- import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
21920
+ import * as path33 from "node:path";
21921
+ import { ToolValidationError as ToolValidationError7 } from "@wrongstack/core/types";
21586
21922
  import {
21587
21923
  buildChildEnv as buildChildEnv5,
21588
21924
  compileGlob as compileGlob3,
@@ -21786,7 +22122,7 @@ var grepTool = {
21786
22122
  },
21787
22123
  async *executeStream(input, ctx, opts) {
21788
22124
  if (!input?.pattern) {
21789
- throw new ToolValidationError4({
22125
+ throw new ToolValidationError7({
21790
22126
  message: "grep: pattern is required",
21791
22127
  field: "pattern"
21792
22128
  });
@@ -21796,12 +22132,12 @@ var grepTool = {
21796
22132
  const limit = Math.max(1, Math.min(input.limit ?? 200, 2e3));
21797
22133
  const validation = compileUserRegex(input.pattern, input.case_insensitive ? "i" : "");
21798
22134
  if (!validation.ok) {
21799
- throw new ToolValidationError4({
22135
+ throw new ToolValidationError7({
21800
22136
  message: `grep: ${validation.reason}`,
21801
22137
  field: "pattern"
21802
22138
  });
21803
22139
  }
21804
- const rgAvailable = await detectRg(opts.signal);
22140
+ const rgAvailable = await detectRg();
21805
22141
  if (rgAvailable) {
21806
22142
  try {
21807
22143
  yield* runRgStream(input, base, mode, limit, opts.signal);
@@ -21814,16 +22150,23 @@ var grepTool = {
21814
22150
  yield { type: "final", output: out };
21815
22151
  }
21816
22152
  };
21817
- async function detectRg(signal) {
21818
- return new Promise((resolve17) => {
22153
+ var rgAvailabilityCache;
22154
+ function detectRg() {
22155
+ rgAvailabilityCache ??= new Promise((resolve18) => {
21819
22156
  try {
21820
- const p = spawn10("rg", ["--version"], { env: buildChildEnv5(), stdio: "ignore", signal, windowsHide: true });
21821
- p.on("error", () => resolve17(false));
21822
- p.on("close", (code) => resolve17(code === 0));
22157
+ const p = spawn10("rg", ["--version"], {
22158
+ env: buildChildEnv5(),
22159
+ stdio: "ignore",
22160
+ signal: AbortSignal.timeout(1e4),
22161
+ windowsHide: true
22162
+ });
22163
+ p.on("error", () => resolve18(false));
22164
+ p.on("close", (code) => resolve18(code === 0));
21823
22165
  } catch {
21824
- resolve17(false);
22166
+ resolve18(false);
21825
22167
  }
21826
22168
  });
22169
+ return rgAvailabilityCache;
21827
22170
  }
21828
22171
  async function* runRgStream(input, base, mode, limit, signal) {
21829
22172
  const args = ["--no-heading"];
@@ -21837,7 +22180,7 @@ async function* runRgStream(input, base, mode, limit, signal) {
21837
22180
  for (const ignored of DEFAULT_IGNORE3) {
21838
22181
  args.push("--glob", `!${ignored}/**`, "--glob", `!**/${ignored}/**`);
21839
22182
  }
21840
- const gitignorePath = path32.join(base, ".gitignore");
22183
+ const gitignorePath = path33.join(base, ".gitignore");
21841
22184
  if (await fs26.access(gitignorePath).then(() => true, () => false)) {
21842
22185
  args.push("--ignore-file", gitignorePath);
21843
22186
  }
@@ -22009,7 +22352,7 @@ async function runNative(input, base, mode, limit, signal) {
22009
22352
  const flags = input.case_insensitive ? "i" : "";
22010
22353
  const compiled = compileUserRegex(input.pattern, flags);
22011
22354
  if (!compiled.ok) {
22012
- throw new ToolValidationError4({
22355
+ throw new ToolValidationError7({
22013
22356
  message: `grep: ${compiled.reason}`,
22014
22357
  field: "pattern"
22015
22358
  });
@@ -22027,8 +22370,8 @@ async function runNative(input, base, mode, limit, signal) {
22027
22370
  if (globRe && !globRe.test(name) && !globRe.test(full)) return;
22028
22371
  if (globRe) globRe.lastIndex = 0;
22029
22372
  try {
22030
- const stat19 = await fs26.stat(full);
22031
- if (!stat19.isFile() || stat19.size > maxBytes || stopped || signal.aborted) return;
22373
+ const stat20 = await fs26.stat(full);
22374
+ if (!stat20.isFile() || stat20.size > maxBytes || stopped || signal.aborted) return;
22032
22375
  const file = await fs26.open(full, "r");
22033
22376
  try {
22034
22377
  let bytesReadTotal = 0;
@@ -22119,7 +22462,7 @@ async function runNative(input, base, mode, limit, signal) {
22119
22462
  if (DEFAULT_IGNORE3.has(e.name)) continue;
22120
22463
  if (e.isSymbolicLink()) continue;
22121
22464
  const rel = relPrefix ? `${relPrefix}/${e.name}` : e.name;
22122
- const full = path32.join(dir, e.name);
22465
+ const full = path33.join(dir, e.name);
22123
22466
  if (e.isDirectory()) {
22124
22467
  if (isGitIgnored(rel, true)) continue;
22125
22468
  subdirs.push({ full, rel });
@@ -22239,26 +22582,14 @@ var installTool = {
22239
22582
  return;
22240
22583
  }
22241
22584
  }
22242
- const pkgManager = await detectPackageManager(cwd);
22585
+ const pkgManager = await detectPackageManager(cwd, ctx.projectRoot);
22243
22586
  yield { type: "log", text: `Resolving with ${pkgManager}\u2026`, data: { phase: "resolve" } };
22244
- const save = input.save === "dev" ? "-D" : input.save === "optional" ? "-O" : "";
22245
22587
  const globalFlag = input.global ? ["-g"] : [];
22246
22588
  const ignoreScripts = input.lifecycleScripts !== true;
22247
- const args = [];
22248
- if (input.dry_run) args.push("--dry-run");
22249
- if (ignoreScripts) args.push("--ignore-scripts");
22250
- if (pkgManager === "pnpm") {
22251
- if (save) args.push(save);
22252
- args.push("add", ...globalFlag);
22253
- } else if (pkgManager === "yarn") {
22254
- args.push("add", ...globalFlag);
22255
- } else {
22256
- args.push("install", ...globalFlag);
22257
- }
22258
22589
  const pkgList = input.packages ? (Array.isArray(input.packages) ? input.packages : input.packages.split(",")).map(
22259
22590
  (p) => p.trim()
22260
22591
  ) : [];
22261
- const PKG_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i;
22592
+ const PKG_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9^~><=*.+-]+)?$/i;
22262
22593
  for (const pkg of pkgList) {
22263
22594
  if (!PKG_NAME_RE.test(pkg) || pkg.startsWith("-") || pkg.length > 200) {
22264
22595
  yield {
@@ -22274,7 +22605,34 @@ var installTool = {
22274
22605
  return;
22275
22606
  }
22276
22607
  }
22277
- if (pkgList.length > 0) args.push(...pkgList);
22608
+ const hasPkgs = pkgList.length > 0;
22609
+ const args = [];
22610
+ if (input.dry_run) args.push("--dry-run");
22611
+ if (ignoreScripts) args.push("--ignore-scripts");
22612
+ if (pkgManager === "pnpm") {
22613
+ if (hasPkgs) {
22614
+ if (input.save === "dev") args.push("-D");
22615
+ else if (input.save === "optional") args.push("-O");
22616
+ args.push("add", ...globalFlag);
22617
+ } else {
22618
+ args.push("install", ...globalFlag);
22619
+ }
22620
+ } else if (pkgManager === "yarn") {
22621
+ if (hasPkgs) {
22622
+ args.push("add", ...globalFlag);
22623
+ if (input.save === "dev") args.push("--dev");
22624
+ else if (input.save === "optional") args.push("--optional");
22625
+ } else {
22626
+ args.push("install", ...globalFlag);
22627
+ }
22628
+ } else {
22629
+ args.push("install", ...globalFlag);
22630
+ if (hasPkgs) {
22631
+ if (input.save === "dev") args.push("--save-dev");
22632
+ else if (input.save === "optional") args.push("--save-optional");
22633
+ }
22634
+ }
22635
+ if (hasPkgs) args.push(...pkgList);
22278
22636
  yield {
22279
22637
  type: "log",
22280
22638
  text: `Fetching ${pkgList.length || "all"} packages\u2026`,
@@ -22355,9 +22713,9 @@ var JsonFileTooLargeError = class extends Error {
22355
22713
  };
22356
22714
  async function readJsonFileBounded(filePath, ctx) {
22357
22715
  const resolved = await safeResolveReal(filePath, ctx);
22358
- const stat19 = await fs27.stat(resolved);
22359
- if (stat19.size > MAX_JSON_FILE_BYTES) {
22360
- throw new JsonFileTooLargeError(filePath, stat19.size);
22716
+ const stat20 = await fs27.stat(resolved);
22717
+ if (stat20.size > MAX_JSON_FILE_BYTES) {
22718
+ throw new JsonFileTooLargeError(filePath, stat20.size);
22361
22719
  }
22362
22720
  return fs27.readFile(resolved, "utf8");
22363
22721
  }
@@ -22763,60 +23121,60 @@ function jmespathSearch(data, query) {
22763
23121
  }
22764
23122
  function validateJsonSchema(data, schema) {
22765
23123
  const errors = [];
22766
- function check(value, s, path40) {
23124
+ function check(value, s, path41) {
22767
23125
  if (s["type"]) {
22768
23126
  const expectedType = s["type"];
22769
23127
  const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
22770
23128
  if (expectedType === "integer") {
22771
- if (!Number.isInteger(value)) errors.push(`${path40}: expected integer, got ${actualType}`);
23129
+ if (!Number.isInteger(value)) errors.push(`${path41}: expected integer, got ${actualType}`);
22772
23130
  } else if (expectedType !== actualType) {
22773
- errors.push(`${path40}: expected ${expectedType}, got ${actualType}`);
23131
+ errors.push(`${path41}: expected ${expectedType}, got ${actualType}`);
22774
23132
  }
22775
23133
  }
22776
23134
  if (typeof value === "string" && s["format"] === "uri" && value) {
22777
23135
  try {
22778
23136
  new URL(value);
22779
23137
  } catch {
22780
- errors.push(`${path40}: not a valid URI`);
23138
+ errors.push(`${path41}: not a valid URI`);
22781
23139
  }
22782
23140
  }
22783
23141
  if (typeof value === "string" && s["pattern"]) {
22784
23142
  const compiled = compileUserRegex(s["pattern"], "");
22785
23143
  if (!compiled.ok) {
22786
- errors.push(`${path40}: invalid schema pattern \u2014 ${compiled.reason}`);
23144
+ errors.push(`${path41}: invalid schema pattern \u2014 ${compiled.reason}`);
22787
23145
  } else if (!compiled.regex.test(capSubject(value))) {
22788
- errors.push(`${path40}: does not match pattern ${s["pattern"]}`);
23146
+ errors.push(`${path41}: does not match pattern ${s["pattern"]}`);
22789
23147
  }
22790
23148
  }
22791
23149
  if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
22792
- errors.push(`${path40}: string too short (min ${s["minLength"]})`);
23150
+ errors.push(`${path41}: string too short (min ${s["minLength"]})`);
22793
23151
  }
22794
23152
  if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
22795
- errors.push(`${path40}: string too long (max ${s["maxLength"]})`);
23153
+ errors.push(`${path41}: string too long (max ${s["maxLength"]})`);
22796
23154
  }
22797
23155
  if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
22798
- errors.push(`${path40}: below minimum ${s["minimum"]}`);
23156
+ errors.push(`${path41}: below minimum ${s["minimum"]}`);
22799
23157
  }
22800
23158
  if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
22801
- errors.push(`${path40}: above maximum ${s["maximum"]}`);
23159
+ errors.push(`${path41}: above maximum ${s["maximum"]}`);
22802
23160
  }
22803
23161
  if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
22804
23162
  for (let i = 0; i < value.length; i++) {
22805
- check(value[i], s["items"], `${path40}[${i}]`);
23163
+ check(value[i], s["items"], `${path41}[${i}]`);
22806
23164
  }
22807
23165
  }
22808
23166
  if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
22809
23167
  const props = s["properties"];
22810
23168
  for (const [k, propSchema] of Object.entries(props)) {
22811
- check(value[k], propSchema, `${path40}.${k}`);
23169
+ check(value[k], propSchema, `${path41}.${k}`);
22812
23170
  }
22813
23171
  }
22814
23172
  }
22815
23173
  check(data, schema, "$");
22816
23174
  return { valid: errors.length === 0, errors };
22817
23175
  }
22818
- function simpleQuery(data, path40) {
22819
- const parts = path40.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
23176
+ function simpleQuery(data, path41) {
23177
+ const parts = path41.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
22820
23178
  let current = data;
22821
23179
  for (const part of parts) {
22822
23180
  if (current === null || current === void 0) return void 0;
@@ -23727,6 +24085,18 @@ var KANBAN_INPUT_SCHEMA = {
23727
24085
  },
23728
24086
  transitionAction: { type: "string" },
23729
24087
  transitionComment: { type: "string" },
24088
+ tickChecks: {
24089
+ type: "array",
24090
+ items: {
24091
+ type: "object",
24092
+ properties: {
24093
+ checkId: { type: "string" },
24094
+ checkStatus: { type: "string", enum: ["passed", "failed", "skipped"] }
24095
+ },
24096
+ required: ["checkId", "checkStatus"]
24097
+ },
24098
+ description: "`transition_task` (to=done only): flip one or more manual criteria to `passed` before the gate fires. Read ids from kanban get_task. Non-manual criteria are refused."
24099
+ },
23730
24100
  attachmentUrl: { type: "string" },
23731
24101
  attachmentTitle: { type: "string" },
23732
24102
  attachmentType: {
@@ -23909,7 +24279,7 @@ var KANBAN_INPUT_SCHEMA = {
23909
24279
 
23910
24280
  // src/session-kanban.ts
23911
24281
  import { watch } from "node:fs";
23912
- import { basename as basename13, dirname as dirname15 } from "node:path";
24282
+ import { basename as basename13, dirname as dirname14 } from "node:path";
23913
24283
  import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
23914
24284
  import {
23915
24285
  loadPlan,
@@ -24527,7 +24897,7 @@ function attachSessionKanbanMirror(context) {
24527
24897
  const configureWatcher = () => {
24528
24898
  const planPath = context.meta["plan.path"];
24529
24899
  const taskPath = context.meta["task.path"];
24530
- const candidate = typeof planPath === "string" && planPath ? dirname15(planPath) : typeof taskPath === "string" && taskPath ? dirname15(taskPath) : "";
24900
+ const candidate = typeof planPath === "string" && planPath ? dirname14(planPath) : typeof taskPath === "string" && taskPath ? dirname14(taskPath) : "";
24531
24901
  if (!candidate || candidate === watchedDir) return;
24532
24902
  watcher?.close();
24533
24903
  watcher = null;
@@ -24618,9 +24988,21 @@ async function rebindSessionKanbanTask(context) {
24618
24988
  context.setCurrentKanbanTask(best.taskId, best.boardId);
24619
24989
  return { boardId: best.boardId, taskId: best.taskId };
24620
24990
  }
24991
+ var degradationReason;
24621
24992
  async function hydrateSessionKanban(context) {
24622
24993
  const id = context.session?.id ?? "";
24623
24994
  if (!id) return null;
24995
+ try {
24996
+ const board = await hydrateSessionKanbanBoard(context, id);
24997
+ degradationReason = void 0;
24998
+ return board;
24999
+ } catch (error) {
25000
+ degradationReason = error instanceof Error ? error.message : String(error);
25001
+ fireAndForget("hydrate", Promise.reject(error));
25002
+ return null;
25003
+ }
25004
+ }
25005
+ async function hydrateSessionKanbanBoard(context, id) {
24624
25006
  await rebindSessionKanbanTask(context);
24625
25007
  await cleanupEmptySessionKanbanBoards(context.projectRoot, id);
24626
25008
  if (context.projectRoot) {
@@ -24843,6 +25225,9 @@ var kanbanTool = {
24843
25225
  description: KANBAN_TOOL_DESCRIPTION,
24844
25226
  usageHint: KANBAN_TOOL_USAGE_HINT,
24845
25227
  permission: "confirm",
25228
+ // WS-046: gives permission decisions something to key on.
25229
+ // The action performed; kanban has no single file or path subject.
25230
+ subjectKey: "action",
24846
25231
  mutating: true,
24847
25232
  capabilities: ["fs.write"],
24848
25233
  icon: "task",
@@ -25284,6 +25669,7 @@ var kanbanTool = {
25284
25669
  actor: input.author,
25285
25670
  comment: input.transitionComment,
25286
25671
  ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
25672
+ ...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
25287
25673
  ...input.attachmentUrl !== void 0 ? {
25288
25674
  attachment: {
25289
25675
  url: input.attachmentUrl,
@@ -25676,8 +26062,52 @@ var kanbanTool = {
25676
26062
  } catch (err) {
25677
26063
  return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
25678
26064
  }
26065
+ },
26066
+ serialize(output, input) {
26067
+ return serializeKanbanOutput(output, input);
25679
26068
  }
25680
26069
  };
26070
+ var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
26071
+ var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
26072
+ "get_board",
26073
+ "export_markdown",
26074
+ "export_task_graph"
26075
+ ]);
26076
+ function serializeKanbanOutput(output, input) {
26077
+ const action = input && typeof input === "object" ? input.action : void 0;
26078
+ const board = output.board;
26079
+ if (board) {
26080
+ const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
26081
+ let boardBytes = 0;
26082
+ if (!keepFull) {
26083
+ try {
26084
+ boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
26085
+ } catch {
26086
+ boardBytes = 0;
26087
+ }
26088
+ }
26089
+ if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
26090
+ const columns = {};
26091
+ for (const column of board.columns) {
26092
+ columns[column.title || column.id] = board.tasks.filter(
26093
+ (task) => task.columnId === column.id
26094
+ ).length;
26095
+ }
26096
+ const compact = {
26097
+ ...output,
26098
+ board: {
26099
+ id: board.id,
26100
+ title: board.title,
26101
+ columns,
26102
+ totalTasks: board.tasks.length,
26103
+ note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
26104
+ }
26105
+ };
26106
+ return JSON.stringify(compact, null, 2);
26107
+ }
26108
+ }
26109
+ return JSON.stringify(output, null, 2);
26110
+ }
25681
26111
 
25682
26112
  // src/builtin.ts
25683
26113
  init_execute_tool();
@@ -25792,11 +26222,11 @@ var lintTool = {
25792
26222
  }
25793
26223
  };
25794
26224
  async function detectLinter(cwd) {
25795
- const { stat: stat19 } = await import("node:fs/promises");
26225
+ const { stat: stat20 } = await import("node:fs/promises");
25796
26226
  const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
25797
26227
  for (const f of checks) {
25798
26228
  try {
25799
- await stat19(`${cwd}/${f}`);
26229
+ await stat20(`${cwd}/${f}`);
25800
26230
  if (f.includes("biome")) return "biome";
25801
26231
  if (f.includes("eslint")) return "eslint";
25802
26232
  if (f.includes("tslint")) return "tslint";
@@ -25813,11 +26243,12 @@ init_util();
25813
26243
  var logsTool = {
25814
26244
  name: "logs",
25815
26245
  category: "Logs",
25816
- description: "Read or stream logs from files, Docker containers, or systemd services. Useful for debugging running applications.",
25817
- usageHint: "DEBUGGING TOOL \u2014 USE CAREFULLY IN AUTONOMOUS MODE:\n\n- Prefer `path` for local files or `service` for containers/systemd.\n- `stream: true` = live tail (can be expensive).\n- Always use `filter` (regex) when possible to reduce noise and token usage.\n- Long-running streams should be avoided unless the user explicitly wants live logs.",
26246
+ description: "Read logs from files or Docker containers. Useful for debugging running applications.",
26247
+ usageHint: "DEBUGGING TOOL \u2014 USE CAREFULLY IN AUTONOMOUS MODE:\n\n- Prefer `path` for local files or `service` for Docker containers.\n- Always use `filter` (regex) when possible to reduce noise and token usage.\n- `since` narrows Docker logs to a recent window.",
25818
26248
  permission: "confirm",
25819
26249
  mutating: false,
25820
26250
  timeoutMs: 3e4,
26251
+ maxOutputBytes: 262144,
25821
26252
  capabilities: ["shell.restricted"],
25822
26253
  icon: "logs",
25823
26254
  inputSchema: {
@@ -25825,7 +26256,7 @@ var logsTool = {
25825
26256
  properties: {
25826
26257
  service: {
25827
26258
  type: "string",
25828
- description: "Service name for Docker or systemd journal"
26259
+ description: "Docker container name (passed to `docker logs`)"
25829
26260
  },
25830
26261
  path: {
25831
26262
  type: "string",
@@ -25837,10 +26268,6 @@ var logsTool = {
25837
26268
  minimum: 0,
25838
26269
  maximum: 1e4
25839
26270
  },
25840
- stream: {
25841
- type: "boolean",
25842
- description: "Stream logs continuously (like tail -f) (default: false)"
25843
- },
25844
26271
  filter: {
25845
26272
  type: "string",
25846
26273
  description: "Regex pattern to filter log lines"
@@ -25848,7 +26275,7 @@ var logsTool = {
25848
26275
  since: {
25849
26276
  type: "string",
25850
26277
  enum: ["1h", "6h", "24h", "all"],
25851
- description: "Only show logs since duration"
26278
+ description: 'Only show Docker logs since duration (ignored for files; "all" = no limit)'
25852
26279
  },
25853
26280
  cwd: { type: "string", description: "Working directory (default: cwd)" }
25854
26281
  }
@@ -25865,10 +26292,10 @@ var logsTool = {
25865
26292
  filterRe = compiled.regex;
25866
26293
  }
25867
26294
  if (input.service) {
25868
- return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal);
26295
+ return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal, input.since);
25869
26296
  }
25870
26297
  if (input.path) {
25871
- return await fileLogs(safeResolve(input.path, ctx), lines, filterRe, input.stream ?? false);
26298
+ return await fileLogs(await safeResolveReal(input.path, ctx), lines, filterRe);
25872
26299
  }
25873
26300
  return {
25874
26301
  source: "none",
@@ -25882,7 +26309,7 @@ var logsTool = {
25882
26309
  async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
25883
26310
  const args = ["logs"];
25884
26311
  if (lines > 0) args.push("--tail", String(lines));
25885
- if (since) {
26312
+ if (since && since !== "all") {
25886
26313
  const sinceMap = { "1h": "1h", "6h": "6h", "24h": "24h" };
25887
26314
  args.push("--since", sinceMap[since] ?? "1h");
25888
26315
  }
@@ -25896,7 +26323,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
25896
26323
  };
25897
26324
  }
25898
26325
  args.push("--timestamps", service);
25899
- return new Promise((resolve17) => {
26326
+ return new Promise((resolve18) => {
25900
26327
  let stdout = "";
25901
26328
  let stderr = "";
25902
26329
  const MAX = 2e5;
@@ -25912,7 +26339,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
25912
26339
  if (settled) return;
25913
26340
  settled = true;
25914
26341
  clearTimeout(timer);
25915
- resolve17(result);
26342
+ resolve18(result);
25916
26343
  };
25917
26344
  const child = spawn11("docker", args, { cwd, signal, env: buildChildEnv6(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
25918
26345
  const timer = setTimeout(() => {
@@ -25947,7 +26374,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
25947
26374
  }
25948
26375
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
25949
26376
  var MAX_TAIL_LINES = 1e5;
25950
- async function fileLogs(path40, lines, filterRe, stream) {
26377
+ async function fileLogs(path41, lines, filterRe) {
25951
26378
  const { createInterface } = await import("node:readline");
25952
26379
  const { createReadStream: createReadStream2 } = await import("node:fs");
25953
26380
  const entries = [];
@@ -25956,7 +26383,7 @@ async function fileLogs(path40, lines, filterRe, stream) {
25956
26383
  let writeIdx = 0;
25957
26384
  let totalLines = 0;
25958
26385
  const rl = createInterface({
25959
- input: createReadStream2(path40),
26386
+ input: createReadStream2(path41),
25960
26387
  crlfDelay: Number.POSITIVE_INFINITY
25961
26388
  });
25962
26389
  for await (const line of rl) {
@@ -25977,11 +26404,11 @@ async function fileLogs(path40, lines, filterRe, stream) {
25977
26404
  if (parsed) entries.push(parsed);
25978
26405
  }
25979
26406
  return {
25980
- source: path40,
26407
+ source: path41,
25981
26408
  entries,
25982
26409
  total: entries.length,
25983
26410
  truncated: totalLines > effLines,
25984
- stream_mode: stream
26411
+ stream_mode: false
25985
26412
  };
25986
26413
  }
25987
26414
  function parseLogLines(output, filterRe) {
@@ -26057,25 +26484,12 @@ var outdatedTool = {
26057
26484
  inputSchema: {
26058
26485
  type: "object",
26059
26486
  properties: {
26060
- cwd: { type: "string", description: "Working directory (default: cwd)" },
26061
- format: {
26062
- type: "string",
26063
- enum: ["list", "table"],
26064
- description: "Output format (default: list)"
26065
- },
26066
- include_deprecated: {
26067
- type: "boolean",
26068
- description: "Include deprecated packages (default: false)"
26069
- },
26070
- check: {
26071
- type: "string",
26072
- description: "Specific package(s) to check (comma-separated)"
26073
- }
26487
+ cwd: { type: "string", description: "Working directory (default: cwd)" }
26074
26488
  }
26075
26489
  },
26076
26490
  async execute(input, ctx, opts) {
26077
26491
  const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
26078
- const manager = await detectPackageManager(cwd);
26492
+ const manager = await detectPackageManager(cwd, ctx.projectRoot);
26079
26493
  if (manager === "npm") {
26080
26494
  try {
26081
26495
  const { detectNonJsEcosystem: detectNonJsEcosystem2 } = await Promise.resolve().then(() => (init_legacy_bridge(), legacy_bridge_exports));
@@ -26128,13 +26542,11 @@ var outdatedTool = {
26128
26542
  }
26129
26543
  }
26130
26544
  const args = ["outdated", "--json"];
26131
- if (input.format === "table") args.push("--table");
26132
- if (input.include_deprecated) args.push("--include", "deprecated");
26133
26545
  return runOutdated(manager, args, cwd, opts.signal);
26134
26546
  }
26135
26547
  };
26136
26548
  function runOutdated(manager, args, cwd, signal) {
26137
- return new Promise((resolve17) => {
26549
+ return new Promise((resolve18) => {
26138
26550
  let stdout = "";
26139
26551
  let stderr = "";
26140
26552
  const MAX = 1e5;
@@ -26159,10 +26571,10 @@ function runOutdated(manager, args, cwd, signal) {
26159
26571
  });
26160
26572
  child.on("close", (code) => {
26161
26573
  const result = parseOutdatedOutput(stdout, code ?? 0);
26162
- resolve17(result);
26574
+ resolve18(result);
26163
26575
  });
26164
26576
  child.on("error", (e) => {
26165
- resolve17({
26577
+ resolve18({
26166
26578
  exit_code: 1,
26167
26579
  packages: [],
26168
26580
  total: 0,
@@ -26183,27 +26595,39 @@ function parseOutdatedOutput(json2, exitCode) {
26183
26595
  truncated: false
26184
26596
  };
26185
26597
  }
26598
+ const truncated = json2.length >= 1e5 || Buffer.byteLength(json2, "utf8") > COMMAND_OUTPUT_MAX_BYTES;
26599
+ let parsedOk = false;
26186
26600
  try {
26187
26601
  const data = JSON.parse(json2);
26602
+ parsedOk = true;
26188
26603
  for (const name of Object.keys(data)) {
26189
- const info = data[name];
26604
+ const info = data[name] ?? {};
26605
+ const str = (v) => typeof v === "string" ? v : void 0;
26190
26606
  packages.push({
26191
26607
  name,
26192
- current: info.current ?? "unknown",
26193
- latest: info.latest ?? "unknown",
26194
- wanted: info.wanted ?? "unknown",
26195
- type: info.type ?? "unknown",
26196
- location: info.location ?? name
26608
+ current: str(info["current"]) ?? "unknown",
26609
+ latest: str(info["latest"]) ?? "unknown",
26610
+ wanted: str(info["wanted"]) ?? "unknown",
26611
+ // npm calls it `type`; pnpm calls it `dependencyType`.
26612
+ type: str(info["type"]) ?? str(info["dependencyType"]) ?? "unknown",
26613
+ location: str(info["location"]) ?? name
26197
26614
  });
26198
26615
  }
26199
26616
  } catch {
26617
+ }
26618
+ const outdatedFound = parsedOk && exitCode === 1;
26619
+ let output = normalizeCommandOutput(json2);
26620
+ if (outdatedFound) {
26621
+ output = `${output}
26622
+
26623
+ Note: exit code 1 from \`outdated\` means outdated packages were found (expected); treated as success.`;
26200
26624
  }
26201
26625
  return {
26202
- exit_code: exitCode,
26626
+ exit_code: outdatedFound ? 0 : exitCode,
26203
26627
  packages,
26204
26628
  total: packages.length,
26205
- output: json2,
26206
- truncated: json2.length >= 1e5
26629
+ output,
26630
+ truncated
26207
26631
  };
26208
26632
  }
26209
26633
 
@@ -26212,8 +26636,8 @@ init_util();
26212
26636
  import { spawn as spawn13 } from "node:child_process";
26213
26637
  import * as fs28 from "node:fs/promises";
26214
26638
  import * as os9 from "node:os";
26215
- import * as path33 from "node:path";
26216
- import { buildChildEnv as buildChildEnv8, toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
26639
+ import * as path34 from "node:path";
26640
+ import { buildChildEnv as buildChildEnv8, toErrorMessage as toErrorMessage7 } from "@wrongstack/core/utils";
26217
26641
  var patchTool = {
26218
26642
  name: "patch",
26219
26643
  category: "Filesystem",
@@ -26257,26 +26681,26 @@ var patchTool = {
26257
26681
  try {
26258
26682
  dir = input.directory ? await safeResolveReal(input.directory, ctx) : ctx.cwd;
26259
26683
  } catch (err) {
26260
- return refuse(`patch refused: ${toErrorMessage4(err)}`);
26684
+ return refuse(`patch refused: ${toErrorMessage7(err)}`);
26261
26685
  }
26262
- const realRoot = await fs28.realpath(ctx.projectRoot).catch(() => path33.resolve(ctx.projectRoot));
26686
+ const realRoot = await fs28.realpath(ctx.projectRoot).catch(() => path34.resolve(ctx.projectRoot));
26263
26687
  const targets = extractDiffTargets(input.patch);
26264
26688
  const resolvedTargets = [];
26265
26689
  for (const t of targets) {
26266
26690
  const stripped = stripPathComponents(t.raw, strip);
26267
26691
  if (!stripped) continue;
26268
- if (path33.isAbsolute(stripped)) {
26692
+ if (path34.isAbsolute(stripped)) {
26269
26693
  return refuse(`patch refused: target "${t.raw}" strips to absolute path`);
26270
26694
  }
26271
- const candidate = path33.resolve(dir, stripped);
26695
+ const candidate = path34.resolve(dir, stripped);
26272
26696
  let real;
26273
26697
  try {
26274
26698
  real = await resolveRealInsideRoot(candidate, ctx);
26275
26699
  } catch (err) {
26276
- return refuse(`patch refused: target "${t.raw}" ${toErrorMessage4(err)}`);
26700
+ return refuse(`patch refused: target "${t.raw}" ${toErrorMessage7(err)}`);
26277
26701
  }
26278
- const rel = path33.relative(realRoot, real);
26279
- if (rel.startsWith("..") || path33.isAbsolute(rel)) {
26702
+ const rel = path34.relative(realRoot, real);
26703
+ if (rel.startsWith("..") || path34.isAbsolute(rel)) {
26280
26704
  return refuse(`patch refused: target "${t.raw}" resolves outside project root`);
26281
26705
  }
26282
26706
  resolvedTargets.push({ raw: t.raw, deleted: t.deleted, abs: real });
@@ -26290,11 +26714,11 @@ var patchTool = {
26290
26714
  beforeContents.set(target.abs, await readTextForTracking(target.abs));
26291
26715
  }
26292
26716
  }
26293
- const tmpDir = await fs28.mkdtemp(path33.join(os9.tmpdir(), ".wstack_patch_"));
26717
+ const tmpDir = await fs28.mkdtemp(path34.join(os9.tmpdir(), ".wstack_patch_"));
26294
26718
  try {
26295
26719
  await fs28.chmod(tmpDir, 448).catch(() => {
26296
26720
  });
26297
- const patchFile = path33.join(tmpDir, "in.diff");
26721
+ const patchFile = path34.join(tmpDir, "in.diff");
26298
26722
  await fs28.writeFile(patchFile, input.patch, { mode: 384 });
26299
26723
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
26300
26724
  const result = await runPatch(args, dir, opts.signal, {
@@ -26307,8 +26731,8 @@ var patchTool = {
26307
26731
  for (const target of resolvedTargets) {
26308
26732
  const abs = target.abs;
26309
26733
  const before = beforeContents.get(abs) ?? null;
26310
- const stat19 = await fs28.stat(abs).catch(() => null);
26311
- if (!stat19?.isFile()) {
26734
+ const stat20 = await fs28.stat(abs).catch(() => null);
26735
+ if (!stat20?.isFile()) {
26312
26736
  if (beforeExisted.has(abs)) {
26313
26737
  touched.push(abs);
26314
26738
  ctx.session?.recordFileChange?.({
@@ -26323,7 +26747,7 @@ var patchTool = {
26323
26747
  const after = await readTextForTracking(abs);
26324
26748
  if (after === null || after === before) continue;
26325
26749
  touched.push(abs);
26326
- ctx.recordRead?.(abs, stat19.mtimeMs, "write", sha256hex(after));
26750
+ ctx.recordRead?.(abs, stat20.mtimeMs, "write", sha256hex(after));
26327
26751
  ctx.session?.recordFileChange?.({
26328
26752
  path: abs,
26329
26753
  action: before === null ? "created" : "modified",
@@ -26334,7 +26758,7 @@ var patchTool = {
26334
26758
  }
26335
26759
  if (result.exitCode !== 0) {
26336
26760
  if (!dryRun) {
26337
- const partial = touched.length > 0 ? ` ${touched.length} file(s) were still modified on disk and have been recorded for rewind: ${touched.map((p) => path33.relative(realRoot, p) || p).join(", ")}.` : "";
26761
+ const partial = touched.length > 0 ? ` ${touched.length} file(s) were still modified on disk and have been recorded for rewind: ${touched.map((p) => path34.relative(realRoot, p) || p).join(", ")}.` : "";
26338
26762
  return {
26339
26763
  applied: touched.length,
26340
26764
  rejected: 1,
@@ -26342,7 +26766,7 @@ var patchTool = {
26342
26766
  // success path (which returns GNU patch's dir-relative names).
26343
26767
  // `touched` entries are realpaths from resolveRealInsideRoot, and
26344
26768
  // realRoot is also a realpath, so path.relative is like-for-like.
26345
- files: touched.map((p) => path33.relative(realRoot, p) || p),
26769
+ files: touched.map((p) => path34.relative(realRoot, p) || p),
26346
26770
  dry_run: dryRun,
26347
26771
  message: `patch failed: ${result.stderr || result.stdout}${partial}`
26348
26772
  };
@@ -26358,7 +26782,7 @@ var patchTool = {
26358
26782
  }
26359
26783
  const patched = result.engine === "git" ? [
26360
26784
  ...new Set(
26361
- resolvedTargets.map((target) => path33.relative(dir, target.abs) || target.abs)
26785
+ resolvedTargets.map((target) => path34.relative(dir, target.abs) || target.abs)
26362
26786
  )
26363
26787
  ] : extractPatchedFiles(result.stdout);
26364
26788
  return {
@@ -26377,8 +26801,8 @@ var patchTool = {
26377
26801
  var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
26378
26802
  async function readTextForTracking(absPath) {
26379
26803
  try {
26380
- const stat19 = await fs28.stat(absPath);
26381
- if (!stat19.isFile() || stat19.size > MAX_TRACKING_BYTES) return null;
26804
+ const stat20 = await fs28.stat(absPath);
26805
+ if (!stat20.isFile() || stat20.size > MAX_TRACKING_BYTES) return null;
26382
26806
  const buf = await fs28.readFile(absPath);
26383
26807
  if (buf.includes(0)) return null;
26384
26808
  return buf.toString("utf8");
@@ -26463,7 +26887,7 @@ function runPatch(args, cwd, signal, fallback) {
26463
26887
  });
26464
26888
  }
26465
26889
  function runPatchProcess(command, args, cwd, signal) {
26466
- return new Promise((resolve17) => {
26890
+ return new Promise((resolve18) => {
26467
26891
  let stdout = "";
26468
26892
  let stderr = "";
26469
26893
  const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
@@ -26482,11 +26906,11 @@ function runPatchProcess(command, args, cwd, signal) {
26482
26906
  });
26483
26907
  child.on(
26484
26908
  "close",
26485
- (code) => resolve17({ exitCode: code ?? 1, stdout, stderr, unavailable: false })
26909
+ (code) => resolve18({ exitCode: code ?? 1, stdout, stderr, unavailable: false })
26486
26910
  );
26487
26911
  child.on(
26488
26912
  "error",
26489
- (e) => resolve17({
26913
+ (e) => resolve18({
26490
26914
  exitCode: 1,
26491
26915
  stdout: "",
26492
26916
  stderr: e.message,
@@ -26864,7 +27288,8 @@ var todoTool = {
26864
27288
  }
26865
27289
  for (const planId of completedPlanIds) {
26866
27290
  if (pendingPlanIds.has(planId)) continue;
26867
- const planPath = ctx.meta["plan.path"];
27291
+ const meta = ctx.meta;
27292
+ const planPath = meta["plan.path.resolved"] ?? meta["plan.path"];
26868
27293
  if (typeof planPath !== "string" || !planPath) continue;
26869
27294
  try {
26870
27295
  const plan = await loadPlan2(planPath);
@@ -26877,7 +27302,8 @@ var todoTool = {
26877
27302
  }
26878
27303
  for (const taskId of completedTaskIds) {
26879
27304
  if (pendingTaskIds.has(taskId)) continue;
26880
- const taskPath = ctx.meta["task.path"];
27305
+ const meta = ctx.meta;
27306
+ const taskPath = meta["task.path.resolved"] ?? meta["task.path"];
26881
27307
  if (typeof taskPath !== "string" || !taskPath) continue;
26882
27308
  try {
26883
27309
  const file = await loadTasks3(taskPath);
@@ -26993,7 +27419,16 @@ var planTool = {
26993
27419
  sessionPlanPath.lastIndexOf("/"),
26994
27420
  sessionPlanPath.lastIndexOf("\\")
26995
27421
  );
26996
- planPath = lastSep >= 0 ? sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
27422
+ if (lastSep < 0) {
27423
+ return {
27424
+ ok: false,
27425
+ message: `Cannot derive the project-scoped plan path: session plan path "${sessionPlanPath}" has no directory component.`,
27426
+ plan: "",
27427
+ count: 0,
27428
+ open: 0
27429
+ };
27430
+ }
27431
+ planPath = sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json";
26997
27432
  }
26998
27433
  } else {
26999
27434
  planPath = sessionPlanPath;
@@ -27186,6 +27621,7 @@ var planTool = {
27186
27621
  open: 0
27187
27622
  };
27188
27623
  }
27624
+ ctx.meta["plan.path.resolved"] = planPath;
27189
27625
  await projectSessionPlanToKanban(ctx.projectRoot, plan.items, sessionId);
27190
27626
  if (todosToReplace) {
27191
27627
  await todoTool.execute({ todos: todosToReplace }, ctx, {
@@ -27218,6 +27654,7 @@ var planTool = {
27218
27654
  });
27219
27655
  return f;
27220
27656
  });
27657
+ ctx.meta["task.path.resolved"] = taskPath;
27221
27658
  return mkResult(
27222
27659
  plan,
27223
27660
  true,
@@ -27251,14 +27688,14 @@ function mkResult(plan, ok, message, todos) {
27251
27688
  // src/read.ts
27252
27689
  init_util();
27253
27690
  import * as fs29 from "node:fs/promises";
27254
- import { FsError, ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
27255
- import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
27691
+ import { FsError, ToolValidationError as ToolValidationError8 } from "@wrongstack/core/types";
27692
+ import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
27256
27693
  var ADVANCED_MODE_META_KEY = "tools.read.advancedMode";
27257
27694
  var MAX_BYTES2 = 5 * 1024 * 1024;
27258
27695
  var readTool = {
27259
27696
  name: "read",
27260
27697
  category: "Filesystem",
27261
- description: "Read the contents of a file with line numbers. This is the primary way to inspect source code, configuration, or any text file before making changes. Lines are returned 1-indexed with a ` N| ` prefix for easy reference in edits. When advanced mode is on or `includeSymbols` is set, the result also includes a `symbols` field listing codebase-index symbol names, kinds, and line numbers for the file (not file content).",
27698
+ description: "Read the contents of a file with line numbers. This is the primary way to inspect source code, configuration, or any text file before making changes. Lines are returned 1-indexed in the form `N\u2192content` (line number, then a `\u2192` separator, then the raw line). The `N\u2192` prefix is display-only \u2014 always strip it before reusing the text, e.g. never include it in `edit.old_string`. When advanced mode is on or `includeSymbols` is set, the result also includes a `symbols` field listing codebase-index symbol names, kinds, and line numbers for the file (not file content).",
27262
27699
  usageHint: "FOUNDATIONAL TOOL \u2014 call this before almost any edit operation.\n\nBest practices:\n- Always read a file before using `edit`, `replace`, or `write` on it (the system often requires it for safety).\n- Use `offset` + `limit` for very large files instead of reading everything at once.\n- Default limit is generous (2000 lines) but can be increased.\n- The output format is designed to be directly usable as context for `edit` operations.\n- Set `includeSymbols: true` to also receive the codebase-index symbol listing for the file.\n- Enable advanced mode (`ctx.meta['tools.read.advancedMode'] = true`) to auto-inject symbols on every read.",
27263
27700
  selection: {
27264
27701
  doNotUseWhen: "you need to search many files for matching content.",
@@ -27279,11 +27716,13 @@ var readTool = {
27279
27716
  },
27280
27717
  offset: {
27281
27718
  type: "integer",
27719
+ minimum: 1,
27282
27720
  description: "1-based starting line number. Use together with `limit` for large files."
27283
27721
  },
27284
27722
  limit: {
27285
27723
  type: "integer",
27286
- description: "Maximum number of lines to return (default is 2000)."
27724
+ minimum: 0,
27725
+ description: "Maximum number of lines to return (default 2000). Values above 5000 are clamped to 5000 \u2014 page with `offset` for more."
27287
27726
  },
27288
27727
  mode: {
27289
27728
  type: "string",
@@ -27299,16 +27738,16 @@ var readTool = {
27299
27738
  },
27300
27739
  async execute(input, ctx, execOpts) {
27301
27740
  if (!input?.path) {
27302
- throw new ToolValidationError5({
27741
+ throw new ToolValidationError8({
27303
27742
  message: "read: path is required",
27304
27743
  field: "path"
27305
27744
  });
27306
27745
  }
27307
27746
  const absPath = await safeResolveReal(input.path, ctx);
27308
27747
  const shouldIncludeSymbols = input.includeSymbols === true || input.includeSymbols !== false && ctx.meta[ADVANCED_MODE_META_KEY] === true;
27309
- let stat19;
27748
+ let stat20;
27310
27749
  try {
27311
- stat19 = await fs29.stat(absPath);
27750
+ stat20 = await fs29.stat(absPath);
27312
27751
  } catch (err) {
27313
27752
  const code = err.code;
27314
27753
  if (code === "ENOENT") {
@@ -27320,14 +27759,14 @@ var readTool = {
27320
27759
  });
27321
27760
  }
27322
27761
  throw new FsError({
27323
- message: `read: failed to stat "${input.path}": ${toErrorMessage5(err)}`,
27762
+ message: `read: failed to stat "${input.path}": ${toErrorMessage8(err)}`,
27324
27763
  code: "FS_READ_FAILED",
27325
27764
  path: absPath,
27326
27765
  context: { errno: code },
27327
27766
  cause: err
27328
27767
  });
27329
27768
  }
27330
- if (!stat19.isFile()) {
27769
+ if (!stat20.isFile()) {
27331
27770
  throw new FsError({
27332
27771
  message: `read: "${input.path}" is not a regular file`,
27333
27772
  code: "FS_READ_FAILED",
@@ -27335,23 +27774,23 @@ var readTool = {
27335
27774
  context: { reason: "not-a-regular-file" }
27336
27775
  });
27337
27776
  }
27338
- if (stat19.size > MAX_BYTES2) {
27777
+ if (stat20.size > MAX_BYTES2) {
27339
27778
  throw new FsError({
27340
- message: `read: file too large (${stat19.size} bytes, limit ${MAX_BYTES2})`,
27779
+ message: `read: file too large (${stat20.size} bytes, limit ${MAX_BYTES2})`,
27341
27780
  code: "FS_READ_FAILED",
27342
27781
  path: absPath,
27343
- context: { size: stat19.size, limit: MAX_BYTES2, reason: "too-large" }
27782
+ context: { size: stat20.size, limit: MAX_BYTES2, reason: "too-large" }
27344
27783
  });
27345
27784
  }
27346
27785
  const offset = Math.max(1, input.offset ?? 1);
27347
27786
  const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
27348
27787
  const prior = getReadRangeRecord(ctx, absPath);
27349
27788
  const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
27350
- if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat19.mtimeMs, offset, requestedEnd)) {
27351
- ctx.recordRead(absPath, stat19.mtimeMs);
27789
+ if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat20.mtimeMs, offset, requestedEnd)) {
27790
+ ctx.recordRead(absPath, stat20.mtimeMs);
27352
27791
  const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
27353
27792
  return {
27354
- text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat19.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
27793
+ text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat20.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
27355
27794
  total_lines: prior.totalLines,
27356
27795
  encoding: "utf8",
27357
27796
  truncated: requestedEnd < prior.totalLines,
@@ -27362,18 +27801,23 @@ var readTool = {
27362
27801
  }
27363
27802
  const buf = await fs29.readFile(absPath);
27364
27803
  if (isBinaryBuffer(buf)) {
27365
- throw new Error(`read: "${input.path}" appears to be binary`);
27804
+ throw new FsError({
27805
+ message: `read: "${input.path}" appears to be binary`,
27806
+ code: "FS_READ_FAILED",
27807
+ path: absPath,
27808
+ context: { reason: "binary" }
27809
+ });
27366
27810
  }
27367
27811
  const text = buf.toString("utf8");
27368
27812
  const contentHash = sha256hex(text);
27369
27813
  const allLines = text.split(/\r\n|\r|\n/);
27370
27814
  const total = allLines.length;
27371
27815
  if (input.mode === "summary") {
27372
- ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
27373
- rememberReadRange(ctx, absPath, stat19.mtimeMs, total, 1, Math.min(total, 200));
27816
+ ctx.recordRead(absPath, stat20.mtimeMs, "user", contentHash);
27817
+ rememberReadRange(ctx, absPath, stat20.mtimeMs, total, 1, Math.min(total, 200));
27374
27818
  const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
27375
27819
  return {
27376
- text: summarizeFile(input.path, stat19.size, allLines),
27820
+ text: summarizeFile(input.path, stat20.size, allLines),
27377
27821
  total_lines: total,
27378
27822
  encoding: "utf8",
27379
27823
  truncated: total > 200,
@@ -27385,8 +27829,8 @@ var readTool = {
27385
27829
  };
27386
27830
  }
27387
27831
  if (limit === 0) {
27388
- ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
27389
- rememberReadRange(ctx, absPath, stat19.mtimeMs, total, 1, 0);
27832
+ ctx.recordRead(absPath, stat20.mtimeMs, "user", contentHash);
27833
+ rememberReadRange(ctx, absPath, stat20.mtimeMs, total, 1, 0);
27390
27834
  const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
27391
27835
  return {
27392
27836
  text: "",
@@ -27398,8 +27842,8 @@ var readTool = {
27398
27842
  };
27399
27843
  }
27400
27844
  if (offset > total) {
27401
- ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
27402
- rememberReadRange(ctx, absPath, stat19.mtimeMs, total, total + 1, total + 1);
27845
+ ctx.recordRead(absPath, stat20.mtimeMs, "user", contentHash);
27846
+ rememberReadRange(ctx, absPath, stat20.mtimeMs, total, total + 1, total + 1);
27403
27847
  const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
27404
27848
  return {
27405
27849
  text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
@@ -27414,8 +27858,8 @@ var readTool = {
27414
27858
  const truncated = offset - 1 + slice.length < total;
27415
27859
  const width = String(offset + slice.length - 1).length;
27416
27860
  const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
27417
- ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
27418
- rememberReadRange(ctx, absPath, stat19.mtimeMs, total, offset, offset + slice.length - 1);
27861
+ ctx.recordRead(absPath, stat20.mtimeMs, "user", contentHash);
27862
+ rememberReadRange(ctx, absPath, stat20.mtimeMs, total, offset, offset + slice.length - 1);
27419
27863
  const symResult = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
27420
27864
  return {
27421
27865
  text: numbered,
@@ -27523,8 +27967,8 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
27523
27967
  // src/replace.ts
27524
27968
  import { spawn as spawn14 } from "node:child_process";
27525
27969
  import * as fs30 from "node:fs/promises";
27526
- import * as path34 from "node:path";
27527
- import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
27970
+ import * as path35 from "node:path";
27971
+ import { ToolValidationError as ToolValidationError9 } from "@wrongstack/core/types";
27528
27972
  import {
27529
27973
  atomicWrite as atomicWrite3,
27530
27974
  buildChildEnv as buildChildEnv9,
@@ -27536,12 +27980,13 @@ import {
27536
27980
  unifiedDiff as unifiedDiff2
27537
27981
  } from "@wrongstack/core/utils";
27538
27982
  init_util();
27983
+ var MAX_DIFF_BYTES2 = 262144;
27539
27984
  var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
27540
27985
  var replaceTool = {
27541
27986
  name: "replace",
27542
27987
  category: "Transform",
27543
27988
  description: "Perform a search-and-replace across multiple files using a regex pattern. This is a powerful bulk transformation tool. Dry-run is ON by default \u2014 set `dry_run: false` to apply changes.",
27544
- usageHint: "DANGEROUS IF USED CARELESSLY \u2014 review the diff output carefully.\n\nRecommended workflow:\n1. Run without `dry_run: false` first to see exactly what would change (dry-run is the default).\n2. Review the diff output, then re-run with `dry_run: false` to apply.\n3. Use a specific enough `pattern` (and `glob` / `files`) to avoid accidental broad changes.\n4. `replace_all` controls whether only the first match per file or all matches are replaced.\nThis tool is excellent for large-scale refactors (renaming, import updates, etc.) but must be used with caution.",
27989
+ usageHint: "DANGEROUS IF USED CARELESSLY \u2014 review the diff output carefully.\n\nRecommended workflow:\n1. Run without `dry_run: false` first to see exactly what would change (dry-run is the default).\n2. Review the diff output, then re-run with `dry_run: false` to apply.\n3. Use a specific enough `pattern` (and `glob` / `files`) to avoid accidental broad changes.\n4. `replace_all` controls whether only the first match per file or all matches are replaced.\n5. `replacement` supports regex substitutions: `$1`\u2013`$9` insert capture groups, `$&` inserts the whole match, and `$$` inserts a literal dollar sign.\nThis tool is excellent for large-scale refactors (renaming, import updates, etc.) but must be used with caution.",
27545
27990
  permission: "confirm",
27546
27991
  // WS-046: gives permission decisions something to key on.
27547
27992
  // The file scope being rewritten, not the pattern: a trust rule should say
@@ -27551,11 +27996,15 @@ var replaceTool = {
27551
27996
  capabilities: ["fs.write"],
27552
27997
  icon: "edit",
27553
27998
  timeoutMs: 3e4,
27999
+ maxOutputBytes: 262144,
27554
28000
  inputSchema: {
27555
28001
  type: "object",
27556
28002
  properties: {
27557
28003
  pattern: { type: "string", description: "Regex pattern to match" },
27558
- replacement: { type: "string", description: "Replacement string" },
28004
+ replacement: {
28005
+ type: "string",
28006
+ description: "Replacement string. Supports `$1`\u2013`$9` (capture groups), `$&` (whole match), and `$$` (literal dollar sign) \u2014 same semantics as JavaScript String.replace."
28007
+ },
27559
28008
  files: {
27560
28009
  type: "string",
27561
28010
  description: "File(s) to target: single path, comma-separated list, or glob pattern"
@@ -27571,19 +28020,19 @@ var replaceTool = {
27571
28020
  },
27572
28021
  async execute(input, ctx) {
27573
28022
  if (!input?.pattern) {
27574
- throw new ToolValidationError6({
28023
+ throw new ToolValidationError9({
27575
28024
  message: "replace: pattern is required",
27576
28025
  field: "pattern"
27577
28026
  });
27578
28027
  }
27579
28028
  if (input.replacement === void 0) {
27580
- throw new ToolValidationError6({
28029
+ throw new ToolValidationError9({
27581
28030
  message: "replace: replacement is required",
27582
28031
  field: "replacement"
27583
28032
  });
27584
28033
  }
27585
28034
  if (!input?.files) {
27586
- throw new ToolValidationError6({
28035
+ throw new ToolValidationError9({
27587
28036
  message: "replace: files is required",
27588
28037
  field: "files"
27589
28038
  });
@@ -27591,7 +28040,7 @@ var replaceTool = {
27591
28040
  const replaceAll = input.replace_all ?? true;
27592
28041
  const compiled = compileUserRegex(input.pattern, "g");
27593
28042
  if (!compiled.ok) {
27594
- throw new ToolValidationError6({
28043
+ throw new ToolValidationError9({
27595
28044
  message: `replace: ${compiled.reason}`,
27596
28045
  field: "pattern"
27597
28046
  });
@@ -27604,6 +28053,9 @@ var replaceTool = {
27604
28053
  const realRoot = await fs30.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
27605
28054
  const results = [];
27606
28055
  let totalReplacements = 0;
28056
+ let diffBytesUsed = 0;
28057
+ let diffsOmitted = 0;
28058
+ let diffsTruncated = 0;
27607
28059
  for (const absPath of fileList) {
27608
28060
  const lstat2 = await fs30.lstat(absPath).catch((err) => {
27609
28061
  if (err.code === "ENOENT") return null;
@@ -27617,10 +28069,10 @@ var replaceTool = {
27617
28069
  } catch {
27618
28070
  continue;
27619
28071
  }
27620
- const rel = path34.relative(realRoot, realPath);
27621
- if (rel.startsWith("..") || path34.isAbsolute(rel)) continue;
27622
- const stat19 = await fs30.stat(realPath).catch(() => null);
27623
- if (!stat19?.isFile()) continue;
28072
+ const rel = path35.relative(realRoot, realPath);
28073
+ if (rel.startsWith("..") || path35.isAbsolute(rel)) continue;
28074
+ const stat20 = await fs30.stat(realPath).catch(() => null);
28075
+ if (!stat20?.isFile()) continue;
27624
28076
  let content;
27625
28077
  try {
27626
28078
  const buf = await fs30.readFile(realPath);
@@ -27639,13 +28091,13 @@ var replaceTool = {
27639
28091
  let newContentLf = contentLf;
27640
28092
  for (let i = matches.length - 1; i >= 0; i--) {
27641
28093
  const m = expectDefined8(matches[i]);
27642
- newContentLf = newContentLf.slice(0, m.index) + input.replacement + newContentLf.slice(expectDefined8(m.index) + m[0].length);
28094
+ newContentLf = newContentLf.slice(0, m.index) + expandReplacement(input.replacement, m) + newContentLf.slice(expectDefined8(m.index) + m[0].length);
27643
28095
  }
27644
28096
  re.lastIndex = 0;
27645
28097
  totalReplacements += count;
27646
28098
  if (!dryRun) {
27647
28099
  const newContent = toStyle2(newContentLf, style);
27648
- await atomicWrite3(realPath, newContent, { mode: stat19.mode & 511 });
28100
+ await atomicWrite3(realPath, newContent, { mode: stat20.mode & 511 });
27649
28101
  const written = await fs30.stat(realPath).catch(() => null);
27650
28102
  if (written) {
27651
28103
  ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
@@ -27657,24 +28109,76 @@ var replaceTool = {
27657
28109
  after: newContent
27658
28110
  });
27659
28111
  }
27660
- const diff = dryRun || matches.length > 0 ? unifiedDiff2(content, toStyle2(newContentLf, style), {
28112
+ let diff = dryRun || matches.length > 0 ? unifiedDiff2(content, toStyle2(newContentLf, style), {
27661
28113
  fromFile: absPath,
27662
28114
  toFile: absPath
27663
28115
  }) : void 0;
28116
+ if (diff !== void 0) {
28117
+ const remaining = MAX_DIFF_BYTES2 - diffBytesUsed;
28118
+ if (remaining <= 0) {
28119
+ diff = void 0;
28120
+ diffsOmitted++;
28121
+ } else {
28122
+ const capped = truncateDiffPayload(diff, remaining);
28123
+ if (capped.truncated) diffsTruncated++;
28124
+ diff = capped.text;
28125
+ diffBytesUsed += Buffer.byteLength(diff, "utf8");
28126
+ }
28127
+ }
27664
28128
  results.push({
27665
28129
  path: absPath,
27666
28130
  replacements: matches.length,
27667
28131
  diff
27668
28132
  });
27669
28133
  }
28134
+ const overBudget = diffsOmitted > 0 || diffsTruncated > 0;
27670
28135
  return {
27671
28136
  files_modified: results.length,
27672
28137
  total_replacements: totalReplacements,
27673
28138
  results,
27674
- dry_run: dryRun
28139
+ dry_run: dryRun,
28140
+ note: overBudget ? `Diff payload exceeded the 256 KiB output budget: ${diffsTruncated} diff(s) truncated, ${diffsOmitted} diff(s) omitted. Replacement counts are complete; use the read tool to inspect individual files.` : void 0
27675
28141
  };
27676
28142
  }
27677
28143
  };
28144
+ function expandReplacement(template, match) {
28145
+ if (!template.includes("$")) return template;
28146
+ let out = "";
28147
+ for (let i = 0; i < template.length; i++) {
28148
+ const ch = template[i];
28149
+ if (ch !== "$") {
28150
+ out += ch;
28151
+ continue;
28152
+ }
28153
+ const next = template[i + 1];
28154
+ if (next === "$") {
28155
+ out += "$";
28156
+ i++;
28157
+ } else if (next === "&") {
28158
+ out += match[0];
28159
+ i++;
28160
+ } else if (next !== void 0 && next >= "1" && next <= "9") {
28161
+ const idx = next.charCodeAt(0) - 48;
28162
+ if (idx < match.length) {
28163
+ out += match[idx] ?? "";
28164
+ i++;
28165
+ } else {
28166
+ out += "$";
28167
+ }
28168
+ } else {
28169
+ out += "$";
28170
+ }
28171
+ }
28172
+ return out;
28173
+ }
28174
+ function passesExtraGlob(extraGlob, name, full) {
28175
+ extraGlob.lastIndex = 0;
28176
+ const nameMatch = extraGlob.test(name);
28177
+ extraGlob.lastIndex = 0;
28178
+ const fullMatch = extraGlob.test(full);
28179
+ extraGlob.lastIndex = 0;
28180
+ return nameMatch || fullMatch;
28181
+ }
27678
28182
  async function resolveFiles2(filesInput, ctx, extraGlob) {
27679
28183
  const base = ctx.cwd;
27680
28184
  const normalized = filesInput.trim();
@@ -27685,8 +28189,9 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
27685
28189
  const resolved = [];
27686
28190
  for (const p of parts) {
27687
28191
  const absPath = safeResolve(p, ctx);
27688
- const stat19 = await fs30.stat(absPath).catch(() => null);
27689
- if (stat19?.isFile()) {
28192
+ if (extraGlob && !passesExtraGlob(extraGlob, path35.basename(absPath), absPath)) continue;
28193
+ const stat20 = await fs30.stat(absPath).catch(() => null);
28194
+ if (stat20?.isFile()) {
27690
28195
  resolved.push(absPath);
27691
28196
  }
27692
28197
  }
@@ -27697,26 +28202,32 @@ async function globFiles(pattern, base, extraGlob) {
27697
28202
  if (rgAvailable) {
27698
28203
  try {
27699
28204
  const { promise } = spawnRgFind(pattern, base);
27700
- return await promise;
28205
+ const files = await promise;
28206
+ if (extraGlob) {
28207
+ return files.filter((f) => passesExtraGlob(extraGlob, path35.basename(f), f));
28208
+ }
28209
+ return files;
27701
28210
  } catch {
27702
28211
  }
27703
28212
  }
27704
28213
  return await globNative(pattern, base, extraGlob);
27705
28214
  }
28215
+ var rgAvailabilityCache2;
27706
28216
  function checkRg() {
27707
- return new Promise((resolve17) => {
28217
+ rgAvailabilityCache2 ??= new Promise((resolve18) => {
27708
28218
  try {
27709
28219
  const p = spawn14("rg", ["--version"], {
27710
28220
  env: buildChildEnv9(),
27711
28221
  stdio: "ignore",
27712
28222
  windowsHide: true
27713
28223
  });
27714
- p.on("error", () => resolve17(false));
27715
- p.on("close", (code) => resolve17(code === 0));
28224
+ p.on("error", () => resolve18(false));
28225
+ p.on("close", (code) => resolve18(code === 0));
27716
28226
  } catch {
27717
- resolve17(false);
28227
+ resolve18(false);
27718
28228
  }
27719
28229
  });
28230
+ return rgAvailabilityCache2;
27720
28231
  }
27721
28232
  function spawnRgFind(pattern, base) {
27722
28233
  const args = ["--files", "--glob", pattern, base];
@@ -27739,10 +28250,10 @@ function spawnRgFind(pattern, base) {
27739
28250
  }
27740
28251
  });
27741
28252
  return {
27742
- promise: new Promise((resolve17, reject) => {
28253
+ promise: new Promise((resolve18, reject) => {
27743
28254
  child.on("error", reject);
27744
28255
  child.on("close", () => {
27745
- resolve17(buf.split("\n").filter(Boolean));
28256
+ resolve18(buf.split("\n").filter(Boolean));
27746
28257
  });
27747
28258
  })
27748
28259
  };
@@ -27759,10 +28270,10 @@ async function globNative(pattern, base, extraGlob) {
27759
28270
  }
27760
28271
  for (const e of entries) {
27761
28272
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
27762
- const full = path34.join(dir, e.name);
28273
+ const full = path35.join(dir, e.name);
27763
28274
  try {
27764
- const stat19 = await fs30.lstat(full);
27765
- if (stat19.isSymbolicLink()) continue;
28275
+ const stat20 = await fs30.lstat(full);
28276
+ if (stat20.isSymbolicLink()) continue;
27766
28277
  } catch {
27767
28278
  continue;
27768
28279
  }
@@ -27786,7 +28297,7 @@ async function globNative(pattern, base, extraGlob) {
27786
28297
  // src/scaffold.ts
27787
28298
  init_util();
27788
28299
  import * as fs31 from "node:fs/promises";
27789
- import * as path35 from "node:path";
28300
+ import * as path36 from "node:path";
27790
28301
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
27791
28302
  var BUILT_IN_TEMPLATES = {
27792
28303
  "npm-package": {
@@ -27937,16 +28448,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
27937
28448
  let filesCreated = 0;
27938
28449
  for (const [filePath, content] of Object.entries(templateFiles)) {
27939
28450
  const resolvedPath = substituteVars(filePath, name, vars);
27940
- const joinedPath = path35.join(cwd, resolvedPath);
27941
- const root = path35.resolve(ctx.projectRoot);
27942
- const target = path35.resolve(joinedPath);
27943
- const rel = path35.relative(root, target);
27944
- if (rel.startsWith("..") || path35.isAbsolute(rel)) {
28451
+ const joinedPath = path36.join(cwd, resolvedPath);
28452
+ const root = path36.resolve(ctx.projectRoot);
28453
+ const target = path36.resolve(joinedPath);
28454
+ const rel = path36.relative(root, target);
28455
+ if (rel.startsWith("..") || path36.isAbsolute(rel)) {
27945
28456
  throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
27946
28457
  }
27947
28458
  const fullPath = target;
27948
28459
  if (!dryRun) {
27949
- await fs31.mkdir(path35.dirname(fullPath), { recursive: true });
28460
+ await fs31.mkdir(path36.dirname(fullPath), { recursive: true });
27950
28461
  await atomicWrite4(fullPath, substituteVars(content, name, vars));
27951
28462
  }
27952
28463
  files.push(resolvedPath);
@@ -27977,11 +28488,12 @@ function substituteVars(content, name, vars) {
27977
28488
  }
27978
28489
 
27979
28490
  // src/search.ts
27980
- import { FetchError as FetchError3, ToolValidationError as ToolValidationError7 } from "@wrongstack/core/types";
28491
+ import { FetchError as FetchError3, ToolValidationError as ToolValidationError10 } from "@wrongstack/core/types";
27981
28492
  import { expectDefined as expectDefined9 } from "@wrongstack/core/utils";
27982
- import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
28493
+ import { toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
27983
28494
  var DEFAULT_NUM = 10;
27984
28495
  var MAX_RESULTS = 50;
28496
+ var MAX_SNIPPET_CHARS = 300;
27985
28497
  var TIMEOUT_MS3 = 15e3;
27986
28498
  var CACHE_TTL_MS = 3e5;
27987
28499
  var CACHE_MAX_ENTRIES = 200;
@@ -27989,7 +28501,7 @@ var cache = /* @__PURE__ */ new Map();
27989
28501
  var searchTool = {
27990
28502
  name: "search",
27991
28503
  category: "Search",
27992
- description: "Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase. Results are cached (5 min TTL) and deduplicated by URL.",
28504
+ description: "Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase. Results are cached (5 min TTL) and deduplicated by URL. google and bing are best-effort HTML scrapes that fall back to duckduckgo when they return nothing usable.",
27993
28505
  usageHint: "Good for: API documentation, error messages, library usage examples, current best practices.\n\n- Prefer specific queries over very broad ones.\n- Results go through the guarded fetch system (same protections as the `fetch` tool).\n- Supports duckduckgo (default), google, and bing sources.\n- Set `skip_cache: true` to force a fresh search.\n- This is often better than the model trying to recall outdated knowledge.",
27994
28506
  permission: "auto",
27995
28507
  mutating: false,
@@ -28030,7 +28542,7 @@ var searchTool = {
28030
28542
  },
28031
28543
  async *executeStream(input, _ctx, opts) {
28032
28544
  if (!input?.query || input.query.trim() === "") {
28033
- throw new ToolValidationError7({
28545
+ throw new ToolValidationError10({
28034
28546
  message: "search: query is required and must be a non-empty string",
28035
28547
  field: "query"
28036
28548
  });
@@ -28063,7 +28575,7 @@ var searchTool = {
28063
28575
  query: input.query,
28064
28576
  results: results.slice(0, num),
28065
28577
  source: entry.source,
28066
- truncated: results.length >= num,
28578
+ truncated: results.length > num,
28067
28579
  cached: true
28068
28580
  }
28069
28581
  };
@@ -28075,41 +28587,45 @@ var searchTool = {
28075
28587
  text: `Querying ${source} for "${input.query}"\u2026`,
28076
28588
  data: { source, query: input.query, cached: false }
28077
28589
  };
28078
- let rawResults;
28590
+ let engine;
28079
28591
  let effectiveSource = source;
28080
28592
  switch (source) {
28081
28593
  case "duckduckgo":
28082
- rawResults = await duckduckgoSearch(input.query, num, opts.signal);
28594
+ engine = await duckduckgoSearch(input.query, opts.signal);
28083
28595
  break;
28084
28596
  case "google":
28085
- rawResults = await googleSearch(input.query, num, opts.signal);
28597
+ engine = await googleSearch(input.query, opts.signal);
28086
28598
  break;
28087
28599
  case "bing":
28088
- rawResults = await bingSearch(input.query, num, opts.signal);
28600
+ engine = await bingSearch(input.query, opts.signal);
28089
28601
  break;
28090
28602
  default:
28091
- throw new ToolValidationError7({
28603
+ throw new ToolValidationError10({
28092
28604
  message: `search: unknown source "${source}"`,
28093
28605
  field: "source"
28094
28606
  });
28095
28607
  }
28096
- let ranked = rankSearchResults(rawResults, input.query);
28608
+ let ranked = rankSearchResults(engine.results, input.query);
28609
+ let engineError = engine.error;
28097
28610
  if (source !== "duckduckgo" && shouldFallbackToDuckDuckGo(ranked, input.query)) {
28098
28611
  yield {
28099
28612
  type: "log",
28100
28613
  text: `${source} returned no relevant static results; falling back to duckduckgo`,
28101
28614
  data: { source, fallback: "duckduckgo", query: input.query }
28102
28615
  };
28103
- rawResults = await duckduckgoSearch(input.query, num, opts.signal);
28104
- ranked = rankSearchResults(rawResults, input.query);
28616
+ const fallback = await duckduckgoSearch(input.query, opts.signal);
28617
+ ranked = rankSearchResults(fallback.results, input.query);
28618
+ engineError = fallback.error;
28105
28619
  effectiveSource = "duckduckgo";
28106
28620
  }
28107
28621
  const finalResults = ranked.slice(0, num);
28108
- cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
28109
- pruneCacheEntries();
28622
+ if (!engineError) {
28623
+ cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
28624
+ pruneCacheEntries();
28625
+ }
28110
28626
  yield {
28111
28627
  type: "partial_output",
28112
- text: `${finalResults.length} results from ${effectiveSource}`,
28628
+ text: engineError ? `search failed: ${engineError}` : `${finalResults.length} results from ${effectiveSource}`,
28113
28629
  data: { count: finalResults.length, cached: false, source: effectiveSource }
28114
28630
  };
28115
28631
  yield {
@@ -28122,8 +28638,9 @@ var searchTool = {
28122
28638
  snippet: r.snippet
28123
28639
  })),
28124
28640
  source: effectiveSource,
28125
- truncated: finalResults.length >= num,
28126
- cached: false
28641
+ truncated: ranked.length > num,
28642
+ cached: false,
28643
+ ...engineError ? { error: engineError } : {}
28127
28644
  }
28128
28645
  };
28129
28646
  }
@@ -28174,18 +28691,18 @@ function shouldFallbackToDuckDuckGo(results, query) {
28174
28691
  return terms.some((term) => haystack.includes(term));
28175
28692
  });
28176
28693
  }
28177
- async function duckduckgoSearch(query, num, signal) {
28694
+ async function duckduckgoSearch(query, signal) {
28178
28695
  const encoded = encodeURIComponent(query);
28179
28696
  const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
28180
28697
  try {
28181
28698
  const response = await fetchWithTimeout(url, signal, TIMEOUT_MS3);
28182
28699
  const html = await response.text();
28183
- return parseDuckDuckGo(html, num);
28700
+ return { results: parseDuckDuckGo(html, MAX_RESULTS) };
28184
28701
  } catch (err) {
28185
28702
  console.log(
28186
- JSON.stringify({ level: "debug", event: "search_failed", query, error: toErrorMessage6(err) })
28703
+ JSON.stringify({ level: "debug", event: "search_failed", query, error: toErrorMessage9(err) })
28187
28704
  );
28188
- return [{ title: "Search unavailable", url: "https://duckduckgo.com/unavailable", snippet: "Could not reach DuckDuckGo", score: 0 }];
28705
+ return { results: [], error: `duckduckgo unreachable: ${toErrorMessage9(err)}` };
28189
28706
  }
28190
28707
  }
28191
28708
  function takeFrom(iter, max) {
@@ -28219,7 +28736,7 @@ function parseDuckDuckGo(html, num) {
28219
28736
  results.push({
28220
28737
  title: entry.title ?? "",
28221
28738
  url: entry.url ?? "",
28222
- snippet: snippetMatches[i] ?? "",
28739
+ snippet: capSnippet(snippetMatches[i] ?? ""),
28223
28740
  score: 1
28224
28741
  });
28225
28742
  }
@@ -28244,11 +28761,15 @@ function normalizeDuckDuckGoUrl(raw) {
28244
28761
  return raw;
28245
28762
  }
28246
28763
  }
28247
- async function googleSearch(query, num, signal) {
28764
+ async function googleSearch(query, signal) {
28248
28765
  const encoded = encodeURIComponent(query);
28249
28766
  const url = `https://www.google.com/search?q=${encoded}&hl=en`;
28250
- const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text()).catch(() => "");
28251
- return parseGoogleResults(html, num);
28767
+ try {
28768
+ const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text());
28769
+ return { results: parseGoogleResults(html, MAX_RESULTS) };
28770
+ } catch (err) {
28771
+ return { results: [], error: `google unreachable: ${toErrorMessage9(err)}` };
28772
+ }
28252
28773
  }
28253
28774
  function parseGoogleResults(html, num) {
28254
28775
  const results = [];
@@ -28271,17 +28792,21 @@ function parseGoogleResults(html, num) {
28271
28792
  results.push({
28272
28793
  title: titles[i] ?? "",
28273
28794
  url: urls[i] ?? "",
28274
- snippet: snippets[i] ?? "",
28795
+ snippet: capSnippet(snippets[i] ?? ""),
28275
28796
  score: 1
28276
28797
  });
28277
28798
  }
28278
28799
  return results;
28279
28800
  }
28280
- async function bingSearch(query, num, signal) {
28801
+ async function bingSearch(query, signal) {
28281
28802
  const encoded = encodeURIComponent(query);
28282
28803
  const url = `https://www.bing.com/search?q=${encoded}`;
28283
- const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text()).catch(() => "");
28284
- return parseBingResults(html, num);
28804
+ try {
28805
+ const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text());
28806
+ return { results: parseBingResults(html, MAX_RESULTS) };
28807
+ } catch (err) {
28808
+ return { results: [], error: `bing unreachable: ${toErrorMessage9(err)}` };
28809
+ }
28285
28810
  }
28286
28811
  function parseBingResults(html, num) {
28287
28812
  const results = [];
@@ -28294,7 +28819,7 @@ function parseBingResults(html, num) {
28294
28819
  const title = stripTags(expectDefined9(titleMatch[2]));
28295
28820
  if (!href || !title) return [];
28296
28821
  const snippetMatch = /<p\b[^>]*class=(["'])[^"']*\b(?:b_paractl|b_lineclamp\d*)\b[^"']*\1[^>]*>([\s\S]*?)<\/p>/i.exec(block) ?? /<p\b[^>]*>([\s\S]*?)<\/p>/i.exec(block);
28297
- const snippet = snippetMatch ? stripTags(expectDefined9(snippetMatch.at(-1))) : "";
28822
+ const snippet = snippetMatch ? capSnippet(stripTags(expectDefined9(snippetMatch.at(-1)))) : "";
28298
28823
  return [{ url: normalizeBingUrl(href), title, snippet, score: 1 }];
28299
28824
  }), num);
28300
28825
  for (let i = 0; i < entries.length; i++) {
@@ -28361,17 +28886,20 @@ function anySignal(...signals) {
28361
28886
  function stripTags(html) {
28362
28887
  return decodeHtmlEntities(html.replace(/<[^>]+>/g, "")).trim();
28363
28888
  }
28889
+ function capSnippet(snippet) {
28890
+ return snippet.length > MAX_SNIPPET_CHARS ? `${snippet.slice(0, MAX_SNIPPET_CHARS - 1)}\u2026` : snippet;
28891
+ }
28364
28892
  function decodeHtmlEntities(text) {
28365
28893
  return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
28366
28894
  }
28367
28895
 
28368
28896
  // src/set-working-dir.ts
28369
28897
  import * as fs32 from "node:fs/promises";
28370
- import { toErrorMessage as toErrorMessage7 } from "@wrongstack/core/utils";
28898
+ import { toErrorMessage as toErrorMessage10 } from "@wrongstack/core/utils";
28371
28899
  var setWorkingDirTool = {
28372
28900
  name: "set_working_dir",
28373
28901
  category: "Context",
28374
- description: "Change the current working directory for all subsequent file operations. The new directory must be inside the project root. Use this to navigate between subdirectories when working on files in different parts of the project.",
28902
+ description: "Change the current working directory for subsequent file operations and shell tools (`bash`/`exec` spawn in this directory unless given an explicit cwd). The new directory must be inside the project root. Use this to navigate between subdirectories when working on files in different parts of the project.",
28375
28903
  usageHint: "Change the working directory so relative paths in subsequent tool calls resolve from a different directory. Pass `path` to set a new directory, or omit to query the current one. The directory must exist and be inside the project root.",
28376
28904
  permission: "confirm",
28377
28905
  mutating: true,
@@ -28401,19 +28929,23 @@ var setWorkingDirTool = {
28401
28929
  } catch (err) {
28402
28930
  return {
28403
28931
  current: ctx.workingDir,
28404
- error: toErrorMessage7(err)
28932
+ error: toErrorMessage10(err)
28405
28933
  };
28406
28934
  }
28935
+ let isDirectory = false;
28407
28936
  try {
28408
- await fs32.access(resolved);
28937
+ isDirectory = (await fs32.stat(resolved)).isDirectory();
28409
28938
  } catch {
28939
+ isDirectory = false;
28940
+ }
28941
+ if (!isDirectory) {
28410
28942
  try {
28411
28943
  ctx.setWorkingDir(previous);
28412
28944
  } catch {
28413
28945
  }
28414
28946
  return {
28415
28947
  current: ctx.workingDir,
28416
- error: `Directory does not exist: ${resolved}`
28948
+ error: `Directory does not exist (or is not a directory): ${resolved}`
28417
28949
  };
28418
28950
  }
28419
28951
  return {
@@ -28868,6 +29400,7 @@ var taskTool = {
28868
29400
  inProgress: 0
28869
29401
  };
28870
29402
  }
29403
+ ctx.meta["task.path.resolved"] = taskPath;
28871
29404
  if (todosToReplace) {
28872
29405
  await todoTool.execute({ todos: todosToReplace }, ctx, {
28873
29406
  signal: AbortSignal.timeout(3e4)
@@ -28892,6 +29425,7 @@ var taskTool = {
28892
29425
  formatted = formatPlan2(updated);
28893
29426
  return updated;
28894
29427
  });
29428
+ ctx.meta["plan.path.resolved"] = planPath;
28895
29429
  } catch (err) {
28896
29430
  return {
28897
29431
  ok: false,
@@ -28935,7 +29469,7 @@ ${formatTaskList2(file.tasks)}` : file.tasks.length > 0 ? formatTaskList2(file.t
28935
29469
  init_spawn_stream();
28936
29470
  init_util();
28937
29471
  init_legacy_bridge();
28938
- import * as path36 from "node:path";
29472
+ import * as path37 from "node:path";
28939
29473
  var testTool = {
28940
29474
  name: "test",
28941
29475
  category: "Code Quality",
@@ -29038,11 +29572,11 @@ var testTool = {
29038
29572
  }
29039
29573
  };
29040
29574
  async function detectRunner(cwd) {
29041
- const { stat: stat19 } = await import("node:fs/promises");
29575
+ const { stat: stat20 } = await import("node:fs/promises");
29042
29576
  const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
29043
29577
  for (const f of candidates) {
29044
29578
  try {
29045
- await stat19(path36.join(cwd, f));
29579
+ await stat20(path37.join(cwd, f));
29046
29580
  if (f.includes("vitest")) return "vitest";
29047
29581
  if (f.includes("jest")) return "jest";
29048
29582
  if (f.includes("mocha")) return "mocha";
@@ -29420,7 +29954,7 @@ var toolUseTool = {
29420
29954
  // src/tree.ts
29421
29955
  init_util();
29422
29956
  import * as fs33 from "node:fs/promises";
29423
- import * as path37 from "node:path";
29957
+ import * as path38 from "node:path";
29424
29958
  import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
29425
29959
  var DEFAULT_IGNORE5 = /* @__PURE__ */ new Set([
29426
29960
  ...DEFAULT_WALK_IGNORE_DIRS4,
@@ -29440,6 +29974,7 @@ var treeTool = {
29440
29974
  mutating: false,
29441
29975
  capabilities: ["fs.read"],
29442
29976
  icon: "tree",
29977
+ maxOutputBytes: 262144,
29443
29978
  timeoutMs: 15e3,
29444
29979
  inputSchema: {
29445
29980
  type: "object",
@@ -29588,17 +30123,15 @@ async function walkDir(dir, depth, opts) {
29588
30123
  if (opts.exclude.has(e.name)) return false;
29589
30124
  return true;
29590
30125
  });
29591
- if (depth > 0) {
29592
- let dirCount = 0;
29593
- let fileCount = 0;
29594
- for (const e of filtered) {
29595
- if (e.isDirectory()) dirCount++;
29596
- else if (e.isFile()) fileCount++;
29597
- }
29598
- opts.totalDirs.value += dirCount;
29599
- opts.totalFiles.value += fileCount;
29600
- opts.onProgress?.();
29601
- }
30126
+ let dirCount = 0;
30127
+ let fileCount = 0;
30128
+ for (const e of filtered) {
30129
+ if (e.isDirectory()) dirCount++;
30130
+ else if (e.isFile()) fileCount++;
30131
+ }
30132
+ opts.totalDirs.value += dirCount;
30133
+ opts.totalFiles.value += fileCount;
30134
+ opts.onProgress?.();
29602
30135
  const items = filtered.sort((a, b) => {
29603
30136
  if (a.isDirectory() && !b.isDirectory()) return -1;
29604
30137
  if (!a.isDirectory() && b.isDirectory()) return 1;
@@ -29626,7 +30159,7 @@ async function walkDir(dir, depth, opts) {
29626
30159
  opts.retention.outputBytes += lineBytes;
29627
30160
  if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
29628
30161
  const childPrefix = opts.prefix + connector;
29629
- await walkDir(path37.join(dir, entry.name), depth + 1, {
30162
+ await walkDir(path38.join(dir, entry.name), depth + 1, {
29630
30163
  ...opts,
29631
30164
  prefix: childPrefix,
29632
30165
  isLast
@@ -29639,7 +30172,7 @@ async function walkDir(dir, depth, opts) {
29639
30172
  init_spawn_stream();
29640
30173
  init_util();
29641
30174
  init_legacy_bridge();
29642
- import * as path38 from "node:path";
30175
+ import * as path39 from "node:path";
29643
30176
  var typecheckTool = {
29644
30177
  name: "typecheck",
29645
30178
  category: "Code Quality",
@@ -29661,11 +30194,7 @@ var typecheckTool = {
29661
30194
  },
29662
30195
  all: {
29663
30196
  type: "boolean",
29664
- description: "Type-check all projects (pnpm -r) (default: false)"
29665
- },
29666
- json: {
29667
- type: "boolean",
29668
- description: "Emit JSON output from tsc (default: false)"
30197
+ description: "Type-check all workspace packages (pnpm workspaces run `pnpm -r exec tsc --noEmit`; other setups run a single `tsc --noEmit` at cwd) (default: false)"
29669
30198
  }
29670
30199
  }
29671
30200
  },
@@ -29701,29 +30230,42 @@ var typecheckTool = {
29701
30230
  };
29702
30231
  return;
29703
30232
  }
29704
- let args;
30233
+ let cmd;
30234
+ let cmdArgs;
29705
30235
  let project;
29706
30236
  if (input.all) {
29707
- args = ["--noEmit"];
29708
30237
  project = "workspace";
30238
+ const tscArgs = ["--noEmit"];
30239
+ if (input.strict) tscArgs.push("--strict");
30240
+ const manager = await detectPackageManager(cwd, ctx.projectRoot);
30241
+ if (manager === "pnpm") {
30242
+ cmd = "pnpm";
30243
+ cmdArgs = ["-r", "--no-bail", "exec", "tsc", ...tscArgs];
30244
+ } else {
30245
+ cmd = "npx";
30246
+ cmdArgs = ["tsc", ...tscArgs];
30247
+ }
29709
30248
  } else {
29710
30249
  const tsconfig = input.project ? safeResolve(input.project, ctx) : await findTsConfig(cwd);
29711
- args = ["--noEmit"];
29712
- if (input.strict) args.push("--strict");
29713
- if (tsconfig) args.push("--project", tsconfig);
30250
+ const tscArgs = ["--noEmit"];
30251
+ if (input.strict) tscArgs.push("--strict");
30252
+ if (tsconfig) tscArgs.push("--project", tsconfig);
29714
30253
  project = tsconfig ?? "default";
30254
+ cmd = "npx";
30255
+ cmdArgs = ["tsc", ...tscArgs];
29715
30256
  }
29716
- if (input.json) args.push("--json");
29717
- yield { type: "log", text: `tsc ${args.join(" ")}`, data: { project } };
30257
+ yield { type: "log", text: `${cmd} ${cmdArgs.join(" ")}`, data: { project } };
29718
30258
  const result = yield* spawnStream({
29719
- cmd: "npx",
29720
- args: ["tsc", ...args],
30259
+ cmd,
30260
+ args: cmdArgs,
29721
30261
  cwd,
29722
30262
  signal: opts.signal,
29723
30263
  maxBytes: 2e5
29724
30264
  });
29725
- const errors = [...result.stdout.matchAll(/\berror\b/gi)].length;
29726
- const warnings = [...result.stdout.matchAll(/\bwarning\b/gi)].length;
30265
+ const combined = `${result.stdout}
30266
+ ${result.stderr}`;
30267
+ const errors = [...combined.matchAll(/^.*\berror TS\d+:/gm)].length;
30268
+ const warnings = [...combined.matchAll(/^.*\bwarning TS\d+:/gm)].length;
29727
30269
  yield {
29728
30270
  type: "final",
29729
30271
  output: {
@@ -29738,12 +30280,12 @@ var typecheckTool = {
29738
30280
  }
29739
30281
  };
29740
30282
  async function findTsConfig(cwd) {
29741
- const { stat: stat19 } = await import("node:fs/promises");
30283
+ const { stat: stat20 } = await import("node:fs/promises");
29742
30284
  const candidates = ["tsconfig.json", "tsconfig.base.json"];
29743
30285
  for (const f of candidates) {
29744
30286
  try {
29745
- const s = await stat19(path38.join(cwd, f));
29746
- if (s.isFile()) return path38.join(cwd, f);
30287
+ const s = await stat20(path39.join(cwd, f));
30288
+ if (s.isFile()) return path39.join(cwd, f);
29747
30289
  } catch {
29748
30290
  }
29749
30291
  }
@@ -29752,21 +30294,32 @@ async function findTsConfig(cwd) {
29752
30294
 
29753
30295
  // src/write.ts
29754
30296
  import * as fs34 from "node:fs/promises";
29755
- import { ToolValidationError as ToolValidationError8 } from "@wrongstack/core/types";
29756
- import { atomicWrite as atomicWrite5, unifiedDiff as unifiedDiff3 } from "@wrongstack/core/utils";
30297
+ import { ToolValidationError as ToolValidationError11 } from "@wrongstack/core/types";
30298
+ import {
30299
+ atomicWrite as atomicWrite5,
30300
+ detectNewlineStyle as detectNewlineStyle3,
30301
+ normalizeToLf as normalizeToLf3,
30302
+ toStyle as toStyle3,
30303
+ unifiedDiff as unifiedDiff3
30304
+ } from "@wrongstack/core/utils";
29757
30305
  init_util();
30306
+ var MAX_DIFF_BYTES3 = 262144;
29758
30307
  var writeTool = {
29759
30308
  name: "write",
29760
30309
  category: "Filesystem",
29761
30310
  description: "Write or completely overwrite a file on disk. This is a high-privilege operation. For modifying existing files, you should almost always prefer the `edit` tool instead, because `edit` is safer and works on the last-read version of the file.",
29762
- usageHint: "RULES FOR CORRECT USAGE:\n- Use `write` primarily for **new files** or when you want to replace the entire content.\n- For any existing file, strongly prefer `edit` (it requires a prior `read` in the same session and is more precise).\n- You MUST have called `read` on the file earlier in the conversation before using `write` on an existing path (the system enforces this for safety).\n- The path is resolved relative to the project root and protected against escaping the workspace.",
30311
+ usageHint: "RULES FOR CORRECT USAGE:\n- Use `write` primarily for **new files** or when you want to replace the entire content.\n- For any existing file, strongly prefer `edit` (it requires a prior `read` in the same session and is more precise).\n- When overwriting an existing file, the tool reads the current content itself to compute the diff \u2014 but still `read` the file first before large rewrites so you know what you are replacing.\n- When overwriting an existing file, the content is normalized to the dominant line-ending style (CRLF/LF) of the existing file; new files are written verbatim.\n- The path is resolved relative to the project root and protected against escaping the workspace.",
29763
30312
  selection: {
29764
30313
  doNotUseWhen: "making a precise change to part of an existing file.",
29765
30314
  useInstead: ["edit"]
29766
30315
  },
29767
30316
  permission: "confirm",
30317
+ // WS-046: gives permission decisions something to key on — the file being
30318
+ // written, so trust rules can scope by path.
30319
+ subjectKey: "path",
29768
30320
  mutating: true,
29769
30321
  timeoutMs: 5e3,
30322
+ maxOutputBytes: 262144,
29770
30323
  capabilities: ["fs.write"],
29771
30324
  icon: "file",
29772
30325
  inputSchema: {
@@ -29802,13 +30355,13 @@ async function writeFile6(input, ctx, signal) {
29802
30355
  }
29803
30356
  async function prepareWrite(input, ctx) {
29804
30357
  if (!input?.path) {
29805
- throw new ToolValidationError8({
30358
+ throw new ToolValidationError11({
29806
30359
  message: "write: path is required",
29807
30360
  field: "path"
29808
30361
  });
29809
30362
  }
29810
30363
  if (input.content === void 0) {
29811
- throw new ToolValidationError8({
30364
+ throw new ToolValidationError11({
29812
30365
  message: "write: content is required",
29813
30366
  field: "content"
29814
30367
  });
@@ -29817,12 +30370,12 @@ async function prepareWrite(input, ctx) {
29817
30370
  let existed = false;
29818
30371
  let prev = "";
29819
30372
  try {
29820
- const stat19 = await fs34.stat(absPath);
29821
- existed = stat19.isFile();
30373
+ const stat20 = await fs34.stat(absPath);
30374
+ existed = stat20.isFile();
29822
30375
  if (existed) {
29823
30376
  if (!ctx.hasRead(absPath)) {
29824
30377
  prev = await fs34.readFile(absPath, "utf8");
29825
- ctx.recordRead(absPath, stat19.mtimeMs, "write", sha256hex(prev));
30378
+ ctx.recordRead(absPath, stat20.mtimeMs, "write", sha256hex(prev));
29826
30379
  } else {
29827
30380
  prev = await fs34.readFile(absPath, "utf8");
29828
30381
  }
@@ -29835,31 +30388,42 @@ async function prepareWrite(input, ctx) {
29835
30388
  return { absPath, existed, prev };
29836
30389
  }
29837
30390
  async function finishWrite(input, ctx, prepared, signal) {
30391
+ const content = prepared.existed ? toStyle3(normalizeToLf3(input.content), detectNewlineStyle3(prepared.prev)) : input.content;
29838
30392
  signal?.throwIfAborted();
29839
- await atomicWrite5(prepared.absPath, input.content);
29840
- const diff = prepared.existed ? unifiedDiff3(prepared.prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
29841
- + (new file, ${input.content.split("\n").length} lines)`;
29842
- const stat19 = await fs34.stat(prepared.absPath);
29843
- ctx.recordRead(prepared.absPath, stat19.mtimeMs, "write", sha256hex(input.content));
30393
+ await atomicWrite5(prepared.absPath, content);
30394
+ const rawDiff = prepared.existed ? unifiedDiff3(prepared.prev, content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
30395
+ + (new file, ${content.split("\n").length} lines)`;
30396
+ const { text: diff, truncated: diffTruncated } = truncateDiffPayload(rawDiff, MAX_DIFF_BYTES3);
30397
+ const stat20 = await fs34.stat(prepared.absPath);
30398
+ ctx.recordRead(prepared.absPath, stat20.mtimeMs, "write", sha256hex(content));
29844
30399
  ctx.session.recordFileChange({
29845
30400
  path: prepared.absPath,
29846
30401
  action: prepared.existed ? "modified" : "created",
29847
30402
  before: prepared.existed ? prepared.prev : null,
29848
- after: input.content
30403
+ after: content
29849
30404
  });
29850
30405
  const syntax = await checkSyntax(
29851
30406
  prepared.absPath,
29852
- input.content,
30407
+ content,
29853
30408
  prepared.existed ? prepared.prev : void 0
29854
30409
  ).catch(() => void 0);
29855
30410
  const hasSyntaxErrors = syntax !== void 0 && syntax.errors.length > 0;
30411
+ const notes = [];
30412
+ if (diffTruncated) {
30413
+ notes.push("Diff truncated to the 256 KiB output budget \u2014 the full write is on disk.");
30414
+ }
30415
+ if (hasSyntaxErrors) {
30416
+ notes.push(
30417
+ syntax.preExisting ? "Syntax check: the file still has parse errors (they pre-date this write) \u2014 see syntax_errors." : `Syntax check: the written content has ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.`
30418
+ );
30419
+ }
29856
30420
  return {
29857
30421
  path: prepared.absPath,
29858
- bytes_written: Buffer.byteLength(input.content, "utf8"),
30422
+ bytes_written: Buffer.byteLength(content, "utf8"),
29859
30423
  created: !prepared.existed,
29860
30424
  diff,
29861
30425
  syntax_errors: hasSyntaxErrors ? syntax.errors : void 0,
29862
- note: hasSyntaxErrors ? syntax.preExisting ? "Syntax check: the file still has parse errors (they pre-date this write) \u2014 see syntax_errors." : `Syntax check: the written content has ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.` : void 0
30426
+ note: notes.length > 0 ? notes.join("\n") : void 0
29863
30427
  };
29864
30428
  }
29865
30429
 
@@ -29984,7 +30548,7 @@ init_circuit_breaker();
29984
30548
  init_languages();
29985
30549
 
29986
30550
  // src/memory.ts
29987
- import { ToolValidationError as ToolValidationError9 } from "@wrongstack/core/types";
30551
+ import { ToolValidationError as ToolValidationError12 } from "@wrongstack/core/types";
29988
30552
  function rememberTool(memory) {
29989
30553
  return {
29990
30554
  name: "remember",
@@ -30028,7 +30592,7 @@ function rememberTool(memory) {
30028
30592
  },
30029
30593
  async execute(input) {
30030
30594
  if (!input?.text) {
30031
- throw new ToolValidationError9({
30595
+ throw new ToolValidationError12({
30032
30596
  message: "remember: text is required",
30033
30597
  field: "text"
30034
30598
  });
@@ -30047,28 +30611,48 @@ function forgetTool(memory) {
30047
30611
  return {
30048
30612
  name: "forget",
30049
30613
  category: "Session",
30050
- description: "Remove memory entries that contain the given substring (case-insensitive). Use with caution.",
30051
- usageHint: "This permanently deletes matching memories in the chosen scope.\n- Provide a reasonably specific `query` to avoid deleting unrelated memories.\n- Always double-check before calling with broad queries.\n- Use `remember` + `forget` together to maintain clean long-term memory.",
30614
+ description: "Remove memory entries that contain the given substring (case-insensitive). Use with caution. Pass `dry_run: true` to preview the matching entries (capped at 20) without deleting anything.",
30615
+ usageHint: "This permanently deletes matching memories in the chosen scope.\n- Provide a reasonably specific `query` to avoid deleting unrelated memories.\n- Always double-check before calling with broad queries \u2014 `dry_run: true` previews the matches without deleting.\n- Use `remember` + `forget` together to maintain clean long-term memory.",
30052
30616
  permission: "confirm",
30617
+ // WS-046: gives permission decisions something to key on — the substring
30618
+ // being forgotten.
30619
+ subjectKey: "query",
30053
30620
  mutating: true,
30054
30621
  timeoutMs: 2e3,
30055
30622
  capabilities: ["memory.delete"],
30623
+ icon: "settings",
30056
30624
  inputSchema: {
30057
30625
  type: "object",
30058
30626
  properties: {
30059
30627
  query: { type: "string" },
30060
- scope: { type: "string", enum: ["project-agents", "project-memory", "user-memory"] }
30628
+ scope: { type: "string", enum: ["project-agents", "project-memory", "user-memory"] },
30629
+ dry_run: {
30630
+ type: "boolean",
30631
+ description: "When true, return the matched entries (capped at 20) WITHOUT deleting them. Default false."
30632
+ }
30061
30633
  },
30062
30634
  required: ["query"]
30063
30635
  },
30064
30636
  async execute(input) {
30065
30637
  if (!input?.query) {
30066
- throw new ToolValidationError9({
30638
+ throw new ToolValidationError12({
30067
30639
  message: "forget: query is required",
30068
30640
  field: "query"
30069
30641
  });
30070
30642
  }
30071
30643
  const scope = input.scope ?? "project-memory";
30644
+ if (input.dry_run) {
30645
+ const entries = await memory.list(scope);
30646
+ const needle = input.query.toLowerCase();
30647
+ const matching = entries.filter((entry) => entry.text.toLowerCase().includes(needle));
30648
+ return {
30649
+ removed: 0,
30650
+ scope,
30651
+ dryRun: true,
30652
+ matched: matching.length,
30653
+ matches: matching.slice(0, 20).map((entry) => entry.text)
30654
+ };
30655
+ }
30072
30656
  const removed = await memory.forget(input.query, scope);
30073
30657
  return { removed, scope };
30074
30658
  }
@@ -30105,7 +30689,7 @@ function searchMemoryTool(memory) {
30105
30689
  },
30106
30690
  async execute(input) {
30107
30691
  if (!input?.query) {
30108
- throw new ToolValidationError9({
30692
+ throw new ToolValidationError12({
30109
30693
  message: "search_memory: query is required",
30110
30694
  field: "query"
30111
30695
  });
@@ -30157,7 +30741,7 @@ function relatedMemoryTool(memory) {
30157
30741
  },
30158
30742
  async execute(input) {
30159
30743
  if (!input?.text) {
30160
- throw new ToolValidationError9({
30744
+ throw new ToolValidationError12({
30161
30745
  message: "find_related_memories: text is required",
30162
30746
  field: "text"
30163
30747
  });
@@ -30193,6 +30777,9 @@ function createModeTool(modeStore) {
30193
30777
  description: "Manage agent operating modes. Modes change the agent's behavior, personality, and system prompt for different workflows (e.g. coding, security review, planning).",
30194
30778
  usageHint: "POWERFUL BEHAVIOR CONTROL TOOL:\n\n- Use `list` to see available modes.\n- Use `set <modeId>` to switch the agent into a specific role/mode.\n- Use `get` to check current mode.\n- Use `clear` to return to default behavior.\nSwitching modes is very effective for specialized tasks. The mode change affects how the agent reasons and which guidelines it follows.",
30195
30779
  permission: "confirm",
30780
+ // WS-046: gives permission decisions something to key on — the mode being
30781
+ // activated. Permission semantics are unchanged.
30782
+ subjectKey: "mode",
30196
30783
  mutating: true,
30197
30784
  timeoutMs: 5e3,
30198
30785
  capabilities: ["session.mode"],
@@ -30931,14 +31518,14 @@ function createGlobalPsSlashCommand() {
30931
31518
 
30932
31519
  // src/skill.ts
30933
31520
  import * as fs35 from "node:fs/promises";
30934
- import * as path39 from "node:path";
31521
+ import * as path40 from "node:path";
30935
31522
  import {
30936
31523
  missingRequiredRuntimeTools,
30937
31524
  missingRuntimeCapabilities,
30938
31525
  runtimeToolReferencesFromText
30939
31526
  } from "@wrongstack/core/agent-catalog";
30940
31527
  import { SKILL_LIMITS, stripFrontmatter } from "@wrongstack/core/skills";
30941
- import { ToolValidationError as ToolValidationError10 } from "@wrongstack/core/types";
31528
+ import { ToolValidationError as ToolValidationError13 } from "@wrongstack/core/types";
30942
31529
  var MAX_BODY_CHARS = SKILL_LIMITS.MAX_SKILL_BODY_CHARS;
30943
31530
  var MAX_RESOURCE_CHARS = SKILL_LIMITS.MAX_RESOURCE_CHARS;
30944
31531
  var MAX_LISTED_RESOURCES = SKILL_LIMITS.MAX_LISTED_RESOURCES;
@@ -30971,11 +31558,11 @@ function makeSkillTool(skillLoader) {
30971
31558
  async execute(input, ctx) {
30972
31559
  const name = input?.name?.trim();
30973
31560
  if (!name) {
30974
- throw new ToolValidationError10({ message: "skill: name is required", field: "name" });
31561
+ throw new ToolValidationError13({ message: "skill: name is required", field: "name" });
30975
31562
  }
30976
31563
  const manifest = await skillLoader.find(name);
30977
31564
  if (!manifest) {
30978
- throw new ToolValidationError10({
31565
+ throw new ToolValidationError13({
30979
31566
  message: `skill "${name}" not found \u2014 use /skill to list available skills`,
30980
31567
  field: "name"
30981
31568
  });
@@ -30987,7 +31574,7 @@ function makeSkillTool(skillLoader) {
30987
31574
  );
30988
31575
  const missingTools = missingRequiredRuntimeTools(manifest.requiredTools, availableToolNames);
30989
31576
  if (missingCapabilities.length > 0 || missingTools.length > 0) {
30990
- throw new ToolValidationError10({
31577
+ throw new ToolValidationError13({
30991
31578
  message: `skill "${name}" is unavailable in this runtime; ` + [
30992
31579
  missingCapabilities.length > 0 ? `missing capabilities: ${missingCapabilities.join(", ")}` : "",
30993
31580
  missingTools.length > 0 ? `missing tools: ${missingTools.join(", ")}` : ""
@@ -30995,7 +31582,7 @@ function makeSkillTool(skillLoader) {
30995
31582
  field: "name"
30996
31583
  });
30997
31584
  }
30998
- const dir = path39.dirname(manifest.path);
31585
+ const dir = path40.dirname(manifest.path);
30999
31586
  let loadedResource;
31000
31587
  if (input.resource?.trim()) {
31001
31588
  loadedResource = await loadResource(dir, input.resource.trim());
@@ -31005,12 +31592,7 @@ function makeSkillTool(skillLoader) {
31005
31592
  runtimeToolReferencesFromText(raw),
31006
31593
  availableToolNames
31007
31594
  );
31008
- if (missingBodyTools.length > 0) {
31009
- throw new ToolValidationError10({
31010
- message: `skill "${name}" references unregistered tools: ${missingBodyTools.join(", ")}`,
31011
- field: "name"
31012
- });
31013
- }
31595
+ const warning = missingBodyTools.length > 0 ? `Warning: skill "${name}" references tools not registered in this runtime: ${missingBodyTools.join(", ")}. Steps that call them may be unavailable.` : void 0;
31014
31596
  const body = stripFrontmatter(raw).trim().slice(0, MAX_BODY_CHARS);
31015
31597
  const resources = loadedResource ? [] : await listResources(dir);
31016
31598
  try {
@@ -31027,44 +31609,48 @@ function makeSkillTool(skillLoader) {
31027
31609
  body,
31028
31610
  resources,
31029
31611
  dir,
31030
- loadedResource
31612
+ loadedResource,
31613
+ warning
31031
31614
  };
31032
31615
  },
31033
31616
  serialize(output) {
31617
+ const warningLine = output.warning ? `
31618
+
31619
+ ${output.warning}` : "";
31034
31620
  if (output.loadedResource) {
31035
31621
  const lr = output.loadedResource;
31036
31622
  const note = lr.truncated ? ` (truncated to ${lr.content.length} chars of ${lr.bytes} B)` : "";
31037
31623
  return `# Resource: ${output.name}/${lr.rel}
31038
31624
  (abs path: ${lr.absPath})${note}
31039
31625
 
31040
- ${lr.content}`;
31626
+ ${lr.content}${warningLine}`;
31041
31627
  }
31042
31628
  const head = `# Skill: ${output.name}
31043
31629
  ${output.description}
31044
31630
 
31045
31631
  ${output.body}`;
31046
- if (output.resources.length === 0) return head;
31632
+ if (output.resources.length === 0) return `${head}${warningLine}`;
31047
31633
  const listing = output.resources.map((r) => `- ${r.path} (${r.bytes} B)`).join("\n");
31048
31634
  return `${head}
31049
31635
 
31050
31636
  ## Bundled resources (load on demand)
31051
31637
  Load any with: \`skill({ name: "${output.name}", resource: "<path>" })\`. Run scripts via bash using their abs path under ${output.dir}.
31052
- ${listing}`;
31638
+ ${listing}${warningLine}`;
31053
31639
  }
31054
31640
  };
31055
31641
  }
31056
31642
  async function loadResource(skillDir, rel) {
31057
31643
  const norm = rel.replace(/\\/g, "/");
31058
- if (path39.isAbsolute(rel) || norm.split("/").some((seg) => seg === "..")) {
31059
- throw new ToolValidationError10({
31644
+ if (path40.isAbsolute(rel) || norm.split("/").some((seg) => seg === "..")) {
31645
+ throw new ToolValidationError13({
31060
31646
  message: `skill: invalid resource path "${rel}"`,
31061
31647
  field: "resource"
31062
31648
  });
31063
31649
  }
31064
- const absPath = path39.resolve(skillDir, rel);
31065
- const root = path39.resolve(skillDir);
31066
- if (absPath !== root && !absPath.startsWith(root + path39.sep)) {
31067
- throw new ToolValidationError10({
31650
+ const absPath = path40.resolve(skillDir, rel);
31651
+ const root = path40.resolve(skillDir);
31652
+ if (absPath !== root && !absPath.startsWith(root + path40.sep)) {
31653
+ throw new ToolValidationError13({
31068
31654
  message: `skill: resource "${rel}" escapes the skill directory`,
31069
31655
  field: "resource"
31070
31656
  });
@@ -31075,13 +31661,13 @@ async function loadResource(skillDir, rel) {
31075
31661
  realRoot = await fs35.realpath(root);
31076
31662
  realPath = await fs35.realpath(absPath);
31077
31663
  } catch {
31078
- throw new ToolValidationError10({
31664
+ throw new ToolValidationError13({
31079
31665
  message: `skill: resource "${rel}" not readable`,
31080
31666
  field: "resource"
31081
31667
  });
31082
31668
  }
31083
- if (realPath !== realRoot && !realPath.startsWith(realRoot + path39.sep)) {
31084
- throw new ToolValidationError10({
31669
+ if (realPath !== realRoot && !realPath.startsWith(realRoot + path40.sep)) {
31670
+ throw new ToolValidationError13({
31085
31671
  message: `skill: resource "${rel}" resolves outside the skill directory`,
31086
31672
  field: "resource"
31087
31673
  });
@@ -31090,7 +31676,7 @@ async function loadResource(skillDir, rel) {
31090
31676
  try {
31091
31677
  buf = await fs35.readFile(realPath);
31092
31678
  } catch {
31093
- throw new ToolValidationError10({
31679
+ throw new ToolValidationError13({
31094
31680
  message: `skill: resource "${rel}" not readable`,
31095
31681
  field: "resource"
31096
31682
  });
@@ -31123,7 +31709,7 @@ async function walk(root, dir, out) {
31123
31709
  }
31124
31710
  for (const e of entries) {
31125
31711
  if (out.length >= MAX_LISTED_RESOURCES) return;
31126
- const fullPath = path39.join(dir, e.name);
31712
+ const fullPath = path40.join(dir, e.name);
31127
31713
  let isDir = e.isDirectory();
31128
31714
  if (e.isSymbolicLink()) {
31129
31715
  try {
@@ -31138,9 +31724,9 @@ async function walk(root, dir, out) {
31138
31724
  } else if (e.isFile()) {
31139
31725
  if (e.name === "SKILL.md" || e.name === "SKILL.save.md") continue;
31140
31726
  try {
31141
- const stat19 = await fs35.stat(fullPath);
31142
- const rel = path39.relative(root, fullPath).split(path39.sep).join("/");
31143
- out.push({ path: rel, bytes: stat19.size });
31727
+ const stat20 = await fs35.stat(fullPath);
31728
+ const rel = path40.relative(root, fullPath).split(path40.sep).join("/");
31729
+ out.push({ path: rel, bytes: stat20.size });
31144
31730
  } catch {
31145
31731
  }
31146
31732
  }
@@ -31233,9 +31819,15 @@ var TOOL_ICON_MAP = {
31233
31819
  "codebase-index": "index",
31234
31820
  "codebase-search": "index",
31235
31821
  "codebase-stats": "index",
31822
+ "codebase-incoming-calls": "index",
31823
+ "codebase-outgoing-calls": "index",
31824
+ "dead-code-scan": "index",
31236
31825
  codebase_index: "index",
31237
31826
  codebase_search: "index",
31238
31827
  codebase_stats: "index",
31828
+ codebase_incoming_calls: "index",
31829
+ codebase_outgoing_calls: "index",
31830
+ dead_code_scan: "index",
31239
31831
  // Data
31240
31832
  json: "json",
31241
31833
  parse: "json",