@wrongstack/core 0.109.1 → 0.141.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/{agent-bridge-mOxbpFcg.d.ts → agent-bridge-r9y6gdn4.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-DukQLUcS.d.ts → agent-subagent-runner-1GeQE_L0.d.ts} +7 -8
  3. package/dist/{brain-Dfv4Y82E.d.ts → brain-Cp_3GIS2.d.ts} +14 -3
  4. package/dist/{compactor-DXLxLcmU.d.ts → compactor-BueGt7LG.d.ts} +9 -1
  5. package/dist/{config-BSU-6vah.d.ts → config-BaVThgnT.d.ts} +104 -3
  6. package/dist/{context-CNRYfhUv.d.ts → context-C7G_MtLV.d.ts} +38 -1
  7. package/dist/coordination/index.d.ts +12 -13
  8. package/dist/coordination/index.js +229 -17
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/defaults/index.d.ts +26 -27
  11. package/dist/defaults/index.js +886 -721
  12. package/dist/defaults/index.js.map +1 -1
  13. package/dist/execution/index.d.ts +15 -16
  14. package/dist/execution/index.js +535 -322
  15. package/dist/execution/index.js.map +1 -1
  16. package/dist/extension/index.d.ts +6 -7
  17. package/dist/{goal-preamble-CI8lxeY1.d.ts → goal-preamble-iuIUTQwk.d.ts} +48 -18
  18. package/dist/{index-BWRN6wOb.d.ts → index-BZdezm3g.d.ts} +9 -10
  19. package/dist/{index-DIKEcfgC.d.ts → index-CPweVoFM.d.ts} +7 -6
  20. package/dist/index.d.ts +51 -52
  21. package/dist/index.js +752 -545
  22. package/dist/index.js.map +1 -1
  23. package/dist/infrastructure/index.d.ts +6 -7
  24. package/dist/infrastructure/index.js +64 -19
  25. package/dist/infrastructure/index.js.map +1 -1
  26. package/dist/kernel/index.d.ts +10 -11
  27. package/dist/kernel/index.js.map +1 -1
  28. package/dist/llm-selector-CP72f1lC.d.ts +58 -0
  29. package/dist/{mcp-servers-CXCsANdY.d.ts → mcp-servers-Bl5LTvQg.d.ts} +3 -3
  30. package/dist/{mode-ARA3HrkY.d.ts → mode-CZlO9iU1.d.ts} +1 -1
  31. package/dist/models/index.d.ts +157 -53
  32. package/dist/models/index.js +475 -20
  33. package/dist/models/index.js.map +1 -1
  34. package/dist/{models-registry-DU64QxQa.d.ts → models-registry-D90K9UnM.d.ts} +1 -1
  35. package/dist/{multi-agent-coordinator-51LvnXkD.d.ts → multi-agent-coordinator-QWEzJDlm.d.ts} +1 -1
  36. package/dist/{null-fleet-bus-D09hMzFQ.d.ts → null-fleet-bus-BUyfqh23.d.ts} +13 -10
  37. package/dist/observability/index.d.ts +2 -2
  38. package/dist/{parallel-eternal-engine-B2CbsKpc.d.ts → parallel-eternal-engine-Dj2SYzha.d.ts} +24 -11
  39. package/dist/{path-resolver-DDJiMAtX.d.ts → path-resolver-DRjQBkoO.d.ts} +3 -3
  40. package/dist/{permission-BDv7z0mk.d.ts → permission-B7nKnEvQ.d.ts} +1 -1
  41. package/dist/{permission-policy-dF74EpDp.d.ts → permission-policy-8-6zBmfA.d.ts} +2 -13
  42. package/dist/{pipeline-BqiA_UMr.d.ts → pipeline-BG7UgbDc.d.ts} +2 -2
  43. package/dist/{plan-templates-BdDxl9cI.d.ts → plan-templates-CkKNPU3I.d.ts} +6 -6
  44. package/dist/{provider-runner-BUunikwY.d.ts → provider-runner-BNpuIyOL.d.ts} +3 -3
  45. package/dist/{retry-policy-BcmuT_V0.d.ts → retry-policy-rutAfVeR.d.ts} +1 -1
  46. package/dist/sdd/index.d.ts +8 -9
  47. package/dist/sdd/index.js +208 -1
  48. package/dist/sdd/index.js.map +1 -1
  49. package/dist/{secret-vault-DrOhc2i5.d.ts → secret-vault-BTcC_T5v.d.ts} +3 -2
  50. package/dist/security/index.d.ts +5 -7
  51. package/dist/security/index.js +4 -35
  52. package/dist/security/index.js.map +1 -1
  53. package/dist/{selector-C7wcdqMA.d.ts → selector-4vDFZKt3.d.ts} +1 -1
  54. package/dist/{session-event-bridge-BpJ5trO9.d.ts → session-event-bridge-DWlvglC2.d.ts} +2 -2
  55. package/dist/{session-reader-DDz1Ek4V.d.ts → session-reader-BAtCxdaw.d.ts} +1 -1
  56. package/dist/storage/index.d.ts +14 -15
  57. package/dist/storage/index.js +140 -134
  58. package/dist/storage/index.js.map +1 -1
  59. package/dist/types/index.d.ts +19 -20
  60. package/dist/types/index.js +186 -102
  61. package/dist/types/index.js.map +1 -1
  62. package/dist/utils/index.d.ts +38 -15
  63. package/dist/utils/index.js +62 -33
  64. package/dist/utils/index.js.map +1 -1
  65. package/dist/{wstack-paths-_lqjzErq.d.ts → wstack-paths-DD50Omgn.d.ts} +3 -0
  66. package/package.json +2 -2
  67. package/dist/models-registry-B6_KfS65.d.ts +0 -95
@@ -1,9 +1,9 @@
1
1
  import * as crypto2 from 'crypto';
2
- import { randomBytes, randomUUID, createCipheriv, createDecipheriv, createHash } from 'crypto';
2
+ import { randomBytes, createCipheriv, createDecipheriv, randomUUID, createHash } from 'crypto';
3
3
  import * as fsp from 'fs/promises';
4
4
  import * as path11 from 'path';
5
5
  import { isAbsolute, resolve } from 'path';
6
- import * as fs5 from 'fs';
6
+ import * as fs4 from 'fs';
7
7
  import * as os from 'os';
8
8
  import { hostname } from 'os';
9
9
  import { execFile } from 'child_process';
@@ -218,7 +218,7 @@ var DefaultLogger = class _DefaultLogger {
218
218
  this.stderr = opts.stderr !== false;
219
219
  if (this.file) {
220
220
  try {
221
- fs5.mkdirSync(path11.dirname(this.file), { recursive: true });
221
+ fs4.mkdirSync(path11.dirname(this.file), { recursive: true });
222
222
  } catch {
223
223
  }
224
224
  }
@@ -258,7 +258,7 @@ var DefaultLogger = class _DefaultLogger {
258
258
  }
259
259
  if (this.file) {
260
260
  try {
261
- fs5.appendFileSync(this.file, `${JSON.stringify(entry)}
261
+ fs4.appendFileSync(this.file, `${JSON.stringify(entry)}
262
262
  `);
263
263
  } catch {
264
264
  }
@@ -452,8 +452,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
452
452
  onClose: (s) => this.appendToIndex(s)
453
453
  });
454
454
  } catch (err) {
455
- await handle.close().catch(() => {
456
- });
455
+ await handle.close().catch((e) => console.warn(`[session-store] handle.close() failed: ${e}`));
457
456
  throw err;
458
457
  }
459
458
  }
@@ -484,8 +483,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
484
483
  );
485
484
  return { writer, data };
486
485
  } catch (err) {
487
- await handle.close().catch(() => {
488
- });
486
+ await handle.close().catch((e) => console.warn(`[session-store] handle.close() failed: ${e}`));
489
487
  throw err;
490
488
  }
491
489
  }
@@ -674,15 +672,15 @@ var DefaultSessionStore = class _DefaultSessionStore {
674
672
  * sidecars, and the session directory (fleet.json, shared/, subagents/).
675
673
  */
676
674
  async deleteSession(id) {
677
- await fsp.unlink(this.sessionPath(id, ".jsonl")).catch(() => void 0);
678
- await fsp.unlink(this.sessionPath(id, ".summary.json")).catch(() => void 0);
675
+ await fsp.unlink(this.sessionPath(id, ".jsonl")).catch((err) => console.warn(`[session-store] delete .jsonl failed: ${err}`));
676
+ await fsp.unlink(this.sessionPath(id, ".summary.json")).catch((err) => console.warn(`[session-store] delete .summary.json failed: ${err}`));
679
677
  const shardDir = path11.dirname(path11.join(this.dir, id));
680
678
  const base = path11.basename(id);
681
679
  for (const ext of [".plan.json", ".todos.json"]) {
682
- await fsp.unlink(path11.join(shardDir, `${base}${ext}`)).catch(() => void 0);
680
+ await fsp.unlink(path11.join(shardDir, `${base}${ext}`)).catch((err) => console.warn(`[session-store] delete ${ext} failed: ${err}`));
683
681
  }
684
682
  const sessDir = path11.join(shardDir, base);
685
- await fsp.rm(sessDir, { recursive: true, force: true }).catch(() => void 0);
683
+ await fsp.rm(sessDir, { recursive: true, force: true }).catch((err) => console.warn(`[session-store] delete session dir failed: ${err}`));
686
684
  await this.writeTombstone(id);
687
685
  }
688
686
  async delete(id) {
@@ -1510,39 +1508,41 @@ ${line}`;
1510
1508
  }
1511
1509
  async forget(scope, query, filePath) {
1512
1510
  const file = this.resolveFile(filePath, scope);
1513
- let existing;
1514
- try {
1515
- existing = await fsp.readFile(file, "utf8");
1516
- } catch {
1517
- return 0;
1518
- }
1519
- const needle = query.toLowerCase();
1520
- const idMatcher = /mem_\d+_\w+/;
1521
- let removed = 0;
1522
- const lines = existing.split("\n").filter((line) => {
1523
- const trimmed = line.trim();
1524
- if (!trimmed.startsWith("- ")) return true;
1525
- if (idMatcher.test(query)) {
1526
- const entryIdMatch = /mem_\d+_\w+/.exec(trimmed);
1527
- if (entryIdMatch && entryIdMatch[0] === query) {
1511
+ return withFileLock(file, async () => {
1512
+ let existing;
1513
+ try {
1514
+ existing = await fsp.readFile(file, "utf8");
1515
+ } catch {
1516
+ return 0;
1517
+ }
1518
+ const needle = query.toLowerCase();
1519
+ const idMatcher = /mem_\d+_\w+/;
1520
+ let removed = 0;
1521
+ const lines = existing.split("\n").filter((line) => {
1522
+ const trimmed = line.trim();
1523
+ if (!trimmed.startsWith("- ")) return true;
1524
+ if (idMatcher.test(query)) {
1525
+ const entryIdMatch = /mem_\d+_\w+/.exec(trimmed);
1526
+ if (entryIdMatch && entryIdMatch[0] === query) {
1527
+ removed++;
1528
+ return false;
1529
+ }
1530
+ }
1531
+ if (trimmed.toLowerCase().includes(needle)) {
1528
1532
  removed++;
1529
1533
  return false;
1530
1534
  }
1535
+ return true;
1536
+ });
1537
+ if (removed > 0) {
1538
+ if (lines.length === 0 || lines.length === 1 && !lines[0]?.trim()) {
1539
+ await atomicWrite(file, "");
1540
+ } else {
1541
+ await atomicWrite(file, lines.join("\n"));
1542
+ }
1531
1543
  }
1532
- if (trimmed.toLowerCase().includes(needle)) {
1533
- removed++;
1534
- return false;
1535
- }
1536
- return true;
1544
+ return removed;
1537
1545
  });
1538
- if (removed > 0) {
1539
- if (lines.length === 0 || lines.length === 1 && !lines[0]?.trim()) {
1540
- await atomicWrite(file, "");
1541
- } else {
1542
- await atomicWrite(file, lines.join("\n"));
1543
- }
1544
- }
1545
- return removed;
1546
1546
  }
1547
1547
  async readAll(scope, filePath) {
1548
1548
  const file = this.resolveFile(filePath, scope);
@@ -1983,20 +1983,101 @@ function deepFreeze(obj) {
1983
1983
  return Object.freeze(obj);
1984
1984
  }
1985
1985
 
1986
- // src/security/config-secrets.ts
1987
- function decryptConfigSecrets(cfg, vault) {
1986
+ // src/types/secret-vault.ts
1987
+ var ENCRYPTED_PREFIX = "enc:v1:";
1988
+
1989
+ // src/security/secret-vault.ts
1990
+ init_atomic_write();
1991
+ var KEY_BYTES = 32;
1992
+ var IV_BYTES = 12;
1993
+ var TAG_BYTES = 16;
1994
+ var ALGO = "aes-256-gcm";
1995
+ var DefaultSecretVault = class {
1996
+ keyFile;
1997
+ key;
1998
+ constructor(opts) {
1999
+ this.keyFile = opts.keyFile;
2000
+ }
2001
+ isEncrypted(value) {
2002
+ return typeof value === "string" && value.startsWith(ENCRYPTED_PREFIX);
2003
+ }
2004
+ encrypt(plaintext) {
2005
+ if (this.isEncrypted(plaintext)) return plaintext;
2006
+ const key = this.loadOrCreateKey();
2007
+ const iv = randomBytes(IV_BYTES);
2008
+ const cipher = createCipheriv(ALGO, key, iv);
2009
+ const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
2010
+ const tag = cipher.getAuthTag();
2011
+ return `${ENCRYPTED_PREFIX}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
2012
+ }
2013
+ decrypt(value) {
2014
+ if (!this.isEncrypted(value)) return value;
2015
+ const rest = value.slice(ENCRYPTED_PREFIX.length);
2016
+ const parts = rest.split(":");
2017
+ if (parts.length !== 3) {
2018
+ throw new Error("SecretVault: malformed encrypted value");
2019
+ }
2020
+ const [ivB64, tagB64, ctB64] = parts;
2021
+ const iv = Buffer.from(ivB64, "base64");
2022
+ const tag = Buffer.from(tagB64, "base64");
2023
+ const ct = Buffer.from(ctB64, "base64");
2024
+ if (iv.length !== IV_BYTES) throw new Error("SecretVault: bad IV length");
2025
+ if (tag.length !== TAG_BYTES) throw new Error("SecretVault: bad tag length");
2026
+ const key = this.loadOrCreateKey();
2027
+ const decipher = createDecipheriv(ALGO, key, iv);
2028
+ decipher.setAuthTag(tag);
2029
+ const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
2030
+ return pt.toString("utf8");
2031
+ }
2032
+ loadOrCreateKey() {
2033
+ if (this.key) return this.key;
2034
+ try {
2035
+ const buf = fs4.readFileSync(this.keyFile);
2036
+ if (buf.length !== KEY_BYTES) {
2037
+ throw new Error(
2038
+ `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`
2039
+ );
2040
+ }
2041
+ this.key = buf;
2042
+ return this.key;
2043
+ } catch (err) {
2044
+ if (err.code !== "ENOENT") throw err;
2045
+ }
2046
+ fs4.mkdirSync(path11.dirname(this.keyFile), { recursive: true });
2047
+ const key = randomBytes(KEY_BYTES);
2048
+ try {
2049
+ fs4.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
2050
+ } catch (err) {
2051
+ if (err.code !== "EEXIST") throw err;
2052
+ const buf = fs4.readFileSync(this.keyFile);
2053
+ if (buf.length !== KEY_BYTES) {
2054
+ throw new Error(
2055
+ `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`
2056
+ );
2057
+ }
2058
+ this.key = buf;
2059
+ return this.key;
2060
+ }
2061
+ this.key = key;
2062
+ return key;
2063
+ }
2064
+ };
2065
+ function decryptConfigSecrets(cfg, vault, opts) {
2066
+ const warn = opts?.warn ?? ((msg) => console.warn(msg));
1988
2067
  return walk(cfg, vault, (v, key) => {
1989
2068
  try {
1990
2069
  return vault.decrypt(v);
1991
2070
  } catch (err) {
1992
- console.warn(
1993
- `[secret-vault] Failed to decrypt "${key}":`,
1994
- err instanceof Error ? err.message : err
2071
+ warn(
2072
+ `[secret-vault] Failed to decrypt "${key}": ${err instanceof Error ? err.message : err}`
1995
2073
  );
1996
2074
  return "";
1997
2075
  }
1998
2076
  });
1999
2077
  }
2078
+ function encryptConfigSecrets(cfg, vault, _opts) {
2079
+ return walk(cfg, vault, (v) => vault.encrypt(v));
2080
+ }
2000
2081
  function walk(node, vault, transform) {
2001
2082
  if (node === null || node === void 0) return node;
2002
2083
  if (typeof node !== "object") return node;
@@ -2022,6 +2103,114 @@ function isSecretField(name) {
2022
2103
  if (NON_SECRET_OVERRIDES.has(lc)) return false;
2023
2104
  return SECRET_KEY_PATTERN.test(lc);
2024
2105
  }
2106
+ async function rewriteConfigEncrypted(configPath, vault, patch) {
2107
+ let current = {};
2108
+ try {
2109
+ const raw = await fsp.readFile(configPath, "utf8");
2110
+ current = JSON.parse(raw);
2111
+ } catch {
2112
+ }
2113
+ const merged = deepMerge(current, patch ?? {});
2114
+ const encrypted = encryptConfigSecrets(merged, vault);
2115
+ await fsp.mkdir(path11.dirname(configPath), { recursive: true });
2116
+ await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
2117
+ await restrictFilePermissions(configPath);
2118
+ }
2119
+ async function migratePlaintextSecrets(configPath, vault) {
2120
+ let raw;
2121
+ try {
2122
+ raw = await fsp.readFile(configPath, "utf8");
2123
+ } catch {
2124
+ return { migrated: 0, file: configPath };
2125
+ }
2126
+ let parsed;
2127
+ try {
2128
+ parsed = JSON.parse(raw);
2129
+ } catch {
2130
+ return { migrated: 0, file: configPath };
2131
+ }
2132
+ const counter = { n: 0 };
2133
+ const migrated = walkCount(parsed, vault, counter);
2134
+ if (counter.n === 0) return { migrated: 0, file: configPath };
2135
+ await atomicWrite(configPath, JSON.stringify(migrated, null, 2), { mode: 384 });
2136
+ await restrictFilePermissions(configPath);
2137
+ return { migrated: counter.n, file: configPath };
2138
+ }
2139
+ async function restrictFilePermissions(filePath, opts) {
2140
+ const warn = ((msg) => console.warn(msg));
2141
+ if (process.platform === "win32") {
2142
+ try {
2143
+ const { execFile: execFile2 } = await import('child_process');
2144
+ const { promisify: promisify2 } = await import('util');
2145
+ const execFileAsync = promisify2(execFile2);
2146
+ const user = windowsAccountName();
2147
+ if (!user) {
2148
+ warn(
2149
+ `[secret-vault] Could not determine the current Windows user for ${filePath}; skipping icacls hardening.`
2150
+ );
2151
+ return;
2152
+ }
2153
+ await execFileAsync("icacls", [filePath, "/inheritance:r", "/grant:r", `${user}:(F)`]);
2154
+ } catch {
2155
+ warn(
2156
+ `[secret-vault] Could not restrict permissions on ${filePath} \u2014 config file may be readable by other users on this system.`
2157
+ );
2158
+ }
2159
+ } else {
2160
+ try {
2161
+ await fsp.chmod(filePath, 384);
2162
+ } catch {
2163
+ }
2164
+ }
2165
+ }
2166
+ function windowsAccountName() {
2167
+ const username = process.env.USERNAME || process.env.USER;
2168
+ if (!username || username.includes("\0")) return void 0;
2169
+ const domain = process.env.USERDOMAIN;
2170
+ if (domain && !domain.includes("\0")) return `${domain}\\${username}`;
2171
+ return username;
2172
+ }
2173
+ function walkCount(node, vault, counter) {
2174
+ if (node === null || node === void 0) return node;
2175
+ if (typeof node !== "object") return node;
2176
+ if (Array.isArray(node)) {
2177
+ return node.map((item) => walkCount(item, vault, counter));
2178
+ }
2179
+ const out = /* @__PURE__ */ Object.create(null);
2180
+ for (const [k, v] of Object.entries(node)) {
2181
+ if (typeof v === "string" && isSecretField(k) && !vault.isEncrypted(v) && v.length > 0) {
2182
+ out[k] = vault.encrypt(v);
2183
+ counter.n++;
2184
+ } else if (typeof v === "object" && v !== null) {
2185
+ out[k] = walkCount(v, vault, counter);
2186
+ } else {
2187
+ out[k] = v;
2188
+ }
2189
+ }
2190
+ return out;
2191
+ }
2192
+ var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
2193
+ "__proto__",
2194
+ "constructor",
2195
+ "prototype",
2196
+ "__defineGetter__",
2197
+ "__defineSetter__",
2198
+ "__lookupGetter__",
2199
+ "__lookupSetter__"
2200
+ ]);
2201
+ function deepMerge(a, b) {
2202
+ const out = { ...a };
2203
+ for (const [k, v] of Object.entries(b)) {
2204
+ if (FORBIDDEN_PROTO_KEYS.has(k)) continue;
2205
+ const existing = out[k];
2206
+ if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
2207
+ out[k] = deepMerge(existing, v);
2208
+ } else {
2209
+ out[k] = v;
2210
+ }
2211
+ }
2212
+ return out;
2213
+ }
2025
2214
 
2026
2215
  // src/storage/config-loader.ts
2027
2216
  init_atomic_write();
@@ -2207,7 +2396,7 @@ var defaultIndexing = {
2207
2396
  function isPrimitiveArray(a) {
2208
2397
  return a.every((v) => v === null || typeof v !== "object");
2209
2398
  }
2210
- var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
2399
+ var FORBIDDEN_PROTO_KEYS2 = /* @__PURE__ */ new Set([
2211
2400
  "__proto__",
2212
2401
  "constructor",
2213
2402
  "prototype",
@@ -2216,12 +2405,12 @@ var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
2216
2405
  "__lookupGetter__",
2217
2406
  "__lookupSetter__"
2218
2407
  ]);
2219
- function deepMerge(base, patch) {
2408
+ function deepMerge2(base, patch) {
2220
2409
  if (typeof base !== "object" || base === null) return patch ?? base;
2221
2410
  if (typeof patch !== "object" || patch === null) return base;
2222
2411
  const out = { ...base };
2223
2412
  for (const [k, v] of Object.entries(patch)) {
2224
- if (FORBIDDEN_PROTO_KEYS.has(k)) continue;
2413
+ if (FORBIDDEN_PROTO_KEYS2.has(k)) continue;
2225
2414
  const existing = out[k];
2226
2415
  if (Array.isArray(v)) {
2227
2416
  if (Array.isArray(existing) && isPrimitiveArray(v) && isPrimitiveArray(existing)) {
@@ -2235,7 +2424,7 @@ function deepMerge(base, patch) {
2235
2424
  }
2236
2425
  }
2237
2426
  } else if (typeof v === "object" && v !== null && typeof existing === "object" && existing !== null) {
2238
- out[k] = deepMerge(existing, v);
2427
+ out[k] = deepMerge2(existing, v);
2239
2428
  } else if (v !== void 0) {
2240
2429
  out[k] = v;
2241
2430
  }
@@ -2255,12 +2444,14 @@ var DefaultConfigLoader = class {
2255
2444
  }
2256
2445
  async load(opts = {}) {
2257
2446
  let cfg = { ...BEHAVIOR_DEFAULTS };
2258
- const [global, local] = await Promise.all([
2447
+ const [global, local, inProject] = await Promise.all([
2259
2448
  this.readJson(this.paths.globalConfig),
2260
- this.readJson(this.paths.projectLocalConfig)
2449
+ this.readJson(this.paths.projectLocalConfig),
2450
+ this.readJson(this.paths.inProjectConfig)
2261
2451
  ]);
2262
- cfg = deepMerge(cfg, global);
2263
- cfg = deepMerge(cfg, local);
2452
+ cfg = deepMerge2(cfg, global);
2453
+ cfg = deepMerge2(cfg, local);
2454
+ cfg = deepMerge2(cfg, inProject);
2264
2455
  for (const [key, fn] of Object.entries(ENV_MAP)) {
2265
2456
  const v = process.env[key];
2266
2457
  if (v) fn(cfg, v);
@@ -2274,14 +2465,14 @@ var DefaultConfigLoader = class {
2274
2465
  try {
2275
2466
  const patch = await src.read();
2276
2467
  if (patch && Object.keys(patch).length > 0) {
2277
- cfg = deepMerge(cfg, patch);
2468
+ cfg = deepMerge2(cfg, patch);
2278
2469
  }
2279
2470
  } catch (err) {
2280
2471
  console.warn(`Config source "${src.name}" failed`, err);
2281
2472
  }
2282
2473
  }
2283
2474
  if (opts.cliFlags) {
2284
- cfg = deepMerge(cfg, opts.cliFlags);
2475
+ cfg = deepMerge2(cfg, opts.cliFlags);
2285
2476
  }
2286
2477
  if (this.vault) {
2287
2478
  cfg = decryptConfigSecrets(cfg, this.vault);
@@ -3693,239 +3884,12 @@ var DefaultSecretScrubber = class {
3693
3884
  }
3694
3885
  };
3695
3886
 
3696
- // src/types/secret-vault.ts
3697
- var ENCRYPTED_PREFIX = "enc:v1:";
3698
-
3699
- // src/security/secret-vault.ts
3700
- init_atomic_write();
3701
- var KEY_BYTES = 32;
3702
- var IV_BYTES = 12;
3703
- var TAG_BYTES = 16;
3704
- var ALGO = "aes-256-gcm";
3705
- var DefaultSecretVault = class {
3706
- keyFile;
3707
- key;
3708
- constructor(opts) {
3709
- this.keyFile = opts.keyFile;
3710
- }
3711
- isEncrypted(value) {
3712
- return typeof value === "string" && value.startsWith(ENCRYPTED_PREFIX);
3713
- }
3714
- encrypt(plaintext) {
3715
- if (this.isEncrypted(plaintext)) return plaintext;
3716
- const key = this.loadOrCreateKey();
3717
- const iv = randomBytes(IV_BYTES);
3718
- const cipher = createCipheriv(ALGO, key, iv);
3719
- const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
3720
- const tag = cipher.getAuthTag();
3721
- return `${ENCRYPTED_PREFIX}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
3722
- }
3723
- decrypt(value) {
3724
- if (!this.isEncrypted(value)) return value;
3725
- const rest = value.slice(ENCRYPTED_PREFIX.length);
3726
- const parts = rest.split(":");
3727
- if (parts.length !== 3) {
3728
- throw new Error("SecretVault: malformed encrypted value");
3729
- }
3730
- const [ivB64, tagB64, ctB64] = parts;
3731
- const iv = Buffer.from(ivB64, "base64");
3732
- const tag = Buffer.from(tagB64, "base64");
3733
- const ct = Buffer.from(ctB64, "base64");
3734
- if (iv.length !== IV_BYTES) throw new Error("SecretVault: bad IV length");
3735
- if (tag.length !== TAG_BYTES) throw new Error("SecretVault: bad tag length");
3736
- const key = this.loadOrCreateKey();
3737
- const decipher = createDecipheriv(ALGO, key, iv);
3738
- decipher.setAuthTag(tag);
3739
- const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
3740
- return pt.toString("utf8");
3741
- }
3742
- loadOrCreateKey() {
3743
- if (this.key) return this.key;
3744
- try {
3745
- const buf = fs5.readFileSync(this.keyFile);
3746
- if (buf.length !== KEY_BYTES) {
3747
- throw new Error(
3748
- `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`
3749
- );
3750
- }
3751
- this.key = buf;
3752
- return this.key;
3753
- } catch (err) {
3754
- if (err.code !== "ENOENT") throw err;
3755
- }
3756
- fs5.mkdirSync(path11.dirname(this.keyFile), { recursive: true });
3757
- const key = randomBytes(KEY_BYTES);
3758
- try {
3759
- fs5.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
3760
- } catch (err) {
3761
- if (err.code !== "EEXIST") throw err;
3762
- const buf = fs5.readFileSync(this.keyFile);
3763
- if (buf.length !== KEY_BYTES) {
3764
- throw new Error(
3765
- `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`
3766
- );
3767
- }
3768
- this.key = buf;
3769
- return this.key;
3770
- }
3771
- this.key = key;
3772
- return key;
3773
- }
3774
- };
3775
- function decryptConfigSecrets2(cfg, vault, opts) {
3776
- const warn = opts?.warn ?? ((msg) => console.warn(msg));
3777
- return walk2(cfg, vault, (v, key) => {
3778
- try {
3779
- return vault.decrypt(v);
3780
- } catch (err) {
3781
- warn(
3782
- `[secret-vault] Failed to decrypt "${key}": ${err instanceof Error ? err.message : err}`
3783
- );
3784
- return "";
3785
- }
3786
- });
3787
- }
3788
- function encryptConfigSecrets(cfg, vault, _opts) {
3789
- return walk2(cfg, vault, (v) => vault.encrypt(v));
3790
- }
3791
- function walk2(node, vault, transform) {
3792
- if (node === null || node === void 0) return node;
3793
- if (typeof node !== "object") return node;
3794
- if (Array.isArray(node)) {
3795
- return node.map((item) => walk2(item, vault, transform));
3796
- }
3797
- const out = /* @__PURE__ */ Object.create(null);
3798
- for (const [k, v] of Object.entries(node)) {
3799
- if (typeof v === "string" && isSecretField2(k)) {
3800
- out[k] = transform(v, k);
3801
- } else if (typeof v === "object" && v !== null) {
3802
- out[k] = walk2(v, vault, transform);
3803
- } else {
3804
- out[k] = v;
3805
- }
3806
- }
3807
- return out;
3808
- }
3809
- var SECRET_KEY_PATTERN2 = /(?:apikey|api_key|authtoken|auth_token|bearer|secret|password|passwd|pwd|refreshtoken|refresh_token|sessionkey|session_key|access[_-]?token|private[_-]?key)/i;
3810
- var NON_SECRET_OVERRIDES2 = /* @__PURE__ */ new Set(["publickey", "public_key"]);
3811
- function isSecretField2(name) {
3812
- const lc = name.toLowerCase();
3813
- if (NON_SECRET_OVERRIDES2.has(lc)) return false;
3814
- return SECRET_KEY_PATTERN2.test(lc);
3815
- }
3816
- async function rewriteConfigEncrypted(configPath, vault, patch) {
3817
- let current = {};
3818
- try {
3819
- const raw = await fsp.readFile(configPath, "utf8");
3820
- current = JSON.parse(raw);
3821
- } catch {
3822
- }
3823
- const merged = deepMerge2(current, patch ?? {});
3824
- const encrypted = encryptConfigSecrets(merged, vault);
3825
- await fsp.mkdir(path11.dirname(configPath), { recursive: true });
3826
- await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
3827
- await restrictFilePermissions(configPath);
3828
- }
3829
- async function migratePlaintextSecrets(configPath, vault) {
3830
- let raw;
3831
- try {
3832
- raw = await fsp.readFile(configPath, "utf8");
3833
- } catch {
3834
- return { migrated: 0, file: configPath };
3835
- }
3836
- let parsed;
3837
- try {
3838
- parsed = JSON.parse(raw);
3839
- } catch {
3840
- return { migrated: 0, file: configPath };
3841
- }
3842
- const counter = { n: 0 };
3843
- const migrated = walkCount(parsed, vault, counter);
3844
- if (counter.n === 0) return { migrated: 0, file: configPath };
3845
- await atomicWrite(configPath, JSON.stringify(migrated, null, 2), { mode: 384 });
3846
- await restrictFilePermissions(configPath);
3847
- return { migrated: counter.n, file: configPath };
3848
- }
3849
- async function restrictFilePermissions(filePath, opts) {
3850
- const warn = ((msg) => console.warn(msg));
3851
- if (process.platform === "win32") {
3852
- try {
3853
- const { execFile: execFile2 } = await import('child_process');
3854
- const { promisify: promisify2 } = await import('util');
3855
- const execFileAsync = promisify2(execFile2);
3856
- const user = windowsAccountName();
3857
- if (!user) {
3858
- warn(
3859
- `[secret-vault] Could not determine the current Windows user for ${filePath}; skipping icacls hardening.`
3860
- );
3861
- return;
3862
- }
3863
- await execFileAsync("icacls", [filePath, "/inheritance:r", "/grant:r", `${user}:(F)`]);
3864
- } catch {
3865
- warn(
3866
- `[secret-vault] Could not restrict permissions on ${filePath} \u2014 config file may be readable by other users on this system.`
3867
- );
3868
- }
3869
- } else {
3870
- try {
3871
- await fsp.chmod(filePath, 384);
3872
- } catch {
3873
- }
3874
- }
3875
- }
3876
- function windowsAccountName() {
3877
- const username = process.env.USERNAME || process.env.USER;
3878
- if (!username || username.includes("\0")) return void 0;
3879
- const domain = process.env.USERDOMAIN;
3880
- if (domain && !domain.includes("\0")) return `${domain}\\${username}`;
3881
- return username;
3882
- }
3883
- function walkCount(node, vault, counter) {
3884
- if (node === null || node === void 0) return node;
3885
- if (typeof node !== "object") return node;
3886
- if (Array.isArray(node)) {
3887
- return node.map((item) => walkCount(item, vault, counter));
3888
- }
3889
- const out = /* @__PURE__ */ Object.create(null);
3890
- for (const [k, v] of Object.entries(node)) {
3891
- if (typeof v === "string" && isSecretField2(k) && !vault.isEncrypted(v) && v.length > 0) {
3892
- out[k] = vault.encrypt(v);
3893
- counter.n++;
3894
- } else if (typeof v === "object" && v !== null) {
3895
- out[k] = walkCount(v, vault, counter);
3896
- } else {
3897
- out[k] = v;
3898
- }
3899
- }
3900
- return out;
3901
- }
3902
- var FORBIDDEN_PROTO_KEYS2 = /* @__PURE__ */ new Set([
3903
- "__proto__",
3904
- "constructor",
3905
- "prototype",
3906
- "__defineGetter__",
3907
- "__defineSetter__",
3908
- "__lookupGetter__",
3909
- "__lookupSetter__"
3910
- ]);
3911
- function deepMerge2(a, b) {
3912
- const out = { ...a };
3913
- for (const [k, v] of Object.entries(b)) {
3914
- if (FORBIDDEN_PROTO_KEYS2.has(k)) continue;
3915
- const existing = out[k];
3916
- if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
3917
- out[k] = deepMerge2(existing, v);
3918
- } else {
3919
- out[k] = v;
3920
- }
3921
- }
3922
- return out;
3923
- }
3924
-
3925
3887
  // src/security/capabilities.ts
3926
3888
  var ToolCapabilities = {
3927
3889
  /** Can execute arbitrary commands in the user's shell (the `bash` tool). */
3928
3890
  SHELL_ARBITRARY: "shell.arbitrary",
3891
+ /** Can execute a restricted set of commands (the `exec` tool). */
3892
+ SHELL_RESTRICTED: "shell.restricted",
3929
3893
  /** Can write / modify / delete files inside the project. */
3930
3894
  FS_WRITE: "fs.write",
3931
3895
  /** Can write files outside the current project root (very high risk). */
@@ -3941,6 +3905,7 @@ var ToolCapabilities = {
3941
3905
  };
3942
3906
  var DANGEROUS_FOR_SUBAGENTS = [
3943
3907
  ToolCapabilities.SHELL_ARBITRARY,
3908
+ ToolCapabilities.SHELL_RESTRICTED,
3944
3909
  ToolCapabilities.FS_WRITE,
3945
3910
  ToolCapabilities.FS_WRITE_OUTSIDE_PROJECT,
3946
3911
  ToolCapabilities.MCP_PROXY,
@@ -4384,38 +4349,15 @@ var DefaultPermissionPolicy = class {
4384
4349
  }
4385
4350
  };
4386
4351
  var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
4387
- /**
4388
- * Legacy name-based denylist.
4389
- * @deprecated Prefer declaring `capabilities` on the Tool and using capability-based checks.
4390
- */
4391
- static LEGACY_NAME_DENY = /* @__PURE__ */ new Set([
4392
- "bash",
4393
- "write",
4394
- "edit",
4395
- "replace",
4396
- "scaffold",
4397
- "patch",
4398
- "install",
4399
- "exec"
4400
- ]);
4401
- // Note: hasDangerousCapabilityForSubagents is now the shared helper from capabilities.ts
4402
- // The old private method was removed in favor of the centralized utility.
4403
- /**
4404
- * Tools from MCP servers (`mcp__<server>__<tool>`) are external code of
4405
- * unknown capability — they may wrap a shell or filesystem. They are
4406
- * fail-closed here: not auto-approved for subagents by default, so the
4407
- * leader must allow them explicitly per-spawn.
4408
- */
4409
4352
  static isMcpTool(name) {
4410
4353
  return name.startsWith("mcp__");
4411
4354
  }
4412
4355
  async evaluate(tool) {
4413
4356
  const hasDangerousCap = hasDangerousCapabilityForSubagents(tool);
4414
- const legacyNameBlock = _AutoApprovePermissionPolicy.LEGACY_NAME_DENY.has(tool.name);
4415
4357
  const isMcp = _AutoApprovePermissionPolicy.isMcpTool(tool.name);
4416
- const blocked = tool.permission === "deny" || hasDangerousCap || legacyNameBlock || isMcp;
4358
+ const blocked = tool.permission === "deny" || hasDangerousCap || isMcp;
4417
4359
  if (blocked) {
4418
- const reason = hasDangerousCap ? `tool declares dangerous capability (${tool.capabilities?.join(", ")}) \u2014 not auto-approved for subagents` : legacyNameBlock || isMcp ? `tool ${tool.name} is not auto-approved for subagents \u2014 ask the leader to allow it explicitly` : "tool default deny";
4360
+ const reason = hasDangerousCap ? `tool declares dangerous capability (${tool.capabilities?.join(", ")}) \u2014 not auto-approved for subagents` : isMcp ? `MCP tool ${tool.name} is not auto-approved for subagents \u2014 ask the leader to allow it explicitly` : "tool default deny";
4419
4361
  return {
4420
4362
  permission: "deny",
4421
4363
  source: "subagent_guard",
@@ -4612,6 +4554,7 @@ var DefaultRetryPolicy = class {
4612
4554
  if (err instanceof ProviderError) {
4613
4555
  if (err.status === 429) return 5;
4614
4556
  if (err.status === 529) return 3;
4557
+ if (err.status === 599) return 5;
4615
4558
  if (err.status >= 500) return 3;
4616
4559
  return 0;
4617
4560
  }
@@ -5083,53 +5026,63 @@ async function streamProviderToResponse(provider, req, signal, ctx, events) {
5083
5026
  const next = await iter.next();
5084
5027
  if (next.done) break;
5085
5028
  const ev = next.value;
5086
- switch (ev.type) {
5087
- case "message_start":
5088
- handleMessageStart(state, ev.model);
5089
- break;
5090
- case "content_block_start":
5091
- handleContentBlockStart(state, ev);
5092
- break;
5093
- case "content_block_stop":
5094
- handleContentBlockStop(state, ev);
5095
- break;
5096
- case "text_delta":
5097
- handleTextDelta(state, ev.text);
5098
- events.emit("provider.text_delta", { ctx, text: ev.text });
5099
- break;
5100
- case "tool_use_start": {
5101
- const idVal = ev.id;
5102
- const nameVal = ev.name;
5103
- handleToolUseStart(state, { id: idVal, name: nameVal });
5104
- const emittedPayload = { ctx, id: idVal ?? "unknown", name: nameVal ?? "unknown" };
5105
- events.emit("provider.tool_use_start", emittedPayload);
5106
- break;
5107
- }
5108
- case "tool_use_input_delta":
5109
- handleToolUseInputDelta(state, ev);
5110
- break;
5111
- case "tool_use_stop": {
5112
- const stoppedName = state.tools.get(ev.id)?.name ?? "unknown";
5113
- handleToolUseStop(state, ev);
5114
- events.emit("provider.tool_use_stop", { ctx, id: ev.id, name: stoppedName });
5115
- break;
5029
+ try {
5030
+ switch (ev.type) {
5031
+ case "message_start":
5032
+ handleMessageStart(state, ev.model);
5033
+ break;
5034
+ case "content_block_start":
5035
+ handleContentBlockStart(state, ev);
5036
+ break;
5037
+ case "content_block_stop":
5038
+ handleContentBlockStop(state, ev);
5039
+ break;
5040
+ case "text_delta":
5041
+ handleTextDelta(state, ev.text);
5042
+ events.emit("provider.text_delta", { ctx, text: ev.text });
5043
+ break;
5044
+ case "tool_use_start": {
5045
+ const idVal = ev.id;
5046
+ const nameVal = ev.name;
5047
+ handleToolUseStart(state, { id: idVal, name: nameVal });
5048
+ const emittedPayload = { ctx, id: idVal ?? "unknown", name: nameVal ?? "unknown" };
5049
+ events.emit("provider.tool_use_start", emittedPayload);
5050
+ break;
5051
+ }
5052
+ case "tool_use_input_delta":
5053
+ handleToolUseInputDelta(state, ev);
5054
+ break;
5055
+ case "tool_use_stop": {
5056
+ const stoppedName = state.tools.get(ev.id)?.name ?? "unknown";
5057
+ handleToolUseStop(state, ev);
5058
+ events.emit("provider.tool_use_stop", { ctx, id: ev.id, name: stoppedName });
5059
+ break;
5060
+ }
5061
+ case "thinking_start":
5062
+ handleThinkingStart(state, ev);
5063
+ break;
5064
+ case "thinking_delta":
5065
+ handleThinkingDelta(state, ev.text);
5066
+ events.emit("provider.thinking_delta", { ctx, text: ev.text });
5067
+ break;
5068
+ case "thinking_signature":
5069
+ handleThinkingSignature(state, ev.signature);
5070
+ break;
5071
+ case "thinking_stop":
5072
+ handleThinkingStop(state);
5073
+ break;
5074
+ case "message_stop":
5075
+ handleMessageStop(state, ev);
5076
+ break;
5077
+ default:
5078
+ break;
5116
5079
  }
5117
- case "thinking_start":
5118
- handleThinkingStart(state, ev);
5119
- break;
5120
- case "thinking_delta":
5121
- handleThinkingDelta(state, ev.text);
5122
- events.emit("provider.thinking_delta", { ctx, text: ev.text });
5123
- break;
5124
- case "thinking_signature":
5125
- handleThinkingSignature(state, ev.signature);
5126
- break;
5127
- case "thinking_stop":
5128
- handleThinkingStop(state);
5129
- break;
5130
- case "message_stop":
5131
- handleMessageStop(state, ev);
5132
- break;
5080
+ } catch (handlerErr) {
5081
+ events.emit("provider.stream_error", {
5082
+ ctx,
5083
+ eventType: ev.type,
5084
+ msg: handlerErr instanceof Error ? handlerErr.message : String(handlerErr)
5085
+ });
5133
5086
  }
5134
5087
  }
5135
5088
  } catch (err) {
@@ -5142,8 +5095,10 @@ async function streamProviderToResponse(provider, req, signal, ctx, events) {
5142
5095
  try {
5143
5096
  let drainTimer = null;
5144
5097
  try {
5098
+ const drainPromise = Promise.resolve(iter.return?.()).catch(() => {
5099
+ });
5145
5100
  await Promise.race([
5146
- Promise.resolve(iter.return?.()),
5101
+ drainPromise,
5147
5102
  new Promise((resolve5) => {
5148
5103
  drainTimer = setTimeout(resolve5, STREAM_DRAIN_TIMEOUT_MS);
5149
5104
  })
@@ -5241,6 +5196,17 @@ var DefaultProviderRunner = class {
5241
5196
 
5242
5197
  // src/utils/token-estimate.ts
5243
5198
  var RoughTokenEstimate = (text, charsPerToken = 3.5) => Math.max(1, Math.ceil(text.length / charsPerToken));
5199
+ var CALIBRATION_GLOBAL_KEY = "__global__";
5200
+ var _cals = /* @__PURE__ */ new Map();
5201
+ function calState(key) {
5202
+ let state = _cals.get(key);
5203
+ if (!state) {
5204
+ state = { ratio: 1, count: 0, prevEst: 0 };
5205
+ _cals.set(key, state);
5206
+ }
5207
+ return state;
5208
+ }
5209
+ var MIN_SAMPLES_FOR_CALIBRATION = 3;
5244
5210
  var ESTIMATE_CACHE = /* @__PURE__ */ new Map();
5245
5211
  var ESTIMATE_CACHE_MAX_SIZE = 1e4;
5246
5212
  function getCachedEstimate(key, compute) {
@@ -5272,10 +5238,25 @@ function estimateToolResultTokens(content) {
5272
5238
  function estimateTextTokens(text) {
5273
5239
  return RoughTokenEstimate(text);
5274
5240
  }
5241
+ function estimateMessageTokens(messages) {
5242
+ let total = 0;
5243
+ for (const m of messages) {
5244
+ if (typeof m.content === "string") {
5245
+ total += estimateTextTokens(m.content);
5246
+ } else {
5247
+ for (const b of m.content) {
5248
+ if (b.type === "text") total += estimateTextTokens(b.text);
5249
+ else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
5250
+ else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
5251
+ }
5252
+ }
5253
+ }
5254
+ return total;
5255
+ }
5275
5256
  function estimateToolDefTokens(tool) {
5276
5257
  return RoughTokenEstimate(tool.name) + RoughTokenEstimate(tool.description ?? "") + RoughTokenEstimate(JSON.stringify(tool.inputSchema));
5277
5258
  }
5278
- function estimateRequestTokens(messages, systemPrompt, tools) {
5259
+ function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
5279
5260
  let messagesTokens = 0;
5280
5261
  if (typeof messages === "string") {
5281
5262
  messagesTokens = RoughTokenEstimate(messages);
@@ -5314,6 +5295,7 @@ function estimateRequestTokens(messages, systemPrompt, tools) {
5314
5295
  toolsTokens += estimateToolDefTokens(t);
5315
5296
  }
5316
5297
  const total = messagesTokens + systemTokens + toolsTokens;
5298
+ calState(calibrationKey).prevEst = total;
5317
5299
  return {
5318
5300
  messages: messagesTokens,
5319
5301
  systemPrompt: systemTokens,
@@ -5321,39 +5303,164 @@ function estimateRequestTokens(messages, systemPrompt, tools) {
5321
5303
  total
5322
5304
  };
5323
5305
  }
5324
- function estimateRequestTokensCalibrated(messages, systemPrompt, tools) {
5325
- const result = estimateRequestTokens(messages, systemPrompt, tools);
5306
+ function estimateRequestTokensCalibrated(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
5307
+ const result = estimateRequestTokens(messages, systemPrompt, tools, calibrationKey);
5308
+ const cal = calState(calibrationKey);
5309
+ if (cal.count >= MIN_SAMPLES_FOR_CALIBRATION) {
5310
+ const safeRatio = Math.min(1.5, Math.max(0.5, cal.ratio));
5311
+ return {
5312
+ messages: Math.round(result.messages * safeRatio),
5313
+ systemPrompt: Math.round(result.systemPrompt * safeRatio),
5314
+ tools: Math.round(result.tools * safeRatio),
5315
+ total: Math.round(result.total * safeRatio)
5316
+ };
5317
+ }
5326
5318
  return result;
5327
5319
  }
5328
5320
 
5321
+ // src/types/blocks.ts
5322
+ function isTextBlock(b) {
5323
+ return b.type === "text";
5324
+ }
5325
+
5326
+ // src/execution/compaction-core.ts
5327
+ var estimateMessages = estimateMessageTokens;
5328
+ function hasTextContent(m) {
5329
+ if (typeof m.content === "string") return m.content.trim().length > 0;
5330
+ return m.content.some((b) => b.type === "text" && b.text.trim().length > 0);
5331
+ }
5332
+ function findPreserveStart(messages, preserveK) {
5333
+ let pairCount = 0;
5334
+ let preserveStart = messages.length;
5335
+ for (let i = messages.length - 1; i >= 0 && pairCount < preserveK; i--) {
5336
+ const m = messages[i];
5337
+ if (!m) continue;
5338
+ if (m.role === "user" || m.role === "assistant") {
5339
+ pairCount++;
5340
+ preserveStart = i;
5341
+ }
5342
+ }
5343
+ for (let i = preserveStart; i < messages.length; i++) {
5344
+ const m = messages[i];
5345
+ if (!m || typeof m.content === "string" || !Array.isArray(m.content)) continue;
5346
+ const hasToolUse2 = m.content.some((b) => b.type === "tool_use");
5347
+ if (hasToolUse2 && i + 1 < messages.length) {
5348
+ const next = messages[i + 1];
5349
+ if (next && next.role === "user" && typeof next.content !== "string" && Array.isArray(next.content) && next.content.some((b) => b.type === "tool_result")) {
5350
+ preserveStart = i + 1;
5351
+ }
5352
+ }
5353
+ }
5354
+ return preserveStart;
5355
+ }
5356
+ function eliseOldToolResults(messages, opts) {
5357
+ const preserveStart = findPreserveStart(messages, opts.preserveK);
5358
+ let saved = 0;
5359
+ let changed = false;
5360
+ const next = new Array(messages.length);
5361
+ for (let i = 0; i < messages.length; i++) {
5362
+ const msg = messages[i];
5363
+ if (i >= preserveStart || !msg || !Array.isArray(msg.content)) {
5364
+ next[i] = msg;
5365
+ continue;
5366
+ }
5367
+ const original = msg.content;
5368
+ const newContent = original.map((b) => {
5369
+ if (b.type !== "tool_result") return b;
5370
+ const tokens = estimateToolResultTokens(b.content);
5371
+ if (tokens < opts.eliseThreshold) return b;
5372
+ saved += tokens;
5373
+ const elided = {
5374
+ type: "tool_result",
5375
+ tool_use_id: b.tool_use_id,
5376
+ content: `[elided: ~${tokens} tokens]`,
5377
+ is_error: b.is_error
5378
+ };
5379
+ return elided;
5380
+ });
5381
+ if (newContent.every((b, idx) => b === original[idx])) {
5382
+ next[i] = msg;
5383
+ } else {
5384
+ next[i] = { ...msg, content: newContent };
5385
+ changed = true;
5386
+ }
5387
+ }
5388
+ return { messages: changed ? next : messages, saved, changed };
5389
+ }
5390
+ function buildLosslessDigest(messages) {
5391
+ const lines = [];
5392
+ for (const m of messages) {
5393
+ let text;
5394
+ let omitted = 0;
5395
+ if (typeof m.content === "string") {
5396
+ text = m.content;
5397
+ } else {
5398
+ const parts = [];
5399
+ for (const b of m.content) {
5400
+ if (isTextBlock(b)) parts.push(b.text);
5401
+ else if (b.type === "tool_use" || b.type === "tool_result") omitted++;
5402
+ }
5403
+ text = parts.join(" ");
5404
+ }
5405
+ if (text.trim().length === 0 && omitted === 0) continue;
5406
+ const marker = omitted > 0 ? ` [${omitted} tool call(s) omitted \u2014 see session log]` : "";
5407
+ lines.push(`[${m.role}]: ${text}${marker}`);
5408
+ }
5409
+ return lines.join("\n");
5410
+ }
5411
+ function findSafeBoundary(messages, from, to) {
5412
+ for (let i = to; i >= from; i--) {
5413
+ const m = messages[i];
5414
+ if (!m) continue;
5415
+ if (m.role === "user" && hasTextContent(m)) {
5416
+ return findExchangeStart(messages, i);
5417
+ }
5418
+ }
5419
+ return -1;
5420
+ }
5421
+ function findExchangeStart(messages, userIndex) {
5422
+ for (let i = userIndex - 1; i >= 0; i--) {
5423
+ const m = messages[i];
5424
+ if (!m) continue;
5425
+ if (m.role === "assistant") {
5426
+ const hasToolUse2 = Array.isArray(m.content) ? m.content.some((b) => b.type === "tool_use") : false;
5427
+ if (!hasToolUse2) return i + 1;
5428
+ } else if (m.role === "user") {
5429
+ return i;
5430
+ }
5431
+ }
5432
+ return 0;
5433
+ }
5434
+
5329
5435
  // src/execution/compactor.ts
5330
5436
  var HybridCompactor = class {
5331
5437
  preserveK;
5332
5438
  eliseThreshold;
5333
- estimator;
5334
5439
  constructor(opts = {}) {
5335
5440
  this.preserveK = opts.preserveK ?? 5;
5336
5441
  this.eliseThreshold = opts.eliseThreshold ?? 2e3;
5337
- this.estimator = opts.estimator ?? estimateTextTokens;
5338
5442
  }
5339
5443
  async compact(ctx, opts = {}) {
5340
- const beforeTokens = this.estimateMessages(ctx.messages);
5444
+ const beforeTokens = estimateMessages(ctx.messages);
5341
5445
  const beforeFull = this.estimateFullRequest(ctx);
5342
5446
  const reductions = [];
5343
5447
  const policy = readContextWindowPolicy(ctx);
5344
5448
  const preserveK = policy?.preserveK ?? this.preserveK;
5345
5449
  const eliseThreshold = policy?.eliseThreshold ?? this.eliseThreshold;
5346
- const phase1Saved = this.eliseOldToolResults(ctx, preserveK, eliseThreshold);
5347
- if (phase1Saved > 0) reductions.push({ phase: "elision", saved: phase1Saved });
5450
+ const elide = eliseOldToolResults(ctx.messages, { preserveK, eliseThreshold });
5451
+ if (elide.changed) ctx.state.replaceMessages(elide.messages);
5452
+ if (elide.saved > 0) reductions.push({ phase: "elision", saved: elide.saved });
5453
+ let collapsedDigest;
5348
5454
  if (opts.aggressive) {
5349
- const phase2Saved = this.collapseAncientTurns(ctx, preserveK);
5350
- if (phase2Saved > 0) reductions.push({ phase: "summary", saved: phase2Saved });
5455
+ const phase2 = this.collapseAncientTurns(ctx, preserveK);
5456
+ if (phase2.saved > 0) reductions.push({ phase: "summary", saved: phase2.saved });
5457
+ collapsedDigest = phase2.digest;
5351
5458
  }
5352
5459
  const repaired = repairToolUseAdjacency(ctx.messages);
5353
5460
  if (repaired.report.changed) {
5354
5461
  ctx.state.replaceMessages(repaired.messages);
5355
5462
  }
5356
- const afterTokens = this.estimateMessages(ctx.messages);
5463
+ const afterTokens = estimateMessages(ctx.messages);
5357
5464
  const afterFull = this.estimateFullRequest(ctx);
5358
5465
  return {
5359
5466
  before: beforeTokens,
@@ -5361,6 +5468,7 @@ var HybridCompactor = class {
5361
5468
  fullRequestTokensBefore: beforeFull,
5362
5469
  fullRequestTokensAfter: afterFull,
5363
5470
  reductions,
5471
+ collapsedDigest,
5364
5472
  repaired: repaired.report.changed ? {
5365
5473
  removedToolUses: repaired.report.removedToolUses,
5366
5474
  removedToolResults: repaired.report.removedToolResults,
@@ -5373,72 +5481,23 @@ var HybridCompactor = class {
5373
5481
  * This is the accurate figure for context-window pressure monitoring.
5374
5482
  */
5375
5483
  estimateFullRequest(ctx) {
5376
- const breakdown = estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []);
5377
- return breakdown.total;
5378
- }
5379
- eliseOldToolResults(ctx, preserveK = this.preserveK, eliseThreshold = this.eliseThreshold) {
5380
- const messages = ctx.messages;
5381
- let pairCount = 0;
5382
- let preserveStart = messages.length;
5383
- for (let i = messages.length - 1; i >= 0 && pairCount < preserveK; i--) {
5384
- const m = messages[i];
5385
- if (!m) continue;
5386
- if (m.role === "user" || m.role === "assistant") {
5387
- pairCount++;
5388
- preserveStart = i;
5389
- }
5390
- }
5391
- for (let i = preserveStart; i < messages.length; i++) {
5392
- const m = messages[i];
5393
- if (!m || typeof m.content === "string" || !Array.isArray(m.content)) continue;
5394
- const hasToolUse2 = m.content.some((b) => b.type === "tool_use");
5395
- if (hasToolUse2 && i + 1 < messages.length) {
5396
- const next = messages[i + 1];
5397
- if (next && next.role === "user" && typeof next.content !== "string" && Array.isArray(next.content) && next.content.some((b) => b.type === "tool_result")) {
5398
- preserveStart = i + 1;
5399
- }
5400
- }
5401
- }
5402
- let saved = 0;
5403
- let changed = false;
5404
- const nextMessages = new Array(messages.length);
5405
- for (let i = 0; i < messages.length; i++) {
5406
- const msg = messages[i];
5407
- if (i >= preserveStart) {
5408
- nextMessages[i] = msg;
5409
- continue;
5410
- }
5411
- if (!msg || !Array.isArray(msg.content)) {
5412
- nextMessages[i] = msg;
5413
- continue;
5414
- }
5415
- const newContent = msg.content.map((b) => {
5416
- if (b.type !== "tool_result") return b;
5417
- const tokens = estimateToolResultTokens(b.content);
5418
- if (tokens < eliseThreshold) return b;
5419
- saved += tokens;
5420
- const elided = {
5421
- type: "tool_result",
5422
- tool_use_id: b.tool_use_id,
5423
- content: `[elided: ~${tokens} tokens]`,
5424
- is_error: b.is_error
5425
- };
5426
- return elided;
5427
- });
5428
- if (newContent.length === msg.content.length && newContent.every((b, idx) => b === msg.content[idx])) {
5429
- nextMessages[i] = msg;
5430
- } else {
5431
- nextMessages[i] = { ...msg, content: newContent };
5432
- changed = true;
5433
- }
5434
- }
5435
- if (changed) ctx.state.replaceMessages(nextMessages);
5436
- return saved;
5484
+ return estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []).total;
5437
5485
  }
5486
+ /**
5487
+ * Lossless rule-based collapse of ancient turns into a single digest message.
5488
+ *
5489
+ * Preserves ALL textual content of the collapsed range — user instructions,
5490
+ * assistant decisions/conclusions, and any prior digests (chained forward so
5491
+ * the digest stays lossless across repeated compactions). Only `tool_use` /
5492
+ * `tool_result` protocol blocks are dropped and replaced with a count marker;
5493
+ * their full payload already lives in the session log. No sub-LLM call.
5494
+ *
5495
+ * Returns the token savings and the digest text (for audit logging).
5496
+ */
5438
5497
  collapseAncientTurns(ctx, preserveK = this.preserveK) {
5439
5498
  const messages = ctx.messages;
5440
5499
  const cutTarget = Math.max(0, messages.length - preserveK * 2);
5441
- if (cutTarget <= 0) return 0;
5500
+ if (cutTarget <= 0) return { saved: 0 };
5442
5501
  let boundary = -1;
5443
5502
  for (let i = cutTarget; i < messages.length; i++) {
5444
5503
  const m = messages[i];
@@ -5448,34 +5507,20 @@ var HybridCompactor = class {
5448
5507
  break;
5449
5508
  }
5450
5509
  }
5451
- if (boundary <= 0) return 0;
5510
+ if (boundary <= 0) return { saved: 0 };
5452
5511
  const removed = messages.slice(0, boundary);
5453
- const removedTokens = this.estimateMessages(removed);
5454
- const summary = [
5455
- {
5456
- role: "user",
5457
- content: `[previous_session_summary: ${removed.length} earlier turns compacted. Todo state preserved in context.]`
5458
- },
5459
- { role: "assistant", content: "Continuing from compacted context." }
5460
- ];
5512
+ const removedTokens = estimateMessages(removed);
5513
+ const digest = buildLosslessDigest(removed) || `${removed.length} earlier turns (no textual content; tool I/O omitted \u2014 see session log)`;
5514
+ const summaryMsg = {
5515
+ role: "system",
5516
+ content: `[prior_turns_digest: ${digest}]`
5517
+ };
5461
5518
  const tail = ctx.messages.slice(boundary);
5462
- ctx.state.replaceMessages([...summary, ...tail]);
5463
- return Math.max(0, removedTokens - this.estimateMessages(summary));
5464
- }
5465
- estimateMessages(messages) {
5466
- let total = 0;
5467
- for (const m of messages) {
5468
- if (typeof m.content === "string") {
5469
- total += this.estimator(m.content);
5470
- } else {
5471
- for (const b of m.content) {
5472
- if (b.type === "text") total += this.estimator(b.text);
5473
- else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
5474
- else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
5475
- }
5476
- }
5477
- }
5478
- return total;
5519
+ ctx.state.replaceMessages([summaryMsg, ...tail]);
5520
+ return {
5521
+ saved: Math.max(0, removedTokens - estimateMessages([summaryMsg])),
5522
+ digest
5523
+ };
5479
5524
  }
5480
5525
  };
5481
5526
  function readContextWindowPolicy(ctx) {
@@ -5487,15 +5532,6 @@ function readContextWindowPolicy(ctx) {
5487
5532
  }
5488
5533
  return candidate;
5489
5534
  }
5490
- function hasTextContent(m) {
5491
- if (typeof m.content === "string") return m.content.trim().length > 0;
5492
- return m.content.some((b) => b.type === "text" && b.text.trim().length > 0);
5493
- }
5494
-
5495
- // src/types/blocks.ts
5496
- function isTextBlock(b) {
5497
- return b.type === "text";
5498
- }
5499
5535
 
5500
5536
  // src/execution/intelligent-compactor.ts
5501
5537
  var IntelligentCompactor = class {
@@ -5520,23 +5556,25 @@ var IntelligentCompactor = class {
5520
5556
  this.summarizerModel = opts.summarizerModel;
5521
5557
  }
5522
5558
  async compact(ctx, opts = {}) {
5523
- const beforeTokens = this.estimateTokens(ctx.messages);
5559
+ const beforeTokens = estimateMessages(ctx.messages);
5524
5560
  const beforeFull = this.estimateFullRequest(ctx);
5525
5561
  const reductions = [];
5526
5562
  const load = beforeFull / this.maxContext;
5527
5563
  const aggressive = load >= this.hardThreshold ? true : opts.aggressive ?? load >= this.softThreshold;
5528
- const saved1 = this.eliseOldToolResults(ctx);
5564
+ const saved1 = this.elide(ctx);
5529
5565
  if (saved1 > 0) reductions.push({ phase: "elision", saved: saved1 });
5566
+ let collapsedDigest;
5530
5567
  if (aggressive) {
5531
- const saved2 = await this.summarizeAncientTurns(ctx);
5532
- if (saved2 > 0) reductions.push({ phase: "summary", saved: saved2 });
5568
+ const phase2 = await this.summarizeAncientTurns(ctx);
5569
+ if (phase2.saved > 0) reductions.push({ phase: "summary", saved: phase2.saved });
5570
+ collapsedDigest = phase2.digest;
5533
5571
  } else if (load >= this.warnThreshold) {
5534
- const saved2 = this.lightweightCompact(ctx);
5572
+ const saved2 = this.elide(ctx);
5535
5573
  if (saved2 > 0) reductions.push({ phase: "elision", saved: saved2 });
5536
5574
  }
5537
5575
  const repaired = repairToolUseAdjacency(ctx.messages);
5538
5576
  if (repaired.report.changed) ctx.state.replaceMessages(repaired.messages);
5539
- const afterTokens = this.estimateTokens(ctx.messages);
5577
+ const afterTokens = estimateMessages(ctx.messages);
5540
5578
  const afterFull = this.estimateFullRequest(ctx);
5541
5579
  return {
5542
5580
  before: beforeTokens,
@@ -5544,6 +5582,7 @@ var IntelligentCompactor = class {
5544
5582
  fullRequestTokensBefore: beforeFull,
5545
5583
  fullRequestTokensAfter: afterFull,
5546
5584
  reductions,
5585
+ collapsedDigest,
5547
5586
  repaired: repaired.report.changed ? {
5548
5587
  removedToolUses: repaired.report.removedToolUses,
5549
5588
  removedToolResults: repaired.report.removedToolResults,
@@ -5556,75 +5595,39 @@ var IntelligentCompactor = class {
5556
5595
  * This is the accurate figure for context-window pressure monitoring.
5557
5596
  */
5558
5597
  estimateFullRequest(ctx) {
5559
- const breakdown = estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []);
5560
- return breakdown.total;
5598
+ return estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []).total;
5599
+ }
5600
+ /** Run shared tool-result elision and commit through ConversationState. */
5601
+ elide(ctx) {
5602
+ const result = eliseOldToolResults(ctx.messages, {
5603
+ preserveK: this.preserveK,
5604
+ eliseThreshold: this.eliseThreshold
5605
+ });
5606
+ if (result.changed) ctx.state.replaceMessages(result.messages);
5607
+ return result.saved;
5561
5608
  }
5562
5609
  async summarizeAncientTurns(ctx) {
5563
5610
  const messages = ctx.messages;
5564
5611
  const cutoff = Math.max(0, messages.length - this.preserveK * 2);
5565
- if (cutoff <= 2) return 0;
5566
- const boundary = this.findSafeBoundary(messages, 0, cutoff);
5567
- if (boundary <= 1) return 0;
5612
+ if (cutoff <= 2) return { saved: 0 };
5613
+ const boundary = findSafeBoundary(messages, 0, cutoff);
5614
+ if (boundary <= 1) return { saved: 0 };
5568
5615
  const toSummarize = messages.slice(0, boundary);
5569
- const removedTokens = this.estimateTokens(toSummarize);
5616
+ const removedTokens = estimateMessages(toSummarize);
5570
5617
  let summaryText;
5571
5618
  try {
5572
5619
  summaryText = await this.callSummarizer(toSummarize, ctx);
5573
5620
  } catch {
5574
- const preservedMessages = [];
5575
- for (const m of toSummarize) {
5576
- if (m.role === "system") continue;
5577
- if (typeof m.content === "string") {
5578
- preservedMessages.push(m);
5579
- } else if (Array.isArray(m.content)) {
5580
- const textParts = m.content.filter(isTextBlock);
5581
- const toolParts = m.content.filter((b) => b.type === "tool_use" || b.type === "tool_result");
5582
- const next = {
5583
- role: m.role,
5584
- content: [
5585
- ...textParts,
5586
- ...toolParts.length > 0 ? [{ type: "text", text: `[${toolParts.length} tool call(s) omitted]` }] : []
5587
- ]
5588
- };
5589
- preservedMessages.push(next);
5590
- }
5591
- }
5592
- summaryText = preservedMessages.map((m) => `[${m.role}]: ${typeof m.content === "string" ? m.content : m.content.filter(isTextBlock).map((b) => b.text).join(" ")}`).join("\n");
5593
- if (!summaryText) summaryText = `${toSummarize.length} earlier turns (semantic content preserved)`;
5594
- }
5595
- const summaryMsg = {
5596
- role: "system",
5597
- content: `[prior_turns_summary: ${summaryText}]`
5598
- };
5599
- const summaryTokens = this.estimateTokens([summaryMsg]);
5600
- const tail = ctx.messages.slice(boundary);
5601
- ctx.state.replaceMessages([summaryMsg, ...tail]);
5602
- return Math.max(0, removedTokens - summaryTokens);
5603
- }
5604
- findSafeBoundary(messages, from, to) {
5605
- for (let i = to; i >= from; i--) {
5606
- const m = messages[i];
5607
- if (!m) continue;
5608
- if (m.role === "user" && this.hasTextContent(m)) {
5609
- return this.findExchangeStart(messages, i);
5610
- }
5611
- }
5612
- return -1;
5613
- }
5614
- findExchangeStart(messages, userIndex) {
5615
- for (let i = userIndex - 1; i >= 0; i--) {
5616
- const m = messages[i];
5617
- if (!m) continue;
5618
- if (m.role === "assistant") {
5619
- const hasToolUse2 = Array.isArray(m.content) ? m.content.some((b) => b.type === "tool_use") : false;
5620
- if (!hasToolUse2) {
5621
- return i + 1;
5622
- }
5623
- } else if (m.role !== "user") ; else {
5624
- return i;
5625
- }
5621
+ summaryText = buildLosslessDigest(toSummarize) || `${toSummarize.length} earlier turns (semantic content preserved)`;
5626
5622
  }
5627
- return 0;
5623
+ const summaryMsg = {
5624
+ role: "system",
5625
+ content: `[prior_turns_summary: ${summaryText}]`
5626
+ };
5627
+ const summaryTokens = estimateMessages([summaryMsg]);
5628
+ const tail = ctx.messages.slice(boundary);
5629
+ ctx.state.replaceMessages([summaryMsg, ...tail]);
5630
+ return { saved: Math.max(0, removedTokens - summaryTokens), digest: summaryText };
5628
5631
  }
5629
5632
  async callSummarizer(messages, ctx) {
5630
5633
  const prompt = [
@@ -5659,86 +5662,6 @@ var IntelligentCompactor = class {
5659
5662
  }
5660
5663
  return [{ type: "text", text: lines.join("\n") }];
5661
5664
  }
5662
- lightweightCompact(ctx) {
5663
- return this.eliseOldToolResults(ctx);
5664
- }
5665
- eliseOldToolResults(ctx) {
5666
- const messages = ctx.messages;
5667
- let pairCount = 0;
5668
- let preserveStart = messages.length;
5669
- for (let i = messages.length - 1; i >= 0 && pairCount < this.preserveK; i--) {
5670
- const m = messages[i];
5671
- if (!m) continue;
5672
- if (m.role === "user" || m.role === "assistant") {
5673
- pairCount++;
5674
- preserveStart = i;
5675
- }
5676
- }
5677
- for (let i = preserveStart; i < messages.length; i++) {
5678
- const m = messages[i];
5679
- if (!m || typeof m.content === "string" || !Array.isArray(m.content)) continue;
5680
- const hasToolUse2 = m.content.some((b) => b.type === "tool_use");
5681
- if (hasToolUse2 && i + 1 < messages.length) {
5682
- const next = messages[i + 1];
5683
- if (next && next.role === "user" && typeof next.content !== "string" && Array.isArray(next.content) && next.content.some((b) => b.type === "tool_result")) {
5684
- preserveStart = i + 1;
5685
- }
5686
- }
5687
- }
5688
- let saved = 0;
5689
- let changed = false;
5690
- const nextMessages = new Array(messages.length);
5691
- for (let i = 0; i < messages.length; i++) {
5692
- const msg = messages[i];
5693
- if (i >= preserveStart) {
5694
- nextMessages[i] = msg;
5695
- continue;
5696
- }
5697
- if (!msg || !Array.isArray(msg.content)) {
5698
- nextMessages[i] = msg;
5699
- continue;
5700
- }
5701
- const newContent = msg.content.map((b) => {
5702
- if (b.type !== "tool_result") return b;
5703
- const tokens = estimateToolResultTokens(b.content);
5704
- if (tokens < this.eliseThreshold) return b;
5705
- saved += tokens;
5706
- return {
5707
- type: "tool_result",
5708
- tool_use_id: b.tool_use_id,
5709
- content: `[elided: ~${tokens} tokens]`,
5710
- is_error: b.is_error
5711
- };
5712
- });
5713
- if (newContent.length === msg.content.length && newContent.every((b, idx) => b === msg.content[idx])) {
5714
- nextMessages[i] = msg;
5715
- } else {
5716
- nextMessages[i] = { ...msg, content: newContent };
5717
- changed = true;
5718
- }
5719
- }
5720
- if (changed) ctx.state.replaceMessages(nextMessages);
5721
- return saved;
5722
- }
5723
- hasTextContent(m) {
5724
- if (typeof m.content === "string") return m.content.trim().length > 0;
5725
- return m.content.some((b) => b.type === "text" && b.text.trim().length > 0);
5726
- }
5727
- estimateTokens(messages) {
5728
- let total = 0;
5729
- for (const m of messages) {
5730
- if (typeof m.content === "string") {
5731
- total += estimateTextTokens(m.content);
5732
- } else {
5733
- for (const b of m.content) {
5734
- if (b.type === "text") total += estimateTextTokens(b.text);
5735
- else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
5736
- else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
5737
- }
5738
- }
5739
- }
5740
- return total;
5741
- }
5742
5665
  };
5743
5666
 
5744
5667
  // src/models/llm-selector.ts
@@ -5764,20 +5687,6 @@ Rules:
5764
5687
  - If unsure, keep rather than collapse (errors are more costly than waste)
5765
5688
 
5766
5689
  Return ONLY the JSON object, no markdown, no explanation outside the JSON.`;
5767
- function estimateTokens(messages) {
5768
- let total = 0;
5769
- for (const m of messages) {
5770
- if (typeof m.content === "string") {
5771
- total += Math.ceil(m.content.length / 4);
5772
- } else if (Array.isArray(m.content)) {
5773
- for (const b of m.content) {
5774
- if (b.type === "text") total += Math.ceil(b.text.length / 4);
5775
- else total += Math.ceil(JSON.stringify(b).length / 4);
5776
- }
5777
- }
5778
- }
5779
- return total;
5780
- }
5781
5690
  function formatMessages(messages, maxChars = 8e3) {
5782
5691
  const lines = [];
5783
5692
  let used = 0;
@@ -5816,7 +5725,7 @@ var LLMSelector = class {
5816
5725
  async select(messages, maxToKeep) {
5817
5726
  const effectiveBudget = Math.min(maxToKeep, this.maxContextTokens);
5818
5727
  const historyText = formatMessages(messages);
5819
- const totalTokens = estimateTokens(messages);
5728
+ const totalTokens = estimateMessageTokens(messages);
5820
5729
  const systemText = `${this.systemPrompt}
5821
5730
 
5822
5731
  Conversation (${messages.length} messages, ~${totalTokens} tokens, budget: ${effectiveBudget}):
@@ -5848,10 +5757,7 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
5848
5757
  let startIdx = 0;
5849
5758
  for (let i = messages.length - 1; i >= 0; i--) {
5850
5759
  const m = expectDefined(messages[i]);
5851
- const cost = typeof m.content === "string" ? Math.ceil(m.content.length / 4) : m.content.reduce(
5852
- (acc, b) => acc + (b.type === "text" ? Math.ceil(b.text.length / 4) : Math.ceil(JSON.stringify(b).length / 4)),
5853
- 0
5854
- );
5760
+ const cost = estimateMessageTokens([m]);
5855
5761
  if (tokenCount + cost <= budget) {
5856
5762
  tokenCount += cost;
5857
5763
  } else {
@@ -6084,52 +5990,12 @@ Summarize the following message range:`;
6084
5990
  return Math.floor(this.maxContext * 0.75);
6085
5991
  }
6086
5992
  eliseOldToolResults(ctx) {
6087
- const messages = ctx.messages;
6088
- let pairCount = 0;
6089
- let preserveStart = messages.length;
6090
- for (let i = messages.length - 1; i >= 0 && pairCount < this.preserveK; i--) {
6091
- const m = messages[i];
6092
- if (!m) continue;
6093
- if (m.role === "user" || m.role === "assistant") {
6094
- pairCount++;
6095
- preserveStart = i;
6096
- }
6097
- }
6098
- let saved = 0;
6099
- let changed = false;
6100
- const nextMessages = new Array(messages.length);
6101
- for (let i = 0; i < messages.length; i++) {
6102
- const msg = messages[i];
6103
- if (i >= preserveStart) {
6104
- nextMessages[i] = msg;
6105
- continue;
6106
- }
6107
- if (!msg || !Array.isArray(msg.content)) {
6108
- nextMessages[i] = msg;
6109
- continue;
6110
- }
6111
- const newContent = msg.content.map((b) => {
6112
- if (b.type !== "tool_result") return b;
6113
- const text = typeof b.content === "string" ? b.content : JSON.stringify(b.content);
6114
- const tokens = this.roughTokenEstimate(text);
6115
- if (tokens < this.eliseThreshold) return b;
6116
- saved += tokens;
6117
- return {
6118
- type: "tool_result",
6119
- tool_use_id: b.tool_use_id,
6120
- content: `[elided: ~${tokens} tokens]`,
6121
- is_error: b.is_error
6122
- };
6123
- });
6124
- if (newContent.every((b, idx) => b === msg.content[idx])) {
6125
- nextMessages[i] = msg;
6126
- } else {
6127
- nextMessages[i] = { ...msg, content: newContent };
6128
- changed = true;
6129
- }
6130
- }
6131
- if (changed) ctx.state.replaceMessages(nextMessages);
6132
- return saved;
5993
+ const result = eliseOldToolResults(ctx.messages, {
5994
+ preserveK: this.preserveK,
5995
+ eliseThreshold: this.eliseThreshold
5996
+ });
5997
+ if (result.changed) ctx.state.replaceMessages(result.messages);
5998
+ return result.saved;
6133
5999
  }
6134
6000
  hasTextContent(m) {
6135
6001
  if (typeof m.content === "string") return m.content.trim().length > 0;
@@ -6159,8 +6025,80 @@ Summarize the following message range:`;
6159
6025
  }
6160
6026
  };
6161
6027
 
6028
+ // src/execution/strategy-compactor.ts
6029
+ function createStrategyCompactor(opts = {}) {
6030
+ const requested = opts.strategy ?? (opts.llmSelector ? "selective" : "hybrid");
6031
+ const strategy = requested;
6032
+ if (strategy === "intelligent" || strategy === "selective") {
6033
+ return new ProviderBackedCompactor(strategy, opts);
6034
+ }
6035
+ return new HybridCompactor({
6036
+ preserveK: opts.preserveK,
6037
+ eliseThreshold: opts.eliseThreshold
6038
+ });
6039
+ }
6040
+ var ProviderBackedCompactor = class {
6041
+ constructor(strategy, opts) {
6042
+ this.strategy = strategy;
6043
+ this.opts = opts;
6044
+ }
6045
+ strategy;
6046
+ opts;
6047
+ async compact(ctx, compactOpts = {}) {
6048
+ return this.resolveInner(ctx).compact(ctx, compactOpts);
6049
+ }
6050
+ /**
6051
+ * Construct the concrete compactor for this run. Rebuilt per call (cheap, no
6052
+ * I/O) so a model switch — which changes `ctx.provider.capabilities.maxContext`
6053
+ * — is always reflected. Reads the active ContextWindowPolicy from `ctx.meta`
6054
+ * so the LLM compactors honor the same thresholds/preserveK as the policy.
6055
+ */
6056
+ resolveInner(ctx) {
6057
+ const provider = ctx.provider;
6058
+ if (!provider) {
6059
+ return new HybridCompactor({
6060
+ preserveK: this.opts.preserveK,
6061
+ eliseThreshold: this.opts.eliseThreshold
6062
+ });
6063
+ }
6064
+ const policy = readPolicy(ctx);
6065
+ const maxContext = provider.capabilities?.maxContext || void 0;
6066
+ const thresholds = policy?.thresholds;
6067
+ const common = {
6068
+ provider,
6069
+ maxContext,
6070
+ preserveK: this.opts.preserveK ?? policy?.preserveK,
6071
+ eliseThreshold: this.opts.eliseThreshold ?? policy?.eliseThreshold,
6072
+ ...thresholds ? { warnThreshold: thresholds.warn, softThreshold: thresholds.soft, hardThreshold: thresholds.hard } : {}
6073
+ };
6074
+ if (this.strategy === "selective") {
6075
+ return new SelectiveCompactor({
6076
+ ...common,
6077
+ selectorModel: this.opts.summarizerModel,
6078
+ summarizerModel: this.opts.summarizerModel
6079
+ });
6080
+ }
6081
+ return new IntelligentCompactor({
6082
+ ...common,
6083
+ summarizerModel: this.opts.summarizerModel
6084
+ });
6085
+ }
6086
+ };
6087
+ function readPolicy(ctx) {
6088
+ const policy = ctx.meta?.["contextWindowPolicy"];
6089
+ if (!policy || typeof policy !== "object") return null;
6090
+ const candidate = policy;
6091
+ if (typeof candidate.preserveK !== "number" || !candidate.thresholds) return null;
6092
+ return candidate;
6093
+ }
6094
+
6162
6095
  // src/execution/auto-compaction-middleware.ts
6163
6096
  var LEVEL_RANK2 = { warn: 0, soft: 1, hard: 2 };
6097
+ var MAX_DIGEST_LOG_CHARS = 4e3;
6098
+ function truncateDigest(digest) {
6099
+ if (digest.length <= MAX_DIGEST_LOG_CHARS) return digest;
6100
+ return `${digest.slice(0, MAX_DIGEST_LOG_CHARS)}\u2026 [+${digest.length - MAX_DIGEST_LOG_CHARS} chars; full turns in session log]`;
6101
+ }
6164
6102
  var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
6165
6103
  name = "AutoCompaction";
6166
6104
  compactor;
@@ -6221,7 +6159,12 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
6221
6159
  }
6222
6160
  handler() {
6223
6161
  return async (ctx, next) => {
6224
- const tokens = this._estimator ? this._estimator(ctx) : estimateRequestTokensCalibrated(ctx.messages, ctx.systemPrompt, ctx.tools ?? []).total;
6162
+ const tokens = this._estimator ? this._estimator(ctx) : estimateRequestTokensCalibrated(
6163
+ ctx.messages,
6164
+ ctx.systemPrompt,
6165
+ ctx.tools ?? [],
6166
+ `${ctx.provider?.id ?? "unknown"}/${ctx.model}`
6167
+ ).total;
6225
6168
  const load = tokens / this._maxContext;
6226
6169
  const policy = this.policyProvider?.(ctx);
6227
6170
  const thresholds = policy?.thresholds ?? {
@@ -6283,7 +6226,11 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
6283
6226
  after: report.after,
6284
6227
  level: pressure.level,
6285
6228
  aggressive,
6286
- reductions: report.reductions?.map((r) => ({ phase: r.phase, saved: r.saved }))
6229
+ reductions: report.reductions?.map((r) => ({ phase: r.phase, saved: r.saved })),
6230
+ // Record what was collapsed so the audit trail shows the preserved
6231
+ // content, not just token counts. Bounded to keep the log line small;
6232
+ // the full original turns are already in the session JSONL.
6233
+ ...report.collapsedDigest ? { digest: truncateDigest(report.collapsedDigest) } : {}
6287
6234
  });
6288
6235
  ctx.clearFileTracking();
6289
6236
  } catch (err) {
@@ -6318,10 +6265,10 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
6318
6265
  // src/utils/json-schema-validate.ts
6319
6266
  function validateAgainstSchema(value, schema) {
6320
6267
  const errors = [];
6321
- walk3(value, schema, "", errors);
6268
+ walk2(value, schema, "", errors);
6322
6269
  return { ok: errors.length === 0, errors };
6323
6270
  }
6324
- function walk3(value, schema, path19, errors) {
6271
+ function walk2(value, schema, path19, errors) {
6325
6272
  if (schema.enum !== void 0) {
6326
6273
  if (!schema.enum.some((e) => deepEqual(e, value))) {
6327
6274
  errors.push({
@@ -6350,13 +6297,13 @@ function walk3(value, schema, path19, errors) {
6350
6297
  if (schema.properties) {
6351
6298
  for (const [key, subSchema] of Object.entries(schema.properties)) {
6352
6299
  if (key in obj) {
6353
- walk3(obj[key], subSchema, joinPath(path19, key), errors);
6300
+ walk2(obj[key], subSchema, joinPath(path19, key), errors);
6354
6301
  }
6355
6302
  }
6356
6303
  }
6357
6304
  }
6358
6305
  if (schema.type === "array" && Array.isArray(value) && schema.items) {
6359
- value.forEach((item, i) => walk3(item, schema.items, `${path19}[${i}]`, errors));
6306
+ value.forEach((item, i) => walk2(item, schema.items, `${path19}[${i}]`, errors));
6360
6307
  }
6361
6308
  }
6362
6309
  function checkType(value, type) {
@@ -6870,6 +6817,13 @@ function extractMalformedRaw(input) {
6870
6817
  }
6871
6818
  }
6872
6819
 
6820
+ // src/utils/assert-never.ts
6821
+ function assertNever(x, message) {
6822
+ throw new Error(
6823
+ `Unhandled case: ${JSON.stringify(x)}`
6824
+ );
6825
+ }
6826
+
6873
6827
  // src/execution/autonomous-runner.ts
6874
6828
  var DoneConditionChecker = class {
6875
6829
  constructor(condition) {
@@ -6923,6 +6877,12 @@ var DoneConditionChecker = class {
6923
6877
  return { done: true, reason: `max tool calls (${this.condition.maxToolCalls}) reached`, ...state };
6924
6878
  }
6925
6879
  break;
6880
+ case "all_tasks_done":
6881
+ break;
6882
+ case "custom":
6883
+ break;
6884
+ default:
6885
+ return assertNever(this.condition.type);
6926
6886
  }
6927
6887
  return { done: false, iterations: state.iterations, toolCalls: state.toolCalls };
6928
6888
  }
@@ -7074,6 +7034,7 @@ function resolveWstackPaths(opts) {
7074
7034
  projectTrust: path11.join(projectDir, "trust.json"),
7075
7035
  projectMeta: path11.join(projectDir, "meta.json"),
7076
7036
  projectLocalConfig: path11.join(projectDir, "config.local.json"),
7037
+ inProjectConfig: path11.join(opts.projectRoot, ".wrongstack", "config.json"),
7077
7038
  inProjectAgentsFile: path11.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
7078
7039
  inProjectSkills: path11.join(opts.projectRoot, ".wrongstack", "skills"),
7079
7040
  inProjectWorktrees: path11.join(opts.projectRoot, ".wrongstack", "worktrees"),
@@ -7098,16 +7059,20 @@ async function loadGoal(filePath) {
7098
7059
  let raw;
7099
7060
  try {
7100
7061
  raw = await fsp.readFile(filePath, "utf8");
7101
- } catch {
7102
- return null;
7062
+ } catch (err) {
7063
+ const code = err.code;
7064
+ if (code === "ENOENT") return null;
7065
+ throw err;
7103
7066
  }
7104
7067
  try {
7105
7068
  const parsed = JSON.parse(raw);
7106
7069
  if (parsed?.version !== 1 || typeof parsed.goal !== "string" || !Array.isArray(parsed.journal)) {
7070
+ console.warn(`[goal-store] Corrupt goal.json at ${filePath} \u2014 invalid schema. Consider deleting it and re-creating.`);
7107
7071
  return null;
7108
7072
  }
7109
7073
  return parsed;
7110
7074
  } catch {
7075
+ console.warn(`[goal-store] Corrupt goal.json at ${filePath} \u2014 JSON parse failed. Consider deleting it and re-creating.`);
7111
7076
  return null;
7112
7077
  }
7113
7078
  }
@@ -7140,9 +7105,9 @@ function parseProgressFromText(text) {
7140
7105
  const re = /\[progress:\s*(\d{1,3})%\]\s*(?:[—\-]\s*(.+))?/i;
7141
7106
  const m = text.match(re);
7142
7107
  if (!m) return null;
7143
- const progress = Math.min(100, Math.max(0, Number.parseInt(m[1], 10)));
7108
+ const progress = Math.min(100, Math.max(0, Number.parseInt(m[1] ?? "0", 10)));
7144
7109
  const note = m[2]?.trim() || void 0;
7145
- return { progress, note };
7110
+ return note === void 0 ? { progress } : { progress, note };
7146
7111
  }
7147
7112
  function recordProgress(goal, progress, note) {
7148
7113
  const clamped = Math.min(100, Math.max(0, progress));
@@ -7161,7 +7126,7 @@ function computeTrend(history) {
7161
7126
  const recent = history.slice(-5);
7162
7127
  const deltas = [];
7163
7128
  for (let i = 1; i < recent.length; i++) {
7164
- deltas.push(recent[i].progress - recent[i - 1].progress);
7129
+ deltas.push((recent[i]?.progress ?? 0) - (recent[i - 1]?.progress ?? 0));
7165
7130
  }
7166
7131
  if (deltas.length < 2) return void 0;
7167
7132
  const avgDelta = deltas.reduce((a, b) => a + b, 0) / deltas.length;
@@ -7740,6 +7705,7 @@ ${recentJournal}` : "No prior iterations.",
7740
7705
  note: note.slice(0, 240)
7741
7706
  });
7742
7707
  await saveGoal(this.goalPath, withEntry);
7708
+ this.opts.onEternalStop?.();
7743
7709
  }
7744
7710
  /**
7745
7711
  * Manually clear the goal — equivalent to `/goal clear` typed by the user.
@@ -7753,8 +7719,8 @@ ${recentJournal}` : "No prior iterations.",
7753
7719
  await saveGoal(this.goalPath, abandoned);
7754
7720
  }
7755
7721
  try {
7756
- const { unlink: unlink9 } = await import('fs/promises');
7757
- await unlink9(this.goalPath);
7722
+ const { unlink: unlink10 } = await import('fs/promises');
7723
+ await unlink10(this.goalPath);
7758
7724
  } catch {
7759
7725
  }
7760
7726
  this.opts.onEternalStop?.();
@@ -8755,6 +8721,53 @@ Working rules:
8755
8721
  "flaws"
8756
8722
  ]
8757
8723
  }
8724
+ },
8725
+ {
8726
+ config: {
8727
+ id: "refactor-planner",
8728
+ name: "Refactor Planner",
8729
+ role: "refactor-planner",
8730
+ tools: [...PLAN_TOOLS, "diff"],
8731
+ prompt: `You are the Refactor Planner agent. Your job is to analyze code
8732
+ structure and produce a concrete, phased refactoring plan with risk
8733
+ assessment, dependency ordering, and rollback strategy.
8734
+
8735
+ Scope:
8736
+ - Map module-level dependencies (import graph)
8737
+ - Identify coupling hotspots (high fan-in/out modules)
8738
+ - Assess refactoring risk by complexity and test coverage
8739
+ - Generate phased plans with checkpoint milestones
8740
+ - Produce diff-friendly task lists (one task = one concern)
8741
+
8742
+ Input format you accept:
8743
+ { "task": "plan | assess | roadmap", "target": "src/core", "constraint": "no-breaking-changes | minimal-downtime | full-rewrite", "focus": "architecture | performance | maintainability" }
8744
+
8745
+ Output: Markdown refactor plan with phases (Low Risk / Medium Risk / High Risk),
8746
+ dependency graph, rollback strategy, and exit criteria.
8747
+
8748
+ Working rules:
8749
+ - Always include rollback strategy \u2014 every refactor can fail
8750
+ - Merge tasks that take <1h into a single phase
8751
+ - Respect team constraints (reviewer availability, parallelization)
8752
+ - Never plan without analyzing the actual code first`
8753
+ },
8754
+ budget: HEAVY_BUDGET,
8755
+ capability: {
8756
+ phase: "planning",
8757
+ summary: "Refactoring planner: analyzes code structure, maps dependencies, produces risk-scored phased plans with rollback strategy.",
8758
+ keywords: [
8759
+ "refactor",
8760
+ "refactoring",
8761
+ "restructure",
8762
+ "debt",
8763
+ "technical debt",
8764
+ "clean up",
8765
+ "modularize",
8766
+ "decouple",
8767
+ "dependency graph",
8768
+ "code structure"
8769
+ ]
8770
+ }
8758
8771
  }
8759
8772
  ];
8760
8773
 
@@ -9294,6 +9307,157 @@ Working rules:
9294
9307
  "robustness"
9295
9308
  ]
9296
9309
  }
9310
+ },
9311
+ {
9312
+ config: {
9313
+ id: "security-scanner",
9314
+ name: "Security Scanner",
9315
+ role: "security-scanner",
9316
+ tools: [...TOOLS.inspect],
9317
+ prompt: `You are the Security Scanner agent. Your job is to scan code,
9318
+ configs, and dependencies for security issues from hardcoded secrets to
9319
+ supply chain risks.
9320
+
9321
+ Scope:
9322
+ - Detect hardcoded secrets: API keys, tokens, passwords, private keys
9323
+ - Find injection vectors: eval, innerHTML, SQL concat, shell injection
9324
+ - Identify insecure patterns: weak crypto, hardcoded IVs, disabled TLS
9325
+ - Scan dependencies for known CVEs (via npm/pnpm audit)
9326
+ - Flag supply chain risks: postinstall hooks, unverified scripts
9327
+
9328
+ Input format you accept:
9329
+ { "task": "scan | audit | secrets | dependencies", "paths": ["src", "config"], "depth": "quick | normal | deep" }
9330
+
9331
+ Output: Markdown security report with severity-ranked findings, injection
9332
+ vectors, dependency issues, and a remediation checklist.
9333
+
9334
+ Working rules:
9335
+ - Never scan node_modules \u2014 use npm audit instead
9336
+ - Always provide remediation steps, not just findings
9337
+ - Verify regex-based secrets before flagging (false positive risk)
9338
+ - When in doubt, flag as medium rather than ignoring potential issues`
9339
+ },
9340
+ budget: HEAVY_BUDGET,
9341
+ capability: {
9342
+ phase: "verify",
9343
+ summary: "Security scanner: detects hardcoded secrets, injection vectors, insecure patterns, and supply-chain risks with remediation.",
9344
+ keywords: [
9345
+ "security",
9346
+ "scan",
9347
+ "vulnerability",
9348
+ "secret",
9349
+ "api key",
9350
+ "hardcoded",
9351
+ "injection",
9352
+ "cve",
9353
+ "audit dependencies",
9354
+ "supply chain",
9355
+ "xss",
9356
+ "sqli",
9357
+ "shell injection",
9358
+ "sensitive data",
9359
+ "credential"
9360
+ ]
9361
+ }
9362
+ },
9363
+ {
9364
+ config: {
9365
+ id: "bug-hunter",
9366
+ name: "Bug Hunter",
9367
+ role: "bug-hunter",
9368
+ tools: [...TOOLS.inspect],
9369
+ prompt: `You are the Bug Hunter agent. Your job is to systematically scan
9370
+ source code for bugs, anti-patterns, and code smells using pattern matching
9371
+ and heuristics. Output a prioritized hit list with file:line references.
9372
+
9373
+ Scope:
9374
+ - Detect common bug patterns (uncaught errors, resource leaks, race conditions)
9375
+ - Identify anti-patterns (callback hell, God objects, circular deps)
9376
+ - Find TypeScript-specific issues (unsafe any, missing null checks, branded types)
9377
+ - Flag security-sensitive constructs (eval, innerHTML, hardcoded secrets)
9378
+ - Rank findings: critical > high > medium > low
9379
+
9380
+ Input format you accept:
9381
+ { "task": "scan | hunt | check", "paths": ["src/**/*.ts"], "focus": "bugs | patterns | security | all", "severityThreshold": "medium" }
9382
+
9383
+ Output: Markdown bug hunt report with critically/high/medium/low sections.
9384
+ Each entry: **[TYPE]** \`file:line\` \u2014 description + suggested fix
9385
+
9386
+ Working rules:
9387
+ - Never scan node_modules \u2014 it's noise
9388
+ - Always include file:line for every finding
9389
+ - If >30% of findings are false positives, note the confidence level
9390
+ - Ask director for clarification if paths are ambiguous`
9391
+ },
9392
+ budget: HEAVY_BUDGET,
9393
+ capability: {
9394
+ phase: "verify",
9395
+ summary: "Bug hunter: scans source code for bugs, anti-patterns, and code smells, producing a file:line-ranked hit list with fixes.",
9396
+ keywords: [
9397
+ "bug",
9398
+ "hunt",
9399
+ "scan",
9400
+ "code smell",
9401
+ "anti-pattern",
9402
+ "race condition",
9403
+ "memory leak",
9404
+ "null deref",
9405
+ "type safety",
9406
+ "unhandled error",
9407
+ "find bugs",
9408
+ "audit code",
9409
+ "code quality"
9410
+ ]
9411
+ }
9412
+ },
9413
+ {
9414
+ config: {
9415
+ id: "audit-log",
9416
+ name: "Audit Log",
9417
+ role: "audit-log",
9418
+ tools: [...TOOLS.inspect],
9419
+ prompt: `You are the Audit Log agent. Your job is to analyze structured JSONL
9420
+ session logs and produce actionable markdown reports.
9421
+
9422
+ Scope:
9423
+ - Parse session logs (iteration counts, tool calls, errors, usage)
9424
+ - Detect repeated failure patterns across multiple runs
9425
+ - Identify tool usage anomalies (over-use, failures, unexpected chains)
9426
+ - Track token consumption trends
9427
+ - Generate structured audit reports with severity ratings
9428
+
9429
+ Input format you accept:
9430
+ { "task": "analyze | report | trends", "sessionPath": "<path>", "focus": "errors | tools | usage | all" }
9431
+
9432
+ Output: Markdown audit report with Summary, Top Errors, Tool Usage table,
9433
+ Anomalies, and Cost Trend sections.
9434
+
9435
+ Working rules:
9436
+ - Never fabricate numbers \u2014 read the actual logs first
9437
+ - Always include file:line references for errors
9438
+ - If sessionPath is missing, ask the director to provide it
9439
+ - Report confidence level: high (>90% accuracy), medium, low`
9440
+ },
9441
+ budget: MEDIUM_BUDGET,
9442
+ capability: {
9443
+ phase: "verify",
9444
+ summary: "Audit log analyzer: parses session JSONL, detects failure patterns, tool anomalies, and cost trends with structured reports.",
9445
+ keywords: [
9446
+ "audit",
9447
+ "log",
9448
+ "logs",
9449
+ "session",
9450
+ "trace",
9451
+ "analyze logs",
9452
+ "error patterns",
9453
+ "cost analysis",
9454
+ "tool usage",
9455
+ "token usage",
9456
+ "post-mortem",
9457
+ "trend",
9458
+ "anomaly"
9459
+ ]
9460
+ }
9297
9461
  }
9298
9462
  ];
9299
9463
 
@@ -12678,7 +12842,7 @@ async function expandGlob(pattern) {
12678
12842
  const abs = isAbsolute(pattern);
12679
12843
  const base = abs ? baseDir(pattern) : baseDir(pattern);
12680
12844
  const relPat = base === "." ? pattern : pattern.slice(base.length + 1);
12681
- async function walk4(dir, pat) {
12845
+ async function walk3(dir, pat) {
12682
12846
  let entries;
12683
12847
  try {
12684
12848
  entries = await fsp.readdir(dir);
@@ -12699,12 +12863,12 @@ async function expandGlob(pattern) {
12699
12863
  const before = pat.slice(0, firstGlob);
12700
12864
  const rest = pat.slice(firstGlob);
12701
12865
  if (before.endsWith("**")) {
12702
- await walk4(dir, rest);
12866
+ await walk3(dir, rest);
12703
12867
  for (const e of entries) {
12704
12868
  const full = `${dir}${SEP}${e}`;
12705
12869
  try {
12706
12870
  const stat6 = await fsp.stat(full);
12707
- if (stat6.isDirectory()) await walk4(full, rest);
12871
+ if (stat6.isDirectory()) await walk3(full, rest);
12708
12872
  } catch {
12709
12873
  }
12710
12874
  }
@@ -12722,13 +12886,13 @@ async function expandGlob(pattern) {
12722
12886
  const full = `${dir}${SEP}${seg}`;
12723
12887
  try {
12724
12888
  const stat6 = await fsp.stat(full);
12725
- if (stat6.isDirectory()) await walk4(full, rest);
12889
+ if (stat6.isDirectory()) await walk3(full, rest);
12726
12890
  } catch {
12727
12891
  }
12728
12892
  }
12729
12893
  }
12730
12894
  }
12731
- await walk4(base === "." ? "." : base, relPat);
12895
+ await walk3(base === "." ? "." : base, relPat);
12732
12896
  return [...results];
12733
12897
  }
12734
12898
 
@@ -14324,7 +14488,10 @@ var Director = class _Director {
14324
14488
  workCompleteFlag = false;
14325
14489
  /** Pending /btw notes stashed by the leader agent (see setLeaderBtwNote). */
14326
14490
  _leaderBtwNotes = [];
14327
- /** Active collab sessions tracked by sessionId (see spawnCollab). */
14491
+ /** Active collab sessions tracked by sessionId (see spawnCollab).
14492
+ * The tuple holds the session and its Director-registered listener unsubs.
14493
+ * Calling the unsubs on cancel/premature-cleanup prevents listener accumulation
14494
+ * on CollabSession (EventEmitter) across many spawnCollab() calls. */
14328
14495
  _activeCollabSessions = /* @__PURE__ */ new Map();
14329
14496
  /** Prevents large `ask_subagent` answers from bloating the leader's context window. */
14330
14497
  largeAnswerStore;
@@ -14654,11 +14821,13 @@ var Director = class _Director {
14654
14821
  * The CollabDebugReport will reflect 'cancelled' disposition when awaited.
14655
14822
  */
14656
14823
  cancelCollabSession(sessionId, reason = "Director cancelled") {
14657
- const session = this._activeCollabSessions.get(sessionId);
14658
- if (!session || session.isCancelled()) return;
14659
- session.cancel(reason);
14660
- for (const [_role, subagentId] of session.getSubagentIds()) {
14661
- this.coordinator.stop(subagentId).catch(() => {
14824
+ const entry = this._activeCollabSessions.get(sessionId);
14825
+ if (!entry || entry.session.isCancelled()) return;
14826
+ for (const unsub of entry.unsubs) unsub();
14827
+ entry.session.cancel(reason);
14828
+ for (const [_role, subagentId] of entry.session.getSubagentIds()) {
14829
+ this.coordinator.stop(subagentId).catch((err) => {
14830
+ console.debug(`[director] stop subagent ${subagentId} failed (may have already completed): ${err}`);
14662
14831
  });
14663
14832
  }
14664
14833
  }
@@ -15306,9 +15475,15 @@ var Director = class _Director {
15306
15475
  return options.onBudgetWarning?.(alert) ?? "ignore";
15307
15476
  }
15308
15477
  });
15309
- this._activeCollabSessions.set(session.sessionId, session);
15310
- session.on("session.done", () => this._activeCollabSessions.delete(session.sessionId));
15311
- session.on("session.error", () => this._activeCollabSessions.delete(session.sessionId));
15478
+ const doneHandler = () => this._activeCollabSessions.delete(session.sessionId);
15479
+ const errorHandler = () => this._activeCollabSessions.delete(session.sessionId);
15480
+ session.on("session.done", doneHandler);
15481
+ session.on("session.error", errorHandler);
15482
+ const unsubs = [
15483
+ () => session.off("session.done", doneHandler),
15484
+ () => session.off("session.error", errorHandler)
15485
+ ];
15486
+ this._activeCollabSessions.set(session.sessionId, { session, unsubs });
15312
15487
  return session.start();
15313
15488
  }
15314
15489
  /**
@@ -18730,6 +18905,8 @@ var SpecVersioning = class {
18730
18905
  switch (type) {
18731
18906
  case "functional":
18732
18907
  return "feature";
18908
+ case "non-functional":
18909
+ return "feature";
18733
18910
  case "security":
18734
18911
  return "feature";
18735
18912
  case "performance":
@@ -18737,7 +18914,7 @@ var SpecVersioning = class {
18737
18914
  case "ux":
18738
18915
  return "feature";
18739
18916
  default:
18740
- return "feature";
18917
+ return assertNever(type);
18741
18918
  }
18742
18919
  }
18743
18920
  };
@@ -19888,27 +20065,13 @@ function startOtlpTraceExporter(opts) {
19888
20065
  // src/infrastructure/context-manager.ts
19889
20066
  var CONTEXT_MANAGER_TOOL_NAME = "context_manager";
19890
20067
  function roughEstimate(messages) {
19891
- let total = 0;
19892
- for (const m of messages) {
19893
- if (typeof m.content === "string") {
19894
- total += Math.ceil(m.content.length / 4);
19895
- } else if (Array.isArray(m.content)) {
19896
- for (const b of m.content) {
19897
- if (b.type === "text") total += Math.ceil(b.text.length / 4);
19898
- else if (b.type === "tool_use" || b.type === "tool_result") {
19899
- total += Math.ceil(JSON.stringify(b).length / 4);
19900
- }
19901
- }
19902
- }
19903
- }
19904
- return total;
20068
+ return estimateMessageTokens(messages);
19905
20069
  }
19906
20070
  function createContextManagerTool(opts = {}) {
19907
20071
  const minCompactThreshold = opts.minCompactThreshold ?? 0;
19908
20072
  const noopRetryDeltaTokens = opts.noopRetryDeltaTokens ?? 2e3;
19909
- const maxContext = opts.maxContext ?? 128e3;
20073
+ const configuredMaxContext = opts.maxContext;
19910
20074
  const compactThresholdFraction = opts.compactThresholdFraction ?? 0.5;
19911
- const effectiveThreshold = minCompactThreshold > 0 ? minCompactThreshold : Math.floor(maxContext * compactThresholdFraction);
19912
20075
  let lastNoopTokens = 0;
19913
20076
  return {
19914
20077
  name: CONTEXT_MANAGER_TOOL_NAME,
@@ -20002,6 +20165,8 @@ function createContextManagerTool(opts = {}) {
20002
20165
  }
20003
20166
  const fullEstimate = input.systemPrompt != null && Array.isArray(input.tools) ? estimateRequestTokens(messages, input.systemPrompt, input.tools) : { total: beforeTokens};
20004
20167
  const currentTokens = fullEstimate.total;
20168
+ const runtimeMaxContext = configuredMaxContext ?? ctx.provider?.capabilities?.maxContext ?? 128e3;
20169
+ const runtimeThreshold = minCompactThreshold > 0 ? minCompactThreshold : Math.floor(runtimeMaxContext * compactThresholdFraction);
20005
20170
  if (lastNoopTokens > 0) {
20006
20171
  const delta = currentTokens - lastNoopTokens;
20007
20172
  if (delta < noopRetryDeltaTokens) {
@@ -20014,13 +20179,13 @@ function createContextManagerTool(opts = {}) {
20014
20179
  };
20015
20180
  }
20016
20181
  }
20017
- if (currentTokens < effectiveThreshold) {
20182
+ if (runtimeThreshold > 0 && currentTokens < runtimeThreshold) {
20018
20183
  return {
20019
20184
  action: "compact",
20020
20185
  beforeTokens,
20021
20186
  afterTokens: beforeTokens,
20022
20187
  messageCount: messages.length,
20023
- notes: `Context tokens (${currentTokens}) below compact threshold (${effectiveThreshold}). Skipping.`
20188
+ notes: `Context tokens (${currentTokens}) below compact threshold (${runtimeThreshold}, based on provider maxContext ${runtimeMaxContext}). Skipping.`
20024
20189
  };
20025
20190
  }
20026
20191
  const report = await opts.compactor.compact(ctx);
@@ -20273,6 +20438,6 @@ var allServers = () => ({
20273
20438
  "minimax-vision": { ...miniMaxVisionServer(), enabled: false }
20274
20439
  });
20275
20440
 
20276
- export { AGENTS_BY_PHASE, AGENT_CATALOG, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, ConfigMigrationError, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPermissionPolicy, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, Director, DirectorStateCheckpoint, DoneConditionChecker, EternalAutonomyEngine, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FleetBus, FleetSpawnBudgetError, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, IntelligentCompactor, LLMSelector, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, SddParallelRun, SddTaskDecomposer, SelectiveCompactor, SessionAnalyzer, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, ToolExecutor, addPlanItem, allServers, analyzeCriticalPath, applyRosterBudget, attachAutoExtend, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, clearPlan, composeDirectorPrompt, composeSubagentPrompt, context7Server, contextManagerTool, createAutoExecutor, createContextManagerTool, createDelegateTool, createMessage, createSessionEventBridge, decryptConfigSecrets2 as decryptConfigSecrets, deriveTodosFromPlanItem, dispatchAgent, emptyPlan, encryptConfigSecrets, everArtServer, filesystemServer, formatPlan, formatPlanTemplates, getAgentDefinition, getPlanTemplate, getTemplate, githubServer, googleMapsServer, listPlanTemplates, listTemplates, loadDirectorState, loadPlan, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeRollUpTool, makeSpawnTool, makeTerminateTool, migratePlaintextSecrets, miniMaxVisionServer, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, resolveAuditLevel, resolveSessionLoggingConfig, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, savePlan, saveTodosCheckpoint, scoreAgents, sentinelServer, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, templateToMarkdown, wireMetricsToEvents, zaiVisionServer };
20441
+ export { AGENTS_BY_PHASE, AGENT_CATALOG, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, ConfigMigrationError, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPermissionPolicy, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, Director, DirectorStateCheckpoint, DoneConditionChecker, EternalAutonomyEngine, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FleetBus, FleetSpawnBudgetError, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, IntelligentCompactor, LLMSelector, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, SddParallelRun, SddTaskDecomposer, SelectiveCompactor, SessionAnalyzer, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, ToolExecutor, addPlanItem, allServers, analyzeCriticalPath, applyRosterBudget, attachAutoExtend, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, clearPlan, composeDirectorPrompt, composeSubagentPrompt, context7Server, contextManagerTool, createAutoExecutor, createContextManagerTool, createDelegateTool, createMessage, createSessionEventBridge, createStrategyCompactor, decryptConfigSecrets, deriveTodosFromPlanItem, dispatchAgent, emptyPlan, encryptConfigSecrets, everArtServer, filesystemServer, formatPlan, formatPlanTemplates, getAgentDefinition, getPlanTemplate, getTemplate, githubServer, googleMapsServer, listPlanTemplates, listTemplates, loadDirectorState, loadPlan, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeRollUpTool, makeSpawnTool, makeTerminateTool, migratePlaintextSecrets, miniMaxVisionServer, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, resolveAuditLevel, resolveSessionLoggingConfig, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, savePlan, saveTodosCheckpoint, scoreAgents, sentinelServer, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, templateToMarkdown, wireMetricsToEvents, zaiVisionServer };
20277
20442
  //# sourceMappingURL=index.js.map
20278
20443
  //# sourceMappingURL=index.js.map