@wrongstack/core 0.308.7 → 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/director-toolset.d.ts +2 -2
- package/dist/coordination/director-mutation-test-tool.d.ts +29 -0
- package/dist/coordination/director-tools.d.ts +2 -0
- package/dist/coordination/director.d.ts +16 -0
- package/dist/coordination/explore-companion.d.ts +9 -6
- package/dist/coordination/fleet.d.ts +12 -0
- package/dist/coordination/index.d.ts +1 -1
- package/dist/coordination/index.js +990 -61
- package/dist/coordination/mail-tools.d.ts +10 -6
- package/dist/coordination/mailbox-codecs.d.ts +31 -0
- package/dist/coordination/multi-agent-coordinator.d.ts +14 -0
- package/dist/coordination/multi-agent-timeout.d.ts +11 -1
- package/dist/coordination/mutation-engine.d.ts +76 -0
- package/dist/coordination/subagent-budget.d.ts +54 -0
- package/dist/coordination/subagent-finish.d.ts +78 -0
- package/dist/core/index.js +58 -13
- package/dist/defaults/index.js +1132 -106
- package/dist/execution/index.js +260 -20
- package/dist/hq/index.js +45 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1456 -263
- package/dist/infrastructure/index.js +22 -3
- package/dist/kernel/events/agent-events.d.ts +31 -2
- package/dist/models/index.js +11 -1
- 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/index.d.ts +1 -1
- package/dist/types/index.js +14 -0
- package/dist/types/multi-agent.d.ts +15 -0
- package/dist/types/provider.d.ts +29 -1
- 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 +61 -0
- package/package.json +4 -4
package/dist/defaults/index.js
CHANGED
|
@@ -356,6 +356,46 @@ function createMessage(type, from, payload, to) {
|
|
|
356
356
|
};
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
+
// src/core/btw.ts
|
|
360
|
+
var META_KEY = "_btwNotes";
|
|
361
|
+
var MAX_PENDING = 20;
|
|
362
|
+
function readQueue(ctx) {
|
|
363
|
+
const raw = ctx.meta[META_KEY];
|
|
364
|
+
return Array.isArray(raw) ? raw : [];
|
|
365
|
+
}
|
|
366
|
+
function setBtwNote(ctx, text) {
|
|
367
|
+
const trimmed = text.trim();
|
|
368
|
+
if (!trimmed) return readQueue(ctx).length;
|
|
369
|
+
const next = [...readQueue(ctx), trimmed].slice(-MAX_PENDING);
|
|
370
|
+
ctx.meta[META_KEY] = next;
|
|
371
|
+
return next.length;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/coordination/subagent-finish.ts
|
|
375
|
+
var SUBAGENT_FINISH_REQUESTED_EVENT = "subagent.finish_requested";
|
|
376
|
+
var DEFAULT_SUBAGENT_FINISH_GRACE_MS = 12e4;
|
|
377
|
+
function resolveGracefulFinish(config) {
|
|
378
|
+
const raw = config.gracefulFinish;
|
|
379
|
+
if (raw === void 0 || raw === false) return void 0;
|
|
380
|
+
if (raw === true) return { graceMs: DEFAULT_SUBAGENT_FINISH_GRACE_MS };
|
|
381
|
+
const graceMs = typeof raw.graceMs === "number" && Number.isFinite(raw.graceMs) && raw.graceMs > 0 ? Math.floor(raw.graceMs) : DEFAULT_SUBAGENT_FINISH_GRACE_MS;
|
|
382
|
+
return { graceMs };
|
|
383
|
+
}
|
|
384
|
+
function buildSubagentFinishNotice(input) {
|
|
385
|
+
const localTime = new Date(input.deadlineMs).toISOString();
|
|
386
|
+
const seconds = Math.max(1, Math.round(input.graceMs / 1e3));
|
|
387
|
+
const timeLeft = input.graceMs > 0 ? `You have roughly ${seconds} seconds (until ${localTime}) of legitimate working time left.` : `Your working-time window is already spent (deadline was ${localTime}) \u2014 finish now.`;
|
|
388
|
+
return [
|
|
389
|
+
"[SUBAGENT FINISH] The leader agent has finished its work.",
|
|
390
|
+
`Reason: ${input.reason}`,
|
|
391
|
+
timeLeft,
|
|
392
|
+
"Finish your task now, in this turn: complete the thought you are working on, stop",
|
|
393
|
+
"starting new tool calls unless one is strictly required to finish, and write your",
|
|
394
|
+
"final answer or report as your final output, then end your turn.",
|
|
395
|
+
"Do not restart the task and do not begin new work."
|
|
396
|
+
].join("\n");
|
|
397
|
+
}
|
|
398
|
+
|
|
359
399
|
// src/coordination/subagent-budget.ts
|
|
360
400
|
var TIMEOUT_PREEMPT_FRACTION = 0.85;
|
|
361
401
|
var DECISION_TIMEOUT_MS = 6e4;
|
|
@@ -413,6 +453,82 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
413
453
|
this.limits.idleTimeoutMs = ext.idleTimeoutMs;
|
|
414
454
|
}
|
|
415
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* Graceful-finish state (see coordination/subagent-finish.ts).
|
|
458
|
+
* `_finishNotified` guards the single in-band emission; `_grace` records a
|
|
459
|
+
* granted working-time extension past the original wall-clock deadline.
|
|
460
|
+
* They are separate because the two callers want different semantics:
|
|
461
|
+
* the watchdog grants grace at the deadline crossing (notify + extend),
|
|
462
|
+
* while an explicit leader-finished request only notifies — a subagent
|
|
463
|
+
* well inside its budget keeps its full legitimate working time and simply
|
|
464
|
+
* accelerates.
|
|
465
|
+
*/
|
|
466
|
+
_finishNotified = false;
|
|
467
|
+
_grace = null;
|
|
468
|
+
/** True once the in-band finish notification has been emitted. */
|
|
469
|
+
get finishNotified() {
|
|
470
|
+
return this._finishNotified;
|
|
471
|
+
}
|
|
472
|
+
/** True once a grace window has been granted past the original deadline. */
|
|
473
|
+
get graceGranted() {
|
|
474
|
+
return this._grace !== null;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Notify the subagent in-band to finish its task in its own turn:
|
|
478
|
+
* `subagent.finish_requested` is emitted on the wired EventBus and the
|
|
479
|
+
* agent loop folds the notice into the conversation between tool batches.
|
|
480
|
+
* Nothing aborts — this is a notification, never an interrupt.
|
|
481
|
+
*
|
|
482
|
+
* `opts.graceMs` additionally extends the wall-clock ceiling by that window
|
|
483
|
+
* (used by the watchdog at a deadline crossing, so the model gets working
|
|
484
|
+
* time instead of a kill). Omit it to notify without touching the budget —
|
|
485
|
+
* the subagent keeps its existing time budget and just accelerates.
|
|
486
|
+
*
|
|
487
|
+
* Returns `true` when this call did something (emitted the notification
|
|
488
|
+
* and/or granted grace); `false` when there was nothing to do (already
|
|
489
|
+
* notified, grace already granted, no EventBus wired, budget not started).
|
|
490
|
+
*/
|
|
491
|
+
notifyFinish(reason, opts, now = Date.now) {
|
|
492
|
+
if (!this._events) return false;
|
|
493
|
+
if (this.startTime === null) return false;
|
|
494
|
+
const shouldEmit = !this._finishNotified;
|
|
495
|
+
const rawGrace = opts?.graceMs;
|
|
496
|
+
const shouldGrant = rawGrace !== void 0 && this._grace === null;
|
|
497
|
+
if (!shouldEmit && !shouldGrant) return false;
|
|
498
|
+
let grantedGraceMs = 0;
|
|
499
|
+
let graceDeadlineMs;
|
|
500
|
+
if (shouldGrant && rawGrace !== void 0) {
|
|
501
|
+
grantedGraceMs = Number.isFinite(rawGrace) && rawGrace > 0 ? Math.floor(rawGrace) : DEFAULT_SUBAGENT_FINISH_GRACE_MS;
|
|
502
|
+
graceDeadlineMs = now() + grantedGraceMs;
|
|
503
|
+
this._grace = { deadlineMs: graceDeadlineMs, graceMs: grantedGraceMs };
|
|
504
|
+
this.patchLimits({ timeoutMs: graceDeadlineMs - this.startTime });
|
|
505
|
+
}
|
|
506
|
+
if (shouldEmit) {
|
|
507
|
+
this._finishNotified = true;
|
|
508
|
+
const effectiveDeadlineMs = graceDeadlineMs ?? (this.limits.timeoutMs !== void 0 ? this.startTime + this.limits.timeoutMs : now() + DEFAULT_SUBAGENT_FINISH_GRACE_MS);
|
|
509
|
+
const effectiveGraceMs = Math.max(0, effectiveDeadlineMs - now());
|
|
510
|
+
const subagentId = this._subagentId;
|
|
511
|
+
this._events.emit(SUBAGENT_FINISH_REQUESTED_EVENT, {
|
|
512
|
+
// Omitted entirely when the budget was built without an id — an
|
|
513
|
+
// empty string is an address that matches nothing.
|
|
514
|
+
...subagentId !== void 0 ? { subagentId } : {},
|
|
515
|
+
reason,
|
|
516
|
+
deadlineMs: effectiveDeadlineMs,
|
|
517
|
+
graceMs: effectiveGraceMs,
|
|
518
|
+
notice: buildSubagentFinishNotice({
|
|
519
|
+
reason,
|
|
520
|
+
deadlineMs: effectiveDeadlineMs,
|
|
521
|
+
graceMs: effectiveGraceMs
|
|
522
|
+
})
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
/** Epoch ms by which the subagent should have produced its final output,
|
|
528
|
+
* once a grace window was granted. Undefined before that. */
|
|
529
|
+
get finishDeadlineMs() {
|
|
530
|
+
return this._grace?.deadlineMs;
|
|
531
|
+
}
|
|
416
532
|
iterations = 0;
|
|
417
533
|
toolCalls = 0;
|
|
418
534
|
tokenInput = 0;
|
|
@@ -428,6 +544,10 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
428
544
|
lastActivityTime = null;
|
|
429
545
|
_onThreshold;
|
|
430
546
|
_sessionId;
|
|
547
|
+
/** Owning subagent id — used to address the graceful-finish event. */
|
|
548
|
+
_subagentId;
|
|
549
|
+
/** True when only the coordinator watchdog may enforce wall-clock limits. */
|
|
550
|
+
_wallClockWatchdogOwned;
|
|
431
551
|
/**
|
|
432
552
|
* Hard cap on how long `_negotiateExtension` waits for the coordinator to
|
|
433
553
|
* respond before defaulting to 'stop'. Without this fallback an absent
|
|
@@ -499,6 +619,8 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
499
619
|
constructor(limits = {}, mode = "auto", options = {}) {
|
|
500
620
|
this._mode = mode;
|
|
501
621
|
this._sessionId = options.sessionId;
|
|
622
|
+
this._subagentId = options.subagentId;
|
|
623
|
+
this._wallClockWatchdogOwned = options.wallClockWatchdogOwned === true;
|
|
502
624
|
this.limits = { ...limits };
|
|
503
625
|
}
|
|
504
626
|
currentSessionId() {
|
|
@@ -577,7 +699,7 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
577
699
|
if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
|
|
578
700
|
exceeded.push({ kind: "idle_timeout", used: idle, limit: this.limits.idleTimeoutMs });
|
|
579
701
|
}
|
|
580
|
-
const wallOwnedByWatchdog = this._onThreshold !== void 0 && this._watchdogActive === this.limits.timeoutMs;
|
|
702
|
+
const wallOwnedByWatchdog = this._wallClockWatchdogOwned || this._onThreshold !== void 0 && this._watchdogActive === this.limits.timeoutMs;
|
|
581
703
|
if (this.limits.timeoutMs !== void 0 && elapsedMs2 > this.limits.timeoutMs && !wallOwnedByWatchdog) {
|
|
582
704
|
exceeded.push({ kind: "timeout", used: elapsedMs2, limit: this.limits.timeoutMs });
|
|
583
705
|
}
|
|
@@ -776,7 +898,7 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
776
898
|
if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
|
|
777
899
|
const elapsed = Date.now() - this.startTime;
|
|
778
900
|
const wallSkipped = this._onThreshold !== void 0 && this._watchdogActive !== void 0 && timeoutMs !== void 0 && this._watchdogActive === timeoutMs;
|
|
779
|
-
const wallTripped = wallSkipped ? false : timeoutMs !== void 0 && elapsed > timeoutMs;
|
|
901
|
+
const wallTripped = this._wallClockWatchdogOwned || wallSkipped ? false : timeoutMs !== void 0 && elapsed > timeoutMs;
|
|
780
902
|
const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
|
|
781
903
|
if (!wallTripped && !idleTripped) return;
|
|
782
904
|
void this.checkLimits(elapsed);
|
|
@@ -1178,6 +1300,14 @@ function makeAgentSubagentRunner(opts) {
|
|
|
1178
1300
|
);
|
|
1179
1301
|
const onParentAbort = () => aborter.abort();
|
|
1180
1302
|
ctx.signal.addEventListener("abort", onParentAbort);
|
|
1303
|
+
if (resolveGracefulFinish(ctx.config)) {
|
|
1304
|
+
unsub.push(
|
|
1305
|
+
events.on("subagent.finish_requested", (e) => {
|
|
1306
|
+
if (e.subagentId && e.subagentId !== ctx.subagentId) return;
|
|
1307
|
+
setBtwNote(agent.ctx, e.notice);
|
|
1308
|
+
})
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1181
1311
|
let result;
|
|
1182
1312
|
try {
|
|
1183
1313
|
result = await agent.run(format(task, ctx.config), { signal: aborter.signal });
|
|
@@ -5440,6 +5570,22 @@ var EXPLORE_COMPANION_AGENT = {
|
|
|
5440
5570
|
textStream: "silent",
|
|
5441
5571
|
toolStream: "silent"
|
|
5442
5572
|
};
|
|
5573
|
+
var CHAOS_MONKEY_AGENT = {
|
|
5574
|
+
...defineAgent("chaos-monkey", "Chaos Monkey"),
|
|
5575
|
+
tools: [...TOOLS.build],
|
|
5576
|
+
skillNames: ["testing", "typescript-strict"],
|
|
5577
|
+
spawnBudgetExempt: true,
|
|
5578
|
+
// Run in the live checkout: mutation targets are usually freshly
|
|
5579
|
+
// written and uncommitted — a worktree spawned from HEAD would not
|
|
5580
|
+
// contain them and every mutant would drift. The mutation_test tool
|
|
5581
|
+
// honors this value as its default; callers can still override per
|
|
5582
|
+
// call via its `chaosWorktree` input when targets are committed and
|
|
5583
|
+
// isolation is wanted.
|
|
5584
|
+
worktree: "off",
|
|
5585
|
+
// Report travels via submit_result + final text, not the leader's stream.
|
|
5586
|
+
textStream: "silent",
|
|
5587
|
+
toolStream: "silent"
|
|
5588
|
+
};
|
|
5443
5589
|
var CRITIC_AGENT = defineAgent("critic", "Critic");
|
|
5444
5590
|
var GENERIC_AGENT = defineAgent("generic", "Generic Project Agent");
|
|
5445
5591
|
function withDispatchMetadata(definition) {
|
|
@@ -5459,6 +5605,7 @@ var FLEET_ROSTER = {
|
|
|
5459
5605
|
generic: GENERIC_AGENT,
|
|
5460
5606
|
"shadow-agent": SHADOW_AGENT,
|
|
5461
5607
|
"explore-companion": EXPLORE_COMPANION_AGENT,
|
|
5608
|
+
"chaos-monkey": CHAOS_MONKEY_AGENT,
|
|
5462
5609
|
...Object.fromEntries(
|
|
5463
5610
|
ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, withDispatchMetadata(d)])
|
|
5464
5611
|
)
|
|
@@ -5489,6 +5636,16 @@ var FLEET_ROSTER_BUDGETS = {
|
|
|
5489
5636
|
maxTokens: 96e3,
|
|
5490
5637
|
maxCostUsd: 0.5
|
|
5491
5638
|
},
|
|
5639
|
+
"chaos-monkey": {
|
|
5640
|
+
// A mutation pass is many short apply/run/restore cycles — per-mutant
|
|
5641
|
+
// work is tiny, but a large plan (25 mutants/file × N files) needs
|
|
5642
|
+
// headroom. Idle-based reaping covers a stalled pass.
|
|
5643
|
+
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
|
5644
|
+
maxIterations: 2e3,
|
|
5645
|
+
maxToolCalls: 6e3,
|
|
5646
|
+
maxTokens: 96e3,
|
|
5647
|
+
maxCostUsd: 0.5
|
|
5648
|
+
},
|
|
5492
5649
|
...Object.fromEntries(
|
|
5493
5650
|
ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.budget])
|
|
5494
5651
|
)
|
|
@@ -6228,7 +6385,7 @@ async function readSubagentPartial(opts, subagentId) {
|
|
|
6228
6385
|
}
|
|
6229
6386
|
|
|
6230
6387
|
// src/coordination/director.ts
|
|
6231
|
-
import { randomUUID as
|
|
6388
|
+
import { randomUUID as randomUUID13 } from "node:crypto";
|
|
6232
6389
|
import * as fsp25 from "node:fs/promises";
|
|
6233
6390
|
|
|
6234
6391
|
// src/core/instruction-template.ts
|
|
@@ -8244,7 +8401,7 @@ ${JSON.stringify(result.result, null, 2)}
|
|
|
8244
8401
|
};
|
|
8245
8402
|
|
|
8246
8403
|
// src/coordination/director-tools.ts
|
|
8247
|
-
import { randomUUID as
|
|
8404
|
+
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
8248
8405
|
import {
|
|
8249
8406
|
completeKanbanDispatch,
|
|
8250
8407
|
failKanbanDispatch,
|
|
@@ -9444,6 +9601,626 @@ function excerpt(text, max) {
|
|
|
9444
9601
|
...(truncated)`;
|
|
9445
9602
|
}
|
|
9446
9603
|
|
|
9604
|
+
// src/coordination/director-mutation-test-tool.ts
|
|
9605
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
9606
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
9607
|
+
import { isAbsolute as isAbsolute2, join as join10 } from "node:path";
|
|
9608
|
+
|
|
9609
|
+
// src/coordination/mutation-engine.ts
|
|
9610
|
+
var TOKEN_PATTERNS = [
|
|
9611
|
+
{
|
|
9612
|
+
kind: "relax-boundary",
|
|
9613
|
+
// `>` not followed by `=` and not part of `=>` or `>>`; require code-ish
|
|
9614
|
+
// context on both sides so generic text (JSX, strings) is not touched.
|
|
9615
|
+
regex: /(?<=[\w\)\]\}'"`])\s*\x20?(?<op>>(?!=|>))/g,
|
|
9616
|
+
replace: () => ">="
|
|
9617
|
+
},
|
|
9618
|
+
{
|
|
9619
|
+
kind: "tighten-boundary",
|
|
9620
|
+
regex: /(?<=[\w\)\]\}'"`])\s*\x20?(?<op>>=)/g,
|
|
9621
|
+
replace: () => ">"
|
|
9622
|
+
},
|
|
9623
|
+
{
|
|
9624
|
+
kind: "arith-plus-to-minus",
|
|
9625
|
+
// `+` between operands (binary), not `++`, unary `+x`, or `+=`.
|
|
9626
|
+
regex: /(?<=[\w\)\]\}'"`])\s*\x20?(?<op>\+(?!\+|=))/g,
|
|
9627
|
+
replace: () => "-"
|
|
9628
|
+
},
|
|
9629
|
+
{
|
|
9630
|
+
kind: "arith-minus-to-plus",
|
|
9631
|
+
// Binary `-` between operands, not `--`, `-=` or negative-number literal.
|
|
9632
|
+
regex: /(?<=[\w\)\]\}'"`])\s*\x20?(?<op>-(?!-|=))/g,
|
|
9633
|
+
replace: () => "+"
|
|
9634
|
+
},
|
|
9635
|
+
{
|
|
9636
|
+
kind: "negate-boolean",
|
|
9637
|
+
// Standalone boolean literals used as values, not property names.
|
|
9638
|
+
regex: /(?<![.\w$])(?<op>true|false)(?![\w$])/g,
|
|
9639
|
+
replace: (m) => m === "true" ? "false" : "true"
|
|
9640
|
+
},
|
|
9641
|
+
{
|
|
9642
|
+
kind: "return-null",
|
|
9643
|
+
// `return <expr>;` where expr is not already null/undefined/void.
|
|
9644
|
+
regex: /(?<indent>\breturn\b)(?<expr>\s+[^;{}\n]+?)\s*;/g,
|
|
9645
|
+
replace: () => "return null;",
|
|
9646
|
+
endpointsInCode: true
|
|
9647
|
+
}
|
|
9648
|
+
];
|
|
9649
|
+
function planMutations(file, source, opts = {}) {
|
|
9650
|
+
const maxPerFile = opts.maxPerFile ?? 25;
|
|
9651
|
+
const out = [];
|
|
9652
|
+
const lines = source.split("\n");
|
|
9653
|
+
const masks = computeLineMasks(source);
|
|
9654
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
9655
|
+
const line = lines[lineIdx];
|
|
9656
|
+
const t = line.trim();
|
|
9657
|
+
if (t.startsWith("//")) continue;
|
|
9658
|
+
const codeRanges = masks[lineIdx];
|
|
9659
|
+
const inCode = (start) => codeRanges.some(([s, e]) => start >= s && start < e);
|
|
9660
|
+
for (const pattern of TOKEN_PATTERNS) {
|
|
9661
|
+
pattern.regex.lastIndex = 0;
|
|
9662
|
+
let m;
|
|
9663
|
+
while ((m = pattern.regex.exec(line)) !== null) {
|
|
9664
|
+
const token = m.groups?.["op"] ?? m[0];
|
|
9665
|
+
const tokenStart = m.index + m[0].indexOf(token);
|
|
9666
|
+
if (!inCode(tokenStart)) continue;
|
|
9667
|
+
if (pattern.endpointsInCode && !inCode(tokenStart + token.length - 1)) continue;
|
|
9668
|
+
const original = line.slice(tokenStart, tokenStart + token.length);
|
|
9669
|
+
const replacement = pattern.replace(token);
|
|
9670
|
+
if (replacement === original) continue;
|
|
9671
|
+
out.push({
|
|
9672
|
+
id: `${pattern.kind}#${lineIdx + 1}#${tokenStart + 1}`,
|
|
9673
|
+
kind: pattern.kind,
|
|
9674
|
+
file,
|
|
9675
|
+
line: lineIdx + 1,
|
|
9676
|
+
column: tokenStart + 1,
|
|
9677
|
+
original,
|
|
9678
|
+
replacement
|
|
9679
|
+
});
|
|
9680
|
+
}
|
|
9681
|
+
}
|
|
9682
|
+
if (out.length >= maxPerFile) break;
|
|
9683
|
+
}
|
|
9684
|
+
return out.slice(0, maxPerFile);
|
|
9685
|
+
}
|
|
9686
|
+
function computeLineMasks(source) {
|
|
9687
|
+
const lines = source.split("\n");
|
|
9688
|
+
const masks = lines.map(() => []);
|
|
9689
|
+
const stack = [{ kind: "code", depth: 0, parens: [] }];
|
|
9690
|
+
let inBlockComment = false;
|
|
9691
|
+
let lastToken = null;
|
|
9692
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
9693
|
+
const line = lines[lineIdx];
|
|
9694
|
+
const ranges = masks[lineIdx];
|
|
9695
|
+
let runStart = null;
|
|
9696
|
+
const closeRun = (end) => {
|
|
9697
|
+
if (runStart !== null && end > runStart) ranges.push([runStart, end]);
|
|
9698
|
+
runStart = null;
|
|
9699
|
+
};
|
|
9700
|
+
let i = 0;
|
|
9701
|
+
if (inBlockComment) {
|
|
9702
|
+
const close = line.indexOf("*/");
|
|
9703
|
+
if (close === -1) continue;
|
|
9704
|
+
inBlockComment = false;
|
|
9705
|
+
i = close + 2;
|
|
9706
|
+
}
|
|
9707
|
+
while (i < line.length) {
|
|
9708
|
+
const top = stack[stack.length - 1];
|
|
9709
|
+
const c = line[i];
|
|
9710
|
+
if (top.kind === "template") {
|
|
9711
|
+
if (c === "\\") {
|
|
9712
|
+
i += 2;
|
|
9713
|
+
continue;
|
|
9714
|
+
}
|
|
9715
|
+
if (c === "`") {
|
|
9716
|
+
stack.pop();
|
|
9717
|
+
lastToken = "`";
|
|
9718
|
+
i++;
|
|
9719
|
+
continue;
|
|
9720
|
+
}
|
|
9721
|
+
if (c === "$" && line[i + 1] === "{") {
|
|
9722
|
+
stack.push({ kind: "code", depth: 0, parens: [] });
|
|
9723
|
+
lastToken = "${";
|
|
9724
|
+
i += 2;
|
|
9725
|
+
continue;
|
|
9726
|
+
}
|
|
9727
|
+
i++;
|
|
9728
|
+
continue;
|
|
9729
|
+
}
|
|
9730
|
+
if (/[\w$]/.test(c)) {
|
|
9731
|
+
let j = i + 1;
|
|
9732
|
+
while (j < line.length && /[\w$]/.test(line[j])) j++;
|
|
9733
|
+
lastToken = line.slice(i, j);
|
|
9734
|
+
if (runStart === null) runStart = i;
|
|
9735
|
+
i = j;
|
|
9736
|
+
continue;
|
|
9737
|
+
}
|
|
9738
|
+
if (c === "'" || c === '"') {
|
|
9739
|
+
closeRun(i);
|
|
9740
|
+
i++;
|
|
9741
|
+
while (i < line.length && line[i] !== c) {
|
|
9742
|
+
if (line[i] === "\\") i++;
|
|
9743
|
+
i++;
|
|
9744
|
+
}
|
|
9745
|
+
i++;
|
|
9746
|
+
lastToken = c;
|
|
9747
|
+
continue;
|
|
9748
|
+
}
|
|
9749
|
+
if (c === "`") {
|
|
9750
|
+
closeRun(i);
|
|
9751
|
+
stack.push({ kind: "template", depth: 0, parens: [] });
|
|
9752
|
+
i++;
|
|
9753
|
+
continue;
|
|
9754
|
+
}
|
|
9755
|
+
if (c === "/" && line[i + 1] === "/") {
|
|
9756
|
+
closeRun(i);
|
|
9757
|
+
break;
|
|
9758
|
+
}
|
|
9759
|
+
if (c === "/" && line[i + 1] === "*") {
|
|
9760
|
+
closeRun(i);
|
|
9761
|
+
const close = line.indexOf("*/", i + 2);
|
|
9762
|
+
if (close === -1) {
|
|
9763
|
+
inBlockComment = true;
|
|
9764
|
+
break;
|
|
9765
|
+
}
|
|
9766
|
+
i = close + 2;
|
|
9767
|
+
continue;
|
|
9768
|
+
}
|
|
9769
|
+
if (c === "/") {
|
|
9770
|
+
if (!tokenCanEndOperand(lastToken)) {
|
|
9771
|
+
closeRun(i);
|
|
9772
|
+
const next = skipRegexLiteral(line, i);
|
|
9773
|
+
lastToken = next > i + 1 ? "regex" : "/";
|
|
9774
|
+
i = next;
|
|
9775
|
+
continue;
|
|
9776
|
+
}
|
|
9777
|
+
}
|
|
9778
|
+
if (c === "(") {
|
|
9779
|
+
top.parens.push(CONTROL_KEYWORDS.has(lastToken ?? "") ? "control" : "expr");
|
|
9780
|
+
lastToken = c;
|
|
9781
|
+
} else if (c === ")") {
|
|
9782
|
+
const kind = top.parens.pop() ?? "expr";
|
|
9783
|
+
lastToken = kind === "control" ? "control-paren-close" : ")";
|
|
9784
|
+
} else if (c === "{") {
|
|
9785
|
+
top.depth++;
|
|
9786
|
+
lastToken = c;
|
|
9787
|
+
} else if (c === "}") {
|
|
9788
|
+
if (top.depth > 0) {
|
|
9789
|
+
top.depth--;
|
|
9790
|
+
lastToken = c;
|
|
9791
|
+
} else if (stack.length > 1) {
|
|
9792
|
+
closeRun(i);
|
|
9793
|
+
stack.pop();
|
|
9794
|
+
i++;
|
|
9795
|
+
continue;
|
|
9796
|
+
} else {
|
|
9797
|
+
lastToken = c;
|
|
9798
|
+
}
|
|
9799
|
+
} else if (c !== " " && c !== " " && c !== "\r") {
|
|
9800
|
+
lastToken = c;
|
|
9801
|
+
}
|
|
9802
|
+
if (runStart === null) runStart = i;
|
|
9803
|
+
i++;
|
|
9804
|
+
}
|
|
9805
|
+
closeRun(line.length);
|
|
9806
|
+
}
|
|
9807
|
+
return masks;
|
|
9808
|
+
}
|
|
9809
|
+
var KEYWORDS_BEFORE_REGEX = /* @__PURE__ */ new Set([
|
|
9810
|
+
"return",
|
|
9811
|
+
"typeof",
|
|
9812
|
+
"instanceof",
|
|
9813
|
+
"in",
|
|
9814
|
+
"of",
|
|
9815
|
+
"new",
|
|
9816
|
+
"delete",
|
|
9817
|
+
"void",
|
|
9818
|
+
"throw",
|
|
9819
|
+
"case",
|
|
9820
|
+
"do",
|
|
9821
|
+
"else",
|
|
9822
|
+
"yield",
|
|
9823
|
+
"await"
|
|
9824
|
+
]);
|
|
9825
|
+
var CONTROL_KEYWORDS = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "catch", "with", "await"]);
|
|
9826
|
+
function tokenCanEndOperand(token) {
|
|
9827
|
+
if (token === null) return false;
|
|
9828
|
+
if (/^[\w$]+$/.test(token)) return !KEYWORDS_BEFORE_REGEX.has(token);
|
|
9829
|
+
return token === ")" || token === "]" || token === "." || token === '"' || token === "'" || token === "`";
|
|
9830
|
+
}
|
|
9831
|
+
function skipRegexLiteral(line, start) {
|
|
9832
|
+
let i = start + 1;
|
|
9833
|
+
let inClass = false;
|
|
9834
|
+
while (i < line.length) {
|
|
9835
|
+
const ch = line[i];
|
|
9836
|
+
if (ch === "\\") {
|
|
9837
|
+
i += 2;
|
|
9838
|
+
continue;
|
|
9839
|
+
}
|
|
9840
|
+
if (inClass) {
|
|
9841
|
+
if (ch === "]") inClass = false;
|
|
9842
|
+
i++;
|
|
9843
|
+
continue;
|
|
9844
|
+
}
|
|
9845
|
+
if (ch === "[") {
|
|
9846
|
+
inClass = true;
|
|
9847
|
+
i++;
|
|
9848
|
+
continue;
|
|
9849
|
+
}
|
|
9850
|
+
if (ch === "/") {
|
|
9851
|
+
i++;
|
|
9852
|
+
break;
|
|
9853
|
+
}
|
|
9854
|
+
if (ch === "\n" || ch === "\r") return line.length;
|
|
9855
|
+
i++;
|
|
9856
|
+
}
|
|
9857
|
+
while (i < line.length && /[a-z]/.test(line[i])) i++;
|
|
9858
|
+
return i;
|
|
9859
|
+
}
|
|
9860
|
+
function parseMutationReport(text) {
|
|
9861
|
+
const candidates = [];
|
|
9862
|
+
const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
9863
|
+
if (fence?.[1]) candidates.push(fence[1].trim());
|
|
9864
|
+
const firstBrace = text.indexOf("{");
|
|
9865
|
+
if (firstBrace >= 0) candidates.push(extractBalancedObject(text, firstBrace));
|
|
9866
|
+
for (const candidate of candidates) {
|
|
9867
|
+
if (!candidate) continue;
|
|
9868
|
+
try {
|
|
9869
|
+
const parsed = JSON.parse(candidate);
|
|
9870
|
+
if (!Array.isArray(parsed.mutants)) continue;
|
|
9871
|
+
return {
|
|
9872
|
+
mutants: parsed.mutants.map(normalizeMutantEntry).filter((x) => Boolean(x)),
|
|
9873
|
+
summary: typeof parsed.summary === "string" ? parsed.summary : void 0
|
|
9874
|
+
};
|
|
9875
|
+
} catch {
|
|
9876
|
+
}
|
|
9877
|
+
}
|
|
9878
|
+
return void 0;
|
|
9879
|
+
}
|
|
9880
|
+
function extractBalancedObject(text, start) {
|
|
9881
|
+
let depth = 0;
|
|
9882
|
+
let inString = false;
|
|
9883
|
+
let escaped = false;
|
|
9884
|
+
for (let i = start; i < text.length; i++) {
|
|
9885
|
+
const c = text[i];
|
|
9886
|
+
if (escaped) {
|
|
9887
|
+
escaped = false;
|
|
9888
|
+
continue;
|
|
9889
|
+
}
|
|
9890
|
+
if (c === "\\") {
|
|
9891
|
+
escaped = true;
|
|
9892
|
+
continue;
|
|
9893
|
+
}
|
|
9894
|
+
if (c === '"') inString = !inString;
|
|
9895
|
+
if (inString) continue;
|
|
9896
|
+
if (c === "{") depth++;
|
|
9897
|
+
else if (c === "}") {
|
|
9898
|
+
depth--;
|
|
9899
|
+
if (depth === 0) return text.slice(start, i + 1);
|
|
9900
|
+
}
|
|
9901
|
+
}
|
|
9902
|
+
return text.slice(start);
|
|
9903
|
+
}
|
|
9904
|
+
function normalizeMutantEntry(value) {
|
|
9905
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
9906
|
+
const rec = value;
|
|
9907
|
+
const id = typeof rec["id"] === "string" ? rec["id"] : void 0;
|
|
9908
|
+
const status = rec["status"];
|
|
9909
|
+
if (!id || status !== "killed" && status !== "survived" && status !== "skipped" && status !== "killed-by-hang") {
|
|
9910
|
+
return void 0;
|
|
9911
|
+
}
|
|
9912
|
+
return {
|
|
9913
|
+
id,
|
|
9914
|
+
file: typeof rec["file"] === "string" ? rec["file"] : "",
|
|
9915
|
+
line: typeof rec["line"] === "number" ? rec["line"] : 0,
|
|
9916
|
+
kind: typeof rec["kind"] === "string" ? rec["kind"] : "",
|
|
9917
|
+
status,
|
|
9918
|
+
evidence: typeof rec["evidence"] === "string" ? rec["evidence"] : void 0
|
|
9919
|
+
};
|
|
9920
|
+
}
|
|
9921
|
+
|
|
9922
|
+
// src/coordination/director-mutation-test-tool.ts
|
|
9923
|
+
var DEFAULT_MAX_PER_FILE = 10;
|
|
9924
|
+
var DEFAULT_MAX_STRENGTHEN_ATTEMPTS = 2;
|
|
9925
|
+
var CHAOS_ROLE = "chaos-monkey";
|
|
9926
|
+
function makeMutationTestTool(director, roster, opts = {}) {
|
|
9927
|
+
return {
|
|
9928
|
+
name: "mutation_test",
|
|
9929
|
+
description: "Chaos Monkey mutation testing: deterministically sabotage boundary conditions in the target code (> to >=, + to -, boolean flips, return null), re-run the tests per mutant, and report which mutants were killed. Surviving mutants mean the tests are weak \u2014 optionally loop a strengthen-tests repair until they die.",
|
|
9930
|
+
usageHint: "Use after writing new code AND its tests, before delivering. Pass targets (files) and testCommand. Provide repairSubagentId to auto-strengthen weak tests. Survivors that persist are reported as suspected-equivalent.",
|
|
9931
|
+
permission: "auto",
|
|
9932
|
+
mutating: false,
|
|
9933
|
+
capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
|
|
9934
|
+
inputSchema: {
|
|
9935
|
+
type: "object",
|
|
9936
|
+
properties: {
|
|
9937
|
+
targets: {
|
|
9938
|
+
type: "array",
|
|
9939
|
+
items: { type: "string" },
|
|
9940
|
+
description: "Project-relative (or absolute) source files to mutate. Keep to files changed by the current task."
|
|
9941
|
+
},
|
|
9942
|
+
testCommand: {
|
|
9943
|
+
type: "string",
|
|
9944
|
+
description: 'Exact command that runs the relevant tests, e.g. "pnpm exec vitest run packages/core/tests/coordination/mutation-engine.test.ts".'
|
|
9945
|
+
},
|
|
9946
|
+
cwd: { type: "string", description: "Working directory for the test command." },
|
|
9947
|
+
maxPerFile: {
|
|
9948
|
+
type: "number",
|
|
9949
|
+
minimum: 1,
|
|
9950
|
+
maximum: 25,
|
|
9951
|
+
description: "Mutant cap per file per pass. Default 10."
|
|
9952
|
+
},
|
|
9953
|
+
maxStrengthenAttempts: {
|
|
9954
|
+
type: "number",
|
|
9955
|
+
minimum: 0,
|
|
9956
|
+
maximum: 5,
|
|
9957
|
+
description: "Strengthen\u2192re-verify rounds. Default 2 when repairSubagentId is set, else 0."
|
|
9958
|
+
},
|
|
9959
|
+
repairSubagentId: {
|
|
9960
|
+
type: "string",
|
|
9961
|
+
description: "Subagent that owns the tests. When set and mutants survive, it receives a strengthen-tests task and the survivors are re-verified."
|
|
9962
|
+
},
|
|
9963
|
+
chaosWorktree: {
|
|
9964
|
+
anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
|
|
9965
|
+
description: "Worktree override for the chaos agent. Defaults to the roster policy for chaos-monkey ('off'), because mutation targets are usually freshly written and uncommitted \u2014 a worktree from HEAD would not contain them and every mutant would drift to skipped. Only pass 'auto' or 'required' when the targets are committed."
|
|
9966
|
+
},
|
|
9967
|
+
timeoutMs: { type: "number", minimum: 1, description: "Per-task timeout for chaos/strengthen/rerun tasks." },
|
|
9968
|
+
reportOnly: {
|
|
9969
|
+
type: "boolean",
|
|
9970
|
+
description: "Skip the strengthen loop even when survivors exist. Default false."
|
|
9971
|
+
}
|
|
9972
|
+
},
|
|
9973
|
+
required: ["targets", "testCommand"],
|
|
9974
|
+
additionalProperties: false
|
|
9975
|
+
},
|
|
9976
|
+
async execute(input, ctx) {
|
|
9977
|
+
const i = normalizeMutationTestInput(input);
|
|
9978
|
+
const root = opts.projectRoot ?? ctx.projectRoot;
|
|
9979
|
+
const plan = buildPlan(i, root);
|
|
9980
|
+
if (plan.length === 0) {
|
|
9981
|
+
return {
|
|
9982
|
+
verdict: "inconclusive",
|
|
9983
|
+
passed: false,
|
|
9984
|
+
error: "No mutable sites found in the given targets (after comment/string filtering)."
|
|
9985
|
+
};
|
|
9986
|
+
}
|
|
9987
|
+
const chaosBase = roster?.[CHAOS_ROLE];
|
|
9988
|
+
if (!chaosBase) {
|
|
9989
|
+
return {
|
|
9990
|
+
verdict: "inconclusive",
|
|
9991
|
+
passed: false,
|
|
9992
|
+
error: "chaos-monkey role missing from the roster \u2014 refusing to spawn a saboteur without its prompt/tools contract. Build the toolset with a roster that includes 'chaos-monkey' (FLEET_ROSTER does)."
|
|
9993
|
+
};
|
|
9994
|
+
}
|
|
9995
|
+
const chaosSubagentId = await director.spawn(
|
|
9996
|
+
makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
|
|
9997
|
+
);
|
|
9998
|
+
const chaosTaskId = await director.assign({
|
|
9999
|
+
id: randomUUID8(),
|
|
10000
|
+
subagentId: chaosSubagentId,
|
|
10001
|
+
description: buildChaosTask(plan, i, 1, []),
|
|
10002
|
+
timeoutMs: i.timeoutMs
|
|
10003
|
+
});
|
|
10004
|
+
const [chaosResult] = await director.awaitTasks([chaosTaskId]);
|
|
10005
|
+
const pass1 = collectOutcomes(chaosResult, plan);
|
|
10006
|
+
const survivors = pass1.filter((m) => m.status === "survived");
|
|
10007
|
+
const maxAttempts = clamp(
|
|
10008
|
+
i.maxStrengthenAttempts ?? (i.repairSubagentId && !i.reportOnly ? DEFAULT_MAX_STRENGTHEN_ATTEMPTS : 0),
|
|
10009
|
+
0,
|
|
10010
|
+
5
|
|
10011
|
+
);
|
|
10012
|
+
const attempts = [];
|
|
10013
|
+
let current = survivors;
|
|
10014
|
+
let rerunUnknowns = [];
|
|
10015
|
+
while (current.length > 0 && attempts.length < maxAttempts && i.repairSubagentId) {
|
|
10016
|
+
const attemptNo = attempts.length + 1;
|
|
10017
|
+
const strengthenTaskId = await director.assign({
|
|
10018
|
+
id: randomUUID8(),
|
|
10019
|
+
subagentId: i.repairSubagentId,
|
|
10020
|
+
description: buildStrengthenTask(current, i, attemptNo),
|
|
10021
|
+
timeoutMs: i.timeoutMs
|
|
10022
|
+
});
|
|
10023
|
+
const [strengthenResult] = await director.awaitTasks([strengthenTaskId]);
|
|
10024
|
+
if (strengthenResult?.status !== "success") {
|
|
10025
|
+
attempts.push({
|
|
10026
|
+
attempt: attemptNo,
|
|
10027
|
+
survivorsBefore: current,
|
|
10028
|
+
strengthenResult: strengthenResult ? { taskId: strengthenResult.taskId, status: strengthenResult.status } : void 0,
|
|
10029
|
+
survivorsAfter: current,
|
|
10030
|
+
suspectedEquivalent: []
|
|
10031
|
+
});
|
|
10032
|
+
break;
|
|
10033
|
+
}
|
|
10034
|
+
const survivorPlan = plan.filter((p) => current.some((s) => s.id === p.id));
|
|
10035
|
+
const rerunSubagentId = await director.spawn(
|
|
10036
|
+
makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
|
|
10037
|
+
);
|
|
10038
|
+
const rerunTaskId = await director.assign({
|
|
10039
|
+
id: randomUUID8(),
|
|
10040
|
+
subagentId: rerunSubagentId,
|
|
10041
|
+
description: buildChaosTask(survivorPlan, i, attemptNo + 1, current),
|
|
10042
|
+
timeoutMs: i.timeoutMs
|
|
10043
|
+
});
|
|
10044
|
+
const [rerunResult] = await director.awaitTasks([rerunTaskId]);
|
|
10045
|
+
const passN = collectOutcomes(rerunResult, survivorPlan);
|
|
10046
|
+
const stillSurviving = passN.filter((m) => !isKill(m.status));
|
|
10047
|
+
rerunUnknowns = passN.filter((m) => m.status === "skipped");
|
|
10048
|
+
attempts.push({
|
|
10049
|
+
attempt: attemptNo,
|
|
10050
|
+
survivorsBefore: current,
|
|
10051
|
+
strengthenResult: { taskId: strengthenResult.taskId, status: strengthenResult.status },
|
|
10052
|
+
rerunResult: { taskId: rerunTaskId, status: rerunResult?.status ?? "unknown" },
|
|
10053
|
+
survivorsAfter: stillSurviving,
|
|
10054
|
+
suspectedEquivalent: stillSurviving.filter((m) => m.status === "survived" && current.some((c) => c.id === m.id)).map((m) => m.id)
|
|
10055
|
+
});
|
|
10056
|
+
current = stillSurviving;
|
|
10057
|
+
}
|
|
10058
|
+
const finalSurvivors = current.filter((m) => m.status === "survived");
|
|
10059
|
+
const verifiedCount = pass1.filter((m) => m.status !== "skipped").length;
|
|
10060
|
+
const skippedCount = pass1.filter((m) => m.status === "skipped").length;
|
|
10061
|
+
const rerunUnknownCount = rerunUnknowns.length;
|
|
10062
|
+
const score = plan.length === 0 ? 0 : pass1.filter((m) => isKill(m.status)).length / plan.length;
|
|
10063
|
+
const verdict = verifiedCount === 0 ? "inconclusive" : finalSurvivors.length === 0 ? skippedCount > 0 || rerunUnknownCount > 0 ? "partial" : "pass" : score >= 0.8 ? "partial" : "fail";
|
|
10064
|
+
return {
|
|
10065
|
+
verdict,
|
|
10066
|
+
passed: verdict === "pass",
|
|
10067
|
+
mutationScore: Number.parseFloat(score.toFixed(3)),
|
|
10068
|
+
planned: plan.length,
|
|
10069
|
+
killed: pass1.filter((m) => isKill(m.status)).length,
|
|
10070
|
+
// Breakout of `killed`: how many kills were detected by the test
|
|
10071
|
+
// command hanging rather than by a failing assertion. A subset of
|
|
10072
|
+
// `killed`, surfaced so a director can distinguish a hang-heavy
|
|
10073
|
+
// suite (mutants breaking termination, not assertions) from an
|
|
10074
|
+
// assertion-strong one. hangHeavy = killedByHang === killed.
|
|
10075
|
+
killedByHang: pass1.filter((m) => m.status === "killed-by-hang").length,
|
|
10076
|
+
survived: pass1.filter((m) => m.status === "survived").length,
|
|
10077
|
+
skipped: pass1.filter((m) => m.status === "skipped").length,
|
|
10078
|
+
finalSurvivors: finalSurvivors.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
|
|
10079
|
+
suspectedEquivalent: attempts.flatMap((a) => a.suspectedEquivalent),
|
|
10080
|
+
strengthenAttempts: attempts.length,
|
|
10081
|
+
attempts,
|
|
10082
|
+
chaosTaskId,
|
|
10083
|
+
// Unverified leftovers from the strengthen loop: surfaced so the
|
|
10084
|
+
// caller can see WHICH mutants lack kill evidence, and counted by
|
|
10085
|
+
// the verdict gate above.
|
|
10086
|
+
unverifiedFromRerun: rerunUnknowns.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
|
|
10087
|
+
nextAction: finalSurvivors.length === 0 && rerunUnknownCount === 0 && skippedCount === 0 ? "accept" : attempts.length >= maxAttempts && i.repairSubagentId ? "manual_review_survivors" : "strengthen_tests"
|
|
10088
|
+
};
|
|
10089
|
+
}
|
|
10090
|
+
};
|
|
10091
|
+
}
|
|
10092
|
+
function normalizeMutationTestInput(input) {
|
|
10093
|
+
const raw = input ?? {};
|
|
10094
|
+
const targets = stringArray2(raw["targets"]) ?? [];
|
|
10095
|
+
const testCommand = typeof raw["testCommand"] === "string" ? raw["testCommand"].trim() : "";
|
|
10096
|
+
return {
|
|
10097
|
+
targets: targets.filter(Boolean),
|
|
10098
|
+
testCommand,
|
|
10099
|
+
cwd: typeof raw["cwd"] === "string" && raw["cwd"].trim() ? raw["cwd"].trim() : void 0,
|
|
10100
|
+
maxPerFile: typeof raw["maxPerFile"] === "number" ? raw["maxPerFile"] : void 0,
|
|
10101
|
+
maxStrengthenAttempts: typeof raw["maxStrengthenAttempts"] === "number" ? raw["maxStrengthenAttempts"] : void 0,
|
|
10102
|
+
repairSubagentId: typeof raw["repairSubagentId"] === "string" && raw["repairSubagentId"].trim() ? raw["repairSubagentId"].trim() : void 0,
|
|
10103
|
+
chaosWorktree: normalizeWorktreeOverride(raw["chaosWorktree"]),
|
|
10104
|
+
timeoutMs: typeof raw["timeoutMs"] === "number" ? raw["timeoutMs"] : void 0,
|
|
10105
|
+
reportOnly: raw["reportOnly"] === true
|
|
10106
|
+
};
|
|
10107
|
+
}
|
|
10108
|
+
function clamp(n, lo, hi) {
|
|
10109
|
+
return Math.min(hi, Math.max(lo, n));
|
|
10110
|
+
}
|
|
10111
|
+
function buildPlan(i, projectRoot) {
|
|
10112
|
+
const plan = [];
|
|
10113
|
+
for (const target of i.targets) {
|
|
10114
|
+
const abs = isAbsolute2(target) ? target : join10(projectRoot ?? process.cwd(), target);
|
|
10115
|
+
let source;
|
|
10116
|
+
try {
|
|
10117
|
+
source = readFileSync9(abs, "utf8");
|
|
10118
|
+
} catch {
|
|
10119
|
+
continue;
|
|
10120
|
+
}
|
|
10121
|
+
plan.push(...planMutations(target, source, { maxPerFile: i.maxPerFile ?? DEFAULT_MAX_PER_FILE }));
|
|
10122
|
+
}
|
|
10123
|
+
return plan;
|
|
10124
|
+
}
|
|
10125
|
+
function makeChaosConfig(base, worktree) {
|
|
10126
|
+
return { ...instantiateRosterConfig2(CHAOS_ROLE, base), worktree };
|
|
10127
|
+
}
|
|
10128
|
+
function buildChaosTask(plan, i, pass, priorSurvivors) {
|
|
10129
|
+
const mutants = plan.map(
|
|
10130
|
+
(m) => `- ${m.id} | ${m.file}:${m.line}:${m.column} | ${m.kind} | "${m.original}" -> "${m.replacement}"`
|
|
10131
|
+
).join("\n");
|
|
10132
|
+
const prior = priorSurvivors.length > 0 ? `
|
|
10133
|
+
These mutants survived a previous pass (pass ${pass - 1}) \u2014 re-verify them against the STRENGTHENED tests:
|
|
10134
|
+
${priorSurvivors.map((s) => `- ${s.id} (${s.kind} @ ${s.file}:${s.line})`).join("\n")}` : "";
|
|
10135
|
+
return [
|
|
10136
|
+
"Execute this deterministic mutation plan against the current checkout.",
|
|
10137
|
+
"",
|
|
10138
|
+
"For each mutant, in order:",
|
|
10139
|
+
"1. Apply ONLY that mutation at its exact (file, line, column).",
|
|
10140
|
+
`2. Run the test command: ${i.testCommand}${i.cwd ? ` (cwd: ${i.cwd})` : ""}`,
|
|
10141
|
+
"3. Record killed (tests failed \u2014 quote first failing assertion), survived (suite green), or killed-by-hang (the test command timed out or was aborted \u2014 the mutation broke the suite by non-termination; record the timeout as evidence, do NOT report it as survived).",
|
|
10142
|
+
"4. Restore the file byte-for-byte before the next mutant.",
|
|
10143
|
+
"",
|
|
10144
|
+
"Mutants:",
|
|
10145
|
+
mutants,
|
|
10146
|
+
prior,
|
|
10147
|
+
"",
|
|
10148
|
+
"Rules: one mutation at a time; never stack; if the anchored token no longer matches, mark skipped with the drift as evidence; do not fix or refactor anything; stay inside the plan.",
|
|
10149
|
+
"Finish with submit_result, then repeat the same JSON as your final text."
|
|
10150
|
+
].join("\n");
|
|
10151
|
+
}
|
|
10152
|
+
function buildStrengthenTask(survivors, i, attempt) {
|
|
10153
|
+
const confirmed = survivors.filter((s) => s.status === "survived");
|
|
10154
|
+
const unverified = survivors.filter((s) => s.status === "skipped");
|
|
10155
|
+
const row = (s) => `- ${s.id} | ${s.file}:${s.line} | ${s.kind}${s.evidence ? ` | ${s.evidence}` : ""}`;
|
|
10156
|
+
return [
|
|
10157
|
+
`Strengthen the tests so the mutants below die (attempt ${attempt}).`,
|
|
10158
|
+
"",
|
|
10159
|
+
...confirmed.length > 0 ? [
|
|
10160
|
+
"CONFIRMED SURVIVORS \u2014 each was a deliberate sabotage of production code that the current suite did NOT catch:",
|
|
10161
|
+
...confirmed.map(row),
|
|
10162
|
+
""
|
|
10163
|
+
] : [],
|
|
10164
|
+
...unverified.length > 0 ? [
|
|
10165
|
+
"UNVERIFIED \u2014 these mutations were never actually re-tested (the re-verify pass skipped or did not report them). Do NOT assume the suite misses them: first apply each mutation, run the tests, and confirm it really survives; if the tests already fail, report that instead of writing new assertions.",
|
|
10166
|
+
...unverified.map(row),
|
|
10167
|
+
""
|
|
10168
|
+
] : [],
|
|
10169
|
+
`Test command that must fail under each CONFIRMED mutant: ${i.testCommand}`,
|
|
10170
|
+
"",
|
|
10171
|
+
"For each CONFIRMED survivor add or tighten exactly one assertion that pins the sabotaged boundary/behavior. Do not change production code. Do not weaken other tests. Run the suite green on clean code before finishing."
|
|
10172
|
+
].join("\n");
|
|
10173
|
+
}
|
|
10174
|
+
function collectOutcomes(result, plan) {
|
|
10175
|
+
const fromText = parseTextOutcomes(result);
|
|
10176
|
+
if (fromText.length > 0) {
|
|
10177
|
+
const remaining = [...plan];
|
|
10178
|
+
const matched = [];
|
|
10179
|
+
for (const m of fromText) {
|
|
10180
|
+
const idx = remaining.findIndex((p) => p.id === m.id);
|
|
10181
|
+
if (idx === -1) continue;
|
|
10182
|
+
remaining.splice(idx, 1);
|
|
10183
|
+
matched.push(m);
|
|
10184
|
+
}
|
|
10185
|
+
if (matched.length > 0) {
|
|
10186
|
+
const missing = remaining.map((p) => ({
|
|
10187
|
+
id: p.id,
|
|
10188
|
+
file: p.file,
|
|
10189
|
+
line: p.line,
|
|
10190
|
+
kind: p.kind,
|
|
10191
|
+
status: "skipped",
|
|
10192
|
+
evidence: "not reported by chaos task"
|
|
10193
|
+
}));
|
|
10194
|
+
return [...matched, ...missing];
|
|
10195
|
+
}
|
|
10196
|
+
}
|
|
10197
|
+
return plan.map((p) => ({
|
|
10198
|
+
id: p.id,
|
|
10199
|
+
file: p.file,
|
|
10200
|
+
line: p.line,
|
|
10201
|
+
kind: p.kind,
|
|
10202
|
+
status: "skipped",
|
|
10203
|
+
evidence: result ? `chaos task ended ${result.status}` : "chaos task produced no result"
|
|
10204
|
+
}));
|
|
10205
|
+
}
|
|
10206
|
+
function isKill(status) {
|
|
10207
|
+
return status === "killed" || status === "killed-by-hang";
|
|
10208
|
+
}
|
|
10209
|
+
function parseTextOutcomes(result) {
|
|
10210
|
+
const text = typeof result?.result === "string" ? result.result : void 0;
|
|
10211
|
+
if (!text) return [];
|
|
10212
|
+
const parsed = parseMutationReport(text);
|
|
10213
|
+
if (!parsed) return [];
|
|
10214
|
+
return parsed.mutants.map((m) => ({
|
|
10215
|
+
id: m.id,
|
|
10216
|
+
file: m.file,
|
|
10217
|
+
line: m.line,
|
|
10218
|
+
kind: m.kind,
|
|
10219
|
+
status: m.status,
|
|
10220
|
+
evidence: m.evidence
|
|
10221
|
+
}));
|
|
10222
|
+
}
|
|
10223
|
+
|
|
9447
10224
|
// src/coordination/director-tools.ts
|
|
9448
10225
|
function makeSpawnTool(director, roster) {
|
|
9449
10226
|
const dispatchCatalog = () => {
|
|
@@ -9736,7 +10513,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
9736
10513
|
try {
|
|
9737
10514
|
const config = buildKanbanSubagentConfig(claim.task, i, roster, instantiateRosterConfig2);
|
|
9738
10515
|
subagentId = await director.spawn(config);
|
|
9739
|
-
const dispatchTaskId =
|
|
10516
|
+
const dispatchTaskId = randomUUID9();
|
|
9740
10517
|
const taskSpec = {
|
|
9741
10518
|
id: dispatchTaskId,
|
|
9742
10519
|
subagentId,
|
|
@@ -10012,6 +10789,7 @@ function buildDirectorToolset(director, roster) {
|
|
|
10012
10789
|
makeAskResultTool(director),
|
|
10013
10790
|
makeRollUpTool(director),
|
|
10014
10791
|
makeQualityGateTool(director, roster),
|
|
10792
|
+
makeMutationTestTool(director, roster),
|
|
10015
10793
|
makeTerminateTool(director),
|
|
10016
10794
|
makeTerminateAllTool(director),
|
|
10017
10795
|
makeFleetTool(director),
|
|
@@ -10098,7 +10876,7 @@ import * as fsp24 from "node:fs/promises";
|
|
|
10098
10876
|
import * as path26 from "node:path";
|
|
10099
10877
|
|
|
10100
10878
|
// src/storage/session-store.ts
|
|
10101
|
-
import { randomUUID as
|
|
10879
|
+
import { randomUUID as randomUUID11 } from "node:crypto";
|
|
10102
10880
|
import * as fsp23 from "node:fs/promises";
|
|
10103
10881
|
import * as path25 from "node:path";
|
|
10104
10882
|
|
|
@@ -10130,6 +10908,14 @@ var PATTERNS = [
|
|
|
10130
10908
|
anchor: "sk-ant-"
|
|
10131
10909
|
},
|
|
10132
10910
|
{ type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g, anchor: "sk-" },
|
|
10911
|
+
{
|
|
10912
|
+
// `xai` is a first-class provider in this codebase, but its key shape was
|
|
10913
|
+
// absent here — so the one credential format WrongStack itself hands users
|
|
10914
|
+
// was the one the scrubber could not recognize (audit 2026-08-20).
|
|
10915
|
+
type: "xai_key",
|
|
10916
|
+
regex: /(?<![A-Za-z0-9])xai-[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g,
|
|
10917
|
+
anchor: "xai-"
|
|
10918
|
+
},
|
|
10133
10919
|
{ type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g, anchor: "ghp_" },
|
|
10134
10920
|
{ type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g, anchor: "github_pat_" },
|
|
10135
10921
|
{ type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g, anchor: "AKIA" },
|
|
@@ -10220,8 +11006,8 @@ var PATTERNS = [
|
|
|
10220
11006
|
// replacement so the separator between adjacent secrets is preserved
|
|
10221
11007
|
// rather than collapsed. Capture groups are therefore: 1=leading
|
|
10222
11008
|
// delimiter, 2=key name, 3=value.
|
|
10223
|
-
regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
|
|
10224
|
-
anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
|
|
11009
|
+
regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD|PASSPHRASE))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
|
|
11010
|
+
anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD", "PASSPHRASE"]
|
|
10225
11011
|
},
|
|
10226
11012
|
{
|
|
10227
11013
|
type: "json_credential_key",
|
|
@@ -10338,6 +11124,27 @@ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key
|
|
|
10338
11124
|
var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
|
|
10339
11125
|
var SCRUB_CHUNK_BYTES = 64 * 1024;
|
|
10340
11126
|
var SCRUB_OVERLAP_BYTES = 1024;
|
|
11127
|
+
var PEM_PRIVATE_KEY_BEGIN_RE = /-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----/;
|
|
11128
|
+
var PEM_END_MARKER = "-----END";
|
|
11129
|
+
var MAX_PEM_BLOCK_BYTES = 64 * 1024;
|
|
11130
|
+
var PEM_END_LINE_TOLERANCE = 64;
|
|
11131
|
+
function extendChunkBoundaryPastPem(text, chunkStart, proposedEnd) {
|
|
11132
|
+
const head = text.slice(chunkStart, proposedEnd);
|
|
11133
|
+
const lastBegin = head.lastIndexOf("-----BEGIN ");
|
|
11134
|
+
if (lastBegin === -1) return proposedEnd;
|
|
11135
|
+
const fromBegin = text.slice(chunkStart + lastBegin);
|
|
11136
|
+
const marker = PEM_PRIVATE_KEY_BEGIN_RE.exec(fromBegin);
|
|
11137
|
+
if (!marker || marker.index !== 0) return proposedEnd;
|
|
11138
|
+
const bodyStart = marker[0].length;
|
|
11139
|
+
const cap = Math.min(text.length, chunkStart + lastBegin + MAX_PEM_BLOCK_BYTES);
|
|
11140
|
+
const closeIdx = fromBegin.indexOf(PEM_END_MARKER, bodyStart);
|
|
11141
|
+
if (closeIdx === -1 || chunkStart + lastBegin + closeIdx >= cap + PEM_END_LINE_TOLERANCE) {
|
|
11142
|
+
return proposedEnd;
|
|
11143
|
+
}
|
|
11144
|
+
const lineEnd = fromBegin.indexOf("\n", closeIdx);
|
|
11145
|
+
const end = lineEnd === -1 ? text.length : chunkStart + lastBegin + lineEnd + 1;
|
|
11146
|
+
return Math.max(proposedEnd, end);
|
|
11147
|
+
}
|
|
10341
11148
|
var PATTERN_ANCHORS = [
|
|
10342
11149
|
...new Set(
|
|
10343
11150
|
PATTERNS.flatMap(
|
|
@@ -10374,6 +11181,7 @@ var DefaultSecretScrubber = class {
|
|
|
10374
11181
|
}
|
|
10375
11182
|
}
|
|
10376
11183
|
end = safe === -1 ? end : safe + 1;
|
|
11184
|
+
end = extendChunkBoundaryPastPem(text, i, end);
|
|
10377
11185
|
}
|
|
10378
11186
|
out.push(this.scrubOne(text.slice(i, end)));
|
|
10379
11187
|
i = end;
|
|
@@ -14205,9 +15013,21 @@ function renderCommandLine(command, args) {
|
|
|
14205
15013
|
});
|
|
14206
15014
|
return [command, ...rendered].join(" ");
|
|
14207
15015
|
}
|
|
14208
|
-
function
|
|
15016
|
+
function renderSubjectFields(obj, fields) {
|
|
15017
|
+
const parts = [];
|
|
15018
|
+
for (const field of fields) {
|
|
15019
|
+
const value = obj[field];
|
|
15020
|
+
if (value === void 0 || value === null || value === "" || value === false) continue;
|
|
15021
|
+
const str = String(value);
|
|
15022
|
+
parts.push(`${field}=${/\s/.test(str) ? `"${str.replace(/"/g, '\\"')}"` : str}`);
|
|
15023
|
+
}
|
|
15024
|
+
return parts.join(" ");
|
|
15025
|
+
}
|
|
15026
|
+
function subjectForToolInput(toolName, input, subjectKey, subjectFields) {
|
|
14209
15027
|
if (!input || typeof input !== "object") return void 0;
|
|
14210
15028
|
const obj = input;
|
|
15029
|
+
const extra = subjectFields && subjectFields.length > 0 ? renderSubjectFields(obj, subjectFields) : "";
|
|
15030
|
+
const withExtra = (base) => extra ? `${base} ${extra}` : base;
|
|
14211
15031
|
if (subjectKey) {
|
|
14212
15032
|
const value = obj[subjectKey];
|
|
14213
15033
|
if (Array.isArray(value)) {
|
|
@@ -14223,9 +15043,9 @@ function subjectForToolInput(toolName, input, subjectKey) {
|
|
|
14223
15043
|
if (subjectKey === "command") {
|
|
14224
15044
|
const rendered = renderCommandLine(value, obj["args"]);
|
|
14225
15045
|
if (value === "commit" && obj["dry_run"] === true) {
|
|
14226
|
-
return `${escapeGlobSubject(rendered)}:dry-run`;
|
|
15046
|
+
return `${escapeGlobSubject(withExtra(rendered))}:dry-run`;
|
|
14227
15047
|
}
|
|
14228
|
-
return escapeGlobSubject(rendered);
|
|
15048
|
+
return escapeGlobSubject(withExtra(rendered));
|
|
14229
15049
|
}
|
|
14230
15050
|
if (subjectKey === "directory" && obj["dry_run"] === true) {
|
|
14231
15051
|
return `${escapeGlobSubject(value)}:dry-run`;
|
|
@@ -15417,7 +16237,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
15417
16237
|
|
|
15418
16238
|
// src/storage/session-checkpoint-cas.ts
|
|
15419
16239
|
import { spawn as spawn2 } from "node:child_process";
|
|
15420
|
-
import { createHash as createHash3, randomUUID as
|
|
16240
|
+
import { createHash as createHash3, randomUUID as randomUUID10 } from "node:crypto";
|
|
15421
16241
|
import * as fsp10 from "node:fs/promises";
|
|
15422
16242
|
import * as path17 from "node:path";
|
|
15423
16243
|
|
|
@@ -15675,7 +16495,7 @@ var SessionCheckpointCas = class {
|
|
|
15675
16495
|
}
|
|
15676
16496
|
const temp = path17.join(
|
|
15677
16497
|
path17.dirname(target),
|
|
15678
|
-
`.${path17.basename(target)}.${process.pid}.${
|
|
16498
|
+
`.${path17.basename(target)}.${process.pid}.${randomUUID10()}.tmp`
|
|
15679
16499
|
);
|
|
15680
16500
|
let handle;
|
|
15681
16501
|
try {
|
|
@@ -17482,7 +18302,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
17482
18302
|
onAppend;
|
|
17483
18303
|
onAppendBatch;
|
|
17484
18304
|
catalogClient;
|
|
17485
|
-
maintenanceHolderId =
|
|
18305
|
+
maintenanceHolderId = randomUUID11();
|
|
17486
18306
|
_loadCache = /* @__PURE__ */ new Map();
|
|
17487
18307
|
loadCache = new SessionLoadCache(this._loadCache);
|
|
17488
18308
|
_indexCache = null;
|
|
@@ -18993,7 +19813,7 @@ function hashStr(s) {
|
|
|
18993
19813
|
}
|
|
18994
19814
|
|
|
18995
19815
|
// src/coordination/multi-agent-coordinator.ts
|
|
18996
|
-
import { randomUUID as
|
|
19816
|
+
import { randomUUID as randomUUID12 } from "node:crypto";
|
|
18997
19817
|
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
18998
19818
|
|
|
18999
19819
|
// src/coordination/coordinator/error-classifier.ts
|
|
@@ -19084,7 +19904,8 @@ async function executeSubagentWithTimeout({
|
|
|
19084
19904
|
budget,
|
|
19085
19905
|
preemptFraction = TIMEOUT_PREEMPT_FRACTION,
|
|
19086
19906
|
abortSubagent,
|
|
19087
|
-
currentSessionId
|
|
19907
|
+
currentSessionId,
|
|
19908
|
+
gracefulFinish
|
|
19088
19909
|
}) {
|
|
19089
19910
|
const initialTimeoutMs = budget.limits.timeoutMs;
|
|
19090
19911
|
const idleLimitMs = budget.limits.idleTimeoutMs;
|
|
@@ -19115,9 +19936,17 @@ async function executeSubagentWithTimeout({
|
|
|
19115
19936
|
const scheduleNext = () => {
|
|
19116
19937
|
const wallLimit = budget.limits.timeoutMs ?? initialTimeoutMs;
|
|
19117
19938
|
const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : wallLimit - (Date.now() - start);
|
|
19118
|
-
const idleRemaining = idleLimitMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
|
|
19119
|
-
const preemptRemaining = initialTimeoutMs === void 0 || preemptedCeiling === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * preemptFraction - (Date.now() - start);
|
|
19120
|
-
|
|
19939
|
+
const idleRemaining = idleLimitMs === void 0 || gracefulFinish !== void 0 && initialTimeoutMs !== void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
|
|
19940
|
+
const preemptRemaining = initialTimeoutMs === void 0 || preemptedCeiling === wallLimit || gracefulFinish !== void 0 ? Number.POSITIVE_INFINITY : wallLimit * preemptFraction - (Date.now() - start);
|
|
19941
|
+
const next = Math.min(wallRemaining, idleRemaining, preemptRemaining);
|
|
19942
|
+
if (!Number.isFinite(next)) {
|
|
19943
|
+
if (timer) {
|
|
19944
|
+
clearTimeout(timer);
|
|
19945
|
+
timer = null;
|
|
19946
|
+
}
|
|
19947
|
+
return;
|
|
19948
|
+
}
|
|
19949
|
+
armFor(Math.max(25, next));
|
|
19121
19950
|
};
|
|
19122
19951
|
const negotiateTimeout = async (used, limit) => {
|
|
19123
19952
|
const handler = budget.onThreshold;
|
|
@@ -19166,6 +19995,10 @@ async function executeSubagentWithTimeout({
|
|
|
19166
19995
|
const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
|
|
19167
19996
|
const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
|
|
19168
19997
|
if (idleExceeded && !wallExceeded) {
|
|
19998
|
+
if (gracefulFinish !== void 0 && initialTimeoutMs !== void 0) {
|
|
19999
|
+
scheduleNext();
|
|
20000
|
+
return;
|
|
20001
|
+
}
|
|
19169
20002
|
const sessionId = currentSessionId();
|
|
19170
20003
|
budget._events?.emit("budget.threshold_reached", {
|
|
19171
20004
|
...sessionId ? { sessionId } : {},
|
|
@@ -19182,7 +20015,7 @@ async function executeSubagentWithTimeout({
|
|
|
19182
20015
|
reject(new BudgetExceededError("idle_timeout", idleLimit ?? 0, budget.idleMs()));
|
|
19183
20016
|
return;
|
|
19184
20017
|
}
|
|
19185
|
-
if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptState === "active" /* ACTIVE */ && elapsed >= wallLimit * preemptFraction) {
|
|
20018
|
+
if (wallLimit !== void 0 && !wallExceeded && gracefulFinish === void 0 && budget.onThreshold && preemptState === "active" /* ACTIVE */ && elapsed >= wallLimit * preemptFraction) {
|
|
19186
20019
|
const activityTs = Date.now() - budget.idleMs();
|
|
19187
20020
|
if (activityTs <= lastGrantActivityTs) {
|
|
19188
20021
|
preemptState = "locked" /* LOCKED */;
|
|
@@ -19216,6 +20049,22 @@ async function executeSubagentWithTimeout({
|
|
|
19216
20049
|
return;
|
|
19217
20050
|
}
|
|
19218
20051
|
const limit = wallLimit ?? 0;
|
|
20052
|
+
if (gracefulFinish !== void 0) {
|
|
20053
|
+
if (!budget.graceGranted) {
|
|
20054
|
+
const reason = `wall-clock budget of ${Math.round(limit / 1e3)}s reached`;
|
|
20055
|
+
if (budget.notifyFinish(reason, { graceMs: gracefulFinish.graceMs })) {
|
|
20056
|
+
scheduleNext();
|
|
20057
|
+
return;
|
|
20058
|
+
}
|
|
20059
|
+
abortSubagent(ctx.subagentId);
|
|
20060
|
+
reject(new BudgetExceededError("timeout", limit, elapsed));
|
|
20061
|
+
return;
|
|
20062
|
+
} else {
|
|
20063
|
+
abortSubagent(ctx.subagentId);
|
|
20064
|
+
reject(new BudgetExceededError("timeout", limit, elapsed));
|
|
20065
|
+
return;
|
|
20066
|
+
}
|
|
20067
|
+
}
|
|
19219
20068
|
if (!budget.onThreshold) {
|
|
19220
20069
|
abortSubagent(ctx.subagentId);
|
|
19221
20070
|
reject(new BudgetExceededError("timeout", limit, elapsed));
|
|
@@ -19363,7 +20212,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
19363
20212
|
return { ...subagent, name: display };
|
|
19364
20213
|
}
|
|
19365
20214
|
async spawn(subagent) {
|
|
19366
|
-
const id = subagent.id ||
|
|
20215
|
+
const id = subagent.id || randomUUID12();
|
|
19367
20216
|
const cfg = this.withNickname(subagent, id);
|
|
19368
20217
|
if (this.subagents.has(id)) {
|
|
19369
20218
|
throw new Error(`Subagent id "${id}" already exists \u2014 refusing to overwrite`);
|
|
@@ -19600,6 +20449,32 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
19600
20449
|
completeTask(result) {
|
|
19601
20450
|
this.recordCompletion(result);
|
|
19602
20451
|
}
|
|
20452
|
+
/**
|
|
20453
|
+
* Ask every RUNNING subagent that opted into `gracefulFinish` to finish its
|
|
20454
|
+
* task in its own turn (see coordination/subagent-finish.ts). This is the
|
|
20455
|
+
* leader-side entry point for "the leader agent has finished": it delivers
|
|
20456
|
+
* an in-band notification between tool batches — never an interrupt, never
|
|
20457
|
+
* an abort. Each notified subagent keeps its existing time budget and
|
|
20458
|
+
* accelerates; the watchdog still bounds the maximum lifetime.
|
|
20459
|
+
*
|
|
20460
|
+
* Subagents without the policy opted in are deliberately untouched — their
|
|
20461
|
+
* lifecycle remains the legacy watchdog contract.
|
|
20462
|
+
*
|
|
20463
|
+
* Returns the number of subagents actually notified.
|
|
20464
|
+
*/
|
|
20465
|
+
requestFinish(reason) {
|
|
20466
|
+
let notified = 0;
|
|
20467
|
+
for (const subagent of this.subagents.values()) {
|
|
20468
|
+
if (subagent.status !== "running") continue;
|
|
20469
|
+
if (!resolveGracefulFinish(subagent.config)) continue;
|
|
20470
|
+
const budget = subagent.activeBudget;
|
|
20471
|
+
if (!budget) continue;
|
|
20472
|
+
const usage = budget.usage();
|
|
20473
|
+
if (usage.iterations === 0 && usage.toolCalls === 0) continue;
|
|
20474
|
+
if (budget.notifyFinish(reason)) notified++;
|
|
20475
|
+
}
|
|
20476
|
+
return notified;
|
|
20477
|
+
}
|
|
19603
20478
|
// --- internal dispatching ---------------------------------------------
|
|
19604
20479
|
tryDispatchNext() {
|
|
19605
20480
|
while (this.canDispatch()) {
|
|
@@ -19773,7 +20648,14 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
19773
20648
|
idleTimeoutMs: rawIdleTimeoutMs ?? this.config.defaultBudget?.idleTimeoutMs ?? configWithRosterDefaults.idleTimeoutMs
|
|
19774
20649
|
},
|
|
19775
20650
|
"auto",
|
|
19776
|
-
{
|
|
20651
|
+
{
|
|
20652
|
+
sessionId: () => this.currentSessionId(),
|
|
20653
|
+
subagentId,
|
|
20654
|
+
// Graceful-finish runs own wall-clock enforcement to the watchdog so
|
|
20655
|
+
// the notify-then-bound lifecycle cannot be raced by tool.progress
|
|
20656
|
+
// heartbeats calling checkTimeout() (see subagent-budget.ts).
|
|
20657
|
+
...resolveGracefulFinish(subagent.config) ? { wallClockWatchdogOwned: true } : {}
|
|
20658
|
+
}
|
|
19777
20659
|
);
|
|
19778
20660
|
subagent.activeBudget = budget;
|
|
19779
20661
|
if (!this.runner) {
|
|
@@ -19806,7 +20688,8 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
19806
20688
|
task,
|
|
19807
20689
|
runCtx,
|
|
19808
20690
|
budget,
|
|
19809
|
-
subagent.config.preemptFraction
|
|
20691
|
+
subagent.config.preemptFraction,
|
|
20692
|
+
resolveGracefulFinish(subagent.config)
|
|
19810
20693
|
);
|
|
19811
20694
|
result = {
|
|
19812
20695
|
subagentId,
|
|
@@ -19836,13 +20719,14 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
19836
20719
|
}
|
|
19837
20720
|
this.recordCompletion(result);
|
|
19838
20721
|
}
|
|
19839
|
-
async executeWithTimeout(runner, task, ctx, budget, preemptFraction) {
|
|
20722
|
+
async executeWithTimeout(runner, task, ctx, budget, preemptFraction, gracefulFinish) {
|
|
19840
20723
|
return executeSubagentWithTimeout({
|
|
19841
20724
|
runner,
|
|
19842
20725
|
task,
|
|
19843
20726
|
ctx,
|
|
19844
20727
|
budget,
|
|
19845
20728
|
preemptFraction,
|
|
20729
|
+
gracefulFinish,
|
|
19846
20730
|
abortSubagent: (subagentId) => this.subagents.get(subagentId)?.abortController.abort(),
|
|
19847
20731
|
currentSessionId: () => this.currentSessionId()
|
|
19848
20732
|
});
|
|
@@ -20151,6 +21035,7 @@ function worktreeOwnerLabel(task, config) {
|
|
|
20151
21035
|
}
|
|
20152
21036
|
|
|
20153
21037
|
// src/coordination/director.ts
|
|
21038
|
+
var BUSY_REARM_FLOOR_MS = 1e3;
|
|
20154
21039
|
var Director = class _Director {
|
|
20155
21040
|
/* eslint-disable-next-line @typescript-eslint/no-unused-vars — just a cast helper */
|
|
20156
21041
|
static _asManifestEntry(v) {
|
|
@@ -20216,6 +21101,13 @@ var Director = class _Director {
|
|
|
20216
21101
|
subagentIdleTimeoutMs;
|
|
20217
21102
|
retireSubagentOnTaskComplete;
|
|
20218
21103
|
subagentIdleTimers = /* @__PURE__ */ new Map();
|
|
21104
|
+
/**
|
|
21105
|
+
* Effective idle window per subagent (spawn-time `idleTimeoutMs` override
|
|
21106
|
+
* or the Director-wide default; undefined = no window). Internal-task
|
|
21107
|
+
* completion re-arms with THIS value, not the Director-wide default, so
|
|
21108
|
+
* a subagent-configured window survives its first internal probe.
|
|
21109
|
+
*/
|
|
21110
|
+
subagentIdleDelayMs = /* @__PURE__ */ new Map();
|
|
20219
21111
|
sharedScratchpadPath;
|
|
20220
21112
|
maxSpawns;
|
|
20221
21113
|
maxSpawnDepth;
|
|
@@ -20248,7 +21140,7 @@ var Director = class _Director {
|
|
|
20248
21140
|
sessionProvider;
|
|
20249
21141
|
sessionModel;
|
|
20250
21142
|
constructor(opts) {
|
|
20251
|
-
this.id = opts.config.coordinatorId ||
|
|
21143
|
+
this.id = opts.config.coordinatorId || randomUUID13();
|
|
20252
21144
|
this.manifestPath = opts.manifestPath;
|
|
20253
21145
|
this.roster = opts.roster;
|
|
20254
21146
|
this.directorPreamble = opts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
|
|
@@ -20372,7 +21264,13 @@ var Director = class _Director {
|
|
|
20372
21264
|
handleTaskCompleted(payload) {
|
|
20373
21265
|
const r = payload.result;
|
|
20374
21266
|
const settled = this.tasks.settle(r);
|
|
20375
|
-
if (settled.internal)
|
|
21267
|
+
if (settled.internal) {
|
|
21268
|
+
this.armSubagentIdleRetirement(
|
|
21269
|
+
r.subagentId,
|
|
21270
|
+
this.subagentIdleDelayMs.get(r.subagentId) ?? this.subagentIdleTimeoutMs
|
|
21271
|
+
);
|
|
21272
|
+
return;
|
|
21273
|
+
}
|
|
20376
21274
|
const title = this.tasks.descriptionFor(r.taskId, payload.task.description ?? r.taskId);
|
|
20377
21275
|
if (!settled.consumedInBand && this.taskResultNotifier) {
|
|
20378
21276
|
const resultText = typeof r.result === "string" ? r.result : r.result !== void 0 ? safeStringify(r.result) : void 0;
|
|
@@ -20437,7 +21335,7 @@ var Director = class _Director {
|
|
|
20437
21335
|
}
|
|
20438
21336
|
this.armSubagentIdleRetirement(
|
|
20439
21337
|
r.subagentId,
|
|
20440
|
-
this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleTimeoutMs
|
|
21338
|
+
this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleDelayMs.get(r.subagentId) ?? this.subagentIdleTimeoutMs
|
|
20441
21339
|
);
|
|
20442
21340
|
}
|
|
20443
21341
|
extensionsFor(subagentId) {
|
|
@@ -20455,6 +21353,17 @@ var Director = class _Director {
|
|
|
20455
21353
|
isWorkComplete() {
|
|
20456
21354
|
return this.workCompleteFlag;
|
|
20457
21355
|
}
|
|
21356
|
+
/**
|
|
21357
|
+
* Ask every running background subagent that opted into `gracefulFinish`
|
|
21358
|
+
* to finish its task in its own turn. In-band notification between tool
|
|
21359
|
+
* batches — no interrupt, no abort; each subagent keeps its time budget and
|
|
21360
|
+
* accelerates. Session shutdown calls this before draining Chimera work so
|
|
21361
|
+
* the post-session reviewer is nudged to complete rather than killed.
|
|
21362
|
+
* Returns the number of subagents notified.
|
|
21363
|
+
*/
|
|
21364
|
+
requestFinish(reason) {
|
|
21365
|
+
return this.coordinator.requestFinish(reason);
|
|
21366
|
+
}
|
|
20458
21367
|
setLeaderBtwNote(note) {
|
|
20459
21368
|
return this.btwNotes.add(note);
|
|
20460
21369
|
}
|
|
@@ -20512,6 +21421,7 @@ var Director = class _Director {
|
|
|
20512
21421
|
this.resolveSpawnModel(config);
|
|
20513
21422
|
const subagentId = await spawn3(this, config, priceLookup);
|
|
20514
21423
|
const perSubagentIdleMs = typeof config.idleTimeoutMs === "number" && Number.isFinite(config.idleTimeoutMs) && config.idleTimeoutMs >= 0 ? config.idleTimeoutMs : this.subagentIdleTimeoutMs;
|
|
21424
|
+
this.subagentIdleDelayMs.set(subagentId, perSubagentIdleMs);
|
|
20515
21425
|
this.armSubagentIdleRetirement(subagentId, perSubagentIdleMs);
|
|
20516
21426
|
return subagentId;
|
|
20517
21427
|
}
|
|
@@ -20531,7 +21441,7 @@ var Director = class _Director {
|
|
|
20531
21441
|
);
|
|
20532
21442
|
}
|
|
20533
21443
|
const msg = {
|
|
20534
|
-
id:
|
|
21444
|
+
id: randomUUID13(),
|
|
20535
21445
|
type: "task",
|
|
20536
21446
|
from: this.id,
|
|
20537
21447
|
to: subagentId,
|
|
@@ -20565,6 +21475,7 @@ var Director = class _Director {
|
|
|
20565
21475
|
this.budgetPolicy.dispose();
|
|
20566
21476
|
for (const timer of this.subagentIdleTimers.values()) clearTimeout(timer);
|
|
20567
21477
|
this.subagentIdleTimers.clear();
|
|
21478
|
+
this.subagentIdleDelayMs.clear();
|
|
20568
21479
|
await this.coordinator.stopAll();
|
|
20569
21480
|
this.tasks.resolveWaitersOnShutdown();
|
|
20570
21481
|
for (const b of this.subagentBridges.values()) {
|
|
@@ -20623,6 +21534,7 @@ var Director = class _Director {
|
|
|
20623
21534
|
}
|
|
20624
21535
|
async remove(subagentId) {
|
|
20625
21536
|
this.clearSubagentIdleRetirement(subagentId);
|
|
21537
|
+
this.subagentIdleDelayMs.delete(subagentId);
|
|
20626
21538
|
void this.appendSessionEvent({
|
|
20627
21539
|
type: "agent_stopped",
|
|
20628
21540
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -20675,9 +21587,13 @@ var Director = class _Director {
|
|
|
20675
21587
|
const timer = setTimeout(() => {
|
|
20676
21588
|
this.subagentIdleTimers.delete(subagentId);
|
|
20677
21589
|
const entry = this.coordinator.getStatus().subagents.find((a) => a.id === subagentId);
|
|
20678
|
-
if (entry
|
|
21590
|
+
if (entry === void 0) return;
|
|
21591
|
+
if (entry.status !== "idle") {
|
|
21592
|
+
this.armSubagentIdleRetirement(subagentId, Math.max(delayMs, BUSY_REARM_FLOOR_MS));
|
|
21593
|
+
return;
|
|
21594
|
+
}
|
|
20679
21595
|
if (this.coordinator.listPendingTasks().some((task) => task.subagentId === subagentId)) {
|
|
20680
|
-
this.armSubagentIdleRetirement(subagentId,
|
|
21596
|
+
this.armSubagentIdleRetirement(subagentId, Math.max(delayMs, BUSY_REARM_FLOOR_MS));
|
|
20681
21597
|
return;
|
|
20682
21598
|
}
|
|
20683
21599
|
void this.remove(subagentId).catch(
|
|
@@ -23961,7 +24877,7 @@ function _resetDesignRulesCache() {
|
|
|
23961
24877
|
}
|
|
23962
24878
|
|
|
23963
24879
|
// src/execution/design-color.ts
|
|
23964
|
-
function
|
|
24880
|
+
function clamp2(n, lo, hi) {
|
|
23965
24881
|
return n < lo ? lo : n > hi ? hi : n;
|
|
23966
24882
|
}
|
|
23967
24883
|
function parseOklch(value) {
|
|
@@ -23977,9 +24893,9 @@ function parseOklch(value) {
|
|
|
23977
24893
|
let a = 1;
|
|
23978
24894
|
if (alphaPart !== void 0) {
|
|
23979
24895
|
const av = parseComponent(alphaPart.trim(), true);
|
|
23980
|
-
if (av !== null) a =
|
|
24896
|
+
if (av !== null) a = clamp2(av, 0, 1);
|
|
23981
24897
|
}
|
|
23982
|
-
return [
|
|
24898
|
+
return [clamp2(L, 0, 1), Math.max(0, C), H, a];
|
|
23983
24899
|
}
|
|
23984
24900
|
function parseComponent(s, percentIsFraction) {
|
|
23985
24901
|
s = s.trim();
|
|
@@ -23998,7 +24914,7 @@ function parseAngle(s) {
|
|
|
23998
24914
|
}
|
|
23999
24915
|
function linearToSrgb(c) {
|
|
24000
24916
|
const v = c <= 31308e-7 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055;
|
|
24001
|
-
return
|
|
24917
|
+
return clamp2(v, 0, 1);
|
|
24002
24918
|
}
|
|
24003
24919
|
function toHex2(n) {
|
|
24004
24920
|
return Math.round(n * 255).toString(16).padStart(2, "0");
|
|
@@ -24203,16 +25119,16 @@ async function runDesignVerify(projectRoot, tokens, explicitFiles) {
|
|
|
24203
25119
|
}
|
|
24204
25120
|
|
|
24205
25121
|
// src/execution/design-detect.ts
|
|
24206
|
-
var
|
|
25122
|
+
var META_KEY2 = "designStudio";
|
|
24207
25123
|
function getDesignState(ctx) {
|
|
24208
|
-
const v = ctx.meta[
|
|
25124
|
+
const v = ctx.meta[META_KEY2];
|
|
24209
25125
|
return v && typeof v === "object" ? v : void 0;
|
|
24210
25126
|
}
|
|
24211
25127
|
function ensureState(ctx) {
|
|
24212
25128
|
let s = getDesignState(ctx);
|
|
24213
25129
|
if (!s) {
|
|
24214
25130
|
s = { active: false, signals: [] };
|
|
24215
|
-
ctx.meta[
|
|
25131
|
+
ctx.meta[META_KEY2] = s;
|
|
24216
25132
|
}
|
|
24217
25133
|
return s;
|
|
24218
25134
|
}
|
|
@@ -26134,7 +27050,7 @@ ${summaryText}` : summaryText;
|
|
|
26134
27050
|
};
|
|
26135
27051
|
|
|
26136
27052
|
// src/execution/parallel-eternal-engine.ts
|
|
26137
|
-
import { randomUUID as
|
|
27053
|
+
import { randomUUID as randomUUID14 } from "node:crypto";
|
|
26138
27054
|
var GOAL_COMPLETE_MARKER2 = /^\s*\[goal[_\s-]?complete\]\s*$/im;
|
|
26139
27055
|
var ParallelEternalEngine = class {
|
|
26140
27056
|
constructor(opts) {
|
|
@@ -26211,7 +27127,7 @@ var ParallelEternalEngine = class {
|
|
|
26211
27127
|
this.state = "running";
|
|
26212
27128
|
await this.persistState("running");
|
|
26213
27129
|
const config = {
|
|
26214
|
-
coordinatorId: `parallel-${
|
|
27130
|
+
coordinatorId: `parallel-${randomUUID14().slice(0, 8)}`,
|
|
26215
27131
|
maxConcurrent: this.slots,
|
|
26216
27132
|
doneCondition: { type: "all_tasks_done" }
|
|
26217
27133
|
};
|
|
@@ -26265,7 +27181,7 @@ var ParallelEternalEngine = class {
|
|
|
26265
27181
|
}
|
|
26266
27182
|
if (!this.coordinator) {
|
|
26267
27183
|
const config = {
|
|
26268
|
-
coordinatorId: `parallel-${
|
|
27184
|
+
coordinatorId: `parallel-${randomUUID14().slice(0, 8)}`,
|
|
26269
27185
|
maxConcurrent: this.slots,
|
|
26270
27186
|
doneCondition: { type: "all_tasks_done" }
|
|
26271
27187
|
};
|
|
@@ -26351,7 +27267,7 @@ ${recentJournal}` : "No prior iterations.",
|
|
|
26351
27267
|
const task = expectDefined(tasks[i]);
|
|
26352
27268
|
const route = routes[i] ?? null;
|
|
26353
27269
|
const subagentId = `parallel-${this.iterations}-${i}`;
|
|
26354
|
-
const taskId =
|
|
27270
|
+
const taskId = randomUUID14();
|
|
26355
27271
|
const personaLine = route ? `Acting agent: ${route.definition.config.name} \u2014 ${route.definition.capability.summary}
|
|
26356
27272
|
` : "";
|
|
26357
27273
|
const spec = {
|
|
@@ -26566,7 +27482,7 @@ ${lastFew}` : "No prior iterations.",
|
|
|
26566
27482
|
};
|
|
26567
27483
|
|
|
26568
27484
|
// src/core/streaming-response-builder.ts
|
|
26569
|
-
import { randomUUID as
|
|
27485
|
+
import { randomUUID as randomUUID15 } from "node:crypto";
|
|
26570
27486
|
var STREAM_DRAIN_TIMEOUT_MS = 500;
|
|
26571
27487
|
function buildResponse(state) {
|
|
26572
27488
|
const content = [];
|
|
@@ -26626,7 +27542,7 @@ function handleContentBlockStart(state, ev) {
|
|
|
26626
27542
|
state.textBuffers.push("");
|
|
26627
27543
|
state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
|
|
26628
27544
|
} else if (kind === "tool_use") {
|
|
26629
|
-
const id = ev.id ??
|
|
27545
|
+
const id = ev.id ?? randomUUID15();
|
|
26630
27546
|
state.tools.set(id, { name: ev.name ?? "unknown", partial: "" });
|
|
26631
27547
|
state.blockOrder.push({ kind: "tool", id });
|
|
26632
27548
|
state.currentTextIndex = -1;
|
|
@@ -27033,7 +27949,7 @@ function replaceAllCaseInsensitive(haystack, needle, replacement) {
|
|
|
27033
27949
|
}
|
|
27034
27950
|
|
|
27035
27951
|
// src/core/provider-runner.ts
|
|
27036
|
-
import { randomUUID as
|
|
27952
|
+
import { randomUUID as randomUUID16 } from "node:crypto";
|
|
27037
27953
|
function scrubProviderBody(body) {
|
|
27038
27954
|
if (!body) return void 0;
|
|
27039
27955
|
return {
|
|
@@ -27055,11 +27971,11 @@ function providerLogCtx(p, r) {
|
|
|
27055
27971
|
}
|
|
27056
27972
|
async function runProviderWithRetry(opts) {
|
|
27057
27973
|
const { provider, request, signal, ctx, events, retry, logger, tracer } = opts;
|
|
27058
|
-
const logicalRequestId =
|
|
27974
|
+
const logicalRequestId = randomUUID16();
|
|
27059
27975
|
const promptManifest = createChroniclePromptManifest(request);
|
|
27060
27976
|
let attempt = 0;
|
|
27061
27977
|
for (; ; ) {
|
|
27062
|
-
const attemptId =
|
|
27978
|
+
const attemptId = randomUUID16();
|
|
27063
27979
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
27064
27980
|
const startedNs = process.hrtime.bigint();
|
|
27065
27981
|
const correlation = {
|
|
@@ -27949,6 +28865,10 @@ var DefaultSkillLoader = class {
|
|
|
27949
28865
|
);
|
|
27950
28866
|
for (const e of entries) {
|
|
27951
28867
|
if (!await entryIsDirectory(dir, e)) continue;
|
|
28868
|
+
if (!isValidSkillNameFormat(e.name)) {
|
|
28869
|
+
this.skipped.push({ dir, entry: e.name, reason: "invalid-name-format" });
|
|
28870
|
+
continue;
|
|
28871
|
+
}
|
|
27952
28872
|
const skillFile = path30.join(dir, e.name, "SKILL.md");
|
|
27953
28873
|
let raw;
|
|
27954
28874
|
try {
|
|
@@ -28622,7 +29542,7 @@ function readPolicy(ctx) {
|
|
|
28622
29542
|
}
|
|
28623
29543
|
|
|
28624
29544
|
// src/execution/tool-executor.ts
|
|
28625
|
-
import { randomUUID as
|
|
29545
|
+
import { randomUUID as randomUUID19 } from "node:crypto";
|
|
28626
29546
|
import * as fs10 from "node:fs/promises";
|
|
28627
29547
|
import * as path35 from "node:path";
|
|
28628
29548
|
|
|
@@ -28630,7 +29550,7 @@ import * as path35 from "node:path";
|
|
|
28630
29550
|
var GOVERNED_TOOL_EXECUTOR_META_KEY = "toolExecutor.executeGoverned";
|
|
28631
29551
|
|
|
28632
29552
|
// src/execution/tool-executor-support.ts
|
|
28633
|
-
import { createHash as createHash8, randomUUID as
|
|
29553
|
+
import { createHash as createHash8, randomUUID as randomUUID17 } from "node:crypto";
|
|
28634
29554
|
import * as fs9 from "node:fs/promises";
|
|
28635
29555
|
import * as path33 from "node:path";
|
|
28636
29556
|
|
|
@@ -28761,7 +29681,7 @@ async function maybePersistLargeToolOutput(toolName, content, budget) {
|
|
|
28761
29681
|
await fs9.mkdir(dir, { recursive: true });
|
|
28762
29682
|
const safeTool = toolName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
|
|
28763
29683
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
28764
|
-
const filePath = path33.join(dir, `${stamp}-${safeTool}-${
|
|
29684
|
+
const filePath = path33.join(dir, `${stamp}-${safeTool}-${randomUUID17()}.log`);
|
|
28765
29685
|
await fs9.writeFile(filePath, content, "utf8");
|
|
28766
29686
|
const marker = `[full tool output: ${bytes} bytes at ${filePath}; read/grep that file selectively instead of re-running or requesting more output]`;
|
|
28767
29687
|
const fixedBytes = Buffer.byteLength(marker + TOOL_OUTPUT_ARTIFACT_OMISSION, "utf8");
|
|
@@ -29310,7 +30230,7 @@ ${errorDetails}`,
|
|
|
29310
30230
|
}
|
|
29311
30231
|
|
|
29312
30232
|
// src/execution/tool-executor-runner.ts
|
|
29313
|
-
import { randomUUID as
|
|
30233
|
+
import { randomUUID as randomUUID18 } from "node:crypto";
|
|
29314
30234
|
|
|
29315
30235
|
// src/observability/process-telemetry.ts
|
|
29316
30236
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
@@ -29449,7 +30369,7 @@ async function runToolWithTimeout(tool, input, parentSignal, ctx, opts, config,
|
|
|
29449
30369
|
progressTailChars: config.progressTailChars,
|
|
29450
30370
|
progressHeadChars: config.progressHeadChars
|
|
29451
30371
|
}) : (async () => tool.execute(input, ctx, { signal: combined }))();
|
|
29452
|
-
const telemetryToolCallId = toolUseId ?? `nested-${
|
|
30372
|
+
const telemetryToolCallId = toolUseId ?? `nested-${randomUUID18()}`;
|
|
29453
30373
|
const toolPromise = opts.events ? runWithNetworkTelemetry(
|
|
29454
30374
|
{
|
|
29455
30375
|
events: opts.events,
|
|
@@ -29622,7 +30542,7 @@ var ToolExecutor = class _ToolExecutor {
|
|
|
29622
30542
|
return { result, tool, durationMs: Date.now() - start };
|
|
29623
30543
|
}
|
|
29624
30544
|
if (effectivePermission === "confirm") {
|
|
29625
|
-
const suggestedPattern = boundary.decision === "confirm" ? `kanban-boundary:${boundary.path ?? tool.name}` : subjectForToolInput(tool.name, use.input, tool.subjectKey) ?? tool.name;
|
|
30545
|
+
const suggestedPattern = boundary.decision === "confirm" ? `kanban-boundary:${boundary.path ?? tool.name}` : subjectForToolInput(tool.name, use.input, tool.subjectKey, tool.subjectFields) ?? tool.name;
|
|
29626
30546
|
if (this.opts.confirmAwaiter) {
|
|
29627
30547
|
const awaiter = this.opts.confirmAwaiter;
|
|
29628
30548
|
const choice = await new Promise(
|
|
@@ -29842,7 +30762,7 @@ ${post.additionalContext}`;
|
|
|
29842
30762
|
const bridge = async (toolName, input) => {
|
|
29843
30763
|
const nestedUse = {
|
|
29844
30764
|
type: "tool_use",
|
|
29845
|
-
id: `nested-${
|
|
30765
|
+
id: `nested-${randomUUID19()}`,
|
|
29846
30766
|
name: toolName,
|
|
29847
30767
|
input
|
|
29848
30768
|
};
|
|
@@ -30446,13 +31366,13 @@ import * as fs11 from "node:fs/promises";
|
|
|
30446
31366
|
import * as path37 from "node:path";
|
|
30447
31367
|
|
|
30448
31368
|
// src/types/mode-prompts.ts
|
|
30449
|
-
import { readFileSync as
|
|
31369
|
+
import { readFileSync as readFileSync11, statSync as statSync4 } from "node:fs";
|
|
30450
31370
|
import * as path36 from "node:path";
|
|
30451
31371
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
30452
31372
|
function modePrompt(id) {
|
|
30453
31373
|
for (const dir of modePromptDirCandidates()) {
|
|
30454
31374
|
try {
|
|
30455
|
-
return
|
|
31375
|
+
return readFileSync11(path36.join(dir, `${id}.md`), "utf8").trimEnd();
|
|
30456
31376
|
} catch {
|
|
30457
31377
|
}
|
|
30458
31378
|
}
|
|
@@ -31130,7 +32050,17 @@ function normalizeModelsDevModel(model) {
|
|
|
31130
32050
|
const reasoningConfig = {
|
|
31131
32051
|
default: disableSupported ? "enabled" : "always_on",
|
|
31132
32052
|
disableSupported,
|
|
31133
|
-
|
|
32053
|
+
// Tri-state (see ReasoningConfig.effortSupported):
|
|
32054
|
+
// options present → documented answer (true when effort values exist;
|
|
32055
|
+
// an explicitly EMPTY array is a documented "no
|
|
32056
|
+
// effort control", not an absent field).
|
|
32057
|
+
// field ABSENT → the model is known to reason but its vocabulary is
|
|
32058
|
+
// undocumented → `undefined`, so the resolver forwards
|
|
32059
|
+
// the request and each wire adapter applies its own
|
|
32060
|
+
// transport gating. Sending `false` here would make
|
|
32061
|
+
// the resolver claim "does not support effort" — an
|
|
32062
|
+
// assertion the catalog never made.
|
|
32063
|
+
...raw === void 0 ? {} : { effortSupported: effortLevels.length > 0 },
|
|
31134
32064
|
effortLevels,
|
|
31135
32065
|
preserveThinking: model.interleaved ? "always_on" : "unsupported"
|
|
31136
32066
|
};
|
|
@@ -31717,9 +32647,9 @@ async function startMetricsServer(opts) {
|
|
|
31717
32647
|
let server;
|
|
31718
32648
|
if (useHttps && tls) {
|
|
31719
32649
|
const { createServer } = await import("node:https");
|
|
31720
|
-
const { readFileSync:
|
|
32650
|
+
const { readFileSync: readFileSync13 } = await import("node:fs");
|
|
31721
32651
|
server = createServer(
|
|
31722
|
-
{ cert:
|
|
32652
|
+
{ cert: readFileSync13(tls.cert), key: readFileSync13(tls.key) },
|
|
31723
32653
|
listener
|
|
31724
32654
|
);
|
|
31725
32655
|
} else {
|
|
@@ -32142,7 +33072,9 @@ function hasRecursiveForceDelete(command, projectRoot) {
|
|
|
32142
33072
|
if (token === "rd" || token === "rmdir") {
|
|
32143
33073
|
const args = commandSegment(tokens, i + 1).map((arg) => arg.toLowerCase());
|
|
32144
33074
|
if (args.includes("/s")) {
|
|
32145
|
-
const targets = args.filter(
|
|
33075
|
+
const targets = args.filter(
|
|
33076
|
+
(arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg)
|
|
33077
|
+
);
|
|
32146
33078
|
if (targets.length === 0) return true;
|
|
32147
33079
|
if (targets.some(isCatastrophicDeleteTarget)) return true;
|
|
32148
33080
|
if (targets.some((target) => !pathLooksInsideProject(target, projectRoot))) return true;
|
|
@@ -32211,7 +33143,8 @@ function hasFindExec(command) {
|
|
|
32211
33143
|
function isCatastrophicDeleteTarget(rawTarget) {
|
|
32212
33144
|
const t = rawTarget.replace(/^['"]|['"]$/g, "").trim();
|
|
32213
33145
|
if (!t) return false;
|
|
32214
|
-
if (t === "*" || t === "." || t === "./" || t === ".\\" || t === "./*" || t === ".\\*")
|
|
33146
|
+
if (t === "*" || t === "." || t === "./" || t === ".\\" || t === "./*" || t === ".\\*")
|
|
33147
|
+
return true;
|
|
32215
33148
|
const s = t.replace(/[\\/]\*+$/, "").replace(/[\\/]+$/, "");
|
|
32216
33149
|
if (s === "") return true;
|
|
32217
33150
|
if (s === "~" || /^\$HOME$/i.test(s) || /^%USERPROFILE%$/i.test(s)) return true;
|
|
@@ -32251,12 +33184,16 @@ function hasCatastrophicDelete(command) {
|
|
|
32251
33184
|
const args = tokens.slice(i + 1);
|
|
32252
33185
|
const recursive = args.some((arg) => arg.toLowerCase() === "/s");
|
|
32253
33186
|
if (!recursive) continue;
|
|
32254
|
-
const targets = args.filter(
|
|
33187
|
+
const targets = args.filter(
|
|
33188
|
+
(arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg)
|
|
33189
|
+
);
|
|
32255
33190
|
if (targets.some(isCatastrophicDeleteTarget)) return true;
|
|
32256
33191
|
}
|
|
32257
33192
|
if (token === "del" || token === "erase") {
|
|
32258
33193
|
const args = tokens.slice(i + 1);
|
|
32259
|
-
const targets = args.filter(
|
|
33194
|
+
const targets = args.filter(
|
|
33195
|
+
(arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg)
|
|
33196
|
+
);
|
|
32260
33197
|
if (targets.some(isCatastrophicDeleteTarget)) return true;
|
|
32261
33198
|
}
|
|
32262
33199
|
}
|
|
@@ -32317,6 +33254,47 @@ function isClearlyDestructiveBashCommand(command, projectRoot) {
|
|
|
32317
33254
|
if (HIGH_IMPACT_PATTERNS.some((pattern) => pattern.test(trimmed))) return true;
|
|
32318
33255
|
return false;
|
|
32319
33256
|
}
|
|
33257
|
+
var WELL_KNOWN_CREDENTIAL_ENV_VARS = /* @__PURE__ */ new Set([
|
|
33258
|
+
"ANTHROPIC_API_KEY",
|
|
33259
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
33260
|
+
"OPENAI_API_KEY",
|
|
33261
|
+
"AZURE_OPENAI_API_KEY",
|
|
33262
|
+
"GEMINI_API_KEY",
|
|
33263
|
+
"GOOGLE_API_KEY",
|
|
33264
|
+
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
33265
|
+
"GOOGLE_GENERATIVE_AI_API_KEY",
|
|
33266
|
+
"GROQ_API_KEY",
|
|
33267
|
+
"MISTRAL_API_KEY",
|
|
33268
|
+
"COHERE_API_KEY",
|
|
33269
|
+
"DEEPSEEK_API_KEY",
|
|
33270
|
+
"XAI_API_KEY",
|
|
33271
|
+
"OPENROUTER_API_KEY",
|
|
33272
|
+
"PERPLEXITY_API_KEY",
|
|
33273
|
+
"TOGETHER_API_KEY",
|
|
33274
|
+
"FIREWORKS_API_KEY",
|
|
33275
|
+
"HUGGINGFACE_API_KEY",
|
|
33276
|
+
"HF_TOKEN",
|
|
33277
|
+
"GITHUB_TOKEN",
|
|
33278
|
+
"GH_TOKEN",
|
|
33279
|
+
"NPM_TOKEN",
|
|
33280
|
+
"AWS_ACCESS_KEY_ID",
|
|
33281
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
33282
|
+
"AWS_SESSION_TOKEN",
|
|
33283
|
+
"AZURE_CLIENT_SECRET",
|
|
33284
|
+
"GITLAB_TOKEN",
|
|
33285
|
+
"SLACK_TOKEN",
|
|
33286
|
+
"STRIPE_SECRET_KEY",
|
|
33287
|
+
"TELEGRAM_BOT_TOKEN",
|
|
33288
|
+
"WRONGSTACK_VAULT_PASSPHRASE"
|
|
33289
|
+
]);
|
|
33290
|
+
function attachesWellKnownCredential(input) {
|
|
33291
|
+
if (!input || typeof input !== "object") return false;
|
|
33292
|
+
const envVars = input["envVars"];
|
|
33293
|
+
if (!Array.isArray(envVars)) return false;
|
|
33294
|
+
return envVars.some(
|
|
33295
|
+
(name) => typeof name === "string" && WELL_KNOWN_CREDENTIAL_ENV_VARS.has(name.toUpperCase())
|
|
33296
|
+
);
|
|
33297
|
+
}
|
|
32320
33298
|
|
|
32321
33299
|
// src/security/permission-helpers.ts
|
|
32322
33300
|
function matchesTrust(patterns, subject) {
|
|
@@ -32333,7 +33311,7 @@ function hasShellSubject(tool) {
|
|
|
32333
33311
|
]);
|
|
32334
33312
|
}
|
|
32335
33313
|
function alwaysAllowUnavailableReason(tool, input) {
|
|
32336
|
-
const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
|
|
33314
|
+
const subject = subjectForToolInput(tool.name, input, tool.subjectKey, tool.subjectFields);
|
|
32337
33315
|
if (subject !== void 0) return void 0;
|
|
32338
33316
|
return `"always allow" needs a subject to remember, and ${tool.name} calls do not carry one (no subjectKey, and no path/url/name input). Recording it would store a rule that can never match. Approve this call, or set a trust rule for ${tool.name} explicitly.`;
|
|
32339
33317
|
}
|
|
@@ -32393,8 +33371,18 @@ var AGENT_STATE_SENSITIVE_BASENAMES = /^(?:config\.json|config\.local\.json|trus
|
|
|
32393
33371
|
function unescapeGlobSubject(value) {
|
|
32394
33372
|
return value.replace(/\\([*?[\]])/g, "$1");
|
|
32395
33373
|
}
|
|
33374
|
+
function stripAdsSuffix(forwardSlashPath) {
|
|
33375
|
+
const cut = forwardSlashPath.lastIndexOf("/");
|
|
33376
|
+
const dir = cut === -1 ? "" : forwardSlashPath.slice(0, cut + 1);
|
|
33377
|
+
const base = cut === -1 ? forwardSlashPath : forwardSlashPath.slice(cut + 1);
|
|
33378
|
+
const colon = base.indexOf(":");
|
|
33379
|
+
if (colon === -1 || cut === -1 && colon === 1 && base.length <= 2) return forwardSlashPath;
|
|
33380
|
+
return dir + base.slice(0, colon);
|
|
33381
|
+
}
|
|
32396
33382
|
function normalizeForCompare(value) {
|
|
32397
|
-
const forward =
|
|
33383
|
+
const forward = stripAdsSuffix(
|
|
33384
|
+
unescapeGlobSubject(value).replace(/\\/g, "/").replace(/\/+$/, "")
|
|
33385
|
+
);
|
|
32398
33386
|
return process.platform === "win32" ? forward.toLowerCase() : forward;
|
|
32399
33387
|
}
|
|
32400
33388
|
function realpathOfNearestExisting(p) {
|
|
@@ -32431,7 +33419,7 @@ function isProtectedAgentStatePath(absPath) {
|
|
|
32431
33419
|
return AGENT_STATE_SENSITIVE_BASENAMES.test(path40.basename(normalizeForCompare(absPath)));
|
|
32432
33420
|
}
|
|
32433
33421
|
function pathLooksSensitive(rawPath) {
|
|
32434
|
-
const normalized = stripShellQuotes(rawPath).replace(/\\/g, "/");
|
|
33422
|
+
const normalized = stripAdsSuffix(stripShellQuotes(rawPath).replace(/\\/g, "/"));
|
|
32435
33423
|
if (SENSITIVE_READ_PATHS.some((pattern) => pattern.test(normalized))) return true;
|
|
32436
33424
|
return isProtectedAgentStatePath(normalized);
|
|
32437
33425
|
}
|
|
@@ -32455,10 +33443,24 @@ function shellCommandReadsSensitivePath(command) {
|
|
|
32455
33443
|
}
|
|
32456
33444
|
return false;
|
|
32457
33445
|
}
|
|
33446
|
+
function isSensitiveReadCall(tool, input) {
|
|
33447
|
+
const isReadTool = hasCapability(tool, ToolCapabilities.FS_READ) || tool.name === "read" || tool.name === "grep" || tool.name === "glob" || tool.name === "tree";
|
|
33448
|
+
if (isReadTool && inputPathLooksSensitive(input)) return true;
|
|
33449
|
+
const hasShellCap = hasCapability(tool, [
|
|
33450
|
+
ToolCapabilities.SHELL_ARBITRARY,
|
|
33451
|
+
ToolCapabilities.SHELL_RESTRICTED,
|
|
33452
|
+
ToolCapabilities.SHELL_EXEC
|
|
33453
|
+
]);
|
|
33454
|
+
if (!hasShellCap && tool.name !== "bash" && tool.name !== "shell" && tool.name !== "exec") {
|
|
33455
|
+
return false;
|
|
33456
|
+
}
|
|
33457
|
+
const command = shellCommandLineFromInput(input);
|
|
33458
|
+
return command ? shellCommandReadsSensitivePath(command) : false;
|
|
33459
|
+
}
|
|
32458
33460
|
|
|
32459
33461
|
// src/security/permission-explain.ts
|
|
32460
33462
|
function explainPermissionTrace(state, tool, input, ctx) {
|
|
32461
|
-
const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
|
|
33463
|
+
const subject = subjectForToolInput(tool.name, input, tool.subjectKey, tool.subjectFields);
|
|
32462
33464
|
const steps = [];
|
|
32463
33465
|
let winnerIndex = -1;
|
|
32464
33466
|
const add = (rule, matched, decision, source, detail) => {
|
|
@@ -32679,13 +33681,7 @@ function explainPermissionTrace(state, tool, input, ctx) {
|
|
|
32679
33681
|
}
|
|
32680
33682
|
};
|
|
32681
33683
|
}
|
|
32682
|
-
add(
|
|
32683
|
-
"yolo",
|
|
32684
|
-
true,
|
|
32685
|
-
"auto",
|
|
32686
|
-
"yolo",
|
|
32687
|
-
"YOLO mode is active \u2014 auto-approving every non-denied call"
|
|
32688
|
-
);
|
|
33684
|
+
add("yolo", true, "auto", "yolo", "YOLO mode is active \u2014 auto-approving every non-denied call");
|
|
32689
33685
|
winnerIndex = steps.length - 1;
|
|
32690
33686
|
return {
|
|
32691
33687
|
toolName: tool.name,
|
|
@@ -32956,7 +33952,14 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
|
|
|
32956
33952
|
static isMcpTool(name) {
|
|
32957
33953
|
return name.startsWith("mcp__");
|
|
32958
33954
|
}
|
|
32959
|
-
async evaluate(tool) {
|
|
33955
|
+
async evaluate(tool, input) {
|
|
33956
|
+
if (input !== void 0 && isSensitiveReadCall(tool, input)) {
|
|
33957
|
+
return {
|
|
33958
|
+
permission: "deny",
|
|
33959
|
+
source: "subagent_guard",
|
|
33960
|
+
reason: "subagents may not read credential-bearing paths \u2014 the leader must perform this read so the user can approve it"
|
|
33961
|
+
};
|
|
33962
|
+
}
|
|
32960
33963
|
const caps = tool.capabilities ?? [];
|
|
32961
33964
|
const hasAllowedCap = caps.some((c) => this.allowedCapabilities.includes(c));
|
|
32962
33965
|
const isMcp = _AutoApprovePermissionPolicy.isMcpTool(tool.name);
|
|
@@ -32983,8 +33986,8 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
|
|
|
32983
33986
|
}
|
|
32984
33987
|
allowOnce() {
|
|
32985
33988
|
}
|
|
32986
|
-
async explain(tool) {
|
|
32987
|
-
const decision = await this.evaluate(tool);
|
|
33989
|
+
async explain(tool, input) {
|
|
33990
|
+
const decision = await this.evaluate(tool, input);
|
|
32988
33991
|
return {
|
|
32989
33992
|
toolName: tool.name,
|
|
32990
33993
|
subject: null,
|
|
@@ -33034,6 +34037,17 @@ function fsWriteTargetPaths(input) {
|
|
|
33034
34037
|
}
|
|
33035
34038
|
return out;
|
|
33036
34039
|
}
|
|
34040
|
+
function mergeTrustEntries(exact, wildcard) {
|
|
34041
|
+
if (!exact) return wildcard;
|
|
34042
|
+
if (!wildcard) return exact;
|
|
34043
|
+
const deny = [...wildcard.deny ?? [], ...exact.deny ?? []];
|
|
34044
|
+
const merged = {
|
|
34045
|
+
...wildcard,
|
|
34046
|
+
...exact
|
|
34047
|
+
};
|
|
34048
|
+
if (deny.length > 0) merged.deny = [...new Set(deny)];
|
|
34049
|
+
return merged;
|
|
34050
|
+
}
|
|
33037
34051
|
var DefaultPermissionPolicy = class {
|
|
33038
34052
|
policy = {};
|
|
33039
34053
|
loaded = false;
|
|
@@ -33072,6 +34086,7 @@ var DefaultPermissionPolicy = class {
|
|
|
33072
34086
|
yoloBlockedAsDestructive(tool, input, ctx) {
|
|
33073
34087
|
if (!this.yolo || this.yoloDestructive) return false;
|
|
33074
34088
|
if (this.hasAgentStateWriteTarget(tool, input, ctx)) return true;
|
|
34089
|
+
if (attachesWellKnownCredential(input)) return true;
|
|
33075
34090
|
const isShellSurface = tool.name === "bash" || tool.name === "exec" || (tool.capabilities ?? []).includes("shell.arbitrary");
|
|
33076
34091
|
if (!isShellSurface) return false;
|
|
33077
34092
|
const command = getInputString(input, "command") ?? shellCommandLineFromInput(input);
|
|
@@ -33165,8 +34180,8 @@ var DefaultPermissionPolicy = class {
|
|
|
33165
34180
|
};
|
|
33166
34181
|
}
|
|
33167
34182
|
const namespaceEntry = this.findNamespaceEntry(tool.name);
|
|
33168
|
-
const entry = this.policy[tool.name]
|
|
33169
|
-
const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
|
|
34183
|
+
const entry = mergeTrustEntries(this.policy[tool.name], namespaceEntry);
|
|
34184
|
+
const subject = subjectForToolInput(tool.name, input, tool.subjectKey, tool.subjectFields);
|
|
33170
34185
|
const cacheKey = `${tool.name}::${subject ?? tool.name}`;
|
|
33171
34186
|
const evalKey = `${cacheKey}::${permissionFingerprint(tool)}`;
|
|
33172
34187
|
if (tool.name !== "write" && !this.hasAgentStateWriteTarget(tool, input, ctx)) {
|
|
@@ -33183,15 +34198,6 @@ var DefaultPermissionPolicy = class {
|
|
|
33183
34198
|
this._evalCache.set(evalKey, decision);
|
|
33184
34199
|
return decision;
|
|
33185
34200
|
}
|
|
33186
|
-
if (this.sessionAllowed.has(cacheKey)) {
|
|
33187
|
-
this.sessionAllowed.delete(cacheKey);
|
|
33188
|
-
const decision = {
|
|
33189
|
-
permission: "auto",
|
|
33190
|
-
source: "trust",
|
|
33191
|
-
reason: "session one-shot allow (user pressed yes)"
|
|
33192
|
-
};
|
|
33193
|
-
return decision;
|
|
33194
|
-
}
|
|
33195
34201
|
if (entry?.deny && subject && matchesTrust(entry.deny, subject)) {
|
|
33196
34202
|
this._logDeny(tool.name, subject, "matched deny pattern");
|
|
33197
34203
|
const decision = {
|
|
@@ -33202,6 +34208,15 @@ var DefaultPermissionPolicy = class {
|
|
|
33202
34208
|
this._evalCache.set(evalKey, decision);
|
|
33203
34209
|
return decision;
|
|
33204
34210
|
}
|
|
34211
|
+
if (this.sessionAllowed.has(cacheKey)) {
|
|
34212
|
+
this.sessionAllowed.delete(cacheKey);
|
|
34213
|
+
const decision = {
|
|
34214
|
+
permission: "auto",
|
|
34215
|
+
source: "trust",
|
|
34216
|
+
reason: "session one-shot allow (user pressed yes)"
|
|
34217
|
+
};
|
|
34218
|
+
return decision;
|
|
34219
|
+
}
|
|
33205
34220
|
if (tool.permission === "deny") {
|
|
33206
34221
|
this._logDeny(tool.name, subject, "tool default deny");
|
|
33207
34222
|
const decision = {
|
|
@@ -33212,6 +34227,7 @@ var DefaultPermissionPolicy = class {
|
|
|
33212
34227
|
this._evalCache.set(evalKey, decision);
|
|
33213
34228
|
return decision;
|
|
33214
34229
|
}
|
|
34230
|
+
const denyUnevaluated = Boolean(entry?.deny?.length) && subject === void 0;
|
|
33215
34231
|
const allowMatches = hasShellSubject(tool) ? matchesCommandTrust : matchesTrust;
|
|
33216
34232
|
if (entry?.allow && subject && allowMatches(entry.allow, subject)) {
|
|
33217
34233
|
const decision = {
|
|
@@ -33222,7 +34238,7 @@ var DefaultPermissionPolicy = class {
|
|
|
33222
34238
|
this._evalCache.set(evalKey, decision);
|
|
33223
34239
|
return decision;
|
|
33224
34240
|
}
|
|
33225
|
-
if (entry?.auto) {
|
|
34241
|
+
if (entry?.auto && !denyUnevaluated) {
|
|
33226
34242
|
const decision = { permission: "auto", source: "trust" };
|
|
33227
34243
|
this._evalCache.set(evalKey, decision);
|
|
33228
34244
|
return decision;
|
|
@@ -33322,19 +34338,10 @@ var DefaultPermissionPolicy = class {
|
|
|
33322
34338
|
}
|
|
33323
34339
|
return { permission: "confirm", source: "default" };
|
|
33324
34340
|
}
|
|
34341
|
+
// Delegates to the shared helper so the subagent policy applies the exact
|
|
34342
|
+
// same rule — see `isSensitiveReadCall` in ./permission-helpers.ts.
|
|
33325
34343
|
isSensitiveReadCall(tool, input) {
|
|
33326
|
-
|
|
33327
|
-
if (isReadTool && inputPathLooksSensitive(input)) return true;
|
|
33328
|
-
const hasShellCap = hasCapability(tool, [
|
|
33329
|
-
ToolCapabilities.SHELL_ARBITRARY,
|
|
33330
|
-
ToolCapabilities.SHELL_RESTRICTED,
|
|
33331
|
-
ToolCapabilities.SHELL_EXEC
|
|
33332
|
-
]);
|
|
33333
|
-
if (!hasShellCap && tool.name !== "bash" && tool.name !== "shell" && tool.name !== "exec") {
|
|
33334
|
-
return false;
|
|
33335
|
-
}
|
|
33336
|
-
const command = shellCommandLineFromInput(input);
|
|
33337
|
-
return command ? shellCommandReadsSensitivePath(command) : false;
|
|
34344
|
+
return isSensitiveReadCall(tool, input);
|
|
33338
34345
|
}
|
|
33339
34346
|
async trust(rule) {
|
|
33340
34347
|
if (!this.loaded) await this.reload();
|
|
@@ -33458,7 +34465,7 @@ function walk2(node, vault, transform) {
|
|
|
33458
34465
|
}
|
|
33459
34466
|
return out;
|
|
33460
34467
|
}
|
|
33461
|
-
var SECRET_KEY_PATTERN = /(?:
|
|
34468
|
+
var SECRET_KEY_PATTERN = /(?:api[-_]?key|auth[-_]?token|authorization|proxy-authorization|cookie|bearer|secret|password|passwd|pwd|refresh[-_]?token|session[-_]?key|access[_-]?token|private[_-]?key|token\b)/i;
|
|
33462
34469
|
var NON_SECRET_OVERRIDES = /* @__PURE__ */ new Set(["publickey", "public_key"]);
|
|
33463
34470
|
function isSecretField(name) {
|
|
33464
34471
|
const lc = name.toLowerCase();
|
|
@@ -33567,6 +34574,14 @@ function keyFileNeedsHardening(keyFile, opts) {
|
|
|
33567
34574
|
}
|
|
33568
34575
|
return false;
|
|
33569
34576
|
}
|
|
34577
|
+
function mkdirSecretDirSync(dir) {
|
|
34578
|
+
fs14.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
34579
|
+
if (process.platform === "win32") return;
|
|
34580
|
+
try {
|
|
34581
|
+
fs14.chmodSync(dir, 448);
|
|
34582
|
+
} catch {
|
|
34583
|
+
}
|
|
34584
|
+
}
|
|
33570
34585
|
function writeKeyFileAtomicSync(keyFile, content) {
|
|
33571
34586
|
const tmp = `${keyFile}.${randomBytes3(4).toString("hex")}.tmp`;
|
|
33572
34587
|
const fd = fs14.openSync(tmp, "w", 384);
|
|
@@ -33714,7 +34729,7 @@ var DefaultSecretVault = class {
|
|
|
33714
34729
|
const oldVersion = this._keyVersion;
|
|
33715
34730
|
const newKey = randomBytes3(KEY_BYTES);
|
|
33716
34731
|
const newVersion = oldVersion + 1;
|
|
33717
|
-
|
|
34732
|
+
mkdirSecretDirSync(path42.dirname(this.keyFile));
|
|
33718
34733
|
const passphrase = getVaultPassphrase();
|
|
33719
34734
|
if (passphrase) {
|
|
33720
34735
|
writeKeyFileAtomicSync(this.keyFile, wrapDataKey(newKey, newVersion, passphrase));
|
|
@@ -33798,7 +34813,7 @@ var DefaultSecretVault = class {
|
|
|
33798
34813
|
} catch (err) {
|
|
33799
34814
|
if (err.code !== "ENOENT") throw err;
|
|
33800
34815
|
}
|
|
33801
|
-
|
|
34816
|
+
mkdirSecretDirSync(path42.dirname(this.keyFile));
|
|
33802
34817
|
const key = randomBytes3(KEY_BYTES);
|
|
33803
34818
|
const passphrase = getVaultPassphrase();
|
|
33804
34819
|
const initialBytes = passphrase ? wrapDataKey(key, 1, passphrase) : key;
|
|
@@ -34500,6 +35515,17 @@ var IN_PROJECT_DENIED_PATHS = [
|
|
|
34500
35515
|
// See discover-mailbox-bridge.ts:findWorkspaceCliEntry.
|
|
34501
35516
|
path: "features.mailboxBridge",
|
|
34502
35517
|
reason: "Enables the mailbox bridge, whose CLI-entry resolution walks up from the project root \u2014 a repo-supplied packages/cli/dist/index.js would be spawned on WebUI boot."
|
|
35518
|
+
},
|
|
35519
|
+
{
|
|
35520
|
+
// `plugins` is already denied above, so a repo cannot ADD a plugin. This
|
|
35521
|
+
// closes the other half: a repo could previously ship
|
|
35522
|
+
// `{"features":{"pluginsTrust":false}}` and switch off the integrity gate
|
|
35523
|
+
// for plugins the user had ALREADY installed globally — disarming the
|
|
35524
|
+
// trust-on-first-use pin that exists to catch a supply-chain update
|
|
35525
|
+
// rewriting a plugin's entry file. Same operator-owned class as the
|
|
35526
|
+
// switches above.
|
|
35527
|
+
path: "features.pluginsTrust",
|
|
35528
|
+
reason: "Disables the plugin trust-on-first-use integrity gate, re-trusting changed code in already-installed global plugins."
|
|
34503
35529
|
}
|
|
34504
35530
|
];
|
|
34505
35531
|
function deleteNestedPath(target, path47) {
|
|
@@ -35472,7 +36498,7 @@ function deepFreeze(obj) {
|
|
|
35472
36498
|
}
|
|
35473
36499
|
|
|
35474
36500
|
// src/storage/plan-store.ts
|
|
35475
|
-
import { randomUUID as
|
|
36501
|
+
import { randomUUID as randomUUID20 } from "node:crypto";
|
|
35476
36502
|
import * as fsp30 from "node:fs/promises";
|
|
35477
36503
|
async function loadPlan(filePath, events) {
|
|
35478
36504
|
const t0 = Date.now();
|
|
@@ -35571,7 +36597,7 @@ function emptyPlan(sessionId, title) {
|
|
|
35571
36597
|
function addPlanItem(plan, title, details) {
|
|
35572
36598
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
35573
36599
|
const item = {
|
|
35574
|
-
id: `plan_${Date.now()}_${
|
|
36600
|
+
id: `plan_${Date.now()}_${randomUUID20().slice(0, 6)}`,
|
|
35575
36601
|
title,
|
|
35576
36602
|
details,
|
|
35577
36603
|
status: "open",
|
|
@@ -35643,7 +36669,7 @@ function deriveTodosFromPlanItem(plan, idOrIndex, subtasks) {
|
|
|
35643
36669
|
if (subtasks && subtasks.length > 0) {
|
|
35644
36670
|
for (const st of subtasks) {
|
|
35645
36671
|
todos.push({
|
|
35646
|
-
id: `todo_${Date.now()}_${
|
|
36672
|
+
id: `todo_${Date.now()}_${randomUUID20().slice(0, 6)}`,
|
|
35647
36673
|
content: st,
|
|
35648
36674
|
status: "pending",
|
|
35649
36675
|
promotedFromPlan: item.id
|