@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.
@@ -285,6 +285,46 @@ function createMessage(type, from, payload, to) {
285
285
  };
286
286
  }
287
287
 
288
+ // src/core/btw.ts
289
+ var META_KEY = "_btwNotes";
290
+ var MAX_PENDING = 20;
291
+ function readQueue(ctx) {
292
+ const raw = ctx.meta[META_KEY];
293
+ return Array.isArray(raw) ? raw : [];
294
+ }
295
+ function setBtwNote(ctx, text) {
296
+ const trimmed = text.trim();
297
+ if (!trimmed) return readQueue(ctx).length;
298
+ const next = [...readQueue(ctx), trimmed].slice(-MAX_PENDING);
299
+ ctx.meta[META_KEY] = next;
300
+ return next.length;
301
+ }
302
+
303
+ // src/coordination/subagent-finish.ts
304
+ var SUBAGENT_FINISH_REQUESTED_EVENT = "subagent.finish_requested";
305
+ var DEFAULT_SUBAGENT_FINISH_GRACE_MS = 12e4;
306
+ function resolveGracefulFinish(config) {
307
+ const raw = config.gracefulFinish;
308
+ if (raw === void 0 || raw === false) return void 0;
309
+ if (raw === true) return { graceMs: DEFAULT_SUBAGENT_FINISH_GRACE_MS };
310
+ const graceMs = typeof raw.graceMs === "number" && Number.isFinite(raw.graceMs) && raw.graceMs > 0 ? Math.floor(raw.graceMs) : DEFAULT_SUBAGENT_FINISH_GRACE_MS;
311
+ return { graceMs };
312
+ }
313
+ function buildSubagentFinishNotice(input) {
314
+ const localTime = new Date(input.deadlineMs).toISOString();
315
+ const seconds = Math.max(1, Math.round(input.graceMs / 1e3));
316
+ 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.`;
317
+ return [
318
+ "[SUBAGENT FINISH] The leader agent has finished its work.",
319
+ `Reason: ${input.reason}`,
320
+ timeLeft,
321
+ "Finish your task now, in this turn: complete the thought you are working on, stop",
322
+ "starting new tool calls unless one is strictly required to finish, and write your",
323
+ "final answer or report as your final output, then end your turn.",
324
+ "Do not restart the task and do not begin new work."
325
+ ].join("\n");
326
+ }
327
+
288
328
  // src/coordination/subagent-budget.ts
289
329
  var TIMEOUT_PREEMPT_FRACTION = 0.85;
290
330
  var DECISION_TIMEOUT_MS = 6e4;
@@ -342,6 +382,82 @@ var SubagentBudget = class _SubagentBudget {
342
382
  this.limits.idleTimeoutMs = ext.idleTimeoutMs;
343
383
  }
344
384
  }
385
+ /**
386
+ * Graceful-finish state (see coordination/subagent-finish.ts).
387
+ * `_finishNotified` guards the single in-band emission; `_grace` records a
388
+ * granted working-time extension past the original wall-clock deadline.
389
+ * They are separate because the two callers want different semantics:
390
+ * the watchdog grants grace at the deadline crossing (notify + extend),
391
+ * while an explicit leader-finished request only notifies — a subagent
392
+ * well inside its budget keeps its full legitimate working time and simply
393
+ * accelerates.
394
+ */
395
+ _finishNotified = false;
396
+ _grace = null;
397
+ /** True once the in-band finish notification has been emitted. */
398
+ get finishNotified() {
399
+ return this._finishNotified;
400
+ }
401
+ /** True once a grace window has been granted past the original deadline. */
402
+ get graceGranted() {
403
+ return this._grace !== null;
404
+ }
405
+ /**
406
+ * Notify the subagent in-band to finish its task in its own turn:
407
+ * `subagent.finish_requested` is emitted on the wired EventBus and the
408
+ * agent loop folds the notice into the conversation between tool batches.
409
+ * Nothing aborts — this is a notification, never an interrupt.
410
+ *
411
+ * `opts.graceMs` additionally extends the wall-clock ceiling by that window
412
+ * (used by the watchdog at a deadline crossing, so the model gets working
413
+ * time instead of a kill). Omit it to notify without touching the budget —
414
+ * the subagent keeps its existing time budget and just accelerates.
415
+ *
416
+ * Returns `true` when this call did something (emitted the notification
417
+ * and/or granted grace); `false` when there was nothing to do (already
418
+ * notified, grace already granted, no EventBus wired, budget not started).
419
+ */
420
+ notifyFinish(reason, opts, now = Date.now) {
421
+ if (!this._events) return false;
422
+ if (this.startTime === null) return false;
423
+ const shouldEmit = !this._finishNotified;
424
+ const rawGrace = opts?.graceMs;
425
+ const shouldGrant = rawGrace !== void 0 && this._grace === null;
426
+ if (!shouldEmit && !shouldGrant) return false;
427
+ let grantedGraceMs = 0;
428
+ let graceDeadlineMs;
429
+ if (shouldGrant && rawGrace !== void 0) {
430
+ grantedGraceMs = Number.isFinite(rawGrace) && rawGrace > 0 ? Math.floor(rawGrace) : DEFAULT_SUBAGENT_FINISH_GRACE_MS;
431
+ graceDeadlineMs = now() + grantedGraceMs;
432
+ this._grace = { deadlineMs: graceDeadlineMs, graceMs: grantedGraceMs };
433
+ this.patchLimits({ timeoutMs: graceDeadlineMs - this.startTime });
434
+ }
435
+ if (shouldEmit) {
436
+ this._finishNotified = true;
437
+ const effectiveDeadlineMs = graceDeadlineMs ?? (this.limits.timeoutMs !== void 0 ? this.startTime + this.limits.timeoutMs : now() + DEFAULT_SUBAGENT_FINISH_GRACE_MS);
438
+ const effectiveGraceMs = Math.max(0, effectiveDeadlineMs - now());
439
+ const subagentId = this._subagentId;
440
+ this._events.emit(SUBAGENT_FINISH_REQUESTED_EVENT, {
441
+ // Omitted entirely when the budget was built without an id — an
442
+ // empty string is an address that matches nothing.
443
+ ...subagentId !== void 0 ? { subagentId } : {},
444
+ reason,
445
+ deadlineMs: effectiveDeadlineMs,
446
+ graceMs: effectiveGraceMs,
447
+ notice: buildSubagentFinishNotice({
448
+ reason,
449
+ deadlineMs: effectiveDeadlineMs,
450
+ graceMs: effectiveGraceMs
451
+ })
452
+ });
453
+ }
454
+ return true;
455
+ }
456
+ /** Epoch ms by which the subagent should have produced its final output,
457
+ * once a grace window was granted. Undefined before that. */
458
+ get finishDeadlineMs() {
459
+ return this._grace?.deadlineMs;
460
+ }
345
461
  iterations = 0;
346
462
  toolCalls = 0;
347
463
  tokenInput = 0;
@@ -357,6 +473,10 @@ var SubagentBudget = class _SubagentBudget {
357
473
  lastActivityTime = null;
358
474
  _onThreshold;
359
475
  _sessionId;
476
+ /** Owning subagent id — used to address the graceful-finish event. */
477
+ _subagentId;
478
+ /** True when only the coordinator watchdog may enforce wall-clock limits. */
479
+ _wallClockWatchdogOwned;
360
480
  /**
361
481
  * Hard cap on how long `_negotiateExtension` waits for the coordinator to
362
482
  * respond before defaulting to 'stop'. Without this fallback an absent
@@ -428,6 +548,8 @@ var SubagentBudget = class _SubagentBudget {
428
548
  constructor(limits = {}, mode = "auto", options = {}) {
429
549
  this._mode = mode;
430
550
  this._sessionId = options.sessionId;
551
+ this._subagentId = options.subagentId;
552
+ this._wallClockWatchdogOwned = options.wallClockWatchdogOwned === true;
431
553
  this.limits = { ...limits };
432
554
  }
433
555
  currentSessionId() {
@@ -506,7 +628,7 @@ var SubagentBudget = class _SubagentBudget {
506
628
  if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
507
629
  exceeded.push({ kind: "idle_timeout", used: idle, limit: this.limits.idleTimeoutMs });
508
630
  }
509
- const wallOwnedByWatchdog = this._onThreshold !== void 0 && this._watchdogActive === this.limits.timeoutMs;
631
+ const wallOwnedByWatchdog = this._wallClockWatchdogOwned || this._onThreshold !== void 0 && this._watchdogActive === this.limits.timeoutMs;
510
632
  if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs && !wallOwnedByWatchdog) {
511
633
  exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
512
634
  }
@@ -705,7 +827,7 @@ var SubagentBudget = class _SubagentBudget {
705
827
  if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
706
828
  const elapsed = Date.now() - this.startTime;
707
829
  const wallSkipped = this._onThreshold !== void 0 && this._watchdogActive !== void 0 && timeoutMs !== void 0 && this._watchdogActive === timeoutMs;
708
- const wallTripped = wallSkipped ? false : timeoutMs !== void 0 && elapsed > timeoutMs;
830
+ const wallTripped = this._wallClockWatchdogOwned || wallSkipped ? false : timeoutMs !== void 0 && elapsed > timeoutMs;
709
831
  const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
710
832
  if (!wallTripped && !idleTripped) return;
711
833
  void this.checkLimits(elapsed);
@@ -1165,6 +1287,14 @@ function makeAgentSubagentRunner(opts) {
1165
1287
  );
1166
1288
  const onParentAbort = () => aborter.abort();
1167
1289
  ctx.signal.addEventListener("abort", onParentAbort);
1290
+ if (resolveGracefulFinish(ctx.config)) {
1291
+ unsub.push(
1292
+ events.on("subagent.finish_requested", (e) => {
1293
+ if (e.subagentId && e.subagentId !== ctx.subagentId) return;
1294
+ setBtwNote(agent.ctx, e.notice);
1295
+ })
1296
+ );
1297
+ }
1168
1298
  let result;
1169
1299
  try {
1170
1300
  result = await agent.run(format(task, ctx.config), { signal: aborter.signal });
@@ -10412,6 +10542,20 @@ var EXPLORE_COMPANION_AGENT = {
10412
10542
  textStream: "silent",
10413
10543
  toolStream: "silent"
10414
10544
  };
10545
+ var CHAOS_MONKEY_AGENT = {
10546
+ ...defineAgent("chaos-monkey", "Chaos Monkey"),
10547
+ tools: [...TOOLS.build],
10548
+ skillNames: ["testing", "typescript-strict"],
10549
+ spawnBudgetExempt: true,
10550
+ // Follow fleet worktree policy (NOT 'required'): mutation targets are
10551
+ // often freshly written and uncommitted — a worktree spawned from HEAD
10552
+ // would not contain them and every mutant would drift. Callers pass
10553
+ // `worktree: 'off'` in the mutation_test input for uncommitted targets.
10554
+ worktree: "auto",
10555
+ // Report travels via submit_result + final text, not the leader's stream.
10556
+ textStream: "silent",
10557
+ toolStream: "silent"
10558
+ };
10415
10559
  var CRITIC_AGENT = defineAgent("critic", "Critic");
10416
10560
  var GENERIC_AGENT = defineAgent("generic", "Generic Project Agent");
10417
10561
  function withDispatchMetadata(definition) {
@@ -10431,6 +10575,7 @@ var FLEET_ROSTER = {
10431
10575
  generic: GENERIC_AGENT,
10432
10576
  "shadow-agent": SHADOW_AGENT,
10433
10577
  "explore-companion": EXPLORE_COMPANION_AGENT,
10578
+ "chaos-monkey": CHAOS_MONKEY_AGENT,
10434
10579
  ...Object.fromEntries(
10435
10580
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, withDispatchMetadata(d)])
10436
10581
  )
@@ -10461,6 +10606,16 @@ var FLEET_ROSTER_BUDGETS = {
10461
10606
  maxTokens: 96e3,
10462
10607
  maxCostUsd: 0.5
10463
10608
  },
10609
+ "chaos-monkey": {
10610
+ // A mutation pass is many short apply/run/restore cycles — per-mutant
10611
+ // work is tiny, but a large plan (25 mutants/file × N files) needs
10612
+ // headroom. Idle-based reaping covers a stalled pass.
10613
+ idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
10614
+ maxIterations: 2e3,
10615
+ maxToolCalls: 6e3,
10616
+ maxTokens: 96e3,
10617
+ maxCostUsd: 0.5
10618
+ },
10464
10619
  ...Object.fromEntries(
10465
10620
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.budget])
10466
10621
  )
@@ -11913,7 +12068,7 @@ function attachDepWatcherBridge(opts) {
11913
12068
  }
11914
12069
 
11915
12070
  // src/coordination/director.ts
11916
- import { randomUUID as randomUUID15 } from "node:crypto";
12071
+ import { randomUUID as randomUUID16 } from "node:crypto";
11917
12072
  import * as fsp25 from "node:fs/promises";
11918
12073
 
11919
12074
  // src/core/instruction-template.ts
@@ -12988,7 +13143,7 @@ ${JSON.stringify(result.result, null, 2)}
12988
13143
  };
12989
13144
 
12990
13145
  // src/coordination/director-tools.ts
12991
- import { randomUUID as randomUUID11 } from "node:crypto";
13146
+ import { randomUUID as randomUUID12 } from "node:crypto";
12992
13147
  import {
12993
13148
  completeKanbanDispatch,
12994
13149
  failKanbanDispatch,
@@ -14188,6 +14343,413 @@ function excerpt(text, max) {
14188
14343
  ...(truncated)`;
14189
14344
  }
14190
14345
 
14346
+ // src/coordination/director-mutation-test-tool.ts
14347
+ import { randomUUID as randomUUID11 } from "node:crypto";
14348
+ import { readFileSync as readFileSync13 } from "node:fs";
14349
+ import { isAbsolute as isAbsolute3, join as join16 } from "node:path";
14350
+
14351
+ // src/coordination/mutation-engine.ts
14352
+ var TOKEN_PATTERNS = [
14353
+ {
14354
+ kind: "relax-boundary",
14355
+ // `>` not followed by `=` and not part of `=>` or `>>`; require code-ish
14356
+ // context on both sides so generic text (JSX, strings) is not touched.
14357
+ regex: /(?<=[\w\)\]\}'"`])\s*\x20?(?<op>>(?!=|>))/g,
14358
+ replace: () => ">="
14359
+ },
14360
+ {
14361
+ kind: "tighten-boundary",
14362
+ regex: /(?<=[\w\)\]\}'"`])\s*\x20?(?<op>>=)/g,
14363
+ replace: () => ">"
14364
+ },
14365
+ {
14366
+ kind: "arith-plus-to-minus",
14367
+ // `+` between operands (binary), not `++`, unary `+x`, or `+=`.
14368
+ regex: /(?<=[\w\)\]\}'"`])\s*\x20?(?<op>\+(?!\+|=))/g,
14369
+ replace: () => "-"
14370
+ },
14371
+ {
14372
+ kind: "arith-minus-to-plus",
14373
+ // Binary `-` between operands, not `--`, `-=` or negative-number literal.
14374
+ regex: /(?<=[\w\)\]\}'"`])\s*\x20?(?<op>-(?!-|=))/g,
14375
+ replace: () => "+"
14376
+ },
14377
+ {
14378
+ kind: "negate-boolean",
14379
+ // Standalone boolean literals used as values, not property names.
14380
+ regex: /(?<![.\w$])(?<op>true|false)(?![\w$])/g,
14381
+ replace: (m) => m === "true" ? "false" : "true"
14382
+ },
14383
+ {
14384
+ kind: "return-null",
14385
+ // `return <expr>;` where expr is not already null/undefined/void.
14386
+ regex: /(?<indent>\breturn\b)(?<expr>\s+[^;{}\n]+?)\s*;/g,
14387
+ replace: () => "return null;"
14388
+ }
14389
+ ];
14390
+ function planMutations(file, source, opts = {}) {
14391
+ const maxPerFile = opts.maxPerFile ?? 25;
14392
+ const out = [];
14393
+ const lines = source.split("\n");
14394
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
14395
+ const line = lines[lineIdx];
14396
+ const t = line.trim();
14397
+ if (t.startsWith("//") || t.startsWith("*") || t.startsWith("/*")) continue;
14398
+ for (const pattern of TOKEN_PATTERNS) {
14399
+ pattern.regex.lastIndex = 0;
14400
+ let m;
14401
+ while ((m = pattern.regex.exec(line)) !== null) {
14402
+ const token = m.groups?.["op"] ?? m[0];
14403
+ const tokenStart = m.index + m[0].indexOf(token);
14404
+ if (isMasked(line, tokenStart, token.length)) continue;
14405
+ const original = line.slice(tokenStart, tokenStart + token.length);
14406
+ const replacement = pattern.replace(token);
14407
+ if (replacement === original) continue;
14408
+ out.push({
14409
+ id: `${pattern.kind}#${lineIdx + 1}#${tokenStart + 1}`,
14410
+ kind: pattern.kind,
14411
+ file,
14412
+ line: lineIdx + 1,
14413
+ column: tokenStart + 1,
14414
+ original,
14415
+ replacement
14416
+ });
14417
+ }
14418
+ }
14419
+ if (out.length >= maxPerFile) break;
14420
+ }
14421
+ return out.slice(0, maxPerFile);
14422
+ }
14423
+ function isMasked(line, start, len) {
14424
+ let inSingle = false;
14425
+ let inDouble = false;
14426
+ for (let i = 0; i < start; i++) {
14427
+ const c = line[i];
14428
+ const prev = i > 0 ? line[i - 1] : void 0;
14429
+ if (c === "'" && prev !== "\\") inSingle = !inSingle;
14430
+ else if (c === '"' && prev !== "\\") inDouble = !inDouble;
14431
+ if (!inSingle && !inDouble && c === "/" && prev === "/") return true;
14432
+ }
14433
+ if (inSingle || inDouble) return true;
14434
+ const window = line.slice(start, start + len);
14435
+ return /['"]/.test(window);
14436
+ }
14437
+ function parseMutationReport(text) {
14438
+ const candidates = [];
14439
+ const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/);
14440
+ if (fence?.[1]) candidates.push(fence[1].trim());
14441
+ const firstBrace = text.indexOf("{");
14442
+ if (firstBrace >= 0) candidates.push(extractBalancedObject(text, firstBrace));
14443
+ for (const candidate of candidates) {
14444
+ if (!candidate) continue;
14445
+ try {
14446
+ const parsed = JSON.parse(candidate);
14447
+ if (!Array.isArray(parsed.mutants)) continue;
14448
+ return {
14449
+ mutants: parsed.mutants.map(normalizeMutantEntry).filter((x) => Boolean(x)),
14450
+ summary: typeof parsed.summary === "string" ? parsed.summary : void 0
14451
+ };
14452
+ } catch {
14453
+ }
14454
+ }
14455
+ return void 0;
14456
+ }
14457
+ function extractBalancedObject(text, start) {
14458
+ let depth = 0;
14459
+ let inString = false;
14460
+ let escaped = false;
14461
+ for (let i = start; i < text.length; i++) {
14462
+ const c = text[i];
14463
+ if (escaped) {
14464
+ escaped = false;
14465
+ continue;
14466
+ }
14467
+ if (c === "\\") {
14468
+ escaped = true;
14469
+ continue;
14470
+ }
14471
+ if (c === '"') inString = !inString;
14472
+ if (inString) continue;
14473
+ if (c === "{") depth++;
14474
+ else if (c === "}") {
14475
+ depth--;
14476
+ if (depth === 0) return text.slice(start, i + 1);
14477
+ }
14478
+ }
14479
+ return text.slice(start);
14480
+ }
14481
+ function normalizeMutantEntry(value) {
14482
+ if (typeof value !== "object" || value === null) return void 0;
14483
+ const rec = value;
14484
+ const id = typeof rec["id"] === "string" ? rec["id"] : void 0;
14485
+ const status = rec["status"];
14486
+ if (!id || status !== "killed" && status !== "survived" && status !== "skipped") {
14487
+ return void 0;
14488
+ }
14489
+ return {
14490
+ id,
14491
+ file: typeof rec["file"] === "string" ? rec["file"] : "",
14492
+ line: typeof rec["line"] === "number" ? rec["line"] : 0,
14493
+ kind: typeof rec["kind"] === "string" ? rec["kind"] : "",
14494
+ status,
14495
+ evidence: typeof rec["evidence"] === "string" ? rec["evidence"] : void 0
14496
+ };
14497
+ }
14498
+
14499
+ // src/coordination/director-mutation-test-tool.ts
14500
+ var DEFAULT_MAX_PER_FILE = 10;
14501
+ var DEFAULT_MAX_STRENGTHEN_ATTEMPTS = 2;
14502
+ var CHAOS_ROLE = "chaos-monkey";
14503
+ function makeMutationTestTool(director, roster, opts = {}) {
14504
+ return {
14505
+ name: "mutation_test",
14506
+ 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.",
14507
+ 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.",
14508
+ permission: "auto",
14509
+ mutating: false,
14510
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
14511
+ inputSchema: {
14512
+ type: "object",
14513
+ properties: {
14514
+ targets: {
14515
+ type: "array",
14516
+ items: { type: "string" },
14517
+ description: "Project-relative (or absolute) source files to mutate. Keep to files changed by the current task."
14518
+ },
14519
+ testCommand: {
14520
+ type: "string",
14521
+ description: 'Exact command that runs the relevant tests, e.g. "pnpm exec vitest run packages/core/tests/coordination/mutation-engine.test.ts".'
14522
+ },
14523
+ cwd: { type: "string", description: "Working directory for the test command." },
14524
+ maxPerFile: {
14525
+ type: "number",
14526
+ minimum: 1,
14527
+ maximum: 25,
14528
+ description: "Mutant cap per file per pass. Default 10."
14529
+ },
14530
+ maxStrengthenAttempts: {
14531
+ type: "number",
14532
+ minimum: 0,
14533
+ maximum: 5,
14534
+ description: "Strengthen\u2192re-verify rounds. Default 2 when repairSubagentId is set, else 0."
14535
+ },
14536
+ repairSubagentId: {
14537
+ type: "string",
14538
+ description: "Subagent that owns the tests. When set and mutants survive, it receives a strengthen-tests task and the survivors are re-verified."
14539
+ },
14540
+ chaosWorktree: {
14541
+ anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
14542
+ description: "Worktree override for the chaos agent. Use 'off' when targets are uncommitted \u2014 a worktree from HEAD would not contain them."
14543
+ },
14544
+ timeoutMs: { type: "number", minimum: 1, description: "Per-task timeout for chaos/strengthen/rerun tasks." },
14545
+ reportOnly: {
14546
+ type: "boolean",
14547
+ description: "Skip the strengthen loop even when survivors exist. Default false."
14548
+ }
14549
+ },
14550
+ required: ["targets", "testCommand"],
14551
+ additionalProperties: false
14552
+ },
14553
+ async execute(input, ctx) {
14554
+ const i = normalizeMutationTestInput(input);
14555
+ const root = opts.projectRoot ?? ctx.projectRoot;
14556
+ const plan = buildPlan(i, root);
14557
+ if (plan.length === 0) {
14558
+ return {
14559
+ verdict: "inconclusive",
14560
+ passed: false,
14561
+ error: "No mutable sites found in the given targets (after comment/string filtering)."
14562
+ };
14563
+ }
14564
+ const chaosSubagentId = await director.spawn(
14565
+ makeChaosConfig(roster, i.chaosWorktree ?? "off")
14566
+ );
14567
+ const chaosTaskId = await director.assign({
14568
+ id: randomUUID11(),
14569
+ subagentId: chaosSubagentId,
14570
+ description: buildChaosTask(plan, i, 1, []),
14571
+ timeoutMs: i.timeoutMs
14572
+ });
14573
+ const [chaosResult] = await director.awaitTasks([chaosTaskId]);
14574
+ const pass1 = collectOutcomes(chaosResult, plan);
14575
+ const survivors = pass1.filter((m) => m.status === "survived");
14576
+ const maxAttempts = clamp(
14577
+ i.maxStrengthenAttempts ?? (i.repairSubagentId && !i.reportOnly ? DEFAULT_MAX_STRENGTHEN_ATTEMPTS : 0),
14578
+ 0,
14579
+ 5
14580
+ );
14581
+ const attempts = [];
14582
+ let current = survivors;
14583
+ while (current.length > 0 && attempts.length < maxAttempts && i.repairSubagentId) {
14584
+ const attemptNo = attempts.length + 1;
14585
+ const strengthenTaskId = await director.assign({
14586
+ id: randomUUID11(),
14587
+ subagentId: i.repairSubagentId,
14588
+ description: buildStrengthenTask(current, i, attemptNo),
14589
+ timeoutMs: i.timeoutMs
14590
+ });
14591
+ const [strengthenResult] = await director.awaitTasks([strengthenTaskId]);
14592
+ if (strengthenResult?.status !== "success") {
14593
+ attempts.push({
14594
+ attempt: attemptNo,
14595
+ survivorsBefore: current,
14596
+ strengthenResult: strengthenResult ? { taskId: strengthenResult.taskId, status: strengthenResult.status } : void 0,
14597
+ survivorsAfter: current,
14598
+ suspectedEquivalent: []
14599
+ });
14600
+ break;
14601
+ }
14602
+ const survivorPlan = plan.filter((p) => current.some((s) => s.id === p.id));
14603
+ const rerunSubagentId = await director.spawn(
14604
+ makeChaosConfig(roster, i.chaosWorktree ?? "off")
14605
+ );
14606
+ const rerunTaskId = await director.assign({
14607
+ id: randomUUID11(),
14608
+ subagentId: rerunSubagentId,
14609
+ description: buildChaosTask(survivorPlan, i, attemptNo + 1, current),
14610
+ timeoutMs: i.timeoutMs
14611
+ });
14612
+ const [rerunResult] = await director.awaitTasks([rerunTaskId]);
14613
+ const passN = collectOutcomes(rerunResult, survivorPlan);
14614
+ const stillSurviving = passN.filter((m) => m.status === "survived" || m.status === "skipped");
14615
+ attempts.push({
14616
+ attempt: attemptNo,
14617
+ survivorsBefore: current,
14618
+ strengthenResult: { taskId: strengthenResult.taskId, status: strengthenResult.status },
14619
+ rerunResult: { taskId: rerunTaskId, status: rerunResult?.status ?? "unknown" },
14620
+ survivorsAfter: stillSurviving,
14621
+ suspectedEquivalent: stillSurviving.filter((m) => current.some((c) => c.id === m.id)).map((m) => m.id)
14622
+ });
14623
+ current = stillSurviving.filter((m) => m.status === "survived");
14624
+ if (passN.every((m) => m.status === "skipped")) break;
14625
+ }
14626
+ const finalSurvivors = current;
14627
+ const verifiedCount = pass1.filter((m) => m.status !== "skipped").length;
14628
+ const skippedCount = pass1.filter((m) => m.status === "skipped").length;
14629
+ const score = plan.length === 0 ? 0 : pass1.filter((m) => m.status === "killed").length / plan.length;
14630
+ const verdict = verifiedCount === 0 ? "inconclusive" : finalSurvivors.length === 0 ? skippedCount > 0 ? "partial" : "pass" : score >= 0.8 ? "partial" : "fail";
14631
+ return {
14632
+ verdict,
14633
+ passed: verdict === "pass",
14634
+ mutationScore: Number.parseFloat(score.toFixed(3)),
14635
+ planned: plan.length,
14636
+ killed: pass1.filter((m) => m.status === "killed").length,
14637
+ survived: pass1.filter((m) => m.status === "survived").length,
14638
+ skipped: pass1.filter((m) => m.status === "skipped").length,
14639
+ finalSurvivors: finalSurvivors.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
14640
+ suspectedEquivalent: attempts.flatMap((a) => a.suspectedEquivalent),
14641
+ strengthenAttempts: attempts.length,
14642
+ attempts,
14643
+ chaosTaskId,
14644
+ nextAction: finalSurvivors.length === 0 ? "accept" : attempts.length >= maxAttempts && i.repairSubagentId ? "manual_review_survivors" : "strengthen_tests"
14645
+ };
14646
+ }
14647
+ };
14648
+ }
14649
+ function normalizeMutationTestInput(input) {
14650
+ const raw = input ?? {};
14651
+ const targets = stringArray2(raw["targets"]) ?? [];
14652
+ const testCommand = typeof raw["testCommand"] === "string" ? raw["testCommand"].trim() : "";
14653
+ return {
14654
+ targets: targets.filter(Boolean),
14655
+ testCommand,
14656
+ cwd: typeof raw["cwd"] === "string" && raw["cwd"].trim() ? raw["cwd"].trim() : void 0,
14657
+ maxPerFile: typeof raw["maxPerFile"] === "number" ? raw["maxPerFile"] : void 0,
14658
+ maxStrengthenAttempts: typeof raw["maxStrengthenAttempts"] === "number" ? raw["maxStrengthenAttempts"] : void 0,
14659
+ repairSubagentId: typeof raw["repairSubagentId"] === "string" && raw["repairSubagentId"].trim() ? raw["repairSubagentId"].trim() : void 0,
14660
+ chaosWorktree: raw["chaosWorktree"] ?? void 0,
14661
+ timeoutMs: typeof raw["timeoutMs"] === "number" ? raw["timeoutMs"] : void 0,
14662
+ reportOnly: raw["reportOnly"] === true
14663
+ };
14664
+ }
14665
+ function clamp(n, lo, hi) {
14666
+ return Math.min(hi, Math.max(lo, n));
14667
+ }
14668
+ function buildPlan(i, projectRoot) {
14669
+ const plan = [];
14670
+ for (const target of i.targets) {
14671
+ const abs = isAbsolute3(target) ? target : join16(projectRoot ?? process.cwd(), target);
14672
+ let source;
14673
+ try {
14674
+ source = readFileSync13(abs, "utf8");
14675
+ } catch {
14676
+ continue;
14677
+ }
14678
+ plan.push(...planMutations(target, source, { maxPerFile: i.maxPerFile ?? DEFAULT_MAX_PER_FILE }));
14679
+ }
14680
+ return plan;
14681
+ }
14682
+ function makeChaosConfig(roster, worktree) {
14683
+ const base = roster?.[CHAOS_ROLE] ?? getAgentDefinition(CHAOS_ROLE)?.config ?? { name: "Chaos Monkey", role: CHAOS_ROLE };
14684
+ return { ...instantiateRosterConfig2(CHAOS_ROLE, base), worktree };
14685
+ }
14686
+ function buildChaosTask(plan, i, pass, priorSurvivors) {
14687
+ const mutants = plan.map(
14688
+ (m) => `- ${m.id} | ${m.file}:${m.line}:${m.column} | ${m.kind} | "${m.original}" -> "${m.replacement}"`
14689
+ ).join("\n");
14690
+ const prior = priorSurvivors.length > 0 ? `
14691
+ These mutants survived a previous pass (pass ${pass - 1}) \u2014 re-verify them against the STRENGTHENED tests:
14692
+ ${priorSurvivors.map((s) => `- ${s.id} (${s.kind} @ ${s.file}:${s.line})`).join("\n")}` : "";
14693
+ return [
14694
+ "Execute this deterministic mutation plan against the current checkout.",
14695
+ "",
14696
+ "For each mutant, in order:",
14697
+ "1. Apply ONLY that mutation at its exact (file, line, column).",
14698
+ `2. Run the test command: ${i.testCommand}${i.cwd ? ` (cwd: ${i.cwd})` : ""}`,
14699
+ "3. Record killed (tests failed \u2014 quote first failing assertion) or survived (suite green).",
14700
+ "4. Restore the file byte-for-byte before the next mutant.",
14701
+ "",
14702
+ "Mutants:",
14703
+ mutants,
14704
+ prior,
14705
+ "",
14706
+ "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.",
14707
+ "Finish with submit_result, then repeat the same JSON as your final text."
14708
+ ].join("\n");
14709
+ }
14710
+ function buildStrengthenTask(survivors, i, attempt) {
14711
+ return [
14712
+ `Strengthen the tests so these SURVIVING mutants die (attempt ${attempt}).`,
14713
+ "",
14714
+ "Each survivor below was a deliberate sabotage of production code that the current suite did NOT catch:",
14715
+ ...survivors.map((s) => `- ${s.id} | ${s.file}:${s.line} | ${s.kind}${s.evidence ? ` | ${s.evidence}` : ""}`),
14716
+ "",
14717
+ `Test command that must fail under each mutant: ${i.testCommand}`,
14718
+ "",
14719
+ "For each survivor add or tighten exactly one assertion that pins the sabotaged boundary/behavior. Do not change production code. Do not weaken other tests. Run the suite green on clean code before finishing."
14720
+ ].join("\n");
14721
+ }
14722
+ function collectOutcomes(result, plan) {
14723
+ const fromText = parseTextOutcomes(result);
14724
+ if (fromText.length > 0) {
14725
+ const planned = new Set(plan.map((p) => p.id));
14726
+ const matched = fromText.filter((m) => planned.has(m.id));
14727
+ if (matched.length > 0) return matched;
14728
+ }
14729
+ return plan.map((p) => ({
14730
+ id: p.id,
14731
+ file: p.file,
14732
+ line: p.line,
14733
+ kind: p.kind,
14734
+ status: "skipped",
14735
+ evidence: result ? `chaos task ended ${result.status}` : "chaos task produced no result"
14736
+ }));
14737
+ }
14738
+ function parseTextOutcomes(result) {
14739
+ const text = typeof result?.result === "string" ? result.result : void 0;
14740
+ if (!text) return [];
14741
+ const parsed = parseMutationReport(text);
14742
+ if (!parsed) return [];
14743
+ return parsed.mutants.map((m) => ({
14744
+ id: m.id,
14745
+ file: m.file,
14746
+ line: m.line,
14747
+ kind: m.kind,
14748
+ status: m.status,
14749
+ evidence: m.evidence
14750
+ }));
14751
+ }
14752
+
14191
14753
  // src/coordination/director-tools.ts
14192
14754
  function makeSpawnTool(director, roster) {
14193
14755
  const dispatchCatalog = () => {
@@ -14480,7 +15042,7 @@ function makeKanbanQueueTool(director, roster) {
14480
15042
  try {
14481
15043
  const config = buildKanbanSubagentConfig(claim.task, i, roster, instantiateRosterConfig2);
14482
15044
  subagentId = await director.spawn(config);
14483
- const dispatchTaskId = randomUUID11();
15045
+ const dispatchTaskId = randomUUID12();
14484
15046
  const taskSpec = {
14485
15047
  id: dispatchTaskId,
14486
15048
  subagentId,
@@ -14756,6 +15318,7 @@ function buildDirectorToolset(director, roster) {
14756
15318
  makeAskResultTool(director),
14757
15319
  makeRollUpTool(director),
14758
15320
  makeQualityGateTool(director, roster),
15321
+ makeMutationTestTool(director, roster),
14759
15322
  makeTerminateTool(director),
14760
15323
  makeTerminateAllTool(director),
14761
15324
  makeFleetTool(director),
@@ -14842,7 +15405,7 @@ import * as fsp24 from "node:fs/promises";
14842
15405
  import * as path32 from "node:path";
14843
15406
 
14844
15407
  // src/storage/session-store.ts
14845
- import { randomUUID as randomUUID13 } from "node:crypto";
15408
+ import { randomUUID as randomUUID14 } from "node:crypto";
14846
15409
  import * as fsp23 from "node:fs/promises";
14847
15410
  import * as path31 from "node:path";
14848
15411
 
@@ -16566,7 +17129,7 @@ var FileSessionWriter = class _FileSessionWriter {
16566
17129
 
16567
17130
  // src/storage/session-checkpoint-cas.ts
16568
17131
  import { spawn as spawn3 } from "node:child_process";
16569
- import { createHash as createHash3, randomUUID as randomUUID12 } from "node:crypto";
17132
+ import { createHash as createHash3, randomUUID as randomUUID13 } from "node:crypto";
16570
17133
  import * as fsp10 from "node:fs/promises";
16571
17134
  import * as path23 from "node:path";
16572
17135
 
@@ -16824,7 +17387,7 @@ var SessionCheckpointCas = class {
16824
17387
  }
16825
17388
  const temp = path23.join(
16826
17389
  path23.dirname(target),
16827
- `.${path23.basename(target)}.${process.pid}.${randomUUID12()}.tmp`
17390
+ `.${path23.basename(target)}.${process.pid}.${randomUUID13()}.tmp`
16828
17391
  );
16829
17392
  let handle;
16830
17393
  try {
@@ -18620,7 +19183,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
18620
19183
  onAppend;
18621
19184
  onAppendBatch;
18622
19185
  catalogClient;
18623
- maintenanceHolderId = randomUUID13();
19186
+ maintenanceHolderId = randomUUID14();
18624
19187
  _loadCache = /* @__PURE__ */ new Map();
18625
19188
  loadCache = new SessionLoadCache(this._loadCache);
18626
19189
  _indexCache = null;
@@ -20827,7 +21390,7 @@ function hashStr(s) {
20827
21390
  }
20828
21391
 
20829
21392
  // src/coordination/multi-agent-coordinator.ts
20830
- import { randomUUID as randomUUID14 } from "node:crypto";
21393
+ import { randomUUID as randomUUID15 } from "node:crypto";
20831
21394
  import { EventEmitter as EventEmitter2 } from "node:events";
20832
21395
 
20833
21396
  // src/coordination/coordinator/error-classifier.ts
@@ -20918,7 +21481,8 @@ async function executeSubagentWithTimeout({
20918
21481
  budget,
20919
21482
  preemptFraction = TIMEOUT_PREEMPT_FRACTION,
20920
21483
  abortSubagent,
20921
- currentSessionId
21484
+ currentSessionId,
21485
+ gracefulFinish
20922
21486
  }) {
20923
21487
  const initialTimeoutMs = budget.limits.timeoutMs;
20924
21488
  const idleLimitMs = budget.limits.idleTimeoutMs;
@@ -20949,9 +21513,17 @@ async function executeSubagentWithTimeout({
20949
21513
  const scheduleNext = () => {
20950
21514
  const wallLimit = budget.limits.timeoutMs ?? initialTimeoutMs;
20951
21515
  const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : wallLimit - (Date.now() - start);
20952
- const idleRemaining = idleLimitMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
20953
- const preemptRemaining = initialTimeoutMs === void 0 || preemptedCeiling === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * preemptFraction - (Date.now() - start);
20954
- armFor(Math.max(25, Math.min(wallRemaining, idleRemaining, preemptRemaining)));
21516
+ const idleRemaining = idleLimitMs === void 0 || gracefulFinish !== void 0 && initialTimeoutMs !== void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
21517
+ const preemptRemaining = initialTimeoutMs === void 0 || preemptedCeiling === wallLimit || gracefulFinish !== void 0 ? Number.POSITIVE_INFINITY : wallLimit * preemptFraction - (Date.now() - start);
21518
+ const next = Math.min(wallRemaining, idleRemaining, preemptRemaining);
21519
+ if (!Number.isFinite(next)) {
21520
+ if (timer) {
21521
+ clearTimeout(timer);
21522
+ timer = null;
21523
+ }
21524
+ return;
21525
+ }
21526
+ armFor(Math.max(25, next));
20955
21527
  };
20956
21528
  const negotiateTimeout = async (used, limit) => {
20957
21529
  const handler = budget.onThreshold;
@@ -21000,6 +21572,10 @@ async function executeSubagentWithTimeout({
21000
21572
  const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
21001
21573
  const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
21002
21574
  if (idleExceeded && !wallExceeded) {
21575
+ if (gracefulFinish !== void 0 && initialTimeoutMs !== void 0) {
21576
+ scheduleNext();
21577
+ return;
21578
+ }
21003
21579
  const sessionId = currentSessionId();
21004
21580
  budget._events?.emit("budget.threshold_reached", {
21005
21581
  ...sessionId ? { sessionId } : {},
@@ -21016,7 +21592,7 @@ async function executeSubagentWithTimeout({
21016
21592
  reject(new BudgetExceededError("idle_timeout", idleLimit ?? 0, budget.idleMs()));
21017
21593
  return;
21018
21594
  }
21019
- if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptState === "active" /* ACTIVE */ && elapsed >= wallLimit * preemptFraction) {
21595
+ if (wallLimit !== void 0 && !wallExceeded && gracefulFinish === void 0 && budget.onThreshold && preemptState === "active" /* ACTIVE */ && elapsed >= wallLimit * preemptFraction) {
21020
21596
  const activityTs = Date.now() - budget.idleMs();
21021
21597
  if (activityTs <= lastGrantActivityTs) {
21022
21598
  preemptState = "locked" /* LOCKED */;
@@ -21050,6 +21626,22 @@ async function executeSubagentWithTimeout({
21050
21626
  return;
21051
21627
  }
21052
21628
  const limit = wallLimit ?? 0;
21629
+ if (gracefulFinish !== void 0) {
21630
+ if (!budget.graceGranted) {
21631
+ const reason = `wall-clock budget of ${Math.round(limit / 1e3)}s reached`;
21632
+ if (budget.notifyFinish(reason, { graceMs: gracefulFinish.graceMs })) {
21633
+ scheduleNext();
21634
+ return;
21635
+ }
21636
+ abortSubagent(ctx.subagentId);
21637
+ reject(new BudgetExceededError("timeout", limit, elapsed));
21638
+ return;
21639
+ } else {
21640
+ abortSubagent(ctx.subagentId);
21641
+ reject(new BudgetExceededError("timeout", limit, elapsed));
21642
+ return;
21643
+ }
21644
+ }
21053
21645
  if (!budget.onThreshold) {
21054
21646
  abortSubagent(ctx.subagentId);
21055
21647
  reject(new BudgetExceededError("timeout", limit, elapsed));
@@ -21197,7 +21789,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
21197
21789
  return { ...subagent, name: display };
21198
21790
  }
21199
21791
  async spawn(subagent) {
21200
- const id = subagent.id || randomUUID14();
21792
+ const id = subagent.id || randomUUID15();
21201
21793
  const cfg = this.withNickname(subagent, id);
21202
21794
  if (this.subagents.has(id)) {
21203
21795
  throw new Error(`Subagent id "${id}" already exists \u2014 refusing to overwrite`);
@@ -21434,6 +22026,32 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
21434
22026
  completeTask(result) {
21435
22027
  this.recordCompletion(result);
21436
22028
  }
22029
+ /**
22030
+ * Ask every RUNNING subagent that opted into `gracefulFinish` to finish its
22031
+ * task in its own turn (see coordination/subagent-finish.ts). This is the
22032
+ * leader-side entry point for "the leader agent has finished": it delivers
22033
+ * an in-band notification between tool batches — never an interrupt, never
22034
+ * an abort. Each notified subagent keeps its existing time budget and
22035
+ * accelerates; the watchdog still bounds the maximum lifetime.
22036
+ *
22037
+ * Subagents without the policy opted in are deliberately untouched — their
22038
+ * lifecycle remains the legacy watchdog contract.
22039
+ *
22040
+ * Returns the number of subagents actually notified.
22041
+ */
22042
+ requestFinish(reason) {
22043
+ let notified = 0;
22044
+ for (const subagent of this.subagents.values()) {
22045
+ if (subagent.status !== "running") continue;
22046
+ if (!resolveGracefulFinish(subagent.config)) continue;
22047
+ const budget = subagent.activeBudget;
22048
+ if (!budget) continue;
22049
+ const usage = budget.usage();
22050
+ if (usage.iterations === 0 && usage.toolCalls === 0) continue;
22051
+ if (budget.notifyFinish(reason)) notified++;
22052
+ }
22053
+ return notified;
22054
+ }
21437
22055
  // --- internal dispatching ---------------------------------------------
21438
22056
  tryDispatchNext() {
21439
22057
  while (this.canDispatch()) {
@@ -21607,7 +22225,14 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
21607
22225
  idleTimeoutMs: rawIdleTimeoutMs ?? this.config.defaultBudget?.idleTimeoutMs ?? configWithRosterDefaults.idleTimeoutMs
21608
22226
  },
21609
22227
  "auto",
21610
- { sessionId: () => this.currentSessionId() }
22228
+ {
22229
+ sessionId: () => this.currentSessionId(),
22230
+ subagentId,
22231
+ // Graceful-finish runs own wall-clock enforcement to the watchdog so
22232
+ // the notify-then-bound lifecycle cannot be raced by tool.progress
22233
+ // heartbeats calling checkTimeout() (see subagent-budget.ts).
22234
+ ...resolveGracefulFinish(subagent.config) ? { wallClockWatchdogOwned: true } : {}
22235
+ }
21611
22236
  );
21612
22237
  subagent.activeBudget = budget;
21613
22238
  if (!this.runner) {
@@ -21640,7 +22265,8 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
21640
22265
  task,
21641
22266
  runCtx,
21642
22267
  budget,
21643
- subagent.config.preemptFraction
22268
+ subagent.config.preemptFraction,
22269
+ resolveGracefulFinish(subagent.config)
21644
22270
  );
21645
22271
  result = {
21646
22272
  subagentId,
@@ -21670,13 +22296,14 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
21670
22296
  }
21671
22297
  this.recordCompletion(result);
21672
22298
  }
21673
- async executeWithTimeout(runner, task, ctx, budget, preemptFraction) {
22299
+ async executeWithTimeout(runner, task, ctx, budget, preemptFraction, gracefulFinish) {
21674
22300
  return executeSubagentWithTimeout({
21675
22301
  runner,
21676
22302
  task,
21677
22303
  ctx,
21678
22304
  budget,
21679
22305
  preemptFraction,
22306
+ gracefulFinish,
21680
22307
  abortSubagent: (subagentId) => this.subagents.get(subagentId)?.abortController.abort(),
21681
22308
  currentSessionId: () => this.currentSessionId()
21682
22309
  });
@@ -22082,7 +22709,7 @@ var Director = class _Director {
22082
22709
  sessionProvider;
22083
22710
  sessionModel;
22084
22711
  constructor(opts) {
22085
- this.id = opts.config.coordinatorId || randomUUID15();
22712
+ this.id = opts.config.coordinatorId || randomUUID16();
22086
22713
  this.manifestPath = opts.manifestPath;
22087
22714
  this.roster = opts.roster;
22088
22715
  this.directorPreamble = opts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
@@ -22289,6 +22916,17 @@ var Director = class _Director {
22289
22916
  isWorkComplete() {
22290
22917
  return this.workCompleteFlag;
22291
22918
  }
22919
+ /**
22920
+ * Ask every running background subagent that opted into `gracefulFinish`
22921
+ * to finish its task in its own turn. In-band notification between tool
22922
+ * batches — no interrupt, no abort; each subagent keeps its time budget and
22923
+ * accelerates. Session shutdown calls this before draining Chimera work so
22924
+ * the post-session reviewer is nudged to complete rather than killed.
22925
+ * Returns the number of subagents notified.
22926
+ */
22927
+ requestFinish(reason) {
22928
+ return this.coordinator.requestFinish(reason);
22929
+ }
22292
22930
  setLeaderBtwNote(note) {
22293
22931
  return this.btwNotes.add(note);
22294
22932
  }
@@ -22365,7 +23003,7 @@ var Director = class _Director {
22365
23003
  );
22366
23004
  }
22367
23005
  const msg = {
22368
- id: randomUUID15(),
23006
+ id: randomUUID16(),
22369
23007
  type: "task",
22370
23008
  from: this.id,
22371
23009
  to: subagentId,
@@ -22619,7 +23257,7 @@ var Director = class _Director {
22619
23257
  };
22620
23258
 
22621
23259
  // src/coordination/fleet-manager.ts
22622
- import { randomUUID as randomUUID16 } from "node:crypto";
23260
+ import { randomUUID as randomUUID17 } from "node:crypto";
22623
23261
  import * as fsp26 from "node:fs/promises";
22624
23262
  import * as path33 from "node:path";
22625
23263
  var FleetManager = class {
@@ -22685,7 +23323,7 @@ var FleetManager = class {
22685
23323
  maxContext;
22686
23324
  constructor(opts = {}) {
22687
23325
  this.manifestPath = opts.manifestPath;
22688
- this.directorRunId = opts.directorRunId ?? randomUUID16();
23326
+ this.directorRunId = opts.directorRunId ?? randomUUID17();
22689
23327
  this.maxSpawns = opts.maxSpawns ?? Number.POSITIVE_INFINITY;
22690
23328
  this.maxSpawnDepth = resolveMaxSpawnDepth(opts.maxSpawnDepth);
22691
23329
  this.spawnDepth = opts.spawnDepth ?? 0;
@@ -24490,7 +25128,7 @@ function makeFleetStatusTool(opts = {}) {
24490
25128
  }
24491
25129
 
24492
25130
  // src/coordination/fleet-supervisor.ts
24493
- import { randomUUID as randomUUID17 } from "node:crypto";
25131
+ import { randomUUID as randomUUID18 } from "node:crypto";
24494
25132
  var COLLAB_ID_PREFIXES2 = ["bug-hunter-", "refactor-planner-", "critic-"];
24495
25133
  var DEFAULTS = {
24496
25134
  intervalMs: 2e4,
@@ -24768,7 +25406,7 @@ var FleetSupervisor = class {
24768
25406
  */
24769
25407
  async decide(question, context, options, risk) {
24770
25408
  const request = {
24771
- id: `fleetsup-${randomUUID17()}`,
25409
+ id: `fleetsup-${randomUUID18()}`,
24772
25410
  sessionId: this.opts.sessionId?.(),
24773
25411
  source: "system",
24774
25412
  question,
@@ -25128,6 +25766,22 @@ var SEND_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
25128
25766
  // receiver trusts the sender-asserted `sessionId`; the boundary must
25129
25767
  // refuse the field entirely.
25130
25768
  ]);
25769
+ var SEND_FORBIDDEN_FIELDS = /* @__PURE__ */ new Set([
25770
+ "from",
25771
+ "sessionAffinity"
25772
+ ]);
25773
+ function filterMailboxSendPayload(input) {
25774
+ const payload = {};
25775
+ const stripped = [];
25776
+ for (const key of Object.keys(input)) {
25777
+ if (SEND_ALLOWED_FIELDS.has(key) || SEND_FORBIDDEN_FIELDS.has(key)) {
25778
+ payload[key] = input[key];
25779
+ } else {
25780
+ stripped.push(key);
25781
+ }
25782
+ }
25783
+ return { payload, stripped };
25784
+ }
25131
25785
  var ACK_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
25132
25786
  "messageId",
25133
25787
  "read",
@@ -25365,7 +26019,9 @@ function makeMailSendTool(opts = {}) {
25365
26019
  required: ["to", "subject", "body"]
25366
26020
  },
25367
26021
  async execute(input, ctx) {
25368
- const i = input ?? {};
26022
+ const { payload: i, stripped } = filterMailboxSendPayload(
26023
+ input ?? {}
26024
+ );
25369
26025
  const rawTo = i.to;
25370
26026
  const subject = i.subject;
25371
26027
  const body = i.body;
@@ -25388,15 +26044,13 @@ function makeMailSendTool(opts = {}) {
25388
26044
  recipientAliases: /* @__PURE__ */ new Set([codecIdentity.baseId]),
25389
26045
  sessionId: codecIdentity.sessionId
25390
26046
  };
26047
+ let parsed;
25391
26048
  try {
25392
- parseMailboxSendInput(i, codecActor);
26049
+ parsed = parseMailboxSendInput(i, codecActor);
25393
26050
  } catch (err) {
25394
26051
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
25395
26052
  }
25396
- const audience = i.audience;
25397
- if (audience !== void 0 && audience !== "all" && audience !== "leaders") {
25398
- return { ok: false, error: '"audience" must be "all" or "leaders".' };
25399
- }
26053
+ const audience = parsed.audience;
25400
26054
  const mb = resolveMailbox(ctx);
25401
26055
  const identity = await register(mb, ctx);
25402
26056
  const requestedTo = normalizeRecipient(rawTo, identity.sessionId);
@@ -25410,10 +26064,10 @@ function makeMailSendTool(opts = {}) {
25410
26064
  to: delivery.to,
25411
26065
  type: resolvedType,
25412
26066
  audience: delivery.audience,
25413
- subject,
25414
- body,
25415
- priority: i.priority ?? "normal",
25416
- replyTo: i.replyTo,
26067
+ subject: parsed.subject,
26068
+ body: parsed.body,
26069
+ priority: parsed.priority,
26070
+ replyTo: parsed.replyTo,
25417
26071
  senderSessionId: identity.sessionId
25418
26072
  });
25419
26073
  return {
@@ -25421,7 +26075,9 @@ function makeMailSendTool(opts = {}) {
25421
26075
  messageId: msg.id,
25422
26076
  from: identity.callerId,
25423
26077
  to: msg.to,
25424
- summary: `Mail sent to ${msg.to === "*" ? "all agents" : msg.to} as ${identity.callerId}.`
26078
+ // Surfacing what was stripped keeps the send auditable without
26079
+ // re-introducing the clutter into the payload itself.
26080
+ ...stripped.length > 0 ? { strippedFields: stripped, summary: `Mail sent to ${msg.to === "*" ? "all agents" : msg.to} as ${identity.callerId}. Ignored ${stripped.length} unrecognized field(s): ${stripped.join(", ")}.` } : { summary: `Mail sent to ${msg.to === "*" ? "all agents" : msg.to} as ${identity.callerId}.` }
25425
26081
  };
25426
26082
  }
25427
26083
  };
@@ -28943,7 +29599,7 @@ function createAgentMonitorService(opts) {
28943
29599
  }
28944
29600
 
28945
29601
  // src/coordination/autonomous-brain.ts
28946
- import { randomUUID as randomUUID18 } from "node:crypto";
29602
+ import { randomUUID as randomUUID19 } from "node:crypto";
28947
29603
  var AutonomousBrain = class {
28948
29604
  graph;
28949
29605
  // Fleet bus for emitting decisions — null-safe, no-op if not provided
@@ -29049,7 +29705,7 @@ var AutonomousBrain = class {
29049
29705
  consequence: i === 0 ? `Spawn the most appropriate agent for: ${taskDescription.slice(0, 80)}` : `Spawn an alternative agent for the same task`
29050
29706
  }));
29051
29707
  return this.decideAuto({
29052
- id: randomUUID18(),
29708
+ id: randomUUID19(),
29053
29709
  source,
29054
29710
  decisionType: "spawn",
29055
29711
  question: `Should we spawn a subagent for this task?`,
@@ -29092,7 +29748,7 @@ var AutonomousBrain = class {
29092
29748
  }
29093
29749
  ];
29094
29750
  return this.decideAuto({
29095
- id: randomUUID18(),
29751
+ id: randomUUID19(),
29096
29752
  source,
29097
29753
  decisionType: "approve_change",
29098
29754
  question: `Should we approve the change "${change.title}"?`,
@@ -29151,7 +29807,7 @@ var AutonomousBrain = class {
29151
29807
  consequence: "Break the task into smaller sub-tasks"
29152
29808
  });
29153
29809
  return this.decideAuto({
29154
- id: randomUUID18(),
29810
+ id: randomUUID19(),
29155
29811
  source,
29156
29812
  decisionType: "escalate_task",
29157
29813
  question: `Task failed: ${error.slice(0, 100)}. How should we proceed?`,
@@ -29285,10 +29941,10 @@ ${ctx.error}`);
29285
29941
  };
29286
29942
 
29287
29943
  // src/coordination/autonomous-coordinator.ts
29288
- import { randomUUID as randomUUID21 } from "node:crypto";
29944
+ import { randomUUID as randomUUID22 } from "node:crypto";
29289
29945
 
29290
29946
  // src/coordination/knowledge-graph.ts
29291
- import { randomUUID as randomUUID19 } from "node:crypto";
29947
+ import { randomUUID as randomUUID20 } from "node:crypto";
29292
29948
  import * as fsp28 from "node:fs/promises";
29293
29949
  import * as path41 from "node:path";
29294
29950
  var DEFAULT_MAX_NODES = 2e3;
@@ -29336,7 +29992,7 @@ var KnowledgeGraph = class _KnowledgeGraph {
29336
29992
  * Returns the node with its assigned id.
29337
29993
  */
29338
29994
  async add(node) {
29339
- const full = { id: randomUUID19(), ...node };
29995
+ const full = { id: randomUUID20(), ...node };
29340
29996
  this.nodes.set(full.id, full);
29341
29997
  this._trackSeq(full.id);
29342
29998
  this._addToIndex(full, this._indexKeys(full));
@@ -29465,8 +30121,8 @@ var KnowledgeGraph = class _KnowledgeGraph {
29465
30121
  if (this.subs.size >= MAX_SUBSCRIPTIONS) {
29466
30122
  throw new Error(`Knowledge graph subscription limit reached (${MAX_SUBSCRIPTIONS})`);
29467
30123
  }
29468
- const channel = randomUUID19();
29469
- const sub = { id: randomUUID19(), agentId, filter, channel };
30124
+ const channel = randomUUID20();
30125
+ const sub = { id: randomUUID20(), agentId, filter, channel };
29470
30126
  this.subs.set(channel, sub);
29471
30127
  this.pendingDeliveries.set(channel, []);
29472
30128
  return channel;
@@ -29947,7 +30603,7 @@ var TaskDAG = class {
29947
30603
  };
29948
30604
 
29949
30605
  // src/coordination/task-auctioneer.ts
29950
- import { randomUUID as randomUUID20 } from "node:crypto";
30606
+ import { randomUUID as randomUUID21 } from "node:crypto";
29951
30607
  function isTerminalGoalStatus(status) {
29952
30608
  return status === "done" || status === "failed";
29953
30609
  }
@@ -30070,7 +30726,7 @@ var TaskAuctioneer = class {
30070
30726
  const score = dispatchResult.confidence * (dispatchResult.role === agent.agentRole ? 1.2 : 1);
30071
30727
  if (score < this.minConfidence) return false;
30072
30728
  const bid = {
30073
- id: randomUUID20(),
30729
+ id: randomUUID21(),
30074
30730
  taskId,
30075
30731
  agentId: agent.agentId,
30076
30732
  agentName: agent.agentName,
@@ -31007,7 +31663,7 @@ var AutonomousCoordinator = class _AutonomousCoordinator {
31007
31663
  break;
31008
31664
  }
31009
31665
  const decision = await this.brain.decideAuto({
31010
- id: randomUUID21(),
31666
+ id: randomUUID22(),
31011
31667
  source: "system",
31012
31668
  decisionType: "prioritize_goals",
31013
31669
  question: `What should we work on next? Open goals: ${dispatchable.map((g) => g.title).join(", ")}`,
@@ -31903,6 +32559,7 @@ export {
31903
32559
  makeMailInboxTool,
31904
32560
  makeMailSendTool,
31905
32561
  makeMailboxTool,
32562
+ makeMutationTestTool,
31906
32563
  makeQualityGateTool,
31907
32564
  makeRollUpTool,
31908
32565
  makeSpawnTool,