@wrongstack/tools 0.305.1 → 0.306.2

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 (63) 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 +1294 -726
  9. package/dist/codebase-index/codebase-search-tool.d.ts +5 -0
  10. package/dist/codebase-index/index.js +223 -152
  11. package/dist/codebase-index/project-server.js +10 -11
  12. package/dist/diff.d.ts +5 -0
  13. package/dist/diff.js +78 -12
  14. package/dist/document.js +18 -6
  15. package/dist/edit.js +69 -16
  16. package/dist/exec.js +44 -22
  17. package/dist/fetch.js +13 -1
  18. package/dist/format.d.ts +4 -2
  19. package/dist/format.js +81 -31
  20. package/dist/glob.js +12 -4
  21. package/dist/grep.d.ts +2 -0
  22. package/dist/grep.js +15 -4
  23. package/dist/index.js +1359 -762
  24. package/dist/install.js +96 -37
  25. package/dist/kanban-tool-types.d.ts +6 -1
  26. package/dist/kanban.js +60 -0
  27. package/dist/languages/index.js +28 -13
  28. package/dist/lint.js +28 -13
  29. package/dist/logs.d.ts +0 -1
  30. package/dist/logs.js +44 -13
  31. package/dist/memory.d.ts +8 -0
  32. package/dist/memory.js +23 -3
  33. package/dist/mode.d.ts +1 -1
  34. package/dist/mode.js +3 -0
  35. package/dist/next-steps.d.ts +2 -3
  36. package/dist/next-steps.js +3 -3
  37. package/dist/outdated.d.ts +0 -3
  38. package/dist/outdated.js +89 -48
  39. package/dist/pack.js +1294 -726
  40. package/dist/plan.js +91 -3
  41. package/dist/process-registry.d.ts +8 -2
  42. package/dist/process-registry.js +28 -13
  43. package/dist/ps-slash.js +22 -12
  44. package/dist/read.js +10 -3
  45. package/dist/replace.d.ts +4 -0
  46. package/dist/replace.js +104 -7
  47. package/dist/search.d.ts +6 -0
  48. package/dist/search.js +47 -26
  49. package/dist/session-kanban.js +3 -1
  50. package/dist/skill.d.ts +6 -0
  51. package/dist/skill.js +9 -10
  52. package/dist/task.js +81 -2
  53. package/dist/test.js +28 -13
  54. package/dist/todo.js +79 -2
  55. package/dist/tool-icons.js +4 -2
  56. package/dist/tool-summary.d.ts +1 -1
  57. package/dist/tool-summary.js +76 -1
  58. package/dist/tool-tier.js +1294 -726
  59. package/dist/tree.js +9 -10
  60. package/dist/typecheck.d.ts +0 -2
  61. package/dist/typecheck.js +98 -31
  62. package/dist/write.js +58 -10
  63. package/package.json +4 -4
package/dist/pack.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
  });
@@ -518,11 +522,15 @@ var init_process_registry = __esm({
518
522
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
519
523
  }
520
524
  _canSignalProcessGroup(p) {
521
- return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
525
+ return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && p.child !== null && typeof p.child.pid === "number" && p.child.pid === p.pid;
522
526
  }
523
527
  _killChildDirect(p, signal) {
524
528
  try {
525
- p.child.kill(signal);
529
+ if (p.child) {
530
+ p.child.kill(signal);
531
+ return;
532
+ }
533
+ if (this._isSafeSignalPid(p.pid)) process.kill(p.pid, signal);
526
534
  } catch {
527
535
  }
528
536
  }
@@ -720,15 +728,15 @@ var init_process_registry = __esm({
720
728
  this._pruneStale(pid);
721
729
  const p = this.processes.get(pid);
722
730
  if (!p) return false;
723
- if (p.killed) return true;
731
+ if (p.killed && opts.force !== true) return true;
724
732
  if (p.protected && opts.includeProtected !== true) return false;
725
733
  if (opts.preserveBackground && p.background) return false;
726
734
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
727
735
  const isWin5 = os.platform() === "win32";
728
736
  if (isWin5) {
729
- const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
737
+ const liveRealChild = p.child === null || p.child.exitCode === null && typeof p.child.pid === "number";
730
738
  const directFallback = () => {
731
- if (p.child.exitCode === null) {
739
+ if (p.child && p.child.exitCode === null) {
732
740
  try {
733
741
  p.child.kill("SIGKILL");
734
742
  } catch {
@@ -740,10 +748,7 @@ var init_process_registry = __esm({
740
748
  onSettled: directFallback
741
749
  })) {
742
750
  } else {
743
- try {
744
- p.child.kill(force ? "SIGKILL" : "SIGTERM");
745
- } catch {
746
- }
751
+ this._killChildDirect(p, force ? "SIGKILL" : "SIGTERM");
747
752
  }
748
753
  p.killed = true;
749
754
  return true;
@@ -754,7 +759,7 @@ var init_process_registry = __esm({
754
759
  } else {
755
760
  this._killPosix(p, "SIGTERM");
756
761
  const timer = setTimeout(() => {
757
- if (this.processes.has(pid) && !p.child.killed) {
762
+ if (this.processes.has(pid) && !p.child?.killed) {
758
763
  this._killPosix(p, "SIGKILL");
759
764
  }
760
765
  }, graceMs);
@@ -809,6 +814,16 @@ var init_process_registry = __esm({
809
814
  * before reusing a PID, but we want to clean up before that becomes a risk.
810
815
  */
811
816
  _isStaleEntry(entry) {
817
+ if (entry.child === null) {
818
+ if (Date.now() - entry.startedAt <= 6e4) return false;
819
+ if (os.platform() === "win32") return false;
820
+ try {
821
+ process.kill(entry.pid, 0);
822
+ return false;
823
+ } catch (err) {
824
+ return err.code !== "EPERM";
825
+ }
826
+ }
812
827
  return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
813
828
  }
814
829
  /**
@@ -1010,8 +1025,8 @@ async function* spawnStream(opts) {
1010
1025
  try {
1011
1026
  for (; ; ) {
1012
1027
  while (queue.length === 0) {
1013
- await new Promise((resolve16) => {
1014
- waiter = resolve16;
1028
+ await new Promise((resolve17) => {
1029
+ waiter = resolve17;
1015
1030
  });
1016
1031
  }
1017
1032
  const chunk = queue.shift();
@@ -1099,19 +1114,48 @@ import * as Core from "@wrongstack/core/utils";
1099
1114
  function sha256hex(content) {
1100
1115
  return createHash("sha256").update(content, "utf8").digest("hex");
1101
1116
  }
1102
- async function detectPackageManager(cwd) {
1103
- const { stat: stat18 } = await import("node:fs/promises");
1104
- try {
1105
- await stat18(`${cwd}/pnpm-lock.yaml`);
1106
- return "pnpm";
1107
- } catch {
1117
+ async function detectPackageManager(cwd, stopAt) {
1118
+ let dir = path3.resolve(cwd);
1119
+ const stop = stopAt ? path3.resolve(stopAt) : dir;
1120
+ for (; ; ) {
1121
+ const found = await detectPackageManagerInDir(dir);
1122
+ if (found) return found;
1123
+ if (dir === stop) break;
1124
+ const parent = path3.dirname(dir);
1125
+ const relParent = path3.relative(stop, parent);
1126
+ if (parent === dir || relParent.startsWith("..") || path3.isAbsolute(relParent)) break;
1127
+ dir = parent;
1108
1128
  }
1129
+ return "npm";
1130
+ }
1131
+ async function detectPackageManagerInDir(dir) {
1132
+ const fs35 = await import("node:fs/promises");
1109
1133
  try {
1110
- await stat18(`${cwd}/yarn.lock`);
1111
- return "yarn";
1134
+ const raw = await fs35.readFile(path3.join(dir, "package.json"), "utf8");
1135
+ const declared = JSON.parse(raw).packageManager;
1136
+ if (typeof declared === "string") {
1137
+ const name = declared.split("@")[0] ?? "";
1138
+ if (name === "pnpm" || name === "yarn") return name;
1139
+ if (name === "npm" || name === "bun") return "npm";
1140
+ }
1112
1141
  } catch {
1113
1142
  }
1114
- return "npm";
1143
+ const lockfiles = [
1144
+ ["pnpm-lock.yaml", "pnpm"],
1145
+ ["yarn.lock", "yarn"],
1146
+ ["bun.lockb", "npm"],
1147
+ ["bun.lock", "npm"],
1148
+ ["package-lock.json", "npm"],
1149
+ ["npm-shrinkwrap.json", "npm"]
1150
+ ];
1151
+ for (const [file, manager] of lockfiles) {
1152
+ try {
1153
+ await fs35.stat(`${dir}/${file}`);
1154
+ return manager;
1155
+ } catch {
1156
+ }
1157
+ }
1158
+ return null;
1115
1159
  }
1116
1160
  function resolvePath(input, ctx) {
1117
1161
  return path3.isAbsolute(input) ? path3.normalize(input) : path3.resolve(ctx.workingDir ?? ctx.cwd, input);
@@ -1170,6 +1214,20 @@ async function safeResolveReal(input, ctx) {
1170
1214
  const abs = safeResolve(input, ctx);
1171
1215
  return await resolveRealInsideRoot(abs, ctx);
1172
1216
  }
1217
+ function truncateDiffPayload(diff, maxBytes) {
1218
+ const total = Buffer.byteLength(diff, "utf8");
1219
+ if (total <= maxBytes) return { text: diff, truncated: false };
1220
+ const MARKER_RESERVE = 96;
1221
+ let head = takeHeadBytes(diff, Math.max(0, maxBytes - MARKER_RESERVE));
1222
+ const nl = head.lastIndexOf("\n");
1223
+ if (nl > 0) head = head.slice(0, nl);
1224
+ const kept = Buffer.byteLength(head, "utf8");
1225
+ return {
1226
+ text: `${head}
1227
+ \u2026[diff truncated: ${total - kept} of ${total} bytes omitted]`,
1228
+ truncated: true
1229
+ };
1230
+ }
1173
1231
  function truncateMiddle(s, max) {
1174
1232
  if (Buffer.byteLength(s, "utf8") <= max) return s;
1175
1233
  const half = Math.floor(max / 2);
@@ -2732,8 +2790,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, extraIgn
2732
2790
  collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2733
2791
  }
2734
2792
  }
2735
- function collectFileEvidence(directory, fullPath, basename13, profiles, state) {
2736
- const lower = basename13.toLowerCase();
2793
+ function collectFileEvidence(directory, fullPath, basename14, profiles, state) {
2794
+ const lower = basename14.toLowerCase();
2737
2795
  const extension = path4.extname(lower);
2738
2796
  for (const profile of profiles) {
2739
2797
  const detector = profile.detectors.find(
@@ -2744,7 +2802,7 @@ function collectFileEvidence(directory, fullPath, basename13, profiles, state) {
2744
2802
  candidate.evidence.push({
2745
2803
  kind: detector.kind,
2746
2804
  path: fullPath,
2747
- value: basename13,
2805
+ value: basename14,
2748
2806
  weight: detector.weight
2749
2807
  });
2750
2808
  if (detector.kind === "manifest" || detector.kind === "config") {
@@ -2872,8 +2930,8 @@ function normalizeLimits(input) {
2872
2930
  async function canonicalDirectory(input) {
2873
2931
  const resolved = path4.resolve(input);
2874
2932
  const real = await fs2.realpath(resolved);
2875
- const stat18 = await fs2.stat(real);
2876
- if (!stat18.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
2933
+ const stat19 = await fs2.stat(real);
2934
+ if (!stat19.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
2877
2935
  return real;
2878
2936
  }
2879
2937
  async function canonicalInside(input, root, label) {
@@ -3772,8 +3830,8 @@ async function executeInternal(options, startedAt) {
3772
3830
  const target = options.plan.evidence.find((item) => item.kind === "target")?.path;
3773
3831
  if (!target) return unavailableResult(options, "Internal syntax plan has no target evidence.");
3774
3832
  const safeTarget = await assertContainedFile(target, options.projectRoot);
3775
- const stat18 = await fs4.stat(safeTarget);
3776
- if (stat18.size > MAX_INTERNAL_SOURCE_BYTES) {
3833
+ const stat19 = await fs4.stat(safeTarget);
3834
+ if (stat19.size > MAX_INTERNAL_SOURCE_BYTES) {
3777
3835
  return unavailableResult(
3778
3836
  options,
3779
3837
  `Internal syntax target exceeds ${MAX_INTERNAL_SOURCE_BYTES} bytes.`
@@ -4060,8 +4118,8 @@ async function snapshotPaths(paths) {
4060
4118
  const existing = [];
4061
4119
  for (const candidate of paths) {
4062
4120
  try {
4063
- const stat18 = await fs4.stat(candidate);
4064
- if (!stat18.isFile()) continue;
4121
+ const stat19 = await fs4.stat(candidate);
4122
+ if (!stat19.isFile()) continue;
4065
4123
  existing.push(candidate);
4066
4124
  } catch {
4067
4125
  }
@@ -4072,15 +4130,15 @@ async function changedPaths(before, after, beforeSizes, afterSizes) {
4072
4130
  const beforeSet = new Set(before);
4073
4131
  const afterSet = new Set(after);
4074
4132
  const changed = /* @__PURE__ */ new Set();
4075
- for (const path39 of after) {
4076
- if (!beforeSet.has(path39)) changed.add(path39);
4133
+ for (const path40 of after) {
4134
+ if (!beforeSet.has(path40)) changed.add(path40);
4077
4135
  }
4078
- for (const path39 of before) {
4079
- if (!afterSet.has(path39)) changed.add(path39);
4136
+ for (const path40 of before) {
4137
+ if (!afterSet.has(path40)) changed.add(path40);
4080
4138
  }
4081
4139
  if (beforeSizes && afterSizes) {
4082
- for (const path39 of after) {
4083
- if (beforeSizes.get(path39) !== afterSizes.get(path39)) changed.add(path39);
4140
+ for (const path40 of after) {
4141
+ if (beforeSizes.get(path40) !== afterSizes.get(path40)) changed.add(path40);
4084
4142
  }
4085
4143
  }
4086
4144
  return [...changed].sort();
@@ -4089,8 +4147,8 @@ async function snapshotSizes(paths) {
4089
4147
  const sizes = /* @__PURE__ */ new Map();
4090
4148
  for (const candidate of paths) {
4091
4149
  try {
4092
- const stat18 = await fs4.stat(candidate);
4093
- if (stat18.isFile()) sizes.set(candidate, stat18.size);
4150
+ const stat19 = await fs4.stat(candidate);
4151
+ if (stat19.isFile()) sizes.set(candidate, stat19.size);
4094
4152
  } catch {
4095
4153
  }
4096
4154
  }
@@ -5545,7 +5603,7 @@ async function syncGoParse(filePath, content, lang) {
5545
5603
  }
5546
5604
  const goBinary = resolveWin32Command("go");
5547
5605
  const goResult = await new Promise(
5548
- (resolve16, reject) => {
5606
+ (resolve17, reject) => {
5549
5607
  let settled = false;
5550
5608
  const proc = spawn5(goBinary, ["run", scriptPath], {
5551
5609
  stdio: ["pipe", "pipe", "pipe"],
@@ -5574,7 +5632,7 @@ async function syncGoParse(filePath, content, lang) {
5574
5632
  if (settled) return;
5575
5633
  settled = true;
5576
5634
  clearTimeout(timer);
5577
- resolve16({ code: code2, stdout: stdout2 });
5635
+ resolve17({ code: code2, stdout: stdout2 });
5578
5636
  });
5579
5637
  }
5580
5638
  );
@@ -6239,7 +6297,7 @@ async function resolvePython() {
6239
6297
  return null;
6240
6298
  }
6241
6299
  function commandIsAvailable(command) {
6242
- return new Promise((resolve16) => {
6300
+ return new Promise((resolve17) => {
6243
6301
  let settled = false;
6244
6302
  const proc = spawn6(command, ["--version"], {
6245
6303
  stdio: "ignore",
@@ -6249,7 +6307,7 @@ function commandIsAvailable(command) {
6249
6307
  if (settled) return;
6250
6308
  settled = true;
6251
6309
  clearTimeout(timer);
6252
- resolve16(available);
6310
+ resolve17(available);
6253
6311
  };
6254
6312
  const timer = setTimeout(() => {
6255
6313
  proc.kill("SIGKILL");
@@ -6261,7 +6319,7 @@ function commandIsAvailable(command) {
6261
6319
  });
6262
6320
  }
6263
6321
  function spawnPyParser(pyBinary, scriptPath, filePath, content) {
6264
- return new Promise((resolve16, reject) => {
6322
+ return new Promise((resolve17, reject) => {
6265
6323
  let settled = false;
6266
6324
  const proc = spawn6(pyBinary, [scriptPath, filePath], {
6267
6325
  stdio: ["pipe", "pipe", "pipe"],
@@ -6290,7 +6348,7 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
6290
6348
  if (settled) return;
6291
6349
  settled = true;
6292
6350
  clearTimeout(timer);
6293
- resolve16({ code, stdout });
6351
+ resolve17({ code, stdout });
6294
6352
  });
6295
6353
  });
6296
6354
  }
@@ -6699,9 +6757,9 @@ function parseSymbols6(opts) {
6699
6757
  function regexParse2(opts) {
6700
6758
  const { file, content, lang } = opts;
6701
6759
  const symbols = [];
6702
- const basename13 = path22.basename(file).toLowerCase();
6703
- const isPackageJson = basename13 === "package.json";
6704
- const isTsconfig = basename13 === "tsconfig.json" || basename13 === "tsconfig.build.json";
6760
+ const basename14 = path22.basename(file).toLowerCase();
6761
+ const isPackageJson = basename14 === "package.json";
6762
+ const isTsconfig = basename14 === "tsconfig.json" || basename14 === "tsconfig.build.json";
6705
6763
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
6706
6764
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
6707
6765
  const lines = content.split("\n");
@@ -7631,11 +7689,18 @@ var init_tree_sitter_parser = __esm({
7631
7689
  init_spawn_stream();
7632
7690
  init_util();
7633
7691
  init_legacy_bridge();
7692
+ var SEVERITY_RANK = {
7693
+ info: 0,
7694
+ low: 1,
7695
+ moderate: 2,
7696
+ high: 3,
7697
+ critical: 4
7698
+ };
7634
7699
  var auditTool = {
7635
7700
  name: "audit",
7636
7701
  category: "Package Management",
7637
7702
  description: "Run a security audit against project dependencies (using pnpm/npm audit). Reports known vulnerabilities with severity.",
7638
- 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.",
7703
+ 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.",
7639
7704
  permission: "confirm",
7640
7705
  mutating: false,
7641
7706
  capabilities: ["shell.restricted"],
@@ -7650,8 +7715,10 @@ var auditTool = {
7650
7715
  enum: ["low", "moderate", "high", "critical"],
7651
7716
  description: "Minimum severity level to report"
7652
7717
  },
7653
- fix: { type: "boolean", description: "Attempt to fix vulnerabilities (default: false)" },
7654
- packages: { type: "string", description: "Specific package(s) to audit (comma-separated)" }
7718
+ fix: {
7719
+ type: "boolean",
7720
+ description: "Deprecated and rejected \u2014 this tool is read-only and never modifies dependencies. Use `install` (or `language_package`) to remediate vulnerabilities."
7721
+ }
7655
7722
  }
7656
7723
  },
7657
7724
  async execute(input, ctx, opts) {
@@ -7665,6 +7732,11 @@ var auditTool = {
7665
7732
  return final;
7666
7733
  },
7667
7734
  async *executeStream(input, ctx, opts) {
7735
+ if (input.fix === true) {
7736
+ throw new Error(
7737
+ "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)."
7738
+ );
7739
+ }
7668
7740
  const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
7669
7741
  const bridge = await tryLegacyPackageOperation("package-audit", {
7670
7742
  cwd,
@@ -7694,13 +7766,11 @@ var auditTool = {
7694
7766
  };
7695
7767
  return;
7696
7768
  }
7697
- const manager = await detectPackageManager(cwd);
7769
+ const manager = await detectPackageManager(cwd, ctx.projectRoot);
7698
7770
  yield { type: "log", text: `Auditing with ${manager}\u2026`, data: { manager } };
7699
7771
  const args = ["audit", "--json"];
7700
- if (input.fix) args.push("--fix");
7701
- if (input.packages) {
7702
- const pkgs = Array.isArray(input.packages) ? input.packages : input.packages.split(",");
7703
- args.push(...pkgs.map((p) => p.trim()));
7772
+ if (input.level && (manager === "npm" || manager === "pnpm")) {
7773
+ args.push(`--audit-level=${input.level}`);
7704
7774
  }
7705
7775
  const result = yield* spawnStream({
7706
7776
  cmd: manager,
@@ -7709,10 +7779,16 @@ var auditTool = {
7709
7779
  signal: opts.signal,
7710
7780
  maxBytes: 1e5
7711
7781
  });
7712
- yield { type: "final", output: parseAuditOutput(result.stdout, result.exitCode) };
7782
+ yield {
7783
+ type: "final",
7784
+ output: parseAuditOutput(result.stdout, result.exitCode, {
7785
+ level: input.level,
7786
+ spawnTruncated: result.truncated
7787
+ })
7788
+ };
7713
7789
  }
7714
7790
  };
7715
- function parseAuditOutput(json2, exitCode) {
7791
+ function parseAuditOutput(json2, exitCode, opts = {}) {
7716
7792
  if (!json2) {
7717
7793
  return {
7718
7794
  exit_code: exitCode,
@@ -7723,18 +7799,14 @@ function parseAuditOutput(json2, exitCode) {
7723
7799
  truncated: false
7724
7800
  };
7725
7801
  }
7802
+ const cappedOutput = normalizeCommandOutput(json2);
7803
+ const truncated = opts.spawnTruncated === true || Buffer.byteLength(json2, "utf8") > COMMAND_OUTPUT_MAX_BYTES;
7726
7804
  try {
7727
7805
  const data = JSON.parse(json2);
7728
- const advisories = [];
7729
- const ads = data.advisories ?? {};
7730
- for (const id of Object.keys(ads)) {
7731
- const adv = ads[id];
7732
- advisories.push({
7733
- severity: adv.severity ?? "unknown",
7734
- package: adv.module_name ?? id,
7735
- title: adv.title ?? "Unknown vulnerability",
7736
- url: adv.url ?? ""
7737
- });
7806
+ let advisories = extractAdvisories(data);
7807
+ const minRank = opts.level ? SEVERITY_RANK[opts.level] ?? 0 : 0;
7808
+ if (minRank > 0) {
7809
+ advisories = advisories.filter((a) => (SEVERITY_RANK[a.severity] ?? 0) >= minRank);
7738
7810
  }
7739
7811
  const total = advisories.length;
7740
7812
  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`;
@@ -7743,8 +7815,8 @@ function parseAuditOutput(json2, exitCode) {
7743
7815
  vulnerabilities: advisories,
7744
7816
  total,
7745
7817
  summary,
7746
- output: json2,
7747
- truncated: json2.length > 1e5
7818
+ output: cappedOutput,
7819
+ truncated
7748
7820
  };
7749
7821
  } catch {
7750
7822
  return {
@@ -7752,15 +7824,47 @@ function parseAuditOutput(json2, exitCode) {
7752
7824
  vulnerabilities: [],
7753
7825
  total: 0,
7754
7826
  summary: "Could not parse audit output",
7755
- output: json2,
7756
- truncated: false
7827
+ output: cappedOutput,
7828
+ truncated
7757
7829
  };
7758
7830
  }
7759
7831
  }
7832
+ function extractAdvisories(data) {
7833
+ const advisories = [];
7834
+ const ads = data["advisories"];
7835
+ if (ads && typeof ads === "object") {
7836
+ for (const [id, value] of Object.entries(ads)) {
7837
+ const adv = value ?? {};
7838
+ advisories.push({
7839
+ severity: typeof adv["severity"] === "string" ? adv["severity"] : "unknown",
7840
+ package: typeof adv["module_name"] === "string" ? adv["module_name"] : id,
7841
+ title: typeof adv["title"] === "string" ? adv["title"] : "Unknown vulnerability",
7842
+ url: typeof adv["url"] === "string" ? adv["url"] : ""
7843
+ });
7844
+ }
7845
+ return advisories;
7846
+ }
7847
+ const vulns = data["vulnerabilities"];
7848
+ if (vulns && typeof vulns === "object") {
7849
+ for (const [pkg, value] of Object.entries(vulns)) {
7850
+ const vuln = value ?? {};
7851
+ const via = Array.isArray(vuln["via"]) ? vuln["via"] : [];
7852
+ const detail = via.find((v) => !!v && typeof v === "object");
7853
+ advisories.push({
7854
+ severity: typeof vuln["severity"] === "string" ? vuln["severity"] : "unknown",
7855
+ package: pkg,
7856
+ title: detail && typeof detail["title"] === "string" ? detail["title"] : "Unknown vulnerability",
7857
+ url: detail && typeof detail["url"] === "string" ? detail["url"] : ""
7858
+ });
7859
+ }
7860
+ }
7861
+ return advisories;
7862
+ }
7760
7863
 
7761
7864
  // src/bash.ts
7762
7865
  import { spawn as spawn3 } from "node:child_process";
7763
7866
  import * as os4 from "node:os";
7867
+ import { StringDecoder } from "node:string_decoder";
7764
7868
  import {
7765
7869
  emitProcessCompleted as emitProcessCompleted2,
7766
7870
  emitProcessOutput as emitProcessOutput2,
@@ -8041,7 +8145,6 @@ var PersistentProcessRegistry = class {
8041
8145
  try {
8042
8146
  const data = await readRegistryFile(this.registryPath);
8043
8147
  data.instances.set(String(entry.pid), entry);
8044
- const child = null;
8045
8148
  this.baseRegistry.register({
8046
8149
  pid: entry.pid,
8047
8150
  name: entry.name,
@@ -8049,7 +8152,7 @@ var PersistentProcessRegistry = class {
8049
8152
  startedAt: entry.startedAt,
8050
8153
  sessionId: entry.sessionId,
8051
8154
  protected: entry.protected,
8052
- child
8155
+ child: null
8053
8156
  });
8054
8157
  await writeRegistryFile(this.registryPath, data);
8055
8158
  } finally {
@@ -8609,7 +8712,7 @@ function looksLikePowerShell(command) {
8609
8712
  return true;
8610
8713
  }
8611
8714
  if (PS_VERB_RE.test(trimmed)) return true;
8612
- if (/(?:^|[\s;&|])(gci|gi|gp|gcm|gps|sl|rm|cat|cp|mv)\b/i.test(trimmed)) {
8715
+ if (/(?:^|[\s;&|])(gci|gi|gp|gcm|gps)\b/i.test(trimmed)) {
8613
8716
  return true;
8614
8717
  }
8615
8718
  if (looksLikePowerShellExtended(command)) return true;
@@ -8701,7 +8804,7 @@ var bashTool = {
8701
8804
  name: "bash",
8702
8805
  category: "Shell",
8703
8806
  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.",
8704
- 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.",
8807
+ 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.",
8705
8808
  selection: {
8706
8809
  doNotUseWhen: "the command is allowlisted and does not require pipes, redirection, or shell expansion.",
8707
8810
  useInstead: ["exec"]
@@ -8715,7 +8818,14 @@ var bashTool = {
8715
8818
  // explicitly removes the implicit cross-tool aliasing.
8716
8819
  subjectKey: "command",
8717
8820
  capabilities: ["shell.arbitrary"],
8718
- timeoutMs: 3e5,
8821
+ // Executor-level abort ceiling. Must sit ABOVE the per-call `timeout_ms`
8822
+ // ceiling (600_000): the tool's own timer tree-kills and returns a
8823
+ // structured `timed_out: true` result, while the executor's
8824
+ // AbortSignal.timeout is a blunt abort. The old value (300_000) meant any
8825
+ // timeout_ms > 5min was silently cut short by the executor. The 10s margin
8826
+ // covers the kill/teardown window. (The executor additionally clamps to
8827
+ // config `tools.maxToolTimeoutMs`.)
8828
+ timeoutMs: 61e4,
8719
8829
  maxOutputBytes: MAX_OUTPUT,
8720
8830
  estimatedDurationMs: 3e4,
8721
8831
  inputSchema: {
@@ -8727,7 +8837,7 @@ var bashTool = {
8727
8837
  },
8728
8838
  timeout_ms: {
8729
8839
  type: "integer",
8730
- description: "Optional timeout for this specific command in milliseconds."
8840
+ description: "Optional timeout for this specific command in milliseconds (default 300000, max 600000)."
8731
8841
  },
8732
8842
  background: {
8733
8843
  type: "boolean",
@@ -8778,16 +8888,7 @@ var bashTool = {
8778
8888
  return;
8779
8889
  }
8780
8890
  const PIPE_TO_SHELL_PATTERN = /\|\s*(sh|bash|ksh|zsh|fish|cmd|powershell|pwsh)/i;
8781
- if (PIPE_TO_SHELL_PATTERN.test(input.command)) {
8782
- console.warn(JSON.stringify({
8783
- level: "warn",
8784
- event: "bash.pipe_to_shell_detected",
8785
- message: "Detected pipe-to-shell pattern. Consider reviewing the full command before confirming.",
8786
- command_prefix: input.command.slice(0, 100),
8787
- // Log first 100 chars for review
8788
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
8789
- }));
8790
- }
8891
+ 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." : "";
8791
8892
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS2, 6e5));
8792
8893
  const isWin5 = os4.platform() === "win32";
8793
8894
  let plan;
@@ -8821,11 +8922,12 @@ var bashTool = {
8821
8922
  const shell = plan.bin;
8822
8923
  const args = plan.useStdin ? [...plan.argv] : [...plan.argv, input.command];
8823
8924
  const env = buildChildEnv2(ctx.session?.id);
8925
+ const spawnCwd = ctx.workingDir ?? ctx.projectRoot;
8824
8926
  const detached = !isWin5;
8825
8927
  const startedAt = Date.now();
8826
8928
  if (input.background) {
8827
8929
  const child2 = spawn3(shell, args, {
8828
- cwd: ctx.projectRoot,
8930
+ cwd: spawnCwd,
8829
8931
  env,
8830
8932
  // PowerShell takes the script on stdin (no argv quoting); cmd.exe
8831
8933
  // and POSIX shells ignore stdin when given the command inline.
@@ -8856,7 +8958,7 @@ var bashTool = {
8856
8958
  parentPid: process.pid,
8857
8959
  command: redactCommand(`${shell} ${args.join(" ")}`),
8858
8960
  args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
8859
- cwd: ctx.projectRoot,
8961
+ cwd: spawnCwd,
8860
8962
  background: true,
8861
8963
  startedAt: new Date(startedAt).toISOString()
8862
8964
  });
@@ -8900,7 +9002,9 @@ var bashTool = {
8900
9002
  yield {
8901
9003
  type: "final",
8902
9004
  output: {
8903
- output: "",
9005
+ // Background runs have no captured output; the pipe-to-shell caution
9006
+ // (when present) is the only thing worth surfacing.
9007
+ output: pipeToShellNote.trim(),
8904
9008
  exit_code: null,
8905
9009
  timed_out: false,
8906
9010
  pid: pid2
@@ -8917,7 +9021,7 @@ var bashTool = {
8917
9021
  return;
8918
9022
  }
8919
9023
  const child = spawn3(shell, args, {
8920
- cwd: ctx.projectRoot,
9024
+ cwd: spawnCwd,
8921
9025
  env,
8922
9026
  // PowerShell takes the script on stdin (no argv quoting); cmd.exe
8923
9027
  // and POSIX shells ignore stdin when given the command inline.
@@ -8942,7 +9046,7 @@ var bashTool = {
8942
9046
  parentPid: process.pid,
8943
9047
  command: redactCommand(`${shell} ${args.join(" ")}`),
8944
9048
  args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
8945
- cwd: ctx.projectRoot,
9049
+ cwd: spawnCwd,
8946
9050
  background: false,
8947
9051
  startedAt: new Date(startedAt).toISOString()
8948
9052
  });
@@ -9033,10 +9137,10 @@ var bashTool = {
9033
9137
  queue.push(c);
9034
9138
  }
9035
9139
  };
9036
- const next = () => new Promise((resolve16) => {
9140
+ const next = () => new Promise((resolve17) => {
9037
9141
  const c = queue.shift();
9038
- if (c) resolve16(c);
9039
- else resolveNext = resolve16;
9142
+ if (c) resolve17(c);
9143
+ else resolveNext = resolve17;
9040
9144
  });
9041
9145
  let lastFlush = Date.now();
9042
9146
  const flush = () => {
@@ -9061,8 +9165,10 @@ var bashTool = {
9061
9165
  child.stderr?.resume();
9062
9166
  }
9063
9167
  };
9168
+ const stdoutDecoder = new StringDecoder("utf8");
9169
+ const stderrDecoder = new StringDecoder("utf8");
9064
9170
  const onData = (chunk, stream) => {
9065
- const text = chunk.toString();
9171
+ const text = (stream === "stdout" ? stdoutDecoder : stderrDecoder).write(chunk);
9066
9172
  if (stream === "stdout") stdoutBytes += chunk.byteLength;
9067
9173
  else stderrBytes += chunk.byteLength;
9068
9174
  emitProcessOutput2({ pid, stream, chunk });
@@ -9089,6 +9195,12 @@ var bashTool = {
9089
9195
  if (typeof pid === "number") registry.unregister(pid);
9090
9196
  registry.afterCall(Date.now() - startedAt, code !== 0 && code !== null);
9091
9197
  completeForeground(timedOut ? 124 : code ?? (signal ? 1 : 0), signal ?? void 0);
9198
+ const tail = stdoutDecoder.end() + stderrDecoder.end();
9199
+ if (tail) {
9200
+ if (buf.length < MAX_OUTPUT) buf += tail.slice(0, MAX_OUTPUT - buf.length);
9201
+ spool.write(tail);
9202
+ pending2 += tail;
9203
+ }
9092
9204
  push({ kind: "end", code });
9093
9205
  });
9094
9206
  try {
@@ -9108,7 +9220,7 @@ var bashTool = {
9108
9220
  output: {
9109
9221
  output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : "") + (hint ? `
9110
9222
 
9111
- ${hint}` : ""),
9223
+ ${hint}` : "") + pipeToShellNote,
9112
9224
  exit_code: c.code,
9113
9225
  timed_out: timedOut
9114
9226
  }
@@ -9170,7 +9282,7 @@ ${hint}` : ""),
9170
9282
  if (!sessionId) return;
9171
9283
  for (const entry of registry.bySession(sessionId)) {
9172
9284
  if (entry.name !== "bash") continue;
9173
- if (entry.child.exitCode !== null) continue;
9285
+ if (entry.child && entry.child.exitCode !== null) continue;
9174
9286
  if (entry.background) continue;
9175
9287
  if (entry.protected) continue;
9176
9288
  registry.kill(entry.pid, { force: true });
@@ -9415,8 +9527,8 @@ function sweepOldArtifacts(root) {
9415
9527
  for (const name of names) {
9416
9528
  const target = path10.join(dir, name);
9417
9529
  try {
9418
- const stat18 = await fs7.stat(target);
9419
- if (stat18.isFile() && stat18.mtimeMs < cutoff) {
9530
+ const stat19 = await fs7.stat(target);
9531
+ if (stat19.isFile() && stat19.mtimeMs < cutoff) {
9420
9532
  await fs7.rm(target, { force: true });
9421
9533
  removed++;
9422
9534
  }
@@ -9444,14 +9556,14 @@ var BrowserArtifactStore = class {
9444
9556
  }
9445
9557
  async record(id, sessionId, kind, target, mimeType) {
9446
9558
  await fs7.chmod(target, 384).catch(() => void 0);
9447
- const [stat18, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
9559
+ const [stat19, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
9448
9560
  const artifact = {
9449
9561
  id,
9450
9562
  kind,
9451
9563
  sensitivity: "sensitive",
9452
9564
  path: target,
9453
9565
  mimeType,
9454
- sizeBytes: stat18.size,
9566
+ sizeBytes: stat19.size,
9455
9567
  sha256,
9456
9568
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
9457
9569
  };
@@ -9491,11 +9603,11 @@ var BrowserArtifactStore = class {
9491
9603
  };
9492
9604
  async function hashFile(target) {
9493
9605
  const hash = createHash3("sha256");
9494
- await new Promise((resolve16, reject) => {
9606
+ await new Promise((resolve17, reject) => {
9495
9607
  const stream = createReadStream(target);
9496
9608
  stream.on("data", (chunk) => hash.update(chunk));
9497
9609
  stream.once("error", reject);
9498
- stream.once("end", resolve16);
9610
+ stream.once("end", resolve17);
9499
9611
  });
9500
9612
  return hash.digest("hex");
9501
9613
  }
@@ -9645,7 +9757,7 @@ var BrowserNetworkGuardProxy = class {
9645
9757
  async start() {
9646
9758
  if (this.url) return this.url;
9647
9759
  if (this.startPromise) return this.startPromise;
9648
- this.startPromise = new Promise((resolve16, reject) => {
9760
+ this.startPromise = new Promise((resolve17, reject) => {
9649
9761
  const onError = (error) => {
9650
9762
  this.server.off("listening", onListening);
9651
9763
  reject(error);
@@ -9658,7 +9770,7 @@ var BrowserNetworkGuardProxy = class {
9658
9770
  return;
9659
9771
  }
9660
9772
  this.url = `http://127.0.0.1:${address.port}`;
9661
- resolve16(this.url);
9773
+ resolve17(this.url);
9662
9774
  };
9663
9775
  this.server.once("error", onError);
9664
9776
  this.server.once("listening", onListening);
@@ -9673,7 +9785,7 @@ var BrowserNetworkGuardProxy = class {
9673
9785
  for (const socket of this.sockets) socket.destroy();
9674
9786
  this.sockets.clear();
9675
9787
  if (!this.server.listening) return;
9676
- await new Promise((resolve16) => this.server.close(() => resolve16()));
9788
+ await new Promise((resolve17) => this.server.close(() => resolve17()));
9677
9789
  }
9678
9790
  async forwardHttp(request2, response) {
9679
9791
  try {
@@ -9703,8 +9815,8 @@ var BrowserNetworkGuardProxy = class {
9703
9815
  upstream.on("error", () => writeProxyError(response, 502, "Bad Gateway"));
9704
9816
  request2.on("aborted", () => upstream.destroy());
9705
9817
  request2.pipe(upstream);
9706
- } catch {
9707
- writeProxyError(response, 403, "Blocked by browser network policy");
9818
+ } catch (error) {
9819
+ writeProxyError(response, 403, policyBlockMessage(error));
9708
9820
  }
9709
9821
  }
9710
9822
  async forwardConnect(request2, client, head) {
@@ -9728,8 +9840,8 @@ var BrowserNetworkGuardProxy = class {
9728
9840
  pipeDuplexPair(upstream, client);
9729
9841
  });
9730
9842
  client.once("close", () => upstream.destroy());
9731
- } catch {
9732
- client.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
9843
+ } catch (error) {
9844
+ client.end(rawForbiddenResponse(policyBlockMessage(error)));
9733
9845
  }
9734
9846
  }
9735
9847
  async forwardUpgrade(request2, client, head) {
@@ -9770,9 +9882,9 @@ var BrowserNetworkGuardProxy = class {
9770
9882
  });
9771
9883
  client.once("close", () => upstream?.destroy());
9772
9884
  upstream.end();
9773
- } catch {
9885
+ } catch (error) {
9774
9886
  upstream?.destroy();
9775
- client.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
9887
+ client.end(rawForbiddenResponse(policyBlockMessage(error)));
9776
9888
  }
9777
9889
  }
9778
9890
  resolve(rawUrl) {
@@ -9812,6 +9924,18 @@ function writeProxyError(response, status, message) {
9812
9924
  response.writeHead(status, { "content-type": "text/plain", connection: "close" });
9813
9925
  response.end(message);
9814
9926
  }
9927
+ function policyBlockMessage(error) {
9928
+ const reason = error instanceof Error ? error.message : String(error);
9929
+ return reason ? `Blocked by browser network policy: ${reason}` : "Blocked by browser network policy";
9930
+ }
9931
+ function rawForbiddenResponse(message) {
9932
+ return `HTTP/1.1 403 Forbidden\r
9933
+ Content-Type: text/plain\r
9934
+ Content-Length: ${Buffer.byteLength(message)}\r
9935
+ Connection: close\r
9936
+ \r
9937
+ ` + message;
9938
+ }
9815
9939
 
9816
9940
  // src/browser/manager.ts
9817
9941
  var DEFAULT_OPERATION_TIMEOUT_MS = 3e4;
@@ -10039,13 +10163,21 @@ var BrowserSessionManager = class {
10039
10163
  if (relative12.startsWith("..") || path11.isAbsolute(relative12)) {
10040
10164
  throw new Error("browser: upload files must stay inside the project root");
10041
10165
  }
10042
- const realFile = await fs8.realpath(absolute);
10166
+ let realFile;
10167
+ try {
10168
+ realFile = await fs8.realpath(absolute);
10169
+ } catch (error) {
10170
+ if (error?.code === "ENOENT") {
10171
+ throw new Error(`browser: upload file not found: ${file}`);
10172
+ }
10173
+ throw error;
10174
+ }
10043
10175
  const realRelative = path11.relative(realRoot, realFile);
10044
10176
  if (realRelative.startsWith("..") || path11.isAbsolute(realRelative)) {
10045
10177
  throw new Error("browser: upload files must not escape the project root through a symlink");
10046
10178
  }
10047
- const stat18 = await fs8.stat(realFile);
10048
- if (!stat18.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
10179
+ const stat19 = await fs8.stat(realFile);
10180
+ if (!stat19.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
10049
10181
  resolved.push(realFile);
10050
10182
  }
10051
10183
  await this.runPageOperation(
@@ -10226,7 +10358,7 @@ function pushBounded(target, value, limit) {
10226
10358
  }
10227
10359
  async function abortable(signal, operation, onAbort) {
10228
10360
  signal.throwIfAborted();
10229
- return new Promise((resolve16, reject) => {
10361
+ return new Promise((resolve17, reject) => {
10230
10362
  let settled = false;
10231
10363
  let aborting = false;
10232
10364
  const finish = (fn) => {
@@ -10244,7 +10376,7 @@ async function abortable(signal, operation, onAbort) {
10244
10376
  signal.addEventListener("abort", abort, { once: true });
10245
10377
  operation().then(
10246
10378
  (value) => {
10247
- if (!aborting) finish(() => resolve16(value));
10379
+ if (!aborting) finish(() => resolve17(value));
10248
10380
  },
10249
10381
  (err) => {
10250
10382
  if (!aborting) finish(() => reject(err));
@@ -10301,7 +10433,7 @@ var sessionIdSchema = {
10301
10433
  };
10302
10434
  var browserOpenTool = {
10303
10435
  name: "browser_open",
10304
- description: "Open an isolated first-party Playwright browser session, optionally navigating to a URL.",
10436
+ 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.",
10305
10437
  usageHint: "browser_open({ url?, width?, height?, trace? })",
10306
10438
  permission: "confirm",
10307
10439
  mutating: true,
@@ -10354,7 +10486,7 @@ var browserStatusTool = {
10354
10486
  };
10355
10487
  var browserNavigateTool = {
10356
10488
  name: "browser_navigate",
10357
- description: "Navigate an owned browser session to an approved http(s) URL.",
10489
+ 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.",
10358
10490
  usageHint: "browser_navigate({ sessionId, url })",
10359
10491
  permission: "confirm",
10360
10492
  mutating: true,
@@ -10693,7 +10825,7 @@ for (const tool of browserTools) tool.timeoutMs ??= 6e4;
10693
10825
  import { spawn as spawn4 } from "node:child_process";
10694
10826
  import * as fs13 from "node:fs";
10695
10827
  import * as net3 from "node:net";
10696
- import { StringDecoder } from "node:string_decoder";
10828
+ import { StringDecoder as StringDecoder2 } from "node:string_decoder";
10697
10829
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10698
10830
  import { checkUnixSocketPath } from "@wrongstack/core/utils";
10699
10831
 
@@ -13734,12 +13866,12 @@ function projectIndexServerBuildId(entrypoint) {
13734
13866
  const cleanHref = href.split(/[?#]/, 1)[0] ?? href;
13735
13867
  const file = cleanHref.startsWith("file:") ? fileURLToPath(cleanHref) : path17.resolve(cleanHref);
13736
13868
  try {
13737
- const stat18 = fs12.statSync(file);
13738
- if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat18.mtimeMs && buildIdCache.size === stat18.size) {
13869
+ const stat19 = fs12.statSync(file);
13870
+ if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat19.mtimeMs && buildIdCache.size === stat19.size) {
13739
13871
  return buildIdCache.buildId;
13740
13872
  }
13741
13873
  const buildId = createHash4("sha256").update(fs12.readFileSync(file)).digest("hex").slice(0, 24);
13742
- buildIdCache = { file, mtimeMs: stat18.mtimeMs, size: stat18.size, buildId };
13874
+ buildIdCache = { file, mtimeMs: stat19.mtimeMs, size: stat19.size, buildId };
13743
13875
  return buildId;
13744
13876
  } catch {
13745
13877
  return `unreadable:${path17.basename(file)}`;
@@ -13881,8 +14013,8 @@ function isProjectIndexServerHealth(value) {
13881
14013
  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";
13882
14014
  }
13883
14015
  function delay(ms) {
13884
- return new Promise((resolve16) => {
13885
- const timer = setTimeout(resolve16, ms);
14016
+ return new Promise((resolve17) => {
14017
+ const timer = setTimeout(resolve17, ms);
13886
14018
  timer.unref?.();
13887
14019
  });
13888
14020
  }
@@ -14086,7 +14218,7 @@ var ProjectServerConnection = class {
14086
14218
  return Promise.reject(new Error("codebase-index server connection is not available"));
14087
14219
  }
14088
14220
  const id = this.nextId++;
14089
- return new Promise((resolve16, reject) => {
14221
+ return new Promise((resolve17, reject) => {
14090
14222
  const timer = setTimeout(() => {
14091
14223
  const entry = this.pending.get(id);
14092
14224
  if (!entry) return;
@@ -14109,7 +14241,7 @@ var ProjectServerConnection = class {
14109
14241
  entry.reject(cancellationError(signal));
14110
14242
  } : void 0;
14111
14243
  this.pending.set(id, {
14112
- resolve: resolve16,
14244
+ resolve: resolve17,
14113
14245
  reject,
14114
14246
  timer,
14115
14247
  signal,
@@ -14178,7 +14310,7 @@ var ProjectServerConnection = class {
14178
14310
  this.binaryBuffer = [];
14179
14311
  this.useBinary = false;
14180
14312
  this.textDecoder = null;
14181
- return new Promise((resolve16, reject) => {
14313
+ return new Promise((resolve17, reject) => {
14182
14314
  const socket = net3.createConnection(this.endpoint);
14183
14315
  this.socket = socket;
14184
14316
  const timer = setTimeout(() => {
@@ -14190,7 +14322,7 @@ var ProjectServerConnection = class {
14190
14322
  clearTimeout(timer);
14191
14323
  this.connectResolve = null;
14192
14324
  this.connectReject = null;
14193
- resolve16();
14325
+ resolve17();
14194
14326
  };
14195
14327
  const finishReject = (error) => {
14196
14328
  clearTimeout(timer);
@@ -14213,7 +14345,7 @@ var ProjectServerConnection = class {
14213
14345
  this.onBinaryData(socket, chunk);
14214
14346
  return;
14215
14347
  }
14216
- if (!this.textDecoder) this.textDecoder = new StringDecoder("utf8");
14348
+ if (!this.textDecoder) this.textDecoder = new StringDecoder2("utf8");
14217
14349
  this.buffer += this.textDecoder.write(chunk);
14218
14350
  while (true) {
14219
14351
  const newline = this.buffer.indexOf("\n");
@@ -15204,9 +15336,9 @@ var ParserWorkerPool = class {
15204
15336
  for (let i = 0; i < files.length; i++) {
15205
15337
  chunks[i % workerCount].push(files[i]);
15206
15338
  }
15207
- return new Promise((resolve16, reject) => {
15339
+ return new Promise((resolve17, reject) => {
15208
15340
  this.pending.set(batchId, {
15209
- resolve: resolve16,
15341
+ resolve: resolve17,
15210
15342
  reject,
15211
15343
  accumulated: [],
15212
15344
  expectedWorkers: workerCount,
@@ -15237,10 +15369,10 @@ var ParserWorkerPool = class {
15237
15369
  await Promise.allSettled(
15238
15370
  workers.map(
15239
15371
  (w) => Promise.race([
15240
- new Promise((resolve16) => {
15241
- w.once("exit", () => resolve16());
15372
+ new Promise((resolve17) => {
15373
+ w.once("exit", () => resolve17());
15242
15374
  }),
15243
- new Promise((resolve16) => setTimeout(() => resolve16(), 2e3))
15375
+ new Promise((resolve17) => setTimeout(() => resolve17(), 2e3))
15244
15376
  ]).then(() => {
15245
15377
  if (!w.threadId) return;
15246
15378
  return w.terminate().catch(() => {
@@ -15304,7 +15436,7 @@ function shouldUseParserWorkerPool(candidateFileCount, parseBatchCount) {
15304
15436
  return !isFrugalPerf() && candidateFileCount >= WORKER_POOL_THRESHOLD && parseBatchCount > 1;
15305
15437
  }
15306
15438
  function yieldEventLoop() {
15307
- return new Promise((resolve16) => setImmediate(resolve16));
15439
+ return new Promise((resolve17) => setImmediate(resolve17));
15308
15440
  }
15309
15441
  function throwIfAborted(signal) {
15310
15442
  if (!signal?.aborted) return;
@@ -15332,7 +15464,7 @@ function normalizeComparablePath(value) {
15332
15464
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
15333
15465
  }
15334
15466
  function gitOutput(projectRoot, args) {
15335
- return new Promise((resolve16, reject) => {
15467
+ return new Promise((resolve17, reject) => {
15336
15468
  execFile(
15337
15469
  "git",
15338
15470
  ["-C", projectRoot, ...args],
@@ -15343,7 +15475,7 @@ function gitOutput(projectRoot, args) {
15343
15475
  },
15344
15476
  (error, stdout) => {
15345
15477
  if (error) reject(error);
15346
- else resolve16(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
15478
+ else resolve17(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
15347
15479
  }
15348
15480
  );
15349
15481
  });
@@ -15575,9 +15707,9 @@ async function runIndexerWithStore(store, opts) {
15575
15707
  const statReadParse = await Promise.allSettled(
15576
15708
  batchFiles.map(
15577
15709
  async (file) => {
15578
- let stat18;
15710
+ let stat19;
15579
15711
  try {
15580
- stat18 = await fs18.stat(file, statOpts);
15712
+ stat19 = await fs18.stat(file, statOpts);
15581
15713
  } catch (e) {
15582
15714
  if (isAbortError(e)) throw e;
15583
15715
  return {
@@ -15589,21 +15721,21 @@ async function runIndexerWithStore(store, opts) {
15589
15721
  missing: isMissingPathError(e)
15590
15722
  };
15591
15723
  }
15592
- if (!stat18.isFile()) return { file, stat: stat18, lang: "", parsed: null };
15724
+ if (!stat19.isFile()) return { file, stat: stat19, lang: "", parsed: null };
15593
15725
  const lang = detectLang(file);
15594
- if (!lang) return { file, stat: stat18, lang: "", parsed: null };
15595
- if (stat18.size > MAX_INDEX_FILE_BYTES) {
15726
+ if (!lang) return { file, stat: stat19, lang: "", parsed: null };
15727
+ if (stat19.size > MAX_INDEX_FILE_BYTES) {
15596
15728
  return {
15597
15729
  file,
15598
- stat: stat18,
15730
+ stat: stat19,
15599
15731
  lang,
15600
15732
  parsed: null,
15601
- error: `file too large (${stat18.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
15733
+ error: `file too large (${stat19.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
15602
15734
  };
15603
15735
  }
15604
15736
  const meta = existingMeta.get(file);
15605
- if (!force && meta && meta.mtimeMs === Math.floor(stat18.mtimeMs)) {
15606
- return { file, stat: stat18, lang, parsed: null, skippedMeta: meta };
15737
+ if (!force && meta && meta.mtimeMs === Math.floor(stat19.mtimeMs)) {
15738
+ return { file, stat: stat19, lang, parsed: null, skippedMeta: meta };
15607
15739
  }
15608
15740
  let content;
15609
15741
  try {
@@ -15612,7 +15744,7 @@ async function runIndexerWithStore(store, opts) {
15612
15744
  if (isAbortError(e)) throw e;
15613
15745
  return {
15614
15746
  file,
15615
- stat: stat18,
15747
+ stat: stat19,
15616
15748
  lang,
15617
15749
  parsed: null,
15618
15750
  error: `read error: ${e instanceof Error ? e.message : String(e)}`
@@ -15622,15 +15754,15 @@ async function runIndexerWithStore(store, opts) {
15622
15754
  if (!force && meta && meta.contentHash && contentHash === meta.contentHash) {
15623
15755
  return {
15624
15756
  file,
15625
- stat: stat18,
15757
+ stat: stat19,
15626
15758
  lang,
15627
15759
  parsed: null,
15628
15760
  content,
15629
15761
  contentHash,
15630
- skippedMeta: { ...meta, mtimeMs: Math.floor(stat18.mtimeMs) }
15762
+ skippedMeta: { ...meta, mtimeMs: Math.floor(stat19.mtimeMs) }
15631
15763
  };
15632
15764
  }
15633
- return { file, stat: stat18, lang, parsed: null, content, contentHash };
15765
+ return { file, stat: stat19, lang, parsed: null, content, contentHash };
15634
15766
  }
15635
15767
  )
15636
15768
  );
@@ -15709,7 +15841,7 @@ async function runIndexerWithStore(store, opts) {
15709
15841
  filesFailed++;
15710
15842
  continue;
15711
15843
  }
15712
- const { stat: stat18, lang, parsed } = result;
15844
+ const { stat: stat19, lang, parsed } = result;
15713
15845
  if (result.skippedMeta) {
15714
15846
  langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
15715
15847
  symbolsIndexed += result.skippedMeta.symbolCount;
@@ -15733,7 +15865,7 @@ async function runIndexerWithStore(store, opts) {
15733
15865
  store.upsertFile({
15734
15866
  file,
15735
15867
  lang,
15736
- mtimeMs: Math.floor(stat18.mtimeMs),
15868
+ mtimeMs: Math.floor(stat19.mtimeMs),
15737
15869
  symbolCount: 0,
15738
15870
  lastIndexed: Date.now(),
15739
15871
  contentHash: result.contentHash ?? ""
@@ -15747,7 +15879,7 @@ async function runIndexerWithStore(store, opts) {
15747
15879
  store.replaceEmptyFile({
15748
15880
  file,
15749
15881
  lang,
15750
- mtimeMs: Math.floor(stat18.mtimeMs),
15882
+ mtimeMs: Math.floor(stat19.mtimeMs),
15751
15883
  symbolCount: 0,
15752
15884
  lastIndexed: Date.now(),
15753
15885
  contentHash: result.contentHash ?? ""
@@ -15761,7 +15893,7 @@ async function runIndexerWithStore(store, opts) {
15761
15893
  lang,
15762
15894
  symbols: parsed.symbols,
15763
15895
  refs: parsed.refs ?? [],
15764
- mtimeMs: Math.floor(stat18.mtimeMs),
15896
+ mtimeMs: Math.floor(stat19.mtimeMs),
15765
15897
  symbolCount: parsed.symbols.length,
15766
15898
  contentHash: result.contentHash ?? ""
15767
15899
  });
@@ -16086,7 +16218,7 @@ function callIndexOp(op, args, opts) {
16086
16218
  opts.signal.reason instanceof Error ? opts.signal.reason : new Error("Indexing cancelled")
16087
16219
  );
16088
16220
  }
16089
- return new Promise((resolve16, reject) => {
16221
+ return new Promise((resolve17, reject) => {
16090
16222
  const id = nextRpcId++;
16091
16223
  const timer = setTimeout(() => {
16092
16224
  pending.delete(id);
@@ -16108,7 +16240,7 @@ function callIndexOp(op, args, opts) {
16108
16240
  pending.set(id, {
16109
16241
  resolve: (v) => {
16110
16242
  cleanup();
16111
- resolve16(v);
16243
+ resolve17(v);
16112
16244
  },
16113
16245
  reject: (e) => {
16114
16246
  cleanup();
@@ -16259,6 +16391,160 @@ async function outgoingCallsService2(args) {
16259
16391
  }
16260
16392
 
16261
16393
  // src/codebase-index/codebase-index-tool.ts
16394
+ import { ToolValidationError } from "@wrongstack/core/types";
16395
+
16396
+ // src/codebase-index/codebase-search-tool.ts
16397
+ import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
16398
+ var INDEXABLE_LANG_IDS = [
16399
+ "ts",
16400
+ "tsx",
16401
+ "js",
16402
+ "jsx",
16403
+ "go",
16404
+ "py",
16405
+ "rs",
16406
+ "json",
16407
+ "yaml"
16408
+ ];
16409
+ var codebaseSearchTool = {
16410
+ name: "codebase-search",
16411
+ category: "Project",
16412
+ icon: "index",
16413
+ 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).",
16414
+ 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.",
16415
+ permission: "auto",
16416
+ mutating: false,
16417
+ capabilities: ["fs.read"],
16418
+ // The index host has its own 30s read watchdog. Leave enough headroom for
16419
+ // worker teardown and structured timeout reporting.
16420
+ timeoutMs: 35e3,
16421
+ inputSchema: {
16422
+ type: "object",
16423
+ properties: {
16424
+ query: {
16425
+ type: "string",
16426
+ description: "Search query \u2014 searches symbol names, signatures, and doc comments"
16427
+ },
16428
+ kind: {
16429
+ type: "string",
16430
+ enum: [
16431
+ "class",
16432
+ "interface",
16433
+ "enum",
16434
+ "type",
16435
+ "function",
16436
+ "method",
16437
+ "var",
16438
+ "const",
16439
+ "let",
16440
+ "property",
16441
+ "parameter",
16442
+ "namespace",
16443
+ "object",
16444
+ "literal",
16445
+ "schema",
16446
+ "struct",
16447
+ "trait",
16448
+ "impl",
16449
+ "static",
16450
+ "mod"
16451
+ ],
16452
+ description: "Filter by indexed symbol kind"
16453
+ },
16454
+ lang: {
16455
+ type: "string",
16456
+ enum: [...INDEXABLE_LANG_IDS],
16457
+ description: "Filter by indexed language"
16458
+ },
16459
+ lspKind: {
16460
+ type: "integer",
16461
+ description: "Filter by LSP SymbolKind number (e.g. 5=Class, 12=Function, 11=Interface, 10=Enum)"
16462
+ },
16463
+ file: {
16464
+ type: "string",
16465
+ description: "Filter to files matching this path substring"
16466
+ },
16467
+ limit: {
16468
+ type: "integer",
16469
+ description: "Maximum results to return (default 20, max 100)",
16470
+ minimum: 1,
16471
+ maximum: 100
16472
+ },
16473
+ preferLsp: {
16474
+ type: "boolean",
16475
+ 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."
16476
+ }
16477
+ },
16478
+ required: ["query"]
16479
+ },
16480
+ async execute(input, ctx, execOpts) {
16481
+ const state = getIndexState();
16482
+ if (state.indexing && !state.ready) {
16483
+ return {
16484
+ results: [],
16485
+ total: 0,
16486
+ query: input.query,
16487
+ indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
16488
+ };
16489
+ }
16490
+ if (state.lastError) {
16491
+ const circuit = state.circuit;
16492
+ 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.";
16493
+ return {
16494
+ results: [],
16495
+ total: 0,
16496
+ query: input.query,
16497
+ indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
16498
+ };
16499
+ }
16500
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
16501
+ let searched;
16502
+ try {
16503
+ searched = await searchCodebaseIndex(
16504
+ {
16505
+ projectRoot: ctx.projectRoot,
16506
+ indexDir: codebaseIndexDirOverride(ctx),
16507
+ query: input.query,
16508
+ kind: input.kind?.toLowerCase(),
16509
+ lang: input.lang?.toLowerCase(),
16510
+ file: input.file,
16511
+ lspKind: input.lspKind,
16512
+ limit
16513
+ },
16514
+ { signal: execOpts?.signal }
16515
+ );
16516
+ } catch (err) {
16517
+ if (execOpts?.signal?.aborted) throw err;
16518
+ return {
16519
+ results: [],
16520
+ total: 0,
16521
+ query: input.query,
16522
+ indexStatus: `Index query failed: ${toErrorMessage3(err)}. Fall back to grep/glob for this lookup.`
16523
+ };
16524
+ }
16525
+ const { results, total } = searched;
16526
+ let hasPersistedIndex = state.ready || total > 0;
16527
+ if (!hasPersistedIndex) {
16528
+ try {
16529
+ const stats = await codebaseIndexStats(
16530
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
16531
+ { signal: execOpts?.signal }
16532
+ );
16533
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
16534
+ } catch {
16535
+ }
16536
+ }
16537
+ return {
16538
+ results,
16539
+ total,
16540
+ query: input.query,
16541
+ ...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
16542
+ };
16543
+ }
16544
+ };
16545
+
16546
+ // src/codebase-index/codebase-index-tool.ts
16547
+ var MAX_REPORTED_ERRORS = 20;
16262
16548
  var codebaseIndexTool = {
16263
16549
  name: "codebase-index",
16264
16550
  category: "Project",
@@ -16284,12 +16570,23 @@ var codebaseIndexTool = {
16284
16570
  },
16285
16571
  langs: {
16286
16572
  type: "array",
16287
- items: { type: "string" },
16288
- description: "Limit reindex to specific languages: ts, tsx, js, jsx, go, py, rs"
16573
+ items: { type: "string", enum: [...INDEXABLE_LANG_IDS] },
16574
+ description: `Limit reindex to specific languages: ${INDEXABLE_LANG_IDS.join(", ")}`
16289
16575
  }
16290
16576
  }
16291
16577
  },
16292
16578
  async execute(input, ctx, execOpts) {
16579
+ if (input.langs) {
16580
+ const unknown = input.langs.filter(
16581
+ (lang) => !INDEXABLE_LANG_IDS.includes(lang)
16582
+ );
16583
+ if (unknown.length > 0) {
16584
+ throw new ToolValidationError({
16585
+ message: `codebase-index: unknown lang(s) ${unknown.map((l) => `"${l}"`).join(", ")}. Valid ids: ${INDEXABLE_LANG_IDS.join(", ")}.`,
16586
+ field: "langs"
16587
+ });
16588
+ }
16589
+ }
16293
16590
  if (isIndexing()) {
16294
16591
  return {
16295
16592
  filesIndexed: 0,
@@ -16311,23 +16608,32 @@ var codebaseIndexTool = {
16311
16608
  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.`
16312
16609
  };
16313
16610
  }
16314
- return await runStartupIndex({
16611
+ const result = await runStartupIndex({
16315
16612
  projectRoot: ctx.projectRoot,
16316
16613
  force: input.force ?? false,
16317
16614
  langs: input.langs,
16318
16615
  indexDir: codebaseIndexDirOverride(ctx),
16319
16616
  signal: execOpts?.signal
16320
16617
  });
16618
+ if (result.errors.length > MAX_REPORTED_ERRORS) {
16619
+ const hidden = result.errors.length - MAX_REPORTED_ERRORS;
16620
+ return {
16621
+ ...result,
16622
+ errors: [...result.errors.slice(0, MAX_REPORTED_ERRORS), `+${hidden} more`]
16623
+ };
16624
+ }
16625
+ return result;
16321
16626
  }
16322
16627
  };
16323
16628
 
16324
16629
  // src/codebase-index/codebase-incoming-calls-tool.ts
16630
+ import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
16325
16631
  var codebaseIncomingCallsTool = {
16326
16632
  name: "codebase-incoming-calls",
16327
16633
  category: "Project",
16328
16634
  icon: "index",
16329
- 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.",
16330
- 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.',
16635
+ 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.",
16636
+ 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.',
16331
16637
  permission: "auto",
16332
16638
  mutating: false,
16333
16639
  capabilities: ["fs.read"],
@@ -16379,16 +16685,27 @@ var codebaseIncomingCallsTool = {
16379
16685
  }
16380
16686
  const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
16381
16687
  const transitive = input.transitive === true;
16382
- const { calls, symbolFound, ambiguous, totalMatches } = await incomingCallsService2(
16383
- {
16384
- projectRoot: ctx.projectRoot,
16385
- indexDir: codebaseIndexDirOverride(ctx),
16688
+ let serviced;
16689
+ try {
16690
+ serviced = await incomingCallsService2(
16691
+ {
16692
+ projectRoot: ctx.projectRoot,
16693
+ indexDir: codebaseIndexDirOverride(ctx),
16694
+ symbol: input.symbol,
16695
+ file: input.file,
16696
+ limit,
16697
+ transitive
16698
+ }
16699
+ );
16700
+ } catch (err) {
16701
+ return {
16386
16702
  symbol: input.symbol,
16387
- file: input.file,
16388
- limit,
16389
- transitive
16390
- }
16391
- );
16703
+ calls: [],
16704
+ total: 0,
16705
+ indexStatus: `Index query failed: ${toErrorMessage4(err)}. Fall back to grep for this lookup.`
16706
+ };
16707
+ }
16708
+ const { calls, symbolFound, ambiguous, totalMatches } = serviced;
16392
16709
  if (!symbolFound) {
16393
16710
  let hasPersistedIndex = state.ready;
16394
16711
  if (!hasPersistedIndex) {
@@ -16433,12 +16750,13 @@ var codebaseIncomingCallsTool = {
16433
16750
  };
16434
16751
 
16435
16752
  // src/codebase-index/codebase-outgoing-calls-tool.ts
16753
+ import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
16436
16754
  var codebaseOutgoingCallsTool = {
16437
16755
  name: "codebase-outgoing-calls",
16438
16756
  category: "Project",
16439
16757
  icon: "index",
16440
16758
  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.",
16441
- 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.',
16759
+ 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.',
16442
16760
  permission: "auto",
16443
16761
  mutating: false,
16444
16762
  capabilities: ["fs.read"],
@@ -16490,16 +16808,27 @@ var codebaseOutgoingCallsTool = {
16490
16808
  }
16491
16809
  const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
16492
16810
  const transitive = input.transitive === true;
16493
- const { calls, symbolFound, unresolvedCount, totalMatches } = await outgoingCallsService2(
16494
- {
16495
- projectRoot: ctx.projectRoot,
16496
- indexDir: codebaseIndexDirOverride(ctx),
16811
+ let serviced;
16812
+ try {
16813
+ serviced = await outgoingCallsService2(
16814
+ {
16815
+ projectRoot: ctx.projectRoot,
16816
+ indexDir: codebaseIndexDirOverride(ctx),
16817
+ symbol: input.symbol,
16818
+ file: input.file,
16819
+ limit,
16820
+ transitive
16821
+ }
16822
+ );
16823
+ } catch (err) {
16824
+ return {
16497
16825
  symbol: input.symbol,
16498
- file: input.file,
16499
- limit,
16500
- transitive
16501
- }
16502
- );
16826
+ calls: [],
16827
+ total: 0,
16828
+ indexStatus: `Index query failed: ${toErrorMessage5(err)}. Fall back to grep for this lookup.`
16829
+ };
16830
+ }
16831
+ const { calls, symbolFound, unresolvedCount, totalMatches } = serviced;
16503
16832
  if (!symbolFound) {
16504
16833
  let hasPersistedIndex = state.ready;
16505
16834
  if (!hasPersistedIndex) {
@@ -16543,135 +16872,9 @@ var codebaseOutgoingCallsTool = {
16543
16872
  }
16544
16873
  };
16545
16874
 
16546
- // src/codebase-index/codebase-search-tool.ts
16547
- var codebaseSearchTool = {
16548
- name: "codebase-search",
16549
- category: "Project",
16550
- icon: "index",
16551
- 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).",
16552
- 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.",
16553
- permission: "auto",
16554
- mutating: false,
16555
- capabilities: ["fs.read"],
16556
- // The index host has its own 30s read watchdog. Leave enough headroom for
16557
- // worker teardown and structured timeout reporting.
16558
- timeoutMs: 35e3,
16559
- inputSchema: {
16560
- type: "object",
16561
- properties: {
16562
- query: {
16563
- type: "string",
16564
- description: "Search query \u2014 searches symbol names, signatures, and doc comments"
16565
- },
16566
- kind: {
16567
- type: "string",
16568
- enum: [
16569
- "class",
16570
- "interface",
16571
- "enum",
16572
- "type",
16573
- "function",
16574
- "method",
16575
- "var",
16576
- "const",
16577
- "let",
16578
- "property",
16579
- "parameter",
16580
- "namespace",
16581
- "object",
16582
- "literal",
16583
- "schema",
16584
- "struct",
16585
- "trait",
16586
- "impl",
16587
- "static",
16588
- "mod"
16589
- ],
16590
- description: "Filter by indexed symbol kind"
16591
- },
16592
- lang: {
16593
- type: "string",
16594
- enum: ["ts", "tsx", "js", "jsx", "go", "py", "rs", "json", "yaml"],
16595
- description: "Filter by indexed language"
16596
- },
16597
- lspKind: {
16598
- type: "integer",
16599
- description: "Filter by LSP SymbolKind number (e.g. 5=Class, 12=Function, 11=Interface, 10=Enum)"
16600
- },
16601
- file: {
16602
- type: "string",
16603
- description: "Filter to files matching this path substring"
16604
- },
16605
- limit: {
16606
- type: "integer",
16607
- description: "Maximum results to return (default 20, max 100)",
16608
- minimum: 1,
16609
- maximum: 100
16610
- },
16611
- preferLsp: {
16612
- type: "boolean",
16613
- 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."
16614
- }
16615
- },
16616
- required: ["query"]
16617
- },
16618
- async execute(input, ctx, execOpts) {
16619
- const state = getIndexState();
16620
- if (state.indexing && !state.ready) {
16621
- return {
16622
- results: [],
16623
- total: 0,
16624
- query: input.query,
16625
- indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
16626
- };
16627
- }
16628
- if (state.lastError) {
16629
- const circuit = state.circuit;
16630
- 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.";
16631
- return {
16632
- results: [],
16633
- total: 0,
16634
- query: input.query,
16635
- indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
16636
- };
16637
- }
16638
- const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
16639
- const { results, total } = await searchCodebaseIndex(
16640
- {
16641
- projectRoot: ctx.projectRoot,
16642
- indexDir: codebaseIndexDirOverride(ctx),
16643
- query: input.query,
16644
- kind: input.kind?.toLowerCase(),
16645
- lang: input.lang?.toLowerCase(),
16646
- file: input.file,
16647
- lspKind: input.lspKind,
16648
- limit
16649
- },
16650
- { signal: execOpts?.signal }
16651
- );
16652
- let hasPersistedIndex = state.ready || total > 0;
16653
- if (!hasPersistedIndex) {
16654
- try {
16655
- const stats = await codebaseIndexStats(
16656
- { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
16657
- { signal: execOpts?.signal }
16658
- );
16659
- hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
16660
- } catch {
16661
- }
16662
- }
16663
- return {
16664
- results,
16665
- total,
16666
- query: input.query,
16667
- ...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
16668
- };
16669
- }
16670
- };
16671
-
16672
- // src/codebase-index/codebase-stats-tool.ts
16673
- var codebaseStatsTool = {
16674
- name: "codebase-stats",
16875
+ // src/codebase-index/codebase-stats-tool.ts
16876
+ var codebaseStatsTool = {
16877
+ name: "codebase-stats",
16675
16878
  category: "Project",
16676
16879
  icon: "index",
16677
16880
  description: "Check whether a persisted codebase index exists and report its health and statistics (symbols, files, language/kind breakdown, size, last update).",
@@ -16762,9 +16965,9 @@ import * as path25 from "node:path";
16762
16965
  var deadCodeScanTool = {
16763
16966
  name: "dead-code-scan",
16764
16967
  category: "Project",
16765
- icon: "search",
16968
+ icon: "index",
16766
16969
  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).",
16767
- 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.',
16970
+ 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).",
16768
16971
  permission: "auto",
16769
16972
  mutating: false,
16770
16973
  capabilities: ["fs.read"],
@@ -17527,12 +17730,14 @@ import { statSync as statSync3 } from "node:fs";
17527
17730
  import * as fs22 from "node:fs/promises";
17528
17731
  import * as path27 from "node:path";
17529
17732
  import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core/utils";
17733
+ import { ToolValidationError as ToolValidationError2 } from "@wrongstack/core/types";
17530
17734
  var MAX_FILE_DUMP_BYTES = 5 * 1024 * 1024;
17735
+ var MAX_GIT_DIFF_CHARS = 1e5;
17531
17736
  var diffTool = {
17532
17737
  name: "diff",
17533
17738
  category: "Filesystem",
17534
17739
  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.",
17535
- 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).',
17740
+ 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).',
17536
17741
  permission: "auto",
17537
17742
  mutating: false,
17538
17743
  maxOutputBytes: 262144,
@@ -17565,11 +17770,12 @@ var diffTool = {
17565
17770
  mode: {
17566
17771
  type: "string",
17567
17772
  enum: ["unified", "side-by-side", "stat"],
17568
- description: 'Output format. "unified" is default, "stat" shows summary only.'
17773
+ 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.'
17569
17774
  },
17570
17775
  context: {
17571
17776
  type: "integer",
17572
- description: "Number of context lines for unified diffs (default: 3)."
17777
+ minimum: 0,
17778
+ description: "Number of context lines for git unified diffs (default: 3, passed as -U<n>). Ignored by the `files`-only dump path."
17573
17779
  }
17574
17780
  }
17575
17781
  },
@@ -17582,16 +17788,31 @@ var diffTool = {
17582
17788
  };
17583
17789
  async function gitDiff(input, ctx, signal) {
17584
17790
  if (input.a?.startsWith("-")) {
17585
- throw new Error(`diff: unsafe ref "${input.a}" \u2014 refs may not begin with '-' (flag injection)`);
17791
+ throw new ToolValidationError2({
17792
+ message: `diff: unsafe ref "${input.a}" \u2014 refs may not begin with '-' (flag injection)`,
17793
+ field: "a"
17794
+ });
17586
17795
  }
17587
17796
  if (input.b?.startsWith("-")) {
17588
- throw new Error(`diff: unsafe ref "${input.b}" \u2014 refs may not begin with '-' (flag injection)`);
17797
+ throw new ToolValidationError2({
17798
+ message: `diff: unsafe ref "${input.b}" \u2014 refs may not begin with '-' (flag injection)`,
17799
+ field: "b"
17800
+ });
17589
17801
  }
17802
+ const requestedMode = input.mode ?? "unified";
17803
+ const statMode = requestedMode === "stat";
17804
+ const effectiveMode = statMode ? "stat" : "unified";
17805
+ const sideBySideNote = requestedMode === "side-by-side" ? "side-by-side output is not supported; a unified diff was produced instead." : void 0;
17590
17806
  const gitDir = findGitDir(ctx.cwd);
17591
17807
  if (!gitDir) {
17592
- return { diff: "", files: [], truncated: false, mode: "unified" };
17808
+ return { diff: "", files: [], truncated: false, mode: effectiveMode };
17593
17809
  }
17594
17810
  const args = ["diff", "--no-color"];
17811
+ if (statMode) args.push("--stat");
17812
+ if (!statMode && input.context !== void 0) {
17813
+ const contextLines = Math.max(0, Math.floor(input.context));
17814
+ if (Number.isFinite(contextLines)) args.push(`-U${contextLines}`);
17815
+ }
17595
17816
  if (input.staged) args.push("--staged");
17596
17817
  if (input.a) args.push(input.a);
17597
17818
  if (input.b) args.push(input.b);
@@ -17600,19 +17821,30 @@ async function gitDiff(input, ctx, signal) {
17600
17821
  args.push("--", ...files.map((f) => f.trim()));
17601
17822
  }
17602
17823
  const result = await runGit(args, gitDir, signal);
17824
+ let diff = result.stdout;
17825
+ let truncated = false;
17826
+ if (diff.length > MAX_GIT_DIFF_CHARS) {
17827
+ let clipped = diff.slice(0, MAX_GIT_DIFF_CHARS);
17828
+ const nl = clipped.lastIndexOf("\n");
17829
+ if (nl > 0) clipped = clipped.slice(0, nl);
17830
+ diff = `${clipped}
17831
+ \u2026[git diff truncated: ${result.stdout.length - clipped.length} of ${result.stdout.length} characters omitted]`;
17832
+ truncated = true;
17833
+ }
17603
17834
  return {
17604
- diff: result.stdout,
17835
+ diff,
17605
17836
  files: [],
17606
- truncated: result.stdout.length > 1e5,
17607
- mode: "unified"
17837
+ truncated,
17838
+ mode: effectiveMode,
17839
+ note: sideBySideNote
17608
17840
  };
17609
17841
  }
17610
17842
  function findGitDir(cwd) {
17611
17843
  let dir = cwd;
17612
17844
  for (let i = 0; i < 20; i++) {
17613
17845
  try {
17614
- const stat18 = statSync3(path27.join(dir, ".git"));
17615
- if (stat18.isDirectory()) return dir;
17846
+ const stat19 = statSync3(path27.join(dir, ".git"));
17847
+ if (stat19.isDirectory()) return dir;
17616
17848
  } catch {
17617
17849
  }
17618
17850
  const parent = path27.dirname(dir);
@@ -17622,7 +17854,7 @@ function findGitDir(cwd) {
17622
17854
  return null;
17623
17855
  }
17624
17856
  function runGit(args, cwd, signal) {
17625
- return new Promise((resolve16) => {
17857
+ return new Promise((resolve17) => {
17626
17858
  let stdout = "";
17627
17859
  let stderr = "";
17628
17860
  const child = spawn7("git", args, {
@@ -17638,8 +17870,8 @@ function runGit(args, cwd, signal) {
17638
17870
  child.stderr?.on("data", (c) => {
17639
17871
  stderr += c.toString();
17640
17872
  });
17641
- child.on("close", (code) => resolve16({ stdout, stderr, exitCode: code ?? 0 }));
17642
- child.on("error", (e) => resolve16({ stdout: "", stderr: e.message, exitCode: 1 }));
17873
+ child.on("close", (code) => resolve17({ stdout, stderr, exitCode: code ?? 0 }));
17874
+ child.on("error", (e) => resolve17({ stdout: "", stderr: e.message, exitCode: 1 }));
17643
17875
  });
17644
17876
  }
17645
17877
  async function fileDiff(input, ctx, _signal) {
@@ -17650,19 +17882,19 @@ async function fileDiff(input, ctx, _signal) {
17650
17882
  diff: "No files specified",
17651
17883
  files: [],
17652
17884
  truncated: false,
17653
- mode: input.mode ?? "unified"
17885
+ mode: "dump"
17654
17886
  };
17655
17887
  }
17656
17888
  const results = [];
17657
17889
  let truncated = false;
17658
17890
  for (const file of files) {
17659
- const absPath = safeResolve(file, ctx);
17660
- const stat18 = await fs22.stat(absPath).catch(() => null);
17661
- if (!stat18?.isFile()) continue;
17662
- if (stat18.size > MAX_FILE_DUMP_BYTES) {
17891
+ const absPath = await safeResolveReal(file, ctx);
17892
+ const stat19 = await fs22.stat(absPath).catch(() => null);
17893
+ if (!stat19?.isFile()) continue;
17894
+ if (stat19.size > MAX_FILE_DUMP_BYTES) {
17663
17895
  truncated = true;
17664
17896
  results.push(
17665
- `--- ${file} (skipped: ${stat18.size} bytes exceeds the ${MAX_FILE_DUMP_BYTES} limit; use the read tool with offset/limit) ---`
17897
+ `--- ${file} (skipped: ${stat19.size} bytes exceeds the ${MAX_FILE_DUMP_BYTES} limit; use the read tool with offset/limit) ---`
17666
17898
  );
17667
17899
  continue;
17668
17900
  }
@@ -17674,7 +17906,10 @@ async function fileDiff(input, ctx, _signal) {
17674
17906
  diff: results.join("\n\n"),
17675
17907
  files,
17676
17908
  truncated,
17677
- mode: input.mode ?? "unified"
17909
+ // Honest mode: this path always produces a line-numbered dump — it never
17910
+ // honors `mode`, so it must not echo the requested value back.
17911
+ mode: "dump",
17912
+ 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
17678
17913
  };
17679
17914
  }
17680
17915
  function formatWithLineNumbers(file, lines) {
@@ -17687,11 +17922,12 @@ ${numbered}`;
17687
17922
  // src/document.ts
17688
17923
  init_util();
17689
17924
  import * as fs23 from "node:fs/promises";
17925
+ import * as path28 from "node:path";
17690
17926
  var documentTool = {
17691
17927
  name: "document",
17692
17928
  category: "Project",
17693
- 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.",
17694
- 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.",
17929
+ 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.",
17930
+ 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).",
17695
17931
  permission: "auto",
17696
17932
  mutating: false,
17697
17933
  timeoutMs: 3e4,
@@ -17727,7 +17963,11 @@ var documentTool = {
17727
17963
  const results = [];
17728
17964
  let filesProcessed = 0;
17729
17965
  let itemsDocumented = 0;
17730
- const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
17966
+ const fileList = input.files ? await resolveFiles(
17967
+ Array.isArray(input.files) ? input.files.join(",") : input.files,
17968
+ cwd,
17969
+ ctx
17970
+ ) : input.path ? [safeResolve(input.path, ctx)] : [];
17731
17971
  for (const absPath of fileList) {
17732
17972
  try {
17733
17973
  const content = await fs23.readFile(absPath, "utf8");
@@ -17760,14 +18000,21 @@ var documentTool = {
17760
18000
  };
17761
18001
  }
17762
18002
  };
17763
- async function resolveFiles(filesInput, cwd) {
17764
- const files = Array.isArray(filesInput) ? filesInput : filesInput.split(",");
18003
+ async function resolveFiles(filesInput, cwd, ctx) {
18004
+ const files = filesInput.split(",");
17765
18005
  const resolved = [];
17766
18006
  for (const f of files) {
17767
- const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
18007
+ const entry = f.trim();
18008
+ if (!entry) continue;
18009
+ let absPath;
18010
+ try {
18011
+ absPath = ensureInsideRoot(path28.resolve(cwd, entry), ctx);
18012
+ } catch {
18013
+ continue;
18014
+ }
17768
18015
  try {
17769
- const stat18 = await fs23.stat(absPath);
17770
- if (stat18.isFile()) resolved.push(absPath);
18016
+ const stat19 = await fs23.stat(absPath);
18017
+ if (stat19.isFile()) resolved.push(absPath);
17771
18018
  } catch {
17772
18019
  }
17773
18020
  }
@@ -17837,7 +18084,7 @@ function processFile(content, absPath, _style, _overwrite, target) {
17837
18084
  // src/e2e.ts
17838
18085
  init_util();
17839
18086
  import { open, readdir as readdir7 } from "node:fs/promises";
17840
- import * as path28 from "node:path";
18087
+ import * as path29 from "node:path";
17841
18088
  async function readBoundedText(filePath, maxBytes) {
17842
18089
  let handle;
17843
18090
  try {
@@ -17887,8 +18134,8 @@ var MAX_PACKAGE_BYTES = 512 * 1024;
17887
18134
  var MAX_CONFIG_BYTES = 512 * 1024;
17888
18135
  var MAX_SPEC_SAMPLES = 20;
17889
18136
  function relativePath(root, target) {
17890
- const value = path28.relative(root, target) || ".";
17891
- return value.split(path28.sep).join("/");
18137
+ const value = path29.relative(root, target) || ".";
18138
+ return value.split(path29.sep).join("/");
17892
18139
  }
17893
18140
  function escapeRegExp(value) {
17894
18141
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -17960,7 +18207,7 @@ async function scanWorkspace(root, maxDepth, signal) {
17960
18207
  }
17961
18208
  for (const entry of entries) {
17962
18209
  signal.throwIfAborted();
17963
- const absolutePath = path28.join(current.directory, entry.name);
18210
+ const absolutePath = path29.join(current.directory, entry.name);
17964
18211
  if (entry.isFile()) {
17965
18212
  if (entry.name === "package.json") result.packageFiles.push(absolutePath);
17966
18213
  const framework = CONFIG_NAMES.get(entry.name);
@@ -18021,9 +18268,9 @@ async function detectPackageManager3(projectRoot, scanRoot, declared) {
18021
18268
  if (names.has("bun.lock") || names.has("bun.lockb")) return "bun";
18022
18269
  if (names.has("package-lock.json") || names.has("npm-shrinkwrap.json")) return "npm";
18023
18270
  if (directory === scanRoot) break;
18024
- const parent = path28.dirname(directory);
18025
- const relativeParent = path28.relative(scanRoot, parent);
18026
- if (parent === directory || relativeParent.startsWith("..") || path28.isAbsolute(relativeParent)) {
18271
+ const parent = path29.dirname(directory);
18272
+ const relativeParent = path29.relative(scanRoot, parent);
18273
+ if (parent === directory || relativeParent.startsWith("..") || path29.isAbsolute(relativeParent)) {
18027
18274
  break;
18028
18275
  }
18029
18276
  directory = parent;
@@ -18068,7 +18315,7 @@ function isSpec(framework, filename) {
18068
18315
  return /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/i.test(filename);
18069
18316
  }
18070
18317
  async function collectSpecs(root, framework, testDirectory, signal) {
18071
- const roots = testDirectory ? [path28.resolve(root, testDirectory)] : framework === "cypress" ? [path28.join(root, "cypress", "e2e"), path28.join(root, "cypress", "integration")] : [path28.join(root, "tests")];
18318
+ const roots = testDirectory ? [path29.resolve(root, testDirectory)] : framework === "cypress" ? [path29.join(root, "cypress", "e2e"), path29.join(root, "cypress", "integration")] : [path29.join(root, "tests")];
18072
18319
  const samples = [];
18073
18320
  let count = 0;
18074
18321
  let scanned = 0;
@@ -18087,7 +18334,7 @@ async function collectSpecs(root, framework, testDirectory, signal) {
18087
18334
  }
18088
18335
  for (const entry of entries) {
18089
18336
  signal.throwIfAborted();
18090
- const target = path28.join(directory, entry.name);
18337
+ const target = path29.join(directory, entry.name);
18091
18338
  if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) queue.push(target);
18092
18339
  else if (entry.isFile() && isSpec(framework, entry.name)) {
18093
18340
  count += 1;
@@ -18107,7 +18354,7 @@ function nearestPackage(projectRoot, packagesByDirectory, scanRoot) {
18107
18354
  const found = packagesByDirectory.get(directory);
18108
18355
  if (found) return found;
18109
18356
  if (directory === scanRoot) return void 0;
18110
- const parent = path28.dirname(directory);
18357
+ const parent = path29.dirname(directory);
18111
18358
  if (parent === directory || relativePath(scanRoot, parent).startsWith("..")) return void 0;
18112
18359
  directory = parent;
18113
18360
  }
@@ -18118,13 +18365,13 @@ async function discoverE2EProjects(root, options) {
18118
18365
  const packages = (await Promise.all(scan.packageFiles.map(readPackageInfo))).filter(
18119
18366
  (info) => Boolean(info)
18120
18367
  );
18121
- const packagesByDirectory = new Map(packages.map((info) => [path28.dirname(info.path), info]));
18368
+ const packagesByDirectory = new Map(packages.map((info) => [path29.dirname(info.path), info]));
18122
18369
  const candidates = /* @__PURE__ */ new Map();
18123
18370
  for (const config of scan.configs) {
18124
18371
  if (options.framework && options.framework !== "all" && config.framework !== options.framework) {
18125
18372
  continue;
18126
18373
  }
18127
- const projectRoot = path28.dirname(config.absolutePath);
18374
+ const projectRoot = path29.dirname(config.absolutePath);
18128
18375
  candidates.set(`${config.framework}:${projectRoot}`, {
18129
18376
  framework: config.framework,
18130
18377
  root: projectRoot,
@@ -18132,7 +18379,7 @@ async function discoverE2EProjects(root, options) {
18132
18379
  });
18133
18380
  }
18134
18381
  for (const info of packages) {
18135
- const projectRoot = path28.dirname(info.path);
18382
+ const projectRoot = path29.dirname(info.path);
18136
18383
  for (const framework of frameworkFromPackage(info)) {
18137
18384
  if (options.framework && options.framework !== "all" && framework !== options.framework)
18138
18385
  continue;
@@ -18148,10 +18395,10 @@ async function discoverE2EProjects(root, options) {
18148
18395
  const scripts = matchingScripts(info, candidate.framework);
18149
18396
  const manager = await detectPackageManager3(candidate.root, root, info?.packageManager);
18150
18397
  const testDirectory = candidate.framework === "playwright" ? staticString(source, "testDir") : void 0;
18151
- const resolvedTestDirectory = testDirectory ? path28.resolve(candidate.root, testDirectory) : void 0;
18152
- const relativeTestDirectory = resolvedTestDirectory ? path28.relative(root, resolvedTestDirectory) : void 0;
18398
+ const resolvedTestDirectory = testDirectory ? path29.resolve(candidate.root, testDirectory) : void 0;
18399
+ const relativeTestDirectory = resolvedTestDirectory ? path29.relative(root, resolvedTestDirectory) : void 0;
18153
18400
  const unsafeTestDirectory = Boolean(
18154
- relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path28.isAbsolute(relativeTestDirectory))
18401
+ relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path29.isAbsolute(relativeTestDirectory))
18155
18402
  );
18156
18403
  const specs = options.includeSpecs === false || unsafeTestDirectory ? { count: 0, samples: [], truncated: false } : await collectSpecs(candidate.root, candidate.framework, testDirectory, options.signal);
18157
18404
  const warnings = [];
@@ -18259,7 +18506,7 @@ import {
18259
18506
  toStyle,
18260
18507
  unifiedDiff
18261
18508
  } from "@wrongstack/core/utils";
18262
- import { ToolValidationError } from "@wrongstack/core/types";
18509
+ import { ToolValidationError as ToolValidationError3 } from "@wrongstack/core/types";
18263
18510
 
18264
18511
  // src/_edit-match.ts
18265
18512
  var TIER_LABEL = {
@@ -18487,7 +18734,7 @@ function prefixSimilarity(a, b) {
18487
18734
  }
18488
18735
 
18489
18736
  // src/_syntax-check.ts
18490
- import * as path29 from "node:path";
18737
+ import * as path30 from "node:path";
18491
18738
  var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
18492
18739
  var MAX_CHECK_CHARS = 15e5;
18493
18740
  var MAX_ERRORS = 5;
@@ -18511,15 +18758,15 @@ async function checkSyntax(filePath, content, previousContent) {
18511
18758
  return { errors, preExisting };
18512
18759
  }
18513
18760
  function isJsoncFile(filePath) {
18514
- const base = path29.basename(filePath).toLowerCase();
18761
+ const base = path30.basename(filePath).toLowerCase();
18515
18762
  if (base.endsWith(".jsonc")) return true;
18516
18763
  if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
18517
- const dir = path29.basename(path29.dirname(filePath)).toLowerCase();
18764
+ const dir = path30.basename(path30.dirname(filePath)).toLowerCase();
18518
18765
  return dir === ".vscode";
18519
18766
  }
18520
18767
  async function parseErrors(filePath, content) {
18521
18768
  if (content.length > MAX_CHECK_CHARS) return void 0;
18522
- const ext = path29.extname(filePath).toLowerCase();
18769
+ const ext = path30.extname(filePath).toLowerCase();
18523
18770
  if (ext === ".json" || ext === ".jsonc") {
18524
18771
  try {
18525
18772
  JSON.parse(content);
@@ -18546,7 +18793,7 @@ async function parseErrors(filePath, content) {
18546
18793
  ts2.ScriptKind.JSX
18547
18794
  );
18548
18795
  const sourceFile = ts2.createSourceFile(
18549
- path29.basename(filePath),
18796
+ path30.basename(filePath),
18550
18797
  content,
18551
18798
  ts2.ScriptTarget.Latest,
18552
18799
  /* setParentNodes */
@@ -18573,6 +18820,7 @@ function formatDiag(ts2, diag, content, sourceFile) {
18573
18820
 
18574
18821
  // src/edit.ts
18575
18822
  init_util();
18823
+ var MAX_DIFF_BYTES = 262144;
18576
18824
  var editTool = {
18577
18825
  name: "edit",
18578
18826
  category: "Filesystem",
@@ -18583,46 +18831,62 @@ var editTool = {
18583
18831
  useInstead: ["write", "patch"]
18584
18832
  },
18585
18833
  permission: "confirm",
18834
+ // WS-046: gives permission decisions something to key on — the file being
18835
+ // edited, so trust rules can scope by path.
18836
+ subjectKey: "path",
18586
18837
  mutating: true,
18587
18838
  capabilities: ["fs.write"],
18588
18839
  icon: "edit",
18589
18840
  timeoutMs: 5e3,
18841
+ maxOutputBytes: 262144,
18590
18842
  inputSchema: {
18591
18843
  type: "object",
18592
18844
  properties: {
18593
- path: { type: "string" },
18594
- old_string: { type: "string" },
18595
- new_string: { type: "string" },
18596
- replace_all: { type: "boolean" }
18845
+ path: {
18846
+ type: "string",
18847
+ description: "Path to the file to edit \u2014 relative to the project root, or absolute inside it."
18848
+ },
18849
+ old_string: {
18850
+ type: "string",
18851
+ 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."
18852
+ },
18853
+ new_string: {
18854
+ type: "string",
18855
+ description: "The exact replacement text (may be empty to delete `old_string`)."
18856
+ },
18857
+ replace_all: {
18858
+ type: "boolean",
18859
+ 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."
18860
+ }
18597
18861
  },
18598
18862
  required: ["path", "old_string", "new_string"]
18599
18863
  },
18600
18864
  async execute(input, ctx, opts) {
18601
18865
  if (!input?.path) {
18602
- throw new ToolValidationError({ message: "edit: path is required", field: "path" });
18866
+ throw new ToolValidationError3({ message: "edit: path is required", field: "path" });
18603
18867
  }
18604
18868
  if (input.old_string === void 0) {
18605
- throw new ToolValidationError({
18869
+ throw new ToolValidationError3({
18606
18870
  message: "edit: old_string is required",
18607
18871
  field: "old_string"
18608
18872
  });
18609
18873
  }
18610
18874
  if (input.new_string === void 0) {
18611
- throw new ToolValidationError({
18875
+ throw new ToolValidationError3({
18612
18876
  message: "edit: new_string is required",
18613
18877
  field: "new_string"
18614
18878
  });
18615
18879
  }
18616
18880
  if (input.old_string === "") {
18617
- throw new ToolValidationError({
18881
+ throw new ToolValidationError3({
18618
18882
  message: "edit: old_string cannot be empty",
18619
18883
  field: "old_string"
18620
18884
  });
18621
18885
  }
18622
18886
  const absPath = await safeResolveReal(input.path, ctx);
18623
- const stat18 = await fs24.stat(absPath).catch((err) => {
18887
+ const stat19 = await fs24.stat(absPath).catch((err) => {
18624
18888
  if (err.code === "ENOENT") {
18625
- throw new ToolValidationError({
18889
+ throw new ToolValidationError3({
18626
18890
  message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
18627
18891
  field: "path",
18628
18892
  context: { exists: false }
@@ -18630,8 +18894,8 @@ var editTool = {
18630
18894
  }
18631
18895
  throw err;
18632
18896
  });
18633
- if (!stat18.isFile()) {
18634
- throw new ToolValidationError({
18897
+ if (!stat19.isFile()) {
18898
+ throw new ToolValidationError3({
18635
18899
  message: `edit: "${input.path}" is not a regular file`,
18636
18900
  field: "path"
18637
18901
  });
@@ -18644,7 +18908,7 @@ var editTool = {
18644
18908
  const lastReadHash = ctx.lastReadHash?.(absPath);
18645
18909
  if (lastReadHash !== void 0) {
18646
18910
  if (lastReadHash !== originalHash) {
18647
- throw new ToolValidationError({
18911
+ throw new ToolValidationError3({
18648
18912
  message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
18649
18913
  field: "path",
18650
18914
  context: { reason: "external_modification" }
@@ -18653,15 +18917,15 @@ var editTool = {
18653
18917
  } else {
18654
18918
  const lastReadMtime = ctx.lastReadMtime(absPath);
18655
18919
  if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
18656
- throw new ToolValidationError({
18920
+ throw new ToolValidationError3({
18657
18921
  message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
18658
18922
  field: "path",
18659
18923
  context: { reason: "external_modification" }
18660
18924
  });
18661
18925
  }
18662
18926
  }
18663
- if (autoRead && updated.mtimeMs > stat18.mtimeMs + mtimeTolerance) {
18664
- throw new ToolValidationError({
18927
+ if (autoRead && updated.mtimeMs > stat19.mtimeMs + mtimeTolerance) {
18928
+ throw new ToolValidationError3({
18665
18929
  message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
18666
18930
  field: "path",
18667
18931
  context: { reason: "auto_read_race" }
@@ -18673,6 +18937,9 @@ var editTool = {
18673
18937
  const oldLf = normalizeToLf(input.old_string);
18674
18938
  const newLf = normalizeToLf(input.new_string);
18675
18939
  if (oldLf === newLf) {
18940
+ if (!fileLf.includes(oldLf)) {
18941
+ throw noMatchError(input.path, fileLf, oldLf);
18942
+ }
18676
18943
  if (autoRead) ctx.recordRead(absPath, updated.mtimeMs, "user", originalHash);
18677
18944
  return {
18678
18945
  path: absPath,
@@ -18686,26 +18953,20 @@ var editTool = {
18686
18953
  const ladder = findLadderMatches(fileLf, oldLf);
18687
18954
  if (!ladder) {
18688
18955
  opts?.signal?.throwIfAborted();
18689
- const hint = nearestMatchHint(fileLf, oldLf);
18690
- throw new ToolValidationError({
18691
- message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint.line}:
18692
- ${hint.snippet}
18693
- Compare this against your old_string and retry with the file's actual text.` : ""}`,
18694
- field: "old_string"
18695
- });
18956
+ throw noMatchError(input.path, fileLf, oldLf);
18696
18957
  }
18697
18958
  const { tier, matches } = ladder;
18698
18959
  const count = matches.length;
18699
18960
  if (ladder.ambiguous) {
18700
18961
  const lines = matches.map((m) => m.startLine);
18701
- throw new ToolValidationError({
18962
+ throw new ToolValidationError3({
18702
18963
  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.`,
18703
18964
  field: "old_string",
18704
18965
  context: { occurrences: count, matchTier: tier }
18705
18966
  });
18706
18967
  }
18707
18968
  if (input.replace_all && tier !== "exact" && tier !== "trailing-whitespace") {
18708
- throw new ToolValidationError({
18969
+ throw new ToolValidationError3({
18709
18970
  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.`,
18710
18971
  field: "old_string",
18711
18972
  context: { matchTier: tier }
@@ -18713,7 +18974,7 @@ Compare this against your old_string and retry with the file's actual text.` : "
18713
18974
  }
18714
18975
  if (count > 1 && !input.replace_all) {
18715
18976
  const lines = matches.map((m) => m.startLine);
18716
- throw new ToolValidationError({
18977
+ throw new ToolValidationError3({
18717
18978
  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.`,
18718
18979
  field: "old_string",
18719
18980
  context: { occurrences: count, matchTier: tier }
@@ -18754,16 +19015,20 @@ Compare this against your old_string and retry with the file's actual text.` : "
18754
19015
  after: newFile
18755
19016
  });
18756
19017
  opts?.signal?.throwIfAborted();
18757
- const diff = unifiedDiff(original, newFile, {
18758
- fromFile: input.path,
18759
- toFile: input.path
18760
- });
19018
+ const { text: diff, truncated: diffTruncated } = truncateDiffPayload(
19019
+ unifiedDiff(original, newFile, {
19020
+ fromFile: input.path,
19021
+ toFile: input.path
19022
+ }),
19023
+ MAX_DIFF_BYTES
19024
+ );
19025
+ const diffNote = diffTruncated ? "Diff truncated to the 256 KiB output budget \u2014 the full edit is on disk." : void 0;
18761
19026
  const syntax = await checkSyntax(absPath, newFile, original).catch(() => void 0);
18762
19027
  let syntaxNote;
18763
19028
  if (syntax && syntax.errors.length > 0) {
18764
19029
  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.`;
18765
19030
  }
18766
- const notes = [autoReadNote, tierNote, syntaxNote].filter(Boolean);
19031
+ const notes = [autoReadNote, tierNote, diffNote, syntaxNote].filter(Boolean);
18767
19032
  return {
18768
19033
  path: absPath,
18769
19034
  replacements: input.replace_all ? count : 1,
@@ -18774,6 +19039,15 @@ Compare this against your old_string and retry with the file's actual text.` : "
18774
19039
  };
18775
19040
  }
18776
19041
  };
19042
+ function noMatchError(inputPath, fileLf, oldLf) {
19043
+ const hint = nearestMatchHint(fileLf, oldLf);
19044
+ return new ToolValidationError3({
19045
+ message: `edit: no match for old_string in "${inputPath}".${hint ? ` Nearest match near line ${hint.line}:
19046
+ ${hint.snippet}
19047
+ Compare this against your old_string and retry with the file's actual text.` : ""}`,
19048
+ field: "old_string"
19049
+ });
19050
+ }
18777
19051
 
18778
19052
  // src/exec.ts
18779
19053
  import { spawn as spawn8 } from "node:child_process";
@@ -18782,7 +19056,7 @@ import {
18782
19056
  emitProcessOutput as emitProcessOutput3,
18783
19057
  emitProcessStarted as emitProcessStarted3
18784
19058
  } from "@wrongstack/core/observability";
18785
- import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils/error";
19059
+ import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils/error";
18786
19060
 
18787
19061
  // src/_danger-detect.ts
18788
19062
  var argHas = (args, value) => args.includes(value);
@@ -19114,7 +19388,7 @@ init_win32_resolve();
19114
19388
 
19115
19389
  // src/exec-kill-guard.ts
19116
19390
  import * as os8 from "node:os";
19117
- import * as path30 from "node:path";
19391
+ import * as path31 from "node:path";
19118
19392
  var isWin3 = os8.platform() === "win32";
19119
19393
  async function checkExecKillCommand(cmd, args) {
19120
19394
  if (!cmd) return { blocked: false };
@@ -19327,7 +19601,7 @@ async function checkKillTarget(target) {
19327
19601
  reason: `Blocked: kill ${target.signal} '${target.name}' targets a WrongStack process name.`
19328
19602
  };
19329
19603
  }
19330
- const currentImage = path30.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
19604
+ const currentImage = path31.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
19331
19605
  const targetsNodeRuntime = nameLower === "node" || nameLower.startsWith("node");
19332
19606
  if (targetsNodeRuntime && currentImage === "node") {
19333
19607
  return {
@@ -19949,6 +20223,7 @@ function isExecCommandAllowed(cmd) {
19949
20223
  var MAX_ARGS = 20;
19950
20224
  var MAX_OUTPUT2 = 2e5;
19951
20225
  var DEFAULT_TIMEOUT_MS3 = 3e4;
20226
+ var MAX_TIMEOUT_MS = 6e5;
19952
20227
  var BLOCKED_ARG_PATTERNS = {
19953
20228
  python: [],
19954
20229
  // git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack;
@@ -20074,8 +20349,8 @@ var SAFE_DANGER = { level: "safe", reasons: [] };
20074
20349
  var execTool = {
20075
20350
  name: "exec",
20076
20351
  category: "Shell",
20077
- 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.",
20078
- 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.",
20352
+ 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.",
20353
+ 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.",
20079
20354
  selection: {
20080
20355
  doNotUseWhen: "the operation requires pipes, redirection, shell expansion, or a non-allowlisted command.",
20081
20356
  useInstead: ["bash"]
@@ -20091,7 +20366,13 @@ var execTool = {
20091
20366
  subjectKey: "command",
20092
20367
  mutating: true,
20093
20368
  riskTier: "standard",
20094
- timeoutMs: DEFAULT_TIMEOUT_MS3,
20369
+ // Executor-level abort ceiling. Must sit ABOVE the per-call timeout ceiling
20370
+ // (MAX_TIMEOUT_MS): the tool's own timer resolves with exit 124 + registry
20371
+ // tree-kill; the executor's AbortSignal.timeout is a blunt abort that would
20372
+ // otherwise fire first and discard the structured timeout result. The 10s
20373
+ // margin covers the kill/teardown window. (The executor additionally clamps
20374
+ // to config `tools.maxToolTimeoutMs`.)
20375
+ timeoutMs: MAX_TIMEOUT_MS + 1e4,
20095
20376
  capabilities: ["shell.restricted"],
20096
20377
  icon: "terminal",
20097
20378
  inputSchema: {
@@ -20112,7 +20393,7 @@ var execTool = {
20112
20393
  },
20113
20394
  timeout: {
20114
20395
  type: "integer",
20115
- description: "Per-command timeout in milliseconds."
20396
+ description: "Per-command timeout in milliseconds (default 30000, max 600000)."
20116
20397
  }
20117
20398
  },
20118
20399
  required: ["command"]
@@ -20156,7 +20437,7 @@ var execTool = {
20156
20437
  };
20157
20438
  }
20158
20439
  const args = (input.args ?? []).slice(0, MAX_ARGS);
20159
- const timeout = Math.max(1, Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS3, DEFAULT_TIMEOUT_MS3));
20440
+ const timeout = Math.max(1, Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS3, MAX_TIMEOUT_MS));
20160
20441
  const danger = detectDanger(cmd, args, dangerBypass);
20161
20442
  const killCheck = await checkExecKillCommand(cmd, args);
20162
20443
  if (killCheck.blocked) {
@@ -20184,15 +20465,16 @@ var execTool = {
20184
20465
  danger
20185
20466
  };
20186
20467
  }
20468
+ const defaultCwd = ctx.workingDir ?? ctx.cwd;
20187
20469
  let cwd;
20188
20470
  try {
20189
- cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : await safeResolveReal(ctx.cwd, ctx);
20471
+ cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : await safeResolveReal(defaultCwd, ctx);
20190
20472
  } catch {
20191
20473
  return {
20192
20474
  command: cmd,
20193
20475
  args,
20194
20476
  stdout: "",
20195
- stderr: `cwd "${input.cwd ?? ctx.cwd}" resolves outside project root`,
20477
+ stderr: `cwd "${input.cwd ?? defaultCwd}" resolves outside project root`,
20196
20478
  exitCode: 1,
20197
20479
  truncated: false,
20198
20480
  allowed: false,
@@ -20204,7 +20486,7 @@ var execTool = {
20204
20486
  }
20205
20487
  };
20206
20488
  function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
20207
- return new Promise((resolve16) => {
20489
+ return new Promise((resolve17) => {
20208
20490
  let stdout = "";
20209
20491
  let stderr = "";
20210
20492
  let killed = false;
@@ -20212,7 +20494,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
20212
20494
  const finish = (result) => {
20213
20495
  if (resolvedOnce.value) return;
20214
20496
  resolvedOnce.value = true;
20215
- resolve16(result);
20497
+ resolve17(result);
20216
20498
  };
20217
20499
  const startedAt = Date.now();
20218
20500
  let stdoutBytes = 0;
@@ -20264,7 +20546,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
20264
20546
  command: cmd,
20265
20547
  args,
20266
20548
  stdout: "",
20267
- stderr: `spawn failed: ${toErrorMessage3(err)}`,
20549
+ stderr: `spawn failed: ${toErrorMessage6(err)}`,
20268
20550
  exitCode: 1,
20269
20551
  truncated: false,
20270
20552
  allowed: true,
@@ -20367,7 +20649,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
20367
20649
  }
20368
20650
 
20369
20651
  // src/fetch.ts
20370
- import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValidationError3 } from "@wrongstack/core/types";
20652
+ import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
20371
20653
  import TurndownService from "turndown";
20372
20654
 
20373
20655
  // src/_fetch-guard.ts
@@ -20377,7 +20659,7 @@ import {
20377
20659
  isPrivateIPv4 as isPrivateIPv42,
20378
20660
  isPrivateIPv6 as isPrivateIPv62
20379
20661
  } from "@wrongstack/core/utils";
20380
- import { FetchError, ToolValidationError as ToolValidationError2 } from "@wrongstack/core/types";
20662
+ import { FetchError, ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
20381
20663
  import { Agent, fetch as undiciFetch } from "undici";
20382
20664
  var nativeGlobalFetch = globalThis.fetch;
20383
20665
  var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
@@ -20448,13 +20730,13 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
20448
20730
  for (; ; ) {
20449
20731
  const parsed = new URL(currentUrl);
20450
20732
  if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
20451
- throw new ToolValidationError2({
20733
+ throw new ToolValidationError4({
20452
20734
  message: `fetch: redirect to unsupported protocol "${parsed.protocol}"`,
20453
20735
  field: "url"
20454
20736
  });
20455
20737
  }
20456
20738
  if (parsed.protocol === "http:" && !ALLOW_PRIVATE) {
20457
- throw new ToolValidationError2({
20739
+ throw new ToolValidationError4({
20458
20740
  message: "fetch: redirect to http:// blocked (HTTPS required by default)",
20459
20741
  field: "url"
20460
20742
  });
@@ -20470,6 +20752,10 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
20470
20752
  if (res.status < 300 || res.status > 399) {
20471
20753
  return res;
20472
20754
  }
20755
+ try {
20756
+ await res.body?.cancel();
20757
+ } catch {
20758
+ }
20473
20759
  redirectCount++;
20474
20760
  if (redirectCount > maxRedirects) {
20475
20761
  throw new FetchError({
@@ -20493,7 +20779,7 @@ async function assertNotPrivate(hostname2) {
20493
20779
  if (ALLOW_PRIVATE) return;
20494
20780
  const host = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
20495
20781
  if (host === "localhost" || host.endsWith(".localhost")) {
20496
- throw new ToolValidationError2({
20782
+ throw new ToolValidationError4({
20497
20783
  message: "fetch: blocked localhost target",
20498
20784
  field: "url"
20499
20785
  });
@@ -20501,14 +20787,14 @@ async function assertNotPrivate(hostname2) {
20501
20787
  const ipVersion = net4.isIP(host);
20502
20788
  if (ipVersion === 4) {
20503
20789
  if (isPrivateIPv42(host)) {
20504
- throw new ToolValidationError2({
20790
+ throw new ToolValidationError4({
20505
20791
  message: `fetch: blocked private/loopback address "${host}"`,
20506
20792
  field: "url"
20507
20793
  });
20508
20794
  }
20509
20795
  } else if (ipVersion === 6) {
20510
20796
  if (isPrivateIPv62(host)) {
20511
- throw new ToolValidationError2({
20797
+ throw new ToolValidationError4({
20512
20798
  message: `fetch: blocked private/loopback address "${host}"`,
20513
20799
  field: "url"
20514
20800
  });
@@ -20519,14 +20805,14 @@ async function assertNotPrivate(hostname2) {
20519
20805
  for (const r of records) {
20520
20806
  const bad = r.family === 4 ? isPrivateIPv42(r.address) : isPrivateIPv62(r.address);
20521
20807
  if (bad) {
20522
- throw new ToolValidationError2({
20808
+ throw new ToolValidationError4({
20523
20809
  message: `fetch: resolved to private address ${r.address}`,
20524
20810
  field: "url"
20525
20811
  });
20526
20812
  }
20527
20813
  }
20528
20814
  } catch (err) {
20529
- if (err instanceof ToolValidationError2) throw err;
20815
+ if (err instanceof ToolValidationError4) throw err;
20530
20816
  }
20531
20817
  }
20532
20818
  }
@@ -20543,6 +20829,8 @@ TD.addRule("stripDangerousElements", {
20543
20829
  filter: ["script", "style", "noscript"],
20544
20830
  replacement: () => ""
20545
20831
  });
20832
+ var PRUNED_BOILERPLATE_TAGS = /* @__PURE__ */ new Set(["nav", "header", "footer", "aside", "svg", "iframe"]);
20833
+ TD.remove((node) => PRUNED_BOILERPLATE_TAGS.has(node.nodeName.toLowerCase()));
20546
20834
  var MAX_BYTES = 131072;
20547
20835
  var TIMEOUT_MS = 2e4;
20548
20836
  var combineSignals = (signals) => AbortSignal.any(signals);
@@ -20572,7 +20860,7 @@ var fetchTool = {
20572
20860
  format: {
20573
20861
  type: "string",
20574
20862
  enum: ["markdown", "text", "raw"],
20575
- description: 'Output format. "markdown" is recommended for HTML pages.'
20863
+ description: 'Output format. "markdown" is recommended for HTML pages; for non-HTML content types it falls back to plain text (JSON is pretty-printed).'
20576
20864
  }
20577
20865
  },
20578
20866
  required: ["url"]
@@ -20601,20 +20889,26 @@ var fetchTool = {
20601
20889
  },
20602
20890
  async *executeStream(input, ctx, opts) {
20603
20891
  if (!input?.url) {
20604
- throw new ToolValidationError3({
20892
+ throw new ToolValidationError5({
20605
20893
  message: "fetch: url is required",
20606
20894
  field: "url"
20607
20895
  });
20608
20896
  }
20609
20897
  const u = new URL(input.url);
20898
+ if (u.username || u.password) {
20899
+ throw new ToolValidationError5({
20900
+ message: "fetch: URLs with embedded credentials (user:pass@host) are not allowed",
20901
+ field: "url"
20902
+ });
20903
+ }
20610
20904
  if (u.protocol !== "https:" && u.protocol !== "http:") {
20611
- throw new ToolValidationError3({
20905
+ throw new ToolValidationError5({
20612
20906
  message: `fetch: unsupported protocol "${u.protocol}"`,
20613
20907
  field: "url"
20614
20908
  });
20615
20909
  }
20616
20910
  if (u.protocol === "http:" && !ALLOW_PRIVATE) {
20617
- throw new ToolValidationError3({
20911
+ throw new ToolValidationError5({
20618
20912
  message: "fetch: http:// blocked (HTTPS required by default)",
20619
20913
  field: "url"
20620
20914
  });
@@ -20804,8 +21098,9 @@ var formatTool = {
20804
21098
  type: "final",
20805
21099
  output: {
20806
21100
  fixer: bridge.language,
20807
- files_checked: 0,
20808
- files_changed: run.summary.errors > 0 ? 0 : 1,
21101
+ // Language-bridge runs don't report per-file counts.
21102
+ files_checked: void 0,
21103
+ files_changed: void 0,
20809
21104
  output: normalizeCommandOutput(run.output || run.error || ""),
20810
21105
  truncated: run.truncated
20811
21106
  }
@@ -20832,11 +21127,14 @@ var formatTool = {
20832
21127
  text: `Running ${detected}\u2026`,
20833
21128
  data: { fixer: detected, check: !!input.check }
20834
21129
  };
20835
- const args = ["format", "--write"];
20836
- if (input.check) args[args.length - 1] = "--check";
20837
- if (input.files) {
20838
- const files = Array.isArray(input.files) ? input.files : input.files.split(",");
20839
- args.push("--", ...files.map((f) => f.trim()));
21130
+ const fileList = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((f) => f.trim()) : [];
21131
+ let args;
21132
+ if (detected === "prettier") {
21133
+ args = [input.check ? "--check" : "--write"];
21134
+ args.push(...fileList.length > 0 ? fileList : ["."]);
21135
+ } else {
21136
+ args = ["format", input.check ? "--check" : "--write"];
21137
+ if (fileList.length > 0) args.push("--", ...fileList);
20840
21138
  }
20841
21139
  const result = yield* spawnStream({
20842
21140
  cmd: detected,
@@ -20845,39 +21143,70 @@ var formatTool = {
20845
21143
  signal: opts.signal,
20846
21144
  maxBytes: 1e5
20847
21145
  });
20848
- const changed = [...result.stdout.matchAll(/\bchanged\b/gi)].length;
21146
+ const combinedOut = `${result.stdout}
21147
+ ${result.stderr}`;
21148
+ const counts = parseFormatterCounts(detected, combinedOut);
20849
21149
  yield {
20850
21150
  type: "final",
20851
21151
  output: {
20852
21152
  fixer: detected,
20853
- files_checked: 0,
20854
- files_changed: changed,
21153
+ files_checked: counts.checked,
21154
+ files_changed: counts.changed,
20855
21155
  output: normalizeCommandOutput(result.stdout || result.stderr || result.error || ""),
20856
21156
  truncated: result.truncated
20857
21157
  }
20858
21158
  };
20859
21159
  }
20860
21160
  };
21161
+ function parseFormatterCounts(fixer, output) {
21162
+ if (fixer !== "biome") return { checked: void 0, changed: void 0 };
21163
+ const checkedMatch = /\b(?:Checked|Formatted)\s+(\d+)\s+files?\b/i.exec(output);
21164
+ const changedMatch = /\bFixed\s+(\d+)\s+files?\b/i.exec(output);
21165
+ return {
21166
+ checked: checkedMatch?.[1] !== void 0 ? Number(checkedMatch[1]) : void 0,
21167
+ changed: changedMatch?.[1] !== void 0 ? Number(changedMatch[1]) : void 0
21168
+ };
21169
+ }
20861
21170
  async function detectFixer(cwd) {
20862
- const { stat: stat18 } = await import("node:fs/promises");
20863
- try {
20864
- await stat18(`${cwd}/biome.json`);
20865
- return "biome";
20866
- } catch {
21171
+ const fs35 = await import("node:fs/promises");
21172
+ const exists = async (file) => {
20867
21173
  try {
20868
- await stat18(`${cwd}/.prettierrc`);
20869
- return "prettier";
21174
+ await fs35.stat(`${cwd}/${file}`);
21175
+ return true;
20870
21176
  } catch {
20871
- return "biome";
21177
+ return false;
20872
21178
  }
21179
+ };
21180
+ if (await exists("biome.json") || await exists("biome.jsonc")) return "biome";
21181
+ const PRETTIER_CONFIGS = [
21182
+ ".prettierrc",
21183
+ ".prettierrc.json",
21184
+ ".prettierrc.yml",
21185
+ ".prettierrc.yaml",
21186
+ ".prettierrc.js",
21187
+ ".prettierrc.cjs",
21188
+ ".prettierrc.mjs",
21189
+ "prettier.config.js",
21190
+ "prettier.config.cjs",
21191
+ "prettier.config.mjs"
21192
+ ];
21193
+ for (const cfg of PRETTIER_CONFIGS) {
21194
+ if (await exists(cfg)) return "prettier";
20873
21195
  }
21196
+ try {
21197
+ const raw = await fs35.readFile(`${cwd}/package.json`, "utf8");
21198
+ const pkg = JSON.parse(raw);
21199
+ if (pkg["prettier"] !== void 0) return "prettier";
21200
+ } catch {
21201
+ }
21202
+ return "biome";
20874
21203
  }
20875
21204
 
20876
21205
  // src/git.ts
20877
21206
  init_util();
20878
21207
  import { spawn as spawn9 } from "node:child_process";
20879
21208
  import { statSync as statSync4 } from "node:fs";
20880
- import { dirname as dirname13, resolve as resolve13, sep as sep6 } from "node:path";
21209
+ import { dirname as dirname13, resolve as resolve14, sep as sep6 } from "node:path";
20881
21210
  import { assessCommitSafety } from "@wrongstack/core/coordination";
20882
21211
  import { buildChildEnv as buildChildEnv4 } from "@wrongstack/core/utils";
20883
21212
  var TIMEOUT_MS2 = 3e4;
@@ -21038,8 +21367,8 @@ function validateWorktreeInput(input, projectRoot) {
21038
21367
  return reject(`unsafe worktree path: ${input.worktreePath}`);
21039
21368
  }
21040
21369
  if ((input.worktreeAction === "add" || input.worktreeAction === "remove") && input.worktreePath) {
21041
- const root = resolve13(projectRoot);
21042
- const abs = resolve13(root, input.worktreePath);
21370
+ const root = resolve14(projectRoot);
21371
+ const abs = resolve14(root, input.worktreePath);
21043
21372
  if (abs !== root && !abs.startsWith(root + sep6)) {
21044
21373
  return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);
21045
21374
  }
@@ -21051,8 +21380,8 @@ function findGitDir2(cwd, projectRoot) {
21051
21380
  let dir = cwd;
21052
21381
  for (let i = 0; i < 20; i++) {
21053
21382
  try {
21054
- const stat18 = statSync4(`${dir}/.git`);
21055
- if (stat18.isDirectory() || stat18.isFile()) return dir;
21383
+ const stat19 = statSync4(`${dir}/.git`);
21384
+ if (stat19.isDirectory() || stat19.isFile()) return dir;
21056
21385
  } catch {
21057
21386
  }
21058
21387
  if (dir === root) break;
@@ -21133,7 +21462,7 @@ function buildArgs(input) {
21133
21462
  }
21134
21463
  }
21135
21464
  function runGit2(args, cwd, signal) {
21136
- return new Promise((resolve16) => {
21465
+ return new Promise((resolve17) => {
21137
21466
  let stdout = "";
21138
21467
  let stderr = "";
21139
21468
  const child = spawn9("git", args, {
@@ -21154,7 +21483,7 @@ function runGit2(args, cwd, signal) {
21154
21483
  }
21155
21484
  });
21156
21485
  child.on("error", (err) => {
21157
- resolve16({
21486
+ resolve17({
21158
21487
  command: args[0],
21159
21488
  stdout: normalizeCommandOutput(stdout),
21160
21489
  stderr: err.message,
@@ -21163,7 +21492,7 @@ function runGit2(args, cwd, signal) {
21163
21492
  });
21164
21493
  });
21165
21494
  child.on("close", (code) => {
21166
- resolve16({
21495
+ resolve17({
21167
21496
  command: args[0],
21168
21497
  stdout: normalizeCommandOutput(stdout),
21169
21498
  stderr: normalizeCommandOutput(stderr),
@@ -21176,8 +21505,9 @@ function runGit2(args, cwd, signal) {
21176
21505
 
21177
21506
  // src/glob.ts
21178
21507
  import * as fs25 from "node:fs/promises";
21179
- import * as path31 from "node:path";
21508
+ import * as path32 from "node:path";
21180
21509
  import { compileGlob as compileGlob2, DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS2 } from "@wrongstack/core/utils";
21510
+ import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
21181
21511
 
21182
21512
  // src/_concurrency.ts
21183
21513
  async function mapWithConcurrency2(items, limit, fn) {
@@ -21203,7 +21533,7 @@ var WALK_CONCURRENCY = 16;
21203
21533
  var globTool = {
21204
21534
  name: "glob",
21205
21535
  category: "Filesystem",
21206
- description: "Find files by path pattern. Use index-backed `codebase-search` first for code symbols or concepts when it is live.",
21536
+ 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.",
21207
21537
  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.",
21208
21538
  selection: {
21209
21539
  doNotUseWhen: "you need to search inside file contents.",
@@ -21214,7 +21544,7 @@ var globTool = {
21214
21544
  capabilities: ["fs.read"],
21215
21545
  icon: "folder",
21216
21546
  maxOutputBytes: 65536,
21217
- timeoutMs: 5e3,
21547
+ timeoutMs: 15e3,
21218
21548
  inputSchema: {
21219
21549
  type: "object",
21220
21550
  properties: {
@@ -21228,13 +21558,20 @@ var globTool = {
21228
21558
  },
21229
21559
  limit: {
21230
21560
  type: "integer",
21231
- description: "Maximum number of results to return (default 1000, max 5000)."
21561
+ minimum: 1,
21562
+ maximum: 5e3,
21563
+ 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."
21232
21564
  }
21233
21565
  },
21234
21566
  required: ["pattern"]
21235
21567
  },
21236
21568
  async execute(input, ctx, opts) {
21237
- if (!input?.pattern) throw new Error("glob: pattern is required");
21569
+ if (!input?.pattern) {
21570
+ throw new ToolValidationError6({
21571
+ message: "glob: pattern is required",
21572
+ field: "pattern"
21573
+ });
21574
+ }
21238
21575
  const signal = opts?.signal;
21239
21576
  const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
21240
21577
  const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
@@ -21279,7 +21616,7 @@ var globTool = {
21279
21616
  const name = e.name;
21280
21617
  if (DEFAULT_IGNORE2.has(name)) continue;
21281
21618
  const rel = relPrefix ? `${relPrefix}/${name}` : name;
21282
- const full = path31.join(dir, name);
21619
+ const full = path32.join(dir, name);
21283
21620
  if (e.isDirectory()) {
21284
21621
  if (isGitIgnored(rel, true)) continue;
21285
21622
  subdirs.push({ full, rel });
@@ -21329,8 +21666,8 @@ var globTool = {
21329
21666
  // src/grep.ts
21330
21667
  import { spawn as spawn10 } from "node:child_process";
21331
21668
  import * as fs26 from "node:fs/promises";
21332
- import * as path32 from "node:path";
21333
- import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
21669
+ import * as path33 from "node:path";
21670
+ import { ToolValidationError as ToolValidationError7 } from "@wrongstack/core/types";
21334
21671
  import {
21335
21672
  buildChildEnv as buildChildEnv5,
21336
21673
  compileGlob as compileGlob3,
@@ -21534,7 +21871,7 @@ var grepTool = {
21534
21871
  },
21535
21872
  async *executeStream(input, ctx, opts) {
21536
21873
  if (!input?.pattern) {
21537
- throw new ToolValidationError4({
21874
+ throw new ToolValidationError7({
21538
21875
  message: "grep: pattern is required",
21539
21876
  field: "pattern"
21540
21877
  });
@@ -21544,12 +21881,12 @@ var grepTool = {
21544
21881
  const limit = Math.max(1, Math.min(input.limit ?? 200, 2e3));
21545
21882
  const validation = compileUserRegex(input.pattern, input.case_insensitive ? "i" : "");
21546
21883
  if (!validation.ok) {
21547
- throw new ToolValidationError4({
21884
+ throw new ToolValidationError7({
21548
21885
  message: `grep: ${validation.reason}`,
21549
21886
  field: "pattern"
21550
21887
  });
21551
21888
  }
21552
- const rgAvailable = await detectRg(opts.signal);
21889
+ const rgAvailable = await detectRg();
21553
21890
  if (rgAvailable) {
21554
21891
  try {
21555
21892
  yield* runRgStream(input, base, mode, limit, opts.signal);
@@ -21562,16 +21899,23 @@ var grepTool = {
21562
21899
  yield { type: "final", output: out };
21563
21900
  }
21564
21901
  };
21565
- async function detectRg(signal) {
21566
- return new Promise((resolve16) => {
21902
+ var rgAvailabilityCache;
21903
+ function detectRg() {
21904
+ rgAvailabilityCache ??= new Promise((resolve17) => {
21567
21905
  try {
21568
- const p = spawn10("rg", ["--version"], { env: buildChildEnv5(), stdio: "ignore", signal, windowsHide: true });
21569
- p.on("error", () => resolve16(false));
21570
- p.on("close", (code) => resolve16(code === 0));
21906
+ const p = spawn10("rg", ["--version"], {
21907
+ env: buildChildEnv5(),
21908
+ stdio: "ignore",
21909
+ signal: AbortSignal.timeout(1e4),
21910
+ windowsHide: true
21911
+ });
21912
+ p.on("error", () => resolve17(false));
21913
+ p.on("close", (code) => resolve17(code === 0));
21571
21914
  } catch {
21572
- resolve16(false);
21915
+ resolve17(false);
21573
21916
  }
21574
21917
  });
21918
+ return rgAvailabilityCache;
21575
21919
  }
21576
21920
  async function* runRgStream(input, base, mode, limit, signal) {
21577
21921
  const args = ["--no-heading"];
@@ -21585,7 +21929,7 @@ async function* runRgStream(input, base, mode, limit, signal) {
21585
21929
  for (const ignored of DEFAULT_IGNORE3) {
21586
21930
  args.push("--glob", `!${ignored}/**`, "--glob", `!**/${ignored}/**`);
21587
21931
  }
21588
- const gitignorePath = path32.join(base, ".gitignore");
21932
+ const gitignorePath = path33.join(base, ".gitignore");
21589
21933
  if (await fs26.access(gitignorePath).then(() => true, () => false)) {
21590
21934
  args.push("--ignore-file", gitignorePath);
21591
21935
  }
@@ -21757,7 +22101,7 @@ async function runNative(input, base, mode, limit, signal) {
21757
22101
  const flags = input.case_insensitive ? "i" : "";
21758
22102
  const compiled = compileUserRegex(input.pattern, flags);
21759
22103
  if (!compiled.ok) {
21760
- throw new ToolValidationError4({
22104
+ throw new ToolValidationError7({
21761
22105
  message: `grep: ${compiled.reason}`,
21762
22106
  field: "pattern"
21763
22107
  });
@@ -21775,8 +22119,8 @@ async function runNative(input, base, mode, limit, signal) {
21775
22119
  if (globRe && !globRe.test(name) && !globRe.test(full)) return;
21776
22120
  if (globRe) globRe.lastIndex = 0;
21777
22121
  try {
21778
- const stat18 = await fs26.stat(full);
21779
- if (!stat18.isFile() || stat18.size > maxBytes || stopped || signal.aborted) return;
22122
+ const stat19 = await fs26.stat(full);
22123
+ if (!stat19.isFile() || stat19.size > maxBytes || stopped || signal.aborted) return;
21780
22124
  const file = await fs26.open(full, "r");
21781
22125
  try {
21782
22126
  let bytesReadTotal = 0;
@@ -21867,7 +22211,7 @@ async function runNative(input, base, mode, limit, signal) {
21867
22211
  if (DEFAULT_IGNORE3.has(e.name)) continue;
21868
22212
  if (e.isSymbolicLink()) continue;
21869
22213
  const rel = relPrefix ? `${relPrefix}/${e.name}` : e.name;
21870
- const full = path32.join(dir, e.name);
22214
+ const full = path33.join(dir, e.name);
21871
22215
  if (e.isDirectory()) {
21872
22216
  if (isGitIgnored(rel, true)) continue;
21873
22217
  subdirs.push({ full, rel });
@@ -21987,26 +22331,14 @@ var installTool = {
21987
22331
  return;
21988
22332
  }
21989
22333
  }
21990
- const pkgManager = await detectPackageManager(cwd);
22334
+ const pkgManager = await detectPackageManager(cwd, ctx.projectRoot);
21991
22335
  yield { type: "log", text: `Resolving with ${pkgManager}\u2026`, data: { phase: "resolve" } };
21992
- const save = input.save === "dev" ? "-D" : input.save === "optional" ? "-O" : "";
21993
22336
  const globalFlag = input.global ? ["-g"] : [];
21994
22337
  const ignoreScripts = input.lifecycleScripts !== true;
21995
- const args = [];
21996
- if (input.dry_run) args.push("--dry-run");
21997
- if (ignoreScripts) args.push("--ignore-scripts");
21998
- if (pkgManager === "pnpm") {
21999
- if (save) args.push(save);
22000
- args.push("add", ...globalFlag);
22001
- } else if (pkgManager === "yarn") {
22002
- args.push("add", ...globalFlag);
22003
- } else {
22004
- args.push("install", ...globalFlag);
22005
- }
22006
22338
  const pkgList = input.packages ? (Array.isArray(input.packages) ? input.packages : input.packages.split(",")).map(
22007
22339
  (p) => p.trim()
22008
22340
  ) : [];
22009
- const PKG_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i;
22341
+ const PKG_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9^~><=*.+-]+)?$/i;
22010
22342
  for (const pkg of pkgList) {
22011
22343
  if (!PKG_NAME_RE.test(pkg) || pkg.startsWith("-") || pkg.length > 200) {
22012
22344
  yield {
@@ -22022,7 +22354,34 @@ var installTool = {
22022
22354
  return;
22023
22355
  }
22024
22356
  }
22025
- if (pkgList.length > 0) args.push(...pkgList);
22357
+ const hasPkgs = pkgList.length > 0;
22358
+ const args = [];
22359
+ if (input.dry_run) args.push("--dry-run");
22360
+ if (ignoreScripts) args.push("--ignore-scripts");
22361
+ if (pkgManager === "pnpm") {
22362
+ if (hasPkgs) {
22363
+ if (input.save === "dev") args.push("-D");
22364
+ else if (input.save === "optional") args.push("-O");
22365
+ args.push("add", ...globalFlag);
22366
+ } else {
22367
+ args.push("install", ...globalFlag);
22368
+ }
22369
+ } else if (pkgManager === "yarn") {
22370
+ if (hasPkgs) {
22371
+ args.push("add", ...globalFlag);
22372
+ if (input.save === "dev") args.push("--dev");
22373
+ else if (input.save === "optional") args.push("--optional");
22374
+ } else {
22375
+ args.push("install", ...globalFlag);
22376
+ }
22377
+ } else {
22378
+ args.push("install", ...globalFlag);
22379
+ if (hasPkgs) {
22380
+ if (input.save === "dev") args.push("--save-dev");
22381
+ else if (input.save === "optional") args.push("--save-optional");
22382
+ }
22383
+ }
22384
+ if (hasPkgs) args.push(...pkgList);
22026
22385
  yield {
22027
22386
  type: "log",
22028
22387
  text: `Fetching ${pkgList.length || "all"} packages\u2026`,
@@ -22103,9 +22462,9 @@ var JsonFileTooLargeError = class extends Error {
22103
22462
  };
22104
22463
  async function readJsonFileBounded(filePath, ctx) {
22105
22464
  const resolved = await safeResolveReal(filePath, ctx);
22106
- const stat18 = await fs27.stat(resolved);
22107
- if (stat18.size > MAX_JSON_FILE_BYTES) {
22108
- throw new JsonFileTooLargeError(filePath, stat18.size);
22465
+ const stat19 = await fs27.stat(resolved);
22466
+ if (stat19.size > MAX_JSON_FILE_BYTES) {
22467
+ throw new JsonFileTooLargeError(filePath, stat19.size);
22109
22468
  }
22110
22469
  return fs27.readFile(resolved, "utf8");
22111
22470
  }
@@ -22511,60 +22870,60 @@ function jmespathSearch(data, query) {
22511
22870
  }
22512
22871
  function validateJsonSchema(data, schema) {
22513
22872
  const errors = [];
22514
- function check(value, s, path39) {
22873
+ function check(value, s, path40) {
22515
22874
  if (s["type"]) {
22516
22875
  const expectedType = s["type"];
22517
22876
  const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
22518
22877
  if (expectedType === "integer") {
22519
- if (!Number.isInteger(value)) errors.push(`${path39}: expected integer, got ${actualType}`);
22878
+ if (!Number.isInteger(value)) errors.push(`${path40}: expected integer, got ${actualType}`);
22520
22879
  } else if (expectedType !== actualType) {
22521
- errors.push(`${path39}: expected ${expectedType}, got ${actualType}`);
22880
+ errors.push(`${path40}: expected ${expectedType}, got ${actualType}`);
22522
22881
  }
22523
22882
  }
22524
22883
  if (typeof value === "string" && s["format"] === "uri" && value) {
22525
22884
  try {
22526
22885
  new URL(value);
22527
22886
  } catch {
22528
- errors.push(`${path39}: not a valid URI`);
22887
+ errors.push(`${path40}: not a valid URI`);
22529
22888
  }
22530
22889
  }
22531
22890
  if (typeof value === "string" && s["pattern"]) {
22532
22891
  const compiled = compileUserRegex(s["pattern"], "");
22533
22892
  if (!compiled.ok) {
22534
- errors.push(`${path39}: invalid schema pattern \u2014 ${compiled.reason}`);
22893
+ errors.push(`${path40}: invalid schema pattern \u2014 ${compiled.reason}`);
22535
22894
  } else if (!compiled.regex.test(capSubject(value))) {
22536
- errors.push(`${path39}: does not match pattern ${s["pattern"]}`);
22895
+ errors.push(`${path40}: does not match pattern ${s["pattern"]}`);
22537
22896
  }
22538
22897
  }
22539
22898
  if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
22540
- errors.push(`${path39}: string too short (min ${s["minLength"]})`);
22899
+ errors.push(`${path40}: string too short (min ${s["minLength"]})`);
22541
22900
  }
22542
22901
  if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
22543
- errors.push(`${path39}: string too long (max ${s["maxLength"]})`);
22902
+ errors.push(`${path40}: string too long (max ${s["maxLength"]})`);
22544
22903
  }
22545
22904
  if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
22546
- errors.push(`${path39}: below minimum ${s["minimum"]}`);
22905
+ errors.push(`${path40}: below minimum ${s["minimum"]}`);
22547
22906
  }
22548
22907
  if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
22549
- errors.push(`${path39}: above maximum ${s["maximum"]}`);
22908
+ errors.push(`${path40}: above maximum ${s["maximum"]}`);
22550
22909
  }
22551
22910
  if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
22552
22911
  for (let i = 0; i < value.length; i++) {
22553
- check(value[i], s["items"], `${path39}[${i}]`);
22912
+ check(value[i], s["items"], `${path40}[${i}]`);
22554
22913
  }
22555
22914
  }
22556
22915
  if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
22557
22916
  const props = s["properties"];
22558
22917
  for (const [k, propSchema] of Object.entries(props)) {
22559
- check(value[k], propSchema, `${path39}.${k}`);
22918
+ check(value[k], propSchema, `${path40}.${k}`);
22560
22919
  }
22561
22920
  }
22562
22921
  }
22563
22922
  check(data, schema, "$");
22564
22923
  return { valid: errors.length === 0, errors };
22565
22924
  }
22566
- function simpleQuery(data, path39) {
22567
- const parts = path39.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
22925
+ function simpleQuery(data, path40) {
22926
+ const parts = path40.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
22568
22927
  let current = data;
22569
22928
  for (const part of parts) {
22570
22929
  if (current === null || current === void 0) return void 0;
@@ -23475,6 +23834,18 @@ var KANBAN_INPUT_SCHEMA = {
23475
23834
  },
23476
23835
  transitionAction: { type: "string" },
23477
23836
  transitionComment: { type: "string" },
23837
+ tickChecks: {
23838
+ type: "array",
23839
+ items: {
23840
+ type: "object",
23841
+ properties: {
23842
+ checkId: { type: "string" },
23843
+ checkStatus: { type: "string", enum: ["passed", "failed", "skipped"] }
23844
+ },
23845
+ required: ["checkId", "checkStatus"]
23846
+ },
23847
+ 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."
23848
+ },
23478
23849
  attachmentUrl: { type: "string" },
23479
23850
  attachmentTitle: { type: "string" },
23480
23851
  attachmentType: {
@@ -24076,6 +24447,7 @@ function sourceStatus(task) {
24076
24447
  function todoStatus(task) {
24077
24448
  const status = sourceStatus(task);
24078
24449
  if (status === "completed") return "completed";
24450
+ if (status === "review" && task.assignment?.status === "completed") return "completed";
24079
24451
  if (status === "in_progress" || status === "review") return "in_progress";
24080
24452
  return "pending";
24081
24453
  }
@@ -24175,6 +24547,9 @@ var kanbanTool = {
24175
24547
  description: KANBAN_TOOL_DESCRIPTION,
24176
24548
  usageHint: KANBAN_TOOL_USAGE_HINT,
24177
24549
  permission: "confirm",
24550
+ // WS-046: gives permission decisions something to key on.
24551
+ // The action performed; kanban has no single file or path subject.
24552
+ subjectKey: "action",
24178
24553
  mutating: true,
24179
24554
  capabilities: ["fs.write"],
24180
24555
  icon: "task",
@@ -24616,6 +24991,7 @@ var kanbanTool = {
24616
24991
  actor: input.author,
24617
24992
  comment: input.transitionComment,
24618
24993
  ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
24994
+ ...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
24619
24995
  ...input.attachmentUrl !== void 0 ? {
24620
24996
  attachment: {
24621
24997
  url: input.attachmentUrl,
@@ -25008,8 +25384,52 @@ var kanbanTool = {
25008
25384
  } catch (err) {
25009
25385
  return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
25010
25386
  }
25387
+ },
25388
+ serialize(output, input) {
25389
+ return serializeKanbanOutput(output, input);
25011
25390
  }
25012
25391
  };
25392
+ var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
25393
+ var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
25394
+ "get_board",
25395
+ "export_markdown",
25396
+ "export_task_graph"
25397
+ ]);
25398
+ function serializeKanbanOutput(output, input) {
25399
+ const action = input && typeof input === "object" ? input.action : void 0;
25400
+ const board = output.board;
25401
+ if (board) {
25402
+ const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
25403
+ let boardBytes = 0;
25404
+ if (!keepFull) {
25405
+ try {
25406
+ boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
25407
+ } catch {
25408
+ boardBytes = 0;
25409
+ }
25410
+ }
25411
+ if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
25412
+ const columns = {};
25413
+ for (const column of board.columns) {
25414
+ columns[column.title || column.id] = board.tasks.filter(
25415
+ (task) => task.columnId === column.id
25416
+ ).length;
25417
+ }
25418
+ const compact = {
25419
+ ...output,
25420
+ board: {
25421
+ id: board.id,
25422
+ title: board.title,
25423
+ columns,
25424
+ totalTasks: board.tasks.length,
25425
+ note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
25426
+ }
25427
+ };
25428
+ return JSON.stringify(compact, null, 2);
25429
+ }
25430
+ }
25431
+ return JSON.stringify(output, null, 2);
25432
+ }
25013
25433
 
25014
25434
  // src/builtin.ts
25015
25435
  init_execute_tool();
@@ -25124,11 +25544,11 @@ var lintTool = {
25124
25544
  }
25125
25545
  };
25126
25546
  async function detectLinter(cwd) {
25127
- const { stat: stat18 } = await import("node:fs/promises");
25547
+ const { stat: stat19 } = await import("node:fs/promises");
25128
25548
  const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
25129
25549
  for (const f of checks) {
25130
25550
  try {
25131
- await stat18(`${cwd}/${f}`);
25551
+ await stat19(`${cwd}/${f}`);
25132
25552
  if (f.includes("biome")) return "biome";
25133
25553
  if (f.includes("eslint")) return "eslint";
25134
25554
  if (f.includes("tslint")) return "tslint";
@@ -25145,11 +25565,12 @@ init_util();
25145
25565
  var logsTool = {
25146
25566
  name: "logs",
25147
25567
  category: "Logs",
25148
- description: "Read or stream logs from files, Docker containers, or systemd services. Useful for debugging running applications.",
25149
- 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.",
25568
+ description: "Read logs from files or Docker containers. Useful for debugging running applications.",
25569
+ 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.",
25150
25570
  permission: "confirm",
25151
25571
  mutating: false,
25152
25572
  timeoutMs: 3e4,
25573
+ maxOutputBytes: 262144,
25153
25574
  capabilities: ["shell.restricted"],
25154
25575
  icon: "logs",
25155
25576
  inputSchema: {
@@ -25157,7 +25578,7 @@ var logsTool = {
25157
25578
  properties: {
25158
25579
  service: {
25159
25580
  type: "string",
25160
- description: "Service name for Docker or systemd journal"
25581
+ description: "Docker container name (passed to `docker logs`)"
25161
25582
  },
25162
25583
  path: {
25163
25584
  type: "string",
@@ -25169,10 +25590,6 @@ var logsTool = {
25169
25590
  minimum: 0,
25170
25591
  maximum: 1e4
25171
25592
  },
25172
- stream: {
25173
- type: "boolean",
25174
- description: "Stream logs continuously (like tail -f) (default: false)"
25175
- },
25176
25593
  filter: {
25177
25594
  type: "string",
25178
25595
  description: "Regex pattern to filter log lines"
@@ -25180,7 +25597,7 @@ var logsTool = {
25180
25597
  since: {
25181
25598
  type: "string",
25182
25599
  enum: ["1h", "6h", "24h", "all"],
25183
- description: "Only show logs since duration"
25600
+ description: 'Only show Docker logs since duration (ignored for files; "all" = no limit)'
25184
25601
  },
25185
25602
  cwd: { type: "string", description: "Working directory (default: cwd)" }
25186
25603
  }
@@ -25197,10 +25614,10 @@ var logsTool = {
25197
25614
  filterRe = compiled.regex;
25198
25615
  }
25199
25616
  if (input.service) {
25200
- return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal);
25617
+ return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal, input.since);
25201
25618
  }
25202
25619
  if (input.path) {
25203
- return await fileLogs(safeResolve(input.path, ctx), lines, filterRe, input.stream ?? false);
25620
+ return await fileLogs(await safeResolveReal(input.path, ctx), lines, filterRe);
25204
25621
  }
25205
25622
  return {
25206
25623
  source: "none",
@@ -25214,7 +25631,7 @@ var logsTool = {
25214
25631
  async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
25215
25632
  const args = ["logs"];
25216
25633
  if (lines > 0) args.push("--tail", String(lines));
25217
- if (since) {
25634
+ if (since && since !== "all") {
25218
25635
  const sinceMap = { "1h": "1h", "6h": "6h", "24h": "24h" };
25219
25636
  args.push("--since", sinceMap[since] ?? "1h");
25220
25637
  }
@@ -25228,7 +25645,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
25228
25645
  };
25229
25646
  }
25230
25647
  args.push("--timestamps", service);
25231
- return new Promise((resolve16) => {
25648
+ return new Promise((resolve17) => {
25232
25649
  let stdout = "";
25233
25650
  let stderr = "";
25234
25651
  const MAX = 2e5;
@@ -25244,7 +25661,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
25244
25661
  if (settled) return;
25245
25662
  settled = true;
25246
25663
  clearTimeout(timer);
25247
- resolve16(result);
25664
+ resolve17(result);
25248
25665
  };
25249
25666
  const child = spawn11("docker", args, { cwd, signal, env: buildChildEnv6(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
25250
25667
  const timer = setTimeout(() => {
@@ -25279,7 +25696,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
25279
25696
  }
25280
25697
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
25281
25698
  var MAX_TAIL_LINES = 1e5;
25282
- async function fileLogs(path39, lines, filterRe, stream) {
25699
+ async function fileLogs(path40, lines, filterRe) {
25283
25700
  const { createInterface } = await import("node:readline");
25284
25701
  const { createReadStream: createReadStream2 } = await import("node:fs");
25285
25702
  const entries = [];
@@ -25288,7 +25705,7 @@ async function fileLogs(path39, lines, filterRe, stream) {
25288
25705
  let writeIdx = 0;
25289
25706
  let totalLines = 0;
25290
25707
  const rl = createInterface({
25291
- input: createReadStream2(path39),
25708
+ input: createReadStream2(path40),
25292
25709
  crlfDelay: Number.POSITIVE_INFINITY
25293
25710
  });
25294
25711
  for await (const line of rl) {
@@ -25309,11 +25726,11 @@ async function fileLogs(path39, lines, filterRe, stream) {
25309
25726
  if (parsed) entries.push(parsed);
25310
25727
  }
25311
25728
  return {
25312
- source: path39,
25729
+ source: path40,
25313
25730
  entries,
25314
25731
  total: entries.length,
25315
25732
  truncated: totalLines > effLines,
25316
- stream_mode: stream
25733
+ stream_mode: false
25317
25734
  };
25318
25735
  }
25319
25736
  function parseLogLines(output, filterRe) {
@@ -25389,25 +25806,12 @@ var outdatedTool = {
25389
25806
  inputSchema: {
25390
25807
  type: "object",
25391
25808
  properties: {
25392
- cwd: { type: "string", description: "Working directory (default: cwd)" },
25393
- format: {
25394
- type: "string",
25395
- enum: ["list", "table"],
25396
- description: "Output format (default: list)"
25397
- },
25398
- include_deprecated: {
25399
- type: "boolean",
25400
- description: "Include deprecated packages (default: false)"
25401
- },
25402
- check: {
25403
- type: "string",
25404
- description: "Specific package(s) to check (comma-separated)"
25405
- }
25809
+ cwd: { type: "string", description: "Working directory (default: cwd)" }
25406
25810
  }
25407
25811
  },
25408
25812
  async execute(input, ctx, opts) {
25409
25813
  const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
25410
- const manager = await detectPackageManager(cwd);
25814
+ const manager = await detectPackageManager(cwd, ctx.projectRoot);
25411
25815
  if (manager === "npm") {
25412
25816
  try {
25413
25817
  const { detectNonJsEcosystem: detectNonJsEcosystem2 } = await Promise.resolve().then(() => (init_legacy_bridge(), legacy_bridge_exports));
@@ -25460,13 +25864,11 @@ var outdatedTool = {
25460
25864
  }
25461
25865
  }
25462
25866
  const args = ["outdated", "--json"];
25463
- if (input.format === "table") args.push("--table");
25464
- if (input.include_deprecated) args.push("--include", "deprecated");
25465
25867
  return runOutdated(manager, args, cwd, opts.signal);
25466
25868
  }
25467
25869
  };
25468
25870
  function runOutdated(manager, args, cwd, signal) {
25469
- return new Promise((resolve16) => {
25871
+ return new Promise((resolve17) => {
25470
25872
  let stdout = "";
25471
25873
  let stderr = "";
25472
25874
  const MAX = 1e5;
@@ -25491,10 +25893,10 @@ function runOutdated(manager, args, cwd, signal) {
25491
25893
  });
25492
25894
  child.on("close", (code) => {
25493
25895
  const result = parseOutdatedOutput(stdout, code ?? 0);
25494
- resolve16(result);
25896
+ resolve17(result);
25495
25897
  });
25496
25898
  child.on("error", (e) => {
25497
- resolve16({
25899
+ resolve17({
25498
25900
  exit_code: 1,
25499
25901
  packages: [],
25500
25902
  total: 0,
@@ -25515,27 +25917,39 @@ function parseOutdatedOutput(json2, exitCode) {
25515
25917
  truncated: false
25516
25918
  };
25517
25919
  }
25920
+ const truncated = json2.length >= 1e5 || Buffer.byteLength(json2, "utf8") > COMMAND_OUTPUT_MAX_BYTES;
25921
+ let parsedOk = false;
25518
25922
  try {
25519
25923
  const data = JSON.parse(json2);
25924
+ parsedOk = true;
25520
25925
  for (const name of Object.keys(data)) {
25521
- const info = data[name];
25926
+ const info = data[name] ?? {};
25927
+ const str = (v) => typeof v === "string" ? v : void 0;
25522
25928
  packages.push({
25523
25929
  name,
25524
- current: info.current ?? "unknown",
25525
- latest: info.latest ?? "unknown",
25526
- wanted: info.wanted ?? "unknown",
25527
- type: info.type ?? "unknown",
25528
- location: info.location ?? name
25930
+ current: str(info["current"]) ?? "unknown",
25931
+ latest: str(info["latest"]) ?? "unknown",
25932
+ wanted: str(info["wanted"]) ?? "unknown",
25933
+ // npm calls it `type`; pnpm calls it `dependencyType`.
25934
+ type: str(info["type"]) ?? str(info["dependencyType"]) ?? "unknown",
25935
+ location: str(info["location"]) ?? name
25529
25936
  });
25530
25937
  }
25531
25938
  } catch {
25939
+ }
25940
+ const outdatedFound = parsedOk && exitCode === 1;
25941
+ let output = normalizeCommandOutput(json2);
25942
+ if (outdatedFound) {
25943
+ output = `${output}
25944
+
25945
+ Note: exit code 1 from \`outdated\` means outdated packages were found (expected); treated as success.`;
25532
25946
  }
25533
25947
  return {
25534
- exit_code: exitCode,
25948
+ exit_code: outdatedFound ? 0 : exitCode,
25535
25949
  packages,
25536
25950
  total: packages.length,
25537
- output: json2,
25538
- truncated: json2.length >= 1e5
25951
+ output,
25952
+ truncated
25539
25953
  };
25540
25954
  }
25541
25955
 
@@ -25544,8 +25958,8 @@ init_util();
25544
25958
  import { spawn as spawn13 } from "node:child_process";
25545
25959
  import * as fs28 from "node:fs/promises";
25546
25960
  import * as os9 from "node:os";
25547
- import * as path33 from "node:path";
25548
- import { buildChildEnv as buildChildEnv8, toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
25961
+ import * as path34 from "node:path";
25962
+ import { buildChildEnv as buildChildEnv8, toErrorMessage as toErrorMessage7 } from "@wrongstack/core/utils";
25549
25963
  var patchTool = {
25550
25964
  name: "patch",
25551
25965
  category: "Filesystem",
@@ -25589,26 +26003,26 @@ var patchTool = {
25589
26003
  try {
25590
26004
  dir = input.directory ? await safeResolveReal(input.directory, ctx) : ctx.cwd;
25591
26005
  } catch (err) {
25592
- return refuse(`patch refused: ${toErrorMessage4(err)}`);
26006
+ return refuse(`patch refused: ${toErrorMessage7(err)}`);
25593
26007
  }
25594
- const realRoot = await fs28.realpath(ctx.projectRoot).catch(() => path33.resolve(ctx.projectRoot));
26008
+ const realRoot = await fs28.realpath(ctx.projectRoot).catch(() => path34.resolve(ctx.projectRoot));
25595
26009
  const targets = extractDiffTargets(input.patch);
25596
26010
  const resolvedTargets = [];
25597
26011
  for (const t of targets) {
25598
26012
  const stripped = stripPathComponents(t.raw, strip);
25599
26013
  if (!stripped) continue;
25600
- if (path33.isAbsolute(stripped)) {
26014
+ if (path34.isAbsolute(stripped)) {
25601
26015
  return refuse(`patch refused: target "${t.raw}" strips to absolute path`);
25602
26016
  }
25603
- const candidate = path33.resolve(dir, stripped);
26017
+ const candidate = path34.resolve(dir, stripped);
25604
26018
  let real;
25605
26019
  try {
25606
26020
  real = await resolveRealInsideRoot(candidate, ctx);
25607
26021
  } catch (err) {
25608
- return refuse(`patch refused: target "${t.raw}" ${toErrorMessage4(err)}`);
26022
+ return refuse(`patch refused: target "${t.raw}" ${toErrorMessage7(err)}`);
25609
26023
  }
25610
- const rel = path33.relative(realRoot, real);
25611
- if (rel.startsWith("..") || path33.isAbsolute(rel)) {
26024
+ const rel = path34.relative(realRoot, real);
26025
+ if (rel.startsWith("..") || path34.isAbsolute(rel)) {
25612
26026
  return refuse(`patch refused: target "${t.raw}" resolves outside project root`);
25613
26027
  }
25614
26028
  resolvedTargets.push({ raw: t.raw, deleted: t.deleted, abs: real });
@@ -25622,11 +26036,11 @@ var patchTool = {
25622
26036
  beforeContents.set(target.abs, await readTextForTracking(target.abs));
25623
26037
  }
25624
26038
  }
25625
- const tmpDir = await fs28.mkdtemp(path33.join(os9.tmpdir(), ".wstack_patch_"));
26039
+ const tmpDir = await fs28.mkdtemp(path34.join(os9.tmpdir(), ".wstack_patch_"));
25626
26040
  try {
25627
26041
  await fs28.chmod(tmpDir, 448).catch(() => {
25628
26042
  });
25629
- const patchFile = path33.join(tmpDir, "in.diff");
26043
+ const patchFile = path34.join(tmpDir, "in.diff");
25630
26044
  await fs28.writeFile(patchFile, input.patch, { mode: 384 });
25631
26045
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
25632
26046
  const result = await runPatch(args, dir, opts.signal, {
@@ -25639,8 +26053,8 @@ var patchTool = {
25639
26053
  for (const target of resolvedTargets) {
25640
26054
  const abs = target.abs;
25641
26055
  const before = beforeContents.get(abs) ?? null;
25642
- const stat18 = await fs28.stat(abs).catch(() => null);
25643
- if (!stat18?.isFile()) {
26056
+ const stat19 = await fs28.stat(abs).catch(() => null);
26057
+ if (!stat19?.isFile()) {
25644
26058
  if (beforeExisted.has(abs)) {
25645
26059
  touched.push(abs);
25646
26060
  ctx.session?.recordFileChange?.({
@@ -25655,7 +26069,7 @@ var patchTool = {
25655
26069
  const after = await readTextForTracking(abs);
25656
26070
  if (after === null || after === before) continue;
25657
26071
  touched.push(abs);
25658
- ctx.recordRead?.(abs, stat18.mtimeMs, "write", sha256hex(after));
26072
+ ctx.recordRead?.(abs, stat19.mtimeMs, "write", sha256hex(after));
25659
26073
  ctx.session?.recordFileChange?.({
25660
26074
  path: abs,
25661
26075
  action: before === null ? "created" : "modified",
@@ -25666,7 +26080,7 @@ var patchTool = {
25666
26080
  }
25667
26081
  if (result.exitCode !== 0) {
25668
26082
  if (!dryRun) {
25669
- 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(", ")}.` : "";
26083
+ 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(", ")}.` : "";
25670
26084
  return {
25671
26085
  applied: touched.length,
25672
26086
  rejected: 1,
@@ -25674,7 +26088,7 @@ var patchTool = {
25674
26088
  // success path (which returns GNU patch's dir-relative names).
25675
26089
  // `touched` entries are realpaths from resolveRealInsideRoot, and
25676
26090
  // realRoot is also a realpath, so path.relative is like-for-like.
25677
- files: touched.map((p) => path33.relative(realRoot, p) || p),
26091
+ files: touched.map((p) => path34.relative(realRoot, p) || p),
25678
26092
  dry_run: dryRun,
25679
26093
  message: `patch failed: ${result.stderr || result.stdout}${partial}`
25680
26094
  };
@@ -25690,7 +26104,7 @@ var patchTool = {
25690
26104
  }
25691
26105
  const patched = result.engine === "git" ? [
25692
26106
  ...new Set(
25693
- resolvedTargets.map((target) => path33.relative(dir, target.abs) || target.abs)
26107
+ resolvedTargets.map((target) => path34.relative(dir, target.abs) || target.abs)
25694
26108
  )
25695
26109
  ] : extractPatchedFiles(result.stdout);
25696
26110
  return {
@@ -25709,8 +26123,8 @@ var patchTool = {
25709
26123
  var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
25710
26124
  async function readTextForTracking(absPath) {
25711
26125
  try {
25712
- const stat18 = await fs28.stat(absPath);
25713
- if (!stat18.isFile() || stat18.size > MAX_TRACKING_BYTES) return null;
26126
+ const stat19 = await fs28.stat(absPath);
26127
+ if (!stat19.isFile() || stat19.size > MAX_TRACKING_BYTES) return null;
25714
26128
  const buf = await fs28.readFile(absPath);
25715
26129
  if (buf.includes(0)) return null;
25716
26130
  return buf.toString("utf8");
@@ -25795,7 +26209,7 @@ function runPatch(args, cwd, signal, fallback) {
25795
26209
  });
25796
26210
  }
25797
26211
  function runPatchProcess(command, args, cwd, signal) {
25798
- return new Promise((resolve16) => {
26212
+ return new Promise((resolve17) => {
25799
26213
  let stdout = "";
25800
26214
  let stderr = "";
25801
26215
  const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
@@ -25814,11 +26228,11 @@ function runPatchProcess(command, args, cwd, signal) {
25814
26228
  });
25815
26229
  child.on(
25816
26230
  "close",
25817
- (code) => resolve16({ exitCode: code ?? 1, stdout, stderr, unavailable: false })
26231
+ (code) => resolve17({ exitCode: code ?? 1, stdout, stderr, unavailable: false })
25818
26232
  );
25819
26233
  child.on(
25820
26234
  "error",
25821
- (e) => resolve16({
26235
+ (e) => resolve17({
25822
26236
  exitCode: 1,
25823
26237
  stdout: "",
25824
26238
  stderr: e.message,
@@ -25985,6 +26399,20 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
25985
26399
  }
25986
26400
  const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
25987
26401
  if (!task || task.status === "completed") continue;
26402
+ const stage = task.lifecycle?.currentStage;
26403
+ if (stage === "backlog" || stage === "todo") {
26404
+ const started = await execute({
26405
+ action: "start_task",
26406
+ boardId: board.id,
26407
+ taskId: task.id,
26408
+ author: actor,
26409
+ agentId: actor,
26410
+ transitionComment: `Auto-started for completion: ${item.content}`
26411
+ });
26412
+ if (!started.ok) {
26413
+ continue;
26414
+ }
26415
+ }
25988
26416
  await execute({
25989
26417
  action: "mark_assignment",
25990
26418
  boardId: board.id,
@@ -26196,7 +26624,8 @@ var todoTool = {
26196
26624
  }
26197
26625
  for (const planId of completedPlanIds) {
26198
26626
  if (pendingPlanIds.has(planId)) continue;
26199
- const planPath = ctx.meta["plan.path"];
26627
+ const meta = ctx.meta;
26628
+ const planPath = meta["plan.path.resolved"] ?? meta["plan.path"];
26200
26629
  if (typeof planPath !== "string" || !planPath) continue;
26201
26630
  try {
26202
26631
  const plan = await loadPlan2(planPath);
@@ -26209,7 +26638,8 @@ var todoTool = {
26209
26638
  }
26210
26639
  for (const taskId of completedTaskIds) {
26211
26640
  if (pendingTaskIds.has(taskId)) continue;
26212
- const taskPath = ctx.meta["task.path"];
26641
+ const meta = ctx.meta;
26642
+ const taskPath = meta["task.path.resolved"] ?? meta["task.path"];
26213
26643
  if (typeof taskPath !== "string" || !taskPath) continue;
26214
26644
  try {
26215
26645
  const file = await loadTasks3(taskPath);
@@ -26325,7 +26755,16 @@ var planTool = {
26325
26755
  sessionPlanPath.lastIndexOf("/"),
26326
26756
  sessionPlanPath.lastIndexOf("\\")
26327
26757
  );
26328
- planPath = lastSep >= 0 ? sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
26758
+ if (lastSep < 0) {
26759
+ return {
26760
+ ok: false,
26761
+ message: `Cannot derive the project-scoped plan path: session plan path "${sessionPlanPath}" has no directory component.`,
26762
+ plan: "",
26763
+ count: 0,
26764
+ open: 0
26765
+ };
26766
+ }
26767
+ planPath = sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json";
26329
26768
  }
26330
26769
  } else {
26331
26770
  planPath = sessionPlanPath;
@@ -26518,6 +26957,7 @@ var planTool = {
26518
26957
  open: 0
26519
26958
  };
26520
26959
  }
26960
+ ctx.meta["plan.path.resolved"] = planPath;
26521
26961
  await projectSessionPlanToKanban(ctx.projectRoot, plan.items, sessionId);
26522
26962
  if (todosToReplace) {
26523
26963
  await todoTool.execute({ todos: todosToReplace }, ctx, {
@@ -26550,6 +26990,7 @@ var planTool = {
26550
26990
  });
26551
26991
  return f;
26552
26992
  });
26993
+ ctx.meta["task.path.resolved"] = taskPath;
26553
26994
  return mkResult(
26554
26995
  plan,
26555
26996
  true,
@@ -26583,14 +27024,14 @@ function mkResult(plan, ok, message, todos) {
26583
27024
  // src/read.ts
26584
27025
  init_util();
26585
27026
  import * as fs29 from "node:fs/promises";
26586
- import { FsError, ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
26587
- import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
27027
+ import { FsError, ToolValidationError as ToolValidationError8 } from "@wrongstack/core/types";
27028
+ import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
26588
27029
  var ADVANCED_MODE_META_KEY = "tools.read.advancedMode";
26589
27030
  var MAX_BYTES2 = 5 * 1024 * 1024;
26590
27031
  var readTool = {
26591
27032
  name: "read",
26592
27033
  category: "Filesystem",
26593
- 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).",
27034
+ 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).",
26594
27035
  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.",
26595
27036
  selection: {
26596
27037
  doNotUseWhen: "you need to search many files for matching content.",
@@ -26611,11 +27052,13 @@ var readTool = {
26611
27052
  },
26612
27053
  offset: {
26613
27054
  type: "integer",
27055
+ minimum: 1,
26614
27056
  description: "1-based starting line number. Use together with `limit` for large files."
26615
27057
  },
26616
27058
  limit: {
26617
27059
  type: "integer",
26618
- description: "Maximum number of lines to return (default is 2000)."
27060
+ minimum: 0,
27061
+ description: "Maximum number of lines to return (default 2000). Values above 5000 are clamped to 5000 \u2014 page with `offset` for more."
26619
27062
  },
26620
27063
  mode: {
26621
27064
  type: "string",
@@ -26631,16 +27074,16 @@ var readTool = {
26631
27074
  },
26632
27075
  async execute(input, ctx, execOpts) {
26633
27076
  if (!input?.path) {
26634
- throw new ToolValidationError5({
27077
+ throw new ToolValidationError8({
26635
27078
  message: "read: path is required",
26636
27079
  field: "path"
26637
27080
  });
26638
27081
  }
26639
27082
  const absPath = await safeResolveReal(input.path, ctx);
26640
27083
  const shouldIncludeSymbols = input.includeSymbols === true || input.includeSymbols !== false && ctx.meta[ADVANCED_MODE_META_KEY] === true;
26641
- let stat18;
27084
+ let stat19;
26642
27085
  try {
26643
- stat18 = await fs29.stat(absPath);
27086
+ stat19 = await fs29.stat(absPath);
26644
27087
  } catch (err) {
26645
27088
  const code = err.code;
26646
27089
  if (code === "ENOENT") {
@@ -26652,14 +27095,14 @@ var readTool = {
26652
27095
  });
26653
27096
  }
26654
27097
  throw new FsError({
26655
- message: `read: failed to stat "${input.path}": ${toErrorMessage5(err)}`,
27098
+ message: `read: failed to stat "${input.path}": ${toErrorMessage8(err)}`,
26656
27099
  code: "FS_READ_FAILED",
26657
27100
  path: absPath,
26658
27101
  context: { errno: code },
26659
27102
  cause: err
26660
27103
  });
26661
27104
  }
26662
- if (!stat18.isFile()) {
27105
+ if (!stat19.isFile()) {
26663
27106
  throw new FsError({
26664
27107
  message: `read: "${input.path}" is not a regular file`,
26665
27108
  code: "FS_READ_FAILED",
@@ -26667,23 +27110,23 @@ var readTool = {
26667
27110
  context: { reason: "not-a-regular-file" }
26668
27111
  });
26669
27112
  }
26670
- if (stat18.size > MAX_BYTES2) {
27113
+ if (stat19.size > MAX_BYTES2) {
26671
27114
  throw new FsError({
26672
- message: `read: file too large (${stat18.size} bytes, limit ${MAX_BYTES2})`,
27115
+ message: `read: file too large (${stat19.size} bytes, limit ${MAX_BYTES2})`,
26673
27116
  code: "FS_READ_FAILED",
26674
27117
  path: absPath,
26675
- context: { size: stat18.size, limit: MAX_BYTES2, reason: "too-large" }
27118
+ context: { size: stat19.size, limit: MAX_BYTES2, reason: "too-large" }
26676
27119
  });
26677
27120
  }
26678
27121
  const offset = Math.max(1, input.offset ?? 1);
26679
27122
  const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
26680
27123
  const prior = getReadRangeRecord(ctx, absPath);
26681
27124
  const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
26682
- if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat18.mtimeMs, offset, requestedEnd)) {
26683
- ctx.recordRead(absPath, stat18.mtimeMs);
27125
+ if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat19.mtimeMs, offset, requestedEnd)) {
27126
+ ctx.recordRead(absPath, stat19.mtimeMs);
26684
27127
  const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
26685
27128
  return {
26686
- text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat18.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
27129
+ 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.]`,
26687
27130
  total_lines: prior.totalLines,
26688
27131
  encoding: "utf8",
26689
27132
  truncated: requestedEnd < prior.totalLines,
@@ -26694,18 +27137,23 @@ var readTool = {
26694
27137
  }
26695
27138
  const buf = await fs29.readFile(absPath);
26696
27139
  if (isBinaryBuffer(buf)) {
26697
- throw new Error(`read: "${input.path}" appears to be binary`);
27140
+ throw new FsError({
27141
+ message: `read: "${input.path}" appears to be binary`,
27142
+ code: "FS_READ_FAILED",
27143
+ path: absPath,
27144
+ context: { reason: "binary" }
27145
+ });
26698
27146
  }
26699
27147
  const text = buf.toString("utf8");
26700
27148
  const contentHash = sha256hex(text);
26701
27149
  const allLines = text.split(/\r\n|\r|\n/);
26702
27150
  const total = allLines.length;
26703
27151
  if (input.mode === "summary") {
26704
- ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
26705
- rememberReadRange(ctx, absPath, stat18.mtimeMs, total, 1, Math.min(total, 200));
27152
+ ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
27153
+ rememberReadRange(ctx, absPath, stat19.mtimeMs, total, 1, Math.min(total, 200));
26706
27154
  const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
26707
27155
  return {
26708
- text: summarizeFile(input.path, stat18.size, allLines),
27156
+ text: summarizeFile(input.path, stat19.size, allLines),
26709
27157
  total_lines: total,
26710
27158
  encoding: "utf8",
26711
27159
  truncated: total > 200,
@@ -26717,8 +27165,8 @@ var readTool = {
26717
27165
  };
26718
27166
  }
26719
27167
  if (limit === 0) {
26720
- ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
26721
- rememberReadRange(ctx, absPath, stat18.mtimeMs, total, 1, 0);
27168
+ ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
27169
+ rememberReadRange(ctx, absPath, stat19.mtimeMs, total, 1, 0);
26722
27170
  const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
26723
27171
  return {
26724
27172
  text: "",
@@ -26730,8 +27178,8 @@ var readTool = {
26730
27178
  };
26731
27179
  }
26732
27180
  if (offset > total) {
26733
- ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
26734
- rememberReadRange(ctx, absPath, stat18.mtimeMs, total, total + 1, total + 1);
27181
+ ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
27182
+ rememberReadRange(ctx, absPath, stat19.mtimeMs, total, total + 1, total + 1);
26735
27183
  const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
26736
27184
  return {
26737
27185
  text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
@@ -26746,8 +27194,8 @@ var readTool = {
26746
27194
  const truncated = offset - 1 + slice.length < total;
26747
27195
  const width = String(offset + slice.length - 1).length;
26748
27196
  const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
26749
- ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
26750
- rememberReadRange(ctx, absPath, stat18.mtimeMs, total, offset, offset + slice.length - 1);
27197
+ ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
27198
+ rememberReadRange(ctx, absPath, stat19.mtimeMs, total, offset, offset + slice.length - 1);
26751
27199
  const symResult = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
26752
27200
  return {
26753
27201
  text: numbered,
@@ -26855,8 +27303,8 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
26855
27303
  // src/replace.ts
26856
27304
  import { spawn as spawn14 } from "node:child_process";
26857
27305
  import * as fs30 from "node:fs/promises";
26858
- import * as path34 from "node:path";
26859
- import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
27306
+ import * as path35 from "node:path";
27307
+ import { ToolValidationError as ToolValidationError9 } from "@wrongstack/core/types";
26860
27308
  import {
26861
27309
  atomicWrite as atomicWrite3,
26862
27310
  buildChildEnv as buildChildEnv9,
@@ -26868,12 +27316,13 @@ import {
26868
27316
  unifiedDiff as unifiedDiff2
26869
27317
  } from "@wrongstack/core/utils";
26870
27318
  init_util();
27319
+ var MAX_DIFF_BYTES2 = 262144;
26871
27320
  var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
26872
27321
  var replaceTool = {
26873
27322
  name: "replace",
26874
27323
  category: "Transform",
26875
27324
  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.",
26876
- 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.",
27325
+ 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.",
26877
27326
  permission: "confirm",
26878
27327
  // WS-046: gives permission decisions something to key on.
26879
27328
  // The file scope being rewritten, not the pattern: a trust rule should say
@@ -26883,11 +27332,15 @@ var replaceTool = {
26883
27332
  capabilities: ["fs.write"],
26884
27333
  icon: "edit",
26885
27334
  timeoutMs: 3e4,
27335
+ maxOutputBytes: 262144,
26886
27336
  inputSchema: {
26887
27337
  type: "object",
26888
27338
  properties: {
26889
27339
  pattern: { type: "string", description: "Regex pattern to match" },
26890
- replacement: { type: "string", description: "Replacement string" },
27340
+ replacement: {
27341
+ type: "string",
27342
+ description: "Replacement string. Supports `$1`\u2013`$9` (capture groups), `$&` (whole match), and `$$` (literal dollar sign) \u2014 same semantics as JavaScript String.replace."
27343
+ },
26891
27344
  files: {
26892
27345
  type: "string",
26893
27346
  description: "File(s) to target: single path, comma-separated list, or glob pattern"
@@ -26903,19 +27356,19 @@ var replaceTool = {
26903
27356
  },
26904
27357
  async execute(input, ctx) {
26905
27358
  if (!input?.pattern) {
26906
- throw new ToolValidationError6({
27359
+ throw new ToolValidationError9({
26907
27360
  message: "replace: pattern is required",
26908
27361
  field: "pattern"
26909
27362
  });
26910
27363
  }
26911
27364
  if (input.replacement === void 0) {
26912
- throw new ToolValidationError6({
27365
+ throw new ToolValidationError9({
26913
27366
  message: "replace: replacement is required",
26914
27367
  field: "replacement"
26915
27368
  });
26916
27369
  }
26917
27370
  if (!input?.files) {
26918
- throw new ToolValidationError6({
27371
+ throw new ToolValidationError9({
26919
27372
  message: "replace: files is required",
26920
27373
  field: "files"
26921
27374
  });
@@ -26923,7 +27376,7 @@ var replaceTool = {
26923
27376
  const replaceAll = input.replace_all ?? true;
26924
27377
  const compiled = compileUserRegex(input.pattern, "g");
26925
27378
  if (!compiled.ok) {
26926
- throw new ToolValidationError6({
27379
+ throw new ToolValidationError9({
26927
27380
  message: `replace: ${compiled.reason}`,
26928
27381
  field: "pattern"
26929
27382
  });
@@ -26936,6 +27389,9 @@ var replaceTool = {
26936
27389
  const realRoot = await fs30.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
26937
27390
  const results = [];
26938
27391
  let totalReplacements = 0;
27392
+ let diffBytesUsed = 0;
27393
+ let diffsOmitted = 0;
27394
+ let diffsTruncated = 0;
26939
27395
  for (const absPath of fileList) {
26940
27396
  const lstat2 = await fs30.lstat(absPath).catch((err) => {
26941
27397
  if (err.code === "ENOENT") return null;
@@ -26949,10 +27405,10 @@ var replaceTool = {
26949
27405
  } catch {
26950
27406
  continue;
26951
27407
  }
26952
- const rel = path34.relative(realRoot, realPath);
26953
- if (rel.startsWith("..") || path34.isAbsolute(rel)) continue;
26954
- const stat18 = await fs30.stat(realPath).catch(() => null);
26955
- if (!stat18?.isFile()) continue;
27408
+ const rel = path35.relative(realRoot, realPath);
27409
+ if (rel.startsWith("..") || path35.isAbsolute(rel)) continue;
27410
+ const stat19 = await fs30.stat(realPath).catch(() => null);
27411
+ if (!stat19?.isFile()) continue;
26956
27412
  let content;
26957
27413
  try {
26958
27414
  const buf = await fs30.readFile(realPath);
@@ -26971,13 +27427,13 @@ var replaceTool = {
26971
27427
  let newContentLf = contentLf;
26972
27428
  for (let i = matches.length - 1; i >= 0; i--) {
26973
27429
  const m = expectDefined8(matches[i]);
26974
- newContentLf = newContentLf.slice(0, m.index) + input.replacement + newContentLf.slice(expectDefined8(m.index) + m[0].length);
27430
+ newContentLf = newContentLf.slice(0, m.index) + expandReplacement(input.replacement, m) + newContentLf.slice(expectDefined8(m.index) + m[0].length);
26975
27431
  }
26976
27432
  re.lastIndex = 0;
26977
27433
  totalReplacements += count;
26978
27434
  if (!dryRun) {
26979
27435
  const newContent = toStyle2(newContentLf, style);
26980
- await atomicWrite3(realPath, newContent, { mode: stat18.mode & 511 });
27436
+ await atomicWrite3(realPath, newContent, { mode: stat19.mode & 511 });
26981
27437
  const written = await fs30.stat(realPath).catch(() => null);
26982
27438
  if (written) {
26983
27439
  ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
@@ -26989,24 +27445,76 @@ var replaceTool = {
26989
27445
  after: newContent
26990
27446
  });
26991
27447
  }
26992
- const diff = dryRun || matches.length > 0 ? unifiedDiff2(content, toStyle2(newContentLf, style), {
27448
+ let diff = dryRun || matches.length > 0 ? unifiedDiff2(content, toStyle2(newContentLf, style), {
26993
27449
  fromFile: absPath,
26994
27450
  toFile: absPath
26995
27451
  }) : void 0;
27452
+ if (diff !== void 0) {
27453
+ const remaining = MAX_DIFF_BYTES2 - diffBytesUsed;
27454
+ if (remaining <= 0) {
27455
+ diff = void 0;
27456
+ diffsOmitted++;
27457
+ } else {
27458
+ const capped = truncateDiffPayload(diff, remaining);
27459
+ if (capped.truncated) diffsTruncated++;
27460
+ diff = capped.text;
27461
+ diffBytesUsed += Buffer.byteLength(diff, "utf8");
27462
+ }
27463
+ }
26996
27464
  results.push({
26997
27465
  path: absPath,
26998
27466
  replacements: matches.length,
26999
27467
  diff
27000
27468
  });
27001
27469
  }
27470
+ const overBudget = diffsOmitted > 0 || diffsTruncated > 0;
27002
27471
  return {
27003
27472
  files_modified: results.length,
27004
27473
  total_replacements: totalReplacements,
27005
27474
  results,
27006
- dry_run: dryRun
27475
+ dry_run: dryRun,
27476
+ 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
27007
27477
  };
27008
27478
  }
27009
27479
  };
27480
+ function expandReplacement(template, match) {
27481
+ if (!template.includes("$")) return template;
27482
+ let out = "";
27483
+ for (let i = 0; i < template.length; i++) {
27484
+ const ch = template[i];
27485
+ if (ch !== "$") {
27486
+ out += ch;
27487
+ continue;
27488
+ }
27489
+ const next = template[i + 1];
27490
+ if (next === "$") {
27491
+ out += "$";
27492
+ i++;
27493
+ } else if (next === "&") {
27494
+ out += match[0];
27495
+ i++;
27496
+ } else if (next !== void 0 && next >= "1" && next <= "9") {
27497
+ const idx = next.charCodeAt(0) - 48;
27498
+ if (idx < match.length) {
27499
+ out += match[idx] ?? "";
27500
+ i++;
27501
+ } else {
27502
+ out += "$";
27503
+ }
27504
+ } else {
27505
+ out += "$";
27506
+ }
27507
+ }
27508
+ return out;
27509
+ }
27510
+ function passesExtraGlob(extraGlob, name, full) {
27511
+ extraGlob.lastIndex = 0;
27512
+ const nameMatch = extraGlob.test(name);
27513
+ extraGlob.lastIndex = 0;
27514
+ const fullMatch = extraGlob.test(full);
27515
+ extraGlob.lastIndex = 0;
27516
+ return nameMatch || fullMatch;
27517
+ }
27010
27518
  async function resolveFiles2(filesInput, ctx, extraGlob) {
27011
27519
  const base = ctx.cwd;
27012
27520
  const normalized = filesInput.trim();
@@ -27017,8 +27525,9 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
27017
27525
  const resolved = [];
27018
27526
  for (const p of parts) {
27019
27527
  const absPath = safeResolve(p, ctx);
27020
- const stat18 = await fs30.stat(absPath).catch(() => null);
27021
- if (stat18?.isFile()) {
27528
+ if (extraGlob && !passesExtraGlob(extraGlob, path35.basename(absPath), absPath)) continue;
27529
+ const stat19 = await fs30.stat(absPath).catch(() => null);
27530
+ if (stat19?.isFile()) {
27022
27531
  resolved.push(absPath);
27023
27532
  }
27024
27533
  }
@@ -27029,26 +27538,32 @@ async function globFiles(pattern, base, extraGlob) {
27029
27538
  if (rgAvailable) {
27030
27539
  try {
27031
27540
  const { promise } = spawnRgFind(pattern, base);
27032
- return await promise;
27541
+ const files = await promise;
27542
+ if (extraGlob) {
27543
+ return files.filter((f) => passesExtraGlob(extraGlob, path35.basename(f), f));
27544
+ }
27545
+ return files;
27033
27546
  } catch {
27034
27547
  }
27035
27548
  }
27036
27549
  return await globNative(pattern, base, extraGlob);
27037
27550
  }
27551
+ var rgAvailabilityCache2;
27038
27552
  function checkRg() {
27039
- return new Promise((resolve16) => {
27553
+ rgAvailabilityCache2 ??= new Promise((resolve17) => {
27040
27554
  try {
27041
27555
  const p = spawn14("rg", ["--version"], {
27042
27556
  env: buildChildEnv9(),
27043
27557
  stdio: "ignore",
27044
27558
  windowsHide: true
27045
27559
  });
27046
- p.on("error", () => resolve16(false));
27047
- p.on("close", (code) => resolve16(code === 0));
27560
+ p.on("error", () => resolve17(false));
27561
+ p.on("close", (code) => resolve17(code === 0));
27048
27562
  } catch {
27049
- resolve16(false);
27563
+ resolve17(false);
27050
27564
  }
27051
27565
  });
27566
+ return rgAvailabilityCache2;
27052
27567
  }
27053
27568
  function spawnRgFind(pattern, base) {
27054
27569
  const args = ["--files", "--glob", pattern, base];
@@ -27071,10 +27586,10 @@ function spawnRgFind(pattern, base) {
27071
27586
  }
27072
27587
  });
27073
27588
  return {
27074
- promise: new Promise((resolve16, reject) => {
27589
+ promise: new Promise((resolve17, reject) => {
27075
27590
  child.on("error", reject);
27076
27591
  child.on("close", () => {
27077
- resolve16(buf.split("\n").filter(Boolean));
27592
+ resolve17(buf.split("\n").filter(Boolean));
27078
27593
  });
27079
27594
  })
27080
27595
  };
@@ -27091,10 +27606,10 @@ async function globNative(pattern, base, extraGlob) {
27091
27606
  }
27092
27607
  for (const e of entries) {
27093
27608
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
27094
- const full = path34.join(dir, e.name);
27609
+ const full = path35.join(dir, e.name);
27095
27610
  try {
27096
- const stat18 = await fs30.lstat(full);
27097
- if (stat18.isSymbolicLink()) continue;
27611
+ const stat19 = await fs30.lstat(full);
27612
+ if (stat19.isSymbolicLink()) continue;
27098
27613
  } catch {
27099
27614
  continue;
27100
27615
  }
@@ -27118,7 +27633,7 @@ async function globNative(pattern, base, extraGlob) {
27118
27633
  // src/scaffold.ts
27119
27634
  init_util();
27120
27635
  import * as fs31 from "node:fs/promises";
27121
- import * as path35 from "node:path";
27636
+ import * as path36 from "node:path";
27122
27637
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
27123
27638
  var BUILT_IN_TEMPLATES = {
27124
27639
  "npm-package": {
@@ -27269,16 +27784,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
27269
27784
  let filesCreated = 0;
27270
27785
  for (const [filePath, content] of Object.entries(templateFiles)) {
27271
27786
  const resolvedPath = substituteVars(filePath, name, vars);
27272
- const joinedPath = path35.join(cwd, resolvedPath);
27273
- const root = path35.resolve(ctx.projectRoot);
27274
- const target = path35.resolve(joinedPath);
27275
- const rel = path35.relative(root, target);
27276
- if (rel.startsWith("..") || path35.isAbsolute(rel)) {
27787
+ const joinedPath = path36.join(cwd, resolvedPath);
27788
+ const root = path36.resolve(ctx.projectRoot);
27789
+ const target = path36.resolve(joinedPath);
27790
+ const rel = path36.relative(root, target);
27791
+ if (rel.startsWith("..") || path36.isAbsolute(rel)) {
27277
27792
  throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
27278
27793
  }
27279
27794
  const fullPath = target;
27280
27795
  if (!dryRun) {
27281
- await fs31.mkdir(path35.dirname(fullPath), { recursive: true });
27796
+ await fs31.mkdir(path36.dirname(fullPath), { recursive: true });
27282
27797
  await atomicWrite4(fullPath, substituteVars(content, name, vars));
27283
27798
  }
27284
27799
  files.push(resolvedPath);
@@ -27309,11 +27824,12 @@ function substituteVars(content, name, vars) {
27309
27824
  }
27310
27825
 
27311
27826
  // src/search.ts
27312
- import { FetchError as FetchError3, ToolValidationError as ToolValidationError7 } from "@wrongstack/core/types";
27827
+ import { FetchError as FetchError3, ToolValidationError as ToolValidationError10 } from "@wrongstack/core/types";
27313
27828
  import { expectDefined as expectDefined9 } from "@wrongstack/core/utils";
27314
- import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
27829
+ import { toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
27315
27830
  var DEFAULT_NUM = 10;
27316
27831
  var MAX_RESULTS = 50;
27832
+ var MAX_SNIPPET_CHARS = 300;
27317
27833
  var TIMEOUT_MS3 = 15e3;
27318
27834
  var CACHE_TTL_MS = 3e5;
27319
27835
  var CACHE_MAX_ENTRIES = 200;
@@ -27321,7 +27837,7 @@ var cache = /* @__PURE__ */ new Map();
27321
27837
  var searchTool = {
27322
27838
  name: "search",
27323
27839
  category: "Search",
27324
- 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.",
27840
+ 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.",
27325
27841
  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.",
27326
27842
  permission: "auto",
27327
27843
  mutating: false,
@@ -27362,7 +27878,7 @@ var searchTool = {
27362
27878
  },
27363
27879
  async *executeStream(input, _ctx, opts) {
27364
27880
  if (!input?.query || input.query.trim() === "") {
27365
- throw new ToolValidationError7({
27881
+ throw new ToolValidationError10({
27366
27882
  message: "search: query is required and must be a non-empty string",
27367
27883
  field: "query"
27368
27884
  });
@@ -27395,7 +27911,7 @@ var searchTool = {
27395
27911
  query: input.query,
27396
27912
  results: results.slice(0, num),
27397
27913
  source: entry.source,
27398
- truncated: results.length >= num,
27914
+ truncated: results.length > num,
27399
27915
  cached: true
27400
27916
  }
27401
27917
  };
@@ -27407,41 +27923,45 @@ var searchTool = {
27407
27923
  text: `Querying ${source} for "${input.query}"\u2026`,
27408
27924
  data: { source, query: input.query, cached: false }
27409
27925
  };
27410
- let rawResults;
27926
+ let engine;
27411
27927
  let effectiveSource = source;
27412
27928
  switch (source) {
27413
27929
  case "duckduckgo":
27414
- rawResults = await duckduckgoSearch(input.query, num, opts.signal);
27930
+ engine = await duckduckgoSearch(input.query, opts.signal);
27415
27931
  break;
27416
27932
  case "google":
27417
- rawResults = await googleSearch(input.query, num, opts.signal);
27933
+ engine = await googleSearch(input.query, opts.signal);
27418
27934
  break;
27419
27935
  case "bing":
27420
- rawResults = await bingSearch(input.query, num, opts.signal);
27936
+ engine = await bingSearch(input.query, opts.signal);
27421
27937
  break;
27422
27938
  default:
27423
- throw new ToolValidationError7({
27939
+ throw new ToolValidationError10({
27424
27940
  message: `search: unknown source "${source}"`,
27425
27941
  field: "source"
27426
27942
  });
27427
27943
  }
27428
- let ranked = rankSearchResults(rawResults, input.query);
27944
+ let ranked = rankSearchResults(engine.results, input.query);
27945
+ let engineError = engine.error;
27429
27946
  if (source !== "duckduckgo" && shouldFallbackToDuckDuckGo(ranked, input.query)) {
27430
27947
  yield {
27431
27948
  type: "log",
27432
27949
  text: `${source} returned no relevant static results; falling back to duckduckgo`,
27433
27950
  data: { source, fallback: "duckduckgo", query: input.query }
27434
27951
  };
27435
- rawResults = await duckduckgoSearch(input.query, num, opts.signal);
27436
- ranked = rankSearchResults(rawResults, input.query);
27952
+ const fallback = await duckduckgoSearch(input.query, opts.signal);
27953
+ ranked = rankSearchResults(fallback.results, input.query);
27954
+ engineError = fallback.error;
27437
27955
  effectiveSource = "duckduckgo";
27438
27956
  }
27439
27957
  const finalResults = ranked.slice(0, num);
27440
- cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
27441
- pruneCacheEntries();
27958
+ if (!engineError) {
27959
+ cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
27960
+ pruneCacheEntries();
27961
+ }
27442
27962
  yield {
27443
27963
  type: "partial_output",
27444
- text: `${finalResults.length} results from ${effectiveSource}`,
27964
+ text: engineError ? `search failed: ${engineError}` : `${finalResults.length} results from ${effectiveSource}`,
27445
27965
  data: { count: finalResults.length, cached: false, source: effectiveSource }
27446
27966
  };
27447
27967
  yield {
@@ -27454,8 +27974,9 @@ var searchTool = {
27454
27974
  snippet: r.snippet
27455
27975
  })),
27456
27976
  source: effectiveSource,
27457
- truncated: finalResults.length >= num,
27458
- cached: false
27977
+ truncated: ranked.length > num,
27978
+ cached: false,
27979
+ ...engineError ? { error: engineError } : {}
27459
27980
  }
27460
27981
  };
27461
27982
  }
@@ -27506,18 +28027,18 @@ function shouldFallbackToDuckDuckGo(results, query) {
27506
28027
  return terms.some((term) => haystack.includes(term));
27507
28028
  });
27508
28029
  }
27509
- async function duckduckgoSearch(query, num, signal) {
28030
+ async function duckduckgoSearch(query, signal) {
27510
28031
  const encoded = encodeURIComponent(query);
27511
28032
  const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
27512
28033
  try {
27513
28034
  const response = await fetchWithTimeout(url, signal, TIMEOUT_MS3);
27514
28035
  const html = await response.text();
27515
- return parseDuckDuckGo(html, num);
28036
+ return { results: parseDuckDuckGo(html, MAX_RESULTS) };
27516
28037
  } catch (err) {
27517
28038
  console.log(
27518
- JSON.stringify({ level: "debug", event: "search_failed", query, error: toErrorMessage6(err) })
28039
+ JSON.stringify({ level: "debug", event: "search_failed", query, error: toErrorMessage9(err) })
27519
28040
  );
27520
- return [{ title: "Search unavailable", url: "https://duckduckgo.com/unavailable", snippet: "Could not reach DuckDuckGo", score: 0 }];
28041
+ return { results: [], error: `duckduckgo unreachable: ${toErrorMessage9(err)}` };
27521
28042
  }
27522
28043
  }
27523
28044
  function takeFrom(iter, max) {
@@ -27551,7 +28072,7 @@ function parseDuckDuckGo(html, num) {
27551
28072
  results.push({
27552
28073
  title: entry.title ?? "",
27553
28074
  url: entry.url ?? "",
27554
- snippet: snippetMatches[i] ?? "",
28075
+ snippet: capSnippet(snippetMatches[i] ?? ""),
27555
28076
  score: 1
27556
28077
  });
27557
28078
  }
@@ -27576,11 +28097,15 @@ function normalizeDuckDuckGoUrl(raw) {
27576
28097
  return raw;
27577
28098
  }
27578
28099
  }
27579
- async function googleSearch(query, num, signal) {
28100
+ async function googleSearch(query, signal) {
27580
28101
  const encoded = encodeURIComponent(query);
27581
28102
  const url = `https://www.google.com/search?q=${encoded}&hl=en`;
27582
- const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text()).catch(() => "");
27583
- return parseGoogleResults(html, num);
28103
+ try {
28104
+ const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text());
28105
+ return { results: parseGoogleResults(html, MAX_RESULTS) };
28106
+ } catch (err) {
28107
+ return { results: [], error: `google unreachable: ${toErrorMessage9(err)}` };
28108
+ }
27584
28109
  }
27585
28110
  function parseGoogleResults(html, num) {
27586
28111
  const results = [];
@@ -27603,17 +28128,21 @@ function parseGoogleResults(html, num) {
27603
28128
  results.push({
27604
28129
  title: titles[i] ?? "",
27605
28130
  url: urls[i] ?? "",
27606
- snippet: snippets[i] ?? "",
28131
+ snippet: capSnippet(snippets[i] ?? ""),
27607
28132
  score: 1
27608
28133
  });
27609
28134
  }
27610
28135
  return results;
27611
28136
  }
27612
- async function bingSearch(query, num, signal) {
28137
+ async function bingSearch(query, signal) {
27613
28138
  const encoded = encodeURIComponent(query);
27614
28139
  const url = `https://www.bing.com/search?q=${encoded}`;
27615
- const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text()).catch(() => "");
27616
- return parseBingResults(html, num);
28140
+ try {
28141
+ const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text());
28142
+ return { results: parseBingResults(html, MAX_RESULTS) };
28143
+ } catch (err) {
28144
+ return { results: [], error: `bing unreachable: ${toErrorMessage9(err)}` };
28145
+ }
27617
28146
  }
27618
28147
  function parseBingResults(html, num) {
27619
28148
  const results = [];
@@ -27626,7 +28155,7 @@ function parseBingResults(html, num) {
27626
28155
  const title = stripTags(expectDefined9(titleMatch[2]));
27627
28156
  if (!href || !title) return [];
27628
28157
  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);
27629
- const snippet = snippetMatch ? stripTags(expectDefined9(snippetMatch.at(-1))) : "";
28158
+ const snippet = snippetMatch ? capSnippet(stripTags(expectDefined9(snippetMatch.at(-1)))) : "";
27630
28159
  return [{ url: normalizeBingUrl(href), title, snippet, score: 1 }];
27631
28160
  }), num);
27632
28161
  for (let i = 0; i < entries.length; i++) {
@@ -27693,17 +28222,20 @@ function anySignal(...signals) {
27693
28222
  function stripTags(html) {
27694
28223
  return decodeHtmlEntities(html.replace(/<[^>]+>/g, "")).trim();
27695
28224
  }
28225
+ function capSnippet(snippet) {
28226
+ return snippet.length > MAX_SNIPPET_CHARS ? `${snippet.slice(0, MAX_SNIPPET_CHARS - 1)}\u2026` : snippet;
28227
+ }
27696
28228
  function decodeHtmlEntities(text) {
27697
28229
  return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
27698
28230
  }
27699
28231
 
27700
28232
  // src/set-working-dir.ts
27701
28233
  import * as fs32 from "node:fs/promises";
27702
- import { toErrorMessage as toErrorMessage7 } from "@wrongstack/core/utils";
28234
+ import { toErrorMessage as toErrorMessage10 } from "@wrongstack/core/utils";
27703
28235
  var setWorkingDirTool = {
27704
28236
  name: "set_working_dir",
27705
28237
  category: "Context",
27706
- 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.",
28238
+ 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.",
27707
28239
  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.",
27708
28240
  permission: "confirm",
27709
28241
  mutating: true,
@@ -27733,19 +28265,23 @@ var setWorkingDirTool = {
27733
28265
  } catch (err) {
27734
28266
  return {
27735
28267
  current: ctx.workingDir,
27736
- error: toErrorMessage7(err)
28268
+ error: toErrorMessage10(err)
27737
28269
  };
27738
28270
  }
28271
+ let isDirectory = false;
27739
28272
  try {
27740
- await fs32.access(resolved);
28273
+ isDirectory = (await fs32.stat(resolved)).isDirectory();
27741
28274
  } catch {
28275
+ isDirectory = false;
28276
+ }
28277
+ if (!isDirectory) {
27742
28278
  try {
27743
28279
  ctx.setWorkingDir(previous);
27744
28280
  } catch {
27745
28281
  }
27746
28282
  return {
27747
28283
  current: ctx.workingDir,
27748
- error: `Directory does not exist: ${resolved}`
28284
+ error: `Directory does not exist (or is not a directory): ${resolved}`
27749
28285
  };
27750
28286
  }
27751
28287
  return {
@@ -28200,6 +28736,7 @@ var taskTool = {
28200
28736
  inProgress: 0
28201
28737
  };
28202
28738
  }
28739
+ ctx.meta["task.path.resolved"] = taskPath;
28203
28740
  if (todosToReplace) {
28204
28741
  await todoTool.execute({ todos: todosToReplace }, ctx, {
28205
28742
  signal: AbortSignal.timeout(3e4)
@@ -28224,6 +28761,7 @@ var taskTool = {
28224
28761
  formatted = formatPlan2(updated);
28225
28762
  return updated;
28226
28763
  });
28764
+ ctx.meta["plan.path.resolved"] = planPath;
28227
28765
  } catch (err) {
28228
28766
  return {
28229
28767
  ok: false,
@@ -28267,7 +28805,7 @@ ${formatTaskList2(file.tasks)}` : file.tasks.length > 0 ? formatTaskList2(file.t
28267
28805
  init_spawn_stream();
28268
28806
  init_util();
28269
28807
  init_legacy_bridge();
28270
- import * as path36 from "node:path";
28808
+ import * as path37 from "node:path";
28271
28809
  var testTool = {
28272
28810
  name: "test",
28273
28811
  category: "Code Quality",
@@ -28370,11 +28908,11 @@ var testTool = {
28370
28908
  }
28371
28909
  };
28372
28910
  async function detectRunner(cwd) {
28373
- const { stat: stat18 } = await import("node:fs/promises");
28911
+ const { stat: stat19 } = await import("node:fs/promises");
28374
28912
  const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
28375
28913
  for (const f of candidates) {
28376
28914
  try {
28377
- await stat18(path36.join(cwd, f));
28915
+ await stat19(path37.join(cwd, f));
28378
28916
  if (f.includes("vitest")) return "vitest";
28379
28917
  if (f.includes("jest")) return "jest";
28380
28918
  if (f.includes("mocha")) return "mocha";
@@ -28752,7 +29290,7 @@ var toolUseTool = {
28752
29290
  // src/tree.ts
28753
29291
  init_util();
28754
29292
  import * as fs33 from "node:fs/promises";
28755
- import * as path37 from "node:path";
29293
+ import * as path38 from "node:path";
28756
29294
  import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
28757
29295
  var DEFAULT_IGNORE5 = /* @__PURE__ */ new Set([
28758
29296
  ...DEFAULT_WALK_IGNORE_DIRS4,
@@ -28772,6 +29310,7 @@ var treeTool = {
28772
29310
  mutating: false,
28773
29311
  capabilities: ["fs.read"],
28774
29312
  icon: "tree",
29313
+ maxOutputBytes: 262144,
28775
29314
  timeoutMs: 15e3,
28776
29315
  inputSchema: {
28777
29316
  type: "object",
@@ -28920,17 +29459,15 @@ async function walkDir(dir, depth, opts) {
28920
29459
  if (opts.exclude.has(e.name)) return false;
28921
29460
  return true;
28922
29461
  });
28923
- if (depth > 0) {
28924
- let dirCount = 0;
28925
- let fileCount = 0;
28926
- for (const e of filtered) {
28927
- if (e.isDirectory()) dirCount++;
28928
- else if (e.isFile()) fileCount++;
28929
- }
28930
- opts.totalDirs.value += dirCount;
28931
- opts.totalFiles.value += fileCount;
28932
- opts.onProgress?.();
28933
- }
29462
+ let dirCount = 0;
29463
+ let fileCount = 0;
29464
+ for (const e of filtered) {
29465
+ if (e.isDirectory()) dirCount++;
29466
+ else if (e.isFile()) fileCount++;
29467
+ }
29468
+ opts.totalDirs.value += dirCount;
29469
+ opts.totalFiles.value += fileCount;
29470
+ opts.onProgress?.();
28934
29471
  const items = filtered.sort((a, b) => {
28935
29472
  if (a.isDirectory() && !b.isDirectory()) return -1;
28936
29473
  if (!a.isDirectory() && b.isDirectory()) return 1;
@@ -28958,7 +29495,7 @@ async function walkDir(dir, depth, opts) {
28958
29495
  opts.retention.outputBytes += lineBytes;
28959
29496
  if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
28960
29497
  const childPrefix = opts.prefix + connector;
28961
- await walkDir(path37.join(dir, entry.name), depth + 1, {
29498
+ await walkDir(path38.join(dir, entry.name), depth + 1, {
28962
29499
  ...opts,
28963
29500
  prefix: childPrefix,
28964
29501
  isLast
@@ -28971,7 +29508,7 @@ async function walkDir(dir, depth, opts) {
28971
29508
  init_spawn_stream();
28972
29509
  init_util();
28973
29510
  init_legacy_bridge();
28974
- import * as path38 from "node:path";
29511
+ import * as path39 from "node:path";
28975
29512
  var typecheckTool = {
28976
29513
  name: "typecheck",
28977
29514
  category: "Code Quality",
@@ -28993,11 +29530,7 @@ var typecheckTool = {
28993
29530
  },
28994
29531
  all: {
28995
29532
  type: "boolean",
28996
- description: "Type-check all projects (pnpm -r) (default: false)"
28997
- },
28998
- json: {
28999
- type: "boolean",
29000
- description: "Emit JSON output from tsc (default: false)"
29533
+ 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)"
29001
29534
  }
29002
29535
  }
29003
29536
  },
@@ -29033,29 +29566,42 @@ var typecheckTool = {
29033
29566
  };
29034
29567
  return;
29035
29568
  }
29036
- let args;
29569
+ let cmd;
29570
+ let cmdArgs;
29037
29571
  let project;
29038
29572
  if (input.all) {
29039
- args = ["--noEmit"];
29040
29573
  project = "workspace";
29574
+ const tscArgs = ["--noEmit"];
29575
+ if (input.strict) tscArgs.push("--strict");
29576
+ const manager = await detectPackageManager(cwd, ctx.projectRoot);
29577
+ if (manager === "pnpm") {
29578
+ cmd = "pnpm";
29579
+ cmdArgs = ["-r", "--no-bail", "exec", "tsc", ...tscArgs];
29580
+ } else {
29581
+ cmd = "npx";
29582
+ cmdArgs = ["tsc", ...tscArgs];
29583
+ }
29041
29584
  } else {
29042
29585
  const tsconfig = input.project ? safeResolve(input.project, ctx) : await findTsConfig(cwd);
29043
- args = ["--noEmit"];
29044
- if (input.strict) args.push("--strict");
29045
- if (tsconfig) args.push("--project", tsconfig);
29586
+ const tscArgs = ["--noEmit"];
29587
+ if (input.strict) tscArgs.push("--strict");
29588
+ if (tsconfig) tscArgs.push("--project", tsconfig);
29046
29589
  project = tsconfig ?? "default";
29590
+ cmd = "npx";
29591
+ cmdArgs = ["tsc", ...tscArgs];
29047
29592
  }
29048
- if (input.json) args.push("--json");
29049
- yield { type: "log", text: `tsc ${args.join(" ")}`, data: { project } };
29593
+ yield { type: "log", text: `${cmd} ${cmdArgs.join(" ")}`, data: { project } };
29050
29594
  const result = yield* spawnStream({
29051
- cmd: "npx",
29052
- args: ["tsc", ...args],
29595
+ cmd,
29596
+ args: cmdArgs,
29053
29597
  cwd,
29054
29598
  signal: opts.signal,
29055
29599
  maxBytes: 2e5
29056
29600
  });
29057
- const errors = [...result.stdout.matchAll(/\berror\b/gi)].length;
29058
- const warnings = [...result.stdout.matchAll(/\bwarning\b/gi)].length;
29601
+ const combined = `${result.stdout}
29602
+ ${result.stderr}`;
29603
+ const errors = [...combined.matchAll(/^.*\berror TS\d+:/gm)].length;
29604
+ const warnings = [...combined.matchAll(/^.*\bwarning TS\d+:/gm)].length;
29059
29605
  yield {
29060
29606
  type: "final",
29061
29607
  output: {
@@ -29070,12 +29616,12 @@ var typecheckTool = {
29070
29616
  }
29071
29617
  };
29072
29618
  async function findTsConfig(cwd) {
29073
- const { stat: stat18 } = await import("node:fs/promises");
29619
+ const { stat: stat19 } = await import("node:fs/promises");
29074
29620
  const candidates = ["tsconfig.json", "tsconfig.base.json"];
29075
29621
  for (const f of candidates) {
29076
29622
  try {
29077
- const s = await stat18(path38.join(cwd, f));
29078
- if (s.isFile()) return path38.join(cwd, f);
29623
+ const s = await stat19(path39.join(cwd, f));
29624
+ if (s.isFile()) return path39.join(cwd, f);
29079
29625
  } catch {
29080
29626
  }
29081
29627
  }
@@ -29084,21 +29630,32 @@ async function findTsConfig(cwd) {
29084
29630
 
29085
29631
  // src/write.ts
29086
29632
  import * as fs34 from "node:fs/promises";
29087
- import { ToolValidationError as ToolValidationError8 } from "@wrongstack/core/types";
29088
- import { atomicWrite as atomicWrite5, unifiedDiff as unifiedDiff3 } from "@wrongstack/core/utils";
29633
+ import { ToolValidationError as ToolValidationError11 } from "@wrongstack/core/types";
29634
+ import {
29635
+ atomicWrite as atomicWrite5,
29636
+ detectNewlineStyle as detectNewlineStyle3,
29637
+ normalizeToLf as normalizeToLf3,
29638
+ toStyle as toStyle3,
29639
+ unifiedDiff as unifiedDiff3
29640
+ } from "@wrongstack/core/utils";
29089
29641
  init_util();
29642
+ var MAX_DIFF_BYTES3 = 262144;
29090
29643
  var writeTool = {
29091
29644
  name: "write",
29092
29645
  category: "Filesystem",
29093
29646
  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.",
29094
- 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.",
29647
+ 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.",
29095
29648
  selection: {
29096
29649
  doNotUseWhen: "making a precise change to part of an existing file.",
29097
29650
  useInstead: ["edit"]
29098
29651
  },
29099
29652
  permission: "confirm",
29653
+ // WS-046: gives permission decisions something to key on — the file being
29654
+ // written, so trust rules can scope by path.
29655
+ subjectKey: "path",
29100
29656
  mutating: true,
29101
29657
  timeoutMs: 5e3,
29658
+ maxOutputBytes: 262144,
29102
29659
  capabilities: ["fs.write"],
29103
29660
  icon: "file",
29104
29661
  inputSchema: {
@@ -29134,13 +29691,13 @@ async function writeFile6(input, ctx, signal) {
29134
29691
  }
29135
29692
  async function prepareWrite(input, ctx) {
29136
29693
  if (!input?.path) {
29137
- throw new ToolValidationError8({
29694
+ throw new ToolValidationError11({
29138
29695
  message: "write: path is required",
29139
29696
  field: "path"
29140
29697
  });
29141
29698
  }
29142
29699
  if (input.content === void 0) {
29143
- throw new ToolValidationError8({
29700
+ throw new ToolValidationError11({
29144
29701
  message: "write: content is required",
29145
29702
  field: "content"
29146
29703
  });
@@ -29149,12 +29706,12 @@ async function prepareWrite(input, ctx) {
29149
29706
  let existed = false;
29150
29707
  let prev = "";
29151
29708
  try {
29152
- const stat18 = await fs34.stat(absPath);
29153
- existed = stat18.isFile();
29709
+ const stat19 = await fs34.stat(absPath);
29710
+ existed = stat19.isFile();
29154
29711
  if (existed) {
29155
29712
  if (!ctx.hasRead(absPath)) {
29156
29713
  prev = await fs34.readFile(absPath, "utf8");
29157
- ctx.recordRead(absPath, stat18.mtimeMs, "write", sha256hex(prev));
29714
+ ctx.recordRead(absPath, stat19.mtimeMs, "write", sha256hex(prev));
29158
29715
  } else {
29159
29716
  prev = await fs34.readFile(absPath, "utf8");
29160
29717
  }
@@ -29167,31 +29724,42 @@ async function prepareWrite(input, ctx) {
29167
29724
  return { absPath, existed, prev };
29168
29725
  }
29169
29726
  async function finishWrite(input, ctx, prepared, signal) {
29727
+ const content = prepared.existed ? toStyle3(normalizeToLf3(input.content), detectNewlineStyle3(prepared.prev)) : input.content;
29170
29728
  signal?.throwIfAborted();
29171
- await atomicWrite5(prepared.absPath, input.content);
29172
- const diff = prepared.existed ? unifiedDiff3(prepared.prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
29173
- + (new file, ${input.content.split("\n").length} lines)`;
29174
- const stat18 = await fs34.stat(prepared.absPath);
29175
- ctx.recordRead(prepared.absPath, stat18.mtimeMs, "write", sha256hex(input.content));
29729
+ await atomicWrite5(prepared.absPath, content);
29730
+ const rawDiff = prepared.existed ? unifiedDiff3(prepared.prev, content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
29731
+ + (new file, ${content.split("\n").length} lines)`;
29732
+ const { text: diff, truncated: diffTruncated } = truncateDiffPayload(rawDiff, MAX_DIFF_BYTES3);
29733
+ const stat19 = await fs34.stat(prepared.absPath);
29734
+ ctx.recordRead(prepared.absPath, stat19.mtimeMs, "write", sha256hex(content));
29176
29735
  ctx.session.recordFileChange({
29177
29736
  path: prepared.absPath,
29178
29737
  action: prepared.existed ? "modified" : "created",
29179
29738
  before: prepared.existed ? prepared.prev : null,
29180
- after: input.content
29739
+ after: content
29181
29740
  });
29182
29741
  const syntax = await checkSyntax(
29183
29742
  prepared.absPath,
29184
- input.content,
29743
+ content,
29185
29744
  prepared.existed ? prepared.prev : void 0
29186
29745
  ).catch(() => void 0);
29187
29746
  const hasSyntaxErrors = syntax !== void 0 && syntax.errors.length > 0;
29747
+ const notes = [];
29748
+ if (diffTruncated) {
29749
+ notes.push("Diff truncated to the 256 KiB output budget \u2014 the full write is on disk.");
29750
+ }
29751
+ if (hasSyntaxErrors) {
29752
+ notes.push(
29753
+ 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.`
29754
+ );
29755
+ }
29188
29756
  return {
29189
29757
  path: prepared.absPath,
29190
- bytes_written: Buffer.byteLength(input.content, "utf8"),
29758
+ bytes_written: Buffer.byteLength(content, "utf8"),
29191
29759
  created: !prepared.existed,
29192
29760
  diff,
29193
29761
  syntax_errors: hasSyntaxErrors ? syntax.errors : void 0,
29194
- 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
29762
+ note: notes.length > 0 ? notes.join("\n") : void 0
29195
29763
  };
29196
29764
  }
29197
29765