@wrongstack/core 0.1.4 → 0.1.8

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.
@@ -180,9 +180,11 @@ var DefaultPathResolver = class {
180
180
  ensureInsideRoot(absPath) {
181
181
  const resolved = this.resolve(absPath);
182
182
  if (!this.isInsideRoot(resolved)) {
183
- throw new Error(
184
- `Path "${absPath}" resolves outside the project root (${this.projectRoot})`
185
- );
183
+ const display = path2.isAbsolute(absPath) ? path2.basename(absPath) : absPath;
184
+ const err = new Error(`Path "${display}" resolves outside the project root`);
185
+ err.fullPath = absPath;
186
+ err.projectRoot = this.projectRoot;
187
+ throw err;
186
188
  }
187
189
  return resolved;
188
190
  }
@@ -352,7 +354,9 @@ var DefaultSessionStore = class {
352
354
  try {
353
355
  handle = await fsp.open(file, "a", 384);
354
356
  } catch (err) {
355
- throw new Error(`Failed to open session file: ${err instanceof Error ? err.message : String(err)}`);
357
+ throw new Error(`Failed to open session file: ${err instanceof Error ? err.message : String(err)}`, {
358
+ cause: err
359
+ });
356
360
  }
357
361
  try {
358
362
  return new FileSessionWriter(id, handle, startedAt, meta, { dir: this.dir, filePath: file });
@@ -370,7 +374,8 @@ var DefaultSessionStore = class {
370
374
  handle = await fsp.open(file, "a", 384);
371
375
  } catch (err) {
372
376
  throw new Error(
373
- `Failed to open session "${id}" for append: ${err instanceof Error ? err.message : String(err)}`
377
+ `Failed to open session "${id}" for append: ${err instanceof Error ? err.message : String(err)}`,
378
+ { cause: err }
374
379
  );
375
380
  }
376
381
  const writer = new FileSessionWriter(
@@ -393,7 +398,10 @@ var DefaultSessionStore = class {
393
398
  const events = [];
394
399
  for (const line of lines) {
395
400
  try {
396
- events.push(JSON.parse(line));
401
+ const parsed = JSON.parse(line);
402
+ if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
403
+ events.push(parsed);
404
+ }
397
405
  } catch {
398
406
  }
399
407
  }
@@ -410,7 +418,11 @@ var DefaultSessionStore = class {
410
418
  ids.map((id) => this.summaryFor(id).catch(() => null))
411
419
  );
412
420
  const out = sessions.filter((s) => s !== null);
413
- out.sort((a, b) => a.startedAt < b.startedAt ? 1 : -1);
421
+ out.sort((a, b) => {
422
+ if (a.startedAt < b.startedAt) return 1;
423
+ if (a.startedAt > b.startedAt) return -1;
424
+ return a.id.localeCompare(b.id);
425
+ });
414
426
  return out.slice(0, limit);
415
427
  } catch {
416
428
  return [];
@@ -556,6 +568,8 @@ var FileSessionWriter = class {
556
568
  filePath;
557
569
  initDone = false;
558
570
  resumed;
571
+ appendFailCount = 0;
572
+ lastAppendWarnAt = 0;
559
573
  async writeSessionStart() {
560
574
  if (this.initDone || this.closed) return;
561
575
  this.initDone = true;
@@ -584,7 +598,19 @@ var FileSessionWriter = class {
584
598
  await this.handle.appendFile(`${JSON.stringify(event)}
585
599
  `, "utf8");
586
600
  } catch (err) {
587
- console.warn("[session] append failed:", err instanceof Error ? err.message : String(err));
601
+ this.appendFailCount++;
602
+ const now = Date.now();
603
+ if (now - this.lastAppendWarnAt > 5e3) {
604
+ const suppressed = this.appendFailCount - 1;
605
+ const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
606
+ console.warn(
607
+ "[session] append failed:",
608
+ err instanceof Error ? err.message : String(err),
609
+ tail
610
+ );
611
+ this.lastAppendWarnAt = now;
612
+ this.appendFailCount = 0;
613
+ }
588
614
  }
589
615
  }
590
616
  /**
@@ -790,6 +816,15 @@ function mergeAdjacentText(blocks) {
790
816
  var MAX_BYTES_TOTAL = 32e3;
791
817
  var DefaultMemoryStore = class {
792
818
  files;
819
+ /**
820
+ * Per-scope serialization queue. `remember` / `forget` / `consolidate` /
821
+ * `clear` are read-modify-write against a single file; without a lock,
822
+ * two concurrent calls on the same scope can read the same baseline and
823
+ * the later write silently drops the earlier entry. We chain each
824
+ * mutation onto the prior promise for the same scope so they run in
825
+ * issue order. Different scopes still proceed in parallel.
826
+ */
827
+ writeChain = /* @__PURE__ */ new Map();
793
828
  constructor(opts) {
794
829
  this.files = {
795
830
  "project-agents": opts.paths.inProjectAgentsFile,
@@ -797,6 +832,18 @@ var DefaultMemoryStore = class {
797
832
  "user-memory": opts.paths.globalMemory
798
833
  };
799
834
  }
835
+ async runSerialized(scope, work) {
836
+ const prior = this.writeChain.get(scope) ?? Promise.resolve();
837
+ const next = prior.catch(() => void 0).then(work);
838
+ this.writeChain.set(scope, next);
839
+ try {
840
+ return await next;
841
+ } finally {
842
+ if (this.writeChain.get(scope) === next) {
843
+ this.writeChain.delete(scope);
844
+ }
845
+ }
846
+ }
800
847
  async readAll() {
801
848
  const parts = [];
802
849
  for (const scope of ["project-agents", "project-memory", "user-memory"]) {
@@ -815,27 +862,32 @@ ${body.trim()}`);
815
862
  }
816
863
  }
817
864
  async remember(text, scope = "project-memory") {
818
- const file = this.files[scope];
819
- await ensureDir(path2.dirname(file));
820
- let existing = "";
821
- try {
822
- existing = await fsp.readFile(file, "utf8");
823
- } catch {
824
- }
825
- const ts = (/* @__PURE__ */ new Date()).toISOString();
826
- const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
827
- const entry = `
865
+ return this.runSerialized(scope, async () => {
866
+ const file = this.files[scope];
867
+ await ensureDir(path2.dirname(file));
868
+ let existing = "";
869
+ try {
870
+ existing = await fsp.readFile(file, "utf8");
871
+ } catch {
872
+ }
873
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
874
+ const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
875
+ const entry = `
828
876
  - [${ts}] ${id} ${text.replace(/\n/g, " ")}
829
877
  `;
830
- const next = existing.trim() ? existing.replace(/\n+$/, "") + entry : `# WrongStack Memory
878
+ const next = existing.trim() ? existing.replace(/\n+$/, "") + entry : `# WrongStack Memory
831
879
  ${entry}`;
832
- await atomicWrite(file, next);
833
- const buf = Buffer.byteLength(next, "utf8");
834
- if (buf > MAX_BYTES_TOTAL) {
835
- await this.consolidate(scope);
836
- }
880
+ await atomicWrite(file, next);
881
+ const buf = Buffer.byteLength(next, "utf8");
882
+ if (buf > MAX_BYTES_TOTAL) {
883
+ await this.consolidateUnsafe(scope);
884
+ }
885
+ });
837
886
  }
838
887
  async forget(query, scope = "project-memory") {
888
+ return this.runSerialized(scope, async () => this.forgetUnsafe(query, scope));
889
+ }
890
+ async forgetUnsafe(query, scope) {
839
891
  const file = this.files[scope];
840
892
  let existing;
841
893
  try {
@@ -872,6 +924,9 @@ ${entry}`;
872
924
  return removed;
873
925
  }
874
926
  async consolidate(scope) {
927
+ return this.runSerialized(scope, async () => this.consolidateUnsafe(scope));
928
+ }
929
+ async consolidateUnsafe(scope) {
875
930
  const file = this.files[scope];
876
931
  let existing;
877
932
  try {
@@ -902,12 +957,14 @@ ${entry}`;
902
957
  }
903
958
  async clear(scope) {
904
959
  if (scope) {
905
- await atomicWrite(this.files[scope], "");
906
- } else {
907
- for (const s of ["project-agents", "project-memory", "user-memory"]) {
908
- await atomicWrite(this.files[s], "");
909
- }
960
+ await this.runSerialized(scope, async () => atomicWrite(this.files[scope], ""));
961
+ return;
910
962
  }
963
+ await Promise.all(
964
+ ["project-agents", "project-memory", "user-memory"].map(
965
+ (s) => this.runSerialized(s, async () => atomicWrite(this.files[s], ""))
966
+ )
967
+ );
911
968
  }
912
969
  };
913
970
  function labelOf(scope) {
@@ -955,9 +1012,27 @@ var PATTERNS = [
955
1012
  regex: /\b([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,})['"]?(?!\s*[A-Za-z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))/g
956
1013
  }
957
1014
  ];
1015
+ var SCRUB_CHUNK_BYTES = 64 * 1024;
958
1016
  var DefaultSecretScrubber = class {
959
1017
  scrub(text) {
960
1018
  if (!text) return text;
1019
+ if (text.length <= SCRUB_CHUNK_BYTES) {
1020
+ return this.scrubOne(text);
1021
+ }
1022
+ const out = [];
1023
+ let i = 0;
1024
+ while (i < text.length) {
1025
+ let end = Math.min(i + SCRUB_CHUNK_BYTES, text.length);
1026
+ if (end < text.length) {
1027
+ const nl = text.lastIndexOf("\n", end);
1028
+ if (nl > i + SCRUB_CHUNK_BYTES / 2) end = nl + 1;
1029
+ }
1030
+ out.push(this.scrubOne(text.slice(i, end)));
1031
+ i = end;
1032
+ }
1033
+ return out.join("");
1034
+ }
1035
+ scrubOne(text) {
961
1036
  let out = text;
962
1037
  for (const p of PATTERNS) {
963
1038
  out = out.replace(p.regex, (_match, group1, group2) => {
@@ -1062,7 +1137,17 @@ var DefaultSecretVault = class {
1062
1137
  }
1063
1138
  };
1064
1139
  function decryptConfigSecrets(cfg, vault) {
1065
- return walk(cfg, vault, (v) => vault.decrypt(v));
1140
+ return walk(cfg, vault, (v, key) => {
1141
+ try {
1142
+ return vault.decrypt(v);
1143
+ } catch (err) {
1144
+ console.warn(
1145
+ `[secret-vault] Failed to decrypt "${key}":`,
1146
+ err instanceof Error ? err.message : err
1147
+ );
1148
+ return "";
1149
+ }
1150
+ });
1066
1151
  }
1067
1152
  function encryptConfigSecrets(cfg, vault) {
1068
1153
  return walk(cfg, vault, (v) => vault.encrypt(v));
@@ -1076,7 +1161,7 @@ function walk(node, vault, transform) {
1076
1161
  const out = {};
1077
1162
  for (const [k, v] of Object.entries(node)) {
1078
1163
  if (typeof v === "string" && isSecretField(k)) {
1079
- out[k] = transform(v);
1164
+ out[k] = transform(v, k);
1080
1165
  } else if (typeof v === "object" && v !== null) {
1081
1166
  out[k] = walk(v, vault, transform);
1082
1167
  } else {
@@ -1150,9 +1235,11 @@ function walkCount(node, vault, counter) {
1150
1235
  }
1151
1236
  return out;
1152
1237
  }
1238
+ var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1153
1239
  function deepMerge(a, b) {
1154
1240
  const out = { ...a };
1155
1241
  for (const [k, v] of Object.entries(b)) {
1242
+ if (FORBIDDEN_PROTO_KEYS.has(k)) continue;
1156
1243
  const existing = out[k];
1157
1244
  if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
1158
1245
  out[k] = deepMerge(existing, v);
@@ -1165,7 +1252,22 @@ function deepMerge(a, b) {
1165
1252
 
1166
1253
  // src/utils/glob-match.ts
1167
1254
  function escapeRegex(s) {
1168
- return s.replace(/[.+^${}()|\\/]/g, "\\$&");
1255
+ return s.replace(/[.+^${}()|\\]/g, "\\$&");
1256
+ }
1257
+ var COMPILED_GLOB_CACHE = /* @__PURE__ */ new Map();
1258
+ var CACHE_MAX_SIZE = 2e3;
1259
+ function getCachedGlob(pattern) {
1260
+ const cached = COMPILED_GLOB_CACHE.get(pattern);
1261
+ if (cached) return cached;
1262
+ if (COMPILED_GLOB_CACHE.size >= CACHE_MAX_SIZE) {
1263
+ const keys = [...COMPILED_GLOB_CACHE.keys()];
1264
+ for (let i = 0; i < Math.floor(CACHE_MAX_SIZE / 4); i++) {
1265
+ COMPILED_GLOB_CACHE.delete(keys[i]);
1266
+ }
1267
+ }
1268
+ const re = compileGlob(pattern);
1269
+ COMPILED_GLOB_CACHE.set(pattern, re);
1270
+ return re;
1169
1271
  }
1170
1272
  function compileGlob(pattern) {
1171
1273
  let i = 0;
@@ -1214,7 +1316,7 @@ function compileGlob(pattern) {
1214
1316
  return new RegExp(re);
1215
1317
  }
1216
1318
  function matchGlob(pattern, input) {
1217
- return compileGlob(pattern).test(input);
1319
+ return getCachedGlob(pattern).test(input);
1218
1320
  }
1219
1321
  function matchAny(patterns, input) {
1220
1322
  return patterns.some((p) => matchGlob(p, input));
@@ -1261,7 +1363,7 @@ var DefaultPermissionPolicy = class {
1261
1363
  if (!this.loaded) await this.reload();
1262
1364
  const namespaceEntry = this.findNamespaceEntry(tool.name);
1263
1365
  const entry = this.policy[tool.name] ?? namespaceEntry;
1264
- const subject = this.subjectFor(tool.name, input);
1366
+ const subject = this.subjectFor(tool.name, input, tool.subjectKey);
1265
1367
  if (entry?.deny && subject && matchAny(entry.deny, subject)) {
1266
1368
  return { permission: "deny", source: "deny", reason: "matched deny pattern" };
1267
1369
  }
@@ -1309,16 +1411,23 @@ var DefaultPermissionPolicy = class {
1309
1411
  throw err;
1310
1412
  }
1311
1413
  }
1312
- subjectFor(toolName, input) {
1414
+ subjectFor(toolName, input, subjectKey) {
1313
1415
  if (!input || typeof input !== "object") return void 0;
1314
1416
  const obj = input;
1315
1417
  const globChars = /[*?\[\]]/g;
1316
1418
  const escapeGlob = (s) => s.replace(globChars, (c) => `\\${c}`);
1419
+ const normalizePath = (s) => escapeGlob(s.replace(/\\/g, "/"));
1420
+ if (subjectKey) {
1421
+ const v = obj[subjectKey];
1422
+ if (typeof v === "string") {
1423
+ return subjectKey === "path" || subjectKey === "file" || subjectKey === "files" ? normalizePath(v) : escapeGlob(v);
1424
+ }
1425
+ }
1317
1426
  if (toolName === "bash" && typeof obj.command === "string") {
1318
1427
  return escapeGlob(obj.command);
1319
1428
  }
1320
1429
  if (typeof obj.path === "string") {
1321
- return escapeGlob(obj.path.replace(/\\/g, "/"));
1430
+ return normalizePath(obj.path);
1322
1431
  }
1323
1432
  if (typeof obj.url === "string") {
1324
1433
  return escapeGlob(obj.url);
@@ -1464,14 +1573,15 @@ function providerStatusToCode(status, type) {
1464
1573
  }
1465
1574
 
1466
1575
  // src/defaults/retry-policy.ts
1467
- var DefaultRetryPolicy = class {
1576
+ var DefaultRetryPolicy = class _DefaultRetryPolicy {
1577
+ static NETWORK_ERR_RE = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i;
1468
1578
  shouldRetry(err, attempt) {
1469
1579
  if (err instanceof ProviderError) {
1470
1580
  if (!err.retryable) return false;
1471
1581
  return attempt < this.maxAttempts(err);
1472
1582
  }
1473
1583
  const msg = err.message ?? "";
1474
- const isNetwork = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(msg);
1584
+ const isNetwork = _DefaultRetryPolicy.NETWORK_ERR_RE.test(msg);
1475
1585
  if (isNetwork) return attempt < 2;
1476
1586
  return false;
1477
1587
  }
@@ -1493,55 +1603,43 @@ var DefaultRetryPolicy = class {
1493
1603
  };
1494
1604
 
1495
1605
  // src/defaults/error-handler.ts
1606
+ var CONTEXT_OVERFLOW_RE = /context|too long|tokens/i;
1607
+ var NETWORK_ERR_RE = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i;
1496
1608
  function buildRecoveryStrategies(opts) {
1497
1609
  return [
1498
1610
  {
1499
1611
  label: "context_overflow_reduce",
1500
1612
  compactor: opts?.compactor,
1501
1613
  async attempt(err, ctx) {
1502
- if (err instanceof ProviderError && (err.status === 413 || /context|too long|tokens/i.test(err.message))) {
1503
- if (this.compactor) {
1504
- try {
1505
- const report = await this.compactor.compact(ctx, { aggressive: true });
1506
- if (report.after < report.before) {
1507
- return {
1508
- content: [{ type: "text", text: "[context compacted automatically \u2014 please retry]" }],
1509
- stopReason: "end_turn",
1510
- usage: { input: 0, output: 0 },
1511
- model: ctx.model
1512
- };
1513
- }
1514
- } catch {
1614
+ if (!(err instanceof ProviderError)) return null;
1615
+ if (err.status !== 413 && !CONTEXT_OVERFLOW_RE.test(err.message)) return null;
1616
+ if (this.compactor) {
1617
+ try {
1618
+ const report = await this.compactor.compact(ctx, { aggressive: true });
1619
+ if (report.after < report.before) {
1620
+ return { action: "retry", reason: "context_compacted" };
1515
1621
  }
1622
+ } catch {
1516
1623
  }
1517
- return null;
1518
1624
  }
1519
1625
  return null;
1520
1626
  }
1521
1627
  },
1522
1628
  {
1523
1629
  label: "rate_limit_backoff",
1524
- async attempt(err, ctx) {
1525
- if (err instanceof ProviderError && err.status === 429) {
1526
- const delayMs = err.body?.retryAfterMs ?? 5e3;
1527
- const delay = Math.max(1e3, Math.min(delayMs, 6e4));
1528
- await new Promise((r) => setTimeout(r, delay));
1529
- return {
1530
- content: [{ type: "text", text: "[rate limit backoff applied \u2014 please retry]" }],
1531
- stopReason: "end_turn",
1532
- usage: { input: 0, output: 0 },
1533
- model: ctx.model
1534
- };
1535
- }
1536
- return null;
1630
+ async attempt(err) {
1631
+ if (!(err instanceof ProviderError) || err.status !== 429) return null;
1632
+ const delayMs = err.body?.retryAfterMs ?? 5e3;
1633
+ const delay = Math.max(1e3, Math.min(delayMs, 6e4));
1634
+ await new Promise((r) => setTimeout(r, delay));
1635
+ return { action: "retry", reason: "rate_limit_backoff" };
1537
1636
  }
1538
1637
  },
1539
1638
  {
1540
1639
  label: "downgrade_model",
1541
1640
  async attempt(err, ctx) {
1542
- if (err instanceof ProviderError && (err.status === 429 || err.status === 529 || err.status >= 500)) {
1543
- return null;
1544
- }
1641
+ if (!(err instanceof ProviderError)) return null;
1642
+ if (err.status !== 429 && err.status !== 529 && err.status < 500) return null;
1545
1643
  return null;
1546
1644
  }
1547
1645
  }
@@ -1564,12 +1662,12 @@ var DefaultErrorHandler = class {
1564
1662
  if (err.status === 429) return { kind: "rate_limit", retryable: true };
1565
1663
  if (err.status === 529) return { kind: "overloaded", retryable: true };
1566
1664
  if (err.status >= 500) return { kind: "server", retryable: true };
1567
- if (err.status === 413 || /context|too long|tokens/i.test(err.message)) {
1665
+ if (err.status === 413 || CONTEXT_OVERFLOW_RE.test(err.message)) {
1568
1666
  return { kind: "context_overflow", retryable: false };
1569
1667
  }
1570
1668
  if (err.status >= 400) return { kind: "client", retryable: false };
1571
1669
  }
1572
- if (err instanceof Error && /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(err.message)) {
1670
+ if (err instanceof Error && NETWORK_ERR_RE.test(err.message)) {
1573
1671
  return { kind: "network", retryable: true };
1574
1672
  }
1575
1673
  return { kind: "unknown", retryable: false };
@@ -1633,13 +1731,28 @@ var DefaultSkillLoader = class {
1633
1731
  async manifestText() {
1634
1732
  const skills = await this.list();
1635
1733
  if (skills.length === 0) return "";
1734
+ const entries = await this.listEntries();
1636
1735
  const lines = ["## Available skills"];
1637
- for (const s of skills) {
1638
- lines.push(`- **${s.name}** \u2014 ${s.description.replace(/\n/g, " ").trim()}`);
1639
- lines.push(` Path: ${s.path}`);
1736
+ for (const e of entries) {
1737
+ const scopeTag = e.scope.length > 0 ? ` \u2014 ${e.scope.slice(0, 3).join(", ")}` : "";
1738
+ lines.push(`- **${e.name}**${scopeTag}`);
1739
+ lines.push(` Use when: ${e.trigger}`);
1640
1740
  }
1641
1741
  return lines.join("\n");
1642
1742
  }
1743
+ async listEntries() {
1744
+ const skills = await this.list();
1745
+ const entries = [];
1746
+ for (const s of skills) {
1747
+ try {
1748
+ const raw = await fsp.readFile(s.path, "utf8");
1749
+ const { trigger, scope } = parseDescription(raw);
1750
+ entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
1751
+ } catch {
1752
+ }
1753
+ }
1754
+ return entries;
1755
+ }
1643
1756
  async readBody(name) {
1644
1757
  const m = await this.find(name);
1645
1758
  if (!m) throw new Error(`Skill "${name}" not found`);
@@ -1682,6 +1795,19 @@ function parseFrontmatter(raw) {
1682
1795
  flush();
1683
1796
  return out;
1684
1797
  }
1798
+ function parseDescription(raw) {
1799
+ const fm = parseFrontmatter(raw);
1800
+ const desc = fm.description ?? "";
1801
+ const firstSentenceEnd = desc.indexOf(". ");
1802
+ const trigger = firstSentenceEnd !== -1 ? desc.slice(0, firstSentenceEnd + 1).trim() : desc.trim().split("\n")[0] ?? "";
1803
+ const scope = [];
1804
+ const coversMatch = /(?:covers|for|including)\s+([^.]+)/i.exec(desc);
1805
+ if (coversMatch) {
1806
+ const items = coversMatch[1].replace(/[·•]/g, ",").split(",").map((s) => s.trim()).filter(Boolean);
1807
+ scope.push(...items);
1808
+ }
1809
+ return { trigger, scope };
1810
+ }
1685
1811
  var BEHAVIOR_DEFAULTS = {
1686
1812
  version: 1,
1687
1813
  context: {
@@ -1730,11 +1856,13 @@ var ENV_MAP = {
1730
1856
  function isPrimitiveArray(a) {
1731
1857
  return a.every((v) => v === null || typeof v !== "object");
1732
1858
  }
1859
+ var FORBIDDEN_PROTO_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1733
1860
  function deepMerge2(base, patch) {
1734
1861
  if (typeof base !== "object" || base === null) return patch ?? base;
1735
1862
  if (typeof patch !== "object" || patch === null) return base;
1736
1863
  const out = { ...base };
1737
1864
  for (const [k, v] of Object.entries(patch)) {
1865
+ if (FORBIDDEN_PROTO_KEYS2.has(k)) continue;
1738
1866
  const existing = out[k];
1739
1867
  if (Array.isArray(v)) {
1740
1868
  if (Array.isArray(existing) && isPrimitiveArray(v) && isPrimitiveArray(existing)) {
@@ -1802,8 +1930,12 @@ var DefaultConfigLoader = class {
1802
1930
  if (cfg.providers) {
1803
1931
  for (const pcfg of Object.values(cfg.providers)) {
1804
1932
  if (!pcfg || typeof pcfg !== "object") continue;
1805
- const keys = pcfg.apiKeys;
1806
- if (!Array.isArray(keys) || keys.length === 0) continue;
1933
+ const rawKeys = pcfg.apiKeys;
1934
+ if (!Array.isArray(rawKeys) || rawKeys.length === 0) continue;
1935
+ const keys = rawKeys.filter(
1936
+ (k) => !!k && typeof k === "object" && typeof k.label === "string" && typeof k.apiKey === "string"
1937
+ );
1938
+ if (keys.length === 0) continue;
1807
1939
  const existing = pcfg.apiKey;
1808
1940
  if (existing && existing.length > 0) continue;
1809
1941
  const activeLabel = pcfg.activeKey;
@@ -1814,23 +1946,42 @@ var DefaultConfigLoader = class {
1814
1946
  }
1815
1947
  }
1816
1948
  this.validateBehavior(cfg);
1817
- if (this.strict) this.validateIdentity(cfg);
1949
+ if (this.strict) {
1950
+ this.validateIdentity(cfg);
1951
+ }
1818
1952
  return Object.freeze(cfg);
1819
1953
  }
1820
1954
  async readJson(file) {
1955
+ let raw;
1821
1956
  try {
1822
- const raw = await fsp.readFile(file, "utf8");
1823
- const parsed = safeParse(raw);
1824
- if (parsed.ok && parsed.value) return parsed.value;
1825
- } catch {
1957
+ raw = await fsp.readFile(file, "utf8");
1958
+ } catch (err) {
1959
+ if (err.code !== "ENOENT") {
1960
+ console.warn(`[config] Failed to read "${file}":`, err);
1961
+ }
1962
+ return {};
1963
+ }
1964
+ const parsed = safeParse(raw);
1965
+ if (!parsed.ok || !parsed.value) {
1966
+ console.warn(
1967
+ `[config] Failed to parse "${file}": invalid JSON. Falling back to defaults for this layer.`
1968
+ );
1969
+ return {};
1826
1970
  }
1827
- return {};
1971
+ return parsed.value;
1828
1972
  }
1829
1973
  validateBehavior(cfg) {
1830
1974
  if (cfg.version === void 0) throw new Error("Config: missing version field");
1831
1975
  if (cfg.version !== 1) throw new Error(`Config: unsupported version ${cfg.version}`);
1832
1976
  const c = cfg.context;
1833
1977
  if (!c) throw new Error("Config: missing context section");
1978
+ const fields = ["warnThreshold", "softThreshold", "hardThreshold"];
1979
+ for (const f of fields) {
1980
+ const v = c[f];
1981
+ if (typeof v !== "number" || !Number.isFinite(v)) {
1982
+ throw new Error(`Config: context.${String(f)} must be a finite number (got ${typeof v})`);
1983
+ }
1984
+ }
1834
1985
  if (c.warnThreshold >= c.softThreshold || c.softThreshold >= c.hardThreshold) {
1835
1986
  throw new Error("Config: context thresholds must satisfy warn < soft < hard");
1836
1987
  }
@@ -1878,7 +2029,8 @@ var DefaultConfigStore = class {
1878
2029
  for (const w of this.watchers) {
1879
2030
  try {
1880
2031
  w(next, prev);
1881
- } catch {
2032
+ } catch (err) {
2033
+ console.error("[config-store] watcher threw:", err);
1882
2034
  }
1883
2035
  }
1884
2036
  return next;
@@ -1955,21 +2107,33 @@ var DEFAULT_CONFIG_MIGRATIONS = [];
1955
2107
 
1956
2108
  // src/utils/token-estimate.ts
1957
2109
  var RoughTokenEstimate = (text) => Math.max(1, Math.ceil(text.length / 4));
2110
+ var ESTIMATE_CACHE = /* @__PURE__ */ new Map();
2111
+ var ESTIMATE_CACHE_MAX_SIZE = 1e4;
2112
+ function getCachedEstimate(key, compute) {
2113
+ const existing = ESTIMATE_CACHE.get(key);
2114
+ if (existing !== void 0) return existing;
2115
+ if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
2116
+ const keys = [...ESTIMATE_CACHE.keys()];
2117
+ for (let i = 0; i < Math.floor(ESTIMATE_CACHE_MAX_SIZE / 4); i++) {
2118
+ ESTIMATE_CACHE.delete(keys[i]);
2119
+ }
2120
+ }
2121
+ const estimate = compute();
2122
+ ESTIMATE_CACHE.set(key, estimate);
2123
+ return estimate;
2124
+ }
1958
2125
  function estimateToolInputTokens(input) {
1959
2126
  if (typeof input === "string") return RoughTokenEstimate(input);
1960
- if (input !== null && typeof input === "object" && "__tokenEstimate" in input) {
1961
- return input.__tokenEstimate;
1962
- }
1963
- const str = typeof input === "object" ? JSON.stringify(input) : String(input);
1964
- const estimate = RoughTokenEstimate(str);
1965
- if (input !== null && typeof input === "object" && !Array.isArray(input)) {
1966
- input.__tokenEstimate = estimate;
2127
+ if (input === null || typeof input !== "object") {
2128
+ return RoughTokenEstimate(String(input));
1967
2129
  }
1968
- return estimate;
2130
+ const key = JSON.stringify(input);
2131
+ return getCachedEstimate(key, () => RoughTokenEstimate(key));
1969
2132
  }
1970
2133
  function estimateToolResultTokens(content) {
1971
2134
  if (typeof content === "string") return RoughTokenEstimate(content);
1972
- return RoughTokenEstimate(JSON.stringify(content));
2135
+ const key = JSON.stringify(content);
2136
+ return getCachedEstimate(key, () => RoughTokenEstimate(key));
1973
2137
  }
1974
2138
  function estimateTextTokens(text) {
1975
2139
  return RoughTokenEstimate(text);
@@ -2010,9 +2174,18 @@ var HybridCompactor = class {
2010
2174
  }
2011
2175
  }
2012
2176
  let saved = 0;
2013
- for (let i = 0; i < preserveStart; i++) {
2177
+ let changed = false;
2178
+ const nextMessages = new Array(messages.length);
2179
+ for (let i = 0; i < messages.length; i++) {
2014
2180
  const msg = messages[i];
2015
- if (!msg || !Array.isArray(msg.content)) continue;
2181
+ if (i >= preserveStart) {
2182
+ nextMessages[i] = msg;
2183
+ continue;
2184
+ }
2185
+ if (!msg || !Array.isArray(msg.content)) {
2186
+ nextMessages[i] = msg;
2187
+ continue;
2188
+ }
2016
2189
  const newContent = msg.content.map((b) => {
2017
2190
  if (b.type !== "tool_result") return b;
2018
2191
  const tokens = estimateToolResultTokens(b.content);
@@ -2026,8 +2199,14 @@ var HybridCompactor = class {
2026
2199
  };
2027
2200
  return elided;
2028
2201
  });
2029
- messages[i] = { ...msg, content: newContent };
2202
+ if (newContent.length === msg.content.length && newContent.every((b, idx) => b === msg.content[idx])) {
2203
+ nextMessages[i] = msg;
2204
+ } else {
2205
+ nextMessages[i] = { ...msg, content: newContent };
2206
+ changed = true;
2207
+ }
2030
2208
  }
2209
+ if (changed) ctx.state.replaceMessages(nextMessages);
2031
2210
  return saved;
2032
2211
  }
2033
2212
  collapseAncientTurns(ctx) {
@@ -2061,10 +2240,10 @@ var HybridCompactor = class {
2061
2240
  let total = 0;
2062
2241
  for (const m of messages) {
2063
2242
  if (typeof m.content === "string") {
2064
- total += estimateTextTokens(m.content);
2243
+ total += this.estimator(m.content);
2065
2244
  } else {
2066
2245
  for (const b of m.content) {
2067
- if (b.type === "text") total += estimateTextTokens(b.text);
2246
+ if (b.type === "text") total += this.estimator(b.text);
2068
2247
  else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
2069
2248
  else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
2070
2249
  }
@@ -2109,7 +2288,7 @@ var IntelligentCompactor = class {
2109
2288
  const beforeTokens = this.estimateTokens(ctx.messages);
2110
2289
  const reductions = [];
2111
2290
  const load = beforeTokens / this.maxContext;
2112
- const aggressive = opts.aggressive ?? load >= this.softThreshold;
2291
+ const aggressive = load >= this.hardThreshold ? true : opts.aggressive ?? load >= this.softThreshold;
2113
2292
  const saved1 = this.eliseOldToolResults(ctx);
2114
2293
  if (saved1 > 0) reductions.push({ phase: "elision", saved: saved1 });
2115
2294
  if (aggressive) {
@@ -2218,9 +2397,18 @@ var IntelligentCompactor = class {
2218
2397
  }
2219
2398
  }
2220
2399
  let saved = 0;
2221
- for (let i = 0; i < preserveStart; i++) {
2400
+ let changed = false;
2401
+ const nextMessages = new Array(messages.length);
2402
+ for (let i = 0; i < messages.length; i++) {
2222
2403
  const msg = messages[i];
2223
- if (!msg || !Array.isArray(msg.content)) continue;
2404
+ if (i >= preserveStart) {
2405
+ nextMessages[i] = msg;
2406
+ continue;
2407
+ }
2408
+ if (!msg || !Array.isArray(msg.content)) {
2409
+ nextMessages[i] = msg;
2410
+ continue;
2411
+ }
2224
2412
  const newContent = msg.content.map((b) => {
2225
2413
  if (b.type !== "tool_result") return b;
2226
2414
  const tokens = estimateToolResultTokens(b.content);
@@ -2233,8 +2421,14 @@ var IntelligentCompactor = class {
2233
2421
  is_error: b.is_error
2234
2422
  };
2235
2423
  });
2236
- messages[i] = { ...msg, content: newContent };
2424
+ if (newContent.length === msg.content.length && newContent.every((b, idx) => b === msg.content[idx])) {
2425
+ nextMessages[i] = msg;
2426
+ } else {
2427
+ nextMessages[i] = { ...msg, content: newContent };
2428
+ changed = true;
2429
+ }
2237
2430
  }
2431
+ if (changed) ctx.state.replaceMessages(nextMessages);
2238
2432
  return saved;
2239
2433
  }
2240
2434
  hasTextContent(m) {
@@ -2387,7 +2581,7 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
2387
2581
  const jsonEnd = raw.lastIndexOf("}");
2388
2582
  if (jsonStart === -1 || jsonEnd === -1) {
2389
2583
  return this.fallbackSelect(
2390
- Array.from({ length: messageCount }, (_, i) => ({ role: "user", content: "" })),
2584
+ Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
2391
2585
  this.maxContextTokens
2392
2586
  );
2393
2587
  }
@@ -2396,7 +2590,7 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
2396
2590
  parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
2397
2591
  } catch {
2398
2592
  return this.fallbackSelect(
2399
- Array.from({ length: messageCount }, (_, i) => ({ role: "user", content: "" })),
2593
+ Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
2400
2594
  this.maxContextTokens
2401
2595
  );
2402
2596
  }
@@ -2449,7 +2643,7 @@ var SelectiveCompactor = class {
2449
2643
  const savedElision = this.eliseOldToolResults(ctx);
2450
2644
  if (savedElision > 0) reductions.push({ phase: "elision", saved: savedElision });
2451
2645
  const afterPhase1 = this.estimateTokens(ctx.messages);
2452
- const targetBudget = this.computeTargetBudget(load, opts.aggressive ?? false);
2646
+ const targetBudget = this.computeTargetBudget(load);
2453
2647
  if (afterPhase1 > targetBudget) {
2454
2648
  const savedSelective = await this.runSelector(ctx, targetBudget);
2455
2649
  if (savedSelective > 0) reductions.push({ phase: "selective", saved: savedSelective });
@@ -2467,7 +2661,7 @@ var SelectiveCompactor = class {
2467
2661
  try {
2468
2662
  result = await this.selector.select(ctx.messages, targetBudget);
2469
2663
  } catch {
2470
- return this.aggressiveRecencyTrim(ctx, targetBudget);
2664
+ return this.aggressiveRecencyTrim(ctx);
2471
2665
  }
2472
2666
  await this.executePlan(ctx, result);
2473
2667
  const after = this.estimateTokens(ctx.messages);
@@ -2522,9 +2716,8 @@ Summarize the following message range:`;
2522
2716
  * Fallback when selector fails: aggressively trim from the oldest end
2523
2717
  * until we hit targetBudget.
2524
2718
  */
2525
- aggressiveRecencyTrim(ctx, targetBudget) {
2719
+ aggressiveRecencyTrim(ctx) {
2526
2720
  const messages = ctx.messages;
2527
- this.estimateTokens(messages);
2528
2721
  const preserveIdx = Math.max(0, messages.length - this.preserveK * 2);
2529
2722
  if (preserveIdx <= 0) return 0;
2530
2723
  let boundary = preserveIdx;
@@ -2545,7 +2738,7 @@ Summarize the following message range:`;
2545
2738
  ctx.state.replaceMessages([summaryMsg, ...tail]);
2546
2739
  return Math.max(0, removedTokens - this.estimateTokens([summaryMsg]));
2547
2740
  }
2548
- computeTargetBudget(load, aggressive) {
2741
+ computeTargetBudget(load) {
2549
2742
  if (load >= this.hardThreshold) {
2550
2743
  return Math.floor(this.maxContext * 0.5);
2551
2744
  }
@@ -2567,9 +2760,18 @@ Summarize the following message range:`;
2567
2760
  }
2568
2761
  }
2569
2762
  let saved = 0;
2570
- for (let i = 0; i < preserveStart; i++) {
2763
+ let changed = false;
2764
+ const nextMessages = new Array(messages.length);
2765
+ for (let i = 0; i < messages.length; i++) {
2571
2766
  const msg = messages[i];
2572
- if (!msg || !Array.isArray(msg.content)) continue;
2767
+ if (i >= preserveStart) {
2768
+ nextMessages[i] = msg;
2769
+ continue;
2770
+ }
2771
+ if (!msg || !Array.isArray(msg.content)) {
2772
+ nextMessages[i] = msg;
2773
+ continue;
2774
+ }
2573
2775
  const newContent = msg.content.map((b) => {
2574
2776
  if (b.type !== "tool_result") return b;
2575
2777
  const text = typeof b.content === "string" ? b.content : JSON.stringify(b.content);
@@ -2583,8 +2785,14 @@ Summarize the following message range:`;
2583
2785
  is_error: b.is_error
2584
2786
  };
2585
2787
  });
2586
- messages[i] = { ...msg, content: newContent };
2788
+ if (newContent.every((b, idx) => b === msg.content[idx])) {
2789
+ nextMessages[i] = msg;
2790
+ } else {
2791
+ nextMessages[i] = { ...msg, content: newContent };
2792
+ changed = true;
2793
+ }
2587
2794
  }
2795
+ if (changed) ctx.state.replaceMessages(nextMessages);
2588
2796
  return saved;
2589
2797
  }
2590
2798
  hasTextContent(m) {
@@ -2620,46 +2828,78 @@ var AutoCompactionMiddleware = class {
2620
2828
  name = "AutoCompaction";
2621
2829
  compactor;
2622
2830
  warnThreshold;
2623
- // fraction of maxContext (0-1)
2624
2831
  softThreshold;
2625
2832
  hardThreshold;
2626
2833
  maxContext;
2627
2834
  estimator;
2628
2835
  aggressiveOn;
2836
+ events;
2837
+ failureMode;
2629
2838
  /**
2630
- * @param compactor Compactor to use for compaction
2631
- * @param maxContext Provider's max context window in tokens
2632
- * @param estimator Token estimation function (ctx → token count)
2633
- * @param thresholds Threshold fractions (0-1) of maxContext
2634
- * @param aggressiveOn Which threshold triggers aggressive (full LLM summarization)
2839
+ * @param compactor Compactor to use for compaction.
2840
+ * @param maxContext Provider's max context window in tokens.
2841
+ * @param estimator Token estimation function.
2842
+ * @param thresholds Threshold fractions (0-1) of maxContext.
2843
+ * @param opts Optional behavior. By default, failures at the
2844
+ * hard threshold throw AGENT_CONTEXT_OVERFLOW so
2845
+ * the agent does not continue into a likely
2846
+ * provider context overflow. Warn/soft failures
2847
+ * still emit compaction.failed and continue.
2635
2848
  */
2636
- constructor(compactor, maxContext, estimator, thresholds, aggressiveOn = "soft") {
2849
+ constructor(compactor, maxContext, estimator, thresholds, optsOrAggressiveOn = {}, events) {
2850
+ const opts = typeof optsOrAggressiveOn === "string" ? { aggressiveOn: optsOrAggressiveOn, events } : optsOrAggressiveOn;
2637
2851
  this.compactor = compactor;
2638
2852
  this.maxContext = maxContext;
2639
2853
  this.estimator = estimator;
2640
2854
  this.warnThreshold = thresholds.warn;
2641
2855
  this.softThreshold = thresholds.soft;
2642
2856
  this.hardThreshold = thresholds.hard;
2643
- this.aggressiveOn = aggressiveOn;
2857
+ this.aggressiveOn = opts.aggressiveOn ?? "soft";
2858
+ this.events = opts.events;
2859
+ this.failureMode = opts.failureMode ?? "throw_on_hard";
2644
2860
  }
2645
2861
  handler() {
2646
2862
  return async (ctx, next) => {
2647
2863
  const tokens = this.estimator(ctx);
2648
2864
  const load = tokens / this.maxContext;
2649
2865
  if (load >= this.hardThreshold) {
2650
- await this.compact(ctx, true);
2866
+ await this.compact(ctx, true, { level: "hard", tokens, load });
2651
2867
  } else if (load >= this.softThreshold) {
2652
- await this.compact(ctx, this.aggressiveOn !== "hard");
2868
+ await this.compact(ctx, this.aggressiveOn !== "hard", { level: "soft", tokens, load });
2653
2869
  } else if (load >= this.warnThreshold) {
2654
- await this.compact(ctx, false);
2870
+ await this.compact(ctx, false, { level: "warn", tokens, load });
2655
2871
  }
2656
2872
  return next(ctx);
2657
2873
  };
2658
2874
  }
2659
- async compact(ctx, aggressive) {
2875
+ async compact(ctx, aggressive, pressure) {
2660
2876
  try {
2661
2877
  await this.compactor.compact(ctx, { aggressive });
2662
- } catch {
2878
+ } catch (err) {
2879
+ const error = err instanceof Error ? err : new Error(String(err));
2880
+ const fatal = this.failureMode === "throw" || this.failureMode === "throw_on_hard" && pressure.level === "hard";
2881
+ this.events?.emit("compaction.failed", {
2882
+ err: error,
2883
+ aggressive,
2884
+ level: pressure.level,
2885
+ tokens: pressure.tokens,
2886
+ maxContext: this.maxContext,
2887
+ load: pressure.load,
2888
+ fatal
2889
+ });
2890
+ if (fatal) {
2891
+ throw new AgentError({
2892
+ message: `Auto-compaction failed at ${pressure.level} threshold`,
2893
+ code: "AGENT_CONTEXT_OVERFLOW",
2894
+ recoverable: true,
2895
+ context: {
2896
+ level: pressure.level,
2897
+ tokens: pressure.tokens,
2898
+ maxContext: this.maxContext
2899
+ },
2900
+ cause: err
2901
+ });
2902
+ }
2663
2903
  }
2664
2904
  }
2665
2905
  };
@@ -3173,8 +3413,9 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3173
3413
  const context = {
3174
3414
  subagentId: id,
3175
3415
  tasks: [],
3176
- // parentBridge: wired by the caller via setSubagentBridge() once the
3177
- // bidirectional bridge is created. Reads gated by hasParentBridge().
3416
+ // Wired later by the caller via setSubagentBridge() once the
3417
+ // bidirectional bridge is created. Readers must null-check / use
3418
+ // hasParentBridge() — the type now reflects this.
3178
3419
  parentBridge: null,
3179
3420
  doneCondition: this.config.doneCondition,
3180
3421
  maxConcurrent: this.config.maxConcurrent ?? 4
@@ -3219,9 +3460,7 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3219
3460
  this.emit("subagent.stopped", { subagentId, reason: "stopped by coordinator" });
3220
3461
  }
3221
3462
  async stopAll() {
3222
- for (const id of this.subagents.keys()) {
3223
- await this.stop(id);
3224
- }
3463
+ await Promise.allSettled([...this.subagents.keys()].map((id) => this.stop(id)));
3225
3464
  }
3226
3465
  getStatus() {
3227
3466
  return {
@@ -3257,7 +3496,17 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3257
3496
  if (!subagentId) return;
3258
3497
  const task = this.pendingTasks.shift();
3259
3498
  if (!task) return;
3260
- void this.runDispatched(subagentId, task);
3499
+ this.runDispatched(subagentId, task).catch((err) => {
3500
+ this.recordCompletion({
3501
+ subagentId,
3502
+ taskId: task.id,
3503
+ status: "failed",
3504
+ error: err instanceof Error ? err.message : String(err),
3505
+ iterations: 0,
3506
+ toolCalls: 0,
3507
+ durationMs: 0
3508
+ });
3509
+ });
3261
3510
  }
3262
3511
  }
3263
3512
  canDispatch() {
@@ -3277,7 +3526,6 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3277
3526
  subagent.currentTask = task.id;
3278
3527
  task.subagentId = subagentId;
3279
3528
  subagent.context.tasks.push(task);
3280
- this.inFlight++;
3281
3529
  this.emit("task.assigned", { task, subagentId });
3282
3530
  const budget = new SubagentBudget({
3283
3531
  maxIterations: subagent.config.maxIterations ?? this.config.defaultBudget?.maxIterations,
@@ -3287,6 +3535,10 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3287
3535
  timeoutMs: task.timeoutMs ?? subagent.config.timeoutMs ?? this.config.defaultBudget?.timeoutMs
3288
3536
  });
3289
3537
  subagent.activeBudget = budget;
3538
+ if (!this.runner) {
3539
+ return;
3540
+ }
3541
+ this.inFlight++;
3290
3542
  const startTime = Date.now();
3291
3543
  const runCtx = {
3292
3544
  subagentId,
@@ -3296,9 +3548,6 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3296
3548
  bridge: subagent.context.parentBridge || null
3297
3549
  };
3298
3550
  let result;
3299
- if (!this.runner) {
3300
- return;
3301
- }
3302
3551
  budget.start();
3303
3552
  try {
3304
3553
  const outcome = await this.executeWithTimeout(this.runner, task, runCtx, budget);
@@ -3313,13 +3562,14 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3313
3562
  };
3314
3563
  } catch (err) {
3315
3564
  const status = err instanceof BudgetExceededError && err.kind === "timeout" ? "timeout" : subagent.abortController.signal.aborted ? "stopped" : "failed";
3565
+ const usage = budget.usage();
3316
3566
  result = {
3317
3567
  subagentId,
3318
3568
  taskId: task.id,
3319
3569
  status,
3320
3570
  error: err instanceof Error ? err.message : String(err),
3321
- iterations: budget.usage().iterations,
3322
- toolCalls: budget.usage().toolCalls,
3571
+ iterations: usage.iterations,
3572
+ toolCalls: usage.toolCalls,
3323
3573
  durationMs: Date.now() - startTime
3324
3574
  };
3325
3575
  }
@@ -3344,11 +3594,24 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3344
3594
  recordCompletion(result) {
3345
3595
  this.completedResults.push(result);
3346
3596
  this.totalIterations += result.iterations;
3347
- this.inFlight = Math.max(0, this.inFlight - 1);
3597
+ if (this.inFlight > 0) {
3598
+ this.inFlight--;
3599
+ } else if (this.runner) {
3600
+ this.emit("warning", {
3601
+ type: "inFlight_underflow",
3602
+ taskId: result.taskId,
3603
+ subagentId: result.subagentId
3604
+ });
3605
+ return;
3606
+ }
3348
3607
  const subagent = this.subagents.get(result.subagentId);
3349
3608
  if (subagent && subagent.status !== "stopped") {
3350
- subagent.status = result.status === "failed" || result.status === "timeout" ? "error" : "idle";
3609
+ const failed = result.status === "failed" || result.status === "timeout";
3610
+ subagent.status = failed ? "error" : "idle";
3351
3611
  subagent.currentTask = void 0;
3612
+ if (subagent.abortController.signal.aborted) {
3613
+ subagent.abortController = new AbortController();
3614
+ }
3352
3615
  if (subagent.status === "error") {
3353
3616
  queueMicrotask(() => {
3354
3617
  if (subagent.status === "error") subagent.status = "idle";
@@ -3450,6 +3713,182 @@ function defaultFormatTaskInput(task) {
3450
3713
  return task.description ?? "";
3451
3714
  }
3452
3715
 
3716
+ // src/defaults/fleet-bus.ts
3717
+ var FleetBus = class {
3718
+ byId = /* @__PURE__ */ new Map();
3719
+ byType = /* @__PURE__ */ new Map();
3720
+ any = /* @__PURE__ */ new Set();
3721
+ /**
3722
+ * Hook a subagent's EventBus into the fleet. EventBus is strongly
3723
+ * typed and doesn't expose an `onAny` hook, so we subscribe to the
3724
+ * canonical set of event types a subagent emits during a run. New
3725
+ * event types added to the kernel must be added here too — but the
3726
+ * cost is a tiny single line per type, and the explicit list keeps
3727
+ * the wire format clear.
3728
+ *
3729
+ * Returns a disposer that detaches every subscription; call on
3730
+ * subagent teardown so the listeners don't outlive the run.
3731
+ */
3732
+ attach(subagentId, bus, taskId) {
3733
+ const FORWARDED_TYPES = [
3734
+ "tool.started",
3735
+ "tool.executed",
3736
+ "tool.progress",
3737
+ "tool.confirm_needed",
3738
+ "iteration.started",
3739
+ "iteration.completed",
3740
+ "provider.text_delta",
3741
+ "provider.response",
3742
+ "provider.retry",
3743
+ "provider.error",
3744
+ "session.started",
3745
+ "session.ended",
3746
+ "token.threshold"
3747
+ ];
3748
+ const offs = [];
3749
+ for (const t of FORWARDED_TYPES) {
3750
+ offs.push(
3751
+ bus.on(t, (payload) => {
3752
+ this.emit({ subagentId, taskId, ts: Date.now(), type: t, payload });
3753
+ })
3754
+ );
3755
+ }
3756
+ return () => {
3757
+ for (const off of offs) off();
3758
+ };
3759
+ }
3760
+ /** Subscribe to every event from one subagent. */
3761
+ subscribe(subagentId, handler) {
3762
+ let set = this.byId.get(subagentId);
3763
+ if (!set) {
3764
+ set = /* @__PURE__ */ new Set();
3765
+ this.byId.set(subagentId, set);
3766
+ }
3767
+ set.add(handler);
3768
+ return () => {
3769
+ set.delete(handler);
3770
+ };
3771
+ }
3772
+ /** Subscribe to one event type across all subagents. */
3773
+ filter(type, handler) {
3774
+ let set = this.byType.get(type);
3775
+ if (!set) {
3776
+ set = /* @__PURE__ */ new Set();
3777
+ this.byType.set(type, set);
3778
+ }
3779
+ set.add(handler);
3780
+ return () => {
3781
+ set.delete(handler);
3782
+ };
3783
+ }
3784
+ /** Subscribe to literally everything. The fleet roll-up uses this. */
3785
+ onAny(handler) {
3786
+ this.any.add(handler);
3787
+ return () => {
3788
+ this.any.delete(handler);
3789
+ };
3790
+ }
3791
+ emit(event) {
3792
+ const byId = this.byId.get(event.subagentId);
3793
+ if (byId) for (const h of byId) {
3794
+ try {
3795
+ h(event);
3796
+ } catch {
3797
+ }
3798
+ }
3799
+ const byType = this.byType.get(event.type);
3800
+ if (byType) for (const h of byType) {
3801
+ try {
3802
+ h(event);
3803
+ } catch {
3804
+ }
3805
+ }
3806
+ for (const h of this.any) {
3807
+ try {
3808
+ h(event);
3809
+ } catch {
3810
+ }
3811
+ }
3812
+ }
3813
+ };
3814
+ var FleetUsageAggregator = class {
3815
+ constructor(bus, priceLookup, metaLookup) {
3816
+ this.bus = bus;
3817
+ this.priceLookup = priceLookup;
3818
+ this.metaLookup = metaLookup;
3819
+ bus.filter("provider.response", (e) => this.onProviderResponse(e));
3820
+ bus.filter("tool.executed", (e) => this.onToolExecuted(e));
3821
+ bus.filter("iteration.started", (e) => this.onIterationStarted(e));
3822
+ }
3823
+ bus;
3824
+ priceLookup;
3825
+ metaLookup;
3826
+ perSubagent = /* @__PURE__ */ new Map();
3827
+ total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
3828
+ /** Live snapshot — safe to call from a tool's execute() body. */
3829
+ snapshot() {
3830
+ return {
3831
+ total: { ...this.total },
3832
+ perSubagent: Object.fromEntries(
3833
+ Array.from(this.perSubagent.entries()).map(([k, v]) => [k, { ...v }])
3834
+ )
3835
+ };
3836
+ }
3837
+ ensure(subagentId) {
3838
+ let snap = this.perSubagent.get(subagentId);
3839
+ if (!snap) {
3840
+ const meta = this.metaLookup?.(subagentId);
3841
+ snap = {
3842
+ subagentId,
3843
+ provider: meta?.provider,
3844
+ model: meta?.model,
3845
+ input: 0,
3846
+ output: 0,
3847
+ cacheRead: 0,
3848
+ cacheWrite: 0,
3849
+ cost: 0,
3850
+ toolCalls: 0,
3851
+ iterations: 0,
3852
+ startedAt: Date.now(),
3853
+ lastEventAt: Date.now()
3854
+ };
3855
+ this.perSubagent.set(subagentId, snap);
3856
+ }
3857
+ return snap;
3858
+ }
3859
+ onProviderResponse(e) {
3860
+ const snap = this.ensure(e.subagentId);
3861
+ const p = e.payload;
3862
+ const usage = p?.usage;
3863
+ if (!usage) return;
3864
+ snap.input += usage.input ?? 0;
3865
+ snap.output += usage.output ?? 0;
3866
+ snap.cacheRead += usage.cacheRead ?? 0;
3867
+ snap.cacheWrite += usage.cacheWrite ?? 0;
3868
+ this.total.input += usage.input ?? 0;
3869
+ this.total.output += usage.output ?? 0;
3870
+ this.total.cacheRead += usage.cacheRead ?? 0;
3871
+ this.total.cacheWrite += usage.cacheWrite ?? 0;
3872
+ const price = this.priceLookup?.(e.subagentId);
3873
+ if (price) {
3874
+ const delta = (usage.input ?? 0) / 1e6 * (price.input ?? 0) + (usage.output ?? 0) / 1e6 * (price.output ?? 0) + (usage.cacheRead ?? 0) / 1e6 * (price.cacheRead ?? 0) + (usage.cacheWrite ?? 0) / 1e6 * (price.cacheWrite ?? 0);
3875
+ snap.cost += delta;
3876
+ this.total.cost += delta;
3877
+ }
3878
+ snap.lastEventAt = e.ts;
3879
+ }
3880
+ onToolExecuted(e) {
3881
+ const snap = this.ensure(e.subagentId);
3882
+ snap.toolCalls += 1;
3883
+ snap.lastEventAt = e.ts;
3884
+ }
3885
+ onIterationStarted(e) {
3886
+ const snap = this.ensure(e.subagentId);
3887
+ snap.iterations += 1;
3888
+ snap.lastEventAt = e.ts;
3889
+ }
3890
+ };
3891
+
3453
3892
  // src/defaults/transport/in-memory-transport.ts
3454
3893
  var InMemoryBridgeTransport = class {
3455
3894
  subs = /* @__PURE__ */ new Map();
@@ -3536,6 +3975,11 @@ var InMemoryAgentBridge = class {
3536
3975
  if (this.stopped) throw new Error("Bridge is stopped");
3537
3976
  const timeout = timeoutMs ?? this.timeoutMs;
3538
3977
  const correlationId = msg.id;
3978
+ if (this.pendingRequests.has(correlationId)) {
3979
+ throw new Error(
3980
+ `Bridge request id "${correlationId}" collides with an in-flight request \u2014 caller is reusing message ids`
3981
+ );
3982
+ }
3539
3983
  return new Promise((resolve3, reject) => {
3540
3984
  const timer = setTimeout(() => {
3541
3985
  this.pendingRequests.delete(correlationId);
@@ -3572,12 +4016,686 @@ function createMessage(type, from, payload, to) {
3572
4016
  };
3573
4017
  }
3574
4018
 
4019
+ // src/defaults/director-prompts.ts
4020
+ var DEFAULT_DIRECTOR_PREAMBLE = `You are the Director of a multi-agent fleet. You orchestrate worker
4021
+ subagents by spawning them, assigning tasks, awaiting completions, and
4022
+ rolling up their outputs into your next decision.
4023
+
4024
+ Core fleet tools available to you:
4025
+ - spawn_subagent \u2014 create a worker with a chosen provider / model / role
4026
+ - assign_task \u2014 hand a piece of work to a specific subagent
4027
+ - await_tasks \u2014 block until named task ids complete (parallel-safe)
4028
+ - ask_subagent \u2014 synchronously query a running subagent via the bridge
4029
+ - roll_up \u2014 aggregate finished tasks into a markdown/json summary
4030
+ - terminate_subagent \u2014 abort a stuck worker (use sparingly)
4031
+ - fleet_status \u2014 snapshot of all subagents and pending tasks
4032
+ - fleet_usage \u2014 token + cost breakdown per subagent and total
4033
+
4034
+ Working rules:
4035
+ 1. Decompose first. Before spawning, decide which sub-tasks are
4036
+ independent and can run in parallel. Sequential work doesn't need a
4037
+ subagent \u2014 do it yourself.
4038
+ 2. Match worker to job. Cheap/fast model for triage, capable model for
4039
+ synthesis. Different providers per sibling is allowed and encouraged.
4040
+ 3. Always pair an assign with an await. Don't fire-and-forget; you owe
4041
+ the user a single coherent answer at the end.
4042
+ 4. Roll up before deciding. After await_tasks resolves, call roll_up so
4043
+ the results are folded back into your context in a compact form.
4044
+ 5. Budget is real. Check fleet_usage periodically. If a subagent is
4045
+ thrashing, terminate it rather than letting cost climb silently.
4046
+ 6. Never claim a subagent's work as your own without verifying it. If a
4047
+ result looks wrong, ask_subagent for clarification before passing it
4048
+ to the user.`;
4049
+ var DEFAULT_SUBAGENT_BASELINE = `You are a subagent operating under a Director. You were spawned to handle
4050
+ a specific slice of a larger plan \u2014 do that slice well and report back.
4051
+
4052
+ Bridge contract:
4053
+ - You have a parent (the Director). You may call \`request\` on the
4054
+ parent bridge to ask a clarifying question. Use this sparingly; the
4055
+ parent is also working.
4056
+ - You MAY NOT request the parent's system prompt, tool list, or other
4057
+ subagents' context. Those are not yours to read.
4058
+ - Your final task output is what the Director sees. Be concise,
4059
+ structured, and self-contained \u2014 assume the Director will paste your
4060
+ output into its own context.`;
4061
+ function composeDirectorPrompt(parts = {}) {
4062
+ const sections = [];
4063
+ const preamble = parts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
4064
+ if (preamble && preamble.trim().length > 0) sections.push(preamble.trim());
4065
+ if (parts.rosterSummary && parts.rosterSummary.trim().length > 0) {
4066
+ sections.push(`Available roles you can spawn:
4067
+ ${parts.rosterSummary.trim()}`);
4068
+ }
4069
+ if (parts.basePrompt && parts.basePrompt.trim().length > 0) {
4070
+ sections.push(parts.basePrompt.trim());
4071
+ }
4072
+ return sections.join("\n\n");
4073
+ }
4074
+ function composeSubagentPrompt(parts = {}) {
4075
+ const sections = [];
4076
+ const baseline = parts.baseline ?? DEFAULT_SUBAGENT_BASELINE;
4077
+ if (baseline && baseline.trim().length > 0) sections.push(baseline.trim());
4078
+ if (parts.role && parts.role.trim().length > 0) {
4079
+ sections.push(`Role:
4080
+ ${parts.role.trim()}`);
4081
+ }
4082
+ if (parts.task && parts.task.trim().length > 0) {
4083
+ sections.push(`Task:
4084
+ ${parts.task.trim()}`);
4085
+ }
4086
+ if (parts.override && parts.override.trim().length > 0) {
4087
+ sections.push(parts.override.trim());
4088
+ }
4089
+ return sections.join("\n\n");
4090
+ }
4091
+ function rosterSummaryFromConfigs(roster) {
4092
+ const lines = [];
4093
+ for (const [roleId, cfg] of Object.entries(roster)) {
4094
+ const tag = cfg.provider && cfg.model ? ` (${cfg.provider}/${cfg.model})` : "";
4095
+ const headline = cfg.prompt ? (cfg.prompt.split("\n").find((l) => l.trim().length > 0) ?? "").trim().slice(0, 80) : "";
4096
+ const tail = headline ? ` \u2014 ${headline}` : "";
4097
+ lines.push(`- ${roleId}: ${cfg.name}${tag}${tail}`);
4098
+ }
4099
+ return lines.join("\n");
4100
+ }
4101
+
4102
+ // src/defaults/director.ts
4103
+ var Director = class {
4104
+ id;
4105
+ fleet;
4106
+ usage;
4107
+ /**
4108
+ * Director-side bridge endpoint. Subagents are wired to the same
4109
+ * in-memory transport so the director can `ask()` them synchronously
4110
+ * and they can `send()` progress back. Exposed so external code (e.g.
4111
+ * the TUI) can subscribe to inbound messages.
4112
+ */
4113
+ bridge;
4114
+ transport;
4115
+ coordinator;
4116
+ /** Resolves with the matching `TaskResult` the first time the
4117
+ * coordinator emits `task.completed` for a given task id. Each entry
4118
+ * is created lazily on first poll/await and cleared once consumed. */
4119
+ taskWaiters = /* @__PURE__ */ new Map();
4120
+ /** Cache of completed results in case the consumer asks AFTER the
4121
+ * coordinator already fired the event — `awaitTasks(['t-1'])` after
4122
+ * t-1 finished should resolve immediately, not hang. */
4123
+ completed = /* @__PURE__ */ new Map();
4124
+ /** Per-subagent provider/model metadata, captured at spawn time so the
4125
+ * FleetUsageAggregator's metaLookup can surface readable rows. */
4126
+ subagentMeta = /* @__PURE__ */ new Map();
4127
+ priceLookups = /* @__PURE__ */ new Map();
4128
+ /** Bridge endpoints we created per subagent (so we can `stop()` them
4129
+ * on shutdown and free transport subscriptions). */
4130
+ subagentBridges = /* @__PURE__ */ new Map();
4131
+ /** Tracks per-spawn config + assigned task ids for manifest writing. */
4132
+ manifestEntries = /* @__PURE__ */ new Map();
4133
+ manifestPath;
4134
+ roster;
4135
+ directorPreamble;
4136
+ subagentBaseline;
4137
+ constructor(opts) {
4138
+ this.id = opts.config.coordinatorId || randomUUID();
4139
+ this.manifestPath = opts.manifestPath;
4140
+ this.roster = opts.roster;
4141
+ this.directorPreamble = opts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
4142
+ this.subagentBaseline = opts.subagentBaseline ?? DEFAULT_SUBAGENT_BASELINE;
4143
+ this.transport = new InMemoryBridgeTransport();
4144
+ this.bridge = new InMemoryAgentBridge(
4145
+ { agentId: this.id, coordinatorId: this.id },
4146
+ this.transport
4147
+ );
4148
+ this.fleet = new FleetBus();
4149
+ this.usage = new FleetUsageAggregator(
4150
+ this.fleet,
4151
+ (id) => this.priceLookups.get(id),
4152
+ (id) => this.subagentMeta.get(id)
4153
+ );
4154
+ this.coordinator = new DefaultMultiAgentCoordinator(
4155
+ { ...opts.config, coordinatorId: this.id },
4156
+ { runner: opts.runner }
4157
+ );
4158
+ this.coordinator.on("task.completed", (payload) => {
4159
+ const r = payload.result;
4160
+ this.completed.set(r.taskId, r);
4161
+ const waiter = this.taskWaiters.get(r.taskId);
4162
+ if (waiter) {
4163
+ waiter.resolve(r);
4164
+ this.taskWaiters.delete(r.taskId);
4165
+ }
4166
+ });
4167
+ }
4168
+ /**
4169
+ * Spawn a subagent. Identical to the coordinator's `spawn()` but
4170
+ * captures provider/model metadata for the usage aggregator and
4171
+ * lets the FleetBus attach to the runner's EventBus when the task
4172
+ * actually runs (see `attachSubagentBus`).
4173
+ *
4174
+ * Caller-supplied `priceLookup` is optional but recommended — without
4175
+ * it the `cost` column in `usage.snapshot()` stays at 0.
4176
+ */
4177
+ async spawn(config, priceLookup) {
4178
+ const result = await this.coordinator.spawn(config);
4179
+ this.subagentMeta.set(result.subagentId, {
4180
+ provider: config.provider,
4181
+ model: config.model
4182
+ });
4183
+ if (priceLookup) this.priceLookups.set(result.subagentId, priceLookup);
4184
+ const subagentBridge = new InMemoryAgentBridge(
4185
+ { agentId: result.subagentId, coordinatorId: this.id },
4186
+ this.transport
4187
+ );
4188
+ this.coordinator.setSubagentBridge(result.subagentId, subagentBridge);
4189
+ this.subagentBridges.set(result.subagentId, subagentBridge);
4190
+ this.manifestEntries.set(result.subagentId, {
4191
+ subagentId: result.subagentId,
4192
+ name: config.name,
4193
+ role: config.role,
4194
+ provider: config.provider,
4195
+ model: config.model,
4196
+ taskIds: []
4197
+ });
4198
+ return result.subagentId;
4199
+ }
4200
+ /**
4201
+ * Synchronously ask a subagent something via the bridge. Sends a
4202
+ * `task` message addressed to the subagent and awaits a matching
4203
+ * reply (matched by message id). Subagent runners that handle these
4204
+ * requests subscribe to `ctx.bridge` and reply with a message whose
4205
+ * `id` equals the incoming request's id (see `InMemoryAgentBridge`'s
4206
+ * `request<T>` implementation).
4207
+ *
4208
+ * Returns the response payload directly (the bridge wrapper is
4209
+ * unwrapped for ergonomics). Times out after `timeoutMs` (default
4210
+ * matches the bridge's own default of 30s) — surface those rejections
4211
+ * to the caller as actionable errors instead of letting tools hang.
4212
+ */
4213
+ async ask(subagentId, payload, timeoutMs) {
4214
+ if (!this.subagentBridges.has(subagentId)) {
4215
+ throw new Error(
4216
+ `ask: unknown subagent "${subagentId}" (spawn() it first; current fleet: ${Array.from(this.subagentBridges.keys()).join(", ") || "(empty)"})`
4217
+ );
4218
+ }
4219
+ const msg = {
4220
+ id: randomUUID(),
4221
+ type: "task",
4222
+ from: this.id,
4223
+ to: subagentId,
4224
+ payload,
4225
+ timestamp: Date.now(),
4226
+ priority: "normal"
4227
+ };
4228
+ const reply = await this.bridge.request(msg, timeoutMs);
4229
+ return reply.payload;
4230
+ }
4231
+ /**
4232
+ * Read completed task results and format them as a structured text
4233
+ * block the director's LLM can paste into its own context. The
4234
+ * Director keeps every completed `TaskResult` in `completed` so this
4235
+ * is a pure read — no bridge round-trip, cheap to call.
4236
+ *
4237
+ * The returned string is intentionally markdown-flavored: headers per
4238
+ * subagent, a one-line meta row (iter / tools / ms), and the task's
4239
+ * result text. Pass `style: 'json'` for a programmatic shape instead
4240
+ * (useful when the director model is doing structured-output work).
4241
+ */
4242
+ rollUp(taskIds, style = "markdown") {
4243
+ const rows = taskIds.map((id) => this.completed.get(id)).filter(
4244
+ (r) => !!r
4245
+ );
4246
+ if (style === "json") {
4247
+ return JSON.stringify(
4248
+ rows.map((r) => ({
4249
+ taskId: r.taskId,
4250
+ subagentId: r.subagentId,
4251
+ status: r.status,
4252
+ iterations: r.iterations,
4253
+ toolCalls: r.toolCalls,
4254
+ durationMs: r.durationMs,
4255
+ result: r.result,
4256
+ error: r.error
4257
+ })),
4258
+ null,
4259
+ 2
4260
+ );
4261
+ }
4262
+ if (rows.length === 0) {
4263
+ return "_No completed tasks for the requested ids \u2014 try waiting first._";
4264
+ }
4265
+ const lines = [];
4266
+ for (const r of rows) {
4267
+ const meta = this.subagentMeta.get(r.subagentId);
4268
+ const tag = meta?.provider && meta?.model ? ` \xB7 ${meta.provider}/${meta.model}` : "";
4269
+ lines.push(`### ${r.subagentId}${tag}`);
4270
+ lines.push(
4271
+ `_${r.status} \u2014 ${r.iterations} iter \xB7 ${r.toolCalls} tools \xB7 ${r.durationMs}ms_`
4272
+ );
4273
+ lines.push("");
4274
+ if (r.error) lines.push(`**Error:** ${r.error}`);
4275
+ else if (typeof r.result === "string") lines.push(r.result);
4276
+ else if (r.result !== void 0) lines.push("```json\n" + JSON.stringify(r.result, null, 2) + "\n```");
4277
+ else lines.push("_(no output)_");
4278
+ lines.push("");
4279
+ }
4280
+ return lines.join("\n").trimEnd();
4281
+ }
4282
+ /**
4283
+ * Write the fleet manifest to `manifestPath`. Returns the path written
4284
+ * or null when no path was configured. Captures every spawn + its
4285
+ * assigned tasks — paired with per-subagent JSONLs, this is enough to
4286
+ * replay an entire director run.
4287
+ */
4288
+ async writeManifest() {
4289
+ if (!this.manifestPath) return null;
4290
+ const manifest = {
4291
+ directorRunId: this.id,
4292
+ writtenAt: (/* @__PURE__ */ new Date()).toISOString(),
4293
+ children: Array.from(this.manifestEntries.values()).map((e) => ({
4294
+ ...e,
4295
+ // Surface final status from `completed` when available — manifest
4296
+ // becomes much more useful for replay when it carries the
4297
+ // success/failure state.
4298
+ results: e.taskIds.map((tid) => {
4299
+ const r = this.completed.get(tid);
4300
+ return r ? {
4301
+ taskId: tid,
4302
+ status: r.status,
4303
+ iterations: r.iterations,
4304
+ toolCalls: r.toolCalls,
4305
+ durationMs: r.durationMs
4306
+ } : { taskId: tid, status: "pending" };
4307
+ })
4308
+ })),
4309
+ usage: this.usage.snapshot()
4310
+ };
4311
+ await fsp.mkdir(path2.dirname(this.manifestPath), { recursive: true });
4312
+ await fsp.writeFile(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
4313
+ return this.manifestPath;
4314
+ }
4315
+ /**
4316
+ * Tear down the director: stop every subagent, close every bridge
4317
+ * endpoint, and (when configured) write the final manifest. Idempotent
4318
+ * — calling shutdown twice is a no-op on the second invocation.
4319
+ */
4320
+ async shutdown() {
4321
+ await this.coordinator.stopAll();
4322
+ for (const b of this.subagentBridges.values()) {
4323
+ await b.stop().catch(() => void 0);
4324
+ }
4325
+ this.subagentBridges.clear();
4326
+ await this.bridge.stop().catch(() => void 0);
4327
+ if (this.manifestPath) await this.writeManifest().catch(() => void 0);
4328
+ }
4329
+ /**
4330
+ * Hand a task to the coordinator. Returns the assigned task id so
4331
+ * callers can wait on it via `awaitTasks([id])`. The coordinator's
4332
+ * concurrency limit applies — the task may queue before running.
4333
+ */
4334
+ async assign(task) {
4335
+ const taskWithId = task.id ? task : { ...task, id: randomUUID() };
4336
+ if (task.subagentId) {
4337
+ const entry = this.manifestEntries.get(task.subagentId);
4338
+ if (entry) entry.taskIds.push(taskWithId.id);
4339
+ }
4340
+ await this.coordinator.assign(taskWithId);
4341
+ return taskWithId.id;
4342
+ }
4343
+ /**
4344
+ * Block until every task id resolves. Returns results in the same
4345
+ * order as the input. If any task hasn't completed by the time this
4346
+ * is called, the promise hangs until it does — pair with a timeout
4347
+ * at the caller if that's a concern. Resolves immediately for ids
4348
+ * whose results were already cached.
4349
+ */
4350
+ awaitTasks(taskIds) {
4351
+ return Promise.all(taskIds.map((id) => {
4352
+ const cached = this.completed.get(id);
4353
+ if (cached) return cached;
4354
+ const existing = this.taskWaiters.get(id);
4355
+ if (existing) return existing.promise;
4356
+ let resolve3;
4357
+ const promise = new Promise((res) => {
4358
+ resolve3 = res;
4359
+ });
4360
+ this.taskWaiters.set(id, { promise, resolve: resolve3 });
4361
+ return promise;
4362
+ }));
4363
+ }
4364
+ async terminate(subagentId) {
4365
+ await this.coordinator.stop(subagentId);
4366
+ }
4367
+ async terminateAll() {
4368
+ await this.coordinator.stopAll();
4369
+ }
4370
+ status() {
4371
+ return this.coordinator.getStatus();
4372
+ }
4373
+ /**
4374
+ * Subscribe to coordinator events. Currently only `task.completed` is
4375
+ * exposed (the others are internal lifecycle). Returns an unsubscribe
4376
+ * function. External callers (e.g. the CLI's `MultiAgentHost`) use this
4377
+ * to drive their own pending/results tracking without poking the
4378
+ * coordinator directly.
4379
+ */
4380
+ on(event, handler) {
4381
+ this.coordinator.on(event, handler);
4382
+ return () => {
4383
+ this.coordinator.off(event, handler);
4384
+ };
4385
+ }
4386
+ /**
4387
+ * Snapshot of every task that has resolved (success, failed, timeout,
4388
+ * stopped) since the director started. Returned in completion order
4389
+ * via the internal map's iteration order. Used by `/fleet status` to
4390
+ * paint the completed table without reaching into private state.
4391
+ */
4392
+ completedResults() {
4393
+ return Array.from(this.completed.values());
4394
+ }
4395
+ snapshot() {
4396
+ return this.usage.snapshot();
4397
+ }
4398
+ /**
4399
+ * Compose the leader/director-agent system prompt: fleet preamble +
4400
+ * (optional) roster summary + user base prompt. Pass the result to your
4401
+ * leader Agent's `ctx.systemPrompt` when constructing it.
4402
+ *
4403
+ * `basePrompt` defaults to `config.leaderSystemPrompt` so callers can
4404
+ * use the no-arg form when the multi-agent config already carries it.
4405
+ */
4406
+ leaderSystemPrompt(basePrompt) {
4407
+ return composeDirectorPrompt({
4408
+ basePrompt: basePrompt ?? this.coordinator.config.leaderSystemPrompt,
4409
+ directorPreamble: this.directorPreamble,
4410
+ rosterSummary: this.roster ? rosterSummaryFromConfigs(this.roster) : void 0
4411
+ });
4412
+ }
4413
+ /**
4414
+ * Compose a subagent's system prompt for a given `SubagentConfig`:
4415
+ * baseline + role + task + per-spawn override. Returned by value — does
4416
+ * not mutate the config. Factories (the user-supplied `AgentFactory`)
4417
+ * should call this when building each subagent's Agent so the bridge
4418
+ * contract, role context, and override are all surfaced.
4419
+ *
4420
+ * When `taskBrief` is omitted the Task section is dropped. Pass the
4421
+ * actual task description here to reinforce it in the system prompt
4422
+ * (the runner already passes it as user input — duplicating in the
4423
+ * system prompt is optional but improves anchoring on small models).
4424
+ */
4425
+ subagentSystemPrompt(config, taskBrief) {
4426
+ return composeSubagentPrompt({
4427
+ baseline: this.subagentBaseline,
4428
+ role: config.prompt,
4429
+ task: taskBrief,
4430
+ override: config.systemPromptOverride
4431
+ });
4432
+ }
4433
+ /**
4434
+ * Build the tool set the LLM-driven director uses to orchestrate.
4435
+ * Returns an array of `Tool` definitions; register these on the
4436
+ * director's `Agent` to expose `spawn_subagent`, `assign_task`, etc.
4437
+ * Each tool's `execute()` delegates straight to the matching method
4438
+ * above.
4439
+ *
4440
+ * Tools all carry `permission: 'auto'` — the *user* has already
4441
+ * approved running the director when they kicked off the run, so
4442
+ * gating individual orchestration calls behind a confirm prompt
4443
+ * would just be noise. The actual subagent tools they spawn are
4444
+ * still permission-checked normally.
4445
+ */
4446
+ tools(roster) {
4447
+ const t = [
4448
+ makeSpawnTool(this, roster),
4449
+ makeAssignTool(this),
4450
+ makeAwaitTasksTool(this),
4451
+ makeAskTool(this),
4452
+ makeRollUpTool(this),
4453
+ makeTerminateTool(this),
4454
+ makeFleetStatusTool(this),
4455
+ makeFleetUsageTool(this)
4456
+ ];
4457
+ return t;
4458
+ }
4459
+ };
4460
+ function makeSpawnTool(director, roster) {
4461
+ const inputSchema = {
4462
+ type: "object",
4463
+ properties: {
4464
+ role: { type: "string", description: "Roster role id (preferred). When set, the spawn uses the matching config from the roster and ignores other fields." },
4465
+ name: { type: "string", description: "Display name for the subagent. Required when not using roster." },
4466
+ provider: { type: "string", description: 'Provider id (e.g. "anthropic", "openai"). Defaults to the leader provider when omitted.' },
4467
+ model: { type: "string", description: "Model id within the provider. Defaults to the leader model when omitted." },
4468
+ systemPromptOverride: { type: "string", description: "Extra prompt text appended after the role-base prompt." },
4469
+ maxIterations: { type: "number" },
4470
+ maxToolCalls: { type: "number" },
4471
+ maxCostUsd: { type: "number" }
4472
+ },
4473
+ required: []
4474
+ };
4475
+ return {
4476
+ name: "spawn_subagent",
4477
+ description: "Create a new subagent under this director. Returns the subagent id. Use this when you need a worker with a specific provider, model, or role to handle a piece of the plan.",
4478
+ usageHint: "Either pass `role` (matches the roster) OR pass `name` + optional `provider`/`model`. Returns `{ subagentId }`.",
4479
+ permission: "auto",
4480
+ mutating: false,
4481
+ inputSchema,
4482
+ async execute(input) {
4483
+ const i = input ?? {};
4484
+ const role = typeof i.role === "string" ? i.role : void 0;
4485
+ const base = role && roster ? roster[role] : void 0;
4486
+ if (role && !base) {
4487
+ return { error: `unknown role "${role}". roster has: ${roster ? Object.keys(roster).join(", ") : "(empty)"}` };
4488
+ }
4489
+ const cfg = {
4490
+ ...base ?? { name: i.name ?? "subagent" }
4491
+ };
4492
+ if (typeof i.name === "string") cfg.name = i.name;
4493
+ if (typeof i.provider === "string") cfg.provider = i.provider;
4494
+ if (typeof i.model === "string") cfg.model = i.model;
4495
+ if (typeof i.systemPromptOverride === "string") cfg.systemPromptOverride = i.systemPromptOverride;
4496
+ if (typeof i.maxIterations === "number") cfg.maxIterations = i.maxIterations;
4497
+ if (typeof i.maxToolCalls === "number") cfg.maxToolCalls = i.maxToolCalls;
4498
+ if (typeof i.maxCostUsd === "number") cfg.maxCostUsd = i.maxCostUsd;
4499
+ const subagentId = await director.spawn(cfg);
4500
+ return { subagentId, provider: cfg.provider, model: cfg.model, name: cfg.name };
4501
+ }
4502
+ };
4503
+ }
4504
+ function makeAssignTool(director) {
4505
+ const inputSchema = {
4506
+ type: "object",
4507
+ properties: {
4508
+ subagentId: { type: "string", description: "Target subagent id. Required." },
4509
+ description: { type: "string", description: "The task in natural language \u2014 what you want this subagent to do." },
4510
+ maxToolCalls: { type: "number", description: "Optional per-task tool-call budget override." },
4511
+ timeoutMs: { type: "number", description: "Optional per-task timeout in ms." }
4512
+ },
4513
+ required: ["subagentId", "description"]
4514
+ };
4515
+ return {
4516
+ name: "assign_task",
4517
+ description: "Hand a task to a previously spawned subagent. Returns the task id \u2014 pass it to `await_tasks` to block on completion.",
4518
+ permission: "auto",
4519
+ mutating: false,
4520
+ inputSchema,
4521
+ async execute(input) {
4522
+ const i = input;
4523
+ const task = {
4524
+ id: randomUUID(),
4525
+ description: i.description,
4526
+ subagentId: i.subagentId,
4527
+ maxToolCalls: i.maxToolCalls,
4528
+ timeoutMs: i.timeoutMs
4529
+ };
4530
+ const taskId = await director.assign(task);
4531
+ return { taskId, subagentId: i.subagentId };
4532
+ }
4533
+ };
4534
+ }
4535
+ function makeAwaitTasksTool(director) {
4536
+ const inputSchema = {
4537
+ type: "object",
4538
+ properties: {
4539
+ taskIds: {
4540
+ type: "array",
4541
+ items: { type: "string" },
4542
+ description: "One or more task ids returned by `assign_task`. The call blocks until every id resolves."
4543
+ }
4544
+ },
4545
+ required: ["taskIds"]
4546
+ };
4547
+ return {
4548
+ name: "await_tasks",
4549
+ description: "Block until every named task completes. Returns the array of TaskResult \u2014 use this to gather subagent output before deciding the next step.",
4550
+ permission: "auto",
4551
+ mutating: false,
4552
+ inputSchema,
4553
+ async execute(input) {
4554
+ const i = input;
4555
+ const results = await director.awaitTasks(i.taskIds);
4556
+ return { results };
4557
+ }
4558
+ };
4559
+ }
4560
+ function makeAskTool(director) {
4561
+ const inputSchema = {
4562
+ type: "object",
4563
+ properties: {
4564
+ subagentId: { type: "string", description: "Subagent to ask. Must be a previously spawned id." },
4565
+ question: { type: "string", description: "The question or instruction. Sent as the bridge message payload." },
4566
+ timeoutMs: { type: "number", description: "Optional timeout in ms (default 30s)." }
4567
+ },
4568
+ required: ["subagentId", "question"]
4569
+ };
4570
+ return {
4571
+ name: "ask_subagent",
4572
+ description: "Synchronously ask a subagent a question. Blocks until the subagent replies via the bridge (or the timeout fires). Use this when you need a one-shot answer without spawning a fresh task.",
4573
+ permission: "auto",
4574
+ mutating: false,
4575
+ inputSchema,
4576
+ async execute(input) {
4577
+ const i = input;
4578
+ try {
4579
+ const answer = await director.ask(i.subagentId, { question: i.question }, i.timeoutMs);
4580
+ return { ok: true, answer };
4581
+ } catch (err) {
4582
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
4583
+ }
4584
+ }
4585
+ };
4586
+ }
4587
+ function makeRollUpTool(director) {
4588
+ const inputSchema = {
4589
+ type: "object",
4590
+ properties: {
4591
+ taskIds: {
4592
+ type: "array",
4593
+ items: { type: "string" },
4594
+ description: "Completed task ids to aggregate. Pass the ids returned by previous `assign_task` calls."
4595
+ },
4596
+ style: {
4597
+ type: "string",
4598
+ enum: ["markdown", "json"],
4599
+ description: "Output flavor \u2014 markdown (default) for in-prompt summarization, json for structured downstream processing."
4600
+ }
4601
+ },
4602
+ required: ["taskIds"]
4603
+ };
4604
+ return {
4605
+ name: "roll_up",
4606
+ description: "Aggregate completed task results into a single formatted summary. Use this after `await_tasks` to fold subagent outputs back into the director's context before deciding the next step.",
4607
+ permission: "auto",
4608
+ mutating: false,
4609
+ inputSchema,
4610
+ async execute(input) {
4611
+ const i = input;
4612
+ const summary = director.rollUp(i.taskIds, i.style ?? "markdown");
4613
+ return { summary, count: i.taskIds.length };
4614
+ }
4615
+ };
4616
+ }
4617
+ function makeTerminateTool(director) {
4618
+ const inputSchema = {
4619
+ type: "object",
4620
+ properties: {
4621
+ subagentId: { type: "string", description: "Subagent to abort." }
4622
+ },
4623
+ required: ["subagentId"]
4624
+ };
4625
+ return {
4626
+ name: "terminate_subagent",
4627
+ description: 'Forcibly abort a subagent. Use sparingly \u2014 prefer waiting on the natural budget to expire. The current task (if any) ends with status "stopped".',
4628
+ permission: "auto",
4629
+ mutating: true,
4630
+ inputSchema,
4631
+ async execute(input) {
4632
+ const i = input;
4633
+ await director.terminate(i.subagentId);
4634
+ return { ok: true };
4635
+ }
4636
+ };
4637
+ }
4638
+ function makeFleetStatusTool(director) {
4639
+ return {
4640
+ name: "fleet_status",
4641
+ description: "Snapshot of the fleet \u2014 every subagent's current status, pending vs. completed task counts, and the running total iteration count. Cheap; call freely.",
4642
+ permission: "auto",
4643
+ mutating: false,
4644
+ inputSchema: { type: "object", properties: {}, required: [] },
4645
+ async execute() {
4646
+ return director.status();
4647
+ }
4648
+ };
4649
+ }
4650
+ function makeFleetUsageTool(director) {
4651
+ return {
4652
+ name: "fleet_usage",
4653
+ description: "Token + cost breakdown across the fleet, per-subagent and totals. Use this to reason about which workers to assign costly tasks to or when to wrap up to stay within budget.",
4654
+ permission: "auto",
4655
+ mutating: false,
4656
+ inputSchema: { type: "object", properties: {}, required: [] },
4657
+ async execute() {
4658
+ return director.snapshot();
4659
+ }
4660
+ };
4661
+ }
4662
+ function makeDirectorSessionFactory(opts) {
4663
+ const runId = opts.directorRunId ?? `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-director`;
4664
+ let store;
4665
+ let dir;
4666
+ if (opts.store) {
4667
+ store = opts.store;
4668
+ dir = opts.sessionsRoot ? path2.join(opts.sessionsRoot, runId) : "(caller-managed)";
4669
+ } else if (opts.sessionsRoot) {
4670
+ dir = path2.join(opts.sessionsRoot, runId);
4671
+ store = new DefaultSessionStore({ dir });
4672
+ } else {
4673
+ throw new Error(
4674
+ "makeDirectorSessionFactory requires either `store` or `sessionsRoot`"
4675
+ );
4676
+ }
4677
+ return {
4678
+ dir,
4679
+ directorRunId: runId,
4680
+ async createSubagentSession({ subagentId, provider, model, title }) {
4681
+ return store.create({
4682
+ id: subagentId,
4683
+ title: title ?? subagentId,
4684
+ provider: provider ?? "unknown",
4685
+ model: model ?? "unknown"
4686
+ });
4687
+ }
4688
+ };
4689
+ }
4690
+
3575
4691
  // src/defaults/autonomous-runner.ts
3576
4692
  var DoneConditionChecker = class {
3577
4693
  constructor(condition) {
3578
4694
  this.condition = condition;
4695
+ this.compiledRegex = condition.type === "output_match" && condition.pattern ? new RegExp(condition.pattern) : null;
3579
4696
  }
3580
4697
  condition;
4698
+ compiledRegex;
3581
4699
  check(state) {
3582
4700
  switch (this.condition.type) {
3583
4701
  case "iterations":
@@ -3591,11 +4709,8 @@ var DoneConditionChecker = class {
3591
4709
  }
3592
4710
  break;
3593
4711
  case "output_match":
3594
- if (this.condition.pattern && state.lastOutput) {
3595
- const regex = new RegExp(this.condition.pattern);
3596
- if (regex.test(state.lastOutput)) {
3597
- return { done: true, reason: `output matched pattern "${this.condition.pattern}"`, ...state };
3598
- }
4712
+ if (this.compiledRegex && state.lastOutput && this.compiledRegex.test(state.lastOutput)) {
4713
+ return { done: true, reason: `output matched pattern "${this.condition.pattern}"`, ...state };
3599
4714
  }
3600
4715
  break;
3601
4716
  }
@@ -3614,6 +4729,16 @@ var AutonomousRunner = class {
3614
4729
  stopped = false;
3615
4730
  doneChecker;
3616
4731
  async run() {
4732
+ const offToolExecuted = this.opts.agent.events?.on?.("tool.executed", () => {
4733
+ this.toolCalls++;
4734
+ });
4735
+ try {
4736
+ return await this.runLoop();
4737
+ } finally {
4738
+ offToolExecuted?.();
4739
+ }
4740
+ }
4741
+ async runLoop() {
3617
4742
  while (!this.stopped) {
3618
4743
  const check = this.doneChecker.check({
3619
4744
  iterations: this.iterations,
@@ -3640,7 +4765,6 @@ var AutonomousRunner = class {
3640
4765
  );
3641
4766
  this.iterations++;
3642
4767
  this.lastOutput = result.finalText;
3643
- this.toolCalls++;
3644
4768
  if (result.status === "failed" || result.status === "aborted") {
3645
4769
  const failedResult = {
3646
4770
  status: result.status,
@@ -3652,7 +4776,8 @@ var AutonomousRunner = class {
3652
4776
  return failedResult;
3653
4777
  }
3654
4778
  } catch (e) {
3655
- if (e.message.includes("timeout")) {
4779
+ const msg = e instanceof Error ? e.message : String(e);
4780
+ if (msg.includes("timeout")) {
3656
4781
  const timeoutResult = {
3657
4782
  status: "failed",
3658
4783
  error: toWrongStackError(e),
@@ -3681,14 +4806,11 @@ var AutonomousRunner = class {
3681
4806
 
3682
4807
  // src/defaults/spec-parser.ts
3683
4808
  var SpecParser = class {
3684
- constructor(opts = {}) {
3685
- this.opts = opts;
3686
- }
3687
- opts;
3688
4809
  parse(content) {
3689
4810
  const lines = content.split("\n");
3690
4811
  const sections = this.extractSections(lines);
3691
4812
  const requirements = this.extractRequirements(lines);
4813
+ const now = Date.now();
3692
4814
  return {
3693
4815
  id: crypto.randomUUID(),
3694
4816
  title: this.extractTitle(lines),
@@ -3697,8 +4819,8 @@ var SpecParser = class {
3697
4819
  overview: this.extractOverview(lines),
3698
4820
  sections,
3699
4821
  requirements,
3700
- createdAt: Date.now(),
3701
- updatedAt: Date.now()
4822
+ createdAt: now,
4823
+ updatedAt: now
3702
4824
  };
3703
4825
  }
3704
4826
  extractTitle(lines) {
@@ -3790,20 +4912,13 @@ var SpecParser = class {
3790
4912
  parseRequirementLine(line, id) {
3791
4913
  const trimmed = line.trim();
3792
4914
  if (!trimmed || trimmed.startsWith("#")) return null;
3793
- const typeMap = {
3794
- "functional": "functional",
3795
- "non-functional": "non-functional",
3796
- "security": "security",
3797
- "performance": "performance",
3798
- "ux": "ux"
3799
- };
4915
+ const lower = trimmed.toLowerCase();
4916
+ const types = ["functional", "non-functional", "security", "performance", "ux"];
3800
4917
  let type = "functional";
3801
- let priority = "medium";
3802
- for (const [key, val] of Object.entries(typeMap)) {
3803
- if (trimmed.toLowerCase().includes(`[${key}]`)) {
3804
- type = val;
3805
- }
4918
+ for (const t of types) {
4919
+ if (lower.includes(`[${t}]`)) type = t;
3806
4920
  }
4921
+ let priority = "medium";
3807
4922
  if (trimmed.includes("[critical]") || trimmed.includes("[prio:high]")) {
3808
4923
  priority = "critical";
3809
4924
  } else if (trimmed.includes("[high]")) {
@@ -3893,9 +5008,10 @@ var SpecParser = class {
3893
5008
  warnings.push({ path: `requirement.${req.id}`, message: "No acceptance criteria defined" });
3894
5009
  }
3895
5010
  }
5011
+ const reqIds = new Set(spec.requirements.map((r) => r.id));
3896
5012
  const blockedByIds = new Set(spec.requirements.flatMap((r) => r.blockedBy ?? []));
3897
5013
  for (const id of blockedByIds) {
3898
- if (!spec.requirements.find((r) => r.id === id)) {
5014
+ if (!reqIds.has(id)) {
3899
5015
  errors.push({ path: "requirements", message: `BlockedBy references non-existent requirement: ${id}` });
3900
5016
  }
3901
5017
  }
@@ -3925,25 +5041,21 @@ var TaskGenerator = class {
3925
5041
  status: "pending"
3926
5042
  });
3927
5043
  }
3928
- const criticalReqs = spec.requirements.filter((r) => r.priority === "critical");
3929
- const highReqs = spec.requirements.filter((r) => r.priority === "high");
3930
- const mediumReqs = spec.requirements.filter((r) => r.priority === "medium");
3931
- const lowReqs = spec.requirements.filter((r) => r.priority === "low");
3932
- for (const req of criticalReqs) {
3933
- const task = this.createTaskFromRequirement(req, spec.title);
3934
- this.opts.taskTracker.addNode(task);
3935
- }
3936
- for (const req of highReqs) {
3937
- const task = this.createTaskFromRequirement(req, spec.title);
3938
- this.opts.taskTracker.addNode(task);
3939
- }
3940
- for (const req of mediumReqs) {
3941
- const task = this.createTaskFromRequirement(req, spec.title);
3942
- this.opts.taskTracker.addNode(task);
5044
+ const byPriority = {
5045
+ critical: [],
5046
+ high: [],
5047
+ medium: [],
5048
+ low: []
5049
+ };
5050
+ for (const req of spec.requirements) {
5051
+ const bucket = byPriority[req.priority] ?? byPriority.medium;
5052
+ bucket.push(req);
3943
5053
  }
3944
- for (const req of lowReqs) {
3945
- const task = this.createTaskFromRequirement(req, spec.title);
3946
- this.opts.taskTracker.addNode(task);
5054
+ const order = ["critical", "high", "medium", "low"];
5055
+ for (const p of order) {
5056
+ for (const req of byPriority[p]) {
5057
+ this.opts.taskTracker.addNode(this.createTaskFromRequirement(req));
5058
+ }
3947
5059
  }
3948
5060
  if (spec.apiEndpoints && spec.apiEndpoints.length > 0) {
3949
5061
  const apiParent = this.opts.taskTracker.addNode({
@@ -3977,17 +5089,15 @@ var TaskGenerator = class {
3977
5089
  });
3978
5090
  return graph;
3979
5091
  }
3980
- createTaskFromRequirement(req, specTitle) {
3981
- const type = this.mapRequirementType(req.type);
3982
- const tags = [req.type, req.priority];
5092
+ createTaskFromRequirement(req) {
3983
5093
  return {
3984
5094
  title: req.description,
3985
- description: this.buildDescription(req, specTitle),
3986
- type,
3987
- priority: this.mapPriority(req.priority),
5095
+ description: this.buildDescription(req),
5096
+ type: this.mapRequirementType(req.type),
5097
+ priority: req.priority,
3988
5098
  status: "pending",
3989
5099
  specRequirementId: req.id,
3990
- tags,
5100
+ tags: [req.type, req.priority],
3991
5101
  estimateHours: this.estimateHours(req)
3992
5102
  };
3993
5103
  }
@@ -4002,7 +5112,7 @@ var TaskGenerator = class {
4002
5112
  estimateHours: this.estimateForEndpoint(endpoint)
4003
5113
  };
4004
5114
  }
4005
- buildDescription(req, specTitle) {
5115
+ buildDescription(req) {
4006
5116
  const lines = [
4007
5117
  req.description,
4008
5118
  "",
@@ -4036,20 +5146,6 @@ var TaskGenerator = class {
4036
5146
  return "feature";
4037
5147
  }
4038
5148
  }
4039
- mapPriority(priority) {
4040
- switch (priority) {
4041
- case "critical":
4042
- return "critical";
4043
- case "high":
4044
- return "high";
4045
- case "medium":
4046
- return "medium";
4047
- case "low":
4048
- return "low";
4049
- default:
4050
- return "medium";
4051
- }
4052
- }
4053
5149
  estimateHours(req) {
4054
5150
  switch (req.priority) {
4055
5151
  case "critical":
@@ -4120,16 +5216,33 @@ var DefaultTaskStore = class {
4120
5216
 
4121
5217
  // src/types/task-graph.ts
4122
5218
  function computeTaskProgress(graph) {
4123
- const nodes = Array.from(graph.nodes.values());
4124
- const total = nodes.length;
4125
- const completed = nodes.filter((n) => n.status === "completed").length;
4126
- const pending = nodes.filter((n) => n.status === "pending").length;
4127
- const inProgress = nodes.filter((n) => n.status === "in_progress").length;
4128
- const blocked = nodes.filter((n) => n.status === "blocked").length;
4129
- const failed = nodes.filter((n) => n.status === "failed").length;
4130
- const review = nodes.filter((n) => n.status === "review").length;
4131
- const estimatedHours = nodes.reduce((sum, n) => sum + (n.estimateHours ?? 0), 0);
4132
- const actualHours = nodes.reduce((sum, n) => sum + (n.actualHours ?? 0), 0);
5219
+ let completed = 0, pending = 0, inProgress = 0, blocked = 0, failed = 0, review = 0;
5220
+ let estimatedHours = 0, actualHours = 0;
5221
+ for (const n of graph.nodes.values()) {
5222
+ switch (n.status) {
5223
+ case "completed":
5224
+ completed++;
5225
+ break;
5226
+ case "pending":
5227
+ pending++;
5228
+ break;
5229
+ case "in_progress":
5230
+ inProgress++;
5231
+ break;
5232
+ case "blocked":
5233
+ blocked++;
5234
+ break;
5235
+ case "failed":
5236
+ failed++;
5237
+ break;
5238
+ case "review":
5239
+ review++;
5240
+ break;
5241
+ }
5242
+ estimatedHours += n.estimateHours ?? 0;
5243
+ actualHours += n.actualHours ?? 0;
5244
+ }
5245
+ const total = graph.nodes.size;
4133
5246
  return {
4134
5247
  total,
4135
5248
  pending,
@@ -4185,40 +5298,40 @@ var TaskTracker = class {
4185
5298
  this.graph.rootNodes.push(newNode.id);
4186
5299
  }
4187
5300
  this.graph.updatedAt = now;
4188
- this.opts.store.saveGraph(this.graph);
5301
+ this.persist();
4189
5302
  return newNode;
4190
5303
  }
4191
5304
  addEdge(from, to, type = "depends_on") {
4192
5305
  if (!this.graph) throw new Error("No graph loaded");
4193
- const edge = {
5306
+ this.graph.edges.push({
4194
5307
  id: crypto.randomUUID(),
4195
5308
  from,
4196
5309
  to,
4197
5310
  type
4198
- };
4199
- this.graph.edges.push(edge);
5311
+ });
4200
5312
  this.graph.updatedAt = Date.now();
4201
- this.opts.store.saveGraph(this.graph);
5313
+ this.persist();
4202
5314
  }
4203
5315
  updateNodeStatus(id, status, reason) {
4204
5316
  if (!this.graph) throw new Error("No graph loaded");
4205
5317
  const node = this.graph.nodes.get(id);
4206
5318
  if (!node) throw new Error(`Node ${id} not found`);
4207
5319
  const from = node.status;
5320
+ const now = Date.now();
4208
5321
  node.status = status;
4209
- node.updatedAt = Date.now();
5322
+ node.updatedAt = now;
4210
5323
  if (status === "completed") {
4211
- node.completedAt = Date.now();
5324
+ node.completedAt = now;
4212
5325
  }
4213
- this.transitions.push({ from, to: status, timestamp: Date.now(), reason });
5326
+ this.transitions.push({ from, to: status, timestamp: now, reason });
4214
5327
  if (status === "completed") {
4215
5328
  this.unblockDependents(id);
4216
5329
  }
4217
5330
  if (status === "in_progress") {
4218
5331
  this.checkAndBlockIfNeeded(id);
4219
5332
  }
4220
- this.graph.updatedAt = Date.now();
4221
- this.opts.store.saveGraph(this.graph);
5333
+ this.graph.updatedAt = now;
5334
+ this.persist();
4222
5335
  }
4223
5336
  getNode(id) {
4224
5337
  return this.graph?.nodes.get(id);
@@ -4239,9 +5352,7 @@ var TaskTracker = class {
4239
5352
  }
4240
5353
  if (sort) {
4241
5354
  nodes.sort((a, b) => {
4242
- const aVal = a[sort.field] ?? "";
4243
- const bVal = b[sort.field] ?? "";
4244
- const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
5355
+ const cmp = compareByField(a, b, sort.field);
4245
5356
  return sort.direction === "asc" ? cmp : -cmp;
4246
5357
  });
4247
5358
  }
@@ -4320,7 +5431,47 @@ var TaskTracker = class {
4320
5431
  }
4321
5432
  }
4322
5433
  }
5434
+ /**
5435
+ * Fire-and-forget persistence with attached error handler.
5436
+ * Synchronous mutators (addNode/addEdge/updateNodeStatus) use this to
5437
+ * avoid forcing an async cascade through every caller; if the store
5438
+ * rejects, the configured `onPersistError` is invoked so failures are
5439
+ * surfaced instead of swallowed by an unhandled promise rejection.
5440
+ */
5441
+ persist() {
5442
+ if (!this.graph) return;
5443
+ this.opts.store.saveGraph(this.graph).catch((err) => {
5444
+ if (this.opts.onPersistError) this.opts.onPersistError(err);
5445
+ else console.warn("[task-tracker] saveGraph failed:", err instanceof Error ? err.message : String(err));
5446
+ });
5447
+ }
5448
+ };
5449
+ var PRIORITY_RANK = {
5450
+ critical: 0,
5451
+ high: 1,
5452
+ medium: 2,
5453
+ low: 3
4323
5454
  };
5455
+ var STATUS_RANK = {
5456
+ in_progress: 0,
5457
+ pending: 1,
5458
+ review: 2,
5459
+ blocked: 3,
5460
+ failed: 4,
5461
+ completed: 5
5462
+ };
5463
+ function compareByField(a, b, field) {
5464
+ switch (field) {
5465
+ case "priority":
5466
+ return PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority];
5467
+ case "status":
5468
+ return STATUS_RANK[a.status] - STATUS_RANK[b.status];
5469
+ case "createdAt":
5470
+ return a.createdAt - b.createdAt;
5471
+ case "updatedAt":
5472
+ return a.updatedAt - b.updatedAt;
5473
+ }
5474
+ }
4324
5475
 
4325
5476
  // src/defaults/task-flow.ts
4326
5477
  var TaskFlow = class {
@@ -4372,9 +5523,10 @@ var TaskFlow = class {
4372
5523
  const task = batch[i];
4373
5524
  if (!result || !task) continue;
4374
5525
  if (result.status === "rejected") {
4375
- this.opts.tracker.updateNodeStatus(task.id, "failed", result.reason?.message);
4376
- this.emit("task.failed", { taskId: task.id, error: result.reason?.message ?? "unknown" });
4377
- ctx.onTaskFail?.(task, result.reason);
5526
+ const reason = result.reason;
5527
+ this.opts.tracker.updateNodeStatus(task.id, "failed", reason?.message);
5528
+ this.emit("task.failed", { taskId: task.id, error: reason?.message ?? "unknown" });
5529
+ ctx.onTaskFail?.(task, reason);
4378
5530
  } else {
4379
5531
  this.opts.tracker.updateNodeStatus(task.id, "completed");
4380
5532
  this.emit("task.completed", { taskId: task.id, result: result.value });
@@ -4657,7 +5809,6 @@ function createToolOutputSerializer(opts = {}) {
4657
5809
  }
4658
5810
  const half = Math.floor(available / 2);
4659
5811
  const first = text.slice(0, half);
4660
- Buffer.byteLength(first, "utf8");
4661
5812
  const second = text.slice(text.length - half);
4662
5813
  return { text: `${first}${marker}${second}`, newBudget: 0 };
4663
5814
  }
@@ -4707,7 +5858,7 @@ var ToolExecutor = class {
4707
5858
  return { result, tool, durationMs: Date.now() - start };
4708
5859
  }
4709
5860
  } else {
4710
- const suggestedPattern = this.subjectFor(tool.name, use.input) ?? tool.name;
5861
+ const suggestedPattern = this.subjectFor(tool.name, use.input, tool.subjectKey) ?? tool.name;
4711
5862
  const pending = { type: "tool_confirm_pending", toolUseId: use.id, toolName: tool.name, input: use.input, suggestedPattern };
4712
5863
  return { result: pending, tool, durationMs: Date.now() - start };
4713
5864
  }
@@ -4739,15 +5890,31 @@ var ToolExecutor = class {
4739
5890
  span?.end();
4740
5891
  }
4741
5892
  };
5893
+ const safeRun = async (use) => {
5894
+ try {
5895
+ return await runOne(use);
5896
+ } catch (err) {
5897
+ const msg = err instanceof Error ? err.message : String(err);
5898
+ const scrubbed = this.opts.secretScrubber.scrub(msg);
5899
+ const result = {
5900
+ type: "tool_result",
5901
+ tool_use_id: use.id,
5902
+ content: `Tool "${use.name}" execution failed: ${scrubbed}`,
5903
+ is_error: true
5904
+ };
5905
+ budget = this.decrementBudget(result, budget);
5906
+ return { result, tool: this.registry.get(use.name), durationMs: 0 };
5907
+ }
5908
+ };
4742
5909
  if (strategy === "sequential") {
4743
5910
  const outputs = [];
4744
5911
  for (const use of toolUses) {
4745
- if (use) outputs.push(await runOne(use));
5912
+ if (use) outputs.push(await safeRun(use));
4746
5913
  }
4747
5914
  return { outputs, remainingBudget: budget };
4748
5915
  }
4749
5916
  if (strategy === "parallel") {
4750
- const outputs = await Promise.all(toolUses.map((use) => runOne(use)));
5917
+ const outputs = await Promise.all(toolUses.map((use) => safeRun(use)));
4751
5918
  return { outputs, remainingBudget: budget };
4752
5919
  }
4753
5920
  const nonMutating = [];
@@ -4758,10 +5925,10 @@ var ToolExecutor = class {
4758
5925
  if (tool?.mutating) mutating.push(use);
4759
5926
  else nonMutating.push(use);
4760
5927
  }
4761
- const firstPass = await Promise.all(nonMutating.map((use) => runOne(use)));
5928
+ const firstPass = await Promise.all(nonMutating.map((use) => safeRun(use)));
4762
5929
  const secondPass = [];
4763
5930
  for (const use of mutating) {
4764
- secondPass.push(await runOne(use));
5931
+ secondPass.push(await safeRun(use));
4765
5932
  }
4766
5933
  return {
4767
5934
  outputs: [...firstPass, ...secondPass],
@@ -4796,7 +5963,8 @@ var ToolExecutor = class {
4796
5963
  }
4797
5964
  async runWithTimeout(tool, input, parentSignal, ctx, toolUseId) {
4798
5965
  if (parentSignal.aborted) {
4799
- throw parentSignal.reason instanceof Error ? parentSignal.reason : new Error(typeof parentSignal.reason === "string" ? parentSignal.reason : "aborted");
5966
+ if (parentSignal.reason instanceof Error) throw parentSignal.reason;
5967
+ throw new Error(typeof parentSignal.reason === "string" ? parentSignal.reason : "aborted");
4800
5968
  }
4801
5969
  const timeoutMs = tool.timeoutMs ?? this.iterationTimeoutMs;
4802
5970
  const ctrl = new AbortController();
@@ -4865,16 +6033,23 @@ var ToolExecutor = class {
4865
6033
  * Matches the logic in DefaultPermissionPolicy so the TUI shows the
4866
6034
  * same subject that the trust file would use.
4867
6035
  */
4868
- subjectFor(toolName, input) {
6036
+ subjectFor(toolName, input, subjectKey) {
4869
6037
  if (!input || typeof input !== "object") return void 0;
4870
6038
  const obj = input;
4871
6039
  const globChars = /[*?\[\]]/g;
4872
6040
  const escapeGlob = (s) => s.replace(globChars, (c) => `\\${c}`);
6041
+ const normalizePath = (s) => escapeGlob(s.replace(/\\/g, "/"));
6042
+ if (subjectKey) {
6043
+ const v = obj[subjectKey];
6044
+ if (typeof v === "string") {
6045
+ return subjectKey === "path" || subjectKey === "file" || subjectKey === "files" ? normalizePath(v) : escapeGlob(v);
6046
+ }
6047
+ }
4873
6048
  if (toolName === "bash" && typeof obj.command === "string") {
4874
6049
  return escapeGlob(obj.command);
4875
6050
  }
4876
6051
  if (typeof obj.path === "string") {
4877
- return escapeGlob(obj.path.replace(/\\/g, "/"));
6052
+ return normalizePath(obj.path);
4878
6053
  }
4879
6054
  if (typeof obj.url === "string") {
4880
6055
  return escapeGlob(obj.url);
@@ -5298,8 +6473,9 @@ var DefaultHealthRegistry = class {
5298
6473
  return { status, timestamp: Date.now(), checks: results };
5299
6474
  }
5300
6475
  async runOne(check) {
6476
+ let timer = null;
5301
6477
  const timeout = new Promise((resolve3) => {
5302
- setTimeout(
6478
+ timer = setTimeout(
5303
6479
  () => resolve3({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
5304
6480
  this.timeoutMs
5305
6481
  );
@@ -5308,6 +6484,8 @@ var DefaultHealthRegistry = class {
5308
6484
  return await Promise.race([check.check(), timeout]);
5309
6485
  } catch (err) {
5310
6486
  return { status: "unhealthy", detail: err instanceof Error ? err.message : String(err) };
6487
+ } finally {
6488
+ if (timer) clearTimeout(timer);
5311
6489
  }
5312
6490
  }
5313
6491
  };
@@ -5480,7 +6658,7 @@ var PROMETHEUS_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
5480
6658
  async function startMetricsServer(opts) {
5481
6659
  const { createServer } = await import('http');
5482
6660
  const host = opts.host ?? "127.0.0.1";
5483
- const path13 = opts.path ?? "/metrics";
6661
+ const path15 = opts.path ?? "/metrics";
5484
6662
  const healthPath = opts.healthPath ?? "/healthz";
5485
6663
  const healthRegistry = opts.healthRegistry;
5486
6664
  const server = createServer((req, res) => {
@@ -5490,7 +6668,7 @@ async function startMetricsServer(opts) {
5490
6668
  return;
5491
6669
  }
5492
6670
  const url = req.url.split("?")[0];
5493
- if (url === path13) {
6671
+ if (url === path15) {
5494
6672
  let body;
5495
6673
  try {
5496
6674
  body = renderPrometheus(opts.sink.snapshot());
@@ -5541,7 +6719,7 @@ async function startMetricsServer(opts) {
5541
6719
  const boundPort = typeof addr === "object" && addr ? addr.port : opts.port;
5542
6720
  return {
5543
6721
  port: boundPort,
5544
- url: `http://${host}:${boundPort}${path13}`,
6722
+ url: `http://${host}:${boundPort}${path15}`,
5545
6723
  close: () => new Promise((resolve3, reject) => {
5546
6724
  server.close((err) => err ? reject(err) : resolve3());
5547
6725
  })
@@ -5946,7 +7124,6 @@ function createContextManagerTool(opts = {}) {
5946
7124
  notes: `Invalid range [${from}, ${to}] for ${messages.length} messages.`
5947
7125
  };
5948
7126
  }
5949
- messages.slice(from, to + 1);
5950
7127
  const summaryText = input.text ?? '[summary placeholder \u2014 provide "text" to record the summary]';
5951
7128
  const summaryMsg = {
5952
7129
  role: "system",
@@ -6076,6 +7253,6 @@ var allServers = () => ({
6076
7253
  sentinel: { ...sentinelServer(), enabled: false }
6077
7254
  });
6078
7255
 
6079
- export { AutoCompactionMiddleware, AutonomousRunner, BudgetExceededError, ConfigMigrationError, DEFAULT_CONFIG_MIGRATIONS, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, DefaultTokenCounter, DoneConditionChecker, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, IntelligentCompactor, LLMSelector, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, QueueStore, RecoveryLock, SelectiveCompactor, SpecDrivenDev, SpecParser, SubagentBudget, TaskFlow, TaskGenerator, TaskTracker, ToolExecutor, allServers, awsServer, blockServer, braveSearchServer, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, context7Server, contextManagerTool, createContextManagerTool, createMessage, decryptConfigSecrets, encryptConfigSecrets, everArtServer, filesystemServer, githubServer, googleMapsServer, loadProjectModes, loadUserModes, makeAgentSubagentRunner, migratePlaintextSecrets, renderPrometheus, rewriteConfigEncrypted, runConfigMigrations, sentinelServer, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, wireMetricsToEvents };
7256
+ export { AutoCompactionMiddleware, AutonomousRunner, BudgetExceededError, ConfigMigrationError, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_SUBAGENT_BASELINE, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, DefaultTokenCounter, Director, DoneConditionChecker, FleetBus, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, IntelligentCompactor, LLMSelector, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, QueueStore, RecoveryLock, SelectiveCompactor, SpecDrivenDev, SpecParser, SubagentBudget, TaskFlow, TaskGenerator, TaskTracker, ToolExecutor, allServers, awsServer, blockServer, braveSearchServer, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, composeDirectorPrompt, composeSubagentPrompt, context7Server, contextManagerTool, createContextManagerTool, createMessage, decryptConfigSecrets, encryptConfigSecrets, everArtServer, filesystemServer, githubServer, googleMapsServer, loadProjectModes, loadUserModes, makeAgentSubagentRunner, makeDirectorSessionFactory, migratePlaintextSecrets, renderPrometheus, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, sentinelServer, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, wireMetricsToEvents };
6080
7257
  //# sourceMappingURL=index.js.map
6081
7258
  //# sourceMappingURL=index.js.map