@wrongstack/tools 0.305.1 → 0.306.0

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