@wrongstack/core 0.308.7 → 0.309.0

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.
@@ -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,20 @@ 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
+ // Follow fleet worktree policy (NOT 'required'): mutation targets are
5579
+ // often freshly written and uncommitted — a worktree spawned from HEAD
5580
+ // would not contain them and every mutant would drift. Callers pass
5581
+ // `worktree: 'off'` in the mutation_test input for uncommitted targets.
5582
+ worktree: "auto",
5583
+ // Report travels via submit_result + final text, not the leader's stream.
5584
+ textStream: "silent",
5585
+ toolStream: "silent"
5586
+ };
5443
5587
  var CRITIC_AGENT = defineAgent("critic", "Critic");
5444
5588
  var GENERIC_AGENT = defineAgent("generic", "Generic Project Agent");
5445
5589
  function withDispatchMetadata(definition) {
@@ -5459,6 +5603,7 @@ var FLEET_ROSTER = {
5459
5603
  generic: GENERIC_AGENT,
5460
5604
  "shadow-agent": SHADOW_AGENT,
5461
5605
  "explore-companion": EXPLORE_COMPANION_AGENT,
5606
+ "chaos-monkey": CHAOS_MONKEY_AGENT,
5462
5607
  ...Object.fromEntries(
5463
5608
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, withDispatchMetadata(d)])
5464
5609
  )
@@ -5489,6 +5634,16 @@ var FLEET_ROSTER_BUDGETS = {
5489
5634
  maxTokens: 96e3,
5490
5635
  maxCostUsd: 0.5
5491
5636
  },
5637
+ "chaos-monkey": {
5638
+ // A mutation pass is many short apply/run/restore cycles — per-mutant
5639
+ // work is tiny, but a large plan (25 mutants/file × N files) needs
5640
+ // headroom. Idle-based reaping covers a stalled pass.
5641
+ idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
5642
+ maxIterations: 2e3,
5643
+ maxToolCalls: 6e3,
5644
+ maxTokens: 96e3,
5645
+ maxCostUsd: 0.5
5646
+ },
5492
5647
  ...Object.fromEntries(
5493
5648
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.budget])
5494
5649
  )
@@ -6228,7 +6383,7 @@ async function readSubagentPartial(opts, subagentId) {
6228
6383
  }
6229
6384
 
6230
6385
  // src/coordination/director.ts
6231
- import { randomUUID as randomUUID12 } from "node:crypto";
6386
+ import { randomUUID as randomUUID13 } from "node:crypto";
6232
6387
  import * as fsp25 from "node:fs/promises";
6233
6388
 
6234
6389
  // src/core/instruction-template.ts
@@ -8244,7 +8399,7 @@ ${JSON.stringify(result.result, null, 2)}
8244
8399
  };
8245
8400
 
8246
8401
  // src/coordination/director-tools.ts
8247
- import { randomUUID as randomUUID8 } from "node:crypto";
8402
+ import { randomUUID as randomUUID9 } from "node:crypto";
8248
8403
  import {
8249
8404
  completeKanbanDispatch,
8250
8405
  failKanbanDispatch,
@@ -9444,6 +9599,413 @@ function excerpt(text, max) {
9444
9599
  ...(truncated)`;
9445
9600
  }
9446
9601
 
9602
+ // src/coordination/director-mutation-test-tool.ts
9603
+ import { randomUUID as randomUUID8 } from "node:crypto";
9604
+ import { readFileSync as readFileSync9 } from "node:fs";
9605
+ import { isAbsolute as isAbsolute2, join as join10 } from "node:path";
9606
+
9607
+ // src/coordination/mutation-engine.ts
9608
+ var TOKEN_PATTERNS = [
9609
+ {
9610
+ kind: "relax-boundary",
9611
+ // `>` not followed by `=` and not part of `=>` or `>>`; require code-ish
9612
+ // context on both sides so generic text (JSX, strings) is not touched.
9613
+ regex: /(?<=[\w\)\]\}'"`])\s*\x20?(?<op>>(?!=|>))/g,
9614
+ replace: () => ">="
9615
+ },
9616
+ {
9617
+ kind: "tighten-boundary",
9618
+ regex: /(?<=[\w\)\]\}'"`])\s*\x20?(?<op>>=)/g,
9619
+ replace: () => ">"
9620
+ },
9621
+ {
9622
+ kind: "arith-plus-to-minus",
9623
+ // `+` between operands (binary), not `++`, unary `+x`, or `+=`.
9624
+ regex: /(?<=[\w\)\]\}'"`])\s*\x20?(?<op>\+(?!\+|=))/g,
9625
+ replace: () => "-"
9626
+ },
9627
+ {
9628
+ kind: "arith-minus-to-plus",
9629
+ // Binary `-` between operands, not `--`, `-=` or negative-number literal.
9630
+ regex: /(?<=[\w\)\]\}'"`])\s*\x20?(?<op>-(?!-|=))/g,
9631
+ replace: () => "+"
9632
+ },
9633
+ {
9634
+ kind: "negate-boolean",
9635
+ // Standalone boolean literals used as values, not property names.
9636
+ regex: /(?<![.\w$])(?<op>true|false)(?![\w$])/g,
9637
+ replace: (m) => m === "true" ? "false" : "true"
9638
+ },
9639
+ {
9640
+ kind: "return-null",
9641
+ // `return <expr>;` where expr is not already null/undefined/void.
9642
+ regex: /(?<indent>\breturn\b)(?<expr>\s+[^;{}\n]+?)\s*;/g,
9643
+ replace: () => "return null;"
9644
+ }
9645
+ ];
9646
+ function planMutations(file, source, opts = {}) {
9647
+ const maxPerFile = opts.maxPerFile ?? 25;
9648
+ const out = [];
9649
+ const lines = source.split("\n");
9650
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
9651
+ const line = lines[lineIdx];
9652
+ const t = line.trim();
9653
+ if (t.startsWith("//") || t.startsWith("*") || t.startsWith("/*")) continue;
9654
+ for (const pattern of TOKEN_PATTERNS) {
9655
+ pattern.regex.lastIndex = 0;
9656
+ let m;
9657
+ while ((m = pattern.regex.exec(line)) !== null) {
9658
+ const token = m.groups?.["op"] ?? m[0];
9659
+ const tokenStart = m.index + m[0].indexOf(token);
9660
+ if (isMasked(line, tokenStart, token.length)) continue;
9661
+ const original = line.slice(tokenStart, tokenStart + token.length);
9662
+ const replacement = pattern.replace(token);
9663
+ if (replacement === original) continue;
9664
+ out.push({
9665
+ id: `${pattern.kind}#${lineIdx + 1}#${tokenStart + 1}`,
9666
+ kind: pattern.kind,
9667
+ file,
9668
+ line: lineIdx + 1,
9669
+ column: tokenStart + 1,
9670
+ original,
9671
+ replacement
9672
+ });
9673
+ }
9674
+ }
9675
+ if (out.length >= maxPerFile) break;
9676
+ }
9677
+ return out.slice(0, maxPerFile);
9678
+ }
9679
+ function isMasked(line, start, len) {
9680
+ let inSingle = false;
9681
+ let inDouble = false;
9682
+ for (let i = 0; i < start; i++) {
9683
+ const c = line[i];
9684
+ const prev = i > 0 ? line[i - 1] : void 0;
9685
+ if (c === "'" && prev !== "\\") inSingle = !inSingle;
9686
+ else if (c === '"' && prev !== "\\") inDouble = !inDouble;
9687
+ if (!inSingle && !inDouble && c === "/" && prev === "/") return true;
9688
+ }
9689
+ if (inSingle || inDouble) return true;
9690
+ const window = line.slice(start, start + len);
9691
+ return /['"]/.test(window);
9692
+ }
9693
+ function parseMutationReport(text) {
9694
+ const candidates = [];
9695
+ const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/);
9696
+ if (fence?.[1]) candidates.push(fence[1].trim());
9697
+ const firstBrace = text.indexOf("{");
9698
+ if (firstBrace >= 0) candidates.push(extractBalancedObject(text, firstBrace));
9699
+ for (const candidate of candidates) {
9700
+ if (!candidate) continue;
9701
+ try {
9702
+ const parsed = JSON.parse(candidate);
9703
+ if (!Array.isArray(parsed.mutants)) continue;
9704
+ return {
9705
+ mutants: parsed.mutants.map(normalizeMutantEntry).filter((x) => Boolean(x)),
9706
+ summary: typeof parsed.summary === "string" ? parsed.summary : void 0
9707
+ };
9708
+ } catch {
9709
+ }
9710
+ }
9711
+ return void 0;
9712
+ }
9713
+ function extractBalancedObject(text, start) {
9714
+ let depth = 0;
9715
+ let inString = false;
9716
+ let escaped = false;
9717
+ for (let i = start; i < text.length; i++) {
9718
+ const c = text[i];
9719
+ if (escaped) {
9720
+ escaped = false;
9721
+ continue;
9722
+ }
9723
+ if (c === "\\") {
9724
+ escaped = true;
9725
+ continue;
9726
+ }
9727
+ if (c === '"') inString = !inString;
9728
+ if (inString) continue;
9729
+ if (c === "{") depth++;
9730
+ else if (c === "}") {
9731
+ depth--;
9732
+ if (depth === 0) return text.slice(start, i + 1);
9733
+ }
9734
+ }
9735
+ return text.slice(start);
9736
+ }
9737
+ function normalizeMutantEntry(value) {
9738
+ if (typeof value !== "object" || value === null) return void 0;
9739
+ const rec = value;
9740
+ const id = typeof rec["id"] === "string" ? rec["id"] : void 0;
9741
+ const status = rec["status"];
9742
+ if (!id || status !== "killed" && status !== "survived" && status !== "skipped") {
9743
+ return void 0;
9744
+ }
9745
+ return {
9746
+ id,
9747
+ file: typeof rec["file"] === "string" ? rec["file"] : "",
9748
+ line: typeof rec["line"] === "number" ? rec["line"] : 0,
9749
+ kind: typeof rec["kind"] === "string" ? rec["kind"] : "",
9750
+ status,
9751
+ evidence: typeof rec["evidence"] === "string" ? rec["evidence"] : void 0
9752
+ };
9753
+ }
9754
+
9755
+ // src/coordination/director-mutation-test-tool.ts
9756
+ var DEFAULT_MAX_PER_FILE = 10;
9757
+ var DEFAULT_MAX_STRENGTHEN_ATTEMPTS = 2;
9758
+ var CHAOS_ROLE = "chaos-monkey";
9759
+ function makeMutationTestTool(director, roster, opts = {}) {
9760
+ return {
9761
+ name: "mutation_test",
9762
+ 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.",
9763
+ 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.",
9764
+ permission: "auto",
9765
+ mutating: false,
9766
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
9767
+ inputSchema: {
9768
+ type: "object",
9769
+ properties: {
9770
+ targets: {
9771
+ type: "array",
9772
+ items: { type: "string" },
9773
+ description: "Project-relative (or absolute) source files to mutate. Keep to files changed by the current task."
9774
+ },
9775
+ testCommand: {
9776
+ type: "string",
9777
+ description: 'Exact command that runs the relevant tests, e.g. "pnpm exec vitest run packages/core/tests/coordination/mutation-engine.test.ts".'
9778
+ },
9779
+ cwd: { type: "string", description: "Working directory for the test command." },
9780
+ maxPerFile: {
9781
+ type: "number",
9782
+ minimum: 1,
9783
+ maximum: 25,
9784
+ description: "Mutant cap per file per pass. Default 10."
9785
+ },
9786
+ maxStrengthenAttempts: {
9787
+ type: "number",
9788
+ minimum: 0,
9789
+ maximum: 5,
9790
+ description: "Strengthen\u2192re-verify rounds. Default 2 when repairSubagentId is set, else 0."
9791
+ },
9792
+ repairSubagentId: {
9793
+ type: "string",
9794
+ description: "Subagent that owns the tests. When set and mutants survive, it receives a strengthen-tests task and the survivors are re-verified."
9795
+ },
9796
+ chaosWorktree: {
9797
+ anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
9798
+ description: "Worktree override for the chaos agent. Use 'off' when targets are uncommitted \u2014 a worktree from HEAD would not contain them."
9799
+ },
9800
+ timeoutMs: { type: "number", minimum: 1, description: "Per-task timeout for chaos/strengthen/rerun tasks." },
9801
+ reportOnly: {
9802
+ type: "boolean",
9803
+ description: "Skip the strengthen loop even when survivors exist. Default false."
9804
+ }
9805
+ },
9806
+ required: ["targets", "testCommand"],
9807
+ additionalProperties: false
9808
+ },
9809
+ async execute(input, ctx) {
9810
+ const i = normalizeMutationTestInput(input);
9811
+ const root = opts.projectRoot ?? ctx.projectRoot;
9812
+ const plan = buildPlan(i, root);
9813
+ if (plan.length === 0) {
9814
+ return {
9815
+ verdict: "inconclusive",
9816
+ passed: false,
9817
+ error: "No mutable sites found in the given targets (after comment/string filtering)."
9818
+ };
9819
+ }
9820
+ const chaosSubagentId = await director.spawn(
9821
+ makeChaosConfig(roster, i.chaosWorktree ?? "off")
9822
+ );
9823
+ const chaosTaskId = await director.assign({
9824
+ id: randomUUID8(),
9825
+ subagentId: chaosSubagentId,
9826
+ description: buildChaosTask(plan, i, 1, []),
9827
+ timeoutMs: i.timeoutMs
9828
+ });
9829
+ const [chaosResult] = await director.awaitTasks([chaosTaskId]);
9830
+ const pass1 = collectOutcomes(chaosResult, plan);
9831
+ const survivors = pass1.filter((m) => m.status === "survived");
9832
+ const maxAttempts = clamp(
9833
+ i.maxStrengthenAttempts ?? (i.repairSubagentId && !i.reportOnly ? DEFAULT_MAX_STRENGTHEN_ATTEMPTS : 0),
9834
+ 0,
9835
+ 5
9836
+ );
9837
+ const attempts = [];
9838
+ let current = survivors;
9839
+ while (current.length > 0 && attempts.length < maxAttempts && i.repairSubagentId) {
9840
+ const attemptNo = attempts.length + 1;
9841
+ const strengthenTaskId = await director.assign({
9842
+ id: randomUUID8(),
9843
+ subagentId: i.repairSubagentId,
9844
+ description: buildStrengthenTask(current, i, attemptNo),
9845
+ timeoutMs: i.timeoutMs
9846
+ });
9847
+ const [strengthenResult] = await director.awaitTasks([strengthenTaskId]);
9848
+ if (strengthenResult?.status !== "success") {
9849
+ attempts.push({
9850
+ attempt: attemptNo,
9851
+ survivorsBefore: current,
9852
+ strengthenResult: strengthenResult ? { taskId: strengthenResult.taskId, status: strengthenResult.status } : void 0,
9853
+ survivorsAfter: current,
9854
+ suspectedEquivalent: []
9855
+ });
9856
+ break;
9857
+ }
9858
+ const survivorPlan = plan.filter((p) => current.some((s) => s.id === p.id));
9859
+ const rerunSubagentId = await director.spawn(
9860
+ makeChaosConfig(roster, i.chaosWorktree ?? "off")
9861
+ );
9862
+ const rerunTaskId = await director.assign({
9863
+ id: randomUUID8(),
9864
+ subagentId: rerunSubagentId,
9865
+ description: buildChaosTask(survivorPlan, i, attemptNo + 1, current),
9866
+ timeoutMs: i.timeoutMs
9867
+ });
9868
+ const [rerunResult] = await director.awaitTasks([rerunTaskId]);
9869
+ const passN = collectOutcomes(rerunResult, survivorPlan);
9870
+ const stillSurviving = passN.filter((m) => m.status === "survived" || m.status === "skipped");
9871
+ attempts.push({
9872
+ attempt: attemptNo,
9873
+ survivorsBefore: current,
9874
+ strengthenResult: { taskId: strengthenResult.taskId, status: strengthenResult.status },
9875
+ rerunResult: { taskId: rerunTaskId, status: rerunResult?.status ?? "unknown" },
9876
+ survivorsAfter: stillSurviving,
9877
+ suspectedEquivalent: stillSurviving.filter((m) => current.some((c) => c.id === m.id)).map((m) => m.id)
9878
+ });
9879
+ current = stillSurviving.filter((m) => m.status === "survived");
9880
+ if (passN.every((m) => m.status === "skipped")) break;
9881
+ }
9882
+ const finalSurvivors = current;
9883
+ const verifiedCount = pass1.filter((m) => m.status !== "skipped").length;
9884
+ const skippedCount = pass1.filter((m) => m.status === "skipped").length;
9885
+ const score = plan.length === 0 ? 0 : pass1.filter((m) => m.status === "killed").length / plan.length;
9886
+ const verdict = verifiedCount === 0 ? "inconclusive" : finalSurvivors.length === 0 ? skippedCount > 0 ? "partial" : "pass" : score >= 0.8 ? "partial" : "fail";
9887
+ return {
9888
+ verdict,
9889
+ passed: verdict === "pass",
9890
+ mutationScore: Number.parseFloat(score.toFixed(3)),
9891
+ planned: plan.length,
9892
+ killed: pass1.filter((m) => m.status === "killed").length,
9893
+ survived: pass1.filter((m) => m.status === "survived").length,
9894
+ skipped: pass1.filter((m) => m.status === "skipped").length,
9895
+ finalSurvivors: finalSurvivors.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
9896
+ suspectedEquivalent: attempts.flatMap((a) => a.suspectedEquivalent),
9897
+ strengthenAttempts: attempts.length,
9898
+ attempts,
9899
+ chaosTaskId,
9900
+ nextAction: finalSurvivors.length === 0 ? "accept" : attempts.length >= maxAttempts && i.repairSubagentId ? "manual_review_survivors" : "strengthen_tests"
9901
+ };
9902
+ }
9903
+ };
9904
+ }
9905
+ function normalizeMutationTestInput(input) {
9906
+ const raw = input ?? {};
9907
+ const targets = stringArray2(raw["targets"]) ?? [];
9908
+ const testCommand = typeof raw["testCommand"] === "string" ? raw["testCommand"].trim() : "";
9909
+ return {
9910
+ targets: targets.filter(Boolean),
9911
+ testCommand,
9912
+ cwd: typeof raw["cwd"] === "string" && raw["cwd"].trim() ? raw["cwd"].trim() : void 0,
9913
+ maxPerFile: typeof raw["maxPerFile"] === "number" ? raw["maxPerFile"] : void 0,
9914
+ maxStrengthenAttempts: typeof raw["maxStrengthenAttempts"] === "number" ? raw["maxStrengthenAttempts"] : void 0,
9915
+ repairSubagentId: typeof raw["repairSubagentId"] === "string" && raw["repairSubagentId"].trim() ? raw["repairSubagentId"].trim() : void 0,
9916
+ chaosWorktree: raw["chaosWorktree"] ?? void 0,
9917
+ timeoutMs: typeof raw["timeoutMs"] === "number" ? raw["timeoutMs"] : void 0,
9918
+ reportOnly: raw["reportOnly"] === true
9919
+ };
9920
+ }
9921
+ function clamp(n, lo, hi) {
9922
+ return Math.min(hi, Math.max(lo, n));
9923
+ }
9924
+ function buildPlan(i, projectRoot) {
9925
+ const plan = [];
9926
+ for (const target of i.targets) {
9927
+ const abs = isAbsolute2(target) ? target : join10(projectRoot ?? process.cwd(), target);
9928
+ let source;
9929
+ try {
9930
+ source = readFileSync9(abs, "utf8");
9931
+ } catch {
9932
+ continue;
9933
+ }
9934
+ plan.push(...planMutations(target, source, { maxPerFile: i.maxPerFile ?? DEFAULT_MAX_PER_FILE }));
9935
+ }
9936
+ return plan;
9937
+ }
9938
+ function makeChaosConfig(roster, worktree) {
9939
+ const base = roster?.[CHAOS_ROLE] ?? getAgentDefinition(CHAOS_ROLE)?.config ?? { name: "Chaos Monkey", role: CHAOS_ROLE };
9940
+ return { ...instantiateRosterConfig2(CHAOS_ROLE, base), worktree };
9941
+ }
9942
+ function buildChaosTask(plan, i, pass, priorSurvivors) {
9943
+ const mutants = plan.map(
9944
+ (m) => `- ${m.id} | ${m.file}:${m.line}:${m.column} | ${m.kind} | "${m.original}" -> "${m.replacement}"`
9945
+ ).join("\n");
9946
+ const prior = priorSurvivors.length > 0 ? `
9947
+ These mutants survived a previous pass (pass ${pass - 1}) \u2014 re-verify them against the STRENGTHENED tests:
9948
+ ${priorSurvivors.map((s) => `- ${s.id} (${s.kind} @ ${s.file}:${s.line})`).join("\n")}` : "";
9949
+ return [
9950
+ "Execute this deterministic mutation plan against the current checkout.",
9951
+ "",
9952
+ "For each mutant, in order:",
9953
+ "1. Apply ONLY that mutation at its exact (file, line, column).",
9954
+ `2. Run the test command: ${i.testCommand}${i.cwd ? ` (cwd: ${i.cwd})` : ""}`,
9955
+ "3. Record killed (tests failed \u2014 quote first failing assertion) or survived (suite green).",
9956
+ "4. Restore the file byte-for-byte before the next mutant.",
9957
+ "",
9958
+ "Mutants:",
9959
+ mutants,
9960
+ prior,
9961
+ "",
9962
+ "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.",
9963
+ "Finish with submit_result, then repeat the same JSON as your final text."
9964
+ ].join("\n");
9965
+ }
9966
+ function buildStrengthenTask(survivors, i, attempt) {
9967
+ return [
9968
+ `Strengthen the tests so these SURVIVING mutants die (attempt ${attempt}).`,
9969
+ "",
9970
+ "Each survivor below was a deliberate sabotage of production code that the current suite did NOT catch:",
9971
+ ...survivors.map((s) => `- ${s.id} | ${s.file}:${s.line} | ${s.kind}${s.evidence ? ` | ${s.evidence}` : ""}`),
9972
+ "",
9973
+ `Test command that must fail under each mutant: ${i.testCommand}`,
9974
+ "",
9975
+ "For each survivor add or tighten exactly one assertion that pins the sabotaged boundary/behavior. Do not change production code. Do not weaken other tests. Run the suite green on clean code before finishing."
9976
+ ].join("\n");
9977
+ }
9978
+ function collectOutcomes(result, plan) {
9979
+ const fromText = parseTextOutcomes(result);
9980
+ if (fromText.length > 0) {
9981
+ const planned = new Set(plan.map((p) => p.id));
9982
+ const matched = fromText.filter((m) => planned.has(m.id));
9983
+ if (matched.length > 0) return matched;
9984
+ }
9985
+ return plan.map((p) => ({
9986
+ id: p.id,
9987
+ file: p.file,
9988
+ line: p.line,
9989
+ kind: p.kind,
9990
+ status: "skipped",
9991
+ evidence: result ? `chaos task ended ${result.status}` : "chaos task produced no result"
9992
+ }));
9993
+ }
9994
+ function parseTextOutcomes(result) {
9995
+ const text = typeof result?.result === "string" ? result.result : void 0;
9996
+ if (!text) return [];
9997
+ const parsed = parseMutationReport(text);
9998
+ if (!parsed) return [];
9999
+ return parsed.mutants.map((m) => ({
10000
+ id: m.id,
10001
+ file: m.file,
10002
+ line: m.line,
10003
+ kind: m.kind,
10004
+ status: m.status,
10005
+ evidence: m.evidence
10006
+ }));
10007
+ }
10008
+
9447
10009
  // src/coordination/director-tools.ts
9448
10010
  function makeSpawnTool(director, roster) {
9449
10011
  const dispatchCatalog = () => {
@@ -9736,7 +10298,7 @@ function makeKanbanQueueTool(director, roster) {
9736
10298
  try {
9737
10299
  const config = buildKanbanSubagentConfig(claim.task, i, roster, instantiateRosterConfig2);
9738
10300
  subagentId = await director.spawn(config);
9739
- const dispatchTaskId = randomUUID8();
10301
+ const dispatchTaskId = randomUUID9();
9740
10302
  const taskSpec = {
9741
10303
  id: dispatchTaskId,
9742
10304
  subagentId,
@@ -10012,6 +10574,7 @@ function buildDirectorToolset(director, roster) {
10012
10574
  makeAskResultTool(director),
10013
10575
  makeRollUpTool(director),
10014
10576
  makeQualityGateTool(director, roster),
10577
+ makeMutationTestTool(director, roster),
10015
10578
  makeTerminateTool(director),
10016
10579
  makeTerminateAllTool(director),
10017
10580
  makeFleetTool(director),
@@ -10098,7 +10661,7 @@ import * as fsp24 from "node:fs/promises";
10098
10661
  import * as path26 from "node:path";
10099
10662
 
10100
10663
  // src/storage/session-store.ts
10101
- import { randomUUID as randomUUID10 } from "node:crypto";
10664
+ import { randomUUID as randomUUID11 } from "node:crypto";
10102
10665
  import * as fsp23 from "node:fs/promises";
10103
10666
  import * as path25 from "node:path";
10104
10667
 
@@ -15417,7 +15980,7 @@ var FileSessionWriter = class _FileSessionWriter {
15417
15980
 
15418
15981
  // src/storage/session-checkpoint-cas.ts
15419
15982
  import { spawn as spawn2 } from "node:child_process";
15420
- import { createHash as createHash3, randomUUID as randomUUID9 } from "node:crypto";
15983
+ import { createHash as createHash3, randomUUID as randomUUID10 } from "node:crypto";
15421
15984
  import * as fsp10 from "node:fs/promises";
15422
15985
  import * as path17 from "node:path";
15423
15986
 
@@ -15675,7 +16238,7 @@ var SessionCheckpointCas = class {
15675
16238
  }
15676
16239
  const temp = path17.join(
15677
16240
  path17.dirname(target),
15678
- `.${path17.basename(target)}.${process.pid}.${randomUUID9()}.tmp`
16241
+ `.${path17.basename(target)}.${process.pid}.${randomUUID10()}.tmp`
15679
16242
  );
15680
16243
  let handle;
15681
16244
  try {
@@ -17482,7 +18045,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
17482
18045
  onAppend;
17483
18046
  onAppendBatch;
17484
18047
  catalogClient;
17485
- maintenanceHolderId = randomUUID10();
18048
+ maintenanceHolderId = randomUUID11();
17486
18049
  _loadCache = /* @__PURE__ */ new Map();
17487
18050
  loadCache = new SessionLoadCache(this._loadCache);
17488
18051
  _indexCache = null;
@@ -18993,7 +19556,7 @@ function hashStr(s) {
18993
19556
  }
18994
19557
 
18995
19558
  // src/coordination/multi-agent-coordinator.ts
18996
- import { randomUUID as randomUUID11 } from "node:crypto";
19559
+ import { randomUUID as randomUUID12 } from "node:crypto";
18997
19560
  import { EventEmitter as EventEmitter2 } from "node:events";
18998
19561
 
18999
19562
  // src/coordination/coordinator/error-classifier.ts
@@ -19084,7 +19647,8 @@ async function executeSubagentWithTimeout({
19084
19647
  budget,
19085
19648
  preemptFraction = TIMEOUT_PREEMPT_FRACTION,
19086
19649
  abortSubagent,
19087
- currentSessionId
19650
+ currentSessionId,
19651
+ gracefulFinish
19088
19652
  }) {
19089
19653
  const initialTimeoutMs = budget.limits.timeoutMs;
19090
19654
  const idleLimitMs = budget.limits.idleTimeoutMs;
@@ -19115,9 +19679,17 @@ async function executeSubagentWithTimeout({
19115
19679
  const scheduleNext = () => {
19116
19680
  const wallLimit = budget.limits.timeoutMs ?? initialTimeoutMs;
19117
19681
  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
- armFor(Math.max(25, Math.min(wallRemaining, idleRemaining, preemptRemaining)));
19682
+ const idleRemaining = idleLimitMs === void 0 || gracefulFinish !== void 0 && initialTimeoutMs !== void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
19683
+ const preemptRemaining = initialTimeoutMs === void 0 || preemptedCeiling === wallLimit || gracefulFinish !== void 0 ? Number.POSITIVE_INFINITY : wallLimit * preemptFraction - (Date.now() - start);
19684
+ const next = Math.min(wallRemaining, idleRemaining, preemptRemaining);
19685
+ if (!Number.isFinite(next)) {
19686
+ if (timer) {
19687
+ clearTimeout(timer);
19688
+ timer = null;
19689
+ }
19690
+ return;
19691
+ }
19692
+ armFor(Math.max(25, next));
19121
19693
  };
19122
19694
  const negotiateTimeout = async (used, limit) => {
19123
19695
  const handler = budget.onThreshold;
@@ -19166,6 +19738,10 @@ async function executeSubagentWithTimeout({
19166
19738
  const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
19167
19739
  const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
19168
19740
  if (idleExceeded && !wallExceeded) {
19741
+ if (gracefulFinish !== void 0 && initialTimeoutMs !== void 0) {
19742
+ scheduleNext();
19743
+ return;
19744
+ }
19169
19745
  const sessionId = currentSessionId();
19170
19746
  budget._events?.emit("budget.threshold_reached", {
19171
19747
  ...sessionId ? { sessionId } : {},
@@ -19182,7 +19758,7 @@ async function executeSubagentWithTimeout({
19182
19758
  reject(new BudgetExceededError("idle_timeout", idleLimit ?? 0, budget.idleMs()));
19183
19759
  return;
19184
19760
  }
19185
- if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptState === "active" /* ACTIVE */ && elapsed >= wallLimit * preemptFraction) {
19761
+ if (wallLimit !== void 0 && !wallExceeded && gracefulFinish === void 0 && budget.onThreshold && preemptState === "active" /* ACTIVE */ && elapsed >= wallLimit * preemptFraction) {
19186
19762
  const activityTs = Date.now() - budget.idleMs();
19187
19763
  if (activityTs <= lastGrantActivityTs) {
19188
19764
  preemptState = "locked" /* LOCKED */;
@@ -19216,6 +19792,22 @@ async function executeSubagentWithTimeout({
19216
19792
  return;
19217
19793
  }
19218
19794
  const limit = wallLimit ?? 0;
19795
+ if (gracefulFinish !== void 0) {
19796
+ if (!budget.graceGranted) {
19797
+ const reason = `wall-clock budget of ${Math.round(limit / 1e3)}s reached`;
19798
+ if (budget.notifyFinish(reason, { graceMs: gracefulFinish.graceMs })) {
19799
+ scheduleNext();
19800
+ return;
19801
+ }
19802
+ abortSubagent(ctx.subagentId);
19803
+ reject(new BudgetExceededError("timeout", limit, elapsed));
19804
+ return;
19805
+ } else {
19806
+ abortSubagent(ctx.subagentId);
19807
+ reject(new BudgetExceededError("timeout", limit, elapsed));
19808
+ return;
19809
+ }
19810
+ }
19219
19811
  if (!budget.onThreshold) {
19220
19812
  abortSubagent(ctx.subagentId);
19221
19813
  reject(new BudgetExceededError("timeout", limit, elapsed));
@@ -19363,7 +19955,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
19363
19955
  return { ...subagent, name: display };
19364
19956
  }
19365
19957
  async spawn(subagent) {
19366
- const id = subagent.id || randomUUID11();
19958
+ const id = subagent.id || randomUUID12();
19367
19959
  const cfg = this.withNickname(subagent, id);
19368
19960
  if (this.subagents.has(id)) {
19369
19961
  throw new Error(`Subagent id "${id}" already exists \u2014 refusing to overwrite`);
@@ -19600,6 +20192,32 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
19600
20192
  completeTask(result) {
19601
20193
  this.recordCompletion(result);
19602
20194
  }
20195
+ /**
20196
+ * Ask every RUNNING subagent that opted into `gracefulFinish` to finish its
20197
+ * task in its own turn (see coordination/subagent-finish.ts). This is the
20198
+ * leader-side entry point for "the leader agent has finished": it delivers
20199
+ * an in-band notification between tool batches — never an interrupt, never
20200
+ * an abort. Each notified subagent keeps its existing time budget and
20201
+ * accelerates; the watchdog still bounds the maximum lifetime.
20202
+ *
20203
+ * Subagents without the policy opted in are deliberately untouched — their
20204
+ * lifecycle remains the legacy watchdog contract.
20205
+ *
20206
+ * Returns the number of subagents actually notified.
20207
+ */
20208
+ requestFinish(reason) {
20209
+ let notified = 0;
20210
+ for (const subagent of this.subagents.values()) {
20211
+ if (subagent.status !== "running") continue;
20212
+ if (!resolveGracefulFinish(subagent.config)) continue;
20213
+ const budget = subagent.activeBudget;
20214
+ if (!budget) continue;
20215
+ const usage = budget.usage();
20216
+ if (usage.iterations === 0 && usage.toolCalls === 0) continue;
20217
+ if (budget.notifyFinish(reason)) notified++;
20218
+ }
20219
+ return notified;
20220
+ }
19603
20221
  // --- internal dispatching ---------------------------------------------
19604
20222
  tryDispatchNext() {
19605
20223
  while (this.canDispatch()) {
@@ -19773,7 +20391,14 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
19773
20391
  idleTimeoutMs: rawIdleTimeoutMs ?? this.config.defaultBudget?.idleTimeoutMs ?? configWithRosterDefaults.idleTimeoutMs
19774
20392
  },
19775
20393
  "auto",
19776
- { sessionId: () => this.currentSessionId() }
20394
+ {
20395
+ sessionId: () => this.currentSessionId(),
20396
+ subagentId,
20397
+ // Graceful-finish runs own wall-clock enforcement to the watchdog so
20398
+ // the notify-then-bound lifecycle cannot be raced by tool.progress
20399
+ // heartbeats calling checkTimeout() (see subagent-budget.ts).
20400
+ ...resolveGracefulFinish(subagent.config) ? { wallClockWatchdogOwned: true } : {}
20401
+ }
19777
20402
  );
19778
20403
  subagent.activeBudget = budget;
19779
20404
  if (!this.runner) {
@@ -19806,7 +20431,8 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
19806
20431
  task,
19807
20432
  runCtx,
19808
20433
  budget,
19809
- subagent.config.preemptFraction
20434
+ subagent.config.preemptFraction,
20435
+ resolveGracefulFinish(subagent.config)
19810
20436
  );
19811
20437
  result = {
19812
20438
  subagentId,
@@ -19836,13 +20462,14 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
19836
20462
  }
19837
20463
  this.recordCompletion(result);
19838
20464
  }
19839
- async executeWithTimeout(runner, task, ctx, budget, preemptFraction) {
20465
+ async executeWithTimeout(runner, task, ctx, budget, preemptFraction, gracefulFinish) {
19840
20466
  return executeSubagentWithTimeout({
19841
20467
  runner,
19842
20468
  task,
19843
20469
  ctx,
19844
20470
  budget,
19845
20471
  preemptFraction,
20472
+ gracefulFinish,
19846
20473
  abortSubagent: (subagentId) => this.subagents.get(subagentId)?.abortController.abort(),
19847
20474
  currentSessionId: () => this.currentSessionId()
19848
20475
  });
@@ -20248,7 +20875,7 @@ var Director = class _Director {
20248
20875
  sessionProvider;
20249
20876
  sessionModel;
20250
20877
  constructor(opts) {
20251
- this.id = opts.config.coordinatorId || randomUUID12();
20878
+ this.id = opts.config.coordinatorId || randomUUID13();
20252
20879
  this.manifestPath = opts.manifestPath;
20253
20880
  this.roster = opts.roster;
20254
20881
  this.directorPreamble = opts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
@@ -20455,6 +21082,17 @@ var Director = class _Director {
20455
21082
  isWorkComplete() {
20456
21083
  return this.workCompleteFlag;
20457
21084
  }
21085
+ /**
21086
+ * Ask every running background subagent that opted into `gracefulFinish`
21087
+ * to finish its task in its own turn. In-band notification between tool
21088
+ * batches — no interrupt, no abort; each subagent keeps its time budget and
21089
+ * accelerates. Session shutdown calls this before draining Chimera work so
21090
+ * the post-session reviewer is nudged to complete rather than killed.
21091
+ * Returns the number of subagents notified.
21092
+ */
21093
+ requestFinish(reason) {
21094
+ return this.coordinator.requestFinish(reason);
21095
+ }
20458
21096
  setLeaderBtwNote(note) {
20459
21097
  return this.btwNotes.add(note);
20460
21098
  }
@@ -20531,7 +21169,7 @@ var Director = class _Director {
20531
21169
  );
20532
21170
  }
20533
21171
  const msg = {
20534
- id: randomUUID12(),
21172
+ id: randomUUID13(),
20535
21173
  type: "task",
20536
21174
  from: this.id,
20537
21175
  to: subagentId,
@@ -23961,7 +24599,7 @@ function _resetDesignRulesCache() {
23961
24599
  }
23962
24600
 
23963
24601
  // src/execution/design-color.ts
23964
- function clamp(n, lo, hi) {
24602
+ function clamp2(n, lo, hi) {
23965
24603
  return n < lo ? lo : n > hi ? hi : n;
23966
24604
  }
23967
24605
  function parseOklch(value) {
@@ -23977,9 +24615,9 @@ function parseOklch(value) {
23977
24615
  let a = 1;
23978
24616
  if (alphaPart !== void 0) {
23979
24617
  const av = parseComponent(alphaPart.trim(), true);
23980
- if (av !== null) a = clamp(av, 0, 1);
24618
+ if (av !== null) a = clamp2(av, 0, 1);
23981
24619
  }
23982
- return [clamp(L, 0, 1), Math.max(0, C), H, a];
24620
+ return [clamp2(L, 0, 1), Math.max(0, C), H, a];
23983
24621
  }
23984
24622
  function parseComponent(s, percentIsFraction) {
23985
24623
  s = s.trim();
@@ -23998,7 +24636,7 @@ function parseAngle(s) {
23998
24636
  }
23999
24637
  function linearToSrgb(c) {
24000
24638
  const v = c <= 31308e-7 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055;
24001
- return clamp(v, 0, 1);
24639
+ return clamp2(v, 0, 1);
24002
24640
  }
24003
24641
  function toHex2(n) {
24004
24642
  return Math.round(n * 255).toString(16).padStart(2, "0");
@@ -24203,16 +24841,16 @@ async function runDesignVerify(projectRoot, tokens, explicitFiles) {
24203
24841
  }
24204
24842
 
24205
24843
  // src/execution/design-detect.ts
24206
- var META_KEY = "designStudio";
24844
+ var META_KEY2 = "designStudio";
24207
24845
  function getDesignState(ctx) {
24208
- const v = ctx.meta[META_KEY];
24846
+ const v = ctx.meta[META_KEY2];
24209
24847
  return v && typeof v === "object" ? v : void 0;
24210
24848
  }
24211
24849
  function ensureState(ctx) {
24212
24850
  let s = getDesignState(ctx);
24213
24851
  if (!s) {
24214
24852
  s = { active: false, signals: [] };
24215
- ctx.meta[META_KEY] = s;
24853
+ ctx.meta[META_KEY2] = s;
24216
24854
  }
24217
24855
  return s;
24218
24856
  }
@@ -26134,7 +26772,7 @@ ${summaryText}` : summaryText;
26134
26772
  };
26135
26773
 
26136
26774
  // src/execution/parallel-eternal-engine.ts
26137
- import { randomUUID as randomUUID13 } from "node:crypto";
26775
+ import { randomUUID as randomUUID14 } from "node:crypto";
26138
26776
  var GOAL_COMPLETE_MARKER2 = /^\s*\[goal[_\s-]?complete\]\s*$/im;
26139
26777
  var ParallelEternalEngine = class {
26140
26778
  constructor(opts) {
@@ -26211,7 +26849,7 @@ var ParallelEternalEngine = class {
26211
26849
  this.state = "running";
26212
26850
  await this.persistState("running");
26213
26851
  const config = {
26214
- coordinatorId: `parallel-${randomUUID13().slice(0, 8)}`,
26852
+ coordinatorId: `parallel-${randomUUID14().slice(0, 8)}`,
26215
26853
  maxConcurrent: this.slots,
26216
26854
  doneCondition: { type: "all_tasks_done" }
26217
26855
  };
@@ -26265,7 +26903,7 @@ var ParallelEternalEngine = class {
26265
26903
  }
26266
26904
  if (!this.coordinator) {
26267
26905
  const config = {
26268
- coordinatorId: `parallel-${randomUUID13().slice(0, 8)}`,
26906
+ coordinatorId: `parallel-${randomUUID14().slice(0, 8)}`,
26269
26907
  maxConcurrent: this.slots,
26270
26908
  doneCondition: { type: "all_tasks_done" }
26271
26909
  };
@@ -26351,7 +26989,7 @@ ${recentJournal}` : "No prior iterations.",
26351
26989
  const task = expectDefined(tasks[i]);
26352
26990
  const route = routes[i] ?? null;
26353
26991
  const subagentId = `parallel-${this.iterations}-${i}`;
26354
- const taskId = randomUUID13();
26992
+ const taskId = randomUUID14();
26355
26993
  const personaLine = route ? `Acting agent: ${route.definition.config.name} \u2014 ${route.definition.capability.summary}
26356
26994
  ` : "";
26357
26995
  const spec = {
@@ -26566,7 +27204,7 @@ ${lastFew}` : "No prior iterations.",
26566
27204
  };
26567
27205
 
26568
27206
  // src/core/streaming-response-builder.ts
26569
- import { randomUUID as randomUUID14 } from "node:crypto";
27207
+ import { randomUUID as randomUUID15 } from "node:crypto";
26570
27208
  var STREAM_DRAIN_TIMEOUT_MS = 500;
26571
27209
  function buildResponse(state) {
26572
27210
  const content = [];
@@ -26626,7 +27264,7 @@ function handleContentBlockStart(state, ev) {
26626
27264
  state.textBuffers.push("");
26627
27265
  state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
26628
27266
  } else if (kind === "tool_use") {
26629
- const id = ev.id ?? randomUUID14();
27267
+ const id = ev.id ?? randomUUID15();
26630
27268
  state.tools.set(id, { name: ev.name ?? "unknown", partial: "" });
26631
27269
  state.blockOrder.push({ kind: "tool", id });
26632
27270
  state.currentTextIndex = -1;
@@ -27033,7 +27671,7 @@ function replaceAllCaseInsensitive(haystack, needle, replacement) {
27033
27671
  }
27034
27672
 
27035
27673
  // src/core/provider-runner.ts
27036
- import { randomUUID as randomUUID15 } from "node:crypto";
27674
+ import { randomUUID as randomUUID16 } from "node:crypto";
27037
27675
  function scrubProviderBody(body) {
27038
27676
  if (!body) return void 0;
27039
27677
  return {
@@ -27055,11 +27693,11 @@ function providerLogCtx(p, r) {
27055
27693
  }
27056
27694
  async function runProviderWithRetry(opts) {
27057
27695
  const { provider, request, signal, ctx, events, retry, logger, tracer } = opts;
27058
- const logicalRequestId = randomUUID15();
27696
+ const logicalRequestId = randomUUID16();
27059
27697
  const promptManifest = createChroniclePromptManifest(request);
27060
27698
  let attempt = 0;
27061
27699
  for (; ; ) {
27062
- const attemptId = randomUUID15();
27700
+ const attemptId = randomUUID16();
27063
27701
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
27064
27702
  const startedNs = process.hrtime.bigint();
27065
27703
  const correlation = {
@@ -28622,7 +29260,7 @@ function readPolicy(ctx) {
28622
29260
  }
28623
29261
 
28624
29262
  // src/execution/tool-executor.ts
28625
- import { randomUUID as randomUUID18 } from "node:crypto";
29263
+ import { randomUUID as randomUUID19 } from "node:crypto";
28626
29264
  import * as fs10 from "node:fs/promises";
28627
29265
  import * as path35 from "node:path";
28628
29266
 
@@ -28630,7 +29268,7 @@ import * as path35 from "node:path";
28630
29268
  var GOVERNED_TOOL_EXECUTOR_META_KEY = "toolExecutor.executeGoverned";
28631
29269
 
28632
29270
  // src/execution/tool-executor-support.ts
28633
- import { createHash as createHash8, randomUUID as randomUUID16 } from "node:crypto";
29271
+ import { createHash as createHash8, randomUUID as randomUUID17 } from "node:crypto";
28634
29272
  import * as fs9 from "node:fs/promises";
28635
29273
  import * as path33 from "node:path";
28636
29274
 
@@ -28761,7 +29399,7 @@ async function maybePersistLargeToolOutput(toolName, content, budget) {
28761
29399
  await fs9.mkdir(dir, { recursive: true });
28762
29400
  const safeTool = toolName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
28763
29401
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
28764
- const filePath = path33.join(dir, `${stamp}-${safeTool}-${randomUUID16()}.log`);
29402
+ const filePath = path33.join(dir, `${stamp}-${safeTool}-${randomUUID17()}.log`);
28765
29403
  await fs9.writeFile(filePath, content, "utf8");
28766
29404
  const marker = `[full tool output: ${bytes} bytes at ${filePath}; read/grep that file selectively instead of re-running or requesting more output]`;
28767
29405
  const fixedBytes = Buffer.byteLength(marker + TOOL_OUTPUT_ARTIFACT_OMISSION, "utf8");
@@ -29310,7 +29948,7 @@ ${errorDetails}`,
29310
29948
  }
29311
29949
 
29312
29950
  // src/execution/tool-executor-runner.ts
29313
- import { randomUUID as randomUUID17 } from "node:crypto";
29951
+ import { randomUUID as randomUUID18 } from "node:crypto";
29314
29952
 
29315
29953
  // src/observability/process-telemetry.ts
29316
29954
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
@@ -29449,7 +30087,7 @@ async function runToolWithTimeout(tool, input, parentSignal, ctx, opts, config,
29449
30087
  progressTailChars: config.progressTailChars,
29450
30088
  progressHeadChars: config.progressHeadChars
29451
30089
  }) : (async () => tool.execute(input, ctx, { signal: combined }))();
29452
- const telemetryToolCallId = toolUseId ?? `nested-${randomUUID17()}`;
30090
+ const telemetryToolCallId = toolUseId ?? `nested-${randomUUID18()}`;
29453
30091
  const toolPromise = opts.events ? runWithNetworkTelemetry(
29454
30092
  {
29455
30093
  events: opts.events,
@@ -29842,7 +30480,7 @@ ${post.additionalContext}`;
29842
30480
  const bridge = async (toolName, input) => {
29843
30481
  const nestedUse = {
29844
30482
  type: "tool_use",
29845
- id: `nested-${randomUUID18()}`,
30483
+ id: `nested-${randomUUID19()}`,
29846
30484
  name: toolName,
29847
30485
  input
29848
30486
  };
@@ -30446,13 +31084,13 @@ import * as fs11 from "node:fs/promises";
30446
31084
  import * as path37 from "node:path";
30447
31085
 
30448
31086
  // src/types/mode-prompts.ts
30449
- import { readFileSync as readFileSync10, statSync as statSync4 } from "node:fs";
31087
+ import { readFileSync as readFileSync11, statSync as statSync4 } from "node:fs";
30450
31088
  import * as path36 from "node:path";
30451
31089
  import { fileURLToPath as fileURLToPath5 } from "node:url";
30452
31090
  function modePrompt(id) {
30453
31091
  for (const dir of modePromptDirCandidates()) {
30454
31092
  try {
30455
- return readFileSync10(path36.join(dir, `${id}.md`), "utf8").trimEnd();
31093
+ return readFileSync11(path36.join(dir, `${id}.md`), "utf8").trimEnd();
30456
31094
  } catch {
30457
31095
  }
30458
31096
  }
@@ -31130,7 +31768,17 @@ function normalizeModelsDevModel(model) {
31130
31768
  const reasoningConfig = {
31131
31769
  default: disableSupported ? "enabled" : "always_on",
31132
31770
  disableSupported,
31133
- effortSupported: effortLevels.length > 0,
31771
+ // Tri-state (see ReasoningConfig.effortSupported):
31772
+ // options present → documented answer (true when effort values exist;
31773
+ // an explicitly EMPTY array is a documented "no
31774
+ // effort control", not an absent field).
31775
+ // field ABSENT → the model is known to reason but its vocabulary is
31776
+ // undocumented → `undefined`, so the resolver forwards
31777
+ // the request and each wire adapter applies its own
31778
+ // transport gating. Sending `false` here would make
31779
+ // the resolver claim "does not support effort" — an
31780
+ // assertion the catalog never made.
31781
+ ...raw === void 0 ? {} : { effortSupported: effortLevels.length > 0 },
31134
31782
  effortLevels,
31135
31783
  preserveThinking: model.interleaved ? "always_on" : "unsupported"
31136
31784
  };
@@ -31717,9 +32365,9 @@ async function startMetricsServer(opts) {
31717
32365
  let server;
31718
32366
  if (useHttps && tls) {
31719
32367
  const { createServer } = await import("node:https");
31720
- const { readFileSync: readFileSync12 } = await import("node:fs");
32368
+ const { readFileSync: readFileSync13 } = await import("node:fs");
31721
32369
  server = createServer(
31722
- { cert: readFileSync12(tls.cert), key: readFileSync12(tls.key) },
32370
+ { cert: readFileSync13(tls.cert), key: readFileSync13(tls.key) },
31723
32371
  listener
31724
32372
  );
31725
32373
  } else {
@@ -35472,7 +36120,7 @@ function deepFreeze(obj) {
35472
36120
  }
35473
36121
 
35474
36122
  // src/storage/plan-store.ts
35475
- import { randomUUID as randomUUID19 } from "node:crypto";
36123
+ import { randomUUID as randomUUID20 } from "node:crypto";
35476
36124
  import * as fsp30 from "node:fs/promises";
35477
36125
  async function loadPlan(filePath, events) {
35478
36126
  const t0 = Date.now();
@@ -35571,7 +36219,7 @@ function emptyPlan(sessionId, title) {
35571
36219
  function addPlanItem(plan, title, details) {
35572
36220
  const now = (/* @__PURE__ */ new Date()).toISOString();
35573
36221
  const item = {
35574
- id: `plan_${Date.now()}_${randomUUID19().slice(0, 6)}`,
36222
+ id: `plan_${Date.now()}_${randomUUID20().slice(0, 6)}`,
35575
36223
  title,
35576
36224
  details,
35577
36225
  status: "open",
@@ -35643,7 +36291,7 @@ function deriveTodosFromPlanItem(plan, idOrIndex, subtasks) {
35643
36291
  if (subtasks && subtasks.length > 0) {
35644
36292
  for (const st of subtasks) {
35645
36293
  todos.push({
35646
- id: `todo_${Date.now()}_${randomUUID19().slice(0, 6)}`,
36294
+ id: `todo_${Date.now()}_${randomUUID20().slice(0, 6)}`,
35647
36295
  content: st,
35648
36296
  status: "pending",
35649
36297
  promotedFromPlan: item.id