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