@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/coordination/director.d.ts +7 -0
- package/dist/coordination/explore-companion.d.ts +9 -6
- package/dist/coordination/index.js +331 -59
- package/dist/coordination/mutation-engine.d.ts +5 -3
- package/dist/core/index.js +39 -9
- package/dist/defaults/index.js +479 -101
- package/dist/execution/index.js +27 -9
- package/dist/hq/index.js +45 -5
- package/dist/index.js +640 -131
- package/dist/infrastructure/index.js +22 -3
- package/dist/observability/index.js +1 -1
- package/dist/plugin/index.js +113 -12
- package/dist/prompts/index.js +360 -3
- package/dist/security/auto-approve-policy.d.ts +2 -2
- package/dist/security/index.js +177 -50
- package/dist/security/permission-helpers.d.ts +11 -0
- package/dist/security/permission-policy.d.ts +10 -1
- package/dist/security/yolo-risk.d.ts +17 -0
- package/dist/session-catalog/index.js +32 -2
- package/dist/session-catalog/project-server.js +32 -2
- package/dist/skills/index.js +39 -6
- package/dist/storage/index.js +44 -3
- package/dist/types/tool.d.ts +15 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +54 -4
- package/dist/utils/terminal-sanitize.d.ts +41 -0
- package/dist/utils/tool-subject.d.ts +1 -1
- package/instructions/agents/chaos-monkey.md +5 -1
- package/package.json +4 -4
|
@@ -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
|
-
//
|
|
10551
|
-
//
|
|
10552
|
-
//
|
|
10553
|
-
//
|
|
10554
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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("//")
|
|
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 (
|
|
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
|
|
14424
|
-
|
|
14425
|
-
|
|
14426
|
-
|
|
14427
|
-
|
|
14428
|
-
|
|
14429
|
-
|
|
14430
|
-
|
|
14431
|
-
|
|
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
|
-
|
|
14434
|
-
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
14630
|
-
const
|
|
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
|
|
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
|
-
|
|
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"]
|
|
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(
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
|
14726
|
-
const matched =
|
|
14727
|
-
|
|
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)
|
|
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
|
|
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,
|
|
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
|
|
49
|
-
* literals
|
|
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;
|