@wrongstack/core 0.9.19 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +37 -33
  2. package/dist/{agent-bridge-DMVOX0cF.d.ts → agent-bridge-DBBGP5O4.d.ts} +1 -1
  3. package/dist/{agent-subagent-runner-C4qt9e5Y.d.ts → agent-subagent-runner-KwufTWwn.d.ts} +3 -4
  4. package/dist/compactor-D1RHFRmF.d.ts +41 -0
  5. package/dist/{config-CWva0qoL.d.ts → config-CJXBka2r.d.ts} +38 -2
  6. package/dist/{context-BRNbHmRM.d.ts → context-7u93AcGD.d.ts} +140 -0
  7. package/dist/coordination/index.d.ts +13 -13
  8. package/dist/coordination/index.js +786 -200
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/{default-config-Uzla85H-.d.ts → default-config-DEXI4jsl.d.ts} +10 -1
  11. package/dist/defaults/index.d.ts +24 -24
  12. package/dist/defaults/index.js +1612 -528
  13. package/dist/defaults/index.js.map +1 -1
  14. package/dist/{events-CiG9qUM_.d.ts → events-Bw1K5E6K.d.ts} +49 -1
  15. package/dist/execution/index.d.ts +46 -32
  16. package/dist/execution/index.js +663 -169
  17. package/dist/execution/index.js.map +1 -1
  18. package/dist/extension/index.d.ts +6 -6
  19. package/dist/extension/index.js.map +1 -1
  20. package/dist/goal-store-BeRsj7YX.d.ts +188 -0
  21. package/dist/{index-p95HQ22A.d.ts → index-DIt5TIaF.d.ts} +8 -8
  22. package/dist/{index-aizK8olO.d.ts → index-DgQWnZxP.d.ts} +11 -8
  23. package/dist/index.d.ts +338 -41
  24. package/dist/index.js +2747 -787
  25. package/dist/index.js.map +1 -1
  26. package/dist/infrastructure/index.d.ts +6 -6
  27. package/dist/infrastructure/index.js +36 -0
  28. package/dist/infrastructure/index.js.map +1 -1
  29. package/dist/kernel/index.d.ts +9 -9
  30. package/dist/kernel/index.js +57 -8
  31. package/dist/kernel/index.js.map +1 -1
  32. package/dist/{mcp-servers-BkVEqkRe.d.ts → mcp-servers-BgINZzuo.d.ts} +25 -3
  33. package/dist/models/index.d.ts +3 -3
  34. package/dist/models/index.js +164 -11
  35. package/dist/models/index.js.map +1 -1
  36. package/dist/models-registry-gwMAo6E3.d.ts +86 -0
  37. package/dist/{multi-agent-3ZnTB1aT.d.ts → multi-agent-C1O8EFzb.d.ts} +77 -24
  38. package/dist/{multi-agent-coordinator-bRaI_aD1.d.ts → multi-agent-coordinator-DoVSBFNx.d.ts} +20 -2
  39. package/dist/{null-fleet-bus-DKM3Iy9d.d.ts → null-fleet-bus-T-s3JaOt.d.ts} +413 -110
  40. package/dist/observability/index.d.ts +2 -2
  41. package/dist/{path-resolver-TcJfc29Y.d.ts → path-resolver-BfyK-I7w.d.ts} +2 -2
  42. package/dist/{permission-bPuzAy4x.d.ts → permission-B6sldrSp.d.ts} +1 -1
  43. package/dist/{permission-policy-BUQSutpl.d.ts → permission-policy-CtNscWOA.d.ts} +10 -9
  44. package/dist/{plan-templates-fkQTyz3U.d.ts → plan-templates-BWs6Je7j.d.ts} +8 -8
  45. package/dist/{provider-runner-BEpikbbN.d.ts → provider-runner-BmPe2RuU.d.ts} +3 -3
  46. package/dist/{retry-policy-BYkq0ugs.d.ts → retry-policy-KF18W4dg.d.ts} +1 -1
  47. package/dist/sdd/index.d.ts +9 -10
  48. package/dist/sdd/index.js +350 -122
  49. package/dist/sdd/index.js.map +1 -1
  50. package/dist/security/index.d.ts +65 -4
  51. package/dist/security/index.js +67 -19
  52. package/dist/security/index.js.map +1 -1
  53. package/dist/{selector-pox8abg0.d.ts → selector-DmXxpFyM.d.ts} +1 -1
  54. package/dist/{session-reader-CSWcb5Ga.d.ts → session-reader-CCSBfQmd.d.ts} +2 -2
  55. package/dist/skills/index.d.ts +1 -1
  56. package/dist/skills/index.js +1 -1
  57. package/dist/skills/index.js.map +1 -1
  58. package/dist/storage/index.d.ts +392 -8
  59. package/dist/storage/index.js +796 -40
  60. package/dist/storage/index.js.map +1 -1
  61. package/dist/{system-prompt-Bs-Wliab.d.ts → system-prompt-CM6zOhd2.d.ts} +19 -1
  62. package/dist/{tool-executor-Boo3dekH.d.ts → tool-executor-D7RxLtg4.d.ts} +9 -4
  63. package/dist/types/index.d.ts +16 -16
  64. package/dist/types/index.js +404 -14
  65. package/dist/types/index.js.map +1 -1
  66. package/dist/utils/index.d.ts +40 -8
  67. package/dist/utils/index.js +127 -57
  68. package/dist/utils/index.js.map +1 -1
  69. package/dist/{wstack-paths-BCgmTNlG.d.ts → wstack-paths-eMXnY1_X.d.ts} +8 -0
  70. package/package.json +1 -1
  71. package/dist/compactor-DVTKL7XD.d.ts +0 -23
  72. package/dist/goal-store-C7jcumEh.d.ts +0 -96
  73. package/dist/models-registry-OG_30xqZ.d.ts +0 -46
package/dist/index.js CHANGED
@@ -56,8 +56,8 @@ async function atomicWrite(targetPath, content, opts = {}) {
56
56
  }
57
57
  let mode;
58
58
  try {
59
- const stat9 = await fsp2.stat(targetPath);
60
- mode = stat9.mode & 511;
59
+ const stat8 = await fsp2.stat(targetPath);
60
+ mode = stat8.mode & 511;
61
61
  } catch {
62
62
  mode = opts.mode;
63
63
  }
@@ -142,8 +142,10 @@ var ERROR_CODES = {
142
142
  // Container / Registry
143
143
  CONTAINER_TOKEN_ALREADY_BOUND: "CONTAINER_TOKEN_ALREADY_BOUND",
144
144
  CONTAINER_TOKEN_NOT_BOUND: "CONTAINER_TOKEN_NOT_BOUND",
145
+ CONTAINER_CIRCULAR_DEPENDENCY: "CONTAINER_CIRCULAR_DEPENDENCY",
145
146
  REGISTRY_DUPLICATE: "REGISTRY_DUPLICATE",
146
147
  REGISTRY_NOT_FOUND: "REGISTRY_NOT_FOUND",
148
+ REGISTRY_INVALID: "REGISTRY_INVALID",
147
149
  // File system
148
150
  FS_READ_FAILED: "FS_READ_FAILED",
149
151
  FS_WRITE_FAILED: "FS_WRITE_FAILED",
@@ -306,6 +308,16 @@ function isFsError(err) {
306
308
  // src/kernel/container.ts
307
309
  var Container = class {
308
310
  entries = /* @__PURE__ */ new Map();
311
+ /**
312
+ * Tokens currently mid-resolve. Tracked so we can detect circular
313
+ * dependencies (A → B → A) and throw a structured error instead of
314
+ * overflowing the call stack with "Maximum call stack size exceeded".
315
+ *
316
+ * Not a memoization cache — the per-entry `cache` field is the source
317
+ * of truth for "have I built this before?". This set only lives for
318
+ * the duration of a single resolve call.
319
+ */
320
+ resolving = /* @__PURE__ */ new Set();
309
321
  bind(token, factory, opts = {}) {
310
322
  if (this.entries.has(token)) {
311
323
  throw new WrongStackError({
@@ -366,14 +378,43 @@ var Container = class {
366
378
  if (entry.singleton && entry.cache !== void 0) {
367
379
  return entry.cache;
368
380
  }
369
- let value = entry.factory(this);
370
- for (const d of entry.decorators) {
371
- value = d(value, this);
381
+ if (this.resolving.has(token)) {
382
+ const cycle = this.describeCycle(token);
383
+ throw new WrongStackError({
384
+ message: `Container: circular dependency detected \u2014 ${cycle}`,
385
+ code: ERROR_CODES.CONTAINER_CIRCULAR_DEPENDENCY,
386
+ subsystem: "container",
387
+ context: { token: token.description, cycle }
388
+ });
389
+ }
390
+ this.resolving.add(token);
391
+ try {
392
+ let value = entry.factory(this);
393
+ for (const d of entry.decorators) {
394
+ value = d(value, this);
395
+ }
396
+ if (entry.singleton) {
397
+ entry.cache = value;
398
+ }
399
+ return value;
400
+ } finally {
401
+ this.resolving.delete(token);
372
402
  }
373
- if (entry.singleton) {
374
- entry.cache = value;
403
+ }
404
+ /**
405
+ * Build a human-readable description of the dependency cycle that
406
+ * caused the resolution to re-enter. Lists the tokens in the order
407
+ * they were entered, then appends the re-entered token to close the
408
+ * loop. Falls back to a generic message if the resolving set is
409
+ * somehow empty (shouldn't happen, but defensive).
410
+ */
411
+ describeCycle(reentry) {
412
+ const descs = [];
413
+ for (const t2 of this.resolving) {
414
+ descs.push(t2.description ?? "unknown");
375
415
  }
376
- return value;
416
+ descs.push(reentry.description ?? "unknown");
417
+ return descs.join(" \u2192 ");
377
418
  }
378
419
  has(token) {
379
420
  return this.entries.has(token);
@@ -644,7 +685,8 @@ var EventBus = class {
644
685
  }
645
686
  if (this.wildcards.length > 0) {
646
687
  const name = event;
647
- for (const { match, fn } of this.wildcards) {
688
+ const snapshot = this.wildcards.slice();
689
+ for (const { match, fn } of snapshot) {
648
690
  if (!match(name)) continue;
649
691
  try {
650
692
  fn(name, payload);
@@ -715,11 +757,19 @@ var ScopedEventBus = class extends EventBus {
715
757
  * Uses EventBus's public API directly to avoid triggering our own `on()`
716
758
  * override (which would consume a key slot for the wrapper, then orphan
717
759
  * our registration entry under a different key).
760
+ *
761
+ * When the wrapper fires, it cleans up BOTH the underlying EventBus
762
+ * listener AND the tracking entry — so `scopedListenerCount` returns to
763
+ * its pre-`once()` value without requiring the caller to invoke the
764
+ * returned unsubscribe. The returned `unsub` is still safe to call
765
+ * after auto-removal (its delete is a no-op and its off() finds
766
+ * nothing to remove).
718
767
  */
719
768
  once(event, fn) {
720
769
  const key = this.nextKey++;
721
770
  const wrapper = (payload) => {
722
771
  EventBus.prototype.off.call(this, event, wrapper);
772
+ this.registrations.delete(key);
723
773
  fn(payload);
724
774
  };
725
775
  EventBus.prototype.on.call(this, event, wrapper);
@@ -1024,6 +1074,14 @@ var DEFAULT_CONTEXT_CONFIG = Object.freeze({
1024
1074
  var DEFAULT_AUTONOMY_CONFIG = Object.freeze({
1025
1075
  autoProceedDelayMs: 45e3
1026
1076
  });
1077
+ var DEFAULT_SESSION_LOGGING_CONFIG = Object.freeze({
1078
+ auditLevel: "standard",
1079
+ sampling: {
1080
+ toolProgress: {
1081
+ sampleRate: 8
1082
+ }
1083
+ }
1084
+ });
1027
1085
 
1028
1086
  // src/types/secret-vault.ts
1029
1087
  var ENCRYPTED_PREFIX = "enc:v1:";
@@ -1676,6 +1734,7 @@ var HybridCompactor = class {
1676
1734
  }
1677
1735
  async compact(ctx, opts = {}) {
1678
1736
  const beforeTokens = this.estimateMessages(ctx.messages);
1737
+ const beforeFull = this.estimateFullRequest(ctx);
1679
1738
  const reductions = [];
1680
1739
  const policy = readContextWindowPolicy(ctx);
1681
1740
  const preserveK = policy?.preserveK ?? this.preserveK;
@@ -1691,9 +1750,12 @@ var HybridCompactor = class {
1691
1750
  ctx.state.replaceMessages(repaired.messages);
1692
1751
  }
1693
1752
  const afterTokens = this.estimateMessages(ctx.messages);
1753
+ const afterFull = this.estimateFullRequest(ctx);
1694
1754
  return {
1695
1755
  before: beforeTokens,
1696
1756
  after: afterTokens,
1757
+ fullRequestTokensBefore: beforeFull,
1758
+ fullRequestTokensAfter: afterFull,
1697
1759
  reductions,
1698
1760
  repaired: repaired.report.changed ? {
1699
1761
  removedToolUses: repaired.report.removedToolUses,
@@ -1702,6 +1764,14 @@ var HybridCompactor = class {
1702
1764
  } : void 0
1703
1765
  };
1704
1766
  }
1767
+ /**
1768
+ * Estimate the full API request token count: messages + systemPrompt + toolDefs.
1769
+ * This is the accurate figure for context-window pressure monitoring.
1770
+ */
1771
+ estimateFullRequest(ctx) {
1772
+ const breakdown = estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []);
1773
+ return breakdown.total;
1774
+ }
1705
1775
  eliseOldToolResults(ctx, preserveK = this.preserveK, eliseThreshold = this.eliseThreshold) {
1706
1776
  const messages = ctx.messages;
1707
1777
  let pairCount = 0;
@@ -2128,6 +2198,71 @@ var DefaultSecretScrubber = class {
2128
2198
 
2129
2199
  // src/models/models-registry.ts
2130
2200
  init_atomic_write();
2201
+
2202
+ // src/utils/merge-models-payload.ts
2203
+ function mergeModelsPayload(base, overlay) {
2204
+ const out = {};
2205
+ for (const [id, provider] of Object.entries(base)) {
2206
+ out[id] = cloneProvider(provider);
2207
+ }
2208
+ for (const [id, ovProvider] of Object.entries(overlay)) {
2209
+ const existing = out[id];
2210
+ out[id] = existing ? mergeProvider(existing, ovProvider) : cloneProvider(ovProvider);
2211
+ }
2212
+ return out;
2213
+ }
2214
+ function mergeProvider(base, overlay) {
2215
+ const models = {};
2216
+ for (const [mid, m] of Object.entries(base.models ?? {})) {
2217
+ models[mid] = { ...m };
2218
+ }
2219
+ for (const [mid, ovModel] of Object.entries(overlay.models ?? {})) {
2220
+ const existing = models[mid];
2221
+ models[mid] = existing ? mergeModel(existing, ovModel) : { ...ovModel };
2222
+ }
2223
+ return {
2224
+ ...base,
2225
+ // Overlay scalar fields win when explicitly provided; otherwise keep base.
2226
+ ...stripUndefined({
2227
+ id: overlay.id,
2228
+ name: overlay.name,
2229
+ npm: overlay.npm,
2230
+ api: overlay.api,
2231
+ env: overlay.env,
2232
+ doc: overlay.doc
2233
+ }),
2234
+ models
2235
+ };
2236
+ }
2237
+ function mergeModel(base, overlay) {
2238
+ const merged = { ...base, ...overlay };
2239
+ if (base.limit || overlay.limit) {
2240
+ merged.limit = { ...base.limit, ...overlay.limit };
2241
+ }
2242
+ if (base.cost || overlay.cost) {
2243
+ merged.cost = { ...base.cost, ...overlay.cost };
2244
+ }
2245
+ if (base.modalities || overlay.modalities) {
2246
+ merged.modalities = { ...base.modalities, ...overlay.modalities };
2247
+ }
2248
+ return merged;
2249
+ }
2250
+ function cloneProvider(p) {
2251
+ const models = {};
2252
+ for (const [mid, m] of Object.entries(p.models ?? {})) {
2253
+ models[mid] = { ...m };
2254
+ }
2255
+ return { ...p, models };
2256
+ }
2257
+ function stripUndefined(obj) {
2258
+ const out = {};
2259
+ for (const [k, v] of Object.entries(obj)) {
2260
+ if (v !== void 0) out[k] = v;
2261
+ }
2262
+ return out;
2263
+ }
2264
+
2265
+ // src/models/models-registry.ts
2131
2266
  var DEFAULT_URL = "https://models.dev/api.json";
2132
2267
  var DEFAULT_TTL_SECONDS = 24 * 3600;
2133
2268
  var FAMILY_BY_NPM = {
@@ -2155,7 +2290,10 @@ function classifyFamily(npm) {
2155
2290
  return FAMILY_BY_NPM[npm] ?? "unsupported";
2156
2291
  }
2157
2292
  var DefaultModelsRegistry = class {
2293
+ /** Merged (base + overlay) payload — what every reader sees. */
2158
2294
  payload;
2295
+ /** Memoised overlay payload (in-memory / fetched / file). */
2296
+ overlayPayload;
2159
2297
  fetchedAt;
2160
2298
  cacheFile;
2161
2299
  url;
@@ -2163,6 +2301,10 @@ var DefaultModelsRegistry = class {
2163
2301
  fetchImpl;
2164
2302
  seed;
2165
2303
  maxStaleAgeMs;
2304
+ overlay;
2305
+ overlayUrl;
2306
+ overlayFile;
2307
+ overlayCacheFile;
2166
2308
  constructor(opts) {
2167
2309
  this.cacheFile = opts.cacheFile;
2168
2310
  this.url = opts.url ?? DEFAULT_URL;
@@ -2171,6 +2313,10 @@ var DefaultModelsRegistry = class {
2171
2313
  this.seed = opts.seed;
2172
2314
  const maxStaleSeconds = opts.maxStaleAgeSeconds ?? 7 * 24 * 3600;
2173
2315
  this.maxStaleAgeMs = maxStaleSeconds * 1e3;
2316
+ this.overlay = opts.overlay;
2317
+ this.overlayUrl = opts.overlayUrl;
2318
+ this.overlayFile = opts.overlayFile;
2319
+ this.overlayCacheFile = opts.overlayCacheFile ?? (opts.overlayUrl ? path6.join(path6.dirname(opts.cacheFile), "models-overlay-cache.json") : void 0);
2174
2320
  }
2175
2321
  async load(opts = {}) {
2176
2322
  if (this.payload && !opts.force) return this.payload;
@@ -2179,27 +2325,45 @@ var DefaultModelsRegistry = class {
2179
2325
  this.fetchedAt = /* @__PURE__ */ new Date();
2180
2326
  return this.payload;
2181
2327
  }
2328
+ const overlay = await this.loadOverlay(opts);
2329
+ const base = await this.loadBase(opts, Object.keys(overlay).length > 0);
2330
+ this.payload = mergeModelsPayload(base, overlay);
2331
+ return this.payload;
2332
+ }
2333
+ /**
2334
+ * Load the models.dev base payload: fresh cache → network → stale cache.
2335
+ * On total failure, degrade to `{}` (so a non-empty overlay still drives
2336
+ * the catalog) rather than throwing — unless there's no curated overlay to
2337
+ * fall back on, in which case the original error propagates so pure-
2338
+ * models.dev setups still surface the problem.
2339
+ */
2340
+ async loadBase(opts = {}, overlayAvailable = false) {
2182
2341
  if (!opts.force) {
2183
- const cached = await this.readCache();
2342
+ const cached = await this.readCacheAt(this.cacheFile);
2184
2343
  if (cached && this.isFresh(cached.fetchedAt)) {
2185
- this.payload = cached.payload;
2186
2344
  this.fetchedAt = new Date(cached.fetchedAt);
2187
2345
  return cached.payload;
2188
2346
  }
2189
2347
  }
2190
2348
  try {
2191
- return await this.refresh();
2349
+ return await this.refreshBase();
2192
2350
  } catch (err) {
2193
- const cached = await this.readCache();
2351
+ const cached = await this.readCacheAt(this.cacheFile);
2194
2352
  if (cached && this.isWithinMaxStaleAge(cached.fetchedAt)) {
2195
- this.payload = cached.payload;
2196
2353
  this.fetchedAt = new Date(cached.fetchedAt);
2197
2354
  return cached.payload;
2198
2355
  }
2356
+ if (overlayAvailable) {
2357
+ console.warn(
2358
+ `ModelsRegistry: models.dev unavailable (${err instanceof Error ? err.message : String(err)}); serving curated overlay only.`
2359
+ );
2360
+ return {};
2361
+ }
2199
2362
  throw err;
2200
2363
  }
2201
2364
  }
2202
- async refresh() {
2365
+ /** Fetch + cache the models.dev base. Throws on failure (used by `refresh`). */
2366
+ async refreshBase() {
2203
2367
  const res = await this.fetchImpl(this.url, {
2204
2368
  method: "GET",
2205
2369
  headers: { accept: "application/json" }
@@ -2208,7 +2372,6 @@ var DefaultModelsRegistry = class {
2208
2372
  throw new Error(`ModelsRegistry: HTTP ${res.status} fetching ${this.url}`);
2209
2373
  }
2210
2374
  const json = await res.json();
2211
- this.payload = json;
2212
2375
  this.fetchedAt = /* @__PURE__ */ new Date();
2213
2376
  const envelope = {
2214
2377
  fetchedAt: this.fetchedAt.toISOString(),
@@ -2218,6 +2381,68 @@ var DefaultModelsRegistry = class {
2218
2381
  await atomicWrite(this.cacheFile, JSON.stringify(envelope));
2219
2382
  return json;
2220
2383
  }
2384
+ /**
2385
+ * Resolve the curated overlay, memoised. Order: in-memory `overlay` →
2386
+ * fetched `overlayUrl` (cached, same TTL/stale rules) → `overlayFile` on
2387
+ * disk. Never throws — a missing/broken overlay yields `{}`.
2388
+ */
2389
+ async loadOverlay(opts = {}) {
2390
+ if (this.overlayPayload && !opts.force) return this.overlayPayload;
2391
+ if (this.overlay) {
2392
+ this.overlayPayload = this.overlay;
2393
+ return this.overlayPayload;
2394
+ }
2395
+ const fetched = await this.loadOverlayFromUrl(opts);
2396
+ if (fetched) {
2397
+ this.overlayPayload = fetched;
2398
+ return fetched;
2399
+ }
2400
+ const fromFile = await this.readOverlayFile();
2401
+ this.overlayPayload = fromFile ?? {};
2402
+ return this.overlayPayload;
2403
+ }
2404
+ async loadOverlayFromUrl(opts) {
2405
+ if (!this.overlayUrl || !this.overlayCacheFile) return void 0;
2406
+ if (!opts.force) {
2407
+ const cached = await this.readCacheAt(this.overlayCacheFile);
2408
+ if (cached && this.isFresh(cached.fetchedAt)) return cached.payload;
2409
+ }
2410
+ try {
2411
+ const res = await this.fetchImpl(this.overlayUrl, {
2412
+ method: "GET",
2413
+ headers: { accept: "application/json" }
2414
+ });
2415
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2416
+ const json = await res.json();
2417
+ const envelope = {
2418
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
2419
+ url: this.overlayUrl,
2420
+ payload: json
2421
+ };
2422
+ await atomicWrite(this.overlayCacheFile, JSON.stringify(envelope)).catch(() => {
2423
+ });
2424
+ return json;
2425
+ } catch {
2426
+ const cached = await this.readCacheAt(this.overlayCacheFile);
2427
+ if (cached && this.isWithinMaxStaleAge(cached.fetchedAt)) return cached.payload;
2428
+ return void 0;
2429
+ }
2430
+ }
2431
+ async readOverlayFile() {
2432
+ if (!this.overlayFile) return void 0;
2433
+ try {
2434
+ const raw = await fsp2.readFile(this.overlayFile, "utf8");
2435
+ return JSON.parse(raw);
2436
+ } catch {
2437
+ return void 0;
2438
+ }
2439
+ }
2440
+ async refresh() {
2441
+ const base = await this.refreshBase();
2442
+ const overlay = await this.loadOverlay({ force: true });
2443
+ this.payload = mergeModelsPayload(base, overlay);
2444
+ return this.payload;
2445
+ }
2221
2446
  async listProviders() {
2222
2447
  const payload = await this.load();
2223
2448
  return Object.values(payload).map((p) => this.resolveProvider(p));
@@ -2258,7 +2483,7 @@ var DefaultModelsRegistry = class {
2258
2483
  }
2259
2484
  async ageSeconds() {
2260
2485
  if (!this.fetchedAt) {
2261
- const cached = await this.readCache();
2486
+ const cached = await this.readCacheAt(this.cacheFile);
2262
2487
  if (!cached) return Number.POSITIVE_INFINITY;
2263
2488
  return (Date.now() - new Date(cached.fetchedAt).getTime()) / 1e3;
2264
2489
  }
@@ -2282,9 +2507,9 @@ var DefaultModelsRegistry = class {
2282
2507
  isWithinMaxStaleAge(fetchedAtIso) {
2283
2508
  return Date.now() - new Date(fetchedAtIso).getTime() < this.maxStaleAgeMs;
2284
2509
  }
2285
- async readCache() {
2510
+ async readCacheAt(file) {
2286
2511
  try {
2287
- const raw = await fsp2.readFile(this.cacheFile, "utf8");
2512
+ const raw = await fsp2.readFile(file, "utf8");
2288
2513
  return JSON.parse(raw);
2289
2514
  } catch {
2290
2515
  return void 0;
@@ -2856,6 +3081,141 @@ function topologicalSort(graph) {
2856
3081
  return result;
2857
3082
  }
2858
3083
 
3084
+ // src/security/capabilities.ts
3085
+ var ToolCapabilities = {
3086
+ /** Can execute arbitrary commands in the user's shell (the `bash` tool). */
3087
+ SHELL_ARBITRARY: "shell.arbitrary",
3088
+ /** Can write / modify / delete files inside the project. */
3089
+ FS_WRITE: "fs.write",
3090
+ /** Can write files outside the current project root (very high risk). */
3091
+ FS_WRITE_OUTSIDE_PROJECT: "fs.write.outside-project",
3092
+ /** Proxies tools from external MCP servers (unknown capability). */
3093
+ MCP_PROXY: "mcp.proxy",
3094
+ /** Can spawn or manage subagents / multi-agent tasks. */
3095
+ SUBAGENT_SPAWN: "subagent.spawn",
3096
+ /** Can mutate global or session configuration / trust state. */
3097
+ CONFIG_MUTATE: "config.mutate",
3098
+ /** Can install packages or run package managers with side effects. */
3099
+ PACKAGE_INSTALL: "package.install"
3100
+ };
3101
+ var DANGEROUS_FOR_SUBAGENTS = [
3102
+ ToolCapabilities.SHELL_ARBITRARY,
3103
+ ToolCapabilities.FS_WRITE,
3104
+ ToolCapabilities.FS_WRITE_OUTSIDE_PROJECT,
3105
+ ToolCapabilities.MCP_PROXY,
3106
+ ToolCapabilities.SUBAGENT_SPAWN,
3107
+ ToolCapabilities.CONFIG_MUTATE,
3108
+ ToolCapabilities.PACKAGE_INSTALL
3109
+ ];
3110
+ function hasDangerousCapabilityForSubagents(toolOrCaps) {
3111
+ if (!toolOrCaps) return false;
3112
+ const input = toolOrCaps;
3113
+ const caps = Array.isArray(input) ? input : input?.capabilities ?? [];
3114
+ return caps.some((c) => DANGEROUS_FOR_SUBAGENTS.includes(c));
3115
+ }
3116
+ function getDangerousCapabilities(toolOrCaps) {
3117
+ if (!toolOrCaps) return [];
3118
+ const input = toolOrCaps;
3119
+ const caps = Array.isArray(input) ? input : input?.capabilities ?? [];
3120
+ return caps.filter(
3121
+ (c) => DANGEROUS_FOR_SUBAGENTS.includes(c)
3122
+ );
3123
+ }
3124
+
3125
+ // src/utils/json-schema-validate.ts
3126
+ function validateAgainstSchema(value, schema) {
3127
+ const errors = [];
3128
+ walk2(value, schema, "", errors);
3129
+ return { ok: errors.length === 0, errors };
3130
+ }
3131
+ function walk2(value, schema, path35, errors) {
3132
+ if (schema.enum !== void 0) {
3133
+ if (!schema.enum.some((e) => deepEqual(e, value))) {
3134
+ errors.push({
3135
+ path: path35 || "<root>",
3136
+ message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
3137
+ });
3138
+ return;
3139
+ }
3140
+ }
3141
+ if (typeof schema.type === "string") {
3142
+ if (!checkType(value, schema.type)) {
3143
+ errors.push({
3144
+ path: path35 || "<root>",
3145
+ message: `expected ${schema.type}, got ${describeType(value)}`
3146
+ });
3147
+ return;
3148
+ }
3149
+ }
3150
+ if (schema.type === "object" && isPlainObject(value)) {
3151
+ const obj = value;
3152
+ for (const req of schema.required ?? []) {
3153
+ if (!(req in obj)) {
3154
+ errors.push({ path: joinPath(path35, req), message: "required property missing" });
3155
+ }
3156
+ }
3157
+ if (schema.properties) {
3158
+ for (const [key, subSchema] of Object.entries(schema.properties)) {
3159
+ if (key in obj) {
3160
+ walk2(obj[key], subSchema, joinPath(path35, key), errors);
3161
+ }
3162
+ }
3163
+ }
3164
+ }
3165
+ if (schema.type === "array" && Array.isArray(value) && schema.items) {
3166
+ value.forEach((item, i) => walk2(item, schema.items, `${path35}[${i}]`, errors));
3167
+ }
3168
+ }
3169
+ function checkType(value, type) {
3170
+ switch (type) {
3171
+ case "string":
3172
+ return typeof value === "string";
3173
+ case "number":
3174
+ return typeof value === "number" && !Number.isNaN(value);
3175
+ case "integer":
3176
+ return typeof value === "number" && Number.isInteger(value);
3177
+ case "boolean":
3178
+ return typeof value === "boolean";
3179
+ case "null":
3180
+ return value === null;
3181
+ case "array":
3182
+ return Array.isArray(value);
3183
+ case "object":
3184
+ return isPlainObject(value);
3185
+ default:
3186
+ return true;
3187
+ }
3188
+ }
3189
+ function isPlainObject(v) {
3190
+ return typeof v === "object" && v !== null && !Array.isArray(v);
3191
+ }
3192
+ function describeType(v) {
3193
+ if (v === null) return "null";
3194
+ if (Array.isArray(v)) return "array";
3195
+ return typeof v;
3196
+ }
3197
+ function joinPath(parent, key) {
3198
+ if (!parent) return key;
3199
+ return `${parent}.${key}`;
3200
+ }
3201
+ function deepEqual(a, b) {
3202
+ if (a === b) return true;
3203
+ if (typeof a !== typeof b) return false;
3204
+ if (a === null || b === null) return a === b;
3205
+ if (Array.isArray(a) && Array.isArray(b)) {
3206
+ return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
3207
+ }
3208
+ if (typeof a === "object" && typeof b === "object") {
3209
+ const ak = Object.keys(a);
3210
+ const bk = Object.keys(b);
3211
+ if (ak.length !== bk.length) return false;
3212
+ return ak.every(
3213
+ (k) => deepEqual(a[k], b[k])
3214
+ );
3215
+ }
3216
+ return false;
3217
+ }
3218
+
2859
3219
  // src/utils/tool-output-serializer.ts
2860
3220
  function createToolOutputSerializer(opts = {}) {
2861
3221
  const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
@@ -2937,18 +3297,45 @@ var ToolExecutor = class {
2937
3297
  budget = this.decrementBudget(result, budget);
2938
3298
  return { result, tool, durationMs: Date.now() - start };
2939
3299
  }
3300
+ const validation = validateAgainstSchema(use.input, tool.inputSchema);
3301
+ if (!validation.ok) {
3302
+ const errorDetails = validation.errors.map((e) => ` - ${e.path || "input"}: ${e.message}`).join("\n");
3303
+ const result = {
3304
+ type: "tool_result",
3305
+ tool_use_id: use.id,
3306
+ content: `Invalid arguments for tool "${tool.name}".
3307
+
3308
+ Validation errors:
3309
+ ${errorDetails}
3310
+
3311
+ Please call the tool again with arguments that match its inputSchema. You can use the "tool-help" tool with name="${tool.name}" to see the exact expected schema.`,
3312
+ is_error: true
3313
+ };
3314
+ budget = this.decrementBudget(result, budget);
3315
+ return { result, tool, durationMs: Date.now() - start };
3316
+ }
3317
+ const toolDangerousCaps = getDangerousCapabilities(tool);
3318
+ if (toolDangerousCaps.length > 0) {
3319
+ if (this.opts.events) ;
3320
+ }
2940
3321
  if (hasMalformedArguments(use.input)) {
2941
3322
  const result = this.malformedInputResult(use, extractMalformedRaw(use.input));
2942
3323
  budget = this.decrementBudget(result, budget);
2943
3324
  return { result, tool, durationMs: Date.now() - start };
2944
3325
  }
2945
3326
  const decision = await this.opts.permissionPolicy.evaluate(tool, use.input, ctx);
2946
- if (decision.permission === "deny") {
3327
+ let effectivePermission = decision.permission;
3328
+ const policy = this.opts.permissionPolicy;
3329
+ const yolo = policy.getYolo?.() === true || policy.getForceAllYolo?.() === true;
3330
+ if (toolDangerousCaps.length > 0 && effectivePermission === "auto" && !yolo) {
3331
+ effectivePermission = "confirm";
3332
+ }
3333
+ if (effectivePermission === "deny") {
2947
3334
  const result = this.deniedResult(use, decision.reason);
2948
3335
  budget = this.decrementBudget(result, budget);
2949
3336
  return { result, tool, durationMs: Date.now() - start };
2950
3337
  }
2951
- if (decision.permission === "confirm") {
3338
+ if (effectivePermission === "confirm") {
2952
3339
  if (this.opts.confirmAwaiter) {
2953
3340
  const choice = await this.opts.confirmAwaiter(tool, use.input, use.id, tool.name);
2954
3341
  if (choice !== "yes" && choice !== "always") {
@@ -2973,10 +3360,13 @@ var ToolExecutor = class {
2973
3360
  return { result: pending, tool, durationMs: Date.now() - start };
2974
3361
  }
2975
3362
  }
3363
+ const toolCapsForAudit = hasDangerousCapabilityForSubagents(tool) ? tool.capabilities ?? [] : [];
2976
3364
  const span = this.opts.tracer?.startSpan(`tool.${tool.name}`, {
2977
3365
  "tool.name": tool.name,
2978
3366
  "tool.mutating": tool.mutating,
2979
- "tool.permission": tool.permission
3367
+ "tool.permission": tool.permission,
3368
+ "tool.capabilities": JSON.stringify(tool.capabilities ?? []),
3369
+ "tool.has_dangerous_capabilities": toolCapsForAudit.length > 0
2980
3370
  });
2981
3371
  try {
2982
3372
  const result = await this.executeTool(tool, use, ctx, budget);
@@ -3919,9 +4309,11 @@ function resolveWstackPaths(opts) {
3919
4309
  globalPrompts: path6.join(globalRoot, "prompts"),
3920
4310
  cacheDir: path6.join(globalRoot, "cache"),
3921
4311
  modelsCache: path6.join(globalRoot, "cache", "models.dev.json"),
4312
+ modelsOverlayCache: path6.join(globalRoot, "cache", "models-overlay.json"),
3922
4313
  historyFile: path6.join(globalRoot, "history"),
3923
4314
  logFile: path6.join(globalRoot, "logs", "wrongstack.log"),
3924
4315
  projectDir,
4316
+ projectCodebaseIndex: path6.join(projectDir, "codebase-index"),
3925
4317
  projectMemory: path6.join(projectDir, "memory.md"),
3926
4318
  projectSessions: path6.join(projectDir, "sessions"),
3927
4319
  projectTrust: path6.join(projectDir, "trust.json"),
@@ -4030,175 +4422,86 @@ function buildChildEnv(optsOrSessionId) {
4030
4422
  return out;
4031
4423
  }
4032
4424
 
4033
- // src/utils/json-schema-validate.ts
4034
- function validateAgainstSchema(value, schema) {
4035
- const errors = [];
4036
- walk2(value, schema, "", errors);
4037
- return { ok: errors.length === 0, errors };
4038
- }
4039
- function walk2(value, schema, path31, errors) {
4040
- if (schema.enum !== void 0) {
4041
- if (!schema.enum.some((e) => deepEqual(e, value))) {
4042
- errors.push({
4043
- path: path31 || "<root>",
4044
- message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
4045
- });
4046
- return;
4425
+ // src/utils/json-repair.ts
4426
+ function completePartialObject(s) {
4427
+ if (!s.trim().startsWith("{")) return s;
4428
+ if (tryParse(s).ok) return s;
4429
+ const stack = [];
4430
+ let inString = false;
4431
+ let escaped = false;
4432
+ let sawKey = false;
4433
+ let prevSig = "";
4434
+ let contentEnd = 0;
4435
+ let stringBraceDepth = 0;
4436
+ for (let i = 0; i < s.length; i++) {
4437
+ const ch = s[i];
4438
+ if (inString) {
4439
+ contentEnd = i + 1;
4440
+ if (escaped) {
4441
+ escaped = false;
4442
+ continue;
4443
+ }
4444
+ if (ch === "\\") {
4445
+ escaped = true;
4446
+ continue;
4447
+ }
4448
+ if (ch === '"') {
4449
+ inString = false;
4450
+ prevSig = '"';
4451
+ stringBraceDepth = 0;
4452
+ continue;
4453
+ }
4454
+ if (ch === "{") stringBraceDepth++;
4455
+ else if (ch === "}" && stringBraceDepth > 0) stringBraceDepth--;
4456
+ continue;
4047
4457
  }
4048
- }
4049
- if (typeof schema.type === "string") {
4050
- if (!checkType(value, schema.type)) {
4051
- errors.push({
4052
- path: path31 || "<root>",
4053
- message: `expected ${schema.type}, got ${describeType(value)}`
4054
- });
4055
- return;
4458
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") continue;
4459
+ contentEnd = i + 1;
4460
+ if (ch === '"') {
4461
+ inString = true;
4462
+ sawKey = true;
4463
+ stringBraceDepth = 0;
4464
+ prevSig = '"';
4465
+ } else if (ch === "{" || ch === "[") {
4466
+ stack.push(ch);
4467
+ prevSig = ch;
4468
+ } else if (ch === "}" || ch === "]") {
4469
+ stack.pop();
4470
+ prevSig = ch;
4471
+ } else {
4472
+ prevSig = ch;
4056
4473
  }
4057
4474
  }
4058
- if (schema.type === "object" && isPlainObject(value)) {
4059
- const obj = value;
4060
- for (const req of schema.required ?? []) {
4061
- if (!(req in obj)) {
4062
- errors.push({ path: joinPath(path31, req), message: "required property missing" });
4063
- }
4064
- }
4065
- if (schema.properties) {
4066
- for (const [key, subSchema] of Object.entries(schema.properties)) {
4067
- if (key in obj) {
4068
- walk2(obj[key], subSchema, joinPath(path31, key), errors);
4069
- }
4070
- }
4475
+ if (!sawKey && !inString) return s;
4476
+ let result = s.slice(0, contentEnd);
4477
+ if (inString) {
4478
+ if (escaped) {
4479
+ result = result.slice(0, -1);
4480
+ } else if (endsWithInvalidEscape(result)) {
4481
+ result = result.slice(0, -2);
4071
4482
  }
4483
+ if (stringBraceDepth > 0) result += "}".repeat(stringBraceDepth);
4484
+ result += '"';
4485
+ } else if (prevSig === ":") {
4486
+ result += "null";
4072
4487
  }
4073
- if (schema.type === "array" && Array.isArray(value) && schema.items) {
4074
- value.forEach((item, i) => walk2(item, schema.items, `${path31}[${i}]`, errors));
4488
+ for (let k = stack.length - 1; k >= 0; k--) {
4489
+ result += stack[k] === "{" ? "}" : "]";
4075
4490
  }
4076
- }
4077
- function checkType(value, type) {
4078
- switch (type) {
4079
- case "string":
4080
- return typeof value === "string";
4081
- case "number":
4082
- return typeof value === "number" && !Number.isNaN(value);
4083
- case "integer":
4084
- return typeof value === "number" && Number.isInteger(value);
4085
- case "boolean":
4086
- return typeof value === "boolean";
4087
- case "null":
4088
- return value === null;
4089
- case "array":
4090
- return Array.isArray(value);
4091
- case "object":
4092
- return isPlainObject(value);
4093
- default:
4094
- return true;
4491
+ if (!tryParse(result).ok) {
4492
+ const patched = result.replace(/:(\s*)([}\]])/g, ":null$2");
4493
+ if (tryParse(patched).ok) result = patched;
4095
4494
  }
4495
+ return result;
4096
4496
  }
4097
- function isPlainObject(v) {
4098
- return typeof v === "object" && v !== null && !Array.isArray(v);
4099
- }
4100
- function describeType(v) {
4101
- if (v === null) return "null";
4102
- if (Array.isArray(v)) return "array";
4103
- return typeof v;
4104
- }
4105
- function joinPath(parent, key) {
4106
- if (!parent) return key;
4107
- return `${parent}.${key}`;
4108
- }
4109
- function deepEqual(a, b) {
4110
- if (a === b) return true;
4111
- if (typeof a !== typeof b) return false;
4112
- if (a === null || b === null) return a === b;
4113
- if (Array.isArray(a) && Array.isArray(b)) {
4114
- return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
4115
- }
4116
- if (typeof a === "object" && typeof b === "object") {
4117
- const ak = Object.keys(a);
4118
- const bk = Object.keys(b);
4119
- if (ak.length !== bk.length) return false;
4120
- return ak.every(
4121
- (k) => deepEqual(a[k], b[k])
4122
- );
4123
- }
4124
- return false;
4125
- }
4126
-
4127
- // src/utils/json-repair.ts
4128
- function completePartialObject(s) {
4129
- let result = s;
4130
- const trimmed = result.trim();
4131
- if (!trimmed.startsWith("{")) return s;
4132
- for (let pass = 0; pass < 3; pass++) {
4133
- let braceDepth = 0;
4134
- let inString2 = false;
4135
- let escaped2 = false;
4136
- let foundClose = false;
4137
- for (const ch of result) {
4138
- if (escaped2) {
4139
- escaped2 = false;
4140
- continue;
4141
- }
4142
- if (ch === "\\") {
4143
- escaped2 = true;
4144
- continue;
4145
- }
4146
- if (ch === '"') {
4147
- inString2 = !inString2;
4148
- continue;
4149
- }
4150
- if (inString2) continue;
4151
- if (ch === "{") {
4152
- braceDepth++;
4153
- foundClose = false;
4154
- } else if (ch === "}") {
4155
- braceDepth--;
4156
- if (braceDepth === 0) foundClose = true;
4157
- }
4158
- }
4159
- if (foundClose || braceDepth <= 0) break;
4160
- result += "}".repeat(braceDepth);
4161
- }
4162
- if (tryParse(result).ok) return result;
4163
- let inString = false;
4164
- let escaped = false;
4165
- for (let i = result.length - 1; i >= 0; i--) {
4166
- const ch = result[i];
4167
- if (escaped) {
4168
- escaped = false;
4169
- continue;
4170
- }
4171
- if (ch === "\\") {
4172
- escaped = true;
4173
- continue;
4174
- }
4175
- if (ch === '"') {
4176
- let nextNonWs;
4177
- for (let j = i + 1; j < result.length; j++) {
4178
- const nc = result[j];
4179
- if (nc === " " || nc === " " || nc === "\n" || nc === "\r") continue;
4180
- nextNonWs = result[j];
4181
- break;
4182
- }
4183
- if (nextNonWs === ":") {
4184
- continue;
4185
- } else {
4186
- inString = !inString;
4187
- continue;
4188
- }
4189
- }
4190
- }
4191
- if (inString) {
4192
- result = result.trimEnd();
4193
- if (result.endsWith("\\")) result = result.slice(0, -1);
4194
- let depth = 0;
4195
- for (const ch of result) {
4196
- if (ch === "{") depth++;
4197
- else if (ch === "}") depth = Math.max(0, depth - 1);
4198
- }
4199
- result += '"' + "}".repeat(Math.max(1, depth));
4200
- }
4201
- return result;
4497
+ var VALID_ESCAPE = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
4498
+ function endsWithInvalidEscape(str) {
4499
+ const last = str[str.length - 1];
4500
+ if (str[str.length - 2] !== "\\" || last === void 0) return false;
4501
+ if (VALID_ESCAPE.has(last)) return false;
4502
+ let backslashes = 0;
4503
+ for (let k = str.length - 2; k >= 0 && str[k] === "\\"; k--) backslashes++;
4504
+ return backslashes % 2 === 1;
4202
4505
  }
4203
4506
  function tryParse(s) {
4204
4507
  try {
@@ -4340,8 +4643,8 @@ var DefaultSessionStore = class {
4340
4643
  return JSON.parse(raw);
4341
4644
  } catch {
4342
4645
  const full = this.sessionPath(id, ".jsonl");
4343
- const stat9 = await fsp2.stat(full);
4344
- const summary = await this.summarize(id, stat9.mtime.toISOString());
4646
+ const stat8 = await fsp2.stat(full);
4647
+ const summary = await this.summarize(id, stat8.mtime.toISOString());
4345
4648
  await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
4346
4649
  console.warn(
4347
4650
  `[session-store] Failed to write manifest for "${id}":`,
@@ -4534,7 +4837,6 @@ var FileSessionWriter = class {
4534
4837
  }
4535
4838
  return event;
4536
4839
  }
4537
- promptIndex = 0;
4538
4840
  pendingFileSnapshots = [];
4539
4841
  /** Tracks open tool_use IDs during the current run to serialize on close for resume. */
4540
4842
  openToolUses = /* @__PURE__ */ new Set();
@@ -4625,7 +4927,6 @@ var FileSessionWriter = class {
4625
4927
  await this.writeFileSnapshot(promptIndex, [...this.pendingFileSnapshots]);
4626
4928
  this.pendingFileSnapshots = [];
4627
4929
  }
4628
- this.promptIndex = promptIndex + 1;
4629
4930
  await this.append({
4630
4931
  type: "checkpoint",
4631
4932
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -4721,6 +5022,38 @@ var FileSessionWriter = class {
4721
5022
  `;
4722
5023
  await fsp2.writeFile(this.filePath, record, "utf8");
4723
5024
  }
5025
+ /**
5026
+ * Idea #1 — write an in-flight marker. The agent loop should call
5027
+ * this at the start of each long-running operation; a matching
5028
+ * `clearInFlightMarker` follows on clean exit. A stale marker
5029
+ * (no end) is what `SessionRecovery.detectStale` looks for.
5030
+ */
5031
+ async writeInFlightMarker(context) {
5032
+ if (!context || context.length > 500) {
5033
+ throw new Error("In-flight context must be 1..500 chars");
5034
+ }
5035
+ await this.append({
5036
+ type: "in_flight_start",
5037
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5038
+ context
5039
+ });
5040
+ this.events?.emit("in_flight.started", { context, ts: (/* @__PURE__ */ new Date()).toISOString() });
5041
+ }
5042
+ /**
5043
+ * Idea #1 — close the in-flight marker. Idempotent in spirit
5044
+ * (you can call it after a successful iteration even if you
5045
+ * didn't open one this round) — but the session log records
5046
+ * every call so postmortem tooling can see "the agent finished
5047
+ * cleanly X times, then died without finishing Y".
5048
+ */
5049
+ async clearInFlightMarker(reason) {
5050
+ await this.append({
5051
+ type: "in_flight_end",
5052
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5053
+ reason
5054
+ });
5055
+ this.events?.emit("in_flight.ended", { reason, ts: (/* @__PURE__ */ new Date()).toISOString() });
5056
+ }
4724
5057
  };
4725
5058
  function userInputTitle(content) {
4726
5059
  const text = typeof content === "string" ? content : content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
@@ -4783,7 +5116,7 @@ function isPersistedQueueItem(v) {
4783
5116
  // src/storage/attachment-store.ts
4784
5117
  init_atomic_write();
4785
5118
  var DEFAULT_SPOOL_THRESHOLD = 256 * 1024;
4786
- var PLACEHOLDER_RE = /\[(pasted|image|file) #(\d+)\]/g;
5119
+ var PLACEHOLDER_RE = /\[(pasted|image|file) #(\d+)[^\]]*\]|\[file:([^\]]+)\]/g;
4787
5120
  var DefaultAttachmentStore = class {
4788
5121
  items = /* @__PURE__ */ new Map();
4789
5122
  refs = [];
@@ -4837,9 +5170,15 @@ var DefaultAttachmentStore = class {
4837
5170
  const idx = m.index ?? 0;
4838
5171
  const before = text.slice(lastIndex, idx);
4839
5172
  if (before) blocks.push({ type: "text", text: before });
4840
- const kind = prefixToKind(m[1]);
4841
- const seq = Number(m[2]);
4842
- const ref = this.refs.find((r) => r.kind === kind && r.seq === seq);
5173
+ let ref;
5174
+ if (m[3] !== void 0) {
5175
+ const wantPath = m[3];
5176
+ ref = findLast(this.refs, (r) => r.kind === "file" && refPath(r) === wantPath);
5177
+ } else {
5178
+ const kind = prefixToKind(m[1]);
5179
+ const seq = Number(m[2]);
5180
+ ref = this.refs.find((r) => r.kind === kind && r.seq === seq);
5181
+ }
4843
5182
  const att = ref ? this.items.get(ref.id) : void 0;
4844
5183
  if (!att) {
4845
5184
  blocks.push({ type: "text", text: m[0] });
@@ -4892,6 +5231,15 @@ function prefixToKind(prefix) {
4892
5231
  if (prefix === "image") return "image";
4893
5232
  return "file";
4894
5233
  }
5234
+ function refPath(ref) {
5235
+ return ref.meta.filename ?? ref.meta.label;
5236
+ }
5237
+ function findLast(arr, pred) {
5238
+ for (let i = arr.length - 1; i >= 0; i--) {
5239
+ if (pred(arr[i])) return arr[i];
5240
+ }
5241
+ return void 0;
5242
+ }
4895
5243
  function mergeAdjacentText(blocks) {
4896
5244
  const out = [];
4897
5245
  for (const b of blocks) {
@@ -5219,7 +5567,8 @@ var BEHAVIOR_DEFAULTS = {
5219
5567
  memory: true,
5220
5568
  modelsRegistry: true,
5221
5569
  skills: true
5222
- }
5570
+ },
5571
+ session: { ...DEFAULT_SESSION_LOGGING_CONFIG }
5223
5572
  };
5224
5573
  var ENV_MAP = {
5225
5574
  WRONGSTACK_PROVIDER: (c, v) => {
@@ -5727,6 +6076,101 @@ var SessionAnalyzer = class {
5727
6076
  }
5728
6077
  };
5729
6078
 
6079
+ // src/storage/session-event-bridge.ts
6080
+ var CORE_RECONSTRUCT_EVENTS = /* @__PURE__ */ new Set([
6081
+ "session_start",
6082
+ "session_resumed",
6083
+ "user_input",
6084
+ "llm_response",
6085
+ "tool_result",
6086
+ "checkpoint",
6087
+ "file_snapshot",
6088
+ "rewound",
6089
+ "in_flight_start",
6090
+ "in_flight_end",
6091
+ "session_end"
6092
+ ]);
6093
+ var STANDARD_AUDIT_EVENTS = /* @__PURE__ */ new Set([
6094
+ "llm_request",
6095
+ "tool_use",
6096
+ "tool_call_start",
6097
+ "tool_call_end",
6098
+ "compaction",
6099
+ "error",
6100
+ "message_truncated",
6101
+ "provider_retry",
6102
+ "provider_error"
6103
+ ]);
6104
+ var FULL_ONLY_EVENTS = /* @__PURE__ */ new Set([
6105
+ "tool_progress"
6106
+ ]);
6107
+ function isAllowed(type, level) {
6108
+ if (CORE_RECONSTRUCT_EVENTS.has(type)) return true;
6109
+ if (level === "minimal") return false;
6110
+ if (STANDARD_AUDIT_EVENTS.has(type)) return true;
6111
+ if (level === "standard") return false;
6112
+ if (FULL_ONLY_EVENTS.has(type)) {
6113
+ return level === "full";
6114
+ }
6115
+ return true;
6116
+ }
6117
+ function createSessionEventBridge(writer, level = "standard", options = {}) {
6118
+ const normalizedLevel = level ?? "standard";
6119
+ const progressCounters = /* @__PURE__ */ new Map();
6120
+ const toolProgressConfig = options.sampling?.toolProgress ?? {};
6121
+ const TOOL_PROGRESS_SAMPLE_RATE = toolProgressConfig.sampleRate ?? 8;
6122
+ function shouldSample(event) {
6123
+ if (event.type !== "tool_progress") return true;
6124
+ const progEvent = event;
6125
+ const innerType = progEvent.event?.type;
6126
+ if (innerType === "warning" || innerType === "metric" || innerType === "file_changed") {
6127
+ return true;
6128
+ }
6129
+ if (innerType === "log" || innerType === "partial_output") {
6130
+ const key = progEvent.id || progEvent.name;
6131
+ const count = (progressCounters.get(key) || 0) + 1;
6132
+ progressCounters.set(key, count);
6133
+ return count === 1 || count % TOOL_PROGRESS_SAMPLE_RATE === 0;
6134
+ }
6135
+ return true;
6136
+ }
6137
+ return {
6138
+ level: normalizedLevel,
6139
+ allows(type) {
6140
+ return isAllowed(type, normalizedLevel);
6141
+ },
6142
+ async append(event) {
6143
+ if (!writer) return;
6144
+ if (!isAllowed(event.type, normalizedLevel)) return;
6145
+ if (!shouldSample(event)) return;
6146
+ try {
6147
+ await writer.append(event);
6148
+ } catch (err) {
6149
+ }
6150
+ }
6151
+ };
6152
+ }
6153
+ function resolveAuditLevel(cfg) {
6154
+ const raw = cfg?.session?.auditLevel;
6155
+ if (raw === "minimal" || raw === "standard" || raw === "full") {
6156
+ return raw;
6157
+ }
6158
+ return "standard";
6159
+ }
6160
+ function resolveSessionLoggingConfig(cfg) {
6161
+ const session = cfg?.session ?? {};
6162
+ const auditLevel = resolveAuditLevel(cfg);
6163
+ const toolProgressSampleRate = session.sampling?.toolProgress?.sampleRate ?? 8;
6164
+ return {
6165
+ auditLevel,
6166
+ sampling: {
6167
+ toolProgress: {
6168
+ sampleRate: Math.max(1, Math.floor(toolProgressSampleRate))
6169
+ }
6170
+ }
6171
+ };
6172
+ }
6173
+
5730
6174
  // src/storage/todos-checkpoint.ts
5731
6175
  init_atomic_write();
5732
6176
  async function loadTodosCheckpoint(filePath) {
@@ -6215,8 +6659,6 @@ var DirectorStateCheckpoint = class {
6215
6659
  }
6216
6660
  }
6217
6661
  };
6218
-
6219
- // src/security/permission-policy.ts
6220
6662
  init_atomic_write();
6221
6663
  var DefaultPermissionPolicy = class {
6222
6664
  policy = {};
@@ -6457,31 +6899,21 @@ var DefaultPermissionPolicy = class {
6457
6899
  };
6458
6900
  var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
6459
6901
  /**
6460
- * Tools that are too dangerous to auto-approve even in a delegated
6461
- * subagent context. Subagents run non-interactively under a director
6462
- * and cannot answer prompts, but inherited authorization does not
6463
- * imply blanket permission for destructive or privilege-escalating
6464
- * operations. These tools remain at their declared `permission`
6465
- * level so the leader must explicitly allow them per-spawn.
6902
+ * Legacy name-based denylist.
6903
+ * @deprecated Prefer declaring `capabilities` on the Tool and using capability-based checks.
6466
6904
  */
6467
- static DENY = /* @__PURE__ */ new Set([
6905
+ static LEGACY_NAME_DENY = /* @__PURE__ */ new Set([
6468
6906
  "bash",
6469
- // arbitrary shell — use exec for constrained shell
6470
6907
  "write",
6471
- // arbitrary file write
6472
6908
  "edit",
6473
- // arbitrary in-project file modification (equivalent to write)
6474
6909
  "replace",
6475
- // arbitrary multi-file find/replace (equivalent to write)
6476
6910
  "scaffold",
6477
- // arbitrary file generation outside project root
6478
6911
  "patch",
6479
- // arbitrary diff application
6480
6912
  "install",
6481
- // installs from arbitrary package sources
6482
6913
  "exec"
6483
- // restricted shell but with arbitrary command args
6484
6914
  ]);
6915
+ // Note: hasDangerousCapabilityForSubagents is now the shared helper from capabilities.ts
6916
+ // The old private method was removed in favor of the centralized utility.
6485
6917
  /**
6486
6918
  * Tools from MCP servers (`mcp__<server>__<tool>`) are external code of
6487
6919
  * unknown capability — they may wrap a shell or filesystem. They are
@@ -6492,12 +6924,16 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
6492
6924
  return name.startsWith("mcp__");
6493
6925
  }
6494
6926
  async evaluate(tool) {
6495
- const blocked = _AutoApprovePermissionPolicy.DENY.has(tool.name) || _AutoApprovePermissionPolicy.isMcpTool(tool.name);
6496
- if (tool.permission === "deny" || blocked) {
6927
+ const hasDangerousCap = hasDangerousCapabilityForSubagents(tool);
6928
+ const legacyNameBlock = _AutoApprovePermissionPolicy.LEGACY_NAME_DENY.has(tool.name);
6929
+ const isMcp = _AutoApprovePermissionPolicy.isMcpTool(tool.name);
6930
+ const blocked = tool.permission === "deny" || hasDangerousCap || legacyNameBlock || isMcp;
6931
+ if (blocked) {
6932
+ const reason = hasDangerousCap ? `tool declares dangerous capability (${tool.capabilities?.join(", ")}) \u2014 not auto-approved for subagents` : legacyNameBlock || isMcp ? `tool ${tool.name} is not auto-approved for subagents \u2014 ask the leader to allow it explicitly` : "tool default deny";
6497
6933
  return {
6498
6934
  permission: "deny",
6499
6935
  source: "subagent_guard",
6500
- reason: blocked ? `tool ${tool.name} is not auto-approved for subagents \u2014 ask the leader to allow it explicitly` : "tool default deny"
6936
+ reason
6501
6937
  };
6502
6938
  }
6503
6939
  return { permission: "auto", source: "yolo" };
@@ -6976,8 +7412,9 @@ var IntelligentCompactor = class {
6976
7412
  }
6977
7413
  async compact(ctx, opts = {}) {
6978
7414
  const beforeTokens = this.estimateTokens(ctx.messages);
7415
+ const beforeFull = this.estimateFullRequest(ctx);
6979
7416
  const reductions = [];
6980
- const load = beforeTokens / this.maxContext;
7417
+ const load = beforeFull / this.maxContext;
6981
7418
  const aggressive = load >= this.hardThreshold ? true : opts.aggressive ?? load >= this.softThreshold;
6982
7419
  const saved1 = this.eliseOldToolResults(ctx);
6983
7420
  if (saved1 > 0) reductions.push({ phase: "elision", saved: saved1 });
@@ -6991,9 +7428,12 @@ var IntelligentCompactor = class {
6991
7428
  const repaired = repairToolUseAdjacency(ctx.messages);
6992
7429
  if (repaired.report.changed) ctx.state.replaceMessages(repaired.messages);
6993
7430
  const afterTokens = this.estimateTokens(ctx.messages);
7431
+ const afterFull = this.estimateFullRequest(ctx);
6994
7432
  return {
6995
7433
  before: beforeTokens,
6996
7434
  after: afterTokens,
7435
+ fullRequestTokensBefore: beforeFull,
7436
+ fullRequestTokensAfter: afterFull,
6997
7437
  reductions,
6998
7438
  repaired: repaired.report.changed ? {
6999
7439
  removedToolUses: repaired.report.removedToolUses,
@@ -7002,6 +7442,14 @@ var IntelligentCompactor = class {
7002
7442
  } : void 0
7003
7443
  };
7004
7444
  }
7445
+ /**
7446
+ * Estimate the full API request token count: messages + systemPrompt + toolDefs.
7447
+ * This is the accurate figure for context-window pressure monitoring.
7448
+ */
7449
+ estimateFullRequest(ctx) {
7450
+ const breakdown = estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []);
7451
+ return breakdown.total;
7452
+ }
7005
7453
  async summarizeAncientTurns(ctx) {
7006
7454
  const messages = ctx.messages;
7007
7455
  const cutoff = Math.max(0, messages.length - this.preserveK * 2);
@@ -7014,29 +7462,26 @@ var IntelligentCompactor = class {
7014
7462
  try {
7015
7463
  summaryText = await this.callSummarizer(toSummarize, ctx);
7016
7464
  } catch {
7017
- const toolNames = /* @__PURE__ */ new Set();
7018
- const filePaths = /* @__PURE__ */ new Set();
7019
- let userTurns = 0;
7020
- let assistantTurns = 0;
7465
+ const preservedMessages = [];
7021
7466
  for (const m of toSummarize) {
7022
- if (m.role === "user") userTurns++;
7023
- else if (m.role === "assistant") {
7024
- assistantTurns++;
7025
- if (Array.isArray(m.content)) {
7026
- for (const b of m.content) {
7027
- if (b.type === "tool_use") toolNames.add(b.name ?? "unknown");
7028
- }
7029
- }
7467
+ if (m.role === "system") continue;
7468
+ if (typeof m.content === "string") {
7469
+ preservedMessages.push(m);
7470
+ } else if (Array.isArray(m.content)) {
7471
+ const textParts = m.content.filter(isTextBlock);
7472
+ const toolParts = m.content.filter((b) => b.type === "tool_use" || b.type === "tool_result");
7473
+ const next = {
7474
+ role: m.role,
7475
+ content: [
7476
+ ...textParts,
7477
+ ...toolParts.length > 0 ? [{ type: "text", text: `[${toolParts.length} tool call(s) omitted]` }] : []
7478
+ ]
7479
+ };
7480
+ preservedMessages.push(next);
7030
7481
  }
7031
- const text = typeof m.content === "string" ? m.content : "";
7032
- const matches = text.matchAll(/(?:[\w.,\-/@]+\/)*[\w.,\-/@]+\.\w+/g);
7033
- for (const m_ of matches) filePaths.add(m_[0]);
7034
7482
  }
7035
- const parts = [`${toSummarize.length} turns (${userTurns} user, ${assistantTurns} assistant)`];
7036
- if (toolNames.size > 0) parts.push(`tools: ${[...toolNames].join(", ")}`);
7037
- if (filePaths.size > 0) parts.push(`files: ${[...filePaths].slice(0, 10).join(", ")}`);
7038
- summaryText = parts.join(" | ");
7039
- if (!summaryText) summaryText = `${toSummarize.length} earlier turns omitted`;
7483
+ summaryText = preservedMessages.map((m) => `[${m.role}]: ${typeof m.content === "string" ? m.content : m.content.filter(isTextBlock).map((b) => b.text).join(" ")}`).join("\n");
7484
+ if (!summaryText) summaryText = `${toSummarize.length} earlier turns (semantic content preserved)`;
7040
7485
  }
7041
7486
  const summaryMsg = {
7042
7487
  role: "system",
@@ -7282,7 +7727,7 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
7282
7727
  const res = await this.provider.complete(req, { signal: ac.signal });
7283
7728
  const textBlocks = res.content.filter(isTextBlock);
7284
7729
  raw = textBlocks.map((b) => b.text).join("\n").trim();
7285
- } catch (err) {
7730
+ } catch (_err) {
7286
7731
  return this.fallbackSelect(messages, effectiveBudget);
7287
7732
  }
7288
7733
  return this.parseSelectorOutput(raw, messages.length);
@@ -7374,15 +7819,24 @@ var SelectiveCompactor = class {
7374
7819
  }
7375
7820
  async compact(ctx, opts = {}) {
7376
7821
  const beforeTokens = this.estimateTokens(ctx.messages);
7822
+ const beforeFull = this.estimateFullRequest(ctx);
7377
7823
  const reductions = [];
7378
- const load = beforeTokens / this.maxContext;
7824
+ const load = beforeFull / this.maxContext;
7379
7825
  const shouldCompact = load >= this.warnThreshold || opts.aggressive;
7380
7826
  if (!shouldCompact) {
7381
7827
  const saved = this.eliseOldToolResults(ctx);
7382
7828
  if (saved > 0) reductions.push({ phase: "elision", saved });
7383
7829
  const repair2 = this.repairProtocolAdjacency(ctx);
7384
7830
  const afterTokens2 = this.estimateTokens(ctx.messages);
7385
- return { before: beforeTokens, after: afterTokens2, reductions, repaired: repair2 };
7831
+ const afterFull2 = this.estimateFullRequest(ctx);
7832
+ return {
7833
+ before: beforeTokens,
7834
+ after: afterTokens2,
7835
+ fullRequestTokensBefore: beforeFull,
7836
+ fullRequestTokensAfter: afterFull2,
7837
+ reductions,
7838
+ repaired: repair2
7839
+ };
7386
7840
  }
7387
7841
  const savedElision = this.eliseOldToolResults(ctx);
7388
7842
  if (savedElision > 0) reductions.push({ phase: "elision", saved: savedElision });
@@ -7394,7 +7848,23 @@ var SelectiveCompactor = class {
7394
7848
  }
7395
7849
  const repair = this.repairProtocolAdjacency(ctx);
7396
7850
  const afterTokens = this.estimateTokens(ctx.messages);
7397
- return { before: beforeTokens, after: afterTokens, reductions, repaired: repair };
7851
+ const afterFull = this.estimateFullRequest(ctx);
7852
+ return {
7853
+ before: beforeTokens,
7854
+ after: afterTokens,
7855
+ fullRequestTokensBefore: beforeFull,
7856
+ fullRequestTokensAfter: afterFull,
7857
+ reductions,
7858
+ repaired: repair
7859
+ };
7860
+ }
7861
+ /**
7862
+ * Estimate the full API request token count: messages + systemPrompt + toolDefs.
7863
+ * This is the accurate figure for context-window pressure monitoring.
7864
+ */
7865
+ estimateFullRequest(ctx) {
7866
+ const breakdown = estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []);
7867
+ return breakdown.total;
7398
7868
  }
7399
7869
  repairProtocolAdjacency(ctx) {
7400
7870
  const repaired = repairToolUseAdjacency(ctx.messages);
@@ -7584,7 +8054,8 @@ var LEVEL_RANK2 = { warn: 0, soft: 1, hard: 2 };
7584
8054
  var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7585
8055
  name = "AutoCompaction";
7586
8056
  compactor;
7587
- estimator;
8057
+ /** Deprecated. Kept for backward compat with tests that pass simpleEstimator. */
8058
+ _estimator;
7588
8059
  warnThreshold;
7589
8060
  softThreshold;
7590
8061
  hardThreshold;
@@ -7594,19 +8065,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7594
8065
  events;
7595
8066
  failureMode;
7596
8067
  policyProvider;
7597
- /**
7598
- * Calibration factor applied to the estimator output. The estimator is now
7599
- * expected to already include messages + system prompt + tool definitions
7600
- * (see `estimateRequestTokens.total`), so no overhead boost is needed. Kept
7601
- * as a constant so a future calibration against real provider tokenization
7602
- * (BPE vs the chars/3.5 rough estimator) can dial this without touching the
7603
- * threshold-check math.
7604
- *
7605
- * Historical note: was 1.3 when the estimator only counted messages — that
7606
- * double-counted system+tools once the estimator was upgraded, firing
7607
- * compaction ~30% earlier than intended (e.g. real 56% load showed as 73%).
7608
- */
7609
- static OVERHEAD_FACTOR = 1;
8068
+ sessionBridge;
7610
8069
  /**
7611
8070
  * Once a compaction attempt reduces nothing (preserveK protects everything,
7612
8071
  * no oversized tool_results remain to elide), retrying on every iteration
@@ -7619,8 +8078,11 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7619
8078
  lastNoopAttempt = null;
7620
8079
  /**
7621
8080
  * @param compactor Compactor to use for compaction.
7622
- * @param maxContext Provider's max context window in tokens.
7623
- * @param estimator Token estimation function.
8081
+ * @param maxContext Provider's max context window in tokens.
8082
+ * @param _estimator Deprecated parameter kept for backward compatibility.
8083
+ * The middleware now uses `estimateRequestTokens` internally
8084
+ * for accurate full-request token counting (messages +
8085
+ * systemPrompt + toolDefs).
7624
8086
  * @param thresholds Threshold fractions (0-1) of maxContext.
7625
8087
  * @param opts Optional behavior. By default, failures at the
7626
8088
  * hard threshold throw AGENT_CONTEXT_OVERFLOW so
@@ -7628,11 +8090,11 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7628
8090
  * provider context overflow. Warn/soft failures
7629
8091
  * still emit compaction.failed and continue.
7630
8092
  */
7631
- constructor(compactor, maxContext, estimator, thresholds, optsOrAggressiveOn = {}, events) {
8093
+ constructor(compactor, maxContext, _estimator, thresholds, optsOrAggressiveOn = {}, events) {
7632
8094
  const opts = typeof optsOrAggressiveOn === "string" ? { aggressiveOn: optsOrAggressiveOn, events } : optsOrAggressiveOn;
7633
8095
  this.compactor = compactor;
7634
8096
  this._maxContext = maxContext;
7635
- this.estimator = estimator;
8097
+ this._estimator = _estimator;
7636
8098
  this.warnThreshold = thresholds.warn;
7637
8099
  this.softThreshold = thresholds.soft;
7638
8100
  this.hardThreshold = thresholds.hard;
@@ -7640,6 +8102,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7640
8102
  this.events = opts.events;
7641
8103
  this.failureMode = opts.failureMode ?? "throw_on_hard";
7642
8104
  this.policyProvider = opts.policyProvider;
8105
+ this.sessionBridge = opts.sessionBridge;
7643
8106
  }
7644
8107
  /** Allow callers (e.g. model-switch in WebUI) to update the context window
7645
8108
  * denominator when the active model changes. */
@@ -7648,8 +8111,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7648
8111
  }
7649
8112
  handler() {
7650
8113
  return async (ctx, next) => {
7651
- const rawTokens = this.estimator(ctx);
7652
- const tokens = Math.ceil(rawTokens * _AutoCompactionMiddleware.OVERHEAD_FACTOR);
8114
+ const tokens = this._estimator ? this._estimator(ctx) : estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []).total;
7653
8115
  const load = tokens / this._maxContext;
7654
8116
  const policy = this.policyProvider?.(ctx);
7655
8117
  const thresholds = policy?.thresholds ?? {
@@ -7684,7 +8146,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7684
8146
  return delta < _AutoCompactionMiddleware.NOOP_RETRY_DELTA_TOKENS;
7685
8147
  }
7686
8148
  recordAttempt(level, tokens, report) {
7687
- const reduced = report.before > report.after;
8149
+ const reduced = (report.fullRequestTokensBefore ?? report.before) > (report.fullRequestTokensAfter ?? report.after);
7688
8150
  const repaired = !!report.repaired;
7689
8151
  if (reduced || repaired) {
7690
8152
  this.lastNoopAttempt = null;
@@ -7704,6 +8166,15 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7704
8166
  report,
7705
8167
  aggressive
7706
8168
  });
8169
+ await this.sessionBridge?.append({
8170
+ type: "compaction",
8171
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
8172
+ before: report.before,
8173
+ after: report.after,
8174
+ level: pressure.level,
8175
+ aggressive,
8176
+ reductions: report.reductions?.map((r) => ({ phase: r.phase, saved: r.saved }))
8177
+ });
7707
8178
  ctx.clearFileTracking();
7708
8179
  } catch (err) {
7709
8180
  const error = err instanceof Error ? err : new Error(String(err));
@@ -8615,19 +9086,16 @@ var SubagentBudget = class _SubagentBudget {
8615
9086
  tokenOutput = 0;
8616
9087
  costUsd = 0;
8617
9088
  startTime = null;
8618
- _onThreshold;
8619
9089
  /**
8620
- * Tracks which budget kinds currently have an extension request
8621
- * in flight. While a kind is here, further `checkLimit` calls for the
8622
- * same kind are no-ops without this dedup, every `recordIteration`
8623
- * after the limit is reached spawns a fresh decision Promise (until
8624
- * the first one lands and patches limits), flooding the FleetBus
8625
- * with redundant threshold events. Cleared in `negotiateExtension`'s
8626
- * `finally`.
9090
+ * Timestamp of the most recent activity (iteration / tool call / token
9091
+ * usage / streamed progress). Drives the idle timeout reset by
9092
+ * `markActivity()`. Initialised to `start()` time so a never-active agent
9093
+ * still eventually trips its idle window.
8627
9094
  */
8628
- pendingExtensions = /* @__PURE__ */ new Set();
9095
+ lastActivityTime = null;
9096
+ _onThreshold;
8629
9097
  /**
8630
- * Hard cap on how long `negotiateExtension` waits for the coordinator to
9098
+ * Hard cap on how long `_negotiateExtension` waits for the coordinator to
8631
9099
  * respond before defaulting to 'stop'. Without this fallback an absent
8632
9100
  * or hung listener (Director not built / event filter detached mid-run)
8633
9101
  * leaves the budget over-limit and never enforces anything.
@@ -8668,6 +9136,24 @@ var SubagentBudget = class _SubagentBudget {
8668
9136
  }
8669
9137
  start() {
8670
9138
  this.startTime = Date.now();
9139
+ this.lastActivityTime = this.startTime;
9140
+ }
9141
+ /**
9142
+ * Reset the idle clock. Called on any sign of forward progress —
9143
+ * iterations, tool calls, token usage, and streamed tool/text progress —
9144
+ * so a long-but-productive subagent never trips its `idleTimeoutMs`.
9145
+ */
9146
+ markActivity() {
9147
+ this.lastActivityTime = Date.now();
9148
+ }
9149
+ /**
9150
+ * Milliseconds since the last activity signal. Returns 0 before `start()`
9151
+ * (nothing to measure yet). Used by the coordinator watchdog to decide
9152
+ * whether to re-arm (still active) or reap (genuinely idle).
9153
+ */
9154
+ idleMs() {
9155
+ const since = this.lastActivityTime ?? this.startTime;
9156
+ return since === null ? 0 : Date.now() - since;
8671
9157
  }
8672
9158
  /** Returns true if we're within 10% of any limit — useful for pre-flight checks. */
8673
9159
  isNearLimit() {
@@ -8689,38 +9175,88 @@ var SubagentBudget = class _SubagentBudget {
8689
9175
  * - `mode === 'auto'` + no listener → throw `BudgetExceededError` (hard stop; no one to ask)
8690
9176
  * - `mode === 'auto'` + listener → throw `BudgetThresholdSignal` with async decision promise
8691
9177
  */
8692
- checkLimit(kind, used, limit) {
9178
+ /**
9179
+ * Collects all exceeded budget kinds into a single NOOP-free negotiation.
9180
+ * Called by recordIteration / recordToolCall / recordUsage — each may call
9181
+ * this for its own kind. The first call starts the negotiation and stores
9182
+ * the Promise in _pendingNegotiation. Subsequent calls for DIFFERENT
9183
+ * kinds (while a negotiation is in flight) are NOOPs — they don't start
9184
+ * new conversations with the coordinator. This prevents an EventBus flood
9185
+ * when multiple budget kinds are exceeded simultaneously in one iteration.
9186
+ *
9187
+ * Returns the kinds that were found to be exceeded (for logging/debugging).
9188
+ */
9189
+ checkLimits(elapsedMs) {
9190
+ const exceeded = [];
9191
+ if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
9192
+ exceeded.push({ kind: "iterations", used: this.iterations, limit: this.limits.maxIterations });
9193
+ }
9194
+ if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
9195
+ exceeded.push({ kind: "tool_calls", used: this.toolCalls, limit: this.limits.maxToolCalls });
9196
+ }
9197
+ const totalTokens = this.tokenInput + this.tokenOutput;
9198
+ if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
9199
+ exceeded.push({ kind: "tokens", used: totalTokens, limit: this.limits.maxTokens });
9200
+ }
9201
+ if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
9202
+ exceeded.push({ kind: "cost", used: this.costUsd, limit: this.limits.maxCostUsd });
9203
+ }
9204
+ if (elapsedMs !== void 0) {
9205
+ const idle = this.idleMs();
9206
+ if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
9207
+ exceeded.push({ kind: "timeout", used: idle, limit: this.limits.idleTimeoutMs });
9208
+ } else if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs) {
9209
+ exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
9210
+ }
9211
+ }
9212
+ if (exceeded.length === 0) return [];
8693
9213
  if (!this._onThreshold) {
8694
- throw new BudgetExceededError(kind, limit, used);
9214
+ const first2 = exceeded[0];
9215
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
8695
9216
  }
8696
9217
  if (this._mode === "sync") {
8697
- throw new BudgetExceededError(kind, limit, used);
9218
+ const first2 = exceeded[0];
9219
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
8698
9220
  }
8699
9221
  const bus = this._events;
8700
9222
  if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
8701
- throw new BudgetExceededError(kind, limit, used);
9223
+ const first2 = exceeded[0];
9224
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
8702
9225
  }
8703
- if (this.pendingExtensions.has(kind)) return;
8704
- this.pendingExtensions.add(kind);
8705
- const decision = this.negotiateExtension(kind, used, limit);
8706
- throw new BudgetThresholdSignal(kind, limit, used, decision);
9226
+ for (const entry of exceeded) {
9227
+ if (this._pendingNegotiations.has(entry.kind)) continue;
9228
+ const decision2 = this._negotiateExtension(entry.kind, exceeded);
9229
+ this._pendingNegotiations.set(entry.kind, decision2);
9230
+ }
9231
+ const first = exceeded[0];
9232
+ const decision = this._pendingNegotiations.get(first.kind);
9233
+ throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
8707
9234
  }
9235
+ /**
9236
+ * Per-kind in-flight negotiation Promises. Each budget kind can have its
9237
+ * own concurrent negotiation — e.g. iterations and timeout can both
9238
+ * be exceeded simultaneously without blocking each other. The same kind
9239
+ * cannot start two concurrent negotiations (dedup guard).
9240
+ * Cleared in `_negotiateExtension`'s `finally` block.
9241
+ */
9242
+ _pendingNegotiations = /* @__PURE__ */ new Map();
8708
9243
  /**
8709
9244
  * Drive the threshold handler to a decision. Resolves with `'stop'`
8710
9245
  * (signal the runner to abort) or `{ extend: ... }` (limits already
8711
- * patched in-place; the runner should not abort). Always releases the
8712
- * `pendingExtensions` slot in `finally`.
9246
+ * patched in-place; the runner should not abort). Clears the
9247
+ * per-kind slot in `_pendingNegotiations` in `finally`.
8713
9248
  *
8714
9249
  * The 'continue' return from a sync handler is treated as
8715
9250
  * `{ extend: {} }` — keep going without patching; next overrun fires
8716
9251
  * a fresh signal.
8717
9252
  */
8718
- async negotiateExtension(kind, used, limit) {
9253
+ async _negotiateExtension(kind, exceeded) {
8719
9254
  try {
9255
+ const first = exceeded[0];
8720
9256
  const result = this._onThreshold({
8721
- kind,
8722
- used,
8723
- limit,
9257
+ kind: first.kind,
9258
+ used: first.used,
9259
+ limit: first.limit,
8724
9260
  requestDecision: () => {
8725
9261
  const bus = this._events;
8726
9262
  if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
@@ -8737,20 +9273,22 @@ var SubagentBudget = class _SubagentBudget {
8737
9273
  () => respond("stop"),
8738
9274
  _SubagentBudget.DECISION_TIMEOUT_MS
8739
9275
  );
8740
- bus.emit("budget.threshold_reached", {
8741
- kind,
8742
- used,
8743
- limit,
8744
- timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
8745
- extend: (extra) => {
8746
- clearTimeout(fallback);
8747
- respond({ extend: extra });
8748
- },
8749
- deny: () => {
8750
- clearTimeout(fallback);
8751
- respond("stop");
8752
- }
8753
- });
9276
+ for (const { kind: kind2, used, limit } of exceeded) {
9277
+ bus.emit("budget.threshold_reached", {
9278
+ kind: kind2,
9279
+ used,
9280
+ limit,
9281
+ timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
9282
+ extend: (extra) => {
9283
+ clearTimeout(fallback);
9284
+ respond({ extend: extra });
9285
+ },
9286
+ deny: () => {
9287
+ clearTimeout(fallback);
9288
+ respond("stop");
9289
+ }
9290
+ });
9291
+ }
8754
9292
  });
8755
9293
  }
8756
9294
  });
@@ -8774,54 +9312,59 @@ var SubagentBudget = class _SubagentBudget {
8774
9312
  if (ext.timeoutMs !== void 0) {
8775
9313
  this.limits.timeoutMs = ext.timeoutMs;
8776
9314
  }
9315
+ if (ext.idleTimeoutMs !== void 0) {
9316
+ this.limits.idleTimeoutMs = ext.idleTimeoutMs;
9317
+ }
8777
9318
  return decision;
8778
9319
  } finally {
8779
- this.pendingExtensions.delete(kind);
9320
+ this._pendingNegotiations.delete(kind);
8780
9321
  }
8781
9322
  }
8782
9323
  recordIteration() {
8783
9324
  this.iterations++;
8784
- if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
8785
- void this.checkLimit("iterations", this.iterations, this.limits.maxIterations);
8786
- }
9325
+ this.markActivity();
9326
+ void this.checkLimits();
8787
9327
  }
8788
9328
  recordToolCall() {
8789
9329
  this.toolCalls++;
8790
- if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
8791
- void this.checkLimit("tool_calls", this.toolCalls, this.limits.maxToolCalls);
8792
- }
9330
+ this.markActivity();
9331
+ void this.checkLimits();
8793
9332
  }
8794
9333
  recordUsage(usage, costUsd = 0) {
8795
9334
  this.tokenInput += usage.input;
8796
9335
  this.tokenOutput += usage.output;
8797
9336
  this.costUsd += costUsd;
8798
- const totalTokens = this.tokenInput + this.tokenOutput;
8799
- if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
8800
- void this.checkLimit("tokens", totalTokens, this.limits.maxTokens);
8801
- }
8802
- if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
8803
- void this.checkLimit("cost", this.costUsd, this.limits.maxCostUsd);
8804
- }
9337
+ this.markActivity();
9338
+ void this.checkLimits();
8805
9339
  }
8806
9340
  /**
8807
- * Wall-clock budget check. Unlike other limits, timeout check passes through
8808
- * `checkLimit` and is subject to the same negotiation-mode decision table.
8809
- * In practice, `'sync'` mode (the usual test configuration) means a timeout
8810
- * immediately throws `BudgetExceededError`. In production with a coordinator,
8811
- * a timeout emits `budget.threshold_reached` so the Director can decide whether
8812
- * to extend or abort.
9341
+ * Wall-clock budget check. Unlike other limits, timeout is always a hard stop
9342
+ * wall-clock time cannot be "extended" by the coordinator, so it throws
9343
+ * synchronously rather than entering the negotiation flow.
9344
+ *
9345
+ * Decision table:
9346
+ * - no `onThreshold` handler → throw `BudgetExceededError`
9347
+ * - `mode === 'sync'` → throw `BudgetExceededError`
9348
+ * - `mode === 'auto'` + no listener → throw `BudgetExceededError`
9349
+ * - `mode === 'auto'` + listener → throw `BudgetExceededError` (timeout is not extendable)
8813
9350
  */
8814
9351
  checkTimeout() {
8815
- if (this.startTime === null || this.limits.timeoutMs === void 0) return;
9352
+ if (this.startTime === null) return;
9353
+ const { timeoutMs, idleTimeoutMs } = this.limits;
9354
+ if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
8816
9355
  const elapsed = Date.now() - this.startTime;
8817
- if (elapsed > this.limits.timeoutMs) {
8818
- void this.checkLimit("timeout", elapsed, this.limits.timeoutMs);
8819
- }
9356
+ const wallTripped = timeoutMs !== void 0 && elapsed > timeoutMs;
9357
+ const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
9358
+ if (!wallTripped && !idleTripped) return;
9359
+ void this.checkLimits(elapsed);
8820
9360
  }
8821
- /** Returns true if a timeout has occurred without throwing. Useful for races. */
9361
+ /** Returns true if a wall-clock or idle timeout has occurred without throwing. */
8822
9362
  isTimedOut() {
8823
- if (this.startTime === null || this.limits.timeoutMs === void 0) return false;
8824
- return Date.now() - this.startTime > this.limits.timeoutMs;
9363
+ if (this.startTime === null) return false;
9364
+ const { timeoutMs, idleTimeoutMs } = this.limits;
9365
+ if (timeoutMs !== void 0 && Date.now() - this.startTime > timeoutMs) return true;
9366
+ if (idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs) return true;
9367
+ return false;
8825
9368
  }
8826
9369
  usage() {
8827
9370
  return {
@@ -8928,6 +9471,7 @@ function makeAgentSubagentRunner(opts) {
8928
9471
  // Cheap: O(1) per progress event, and the budget short-circuits
8929
9472
  // when timeoutMs is unset (most subagents have one set anyway).
8930
9473
  events.on("tool.progress", () => {
9474
+ ctx.budget.markActivity();
8931
9475
  try {
8932
9476
  ctx.budget.checkTimeout();
8933
9477
  } catch (e) {
@@ -8944,6 +9488,7 @@ function makeAgentSubagentRunner(opts) {
8944
9488
  currentToolName = e.name;
8945
9489
  }),
8946
9490
  events.on("provider.text_delta", (e) => {
9491
+ ctx.budget.markActivity();
8947
9492
  streamingTextAcc = (streamingTextAcc + e.text).slice(-200);
8948
9493
  })
8949
9494
  );
@@ -11513,6 +12058,7 @@ var FLEET_ROSTER = {
11513
12058
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.config])
11514
12059
  )
11515
12060
  };
12061
+ var DEFAULT_IDLE_TIMEOUT_MS = 15 * 60 * 1e3;
11516
12062
  var FLEET_ROSTER_BUDGETS = {
11517
12063
  "audit-log": { timeoutMs: 7.5 * 60 * 60 * 1e3, maxIterations: 5e3, maxToolCalls: 15e3 },
11518
12064
  "bug-hunter": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
@@ -11524,7 +12070,7 @@ var FLEET_ROSTER_BUDGETS = {
11524
12070
  )
11525
12071
  };
11526
12072
  var GENERIC_SUBAGENT_BUDGET = {
11527
- timeoutMs: 3 * 60 * 60 * 1e3,
12073
+ idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
11528
12074
  maxIterations: 5e3,
11529
12075
  maxToolCalls: 15e3
11530
12076
  };
@@ -11534,7 +12080,13 @@ function applyRosterBudget(cfg) {
11534
12080
  if (!defaultBudget) return cfg;
11535
12081
  return {
11536
12082
  ...cfg,
11537
- timeoutMs: cfg.timeoutMs ?? defaultBudget.timeoutMs,
12083
+ // Wall-clock cap is opt-in only: forward an explicit `cfg.timeoutMs`, but
12084
+ // do NOT impose the roster's historical multi-hour wall-clock default — it
12085
+ // killed agents that were still actively working. Reaping is idle-based.
12086
+ timeoutMs: cfg.timeoutMs,
12087
+ // Idle window is the default reaper. Resets on activity, so a long-but-
12088
+ // productive run is never killed; only a genuine stall is reaped.
12089
+ idleTimeoutMs: cfg.idleTimeoutMs ?? defaultBudget.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS,
11538
12090
  maxIterations: cfg.maxIterations ?? defaultBudget.maxIterations,
11539
12091
  maxToolCalls: cfg.maxToolCalls ?? defaultBudget.maxToolCalls,
11540
12092
  maxTokens: cfg.maxTokens ?? defaultBudget.maxTokens,
@@ -11604,84 +12156,213 @@ var FLEET_ROSTER_WITHACP = {
11604
12156
  ...Object.fromEntries(ACP_AGENTS.map((a) => [a.role, a]))
11605
12157
  };
11606
12158
 
11607
- // src/coordination/multi-agent-coordinator.ts
11608
- var DefaultMultiAgentCoordinator = class extends EventEmitter {
11609
- coordinatorId;
11610
- config;
11611
- runner;
11612
- fleetBus;
11613
- subagents = /* @__PURE__ */ new Map();
11614
- pendingTasks = [];
11615
- completedResults = [];
11616
- totalIterations = 0;
11617
- inFlight = 0;
11618
- /**
11619
- * Subagents currently being stopped. Set on entry to `stop()`, cleared
11620
- * once `recordCompletion` lands the terminal TaskResult. Used by
11621
- * `runDispatched` and `findIdleSubagent` to refuse mid-flight dispatch
11622
- * to a subagent the caller has already asked to terminate — closes the
11623
- * assign+terminate race where a fresh task could land on a worker that
11624
- * was about to be killed.
11625
- */
11626
- terminating = /* @__PURE__ */ new Set();
11627
- constructor(config, options = {}) {
11628
- super();
11629
- this.coordinatorId = config.coordinatorId;
11630
- this.config = config;
11631
- this.runner = options.runner;
11632
- }
11633
- /**
11634
- * Replace the runner after construction. Used when the runner depends
11635
- * on infrastructure (e.g. FleetBus) that isn't available until after
11636
- * the coordinator's owning Director is built.
11637
- */
11638
- setRunner(runner) {
11639
- this.runner = runner;
11640
- }
11641
- /**
11642
- * Wire a FleetBus for director-mode event emission. Call after the
11643
- * FleetManager is constructed so the coordinator can emit lifecycle
11644
- * events the TUI and monitoring tools subscribe to.
11645
- */
11646
- setFleetBus(fleet) {
11647
- this.fleetBus = fleet;
11648
- }
11649
- /**
11650
- * Change the in-flight dispatch ceiling at runtime. Lowering does NOT
11651
- * preempt running tasks already-dispatched subagents finish their
11652
- * current task; only future dispatches respect the new cap. Raising
11653
- * immediately tries to fill the freed slots from the pending queue.
11654
- */
11655
- setMaxConcurrent(n) {
11656
- if (!Number.isFinite(n) || n < 1) {
11657
- throw new Error(`maxConcurrent must be a finite integer >= 1, got ${n}`);
11658
- }
11659
- this.config.maxConcurrent = Math.floor(n);
11660
- this.tryDispatchNext();
11661
- }
11662
- async spawn(subagent) {
11663
- const id = subagent.id || randomUUID();
11664
- if (this.subagents.has(id)) {
11665
- throw new Error(`Subagent id "${id}" already exists \u2014 refusing to overwrite`);
11666
- }
11667
- const context = {
11668
- subagentId: id,
11669
- tasks: [],
11670
- // Wired later by the caller via setSubagentBridge() once the
11671
- // bidirectional bridge is created. Readers must null-check / use
11672
- // hasParentBridge() the type now reflects this.
11673
- parentBridge: null,
11674
- doneCondition: this.config.doneCondition,
11675
- maxConcurrent: this.config.maxConcurrent ?? 16
11676
- };
11677
- this.subagents.set(id, {
11678
- config: { ...subagent, id },
11679
- context,
11680
- status: "idle",
11681
- abortController: new AbortController()
11682
- });
11683
- this.emit("subagent.started", { subagent: { ...subagent, id } });
11684
- this.fleetBus?.emit({
12159
+ // src/coordination/subagent-nicknames.ts
12160
+ var NICKNAME_POOL = {
12161
+ // Physics & fundamental sciences
12162
+ "einstein": { name: "Einstein", domain: "physics" },
12163
+ "newton": { name: "Newton", domain: "physics" },
12164
+ "feynman": { name: "Feynman", domain: "physics" },
12165
+ "dirac": { name: "Dirac", domain: "physics" },
12166
+ "bohr": { name: "Bohr", domain: "physics" },
12167
+ "planck": { name: "Planck", domain: "physics" },
12168
+ "curie": { name: "Curie", domain: "physics" },
12169
+ "fermi": { name: "Fermi", domain: "physics" },
12170
+ "heisenberg": { name: "Heisenberg", domain: "physics" },
12171
+ "schrodinger": { name: "Schr\xF6dinger", domain: "physics" },
12172
+ // Mathematics
12173
+ "euclid": { name: "Euclid", domain: "math" },
12174
+ "gauss": { name: "Gauss", domain: "math" },
12175
+ "turing": { name: "Turing", domain: "math" },
12176
+ "poincare": { name: "Poincar\xE9", domain: "math" },
12177
+ "riemann": { name: "Riemann", domain: "math" },
12178
+ "hilbert": { name: "Hilbert", domain: "math" },
12179
+ "pythagoras": { name: "Pythagoras", domain: "math" },
12180
+ // Computing & information theory
12181
+ "von-neumann": { name: "Von Neumann", domain: "computing" },
12182
+ "shannon": { name: "Shannon", domain: "computing" },
12183
+ "hopper": { name: "Hopper", domain: "computing" },
12184
+ "backus": { name: "Backus", domain: "computing" },
12185
+ "knuth": { name: "Knuth", domain: "computing" },
12186
+ "torvalds": { name: "Torvalds", domain: "computing" },
12187
+ "stallman": { name: "Stallman", domain: "computing" },
12188
+ "berners-lee": { name: "Berners-Lee", domain: "computing" },
12189
+ "babbage": { name: "Babbage", domain: "computing" },
12190
+ "lovelace": { name: "Lovelace", domain: "computing" },
12191
+ "klein": { name: "Klein", domain: "computing" },
12192
+ // Electronics & electrical engineering
12193
+ "edison": { name: "Edison", domain: "ee" },
12194
+ "tesla": { name: "Tesla", domain: "ee" },
12195
+ "faraday": { name: "Faraday", domain: "ee" },
12196
+ "maxwell": { name: "Maxwell", domain: "ee" },
12197
+ "ohm": { name: "Ohm", domain: "ee" },
12198
+ "bell": { name: "Bell", domain: "ee" },
12199
+ "marconi": { name: "Marconi", domain: "ee" },
12200
+ "lamarr": { name: "Lamarr", domain: "ee" },
12201
+ // General science / multi-disciplinary
12202
+ "darwin": { name: "Darwin", domain: "biology" },
12203
+ "mendel": { name: "Mendel", domain: "biology" },
12204
+ "pasteur": { name: "Pasteur", domain: "biology" },
12205
+ "hawking": { name: "Hawking", domain: "cosmology" },
12206
+ "sagan": { name: "Sagan", domain: "cosmology" },
12207
+ // Chemistry / materials
12208
+ "lavoisier": { name: "Lavoisier", domain: "chemistry" },
12209
+ "mendeleev": { name: "Mendeleev", domain: "chemistry" }
12210
+ };
12211
+ var ALL_NICKNAMES = Object.values(NICKNAME_POOL);
12212
+ var DOMAIN_PREFERENCES = {
12213
+ "security": ["shannon", "turing", "lamarr", "stallman"],
12214
+ "bug-hunter": ["darwin", "curie", "feynman", "fermi"],
12215
+ "refactor": ["gauss", "hilbert", "euclid", "planck"],
12216
+ "audit-log": ["sagan", "hawking", "poincare", "newton"],
12217
+ "planner": ["hilbert", "gauss", "turing", "euclid"],
12218
+ "researcher": ["sagan", "hawking", "darwin", "pasteur"],
12219
+ "explorer": ["marconi", "bell", "columbus", "polo"],
12220
+ "testing": ["pasteur", "curie", "fermi", "bohr"],
12221
+ "frontend": ["lovelace", "hopper", "babbage", "backus"],
12222
+ "backend": ["torvalds", "stallman", "von-neumann", "backus"],
12223
+ "database": ["turing", "shannon", "backus", "knuth"],
12224
+ "devops": ["tesla", "edison", "faraday", "bell"],
12225
+ "security-scanner": ["shannon", "turing", "lamarr", "stallman"],
12226
+ "refactor-planner": ["gauss", "hilbert", "planck", "newton"],
12227
+ "architect": ["von-neumann", "turing", "gauss", "hilbert"],
12228
+ "critic": ["einstein", "feynman", "dirac", "bohr"],
12229
+ "e2e": ["hopper", "bell", "marconi", "tesla"],
12230
+ "performance": ["knuth", "gauss", "planck", "feynman"],
12231
+ "chaos": ["tesla", "edison", "curie", "fermi"],
12232
+ "cost": ["ohm", "bell", "marconi", "tesla"],
12233
+ // default fallback
12234
+ "default": ["einstein", "newton", "curie", "tesla", "edison", "turing", "shannon", "hopper", "knuth", "stallman"]
12235
+ };
12236
+ function assignNickname(role, used) {
12237
+ const preferences = [
12238
+ ...DOMAIN_PREFERENCES[role] ?? [],
12239
+ ...DOMAIN_PREFERENCES["default"] ?? []
12240
+ ];
12241
+ for (const key of preferences) {
12242
+ const entry = NICKNAME_POOL[key];
12243
+ if (entry && !used.has(key)) {
12244
+ return `${entry.name} (${formatRole(role)})`;
12245
+ }
12246
+ }
12247
+ for (const entry of ALL_NICKNAMES) {
12248
+ const key = Object.entries(NICKNAME_POOL).find(([, v]) => v.name === entry.name)?.[0];
12249
+ if (key && !used.has(key)) {
12250
+ return `${entry.name} (${formatRole(role)})`;
12251
+ }
12252
+ }
12253
+ const counter = used.size + 1;
12254
+ return `Scientist #${counter} (${formatRole(role)})`;
12255
+ }
12256
+ function formatRole(role) {
12257
+ return role.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
12258
+ }
12259
+
12260
+ // src/coordination/multi-agent-coordinator.ts
12261
+ var DefaultMultiAgentCoordinator = class extends EventEmitter {
12262
+ coordinatorId;
12263
+ config;
12264
+ runner;
12265
+ fleetBus;
12266
+ subagents = /* @__PURE__ */ new Map();
12267
+ /**
12268
+ * Base nickname keys already handed out this run (e.g. `einstein`, `tesla`).
12269
+ * Prevents two workers sharing a name. Direct `coordinator.spawn()` callers
12270
+ * (parallel/eternal engine, SDD parallel run) don't go through
12271
+ * `Director.spawn()` where nicknames are normally assigned, so the
12272
+ * coordinator upgrades placeholder names ("Executor", "slot-ab12cd", role
12273
+ * names) to memorable ones here — that's what surfaces in the fleet monitor.
12274
+ */
12275
+ usedNicknames = /* @__PURE__ */ new Set();
12276
+ pendingTasks = [];
12277
+ completedResults = [];
12278
+ totalIterations = 0;
12279
+ inFlight = 0;
12280
+ /**
12281
+ * Subagents currently being stopped. Set on entry to `stop()`, cleared
12282
+ * once `recordCompletion` lands the terminal TaskResult. Used by
12283
+ * `runDispatched` and `findIdleSubagent` to refuse mid-flight dispatch
12284
+ * to a subagent the caller has already asked to terminate — closes the
12285
+ * assign+terminate race where a fresh task could land on a worker that
12286
+ * was about to be killed.
12287
+ */
12288
+ terminating = /* @__PURE__ */ new Set();
12289
+ constructor(config, options = {}) {
12290
+ super();
12291
+ this.coordinatorId = config.coordinatorId;
12292
+ this.config = config;
12293
+ this.runner = options.runner;
12294
+ }
12295
+ /**
12296
+ * Replace the runner after construction. Used when the runner depends
12297
+ * on infrastructure (e.g. FleetBus) that isn't available until after
12298
+ * the coordinator's owning Director is built.
12299
+ */
12300
+ setRunner(runner) {
12301
+ this.runner = runner;
12302
+ }
12303
+ /**
12304
+ * Wire a FleetBus for director-mode event emission. Call after the
12305
+ * FleetManager is constructed so the coordinator can emit lifecycle
12306
+ * events the TUI and monitoring tools subscribe to.
12307
+ */
12308
+ setFleetBus(fleet) {
12309
+ this.fleetBus = fleet;
12310
+ }
12311
+ /**
12312
+ * Change the in-flight dispatch ceiling at runtime. Lowering does NOT
12313
+ * preempt running tasks — already-dispatched subagents finish their
12314
+ * current task; only future dispatches respect the new cap. Raising
12315
+ * immediately tries to fill the freed slots from the pending queue.
12316
+ */
12317
+ setMaxConcurrent(n) {
12318
+ if (!Number.isFinite(n) || n < 1) {
12319
+ throw new Error(`maxConcurrent must be a finite integer >= 1, got ${n}`);
12320
+ }
12321
+ this.config.maxConcurrent = Math.floor(n);
12322
+ this.tryDispatchNext();
12323
+ }
12324
+ /**
12325
+ * Upgrade a placeholder/role-derived name to a memorable scientist nickname
12326
+ * (e.g. "Einstein (Executor)"). A name is treated as a placeholder when it is
12327
+ * empty, equals the role (case-insensitive), is a generic default
12328
+ * ("subagent"/"adhoc"/"generic"), or is an auto-generated `slot-…` id.
12329
+ * Explicit, human-chosen names — including nicknames already assigned by
12330
+ * `Director.spawn()` — are left untouched, so this never double-assigns.
12331
+ */
12332
+ withNickname(subagent) {
12333
+ const role = subagent.role ?? "subagent";
12334
+ const name = subagent.name?.trim() ?? "";
12335
+ const isPlaceholder = name === "" || name.toLowerCase() === role.toLowerCase() || name === "subagent" || name === "adhoc" || name === "generic" || /^slot-/.test(name);
12336
+ if (!isPlaceholder) return subagent;
12337
+ const nickname = assignNickname(role, this.usedNicknames);
12338
+ const baseKey = nickname.split(" ")[0].toLowerCase().replace(/[^a-z0-9-]/g, "-");
12339
+ this.usedNicknames.add(baseKey);
12340
+ return { ...subagent, name: nickname };
12341
+ }
12342
+ async spawn(subagent) {
12343
+ subagent = this.withNickname(subagent);
12344
+ const id = subagent.id || randomUUID();
12345
+ if (this.subagents.has(id)) {
12346
+ throw new Error(`Subagent id "${id}" already exists \u2014 refusing to overwrite`);
12347
+ }
12348
+ const context = {
12349
+ subagentId: id,
12350
+ tasks: [],
12351
+ // Wired later by the caller via setSubagentBridge() once the
12352
+ // bidirectional bridge is created. Readers must null-check / use
12353
+ // hasParentBridge() — the type now reflects this.
12354
+ parentBridge: null,
12355
+ doneCondition: this.config.doneCondition,
12356
+ maxConcurrent: this.config.maxConcurrent ?? 16
12357
+ };
12358
+ this.subagents.set(id, {
12359
+ config: { ...subagent, id },
12360
+ context,
12361
+ status: "idle",
12362
+ abortController: new AbortController()
12363
+ });
12364
+ this.emit("subagent.started", { subagent: { ...subagent, id } });
12365
+ this.fleetBus?.emit({
11685
12366
  subagentId: id,
11686
12367
  ts: Date.now(),
11687
12368
  type: "subagent.assigned",
@@ -11967,13 +12648,17 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
11967
12648
  const rawMaxTokens = subagent.config.maxTokens;
11968
12649
  const rawMaxCostUsd = subagent.config.maxCostUsd;
11969
12650
  const rawTimeoutMs = subagent.config.timeoutMs;
12651
+ const rawIdleTimeoutMs = subagent.config.idleTimeoutMs;
11970
12652
  const configWithRosterDefaults = applyRosterBudget(subagent.config);
11971
12653
  const budget = new SubagentBudget({
11972
12654
  maxIterations: rawMaxIterations ?? this.config.defaultBudget?.maxIterations ?? configWithRosterDefaults.maxIterations,
11973
12655
  maxToolCalls: rawMaxToolCalls ?? this.config.defaultBudget?.maxToolCalls ?? configWithRosterDefaults.maxToolCalls,
11974
12656
  maxTokens: rawMaxTokens ?? this.config.defaultBudget?.maxTokens ?? configWithRosterDefaults.maxTokens,
11975
12657
  maxCostUsd: rawMaxCostUsd ?? this.config.defaultBudget?.maxCostUsd ?? configWithRosterDefaults.maxCostUsd,
11976
- timeoutMs: rawTimeoutMs ?? this.config.defaultBudget?.timeoutMs ?? configWithRosterDefaults.timeoutMs
12658
+ // Wall-clock cap is opt-in (explicit config / defaultBudget only); the
12659
+ // roster no longer supplies one. Idle is the default reaper.
12660
+ timeoutMs: rawTimeoutMs ?? this.config.defaultBudget?.timeoutMs ?? configWithRosterDefaults.timeoutMs,
12661
+ idleTimeoutMs: rawIdleTimeoutMs ?? this.config.defaultBudget?.idleTimeoutMs ?? configWithRosterDefaults.idleTimeoutMs
11977
12662
  });
11978
12663
  subagent.activeBudget = budget;
11979
12664
  if (!this.runner) {
@@ -12020,63 +12705,79 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
12020
12705
  }
12021
12706
  async executeWithTimeout(runner, task, ctx, budget) {
12022
12707
  const initialTimeoutMs = budget.limits.timeoutMs;
12023
- if (initialTimeoutMs === void 0) return runner(task, ctx);
12708
+ const idleLimitMs = budget.limits.idleTimeoutMs;
12709
+ if (initialTimeoutMs === void 0 && idleLimitMs === void 0) {
12710
+ return runner(task, ctx);
12711
+ }
12024
12712
  const start = Date.now();
12025
12713
  let timer = null;
12026
12714
  const timeoutPromise = new Promise((_, reject) => {
12027
12715
  const armFor = (ms) => {
12028
12716
  if (timer) clearTimeout(timer);
12029
- timer = setTimeout(async () => {
12030
- const elapsed = Date.now() - start;
12031
- const limit = budget.limits.timeoutMs ?? initialTimeoutMs;
12032
- if (!budget.onThreshold) {
12033
- this.subagents.get(ctx.subagentId)?.abortController.abort();
12034
- reject(new BudgetExceededError("timeout", limit, elapsed));
12717
+ timer = setTimeout(onTick, Math.max(0, ms));
12718
+ };
12719
+ const scheduleNext = () => {
12720
+ const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.timeoutMs ?? initialTimeoutMs) - (Date.now() - start);
12721
+ const idleRemaining = idleLimitMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
12722
+ armFor(Math.max(25, Math.min(wallRemaining, idleRemaining)));
12723
+ };
12724
+ const onTick = async () => {
12725
+ const elapsed = Date.now() - start;
12726
+ const wallLimit = initialTimeoutMs === void 0 ? void 0 : budget.limits.timeoutMs ?? initialTimeoutMs;
12727
+ const idleLimit = idleLimitMs === void 0 ? void 0 : budget.limits.idleTimeoutMs ?? idleLimitMs;
12728
+ const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
12729
+ const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
12730
+ if (idleExceeded && !wallExceeded) {
12731
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
12732
+ reject(new BudgetExceededError("timeout", idleLimit, budget.idleMs()));
12733
+ return;
12734
+ }
12735
+ if (!wallExceeded) {
12736
+ scheduleNext();
12737
+ return;
12738
+ }
12739
+ const limit = wallLimit;
12740
+ if (!budget.onThreshold) {
12741
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
12742
+ reject(new BudgetExceededError("timeout", limit, elapsed));
12743
+ return;
12744
+ }
12745
+ try {
12746
+ const result = budget.onThreshold({
12747
+ kind: "timeout",
12748
+ used: elapsed,
12749
+ limit,
12750
+ requestDecision: () => new Promise((resolveDecision) => {
12751
+ budget._events?.emit("budget.threshold_reached", {
12752
+ kind: "timeout",
12753
+ used: elapsed,
12754
+ limit,
12755
+ timeoutMs: 6e4,
12756
+ extend: (extra) => resolveDecision({ extend: extra }),
12757
+ deny: () => resolveDecision("stop")
12758
+ });
12759
+ })
12760
+ });
12761
+ const decision = typeof result === "string" ? result : await result;
12762
+ if (decision === "continue" || decision === "throw" || decision === "stop") {
12763
+ armFor(Math.max(1e3, limit));
12035
12764
  return;
12036
12765
  }
12037
- try {
12038
- const result = budget.onThreshold({
12039
- kind: "timeout",
12040
- used: elapsed,
12041
- limit,
12042
- requestDecision: () => new Promise((resolveDecision) => {
12043
- budget._events?.emit("budget.threshold_reached", {
12044
- kind: "timeout",
12045
- used: elapsed,
12046
- limit,
12047
- timeoutMs: 6e4,
12048
- extend: (extra) => resolveDecision({ extend: extra }),
12049
- deny: () => resolveDecision("stop")
12050
- });
12051
- })
12052
- });
12053
- const decision = typeof result === "string" ? result : await result;
12054
- if (decision === "continue") {
12055
- armFor(Math.max(1e3, limit));
12056
- return;
12057
- }
12058
- if (decision === "throw" || decision === "stop") {
12059
- armFor(Math.max(1e3, limit));
12060
- return;
12061
- }
12062
- if (decision.extend.timeoutMs !== void 0) {
12063
- budget.limits.timeoutMs = decision.extend.timeoutMs;
12064
- const newLimit = decision.extend.timeoutMs;
12065
- const remaining = Math.max(1e3, newLimit - elapsed);
12066
- armFor(remaining);
12067
- return;
12068
- }
12069
- this.subagents.get(ctx.subagentId)?.abortController.abort();
12070
- reject(new BudgetExceededError("timeout", limit, elapsed));
12071
- } catch (err) {
12072
- this.subagents.get(ctx.subagentId)?.abortController.abort();
12073
- reject(
12074
- err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
12075
- );
12766
+ if (decision.extend.timeoutMs !== void 0) {
12767
+ budget.limits.timeoutMs = decision.extend.timeoutMs;
12768
+ scheduleNext();
12769
+ return;
12076
12770
  }
12077
- }, ms);
12771
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
12772
+ reject(new BudgetExceededError("timeout", limit, elapsed));
12773
+ } catch (err) {
12774
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
12775
+ reject(
12776
+ err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
12777
+ );
12778
+ }
12078
12779
  };
12079
- armFor(initialTimeoutMs);
12780
+ scheduleNext();
12080
12781
  });
12081
12782
  try {
12082
12783
  return await Promise.race([runner(task, ctx), timeoutPromise]);
@@ -12253,12 +12954,12 @@ var GOAL_COMPLETE_MARKER2 = /^\s*\[goal[_\s-]?complete\]\s*$/im;
12253
12954
  var ParallelEternalEngine = class {
12254
12955
  constructor(opts) {
12255
12956
  this.opts = opts;
12256
- this.goalPath = goalFilePath(opts.projectRoot);
12957
+ this.goalPath = opts.goalPath ?? goalFilePath(opts.projectRoot);
12257
12958
  this.slots = Math.min(16, Math.max(1, opts.parallelSlots ?? 4));
12258
12959
  this.timeoutMs = opts.iterationTimeoutMs ?? 3e5;
12259
12960
  this.dispatchEnabled = opts.dispatch !== false;
12260
12961
  this.dispatchClassifier = opts.dispatchClassifier;
12261
- this.agentFactory = opts.subagentFactory ?? (async (config) => ({
12962
+ this.agentFactory = opts.subagentFactory ?? (async (_config) => ({
12262
12963
  agent: this.opts.agent,
12263
12964
  events: this.opts.agent.events
12264
12965
  }));
@@ -12352,7 +13053,6 @@ var ParallelEternalEngine = class {
12352
13053
  }
12353
13054
  const tasks = await this.decomposeGoal(goal);
12354
13055
  if (!tasks || tasks.length === 0) {
12355
- await sleep2(5e3);
12356
13056
  return false;
12357
13057
  }
12358
13058
  const fanOut = await this.fanOut(goal, tasks);
@@ -12373,6 +13073,7 @@ var ParallelEternalEngine = class {
12373
13073
  });
12374
13074
  if (fanOut.goalComplete) {
12375
13075
  this.stopRequested = true;
13076
+ this.state = "stopped";
12376
13077
  return true;
12377
13078
  }
12378
13079
  await this.maybeCompact();
@@ -12553,7 +13254,11 @@ ${lastFew}` : "No prior iterations.",
12553
13254
  } finally {
12554
13255
  clearTimeout(timer);
12555
13256
  }
12556
- } catch {
13257
+ } catch (err) {
13258
+ this.opts.onError?.(
13259
+ err instanceof Error ? err : new Error(String(err)),
13260
+ this.consecutiveFailures
13261
+ );
12557
13262
  return [];
12558
13263
  }
12559
13264
  }
@@ -12770,7 +13475,11 @@ Working rules:
12770
13475
  thrashing, terminate it rather than letting cost climb silently.
12771
13476
  6. Never claim a subagent's work as your own without verifying it. If a
12772
13477
  result looks wrong, ask_subagent for clarification before passing it
12773
- to the user.`;
13478
+ to the user.
13479
+ 7. Wind down when satisfied. When the results are good enough, call
13480
+ work_complete \u2014 no new subagents will spawn and queued tasks complete
13481
+ as aborted. Running subagents finish naturally. Call terminate_subagent
13482
+ only for ones you need to stop immediately.`;
12774
13483
  var DEFAULT_SUBAGENT_BASELINE = `You are a subagent operating under a Director. You were spawned to handle
12775
13484
  a specific slice of a larger plan \u2014 do that slice well and report back.
12776
13485
 
@@ -12915,14 +13624,12 @@ var FleetBus = class {
12915
13624
  };
12916
13625
  var FleetUsageAggregator = class {
12917
13626
  constructor(bus, priceLookup, metaLookup) {
12918
- this.bus = bus;
12919
13627
  this.priceLookup = priceLookup;
12920
13628
  this.metaLookup = metaLookup;
12921
13629
  bus.filter("provider.response", (e) => this.onProviderResponse(e));
12922
13630
  bus.filter("tool.executed", (e) => this.onToolExecuted(e));
12923
13631
  bus.filter("iteration.started", (e) => this.onIterationStarted(e));
12924
13632
  }
12925
- bus;
12926
13633
  priceLookup;
12927
13634
  metaLookup;
12928
13635
  perSubagent = /* @__PURE__ */ new Map();
@@ -12990,106 +13697,6 @@ var FleetUsageAggregator = class {
12990
13697
  snap.lastEventAt = e.ts;
12991
13698
  }
12992
13699
  };
12993
-
12994
- // src/coordination/subagent-nicknames.ts
12995
- var NICKNAME_POOL = {
12996
- // Physics & fundamental sciences
12997
- "einstein": { name: "Einstein", domain: "physics" },
12998
- "newton": { name: "Newton", domain: "physics" },
12999
- "feynman": { name: "Feynman", domain: "physics" },
13000
- "dirac": { name: "Dirac", domain: "physics" },
13001
- "bohr": { name: "Bohr", domain: "physics" },
13002
- "planck": { name: "Planck", domain: "physics" },
13003
- "curie": { name: "Curie", domain: "physics" },
13004
- "fermi": { name: "Fermi", domain: "physics" },
13005
- "heisenberg": { name: "Heisenberg", domain: "physics" },
13006
- "schrodinger": { name: "Schr\xF6dinger", domain: "physics" },
13007
- // Mathematics
13008
- "euclid": { name: "Euclid", domain: "math" },
13009
- "gauss": { name: "Gauss", domain: "math" },
13010
- "turing": { name: "Turing", domain: "math" },
13011
- "poincare": { name: "Poincar\xE9", domain: "math" },
13012
- "riemann": { name: "Riemann", domain: "math" },
13013
- "hilbert": { name: "Hilbert", domain: "math" },
13014
- "pythagoras": { name: "Pythagoras", domain: "math" },
13015
- // Computing & information theory
13016
- "von-neumann": { name: "Von Neumann", domain: "computing" },
13017
- "shannon": { name: "Shannon", domain: "computing" },
13018
- "hopper": { name: "Hopper", domain: "computing" },
13019
- "backus": { name: "Backus", domain: "computing" },
13020
- "knuth": { name: "Knuth", domain: "computing" },
13021
- "torvalds": { name: "Torvalds", domain: "computing" },
13022
- "stallman": { name: "Stallman", domain: "computing" },
13023
- "berners-lee": { name: "Berners-Lee", domain: "computing" },
13024
- "babbage": { name: "Babbage", domain: "computing" },
13025
- "lovelace": { name: "Lovelace", domain: "computing" },
13026
- "klein": { name: "Klein", domain: "computing" },
13027
- // Electronics & electrical engineering
13028
- "edison": { name: "Edison", domain: "ee" },
13029
- "tesla": { name: "Tesla", domain: "ee" },
13030
- "faraday": { name: "Faraday", domain: "ee" },
13031
- "maxwell": { name: "Maxwell", domain: "ee" },
13032
- "ohm": { name: "Ohm", domain: "ee" },
13033
- "bell": { name: "Bell", domain: "ee" },
13034
- "marconi": { name: "Marconi", domain: "ee" },
13035
- "lamarr": { name: "Lamarr", domain: "ee" },
13036
- // General science / multi-disciplinary
13037
- "darwin": { name: "Darwin", domain: "biology" },
13038
- "mendel": { name: "Mendel", domain: "biology" },
13039
- "pasteur": { name: "Pasteur", domain: "biology" },
13040
- "hawking": { name: "Hawking", domain: "cosmology" },
13041
- "sagan": { name: "Sagan", domain: "cosmology" },
13042
- // Chemistry / materials
13043
- "lavoisier": { name: "Lavoisier", domain: "chemistry" },
13044
- "mendeleev": { name: "Mendeleev", domain: "chemistry" }
13045
- };
13046
- var ALL_NICKNAMES = Object.values(NICKNAME_POOL);
13047
- var DOMAIN_PREFERENCES = {
13048
- "security": ["shannon", "turing", "lamarr", "stallman"],
13049
- "bug-hunter": ["darwin", "curie", "feynman", "fermi"],
13050
- "refactor": ["gauss", "hilbert", "euclid", "planck"],
13051
- "audit-log": ["sagan", "hawking", "poincare", "newton"],
13052
- "planner": ["hilbert", "gauss", "turing", "euclid"],
13053
- "researcher": ["sagan", "hawking", "darwin", "pasteur"],
13054
- "explorer": ["marconi", "bell", "columbus", "polo"],
13055
- "testing": ["pasteur", "curie", "fermi", "bohr"],
13056
- "frontend": ["lovelace", "hopper", "babbage", "backus"],
13057
- "backend": ["torvalds", "stallman", "von-neumann", "backus"],
13058
- "database": ["turing", "shannon", "backus", "knuth"],
13059
- "devops": ["tesla", "edison", "faraday", "bell"],
13060
- "security-scanner": ["shannon", "turing", "lamarr", "stallman"],
13061
- "refactor-planner": ["gauss", "hilbert", "planck", "newton"],
13062
- "architect": ["von-neumann", "turing", "gauss", "hilbert"],
13063
- "critic": ["einstein", "feynman", "dirac", "bohr"],
13064
- "e2e": ["hopper", "bell", "marconi", "tesla"],
13065
- "performance": ["knuth", "gauss", "planck", "feynman"],
13066
- "chaos": ["tesla", "edison", "curie", "fermi"],
13067
- "cost": ["oh", "bell", "marconi", "tesla"],
13068
- // default fallback
13069
- "default": ["einstein", "newton", "curie", "tesla", "edison", "turing", "shannon", "hopper", "knuth", "stallman"]
13070
- };
13071
- function assignNickname(role, used) {
13072
- const preferences = [
13073
- ...DOMAIN_PREFERENCES[role] ?? [],
13074
- ...DOMAIN_PREFERENCES["default"] ?? []
13075
- ];
13076
- for (const key of preferences) {
13077
- if (!used.has(key)) {
13078
- return `${NICKNAME_POOL[key].name} (${formatRole(role)})`;
13079
- }
13080
- }
13081
- for (const entry of ALL_NICKNAMES) {
13082
- const key = Object.entries(NICKNAME_POOL).find(([, v]) => v.name === entry.name)?.[0];
13083
- if (key && !used.has(key)) {
13084
- return `${entry.name} (${formatRole(role)})`;
13085
- }
13086
- }
13087
- const counter = used.size + 1;
13088
- return `Scientist #${counter} (${formatRole(role)})`;
13089
- }
13090
- function formatRole(role) {
13091
- return role.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
13092
- }
13093
13700
  function makeSpawnTool(director, roster) {
13094
13701
  const inputSchema = {
13095
13702
  type: "object",
@@ -13260,7 +13867,7 @@ function makeRollUpTool(director) {
13260
13867
  function makeTerminateTool(director) {
13261
13868
  return {
13262
13869
  name: "terminate_subagent",
13263
- description: "Forcibly abort a subagent.",
13870
+ description: 'Forcibly abort a subagent. The subagent finishes its current iteration then exits with status "stopped".',
13264
13871
  permission: "auto",
13265
13872
  mutating: true,
13266
13873
  inputSchema: { type: "object", properties: { subagentId: { type: "string", description: "Subagent to abort." } }, required: ["subagentId"] },
@@ -13271,6 +13878,19 @@ function makeTerminateTool(director) {
13271
13878
  }
13272
13879
  };
13273
13880
  }
13881
+ function makeTerminateAllTool(director) {
13882
+ return {
13883
+ name: "terminate_all",
13884
+ description: 'Forcibly stop every subagent in the fleet and drain the pending task queue. In-flight tasks are terminated mid-execution; pending tasks receive "aborted_by_parent" completion immediately. Use this when the fleet is wedged, looping, or you need a clean slate. Compare: work_complete stops spawning but lets running agents finish naturally.',
13885
+ permission: "auto",
13886
+ mutating: true,
13887
+ inputSchema: { type: "object", properties: {}, required: [] },
13888
+ async execute() {
13889
+ await director.terminateAll();
13890
+ return { ok: true, message: `Fleet shutdown complete \u2014 all subagents stopped, pending tasks drained.` };
13891
+ }
13892
+ };
13893
+ }
13274
13894
  function makeFleetStatusTool(director) {
13275
13895
  return {
13276
13896
  name: "fleet_status",
@@ -13413,7 +14033,7 @@ function makeCollabDebugTool(director) {
13413
14033
  function makeFleetEmitTool(director) {
13414
14034
  return {
13415
14035
  name: "fleet_emit",
13416
- description: "Emit a structured event on the FleetBus. Use this when you find a bug (bug.found), complete a refactor plan (refactor.plan), or finish an evaluation (critic.evaluation). Events are routed to other agents in the collab session.",
14036
+ description: "Emit a structured event on the FleetBus. Any subagent can emit any event type; the Director routes it to all listeners. Use it to stream findings, progress updates, or final results to other agents in real time.",
13417
14037
  permission: "auto",
13418
14038
  mutating: false,
13419
14039
  inputSchema: {
@@ -13421,14 +14041,14 @@ function makeFleetEmitTool(director) {
13421
14041
  properties: {
13422
14042
  type: {
13423
14043
  type: "string",
13424
- description: "Event type: bug.found | refactor.plan | critic.evaluation"
14044
+ description: "Event type string (e.g. bug.found, refactor.plan, critic.evaluation, progress, result)."
13425
14045
  },
13426
14046
  payload: {
13427
14047
  type: "object",
13428
- description: "Event payload matching the event type."
14048
+ description: "Event payload. Structure depends on event type. Use null if no payload."
13429
14049
  }
13430
14050
  },
13431
- required: ["type", "payload"]
14051
+ required: ["type"]
13432
14052
  },
13433
14053
  async execute(input) {
13434
14054
  const i = input;
@@ -13436,12 +14056,25 @@ function makeFleetEmitTool(director) {
13436
14056
  subagentId: director.id,
13437
14057
  ts: Date.now(),
13438
14058
  type: i.type,
13439
- payload: i.payload
14059
+ payload: i.payload ?? {}
13440
14060
  });
13441
14061
  return { ok: true, event: i.type };
13442
14062
  }
13443
14063
  };
13444
14064
  }
14065
+ function makeWorkCompleteTool(director) {
14066
+ return {
14067
+ name: "work_complete",
14068
+ description: "Signal that the director is satisfied with the results and the fleet should wind down. After calling this, spawn_subagent will refuse with a budget error and assign_task will instantly complete any queued tasks as aborted. Running subagents finish naturally. Call terminate_subagent separately to stop specific subagents immediately.",
14069
+ permission: "auto",
14070
+ mutating: false,
14071
+ inputSchema: { type: "object", properties: {}, required: [] },
14072
+ async execute() {
14073
+ director.workComplete();
14074
+ return { ok: true, message: "Fleet wind-down signaled. No new spawns or task dispatches." };
14075
+ }
14076
+ };
14077
+ }
13445
14078
  var CollabSession = class extends EventEmitter {
13446
14079
  sessionId;
13447
14080
  options;
@@ -13456,6 +14089,14 @@ var CollabSession = class extends EventEmitter {
13456
14089
  disposers = new Array();
13457
14090
  settled = false;
13458
14091
  timeoutMs;
14092
+ cancelled = false;
14093
+ alerts = [];
14094
+ /** Tracks tool call counts per subagent for progress-based timeout decisions. */
14095
+ progressBySubagent = /* @__PURE__ */ new Map();
14096
+ /** Last tool call count when a timeout warning was handled. */
14097
+ lastTimeoutProgress = /* @__PURE__ */ new Map();
14098
+ /** Session-level timeout timer handle (cleared on cancel or natural completion). */
14099
+ _timeoutTimer;
13459
14100
  constructor(director, fleetBus, options) {
13460
14101
  super();
13461
14102
  this.sessionId = randomUUID();
@@ -13473,11 +14114,23 @@ var CollabSession = class extends EventEmitter {
13473
14114
  };
13474
14115
  }
13475
14116
  }
14117
+ get id() {
14118
+ return this.sessionId;
14119
+ }
14120
+ getSessionAlerts() {
14121
+ return [...this.alerts];
14122
+ }
14123
+ isCancelled() {
14124
+ return this.cancelled;
14125
+ }
13476
14126
  /**
13477
- * Read the target files from disk and populate the snapshot.
13478
- * Call this after construction if you did not provide a prebuiltSnapshot
13479
- * and want the session to operate on real file contents.
14127
+ * Snapshot of role subagentId map. The Director calls coordinator.stop()
14128
+ * for each agent when cancelling the session, using this map to enumerate
14129
+ * all three collab agents.
13480
14130
  */
14131
+ getSubagentIds() {
14132
+ return new Map(this.subagentIds);
14133
+ }
13481
14134
  async buildSnapshot() {
13482
14135
  if (this.snapshot.files.length > 0) return this.snapshot;
13483
14136
  for (const filePath of this.options.targetPaths) {
@@ -13493,9 +14146,30 @@ var CollabSession = class extends EventEmitter {
13493
14146
  return this.snapshot;
13494
14147
  }
13495
14148
  /**
13496
- * Start the collaborative session: snapshot files, spawn all three agents,
13497
- * wire up event routing, and wait for completion.
14149
+ * Cancel the session. Emits director.cancel_collab on the FleetBus so all
14150
+ * collab agents finish early. The session-level timeout timer is also cleared.
14151
+ * Safe to call multiple times (idempotent after first call).
13498
14152
  */
14153
+ cancel(reason = "Director cancelled collab session") {
14154
+ if (this.settled) return;
14155
+ this.cancelled = true;
14156
+ if (this._timeoutTimer) {
14157
+ clearTimeout(this._timeoutTimer);
14158
+ this._timeoutTimer = void 0;
14159
+ }
14160
+ this.fleetBus.emit({
14161
+ subagentId: this.director.id,
14162
+ ts: Date.now(),
14163
+ type: "director.cancel_collab",
14164
+ payload: { sessionId: this.sessionId, reason, cancelledAt: (/* @__PURE__ */ new Date()).toISOString() }
14165
+ });
14166
+ this.fleetBus.emit({
14167
+ subagentId: this.director.id,
14168
+ ts: Date.now(),
14169
+ type: "collab.cancelled",
14170
+ payload: { sessionId: this.sessionId, reason }
14171
+ });
14172
+ }
13499
14173
  async start() {
13500
14174
  if (this.settled) throw new Error("session already settled");
13501
14175
  this.settled = true;
@@ -13510,10 +14184,10 @@ var CollabSession = class extends EventEmitter {
13510
14184
  this.subagentIds.set("refactor-planner", refactorPlannerId);
13511
14185
  this.subagentIds.set("critic", criticId);
13512
14186
  const timeout = new Promise((_, reject) => {
13513
- setTimeout(
13514
- () => reject(new Error(`CollabSession timed out after ${this.timeoutMs}ms`)),
13515
- this.timeoutMs
13516
- );
14187
+ this._timeoutTimer = setTimeout(() => {
14188
+ this.cancel("Session-level timeout reached");
14189
+ reject(new Error(`CollabSession timed out after ${this.timeoutMs}ms`));
14190
+ }, this.timeoutMs);
13517
14191
  });
13518
14192
  let results;
13519
14193
  try {
@@ -13526,6 +14200,10 @@ var CollabSession = class extends EventEmitter {
13526
14200
  timeout
13527
14201
  ]);
13528
14202
  } catch (err) {
14203
+ if (this._timeoutTimer) {
14204
+ clearTimeout(this._timeoutTimer);
14205
+ this._timeoutTimer = void 0;
14206
+ }
13529
14207
  this.cleanup();
13530
14208
  const error = err instanceof Error ? err : new Error(String(err));
13531
14209
  this.emit("session.error", error);
@@ -13539,17 +14217,6 @@ var CollabSession = class extends EventEmitter {
13539
14217
  this.emit("session.done", report);
13540
14218
  return report;
13541
14219
  }
13542
- /**
13543
- * Parse a completed agent's final text output and route the structured
13544
- * findings onto the FleetBus. Agents are instructed to emit one JSON object
13545
- * per finding / plan / evaluation; we scan the output for balanced JSON
13546
- * objects (tolerating prose, markdown, and ```json fences around them) and
13547
- * dispatch each by its discriminating key. The wired FleetBus listeners then
13548
- * collect them into the bugs / plans / evaluations maps.
13549
- *
13550
- * Failed or empty results are ignored, and a single malformed object never
13551
- * aborts the collation pass — see {@link extractJsonObjects}.
13552
- */
13553
14220
  async parseAndEmit(result) {
13554
14221
  if (result.status !== "success" || result.result == null) return;
13555
14222
  const text = typeof result.result === "string" ? result.result : JSON.stringify(result.result);
@@ -13565,12 +14232,6 @@ var CollabSession = class extends EventEmitter {
13565
14232
  });
13566
14233
  }
13567
14234
  }
13568
- /**
13569
- * Extract every top-level JSON object embedded in free-form agent text.
13570
- * Scans for balanced braces while respecting string literals and escape
13571
- * sequences, so prose, markdown headers, and code fences around the JSON
13572
- * are skipped. Malformed spans are dropped silently rather than thrown.
13573
- */
13574
14235
  extractJsonObjects(text) {
13575
14236
  const objects = [];
13576
14237
  let depth = 0;
@@ -13607,25 +14268,33 @@ var CollabSession = class extends EventEmitter {
13607
14268
  }
13608
14269
  return objects;
13609
14270
  }
14271
+ budgetForRole(role) {
14272
+ if (this.options.budgetOverrides?.[role]) {
14273
+ return this.options.budgetOverrides[role];
14274
+ }
14275
+ const defaults = {
14276
+ "bug-hunter": { maxIterations: 2e3, maxToolCalls: 5e3, timeoutMs: 10 * 60 * 1e3 },
14277
+ "refactor-planner": { maxIterations: 1500, maxToolCalls: 4e3, timeoutMs: 8 * 60 * 1e3 },
14278
+ "critic": { maxIterations: 1e3, maxToolCalls: 3e3, timeoutMs: 6 * 60 * 1e3 }
14279
+ };
14280
+ return defaults[role] ?? { maxIterations: 1500, maxToolCalls: 4e3, timeoutMs: 8 * 60 * 1e3 };
14281
+ }
13610
14282
  async spawnAgent(role, taskBrief) {
14283
+ const budget = this.budgetForRole(role);
13611
14284
  const cfg = {
13612
14285
  id: `${role}-${this.sessionId}`,
13613
14286
  name: role,
13614
- role
13615
- // No fleet_emit tool agents output structured JSON, the Director parses
13616
- // results and emits FleetBus events. This keeps the agent simple and the
13617
- // routing deterministic.
14287
+ role,
14288
+ tools: ["fleet_emit", "fleet_status", "read", "grep", "glob", "bash", "write"],
14289
+ maxIterations: budget.maxIterations,
14290
+ maxToolCalls: budget.maxToolCalls,
14291
+ timeoutMs: budget.timeoutMs
13618
14292
  };
13619
14293
  const subagentId = await this.director.spawn(cfg);
13620
- await this.director.assign({
13621
- id: randomUUID(),
13622
- subagentId,
13623
- description: taskBrief
13624
- });
14294
+ await this.director.assign({ id: randomUUID(), subagentId, description: taskBrief });
13625
14295
  return subagentId;
13626
14296
  }
13627
14297
  buildBugHunterTask() {
13628
- this.options.targetPaths.join(", ");
13629
14298
  const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
13630
14299
  const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
13631
14300
  ${f.content}`).join("\n\n");
@@ -13634,13 +14303,17 @@ ${f.content}`).join("\n\n");
13634
14303
  Target files:
13635
14304
  ${fileContents}
13636
14305
 
13637
- For each bug found, write ONE valid JSON line to the scratchpad with this exact structure:
13638
- { "finding": { "id": "<uuid>", "type": "<pattern>", "severity": "<critical|high|medium|low>", "location": { "file": "<path>", "line": <n> }, "description": "<explain>", "suggestedFix": "<optional>" } }.
13639
- After scanning all files, write your full markdown bug report to the shared scratchpad at:
13640
- ${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
14306
+ For each bug found, emit it using the fleet_emit tool immediately:
14307
+ { "type": "bug.found", "payload": { "finding": { "id": "<uuid>", "type": "<pattern>", "severity": "<critical|high|medium|low>", "location": { "file": "<path>", "line": <n> }, "description": "<explain>", "suggestedFix": "<optional>" } } }
14308
+
14309
+ After scanning all files, write your full markdown bug report to:
14310
+ ${scratchpad}/bug-hunter-report-${this.sessionId}.md
14311
+
14312
+ Important: emit each finding as soon as you find it. Do not batch or wait until the end.`;
13641
14313
  }
13642
14314
  buildRefactorPlannerTask() {
13643
14315
  const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
14316
+ const bugHunterReportPath = `${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
13644
14317
  const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
13645
14318
  ${f.content}`).join("\n\n");
13646
14319
  return `You are RefactorPlanner. Plan refactorings for the following files.
@@ -13648,26 +14321,146 @@ ${f.content}`).join("\n\n");
13648
14321
  Target files:
13649
14322
  ${fileContents}
13650
14323
 
13651
- Listen for bug.found events from BugHunter on the fleet bus. For each bug, create a refactor plan and write one JSON line per plan:
13652
- { "plan": { "id": "<uuid>", "basedOnBugIds": ["<bug-id>"], "phases": [...], "riskScore": "<low|medium|high>", "estimatedChangeCount": <n>, "rollbackStrategy": "<text>" } }.
13653
- After planning, write your full markdown plan to:
13654
- ${scratchpad}/refactor-plan-${this.sessionId}.md`;
14324
+ Read the BugHunter report at: ${bugHunterReportPath}
14325
+
14326
+ For each bug you can address, emit a refactor plan using fleet_emit:
14327
+ { "type": "refactor.plan", "payload": { "plan": { "id": "<uuid>", "basedOnBugIds": ["<bug-id>"], "phases": [{ "number": 1, "title": "<phase>", "tasks": ["<task>"], "risk": "<low|medium|high>" }], "riskScore": "<low|medium|high>", "estimatedChangeCount": <n>, "rollbackStrategy": "<text>" } } }
14328
+
14329
+ Also write your full markdown plan to:
14330
+ ${scratchpad}/refactor-plan-${this.sessionId}.md
14331
+
14332
+ Emit each plan immediately. Do not wait until planning is complete.`;
13655
14333
  }
13656
14334
  buildCriticTask() {
13657
14335
  const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
14336
+ const bugHunterReportPath = `${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
14337
+ const refactorPlanPath = `${scratchpad}/refactor-plan-${this.sessionId}.md`;
13658
14338
  const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
13659
14339
  ${f.content}`).join("\n\n");
13660
- return `You are Critic. Evaluate bug findings and refactor plans as they arrive via fleet bus events.
14340
+ return `You are Critic. Evaluate bug findings and refactor plans.
13661
14341
 
13662
14342
  Target files:
13663
14343
  ${fileContents}
13664
14344
 
13665
- Subscribe to bug.found events (from BugHunter) and refactor.plan events (from RefactorPlanner). For each subject, write one JSON line per evaluation:
13666
- { "evaluation": { "id": "<uuid>", "subjectType": "<bug_finding|refactor_plan>", "subjectId": "<id>", "score": <0-10>, "verdict": "<approve|needs_revision|reject>", "strengths": [...], "weaknesses": [...], "concerns": [...] } }.
13667
- After all evaluations, write your markdown critic report to:
13668
- ${scratchpad}/critic-report-${this.sessionId}.md`;
14345
+ Read the BugHunter report at: ${bugHunterReportPath}
14346
+ Read the RefactorPlanner report at: ${refactorPlanPath}
14347
+
14348
+ For each bug and refactor plan, emit your evaluation using fleet_emit:
14349
+ { "type": "critic.evaluation", "payload": { "evaluation": { "id": "<uuid>", "subjectType": "<bug_finding|refactor_plan>", "subjectId": "<id>", "score": <0-10>, "verdict": "<approve|needs_revision|reject>", "strengths": ["<strength>"], "weaknesses": ["<weakness>"], "concerns": [{ "description": "<concern>", "severity": "<blocking|advisory>" }] } } }
14350
+
14351
+ After all evaluations, write your markdown report to:
14352
+ ${scratchpad}/critic-report-${this.sessionId}.md
14353
+
14354
+ Emit each evaluation immediately. Do not wait until you have read all reports.`;
13669
14355
  }
13670
14356
  wireFleetBus() {
14357
+ const dTool = this.fleetBus.filter("tool.executed", (e) => {
14358
+ this.progressBySubagent.set(e.subagentId, (this.progressBySubagent.get(e.subagentId) ?? 0) + 1);
14359
+ });
14360
+ this.disposers.push(dTool);
14361
+ const dBudget = this.fleetBus.filter("budget.threshold_reached", (e) => {
14362
+ const payload = e.payload;
14363
+ const role = this.roleFromSubagentId(e.subagentId);
14364
+ if (!role) return;
14365
+ const btwNotes = this.director.getLeaderBtwNotes();
14366
+ const alert = {
14367
+ sessionId: this.sessionId,
14368
+ subagentId: e.subagentId,
14369
+ role,
14370
+ level: "warning" /* WARNING */,
14371
+ message: `${role} hit ${payload.kind} soft limit (${payload.used}/${payload.limit})`,
14372
+ budgetKind: payload.kind,
14373
+ elapsedMs: payload.timeoutMs,
14374
+ limit: payload.limit,
14375
+ btwNotes
14376
+ };
14377
+ this.alerts.push(alert);
14378
+ this.fleetBus.emit({
14379
+ subagentId: e.subagentId,
14380
+ ts: Date.now(),
14381
+ type: "collab.warning",
14382
+ payload: alert
14383
+ });
14384
+ const decision = this.options.onBudgetWarning?.(alert) ?? "ignore";
14385
+ if (decision === "cancel") {
14386
+ this.cancel(`Director cancelled: ${role} ${payload.kind} threshold`);
14387
+ return;
14388
+ }
14389
+ if (payload.kind === "timeout") {
14390
+ const progress = this.progressBySubagent.get(e.subagentId) ?? 0;
14391
+ const lastProgress = this.lastTimeoutProgress.get(e.subagentId) ?? -1;
14392
+ if (progress <= lastProgress) {
14393
+ payload.deny();
14394
+ return;
14395
+ }
14396
+ this.lastTimeoutProgress.set(e.subagentId, progress);
14397
+ const newLimit = Math.min(Math.ceil((payload.timeoutMs ?? payload.limit) * 2), 24 * 60 * 6e4);
14398
+ setImmediate(() => {
14399
+ payload.extend({ timeoutMs: newLimit });
14400
+ });
14401
+ return;
14402
+ }
14403
+ if (decision === "extend") {
14404
+ setImmediate(() => {
14405
+ const base = Math.max(payload.limit, payload.used);
14406
+ const extra = {};
14407
+ switch (payload.kind) {
14408
+ case "iterations":
14409
+ extra.maxIterations = Math.min(Math.ceil(base * 1.5), 5e4);
14410
+ break;
14411
+ case "tool_calls":
14412
+ extra.maxToolCalls = Math.min(Math.ceil(base * 1.5), 1e5);
14413
+ break;
14414
+ case "tokens":
14415
+ extra.maxTokens = Math.min(Math.ceil(base * 1.5), 5e6);
14416
+ break;
14417
+ case "cost":
14418
+ extra.maxCostUsd = Math.min(base * 1.5, 100);
14419
+ break;
14420
+ }
14421
+ payload.extend(extra);
14422
+ });
14423
+ return;
14424
+ }
14425
+ if (payload.kind !== "timeout") {
14426
+ setImmediate(() => {
14427
+ const base = Math.max(payload.limit, payload.used);
14428
+ const extra = {};
14429
+ switch (payload.kind) {
14430
+ case "iterations":
14431
+ extra.maxIterations = Math.min(Math.ceil(base * 1.25), 5e4);
14432
+ break;
14433
+ case "tool_calls":
14434
+ extra.maxToolCalls = Math.min(Math.ceil(base * 1.25), 1e5);
14435
+ break;
14436
+ case "tokens":
14437
+ extra.maxTokens = Math.min(Math.ceil(base * 1.25), 5e6);
14438
+ break;
14439
+ case "cost":
14440
+ extra.maxCostUsd = Math.min(base * 1.25, 100);
14441
+ break;
14442
+ }
14443
+ payload.extend(extra);
14444
+ });
14445
+ }
14446
+ });
14447
+ this.disposers.push(dBudget);
14448
+ const dCancel = this.fleetBus.filter("director.cancel_collab", (e) => {
14449
+ const payload = e.payload;
14450
+ if (payload.sessionId !== this.sessionId) return;
14451
+ this.cancelled = true;
14452
+ if (this._timeoutTimer) {
14453
+ clearTimeout(this._timeoutTimer);
14454
+ this._timeoutTimer = void 0;
14455
+ }
14456
+ this.fleetBus.emit({
14457
+ subagentId: this.director.id,
14458
+ ts: Date.now(),
14459
+ type: "collab.cancelled",
14460
+ payload: { sessionId: this.sessionId, reason: payload.reason }
14461
+ });
14462
+ });
14463
+ this.disposers.push(dCancel);
13671
14464
  const d1 = this.fleetBus.filter("bug.found", (e) => {
13672
14465
  const payload = e.payload;
13673
14466
  if (payload?.finding) {
@@ -13693,10 +14486,19 @@ ${scratchpad}/critic-report-${this.sessionId}.md`;
13693
14486
  });
13694
14487
  this.disposers.push(d3);
13695
14488
  }
14489
+ roleFromSubagentId(subagentId) {
14490
+ for (const [role, id] of this.subagentIds) {
14491
+ if (id === subagentId) return role;
14492
+ }
14493
+ const match = subagentId.match(/^(bug-hunter|refactor-planner|critic)/);
14494
+ return match?.[1] ?? null;
14495
+ }
13696
14496
  assembleReport() {
13697
14497
  const bugList = Array.from(this.bugs.values());
13698
14498
  const planList = Array.from(this.plans.values());
13699
14499
  const evalList = Array.from(this.evaluations.values());
14500
+ let disposition = "completed";
14501
+ if (this.cancelled) disposition = "cancelled";
13700
14502
  const verdictOrder = {
13701
14503
  approve: 0,
13702
14504
  needs_revision: 1,
@@ -13710,33 +14512,41 @@ ${scratchpad}/critic-report-${this.sessionId}.md`;
13710
14512
  },
13711
14513
  "approve"
13712
14514
  );
13713
- const summary = this.buildMarkdownSummary(bugList, planList, evalList, overallVerdict);
14515
+ const summary = this.buildMarkdownSummary(bugList, planList, evalList, overallVerdict, disposition);
13714
14516
  return {
13715
14517
  sessionId: this.sessionId,
13716
14518
  startedAt: this.snapshot.createdAt,
13717
14519
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
13718
14520
  targetPaths: this.options.targetPaths,
14521
+ disposition,
13719
14522
  bugs: bugList,
13720
14523
  refactorPlans: planList,
13721
14524
  evaluations: evalList,
14525
+ alerts: [...this.alerts],
13722
14526
  overallVerdict,
13723
14527
  summary
13724
14528
  };
13725
14529
  }
13726
- buildMarkdownSummary(bugs, plans, evals, overallVerdict) {
14530
+ buildMarkdownSummary(bugs, plans, evals, overallVerdict, disposition) {
13727
14531
  const lines = [
13728
14532
  `## Collaborative Debugging Report \u2014 ${this.sessionId}`,
13729
14533
  "",
13730
14534
  `**Target:** ${this.options.targetPaths.join(", ")}`,
14535
+ `**Disposition:** ${disposition.toUpperCase()}`,
13731
14536
  `**Overall Verdict:** **${overallVerdict.toUpperCase()}**`,
13732
14537
  ""
13733
14538
  ];
14539
+ if (this.alerts.length > 0) {
14540
+ lines.push("### Alerts", "");
14541
+ for (const alert of this.alerts) {
14542
+ lines.push(`- **[${alert.level.toUpperCase()}]** ${alert.role}: ${alert.message}`);
14543
+ }
14544
+ lines.push("");
14545
+ }
13734
14546
  if (bugs.length > 0) {
13735
14547
  lines.push("### Bugs Found", "");
13736
14548
  for (const b of bugs) {
13737
- lines.push(
13738
- `- **[${b.severity.toUpperCase()}]** \`${b.location.file}:${b.location.line}\` \u2014 ${b.description}`
13739
- );
14549
+ lines.push(`- **[${b.severity.toUpperCase()}]** \`${b.location.file}:${b.location.line}\` \u2014 ${b.description}`);
13740
14550
  }
13741
14551
  lines.push("");
13742
14552
  }
@@ -13755,9 +14565,7 @@ ${scratchpad}/critic-report-${this.sessionId}.md`;
13755
14565
  for (const e of evals) {
13756
14566
  lines.push(`- [${e.subjectType}] score=${e.score}/10 \u2014 **${e.verdict.toUpperCase()}**`);
13757
14567
  for (const c of e.concerns) {
13758
- if (c.severity === "blocking") {
13759
- lines.push(` - ${c.description}`);
13760
- }
14568
+ if (c.severity === "blocking") lines.push(` - ${c.description}`);
13761
14569
  }
13762
14570
  }
13763
14571
  lines.push("");
@@ -13775,10 +14583,9 @@ var FleetSpawnBudgetError = class extends Error {
13775
14583
  kind;
13776
14584
  limit;
13777
14585
  observed;
13778
- constructor(kind, limit, observed) {
13779
- super(
13780
- kind === "max_spawns" ? `Director spawn budget exceeded: tried to spawn #${observed} but maxSpawns is ${limit}` : `Director spawn depth budget exceeded: this director is at depth ${observed} and maxSpawnDepth is ${limit}`
13781
- );
14586
+ constructor(kind, limit, observed, message) {
14587
+ const defaultMsg = kind === "max_spawns" ? `Director spawn budget exceeded: tried to spawn #${observed} but maxSpawns is ${limit}` : `Director spawn depth budget exceeded: this director is at depth ${observed} and maxSpawnDepth is ${limit}`;
14588
+ super(message ?? defaultMsg);
13782
14589
  this.name = "FleetSpawnBudgetError";
13783
14590
  this.kind = kind;
13784
14591
  this.limit = limit;
@@ -13799,6 +14606,20 @@ var FleetCostCapError = class extends Error {
13799
14606
  this.observed = observed;
13800
14607
  }
13801
14608
  };
14609
+ var FleetContextOverflowError = class extends Error {
14610
+ kind;
14611
+ limit;
14612
+ observed;
14613
+ constructor(limit, observed) {
14614
+ super(
14615
+ `Leader context overflow: leader has ${observed} tokens in flight (limit: ${limit}). Compact the leader context before spawning more subagents.`
14616
+ );
14617
+ this.name = "FleetContextOverflowError";
14618
+ this.kind = "max_context_load";
14619
+ this.limit = limit;
14620
+ this.observed = observed;
14621
+ }
14622
+ };
13802
14623
  var Director = class {
13803
14624
  /** Alias for the ICoordinator contract. `id` is retained for backward compatibility. */
13804
14625
  get coordinatorId() {
@@ -13815,6 +14636,25 @@ var Director = class {
13815
14636
  * injected; otherwise own FleetUsageAggregator.
13816
14637
  */
13817
14638
  usage;
14639
+ /**
14640
+ * Update the leader agent's current context pressure (full request tokens:
14641
+ * messages + systemPrompt + toolDefs). The director checks this before every
14642
+ * spawn — if the pressure exceeds `maxLeaderContextLoad * maxContext`,
14643
+ * spawning is refused with a `FleetContextOverflowError`.
14644
+ *
14645
+ * Call this after each leader agent iteration to keep the pressure current.
14646
+ * The compactor's `CompactReport.fullRequestTokensAfter` is a natural source.
14647
+ */
14648
+ setLeaderContextPressure(tokens) {
14649
+ this.leaderContextPressure = tokens;
14650
+ this.fleetManager?.setLeaderContextPressure(tokens);
14651
+ }
14652
+ /**
14653
+ * Read the leader agent's current context pressure.
14654
+ */
14655
+ getLeaderContextPressure() {
14656
+ return this.leaderContextPressure;
14657
+ }
13818
14658
  /**
13819
14659
  * Optional fleet-level policy container. When provided the Director
13820
14660
  * delegates spawn budgeting, manifest entries, and checkpointing to it
@@ -13900,6 +14740,23 @@ var Director = class {
13900
14740
  taskCompletedListener = null;
13901
14741
  /** Optional LLM classifier for smart dispatch. Passed from options. */
13902
14742
  dispatchClassifier;
14743
+ /** Leader agent's current context pressure (full request tokens). */
14744
+ leaderContextPressure = 0;
14745
+ /** Maximum context load fraction before spawn is refused. */
14746
+ maxLeaderContextLoad;
14747
+ /** Provider's max context window in tokens. */
14748
+ maxContext;
14749
+ /**
14750
+ * When set by `workComplete()`, the director stops dispatching new tasks
14751
+ * and terminates all running subagents. Used when the director's LLM decides
14752
+ * the goal is satisfied and no further spawns are needed — prevents the
14753
+ * coordinator from keeping workers alive for tasks that will never arrive.
14754
+ */
14755
+ workCompleteFlag = false;
14756
+ /** Pending /btw notes stashed by the leader agent (see setLeaderBtwNote). */
14757
+ _leaderBtwNotes = [];
14758
+ /** Active collab sessions tracked by sessionId (see spawnCollab). */
14759
+ _activeCollabSessions = /* @__PURE__ */ new Map();
13903
14760
  constructor(opts) {
13904
14761
  this.id = opts.config.coordinatorId || randomUUID();
13905
14762
  this.manifestPath = opts.manifestPath;
@@ -13915,6 +14772,8 @@ var Director = class {
13915
14772
  this.dispatchClassifier = opts.dispatchClassifier;
13916
14773
  this.maxFleetCostUsd = opts.directorBudget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
13917
14774
  this.maxBudgetExtensions = opts.maxBudgetExtensions ?? 5;
14775
+ this.maxLeaderContextLoad = opts.maxLeaderContextLoad ?? 0.85;
14776
+ this.maxContext = opts.maxContext ?? 128e3;
13918
14777
  this.sessionsRoot = opts.sessionsRoot;
13919
14778
  this.directorRunId = opts.directorRunId ?? this.id;
13920
14779
  this.stateCheckpoint = opts.stateCheckpointPath ? new DirectorStateCheckpoint(opts.stateCheckpointPath, {
@@ -13942,7 +14801,7 @@ var Director = class {
13942
14801
  this.fleet = new FleetBus();
13943
14802
  this.usage = new FleetUsageAggregator(
13944
14803
  this.fleet,
13945
- (id, provider, model) => {
14804
+ (_id, provider, model) => {
13946
14805
  if (provider && model) return this.priceLookups.get(`${provider}/${model}`);
13947
14806
  return void 0;
13948
14807
  },
@@ -14004,6 +14863,9 @@ var Director = class {
14004
14863
  });
14005
14864
  this.fleet.filter("budget.threshold_reached", (e) => {
14006
14865
  const payload = e.payload;
14866
+ if (e.subagentId.startsWith("bug-hunter-") || e.subagentId.startsWith("refactor-planner-") || e.subagentId.startsWith("critic-")) {
14867
+ return;
14868
+ }
14007
14869
  if (payload.kind === "timeout") {
14008
14870
  const progress = progressBySubagent.get(e.subagentId) ?? 0;
14009
14871
  const lastProgress = lastTimeoutProgress.get(e.subagentId) ?? -1;
@@ -14082,6 +14944,107 @@ var Director = class {
14082
14944
  extensionsFor(subagentId) {
14083
14945
  return this.extendTotals.get(subagentId) ?? 0;
14084
14946
  }
14947
+ /**
14948
+ * Signal that the director's work is done. Once called:
14949
+ * - `spawn()` throws `FleetSpawnBudgetError('max_spawns', …)` — no new
14950
+ * subagents can be created
14951
+ * - Running subagents are NOT forcibly stopped — they finish naturally,
14952
+ * but no new tasks are dispatched to them
14953
+ *
14954
+ * This lets the director LLM say "I'm satisfied with the results, stop
14955
+ * spawning and wind down" — without killing in-flight work mid-execution.
14956
+ * Call `terminateAll()` separately if you need immediate teardown.
14957
+ *
14958
+ * Idempotent — calling twice is a no-op.
14959
+ */
14960
+ workComplete() {
14961
+ this.workCompleteFlag = true;
14962
+ this.fleet.emit({ subagentId: this.id, ts: Date.now(), type: "director.work_complete", payload: {} });
14963
+ }
14964
+ /** Returns true if `workComplete()` has been called on this director. */
14965
+ isWorkComplete() {
14966
+ return this.workCompleteFlag;
14967
+ }
14968
+ /**
14969
+ * Stashes a /btw note on the leader agent's context. The leader's agent loop
14970
+ * calls `consumeBtwNotes()` at each iteration boundary and folds pending notes
14971
+ * into the conversation as a visible block — no abort, no restart, just a
14972
+ * "by the way" nudge the model picks up on its next turn.
14973
+ *
14974
+ * This is the entry point for the host (CLI, TUI) to inject /btw notes
14975
+ * programmatically without going through the slash-command path.
14976
+ */
14977
+ setLeaderBtwNote(note) {
14978
+ const trimmed = note.trim();
14979
+ if (!trimmed) return this._leaderBtwNotes.length;
14980
+ this._leaderBtwNotes = [...this._leaderBtwNotes, trimmed].slice(-20);
14981
+ return this._leaderBtwNotes.length;
14982
+ }
14983
+ /**
14984
+ * Read and clear all pending /btw notes the leader has stashed.
14985
+ * Returns them in FIFO order (empty array when none).
14986
+ *
14987
+ * Called by CollabSession when a budget threshold event fires so the
14988
+ * Director can inspect accumulated /btw notes before deciding whether
14989
+ * to cancel the collab session or let it continue.
14990
+ */
14991
+ getLeaderBtwNotes() {
14992
+ const notes = this._leaderBtwNotes;
14993
+ this._leaderBtwNotes = [];
14994
+ return notes;
14995
+ }
14996
+ /**
14997
+ * Peek at pending /btw notes without consuming them.
14998
+ * Useful for UI to show "N pending notes" without clearing them.
14999
+ */
15000
+ peekLeaderBtwNotes() {
15001
+ return [...this._leaderBtwNotes];
15002
+ }
15003
+ /**
15004
+ * Drain (read + clear) all /btw notes in one call.
15005
+ * Alias for getLeaderBtwNotes() — kept for symmetry with consumeBtwNotes()
15006
+ * in the agent's btw.ts. The Director calls this at the point where it
15007
+ * makes a cancellation decision, not on every budget event.
15008
+ */
15009
+ drainLeaderBtwNotes() {
15010
+ return this.getLeaderBtwNotes();
15011
+ }
15012
+ /**
15013
+ * Cancel an active collab session by its id.
15014
+ * Emits `director.cancel_collab` on the FleetBus so the session's agents
15015
+ * finish early with a 'cancelled' disposition.
15016
+ *
15017
+ * Returns silently if the session id is not tracked or already settled.
15018
+ * The CollabDebugReport will reflect 'cancelled' disposition when awaited.
15019
+ */
15020
+ cancelCollabSession(sessionId, reason = "Director cancelled") {
15021
+ const session = this._activeCollabSessions.get(sessionId);
15022
+ if (!session || session.isCancelled()) return;
15023
+ session.cancel(reason);
15024
+ for (const [_role, subagentId] of session.getSubagentIds()) {
15025
+ this.coordinator.stop(subagentId).catch(() => {
15026
+ });
15027
+ }
15028
+ }
15029
+ /**
15030
+ * Subscribe a callback to be notified whenever a collab session raises
15031
+ * an alert (warning level). The callback receives the full DirectorAlert
15032
+ * payload so the host (CLI, TUI) can display it to the user.
15033
+ * Returns an unsubscribe function.
15034
+ */
15035
+ onCollabAlert(handler) {
15036
+ return this.fleet.filter("collab.warning", (e) => {
15037
+ handler(e.payload);
15038
+ });
15039
+ }
15040
+ /**
15041
+ * Returns all active (not yet settled) collab session ids.
15042
+ * Useful for the TUI to render a "N active sessions" badge and for
15043
+ * the host to know what can be cancelled.
15044
+ */
15045
+ activeCollabSessions() {
15046
+ return Array.from(this._activeCollabSessions.keys());
15047
+ }
14085
15048
  /** Best-effort session-writer append. Swallows failures — the director
14086
15049
  * must not break a fleet run because the session JSONL handle closed. */
14087
15050
  async appendSessionEvent(event) {
@@ -14120,12 +15083,21 @@ var Director = class {
14120
15083
  * it the `cost` column in `usage.snapshot()` stays at 0.
14121
15084
  */
14122
15085
  async spawn(config, priceLookup) {
15086
+ if (this.workCompleteFlag) {
15087
+ throw new FleetSpawnBudgetError(
15088
+ "max_spawns",
15089
+ this.maxSpawns,
15090
+ this.spawnCount + 1,
15091
+ "workComplete() has been called \u2014 director closed further spawning"
15092
+ );
15093
+ }
14123
15094
  if (this.fleetManager) {
14124
15095
  const rejection = this.fleetManager.canSpawn(config);
14125
15096
  if (rejection) {
14126
15097
  if (rejection.kind === "max_spawn_depth") throw new FleetSpawnBudgetError("max_spawn_depth", rejection.limit, rejection.observed);
14127
15098
  if (rejection.kind === "max_spawns") throw new FleetSpawnBudgetError("max_spawns", rejection.limit, rejection.observed);
14128
15099
  if (rejection.kind === "max_cost_usd") throw new FleetCostCapError(rejection.limit, rejection.observed);
15100
+ if (rejection.kind === "max_context_load") throw new FleetContextOverflowError(rejection.limit, rejection.observed);
14129
15101
  }
14130
15102
  } else {
14131
15103
  if (this.spawnDepth >= this.maxSpawnDepth) {
@@ -14140,9 +15112,15 @@ var Director = class {
14140
15112
  throw new FleetCostCapError(this.maxFleetCostUsd, totalCost);
14141
15113
  }
14142
15114
  }
15115
+ if (this.maxLeaderContextLoad < 1) {
15116
+ const threshold = this.maxContext * this.maxLeaderContextLoad;
15117
+ if (this.leaderContextPressure >= threshold) {
15118
+ throw new FleetContextOverflowError(threshold, this.leaderContextPressure);
15119
+ }
15120
+ }
14143
15121
  }
14144
15122
  let result;
14145
- const needsNickname = config.name === config.role || !config.name || config.name === "subagent";
15123
+ const needsNickname = config.name === config.role || !config.name || config.name === "subagent" || config.name === "adhoc";
14146
15124
  if (needsNickname) {
14147
15125
  const role = config.role ?? "subagent";
14148
15126
  if (this.fleetManager) {
@@ -14367,6 +15345,24 @@ var Director = class {
14367
15345
  */
14368
15346
  async assign(task) {
14369
15347
  const taskWithId = task.id ? task : { ...task, id: randomUUID() };
15348
+ if (this.workCompleteFlag) {
15349
+ const synthetic = {
15350
+ subagentId: taskWithId.subagentId ?? "unassigned",
15351
+ taskId: taskWithId.id,
15352
+ status: "stopped",
15353
+ error: { kind: "aborted_by_parent", message: "Director called workComplete() \u2014 no further tasks will run", retryable: false },
15354
+ iterations: 0,
15355
+ toolCalls: 0,
15356
+ durationMs: 0
15357
+ };
15358
+ this.completed.set(taskWithId.id, synthetic);
15359
+ const waiter = this.taskWaiters.get(taskWithId.id);
15360
+ if (waiter) {
15361
+ waiter.resolve(synthetic);
15362
+ this.taskWaiters.delete(taskWithId.id);
15363
+ }
15364
+ return taskWithId.id;
15365
+ }
14370
15366
  if (task.subagentId) {
14371
15367
  if (this.fleetManager) {
14372
15368
  this.fleetManager.addTaskToSubagent(task.subagentId, taskWithId.id);
@@ -14589,12 +15585,14 @@ var Director = class {
14589
15585
  makeAskTool(this),
14590
15586
  makeRollUpTool(this),
14591
15587
  makeTerminateTool(this),
15588
+ makeTerminateAllTool(this),
14592
15589
  makeFleetStatusTool(this),
14593
15590
  makeFleetUsageTool(this),
14594
15591
  makeFleetSessionTool(this),
14595
15592
  makeFleetHealthTool(this),
14596
15593
  makeCollabDebugTool(this),
14597
- makeFleetEmitTool(this)
15594
+ makeFleetEmitTool(this),
15595
+ makeWorkCompleteTool(this)
14598
15596
  ];
14599
15597
  return t2;
14600
15598
  }
@@ -14614,7 +15612,15 @@ var Director = class {
14614
15612
  * three agents complete or the session times out.
14615
15613
  */
14616
15614
  async spawnCollab(options) {
14617
- const session = new CollabSession(this, this.fleet, options);
15615
+ const session = new CollabSession(this, this.fleet, {
15616
+ ...options,
15617
+ onBudgetWarning: (alert) => {
15618
+ return options.onBudgetWarning?.(alert) ?? "ignore";
15619
+ }
15620
+ });
15621
+ this._activeCollabSessions.set(session.sessionId, session);
15622
+ session.on("session.done", () => this._activeCollabSessions.delete(session.sessionId));
15623
+ session.on("session.error", () => this._activeCollabSessions.delete(session.sessionId));
14618
15624
  return session.start();
14619
15625
  }
14620
15626
  /**
@@ -15104,8 +16110,8 @@ async function loadProjectModes(modesDir) {
15104
16110
  for (const entry of entries) {
15105
16111
  if (!entry.endsWith(".md") && !entry.endsWith(".txt")) continue;
15106
16112
  const filePath = path6.join(modesDir, entry);
15107
- const stat9 = await fsp2.stat(filePath);
15108
- if (!stat9.isFile()) continue;
16113
+ const stat8 = await fsp2.stat(filePath);
16114
+ if (!stat8.isFile()) continue;
15109
16115
  const content = await fsp2.readFile(filePath, "utf8");
15110
16116
  const id = path6.basename(entry, path6.extname(entry));
15111
16117
  modes.push({
@@ -16379,9 +17385,9 @@ var AISpecBuilder = class {
16379
17385
  if (!this.sessionPath) return;
16380
17386
  try {
16381
17387
  const fsp19 = await import('fs/promises');
16382
- const path31 = await import('path');
17388
+ const path35 = await import('path');
16383
17389
  const { atomicWrite: atomicWrite2 } = await Promise.resolve().then(() => (init_atomic_write(), atomic_write_exports));
16384
- await fsp19.mkdir(path31.dirname(this.sessionPath), { recursive: true });
17390
+ await fsp19.mkdir(path35.dirname(this.sessionPath), { recursive: true });
16385
17391
  await atomicWrite2(this.sessionPath, JSON.stringify(this.session, null, 2));
16386
17392
  } catch {
16387
17393
  }
@@ -17033,7 +18039,7 @@ function analyzeCriticalPath(graph) {
17033
18039
  blockedTasks
17034
18040
  };
17035
18041
  }
17036
- function getTransitiveBlocked(graph, taskId, blocksMap) {
18042
+ function getTransitiveBlocked(_graph, taskId, blocksMap) {
17037
18043
  const visited = /* @__PURE__ */ new Set();
17038
18044
  const queue = [taskId];
17039
18045
  while (queue.length > 0) {
@@ -17049,7 +18055,7 @@ function getTransitiveBlocked(graph, taskId, blocksMap) {
17049
18055
  }
17050
18056
  return visited;
17051
18057
  }
17052
- function computeCriticalPath(graph, topoOrder, blockedByMap) {
18058
+ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
17053
18059
  const allIds = Array.from(graph.nodes.keys());
17054
18060
  if (allIds.length === 0) return [];
17055
18061
  const dist = /* @__PURE__ */ new Map();
@@ -17091,15 +18097,15 @@ function computeCriticalPath(graph, topoOrder, blockedByMap) {
17091
18097
  maxId = id;
17092
18098
  }
17093
18099
  }
17094
- const path31 = [];
18100
+ const path35 = [];
17095
18101
  let current = maxId;
17096
18102
  const visited = /* @__PURE__ */ new Set();
17097
18103
  while (current && !visited.has(current)) {
17098
18104
  visited.add(current);
17099
- path31.unshift(current);
18105
+ path35.unshift(current);
17100
18106
  current = prev.get(current) ?? null;
17101
18107
  }
17102
- return path31;
18108
+ return path35;
17103
18109
  }
17104
18110
  function computeParallelGroups(graph, blockedByMap) {
17105
18111
  const groups = [];
@@ -17467,13 +18473,11 @@ function createAutoExecutor(opts) {
17467
18473
 
17468
18474
  // src/sdd/sdd-task-decomposer.ts
17469
18475
  var SddTaskDecomposer = class {
17470
- constructor(tracker, graph, opts = {}) {
18476
+ constructor(tracker, _graph, opts = {}) {
17471
18477
  this.tracker = tracker;
17472
- this.graph = graph;
17473
18478
  this.slots = Math.min(16, Math.max(1, opts.parallelSlots ?? 4));
17474
18479
  }
17475
18480
  tracker;
17476
- graph;
17477
18481
  slots;
17478
18482
  wave = 0;
17479
18483
  // -------------------------------------------------------------------
@@ -17626,7 +18630,7 @@ var SddParallelRun = class {
17626
18630
  this.coordinator.setRunner?.(runner);
17627
18631
  }
17628
18632
  defaultFactory() {
17629
- return async (config) => ({
18633
+ return async (_config) => ({
17630
18634
  agent: this.opts.agent,
17631
18635
  events: this.opts.agent.events
17632
18636
  });
@@ -18079,7 +19083,7 @@ async function startMetricsServer(opts) {
18079
19083
  const tls = opts.tls;
18080
19084
  const useHttps = !!(tls?.cert && tls?.key);
18081
19085
  const host = opts.host ?? "127.0.0.1";
18082
- const path31 = opts.path ?? "/metrics";
19086
+ const path35 = opts.path ?? "/metrics";
18083
19087
  const healthPath = opts.healthPath ?? "/healthz";
18084
19088
  const healthRegistry = opts.healthRegistry;
18085
19089
  const listener = (req, res) => {
@@ -18089,7 +19093,7 @@ async function startMetricsServer(opts) {
18089
19093
  return;
18090
19094
  }
18091
19095
  const url = req.url.split("?")[0];
18092
- if (url === path31) {
19096
+ if (url === path35) {
18093
19097
  let body;
18094
19098
  try {
18095
19099
  body = renderPrometheus(opts.sink.snapshot());
@@ -18153,7 +19157,7 @@ async function startMetricsServer(opts) {
18153
19157
  const protocol = useHttps ? "https" : "http";
18154
19158
  return {
18155
19159
  port: boundPort,
18156
- url: `${protocol}://${host}:${boundPort}${path31}`,
19160
+ url: `${protocol}://${host}:${boundPort}${path35}`,
18157
19161
  close: () => new Promise((resolve9, reject) => {
18158
19162
  server.close((err) => err ? reject(err) : resolve9());
18159
19163
  })
@@ -18440,6 +19444,12 @@ function roughEstimate(messages) {
18440
19444
  return total;
18441
19445
  }
18442
19446
  function createContextManagerTool(opts = {}) {
19447
+ const minCompactThreshold = opts.minCompactThreshold ?? 0;
19448
+ const noopRetryDeltaTokens = opts.noopRetryDeltaTokens ?? 2e3;
19449
+ const maxContext = opts.maxContext ?? 128e3;
19450
+ const compactThresholdFraction = opts.compactThresholdFraction ?? 0.5;
19451
+ const effectiveThreshold = minCompactThreshold > 0 ? minCompactThreshold : Math.floor(maxContext * compactThresholdFraction);
19452
+ let lastNoopTokens = 0;
18443
19453
  return {
18444
19454
  name: CONTEXT_MANAGER_TOOL_NAME,
18445
19455
  description: 'Inspect or reorganize the conversation context window. Use "check" to see token budget. Use "summary" to collapse a message range into a concise note (provide "text" for custom summary). Use "prune" to remove specific messages by index. Use "add_note" to inject a summary note. Use "compact" to run aggressive compaction. Use "repair" to remove orphan tool_use/tool_result blocks after manual context surgery.',
@@ -18530,11 +19540,41 @@ function createContextManagerTool(opts = {}) {
18530
19540
  notes: "No compactor registered. Use /compact aggressive via slash command instead."
18531
19541
  };
18532
19542
  }
19543
+ const fullEstimate = input.systemPrompt != null && Array.isArray(input.tools) ? estimateRequestTokens(messages, input.systemPrompt, input.tools) : { total: beforeTokens};
19544
+ const currentTokens = fullEstimate.total;
19545
+ if (lastNoopTokens > 0) {
19546
+ const delta = currentTokens - lastNoopTokens;
19547
+ if (delta < noopRetryDeltaTokens) {
19548
+ return {
19549
+ action: "compact",
19550
+ beforeTokens,
19551
+ afterTokens: beforeTokens,
19552
+ messageCount: messages.length,
19553
+ notes: `Compact is a NOOP retry: context grew only ${delta} tokens since the last no-op attempt (threshold: ${noopRetryDeltaTokens}). Skip until more content accumulates.`
19554
+ };
19555
+ }
19556
+ }
19557
+ if (currentTokens < effectiveThreshold) {
19558
+ return {
19559
+ action: "compact",
19560
+ beforeTokens,
19561
+ afterTokens: beforeTokens,
19562
+ messageCount: messages.length,
19563
+ notes: `Context tokens (${currentTokens}) below compact threshold (${effectiveThreshold}). Skipping.`
19564
+ };
19565
+ }
18533
19566
  const report = await opts.compactor.compact(ctx);
18534
19567
  ctx.clearFileTracking();
18535
19568
  const repair = applyMessages([...ctx.messages]);
18536
19569
  const afterTokens = repair.changed ? roughEstimate(ctx.messages) : report.after;
18537
19570
  const repaired = report.repaired ?? (repair.changed ? repair : void 0);
19571
+ const reduced = report.fullRequestTokensBefore > report.fullRequestTokensAfter;
19572
+ const repairedSomething = !!report.repaired;
19573
+ if (reduced || repairedSomething) {
19574
+ lastNoopTokens = 0;
19575
+ } else {
19576
+ lastNoopTokens = currentTokens;
19577
+ }
18538
19578
  return {
18539
19579
  action: "compact",
18540
19580
  beforeTokens,
@@ -18775,16 +19815,16 @@ var allServers = () => ({
18775
19815
  function parseSkillRef(input) {
18776
19816
  const trimmed = input.trim().replace(/^https?:\/\/github\.com\//, "").replace(/\.git$/, "");
18777
19817
  const atIdx = trimmed.indexOf("@");
18778
- let refPath;
19818
+ let refPath2;
18779
19819
  let ref;
18780
19820
  if (atIdx > 0) {
18781
- refPath = trimmed.slice(0, atIdx);
19821
+ refPath2 = trimmed.slice(0, atIdx);
18782
19822
  ref = trimmed.slice(atIdx + 1);
18783
19823
  } else {
18784
- refPath = trimmed;
19824
+ refPath2 = trimmed;
18785
19825
  ref = "main";
18786
19826
  }
18787
- const parts = refPath.split("/").filter(Boolean);
19827
+ const parts = refPath2.split("/").filter(Boolean);
18788
19828
  if (parts.length < 2) {
18789
19829
  throw new Error(`Invalid skill reference "${input}". Expected format: user/repo or user/repo@ref`);
18790
19830
  }
@@ -19000,10 +20040,10 @@ var SkillInstaller = class {
19000
20040
  if (!resolved.startsWith(path6.resolve(destDir))) {
19001
20041
  throw new Error(`Path traversal detected in skill file: ${file}`);
19002
20042
  }
19003
- const stat9 = await fsp2.stat(srcPath);
19004
- if (stat9.size > MAX_SKILL_FILE_SIZE) {
20043
+ const stat8 = await fsp2.stat(srcPath);
20044
+ if (stat8.size > MAX_SKILL_FILE_SIZE) {
19005
20045
  throw new Error(
19006
- `Skill file "${file}" is too large (${(stat9.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`
20046
+ `Skill file "${file}" is too large (${(stat8.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`
19007
20047
  );
19008
20048
  }
19009
20049
  await fsp2.mkdir(path6.dirname(destPath), { recursive: true });
@@ -19042,7 +20082,7 @@ var SkillInstaller = class {
19042
20082
  * - Name: update that specific skill
19043
20083
  * - Name + newRef: update to a different ref
19044
20084
  */
19045
- async update(nameOrRef, opts) {
20085
+ async update(nameOrRef, _opts) {
19046
20086
  const result = { updated: [], unchanged: [], errors: [] };
19047
20087
  const allEntries = await this.manifest.listAll();
19048
20088
  let targets;
@@ -19121,131 +20161,747 @@ var SkillInstaller = class {
19121
20161
  if (!entry) {
19122
20162
  throw new Error(`Skill "${name}" is not installed${scope === "user" ? " (global)" : ""}.`);
19123
20163
  }
19124
- await this.removeSkillFiles(name, scope);
19125
- await this.manifest.removeEntry(name, scope);
19126
- this.invalidateLoaderCache();
20164
+ await this.removeSkillFiles(name, scope);
20165
+ await this.manifest.removeEntry(name, scope);
20166
+ this.invalidateLoaderCache();
20167
+ }
20168
+ /**
20169
+ * List all installed skills from the manifest.
20170
+ */
20171
+ async listInstalled() {
20172
+ return this.manifest.listAll();
20173
+ }
20174
+ // ── Private helpers ──────────────────────────────────────────────
20175
+ /**
20176
+ * Detect skills in an extracted repository.
20177
+ * Returns an array of detected skills with their files.
20178
+ */
20179
+ async detectSkills(baseDir) {
20180
+ const results = [];
20181
+ const rootSkillMd = path6.join(baseDir, "SKILL.md");
20182
+ try {
20183
+ await fsp2.access(rootSkillMd);
20184
+ const content = await fsp2.readFile(rootSkillMd, "utf8");
20185
+ const meta = parseFrontmatter2(content);
20186
+ if (meta.name && meta.description) {
20187
+ results.push({
20188
+ name: meta.name,
20189
+ baseDir,
20190
+ files: ["SKILL.md"]
20191
+ });
20192
+ return results;
20193
+ }
20194
+ } catch {
20195
+ }
20196
+ const skillsDir = path6.join(baseDir, "skills");
20197
+ try {
20198
+ const entries = await fsp2.readdir(skillsDir, { withFileTypes: true });
20199
+ for (const entry of entries) {
20200
+ if (!entry.isDirectory()) continue;
20201
+ const skillFile = path6.join(skillsDir, entry.name, "SKILL.md");
20202
+ try {
20203
+ const content = await fsp2.readFile(skillFile, "utf8");
20204
+ const meta = parseFrontmatter2(content);
20205
+ if (meta.name && meta.description) {
20206
+ const skillDir = path6.join(skillsDir, entry.name);
20207
+ const files = await collectFiles(skillDir, skillDir);
20208
+ results.push({
20209
+ name: meta.name,
20210
+ baseDir: skillDir,
20211
+ files
20212
+ });
20213
+ }
20214
+ } catch {
20215
+ }
20216
+ }
20217
+ } catch {
20218
+ }
20219
+ return results;
20220
+ }
20221
+ /**
20222
+ * Remove all files for an installed skill.
20223
+ */
20224
+ async removeSkillFiles(name, scope) {
20225
+ const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
20226
+ const skillDir = path6.join(targetDir, name);
20227
+ await fsp2.rm(skillDir, { recursive: true, force: true });
20228
+ }
20229
+ /**
20230
+ * Invalidate the skill loader's cache so newly installed skills appear.
20231
+ */
20232
+ invalidateLoaderCache() {
20233
+ const loader = this.opts.skillLoader;
20234
+ if (loader && typeof loader.invalidateCache === "function") {
20235
+ loader.invalidateCache();
20236
+ }
20237
+ }
20238
+ };
20239
+ function parseFrontmatter2(raw) {
20240
+ if (!raw.startsWith("---")) return {};
20241
+ const end = raw.indexOf("\n---", 4);
20242
+ if (end === -1) return {};
20243
+ const block = raw.slice(4, end);
20244
+ const out = {};
20245
+ let key = null;
20246
+ let value = [];
20247
+ const flush = () => {
20248
+ if (key) {
20249
+ out[key] = value.join("\n").trim();
20250
+ }
20251
+ key = null;
20252
+ value = [];
20253
+ };
20254
+ for (const line of block.split("\n")) {
20255
+ const m = /^([a-zA-Z_]+):\s*(\|?)\s*(.*)$/.exec(line);
20256
+ if (m) {
20257
+ flush();
20258
+ key = m[1] ?? "";
20259
+ const pipe = m[2];
20260
+ const rest = m[3] ?? "";
20261
+ if (pipe === "|") {
20262
+ value = [];
20263
+ } else if (rest) {
20264
+ value = [rest];
20265
+ } else {
20266
+ value = [];
20267
+ }
20268
+ } else if (key) {
20269
+ value.push(line.replace(/^\s+/, ""));
20270
+ }
20271
+ }
20272
+ flush();
20273
+ return out;
20274
+ }
20275
+ async function collectFiles(dir, baseDir) {
20276
+ const results = [];
20277
+ const entries = await fsp2.readdir(dir, { withFileTypes: true });
20278
+ for (const entry of entries) {
20279
+ const fullPath = path6.join(dir, entry.name);
20280
+ const relPath = path6.relative(baseDir, fullPath);
20281
+ if (entry.isDirectory()) {
20282
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
20283
+ results.push(...await collectFiles(fullPath, baseDir));
20284
+ } else {
20285
+ results.push(relPath);
20286
+ }
20287
+ }
20288
+ return results;
20289
+ }
20290
+
20291
+ // src/storage/annotations-store.ts
20292
+ init_atomic_write();
20293
+ var FILE_VERSION = 1;
20294
+ var MAX_TEXT_LENGTH = 2e3;
20295
+ var MAX_ANNOTATIONS = 1e3;
20296
+ var AnnotationsStore = class {
20297
+ dir;
20298
+ /** Per-session write queue. Created lazily on first add. */
20299
+ writeChains = /* @__PURE__ */ new Map();
20300
+ constructor(opts) {
20301
+ this.dir = opts.dir;
20302
+ }
20303
+ // ── Reads ──────────────────────────────────────────────────────────────
20304
+ /**
20305
+ * Return all annotations for `sessionId` in insertion order
20306
+ * (oldest first). Returns an empty array when no file exists
20307
+ * yet (the normal case for a fresh session).
20308
+ */
20309
+ async list(sessionId) {
20310
+ const file = await this.readFile(sessionId);
20311
+ return file ? file.annotations : [];
20312
+ }
20313
+ /**
20314
+ * Convenience: only unresolved annotations, newest first — the
20315
+ * common UI rendering for "what still needs attention?".
20316
+ */
20317
+ async listOpen(sessionId) {
20318
+ const all = await this.list(sessionId);
20319
+ return all.filter((a) => !a.resolved).reverse();
20320
+ }
20321
+ // ── Writes ─────────────────────────────────────────────────────────────
20322
+ /**
20323
+ * Add a new annotation. Returns the persisted record (with id
20324
+ * and timestamps filled in). Throws when `text` is empty or
20325
+ * exceeds `MAX_TEXT_LENGTH`.
20326
+ */
20327
+ async add(input) {
20328
+ const text = input.text.trim();
20329
+ if (text.length === 0) {
20330
+ throw new Error("Annotation text must be non-empty");
20331
+ }
20332
+ if (text.length > MAX_TEXT_LENGTH) {
20333
+ throw new Error(
20334
+ `Annotation text exceeds ${MAX_TEXT_LENGTH} chars (got ${text.length})`
20335
+ );
20336
+ }
20337
+ if (!Number.isInteger(input.atEventIndex) || input.atEventIndex < 0) {
20338
+ throw new Error("atEventIndex must be a non-negative integer");
20339
+ }
20340
+ const annotation = {
20341
+ id: randomUUID(),
20342
+ sessionId: input.sessionId,
20343
+ atEventIndex: input.atEventIndex,
20344
+ authorId: input.authorId,
20345
+ authorRole: "annotator",
20346
+ text,
20347
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
20348
+ resolved: false
20349
+ };
20350
+ await this.enqueue(input.sessionId, async () => {
20351
+ const all = await this.list(input.sessionId);
20352
+ all.push(annotation);
20353
+ if (all.length > MAX_ANNOTATIONS) {
20354
+ const sorted = all.map((a, i) => ({ a, i })).sort((x, y) => {
20355
+ if (x.a.resolved !== y.a.resolved) return x.a.resolved ? 1 : -1;
20356
+ return x.a.createdAt.localeCompare(y.a.createdAt);
20357
+ });
20358
+ const evictCount = all.length - MAX_ANNOTATIONS;
20359
+ const toEvict = new Set(sorted.slice(0, evictCount).map((s) => s.a.id));
20360
+ const kept = all.filter((a) => !toEvict.has(a.id));
20361
+ await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: kept });
20362
+ } else {
20363
+ await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: all });
20364
+ }
20365
+ });
20366
+ return annotation;
20367
+ }
20368
+ /**
20369
+ * Mark an annotation as resolved. Returns the updated record, or
20370
+ * `null` if no annotation with that id exists in this session.
20371
+ * Idempotent: resolving an already-resolved annotation refreshes
20372
+ * `resolvedAt` / `resolvedBy` to the latest call.
20373
+ */
20374
+ async resolve(input) {
20375
+ let updated = null;
20376
+ await this.enqueue(input.sessionId, async () => {
20377
+ const all = await this.list(input.sessionId);
20378
+ const idx = all.findIndex((a) => a.id === input.annotationId);
20379
+ if (idx === -1) {
20380
+ updated = null;
20381
+ return;
20382
+ }
20383
+ const next = {
20384
+ ...all[idx],
20385
+ resolved: true,
20386
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
20387
+ resolvedBy: input.resolvedBy
20388
+ };
20389
+ all[idx] = next;
20390
+ await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: all });
20391
+ updated = next;
20392
+ });
20393
+ return updated;
20394
+ }
20395
+ // ── Internals ──────────────────────────────────────────────────────────
20396
+ filePath(sessionId) {
20397
+ if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
20398
+ throw new Error(`Invalid sessionId: ${sessionId}`);
20399
+ }
20400
+ return path6.join(this.dir, `${sessionId}.annotations.json`);
20401
+ }
20402
+ async readFile(sessionId) {
20403
+ const fp = this.filePath(sessionId);
20404
+ try {
20405
+ const raw = await fsp2.readFile(fp, "utf8");
20406
+ const parsed = JSON.parse(raw);
20407
+ if (parsed.version !== FILE_VERSION) {
20408
+ return { version: FILE_VERSION, annotations: [] };
20409
+ }
20410
+ return parsed;
20411
+ } catch (err) {
20412
+ if (err.code === "ENOENT") return null;
20413
+ return { version: FILE_VERSION, annotations: [] };
20414
+ }
20415
+ }
20416
+ async writeFile(sessionId, file) {
20417
+ const fp = this.filePath(sessionId);
20418
+ await atomicWrite(fp, JSON.stringify(file, null, 2));
20419
+ }
20420
+ /**
20421
+ * Serialize writes per-sessionId. We chain promises instead of
20422
+ * using a Mutex class so the contract is obvious from the
20423
+ * call-site: `enqueue(sid, fn)` runs `fn` after every prior
20424
+ * enqueue for `sid` has settled.
20425
+ */
20426
+ enqueue(sessionId, fn) {
20427
+ const prev = this.writeChains.get(sessionId) ?? Promise.resolve();
20428
+ const next = prev.then(fn, fn);
20429
+ this.writeChains.set(
20430
+ sessionId,
20431
+ next.catch(() => void 0)
20432
+ );
20433
+ return next;
20434
+ }
20435
+ };
20436
+
20437
+ // src/storage/replay-log-store.ts
20438
+ init_atomic_write();
20439
+ function stableStringify(value) {
20440
+ return JSON.stringify(sortKeys(value));
20441
+ }
20442
+ function sortKeys(value) {
20443
+ if (Array.isArray(value)) return value.map(sortKeys);
20444
+ if (value && typeof value === "object") {
20445
+ const obj = value;
20446
+ const sorted = {};
20447
+ for (const key of Object.keys(obj).sort()) {
20448
+ sorted[key] = sortKeys(obj[key]);
20449
+ }
20450
+ return sorted;
20451
+ }
20452
+ return value;
20453
+ }
20454
+ function hashRequest(request) {
20455
+ const payload = {
20456
+ model: request.model,
20457
+ system: request.system,
20458
+ messages: request.messages,
20459
+ tools: request.tools,
20460
+ maxTokens: request.maxTokens,
20461
+ temperature: request.temperature,
20462
+ topP: request.topP,
20463
+ stopSequences: request.stopSequences,
20464
+ toolChoice: request.toolChoice
20465
+ };
20466
+ const json = stableStringify(payload);
20467
+ const digest = createHash("sha256").update(json, "utf8").digest("hex");
20468
+ return `sha256:${digest}`;
20469
+ }
20470
+ var DEFAULT_MAX_ENTRIES = 1e3;
20471
+ var ReplayLogStore = class {
20472
+ dir;
20473
+ writeChains = /* @__PURE__ */ new Map();
20474
+ /** Per-session hash → entry index, kept in memory after the first load. */
20475
+ cache = /* @__PURE__ */ new Map();
20476
+ maxEntries;
20477
+ constructor(opts) {
20478
+ this.dir = opts.dir;
20479
+ this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
20480
+ }
20481
+ // ── Writes ──────────────────────────────────────────────────────────────
20482
+ /**
20483
+ * Record a request/response pair. Idempotent on hash: a second
20484
+ * `record` for the same hash is a no-op (the existing entry wins).
20485
+ * Returns the hash.
20486
+ */
20487
+ async record(input) {
20488
+ const hash = hashRequest(input.request);
20489
+ await this.enqueue(input.sessionId, async () => {
20490
+ const cache = await this.ensureCache(input.sessionId);
20491
+ if (cache.has(hash)) return;
20492
+ const entry = {
20493
+ hash,
20494
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
20495
+ request: input.request,
20496
+ response: input.response
20497
+ };
20498
+ let all = [...cache.values(), entry];
20499
+ if (all.length > this.maxEntries) {
20500
+ const evictCount = all.length - this.maxEntries;
20501
+ all = all.slice(evictCount);
20502
+ }
20503
+ await this.writeAll(input.sessionId, all);
20504
+ cache.clear();
20505
+ for (const e of all) cache.set(e.hash, e);
20506
+ });
20507
+ return hash;
20508
+ }
20509
+ // ── Reads ───────────────────────────────────────────────────────────────
20510
+ /**
20511
+ * Look up an entry by hash. Returns `null` when the request has
20512
+ * not been recorded for this session. O(1) after the first call
20513
+ * per session (in-memory cache).
20514
+ */
20515
+ async lookup(sessionId, hash) {
20516
+ const cache = await this.ensureCache(sessionId);
20517
+ return cache.get(hash) ?? null;
20518
+ }
20519
+ /** All recorded entries for a session, in insertion order. */
20520
+ async load(sessionId) {
20521
+ const cache = await this.ensureCache(sessionId);
20522
+ return [...cache.values()];
20523
+ }
20524
+ /**
20525
+ * List every session id that has a replay log in the store dir.
20526
+ * Returns an array of `{ sessionId, entryCount, path }` sorted
20527
+ * by sessionId for stable output. Used by `wstack replay --list`.
20528
+ */
20529
+ async list() {
20530
+ let entries;
20531
+ try {
20532
+ entries = await fsp2.readdir(this.dir);
20533
+ } catch (err) {
20534
+ if (err.code === "ENOENT") return [];
20535
+ return [];
20536
+ }
20537
+ const out = [];
20538
+ for (const name of entries) {
20539
+ if (!name.endsWith(".replay.jsonl")) continue;
20540
+ const sessionId = name.slice(0, -".replay.jsonl".length);
20541
+ const all = await this.load(sessionId);
20542
+ out.push({
20543
+ sessionId,
20544
+ entryCount: all.length,
20545
+ path: path6.join(this.dir, name)
20546
+ });
20547
+ }
20548
+ return out.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
20549
+ }
20550
+ // ── Internals ───────────────────────────────────────────────────────────
20551
+ filePath(sessionId) {
20552
+ if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
20553
+ throw new Error(`Invalid sessionId: ${sessionId}`);
20554
+ }
20555
+ return path6.join(this.dir, `${sessionId}.replay.jsonl`);
20556
+ }
20557
+ async readAll(sessionId) {
20558
+ const fp = this.filePath(sessionId);
20559
+ try {
20560
+ const raw = await fsp2.readFile(fp, "utf8");
20561
+ const out = [];
20562
+ for (const line of raw.split("\n")) {
20563
+ if (!line.trim()) continue;
20564
+ try {
20565
+ const parsed = JSON.parse(line);
20566
+ if ("entry" in parsed && parsed.entry) {
20567
+ out.push(parsed.entry);
20568
+ } else {
20569
+ out.push(parsed);
20570
+ }
20571
+ } catch {
20572
+ }
20573
+ }
20574
+ return out;
20575
+ } catch (err) {
20576
+ if (err.code === "ENOENT") return [];
20577
+ return [];
20578
+ }
20579
+ }
20580
+ async writeAll(sessionId, entries) {
20581
+ const fp = this.filePath(sessionId);
20582
+ const body = entries.map((e) => JSON.stringify(e)).join("\n") + (entries.length ? "\n" : "");
20583
+ await atomicWrite(fp, body);
20584
+ }
20585
+ async ensureCache(sessionId) {
20586
+ let cache = this.cache.get(sessionId);
20587
+ if (cache) return cache;
20588
+ const all = await this.readAll(sessionId);
20589
+ cache = /* @__PURE__ */ new Map();
20590
+ for (const e of all) cache.set(e.hash, e);
20591
+ this.cache.set(sessionId, cache);
20592
+ return cache;
20593
+ }
20594
+ enqueue(sessionId, fn) {
20595
+ const prev = this.writeChains.get(sessionId) ?? Promise.resolve();
20596
+ const next = prev.then(fn, fn);
20597
+ this.writeChains.set(
20598
+ sessionId,
20599
+ next.catch(() => void 0)
20600
+ );
20601
+ return next;
20602
+ }
20603
+ };
20604
+ var SessionRecovery = class {
20605
+ constructor(dir) {
20606
+ this.dir = dir;
20607
+ }
20608
+ dir;
20609
+ /**
20610
+ * Scan a session log and return a `StaleSession` if and only
20611
+ * if the last event is an `in_flight_start` without a matching
20612
+ * `in_flight_end`. Returns `null` when:
20613
+ * - the log does not exist;
20614
+ * - the log is empty;
20615
+ * - the last event is `in_flight_end` (clean shutdown);
20616
+ * - the last event is something else (e.g. an unannotated
20617
+ * legacy log without in-flight markers).
20618
+ */
20619
+ async detectStale(sessionId) {
20620
+ const fp = this.filePath(sessionId);
20621
+ let raw;
20622
+ try {
20623
+ raw = await fsp2.readFile(fp, "utf8");
20624
+ } catch (err) {
20625
+ if (err.code === "ENOENT") return null;
20626
+ return null;
20627
+ }
20628
+ return this.parseForStale(sessionId, fp, raw);
20629
+ }
20630
+ /**
20631
+ * Generate a recovery plan for a session. The plan describes
20632
+ * "what would be re-executed" if the user chose to resume —
20633
+ * everything after the last `checkpoint` event, plus the
20634
+ * dangling in-flight marker if present.
20635
+ *
20636
+ * Returns a non-null plan for ANY session that has at least
20637
+ * one event after a checkpoint (or, for legacy sessions, at
20638
+ * least one event). Pure read; no mutation.
20639
+ */
20640
+ async recover(sessionId) {
20641
+ const fp = this.filePath(sessionId);
20642
+ let raw;
20643
+ try {
20644
+ raw = await fsp2.readFile(fp, "utf8");
20645
+ } catch (err) {
20646
+ if (err.code === "ENOENT") return null;
20647
+ return null;
20648
+ }
20649
+ const events = [];
20650
+ for (const line of raw.split("\n")) {
20651
+ if (!line.trim()) continue;
20652
+ try {
20653
+ events.push(JSON.parse(line));
20654
+ } catch {
20655
+ }
20656
+ }
20657
+ if (events.length === 0) return null;
20658
+ let lastCheckpoint = null;
20659
+ let lastCheckpointIdx = -1;
20660
+ for (let i = 0; i < events.length; i++) {
20661
+ if (events[i].type === "checkpoint") {
20662
+ lastCheckpoint = events[i];
20663
+ lastCheckpointIdx = i;
20664
+ }
20665
+ }
20666
+ const pendingEvents = lastCheckpointIdx >= 0 ? events.slice(lastCheckpointIdx + 1) : events;
20667
+ const lastEv = events[events.length - 1];
20668
+ const inFlightStart = lastEv.type === "in_flight_start" ? lastEv : null;
20669
+ const context = inFlightStart && inFlightStart.type === "in_flight_start" ? inFlightStart.context : null;
20670
+ return {
20671
+ sessionId,
20672
+ stale: inFlightStart !== null,
20673
+ lastCheckpoint,
20674
+ pendingEvents,
20675
+ inFlightStart,
20676
+ context
20677
+ };
20678
+ }
20679
+ /**
20680
+ * List every stale session in a directory. Returns an array
20681
+ * (possibly empty) sorted by `lastEventTs` descending — most
20682
+ * recent crash first.
20683
+ */
20684
+ async listResumable() {
20685
+ let entries;
20686
+ try {
20687
+ entries = await fsp2.readdir(this.dir);
20688
+ } catch (err) {
20689
+ if (err.code === "ENOENT") return [];
20690
+ return [];
20691
+ }
20692
+ const out = [];
20693
+ for (const name of entries) {
20694
+ if (!name.endsWith(".jsonl")) continue;
20695
+ const sessionId = name.slice(0, -".jsonl".length);
20696
+ if (sessionId.includes(".replay") || sessionId.includes(".annotations")) continue;
20697
+ const stale = await this.detectStale(sessionId);
20698
+ if (stale) out.push(stale);
20699
+ }
20700
+ return out.sort((a, b) => b.lastEventTs.localeCompare(a.lastEventTs));
20701
+ }
20702
+ // ── Internals ──────────────────────────────────────────────────────────
20703
+ filePath(sessionId) {
20704
+ if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
20705
+ throw new Error(`Invalid sessionId: ${sessionId}`);
20706
+ }
20707
+ return path6.join(this.dir, `${sessionId}.jsonl`);
20708
+ }
20709
+ /**
20710
+ * Stream-parse the last few lines of a JSONL log. We do NOT load
20711
+ * the whole file into memory — for long-running sessions the log
20712
+ * can be megabytes. Instead we read tail-ward and find the last
20713
+ * `in_flight_start` / `in_flight_end` pair.
20714
+ */
20715
+ async parseForStale(sessionId, fp, raw) {
20716
+ const lines = raw.split("\n");
20717
+ let lastEvent = null;
20718
+ let eventCount = 0;
20719
+ for (const line of lines) {
20720
+ if (!line.trim()) continue;
20721
+ try {
20722
+ const ev = JSON.parse(line);
20723
+ lastEvent = ev;
20724
+ eventCount++;
20725
+ } catch {
20726
+ }
20727
+ }
20728
+ if (!lastEvent) return null;
20729
+ if (lastEvent.type === "in_flight_start") {
20730
+ return {
20731
+ sessionId,
20732
+ path: fp,
20733
+ lastEventTs: lastEvent.ts,
20734
+ context: lastEvent.context,
20735
+ eventCount
20736
+ };
20737
+ }
20738
+ return null;
20739
+ }
20740
+ };
20741
+
20742
+ // src/storage/tool-audit-log.ts
20743
+ init_atomic_write();
20744
+ var GENESIS_PREV = "0".repeat(64);
20745
+ var ToolAuditLog = class {
20746
+ dir;
20747
+ /** In-memory cache of the last entry's hash (per session), to compute chains efficiently. */
20748
+ tailHash = /* @__PURE__ */ new Map();
20749
+ writeChains = /* @__PURE__ */ new Map();
20750
+ constructor(opts) {
20751
+ this.dir = opts.dir;
19127
20752
  }
19128
20753
  /**
19129
- * List all installed skills from the manifest.
20754
+ * Append a tool call/result pair to the chain. Returns the
20755
+ * resulting entry. Idempotency is not guaranteed — if you
20756
+ * record the same tool_use twice you get two entries. That's
20757
+ * intentional: the audit log is a record, not a cache.
19130
20758
  */
19131
- async listInstalled() {
19132
- return this.manifest.listAll();
20759
+ async record(input) {
20760
+ let entry = null;
20761
+ await this.enqueue(input.sessionId, async () => {
20762
+ const prevHash = this.tailHash.get(input.sessionId) ?? GENESIS_PREV;
20763
+ const id = randomUUID();
20764
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
20765
+ const content = {
20766
+ id,
20767
+ ts,
20768
+ prevHash,
20769
+ toolName: input.toolName,
20770
+ toolUseId: input.toolUseId,
20771
+ input: input.input,
20772
+ output: input.output,
20773
+ isError: input.isError
20774
+ };
20775
+ const hash = createHash("sha256").update(stableStringify2(content), "utf8").digest("hex");
20776
+ entry = {
20777
+ index: this.tailHash.has(input.sessionId) ? -1 : 0,
20778
+ // placeholder; recomputed below
20779
+ id,
20780
+ ts,
20781
+ prevHash,
20782
+ hash,
20783
+ toolName: input.toolName,
20784
+ toolUseId: input.toolUseId,
20785
+ input: input.input,
20786
+ output: input.output,
20787
+ isError: input.isError
20788
+ };
20789
+ const existing = await this.readAll(input.sessionId);
20790
+ const index = existing.length;
20791
+ const finalContent = { ...content, index };
20792
+ const finalHash = createHash("sha256").update(stableStringify2(finalContent), "utf8").digest("hex");
20793
+ entry = { ...entry, index, hash: finalHash };
20794
+ await this.appendLine(input.sessionId, entry);
20795
+ this.tailHash.set(input.sessionId, finalHash);
20796
+ });
20797
+ return entry;
19133
20798
  }
19134
- // ── Private helpers ──────────────────────────────────────────────
19135
20799
  /**
19136
- * Detect skills in an extracted repository.
19137
- * Returns an array of detected skills with their files.
20800
+ * Walk the chain and verify every entry's hash and prevHash.
20801
+ * Returns a structured verdict never throws.
19138
20802
  */
19139
- async detectSkills(baseDir) {
19140
- const results = [];
19141
- const rootSkillMd = path6.join(baseDir, "SKILL.md");
19142
- try {
19143
- await fsp2.access(rootSkillMd);
19144
- const content = await fsp2.readFile(rootSkillMd, "utf8");
19145
- const meta = parseFrontmatter2(content);
19146
- if (meta.name && meta.description) {
19147
- results.push({
19148
- name: meta.name,
19149
- baseDir,
19150
- files: ["SKILL.md"]
19151
- });
19152
- return results;
20803
+ async verify(sessionId) {
20804
+ const entries = await this.readAll(sessionId);
20805
+ if (entries.length === 0) return { ok: true, entries: 0 };
20806
+ if (entries[0].prevHash !== GENESIS_PREV) {
20807
+ return {
20808
+ ok: false,
20809
+ brokenAt: 0,
20810
+ reason: "first entry is not the genesis (prevHash != 0\u20260)"
20811
+ };
20812
+ }
20813
+ let prevHash = GENESIS_PREV;
20814
+ for (let i = 0; i < entries.length; i++) {
20815
+ const e = entries[i];
20816
+ if (e.prevHash !== prevHash) {
20817
+ return {
20818
+ ok: false,
20819
+ brokenAt: i,
20820
+ reason: `prevHash mismatch at entry ${i} (expected ${prevHash.slice(0, 8)}\u2026, got ${e.prevHash.slice(0, 8)}\u2026)`
20821
+ };
19153
20822
  }
19154
- } catch {
20823
+ const content = {
20824
+ id: e.id,
20825
+ ts: e.ts,
20826
+ prevHash: e.prevHash,
20827
+ toolName: e.toolName,
20828
+ toolUseId: e.toolUseId,
20829
+ input: e.input,
20830
+ output: e.output,
20831
+ isError: e.isError,
20832
+ index: e.index
20833
+ };
20834
+ const expectedHash = createHash("sha256").update(stableStringify2(content), "utf8").digest("hex");
20835
+ if (expectedHash !== e.hash) {
20836
+ return {
20837
+ ok: false,
20838
+ brokenAt: i,
20839
+ reason: `hash mismatch at entry ${i} (entry content was modified)`
20840
+ };
20841
+ }
20842
+ prevHash = e.hash;
19155
20843
  }
19156
- const skillsDir = path6.join(baseDir, "skills");
20844
+ return { ok: true, entries: entries.length };
20845
+ }
20846
+ /** All entries for a session, in insertion order. */
20847
+ async load(sessionId) {
20848
+ return this.readAll(sessionId);
20849
+ }
20850
+ // ── Internals ────────────────────────────────────────────────────────────
20851
+ filePath(sessionId) {
20852
+ if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
20853
+ throw new Error(`Invalid sessionId: ${sessionId}`);
20854
+ }
20855
+ return path6.join(this.dir, `${sessionId}.audit.jsonl`);
20856
+ }
20857
+ async readAll(sessionId) {
20858
+ const fp = this.filePath(sessionId);
19157
20859
  try {
19158
- const entries = await fsp2.readdir(skillsDir, { withFileTypes: true });
19159
- for (const entry of entries) {
19160
- if (!entry.isDirectory()) continue;
19161
- const skillFile = path6.join(skillsDir, entry.name, "SKILL.md");
20860
+ const raw = await fsp2.readFile(fp, "utf8");
20861
+ const out = [];
20862
+ for (const line of raw.split("\n")) {
20863
+ if (!line.trim()) continue;
19162
20864
  try {
19163
- const content = await fsp2.readFile(skillFile, "utf8");
19164
- const meta = parseFrontmatter2(content);
19165
- if (meta.name && meta.description) {
19166
- const skillDir = path6.join(skillsDir, entry.name);
19167
- const files = await collectFiles(skillDir, skillDir);
19168
- results.push({
19169
- name: meta.name,
19170
- baseDir: skillDir,
19171
- files
19172
- });
19173
- }
20865
+ out.push(JSON.parse(line));
19174
20866
  } catch {
19175
20867
  }
19176
20868
  }
19177
- } catch {
20869
+ return out;
20870
+ } catch (err) {
20871
+ if (err.code === "ENOENT") return [];
20872
+ return [];
19178
20873
  }
19179
- return results;
19180
20874
  }
19181
- /**
19182
- * Remove all files for an installed skill.
19183
- */
19184
- async removeSkillFiles(name, scope) {
19185
- const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
19186
- const skillDir = path6.join(targetDir, name);
19187
- await fsp2.rm(skillDir, { recursive: true, force: true });
20875
+ async appendLine(sessionId, entry) {
20876
+ const fp = this.filePath(sessionId);
20877
+ const existing = await this.readAll(sessionId);
20878
+ const all = [...existing, entry];
20879
+ await atomicWrite(fp, all.map((e) => JSON.stringify(e)).join("\n") + (all.length ? "\n" : ""));
19188
20880
  }
19189
- /**
19190
- * Invalidate the skill loader's cache so newly installed skills appear.
19191
- */
19192
- invalidateLoaderCache() {
19193
- const loader = this.opts.skillLoader;
19194
- if (loader && typeof loader.invalidateCache === "function") {
19195
- loader.invalidateCache();
19196
- }
20881
+ enqueue(sessionId, fn) {
20882
+ const prev = this.writeChains.get(sessionId) ?? Promise.resolve();
20883
+ const next = prev.then(fn, fn);
20884
+ this.writeChains.set(
20885
+ sessionId,
20886
+ next.catch(() => void 0)
20887
+ );
20888
+ return next;
19197
20889
  }
19198
20890
  };
19199
- function parseFrontmatter2(raw) {
19200
- if (!raw.startsWith("---")) return {};
19201
- const end = raw.indexOf("\n---", 4);
19202
- if (end === -1) return {};
19203
- const block = raw.slice(4, end);
19204
- const out = {};
19205
- let key = null;
19206
- let value = [];
19207
- const flush = () => {
19208
- if (key) {
19209
- out[key] = value.join("\n").trim();
19210
- }
19211
- key = null;
19212
- value = [];
19213
- };
19214
- for (const line of block.split("\n")) {
19215
- const m = /^([a-zA-Z_]+):\s*(\|?)\s*(.*)$/.exec(line);
19216
- if (m) {
19217
- flush();
19218
- key = m[1] ?? "";
19219
- const pipe = m[2];
19220
- const rest = m[3] ?? "";
19221
- if (pipe === "|") {
19222
- value = [];
19223
- } else if (rest) {
19224
- value = [rest];
19225
- } else {
19226
- value = [];
19227
- }
19228
- } else if (key) {
19229
- value.push(line.replace(/^\s+/, ""));
19230
- }
19231
- }
19232
- flush();
19233
- return out;
20891
+ function stableStringify2(value) {
20892
+ return JSON.stringify(sortKeys2(value));
19234
20893
  }
19235
- async function collectFiles(dir, baseDir) {
19236
- const results = [];
19237
- const entries = await fsp2.readdir(dir, { withFileTypes: true });
19238
- for (const entry of entries) {
19239
- const fullPath = path6.join(dir, entry.name);
19240
- const relPath = path6.relative(baseDir, fullPath);
19241
- if (entry.isDirectory()) {
19242
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
19243
- results.push(...await collectFiles(fullPath, baseDir));
19244
- } else {
19245
- results.push(relPath);
20894
+ function sortKeys2(value) {
20895
+ if (Array.isArray(value)) return value.map(sortKeys2);
20896
+ if (value && typeof value === "object") {
20897
+ const obj = value;
20898
+ const sorted = {};
20899
+ for (const key of Object.keys(obj).sort()) {
20900
+ sorted[key] = sortKeys2(obj[key]);
19246
20901
  }
20902
+ return sorted;
19247
20903
  }
19248
- return results;
20904
+ return value;
19249
20905
  }
19250
20906
 
19251
20907
  // src/storage/session-rewinder.ts
@@ -19421,7 +21077,7 @@ var DefaultPromptStore = class {
19421
21077
  }
19422
21078
  async list() {
19423
21079
  await ensureDir(this.dir);
19424
- let entries = [];
21080
+ const entries = [];
19425
21081
  try {
19426
21082
  const files = await fsp2.readdir(this.dir);
19427
21083
  for (const file of files) {
@@ -19697,8 +21353,8 @@ var CloudSync = class {
19697
21353
  const localPath = this.categoryToPath(cat);
19698
21354
  if (!localPath) continue;
19699
21355
  try {
19700
- const stat9 = await fsp2.stat(localPath);
19701
- if (stat9.isDirectory()) {
21356
+ const stat8 = await fsp2.stat(localPath);
21357
+ if (stat8.isDirectory()) {
19702
21358
  const files = await this.walkDir(localPath, localPath);
19703
21359
  for (const file of files) {
19704
21360
  const content = await fsp2.readFile(file, "utf8");
@@ -19723,8 +21379,8 @@ var CloudSync = class {
19723
21379
  const localPath = this.categoryToPath(cat);
19724
21380
  if (!localPath) continue;
19725
21381
  try {
19726
- const stat9 = await fsp2.stat(localPath);
19727
- if (stat9.isDirectory()) {
21382
+ const stat8 = await fsp2.stat(localPath);
21383
+ if (stat8.isDirectory()) {
19728
21384
  const files = await this.walkDir(localPath, localPath);
19729
21385
  for (const file of files) {
19730
21386
  const content = await fsp2.readFile(file);
@@ -20325,7 +21981,7 @@ var SkillGenerator = class {
20325
21981
  };
20326
21982
  return [commonInjection, ...stackSpecific[stack] ?? []];
20327
21983
  }
20328
- getConfigPatterns(stack) {
21984
+ getConfigPatterns(_stack) {
20329
21985
  const commonConfig = [
20330
21986
  {
20331
21987
  id: "insecure-tls",
@@ -20907,7 +22563,6 @@ var SecurityScannerOrchestrator = class {
20907
22563
  retryPolicy;
20908
22564
  errorHandler;
20909
22565
  detector = defaultTechStackDetector;
20910
- reportGenerator = defaultReportGenerator;
20911
22566
  gitignoreUpdater = defaultGitignoreUpdater;
20912
22567
  /**
20913
22568
  * Wraps provider.complete with retry logic using the injected RetryPolicy.
@@ -21091,7 +22746,7 @@ Return ONLY the JSON object, no markdown, no explanation.`;
21091
22746
  /**
21092
22747
  * Scan a batch of files using LLM.
21093
22748
  */
21094
- async scanFileBatchLLM(provider, model, files, skill, techStack) {
22749
+ async scanFileBatchLLM(provider, model, files, skill, _techStack) {
21095
22750
  const fileContents = [];
21096
22751
  for (const file of files) {
21097
22752
  try {
@@ -21288,7 +22943,7 @@ Be specific about the vulnerabilities found and how to fix them.`;
21288
22943
  /**
21289
22944
  * Gather project info for skill generation.
21290
22945
  */
21291
- async gatherProjectInfo(projectRoot, techStack) {
22946
+ async gatherProjectInfo(projectRoot, _techStack) {
21292
22947
  const info = [];
21293
22948
  const keyFiles = [
21294
22949
  "package.json",
@@ -21320,7 +22975,7 @@ ${dirs.join(", ")}`);
21320
22975
  /**
21321
22976
  * Gather files to scan based on patterns.
21322
22977
  */
21323
- async gatherFiles(root, patterns, depth) {
22978
+ async gatherFiles(root, _patterns, depth) {
21324
22979
  const files = [];
21325
22980
  const maxDepth = depth === "quick" ? 2 : depth === "deep" ? 20 : 5;
21326
22981
  const extensions = [".ts", ".js", ".jsx", ".tsx", ".py", ".go", ".java", ".cs", ".rs"];
@@ -21579,22 +23234,22 @@ Use \`/security report <number>\` to view a specific report.` };
21579
23234
  }
21580
23235
  const index = Number.parseInt(reportId, 10) - 1;
21581
23236
  if (!Number.isNaN(index) && reports[index]) {
21582
- const { readFile: readFile35 } = await import('fs/promises');
21583
- const content = await readFile35(join(reportsDir, reports[index]), "utf-8");
23237
+ const { readFile: readFile37 } = await import('fs/promises');
23238
+ const content = await readFile37(join(reportsDir, reports[index]), "utf-8");
21584
23239
  return { message: `# Security Report
21585
23240
 
21586
23241
  ${content}` };
21587
23242
  }
21588
23243
  const match = reports.find((r) => r.includes(reportId));
21589
23244
  if (match) {
21590
- const { readFile: readFile35 } = await import('fs/promises');
21591
- const content = await readFile35(join(reportsDir, match), "utf-8");
23245
+ const { readFile: readFile37 } = await import('fs/promises');
23246
+ const content = await readFile37(join(reportsDir, match), "utf-8");
21592
23247
  return { message: `# Security Report
21593
23248
 
21594
23249
  ${content}` };
21595
23250
  }
21596
23251
  return { message: `\u274C Report "${reportId}" not found. Use \`/security report\` to see available reports.` };
21597
- } catch (error) {
23252
+ } catch (_error) {
21598
23253
  return { message: "\u{1F4ED} No security reports found. Run `/security scan` first." };
21599
23254
  }
21600
23255
  }
@@ -21647,7 +23302,6 @@ var FleetManager = class {
21647
23302
  /** Usage rollup across all subagents. */
21648
23303
  usage;
21649
23304
  manifestPath;
21650
- sessionsRoot;
21651
23305
  directorRunId;
21652
23306
  /** Spawn cap (lifetime total). Infinity means unlimited. */
21653
23307
  maxSpawns;
@@ -21675,9 +23329,14 @@ var FleetManager = class {
21675
23329
  _usedNicknames = /* @__PURE__ */ new Set();
21676
23330
  /** The coordinator (wired via setCoordinator by Director after construction). */
21677
23331
  coordinator = null;
23332
+ /** Leader agent's current context pressure (full request tokens). */
23333
+ leaderContextPressure = 0;
23334
+ /** Maximum context load fraction before spawn is refused. */
23335
+ maxLeaderContextLoad;
23336
+ /** Provider's max context window in tokens. */
23337
+ maxContext;
21678
23338
  constructor(opts = {}) {
21679
23339
  this.manifestPath = opts.manifestPath;
21680
- this.sessionsRoot = opts.sessionsRoot;
21681
23340
  this.directorRunId = opts.directorRunId ?? randomUUID();
21682
23341
  this.maxSpawns = opts.maxSpawns ?? Number.POSITIVE_INFINITY;
21683
23342
  this.maxSpawnDepth = opts.maxSpawnDepth ?? 2;
@@ -21685,6 +23344,8 @@ var FleetManager = class {
21685
23344
  this.sessionWriter = opts.sessionWriter ?? null;
21686
23345
  this.manifestDebounceMs = opts.manifestDebounceMs ?? 2e3;
21687
23346
  this.maxFleetCostUsd = opts.directorBudget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
23347
+ this.maxLeaderContextLoad = opts.maxLeaderContextLoad ?? 0.85;
23348
+ this.maxContext = opts.maxContext ?? 128e3;
21688
23349
  this.stateCheckpoint = opts.stateCheckpointPath ? new DirectorStateCheckpoint(
21689
23350
  opts.stateCheckpointPath,
21690
23351
  {
@@ -21699,7 +23360,7 @@ var FleetManager = class {
21699
23360
  this.fleet = new FleetBus();
21700
23361
  this.usage = new FleetUsageAggregator(
21701
23362
  this.fleet,
21702
- (id, provider, model) => {
23363
+ (_id, provider, model) => {
21703
23364
  if (provider && model) return this.priceLookups.get(`${provider}/${model}`);
21704
23365
  return void 0;
21705
23366
  },
@@ -21738,7 +23399,7 @@ var FleetManager = class {
21738
23399
  * which cap was exceeded. Does NOT throw — the caller decides
21739
23400
  * how to surface the rejection.
21740
23401
  */
21741
- canSpawn(config) {
23402
+ canSpawn(_config) {
21742
23403
  if (this.spawnDepth >= this.maxSpawnDepth) {
21743
23404
  return { kind: "max_spawn_depth", limit: this.maxSpawnDepth, observed: this.spawnDepth };
21744
23405
  }
@@ -21751,8 +23412,21 @@ var FleetManager = class {
21751
23412
  return { kind: "max_cost_usd", limit: this.maxFleetCostUsd, observed: totalCost };
21752
23413
  }
21753
23414
  }
23415
+ if (this.maxLeaderContextLoad < 1) {
23416
+ const threshold = this.maxContext * this.maxLeaderContextLoad;
23417
+ if (this.leaderContextPressure >= threshold) {
23418
+ return {
23419
+ kind: "max_context_load",
23420
+ limit: threshold,
23421
+ observed: this.leaderContextPressure
23422
+ };
23423
+ }
23424
+ }
21754
23425
  return null;
21755
23426
  }
23427
+ setLeaderContextPressure(tokens) {
23428
+ this.leaderContextPressure = tokens;
23429
+ }
21756
23430
  /**
21757
23431
  * Assign a memorable nickname (e.g. "Einstein (Bug Hunter)") to the config,
21758
23432
  * record it so the same name is never reused, then record the spawn.
@@ -21977,7 +23651,7 @@ function createMcpControlTool(opts) {
21977
23651
  mutating: true,
21978
23652
  riskTier: "standard",
21979
23653
  inputSchema,
21980
- async execute(raw, ctx) {
23654
+ async execute(raw) {
21981
23655
  const input = raw;
21982
23656
  return mcpControlDispatch(input, { getConfig, configPath, registry });
21983
23657
  }
@@ -22525,7 +24199,6 @@ var Agent = class {
22525
24199
  pipelines;
22526
24200
  ctx;
22527
24201
  maxIterations;
22528
- iterationTimeoutMs;
22529
24202
  executionStrategy;
22530
24203
  perIterationOutputCapBytes;
22531
24204
  plugins = [];
@@ -22543,7 +24216,6 @@ var Agent = class {
22543
24216
  this.pipelines = init.pipelines;
22544
24217
  this.ctx = init.context;
22545
24218
  this.maxIterations = init.maxIterations ?? DEFAULT_MAX_ITERATIONS;
22546
- this.iterationTimeoutMs = init.iterationTimeoutMs ?? 3e5;
22547
24219
  this.executionStrategy = init.executionStrategy ?? "smart";
22548
24220
  this.perIterationOutputCapBytes = init.perIterationOutputCapBytes ?? 1e5;
22549
24221
  this.autoExtendLimit = init.autoExtendLimit ?? true;
@@ -22565,9 +24237,6 @@ var Agent = class {
22565
24237
  get permission() {
22566
24238
  return this.container.resolve(TOKENS.PermissionPolicy);
22567
24239
  }
22568
- get scrubber() {
22569
- return this.container.resolve(TOKENS.SecretScrubber);
22570
- }
22571
24240
  get renderer() {
22572
24241
  return this.container.has(TOKENS.Renderer) ? this.container.resolve(TOKENS.Renderer) : void 0;
22573
24242
  }
@@ -22697,6 +24366,11 @@ var Agent = class {
22697
24366
  if (controller.signal.aborted) {
22698
24367
  return { status: "aborted", iterations };
22699
24368
  }
24369
+ await this.ctx.session.writeInFlightMarker(`iteration ${i} / max ${this.maxIterations}`).catch((err) => {
24370
+ this.logger.debug?.(
24371
+ `in-flight marker write failed: ${err instanceof Error ? err.message : String(err)}`
24372
+ );
24373
+ });
22700
24374
  if (autonomousContinue) {
22701
24375
  consumeAutonomousContinue(this.ctx);
22702
24376
  }
@@ -22715,6 +24389,15 @@ var Agent = class {
22715
24389
  this.events.emit("iteration.started", { ctx: this.ctx, index: i });
22716
24390
  this.injectPendingBtwNotes();
22717
24391
  const req = await this.buildAndRunRequestPipeline(opts);
24392
+ await this.ctx.session.append({
24393
+ type: "llm_request",
24394
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
24395
+ model: req.model,
24396
+ messageCount: req.messages.length,
24397
+ estimatedInputTokens: estimateRequestTokens(req.messages, req.system, req.tools ?? []).total,
24398
+ toolCount: (req.tools ?? []).length
24399
+ }).catch(() => {
24400
+ });
22718
24401
  let res;
22719
24402
  try {
22720
24403
  res = await customRunner(this.ctx, req);
@@ -22778,6 +24461,7 @@ var Agent = class {
22778
24461
  finalText = responseResult.finalText;
22779
24462
  const toolUses = res.content.filter(isToolUseBlock);
22780
24463
  if (toolUses.length === 0) {
24464
+ this.emitContextPct();
22781
24465
  this.events.emit("iteration.completed", { ctx: this.ctx, index: i });
22782
24466
  if (autonomousContinue && responseResult.directive === "continue") {
22783
24467
  await this.compactContextIfNeeded();
@@ -22791,11 +24475,13 @@ var Agent = class {
22791
24475
  }
22792
24476
  await this.executeTools(toolUses);
22793
24477
  if (autonomousContinue && consumeAutonomousContinue(this.ctx)) {
24478
+ this.emitContextPct();
22794
24479
  this.events.emit("iteration.completed", { ctx: this.ctx, index: i });
22795
24480
  await this.compactContextIfNeeded();
22796
24481
  await this.extensions.runAfterIteration(this.ctx, i);
22797
24482
  continue;
22798
24483
  }
24484
+ this.emitContextPct();
22799
24485
  this.events.emit("iteration.completed", { ctx: this.ctx, index: i });
22800
24486
  await this.compactContextIfNeeded();
22801
24487
  await this.extensions.runAfterIteration(this.ctx, i);
@@ -22808,6 +24494,12 @@ var Agent = class {
22808
24494
  }
22809
24495
  } finally {
22810
24496
  offSubagentDone();
24497
+ const reason = controller.signal.aborted ? "aborted" : "clean";
24498
+ await this.ctx.session.clearInFlightMarker(reason).catch((err) => {
24499
+ this.logger.debug?.(
24500
+ `in-flight marker clear failed: ${err instanceof Error ? err.message : String(err)}`
24501
+ );
24502
+ });
22811
24503
  }
22812
24504
  }
22813
24505
  /**
@@ -22915,6 +24607,7 @@ var Agent = class {
22915
24607
  this.executionStrategy
22916
24608
  );
22917
24609
  const useById = new Map(selectedToolUses.map((u) => [u.id, u]));
24610
+ const resultsForMessage = [];
22918
24611
  for (const { result, tool, durationMs } of outputs) {
22919
24612
  if (result.type === "tool_confirm_pending") {
22920
24613
  const decision = await this.waitForConfirm({
@@ -22999,8 +24692,10 @@ var Agent = class {
22999
24692
  });
23000
24693
  }
23001
24694
  }
24695
+ resultsForMessage.push(reRunResult.result);
23002
24696
  continue;
23003
24697
  }
24698
+ resultsForMessage.push(result);
23004
24699
  const use = useById.get(result.tool_use_id);
23005
24700
  if (!use) continue;
23006
24701
  await this.pipelines.toolCall.run({
@@ -23033,7 +24728,7 @@ var Agent = class {
23033
24728
  }
23034
24729
  this.ctx.state.appendMessage({
23035
24730
  role: "user",
23036
- content: outputs.map((o) => o.result)
24731
+ content: resultsForMessage
23037
24732
  });
23038
24733
  await this.extensions.runAfterToolExecution(this.ctx, outputs);
23039
24734
  }
@@ -23100,6 +24795,19 @@ var Agent = class {
23100
24795
  async compactContextIfNeeded() {
23101
24796
  await this.pipelines.contextWindow.run(this.ctx);
23102
24797
  }
24798
+ /**
24799
+ * Emit the current context window load as a `ctx.pct` event so subscribers
24800
+ * (FleetBus → TUI) can render a live fill bar per agent.
24801
+ */
24802
+ emitContextPct() {
24803
+ const maxContext = this.ctx.provider.capabilities.maxContext ?? 2e5;
24804
+ const { total } = estimateRequestTokens(
24805
+ this.ctx.messages,
24806
+ this.ctx.systemPrompt,
24807
+ this.ctx.tools ?? []
24808
+ );
24809
+ this.events.emit("ctx.pct", { load: total / maxContext, tokens: total, maxContext });
24810
+ }
23103
24811
  };
23104
24812
  function toError(err) {
23105
24813
  return err instanceof Error ? err : new Error(String(err));
@@ -23385,6 +25093,50 @@ var InputBuilder = class {
23385
25093
  this.refs.push(ref);
23386
25094
  return placeholder;
23387
25095
  }
25096
+ /**
25097
+ * Register-only variant of `appendPaste`. Always stores the paste and
25098
+ * returns the inline `[pasted #N, L lines]` token WITHOUT mutating
25099
+ * `display` — the caller (TUI) owns its own editable buffer as the single
25100
+ * source of truth, inserts the token there, and expands the buffer at
25101
+ * submit. The collapse decision is the caller's (it gates this call); use
25102
+ * `wouldCollapse()` for that.
25103
+ */
25104
+ async registerPaste(text) {
25105
+ const ref = await this.store.add({
25106
+ kind: "text",
25107
+ data: text,
25108
+ meta: { label: paragraphLabel(text) }
25109
+ });
25110
+ this.refs.push(ref);
25111
+ const lines = text.split("\n").length;
25112
+ return `[pasted #${ref.seq}, ${lines} lines]`;
25113
+ }
25114
+ /**
25115
+ * Register-only variant of `appendImage` — see `registerPaste`. Returns a
25116
+ * seq-keyed `[image #N, LABEL]` token; does not touch `display`.
25117
+ */
25118
+ async registerImage(dataBase64, mediaType) {
25119
+ const label = `${mediaType.split("/")[1]?.toUpperCase() ?? "IMG"}`;
25120
+ const ref = await this.store.add({
25121
+ kind: "image",
25122
+ data: dataBase64,
25123
+ meta: { mediaType, label }
25124
+ });
25125
+ this.refs.push(ref);
25126
+ return `[image #${ref.seq}, ${label}]`;
25127
+ }
25128
+ /**
25129
+ * Register-only variant of `appendFile` — see `registerPaste`. Returns a
25130
+ * path-keyed `[file:<path>]` token (resolved by path at expand time); does
25131
+ * not touch `display`. The path is read from `meta.filename` (falling back
25132
+ * to `meta.label`).
25133
+ */
25134
+ async registerFile(input) {
25135
+ const ref = await this.store.add({ ...input, kind: "file" });
25136
+ this.refs.push(ref);
25137
+ const path35 = ref.meta.filename ?? ref.meta.label ?? String(ref.seq);
25138
+ return `[file:${path35}]`;
25139
+ }
23388
25140
  /**
23389
25141
  * Whether `appendPaste(text)` would collapse the text to a placeholder
23390
25142
  * (rather than inlining it). Lets a frontend decide where to route a paste
@@ -23776,8 +25528,8 @@ ${mem}`);
23776
25528
  }
23777
25529
  async dirExists(p) {
23778
25530
  try {
23779
- const stat9 = await fsp2.stat(p);
23780
- return stat9.isDirectory();
25531
+ const stat8 = await fsp2.stat(p);
25532
+ return stat8.isDirectory();
23781
25533
  } catch {
23782
25534
  return false;
23783
25535
  }
@@ -23867,6 +25619,14 @@ var ToolRegistry = class _ToolRegistry {
23867
25619
  context: { tool: tool.name }
23868
25620
  });
23869
25621
  }
25622
+ if (!tool.inputSchema || typeof tool.inputSchema !== "object") {
25623
+ throw new WrongStackError({
25624
+ message: `Tool "${tool.name}" has an invalid or missing inputSchema`,
25625
+ code: ERROR_CODES.REGISTRY_INVALID,
25626
+ subsystem: "container",
25627
+ context: { tool: tool.name }
25628
+ });
25629
+ }
23870
25630
  this.tools.set(tool.name, { tool, owner });
23871
25631
  }
23872
25632
  /**
@@ -23876,6 +25636,9 @@ var ToolRegistry = class _ToolRegistry {
23876
25636
  */
23877
25637
  tryRegister(tool, owner = "core") {
23878
25638
  if (this.tools.has(tool.name)) return false;
25639
+ if (!tool.inputSchema || typeof tool.inputSchema !== "object") {
25640
+ return false;
25641
+ }
23879
25642
  this.tools.set(tool.name, { tool, owner });
23880
25643
  return true;
23881
25644
  }
@@ -26113,6 +27876,203 @@ function assertSafePath(dir, projectRoot) {
26113
27876
  }
26114
27877
  }
26115
27878
 
27879
+ // src/coordination/collab-bus.ts
27880
+ var CollaborationBus = class {
27881
+ pausePromise = null;
27882
+ pauseResolve = null;
27883
+ pausedAtMs = null;
27884
+ pausedBy = null;
27885
+ // ── State queries ──────────────────────────────────────────────────────
27886
+ isPaused() {
27887
+ return this.pausePromise !== null;
27888
+ }
27889
+ getState() {
27890
+ return {
27891
+ paused: this.isPaused(),
27892
+ pausedAt: this.pausedAtMs ? new Date(this.pausedAtMs).toISOString() : null,
27893
+ pausedBy: this.pausedBy
27894
+ };
27895
+ }
27896
+ // ── Pause / resume control ─────────────────────────────────────────────
27897
+ /**
27898
+ * Pause the agent loop. Idempotent: a second `requestPause` while
27899
+ * already paused is a no-op (the original pause wins; we do not
27900
+ * overwrite `pausedBy`). Returns true when the state actually
27901
+ * transitioned, false when it was already paused.
27902
+ */
27903
+ requestPause(byParticipant) {
27904
+ if (this.isPaused()) return false;
27905
+ this.pausedAtMs = Date.now();
27906
+ this.pausedBy = byParticipant;
27907
+ this.pausePromise = new Promise((resolve9) => {
27908
+ this.pauseResolve = resolve9;
27909
+ });
27910
+ return true;
27911
+ }
27912
+ /**
27913
+ * Resume the agent loop. Returns true when the state actually
27914
+ * transitioned from paused → running, false when it was already
27915
+ * running (no-op).
27916
+ */
27917
+ resume() {
27918
+ if (!this.isPaused()) return false;
27919
+ if (this.pauseResolve) this.pauseResolve();
27920
+ this.pausePromise = null;
27921
+ this.pauseResolve = null;
27922
+ this.pausedAtMs = null;
27923
+ this.pausedBy = null;
27924
+ return true;
27925
+ }
27926
+ // ── Wait semantics (consumed by the middleware) ────────────────────────
27927
+ /**
27928
+ * Block until the bus is resumed or the timeout fires. Returns:
27929
+ * - `true` → bus was resumed in time
27930
+ * - `false` → timeout fired; bus was auto-resumed as a side effect
27931
+ *
27932
+ * When `timeoutMs` is `0` the wait is unbounded (the middleware must
27933
+ * be paired with an external AbortSignal in that case — we don't
27934
+ * expose one here to keep the API simple).
27935
+ */
27936
+ async waitForResume(timeoutMs) {
27937
+ if (!this.isPaused()) return true;
27938
+ if (!this.pausePromise) return true;
27939
+ if (timeoutMs === 0) {
27940
+ await this.pausePromise;
27941
+ return true;
27942
+ }
27943
+ let timer;
27944
+ const timeoutPromise = new Promise((resolve9) => {
27945
+ timer = setTimeout(() => resolve9("timeout"), timeoutMs);
27946
+ });
27947
+ const resumedPromise = this.pausePromise.then(() => "resumed");
27948
+ const winner = await Promise.race([resumedPromise, timeoutPromise]);
27949
+ if (timer) clearTimeout(timer);
27950
+ if (winner === "timeout") {
27951
+ this.resume();
27952
+ return false;
27953
+ }
27954
+ return true;
27955
+ }
27956
+ // ── Manual tool-call injection (Phase 4) ─────────────────────────────────
27957
+ //
27958
+ // A controller can ask the agent loop to use a specific tool_result
27959
+ // for a given tool_use_id — bypassing the real tool execution. This
27960
+ // is "I want the agent to think the read returned THIS content" or
27961
+ // "skip the bash call, just give it the answer I typed". The
27962
+ // injection is matched by tool_use_id, consumed once, and discarded.
27963
+ injectionQueue = /* @__PURE__ */ new Map();
27964
+ /**
27965
+ * Queue a manual tool result. The next time the agent's toolCall
27966
+ * pipeline sees a matching `toolUse.id`, the
27967
+ * `collabInjectMiddleware` consumes this entry and replaces the
27968
+ * real tool execution. Returns `false` if an injection for the
27969
+ * same id is already queued (idempotent — first write wins).
27970
+ */
27971
+ injectToolResult(input) {
27972
+ if (this.injectionQueue.has(input.toolUseId)) return false;
27973
+ this.injectionQueue.set(input.toolUseId, input);
27974
+ return true;
27975
+ }
27976
+ /**
27977
+ * Pop an injection from the queue. Returns the payload (and
27978
+ * removes it) if one is pending, or `null` when nothing matches.
27979
+ * Called by the middleware on every tool call.
27980
+ */
27981
+ takeInjection(toolUseId) {
27982
+ const v = this.injectionQueue.get(toolUseId);
27983
+ if (!v) return null;
27984
+ this.injectionQueue.delete(toolUseId);
27985
+ return v;
27986
+ }
27987
+ /** Inspect the queue size (for observability / tests). */
27988
+ pendingInjectionCount() {
27989
+ return this.injectionQueue.size;
27990
+ }
27991
+ };
27992
+
27993
+ // src/middleware/collab-pause.ts
27994
+ function collabPauseMiddleware(bus, opts = {}) {
27995
+ const timeoutMs = opts.defaultTimeoutMs ?? 6e4;
27996
+ const logger = opts.logger;
27997
+ return async function collabPause(payload, next) {
27998
+ if (!bus.isPaused()) {
27999
+ return next();
28000
+ }
28001
+ const state = bus.getState();
28002
+ logger?.debug?.(
28003
+ `collab-pause: tool '${payload.toolUse.name}' blocked \u2014 bus paused by ${state.pausedBy ?? "?"} at ${state.pausedAt ?? "?"}, waiting up to ${timeoutMs}ms for resume`
28004
+ );
28005
+ const resumed = await bus.waitForResume(timeoutMs);
28006
+ if (!resumed) {
28007
+ logger?.warn?.(
28008
+ `collab-pause: timeout after ${timeoutMs}ms \u2014 auto-resuming the bus to unblock the agent loop`
28009
+ );
28010
+ } else {
28011
+ logger?.debug?.(`collab-pause: resumed \u2014 proceeding with tool '${payload.toolUse.name}'`);
28012
+ }
28013
+ return next();
28014
+ };
28015
+ }
28016
+ function collabInjectMiddleware(bus, opts = {}) {
28017
+ const logger = opts.logger;
28018
+ return async function collabInject(payload, next) {
28019
+ const injected = bus.takeInjection(payload.toolUse.id);
28020
+ if (!injected) {
28021
+ return next();
28022
+ }
28023
+ logger?.debug?.(
28024
+ `collab-inject: tool '${payload.toolUse.name}' (id ${payload.toolUse.id}) \u2014 using controller-injected result (reason: ${injected.reason})`
28025
+ );
28026
+ payload.result = {
28027
+ type: "tool_result",
28028
+ tool_use_id: payload.toolUse.id,
28029
+ content: typeof injected.content === "string" ? injected.content : JSON.stringify(injected.content),
28030
+ is_error: injected.isError
28031
+ };
28032
+ };
28033
+ }
28034
+
28035
+ // src/replay/replay-provider-runner.ts
28036
+ var ReplayProviderRunner = class {
28037
+ constructor(inner, opts) {
28038
+ this.inner = inner;
28039
+ this.opts = opts;
28040
+ }
28041
+ inner;
28042
+ opts;
28043
+ async run(runOpts) {
28044
+ const hash = hashRequest(runOpts.request);
28045
+ const cached = await this.opts.log.lookup(this.opts.sessionId, hash);
28046
+ if (this.opts.mode === "replay") {
28047
+ if (!cached) {
28048
+ this.opts.logger?.warn?.(
28049
+ `replay: no recorded response for hash ${hash} (model ${runOpts.request.model})`
28050
+ );
28051
+ throw new Error(
28052
+ `ReplayProviderRunner: no recorded response for hash ${hash} in session ${this.opts.sessionId}. Either the request changed since recording, or this session has no replay log.`
28053
+ );
28054
+ }
28055
+ this.opts.logger?.debug?.(
28056
+ `replay: served cached response for hash ${hash} (recorded ${cached.ts})`
28057
+ );
28058
+ return cached.response;
28059
+ }
28060
+ if (this.opts.mode === "auto" && cached) {
28061
+ this.opts.logger?.debug?.(
28062
+ `replay: auto-hit hash ${hash}, served cached response`
28063
+ );
28064
+ return cached.response;
28065
+ }
28066
+ const response = await this.inner.run(runOpts);
28067
+ await this.opts.log.record({
28068
+ sessionId: this.opts.sessionId,
28069
+ request: runOpts.request,
28070
+ response
28071
+ });
28072
+ return response;
28073
+ }
28074
+ };
28075
+
26116
28076
  // src/plugins/prompts-plugin.ts
26117
28077
  function createPromptsPlugin(opts) {
26118
28078
  let store = null;
@@ -27170,6 +29130,6 @@ ${formatPlan(updated)}`
27170
29130
  };
27171
29131
  }
27172
29132
 
27173
- export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, ALL_SYNC_CATEGORIES, AUDIT_LOG_AGENT, Agent, AgentError, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutoPhasePlanner, AutoPhaseRunner, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, CONTEXT_WINDOW_MODES, CheckpointManager, CloudSync, ConfigError, ConfigMigrationError, Container, Context, ConversationState, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultPromptStore, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorStateCheckpoint, DoneConditionChecker, ERROR_CODES, EternalAutonomyEngine, EventBus, ExtensionRegistry, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FleetBus, FleetCostCapError, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, FsError, GitignoreUpdater, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, LAYER_1_IDENTITY, LLMSelector, MAX_JOURNAL_ENTRIES, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, PhaseGraphBuilder, PhaseOrchestrator, PhaseStore, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, ReportGenerator, RunController, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, ScopedEventBus, SddParallelRun, SddTaskDecomposer, SecurityScanner, SecurityScannerOrchestrator, SelectiveCompactor, SessionAnalyzer, SessionError, SkillGenerator, SkillInstaller, SkillManifestStore, SlashCommandRegistry, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TOKENS, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, TechStackDetector, ToolError, ToolExecutor, ToolRegistry, WorktreeManager, WrongStackError, addPlanItem, allServers, analyzeCriticalPath, appendJournal, applyRosterBudget, asBlocks, asText, assertSafePath, atomicWrite, attachAutoExtend, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildBtwBlock, buildChildEnv, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildRecoveryStrategies, classifyFamily, clearPlan, color, compileGlob, compileUserRegex, completePartialObject, composeDirectorPrompt, composeSubagentPrompt, computeTaskProgress, consumeBtwNotes, context7Server, contextManagerTool, createAutoExecutor, createAutoPhaseFromTaskGraph, createContextManagerTool, createDefaultPipelines, createDelegateTool, createGitPlugin, createMcpControlTool, createMessage, createObservabilityPlugin, createPlanPlugin, createPromptsPlugin, createSecurityPlugin, createSecuritySlashCommand, createSkillsPlugin, createSyncPlugin, createToolOutputSerializer, decryptConfigSecrets, defaultGitignoreUpdater, defaultOrchestrator, defaultReportGenerator, defaultSecurityScanner, defaultSkillGenerator, defaultTechStackDetector, deriveTodosFromPlanItem, detectNewlineStyle, dispatchAgent, downloadGitHubTarball, emptyGoal, emptyPlan, encryptConfigSecrets, ensureDir, estimateRequestTokens, estimateTextTokens, estimateToolDefTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, extractRunEnv, filesystemServer, findCriticalPath, formatContextWindowModeList, formatGoal, formatPlan, formatPlanTemplates, formatTodosList, getAgentDefinition, getContextWindowMode, getPlanTemplate, getTemplate, githubServer, goalFilePath, googleMapsServer, isAgentError, isConfigError, isContextWindowModeId, isFsError, isImageBlock, isPluginError, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, listContextWindowModes, listPlanTemplates, listTemplates, loadDirectorState, loadGoal, loadPlan, loadPlugins, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeContinueToNextIterationTool, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeRollUpTool, makeSpawnTool, makeTerminateTool, matchAny, matchGlob, migratePlaintextSecrets, miniMaxVisionServer, normalizeToLf, parseContinueDirective, parseSkillRef, pendingBtwCount, projectHash, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, repairToolUseAdjacency, resolveContextWindowPolicy, resolveWstackPaths, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, safeParse, safeStringify, sanitizeJsonString, saveGoal, savePlan, saveTodosCheckpoint, scoreAgents, securitySlashCommand, sentinelServer, setBtwNote, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, stripAnsi, summarizeUsage, templateToMarkdown, toStyle, toWrongStackError, topologicalSort, unifiedDiff, unloadPlugins, validateAgainstSchema, wireMetricsToEvents, wrapAsState, zaiVisionServer };
29133
+ export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, ALL_SYNC_CATEGORIES, AUDIT_LOG_AGENT, Agent, AgentError, AnnotationsStore, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutoPhasePlanner, AutoPhaseRunner, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, CONTEXT_WINDOW_MODES, CORE_RECONSTRUCT_EVENTS, CheckpointManager, CloudSync, CollaborationBus, ConfigError, ConfigMigrationError, Container, Context, ConversationState, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SESSION_LOGGING_CONFIG, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultPromptStore, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorStateCheckpoint, DoneConditionChecker, ERROR_CODES, EternalAutonomyEngine, EventBus, ExtensionRegistry, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FleetBus, FleetCostCapError, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, FsError, GitignoreUpdater, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, LAYER_1_IDENTITY, LLMSelector, MAX_JOURNAL_ENTRIES, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, PhaseGraphBuilder, PhaseOrchestrator, PhaseStore, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, ReplayLogStore, ReplayProviderRunner, ReportGenerator, RunController, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, STANDARD_AUDIT_EVENTS, ScopedEventBus, SddParallelRun, SddTaskDecomposer, SecurityScanner, SecurityScannerOrchestrator, SelectiveCompactor, SessionAnalyzer, SessionError, SessionRecovery, SkillGenerator, SkillInstaller, SkillManifestStore, SlashCommandRegistry, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TOKENS, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, TechStackDetector, ToolAuditLog, ToolError, ToolExecutor, ToolRegistry, WorktreeManager, WrongStackError, addPlanItem, allServers, analyzeCriticalPath, appendJournal, applyRosterBudget, asBlocks, asText, assertSafePath, atomicWrite, attachAutoExtend, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildBtwBlock, buildChildEnv, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildRecoveryStrategies, classifyFamily, clearPlan, collabInjectMiddleware, collabPauseMiddleware, color, compileGlob, compileUserRegex, completePartialObject, composeDirectorPrompt, composeSubagentPrompt, computeTaskProgress, consumeBtwNotes, context7Server, contextManagerTool, createAutoExecutor, createAutoPhaseFromTaskGraph, createContextManagerTool, createDefaultPipelines, createDelegateTool, createGitPlugin, createMcpControlTool, createMessage, createObservabilityPlugin, createPlanPlugin, createPromptsPlugin, createSecurityPlugin, createSecuritySlashCommand, createSessionEventBridge, createSkillsPlugin, createSyncPlugin, createToolOutputSerializer, decryptConfigSecrets, defaultGitignoreUpdater, defaultOrchestrator, defaultReportGenerator, defaultSecurityScanner, defaultSkillGenerator, defaultTechStackDetector, deriveTodosFromPlanItem, detectNewlineStyle, dispatchAgent, downloadGitHubTarball, emptyGoal, emptyPlan, encryptConfigSecrets, ensureDir, estimateRequestTokens, estimateTextTokens, estimateToolDefTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, extractRunEnv, filesystemServer, findCriticalPath, formatContextWindowModeList, formatGoal, formatPlan, formatPlanTemplates, formatTodosList, getAgentDefinition, getContextWindowMode, getPlanTemplate, getTemplate, githubServer, goalFilePath, googleMapsServer, hashRequest, isAgentError, isConfigError, isContextWindowModeId, isFsError, isImageBlock, isPluginError, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, listContextWindowModes, listPlanTemplates, listTemplates, loadDirectorState, loadGoal, loadPlan, loadPlugins, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeContinueToNextIterationTool, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeRollUpTool, makeSpawnTool, makeTerminateTool, matchAny, matchGlob, mergeModelsPayload, migratePlaintextSecrets, miniMaxVisionServer, normalizeToLf, parseContinueDirective, parseSkillRef, pendingBtwCount, projectHash, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, repairToolUseAdjacency, resolveAuditLevel, resolveContextWindowPolicy, resolveSessionLoggingConfig, resolveWstackPaths, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, runProviderWithRetry, safeParse, safeStringify, sanitizeJsonString, saveGoal, savePlan, saveTodosCheckpoint, scoreAgents, securitySlashCommand, sentinelServer, setBtwNote, setPlanItemStatus, slackServer, stableStringify, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, stripAnsi, summarizeUsage, templateToMarkdown, toStyle, toWrongStackError, topologicalSort, unifiedDiff, unloadPlugins, validateAgainstSchema, wireMetricsToEvents, wrapAsState, zaiVisionServer };
27174
29134
  //# sourceMappingURL=index.js.map
27175
29135
  //# sourceMappingURL=index.js.map