@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.
@@ -8599,6 +8599,14 @@ var PATTERNS = [
8599
8599
  anchor: "sk-ant-"
8600
8600
  },
8601
8601
  { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g, anchor: "sk-" },
8602
+ {
8603
+ // `xai` is a first-class provider in this codebase, but its key shape was
8604
+ // absent here — so the one credential format WrongStack itself hands users
8605
+ // was the one the scrubber could not recognize (audit 2026-08-20).
8606
+ type: "xai_key",
8607
+ regex: /(?<![A-Za-z0-9])xai-[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g,
8608
+ anchor: "xai-"
8609
+ },
8602
8610
  { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g, anchor: "ghp_" },
8603
8611
  { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g, anchor: "github_pat_" },
8604
8612
  { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g, anchor: "AKIA" },
@@ -8689,8 +8697,8 @@ var PATTERNS = [
8689
8697
  // replacement so the separator between adjacent secrets is preserved
8690
8698
  // rather than collapsed. Capture groups are therefore: 1=leading
8691
8699
  // delimiter, 2=key name, 3=value.
8692
- regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
8693
- anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
8700
+ regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD|PASSPHRASE))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
8701
+ anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD", "PASSPHRASE"]
8694
8702
  },
8695
8703
  {
8696
8704
  type: "json_credential_key",
@@ -8807,6 +8815,27 @@ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key
8807
8815
  var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
8808
8816
  var SCRUB_CHUNK_BYTES = 64 * 1024;
8809
8817
  var SCRUB_OVERLAP_BYTES = 1024;
8818
+ var PEM_PRIVATE_KEY_BEGIN_RE = /-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----/;
8819
+ var PEM_END_MARKER = "-----END";
8820
+ var MAX_PEM_BLOCK_BYTES = 64 * 1024;
8821
+ var PEM_END_LINE_TOLERANCE = 64;
8822
+ function extendChunkBoundaryPastPem(text, chunkStart, proposedEnd) {
8823
+ const head = text.slice(chunkStart, proposedEnd);
8824
+ const lastBegin = head.lastIndexOf("-----BEGIN ");
8825
+ if (lastBegin === -1) return proposedEnd;
8826
+ const fromBegin = text.slice(chunkStart + lastBegin);
8827
+ const marker = PEM_PRIVATE_KEY_BEGIN_RE.exec(fromBegin);
8828
+ if (!marker || marker.index !== 0) return proposedEnd;
8829
+ const bodyStart = marker[0].length;
8830
+ const cap = Math.min(text.length, chunkStart + lastBegin + MAX_PEM_BLOCK_BYTES);
8831
+ const closeIdx = fromBegin.indexOf(PEM_END_MARKER, bodyStart);
8832
+ if (closeIdx === -1 || chunkStart + lastBegin + closeIdx >= cap + PEM_END_LINE_TOLERANCE) {
8833
+ return proposedEnd;
8834
+ }
8835
+ const lineEnd = fromBegin.indexOf("\n", closeIdx);
8836
+ const end = lineEnd === -1 ? text.length : chunkStart + lastBegin + lineEnd + 1;
8837
+ return Math.max(proposedEnd, end);
8838
+ }
8810
8839
  var PATTERN_ANCHORS = [
8811
8840
  ...new Set(
8812
8841
  PATTERNS.flatMap(
@@ -8843,6 +8872,7 @@ var DefaultSecretScrubber = class {
8843
8872
  }
8844
8873
  }
8845
8874
  end = safe === -1 ? end : safe + 1;
8875
+ end = extendChunkBoundaryPastPem(text, i, end);
8846
8876
  }
8847
8877
  out.push(this.scrubOne(text.slice(i, end)));
8848
8878
  i = end;
@@ -10547,11 +10577,13 @@ var CHAOS_MONKEY_AGENT = {
10547
10577
  tools: [...TOOLS.build],
10548
10578
  skillNames: ["testing", "typescript-strict"],
10549
10579
  spawnBudgetExempt: true,
10550
- // Follow fleet worktree policy (NOT 'required'): mutation targets are
10551
- // often freshly written and uncommitted — a worktree spawned from HEAD
10552
- // would not contain them and every mutant would drift. Callers pass
10553
- // `worktree: 'off'` in the mutation_test input for uncommitted targets.
10554
- worktree: "auto",
10580
+ // Run in the live checkout: mutation targets are usually freshly
10581
+ // written and uncommitted — a worktree spawned from HEAD would not
10582
+ // contain them and every mutant would drift. The mutation_test tool
10583
+ // honors this value as its default; callers can still override per
10584
+ // call via its `chaosWorktree` input when targets are committed and
10585
+ // isolation is wanted.
10586
+ worktree: "off",
10555
10587
  // Report travels via submit_result + final text, not the leader's stream.
10556
10588
  textStream: "silent",
10557
10589
  toolStream: "silent"
@@ -11726,16 +11758,14 @@ var ExploreCompanion = class {
11726
11758
  this.running = true;
11727
11759
  this.unsubscribers.push(
11728
11760
  this.opts.events.on("tool.executed", (e) => {
11729
- const lsid = this.resolveLeaderSessionId();
11730
- if (lsid && e.sessionId && e.sessionId !== lsid) return;
11761
+ if (e.sessionId !== this.resolveLeaderSessionId()) return;
11731
11762
  this.trackToolExecuted(e);
11732
11763
  })
11733
11764
  );
11734
11765
  if (this.cfg.signals.todoInProgress && this.resolveLeaderAgentId()) {
11735
11766
  this.unsubscribers.push(
11736
11767
  this.opts.events.on("session.agents_updated", (e) => {
11737
- const lsid = this.resolveLeaderSessionId();
11738
- if (lsid && e.sessionId && e.sessionId !== lsid) return;
11768
+ if (e.sessionId !== this.resolveLeaderSessionId()) return;
11739
11769
  this.trackAgentTodos(e.agents);
11740
11770
  })
11741
11771
  );
@@ -11743,8 +11773,7 @@ var ExploreCompanion = class {
11743
11773
  if (this.cfg.signals.errorSymbol) {
11744
11774
  this.unsubscribers.push(
11745
11775
  this.opts.events.on("error", (e) => {
11746
- const lsid = this.resolveLeaderSessionId();
11747
- if (lsid && e.sessionId && e.sessionId !== lsid) return;
11776
+ if (e.sessionId !== this.resolveLeaderSessionId()) return;
11748
11777
  this.trackError(e.err);
11749
11778
  })
11750
11779
  );
@@ -11857,9 +11886,18 @@ var ExploreCompanion = class {
11857
11886
  limit: 20
11858
11887
  });
11859
11888
  const lsid = this.resolveLeaderSessionId();
11889
+ const selfRecipients = new Set(
11890
+ [
11891
+ this.cfg.companionAgentId,
11892
+ mailboxIdentityBase(this.cfg.companionAgentId),
11893
+ ...lsid != null ? [sessionRecipient(lsid)] : []
11894
+ ].map((r) => r.toLowerCase())
11895
+ );
11860
11896
  for (const msg of messages) {
11861
11897
  if (msg.type !== "ask" && msg.type !== "assign") continue;
11862
- const fromLeader = isMailboxLeader(msg.from) || lsid != null && msg.senderSessionId === lsid;
11898
+ const to = msg.to.trim().toLowerCase();
11899
+ if (to !== "*" && !selfRecipients.has(to)) continue;
11900
+ const fromLeader = msg.senderSessionId === void 0 && isMailboxLeader(msg.from) || lsid != null && msg.senderSessionId === lsid;
11863
11901
  if (!fromLeader) continue;
11864
11902
  this.engage({
11865
11903
  id: randomUUID6(),
@@ -14384,24 +14422,29 @@ var TOKEN_PATTERNS = [
14384
14422
  kind: "return-null",
14385
14423
  // `return <expr>;` where expr is not already null/undefined/void.
14386
14424
  regex: /(?<indent>\breturn\b)(?<expr>\s+[^;{}\n]+?)\s*;/g,
14387
- replace: () => "return null;"
14425
+ replace: () => "return null;",
14426
+ endpointsInCode: true
14388
14427
  }
14389
14428
  ];
14390
14429
  function planMutations(file, source, opts = {}) {
14391
14430
  const maxPerFile = opts.maxPerFile ?? 25;
14392
14431
  const out = [];
14393
14432
  const lines = source.split("\n");
14433
+ const masks = computeLineMasks(source);
14394
14434
  for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
14395
14435
  const line = lines[lineIdx];
14396
14436
  const t = line.trim();
14397
- if (t.startsWith("//") || t.startsWith("*") || t.startsWith("/*")) continue;
14437
+ if (t.startsWith("//")) continue;
14438
+ const codeRanges = masks[lineIdx];
14439
+ const inCode = (start) => codeRanges.some(([s, e]) => start >= s && start < e);
14398
14440
  for (const pattern of TOKEN_PATTERNS) {
14399
14441
  pattern.regex.lastIndex = 0;
14400
14442
  let m;
14401
14443
  while ((m = pattern.regex.exec(line)) !== null) {
14402
14444
  const token = m.groups?.["op"] ?? m[0];
14403
14445
  const tokenStart = m.index + m[0].indexOf(token);
14404
- if (isMasked(line, tokenStart, token.length)) continue;
14446
+ if (!inCode(tokenStart)) continue;
14447
+ if (pattern.endpointsInCode && !inCode(tokenStart + token.length - 1)) continue;
14405
14448
  const original = line.slice(tokenStart, tokenStart + token.length);
14406
14449
  const replacement = pattern.replace(token);
14407
14450
  if (replacement === original) continue;
@@ -14420,19 +14463,179 @@ function planMutations(file, source, opts = {}) {
14420
14463
  }
14421
14464
  return out.slice(0, maxPerFile);
14422
14465
  }
14423
- function isMasked(line, start, len) {
14424
- let inSingle = false;
14425
- let inDouble = false;
14426
- for (let i = 0; i < start; i++) {
14427
- const c = line[i];
14428
- const prev = i > 0 ? line[i - 1] : void 0;
14429
- if (c === "'" && prev !== "\\") inSingle = !inSingle;
14430
- else if (c === '"' && prev !== "\\") inDouble = !inDouble;
14431
- if (!inSingle && !inDouble && c === "/" && prev === "/") return true;
14466
+ function computeLineMasks(source) {
14467
+ const lines = source.split("\n");
14468
+ const masks = lines.map(() => []);
14469
+ const stack = [{ kind: "code", depth: 0, parens: [] }];
14470
+ let inBlockComment = false;
14471
+ let lastToken = null;
14472
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
14473
+ const line = lines[lineIdx];
14474
+ const ranges = masks[lineIdx];
14475
+ let runStart = null;
14476
+ const closeRun = (end) => {
14477
+ if (runStart !== null && end > runStart) ranges.push([runStart, end]);
14478
+ runStart = null;
14479
+ };
14480
+ let i = 0;
14481
+ if (inBlockComment) {
14482
+ const close = line.indexOf("*/");
14483
+ if (close === -1) continue;
14484
+ inBlockComment = false;
14485
+ i = close + 2;
14486
+ }
14487
+ while (i < line.length) {
14488
+ const top = stack[stack.length - 1];
14489
+ const c = line[i];
14490
+ if (top.kind === "template") {
14491
+ if (c === "\\") {
14492
+ i += 2;
14493
+ continue;
14494
+ }
14495
+ if (c === "`") {
14496
+ stack.pop();
14497
+ lastToken = "`";
14498
+ i++;
14499
+ continue;
14500
+ }
14501
+ if (c === "$" && line[i + 1] === "{") {
14502
+ stack.push({ kind: "code", depth: 0, parens: [] });
14503
+ lastToken = "${";
14504
+ i += 2;
14505
+ continue;
14506
+ }
14507
+ i++;
14508
+ continue;
14509
+ }
14510
+ if (/[\w$]/.test(c)) {
14511
+ let j = i + 1;
14512
+ while (j < line.length && /[\w$]/.test(line[j])) j++;
14513
+ lastToken = line.slice(i, j);
14514
+ if (runStart === null) runStart = i;
14515
+ i = j;
14516
+ continue;
14517
+ }
14518
+ if (c === "'" || c === '"') {
14519
+ closeRun(i);
14520
+ i++;
14521
+ while (i < line.length && line[i] !== c) {
14522
+ if (line[i] === "\\") i++;
14523
+ i++;
14524
+ }
14525
+ i++;
14526
+ lastToken = c;
14527
+ continue;
14528
+ }
14529
+ if (c === "`") {
14530
+ closeRun(i);
14531
+ stack.push({ kind: "template", depth: 0, parens: [] });
14532
+ i++;
14533
+ continue;
14534
+ }
14535
+ if (c === "/" && line[i + 1] === "/") {
14536
+ closeRun(i);
14537
+ break;
14538
+ }
14539
+ if (c === "/" && line[i + 1] === "*") {
14540
+ closeRun(i);
14541
+ const close = line.indexOf("*/", i + 2);
14542
+ if (close === -1) {
14543
+ inBlockComment = true;
14544
+ break;
14545
+ }
14546
+ i = close + 2;
14547
+ continue;
14548
+ }
14549
+ if (c === "/") {
14550
+ if (!tokenCanEndOperand(lastToken)) {
14551
+ closeRun(i);
14552
+ const next = skipRegexLiteral(line, i);
14553
+ lastToken = next > i + 1 ? "regex" : "/";
14554
+ i = next;
14555
+ continue;
14556
+ }
14557
+ }
14558
+ if (c === "(") {
14559
+ top.parens.push(CONTROL_KEYWORDS.has(lastToken ?? "") ? "control" : "expr");
14560
+ lastToken = c;
14561
+ } else if (c === ")") {
14562
+ const kind = top.parens.pop() ?? "expr";
14563
+ lastToken = kind === "control" ? "control-paren-close" : ")";
14564
+ } else if (c === "{") {
14565
+ top.depth++;
14566
+ lastToken = c;
14567
+ } else if (c === "}") {
14568
+ if (top.depth > 0) {
14569
+ top.depth--;
14570
+ lastToken = c;
14571
+ } else if (stack.length > 1) {
14572
+ closeRun(i);
14573
+ stack.pop();
14574
+ i++;
14575
+ continue;
14576
+ } else {
14577
+ lastToken = c;
14578
+ }
14579
+ } else if (c !== " " && c !== " " && c !== "\r") {
14580
+ lastToken = c;
14581
+ }
14582
+ if (runStart === null) runStart = i;
14583
+ i++;
14584
+ }
14585
+ closeRun(line.length);
14586
+ }
14587
+ return masks;
14588
+ }
14589
+ var KEYWORDS_BEFORE_REGEX = /* @__PURE__ */ new Set([
14590
+ "return",
14591
+ "typeof",
14592
+ "instanceof",
14593
+ "in",
14594
+ "of",
14595
+ "new",
14596
+ "delete",
14597
+ "void",
14598
+ "throw",
14599
+ "case",
14600
+ "do",
14601
+ "else",
14602
+ "yield",
14603
+ "await"
14604
+ ]);
14605
+ var CONTROL_KEYWORDS = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "catch", "with", "await"]);
14606
+ function tokenCanEndOperand(token) {
14607
+ if (token === null) return false;
14608
+ if (/^[\w$]+$/.test(token)) return !KEYWORDS_BEFORE_REGEX.has(token);
14609
+ return token === ")" || token === "]" || token === "." || token === '"' || token === "'" || token === "`";
14610
+ }
14611
+ function skipRegexLiteral(line, start) {
14612
+ let i = start + 1;
14613
+ let inClass = false;
14614
+ while (i < line.length) {
14615
+ const ch = line[i];
14616
+ if (ch === "\\") {
14617
+ i += 2;
14618
+ continue;
14619
+ }
14620
+ if (inClass) {
14621
+ if (ch === "]") inClass = false;
14622
+ i++;
14623
+ continue;
14624
+ }
14625
+ if (ch === "[") {
14626
+ inClass = true;
14627
+ i++;
14628
+ continue;
14629
+ }
14630
+ if (ch === "/") {
14631
+ i++;
14632
+ break;
14633
+ }
14634
+ if (ch === "\n" || ch === "\r") return line.length;
14635
+ i++;
14432
14636
  }
14433
- if (inSingle || inDouble) return true;
14434
- const window = line.slice(start, start + len);
14435
- return /['"]/.test(window);
14637
+ while (i < line.length && /[a-z]/.test(line[i])) i++;
14638
+ return i;
14436
14639
  }
14437
14640
  function parseMutationReport(text) {
14438
14641
  const candidates = [];
@@ -14483,7 +14686,7 @@ function normalizeMutantEntry(value) {
14483
14686
  const rec = value;
14484
14687
  const id = typeof rec["id"] === "string" ? rec["id"] : void 0;
14485
14688
  const status = rec["status"];
14486
- if (!id || status !== "killed" && status !== "survived" && status !== "skipped") {
14689
+ if (!id || status !== "killed" && status !== "survived" && status !== "skipped" && status !== "killed-by-hang") {
14487
14690
  return void 0;
14488
14691
  }
14489
14692
  return {
@@ -14539,7 +14742,7 @@ function makeMutationTestTool(director, roster, opts = {}) {
14539
14742
  },
14540
14743
  chaosWorktree: {
14541
14744
  anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
14542
- description: "Worktree override for the chaos agent. Use 'off' when targets are uncommitted \u2014 a worktree from HEAD would not contain them."
14745
+ 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."
14543
14746
  },
14544
14747
  timeoutMs: { type: "number", minimum: 1, description: "Per-task timeout for chaos/strengthen/rerun tasks." },
14545
14748
  reportOnly: {
@@ -14561,8 +14764,16 @@ function makeMutationTestTool(director, roster, opts = {}) {
14561
14764
  error: "No mutable sites found in the given targets (after comment/string filtering)."
14562
14765
  };
14563
14766
  }
14767
+ const chaosBase = roster?.[CHAOS_ROLE];
14768
+ if (!chaosBase) {
14769
+ return {
14770
+ verdict: "inconclusive",
14771
+ passed: false,
14772
+ 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)."
14773
+ };
14774
+ }
14564
14775
  const chaosSubagentId = await director.spawn(
14565
- makeChaosConfig(roster, i.chaosWorktree ?? "off")
14776
+ makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
14566
14777
  );
14567
14778
  const chaosTaskId = await director.assign({
14568
14779
  id: randomUUID11(),
@@ -14580,6 +14791,7 @@ function makeMutationTestTool(director, roster, opts = {}) {
14580
14791
  );
14581
14792
  const attempts = [];
14582
14793
  let current = survivors;
14794
+ let rerunUnknowns = [];
14583
14795
  while (current.length > 0 && attempts.length < maxAttempts && i.repairSubagentId) {
14584
14796
  const attemptNo = attempts.length + 1;
14585
14797
  const strengthenTaskId = await director.assign({
@@ -14601,7 +14813,7 @@ function makeMutationTestTool(director, roster, opts = {}) {
14601
14813
  }
14602
14814
  const survivorPlan = plan.filter((p) => current.some((s) => s.id === p.id));
14603
14815
  const rerunSubagentId = await director.spawn(
14604
- makeChaosConfig(roster, i.chaosWorktree ?? "off")
14816
+ makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
14605
14817
  );
14606
14818
  const rerunTaskId = await director.assign({
14607
14819
  id: randomUUID11(),
@@ -14611,29 +14823,36 @@ function makeMutationTestTool(director, roster, opts = {}) {
14611
14823
  });
14612
14824
  const [rerunResult] = await director.awaitTasks([rerunTaskId]);
14613
14825
  const passN = collectOutcomes(rerunResult, survivorPlan);
14614
- const stillSurviving = passN.filter((m) => m.status === "survived" || m.status === "skipped");
14826
+ const stillSurviving = passN.filter((m) => !isKill(m.status));
14827
+ rerunUnknowns = passN.filter((m) => m.status === "skipped");
14615
14828
  attempts.push({
14616
14829
  attempt: attemptNo,
14617
14830
  survivorsBefore: current,
14618
14831
  strengthenResult: { taskId: strengthenResult.taskId, status: strengthenResult.status },
14619
14832
  rerunResult: { taskId: rerunTaskId, status: rerunResult?.status ?? "unknown" },
14620
14833
  survivorsAfter: stillSurviving,
14621
- suspectedEquivalent: stillSurviving.filter((m) => current.some((c) => c.id === m.id)).map((m) => m.id)
14834
+ suspectedEquivalent: stillSurviving.filter((m) => m.status === "survived" && current.some((c) => c.id === m.id)).map((m) => m.id)
14622
14835
  });
14623
- current = stillSurviving.filter((m) => m.status === "survived");
14624
- if (passN.every((m) => m.status === "skipped")) break;
14836
+ current = stillSurviving;
14625
14837
  }
14626
- const finalSurvivors = current;
14838
+ const finalSurvivors = current.filter((m) => m.status === "survived");
14627
14839
  const verifiedCount = pass1.filter((m) => m.status !== "skipped").length;
14628
14840
  const skippedCount = pass1.filter((m) => m.status === "skipped").length;
14629
- const score = plan.length === 0 ? 0 : pass1.filter((m) => m.status === "killed").length / plan.length;
14630
- const verdict = verifiedCount === 0 ? "inconclusive" : finalSurvivors.length === 0 ? skippedCount > 0 ? "partial" : "pass" : score >= 0.8 ? "partial" : "fail";
14841
+ const rerunUnknownCount = rerunUnknowns.length;
14842
+ const score = plan.length === 0 ? 0 : pass1.filter((m) => isKill(m.status)).length / plan.length;
14843
+ const verdict = verifiedCount === 0 ? "inconclusive" : finalSurvivors.length === 0 ? skippedCount > 0 || rerunUnknownCount > 0 ? "partial" : "pass" : score >= 0.8 ? "partial" : "fail";
14631
14844
  return {
14632
14845
  verdict,
14633
14846
  passed: verdict === "pass",
14634
14847
  mutationScore: Number.parseFloat(score.toFixed(3)),
14635
14848
  planned: plan.length,
14636
- killed: pass1.filter((m) => m.status === "killed").length,
14849
+ killed: pass1.filter((m) => isKill(m.status)).length,
14850
+ // Breakout of `killed`: how many kills were detected by the test
14851
+ // command hanging rather than by a failing assertion. A subset of
14852
+ // `killed`, surfaced so a director can distinguish a hang-heavy
14853
+ // suite (mutants breaking termination, not assertions) from an
14854
+ // assertion-strong one. hangHeavy = killedByHang === killed.
14855
+ killedByHang: pass1.filter((m) => m.status === "killed-by-hang").length,
14637
14856
  survived: pass1.filter((m) => m.status === "survived").length,
14638
14857
  skipped: pass1.filter((m) => m.status === "skipped").length,
14639
14858
  finalSurvivors: finalSurvivors.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
@@ -14641,7 +14860,11 @@ function makeMutationTestTool(director, roster, opts = {}) {
14641
14860
  strengthenAttempts: attempts.length,
14642
14861
  attempts,
14643
14862
  chaosTaskId,
14644
- nextAction: finalSurvivors.length === 0 ? "accept" : attempts.length >= maxAttempts && i.repairSubagentId ? "manual_review_survivors" : "strengthen_tests"
14863
+ // Unverified leftovers from the strengthen loop: surfaced so the
14864
+ // caller can see WHICH mutants lack kill evidence, and counted by
14865
+ // the verdict gate above.
14866
+ unverifiedFromRerun: rerunUnknowns.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
14867
+ nextAction: finalSurvivors.length === 0 && rerunUnknownCount === 0 && skippedCount === 0 ? "accept" : attempts.length >= maxAttempts && i.repairSubagentId ? "manual_review_survivors" : "strengthen_tests"
14645
14868
  };
14646
14869
  }
14647
14870
  };
@@ -14657,7 +14880,7 @@ function normalizeMutationTestInput(input) {
14657
14880
  maxPerFile: typeof raw["maxPerFile"] === "number" ? raw["maxPerFile"] : void 0,
14658
14881
  maxStrengthenAttempts: typeof raw["maxStrengthenAttempts"] === "number" ? raw["maxStrengthenAttempts"] : void 0,
14659
14882
  repairSubagentId: typeof raw["repairSubagentId"] === "string" && raw["repairSubagentId"].trim() ? raw["repairSubagentId"].trim() : void 0,
14660
- chaosWorktree: raw["chaosWorktree"] ?? void 0,
14883
+ chaosWorktree: normalizeWorktreeOverride(raw["chaosWorktree"]),
14661
14884
  timeoutMs: typeof raw["timeoutMs"] === "number" ? raw["timeoutMs"] : void 0,
14662
14885
  reportOnly: raw["reportOnly"] === true
14663
14886
  };
@@ -14679,8 +14902,7 @@ function buildPlan(i, projectRoot) {
14679
14902
  }
14680
14903
  return plan;
14681
14904
  }
14682
- function makeChaosConfig(roster, worktree) {
14683
- const base = roster?.[CHAOS_ROLE] ?? getAgentDefinition(CHAOS_ROLE)?.config ?? { name: "Chaos Monkey", role: CHAOS_ROLE };
14905
+ function makeChaosConfig(base, worktree) {
14684
14906
  return { ...instantiateRosterConfig2(CHAOS_ROLE, base), worktree };
14685
14907
  }
14686
14908
  function buildChaosTask(plan, i, pass, priorSurvivors) {
@@ -14696,7 +14918,7 @@ ${priorSurvivors.map((s) => `- ${s.id} (${s.kind} @ ${s.file}:${s.line})`).join(
14696
14918
  "For each mutant, in order:",
14697
14919
  "1. Apply ONLY that mutation at its exact (file, line, column).",
14698
14920
  `2. Run the test command: ${i.testCommand}${i.cwd ? ` (cwd: ${i.cwd})` : ""}`,
14699
- "3. Record killed (tests failed \u2014 quote first failing assertion) or survived (suite green).",
14921
+ "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).",
14700
14922
  "4. Restore the file byte-for-byte before the next mutant.",
14701
14923
  "",
14702
14924
  "Mutants:",
@@ -14708,23 +14930,49 @@ ${priorSurvivors.map((s) => `- ${s.id} (${s.kind} @ ${s.file}:${s.line})`).join(
14708
14930
  ].join("\n");
14709
14931
  }
14710
14932
  function buildStrengthenTask(survivors, i, attempt) {
14933
+ const confirmed = survivors.filter((s) => s.status === "survived");
14934
+ const unverified = survivors.filter((s) => s.status === "skipped");
14935
+ const row = (s) => `- ${s.id} | ${s.file}:${s.line} | ${s.kind}${s.evidence ? ` | ${s.evidence}` : ""}`;
14711
14936
  return [
14712
- `Strengthen the tests so these SURVIVING mutants die (attempt ${attempt}).`,
14713
- "",
14714
- "Each survivor below was a deliberate sabotage of production code that the current suite did NOT catch:",
14715
- ...survivors.map((s) => `- ${s.id} | ${s.file}:${s.line} | ${s.kind}${s.evidence ? ` | ${s.evidence}` : ""}`),
14937
+ `Strengthen the tests so the mutants below die (attempt ${attempt}).`,
14716
14938
  "",
14717
- `Test command that must fail under each mutant: ${i.testCommand}`,
14939
+ ...confirmed.length > 0 ? [
14940
+ "CONFIRMED SURVIVORS \u2014 each was a deliberate sabotage of production code that the current suite did NOT catch:",
14941
+ ...confirmed.map(row),
14942
+ ""
14943
+ ] : [],
14944
+ ...unverified.length > 0 ? [
14945
+ "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.",
14946
+ ...unverified.map(row),
14947
+ ""
14948
+ ] : [],
14949
+ `Test command that must fail under each CONFIRMED mutant: ${i.testCommand}`,
14718
14950
  "",
14719
- "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."
14951
+ "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."
14720
14952
  ].join("\n");
14721
14953
  }
14722
14954
  function collectOutcomes(result, plan) {
14723
14955
  const fromText = parseTextOutcomes(result);
14724
14956
  if (fromText.length > 0) {
14725
- const planned = new Set(plan.map((p) => p.id));
14726
- const matched = fromText.filter((m) => planned.has(m.id));
14727
- if (matched.length > 0) return matched;
14957
+ const remaining = [...plan];
14958
+ const matched = [];
14959
+ for (const m of fromText) {
14960
+ const idx = remaining.findIndex((p) => p.id === m.id);
14961
+ if (idx === -1) continue;
14962
+ remaining.splice(idx, 1);
14963
+ matched.push(m);
14964
+ }
14965
+ if (matched.length > 0) {
14966
+ const missing = remaining.map((p) => ({
14967
+ id: p.id,
14968
+ file: p.file,
14969
+ line: p.line,
14970
+ kind: p.kind,
14971
+ status: "skipped",
14972
+ evidence: "not reported by chaos task"
14973
+ }));
14974
+ return [...matched, ...missing];
14975
+ }
14728
14976
  }
14729
14977
  return plan.map((p) => ({
14730
14978
  id: p.id,
@@ -14735,6 +14983,9 @@ function collectOutcomes(result, plan) {
14735
14983
  evidence: result ? `chaos task ended ${result.status}` : "chaos task produced no result"
14736
14984
  }));
14737
14985
  }
14986
+ function isKill(status) {
14987
+ return status === "killed" || status === "killed-by-hang";
14988
+ }
14738
14989
  function parseTextOutcomes(result) {
14739
14990
  const text = typeof result?.result === "string" ? result.result : void 0;
14740
14991
  if (!text) return [];
@@ -22612,6 +22863,7 @@ function worktreeOwnerLabel(task, config) {
22612
22863
  }
22613
22864
 
22614
22865
  // src/coordination/director.ts
22866
+ var BUSY_REARM_FLOOR_MS = 1e3;
22615
22867
  var Director = class _Director {
22616
22868
  /* eslint-disable-next-line @typescript-eslint/no-unused-vars — just a cast helper */
22617
22869
  static _asManifestEntry(v) {
@@ -22677,6 +22929,13 @@ var Director = class _Director {
22677
22929
  subagentIdleTimeoutMs;
22678
22930
  retireSubagentOnTaskComplete;
22679
22931
  subagentIdleTimers = /* @__PURE__ */ new Map();
22932
+ /**
22933
+ * Effective idle window per subagent (spawn-time `idleTimeoutMs` override
22934
+ * or the Director-wide default; undefined = no window). Internal-task
22935
+ * completion re-arms with THIS value, not the Director-wide default, so
22936
+ * a subagent-configured window survives its first internal probe.
22937
+ */
22938
+ subagentIdleDelayMs = /* @__PURE__ */ new Map();
22680
22939
  sharedScratchpadPath;
22681
22940
  maxSpawns;
22682
22941
  maxSpawnDepth;
@@ -22833,7 +23092,13 @@ var Director = class _Director {
22833
23092
  handleTaskCompleted(payload) {
22834
23093
  const r = payload.result;
22835
23094
  const settled = this.tasks.settle(r);
22836
- if (settled.internal) return;
23095
+ if (settled.internal) {
23096
+ this.armSubagentIdleRetirement(
23097
+ r.subagentId,
23098
+ this.subagentIdleDelayMs.get(r.subagentId) ?? this.subagentIdleTimeoutMs
23099
+ );
23100
+ return;
23101
+ }
22837
23102
  const title = this.tasks.descriptionFor(r.taskId, payload.task.description ?? r.taskId);
22838
23103
  if (!settled.consumedInBand && this.taskResultNotifier) {
22839
23104
  const resultText = typeof r.result === "string" ? r.result : r.result !== void 0 ? safeStringify(r.result) : void 0;
@@ -22898,7 +23163,7 @@ var Director = class _Director {
22898
23163
  }
22899
23164
  this.armSubagentIdleRetirement(
22900
23165
  r.subagentId,
22901
- this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleTimeoutMs
23166
+ this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleDelayMs.get(r.subagentId) ?? this.subagentIdleTimeoutMs
22902
23167
  );
22903
23168
  }
22904
23169
  extensionsFor(subagentId) {
@@ -22984,6 +23249,7 @@ var Director = class _Director {
22984
23249
  this.resolveSpawnModel(config);
22985
23250
  const subagentId = await spawn4(this, config, priceLookup);
22986
23251
  const perSubagentIdleMs = typeof config.idleTimeoutMs === "number" && Number.isFinite(config.idleTimeoutMs) && config.idleTimeoutMs >= 0 ? config.idleTimeoutMs : this.subagentIdleTimeoutMs;
23252
+ this.subagentIdleDelayMs.set(subagentId, perSubagentIdleMs);
22987
23253
  this.armSubagentIdleRetirement(subagentId, perSubagentIdleMs);
22988
23254
  return subagentId;
22989
23255
  }
@@ -23037,6 +23303,7 @@ var Director = class _Director {
23037
23303
  this.budgetPolicy.dispose();
23038
23304
  for (const timer of this.subagentIdleTimers.values()) clearTimeout(timer);
23039
23305
  this.subagentIdleTimers.clear();
23306
+ this.subagentIdleDelayMs.clear();
23040
23307
  await this.coordinator.stopAll();
23041
23308
  this.tasks.resolveWaitersOnShutdown();
23042
23309
  for (const b of this.subagentBridges.values()) {
@@ -23095,6 +23362,7 @@ var Director = class _Director {
23095
23362
  }
23096
23363
  async remove(subagentId) {
23097
23364
  this.clearSubagentIdleRetirement(subagentId);
23365
+ this.subagentIdleDelayMs.delete(subagentId);
23098
23366
  void this.appendSessionEvent({
23099
23367
  type: "agent_stopped",
23100
23368
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -23147,9 +23415,13 @@ var Director = class _Director {
23147
23415
  const timer = setTimeout(() => {
23148
23416
  this.subagentIdleTimers.delete(subagentId);
23149
23417
  const entry = this.coordinator.getStatus().subagents.find((a) => a.id === subagentId);
23150
- if (entry?.status !== "idle") return;
23418
+ if (entry === void 0) return;
23419
+ if (entry.status !== "idle") {
23420
+ this.armSubagentIdleRetirement(subagentId, Math.max(delayMs, BUSY_REARM_FLOOR_MS));
23421
+ return;
23422
+ }
23151
23423
  if (this.coordinator.listPendingTasks().some((task) => task.subagentId === subagentId)) {
23152
- this.armSubagentIdleRetirement(subagentId, this.subagentIdleTimeoutMs);
23424
+ this.armSubagentIdleRetirement(subagentId, Math.max(delayMs, BUSY_REARM_FLOOR_MS));
23153
23425
  return;
23154
23426
  }
23155
23427
  void this.remove(subagentId).catch(
@@ -45,8 +45,10 @@ export interface PlanMutationsOptions {
45
45
  * Plan mutations for one file's source text.
46
46
  *
47
47
  * The scan is line-by-line with the file's own line splits preserved so ids
48
- * stay (line, column) anchored. Mutations inside comments and string
49
- * literals are filtered out by `isMasked` below.
48
+ * stay (line, column) anchored. Mutations inside comments, string literals,
49
+ * and template literals (single- or multi-line, interpolation contents
50
+ * excepted) are filtered out by the cross-line scanner `computeLineMasks`
51
+ * below.
50
52
  */
51
53
  export declare function planMutations(file: string, source: string, opts?: PlanMutationsOptions): MutationPlanItem[];
52
54
  /**
@@ -66,7 +68,7 @@ export declare function parseMutationReport(text: string): {
66
68
  file: string;
67
69
  line: number;
68
70
  kind: string;
69
- status: 'killed' | 'survived' | 'skipped';
71
+ status: 'killed' | 'survived' | 'skipped' | 'killed-by-hang';
70
72
  evidence?: string | undefined;
71
73
  }>;
72
74
  summary?: string | undefined;