@wrongstack/core 0.9.7 → 0.9.20

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 (67) hide show
  1. package/dist/{agent-bridge-DMVOX0cF.d.ts → agent-bridge-Dti3KXGk.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-DaF_EgRG.d.ts → agent-subagent-runner-U-rs7kk7.d.ts} +3 -4
  3. package/dist/compactor-D7X96RLZ.d.ts +41 -0
  4. package/dist/{config-SkMIDN9L.d.ts → config-CLXMDOSs.d.ts} +5 -1
  5. package/dist/{context-BRNbHmRM.d.ts → context-zkZeILpr.d.ts} +46 -0
  6. package/dist/coordination/index.d.ts +14 -13
  7. package/dist/coordination/index.js +1263 -123
  8. package/dist/coordination/index.js.map +1 -1
  9. package/dist/defaults/index.d.ts +22 -21
  10. package/dist/defaults/index.js +1477 -152
  11. package/dist/defaults/index.js.map +1 -1
  12. package/dist/{events-CiG9qUM_.d.ts → events-DH-9r-_C.d.ts} +42 -1
  13. package/dist/execution/index.d.ts +42 -30
  14. package/dist/execution/index.js +359 -112
  15. package/dist/execution/index.js.map +1 -1
  16. package/dist/extension/index.d.ts +7 -6
  17. package/dist/extension/index.js.map +1 -1
  18. package/dist/{index-CP8638Wm.d.ts → index-BIHJ4uII.d.ts} +12 -8
  19. package/dist/{index-Bsha5K4D.d.ts → index-CFO9QmJo.d.ts} +9 -8
  20. package/dist/index.d.ts +319 -37
  21. package/dist/index.js +2571 -324
  22. package/dist/index.js.map +1 -1
  23. package/dist/infrastructure/index.d.ts +6 -6
  24. package/dist/infrastructure/index.js +36 -0
  25. package/dist/infrastructure/index.js.map +1 -1
  26. package/dist/kernel/index.d.ts +10 -9
  27. package/dist/kernel/index.js.map +1 -1
  28. package/dist/{mcp-servers-BouUWYW6.d.ts → mcp-servers-DkESgh0G.d.ts} +25 -3
  29. package/dist/models/index.d.ts +2 -2
  30. package/dist/models/index.js +1 -1
  31. package/dist/models/index.js.map +1 -1
  32. package/dist/{multi-agent-3ZnTB1aT.d.ts → multi-agent-DNp6lAzg.d.ts} +35 -23
  33. package/dist/{multi-agent-coordinator-DTXF2aAl.d.ts → multi-agent-coordinator-CAhsegPz.d.ts} +20 -2
  34. package/dist/{null-fleet-bus-Chrc_3Pp.d.ts → null-fleet-bus-Dnl19vmf.d.ts} +496 -15
  35. package/dist/observability/index.d.ts +2 -2
  36. package/dist/{path-resolver-TcJfc29Y.d.ts → path-resolver-CHiBL0DD.d.ts} +2 -2
  37. package/dist/{secret-scrubber-DttNiGYA.d.ts → permission-H35s9Evv.d.ts} +2 -7
  38. package/dist/{permission-policy-BpCGYBud.d.ts → permission-policy-CT-nRmTn.d.ts} +9 -2
  39. package/dist/{plan-templates-envSmNlZ.d.ts → plan-templates-Bs8iRwi6.d.ts} +28 -4
  40. package/dist/{provider-runner-BEpikbbN.d.ts → provider-runner-BZdDrWrS.d.ts} +3 -3
  41. package/dist/{retry-policy-BYkq0ugs.d.ts → retry-policy-J9N_PM40.d.ts} +1 -1
  42. package/dist/sdd/index.d.ts +10 -10
  43. package/dist/sdd/index.js +225 -68
  44. package/dist/sdd/index.js.map +1 -1
  45. package/dist/secret-scrubber-3MHDDAtm.d.ts +6 -0
  46. package/dist/{secret-scrubber-QSeI0ADi.d.ts → secret-scrubber-7rSC_emZ.d.ts} +1 -1
  47. package/dist/security/index.d.ts +5 -4
  48. package/dist/security/index.js +37 -6
  49. package/dist/security/index.js.map +1 -1
  50. package/dist/{selector-pox8abg0.d.ts → selector-CFTh3Z6p.d.ts} +1 -1
  51. package/dist/{session-reader-CSWcb5Ga.d.ts → session-reader-C7JJlxOw.d.ts} +2 -2
  52. package/dist/skills/index.d.ts +1 -1
  53. package/dist/skills/index.js +1 -1
  54. package/dist/skills/index.js.map +1 -1
  55. package/dist/storage/index.d.ts +391 -6
  56. package/dist/storage/index.js +760 -40
  57. package/dist/storage/index.js.map +1 -1
  58. package/dist/{system-prompt-Bs-Wliab.d.ts → system-prompt-CneIxVip.d.ts} +1 -1
  59. package/dist/{tool-executor-CsktM3h9.d.ts → tool-executor-flTuxsqO.d.ts} +9 -4
  60. package/dist/types/index.d.ts +16 -15
  61. package/dist/types/index.js +60 -0
  62. package/dist/types/index.js.map +1 -1
  63. package/dist/utils/index.d.ts +25 -2
  64. package/dist/utils/index.js +90 -1
  65. package/dist/utils/index.js.map +1 -1
  66. package/package.json +1 -1
  67. package/dist/compactor-DVTKL7XD.d.ts +0 -23
package/dist/index.js CHANGED
@@ -56,8 +56,8 @@ async function atomicWrite(targetPath, content, opts = {}) {
56
56
  }
57
57
  let mode;
58
58
  try {
59
- const stat9 = await fsp2.stat(targetPath);
60
- mode = stat9.mode & 511;
59
+ const stat8 = await fsp2.stat(targetPath);
60
+ mode = stat8.mode & 511;
61
61
  } catch {
62
62
  mode = opts.mode;
63
63
  }
@@ -1676,6 +1676,7 @@ var HybridCompactor = class {
1676
1676
  }
1677
1677
  async compact(ctx, opts = {}) {
1678
1678
  const beforeTokens = this.estimateMessages(ctx.messages);
1679
+ const beforeFull = this.estimateFullRequest(ctx);
1679
1680
  const reductions = [];
1680
1681
  const policy = readContextWindowPolicy(ctx);
1681
1682
  const preserveK = policy?.preserveK ?? this.preserveK;
@@ -1691,9 +1692,12 @@ var HybridCompactor = class {
1691
1692
  ctx.state.replaceMessages(repaired.messages);
1692
1693
  }
1693
1694
  const afterTokens = this.estimateMessages(ctx.messages);
1695
+ const afterFull = this.estimateFullRequest(ctx);
1694
1696
  return {
1695
1697
  before: beforeTokens,
1696
1698
  after: afterTokens,
1699
+ fullRequestTokensBefore: beforeFull,
1700
+ fullRequestTokensAfter: afterFull,
1697
1701
  reductions,
1698
1702
  repaired: repaired.report.changed ? {
1699
1703
  removedToolUses: repaired.report.removedToolUses,
@@ -1702,6 +1706,14 @@ var HybridCompactor = class {
1702
1706
  } : void 0
1703
1707
  };
1704
1708
  }
1709
+ /**
1710
+ * Estimate the full API request token count: messages + systemPrompt + toolDefs.
1711
+ * This is the accurate figure for context-window pressure monitoring.
1712
+ */
1713
+ estimateFullRequest(ctx) {
1714
+ const breakdown = estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []);
1715
+ return breakdown.total;
1716
+ }
1705
1717
  eliseOldToolResults(ctx, preserveK = this.preserveK, eliseThreshold = this.eliseThreshold) {
1706
1718
  const messages = ctx.messages;
1707
1719
  let pairCount = 0;
@@ -4036,11 +4048,11 @@ function validateAgainstSchema(value, schema) {
4036
4048
  walk2(value, schema, "", errors);
4037
4049
  return { ok: errors.length === 0, errors };
4038
4050
  }
4039
- function walk2(value, schema, path31, errors) {
4051
+ function walk2(value, schema, path35, errors) {
4040
4052
  if (schema.enum !== void 0) {
4041
4053
  if (!schema.enum.some((e) => deepEqual(e, value))) {
4042
4054
  errors.push({
4043
- path: path31 || "<root>",
4055
+ path: path35 || "<root>",
4044
4056
  message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
4045
4057
  });
4046
4058
  return;
@@ -4049,7 +4061,7 @@ function walk2(value, schema, path31, errors) {
4049
4061
  if (typeof schema.type === "string") {
4050
4062
  if (!checkType(value, schema.type)) {
4051
4063
  errors.push({
4052
- path: path31 || "<root>",
4064
+ path: path35 || "<root>",
4053
4065
  message: `expected ${schema.type}, got ${describeType(value)}`
4054
4066
  });
4055
4067
  return;
@@ -4059,19 +4071,19 @@ function walk2(value, schema, path31, errors) {
4059
4071
  const obj = value;
4060
4072
  for (const req of schema.required ?? []) {
4061
4073
  if (!(req in obj)) {
4062
- errors.push({ path: joinPath(path31, req), message: "required property missing" });
4074
+ errors.push({ path: joinPath(path35, req), message: "required property missing" });
4063
4075
  }
4064
4076
  }
4065
4077
  if (schema.properties) {
4066
4078
  for (const [key, subSchema] of Object.entries(schema.properties)) {
4067
4079
  if (key in obj) {
4068
- walk2(obj[key], subSchema, joinPath(path31, key), errors);
4080
+ walk2(obj[key], subSchema, joinPath(path35, key), errors);
4069
4081
  }
4070
4082
  }
4071
4083
  }
4072
4084
  }
4073
4085
  if (schema.type === "array" && Array.isArray(value) && schema.items) {
4074
- value.forEach((item, i) => walk2(item, schema.items, `${path31}[${i}]`, errors));
4086
+ value.forEach((item, i) => walk2(item, schema.items, `${path35}[${i}]`, errors));
4075
4087
  }
4076
4088
  }
4077
4089
  function checkType(value, type) {
@@ -4124,14 +4136,105 @@ function deepEqual(a, b) {
4124
4136
  return false;
4125
4137
  }
4126
4138
 
4139
+ // src/utils/json-repair.ts
4140
+ function completePartialObject(s) {
4141
+ if (!s.trim().startsWith("{")) return s;
4142
+ if (tryParse(s).ok) return s;
4143
+ const stack = [];
4144
+ let inString = false;
4145
+ let escaped = false;
4146
+ let sawKey = false;
4147
+ let prevSig = "";
4148
+ let contentEnd = 0;
4149
+ let stringBraceDepth = 0;
4150
+ for (let i = 0; i < s.length; i++) {
4151
+ const ch = s[i];
4152
+ if (inString) {
4153
+ contentEnd = i + 1;
4154
+ if (escaped) {
4155
+ escaped = false;
4156
+ continue;
4157
+ }
4158
+ if (ch === "\\") {
4159
+ escaped = true;
4160
+ continue;
4161
+ }
4162
+ if (ch === '"') {
4163
+ inString = false;
4164
+ prevSig = '"';
4165
+ stringBraceDepth = 0;
4166
+ continue;
4167
+ }
4168
+ if (ch === "{") stringBraceDepth++;
4169
+ else if (ch === "}" && stringBraceDepth > 0) stringBraceDepth--;
4170
+ continue;
4171
+ }
4172
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") continue;
4173
+ contentEnd = i + 1;
4174
+ if (ch === '"') {
4175
+ inString = true;
4176
+ sawKey = true;
4177
+ stringBraceDepth = 0;
4178
+ prevSig = '"';
4179
+ } else if (ch === "{" || ch === "[") {
4180
+ stack.push(ch);
4181
+ prevSig = ch;
4182
+ } else if (ch === "}" || ch === "]") {
4183
+ stack.pop();
4184
+ prevSig = ch;
4185
+ } else {
4186
+ prevSig = ch;
4187
+ }
4188
+ }
4189
+ if (!sawKey && !inString) return s;
4190
+ let result = s.slice(0, contentEnd);
4191
+ if (inString) {
4192
+ if (escaped) {
4193
+ result = result.slice(0, -1);
4194
+ } else if (endsWithInvalidEscape(result)) {
4195
+ result = result.slice(0, -2);
4196
+ }
4197
+ if (stringBraceDepth > 0) result += "}".repeat(stringBraceDepth);
4198
+ result += '"';
4199
+ } else if (prevSig === ":") {
4200
+ result += "null";
4201
+ }
4202
+ for (let k = stack.length - 1; k >= 0; k--) {
4203
+ result += stack[k] === "{" ? "}" : "]";
4204
+ }
4205
+ if (!tryParse(result).ok) {
4206
+ const patched = result.replace(/:(\s*)([}\]])/g, ":null$2");
4207
+ if (tryParse(patched).ok) result = patched;
4208
+ }
4209
+ return result;
4210
+ }
4211
+ var VALID_ESCAPE = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
4212
+ function endsWithInvalidEscape(str) {
4213
+ const last = str[str.length - 1];
4214
+ if (str[str.length - 2] !== "\\" || last === void 0) return false;
4215
+ if (VALID_ESCAPE.has(last)) return false;
4216
+ let backslashes = 0;
4217
+ for (let k = str.length - 2; k >= 0 && str[k] === "\\"; k--) backslashes++;
4218
+ return backslashes % 2 === 1;
4219
+ }
4220
+ function tryParse(s) {
4221
+ try {
4222
+ return { ok: true, value: JSON.parse(s) };
4223
+ } catch {
4224
+ return { ok: false };
4225
+ }
4226
+ }
4227
+
4127
4228
  // src/storage/session-store.ts
4128
4229
  init_atomic_write();
4129
4230
  var DefaultSessionStore = class {
4130
4231
  dir;
4131
4232
  events;
4233
+ secretScrubber;
4132
4234
  constructor(opts) {
4133
4235
  this.dir = opts.dir;
4134
4236
  this.events = opts.events;
4237
+ this.secretScrubber = opts.secretScrubber;
4135
4238
  }
4136
4239
  /** Join session ID to its absolute path within the store directory. */
4137
4240
  sessionPath(id, ext) {
@@ -4156,7 +4259,11 @@ var DefaultSessionStore = class {
4156
4259
  );
4157
4260
  }
4158
4261
  try {
4159
- return new FileSessionWriter(id, handle, startedAt, meta, this.events, { dir: shardDir, filePath: file });
4262
+ return new FileSessionWriter(id, handle, startedAt, meta, this.events, {
4263
+ dir: shardDir,
4264
+ filePath: file,
4265
+ secretScrubber: this.secretScrubber
4266
+ });
4160
4267
  } catch (err) {
4161
4268
  await handle.close().catch(() => {
4162
4269
  });
@@ -4186,7 +4293,7 @@ var DefaultSessionStore = class {
4186
4293
  provider: data.metadata.provider
4187
4294
  },
4188
4295
  this.events,
4189
- { resumed: true, dir: this.dir, filePath: file }
4296
+ { resumed: true, dir: this.dir, filePath: file, secretScrubber: this.secretScrubber }
4190
4297
  );
4191
4298
  return { writer, data };
4192
4299
  } catch (err) {
@@ -4250,8 +4357,8 @@ var DefaultSessionStore = class {
4250
4357
  return JSON.parse(raw);
4251
4358
  } catch {
4252
4359
  const full = this.sessionPath(id, ".jsonl");
4253
- const stat9 = await fsp2.stat(full);
4254
- const summary = await this.summarize(id, stat9.mtime.toISOString());
4360
+ const stat8 = await fsp2.stat(full);
4361
+ const summary = await this.summarize(id, stat8.mtime.toISOString());
4255
4362
  await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
4256
4363
  console.warn(
4257
4364
  `[session-store] Failed to write manifest for "${id}":`,
@@ -4392,6 +4499,7 @@ var FileSessionWriter = class {
4392
4499
  this.resumed = opts.resumed ?? false;
4393
4500
  this.manifestFile = opts.dir ? path6.join(opts.dir, `${id}.summary.json`) : "";
4394
4501
  this.filePath = opts.filePath ?? "";
4502
+ this.secretScrubber = opts.secretScrubber;
4395
4503
  this.summary = {
4396
4504
  id,
4397
4505
  title: "(empty session)",
@@ -4420,7 +4528,29 @@ var FileSessionWriter = class {
4420
4528
  resumed;
4421
4529
  appendFailCount = 0;
4422
4530
  lastAppendWarnAt = 0;
4423
- promptIndex = 0;
4531
+ secretScrubber;
4532
+ /**
4533
+ * Scrub secrets out of conversation-turn events before they are observed
4534
+ * for the summary, written to the JSONL log, or surfaced on resume. Only
4535
+ * `user_input` / `llm_response` carry free-form user/model text; other event
4536
+ * types either have no secret-bearing content or are already scrubbed
4537
+ * upstream (tool results). Returns the event unchanged when no scrubber is
4538
+ * configured.
4539
+ */
4540
+ scrubEvent(event) {
4541
+ const s = this.secretScrubber;
4542
+ if (!s) return event;
4543
+ if (event.type === "user_input") {
4544
+ return {
4545
+ ...event,
4546
+ content: typeof event.content === "string" ? s.scrub(event.content) : s.scrubObject(event.content)
4547
+ };
4548
+ }
4549
+ if (event.type === "llm_response") {
4550
+ return { ...event, content: s.scrubObject(event.content) };
4551
+ }
4552
+ return event;
4553
+ }
4424
4554
  pendingFileSnapshots = [];
4425
4555
  /** Tracks open tool_use IDs during the current run to serialize on close for resume. */
4426
4556
  openToolUses = /* @__PURE__ */ new Set();
@@ -4452,9 +4582,10 @@ var FileSessionWriter = class {
4452
4582
  this.initDone = true;
4453
4583
  await this.writeSessionStartLazy();
4454
4584
  }
4455
- this.observeForSummary(event);
4585
+ const scrubbed = this.scrubEvent(event);
4586
+ this.observeForSummary(scrubbed);
4456
4587
  try {
4457
- await this.handle.appendFile(`${JSON.stringify(event)}
4588
+ await this.handle.appendFile(`${JSON.stringify(scrubbed)}
4458
4589
  `, "utf8");
4459
4590
  } catch (err) {
4460
4591
  this.appendFailCount++;
@@ -4510,7 +4641,6 @@ var FileSessionWriter = class {
4510
4641
  await this.writeFileSnapshot(promptIndex, [...this.pendingFileSnapshots]);
4511
4642
  this.pendingFileSnapshots = [];
4512
4643
  }
4513
- this.promptIndex = promptIndex + 1;
4514
4644
  await this.append({
4515
4645
  type: "checkpoint",
4516
4646
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -4606,6 +4736,38 @@ var FileSessionWriter = class {
4606
4736
  `;
4607
4737
  await fsp2.writeFile(this.filePath, record, "utf8");
4608
4738
  }
4739
+ /**
4740
+ * Idea #1 — write an in-flight marker. The agent loop should call
4741
+ * this at the start of each long-running operation; a matching
4742
+ * `clearInFlightMarker` follows on clean exit. A stale marker
4743
+ * (no end) is what `SessionRecovery.detectStale` looks for.
4744
+ */
4745
+ async writeInFlightMarker(context) {
4746
+ if (!context || context.length > 500) {
4747
+ throw new Error("In-flight context must be 1..500 chars");
4748
+ }
4749
+ await this.append({
4750
+ type: "in_flight_start",
4751
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
4752
+ context
4753
+ });
4754
+ this.events?.emit("in_flight.started", { context, ts: (/* @__PURE__ */ new Date()).toISOString() });
4755
+ }
4756
+ /**
4757
+ * Idea #1 — close the in-flight marker. Idempotent in spirit
4758
+ * (you can call it after a successful iteration even if you
4759
+ * didn't open one this round) — but the session log records
4760
+ * every call so postmortem tooling can see "the agent finished
4761
+ * cleanly X times, then died without finishing Y".
4762
+ */
4763
+ async clearInFlightMarker(reason) {
4764
+ await this.append({
4765
+ type: "in_flight_end",
4766
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
4767
+ reason
4768
+ });
4769
+ this.events?.emit("in_flight.ended", { reason, ts: (/* @__PURE__ */ new Date()).toISOString() });
4770
+ }
4609
4771
  };
4610
4772
  function userInputTitle(content) {
4611
4773
  const text = typeof content === "string" ? content : content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
@@ -5077,6 +5239,7 @@ function isSecretField2(name) {
5077
5239
  }
5078
5240
 
5079
5241
  // src/storage/config-loader.ts
5242
+ init_atomic_write();
5080
5243
  var BEHAVIOR_DEFAULTS = {
5081
5244
  version: 1,
5082
5245
  context: {
@@ -5237,6 +5400,40 @@ var DefaultConfigLoader = class {
5237
5400
  }
5238
5401
  return Object.freeze(cfg);
5239
5402
  }
5403
+ /**
5404
+ * Persist a sync config to ~/.wrongstack/sync.json, with the token encrypted
5405
+ * by the vault (if provided). The file is isolated from the main config
5406
+ * hierarchy to prevent accidental commits.
5407
+ */
5408
+ async persistSyncConfig(cfg) {
5409
+ let toWrite = { ...cfg };
5410
+ if (this.vault && toWrite.githubToken && !toWrite.githubToken.startsWith("enc:")) {
5411
+ toWrite = { ...toWrite, githubToken: this.vault.encrypt(toWrite.githubToken) };
5412
+ }
5413
+ await atomicWrite(this.paths.syncConfig, JSON.stringify(toWrite, null, 2), { mode: 384 });
5414
+ }
5415
+ /**
5416
+ * Read ~/.wrongstack/sync.json (encrypted GitHub token storage) and decrypt
5417
+ * the token if a vault is available. Returns null if the file doesn't exist.
5418
+ * This is separate from main config loading because sync.json is intentionally
5419
+ * isolated — it should never be part of project-local or env-driven config.
5420
+ */
5421
+ async loadSyncConfig() {
5422
+ try {
5423
+ const raw = await fsp2.readFile(this.paths.syncConfig, "utf8");
5424
+ const parsed = safeParse(raw);
5425
+ if (!parsed.ok || !parsed.value) return null;
5426
+ if (this.vault) {
5427
+ const decrypted = decryptConfigSecrets2({ sync: parsed.value }, this.vault);
5428
+ return decrypted.sync ?? null;
5429
+ }
5430
+ return parsed.value;
5431
+ } catch (err) {
5432
+ if (err.code === "ENOENT") return null;
5433
+ console.warn("[config] Failed to load sync config:", err);
5434
+ return null;
5435
+ }
5436
+ }
5240
5437
  async readJson(file) {
5241
5438
  let raw;
5242
5439
  try {
@@ -6160,7 +6357,11 @@ var DefaultPermissionPolicy = class {
6160
6357
  return { permission: "deny", source: "deny", reason: "session soft deny (user pressed no)" };
6161
6358
  }
6162
6359
  if (this.sessionAllowed.has(subjectKey)) {
6163
- return { permission: "auto", source: "trust", reason: "session soft allow (user pressed yes)" };
6360
+ return {
6361
+ permission: "auto",
6362
+ source: "trust",
6363
+ reason: "session soft allow (user pressed yes)"
6364
+ };
6164
6365
  }
6165
6366
  if (entry?.deny && subject && matchAny(entry.deny, subject)) {
6166
6367
  return { permission: "deny", source: "deny", reason: "matched deny pattern" };
@@ -6180,7 +6381,11 @@ var DefaultPermissionPolicy = class {
6180
6381
  const decision = await this.promptDelegate(tool, input, subject ?? tool.name);
6181
6382
  if (decision === "always") {
6182
6383
  await this.trust({ tool: tool.name, pattern: subject ?? tool.name });
6183
- return { permission: "auto", source: "user", reason: "destructive yolo always-allowed" };
6384
+ return {
6385
+ permission: "auto",
6386
+ source: "user",
6387
+ reason: "destructive yolo always-allowed"
6388
+ };
6184
6389
  }
6185
6390
  if (decision === "deny") {
6186
6391
  await this.deny({ tool: tool.name, pattern: subject ?? tool.name });
@@ -6188,13 +6393,22 @@ var DefaultPermissionPolicy = class {
6188
6393
  }
6189
6394
  return { permission: decision === "yes" ? "auto" : "deny", source: "user" };
6190
6395
  }
6191
- return { permission: "confirm", source: "yolo_destructive", riskTier: "destructive", reason: "destructive tool needs explicit approval even in yolo mode" };
6396
+ return {
6397
+ permission: "confirm",
6398
+ source: "yolo_destructive",
6399
+ riskTier: "destructive",
6400
+ reason: "destructive tool needs explicit approval even in yolo mode"
6401
+ };
6192
6402
  }
6193
6403
  return { permission: "auto", source: "yolo" };
6194
6404
  }
6195
6405
  if (tool.name === "write" && subject) {
6196
6406
  if (ctx.hasRead(subject)) {
6197
- return { permission: "auto", source: "context", reason: "file already read in this session" };
6407
+ return {
6408
+ permission: "auto",
6409
+ source: "context",
6410
+ reason: "file already read in this session"
6411
+ };
6198
6412
  }
6199
6413
  }
6200
6414
  if (tool.permission === "auto" && !tool.mutating) {
@@ -6302,6 +6516,10 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
6302
6516
  // arbitrary shell — use exec for constrained shell
6303
6517
  "write",
6304
6518
  // arbitrary file write
6519
+ "edit",
6520
+ // arbitrary in-project file modification (equivalent to write)
6521
+ "replace",
6522
+ // arbitrary multi-file find/replace (equivalent to write)
6305
6523
  "scaffold",
6306
6524
  // arbitrary file generation outside project root
6307
6525
  "patch",
@@ -6311,12 +6529,22 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
6311
6529
  "exec"
6312
6530
  // restricted shell but with arbitrary command args
6313
6531
  ]);
6532
+ /**
6533
+ * Tools from MCP servers (`mcp__<server>__<tool>`) are external code of
6534
+ * unknown capability — they may wrap a shell or filesystem. They are
6535
+ * fail-closed here: not auto-approved for subagents by default, so the
6536
+ * leader must allow them explicitly per-spawn.
6537
+ */
6538
+ static isMcpTool(name) {
6539
+ return name.startsWith("mcp__");
6540
+ }
6314
6541
  async evaluate(tool) {
6315
- if (tool.permission === "deny" || _AutoApprovePermissionPolicy.DENY.has(tool.name)) {
6542
+ const blocked = _AutoApprovePermissionPolicy.DENY.has(tool.name) || _AutoApprovePermissionPolicy.isMcpTool(tool.name);
6543
+ if (tool.permission === "deny" || blocked) {
6316
6544
  return {
6317
6545
  permission: "deny",
6318
6546
  source: "subagent_guard",
6319
- reason: _AutoApprovePermissionPolicy.DENY.has(tool.name) ? `tool ${tool.name} is not auto-approved for subagents \u2014 ask the leader to allow it explicitly` : "tool default deny"
6547
+ reason: blocked ? `tool ${tool.name} is not auto-approved for subagents \u2014 ask the leader to allow it explicitly` : "tool default deny"
6320
6548
  };
6321
6549
  }
6322
6550
  return { permission: "auto", source: "yolo" };
@@ -6561,7 +6789,12 @@ function safeJsonOrRaw(s) {
6561
6789
  try {
6562
6790
  return JSON.parse(s);
6563
6791
  } catch {
6564
- return { _raw: s };
6792
+ const repaired = completePartialObject(s);
6793
+ try {
6794
+ return JSON.parse(repaired);
6795
+ } catch {
6796
+ return { _raw: repaired };
6797
+ }
6565
6798
  }
6566
6799
  }
6567
6800
  function handleToolUseStop(state, ev) {
@@ -6790,8 +7023,9 @@ var IntelligentCompactor = class {
6790
7023
  }
6791
7024
  async compact(ctx, opts = {}) {
6792
7025
  const beforeTokens = this.estimateTokens(ctx.messages);
7026
+ const beforeFull = this.estimateFullRequest(ctx);
6793
7027
  const reductions = [];
6794
- const load = beforeTokens / this.maxContext;
7028
+ const load = beforeFull / this.maxContext;
6795
7029
  const aggressive = load >= this.hardThreshold ? true : opts.aggressive ?? load >= this.softThreshold;
6796
7030
  const saved1 = this.eliseOldToolResults(ctx);
6797
7031
  if (saved1 > 0) reductions.push({ phase: "elision", saved: saved1 });
@@ -6805,9 +7039,12 @@ var IntelligentCompactor = class {
6805
7039
  const repaired = repairToolUseAdjacency(ctx.messages);
6806
7040
  if (repaired.report.changed) ctx.state.replaceMessages(repaired.messages);
6807
7041
  const afterTokens = this.estimateTokens(ctx.messages);
7042
+ const afterFull = this.estimateFullRequest(ctx);
6808
7043
  return {
6809
7044
  before: beforeTokens,
6810
7045
  after: afterTokens,
7046
+ fullRequestTokensBefore: beforeFull,
7047
+ fullRequestTokensAfter: afterFull,
6811
7048
  reductions,
6812
7049
  repaired: repaired.report.changed ? {
6813
7050
  removedToolUses: repaired.report.removedToolUses,
@@ -6816,6 +7053,14 @@ var IntelligentCompactor = class {
6816
7053
  } : void 0
6817
7054
  };
6818
7055
  }
7056
+ /**
7057
+ * Estimate the full API request token count: messages + systemPrompt + toolDefs.
7058
+ * This is the accurate figure for context-window pressure monitoring.
7059
+ */
7060
+ estimateFullRequest(ctx) {
7061
+ const breakdown = estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []);
7062
+ return breakdown.total;
7063
+ }
6819
7064
  async summarizeAncientTurns(ctx) {
6820
7065
  const messages = ctx.messages;
6821
7066
  const cutoff = Math.max(0, messages.length - this.preserveK * 2);
@@ -6828,29 +7073,26 @@ var IntelligentCompactor = class {
6828
7073
  try {
6829
7074
  summaryText = await this.callSummarizer(toSummarize, ctx);
6830
7075
  } catch {
6831
- const toolNames = /* @__PURE__ */ new Set();
6832
- const filePaths = /* @__PURE__ */ new Set();
6833
- let userTurns = 0;
6834
- let assistantTurns = 0;
7076
+ const preservedMessages = [];
6835
7077
  for (const m of toSummarize) {
6836
- if (m.role === "user") userTurns++;
6837
- else if (m.role === "assistant") {
6838
- assistantTurns++;
6839
- if (Array.isArray(m.content)) {
6840
- for (const b of m.content) {
6841
- if (b.type === "tool_use") toolNames.add(b.name ?? "unknown");
6842
- }
6843
- }
7078
+ if (m.role === "system") continue;
7079
+ if (typeof m.content === "string") {
7080
+ preservedMessages.push(m);
7081
+ } else if (Array.isArray(m.content)) {
7082
+ const textParts = m.content.filter(isTextBlock);
7083
+ const toolParts = m.content.filter((b) => b.type === "tool_use" || b.type === "tool_result");
7084
+ const next = {
7085
+ role: m.role,
7086
+ content: [
7087
+ ...textParts,
7088
+ ...toolParts.length > 0 ? [{ type: "text", text: `[${toolParts.length} tool call(s) omitted]` }] : []
7089
+ ]
7090
+ };
7091
+ preservedMessages.push(next);
6844
7092
  }
6845
- const text = typeof m.content === "string" ? m.content : "";
6846
- const matches = text.matchAll(/(?:[\w.,\-/@]+\/)*[\w.,\-/@]+\.\w+/g);
6847
- for (const m_ of matches) filePaths.add(m_[0]);
6848
7093
  }
6849
- const parts = [`${toSummarize.length} turns (${userTurns} user, ${assistantTurns} assistant)`];
6850
- if (toolNames.size > 0) parts.push(`tools: ${[...toolNames].join(", ")}`);
6851
- if (filePaths.size > 0) parts.push(`files: ${[...filePaths].slice(0, 10).join(", ")}`);
6852
- summaryText = parts.join(" | ");
6853
- if (!summaryText) summaryText = `${toSummarize.length} earlier turns omitted`;
7094
+ summaryText = preservedMessages.map((m) => `[${m.role}]: ${typeof m.content === "string" ? m.content : m.content.filter(isTextBlock).map((b) => b.text).join(" ")}`).join("\n");
7095
+ if (!summaryText) summaryText = `${toSummarize.length} earlier turns (semantic content preserved)`;
6854
7096
  }
6855
7097
  const summaryMsg = {
6856
7098
  role: "system",
@@ -7096,7 +7338,7 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
7096
7338
  const res = await this.provider.complete(req, { signal: ac.signal });
7097
7339
  const textBlocks = res.content.filter(isTextBlock);
7098
7340
  raw = textBlocks.map((b) => b.text).join("\n").trim();
7099
- } catch (err) {
7341
+ } catch (_err) {
7100
7342
  return this.fallbackSelect(messages, effectiveBudget);
7101
7343
  }
7102
7344
  return this.parseSelectorOutput(raw, messages.length);
@@ -7188,15 +7430,24 @@ var SelectiveCompactor = class {
7188
7430
  }
7189
7431
  async compact(ctx, opts = {}) {
7190
7432
  const beforeTokens = this.estimateTokens(ctx.messages);
7433
+ const beforeFull = this.estimateFullRequest(ctx);
7191
7434
  const reductions = [];
7192
- const load = beforeTokens / this.maxContext;
7435
+ const load = beforeFull / this.maxContext;
7193
7436
  const shouldCompact = load >= this.warnThreshold || opts.aggressive;
7194
7437
  if (!shouldCompact) {
7195
7438
  const saved = this.eliseOldToolResults(ctx);
7196
7439
  if (saved > 0) reductions.push({ phase: "elision", saved });
7197
7440
  const repair2 = this.repairProtocolAdjacency(ctx);
7198
7441
  const afterTokens2 = this.estimateTokens(ctx.messages);
7199
- return { before: beforeTokens, after: afterTokens2, reductions, repaired: repair2 };
7442
+ const afterFull2 = this.estimateFullRequest(ctx);
7443
+ return {
7444
+ before: beforeTokens,
7445
+ after: afterTokens2,
7446
+ fullRequestTokensBefore: beforeFull,
7447
+ fullRequestTokensAfter: afterFull2,
7448
+ reductions,
7449
+ repaired: repair2
7450
+ };
7200
7451
  }
7201
7452
  const savedElision = this.eliseOldToolResults(ctx);
7202
7453
  if (savedElision > 0) reductions.push({ phase: "elision", saved: savedElision });
@@ -7208,7 +7459,23 @@ var SelectiveCompactor = class {
7208
7459
  }
7209
7460
  const repair = this.repairProtocolAdjacency(ctx);
7210
7461
  const afterTokens = this.estimateTokens(ctx.messages);
7211
- return { before: beforeTokens, after: afterTokens, reductions, repaired: repair };
7462
+ const afterFull = this.estimateFullRequest(ctx);
7463
+ return {
7464
+ before: beforeTokens,
7465
+ after: afterTokens,
7466
+ fullRequestTokensBefore: beforeFull,
7467
+ fullRequestTokensAfter: afterFull,
7468
+ reductions,
7469
+ repaired: repair
7470
+ };
7471
+ }
7472
+ /**
7473
+ * Estimate the full API request token count: messages + systemPrompt + toolDefs.
7474
+ * This is the accurate figure for context-window pressure monitoring.
7475
+ */
7476
+ estimateFullRequest(ctx) {
7477
+ const breakdown = estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []);
7478
+ return breakdown.total;
7212
7479
  }
7213
7480
  repairProtocolAdjacency(ctx) {
7214
7481
  const repaired = repairToolUseAdjacency(ctx.messages);
@@ -7398,7 +7665,8 @@ var LEVEL_RANK2 = { warn: 0, soft: 1, hard: 2 };
7398
7665
  var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7399
7666
  name = "AutoCompaction";
7400
7667
  compactor;
7401
- estimator;
7668
+ /** Deprecated. Kept for backward compat with tests that pass simpleEstimator. */
7669
+ _estimator;
7402
7670
  warnThreshold;
7403
7671
  softThreshold;
7404
7672
  hardThreshold;
@@ -7408,19 +7676,6 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7408
7676
  events;
7409
7677
  failureMode;
7410
7678
  policyProvider;
7411
- /**
7412
- * Calibration factor applied to the estimator output. The estimator is now
7413
- * expected to already include messages + system prompt + tool definitions
7414
- * (see `estimateRequestTokens.total`), so no overhead boost is needed. Kept
7415
- * as a constant so a future calibration against real provider tokenization
7416
- * (BPE vs the chars/3.5 rough estimator) can dial this without touching the
7417
- * threshold-check math.
7418
- *
7419
- * Historical note: was 1.3 when the estimator only counted messages — that
7420
- * double-counted system+tools once the estimator was upgraded, firing
7421
- * compaction ~30% earlier than intended (e.g. real 56% load showed as 73%).
7422
- */
7423
- static OVERHEAD_FACTOR = 1;
7424
7679
  /**
7425
7680
  * Once a compaction attempt reduces nothing (preserveK protects everything,
7426
7681
  * no oversized tool_results remain to elide), retrying on every iteration
@@ -7433,8 +7688,11 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7433
7688
  lastNoopAttempt = null;
7434
7689
  /**
7435
7690
  * @param compactor Compactor to use for compaction.
7436
- * @param maxContext Provider's max context window in tokens.
7437
- * @param estimator Token estimation function.
7691
+ * @param maxContext Provider's max context window in tokens.
7692
+ * @param _estimator Deprecated parameter kept for backward compatibility.
7693
+ * The middleware now uses `estimateRequestTokens` internally
7694
+ * for accurate full-request token counting (messages +
7695
+ * systemPrompt + toolDefs).
7438
7696
  * @param thresholds Threshold fractions (0-1) of maxContext.
7439
7697
  * @param opts Optional behavior. By default, failures at the
7440
7698
  * hard threshold throw AGENT_CONTEXT_OVERFLOW so
@@ -7442,11 +7700,11 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7442
7700
  * provider context overflow. Warn/soft failures
7443
7701
  * still emit compaction.failed and continue.
7444
7702
  */
7445
- constructor(compactor, maxContext, estimator, thresholds, optsOrAggressiveOn = {}, events) {
7703
+ constructor(compactor, maxContext, _estimator, thresholds, optsOrAggressiveOn = {}, events) {
7446
7704
  const opts = typeof optsOrAggressiveOn === "string" ? { aggressiveOn: optsOrAggressiveOn, events } : optsOrAggressiveOn;
7447
7705
  this.compactor = compactor;
7448
7706
  this._maxContext = maxContext;
7449
- this.estimator = estimator;
7707
+ this._estimator = _estimator;
7450
7708
  this.warnThreshold = thresholds.warn;
7451
7709
  this.softThreshold = thresholds.soft;
7452
7710
  this.hardThreshold = thresholds.hard;
@@ -7462,8 +7720,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7462
7720
  }
7463
7721
  handler() {
7464
7722
  return async (ctx, next) => {
7465
- const rawTokens = this.estimator(ctx);
7466
- const tokens = Math.ceil(rawTokens * _AutoCompactionMiddleware.OVERHEAD_FACTOR);
7723
+ const tokens = this._estimator ? this._estimator(ctx) : estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []).total;
7467
7724
  const load = tokens / this._maxContext;
7468
7725
  const policy = this.policyProvider?.(ctx);
7469
7726
  const thresholds = policy?.thresholds ?? {
@@ -7498,7 +7755,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7498
7755
  return delta < _AutoCompactionMiddleware.NOOP_RETRY_DELTA_TOKENS;
7499
7756
  }
7500
7757
  recordAttempt(level, tokens, report) {
7501
- const reduced = report.before > report.after;
7758
+ const reduced = (report.fullRequestTokensBefore ?? report.before) > (report.fullRequestTokensAfter ?? report.after);
7502
7759
  const repaired = !!report.repaired;
7503
7760
  if (reduced || repaired) {
7504
7761
  this.lastNoopAttempt = null;
@@ -8431,17 +8688,7 @@ var SubagentBudget = class _SubagentBudget {
8431
8688
  startTime = null;
8432
8689
  _onThreshold;
8433
8690
  /**
8434
- * Tracks which budget kinds currently have an extension request
8435
- * in flight. While a kind is here, further `checkLimit` calls for the
8436
- * same kind are no-ops — without this dedup, every `recordIteration`
8437
- * after the limit is reached spawns a fresh decision Promise (until
8438
- * the first one lands and patches limits), flooding the FleetBus
8439
- * with redundant threshold events. Cleared in `negotiateExtension`'s
8440
- * `finally`.
8441
- */
8442
- pendingExtensions = /* @__PURE__ */ new Set();
8443
- /**
8444
- * Hard cap on how long `negotiateExtension` waits for the coordinator to
8691
+ * Hard cap on how long `_negotiateExtension` waits for the coordinator to
8445
8692
  * respond before defaulting to 'stop'. Without this fallback an absent
8446
8693
  * or hung listener (Director not built / event filter detached mid-run)
8447
8694
  * leaves the budget over-limit and never enforces anything.
@@ -8503,38 +8750,83 @@ var SubagentBudget = class _SubagentBudget {
8503
8750
  * - `mode === 'auto'` + no listener → throw `BudgetExceededError` (hard stop; no one to ask)
8504
8751
  * - `mode === 'auto'` + listener → throw `BudgetThresholdSignal` with async decision promise
8505
8752
  */
8506
- checkLimit(kind, used, limit) {
8753
+ /**
8754
+ * Collects all exceeded budget kinds into a single NOOP-free negotiation.
8755
+ * Called by recordIteration / recordToolCall / recordUsage — each may call
8756
+ * this for its own kind. The first call starts the negotiation and stores
8757
+ * the Promise in _pendingNegotiation. Subsequent calls for DIFFERENT
8758
+ * kinds (while a negotiation is in flight) are NOOPs — they don't start
8759
+ * new conversations with the coordinator. This prevents an EventBus flood
8760
+ * when multiple budget kinds are exceeded simultaneously in one iteration.
8761
+ *
8762
+ * Returns the kinds that were found to be exceeded (for logging/debugging).
8763
+ */
8764
+ checkLimits(elapsedMs) {
8765
+ const exceeded = [];
8766
+ if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
8767
+ exceeded.push({ kind: "iterations", used: this.iterations, limit: this.limits.maxIterations });
8768
+ }
8769
+ if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
8770
+ exceeded.push({ kind: "tool_calls", used: this.toolCalls, limit: this.limits.maxToolCalls });
8771
+ }
8772
+ const totalTokens = this.tokenInput + this.tokenOutput;
8773
+ if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
8774
+ exceeded.push({ kind: "tokens", used: totalTokens, limit: this.limits.maxTokens });
8775
+ }
8776
+ if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
8777
+ exceeded.push({ kind: "cost", used: this.costUsd, limit: this.limits.maxCostUsd });
8778
+ }
8779
+ if (elapsedMs !== void 0 && this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs) {
8780
+ exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
8781
+ }
8782
+ if (exceeded.length === 0) return [];
8507
8783
  if (!this._onThreshold) {
8508
- throw new BudgetExceededError(kind, limit, used);
8784
+ const first2 = exceeded[0];
8785
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
8509
8786
  }
8510
8787
  if (this._mode === "sync") {
8511
- throw new BudgetExceededError(kind, limit, used);
8788
+ const first2 = exceeded[0];
8789
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
8512
8790
  }
8513
8791
  const bus = this._events;
8514
8792
  if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
8515
- throw new BudgetExceededError(kind, limit, used);
8793
+ const first2 = exceeded[0];
8794
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
8795
+ }
8796
+ for (const entry of exceeded) {
8797
+ if (this._pendingNegotiations.has(entry.kind)) continue;
8798
+ const decision2 = this._negotiateExtension(entry.kind, exceeded);
8799
+ this._pendingNegotiations.set(entry.kind, decision2);
8516
8800
  }
8517
- if (this.pendingExtensions.has(kind)) return;
8518
- this.pendingExtensions.add(kind);
8519
- const decision = this.negotiateExtension(kind, used, limit);
8520
- throw new BudgetThresholdSignal(kind, limit, used, decision);
8801
+ const first = exceeded[0];
8802
+ const decision = this._pendingNegotiations.get(first.kind);
8803
+ throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
8521
8804
  }
8805
+ /**
8806
+ * Per-kind in-flight negotiation Promises. Each budget kind can have its
8807
+ * own concurrent negotiation — e.g. iterations and timeout can both
8808
+ * be exceeded simultaneously without blocking each other. The same kind
8809
+ * cannot start two concurrent negotiations (dedup guard).
8810
+ * Cleared in `_negotiateExtension`'s `finally` block.
8811
+ */
8812
+ _pendingNegotiations = /* @__PURE__ */ new Map();
8522
8813
  /**
8523
8814
  * Drive the threshold handler to a decision. Resolves with `'stop'`
8524
8815
  * (signal the runner to abort) or `{ extend: ... }` (limits already
8525
- * patched in-place; the runner should not abort). Always releases the
8526
- * `pendingExtensions` slot in `finally`.
8816
+ * patched in-place; the runner should not abort). Clears the
8817
+ * per-kind slot in `_pendingNegotiations` in `finally`.
8527
8818
  *
8528
8819
  * The 'continue' return from a sync handler is treated as
8529
8820
  * `{ extend: {} }` — keep going without patching; next overrun fires
8530
8821
  * a fresh signal.
8531
8822
  */
8532
- async negotiateExtension(kind, used, limit) {
8823
+ async _negotiateExtension(kind, exceeded) {
8533
8824
  try {
8825
+ const first = exceeded[0];
8534
8826
  const result = this._onThreshold({
8535
- kind,
8536
- used,
8537
- limit,
8827
+ kind: first.kind,
8828
+ used: first.used,
8829
+ limit: first.limit,
8538
8830
  requestDecision: () => {
8539
8831
  const bus = this._events;
8540
8832
  if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
@@ -8551,20 +8843,22 @@ var SubagentBudget = class _SubagentBudget {
8551
8843
  () => respond("stop"),
8552
8844
  _SubagentBudget.DECISION_TIMEOUT_MS
8553
8845
  );
8554
- bus.emit("budget.threshold_reached", {
8555
- kind,
8556
- used,
8557
- limit,
8558
- timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
8559
- extend: (extra) => {
8560
- clearTimeout(fallback);
8561
- respond({ extend: extra });
8562
- },
8563
- deny: () => {
8564
- clearTimeout(fallback);
8565
- respond("stop");
8566
- }
8567
- });
8846
+ for (const { kind: kind2, used, limit } of exceeded) {
8847
+ bus.emit("budget.threshold_reached", {
8848
+ kind: kind2,
8849
+ used,
8850
+ limit,
8851
+ timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
8852
+ extend: (extra) => {
8853
+ clearTimeout(fallback);
8854
+ respond({ extend: extra });
8855
+ },
8856
+ deny: () => {
8857
+ clearTimeout(fallback);
8858
+ respond("stop");
8859
+ }
8860
+ });
8861
+ }
8568
8862
  });
8569
8863
  }
8570
8864
  });
@@ -8590,47 +8884,39 @@ var SubagentBudget = class _SubagentBudget {
8590
8884
  }
8591
8885
  return decision;
8592
8886
  } finally {
8593
- this.pendingExtensions.delete(kind);
8887
+ this._pendingNegotiations.delete(kind);
8594
8888
  }
8595
8889
  }
8596
8890
  recordIteration() {
8597
8891
  this.iterations++;
8598
- if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
8599
- void this.checkLimit("iterations", this.iterations, this.limits.maxIterations);
8600
- }
8892
+ void this.checkLimits();
8601
8893
  }
8602
8894
  recordToolCall() {
8603
8895
  this.toolCalls++;
8604
- if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
8605
- void this.checkLimit("tool_calls", this.toolCalls, this.limits.maxToolCalls);
8606
- }
8896
+ void this.checkLimits();
8607
8897
  }
8608
8898
  recordUsage(usage, costUsd = 0) {
8609
8899
  this.tokenInput += usage.input;
8610
8900
  this.tokenOutput += usage.output;
8611
8901
  this.costUsd += costUsd;
8612
- const totalTokens = this.tokenInput + this.tokenOutput;
8613
- if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
8614
- void this.checkLimit("tokens", totalTokens, this.limits.maxTokens);
8615
- }
8616
- if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
8617
- void this.checkLimit("cost", this.costUsd, this.limits.maxCostUsd);
8618
- }
8902
+ void this.checkLimits();
8619
8903
  }
8620
8904
  /**
8621
- * Wall-clock budget check. Unlike other limits, timeout check passes through
8622
- * `checkLimit` and is subject to the same negotiation-mode decision table.
8623
- * In practice, `'sync'` mode (the usual test configuration) means a timeout
8624
- * immediately throws `BudgetExceededError`. In production with a coordinator,
8625
- * a timeout emits `budget.threshold_reached` so the Director can decide whether
8626
- * to extend or abort.
8905
+ * Wall-clock budget check. Unlike other limits, timeout is always a hard stop
8906
+ * wall-clock time cannot be "extended" by the coordinator, so it throws
8907
+ * synchronously rather than entering the negotiation flow.
8908
+ *
8909
+ * Decision table:
8910
+ * - no `onThreshold` handler → throw `BudgetExceededError`
8911
+ * - `mode === 'sync'` → throw `BudgetExceededError`
8912
+ * - `mode === 'auto'` + no listener → throw `BudgetExceededError`
8913
+ * - `mode === 'auto'` + listener → throw `BudgetExceededError` (timeout is not extendable)
8627
8914
  */
8628
8915
  checkTimeout() {
8629
8916
  if (this.startTime === null || this.limits.timeoutMs === void 0) return;
8630
8917
  const elapsed = Date.now() - this.startTime;
8631
- if (elapsed > this.limits.timeoutMs) {
8632
- void this.checkLimit("timeout", elapsed, this.limits.timeoutMs);
8633
- }
8918
+ if (elapsed <= this.limits.timeoutMs) return;
8919
+ void this.checkLimits(elapsed);
8634
8920
  }
8635
8921
  /** Returns true if a timeout has occurred without throwing. Useful for races. */
8636
8922
  isTimedOut() {
@@ -11283,11 +11569,46 @@ Working rules:
11283
11569
  - When in doubt, flag as medium rather than ignoring potential issues`
11284
11570
  // Budgets are set by the orchestrator per task — see fleet.ts header.
11285
11571
  };
11572
+ var CRITIC_AGENT = {
11573
+ id: "critic",
11574
+ name: "Critic",
11575
+ role: "critic",
11576
+ prompt: `You are the Critic agent. Your job is to evaluate code quality,
11577
+ architectural decisions, and proposed changes against project conventions,
11578
+ engineering standards, and known quality gates. You do not write code \u2014
11579
+ you judge it.
11580
+
11581
+ Scope:
11582
+ - Evaluate bug severity and fix quality from Bug Hunter reports
11583
+ - Score refactoring plans from Refactor Planner (risk, completeness, trade-offs)
11584
+ - Flag gaps in test coverage, error handling, and edge case coverage
11585
+ - Assess whether a proposed change aligns with existing project patterns
11586
+ - Detect over-engineering or under-engineering relative to the problem scope
11587
+
11588
+ Input format you accept:
11589
+ { "task": "evaluate | score | review", "subject": "bug_report | refactor_plan | diff", "focus": "correctness | maintainability | risk | all" }
11590
+
11591
+ Output: Markdown critic report:
11592
+ - ## Overall Score (0-10 with rationale)
11593
+ - ## Strengths (what's solid)
11594
+ - ## Weaknesses (what needs work)
11595
+ - ## Specific Concerns (with file:line when applicable)
11596
+ - ## Verdict: **Approve / Needs Revision / Reject**
11597
+
11598
+ Working rules:
11599
+ - Be specific \u2014 "looks fine" is not a review. Cite concrete evidence.
11600
+ - When scoring, explain the delta from a perfect score.
11601
+ - If you have no basis to evaluate a concern, say so rather than speculating.
11602
+ - Prioritise correctness over style; correctness issues block approval.
11603
+ - Score thresholds: \u22657 = Approve, 4-6 = Needs Revision, <4 = Reject`
11604
+ // Budgets are set by the orchestrator per task — see fleet.ts header.
11605
+ };
11286
11606
  var FLEET_ROSTER = {
11287
11607
  "audit-log": AUDIT_LOG_AGENT,
11288
11608
  "bug-hunter": BUG_HUNTER_AGENT,
11289
11609
  "refactor-planner": REFACTOR_PLANNER_AGENT,
11290
11610
  "security-scanner": SECURITY_SCANNER_AGENT,
11611
+ "critic": CRITIC_AGENT,
11291
11612
  ...Object.fromEntries(
11292
11613
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.config])
11293
11614
  )
@@ -11297,6 +11618,7 @@ var FLEET_ROSTER_BUDGETS = {
11297
11618
  "bug-hunter": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
11298
11619
  "refactor-planner": { timeoutMs: 7.5 * 60 * 60 * 1e3, maxIterations: 6e3, maxToolCalls: 18e3 },
11299
11620
  "security-scanner": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
11621
+ "critic": { timeoutMs: 5 * 60 * 60 * 1e3, maxIterations: 4e3, maxToolCalls: 12e3 },
11300
11622
  ...Object.fromEntries(
11301
11623
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.budget])
11302
11624
  )
@@ -11377,10 +11699,111 @@ FLEET_ROSTER_BUDGETS["gemini-cli"] = { timeoutMs: 10 * 60 * 60 * 1e3, maxIterati
11377
11699
  FLEET_ROSTER_BUDGETS["copilot"] = { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 };
11378
11700
  FLEET_ROSTER_BUDGETS["openhands"] = { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 };
11379
11701
  FLEET_ROSTER_BUDGETS["goose"] = { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 };
11380
- ({
11702
+ var FLEET_ROSTER_WITHACP = {
11381
11703
  ...FLEET_ROSTER,
11382
11704
  ...Object.fromEntries(ACP_AGENTS.map((a) => [a.role, a]))
11383
- });
11705
+ };
11706
+
11707
+ // src/coordination/subagent-nicknames.ts
11708
+ var NICKNAME_POOL = {
11709
+ // Physics & fundamental sciences
11710
+ "einstein": { name: "Einstein", domain: "physics" },
11711
+ "newton": { name: "Newton", domain: "physics" },
11712
+ "feynman": { name: "Feynman", domain: "physics" },
11713
+ "dirac": { name: "Dirac", domain: "physics" },
11714
+ "bohr": { name: "Bohr", domain: "physics" },
11715
+ "planck": { name: "Planck", domain: "physics" },
11716
+ "curie": { name: "Curie", domain: "physics" },
11717
+ "fermi": { name: "Fermi", domain: "physics" },
11718
+ "heisenberg": { name: "Heisenberg", domain: "physics" },
11719
+ "schrodinger": { name: "Schr\xF6dinger", domain: "physics" },
11720
+ // Mathematics
11721
+ "euclid": { name: "Euclid", domain: "math" },
11722
+ "gauss": { name: "Gauss", domain: "math" },
11723
+ "turing": { name: "Turing", domain: "math" },
11724
+ "poincare": { name: "Poincar\xE9", domain: "math" },
11725
+ "riemann": { name: "Riemann", domain: "math" },
11726
+ "hilbert": { name: "Hilbert", domain: "math" },
11727
+ "pythagoras": { name: "Pythagoras", domain: "math" },
11728
+ // Computing & information theory
11729
+ "von-neumann": { name: "Von Neumann", domain: "computing" },
11730
+ "shannon": { name: "Shannon", domain: "computing" },
11731
+ "hopper": { name: "Hopper", domain: "computing" },
11732
+ "backus": { name: "Backus", domain: "computing" },
11733
+ "knuth": { name: "Knuth", domain: "computing" },
11734
+ "torvalds": { name: "Torvalds", domain: "computing" },
11735
+ "stallman": { name: "Stallman", domain: "computing" },
11736
+ "berners-lee": { name: "Berners-Lee", domain: "computing" },
11737
+ "babbage": { name: "Babbage", domain: "computing" },
11738
+ "lovelace": { name: "Lovelace", domain: "computing" },
11739
+ "klein": { name: "Klein", domain: "computing" },
11740
+ // Electronics & electrical engineering
11741
+ "edison": { name: "Edison", domain: "ee" },
11742
+ "tesla": { name: "Tesla", domain: "ee" },
11743
+ "faraday": { name: "Faraday", domain: "ee" },
11744
+ "maxwell": { name: "Maxwell", domain: "ee" },
11745
+ "ohm": { name: "Ohm", domain: "ee" },
11746
+ "bell": { name: "Bell", domain: "ee" },
11747
+ "marconi": { name: "Marconi", domain: "ee" },
11748
+ "lamarr": { name: "Lamarr", domain: "ee" },
11749
+ // General science / multi-disciplinary
11750
+ "darwin": { name: "Darwin", domain: "biology" },
11751
+ "mendel": { name: "Mendel", domain: "biology" },
11752
+ "pasteur": { name: "Pasteur", domain: "biology" },
11753
+ "hawking": { name: "Hawking", domain: "cosmology" },
11754
+ "sagan": { name: "Sagan", domain: "cosmology" },
11755
+ // Chemistry / materials
11756
+ "lavoisier": { name: "Lavoisier", domain: "chemistry" },
11757
+ "mendeleev": { name: "Mendeleev", domain: "chemistry" }
11758
+ };
11759
+ var ALL_NICKNAMES = Object.values(NICKNAME_POOL);
11760
+ var DOMAIN_PREFERENCES = {
11761
+ "security": ["shannon", "turing", "lamarr", "stallman"],
11762
+ "bug-hunter": ["darwin", "curie", "feynman", "fermi"],
11763
+ "refactor": ["gauss", "hilbert", "euclid", "planck"],
11764
+ "audit-log": ["sagan", "hawking", "poincare", "newton"],
11765
+ "planner": ["hilbert", "gauss", "turing", "euclid"],
11766
+ "researcher": ["sagan", "hawking", "darwin", "pasteur"],
11767
+ "explorer": ["marconi", "bell", "columbus", "polo"],
11768
+ "testing": ["pasteur", "curie", "fermi", "bohr"],
11769
+ "frontend": ["lovelace", "hopper", "babbage", "backus"],
11770
+ "backend": ["torvalds", "stallman", "von-neumann", "backus"],
11771
+ "database": ["turing", "shannon", "backus", "knuth"],
11772
+ "devops": ["tesla", "edison", "faraday", "bell"],
11773
+ "security-scanner": ["shannon", "turing", "lamarr", "stallman"],
11774
+ "refactor-planner": ["gauss", "hilbert", "planck", "newton"],
11775
+ "architect": ["von-neumann", "turing", "gauss", "hilbert"],
11776
+ "critic": ["einstein", "feynman", "dirac", "bohr"],
11777
+ "e2e": ["hopper", "bell", "marconi", "tesla"],
11778
+ "performance": ["knuth", "gauss", "planck", "feynman"],
11779
+ "chaos": ["tesla", "edison", "curie", "fermi"],
11780
+ "cost": ["ohm", "bell", "marconi", "tesla"],
11781
+ // default fallback
11782
+ "default": ["einstein", "newton", "curie", "tesla", "edison", "turing", "shannon", "hopper", "knuth", "stallman"]
11783
+ };
11784
+ function assignNickname(role, used) {
11785
+ const preferences = [
11786
+ ...DOMAIN_PREFERENCES[role] ?? [],
11787
+ ...DOMAIN_PREFERENCES["default"] ?? []
11788
+ ];
11789
+ for (const key of preferences) {
11790
+ const entry = NICKNAME_POOL[key];
11791
+ if (entry && !used.has(key)) {
11792
+ return `${entry.name} (${formatRole(role)})`;
11793
+ }
11794
+ }
11795
+ for (const entry of ALL_NICKNAMES) {
11796
+ const key = Object.entries(NICKNAME_POOL).find(([, v]) => v.name === entry.name)?.[0];
11797
+ if (key && !used.has(key)) {
11798
+ return `${entry.name} (${formatRole(role)})`;
11799
+ }
11800
+ }
11801
+ const counter = used.size + 1;
11802
+ return `Scientist #${counter} (${formatRole(role)})`;
11803
+ }
11804
+ function formatRole(role) {
11805
+ return role.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
11806
+ }
11384
11807
 
11385
11808
  // src/coordination/multi-agent-coordinator.ts
11386
11809
  var DefaultMultiAgentCoordinator = class extends EventEmitter {
@@ -11389,6 +11812,15 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
11389
11812
  runner;
11390
11813
  fleetBus;
11391
11814
  subagents = /* @__PURE__ */ new Map();
11815
+ /**
11816
+ * Base nickname keys already handed out this run (e.g. `einstein`, `tesla`).
11817
+ * Prevents two workers sharing a name. Direct `coordinator.spawn()` callers
11818
+ * (parallel/eternal engine, SDD parallel run) don't go through
11819
+ * `Director.spawn()` where nicknames are normally assigned, so the
11820
+ * coordinator upgrades placeholder names ("Executor", "slot-ab12cd", role
11821
+ * names) to memorable ones here — that's what surfaces in the fleet monitor.
11822
+ */
11823
+ usedNicknames = /* @__PURE__ */ new Set();
11392
11824
  pendingTasks = [];
11393
11825
  completedResults = [];
11394
11826
  totalIterations = 0;
@@ -11437,7 +11869,26 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
11437
11869
  this.config.maxConcurrent = Math.floor(n);
11438
11870
  this.tryDispatchNext();
11439
11871
  }
11872
+ /**
11873
+ * Upgrade a placeholder/role-derived name to a memorable scientist nickname
11874
+ * (e.g. "Einstein (Executor)"). A name is treated as a placeholder when it is
11875
+ * empty, equals the role (case-insensitive), is a generic default
11876
+ * ("subagent"/"adhoc"/"generic"), or is an auto-generated `slot-…` id.
11877
+ * Explicit, human-chosen names — including nicknames already assigned by
11878
+ * `Director.spawn()` — are left untouched, so this never double-assigns.
11879
+ */
11880
+ withNickname(subagent) {
11881
+ const role = subagent.role ?? "subagent";
11882
+ const name = subagent.name?.trim() ?? "";
11883
+ const isPlaceholder = name === "" || name.toLowerCase() === role.toLowerCase() || name === "subagent" || name === "adhoc" || name === "generic" || /^slot-/.test(name);
11884
+ if (!isPlaceholder) return subagent;
11885
+ const nickname = assignNickname(role, this.usedNicknames);
11886
+ const baseKey = nickname.split(" ")[0].toLowerCase().replace(/[^a-z0-9-]/g, "-");
11887
+ this.usedNicknames.add(baseKey);
11888
+ return { ...subagent, name: nickname };
11889
+ }
11440
11890
  async spawn(subagent) {
11891
+ subagent = this.withNickname(subagent);
11441
11892
  const id = subagent.id || randomUUID();
11442
11893
  if (this.subagents.has(id)) {
11443
11894
  throw new Error(`Subagent id "${id}" already exists \u2014 refusing to overwrite`);
@@ -12031,12 +12482,12 @@ var GOAL_COMPLETE_MARKER2 = /^\s*\[goal[_\s-]?complete\]\s*$/im;
12031
12482
  var ParallelEternalEngine = class {
12032
12483
  constructor(opts) {
12033
12484
  this.opts = opts;
12034
- this.goalPath = goalFilePath(opts.projectRoot);
12485
+ this.goalPath = opts.goalPath ?? goalFilePath(opts.projectRoot);
12035
12486
  this.slots = Math.min(16, Math.max(1, opts.parallelSlots ?? 4));
12036
12487
  this.timeoutMs = opts.iterationTimeoutMs ?? 3e5;
12037
12488
  this.dispatchEnabled = opts.dispatch !== false;
12038
12489
  this.dispatchClassifier = opts.dispatchClassifier;
12039
- this.agentFactory = opts.subagentFactory ?? (async (config) => ({
12490
+ this.agentFactory = opts.subagentFactory ?? (async (_config) => ({
12040
12491
  agent: this.opts.agent,
12041
12492
  events: this.opts.agent.events
12042
12493
  }));
@@ -12130,7 +12581,6 @@ var ParallelEternalEngine = class {
12130
12581
  }
12131
12582
  const tasks = await this.decomposeGoal(goal);
12132
12583
  if (!tasks || tasks.length === 0) {
12133
- await sleep2(5e3);
12134
12584
  return false;
12135
12585
  }
12136
12586
  const fanOut = await this.fanOut(goal, tasks);
@@ -12151,6 +12601,7 @@ var ParallelEternalEngine = class {
12151
12601
  });
12152
12602
  if (fanOut.goalComplete) {
12153
12603
  this.stopRequested = true;
12604
+ this.state = "stopped";
12154
12605
  return true;
12155
12606
  }
12156
12607
  await this.maybeCompact();
@@ -12331,7 +12782,11 @@ ${lastFew}` : "No prior iterations.",
12331
12782
  } finally {
12332
12783
  clearTimeout(timer);
12333
12784
  }
12334
- } catch {
12785
+ } catch (err) {
12786
+ this.opts.onError?.(
12787
+ err instanceof Error ? err : new Error(String(err)),
12788
+ this.consecutiveFailures
12789
+ );
12335
12790
  return [];
12336
12791
  }
12337
12792
  }
@@ -12548,7 +13003,11 @@ Working rules:
12548
13003
  thrashing, terminate it rather than letting cost climb silently.
12549
13004
  6. Never claim a subagent's work as your own without verifying it. If a
12550
13005
  result looks wrong, ask_subagent for clarification before passing it
12551
- to the user.`;
13006
+ to the user.
13007
+ 7. Wind down when satisfied. When the results are good enough, call
13008
+ work_complete \u2014 no new subagents will spawn and queued tasks complete
13009
+ as aborted. Running subagents finish naturally. Call terminate_subagent
13010
+ only for ones you need to stop immediately.`;
12552
13011
  var DEFAULT_SUBAGENT_BASELINE = `You are a subagent operating under a Director. You were spawned to handle
12553
13012
  a specific slice of a larger plan \u2014 do that slice well and report back.
12554
13013
 
@@ -12693,14 +13152,12 @@ var FleetBus = class {
12693
13152
  };
12694
13153
  var FleetUsageAggregator = class {
12695
13154
  constructor(bus, priceLookup, metaLookup) {
12696
- this.bus = bus;
12697
13155
  this.priceLookup = priceLookup;
12698
13156
  this.metaLookup = metaLookup;
12699
13157
  bus.filter("provider.response", (e) => this.onProviderResponse(e));
12700
13158
  bus.filter("tool.executed", (e) => this.onToolExecuted(e));
12701
13159
  bus.filter("iteration.started", (e) => this.onIterationStarted(e));
12702
13160
  }
12703
- bus;
12704
13161
  priceLookup;
12705
13162
  metaLookup;
12706
13163
  perSubagent = /* @__PURE__ */ new Map();
@@ -12938,7 +13395,7 @@ function makeRollUpTool(director) {
12938
13395
  function makeTerminateTool(director) {
12939
13396
  return {
12940
13397
  name: "terminate_subagent",
12941
- description: "Forcibly abort a subagent.",
13398
+ description: 'Forcibly abort a subagent. The subagent finishes its current iteration then exits with status "stopped".',
12942
13399
  permission: "auto",
12943
13400
  mutating: true,
12944
13401
  inputSchema: { type: "object", properties: { subagentId: { type: "string", description: "Subagent to abort." } }, required: ["subagentId"] },
@@ -12949,6 +13406,19 @@ function makeTerminateTool(director) {
12949
13406
  }
12950
13407
  };
12951
13408
  }
13409
+ function makeTerminateAllTool(director) {
13410
+ return {
13411
+ name: "terminate_all",
13412
+ description: 'Forcibly stop every subagent in the fleet and drain the pending task queue. In-flight tasks are terminated mid-execution; pending tasks receive "aborted_by_parent" completion immediately. Use this when the fleet is wedged, looping, or you need a clean slate. Compare: work_complete stops spawning but lets running agents finish naturally.',
13413
+ permission: "auto",
13414
+ mutating: true,
13415
+ inputSchema: { type: "object", properties: {}, required: [] },
13416
+ async execute() {
13417
+ await director.terminateAll();
13418
+ return { ok: true, message: `Fleet shutdown complete \u2014 all subagents stopped, pending tasks drained.` };
13419
+ }
13420
+ };
13421
+ }
12952
13422
  function makeFleetStatusTool(director) {
12953
13423
  return {
12954
13424
  name: "fleet_status",
@@ -12992,7 +13462,6 @@ function makeFleetSessionTool(director) {
12992
13462
  type: "object",
12993
13463
  properties: {
12994
13464
  subagentId: { type: "string", description: "Subagent id to read the transcript of." },
12995
- /** Number of trailing lines to return (last N JSONL lines). Default: all. */
12996
13465
  tail: { type: "number", description: "Number of trailing JSONL lines to return. Omit for the full transcript." }
12997
13466
  },
12998
13467
  required: ["subagentId"]
@@ -13028,8 +13497,6 @@ function makeFleetHealthTool(director) {
13028
13497
  id: s.id,
13029
13498
  status: s.status,
13030
13499
  lastEventAt: usage?.lastEventAt,
13031
- // Budget pressure: fraction of each limit consumed if we have it.
13032
- // BudgetWarning events carry used/limit ratios; surface them here.
13033
13500
  budgetPressure: {
13034
13501
  iterations: usage?.iterations,
13035
13502
  toolCalls: usage?.toolCalls,
@@ -13041,20 +13508,616 @@ function makeFleetHealthTool(director) {
13041
13508
  }
13042
13509
  };
13043
13510
  }
13044
-
13045
- // src/coordination/director.ts
13046
- var FleetSpawnBudgetError = class extends Error {
13047
- kind;
13048
- limit;
13049
- observed;
13050
- constructor(kind, limit, observed) {
13051
- super(
13052
- kind === "max_spawns" ? `Director spawn budget exceeded: tried to spawn #${observed} but maxSpawns is ${limit}` : `Director spawn depth budget exceeded: this director is at depth ${observed} and maxSpawnDepth is ${limit}`
13053
- );
13054
- this.name = "FleetSpawnBudgetError";
13055
- this.kind = kind;
13056
- this.limit = limit;
13057
- this.observed = observed;
13511
+ function makeCollabDebugTool(director) {
13512
+ return {
13513
+ name: "collab_debug",
13514
+ 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).",
13515
+ permission: "auto",
13516
+ mutating: false,
13517
+ inputSchema: {
13518
+ type: "object",
13519
+ properties: {
13520
+ targetPaths: {
13521
+ type: "array",
13522
+ items: { type: "string" },
13523
+ description: "File paths / glob patterns to scan for bugs."
13524
+ },
13525
+ timeoutMs: {
13526
+ type: "number",
13527
+ description: "Timeout in ms. Default: 600000 (10 minutes)."
13528
+ }
13529
+ },
13530
+ required: ["targetPaths"]
13531
+ },
13532
+ async execute(input) {
13533
+ const i = input;
13534
+ if (!i.targetPaths?.length) {
13535
+ return { error: "collab_debug: targetPaths is required and must be non-empty." };
13536
+ }
13537
+ const options = {
13538
+ targetPaths: i.targetPaths,
13539
+ timeoutMs: i.timeoutMs
13540
+ };
13541
+ try {
13542
+ const report = await director.spawnCollab(options);
13543
+ return {
13544
+ sessionId: report.sessionId,
13545
+ overallVerdict: report.overallVerdict,
13546
+ bugCount: report.bugs.length,
13547
+ planCount: report.refactorPlans.length,
13548
+ evaluationCount: report.evaluations.length,
13549
+ summary: report.summary,
13550
+ bugs: report.bugs,
13551
+ refactorPlans: report.refactorPlans,
13552
+ evaluations: report.evaluations
13553
+ };
13554
+ } catch (err) {
13555
+ const msg = err instanceof Error ? err.message : String(err);
13556
+ return { error: "collab_debug failed: " + msg };
13557
+ }
13558
+ }
13559
+ };
13560
+ }
13561
+ function makeFleetEmitTool(director) {
13562
+ return {
13563
+ name: "fleet_emit",
13564
+ 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.",
13565
+ permission: "auto",
13566
+ mutating: false,
13567
+ inputSchema: {
13568
+ type: "object",
13569
+ properties: {
13570
+ type: {
13571
+ type: "string",
13572
+ description: "Event type string (e.g. bug.found, refactor.plan, critic.evaluation, progress, result)."
13573
+ },
13574
+ payload: {
13575
+ type: "object",
13576
+ description: "Event payload. Structure depends on event type. Use null if no payload."
13577
+ }
13578
+ },
13579
+ required: ["type"]
13580
+ },
13581
+ async execute(input) {
13582
+ const i = input;
13583
+ director.fleet.emit({
13584
+ subagentId: director.id,
13585
+ ts: Date.now(),
13586
+ type: i.type,
13587
+ payload: i.payload ?? {}
13588
+ });
13589
+ return { ok: true, event: i.type };
13590
+ }
13591
+ };
13592
+ }
13593
+ function makeWorkCompleteTool(director) {
13594
+ return {
13595
+ name: "work_complete",
13596
+ 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.",
13597
+ permission: "auto",
13598
+ mutating: false,
13599
+ inputSchema: { type: "object", properties: {}, required: [] },
13600
+ async execute() {
13601
+ director.workComplete();
13602
+ return { ok: true, message: "Fleet wind-down signaled. No new spawns or task dispatches." };
13603
+ }
13604
+ };
13605
+ }
13606
+ var CollabSession = class extends EventEmitter {
13607
+ sessionId;
13608
+ options;
13609
+ snapshot;
13610
+ director;
13611
+ fleetBus;
13612
+ subagentIds = /* @__PURE__ */ new Map();
13613
+ // role → subagentId
13614
+ bugs = /* @__PURE__ */ new Map();
13615
+ plans = /* @__PURE__ */ new Map();
13616
+ evaluations = /* @__PURE__ */ new Map();
13617
+ disposers = new Array();
13618
+ settled = false;
13619
+ timeoutMs;
13620
+ cancelled = false;
13621
+ alerts = [];
13622
+ /** Tracks tool call counts per subagent for progress-based timeout decisions. */
13623
+ progressBySubagent = /* @__PURE__ */ new Map();
13624
+ /** Last tool call count when a timeout warning was handled. */
13625
+ lastTimeoutProgress = /* @__PURE__ */ new Map();
13626
+ /** Session-level timeout timer handle (cleared on cancel or natural completion). */
13627
+ _timeoutTimer;
13628
+ constructor(director, fleetBus, options) {
13629
+ super();
13630
+ this.sessionId = randomUUID();
13631
+ this.options = options;
13632
+ this.director = director;
13633
+ this.fleetBus = fleetBus;
13634
+ this.timeoutMs = options.timeoutMs ?? 10 * 60 * 1e3;
13635
+ if (options.prebuiltSnapshot) {
13636
+ this.snapshot = options.prebuiltSnapshot;
13637
+ } else {
13638
+ this.snapshot = {
13639
+ id: this.sessionId,
13640
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
13641
+ files: []
13642
+ };
13643
+ }
13644
+ }
13645
+ get id() {
13646
+ return this.sessionId;
13647
+ }
13648
+ getSessionAlerts() {
13649
+ return [...this.alerts];
13650
+ }
13651
+ isCancelled() {
13652
+ return this.cancelled;
13653
+ }
13654
+ /**
13655
+ * Snapshot of role → subagentId map. The Director calls coordinator.stop()
13656
+ * for each agent when cancelling the session, using this map to enumerate
13657
+ * all three collab agents.
13658
+ */
13659
+ getSubagentIds() {
13660
+ return new Map(this.subagentIds);
13661
+ }
13662
+ async buildSnapshot() {
13663
+ if (this.snapshot.files.length > 0) return this.snapshot;
13664
+ for (const filePath of this.options.targetPaths) {
13665
+ try {
13666
+ const content = await fsp2.readFile(filePath, "utf8");
13667
+ const ext = filePath.split(".").pop() ?? "";
13668
+ const language = ext === "ts" || ext === "tsx" ? "typescript" : ext === "js" || ext === "jsx" ? "javascript" : ext === "md" ? "markdown" : ext === "json" ? "json" : void 0;
13669
+ this.snapshot.files.push({ path: filePath, content, language });
13670
+ } catch {
13671
+ this.snapshot.files.push({ path: filePath, content: "", language: void 0 });
13672
+ }
13673
+ }
13674
+ return this.snapshot;
13675
+ }
13676
+ /**
13677
+ * Cancel the session. Emits director.cancel_collab on the FleetBus so all
13678
+ * collab agents finish early. The session-level timeout timer is also cleared.
13679
+ * Safe to call multiple times (idempotent after first call).
13680
+ */
13681
+ cancel(reason = "Director cancelled collab session") {
13682
+ if (this.settled) return;
13683
+ this.cancelled = true;
13684
+ if (this._timeoutTimer) {
13685
+ clearTimeout(this._timeoutTimer);
13686
+ this._timeoutTimer = void 0;
13687
+ }
13688
+ this.fleetBus.emit({
13689
+ subagentId: this.director.id,
13690
+ ts: Date.now(),
13691
+ type: "director.cancel_collab",
13692
+ payload: { sessionId: this.sessionId, reason, cancelledAt: (/* @__PURE__ */ new Date()).toISOString() }
13693
+ });
13694
+ this.fleetBus.emit({
13695
+ subagentId: this.director.id,
13696
+ ts: Date.now(),
13697
+ type: "collab.cancelled",
13698
+ payload: { sessionId: this.sessionId, reason }
13699
+ });
13700
+ }
13701
+ async start() {
13702
+ if (this.settled) throw new Error("session already settled");
13703
+ this.settled = true;
13704
+ await this.buildSnapshot();
13705
+ this.wireFleetBus();
13706
+ const [bugHunterId, refactorPlannerId, criticId] = await Promise.all([
13707
+ this.spawnAgent("bug-hunter", this.buildBugHunterTask()),
13708
+ this.spawnAgent("refactor-planner", this.buildRefactorPlannerTask()),
13709
+ this.spawnAgent("critic", this.buildCriticTask())
13710
+ ]);
13711
+ this.subagentIds.set("bug-hunter", bugHunterId);
13712
+ this.subagentIds.set("refactor-planner", refactorPlannerId);
13713
+ this.subagentIds.set("critic", criticId);
13714
+ const timeout = new Promise((_, reject) => {
13715
+ this._timeoutTimer = setTimeout(() => {
13716
+ this.cancel("Session-level timeout reached");
13717
+ reject(new Error(`CollabSession timed out after ${this.timeoutMs}ms`));
13718
+ }, this.timeoutMs);
13719
+ });
13720
+ let results;
13721
+ try {
13722
+ results = await Promise.race([
13723
+ Promise.all([
13724
+ this.director.awaitTasks([bugHunterId]),
13725
+ this.director.awaitTasks([refactorPlannerId]),
13726
+ this.director.awaitTasks([criticId])
13727
+ ]),
13728
+ timeout
13729
+ ]);
13730
+ } catch (err) {
13731
+ if (this._timeoutTimer) {
13732
+ clearTimeout(this._timeoutTimer);
13733
+ this._timeoutTimer = void 0;
13734
+ }
13735
+ this.cleanup();
13736
+ const error = err instanceof Error ? err : new Error(String(err));
13737
+ this.emit("session.error", error);
13738
+ throw error;
13739
+ }
13740
+ for (const result of results.flat()) {
13741
+ await this.parseAndEmit(result);
13742
+ }
13743
+ const report = this.assembleReport();
13744
+ this.cleanup();
13745
+ this.emit("session.done", report);
13746
+ return report;
13747
+ }
13748
+ async parseAndEmit(result) {
13749
+ if (result.status !== "success" || result.result == null) return;
13750
+ const text = typeof result.result === "string" ? result.result : JSON.stringify(result.result);
13751
+ for (const obj of this.extractJsonObjects(text)) {
13752
+ const type = "finding" in obj ? "bug.found" : "plan" in obj ? "refactor.plan" : "evaluation" in obj ? "critic.evaluation" : null;
13753
+ if (!type) continue;
13754
+ this.fleetBus.emit({
13755
+ subagentId: result.subagentId,
13756
+ taskId: result.taskId,
13757
+ ts: Date.now(),
13758
+ type,
13759
+ payload: obj
13760
+ });
13761
+ }
13762
+ }
13763
+ extractJsonObjects(text) {
13764
+ const objects = [];
13765
+ let depth = 0;
13766
+ let start = -1;
13767
+ let inString = false;
13768
+ let escaped = false;
13769
+ for (let i = 0; i < text.length; i++) {
13770
+ const ch = text[i];
13771
+ if (inString) {
13772
+ if (escaped) escaped = false;
13773
+ else if (ch === "\\") escaped = true;
13774
+ else if (ch === '"') inString = false;
13775
+ continue;
13776
+ }
13777
+ if (ch === '"') {
13778
+ inString = true;
13779
+ } else if (ch === "{") {
13780
+ if (depth === 0) start = i;
13781
+ depth++;
13782
+ } else if (ch === "}" && depth > 0) {
13783
+ depth--;
13784
+ if (depth === 0 && start >= 0) {
13785
+ const candidate = text.slice(start, i + 1);
13786
+ try {
13787
+ const parsed = JSON.parse(candidate);
13788
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
13789
+ objects.push(parsed);
13790
+ }
13791
+ } catch {
13792
+ }
13793
+ start = -1;
13794
+ }
13795
+ }
13796
+ }
13797
+ return objects;
13798
+ }
13799
+ budgetForRole(role) {
13800
+ if (this.options.budgetOverrides?.[role]) {
13801
+ return this.options.budgetOverrides[role];
13802
+ }
13803
+ const defaults = {
13804
+ "bug-hunter": { maxIterations: 2e3, maxToolCalls: 5e3, timeoutMs: 10 * 60 * 1e3 },
13805
+ "refactor-planner": { maxIterations: 1500, maxToolCalls: 4e3, timeoutMs: 8 * 60 * 1e3 },
13806
+ "critic": { maxIterations: 1e3, maxToolCalls: 3e3, timeoutMs: 6 * 60 * 1e3 }
13807
+ };
13808
+ return defaults[role] ?? { maxIterations: 1500, maxToolCalls: 4e3, timeoutMs: 8 * 60 * 1e3 };
13809
+ }
13810
+ async spawnAgent(role, taskBrief) {
13811
+ const budget = this.budgetForRole(role);
13812
+ const cfg = {
13813
+ id: `${role}-${this.sessionId}`,
13814
+ name: role,
13815
+ role,
13816
+ tools: ["fleet_emit", "fleet_status", "read", "grep", "glob", "bash", "write"],
13817
+ maxIterations: budget.maxIterations,
13818
+ maxToolCalls: budget.maxToolCalls,
13819
+ timeoutMs: budget.timeoutMs
13820
+ };
13821
+ const subagentId = await this.director.spawn(cfg);
13822
+ await this.director.assign({ id: randomUUID(), subagentId, description: taskBrief });
13823
+ return subagentId;
13824
+ }
13825
+ buildBugHunterTask() {
13826
+ const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
13827
+ const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
13828
+ ${f.content}`).join("\n\n");
13829
+ return `You are BugHunter. Scan the following files for bugs and code smells.
13830
+
13831
+ Target files:
13832
+ ${fileContents}
13833
+
13834
+ For each bug found, emit it using the fleet_emit tool immediately:
13835
+ { "type": "bug.found", "payload": { "finding": { "id": "<uuid>", "type": "<pattern>", "severity": "<critical|high|medium|low>", "location": { "file": "<path>", "line": <n> }, "description": "<explain>", "suggestedFix": "<optional>" } } }
13836
+
13837
+ After scanning all files, write your full markdown bug report to:
13838
+ ${scratchpad}/bug-hunter-report-${this.sessionId}.md
13839
+
13840
+ Important: emit each finding as soon as you find it. Do not batch or wait until the end.`;
13841
+ }
13842
+ buildRefactorPlannerTask() {
13843
+ const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
13844
+ const bugHunterReportPath = `${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
13845
+ const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
13846
+ ${f.content}`).join("\n\n");
13847
+ return `You are RefactorPlanner. Plan refactorings for the following files.
13848
+
13849
+ Target files:
13850
+ ${fileContents}
13851
+
13852
+ Read the BugHunter report at: ${bugHunterReportPath}
13853
+
13854
+ For each bug you can address, emit a refactor plan using fleet_emit:
13855
+ { "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>" } } }
13856
+
13857
+ Also write your full markdown plan to:
13858
+ ${scratchpad}/refactor-plan-${this.sessionId}.md
13859
+
13860
+ Emit each plan immediately. Do not wait until planning is complete.`;
13861
+ }
13862
+ buildCriticTask() {
13863
+ const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
13864
+ const bugHunterReportPath = `${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
13865
+ const refactorPlanPath = `${scratchpad}/refactor-plan-${this.sessionId}.md`;
13866
+ const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
13867
+ ${f.content}`).join("\n\n");
13868
+ return `You are Critic. Evaluate bug findings and refactor plans.
13869
+
13870
+ Target files:
13871
+ ${fileContents}
13872
+
13873
+ Read the BugHunter report at: ${bugHunterReportPath}
13874
+ Read the RefactorPlanner report at: ${refactorPlanPath}
13875
+
13876
+ For each bug and refactor plan, emit your evaluation using fleet_emit:
13877
+ { "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>" }] } } }
13878
+
13879
+ After all evaluations, write your markdown report to:
13880
+ ${scratchpad}/critic-report-${this.sessionId}.md
13881
+
13882
+ Emit each evaluation immediately. Do not wait until you have read all reports.`;
13883
+ }
13884
+ wireFleetBus() {
13885
+ const dTool = this.fleetBus.filter("tool.executed", (e) => {
13886
+ this.progressBySubagent.set(e.subagentId, (this.progressBySubagent.get(e.subagentId) ?? 0) + 1);
13887
+ });
13888
+ this.disposers.push(dTool);
13889
+ const dBudget = this.fleetBus.filter("budget.threshold_reached", (e) => {
13890
+ const payload = e.payload;
13891
+ const role = this.roleFromSubagentId(e.subagentId);
13892
+ if (!role) return;
13893
+ const btwNotes = this.director.getLeaderBtwNotes();
13894
+ const alert = {
13895
+ sessionId: this.sessionId,
13896
+ subagentId: e.subagentId,
13897
+ role,
13898
+ level: "warning" /* WARNING */,
13899
+ message: `${role} hit ${payload.kind} soft limit (${payload.used}/${payload.limit})`,
13900
+ budgetKind: payload.kind,
13901
+ elapsedMs: payload.timeoutMs,
13902
+ limit: payload.limit,
13903
+ btwNotes
13904
+ };
13905
+ this.alerts.push(alert);
13906
+ this.fleetBus.emit({
13907
+ subagentId: e.subagentId,
13908
+ ts: Date.now(),
13909
+ type: "collab.warning",
13910
+ payload: alert
13911
+ });
13912
+ const decision = this.options.onBudgetWarning?.(alert) ?? "ignore";
13913
+ if (decision === "cancel") {
13914
+ this.cancel(`Director cancelled: ${role} ${payload.kind} threshold`);
13915
+ return;
13916
+ }
13917
+ if (payload.kind === "timeout") {
13918
+ const progress = this.progressBySubagent.get(e.subagentId) ?? 0;
13919
+ const lastProgress = this.lastTimeoutProgress.get(e.subagentId) ?? -1;
13920
+ if (progress <= lastProgress) {
13921
+ payload.deny();
13922
+ return;
13923
+ }
13924
+ this.lastTimeoutProgress.set(e.subagentId, progress);
13925
+ const newLimit = Math.min(Math.ceil((payload.timeoutMs ?? payload.limit) * 2), 24 * 60 * 6e4);
13926
+ setImmediate(() => {
13927
+ payload.extend({ timeoutMs: newLimit });
13928
+ });
13929
+ return;
13930
+ }
13931
+ if (decision === "extend") {
13932
+ setImmediate(() => {
13933
+ const base = Math.max(payload.limit, payload.used);
13934
+ const extra = {};
13935
+ switch (payload.kind) {
13936
+ case "iterations":
13937
+ extra.maxIterations = Math.min(Math.ceil(base * 1.5), 5e4);
13938
+ break;
13939
+ case "tool_calls":
13940
+ extra.maxToolCalls = Math.min(Math.ceil(base * 1.5), 1e5);
13941
+ break;
13942
+ case "tokens":
13943
+ extra.maxTokens = Math.min(Math.ceil(base * 1.5), 5e6);
13944
+ break;
13945
+ case "cost":
13946
+ extra.maxCostUsd = Math.min(base * 1.5, 100);
13947
+ break;
13948
+ }
13949
+ payload.extend(extra);
13950
+ });
13951
+ return;
13952
+ }
13953
+ if (payload.kind !== "timeout") {
13954
+ setImmediate(() => {
13955
+ const base = Math.max(payload.limit, payload.used);
13956
+ const extra = {};
13957
+ switch (payload.kind) {
13958
+ case "iterations":
13959
+ extra.maxIterations = Math.min(Math.ceil(base * 1.25), 5e4);
13960
+ break;
13961
+ case "tool_calls":
13962
+ extra.maxToolCalls = Math.min(Math.ceil(base * 1.25), 1e5);
13963
+ break;
13964
+ case "tokens":
13965
+ extra.maxTokens = Math.min(Math.ceil(base * 1.25), 5e6);
13966
+ break;
13967
+ case "cost":
13968
+ extra.maxCostUsd = Math.min(base * 1.25, 100);
13969
+ break;
13970
+ }
13971
+ payload.extend(extra);
13972
+ });
13973
+ }
13974
+ });
13975
+ this.disposers.push(dBudget);
13976
+ const dCancel = this.fleetBus.filter("director.cancel_collab", (e) => {
13977
+ const payload = e.payload;
13978
+ if (payload.sessionId !== this.sessionId) return;
13979
+ this.cancelled = true;
13980
+ if (this._timeoutTimer) {
13981
+ clearTimeout(this._timeoutTimer);
13982
+ this._timeoutTimer = void 0;
13983
+ }
13984
+ this.fleetBus.emit({
13985
+ subagentId: this.director.id,
13986
+ ts: Date.now(),
13987
+ type: "collab.cancelled",
13988
+ payload: { sessionId: this.sessionId, reason: payload.reason }
13989
+ });
13990
+ });
13991
+ this.disposers.push(dCancel);
13992
+ const d1 = this.fleetBus.filter("bug.found", (e) => {
13993
+ const payload = e.payload;
13994
+ if (payload?.finding) {
13995
+ this.bugs.set(payload.finding.id, payload.finding);
13996
+ this.emit("bug.found", payload);
13997
+ }
13998
+ });
13999
+ this.disposers.push(d1);
14000
+ const d2 = this.fleetBus.filter("refactor.plan", (e) => {
14001
+ const payload = e.payload;
14002
+ if (payload?.plan) {
14003
+ this.plans.set(payload.plan.id, payload.plan);
14004
+ this.emit("refactor.plan", payload);
14005
+ }
14006
+ });
14007
+ this.disposers.push(d2);
14008
+ const d3 = this.fleetBus.filter("critic.evaluation", (e) => {
14009
+ const payload = e.payload;
14010
+ if (payload?.evaluation) {
14011
+ this.evaluations.set(payload.evaluation.id, payload.evaluation);
14012
+ this.emit("critic.evaluation", payload);
14013
+ }
14014
+ });
14015
+ this.disposers.push(d3);
14016
+ }
14017
+ roleFromSubagentId(subagentId) {
14018
+ for (const [role, id] of this.subagentIds) {
14019
+ if (id === subagentId) return role;
14020
+ }
14021
+ const match = subagentId.match(/^(bug-hunter|refactor-planner|critic)/);
14022
+ return match?.[1] ?? null;
14023
+ }
14024
+ assembleReport() {
14025
+ const bugList = Array.from(this.bugs.values());
14026
+ const planList = Array.from(this.plans.values());
14027
+ const evalList = Array.from(this.evaluations.values());
14028
+ let disposition = "completed";
14029
+ if (this.cancelled) disposition = "cancelled";
14030
+ const verdictOrder = {
14031
+ approve: 0,
14032
+ needs_revision: 1,
14033
+ reject: 2
14034
+ };
14035
+ const overallVerdict = evalList.reduce(
14036
+ (worst, eval_) => {
14037
+ const w = verdictOrder[worst];
14038
+ const c = verdictOrder[eval_.verdict];
14039
+ return c > w ? eval_.verdict : worst;
14040
+ },
14041
+ "approve"
14042
+ );
14043
+ const summary = this.buildMarkdownSummary(bugList, planList, evalList, overallVerdict, disposition);
14044
+ return {
14045
+ sessionId: this.sessionId,
14046
+ startedAt: this.snapshot.createdAt,
14047
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
14048
+ targetPaths: this.options.targetPaths,
14049
+ disposition,
14050
+ bugs: bugList,
14051
+ refactorPlans: planList,
14052
+ evaluations: evalList,
14053
+ alerts: [...this.alerts],
14054
+ overallVerdict,
14055
+ summary
14056
+ };
14057
+ }
14058
+ buildMarkdownSummary(bugs, plans, evals, overallVerdict, disposition) {
14059
+ const lines = [
14060
+ `## Collaborative Debugging Report \u2014 ${this.sessionId}`,
14061
+ "",
14062
+ `**Target:** ${this.options.targetPaths.join(", ")}`,
14063
+ `**Disposition:** ${disposition.toUpperCase()}`,
14064
+ `**Overall Verdict:** **${overallVerdict.toUpperCase()}**`,
14065
+ ""
14066
+ ];
14067
+ if (this.alerts.length > 0) {
14068
+ lines.push("### Alerts", "");
14069
+ for (const alert of this.alerts) {
14070
+ lines.push(`- **[${alert.level.toUpperCase()}]** ${alert.role}: ${alert.message}`);
14071
+ }
14072
+ lines.push("");
14073
+ }
14074
+ if (bugs.length > 0) {
14075
+ lines.push("### Bugs Found", "");
14076
+ for (const b of bugs) {
14077
+ lines.push(`- **[${b.severity.toUpperCase()}]** \`${b.location.file}:${b.location.line}\` \u2014 ${b.description}`);
14078
+ }
14079
+ lines.push("");
14080
+ }
14081
+ if (plans.length > 0) {
14082
+ lines.push("### Refactor Plans", "");
14083
+ for (const p of plans) {
14084
+ lines.push(`- **Phase plan** (risk: ${p.riskScore}, ~${p.estimatedChangeCount} changes)`);
14085
+ for (const phase of p.phases) {
14086
+ lines.push(` - Phase ${phase.number}: ${phase.title} [${phase.risk}]`);
14087
+ }
14088
+ }
14089
+ lines.push("");
14090
+ }
14091
+ if (evals.length > 0) {
14092
+ lines.push("### Critic Evaluations", "");
14093
+ for (const e of evals) {
14094
+ lines.push(`- [${e.subjectType}] score=${e.score}/10 \u2014 **${e.verdict.toUpperCase()}**`);
14095
+ for (const c of e.concerns) {
14096
+ if (c.severity === "blocking") lines.push(` - ${c.description}`);
14097
+ }
14098
+ }
14099
+ lines.push("");
14100
+ }
14101
+ return lines.join("\n");
14102
+ }
14103
+ cleanup() {
14104
+ for (const dispose of this.disposers) dispose();
14105
+ this.disposers.length = 0;
14106
+ }
14107
+ };
14108
+
14109
+ // src/coordination/director.ts
14110
+ var FleetSpawnBudgetError = class extends Error {
14111
+ kind;
14112
+ limit;
14113
+ observed;
14114
+ constructor(kind, limit, observed, message) {
14115
+ const defaultMsg = kind === "max_spawns" ? `Director spawn budget exceeded: tried to spawn #${observed} but maxSpawns is ${limit}` : `Director spawn depth budget exceeded: this director is at depth ${observed} and maxSpawnDepth is ${limit}`;
14116
+ super(message ?? defaultMsg);
14117
+ this.name = "FleetSpawnBudgetError";
14118
+ this.kind = kind;
14119
+ this.limit = limit;
14120
+ this.observed = observed;
13058
14121
  }
13059
14122
  };
13060
14123
  var FleetCostCapError = class extends Error {
@@ -13071,6 +14134,20 @@ var FleetCostCapError = class extends Error {
13071
14134
  this.observed = observed;
13072
14135
  }
13073
14136
  };
14137
+ var FleetContextOverflowError = class extends Error {
14138
+ kind;
14139
+ limit;
14140
+ observed;
14141
+ constructor(limit, observed) {
14142
+ super(
14143
+ `Leader context overflow: leader has ${observed} tokens in flight (limit: ${limit}). Compact the leader context before spawning more subagents.`
14144
+ );
14145
+ this.name = "FleetContextOverflowError";
14146
+ this.kind = "max_context_load";
14147
+ this.limit = limit;
14148
+ this.observed = observed;
14149
+ }
14150
+ };
13074
14151
  var Director = class {
13075
14152
  /** Alias for the ICoordinator contract. `id` is retained for backward compatibility. */
13076
14153
  get coordinatorId() {
@@ -13087,6 +14164,25 @@ var Director = class {
13087
14164
  * injected; otherwise own FleetUsageAggregator.
13088
14165
  */
13089
14166
  usage;
14167
+ /**
14168
+ * Update the leader agent's current context pressure (full request tokens:
14169
+ * messages + systemPrompt + toolDefs). The director checks this before every
14170
+ * spawn — if the pressure exceeds `maxLeaderContextLoad * maxContext`,
14171
+ * spawning is refused with a `FleetContextOverflowError`.
14172
+ *
14173
+ * Call this after each leader agent iteration to keep the pressure current.
14174
+ * The compactor's `CompactReport.fullRequestTokensAfter` is a natural source.
14175
+ */
14176
+ setLeaderContextPressure(tokens) {
14177
+ this.leaderContextPressure = tokens;
14178
+ this.fleetManager?.setLeaderContextPressure(tokens);
14179
+ }
14180
+ /**
14181
+ * Read the leader agent's current context pressure.
14182
+ */
14183
+ getLeaderContextPressure() {
14184
+ return this.leaderContextPressure;
14185
+ }
13090
14186
  /**
13091
14187
  * Optional fleet-level policy container. When provided the Director
13092
14188
  * delegates spawn budgeting, manifest entries, and checkpointing to it
@@ -13119,6 +14215,8 @@ var Director = class {
13119
14215
  subagentBridges = /* @__PURE__ */ new Map();
13120
14216
  /** Tracks per-spawn config + assigned task ids for manifest writing. */
13121
14217
  manifestEntries = /* @__PURE__ */ new Map();
14218
+ /** Tracks assigned nicknames so the same name is never reused in one fleet. */
14219
+ _usedNicknames = /* @__PURE__ */ new Set();
13122
14220
  manifestPath;
13123
14221
  roster;
13124
14222
  directorPreamble;
@@ -13170,6 +14268,23 @@ var Director = class {
13170
14268
  taskCompletedListener = null;
13171
14269
  /** Optional LLM classifier for smart dispatch. Passed from options. */
13172
14270
  dispatchClassifier;
14271
+ /** Leader agent's current context pressure (full request tokens). */
14272
+ leaderContextPressure = 0;
14273
+ /** Maximum context load fraction before spawn is refused. */
14274
+ maxLeaderContextLoad;
14275
+ /** Provider's max context window in tokens. */
14276
+ maxContext;
14277
+ /**
14278
+ * When set by `workComplete()`, the director stops dispatching new tasks
14279
+ * and terminates all running subagents. Used when the director's LLM decides
14280
+ * the goal is satisfied and no further spawns are needed — prevents the
14281
+ * coordinator from keeping workers alive for tasks that will never arrive.
14282
+ */
14283
+ workCompleteFlag = false;
14284
+ /** Pending /btw notes stashed by the leader agent (see setLeaderBtwNote). */
14285
+ _leaderBtwNotes = [];
14286
+ /** Active collab sessions tracked by sessionId (see spawnCollab). */
14287
+ _activeCollabSessions = /* @__PURE__ */ new Map();
13173
14288
  constructor(opts) {
13174
14289
  this.id = opts.config.coordinatorId || randomUUID();
13175
14290
  this.manifestPath = opts.manifestPath;
@@ -13185,6 +14300,8 @@ var Director = class {
13185
14300
  this.dispatchClassifier = opts.dispatchClassifier;
13186
14301
  this.maxFleetCostUsd = opts.directorBudget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
13187
14302
  this.maxBudgetExtensions = opts.maxBudgetExtensions ?? 5;
14303
+ this.maxLeaderContextLoad = opts.maxLeaderContextLoad ?? 0.85;
14304
+ this.maxContext = opts.maxContext ?? 128e3;
13188
14305
  this.sessionsRoot = opts.sessionsRoot;
13189
14306
  this.directorRunId = opts.directorRunId ?? this.id;
13190
14307
  this.stateCheckpoint = opts.stateCheckpointPath ? new DirectorStateCheckpoint(opts.stateCheckpointPath, {
@@ -13212,7 +14329,7 @@ var Director = class {
13212
14329
  this.fleet = new FleetBus();
13213
14330
  this.usage = new FleetUsageAggregator(
13214
14331
  this.fleet,
13215
- (id, provider, model) => {
14332
+ (_id, provider, model) => {
13216
14333
  if (provider && model) return this.priceLookups.get(`${provider}/${model}`);
13217
14334
  return void 0;
13218
14335
  },
@@ -13274,6 +14391,9 @@ var Director = class {
13274
14391
  });
13275
14392
  this.fleet.filter("budget.threshold_reached", (e) => {
13276
14393
  const payload = e.payload;
14394
+ if (e.subagentId.startsWith("bug-hunter-") || e.subagentId.startsWith("refactor-planner-") || e.subagentId.startsWith("critic-")) {
14395
+ return;
14396
+ }
13277
14397
  if (payload.kind === "timeout") {
13278
14398
  const progress = progressBySubagent.get(e.subagentId) ?? 0;
13279
14399
  const lastProgress = lastTimeoutProgress.get(e.subagentId) ?? -1;
@@ -13352,6 +14472,107 @@ var Director = class {
13352
14472
  extensionsFor(subagentId) {
13353
14473
  return this.extendTotals.get(subagentId) ?? 0;
13354
14474
  }
14475
+ /**
14476
+ * Signal that the director's work is done. Once called:
14477
+ * - `spawn()` throws `FleetSpawnBudgetError('max_spawns', …)` — no new
14478
+ * subagents can be created
14479
+ * - Running subagents are NOT forcibly stopped — they finish naturally,
14480
+ * but no new tasks are dispatched to them
14481
+ *
14482
+ * This lets the director LLM say "I'm satisfied with the results, stop
14483
+ * spawning and wind down" — without killing in-flight work mid-execution.
14484
+ * Call `terminateAll()` separately if you need immediate teardown.
14485
+ *
14486
+ * Idempotent — calling twice is a no-op.
14487
+ */
14488
+ workComplete() {
14489
+ this.workCompleteFlag = true;
14490
+ this.fleet.emit({ subagentId: this.id, ts: Date.now(), type: "director.work_complete", payload: {} });
14491
+ }
14492
+ /** Returns true if `workComplete()` has been called on this director. */
14493
+ isWorkComplete() {
14494
+ return this.workCompleteFlag;
14495
+ }
14496
+ /**
14497
+ * Stashes a /btw note on the leader agent's context. The leader's agent loop
14498
+ * calls `consumeBtwNotes()` at each iteration boundary and folds pending notes
14499
+ * into the conversation as a visible block — no abort, no restart, just a
14500
+ * "by the way" nudge the model picks up on its next turn.
14501
+ *
14502
+ * This is the entry point for the host (CLI, TUI) to inject /btw notes
14503
+ * programmatically without going through the slash-command path.
14504
+ */
14505
+ setLeaderBtwNote(note) {
14506
+ const trimmed = note.trim();
14507
+ if (!trimmed) return this._leaderBtwNotes.length;
14508
+ this._leaderBtwNotes = [...this._leaderBtwNotes, trimmed].slice(-20);
14509
+ return this._leaderBtwNotes.length;
14510
+ }
14511
+ /**
14512
+ * Read and clear all pending /btw notes the leader has stashed.
14513
+ * Returns them in FIFO order (empty array when none).
14514
+ *
14515
+ * Called by CollabSession when a budget threshold event fires so the
14516
+ * Director can inspect accumulated /btw notes before deciding whether
14517
+ * to cancel the collab session or let it continue.
14518
+ */
14519
+ getLeaderBtwNotes() {
14520
+ const notes = this._leaderBtwNotes;
14521
+ this._leaderBtwNotes = [];
14522
+ return notes;
14523
+ }
14524
+ /**
14525
+ * Peek at pending /btw notes without consuming them.
14526
+ * Useful for UI to show "N pending notes" without clearing them.
14527
+ */
14528
+ peekLeaderBtwNotes() {
14529
+ return [...this._leaderBtwNotes];
14530
+ }
14531
+ /**
14532
+ * Drain (read + clear) all /btw notes in one call.
14533
+ * Alias for getLeaderBtwNotes() — kept for symmetry with consumeBtwNotes()
14534
+ * in the agent's btw.ts. The Director calls this at the point where it
14535
+ * makes a cancellation decision, not on every budget event.
14536
+ */
14537
+ drainLeaderBtwNotes() {
14538
+ return this.getLeaderBtwNotes();
14539
+ }
14540
+ /**
14541
+ * Cancel an active collab session by its id.
14542
+ * Emits `director.cancel_collab` on the FleetBus so the session's agents
14543
+ * finish early with a 'cancelled' disposition.
14544
+ *
14545
+ * Returns silently if the session id is not tracked or already settled.
14546
+ * The CollabDebugReport will reflect 'cancelled' disposition when awaited.
14547
+ */
14548
+ cancelCollabSession(sessionId, reason = "Director cancelled") {
14549
+ const session = this._activeCollabSessions.get(sessionId);
14550
+ if (!session || session.isCancelled()) return;
14551
+ session.cancel(reason);
14552
+ for (const [_role, subagentId] of session.getSubagentIds()) {
14553
+ this.coordinator.stop(subagentId).catch(() => {
14554
+ });
14555
+ }
14556
+ }
14557
+ /**
14558
+ * Subscribe a callback to be notified whenever a collab session raises
14559
+ * an alert (warning level). The callback receives the full DirectorAlert
14560
+ * payload so the host (CLI, TUI) can display it to the user.
14561
+ * Returns an unsubscribe function.
14562
+ */
14563
+ onCollabAlert(handler) {
14564
+ return this.fleet.filter("collab.warning", (e) => {
14565
+ handler(e.payload);
14566
+ });
14567
+ }
14568
+ /**
14569
+ * Returns all active (not yet settled) collab session ids.
14570
+ * Useful for the TUI to render a "N active sessions" badge and for
14571
+ * the host to know what can be cancelled.
14572
+ */
14573
+ activeCollabSessions() {
14574
+ return Array.from(this._activeCollabSessions.keys());
14575
+ }
13355
14576
  /** Best-effort session-writer append. Swallows failures — the director
13356
14577
  * must not break a fleet run because the session JSONL handle closed. */
13357
14578
  async appendSessionEvent(event) {
@@ -13390,12 +14611,21 @@ var Director = class {
13390
14611
  * it the `cost` column in `usage.snapshot()` stays at 0.
13391
14612
  */
13392
14613
  async spawn(config, priceLookup) {
14614
+ if (this.workCompleteFlag) {
14615
+ throw new FleetSpawnBudgetError(
14616
+ "max_spawns",
14617
+ this.maxSpawns,
14618
+ this.spawnCount + 1,
14619
+ "workComplete() has been called \u2014 director closed further spawning"
14620
+ );
14621
+ }
13393
14622
  if (this.fleetManager) {
13394
14623
  const rejection = this.fleetManager.canSpawn(config);
13395
14624
  if (rejection) {
13396
14625
  if (rejection.kind === "max_spawn_depth") throw new FleetSpawnBudgetError("max_spawn_depth", rejection.limit, rejection.observed);
13397
14626
  if (rejection.kind === "max_spawns") throw new FleetSpawnBudgetError("max_spawns", rejection.limit, rejection.observed);
13398
14627
  if (rejection.kind === "max_cost_usd") throw new FleetCostCapError(rejection.limit, rejection.observed);
14628
+ if (rejection.kind === "max_context_load") throw new FleetContextOverflowError(rejection.limit, rejection.observed);
13399
14629
  }
13400
14630
  } else {
13401
14631
  if (this.spawnDepth >= this.maxSpawnDepth) {
@@ -13410,11 +14640,29 @@ var Director = class {
13410
14640
  throw new FleetCostCapError(this.maxFleetCostUsd, totalCost);
13411
14641
  }
13412
14642
  }
14643
+ if (this.maxLeaderContextLoad < 1) {
14644
+ const threshold = this.maxContext * this.maxLeaderContextLoad;
14645
+ if (this.leaderContextPressure >= threshold) {
14646
+ throw new FleetContextOverflowError(threshold, this.leaderContextPressure);
14647
+ }
14648
+ }
13413
14649
  }
13414
14650
  let result;
14651
+ const needsNickname = config.name === config.role || !config.name || config.name === "subagent" || config.name === "adhoc";
14652
+ if (needsNickname) {
14653
+ const role = config.role ?? "subagent";
14654
+ if (this.fleetManager) {
14655
+ this.fleetManager.assignNicknameAndRecord("", config, priceLookup);
14656
+ } else {
14657
+ config.name = assignNickname(role, this._usedNicknames);
14658
+ this._usedNicknames.add(config.name.split(" ")[0].toLowerCase().replace(/[^a-z0-9-]/g, "-"));
14659
+ }
14660
+ }
13415
14661
  result = await this.coordinator.spawn(config);
13416
14662
  if (this.fleetManager) {
13417
- this.fleetManager.recordSpawn(result.subagentId, config, priceLookup);
14663
+ if (!needsNickname) {
14664
+ this.fleetManager.recordSpawn(result.subagentId, config, priceLookup);
14665
+ }
13418
14666
  } else {
13419
14667
  this.spawnCount += 1;
13420
14668
  this.subagentMeta.set(result.subagentId, {
@@ -13625,6 +14873,24 @@ var Director = class {
13625
14873
  */
13626
14874
  async assign(task) {
13627
14875
  const taskWithId = task.id ? task : { ...task, id: randomUUID() };
14876
+ if (this.workCompleteFlag) {
14877
+ const synthetic = {
14878
+ subagentId: taskWithId.subagentId ?? "unassigned",
14879
+ taskId: taskWithId.id,
14880
+ status: "stopped",
14881
+ error: { kind: "aborted_by_parent", message: "Director called workComplete() \u2014 no further tasks will run", retryable: false },
14882
+ iterations: 0,
14883
+ toolCalls: 0,
14884
+ durationMs: 0
14885
+ };
14886
+ this.completed.set(taskWithId.id, synthetic);
14887
+ const waiter = this.taskWaiters.get(taskWithId.id);
14888
+ if (waiter) {
14889
+ waiter.resolve(synthetic);
14890
+ this.taskWaiters.delete(taskWithId.id);
14891
+ }
14892
+ return taskWithId.id;
14893
+ }
13628
14894
  if (task.subagentId) {
13629
14895
  if (this.fleetManager) {
13630
14896
  this.fleetManager.addTaskToSubagent(task.subagentId, taskWithId.id);
@@ -13847,10 +15113,14 @@ var Director = class {
13847
15113
  makeAskTool(this),
13848
15114
  makeRollUpTool(this),
13849
15115
  makeTerminateTool(this),
15116
+ makeTerminateAllTool(this),
13850
15117
  makeFleetStatusTool(this),
13851
15118
  makeFleetUsageTool(this),
13852
15119
  makeFleetSessionTool(this),
13853
- makeFleetHealthTool(this)
15120
+ makeFleetHealthTool(this),
15121
+ makeCollabDebugTool(this),
15122
+ makeFleetEmitTool(this),
15123
+ makeWorkCompleteTool(this)
13854
15124
  ];
13855
15125
  return t2;
13856
15126
  }
@@ -13862,6 +15132,25 @@ var Director = class {
13862
15132
  async acquireCheckpointLock() {
13863
15133
  return this.stateCheckpoint ? this.stateCheckpoint.acquireLock() : true;
13864
15134
  }
15135
+ /**
15136
+ * Start a collaborative debugging session: BugHunter, RefactorPlanner,
15137
+ * and Critic run in parallel on the same target files, with findings
15138
+ * flowing through the FleetBus (bug.found → refactor.plan →
15139
+ * critic.evaluation). Returns a structured CollabDebugReport when all
15140
+ * three agents complete or the session times out.
15141
+ */
15142
+ async spawnCollab(options) {
15143
+ const session = new CollabSession(this, this.fleet, {
15144
+ ...options,
15145
+ onBudgetWarning: (alert) => {
15146
+ return options.onBudgetWarning?.(alert) ?? "ignore";
15147
+ }
15148
+ });
15149
+ this._activeCollabSessions.set(session.sessionId, session);
15150
+ session.on("session.done", () => this._activeCollabSessions.delete(session.sessionId));
15151
+ session.on("session.error", () => this._activeCollabSessions.delete(session.sessionId));
15152
+ return session.start();
15153
+ }
13865
15154
  /**
13866
15155
  * Resume from a prior checkpoint snapshot (loaded via
13867
15156
  * `loadDirectorState()`). Re-attach to the fleet mid-flight so
@@ -14349,8 +15638,8 @@ async function loadProjectModes(modesDir) {
14349
15638
  for (const entry of entries) {
14350
15639
  if (!entry.endsWith(".md") && !entry.endsWith(".txt")) continue;
14351
15640
  const filePath = path6.join(modesDir, entry);
14352
- const stat9 = await fsp2.stat(filePath);
14353
- if (!stat9.isFile()) continue;
15641
+ const stat8 = await fsp2.stat(filePath);
15642
+ if (!stat8.isFile()) continue;
14354
15643
  const content = await fsp2.readFile(filePath, "utf8");
14355
15644
  const id = path6.basename(entry, path6.extname(entry));
14356
15645
  modes.push({
@@ -15623,10 +16912,10 @@ var AISpecBuilder = class {
15623
16912
  async saveSession() {
15624
16913
  if (!this.sessionPath) return;
15625
16914
  try {
15626
- const fsp18 = await import('fs/promises');
15627
- const path31 = await import('path');
16915
+ const fsp19 = await import('fs/promises');
16916
+ const path35 = await import('path');
15628
16917
  const { atomicWrite: atomicWrite2 } = await Promise.resolve().then(() => (init_atomic_write(), atomic_write_exports));
15629
- await fsp18.mkdir(path31.dirname(this.sessionPath), { recursive: true });
16918
+ await fsp19.mkdir(path35.dirname(this.sessionPath), { recursive: true });
15630
16919
  await atomicWrite2(this.sessionPath, JSON.stringify(this.session, null, 2));
15631
16920
  } catch {
15632
16921
  }
@@ -15635,8 +16924,8 @@ var AISpecBuilder = class {
15635
16924
  async loadSession() {
15636
16925
  if (!this.sessionPath) return false;
15637
16926
  try {
15638
- const fsp18 = await import('fs/promises');
15639
- const raw = await fsp18.readFile(this.sessionPath, "utf8");
16927
+ const fsp19 = await import('fs/promises');
16928
+ const raw = await fsp19.readFile(this.sessionPath, "utf8");
15640
16929
  const loaded = JSON.parse(raw);
15641
16930
  if (loaded?.id && loaded?.phase && loaded?.title) {
15642
16931
  this.session = loaded;
@@ -15650,8 +16939,8 @@ var AISpecBuilder = class {
15650
16939
  async deleteSession() {
15651
16940
  if (!this.sessionPath) return;
15652
16941
  try {
15653
- const fsp18 = await import('fs/promises');
15654
- await fsp18.unlink(this.sessionPath);
16942
+ const fsp19 = await import('fs/promises');
16943
+ await fsp19.unlink(this.sessionPath);
15655
16944
  } catch {
15656
16945
  }
15657
16946
  }
@@ -16278,7 +17567,7 @@ function analyzeCriticalPath(graph) {
16278
17567
  blockedTasks
16279
17568
  };
16280
17569
  }
16281
- function getTransitiveBlocked(graph, taskId, blocksMap) {
17570
+ function getTransitiveBlocked(_graph, taskId, blocksMap) {
16282
17571
  const visited = /* @__PURE__ */ new Set();
16283
17572
  const queue = [taskId];
16284
17573
  while (queue.length > 0) {
@@ -16294,7 +17583,7 @@ function getTransitiveBlocked(graph, taskId, blocksMap) {
16294
17583
  }
16295
17584
  return visited;
16296
17585
  }
16297
- function computeCriticalPath(graph, topoOrder, blockedByMap) {
17586
+ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
16298
17587
  const allIds = Array.from(graph.nodes.keys());
16299
17588
  if (allIds.length === 0) return [];
16300
17589
  const dist = /* @__PURE__ */ new Map();
@@ -16336,15 +17625,15 @@ function computeCriticalPath(graph, topoOrder, blockedByMap) {
16336
17625
  maxId = id;
16337
17626
  }
16338
17627
  }
16339
- const path31 = [];
17628
+ const path35 = [];
16340
17629
  let current = maxId;
16341
17630
  const visited = /* @__PURE__ */ new Set();
16342
17631
  while (current && !visited.has(current)) {
16343
17632
  visited.add(current);
16344
- path31.unshift(current);
17633
+ path35.unshift(current);
16345
17634
  current = prev.get(current) ?? null;
16346
17635
  }
16347
- return path31;
17636
+ return path35;
16348
17637
  }
16349
17638
  function computeParallelGroups(graph, blockedByMap) {
16350
17639
  const groups = [];
@@ -16712,13 +18001,11 @@ function createAutoExecutor(opts) {
16712
18001
 
16713
18002
  // src/sdd/sdd-task-decomposer.ts
16714
18003
  var SddTaskDecomposer = class {
16715
- constructor(tracker, graph, opts = {}) {
18004
+ constructor(tracker, _graph, opts = {}) {
16716
18005
  this.tracker = tracker;
16717
- this.graph = graph;
16718
18006
  this.slots = Math.min(16, Math.max(1, opts.parallelSlots ?? 4));
16719
18007
  }
16720
18008
  tracker;
16721
- graph;
16722
18009
  slots;
16723
18010
  wave = 0;
16724
18011
  // -------------------------------------------------------------------
@@ -16871,7 +18158,7 @@ var SddParallelRun = class {
16871
18158
  this.coordinator.setRunner?.(runner);
16872
18159
  }
16873
18160
  defaultFactory() {
16874
- return async (config) => ({
18161
+ return async (_config) => ({
16875
18162
  agent: this.opts.agent,
16876
18163
  events: this.opts.agent.events
16877
18164
  });
@@ -17324,7 +18611,7 @@ async function startMetricsServer(opts) {
17324
18611
  const tls = opts.tls;
17325
18612
  const useHttps = !!(tls?.cert && tls?.key);
17326
18613
  const host = opts.host ?? "127.0.0.1";
17327
- const path31 = opts.path ?? "/metrics";
18614
+ const path35 = opts.path ?? "/metrics";
17328
18615
  const healthPath = opts.healthPath ?? "/healthz";
17329
18616
  const healthRegistry = opts.healthRegistry;
17330
18617
  const listener = (req, res) => {
@@ -17334,7 +18621,7 @@ async function startMetricsServer(opts) {
17334
18621
  return;
17335
18622
  }
17336
18623
  const url = req.url.split("?")[0];
17337
- if (url === path31) {
18624
+ if (url === path35) {
17338
18625
  let body;
17339
18626
  try {
17340
18627
  body = renderPrometheus(opts.sink.snapshot());
@@ -17398,7 +18685,7 @@ async function startMetricsServer(opts) {
17398
18685
  const protocol = useHttps ? "https" : "http";
17399
18686
  return {
17400
18687
  port: boundPort,
17401
- url: `${protocol}://${host}:${boundPort}${path31}`,
18688
+ url: `${protocol}://${host}:${boundPort}${path35}`,
17402
18689
  close: () => new Promise((resolve9, reject) => {
17403
18690
  server.close((err) => err ? reject(err) : resolve9());
17404
18691
  })
@@ -17685,6 +18972,12 @@ function roughEstimate(messages) {
17685
18972
  return total;
17686
18973
  }
17687
18974
  function createContextManagerTool(opts = {}) {
18975
+ const minCompactThreshold = opts.minCompactThreshold ?? 0;
18976
+ const noopRetryDeltaTokens = opts.noopRetryDeltaTokens ?? 2e3;
18977
+ const maxContext = opts.maxContext ?? 128e3;
18978
+ const compactThresholdFraction = opts.compactThresholdFraction ?? 0.5;
18979
+ const effectiveThreshold = minCompactThreshold > 0 ? minCompactThreshold : Math.floor(maxContext * compactThresholdFraction);
18980
+ let lastNoopTokens = 0;
17688
18981
  return {
17689
18982
  name: CONTEXT_MANAGER_TOOL_NAME,
17690
18983
  description: 'Inspect or reorganize the conversation context window. Use "check" to see token budget. Use "summary" to collapse a message range into a concise note (provide "text" for custom summary). Use "prune" to remove specific messages by index. Use "add_note" to inject a summary note. Use "compact" to run aggressive compaction. Use "repair" to remove orphan tool_use/tool_result blocks after manual context surgery.',
@@ -17775,11 +19068,41 @@ function createContextManagerTool(opts = {}) {
17775
19068
  notes: "No compactor registered. Use /compact aggressive via slash command instead."
17776
19069
  };
17777
19070
  }
19071
+ const fullEstimate = input.systemPrompt != null && Array.isArray(input.tools) ? estimateRequestTokens(messages, input.systemPrompt, input.tools) : { total: beforeTokens};
19072
+ const currentTokens = fullEstimate.total;
19073
+ if (lastNoopTokens > 0) {
19074
+ const delta = currentTokens - lastNoopTokens;
19075
+ if (delta < noopRetryDeltaTokens) {
19076
+ return {
19077
+ action: "compact",
19078
+ beforeTokens,
19079
+ afterTokens: beforeTokens,
19080
+ messageCount: messages.length,
19081
+ notes: `Compact is a NOOP retry: context grew only ${delta} tokens since the last no-op attempt (threshold: ${noopRetryDeltaTokens}). Skip until more content accumulates.`
19082
+ };
19083
+ }
19084
+ }
19085
+ if (currentTokens < effectiveThreshold) {
19086
+ return {
19087
+ action: "compact",
19088
+ beforeTokens,
19089
+ afterTokens: beforeTokens,
19090
+ messageCount: messages.length,
19091
+ notes: `Context tokens (${currentTokens}) below compact threshold (${effectiveThreshold}). Skipping.`
19092
+ };
19093
+ }
17778
19094
  const report = await opts.compactor.compact(ctx);
17779
19095
  ctx.clearFileTracking();
17780
19096
  const repair = applyMessages([...ctx.messages]);
17781
19097
  const afterTokens = repair.changed ? roughEstimate(ctx.messages) : report.after;
17782
19098
  const repaired = report.repaired ?? (repair.changed ? repair : void 0);
19099
+ const reduced = report.fullRequestTokensBefore > report.fullRequestTokensAfter;
19100
+ const repairedSomething = !!report.repaired;
19101
+ if (reduced || repairedSomething) {
19102
+ lastNoopTokens = 0;
19103
+ } else {
19104
+ lastNoopTokens = currentTokens;
19105
+ }
17783
19106
  return {
17784
19107
  action: "compact",
17785
19108
  beforeTokens,
@@ -18245,10 +19568,10 @@ var SkillInstaller = class {
18245
19568
  if (!resolved.startsWith(path6.resolve(destDir))) {
18246
19569
  throw new Error(`Path traversal detected in skill file: ${file}`);
18247
19570
  }
18248
- const stat9 = await fsp2.stat(srcPath);
18249
- if (stat9.size > MAX_SKILL_FILE_SIZE) {
19571
+ const stat8 = await fsp2.stat(srcPath);
19572
+ if (stat8.size > MAX_SKILL_FILE_SIZE) {
18250
19573
  throw new Error(
18251
- `Skill file "${file}" is too large (${(stat9.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`
19574
+ `Skill file "${file}" is too large (${(stat8.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`
18252
19575
  );
18253
19576
  }
18254
19577
  await fsp2.mkdir(path6.dirname(destPath), { recursive: true });
@@ -18287,7 +19610,7 @@ var SkillInstaller = class {
18287
19610
  * - Name: update that specific skill
18288
19611
  * - Name + newRef: update to a different ref
18289
19612
  */
18290
- async update(nameOrRef, opts) {
19613
+ async update(nameOrRef, _opts) {
18291
19614
  const result = { updated: [], unchanged: [], errors: [] };
18292
19615
  const allEntries = await this.manifest.listAll();
18293
19616
  let targets;
@@ -18366,131 +19689,747 @@ var SkillInstaller = class {
18366
19689
  if (!entry) {
18367
19690
  throw new Error(`Skill "${name}" is not installed${scope === "user" ? " (global)" : ""}.`);
18368
19691
  }
18369
- await this.removeSkillFiles(name, scope);
18370
- await this.manifest.removeEntry(name, scope);
18371
- this.invalidateLoaderCache();
19692
+ await this.removeSkillFiles(name, scope);
19693
+ await this.manifest.removeEntry(name, scope);
19694
+ this.invalidateLoaderCache();
19695
+ }
19696
+ /**
19697
+ * List all installed skills from the manifest.
19698
+ */
19699
+ async listInstalled() {
19700
+ return this.manifest.listAll();
19701
+ }
19702
+ // ── Private helpers ──────────────────────────────────────────────
19703
+ /**
19704
+ * Detect skills in an extracted repository.
19705
+ * Returns an array of detected skills with their files.
19706
+ */
19707
+ async detectSkills(baseDir) {
19708
+ const results = [];
19709
+ const rootSkillMd = path6.join(baseDir, "SKILL.md");
19710
+ try {
19711
+ await fsp2.access(rootSkillMd);
19712
+ const content = await fsp2.readFile(rootSkillMd, "utf8");
19713
+ const meta = parseFrontmatter2(content);
19714
+ if (meta.name && meta.description) {
19715
+ results.push({
19716
+ name: meta.name,
19717
+ baseDir,
19718
+ files: ["SKILL.md"]
19719
+ });
19720
+ return results;
19721
+ }
19722
+ } catch {
19723
+ }
19724
+ const skillsDir = path6.join(baseDir, "skills");
19725
+ try {
19726
+ const entries = await fsp2.readdir(skillsDir, { withFileTypes: true });
19727
+ for (const entry of entries) {
19728
+ if (!entry.isDirectory()) continue;
19729
+ const skillFile = path6.join(skillsDir, entry.name, "SKILL.md");
19730
+ try {
19731
+ const content = await fsp2.readFile(skillFile, "utf8");
19732
+ const meta = parseFrontmatter2(content);
19733
+ if (meta.name && meta.description) {
19734
+ const skillDir = path6.join(skillsDir, entry.name);
19735
+ const files = await collectFiles(skillDir, skillDir);
19736
+ results.push({
19737
+ name: meta.name,
19738
+ baseDir: skillDir,
19739
+ files
19740
+ });
19741
+ }
19742
+ } catch {
19743
+ }
19744
+ }
19745
+ } catch {
19746
+ }
19747
+ return results;
19748
+ }
19749
+ /**
19750
+ * Remove all files for an installed skill.
19751
+ */
19752
+ async removeSkillFiles(name, scope) {
19753
+ const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
19754
+ const skillDir = path6.join(targetDir, name);
19755
+ await fsp2.rm(skillDir, { recursive: true, force: true });
19756
+ }
19757
+ /**
19758
+ * Invalidate the skill loader's cache so newly installed skills appear.
19759
+ */
19760
+ invalidateLoaderCache() {
19761
+ const loader = this.opts.skillLoader;
19762
+ if (loader && typeof loader.invalidateCache === "function") {
19763
+ loader.invalidateCache();
19764
+ }
19765
+ }
19766
+ };
19767
+ function parseFrontmatter2(raw) {
19768
+ if (!raw.startsWith("---")) return {};
19769
+ const end = raw.indexOf("\n---", 4);
19770
+ if (end === -1) return {};
19771
+ const block = raw.slice(4, end);
19772
+ const out = {};
19773
+ let key = null;
19774
+ let value = [];
19775
+ const flush = () => {
19776
+ if (key) {
19777
+ out[key] = value.join("\n").trim();
19778
+ }
19779
+ key = null;
19780
+ value = [];
19781
+ };
19782
+ for (const line of block.split("\n")) {
19783
+ const m = /^([a-zA-Z_]+):\s*(\|?)\s*(.*)$/.exec(line);
19784
+ if (m) {
19785
+ flush();
19786
+ key = m[1] ?? "";
19787
+ const pipe = m[2];
19788
+ const rest = m[3] ?? "";
19789
+ if (pipe === "|") {
19790
+ value = [];
19791
+ } else if (rest) {
19792
+ value = [rest];
19793
+ } else {
19794
+ value = [];
19795
+ }
19796
+ } else if (key) {
19797
+ value.push(line.replace(/^\s+/, ""));
19798
+ }
19799
+ }
19800
+ flush();
19801
+ return out;
19802
+ }
19803
+ async function collectFiles(dir, baseDir) {
19804
+ const results = [];
19805
+ const entries = await fsp2.readdir(dir, { withFileTypes: true });
19806
+ for (const entry of entries) {
19807
+ const fullPath = path6.join(dir, entry.name);
19808
+ const relPath = path6.relative(baseDir, fullPath);
19809
+ if (entry.isDirectory()) {
19810
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
19811
+ results.push(...await collectFiles(fullPath, baseDir));
19812
+ } else {
19813
+ results.push(relPath);
19814
+ }
19815
+ }
19816
+ return results;
19817
+ }
19818
+
19819
+ // src/storage/annotations-store.ts
19820
+ init_atomic_write();
19821
+ var FILE_VERSION = 1;
19822
+ var MAX_TEXT_LENGTH = 2e3;
19823
+ var MAX_ANNOTATIONS = 1e3;
19824
+ var AnnotationsStore = class {
19825
+ dir;
19826
+ /** Per-session write queue. Created lazily on first add. */
19827
+ writeChains = /* @__PURE__ */ new Map();
19828
+ constructor(opts) {
19829
+ this.dir = opts.dir;
19830
+ }
19831
+ // ── Reads ──────────────────────────────────────────────────────────────
19832
+ /**
19833
+ * Return all annotations for `sessionId` in insertion order
19834
+ * (oldest first). Returns an empty array when no file exists
19835
+ * yet (the normal case for a fresh session).
19836
+ */
19837
+ async list(sessionId) {
19838
+ const file = await this.readFile(sessionId);
19839
+ return file ? file.annotations : [];
19840
+ }
19841
+ /**
19842
+ * Convenience: only unresolved annotations, newest first — the
19843
+ * common UI rendering for "what still needs attention?".
19844
+ */
19845
+ async listOpen(sessionId) {
19846
+ const all = await this.list(sessionId);
19847
+ return all.filter((a) => !a.resolved).reverse();
19848
+ }
19849
+ // ── Writes ─────────────────────────────────────────────────────────────
19850
+ /**
19851
+ * Add a new annotation. Returns the persisted record (with id
19852
+ * and timestamps filled in). Throws when `text` is empty or
19853
+ * exceeds `MAX_TEXT_LENGTH`.
19854
+ */
19855
+ async add(input) {
19856
+ const text = input.text.trim();
19857
+ if (text.length === 0) {
19858
+ throw new Error("Annotation text must be non-empty");
19859
+ }
19860
+ if (text.length > MAX_TEXT_LENGTH) {
19861
+ throw new Error(
19862
+ `Annotation text exceeds ${MAX_TEXT_LENGTH} chars (got ${text.length})`
19863
+ );
19864
+ }
19865
+ if (!Number.isInteger(input.atEventIndex) || input.atEventIndex < 0) {
19866
+ throw new Error("atEventIndex must be a non-negative integer");
19867
+ }
19868
+ const annotation = {
19869
+ id: randomUUID(),
19870
+ sessionId: input.sessionId,
19871
+ atEventIndex: input.atEventIndex,
19872
+ authorId: input.authorId,
19873
+ authorRole: "annotator",
19874
+ text,
19875
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
19876
+ resolved: false
19877
+ };
19878
+ await this.enqueue(input.sessionId, async () => {
19879
+ const all = await this.list(input.sessionId);
19880
+ all.push(annotation);
19881
+ if (all.length > MAX_ANNOTATIONS) {
19882
+ const sorted = all.map((a, i) => ({ a, i })).sort((x, y) => {
19883
+ if (x.a.resolved !== y.a.resolved) return x.a.resolved ? 1 : -1;
19884
+ return x.a.createdAt.localeCompare(y.a.createdAt);
19885
+ });
19886
+ const evictCount = all.length - MAX_ANNOTATIONS;
19887
+ const toEvict = new Set(sorted.slice(0, evictCount).map((s) => s.a.id));
19888
+ const kept = all.filter((a) => !toEvict.has(a.id));
19889
+ await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: kept });
19890
+ } else {
19891
+ await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: all });
19892
+ }
19893
+ });
19894
+ return annotation;
19895
+ }
19896
+ /**
19897
+ * Mark an annotation as resolved. Returns the updated record, or
19898
+ * `null` if no annotation with that id exists in this session.
19899
+ * Idempotent: resolving an already-resolved annotation refreshes
19900
+ * `resolvedAt` / `resolvedBy` to the latest call.
19901
+ */
19902
+ async resolve(input) {
19903
+ let updated = null;
19904
+ await this.enqueue(input.sessionId, async () => {
19905
+ const all = await this.list(input.sessionId);
19906
+ const idx = all.findIndex((a) => a.id === input.annotationId);
19907
+ if (idx === -1) {
19908
+ updated = null;
19909
+ return;
19910
+ }
19911
+ const next = {
19912
+ ...all[idx],
19913
+ resolved: true,
19914
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
19915
+ resolvedBy: input.resolvedBy
19916
+ };
19917
+ all[idx] = next;
19918
+ await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: all });
19919
+ updated = next;
19920
+ });
19921
+ return updated;
19922
+ }
19923
+ // ── Internals ──────────────────────────────────────────────────────────
19924
+ filePath(sessionId) {
19925
+ if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
19926
+ throw new Error(`Invalid sessionId: ${sessionId}`);
19927
+ }
19928
+ return path6.join(this.dir, `${sessionId}.annotations.json`);
19929
+ }
19930
+ async readFile(sessionId) {
19931
+ const fp = this.filePath(sessionId);
19932
+ try {
19933
+ const raw = await fsp2.readFile(fp, "utf8");
19934
+ const parsed = JSON.parse(raw);
19935
+ if (parsed.version !== FILE_VERSION) {
19936
+ return { version: FILE_VERSION, annotations: [] };
19937
+ }
19938
+ return parsed;
19939
+ } catch (err) {
19940
+ if (err.code === "ENOENT") return null;
19941
+ return { version: FILE_VERSION, annotations: [] };
19942
+ }
19943
+ }
19944
+ async writeFile(sessionId, file) {
19945
+ const fp = this.filePath(sessionId);
19946
+ await atomicWrite(fp, JSON.stringify(file, null, 2));
19947
+ }
19948
+ /**
19949
+ * Serialize writes per-sessionId. We chain promises instead of
19950
+ * using a Mutex class so the contract is obvious from the
19951
+ * call-site: `enqueue(sid, fn)` runs `fn` after every prior
19952
+ * enqueue for `sid` has settled.
19953
+ */
19954
+ enqueue(sessionId, fn) {
19955
+ const prev = this.writeChains.get(sessionId) ?? Promise.resolve();
19956
+ const next = prev.then(fn, fn);
19957
+ this.writeChains.set(
19958
+ sessionId,
19959
+ next.catch(() => void 0)
19960
+ );
19961
+ return next;
19962
+ }
19963
+ };
19964
+
19965
+ // src/storage/replay-log-store.ts
19966
+ init_atomic_write();
19967
+ function stableStringify(value) {
19968
+ return JSON.stringify(sortKeys(value));
19969
+ }
19970
+ function sortKeys(value) {
19971
+ if (Array.isArray(value)) return value.map(sortKeys);
19972
+ if (value && typeof value === "object") {
19973
+ const obj = value;
19974
+ const sorted = {};
19975
+ for (const key of Object.keys(obj).sort()) {
19976
+ sorted[key] = sortKeys(obj[key]);
19977
+ }
19978
+ return sorted;
19979
+ }
19980
+ return value;
19981
+ }
19982
+ function hashRequest(request) {
19983
+ const payload = {
19984
+ model: request.model,
19985
+ system: request.system,
19986
+ messages: request.messages,
19987
+ tools: request.tools,
19988
+ maxTokens: request.maxTokens,
19989
+ temperature: request.temperature,
19990
+ topP: request.topP,
19991
+ stopSequences: request.stopSequences,
19992
+ toolChoice: request.toolChoice
19993
+ };
19994
+ const json = stableStringify(payload);
19995
+ const digest = createHash("sha256").update(json, "utf8").digest("hex");
19996
+ return `sha256:${digest}`;
19997
+ }
19998
+ var DEFAULT_MAX_ENTRIES = 1e3;
19999
+ var ReplayLogStore = class {
20000
+ dir;
20001
+ writeChains = /* @__PURE__ */ new Map();
20002
+ /** Per-session hash → entry index, kept in memory after the first load. */
20003
+ cache = /* @__PURE__ */ new Map();
20004
+ maxEntries;
20005
+ constructor(opts) {
20006
+ this.dir = opts.dir;
20007
+ this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
20008
+ }
20009
+ // ── Writes ──────────────────────────────────────────────────────────────
20010
+ /**
20011
+ * Record a request/response pair. Idempotent on hash: a second
20012
+ * `record` for the same hash is a no-op (the existing entry wins).
20013
+ * Returns the hash.
20014
+ */
20015
+ async record(input) {
20016
+ const hash = hashRequest(input.request);
20017
+ await this.enqueue(input.sessionId, async () => {
20018
+ const cache = await this.ensureCache(input.sessionId);
20019
+ if (cache.has(hash)) return;
20020
+ const entry = {
20021
+ hash,
20022
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
20023
+ request: input.request,
20024
+ response: input.response
20025
+ };
20026
+ let all = [...cache.values(), entry];
20027
+ if (all.length > this.maxEntries) {
20028
+ const evictCount = all.length - this.maxEntries;
20029
+ all = all.slice(evictCount);
20030
+ }
20031
+ await this.writeAll(input.sessionId, all);
20032
+ cache.clear();
20033
+ for (const e of all) cache.set(e.hash, e);
20034
+ });
20035
+ return hash;
20036
+ }
20037
+ // ── Reads ───────────────────────────────────────────────────────────────
20038
+ /**
20039
+ * Look up an entry by hash. Returns `null` when the request has
20040
+ * not been recorded for this session. O(1) after the first call
20041
+ * per session (in-memory cache).
20042
+ */
20043
+ async lookup(sessionId, hash) {
20044
+ const cache = await this.ensureCache(sessionId);
20045
+ return cache.get(hash) ?? null;
20046
+ }
20047
+ /** All recorded entries for a session, in insertion order. */
20048
+ async load(sessionId) {
20049
+ const cache = await this.ensureCache(sessionId);
20050
+ return [...cache.values()];
20051
+ }
20052
+ /**
20053
+ * List every session id that has a replay log in the store dir.
20054
+ * Returns an array of `{ sessionId, entryCount, path }` sorted
20055
+ * by sessionId for stable output. Used by `wstack replay --list`.
20056
+ */
20057
+ async list() {
20058
+ let entries;
20059
+ try {
20060
+ entries = await fsp2.readdir(this.dir);
20061
+ } catch (err) {
20062
+ if (err.code === "ENOENT") return [];
20063
+ return [];
20064
+ }
20065
+ const out = [];
20066
+ for (const name of entries) {
20067
+ if (!name.endsWith(".replay.jsonl")) continue;
20068
+ const sessionId = name.slice(0, -".replay.jsonl".length);
20069
+ const all = await this.load(sessionId);
20070
+ out.push({
20071
+ sessionId,
20072
+ entryCount: all.length,
20073
+ path: path6.join(this.dir, name)
20074
+ });
20075
+ }
20076
+ return out.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
20077
+ }
20078
+ // ── Internals ───────────────────────────────────────────────────────────
20079
+ filePath(sessionId) {
20080
+ if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
20081
+ throw new Error(`Invalid sessionId: ${sessionId}`);
20082
+ }
20083
+ return path6.join(this.dir, `${sessionId}.replay.jsonl`);
20084
+ }
20085
+ async readAll(sessionId) {
20086
+ const fp = this.filePath(sessionId);
20087
+ try {
20088
+ const raw = await fsp2.readFile(fp, "utf8");
20089
+ const out = [];
20090
+ for (const line of raw.split("\n")) {
20091
+ if (!line.trim()) continue;
20092
+ try {
20093
+ const parsed = JSON.parse(line);
20094
+ if ("entry" in parsed && parsed.entry) {
20095
+ out.push(parsed.entry);
20096
+ } else {
20097
+ out.push(parsed);
20098
+ }
20099
+ } catch {
20100
+ }
20101
+ }
20102
+ return out;
20103
+ } catch (err) {
20104
+ if (err.code === "ENOENT") return [];
20105
+ return [];
20106
+ }
20107
+ }
20108
+ async writeAll(sessionId, entries) {
20109
+ const fp = this.filePath(sessionId);
20110
+ const body = entries.map((e) => JSON.stringify(e)).join("\n") + (entries.length ? "\n" : "");
20111
+ await atomicWrite(fp, body);
20112
+ }
20113
+ async ensureCache(sessionId) {
20114
+ let cache = this.cache.get(sessionId);
20115
+ if (cache) return cache;
20116
+ const all = await this.readAll(sessionId);
20117
+ cache = /* @__PURE__ */ new Map();
20118
+ for (const e of all) cache.set(e.hash, e);
20119
+ this.cache.set(sessionId, cache);
20120
+ return cache;
20121
+ }
20122
+ enqueue(sessionId, fn) {
20123
+ const prev = this.writeChains.get(sessionId) ?? Promise.resolve();
20124
+ const next = prev.then(fn, fn);
20125
+ this.writeChains.set(
20126
+ sessionId,
20127
+ next.catch(() => void 0)
20128
+ );
20129
+ return next;
20130
+ }
20131
+ };
20132
+ var SessionRecovery = class {
20133
+ constructor(dir) {
20134
+ this.dir = dir;
20135
+ }
20136
+ dir;
20137
+ /**
20138
+ * Scan a session log and return a `StaleSession` if and only
20139
+ * if the last event is an `in_flight_start` without a matching
20140
+ * `in_flight_end`. Returns `null` when:
20141
+ * - the log does not exist;
20142
+ * - the log is empty;
20143
+ * - the last event is `in_flight_end` (clean shutdown);
20144
+ * - the last event is something else (e.g. an unannotated
20145
+ * legacy log without in-flight markers).
20146
+ */
20147
+ async detectStale(sessionId) {
20148
+ const fp = this.filePath(sessionId);
20149
+ let raw;
20150
+ try {
20151
+ raw = await fsp2.readFile(fp, "utf8");
20152
+ } catch (err) {
20153
+ if (err.code === "ENOENT") return null;
20154
+ return null;
20155
+ }
20156
+ return this.parseForStale(sessionId, fp, raw);
20157
+ }
20158
+ /**
20159
+ * Generate a recovery plan for a session. The plan describes
20160
+ * "what would be re-executed" if the user chose to resume —
20161
+ * everything after the last `checkpoint` event, plus the
20162
+ * dangling in-flight marker if present.
20163
+ *
20164
+ * Returns a non-null plan for ANY session that has at least
20165
+ * one event after a checkpoint (or, for legacy sessions, at
20166
+ * least one event). Pure read; no mutation.
20167
+ */
20168
+ async recover(sessionId) {
20169
+ const fp = this.filePath(sessionId);
20170
+ let raw;
20171
+ try {
20172
+ raw = await fsp2.readFile(fp, "utf8");
20173
+ } catch (err) {
20174
+ if (err.code === "ENOENT") return null;
20175
+ return null;
20176
+ }
20177
+ const events = [];
20178
+ for (const line of raw.split("\n")) {
20179
+ if (!line.trim()) continue;
20180
+ try {
20181
+ events.push(JSON.parse(line));
20182
+ } catch {
20183
+ }
20184
+ }
20185
+ if (events.length === 0) return null;
20186
+ let lastCheckpoint = null;
20187
+ let lastCheckpointIdx = -1;
20188
+ for (let i = 0; i < events.length; i++) {
20189
+ if (events[i].type === "checkpoint") {
20190
+ lastCheckpoint = events[i];
20191
+ lastCheckpointIdx = i;
20192
+ }
20193
+ }
20194
+ const pendingEvents = lastCheckpointIdx >= 0 ? events.slice(lastCheckpointIdx + 1) : events;
20195
+ const lastEv = events[events.length - 1];
20196
+ const inFlightStart = lastEv.type === "in_flight_start" ? lastEv : null;
20197
+ const context = inFlightStart && inFlightStart.type === "in_flight_start" ? inFlightStart.context : null;
20198
+ return {
20199
+ sessionId,
20200
+ stale: inFlightStart !== null,
20201
+ lastCheckpoint,
20202
+ pendingEvents,
20203
+ inFlightStart,
20204
+ context
20205
+ };
20206
+ }
20207
+ /**
20208
+ * List every stale session in a directory. Returns an array
20209
+ * (possibly empty) sorted by `lastEventTs` descending — most
20210
+ * recent crash first.
20211
+ */
20212
+ async listResumable() {
20213
+ let entries;
20214
+ try {
20215
+ entries = await fsp2.readdir(this.dir);
20216
+ } catch (err) {
20217
+ if (err.code === "ENOENT") return [];
20218
+ return [];
20219
+ }
20220
+ const out = [];
20221
+ for (const name of entries) {
20222
+ if (!name.endsWith(".jsonl")) continue;
20223
+ const sessionId = name.slice(0, -".jsonl".length);
20224
+ if (sessionId.includes(".replay") || sessionId.includes(".annotations")) continue;
20225
+ const stale = await this.detectStale(sessionId);
20226
+ if (stale) out.push(stale);
20227
+ }
20228
+ return out.sort((a, b) => b.lastEventTs.localeCompare(a.lastEventTs));
20229
+ }
20230
+ // ── Internals ──────────────────────────────────────────────────────────
20231
+ filePath(sessionId) {
20232
+ if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
20233
+ throw new Error(`Invalid sessionId: ${sessionId}`);
20234
+ }
20235
+ return path6.join(this.dir, `${sessionId}.jsonl`);
20236
+ }
20237
+ /**
20238
+ * Stream-parse the last few lines of a JSONL log. We do NOT load
20239
+ * the whole file into memory — for long-running sessions the log
20240
+ * can be megabytes. Instead we read tail-ward and find the last
20241
+ * `in_flight_start` / `in_flight_end` pair.
20242
+ */
20243
+ async parseForStale(sessionId, fp, raw) {
20244
+ const lines = raw.split("\n");
20245
+ let lastEvent = null;
20246
+ let eventCount = 0;
20247
+ for (const line of lines) {
20248
+ if (!line.trim()) continue;
20249
+ try {
20250
+ const ev = JSON.parse(line);
20251
+ lastEvent = ev;
20252
+ eventCount++;
20253
+ } catch {
20254
+ }
20255
+ }
20256
+ if (!lastEvent) return null;
20257
+ if (lastEvent.type === "in_flight_start") {
20258
+ return {
20259
+ sessionId,
20260
+ path: fp,
20261
+ lastEventTs: lastEvent.ts,
20262
+ context: lastEvent.context,
20263
+ eventCount
20264
+ };
20265
+ }
20266
+ return null;
20267
+ }
20268
+ };
20269
+
20270
+ // src/storage/tool-audit-log.ts
20271
+ init_atomic_write();
20272
+ var GENESIS_PREV = "0".repeat(64);
20273
+ var ToolAuditLog = class {
20274
+ dir;
20275
+ /** In-memory cache of the last entry's hash (per session), to compute chains efficiently. */
20276
+ tailHash = /* @__PURE__ */ new Map();
20277
+ writeChains = /* @__PURE__ */ new Map();
20278
+ constructor(opts) {
20279
+ this.dir = opts.dir;
18372
20280
  }
18373
20281
  /**
18374
- * List all installed skills from the manifest.
20282
+ * Append a tool call/result pair to the chain. Returns the
20283
+ * resulting entry. Idempotency is not guaranteed — if you
20284
+ * record the same tool_use twice you get two entries. That's
20285
+ * intentional: the audit log is a record, not a cache.
18375
20286
  */
18376
- async listInstalled() {
18377
- return this.manifest.listAll();
20287
+ async record(input) {
20288
+ let entry = null;
20289
+ await this.enqueue(input.sessionId, async () => {
20290
+ const prevHash = this.tailHash.get(input.sessionId) ?? GENESIS_PREV;
20291
+ const id = randomUUID();
20292
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
20293
+ const content = {
20294
+ id,
20295
+ ts,
20296
+ prevHash,
20297
+ toolName: input.toolName,
20298
+ toolUseId: input.toolUseId,
20299
+ input: input.input,
20300
+ output: input.output,
20301
+ isError: input.isError
20302
+ };
20303
+ const hash = createHash("sha256").update(stableStringify2(content), "utf8").digest("hex");
20304
+ entry = {
20305
+ index: this.tailHash.has(input.sessionId) ? -1 : 0,
20306
+ // placeholder; recomputed below
20307
+ id,
20308
+ ts,
20309
+ prevHash,
20310
+ hash,
20311
+ toolName: input.toolName,
20312
+ toolUseId: input.toolUseId,
20313
+ input: input.input,
20314
+ output: input.output,
20315
+ isError: input.isError
20316
+ };
20317
+ const existing = await this.readAll(input.sessionId);
20318
+ const index = existing.length;
20319
+ const finalContent = { ...content, index };
20320
+ const finalHash = createHash("sha256").update(stableStringify2(finalContent), "utf8").digest("hex");
20321
+ entry = { ...entry, index, hash: finalHash };
20322
+ await this.appendLine(input.sessionId, entry);
20323
+ this.tailHash.set(input.sessionId, finalHash);
20324
+ });
20325
+ return entry;
18378
20326
  }
18379
- // ── Private helpers ──────────────────────────────────────────────
18380
20327
  /**
18381
- * Detect skills in an extracted repository.
18382
- * Returns an array of detected skills with their files.
20328
+ * Walk the chain and verify every entry's hash and prevHash.
20329
+ * Returns a structured verdict never throws.
18383
20330
  */
18384
- async detectSkills(baseDir) {
18385
- const results = [];
18386
- const rootSkillMd = path6.join(baseDir, "SKILL.md");
18387
- try {
18388
- await fsp2.access(rootSkillMd);
18389
- const content = await fsp2.readFile(rootSkillMd, "utf8");
18390
- const meta = parseFrontmatter2(content);
18391
- if (meta.name && meta.description) {
18392
- results.push({
18393
- name: meta.name,
18394
- baseDir,
18395
- files: ["SKILL.md"]
18396
- });
18397
- return results;
20331
+ async verify(sessionId) {
20332
+ const entries = await this.readAll(sessionId);
20333
+ if (entries.length === 0) return { ok: true, entries: 0 };
20334
+ if (entries[0].prevHash !== GENESIS_PREV) {
20335
+ return {
20336
+ ok: false,
20337
+ brokenAt: 0,
20338
+ reason: "first entry is not the genesis (prevHash != 0\u20260)"
20339
+ };
20340
+ }
20341
+ let prevHash = GENESIS_PREV;
20342
+ for (let i = 0; i < entries.length; i++) {
20343
+ const e = entries[i];
20344
+ if (e.prevHash !== prevHash) {
20345
+ return {
20346
+ ok: false,
20347
+ brokenAt: i,
20348
+ reason: `prevHash mismatch at entry ${i} (expected ${prevHash.slice(0, 8)}\u2026, got ${e.prevHash.slice(0, 8)}\u2026)`
20349
+ };
18398
20350
  }
18399
- } catch {
20351
+ const content = {
20352
+ id: e.id,
20353
+ ts: e.ts,
20354
+ prevHash: e.prevHash,
20355
+ toolName: e.toolName,
20356
+ toolUseId: e.toolUseId,
20357
+ input: e.input,
20358
+ output: e.output,
20359
+ isError: e.isError,
20360
+ index: e.index
20361
+ };
20362
+ const expectedHash = createHash("sha256").update(stableStringify2(content), "utf8").digest("hex");
20363
+ if (expectedHash !== e.hash) {
20364
+ return {
20365
+ ok: false,
20366
+ brokenAt: i,
20367
+ reason: `hash mismatch at entry ${i} (entry content was modified)`
20368
+ };
20369
+ }
20370
+ prevHash = e.hash;
18400
20371
  }
18401
- const skillsDir = path6.join(baseDir, "skills");
20372
+ return { ok: true, entries: entries.length };
20373
+ }
20374
+ /** All entries for a session, in insertion order. */
20375
+ async load(sessionId) {
20376
+ return this.readAll(sessionId);
20377
+ }
20378
+ // ── Internals ────────────────────────────────────────────────────────────
20379
+ filePath(sessionId) {
20380
+ if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
20381
+ throw new Error(`Invalid sessionId: ${sessionId}`);
20382
+ }
20383
+ return path6.join(this.dir, `${sessionId}.audit.jsonl`);
20384
+ }
20385
+ async readAll(sessionId) {
20386
+ const fp = this.filePath(sessionId);
18402
20387
  try {
18403
- const entries = await fsp2.readdir(skillsDir, { withFileTypes: true });
18404
- for (const entry of entries) {
18405
- if (!entry.isDirectory()) continue;
18406
- const skillFile = path6.join(skillsDir, entry.name, "SKILL.md");
20388
+ const raw = await fsp2.readFile(fp, "utf8");
20389
+ const out = [];
20390
+ for (const line of raw.split("\n")) {
20391
+ if (!line.trim()) continue;
18407
20392
  try {
18408
- const content = await fsp2.readFile(skillFile, "utf8");
18409
- const meta = parseFrontmatter2(content);
18410
- if (meta.name && meta.description) {
18411
- const skillDir = path6.join(skillsDir, entry.name);
18412
- const files = await collectFiles(skillDir, skillDir);
18413
- results.push({
18414
- name: meta.name,
18415
- baseDir: skillDir,
18416
- files
18417
- });
18418
- }
20393
+ out.push(JSON.parse(line));
18419
20394
  } catch {
18420
20395
  }
18421
20396
  }
18422
- } catch {
20397
+ return out;
20398
+ } catch (err) {
20399
+ if (err.code === "ENOENT") return [];
20400
+ return [];
18423
20401
  }
18424
- return results;
18425
20402
  }
18426
- /**
18427
- * Remove all files for an installed skill.
18428
- */
18429
- async removeSkillFiles(name, scope) {
18430
- const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
18431
- const skillDir = path6.join(targetDir, name);
18432
- await fsp2.rm(skillDir, { recursive: true, force: true });
20403
+ async appendLine(sessionId, entry) {
20404
+ const fp = this.filePath(sessionId);
20405
+ const existing = await this.readAll(sessionId);
20406
+ const all = [...existing, entry];
20407
+ await atomicWrite(fp, all.map((e) => JSON.stringify(e)).join("\n") + (all.length ? "\n" : ""));
18433
20408
  }
18434
- /**
18435
- * Invalidate the skill loader's cache so newly installed skills appear.
18436
- */
18437
- invalidateLoaderCache() {
18438
- const loader = this.opts.skillLoader;
18439
- if (loader && typeof loader.invalidateCache === "function") {
18440
- loader.invalidateCache();
18441
- }
20409
+ enqueue(sessionId, fn) {
20410
+ const prev = this.writeChains.get(sessionId) ?? Promise.resolve();
20411
+ const next = prev.then(fn, fn);
20412
+ this.writeChains.set(
20413
+ sessionId,
20414
+ next.catch(() => void 0)
20415
+ );
20416
+ return next;
18442
20417
  }
18443
20418
  };
18444
- function parseFrontmatter2(raw) {
18445
- if (!raw.startsWith("---")) return {};
18446
- const end = raw.indexOf("\n---", 4);
18447
- if (end === -1) return {};
18448
- const block = raw.slice(4, end);
18449
- const out = {};
18450
- let key = null;
18451
- let value = [];
18452
- const flush = () => {
18453
- if (key) {
18454
- out[key] = value.join("\n").trim();
18455
- }
18456
- key = null;
18457
- value = [];
18458
- };
18459
- for (const line of block.split("\n")) {
18460
- const m = /^([a-zA-Z_]+):\s*(\|?)\s*(.*)$/.exec(line);
18461
- if (m) {
18462
- flush();
18463
- key = m[1] ?? "";
18464
- const pipe = m[2];
18465
- const rest = m[3] ?? "";
18466
- if (pipe === "|") {
18467
- value = [];
18468
- } else if (rest) {
18469
- value = [rest];
18470
- } else {
18471
- value = [];
18472
- }
18473
- } else if (key) {
18474
- value.push(line.replace(/^\s+/, ""));
18475
- }
18476
- }
18477
- flush();
18478
- return out;
20419
+ function stableStringify2(value) {
20420
+ return JSON.stringify(sortKeys2(value));
18479
20421
  }
18480
- async function collectFiles(dir, baseDir) {
18481
- const results = [];
18482
- const entries = await fsp2.readdir(dir, { withFileTypes: true });
18483
- for (const entry of entries) {
18484
- const fullPath = path6.join(dir, entry.name);
18485
- const relPath = path6.relative(baseDir, fullPath);
18486
- if (entry.isDirectory()) {
18487
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
18488
- results.push(...await collectFiles(fullPath, baseDir));
18489
- } else {
18490
- results.push(relPath);
20422
+ function sortKeys2(value) {
20423
+ if (Array.isArray(value)) return value.map(sortKeys2);
20424
+ if (value && typeof value === "object") {
20425
+ const obj = value;
20426
+ const sorted = {};
20427
+ for (const key of Object.keys(obj).sort()) {
20428
+ sorted[key] = sortKeys2(obj[key]);
18491
20429
  }
20430
+ return sorted;
18492
20431
  }
18493
- return results;
20432
+ return value;
18494
20433
  }
18495
20434
 
18496
20435
  // src/storage/session-rewinder.ts
@@ -18666,7 +20605,7 @@ var DefaultPromptStore = class {
18666
20605
  }
18667
20606
  async list() {
18668
20607
  await ensureDir(this.dir);
18669
- let entries = [];
20608
+ const entries = [];
18670
20609
  try {
18671
20610
  const files = await fsp2.readdir(this.dir);
18672
20611
  for (const file of files) {
@@ -18785,7 +20724,25 @@ var CloudSync = class {
18785
20724
  baseTreeSha,
18786
20725
  `Sync ${cfg.categories.join(", ")} \u2014 ${(/* @__PURE__ */ new Date()).toISOString()}`
18787
20726
  );
18788
- await this.updateRef(token, owner, repoName, branch, commitSha);
20727
+ try {
20728
+ await this.updateRef(token, owner, repoName, branch, commitSha);
20729
+ } catch (err) {
20730
+ if (err instanceof Error && err.message.includes("422")) {
20731
+ const remote = await this.getRef(token, owner, repoName, branch);
20732
+ const currentSha = remote.object.sha;
20733
+ const rebasedCommitSha = await this.createCommit(
20734
+ token,
20735
+ owner,
20736
+ repoName,
20737
+ newTreeSha,
20738
+ currentSha,
20739
+ `Sync ${cfg.categories.join(", ")} \u2014 ${(/* @__PURE__ */ new Date()).toISOString()}`
20740
+ );
20741
+ await this.updateRef(token, owner, repoName, branch, rebasedCommitSha);
20742
+ } else {
20743
+ throw err;
20744
+ }
20745
+ }
18789
20746
  const syncState = {
18790
20747
  version: 1,
18791
20748
  sha: commitSha,
@@ -18924,8 +20881,8 @@ var CloudSync = class {
18924
20881
  const localPath = this.categoryToPath(cat);
18925
20882
  if (!localPath) continue;
18926
20883
  try {
18927
- const stat9 = await fsp2.stat(localPath);
18928
- if (stat9.isDirectory()) {
20884
+ const stat8 = await fsp2.stat(localPath);
20885
+ if (stat8.isDirectory()) {
18929
20886
  const files = await this.walkDir(localPath, localPath);
18930
20887
  for (const file of files) {
18931
20888
  const content = await fsp2.readFile(file, "utf8");
@@ -18950,8 +20907,8 @@ var CloudSync = class {
18950
20907
  const localPath = this.categoryToPath(cat);
18951
20908
  if (!localPath) continue;
18952
20909
  try {
18953
- const stat9 = await fsp2.stat(localPath);
18954
- if (stat9.isDirectory()) {
20910
+ const stat8 = await fsp2.stat(localPath);
20911
+ if (stat8.isDirectory()) {
18955
20912
  const files = await this.walkDir(localPath, localPath);
18956
20913
  for (const file of files) {
18957
20914
  const content = await fsp2.readFile(file);
@@ -19552,7 +21509,7 @@ var SkillGenerator = class {
19552
21509
  };
19553
21510
  return [commonInjection, ...stackSpecific[stack] ?? []];
19554
21511
  }
19555
- getConfigPatterns(stack) {
21512
+ getConfigPatterns(_stack) {
19556
21513
  const commonConfig = [
19557
21514
  {
19558
21515
  id: "insecure-tls",
@@ -20134,7 +22091,6 @@ var SecurityScannerOrchestrator = class {
20134
22091
  retryPolicy;
20135
22092
  errorHandler;
20136
22093
  detector = defaultTechStackDetector;
20137
- reportGenerator = defaultReportGenerator;
20138
22094
  gitignoreUpdater = defaultGitignoreUpdater;
20139
22095
  /**
20140
22096
  * Wraps provider.complete with retry logic using the injected RetryPolicy.
@@ -20318,7 +22274,7 @@ Return ONLY the JSON object, no markdown, no explanation.`;
20318
22274
  /**
20319
22275
  * Scan a batch of files using LLM.
20320
22276
  */
20321
- async scanFileBatchLLM(provider, model, files, skill, techStack) {
22277
+ async scanFileBatchLLM(provider, model, files, skill, _techStack) {
20322
22278
  const fileContents = [];
20323
22279
  for (const file of files) {
20324
22280
  try {
@@ -20515,7 +22471,7 @@ Be specific about the vulnerabilities found and how to fix them.`;
20515
22471
  /**
20516
22472
  * Gather project info for skill generation.
20517
22473
  */
20518
- async gatherProjectInfo(projectRoot, techStack) {
22474
+ async gatherProjectInfo(projectRoot, _techStack) {
20519
22475
  const info = [];
20520
22476
  const keyFiles = [
20521
22477
  "package.json",
@@ -20547,7 +22503,7 @@ ${dirs.join(", ")}`);
20547
22503
  /**
20548
22504
  * Gather files to scan based on patterns.
20549
22505
  */
20550
- async gatherFiles(root, patterns, depth) {
22506
+ async gatherFiles(root, _patterns, depth) {
20551
22507
  const files = [];
20552
22508
  const maxDepth = depth === "quick" ? 2 : depth === "deep" ? 20 : 5;
20553
22509
  const extensions = [".ts", ".js", ".jsx", ".tsx", ".py", ".go", ".java", ".cs", ".rs"];
@@ -20806,22 +22762,22 @@ Use \`/security report <number>\` to view a specific report.` };
20806
22762
  }
20807
22763
  const index = Number.parseInt(reportId, 10) - 1;
20808
22764
  if (!Number.isNaN(index) && reports[index]) {
20809
- const { readFile: readFile34 } = await import('fs/promises');
20810
- const content = await readFile34(join(reportsDir, reports[index]), "utf-8");
22765
+ const { readFile: readFile37 } = await import('fs/promises');
22766
+ const content = await readFile37(join(reportsDir, reports[index]), "utf-8");
20811
22767
  return { message: `# Security Report
20812
22768
 
20813
22769
  ${content}` };
20814
22770
  }
20815
22771
  const match = reports.find((r) => r.includes(reportId));
20816
22772
  if (match) {
20817
- const { readFile: readFile34 } = await import('fs/promises');
20818
- const content = await readFile34(join(reportsDir, match), "utf-8");
22773
+ const { readFile: readFile37 } = await import('fs/promises');
22774
+ const content = await readFile37(join(reportsDir, match), "utf-8");
20819
22775
  return { message: `# Security Report
20820
22776
 
20821
22777
  ${content}` };
20822
22778
  }
20823
22779
  return { message: `\u274C Report "${reportId}" not found. Use \`/security report\` to see available reports.` };
20824
- } catch (error) {
22780
+ } catch (_error) {
20825
22781
  return { message: "\u{1F4ED} No security reports found. Run `/security scan` first." };
20826
22782
  }
20827
22783
  }
@@ -20874,7 +22830,6 @@ var FleetManager = class {
20874
22830
  /** Usage rollup across all subagents. */
20875
22831
  usage;
20876
22832
  manifestPath;
20877
- sessionsRoot;
20878
22833
  directorRunId;
20879
22834
  /** Spawn cap (lifetime total). Infinity means unlimited. */
20880
22835
  maxSpawns;
@@ -20898,11 +22853,18 @@ var FleetManager = class {
20898
22853
  pendingTasks = /* @__PURE__ */ new Map();
20899
22854
  subagentMeta = /* @__PURE__ */ new Map();
20900
22855
  priceLookups = /* @__PURE__ */ new Map();
22856
+ /** Tracks which nickname keys are already assigned — prevents collisions. */
22857
+ _usedNicknames = /* @__PURE__ */ new Set();
20901
22858
  /** The coordinator (wired via setCoordinator by Director after construction). */
20902
22859
  coordinator = null;
22860
+ /** Leader agent's current context pressure (full request tokens). */
22861
+ leaderContextPressure = 0;
22862
+ /** Maximum context load fraction before spawn is refused. */
22863
+ maxLeaderContextLoad;
22864
+ /** Provider's max context window in tokens. */
22865
+ maxContext;
20903
22866
  constructor(opts = {}) {
20904
22867
  this.manifestPath = opts.manifestPath;
20905
- this.sessionsRoot = opts.sessionsRoot;
20906
22868
  this.directorRunId = opts.directorRunId ?? randomUUID();
20907
22869
  this.maxSpawns = opts.maxSpawns ?? Number.POSITIVE_INFINITY;
20908
22870
  this.maxSpawnDepth = opts.maxSpawnDepth ?? 2;
@@ -20910,6 +22872,8 @@ var FleetManager = class {
20910
22872
  this.sessionWriter = opts.sessionWriter ?? null;
20911
22873
  this.manifestDebounceMs = opts.manifestDebounceMs ?? 2e3;
20912
22874
  this.maxFleetCostUsd = opts.directorBudget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
22875
+ this.maxLeaderContextLoad = opts.maxLeaderContextLoad ?? 0.85;
22876
+ this.maxContext = opts.maxContext ?? 128e3;
20913
22877
  this.stateCheckpoint = opts.stateCheckpointPath ? new DirectorStateCheckpoint(
20914
22878
  opts.stateCheckpointPath,
20915
22879
  {
@@ -20924,7 +22888,7 @@ var FleetManager = class {
20924
22888
  this.fleet = new FleetBus();
20925
22889
  this.usage = new FleetUsageAggregator(
20926
22890
  this.fleet,
20927
- (id, provider, model) => {
22891
+ (_id, provider, model) => {
20928
22892
  if (provider && model) return this.priceLookups.get(`${provider}/${model}`);
20929
22893
  return void 0;
20930
22894
  },
@@ -20963,7 +22927,7 @@ var FleetManager = class {
20963
22927
  * which cap was exceeded. Does NOT throw — the caller decides
20964
22928
  * how to surface the rejection.
20965
22929
  */
20966
- canSpawn(config) {
22930
+ canSpawn(_config) {
20967
22931
  if (this.spawnDepth >= this.maxSpawnDepth) {
20968
22932
  return { kind: "max_spawn_depth", limit: this.maxSpawnDepth, observed: this.spawnDepth };
20969
22933
  }
@@ -20976,8 +22940,45 @@ var FleetManager = class {
20976
22940
  return { kind: "max_cost_usd", limit: this.maxFleetCostUsd, observed: totalCost };
20977
22941
  }
20978
22942
  }
22943
+ if (this.maxLeaderContextLoad < 1) {
22944
+ const threshold = this.maxContext * this.maxLeaderContextLoad;
22945
+ if (this.leaderContextPressure >= threshold) {
22946
+ return {
22947
+ kind: "max_context_load",
22948
+ limit: threshold,
22949
+ observed: this.leaderContextPressure
22950
+ };
22951
+ }
22952
+ }
20979
22953
  return null;
20980
22954
  }
22955
+ setLeaderContextPressure(tokens) {
22956
+ this.leaderContextPressure = tokens;
22957
+ }
22958
+ /**
22959
+ * Assign a memorable nickname (e.g. "Einstein (Bug Hunter)") to the config,
22960
+ * record it so the same name is never reused, then record the spawn.
22961
+ *
22962
+ * Call this INSTEAD of `recordSpawn` when you want automatic nicknames.
22963
+ * The nickname is written back to `config.name` BEFORE the coordinator
22964
+ * sees the config, so the manifest, logs, and fleet UI all show it.
22965
+ */
22966
+ assignNicknameAndRecord(subagentId, config, priceLookup) {
22967
+ const role = config.role ?? "subagent";
22968
+ const nickname = assignNickname(role, this._usedNicknames);
22969
+ const baseKey = nickname.split(" ")[0].toLowerCase().replace(/[^a-z0-9-]/g, "-");
22970
+ this._usedNicknames.add(baseKey);
22971
+ config.name = nickname;
22972
+ this.recordSpawn(subagentId, config, priceLookup);
22973
+ return nickname;
22974
+ }
22975
+ /**
22976
+ * Returns the set of already-assigned nickname keys — useful for debugging
22977
+ * and testing.
22978
+ */
22979
+ get usedNicknames() {
22980
+ return this._usedNicknames;
22981
+ }
20981
22982
  /**
20982
22983
  * Records a spawn: increments counter, stores metadata, updates state checkpoint,
20983
22984
  * and schedules a debounced manifest write. Call AFTER the coordinator
@@ -21178,7 +23179,7 @@ function createMcpControlTool(opts) {
21178
23179
  mutating: true,
21179
23180
  riskTier: "standard",
21180
23181
  inputSchema,
21181
- async execute(raw, ctx) {
23182
+ async execute(raw) {
21182
23183
  const input = raw;
21183
23184
  return mcpControlDispatch(input, { getConfig, configPath, registry });
21184
23185
  }
@@ -21726,7 +23727,6 @@ var Agent = class {
21726
23727
  pipelines;
21727
23728
  ctx;
21728
23729
  maxIterations;
21729
- iterationTimeoutMs;
21730
23730
  executionStrategy;
21731
23731
  perIterationOutputCapBytes;
21732
23732
  plugins = [];
@@ -21744,7 +23744,6 @@ var Agent = class {
21744
23744
  this.pipelines = init.pipelines;
21745
23745
  this.ctx = init.context;
21746
23746
  this.maxIterations = init.maxIterations ?? DEFAULT_MAX_ITERATIONS;
21747
- this.iterationTimeoutMs = init.iterationTimeoutMs ?? 3e5;
21748
23747
  this.executionStrategy = init.executionStrategy ?? "smart";
21749
23748
  this.perIterationOutputCapBytes = init.perIterationOutputCapBytes ?? 1e5;
21750
23749
  this.autoExtendLimit = init.autoExtendLimit ?? true;
@@ -21766,9 +23765,6 @@ var Agent = class {
21766
23765
  get permission() {
21767
23766
  return this.container.resolve(TOKENS.PermissionPolicy);
21768
23767
  }
21769
- get scrubber() {
21770
- return this.container.resolve(TOKENS.SecretScrubber);
21771
- }
21772
23768
  get renderer() {
21773
23769
  return this.container.has(TOKENS.Renderer) ? this.container.resolve(TOKENS.Renderer) : void 0;
21774
23770
  }
@@ -21898,6 +23894,11 @@ var Agent = class {
21898
23894
  if (controller.signal.aborted) {
21899
23895
  return { status: "aborted", iterations };
21900
23896
  }
23897
+ await this.ctx.session.writeInFlightMarker(`iteration ${i} / max ${this.maxIterations}`).catch((err) => {
23898
+ this.logger.debug?.(
23899
+ `in-flight marker write failed: ${err instanceof Error ? err.message : String(err)}`
23900
+ );
23901
+ });
21901
23902
  if (autonomousContinue) {
21902
23903
  consumeAutonomousContinue(this.ctx);
21903
23904
  }
@@ -21979,6 +23980,7 @@ var Agent = class {
21979
23980
  finalText = responseResult.finalText;
21980
23981
  const toolUses = res.content.filter(isToolUseBlock);
21981
23982
  if (toolUses.length === 0) {
23983
+ this.emitContextPct();
21982
23984
  this.events.emit("iteration.completed", { ctx: this.ctx, index: i });
21983
23985
  if (autonomousContinue && responseResult.directive === "continue") {
21984
23986
  await this.compactContextIfNeeded();
@@ -21992,11 +23994,13 @@ var Agent = class {
21992
23994
  }
21993
23995
  await this.executeTools(toolUses);
21994
23996
  if (autonomousContinue && consumeAutonomousContinue(this.ctx)) {
23997
+ this.emitContextPct();
21995
23998
  this.events.emit("iteration.completed", { ctx: this.ctx, index: i });
21996
23999
  await this.compactContextIfNeeded();
21997
24000
  await this.extensions.runAfterIteration(this.ctx, i);
21998
24001
  continue;
21999
24002
  }
24003
+ this.emitContextPct();
22000
24004
  this.events.emit("iteration.completed", { ctx: this.ctx, index: i });
22001
24005
  await this.compactContextIfNeeded();
22002
24006
  await this.extensions.runAfterIteration(this.ctx, i);
@@ -22009,6 +24013,12 @@ var Agent = class {
22009
24013
  }
22010
24014
  } finally {
22011
24015
  offSubagentDone();
24016
+ const reason = controller.signal.aborted ? "aborted" : "clean";
24017
+ await this.ctx.session.clearInFlightMarker(reason).catch((err) => {
24018
+ this.logger.debug?.(
24019
+ `in-flight marker clear failed: ${err instanceof Error ? err.message : String(err)}`
24020
+ );
24021
+ });
22012
24022
  }
22013
24023
  }
22014
24024
  /**
@@ -22301,6 +24311,19 @@ var Agent = class {
22301
24311
  async compactContextIfNeeded() {
22302
24312
  await this.pipelines.contextWindow.run(this.ctx);
22303
24313
  }
24314
+ /**
24315
+ * Emit the current context window load as a `ctx.pct` event so subscribers
24316
+ * (FleetBus → TUI) can render a live fill bar per agent.
24317
+ */
24318
+ emitContextPct() {
24319
+ const maxContext = this.ctx.provider.capabilities.maxContext ?? 2e5;
24320
+ const { total } = estimateRequestTokens(
24321
+ this.ctx.messages,
24322
+ this.ctx.systemPrompt,
24323
+ this.ctx.tools ?? []
24324
+ );
24325
+ this.events.emit("ctx.pct", { load: total / maxContext, tokens: total, maxContext });
24326
+ }
22304
24327
  };
22305
24328
  function toError(err) {
22306
24329
  return err instanceof Error ? err : new Error(String(err));
@@ -22977,8 +25000,8 @@ ${mem}`);
22977
25000
  }
22978
25001
  async dirExists(p) {
22979
25002
  try {
22980
- const stat9 = await fsp2.stat(p);
22981
- return stat9.isDirectory();
25003
+ const stat8 = await fsp2.stat(p);
25004
+ return stat8.isDirectory();
22982
25005
  } catch {
22983
25006
  return false;
22984
25007
  }
@@ -23391,10 +25414,28 @@ var DefaultPluginAPI = class {
23391
25414
  }
23392
25415
  this.pipelines = readonlyPipelines;
23393
25416
  const tr = init.toolRegistry;
25417
+ const isOfficial = init.official === true;
25418
+ const assertCanMutateTool = (name, op) => {
25419
+ if (isOfficial) return;
25420
+ const currentOwner = tr.ownerOf(name);
25421
+ if (currentOwner === void 0) return;
25422
+ const ownedSolelyByMe = currentOwner.split("+").every((seg) => seg === owner);
25423
+ if (!ownedSolelyByMe) {
25424
+ throw new Error(
25425
+ `Plugin "${owner}" may not ${op} tool "${name}" \u2014 it is owned by "${currentOwner}". Only official (first-party) plugins may modify tools they do not own.`
25426
+ );
25427
+ }
25428
+ };
23394
25429
  this.tools = {
23395
25430
  register: (t2) => tr.register(t2, owner),
23396
- unregister: (name) => tr.unregister(name),
23397
- wrap: (name, wrapper) => tr.wrap(name, wrapper, owner),
25431
+ unregister: (name) => {
25432
+ assertCanMutateTool(name, "unregister");
25433
+ return tr.unregister(name);
25434
+ },
25435
+ wrap: (name, wrapper) => {
25436
+ assertCanMutateTool(name, "wrap");
25437
+ tr.wrap(name, wrapper, owner);
25438
+ },
23398
25439
  get: (name) => tr.get(name),
23399
25440
  list: () => tr.list()
23400
25441
  };
@@ -25296,6 +27337,203 @@ function assertSafePath(dir, projectRoot) {
25296
27337
  }
25297
27338
  }
25298
27339
 
27340
+ // src/coordination/collab-bus.ts
27341
+ var CollaborationBus = class {
27342
+ pausePromise = null;
27343
+ pauseResolve = null;
27344
+ pausedAtMs = null;
27345
+ pausedBy = null;
27346
+ // ── State queries ──────────────────────────────────────────────────────
27347
+ isPaused() {
27348
+ return this.pausePromise !== null;
27349
+ }
27350
+ getState() {
27351
+ return {
27352
+ paused: this.isPaused(),
27353
+ pausedAt: this.pausedAtMs ? new Date(this.pausedAtMs).toISOString() : null,
27354
+ pausedBy: this.pausedBy
27355
+ };
27356
+ }
27357
+ // ── Pause / resume control ─────────────────────────────────────────────
27358
+ /**
27359
+ * Pause the agent loop. Idempotent: a second `requestPause` while
27360
+ * already paused is a no-op (the original pause wins; we do not
27361
+ * overwrite `pausedBy`). Returns true when the state actually
27362
+ * transitioned, false when it was already paused.
27363
+ */
27364
+ requestPause(byParticipant) {
27365
+ if (this.isPaused()) return false;
27366
+ this.pausedAtMs = Date.now();
27367
+ this.pausedBy = byParticipant;
27368
+ this.pausePromise = new Promise((resolve9) => {
27369
+ this.pauseResolve = resolve9;
27370
+ });
27371
+ return true;
27372
+ }
27373
+ /**
27374
+ * Resume the agent loop. Returns true when the state actually
27375
+ * transitioned from paused → running, false when it was already
27376
+ * running (no-op).
27377
+ */
27378
+ resume() {
27379
+ if (!this.isPaused()) return false;
27380
+ if (this.pauseResolve) this.pauseResolve();
27381
+ this.pausePromise = null;
27382
+ this.pauseResolve = null;
27383
+ this.pausedAtMs = null;
27384
+ this.pausedBy = null;
27385
+ return true;
27386
+ }
27387
+ // ── Wait semantics (consumed by the middleware) ────────────────────────
27388
+ /**
27389
+ * Block until the bus is resumed or the timeout fires. Returns:
27390
+ * - `true` → bus was resumed in time
27391
+ * - `false` → timeout fired; bus was auto-resumed as a side effect
27392
+ *
27393
+ * When `timeoutMs` is `0` the wait is unbounded (the middleware must
27394
+ * be paired with an external AbortSignal in that case — we don't
27395
+ * expose one here to keep the API simple).
27396
+ */
27397
+ async waitForResume(timeoutMs) {
27398
+ if (!this.isPaused()) return true;
27399
+ if (!this.pausePromise) return true;
27400
+ if (timeoutMs === 0) {
27401
+ await this.pausePromise;
27402
+ return true;
27403
+ }
27404
+ let timer;
27405
+ const timeoutPromise = new Promise((resolve9) => {
27406
+ timer = setTimeout(() => resolve9("timeout"), timeoutMs);
27407
+ });
27408
+ const resumedPromise = this.pausePromise.then(() => "resumed");
27409
+ const winner = await Promise.race([resumedPromise, timeoutPromise]);
27410
+ if (timer) clearTimeout(timer);
27411
+ if (winner === "timeout") {
27412
+ this.resume();
27413
+ return false;
27414
+ }
27415
+ return true;
27416
+ }
27417
+ // ── Manual tool-call injection (Phase 4) ─────────────────────────────────
27418
+ //
27419
+ // A controller can ask the agent loop to use a specific tool_result
27420
+ // for a given tool_use_id — bypassing the real tool execution. This
27421
+ // is "I want the agent to think the read returned THIS content" or
27422
+ // "skip the bash call, just give it the answer I typed". The
27423
+ // injection is matched by tool_use_id, consumed once, and discarded.
27424
+ injectionQueue = /* @__PURE__ */ new Map();
27425
+ /**
27426
+ * Queue a manual tool result. The next time the agent's toolCall
27427
+ * pipeline sees a matching `toolUse.id`, the
27428
+ * `collabInjectMiddleware` consumes this entry and replaces the
27429
+ * real tool execution. Returns `false` if an injection for the
27430
+ * same id is already queued (idempotent — first write wins).
27431
+ */
27432
+ injectToolResult(input) {
27433
+ if (this.injectionQueue.has(input.toolUseId)) return false;
27434
+ this.injectionQueue.set(input.toolUseId, input);
27435
+ return true;
27436
+ }
27437
+ /**
27438
+ * Pop an injection from the queue. Returns the payload (and
27439
+ * removes it) if one is pending, or `null` when nothing matches.
27440
+ * Called by the middleware on every tool call.
27441
+ */
27442
+ takeInjection(toolUseId) {
27443
+ const v = this.injectionQueue.get(toolUseId);
27444
+ if (!v) return null;
27445
+ this.injectionQueue.delete(toolUseId);
27446
+ return v;
27447
+ }
27448
+ /** Inspect the queue size (for observability / tests). */
27449
+ pendingInjectionCount() {
27450
+ return this.injectionQueue.size;
27451
+ }
27452
+ };
27453
+
27454
+ // src/middleware/collab-pause.ts
27455
+ function collabPauseMiddleware(bus, opts = {}) {
27456
+ const timeoutMs = opts.defaultTimeoutMs ?? 6e4;
27457
+ const logger = opts.logger;
27458
+ return async function collabPause(payload, next) {
27459
+ if (!bus.isPaused()) {
27460
+ return next();
27461
+ }
27462
+ const state = bus.getState();
27463
+ logger?.debug?.(
27464
+ `collab-pause: tool '${payload.toolUse.name}' blocked \u2014 bus paused by ${state.pausedBy ?? "?"} at ${state.pausedAt ?? "?"}, waiting up to ${timeoutMs}ms for resume`
27465
+ );
27466
+ const resumed = await bus.waitForResume(timeoutMs);
27467
+ if (!resumed) {
27468
+ logger?.warn?.(
27469
+ `collab-pause: timeout after ${timeoutMs}ms \u2014 auto-resuming the bus to unblock the agent loop`
27470
+ );
27471
+ } else {
27472
+ logger?.debug?.(`collab-pause: resumed \u2014 proceeding with tool '${payload.toolUse.name}'`);
27473
+ }
27474
+ return next();
27475
+ };
27476
+ }
27477
+ function collabInjectMiddleware(bus, opts = {}) {
27478
+ const logger = opts.logger;
27479
+ return async function collabInject(payload, next) {
27480
+ const injected = bus.takeInjection(payload.toolUse.id);
27481
+ if (!injected) {
27482
+ return next();
27483
+ }
27484
+ logger?.debug?.(
27485
+ `collab-inject: tool '${payload.toolUse.name}' (id ${payload.toolUse.id}) \u2014 using controller-injected result (reason: ${injected.reason})`
27486
+ );
27487
+ payload.result = {
27488
+ type: "tool_result",
27489
+ tool_use_id: payload.toolUse.id,
27490
+ content: typeof injected.content === "string" ? injected.content : JSON.stringify(injected.content),
27491
+ is_error: injected.isError
27492
+ };
27493
+ };
27494
+ }
27495
+
27496
+ // src/replay/replay-provider-runner.ts
27497
+ var ReplayProviderRunner = class {
27498
+ constructor(inner, opts) {
27499
+ this.inner = inner;
27500
+ this.opts = opts;
27501
+ }
27502
+ inner;
27503
+ opts;
27504
+ async run(runOpts) {
27505
+ const hash = hashRequest(runOpts.request);
27506
+ const cached = await this.opts.log.lookup(this.opts.sessionId, hash);
27507
+ if (this.opts.mode === "replay") {
27508
+ if (!cached) {
27509
+ this.opts.logger?.warn?.(
27510
+ `replay: no recorded response for hash ${hash} (model ${runOpts.request.model})`
27511
+ );
27512
+ throw new Error(
27513
+ `ReplayProviderRunner: no recorded response for hash ${hash} in session ${this.opts.sessionId}. Either the request changed since recording, or this session has no replay log.`
27514
+ );
27515
+ }
27516
+ this.opts.logger?.debug?.(
27517
+ `replay: served cached response for hash ${hash} (recorded ${cached.ts})`
27518
+ );
27519
+ return cached.response;
27520
+ }
27521
+ if (this.opts.mode === "auto" && cached) {
27522
+ this.opts.logger?.debug?.(
27523
+ `replay: auto-hit hash ${hash}, served cached response`
27524
+ );
27525
+ return cached.response;
27526
+ }
27527
+ const response = await this.inner.run(runOpts);
27528
+ await this.opts.log.record({
27529
+ sessionId: this.opts.sessionId,
27530
+ request: runOpts.request,
27531
+ response
27532
+ });
27533
+ return response;
27534
+ }
27535
+ };
27536
+
25299
27537
  // src/plugins/prompts-plugin.ts
25300
27538
  function createPromptsPlugin(opts) {
25301
27539
  let store = null;
@@ -25442,9 +27680,12 @@ function dim2(s) {
25442
27680
  }
25443
27681
 
25444
27682
  // src/plugins/sync-plugin.ts
27683
+ init_atomic_write();
25445
27684
  function createSyncPlugin(opts) {
25446
27685
  let cloud = null;
25447
27686
  let configStore;
27687
+ let vault;
27688
+ let syncConfigPath;
25448
27689
  return {
25449
27690
  name: "wstack-sync",
25450
27691
  version: "1.0.0",
@@ -25456,6 +27697,8 @@ function createSyncPlugin(opts) {
25456
27697
  const rawConfig = api.config;
25457
27698
  const paths = opts?.paths ?? rawConfig.paths;
25458
27699
  configStore = opts?.configStore ?? rawConfig.configStore;
27700
+ vault = opts?.vault ?? rawConfig.vault;
27701
+ syncConfigPath = paths?.syncConfig;
25459
27702
  if (!paths || !configStore) {
25460
27703
  api.log.warn("[sync] paths or configStore not available \u2014 /sync disabled");
25461
27704
  return;
@@ -25471,7 +27714,7 @@ function createSyncPlugin(opts) {
25471
27714
  }
25472
27715
  );
25473
27716
  void cloud.loadState();
25474
- api.slashCommands.register(buildSyncCommand(cloud, configStore));
27717
+ api.slashCommands.register(buildSyncCommand(cloud, configStore, vault, syncConfigPath));
25475
27718
  api.log.info("[sync] loaded \u2014 /sync available. Run /sync to get started.");
25476
27719
  },
25477
27720
  teardown(api) {
@@ -25483,7 +27726,7 @@ function createSyncPlugin(opts) {
25483
27726
  }
25484
27727
  };
25485
27728
  }
25486
- function buildSyncCommand(cloud, configStore) {
27729
+ function buildSyncCommand(cloud, configStore, vault, syncConfigPath) {
25487
27730
  return {
25488
27731
  name: "sync",
25489
27732
  description: "Cloud sync: /sync [status|enable|disable|push|pull|categories]",
@@ -25511,13 +27754,17 @@ function buildSyncCommand(cloud, configStore) {
25511
27754
  if (!repo || !repo.includes("/")) {
25512
27755
  return { message: 'Invalid repo format. Expected "owner/repo".' };
25513
27756
  }
27757
+ const storedToken = vault ? vault.encrypt(token) : token;
25514
27758
  const syncConfig = {
25515
27759
  enabled: true,
25516
27760
  repo,
25517
- githubToken: token,
27761
+ githubToken: storedToken,
25518
27762
  categories: cats.length > 0 ? cats : ALL_SYNC_CATEGORIES,
25519
27763
  lastSyncedAt: void 0
25520
27764
  };
27765
+ if (syncConfigPath) {
27766
+ await atomicWrite(syncConfigPath, JSON.stringify(syncConfig, null, 2), { mode: 384 });
27767
+ }
25521
27768
  configStore.update({ sync: syncConfig });
25522
27769
  await cloud.loadState();
25523
27770
  return {
@@ -26344,6 +28591,6 @@ ${formatPlan(updated)}`
26344
28591
  };
26345
28592
  }
26346
28593
 
26347
- export { AGENTS_BY_PHASE, AGENT_CATALOG, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, ALL_SYNC_CATEGORIES, AUDIT_LOG_AGENT, Agent, AgentError, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutoPhasePlanner, AutoPhaseRunner, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, CONTEXT_WINDOW_MODES, CheckpointManager, CloudSync, ConfigError, ConfigMigrationError, Container, Context, ConversationState, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultPromptStore, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorStateCheckpoint, DoneConditionChecker, ERROR_CODES, EternalAutonomyEngine, EventBus, ExtensionRegistry, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FleetBus, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, FsError, GitignoreUpdater, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, LAYER_1_IDENTITY, LLMSelector, MAX_JOURNAL_ENTRIES, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, PhaseGraphBuilder, PhaseOrchestrator, PhaseStore, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, ReportGenerator, RunController, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, ScopedEventBus, SddParallelRun, SddTaskDecomposer, SecurityScanner, SecurityScannerOrchestrator, SelectiveCompactor, SessionAnalyzer, SessionError, SkillGenerator, SkillInstaller, SkillManifestStore, SlashCommandRegistry, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TOKENS, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, TechStackDetector, ToolError, ToolExecutor, ToolRegistry, WorktreeManager, WrongStackError, addPlanItem, allServers, analyzeCriticalPath, appendJournal, applyRosterBudget, asBlocks, asText, assertSafePath, atomicWrite, attachAutoExtend, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildBtwBlock, buildChildEnv, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildRecoveryStrategies, classifyFamily, clearPlan, color, compileGlob, compileUserRegex, composeDirectorPrompt, composeSubagentPrompt, computeTaskProgress, consumeBtwNotes, context7Server, contextManagerTool, createAutoExecutor, createAutoPhaseFromTaskGraph, createContextManagerTool, createDefaultPipelines, createDelegateTool, createGitPlugin, createMcpControlTool, createMessage, createObservabilityPlugin, createPlanPlugin, createPromptsPlugin, createSecurityPlugin, createSecuritySlashCommand, createSkillsPlugin, createSyncPlugin, createToolOutputSerializer, decryptConfigSecrets, defaultGitignoreUpdater, defaultOrchestrator, defaultReportGenerator, defaultSecurityScanner, defaultSkillGenerator, defaultTechStackDetector, deriveTodosFromPlanItem, detectNewlineStyle, dispatchAgent, downloadGitHubTarball, emptyGoal, emptyPlan, encryptConfigSecrets, ensureDir, estimateRequestTokens, estimateTextTokens, estimateToolDefTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, extractRunEnv, filesystemServer, findCriticalPath, formatContextWindowModeList, formatGoal, formatPlan, formatPlanTemplates, formatTodosList, getAgentDefinition, getContextWindowMode, getPlanTemplate, getTemplate, githubServer, goalFilePath, googleMapsServer, isAgentError, isConfigError, isContextWindowModeId, isFsError, isImageBlock, isPluginError, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, listContextWindowModes, listPlanTemplates, listTemplates, loadDirectorState, loadGoal, loadPlan, loadPlugins, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAutonomyPromptContributor, makeContinueToNextIterationTool, makeDirectorSessionFactory, makeLLMClassifier, matchAny, matchGlob, migratePlaintextSecrets, miniMaxVisionServer, normalizeToLf, parseContinueDirective, parseSkillRef, pendingBtwCount, projectHash, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, repairToolUseAdjacency, resolveContextWindowPolicy, resolveWstackPaths, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, safeParse, safeStringify, sanitizeJsonString, saveGoal, savePlan, saveTodosCheckpoint, scoreAgents, securitySlashCommand, sentinelServer, setBtwNote, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, stripAnsi, summarizeUsage, templateToMarkdown, toStyle, toWrongStackError, topologicalSort, unifiedDiff, unloadPlugins, validateAgainstSchema, wireMetricsToEvents, wrapAsState, zaiVisionServer };
28594
+ export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, ALL_SYNC_CATEGORIES, AUDIT_LOG_AGENT, Agent, AgentError, AnnotationsStore, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutoPhasePlanner, AutoPhaseRunner, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, CONTEXT_WINDOW_MODES, CheckpointManager, CloudSync, CollaborationBus, ConfigError, ConfigMigrationError, Container, Context, ConversationState, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultPromptStore, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorStateCheckpoint, DoneConditionChecker, ERROR_CODES, EternalAutonomyEngine, EventBus, ExtensionRegistry, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FleetBus, FleetCostCapError, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, FsError, GitignoreUpdater, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, LAYER_1_IDENTITY, LLMSelector, MAX_JOURNAL_ENTRIES, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, PhaseGraphBuilder, PhaseOrchestrator, PhaseStore, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, ReplayLogStore, ReplayProviderRunner, ReportGenerator, RunController, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, ScopedEventBus, SddParallelRun, SddTaskDecomposer, SecurityScanner, SecurityScannerOrchestrator, SelectiveCompactor, SessionAnalyzer, SessionError, SessionRecovery, SkillGenerator, SkillInstaller, SkillManifestStore, SlashCommandRegistry, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TOKENS, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, TechStackDetector, ToolAuditLog, ToolError, ToolExecutor, ToolRegistry, WorktreeManager, WrongStackError, addPlanItem, allServers, analyzeCriticalPath, appendJournal, applyRosterBudget, asBlocks, asText, assertSafePath, atomicWrite, attachAutoExtend, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildBtwBlock, buildChildEnv, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildRecoveryStrategies, classifyFamily, clearPlan, collabInjectMiddleware, collabPauseMiddleware, color, compileGlob, compileUserRegex, completePartialObject, composeDirectorPrompt, composeSubagentPrompt, computeTaskProgress, consumeBtwNotes, context7Server, contextManagerTool, createAutoExecutor, createAutoPhaseFromTaskGraph, createContextManagerTool, createDefaultPipelines, createDelegateTool, createGitPlugin, createMcpControlTool, createMessage, createObservabilityPlugin, createPlanPlugin, createPromptsPlugin, createSecurityPlugin, createSecuritySlashCommand, createSkillsPlugin, createSyncPlugin, createToolOutputSerializer, decryptConfigSecrets, defaultGitignoreUpdater, defaultOrchestrator, defaultReportGenerator, defaultSecurityScanner, defaultSkillGenerator, defaultTechStackDetector, deriveTodosFromPlanItem, detectNewlineStyle, dispatchAgent, downloadGitHubTarball, emptyGoal, emptyPlan, encryptConfigSecrets, ensureDir, estimateRequestTokens, estimateTextTokens, estimateToolDefTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, extractRunEnv, filesystemServer, findCriticalPath, formatContextWindowModeList, formatGoal, formatPlan, formatPlanTemplates, formatTodosList, getAgentDefinition, getContextWindowMode, getPlanTemplate, getTemplate, githubServer, goalFilePath, googleMapsServer, hashRequest, isAgentError, isConfigError, isContextWindowModeId, isFsError, isImageBlock, isPluginError, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, listContextWindowModes, listPlanTemplates, listTemplates, loadDirectorState, loadGoal, loadPlan, loadPlugins, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeContinueToNextIterationTool, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeRollUpTool, makeSpawnTool, makeTerminateTool, matchAny, matchGlob, migratePlaintextSecrets, miniMaxVisionServer, normalizeToLf, parseContinueDirective, parseSkillRef, pendingBtwCount, projectHash, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, repairToolUseAdjacency, resolveContextWindowPolicy, resolveWstackPaths, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, runProviderWithRetry, safeParse, safeStringify, sanitizeJsonString, saveGoal, savePlan, saveTodosCheckpoint, scoreAgents, securitySlashCommand, sentinelServer, setBtwNote, setPlanItemStatus, slackServer, stableStringify, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, stripAnsi, summarizeUsage, templateToMarkdown, toStyle, toWrongStackError, topologicalSort, unifiedDiff, unloadPlugins, validateAgainstSchema, wireMetricsToEvents, wrapAsState, zaiVisionServer };
26348
28595
  //# sourceMappingURL=index.js.map
26349
28596
  //# sourceMappingURL=index.js.map