@wrongstack/core 0.308.6 → 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.
Files changed (44) hide show
  1. package/dist/coordination/agents/index.js +1 -0
  2. package/dist/coordination/agents/role-skills.d.ts +1 -0
  3. package/dist/coordination/director/director-toolset.d.ts +2 -2
  4. package/dist/coordination/director-mutation-test-tool.d.ts +29 -0
  5. package/dist/coordination/director-tools.d.ts +2 -0
  6. package/dist/coordination/director.d.ts +9 -0
  7. package/dist/coordination/explore-companion.d.ts +191 -0
  8. package/dist/coordination/fleet.d.ts +26 -0
  9. package/dist/coordination/index.d.ts +2 -1
  10. package/dist/coordination/index.js +1396 -370
  11. package/dist/coordination/mail-tools.d.ts +10 -6
  12. package/dist/coordination/mailbox-codecs.d.ts +31 -0
  13. package/dist/coordination/multi-agent-coordinator.d.ts +14 -0
  14. package/dist/coordination/multi-agent-timeout.d.ts +11 -1
  15. package/dist/coordination/mutation-engine.d.ts +74 -0
  16. package/dist/coordination/subagent-budget.d.ts +54 -0
  17. package/dist/coordination/subagent-finish.d.ts +78 -0
  18. package/dist/core/index.js +19 -4
  19. package/dist/defaults/index.js +731 -52
  20. package/dist/execution/compaction-core.d.ts +1 -1
  21. package/dist/execution/compaction-elision.d.ts +0 -10
  22. package/dist/execution/index.js +269 -16
  23. package/dist/goal/index.js +54 -27
  24. package/dist/goal/phase-orchestrator.d.ts +7 -0
  25. package/dist/goal/types.d.ts +1 -1
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.js +1280 -201
  28. package/dist/kernel/events/agent-events.d.ts +31 -2
  29. package/dist/models/index.js +11 -1
  30. package/dist/plugin/discovery.d.ts +73 -0
  31. package/dist/plugin/index.d.ts +2 -0
  32. package/dist/plugin/index.js +270 -29
  33. package/dist/plugin/loader.d.ts +5 -1
  34. package/dist/plugin/trust.d.ts +78 -0
  35. package/dist/tools/index.js +1 -0
  36. package/dist/types/config/mcp-features.d.ts +21 -0
  37. package/dist/types/config/skills-fleet-brain.d.ts +18 -0
  38. package/dist/types/index.d.ts +1 -1
  39. package/dist/types/index.js +14 -0
  40. package/dist/types/multi-agent.d.ts +15 -0
  41. package/dist/types/provider.d.ts +29 -1
  42. package/instructions/agents/chaos-monkey.md +57 -0
  43. package/instructions/agents/explore-companion.md +35 -0
  44. package/package.json +3 -3
@@ -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 });
@@ -2029,6 +2159,7 @@ function inferRuntimeCapabilities(toolNames) {
2029
2159
  var skillSet = (...names) => names;
2030
2160
  var ROLE_SKILL_SETS = {
2031
2161
  explore: skillSet("research-web", "node-modern", "typescript-strict"),
2162
+ "explore-companion": skillSet("node-modern", "typescript-strict"),
2032
2163
  search: skillSet("bug-hunter", "typescript-strict", "research-web"),
2033
2164
  research: skillSet("research-web", "tech-stack", "security-scanner", "api-design"),
2034
2165
  analyst: skillSet("sdd", "api-design", "testing", "security-scanner"),
@@ -5417,6 +5548,42 @@ var SHADOW_AGENT = {
5417
5548
  ...defineAgent("shadow-agent", "Shadow"),
5418
5549
  skillNames: [...SHADOW_AGENT_SKILLS]
5419
5550
  };
5551
+ var EXPLORE_COMPANION_AGENT = {
5552
+ ...defineAgent("explore-companion", "Explore Companion"),
5553
+ tools: [...TOOLS.read, ...TOOLS.index],
5554
+ // Read-only, triple-enforced: allowlist has no write/bash, and the
5555
+ // disabled list blocks the escape hatches explicitly.
5556
+ disabledTools: [
5557
+ "write",
5558
+ "edit",
5559
+ "replace",
5560
+ "patch",
5561
+ "bash",
5562
+ "exec",
5563
+ "delegate",
5564
+ "spawn_subagent",
5565
+ "assign_task"
5566
+ ],
5567
+ skillNames: [...ROLE_SKILL_SETS["explore-companion"]],
5568
+ spawnBudgetExempt: true,
5569
+ // Findings travel via mailbox + submit_result, not the leader's stream.
5570
+ textStream: "silent",
5571
+ toolStream: "silent"
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
+ };
5420
5587
  var CRITIC_AGENT = defineAgent("critic", "Critic");
5421
5588
  var GENERIC_AGENT = defineAgent("generic", "Generic Project Agent");
5422
5589
  function withDispatchMetadata(definition) {
@@ -5435,6 +5602,8 @@ var FLEET_ROSTER = {
5435
5602
  critic: CRITIC_AGENT,
5436
5603
  generic: GENERIC_AGENT,
5437
5604
  "shadow-agent": SHADOW_AGENT,
5605
+ "explore-companion": EXPLORE_COMPANION_AGENT,
5606
+ "chaos-monkey": CHAOS_MONKEY_AGENT,
5438
5607
  ...Object.fromEntries(
5439
5608
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, withDispatchMetadata(d)])
5440
5609
  )
@@ -5458,6 +5627,23 @@ var FLEET_ROSTER_BUDGETS = {
5458
5627
  maxTokens: 96e3,
5459
5628
  maxCostUsd: 0.5
5460
5629
  },
5630
+ "explore-companion": {
5631
+ idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
5632
+ maxIterations: 3e3,
5633
+ maxToolCalls: 8e3,
5634
+ maxTokens: 96e3,
5635
+ maxCostUsd: 0.5
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
+ },
5461
5647
  ...Object.fromEntries(
5462
5648
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.budget])
5463
5649
  )
@@ -6197,7 +6383,7 @@ async function readSubagentPartial(opts, subagentId) {
6197
6383
  }
6198
6384
 
6199
6385
  // src/coordination/director.ts
6200
- import { randomUUID as randomUUID12 } from "node:crypto";
6386
+ import { randomUUID as randomUUID13 } from "node:crypto";
6201
6387
  import * as fsp25 from "node:fs/promises";
6202
6388
 
6203
6389
  // src/core/instruction-template.ts
@@ -8213,7 +8399,7 @@ ${JSON.stringify(result.result, null, 2)}
8213
8399
  };
8214
8400
 
8215
8401
  // src/coordination/director-tools.ts
8216
- import { randomUUID as randomUUID8 } from "node:crypto";
8402
+ import { randomUUID as randomUUID9 } from "node:crypto";
8217
8403
  import {
8218
8404
  completeKanbanDispatch,
8219
8405
  failKanbanDispatch,
@@ -8307,7 +8493,7 @@ function buildKanbanFleetTaskPrompt(board, task, lease) {
8307
8493
  const dependencyLines = (task.dependsOn ?? []).map((depId) => board.tasks.find((candidate) => candidate.id === depId)).filter((dep) => Boolean(dep)).map((dep) => `- ${dep.title} [${dep.status}] (${dep.id})`);
8308
8494
  const checks = task.successCriteria?.map((check) => `- ${check.description}`).join("\n");
8309
8495
  const metrics = task.goalMetrics?.map(
8310
- (metric) => `- ${metric.name}: ${metric.current ?? "n/a"}${metric.target !== void 0 ? ` / ${metric.target}` : ""}${metric.unit ? ` ${metric.unit}` : ""} [${metric.status}]`
8496
+ (metric) => `- ${metric.name}: ${metric.current ?? "n/a"}${metric.target !== void 0 ? ` / ${metric.direction === "at_most" ? "\u2264" : "\u2265"} ${metric.target}` : ""}${metric.unit ? ` ${metric.unit}` : ""} [${metric.status}]`
8311
8497
  ).join("\n");
8312
8498
  const chain = task.chain ? [
8313
8499
  `chainId: ${task.chain.chainId}`,
@@ -9413,6 +9599,413 @@ function excerpt(text, max) {
9413
9599
  ...(truncated)`;
9414
9600
  }
9415
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
+
9416
10009
  // src/coordination/director-tools.ts
9417
10010
  function makeSpawnTool(director, roster) {
9418
10011
  const dispatchCatalog = () => {
@@ -9705,7 +10298,7 @@ function makeKanbanQueueTool(director, roster) {
9705
10298
  try {
9706
10299
  const config = buildKanbanSubagentConfig(claim.task, i, roster, instantiateRosterConfig2);
9707
10300
  subagentId = await director.spawn(config);
9708
- const dispatchTaskId = randomUUID8();
10301
+ const dispatchTaskId = randomUUID9();
9709
10302
  const taskSpec = {
9710
10303
  id: dispatchTaskId,
9711
10304
  subagentId,
@@ -9981,6 +10574,7 @@ function buildDirectorToolset(director, roster) {
9981
10574
  makeAskResultTool(director),
9982
10575
  makeRollUpTool(director),
9983
10576
  makeQualityGateTool(director, roster),
10577
+ makeMutationTestTool(director, roster),
9984
10578
  makeTerminateTool(director),
9985
10579
  makeTerminateAllTool(director),
9986
10580
  makeFleetTool(director),
@@ -10067,7 +10661,7 @@ import * as fsp24 from "node:fs/promises";
10067
10661
  import * as path26 from "node:path";
10068
10662
 
10069
10663
  // src/storage/session-store.ts
10070
- import { randomUUID as randomUUID10 } from "node:crypto";
10664
+ import { randomUUID as randomUUID11 } from "node:crypto";
10071
10665
  import * as fsp23 from "node:fs/promises";
10072
10666
  import * as path25 from "node:path";
10073
10667
 
@@ -15386,7 +15980,7 @@ var FileSessionWriter = class _FileSessionWriter {
15386
15980
 
15387
15981
  // src/storage/session-checkpoint-cas.ts
15388
15982
  import { spawn as spawn2 } from "node:child_process";
15389
- import { createHash as createHash3, randomUUID as randomUUID9 } from "node:crypto";
15983
+ import { createHash as createHash3, randomUUID as randomUUID10 } from "node:crypto";
15390
15984
  import * as fsp10 from "node:fs/promises";
15391
15985
  import * as path17 from "node:path";
15392
15986
 
@@ -15644,7 +16238,7 @@ var SessionCheckpointCas = class {
15644
16238
  }
15645
16239
  const temp = path17.join(
15646
16240
  path17.dirname(target),
15647
- `.${path17.basename(target)}.${process.pid}.${randomUUID9()}.tmp`
16241
+ `.${path17.basename(target)}.${process.pid}.${randomUUID10()}.tmp`
15648
16242
  );
15649
16243
  let handle;
15650
16244
  try {
@@ -17451,7 +18045,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
17451
18045
  onAppend;
17452
18046
  onAppendBatch;
17453
18047
  catalogClient;
17454
- maintenanceHolderId = randomUUID10();
18048
+ maintenanceHolderId = randomUUID11();
17455
18049
  _loadCache = /* @__PURE__ */ new Map();
17456
18050
  loadCache = new SessionLoadCache(this._loadCache);
17457
18051
  _indexCache = null;
@@ -18962,7 +19556,7 @@ function hashStr(s) {
18962
19556
  }
18963
19557
 
18964
19558
  // src/coordination/multi-agent-coordinator.ts
18965
- import { randomUUID as randomUUID11 } from "node:crypto";
19559
+ import { randomUUID as randomUUID12 } from "node:crypto";
18966
19560
  import { EventEmitter as EventEmitter2 } from "node:events";
18967
19561
 
18968
19562
  // src/coordination/coordinator/error-classifier.ts
@@ -19053,7 +19647,8 @@ async function executeSubagentWithTimeout({
19053
19647
  budget,
19054
19648
  preemptFraction = TIMEOUT_PREEMPT_FRACTION,
19055
19649
  abortSubagent,
19056
- currentSessionId
19650
+ currentSessionId,
19651
+ gracefulFinish
19057
19652
  }) {
19058
19653
  const initialTimeoutMs = budget.limits.timeoutMs;
19059
19654
  const idleLimitMs = budget.limits.idleTimeoutMs;
@@ -19084,9 +19679,17 @@ async function executeSubagentWithTimeout({
19084
19679
  const scheduleNext = () => {
19085
19680
  const wallLimit = budget.limits.timeoutMs ?? initialTimeoutMs;
19086
19681
  const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : wallLimit - (Date.now() - start);
19087
- const idleRemaining = idleLimitMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
19088
- const preemptRemaining = initialTimeoutMs === void 0 || preemptedCeiling === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * preemptFraction - (Date.now() - start);
19089
- 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));
19090
19693
  };
19091
19694
  const negotiateTimeout = async (used, limit) => {
19092
19695
  const handler = budget.onThreshold;
@@ -19135,6 +19738,10 @@ async function executeSubagentWithTimeout({
19135
19738
  const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
19136
19739
  const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
19137
19740
  if (idleExceeded && !wallExceeded) {
19741
+ if (gracefulFinish !== void 0 && initialTimeoutMs !== void 0) {
19742
+ scheduleNext();
19743
+ return;
19744
+ }
19138
19745
  const sessionId = currentSessionId();
19139
19746
  budget._events?.emit("budget.threshold_reached", {
19140
19747
  ...sessionId ? { sessionId } : {},
@@ -19151,7 +19758,7 @@ async function executeSubagentWithTimeout({
19151
19758
  reject(new BudgetExceededError("idle_timeout", idleLimit ?? 0, budget.idleMs()));
19152
19759
  return;
19153
19760
  }
19154
- 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) {
19155
19762
  const activityTs = Date.now() - budget.idleMs();
19156
19763
  if (activityTs <= lastGrantActivityTs) {
19157
19764
  preemptState = "locked" /* LOCKED */;
@@ -19185,6 +19792,22 @@ async function executeSubagentWithTimeout({
19185
19792
  return;
19186
19793
  }
19187
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
+ }
19188
19811
  if (!budget.onThreshold) {
19189
19812
  abortSubagent(ctx.subagentId);
19190
19813
  reject(new BudgetExceededError("timeout", limit, elapsed));
@@ -19332,7 +19955,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
19332
19955
  return { ...subagent, name: display };
19333
19956
  }
19334
19957
  async spawn(subagent) {
19335
- const id = subagent.id || randomUUID11();
19958
+ const id = subagent.id || randomUUID12();
19336
19959
  const cfg = this.withNickname(subagent, id);
19337
19960
  if (this.subagents.has(id)) {
19338
19961
  throw new Error(`Subagent id "${id}" already exists \u2014 refusing to overwrite`);
@@ -19569,6 +20192,32 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
19569
20192
  completeTask(result) {
19570
20193
  this.recordCompletion(result);
19571
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
+ }
19572
20221
  // --- internal dispatching ---------------------------------------------
19573
20222
  tryDispatchNext() {
19574
20223
  while (this.canDispatch()) {
@@ -19742,7 +20391,14 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
19742
20391
  idleTimeoutMs: rawIdleTimeoutMs ?? this.config.defaultBudget?.idleTimeoutMs ?? configWithRosterDefaults.idleTimeoutMs
19743
20392
  },
19744
20393
  "auto",
19745
- { 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
+ }
19746
20402
  );
19747
20403
  subagent.activeBudget = budget;
19748
20404
  if (!this.runner) {
@@ -19775,7 +20431,8 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
19775
20431
  task,
19776
20432
  runCtx,
19777
20433
  budget,
19778
- subagent.config.preemptFraction
20434
+ subagent.config.preemptFraction,
20435
+ resolveGracefulFinish(subagent.config)
19779
20436
  );
19780
20437
  result = {
19781
20438
  subagentId,
@@ -19805,13 +20462,14 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
19805
20462
  }
19806
20463
  this.recordCompletion(result);
19807
20464
  }
19808
- async executeWithTimeout(runner, task, ctx, budget, preemptFraction) {
20465
+ async executeWithTimeout(runner, task, ctx, budget, preemptFraction, gracefulFinish) {
19809
20466
  return executeSubagentWithTimeout({
19810
20467
  runner,
19811
20468
  task,
19812
20469
  ctx,
19813
20470
  budget,
19814
20471
  preemptFraction,
20472
+ gracefulFinish,
19815
20473
  abortSubagent: (subagentId) => this.subagents.get(subagentId)?.abortController.abort(),
19816
20474
  currentSessionId: () => this.currentSessionId()
19817
20475
  });
@@ -20217,7 +20875,7 @@ var Director = class _Director {
20217
20875
  sessionProvider;
20218
20876
  sessionModel;
20219
20877
  constructor(opts) {
20220
- this.id = opts.config.coordinatorId || randomUUID12();
20878
+ this.id = opts.config.coordinatorId || randomUUID13();
20221
20879
  this.manifestPath = opts.manifestPath;
20222
20880
  this.roster = opts.roster;
20223
20881
  this.directorPreamble = opts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
@@ -20424,6 +21082,17 @@ var Director = class _Director {
20424
21082
  isWorkComplete() {
20425
21083
  return this.workCompleteFlag;
20426
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
+ }
20427
21096
  setLeaderBtwNote(note) {
20428
21097
  return this.btwNotes.add(note);
20429
21098
  }
@@ -20500,7 +21169,7 @@ var Director = class _Director {
20500
21169
  );
20501
21170
  }
20502
21171
  const msg = {
20503
- id: randomUUID12(),
21172
+ id: randomUUID13(),
20504
21173
  type: "task",
20505
21174
  from: this.id,
20506
21175
  to: subagentId,
@@ -23930,7 +24599,7 @@ function _resetDesignRulesCache() {
23930
24599
  }
23931
24600
 
23932
24601
  // src/execution/design-color.ts
23933
- function clamp(n, lo, hi) {
24602
+ function clamp2(n, lo, hi) {
23934
24603
  return n < lo ? lo : n > hi ? hi : n;
23935
24604
  }
23936
24605
  function parseOklch(value) {
@@ -23946,9 +24615,9 @@ function parseOklch(value) {
23946
24615
  let a = 1;
23947
24616
  if (alphaPart !== void 0) {
23948
24617
  const av = parseComponent(alphaPart.trim(), true);
23949
- if (av !== null) a = clamp(av, 0, 1);
24618
+ if (av !== null) a = clamp2(av, 0, 1);
23950
24619
  }
23951
- return [clamp(L, 0, 1), Math.max(0, C), H, a];
24620
+ return [clamp2(L, 0, 1), Math.max(0, C), H, a];
23952
24621
  }
23953
24622
  function parseComponent(s, percentIsFraction) {
23954
24623
  s = s.trim();
@@ -23967,7 +24636,7 @@ function parseAngle(s) {
23967
24636
  }
23968
24637
  function linearToSrgb(c) {
23969
24638
  const v = c <= 31308e-7 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055;
23970
- return clamp(v, 0, 1);
24639
+ return clamp2(v, 0, 1);
23971
24640
  }
23972
24641
  function toHex2(n) {
23973
24642
  return Math.round(n * 255).toString(16).padStart(2, "0");
@@ -24172,16 +24841,16 @@ async function runDesignVerify(projectRoot, tokens, explicitFiles) {
24172
24841
  }
24173
24842
 
24174
24843
  // src/execution/design-detect.ts
24175
- var META_KEY = "designStudio";
24844
+ var META_KEY2 = "designStudio";
24176
24845
  function getDesignState(ctx) {
24177
- const v = ctx.meta[META_KEY];
24846
+ const v = ctx.meta[META_KEY2];
24178
24847
  return v && typeof v === "object" ? v : void 0;
24179
24848
  }
24180
24849
  function ensureState(ctx) {
24181
24850
  let s = getDesignState(ctx);
24182
24851
  if (!s) {
24183
24852
  s = { active: false, signals: [] };
24184
- ctx.meta[META_KEY] = s;
24853
+ ctx.meta[META_KEY2] = s;
24185
24854
  }
24186
24855
  return s;
24187
24856
  }
@@ -26103,7 +26772,7 @@ ${summaryText}` : summaryText;
26103
26772
  };
26104
26773
 
26105
26774
  // src/execution/parallel-eternal-engine.ts
26106
- import { randomUUID as randomUUID13 } from "node:crypto";
26775
+ import { randomUUID as randomUUID14 } from "node:crypto";
26107
26776
  var GOAL_COMPLETE_MARKER2 = /^\s*\[goal[_\s-]?complete\]\s*$/im;
26108
26777
  var ParallelEternalEngine = class {
26109
26778
  constructor(opts) {
@@ -26180,7 +26849,7 @@ var ParallelEternalEngine = class {
26180
26849
  this.state = "running";
26181
26850
  await this.persistState("running");
26182
26851
  const config = {
26183
- coordinatorId: `parallel-${randomUUID13().slice(0, 8)}`,
26852
+ coordinatorId: `parallel-${randomUUID14().slice(0, 8)}`,
26184
26853
  maxConcurrent: this.slots,
26185
26854
  doneCondition: { type: "all_tasks_done" }
26186
26855
  };
@@ -26234,7 +26903,7 @@ var ParallelEternalEngine = class {
26234
26903
  }
26235
26904
  if (!this.coordinator) {
26236
26905
  const config = {
26237
- coordinatorId: `parallel-${randomUUID13().slice(0, 8)}`,
26906
+ coordinatorId: `parallel-${randomUUID14().slice(0, 8)}`,
26238
26907
  maxConcurrent: this.slots,
26239
26908
  doneCondition: { type: "all_tasks_done" }
26240
26909
  };
@@ -26320,7 +26989,7 @@ ${recentJournal}` : "No prior iterations.",
26320
26989
  const task = expectDefined(tasks[i]);
26321
26990
  const route = routes[i] ?? null;
26322
26991
  const subagentId = `parallel-${this.iterations}-${i}`;
26323
- const taskId = randomUUID13();
26992
+ const taskId = randomUUID14();
26324
26993
  const personaLine = route ? `Acting agent: ${route.definition.config.name} \u2014 ${route.definition.capability.summary}
26325
26994
  ` : "";
26326
26995
  const spec = {
@@ -26535,7 +27204,7 @@ ${lastFew}` : "No prior iterations.",
26535
27204
  };
26536
27205
 
26537
27206
  // src/core/streaming-response-builder.ts
26538
- import { randomUUID as randomUUID14 } from "node:crypto";
27207
+ import { randomUUID as randomUUID15 } from "node:crypto";
26539
27208
  var STREAM_DRAIN_TIMEOUT_MS = 500;
26540
27209
  function buildResponse(state) {
26541
27210
  const content = [];
@@ -26595,7 +27264,7 @@ function handleContentBlockStart(state, ev) {
26595
27264
  state.textBuffers.push("");
26596
27265
  state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
26597
27266
  } else if (kind === "tool_use") {
26598
- const id = ev.id ?? randomUUID14();
27267
+ const id = ev.id ?? randomUUID15();
26599
27268
  state.tools.set(id, { name: ev.name ?? "unknown", partial: "" });
26600
27269
  state.blockOrder.push({ kind: "tool", id });
26601
27270
  state.currentTextIndex = -1;
@@ -27002,7 +27671,7 @@ function replaceAllCaseInsensitive(haystack, needle, replacement) {
27002
27671
  }
27003
27672
 
27004
27673
  // src/core/provider-runner.ts
27005
- import { randomUUID as randomUUID15 } from "node:crypto";
27674
+ import { randomUUID as randomUUID16 } from "node:crypto";
27006
27675
  function scrubProviderBody(body) {
27007
27676
  if (!body) return void 0;
27008
27677
  return {
@@ -27024,11 +27693,11 @@ function providerLogCtx(p, r) {
27024
27693
  }
27025
27694
  async function runProviderWithRetry(opts) {
27026
27695
  const { provider, request, signal, ctx, events, retry, logger, tracer } = opts;
27027
- const logicalRequestId = randomUUID15();
27696
+ const logicalRequestId = randomUUID16();
27028
27697
  const promptManifest = createChroniclePromptManifest(request);
27029
27698
  let attempt = 0;
27030
27699
  for (; ; ) {
27031
- const attemptId = randomUUID15();
27700
+ const attemptId = randomUUID16();
27032
27701
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
27033
27702
  const startedNs = process.hrtime.bigint();
27034
27703
  const correlation = {
@@ -28591,7 +29260,7 @@ function readPolicy(ctx) {
28591
29260
  }
28592
29261
 
28593
29262
  // src/execution/tool-executor.ts
28594
- import { randomUUID as randomUUID18 } from "node:crypto";
29263
+ import { randomUUID as randomUUID19 } from "node:crypto";
28595
29264
  import * as fs10 from "node:fs/promises";
28596
29265
  import * as path35 from "node:path";
28597
29266
 
@@ -28599,7 +29268,7 @@ import * as path35 from "node:path";
28599
29268
  var GOVERNED_TOOL_EXECUTOR_META_KEY = "toolExecutor.executeGoverned";
28600
29269
 
28601
29270
  // src/execution/tool-executor-support.ts
28602
- import { createHash as createHash8, randomUUID as randomUUID16 } from "node:crypto";
29271
+ import { createHash as createHash8, randomUUID as randomUUID17 } from "node:crypto";
28603
29272
  import * as fs9 from "node:fs/promises";
28604
29273
  import * as path33 from "node:path";
28605
29274
 
@@ -28730,7 +29399,7 @@ async function maybePersistLargeToolOutput(toolName, content, budget) {
28730
29399
  await fs9.mkdir(dir, { recursive: true });
28731
29400
  const safeTool = toolName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
28732
29401
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
28733
- const filePath = path33.join(dir, `${stamp}-${safeTool}-${randomUUID16()}.log`);
29402
+ const filePath = path33.join(dir, `${stamp}-${safeTool}-${randomUUID17()}.log`);
28734
29403
  await fs9.writeFile(filePath, content, "utf8");
28735
29404
  const marker = `[full tool output: ${bytes} bytes at ${filePath}; read/grep that file selectively instead of re-running or requesting more output]`;
28736
29405
  const fixedBytes = Buffer.byteLength(marker + TOOL_OUTPUT_ARTIFACT_OMISSION, "utf8");
@@ -29279,7 +29948,7 @@ ${errorDetails}`,
29279
29948
  }
29280
29949
 
29281
29950
  // src/execution/tool-executor-runner.ts
29282
- import { randomUUID as randomUUID17 } from "node:crypto";
29951
+ import { randomUUID as randomUUID18 } from "node:crypto";
29283
29952
 
29284
29953
  // src/observability/process-telemetry.ts
29285
29954
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
@@ -29418,7 +30087,7 @@ async function runToolWithTimeout(tool, input, parentSignal, ctx, opts, config,
29418
30087
  progressTailChars: config.progressTailChars,
29419
30088
  progressHeadChars: config.progressHeadChars
29420
30089
  }) : (async () => tool.execute(input, ctx, { signal: combined }))();
29421
- const telemetryToolCallId = toolUseId ?? `nested-${randomUUID17()}`;
30090
+ const telemetryToolCallId = toolUseId ?? `nested-${randomUUID18()}`;
29422
30091
  const toolPromise = opts.events ? runWithNetworkTelemetry(
29423
30092
  {
29424
30093
  events: opts.events,
@@ -29811,7 +30480,7 @@ ${post.additionalContext}`;
29811
30480
  const bridge = async (toolName, input) => {
29812
30481
  const nestedUse = {
29813
30482
  type: "tool_use",
29814
- id: `nested-${randomUUID18()}`,
30483
+ id: `nested-${randomUUID19()}`,
29815
30484
  name: toolName,
29816
30485
  input
29817
30486
  };
@@ -30415,13 +31084,13 @@ import * as fs11 from "node:fs/promises";
30415
31084
  import * as path37 from "node:path";
30416
31085
 
30417
31086
  // src/types/mode-prompts.ts
30418
- import { readFileSync as readFileSync10, statSync as statSync4 } from "node:fs";
31087
+ import { readFileSync as readFileSync11, statSync as statSync4 } from "node:fs";
30419
31088
  import * as path36 from "node:path";
30420
31089
  import { fileURLToPath as fileURLToPath5 } from "node:url";
30421
31090
  function modePrompt(id) {
30422
31091
  for (const dir of modePromptDirCandidates()) {
30423
31092
  try {
30424
- return readFileSync10(path36.join(dir, `${id}.md`), "utf8").trimEnd();
31093
+ return readFileSync11(path36.join(dir, `${id}.md`), "utf8").trimEnd();
30425
31094
  } catch {
30426
31095
  }
30427
31096
  }
@@ -31099,7 +31768,17 @@ function normalizeModelsDevModel(model) {
31099
31768
  const reasoningConfig = {
31100
31769
  default: disableSupported ? "enabled" : "always_on",
31101
31770
  disableSupported,
31102
- 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 },
31103
31782
  effortLevels,
31104
31783
  preserveThinking: model.interleaved ? "always_on" : "unsupported"
31105
31784
  };
@@ -31686,9 +32365,9 @@ async function startMetricsServer(opts) {
31686
32365
  let server;
31687
32366
  if (useHttps && tls) {
31688
32367
  const { createServer } = await import("node:https");
31689
- const { readFileSync: readFileSync12 } = await import("node:fs");
32368
+ const { readFileSync: readFileSync13 } = await import("node:fs");
31690
32369
  server = createServer(
31691
- { cert: readFileSync12(tls.cert), key: readFileSync12(tls.key) },
32370
+ { cert: readFileSync13(tls.cert), key: readFileSync13(tls.key) },
31692
32371
  listener
31693
32372
  );
31694
32373
  } else {
@@ -35441,7 +36120,7 @@ function deepFreeze(obj) {
35441
36120
  }
35442
36121
 
35443
36122
  // src/storage/plan-store.ts
35444
- import { randomUUID as randomUUID19 } from "node:crypto";
36123
+ import { randomUUID as randomUUID20 } from "node:crypto";
35445
36124
  import * as fsp30 from "node:fs/promises";
35446
36125
  async function loadPlan(filePath, events) {
35447
36126
  const t0 = Date.now();
@@ -35540,7 +36219,7 @@ function emptyPlan(sessionId, title) {
35540
36219
  function addPlanItem(plan, title, details) {
35541
36220
  const now = (/* @__PURE__ */ new Date()).toISOString();
35542
36221
  const item = {
35543
- id: `plan_${Date.now()}_${randomUUID19().slice(0, 6)}`,
36222
+ id: `plan_${Date.now()}_${randomUUID20().slice(0, 6)}`,
35544
36223
  title,
35545
36224
  details,
35546
36225
  status: "open",
@@ -35612,7 +36291,7 @@ function deriveTodosFromPlanItem(plan, idOrIndex, subtasks) {
35612
36291
  if (subtasks && subtasks.length > 0) {
35613
36292
  for (const st of subtasks) {
35614
36293
  todos.push({
35615
- id: `todo_${Date.now()}_${randomUUID19().slice(0, 6)}`,
36294
+ id: `todo_${Date.now()}_${randomUUID20().slice(0, 6)}`,
35616
36295
  content: st,
35617
36296
  status: "pending",
35618
36297
  promotedFromPlan: item.id