@wrongstack/core 0.308.7 → 0.309.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/coordination/director/director-toolset.d.ts +2 -2
  2. package/dist/coordination/director-mutation-test-tool.d.ts +29 -0
  3. package/dist/coordination/director-tools.d.ts +2 -0
  4. package/dist/coordination/director.d.ts +16 -0
  5. package/dist/coordination/explore-companion.d.ts +9 -6
  6. package/dist/coordination/fleet.d.ts +12 -0
  7. package/dist/coordination/index.d.ts +1 -1
  8. package/dist/coordination/index.js +990 -61
  9. package/dist/coordination/mail-tools.d.ts +10 -6
  10. package/dist/coordination/mailbox-codecs.d.ts +31 -0
  11. package/dist/coordination/multi-agent-coordinator.d.ts +14 -0
  12. package/dist/coordination/multi-agent-timeout.d.ts +11 -1
  13. package/dist/coordination/mutation-engine.d.ts +76 -0
  14. package/dist/coordination/subagent-budget.d.ts +54 -0
  15. package/dist/coordination/subagent-finish.d.ts +78 -0
  16. package/dist/core/index.js +58 -13
  17. package/dist/defaults/index.js +1132 -106
  18. package/dist/execution/index.js +260 -20
  19. package/dist/hq/index.js +45 -5
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +1456 -263
  22. package/dist/infrastructure/index.js +22 -3
  23. package/dist/kernel/events/agent-events.d.ts +31 -2
  24. package/dist/models/index.js +11 -1
  25. package/dist/observability/index.js +1 -1
  26. package/dist/plugin/index.js +113 -12
  27. package/dist/prompts/index.js +360 -3
  28. package/dist/security/auto-approve-policy.d.ts +2 -2
  29. package/dist/security/index.js +177 -50
  30. package/dist/security/permission-helpers.d.ts +11 -0
  31. package/dist/security/permission-policy.d.ts +10 -1
  32. package/dist/security/yolo-risk.d.ts +17 -0
  33. package/dist/session-catalog/index.js +32 -2
  34. package/dist/session-catalog/project-server.js +32 -2
  35. package/dist/skills/index.js +39 -6
  36. package/dist/storage/index.js +44 -3
  37. package/dist/types/index.d.ts +1 -1
  38. package/dist/types/index.js +14 -0
  39. package/dist/types/multi-agent.d.ts +15 -0
  40. package/dist/types/provider.d.ts +29 -1
  41. package/dist/types/tool.d.ts +15 -0
  42. package/dist/utils/index.d.ts +1 -0
  43. package/dist/utils/index.js +54 -4
  44. package/dist/utils/terminal-sanitize.d.ts +41 -0
  45. package/dist/utils/tool-subject.d.ts +1 -1
  46. package/instructions/agents/chaos-monkey.md +61 -0
  47. package/package.json +4 -4
@@ -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 });
@@ -8469,6 +8599,14 @@ var PATTERNS = [
8469
8599
  anchor: "sk-ant-"
8470
8600
  },
8471
8601
  { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g, anchor: "sk-" },
8602
+ {
8603
+ // `xai` is a first-class provider in this codebase, but its key shape was
8604
+ // absent here — so the one credential format WrongStack itself hands users
8605
+ // was the one the scrubber could not recognize (audit 2026-08-20).
8606
+ type: "xai_key",
8607
+ regex: /(?<![A-Za-z0-9])xai-[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g,
8608
+ anchor: "xai-"
8609
+ },
8472
8610
  { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g, anchor: "ghp_" },
8473
8611
  { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g, anchor: "github_pat_" },
8474
8612
  { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g, anchor: "AKIA" },
@@ -8559,8 +8697,8 @@ var PATTERNS = [
8559
8697
  // replacement so the separator between adjacent secrets is preserved
8560
8698
  // rather than collapsed. Capture groups are therefore: 1=leading
8561
8699
  // delimiter, 2=key name, 3=value.
8562
- regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
8563
- anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
8700
+ regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD|PASSPHRASE))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
8701
+ anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD", "PASSPHRASE"]
8564
8702
  },
8565
8703
  {
8566
8704
  type: "json_credential_key",
@@ -8677,6 +8815,27 @@ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key
8677
8815
  var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
8678
8816
  var SCRUB_CHUNK_BYTES = 64 * 1024;
8679
8817
  var SCRUB_OVERLAP_BYTES = 1024;
8818
+ var PEM_PRIVATE_KEY_BEGIN_RE = /-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----/;
8819
+ var PEM_END_MARKER = "-----END";
8820
+ var MAX_PEM_BLOCK_BYTES = 64 * 1024;
8821
+ var PEM_END_LINE_TOLERANCE = 64;
8822
+ function extendChunkBoundaryPastPem(text, chunkStart, proposedEnd) {
8823
+ const head = text.slice(chunkStart, proposedEnd);
8824
+ const lastBegin = head.lastIndexOf("-----BEGIN ");
8825
+ if (lastBegin === -1) return proposedEnd;
8826
+ const fromBegin = text.slice(chunkStart + lastBegin);
8827
+ const marker = PEM_PRIVATE_KEY_BEGIN_RE.exec(fromBegin);
8828
+ if (!marker || marker.index !== 0) return proposedEnd;
8829
+ const bodyStart = marker[0].length;
8830
+ const cap = Math.min(text.length, chunkStart + lastBegin + MAX_PEM_BLOCK_BYTES);
8831
+ const closeIdx = fromBegin.indexOf(PEM_END_MARKER, bodyStart);
8832
+ if (closeIdx === -1 || chunkStart + lastBegin + closeIdx >= cap + PEM_END_LINE_TOLERANCE) {
8833
+ return proposedEnd;
8834
+ }
8835
+ const lineEnd = fromBegin.indexOf("\n", closeIdx);
8836
+ const end = lineEnd === -1 ? text.length : chunkStart + lastBegin + lineEnd + 1;
8837
+ return Math.max(proposedEnd, end);
8838
+ }
8680
8839
  var PATTERN_ANCHORS = [
8681
8840
  ...new Set(
8682
8841
  PATTERNS.flatMap(
@@ -8713,6 +8872,7 @@ var DefaultSecretScrubber = class {
8713
8872
  }
8714
8873
  }
8715
8874
  end = safe === -1 ? end : safe + 1;
8875
+ end = extendChunkBoundaryPastPem(text, i, end);
8716
8876
  }
8717
8877
  out.push(this.scrubOne(text.slice(i, end)));
8718
8878
  i = end;
@@ -10412,6 +10572,22 @@ var EXPLORE_COMPANION_AGENT = {
10412
10572
  textStream: "silent",
10413
10573
  toolStream: "silent"
10414
10574
  };
10575
+ var CHAOS_MONKEY_AGENT = {
10576
+ ...defineAgent("chaos-monkey", "Chaos Monkey"),
10577
+ tools: [...TOOLS.build],
10578
+ skillNames: ["testing", "typescript-strict"],
10579
+ spawnBudgetExempt: true,
10580
+ // Run in the live checkout: mutation targets are usually freshly
10581
+ // written and uncommitted — a worktree spawned from HEAD would not
10582
+ // contain them and every mutant would drift. The mutation_test tool
10583
+ // honors this value as its default; callers can still override per
10584
+ // call via its `chaosWorktree` input when targets are committed and
10585
+ // isolation is wanted.
10586
+ worktree: "off",
10587
+ // Report travels via submit_result + final text, not the leader's stream.
10588
+ textStream: "silent",
10589
+ toolStream: "silent"
10590
+ };
10415
10591
  var CRITIC_AGENT = defineAgent("critic", "Critic");
10416
10592
  var GENERIC_AGENT = defineAgent("generic", "Generic Project Agent");
10417
10593
  function withDispatchMetadata(definition) {
@@ -10431,6 +10607,7 @@ var FLEET_ROSTER = {
10431
10607
  generic: GENERIC_AGENT,
10432
10608
  "shadow-agent": SHADOW_AGENT,
10433
10609
  "explore-companion": EXPLORE_COMPANION_AGENT,
10610
+ "chaos-monkey": CHAOS_MONKEY_AGENT,
10434
10611
  ...Object.fromEntries(
10435
10612
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, withDispatchMetadata(d)])
10436
10613
  )
@@ -10461,6 +10638,16 @@ var FLEET_ROSTER_BUDGETS = {
10461
10638
  maxTokens: 96e3,
10462
10639
  maxCostUsd: 0.5
10463
10640
  },
10641
+ "chaos-monkey": {
10642
+ // A mutation pass is many short apply/run/restore cycles — per-mutant
10643
+ // work is tiny, but a large plan (25 mutants/file × N files) needs
10644
+ // headroom. Idle-based reaping covers a stalled pass.
10645
+ idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
10646
+ maxIterations: 2e3,
10647
+ maxToolCalls: 6e3,
10648
+ maxTokens: 96e3,
10649
+ maxCostUsd: 0.5
10650
+ },
10464
10651
  ...Object.fromEntries(
10465
10652
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.budget])
10466
10653
  )
@@ -11571,16 +11758,14 @@ var ExploreCompanion = class {
11571
11758
  this.running = true;
11572
11759
  this.unsubscribers.push(
11573
11760
  this.opts.events.on("tool.executed", (e) => {
11574
- const lsid = this.resolveLeaderSessionId();
11575
- if (lsid && e.sessionId && e.sessionId !== lsid) return;
11761
+ if (e.sessionId !== this.resolveLeaderSessionId()) return;
11576
11762
  this.trackToolExecuted(e);
11577
11763
  })
11578
11764
  );
11579
11765
  if (this.cfg.signals.todoInProgress && this.resolveLeaderAgentId()) {
11580
11766
  this.unsubscribers.push(
11581
11767
  this.opts.events.on("session.agents_updated", (e) => {
11582
- const lsid = this.resolveLeaderSessionId();
11583
- if (lsid && e.sessionId && e.sessionId !== lsid) return;
11768
+ if (e.sessionId !== this.resolveLeaderSessionId()) return;
11584
11769
  this.trackAgentTodos(e.agents);
11585
11770
  })
11586
11771
  );
@@ -11588,8 +11773,7 @@ var ExploreCompanion = class {
11588
11773
  if (this.cfg.signals.errorSymbol) {
11589
11774
  this.unsubscribers.push(
11590
11775
  this.opts.events.on("error", (e) => {
11591
- const lsid = this.resolveLeaderSessionId();
11592
- if (lsid && e.sessionId && e.sessionId !== lsid) return;
11776
+ if (e.sessionId !== this.resolveLeaderSessionId()) return;
11593
11777
  this.trackError(e.err);
11594
11778
  })
11595
11779
  );
@@ -11702,9 +11886,18 @@ var ExploreCompanion = class {
11702
11886
  limit: 20
11703
11887
  });
11704
11888
  const lsid = this.resolveLeaderSessionId();
11889
+ const selfRecipients = new Set(
11890
+ [
11891
+ this.cfg.companionAgentId,
11892
+ mailboxIdentityBase(this.cfg.companionAgentId),
11893
+ ...lsid != null ? [sessionRecipient(lsid)] : []
11894
+ ].map((r) => r.toLowerCase())
11895
+ );
11705
11896
  for (const msg of messages) {
11706
11897
  if (msg.type !== "ask" && msg.type !== "assign") continue;
11707
- const fromLeader = isMailboxLeader(msg.from) || lsid != null && msg.senderSessionId === lsid;
11898
+ const to = msg.to.trim().toLowerCase();
11899
+ if (to !== "*" && !selfRecipients.has(to)) continue;
11900
+ const fromLeader = msg.senderSessionId === void 0 && isMailboxLeader(msg.from) || lsid != null && msg.senderSessionId === lsid;
11708
11901
  if (!fromLeader) continue;
11709
11902
  this.engage({
11710
11903
  id: randomUUID6(),
@@ -11913,7 +12106,7 @@ function attachDepWatcherBridge(opts) {
11913
12106
  }
11914
12107
 
11915
12108
  // src/coordination/director.ts
11916
- import { randomUUID as randomUUID15 } from "node:crypto";
12109
+ import { randomUUID as randomUUID16 } from "node:crypto";
11917
12110
  import * as fsp25 from "node:fs/promises";
11918
12111
 
11919
12112
  // src/core/instruction-template.ts
@@ -12988,7 +13181,7 @@ ${JSON.stringify(result.result, null, 2)}
12988
13181
  };
12989
13182
 
12990
13183
  // src/coordination/director-tools.ts
12991
- import { randomUUID as randomUUID11 } from "node:crypto";
13184
+ import { randomUUID as randomUUID12 } from "node:crypto";
12992
13185
  import {
12993
13186
  completeKanbanDispatch,
12994
13187
  failKanbanDispatch,
@@ -14188,6 +14381,626 @@ function excerpt(text, max) {
14188
14381
  ...(truncated)`;
14189
14382
  }
14190
14383
 
14384
+ // src/coordination/director-mutation-test-tool.ts
14385
+ import { randomUUID as randomUUID11 } from "node:crypto";
14386
+ import { readFileSync as readFileSync13 } from "node:fs";
14387
+ import { isAbsolute as isAbsolute3, join as join16 } from "node:path";
14388
+
14389
+ // src/coordination/mutation-engine.ts
14390
+ var TOKEN_PATTERNS = [
14391
+ {
14392
+ kind: "relax-boundary",
14393
+ // `>` not followed by `=` and not part of `=>` or `>>`; require code-ish
14394
+ // context on both sides so generic text (JSX, strings) is not touched.
14395
+ regex: /(?<=[\w\)\]\}'"`])\s*\x20?(?<op>>(?!=|>))/g,
14396
+ replace: () => ">="
14397
+ },
14398
+ {
14399
+ kind: "tighten-boundary",
14400
+ regex: /(?<=[\w\)\]\}'"`])\s*\x20?(?<op>>=)/g,
14401
+ replace: () => ">"
14402
+ },
14403
+ {
14404
+ kind: "arith-plus-to-minus",
14405
+ // `+` between operands (binary), not `++`, unary `+x`, or `+=`.
14406
+ regex: /(?<=[\w\)\]\}'"`])\s*\x20?(?<op>\+(?!\+|=))/g,
14407
+ replace: () => "-"
14408
+ },
14409
+ {
14410
+ kind: "arith-minus-to-plus",
14411
+ // Binary `-` between operands, not `--`, `-=` or negative-number literal.
14412
+ regex: /(?<=[\w\)\]\}'"`])\s*\x20?(?<op>-(?!-|=))/g,
14413
+ replace: () => "+"
14414
+ },
14415
+ {
14416
+ kind: "negate-boolean",
14417
+ // Standalone boolean literals used as values, not property names.
14418
+ regex: /(?<![.\w$])(?<op>true|false)(?![\w$])/g,
14419
+ replace: (m) => m === "true" ? "false" : "true"
14420
+ },
14421
+ {
14422
+ kind: "return-null",
14423
+ // `return <expr>;` where expr is not already null/undefined/void.
14424
+ regex: /(?<indent>\breturn\b)(?<expr>\s+[^;{}\n]+?)\s*;/g,
14425
+ replace: () => "return null;",
14426
+ endpointsInCode: true
14427
+ }
14428
+ ];
14429
+ function planMutations(file, source, opts = {}) {
14430
+ const maxPerFile = opts.maxPerFile ?? 25;
14431
+ const out = [];
14432
+ const lines = source.split("\n");
14433
+ const masks = computeLineMasks(source);
14434
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
14435
+ const line = lines[lineIdx];
14436
+ const t = line.trim();
14437
+ if (t.startsWith("//")) continue;
14438
+ const codeRanges = masks[lineIdx];
14439
+ const inCode = (start) => codeRanges.some(([s, e]) => start >= s && start < e);
14440
+ for (const pattern of TOKEN_PATTERNS) {
14441
+ pattern.regex.lastIndex = 0;
14442
+ let m;
14443
+ while ((m = pattern.regex.exec(line)) !== null) {
14444
+ const token = m.groups?.["op"] ?? m[0];
14445
+ const tokenStart = m.index + m[0].indexOf(token);
14446
+ if (!inCode(tokenStart)) continue;
14447
+ if (pattern.endpointsInCode && !inCode(tokenStart + token.length - 1)) continue;
14448
+ const original = line.slice(tokenStart, tokenStart + token.length);
14449
+ const replacement = pattern.replace(token);
14450
+ if (replacement === original) continue;
14451
+ out.push({
14452
+ id: `${pattern.kind}#${lineIdx + 1}#${tokenStart + 1}`,
14453
+ kind: pattern.kind,
14454
+ file,
14455
+ line: lineIdx + 1,
14456
+ column: tokenStart + 1,
14457
+ original,
14458
+ replacement
14459
+ });
14460
+ }
14461
+ }
14462
+ if (out.length >= maxPerFile) break;
14463
+ }
14464
+ return out.slice(0, maxPerFile);
14465
+ }
14466
+ function computeLineMasks(source) {
14467
+ const lines = source.split("\n");
14468
+ const masks = lines.map(() => []);
14469
+ const stack = [{ kind: "code", depth: 0, parens: [] }];
14470
+ let inBlockComment = false;
14471
+ let lastToken = null;
14472
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
14473
+ const line = lines[lineIdx];
14474
+ const ranges = masks[lineIdx];
14475
+ let runStart = null;
14476
+ const closeRun = (end) => {
14477
+ if (runStart !== null && end > runStart) ranges.push([runStart, end]);
14478
+ runStart = null;
14479
+ };
14480
+ let i = 0;
14481
+ if (inBlockComment) {
14482
+ const close = line.indexOf("*/");
14483
+ if (close === -1) continue;
14484
+ inBlockComment = false;
14485
+ i = close + 2;
14486
+ }
14487
+ while (i < line.length) {
14488
+ const top = stack[stack.length - 1];
14489
+ const c = line[i];
14490
+ if (top.kind === "template") {
14491
+ if (c === "\\") {
14492
+ i += 2;
14493
+ continue;
14494
+ }
14495
+ if (c === "`") {
14496
+ stack.pop();
14497
+ lastToken = "`";
14498
+ i++;
14499
+ continue;
14500
+ }
14501
+ if (c === "$" && line[i + 1] === "{") {
14502
+ stack.push({ kind: "code", depth: 0, parens: [] });
14503
+ lastToken = "${";
14504
+ i += 2;
14505
+ continue;
14506
+ }
14507
+ i++;
14508
+ continue;
14509
+ }
14510
+ if (/[\w$]/.test(c)) {
14511
+ let j = i + 1;
14512
+ while (j < line.length && /[\w$]/.test(line[j])) j++;
14513
+ lastToken = line.slice(i, j);
14514
+ if (runStart === null) runStart = i;
14515
+ i = j;
14516
+ continue;
14517
+ }
14518
+ if (c === "'" || c === '"') {
14519
+ closeRun(i);
14520
+ i++;
14521
+ while (i < line.length && line[i] !== c) {
14522
+ if (line[i] === "\\") i++;
14523
+ i++;
14524
+ }
14525
+ i++;
14526
+ lastToken = c;
14527
+ continue;
14528
+ }
14529
+ if (c === "`") {
14530
+ closeRun(i);
14531
+ stack.push({ kind: "template", depth: 0, parens: [] });
14532
+ i++;
14533
+ continue;
14534
+ }
14535
+ if (c === "/" && line[i + 1] === "/") {
14536
+ closeRun(i);
14537
+ break;
14538
+ }
14539
+ if (c === "/" && line[i + 1] === "*") {
14540
+ closeRun(i);
14541
+ const close = line.indexOf("*/", i + 2);
14542
+ if (close === -1) {
14543
+ inBlockComment = true;
14544
+ break;
14545
+ }
14546
+ i = close + 2;
14547
+ continue;
14548
+ }
14549
+ if (c === "/") {
14550
+ if (!tokenCanEndOperand(lastToken)) {
14551
+ closeRun(i);
14552
+ const next = skipRegexLiteral(line, i);
14553
+ lastToken = next > i + 1 ? "regex" : "/";
14554
+ i = next;
14555
+ continue;
14556
+ }
14557
+ }
14558
+ if (c === "(") {
14559
+ top.parens.push(CONTROL_KEYWORDS.has(lastToken ?? "") ? "control" : "expr");
14560
+ lastToken = c;
14561
+ } else if (c === ")") {
14562
+ const kind = top.parens.pop() ?? "expr";
14563
+ lastToken = kind === "control" ? "control-paren-close" : ")";
14564
+ } else if (c === "{") {
14565
+ top.depth++;
14566
+ lastToken = c;
14567
+ } else if (c === "}") {
14568
+ if (top.depth > 0) {
14569
+ top.depth--;
14570
+ lastToken = c;
14571
+ } else if (stack.length > 1) {
14572
+ closeRun(i);
14573
+ stack.pop();
14574
+ i++;
14575
+ continue;
14576
+ } else {
14577
+ lastToken = c;
14578
+ }
14579
+ } else if (c !== " " && c !== " " && c !== "\r") {
14580
+ lastToken = c;
14581
+ }
14582
+ if (runStart === null) runStart = i;
14583
+ i++;
14584
+ }
14585
+ closeRun(line.length);
14586
+ }
14587
+ return masks;
14588
+ }
14589
+ var KEYWORDS_BEFORE_REGEX = /* @__PURE__ */ new Set([
14590
+ "return",
14591
+ "typeof",
14592
+ "instanceof",
14593
+ "in",
14594
+ "of",
14595
+ "new",
14596
+ "delete",
14597
+ "void",
14598
+ "throw",
14599
+ "case",
14600
+ "do",
14601
+ "else",
14602
+ "yield",
14603
+ "await"
14604
+ ]);
14605
+ var CONTROL_KEYWORDS = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "catch", "with", "await"]);
14606
+ function tokenCanEndOperand(token) {
14607
+ if (token === null) return false;
14608
+ if (/^[\w$]+$/.test(token)) return !KEYWORDS_BEFORE_REGEX.has(token);
14609
+ return token === ")" || token === "]" || token === "." || token === '"' || token === "'" || token === "`";
14610
+ }
14611
+ function skipRegexLiteral(line, start) {
14612
+ let i = start + 1;
14613
+ let inClass = false;
14614
+ while (i < line.length) {
14615
+ const ch = line[i];
14616
+ if (ch === "\\") {
14617
+ i += 2;
14618
+ continue;
14619
+ }
14620
+ if (inClass) {
14621
+ if (ch === "]") inClass = false;
14622
+ i++;
14623
+ continue;
14624
+ }
14625
+ if (ch === "[") {
14626
+ inClass = true;
14627
+ i++;
14628
+ continue;
14629
+ }
14630
+ if (ch === "/") {
14631
+ i++;
14632
+ break;
14633
+ }
14634
+ if (ch === "\n" || ch === "\r") return line.length;
14635
+ i++;
14636
+ }
14637
+ while (i < line.length && /[a-z]/.test(line[i])) i++;
14638
+ return i;
14639
+ }
14640
+ function parseMutationReport(text) {
14641
+ const candidates = [];
14642
+ const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/);
14643
+ if (fence?.[1]) candidates.push(fence[1].trim());
14644
+ const firstBrace = text.indexOf("{");
14645
+ if (firstBrace >= 0) candidates.push(extractBalancedObject(text, firstBrace));
14646
+ for (const candidate of candidates) {
14647
+ if (!candidate) continue;
14648
+ try {
14649
+ const parsed = JSON.parse(candidate);
14650
+ if (!Array.isArray(parsed.mutants)) continue;
14651
+ return {
14652
+ mutants: parsed.mutants.map(normalizeMutantEntry).filter((x) => Boolean(x)),
14653
+ summary: typeof parsed.summary === "string" ? parsed.summary : void 0
14654
+ };
14655
+ } catch {
14656
+ }
14657
+ }
14658
+ return void 0;
14659
+ }
14660
+ function extractBalancedObject(text, start) {
14661
+ let depth = 0;
14662
+ let inString = false;
14663
+ let escaped = false;
14664
+ for (let i = start; i < text.length; i++) {
14665
+ const c = text[i];
14666
+ if (escaped) {
14667
+ escaped = false;
14668
+ continue;
14669
+ }
14670
+ if (c === "\\") {
14671
+ escaped = true;
14672
+ continue;
14673
+ }
14674
+ if (c === '"') inString = !inString;
14675
+ if (inString) continue;
14676
+ if (c === "{") depth++;
14677
+ else if (c === "}") {
14678
+ depth--;
14679
+ if (depth === 0) return text.slice(start, i + 1);
14680
+ }
14681
+ }
14682
+ return text.slice(start);
14683
+ }
14684
+ function normalizeMutantEntry(value) {
14685
+ if (typeof value !== "object" || value === null) return void 0;
14686
+ const rec = value;
14687
+ const id = typeof rec["id"] === "string" ? rec["id"] : void 0;
14688
+ const status = rec["status"];
14689
+ if (!id || status !== "killed" && status !== "survived" && status !== "skipped" && status !== "killed-by-hang") {
14690
+ return void 0;
14691
+ }
14692
+ return {
14693
+ id,
14694
+ file: typeof rec["file"] === "string" ? rec["file"] : "",
14695
+ line: typeof rec["line"] === "number" ? rec["line"] : 0,
14696
+ kind: typeof rec["kind"] === "string" ? rec["kind"] : "",
14697
+ status,
14698
+ evidence: typeof rec["evidence"] === "string" ? rec["evidence"] : void 0
14699
+ };
14700
+ }
14701
+
14702
+ // src/coordination/director-mutation-test-tool.ts
14703
+ var DEFAULT_MAX_PER_FILE = 10;
14704
+ var DEFAULT_MAX_STRENGTHEN_ATTEMPTS = 2;
14705
+ var CHAOS_ROLE = "chaos-monkey";
14706
+ function makeMutationTestTool(director, roster, opts = {}) {
14707
+ return {
14708
+ name: "mutation_test",
14709
+ 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.",
14710
+ 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.",
14711
+ permission: "auto",
14712
+ mutating: false,
14713
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
14714
+ inputSchema: {
14715
+ type: "object",
14716
+ properties: {
14717
+ targets: {
14718
+ type: "array",
14719
+ items: { type: "string" },
14720
+ description: "Project-relative (or absolute) source files to mutate. Keep to files changed by the current task."
14721
+ },
14722
+ testCommand: {
14723
+ type: "string",
14724
+ description: 'Exact command that runs the relevant tests, e.g. "pnpm exec vitest run packages/core/tests/coordination/mutation-engine.test.ts".'
14725
+ },
14726
+ cwd: { type: "string", description: "Working directory for the test command." },
14727
+ maxPerFile: {
14728
+ type: "number",
14729
+ minimum: 1,
14730
+ maximum: 25,
14731
+ description: "Mutant cap per file per pass. Default 10."
14732
+ },
14733
+ maxStrengthenAttempts: {
14734
+ type: "number",
14735
+ minimum: 0,
14736
+ maximum: 5,
14737
+ description: "Strengthen\u2192re-verify rounds. Default 2 when repairSubagentId is set, else 0."
14738
+ },
14739
+ repairSubagentId: {
14740
+ type: "string",
14741
+ description: "Subagent that owns the tests. When set and mutants survive, it receives a strengthen-tests task and the survivors are re-verified."
14742
+ },
14743
+ chaosWorktree: {
14744
+ anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
14745
+ description: "Worktree override for the chaos agent. Defaults to the roster policy for chaos-monkey ('off'), because mutation targets are usually freshly written and uncommitted \u2014 a worktree from HEAD would not contain them and every mutant would drift to skipped. Only pass 'auto' or 'required' when the targets are committed."
14746
+ },
14747
+ timeoutMs: { type: "number", minimum: 1, description: "Per-task timeout for chaos/strengthen/rerun tasks." },
14748
+ reportOnly: {
14749
+ type: "boolean",
14750
+ description: "Skip the strengthen loop even when survivors exist. Default false."
14751
+ }
14752
+ },
14753
+ required: ["targets", "testCommand"],
14754
+ additionalProperties: false
14755
+ },
14756
+ async execute(input, ctx) {
14757
+ const i = normalizeMutationTestInput(input);
14758
+ const root = opts.projectRoot ?? ctx.projectRoot;
14759
+ const plan = buildPlan(i, root);
14760
+ if (plan.length === 0) {
14761
+ return {
14762
+ verdict: "inconclusive",
14763
+ passed: false,
14764
+ error: "No mutable sites found in the given targets (after comment/string filtering)."
14765
+ };
14766
+ }
14767
+ const chaosBase = roster?.[CHAOS_ROLE];
14768
+ if (!chaosBase) {
14769
+ return {
14770
+ verdict: "inconclusive",
14771
+ passed: false,
14772
+ error: "chaos-monkey role missing from the roster \u2014 refusing to spawn a saboteur without its prompt/tools contract. Build the toolset with a roster that includes 'chaos-monkey' (FLEET_ROSTER does)."
14773
+ };
14774
+ }
14775
+ const chaosSubagentId = await director.spawn(
14776
+ makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
14777
+ );
14778
+ const chaosTaskId = await director.assign({
14779
+ id: randomUUID11(),
14780
+ subagentId: chaosSubagentId,
14781
+ description: buildChaosTask(plan, i, 1, []),
14782
+ timeoutMs: i.timeoutMs
14783
+ });
14784
+ const [chaosResult] = await director.awaitTasks([chaosTaskId]);
14785
+ const pass1 = collectOutcomes(chaosResult, plan);
14786
+ const survivors = pass1.filter((m) => m.status === "survived");
14787
+ const maxAttempts = clamp(
14788
+ i.maxStrengthenAttempts ?? (i.repairSubagentId && !i.reportOnly ? DEFAULT_MAX_STRENGTHEN_ATTEMPTS : 0),
14789
+ 0,
14790
+ 5
14791
+ );
14792
+ const attempts = [];
14793
+ let current = survivors;
14794
+ let rerunUnknowns = [];
14795
+ while (current.length > 0 && attempts.length < maxAttempts && i.repairSubagentId) {
14796
+ const attemptNo = attempts.length + 1;
14797
+ const strengthenTaskId = await director.assign({
14798
+ id: randomUUID11(),
14799
+ subagentId: i.repairSubagentId,
14800
+ description: buildStrengthenTask(current, i, attemptNo),
14801
+ timeoutMs: i.timeoutMs
14802
+ });
14803
+ const [strengthenResult] = await director.awaitTasks([strengthenTaskId]);
14804
+ if (strengthenResult?.status !== "success") {
14805
+ attempts.push({
14806
+ attempt: attemptNo,
14807
+ survivorsBefore: current,
14808
+ strengthenResult: strengthenResult ? { taskId: strengthenResult.taskId, status: strengthenResult.status } : void 0,
14809
+ survivorsAfter: current,
14810
+ suspectedEquivalent: []
14811
+ });
14812
+ break;
14813
+ }
14814
+ const survivorPlan = plan.filter((p) => current.some((s) => s.id === p.id));
14815
+ const rerunSubagentId = await director.spawn(
14816
+ makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
14817
+ );
14818
+ const rerunTaskId = await director.assign({
14819
+ id: randomUUID11(),
14820
+ subagentId: rerunSubagentId,
14821
+ description: buildChaosTask(survivorPlan, i, attemptNo + 1, current),
14822
+ timeoutMs: i.timeoutMs
14823
+ });
14824
+ const [rerunResult] = await director.awaitTasks([rerunTaskId]);
14825
+ const passN = collectOutcomes(rerunResult, survivorPlan);
14826
+ const stillSurviving = passN.filter((m) => !isKill(m.status));
14827
+ rerunUnknowns = passN.filter((m) => m.status === "skipped");
14828
+ attempts.push({
14829
+ attempt: attemptNo,
14830
+ survivorsBefore: current,
14831
+ strengthenResult: { taskId: strengthenResult.taskId, status: strengthenResult.status },
14832
+ rerunResult: { taskId: rerunTaskId, status: rerunResult?.status ?? "unknown" },
14833
+ survivorsAfter: stillSurviving,
14834
+ suspectedEquivalent: stillSurviving.filter((m) => m.status === "survived" && current.some((c) => c.id === m.id)).map((m) => m.id)
14835
+ });
14836
+ current = stillSurviving;
14837
+ }
14838
+ const finalSurvivors = current.filter((m) => m.status === "survived");
14839
+ const verifiedCount = pass1.filter((m) => m.status !== "skipped").length;
14840
+ const skippedCount = pass1.filter((m) => m.status === "skipped").length;
14841
+ const rerunUnknownCount = rerunUnknowns.length;
14842
+ const score = plan.length === 0 ? 0 : pass1.filter((m) => isKill(m.status)).length / plan.length;
14843
+ const verdict = verifiedCount === 0 ? "inconclusive" : finalSurvivors.length === 0 ? skippedCount > 0 || rerunUnknownCount > 0 ? "partial" : "pass" : score >= 0.8 ? "partial" : "fail";
14844
+ return {
14845
+ verdict,
14846
+ passed: verdict === "pass",
14847
+ mutationScore: Number.parseFloat(score.toFixed(3)),
14848
+ planned: plan.length,
14849
+ killed: pass1.filter((m) => isKill(m.status)).length,
14850
+ // Breakout of `killed`: how many kills were detected by the test
14851
+ // command hanging rather than by a failing assertion. A subset of
14852
+ // `killed`, surfaced so a director can distinguish a hang-heavy
14853
+ // suite (mutants breaking termination, not assertions) from an
14854
+ // assertion-strong one. hangHeavy = killedByHang === killed.
14855
+ killedByHang: pass1.filter((m) => m.status === "killed-by-hang").length,
14856
+ survived: pass1.filter((m) => m.status === "survived").length,
14857
+ skipped: pass1.filter((m) => m.status === "skipped").length,
14858
+ finalSurvivors: finalSurvivors.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
14859
+ suspectedEquivalent: attempts.flatMap((a) => a.suspectedEquivalent),
14860
+ strengthenAttempts: attempts.length,
14861
+ attempts,
14862
+ chaosTaskId,
14863
+ // Unverified leftovers from the strengthen loop: surfaced so the
14864
+ // caller can see WHICH mutants lack kill evidence, and counted by
14865
+ // the verdict gate above.
14866
+ unverifiedFromRerun: rerunUnknowns.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
14867
+ nextAction: finalSurvivors.length === 0 && rerunUnknownCount === 0 && skippedCount === 0 ? "accept" : attempts.length >= maxAttempts && i.repairSubagentId ? "manual_review_survivors" : "strengthen_tests"
14868
+ };
14869
+ }
14870
+ };
14871
+ }
14872
+ function normalizeMutationTestInput(input) {
14873
+ const raw = input ?? {};
14874
+ const targets = stringArray2(raw["targets"]) ?? [];
14875
+ const testCommand = typeof raw["testCommand"] === "string" ? raw["testCommand"].trim() : "";
14876
+ return {
14877
+ targets: targets.filter(Boolean),
14878
+ testCommand,
14879
+ cwd: typeof raw["cwd"] === "string" && raw["cwd"].trim() ? raw["cwd"].trim() : void 0,
14880
+ maxPerFile: typeof raw["maxPerFile"] === "number" ? raw["maxPerFile"] : void 0,
14881
+ maxStrengthenAttempts: typeof raw["maxStrengthenAttempts"] === "number" ? raw["maxStrengthenAttempts"] : void 0,
14882
+ repairSubagentId: typeof raw["repairSubagentId"] === "string" && raw["repairSubagentId"].trim() ? raw["repairSubagentId"].trim() : void 0,
14883
+ chaosWorktree: normalizeWorktreeOverride(raw["chaosWorktree"]),
14884
+ timeoutMs: typeof raw["timeoutMs"] === "number" ? raw["timeoutMs"] : void 0,
14885
+ reportOnly: raw["reportOnly"] === true
14886
+ };
14887
+ }
14888
+ function clamp(n, lo, hi) {
14889
+ return Math.min(hi, Math.max(lo, n));
14890
+ }
14891
+ function buildPlan(i, projectRoot) {
14892
+ const plan = [];
14893
+ for (const target of i.targets) {
14894
+ const abs = isAbsolute3(target) ? target : join16(projectRoot ?? process.cwd(), target);
14895
+ let source;
14896
+ try {
14897
+ source = readFileSync13(abs, "utf8");
14898
+ } catch {
14899
+ continue;
14900
+ }
14901
+ plan.push(...planMutations(target, source, { maxPerFile: i.maxPerFile ?? DEFAULT_MAX_PER_FILE }));
14902
+ }
14903
+ return plan;
14904
+ }
14905
+ function makeChaosConfig(base, worktree) {
14906
+ return { ...instantiateRosterConfig2(CHAOS_ROLE, base), worktree };
14907
+ }
14908
+ function buildChaosTask(plan, i, pass, priorSurvivors) {
14909
+ const mutants = plan.map(
14910
+ (m) => `- ${m.id} | ${m.file}:${m.line}:${m.column} | ${m.kind} | "${m.original}" -> "${m.replacement}"`
14911
+ ).join("\n");
14912
+ const prior = priorSurvivors.length > 0 ? `
14913
+ These mutants survived a previous pass (pass ${pass - 1}) \u2014 re-verify them against the STRENGTHENED tests:
14914
+ ${priorSurvivors.map((s) => `- ${s.id} (${s.kind} @ ${s.file}:${s.line})`).join("\n")}` : "";
14915
+ return [
14916
+ "Execute this deterministic mutation plan against the current checkout.",
14917
+ "",
14918
+ "For each mutant, in order:",
14919
+ "1. Apply ONLY that mutation at its exact (file, line, column).",
14920
+ `2. Run the test command: ${i.testCommand}${i.cwd ? ` (cwd: ${i.cwd})` : ""}`,
14921
+ "3. Record killed (tests failed \u2014 quote first failing assertion), survived (suite green), or killed-by-hang (the test command timed out or was aborted \u2014 the mutation broke the suite by non-termination; record the timeout as evidence, do NOT report it as survived).",
14922
+ "4. Restore the file byte-for-byte before the next mutant.",
14923
+ "",
14924
+ "Mutants:",
14925
+ mutants,
14926
+ prior,
14927
+ "",
14928
+ "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.",
14929
+ "Finish with submit_result, then repeat the same JSON as your final text."
14930
+ ].join("\n");
14931
+ }
14932
+ function buildStrengthenTask(survivors, i, attempt) {
14933
+ const confirmed = survivors.filter((s) => s.status === "survived");
14934
+ const unverified = survivors.filter((s) => s.status === "skipped");
14935
+ const row = (s) => `- ${s.id} | ${s.file}:${s.line} | ${s.kind}${s.evidence ? ` | ${s.evidence}` : ""}`;
14936
+ return [
14937
+ `Strengthen the tests so the mutants below die (attempt ${attempt}).`,
14938
+ "",
14939
+ ...confirmed.length > 0 ? [
14940
+ "CONFIRMED SURVIVORS \u2014 each was a deliberate sabotage of production code that the current suite did NOT catch:",
14941
+ ...confirmed.map(row),
14942
+ ""
14943
+ ] : [],
14944
+ ...unverified.length > 0 ? [
14945
+ "UNVERIFIED \u2014 these mutations were never actually re-tested (the re-verify pass skipped or did not report them). Do NOT assume the suite misses them: first apply each mutation, run the tests, and confirm it really survives; if the tests already fail, report that instead of writing new assertions.",
14946
+ ...unverified.map(row),
14947
+ ""
14948
+ ] : [],
14949
+ `Test command that must fail under each CONFIRMED mutant: ${i.testCommand}`,
14950
+ "",
14951
+ "For each CONFIRMED survivor add or tighten exactly one assertion that pins the sabotaged boundary/behavior. Do not change production code. Do not weaken other tests. Run the suite green on clean code before finishing."
14952
+ ].join("\n");
14953
+ }
14954
+ function collectOutcomes(result, plan) {
14955
+ const fromText = parseTextOutcomes(result);
14956
+ if (fromText.length > 0) {
14957
+ const remaining = [...plan];
14958
+ const matched = [];
14959
+ for (const m of fromText) {
14960
+ const idx = remaining.findIndex((p) => p.id === m.id);
14961
+ if (idx === -1) continue;
14962
+ remaining.splice(idx, 1);
14963
+ matched.push(m);
14964
+ }
14965
+ if (matched.length > 0) {
14966
+ const missing = remaining.map((p) => ({
14967
+ id: p.id,
14968
+ file: p.file,
14969
+ line: p.line,
14970
+ kind: p.kind,
14971
+ status: "skipped",
14972
+ evidence: "not reported by chaos task"
14973
+ }));
14974
+ return [...matched, ...missing];
14975
+ }
14976
+ }
14977
+ return plan.map((p) => ({
14978
+ id: p.id,
14979
+ file: p.file,
14980
+ line: p.line,
14981
+ kind: p.kind,
14982
+ status: "skipped",
14983
+ evidence: result ? `chaos task ended ${result.status}` : "chaos task produced no result"
14984
+ }));
14985
+ }
14986
+ function isKill(status) {
14987
+ return status === "killed" || status === "killed-by-hang";
14988
+ }
14989
+ function parseTextOutcomes(result) {
14990
+ const text = typeof result?.result === "string" ? result.result : void 0;
14991
+ if (!text) return [];
14992
+ const parsed = parseMutationReport(text);
14993
+ if (!parsed) return [];
14994
+ return parsed.mutants.map((m) => ({
14995
+ id: m.id,
14996
+ file: m.file,
14997
+ line: m.line,
14998
+ kind: m.kind,
14999
+ status: m.status,
15000
+ evidence: m.evidence
15001
+ }));
15002
+ }
15003
+
14191
15004
  // src/coordination/director-tools.ts
14192
15005
  function makeSpawnTool(director, roster) {
14193
15006
  const dispatchCatalog = () => {
@@ -14480,7 +15293,7 @@ function makeKanbanQueueTool(director, roster) {
14480
15293
  try {
14481
15294
  const config = buildKanbanSubagentConfig(claim.task, i, roster, instantiateRosterConfig2);
14482
15295
  subagentId = await director.spawn(config);
14483
- const dispatchTaskId = randomUUID11();
15296
+ const dispatchTaskId = randomUUID12();
14484
15297
  const taskSpec = {
14485
15298
  id: dispatchTaskId,
14486
15299
  subagentId,
@@ -14756,6 +15569,7 @@ function buildDirectorToolset(director, roster) {
14756
15569
  makeAskResultTool(director),
14757
15570
  makeRollUpTool(director),
14758
15571
  makeQualityGateTool(director, roster),
15572
+ makeMutationTestTool(director, roster),
14759
15573
  makeTerminateTool(director),
14760
15574
  makeTerminateAllTool(director),
14761
15575
  makeFleetTool(director),
@@ -14842,7 +15656,7 @@ import * as fsp24 from "node:fs/promises";
14842
15656
  import * as path32 from "node:path";
14843
15657
 
14844
15658
  // src/storage/session-store.ts
14845
- import { randomUUID as randomUUID13 } from "node:crypto";
15659
+ import { randomUUID as randomUUID14 } from "node:crypto";
14846
15660
  import * as fsp23 from "node:fs/promises";
14847
15661
  import * as path31 from "node:path";
14848
15662
 
@@ -16566,7 +17380,7 @@ var FileSessionWriter = class _FileSessionWriter {
16566
17380
 
16567
17381
  // src/storage/session-checkpoint-cas.ts
16568
17382
  import { spawn as spawn3 } from "node:child_process";
16569
- import { createHash as createHash3, randomUUID as randomUUID12 } from "node:crypto";
17383
+ import { createHash as createHash3, randomUUID as randomUUID13 } from "node:crypto";
16570
17384
  import * as fsp10 from "node:fs/promises";
16571
17385
  import * as path23 from "node:path";
16572
17386
 
@@ -16824,7 +17638,7 @@ var SessionCheckpointCas = class {
16824
17638
  }
16825
17639
  const temp = path23.join(
16826
17640
  path23.dirname(target),
16827
- `.${path23.basename(target)}.${process.pid}.${randomUUID12()}.tmp`
17641
+ `.${path23.basename(target)}.${process.pid}.${randomUUID13()}.tmp`
16828
17642
  );
16829
17643
  let handle;
16830
17644
  try {
@@ -18620,7 +19434,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
18620
19434
  onAppend;
18621
19435
  onAppendBatch;
18622
19436
  catalogClient;
18623
- maintenanceHolderId = randomUUID13();
19437
+ maintenanceHolderId = randomUUID14();
18624
19438
  _loadCache = /* @__PURE__ */ new Map();
18625
19439
  loadCache = new SessionLoadCache(this._loadCache);
18626
19440
  _indexCache = null;
@@ -20827,7 +21641,7 @@ function hashStr(s) {
20827
21641
  }
20828
21642
 
20829
21643
  // src/coordination/multi-agent-coordinator.ts
20830
- import { randomUUID as randomUUID14 } from "node:crypto";
21644
+ import { randomUUID as randomUUID15 } from "node:crypto";
20831
21645
  import { EventEmitter as EventEmitter2 } from "node:events";
20832
21646
 
20833
21647
  // src/coordination/coordinator/error-classifier.ts
@@ -20918,7 +21732,8 @@ async function executeSubagentWithTimeout({
20918
21732
  budget,
20919
21733
  preemptFraction = TIMEOUT_PREEMPT_FRACTION,
20920
21734
  abortSubagent,
20921
- currentSessionId
21735
+ currentSessionId,
21736
+ gracefulFinish
20922
21737
  }) {
20923
21738
  const initialTimeoutMs = budget.limits.timeoutMs;
20924
21739
  const idleLimitMs = budget.limits.idleTimeoutMs;
@@ -20949,9 +21764,17 @@ async function executeSubagentWithTimeout({
20949
21764
  const scheduleNext = () => {
20950
21765
  const wallLimit = budget.limits.timeoutMs ?? initialTimeoutMs;
20951
21766
  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)));
21767
+ const idleRemaining = idleLimitMs === void 0 || gracefulFinish !== void 0 && initialTimeoutMs !== void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
21768
+ const preemptRemaining = initialTimeoutMs === void 0 || preemptedCeiling === wallLimit || gracefulFinish !== void 0 ? Number.POSITIVE_INFINITY : wallLimit * preemptFraction - (Date.now() - start);
21769
+ const next = Math.min(wallRemaining, idleRemaining, preemptRemaining);
21770
+ if (!Number.isFinite(next)) {
21771
+ if (timer) {
21772
+ clearTimeout(timer);
21773
+ timer = null;
21774
+ }
21775
+ return;
21776
+ }
21777
+ armFor(Math.max(25, next));
20955
21778
  };
20956
21779
  const negotiateTimeout = async (used, limit) => {
20957
21780
  const handler = budget.onThreshold;
@@ -21000,6 +21823,10 @@ async function executeSubagentWithTimeout({
21000
21823
  const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
21001
21824
  const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
21002
21825
  if (idleExceeded && !wallExceeded) {
21826
+ if (gracefulFinish !== void 0 && initialTimeoutMs !== void 0) {
21827
+ scheduleNext();
21828
+ return;
21829
+ }
21003
21830
  const sessionId = currentSessionId();
21004
21831
  budget._events?.emit("budget.threshold_reached", {
21005
21832
  ...sessionId ? { sessionId } : {},
@@ -21016,7 +21843,7 @@ async function executeSubagentWithTimeout({
21016
21843
  reject(new BudgetExceededError("idle_timeout", idleLimit ?? 0, budget.idleMs()));
21017
21844
  return;
21018
21845
  }
21019
- if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptState === "active" /* ACTIVE */ && elapsed >= wallLimit * preemptFraction) {
21846
+ if (wallLimit !== void 0 && !wallExceeded && gracefulFinish === void 0 && budget.onThreshold && preemptState === "active" /* ACTIVE */ && elapsed >= wallLimit * preemptFraction) {
21020
21847
  const activityTs = Date.now() - budget.idleMs();
21021
21848
  if (activityTs <= lastGrantActivityTs) {
21022
21849
  preemptState = "locked" /* LOCKED */;
@@ -21050,6 +21877,22 @@ async function executeSubagentWithTimeout({
21050
21877
  return;
21051
21878
  }
21052
21879
  const limit = wallLimit ?? 0;
21880
+ if (gracefulFinish !== void 0) {
21881
+ if (!budget.graceGranted) {
21882
+ const reason = `wall-clock budget of ${Math.round(limit / 1e3)}s reached`;
21883
+ if (budget.notifyFinish(reason, { graceMs: gracefulFinish.graceMs })) {
21884
+ scheduleNext();
21885
+ return;
21886
+ }
21887
+ abortSubagent(ctx.subagentId);
21888
+ reject(new BudgetExceededError("timeout", limit, elapsed));
21889
+ return;
21890
+ } else {
21891
+ abortSubagent(ctx.subagentId);
21892
+ reject(new BudgetExceededError("timeout", limit, elapsed));
21893
+ return;
21894
+ }
21895
+ }
21053
21896
  if (!budget.onThreshold) {
21054
21897
  abortSubagent(ctx.subagentId);
21055
21898
  reject(new BudgetExceededError("timeout", limit, elapsed));
@@ -21197,7 +22040,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
21197
22040
  return { ...subagent, name: display };
21198
22041
  }
21199
22042
  async spawn(subagent) {
21200
- const id = subagent.id || randomUUID14();
22043
+ const id = subagent.id || randomUUID15();
21201
22044
  const cfg = this.withNickname(subagent, id);
21202
22045
  if (this.subagents.has(id)) {
21203
22046
  throw new Error(`Subagent id "${id}" already exists \u2014 refusing to overwrite`);
@@ -21434,6 +22277,32 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
21434
22277
  completeTask(result) {
21435
22278
  this.recordCompletion(result);
21436
22279
  }
22280
+ /**
22281
+ * Ask every RUNNING subagent that opted into `gracefulFinish` to finish its
22282
+ * task in its own turn (see coordination/subagent-finish.ts). This is the
22283
+ * leader-side entry point for "the leader agent has finished": it delivers
22284
+ * an in-band notification between tool batches — never an interrupt, never
22285
+ * an abort. Each notified subagent keeps its existing time budget and
22286
+ * accelerates; the watchdog still bounds the maximum lifetime.
22287
+ *
22288
+ * Subagents without the policy opted in are deliberately untouched — their
22289
+ * lifecycle remains the legacy watchdog contract.
22290
+ *
22291
+ * Returns the number of subagents actually notified.
22292
+ */
22293
+ requestFinish(reason) {
22294
+ let notified = 0;
22295
+ for (const subagent of this.subagents.values()) {
22296
+ if (subagent.status !== "running") continue;
22297
+ if (!resolveGracefulFinish(subagent.config)) continue;
22298
+ const budget = subagent.activeBudget;
22299
+ if (!budget) continue;
22300
+ const usage = budget.usage();
22301
+ if (usage.iterations === 0 && usage.toolCalls === 0) continue;
22302
+ if (budget.notifyFinish(reason)) notified++;
22303
+ }
22304
+ return notified;
22305
+ }
21437
22306
  // --- internal dispatching ---------------------------------------------
21438
22307
  tryDispatchNext() {
21439
22308
  while (this.canDispatch()) {
@@ -21607,7 +22476,14 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
21607
22476
  idleTimeoutMs: rawIdleTimeoutMs ?? this.config.defaultBudget?.idleTimeoutMs ?? configWithRosterDefaults.idleTimeoutMs
21608
22477
  },
21609
22478
  "auto",
21610
- { sessionId: () => this.currentSessionId() }
22479
+ {
22480
+ sessionId: () => this.currentSessionId(),
22481
+ subagentId,
22482
+ // Graceful-finish runs own wall-clock enforcement to the watchdog so
22483
+ // the notify-then-bound lifecycle cannot be raced by tool.progress
22484
+ // heartbeats calling checkTimeout() (see subagent-budget.ts).
22485
+ ...resolveGracefulFinish(subagent.config) ? { wallClockWatchdogOwned: true } : {}
22486
+ }
21611
22487
  );
21612
22488
  subagent.activeBudget = budget;
21613
22489
  if (!this.runner) {
@@ -21640,7 +22516,8 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
21640
22516
  task,
21641
22517
  runCtx,
21642
22518
  budget,
21643
- subagent.config.preemptFraction
22519
+ subagent.config.preemptFraction,
22520
+ resolveGracefulFinish(subagent.config)
21644
22521
  );
21645
22522
  result = {
21646
22523
  subagentId,
@@ -21670,13 +22547,14 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
21670
22547
  }
21671
22548
  this.recordCompletion(result);
21672
22549
  }
21673
- async executeWithTimeout(runner, task, ctx, budget, preemptFraction) {
22550
+ async executeWithTimeout(runner, task, ctx, budget, preemptFraction, gracefulFinish) {
21674
22551
  return executeSubagentWithTimeout({
21675
22552
  runner,
21676
22553
  task,
21677
22554
  ctx,
21678
22555
  budget,
21679
22556
  preemptFraction,
22557
+ gracefulFinish,
21680
22558
  abortSubagent: (subagentId) => this.subagents.get(subagentId)?.abortController.abort(),
21681
22559
  currentSessionId: () => this.currentSessionId()
21682
22560
  });
@@ -21985,6 +22863,7 @@ function worktreeOwnerLabel(task, config) {
21985
22863
  }
21986
22864
 
21987
22865
  // src/coordination/director.ts
22866
+ var BUSY_REARM_FLOOR_MS = 1e3;
21988
22867
  var Director = class _Director {
21989
22868
  /* eslint-disable-next-line @typescript-eslint/no-unused-vars — just a cast helper */
21990
22869
  static _asManifestEntry(v) {
@@ -22050,6 +22929,13 @@ var Director = class _Director {
22050
22929
  subagentIdleTimeoutMs;
22051
22930
  retireSubagentOnTaskComplete;
22052
22931
  subagentIdleTimers = /* @__PURE__ */ new Map();
22932
+ /**
22933
+ * Effective idle window per subagent (spawn-time `idleTimeoutMs` override
22934
+ * or the Director-wide default; undefined = no window). Internal-task
22935
+ * completion re-arms with THIS value, not the Director-wide default, so
22936
+ * a subagent-configured window survives its first internal probe.
22937
+ */
22938
+ subagentIdleDelayMs = /* @__PURE__ */ new Map();
22053
22939
  sharedScratchpadPath;
22054
22940
  maxSpawns;
22055
22941
  maxSpawnDepth;
@@ -22082,7 +22968,7 @@ var Director = class _Director {
22082
22968
  sessionProvider;
22083
22969
  sessionModel;
22084
22970
  constructor(opts) {
22085
- this.id = opts.config.coordinatorId || randomUUID15();
22971
+ this.id = opts.config.coordinatorId || randomUUID16();
22086
22972
  this.manifestPath = opts.manifestPath;
22087
22973
  this.roster = opts.roster;
22088
22974
  this.directorPreamble = opts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
@@ -22206,7 +23092,13 @@ var Director = class _Director {
22206
23092
  handleTaskCompleted(payload) {
22207
23093
  const r = payload.result;
22208
23094
  const settled = this.tasks.settle(r);
22209
- if (settled.internal) return;
23095
+ if (settled.internal) {
23096
+ this.armSubagentIdleRetirement(
23097
+ r.subagentId,
23098
+ this.subagentIdleDelayMs.get(r.subagentId) ?? this.subagentIdleTimeoutMs
23099
+ );
23100
+ return;
23101
+ }
22210
23102
  const title = this.tasks.descriptionFor(r.taskId, payload.task.description ?? r.taskId);
22211
23103
  if (!settled.consumedInBand && this.taskResultNotifier) {
22212
23104
  const resultText = typeof r.result === "string" ? r.result : r.result !== void 0 ? safeStringify(r.result) : void 0;
@@ -22271,7 +23163,7 @@ var Director = class _Director {
22271
23163
  }
22272
23164
  this.armSubagentIdleRetirement(
22273
23165
  r.subagentId,
22274
- this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleTimeoutMs
23166
+ this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleDelayMs.get(r.subagentId) ?? this.subagentIdleTimeoutMs
22275
23167
  );
22276
23168
  }
22277
23169
  extensionsFor(subagentId) {
@@ -22289,6 +23181,17 @@ var Director = class _Director {
22289
23181
  isWorkComplete() {
22290
23182
  return this.workCompleteFlag;
22291
23183
  }
23184
+ /**
23185
+ * Ask every running background subagent that opted into `gracefulFinish`
23186
+ * to finish its task in its own turn. In-band notification between tool
23187
+ * batches — no interrupt, no abort; each subagent keeps its time budget and
23188
+ * accelerates. Session shutdown calls this before draining Chimera work so
23189
+ * the post-session reviewer is nudged to complete rather than killed.
23190
+ * Returns the number of subagents notified.
23191
+ */
23192
+ requestFinish(reason) {
23193
+ return this.coordinator.requestFinish(reason);
23194
+ }
22292
23195
  setLeaderBtwNote(note) {
22293
23196
  return this.btwNotes.add(note);
22294
23197
  }
@@ -22346,6 +23249,7 @@ var Director = class _Director {
22346
23249
  this.resolveSpawnModel(config);
22347
23250
  const subagentId = await spawn4(this, config, priceLookup);
22348
23251
  const perSubagentIdleMs = typeof config.idleTimeoutMs === "number" && Number.isFinite(config.idleTimeoutMs) && config.idleTimeoutMs >= 0 ? config.idleTimeoutMs : this.subagentIdleTimeoutMs;
23252
+ this.subagentIdleDelayMs.set(subagentId, perSubagentIdleMs);
22349
23253
  this.armSubagentIdleRetirement(subagentId, perSubagentIdleMs);
22350
23254
  return subagentId;
22351
23255
  }
@@ -22365,7 +23269,7 @@ var Director = class _Director {
22365
23269
  );
22366
23270
  }
22367
23271
  const msg = {
22368
- id: randomUUID15(),
23272
+ id: randomUUID16(),
22369
23273
  type: "task",
22370
23274
  from: this.id,
22371
23275
  to: subagentId,
@@ -22399,6 +23303,7 @@ var Director = class _Director {
22399
23303
  this.budgetPolicy.dispose();
22400
23304
  for (const timer of this.subagentIdleTimers.values()) clearTimeout(timer);
22401
23305
  this.subagentIdleTimers.clear();
23306
+ this.subagentIdleDelayMs.clear();
22402
23307
  await this.coordinator.stopAll();
22403
23308
  this.tasks.resolveWaitersOnShutdown();
22404
23309
  for (const b of this.subagentBridges.values()) {
@@ -22457,6 +23362,7 @@ var Director = class _Director {
22457
23362
  }
22458
23363
  async remove(subagentId) {
22459
23364
  this.clearSubagentIdleRetirement(subagentId);
23365
+ this.subagentIdleDelayMs.delete(subagentId);
22460
23366
  void this.appendSessionEvent({
22461
23367
  type: "agent_stopped",
22462
23368
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -22509,9 +23415,13 @@ var Director = class _Director {
22509
23415
  const timer = setTimeout(() => {
22510
23416
  this.subagentIdleTimers.delete(subagentId);
22511
23417
  const entry = this.coordinator.getStatus().subagents.find((a) => a.id === subagentId);
22512
- if (entry?.status !== "idle") return;
23418
+ if (entry === void 0) return;
23419
+ if (entry.status !== "idle") {
23420
+ this.armSubagentIdleRetirement(subagentId, Math.max(delayMs, BUSY_REARM_FLOOR_MS));
23421
+ return;
23422
+ }
22513
23423
  if (this.coordinator.listPendingTasks().some((task) => task.subagentId === subagentId)) {
22514
- this.armSubagentIdleRetirement(subagentId, this.subagentIdleTimeoutMs);
23424
+ this.armSubagentIdleRetirement(subagentId, Math.max(delayMs, BUSY_REARM_FLOOR_MS));
22515
23425
  return;
22516
23426
  }
22517
23427
  void this.remove(subagentId).catch(
@@ -22619,7 +23529,7 @@ var Director = class _Director {
22619
23529
  };
22620
23530
 
22621
23531
  // src/coordination/fleet-manager.ts
22622
- import { randomUUID as randomUUID16 } from "node:crypto";
23532
+ import { randomUUID as randomUUID17 } from "node:crypto";
22623
23533
  import * as fsp26 from "node:fs/promises";
22624
23534
  import * as path33 from "node:path";
22625
23535
  var FleetManager = class {
@@ -22685,7 +23595,7 @@ var FleetManager = class {
22685
23595
  maxContext;
22686
23596
  constructor(opts = {}) {
22687
23597
  this.manifestPath = opts.manifestPath;
22688
- this.directorRunId = opts.directorRunId ?? randomUUID16();
23598
+ this.directorRunId = opts.directorRunId ?? randomUUID17();
22689
23599
  this.maxSpawns = opts.maxSpawns ?? Number.POSITIVE_INFINITY;
22690
23600
  this.maxSpawnDepth = resolveMaxSpawnDepth(opts.maxSpawnDepth);
22691
23601
  this.spawnDepth = opts.spawnDepth ?? 0;
@@ -24490,7 +25400,7 @@ function makeFleetStatusTool(opts = {}) {
24490
25400
  }
24491
25401
 
24492
25402
  // src/coordination/fleet-supervisor.ts
24493
- import { randomUUID as randomUUID17 } from "node:crypto";
25403
+ import { randomUUID as randomUUID18 } from "node:crypto";
24494
25404
  var COLLAB_ID_PREFIXES2 = ["bug-hunter-", "refactor-planner-", "critic-"];
24495
25405
  var DEFAULTS = {
24496
25406
  intervalMs: 2e4,
@@ -24768,7 +25678,7 @@ var FleetSupervisor = class {
24768
25678
  */
24769
25679
  async decide(question, context, options, risk) {
24770
25680
  const request = {
24771
- id: `fleetsup-${randomUUID17()}`,
25681
+ id: `fleetsup-${randomUUID18()}`,
24772
25682
  sessionId: this.opts.sessionId?.(),
24773
25683
  source: "system",
24774
25684
  question,
@@ -25128,6 +26038,22 @@ var SEND_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
25128
26038
  // receiver trusts the sender-asserted `sessionId`; the boundary must
25129
26039
  // refuse the field entirely.
25130
26040
  ]);
26041
+ var SEND_FORBIDDEN_FIELDS = /* @__PURE__ */ new Set([
26042
+ "from",
26043
+ "sessionAffinity"
26044
+ ]);
26045
+ function filterMailboxSendPayload(input) {
26046
+ const payload = {};
26047
+ const stripped = [];
26048
+ for (const key of Object.keys(input)) {
26049
+ if (SEND_ALLOWED_FIELDS.has(key) || SEND_FORBIDDEN_FIELDS.has(key)) {
26050
+ payload[key] = input[key];
26051
+ } else {
26052
+ stripped.push(key);
26053
+ }
26054
+ }
26055
+ return { payload, stripped };
26056
+ }
25131
26057
  var ACK_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
25132
26058
  "messageId",
25133
26059
  "read",
@@ -25365,7 +26291,9 @@ function makeMailSendTool(opts = {}) {
25365
26291
  required: ["to", "subject", "body"]
25366
26292
  },
25367
26293
  async execute(input, ctx) {
25368
- const i = input ?? {};
26294
+ const { payload: i, stripped } = filterMailboxSendPayload(
26295
+ input ?? {}
26296
+ );
25369
26297
  const rawTo = i.to;
25370
26298
  const subject = i.subject;
25371
26299
  const body = i.body;
@@ -25388,15 +26316,13 @@ function makeMailSendTool(opts = {}) {
25388
26316
  recipientAliases: /* @__PURE__ */ new Set([codecIdentity.baseId]),
25389
26317
  sessionId: codecIdentity.sessionId
25390
26318
  };
26319
+ let parsed;
25391
26320
  try {
25392
- parseMailboxSendInput(i, codecActor);
26321
+ parsed = parseMailboxSendInput(i, codecActor);
25393
26322
  } catch (err) {
25394
26323
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
25395
26324
  }
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
- }
26325
+ const audience = parsed.audience;
25400
26326
  const mb = resolveMailbox(ctx);
25401
26327
  const identity = await register(mb, ctx);
25402
26328
  const requestedTo = normalizeRecipient(rawTo, identity.sessionId);
@@ -25410,10 +26336,10 @@ function makeMailSendTool(opts = {}) {
25410
26336
  to: delivery.to,
25411
26337
  type: resolvedType,
25412
26338
  audience: delivery.audience,
25413
- subject,
25414
- body,
25415
- priority: i.priority ?? "normal",
25416
- replyTo: i.replyTo,
26339
+ subject: parsed.subject,
26340
+ body: parsed.body,
26341
+ priority: parsed.priority,
26342
+ replyTo: parsed.replyTo,
25417
26343
  senderSessionId: identity.sessionId
25418
26344
  });
25419
26345
  return {
@@ -25421,7 +26347,9 @@ function makeMailSendTool(opts = {}) {
25421
26347
  messageId: msg.id,
25422
26348
  from: identity.callerId,
25423
26349
  to: msg.to,
25424
- summary: `Mail sent to ${msg.to === "*" ? "all agents" : msg.to} as ${identity.callerId}.`
26350
+ // Surfacing what was stripped keeps the send auditable without
26351
+ // re-introducing the clutter into the payload itself.
26352
+ ...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
26353
  };
25426
26354
  }
25427
26355
  };
@@ -28943,7 +29871,7 @@ function createAgentMonitorService(opts) {
28943
29871
  }
28944
29872
 
28945
29873
  // src/coordination/autonomous-brain.ts
28946
- import { randomUUID as randomUUID18 } from "node:crypto";
29874
+ import { randomUUID as randomUUID19 } from "node:crypto";
28947
29875
  var AutonomousBrain = class {
28948
29876
  graph;
28949
29877
  // Fleet bus for emitting decisions — null-safe, no-op if not provided
@@ -29049,7 +29977,7 @@ var AutonomousBrain = class {
29049
29977
  consequence: i === 0 ? `Spawn the most appropriate agent for: ${taskDescription.slice(0, 80)}` : `Spawn an alternative agent for the same task`
29050
29978
  }));
29051
29979
  return this.decideAuto({
29052
- id: randomUUID18(),
29980
+ id: randomUUID19(),
29053
29981
  source,
29054
29982
  decisionType: "spawn",
29055
29983
  question: `Should we spawn a subagent for this task?`,
@@ -29092,7 +30020,7 @@ var AutonomousBrain = class {
29092
30020
  }
29093
30021
  ];
29094
30022
  return this.decideAuto({
29095
- id: randomUUID18(),
30023
+ id: randomUUID19(),
29096
30024
  source,
29097
30025
  decisionType: "approve_change",
29098
30026
  question: `Should we approve the change "${change.title}"?`,
@@ -29151,7 +30079,7 @@ var AutonomousBrain = class {
29151
30079
  consequence: "Break the task into smaller sub-tasks"
29152
30080
  });
29153
30081
  return this.decideAuto({
29154
- id: randomUUID18(),
30082
+ id: randomUUID19(),
29155
30083
  source,
29156
30084
  decisionType: "escalate_task",
29157
30085
  question: `Task failed: ${error.slice(0, 100)}. How should we proceed?`,
@@ -29285,10 +30213,10 @@ ${ctx.error}`);
29285
30213
  };
29286
30214
 
29287
30215
  // src/coordination/autonomous-coordinator.ts
29288
- import { randomUUID as randomUUID21 } from "node:crypto";
30216
+ import { randomUUID as randomUUID22 } from "node:crypto";
29289
30217
 
29290
30218
  // src/coordination/knowledge-graph.ts
29291
- import { randomUUID as randomUUID19 } from "node:crypto";
30219
+ import { randomUUID as randomUUID20 } from "node:crypto";
29292
30220
  import * as fsp28 from "node:fs/promises";
29293
30221
  import * as path41 from "node:path";
29294
30222
  var DEFAULT_MAX_NODES = 2e3;
@@ -29336,7 +30264,7 @@ var KnowledgeGraph = class _KnowledgeGraph {
29336
30264
  * Returns the node with its assigned id.
29337
30265
  */
29338
30266
  async add(node) {
29339
- const full = { id: randomUUID19(), ...node };
30267
+ const full = { id: randomUUID20(), ...node };
29340
30268
  this.nodes.set(full.id, full);
29341
30269
  this._trackSeq(full.id);
29342
30270
  this._addToIndex(full, this._indexKeys(full));
@@ -29465,8 +30393,8 @@ var KnowledgeGraph = class _KnowledgeGraph {
29465
30393
  if (this.subs.size >= MAX_SUBSCRIPTIONS) {
29466
30394
  throw new Error(`Knowledge graph subscription limit reached (${MAX_SUBSCRIPTIONS})`);
29467
30395
  }
29468
- const channel = randomUUID19();
29469
- const sub = { id: randomUUID19(), agentId, filter, channel };
30396
+ const channel = randomUUID20();
30397
+ const sub = { id: randomUUID20(), agentId, filter, channel };
29470
30398
  this.subs.set(channel, sub);
29471
30399
  this.pendingDeliveries.set(channel, []);
29472
30400
  return channel;
@@ -29947,7 +30875,7 @@ var TaskDAG = class {
29947
30875
  };
29948
30876
 
29949
30877
  // src/coordination/task-auctioneer.ts
29950
- import { randomUUID as randomUUID20 } from "node:crypto";
30878
+ import { randomUUID as randomUUID21 } from "node:crypto";
29951
30879
  function isTerminalGoalStatus(status) {
29952
30880
  return status === "done" || status === "failed";
29953
30881
  }
@@ -30070,7 +30998,7 @@ var TaskAuctioneer = class {
30070
30998
  const score = dispatchResult.confidence * (dispatchResult.role === agent.agentRole ? 1.2 : 1);
30071
30999
  if (score < this.minConfidence) return false;
30072
31000
  const bid = {
30073
- id: randomUUID20(),
31001
+ id: randomUUID21(),
30074
31002
  taskId,
30075
31003
  agentId: agent.agentId,
30076
31004
  agentName: agent.agentName,
@@ -31007,7 +31935,7 @@ var AutonomousCoordinator = class _AutonomousCoordinator {
31007
31935
  break;
31008
31936
  }
31009
31937
  const decision = await this.brain.decideAuto({
31010
- id: randomUUID21(),
31938
+ id: randomUUID22(),
31011
31939
  source: "system",
31012
31940
  decisionType: "prioritize_goals",
31013
31941
  question: `What should we work on next? Open goals: ${dispatchable.map((g) => g.title).join(", ")}`,
@@ -31903,6 +32831,7 @@ export {
31903
32831
  makeMailInboxTool,
31904
32832
  makeMailSendTool,
31905
32833
  makeMailboxTool,
32834
+ makeMutationTestTool,
31906
32835
  makeQualityGateTool,
31907
32836
  makeRollUpTool,
31908
32837
  makeSpawnTool,