@wrongstack/core 0.309.0 → 0.309.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.
@@ -5575,11 +5575,13 @@ var CHAOS_MONKEY_AGENT = {
5575
5575
  tools: [...TOOLS.build],
5576
5576
  skillNames: ["testing", "typescript-strict"],
5577
5577
  spawnBudgetExempt: true,
5578
- // Follow fleet worktree policy (NOT 'required'): mutation targets are
5579
- // often freshly written and uncommitted — a worktree spawned from HEAD
5580
- // would not contain them and every mutant would drift. Callers pass
5581
- // `worktree: 'off'` in the mutation_test input for uncommitted targets.
5582
- worktree: "auto",
5578
+ // Run in the live checkout: mutation targets are usually freshly
5579
+ // written and uncommitted — a worktree spawned from HEAD would not
5580
+ // contain them and every mutant would drift. The mutation_test tool
5581
+ // honors this value as its default; callers can still override per
5582
+ // call via its `chaosWorktree` input when targets are committed and
5583
+ // isolation is wanted.
5584
+ worktree: "off",
5583
5585
  // Report travels via submit_result + final text, not the leader's stream.
5584
5586
  textStream: "silent",
5585
5587
  toolStream: "silent"
@@ -9640,24 +9642,29 @@ var TOKEN_PATTERNS = [
9640
9642
  kind: "return-null",
9641
9643
  // `return <expr>;` where expr is not already null/undefined/void.
9642
9644
  regex: /(?<indent>\breturn\b)(?<expr>\s+[^;{}\n]+?)\s*;/g,
9643
- replace: () => "return null;"
9645
+ replace: () => "return null;",
9646
+ endpointsInCode: true
9644
9647
  }
9645
9648
  ];
9646
9649
  function planMutations(file, source, opts = {}) {
9647
9650
  const maxPerFile = opts.maxPerFile ?? 25;
9648
9651
  const out = [];
9649
9652
  const lines = source.split("\n");
9653
+ const masks = computeLineMasks(source);
9650
9654
  for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
9651
9655
  const line = lines[lineIdx];
9652
9656
  const t = line.trim();
9653
- if (t.startsWith("//") || t.startsWith("*") || t.startsWith("/*")) continue;
9657
+ if (t.startsWith("//")) continue;
9658
+ const codeRanges = masks[lineIdx];
9659
+ const inCode = (start) => codeRanges.some(([s, e]) => start >= s && start < e);
9654
9660
  for (const pattern of TOKEN_PATTERNS) {
9655
9661
  pattern.regex.lastIndex = 0;
9656
9662
  let m;
9657
9663
  while ((m = pattern.regex.exec(line)) !== null) {
9658
9664
  const token = m.groups?.["op"] ?? m[0];
9659
9665
  const tokenStart = m.index + m[0].indexOf(token);
9660
- if (isMasked(line, tokenStart, token.length)) continue;
9666
+ if (!inCode(tokenStart)) continue;
9667
+ if (pattern.endpointsInCode && !inCode(tokenStart + token.length - 1)) continue;
9661
9668
  const original = line.slice(tokenStart, tokenStart + token.length);
9662
9669
  const replacement = pattern.replace(token);
9663
9670
  if (replacement === original) continue;
@@ -9676,19 +9683,179 @@ function planMutations(file, source, opts = {}) {
9676
9683
  }
9677
9684
  return out.slice(0, maxPerFile);
9678
9685
  }
9679
- function isMasked(line, start, len) {
9680
- let inSingle = false;
9681
- let inDouble = false;
9682
- for (let i = 0; i < start; i++) {
9683
- const c = line[i];
9684
- const prev = i > 0 ? line[i - 1] : void 0;
9685
- if (c === "'" && prev !== "\\") inSingle = !inSingle;
9686
- else if (c === '"' && prev !== "\\") inDouble = !inDouble;
9687
- if (!inSingle && !inDouble && c === "/" && prev === "/") return true;
9686
+ function computeLineMasks(source) {
9687
+ const lines = source.split("\n");
9688
+ const masks = lines.map(() => []);
9689
+ const stack = [{ kind: "code", depth: 0, parens: [] }];
9690
+ let inBlockComment = false;
9691
+ let lastToken = null;
9692
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
9693
+ const line = lines[lineIdx];
9694
+ const ranges = masks[lineIdx];
9695
+ let runStart = null;
9696
+ const closeRun = (end) => {
9697
+ if (runStart !== null && end > runStart) ranges.push([runStart, end]);
9698
+ runStart = null;
9699
+ };
9700
+ let i = 0;
9701
+ if (inBlockComment) {
9702
+ const close = line.indexOf("*/");
9703
+ if (close === -1) continue;
9704
+ inBlockComment = false;
9705
+ i = close + 2;
9706
+ }
9707
+ while (i < line.length) {
9708
+ const top = stack[stack.length - 1];
9709
+ const c = line[i];
9710
+ if (top.kind === "template") {
9711
+ if (c === "\\") {
9712
+ i += 2;
9713
+ continue;
9714
+ }
9715
+ if (c === "`") {
9716
+ stack.pop();
9717
+ lastToken = "`";
9718
+ i++;
9719
+ continue;
9720
+ }
9721
+ if (c === "$" && line[i + 1] === "{") {
9722
+ stack.push({ kind: "code", depth: 0, parens: [] });
9723
+ lastToken = "${";
9724
+ i += 2;
9725
+ continue;
9726
+ }
9727
+ i++;
9728
+ continue;
9729
+ }
9730
+ if (/[\w$]/.test(c)) {
9731
+ let j = i + 1;
9732
+ while (j < line.length && /[\w$]/.test(line[j])) j++;
9733
+ lastToken = line.slice(i, j);
9734
+ if (runStart === null) runStart = i;
9735
+ i = j;
9736
+ continue;
9737
+ }
9738
+ if (c === "'" || c === '"') {
9739
+ closeRun(i);
9740
+ i++;
9741
+ while (i < line.length && line[i] !== c) {
9742
+ if (line[i] === "\\") i++;
9743
+ i++;
9744
+ }
9745
+ i++;
9746
+ lastToken = c;
9747
+ continue;
9748
+ }
9749
+ if (c === "`") {
9750
+ closeRun(i);
9751
+ stack.push({ kind: "template", depth: 0, parens: [] });
9752
+ i++;
9753
+ continue;
9754
+ }
9755
+ if (c === "/" && line[i + 1] === "/") {
9756
+ closeRun(i);
9757
+ break;
9758
+ }
9759
+ if (c === "/" && line[i + 1] === "*") {
9760
+ closeRun(i);
9761
+ const close = line.indexOf("*/", i + 2);
9762
+ if (close === -1) {
9763
+ inBlockComment = true;
9764
+ break;
9765
+ }
9766
+ i = close + 2;
9767
+ continue;
9768
+ }
9769
+ if (c === "/") {
9770
+ if (!tokenCanEndOperand(lastToken)) {
9771
+ closeRun(i);
9772
+ const next = skipRegexLiteral(line, i);
9773
+ lastToken = next > i + 1 ? "regex" : "/";
9774
+ i = next;
9775
+ continue;
9776
+ }
9777
+ }
9778
+ if (c === "(") {
9779
+ top.parens.push(CONTROL_KEYWORDS.has(lastToken ?? "") ? "control" : "expr");
9780
+ lastToken = c;
9781
+ } else if (c === ")") {
9782
+ const kind = top.parens.pop() ?? "expr";
9783
+ lastToken = kind === "control" ? "control-paren-close" : ")";
9784
+ } else if (c === "{") {
9785
+ top.depth++;
9786
+ lastToken = c;
9787
+ } else if (c === "}") {
9788
+ if (top.depth > 0) {
9789
+ top.depth--;
9790
+ lastToken = c;
9791
+ } else if (stack.length > 1) {
9792
+ closeRun(i);
9793
+ stack.pop();
9794
+ i++;
9795
+ continue;
9796
+ } else {
9797
+ lastToken = c;
9798
+ }
9799
+ } else if (c !== " " && c !== " " && c !== "\r") {
9800
+ lastToken = c;
9801
+ }
9802
+ if (runStart === null) runStart = i;
9803
+ i++;
9804
+ }
9805
+ closeRun(line.length);
9806
+ }
9807
+ return masks;
9808
+ }
9809
+ var KEYWORDS_BEFORE_REGEX = /* @__PURE__ */ new Set([
9810
+ "return",
9811
+ "typeof",
9812
+ "instanceof",
9813
+ "in",
9814
+ "of",
9815
+ "new",
9816
+ "delete",
9817
+ "void",
9818
+ "throw",
9819
+ "case",
9820
+ "do",
9821
+ "else",
9822
+ "yield",
9823
+ "await"
9824
+ ]);
9825
+ var CONTROL_KEYWORDS = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "catch", "with", "await"]);
9826
+ function tokenCanEndOperand(token) {
9827
+ if (token === null) return false;
9828
+ if (/^[\w$]+$/.test(token)) return !KEYWORDS_BEFORE_REGEX.has(token);
9829
+ return token === ")" || token === "]" || token === "." || token === '"' || token === "'" || token === "`";
9830
+ }
9831
+ function skipRegexLiteral(line, start) {
9832
+ let i = start + 1;
9833
+ let inClass = false;
9834
+ while (i < line.length) {
9835
+ const ch = line[i];
9836
+ if (ch === "\\") {
9837
+ i += 2;
9838
+ continue;
9839
+ }
9840
+ if (inClass) {
9841
+ if (ch === "]") inClass = false;
9842
+ i++;
9843
+ continue;
9844
+ }
9845
+ if (ch === "[") {
9846
+ inClass = true;
9847
+ i++;
9848
+ continue;
9849
+ }
9850
+ if (ch === "/") {
9851
+ i++;
9852
+ break;
9853
+ }
9854
+ if (ch === "\n" || ch === "\r") return line.length;
9855
+ i++;
9688
9856
  }
9689
- if (inSingle || inDouble) return true;
9690
- const window = line.slice(start, start + len);
9691
- return /['"]/.test(window);
9857
+ while (i < line.length && /[a-z]/.test(line[i])) i++;
9858
+ return i;
9692
9859
  }
9693
9860
  function parseMutationReport(text) {
9694
9861
  const candidates = [];
@@ -9739,7 +9906,7 @@ function normalizeMutantEntry(value) {
9739
9906
  const rec = value;
9740
9907
  const id = typeof rec["id"] === "string" ? rec["id"] : void 0;
9741
9908
  const status = rec["status"];
9742
- if (!id || status !== "killed" && status !== "survived" && status !== "skipped") {
9909
+ if (!id || status !== "killed" && status !== "survived" && status !== "skipped" && status !== "killed-by-hang") {
9743
9910
  return void 0;
9744
9911
  }
9745
9912
  return {
@@ -9795,7 +9962,7 @@ function makeMutationTestTool(director, roster, opts = {}) {
9795
9962
  },
9796
9963
  chaosWorktree: {
9797
9964
  anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
9798
- description: "Worktree override for the chaos agent. Use 'off' when targets are uncommitted \u2014 a worktree from HEAD would not contain them."
9965
+ description: "Worktree override for the chaos agent. Defaults to the roster policy for chaos-monkey ('off'), because mutation targets are usually freshly written and uncommitted \u2014 a worktree from HEAD would not contain them and every mutant would drift to skipped. Only pass 'auto' or 'required' when the targets are committed."
9799
9966
  },
9800
9967
  timeoutMs: { type: "number", minimum: 1, description: "Per-task timeout for chaos/strengthen/rerun tasks." },
9801
9968
  reportOnly: {
@@ -9817,8 +9984,16 @@ function makeMutationTestTool(director, roster, opts = {}) {
9817
9984
  error: "No mutable sites found in the given targets (after comment/string filtering)."
9818
9985
  };
9819
9986
  }
9987
+ const chaosBase = roster?.[CHAOS_ROLE];
9988
+ if (!chaosBase) {
9989
+ return {
9990
+ verdict: "inconclusive",
9991
+ passed: false,
9992
+ error: "chaos-monkey role missing from the roster \u2014 refusing to spawn a saboteur without its prompt/tools contract. Build the toolset with a roster that includes 'chaos-monkey' (FLEET_ROSTER does)."
9993
+ };
9994
+ }
9820
9995
  const chaosSubagentId = await director.spawn(
9821
- makeChaosConfig(roster, i.chaosWorktree ?? "off")
9996
+ makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
9822
9997
  );
9823
9998
  const chaosTaskId = await director.assign({
9824
9999
  id: randomUUID8(),
@@ -9836,6 +10011,7 @@ function makeMutationTestTool(director, roster, opts = {}) {
9836
10011
  );
9837
10012
  const attempts = [];
9838
10013
  let current = survivors;
10014
+ let rerunUnknowns = [];
9839
10015
  while (current.length > 0 && attempts.length < maxAttempts && i.repairSubagentId) {
9840
10016
  const attemptNo = attempts.length + 1;
9841
10017
  const strengthenTaskId = await director.assign({
@@ -9857,7 +10033,7 @@ function makeMutationTestTool(director, roster, opts = {}) {
9857
10033
  }
9858
10034
  const survivorPlan = plan.filter((p) => current.some((s) => s.id === p.id));
9859
10035
  const rerunSubagentId = await director.spawn(
9860
- makeChaosConfig(roster, i.chaosWorktree ?? "off")
10036
+ makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
9861
10037
  );
9862
10038
  const rerunTaskId = await director.assign({
9863
10039
  id: randomUUID8(),
@@ -9867,29 +10043,36 @@ function makeMutationTestTool(director, roster, opts = {}) {
9867
10043
  });
9868
10044
  const [rerunResult] = await director.awaitTasks([rerunTaskId]);
9869
10045
  const passN = collectOutcomes(rerunResult, survivorPlan);
9870
- const stillSurviving = passN.filter((m) => m.status === "survived" || m.status === "skipped");
10046
+ const stillSurviving = passN.filter((m) => !isKill(m.status));
10047
+ rerunUnknowns = passN.filter((m) => m.status === "skipped");
9871
10048
  attempts.push({
9872
10049
  attempt: attemptNo,
9873
10050
  survivorsBefore: current,
9874
10051
  strengthenResult: { taskId: strengthenResult.taskId, status: strengthenResult.status },
9875
10052
  rerunResult: { taskId: rerunTaskId, status: rerunResult?.status ?? "unknown" },
9876
10053
  survivorsAfter: stillSurviving,
9877
- suspectedEquivalent: stillSurviving.filter((m) => current.some((c) => c.id === m.id)).map((m) => m.id)
10054
+ suspectedEquivalent: stillSurviving.filter((m) => m.status === "survived" && current.some((c) => c.id === m.id)).map((m) => m.id)
9878
10055
  });
9879
- current = stillSurviving.filter((m) => m.status === "survived");
9880
- if (passN.every((m) => m.status === "skipped")) break;
10056
+ current = stillSurviving;
9881
10057
  }
9882
- const finalSurvivors = current;
10058
+ const finalSurvivors = current.filter((m) => m.status === "survived");
9883
10059
  const verifiedCount = pass1.filter((m) => m.status !== "skipped").length;
9884
10060
  const skippedCount = pass1.filter((m) => m.status === "skipped").length;
9885
- const score = plan.length === 0 ? 0 : pass1.filter((m) => m.status === "killed").length / plan.length;
9886
- const verdict = verifiedCount === 0 ? "inconclusive" : finalSurvivors.length === 0 ? skippedCount > 0 ? "partial" : "pass" : score >= 0.8 ? "partial" : "fail";
10061
+ const rerunUnknownCount = rerunUnknowns.length;
10062
+ const score = plan.length === 0 ? 0 : pass1.filter((m) => isKill(m.status)).length / plan.length;
10063
+ const verdict = verifiedCount === 0 ? "inconclusive" : finalSurvivors.length === 0 ? skippedCount > 0 || rerunUnknownCount > 0 ? "partial" : "pass" : score >= 0.8 ? "partial" : "fail";
9887
10064
  return {
9888
10065
  verdict,
9889
10066
  passed: verdict === "pass",
9890
10067
  mutationScore: Number.parseFloat(score.toFixed(3)),
9891
10068
  planned: plan.length,
9892
- killed: pass1.filter((m) => m.status === "killed").length,
10069
+ killed: pass1.filter((m) => isKill(m.status)).length,
10070
+ // Breakout of `killed`: how many kills were detected by the test
10071
+ // command hanging rather than by a failing assertion. A subset of
10072
+ // `killed`, surfaced so a director can distinguish a hang-heavy
10073
+ // suite (mutants breaking termination, not assertions) from an
10074
+ // assertion-strong one. hangHeavy = killedByHang === killed.
10075
+ killedByHang: pass1.filter((m) => m.status === "killed-by-hang").length,
9893
10076
  survived: pass1.filter((m) => m.status === "survived").length,
9894
10077
  skipped: pass1.filter((m) => m.status === "skipped").length,
9895
10078
  finalSurvivors: finalSurvivors.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
@@ -9897,7 +10080,11 @@ function makeMutationTestTool(director, roster, opts = {}) {
9897
10080
  strengthenAttempts: attempts.length,
9898
10081
  attempts,
9899
10082
  chaosTaskId,
9900
- nextAction: finalSurvivors.length === 0 ? "accept" : attempts.length >= maxAttempts && i.repairSubagentId ? "manual_review_survivors" : "strengthen_tests"
10083
+ // Unverified leftovers from the strengthen loop: surfaced so the
10084
+ // caller can see WHICH mutants lack kill evidence, and counted by
10085
+ // the verdict gate above.
10086
+ unverifiedFromRerun: rerunUnknowns.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
10087
+ nextAction: finalSurvivors.length === 0 && rerunUnknownCount === 0 && skippedCount === 0 ? "accept" : attempts.length >= maxAttempts && i.repairSubagentId ? "manual_review_survivors" : "strengthen_tests"
9901
10088
  };
9902
10089
  }
9903
10090
  };
@@ -9913,7 +10100,7 @@ function normalizeMutationTestInput(input) {
9913
10100
  maxPerFile: typeof raw["maxPerFile"] === "number" ? raw["maxPerFile"] : void 0,
9914
10101
  maxStrengthenAttempts: typeof raw["maxStrengthenAttempts"] === "number" ? raw["maxStrengthenAttempts"] : void 0,
9915
10102
  repairSubagentId: typeof raw["repairSubagentId"] === "string" && raw["repairSubagentId"].trim() ? raw["repairSubagentId"].trim() : void 0,
9916
- chaosWorktree: raw["chaosWorktree"] ?? void 0,
10103
+ chaosWorktree: normalizeWorktreeOverride(raw["chaosWorktree"]),
9917
10104
  timeoutMs: typeof raw["timeoutMs"] === "number" ? raw["timeoutMs"] : void 0,
9918
10105
  reportOnly: raw["reportOnly"] === true
9919
10106
  };
@@ -9935,8 +10122,7 @@ function buildPlan(i, projectRoot) {
9935
10122
  }
9936
10123
  return plan;
9937
10124
  }
9938
- function makeChaosConfig(roster, worktree) {
9939
- const base = roster?.[CHAOS_ROLE] ?? getAgentDefinition(CHAOS_ROLE)?.config ?? { name: "Chaos Monkey", role: CHAOS_ROLE };
10125
+ function makeChaosConfig(base, worktree) {
9940
10126
  return { ...instantiateRosterConfig2(CHAOS_ROLE, base), worktree };
9941
10127
  }
9942
10128
  function buildChaosTask(plan, i, pass, priorSurvivors) {
@@ -9952,7 +10138,7 @@ ${priorSurvivors.map((s) => `- ${s.id} (${s.kind} @ ${s.file}:${s.line})`).join(
9952
10138
  "For each mutant, in order:",
9953
10139
  "1. Apply ONLY that mutation at its exact (file, line, column).",
9954
10140
  `2. Run the test command: ${i.testCommand}${i.cwd ? ` (cwd: ${i.cwd})` : ""}`,
9955
- "3. Record killed (tests failed \u2014 quote first failing assertion) or survived (suite green).",
10141
+ "3. Record killed (tests failed \u2014 quote first failing assertion), survived (suite green), or killed-by-hang (the test command timed out or was aborted \u2014 the mutation broke the suite by non-termination; record the timeout as evidence, do NOT report it as survived).",
9956
10142
  "4. Restore the file byte-for-byte before the next mutant.",
9957
10143
  "",
9958
10144
  "Mutants:",
@@ -9964,23 +10150,49 @@ ${priorSurvivors.map((s) => `- ${s.id} (${s.kind} @ ${s.file}:${s.line})`).join(
9964
10150
  ].join("\n");
9965
10151
  }
9966
10152
  function buildStrengthenTask(survivors, i, attempt) {
10153
+ const confirmed = survivors.filter((s) => s.status === "survived");
10154
+ const unverified = survivors.filter((s) => s.status === "skipped");
10155
+ const row = (s) => `- ${s.id} | ${s.file}:${s.line} | ${s.kind}${s.evidence ? ` | ${s.evidence}` : ""}`;
9967
10156
  return [
9968
- `Strengthen the tests so these SURVIVING mutants die (attempt ${attempt}).`,
9969
- "",
9970
- "Each survivor below was a deliberate sabotage of production code that the current suite did NOT catch:",
9971
- ...survivors.map((s) => `- ${s.id} | ${s.file}:${s.line} | ${s.kind}${s.evidence ? ` | ${s.evidence}` : ""}`),
10157
+ `Strengthen the tests so the mutants below die (attempt ${attempt}).`,
9972
10158
  "",
9973
- `Test command that must fail under each mutant: ${i.testCommand}`,
10159
+ ...confirmed.length > 0 ? [
10160
+ "CONFIRMED SURVIVORS \u2014 each was a deliberate sabotage of production code that the current suite did NOT catch:",
10161
+ ...confirmed.map(row),
10162
+ ""
10163
+ ] : [],
10164
+ ...unverified.length > 0 ? [
10165
+ "UNVERIFIED \u2014 these mutations were never actually re-tested (the re-verify pass skipped or did not report them). Do NOT assume the suite misses them: first apply each mutation, run the tests, and confirm it really survives; if the tests already fail, report that instead of writing new assertions.",
10166
+ ...unverified.map(row),
10167
+ ""
10168
+ ] : [],
10169
+ `Test command that must fail under each CONFIRMED mutant: ${i.testCommand}`,
9974
10170
  "",
9975
- "For each survivor add or tighten exactly one assertion that pins the sabotaged boundary/behavior. Do not change production code. Do not weaken other tests. Run the suite green on clean code before finishing."
10171
+ "For each CONFIRMED survivor add or tighten exactly one assertion that pins the sabotaged boundary/behavior. Do not change production code. Do not weaken other tests. Run the suite green on clean code before finishing."
9976
10172
  ].join("\n");
9977
10173
  }
9978
10174
  function collectOutcomes(result, plan) {
9979
10175
  const fromText = parseTextOutcomes(result);
9980
10176
  if (fromText.length > 0) {
9981
- const planned = new Set(plan.map((p) => p.id));
9982
- const matched = fromText.filter((m) => planned.has(m.id));
9983
- if (matched.length > 0) return matched;
10177
+ const remaining = [...plan];
10178
+ const matched = [];
10179
+ for (const m of fromText) {
10180
+ const idx = remaining.findIndex((p) => p.id === m.id);
10181
+ if (idx === -1) continue;
10182
+ remaining.splice(idx, 1);
10183
+ matched.push(m);
10184
+ }
10185
+ if (matched.length > 0) {
10186
+ const missing = remaining.map((p) => ({
10187
+ id: p.id,
10188
+ file: p.file,
10189
+ line: p.line,
10190
+ kind: p.kind,
10191
+ status: "skipped",
10192
+ evidence: "not reported by chaos task"
10193
+ }));
10194
+ return [...matched, ...missing];
10195
+ }
9984
10196
  }
9985
10197
  return plan.map((p) => ({
9986
10198
  id: p.id,
@@ -9991,6 +10203,9 @@ function collectOutcomes(result, plan) {
9991
10203
  evidence: result ? `chaos task ended ${result.status}` : "chaos task produced no result"
9992
10204
  }));
9993
10205
  }
10206
+ function isKill(status) {
10207
+ return status === "killed" || status === "killed-by-hang";
10208
+ }
9994
10209
  function parseTextOutcomes(result) {
9995
10210
  const text = typeof result?.result === "string" ? result.result : void 0;
9996
10211
  if (!text) return [];
@@ -10693,6 +10908,14 @@ var PATTERNS = [
10693
10908
  anchor: "sk-ant-"
10694
10909
  },
10695
10910
  { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g, anchor: "sk-" },
10911
+ {
10912
+ // `xai` is a first-class provider in this codebase, but its key shape was
10913
+ // absent here — so the one credential format WrongStack itself hands users
10914
+ // was the one the scrubber could not recognize (audit 2026-08-20).
10915
+ type: "xai_key",
10916
+ regex: /(?<![A-Za-z0-9])xai-[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g,
10917
+ anchor: "xai-"
10918
+ },
10696
10919
  { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g, anchor: "ghp_" },
10697
10920
  { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g, anchor: "github_pat_" },
10698
10921
  { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g, anchor: "AKIA" },
@@ -10783,8 +11006,8 @@ var PATTERNS = [
10783
11006
  // replacement so the separator between adjacent secrets is preserved
10784
11007
  // rather than collapsed. Capture groups are therefore: 1=leading
10785
11008
  // delimiter, 2=key name, 3=value.
10786
- regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
10787
- anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
11009
+ regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD|PASSPHRASE))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
11010
+ anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD", "PASSPHRASE"]
10788
11011
  },
10789
11012
  {
10790
11013
  type: "json_credential_key",
@@ -10901,6 +11124,27 @@ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key
10901
11124
  var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
10902
11125
  var SCRUB_CHUNK_BYTES = 64 * 1024;
10903
11126
  var SCRUB_OVERLAP_BYTES = 1024;
11127
+ var PEM_PRIVATE_KEY_BEGIN_RE = /-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----/;
11128
+ var PEM_END_MARKER = "-----END";
11129
+ var MAX_PEM_BLOCK_BYTES = 64 * 1024;
11130
+ var PEM_END_LINE_TOLERANCE = 64;
11131
+ function extendChunkBoundaryPastPem(text, chunkStart, proposedEnd) {
11132
+ const head = text.slice(chunkStart, proposedEnd);
11133
+ const lastBegin = head.lastIndexOf("-----BEGIN ");
11134
+ if (lastBegin === -1) return proposedEnd;
11135
+ const fromBegin = text.slice(chunkStart + lastBegin);
11136
+ const marker = PEM_PRIVATE_KEY_BEGIN_RE.exec(fromBegin);
11137
+ if (!marker || marker.index !== 0) return proposedEnd;
11138
+ const bodyStart = marker[0].length;
11139
+ const cap = Math.min(text.length, chunkStart + lastBegin + MAX_PEM_BLOCK_BYTES);
11140
+ const closeIdx = fromBegin.indexOf(PEM_END_MARKER, bodyStart);
11141
+ if (closeIdx === -1 || chunkStart + lastBegin + closeIdx >= cap + PEM_END_LINE_TOLERANCE) {
11142
+ return proposedEnd;
11143
+ }
11144
+ const lineEnd = fromBegin.indexOf("\n", closeIdx);
11145
+ const end = lineEnd === -1 ? text.length : chunkStart + lastBegin + lineEnd + 1;
11146
+ return Math.max(proposedEnd, end);
11147
+ }
10904
11148
  var PATTERN_ANCHORS = [
10905
11149
  ...new Set(
10906
11150
  PATTERNS.flatMap(
@@ -10937,6 +11181,7 @@ var DefaultSecretScrubber = class {
10937
11181
  }
10938
11182
  }
10939
11183
  end = safe === -1 ? end : safe + 1;
11184
+ end = extendChunkBoundaryPastPem(text, i, end);
10940
11185
  }
10941
11186
  out.push(this.scrubOne(text.slice(i, end)));
10942
11187
  i = end;
@@ -14768,9 +15013,21 @@ function renderCommandLine(command, args) {
14768
15013
  });
14769
15014
  return [command, ...rendered].join(" ");
14770
15015
  }
14771
- function subjectForToolInput(toolName, input, subjectKey) {
15016
+ function renderSubjectFields(obj, fields) {
15017
+ const parts = [];
15018
+ for (const field of fields) {
15019
+ const value = obj[field];
15020
+ if (value === void 0 || value === null || value === "" || value === false) continue;
15021
+ const str = String(value);
15022
+ parts.push(`${field}=${/\s/.test(str) ? `"${str.replace(/"/g, '\\"')}"` : str}`);
15023
+ }
15024
+ return parts.join(" ");
15025
+ }
15026
+ function subjectForToolInput(toolName, input, subjectKey, subjectFields) {
14772
15027
  if (!input || typeof input !== "object") return void 0;
14773
15028
  const obj = input;
15029
+ const extra = subjectFields && subjectFields.length > 0 ? renderSubjectFields(obj, subjectFields) : "";
15030
+ const withExtra = (base) => extra ? `${base} ${extra}` : base;
14774
15031
  if (subjectKey) {
14775
15032
  const value = obj[subjectKey];
14776
15033
  if (Array.isArray(value)) {
@@ -14786,9 +15043,9 @@ function subjectForToolInput(toolName, input, subjectKey) {
14786
15043
  if (subjectKey === "command") {
14787
15044
  const rendered = renderCommandLine(value, obj["args"]);
14788
15045
  if (value === "commit" && obj["dry_run"] === true) {
14789
- return `${escapeGlobSubject(rendered)}:dry-run`;
15046
+ return `${escapeGlobSubject(withExtra(rendered))}:dry-run`;
14790
15047
  }
14791
- return escapeGlobSubject(rendered);
15048
+ return escapeGlobSubject(withExtra(rendered));
14792
15049
  }
14793
15050
  if (subjectKey === "directory" && obj["dry_run"] === true) {
14794
15051
  return `${escapeGlobSubject(value)}:dry-run`;
@@ -20778,6 +21035,7 @@ function worktreeOwnerLabel(task, config) {
20778
21035
  }
20779
21036
 
20780
21037
  // src/coordination/director.ts
21038
+ var BUSY_REARM_FLOOR_MS = 1e3;
20781
21039
  var Director = class _Director {
20782
21040
  /* eslint-disable-next-line @typescript-eslint/no-unused-vars — just a cast helper */
20783
21041
  static _asManifestEntry(v) {
@@ -20843,6 +21101,13 @@ var Director = class _Director {
20843
21101
  subagentIdleTimeoutMs;
20844
21102
  retireSubagentOnTaskComplete;
20845
21103
  subagentIdleTimers = /* @__PURE__ */ new Map();
21104
+ /**
21105
+ * Effective idle window per subagent (spawn-time `idleTimeoutMs` override
21106
+ * or the Director-wide default; undefined = no window). Internal-task
21107
+ * completion re-arms with THIS value, not the Director-wide default, so
21108
+ * a subagent-configured window survives its first internal probe.
21109
+ */
21110
+ subagentIdleDelayMs = /* @__PURE__ */ new Map();
20846
21111
  sharedScratchpadPath;
20847
21112
  maxSpawns;
20848
21113
  maxSpawnDepth;
@@ -20999,7 +21264,13 @@ var Director = class _Director {
20999
21264
  handleTaskCompleted(payload) {
21000
21265
  const r = payload.result;
21001
21266
  const settled = this.tasks.settle(r);
21002
- if (settled.internal) return;
21267
+ if (settled.internal) {
21268
+ this.armSubagentIdleRetirement(
21269
+ r.subagentId,
21270
+ this.subagentIdleDelayMs.get(r.subagentId) ?? this.subagentIdleTimeoutMs
21271
+ );
21272
+ return;
21273
+ }
21003
21274
  const title = this.tasks.descriptionFor(r.taskId, payload.task.description ?? r.taskId);
21004
21275
  if (!settled.consumedInBand && this.taskResultNotifier) {
21005
21276
  const resultText = typeof r.result === "string" ? r.result : r.result !== void 0 ? safeStringify(r.result) : void 0;
@@ -21064,7 +21335,7 @@ var Director = class _Director {
21064
21335
  }
21065
21336
  this.armSubagentIdleRetirement(
21066
21337
  r.subagentId,
21067
- this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleTimeoutMs
21338
+ this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleDelayMs.get(r.subagentId) ?? this.subagentIdleTimeoutMs
21068
21339
  );
21069
21340
  }
21070
21341
  extensionsFor(subagentId) {
@@ -21150,6 +21421,7 @@ var Director = class _Director {
21150
21421
  this.resolveSpawnModel(config);
21151
21422
  const subagentId = await spawn3(this, config, priceLookup);
21152
21423
  const perSubagentIdleMs = typeof config.idleTimeoutMs === "number" && Number.isFinite(config.idleTimeoutMs) && config.idleTimeoutMs >= 0 ? config.idleTimeoutMs : this.subagentIdleTimeoutMs;
21424
+ this.subagentIdleDelayMs.set(subagentId, perSubagentIdleMs);
21153
21425
  this.armSubagentIdleRetirement(subagentId, perSubagentIdleMs);
21154
21426
  return subagentId;
21155
21427
  }
@@ -21203,6 +21475,7 @@ var Director = class _Director {
21203
21475
  this.budgetPolicy.dispose();
21204
21476
  for (const timer of this.subagentIdleTimers.values()) clearTimeout(timer);
21205
21477
  this.subagentIdleTimers.clear();
21478
+ this.subagentIdleDelayMs.clear();
21206
21479
  await this.coordinator.stopAll();
21207
21480
  this.tasks.resolveWaitersOnShutdown();
21208
21481
  for (const b of this.subagentBridges.values()) {
@@ -21261,6 +21534,7 @@ var Director = class _Director {
21261
21534
  }
21262
21535
  async remove(subagentId) {
21263
21536
  this.clearSubagentIdleRetirement(subagentId);
21537
+ this.subagentIdleDelayMs.delete(subagentId);
21264
21538
  void this.appendSessionEvent({
21265
21539
  type: "agent_stopped",
21266
21540
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -21313,9 +21587,13 @@ var Director = class _Director {
21313
21587
  const timer = setTimeout(() => {
21314
21588
  this.subagentIdleTimers.delete(subagentId);
21315
21589
  const entry = this.coordinator.getStatus().subagents.find((a) => a.id === subagentId);
21316
- if (entry?.status !== "idle") return;
21590
+ if (entry === void 0) return;
21591
+ if (entry.status !== "idle") {
21592
+ this.armSubagentIdleRetirement(subagentId, Math.max(delayMs, BUSY_REARM_FLOOR_MS));
21593
+ return;
21594
+ }
21317
21595
  if (this.coordinator.listPendingTasks().some((task) => task.subagentId === subagentId)) {
21318
- this.armSubagentIdleRetirement(subagentId, this.subagentIdleTimeoutMs);
21596
+ this.armSubagentIdleRetirement(subagentId, Math.max(delayMs, BUSY_REARM_FLOOR_MS));
21319
21597
  return;
21320
21598
  }
21321
21599
  void this.remove(subagentId).catch(
@@ -28587,6 +28865,10 @@ var DefaultSkillLoader = class {
28587
28865
  );
28588
28866
  for (const e of entries) {
28589
28867
  if (!await entryIsDirectory(dir, e)) continue;
28868
+ if (!isValidSkillNameFormat(e.name)) {
28869
+ this.skipped.push({ dir, entry: e.name, reason: "invalid-name-format" });
28870
+ continue;
28871
+ }
28590
28872
  const skillFile = path30.join(dir, e.name, "SKILL.md");
28591
28873
  let raw;
28592
28874
  try {
@@ -30260,7 +30542,7 @@ var ToolExecutor = class _ToolExecutor {
30260
30542
  return { result, tool, durationMs: Date.now() - start };
30261
30543
  }
30262
30544
  if (effectivePermission === "confirm") {
30263
- const suggestedPattern = boundary.decision === "confirm" ? `kanban-boundary:${boundary.path ?? tool.name}` : subjectForToolInput(tool.name, use.input, tool.subjectKey) ?? tool.name;
30545
+ const suggestedPattern = boundary.decision === "confirm" ? `kanban-boundary:${boundary.path ?? tool.name}` : subjectForToolInput(tool.name, use.input, tool.subjectKey, tool.subjectFields) ?? tool.name;
30264
30546
  if (this.opts.confirmAwaiter) {
30265
30547
  const awaiter = this.opts.confirmAwaiter;
30266
30548
  const choice = await new Promise(
@@ -32790,7 +33072,9 @@ function hasRecursiveForceDelete(command, projectRoot) {
32790
33072
  if (token === "rd" || token === "rmdir") {
32791
33073
  const args = commandSegment(tokens, i + 1).map((arg) => arg.toLowerCase());
32792
33074
  if (args.includes("/s")) {
32793
- const targets = args.filter((arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg));
33075
+ const targets = args.filter(
33076
+ (arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg)
33077
+ );
32794
33078
  if (targets.length === 0) return true;
32795
33079
  if (targets.some(isCatastrophicDeleteTarget)) return true;
32796
33080
  if (targets.some((target) => !pathLooksInsideProject(target, projectRoot))) return true;
@@ -32859,7 +33143,8 @@ function hasFindExec(command) {
32859
33143
  function isCatastrophicDeleteTarget(rawTarget) {
32860
33144
  const t = rawTarget.replace(/^['"]|['"]$/g, "").trim();
32861
33145
  if (!t) return false;
32862
- if (t === "*" || t === "." || t === "./" || t === ".\\" || t === "./*" || t === ".\\*") return true;
33146
+ if (t === "*" || t === "." || t === "./" || t === ".\\" || t === "./*" || t === ".\\*")
33147
+ return true;
32863
33148
  const s = t.replace(/[\\/]\*+$/, "").replace(/[\\/]+$/, "");
32864
33149
  if (s === "") return true;
32865
33150
  if (s === "~" || /^\$HOME$/i.test(s) || /^%USERPROFILE%$/i.test(s)) return true;
@@ -32899,12 +33184,16 @@ function hasCatastrophicDelete(command) {
32899
33184
  const args = tokens.slice(i + 1);
32900
33185
  const recursive = args.some((arg) => arg.toLowerCase() === "/s");
32901
33186
  if (!recursive) continue;
32902
- const targets = args.filter((arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg));
33187
+ const targets = args.filter(
33188
+ (arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg)
33189
+ );
32903
33190
  if (targets.some(isCatastrophicDeleteTarget)) return true;
32904
33191
  }
32905
33192
  if (token === "del" || token === "erase") {
32906
33193
  const args = tokens.slice(i + 1);
32907
- const targets = args.filter((arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg));
33194
+ const targets = args.filter(
33195
+ (arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg)
33196
+ );
32908
33197
  if (targets.some(isCatastrophicDeleteTarget)) return true;
32909
33198
  }
32910
33199
  }
@@ -32965,6 +33254,47 @@ function isClearlyDestructiveBashCommand(command, projectRoot) {
32965
33254
  if (HIGH_IMPACT_PATTERNS.some((pattern) => pattern.test(trimmed))) return true;
32966
33255
  return false;
32967
33256
  }
33257
+ var WELL_KNOWN_CREDENTIAL_ENV_VARS = /* @__PURE__ */ new Set([
33258
+ "ANTHROPIC_API_KEY",
33259
+ "ANTHROPIC_AUTH_TOKEN",
33260
+ "OPENAI_API_KEY",
33261
+ "AZURE_OPENAI_API_KEY",
33262
+ "GEMINI_API_KEY",
33263
+ "GOOGLE_API_KEY",
33264
+ "GOOGLE_APPLICATION_CREDENTIALS",
33265
+ "GOOGLE_GENERATIVE_AI_API_KEY",
33266
+ "GROQ_API_KEY",
33267
+ "MISTRAL_API_KEY",
33268
+ "COHERE_API_KEY",
33269
+ "DEEPSEEK_API_KEY",
33270
+ "XAI_API_KEY",
33271
+ "OPENROUTER_API_KEY",
33272
+ "PERPLEXITY_API_KEY",
33273
+ "TOGETHER_API_KEY",
33274
+ "FIREWORKS_API_KEY",
33275
+ "HUGGINGFACE_API_KEY",
33276
+ "HF_TOKEN",
33277
+ "GITHUB_TOKEN",
33278
+ "GH_TOKEN",
33279
+ "NPM_TOKEN",
33280
+ "AWS_ACCESS_KEY_ID",
33281
+ "AWS_SECRET_ACCESS_KEY",
33282
+ "AWS_SESSION_TOKEN",
33283
+ "AZURE_CLIENT_SECRET",
33284
+ "GITLAB_TOKEN",
33285
+ "SLACK_TOKEN",
33286
+ "STRIPE_SECRET_KEY",
33287
+ "TELEGRAM_BOT_TOKEN",
33288
+ "WRONGSTACK_VAULT_PASSPHRASE"
33289
+ ]);
33290
+ function attachesWellKnownCredential(input) {
33291
+ if (!input || typeof input !== "object") return false;
33292
+ const envVars = input["envVars"];
33293
+ if (!Array.isArray(envVars)) return false;
33294
+ return envVars.some(
33295
+ (name) => typeof name === "string" && WELL_KNOWN_CREDENTIAL_ENV_VARS.has(name.toUpperCase())
33296
+ );
33297
+ }
32968
33298
 
32969
33299
  // src/security/permission-helpers.ts
32970
33300
  function matchesTrust(patterns, subject) {
@@ -32981,7 +33311,7 @@ function hasShellSubject(tool) {
32981
33311
  ]);
32982
33312
  }
32983
33313
  function alwaysAllowUnavailableReason(tool, input) {
32984
- const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
33314
+ const subject = subjectForToolInput(tool.name, input, tool.subjectKey, tool.subjectFields);
32985
33315
  if (subject !== void 0) return void 0;
32986
33316
  return `"always allow" needs a subject to remember, and ${tool.name} calls do not carry one (no subjectKey, and no path/url/name input). Recording it would store a rule that can never match. Approve this call, or set a trust rule for ${tool.name} explicitly.`;
32987
33317
  }
@@ -33041,8 +33371,18 @@ var AGENT_STATE_SENSITIVE_BASENAMES = /^(?:config\.json|config\.local\.json|trus
33041
33371
  function unescapeGlobSubject(value) {
33042
33372
  return value.replace(/\\([*?[\]])/g, "$1");
33043
33373
  }
33374
+ function stripAdsSuffix(forwardSlashPath) {
33375
+ const cut = forwardSlashPath.lastIndexOf("/");
33376
+ const dir = cut === -1 ? "" : forwardSlashPath.slice(0, cut + 1);
33377
+ const base = cut === -1 ? forwardSlashPath : forwardSlashPath.slice(cut + 1);
33378
+ const colon = base.indexOf(":");
33379
+ if (colon === -1 || cut === -1 && colon === 1 && base.length <= 2) return forwardSlashPath;
33380
+ return dir + base.slice(0, colon);
33381
+ }
33044
33382
  function normalizeForCompare(value) {
33045
- const forward = unescapeGlobSubject(value).replace(/\\/g, "/").replace(/\/+$/, "");
33383
+ const forward = stripAdsSuffix(
33384
+ unescapeGlobSubject(value).replace(/\\/g, "/").replace(/\/+$/, "")
33385
+ );
33046
33386
  return process.platform === "win32" ? forward.toLowerCase() : forward;
33047
33387
  }
33048
33388
  function realpathOfNearestExisting(p) {
@@ -33079,7 +33419,7 @@ function isProtectedAgentStatePath(absPath) {
33079
33419
  return AGENT_STATE_SENSITIVE_BASENAMES.test(path40.basename(normalizeForCompare(absPath)));
33080
33420
  }
33081
33421
  function pathLooksSensitive(rawPath) {
33082
- const normalized = stripShellQuotes(rawPath).replace(/\\/g, "/");
33422
+ const normalized = stripAdsSuffix(stripShellQuotes(rawPath).replace(/\\/g, "/"));
33083
33423
  if (SENSITIVE_READ_PATHS.some((pattern) => pattern.test(normalized))) return true;
33084
33424
  return isProtectedAgentStatePath(normalized);
33085
33425
  }
@@ -33103,10 +33443,24 @@ function shellCommandReadsSensitivePath(command) {
33103
33443
  }
33104
33444
  return false;
33105
33445
  }
33446
+ function isSensitiveReadCall(tool, input) {
33447
+ const isReadTool = hasCapability(tool, ToolCapabilities.FS_READ) || tool.name === "read" || tool.name === "grep" || tool.name === "glob" || tool.name === "tree";
33448
+ if (isReadTool && inputPathLooksSensitive(input)) return true;
33449
+ const hasShellCap = hasCapability(tool, [
33450
+ ToolCapabilities.SHELL_ARBITRARY,
33451
+ ToolCapabilities.SHELL_RESTRICTED,
33452
+ ToolCapabilities.SHELL_EXEC
33453
+ ]);
33454
+ if (!hasShellCap && tool.name !== "bash" && tool.name !== "shell" && tool.name !== "exec") {
33455
+ return false;
33456
+ }
33457
+ const command = shellCommandLineFromInput(input);
33458
+ return command ? shellCommandReadsSensitivePath(command) : false;
33459
+ }
33106
33460
 
33107
33461
  // src/security/permission-explain.ts
33108
33462
  function explainPermissionTrace(state, tool, input, ctx) {
33109
- const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
33463
+ const subject = subjectForToolInput(tool.name, input, tool.subjectKey, tool.subjectFields);
33110
33464
  const steps = [];
33111
33465
  let winnerIndex = -1;
33112
33466
  const add = (rule, matched, decision, source, detail) => {
@@ -33327,13 +33681,7 @@ function explainPermissionTrace(state, tool, input, ctx) {
33327
33681
  }
33328
33682
  };
33329
33683
  }
33330
- add(
33331
- "yolo",
33332
- true,
33333
- "auto",
33334
- "yolo",
33335
- "YOLO mode is active \u2014 auto-approving every non-denied call"
33336
- );
33684
+ add("yolo", true, "auto", "yolo", "YOLO mode is active \u2014 auto-approving every non-denied call");
33337
33685
  winnerIndex = steps.length - 1;
33338
33686
  return {
33339
33687
  toolName: tool.name,
@@ -33604,7 +33952,14 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
33604
33952
  static isMcpTool(name) {
33605
33953
  return name.startsWith("mcp__");
33606
33954
  }
33607
- async evaluate(tool) {
33955
+ async evaluate(tool, input) {
33956
+ if (input !== void 0 && isSensitiveReadCall(tool, input)) {
33957
+ return {
33958
+ permission: "deny",
33959
+ source: "subagent_guard",
33960
+ reason: "subagents may not read credential-bearing paths \u2014 the leader must perform this read so the user can approve it"
33961
+ };
33962
+ }
33608
33963
  const caps = tool.capabilities ?? [];
33609
33964
  const hasAllowedCap = caps.some((c) => this.allowedCapabilities.includes(c));
33610
33965
  const isMcp = _AutoApprovePermissionPolicy.isMcpTool(tool.name);
@@ -33631,8 +33986,8 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
33631
33986
  }
33632
33987
  allowOnce() {
33633
33988
  }
33634
- async explain(tool) {
33635
- const decision = await this.evaluate(tool);
33989
+ async explain(tool, input) {
33990
+ const decision = await this.evaluate(tool, input);
33636
33991
  return {
33637
33992
  toolName: tool.name,
33638
33993
  subject: null,
@@ -33682,6 +34037,17 @@ function fsWriteTargetPaths(input) {
33682
34037
  }
33683
34038
  return out;
33684
34039
  }
34040
+ function mergeTrustEntries(exact, wildcard) {
34041
+ if (!exact) return wildcard;
34042
+ if (!wildcard) return exact;
34043
+ const deny = [...wildcard.deny ?? [], ...exact.deny ?? []];
34044
+ const merged = {
34045
+ ...wildcard,
34046
+ ...exact
34047
+ };
34048
+ if (deny.length > 0) merged.deny = [...new Set(deny)];
34049
+ return merged;
34050
+ }
33685
34051
  var DefaultPermissionPolicy = class {
33686
34052
  policy = {};
33687
34053
  loaded = false;
@@ -33720,6 +34086,7 @@ var DefaultPermissionPolicy = class {
33720
34086
  yoloBlockedAsDestructive(tool, input, ctx) {
33721
34087
  if (!this.yolo || this.yoloDestructive) return false;
33722
34088
  if (this.hasAgentStateWriteTarget(tool, input, ctx)) return true;
34089
+ if (attachesWellKnownCredential(input)) return true;
33723
34090
  const isShellSurface = tool.name === "bash" || tool.name === "exec" || (tool.capabilities ?? []).includes("shell.arbitrary");
33724
34091
  if (!isShellSurface) return false;
33725
34092
  const command = getInputString(input, "command") ?? shellCommandLineFromInput(input);
@@ -33813,8 +34180,8 @@ var DefaultPermissionPolicy = class {
33813
34180
  };
33814
34181
  }
33815
34182
  const namespaceEntry = this.findNamespaceEntry(tool.name);
33816
- const entry = this.policy[tool.name] ?? namespaceEntry;
33817
- const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
34183
+ const entry = mergeTrustEntries(this.policy[tool.name], namespaceEntry);
34184
+ const subject = subjectForToolInput(tool.name, input, tool.subjectKey, tool.subjectFields);
33818
34185
  const cacheKey = `${tool.name}::${subject ?? tool.name}`;
33819
34186
  const evalKey = `${cacheKey}::${permissionFingerprint(tool)}`;
33820
34187
  if (tool.name !== "write" && !this.hasAgentStateWriteTarget(tool, input, ctx)) {
@@ -33831,15 +34198,6 @@ var DefaultPermissionPolicy = class {
33831
34198
  this._evalCache.set(evalKey, decision);
33832
34199
  return decision;
33833
34200
  }
33834
- if (this.sessionAllowed.has(cacheKey)) {
33835
- this.sessionAllowed.delete(cacheKey);
33836
- const decision = {
33837
- permission: "auto",
33838
- source: "trust",
33839
- reason: "session one-shot allow (user pressed yes)"
33840
- };
33841
- return decision;
33842
- }
33843
34201
  if (entry?.deny && subject && matchesTrust(entry.deny, subject)) {
33844
34202
  this._logDeny(tool.name, subject, "matched deny pattern");
33845
34203
  const decision = {
@@ -33850,6 +34208,15 @@ var DefaultPermissionPolicy = class {
33850
34208
  this._evalCache.set(evalKey, decision);
33851
34209
  return decision;
33852
34210
  }
34211
+ if (this.sessionAllowed.has(cacheKey)) {
34212
+ this.sessionAllowed.delete(cacheKey);
34213
+ const decision = {
34214
+ permission: "auto",
34215
+ source: "trust",
34216
+ reason: "session one-shot allow (user pressed yes)"
34217
+ };
34218
+ return decision;
34219
+ }
33853
34220
  if (tool.permission === "deny") {
33854
34221
  this._logDeny(tool.name, subject, "tool default deny");
33855
34222
  const decision = {
@@ -33860,6 +34227,7 @@ var DefaultPermissionPolicy = class {
33860
34227
  this._evalCache.set(evalKey, decision);
33861
34228
  return decision;
33862
34229
  }
34230
+ const denyUnevaluated = Boolean(entry?.deny?.length) && subject === void 0;
33863
34231
  const allowMatches = hasShellSubject(tool) ? matchesCommandTrust : matchesTrust;
33864
34232
  if (entry?.allow && subject && allowMatches(entry.allow, subject)) {
33865
34233
  const decision = {
@@ -33870,7 +34238,7 @@ var DefaultPermissionPolicy = class {
33870
34238
  this._evalCache.set(evalKey, decision);
33871
34239
  return decision;
33872
34240
  }
33873
- if (entry?.auto) {
34241
+ if (entry?.auto && !denyUnevaluated) {
33874
34242
  const decision = { permission: "auto", source: "trust" };
33875
34243
  this._evalCache.set(evalKey, decision);
33876
34244
  return decision;
@@ -33970,19 +34338,10 @@ var DefaultPermissionPolicy = class {
33970
34338
  }
33971
34339
  return { permission: "confirm", source: "default" };
33972
34340
  }
34341
+ // Delegates to the shared helper so the subagent policy applies the exact
34342
+ // same rule — see `isSensitiveReadCall` in ./permission-helpers.ts.
33973
34343
  isSensitiveReadCall(tool, input) {
33974
- const isReadTool = hasCapability(tool, ToolCapabilities.FS_READ) || tool.name === "read" || tool.name === "grep" || tool.name === "glob" || tool.name === "tree";
33975
- if (isReadTool && inputPathLooksSensitive(input)) return true;
33976
- const hasShellCap = hasCapability(tool, [
33977
- ToolCapabilities.SHELL_ARBITRARY,
33978
- ToolCapabilities.SHELL_RESTRICTED,
33979
- ToolCapabilities.SHELL_EXEC
33980
- ]);
33981
- if (!hasShellCap && tool.name !== "bash" && tool.name !== "shell" && tool.name !== "exec") {
33982
- return false;
33983
- }
33984
- const command = shellCommandLineFromInput(input);
33985
- return command ? shellCommandReadsSensitivePath(command) : false;
34344
+ return isSensitiveReadCall(tool, input);
33986
34345
  }
33987
34346
  async trust(rule) {
33988
34347
  if (!this.loaded) await this.reload();
@@ -34106,7 +34465,7 @@ function walk2(node, vault, transform) {
34106
34465
  }
34107
34466
  return out;
34108
34467
  }
34109
- var SECRET_KEY_PATTERN = /(?:apikey|api_key|authtoken|auth_token|bearer|secret|password|passwd|pwd|refreshtoken|refresh_token|sessionkey|session_key|access[_-]?token|private[_-]?key|token\b)/i;
34468
+ var SECRET_KEY_PATTERN = /(?:api[-_]?key|auth[-_]?token|authorization|proxy-authorization|cookie|bearer|secret|password|passwd|pwd|refresh[-_]?token|session[-_]?key|access[_-]?token|private[_-]?key|token\b)/i;
34110
34469
  var NON_SECRET_OVERRIDES = /* @__PURE__ */ new Set(["publickey", "public_key"]);
34111
34470
  function isSecretField(name) {
34112
34471
  const lc = name.toLowerCase();
@@ -34215,6 +34574,14 @@ function keyFileNeedsHardening(keyFile, opts) {
34215
34574
  }
34216
34575
  return false;
34217
34576
  }
34577
+ function mkdirSecretDirSync(dir) {
34578
+ fs14.mkdirSync(dir, { recursive: true, mode: 448 });
34579
+ if (process.platform === "win32") return;
34580
+ try {
34581
+ fs14.chmodSync(dir, 448);
34582
+ } catch {
34583
+ }
34584
+ }
34218
34585
  function writeKeyFileAtomicSync(keyFile, content) {
34219
34586
  const tmp = `${keyFile}.${randomBytes3(4).toString("hex")}.tmp`;
34220
34587
  const fd = fs14.openSync(tmp, "w", 384);
@@ -34362,7 +34729,7 @@ var DefaultSecretVault = class {
34362
34729
  const oldVersion = this._keyVersion;
34363
34730
  const newKey = randomBytes3(KEY_BYTES);
34364
34731
  const newVersion = oldVersion + 1;
34365
- fs14.mkdirSync(path42.dirname(this.keyFile), { recursive: true });
34732
+ mkdirSecretDirSync(path42.dirname(this.keyFile));
34366
34733
  const passphrase = getVaultPassphrase();
34367
34734
  if (passphrase) {
34368
34735
  writeKeyFileAtomicSync(this.keyFile, wrapDataKey(newKey, newVersion, passphrase));
@@ -34446,7 +34813,7 @@ var DefaultSecretVault = class {
34446
34813
  } catch (err) {
34447
34814
  if (err.code !== "ENOENT") throw err;
34448
34815
  }
34449
- fs14.mkdirSync(path42.dirname(this.keyFile), { recursive: true });
34816
+ mkdirSecretDirSync(path42.dirname(this.keyFile));
34450
34817
  const key = randomBytes3(KEY_BYTES);
34451
34818
  const passphrase = getVaultPassphrase();
34452
34819
  const initialBytes = passphrase ? wrapDataKey(key, 1, passphrase) : key;
@@ -35148,6 +35515,17 @@ var IN_PROJECT_DENIED_PATHS = [
35148
35515
  // See discover-mailbox-bridge.ts:findWorkspaceCliEntry.
35149
35516
  path: "features.mailboxBridge",
35150
35517
  reason: "Enables the mailbox bridge, whose CLI-entry resolution walks up from the project root \u2014 a repo-supplied packages/cli/dist/index.js would be spawned on WebUI boot."
35518
+ },
35519
+ {
35520
+ // `plugins` is already denied above, so a repo cannot ADD a plugin. This
35521
+ // closes the other half: a repo could previously ship
35522
+ // `{"features":{"pluginsTrust":false}}` and switch off the integrity gate
35523
+ // for plugins the user had ALREADY installed globally — disarming the
35524
+ // trust-on-first-use pin that exists to catch a supply-chain update
35525
+ // rewriting a plugin's entry file. Same operator-owned class as the
35526
+ // switches above.
35527
+ path: "features.pluginsTrust",
35528
+ reason: "Disables the plugin trust-on-first-use integrity gate, re-trusting changed code in already-installed global plugins."
35151
35529
  }
35152
35530
  ];
35153
35531
  function deleteNestedPath(target, path47) {