@wrongstack/plugins 0.307.1 → 0.308.1

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.
package/dist/index.js CHANGED
@@ -345,6 +345,83 @@ function releaseHandle(off) {
345
345
  return null;
346
346
  }
347
347
 
348
+ // src/runtime/redos-guard.ts
349
+ import { Worker } from "node:worker_threads";
350
+ function withReDoSGuard(re, input, budgetMs = 50, options = {}) {
351
+ const opts = { budgetMs, ...options };
352
+ const start = Date.now();
353
+ const workerSource = buildWorkerSource(re.source, input, re.flags);
354
+ const worker = new Worker(workerSource, {
355
+ eval: true,
356
+ name: `redos-guard:${re.source.slice(0, 32)}`
357
+ });
358
+ return new Promise((resolve29) => {
359
+ let settled = false;
360
+ const onMessage = (msg) => {
361
+ if (settled) return;
362
+ settled = true;
363
+ clearTimeout(timer);
364
+ worker.terminate().catch(() => {
365
+ });
366
+ if (!msg.ok) {
367
+ resolve29({ timedOut: true, match: null });
368
+ return;
369
+ }
370
+ resolve29({ timedOut: false, match: msg.match });
371
+ };
372
+ const onError = () => {
373
+ if (settled) return;
374
+ settled = true;
375
+ clearTimeout(timer);
376
+ worker.terminate().catch(() => {
377
+ });
378
+ resolve29({ timedOut: true, match: null });
379
+ };
380
+ const timer = setTimeout(() => {
381
+ if (settled) return;
382
+ settled = true;
383
+ const elapsedMs = Date.now() - start;
384
+ worker.terminate().catch(() => {
385
+ });
386
+ try {
387
+ opts.onTimeout?.({
388
+ regex: re,
389
+ input,
390
+ budgetMs: opts.budgetMs,
391
+ elapsedMs
392
+ });
393
+ } catch {
394
+ }
395
+ resolve29({ timedOut: true, match: null });
396
+ }, opts.budgetMs);
397
+ timer.unref?.();
398
+ worker.on("message", onMessage);
399
+ worker.on("error", onError);
400
+ });
401
+ }
402
+ function buildWorkerSource(source, input, flags) {
403
+ const S = JSON.stringify(source);
404
+ const I = JSON.stringify(input);
405
+ const F = JSON.stringify(flags);
406
+ return `
407
+ const { parentPort } = require('node:worker_threads');
408
+ const source = ${S};
409
+ const input = ${I};
410
+ const flags = ${F};
411
+ try {
412
+ const re = new RegExp(source, flags);
413
+ const match = re.exec(input);
414
+ // parentPort.postMessage, NOT bare postMessage: with eval:true
415
+ // workers this Node version does not expose the bare postMessage
416
+ // global \u2014 the worker throws ReferenceError at startup and the
417
+ // host misreads it as a timeout (positive-path regression).
418
+ parentPort.postMessage({ ok: true, match });
419
+ } catch (err) {
420
+ parentPort.postMessage({ ok: false, error: err && err.message ? err.message : String(err) });
421
+ }
422
+ `;
423
+ }
424
+
348
425
  // src/runtime/index.ts
349
426
  var META_CHARS = /["'`;&|<>\r\n]/;
350
427
  var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
@@ -446,6 +523,7 @@ function runRunnerCommand(argv, options) {
446
523
  }
447
524
  let timedOut = false;
448
525
  let spawnErrored = false;
526
+ const start = Date.now();
449
527
  const stdoutChunks = [];
450
528
  const stderrChunks = [];
451
529
  let stdoutBytes = 0;
@@ -475,6 +553,15 @@ function runRunnerCommand(argv, options) {
475
553
  timeout: options.timeoutMs,
476
554
  signal: options.signal,
477
555
  maxBuffer: MAX_BUFFER_BYTES,
556
+ // execFile defaults `encoding` to 'utf8', which makes the
557
+ // stdout/stderr `data` events emit *strings*. The chunk arrays
558
+ // below are typed Buffer[] and every consumer runs them through
559
+ // Buffer.concat(...).toString('utf8'), which throws
560
+ // ERR_INVALID_ARG_TYPE on any non-empty string output. Pin the
561
+ // streams to buffers so the declared contract holds (regression:
562
+ // runRunnerCommand crashed on any child that actually wrote
563
+ // output; only the maxBuffer fixture exercised this path).
564
+ encoding: "buffer",
478
565
  windowsHide: true,
479
566
  shell: false,
480
567
  ...invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
@@ -503,11 +590,31 @@ function runRunnerCommand(argv, options) {
503
590
  }
504
591
  if (err) {
505
592
  const anyErr = err;
593
+ if ((anyErr.killed === true || anyErr.signal === "SIGTERM") && // maxBuffer overflow must NOT be misreported as a timeout
594
+ // — it's a real failure (the child wrote too much) and
595
+ // downstream callers (type-gate/index.ts:227) return
596
+ // null on timedOut=true, which would silently swallow
597
+ // maxBuffer overflow into a confusing empty-output
598
+ // result. Skip the timeout resolve when the err shape
599
+ // names maxBuffer explicitly.
600
+ !/maxBuffer length exceeded/i.test(anyErr.message ?? "") && // And only count it as a timeout if the wall clock has
601
+ // actually elapsed past the budget. External SIGTERMs and
602
+ // races against the exit handler don't satisfy this.
603
+ Date.now() - start >= options.timeoutMs) {
604
+ resolvePromise({
605
+ code: null,
606
+ stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
607
+ stderr: Buffer.concat(stderrChunks).toString("utf-8"),
608
+ timedOut: true,
609
+ spawnError: false
610
+ });
611
+ return;
612
+ }
506
613
  const code = typeof anyErr.code === "number" ? anyErr.code : 1;
507
614
  resolvePromise({
508
615
  code,
509
- stdout: Buffer.concat(stdoutChunks).toString("utf8"),
510
- stderr: Buffer.concat(stderrChunks).toString("utf8"),
616
+ stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
617
+ stderr: Buffer.concat(stderrChunks).toString("utf-8"),
511
618
  timedOut: false,
512
619
  spawnError: false
513
620
  });
@@ -523,7 +630,7 @@ function runRunnerCommand(argv, options) {
523
630
  }
524
631
  );
525
632
  child.on("exit", (_code, signal) => {
526
- if (signal !== null) {
633
+ if (signal !== null && Date.now() - start >= options.timeoutMs) {
527
634
  timedOut = true;
528
635
  }
529
636
  });
@@ -626,15 +733,40 @@ var INPUT_BUTTON = /<input\b[^>]*\btype\s*=\s*["'](submit|button|reset)["'][^>]*
626
733
  var ATTR_ID = /\bid\s*=\s*["']([^"']+)["']/gi;
627
734
  var ATTR_ALT = /\balt\s*=/i;
628
735
  var ATTR_ARIA_LABEL = /\b(?:aria-label|aria-labelledby)\s*=/i;
736
+ var ATTR_ARIA_DESCRIBEDBY = /\baria-describedby\s*=/i;
629
737
  var ATTR_TITLE = /\btitle\s*=/i;
630
738
  var ATTR_PLACEHOLDER = /\bplaceholder\s*=/i;
631
739
  var ATTR_VALUE = /\bvalue\s*=/i;
740
+ var ATTR_ROLE_DECORATIVE = /\brole\s*=\s*["'](?:presentation|none)["']/i;
741
+ var SINGLE_FILE_LABEL_NOTE = "Single-file heuristic: a label declared in a sibling component file is not visible to this scan.";
742
+ function escapeRegExp(value) {
743
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
744
+ }
632
745
  function hasMeaningfulAlt(tag) {
633
746
  const m = ATTR_ALT.exec(tag);
634
747
  ATTR_ALT.lastIndex = 0;
635
748
  if (!m) return false;
636
749
  const valMatch = tag.match(/\balt\s*=\s*["']?([^"'\s>]*)["']?/i);
637
- return valMatch ? valMatch[1].trim().length > 0 : false;
750
+ const alt = valMatch ? valMatch[1].trim() : "";
751
+ if (alt.length > 0) return true;
752
+ return ATTR_ROLE_DECORATIVE.test(tag);
753
+ }
754
+ function hasFieldsetLegendLabel(tag, content) {
755
+ const labelledBy = tag.match(/\baria-labelledby\s*=\s*["']([^"']+)["']/i);
756
+ if (labelledBy?.[1]) {
757
+ for (const id of labelledBy[1].split(/\s+/).filter(Boolean)) {
758
+ const idRe = new RegExp(`\\bid\\s*=\\s*["']${escapeRegExp(id)}["']`, "i");
759
+ if (idRe.test(content)) return true;
760
+ }
761
+ }
762
+ const typeMatch = tag.match(/\btype\s*=\s*["']?([^"'\s>]*)["']?/i);
763
+ const type = typeMatch ? typeMatch[1].toLowerCase() : "text";
764
+ if (type === "checkbox" || type === "radio") {
765
+ return /<fieldset\b[\s\S]*?<legend\b[\s\S]*?<\/legend>[\s\S]*?<input\b[\s\S]*?<\/fieldset>/i.test(
766
+ content
767
+ );
768
+ }
769
+ return false;
638
770
  }
639
771
  async function auditFile(filePath, projectRoot) {
640
772
  let content;
@@ -646,13 +778,14 @@ async function auditFile(filePath, projectRoot) {
646
778
  const findings = [];
647
779
  const lines = content.split(/\r?\n/);
648
780
  const idsByValue = /* @__PURE__ */ new Map();
649
- function add(line, rule, severity, message) {
781
+ function add(line, rule, severity, message, note) {
650
782
  findings.push({
651
783
  file: relative3(projectRoot, filePath),
652
784
  line,
653
785
  rule,
654
786
  severity,
655
- message
787
+ message,
788
+ ...note ? { note } : {}
656
789
  });
657
790
  }
658
791
  for (let i = 0; i < lines.length; i++) {
@@ -680,17 +813,36 @@ async function auditFile(filePath, projectRoot) {
680
813
  continue;
681
814
  }
682
815
  const hasAriaLabel = ATTR_ARIA_LABEL.test(tag);
816
+ const hasDescribedBy = ATTR_ARIA_DESCRIBEDBY.test(tag);
683
817
  const hasTitle = ATTR_TITLE.test(tag);
684
818
  const idMatchLocal = tag.match(/\bid\s*=\s*["']([^"']+)["']/i);
685
819
  const id = idMatchLocal ? idMatchLocal[1] : null;
686
820
  let hasLabelFor = false;
687
821
  if (id) {
688
- const labelForRe = new RegExp(`<label\\b[^>]*\\bfor\\s*=\\s*["']${id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`, "i");
822
+ const labelForRe = new RegExp(
823
+ `<label\\b[^>]*\\bfor\\s*=\\s*["']${escapeRegExp(id)}["']`,
824
+ "i"
825
+ );
689
826
  hasLabelFor = labelForRe.test(content);
690
827
  }
691
828
  const wrappedInLabel = /<label\b[\s\S]*?<input\b[\s\S]*?<\/label>/i.test(content);
692
- if (!hasAriaLabel && !hasTitle && !hasLabelFor && !wrappedInLabel) {
693
- add(lineNo, "missing-input-label", "error", `<input type="${type}"> is missing an associated label`);
829
+ const hasLegend = hasFieldsetLegendLabel(tag, content);
830
+ const hasPrimaryLabel = hasAriaLabel || hasTitle || hasLabelFor || wrappedInLabel || hasLegend;
831
+ if (!hasPrimaryLabel && hasDescribedBy) {
832
+ add(
833
+ lineNo,
834
+ "low-contrast-placeholder",
835
+ "warning",
836
+ `<input type="${type}"> uses aria-describedby as a description (supplementary, not a primary label)`
837
+ );
838
+ } else if (!hasPrimaryLabel) {
839
+ add(
840
+ lineNo,
841
+ "missing-input-label",
842
+ "error",
843
+ `<input type="${type}"> is missing an associated label`,
844
+ SINGLE_FILE_LABEL_NOTE
845
+ );
694
846
  }
695
847
  if (ATTR_PLACEHOLDER.test(tag)) {
696
848
  add(lineNo, "low-contrast-placeholder", "warning", "<input> uses placeholder text (often low contrast and disappears on input)");
@@ -750,15 +902,24 @@ async function auditPath(rawPath, cfg) {
750
902
  truncated
751
903
  };
752
904
  }
905
+ function truncationWarning(result) {
906
+ if (!result.truncated) return "";
907
+ const unexamined = Math.max(0, result.fileCount - (result.scannedFiles ?? result.fileCount));
908
+ return `partial scan \u2014 ${unexamined} files not examined`;
909
+ }
753
910
  function formatSummary(result) {
911
+ const trunc = truncationWarning(result);
754
912
  if (result.findings.length === 0) {
755
- return `
913
+ const clean = `
756
914
  \u2705 accessibility-auditor: no issues found in ${result.path} (${result.fileCount} file${result.fileCount === 1 ? "" : "s"}).`;
915
+ return trunc ? `${clean}
916
+ \u26A0\uFE0F ${trunc}` : clean;
757
917
  }
758
918
  const lines = result.findings.map((f) => ` - ${f.file}:${f.line} \u2014 ${f.message} (${f.rule})`);
759
919
  return `
760
920
  \u26A0\uFE0F accessibility-auditor: ${result.findings.length} issue(s) in ${result.path} (${result.fileCount} file${result.fileCount === 1 ? "" : "s"}):
761
- ` + lines.join("\n") + "\nConsider adding missing labels/alt text or resolving duplicate ids.";
921
+ ` + lines.join("\n") + "\nConsider adding missing labels/alt text or resolving duplicate ids." + (trunc ? `
922
+ \u26A0\uFE0F ${trunc}` : "");
762
923
  }
763
924
  var plugin = {
764
925
  name: "accessibility-auditor",
@@ -829,9 +990,9 @@ var plugin = {
829
990
  findingCount: result.findings.length,
830
991
  when: (/* @__PURE__ */ new Date()).toISOString()
831
992
  };
832
- if (result.findings.length === 0) return;
993
+ if (result.findings.length === 0 && !result.truncated) return;
833
994
  const summary = formatSummary(result);
834
- if (cfg.severity === "block") {
995
+ if (cfg.severity === "block" && result.findings.length > 0) {
835
996
  return { decision: "block", reason: summary };
836
997
  }
837
998
  return { additionalContext: summary };
@@ -872,6 +1033,7 @@ var plugin = {
872
1033
  findingCount: result.findings.length,
873
1034
  when: (/* @__PURE__ */ new Date()).toISOString()
874
1035
  };
1036
+ const warning = truncationWarning(result);
875
1037
  return {
876
1038
  ok: true,
877
1039
  path: result.path,
@@ -881,7 +1043,8 @@ var plugin = {
881
1043
  // that reports few findings must not read as a clean result.
882
1044
  truncated: result.truncated,
883
1045
  findingCount: result.findings.length,
884
- findings: result.findings
1046
+ findings: result.findings,
1047
+ ...warning ? { additionalContext: `\u26A0\uFE0F ${warning}`, warning } : {}
885
1048
  };
886
1049
  }
887
1050
  });
@@ -3299,9 +3462,9 @@ var plugin9 = {
3299
3462
  const ti = input.toolInput ?? {};
3300
3463
  const raw = ti["path"] ?? ti["file_path"] ?? ti["filePath"];
3301
3464
  if (typeof raw !== "string" || raw.length === 0) return;
3302
- const safePath = resolveProjectPath3(raw);
3303
- if (!safePath) return;
3304
- const captured = await captureFileForHook(safePath, cfg.maxFileBytes, runtime.signal);
3465
+ const safePath2 = resolveProjectPath3(raw);
3466
+ if (!safePath2) return;
3467
+ const captured = await captureFileForHook(safePath2, cfg.maxFileBytes, runtime.signal);
3305
3468
  if (captured === "too-large") {
3306
3469
  state9.skippedLarge += 1;
3307
3470
  return;
@@ -3319,7 +3482,7 @@ var plugin9 = {
3319
3482
  state9.captures += 1;
3320
3483
  api.metrics.counter("captures");
3321
3484
  api.emitCustom?.("checkpoint:captured", {
3322
- path: safePath,
3485
+ path: safePath2,
3323
3486
  bytes: captured.bytes,
3324
3487
  hadContent: captured.content !== null,
3325
3488
  // 32-bit unsigned hash of the captured bytes. Collisions
@@ -3363,12 +3526,12 @@ var plugin9 = {
3363
3526
  const rejectedOutsideProject = [];
3364
3527
  let skipped = 0;
3365
3528
  for (const p of paths) {
3366
- const safePath = resolveProjectPath3(p);
3367
- if (!safePath) {
3529
+ const safePath2 = resolveProjectPath3(p);
3530
+ if (!safePath2) {
3368
3531
  rejectedOutsideProject.push(p);
3369
3532
  continue;
3370
3533
  }
3371
- const captured = await captureFile(safePath, cfg.maxFileBytes);
3534
+ const captured = await captureFile(safePath2, cfg.maxFileBytes);
3372
3535
  if (captured === "too-large") {
3373
3536
  skipped += 1;
3374
3537
  state9.skippedLarge += 1;
@@ -3601,36 +3764,65 @@ function countFunctions(content) {
3601
3764
  for (const _match of content.matchAll(arrowRe)) count++;
3602
3765
  return count;
3603
3766
  }
3767
+ var COMPLEXITY_FORMULA = "control(if|else if|for|while|switch|catch) + (&& || ?? ||= &&= ??=) + ternary ?; optional chaining ?. is not counted";
3604
3768
  function countComplexity(content) {
3605
3769
  const controlRe = /\b(if|else\s+if|for|while|switch|catch)\b/g;
3606
- const operatorRe = /[?&|]/g;
3607
3770
  let complexity = 0;
3608
3771
  controlRe.lastIndex = 0;
3609
3772
  for (const _match of content.matchAll(controlRe)) complexity++;
3610
- operatorRe.lastIndex = 0;
3611
- for (const match of content.matchAll(operatorRe)) {
3612
- const ch = match[0];
3613
- if (ch === "?") {
3773
+ for (let i = 0; i < content.length; i++) {
3774
+ const c = content[i];
3775
+ const n1 = content[i + 1];
3776
+ const n2 = content[i + 2];
3777
+ if (c === "?" && n1 === "?" && n2 === "=") {
3614
3778
  complexity++;
3615
- } else {
3616
- const next = content[match.index + 1];
3617
- if (next === ch) complexity++;
3779
+ i += 2;
3780
+ continue;
3781
+ }
3782
+ if (c === "|" && n1 === "|" && n2 === "=" || c === "&" && n1 === "&" && n2 === "=") {
3783
+ complexity++;
3784
+ i += 2;
3785
+ continue;
3618
3786
  }
3787
+ if (c === "?" && n1 === "?" || c === "|" && n1 === "|" || c === "&" && n1 === "&") {
3788
+ complexity++;
3789
+ i += 1;
3790
+ continue;
3791
+ }
3792
+ if (c === "?" && n1 === ".") {
3793
+ i += 1;
3794
+ continue;
3795
+ }
3796
+ if (c === "?") complexity++;
3619
3797
  }
3620
3798
  return complexity;
3621
3799
  }
3622
3800
  function analyzeFile(filePath, content) {
3801
+ if (content.length === 0) {
3802
+ return {
3803
+ file: relativePath(filePath),
3804
+ lines: 0,
3805
+ codeLines: 0,
3806
+ commentLines: 0,
3807
+ blankLines: 0,
3808
+ functionCount: 0,
3809
+ complexity: 0
3810
+ };
3811
+ }
3623
3812
  const lines = content.split(/\r?\n/);
3624
3813
  let codeLines = 0;
3625
3814
  let commentLines = 0;
3626
3815
  let blankLines = 0;
3627
3816
  let inBlockComment = false;
3628
- for (const rawLine of lines) {
3629
- const line = rawLine.trim();
3817
+ for (let i = 0; i < lines.length; i++) {
3818
+ const line = lines[i].trim();
3630
3819
  if (line.length === 0) {
3631
3820
  blankLines++;
3632
3821
  continue;
3633
3822
  }
3823
+ if (i === 0 && line.startsWith("#!")) {
3824
+ continue;
3825
+ }
3634
3826
  if (inBlockComment) {
3635
3827
  commentLines++;
3636
3828
  if (line.includes("*/")) inBlockComment = false;
@@ -3791,7 +3983,8 @@ var plugin10 = {
3791
3983
  files: state10.fileCount,
3792
3984
  hookInvocations: state10.hookInvocationCount,
3793
3985
  errors: state10.errorCount
3794
- }
3986
+ },
3987
+ complexityFormula: COMPLEXITY_FORMULA
3795
3988
  };
3796
3989
  }
3797
3990
  });
@@ -5860,10 +6053,12 @@ function parseInstallCommands(command) {
5860
6053
  if (!cleaned) continue;
5861
6054
  let name = cleaned;
5862
6055
  let version = null;
5863
- const pipMatch = /^([A-Za-z0-9_.-]+)\s*(?:==|>=|<=|~=|!=|>|<)\s*(.+)$/.exec(cleaned);
6056
+ const pipMatch = /^([A-Za-z0-9_.-]+(?:\[[^\]]+\])?)\s*(==|>=|<=|~=|!=|>|<)\s*(.+)$/.exec(cleaned);
5864
6057
  if (pipMatch?.[1]) {
5865
6058
  name = pipMatch[1];
5866
- version = pipMatch[2] ?? null;
6059
+ const op = pipMatch[2] ?? "";
6060
+ const ver = (pipMatch[3] ?? "").trim();
6061
+ version = op === "==" || op === "" ? ver || null : `${op}${ver}`;
5867
6062
  } else {
5868
6063
  const at = cleaned.lastIndexOf("@");
5869
6064
  if (at > 0) {
@@ -6288,13 +6483,12 @@ function exceedsThreshold(report, threshold) {
6288
6483
  if (thresholdRank == null || maxRank == null) return false;
6289
6484
  return maxRank >= thresholdRank;
6290
6485
  }
6291
- var INSTALL_RE2 = /\b(?:npm\s+(?:install|i)|pnpm\s+add|yarn\s+add)\b/i;
6292
6486
  function isInstallCommand(input) {
6293
6487
  if (input.toolName === "install") return true;
6294
6488
  if (input.toolName !== "bash" && input.toolName !== "exec") return false;
6295
6489
  const ti = input.toolInput ?? {};
6296
6490
  const command = typeof ti["command"] === "string" ? ti["command"] : "";
6297
- return INSTALL_RE2.test(command);
6491
+ return parseInstallCommands(command).length > 0;
6298
6492
  }
6299
6493
  var LOCKFILE_MANAGERS = [
6300
6494
  { file: "pnpm-lock.yaml", manager: "pnpm" },
@@ -8067,18 +8261,8 @@ var feature_flag_tracker_default = plugin23;
8067
8261
 
8068
8262
  // src/file-watcher/index.ts
8069
8263
  import { watch as fsWatch } from "node:fs";
8070
- import { isAbsolute as isAbsolute12, join as join3, relative as relative13, resolve as resolve13 } from "node:path";
8264
+ import { join as join3 } from "node:path";
8071
8265
  var API_VERSION16 = "^0.1.10";
8072
- function withinProject3(p) {
8073
- if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
8074
- const root = process.cwd();
8075
- const resolved = isAbsolute12(p) ? resolve13(p) : resolve13(root, p);
8076
- const rel = relative13(root, resolved);
8077
- if (rel === "" || rel === ".") return true;
8078
- if (rel.startsWith("..")) return false;
8079
- if (isAbsolute12(rel)) return false;
8080
- return true;
8081
- }
8082
8266
  var watch_idCounter = 0;
8083
8267
  function nextId() {
8084
8268
  return `watch_${++watch_idCounter}_${Date.now().toString(36)}`;
@@ -8159,7 +8343,7 @@ var plugin24 = {
8159
8343
  }
8160
8344
  const autoIndex = api.config.extensions?.["file-watcher"]?.["autoIndex"] ?? false;
8161
8345
  const indexProjectRoot = api.config.extensions?.["file-watcher"]?.["indexProjectRoot"] ?? "";
8162
- const safeIndexRoot = indexProjectRoot !== "" && withinProject3(indexProjectRoot) ? indexProjectRoot : "";
8346
+ const safeIndexRoot = indexProjectRoot !== "" && withinProject(indexProjectRoot) ? indexProjectRoot : "";
8163
8347
  if (indexProjectRoot !== "" && safeIndexRoot === "") {
8164
8348
  api.log.warn(
8165
8349
  "file-watcher: indexProjectRoot is outside the project root \u2014 using watched dirPath instead",
@@ -8304,7 +8488,7 @@ var plugin24 = {
8304
8488
  }
8305
8489
  const events = input["events"] ?? ["change", "add", "delete"];
8306
8490
  const recursive = input["recursive"] ?? true;
8307
- const bad = paths.find((p) => !withinProject3(p));
8491
+ const bad = paths.find((p) => !withinProject(p));
8308
8492
  if (bad !== void 0) {
8309
8493
  return {
8310
8494
  ok: false,
@@ -8549,6 +8733,7 @@ async function formatFile(filePath, timeoutMs) {
8549
8733
  }
8550
8734
  const formatter = resolveFormatter();
8551
8735
  if (!formatter) return null;
8736
+ const started = Date.now();
8552
8737
  let bytesBefore;
8553
8738
  try {
8554
8739
  bytesBefore = (await stat4(filePath)).size;
@@ -8593,14 +8778,15 @@ async function formatFile(filePath, timeoutMs) {
8593
8778
  } catch {
8594
8779
  return null;
8595
8780
  }
8781
+ const durationMs = Date.now() - started;
8596
8782
  if (bytesAfter !== bytesBefore) {
8597
- return { changed: true, bytesBefore, bytesAfter };
8783
+ return { changed: true, bytesBefore, bytesAfter, durationMs };
8598
8784
  }
8599
8785
  const hashAfter = await sha256File(filePath);
8600
8786
  if (hashBefore === null || hashAfter === null) {
8601
- return { changed: false, bytesBefore, bytesAfter };
8787
+ return { changed: false, bytesBefore, bytesAfter, durationMs };
8602
8788
  }
8603
- return { changed: hashBefore !== hashAfter, bytesBefore, bytesAfter };
8789
+ return { changed: hashBefore !== hashAfter, bytesBefore, bytesAfter, durationMs };
8604
8790
  }
8605
8791
  var plugin25 = {
8606
8792
  name: "format-on-save",
@@ -8689,6 +8875,7 @@ var plugin25 = {
8689
8875
  changed: result.changed,
8690
8876
  bytesBefore: result.bytesBefore,
8691
8877
  bytesAfter: result.bytesAfter,
8878
+ durationMs: result.durationMs,
8692
8879
  when: (/* @__PURE__ */ new Date()).toISOString()
8693
8880
  };
8694
8881
  if (result.changed) {
@@ -8743,7 +8930,10 @@ var plugin25 = {
8743
8930
  formatted: state23.formattedCount,
8744
8931
  clean: state23.cleanCount,
8745
8932
  errors: state23.errorCount,
8746
- coveredSkips: state23.coveredSkipCount
8933
+ coveredSkips: state23.coveredSkipCount,
8934
+ bytesBefore: state23.lastResult?.bytesBefore ?? 0,
8935
+ bytesAfter: state23.lastResult?.bytesAfter ?? 0,
8936
+ durationMs: state23.lastResult?.durationMs ?? 0
8747
8937
  },
8748
8938
  lastResult: state23.lastResult
8749
8939
  };
@@ -9316,7 +9506,7 @@ var git_autocommit_default = plugin26;
9316
9506
 
9317
9507
  // src/gitignore-guard/index.ts
9318
9508
  import { access as access3, readFile as readFile9, writeFile as writeFile2 } from "node:fs/promises";
9319
- import { basename as basename4, isAbsolute as isAbsolute13, join as join4, relative as relative14, resolve as resolve14, sep as sep3 } from "node:path";
9509
+ import { basename as basename4, isAbsolute as isAbsolute12, join as join4, relative as relative13, resolve as resolve13, sep as sep3 } from "node:path";
9320
9510
  var API_VERSION19 = "^0.1.10";
9321
9511
  var DEFAULT_ARTIFACT_PATTERNS = Object.freeze([
9322
9512
  "dist/",
@@ -9438,9 +9628,9 @@ async function appendPatterns(target, patterns, limit) {
9438
9628
  }
9439
9629
  function projectRelativePath(rawPath, cwd) {
9440
9630
  const root = cwd ?? process.cwd();
9441
- const abs = isAbsolute13(rawPath) ? rawPath : resolve14(root, rawPath);
9442
- const rel = toForwardSlashes(relative14(root, abs));
9443
- if (rel.startsWith("..") || isAbsolute13(rel) || rel === "") return null;
9631
+ const abs = isAbsolute12(rawPath) ? rawPath : resolve13(root, rawPath);
9632
+ const rel = toForwardSlashes(relative13(root, abs));
9633
+ if (rel.startsWith("..") || isAbsolute12(rel) || rel === "") return null;
9444
9634
  return { abs, rel, root };
9445
9635
  }
9446
9636
  var DEFAULTS21 = {
@@ -9752,7 +9942,7 @@ var gitignore_guard_default = plugin27;
9752
9942
  // src/import-organizer/index.ts
9753
9943
  import { spawn } from "node:child_process";
9754
9944
  import { existsSync as existsSync3, statSync as statSync2 } from "node:fs";
9755
- import { basename as basename5, isAbsolute as isAbsolute14 } from "node:path";
9945
+ import { basename as basename5, isAbsolute as isAbsolute13 } from "node:path";
9756
9946
  var ALLOWED_FIRST_TOKENS = /* @__PURE__ */ new Set([
9757
9947
  "npx",
9758
9948
  "pnpm",
@@ -9795,17 +9985,31 @@ function resolveAllowedCommand(command) {
9795
9985
  const tokens = command.split(/\s+/).filter(Boolean);
9796
9986
  if (tokens.length === 0) return null;
9797
9987
  const head = tokens[0];
9988
+ if (PACKAGE_RUNNERS.has(head)) {
9989
+ let i = 1;
9990
+ while (i < tokens.length && (tokens[i] === "exec" || tokens[i] === "dlx" || tokens[i] === "run")) {
9991
+ i++;
9992
+ }
9993
+ const toolToken = tokens[i];
9994
+ if (!toolToken || !(toolToken in LOCAL_BIN_PACKAGES)) return null;
9995
+ }
9996
+ if (head === "node") {
9997
+ const target = tokens[1];
9998
+ if (!target) return null;
9999
+ const base = basename5(target);
10000
+ if (!(base in LOCAL_BIN_PACKAGES) && !ALLOWED_FIRST_TOKENS.has(base)) return null;
10001
+ }
9798
10002
  const local = ALLOWED_FIRST_TOKENS.has(head) ? resolveLocalToolCommand(tokens) : null;
9799
10003
  if (local) return local;
9800
10004
  if (ALLOWED_FIRST_TOKENS.has(head)) {
9801
10005
  return { cmd: head, args: tokens.slice(1) };
9802
10006
  }
9803
- if (isAbsolute14(head)) {
10007
+ if (isAbsolute13(head)) {
9804
10008
  if (!withinProject(head)) return null;
9805
10009
  const base = basename5(head);
9806
10010
  if (ALLOWED_FIRST_TOKENS.has(base)) return { cmd: head, args: tokens.slice(1) };
9807
10011
  }
9808
- if (!isAbsolute14(head) && withinProject(head)) {
10012
+ if (!isAbsolute13(head) && withinProject(head)) {
9809
10013
  const base = basename5(head);
9810
10014
  if (ALLOWED_FIRST_TOKENS.has(base)) return { cmd: head, args: tokens.slice(1) };
9811
10015
  }
@@ -10032,6 +10236,12 @@ var plugin28 = {
10032
10236
  bytesAfter: result.bytesAfter,
10033
10237
  when: (/* @__PURE__ */ new Date()).toISOString()
10034
10238
  };
10239
+ if (/\boxlint\b/.test(result.command)) {
10240
+ return {
10241
+ additionalContext: `
10242
+ \u{1F4E6} import-organizer: oxlint has no import-organize support \u2014 ran '${result.command}' as a no-op for import sorting on '${filePath}'.`
10243
+ };
10244
+ }
10035
10245
  if (result.changed) {
10036
10246
  state25.organizedCount += 1;
10037
10247
  const delta = result.bytesAfter - result.bytesBefore;
@@ -10088,6 +10298,7 @@ ${result.stderr.trim()}`
10088
10298
  });
10089
10299
  },
10090
10300
  teardown(api) {
10301
+ clearLocalBinCache();
10091
10302
  if (state25.hookUnregister) {
10092
10303
  try {
10093
10304
  state25.hookUnregister();
@@ -10342,7 +10553,7 @@ var injection_shield_default = plugin29;
10342
10553
 
10343
10554
  // src/interface-contract-guard/index.ts
10344
10555
  import { readFile as readFile10 } from "node:fs/promises";
10345
- import { isAbsolute as isAbsolute15, relative as relative15, resolve as resolve15 } from "node:path";
10556
+ import { isAbsolute as isAbsolute14, relative as relative14, resolve as resolve14 } from "node:path";
10346
10557
  var API_VERSION21 = "^0.1.10";
10347
10558
  var state27 = {
10348
10559
  scanCount: 0,
@@ -10375,7 +10586,7 @@ function toPosix5(p) {
10375
10586
  return p.replace(/\\/g, "/");
10376
10587
  }
10377
10588
  function relativePath5(p) {
10378
- return toPosix5(relative15(process.cwd(), p));
10589
+ return toPosix5(relative14(process.cwd(), p));
10379
10590
  }
10380
10591
  var INTERFACE_RE = /(?:export\s+)?interface\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
10381
10592
  function extractInterfaceNames(content) {
@@ -10403,7 +10614,7 @@ function collectImplementedNames(content, into) {
10403
10614
  }
10404
10615
  async function scanPath3(rawPath, cfg) {
10405
10616
  const root = process.cwd();
10406
- const resolved = isAbsolute15(rawPath) ? resolve15(rawPath) : resolve15(root, rawPath);
10617
+ const resolved = isAbsolute14(rawPath) ? resolve14(rawPath) : resolve14(root, rawPath);
10407
10618
  const exts = normalizeExtensions4(cfg.extensions);
10408
10619
  const allFiles = await collectSourceFilesAsync(resolved, { extensions: exts });
10409
10620
  const files = allFiles.slice(0, cfg.maxFiles);
@@ -10493,7 +10704,7 @@ var plugin30 = {
10493
10704
  const exts = normalizeExtensions4(cfg.extensions);
10494
10705
  if (!matchesExtension(sourcePath, exts)) return;
10495
10706
  state27.hookInvocationCount += 1;
10496
- const resolved = resolve15(process.cwd(), sourcePath);
10707
+ const resolved = resolve14(process.cwd(), sourcePath);
10497
10708
  let content;
10498
10709
  try {
10499
10710
  content = await readFile10(resolved, "utf-8");
@@ -10541,7 +10752,7 @@ If you changed member shapes, search the project for implementers/\`satisfies\`/
10541
10752
  state27.findingCount += result.findings.length;
10542
10753
  return {
10543
10754
  ok: true,
10544
- path: relativePath5(resolve15(process.cwd(), rawPath)),
10755
+ path: relativePath5(resolve14(process.cwd(), rawPath)),
10545
10756
  scannedFiles: result.scannedFiles,
10546
10757
  findings: result.findings,
10547
10758
  // Say so when the corpus was cut short. A partial scan that
@@ -10622,7 +10833,7 @@ var interface_contract_guard_default = plugin30;
10622
10833
 
10623
10834
  // src/knowledge-graph/index.ts
10624
10835
  import { readFileSync as readFileSync7 } from "node:fs";
10625
- import { dirname as dirname5, isAbsolute as isAbsolute16, relative as relative16, resolve as resolve16 } from "node:path";
10836
+ import { dirname as dirname5, isAbsolute as isAbsolute15, relative as relative15, resolve as resolve15 } from "node:path";
10626
10837
  import { atomicWrite as atomicWrite2, ensureDir as ensureDir2 } from "@wrongstack/core/utils";
10627
10838
  var API_VERSION22 = "^0.1.10";
10628
10839
  var state28 = {
@@ -10644,10 +10855,10 @@ var DEFAULTS25 = {
10644
10855
  };
10645
10856
  function resolveProjectPath5(rawPath, cwd = process.cwd()) {
10646
10857
  if (typeof rawPath !== "string" || rawPath.length === 0) return null;
10647
- const root = resolve16(cwd);
10648
- const resolved = isAbsolute16(rawPath) ? resolve16(rawPath) : resolve16(root, rawPath);
10649
- const rel = relative16(root, resolved);
10650
- if (rel === "" || !rel.startsWith("..") && !isAbsolute16(rel)) return resolved;
10858
+ const root = resolve15(cwd);
10859
+ const resolved = isAbsolute15(rawPath) ? resolve15(rawPath) : resolve15(root, rawPath);
10860
+ const rel = relative15(root, resolved);
10861
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute15(rel)) return resolved;
10651
10862
  return null;
10652
10863
  }
10653
10864
  function readConfig26(raw) {
@@ -10946,7 +11157,7 @@ var knowledge_graph_default = plugin31;
10946
11157
 
10947
11158
  // src/license-audit-gate/index.ts
10948
11159
  import { readFileSync as readFileSync8 } from "node:fs";
10949
- import { resolve as resolve17 } from "node:path";
11160
+ import { resolve as resolve16 } from "node:path";
10950
11161
  var API_VERSION23 = "^0.1.10";
10951
11162
  var state29 = {
10952
11163
  invocations: 0,
@@ -10996,24 +11207,10 @@ function extractLicenseStrings(pkg) {
10996
11207
  }
10997
11208
  return [...new Set(out)];
10998
11209
  }
10999
- var INSTALL_RE3 = /(?:^|[;&|]\s*)(npm|pnpm|yarn|bun)\s+(?:install|i|add)\s+([^;&|]+)/gi;
11000
11210
  function parsePackageNames(command) {
11001
- const names = [];
11002
- INSTALL_RE3.lastIndex = 0;
11003
- for (const m of command.matchAll(INSTALL_RE3)) {
11004
- const argString = m[2] ?? "";
11005
- for (const token of argString.split(/\s+/)) {
11006
- if (!token || token.startsWith("-")) continue;
11007
- if (/^(\.|\/|file:|git\+|https?:)/i.test(token) || token.endsWith(".tgz")) continue;
11008
- const cleaned = token.replace(/^['"]|['"]$/g, "");
11009
- if (!cleaned) continue;
11010
- let name = cleaned;
11011
- const at = cleaned.lastIndexOf("@");
11012
- if (at > 0) name = cleaned.slice(0, at);
11013
- if (name) names.push(name);
11014
- }
11015
- }
11016
- return [...new Set(names)];
11211
+ return [
11212
+ ...new Set(parseInstallCommands(command).flatMap((entry) => entry.packages.map((pkg) => pkg.name)))
11213
+ ];
11017
11214
  }
11018
11215
  function auditPackages(names, allowedLicenses) {
11019
11216
  const results = [];
@@ -11022,7 +11219,7 @@ function auditPackages(names, allowedLicenses) {
11022
11219
  for (const name of names) {
11023
11220
  let licenses = [];
11024
11221
  try {
11025
- const pkgPath = resolve17("node_modules", name, "package.json");
11222
+ const pkgPath = resolve16("node_modules", name, "package.json");
11026
11223
  const raw = JSON.parse(readFileSync8(pkgPath, "utf-8"));
11027
11224
  licenses = extractLicenseStrings(raw);
11028
11225
  } catch {
@@ -11207,7 +11404,7 @@ import { readFileSync as readFileSync9 } from "node:fs";
11207
11404
  import { mkdtemp, readFile as readFile11, rm, writeFile as writeFile3 } from "node:fs/promises";
11208
11405
  import { createRequire as createRequire2 } from "node:module";
11209
11406
  import { tmpdir } from "node:os";
11210
- import { dirname as dirname6, isAbsolute as isAbsolute17, join as join5, relative as relative17, resolve as resolve18, sep as sep4 } from "node:path";
11407
+ import { dirname as dirname6, isAbsolute as isAbsolute16, join as join5, relative as relative16, resolve as resolve17, sep as sep4 } from "node:path";
11211
11408
  var API_VERSION24 = "^0.1.10";
11212
11409
  var state30 = {
11213
11410
  /** Total PreToolUse invocations. */
@@ -11247,19 +11444,19 @@ var LINTER_PACKAGES = {
11247
11444
  };
11248
11445
  var linterCache = new BoundedMap({ max: 32, ttlMs: 3e5 });
11249
11446
  function isInside2(parent, candidate) {
11250
- const rel = relative17(parent, candidate);
11251
- return rel === "" || !rel.startsWith(`..${sep4}`) && rel !== ".." && !isAbsolute17(rel);
11447
+ const rel = relative16(parent, candidate);
11448
+ return rel === "" || !rel.startsWith(`..${sep4}`) && rel !== ".." && !isAbsolute16(rel);
11252
11449
  }
11253
11450
  function resolveLocalLinter(name, cwd) {
11254
11451
  try {
11255
11452
  const packageName = LINTER_PACKAGES[name];
11256
- const requireFromProject = createRequire2(resolve18(cwd, "package.json"));
11453
+ const requireFromProject = createRequire2(resolve17(cwd, "package.json"));
11257
11454
  const packagePath = requireFromProject.resolve(`${packageName}/package.json`);
11258
11455
  const packageJson = JSON.parse(readFileSync9(packagePath, "utf-8"));
11259
11456
  const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[name] ?? Object.values(packageJson.bin ?? {})[0];
11260
- if (!relativeBin || isAbsolute17(relativeBin)) return null;
11457
+ if (!relativeBin || isAbsolute16(relativeBin)) return null;
11261
11458
  const packageDir = dirname6(packagePath);
11262
- const entry = resolve18(packageDir, relativeBin);
11459
+ const entry = resolve17(packageDir, relativeBin);
11263
11460
  if (!isInside2(packageDir, entry)) return null;
11264
11461
  return {
11265
11462
  cmd: process.execPath,
@@ -11311,9 +11508,10 @@ async function detectLinter(requested, cwd) {
11311
11508
  }
11312
11509
  async function lintContent(content, filePath, linter, timeoutMs, cwd, signal) {
11313
11510
  const ext = filePath.includes(".") ? filePath.slice(filePath.lastIndexOf(".")) : ".ts";
11314
- const tmpDir = await mkdtemp(join5(tmpdir(), "lint-gate-"));
11315
- const tmpFile = join5(tmpDir, `input${ext}`);
11511
+ let tmpDir;
11316
11512
  try {
11513
+ tmpDir = await mkdtemp(join5(tmpdir(), "lint-gate-"));
11514
+ const tmpFile = join5(tmpDir, `input${ext}`);
11317
11515
  await writeFile3(tmpFile, content, "utf-8");
11318
11516
  const fullArgs = [...linter.args, tmpFile];
11319
11517
  const result = await runCommand2(linter.cmd, fullArgs, timeoutMs, cwd, signal);
@@ -11324,14 +11522,15 @@ async function lintContent(content, filePath, linter, timeoutMs, cwd, signal) {
11324
11522
  if (signal.aborted) throw signal.reason;
11325
11523
  return null;
11326
11524
  } finally {
11327
- await rm(tmpDir, { recursive: true, force: true }).catch(() => void 0);
11525
+ if (tmpDir) await rm(tmpDir, { recursive: true, force: true }).catch(() => void 0);
11328
11526
  }
11329
11527
  }
11330
11528
  async function lintAndFix(content, filePath, linter, timeoutMs, cwd, signal) {
11331
11529
  const ext = filePath.includes(".") ? filePath.slice(filePath.lastIndexOf(".")) : ".ts";
11332
- const tmpDir = await mkdtemp(join5(tmpdir(), "lint-gate-fix-"));
11333
- const tmpFile = join5(tmpDir, `input${ext}`);
11530
+ let tmpDir;
11334
11531
  try {
11532
+ tmpDir = await mkdtemp(join5(tmpdir(), "lint-gate-fix-"));
11533
+ const tmpFile = join5(tmpDir, `input${ext}`);
11335
11534
  await writeFile3(tmpFile, content, "utf-8");
11336
11535
  const fixArgs = linter.name === "biome" ? [linter.args[0], "check", "--write", tmpFile] : [linter.args[0], "--fix", tmpFile];
11337
11536
  await runCommand2(linter.cmd, fixArgs, timeoutMs, cwd, signal);
@@ -11341,7 +11540,7 @@ async function lintAndFix(content, filePath, linter, timeoutMs, cwd, signal) {
11341
11540
  if (signal.aborted) throw signal.reason;
11342
11541
  return content;
11343
11542
  } finally {
11344
- await rm(tmpDir, { recursive: true, force: true }).catch(() => void 0);
11543
+ if (tmpDir) await rm(tmpDir, { recursive: true, force: true }).catch(() => void 0);
11345
11544
  }
11346
11545
  }
11347
11546
  function parseLinterOutput(stdout, linterName) {
@@ -11436,7 +11635,7 @@ var plugin33 = {
11436
11635
  state30.lastResult = null;
11437
11636
  linterCache.clear();
11438
11637
  const cfg = readConfig28(api.config.extensions?.["lint-gate"]);
11439
- const cwd = resolve18(api.config.cwd ?? process.cwd());
11638
+ const cwd = resolve17(api.config.cwd ?? process.cwd());
11440
11639
  const linterReady = detectLinter(cfg.linter, cwd).then((linter) => {
11441
11640
  if (!linter) {
11442
11641
  api.log.warn("lint-gate: no linter found (biome or eslint) \u2014 hook will be a no-op", {
@@ -11595,9 +11794,9 @@ ${summary}${truncated}`
11595
11794
  name: "lint-gate",
11596
11795
  stage: "mutate",
11597
11796
  timeoutMs: Math.max(1e3, cfg.timeoutMs + 1e3),
11598
- // Formatter/linter availability must not create approval or denial
11599
- // loops in YOLO mode. Explicit lint findings still block in block mode.
11600
- failurePolicy: "open"
11797
+ // Fail closed: a linter crash or timeout must not let unlinted
11798
+ // content through in block mode (issue #363).
11799
+ failurePolicy: "closed"
11601
11800
  });
11602
11801
  api.tools.register({
11603
11802
  name: "lint_gate_status",
@@ -11753,6 +11952,12 @@ var plugin34 = {
11753
11952
  description: "Caches identical provider requests and short-circuits the provider call on a hit (wrapProviderRunner). Opt-in; deterministic-only by default.",
11754
11953
  apiVersion: "^0.1.10",
11755
11954
  capabilities: { tools: true },
11955
+ // Wrap-stack contract (issue #362): ExtensionRegistry composes wrappers
11956
+ // first-registered = outermost, and both dependsOn and optionalDeps load
11957
+ // dependencies first. Declaring prompt-firewall here guarantees the
11958
+ // firewall wraps OUTSIDE this cache even if the manifest order changes —
11959
+ // otherwise a cache hit short-circuits `inner` and bypasses redaction.
11960
+ optionalDeps: ["prompt-firewall"],
11756
11961
  defaultConfig: { ...DEFAULTS28 },
11757
11962
  configSchema: {
11758
11963
  type: "object",
@@ -11933,7 +12138,7 @@ var llm_cache_default = plugin34;
11933
12138
 
11934
12139
  // src/loop-breaker/index.ts
11935
12140
  import { execFile as execFile9 } from "node:child_process";
11936
- import { isAbsolute as isAbsolute18, relative as relative18 } from "node:path";
12141
+ import { isAbsolute as isAbsolute17, relative as relative17 } from "node:path";
11937
12142
  var state32 = {
11938
12143
  lastFingerprint: null,
11939
12144
  streak: 0,
@@ -12044,7 +12249,7 @@ function hashString2(value) {
12044
12249
  return String(h >>> 0);
12045
12250
  }
12046
12251
  async function gitDiffFingerprint(cwd, targetPath, signal) {
12047
- const pathspec = isAbsolute18(targetPath) ? relative18(cwd, targetPath) : targetPath;
12252
+ const pathspec = isAbsolute17(targetPath) ? relative17(cwd, targetPath) : targetPath;
12048
12253
  if (!pathspec || pathspec === ".." || pathspec.startsWith("../") || pathspec.startsWith("..\\")) {
12049
12254
  return null;
12050
12255
  }
@@ -13579,6 +13784,29 @@ var plugin38 = {
13579
13784
  var notify_hub_default = plugin38;
13580
13785
 
13581
13786
  // src/path-guard/glob.ts
13787
+ var GLOB_REDOS_BUDGET_MS = 250;
13788
+ function mergeGuardedRegex(patterns) {
13789
+ const flags = /* @__PURE__ */ new Set();
13790
+ for (const p of patterns) {
13791
+ for (const f of p.flags) {
13792
+ if (f !== "g" && f !== "y") flags.add(f);
13793
+ }
13794
+ }
13795
+ const uniqueFlags = [...flags].join("");
13796
+ return new RegExp(patterns.map((p) => p.source).join("|"), uniqueFlags);
13797
+ }
13798
+ async function matchesAnyGuarded(path, patterns, options = {}) {
13799
+ const normalized = normalizePath2(path);
13800
+ if (patterns.length === 0) return false;
13801
+ const result = await withReDoSGuard(
13802
+ mergeGuardedRegex(patterns),
13803
+ normalized,
13804
+ options.budgetMs ?? GLOB_REDOS_BUDGET_MS,
13805
+ options.onTimeout ? { onTimeout: options.onTimeout } : {}
13806
+ );
13807
+ if (result.timedOut) return true;
13808
+ return result.match !== null;
13809
+ }
13582
13810
  function compilePathGlob(pattern) {
13583
13811
  const normalized = pattern.replace(/\\/g, "/");
13584
13812
  let source = "";
@@ -13713,8 +13941,7 @@ function scopesMayOverlap(left, right) {
13713
13941
  }
13714
13942
  return hasPartialSegmentWildcard(left) && rightPrefix.startsWith(leftPrefix) || hasPartialSegmentWildcard(right) && leftPrefix.startsWith(rightPrefix);
13715
13943
  }
13716
- function targetIntersectsPatterns(target, patternTexts, patterns) {
13717
- if (matchesAny(target.path, patterns)) return true;
13944
+ function targetIntersectsScope(target, patternTexts) {
13718
13945
  if (target.kind === "file") return false;
13719
13946
  const normalized = normalizePath2(target.path).replace(/\/$/, "");
13720
13947
  if (!isUnresolvedPathScope(normalized)) {
@@ -13726,6 +13953,14 @@ function targetIntersectsPatterns(target, patternTexts, patterns) {
13726
13953
  }
13727
13954
  return patternTexts.some((pattern) => scopesMayOverlap(normalized, normalizePath2(pattern)));
13728
13955
  }
13956
+ function targetIntersectsPatterns(target, patternTexts, patterns) {
13957
+ if (matchesAny(target.path, patterns)) return true;
13958
+ return targetIntersectsScope(target, patternTexts);
13959
+ }
13960
+ async function targetIntersectsPatternsGuarded(target, patternTexts, patterns, options = {}) {
13961
+ if (await matchesAnyGuarded(target.path, patterns, options)) return true;
13962
+ return targetIntersectsScope(target, patternTexts);
13963
+ }
13729
13964
  function targetFullyAllowed(target, allowTexts, allowRes) {
13730
13965
  if (target.kind === "file") return matchesAny(target.path, allowRes);
13731
13966
  const normalized = normalizePath2(target.path).replace(/\/$/, "");
@@ -14042,7 +14277,7 @@ function commandRecursivelyDeletes(command) {
14042
14277
  )) {
14043
14278
  return true;
14044
14279
  }
14045
- const destructive = /(?:^|[;&|\r\n]\s*|\{\s*|\(\s*|\bxargs(?:\s+-[^\s]+)*\s+)(rm|rmdir|del)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)/gi;
14280
+ const destructive = /(?:^|[;&|\r\n]\s*|\{\s*|\(\s*|\bxargs(?:\s+-[^\s]+)*\s+)(rm|rmdir|del|rd|Remove-Item)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)/gi;
14046
14281
  let match = destructive.exec(stripped);
14047
14282
  while (match !== null) {
14048
14283
  const tool = match[1]?.toLowerCase();
@@ -14055,6 +14290,8 @@ function commandRecursivelyDeletes(command) {
14055
14290
  if (token === "--") break;
14056
14291
  if (tool === "rm") {
14057
14292
  if (token === "--recursive" || /^-[^-]*[rR]/.test(token)) recursive = true;
14293
+ } else if (tool === "remove-item") {
14294
+ if (/^-Recurse$/i.test(token) || /^-r$/i.test(token)) recursive = true;
14058
14295
  } else if (/^\/[a-z]*s[a-z]*$/i.test(token)) {
14059
14296
  recursive = true;
14060
14297
  }
@@ -14303,7 +14540,7 @@ function destructiveTargetsAtDepth(command, depth) {
14303
14540
  }
14304
14541
  targets.push(...destructiveTargetsAtDepth(executableBody, depth + 1));
14305
14542
  }
14306
- const destructive = /(?:^|[;&|\r\n]\s*|\{\s*|(?<![$(])\(\s*|\bxargs(?:\s+-[^\s]+)*\s+)(?:sudo\s+)?(rm|rmdir|del|unlink|truncate|shred|mv)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)/gi;
14543
+ const destructive = /(?:^|[;&|\r\n]\s*|\{\s*|(?<![$(])\(\s*|\bxargs(?:\s+-[^\s]+)*\s+)(?:sudo\s+)?(rm|rmdir|del|rd|Remove-Item|unlink|truncate|shred|mv)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)/gi;
14307
14544
  let m = destructive.exec(normalizedCommand);
14308
14545
  while (m !== null) {
14309
14546
  if (!tokenIsQuoted(m, m[1] ?? "")) targets.push(...shellArgs(m[2] ?? ""));
@@ -14791,8 +15028,27 @@ function operationLabel(toolName) {
14791
15028
  }
14792
15029
 
14793
15030
  // src/path-guard/index.ts
15031
+ import { realpathSync } from "node:fs";
15032
+ import { resolve as resolve18 } from "node:path";
15033
+ function isSymlinkEscape(path, cwd) {
15034
+ if (!withinProject(path)) return false;
15035
+ try {
15036
+ const abs = resolve18(cwd ?? process.cwd(), path);
15037
+ const real = realpathSync(abs);
15038
+ return !withinProject(real);
15039
+ } catch {
15040
+ return false;
15041
+ }
15042
+ }
14794
15043
  function createState() {
14795
- return { invocations: 0, blocks: 0, warns: 0, lastBlock: null, hookUnregister: null };
15044
+ return {
15045
+ invocations: 0,
15046
+ blocks: 0,
15047
+ warns: 0,
15048
+ redosTimeouts: 0,
15049
+ lastBlock: null,
15050
+ hookUnregister: null
15051
+ };
14796
15052
  }
14797
15053
  var states = /* @__PURE__ */ new WeakMap();
14798
15054
  var latestState = createState();
@@ -14887,7 +15143,11 @@ var plugin39 = {
14887
15143
  additionalContext: `path-guard (warn mode): ${subject} and this ${operation} would modify it. Double-check this is intentional.`
14888
15144
  };
14889
15145
  };
14890
- const hook = (input) => {
15146
+ const bumpRedos = () => {
15147
+ state60.redosTimeouts += 1;
15148
+ api.metrics.counter("redos_timeouts");
15149
+ };
15150
+ const hook = async (input) => {
14891
15151
  if (!cfg.enabled) return;
14892
15152
  state60.invocations += 1;
14893
15153
  const toolName = input.toolName ?? "";
@@ -14908,21 +15168,27 @@ var plugin39 = {
14908
15168
  const effectiveCwd = effectiveToolCwd(ti["cwd"], input.cwd);
14909
15169
  const recursivelyDeletes = commandRecursivelyDeletes(commandForInspection);
14910
15170
  const deletesImplicitScope = commandDeletesImplicitScope(commandForInspection);
14911
- for (const path of shellTargets) {
14912
- const target = {
14913
- path: relativeToInvocationCwd(resolveTargetPath(path, effectiveCwd), input.cwd),
14914
- kind: isRootPathScope(path) && deletesImplicitScope || isUnresolvedPathScope(path) || recursivelyDeletes && (isDirectoryAmbiguousPath(path) || hasConfiguredProtectedDescendant(path, cfg.protect)) ? "deletion-scope" : "file"
14915
- };
14916
- if (targetFullyAllowed(target, cfg.allow, allowRes)) continue;
14917
- const protectedShellTarget = targetIntersectsPatterns(target, cfg.protect, protectRes) || matchesAny(`${target.path.replace(/\/$/, "")}/.path-guard-probe`, protectRes);
14918
- if (protectedShellTarget) {
14919
- return verdict(
14920
- target.path,
14921
- toolName,
14922
- "destructive shell command",
14923
- target.kind !== "file"
14924
- );
14925
- }
15171
+ const shellHits = await Promise.all(
15172
+ shellTargets.map(async (path) => {
15173
+ const target = {
15174
+ path: relativeToInvocationCwd(resolveTargetPath(path, effectiveCwd), input.cwd),
15175
+ kind: isRootPathScope(path) && deletesImplicitScope || isUnresolvedPathScope(path) || recursivelyDeletes && (isDirectoryAmbiguousPath(path) || hasConfiguredProtectedDescendant(path, cfg.protect)) ? "deletion-scope" : "file"
15176
+ };
15177
+ if (targetFullyAllowed(target, cfg.allow, allowRes)) return null;
15178
+ const protectedShellTarget = await targetIntersectsPatternsGuarded(target, cfg.protect, protectRes, { onTimeout: bumpRedos }) || await matchesAnyGuarded(`${target.path.replace(/\/$/, "")}/.path-guard-probe`, protectRes, {
15179
+ onTimeout: bumpRedos
15180
+ });
15181
+ return protectedShellTarget ? target : null;
15182
+ })
15183
+ );
15184
+ const firstProtected = shellHits.find((t) => t !== null);
15185
+ if (firstProtected) {
15186
+ return verdict(
15187
+ firstProtected.path,
15188
+ toolName,
15189
+ "destructive shell command",
15190
+ firstProtected.kind !== "file"
15191
+ );
14926
15192
  }
14927
15193
  const writes = writesToDisk({ ...input, toolInput: ti });
14928
15194
  const hasStructuredTarget = [...PATH_FIELDS, ...PATH_LIST_FIELDS].some((field) => ti[field] !== void 0) || typeof ti["patch"] === "string";
@@ -14933,11 +15199,21 @@ var plugin39 = {
14933
15199
  }
14934
15200
  if (!writesToDisk({ ...input, toolInput: ti }) || isReadOnlyInvocation(toolName, ti)) return;
14935
15201
  const targets = pathsFromToolInput(ti, toolName, input.cwd);
14936
- for (const target of targets) {
14937
- if (targetFullyAllowed(target, cfg.allow, allowRes)) continue;
14938
- if (targetIntersectsPatterns(target, cfg.protect, protectRes)) {
14939
- return verdict(target.path, toolName, operationLabel(toolName), target.kind !== "file");
14940
- }
15202
+ const writeHits = await Promise.all(
15203
+ targets.map(async (target) => {
15204
+ if (target.kind === "file" && isSymlinkEscape(target.path, input.cwd)) return target;
15205
+ if (targetFullyAllowed(target, cfg.allow, allowRes)) return null;
15206
+ return targetIntersectsPatterns(target, cfg.protect, protectRes) ? target : null;
15207
+ })
15208
+ );
15209
+ const firstProtectedWrite = writeHits.find((t) => t !== null);
15210
+ if (firstProtectedWrite) {
15211
+ return verdict(
15212
+ firstProtectedWrite.path,
15213
+ toolName,
15214
+ operationLabel(toolName),
15215
+ firstProtectedWrite.kind !== "file"
15216
+ );
14941
15217
  }
14942
15218
  return;
14943
15219
  };
@@ -14964,7 +15240,8 @@ var plugin39 = {
14964
15240
  counters: {
14965
15241
  invocations: state60.invocations,
14966
15242
  blocks: state60.blocks,
14967
- warns: state60.warns
15243
+ warns: state60.warns,
15244
+ redosTimeouts: state60.redosTimeouts
14968
15245
  },
14969
15246
  lastBlock: state60.lastBlock
14970
15247
  };
@@ -14987,10 +15264,16 @@ var plugin39 = {
14987
15264
  }
14988
15265
  state60.hookUnregister = null;
14989
15266
  }
14990
- const final = { invocations: state60.invocations, blocks: state60.blocks, warns: state60.warns };
15267
+ const final = {
15268
+ invocations: state60.invocations,
15269
+ blocks: state60.blocks,
15270
+ warns: state60.warns,
15271
+ redosTimeouts: state60.redosTimeouts
15272
+ };
14991
15273
  state60.invocations = 0;
14992
15274
  state60.blocks = 0;
14993
15275
  state60.warns = 0;
15276
+ state60.redosTimeouts = 0;
14994
15277
  state60.lastBlock = null;
14995
15278
  states.delete(api);
14996
15279
  api.log.info("path-guard: teardown complete", { final });
@@ -15003,7 +15286,8 @@ var plugin39 = {
15003
15286
  counters: {
15004
15287
  invocations: state60.invocations,
15005
15288
  blocks: state60.blocks,
15006
- warns: state60.warns
15289
+ warns: state60.warns,
15290
+ redosTimeouts: state60.redosTimeouts
15007
15291
  }
15008
15292
  };
15009
15293
  }
@@ -15012,7 +15296,7 @@ var path_guard_default = plugin39;
15012
15296
 
15013
15297
  // src/performance-regression-gate/index.ts
15014
15298
  import { existsSync as existsSync5, readFileSync as readFileSync11 } from "node:fs";
15015
- import { isAbsolute as isAbsolute19, relative as relative19, resolve as resolve19 } from "node:path";
15299
+ import { isAbsolute as isAbsolute18, relative as relative18, resolve as resolve19 } from "node:path";
15016
15300
  var API_VERSION26 = "^0.1.10";
15017
15301
  var state36 = {
15018
15302
  invocationCount: 0,
@@ -15035,21 +15319,21 @@ function readConfig35(raw) {
15035
15319
  thresholdPercent: threshold
15036
15320
  };
15037
15321
  }
15038
- function withinProject4(p, cwd = process.cwd()) {
15322
+ function withinProject3(p, cwd = process.cwd()) {
15039
15323
  if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
15040
15324
  const root = resolve19(cwd);
15041
- const resolved = isAbsolute19(p) ? resolve19(p) : resolve19(root, p);
15042
- const rel = relative19(root, resolved);
15325
+ const resolved = isAbsolute18(p) ? resolve19(p) : resolve19(root, p);
15326
+ const rel = relative18(root, resolved);
15043
15327
  if (rel === "" || rel === ".") return true;
15044
15328
  if (rel.startsWith("..")) return false;
15045
- if (isAbsolute19(rel)) return false;
15329
+ if (isAbsolute18(rel)) return false;
15046
15330
  return true;
15047
15331
  }
15048
15332
  function resolveProjectPath6(rawPath, cwd = process.cwd()) {
15049
15333
  if (typeof rawPath !== "string" || rawPath.length === 0) return null;
15050
15334
  const root = resolve19(cwd);
15051
- const resolved = isAbsolute19(rawPath) ? resolve19(rawPath) : resolve19(root, rawPath);
15052
- if (!withinProject4(resolved, cwd)) return null;
15335
+ const resolved = isAbsolute18(rawPath) ? resolve19(rawPath) : resolve19(root, rawPath);
15336
+ if (!withinProject3(resolved, cwd)) return null;
15053
15337
  return resolved;
15054
15338
  }
15055
15339
  function isValidNumber(n) {
@@ -15463,7 +15747,7 @@ var plugin_stack_observer_default = PLUGIN;
15463
15747
  // src/pr-drafter/index.ts
15464
15748
  import { execFile as execFile10 } from "node:child_process";
15465
15749
  import { mkdir as mkdir2, writeFile as writeFile4 } from "node:fs/promises";
15466
- import { dirname as dirname7, isAbsolute as isAbsolute20, relative as relative20, resolve as resolve20 } from "node:path";
15750
+ import { dirname as dirname7, isAbsolute as isAbsolute19, relative as relative19, resolve as resolve20 } from "node:path";
15467
15751
  var API_VERSION27 = "^0.1.10";
15468
15752
  var state38 = {
15469
15753
  commits: [],
@@ -15503,9 +15787,9 @@ function readConfig37(raw) {
15503
15787
  function resolveProjectPath7(rawPath, cwd = process.cwd()) {
15504
15788
  if (typeof rawPath !== "string" || rawPath.length === 0) return null;
15505
15789
  const root = resolve20(cwd);
15506
- const resolved = isAbsolute20(rawPath) ? resolve20(rawPath) : resolve20(root, rawPath);
15507
- const rel = relative20(root, resolved);
15508
- if (rel === "" || !rel.startsWith("..") && !isAbsolute20(rel)) return resolved;
15790
+ const resolved = isAbsolute19(rawPath) ? resolve20(rawPath) : resolve20(root, rawPath);
15791
+ const rel = relative19(root, resolved);
15792
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute19(rel)) return resolved;
15509
15793
  return null;
15510
15794
  }
15511
15795
  function runGit4(args, timeout) {
@@ -15949,6 +16233,9 @@ var plugin42 = {
15949
16233
  };
15950
16234
  var process_guard_default = plugin42;
15951
16235
 
16236
+ // src/prompt-firewall/index.ts
16237
+ import { performance as performance2 } from "node:perf_hooks";
16238
+
15952
16239
  // src/runtime/credential-patterns.ts
15953
16240
  var CREDENTIAL_PATTERNS = [
15954
16241
  // LLM provider keys
@@ -16120,32 +16407,156 @@ var PATTERNS2 = [
16120
16407
  })),
16121
16408
  ...EXTRA_PATTERNS
16122
16409
  ];
16123
- function detectSecrets(text, allow) {
16124
- const counts = /* @__PURE__ */ new Map();
16125
- for (const p of PATTERNS2) {
16126
- p.re.lastIndex = 0;
16127
- let m = p.re.exec(text);
16410
+ var PATTERN_BUDGET_MS = 250;
16411
+ var GUARD_PROBE_LENGTH = 1e5;
16412
+ var GUARD_PROBE_OVERLAP = 4096;
16413
+ var SCAN_PASS_BUDGET_MS = 250;
16414
+ function createScanDeadline() {
16415
+ return { deadline: performance2.now() + SCAN_PASS_BUDGET_MS, tripped: /* @__PURE__ */ new Set() };
16416
+ }
16417
+ var SCAN_WINDOW_STRIDE = GUARD_PROBE_LENGTH - GUARD_PROBE_OVERLAP;
16418
+ var SCAN_WINDOW_TAIL = 65536;
16419
+ var SCAN_WINDOW_LIMIT = GUARD_PROBE_LENGTH + SCAN_WINDOW_TAIL;
16420
+ var SCAN_MAX_GROWTH = 16;
16421
+ function growMatch(re, p, text, absStart, deadline) {
16422
+ let size = GUARD_PROBE_LENGTH;
16423
+ for (let grown = 0; grown < SCAN_MAX_GROWTH; grown++) {
16424
+ if (deadline && performance2.now() > deadline.deadline) {
16425
+ deadline.tripped.add(p.kind);
16426
+ return null;
16427
+ }
16428
+ const extEnd = Math.min(text.length, absStart + size);
16429
+ const ext = text.slice(absStart, extEnd);
16430
+ re.lastIndex = 0;
16431
+ const em = re.exec(ext);
16432
+ if (!em || em.index !== 0 || em[0].length === 0) return null;
16433
+ const end = absStart + em[0].length;
16434
+ if (end < extEnd || extEnd === text.length) {
16435
+ return { start: absStart, end, matched: em[0] };
16436
+ }
16437
+ size *= 2;
16438
+ }
16439
+ return null;
16440
+ }
16441
+ function* execWindowed(p, text, deadline) {
16442
+ const re = new RegExp(p.re.source, p.re.flags);
16443
+ let acceptLo = 0;
16444
+ let highWater = 0;
16445
+ for (let window = 0; acceptLo < text.length; window++) {
16446
+ const sliceStart = window === 0 ? 0 : acceptLo - GUARD_PROBE_OVERLAP;
16447
+ const sliceEnd = Math.min(text.length, sliceStart + SCAN_WINDOW_LIMIT);
16448
+ const slice = text.slice(sliceStart, sliceEnd);
16449
+ const acceptHi = Math.min(acceptLo + SCAN_WINDOW_STRIDE, text.length);
16450
+ re.lastIndex = 0;
16451
+ let m = re.exec(slice);
16128
16452
  while (m !== null) {
16453
+ if (deadline && performance2.now() > deadline.deadline) {
16454
+ deadline.tripped.add(p.kind);
16455
+ return;
16456
+ }
16129
16457
  const matched = m[0];
16130
- if (!allow.some((a) => a.test(matched))) {
16131
- counts.set(p.kind, (counts.get(p.kind) ?? 0) + 1);
16458
+ if (matched.length === 0) {
16459
+ re.lastIndex += 1;
16460
+ m = re.exec(slice);
16461
+ continue;
16462
+ }
16463
+ const absStart = sliceStart + m.index;
16464
+ const absEnd = absStart + matched.length;
16465
+ if (absStart >= acceptLo && absStart < acceptHi) {
16466
+ let final = { start: absStart, end: absEnd, matched };
16467
+ if (absEnd === sliceEnd && sliceEnd < text.length) {
16468
+ final = growMatch(re, p, text, absStart, deadline) ?? final;
16469
+ re.lastIndex = m.index + final.matched.length;
16470
+ }
16471
+ if (final.start >= highWater) {
16472
+ highWater = Math.max(highWater, final.end);
16473
+ yield final;
16474
+ }
16132
16475
  }
16133
- m = p.re.exec(text);
16476
+ m = re.exec(slice);
16477
+ }
16478
+ acceptLo += SCAN_WINDOW_STRIDE;
16479
+ }
16480
+ }
16481
+ function countMatches(p, text, allow, counts, deadline) {
16482
+ if (deadline && performance2.now() > deadline.deadline) {
16483
+ deadline.tripped.add(p.kind);
16484
+ return;
16485
+ }
16486
+ for (const m of execWindowed(p, text, deadline)) {
16487
+ if (!allow.some((a) => a.test(m.matched))) {
16488
+ counts.set(p.kind, (counts.get(p.kind) ?? 0) + 1);
16489
+ }
16490
+ }
16491
+ }
16492
+ function replacePattern(p, text, allow, redactions, deadline) {
16493
+ if (deadline && performance2.now() > deadline.deadline) {
16494
+ deadline.tripped.add(p.kind);
16495
+ return text;
16496
+ }
16497
+ let out = "";
16498
+ let copied = 0;
16499
+ for (const m of execWindowed(p, text, deadline)) {
16500
+ out += text.slice(copied, m.start);
16501
+ if (allow.some((a) => a.test(m.matched))) {
16502
+ out += m.matched;
16503
+ } else {
16504
+ redactions.n += 1;
16505
+ out += `[REDACTED:${p.kind}]`;
16134
16506
  }
16507
+ copied = m.end;
16135
16508
  }
16136
- return [...counts.entries()].map(([kind, count]) => ({ kind, count }));
16509
+ return out + text.slice(copied);
16137
16510
  }
16138
- function redactSecrets(text, allow) {
16511
+ function redactSecrets(text, allow, deadline) {
16512
+ const redactions = { n: 0 };
16139
16513
  let out = text;
16140
- let redactions = 0;
16141
- for (const p of PATTERNS2) {
16142
- out = out.replace(new RegExp(p.re.source, p.re.flags), (match) => {
16143
- if (allow.some((a) => a.test(match))) return match;
16144
- redactions += 1;
16145
- return `[REDACTED:${p.kind}]`;
16514
+ for (const p of PATTERNS2) out = replacePattern(p, out, allow, redactions, deadline);
16515
+ return { text: out, redactions: redactions.n };
16516
+ }
16517
+ var DISTINCT_PATTERN_KINDS = new Set(PATTERNS2.map((p) => p.kind)).size;
16518
+ async function probeTimedOutPatterns(text) {
16519
+ const timedOut = /* @__PURE__ */ new Set();
16520
+ if (text.length === 0) return timedOut;
16521
+ const probeWindow = (offset) => {
16522
+ const window = text.slice(offset, offset + GUARD_PROBE_LENGTH);
16523
+ const combined = new RegExp(PATTERNS2.map((p) => `(${p.re.source})`).join("|"), "gi");
16524
+ return withReDoSGuard(combined, window, PATTERN_BUDGET_MS).then(async (combinedResult) => {
16525
+ if (!combinedResult.timedOut) return;
16526
+ for (const p of PATTERNS2) {
16527
+ if (timedOut.has(p.kind)) continue;
16528
+ const result = await withReDoSGuard(p.re, window, PATTERN_BUDGET_MS);
16529
+ if (result.timedOut) timedOut.add(p.kind);
16530
+ }
16146
16531
  });
16532
+ };
16533
+ const stride = GUARD_PROBE_LENGTH - GUARD_PROBE_OVERLAP;
16534
+ for (let offset = 0; offset < text.length; offset += stride) {
16535
+ if (timedOut.size >= DISTINCT_PATTERN_KINDS) break;
16536
+ await probeWindow(offset);
16147
16537
  }
16148
- return { text: out, redactions };
16538
+ return timedOut;
16539
+ }
16540
+ async function detectSecretsGuarded(text, allow) {
16541
+ const timedOut = await probeTimedOutPatterns(text);
16542
+ const deadline = createScanDeadline();
16543
+ const counts = /* @__PURE__ */ new Map();
16544
+ for (const p of PATTERNS2) {
16545
+ if (!timedOut.has(p.kind)) countMatches(p, text, allow, counts, deadline);
16546
+ }
16547
+ const allSkipped = /* @__PURE__ */ new Set([...timedOut, ...deadline.tripped]);
16548
+ return {
16549
+ detections: [...counts.entries()].map(([kind, count]) => ({ kind, count })),
16550
+ skipped: [...allSkipped].sort().map((kind) => ({ kind, reason: "redos-timeout" }))
16551
+ };
16552
+ }
16553
+ function redactSecretsGuarded(text, allow, skip, deadline) {
16554
+ const redactions = { n: 0 };
16555
+ let out = text;
16556
+ for (const p of PATTERNS2) {
16557
+ if (!skip.has(p.kind)) out = replacePattern(p, out, allow, redactions, deadline);
16558
+ }
16559
+ return { text: out, redactions: redactions.n };
16149
16560
  }
16150
16561
  function collectText(request) {
16151
16562
  const parts = [];
@@ -16159,17 +16570,27 @@ function collectText(request) {
16159
16570
  walk(request["messages"]);
16160
16571
  return parts.join("\n");
16161
16572
  }
16162
- function redactDeep(value, allow, counter) {
16573
+ var RESPONSE_SCAN_BUDGET = 1e6;
16574
+ function redactDeep(value, allow, counter, skip, budget, deadline) {
16163
16575
  if (typeof value === "string") {
16164
- const { text, redactions } = redactSecrets(value, allow);
16576
+ if (budget) {
16577
+ if (value.length > budget.remaining) {
16578
+ budget.remaining = 0;
16579
+ budget.truncated = true;
16580
+ return value;
16581
+ }
16582
+ budget.remaining -= value.length;
16583
+ }
16584
+ const { text, redactions } = skip ? redactSecretsGuarded(value, allow, skip, deadline) : redactSecrets(value, allow, deadline);
16165
16585
  counter.n += redactions;
16166
16586
  return text;
16167
16587
  }
16168
- if (Array.isArray(value)) return value.map((v) => redactDeep(v, allow, counter));
16588
+ if (Array.isArray(value))
16589
+ return value.map((v) => redactDeep(v, allow, counter, skip, budget, deadline));
16169
16590
  if (value && typeof value === "object") {
16170
16591
  const out = {};
16171
16592
  for (const [k, v] of Object.entries(value)) {
16172
- out[k] = redactDeep(v, allow, counter);
16593
+ out[k] = redactDeep(v, allow, counter, skip, budget, deadline);
16173
16594
  }
16174
16595
  return out;
16175
16596
  }
@@ -16204,16 +16625,34 @@ var state40 = {
16204
16625
  requestRedactions: 0,
16205
16626
  responseRedactions: 0,
16206
16627
  blocked: 0,
16628
+ timeoutCount: 0,
16629
+ skippedPatterns: [],
16630
+ responseTruncated: false,
16207
16631
  byKind: /* @__PURE__ */ new Map(),
16208
16632
  lastDetection: null,
16209
16633
  extensionUnregister: null
16210
16634
  };
16635
+ function surfaceScanTrips(api, tripped) {
16636
+ for (const kind of tripped) {
16637
+ if (!state40.skippedPatterns.includes(kind)) state40.skippedPatterns.push(kind);
16638
+ }
16639
+ state40.timeoutCount += 1;
16640
+ api.metrics.counter("redos_skips", 1);
16641
+ api.log.warn("prompt-firewall: scan-pass budget exceeded \u2014 patterns skipped mid-pass (issue #370)", {
16642
+ skipped: [...tripped]
16643
+ });
16644
+ }
16211
16645
  var plugin43 = {
16212
16646
  name: "prompt-firewall",
16213
16647
  version: "0.1.0",
16214
16648
  description: "Scans the provider wire for credential leaks before context reaches the LLM API (wrapProviderRunner); redact/warn/block. Opt-in; redact by default.",
16215
16649
  apiVersion: "^0.1.10",
16216
16650
  capabilities: { tools: true },
16651
+ // Wrap-stack contract (issue #362): ExtensionRegistry composes wrappers
16652
+ // first-registered = outermost. The manifest lists this plugin before
16653
+ // llm-cache, and llm-cache declares this plugin in optionalDeps, so the
16654
+ // firewall is the outer wrap: every request is scanned/redacted before
16655
+ // llm-cache can fingerprint or cache it.
16217
16656
  defaultConfig: { enabled: false, mode: "redact", scanResponse: true, allow: [] },
16218
16657
  configSchema: {
16219
16658
  type: "object",
@@ -16248,6 +16687,9 @@ var plugin43 = {
16248
16687
  state40.requestRedactions = 0;
16249
16688
  state40.responseRedactions = 0;
16250
16689
  state40.blocked = 0;
16690
+ state40.timeoutCount = 0;
16691
+ state40.skippedPatterns = [];
16692
+ state40.responseTruncated = false;
16251
16693
  state40.byKind.clear();
16252
16694
  state40.lastDetection = null;
16253
16695
  if (state40.extensionUnregister) {
@@ -16270,7 +16712,17 @@ var plugin43 = {
16270
16712
  async wrapProviderRunner(_ctx, request, inner) {
16271
16713
  const req = request ?? {};
16272
16714
  state40.invocations += 1;
16273
- const detections = detectSecrets(collectText(req), cfg.allow);
16715
+ const requestText = collectText(req);
16716
+ const { detections, skipped } = await detectSecretsGuarded(requestText, cfg.allow);
16717
+ state40.skippedPatterns = skipped.map((s) => s.kind);
16718
+ if (skipped.length > 0) {
16719
+ state40.timeoutCount += 1;
16720
+ api.log.warn("prompt-firewall: ReDoS budget exceeded, patterns skipped", {
16721
+ skipped: state40.skippedPatterns
16722
+ });
16723
+ api.metrics.counter("redos_skips", 1);
16724
+ }
16725
+ const skipSet = new Set(state40.skippedPatterns);
16274
16726
  if (detections.length > 0) {
16275
16727
  state40.requestsWithSecrets += 1;
16276
16728
  for (const d of detections) {
@@ -16289,27 +16741,39 @@ var plugin43 = {
16289
16741
  }
16290
16742
  if (cfg.mode === "redact") {
16291
16743
  const counter = { n: 0 };
16292
- const redactedReq = redactDeep(req, cfg.allow, counter);
16744
+ const deadline = createScanDeadline();
16745
+ const redactedReq = redactDeep(req, cfg.allow, counter, skipSet, void 0, deadline);
16293
16746
  state40.requestRedactions += counter.n;
16294
16747
  api.metrics.counter("request_redactions", counter.n);
16748
+ if (deadline.tripped.size > 0) surfaceScanTrips(api, deadline.tripped);
16295
16749
  const response2 = await inner(_ctx, redactedReq);
16296
- return cfg.scanResponse ? redactResponse(response2, cfg.allow) : response2;
16750
+ return cfg.scanResponse ? redactResponse(response2, cfg.allow, skipSet) : response2;
16297
16751
  }
16298
16752
  }
16299
16753
  const response = await inner(_ctx, request);
16300
16754
  if (cfg.mode === "redact" && cfg.scanResponse) {
16301
- return redactResponse(response, cfg.allow);
16755
+ return redactResponse(response, cfg.allow, skipSet);
16302
16756
  }
16303
16757
  return response;
16304
16758
  }
16305
16759
  });
16306
16760
  }
16307
- function redactResponse(response, allow) {
16761
+ function redactResponse(response, allow, skip) {
16308
16762
  if (!response || typeof response !== "object") return response;
16309
16763
  const counter = { n: 0 };
16310
16764
  const content = response.content;
16311
16765
  if (content === void 0) return response;
16312
- const redacted = redactDeep(content, allow, counter);
16766
+ const budget = { remaining: RESPONSE_SCAN_BUDGET, truncated: false };
16767
+ const deadline = createScanDeadline();
16768
+ const redacted = redactDeep(content, allow, counter, skip, budget, deadline);
16769
+ if (deadline.tripped.size > 0) surfaceScanTrips(api, deadline.tripped);
16770
+ if (budget.truncated) {
16771
+ state40.responseTruncated = true;
16772
+ api.log.warn(
16773
+ "prompt-firewall: response scan budget exhausted \u2014 part of the response was returned unredacted"
16774
+ );
16775
+ api.metrics.counter("response_scan_truncated", 1);
16776
+ }
16313
16777
  if (counter.n > 0) {
16314
16778
  state40.responseRedactions += counter.n;
16315
16779
  api.metrics.counter("response_redactions", counter.n);
@@ -16335,12 +16799,15 @@ var plugin43 = {
16335
16799
  mode: cfg.mode,
16336
16800
  scanResponse: cfg.scanResponse,
16337
16801
  patterns: PATTERNS2.map((p) => p.kind),
16802
+ skippedPatterns: state40.skippedPatterns,
16803
+ responseTruncated: state40.responseTruncated,
16338
16804
  counters: {
16339
16805
  invocations: state40.invocations,
16340
16806
  requestsWithSecrets: state40.requestsWithSecrets,
16341
16807
  requestRedactions: state40.requestRedactions,
16342
16808
  responseRedactions: state40.responseRedactions,
16343
- blocked: state40.blocked
16809
+ blocked: state40.blocked,
16810
+ timeoutCount: state40.timeoutCount
16344
16811
  },
16345
16812
  byKind: Object.fromEntries(state40.byKind),
16346
16813
  lastDetection: state40.lastDetection
@@ -16367,13 +16834,17 @@ var plugin43 = {
16367
16834
  requestsWithSecrets: state40.requestsWithSecrets,
16368
16835
  requestRedactions: state40.requestRedactions,
16369
16836
  responseRedactions: state40.responseRedactions,
16370
- blocked: state40.blocked
16837
+ blocked: state40.blocked,
16838
+ timeoutCount: state40.timeoutCount
16371
16839
  };
16372
16840
  state40.invocations = 0;
16373
16841
  state40.requestsWithSecrets = 0;
16374
16842
  state40.requestRedactions = 0;
16375
16843
  state40.responseRedactions = 0;
16376
16844
  state40.blocked = 0;
16845
+ state40.timeoutCount = 0;
16846
+ state40.skippedPatterns = [];
16847
+ state40.responseTruncated = false;
16377
16848
  state40.byKind.clear();
16378
16849
  state40.lastDetection = null;
16379
16850
  api.log.info("prompt-firewall: teardown complete", { final });
@@ -16396,7 +16867,7 @@ var prompt_firewall_default = plugin43;
16396
16867
 
16397
16868
  // src/refactor-suggester/index.ts
16398
16869
  import { readFile as readFile12 } from "node:fs/promises";
16399
- import { isAbsolute as isAbsolute21, relative as relative21, resolve as resolve21 } from "node:path";
16870
+ import { isAbsolute as isAbsolute20, relative as relative20, resolve as resolve21 } from "node:path";
16400
16871
  var API_VERSION28 = "^0.1.10";
16401
16872
  var HOOK_WARNING_COOLDOWN_MS2 = 6e4;
16402
16873
  var state41 = {
@@ -16440,7 +16911,7 @@ function toPosix6(p) {
16440
16911
  return p.replace(/\\/g, "/");
16441
16912
  }
16442
16913
  function relativePath6(p) {
16443
- return toPosix6(relative21(process.cwd(), p));
16914
+ return toPosix6(relative20(process.cwd(), p));
16444
16915
  }
16445
16916
  function leadingIndentLevel(line) {
16446
16917
  const leading = line.match(/^(\s*)/)?.[1] ?? "";
@@ -16527,7 +16998,7 @@ function detectSmells(filePath, content, rules) {
16527
16998
  }
16528
16999
  async function scanPath4(rawPath, cfg) {
16529
17000
  const root = process.cwd();
16530
- const resolved = isAbsolute21(rawPath) ? resolve21(rawPath) : resolve21(root, rawPath);
17001
+ const resolved = isAbsolute20(rawPath) ? resolve21(rawPath) : resolve21(root, rawPath);
16531
17002
  const exts = normalizeExtensions5(cfg.extensions);
16532
17003
  const files = await collectSourceFilesAsync(resolved, { extensions: exts });
16533
17004
  const suggestions = [];
@@ -17423,6 +17894,7 @@ function createState2() {
17423
17894
  allowCount: 0,
17424
17895
  /** PostToolUse: secrets detected in tool output. */
17425
17896
  leakCount: 0,
17897
+ timeoutCount: 0,
17426
17898
  /** Most recent PreToolUse block — surfaced by `secret_scanner_status`. */
17427
17899
  lastBlock: null,
17428
17900
  /** Most recent PostToolUse leak — surfaced by `secret_scanner_status`. */
@@ -17591,7 +18063,19 @@ function buildHook(cfg, log, runtime) {
17591
18063
  const { state: state60 } = runtime;
17592
18064
  if (!cfg.enabled) return;
17593
18065
  const toolName = input.toolName ?? "unknown";
17594
- const matched = scanInput(input.toolInput);
18066
+ let matched;
18067
+ try {
18068
+ matched = scanInput(input.toolInput);
18069
+ } catch (err) {
18070
+ if (String(err).includes("ReDoS")) {
18071
+ state60.timeoutCount += 1;
18072
+ return {
18073
+ decision: "block",
18074
+ reason: "secret-scanner: ReDoS timeout \u2014 regex scan exceeded the wall-clock budget. Fail-closed: treated as a block."
18075
+ };
18076
+ }
18077
+ throw err;
18078
+ }
17595
18079
  if (!matched) return;
17596
18080
  const summary = matched.join(", ");
17597
18081
  const when = (/* @__PURE__ */ new Date()).toISOString();
@@ -17769,7 +18253,8 @@ var plugin47 = {
17769
18253
  block: state60.blockCount,
17770
18254
  redact: state60.redactCount,
17771
18255
  allow: state60.allowCount,
17772
- leak: state60.leakCount
18256
+ leak: state60.leakCount,
18257
+ timeoutCount: state60.timeoutCount
17773
18258
  },
17774
18259
  lastBlock: state60.lastBlock,
17775
18260
  lastLeak: state60.lastLeak
@@ -17859,7 +18344,7 @@ var secret_scanner_default = plugin47;
17859
18344
 
17860
18345
  // src/security-hotspot-scanner/index.ts
17861
18346
  import { readdir as readdir2, readFile as readFile13, stat as stat5 } from "node:fs/promises";
17862
- import { isAbsolute as isAbsolute22, relative as relative22, resolve as resolve22 } from "node:path";
18347
+ import { isAbsolute as isAbsolute21, relative as relative21, resolve as resolve22 } from "node:path";
17863
18348
  var API_VERSION31 = "^0.1.10";
17864
18349
  var state44 = {
17865
18350
  scanCount: 0,
@@ -17950,7 +18435,7 @@ function isSourceFile2(filePath, extensions) {
17950
18435
  async function scanPath5(inputPath, cfg) {
17951
18436
  const start = Date.now();
17952
18437
  const root = process.cwd();
17953
- const resolved = isAbsolute22(inputPath) ? resolve22(inputPath) : resolve22(root, inputPath);
18438
+ const resolved = isAbsolute21(inputPath) ? resolve22(inputPath) : resolve22(root, inputPath);
17954
18439
  if (!withinProject(inputPath)) {
17955
18440
  return {
17956
18441
  path: inputPath,
@@ -17971,7 +18456,7 @@ async function scanPath5(inputPath, cfg) {
17971
18456
  filesScanned += 1;
17972
18457
  const findings = scanSource(content, maxPerFile);
17973
18458
  for (const f of findings) {
17974
- allFindings.push({ ...f, snippet: `${relative22(root, filePath)}:${f.line}: ${f.snippet}` });
18459
+ allFindings.push({ ...f, snippet: `${relative21(root, filePath)}:${f.line}: ${f.snippet}` });
17975
18460
  if (allFindings.length >= cfg.maxFindings) return;
17976
18461
  }
17977
18462
  } catch {
@@ -18255,7 +18740,7 @@ var security_hotspot_scanner_default = plugin48;
18255
18740
 
18256
18741
  // src/semantic-search-indexer/index.ts
18257
18742
  import * as fs2 from "node:fs/promises";
18258
- import { isAbsolute as isAbsolute23, relative as relative23, resolve as resolve23 } from "node:path";
18743
+ import { isAbsolute as isAbsolute22, relative as relative22, resolve as resolve23 } from "node:path";
18259
18744
  import { DEFAULT_WALK_IGNORE_DIRS } from "@wrongstack/core/utils";
18260
18745
  var API_VERSION32 = "^0.1.10";
18261
18746
  var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -18333,21 +18818,21 @@ function readConfig45(raw) {
18333
18818
  function normalizeSlashes2(p) {
18334
18819
  return p.replace(/\\/g, "/");
18335
18820
  }
18336
- function withinProject5(p) {
18821
+ function withinProject4(p) {
18337
18822
  if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
18338
18823
  const root = normalizeSlashes2(process.cwd());
18339
- const resolved = normalizeSlashes2(isAbsolute23(p) ? resolve23(p) : resolve23(root, p));
18340
- const rel = normalizeSlashes2(relative23(root, resolved));
18824
+ const resolved = normalizeSlashes2(isAbsolute22(p) ? resolve23(p) : resolve23(root, p));
18825
+ const rel = normalizeSlashes2(relative22(root, resolved));
18341
18826
  if (rel === "" || rel === ".") return true;
18342
18827
  if (rel.startsWith("..")) return false;
18343
- if (isAbsolute23(rel)) return false;
18828
+ if (isAbsolute22(rel)) return false;
18344
18829
  return true;
18345
18830
  }
18346
18831
  function resolveProjectPath8(p) {
18347
18832
  const raw = typeof p === "string" && p.length > 0 ? p : ".";
18348
- if (!withinProject5(raw)) return null;
18833
+ if (!withinProject4(raw)) return null;
18349
18834
  const root = normalizeSlashes2(process.cwd());
18350
- return normalizeSlashes2(isAbsolute23(raw) ? resolve23(raw) : resolve23(root, raw));
18835
+ return normalizeSlashes2(isAbsolute22(raw) ? resolve23(raw) : resolve23(root, raw));
18351
18836
  }
18352
18837
  function tokenize(text, minLength) {
18353
18838
  const tokens = [];
@@ -18432,7 +18917,7 @@ async function walkDirectory(absPath, cfg, excludes, fileBatch) {
18432
18917
  return;
18433
18918
  }
18434
18919
  const absChild = normalizeSlashes2(resolve23(absPath, ent.name));
18435
- const relChild = normalizeSlashes2(relative23(root, absChild));
18920
+ const relChild = normalizeSlashes2(relative22(root, absChild));
18436
18921
  if (relChild === "" || relChild === ".") continue;
18437
18922
  if (excludes.some((re) => re.test(relChild))) continue;
18438
18923
  if (ent.isDirectory()) {
@@ -18474,7 +18959,7 @@ async function buildIndex(rootPath, cfg) {
18474
18959
  return;
18475
18960
  }
18476
18961
  if (rootStats.isFile()) {
18477
- const relPath = normalizeSlashes2(relative23(normalizeSlashes2(process.cwd()), rootPath));
18962
+ const relPath = normalizeSlashes2(relative22(normalizeSlashes2(process.cwd()), rootPath));
18478
18963
  await indexFileFromStats(rootPath, relPath === "" ? "." : relPath, rootStats, cfg);
18479
18964
  state45.fileCount = state45.index.files.size;
18480
18965
  } else if (rootStats.isDirectory()) {
@@ -18771,14 +19256,14 @@ import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
18771
19256
  import { toErrorMessage } from "@wrongstack/core/utils";
18772
19257
  import { execFile as execFile12 } from "node:child_process";
18773
19258
  import { access as access4, readFile as readFile15, readdir as readdir4, writeFile as writeFile5 } from "node:fs/promises";
18774
- import { isAbsolute as isAbsolute24, join as join6, relative as relative24, resolve as resolve24 } from "node:path";
19259
+ import { isAbsolute as isAbsolute23, join as join6, relative as relative23, resolve as resolve24 } from "node:path";
18775
19260
  var API_VERSION33 = "^0.1.10";
18776
19261
  function resolveProjectRoot(rawCwd, root = process.cwd()) {
18777
19262
  if (typeof rawCwd !== "string" || rawCwd.length === 0) return root;
18778
19263
  const base = resolve24(root);
18779
- const resolved = isAbsolute24(rawCwd) ? resolve24(rawCwd) : resolve24(base, rawCwd);
18780
- const rel = relative24(base, resolved);
18781
- if (rel === "" || !rel.startsWith("..") && !isAbsolute24(rel)) return resolved;
19264
+ const resolved = isAbsolute23(rawCwd) ? resolve24(rawCwd) : resolve24(base, rawCwd);
19265
+ const rel = relative23(base, resolved);
19266
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute23(rel)) return resolved;
18782
19267
  return null;
18783
19268
  }
18784
19269
  var state46 = {
@@ -19801,16 +20286,16 @@ var session_recap_default = plugin51;
19801
20286
  // src/shell-check/index.ts
19802
20287
  import { execFile as execFile13 } from "node:child_process";
19803
20288
  import { readdir as readdir5 } from "node:fs/promises";
19804
- import { isAbsolute as isAbsolute25, join as join7, relative as relative25, resolve as resolve25 } from "node:path";
20289
+ import { isAbsolute as isAbsolute24, join as join7, relative as relative24, resolve as resolve25 } from "node:path";
19805
20290
  var API_VERSION34 = "^0.1.10";
19806
- function withinProject6(p) {
20291
+ function withinProject5(p) {
19807
20292
  if (p.startsWith("-")) return false;
19808
20293
  const root = process.cwd();
19809
- const resolved = isAbsolute25(p) ? resolve25(p) : resolve25(root, p);
19810
- const rel = relative25(root, resolved);
20294
+ const resolved = isAbsolute24(p) ? resolve25(p) : resolve25(root, p);
20295
+ const rel = relative24(root, resolved);
19811
20296
  if (rel === "" || rel === ".") return true;
19812
20297
  if (rel.startsWith("..")) return false;
19813
- if (isAbsolute25(rel)) return false;
20298
+ if (isAbsolute24(rel)) return false;
19814
20299
  return true;
19815
20300
  }
19816
20301
  var MAX_PATH_LEN = 4096;
@@ -19987,7 +20472,7 @@ var plugin52 = {
19987
20472
  const pattern = inp.pattern ?? "";
19988
20473
  const severity = inp.severity ?? "warning";
19989
20474
  state48.invocationCount += 1;
19990
- const pathIsSafe = (p) => typeof p === "string" && p.length > 0 && p.length <= MAX_PATH_LEN && withinProject6(p);
20475
+ const pathIsSafe = (p) => typeof p === "string" && p.length > 0 && p.length <= MAX_PATH_LEN && withinProject5(p);
19991
20476
  if (!pathIsSafe(directory)) {
19992
20477
  return {
19993
20478
  ok: false,
@@ -20103,7 +20588,7 @@ var shell_check_default = plugin52;
20103
20588
 
20104
20589
  // src/smart-rename/index.ts
20105
20590
  import { readFileSync as readFileSync13, writeFileSync as writeFileSync2 } from "node:fs";
20106
- import { extname as extname6, isAbsolute as isAbsolute26, relative as relative26, resolve as resolve26 } from "node:path";
20591
+ import { extname as extname6, isAbsolute as isAbsolute25, relative as relative25, resolve as resolve26 } from "node:path";
20107
20592
  var API_VERSION35 = "^0.1.10";
20108
20593
  var state49 = {
20109
20594
  renameCount: 0,
@@ -20122,21 +20607,21 @@ function readConfig47(raw) {
20122
20607
  extensions: Array.isArray(r["extensions"]) ? r["extensions"].filter((x) => typeof x === "string") : DEFAULTS44.extensions
20123
20608
  };
20124
20609
  }
20125
- function withinProject7(p) {
20610
+ function withinProject6(p) {
20126
20611
  if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
20127
20612
  const root = process.cwd();
20128
- const resolved = isAbsolute26(p) ? resolve26(p) : resolve26(root, p);
20129
- const rel = relative26(root, resolved);
20613
+ const resolved = isAbsolute25(p) ? resolve26(p) : resolve26(root, p);
20614
+ const rel = relative25(root, resolved);
20130
20615
  if (rel === "" || rel === ".") return true;
20131
20616
  if (rel.startsWith("..")) return false;
20132
- if (isAbsolute26(rel)) return false;
20617
+ if (isAbsolute25(rel)) return false;
20133
20618
  return true;
20134
20619
  }
20135
20620
  function toPosix7(p) {
20136
20621
  return p.replace(/\\/g, "/");
20137
20622
  }
20138
20623
  function relativePath7(p) {
20139
- return toPosix7(relative26(process.cwd(), p));
20624
+ return toPosix7(relative25(process.cwd(), p));
20140
20625
  }
20141
20626
  function escapeRegex2(s) {
20142
20627
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -20221,7 +20706,7 @@ var plugin53 = {
20221
20706
  if (!isIdentifier(newName)) {
20222
20707
  return { ok: false, error: `newName "${newName}" is not a valid identifier` };
20223
20708
  }
20224
- if (!withinProject7(rawPath)) {
20709
+ if (!withinProject6(rawPath)) {
20225
20710
  return { ok: false, error: "path is outside the project root" };
20226
20711
  }
20227
20712
  const ext = extname6(rawPath).toLowerCase();
@@ -20326,10 +20811,10 @@ var OFFICIAL_PLUGIN_NAMES = [
20326
20811
  "notify-hub",
20327
20812
  "changelog-writer",
20328
20813
  "injection-shield",
20814
+ "prompt-firewall",
20329
20815
  "llm-cache",
20330
20816
  "model-router",
20331
20817
  "pr-drafter",
20332
- "prompt-firewall",
20333
20818
  "auto-escalate",
20334
20819
  "test-coverage-gate",
20335
20820
  "type-gate",
@@ -20499,7 +20984,7 @@ function isWrappedAsLinkOrCode(line, name) {
20499
20984
  if (lower.includes(`[\``) && lower.includes(`\`](`)) return true;
20500
20985
  return false;
20501
20986
  }
20502
- function escapeRegExp(s) {
20987
+ function escapeRegExp2(s) {
20503
20988
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20504
20989
  }
20505
20990
  function mapMarkdownFences(lines) {
@@ -20524,7 +21009,7 @@ function findUnlinkedReferences(lines, names) {
20524
21009
  const line = lines[i];
20525
21010
  if (line.length === 0) continue;
20526
21011
  for (const name of names) {
20527
- const re = new RegExp(`(^|[^\\w-])${escapeRegExp(name)}(?![\\w-])`, "i");
21012
+ const re = new RegExp(`(^|[^\\w-])${escapeRegExp2(name)}(?![\\w-])`, "i");
20528
21013
  if (re.test(line) && !isWrappedAsLinkOrCode(line, name)) {
20529
21014
  if (!found.has(name)) found.set(name, true);
20530
21015
  }
@@ -20553,7 +21038,7 @@ function wrapLineReferences(line) {
20553
21038
  let cursor = 0;
20554
21039
  const spans = [];
20555
21040
  for (const name of PLUGIN_NAMES) {
20556
- const re = new RegExp(`(^|[^\\w-])(${escapeRegExp(name)})(?![\\w-])`, "gi");
21041
+ const re = new RegExp(`(^|[^\\w-])(${escapeRegExp2(name)})(?![\\w-])`, "gi");
20557
21042
  let m;
20558
21043
  re.lastIndex = 0;
20559
21044
  while ((m = re.exec(line)) !== null) {
@@ -20774,7 +21259,7 @@ var spec_linker_default = plugin54;
20774
21259
 
20775
21260
  // src/template-engine/index.ts
20776
21261
  import { readFile as readFile17, writeFile as writeFile6 } from "node:fs/promises";
20777
- import { isAbsolute as isAbsolute27 } from "node:path";
21262
+ import { isAbsolute as isAbsolute26 } from "node:path";
20778
21263
  var API_VERSION36 = "^0.1.10";
20779
21264
  var templates = /* @__PURE__ */ new Map();
20780
21265
  var MAX_TEMPLATES = 256;
@@ -20830,7 +21315,7 @@ function renderTemplateRaw(template, variables) {
20830
21315
  return result;
20831
21316
  }
20832
21317
  function validateRelativeTemplatePath(field, value) {
20833
- if (isAbsolute27(value) || value.split(/[\\/]+/).includes("..")) {
21318
+ if (isAbsolute26(value) || value.split(/[\\/]+/).includes("..")) {
20834
21319
  return `${field} must be a relative path without ".." components`;
20835
21320
  }
20836
21321
  if (!withinProject(value)) {
@@ -21430,7 +21915,7 @@ var test_coverage_gate_default = plugin56;
21430
21915
  import { execFile as execFile14 } from "node:child_process";
21431
21916
  import { readFileSync as readFileSync15 } from "node:fs";
21432
21917
  import { createRequire as createRequire3 } from "node:module";
21433
- import { dirname as dirname8, isAbsolute as isAbsolute28, relative as relative27, resolve as resolve27 } from "node:path";
21918
+ import { dirname as dirname8, isAbsolute as isAbsolute27, relative as relative26, resolve as resolve27 } from "node:path";
21434
21919
  var API_VERSION38 = "^0.1.10";
21435
21920
  var state52 = {
21436
21921
  invocationCount: 0,
@@ -21499,17 +21984,17 @@ var ALLOWED_RUNNER_FLAGS = /* @__PURE__ */ new Set([
21499
21984
  "--reporter=verbose",
21500
21985
  "--reporter=default"
21501
21986
  ]);
21502
- function withinProject8(p) {
21987
+ function withinProject7(p) {
21503
21988
  if (p.length === 0 || p.length > 4096 || p.startsWith("-")) return false;
21504
21989
  const root = resolve27(process.cwd());
21505
- const resolved = isAbsolute28(p) ? resolve27(p) : resolve27(root, p);
21506
- const rel = relative27(root, resolved);
21507
- return rel === "" || !rel.startsWith("..") && !isAbsolute28(rel);
21990
+ const resolved = isAbsolute27(p) ? resolve27(p) : resolve27(root, p);
21991
+ const rel = relative26(root, resolved);
21992
+ return rel === "" || !rel.startsWith("..") && !isAbsolute27(rel);
21508
21993
  }
21509
21994
  function isInside3(parent, child) {
21510
21995
  if (parent === child) return true;
21511
- const rel = relative27(parent, child);
21512
- return rel !== "" && !rel.startsWith("..") && !isAbsolute28(rel);
21996
+ const rel = relative26(parent, child);
21997
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute27(rel);
21513
21998
  }
21514
21999
  function tokenizeCommand(command) {
21515
22000
  const trimmed = command.trim();
@@ -21553,7 +22038,7 @@ function resolveTestCommand(baseCommand, testPattern) {
21553
22038
  if (!relativeBin) return null;
21554
22039
  const packageDir = dirname8(packagePath);
21555
22040
  const candidate = resolve27(packageDir, relativeBin);
21556
- if (isAbsolute28(relativeBin) || !isInside3(packageDir, candidate)) {
22041
+ if (isAbsolute27(relativeBin) || !isInside3(packageDir, candidate)) {
21557
22042
  return null;
21558
22043
  }
21559
22044
  resolvedEntry = candidate;
@@ -21562,7 +22047,7 @@ function resolveTestCommand(baseCommand, testPattern) {
21562
22047
  }
21563
22048
  const args = [resolvedEntry, ...runnerArgs];
21564
22049
  if (testPattern) {
21565
- if (!withinProject8(testPattern)) return null;
22050
+ if (!withinProject7(testPattern)) return null;
21566
22051
  args.push(testPattern);
21567
22052
  }
21568
22053
  return {
@@ -21796,7 +22281,7 @@ var test_flake_detector_default = plugin57;
21796
22281
 
21797
22282
  // src/test-generator/index.ts
21798
22283
  import { readFileSync as readFileSync16 } from "node:fs";
21799
- import { isAbsolute as isAbsolute29, relative as relative28, resolve as resolve28 } from "node:path";
22284
+ import { isAbsolute as isAbsolute28, relative as relative27, resolve as resolve28 } from "node:path";
21800
22285
  var API_VERSION39 = "^0.1.10";
21801
22286
  var state53 = {
21802
22287
  generateCount: 0,
@@ -21852,21 +22337,21 @@ var SOURCE_EXTENSIONS = [
21852
22337
  ".cpp",
21853
22338
  ".hpp"
21854
22339
  ];
21855
- function withinProject9(p) {
22340
+ function withinProject8(p) {
21856
22341
  if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
21857
22342
  const root = process.cwd();
21858
- const resolved = isAbsolute29(p) ? resolve28(p) : resolve28(root, p);
21859
- const rel = relative28(root, resolved);
22343
+ const resolved = isAbsolute28(p) ? resolve28(p) : resolve28(root, p);
22344
+ const rel = relative27(root, resolved);
21860
22345
  if (rel === "" || rel === ".") return true;
21861
22346
  if (rel.startsWith("..")) return false;
21862
- if (isAbsolute29(rel)) return false;
22347
+ if (isAbsolute28(rel)) return false;
21863
22348
  return true;
21864
22349
  }
21865
22350
  function toPosix8(p) {
21866
22351
  return p.replace(/\\/g, "/");
21867
22352
  }
21868
22353
  function relativePath8(p) {
21869
- return toPosix8(relative28(process.cwd(), p));
22354
+ return toPosix8(relative27(process.cwd(), p));
21870
22355
  }
21871
22356
  function detectExports(content) {
21872
22357
  const exports = [];
@@ -22076,7 +22561,7 @@ var plugin58 = {
22076
22561
  if (!rawPath || typeof rawPath !== "string") {
22077
22562
  return { ok: false, error: "path is required" };
22078
22563
  }
22079
- if (!withinProject9(rawPath)) {
22564
+ if (!withinProject8(rawPath)) {
22080
22565
  return { ok: false, error: "path is outside the project root" };
22081
22566
  }
22082
22567
  if (!SOURCE_EXTENSIONS.some((ext) => rawPath.toLowerCase().endsWith(ext))) {
@@ -22172,7 +22657,7 @@ var test_generator_default = plugin58;
22172
22657
  // src/test-runner-gate/index.ts
22173
22658
  import { execFile as execFile15 } from "node:child_process";
22174
22659
  import { access as access5 } from "node:fs/promises";
22175
- import { basename as basename7, dirname as dirname9, isAbsolute as isAbsolute30, join as join8 } from "node:path";
22660
+ import { basename as basename7, dirname as dirname9, isAbsolute as isAbsolute29, join as join8 } from "node:path";
22176
22661
  import { buildWin32CmdShimInvocation as buildWin32CmdShimInvocation2, resolveWin32Command as resolveWin32Command2 } from "@wrongstack/tools/win32";
22177
22662
  function resolveExec(command, args) {
22178
22663
  const resolved = resolveWin32Command2(command);
@@ -22341,7 +22826,7 @@ function resolveAllowedCommand2(customCommand) {
22341
22826
  if (ALLOWED_COMMAND_TOKENS.has(head)) {
22342
22827
  return { cmd: head, args: tokens.slice(1) };
22343
22828
  }
22344
- if (isAbsolute30(head)) {
22829
+ if (isAbsolute29(head)) {
22345
22830
  if (!withinProject(head)) return null;
22346
22831
  const base = basename7(head);
22347
22832
  if (ALLOWED_COMMAND_TOKENS.has(base)) {