@wrongstack/core 0.32.0 → 0.41.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 (33) hide show
  1. package/dist/{agent-subagent-runner-DpZTLdBe.d.ts → agent-subagent-runner-C66vi4Gq.d.ts} +1 -1
  2. package/dist/{config-BUEGM4JP.d.ts → config-ZRCf7sTu.d.ts} +21 -1
  3. package/dist/coordination/index.d.ts +7 -7
  4. package/dist/coordination/index.js +3028 -2976
  5. package/dist/coordination/index.js.map +1 -1
  6. package/dist/defaults/index.d.ts +8 -8
  7. package/dist/defaults/index.js +1166 -1104
  8. package/dist/defaults/index.js.map +1 -1
  9. package/dist/execution/index.d.ts +5 -5
  10. package/dist/extension/index.d.ts +2 -2
  11. package/dist/{index-ysfO_DlX.d.ts → index-6_csX32J.d.ts} +1 -1
  12. package/dist/{index-pXJdVLe0.d.ts → index-DkVgH3wC.d.ts} +31 -1
  13. package/dist/index.d.ts +135 -15
  14. package/dist/index.js +1467 -1214
  15. package/dist/index.js.map +1 -1
  16. package/dist/infrastructure/index.d.ts +2 -2
  17. package/dist/infrastructure/index.js +17 -3
  18. package/dist/infrastructure/index.js.map +1 -1
  19. package/dist/kernel/index.d.ts +2 -2
  20. package/dist/{mcp-servers-BzB3r7_c.d.ts → mcp-servers-DONdo-XM.d.ts} +1 -1
  21. package/dist/{multi-agent-coordinator-DOXSgtom.d.ts → multi-agent-coordinator-BUsjiRWl.d.ts} +1 -1
  22. package/dist/{null-fleet-bus-DLsUjOyB.d.ts → null-fleet-bus-FvgHnZah.d.ts} +150 -131
  23. package/dist/{plan-templates-BZMi-VpU.d.ts → plan-templates-DYCeRCDN.d.ts} +1 -1
  24. package/dist/sdd/index.d.ts +3 -3
  25. package/dist/storage/index.d.ts +2 -2
  26. package/dist/{tool-executor-BAi4WI2d.d.ts → tool-executor-BpK-SWtJ.d.ts} +1 -1
  27. package/dist/types/index.d.ts +3 -3
  28. package/dist/types/index.js +17 -3
  29. package/dist/types/index.js.map +1 -1
  30. package/dist/utils/index.d.ts +107 -1
  31. package/dist/utils/index.js +53 -2
  32. package/dist/utils/index.js.map +1 -1
  33. package/package.json +1 -1
@@ -101,11 +101,25 @@ var init_atomic_write = __esm({
101
101
  }
102
102
  });
103
103
 
104
+ // src/utils/term.ts
105
+ var hasStdout = () => typeof process !== "undefined" && !!process.stdout;
106
+ function isStdoutTTY() {
107
+ return hasStdout() && Boolean(process.stdout.isTTY);
108
+ }
109
+ function writeTo(s, stream) {
110
+ if (!stream || typeof stream.write !== "function") return false;
111
+ stream.write(s);
112
+ return true;
113
+ }
114
+ function writeErr(s, stream = process.stderr) {
115
+ return writeTo(s, stream);
116
+ }
117
+
104
118
  // src/utils/color.ts
105
119
  var isColorTty = () => {
106
120
  if (process.env.NO_COLOR) return false;
107
121
  if (process.env.FORCE_COLOR) return true;
108
- return Boolean(process.stdout?.isTTY);
122
+ return isStdoutTTY();
109
123
  };
110
124
  var COLOR = isColorTty();
111
125
  var wrap = (open3, close) => (s) => COLOR ? `\x1B[${open3}m${s}\x1B[${close}m` : s;
@@ -202,10 +216,10 @@ var DefaultLogger = class _DefaultLogger {
202
216
  if (r <= LEVEL_RANK.warn || this.level === "debug" || this.level === "trace") {
203
217
  const head = `${color.dim(ts)} ${COLORS[level](level.toUpperCase().padEnd(5))} ${msg}`;
204
218
  if (ctx !== void 0) {
205
- process.stderr.write(`${head} ${formatCtx(ctx)}
219
+ writeErr(`${head} ${formatCtx(ctx)}
206
220
  `);
207
221
  } else {
208
- process.stderr.write(`${head}
222
+ writeErr(`${head}
209
223
  `);
210
224
  }
211
225
  }
@@ -1106,9 +1120,9 @@ var DefaultMemoryStore = class {
1106
1120
  async readAll() {
1107
1121
  const parts = [];
1108
1122
  for (const scope of ["project-agents", "project-memory", "user-memory"]) {
1109
- const writeErr = this.writeErrors.get(scope);
1110
- if (writeErr) {
1111
- parts.push(`> \u26A0\uFE0F Memory write error (${labelOf(scope)}): ${writeErr.message}`);
1123
+ const writeErr2 = this.writeErrors.get(scope);
1124
+ if (writeErr2) {
1125
+ parts.push(`> \u26A0\uFE0F Memory write error (${labelOf(scope)}): ${writeErr2.message}`);
1112
1126
  }
1113
1127
  const body = await this.read(scope);
1114
1128
  if (body.trim()) parts.push(`## ${labelOf(scope)}
@@ -11200,84 +11214,8 @@ function buildGoalPreamble(goal) {
11200
11214
  "BEGIN.]"
11201
11215
  ].join("\n");
11202
11216
  }
11203
-
11204
- // src/coordination/director.ts
11205
11217
  init_atomic_write();
11206
11218
 
11207
- // src/coordination/large-answer-store.ts
11208
- var LargeAnswerStore = class {
11209
- /**
11210
- * Responses above this size (in characters) are stored out-of-context.
11211
- * Below this, the full answer is returned inline (no overhead).
11212
- * Default: 2000 chars ≈ 400-600 tokens.
11213
- */
11214
- sizeThreshold;
11215
- store = /* @__PURE__ */ new Map();
11216
- constructor(sizeThreshold = 2e3) {
11217
- this.sizeThreshold = sizeThreshold;
11218
- }
11219
- /**
11220
- * Store a value, returning a summary + key for inline use.
11221
- * If the value is below sizeThreshold, returns it as-is (no store entry).
11222
- */
11223
- storeAnswer(value) {
11224
- if (value === void 0 || value === null) {
11225
- return { summary: String(value), inline: true };
11226
- }
11227
- const serialized = typeof value === "string" ? value : JSON.stringify(value);
11228
- const size = serialized.length;
11229
- if (size <= this.sizeThreshold) {
11230
- return { summary: serialized.slice(0, 500), inline: true };
11231
- }
11232
- const key = `a-${hashStr(serialized)}`;
11233
- this.store.set(key, {
11234
- key,
11235
- value,
11236
- size,
11237
- storedAt: Date.now()
11238
- });
11239
- return {
11240
- key,
11241
- summary: `[stored: ${size} chars \u2014 use roll_up or ask_result tool to retrieve, key=${key}]`,
11242
- inline: false
11243
- };
11244
- }
11245
- /**
11246
- * Retrieve a previously stored answer by its key.
11247
- * Returns undefined if the key is unknown or the store was cleared.
11248
- */
11249
- retrieveAnswer(key) {
11250
- return this.store.get(key)?.value;
11251
- }
11252
- /**
11253
- * Check if a key exists in the store.
11254
- */
11255
- hasAnswer(key) {
11256
- return this.store.has(key);
11257
- }
11258
- /** Number of stored entries. */
11259
- get size() {
11260
- return this.store.size;
11261
- }
11262
- /** Total characters stored. */
11263
- get totalChars() {
11264
- let total = 0;
11265
- for (const e of this.store.values()) total += e.size;
11266
- return total;
11267
- }
11268
- /** Clear all stored entries. Call at the end of a director run. */
11269
- clear() {
11270
- this.store.clear();
11271
- }
11272
- };
11273
- function hashStr(s) {
11274
- let h = 5381;
11275
- for (let i = 0; i < s.length; i++) {
11276
- h = h * 33 ^ s.charCodeAt(i);
11277
- }
11278
- return (h >>> 0).toString(36);
11279
- }
11280
-
11281
11219
  // src/coordination/in-memory-transport.ts
11282
11220
  var InMemoryBridgeTransport = class {
11283
11221
  subs = /* @__PURE__ */ new Map();
@@ -11425,280 +11363,746 @@ function createMessage(type, from, payload, to) {
11425
11363
  priority: "normal"
11426
11364
  };
11427
11365
  }
11428
-
11429
- // src/coordination/director-prompts.ts
11430
- var DEFAULT_DIRECTOR_PREAMBLE = `You are the Director of a multi-agent fleet. You orchestrate worker
11431
- subagents by spawning them, assigning tasks, awaiting completions, and
11432
- rolling up their outputs into your next decision.
11433
-
11434
- Core fleet tools available to you:
11435
- - spawn_subagent \u2014 create a worker with a chosen provider / model / role
11436
- - assign_task \u2014 hand a piece of work to a specific subagent
11437
- - await_tasks \u2014 block until named task ids complete (parallel-safe)
11438
- - ask_subagent \u2014 synchronously query a running subagent via the bridge
11439
- - roll_up \u2014 aggregate finished tasks into a markdown/json summary
11440
- - terminate_subagent \u2014 abort a stuck worker (use sparingly)
11441
- - fleet_status \u2014 snapshot of all subagents and pending tasks
11442
- - fleet_usage \u2014 token + cost breakdown per subagent and total
11443
-
11444
- Working rules:
11445
- 1. Decompose first. Before spawning, decide which sub-tasks are
11446
- independent and can run in parallel. Sequential work doesn't need a
11447
- subagent \u2014 do it yourself.
11448
- 2. Match worker to job. Cheap/fast model for triage, capable model for
11449
- synthesis. Different providers per sibling is allowed and encouraged.
11450
- 3. Always pair an assign with an await. Don't fire-and-forget; you owe
11451
- the user a single coherent answer at the end.
11452
- 4. Roll up before deciding. After await_tasks resolves, call roll_up so
11453
- the results are folded back into your context in a compact form.
11454
- 5. Budget is real. Check fleet_usage periodically. If a subagent is
11455
- thrashing, terminate it rather than letting cost climb silently.
11456
- 6. Never claim a subagent's work as your own without verifying it. If a
11457
- result looks wrong, ask_subagent for clarification before passing it
11458
- to the user.
11459
- 7. Wind down when satisfied. When the results are good enough, call
11460
- work_complete \u2014 no new subagents will spawn and queued tasks complete
11461
- as aborted. Running subagents finish naturally. Call terminate_subagent
11462
- only for ones you need to stop immediately.`;
11463
- var DEFAULT_SUBAGENT_BASELINE = `You are a subagent operating under a Director. You were spawned to handle
11464
- a specific slice of a larger plan \u2014 do that slice well and report back.
11465
-
11466
- Bridge contract:
11467
- - You have a parent (the Director). You may call \`request\` on the
11468
- parent bridge to ask a clarifying question. Use this sparingly; the
11469
- parent is also working.
11470
- - You MAY NOT request the parent's system prompt, tool list, or other
11471
- subagents' context. Those are not yours to read.
11472
- - Your final task output is what the Director sees. Be concise,
11473
- structured, and self-contained \u2014 assume the Director will paste your
11474
- output into its own context.`;
11475
- function composeDirectorPrompt(parts = {}) {
11476
- const sections = [];
11477
- const preamble = parts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
11478
- if (preamble && preamble.trim().length > 0) sections.push(preamble.trim());
11479
- if (parts.rosterSummary && parts.rosterSummary.trim().length > 0) {
11480
- sections.push(`Available roles you can spawn:
11481
- ${parts.rosterSummary.trim()}`);
11482
- }
11483
- if (parts.basePrompt && parts.basePrompt.trim().length > 0) {
11484
- sections.push(parts.basePrompt.trim());
11366
+ var GLOB_CHARS = /* @__PURE__ */ new Set(["*", "?", "["]);
11367
+ var IS_WINDOWS = process.platform === "win32";
11368
+ var SEP = IS_WINDOWS ? "\\" : "/";
11369
+ function isGlob(p) {
11370
+ for (const c of p) {
11371
+ if (GLOB_CHARS.has(c)) return true;
11485
11372
  }
11486
- return sections.join("\n\n");
11373
+ return false;
11487
11374
  }
11488
- function composeSubagentPrompt(parts = {}) {
11489
- const sections = [];
11490
- const baseline = parts.baseline ?? DEFAULT_SUBAGENT_BASELINE;
11491
- if (baseline && baseline.trim().length > 0) sections.push(baseline.trim());
11492
- if (parts.role && parts.role.trim().length > 0) {
11493
- sections.push(`Role:
11494
- ${parts.role.trim()}`);
11495
- }
11496
- if (parts.task && parts.task.trim().length > 0) {
11497
- sections.push(`Task:
11498
- ${parts.task.trim()}`);
11499
- }
11500
- if (parts.sharedScratchpad && parts.sharedScratchpad.trim().length > 0) {
11501
- sections.push(
11502
- `Shared notes:
11503
- A scratchpad shared with the rest of the fleet is mounted at \`${parts.sharedScratchpad.trim()}\`.
11504
- - Write your final findings as markdown files there (e.g. \`findings.md\`, \`security.md\`).
11505
- - Before starting, list the directory and read any sibling files relevant to your task \u2014 they may already contain context you can build on.
11506
- - Use stable filenames (one file per concern); overwrite instead of appending so the Director sees the latest state.`
11507
- );
11508
- }
11509
- if (parts.override && parts.override.trim().length > 0) {
11510
- sections.push(parts.override.trim());
11375
+ function globToRegex(pat) {
11376
+ let i = 0, re = "^";
11377
+ while (i < pat.length) {
11378
+ const c = pat[i];
11379
+ if (c === "*") {
11380
+ if (pat[i + 1] === "*") {
11381
+ re += ".*";
11382
+ i += 2;
11383
+ if (pat[i] === "/") i++;
11384
+ } else {
11385
+ re += "[^/\\\\]*";
11386
+ i++;
11387
+ }
11388
+ } else if (c === "?") {
11389
+ re += "[^/\\\\]";
11390
+ i++;
11391
+ } else if (c === "[") {
11392
+ let cls = "[";
11393
+ i++;
11394
+ if (pat[i] === "!" || pat[i] === "^") {
11395
+ cls += "^";
11396
+ i++;
11397
+ }
11398
+ while (i < pat.length && pat[i] !== "]") {
11399
+ const ch = pat[i] ?? "";
11400
+ if (ch === "\\") cls += "\\\\";
11401
+ else if (ch === "]" || ch === "^") cls += `\\${ch}`;
11402
+ else cls += ch;
11403
+ i++;
11404
+ }
11405
+ cls += "]";
11406
+ re += cls;
11407
+ i++;
11408
+ } else {
11409
+ re += c.replace(/[.+^${}()|\\]/g, "\\$&");
11410
+ i++;
11411
+ }
11511
11412
  }
11512
- return sections.join("\n\n");
11413
+ return new RegExp(re + "$");
11513
11414
  }
11514
- function rosterSummaryFromConfigs(roster) {
11515
- const lines = [];
11516
- for (const [roleId, cfg] of Object.entries(roster)) {
11517
- const tag = cfg.provider && cfg.model ? ` (${cfg.provider}/${cfg.model})` : "";
11518
- const headline = cfg.prompt ? (cfg.prompt.split("\n").find((l) => l.trim().length > 0) ?? "").trim().slice(0, 80) : "";
11519
- const tail = headline ? ` \u2014 ${headline}` : "";
11520
- lines.push(`- ${roleId}: ${cfg.name}${tag}${tail}`);
11521
- }
11522
- return lines.join("\n");
11415
+ function baseDir(pat) {
11416
+ let i = pat.length - 1;
11417
+ while (i >= 0 && !GLOB_CHARS.has(pat[i]) && pat[i] !== SEP && pat[i] !== "/") i--;
11418
+ const cut = i >= 0 ? pat.lastIndexOf(SEP, i) : pat.lastIndexOf("/", i);
11419
+ return cut < 0 ? "." : pat.slice(0, cut);
11523
11420
  }
11524
-
11525
- // src/coordination/fleet-bus.ts
11526
- var FleetBus = class {
11527
- byId = /* @__PURE__ */ new Map();
11528
- byType = /* @__PURE__ */ new Map();
11529
- any = /* @__PURE__ */ new Set();
11530
- /**
11531
- * Hook a subagent's EventBus into the fleet. Uses `onAny()` (an alias for
11532
- * `onPattern('*')`) to forward all events with subagent attribution, so
11533
- * new kernel event types are automatically forwarded without any manual
11534
- * registration. `subagent.*` events are excluded because they originate
11535
- * from MultiAgentHost on the parent bus, not the subagent's own bus.
11536
- *
11537
- * Returns a disposer that detaches every subscription; call on
11538
- * subagent teardown so the listeners don't outlive the run.
11539
- */
11540
- attach(subagentId, bus, taskId) {
11541
- const off = bus.onAny((type, payload) => {
11542
- if (type.startsWith("subagent.")) return;
11543
- this.emit({ subagentId, taskId, ts: Date.now(), type, payload });
11544
- });
11545
- return () => {
11546
- off();
11547
- };
11548
- }
11549
- /** Subscribe to every event from one subagent. */
11550
- subscribe(subagentId, handler) {
11551
- let set = this.byId.get(subagentId);
11552
- if (!set) {
11553
- set = /* @__PURE__ */ new Set();
11554
- this.byId.set(subagentId, set);
11555
- }
11556
- set.add(handler);
11557
- return () => {
11558
- set.delete(handler);
11559
- };
11560
- }
11561
- /** Subscribe to one event type across all subagents. */
11562
- filter(type, handler) {
11563
- let set = this.byType.get(type);
11564
- if (!set) {
11565
- set = /* @__PURE__ */ new Set();
11566
- this.byType.set(type, set);
11421
+ async function expandGlob(pattern) {
11422
+ if (!isGlob(pattern)) return [pattern];
11423
+ const results = /* @__PURE__ */ new Set();
11424
+ const abs = isAbsolute(pattern);
11425
+ const base = abs ? baseDir(pattern) : baseDir(pattern);
11426
+ const relPat = base === "." ? pattern : pattern.slice(base.length + 1);
11427
+ async function walk4(dir, pat) {
11428
+ let entries;
11429
+ try {
11430
+ entries = await fsp.readdir(dir);
11431
+ } catch {
11432
+ return;
11567
11433
  }
11568
- set.add(handler);
11569
- return () => {
11570
- set.delete(handler);
11571
- };
11572
- }
11573
- /** Subscribe to literally everything. The fleet roll-up uses this. */
11574
- onAny(handler) {
11575
- this.any.add(handler);
11576
- return () => {
11577
- this.any.delete(handler);
11578
- };
11579
- }
11580
- emit(event) {
11581
- const byId = this.byId.get(event.subagentId);
11582
- if (byId)
11583
- for (const h of byId) {
11434
+ const firstGlob = pat.search(/[*?[\[]/);
11435
+ if (firstGlob < 0) {
11436
+ const re = globToRegex(pat);
11437
+ for (const e of entries) {
11438
+ if (re.test(e)) {
11439
+ const full = `${dir}${SEP}${e}`;
11440
+ results.add(abs ? resolve(full) : full);
11441
+ }
11442
+ }
11443
+ return;
11444
+ }
11445
+ const before = pat.slice(0, firstGlob);
11446
+ const rest = pat.slice(firstGlob);
11447
+ if (before.endsWith("**")) {
11448
+ await walk4(dir, rest);
11449
+ for (const e of entries) {
11450
+ const full = `${dir}${SEP}${e}`;
11584
11451
  try {
11585
- h(event);
11452
+ const stat5 = await fsp.stat(full);
11453
+ if (stat5.isDirectory()) await walk4(full, rest);
11586
11454
  } catch {
11587
11455
  }
11588
11456
  }
11589
- const byType = this.byType.get(event.type);
11590
- if (byType)
11591
- for (const h of byType) {
11457
+ } else if (before === "") {
11458
+ const re = globToRegex(rest);
11459
+ for (const e of entries) {
11460
+ if (re.test(e)) {
11461
+ const full = `${dir}${SEP}${e}`;
11462
+ results.add(abs ? resolve(full) : full);
11463
+ }
11464
+ }
11465
+ } else {
11466
+ const seg = before.replace(/[*?[\]]/g, "").replace(/\/$/, "");
11467
+ if (entries.includes(seg)) {
11468
+ const full = `${dir}${SEP}${seg}`;
11592
11469
  try {
11593
- h(event);
11470
+ const stat5 = await fsp.stat(full);
11471
+ if (stat5.isDirectory()) await walk4(full, rest);
11594
11472
  } catch {
11595
11473
  }
11596
11474
  }
11597
- for (const h of this.any) {
11598
- try {
11599
- h(event);
11600
- } catch {
11601
- }
11602
11475
  }
11603
11476
  }
11604
- };
11605
- var FleetUsageAggregator = class {
11606
- constructor(bus, priceLookup, metaLookup) {
11607
- this.priceLookup = priceLookup;
11608
- this.metaLookup = metaLookup;
11609
- this.unsub.push(bus.filter("provider.response", (e) => this.onProviderResponse(e)));
11610
- this.unsub.push(bus.filter("tool.executed", (e) => this.onToolExecuted(e)));
11611
- this.unsub.push(bus.filter("iteration.started", (e) => this.onIterationStarted(e)));
11477
+ await walk4(base === "." ? "." : base, relPat);
11478
+ return [...results];
11479
+ }
11480
+
11481
+ // src/coordination/collab-debug.ts
11482
+ var DEFAULT_MAX_TARGET_FILES = 30;
11483
+ var CollabSession = class extends EventEmitter {
11484
+ sessionId;
11485
+ options;
11486
+ snapshot;
11487
+ director;
11488
+ fleetBus;
11489
+ subagentIds = /* @__PURE__ */ new Map();
11490
+ // role → subagentId
11491
+ bugs = /* @__PURE__ */ new Map();
11492
+ plans = /* @__PURE__ */ new Map();
11493
+ evaluations = /* @__PURE__ */ new Map();
11494
+ disposers = new Array();
11495
+ settled = false;
11496
+ timeoutMs;
11497
+ cancelled = false;
11498
+ alerts = [];
11499
+ /** Tracks tool call counts per subagent for progress-based timeout decisions. */
11500
+ progressBySubagent = /* @__PURE__ */ new Map();
11501
+ /** Last tool call count when a timeout warning was handled. */
11502
+ lastTimeoutProgress = /* @__PURE__ */ new Map();
11503
+ /** Session-level timeout timer handle (cleared on cancel or natural completion). */
11504
+ _timeoutTimer;
11505
+ constructor(director, fleetBus, options) {
11506
+ super();
11507
+ this.sessionId = randomUUID();
11508
+ this.options = options;
11509
+ this.director = director;
11510
+ this.fleetBus = fleetBus;
11511
+ this.timeoutMs = options.timeoutMs ?? 10 * 60 * 1e3;
11512
+ if (options.prebuiltSnapshot) {
11513
+ this.snapshot = options.prebuiltSnapshot;
11514
+ } else {
11515
+ this.snapshot = {
11516
+ id: this.sessionId,
11517
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
11518
+ files: []
11519
+ };
11520
+ }
11612
11521
  }
11613
- priceLookup;
11614
- metaLookup;
11615
- perSubagent = /* @__PURE__ */ new Map();
11616
- total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
11617
- unsub = new Array();
11618
- /**
11619
- * Remove a terminated subagent's data from the aggregator and subtract its
11620
- * contribution from the running totals. Call this when a subagent is removed
11621
- * from the fleet so the aggregator doesn't accumulate unbounded data for
11622
- * entities that will never emit events again.
11623
- */
11624
- removeSubagent(subagentId) {
11625
- const snap = this.perSubagent.get(subagentId);
11626
- if (!snap) return;
11627
- this.perSubagent.delete(subagentId);
11628
- this.total.input -= snap.input;
11629
- this.total.output -= snap.output;
11630
- this.total.cacheRead -= snap.cacheRead;
11631
- this.total.cacheWrite -= snap.cacheWrite;
11632
- this.total.cost -= snap.cost;
11522
+ get id() {
11523
+ return this.sessionId;
11633
11524
  }
11634
- /** Disposes all fleet-bus subscriptions. Call when the aggregator is no longer needed. */
11635
- dispose() {
11636
- for (const off of this.unsub) off();
11637
- this.unsub.length = 0;
11525
+ getSessionAlerts() {
11526
+ return [...this.alerts];
11638
11527
  }
11639
- /** Live snapshot — safe to call from a tool's execute() body. */
11640
- snapshot() {
11641
- return {
11642
- total: { ...this.total },
11643
- perSubagent: Object.fromEntries(
11644
- Array.from(this.perSubagent.entries()).map(([k, v]) => [k, { ...v }])
11645
- )
11646
- };
11528
+ isCancelled() {
11529
+ return this.cancelled;
11647
11530
  }
11648
- ensure(subagentId) {
11649
- let snap = this.perSubagent.get(subagentId);
11650
- if (!snap) {
11651
- const meta = this.metaLookup?.(subagentId);
11652
- snap = {
11653
- subagentId,
11654
- provider: meta?.provider,
11655
- model: meta?.model,
11656
- input: 0,
11657
- output: 0,
11658
- cacheRead: 0,
11659
- cacheWrite: 0,
11660
- cost: 0,
11661
- toolCalls: 0,
11662
- iterations: 0,
11663
- startedAt: Date.now(),
11664
- lastEventAt: Date.now()
11665
- };
11666
- this.perSubagent.set(subagentId, snap);
11531
+ /**
11532
+ * Snapshot of role → subagentId map. The Director calls coordinator.stop()
11533
+ * for each agent when cancelling the session, using this map to enumerate
11534
+ * all three collab agents.
11535
+ */
11536
+ getSubagentIds() {
11537
+ return new Map(this.subagentIds);
11538
+ }
11539
+ /**
11540
+ * Returns the effective file limit for this session.
11541
+ * Priority: explicit `maxTargetFiles` > dynamic from `contextWindow` > `DEFAULT_MAX_TARGET_FILES`.
11542
+ */
11543
+ effectiveFileLimit() {
11544
+ if (this.options.maxTargetFiles !== void 0) {
11545
+ return this.options.maxTargetFiles;
11667
11546
  }
11668
- return snap;
11547
+ if (this.options.contextWindow !== void 0) {
11548
+ return Math.max(5, Math.floor(this.options.contextWindow * 0.4 / 2e3));
11549
+ }
11550
+ return DEFAULT_MAX_TARGET_FILES;
11669
11551
  }
11670
- onProviderResponse(e) {
11671
- const snap = this.ensure(e.subagentId);
11672
- const p = e.payload;
11673
- const usage = p?.usage;
11674
- if (!usage) return;
11675
- snap.input += usage.input ?? 0;
11676
- snap.output += usage.output ?? 0;
11677
- snap.cacheRead += usage.cacheRead ?? 0;
11678
- snap.cacheWrite += usage.cacheWrite ?? 0;
11679
- this.total.input += usage.input ?? 0;
11680
- this.total.output += usage.output ?? 0;
11681
- this.total.cacheRead += usage.cacheRead ?? 0;
11682
- this.total.cacheWrite += usage.cacheWrite ?? 0;
11683
- const price = this.priceLookup?.(e.subagentId, snap.provider, snap.model);
11684
- if (price) {
11685
- const delta = (usage.input ?? 0) / 1e6 * (price.input ?? 0) + (usage.output ?? 0) / 1e6 * (price.output ?? 0) + (usage.cacheRead ?? 0) / 1e6 * (price.cacheRead ?? 0) + (usage.cacheWrite ?? 0) / 1e6 * (price.cacheWrite ?? 0);
11686
- snap.cost += delta;
11687
- this.total.cost += delta;
11552
+ async buildSnapshot() {
11553
+ if (this.snapshot.files.length > 0) return this.snapshot;
11554
+ const allFiles = [];
11555
+ for (const pattern of this.options.targetPaths) {
11556
+ const expanded = await expandGlob(pattern);
11557
+ allFiles.push(...expanded);
11688
11558
  }
11689
- snap.lastEventAt = e.ts;
11559
+ const limit = this.effectiveFileLimit();
11560
+ if (allFiles.length > limit) {
11561
+ const hint = this.options.contextWindow ? `contextWindow=${this.options.contextWindow} \u2192 calculated limit=${limit}` : `default limit=${DEFAULT_MAX_TARGET_FILES}`;
11562
+ throw new Error(
11563
+ `[collab_debug] Target has ${allFiles.length} files, which exceeds the limit (${hint}). Narrow the target or pass maxTargetFiles / contextWindow to override. For large codebases, run package-by-package or module-by-module sessions instead of targeting the entire repo.`
11564
+ );
11565
+ }
11566
+ for (const filePath of allFiles) {
11567
+ try {
11568
+ const content = await fsp.readFile(filePath, "utf8");
11569
+ const ext = filePath.split(".").pop() ?? "";
11570
+ const language = ext === "ts" || ext === "tsx" ? "typescript" : ext === "js" || ext === "jsx" ? "javascript" : ext === "md" ? "markdown" : ext === "json" ? "json" : void 0;
11571
+ this.snapshot.files.push({ path: filePath, content, language });
11572
+ } catch {
11573
+ this.snapshot.files.push({ path: filePath, content: "", language: void 0 });
11574
+ }
11575
+ }
11576
+ return this.snapshot;
11690
11577
  }
11691
- onToolExecuted(e) {
11692
- const snap = this.ensure(e.subagentId);
11693
- snap.toolCalls += 1;
11694
- snap.lastEventAt = e.ts;
11578
+ /**
11579
+ * Cancel the session. Emits director.cancel_collab on the FleetBus so all
11580
+ * collab agents finish early. The session-level timeout timer is also cleared.
11581
+ * Safe to call multiple times (idempotent after first call).
11582
+ */
11583
+ cancel(reason = "Director cancelled collab session") {
11584
+ if (this.settled) return;
11585
+ this.cancelled = true;
11586
+ if (this._timeoutTimer) {
11587
+ clearTimeout(this._timeoutTimer);
11588
+ this._timeoutTimer = void 0;
11589
+ }
11590
+ this.fleetBus.emit({
11591
+ subagentId: this.director.id,
11592
+ ts: Date.now(),
11593
+ type: "director.cancel_collab",
11594
+ payload: { sessionId: this.sessionId, reason, cancelledAt: (/* @__PURE__ */ new Date()).toISOString() }
11595
+ });
11596
+ this.fleetBus.emit({
11597
+ subagentId: this.director.id,
11598
+ ts: Date.now(),
11599
+ type: "collab.cancelled",
11600
+ payload: { sessionId: this.sessionId, reason }
11601
+ });
11602
+ }
11603
+ async start() {
11604
+ if (this.settled) throw new Error("session already settled");
11605
+ this.settled = true;
11606
+ await this.buildSnapshot();
11607
+ this.wireFleetBus();
11608
+ const [bugHunterId, refactorPlannerId, criticId] = await Promise.all([
11609
+ this.spawnAgent("bug-hunter", this.buildBugHunterTask()),
11610
+ this.spawnAgent("refactor-planner", this.buildRefactorPlannerTask()),
11611
+ this.spawnAgent("critic", this.buildCriticTask())
11612
+ ]);
11613
+ this.subagentIds.set("bug-hunter", bugHunterId);
11614
+ this.subagentIds.set("refactor-planner", refactorPlannerId);
11615
+ this.subagentIds.set("critic", criticId);
11616
+ const timeout = new Promise((_, reject) => {
11617
+ this._timeoutTimer = setTimeout(() => {
11618
+ this.cancel("Session-level timeout reached");
11619
+ reject(new Error(`CollabSession timed out after ${this.timeoutMs}ms`));
11620
+ }, this.timeoutMs);
11621
+ });
11622
+ let results = null;
11623
+ try {
11624
+ results = await Promise.race([
11625
+ Promise.all([
11626
+ this.director.awaitTasks([bugHunterId]),
11627
+ this.director.awaitTasks([refactorPlannerId]),
11628
+ this.director.awaitTasks([criticId])
11629
+ ]),
11630
+ timeout
11631
+ ]);
11632
+ } catch (err) {
11633
+ if (this._timeoutTimer) {
11634
+ clearTimeout(this._timeoutTimer);
11635
+ this._timeoutTimer = void 0;
11636
+ }
11637
+ this.cleanup();
11638
+ const error = err instanceof Error ? err : new Error(String(err));
11639
+ this.emit("session.error", error);
11640
+ throw error;
11641
+ }
11642
+ for (const result of results.flat()) {
11643
+ await this.parseAndEmit(result);
11644
+ }
11645
+ const report = this.assembleReport();
11646
+ this.cleanup();
11647
+ this.emit("session.done", report);
11648
+ return report;
11649
+ }
11650
+ async parseAndEmit(result) {
11651
+ if (result.status !== "success" || result.result == null) return;
11652
+ const text = typeof result.result === "string" ? result.result : JSON.stringify(result.result);
11653
+ for (const obj of this.extractJsonObjects(text)) {
11654
+ const type = "finding" in obj ? "bug.found" : "plan" in obj ? "refactor.plan" : "evaluation" in obj ? "critic.evaluation" : null;
11655
+ if (!type) continue;
11656
+ this.fleetBus.emit({
11657
+ subagentId: result.subagentId,
11658
+ taskId: result.taskId,
11659
+ ts: Date.now(),
11660
+ type,
11661
+ payload: obj
11662
+ });
11663
+ }
11664
+ }
11665
+ extractJsonObjects(text) {
11666
+ const objects = [];
11667
+ let depth = 0;
11668
+ let start = -1;
11669
+ let inString = false;
11670
+ let escaped = false;
11671
+ for (let i = 0; i < text.length; i++) {
11672
+ const ch = text[i];
11673
+ if (inString) {
11674
+ if (escaped) escaped = false;
11675
+ else if (ch === "\\") escaped = true;
11676
+ else if (ch === '"') inString = false;
11677
+ continue;
11678
+ }
11679
+ if (ch === '"') {
11680
+ inString = true;
11681
+ } else if (ch === "{") {
11682
+ if (depth === 0) start = i;
11683
+ depth++;
11684
+ } else if (ch === "}" && depth > 0) {
11685
+ depth--;
11686
+ if (depth === 0 && start >= 0) {
11687
+ const candidate = text.slice(start, i + 1);
11688
+ try {
11689
+ const parsed = JSON.parse(candidate);
11690
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
11691
+ objects.push(parsed);
11692
+ }
11693
+ } catch {
11694
+ }
11695
+ start = -1;
11696
+ }
11697
+ }
11698
+ }
11699
+ return objects;
11700
+ }
11701
+ budgetForRole(role) {
11702
+ if (this.options.budgetOverrides?.[role]) {
11703
+ return this.options.budgetOverrides[role];
11704
+ }
11705
+ const defaults = {
11706
+ "bug-hunter": { maxIterations: 2e3, maxToolCalls: 5e3, timeoutMs: 10 * 60 * 1e3 },
11707
+ "refactor-planner": { maxIterations: 1500, maxToolCalls: 4e3, timeoutMs: 8 * 60 * 1e3 },
11708
+ "critic": { maxIterations: 1e3, maxToolCalls: 3e3, timeoutMs: 6 * 60 * 1e3 }
11709
+ };
11710
+ return defaults[role] ?? { maxIterations: 1500, maxToolCalls: 4e3, timeoutMs: 8 * 60 * 1e3 };
11711
+ }
11712
+ async spawnAgent(role, taskBrief) {
11713
+ const budget = this.budgetForRole(role);
11714
+ const cfg = {
11715
+ id: `${role}-${this.sessionId}`,
11716
+ name: role,
11717
+ role,
11718
+ tools: ["fleet_emit", "fleet_status", "read", "grep", "glob", "bash", "write"],
11719
+ maxIterations: budget.maxIterations,
11720
+ maxToolCalls: budget.maxToolCalls,
11721
+ timeoutMs: budget.timeoutMs
11722
+ };
11723
+ const subagentId = await this.director.spawn(cfg);
11724
+ await this.director.assign({ id: randomUUID(), subagentId, description: taskBrief });
11725
+ return subagentId;
11726
+ }
11727
+ buildBugHunterTask() {
11728
+ const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
11729
+ const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
11730
+ ${f.content}`).join("\n\n");
11731
+ return `You are BugHunter. Scan the following files for bugs and code smells.
11732
+
11733
+ Target files:
11734
+ ${fileContents}
11735
+
11736
+ For each bug found, emit it using the fleet_emit tool immediately:
11737
+ { "type": "bug.found", "payload": { "finding": { "id": "<uuid>", "type": "<pattern>", "severity": "<critical|high|medium|low>", "location": { "file": "<path>", "line": <n> }, "description": "<explain>", "suggestedFix": "<optional>" } } }
11738
+
11739
+ After scanning all files, write your full markdown bug report to:
11740
+ ${scratchpad}/bug-hunter-report-${this.sessionId}.md
11741
+
11742
+ Important: emit each finding as soon as you find it. Do not batch or wait until the end.`;
11743
+ }
11744
+ buildRefactorPlannerTask() {
11745
+ const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
11746
+ const bugHunterReportPath = `${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
11747
+ const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
11748
+ ${f.content}`).join("\n\n");
11749
+ return `You are RefactorPlanner. Plan refactorings for the following files.
11750
+
11751
+ Target files:
11752
+ ${fileContents}
11753
+
11754
+ Read the BugHunter report at: ${bugHunterReportPath}
11755
+
11756
+ For each bug you can address, emit a refactor plan using fleet_emit:
11757
+ { "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>" } } }
11758
+
11759
+ Also write your full markdown plan to:
11760
+ ${scratchpad}/refactor-plan-${this.sessionId}.md
11761
+
11762
+ Emit each plan immediately. Do not wait until planning is complete.`;
11763
+ }
11764
+ buildCriticTask() {
11765
+ const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
11766
+ const bugHunterReportPath = `${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
11767
+ const refactorPlanPath = `${scratchpad}/refactor-plan-${this.sessionId}.md`;
11768
+ const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
11769
+ ${f.content}`).join("\n\n");
11770
+ return `You are Critic. Evaluate bug findings and refactor plans.
11771
+
11772
+ Target files:
11773
+ ${fileContents}
11774
+
11775
+ Read the BugHunter report at: ${bugHunterReportPath}
11776
+ Read the RefactorPlanner report at: ${refactorPlanPath}
11777
+
11778
+ For each bug and refactor plan, emit your evaluation using fleet_emit:
11779
+ { "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>" }] } } }
11780
+
11781
+ After all evaluations, write your markdown report to:
11782
+ ${scratchpad}/critic-report-${this.sessionId}.md
11783
+
11784
+ Emit each evaluation immediately. Do not wait until you have read all reports.`;
11785
+ }
11786
+ wireFleetBus() {
11787
+ const dTool = this.fleetBus.filter("tool.executed", (e) => {
11788
+ this.progressBySubagent.set(e.subagentId, (this.progressBySubagent.get(e.subagentId) ?? 0) + 1);
11789
+ });
11790
+ this.disposers.push(dTool);
11791
+ const dBudget = this.fleetBus.filter("budget.threshold_reached", (e) => {
11792
+ const payload = e.payload;
11793
+ const role = this.roleFromSubagentId(e.subagentId);
11794
+ if (!role) return;
11795
+ const btwNotes = this.director.getLeaderBtwNotes();
11796
+ const alert = {
11797
+ sessionId: this.sessionId,
11798
+ subagentId: e.subagentId,
11799
+ role,
11800
+ level: "warning" /* WARNING */,
11801
+ message: `${role} hit ${payload.kind} soft limit (${payload.used}/${payload.limit})`,
11802
+ budgetKind: payload.kind,
11803
+ elapsedMs: payload.timeoutMs,
11804
+ limit: payload.limit,
11805
+ btwNotes
11806
+ };
11807
+ this.alerts.push(alert);
11808
+ this.fleetBus.emit({
11809
+ subagentId: e.subagentId,
11810
+ ts: Date.now(),
11811
+ type: "collab.warning",
11812
+ payload: alert
11813
+ });
11814
+ const decision = this.options.onBudgetWarning?.(alert) ?? "ignore";
11815
+ if (decision === "cancel") {
11816
+ this.cancel(`Director cancelled: ${role} ${payload.kind} threshold`);
11817
+ return;
11818
+ }
11819
+ if (payload.kind === "timeout" || payload.kind === "idle_timeout") {
11820
+ const progress = this.progressBySubagent.get(e.subagentId) ?? 0;
11821
+ const lastProgress = this.lastTimeoutProgress.get(e.subagentId) ?? -1;
11822
+ if (progress <= lastProgress) {
11823
+ payload.deny();
11824
+ return;
11825
+ }
11826
+ this.lastTimeoutProgress.set(e.subagentId, progress);
11827
+ const newLimit = Math.min(Math.ceil((payload.timeoutMs ?? payload.limit) * 2), 24 * 60 * 6e4);
11828
+ setImmediate(() => {
11829
+ payload.extend({ timeoutMs: newLimit });
11830
+ });
11831
+ return;
11832
+ }
11833
+ if (decision === "extend") {
11834
+ setImmediate(() => {
11835
+ const base = Math.max(payload.limit, payload.used);
11836
+ const extra = {};
11837
+ switch (payload.kind) {
11838
+ case "iterations":
11839
+ extra.maxIterations = Math.min(Math.ceil(base * 1.5), 5e4);
11840
+ break;
11841
+ case "tool_calls":
11842
+ extra.maxToolCalls = Math.min(Math.ceil(base * 1.5), 1e5);
11843
+ break;
11844
+ case "tokens":
11845
+ extra.maxTokens = Math.min(Math.ceil(base * 1.5), 5e6);
11846
+ break;
11847
+ case "cost":
11848
+ extra.maxCostUsd = Math.min(base * 1.5, 100);
11849
+ break;
11850
+ }
11851
+ payload.extend(extra);
11852
+ });
11853
+ return;
11854
+ }
11855
+ if (payload.kind !== "timeout") {
11856
+ setImmediate(() => {
11857
+ const base = Math.max(payload.limit, payload.used);
11858
+ const extra = {};
11859
+ switch (payload.kind) {
11860
+ case "iterations":
11861
+ extra.maxIterations = Math.min(Math.ceil(base * 1.25), 5e4);
11862
+ break;
11863
+ case "tool_calls":
11864
+ extra.maxToolCalls = Math.min(Math.ceil(base * 1.25), 1e5);
11865
+ break;
11866
+ case "tokens":
11867
+ extra.maxTokens = Math.min(Math.ceil(base * 1.25), 5e6);
11868
+ break;
11869
+ case "cost":
11870
+ extra.maxCostUsd = Math.min(base * 1.25, 100);
11871
+ break;
11872
+ }
11873
+ payload.extend(extra);
11874
+ });
11875
+ }
11876
+ });
11877
+ this.disposers.push(dBudget);
11878
+ const dCancel = this.fleetBus.filter("director.cancel_collab", (e) => {
11879
+ const payload = e.payload;
11880
+ if (payload.sessionId !== this.sessionId) return;
11881
+ this.cancelled = true;
11882
+ if (this._timeoutTimer) {
11883
+ clearTimeout(this._timeoutTimer);
11884
+ this._timeoutTimer = void 0;
11885
+ }
11886
+ this.fleetBus.emit({
11887
+ subagentId: this.director.id,
11888
+ ts: Date.now(),
11889
+ type: "collab.cancelled",
11890
+ payload: { sessionId: this.sessionId, reason: payload.reason }
11891
+ });
11892
+ });
11893
+ this.disposers.push(dCancel);
11894
+ const d1 = this.fleetBus.filter("bug.found", (e) => {
11895
+ const payload = e.payload;
11896
+ if (payload?.finding) {
11897
+ this.bugs.set(payload.finding.id, payload.finding);
11898
+ this.emit("bug.found", payload);
11899
+ }
11900
+ });
11901
+ this.disposers.push(d1);
11902
+ const d2 = this.fleetBus.filter("refactor.plan", (e) => {
11903
+ const payload = e.payload;
11904
+ if (payload?.plan) {
11905
+ this.plans.set(payload.plan.id, payload.plan);
11906
+ this.emit("refactor.plan", payload);
11907
+ }
11908
+ });
11909
+ this.disposers.push(d2);
11910
+ const d3 = this.fleetBus.filter("critic.evaluation", (e) => {
11911
+ const payload = e.payload;
11912
+ if (payload?.evaluation) {
11913
+ this.evaluations.set(payload.evaluation.id, payload.evaluation);
11914
+ this.emit("critic.evaluation", payload);
11915
+ }
11916
+ });
11917
+ this.disposers.push(d3);
11918
+ }
11919
+ roleFromSubagentId(subagentId) {
11920
+ for (const [role, id] of this.subagentIds) {
11921
+ if (id === subagentId) return role;
11922
+ }
11923
+ const match = subagentId.match(/^(bug-hunter|refactor-planner|critic)/);
11924
+ return match?.[1] ?? null;
11925
+ }
11926
+ assembleReport() {
11927
+ const bugList = Array.from(this.bugs.values());
11928
+ const planList = Array.from(this.plans.values());
11929
+ const evalList = Array.from(this.evaluations.values());
11930
+ let disposition = "completed";
11931
+ if (this.cancelled) disposition = "cancelled";
11932
+ const verdictOrder = {
11933
+ approve: 0,
11934
+ needs_revision: 1,
11935
+ reject: 2
11936
+ };
11937
+ const overallVerdict = evalList.reduce(
11938
+ (worst, eval_) => {
11939
+ const w = verdictOrder[worst];
11940
+ const c = verdictOrder[eval_.verdict];
11941
+ return c > w ? eval_.verdict : worst;
11942
+ },
11943
+ "approve"
11944
+ );
11945
+ const summary = this.buildMarkdownSummary(bugList, planList, evalList, overallVerdict, disposition);
11946
+ return {
11947
+ sessionId: this.sessionId,
11948
+ startedAt: this.snapshot.createdAt,
11949
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
11950
+ targetPaths: this.options.targetPaths,
11951
+ disposition,
11952
+ bugs: bugList,
11953
+ refactorPlans: planList,
11954
+ evaluations: evalList,
11955
+ alerts: [...this.alerts],
11956
+ overallVerdict,
11957
+ summary
11958
+ };
11959
+ }
11960
+ buildMarkdownSummary(bugs, plans, evals, overallVerdict, disposition) {
11961
+ const lines = [
11962
+ `## Collaborative Debugging Report \u2014 ${this.sessionId}`,
11963
+ "",
11964
+ `**Target:** ${this.options.targetPaths.join(", ")}`,
11965
+ `**Disposition:** ${disposition.toUpperCase()}`,
11966
+ `**Overall Verdict:** **${overallVerdict.toUpperCase()}**`,
11967
+ ""
11968
+ ];
11969
+ if (this.alerts.length > 0) {
11970
+ lines.push("### Alerts", "");
11971
+ for (const alert of this.alerts) {
11972
+ lines.push(`- **[${alert.level.toUpperCase()}]** ${alert.role}: ${alert.message}`);
11973
+ }
11974
+ lines.push("");
11975
+ }
11976
+ if (bugs.length > 0) {
11977
+ lines.push("### Bugs Found", "");
11978
+ for (const b of bugs) {
11979
+ lines.push(`- **[${b.severity.toUpperCase()}]** \`${b.location.file}:${b.location.line}\` \u2014 ${b.description}`);
11980
+ }
11981
+ lines.push("");
11982
+ }
11983
+ if (plans.length > 0) {
11984
+ lines.push("### Refactor Plans", "");
11985
+ for (const p of plans) {
11986
+ lines.push(`- **Phase plan** (risk: ${p.riskScore}, ~${p.estimatedChangeCount} changes)`);
11987
+ for (const phase of p.phases) {
11988
+ lines.push(` - Phase ${phase.number}: ${phase.title} [${phase.risk}]`);
11989
+ }
11990
+ }
11991
+ lines.push("");
11992
+ }
11993
+ if (evals.length > 0) {
11994
+ lines.push("### Critic Evaluations", "");
11995
+ for (const e of evals) {
11996
+ lines.push(`- [${e.subjectType}] score=${e.score}/10 \u2014 **${e.verdict.toUpperCase()}**`);
11997
+ for (const c of e.concerns) {
11998
+ if (c.severity === "blocking") lines.push(` - ${c.description}`);
11999
+ }
12000
+ }
12001
+ lines.push("");
12002
+ }
12003
+ return lines.join("\n");
11695
12004
  }
11696
- onIterationStarted(e) {
11697
- const snap = this.ensure(e.subagentId);
11698
- snap.iterations += 1;
11699
- snap.lastEventAt = e.ts;
12005
+ cleanup() {
12006
+ for (const dispose of this.disposers) dispose();
12007
+ this.disposers.length = 0;
11700
12008
  }
11701
12009
  };
12010
+
12011
+ // src/coordination/director-prompts.ts
12012
+ var DEFAULT_DIRECTOR_PREAMBLE = `You are the Director of a multi-agent fleet. You orchestrate worker
12013
+ subagents by spawning them, assigning tasks, awaiting completions, and
12014
+ rolling up their outputs into your next decision.
12015
+
12016
+ Core fleet tools available to you:
12017
+ - spawn_subagent \u2014 create a worker with a chosen provider / model / role
12018
+ - assign_task \u2014 hand a piece of work to a specific subagent
12019
+ - await_tasks \u2014 block until named task ids complete (parallel-safe)
12020
+ - ask_subagent \u2014 synchronously query a running subagent via the bridge
12021
+ - roll_up \u2014 aggregate finished tasks into a markdown/json summary
12022
+ - terminate_subagent \u2014 abort a stuck worker (use sparingly)
12023
+ - fleet_status \u2014 snapshot of all subagents and pending tasks
12024
+ - fleet_usage \u2014 token + cost breakdown per subagent and total
12025
+
12026
+ Working rules:
12027
+ 1. Decompose first. Before spawning, decide which sub-tasks are
12028
+ independent and can run in parallel. Sequential work doesn't need a
12029
+ subagent \u2014 do it yourself.
12030
+ 2. Match worker to job. Cheap/fast model for triage, capable model for
12031
+ synthesis. Different providers per sibling is allowed and encouraged.
12032
+ 3. Always pair an assign with an await. Don't fire-and-forget; you owe
12033
+ the user a single coherent answer at the end.
12034
+ 4. Roll up before deciding. After await_tasks resolves, call roll_up so
12035
+ the results are folded back into your context in a compact form.
12036
+ 5. Budget is real. Check fleet_usage periodically. If a subagent is
12037
+ thrashing, terminate it rather than letting cost climb silently.
12038
+ 6. Never claim a subagent's work as your own without verifying it. If a
12039
+ result looks wrong, ask_subagent for clarification before passing it
12040
+ to the user.
12041
+ 7. Wind down when satisfied. When the results are good enough, call
12042
+ work_complete \u2014 no new subagents will spawn and queued tasks complete
12043
+ as aborted. Running subagents finish naturally. Call terminate_subagent
12044
+ only for ones you need to stop immediately.`;
12045
+ var DEFAULT_SUBAGENT_BASELINE = `You are a subagent operating under a Director. You were spawned to handle
12046
+ a specific slice of a larger plan \u2014 do that slice well and report back.
12047
+
12048
+ Bridge contract:
12049
+ - You have a parent (the Director). You may call \`request\` on the
12050
+ parent bridge to ask a clarifying question. Use this sparingly; the
12051
+ parent is also working.
12052
+ - You MAY NOT request the parent's system prompt, tool list, or other
12053
+ subagents' context. Those are not yours to read.
12054
+ - Your final task output is what the Director sees. Be concise,
12055
+ structured, and self-contained \u2014 assume the Director will paste your
12056
+ output into its own context.`;
12057
+ function composeDirectorPrompt(parts = {}) {
12058
+ const sections = [];
12059
+ const preamble = parts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
12060
+ if (preamble && preamble.trim().length > 0) sections.push(preamble.trim());
12061
+ if (parts.rosterSummary && parts.rosterSummary.trim().length > 0) {
12062
+ sections.push(`Available roles you can spawn:
12063
+ ${parts.rosterSummary.trim()}`);
12064
+ }
12065
+ if (parts.basePrompt && parts.basePrompt.trim().length > 0) {
12066
+ sections.push(parts.basePrompt.trim());
12067
+ }
12068
+ return sections.join("\n\n");
12069
+ }
12070
+ function composeSubagentPrompt(parts = {}) {
12071
+ const sections = [];
12072
+ const baseline = parts.baseline ?? DEFAULT_SUBAGENT_BASELINE;
12073
+ if (baseline && baseline.trim().length > 0) sections.push(baseline.trim());
12074
+ if (parts.role && parts.role.trim().length > 0) {
12075
+ sections.push(`Role:
12076
+ ${parts.role.trim()}`);
12077
+ }
12078
+ if (parts.task && parts.task.trim().length > 0) {
12079
+ sections.push(`Task:
12080
+ ${parts.task.trim()}`);
12081
+ }
12082
+ if (parts.sharedScratchpad && parts.sharedScratchpad.trim().length > 0) {
12083
+ sections.push(
12084
+ `Shared notes:
12085
+ A scratchpad shared with the rest of the fleet is mounted at \`${parts.sharedScratchpad.trim()}\`.
12086
+ - Write your final findings as markdown files there (e.g. \`findings.md\`, \`security.md\`).
12087
+ - Before starting, list the directory and read any sibling files relevant to your task \u2014 they may already contain context you can build on.
12088
+ - Use stable filenames (one file per concern); overwrite instead of appending so the Director sees the latest state.`
12089
+ );
12090
+ }
12091
+ if (parts.override && parts.override.trim().length > 0) {
12092
+ sections.push(parts.override.trim());
12093
+ }
12094
+ return sections.join("\n\n");
12095
+ }
12096
+ function rosterSummaryFromConfigs(roster) {
12097
+ const lines = [];
12098
+ for (const [roleId, cfg] of Object.entries(roster)) {
12099
+ const tag = cfg.provider && cfg.model ? ` (${cfg.provider}/${cfg.model})` : "";
12100
+ const headline = cfg.prompt ? (cfg.prompt.split("\n").find((l) => l.trim().length > 0) ?? "").trim().slice(0, 80) : "";
12101
+ const tail = headline ? ` \u2014 ${headline}` : "";
12102
+ lines.push(`- ${roleId}: ${cfg.name}${tag}${tail}`);
12103
+ }
12104
+ return lines.join("\n");
12105
+ }
11702
12106
  function makeSpawnTool(director, roster) {
11703
12107
  const inputSchema = {
11704
12108
  type: "object",
@@ -11969,813 +12373,442 @@ function makeFleetUsageTool(director) {
11969
12373
  };
11970
12374
  }
11971
12375
  function makeFleetSessionTool(director) {
11972
- return {
11973
- name: "fleet_session",
11974
- description: "Read a subagent's JSONL transcript and extract its last assistant text, stop reason, and tool-use count. Use this to see what a running or timed-out subagent actually produced.",
11975
- permission: "auto",
11976
- mutating: false,
11977
- inputSchema: {
11978
- type: "object",
11979
- properties: {
11980
- subagentId: { type: "string", description: "Subagent id to read the transcript of." },
11981
- tail: { type: "number", description: "Number of trailing JSONL lines to return. Omit for the full transcript." }
11982
- },
11983
- required: ["subagentId"]
11984
- },
11985
- async execute(input) {
11986
- const i = input;
11987
- const result = await director.readSession(i.subagentId, i.tail);
11988
- if (!result) {
11989
- return {
11990
- error: `fleet_session: transcript unavailable for "${i.subagentId}". Is sessionsRoot configured?`
11991
- };
11992
- }
11993
- return result;
11994
- }
11995
- };
11996
- }
11997
- function makeFleetHealthTool(director) {
11998
- return {
11999
- name: "fleet_health",
12000
- description: "Per-subagent health report: budget pressure (pct of limits consumed), last activity timestamp, and current status. Use to decide whether to assign more work to a subagent or spawn a fresh one.",
12001
- permission: "auto",
12002
- mutating: false,
12003
- inputSchema: { type: "object", properties: {}, required: [] },
12004
- async execute() {
12005
- const status = director.status();
12006
- const snapshot = director.snapshot();
12007
- const subagents = status.subagents ?? [];
12008
- const perSubagent = snapshot.perSubagent ?? {};
12009
- return {
12010
- subagents: subagents.map((s) => {
12011
- const usage = perSubagent[s.id];
12012
- return {
12013
- id: s.id,
12014
- status: s.status,
12015
- lastEventAt: usage?.lastEventAt,
12016
- budgetPressure: {
12017
- iterations: usage?.iterations,
12018
- toolCalls: usage?.toolCalls,
12019
- costUsd: usage?.cost
12020
- }
12021
- };
12022
- })
12023
- };
12024
- }
12025
- };
12026
- }
12027
- function makeCollabDebugTool(director) {
12028
- return {
12029
- name: "collab_debug",
12030
- description: "Start a collaborative debugging session: BugHunter, RefactorPlanner, and Critic run in parallel on the same target files. BugHunter finds bugs and emits bug.found events. RefactorPlanner listens for bug.found and emits refactor.plan events. Critic evaluates both and emits critic.evaluation events. Returns a structured report with overall verdict (approve / needs_revision / reject).",
12031
- permission: "auto",
12032
- mutating: false,
12033
- inputSchema: {
12034
- type: "object",
12035
- properties: {
12036
- targetPaths: {
12037
- type: "array",
12038
- items: { type: "string" },
12039
- description: "File paths / glob patterns to scan for bugs."
12040
- },
12041
- timeoutMs: {
12042
- type: "number",
12043
- minimum: 1,
12044
- description: "Timeout in ms. Default: 600000 (10 minutes)."
12045
- },
12046
- maxTargetFiles: {
12047
- type: "number",
12048
- minimum: 1,
12049
- description: "Maximum number of files to include in the snapshot. If not set, the limit is computed dynamically from contextWindow or falls back to the default (30)."
12050
- },
12051
- contextWindow: {
12052
- type: "number",
12053
- minimum: 1,
12054
- description: "Context window size (tokens) of the model. When provided and maxTargetFiles is not set, the file limit is computed dynamically as floor((contextWindow * 0.4) / 2000)."
12055
- }
12056
- },
12057
- required: ["targetPaths"]
12058
- },
12059
- async execute(input) {
12060
- const i = input;
12061
- if (!i.targetPaths?.length) {
12062
- return { error: "collab_debug: targetPaths is required and must be non-empty." };
12063
- }
12064
- const options = {
12065
- targetPaths: i.targetPaths,
12066
- timeoutMs: i.timeoutMs,
12067
- maxTargetFiles: i.maxTargetFiles,
12068
- contextWindow: i.contextWindow
12069
- };
12070
- try {
12071
- const report = await director.spawnCollab(options);
12072
- return {
12073
- sessionId: report.sessionId,
12074
- overallVerdict: report.overallVerdict,
12075
- bugCount: report.bugs.length,
12076
- planCount: report.refactorPlans.length,
12077
- evaluationCount: report.evaluations.length,
12078
- summary: report.summary,
12079
- bugs: report.bugs,
12080
- refactorPlans: report.refactorPlans,
12081
- evaluations: report.evaluations
12082
- };
12083
- } catch (err) {
12084
- const msg = err instanceof Error ? err.message : String(err);
12085
- return { error: "collab_debug failed: " + msg };
12086
- }
12087
- }
12088
- };
12089
- }
12090
- function makeFleetEmitTool(director) {
12091
- return {
12092
- name: "fleet_emit",
12093
- 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.",
12376
+ return {
12377
+ name: "fleet_session",
12378
+ description: "Read a subagent's JSONL transcript and extract its last assistant text, stop reason, and tool-use count. Use this to see what a running or timed-out subagent actually produced.",
12094
12379
  permission: "auto",
12095
12380
  mutating: false,
12096
12381
  inputSchema: {
12097
12382
  type: "object",
12098
12383
  properties: {
12099
- type: {
12100
- type: "string",
12101
- description: "Event type string (e.g. bug.found, refactor.plan, critic.evaluation, progress, result)."
12102
- },
12103
- payload: {
12104
- type: "object",
12105
- description: "Event payload. Structure depends on event type. Use null if no payload."
12106
- }
12384
+ subagentId: { type: "string", description: "Subagent id to read the transcript of." },
12385
+ tail: { type: "number", description: "Number of trailing JSONL lines to return. Omit for the full transcript." }
12107
12386
  },
12108
- required: ["type"]
12387
+ required: ["subagentId"]
12109
12388
  },
12110
12389
  async execute(input) {
12111
12390
  const i = input;
12112
- director.fleet.emit({
12113
- subagentId: director.id,
12114
- ts: Date.now(),
12115
- type: i.type,
12116
- payload: i.payload ?? {}
12117
- });
12118
- return { ok: true, event: i.type };
12391
+ const result = await director.readSession(i.subagentId, i.tail);
12392
+ if (!result) {
12393
+ return {
12394
+ error: `fleet_session: transcript unavailable for "${i.subagentId}". Is sessionsRoot configured?`
12395
+ };
12396
+ }
12397
+ return result;
12119
12398
  }
12120
12399
  };
12121
12400
  }
12122
- function makeWorkCompleteTool(director) {
12401
+ function makeFleetHealthTool(director) {
12123
12402
  return {
12124
- name: "work_complete",
12125
- 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.",
12403
+ name: "fleet_health",
12404
+ description: "Per-subagent health report: budget pressure (pct of limits consumed), last activity timestamp, and current status. Use to decide whether to assign more work to a subagent or spawn a fresh one.",
12126
12405
  permission: "auto",
12127
12406
  mutating: false,
12128
12407
  inputSchema: { type: "object", properties: {}, required: [] },
12129
12408
  async execute() {
12130
- director.workComplete();
12131
- return { ok: true, message: "Fleet wind-down signaled. No new spawns or task dispatches." };
12132
- }
12133
- };
12134
- }
12135
- var GLOB_CHARS = /* @__PURE__ */ new Set(["*", "?", "["]);
12136
- var IS_WINDOWS = process.platform === "win32";
12137
- var SEP = IS_WINDOWS ? "\\" : "/";
12138
- function isGlob(p) {
12139
- for (const c of p) {
12140
- if (GLOB_CHARS.has(c)) return true;
12141
- }
12142
- return false;
12143
- }
12144
- function globToRegex(pat) {
12145
- let i = 0, re = "^";
12146
- while (i < pat.length) {
12147
- const c = pat[i];
12148
- if (c === "*") {
12149
- if (pat[i + 1] === "*") {
12150
- re += ".*";
12151
- i += 2;
12152
- if (pat[i] === "/") i++;
12153
- } else {
12154
- re += "[^/\\\\]*";
12155
- i++;
12156
- }
12157
- } else if (c === "?") {
12158
- re += "[^/\\\\]";
12159
- i++;
12160
- } else if (c === "[") {
12161
- let cls = "[";
12162
- i++;
12163
- if (pat[i] === "!" || pat[i] === "^") {
12164
- cls += "^";
12165
- i++;
12166
- }
12167
- while (i < pat.length && pat[i] !== "]") {
12168
- const ch = pat[i] ?? "";
12169
- if (ch === "\\") cls += "\\\\";
12170
- else if (ch === "]" || ch === "^") cls += `\\${ch}`;
12171
- else cls += ch;
12172
- i++;
12173
- }
12174
- cls += "]";
12175
- re += cls;
12176
- i++;
12177
- } else {
12178
- re += c.replace(/[.+^${}()|\\]/g, "\\$&");
12179
- i++;
12180
- }
12181
- }
12182
- return new RegExp(re + "$");
12183
- }
12184
- function baseDir(pat) {
12185
- let i = pat.length - 1;
12186
- while (i >= 0 && !GLOB_CHARS.has(pat[i]) && pat[i] !== SEP && pat[i] !== "/") i--;
12187
- const cut = i >= 0 ? pat.lastIndexOf(SEP, i) : pat.lastIndexOf("/", i);
12188
- return cut < 0 ? "." : pat.slice(0, cut);
12189
- }
12190
- async function expandGlob(pattern) {
12191
- if (!isGlob(pattern)) return [pattern];
12192
- const results = /* @__PURE__ */ new Set();
12193
- const abs = isAbsolute(pattern);
12194
- const base = abs ? baseDir(pattern) : baseDir(pattern);
12195
- const relPat = base === "." ? pattern : pattern.slice(base.length + 1);
12196
- async function walk4(dir, pat) {
12197
- let entries;
12198
- try {
12199
- entries = await fsp.readdir(dir);
12200
- } catch {
12201
- return;
12202
- }
12203
- const firstGlob = pat.search(/[*?[\[]/);
12204
- if (firstGlob < 0) {
12205
- const re = globToRegex(pat);
12206
- for (const e of entries) {
12207
- if (re.test(e)) {
12208
- const full = `${dir}${SEP}${e}`;
12209
- results.add(abs ? resolve(full) : full);
12210
- }
12211
- }
12212
- return;
12213
- }
12214
- const before = pat.slice(0, firstGlob);
12215
- const rest = pat.slice(firstGlob);
12216
- if (before.endsWith("**")) {
12217
- await walk4(dir, rest);
12218
- for (const e of entries) {
12219
- const full = `${dir}${SEP}${e}`;
12220
- try {
12221
- const stat5 = await fsp.stat(full);
12222
- if (stat5.isDirectory()) await walk4(full, rest);
12223
- } catch {
12224
- }
12225
- }
12226
- } else if (before === "") {
12227
- const re = globToRegex(rest);
12228
- for (const e of entries) {
12229
- if (re.test(e)) {
12230
- const full = `${dir}${SEP}${e}`;
12231
- results.add(abs ? resolve(full) : full);
12232
- }
12233
- }
12234
- } else {
12235
- const seg = before.replace(/[*?[\]]/g, "").replace(/\/$/, "");
12236
- if (entries.includes(seg)) {
12237
- const full = `${dir}${SEP}${seg}`;
12238
- try {
12239
- const stat5 = await fsp.stat(full);
12240
- if (stat5.isDirectory()) await walk4(full, rest);
12241
- } catch {
12242
- }
12243
- }
12244
- }
12245
- }
12246
- await walk4(base === "." ? "." : base, relPat);
12247
- return [...results];
12248
- }
12249
-
12250
- // src/coordination/collab-debug.ts
12251
- var DEFAULT_MAX_TARGET_FILES = 30;
12252
- var CollabSession = class extends EventEmitter {
12253
- sessionId;
12254
- options;
12255
- snapshot;
12256
- director;
12257
- fleetBus;
12258
- subagentIds = /* @__PURE__ */ new Map();
12259
- // role → subagentId
12260
- bugs = /* @__PURE__ */ new Map();
12261
- plans = /* @__PURE__ */ new Map();
12262
- evaluations = /* @__PURE__ */ new Map();
12263
- disposers = new Array();
12264
- settled = false;
12265
- timeoutMs;
12266
- cancelled = false;
12267
- alerts = [];
12268
- /** Tracks tool call counts per subagent for progress-based timeout decisions. */
12269
- progressBySubagent = /* @__PURE__ */ new Map();
12270
- /** Last tool call count when a timeout warning was handled. */
12271
- lastTimeoutProgress = /* @__PURE__ */ new Map();
12272
- /** Session-level timeout timer handle (cleared on cancel or natural completion). */
12273
- _timeoutTimer;
12274
- constructor(director, fleetBus, options) {
12275
- super();
12276
- this.sessionId = randomUUID();
12277
- this.options = options;
12278
- this.director = director;
12279
- this.fleetBus = fleetBus;
12280
- this.timeoutMs = options.timeoutMs ?? 10 * 60 * 1e3;
12281
- if (options.prebuiltSnapshot) {
12282
- this.snapshot = options.prebuiltSnapshot;
12283
- } else {
12284
- this.snapshot = {
12285
- id: this.sessionId,
12286
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
12287
- files: []
12409
+ const status = director.status();
12410
+ const snapshot = director.snapshot();
12411
+ const subagents = status.subagents ?? [];
12412
+ const perSubagent = snapshot.perSubagent ?? {};
12413
+ return {
12414
+ subagents: subagents.map((s) => {
12415
+ const usage = perSubagent[s.id];
12416
+ return {
12417
+ id: s.id,
12418
+ status: s.status,
12419
+ lastEventAt: usage?.lastEventAt,
12420
+ budgetPressure: {
12421
+ iterations: usage?.iterations,
12422
+ toolCalls: usage?.toolCalls,
12423
+ costUsd: usage?.cost
12424
+ }
12425
+ };
12426
+ })
12288
12427
  };
12289
12428
  }
12290
- }
12291
- get id() {
12292
- return this.sessionId;
12293
- }
12294
- getSessionAlerts() {
12295
- return [...this.alerts];
12296
- }
12297
- isCancelled() {
12298
- return this.cancelled;
12299
- }
12300
- /**
12301
- * Snapshot of role → subagentId map. The Director calls coordinator.stop()
12302
- * for each agent when cancelling the session, using this map to enumerate
12303
- * all three collab agents.
12304
- */
12305
- getSubagentIds() {
12306
- return new Map(this.subagentIds);
12307
- }
12308
- /**
12309
- * Returns the effective file limit for this session.
12310
- * Priority: explicit `maxTargetFiles` > dynamic from `contextWindow` > `DEFAULT_MAX_TARGET_FILES`.
12311
- */
12312
- effectiveFileLimit() {
12313
- if (this.options.maxTargetFiles !== void 0) {
12314
- return this.options.maxTargetFiles;
12315
- }
12316
- if (this.options.contextWindow !== void 0) {
12317
- return Math.max(5, Math.floor(this.options.contextWindow * 0.4 / 2e3));
12318
- }
12319
- return DEFAULT_MAX_TARGET_FILES;
12320
- }
12321
- async buildSnapshot() {
12322
- if (this.snapshot.files.length > 0) return this.snapshot;
12323
- const allFiles = [];
12324
- for (const pattern of this.options.targetPaths) {
12325
- const expanded = await expandGlob(pattern);
12326
- allFiles.push(...expanded);
12327
- }
12328
- const limit = this.effectiveFileLimit();
12329
- if (allFiles.length > limit) {
12330
- const hint = this.options.contextWindow ? `contextWindow=${this.options.contextWindow} \u2192 calculated limit=${limit}` : `default limit=${DEFAULT_MAX_TARGET_FILES}`;
12331
- throw new Error(
12332
- `[collab_debug] Target has ${allFiles.length} files, which exceeds the limit (${hint}). Narrow the target or pass maxTargetFiles / contextWindow to override. For large codebases, run package-by-package or module-by-module sessions instead of targeting the entire repo.`
12333
- );
12334
- }
12335
- for (const filePath of allFiles) {
12336
- try {
12337
- const content = await fsp.readFile(filePath, "utf8");
12338
- const ext = filePath.split(".").pop() ?? "";
12339
- const language = ext === "ts" || ext === "tsx" ? "typescript" : ext === "js" || ext === "jsx" ? "javascript" : ext === "md" ? "markdown" : ext === "json" ? "json" : void 0;
12340
- this.snapshot.files.push({ path: filePath, content, language });
12341
- } catch {
12342
- this.snapshot.files.push({ path: filePath, content: "", language: void 0 });
12429
+ };
12430
+ }
12431
+ function makeCollabDebugTool(director) {
12432
+ return {
12433
+ name: "collab_debug",
12434
+ description: "Start a collaborative debugging session: BugHunter, RefactorPlanner, and Critic run in parallel on the same target files. BugHunter finds bugs and emits bug.found events. RefactorPlanner listens for bug.found and emits refactor.plan events. Critic evaluates both and emits critic.evaluation events. Returns a structured report with overall verdict (approve / needs_revision / reject).",
12435
+ permission: "auto",
12436
+ mutating: false,
12437
+ inputSchema: {
12438
+ type: "object",
12439
+ properties: {
12440
+ targetPaths: {
12441
+ type: "array",
12442
+ items: { type: "string" },
12443
+ description: "File paths / glob patterns to scan for bugs."
12444
+ },
12445
+ timeoutMs: {
12446
+ type: "number",
12447
+ minimum: 1,
12448
+ description: "Timeout in ms. Default: 600000 (10 minutes)."
12449
+ },
12450
+ maxTargetFiles: {
12451
+ type: "number",
12452
+ minimum: 1,
12453
+ description: "Maximum number of files to include in the snapshot. If not set, the limit is computed dynamically from contextWindow or falls back to the default (30)."
12454
+ },
12455
+ contextWindow: {
12456
+ type: "number",
12457
+ minimum: 1,
12458
+ description: "Context window size (tokens) of the model. When provided and maxTargetFiles is not set, the file limit is computed dynamically as floor((contextWindow * 0.4) / 2000)."
12459
+ }
12460
+ },
12461
+ required: ["targetPaths"]
12462
+ },
12463
+ async execute(input) {
12464
+ const i = input;
12465
+ if (!i.targetPaths?.length) {
12466
+ return { error: "collab_debug: targetPaths is required and must be non-empty." };
12343
12467
  }
12344
- }
12345
- return this.snapshot;
12346
- }
12347
- /**
12348
- * Cancel the session. Emits director.cancel_collab on the FleetBus so all
12349
- * collab agents finish early. The session-level timeout timer is also cleared.
12350
- * Safe to call multiple times (idempotent after first call).
12351
- */
12352
- cancel(reason = "Director cancelled collab session") {
12353
- if (this.settled) return;
12354
- this.cancelled = true;
12355
- if (this._timeoutTimer) {
12356
- clearTimeout(this._timeoutTimer);
12357
- this._timeoutTimer = void 0;
12358
- }
12359
- this.fleetBus.emit({
12360
- subagentId: this.director.id,
12361
- ts: Date.now(),
12362
- type: "director.cancel_collab",
12363
- payload: { sessionId: this.sessionId, reason, cancelledAt: (/* @__PURE__ */ new Date()).toISOString() }
12364
- });
12365
- this.fleetBus.emit({
12366
- subagentId: this.director.id,
12367
- ts: Date.now(),
12368
- type: "collab.cancelled",
12369
- payload: { sessionId: this.sessionId, reason }
12370
- });
12371
- }
12372
- async start() {
12373
- if (this.settled) throw new Error("session already settled");
12374
- this.settled = true;
12375
- await this.buildSnapshot();
12376
- this.wireFleetBus();
12377
- const [bugHunterId, refactorPlannerId, criticId] = await Promise.all([
12378
- this.spawnAgent("bug-hunter", this.buildBugHunterTask()),
12379
- this.spawnAgent("refactor-planner", this.buildRefactorPlannerTask()),
12380
- this.spawnAgent("critic", this.buildCriticTask())
12381
- ]);
12382
- this.subagentIds.set("bug-hunter", bugHunterId);
12383
- this.subagentIds.set("refactor-planner", refactorPlannerId);
12384
- this.subagentIds.set("critic", criticId);
12385
- const timeout = new Promise((_, reject) => {
12386
- this._timeoutTimer = setTimeout(() => {
12387
- this.cancel("Session-level timeout reached");
12388
- reject(new Error(`CollabSession timed out after ${this.timeoutMs}ms`));
12389
- }, this.timeoutMs);
12390
- });
12391
- let results = null;
12392
- try {
12393
- results = await Promise.race([
12394
- Promise.all([
12395
- this.director.awaitTasks([bugHunterId]),
12396
- this.director.awaitTasks([refactorPlannerId]),
12397
- this.director.awaitTasks([criticId])
12398
- ]),
12399
- timeout
12400
- ]);
12401
- } catch (err) {
12402
- if (this._timeoutTimer) {
12403
- clearTimeout(this._timeoutTimer);
12404
- this._timeoutTimer = void 0;
12468
+ const options = {
12469
+ targetPaths: i.targetPaths,
12470
+ timeoutMs: i.timeoutMs,
12471
+ maxTargetFiles: i.maxTargetFiles,
12472
+ contextWindow: i.contextWindow
12473
+ };
12474
+ try {
12475
+ const report = await director.spawnCollab(options);
12476
+ return {
12477
+ sessionId: report.sessionId,
12478
+ overallVerdict: report.overallVerdict,
12479
+ bugCount: report.bugs.length,
12480
+ planCount: report.refactorPlans.length,
12481
+ evaluationCount: report.evaluations.length,
12482
+ summary: report.summary,
12483
+ bugs: report.bugs,
12484
+ refactorPlans: report.refactorPlans,
12485
+ evaluations: report.evaluations
12486
+ };
12487
+ } catch (err) {
12488
+ const msg = err instanceof Error ? err.message : String(err);
12489
+ return { error: "collab_debug failed: " + msg };
12405
12490
  }
12406
- this.cleanup();
12407
- const error = err instanceof Error ? err : new Error(String(err));
12408
- this.emit("session.error", error);
12409
- throw error;
12410
- }
12411
- for (const result of results.flat()) {
12412
- await this.parseAndEmit(result);
12413
12491
  }
12414
- const report = this.assembleReport();
12415
- this.cleanup();
12416
- this.emit("session.done", report);
12417
- return report;
12418
- }
12419
- async parseAndEmit(result) {
12420
- if (result.status !== "success" || result.result == null) return;
12421
- const text = typeof result.result === "string" ? result.result : JSON.stringify(result.result);
12422
- for (const obj of this.extractJsonObjects(text)) {
12423
- const type = "finding" in obj ? "bug.found" : "plan" in obj ? "refactor.plan" : "evaluation" in obj ? "critic.evaluation" : null;
12424
- if (!type) continue;
12425
- this.fleetBus.emit({
12426
- subagentId: result.subagentId,
12427
- taskId: result.taskId,
12492
+ };
12493
+ }
12494
+ function makeFleetEmitTool(director) {
12495
+ return {
12496
+ name: "fleet_emit",
12497
+ 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.",
12498
+ permission: "auto",
12499
+ mutating: false,
12500
+ inputSchema: {
12501
+ type: "object",
12502
+ properties: {
12503
+ type: {
12504
+ type: "string",
12505
+ description: "Event type string (e.g. bug.found, refactor.plan, critic.evaluation, progress, result)."
12506
+ },
12507
+ payload: {
12508
+ type: "object",
12509
+ description: "Event payload. Structure depends on event type. Use null if no payload."
12510
+ }
12511
+ },
12512
+ required: ["type"]
12513
+ },
12514
+ async execute(input) {
12515
+ const i = input;
12516
+ director.fleet.emit({
12517
+ subagentId: director.id,
12428
12518
  ts: Date.now(),
12429
- type,
12430
- payload: obj
12519
+ type: i.type,
12520
+ payload: i.payload ?? {}
12431
12521
  });
12522
+ return { ok: true, event: i.type };
12523
+ }
12524
+ };
12525
+ }
12526
+ function makeWorkCompleteTool(director) {
12527
+ return {
12528
+ name: "work_complete",
12529
+ 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.",
12530
+ permission: "auto",
12531
+ mutating: false,
12532
+ inputSchema: { type: "object", properties: {}, required: [] },
12533
+ async execute() {
12534
+ director.workComplete();
12535
+ return { ok: true, message: "Fleet wind-down signaled. No new spawns or task dispatches." };
12432
12536
  }
12537
+ };
12538
+ }
12539
+
12540
+ // src/coordination/fleet-bus.ts
12541
+ var FleetBus = class {
12542
+ byId = /* @__PURE__ */ new Map();
12543
+ byType = /* @__PURE__ */ new Map();
12544
+ any = /* @__PURE__ */ new Set();
12545
+ /**
12546
+ * Hook a subagent's EventBus into the fleet. Uses `onAny()` (an alias for
12547
+ * `onPattern('*')`) to forward all events with subagent attribution, so
12548
+ * new kernel event types are automatically forwarded without any manual
12549
+ * registration. `subagent.*` events are excluded because they originate
12550
+ * from MultiAgentHost on the parent bus, not the subagent's own bus.
12551
+ *
12552
+ * Returns a disposer that detaches every subscription; call on
12553
+ * subagent teardown so the listeners don't outlive the run.
12554
+ */
12555
+ attach(subagentId, bus, taskId) {
12556
+ const off = bus.onAny((type, payload) => {
12557
+ if (type.startsWith("subagent.")) return;
12558
+ this.emit({ subagentId, taskId, ts: Date.now(), type, payload });
12559
+ });
12560
+ return () => {
12561
+ off();
12562
+ };
12433
12563
  }
12434
- extractJsonObjects(text) {
12435
- const objects = [];
12436
- let depth = 0;
12437
- let start = -1;
12438
- let inString = false;
12439
- let escaped = false;
12440
- for (let i = 0; i < text.length; i++) {
12441
- const ch = text[i];
12442
- if (inString) {
12443
- if (escaped) escaped = false;
12444
- else if (ch === "\\") escaped = true;
12445
- else if (ch === '"') inString = false;
12446
- continue;
12447
- }
12448
- if (ch === '"') {
12449
- inString = true;
12450
- } else if (ch === "{") {
12451
- if (depth === 0) start = i;
12452
- depth++;
12453
- } else if (ch === "}" && depth > 0) {
12454
- depth--;
12455
- if (depth === 0 && start >= 0) {
12456
- const candidate = text.slice(start, i + 1);
12457
- try {
12458
- const parsed = JSON.parse(candidate);
12459
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
12460
- objects.push(parsed);
12461
- }
12462
- } catch {
12463
- }
12464
- start = -1;
12465
- }
12466
- }
12564
+ /** Subscribe to every event from one subagent. */
12565
+ subscribe(subagentId, handler) {
12566
+ let set = this.byId.get(subagentId);
12567
+ if (!set) {
12568
+ set = /* @__PURE__ */ new Set();
12569
+ this.byId.set(subagentId, set);
12467
12570
  }
12468
- return objects;
12571
+ set.add(handler);
12572
+ return () => {
12573
+ set.delete(handler);
12574
+ };
12469
12575
  }
12470
- budgetForRole(role) {
12471
- if (this.options.budgetOverrides?.[role]) {
12472
- return this.options.budgetOverrides[role];
12576
+ /** Subscribe to one event type across all subagents. */
12577
+ filter(type, handler) {
12578
+ let set = this.byType.get(type);
12579
+ if (!set) {
12580
+ set = /* @__PURE__ */ new Set();
12581
+ this.byType.set(type, set);
12473
12582
  }
12474
- const defaults = {
12475
- "bug-hunter": { maxIterations: 2e3, maxToolCalls: 5e3, timeoutMs: 10 * 60 * 1e3 },
12476
- "refactor-planner": { maxIterations: 1500, maxToolCalls: 4e3, timeoutMs: 8 * 60 * 1e3 },
12477
- "critic": { maxIterations: 1e3, maxToolCalls: 3e3, timeoutMs: 6 * 60 * 1e3 }
12583
+ set.add(handler);
12584
+ return () => {
12585
+ set.delete(handler);
12478
12586
  };
12479
- return defaults[role] ?? { maxIterations: 1500, maxToolCalls: 4e3, timeoutMs: 8 * 60 * 1e3 };
12480
12587
  }
12481
- async spawnAgent(role, taskBrief) {
12482
- const budget = this.budgetForRole(role);
12483
- const cfg = {
12484
- id: `${role}-${this.sessionId}`,
12485
- name: role,
12486
- role,
12487
- tools: ["fleet_emit", "fleet_status", "read", "grep", "glob", "bash", "write"],
12488
- maxIterations: budget.maxIterations,
12489
- maxToolCalls: budget.maxToolCalls,
12490
- timeoutMs: budget.timeoutMs
12588
+ /** Subscribe to literally everything. The fleet roll-up uses this. */
12589
+ onAny(handler) {
12590
+ this.any.add(handler);
12591
+ return () => {
12592
+ this.any.delete(handler);
12491
12593
  };
12492
- const subagentId = await this.director.spawn(cfg);
12493
- await this.director.assign({ id: randomUUID(), subagentId, description: taskBrief });
12494
- return subagentId;
12495
- }
12496
- buildBugHunterTask() {
12497
- const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
12498
- const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
12499
- ${f.content}`).join("\n\n");
12500
- return `You are BugHunter. Scan the following files for bugs and code smells.
12501
-
12502
- Target files:
12503
- ${fileContents}
12504
-
12505
- For each bug found, emit it using the fleet_emit tool immediately:
12506
- { "type": "bug.found", "payload": { "finding": { "id": "<uuid>", "type": "<pattern>", "severity": "<critical|high|medium|low>", "location": { "file": "<path>", "line": <n> }, "description": "<explain>", "suggestedFix": "<optional>" } } }
12507
-
12508
- After scanning all files, write your full markdown bug report to:
12509
- ${scratchpad}/bug-hunter-report-${this.sessionId}.md
12510
-
12511
- Important: emit each finding as soon as you find it. Do not batch or wait until the end.`;
12512
- }
12513
- buildRefactorPlannerTask() {
12514
- const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
12515
- const bugHunterReportPath = `${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
12516
- const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
12517
- ${f.content}`).join("\n\n");
12518
- return `You are RefactorPlanner. Plan refactorings for the following files.
12519
-
12520
- Target files:
12521
- ${fileContents}
12522
-
12523
- Read the BugHunter report at: ${bugHunterReportPath}
12524
-
12525
- For each bug you can address, emit a refactor plan using fleet_emit:
12526
- { "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>" } } }
12527
-
12528
- Also write your full markdown plan to:
12529
- ${scratchpad}/refactor-plan-${this.sessionId}.md
12530
-
12531
- Emit each plan immediately. Do not wait until planning is complete.`;
12532
- }
12533
- buildCriticTask() {
12534
- const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
12535
- const bugHunterReportPath = `${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
12536
- const refactorPlanPath = `${scratchpad}/refactor-plan-${this.sessionId}.md`;
12537
- const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
12538
- ${f.content}`).join("\n\n");
12539
- return `You are Critic. Evaluate bug findings and refactor plans.
12540
-
12541
- Target files:
12542
- ${fileContents}
12543
-
12544
- Read the BugHunter report at: ${bugHunterReportPath}
12545
- Read the RefactorPlanner report at: ${refactorPlanPath}
12546
-
12547
- For each bug and refactor plan, emit your evaluation using fleet_emit:
12548
- { "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>" }] } } }
12549
-
12550
- After all evaluations, write your markdown report to:
12551
- ${scratchpad}/critic-report-${this.sessionId}.md
12552
-
12553
- Emit each evaluation immediately. Do not wait until you have read all reports.`;
12554
12594
  }
12555
- wireFleetBus() {
12556
- const dTool = this.fleetBus.filter("tool.executed", (e) => {
12557
- this.progressBySubagent.set(e.subagentId, (this.progressBySubagent.get(e.subagentId) ?? 0) + 1);
12558
- });
12559
- this.disposers.push(dTool);
12560
- const dBudget = this.fleetBus.filter("budget.threshold_reached", (e) => {
12561
- const payload = e.payload;
12562
- const role = this.roleFromSubagentId(e.subagentId);
12563
- if (!role) return;
12564
- const btwNotes = this.director.getLeaderBtwNotes();
12565
- const alert = {
12566
- sessionId: this.sessionId,
12567
- subagentId: e.subagentId,
12568
- role,
12569
- level: "warning" /* WARNING */,
12570
- message: `${role} hit ${payload.kind} soft limit (${payload.used}/${payload.limit})`,
12571
- budgetKind: payload.kind,
12572
- elapsedMs: payload.timeoutMs,
12573
- limit: payload.limit,
12574
- btwNotes
12575
- };
12576
- this.alerts.push(alert);
12577
- this.fleetBus.emit({
12578
- subagentId: e.subagentId,
12579
- ts: Date.now(),
12580
- type: "collab.warning",
12581
- payload: alert
12582
- });
12583
- const decision = this.options.onBudgetWarning?.(alert) ?? "ignore";
12584
- if (decision === "cancel") {
12585
- this.cancel(`Director cancelled: ${role} ${payload.kind} threshold`);
12586
- return;
12587
- }
12588
- if (payload.kind === "timeout" || payload.kind === "idle_timeout") {
12589
- const progress = this.progressBySubagent.get(e.subagentId) ?? 0;
12590
- const lastProgress = this.lastTimeoutProgress.get(e.subagentId) ?? -1;
12591
- if (progress <= lastProgress) {
12592
- payload.deny();
12593
- return;
12595
+ emit(event) {
12596
+ const byId = this.byId.get(event.subagentId);
12597
+ if (byId)
12598
+ for (const h of byId) {
12599
+ try {
12600
+ h(event);
12601
+ } catch {
12594
12602
  }
12595
- this.lastTimeoutProgress.set(e.subagentId, progress);
12596
- const newLimit = Math.min(Math.ceil((payload.timeoutMs ?? payload.limit) * 2), 24 * 60 * 6e4);
12597
- setImmediate(() => {
12598
- payload.extend({ timeoutMs: newLimit });
12599
- });
12600
- return;
12601
- }
12602
- if (decision === "extend") {
12603
- setImmediate(() => {
12604
- const base = Math.max(payload.limit, payload.used);
12605
- const extra = {};
12606
- switch (payload.kind) {
12607
- case "iterations":
12608
- extra.maxIterations = Math.min(Math.ceil(base * 1.5), 5e4);
12609
- break;
12610
- case "tool_calls":
12611
- extra.maxToolCalls = Math.min(Math.ceil(base * 1.5), 1e5);
12612
- break;
12613
- case "tokens":
12614
- extra.maxTokens = Math.min(Math.ceil(base * 1.5), 5e6);
12615
- break;
12616
- case "cost":
12617
- extra.maxCostUsd = Math.min(base * 1.5, 100);
12618
- break;
12619
- }
12620
- payload.extend(extra);
12621
- });
12622
- return;
12623
- }
12624
- if (payload.kind !== "timeout") {
12625
- setImmediate(() => {
12626
- const base = Math.max(payload.limit, payload.used);
12627
- const extra = {};
12628
- switch (payload.kind) {
12629
- case "iterations":
12630
- extra.maxIterations = Math.min(Math.ceil(base * 1.25), 5e4);
12631
- break;
12632
- case "tool_calls":
12633
- extra.maxToolCalls = Math.min(Math.ceil(base * 1.25), 1e5);
12634
- break;
12635
- case "tokens":
12636
- extra.maxTokens = Math.min(Math.ceil(base * 1.25), 5e6);
12637
- break;
12638
- case "cost":
12639
- extra.maxCostUsd = Math.min(base * 1.25, 100);
12640
- break;
12641
- }
12642
- payload.extend(extra);
12643
- });
12644
- }
12645
- });
12646
- this.disposers.push(dBudget);
12647
- const dCancel = this.fleetBus.filter("director.cancel_collab", (e) => {
12648
- const payload = e.payload;
12649
- if (payload.sessionId !== this.sessionId) return;
12650
- this.cancelled = true;
12651
- if (this._timeoutTimer) {
12652
- clearTimeout(this._timeoutTimer);
12653
- this._timeoutTimer = void 0;
12654
- }
12655
- this.fleetBus.emit({
12656
- subagentId: this.director.id,
12657
- ts: Date.now(),
12658
- type: "collab.cancelled",
12659
- payload: { sessionId: this.sessionId, reason: payload.reason }
12660
- });
12661
- });
12662
- this.disposers.push(dCancel);
12663
- const d1 = this.fleetBus.filter("bug.found", (e) => {
12664
- const payload = e.payload;
12665
- if (payload?.finding) {
12666
- this.bugs.set(payload.finding.id, payload.finding);
12667
- this.emit("bug.found", payload);
12668
12603
  }
12669
- });
12670
- this.disposers.push(d1);
12671
- const d2 = this.fleetBus.filter("refactor.plan", (e) => {
12672
- const payload = e.payload;
12673
- if (payload?.plan) {
12674
- this.plans.set(payload.plan.id, payload.plan);
12675
- this.emit("refactor.plan", payload);
12604
+ const byType = this.byType.get(event.type);
12605
+ if (byType)
12606
+ for (const h of byType) {
12607
+ try {
12608
+ h(event);
12609
+ } catch {
12610
+ }
12676
12611
  }
12677
- });
12678
- this.disposers.push(d2);
12679
- const d3 = this.fleetBus.filter("critic.evaluation", (e) => {
12680
- const payload = e.payload;
12681
- if (payload?.evaluation) {
12682
- this.evaluations.set(payload.evaluation.id, payload.evaluation);
12683
- this.emit("critic.evaluation", payload);
12612
+ for (const h of this.any) {
12613
+ try {
12614
+ h(event);
12615
+ } catch {
12684
12616
  }
12685
- });
12686
- this.disposers.push(d3);
12617
+ }
12618
+ }
12619
+ };
12620
+ var FleetUsageAggregator = class {
12621
+ constructor(bus, priceLookup, metaLookup) {
12622
+ this.priceLookup = priceLookup;
12623
+ this.metaLookup = metaLookup;
12624
+ this.unsub.push(bus.filter("provider.response", (e) => this.onProviderResponse(e)));
12625
+ this.unsub.push(bus.filter("tool.executed", (e) => this.onToolExecuted(e)));
12626
+ this.unsub.push(bus.filter("iteration.started", (e) => this.onIterationStarted(e)));
12687
12627
  }
12688
- roleFromSubagentId(subagentId) {
12689
- for (const [role, id] of this.subagentIds) {
12690
- if (id === subagentId) return role;
12691
- }
12692
- const match = subagentId.match(/^(bug-hunter|refactor-planner|critic)/);
12693
- return match?.[1] ?? null;
12628
+ priceLookup;
12629
+ metaLookup;
12630
+ perSubagent = /* @__PURE__ */ new Map();
12631
+ total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
12632
+ unsub = new Array();
12633
+ /**
12634
+ * Remove a terminated subagent's data from the aggregator and subtract its
12635
+ * contribution from the running totals. Call this when a subagent is removed
12636
+ * from the fleet so the aggregator doesn't accumulate unbounded data for
12637
+ * entities that will never emit events again.
12638
+ */
12639
+ removeSubagent(subagentId) {
12640
+ const snap = this.perSubagent.get(subagentId);
12641
+ if (!snap) return;
12642
+ this.perSubagent.delete(subagentId);
12643
+ this.total.input -= snap.input;
12644
+ this.total.output -= snap.output;
12645
+ this.total.cacheRead -= snap.cacheRead;
12646
+ this.total.cacheWrite -= snap.cacheWrite;
12647
+ this.total.cost -= snap.cost;
12694
12648
  }
12695
- assembleReport() {
12696
- const bugList = Array.from(this.bugs.values());
12697
- const planList = Array.from(this.plans.values());
12698
- const evalList = Array.from(this.evaluations.values());
12699
- let disposition = "completed";
12700
- if (this.cancelled) disposition = "cancelled";
12701
- const verdictOrder = {
12702
- approve: 0,
12703
- needs_revision: 1,
12704
- reject: 2
12705
- };
12706
- const overallVerdict = evalList.reduce(
12707
- (worst, eval_) => {
12708
- const w = verdictOrder[worst];
12709
- const c = verdictOrder[eval_.verdict];
12710
- return c > w ? eval_.verdict : worst;
12711
- },
12712
- "approve"
12713
- );
12714
- const summary = this.buildMarkdownSummary(bugList, planList, evalList, overallVerdict, disposition);
12649
+ /** Disposes all fleet-bus subscriptions. Call when the aggregator is no longer needed. */
12650
+ dispose() {
12651
+ for (const off of this.unsub) off();
12652
+ this.unsub.length = 0;
12653
+ }
12654
+ /** Live snapshot — safe to call from a tool's execute() body. */
12655
+ snapshot() {
12715
12656
  return {
12716
- sessionId: this.sessionId,
12717
- startedAt: this.snapshot.createdAt,
12718
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
12719
- targetPaths: this.options.targetPaths,
12720
- disposition,
12721
- bugs: bugList,
12722
- refactorPlans: planList,
12723
- evaluations: evalList,
12724
- alerts: [...this.alerts],
12725
- overallVerdict,
12726
- summary
12657
+ total: { ...this.total },
12658
+ perSubagent: Object.fromEntries(
12659
+ Array.from(this.perSubagent.entries()).map(([k, v]) => [k, { ...v }])
12660
+ )
12727
12661
  };
12728
12662
  }
12729
- buildMarkdownSummary(bugs, plans, evals, overallVerdict, disposition) {
12730
- const lines = [
12731
- `## Collaborative Debugging Report \u2014 ${this.sessionId}`,
12732
- "",
12733
- `**Target:** ${this.options.targetPaths.join(", ")}`,
12734
- `**Disposition:** ${disposition.toUpperCase()}`,
12735
- `**Overall Verdict:** **${overallVerdict.toUpperCase()}**`,
12736
- ""
12737
- ];
12738
- if (this.alerts.length > 0) {
12739
- lines.push("### Alerts", "");
12740
- for (const alert of this.alerts) {
12741
- lines.push(`- **[${alert.level.toUpperCase()}]** ${alert.role}: ${alert.message}`);
12742
- }
12743
- lines.push("");
12663
+ ensure(subagentId) {
12664
+ let snap = this.perSubagent.get(subagentId);
12665
+ if (!snap) {
12666
+ const meta = this.metaLookup?.(subagentId);
12667
+ snap = {
12668
+ subagentId,
12669
+ provider: meta?.provider,
12670
+ model: meta?.model,
12671
+ input: 0,
12672
+ output: 0,
12673
+ cacheRead: 0,
12674
+ cacheWrite: 0,
12675
+ cost: 0,
12676
+ toolCalls: 0,
12677
+ iterations: 0,
12678
+ startedAt: Date.now(),
12679
+ lastEventAt: Date.now()
12680
+ };
12681
+ this.perSubagent.set(subagentId, snap);
12744
12682
  }
12745
- if (bugs.length > 0) {
12746
- lines.push("### Bugs Found", "");
12747
- for (const b of bugs) {
12748
- lines.push(`- **[${b.severity.toUpperCase()}]** \`${b.location.file}:${b.location.line}\` \u2014 ${b.description}`);
12749
- }
12750
- lines.push("");
12683
+ return snap;
12684
+ }
12685
+ onProviderResponse(e) {
12686
+ const snap = this.ensure(e.subagentId);
12687
+ const p = e.payload;
12688
+ const usage = p?.usage;
12689
+ if (!usage) return;
12690
+ snap.input += usage.input ?? 0;
12691
+ snap.output += usage.output ?? 0;
12692
+ snap.cacheRead += usage.cacheRead ?? 0;
12693
+ snap.cacheWrite += usage.cacheWrite ?? 0;
12694
+ this.total.input += usage.input ?? 0;
12695
+ this.total.output += usage.output ?? 0;
12696
+ this.total.cacheRead += usage.cacheRead ?? 0;
12697
+ this.total.cacheWrite += usage.cacheWrite ?? 0;
12698
+ const price = this.priceLookup?.(e.subagentId, snap.provider, snap.model);
12699
+ if (price) {
12700
+ const delta = (usage.input ?? 0) / 1e6 * (price.input ?? 0) + (usage.output ?? 0) / 1e6 * (price.output ?? 0) + (usage.cacheRead ?? 0) / 1e6 * (price.cacheRead ?? 0) + (usage.cacheWrite ?? 0) / 1e6 * (price.cacheWrite ?? 0);
12701
+ snap.cost += delta;
12702
+ this.total.cost += delta;
12751
12703
  }
12752
- if (plans.length > 0) {
12753
- lines.push("### Refactor Plans", "");
12754
- for (const p of plans) {
12755
- lines.push(`- **Phase plan** (risk: ${p.riskScore}, ~${p.estimatedChangeCount} changes)`);
12756
- for (const phase of p.phases) {
12757
- lines.push(` - Phase ${phase.number}: ${phase.title} [${phase.risk}]`);
12758
- }
12759
- }
12760
- lines.push("");
12704
+ snap.lastEventAt = e.ts;
12705
+ }
12706
+ onToolExecuted(e) {
12707
+ const snap = this.ensure(e.subagentId);
12708
+ snap.toolCalls += 1;
12709
+ snap.lastEventAt = e.ts;
12710
+ }
12711
+ onIterationStarted(e) {
12712
+ const snap = this.ensure(e.subagentId);
12713
+ snap.iterations += 1;
12714
+ snap.lastEventAt = e.ts;
12715
+ }
12716
+ };
12717
+
12718
+ // src/coordination/large-answer-store.ts
12719
+ var LargeAnswerStore = class {
12720
+ /**
12721
+ * Responses above this size (in characters) are stored out-of-context.
12722
+ * Below this, the full answer is returned inline (no overhead).
12723
+ * Default: 2000 chars ≈ 400-600 tokens.
12724
+ */
12725
+ sizeThreshold;
12726
+ store = /* @__PURE__ */ new Map();
12727
+ constructor(sizeThreshold = 2e3) {
12728
+ this.sizeThreshold = sizeThreshold;
12729
+ }
12730
+ /**
12731
+ * Store a value, returning a summary + key for inline use.
12732
+ * If the value is below sizeThreshold, returns it as-is (no store entry).
12733
+ */
12734
+ storeAnswer(value) {
12735
+ if (value === void 0 || value === null) {
12736
+ return { summary: String(value), inline: true };
12761
12737
  }
12762
- if (evals.length > 0) {
12763
- lines.push("### Critic Evaluations", "");
12764
- for (const e of evals) {
12765
- lines.push(`- [${e.subjectType}] score=${e.score}/10 \u2014 **${e.verdict.toUpperCase()}**`);
12766
- for (const c of e.concerns) {
12767
- if (c.severity === "blocking") lines.push(` - ${c.description}`);
12768
- }
12769
- }
12770
- lines.push("");
12738
+ const serialized = typeof value === "string" ? value : JSON.stringify(value);
12739
+ const size = serialized.length;
12740
+ if (size <= this.sizeThreshold) {
12741
+ return { summary: serialized.slice(0, 500), inline: true };
12771
12742
  }
12772
- return lines.join("\n");
12743
+ const key = `a-${hashStr(serialized)}`;
12744
+ this.store.set(key, {
12745
+ key,
12746
+ value,
12747
+ size,
12748
+ storedAt: Date.now()
12749
+ });
12750
+ return {
12751
+ key,
12752
+ summary: `[stored: ${size} chars \u2014 use roll_up or ask_result tool to retrieve, key=${key}]`,
12753
+ inline: false
12754
+ };
12773
12755
  }
12774
- cleanup() {
12775
- for (const dispose of this.disposers) dispose();
12776
- this.disposers.length = 0;
12756
+ /**
12757
+ * Retrieve a previously stored answer by its key.
12758
+ * Returns undefined if the key is unknown or the store was cleared.
12759
+ */
12760
+ retrieveAnswer(key) {
12761
+ return this.store.get(key)?.value;
12762
+ }
12763
+ /**
12764
+ * Check if a key exists in the store.
12765
+ */
12766
+ hasAnswer(key) {
12767
+ return this.store.has(key);
12768
+ }
12769
+ /** Number of stored entries. */
12770
+ get size() {
12771
+ return this.store.size;
12772
+ }
12773
+ /** Total characters stored. */
12774
+ get totalChars() {
12775
+ let total = 0;
12776
+ for (const e of this.store.values()) total += e.size;
12777
+ return total;
12778
+ }
12779
+ /** Clear all stored entries. Call at the end of a director run. */
12780
+ clear() {
12781
+ this.store.clear();
12777
12782
  }
12778
12783
  };
12784
+ function hashStr(s) {
12785
+ let h = 5381;
12786
+ for (let i = 0; i < s.length; i++) {
12787
+ h = h * 33 ^ s.charCodeAt(i);
12788
+ }
12789
+ return (h >>> 0).toString(36);
12790
+ }
12791
+ var ROLE_TO_PHASE = (() => {
12792
+ const map = {};
12793
+ for (const [phase, defs] of Object.entries(AGENTS_BY_PHASE)) {
12794
+ for (const def of defs) {
12795
+ const role = def.config.role;
12796
+ if (role) map[role] = phase;
12797
+ }
12798
+ }
12799
+ return map;
12800
+ })();
12801
+ function phaseForRole(role) {
12802
+ return role ? ROLE_TO_PHASE[role] : void 0;
12803
+ }
12804
+ function resolveModelMatrix(matrix, role) {
12805
+ if (!matrix) return void 0;
12806
+ if (role && matrix[role]) return matrix[role];
12807
+ const phase = phaseForRole(role);
12808
+ if (phase && matrix[phase]) return matrix[phase];
12809
+ if (matrix["*"]) return matrix["*"];
12810
+ return void 0;
12811
+ }
12779
12812
 
12780
12813
  // src/coordination/director.ts
12781
12814
  var FleetSpawnBudgetError = class extends Error {
@@ -12947,6 +12980,9 @@ var Director = class _Director {
12947
12980
  maxLeaderContextLoad;
12948
12981
  /** Provider's max context window in tokens. */
12949
12982
  maxContext;
12983
+ /** Per-task model matrix (static record or live getter); resolved
12984
+ * per-spawn when no explicit model is set. */
12985
+ modelMatrix;
12950
12986
  /**
12951
12987
  * When set by `workComplete()`, the director stops dispatching new tasks
12952
12988
  * and terminates all running subagents. Used when the director's LLM decides
@@ -12977,20 +13013,23 @@ var Director = class _Director {
12977
13013
  this.maxBudgetExtensions = opts.maxBudgetExtensions ?? 5;
12978
13014
  this.maxLeaderContextLoad = opts.maxLeaderContextLoad ?? 0.85;
12979
13015
  this.maxContext = opts.maxContext ?? 128e3;
13016
+ this.modelMatrix = opts.modelMatrix;
12980
13017
  this.sessionsRoot = opts.sessionsRoot;
12981
13018
  this.directorRunId = opts.directorRunId ?? this.id;
12982
- this.stateCheckpoint = opts.stateCheckpointPath ? new DirectorStateCheckpoint(opts.stateCheckpointPath, {
12983
- directorRunId: this.id,
12984
- maxSpawns: opts.maxSpawns,
12985
- spawnDepth: this.spawnDepth,
12986
- maxSpawnDepth: this.maxSpawnDepth,
12987
- directorBudget: opts.directorBudget
12988
- }, opts.checkpointDebounceMs ?? 250) : null;
13019
+ this.stateCheckpoint = opts.stateCheckpointPath ? new DirectorStateCheckpoint(
13020
+ opts.stateCheckpointPath,
13021
+ {
13022
+ directorRunId: this.id,
13023
+ maxSpawns: opts.maxSpawns,
13024
+ spawnDepth: this.spawnDepth,
13025
+ maxSpawnDepth: this.maxSpawnDepth,
13026
+ directorBudget: opts.directorBudget
13027
+ },
13028
+ opts.checkpointDebounceMs ?? 250
13029
+ ) : null;
12989
13030
  this.fleetManager = opts.fleetManager;
12990
13031
  if (this.sharedScratchpadPath) {
12991
- void fsp.mkdir(this.sharedScratchpadPath, { recursive: true }).catch(
12992
- (err) => this.logShutdownError("shared_scratchpad_mkdir", err)
12993
- );
13032
+ void fsp.mkdir(this.sharedScratchpadPath, { recursive: true }).catch((err) => this.logShutdownError("shared_scratchpad_mkdir", err));
12994
13033
  }
12995
13034
  this.transport = new InMemoryBridgeTransport();
12996
13035
  this.bridge = new InMemoryAgentBridge(
@@ -13168,7 +13207,12 @@ var Director = class _Director {
13168
13207
  */
13169
13208
  workComplete() {
13170
13209
  this.workCompleteFlag = true;
13171
- this.fleet.emit({ subagentId: this.id, ts: Date.now(), type: "director.work_complete", payload: {} });
13210
+ this.fleet.emit({
13211
+ subagentId: this.id,
13212
+ ts: Date.now(),
13213
+ type: "director.work_complete",
13214
+ payload: {}
13215
+ });
13172
13216
  }
13173
13217
  /** Returns true if `workComplete()` has been called on this director. */
13174
13218
  isWorkComplete() {
@@ -13300,13 +13344,25 @@ var Director = class _Director {
13300
13344
  "workComplete() has been called \u2014 director closed further spawning"
13301
13345
  );
13302
13346
  }
13347
+ if (!config.model && this.modelMatrix) {
13348
+ const matrix = typeof this.modelMatrix === "function" ? this.modelMatrix() : this.modelMatrix;
13349
+ const entry = resolveModelMatrix(matrix, config.role);
13350
+ if (entry) {
13351
+ config.model = entry.model;
13352
+ if (entry.provider) config.provider = entry.provider;
13353
+ }
13354
+ }
13303
13355
  if (this.fleetManager) {
13304
13356
  const rejection = this.fleetManager.canSpawn(config);
13305
13357
  if (rejection) {
13306
- if (rejection.kind === "max_spawn_depth") throw new FleetSpawnBudgetError("max_spawn_depth", rejection.limit, rejection.observed);
13307
- if (rejection.kind === "max_spawns") throw new FleetSpawnBudgetError("max_spawns", rejection.limit, rejection.observed);
13308
- if (rejection.kind === "max_cost_usd") throw new FleetCostCapError(rejection.limit, rejection.observed);
13309
- if (rejection.kind === "max_context_load") throw new FleetContextOverflowError(rejection.limit, rejection.observed);
13358
+ if (rejection.kind === "max_spawn_depth")
13359
+ throw new FleetSpawnBudgetError("max_spawn_depth", rejection.limit, rejection.observed);
13360
+ if (rejection.kind === "max_spawns")
13361
+ throw new FleetSpawnBudgetError("max_spawns", rejection.limit, rejection.observed);
13362
+ if (rejection.kind === "max_cost_usd")
13363
+ throw new FleetCostCapError(rejection.limit, rejection.observed);
13364
+ if (rejection.kind === "max_context_load")
13365
+ throw new FleetContextOverflowError(rejection.limit, rejection.observed);
13310
13366
  }
13311
13367
  } else {
13312
13368
  if (this.spawnDepth >= this.maxSpawnDepth) {
@@ -13336,7 +13392,9 @@ var Director = class _Director {
13336
13392
  this.fleetManager.assignNicknameAndRecord(config);
13337
13393
  } else {
13338
13394
  config.name = assignNickname(role, this._usedNicknames);
13339
- this._usedNicknames.add(config.name.split(" ")[0].toLowerCase().replace(/[^a-z0-9-]/g, "-"));
13395
+ this._usedNicknames.add(
13396
+ config.name.split(" ")[0].toLowerCase().replace(/[^a-z0-9-]/g, "-")
13397
+ );
13340
13398
  }
13341
13399
  }
13342
13400
  result = await this.coordinator.spawn(config);
@@ -13571,7 +13629,11 @@ var Director = class _Director {
13571
13629
  subagentId: taskWithId.subagentId ?? "unassigned",
13572
13630
  taskId: taskWithId.id,
13573
13631
  status: "stopped",
13574
- error: { kind: "aborted_by_parent", message: "Director called workComplete() \u2014 no further tasks will run", retryable: false },
13632
+ error: {
13633
+ kind: "aborted_by_parent",
13634
+ message: "Director called workComplete() \u2014 no further tasks will run",
13635
+ retryable: false
13636
+ },
13575
13637
  iterations: 0,
13576
13638
  toolCalls: 0,
13577
13639
  durationMs: 0