@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.
package/dist/index.js CHANGED
@@ -2513,7 +2513,7 @@ function walk(node, vault, transform) {
2513
2513
  }
2514
2514
  return out;
2515
2515
  }
2516
- 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;
2516
+ 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;
2517
2517
  var NON_SECRET_OVERRIDES = /* @__PURE__ */ new Set(["publickey", "public_key"]);
2518
2518
  function isSecretField(name) {
2519
2519
  const lc = name.toLowerCase();
@@ -2684,6 +2684,14 @@ function keyFileNeedsHardening(keyFile, opts) {
2684
2684
  }
2685
2685
  return false;
2686
2686
  }
2687
+ function mkdirSecretDirSync(dir) {
2688
+ fs2.mkdirSync(dir, { recursive: true, mode: 448 });
2689
+ if (process.platform === "win32") return;
2690
+ try {
2691
+ fs2.chmodSync(dir, 448);
2692
+ } catch {
2693
+ }
2694
+ }
2687
2695
  function writeKeyFileAtomicSync(keyFile, content) {
2688
2696
  const tmp = `${keyFile}.${randomBytes(4).toString("hex")}.tmp`;
2689
2697
  const fd = fs2.openSync(tmp, "w", 384);
@@ -2831,7 +2839,7 @@ var DefaultSecretVault = class {
2831
2839
  const oldVersion = this._keyVersion;
2832
2840
  const newKey = randomBytes(KEY_BYTES);
2833
2841
  const newVersion = oldVersion + 1;
2834
- fs2.mkdirSync(path3.dirname(this.keyFile), { recursive: true });
2842
+ mkdirSecretDirSync(path3.dirname(this.keyFile));
2835
2843
  const passphrase = getVaultPassphrase();
2836
2844
  if (passphrase) {
2837
2845
  writeKeyFileAtomicSync(this.keyFile, wrapDataKey(newKey, newVersion, passphrase));
@@ -2915,7 +2923,7 @@ var DefaultSecretVault = class {
2915
2923
  } catch (err) {
2916
2924
  if (err.code !== "ENOENT") throw err;
2917
2925
  }
2918
- fs2.mkdirSync(path3.dirname(this.keyFile), { recursive: true });
2926
+ mkdirSecretDirSync(path3.dirname(this.keyFile));
2919
2927
  const key = randomBytes(KEY_BYTES);
2920
2928
  const passphrase = getVaultPassphrase();
2921
2929
  const initialBytes = passphrase ? wrapDataKey(key, 1, passphrase) : key;
@@ -4069,6 +4077,17 @@ var IN_PROJECT_DENIED_PATHS = [
4069
4077
  // See discover-mailbox-bridge.ts:findWorkspaceCliEntry.
4070
4078
  path: "features.mailboxBridge",
4071
4079
  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."
4080
+ },
4081
+ {
4082
+ // `plugins` is already denied above, so a repo cannot ADD a plugin. This
4083
+ // closes the other half: a repo could previously ship
4084
+ // `{"features":{"pluginsTrust":false}}` and switch off the integrity gate
4085
+ // for plugins the user had ALREADY installed globally — disarming the
4086
+ // trust-on-first-use pin that exists to catch a supply-chain update
4087
+ // rewriting a plugin's entry file. Same operator-owned class as the
4088
+ // switches above.
4089
+ path: "features.pluginsTrust",
4090
+ reason: "Disables the plugin trust-on-first-use integrity gate, re-trusting changed code in already-installed global plugins."
4072
4091
  }
4073
4092
  ];
4074
4093
  function deleteNestedPath(target, path131) {
@@ -14860,6 +14879,42 @@ function formatTaskList(tasks) {
14860
14879
  return lines.join("\n");
14861
14880
  }
14862
14881
 
14882
+ // src/utils/terminal-sanitize.ts
14883
+ var ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
14884
+ var ANSI_OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
14885
+ var ANSI_CONTROL_STRING_RE = /\x1b[P^_X][\s\S]*?\x1b\\/g;
14886
+ var ANSI_ESCAPE_RE = /\x1b[ -/]*[@-~]/g;
14887
+ var BIDI_AND_ZERO_WIDTH_RE = /[​-‏‪-‮⁦-⁩]/g;
14888
+ function sanitizeTerminalText(value, tabWidth = 2) {
14889
+ const tab = " ".repeat(Math.max(1, Math.min(8, Math.floor(tabWidth))));
14890
+ const withoutEscapes = value.replace(ANSI_OSC_RE, "").replace(ANSI_CONTROL_STRING_RE, "").replace(ANSI_RE, "").replace(ANSI_ESCAPE_RE, "").replace(BIDI_AND_ZERO_WIDTH_RE, "").replace(/\t/g, tab).replace(/\r/g, "");
14891
+ let safe = "";
14892
+ for (const char of withoutEscapes) {
14893
+ const code = char.codePointAt(0) ?? 0;
14894
+ if (char === "\n" || code >= 32 && code !== 127 && !(code >= 128 && code <= 159)) {
14895
+ safe += char;
14896
+ }
14897
+ }
14898
+ return safe;
14899
+ }
14900
+ function sanitizeTerminalPreview(value, opts = {}) {
14901
+ const maxLines = opts.maxLines ?? 40;
14902
+ const maxChars = opts.maxChars ?? 8e3;
14903
+ const safe = sanitizeTerminalText(value, opts.tabWidth);
14904
+ let truncated = false;
14905
+ let clipped = safe;
14906
+ if (clipped.length > maxChars) {
14907
+ clipped = clipped.slice(0, maxChars);
14908
+ truncated = true;
14909
+ }
14910
+ const lines = clipped.split("\n");
14911
+ if (lines.length > maxLines) {
14912
+ clipped = lines.slice(0, maxLines).join("\n");
14913
+ truncated = true;
14914
+ }
14915
+ return { text: clipped, truncated };
14916
+ }
14917
+
14863
14918
  // src/utils/tool-description-mode.ts
14864
14919
  var DEFAULT_TOOL_DESCRIPTION_MODE = "extend";
14865
14920
  var ORIGINAL_TOOL_DESCRIPTION = /* @__PURE__ */ Symbol.for("wrongstack.tool.originalDescription");
@@ -15763,9 +15818,21 @@ function renderCommandLine(command, args) {
15763
15818
  });
15764
15819
  return [command, ...rendered].join(" ");
15765
15820
  }
15766
- function subjectForToolInput(toolName, input, subjectKey) {
15821
+ function renderSubjectFields(obj, fields) {
15822
+ const parts = [];
15823
+ for (const field of fields) {
15824
+ const value = obj[field];
15825
+ if (value === void 0 || value === null || value === "" || value === false) continue;
15826
+ const str = String(value);
15827
+ parts.push(`${field}=${/\s/.test(str) ? `"${str.replace(/"/g, '\\"')}"` : str}`);
15828
+ }
15829
+ return parts.join(" ");
15830
+ }
15831
+ function subjectForToolInput(toolName, input, subjectKey, subjectFields) {
15767
15832
  if (!input || typeof input !== "object") return void 0;
15768
15833
  const obj = input;
15834
+ const extra = subjectFields && subjectFields.length > 0 ? renderSubjectFields(obj, subjectFields) : "";
15835
+ const withExtra = (base) => extra ? `${base} ${extra}` : base;
15769
15836
  if (subjectKey) {
15770
15837
  const value = obj[subjectKey];
15771
15838
  if (Array.isArray(value)) {
@@ -15781,9 +15848,9 @@ function subjectForToolInput(toolName, input, subjectKey) {
15781
15848
  if (subjectKey === "command") {
15782
15849
  const rendered = renderCommandLine(value, obj["args"]);
15783
15850
  if (value === "commit" && obj["dry_run"] === true) {
15784
- return `${escapeGlobSubject(rendered)}:dry-run`;
15851
+ return `${escapeGlobSubject(withExtra(rendered))}:dry-run`;
15785
15852
  }
15786
- return escapeGlobSubject(rendered);
15853
+ return escapeGlobSubject(withExtra(rendered));
15787
15854
  }
15788
15855
  if (subjectKey === "directory" && obj["dry_run"] === true) {
15789
15856
  return `${escapeGlobSubject(value)}:dry-run`;
@@ -15813,7 +15880,7 @@ function subjectForToolInput(toolName, input, subjectKey) {
15813
15880
  }
15814
15881
 
15815
15882
  // src/utils/win32-cmd.ts
15816
- var WIN32_CMD_META = /[&|<>"\r\n\0]/;
15883
+ var WIN32_CMD_META = /[&|<>"%\r\n\0]/;
15817
15884
  function buildWin32CmdShimInvocation(command, args = []) {
15818
15885
  assertSafeWin32CmdArgs([command, ...args]);
15819
15886
  const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
@@ -27976,6 +28043,14 @@ var PATTERNS = [
27976
28043
  anchor: "sk-ant-"
27977
28044
  },
27978
28045
  { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g, anchor: "sk-" },
28046
+ {
28047
+ // `xai` is a first-class provider in this codebase, but its key shape was
28048
+ // absent here — so the one credential format WrongStack itself hands users
28049
+ // was the one the scrubber could not recognize (audit 2026-08-20).
28050
+ type: "xai_key",
28051
+ regex: /(?<![A-Za-z0-9])xai-[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g,
28052
+ anchor: "xai-"
28053
+ },
27979
28054
  { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g, anchor: "ghp_" },
27980
28055
  { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g, anchor: "github_pat_" },
27981
28056
  { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g, anchor: "AKIA" },
@@ -28066,8 +28141,8 @@ var PATTERNS = [
28066
28141
  // replacement so the separator between adjacent secrets is preserved
28067
28142
  // rather than collapsed. Capture groups are therefore: 1=leading
28068
28143
  // delimiter, 2=key name, 3=value.
28069
- regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
28070
- anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
28144
+ regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD|PASSPHRASE))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
28145
+ anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD", "PASSPHRASE"]
28071
28146
  },
28072
28147
  {
28073
28148
  type: "json_credential_key",
@@ -28184,6 +28259,27 @@ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key
28184
28259
  var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
28185
28260
  var SCRUB_CHUNK_BYTES = 64 * 1024;
28186
28261
  var SCRUB_OVERLAP_BYTES = 1024;
28262
+ var PEM_PRIVATE_KEY_BEGIN_RE = /-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----/;
28263
+ var PEM_END_MARKER = "-----END";
28264
+ var MAX_PEM_BLOCK_BYTES = 64 * 1024;
28265
+ var PEM_END_LINE_TOLERANCE = 64;
28266
+ function extendChunkBoundaryPastPem(text2, chunkStart, proposedEnd) {
28267
+ const head = text2.slice(chunkStart, proposedEnd);
28268
+ const lastBegin = head.lastIndexOf("-----BEGIN ");
28269
+ if (lastBegin === -1) return proposedEnd;
28270
+ const fromBegin = text2.slice(chunkStart + lastBegin);
28271
+ const marker = PEM_PRIVATE_KEY_BEGIN_RE.exec(fromBegin);
28272
+ if (!marker || marker.index !== 0) return proposedEnd;
28273
+ const bodyStart = marker[0].length;
28274
+ const cap = Math.min(text2.length, chunkStart + lastBegin + MAX_PEM_BLOCK_BYTES);
28275
+ const closeIdx = fromBegin.indexOf(PEM_END_MARKER, bodyStart);
28276
+ if (closeIdx === -1 || chunkStart + lastBegin + closeIdx >= cap + PEM_END_LINE_TOLERANCE) {
28277
+ return proposedEnd;
28278
+ }
28279
+ const lineEnd = fromBegin.indexOf("\n", closeIdx);
28280
+ const end = lineEnd === -1 ? text2.length : chunkStart + lastBegin + lineEnd + 1;
28281
+ return Math.max(proposedEnd, end);
28282
+ }
28187
28283
  var PATTERN_ANCHORS = [
28188
28284
  ...new Set(
28189
28285
  PATTERNS.flatMap(
@@ -28220,6 +28316,7 @@ var DefaultSecretScrubber = class {
28220
28316
  }
28221
28317
  }
28222
28318
  end = safe === -1 ? end : safe + 1;
28319
+ end = extendChunkBoundaryPastPem(text2, i, end);
28223
28320
  }
28224
28321
  out.push(this.scrubOne(text2.slice(i, end)));
28225
28322
  i = end;
@@ -32326,24 +32423,29 @@ var TOKEN_PATTERNS = [
32326
32423
  kind: "return-null",
32327
32424
  // `return <expr>;` where expr is not already null/undefined/void.
32328
32425
  regex: /(?<indent>\breturn\b)(?<expr>\s+[^;{}\n]+?)\s*;/g,
32329
- replace: () => "return null;"
32426
+ replace: () => "return null;",
32427
+ endpointsInCode: true
32330
32428
  }
32331
32429
  ];
32332
32430
  function planMutations(file, source, opts = {}) {
32333
32431
  const maxPerFile = opts.maxPerFile ?? 25;
32334
32432
  const out = [];
32335
32433
  const lines = source.split("\n");
32434
+ const masks = computeLineMasks(source);
32336
32435
  for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
32337
32436
  const line = lines[lineIdx];
32338
32437
  const t2 = line.trim();
32339
- if (t2.startsWith("//") || t2.startsWith("*") || t2.startsWith("/*")) continue;
32438
+ if (t2.startsWith("//")) continue;
32439
+ const codeRanges = masks[lineIdx];
32440
+ const inCode = (start) => codeRanges.some(([s, e]) => start >= s && start < e);
32340
32441
  for (const pattern of TOKEN_PATTERNS) {
32341
32442
  pattern.regex.lastIndex = 0;
32342
32443
  let m;
32343
32444
  while ((m = pattern.regex.exec(line)) !== null) {
32344
32445
  const token = m.groups?.["op"] ?? m[0];
32345
32446
  const tokenStart = m.index + m[0].indexOf(token);
32346
- if (isMasked(line, tokenStart, token.length)) continue;
32447
+ if (!inCode(tokenStart)) continue;
32448
+ if (pattern.endpointsInCode && !inCode(tokenStart + token.length - 1)) continue;
32347
32449
  const original = line.slice(tokenStart, tokenStart + token.length);
32348
32450
  const replacement = pattern.replace(token);
32349
32451
  if (replacement === original) continue;
@@ -32362,19 +32464,179 @@ function planMutations(file, source, opts = {}) {
32362
32464
  }
32363
32465
  return out.slice(0, maxPerFile);
32364
32466
  }
32365
- function isMasked(line, start, len) {
32366
- let inSingle = false;
32367
- let inDouble = false;
32368
- for (let i = 0; i < start; i++) {
32369
- const c = line[i];
32370
- const prev = i > 0 ? line[i - 1] : void 0;
32371
- if (c === "'" && prev !== "\\") inSingle = !inSingle;
32372
- else if (c === '"' && prev !== "\\") inDouble = !inDouble;
32373
- if (!inSingle && !inDouble && c === "/" && prev === "/") return true;
32467
+ function computeLineMasks(source) {
32468
+ const lines = source.split("\n");
32469
+ const masks = lines.map(() => []);
32470
+ const stack = [{ kind: "code", depth: 0, parens: [] }];
32471
+ let inBlockComment = false;
32472
+ let lastToken = null;
32473
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
32474
+ const line = lines[lineIdx];
32475
+ const ranges = masks[lineIdx];
32476
+ let runStart = null;
32477
+ const closeRun = (end) => {
32478
+ if (runStart !== null && end > runStart) ranges.push([runStart, end]);
32479
+ runStart = null;
32480
+ };
32481
+ let i = 0;
32482
+ if (inBlockComment) {
32483
+ const close = line.indexOf("*/");
32484
+ if (close === -1) continue;
32485
+ inBlockComment = false;
32486
+ i = close + 2;
32487
+ }
32488
+ while (i < line.length) {
32489
+ const top = stack[stack.length - 1];
32490
+ const c = line[i];
32491
+ if (top.kind === "template") {
32492
+ if (c === "\\") {
32493
+ i += 2;
32494
+ continue;
32495
+ }
32496
+ if (c === "`") {
32497
+ stack.pop();
32498
+ lastToken = "`";
32499
+ i++;
32500
+ continue;
32501
+ }
32502
+ if (c === "$" && line[i + 1] === "{") {
32503
+ stack.push({ kind: "code", depth: 0, parens: [] });
32504
+ lastToken = "${";
32505
+ i += 2;
32506
+ continue;
32507
+ }
32508
+ i++;
32509
+ continue;
32510
+ }
32511
+ if (/[\w$]/.test(c)) {
32512
+ let j = i + 1;
32513
+ while (j < line.length && /[\w$]/.test(line[j])) j++;
32514
+ lastToken = line.slice(i, j);
32515
+ if (runStart === null) runStart = i;
32516
+ i = j;
32517
+ continue;
32518
+ }
32519
+ if (c === "'" || c === '"') {
32520
+ closeRun(i);
32521
+ i++;
32522
+ while (i < line.length && line[i] !== c) {
32523
+ if (line[i] === "\\") i++;
32524
+ i++;
32525
+ }
32526
+ i++;
32527
+ lastToken = c;
32528
+ continue;
32529
+ }
32530
+ if (c === "`") {
32531
+ closeRun(i);
32532
+ stack.push({ kind: "template", depth: 0, parens: [] });
32533
+ i++;
32534
+ continue;
32535
+ }
32536
+ if (c === "/" && line[i + 1] === "/") {
32537
+ closeRun(i);
32538
+ break;
32539
+ }
32540
+ if (c === "/" && line[i + 1] === "*") {
32541
+ closeRun(i);
32542
+ const close = line.indexOf("*/", i + 2);
32543
+ if (close === -1) {
32544
+ inBlockComment = true;
32545
+ break;
32546
+ }
32547
+ i = close + 2;
32548
+ continue;
32549
+ }
32550
+ if (c === "/") {
32551
+ if (!tokenCanEndOperand(lastToken)) {
32552
+ closeRun(i);
32553
+ const next = skipRegexLiteral(line, i);
32554
+ lastToken = next > i + 1 ? "regex" : "/";
32555
+ i = next;
32556
+ continue;
32557
+ }
32558
+ }
32559
+ if (c === "(") {
32560
+ top.parens.push(CONTROL_KEYWORDS.has(lastToken ?? "") ? "control" : "expr");
32561
+ lastToken = c;
32562
+ } else if (c === ")") {
32563
+ const kind = top.parens.pop() ?? "expr";
32564
+ lastToken = kind === "control" ? "control-paren-close" : ")";
32565
+ } else if (c === "{") {
32566
+ top.depth++;
32567
+ lastToken = c;
32568
+ } else if (c === "}") {
32569
+ if (top.depth > 0) {
32570
+ top.depth--;
32571
+ lastToken = c;
32572
+ } else if (stack.length > 1) {
32573
+ closeRun(i);
32574
+ stack.pop();
32575
+ i++;
32576
+ continue;
32577
+ } else {
32578
+ lastToken = c;
32579
+ }
32580
+ } else if (c !== " " && c !== " " && c !== "\r") {
32581
+ lastToken = c;
32582
+ }
32583
+ if (runStart === null) runStart = i;
32584
+ i++;
32585
+ }
32586
+ closeRun(line.length);
32587
+ }
32588
+ return masks;
32589
+ }
32590
+ var KEYWORDS_BEFORE_REGEX = /* @__PURE__ */ new Set([
32591
+ "return",
32592
+ "typeof",
32593
+ "instanceof",
32594
+ "in",
32595
+ "of",
32596
+ "new",
32597
+ "delete",
32598
+ "void",
32599
+ "throw",
32600
+ "case",
32601
+ "do",
32602
+ "else",
32603
+ "yield",
32604
+ "await"
32605
+ ]);
32606
+ var CONTROL_KEYWORDS = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "catch", "with", "await"]);
32607
+ function tokenCanEndOperand(token) {
32608
+ if (token === null) return false;
32609
+ if (/^[\w$]+$/.test(token)) return !KEYWORDS_BEFORE_REGEX.has(token);
32610
+ return token === ")" || token === "]" || token === "." || token === '"' || token === "'" || token === "`";
32611
+ }
32612
+ function skipRegexLiteral(line, start) {
32613
+ let i = start + 1;
32614
+ let inClass = false;
32615
+ while (i < line.length) {
32616
+ const ch = line[i];
32617
+ if (ch === "\\") {
32618
+ i += 2;
32619
+ continue;
32620
+ }
32621
+ if (inClass) {
32622
+ if (ch === "]") inClass = false;
32623
+ i++;
32624
+ continue;
32625
+ }
32626
+ if (ch === "[") {
32627
+ inClass = true;
32628
+ i++;
32629
+ continue;
32630
+ }
32631
+ if (ch === "/") {
32632
+ i++;
32633
+ break;
32634
+ }
32635
+ if (ch === "\n" || ch === "\r") return line.length;
32636
+ i++;
32374
32637
  }
32375
- if (inSingle || inDouble) return true;
32376
- const window = line.slice(start, start + len);
32377
- return /['"]/.test(window);
32638
+ while (i < line.length && /[a-z]/.test(line[i])) i++;
32639
+ return i;
32378
32640
  }
32379
32641
  function parseMutationReport(text2) {
32380
32642
  const candidates = [];
@@ -32425,7 +32687,7 @@ function normalizeMutantEntry(value) {
32425
32687
  const rec = value;
32426
32688
  const id = typeof rec["id"] === "string" ? rec["id"] : void 0;
32427
32689
  const status = rec["status"];
32428
- if (!id || status !== "killed" && status !== "survived" && status !== "skipped") {
32690
+ if (!id || status !== "killed" && status !== "survived" && status !== "skipped" && status !== "killed-by-hang") {
32429
32691
  return void 0;
32430
32692
  }
32431
32693
  return {
@@ -32481,7 +32743,7 @@ function makeMutationTestTool(director, roster, opts = {}) {
32481
32743
  },
32482
32744
  chaosWorktree: {
32483
32745
  anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
32484
- description: "Worktree override for the chaos agent. Use 'off' when targets are uncommitted \u2014 a worktree from HEAD would not contain them."
32746
+ 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."
32485
32747
  },
32486
32748
  timeoutMs: { type: "number", minimum: 1, description: "Per-task timeout for chaos/strengthen/rerun tasks." },
32487
32749
  reportOnly: {
@@ -32503,8 +32765,16 @@ function makeMutationTestTool(director, roster, opts = {}) {
32503
32765
  error: "No mutable sites found in the given targets (after comment/string filtering)."
32504
32766
  };
32505
32767
  }
32768
+ const chaosBase = roster?.[CHAOS_ROLE];
32769
+ if (!chaosBase) {
32770
+ return {
32771
+ verdict: "inconclusive",
32772
+ passed: false,
32773
+ 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)."
32774
+ };
32775
+ }
32506
32776
  const chaosSubagentId = await director.spawn(
32507
- makeChaosConfig(roster, i.chaosWorktree ?? "off")
32777
+ makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
32508
32778
  );
32509
32779
  const chaosTaskId = await director.assign({
32510
32780
  id: randomUUID14(),
@@ -32522,6 +32792,7 @@ function makeMutationTestTool(director, roster, opts = {}) {
32522
32792
  );
32523
32793
  const attempts = [];
32524
32794
  let current = survivors;
32795
+ let rerunUnknowns = [];
32525
32796
  while (current.length > 0 && attempts.length < maxAttempts && i.repairSubagentId) {
32526
32797
  const attemptNo = attempts.length + 1;
32527
32798
  const strengthenTaskId = await director.assign({
@@ -32543,7 +32814,7 @@ function makeMutationTestTool(director, roster, opts = {}) {
32543
32814
  }
32544
32815
  const survivorPlan = plan.filter((p) => current.some((s) => s.id === p.id));
32545
32816
  const rerunSubagentId = await director.spawn(
32546
- makeChaosConfig(roster, i.chaosWorktree ?? "off")
32817
+ makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
32547
32818
  );
32548
32819
  const rerunTaskId = await director.assign({
32549
32820
  id: randomUUID14(),
@@ -32553,29 +32824,36 @@ function makeMutationTestTool(director, roster, opts = {}) {
32553
32824
  });
32554
32825
  const [rerunResult] = await director.awaitTasks([rerunTaskId]);
32555
32826
  const passN = collectOutcomes(rerunResult, survivorPlan);
32556
- const stillSurviving = passN.filter((m) => m.status === "survived" || m.status === "skipped");
32827
+ const stillSurviving = passN.filter((m) => !isKill(m.status));
32828
+ rerunUnknowns = passN.filter((m) => m.status === "skipped");
32557
32829
  attempts.push({
32558
32830
  attempt: attemptNo,
32559
32831
  survivorsBefore: current,
32560
32832
  strengthenResult: { taskId: strengthenResult.taskId, status: strengthenResult.status },
32561
32833
  rerunResult: { taskId: rerunTaskId, status: rerunResult?.status ?? "unknown" },
32562
32834
  survivorsAfter: stillSurviving,
32563
- suspectedEquivalent: stillSurviving.filter((m) => current.some((c) => c.id === m.id)).map((m) => m.id)
32835
+ suspectedEquivalent: stillSurviving.filter((m) => m.status === "survived" && current.some((c) => c.id === m.id)).map((m) => m.id)
32564
32836
  });
32565
- current = stillSurviving.filter((m) => m.status === "survived");
32566
- if (passN.every((m) => m.status === "skipped")) break;
32837
+ current = stillSurviving;
32567
32838
  }
32568
- const finalSurvivors = current;
32839
+ const finalSurvivors = current.filter((m) => m.status === "survived");
32569
32840
  const verifiedCount = pass1.filter((m) => m.status !== "skipped").length;
32570
32841
  const skippedCount = pass1.filter((m) => m.status === "skipped").length;
32571
- const score = plan.length === 0 ? 0 : pass1.filter((m) => m.status === "killed").length / plan.length;
32572
- const verdict = verifiedCount === 0 ? "inconclusive" : finalSurvivors.length === 0 ? skippedCount > 0 ? "partial" : "pass" : score >= 0.8 ? "partial" : "fail";
32842
+ const rerunUnknownCount = rerunUnknowns.length;
32843
+ const score = plan.length === 0 ? 0 : pass1.filter((m) => isKill(m.status)).length / plan.length;
32844
+ const verdict = verifiedCount === 0 ? "inconclusive" : finalSurvivors.length === 0 ? skippedCount > 0 || rerunUnknownCount > 0 ? "partial" : "pass" : score >= 0.8 ? "partial" : "fail";
32573
32845
  return {
32574
32846
  verdict,
32575
32847
  passed: verdict === "pass",
32576
32848
  mutationScore: Number.parseFloat(score.toFixed(3)),
32577
32849
  planned: plan.length,
32578
- killed: pass1.filter((m) => m.status === "killed").length,
32850
+ killed: pass1.filter((m) => isKill(m.status)).length,
32851
+ // Breakout of `killed`: how many kills were detected by the test
32852
+ // command hanging rather than by a failing assertion. A subset of
32853
+ // `killed`, surfaced so a director can distinguish a hang-heavy
32854
+ // suite (mutants breaking termination, not assertions) from an
32855
+ // assertion-strong one. hangHeavy = killedByHang === killed.
32856
+ killedByHang: pass1.filter((m) => m.status === "killed-by-hang").length,
32579
32857
  survived: pass1.filter((m) => m.status === "survived").length,
32580
32858
  skipped: pass1.filter((m) => m.status === "skipped").length,
32581
32859
  finalSurvivors: finalSurvivors.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
@@ -32583,7 +32861,11 @@ function makeMutationTestTool(director, roster, opts = {}) {
32583
32861
  strengthenAttempts: attempts.length,
32584
32862
  attempts,
32585
32863
  chaosTaskId,
32586
- nextAction: finalSurvivors.length === 0 ? "accept" : attempts.length >= maxAttempts && i.repairSubagentId ? "manual_review_survivors" : "strengthen_tests"
32864
+ // Unverified leftovers from the strengthen loop: surfaced so the
32865
+ // caller can see WHICH mutants lack kill evidence, and counted by
32866
+ // the verdict gate above.
32867
+ unverifiedFromRerun: rerunUnknowns.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
32868
+ nextAction: finalSurvivors.length === 0 && rerunUnknownCount === 0 && skippedCount === 0 ? "accept" : attempts.length >= maxAttempts && i.repairSubagentId ? "manual_review_survivors" : "strengthen_tests"
32587
32869
  };
32588
32870
  }
32589
32871
  };
@@ -32599,7 +32881,7 @@ function normalizeMutationTestInput(input) {
32599
32881
  maxPerFile: typeof raw["maxPerFile"] === "number" ? raw["maxPerFile"] : void 0,
32600
32882
  maxStrengthenAttempts: typeof raw["maxStrengthenAttempts"] === "number" ? raw["maxStrengthenAttempts"] : void 0,
32601
32883
  repairSubagentId: typeof raw["repairSubagentId"] === "string" && raw["repairSubagentId"].trim() ? raw["repairSubagentId"].trim() : void 0,
32602
- chaosWorktree: raw["chaosWorktree"] ?? void 0,
32884
+ chaosWorktree: normalizeWorktreeOverride(raw["chaosWorktree"]),
32603
32885
  timeoutMs: typeof raw["timeoutMs"] === "number" ? raw["timeoutMs"] : void 0,
32604
32886
  reportOnly: raw["reportOnly"] === true
32605
32887
  };
@@ -32621,8 +32903,7 @@ function buildPlan(i, projectRoot) {
32621
32903
  }
32622
32904
  return plan;
32623
32905
  }
32624
- function makeChaosConfig(roster, worktree) {
32625
- const base = roster?.[CHAOS_ROLE] ?? getAgentDefinition(CHAOS_ROLE)?.config ?? { name: "Chaos Monkey", role: CHAOS_ROLE };
32906
+ function makeChaosConfig(base, worktree) {
32626
32907
  return { ...instantiateRosterConfig(CHAOS_ROLE, base), worktree };
32627
32908
  }
32628
32909
  function buildChaosTask(plan, i, pass, priorSurvivors) {
@@ -32638,7 +32919,7 @@ ${priorSurvivors.map((s) => `- ${s.id} (${s.kind} @ ${s.file}:${s.line})`).join(
32638
32919
  "For each mutant, in order:",
32639
32920
  "1. Apply ONLY that mutation at its exact (file, line, column).",
32640
32921
  `2. Run the test command: ${i.testCommand}${i.cwd ? ` (cwd: ${i.cwd})` : ""}`,
32641
- "3. Record killed (tests failed \u2014 quote first failing assertion) or survived (suite green).",
32922
+ "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).",
32642
32923
  "4. Restore the file byte-for-byte before the next mutant.",
32643
32924
  "",
32644
32925
  "Mutants:",
@@ -32650,23 +32931,49 @@ ${priorSurvivors.map((s) => `- ${s.id} (${s.kind} @ ${s.file}:${s.line})`).join(
32650
32931
  ].join("\n");
32651
32932
  }
32652
32933
  function buildStrengthenTask(survivors, i, attempt) {
32934
+ const confirmed = survivors.filter((s) => s.status === "survived");
32935
+ const unverified = survivors.filter((s) => s.status === "skipped");
32936
+ const row = (s) => `- ${s.id} | ${s.file}:${s.line} | ${s.kind}${s.evidence ? ` | ${s.evidence}` : ""}`;
32653
32937
  return [
32654
- `Strengthen the tests so these SURVIVING mutants die (attempt ${attempt}).`,
32655
- "",
32656
- "Each survivor below was a deliberate sabotage of production code that the current suite did NOT catch:",
32657
- ...survivors.map((s) => `- ${s.id} | ${s.file}:${s.line} | ${s.kind}${s.evidence ? ` | ${s.evidence}` : ""}`),
32938
+ `Strengthen the tests so the mutants below die (attempt ${attempt}).`,
32658
32939
  "",
32659
- `Test command that must fail under each mutant: ${i.testCommand}`,
32940
+ ...confirmed.length > 0 ? [
32941
+ "CONFIRMED SURVIVORS \u2014 each was a deliberate sabotage of production code that the current suite did NOT catch:",
32942
+ ...confirmed.map(row),
32943
+ ""
32944
+ ] : [],
32945
+ ...unverified.length > 0 ? [
32946
+ "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.",
32947
+ ...unverified.map(row),
32948
+ ""
32949
+ ] : [],
32950
+ `Test command that must fail under each CONFIRMED mutant: ${i.testCommand}`,
32660
32951
  "",
32661
- "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."
32952
+ "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."
32662
32953
  ].join("\n");
32663
32954
  }
32664
32955
  function collectOutcomes(result, plan) {
32665
32956
  const fromText = parseTextOutcomes(result);
32666
32957
  if (fromText.length > 0) {
32667
- const planned = new Set(plan.map((p) => p.id));
32668
- const matched = fromText.filter((m) => planned.has(m.id));
32669
- if (matched.length > 0) return matched;
32958
+ const remaining = [...plan];
32959
+ const matched = [];
32960
+ for (const m of fromText) {
32961
+ const idx = remaining.findIndex((p) => p.id === m.id);
32962
+ if (idx === -1) continue;
32963
+ remaining.splice(idx, 1);
32964
+ matched.push(m);
32965
+ }
32966
+ if (matched.length > 0) {
32967
+ const missing = remaining.map((p) => ({
32968
+ id: p.id,
32969
+ file: p.file,
32970
+ line: p.line,
32971
+ kind: p.kind,
32972
+ status: "skipped",
32973
+ evidence: "not reported by chaos task"
32974
+ }));
32975
+ return [...matched, ...missing];
32976
+ }
32670
32977
  }
32671
32978
  return plan.map((p) => ({
32672
32979
  id: p.id,
@@ -32677,6 +32984,9 @@ function collectOutcomes(result, plan) {
32677
32984
  evidence: result ? `chaos task ended ${result.status}` : "chaos task produced no result"
32678
32985
  }));
32679
32986
  }
32987
+ function isKill(status) {
32988
+ return status === "killed" || status === "killed-by-hang";
32989
+ }
32680
32990
  function parseTextOutcomes(result) {
32681
32991
  const text2 = typeof result?.result === "string" ? result.result : void 0;
32682
32992
  if (!text2) return [];
@@ -39402,11 +39712,13 @@ var CHAOS_MONKEY_AGENT = {
39402
39712
  tools: [...TOOLS.build],
39403
39713
  skillNames: ["testing", "typescript-strict"],
39404
39714
  spawnBudgetExempt: true,
39405
- // Follow fleet worktree policy (NOT 'required'): mutation targets are
39406
- // often freshly written and uncommitted — a worktree spawned from HEAD
39407
- // would not contain them and every mutant would drift. Callers pass
39408
- // `worktree: 'off'` in the mutation_test input for uncommitted targets.
39409
- worktree: "auto",
39715
+ // Run in the live checkout: mutation targets are usually freshly
39716
+ // written and uncommitted — a worktree spawned from HEAD would not
39717
+ // contain them and every mutant would drift. The mutation_test tool
39718
+ // honors this value as its default; callers can still override per
39719
+ // call via its `chaosWorktree` input when targets are committed and
39720
+ // isolation is wanted.
39721
+ worktree: "off",
39410
39722
  // Report travels via submit_result + final text, not the leader's stream.
39411
39723
  textStream: "silent",
39412
39724
  toolStream: "silent"
@@ -40665,6 +40977,7 @@ function worktreeOwnerLabel(task, config) {
40665
40977
  }
40666
40978
 
40667
40979
  // src/coordination/director.ts
40980
+ var BUSY_REARM_FLOOR_MS = 1e3;
40668
40981
  var Director = class _Director {
40669
40982
  /* eslint-disable-next-line @typescript-eslint/no-unused-vars — just a cast helper */
40670
40983
  static _asManifestEntry(v) {
@@ -40730,6 +41043,13 @@ var Director = class _Director {
40730
41043
  subagentIdleTimeoutMs;
40731
41044
  retireSubagentOnTaskComplete;
40732
41045
  subagentIdleTimers = /* @__PURE__ */ new Map();
41046
+ /**
41047
+ * Effective idle window per subagent (spawn-time `idleTimeoutMs` override
41048
+ * or the Director-wide default; undefined = no window). Internal-task
41049
+ * completion re-arms with THIS value, not the Director-wide default, so
41050
+ * a subagent-configured window survives its first internal probe.
41051
+ */
41052
+ subagentIdleDelayMs = /* @__PURE__ */ new Map();
40733
41053
  sharedScratchpadPath;
40734
41054
  maxSpawns;
40735
41055
  maxSpawnDepth;
@@ -40886,7 +41206,13 @@ var Director = class _Director {
40886
41206
  handleTaskCompleted(payload) {
40887
41207
  const r = payload.result;
40888
41208
  const settled = this.tasks.settle(r);
40889
- if (settled.internal) return;
41209
+ if (settled.internal) {
41210
+ this.armSubagentIdleRetirement(
41211
+ r.subagentId,
41212
+ this.subagentIdleDelayMs.get(r.subagentId) ?? this.subagentIdleTimeoutMs
41213
+ );
41214
+ return;
41215
+ }
40890
41216
  const title = this.tasks.descriptionFor(r.taskId, payload.task.description ?? r.taskId);
40891
41217
  if (!settled.consumedInBand && this.taskResultNotifier) {
40892
41218
  const resultText = typeof r.result === "string" ? r.result : r.result !== void 0 ? safeStringify(r.result) : void 0;
@@ -40951,7 +41277,7 @@ var Director = class _Director {
40951
41277
  }
40952
41278
  this.armSubagentIdleRetirement(
40953
41279
  r.subagentId,
40954
- this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleTimeoutMs
41280
+ this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleDelayMs.get(r.subagentId) ?? this.subagentIdleTimeoutMs
40955
41281
  );
40956
41282
  }
40957
41283
  extensionsFor(subagentId) {
@@ -41037,6 +41363,7 @@ var Director = class _Director {
41037
41363
  this.resolveSpawnModel(config);
41038
41364
  const subagentId = await spawn6(this, config, priceLookup);
41039
41365
  const perSubagentIdleMs = typeof config.idleTimeoutMs === "number" && Number.isFinite(config.idleTimeoutMs) && config.idleTimeoutMs >= 0 ? config.idleTimeoutMs : this.subagentIdleTimeoutMs;
41366
+ this.subagentIdleDelayMs.set(subagentId, perSubagentIdleMs);
41040
41367
  this.armSubagentIdleRetirement(subagentId, perSubagentIdleMs);
41041
41368
  return subagentId;
41042
41369
  }
@@ -41090,6 +41417,7 @@ var Director = class _Director {
41090
41417
  this.budgetPolicy.dispose();
41091
41418
  for (const timer of this.subagentIdleTimers.values()) clearTimeout(timer);
41092
41419
  this.subagentIdleTimers.clear();
41420
+ this.subagentIdleDelayMs.clear();
41093
41421
  await this.coordinator.stopAll();
41094
41422
  this.tasks.resolveWaitersOnShutdown();
41095
41423
  for (const b of this.subagentBridges.values()) {
@@ -41148,6 +41476,7 @@ var Director = class _Director {
41148
41476
  }
41149
41477
  async remove(subagentId) {
41150
41478
  this.clearSubagentIdleRetirement(subagentId);
41479
+ this.subagentIdleDelayMs.delete(subagentId);
41151
41480
  void this.appendSessionEvent({
41152
41481
  type: "agent_stopped",
41153
41482
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -41200,9 +41529,13 @@ var Director = class _Director {
41200
41529
  const timer = setTimeout(() => {
41201
41530
  this.subagentIdleTimers.delete(subagentId);
41202
41531
  const entry = this.coordinator.getStatus().subagents.find((a) => a.id === subagentId);
41203
- if (entry?.status !== "idle") return;
41532
+ if (entry === void 0) return;
41533
+ if (entry.status !== "idle") {
41534
+ this.armSubagentIdleRetirement(subagentId, Math.max(delayMs, BUSY_REARM_FLOOR_MS));
41535
+ return;
41536
+ }
41204
41537
  if (this.coordinator.listPendingTasks().some((task) => task.subagentId === subagentId)) {
41205
- this.armSubagentIdleRetirement(subagentId, this.subagentIdleTimeoutMs);
41538
+ this.armSubagentIdleRetirement(subagentId, Math.max(delayMs, BUSY_REARM_FLOOR_MS));
41206
41539
  return;
41207
41540
  }
41208
41541
  void this.remove(subagentId).catch(
@@ -44858,16 +45191,14 @@ var ExploreCompanion = class {
44858
45191
  this.running = true;
44859
45192
  this.unsubscribers.push(
44860
45193
  this.opts.events.on("tool.executed", (e) => {
44861
- const lsid = this.resolveLeaderSessionId();
44862
- if (lsid && e.sessionId && e.sessionId !== lsid) return;
45194
+ if (e.sessionId !== this.resolveLeaderSessionId()) return;
44863
45195
  this.trackToolExecuted(e);
44864
45196
  })
44865
45197
  );
44866
45198
  if (this.cfg.signals.todoInProgress && this.resolveLeaderAgentId()) {
44867
45199
  this.unsubscribers.push(
44868
45200
  this.opts.events.on("session.agents_updated", (e) => {
44869
- const lsid = this.resolveLeaderSessionId();
44870
- if (lsid && e.sessionId && e.sessionId !== lsid) return;
45201
+ if (e.sessionId !== this.resolveLeaderSessionId()) return;
44871
45202
  this.trackAgentTodos(e.agents);
44872
45203
  })
44873
45204
  );
@@ -44875,8 +45206,7 @@ var ExploreCompanion = class {
44875
45206
  if (this.cfg.signals.errorSymbol) {
44876
45207
  this.unsubscribers.push(
44877
45208
  this.opts.events.on("error", (e) => {
44878
- const lsid = this.resolveLeaderSessionId();
44879
- if (lsid && e.sessionId && e.sessionId !== lsid) return;
45209
+ if (e.sessionId !== this.resolveLeaderSessionId()) return;
44880
45210
  this.trackError(e.err);
44881
45211
  })
44882
45212
  );
@@ -44989,9 +45319,18 @@ var ExploreCompanion = class {
44989
45319
  limit: 20
44990
45320
  });
44991
45321
  const lsid = this.resolveLeaderSessionId();
45322
+ const selfRecipients = new Set(
45323
+ [
45324
+ this.cfg.companionAgentId,
45325
+ mailboxIdentityBase(this.cfg.companionAgentId),
45326
+ ...lsid != null ? [sessionRecipient(lsid)] : []
45327
+ ].map((r) => r.toLowerCase())
45328
+ );
44992
45329
  for (const msg of messages) {
44993
45330
  if (msg.type !== "ask" && msg.type !== "assign") continue;
44994
- const fromLeader = isMailboxLeader(msg.from) || lsid != null && msg.senderSessionId === lsid;
45331
+ const to = msg.to.trim().toLowerCase();
45332
+ if (to !== "*" && !selfRecipients.has(to)) continue;
45333
+ const fromLeader = msg.senderSessionId === void 0 && isMailboxLeader(msg.from) || lsid != null && msg.senderSessionId === lsid;
44995
45334
  if (!fromLeader) continue;
44996
45335
  this.engage({
44997
45336
  id: randomUUID24(),
@@ -54945,6 +55284,7 @@ function requestLimitExtension(opts) {
54945
55284
  // src/prompts/prompt-journal.ts
54946
55285
  import * as fs31 from "node:fs/promises";
54947
55286
  import * as path77 from "node:path";
55287
+ var defaultScrubber2 = new DefaultSecretScrubber();
54948
55288
  var PROMPT_JOURNAL_RAW_MARKER = "promptJournal.raw";
54949
55289
  async function ensureGitignore(projectRoot) {
54950
55290
  const gitignorePath = path77.join(projectRoot, ".gitignore");
@@ -54973,7 +55313,9 @@ async function recordPromptJournalEntry(opts) {
54973
55313
  const monthStr = dateStr.slice(0, 7);
54974
55314
  const sessionId = opts.sessionId && opts.sessionId.trim() ? opts.sessionId.trim() : "general";
54975
55315
  const id = `pmt_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
54976
- const content = opts.content ?? "";
55316
+ const content = defaultScrubber2.scrub(opts.content ?? "");
55317
+ const rawContent = typeof opts.rawContent === "string" && opts.rawContent.length > 0 ? defaultScrubber2.scrub(opts.rawContent) : opts.rawContent;
55318
+ const decisionReason = typeof opts.decisionReason === "string" && opts.decisionReason.length > 0 ? defaultScrubber2.scrub(opts.decisionReason) : opts.decisionReason;
54977
55319
  const lines = content.split("\n");
54978
55320
  const characterCount = content.length;
54979
55321
  const lineCount2 = lines.length;
@@ -54986,7 +55328,7 @@ async function recordPromptJournalEntry(opts) {
54986
55328
  role: opts.role ?? (opts.category === "system_prompt" ? "system" : "user"),
54987
55329
  category: opts.category,
54988
55330
  content,
54989
- rawContent: opts.rawContent,
55331
+ rawContent,
54990
55332
  metadata: {
54991
55333
  model: opts.model,
54992
55334
  provider: opts.provider,
@@ -54997,7 +55339,7 @@ async function recordPromptJournalEntry(opts) {
54997
55339
  activeTools: opts.activeTools,
54998
55340
  contextFiles: opts.contextFiles,
54999
55341
  durationMs: opts.durationMs,
55000
- decisionReason: opts.decisionReason,
55342
+ decisionReason,
55001
55343
  tags: opts.tags
55002
55344
  }
55003
55345
  };
@@ -56560,10 +56902,7 @@ function createAgentToolHandler(a) {
56560
56902
  } catch {
56561
56903
  }
56562
56904
  }
56563
- if (decision === "yes") {
56564
- const p = a.permission;
56565
- p.allowOnce?.({ tool: tool.name, pattern: result.suggestedPattern });
56566
- } else if (decision === "no") {
56905
+ if (decision === "no") {
56567
56906
  const p = a.permission;
56568
56907
  p.denyOnce?.({ tool: tool.name, pattern: result.suggestedPattern });
56569
56908
  }
@@ -64327,6 +64666,10 @@ var DefaultSkillLoader = class {
64327
64666
  );
64328
64667
  for (const e of entries) {
64329
64668
  if (!await entryIsDirectory(dir, e)) continue;
64669
+ if (!isValidSkillNameFormat(e.name)) {
64670
+ this.skipped.push({ dir, entry: e.name, reason: "invalid-name-format" });
64671
+ continue;
64672
+ }
64330
64673
  const skillFile = path80.join(dir, e.name, "SKILL.md");
64331
64674
  let raw;
64332
64675
  try {
@@ -65731,7 +66074,7 @@ var SENSITIVE_FLAG_PATTERNS = [
65731
66074
  // redaction function (false positive = cosmetic noise; false negative = leak).
65732
66075
  /(?<![-\w])-(?:password|p|a)(?:[=\s]+)?[^\s,-]+/gi,
65733
66076
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
65734
- /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
66077
+ /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD|PASSPHRASE)\s*[=:]\s*[^\s,]+/gi,
65735
66078
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
65736
66079
  // when preceded by a flag name (e.g. --github-token=EyJ...). Global flag so
65737
66080
  // every such flag in the command line is redacted, not just the first.
@@ -66106,7 +66449,7 @@ var ToolExecutor = class _ToolExecutor {
66106
66449
  return { result, tool, durationMs: Date.now() - start };
66107
66450
  }
66108
66451
  if (effectivePermission === "confirm") {
66109
- const suggestedPattern = boundary.decision === "confirm" ? `kanban-boundary:${boundary.path ?? tool.name}` : subjectForToolInput(tool.name, use.input, tool.subjectKey) ?? tool.name;
66452
+ const suggestedPattern = boundary.decision === "confirm" ? `kanban-boundary:${boundary.path ?? tool.name}` : subjectForToolInput(tool.name, use.input, tool.subjectKey, tool.subjectFields) ?? tool.name;
66110
66453
  if (this.opts.confirmAwaiter) {
66111
66454
  const awaiter = this.opts.confirmAwaiter;
66112
66455
  const choice = await new Promise(
@@ -68643,7 +68986,9 @@ function hasRecursiveForceDelete(command, projectRoot) {
68643
68986
  if (token === "rd" || token === "rmdir") {
68644
68987
  const args = commandSegment(tokens, i + 1).map((arg) => arg.toLowerCase());
68645
68988
  if (args.includes("/s")) {
68646
- const targets = args.filter((arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg));
68989
+ const targets = args.filter(
68990
+ (arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg)
68991
+ );
68647
68992
  if (targets.length === 0) return true;
68648
68993
  if (targets.some(isCatastrophicDeleteTarget)) return true;
68649
68994
  if (targets.some((target) => !pathLooksInsideProject(target, projectRoot))) return true;
@@ -68712,7 +69057,8 @@ function hasFindExec(command) {
68712
69057
  function isCatastrophicDeleteTarget(rawTarget) {
68713
69058
  const t2 = rawTarget.replace(/^['"]|['"]$/g, "").trim();
68714
69059
  if (!t2) return false;
68715
- if (t2 === "*" || t2 === "." || t2 === "./" || t2 === ".\\" || t2 === "./*" || t2 === ".\\*") return true;
69060
+ if (t2 === "*" || t2 === "." || t2 === "./" || t2 === ".\\" || t2 === "./*" || t2 === ".\\*")
69061
+ return true;
68716
69062
  const s = t2.replace(/[\\/]\*+$/, "").replace(/[\\/]+$/, "");
68717
69063
  if (s === "") return true;
68718
69064
  if (s === "~" || /^\$HOME$/i.test(s) || /^%USERPROFILE%$/i.test(s)) return true;
@@ -68752,12 +69098,16 @@ function hasCatastrophicDelete(command) {
68752
69098
  const args = tokens.slice(i + 1);
68753
69099
  const recursive = args.some((arg) => arg.toLowerCase() === "/s");
68754
69100
  if (!recursive) continue;
68755
- const targets = args.filter((arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg));
69101
+ const targets = args.filter(
69102
+ (arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg)
69103
+ );
68756
69104
  if (targets.some(isCatastrophicDeleteTarget)) return true;
68757
69105
  }
68758
69106
  if (token === "del" || token === "erase") {
68759
69107
  const args = tokens.slice(i + 1);
68760
- const targets = args.filter((arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg));
69108
+ const targets = args.filter(
69109
+ (arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg)
69110
+ );
68761
69111
  if (targets.some(isCatastrophicDeleteTarget)) return true;
68762
69112
  }
68763
69113
  }
@@ -68818,6 +69168,47 @@ function isClearlyDestructiveBashCommand(command, projectRoot) {
68818
69168
  if (HIGH_IMPACT_PATTERNS.some((pattern) => pattern.test(trimmed))) return true;
68819
69169
  return false;
68820
69170
  }
69171
+ var WELL_KNOWN_CREDENTIAL_ENV_VARS = /* @__PURE__ */ new Set([
69172
+ "ANTHROPIC_API_KEY",
69173
+ "ANTHROPIC_AUTH_TOKEN",
69174
+ "OPENAI_API_KEY",
69175
+ "AZURE_OPENAI_API_KEY",
69176
+ "GEMINI_API_KEY",
69177
+ "GOOGLE_API_KEY",
69178
+ "GOOGLE_APPLICATION_CREDENTIALS",
69179
+ "GOOGLE_GENERATIVE_AI_API_KEY",
69180
+ "GROQ_API_KEY",
69181
+ "MISTRAL_API_KEY",
69182
+ "COHERE_API_KEY",
69183
+ "DEEPSEEK_API_KEY",
69184
+ "XAI_API_KEY",
69185
+ "OPENROUTER_API_KEY",
69186
+ "PERPLEXITY_API_KEY",
69187
+ "TOGETHER_API_KEY",
69188
+ "FIREWORKS_API_KEY",
69189
+ "HUGGINGFACE_API_KEY",
69190
+ "HF_TOKEN",
69191
+ "GITHUB_TOKEN",
69192
+ "GH_TOKEN",
69193
+ "NPM_TOKEN",
69194
+ "AWS_ACCESS_KEY_ID",
69195
+ "AWS_SECRET_ACCESS_KEY",
69196
+ "AWS_SESSION_TOKEN",
69197
+ "AZURE_CLIENT_SECRET",
69198
+ "GITLAB_TOKEN",
69199
+ "SLACK_TOKEN",
69200
+ "STRIPE_SECRET_KEY",
69201
+ "TELEGRAM_BOT_TOKEN",
69202
+ "WRONGSTACK_VAULT_PASSPHRASE"
69203
+ ]);
69204
+ function attachesWellKnownCredential(input) {
69205
+ if (!input || typeof input !== "object") return false;
69206
+ const envVars = input["envVars"];
69207
+ if (!Array.isArray(envVars)) return false;
69208
+ return envVars.some(
69209
+ (name) => typeof name === "string" && WELL_KNOWN_CREDENTIAL_ENV_VARS.has(name.toUpperCase())
69210
+ );
69211
+ }
68821
69212
 
68822
69213
  // src/security/permission-helpers.ts
68823
69214
  function matchesTrust(patterns, subject2) {
@@ -68834,7 +69225,7 @@ function hasShellSubject(tool) {
68834
69225
  ]);
68835
69226
  }
68836
69227
  function alwaysAllowUnavailableReason(tool, input) {
68837
- const subject2 = subjectForToolInput(tool.name, input, tool.subjectKey);
69228
+ const subject2 = subjectForToolInput(tool.name, input, tool.subjectKey, tool.subjectFields);
68838
69229
  if (subject2 !== void 0) return void 0;
68839
69230
  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.`;
68840
69231
  }
@@ -68894,8 +69285,18 @@ var AGENT_STATE_SENSITIVE_BASENAMES = /^(?:config\.json|config\.local\.json|trus
68894
69285
  function unescapeGlobSubject(value) {
68895
69286
  return value.replace(/\\([*?[\]])/g, "$1");
68896
69287
  }
69288
+ function stripAdsSuffix(forwardSlashPath) {
69289
+ const cut = forwardSlashPath.lastIndexOf("/");
69290
+ const dir = cut === -1 ? "" : forwardSlashPath.slice(0, cut + 1);
69291
+ const base = cut === -1 ? forwardSlashPath : forwardSlashPath.slice(cut + 1);
69292
+ const colon = base.indexOf(":");
69293
+ if (colon === -1 || cut === -1 && colon === 1 && base.length <= 2) return forwardSlashPath;
69294
+ return dir + base.slice(0, colon);
69295
+ }
68897
69296
  function normalizeForCompare(value) {
68898
- const forward = unescapeGlobSubject(value).replace(/\\/g, "/").replace(/\/+$/, "");
69297
+ const forward = stripAdsSuffix(
69298
+ unescapeGlobSubject(value).replace(/\\/g, "/").replace(/\/+$/, "")
69299
+ );
68899
69300
  return process.platform === "win32" ? forward.toLowerCase() : forward;
68900
69301
  }
68901
69302
  function realpathOfNearestExisting(p) {
@@ -68932,7 +69333,7 @@ function isProtectedAgentStatePath(absPath) {
68932
69333
  return AGENT_STATE_SENSITIVE_BASENAMES.test(path90.basename(normalizeForCompare(absPath)));
68933
69334
  }
68934
69335
  function pathLooksSensitive(rawPath) {
68935
- const normalized = stripShellQuotes(rawPath).replace(/\\/g, "/");
69336
+ const normalized = stripAdsSuffix(stripShellQuotes(rawPath).replace(/\\/g, "/"));
68936
69337
  if (SENSITIVE_READ_PATHS.some((pattern) => pattern.test(normalized))) return true;
68937
69338
  return isProtectedAgentStatePath(normalized);
68938
69339
  }
@@ -68956,10 +69357,24 @@ function shellCommandReadsSensitivePath(command) {
68956
69357
  }
68957
69358
  return false;
68958
69359
  }
69360
+ function isSensitiveReadCall(tool, input) {
69361
+ const isReadTool = hasCapability(tool, ToolCapabilities.FS_READ) || tool.name === "read" || tool.name === "grep" || tool.name === "glob" || tool.name === "tree";
69362
+ if (isReadTool && inputPathLooksSensitive(input)) return true;
69363
+ const hasShellCap = hasCapability(tool, [
69364
+ ToolCapabilities.SHELL_ARBITRARY,
69365
+ ToolCapabilities.SHELL_RESTRICTED,
69366
+ ToolCapabilities.SHELL_EXEC
69367
+ ]);
69368
+ if (!hasShellCap && tool.name !== "bash" && tool.name !== "shell" && tool.name !== "exec") {
69369
+ return false;
69370
+ }
69371
+ const command = shellCommandLineFromInput(input);
69372
+ return command ? shellCommandReadsSensitivePath(command) : false;
69373
+ }
68959
69374
 
68960
69375
  // src/security/permission-explain.ts
68961
69376
  function explainPermissionTrace(state, tool, input, ctx) {
68962
- const subject2 = subjectForToolInput(tool.name, input, tool.subjectKey);
69377
+ const subject2 = subjectForToolInput(tool.name, input, tool.subjectKey, tool.subjectFields);
68963
69378
  const steps = [];
68964
69379
  let winnerIndex = -1;
68965
69380
  const add = (rule, matched, decision, source, detail) => {
@@ -69180,13 +69595,7 @@ function explainPermissionTrace(state, tool, input, ctx) {
69180
69595
  }
69181
69596
  };
69182
69597
  }
69183
- add(
69184
- "yolo",
69185
- true,
69186
- "auto",
69187
- "yolo",
69188
- "YOLO mode is active \u2014 auto-approving every non-denied call"
69189
- );
69598
+ add("yolo", true, "auto", "yolo", "YOLO mode is active \u2014 auto-approving every non-denied call");
69190
69599
  winnerIndex = steps.length - 1;
69191
69600
  return {
69192
69601
  toolName: tool.name,
@@ -69458,7 +69867,14 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
69458
69867
  static isMcpTool(name) {
69459
69868
  return name.startsWith("mcp__");
69460
69869
  }
69461
- async evaluate(tool) {
69870
+ async evaluate(tool, input) {
69871
+ if (input !== void 0 && isSensitiveReadCall(tool, input)) {
69872
+ return {
69873
+ permission: "deny",
69874
+ source: "subagent_guard",
69875
+ reason: "subagents may not read credential-bearing paths \u2014 the leader must perform this read so the user can approve it"
69876
+ };
69877
+ }
69462
69878
  const caps = tool.capabilities ?? [];
69463
69879
  const hasAllowedCap = caps.some((c) => this.allowedCapabilities.includes(c));
69464
69880
  const isMcp = _AutoApprovePermissionPolicy.isMcpTool(tool.name);
@@ -69485,8 +69901,8 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
69485
69901
  }
69486
69902
  allowOnce() {
69487
69903
  }
69488
- async explain(tool) {
69489
- const decision = await this.evaluate(tool);
69904
+ async explain(tool, input) {
69905
+ const decision = await this.evaluate(tool, input);
69490
69906
  return {
69491
69907
  toolName: tool.name,
69492
69908
  subject: null,
@@ -69536,6 +69952,17 @@ function fsWriteTargetPaths(input) {
69536
69952
  }
69537
69953
  return out;
69538
69954
  }
69955
+ function mergeTrustEntries(exact, wildcard) {
69956
+ if (!exact) return wildcard;
69957
+ if (!wildcard) return exact;
69958
+ const deny = [...wildcard.deny ?? [], ...exact.deny ?? []];
69959
+ const merged = {
69960
+ ...wildcard,
69961
+ ...exact
69962
+ };
69963
+ if (deny.length > 0) merged.deny = [...new Set(deny)];
69964
+ return merged;
69965
+ }
69539
69966
  var DefaultPermissionPolicy = class {
69540
69967
  policy = {};
69541
69968
  loaded = false;
@@ -69574,6 +70001,7 @@ var DefaultPermissionPolicy = class {
69574
70001
  yoloBlockedAsDestructive(tool, input, ctx) {
69575
70002
  if (!this.yolo || this.yoloDestructive) return false;
69576
70003
  if (this.hasAgentStateWriteTarget(tool, input, ctx)) return true;
70004
+ if (attachesWellKnownCredential(input)) return true;
69577
70005
  const isShellSurface = tool.name === "bash" || tool.name === "exec" || (tool.capabilities ?? []).includes("shell.arbitrary");
69578
70006
  if (!isShellSurface) return false;
69579
70007
  const command = getInputString(input, "command") ?? shellCommandLineFromInput(input);
@@ -69667,8 +70095,8 @@ var DefaultPermissionPolicy = class {
69667
70095
  };
69668
70096
  }
69669
70097
  const namespaceEntry = this.findNamespaceEntry(tool.name);
69670
- const entry = this.policy[tool.name] ?? namespaceEntry;
69671
- const subject2 = subjectForToolInput(tool.name, input, tool.subjectKey);
70098
+ const entry = mergeTrustEntries(this.policy[tool.name], namespaceEntry);
70099
+ const subject2 = subjectForToolInput(tool.name, input, tool.subjectKey, tool.subjectFields);
69672
70100
  const cacheKey = `${tool.name}::${subject2 ?? tool.name}`;
69673
70101
  const evalKey = `${cacheKey}::${permissionFingerprint(tool)}`;
69674
70102
  if (tool.name !== "write" && !this.hasAgentStateWriteTarget(tool, input, ctx)) {
@@ -69685,15 +70113,6 @@ var DefaultPermissionPolicy = class {
69685
70113
  this._evalCache.set(evalKey, decision);
69686
70114
  return decision;
69687
70115
  }
69688
- if (this.sessionAllowed.has(cacheKey)) {
69689
- this.sessionAllowed.delete(cacheKey);
69690
- const decision = {
69691
- permission: "auto",
69692
- source: "trust",
69693
- reason: "session one-shot allow (user pressed yes)"
69694
- };
69695
- return decision;
69696
- }
69697
70116
  if (entry?.deny && subject2 && matchesTrust(entry.deny, subject2)) {
69698
70117
  this._logDeny(tool.name, subject2, "matched deny pattern");
69699
70118
  const decision = {
@@ -69704,6 +70123,15 @@ var DefaultPermissionPolicy = class {
69704
70123
  this._evalCache.set(evalKey, decision);
69705
70124
  return decision;
69706
70125
  }
70126
+ if (this.sessionAllowed.has(cacheKey)) {
70127
+ this.sessionAllowed.delete(cacheKey);
70128
+ const decision = {
70129
+ permission: "auto",
70130
+ source: "trust",
70131
+ reason: "session one-shot allow (user pressed yes)"
70132
+ };
70133
+ return decision;
70134
+ }
69707
70135
  if (tool.permission === "deny") {
69708
70136
  this._logDeny(tool.name, subject2, "tool default deny");
69709
70137
  const decision = {
@@ -69714,6 +70142,7 @@ var DefaultPermissionPolicy = class {
69714
70142
  this._evalCache.set(evalKey, decision);
69715
70143
  return decision;
69716
70144
  }
70145
+ const denyUnevaluated = Boolean(entry?.deny?.length) && subject2 === void 0;
69717
70146
  const allowMatches = hasShellSubject(tool) ? matchesCommandTrust : matchesTrust;
69718
70147
  if (entry?.allow && subject2 && allowMatches(entry.allow, subject2)) {
69719
70148
  const decision = {
@@ -69724,7 +70153,7 @@ var DefaultPermissionPolicy = class {
69724
70153
  this._evalCache.set(evalKey, decision);
69725
70154
  return decision;
69726
70155
  }
69727
- if (entry?.auto) {
70156
+ if (entry?.auto && !denyUnevaluated) {
69728
70157
  const decision = { permission: "auto", source: "trust" };
69729
70158
  this._evalCache.set(evalKey, decision);
69730
70159
  return decision;
@@ -69824,19 +70253,10 @@ var DefaultPermissionPolicy = class {
69824
70253
  }
69825
70254
  return { permission: "confirm", source: "default" };
69826
70255
  }
70256
+ // Delegates to the shared helper so the subagent policy applies the exact
70257
+ // same rule — see `isSensitiveReadCall` in ./permission-helpers.ts.
69827
70258
  isSensitiveReadCall(tool, input) {
69828
- const isReadTool = hasCapability(tool, ToolCapabilities.FS_READ) || tool.name === "read" || tool.name === "grep" || tool.name === "glob" || tool.name === "tree";
69829
- if (isReadTool && inputPathLooksSensitive(input)) return true;
69830
- const hasShellCap = hasCapability(tool, [
69831
- ToolCapabilities.SHELL_ARBITRARY,
69832
- ToolCapabilities.SHELL_RESTRICTED,
69833
- ToolCapabilities.SHELL_EXEC
69834
- ]);
69835
- if (!hasShellCap && tool.name !== "bash" && tool.name !== "shell" && tool.name !== "exec") {
69836
- return false;
69837
- }
69838
- const command = shellCommandLineFromInput(input);
69839
- return command ? shellCommandReadsSensitivePath(command) : false;
70259
+ return isSensitiveReadCall(tool, input);
69840
70260
  }
69841
70261
  async trust(rule) {
69842
70262
  if (!this.loaded) await this.reload();
@@ -78870,6 +79290,15 @@ function deriveSessionStatus(agents) {
78870
79290
  (a) => a.status === "running" || a.status === "streaming" || a.status === "waiting_user"
78871
79291
  ) ? "active" : "idle";
78872
79292
  }
79293
+ function downgradeStaleAgentStatuses(agents, nowMs) {
79294
+ const cutoff = nowMs - HQ_STALE_SNAPSHOT_WINDOW_MS;
79295
+ return agents.map((agent) => {
79296
+ if (agent.status !== "running" && agent.status !== "streaming") return agent;
79297
+ const lastActivityAt = Date.parse(agent.lastActivityAt);
79298
+ if (!Number.isFinite(lastActivityAt) || lastActivityAt >= cutoff) return agent;
79299
+ return { ...agent, status: "idle" };
79300
+ });
79301
+ }
78873
79302
  function startSessionTelemetryBridge(opts) {
78874
79303
  const now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
78875
79304
  const publisher = opts.publisher;
@@ -78890,6 +79319,7 @@ function startSessionTelemetryBridge(opts) {
78890
79319
  let lastPublishedAtMs = Date.now();
78891
79320
  let disposed = false;
78892
79321
  function buildSnapshot() {
79322
+ const effectiveAgents = downgradeStaleAgentStatuses(agents, Date.parse(now()));
78893
79323
  return {
78894
79324
  sessionId: opts.sessionId,
78895
79325
  clientKind: identity2.kind,
@@ -78897,11 +79327,11 @@ function startSessionTelemetryBridge(opts) {
78897
79327
  projectId: project2.projectId,
78898
79328
  projectName: opts.projectName ?? project2.projectName,
78899
79329
  projectRoot: opts.projectRoot,
78900
- status: deriveSessionStatus(agents),
79330
+ status: deriveSessionStatus(effectiveAgents),
78901
79331
  startedAt,
78902
79332
  lastActivityAt,
78903
- agentCount: agents.length,
78904
- agents,
79333
+ agentCount: effectiveAgents.length,
79334
+ agents: effectiveAgents,
78905
79335
  ...identity2.hostname !== void 0 ? { hostname: identity2.hostname } : {},
78906
79336
  ...identity2.pid !== void 0 ? { pid: identity2.pid } : {},
78907
79337
  ...opts.gitBranch !== void 0 ? { gitBranch: opts.gitBranch } : {}
@@ -82282,18 +82712,88 @@ var DefaultPluginAPI = class {
82282
82712
  list: () => tr.list()
82283
82713
  };
82284
82714
  const pr = init.providerRegistry;
82715
+ const providerTypesIOwn = /* @__PURE__ */ new Set();
82716
+ const assertCanMutateProvider = (type, op) => {
82717
+ if (isOfficial) return;
82718
+ if (providerTypesIOwn.has(type)) return;
82719
+ if (!pr.has(type)) return;
82720
+ throw new Error(
82721
+ `Plugin "${owner}" may not ${op} provider "${type}" \u2014 it was not registered by this plugin. Replacing an existing provider would route prompts and credentials through plugin code.`
82722
+ );
82723
+ };
82285
82724
  this.providers = {
82286
- register: (f) => pr.register(f),
82287
- unregister: (type) => pr.unregister(type),
82725
+ register: (f) => {
82726
+ assertCanMutateProvider(f.type, "replace");
82727
+ pr.register(f);
82728
+ providerTypesIOwn.add(f.type);
82729
+ },
82730
+ unregister: (type) => {
82731
+ assertCanMutateProvider(type, "unregister");
82732
+ providerTypesIOwn.delete(type);
82733
+ return pr.unregister(type);
82734
+ },
82288
82735
  create: (cfg) => pr.create(cfg),
82289
82736
  list: () => pr.list()
82290
82737
  };
82291
- this.mcp = init.mcpRegistry ?? noopMcp;
82738
+ const mcpRegistry = init.mcpRegistry;
82739
+ if (!mcpRegistry) {
82740
+ this.mcp = noopMcp;
82741
+ } else {
82742
+ const mcpServersIStarted = /* @__PURE__ */ new Set();
82743
+ const assertOwnsMcpServer = (name, op) => {
82744
+ if (isOfficial) return;
82745
+ if (mcpServersIStarted.has(name)) return;
82746
+ if (!mcpRegistry.list().some((s) => s.name === name)) return;
82747
+ throw new Error(
82748
+ `Plugin "${owner}" may not ${op} MCP server "${name}" \u2014 it was not started by this plugin.`
82749
+ );
82750
+ };
82751
+ this.mcp = {
82752
+ start: async (cfg) => {
82753
+ const name = cfg?.name;
82754
+ if (typeof name === "string" && mcpRegistry.list().some((s) => s.name === name)) {
82755
+ assertOwnsMcpServer(name, "start");
82756
+ }
82757
+ await mcpRegistry.start(cfg);
82758
+ if (typeof name === "string") mcpServersIStarted.add(name);
82759
+ },
82760
+ stop: async (name) => {
82761
+ assertOwnsMcpServer(name, "stop");
82762
+ await mcpRegistry.stop(name);
82763
+ },
82764
+ restart: async (name) => {
82765
+ assertOwnsMcpServer(name, "restart");
82766
+ await mcpRegistry.restart(name);
82767
+ },
82768
+ list: () => mcpRegistry.list()
82769
+ };
82770
+ }
82292
82771
  const scr = init.slashCommandRegistry;
82293
82772
  const official = init.official === true;
82773
+ const commandsIOwn = /* @__PURE__ */ new Set();
82294
82774
  this.slashCommands = scr ? {
82295
- register: (cmd) => scr.register(cmd, owner, { official }),
82296
- unregister: (name) => scr.unregister(name),
82775
+ register: (cmd) => {
82776
+ scr.register(cmd, owner, { official });
82777
+ for (const key of [cmd.name, ...cmd.aliases ?? []]) {
82778
+ commandsIOwn.add(key);
82779
+ commandsIOwn.add(`${owner}:${key}`);
82780
+ }
82781
+ },
82782
+ unregister: (name) => {
82783
+ if (!official && !commandsIOwn.has(name) && scr.get(name) !== void 0) {
82784
+ throw new Error(
82785
+ `Plugin "${owner}" may not unregister slash command "${name}" \u2014 it was not registered by this plugin.`
82786
+ );
82787
+ }
82788
+ for (const key of [
82789
+ name,
82790
+ `${owner}:${name}`,
82791
+ name.startsWith(`${owner}:`) ? name.slice(owner.length + 1) : name
82792
+ ]) {
82793
+ commandsIOwn.delete(key);
82794
+ }
82795
+ return scr.unregister(name);
82796
+ },
82297
82797
  get: (name) => scr.get(name),
82298
82798
  list: () => scr.list()
82299
82799
  } : noopSlashCommands;
@@ -86898,14 +87398,21 @@ async function openInEditor(filePath, env = process.env) {
86898
87398
  context: { filePath }
86899
87399
  });
86900
87400
  }
86901
- const child = spawn12(parts[0], [...parts.slice(1), filePath], {
87401
+ const editorArgs = [...parts.slice(1), filePath];
87402
+ const child = shell ? (() => {
87403
+ const inv = buildWin32CmdShimInvocation(parts[0], editorArgs);
87404
+ return spawn12(inv.command, inv.args, {
87405
+ stdio: "ignore",
87406
+ detached: true,
87407
+ windowsVerbatimArguments: inv.windowsVerbatimArguments,
87408
+ // Suppresses the console flash the cmd.exe wrapper would otherwise
87409
+ // show before the editor appears. Repo convention — see
87410
+ // `core/tests/architecture/spawn-convention.test.ts`.
87411
+ windowsHide: true
87412
+ });
87413
+ })() : spawn12(parts[0], editorArgs, {
86902
87414
  stdio: "ignore",
86903
87415
  detached: true,
86904
- shell,
86905
- // `shell` routes through cmd.exe on win32, which flashes a console window
86906
- // before the editor appears. A GUI editor is unaffected by the flag; the
86907
- // shell wrapper is what it suppresses. Repo convention — see
86908
- // `core/tests/architecture/spawn-convention.test.ts`.
86909
87416
  windowsHide: true
86910
87417
  });
86911
87418
  child.unref();
@@ -99293,6 +99800,8 @@ export {
99293
99800
  sanitizeModel,
99294
99801
  sanitizeNodeOptions,
99295
99802
  sanitizeRequest,
99803
+ sanitizeTerminalPreview,
99804
+ sanitizeTerminalText,
99296
99805
  sanitizeWireToolName,
99297
99806
  saveCompletedWorkCheckpoint,
99298
99807
  saveGoal,