@wrongstack/core 0.260.0 → 0.265.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 (99) hide show
  1. package/dist/{agent-bridge-BbskZ7HH.d.ts → agent-bridge-DrkBxszZ.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-BNIGZx18.d.ts → agent-subagent-runner-DM2pP-B6.d.ts} +116 -12
  3. package/dist/{brain-C2yDd7Lw.d.ts → brain-BXd_61kQ.d.ts} +32 -3
  4. package/dist/{compactor-t0R_AIt_.d.ts → compactor-B8pOf45Y.d.ts} +1 -1
  5. package/dist/{config-FG6As4H5.d.ts → config-BMCj_XDs.d.ts} +86 -12
  6. package/dist/{context-JFOVvu6z.d.ts → context-MRk5PhNv.d.ts} +26 -1
  7. package/dist/coordination/index.d.ts +1737 -15
  8. package/dist/coordination/index.js +3152 -494
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/{default-config-CXsDvOmP.d.ts → default-config-B0cj-Hry.d.ts} +11 -1
  11. package/dist/defaults/index.d.ts +28 -28
  12. package/dist/defaults/index.js +1804 -1363
  13. package/dist/defaults/index.js.map +1 -1
  14. package/dist/dispatcher-types.d-BBeXBQgS.d.ts +66 -0
  15. package/dist/execution/index.d.ts +16 -16
  16. package/dist/execution/index.js +933 -672
  17. package/dist/execution/index.js.map +1 -1
  18. package/dist/execution/prompt-enhancer.d.ts +1 -1
  19. package/dist/execution/prompt-enhancer.js +7 -1
  20. package/dist/execution/prompt-enhancer.js.map +1 -1
  21. package/dist/extension/index.d.ts +6 -6
  22. package/dist/extension/index.js.map +1 -1
  23. package/dist/{goal-preamble-B1IXJtLX.d.ts → goal-preamble-DvHDSKSe.d.ts} +26 -10
  24. package/dist/{goal-store-CPXz6Mml.d.ts → goal-store-DtLMySNb.d.ts} +1 -1
  25. package/dist/{index-CebbJB94.d.ts → index-B-ch8K9C.d.ts} +8 -8
  26. package/dist/{index-BPcg4N3M.d.ts → index-CEDeNodM.d.ts} +5 -5
  27. package/dist/index.d.ts +189 -104
  28. package/dist/index.js +24693 -21162
  29. package/dist/index.js.map +1 -1
  30. package/dist/infrastructure/index.d.ts +6 -6
  31. package/dist/infrastructure/index.js +12 -8
  32. package/dist/infrastructure/index.js.map +1 -1
  33. package/dist/kernel/index.d.ts +9 -9
  34. package/dist/kernel/index.js +7 -2
  35. package/dist/kernel/index.js.map +1 -1
  36. package/dist/{llm-selector-DXxI2tlu.d.ts → llm-selector-C0tfTCUe.d.ts} +14 -2
  37. package/dist/{mcp-servers-OwNHo43-.d.ts → mcp-servers-2x4w6Jn9.d.ts} +3 -3
  38. package/dist/models/index.d.ts +5 -5
  39. package/dist/models/index.js +80 -31
  40. package/dist/models/index.js.map +1 -1
  41. package/dist/{models-registry-Djlmq4uB.d.ts → models-registry-DmJlKuNp.d.ts} +1 -1
  42. package/dist/{multi-agent-coordinator-CEmrSCMJ.d.ts → multi-agent-coordinator-DyCkCZnU.d.ts} +2 -2
  43. package/dist/{null-fleet-bus-DT92xqgJ.d.ts → null-fleet-bus-CG9QY2aP.d.ts} +6 -6
  44. package/dist/observability/index.d.ts +2 -2
  45. package/dist/observability/index.js +8 -3
  46. package/dist/observability/index.js.map +1 -1
  47. package/dist/{parallel-eternal-engine-0SItuq5r.d.ts → parallel-eternal-engine-Jw9uhEoT.d.ts} +9 -9
  48. package/dist/{path-resolver-DKBh6Jlo.d.ts → path-resolver-Dy2ej-gE.d.ts} +3 -3
  49. package/dist/{permission-BJ7eO9Vl.d.ts → permission-B9SB45lp.d.ts} +1 -1
  50. package/dist/{permission-policy-DEXOfnpm.d.ts → permission-policy-CkjSXabK.d.ts} +2 -2
  51. package/dist/{pipeline-zflkI2dp.d.ts → pipeline-DPDxH_7m.d.ts} +59 -4
  52. package/dist/{plan-templates-BFXyRkEK.d.ts → plan-templates-CzD9GnAU.d.ts} +32 -8
  53. package/dist/{provider-runner-BC-uywtT.d.ts → provider-runner-DMa70ODu.d.ts} +3 -3
  54. package/dist/{retry-policy-Cavrzmtk.d.ts → retry-policy-CN0khdlj.d.ts} +1 -1
  55. package/dist/sdd/index.d.ts +8 -8
  56. package/dist/sdd/index.js +313 -122
  57. package/dist/sdd/index.js.map +1 -1
  58. package/dist/{secret-vault-CDvDYXWX.d.ts → secret-vault-B2yw84VT.d.ts} +43 -4
  59. package/dist/secret-vault-BAKpgFw_.d.ts +57 -0
  60. package/dist/security/index.d.ts +5 -5
  61. package/dist/security/index.js +411 -225
  62. package/dist/security/index.js.map +1 -1
  63. package/dist/{selector-B7AivHsu.d.ts → selector-CzHh_igB.d.ts} +1 -1
  64. package/dist/{session-event-bridge-BmIDxdJd.d.ts → session-event-bridge-BUI6Jf-4.d.ts} +8 -2
  65. package/dist/{session-reader-DtofsB-2.d.ts → session-reader-CMgdMSRP.d.ts} +1 -1
  66. package/dist/skills/index.js +67 -64
  67. package/dist/skills/index.js.map +1 -1
  68. package/dist/storage/index.d.ts +132 -16
  69. package/dist/storage/index.js +851 -432
  70. package/dist/storage/index.js.map +1 -1
  71. package/dist/tools/index.d.ts +57 -0
  72. package/dist/tools/index.js +411 -0
  73. package/dist/tools/index.js.map +1 -0
  74. package/dist/types/index.d.ts +21 -21
  75. package/dist/types/index.js +928 -711
  76. package/dist/types/index.js.map +1 -1
  77. package/dist/utils/error.d.ts +7 -0
  78. package/dist/utils/error.js +8 -0
  79. package/dist/utils/error.js.map +1 -0
  80. package/dist/utils/index.d.ts +8 -68
  81. package/dist/utils/index.js +20 -10
  82. package/dist/utils/index.js.map +1 -1
  83. package/dist/{wstack-paths-CJjEwPXn.d.ts → wstack-paths-hOpNLmvf.d.ts} +2 -0
  84. package/package.json +5 -1
  85. package/skills/api-design/SKILL.md +1 -1
  86. package/skills/audit-log/SKILL.md +6 -6
  87. package/skills/bug-hunter/SKILL.md +5 -5
  88. package/skills/chimera/SKILL.md +4 -4
  89. package/skills/docker-deploy/SKILL.md +1 -1
  90. package/skills/git-flow/SKILL.md +3 -3
  91. package/skills/multi-agent/SKILL.md +3 -3
  92. package/skills/node-modern/SKILL.md +1 -0
  93. package/skills/observability/SKILL.md +2 -2
  94. package/skills/output-standards/SKILL.md +51 -28
  95. package/skills/refactor-planner/SKILL.md +3 -3
  96. package/skills/security-scanner/SKILL.md +4 -3
  97. package/skills/tech-stack/SKILL.md +1 -2
  98. package/dist/package-outdated-watcher-C70ag2G9.d.ts +0 -581
  99. package/dist/secret-vault-BJDY28ev.d.ts +0 -25
@@ -1,6 +1,6 @@
1
1
  import { randomUUID, createHash, randomBytes } from 'crypto';
2
2
  import * as fsp6 from 'fs/promises';
3
- import * as path4 from 'path';
3
+ import * as path5 from 'path';
4
4
  import { isAbsolute, resolve } from 'path';
5
5
  import * as os from 'os';
6
6
  import { hostname } from 'os';
@@ -152,9 +152,9 @@ function formatHumanPrompt(request) {
152
152
  return lines.join("\n");
153
153
  }
154
154
  async function atomicWrite(targetPath, content, opts = {}) {
155
- const dir = path4.dirname(targetPath);
155
+ const dir = path5.dirname(targetPath);
156
156
  await fsp6.mkdir(dir, { recursive: true });
157
- const tmp = path4.join(dir, `.${path4.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
157
+ const tmp = path5.join(dir, `.${path5.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
158
158
  try {
159
159
  if (typeof content === "string") {
160
160
  await fsp6.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
@@ -172,8 +172,8 @@ async function atomicWrite(targetPath, content, opts = {}) {
172
172
  }
173
173
  let mode;
174
174
  try {
175
- const stat5 = await fsp6.stat(targetPath);
176
- mode = stat5.mode & 511;
175
+ const stat6 = await fsp6.stat(targetPath);
176
+ mode = stat6.mode & 511;
177
177
  } catch {
178
178
  mode = opts.mode;
179
179
  }
@@ -193,9 +193,9 @@ async function ensureDir(dir) {
193
193
  await fsp6.mkdir(dir, { recursive: true });
194
194
  }
195
195
  async function withFileLock(targetPath, fn, opts = {}) {
196
- const dir = path4.dirname(targetPath);
196
+ const dir = path5.dirname(targetPath);
197
197
  await fsp6.mkdir(dir, { recursive: true });
198
- const lockPath = path4.join(dir, `.${path4.basename(targetPath)}.lock`);
198
+ const lockPath = path5.join(dir, `.${path5.basename(targetPath)}.lock`);
199
199
  const timeoutMs = opts.timeoutMs ?? 5e3;
200
200
  const staleMs = opts.staleMs ?? 3e4;
201
201
  const started = Date.now();
@@ -206,10 +206,15 @@ async function withFileLock(targetPath, fn, opts = {}) {
206
206
  await handle.writeFile(`${process.pid}:${Date.now()}`);
207
207
  break;
208
208
  } catch (err) {
209
- if (err.code !== "EEXIST") throw err;
209
+ const code = err.code;
210
+ if (code === "ENOENT") {
211
+ await fsp6.mkdir(dir, { recursive: true });
212
+ continue;
213
+ }
214
+ if (code !== "EEXIST") throw err;
210
215
  try {
211
- const stat5 = await fsp6.stat(lockPath);
212
- if (Date.now() - stat5.mtimeMs > staleMs) {
216
+ const stat6 = await fsp6.stat(lockPath);
217
+ if (Date.now() - stat6.mtimeMs > staleMs) {
213
218
  await fsp6.unlink(lockPath);
214
219
  continue;
215
220
  }
@@ -259,6 +264,11 @@ async function renameWithRetry(from, to) {
259
264
  throw lastErr;
260
265
  }
261
266
 
267
+ // src/utils/error.ts
268
+ function toErrorMessage(err) {
269
+ return err instanceof Error ? err.message : String(err);
270
+ }
271
+
262
272
  // src/storage/director-state.ts
263
273
  async function acquireDirectorStateLock(lockPath, processId = process.pid) {
264
274
  let existing;
@@ -412,7 +422,7 @@ var DirectorStateCheckpoint = class {
412
422
  } catch (err) {
413
423
  console.warn(
414
424
  "[director-state] checkpoint write failed:",
415
- err instanceof Error ? err.message : String(err)
425
+ toErrorMessage(err)
416
426
  );
417
427
  } finally {
418
428
  this.writing = false;
@@ -434,11 +444,246 @@ function safeParse(input, maxBytes = 5e6) {
434
444
  } catch (err) {
435
445
  return {
436
446
  ok: false,
437
- error: err instanceof Error ? err.message : String(err)
447
+ error: toErrorMessage(err)
438
448
  };
439
449
  }
440
450
  }
441
451
 
452
+ // src/utils/string.ts
453
+ function truncate(s, max) {
454
+ return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
455
+ }
456
+
457
+ // src/utils/expect-defined.ts
458
+ function expectDefined(value, label) {
459
+ if (value === null || value === void 0) {
460
+ const err = new Error("Expected value to be defined");
461
+ err.name = "ExpectDefinedError";
462
+ throw err;
463
+ }
464
+ return value;
465
+ }
466
+ function projectSlug(absRoot) {
467
+ const base = slugify(path5.basename(absRoot));
468
+ const hash = createHash("sha256").update(path5.resolve(absRoot)).digest("hex").slice(0, 6);
469
+ return `${base}-${hash}`;
470
+ }
471
+ function slugify(name) {
472
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
473
+ }
474
+ function wstackGlobalRoot() {
475
+ const fromEnv = process.env["WRONGSTACK_HOME"];
476
+ if (fromEnv && fromEnv.trim().length > 0) return path5.resolve(fromEnv);
477
+ return path5.join(os.homedir(), ".wrongstack");
478
+ }
479
+
480
+ // src/utils/message-invariants.ts
481
+ function repairToolUseAdjacency(messages) {
482
+ const removedToolUses = [];
483
+ const removedToolResults = [];
484
+ let removedMessages = 0;
485
+ let changed = false;
486
+ const out = [];
487
+ for (let i = 0; i < messages.length; i++) {
488
+ const original = expectDefined(messages[i]);
489
+ let msg = original;
490
+ if (hasToolUse(msg)) {
491
+ const nextIds = toolResultIds(messages[i + 1]);
492
+ const filtered = mapContent(msg, (blocks) => {
493
+ const next = [];
494
+ for (const block of blocks) {
495
+ if (block.type === "tool_use" && !nextIds.has(block.id)) {
496
+ removedToolUses.push(block.id);
497
+ changed = true;
498
+ continue;
499
+ }
500
+ next.push(block);
501
+ }
502
+ return next;
503
+ });
504
+ msg = filtered ?? msg;
505
+ }
506
+ if (hasToolResult(msg)) {
507
+ const allowed = toolUseIds(out[out.length - 1]);
508
+ const filtered = mapContent(msg, (blocks) => {
509
+ const next = [];
510
+ for (const block of blocks) {
511
+ if (block.type === "tool_result" && !allowed.has(block.tool_use_id)) {
512
+ removedToolResults.push(block.tool_use_id);
513
+ changed = true;
514
+ continue;
515
+ }
516
+ next.push(block);
517
+ }
518
+ return next;
519
+ });
520
+ msg = filtered ?? msg;
521
+ }
522
+ if (isEmptyMessage(msg)) {
523
+ removedMessages++;
524
+ changed = true;
525
+ continue;
526
+ }
527
+ out.push(msg);
528
+ }
529
+ return {
530
+ messages: changed ? out : messages,
531
+ report: { changed, removedToolUses, removedToolResults, removedMessages }
532
+ };
533
+ }
534
+ function hasToolUse(msg) {
535
+ return contentBlocks(msg).some((b) => b.type === "tool_use");
536
+ }
537
+ function hasToolResult(msg) {
538
+ return contentBlocks(msg).some((b) => b.type === "tool_result");
539
+ }
540
+ function toolUseIds(msg) {
541
+ const ids = /* @__PURE__ */ new Set();
542
+ if (!msg || msg.role !== "assistant") return ids;
543
+ for (const block of contentBlocks(msg)) {
544
+ if (block.type === "tool_use") ids.add(block.id);
545
+ }
546
+ return ids;
547
+ }
548
+ function toolResultIds(msg) {
549
+ const ids = /* @__PURE__ */ new Set();
550
+ if (!msg || msg.role !== "user") return ids;
551
+ for (const block of contentBlocks(msg)) {
552
+ if (block.type === "tool_result") ids.add(block.tool_use_id);
553
+ }
554
+ return ids;
555
+ }
556
+ function contentBlocks(msg) {
557
+ return msg && Array.isArray(msg.content) ? msg.content : [];
558
+ }
559
+ function mapContent(msg, fn) {
560
+ if (!Array.isArray(msg.content)) return msg;
561
+ const next = fn(msg.content);
562
+ if (next.length === msg.content.length && next.every((b, idx) => b === msg.content[idx])) {
563
+ return msg;
564
+ }
565
+ return { ...msg, content: next };
566
+ }
567
+ function isEmptyMessage(msg) {
568
+ if (typeof msg.content === "string") return msg.content.trim().length === 0;
569
+ return msg.content.length === 0;
570
+ }
571
+ var GLOB_CHARS = /* @__PURE__ */ new Set(["*", "?", "["]);
572
+ var IS_WINDOWS = process.platform === "win32";
573
+ var SEP = IS_WINDOWS ? "\\" : "/";
574
+ function isGlob(p) {
575
+ for (const c of p) {
576
+ if (GLOB_CHARS.has(c)) return true;
577
+ }
578
+ return false;
579
+ }
580
+ function globToRegex(pat) {
581
+ let i = 0;
582
+ let re = "^";
583
+ while (i < pat.length) {
584
+ const c = expectDefined(pat[i]);
585
+ if (c === "*") {
586
+ if (pat[i + 1] === "*") {
587
+ re += ".*";
588
+ i += 2;
589
+ if (pat[i] === "/") i++;
590
+ } else {
591
+ re += "[^/\\\\]*";
592
+ i++;
593
+ }
594
+ } else if (c === "?") {
595
+ re += "[^/\\\\]";
596
+ i++;
597
+ } else if (c === "[") {
598
+ let cls = "[";
599
+ i++;
600
+ if (pat[i] === "!" || pat[i] === "^") {
601
+ cls += "^";
602
+ i++;
603
+ }
604
+ while (i < pat.length && pat[i] !== "]") {
605
+ const ch = pat[i] ?? "";
606
+ if (ch === "\\") cls += "\\\\";
607
+ else if (ch === "]" || ch === "^") cls += `\\${ch}`;
608
+ else cls += ch;
609
+ i++;
610
+ }
611
+ cls += "]";
612
+ re += cls;
613
+ i++;
614
+ } else {
615
+ re += c.replace(/[.+^${}()|\\]/g, "\\$&");
616
+ i++;
617
+ }
618
+ }
619
+ return new RegExp(re + "$");
620
+ }
621
+ function baseDir(pat) {
622
+ let i = pat.length - 1;
623
+ while (i >= 0 && !GLOB_CHARS.has(expectDefined(pat[i])) && pat[i] !== SEP && pat[i] !== "/") i--;
624
+ const cut = i >= 0 ? pat.lastIndexOf(SEP, i) : pat.lastIndexOf("/", i);
625
+ return cut < 0 ? "." : pat.slice(0, cut);
626
+ }
627
+ async function expandGlob(pattern) {
628
+ if (!isGlob(pattern)) return [pattern];
629
+ const results = /* @__PURE__ */ new Set();
630
+ const abs = isAbsolute(pattern);
631
+ const base = abs ? baseDir(pattern) : baseDir(pattern);
632
+ const relPat = base === "." ? pattern : pattern.slice(base.length + 1);
633
+ async function walk(dir, pat) {
634
+ let entries;
635
+ try {
636
+ entries = await fsp6.readdir(dir);
637
+ } catch {
638
+ return;
639
+ }
640
+ const firstGlob = pat.search(/[*?[[]/);
641
+ if (firstGlob < 0) {
642
+ const re = globToRegex(pat);
643
+ for (const e of entries) {
644
+ if (re.test(e)) {
645
+ const full = `${dir}${SEP}${e}`;
646
+ results.add(abs ? resolve(full) : full);
647
+ }
648
+ }
649
+ return;
650
+ }
651
+ const before = pat.slice(0, firstGlob);
652
+ const rest = pat.slice(firstGlob);
653
+ if (before.endsWith("**")) {
654
+ await walk(dir, rest);
655
+ for (const e of entries) {
656
+ const full = `${dir}${SEP}${e}`;
657
+ try {
658
+ const stat6 = await fsp6.stat(full);
659
+ if (stat6.isDirectory()) await walk(full, rest);
660
+ } catch {
661
+ }
662
+ }
663
+ } else if (before === "") {
664
+ const re = globToRegex(rest);
665
+ for (const e of entries) {
666
+ if (re.test(e)) {
667
+ const full = `${dir}${SEP}${e}`;
668
+ results.add(abs ? resolve(full) : full);
669
+ }
670
+ }
671
+ } else {
672
+ const seg = before.replace(/[*?[\]]/g, "").replace(/\/$/, "");
673
+ if (entries.includes(seg)) {
674
+ const full = `${dir}${SEP}${seg}`;
675
+ try {
676
+ const stat6 = await fsp6.stat(full);
677
+ if (stat6.isDirectory()) await walk(full, rest);
678
+ } catch {
679
+ }
680
+ }
681
+ }
682
+ }
683
+ await walk(base === "." ? "." : base, relPat);
684
+ return [...results];
685
+ }
686
+
442
687
  // src/types/errors.ts
443
688
  var ERROR_CODES = {
444
689
  // Provider
@@ -657,133 +902,6 @@ function createMessage(type, from, payload, to) {
657
902
  priority: "normal"
658
903
  };
659
904
  }
660
-
661
- // src/utils/expect-defined.ts
662
- function expectDefined(value, label) {
663
- if (value === null || value === void 0) {
664
- const err = new Error("Expected value to be defined");
665
- err.name = "ExpectDefinedError";
666
- throw err;
667
- }
668
- return value;
669
- }
670
- var GLOB_CHARS = /* @__PURE__ */ new Set(["*", "?", "["]);
671
- var IS_WINDOWS = process.platform === "win32";
672
- var SEP = IS_WINDOWS ? "\\" : "/";
673
- function isGlob(p) {
674
- for (const c of p) {
675
- if (GLOB_CHARS.has(c)) return true;
676
- }
677
- return false;
678
- }
679
- function globToRegex(pat) {
680
- let i = 0;
681
- let re = "^";
682
- while (i < pat.length) {
683
- const c = expectDefined(pat[i]);
684
- if (c === "*") {
685
- if (pat[i + 1] === "*") {
686
- re += ".*";
687
- i += 2;
688
- if (pat[i] === "/") i++;
689
- } else {
690
- re += "[^/\\\\]*";
691
- i++;
692
- }
693
- } else if (c === "?") {
694
- re += "[^/\\\\]";
695
- i++;
696
- } else if (c === "[") {
697
- let cls = "[";
698
- i++;
699
- if (pat[i] === "!" || pat[i] === "^") {
700
- cls += "^";
701
- i++;
702
- }
703
- while (i < pat.length && pat[i] !== "]") {
704
- const ch = pat[i] ?? "";
705
- if (ch === "\\") cls += "\\\\";
706
- else if (ch === "]" || ch === "^") cls += `\\${ch}`;
707
- else cls += ch;
708
- i++;
709
- }
710
- cls += "]";
711
- re += cls;
712
- i++;
713
- } else {
714
- re += c.replace(/[.+^${}()|\\]/g, "\\$&");
715
- i++;
716
- }
717
- }
718
- return new RegExp(re + "$");
719
- }
720
- function baseDir(pat) {
721
- let i = pat.length - 1;
722
- while (i >= 0 && !GLOB_CHARS.has(expectDefined(pat[i])) && pat[i] !== SEP && pat[i] !== "/") i--;
723
- const cut = i >= 0 ? pat.lastIndexOf(SEP, i) : pat.lastIndexOf("/", i);
724
- return cut < 0 ? "." : pat.slice(0, cut);
725
- }
726
- async function expandGlob(pattern) {
727
- if (!isGlob(pattern)) return [pattern];
728
- const results = /* @__PURE__ */ new Set();
729
- const abs = isAbsolute(pattern);
730
- const base = abs ? baseDir(pattern) : baseDir(pattern);
731
- const relPat = base === "." ? pattern : pattern.slice(base.length + 1);
732
- async function walk(dir, pat) {
733
- let entries;
734
- try {
735
- entries = await fsp6.readdir(dir);
736
- } catch {
737
- return;
738
- }
739
- const firstGlob = pat.search(/[*?[[]/);
740
- if (firstGlob < 0) {
741
- const re = globToRegex(pat);
742
- for (const e of entries) {
743
- if (re.test(e)) {
744
- const full = `${dir}${SEP}${e}`;
745
- results.add(abs ? resolve(full) : full);
746
- }
747
- }
748
- return;
749
- }
750
- const before = pat.slice(0, firstGlob);
751
- const rest = pat.slice(firstGlob);
752
- if (before.endsWith("**")) {
753
- await walk(dir, rest);
754
- for (const e of entries) {
755
- const full = `${dir}${SEP}${e}`;
756
- try {
757
- const stat5 = await fsp6.stat(full);
758
- if (stat5.isDirectory()) await walk(full, rest);
759
- } catch {
760
- }
761
- }
762
- } else if (before === "") {
763
- const re = globToRegex(rest);
764
- for (const e of entries) {
765
- if (re.test(e)) {
766
- const full = `${dir}${SEP}${e}`;
767
- results.add(abs ? resolve(full) : full);
768
- }
769
- }
770
- } else {
771
- const seg = before.replace(/[*?[\]]/g, "").replace(/\/$/, "");
772
- if (entries.includes(seg)) {
773
- const full = `${dir}${SEP}${seg}`;
774
- try {
775
- const stat5 = await fsp6.stat(full);
776
- if (stat5.isDirectory()) await walk(full, rest);
777
- } catch {
778
- }
779
- }
780
- }
781
- }
782
- await walk(base === "." ? "." : base, relPat);
783
- return [...results];
784
- }
785
-
786
- // src/coordination/collab-debug.ts
787
905
  var DEFAULT_MAX_TARGET_FILES = 30;
788
906
  var DirectorAlertLevel = /* @__PURE__ */ ((DirectorAlertLevel2) => {
789
907
  DirectorAlertLevel2["WARNING"] = "warning";
@@ -878,7 +996,7 @@ var CollabSession = class extends EventEmitter {
878
996
  }
879
997
  for (const filePath of allFiles) {
880
998
  try {
881
- const [content, stat5] = await Promise.all([
999
+ const [content, stat6] = await Promise.all([
882
1000
  fsp6.readFile(filePath, "utf8"),
883
1001
  fsp6.stat(filePath)
884
1002
  ]);
@@ -888,8 +1006,8 @@ var CollabSession = class extends EventEmitter {
888
1006
  path: filePath,
889
1007
  content,
890
1008
  language,
891
- snapshotMtimeMs: stat5.mtimeMs,
892
- snapshotSizeBytes: stat5.size
1009
+ snapshotMtimeMs: stat6.mtimeMs,
1010
+ snapshotSizeBytes: stat6.size
893
1011
  });
894
1012
  } catch {
895
1013
  this.snapshot.files.push({ path: filePath, content: "", language: void 0 });
@@ -1302,9 +1420,9 @@ Emit each evaluation immediately. Do not wait until you have read all reports.`;
1302
1420
  for (const file of this.snapshot.files) {
1303
1421
  if (file.snapshotMtimeMs === void 0 && file.snapshotSizeBytes === void 0) continue;
1304
1422
  try {
1305
- const stat5 = await fsp6.stat(file.path);
1306
- const mtimeChanged = file.snapshotMtimeMs !== void 0 && stat5.mtimeMs > file.snapshotMtimeMs + 1;
1307
- const sizeChanged = file.snapshotSizeBytes !== void 0 && stat5.size !== file.snapshotSizeBytes;
1423
+ const stat6 = await fsp6.stat(file.path);
1424
+ const mtimeChanged = file.snapshotMtimeMs !== void 0 && stat6.mtimeMs > file.snapshotMtimeMs + 1;
1425
+ const sizeChanged = file.snapshotSizeBytes !== void 0 && stat6.size !== file.snapshotSizeBytes;
1308
1426
  if (mtimeChanged || sizeChanged) {
1309
1427
  warnings.push(`${file.path} changed after the collab snapshot was captured.`);
1310
1428
  }
@@ -4329,7 +4447,7 @@ function makeSpawnTool(director, roster) {
4329
4447
  if (err instanceof FleetCostCapError) {
4330
4448
  return { error: err.message, kind: err.kind, limit: err.limit, observed: err.observed };
4331
4449
  }
4332
- return { error: err instanceof Error ? err.message : String(err) };
4450
+ return { error: toErrorMessage(err) };
4333
4451
  }
4334
4452
  }
4335
4453
  };
@@ -4411,7 +4529,7 @@ function makeAskTool(director) {
4411
4529
  _hint: "Response was large and stored. Use ask_result with the key to retrieve it."
4412
4530
  };
4413
4531
  } catch (err) {
4414
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
4532
+ return { ok: false, error: toErrorMessage(err) };
4415
4533
  }
4416
4534
  }
4417
4535
  };
@@ -4637,7 +4755,7 @@ function makeCollabDebugTool(director) {
4637
4755
  evaluations: report.evaluations
4638
4756
  };
4639
4757
  } catch (err) {
4640
- const msg = err instanceof Error ? err.message : String(err);
4758
+ const msg = toErrorMessage(err);
4641
4759
  return { error: "collab_debug failed: " + msg };
4642
4760
  }
4643
4761
  }
@@ -4971,6 +5089,7 @@ function resolveModelMatrix(matrix, role) {
4971
5089
 
4972
5090
  // src/coordination/subagent-budget.ts
4973
5091
  var TIMEOUT_PREEMPT_FRACTION = 0.85;
5092
+ var DECISION_TIMEOUT_MS = 6e4;
4974
5093
  var BudgetExceededError = class extends Error {
4975
5094
  kind;
4976
5095
  limit;
@@ -5000,6 +5119,31 @@ var BudgetThresholdSignal = class extends Error {
5000
5119
  };
5001
5120
  var SubagentBudget = class _SubagentBudget {
5002
5121
  limits;
5122
+ /** Patch one or more budget limits in-place after construction.
5123
+ * Used by the coordinator watchdog when granting an extension.
5124
+ * All fields are optional — only provided fields are updated.
5125
+ * This is the single write path for limit mutations so that future
5126
+ * validation or side-effects live in one place (M1). */
5127
+ patchLimits(ext) {
5128
+ if (ext.maxIterations !== void 0) {
5129
+ this.limits.maxIterations = ext.maxIterations;
5130
+ }
5131
+ if (ext.maxToolCalls !== void 0) {
5132
+ this.limits.maxToolCalls = ext.maxToolCalls;
5133
+ }
5134
+ if (ext.maxTokens !== void 0) {
5135
+ this.limits.maxTokens = ext.maxTokens;
5136
+ }
5137
+ if (ext.maxCostUsd !== void 0) {
5138
+ this.limits.maxCostUsd = ext.maxCostUsd;
5139
+ }
5140
+ if (ext.timeoutMs !== void 0) {
5141
+ this.limits.timeoutMs = ext.timeoutMs;
5142
+ }
5143
+ if (ext.idleTimeoutMs !== void 0) {
5144
+ this.limits.idleTimeoutMs = ext.idleTimeoutMs;
5145
+ }
5146
+ }
5003
5147
  iterations = 0;
5004
5148
  toolCalls = 0;
5005
5149
  tokenInput = 0;
@@ -5020,12 +5164,44 @@ var SubagentBudget = class _SubagentBudget {
5020
5164
  * or hung listener (Director not built / event filter detached mid-run)
5021
5165
  * leaves the budget over-limit and never enforces anything.
5022
5166
  */
5023
- static DECISION_TIMEOUT_MS = 6e4;
5167
+ static DECISION_TIMEOUT_MS = DECISION_TIMEOUT_MS;
5024
5168
  /**
5025
5169
  * Injected by the runner when wiring the budget to its EventBus.
5026
5170
  * Used to emit `budget.threshold_reached` events in `'auto'` mode.
5027
5171
  */
5028
5172
  _events;
5173
+ /**
5174
+ * Guard against dual-path races between the coordinator watchdog
5175
+ * (`executeWithTimeout`) and the budget's own `checkTimeout()`.
5176
+ * Both paths detect `elapsed >= timeoutMs` and can emit
5177
+ * `budget.threshold_reached` for kind `'timeout'` simultaneously.
5178
+ * Set to the current `timeoutMs` ceiling by the coordinator BEFORE
5179
+ * calling `onThreshold`, and cleared after the negotiation resolves.
5180
+ * `checkTimeout()` skips its wall-clock check while this is set so
5181
+ * the coordinator's watchdog is the sole source of wall-clock timeout
5182
+ * events — `checkTimeout()` focuses exclusively on `idle_timeout`.
5183
+ */
5184
+ _watchdogActive;
5185
+ /** Returns the timeout ceiling currently being negotiated by the watchdog,
5186
+ * or `undefined` when no wall-clock negotiation is in flight.
5187
+ * Used by `executeWithTimeout` to detect a stale lock (M3). */
5188
+ get watchdogActive() {
5189
+ return this._watchdogActive;
5190
+ }
5191
+ /** Called by the coordinator watchdog BEFORE calling `onThreshold` so that
5192
+ * `checkTimeout()` skips its wall-clock check for this ceiling. Prevents
5193
+ * the budget's own `checkTimeout()` from emitting a second
5194
+ * `budget.threshold_reached` event while the watchdog is already
5195
+ * negotiating the same wall-clock deadline (C1). */
5196
+ setWatchdogNegotiation(timeoutMs) {
5197
+ this._watchdogActive = timeoutMs;
5198
+ }
5199
+ /** Clears the watchdog guard after negotiation resolves. Called in the
5200
+ * `finally` block of both the pre-empt and deadline branches so it fires
5201
+ * on every exit path: grant, deny, throw, or error. */
5202
+ clearWatchdogNegotiation() {
5203
+ this._watchdogActive = void 0;
5204
+ }
5029
5205
  /**
5030
5206
  * Negotiation mode — controls whether a threshold hit tries to emit
5031
5207
  * `budget.threshold_reached` and wait for a coordinator decision, or
@@ -5126,7 +5302,8 @@ var SubagentBudget = class _SubagentBudget {
5126
5302
  if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
5127
5303
  exceeded.push({ kind: "idle_timeout", used: idle, limit: this.limits.idleTimeoutMs });
5128
5304
  }
5129
- if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs) {
5305
+ const wallOwnedByWatchdog = this._onThreshold !== void 0 && this._watchdogActive === this.limits.timeoutMs;
5306
+ if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs && !wallOwnedByWatchdog) {
5130
5307
  exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
5131
5308
  }
5132
5309
  }
@@ -5140,19 +5317,99 @@ var SubagentBudget = class _SubagentBudget {
5140
5317
  throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
5141
5318
  }
5142
5319
  const bus = this._events;
5143
- if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
5320
+ if (!bus) {
5144
5321
  const first2 = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
5145
5322
  throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
5146
5323
  }
5324
+ const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
5325
+ if (bus.hasListenerFor("budget.threshold_reached")) {
5326
+ for (const entry of exceeded) {
5327
+ if (this._pendingNegotiations.has(entry.kind)) continue;
5328
+ this._pendingNegotiations.set(entry.kind, this._negotiateExtension(entry));
5329
+ }
5330
+ const decision = this._pendingNegotiations.get(first.kind);
5331
+ if (!decision) throw new Error(`No pending negotiation for ${first.kind}`);
5332
+ throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
5333
+ }
5334
+ let hardStop = null;
5147
5335
  for (const entry of exceeded) {
5148
5336
  if (this._pendingNegotiations.has(entry.kind)) continue;
5149
- const decision2 = this._negotiateExtension(entry.kind, exceeded);
5150
- this._pendingNegotiations.set(entry.kind, decision2);
5337
+ const marker = Promise.resolve("stop");
5338
+ this._pendingNegotiations.set(entry.kind, marker);
5339
+ void marker.finally(() => this._pendingNegotiations.delete(entry.kind));
5340
+ const sync = this._invokeHandlerSync(entry);
5341
+ if (!sync) hardStop ??= new BudgetExceededError(entry.kind, entry.limit, entry.used);
5151
5342
  }
5152
- const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
5153
- const decision = this._pendingNegotiations.get(first.kind);
5154
- if (!decision) throw new Error(`No pending negotiation for ${first.kind}`);
5155
- throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
5343
+ if (hardStop) throw hardStop;
5344
+ return exceeded;
5345
+ }
5346
+ /**
5347
+ * Invoke `onThreshold` once for `entry` on the NO-LISTENER path and report
5348
+ * whether it decided synchronously. Returns `true` when the handler returned
5349
+ * a synchronous decision (already honored — an `extend` patched the limits),
5350
+ * or `false` when it returned a Promise (async; the caller hard-stops, since
5351
+ * there is no listener to resolve the negotiation). The handler is given the
5352
+ * full info shape (`requestDecision` plus direct `extend`/`deny`) so both
5353
+ * recording handlers and policy handlers work without a wired listener.
5354
+ */
5355
+ _invokeHandlerSync(entry) {
5356
+ const handler = this._onThreshold;
5357
+ if (!handler) return false;
5358
+ let extendArg;
5359
+ const result = handler({
5360
+ kind: entry.kind,
5361
+ used: entry.used,
5362
+ limit: entry.limit,
5363
+ requestDecision: () => this._busRequestDecision(entry),
5364
+ // Direct hooks for synchronous policy/recording handlers.
5365
+ extend: (extra) => {
5366
+ extendArg = extra;
5367
+ },
5368
+ deny: () => {
5369
+ }
5370
+ });
5371
+ if (result && typeof result.then === "function") return false;
5372
+ if (result === "throw") return false;
5373
+ if (result && typeof result === "object" && "extend" in result) {
5374
+ extendArg = result.extend;
5375
+ }
5376
+ if (extendArg) this.patchLimits(extendArg);
5377
+ return true;
5378
+ }
5379
+ /**
5380
+ * Emit `budget.threshold_reached` and resolve to the listener's verdict.
5381
+ * Resolves to `'stop'` immediately when there is no listener (or no bus) so
5382
+ * no negotiation can hang and no fallback timer leaks. Mirrors the
5383
+ * coordinator watchdog's own request path so both agree on the no-listener
5384
+ * default.
5385
+ */
5386
+ _busRequestDecision(entry) {
5387
+ const bus = this._events;
5388
+ if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
5389
+ return Promise.resolve("stop");
5390
+ }
5391
+ return new Promise((resolve3) => {
5392
+ let resolved = false;
5393
+ const respond = (d) => {
5394
+ if (resolved) return;
5395
+ resolved = true;
5396
+ clearTimeout(fallback);
5397
+ resolve3(d);
5398
+ };
5399
+ const fallback = setTimeout(() => respond("stop"), _SubagentBudget.DECISION_TIMEOUT_MS);
5400
+ bus.emit("budget.threshold_reached", {
5401
+ kind: entry.kind,
5402
+ used: entry.used,
5403
+ limit: entry.limit,
5404
+ timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
5405
+ // deny() wins over a same-dispatch extend(): a listener that both grants
5406
+ // and denies (or two listeners disagreeing) is resolved as a stop. The
5407
+ // grant is deferred a microtask so a synchronous deny in the same emit
5408
+ // pre-empts it; async grants still resolve normally.
5409
+ extend: (extra) => queueMicrotask(() => respond({ extend: extra })),
5410
+ deny: () => respond("stop")
5411
+ });
5412
+ });
5156
5413
  }
5157
5414
  /**
5158
5415
  * Per-kind in-flight negotiation Promises. Each budget kind can have its
@@ -5172,77 +5429,33 @@ var SubagentBudget = class _SubagentBudget {
5172
5429
  * `{ extend: {} }` — keep going without patching; next overrun fires
5173
5430
  * a fresh signal.
5174
5431
  */
5175
- async _negotiateExtension(kind, exceeded) {
5432
+ async _negotiateExtension(entry) {
5176
5433
  if (!this._onThreshold) {
5177
5434
  return "stop";
5178
5435
  }
5179
5436
  try {
5180
- const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
5181
5437
  const result = this._onThreshold({
5182
- kind: first.kind,
5183
- used: first.used,
5184
- limit: first.limit,
5185
- requestDecision: () => {
5186
- const bus = this._events;
5187
- if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
5188
- return Promise.resolve("stop");
5189
- }
5190
- return new Promise((resolve3) => {
5191
- let resolved = false;
5192
- const respond = (d) => {
5193
- if (resolved) return;
5194
- resolved = true;
5195
- resolve3(d);
5196
- };
5197
- const fallback = setTimeout(
5198
- () => respond("stop"),
5199
- _SubagentBudget.DECISION_TIMEOUT_MS
5200
- );
5201
- for (const { kind: kind2, used, limit } of exceeded) {
5202
- bus.emit("budget.threshold_reached", {
5203
- kind: kind2,
5204
- used,
5205
- limit,
5206
- timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
5207
- extend: (extra) => {
5208
- clearTimeout(fallback);
5209
- respond({ extend: extra });
5210
- },
5211
- deny: () => {
5212
- clearTimeout(fallback);
5213
- respond("stop");
5214
- }
5215
- });
5216
- }
5217
- });
5438
+ kind: entry.kind,
5439
+ used: entry.used,
5440
+ limit: entry.limit,
5441
+ // One event for THIS kind only — each exceeded kind has its own
5442
+ // negotiation (and its own resolve), so there is no cross-kind
5443
+ // first-wins drop and no O(N^2) re-emission.
5444
+ requestDecision: () => this._busRequestDecision(entry),
5445
+ extend: (extra) => {
5446
+ this.patchLimits(extra);
5447
+ },
5448
+ deny: () => {
5218
5449
  }
5219
5450
  });
5220
5451
  if (result === "throw") return "stop";
5221
5452
  if (result === "continue") return { extend: {} };
5222
5453
  const decision = await result;
5223
5454
  if (decision === "stop") return "stop";
5224
- const ext = decision.extend;
5225
- if (ext.maxIterations !== void 0) {
5226
- this.limits.maxIterations = ext.maxIterations;
5227
- }
5228
- if (ext.maxToolCalls !== void 0) {
5229
- this.limits.maxToolCalls = ext.maxToolCalls;
5230
- }
5231
- if (ext.maxTokens !== void 0) {
5232
- this.limits.maxTokens = ext.maxTokens;
5233
- }
5234
- if (ext.maxCostUsd !== void 0) {
5235
- this.limits.maxCostUsd = ext.maxCostUsd;
5236
- }
5237
- if (ext.timeoutMs !== void 0) {
5238
- this.limits.timeoutMs = ext.timeoutMs;
5239
- }
5240
- if (ext.idleTimeoutMs !== void 0) {
5241
- this.limits.idleTimeoutMs = ext.idleTimeoutMs;
5242
- }
5455
+ this.patchLimits(decision.extend);
5243
5456
  return decision;
5244
5457
  } finally {
5245
- this._pendingNegotiations.delete(kind);
5458
+ this._pendingNegotiations.delete(entry.kind);
5246
5459
  }
5247
5460
  }
5248
5461
  recordIteration() {
@@ -5285,7 +5498,8 @@ var SubagentBudget = class _SubagentBudget {
5285
5498
  const { timeoutMs, idleTimeoutMs } = this.limits;
5286
5499
  if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
5287
5500
  const elapsed = Date.now() - this.startTime;
5288
- const wallTripped = timeoutMs !== void 0 && elapsed > timeoutMs;
5501
+ const wallSkipped = this._onThreshold !== void 0 && this._watchdogActive !== void 0 && timeoutMs !== void 0 && this._watchdogActive === timeoutMs;
5502
+ const wallTripped = wallSkipped ? false : timeoutMs !== void 0 && elapsed > timeoutMs;
5289
5503
  const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
5290
5504
  if (!wallTripped && !idleTripped) return;
5291
5505
  void this.checkLimits(elapsed);
@@ -5313,11 +5527,6 @@ var SubagentBudget = class _SubagentBudget {
5313
5527
  }
5314
5528
  };
5315
5529
 
5316
- // src/utils/string.ts
5317
- function truncate(s, max) {
5318
- return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
5319
- }
5320
-
5321
5530
  // src/types/provider.ts
5322
5531
  var ProviderError = class extends WrongStackError {
5323
5532
  status;
@@ -5397,7 +5606,7 @@ function classifySubagentError(err, hints = {}) {
5397
5606
  const baseMessage2 = err.describe();
5398
5607
  return providerErrorToSubagentError(err, baseMessage2, cause);
5399
5608
  }
5400
- const baseMessage = err instanceof Error ? err.message : String(err);
5609
+ const baseMessage = toErrorMessage(err);
5401
5610
  if (err instanceof BudgetExceededError) {
5402
5611
  const map = {
5403
5612
  iterations: "budget_iterations",
@@ -5912,6 +6121,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
5912
6121
  terminating = /* @__PURE__ */ new Set();
5913
6122
  constructor(config, options = {}) {
5914
6123
  super();
6124
+ this.setMaxListeners(0);
5915
6125
  this.coordinatorId = config.coordinatorId;
5916
6126
  this.config = config;
5917
6127
  this.runner = options.runner;
@@ -6306,7 +6516,13 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
6306
6516
  let result;
6307
6517
  budget.start();
6308
6518
  try {
6309
- const outcome = await this.executeWithTimeout(this.runner, task, runCtx, budget);
6519
+ const outcome = await this.executeWithTimeout(
6520
+ this.runner,
6521
+ task,
6522
+ runCtx,
6523
+ budget,
6524
+ subagent.config.preemptFraction
6525
+ );
6310
6526
  result = {
6311
6527
  subagentId,
6312
6528
  taskId: task.id,
@@ -6333,7 +6549,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
6333
6549
  }
6334
6550
  this.recordCompletion(result);
6335
6551
  }
6336
- async executeWithTimeout(runner, task, ctx, budget) {
6552
+ async executeWithTimeout(runner, task, ctx, budget, preemptFraction = TIMEOUT_PREEMPT_FRACTION) {
6337
6553
  const initialTimeoutMs = budget.limits.timeoutMs;
6338
6554
  const idleLimitMs = budget.limits.idleTimeoutMs;
6339
6555
  if (initialTimeoutMs === void 0 && idleLimitMs === void 0) {
@@ -6341,8 +6557,21 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
6341
6557
  }
6342
6558
  const start = Date.now();
6343
6559
  let timer = null;
6344
- let preemptedForLimit = null;
6560
+ let PreemptState;
6561
+ ((PreemptState2) => {
6562
+ PreemptState2["ACTIVE"] = "active";
6563
+ PreemptState2["LOCKED"] = "locked";
6564
+ })(PreemptState || (PreemptState = {}));
6565
+ let preemptedCeiling = null;
6566
+ let preemptState = "active" /* ACTIVE */;
6567
+ let lastGrantActivityTs = -1;
6345
6568
  const timeoutPromise = new Promise((_, reject) => {
6569
+ const terminate = (kind, limit, used) => {
6570
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
6571
+ reject(
6572
+ budget._events?.hasListenerFor("budget.threshold_reached") ? new Error(`subagent stopped: budget ${kind} (limit=${limit}, used=${used})`) : new BudgetExceededError(kind, limit, used)
6573
+ );
6574
+ };
6346
6575
  const armFor = (ms) => {
6347
6576
  if (timer) clearTimeout(timer);
6348
6577
  timer = setTimeout(onTick, Math.max(0, ms));
@@ -6351,7 +6580,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
6351
6580
  const wallLimit = budget.limits.timeoutMs ?? initialTimeoutMs;
6352
6581
  const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : wallLimit - (Date.now() - start);
6353
6582
  const idleRemaining = idleLimitMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
6354
- const preemptRemaining = initialTimeoutMs === void 0 || preemptedForLimit === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * TIMEOUT_PREEMPT_FRACTION - (Date.now() - start);
6583
+ const preemptRemaining = initialTimeoutMs === void 0 || preemptedCeiling === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * preemptFraction - (Date.now() - start);
6355
6584
  armFor(Math.max(25, Math.min(wallRemaining, idleRemaining, preemptRemaining)));
6356
6585
  };
6357
6586
  const negotiateTimeout = async (used, limit) => {
@@ -6361,16 +6590,42 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
6361
6590
  kind: "timeout",
6362
6591
  used,
6363
6592
  limit,
6364
- requestDecision: () => new Promise((resolveDecision) => {
6365
- budget._events?.emit("budget.threshold_reached", {
6366
- kind: "timeout",
6367
- used,
6368
- limit,
6369
- timeoutMs: 6e4,
6370
- extend: (extra) => resolveDecision({ extend: extra }),
6371
- deny: () => resolveDecision("stop")
6593
+ requestDecision: () => {
6594
+ if (!budget._events?.hasListenerFor("budget.threshold_reached")) {
6595
+ return Promise.resolve("stop");
6596
+ }
6597
+ return new Promise((resolveDecision) => {
6598
+ let settled = false;
6599
+ const resolve3 = (d) => {
6600
+ if (settled) return;
6601
+ settled = true;
6602
+ resolveDecision(d);
6603
+ };
6604
+ const fallback = setTimeout(() => resolve3("stop"), DECISION_TIMEOUT_MS);
6605
+ budget._events?.emit("budget.threshold_reached", {
6606
+ kind: "timeout",
6607
+ used,
6608
+ limit,
6609
+ // Informational: the budget's own decision deadline. Listeners may use
6610
+ // this to display a countdown. The coordinator does NOT enforce it —
6611
+ // it is the budget's own `setTimeout(fallback)` that races against
6612
+ // the listener's `extend()`/`deny()` call to guarantee progress.
6613
+ timeoutMs: DECISION_TIMEOUT_MS,
6614
+ // deny() wins over a same-dispatch extend(): defer the grant a
6615
+ // microtask so a synchronous deny in the same emit pre-empts it
6616
+ // (a listener that both grants and denies, or two listeners
6617
+ // disagreeing, resolves as a stop). Async grants still resolve.
6618
+ extend: (extra) => {
6619
+ clearTimeout(fallback);
6620
+ queueMicrotask(() => resolve3({ extend: extra }));
6621
+ },
6622
+ deny: () => {
6623
+ clearTimeout(fallback);
6624
+ resolve3("stop");
6625
+ }
6626
+ });
6372
6627
  });
6373
- })
6628
+ }
6374
6629
  });
6375
6630
  return typeof result === "string" ? result : await result;
6376
6631
  };
@@ -6381,21 +6636,45 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
6381
6636
  const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
6382
6637
  const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
6383
6638
  if (idleExceeded && !wallExceeded) {
6639
+ budget._events?.emit("budget.threshold_reached", {
6640
+ kind: "idle_timeout",
6641
+ used: budget.idleMs(),
6642
+ limit: idleLimit ?? 0,
6643
+ timeoutMs: DECISION_TIMEOUT_MS,
6644
+ extend: () => {
6645
+ },
6646
+ deny: () => {
6647
+ }
6648
+ });
6384
6649
  this.subagents.get(ctx.subagentId)?.abortController.abort();
6385
- reject(new BudgetExceededError("timeout", idleLimit ?? 0, budget.idleMs()));
6650
+ reject(new BudgetExceededError("idle_timeout", idleLimit ?? 0, budget.idleMs()));
6386
6651
  return;
6387
6652
  }
6388
- if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptedForLimit !== wallLimit && elapsed >= wallLimit * TIMEOUT_PREEMPT_FRACTION) {
6653
+ if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptState === "active" /* ACTIVE */ && elapsed >= wallLimit * preemptFraction) {
6654
+ const activityTs = Date.now() - budget.idleMs();
6655
+ if (activityTs <= lastGrantActivityTs) {
6656
+ preemptState = "locked" /* LOCKED */;
6657
+ preemptedCeiling = wallLimit;
6658
+ scheduleNext();
6659
+ return;
6660
+ }
6661
+ budget.setWatchdogNegotiation(wallLimit);
6389
6662
  try {
6390
6663
  const decision = await negotiateTimeout(elapsed, wallLimit);
6391
6664
  if (typeof decision !== "string" && decision.extend.timeoutMs !== void 0) {
6392
- budget.limits.timeoutMs = decision.extend.timeoutMs;
6393
- preemptedForLimit = null;
6665
+ budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
6666
+ lastGrantActivityTs = Date.now() - budget.idleMs();
6667
+ preemptState = "active" /* ACTIVE */;
6668
+ preemptedCeiling = null;
6394
6669
  } else {
6395
- preemptedForLimit = wallLimit;
6670
+ preemptState = "locked" /* LOCKED */;
6671
+ preemptedCeiling = wallLimit;
6396
6672
  }
6397
6673
  } catch {
6398
- preemptedForLimit = wallLimit;
6674
+ preemptState = "locked" /* LOCKED */;
6675
+ preemptedCeiling = wallLimit;
6676
+ } finally {
6677
+ budget.clearWatchdogNegotiation();
6399
6678
  }
6400
6679
  scheduleNext();
6401
6680
  return;
@@ -6410,26 +6689,41 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
6410
6689
  reject(new BudgetExceededError("timeout", limit, elapsed));
6411
6690
  return;
6412
6691
  }
6692
+ budget.setWatchdogNegotiation(limit);
6413
6693
  try {
6414
6694
  const decision = await negotiateTimeout(elapsed, limit);
6415
- if (decision === "continue" || decision === "throw" || decision === "stop") {
6416
- preemptedForLimit = null;
6695
+ if (decision === "throw") {
6696
+ terminate("timeout", limit, elapsed);
6697
+ return;
6698
+ }
6699
+ if (decision === "continue") {
6700
+ preemptState = "locked" /* LOCKED */;
6701
+ preemptedCeiling = wallLimit;
6417
6702
  armFor(Math.max(1e3, limit));
6418
6703
  return;
6419
6704
  }
6705
+ if (decision === "stop") {
6706
+ terminate("timeout", limit, elapsed);
6707
+ return;
6708
+ }
6420
6709
  if (decision.extend.timeoutMs !== void 0) {
6421
- budget.limits.timeoutMs = decision.extend.timeoutMs;
6422
- preemptedForLimit = null;
6710
+ budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
6711
+ lastGrantActivityTs = Date.now() - budget.idleMs();
6712
+ preemptState = "active" /* ACTIVE */;
6713
+ preemptedCeiling = null;
6423
6714
  scheduleNext();
6424
6715
  return;
6425
6716
  }
6426
- this.subagents.get(ctx.subagentId)?.abortController.abort();
6427
- reject(new BudgetExceededError("timeout", limit, elapsed));
6717
+ terminate("timeout", limit, elapsed);
6718
+ return;
6428
6719
  } catch (err) {
6429
6720
  this.subagents.get(ctx.subagentId)?.abortController.abort();
6430
6721
  reject(
6431
6722
  err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
6432
6723
  );
6724
+ return;
6725
+ } finally {
6726
+ budget.clearWatchdogNegotiation();
6433
6727
  }
6434
6728
  };
6435
6729
  scheduleNext();
@@ -7383,7 +7677,7 @@ var Director = class _Director {
7383
7677
  })),
7384
7678
  usage: this.usage.snapshot()
7385
7679
  };
7386
- await fsp6.mkdir(path4.dirname(this.manifestPath), { recursive: true });
7680
+ await fsp6.mkdir(path5.dirname(this.manifestPath), { recursive: true });
7387
7681
  await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
7388
7682
  return this.manifestPath;
7389
7683
  }
@@ -7433,7 +7727,7 @@ var Director = class _Director {
7433
7727
  * listener for structured collection, and never affects exit code.
7434
7728
  */
7435
7729
  logShutdownError(phase, err) {
7436
- const detail = err instanceof Error ? err.message : String(err);
7730
+ const detail = toErrorMessage(err);
7437
7731
  process.emitWarning(
7438
7732
  `Director shutdown phase "${phase}" failed: ${detail}`,
7439
7733
  "DirectorShutdownWarning"
@@ -7597,7 +7891,7 @@ var Director = class _Director {
7597
7891
  */
7598
7892
  async readSession(subagentId, tail) {
7599
7893
  if (!this.sessionsRoot) return null;
7600
- const filePath = path4.join(this.sessionsRoot, this.directorRunId, `${subagentId}.jsonl`);
7894
+ const filePath = path5.join(this.sessionsRoot, this.directorRunId, `${subagentId}.jsonl`);
7601
7895
  let raw;
7602
7896
  try {
7603
7897
  raw = await fsp6.readFile(filePath, "utf8");
@@ -8026,7 +8320,7 @@ function createDelegateTool(opts) {
8026
8320
  summary
8027
8321
  };
8028
8322
  } catch (err) {
8029
- const message = err instanceof Error ? err.message : String(err);
8323
+ const message = toErrorMessage(err);
8030
8324
  opts.events?.emit("delegate.completed", {
8031
8325
  target,
8032
8326
  task: i.task,
@@ -8127,13 +8421,13 @@ async function readSubagentPartial(opts, subagentId) {
8127
8421
  if (!opts.sessionsRoot) return void 0;
8128
8422
  const candidates = [];
8129
8423
  if (opts.directorRunId) {
8130
- candidates.push(path4.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
8424
+ candidates.push(path5.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
8131
8425
  } else {
8132
8426
  try {
8133
8427
  const entries = await fsp6.readdir(opts.sessionsRoot, { withFileTypes: true });
8134
8428
  for (const entry of entries) {
8135
8429
  if (entry.isDirectory()) {
8136
- candidates.push(path4.join(opts.sessionsRoot, entry.name, `${subagentId}.jsonl`));
8430
+ candidates.push(path5.join(opts.sessionsRoot, entry.name, `${subagentId}.jsonl`));
8137
8431
  }
8138
8432
  }
8139
8433
  } catch {
@@ -8389,119 +8683,48 @@ function makeAgentSubagentRunner(opts) {
8389
8683
  function defaultFormatTaskInput(task) {
8390
8684
  return task.description ?? "";
8391
8685
  }
8392
-
8393
- // src/utils/message-invariants.ts
8394
- function repairToolUseAdjacency(messages) {
8395
- const removedToolUses = [];
8396
- const removedToolResults = [];
8397
- let removedMessages = 0;
8398
- let changed = false;
8399
- const out = [];
8400
- for (let i = 0; i < messages.length; i++) {
8401
- const original = expectDefined(messages[i]);
8402
- let msg = original;
8403
- if (hasToolUse(msg)) {
8404
- const nextIds = toolResultIds(messages[i + 1]);
8405
- const filtered = mapContent(msg, (blocks) => {
8406
- const next = [];
8407
- for (const block of blocks) {
8408
- if (block.type === "tool_use" && !nextIds.has(block.id)) {
8409
- removedToolUses.push(block.id);
8410
- changed = true;
8411
- continue;
8412
- }
8413
- next.push(block);
8414
- }
8415
- return next;
8416
- });
8417
- msg = filtered ?? msg;
8418
- }
8419
- if (hasToolResult(msg)) {
8420
- const allowed = toolUseIds(out[out.length - 1]);
8421
- const filtered = mapContent(msg, (blocks) => {
8422
- const next = [];
8423
- for (const block of blocks) {
8424
- if (block.type === "tool_result" && !allowed.has(block.tool_use_id)) {
8425
- removedToolResults.push(block.tool_use_id);
8426
- changed = true;
8427
- continue;
8428
- }
8429
- next.push(block);
8430
- }
8431
- return next;
8432
- });
8433
- msg = filtered ?? msg;
8434
- }
8435
- if (isEmptyMessage(msg)) {
8436
- removedMessages++;
8437
- changed = true;
8438
- continue;
8439
- }
8440
- out.push(msg);
8441
- }
8442
- return {
8443
- messages: changed ? out : messages,
8444
- report: { changed, removedToolUses, removedToolResults, removedMessages }
8445
- };
8446
- }
8447
- function hasToolUse(msg) {
8448
- return contentBlocks(msg).some((b) => b.type === "tool_use");
8686
+ function sanitizeModel(model) {
8687
+ return model.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
8449
8688
  }
8450
- function hasToolResult(msg) {
8451
- return contentBlocks(msg).some((b) => b.type === "tool_result");
8452
- }
8453
- function toolUseIds(msg) {
8454
- const ids = /* @__PURE__ */ new Set();
8455
- if (!msg || msg.role !== "assistant") return ids;
8456
- for (const block of contentBlocks(msg)) {
8457
- if (block.type === "tool_use") ids.add(block.id);
8458
- }
8459
- return ids;
8460
- }
8461
- function toolResultIds(msg) {
8462
- const ids = /* @__PURE__ */ new Set();
8463
- if (!msg || msg.role !== "user") return ids;
8464
- for (const block of contentBlocks(msg)) {
8465
- if (block.type === "tool_result") ids.add(block.tool_use_id);
8466
- }
8467
- return ids;
8468
- }
8469
- function contentBlocks(msg) {
8470
- return msg && Array.isArray(msg.content) ? msg.content : [];
8471
- }
8472
- function mapContent(msg, fn) {
8473
- if (!Array.isArray(msg.content)) return msg;
8474
- const next = fn(msg.content);
8475
- if (next.length === msg.content.length && next.every((b, idx) => b === msg.content[idx])) {
8476
- return msg;
8477
- }
8478
- return { ...msg, content: next };
8479
- }
8480
- function isEmptyMessage(msg) {
8481
- if (typeof msg.content === "string") return msg.content.trim().length === 0;
8482
- return msg.content.length === 0;
8483
- }
8484
-
8485
- // src/storage/session-store.ts
8486
- function sanitizeModel(model) {
8487
- return model.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
8488
- }
8489
- function generateSessionId(startedAt, model) {
8490
- const date = startedAt.slice(0, 10);
8491
- const time = startedAt.slice(11, 19).replace(/:/g, "-");
8492
- const suffix = randomBytes(2).toString("hex");
8493
- const modelPart = model ? `_${sanitizeModel(model)}` : "";
8494
- return `${date}/${time}Z${modelPart}_${suffix}`;
8689
+ function generateSessionId(startedAt, model) {
8690
+ const date = startedAt.slice(0, 10);
8691
+ const time = startedAt.slice(11, 19).replace(/:/g, "-");
8692
+ const suffix = randomBytes(2).toString("hex");
8693
+ const modelPart = model ? `_${sanitizeModel(model)}` : "";
8694
+ return `${date}/${time}Z${modelPart}_${suffix}`;
8495
8695
  }
8496
8696
  var DefaultSessionStore = class _DefaultSessionStore {
8497
8697
  dir;
8498
8698
  events;
8499
8699
  secretScrubber;
8700
+ /**
8701
+ * In-memory cache for load() results, keyed by session ID. The cache is
8702
+ * invalidated when the file's mtimeMs or size changes (indicating the
8703
+ * file was written to). This eliminates redundant full-file reads and
8704
+ * JSON parses when the same session is loaded multiple times within the
8705
+ * store's lifetime (e.g., webui session detail views, list() fallbacks).
8706
+ *
8707
+ * Max size is capped to prevent unbounded memory growth in long-running
8708
+ * processes. When the limit is reached, the oldest entry is evicted.
8709
+ */
8710
+ _loadCache = /* @__PURE__ */ new Map();
8711
+ static LOAD_CACHE_MAX_ENTRIES = 50;
8500
8712
  constructor(opts) {
8501
8713
  this.dir = opts.dir;
8502
8714
  this.events = opts.events;
8503
8715
  this.secretScrubber = opts.secretScrubber;
8504
8716
  }
8717
+ /**
8718
+ * Clear the load() cache. Useful for testing or when the caller knows
8719
+ * the file has changed externally (e.g., another process wrote to it).
8720
+ */
8721
+ clearLoadCache(sessionId) {
8722
+ if (sessionId !== void 0) {
8723
+ this._loadCache.delete(sessionId);
8724
+ } else {
8725
+ this._loadCache.clear();
8726
+ }
8727
+ }
8505
8728
  // ── Storage event helpers ───────────────────────────────────────────────────
8506
8729
  emitRead(sessionId, filePath, operation, outcome, durationMs, error) {
8507
8730
  this.events?.emit("storage.read", {
@@ -8538,11 +8761,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
8538
8761
  }
8539
8762
  /** Absolute path to the session index file. */
8540
8763
  get indexFile() {
8541
- return path4.join(this.dir, "_index.jsonl");
8764
+ return path5.join(this.dir, "_index.jsonl");
8542
8765
  }
8543
8766
  /** Join session ID to its absolute path within the store directory. */
8544
8767
  sessionPath(id, ext) {
8545
- return path4.join(this.dir, `${id}${ext}`);
8768
+ return path5.join(this.dir, `${id}${ext}`);
8546
8769
  }
8547
8770
  /**
8548
8771
  * Ensure the directory implied by the session ID exists. When the ID
@@ -8550,7 +8773,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
8550
8773
  * subdirectory so sessions group naturally by day.
8551
8774
  */
8552
8775
  async ensureShardDir(id) {
8553
- const dirPath = path4.dirname(path4.join(this.dir, id));
8776
+ const dirPath = path5.dirname(path5.join(this.dir, id));
8554
8777
  await ensureDir(dirPath);
8555
8778
  return dirPath;
8556
8779
  }
@@ -8558,15 +8781,15 @@ var DefaultSessionStore = class _DefaultSessionStore {
8558
8781
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
8559
8782
  const id = meta.id && meta.id.length > 0 ? meta.id : generateSessionId(startedAt, meta.model ?? meta.provider);
8560
8783
  const shardDir = await this.ensureShardDir(id);
8561
- const file = path4.join(shardDir, `${path4.basename(id)}.jsonl`);
8784
+ const file = path5.join(shardDir, `${path5.basename(id)}.jsonl`);
8562
8785
  const t0 = Date.now();
8563
8786
  let handle;
8564
8787
  try {
8565
8788
  handle = await fsp6.open(file, "a", 384);
8566
8789
  } catch (err) {
8567
- this.emitError(id, file, "create", err instanceof Error ? err.message : String(err), false);
8790
+ this.emitError(id, file, "create", toErrorMessage(err), false);
8568
8791
  throw new Error(
8569
- `Failed to open session file: ${err instanceof Error ? err.message : String(err)}`,
8792
+ `Failed to open session file: ${toErrorMessage(err)}`,
8570
8793
  { cause: err }
8571
8794
  );
8572
8795
  }
@@ -8586,7 +8809,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
8586
8809
  message: e instanceof Error ? e.message : String(e),
8587
8810
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
8588
8811
  })));
8589
- this.emitError(id, file, "create", err instanceof Error ? err.message : String(err), true);
8812
+ this.emitError(id, file, "create", toErrorMessage(err), true);
8590
8813
  throw err;
8591
8814
  }
8592
8815
  }
@@ -8598,9 +8821,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
8598
8821
  try {
8599
8822
  handle = await fsp6.open(file, "a", 384);
8600
8823
  } catch (err) {
8601
- this.emitError(id, file, "resume", err instanceof Error ? err.message : String(err), false);
8824
+ this.emitError(id, file, "resume", toErrorMessage(err), false);
8602
8825
  throw new Error(
8603
- `Failed to open session "${id}" for append: ${err instanceof Error ? err.message : String(err)}`,
8826
+ `Failed to open session "${id}" for append: ${toErrorMessage(err)}`,
8604
8827
  { cause: err }
8605
8828
  );
8606
8829
  }
@@ -8620,7 +8843,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
8620
8843
  // Shard directory (sessions/<date>/) — must match create() so the
8621
8844
  // .summary.json sidecar lands next to the JSONL instead of the
8622
8845
  // sessions root (where summaryFor() would never find it).
8623
- dir: path4.dirname(file),
8846
+ dir: path5.dirname(file),
8624
8847
  filePath: file,
8625
8848
  secretScrubber: this.secretScrubber,
8626
8849
  onClose: (s) => this.appendToIndex(s)
@@ -8635,7 +8858,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
8635
8858
  message: e instanceof Error ? e.message : String(e),
8636
8859
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
8637
8860
  })));
8638
- this.emitError(id, file, "resume", err instanceof Error ? err.message : String(err), true);
8861
+ this.emitError(id, file, "resume", toErrorMessage(err), true);
8639
8862
  throw err;
8640
8863
  }
8641
8864
  }
@@ -8644,7 +8867,20 @@ var DefaultSessionStore = class _DefaultSessionStore {
8644
8867
  const t0 = Date.now();
8645
8868
  let outcome = "success";
8646
8869
  let errorMsg;
8870
+ let cacheHit = false;
8647
8871
  try {
8872
+ let stat6;
8873
+ try {
8874
+ const s = await fsp6.stat(file);
8875
+ stat6 = { mtimeMs: s.mtimeMs, size: s.size };
8876
+ } catch (err) {
8877
+ throw err;
8878
+ }
8879
+ const cached = this._loadCache.get(id);
8880
+ if (cached && cached.mtimeMs === stat6.mtimeMs && cached.size === stat6.size) {
8881
+ cacheHit = true;
8882
+ return cached.data;
8883
+ }
8648
8884
  const raw = await fsp6.readFile(file, "utf8");
8649
8885
  const lines = raw.split("\n").filter((l) => l.trim());
8650
8886
  const events = [];
@@ -8660,13 +8896,30 @@ var DefaultSessionStore = class _DefaultSessionStore {
8660
8896
  const meta = this.metaFromEvents(id, events);
8661
8897
  const { messages, usage } = this.replay(events, id);
8662
8898
  const toolCallEnds = extractToolCallEnds(events);
8663
- return { metadata: meta, events, messages, usage, toolCallEnds };
8899
+ const data = { metadata: meta, events, messages, usage, toolCallEnds };
8900
+ if (this._loadCache.size >= _DefaultSessionStore.LOAD_CACHE_MAX_ENTRIES) {
8901
+ const oldest = this._loadCache.keys().next().value;
8902
+ if (oldest !== void 0) {
8903
+ this._loadCache.delete(oldest);
8904
+ }
8905
+ }
8906
+ this._loadCache.set(id, { mtimeMs: stat6.mtimeMs, size: stat6.size, data });
8907
+ return data;
8664
8908
  } catch (err) {
8665
8909
  outcome = "failure";
8666
- errorMsg = err instanceof Error ? err.message : String(err);
8910
+ errorMsg = toErrorMessage(err);
8667
8911
  throw err;
8668
8912
  } finally {
8669
8913
  this.emitRead(id, file, "load", outcome, Date.now() - t0, errorMsg);
8914
+ if (cacheHit) {
8915
+ this.events?.emit("storage.cache_hit", {
8916
+ sessionId: id,
8917
+ store: "session",
8918
+ filePath: file,
8919
+ operation: "load",
8920
+ durationMs: Date.now() - t0
8921
+ });
8922
+ }
8670
8923
  }
8671
8924
  }
8672
8925
  async list(limit = 20) {
@@ -8746,7 +8999,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
8746
8999
  await fsp6.rename(tmp, this.indexFile);
8747
9000
  } catch (err) {
8748
9001
  outcome = "failure";
8749
- errorMsg = err instanceof Error ? err.message : String(err);
9002
+ errorMsg = toErrorMessage(err);
8750
9003
  } finally {
8751
9004
  this.emitWrite("~compact~", this.indexFile, "compact", outcome, Date.now() - t0, void 0, errorMsg);
8752
9005
  }
@@ -8814,7 +9067,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
8814
9067
  continue;
8815
9068
  if (entry.isDirectory()) {
8816
9069
  const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
8817
- ids.push(...await this.collectSessionIds(path4.join(dir, entry.name), childPrefix, depth + 1));
9070
+ ids.push(...await this.collectSessionIds(path5.join(dir, entry.name), childPrefix, depth + 1));
8818
9071
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
8819
9072
  if (entry.name === "_index.jsonl") continue;
8820
9073
  const base = entry.name.replace(/\.jsonl$/, "");
@@ -8834,10 +9087,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
8834
9087
  return JSON.parse(raw);
8835
9088
  } catch {
8836
9089
  const full = this.sessionPath(id, ".jsonl");
8837
- const stat5 = await fsp6.stat(full);
8838
- const summary = await this.summarize(id, stat5.mtime.toISOString());
9090
+ const stat6 = await fsp6.stat(full);
9091
+ const summary = await this.summarize(id, stat6.mtime.toISOString());
8839
9092
  await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
8840
- const msg = err instanceof Error ? err.message : String(err);
9093
+ const msg = toErrorMessage(err);
8841
9094
  this.emitError(id, manifest, "summary_fallback", msg, true);
8842
9095
  console.warn(JSON.stringify({
8843
9096
  level: "warn",
@@ -8865,14 +9118,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
8865
9118
  async deleteSession(id) {
8866
9119
  const jsonlPath = this.sessionPath(id, ".jsonl");
8867
9120
  const summaryPath = this.sessionPath(id, ".summary.json");
8868
- const shardDir = path4.dirname(path4.join(this.dir, id));
8869
- const base = path4.basename(id);
8870
- const sessDir = path4.join(shardDir, base);
9121
+ const shardDir = path5.dirname(path5.join(this.dir, id));
9122
+ const base = path5.basename(id);
9123
+ const sessDir = path5.join(shardDir, base);
8871
9124
  const deletions = [
8872
9125
  fsp6.unlink(jsonlPath),
8873
9126
  fsp6.unlink(summaryPath),
8874
- fsp6.unlink(path4.join(shardDir, `${base}.plan.json`)),
8875
- fsp6.unlink(path4.join(shardDir, `${base}.todos.json`))
9127
+ fsp6.unlink(path5.join(shardDir, `${base}.plan.json`)),
9128
+ fsp6.unlink(path5.join(shardDir, `${base}.todos.json`))
8876
9129
  ];
8877
9130
  const results = await Promise.allSettled(deletions);
8878
9131
  for (const r of results) {
@@ -8894,7 +9147,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
8894
9147
  level: "warn",
8895
9148
  event: "session_store.rmdir_failed",
8896
9149
  sessionId: id,
8897
- message: err instanceof Error ? err.message : String(err),
9150
+ message: toErrorMessage(err),
8898
9151
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
8899
9152
  }));
8900
9153
  });
@@ -8908,17 +9161,17 @@ var DefaultSessionStore = class _DefaultSessionStore {
8908
9161
  let deleted = 0;
8909
9162
  let activeSessionId = null;
8910
9163
  try {
8911
- const raw = await fsp6.readFile(path4.join(this.dir, "active.json"), "utf8");
9164
+ const raw = await fsp6.readFile(path5.join(this.dir, "active.json"), "utf8");
8912
9165
  const active = JSON.parse(raw);
8913
9166
  activeSessionId = active.sessionId ?? null;
8914
9167
  } catch {
8915
9168
  }
8916
9169
  const isPrunableJsonl = (name) => name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
8917
9170
  const pruneFile = async (dir, name, prefix) => {
8918
- const jsonlPath = path4.join(dir, name);
9171
+ const jsonlPath = path5.join(dir, name);
8919
9172
  try {
8920
- const stat5 = await fsp6.stat(jsonlPath);
8921
- if (stat5.mtimeMs >= cutoff) return;
9173
+ const stat6 = await fsp6.stat(jsonlPath);
9174
+ if (stat6.mtimeMs >= cutoff) return;
8922
9175
  } catch {
8923
9176
  return;
8924
9177
  }
@@ -8935,7 +9188,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
8935
9188
  continue;
8936
9189
  }
8937
9190
  if (!entry.isDirectory()) continue;
8938
- const dateDir = path4.join(this.dir, entry.name);
9191
+ const dateDir = path5.join(this.dir, entry.name);
8939
9192
  const files = await fsp6.readdir(dateDir, { withFileTypes: true }).catch(() => []);
8940
9193
  for (const file of files) {
8941
9194
  if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
@@ -8947,7 +9200,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
8947
9200
  }
8948
9201
  for (const entry of entries) {
8949
9202
  if (!entry.isDirectory()) continue;
8950
- const dateDir = path4.join(this.dir, entry.name);
9203
+ const dateDir = path5.join(this.dir, entry.name);
8951
9204
  try {
8952
9205
  const remaining = await fsp6.readdir(dateDir);
8953
9206
  if (remaining.length === 0) {
@@ -9122,7 +9375,7 @@ var FileSessionWriter = class _FileSessionWriter {
9122
9375
  this.meta = meta;
9123
9376
  this.events = events;
9124
9377
  this.resumed = opts.resumed ?? false;
9125
- this.manifestFile = opts.dir ? path4.join(opts.dir, `${path4.basename(id)}.summary.json`) : "";
9378
+ this.manifestFile = opts.dir ? path5.join(opts.dir, `${path5.basename(id)}.summary.json`) : "";
9126
9379
  this.filePath = opts.filePath ?? "";
9127
9380
  this.secretScrubber = opts.secretScrubber;
9128
9381
  this.onCloseCb = opts.onClose;
@@ -9329,7 +9582,7 @@ var FileSessionWriter = class _FileSessionWriter {
9329
9582
  await this.enqueueWrite(batch);
9330
9583
  } catch (err) {
9331
9584
  outcome = "failure";
9332
- errorMsg = err instanceof Error ? err.message : String(err);
9585
+ errorMsg = toErrorMessage(err);
9333
9586
  this.appendFailCount += eventCount;
9334
9587
  const now = Date.now();
9335
9588
  if (now - this.lastAppendWarnAt > 5e3) {
@@ -9337,7 +9590,7 @@ var FileSessionWriter = class _FileSessionWriter {
9337
9590
  const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
9338
9591
  console.warn(
9339
9592
  "[session] flush failed:",
9340
- err instanceof Error ? err.message : String(err),
9593
+ toErrorMessage(err),
9341
9594
  tail
9342
9595
  );
9343
9596
  this.lastAppendWarnAt = now;
@@ -9427,7 +9680,7 @@ var FileSessionWriter = class _FileSessionWriter {
9427
9680
  await atomicWrite(this.manifestFile, JSON.stringify(this.summary), { mode: 384 });
9428
9681
  } catch (err) {
9429
9682
  outcome = "failure";
9430
- errorMsg = err instanceof Error ? err.message : String(err);
9683
+ errorMsg = toErrorMessage(err);
9431
9684
  } finally {
9432
9685
  this.events?.emit("storage.write", {
9433
9686
  sessionId: this.id,
@@ -9448,7 +9701,7 @@ var FileSessionWriter = class _FileSessionWriter {
9448
9701
  await this.onCloseCb?.(this.summary);
9449
9702
  } catch (err) {
9450
9703
  idxOutcome = "failure";
9451
- idxError = err instanceof Error ? err.message : String(err);
9704
+ idxError = toErrorMessage(err);
9452
9705
  } finally {
9453
9706
  this.events?.emit("storage.write", {
9454
9707
  sessionId: this.summary.id,
@@ -9625,9 +9878,9 @@ function makeDirectorSessionFactory(opts) {
9625
9878
  let dir;
9626
9879
  if (opts.store) {
9627
9880
  store = opts.store;
9628
- dir = opts.sessionsRoot ? path4.join(opts.sessionsRoot, runId) : "(caller-managed)";
9881
+ dir = opts.sessionsRoot ? path5.join(opts.sessionsRoot, runId) : "(caller-managed)";
9629
9882
  } else if (opts.sessionsRoot) {
9630
- dir = path4.join(opts.sessionsRoot, runId);
9883
+ dir = path5.join(opts.sessionsRoot, runId);
9631
9884
  store = new DefaultSessionStore({ dir });
9632
9885
  } else {
9633
9886
  throw new Error("makeDirectorSessionFactory requires either `store` or `sessionsRoot`");
@@ -9673,6 +9926,7 @@ function attachAutoExtend(events, policy = {}) {
9673
9926
  const extendCounts = /* @__PURE__ */ new Map();
9674
9927
  let progress = 0;
9675
9928
  let lastTimeoutProgress = -1;
9929
+ let lastSeenKey = null;
9676
9930
  const unsubs = [
9677
9931
  events.on("tool.executed", () => {
9678
9932
  progress++;
@@ -9682,6 +9936,9 @@ function attachAutoExtend(events, policy = {}) {
9682
9936
  }),
9683
9937
  events.on("budget.threshold_reached", (e) => {
9684
9938
  const { kind, limit, extend, deny } = e;
9939
+ const key = `${kind}:${limit}`;
9940
+ if (key === lastSeenKey) return;
9941
+ lastSeenKey = key;
9685
9942
  if (kind === "timeout" || kind === "idle_timeout") {
9686
9943
  if (progress > lastTimeoutProgress) {
9687
9944
  lastTimeoutProgress = progress;
@@ -9935,7 +10192,7 @@ var FleetManager = class {
9935
10192
  })),
9936
10193
  usage: this.usage.snapshot()
9937
10194
  };
9938
- await fsp6.mkdir(path4.dirname(this.manifestPath), { recursive: true });
10195
+ await fsp6.mkdir(path5.dirname(this.manifestPath), { recursive: true });
9939
10196
  await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
9940
10197
  return this.manifestPath;
9941
10198
  }
@@ -9956,7 +10213,7 @@ var FleetManager = class {
9956
10213
  if (!this.manifestPath) return;
9957
10214
  if (this.manifestDebounceMs === 0) {
9958
10215
  void this.writeManifest().catch((err) => {
9959
- const detail = err instanceof Error ? err.message : String(err);
10216
+ const detail = toErrorMessage(err);
9960
10217
  process.emitWarning(
9961
10218
  `FleetManager manifest write failed: ${detail}`,
9962
10219
  "FleetManagerWarning"
@@ -9969,7 +10226,7 @@ var FleetManager = class {
9969
10226
  this.manifestTimer = setTimeout(() => {
9970
10227
  this.manifestTimer = null;
9971
10228
  void this.writeManifest().catch((err) => {
9972
- const detail = err instanceof Error ? err.message : String(err);
10229
+ const detail = toErrorMessage(err);
9973
10230
  process.emitWarning(
9974
10231
  `FleetManager manifest write failed: ${detail}`,
9975
10232
  "FleetManagerWarning"
@@ -9988,7 +10245,7 @@ var FleetManager = class {
9988
10245
  this.manifestTimer = null;
9989
10246
  }
9990
10247
  await this.writeManifest().catch((err) => {
9991
- const detail = err instanceof Error ? err.message : String(err);
10248
+ const detail = toErrorMessage(err);
9992
10249
  process.emitWarning(
9993
10250
  `FleetManager manifest write failed: ${detail}`,
9994
10251
  "FleetManagerWarning"
@@ -10083,7 +10340,7 @@ var LINE_SEPARATOR = "\n";
10083
10340
  var DefaultMailbox = class {
10084
10341
  filePath;
10085
10342
  constructor(sessionDir) {
10086
- this.filePath = path4.join(sessionDir, MAILBOX_FILE);
10343
+ this.filePath = path5.join(sessionDir, MAILBOX_FILE);
10087
10344
  }
10088
10345
  get mailboxPath() {
10089
10346
  return this.filePath;
@@ -10108,7 +10365,7 @@ var DefaultMailbox = class {
10108
10365
  taskContext: input.taskContext
10109
10366
  };
10110
10367
  const line = JSON.stringify(msg) + LINE_SEPARATOR;
10111
- await fsp6.mkdir(path4.dirname(this.filePath), { recursive: true });
10368
+ await fsp6.mkdir(path5.dirname(this.filePath), { recursive: true });
10112
10369
  await withFileLock(this.filePath, async () => {
10113
10370
  await fsp6.appendFile(this.filePath, line, "utf8");
10114
10371
  });
@@ -10148,29 +10405,43 @@ var DefaultMailbox = class {
10148
10405
  }
10149
10406
  // ── Ack ───────────────────────────────────────────────────────────────
10150
10407
  async ack(input) {
10151
- let result = null;
10408
+ const updated = await this.ackMany({ acks: [input] });
10409
+ return updated.length > 0 ? updated[0] : null;
10410
+ }
10411
+ async ackMany(input) {
10412
+ if (input.acks.length === 0) return [];
10413
+ const updated = [];
10414
+ const byId = /* @__PURE__ */ new Map();
10415
+ for (const a of input.acks) byId.set(a.messageId, a);
10152
10416
  await withFileLock(this.filePath, async () => {
10153
10417
  const all = await this._readAll();
10154
- const idx = all.findIndex((m) => m.id === input.messageId);
10155
- if (idx === -1) return;
10156
- const msg = all[idx];
10157
10418
  const now = (/* @__PURE__ */ new Date()).toISOString();
10158
- if (input.read !== false) {
10159
- msg.readBy[input.readerId] = now;
10160
- }
10161
- if (input.completed) {
10162
- msg.completed = true;
10163
- msg.completedBy = input.readerId;
10164
- msg.completedAt = now;
10419
+ let changed = false;
10420
+ for (const msg of all) {
10421
+ const a = byId.get(msg.id);
10422
+ if (!a) continue;
10423
+ updated.push(msg);
10424
+ if (a.read !== false && !(a.readerId in msg.readBy)) {
10425
+ msg.readBy[a.readerId] = now;
10426
+ changed = true;
10427
+ }
10428
+ if (a.completed && !msg.completed) {
10429
+ msg.completed = true;
10430
+ msg.completedBy = a.readerId;
10431
+ msg.completedAt = now;
10432
+ changed = true;
10433
+ }
10434
+ if (a.outcome !== void 0 && msg.outcome !== a.outcome) {
10435
+ msg.outcome = a.outcome;
10436
+ changed = true;
10437
+ }
10165
10438
  }
10166
- if (input.outcome !== void 0) {
10167
- msg.outcome = input.outcome;
10439
+ if (changed) {
10440
+ const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
10441
+ await fsp6.writeFile(this.filePath, serialized, "utf8");
10168
10442
  }
10169
- const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
10170
- await fsp6.writeFile(this.filePath, serialized, "utf8");
10171
- result = msg;
10172
10443
  });
10173
- return result;
10444
+ return updated;
10174
10445
  }
10175
10446
  // ── Agent statuses ────────────────────────────────────────────────────
10176
10447
  async getAgentStatuses() {
@@ -10222,6 +10493,43 @@ var DefaultMailbox = class {
10222
10493
  await fsp6.writeFile(this.filePath, "", "utf8");
10223
10494
  });
10224
10495
  }
10496
+ async purgeStale(opts) {
10497
+ const COMPLETED_MAX_AGE_MS = opts?.completedMaxAgeMs ?? 864e5;
10498
+ const INCOMPLETE_MAX_AGE_MS = opts?.incompleteMaxAgeMs ?? 6048e5;
10499
+ let completedPurged = 0;
10500
+ let incompletePurged = 0;
10501
+ await withFileLock(this.filePath, async () => {
10502
+ const all2 = await this._readAll();
10503
+ const now = Date.now();
10504
+ const cutoffCompleted = now - COMPLETED_MAX_AGE_MS;
10505
+ const cutoffIncomplete = now - INCOMPLETE_MAX_AGE_MS;
10506
+ const kept = [];
10507
+ for (const msg of all2) {
10508
+ const msgTime = new Date(msg.timestamp).getTime();
10509
+ const completedTime = msg.completedAt ? new Date(msg.completedAt).getTime() : 0;
10510
+ if (msg.completed && completedTime < cutoffCompleted) {
10511
+ completedPurged++;
10512
+ continue;
10513
+ }
10514
+ if (!msg.completed && msgTime < cutoffIncomplete) {
10515
+ incompletePurged++;
10516
+ continue;
10517
+ }
10518
+ kept.push(msg);
10519
+ }
10520
+ if (kept.length < all2.length) {
10521
+ const content = kept.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
10522
+ await fsp6.writeFile(this.filePath, content, "utf8");
10523
+ }
10524
+ });
10525
+ const all = await this._readAll();
10526
+ return {
10527
+ completedPurged,
10528
+ incompletePurged,
10529
+ totalPurged: completedPurged + incompletePurged,
10530
+ remaining: all.length
10531
+ };
10532
+ }
10225
10533
  // ── Client registry stubs (not applicable per-session) ─────────────────
10226
10534
  async registerClient(_input) {
10227
10535
  }
@@ -10340,7 +10648,8 @@ var BrainMonitor = class {
10340
10648
  id: "steer",
10341
10649
  label: "Steer the agent with corrective guidance",
10342
10650
  consequence: "A steer message is injected before its next step.",
10343
- risk: "low"
10651
+ risk: "low",
10652
+ recommended: true
10344
10653
  },
10345
10654
  {
10346
10655
  id: "continue",
@@ -10349,9 +10658,9 @@ var BrainMonitor = class {
10349
10658
  }
10350
10659
  ],
10351
10660
  risk: "medium",
10352
- // Without an LLM layer the policy brain resolves this fallback to
10353
- // "continue" the monitor observes but never interferes.
10354
- fallback: "continue"
10661
+ // 'ask_human' routes to the LLM-backed autonomous layer via
10662
+ // createTieredBrainArbiter before any human escalation.
10663
+ fallback: "ask_human"
10355
10664
  };
10356
10665
  const decision = await this.opts.brain.decide(request);
10357
10666
  const intervened = await this.maybeIntervene(kind, request, decision);
@@ -10390,21 +10699,6 @@ var BrainMonitor = class {
10390
10699
  }
10391
10700
  }
10392
10701
  };
10393
- function projectSlug(absRoot) {
10394
- const base = slugify(path4.basename(absRoot));
10395
- const hash = createHash("sha256").update(path4.resolve(absRoot)).digest("hex").slice(0, 6);
10396
- return `${base}-${hash}`;
10397
- }
10398
- function slugify(name) {
10399
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
10400
- }
10401
- function wstackGlobalRoot() {
10402
- const fromEnv = process.env["WRONGSTACK_HOME"];
10403
- if (fromEnv && fromEnv.trim().length > 0) return path4.resolve(fromEnv);
10404
- return path4.join(os.homedir(), ".wrongstack");
10405
- }
10406
-
10407
- // src/coordination/global-mailbox.ts
10408
10702
  var MAILBOX_FILE2 = "_mailbox.jsonl";
10409
10703
  var CLIENT_REGISTRY_FILE = "_mailbox.clients.json";
10410
10704
  var AGENT_STALE_MS = 6e4;
@@ -10412,8 +10706,9 @@ var CLIENT_STALE_MS = 6e4;
10412
10706
  var HEARTBEAT_THROTTLE_MS = 5e3;
10413
10707
  var REGISTRY_CACHE_TTL_MS = 2e3;
10414
10708
  var LINE_SEPARATOR2 = "\n";
10709
+ var MESSAGE_CACHE_MAX_ENTRIES = 1e4;
10415
10710
  function resolveProjectDir(projectRoot, globalRoot) {
10416
- return path4.join(globalRoot, "projects", projectSlug(projectRoot));
10711
+ return path5.join(globalRoot, "projects", projectSlug(projectRoot));
10417
10712
  }
10418
10713
  var GlobalMailbox = class {
10419
10714
  /** Path to the JSONL message file. */
@@ -10444,14 +10739,28 @@ var GlobalMailbox = class {
10444
10739
  _lastHeartbeat = /* @__PURE__ */ new Map();
10445
10740
  /** Last time each local client sent a heartbeat (throttle). */
10446
10741
  _lastClientHeartbeat = /* @__PURE__ */ new Map();
10742
+ /**
10743
+ * In-memory mirror of the JSONL message file. The mailbox is shared
10744
+ * ACROSS PROCESSES, so reads cannot trust the cache blindly — we pair it
10745
+ * with an mtime check. The file lock serializes every write, so a
10746
+ * changed mtimeMs is a definitive signal that another process (or this
10747
+ * one) wrote; an unchanged mtimeMs guarantees no write happened and the
10748
+ * cache is current. This collapses the per-iteration `query()` cost from
10749
+ * O(file_size) disk + parse to O(messages) in memory.
10750
+ */
10751
+ _messageCache = null;
10752
+ /** mtimeMs of the file when `_messageCache` was populated. */
10753
+ _messageCacheMtime = -1;
10754
+ /** Size of the file when `_messageCache` was populated (extra guard). */
10755
+ _messageCacheSize = -1;
10447
10756
  /**
10448
10757
  * @param projectDir — `~/.wrongstack/projects/<slug>/`
10449
10758
  * @param events — optional EventBus for real-time TUI/WebUI notifications
10450
10759
  */
10451
10760
  constructor(projectDir, events) {
10452
- this.messagePath = path4.join(projectDir, MAILBOX_FILE2);
10453
- this.registryPath = path4.join(projectDir, "_mailbox.registry.json");
10454
- this.clientRegistryPath = path4.join(projectDir, CLIENT_REGISTRY_FILE);
10761
+ this.messagePath = path5.join(projectDir, MAILBOX_FILE2);
10762
+ this.registryPath = path5.join(projectDir, "_mailbox.registry.json");
10763
+ this.clientRegistryPath = path5.join(projectDir, CLIENT_REGISTRY_FILE);
10455
10764
  this._events = events;
10456
10765
  }
10457
10766
  // ── Messages ────────────────────────────────────────────────────────────
@@ -10474,73 +10783,89 @@ var GlobalMailbox = class {
10474
10783
  taskContext: input.taskContext
10475
10784
  };
10476
10785
  const line = JSON.stringify(msg) + LINE_SEPARATOR2;
10477
- await fsp6.mkdir(path4.dirname(this.messagePath), { recursive: true });
10786
+ await fsp6.mkdir(path5.dirname(this.messagePath), { recursive: true });
10478
10787
  await withFileLock(this.messagePath, async () => {
10479
10788
  await fsp6.appendFile(this.messagePath, line, "utf8");
10789
+ this._pushToCache(msg);
10480
10790
  });
10481
10791
  return msg;
10482
10792
  }
10483
10793
  async query(q) {
10484
- const all = await this._readMessages();
10794
+ const all = await this._readMessagesCached();
10485
10795
  const limit = q.limit ?? 50;
10486
- let filtered = all;
10487
- if (q.to !== void 0) {
10488
- filtered = filtered.filter((m) => m.to === q.to || m.to === "*");
10489
- }
10490
- if (q.from !== void 0) {
10491
- filtered = filtered.filter((m) => m.from === q.from);
10492
- }
10493
- if (q.unreadBy !== void 0) {
10494
- filtered = filtered.filter((m) => !(q.unreadBy in m.readBy));
10495
- }
10496
- if (q.incompleteOnly) {
10497
- filtered = filtered.filter((m) => !m.completed);
10498
- }
10499
- if (q.type !== void 0) {
10500
- filtered = filtered.filter((m) => m.type === q.type);
10501
- }
10502
- if (q.minPriority !== void 0) {
10503
- const order = { low: 0, normal: 1, high: 2 };
10504
- const min = order[q.minPriority];
10505
- filtered = filtered.filter((m) => (order[m.priority] ?? 1) >= min);
10506
- }
10507
- if (q.since !== void 0) {
10508
- const since = q.since;
10509
- filtered = filtered.filter((m) => m.timestamp > since);
10796
+ const order = q.minPriority !== void 0 ? { low: 0, normal: 1, high: 2 } : null;
10797
+ const minPriorityRank = order && q.minPriority !== void 0 ? order[q.minPriority] : 0;
10798
+ const out = [];
10799
+ for (let i = 0; i < all.length; i++) {
10800
+ const m = all[i];
10801
+ if (q.to !== void 0 && m.to !== q.to && m.to !== "*") continue;
10802
+ if (q.from !== void 0 && m.from !== q.from) continue;
10803
+ if (q.unreadBy !== void 0 && q.unreadBy in m.readBy) continue;
10804
+ if (q.incompleteOnly && m.completed) continue;
10805
+ if (q.type !== void 0 && m.type !== q.type) continue;
10806
+ if (order !== null && (order[m.priority] ?? 1) < minPriorityRank) {
10807
+ continue;
10808
+ }
10809
+ if (q.since !== void 0 && m.timestamp <= q.since) continue;
10810
+ out.push(m);
10510
10811
  }
10511
- filtered.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
10512
- return filtered.slice(0, limit);
10812
+ out.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
10813
+ return out.slice(0, limit).map((m) => ({ ...m, readBy: { ...m.readBy } }));
10513
10814
  }
10514
10815
  async ack(input) {
10515
- let result = null;
10816
+ const updated = await this.ackMany({ acks: [input] });
10817
+ return updated.length > 0 ? updated[0] : null;
10818
+ }
10819
+ async ackMany(input) {
10820
+ if (input.acks.length === 0) return [];
10821
+ const updated = [];
10822
+ const byId = /* @__PURE__ */ new Map();
10823
+ for (const a of input.acks) {
10824
+ byId.set(a.messageId, a);
10825
+ }
10826
+ let cacheSnapshot = null;
10516
10827
  await withFileLock(this.messagePath, async () => {
10517
- const all = await this._readMessages();
10518
- const idx = all.findIndex((m) => m.id === input.messageId);
10519
- if (idx === -1) return;
10520
- const msg = all[idx];
10828
+ const all = await this._readMessagesFresh();
10521
10829
  const now = (/* @__PURE__ */ new Date()).toISOString();
10522
- if (input.read !== false) {
10523
- msg.readBy[input.readerId] = now;
10524
- }
10525
- if (input.completed) {
10526
- msg.completed = true;
10527
- msg.completedBy = input.readerId;
10528
- msg.completedAt = now;
10830
+ let changed = false;
10831
+ for (const msg of all) {
10832
+ const a = byId.get(msg.id);
10833
+ if (!a) continue;
10834
+ updated.push(msg);
10835
+ if (a.read !== false && !(a.readerId in msg.readBy)) {
10836
+ msg.readBy[a.readerId] = now;
10837
+ changed = true;
10838
+ }
10839
+ if (a.completed && !msg.completed) {
10840
+ msg.completed = true;
10841
+ msg.completedBy = a.readerId;
10842
+ msg.completedAt = now;
10843
+ changed = true;
10844
+ }
10845
+ if (a.outcome !== void 0 && msg.outcome !== a.outcome) {
10846
+ msg.outcome = a.outcome;
10847
+ changed = true;
10848
+ }
10529
10849
  }
10530
- if (input.outcome !== void 0) {
10531
- msg.outcome = input.outcome;
10850
+ if (changed) {
10851
+ const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR2) + LINE_SEPARATOR2;
10852
+ await fsp6.writeFile(this.messagePath, serialized, "utf8");
10532
10853
  }
10533
- const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR2) + LINE_SEPARATOR2;
10534
- await fsp6.writeFile(this.messagePath, serialized, "utf8");
10535
- result = msg;
10854
+ cacheSnapshot = all;
10536
10855
  });
10537
- return result;
10856
+ if (cacheSnapshot) this._setMessageCache(cacheSnapshot);
10857
+ return updated;
10538
10858
  }
10539
10859
  async unreadCount(forAgentId) {
10540
- const all = await this._readMessages();
10541
- return all.filter(
10542
- (m) => (m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed
10543
- ).length;
10860
+ const all = await this._readMessagesCached();
10861
+ let count = 0;
10862
+ for (let i = 0; i < all.length; i++) {
10863
+ const m = all[i];
10864
+ if ((m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed) {
10865
+ count++;
10866
+ }
10867
+ }
10868
+ return count;
10544
10869
  }
10545
10870
  // ── Agent registry ──────────────────────────────────────────────────────
10546
10871
  async registerAgent(input) {
@@ -10701,13 +11026,62 @@ var GlobalMailbox = class {
10701
11026
  async close() {
10702
11027
  this._registryCache = null;
10703
11028
  this._clientRegistryCache = null;
11029
+ this._messageCache = null;
11030
+ this._messageCacheMtime = -1;
11031
+ this._messageCacheSize = -1;
10704
11032
  }
10705
11033
  async clearAll() {
10706
11034
  await withFileLock(this.messagePath, async () => {
10707
11035
  await fsp6.writeFile(this.messagePath, "", "utf8");
10708
11036
  });
11037
+ this._setMessageCache([]);
11038
+ }
11039
+ async purgeStale(opts) {
11040
+ const COMPLETED_MAX_AGE_MS = opts?.completedMaxAgeMs ?? 864e5;
11041
+ const INCOMPLETE_MAX_AGE_MS = opts?.incompleteMaxAgeMs ?? 6048e5;
11042
+ let completedPurged = 0;
11043
+ let incompletePurged = 0;
11044
+ let remaining = 0;
11045
+ await withFileLock(this.messagePath, async () => {
11046
+ const all = await this._readMessagesFresh();
11047
+ const now = Date.now();
11048
+ const cutoffCompleted = now - COMPLETED_MAX_AGE_MS;
11049
+ const cutoffIncomplete = now - INCOMPLETE_MAX_AGE_MS;
11050
+ const kept = [];
11051
+ for (const msg of all) {
11052
+ const msgTime = new Date(msg.timestamp).getTime();
11053
+ const completedTime = msg.completedAt ? new Date(msg.completedAt).getTime() : 0;
11054
+ if (msg.completed && completedTime < cutoffCompleted) {
11055
+ completedPurged++;
11056
+ continue;
11057
+ }
11058
+ if (!msg.completed && msgTime < cutoffIncomplete) {
11059
+ incompletePurged++;
11060
+ continue;
11061
+ }
11062
+ kept.push(msg);
11063
+ }
11064
+ remaining = kept.length;
11065
+ if (kept.length < all.length) {
11066
+ const content = kept.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR2) + LINE_SEPARATOR2;
11067
+ await fsp6.writeFile(this.messagePath, content, "utf8");
11068
+ }
11069
+ this._setMessageCache(kept);
11070
+ });
11071
+ return {
11072
+ completedPurged,
11073
+ incompletePurged,
11074
+ totalPurged: completedPurged + incompletePurged,
11075
+ remaining
11076
+ };
10709
11077
  }
10710
11078
  // ── Internal ────────────────────────────────────────────────────────────
11079
+ /**
11080
+ * Read all messages from the JSONL file. Always reads + parses the file.
11081
+ * Callers that can tolerate a stale-by-mtime view should use
11082
+ * {@link _readMessagesCached}; writers that need the post-lock truth
11083
+ * should call this directly (it's what {@link _readMessagesFresh} aliases).
11084
+ */
10711
11085
  async _readMessages() {
10712
11086
  try {
10713
11087
  const raw = await fsp6.readFile(this.messagePath, "utf8");
@@ -10735,8 +11109,85 @@ var GlobalMailbox = class {
10735
11109
  throw err;
10736
11110
  }
10737
11111
  }
11112
+ /**
11113
+ * Read messages, then adopt the result as the in-memory cache. Use this
11114
+ * from writers that just took the file lock — the read reflects the
11115
+ * authoritative post-lock state and should be served to subsequent
11116
+ * queries without re-reading.
11117
+ */
11118
+ async _readMessagesFresh() {
11119
+ const all = await this._readMessages();
11120
+ this._setMessageCache(all);
11121
+ return all;
11122
+ }
11123
+ /**
11124
+ * Read messages, consulting the mtime-bounded in-memory cache first.
11125
+ * The mailbox file is shared across processes; every `send`/`ack`/
11126
+ * `clearAll`/`purgeStale` takes the file lock, so writes are serialized
11127
+ * and a changed mtimeMs is a definitive freshness signal. When the
11128
+ * stat matches the cached mtime+size we return the cached array — no
11129
+ * file read and no JSON.parse — collapsing the per-iteration query
11130
+ * cost on the mailbox-loop hot path.
11131
+ */
11132
+ async _readMessagesCached() {
11133
+ try {
11134
+ const st = await fsp6.stat(this.messagePath);
11135
+ if (this._messageCache !== null && this._messageCacheMtime === st.mtimeMs && this._messageCacheSize === st.size) {
11136
+ return this._messageCache;
11137
+ }
11138
+ const all = await this._readMessages();
11139
+ this._setMessageCache(all, st.mtimeMs, st.size);
11140
+ return all;
11141
+ } catch (err) {
11142
+ if (err.code === "ENOENT") {
11143
+ this._setMessageCache([], -1, -1);
11144
+ return [];
11145
+ }
11146
+ throw err;
11147
+ }
11148
+ }
11149
+ /**
11150
+ * Replace the in-memory cache. Caller is responsible for guaranteeing
11151
+ * that `messages` reflects the current on-disk state (e.g. they just
11152
+ * read or wrote it under the file lock).
11153
+ */
11154
+ _setMessageCache(messages, mtime, size) {
11155
+ if (messages.length > MESSAGE_CACHE_MAX_ENTRIES) {
11156
+ this._messageCache = null;
11157
+ this._messageCacheMtime = -1;
11158
+ this._messageCacheSize = -1;
11159
+ return;
11160
+ }
11161
+ this._messageCache = messages;
11162
+ if (mtime !== void 0 && size !== void 0) {
11163
+ this._messageCacheMtime = mtime;
11164
+ this._messageCacheSize = size;
11165
+ } else {
11166
+ void fsp6.stat(this.messagePath).then((st) => {
11167
+ this._messageCacheMtime = st.mtimeMs;
11168
+ this._messageCacheSize = st.size;
11169
+ }).catch(() => {
11170
+ });
11171
+ }
11172
+ }
11173
+ /**
11174
+ * Append a single just-sent message to the in-memory cache without
11175
+ * re-reading the file. The caller must hold the file lock (or have
11176
+ * just released it after a successful append) so the cache stays
11177
+ * consistent with on-disk state.
11178
+ */
11179
+ _pushToCache(msg) {
11180
+ if (this._messageCache === null) return;
11181
+ if (this._messageCache.length >= MESSAGE_CACHE_MAX_ENTRIES) {
11182
+ this._messageCache = null;
11183
+ this._messageCacheMtime = -1;
11184
+ this._messageCacheSize = -1;
11185
+ return;
11186
+ }
11187
+ this._messageCache.push(msg);
11188
+ }
10738
11189
  async _ensureRegistry() {
10739
- await fsp6.mkdir(path4.dirname(this.registryPath), { recursive: true });
11190
+ await fsp6.mkdir(path5.dirname(this.registryPath), { recursive: true });
10740
11191
  }
10741
11192
  async _readRegistry(opts) {
10742
11193
  if (!opts?.fresh && this._registryCache && Date.now() - this._registryCacheAt < REGISTRY_CACHE_TTL_MS) {
@@ -10781,7 +11232,7 @@ var GlobalMailbox = class {
10781
11232
  }
10782
11233
  // ── Client registry internals ───────────────────────────────────────────
10783
11234
  async _ensureClientRegistry() {
10784
- await fsp6.mkdir(path4.dirname(this.clientRegistryPath), { recursive: true });
11235
+ await fsp6.mkdir(path5.dirname(this.clientRegistryPath), { recursive: true });
10785
11236
  }
10786
11237
  async _readClientRegistry(opts) {
10787
11238
  if (!opts?.fresh && this._clientRegistryCache && Date.now() - this._clientRegistryCacheAt < REGISTRY_CACHE_TTL_MS) {
@@ -11306,6 +11757,10 @@ function makeDependencyWatcherConfig(opts) {
11306
11757
  return {
11307
11758
  watchPaths: unique,
11308
11759
  debounceMs,
11760
+ dispose() {
11761
+ for (const t of pending.values()) clearTimeout(t);
11762
+ pending.clear();
11763
+ },
11309
11764
  async onChange(entry) {
11310
11765
  if (entry.event === "delete") return;
11311
11766
  if (!matchesPattern(entry.path)) return;
@@ -11421,7 +11876,7 @@ function createMailboxHooks(opts) {
11421
11876
  var DEFAULT_MAX_ENTRIES = 1e4;
11422
11877
  var LOG_FILENAME = "package-authors.json";
11423
11878
  function logPath(storageDir) {
11424
- return path4.join(storageDir, LOG_FILENAME);
11879
+ return path5.join(storageDir, LOG_FILENAME);
11425
11880
  }
11426
11881
  async function loadLog(storageDir, projectRoot) {
11427
11882
  try {
@@ -11445,7 +11900,7 @@ async function saveLog(storageDir, log) {
11445
11900
  await fsp6.rename(tmp, logPath(storageDir));
11446
11901
  }
11447
11902
  function detectEcosystem(manifestPath) {
11448
- const name = path4.basename(manifestPath).toLowerCase();
11903
+ const name = path5.basename(manifestPath).toLowerCase();
11449
11904
  if (name === "package.json") return "npm";
11450
11905
  if (name === "go.mod") return "go";
11451
11906
  if (name === "cargo.toml") return "cargo";
@@ -11642,7 +12097,7 @@ function startPackageOutdatedWatcher(opts) {
11642
12097
  );
11643
12098
  } catch (err) {
11644
12099
  handleError(err);
11645
- log(`[pkg-outdated-watcher] Failed to notify for ${entry.name}: ${err instanceof Error ? err.message : String(err)}`);
12100
+ log(`[pkg-outdated-watcher] Failed to notify for ${entry.name}: ${toErrorMessage(err)}`);
11646
12101
  }
11647
12102
  }
11648
12103
  }
@@ -11715,7 +12170,2210 @@ mvn versions:use-latest-versions`;
11715
12170
  return `# Update ${entry.name} to ${entry.latestVersion} using your package manager`;
11716
12171
  }
11717
12172
  }
12173
+ var KnowledgeGraph = class {
12174
+ nodes = /* @__PURE__ */ new Map();
12175
+ index = /* @__PURE__ */ new Map();
12176
+ // tag/field → node ids
12177
+ subs = /* @__PURE__ */ new Map();
12178
+ pendingDeliveries = /* @__PURE__ */ new Map();
12179
+ filePath;
12180
+ graphFilePath;
12181
+ /** Exposed for unit-testing only: read current index contents. */
12182
+ getIndex() {
12183
+ return this.index;
12184
+ }
12185
+ constructor(sessionDir) {
12186
+ this.filePath = path5.join(sessionDir, "_knowledge_graph");
12187
+ this.graphFilePath = path5.join(this.filePath, "graph.jsonl");
12188
+ }
12189
+ // ── Write ──────────────────────────────────────────────────────────────
12190
+ /**
12191
+ * Add a node. Fires to all matching subscriptions synchronously.
12192
+ * Returns the node with its assigned id.
12193
+ */
12194
+ async add(node) {
12195
+ const full = { id: randomUUID(), ...node };
12196
+ this.nodes.set(full.id, full);
12197
+ this._addToIndex(full, this._indexKeys(full));
12198
+ await this._persist(full);
12199
+ this._deliver(full);
12200
+ return full;
12201
+ }
12202
+ /** Update an existing node by id. Returns updated node or null if not found. */
12203
+ async update(id, patch) {
12204
+ const existing = this.nodes.get(id);
12205
+ if (!existing) return null;
12206
+ this._removeFromIndex(existing, this._indexKeys(existing));
12207
+ const updated = { ...existing, ...patch };
12208
+ this.nodes.set(id, updated);
12209
+ this._addToIndex(updated, this._indexKeys(updated));
12210
+ this._deliver(updated);
12211
+ await this._append(updated);
12212
+ return updated;
12213
+ }
12214
+ // ── Read ───────────────────────────────────────────────────────────────
12215
+ get(id) {
12216
+ return this.nodes.get(id);
12217
+ }
12218
+ getAll(filter) {
12219
+ return Array.from(this.nodes.values()).filter((n) => this._matches(n, filter ?? {}));
12220
+ }
12221
+ getGoals(filter) {
12222
+ return this.getAll({ type: "goal", ...filter });
12223
+ }
12224
+ getFacts(filter) {
12225
+ return this.getAll({ type: "fact", ...filter });
12226
+ }
12227
+ getChanges(filter) {
12228
+ return this.getAll({ type: "change", ...filter });
12229
+ }
12230
+ getOpenGoals() {
12231
+ return this.getGoals({ status: "pending" }).concat(
12232
+ this.getGoals({ status: "in_progress" })
12233
+ );
12234
+ }
12235
+ getTopLevelGoals() {
12236
+ return this.getGoals({}).filter((g) => !g.parentGoal);
12237
+ }
12238
+ getBlockedGoals() {
12239
+ return this.getGoals({ status: "blocked" });
12240
+ }
12241
+ getPendingChanges() {
12242
+ return this.getChanges({ status: "proposed" });
12243
+ }
12244
+ getDecisions(since) {
12245
+ return this.getAll({ type: "decision", since });
12246
+ }
12247
+ // ── Search ─────────────────────────────────────────────────────────────
12248
+ searchFacts(query) {
12249
+ const q = query.toLowerCase();
12250
+ return this.getFacts().filter(
12251
+ (f) => f.subject.toLowerCase().includes(q) || f.detail.toLowerCase().includes(q) || f.file?.toLowerCase().includes(q) || f.tags.some((t) => t.toLowerCase().includes(q))
12252
+ );
12253
+ }
12254
+ getRelatedFacts(factId) {
12255
+ const fact = this.nodes.get(factId);
12256
+ if (!fact) return [];
12257
+ return fact.related.map((id) => this.nodes.get(id)).filter((n) => n?.type === "fact");
12258
+ }
12259
+ // ── Subscriptions ──────────────────────────────────────────────────────
12260
+ /**
12261
+ * Subscribe to nodes matching a filter. Returns a channel id that can be
12262
+ * used to poll for new nodes since the last check.
12263
+ */
12264
+ subscribe(agentId, filter) {
12265
+ const channel = randomUUID();
12266
+ const sub = { id: randomUUID(), agentId, filter, channel };
12267
+ this.subs.set(channel, sub);
12268
+ this.pendingDeliveries.set(channel, []);
12269
+ return channel;
12270
+ }
12271
+ /**
12272
+ * Poll for new nodes delivered to a channel since last check.
12273
+ * Clears the delivery buffer after reading.
12274
+ */
12275
+ poll(channel) {
12276
+ const pending = this.pendingDeliveries.get(channel);
12277
+ if (!pending) return [];
12278
+ const delivered = [...pending];
12279
+ pending.length = 0;
12280
+ return delivered;
12281
+ }
12282
+ unsubscribe(channel) {
12283
+ this.subs.delete(channel);
12284
+ this.pendingDeliveries.delete(channel);
12285
+ }
12286
+ // ── Quality gate helpers ───────────────────────────────────────────────
12287
+ /**
12288
+ * Create a quality gate result. Call this when a change is being proposed
12289
+ * so the change node carries the gate result.
12290
+ */
12291
+ static makeQualityGate(checks) {
12292
+ return { passed: checks.every((c) => c.passed), checks };
12293
+ }
12294
+ // ── Private ────────────────────────────────────────────────────────────
12295
+ /** Pure: compute the set of index keys a node would belong to. */
12296
+ _indexKeys(node) {
12297
+ const keys = /* @__PURE__ */ new Set();
12298
+ const add = (key) => keys.add(key);
12299
+ add(`type:${node.type}`);
12300
+ if (node.type === "fact") {
12301
+ const f = node;
12302
+ add(`cat:${f.category}`);
12303
+ if (f.severity) add(`sev:${f.severity}`);
12304
+ add(`by:${f.discoveredBy}`);
12305
+ for (const tag of f.tags) add(`tag:${tag}`);
12306
+ add(`key:${f.key}`);
12307
+ add(`subject:${f.subject}`);
12308
+ if (f.detail) add(`detail:${f.detail}`);
12309
+ }
12310
+ if (node.type === "goal") {
12311
+ const g = node;
12312
+ add(`status:${g.status}`);
12313
+ add(`prio:${g.priority}`);
12314
+ if (g.assignee) add(`assign:${g.assignee}`);
12315
+ for (const tag of g.tags) add(`tag:${tag}`);
12316
+ }
12317
+ if (node.type === "change") {
12318
+ const c = node;
12319
+ add(`change:${c.status}`);
12320
+ add(`by:${c.proposedBy}`);
12321
+ for (const g of c.satisfiesGoals) add(`goal:${g}`);
12322
+ }
12323
+ return keys;
12324
+ }
12325
+ /** Mutate the index: add a node's id to every set for the given keys. */
12326
+ _addToIndex(node, keys) {
12327
+ for (const key of keys) {
12328
+ let set = this.index.get(key);
12329
+ if (!set) {
12330
+ set = /* @__PURE__ */ new Set();
12331
+ this.index.set(key, set);
12332
+ }
12333
+ set.add(node.id);
12334
+ }
12335
+ }
12336
+ /** Remove a node's id from all index sets for the given keys. */
12337
+ _removeFromIndex(node, keys) {
12338
+ for (const key of keys) {
12339
+ this.index.get(key)?.delete(node.id);
12340
+ }
12341
+ }
12342
+ _matches(node, f) {
12343
+ if (f.type && node.type !== f.type) return false;
12344
+ if (f.category && node.category !== f.category) return false;
12345
+ if (f.status) {
12346
+ if (node.type === "goal" && node.status !== f.status) return false;
12347
+ if (node.type === "change" && node.status !== f.status) return false;
12348
+ }
12349
+ if (f.assignee && node.assignee !== f.assignee) return false;
12350
+ if (f.discoveredBy && node.discoveredBy !== f.discoveredBy) return false;
12351
+ if (f.proposedBy && node.proposedBy !== f.proposedBy) return false;
12352
+ if (f.tags?.length) {
12353
+ const nodeTags = node.tags ?? node.tags ?? [];
12354
+ if (!f.tags.some((t) => nodeTags.includes(t))) return false;
12355
+ }
12356
+ if (f.since && node.id > f.since) ;
12357
+ return true;
12358
+ }
12359
+ _deliver(node) {
12360
+ for (const sub of this.subs.values()) {
12361
+ if (this._matches(node, sub.filter)) {
12362
+ const pending = this.pendingDeliveries.get(sub.channel);
12363
+ if (pending) pending.push(node);
12364
+ }
12365
+ }
12366
+ }
12367
+ async _persist(node) {
12368
+ await fsp6.mkdir(this.filePath, { recursive: true });
12369
+ const line = JSON.stringify(node) + "\n";
12370
+ await withFileLock(this.graphFilePath, async () => {
12371
+ await fsp6.appendFile(this.graphFilePath, line, "utf8");
12372
+ });
12373
+ }
12374
+ async _append(node) {
12375
+ await fsp6.mkdir(this.filePath, { recursive: true });
12376
+ const line = JSON.stringify({ op: "update", node }) + "\n";
12377
+ await withFileLock(this.graphFilePath, async () => {
12378
+ await fsp6.appendFile(this.graphFilePath, line, "utf8");
12379
+ });
12380
+ }
12381
+ /** Rebuild in-memory state from the log file. Call on startup. */
12382
+ async load() {
12383
+ try {
12384
+ const content = await fsp6.readFile(this.graphFilePath, "utf8");
12385
+ const lines = content.split("\n").filter(Boolean);
12386
+ for (const line of lines) {
12387
+ try {
12388
+ const parsed = JSON.parse(line);
12389
+ if (parsed.op === "update") {
12390
+ const oldNode = this.nodes.get(parsed.node.id);
12391
+ if (oldNode) {
12392
+ this._removeFromIndex(oldNode, this._indexKeys(oldNode));
12393
+ }
12394
+ this.nodes.set(parsed.node.id, parsed.node);
12395
+ this._addToIndex(parsed.node, this._indexKeys(parsed.node));
12396
+ } else {
12397
+ this.nodes.set(parsed.id, parsed);
12398
+ this._addToIndex(parsed, this._indexKeys(parsed));
12399
+ }
12400
+ } catch {
12401
+ }
12402
+ }
12403
+ } catch {
12404
+ }
12405
+ }
12406
+ /** Snapshot for serialization. */
12407
+ snapshot() {
12408
+ return {
12409
+ nodes: Array.from(this.nodes.values()),
12410
+ subs: this.subs.size
12411
+ };
12412
+ }
12413
+ };
12414
+
12415
+ // src/coordination/task-dag.ts
12416
+ var TaskDAG = class {
12417
+ nodes = /* @__PURE__ */ new Map();
12418
+ handlers = /* @__PURE__ */ new Set();
12419
+ runnablesHandlers = /* @__PURE__ */ new Set();
12420
+ runnableCache = null;
12421
+ // ── Node management ───────────────────────────────────────────────────
12422
+ /**
12423
+ * Add a task node. Dependencies are validated for cycles.
12424
+ * Throws if adding a dep would create a cycle.
12425
+ */
12426
+ addNode(id, description, deps = [], opts = {}) {
12427
+ if (this.nodes.has(id)) return;
12428
+ for (const depId of deps) {
12429
+ if (!this.nodes.has(depId)) {
12430
+ throw new Error(`TaskDAG.addNode: unknown dependency "${depId}" for task "${id}". Add the dep first.`);
12431
+ }
12432
+ }
12433
+ if (this._wouldCycle(id, deps)) {
12434
+ throw new Error(`TaskDAG.addNode: adding deps [${deps.join(", ")}] to "${id}" would create a cycle.`);
12435
+ }
12436
+ const node = {
12437
+ id,
12438
+ description,
12439
+ deps: [...deps],
12440
+ status: deps.length === 0 ? "ready" : "pending",
12441
+ role: opts.role,
12442
+ priority: opts.priority ?? 5,
12443
+ dependents: [],
12444
+ tags: opts.tags ?? []
12445
+ };
12446
+ this.nodes.set(id, node);
12447
+ for (const depId of deps) {
12448
+ this.nodes.get(depId).dependents.push(id);
12449
+ }
12450
+ this.invalidateCache();
12451
+ this._emitReady();
12452
+ }
12453
+ /**
12454
+ * Remove a node and all edges to/from it.
12455
+ * Skips any dependents that would become dangling.
12456
+ */
12457
+ removeNode(id) {
12458
+ const node = this.nodes.get(id);
12459
+ if (!node) return;
12460
+ for (const depId of node.deps) {
12461
+ const dep = this.nodes.get(depId);
12462
+ if (dep) {
12463
+ dep.dependents = dep.dependents.filter((d) => d !== id);
12464
+ }
12465
+ }
12466
+ for (const depId of node.dependents) {
12467
+ const dep = this.nodes.get(depId);
12468
+ if (dep && dep.deps.every((d) => !this.nodes.has(d) || this.nodes.get(d).status === "done")) {
12469
+ this._transition(depId, "pending", "ready");
12470
+ }
12471
+ }
12472
+ this.nodes.delete(id);
12473
+ this.invalidateCache();
12474
+ }
12475
+ // ── State transitions ──────────────────────────────────────────────────
12476
+ /**
12477
+ * Mark a task as running. Returns true if the transition was valid
12478
+ * (task was in 'ready' state), false otherwise.
12479
+ */
12480
+ start(id, assignedTo) {
12481
+ const node = this.nodes.get(id);
12482
+ if (!node) return false;
12483
+ if (node.status !== "ready") return false;
12484
+ node.status = "running";
12485
+ node.assignedTo = assignedTo;
12486
+ node.spawnedAt = (/* @__PURE__ */ new Date()).toISOString();
12487
+ this.invalidateCache();
12488
+ this._emit({ type: "node:started", nodeId: id, assignedTo });
12489
+ return true;
12490
+ }
12491
+ /**
12492
+ * Mark a task as completed. Unblocks all dependents; they become 'ready'
12493
+ * if all their deps are done.
12494
+ */
12495
+ complete(id, result) {
12496
+ const node = this.nodes.get(id);
12497
+ if (!node) return;
12498
+ node.status = "done";
12499
+ node.result = result;
12500
+ node.completedAt = (/* @__PURE__ */ new Date()).toISOString();
12501
+ this.invalidateCache();
12502
+ const blocked = [];
12503
+ for (const depId of node.dependents) {
12504
+ const dep = this.nodes.get(depId);
12505
+ if (!dep) continue;
12506
+ const allDone = dep.deps.filter((d) => this.nodes.has(d)).every((d) => this.nodes.get(d).status === "done");
12507
+ if (allDone) {
12508
+ this._transition(depId, "pending", "ready");
12509
+ } else {
12510
+ blocked.push(depId);
12511
+ }
12512
+ }
12513
+ this._emit({ type: "node:completed", nodeId: id, result, blockers: blocked });
12514
+ if (blocked.length === 0) this._emitReady();
12515
+ }
12516
+ /**
12517
+ * Mark a task as failed. Unblocks dependents but they remain 'pending'
12518
+ * (they may still be runnable if other deps succeeded).
12519
+ */
12520
+ fail(id, error) {
12521
+ const node = this.nodes.get(id);
12522
+ if (!node) return;
12523
+ node.status = "failed";
12524
+ node.error = error;
12525
+ node.completedAt = (/* @__PURE__ */ new Date()).toISOString();
12526
+ this.invalidateCache();
12527
+ const blocked = [];
12528
+ for (const depId of node.dependents) {
12529
+ const dep = this.nodes.get(depId);
12530
+ if (!dep) continue;
12531
+ const allDone = dep.deps.filter((d) => this.nodes.has(d)).every((d) => {
12532
+ const s = this.nodes.get(d).status;
12533
+ return s === "done" || s === "skipped";
12534
+ });
12535
+ if (allDone) {
12536
+ this._transition(depId, "pending", "ready");
12537
+ } else {
12538
+ blocked.push(depId);
12539
+ }
12540
+ }
12541
+ this._emit({ type: "node:failed", nodeId: id, error, blockers: blocked });
12542
+ if (blocked.length === 0) this._emitReady();
12543
+ }
12544
+ /**
12545
+ * Skip a task (e.g., it was deemed unnecessary by an earlier step).
12546
+ * Treats it as done for dependency purposes.
12547
+ */
12548
+ skip(id, reason) {
12549
+ const node = this.nodes.get(id);
12550
+ if (!node) return;
12551
+ node.status = "skipped";
12552
+ node.completedAt = (/* @__PURE__ */ new Date()).toISOString();
12553
+ this.invalidateCache();
12554
+ for (const depId of node.dependents) {
12555
+ const dep = this.nodes.get(depId);
12556
+ if (!dep) continue;
12557
+ const allDone = dep.deps.filter((d) => this.nodes.has(d)).every((d) => {
12558
+ const s = this.nodes.get(d).status;
12559
+ return s === "done" || s === "skipped";
12560
+ });
12561
+ if (allDone) this._transition(depId, "pending", "ready");
12562
+ }
12563
+ this._emit({ type: "node:skipped", nodeId: id, reason });
12564
+ }
12565
+ // ── Queries ────────────────────────────────────────────────────────────
12566
+ getNode(id) {
12567
+ return this.nodes.get(id);
12568
+ }
12569
+ getAll() {
12570
+ return Array.from(this.nodes.values());
12571
+ }
12572
+ getReady() {
12573
+ if (this.runnableCache) return this.runnableCache;
12574
+ const runnable = Array.from(this.nodes.values()).filter((n) => n.status === "ready").sort((a, b) => a.priority - b.priority);
12575
+ this.runnableCache = runnable;
12576
+ return runnable;
12577
+ }
12578
+ getRunning() {
12579
+ return Array.from(this.nodes.values()).filter((n) => n.status === "running");
12580
+ }
12581
+ getPending() {
12582
+ return Array.from(this.nodes.values()).filter((n) => n.status === "pending");
12583
+ }
12584
+ getDone() {
12585
+ return Array.from(this.nodes.values()).filter((n) => n.status === "done");
12586
+ }
12587
+ getFailed() {
12588
+ return Array.from(this.nodes.values()).filter((n) => n.status === "failed");
12589
+ }
12590
+ getCompleted() {
12591
+ return Array.from(this.nodes.values()).filter((n) => n.status === "done" || n.status === "skipped");
12592
+ }
12593
+ isDone() {
12594
+ return Array.from(this.nodes.values()).every(
12595
+ (n) => n.status === "done" || n.status === "failed" || n.status === "skipped"
12596
+ );
12597
+ }
12598
+ isFailed() {
12599
+ return Array.from(this.nodes.values()).some((n) => n.status === "failed");
12600
+ }
12601
+ /** All tasks that are currently blocked (pending but not ready). */
12602
+ getBlocked() {
12603
+ return Array.from(this.nodes.values()).filter((n) => n.status === "pending");
12604
+ }
12605
+ /** Topological sort — tasks in dependency order. */
12606
+ getTopologicalOrder() {
12607
+ const visited = /* @__PURE__ */ new Set();
12608
+ const result = [];
12609
+ const visit = (id) => {
12610
+ if (visited.has(id)) return;
12611
+ visited.add(id);
12612
+ const node = this.nodes.get(id);
12613
+ if (!node) return;
12614
+ for (const depId of node.deps) visit(depId);
12615
+ result.push(node);
12616
+ };
12617
+ for (const id of this.nodes.keys()) visit(id);
12618
+ return result;
12619
+ }
12620
+ /** Check for deadlock: no runnable tasks but not done. */
12621
+ hasDeadlock() {
12622
+ if (this.isDone()) return false;
12623
+ return this.getReady().length === 0 && this.getRunning().length === 0;
12624
+ }
12625
+ /** Stats snapshot for reporting. */
12626
+ stats() {
12627
+ const all = Array.from(this.nodes.values());
12628
+ const done = all.filter((n) => n.status === "done" || n.status === "skipped").length;
12629
+ return {
12630
+ total: all.length,
12631
+ pending: all.filter((n) => n.status === "pending").length,
12632
+ ready: all.filter((n) => n.status === "ready").length,
12633
+ running: all.filter((n) => n.status === "running").length,
12634
+ done: all.filter((n) => n.status === "done").length,
12635
+ failed: all.filter((n) => n.status === "failed").length,
12636
+ skipped: all.filter((n) => n.status === "skipped").length,
12637
+ progress: all.length ? done / all.length : 0
12638
+ };
12639
+ }
12640
+ // ── Events ────────────────────────────────────────────────────────────
12641
+ onEvent(handler) {
12642
+ this.handlers.add(handler);
12643
+ return () => void this.handlers.delete(handler);
12644
+ }
12645
+ onRunnable(handler) {
12646
+ this.runnablesHandlers.add(handler);
12647
+ return () => void this.runnablesHandlers.delete(handler);
12648
+ }
12649
+ // ── Private ───────────────────────────────────────────────────────────
12650
+ _transition(id, from, to) {
12651
+ const node = this.nodes.get(id);
12652
+ if (!node || node.status !== from) return;
12653
+ node.status = to;
12654
+ this.invalidateCache();
12655
+ if (to === "ready") {
12656
+ this._emit({ type: "node:ready", nodeId: id, deps: node.deps });
12657
+ }
12658
+ }
12659
+ _emit(event) {
12660
+ for (const h of this.handlers) {
12661
+ try {
12662
+ h(event);
12663
+ } catch {
12664
+ }
12665
+ }
12666
+ }
12667
+ _emitReady() {
12668
+ const runnable = this.getReady();
12669
+ if (this.hasDeadlock()) {
12670
+ this._emit({ type: "deadlock", blocked: this.getBlocked().map((n) => n.id) });
12671
+ } else {
12672
+ this._emit({ type: "graph:done", allDone: this.isDone() });
12673
+ }
12674
+ if (runnable.length > 0) {
12675
+ for (const h of this.runnablesHandlers) {
12676
+ try {
12677
+ h(runnable);
12678
+ } catch {
12679
+ }
12680
+ }
12681
+ }
12682
+ }
12683
+ invalidateCache() {
12684
+ this.runnableCache = null;
12685
+ }
12686
+ /**
12687
+ * DFS cycle detection. Adding edge (id → dep) creates a cycle if
12688
+ * there already exists a path from dep to id.
12689
+ */
12690
+ _wouldCycle(id, newDeps) {
12691
+ const visited = /* @__PURE__ */ new Set();
12692
+ const stack = [...newDeps];
12693
+ while (stack.length > 0) {
12694
+ const current = stack.pop();
12695
+ if (current === id) return true;
12696
+ if (visited.has(current)) continue;
12697
+ visited.add(current);
12698
+ const node = this.nodes.get(current);
12699
+ if (node) stack.push(...node.deps);
12700
+ }
12701
+ return false;
12702
+ }
12703
+ };
12704
+
12705
+ // src/coordination/consensus-protocol.ts
12706
+ var ConsensusProtocol = class {
12707
+ graph;
12708
+ fleet;
12709
+ rules;
12710
+ voters;
12711
+ // agentId → config
12712
+ constructor(opts) {
12713
+ this.graph = opts.graph;
12714
+ this.fleet = opts.fleet ?? void 0;
12715
+ this.voters = new Map(opts.voters.map((v) => [v.agentId, v]));
12716
+ this.rules = {
12717
+ quorumFraction: opts.rules?.quorumFraction ?? 0.5,
12718
+ approvalFraction: opts.rules?.approvalFraction ?? 0.6,
12719
+ vetoRoles: opts.rules?.vetoRoles ?? [],
12720
+ approvalWeightFraction: opts.rules?.approvalWeightFraction
12721
+ };
12722
+ }
12723
+ // ── Vote lifecycle ────────────────────────────────────────────────────
12724
+ /**
12725
+ * Initiate a vote on a proposed change. Updates the change node's status
12726
+ * to 'proposed' and notifies eligible voters via FleetBus.
12727
+ */
12728
+ initiateVote(changeId) {
12729
+ const change = this.graph.get(changeId);
12730
+ if (!change || change.type !== "change") {
12731
+ throw new Error(`ConsensusProtocol: no change found with id "${changeId}"`);
12732
+ }
12733
+ this.graph.update(changeId, { status: "proposed", votes: [] });
12734
+ const eligible = this._eligibleVoters(change);
12735
+ this._notifyVoters(change, eligible, "vote_initiated");
12736
+ }
12737
+ /**
12738
+ * Cast a vote. Updates the change node in the graph and re-evaluates
12739
+ * consensus. If the vote triggers a resolution, updates the change status.
12740
+ */
12741
+ castVote(changeId, voterId, value, rationale) {
12742
+ const change = this.graph.get(changeId);
12743
+ if (!change || change.type !== "change") {
12744
+ throw new Error(`ConsensusProtocol: no change found for "${changeId}"`);
12745
+ }
12746
+ const voter = this.voters.get(voterId);
12747
+ if (!voter) {
12748
+ throw new Error(`ConsensusProtocol: unknown voter "${voterId}"`);
12749
+ }
12750
+ const eligible = this._eligibleVoters(change);
12751
+ if (!eligible.includes(voterId)) {
12752
+ throw new Error(`ConsensusProtocol: voter "${voterId}" is not eligible for this vote`);
12753
+ }
12754
+ const vote = {
12755
+ agentId: voterId,
12756
+ agentName: voter.agentName,
12757
+ value,
12758
+ rationale,
12759
+ votedAt: (/* @__PURE__ */ new Date()).toISOString()
12760
+ };
12761
+ const existingIdx = change.votes.findIndex((v) => v.agentId === voterId);
12762
+ const newVotes = existingIdx >= 0 ? change.votes.with(existingIdx, vote) : [...change.votes, vote];
12763
+ const result = this._resolve(changeId, newVotes, eligible);
12764
+ this.graph.update(changeId, {
12765
+ votes: newVotes,
12766
+ ...result.outcome !== "pending" ? { status: this._toChangeStatus(result.outcome) } : {}
12767
+ });
12768
+ this._notifyVoters(change, eligible, "vote_cast", { voterId, value, result });
12769
+ return result;
12770
+ }
12771
+ /**
12772
+ * Resolve the current vote without waiting for all eligible voters.
12773
+ * Useful when a timeout fires or an agent decides to finalize early.
12774
+ */
12775
+ resolveNow(changeId) {
12776
+ const change = this.graph.get(changeId);
12777
+ if (!change) throw new Error(`ConsensusProtocol: unknown change "${changeId}"`);
12778
+ const eligible = this._eligibleVoters(change);
12779
+ const result = this._resolve(changeId, change.votes, eligible);
12780
+ if (result.outcome !== "pending") {
12781
+ this.graph.update(changeId, { status: this._toChangeStatus(result.outcome) });
12782
+ this._notifyVoters(change, eligible, "vote_resolved", { result });
12783
+ }
12784
+ return result;
12785
+ }
12786
+ /**
12787
+ * Register or update a voter's configuration.
12788
+ */
12789
+ registerVoter(config) {
12790
+ this.voters.set(config.agentId, config);
12791
+ }
12792
+ /**
12793
+ * Get the current vote status for a change.
12794
+ */
12795
+ getStatus(changeId) {
12796
+ const change = this.graph.get(changeId);
12797
+ if (!change || change.type !== "change") return null;
12798
+ const eligible = this._eligibleVoters(change);
12799
+ return this._resolve(changeId, change.votes, eligible);
12800
+ }
12801
+ // ── Private ───────────────────────────────────────────────────────────
12802
+ _eligibleVoters(change) {
12803
+ return Array.from(this.voters.keys()).filter(
12804
+ (agentId) => agentId !== change.proposedBy
12805
+ );
12806
+ }
12807
+ _resolve(changeId, votes, eligible) {
12808
+ const totalEligible = eligible.length;
12809
+ const approve = votes.filter((v) => v.value === "approve");
12810
+ const reject = votes.filter((v) => v.value === "reject");
12811
+ const abstain = votes.filter((v) => v.value === "abstain");
12812
+ const totalWeightApprove = approve.reduce(
12813
+ (sum, v) => sum + (this.voters.get(v.agentId)?.weight ?? 1),
12814
+ 0
12815
+ );
12816
+ const totalWeightReject = reject.reduce(
12817
+ (sum, v) => sum + (this.voters.get(v.agentId)?.weight ?? 1),
12818
+ 0
12819
+ );
12820
+ const totalWeight = Array.from(this.voters.values()).reduce(
12821
+ (sum, v) => sum + v.weight,
12822
+ 0
12823
+ );
12824
+ const castCount = votes.length;
12825
+ const quorumRequired = Math.ceil(totalEligible * this.rules.quorumFraction);
12826
+ const quorumMet = castCount >= quorumRequired;
12827
+ for (const v of reject) {
12828
+ const config = this.voters.get(v.agentId);
12829
+ if (config?.veto && this.rules.vetoRoles.includes(config.role)) {
12830
+ return {
12831
+ changeId,
12832
+ outcome: "vetoed",
12833
+ votes,
12834
+ approveCount: approve.length,
12835
+ rejectCount: reject.length,
12836
+ abstainCount: abstain.length,
12837
+ totalWeightApprove,
12838
+ totalWeightReject,
12839
+ eligibleVoters: eligible,
12840
+ quorumMet,
12841
+ approvalMet: false,
12842
+ vetoedBy: config.agentId,
12843
+ rationale: `Hard veto from role "${config.role}" (${config.agentName}).`
12844
+ };
12845
+ }
12846
+ }
12847
+ if (!quorumMet) {
12848
+ return {
12849
+ changeId,
12850
+ outcome: "quorum_not_met",
12851
+ votes,
12852
+ approveCount: approve.length,
12853
+ rejectCount: reject.length,
12854
+ abstainCount: abstain.length,
12855
+ totalWeightApprove,
12856
+ totalWeightReject,
12857
+ eligibleVoters: eligible,
12858
+ quorumMet: false,
12859
+ approvalMet: false,
12860
+ rationale: `Quorum not met: ${castCount}/${quorumRequired} required.`
12861
+ };
12862
+ }
12863
+ const approvalRequired = Math.ceil(castCount * this.rules.approvalFraction);
12864
+ const approvalMet = approve.length >= approvalRequired;
12865
+ if (this.rules.approvalWeightFraction !== void 0 && approvalMet) {
12866
+ const weightRequired = totalWeight * this.rules.approvalWeightFraction;
12867
+ if (totalWeightApprove < weightRequired) {
12868
+ return {
12869
+ changeId,
12870
+ outcome: "rejected",
12871
+ votes,
12872
+ approveCount: approve.length,
12873
+ rejectCount: reject.length,
12874
+ abstainCount: abstain.length,
12875
+ totalWeightApprove,
12876
+ totalWeightReject,
12877
+ eligibleVoters: eligible,
12878
+ quorumMet: true,
12879
+ approvalMet: false,
12880
+ rationale: `Weight threshold not met: ${totalWeightApprove.toFixed(2)}/${weightRequired.toFixed(2)} required.`
12881
+ };
12882
+ }
12883
+ }
12884
+ if (approvalMet) {
12885
+ return {
12886
+ changeId,
12887
+ outcome: "approved",
12888
+ votes,
12889
+ approveCount: approve.length,
12890
+ rejectCount: reject.length,
12891
+ abstainCount: abstain.length,
12892
+ totalWeightApprove,
12893
+ totalWeightReject,
12894
+ eligibleVoters: eligible,
12895
+ quorumMet: true,
12896
+ approvalMet: true,
12897
+ rationale: `Approved: ${approve.length}/${castCount} votes (threshold: ${approvalRequired}).`
12898
+ };
12899
+ }
12900
+ return {
12901
+ changeId,
12902
+ outcome: "rejected",
12903
+ votes,
12904
+ approveCount: approve.length,
12905
+ rejectCount: reject.length,
12906
+ abstainCount: abstain.length,
12907
+ totalWeightApprove,
12908
+ totalWeightReject,
12909
+ eligibleVoters: eligible,
12910
+ quorumMet: true,
12911
+ approvalMet: false,
12912
+ rationale: `Rejected: ${approve.length}/${castCount} approve votes (threshold: ${approvalRequired}).`
12913
+ };
12914
+ }
12915
+ _toChangeStatus(outcome) {
12916
+ switch (outcome) {
12917
+ case "approved":
12918
+ return "approved";
12919
+ case "rejected":
12920
+ return "rejected";
12921
+ case "vetoed":
12922
+ return "rejected";
12923
+ case "quorum_not_met":
12924
+ return "proposed";
12925
+ default:
12926
+ return "proposed";
12927
+ }
12928
+ }
12929
+ _notifyVoters(change, eligible, event, extra) {
12930
+ if (!this.fleet) return;
12931
+ this.fleet.emit({
12932
+ subagentId: "consensus",
12933
+ ts: Date.now(),
12934
+ type: `consensus:${event}`,
12935
+ payload: { changeId: change.id, changeTitle: change.title, eligible, ...extra }
12936
+ });
12937
+ }
12938
+ };
12939
+
12940
+ // src/coordination/change-manager.ts
12941
+ var DEFAULT_QUALITY_CHECKS = {
12942
+ runTests: true,
12943
+ runTypecheck: true,
12944
+ runLint: false,
12945
+ // warning only
12946
+ runSecurityScan: true,
12947
+ checkTestCoverage: false,
12948
+ minCoveragePercent: 70
12949
+ };
12950
+ var ChangeManager = class {
12951
+ graph;
12952
+ consensus;
12953
+ fleet;
12954
+ checks;
12955
+ /** Track applied changes for rollback lookup. */
12956
+ appliedChanges = /* @__PURE__ */ new Map();
12957
+ // changeId → rollbackId
12958
+ constructor(opts) {
12959
+ this.graph = opts.graph;
12960
+ this.consensus = opts.consensus;
12961
+ this.fleet = opts.fleet ?? void 0;
12962
+ this.checks = { ...DEFAULT_QUALITY_CHECKS, ...opts.checks };
12963
+ }
12964
+ // ── Lifecycle ────────────────────────────────────────────────────────
12965
+ /**
12966
+ * Propose a new code change. Creates a ChangeNode in the knowledge graph.
12967
+ * Does NOT automatically initiate voting — call `submitForReview()` for that.
12968
+ */
12969
+ async propose(input) {
12970
+ const node = await this.graph.add({
12971
+ type: "change",
12972
+ title: input.title,
12973
+ description: input.description,
12974
+ files: input.files.map((f) => ({
12975
+ path: f.path,
12976
+ action: f.action
12977
+ })),
12978
+ status: "proposed",
12979
+ proposedBy: input.proposedBy,
12980
+ proposedAt: (/* @__PURE__ */ new Date()).toISOString(),
12981
+ approvedBy: [],
12982
+ rejectedBy: [],
12983
+ votes: [],
12984
+ qualityGate: { passed: false, checks: [] },
12985
+ // filled after quality gate
12986
+ satisfiesGoals: input.satisfiesGoals
12987
+ });
12988
+ void this._runQualityGate(node.id, input.files).then((gate) => {
12989
+ void this.graph.update(node.id, { qualityGate: gate });
12990
+ });
12991
+ this._emit("change:proposed", { changeId: node.id, title: node.title });
12992
+ return node;
12993
+ }
12994
+ /**
12995
+ * Submit an approved change for application.
12996
+ * Returns the change node — actual file mutations are performed by agents
12997
+ * acting on this node's data from the knowledge graph.
12998
+ */
12999
+ async submitForReview(changeId) {
13000
+ const change = this.graph.get(changeId);
13001
+ if (!change || change.type !== "change") {
13002
+ throw new Error(`ChangeManager: no change found "${changeId}"`);
13003
+ }
13004
+ if (change.status !== "proposed") {
13005
+ throw new Error(`ChangeManager: change "${changeId}" is not in 'proposed' state`);
13006
+ }
13007
+ this.consensus.initiateVote(changeId);
13008
+ this._emit("change:submitted_for_review", { changeId, title: change.title });
13009
+ }
13010
+ /**
13011
+ * Apply an approved change. Updates the change node to 'applied'.
13012
+ * Agents should watch for 'applied' status and perform the actual file mutations.
13013
+ */
13014
+ async markApplied(changeId, appliedAt) {
13015
+ const change = this.graph.get(changeId);
13016
+ if (!change) return null;
13017
+ const updated = await this.graph.update(changeId, {
13018
+ status: "applied",
13019
+ appliedAt
13020
+ });
13021
+ if (updated) {
13022
+ this.appliedChanges.set(changeId, "");
13023
+ this._emit("change:applied", { changeId, title: updated.title, files: updated.files });
13024
+ }
13025
+ return updated;
13026
+ }
13027
+ /**
13028
+ * Mark a change as applied and trigger rollback for any satisfied goal
13029
+ * that turns out to be broken.
13030
+ */
13031
+ async markAppliedWithVerification(changeId, verify) {
13032
+ const change = this.graph.get(changeId);
13033
+ if (!change) throw new Error(`ChangeManager: unknown change "${changeId}"`);
13034
+ const appliedAt = (/* @__PURE__ */ new Date()).toISOString();
13035
+ await this.markApplied(changeId, appliedAt);
13036
+ const verificationResult = await verify();
13037
+ if (!verificationResult.passed) {
13038
+ const rollbackResult = await this.proposeRollback(changeId, "Quality gate failed after apply");
13039
+ return {
13040
+ changeId,
13041
+ success: false,
13042
+ appliedAt,
13043
+ filesTouched: change.files.map((f) => f.path),
13044
+ verificationResult,
13045
+ rollbackChangeId: rollbackResult?.id,
13046
+ error: `Quality gate failed: ${verificationResult.checks.filter((c) => !c.passed).map((c) => c.name).join(", ")}`
13047
+ };
13048
+ }
13049
+ return {
13050
+ changeId,
13051
+ success: true,
13052
+ appliedAt,
13053
+ filesTouched: change.files.map((f) => f.path),
13054
+ verificationResult
13055
+ };
13056
+ }
13057
+ /**
13058
+ * Propose a rollback for an applied change. Creates a new change that
13059
+ * reverses the original. Goes through full consensus.
13060
+ */
13061
+ async proposeRollback(appliedChangeId, reason) {
13062
+ const original = this.graph.get(appliedChangeId);
13063
+ if (!original || original.type !== "change") return null;
13064
+ const rollbackFiles = original.files.map((f) => ({
13065
+ path: f.path,
13066
+ action: f.action === "create" ? "delete" : f.action === "delete" ? "create" : "modify"
13067
+ }));
13068
+ const rollback = await this.propose({
13069
+ title: `Rollback: ${original.title}`,
13070
+ description: `Rollback of "${original.title}" applied at ${original.appliedAt}. Reason: ${reason}`,
13071
+ files: rollbackFiles,
13072
+ proposedBy: "change-manager",
13073
+ satisfiesGoals: [],
13074
+ tags: ["rollback", `original:${appliedChangeId}`]
13075
+ });
13076
+ this.appliedChanges.set(appliedChangeId, rollback.id);
13077
+ await this.graph.update(appliedChangeId, {
13078
+ rolledBackAt: (/* @__PURE__ */ new Date()).toISOString(),
13079
+ rollbackReason: reason
13080
+ });
13081
+ this._emit("change:rollback_proposed", {
13082
+ originalChangeId: appliedChangeId,
13083
+ rollbackChangeId: rollback.id,
13084
+ reason
13085
+ });
13086
+ return rollback;
13087
+ }
13088
+ /**
13089
+ * Mark a change as rolled back.
13090
+ */
13091
+ async markRolledBack(changeId, rolledBackAt) {
13092
+ const updated = await this.graph.update(changeId, {
13093
+ status: "rolled_back",
13094
+ rolledBackAt
13095
+ });
13096
+ if (updated) {
13097
+ this._emit("change:rolled_back", { changeId, title: updated.title });
13098
+ }
13099
+ return updated;
13100
+ }
13101
+ // ── Queries ───────────────────────────────────────────────────────────
13102
+ getPendingReviews() {
13103
+ return this.graph.getChanges({ status: "proposed" });
13104
+ }
13105
+ getAppliedChanges() {
13106
+ return this.graph.getChanges({ status: "applied" });
13107
+ }
13108
+ getChange(id) {
13109
+ return this.graph.get(id);
13110
+ }
13111
+ getChangesForGoal(goalId) {
13112
+ return this.graph.getChanges({}).filter(
13113
+ (c) => c.satisfiesGoals.includes(goalId)
13114
+ );
13115
+ }
13116
+ // ── Quality gate ──────────────────────────────────────────────────────
13117
+ /**
13118
+ * Run quality gate checks. This is informational — actual test/lint/typecheck
13119
+ * execution is done by agents spawned for this purpose. This method stores
13120
+ * the result in the change node.
13121
+ */
13122
+ async _runQualityGate(_changeId, _files) {
13123
+ const checks = [];
13124
+ if (this.checks.runTests) {
13125
+ checks.push({ name: "tests", passed: false, detail: "Tests must be run by a verify agent" });
13126
+ }
13127
+ if (this.checks.runTypecheck) {
13128
+ checks.push({ name: "typecheck", passed: false, detail: "TypeScript must compile" });
13129
+ }
13130
+ if (this.checks.runSecurityScan) {
13131
+ checks.push({ name: "security", passed: false, detail: "Security scan must pass" });
13132
+ }
13133
+ if (this.checks.runLint) {
13134
+ checks.push({ name: "lint", passed: false, detail: "Lint check" });
13135
+ }
13136
+ const result = {
13137
+ passed: checks.length === 0,
13138
+ checks
13139
+ };
13140
+ await this.graph.update(_changeId, { qualityGate: result });
13141
+ return result;
13142
+ }
13143
+ /**
13144
+ * Update quality gate result for a change. Called by verify agents
13145
+ * after running their checks.
13146
+ */
13147
+ async updateQualityGate(changeId, checkName, result) {
13148
+ const change = this.graph.get(changeId);
13149
+ if (!change) return;
13150
+ const checks = change.qualityGate.checks.map(
13151
+ (c) => c.name === checkName ? { ...c, ...result } : c
13152
+ );
13153
+ const allPassed = checks.every((c) => c.passed);
13154
+ await this.graph.update(changeId, {
13155
+ qualityGate: { passed: allPassed, checks }
13156
+ });
13157
+ this._emit("quality_gate:updated", { changeId, checkName, passed: result.passed });
13158
+ }
13159
+ // ── Helpers ──────────────────────────────────────────────────────────
13160
+ _emit(type, payload) {
13161
+ if (!this.fleet) return;
13162
+ this.fleet.emit({ subagentId: "change-manager", ts: Date.now(), type, payload });
13163
+ }
13164
+ };
13165
+ var AutonomousBrain = class {
13166
+ graph;
13167
+ // Fleet bus for emitting decisions — null-safe, no-op if not provided
13168
+ fleetBus;
13169
+ llmProvider;
13170
+ maxRetries;
13171
+ consensusRiskThreshold;
13172
+ selfImprove;
13173
+ /** Decision history for self-improvement and audit. */
13174
+ decisionHistory = [];
13175
+ /** Tracks failure patterns for self-improvement. */
13176
+ failurePatterns = /* @__PURE__ */ new Map();
13177
+ RISK_ORDER = ["low", "medium", "high", "critical"];
13178
+ // ── Fleet bus integration ─────────────────────────────────────────────
13179
+ _emit(type, payload) {
13180
+ if (!this.fleetBus) return;
13181
+ this.fleetBus.emit({ subagentId: "brain", ts: Date.now(), type, payload });
13182
+ }
13183
+ constructor(opts) {
13184
+ this.graph = opts.graph;
13185
+ this.fleetBus = opts.fleet ?? void 0;
13186
+ this.llmProvider = opts.llmProvider;
13187
+ this.maxRetries = opts.maxRetries ?? 3;
13188
+ this.consensusRiskThreshold = opts.consensusRiskThreshold ?? "high";
13189
+ this.selfImprove = opts.selfImprove ?? true;
13190
+ }
13191
+ // ── BrainArbiter interface ────────────────────────────────────────────
13192
+ /** Implements BrainArbiter — bridges standard brain.ts interface to autonomous engine. */
13193
+ async decide(request) {
13194
+ return this.decideAuto(this._toAutonomous(request));
13195
+ }
13196
+ // ── Main entry point ──────────────────────────────────────────────────
13197
+ /**
13198
+ * Primary autonomous decision engine — receives AutonomousDecisionRequest,
13199
+ * queries the LLM, records the decision, and returns a BrainDecision.
13200
+ *
13201
+ * Specialized methods (decideSpawn, decideApproval, etc.) should call this
13202
+ * directly with a pre-built AutonomousDecisionRequest.
13203
+ */
13204
+ async decideAuto(request) {
13205
+ const { id, decisionType, question, context, options, risk, requiresConsensus } = request;
13206
+ const history = this.selfImprove ? this._loadHistory(decisionType, risk) : [];
13207
+ const hints = this.selfImprove ? this._getSelfImproveHints(decisionType) : [];
13208
+ const prompt = {
13209
+ decisionType,
13210
+ question,
13211
+ context: this._serializeContext(context),
13212
+ options,
13213
+ risk,
13214
+ decisionHistory: history,
13215
+ selfImproveHints: hints
13216
+ };
13217
+ let result;
13218
+ try {
13219
+ result = await this.llmProvider.decide(prompt);
13220
+ } catch (err) {
13221
+ const recommended = options.find((o) => o.recommended);
13222
+ if (recommended && risk === "low") {
13223
+ return { type: "answer", optionId: recommended.id, text: recommended.label };
13224
+ }
13225
+ return { type: "deny", reason: `Brain LLM failed: ${String(err)}` };
13226
+ }
13227
+ this._recordDecision({
13228
+ id,
13229
+ decisionType,
13230
+ question,
13231
+ options,
13232
+ chosen: result.optionId,
13233
+ rationale: result.rationale,
13234
+ madeBy: "autonomous-brain",
13235
+ context: JSON.stringify(context)
13236
+ }).catch(() => {
13237
+ });
13238
+ if (requiresConsensus) {
13239
+ this._emit("brain.decision", { id, decisionType, optionId: result.optionId, rationale: result.rationale, consensusRequired: true });
13240
+ return {
13241
+ type: "answer",
13242
+ optionId: result.optionId,
13243
+ text: options.find((o) => o.id === result.optionId)?.label ?? result.optionId,
13244
+ rationale: `${result.rationale}
13245
+
13246
+ \u26A0\uFE0F This decision requires consensus approval before execution.`
13247
+ };
13248
+ }
13249
+ this._emit("brain.decision", { id, decisionType, optionId: result.optionId, rationale: result.rationale, consensusRequired: false });
13250
+ return {
13251
+ type: "answer",
13252
+ optionId: result.optionId,
13253
+ text: options.find((o) => o.id === result.optionId)?.label ?? result.optionId,
13254
+ rationale: result.rationale
13255
+ };
13256
+ }
13257
+ // ── Specialized decision methods ────────────────────────────────────
13258
+ /**
13259
+ * Decide whether to spawn a subagent, which role to use, and what budget.
13260
+ */
13261
+ async decideSpawn(source, taskDescription, availableFacts, fleetStatus) {
13262
+ const roleHints = this._inferRoles(taskDescription);
13263
+ const risk = roleHints.length > 1 ? "medium" : "low";
13264
+ const options = roleHints.map((role, i) => ({
13265
+ id: `spawn:${role}`,
13266
+ label: `Spawn ${role} agent`,
13267
+ risk: i === 0 ? "low" : "medium",
13268
+ recommended: i === 0,
13269
+ consequence: i === 0 ? `Spawn the most appropriate agent for: ${taskDescription.slice(0, 80)}` : `Spawn an alternative agent for the same task`
13270
+ }));
13271
+ return this.decideAuto({
13272
+ id: randomUUID(),
13273
+ source,
13274
+ decisionType: "spawn",
13275
+ question: `Should we spawn a subagent for this task?`,
13276
+ context: {
13277
+ facts: availableFacts,
13278
+ fleetStatus,
13279
+ taskDescription
13280
+ },
13281
+ options,
13282
+ risk,
13283
+ requiresConsensus: false
13284
+ });
13285
+ }
13286
+ /**
13287
+ * Decide whether to approve a proposed change.
13288
+ */
13289
+ async decideApproval(source, change, relevantFacts) {
13290
+ const risk = this._changeRisk(change);
13291
+ const options = [
13292
+ {
13293
+ id: "approve",
13294
+ label: "Approve change",
13295
+ recommended: change.qualityGate.passed && relevantFacts.filter((f) => f.severity === "critical").length === 0,
13296
+ risk,
13297
+ consequence: `Apply changes to: ${change.files.map((f) => f.path).join(", ")}`
13298
+ },
13299
+ {
13300
+ id: "reject",
13301
+ label: "Reject change",
13302
+ recommended: false,
13303
+ risk: "medium",
13304
+ consequence: "Return change to proposer with feedback"
13305
+ },
13306
+ {
13307
+ id: "request_changes",
13308
+ label: "Request specific changes",
13309
+ recommended: false,
13310
+ risk: "low",
13311
+ consequence: "Send back for revision with conditions"
13312
+ }
13313
+ ];
13314
+ return this.decideAuto({
13315
+ id: randomUUID(),
13316
+ source,
13317
+ decisionType: "approve_change",
13318
+ question: `Should we approve the change "${change.title}"?`,
13319
+ context: {
13320
+ facts: relevantFacts,
13321
+ change
13322
+ },
13323
+ options,
13324
+ risk,
13325
+ requiresConsensus: risk === "critical" || risk === "high"
13326
+ });
13327
+ }
13328
+ /**
13329
+ * Decide how to handle a failed task.
13330
+ */
13331
+ async decideEscalation(source, taskId, error, attempts) {
13332
+ const retryCount = this.failurePatterns.get(taskId)?.failures ?? attempts;
13333
+ const options = [];
13334
+ if (retryCount < this.maxRetries) {
13335
+ options.push({
13336
+ id: "retry",
13337
+ label: `Retry task (attempt ${retryCount + 1}/${this.maxRetries})`,
13338
+ recommended: retryCount < 2,
13339
+ risk: "medium",
13340
+ consequence: `Restart the task with same or adjusted budget`
13341
+ });
13342
+ options.push({
13343
+ id: "retry_with_adjustment",
13344
+ label: `Retry with more budget`,
13345
+ recommended: retryCount >= 1,
13346
+ risk: "medium",
13347
+ consequence: `Increase timeout or iterations before retrying`
13348
+ });
13349
+ }
13350
+ options.push({
13351
+ id: "delegate",
13352
+ label: "Delegate to different role",
13353
+ recommended: retryCount >= 1,
13354
+ risk: "medium",
13355
+ consequence: "Try a different agent role for the same task"
13356
+ });
13357
+ if (retryCount >= this.maxRetries) {
13358
+ options.push({
13359
+ id: "mark_failed",
13360
+ label: "Mark task as failed",
13361
+ recommended: true,
13362
+ risk: "high",
13363
+ consequence: "Stop retrying, propagate failure upward"
13364
+ });
13365
+ }
13366
+ options.push({
13367
+ id: "decompose",
13368
+ label: "Decompose and retry in parts",
13369
+ recommended: false,
13370
+ risk: "low",
13371
+ consequence: "Break the task into smaller sub-tasks"
13372
+ });
13373
+ return this.decideAuto({
13374
+ id: randomUUID(),
13375
+ source,
13376
+ decisionType: "escalate_task",
13377
+ question: `Task failed: ${error.slice(0, 100)}. How should we proceed?`,
13378
+ context: { error, attempts: retryCount },
13379
+ options,
13380
+ risk: retryCount >= this.maxRetries ? "critical" : "medium",
13381
+ requiresConsensus: false
13382
+ });
13383
+ }
13384
+ // ── Self-improvement ─────────────────────────────────────────────────
13385
+ /**
13386
+ * Record the outcome of a decision for self-improvement.
13387
+ * Call this after a spawned agent completes or a change is applied.
13388
+ */
13389
+ recordOutcome(decisionId, outcome, _detail) {
13390
+ const node = this.graph.get(decisionId);
13391
+ if (!node) return;
13392
+ const key = `decision:${node.decisionType}`;
13393
+ if (outcome === "failure") {
13394
+ const existing = this.failurePatterns.get(key) ?? { failures: 0, lastFailure: "" };
13395
+ existing.failures += 1;
13396
+ existing.lastFailure = (/* @__PURE__ */ new Date()).toISOString();
13397
+ this.failurePatterns.set(key, existing);
13398
+ } else {
13399
+ this.failurePatterns.delete(key);
13400
+ }
13401
+ void this.graph.update(decisionId, { decisionType: node.decisionType });
13402
+ }
13403
+ _getSelfImproveHints(decisionType) {
13404
+ const pattern = this.failurePatterns.get(`decision:${decisionType}`);
13405
+ if (!pattern || pattern.failures < 2) return [];
13406
+ return [
13407
+ `\u26A0\uFE0F ${decisionType} decisions have failed ${pattern.failures} times recently.`,
13408
+ "Consider alternative approaches before defaulting to this pattern."
13409
+ ];
13410
+ }
13411
+ // ── Private ───────────────────────────────────────────────────────────
13412
+ _toAutonomous(req) {
13413
+ const decisionType = this._inferDecisionType(req);
13414
+ return {
13415
+ id: req.id,
13416
+ source: req.source,
13417
+ decisionType,
13418
+ question: req.question,
13419
+ context: {
13420
+ taskDescription: req.context ?? ""
13421
+ },
13422
+ options: req.options ?? [],
13423
+ risk: req.risk,
13424
+ requiresConsensus: this.RISK_ORDER.indexOf(req.risk) >= this.RISK_ORDER.indexOf(this.consensusRiskThreshold)
13425
+ };
13426
+ }
13427
+ _inferDecisionType(req) {
13428
+ if (req.question.toLowerCase().includes("spawn")) return "spawn";
13429
+ if (req.question.toLowerCase().includes("approve") || req.question.toLowerCase().includes("change")) return "approve_change";
13430
+ if (req.question.toLowerCase().includes("retry") || req.question.toLowerCase().includes("fail")) return "retry_task";
13431
+ if (req.question.toLowerCase().includes("priorit")) return "prioritize_goals";
13432
+ if (req.question.toLowerCase().includes("decompos")) return "decompose_goal";
13433
+ return "assign_task";
13434
+ }
13435
+ _serializeContext(ctx) {
13436
+ const parts = [];
13437
+ if (ctx.facts?.length) {
13438
+ parts.push(`## Relevant Facts
13439
+ ${ctx.facts.map((f) => `- [${f.severity ?? "info"}] ${f.subject}: ${f.detail}`).join("\n")}`);
13440
+ }
13441
+ if (ctx.goals?.length) {
13442
+ parts.push(`## Active Goals
13443
+ ${ctx.goals.map((g) => `- [${g.status}] ${g.priority}: ${g.title}`).join("\n")}`);
13444
+ }
13445
+ if (ctx.change) {
13446
+ const c = ctx.change;
13447
+ parts.push(`## Change Under Review
13448
+ - Title: ${c.title}
13449
+ - Status: ${c.status}
13450
+ - Files: ${c.files.map((f) => `${f.action} ${f.path}`).join(", ")}
13451
+ - Quality gate: ${c.qualityGate.passed ? "PASSED" : "FAILED"}
13452
+ Checks: ${c.qualityGate.checks.map((ch) => `${ch.name}:${ch.passed ? "\u2705" : "\u274C"}`).join(", ")}`);
13453
+ }
13454
+ if (ctx.fleetStatus) {
13455
+ parts.push(`## Fleet Status
13456
+ - Running: ${ctx.fleetStatus.running}, Idle: ${ctx.fleetStatus.idle}, Total: ${ctx.fleetStatus.total}
13457
+ - Cost so far: $${ctx.fleetStatus.costSoFar.toFixed(4)}`);
13458
+ }
13459
+ if (ctx.taskDescription) {
13460
+ parts.push(`## Task
13461
+ ${ctx.taskDescription}`);
13462
+ }
13463
+ if (ctx.error) {
13464
+ parts.push(`## Error
13465
+ ${ctx.error}`);
13466
+ }
13467
+ return parts.join("\n\n");
13468
+ }
13469
+ _loadHistory(type, _risk) {
13470
+ const all = this.graph.getDecisions().filter((d) => d.decisionType === type);
13471
+ return all.slice(-10);
13472
+ }
13473
+ async _recordDecision(input) {
13474
+ const node = await this.graph.add({
13475
+ type: "decision",
13476
+ decisionType: input.decisionType,
13477
+ question: input.question,
13478
+ options: input.options,
13479
+ chosen: input.chosen,
13480
+ rationale: input.rationale,
13481
+ madeBy: input.madeBy,
13482
+ madeAt: (/* @__PURE__ */ new Date()).toISOString(),
13483
+ context: input.context
13484
+ });
13485
+ this.decisionHistory.push(node);
13486
+ return node;
13487
+ }
13488
+ _inferRoles(task) {
13489
+ const t = task.toLowerCase();
13490
+ if (t.includes("bug") || t.includes("error") || t.includes("crash")) return ["bug-hunter", "fixer"];
13491
+ if (t.includes("security") || t.includes("secret") || t.includes("injection")) return ["security-scanner"];
13492
+ if (t.includes("refactor") || t.includes("architecture") || t.includes("debt")) return ["refactor-planner", "critic"];
13493
+ if (t.includes("audit") || t.includes("log") || t.includes("analyze")) return ["audit-log"];
13494
+ if (t.includes("test") || t.includes("coverage")) return ["tester", "bug-hunter"];
13495
+ return ["bug-hunter", "refactor-planner"];
13496
+ }
13497
+ _changeRisk(change) {
13498
+ const criticalFiles = change.files.filter(
13499
+ (f) => f.path.includes("auth") || f.path.includes("config") || f.path.includes("schema")
13500
+ );
13501
+ if (criticalFiles.length > 0) return "high";
13502
+ if (change.files.length > 10) return "medium";
13503
+ return "low";
13504
+ }
13505
+ };
13506
+ var TaskAuctioneer = class {
13507
+ graph;
13508
+ fleet;
13509
+ mailbox;
13510
+ selfAgentId;
13511
+ bidWindowMs;
13512
+ maxTasksPerAgent;
13513
+ minConfidence;
13514
+ // minimum dispatcher confidence to accept a bid
13515
+ maxBidRetries;
13516
+ // max republished attempts before marking task failed
13517
+ /** Pending bids keyed by taskId. */
13518
+ pendingBids = /* @__PURE__ */ new Map();
13519
+ /** Active bid windows keyed by taskId. */
13520
+ bidTimers = /* @__PURE__ */ new Map();
13521
+ /** FleetBus subscription disposers, detached in dispose(). */
13522
+ unsubs = [];
13523
+ /** How many times a task has been republished with no bids received. */
13524
+ bidRetryCounts = /* @__PURE__ */ new Map();
13525
+ /** Agent → current task count (from graph + in-flight). */
13526
+ agentTaskCounts = /* @__PURE__ */ new Map();
13527
+ constructor(opts) {
13528
+ this.graph = opts.graph;
13529
+ this.fleet = opts.fleet;
13530
+ this.mailbox = opts.mailbox;
13531
+ this.selfAgentId = opts.selfAgentId ?? "auctioneer";
13532
+ this.bidWindowMs = opts.bidWindowMs ?? 3e4;
13533
+ this.maxTasksPerAgent = opts.maxTasksPerAgent ?? 3;
13534
+ this.minConfidence = opts.minConfidence ?? 0.3;
13535
+ this.maxBidRetries = opts.maxBidRetries ?? 3;
13536
+ const offBid = this.fleet?.filter("task:bid", (e) => this._onBidEvent(e));
13537
+ const offClaimed = this.fleet?.filter("task:claimed", (e) => this._onClaimedEvent(e));
13538
+ if (offBid) this.unsubs.push(offBid);
13539
+ if (offClaimed) this.unsubs.push(offClaimed);
13540
+ }
13541
+ /**
13542
+ * Detach all FleetBus subscriptions and cancel any open bid-window timers.
13543
+ * Call when the owning coordinator stops/restarts so handlers and timers
13544
+ * don't accumulate across cycles.
13545
+ */
13546
+ dispose() {
13547
+ for (const off of this.unsubs.splice(0)) {
13548
+ try {
13549
+ off();
13550
+ } catch {
13551
+ }
13552
+ }
13553
+ for (const t of this.bidTimers.values()) clearTimeout(t);
13554
+ this.bidTimers.clear();
13555
+ }
13556
+ // ── Publish a task ────────────────────────────────────────────────────
13557
+ /**
13558
+ * Publish a new task to the auction. Creates a GoalNode and broadcasts
13559
+ * it to all online agents. Returns the goal id.
13560
+ *
13561
+ * If `targetAgent` is specified, the task is assigned directly without auction.
13562
+ */
13563
+ async publishTask(input) {
13564
+ const blockedBy = input.blockedBy ?? [];
13565
+ const hasOpenBlockers = blockedBy.length > 0 && blockedBy.some((id) => this.graph.get(id)?.status !== "done");
13566
+ const goal = await this.graph.add({
13567
+ type: "goal",
13568
+ title: input.title,
13569
+ description: input.description,
13570
+ status: input.targetAgent ? "in_progress" : hasOpenBlockers ? "blocked" : "pending",
13571
+ priority: input.priority ?? "medium",
13572
+ assignee: input.targetAgent,
13573
+ blockedBy,
13574
+ dependsOn: input.satisfiesGoals ?? [],
13575
+ createdBy: this.selfAgentId,
13576
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
13577
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
13578
+ tags: input.tags ?? [],
13579
+ children: [],
13580
+ parentGoal: input.parentGoal
13581
+ });
13582
+ if (input.parentGoal) {
13583
+ const parent = this.graph.get(input.parentGoal);
13584
+ if (parent) {
13585
+ await this.graph.update(input.parentGoal, {
13586
+ children: [...parent.children, goal.id]
13587
+ });
13588
+ }
13589
+ }
13590
+ for (const blockerId of blockedBy) {
13591
+ const blocker = this.graph.get(blockerId);
13592
+ if (blocker && !blocker.children.includes(goal.id)) {
13593
+ await this.graph.update(blockerId, { children: [...blocker.children, goal.id] });
13594
+ }
13595
+ }
13596
+ if (input.targetAgent) {
13597
+ await this._assignDirect(goal.id, input.targetAgent);
13598
+ } else {
13599
+ await this._broadcastTask(goal);
13600
+ this._startBidWindow(goal.id);
13601
+ }
13602
+ this._emit("task:published", {
13603
+ taskId: goal.id,
13604
+ title: goal.title,
13605
+ priority: goal.priority,
13606
+ tags: goal.tags
13607
+ });
13608
+ return goal.id;
13609
+ }
13610
+ // ── Bid on a task ─────────────────────────────────────────────────────
13611
+ /**
13612
+ * Submit a bid for a task. Called by agents who want to work on it.
13613
+ * Returns true if the bid was accepted, false if the task was already claimed.
13614
+ */
13615
+ async bid(taskId, agent, rationale) {
13616
+ const goal = this.graph.get(taskId);
13617
+ if (!goal || goal.type !== "goal") return false;
13618
+ if (goal.status !== "pending") return false;
13619
+ const currentCount = this._getAgentTaskCount(agent.agentId);
13620
+ if (currentCount >= this.maxTasksPerAgent) return false;
13621
+ const dispatchResult = await dispatchAgent(goal.description);
13622
+ const score = dispatchResult.confidence * (dispatchResult.role === agent.agentRole ? 1.2 : 1);
13623
+ if (score < this.minConfidence) return false;
13624
+ const bid = {
13625
+ id: randomUUID(),
13626
+ taskId,
13627
+ agentId: agent.agentId,
13628
+ agentName: agent.agentName,
13629
+ agentRole: agent.agentRole,
13630
+ score,
13631
+ rationale,
13632
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString()
13633
+ };
13634
+ let bids = this.pendingBids.get(taskId);
13635
+ if (!bids) {
13636
+ bids = [];
13637
+ this.pendingBids.set(taskId, bids);
13638
+ }
13639
+ const existingIdx = bids.findIndex((b) => b.agentId === agent.agentId);
13640
+ if (existingIdx >= 0) {
13641
+ bids[existingIdx] = bid;
13642
+ } else {
13643
+ bids.push(bid);
13644
+ }
13645
+ this._emit("task:bid", {
13646
+ taskId,
13647
+ bid: { ...bid, score: Math.round(score * 100) / 100 },
13648
+ agentName: agent.agentName
13649
+ });
13650
+ await this._mailboxPublish({
13651
+ type: "note",
13652
+ subject: `[bid] ${agent.agentName} \u2192 ${goal.title}`,
13653
+ body: `${agent.agentName} (${agent.agentRole}) bidded on task "${goal.title}" (${goal.id})
13654
+ Rationale: ${rationale}
13655
+ Score: ${score.toFixed(2)}`
13656
+ });
13657
+ return true;
13658
+ }
13659
+ // ── Claim (award) a task ───────────────────────────────────────────────
13660
+ /**
13661
+ * Award a task to a specific agent. Called internally by the bid window
13662
+ * expiry, or can be called directly to force an award.
13663
+ */
13664
+ async claim(taskId, agentId, agentName) {
13665
+ const goal = this.graph.get(taskId);
13666
+ if (!goal || goal.type !== "goal") return false;
13667
+ if (goal.status !== "pending") return false;
13668
+ this._cancelBidWindow(taskId);
13669
+ await this.graph.update(taskId, {
13670
+ status: "in_progress",
13671
+ assignee: agentId,
13672
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13673
+ });
13674
+ this.pendingBids.delete(taskId);
13675
+ this.agentTaskCount(agentId, 1);
13676
+ await this._notifyAgent(agentId, {
13677
+ type: "assign",
13678
+ subject: `[assigned] ${goal.title}`,
13679
+ body: `You have been assigned: "${goal.title}"
13680
+
13681
+ ${goal.description}
13682
+
13683
+ Task ID: ${taskId}
13684
+ Priority: ${goal.priority}`,
13685
+ taskContext: {
13686
+ agentRole: goal.tags[0],
13687
+ taskId,
13688
+ status: "in_progress"
13689
+ }
13690
+ });
13691
+ this._emit("task:claimed", { taskId, agentId, agentName });
13692
+ return true;
13693
+ }
13694
+ // ── Complete a task ────────────────────────────────────────────────────
13695
+ /**
13696
+ * Mark a task as done. Called by the agent when it finishes.
13697
+ */
13698
+ async complete(taskId, _result) {
13699
+ const goal = this.graph.get(taskId);
13700
+ if (!goal) return;
13701
+ const agentId = goal.assignee ?? "unknown";
13702
+ this.agentTaskCount(agentId, -1);
13703
+ await this.graph.update(taskId, {
13704
+ status: "done",
13705
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13706
+ });
13707
+ this.bidRetryCounts.delete(taskId);
13708
+ this.pendingBids.delete(taskId);
13709
+ this._cancelBidWindow(taskId);
13710
+ for (const childId of goal.children) {
13711
+ const child = this.graph.get(childId);
13712
+ if (child && child.status === "blocked") {
13713
+ const allUnblocked = child.blockedBy.every((blockedId) => {
13714
+ const blocked = this.graph.get(blockedId);
13715
+ return blocked?.status === "done";
13716
+ });
13717
+ if (allUnblocked) {
13718
+ await this.graph.update(childId, { status: "pending", updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
13719
+ const unblockedGoal = this.graph.get(childId);
13720
+ await this._broadcastTask(unblockedGoal);
13721
+ this._startBidWindow(childId);
13722
+ }
13723
+ }
13724
+ }
13725
+ this._emit("task:completed", { taskId, agentId, result: _result });
13726
+ await this._mailboxPublish({
13727
+ type: "result",
13728
+ subject: `[done] ${goal.title}`,
13729
+ body: `Task completed by ${agentId}: "${goal.title}"
13730
+
13731
+ ${_result ?? "No result provided."}`
13732
+ });
13733
+ }
13734
+ /**
13735
+ * Mark a task as failed. Optionally spawn a retry.
13736
+ */
13737
+ async fail(taskId, error) {
13738
+ const goal = this.graph.get(taskId);
13739
+ if (!goal) return;
13740
+ const agentId = goal.assignee ?? "unknown";
13741
+ this.agentTaskCount(agentId, -1);
13742
+ await this.graph.update(taskId, {
13743
+ status: "failed",
13744
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
13745
+ result: error
13746
+ });
13747
+ this.bidRetryCounts.delete(taskId);
13748
+ this.pendingBids.delete(taskId);
13749
+ this._cancelBidWindow(taskId);
13750
+ this._emit("task:failed", { taskId, agentId, error });
13751
+ await this._mailboxPublish({
13752
+ type: "note",
13753
+ subject: `[failed] ${goal.title}`,
13754
+ body: `Task failed: "${goal.title}"
13755
+ Error: ${error}
13756
+ Assignee: ${agentId}`
13757
+ });
13758
+ }
13759
+ // ── Work finding ──────────────────────────────────────────────────────
13760
+ /**
13761
+ * Find the best available tasks for an agent based on its capabilities.
13762
+ * Returns tasks sorted by match score (best first).
13763
+ */
13764
+ async findWork(_agentId, agentRole, limit = 5) {
13765
+ const pending = this.graph.getGoals({ status: "pending" });
13766
+ const scored = [];
13767
+ for (const goal of pending) {
13768
+ if (goal.blockedBy.length > 0) continue;
13769
+ const dispatchResult = await dispatchAgent(goal.description);
13770
+ const roleBonus = agentRole && goal.tags.includes(agentRole) ? 1.3 : 1;
13771
+ const priorityBonus = goal.priority === "critical" ? 1.5 : goal.priority === "high" ? 1.2 : 1;
13772
+ const score = dispatchResult.confidence * roleBonus * priorityBonus;
13773
+ const bids = this.pendingBids.get(goal.id)?.length ?? 0;
13774
+ scored.push({ task: goal, score, bids });
13775
+ }
13776
+ scored.sort((a, b) => b.score - a.score);
13777
+ return scored.slice(0, limit);
13778
+ }
13779
+ // ── Queries ──────────────────────────────────────────────────────────
13780
+ /** Get all pending tasks (available for bidding). */
13781
+ getPendingTasks() {
13782
+ return this.graph.getGoals({ status: "pending" }).filter((g) => g.blockedBy.length === 0);
13783
+ }
13784
+ /** Get tasks assigned to a specific agent. */
13785
+ getTasksForAgent(agentId) {
13786
+ return this.graph.getGoals({}).filter((g) => g.assignee === agentId);
13787
+ }
13788
+ /** Get the current bid count for a task. */
13789
+ getBidCount(taskId) {
13790
+ return this.pendingBids.get(taskId)?.length ?? 0;
13791
+ }
13792
+ /** Get bids for a task. */
13793
+ getBids(taskId) {
13794
+ return this.pendingBids.get(taskId) ?? [];
13795
+ }
13796
+ /** Get task stats for a project-wide dashboard. */
13797
+ getStats() {
13798
+ const all = this.graph.getGoals({});
13799
+ const pending = all.filter((g) => g.status === "pending" && g.blockedBy.length === 0);
13800
+ const inProgress = all.filter((g) => g.status === "in_progress");
13801
+ const done = all.filter((g) => g.status === "done");
13802
+ const failed = all.filter((g) => g.status === "failed");
13803
+ let totalBids = 0;
13804
+ for (const bids of this.pendingBids.values()) totalBids += bids.length;
13805
+ return {
13806
+ total: all.length,
13807
+ pending: pending.length,
13808
+ in_progress: inProgress.length,
13809
+ done: done.length,
13810
+ failed: failed.length,
13811
+ totalBids,
13812
+ avgBidsPerTask: pending.length > 0 ? totalBids / pending.length : 0
13813
+ };
13814
+ }
13815
+ // ── Private ───────────────────────────────────────────────────────────
13816
+ _emit(type, payload) {
13817
+ if (!this.fleet) return;
13818
+ this.fleet.emit({ subagentId: this.selfAgentId, ts: Date.now(), type, payload });
13819
+ }
13820
+ _broadcastTask(goal) {
13821
+ this._emit("task:available", {
13822
+ taskId: goal.id,
13823
+ title: goal.title,
13824
+ description: goal.description,
13825
+ priority: goal.priority,
13826
+ tags: goal.tags
13827
+ });
13828
+ this._mailboxPublish({
13829
+ type: "broadcast",
13830
+ subject: `[task] ${goal.title} (${goal.priority})`,
13831
+ body: `New task available: "${goal.title}"
13832
+ Priority: ${goal.priority}
13833
+ Description: ${goal.description.slice(0, 200)}${goal.description.length > 200 ? "..." : ""}
13834
+
13835
+ Task ID: ${goal.id}
13836
+ Tags: ${goal.tags.join(", ") || "none"}
13837
+
13838
+ Bid by calling taskAuctioneer.bid("${goal.id}", ...)`
13839
+ }).catch(() => {
13840
+ });
13841
+ }
13842
+ async _mailboxPublish(msg) {
13843
+ if (!this.mailbox) return;
13844
+ try {
13845
+ await this.mailbox.send({
13846
+ from: this.selfAgentId,
13847
+ to: "*",
13848
+ type: msg.type,
13849
+ subject: msg.subject,
13850
+ body: msg.body,
13851
+ priority: "normal"
13852
+ });
13853
+ } catch {
13854
+ }
13855
+ }
13856
+ async _notifyAgent(agentId, msg) {
13857
+ if (!this.mailbox) return;
13858
+ try {
13859
+ await this.mailbox.send({
13860
+ from: this.selfAgentId,
13861
+ to: agentId,
13862
+ type: msg.type,
13863
+ subject: msg.subject,
13864
+ body: msg.body,
13865
+ priority: "high",
13866
+ taskContext: msg.taskContext
13867
+ });
13868
+ } catch {
13869
+ }
13870
+ }
13871
+ _startBidWindow(taskId) {
13872
+ this._cancelBidWindow(taskId);
13873
+ const timer = setTimeout(() => {
13874
+ this.bidTimers.delete(taskId);
13875
+ void this._evaluateBids(taskId).catch(() => {
13876
+ });
13877
+ }, this.bidWindowMs);
13878
+ this.bidTimers.set(taskId, timer);
13879
+ }
13880
+ _cancelBidWindow(taskId) {
13881
+ const timer = this.bidTimers.get(taskId);
13882
+ if (timer) {
13883
+ clearTimeout(timer);
13884
+ this.bidTimers.delete(taskId);
13885
+ }
13886
+ }
13887
+ async _evaluateBids(taskId) {
13888
+ const bids = this.pendingBids.get(taskId);
13889
+ if (!bids || bids.length === 0) {
13890
+ const retryCount = (this.bidRetryCounts.get(taskId) ?? 0) + 1;
13891
+ this.bidRetryCounts.set(taskId, retryCount);
13892
+ if (retryCount >= this.maxBidRetries) {
13893
+ await this.fail(taskId, `No bids received after ${this.maxBidRetries} attempts`);
13894
+ this.bidRetryCounts.delete(taskId);
13895
+ return;
13896
+ }
13897
+ const goal = this.graph.get(taskId);
13898
+ if (goal) {
13899
+ await this._broadcastTask(goal);
13900
+ this._startBidWindow(taskId);
13901
+ }
13902
+ return;
13903
+ }
13904
+ bids.sort((a, b) => b.score - a.score);
13905
+ const winner = bids.find((b) => this._getAgentTaskCount(b.agentId) < this.maxTasksPerAgent);
13906
+ if (!winner) {
13907
+ const goal = this.graph.get(taskId);
13908
+ if (goal) {
13909
+ await this._broadcastTask(goal);
13910
+ this._startBidWindow(taskId);
13911
+ }
13912
+ return;
13913
+ }
13914
+ await this.claim(taskId, winner.agentId, winner.agentName);
13915
+ }
13916
+ async _assignDirect(taskId, agentId) {
13917
+ const goal = this.graph.get(taskId);
13918
+ if (!goal) return;
13919
+ await this.graph.update(taskId, {
13920
+ status: "in_progress",
13921
+ assignee: agentId,
13922
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13923
+ });
13924
+ this.agentTaskCount(agentId, 1);
13925
+ await this._notifyAgent(agentId, {
13926
+ type: "assign",
13927
+ subject: `[assigned] ${goal.title}`,
13928
+ body: `You have been directly assigned: "${goal.title}"
13929
+
13930
+ ${goal.description}`,
13931
+ taskContext: { taskId, status: "in_progress" }
13932
+ });
13933
+ }
13934
+ _onBidEvent(_e) {
13935
+ }
13936
+ _onClaimedEvent(e) {
13937
+ const { taskId } = e.payload;
13938
+ this.pendingBids.delete(taskId);
13939
+ this._cancelBidWindow(taskId);
13940
+ }
13941
+ _getAgentTaskCount(agentId) {
13942
+ return this.agentTaskCounts.get(agentId) ?? 0;
13943
+ }
13944
+ agentTaskCount(agentId, delta) {
13945
+ const current = this._getAgentTaskCount(agentId);
13946
+ const next = Math.max(0, current + delta);
13947
+ this.agentTaskCounts.set(agentId, next);
13948
+ }
13949
+ };
13950
+ var AutonomousCoordinator = class {
13951
+ graph;
13952
+ dag;
13953
+ auction;
13954
+ consensus;
13955
+ changes;
13956
+ brain;
13957
+ selfAgentId;
13958
+ fleet;
13959
+ fleetManager;
13960
+ director;
13961
+ mailbox;
13962
+ events;
13963
+ onCoordinatorEvent;
13964
+ running = false;
13965
+ iterationCount = 0;
13966
+ /** Tasks already handled by _onSubagentTerminated (to avoid double goal:failed on fleet event). */
13967
+ _handledBySubagent = /* @__PURE__ */ new Set();
13968
+ /** FleetBus subscription disposers, detached in dispose(). */
13969
+ unsubs = [];
13970
+ constructor(opts) {
13971
+ this.selfAgentId = opts.selfAgentId;
13972
+ this.fleet = opts.fleet ?? void 0;
13973
+ this.fleetManager = opts.fleetManager ?? void 0;
13974
+ this.director = opts.director ?? void 0;
13975
+ this.mailbox = opts.mailbox ?? void 0;
13976
+ this.events = opts.events ?? void 0;
13977
+ this.onCoordinatorEvent = opts.onCoordinatorEvent;
13978
+ this.graph = new KnowledgeGraph(opts.sessionDir);
13979
+ this.dag = new TaskDAG();
13980
+ this.auction = new TaskAuctioneer({
13981
+ graph: this.graph,
13982
+ fleet: this.fleet ?? void 0,
13983
+ mailbox: this.mailbox ?? void 0,
13984
+ selfAgentId: this.selfAgentId
13985
+ });
13986
+ this.consensus = new ConsensusProtocol({
13987
+ graph: this.graph,
13988
+ fleet: this.fleet ?? void 0,
13989
+ voters: this._buildVoters(),
13990
+ rules: {
13991
+ quorumFraction: 0.5,
13992
+ approvalFraction: 0.6,
13993
+ vetoRoles: ["critic"],
13994
+ // Critic has veto power
13995
+ approvalWeightFraction: 0.5
13996
+ }
13997
+ });
13998
+ this.changes = new ChangeManager({
13999
+ graph: this.graph,
14000
+ consensus: this.consensus,
14001
+ fleet: this.fleet ?? void 0,
14002
+ checks: DEFAULT_QUALITY_CHECKS
14003
+ });
14004
+ this.brain = new AutonomousBrain({
14005
+ llmProvider: opts.llmProvider,
14006
+ graph: this.graph,
14007
+ fleet: this.fleet ?? void 0,
14008
+ selfImprove: !opts.disableSelfImprove
14009
+ });
14010
+ this.dag.onEvent((event) => {
14011
+ this._onDagEvent(event);
14012
+ });
14013
+ const offCompleted = this.fleet?.filter("subagent.completed", (e) => {
14014
+ this._onSubagentTerminated(e);
14015
+ });
14016
+ if (offCompleted) this.unsubs.push(offCompleted);
14017
+ const offFailed = this.fleet?.filter("task:failed", (e) => {
14018
+ const payload = e.payload;
14019
+ const taskId = payload?.taskId;
14020
+ if (!taskId || this._handledBySubagent.has(taskId)) return;
14021
+ this._handledBySubagent.add(taskId);
14022
+ this._emit({ type: "goal:failed", goalId: taskId, text: payload?.error ?? "Task failed" });
14023
+ });
14024
+ if (offFailed) this.unsubs.push(offFailed);
14025
+ this._emit({ type: "coordinator:mode", mode: this.fleet ? "fleet" : "standalone" });
14026
+ }
14027
+ // ── Public API ───────────────────────────────────────────────────────
14028
+ /**
14029
+ * Run the autonomous loop until the goal is satisfied or max iterations reached.
14030
+ * This is the main entry point for a fully autonomous session.
14031
+ */
14032
+ async run(opts = {}) {
14033
+ if (this.running) throw new Error("AutonomousCoordinator: already running");
14034
+ this.running = true;
14035
+ this.iterationCount = 0;
14036
+ const maxIterations = opts.maxIterations ?? 100;
14037
+ const goal = opts.goal ?? "Improve the codebase";
14038
+ const maxCost = opts.maxCostUsd;
14039
+ try {
14040
+ await this.graph.load();
14041
+ const goalConfigs = await this._decomposeGoal(goal);
14042
+ for (const g of goalConfigs) {
14043
+ const goalId = await this.auction.publishTask(g);
14044
+ this.dag.addNode(goalId, g.description, []);
14045
+ this._emit({ type: "goal:added", goalId, title: g.title, text: g.description });
14046
+ }
14047
+ while (this.running) {
14048
+ this.iterationCount++;
14049
+ if (this.iterationCount >= maxIterations) break;
14050
+ if (maxCost !== void 0) {
14051
+ const cost = this.fleetManager?.snapshot()?.total?.cost ?? 0;
14052
+ if (cost >= maxCost) break;
14053
+ }
14054
+ if (opts.runUntilComplete && this.dag.isDone()) break;
14055
+ const decision = await this.brain.decideAuto({
14056
+ id: randomUUID(),
14057
+ source: "system",
14058
+ decisionType: "prioritize_goals",
14059
+ question: `What should we work on next? Open goals: ${this.auction.getPendingTasks().map((g) => g.title).join(", ") || "none"}`,
14060
+ context: {
14061
+ goals: this.auction.getPendingTasks(),
14062
+ fleetStatus: this._fleetStatus()
14063
+ },
14064
+ options: this._goalToOptions(this.auction.getPendingTasks()),
14065
+ risk: "medium",
14066
+ requiresConsensus: false
14067
+ });
14068
+ if (decision.type === "deny") {
14069
+ const blocked = this.dag.getBlocked();
14070
+ if (blocked.length > 0 && this.dag.hasDeadlock()) {
14071
+ (this.events?.emit)("autonomous:deadlock", { blocked });
14072
+ this._emit({ type: "deadlock:detected", goalId: blocked[0]?.id ?? "", text: `Deadlock detected: ${blocked.map((n) => n.id).join(", ")}` });
14073
+ this.running = false;
14074
+ }
14075
+ break;
14076
+ }
14077
+ if (decision.type === "ask_human") {
14078
+ (this.events?.emit)("autonomous:ask_human", { prompt: decision.prompt });
14079
+ break;
14080
+ }
14081
+ if (decision.optionId) {
14082
+ const goalNode = this._optionToGoal(decision.optionId);
14083
+ if (goalNode) {
14084
+ await this._processGoal(goalNode.id);
14085
+ }
14086
+ }
14087
+ const pendingChanges = this.changes.getPendingReviews();
14088
+ for (const change of pendingChanges) {
14089
+ try {
14090
+ await this._handlePendingChange(change);
14091
+ } catch (err) {
14092
+ this._emit({
14093
+ type: "goal:failed",
14094
+ goalId: change.id,
14095
+ text: `Consensus handling failed: ${err instanceof Error ? err.message : String(err)}`
14096
+ });
14097
+ }
14098
+ }
14099
+ }
14100
+ } finally {
14101
+ this.running = false;
14102
+ }
14103
+ return this.getStats();
14104
+ }
14105
+ /** Stop the autonomous loop. */
14106
+ stop() {
14107
+ if (!this.running) return;
14108
+ this.running = false;
14109
+ console.error(`[AutonomousCoordinator] stop signal received \u2014 shutting down (iteration ${this.iterationCount})`);
14110
+ }
14111
+ /**
14112
+ * Tear down the coordinator for good: stop the loop and detach all FleetBus
14113
+ * subscriptions (this coordinator's + the auctioneer's) plus any open bid
14114
+ * timers. Call this when discarding the instance (e.g. `/coordinator stop`
14115
+ * that recreates a fresh coordinator on the next start) so handlers and
14116
+ * timers don't accumulate across cycles. `stop()` only pauses the loop.
14117
+ */
14118
+ dispose() {
14119
+ this.stop();
14120
+ for (const off of this.unsubs.splice(0)) {
14121
+ try {
14122
+ off();
14123
+ } catch {
14124
+ }
14125
+ }
14126
+ this.auction.dispose();
14127
+ }
14128
+ /** Get a stats snapshot. */
14129
+ getStats() {
14130
+ const dagStats = this.dag.stats();
14131
+ const auctionStats = this.auction.getStats();
14132
+ const allGoals = this.graph.getGoals({});
14133
+ const allChanges = this.graph.getChanges({});
14134
+ const allDecisions = this.graph.getDecisions();
14135
+ return {
14136
+ goals: {
14137
+ total: allGoals.length,
14138
+ done: allGoals.filter((g) => g.status === "done").length,
14139
+ pending: allGoals.filter((g) => g.status === "pending").length,
14140
+ failed: allGoals.filter((g) => g.status === "failed").length,
14141
+ progress: allGoals.length > 0 ? allGoals.filter((g) => g.status === "done").length / allGoals.length : 0
14142
+ },
14143
+ dag: dagStats,
14144
+ auction: auctionStats,
14145
+ changes: {
14146
+ proposed: allChanges.filter((c) => c.status === "proposed").length,
14147
+ approved: allChanges.filter((c) => c.status === "approved").length,
14148
+ applied: allChanges.filter((c) => c.status === "applied").length,
14149
+ rejected: allChanges.filter((c) => c.status === "rejected").length
14150
+ },
14151
+ decisions: allDecisions.length,
14152
+ costSoFar: this.fleetManager?.snapshot()?.total?.cost
14153
+ };
14154
+ }
14155
+ // ── Fact publishing ──────────────────────────────────────────────────
14156
+ /**
14157
+ * Publish a fact discovered by an agent. Facts are immutable and form
14158
+ * the basis for other agents' decisions.
14159
+ */
14160
+ async publishFact(input) {
14161
+ const fact = await this.graph.add({
14162
+ type: "fact",
14163
+ category: input.category,
14164
+ subject: input.subject,
14165
+ detail: input.detail,
14166
+ file: input.file,
14167
+ line: input.line,
14168
+ severity: input.severity,
14169
+ discoveredBy: this.selfAgentId,
14170
+ discoveredAt: (/* @__PURE__ */ new Date()).toISOString(),
14171
+ tags: input.tags ?? [],
14172
+ key: `${input.category}:${input.subject}:${input.file ?? ""}:${input.line ?? ""}`,
14173
+ related: []
14174
+ });
14175
+ await this._mailboxBroadcast({
14176
+ type: "note",
14177
+ subject: `[${input.severity ?? "info"}] ${input.category}: ${input.subject}`,
14178
+ body: `**${input.category}**${input.file ? ` in ${input.file}${input.line ? `:${input.line}` : ""}` : ""}
14179
+ ${input.detail}`
14180
+ });
14181
+ this._emit({ type: "knowledge:added", knowledgeId: fact.id, title: input.subject, text: input.detail });
14182
+ return fact;
14183
+ }
14184
+ // ── Goal creation helpers ────────────────────────────────────────────
14185
+ /**
14186
+ * Publish a goal and add it to the DAG.
14187
+ */
14188
+ async createGoal(input) {
14189
+ const resolvedPriority = input.priority ?? "medium";
14190
+ const goalId = await this.auction.publishTask({
14191
+ title: input.title,
14192
+ description: input.description,
14193
+ priority: resolvedPriority,
14194
+ ...input.tags ? { tags: input.tags } : {},
14195
+ // Mirror the dependency edges into the auction so blocked goals aren't
14196
+ // biddable until their deps complete (the DAG tracks the same edges).
14197
+ ...input.deps && input.deps.length > 0 ? { blockedBy: input.deps } : {}
14198
+ });
14199
+ const goal = this.graph.get(goalId);
14200
+ for (const depId of input.deps ?? []) {
14201
+ this.dag.addNode(depId, this.graph.get(depId)?.type === "goal" ? this.graph.get(depId).title : depId);
14202
+ }
14203
+ this.dag.addNode(goalId, input.description, input.deps ?? []);
14204
+ return goal;
14205
+ }
14206
+ // ── Private ───────────────────────────────────────────────────────────
14207
+ async _decomposeGoal(goalText) {
14208
+ const category = this._inferCategory(goalText);
14209
+ const subGoals = [];
14210
+ if (category === "security") {
14211
+ subGoals.push({ title: "Audit for secrets", description: "Scan codebase for hardcoded secrets and API keys", priority: "critical", tags: ["security"] });
14212
+ subGoals.push({ title: "Check injection vectors", description: "Find eval, innerHTML, SQL concat, shell injection patterns", priority: "critical", tags: ["security", "injection"] });
14213
+ subGoals.push({ title: "Dependency audit", description: "Run npm/pnpm audit for known CVEs", priority: "high", tags: ["security", "deps"] });
14214
+ } else if (category === "bug") {
14215
+ subGoals.push({ title: "Find bugs", description: `Scan for bugs related to: ${goalText}`, priority: "high", tags: ["bug"] });
14216
+ subGoals.push({ title: "Fix bugs", description: "Fix discovered bugs with tests", priority: "high", tags: ["fix"] });
14217
+ } else if (category === "refactor") {
14218
+ subGoals.push({ title: "Plan refactor", description: `Analyze code structure for: ${goalText}`, priority: "medium", tags: ["refactor", "planning"] });
14219
+ subGoals.push({ title: "Implement refactor", description: "Apply the refactoring plan", priority: "medium", tags: ["refactor", "implementation"] });
14220
+ } else {
14221
+ subGoals.push({ title: goalText, description: goalText, priority: "medium", tags: [category] });
14222
+ }
14223
+ return subGoals;
14224
+ }
14225
+ _inferCategory(goal) {
14226
+ const g = goal.toLowerCase();
14227
+ if (g.includes("security") || g.includes("secret") || g.includes("injection")) return "security";
14228
+ if (g.includes("bug") || g.includes("fix") || g.includes("error")) return "bug";
14229
+ if (g.includes("refactor") || g.includes("debt") || g.includes("architecture")) return "architecture";
14230
+ if (g.includes("test") || g.includes("coverage")) return "test";
14231
+ if (g.includes("perf") || g.includes("speed") || g.includes("optimize")) return "perf";
14232
+ if (g.includes("deps") || g.includes("package") || g.includes("update")) return "deps";
14233
+ return "quality";
14234
+ }
14235
+ async _processGoal(goalId) {
14236
+ const ready = this.dag.getReady();
14237
+ if (ready.length === 0) return;
14238
+ const dagNode = ready.find((n) => n.id === goalId) ?? ready[0];
14239
+ this.dag.start(dagNode.id, "auctioneer");
14240
+ const goalNode = this.graph.get(goalId);
14241
+ if (!goalNode) return;
14242
+ const title = goalNode.title || dagNode.description;
14243
+ const taskId = await this.auction.publishTask({
14244
+ title,
14245
+ description: goalNode.description,
14246
+ priority: this._dagPriorityToGoal(dagNode.priority),
14247
+ tags: dagNode.tags
14248
+ });
14249
+ this._emit({ type: "task:ready", goalId, taskId, title });
14250
+ if (this.director) {
14251
+ const config = {
14252
+ name: `worker-${goalId.slice(0, 8)}`,
14253
+ role: "general",
14254
+ maxIterations: 100,
14255
+ timeoutMs: 6e5
14256
+ // 10 minutes per goal
14257
+ };
14258
+ const subagentId = await this.director.spawn(config);
14259
+ await this.auction.claim(taskId, subagentId, config.name);
14260
+ await this.director.assign({
14261
+ id: goalId,
14262
+ subagentId,
14263
+ description: goalNode.description
14264
+ });
14265
+ }
14266
+ }
14267
+ async _handlePendingChange(change) {
14268
+ const result = this.consensus.getStatus(change.id);
14269
+ if (result?.outcome !== "pending") return;
14270
+ if (change.qualityGate.passed) {
14271
+ const voteResult = await this.consensus.castVote(
14272
+ change.id,
14273
+ this.selfAgentId,
14274
+ "approve",
14275
+ `Quality gate passed: ${change.qualityGate.checks.map((c) => c.name).join(", ")}`
14276
+ );
14277
+ if (voteResult.outcome === "approved") {
14278
+ await this.changes.markApplied(change.id, (/* @__PURE__ */ new Date()).toISOString());
14279
+ this._emit({ type: "consensus:reached", goalId: change.id, text: "Change approved and applied" });
14280
+ }
14281
+ } else {
14282
+ const voteResult = await this.consensus.castVote(
14283
+ change.id,
14284
+ this.selfAgentId,
14285
+ "reject",
14286
+ `Quality gate failed: ${change.qualityGate.checks.map((c) => `${c.name}=${c.passed}`).join(", ")}`
14287
+ );
14288
+ if (voteResult.outcome === "rejected" || voteResult.outcome === "vetoed") {
14289
+ this._emit({ type: "consensus:reached", goalId: change.id, text: "Change rejected by quality gate" });
14290
+ }
14291
+ }
14292
+ }
14293
+ _onDagEvent(event) {
14294
+ if (event.type === "node:ready") {
14295
+ const node = this.dag.getNode(event.nodeId);
14296
+ if (node) {
14297
+ (this.events?.emit)("autonomous:task_ready", { taskId: event.nodeId, description: node.description });
14298
+ }
14299
+ }
14300
+ if (event.type === "graph:done") {
14301
+ (this.events?.emit)("autonomous:all_done", this.getStats());
14302
+ }
14303
+ }
14304
+ _onSubagentTerminated(e) {
14305
+ const payload = e.payload;
14306
+ const subagentId = payload?.subagentId ?? e.subagentId;
14307
+ const stopReason = payload?.stopReason ?? (payload?.status === "ok" ? "end_turn" : payload?.status ?? "unknown");
14308
+ const tasks = this.auction.getTasksForAgent(subagentId);
14309
+ for (const task of tasks) {
14310
+ this._handledBySubagent.add(task.id);
14311
+ if (stopReason === "end_turn") {
14312
+ void this.auction.complete(task.id, "Subagent completed successfully");
14313
+ this._emit({ type: "task:completed", goalId: task.id, taskId: task.id, text: "Subagent completed successfully" });
14314
+ } else {
14315
+ void this.auction.fail(task.id, `Subagent terminated: ${stopReason}`);
14316
+ this._emit({ type: "goal:failed", goalId: task.id, text: `Subagent terminated: ${stopReason}` });
14317
+ }
14318
+ }
14319
+ }
14320
+ _fleetStatus() {
14321
+ return {
14322
+ running: this.fleetManager?.getFleetStats().running ?? 0,
14323
+ idle: this.fleetManager?.getFleetStats().idle ?? 0,
14324
+ total: this.fleetManager?.getFleetStats().total ?? 0,
14325
+ costSoFar: this.fleetManager?.snapshot()?.total?.cost ?? 0
14326
+ };
14327
+ }
14328
+ _buildVoters() {
14329
+ return [
14330
+ // The coordinator itself casts the quality-gate auto-vote in
14331
+ // _handlePendingChange — it MUST be a registered, eligible voter or
14332
+ // castVote throws "unknown voter" and tears down the run() loop.
14333
+ { agentId: this.selfAgentId, agentName: "Coordinator", role: "coordinator", weight: 1 },
14334
+ { agentId: "critic", agentName: "Critic", role: "critic", weight: 2, veto: true },
14335
+ { agentId: "bug-hunter", agentName: "Bug Hunter", role: "bug-hunter", weight: 1.5 },
14336
+ { agentId: "security-scanner", agentName: "Security Scanner", role: "security-scanner", weight: 1.5 },
14337
+ { agentId: "audit-log", agentName: "Audit Log", role: "audit-log", weight: 1 },
14338
+ { agentId: "refactor-planner", agentName: "Refactor Planner", role: "refactor-planner", weight: 1 }
14339
+ ];
14340
+ }
14341
+ _goalToOptions(goals) {
14342
+ return goals.slice(0, 5).map((g, i) => ({
14343
+ id: g.id,
14344
+ label: `[${g.priority}] ${g.title}`,
14345
+ recommended: i === 0
14346
+ }));
14347
+ }
14348
+ _optionToGoal(optionId) {
14349
+ return this.graph.get(optionId);
14350
+ }
14351
+ _dagPriorityToGoal(p) {
14352
+ if (p <= 1) return "critical";
14353
+ if (p <= 2) return "high";
14354
+ if (p <= 4) return "medium";
14355
+ return "low";
14356
+ }
14357
+ async _mailboxBroadcast(msg) {
14358
+ if (!this.mailbox) return;
14359
+ try {
14360
+ await this.mailbox.send({
14361
+ from: this.selfAgentId,
14362
+ to: "*",
14363
+ type: msg.type,
14364
+ subject: msg.subject,
14365
+ body: msg.body,
14366
+ priority: "normal"
14367
+ });
14368
+ } catch {
14369
+ }
14370
+ }
14371
+ /** Emit a CoordinatorEvent to the subscriber (e.g. TUI panel timeline). */
14372
+ _emit(event) {
14373
+ this.onCoordinatorEvent?.(event);
14374
+ }
14375
+ };
11718
14376
 
11719
- export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, TOOLS as AGENT_TOOL_PRESETS, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, BUG_HUNTER_AGENT, BUILD_AGENTS, BrainDecisionQueue, BrainMonitor, BudgetExceededError, BudgetThresholdSignal, CollabSession, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_SUBAGENT_BASELINE, DELIVERY_AGENTS, DEPENDENCY_FILE_PATTERNS, DISCOVERY_AGENTS, DOMAIN_AGENTS, DefaultBrainArbiter, DefaultMailbox, DefaultMultiAgentCoordinator, Director, DirectorAlertLevel, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FleetBus, FleetCostCapError, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, GlobalMailbox, HEAVY_BUDGET, HumanEscalatingBrainArbiter, InMemoryAgentBridge, InMemoryBridgeTransport, KNOWLEDGE_AGENTS, LIGHT_BUDGET, LargeAnswerStore, MEDIUM_BUDGET, META_AGENTS, NULL_FLEET_BUS, ObservableBrainArbiter, PLANNING_AGENTS, REFACTOR_PLANNER_AGENT, REVIEW_AGENTS, SECURITY_SCANNER_AGENT, SubagentBudget, VERIFY_AGENTS, applyRosterBudget, attachAutoExtend, attachDepWatcherBridge, composeDirectorPrompt, composeSubagentPrompt, createDelegateTool, createMailboxHooks, createMessage, detectEcosystem, dispatchAgent, formatHumanPrompt, getAgentDefinition, getFullPackageLog, getManifestPackages, getPackageAuthor, getPackagesByAgent, mailboxSessionTag, makeAgentSubagentRunner, makeAskResultTool, makeAskTool, makeAssignTool, makeAwaitTasksTool, makeCollabDebugTool, makeDependencyWatcherConfig, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeMailInboxTool, makeMailSendTool, makeMailboxTool, makeRollUpTool, makeSpawnTool, makeTerminateTool, makeWorkCompleteTool, normalizeRecipient, recordPackageAction, resolveMailboxIdentity, resolveProjectDir, rosterSummaryFromConfigs, scoreAgents, startPackageOutdatedWatcher, updatePackageOutdatedStatus, withDisabledToolFiltering };
14377
+ export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, TOOLS as AGENT_TOOL_PRESETS, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, AutonomousBrain, AutonomousCoordinator, BUG_HUNTER_AGENT, BUILD_AGENTS, BrainDecisionQueue, BrainMonitor, BudgetExceededError, BudgetThresholdSignal, ChangeManager, CollabSession, ConsensusProtocol, DECISION_TIMEOUT_MS, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_QUALITY_CHECKS, DEFAULT_SUBAGENT_BASELINE, DELIVERY_AGENTS, DEPENDENCY_FILE_PATTERNS, DISCOVERY_AGENTS, DOMAIN_AGENTS, DefaultBrainArbiter, DefaultMailbox, DefaultMultiAgentCoordinator, Director, DirectorAlertLevel, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FleetBus, FleetCostCapError, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, GlobalMailbox, HEAVY_BUDGET, HumanEscalatingBrainArbiter, InMemoryAgentBridge, InMemoryBridgeTransport, KNOWLEDGE_AGENTS, KnowledgeGraph, LIGHT_BUDGET, LargeAnswerStore, MEDIUM_BUDGET, META_AGENTS, NULL_FLEET_BUS, ObservableBrainArbiter, PLANNING_AGENTS, REFACTOR_PLANNER_AGENT, REVIEW_AGENTS, SECURITY_SCANNER_AGENT, SubagentBudget, TIMEOUT_PREEMPT_FRACTION, TaskAuctioneer, TaskDAG, VERIFY_AGENTS, applyRosterBudget, attachAutoExtend, attachDepWatcherBridge, composeDirectorPrompt, composeSubagentPrompt, createDelegateTool, createMailboxHooks, createMessage, detectEcosystem, dispatchAgent, formatHumanPrompt, getAgentDefinition, getFullPackageLog, getManifestPackages, getPackageAuthor, getPackagesByAgent, mailboxSessionTag, makeAgentSubagentRunner, makeAskResultTool, makeAskTool, makeAssignTool, makeAwaitTasksTool, makeCollabDebugTool, makeDependencyWatcherConfig, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeMailInboxTool, makeMailSendTool, makeMailboxTool, makeRollUpTool, makeSpawnTool, makeTerminateTool, makeWorkCompleteTool, normalizeRecipient, recordPackageAction, resolveMailboxIdentity, resolveProjectDir, rosterSummaryFromConfigs, scoreAgents, startPackageOutdatedWatcher, updatePackageOutdatedStatus, withDisabledToolFiltering };
11720
14378
  //# sourceMappingURL=index.js.map
11721
14379
  //# sourceMappingURL=index.js.map