@wrongstack/core 0.264.0 → 0.267.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 (90) hide show
  1. package/dist/{agent-bridge-D8sa1vtv.d.ts → agent-bridge-STJ3JwwK.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-c9DLkaas.d.ts → agent-subagent-runner-CzPGP3jA.d.ts} +131 -11
  3. package/dist/{brain-O1IdKPaK.d.ts → brain-Cdg77tVN.d.ts} +103 -2
  4. package/dist/{compactor-BBy0rCtB.d.ts → compactor-iMZ84CXq.d.ts} +19 -1
  5. package/dist/{config-Dz2F3H2K.d.ts → config-Du3pYYln.d.ts} +132 -13
  6. package/dist/{context-BGSpZNSE.d.ts → context-dT5Ueund.d.ts} +90 -12
  7. package/dist/coordination/index.d.ts +78 -22
  8. package/dist/coordination/index.js +695 -273
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/{default-config-CXsDvOmP.d.ts → default-config-B0cj-Hry.d.ts} +11 -1
  11. package/dist/defaults/index.d.ts +28 -28
  12. package/dist/defaults/index.js +2327 -965
  13. package/dist/defaults/index.js.map +1 -1
  14. package/dist/execution/index.d.ts +16 -16
  15. package/dist/execution/index.js +1500 -371
  16. package/dist/execution/index.js.map +1 -1
  17. package/dist/execution/prompt-enhancer.d.ts +2 -2
  18. package/dist/execution/prompt-enhancer.js +1 -1
  19. package/dist/execution/prompt-enhancer.js.map +1 -1
  20. package/dist/extension/index.d.ts +6 -6
  21. package/dist/{goal-preamble-DzjFuN3p.d.ts → goal-preamble-SulMTowG.d.ts} +33 -12
  22. package/dist/{goal-store-CxWmCGbH.d.ts → goal-store-CABDwdFE.d.ts} +1 -1
  23. package/dist/{index-CbLSI66_.d.ts → index-Bms0m4oy.d.ts} +5 -5
  24. package/dist/{index-CYIQrXVF.d.ts → index-DtCVWel4.d.ts} +8 -8
  25. package/dist/index-IEuxQd-E.d.ts +82 -0
  26. package/dist/index.d.ts +261 -57
  27. package/dist/index.js +4799 -2212
  28. package/dist/index.js.map +1 -1
  29. package/dist/infrastructure/index.d.ts +6 -6
  30. package/dist/infrastructure/index.js +84 -9
  31. package/dist/infrastructure/index.js.map +1 -1
  32. package/dist/kernel/index.d.ts +9 -9
  33. package/dist/kernel/index.js +1 -1
  34. package/dist/kernel/index.js.map +1 -1
  35. package/dist/{mcp-servers-DC4QRPUI.d.ts → mcp-servers-C2cBTxUR.d.ts} +3 -3
  36. package/dist/models/index.d.ts +5 -5
  37. package/dist/models/index.js +104 -31
  38. package/dist/models/index.js.map +1 -1
  39. package/dist/{models-registry-B_siPxqN.d.ts → models-registry-BqGZNJQ-.d.ts} +1 -1
  40. package/dist/{multi-agent-coordinator-CK5Jdj9K.d.ts → multi-agent-coordinator-B8R43uPz.d.ts} +1 -1
  41. package/dist/{null-fleet-bus-DgvD4SCO.d.ts → null-fleet-bus-CnXa5oTH.d.ts} +14 -9
  42. package/dist/observability/index.d.ts +2 -2
  43. package/dist/{parallel-eternal-engine-bK0JQBR_.d.ts → parallel-eternal-engine-DdNnw9BQ.d.ts} +11 -9
  44. package/dist/{path-resolver-BPEDlN38.d.ts → path-resolver-COIMLCQL.d.ts} +3 -3
  45. package/dist/{permission-4yvGmMRB.d.ts → permission-B75JAi3-.d.ts} +1 -1
  46. package/dist/{permission-policy-C6XpsBOy.d.ts → permission-policy-DlR9eJAM.d.ts} +2 -2
  47. package/dist/{pipeline-CXCeMz8J.d.ts → pipeline-BfD2k1rT.d.ts} +3 -3
  48. package/dist/{plan-templates-BvzRBkJc.d.ts → plan-templates-DSIKCXZN.d.ts} +32 -8
  49. package/dist/provider-model-resolve-BNRsNuJx.d.ts +107 -0
  50. package/dist/{provider-runner-C5aQpDWE.d.ts → provider-runner-CX7iIvox.d.ts} +3 -3
  51. package/dist/{retry-policy-CFhdtRzz.d.ts → retry-policy-BilV1ujH.d.ts} +1 -1
  52. package/dist/sdd/index.d.ts +8 -8
  53. package/dist/sdd/index.js +286 -105
  54. package/dist/sdd/index.js.map +1 -1
  55. package/dist/secret-vault-BAKpgFw_.d.ts +57 -0
  56. package/dist/{secret-vault-CxiVLbt1.d.ts → secret-vault-gkvEZZfE.d.ts} +43 -4
  57. package/dist/security/index.d.ts +6 -68
  58. package/dist/security/index.js +296 -95
  59. package/dist/security/index.js.map +1 -1
  60. package/dist/{selector-gIuhRTkN.d.ts → selector-Bc7eWtT3.d.ts} +1 -1
  61. package/dist/{session-event-bridge-DkvvrpDt.d.ts → session-event-bridge-D-araDEz.d.ts} +1 -1
  62. package/dist/{session-reader-KdfVwkKP.d.ts → session-reader-D7Dapswh.d.ts} +1 -1
  63. package/dist/storage/index.d.ts +112 -15
  64. package/dist/storage/index.js +491 -156
  65. package/dist/storage/index.js.map +1 -1
  66. package/dist/tools/index.d.ts +4 -2
  67. package/dist/tools/index.js.map +1 -1
  68. package/dist/types/index.d.ts +21 -21
  69. package/dist/types/index.js +1523 -450
  70. package/dist/types/index.js.map +1 -1
  71. package/dist/utils/index.d.ts +455 -407
  72. package/dist/utils/index.js +2191 -1203
  73. package/dist/utils/index.js.map +1 -1
  74. package/dist/{wstack-paths-CJjEwPXn.d.ts → wstack-paths-hOpNLmvf.d.ts} +2 -0
  75. package/package.json +1 -1
  76. package/skills/api-design/SKILL.md +1 -1
  77. package/skills/audit-log/SKILL.md +6 -6
  78. package/skills/bug-hunter/SKILL.md +5 -5
  79. package/skills/chimera/SKILL.md +4 -4
  80. package/skills/docker-deploy/SKILL.md +1 -1
  81. package/skills/git-flow/SKILL.md +3 -3
  82. package/skills/multi-agent/SKILL.md +3 -3
  83. package/skills/node-modern/SKILL.md +1 -0
  84. package/skills/observability/SKILL.md +2 -2
  85. package/skills/output-standards/SKILL.md +51 -28
  86. package/skills/refactor-planner/SKILL.md +3 -3
  87. package/skills/security-scanner/SKILL.md +4 -3
  88. package/skills/tech-stack/SKILL.md +1 -2
  89. package/dist/llm-selector-DzxuZnNz.d.ts +0 -58
  90. package/dist/secret-vault-BJDY28ev.d.ts +0 -25
@@ -1,8 +1,8 @@
1
1
  import { randomBytes, createCipheriv, createDecipheriv, randomUUID } from 'crypto';
2
2
  import * as fs from 'fs/promises';
3
- import * as path4 from 'path';
4
- import * as fs2 from 'fs';
3
+ import * as path6 from 'path';
5
4
  import * as os from 'os';
5
+ import * as fs2 from 'fs';
6
6
 
7
7
  // src/types/blocks.ts
8
8
  function isTextBlock(b) {
@@ -30,9 +30,9 @@ function asText(content) {
30
30
  return content.filter((b) => b.type === "text").map((b) => b.text).join("");
31
31
  }
32
32
  async function atomicWrite(targetPath, content, opts = {}) {
33
- const dir = path4.dirname(targetPath);
33
+ const dir = path6.dirname(targetPath);
34
34
  await fs.mkdir(dir, { recursive: true });
35
- const tmp = path4.join(dir, `.${path4.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
35
+ const tmp = path6.join(dir, `.${path6.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
36
36
  try {
37
37
  if (typeof content === "string") {
38
38
  await fs.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
@@ -85,17 +85,12 @@ async function renameWithRetry(from, to) {
85
85
  if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
86
86
  throw err;
87
87
  }
88
- await new Promise((resolve4) => setTimeout(resolve4, delays[i]));
88
+ await new Promise((resolve6) => setTimeout(resolve6, delays[i]));
89
89
  }
90
90
  }
91
91
  throw lastErr;
92
92
  }
93
93
 
94
- // src/utils/error.ts
95
- function toErrorMessage(err) {
96
- return err instanceof Error ? err.message : String(err);
97
- }
98
-
99
94
  // src/utils/term.ts
100
95
  var hasStdout = () => typeof process !== "undefined" && !!process.stdout;
101
96
  function isStdoutTTY() {
@@ -143,20 +138,75 @@ var color = {
143
138
  bgRed: wrap("41", "49"),
144
139
  bgGreen: wrap("42", "49")
145
140
  };
146
-
147
- // src/utils/string.ts
148
- function truncate(s, max) {
149
- return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
141
+ var MAX_DIGEST_CHARS = 4e3;
142
+ function createContextEvidenceState() {
143
+ return {
144
+ sessionGoals: [],
145
+ implicitFacts: [],
146
+ activeErrors: [],
147
+ toolCalls: [],
148
+ fileGraph: {},
149
+ repeatedReads: [],
150
+ updatedAt: Date.now()
151
+ };
150
152
  }
151
-
152
- // src/utils/expect-defined.ts
153
- function expectDefined(value, label) {
154
- if (value === null || value === void 0) {
155
- const err = new Error("Expected value to be defined");
156
- err.name = "ExpectDefinedError";
157
- throw err;
153
+ function buildContextEvidenceDigest(ctx) {
154
+ const state = ensureEvidence(ctx);
155
+ const lines = [];
156
+ if (state.currentIntent?.text) {
157
+ lines.push(`intent: ${state.currentIntent.text}`);
158
+ }
159
+ const goals = state.sessionGoals.slice(-3);
160
+ if (goals.length > 0) {
161
+ lines.push("session_goals:");
162
+ for (const goal of goals) lines.push(`- ${goal}`);
163
+ }
164
+ const activeErrors = state.activeErrors.slice(-5);
165
+ if (activeErrors.length > 0) {
166
+ lines.push("active_errors:");
167
+ for (const err of activeErrors) lines.push(`- ${err}`);
168
+ }
169
+ const files = Object.values(state.fileGraph).sort((a, b) => b.writes - a.writes || b.reads - a.reads || a.path.localeCompare(b.path)).slice(0, 12);
170
+ if (files.length > 0) {
171
+ lines.push("dependency_graph:");
172
+ for (const file of files) {
173
+ const actions = [
174
+ file.reads > 0 ? `read ${file.reads}x` : "",
175
+ file.writes > 0 ? `write ${file.writes}x` : ""
176
+ ].filter(Boolean).join(", ");
177
+ const refs = file.referenced ? "; referenced by assistant" : "";
178
+ const via = file.lastToolUseId ? `; last via ${file.lastToolUseId}` : "";
179
+ lines.push(`- ${file.path} (${actions || "seen"}${refs}${via})`);
180
+ }
158
181
  }
159
- return value;
182
+ const referenced = state.toolCalls.filter((tool) => tool.status === "referenced").slice(-10);
183
+ const recentSeen = state.toolCalls.filter((tool) => tool.status === "seen").slice(-5);
184
+ const trail = [...referenced, ...recentSeen];
185
+ if (trail.length > 0) {
186
+ lines.push("tool_trail:");
187
+ for (const tool of trail) {
188
+ const size = tool.outputTokens ? `; ~${tool.outputTokens} tokens` : "";
189
+ const filesText = tool.files.length > 0 ? `; files=${tool.files.slice(0, 4).join(", ")}` : "";
190
+ const symbolsText = tool.symbols.length > 0 ? `; symbols=${tool.symbols.slice(0, 4).join(", ")}` : "";
191
+ lines.push(
192
+ `- ${tool.toolUseId} ${tool.toolName} ${tool.status}: ${tool.summary}${filesText}${symbolsText}${size}`
193
+ );
194
+ }
195
+ }
196
+ const facts = state.implicitFacts.slice(-8);
197
+ if (facts.length > 0) {
198
+ lines.push("implicit_facts:");
199
+ for (const fact of facts) lines.push(`- ${fact}`);
200
+ }
201
+ const digest = lines.join("\n");
202
+ if (digest.length <= MAX_DIGEST_CHARS) return digest;
203
+ return `${digest.slice(0, MAX_DIGEST_CHARS)}... [+${digest.length - MAX_DIGEST_CHARS} chars]`;
204
+ }
205
+ function ensureEvidence(ctx) {
206
+ if (!ctx.contextEvidence) {
207
+ ctx.contextEvidence = createContextEvidenceState();
208
+ }
209
+ return ctx.contextEvidence;
160
210
  }
161
211
 
162
212
  // src/utils/deep-merge.ts
@@ -218,173 +268,179 @@ function deepMerge(base, patch, options = {}) {
218
268
  return out;
219
269
  }
220
270
 
221
- // src/utils/tool-output-serializer.ts
222
- function createToolOutputSerializer(opts = {}) {
223
- const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
224
- function serialize(value) {
225
- if (typeof value === "string") return value;
226
- if (value === null || value === void 0) return "";
227
- if (typeof value === "object") {
228
- if (Array.isArray(value)) return value.map(serialize).join("\n");
229
- if ("text" in value) {
230
- const t = value.text;
231
- return typeof t === "string" ? t : JSON.stringify(value, null, 2);
232
- }
233
- try {
234
- return JSON.stringify(value, null, 2);
235
- } catch {
236
- return String(value);
237
- }
271
+ // src/utils/error.ts
272
+ function toErrorMessage(err) {
273
+ return err instanceof Error ? err.message : String(err);
274
+ }
275
+
276
+ // src/utils/expect-defined.ts
277
+ function expectDefined(value, label) {
278
+ if (value === null || value === void 0) {
279
+ const err = new Error("Expected value to be defined");
280
+ err.name = "ExpectDefinedError";
281
+ throw err;
282
+ }
283
+ return value;
284
+ }
285
+
286
+ // src/utils/json-schema-validate.ts
287
+ function validateAgainstSchema(value, schema) {
288
+ const errors = [];
289
+ walk(value, schema, "", errors);
290
+ return { ok: errors.length === 0, errors };
291
+ }
292
+ function walk(value, schema, path10, errors) {
293
+ if (schema.enum !== void 0) {
294
+ if (!schema.enum.some((e) => deepEqual(e, value))) {
295
+ errors.push({
296
+ path: path10 || "<root>",
297
+ message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
298
+ });
299
+ return;
238
300
  }
239
- return String(value);
240
301
  }
241
- function enforceCap(text, remainingBudget) {
242
- if (remainingBudget <= 0) {
243
- return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
302
+ if (typeof schema.type === "string") {
303
+ if (!checkType(value, schema.type)) {
304
+ errors.push({
305
+ path: path10 || "<root>",
306
+ message: `expected ${schema.type}, got ${describeType(value)}`
307
+ });
308
+ return;
244
309
  }
245
- const textBytes = Buffer.byteLength(text, "utf8");
246
- if (textBytes <= remainingBudget) {
247
- return { text, newBudget: remainingBudget - textBytes };
310
+ }
311
+ if (schema.type === "object" && isPlainObject(value)) {
312
+ const obj = value;
313
+ for (const req of schema.required ?? []) {
314
+ if (!(req in obj)) {
315
+ errors.push({ path: joinPath(path10, req), message: "required property missing" });
316
+ }
248
317
  }
249
- const marker = `
250
- \u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
251
- `;
252
- const markerBytes = Buffer.byteLength(marker, "utf8");
253
- const available = remainingBudget - markerBytes;
254
- if (available <= 0) {
255
- return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
318
+ if (schema.properties) {
319
+ for (const [key, subSchema] of Object.entries(schema.properties)) {
320
+ if (key in obj) {
321
+ walk(obj[key], subSchema, joinPath(path10, key), errors);
322
+ }
323
+ }
256
324
  }
257
- const half = Math.floor(available / 2);
258
- const first = text.slice(0, half);
259
- const second = text.slice(text.length - half);
260
- return { text: `${first}${marker}${second}`, newBudget: 0 };
261
- }
262
- return { serialize, enforceCap, capBytes };
263
- }
264
-
265
- // src/utils/token-estimate.ts
266
- var RoughTokenEstimate = (text, charsPerToken = 3.5) => Math.max(1, Math.ceil(text.length / charsPerToken));
267
- var CALIBRATION_GLOBAL_KEY = "__global__";
268
- var _cals = /* @__PURE__ */ new Map();
269
- function calState(key) {
270
- let state = _cals.get(key);
271
- if (!state) {
272
- state = { ratio: 1, count: 0, prevEst: 0 };
273
- _cals.set(key, state);
274
325
  }
275
- return state;
276
- }
277
- var ESTIMATE_CACHE = /* @__PURE__ */ new Map();
278
- var ESTIMATE_CACHE_MAX_SIZE = 1e4;
279
- function getCachedEstimate(key, compute) {
280
- const existing = ESTIMATE_CACHE.get(key);
281
- if (existing !== void 0) return existing;
282
- if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
283
- let evicted = 0;
284
- const maxEvict = Math.floor(ESTIMATE_CACHE_MAX_SIZE / 4);
285
- for (const k of ESTIMATE_CACHE.keys()) {
286
- if (evicted >= maxEvict) break;
287
- ESTIMATE_CACHE.delete(k);
288
- evicted++;
326
+ if (schema.type === "array" && Array.isArray(value) && schema.items) {
327
+ for (let i = 0; i < value.length; i++) {
328
+ walk(value[i], schema.items, `${path10}[${i}]`, errors);
289
329
  }
290
330
  }
291
- const estimate = compute(key);
292
- ESTIMATE_CACHE.set(key, estimate);
293
- return estimate;
294
331
  }
295
- function estimateToolInputTokens(input) {
296
- if (typeof input === "string") return RoughTokenEstimate(input);
297
- if (input === null || typeof input !== "object") {
298
- return RoughTokenEstimate(String(input));
332
+ function checkType(value, type) {
333
+ switch (type) {
334
+ case "string":
335
+ return typeof value === "string";
336
+ case "number":
337
+ return typeof value === "number" && !Number.isNaN(value);
338
+ case "integer":
339
+ return typeof value === "number" && Number.isInteger(value);
340
+ case "boolean":
341
+ return typeof value === "boolean";
342
+ case "null":
343
+ return value === null;
344
+ case "array":
345
+ return Array.isArray(value);
346
+ case "object":
347
+ return isPlainObject(value);
348
+ default:
349
+ return true;
299
350
  }
300
- return getCachedEstimate(JSON.stringify(input), (key) => RoughTokenEstimate(key));
301
351
  }
302
- function estimateToolResultTokens(content) {
303
- if (typeof content === "string") return RoughTokenEstimate(content);
304
- return getCachedEstimate(JSON.stringify(content), (key) => RoughTokenEstimate(key));
352
+ function isPlainObject(v) {
353
+ return typeof v === "object" && v !== null && !Array.isArray(v);
305
354
  }
306
- function estimateTextTokens(text) {
307
- return RoughTokenEstimate(text);
355
+ function describeType(v) {
356
+ if (v === null) return "null";
357
+ if (Array.isArray(v)) return "array";
358
+ return typeof v;
308
359
  }
309
- function computeMessageTokens(msg) {
310
- if (typeof msg.content === "string") return estimateTextTokens(msg.content);
311
- let total = 0;
312
- for (const b of msg.content) {
313
- if (b.type === "text") total += estimateTextTokens(b.text);
314
- else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
315
- else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
316
- else total += RoughTokenEstimate(JSON.stringify(b));
317
- }
318
- return total;
360
+ function joinPath(parent, key) {
361
+ if (!parent) return key;
362
+ return `${parent}.${key}`;
319
363
  }
320
- function estimateMessageTokens(messages) {
321
- let total = 0;
322
- for (const m of messages) {
323
- if (typeof m._estTokens === "number" && m._estTokens > 0) {
324
- total += m._estTokens;
325
- continue;
326
- }
327
- total += computeMessageTokens(m);
364
+ function deepEqual(a, b) {
365
+ if (a === b) return true;
366
+ if (typeof a !== typeof b) return false;
367
+ if (a === null || b === null) return a === b;
368
+ if (Array.isArray(a) && Array.isArray(b)) {
369
+ return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
328
370
  }
329
- return total;
330
- }
331
- function estimateToolDefTokens(tool) {
332
- const cached = tool._estDefTokens;
333
- if (typeof cached === "number" && cached > 0) return cached;
334
- return RoughTokenEstimate(tool.name) + RoughTokenEstimate(tool.description ?? "") + RoughTokenEstimate(JSON.stringify(tool.inputSchema));
371
+ if (typeof a === "object" && typeof b === "object") {
372
+ const ak = Object.keys(a);
373
+ const bk = Object.keys(b);
374
+ if (ak.length !== bk.length) return false;
375
+ return ak.every(
376
+ (k) => deepEqual(a[k], b[k])
377
+ );
378
+ }
379
+ return false;
335
380
  }
336
- function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
337
- let messagesTokens = 0;
338
- if (typeof messages === "string") {
339
- messagesTokens = RoughTokenEstimate(messages);
340
- } else if (Array.isArray(messages)) {
341
- for (const m of messages) {
342
- if (typeof m === "object" && m !== null && "content" in m) {
343
- const cached = m._estTokens;
344
- if (typeof cached === "number" && cached > 0) {
345
- messagesTokens += cached;
346
- continue;
347
- }
348
- const content = m.content;
349
- if (typeof content === "string") {
350
- messagesTokens += RoughTokenEstimate(content);
351
- } else if (Array.isArray(content)) {
352
- for (const b of content) {
353
- if (typeof b === "object" && b !== null) {
354
- if (b.type === "text") {
355
- messagesTokens += RoughTokenEstimate(b.text);
356
- } else {
357
- messagesTokens += RoughTokenEstimate(JSON.stringify(b));
358
- }
359
- }
360
- }
361
- }
362
- }
363
- }
381
+
382
+ // src/utils/merge-models-payload.ts
383
+ function mergeModelsPayload(base, overlay) {
384
+ const out = {};
385
+ for (const [id, provider] of Object.entries(base)) {
386
+ out[id] = cloneProvider(provider);
364
387
  }
365
- let systemTokens = 0;
366
- if (typeof systemPrompt === "string") {
367
- systemTokens = RoughTokenEstimate(systemPrompt);
368
- } else if (Array.isArray(systemPrompt)) {
369
- for (const b of systemPrompt) {
370
- if (typeof b === "object" && b !== null && b.type === "text") {
371
- systemTokens += RoughTokenEstimate(b.text);
372
- }
373
- }
388
+ for (const [id, ovProvider] of Object.entries(overlay)) {
389
+ const existing = out[id];
390
+ out[id] = existing ? mergeProvider(existing, ovProvider) : cloneProvider(ovProvider);
374
391
  }
375
- let toolsTokens = 0;
376
- for (const t of tools) {
377
- toolsTokens += estimateToolDefTokens(t);
392
+ return out;
393
+ }
394
+ function mergeProvider(base, overlay) {
395
+ const models = {};
396
+ for (const [mid, m] of Object.entries(base.models ?? {})) {
397
+ models[mid] = { ...m };
398
+ }
399
+ for (const [mid, ovModel] of Object.entries(overlay.models ?? {})) {
400
+ const existing = models[mid];
401
+ models[mid] = existing ? mergeModel(existing, ovModel) : { ...ovModel };
378
402
  }
379
- const total = messagesTokens + systemTokens + toolsTokens;
380
- calState(calibrationKey).prevEst = total;
381
403
  return {
382
- messages: messagesTokens,
383
- systemPrompt: systemTokens,
384
- tools: toolsTokens,
385
- total
404
+ ...base,
405
+ // Overlay scalar fields win when explicitly provided; otherwise keep base.
406
+ ...stripUndefined({
407
+ id: overlay.id,
408
+ name: overlay.name,
409
+ npm: overlay.npm,
410
+ api: overlay.api,
411
+ env: overlay.env,
412
+ doc: overlay.doc
413
+ }),
414
+ models
386
415
  };
387
416
  }
417
+ function mergeModel(base, overlay) {
418
+ const merged = { ...base, ...overlay };
419
+ if (base.limit || overlay.limit) {
420
+ merged.limit = { ...base.limit, ...overlay.limit };
421
+ }
422
+ if (base.cost || overlay.cost) {
423
+ merged.cost = { ...base.cost, ...overlay.cost };
424
+ }
425
+ if (base.modalities || overlay.modalities) {
426
+ merged.modalities = { ...base.modalities, ...overlay.modalities };
427
+ }
428
+ return merged;
429
+ }
430
+ function cloneProvider(p) {
431
+ const models = {};
432
+ for (const [mid, m] of Object.entries(p.models ?? {})) {
433
+ models[mid] = { ...m };
434
+ }
435
+ return { ...p, models };
436
+ }
437
+ function stripUndefined(obj) {
438
+ const out = {};
439
+ for (const [k, v] of Object.entries(obj)) {
440
+ if (v !== void 0) out[k] = v;
441
+ }
442
+ return out;
443
+ }
388
444
 
389
445
  // src/utils/message-invariants.ts
390
446
  function repairToolUseAdjacency(messages) {
@@ -478,102 +534,6 @@ function isEmptyMessage(msg) {
478
534
  return msg.content.length === 0;
479
535
  }
480
536
 
481
- // src/utils/json-schema-validate.ts
482
- function validateAgainstSchema(value, schema) {
483
- const errors = [];
484
- walk(value, schema, "", errors);
485
- return { ok: errors.length === 0, errors };
486
- }
487
- function walk(value, schema, path7, errors) {
488
- if (schema.enum !== void 0) {
489
- if (!schema.enum.some((e) => deepEqual(e, value))) {
490
- errors.push({
491
- path: path7 || "<root>",
492
- message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
493
- });
494
- return;
495
- }
496
- }
497
- if (typeof schema.type === "string") {
498
- if (!checkType(value, schema.type)) {
499
- errors.push({
500
- path: path7 || "<root>",
501
- message: `expected ${schema.type}, got ${describeType(value)}`
502
- });
503
- return;
504
- }
505
- }
506
- if (schema.type === "object" && isPlainObject(value)) {
507
- const obj = value;
508
- for (const req of schema.required ?? []) {
509
- if (!(req in obj)) {
510
- errors.push({ path: joinPath(path7, req), message: "required property missing" });
511
- }
512
- }
513
- if (schema.properties) {
514
- for (const [key, subSchema] of Object.entries(schema.properties)) {
515
- if (key in obj) {
516
- walk(obj[key], subSchema, joinPath(path7, key), errors);
517
- }
518
- }
519
- }
520
- }
521
- if (schema.type === "array" && Array.isArray(value) && schema.items) {
522
- for (let i = 0; i < value.length; i++) {
523
- walk(value[i], schema.items, `${path7}[${i}]`, errors);
524
- }
525
- }
526
- }
527
- function checkType(value, type) {
528
- switch (type) {
529
- case "string":
530
- return typeof value === "string";
531
- case "number":
532
- return typeof value === "number" && !Number.isNaN(value);
533
- case "integer":
534
- return typeof value === "number" && Number.isInteger(value);
535
- case "boolean":
536
- return typeof value === "boolean";
537
- case "null":
538
- return value === null;
539
- case "array":
540
- return Array.isArray(value);
541
- case "object":
542
- return isPlainObject(value);
543
- default:
544
- return true;
545
- }
546
- }
547
- function isPlainObject(v) {
548
- return typeof v === "object" && v !== null && !Array.isArray(v);
549
- }
550
- function describeType(v) {
551
- if (v === null) return "null";
552
- if (Array.isArray(v)) return "array";
553
- return typeof v;
554
- }
555
- function joinPath(parent, key) {
556
- if (!parent) return key;
557
- return `${parent}.${key}`;
558
- }
559
- function deepEqual(a, b) {
560
- if (a === b) return true;
561
- if (typeof a !== typeof b) return false;
562
- if (a === null || b === null) return a === b;
563
- if (Array.isArray(a) && Array.isArray(b)) {
564
- return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
565
- }
566
- if (typeof a === "object" && typeof b === "object") {
567
- const ak = Object.keys(a);
568
- const bk = Object.keys(b);
569
- if (ak.length !== bk.length) return false;
570
- return ak.every(
571
- (k) => deepEqual(a[k], b[k])
572
- );
573
- }
574
- return false;
575
- }
576
-
577
537
  // src/utils/regex-guard.ts
578
538
  var MAX_PATTERN_LEN = 512;
579
539
  var DANGEROUS_PATTERNS = [
@@ -599,78 +559,858 @@ function compileUserRegex(pattern, flags) {
599
559
  reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
600
560
  };
601
561
  }
602
- }
603
- try {
604
- return { ok: true, regex: new RegExp(pattern, flags) };
605
- } catch (err) {
606
- return {
607
- ok: false,
608
- reason: err instanceof Error ? err.message : "invalid regex"
609
- };
610
- }
562
+ }
563
+ try {
564
+ return { ok: true, regex: new RegExp(pattern, flags) };
565
+ } catch (err) {
566
+ return {
567
+ ok: false,
568
+ reason: err instanceof Error ? err.message : "invalid regex"
569
+ };
570
+ }
571
+ }
572
+
573
+ // src/utils/string.ts
574
+ function truncate(s, max) {
575
+ return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
576
+ }
577
+
578
+ // src/utils/tool-wire-compact.ts
579
+ var TOOL_DESCRIPTION_MAX_CHARS = 640;
580
+ var SCHEMA_DESCRIPTION_MAX_CHARS = 180;
581
+ var compactCache = /* @__PURE__ */ new WeakMap();
582
+ function compactToolDefinitionForWire(tool, opts = {}) {
583
+ const useDefaultOptions = opts.descriptionMaxChars === void 0 && opts.schemaDescriptionMaxChars === void 0;
584
+ if (useDefaultOptions && typeof tool === "object" && tool !== null) {
585
+ const cached = compactCache.get(tool);
586
+ if (cached) return cached;
587
+ }
588
+ const compact = {
589
+ name: tool.name,
590
+ description: compactDescription(
591
+ tool.description ?? "",
592
+ opts.descriptionMaxChars ?? TOOL_DESCRIPTION_MAX_CHARS
593
+ ),
594
+ inputSchema: compactSchemaDescriptions(
595
+ tool.inputSchema,
596
+ opts.schemaDescriptionMaxChars ?? SCHEMA_DESCRIPTION_MAX_CHARS
597
+ )
598
+ };
599
+ if (useDefaultOptions && typeof tool === "object" && tool !== null) {
600
+ compactCache.set(tool, compact);
601
+ }
602
+ return compact;
603
+ }
604
+ function compactSchemaDescriptions(schema, maxDescriptionChars = SCHEMA_DESCRIPTION_MAX_CHARS) {
605
+ const compact = compactSchemaNode(schema, maxDescriptionChars);
606
+ return isRecord(compact) ? compact : { type: "object", properties: {} };
607
+ }
608
+ function compactSchemaNode(node, maxDescriptionChars) {
609
+ if (Array.isArray(node)) {
610
+ return node.map((item) => compactSchemaNode(item, maxDescriptionChars));
611
+ }
612
+ if (!isRecord(node)) return node;
613
+ const out = {};
614
+ for (const [key, value] of Object.entries(node)) {
615
+ if (key === "description" && typeof value === "string") {
616
+ out[key] = compactDescription(value, maxDescriptionChars);
617
+ } else {
618
+ out[key] = compactSchemaNode(value, maxDescriptionChars);
619
+ }
620
+ }
621
+ return out;
622
+ }
623
+ function compactDescription(text, maxChars) {
624
+ const normalized = text.replace(/\s+/g, " ").trim();
625
+ if (normalized.length <= maxChars) return normalized;
626
+ if (maxChars <= 20) return normalized.slice(0, maxChars);
627
+ const hardLimit = maxChars - 12;
628
+ const boundary = findSemanticBoundary(normalized, hardLimit);
629
+ const head = normalized.slice(0, boundary > 0 ? boundary : hardLimit).trimEnd();
630
+ return `${head} ...`;
631
+ }
632
+ function findSemanticBoundary(text, limit) {
633
+ const punctuation = Math.max(
634
+ text.lastIndexOf(". ", limit),
635
+ text.lastIndexOf("; ", limit),
636
+ text.lastIndexOf(": ", limit)
637
+ );
638
+ if (punctuation >= Math.floor(limit * 0.45)) return punctuation + 1;
639
+ const comma = text.lastIndexOf(", ", limit);
640
+ if (comma >= Math.floor(limit * 0.6)) return comma + 1;
641
+ const space = text.lastIndexOf(" ", limit);
642
+ return space >= Math.floor(limit * 0.6) ? space : limit;
643
+ }
644
+ function isRecord(value) {
645
+ return !!value && typeof value === "object" && !Array.isArray(value);
646
+ }
647
+
648
+ // src/utils/token-estimate.ts
649
+ var RoughTokenEstimate = (text, charsPerToken = 3.5) => Math.max(1, Math.ceil(text.length / charsPerToken));
650
+ var CALIBRATION_GLOBAL_KEY = "__global__";
651
+ var _cals = /* @__PURE__ */ new Map();
652
+ function calState(key) {
653
+ let state = _cals.get(key);
654
+ if (!state) {
655
+ state = { ratio: 1, count: 0, prevEst: 0 };
656
+ _cals.set(key, state);
657
+ }
658
+ return state;
659
+ }
660
+ var ESTIMATE_CACHE = /* @__PURE__ */ new Map();
661
+ var ESTIMATE_CACHE_MAX_SIZE = 1e4;
662
+ function getCachedEstimate(key, compute) {
663
+ const existing = ESTIMATE_CACHE.get(key);
664
+ if (existing !== void 0) return existing;
665
+ if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
666
+ for (const k of ESTIMATE_CACHE.keys()) {
667
+ if (ESTIMATE_CACHE.size <= Math.floor(ESTIMATE_CACHE_MAX_SIZE / 2)) break;
668
+ ESTIMATE_CACHE.delete(k);
669
+ }
670
+ }
671
+ const estimate = compute(key);
672
+ ESTIMATE_CACHE.set(key, estimate);
673
+ return estimate;
674
+ }
675
+ function estimateToolInputTokens(input) {
676
+ if (typeof input === "string") return RoughTokenEstimate(input);
677
+ if (input === null || typeof input !== "object") {
678
+ return RoughTokenEstimate(String(input));
679
+ }
680
+ return getCachedEstimate(JSON.stringify(input), (key) => RoughTokenEstimate(key));
681
+ }
682
+ function estimateToolResultTokens(content) {
683
+ if (typeof content === "string") return RoughTokenEstimate(content);
684
+ return getCachedEstimate(JSON.stringify(content), (key) => RoughTokenEstimate(key));
685
+ }
686
+ function estimateTextTokens(text) {
687
+ return RoughTokenEstimate(text);
688
+ }
689
+ function computeMessageTokens(msg) {
690
+ if (typeof msg.content === "string") return estimateTextTokens(msg.content);
691
+ let total = 0;
692
+ for (const b of msg.content) {
693
+ if (b.type === "text") total += estimateTextTokens(b.text);
694
+ else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
695
+ else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
696
+ else total += RoughTokenEstimate(JSON.stringify(b));
697
+ }
698
+ return total;
699
+ }
700
+ function estimateMessageTokens(messages) {
701
+ let total = 0;
702
+ for (const m of messages) {
703
+ if (typeof m._estTokens === "number" && m._estTokens > 0) {
704
+ total += m._estTokens;
705
+ continue;
706
+ }
707
+ total += computeMessageTokens(m);
708
+ }
709
+ return total;
710
+ }
711
+ function estimateToolDefTokens(tool) {
712
+ const cached = tool._estDefTokens;
713
+ if (typeof cached === "number" && cached > 0) return cached;
714
+ const compact = compactToolDefinitionForWire(tool);
715
+ return RoughTokenEstimate(tool.name) + RoughTokenEstimate(compact.description) + RoughTokenEstimate(JSON.stringify(compact.inputSchema));
716
+ }
717
+ function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
718
+ let messagesTokens = 0;
719
+ if (typeof messages === "string") {
720
+ messagesTokens = RoughTokenEstimate(messages);
721
+ } else if (Array.isArray(messages)) {
722
+ for (const m of messages) {
723
+ if (typeof m === "object" && m !== null && "content" in m) {
724
+ const cached = m._estTokens;
725
+ if (typeof cached === "number" && cached > 0) {
726
+ messagesTokens += cached;
727
+ continue;
728
+ }
729
+ const content = m.content;
730
+ if (typeof content === "string") {
731
+ messagesTokens += RoughTokenEstimate(content);
732
+ } else if (Array.isArray(content)) {
733
+ for (const b of content) {
734
+ if (typeof b === "object" && b !== null) {
735
+ if (b.type === "text") {
736
+ messagesTokens += RoughTokenEstimate(b.text);
737
+ } else {
738
+ messagesTokens += RoughTokenEstimate(JSON.stringify(b));
739
+ }
740
+ }
741
+ }
742
+ }
743
+ }
744
+ }
745
+ }
746
+ let systemTokens = 0;
747
+ if (typeof systemPrompt === "string") {
748
+ systemTokens = RoughTokenEstimate(systemPrompt);
749
+ } else if (Array.isArray(systemPrompt)) {
750
+ for (const b of systemPrompt) {
751
+ if (typeof b === "object" && b !== null && b.type === "text") {
752
+ systemTokens += RoughTokenEstimate(b.text);
753
+ }
754
+ }
755
+ }
756
+ let toolsTokens = 0;
757
+ for (const t of tools) {
758
+ toolsTokens += estimateToolDefTokens(t);
759
+ }
760
+ const total = messagesTokens + systemTokens + toolsTokens;
761
+ calState(calibrationKey).prevEst = total;
762
+ return {
763
+ messages: messagesTokens,
764
+ systemPrompt: systemTokens,
765
+ tools: toolsTokens,
766
+ total
767
+ };
768
+ }
769
+
770
+ // src/utils/tool-output-serializer.ts
771
+ var DEFAULT_LIST_LIMIT = 500;
772
+ var LOG_ENTRY_LIMIT = 200;
773
+ var INLINE_LIMIT = 240;
774
+ var GREP_FILE_LIMIT = 80;
775
+ var GREP_MATCHES_PER_FILE = 3;
776
+ var DIFF_INLINE_LINE_LIMIT = 260;
777
+ var DIFF_HUNK_LIMIT = 8;
778
+ var DIFF_HUNK_CONTEXT = 14;
779
+ function createToolOutputSerializer(opts = {}) {
780
+ const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
781
+ function serialize(value, context = {}) {
782
+ if (typeof value === "string") return value;
783
+ if (value === null || value === void 0) return "";
784
+ if (typeof value === "object") {
785
+ if (Array.isArray(value)) return value.map((item) => serialize(item)).join("\n");
786
+ if (context.toolName) {
787
+ const compact = renderToolObject(context.toolName, value, context.input);
788
+ if (compact !== void 0) return compact;
789
+ return renderGenericToolObject(context.toolName, value);
790
+ }
791
+ if ("text" in value) {
792
+ const t = value.text;
793
+ return typeof t === "string" ? t : JSON.stringify(value, null, 2);
794
+ }
795
+ try {
796
+ return JSON.stringify(value, null, 2);
797
+ } catch {
798
+ return String(value);
799
+ }
800
+ }
801
+ return String(value);
802
+ }
803
+ function enforceCap(text, remainingBudget) {
804
+ if (remainingBudget <= 0) {
805
+ return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
806
+ }
807
+ const textBytes = Buffer.byteLength(text, "utf8");
808
+ if (textBytes <= remainingBudget) {
809
+ return { text, newBudget: remainingBudget - textBytes };
810
+ }
811
+ const marker = `
812
+ \u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
813
+ `;
814
+ const markerBytes = Buffer.byteLength(marker, "utf8");
815
+ const available = remainingBudget - markerBytes;
816
+ if (available <= 0) {
817
+ return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
818
+ }
819
+ const half = Math.floor(available / 2);
820
+ const first = text.slice(0, half);
821
+ const second = text.slice(text.length - half);
822
+ return { text: `${first}${marker}${second}`, newBudget: 0 };
823
+ }
824
+ return { serialize, enforceCap, capBytes };
825
+ }
826
+ function renderToolObject(toolName, obj, input) {
827
+ if (toolName === "read" && typeof obj["text"] === "string") {
828
+ return joinSections([
829
+ renderHeader(
830
+ `read: ${stringFromInput(input, "path") ?? stringField(obj, "path") ?? "<unknown>"}`,
831
+ {
832
+ offset: numberFromInput(input, "offset"),
833
+ limit: numberFromInput(input, "limit"),
834
+ total_lines: obj["total_lines"],
835
+ encoding: obj["encoding"],
836
+ truncated: obj["truncated"],
837
+ cached: obj["cached"],
838
+ note: obj["note"]
839
+ }
840
+ ),
841
+ obj["text"]
842
+ ]);
843
+ }
844
+ if (toolName === "grep" && Array.isArray(obj["matches"])) {
845
+ const matches = stringArrayField(obj, "matches");
846
+ return joinSections([
847
+ renderHeader(`grep: ${stringFromInput(input, "pattern") ?? "<pattern>"}`, {
848
+ path: stringFromInput(input, "path"),
849
+ glob: stringFromInput(input, "glob"),
850
+ mode: stringFromInput(input, "output_mode"),
851
+ count: obj["count"],
852
+ shown: matches.length,
853
+ truncated: obj["truncated"],
854
+ used: obj["used"]
855
+ }),
856
+ renderGrepMatches(matches, stringFromInput(input, "output_mode"))
857
+ ]);
858
+ }
859
+ if (toolName === "patch" && Array.isArray(obj["files"])) {
860
+ const files = stringArrayField(obj, "files");
861
+ return joinSections([
862
+ renderHeader("patch", {
863
+ applied: obj["applied"],
864
+ rejected: obj["rejected"],
865
+ files: files.length,
866
+ dry_run: obj["dry_run"]
867
+ }),
868
+ typeof obj["message"] === "string" ? `message:
869
+ ${obj["message"]}` : void 0,
870
+ files.length > 0 ? `files:
871
+ ${renderStringList(files)}` : void 0
872
+ ]);
873
+ }
874
+ if (toolName === "glob" && Array.isArray(obj["files"])) {
875
+ const files = stringArrayField(obj, "files");
876
+ return joinSections([
877
+ renderHeader(
878
+ `${toolName}: ${stringFromInput(input, "pattern") ?? stringFromInput(input, "files") ?? stringFromInput(input, "path") ?? ""}`.trim(),
879
+ {
880
+ path: stringFromInput(input, "path"),
881
+ files: files.length,
882
+ truncated: obj["truncated"]
883
+ }
884
+ ),
885
+ renderStringList(files, "(no files)")
886
+ ]);
887
+ }
888
+ if (toolName === "tree" && typeof obj["tree"] === "string") {
889
+ return joinSections([
890
+ renderHeader(
891
+ `tree: ${stringField(obj, "path") ?? stringFromInput(input, "path") ?? "<cwd>"}`,
892
+ {
893
+ total_files: obj["total_files"],
894
+ total_dirs: obj["total_dirs"],
895
+ truncated: obj["truncated"]
896
+ }
897
+ ),
898
+ obj["tree"]
899
+ ]);
900
+ }
901
+ if (toolName === "fetch" && typeof obj["content"] === "string") {
902
+ return joinSections([
903
+ renderHeader(
904
+ `fetch: ${stringField(obj, "url") ?? stringFromInput(input, "url") ?? "<url>"}`,
905
+ {
906
+ status: obj["status"],
907
+ content_type: obj["content_type"]
908
+ }
909
+ ),
910
+ obj["content"]
911
+ ]);
912
+ }
913
+ if (toolName === "replace" && Array.isArray(obj["results"])) {
914
+ const results = obj["results"].filter(isRecord2);
915
+ const sections = [
916
+ renderHeader("replace", {
917
+ files_modified: obj["files_modified"],
918
+ total_replacements: obj["total_replacements"],
919
+ dry_run: obj["dry_run"]
920
+ })
921
+ ];
922
+ for (const r of results.slice(0, DEFAULT_LIST_LIMIT)) {
923
+ sections.push(
924
+ joinSections([
925
+ renderHeader(`file: ${stringField(r, "path") ?? "<unknown>"}`, {
926
+ replacements: r["replacements"]
927
+ }),
928
+ typeof r["diff"] === "string" ? r["diff"] : void 0
929
+ ])
930
+ );
931
+ }
932
+ if (results.length > DEFAULT_LIST_LIMIT) {
933
+ sections.push(`[serializer omitted ${results.length - DEFAULT_LIST_LIMIT} result item(s)]`);
934
+ }
935
+ return joinSections(sections);
936
+ }
937
+ if (typeof obj["diff"] === "string") {
938
+ const diff = obj["diff"];
939
+ return joinSections([
940
+ renderHeader(toolName, {
941
+ path: obj["path"],
942
+ replacements: obj["replacements"],
943
+ bytes_written: obj["bytes_written"],
944
+ created: obj["created"],
945
+ note: obj["note"],
946
+ files: Array.isArray(obj["files"]) ? obj["files"].length : void 0,
947
+ truncated: obj["truncated"],
948
+ mode: obj["mode"]
949
+ }),
950
+ compactDiff(diff)
951
+ ]);
952
+ }
953
+ if (toolName === "test" && typeof obj["output"] === "string") {
954
+ return renderTestOutput(obj, input);
955
+ }
956
+ if ((toolName === "typecheck" || toolName === "lint" || toolName === "format") && typeof obj["output"] === "string") {
957
+ return renderVerifierOutput(toolName, obj, input);
958
+ }
959
+ if (hasCommandOutputShape(obj)) {
960
+ return renderCommandOutput(toolName, obj, input);
961
+ }
962
+ if (toolName === "json" && typeof obj["formatted"] === "string") {
963
+ return joinSections([
964
+ renderHeader("json", {
965
+ type: obj["type"],
966
+ keys: Array.isArray(obj["keys"]) ? obj["keys"].length : void 0,
967
+ query: stringFromInput(input, "query"),
968
+ error: obj["error"]
969
+ }),
970
+ obj["formatted"]
971
+ ]);
972
+ }
973
+ if (toolName === "logs" && Array.isArray(obj["entries"])) {
974
+ const entries = obj["entries"].filter(isRecord2);
975
+ const lines = entries.slice(0, LOG_ENTRY_LIMIT).map((entry) => {
976
+ const ts = stringField(entry, "timestamp") ?? "";
977
+ const level = stringField(entry, "level") ?? "info";
978
+ const message = stringField(entry, "message") ?? "";
979
+ const source = stringField(entry, "source");
980
+ return [ts, level, source, message].filter(Boolean).join(" ");
981
+ });
982
+ if (entries.length > LOG_ENTRY_LIMIT) {
983
+ lines.push(`[serializer omitted ${entries.length - LOG_ENTRY_LIMIT} log entry item(s)]`);
984
+ }
985
+ return joinSections([
986
+ renderHeader(`logs: ${stringField(obj, "source") ?? "<source>"}`, {
987
+ total: obj["total"],
988
+ shown: Math.min(entries.length, LOG_ENTRY_LIMIT),
989
+ truncated: obj["truncated"],
990
+ stream_mode: obj["stream_mode"]
991
+ }),
992
+ lines.length > 0 ? lines.join("\n") : "(no log entries)"
993
+ ]);
994
+ }
995
+ if (toolName === "audit" && Array.isArray(obj["vulnerabilities"])) {
996
+ const vulns = obj["vulnerabilities"].filter(isRecord2);
997
+ const lines = vulns.slice(0, DEFAULT_LIST_LIMIT).map((v) => {
998
+ const severity = stringField(v, "severity") ?? "unknown";
999
+ const pkg = stringField(v, "package") ?? "<package>";
1000
+ const title = stringField(v, "title") ?? "";
1001
+ const url = stringField(v, "url");
1002
+ return [severity, pkg, title, url].filter(Boolean).join(" | ");
1003
+ });
1004
+ if (vulns.length > DEFAULT_LIST_LIMIT) {
1005
+ lines.push(`[serializer omitted ${vulns.length - DEFAULT_LIST_LIMIT} vulnerability item(s)]`);
1006
+ }
1007
+ return joinSections([
1008
+ renderHeader("audit", {
1009
+ exit_code: obj["exit_code"],
1010
+ total: obj["total"],
1011
+ summary: obj["summary"],
1012
+ truncated: obj["truncated"]
1013
+ }),
1014
+ lines.length > 0 ? lines.join("\n") : stringField(obj, "output")
1015
+ ]);
1016
+ }
1017
+ if (toolName === "outdated" && Array.isArray(obj["packages"])) {
1018
+ const packages = obj["packages"].filter(isRecord2);
1019
+ const lines = packages.slice(0, DEFAULT_LIST_LIMIT).map(
1020
+ (p) => [
1021
+ stringField(p, "name") ?? "<package>",
1022
+ `current=${stringField(p, "current") ?? "unknown"}`,
1023
+ `wanted=${stringField(p, "wanted") ?? "unknown"}`,
1024
+ `latest=${stringField(p, "latest") ?? "unknown"}`,
1025
+ stringField(p, "type")
1026
+ ].filter(Boolean).join(" | ")
1027
+ );
1028
+ if (packages.length > DEFAULT_LIST_LIMIT) {
1029
+ lines.push(`[serializer omitted ${packages.length - DEFAULT_LIST_LIMIT} package item(s)]`);
1030
+ }
1031
+ return joinSections([
1032
+ renderHeader("outdated", {
1033
+ exit_code: obj["exit_code"],
1034
+ total: obj["total"],
1035
+ truncated: obj["truncated"]
1036
+ }),
1037
+ lines.length > 0 ? lines.join("\n") : stringField(obj, "output")
1038
+ ]);
1039
+ }
1040
+ return void 0;
611
1041
  }
612
-
613
- // src/utils/merge-models-payload.ts
614
- function mergeModelsPayload(base, overlay) {
615
- const out = {};
616
- for (const [id, provider] of Object.entries(base)) {
617
- out[id] = cloneProvider(provider);
1042
+ function renderTestOutput(obj, input) {
1043
+ const exitCode = numberField(obj, "exit_code") ?? 0;
1044
+ const failed = numberField(obj, "failed") ?? 0;
1045
+ const output = stringField(obj, "output") ?? "";
1046
+ const header = renderHeader(`test: ${stringField(obj, "runner") ?? "runner"}`, {
1047
+ exit_code: obj["exit_code"],
1048
+ tests_run: obj["tests_run"],
1049
+ passed: obj["passed"],
1050
+ failed: obj["failed"],
1051
+ duration_ms: obj["duration_ms"],
1052
+ truncated: obj["truncated"],
1053
+ files: inputListSummary(input, "files"),
1054
+ grep: stringFromInput(input, "grep")
1055
+ });
1056
+ if (exitCode === 0 && failed === 0) {
1057
+ return joinSections([
1058
+ header,
1059
+ joinSections([
1060
+ "report:",
1061
+ `status=passed`,
1062
+ `tests_run=${obj["tests_run"] ?? 0}`,
1063
+ `passed=${obj["passed"] ?? 0}`,
1064
+ `failed=${obj["failed"] ?? 0}`,
1065
+ `duration_ms=${obj["duration_ms"] ?? 0}`,
1066
+ extractSpoolNote(output)
1067
+ ])
1068
+ ]);
1069
+ }
1070
+ return joinSections([
1071
+ header,
1072
+ `error_context:
1073
+ ${compactFailureOutput(output || "(no runner output)")}`
1074
+ ]);
1075
+ }
1076
+ function renderVerifierOutput(toolName, obj, input) {
1077
+ const exitCode = numberField(obj, "exit_code") ?? 0;
1078
+ const errors = numberField(obj, "errors") ?? 0;
1079
+ const warnings = numberField(obj, "warnings") ?? 0;
1080
+ const output = stringField(obj, "output") ?? "";
1081
+ const changed = numberField(obj, "files_changed") ?? 0;
1082
+ const header = renderHeader(toolName, {
1083
+ exit_code: obj["exit_code"],
1084
+ errors: obj["errors"],
1085
+ warnings: obj["warnings"],
1086
+ files_checked: obj["files_checked"],
1087
+ files_changed: obj["files_changed"],
1088
+ fix_applied: obj["fix_applied"],
1089
+ fixer: obj["fixer"],
1090
+ linter: obj["linter"],
1091
+ project: obj["project"],
1092
+ truncated: obj["truncated"],
1093
+ files: inputListSummary(input, "files"),
1094
+ cwd: stringFromInput(input, "cwd")
1095
+ });
1096
+ if (exitCode === 0 && errors === 0 && (toolName !== "format" || changed === 0)) {
1097
+ return joinSections([
1098
+ header,
1099
+ joinSections([
1100
+ "report:",
1101
+ "status=passed",
1102
+ `errors=${errors}`,
1103
+ `warnings=${warnings}`,
1104
+ toolName === "format" ? `files_changed=${changed}` : void 0,
1105
+ extractSpoolNote(output)
1106
+ ])
1107
+ ]);
1108
+ }
1109
+ if (exitCode === 0 && toolName === "format") {
1110
+ return joinSections([
1111
+ header,
1112
+ joinSections([
1113
+ "report:",
1114
+ "status=changed",
1115
+ `files_changed=${changed}`,
1116
+ extractSpoolNote(output)
1117
+ ])
1118
+ ]);
1119
+ }
1120
+ return joinSections([
1121
+ header,
1122
+ `error_context:
1123
+ ${compactFailureOutput(output || "(no verifier output)")}`
1124
+ ]);
1125
+ }
1126
+ function renderGrepMatches(matches, mode) {
1127
+ if (matches.length === 0) return "(no matches)";
1128
+ if (mode === "files_with_matches") return renderStringList(matches, "(no files)");
1129
+ if (mode === "count") return renderStringList(matches, "(no counts)");
1130
+ const groups = /* @__PURE__ */ new Map();
1131
+ const passthrough = [];
1132
+ for (const match of matches) {
1133
+ const parsed = parseGrepContentLine(match);
1134
+ if (!parsed) {
1135
+ passthrough.push(match);
1136
+ continue;
1137
+ }
1138
+ const list = groups.get(parsed.file) ?? [];
1139
+ list.push(`${parsed.line}:${parsed.text}`);
1140
+ groups.set(parsed.file, list);
1141
+ }
1142
+ if (groups.size === 0) return renderStringList(matches, "(no matches)");
1143
+ const sections = [];
1144
+ let fileIndex = 0;
1145
+ for (const [file, lines] of groups) {
1146
+ fileIndex++;
1147
+ if (fileIndex > GREP_FILE_LIMIT) break;
1148
+ const shown = lines.slice(0, GREP_MATCHES_PER_FILE);
1149
+ sections.push(
1150
+ `${file} (${lines.length} match(es), showing ${shown.length})
1151
+ ${shown.join("\n")}`
1152
+ );
618
1153
  }
619
- for (const [id, ovProvider] of Object.entries(overlay)) {
620
- const existing = out[id];
621
- out[id] = existing ? mergeProvider(existing, ovProvider) : cloneProvider(ovProvider);
1154
+ if (groups.size > GREP_FILE_LIMIT) {
1155
+ sections.push(`[serializer omitted ${groups.size - GREP_FILE_LIMIT} file group(s)]`);
622
1156
  }
623
- return out;
624
- }
625
- function mergeProvider(base, overlay) {
626
- const models = {};
627
- for (const [mid, m] of Object.entries(base.models ?? {})) {
628
- models[mid] = { ...m };
1157
+ if (passthrough.length > 0) {
1158
+ sections.push(`ungrouped:
1159
+ ${renderStringList(passthrough, "", 50)}`);
629
1160
  }
630
- for (const [mid, ovModel] of Object.entries(overlay.models ?? {})) {
631
- const existing = models[mid];
632
- models[mid] = existing ? mergeModel(existing, ovModel) : { ...ovModel };
1161
+ return sections.join("\n");
1162
+ }
1163
+ function parseGrepContentLine(line) {
1164
+ const match = /^(.+?):(\d+):(.*)$/.exec(line);
1165
+ if (!match?.[1] || !match[2]) return void 0;
1166
+ return { file: match[1], line: match[2], text: match[3] ?? "" };
1167
+ }
1168
+ function compactDiff(diff) {
1169
+ const lines = diff.split(/\r?\n/);
1170
+ if (lines.length <= DIFF_INLINE_LINE_LIMIT) return diff;
1171
+ const fileCount = Math.max(
1172
+ new Set(
1173
+ lines.map(
1174
+ (line) => /^diff --git\s+a\/(.+?)\s+b\//.exec(line)?.[1] ?? /^---\s+(.+)/.exec(line)?.[1]
1175
+ ).filter(Boolean)
1176
+ ).size,
1177
+ 0
1178
+ );
1179
+ const hunks = lines.filter((line) => line.startsWith("@@")).length;
1180
+ const added = lines.filter((line) => line.startsWith("+") && !line.startsWith("+++")).length;
1181
+ const removed = lines.filter((line) => line.startsWith("-") && !line.startsWith("---")).length;
1182
+ const selected = /* @__PURE__ */ new Set();
1183
+ let hunkCount = 0;
1184
+ for (let i = 0; i < lines.length; i++) {
1185
+ const line = lines[i] ?? "";
1186
+ if (line.startsWith("diff --git") || line.startsWith("--- ") || line.startsWith("+++ ")) {
1187
+ selected.add(i);
1188
+ continue;
1189
+ }
1190
+ if (!line.startsWith("@@")) continue;
1191
+ if (hunkCount >= DIFF_HUNK_LIMIT) continue;
1192
+ hunkCount++;
1193
+ for (let j = i; j <= Math.min(lines.length - 1, i + DIFF_HUNK_CONTEXT); j++) {
1194
+ selected.add(j);
1195
+ }
633
1196
  }
634
- return {
635
- ...base,
636
- // Overlay scalar fields win when explicitly provided; otherwise keep base.
637
- ...stripUndefined({
638
- id: overlay.id,
639
- name: overlay.name,
640
- npm: overlay.npm,
641
- api: overlay.api,
642
- env: overlay.env,
643
- doc: overlay.doc
1197
+ if (selected.size === 0) {
1198
+ return joinSections([
1199
+ renderHeader("diff_summary", {
1200
+ files: fileCount,
1201
+ hunks,
1202
+ added,
1203
+ removed,
1204
+ lines: lines.length
1205
+ }),
1206
+ lines.slice(0, DIFF_INLINE_LINE_LIMIT).join("\n"),
1207
+ `[serializer omitted ${Math.max(0, lines.length - DIFF_INLINE_LINE_LIMIT)} diff line(s)]`
1208
+ ]);
1209
+ }
1210
+ const excerpt = [];
1211
+ let previous = -1;
1212
+ for (const index of [...selected].sort((a, b) => a - b)) {
1213
+ if (index > previous + 1) {
1214
+ const omitted = previous === -1 ? index : index - previous - 1;
1215
+ excerpt.push(`[serializer omitted ${omitted} diff line(s)]`);
1216
+ }
1217
+ excerpt.push(lines[index] ?? "");
1218
+ previous = index;
1219
+ }
1220
+ const trailing = lines.length - previous - 1;
1221
+ if (trailing > 0) excerpt.push(`[serializer omitted ${trailing} trailing diff line(s)]`);
1222
+ return joinSections([
1223
+ renderHeader("diff_summary", {
1224
+ files: fileCount,
1225
+ hunks,
1226
+ shown_hunks: Math.min(hunks, DIFF_HUNK_LIMIT),
1227
+ added,
1228
+ removed,
1229
+ lines: lines.length
644
1230
  }),
645
- models
646
- };
1231
+ excerpt.join("\n")
1232
+ ]);
647
1233
  }
648
- function mergeModel(base, overlay) {
649
- const merged = { ...base, ...overlay };
650
- if (base.limit || overlay.limit) {
651
- merged.limit = { ...base.limit, ...overlay.limit };
1234
+ function compactFailureOutput(output) {
1235
+ const lines = output.split(/\r?\n/);
1236
+ if (lines.length <= 260) return output.trimEnd();
1237
+ const selected = /* @__PURE__ */ new Set();
1238
+ const marker = /\b(fail|failed|failure|error|exception|assertionerror|expected|received|actual|timeout|stack)\b/i;
1239
+ let markerHits = 0;
1240
+ for (let i = 0; i < lines.length; i++) {
1241
+ if (!marker.test(lines[i] ?? "")) continue;
1242
+ markerHits++;
1243
+ for (let j = Math.max(0, i - 4); j <= Math.min(lines.length - 1, i + 10); j++) {
1244
+ selected.add(j);
1245
+ }
652
1246
  }
653
- if (base.cost || overlay.cost) {
654
- merged.cost = { ...base.cost, ...overlay.cost };
1247
+ if (markerHits === 0) {
1248
+ return lines.slice(-220).join("\n").trimEnd();
655
1249
  }
656
- if (base.modalities || overlay.modalities) {
657
- merged.modalities = { ...base.modalities, ...overlay.modalities };
1250
+ const ordered = [...selected].sort((a, b) => a - b);
1251
+ const out = [];
1252
+ let previous = -1;
1253
+ for (const index of ordered) {
1254
+ if (index > previous + 1) {
1255
+ const omitted = previous === -1 ? index : index - previous - 1;
1256
+ out.push(`[serializer omitted ${omitted} line(s)]`);
1257
+ }
1258
+ out.push(lines[index] ?? "");
1259
+ previous = index;
658
1260
  }
659
- return merged;
1261
+ return out.join("\n").trimEnd();
660
1262
  }
661
- function cloneProvider(p) {
662
- const models = {};
663
- for (const [mid, m] of Object.entries(p.models ?? {})) {
664
- models[mid] = { ...m };
1263
+ function extractSpoolNote(output) {
1264
+ return output.split(/\r?\n/).find((line) => line.startsWith("[output truncated") && line.includes("full"));
1265
+ }
1266
+ function hasCommandOutputShape(obj) {
1267
+ return typeof obj["stdout"] === "string" || typeof obj["stderr"] === "string" || typeof obj["output"] === "string" || typeof obj["exitCode"] === "number" || typeof obj["exit_code"] === "number";
1268
+ }
1269
+ function renderCommandOutput(toolName, obj, input) {
1270
+ const command = stringField(obj, "command") ?? stringFromInput(input, "command");
1271
+ const args = stringArrayField(obj, "args");
1272
+ const commandLine = command ? [command, ...args].join(" ") : void 0;
1273
+ const output = stringField(obj, "output");
1274
+ const stdout = stringField(obj, "stdout");
1275
+ const stderr = stringField(obj, "stderr");
1276
+ return joinSections([
1277
+ renderHeader(commandLine ? `${toolName}: ${commandLine}` : toolName, {
1278
+ exit_code: obj["exit_code"] ?? obj["exitCode"],
1279
+ timed_out: obj["timed_out"],
1280
+ pid: obj["pid"],
1281
+ allowed: obj["allowed"],
1282
+ truncated: obj["truncated"],
1283
+ runner: obj["runner"],
1284
+ linter: obj["linter"],
1285
+ fixer: obj["fixer"],
1286
+ project: obj["project"],
1287
+ tests_run: obj["tests_run"],
1288
+ passed: obj["passed"],
1289
+ failed: obj["failed"],
1290
+ duration_ms: obj["duration_ms"],
1291
+ errors: obj["errors"],
1292
+ warnings: obj["warnings"],
1293
+ files_checked: obj["files_checked"],
1294
+ files_changed: obj["files_changed"],
1295
+ fix_applied: obj["fix_applied"]
1296
+ }),
1297
+ stringField(obj, "error") ? `error:
1298
+ ${stringField(obj, "error")}` : void 0,
1299
+ output ? `output:
1300
+ ${output}` : void 0,
1301
+ stdout ? `stdout:
1302
+ ${stdout}` : void 0,
1303
+ stderr ? `stderr:
1304
+ ${stderr}` : void 0
1305
+ ]);
1306
+ }
1307
+ function renderGenericToolObject(toolName, obj) {
1308
+ const scalars = {};
1309
+ const blocks = [];
1310
+ for (const [key, value] of Object.entries(obj)) {
1311
+ if (value === void 0) continue;
1312
+ if (isScalar(value)) {
1313
+ const inline = String(value);
1314
+ if (inline.length <= INLINE_LIMIT && !inline.includes("\n")) {
1315
+ scalars[key] = value;
1316
+ } else {
1317
+ blocks.push(`${key}:
1318
+ ${inline}`);
1319
+ }
1320
+ continue;
1321
+ }
1322
+ if (Array.isArray(value)) {
1323
+ if (value.every((item) => typeof item === "string")) {
1324
+ blocks.push(`${key}:
1325
+ ${renderStringList(value)}`);
1326
+ } else {
1327
+ blocks.push(`${key}:
1328
+ ${renderUnknownList(value)}`);
1329
+ }
1330
+ continue;
1331
+ }
1332
+ blocks.push(`${key}: ${clipInline(oneLineJson(value))}`);
665
1333
  }
666
- return { ...p, models };
1334
+ return joinSections([renderHeader(toolName, scalars), ...blocks]);
667
1335
  }
668
- function stripUndefined(obj) {
669
- const out = {};
670
- for (const [k, v] of Object.entries(obj)) {
671
- if (v !== void 0) out[k] = v;
1336
+ function renderHeader(label, fields) {
1337
+ const parts = Object.entries(fields).filter(([, value]) => value !== void 0 && value !== null && value !== "").map(([key, value]) => `${key}=${clipInline(formatInlineValue(value))}`);
1338
+ return parts.length > 0 ? `${label} (${parts.join(" ")})` : label;
1339
+ }
1340
+ function renderStringList(items, empty = "", limit = DEFAULT_LIST_LIMIT) {
1341
+ if (items.length === 0) return empty;
1342
+ const shown = items.slice(0, limit);
1343
+ const omitted = items.length - shown.length;
1344
+ return [
1345
+ ...shown,
1346
+ ...omitted > 0 ? [`[serializer omitted ${omitted} item(s); narrow the request for more]`] : []
1347
+ ].join("\n");
1348
+ }
1349
+ function renderUnknownList(items, limit = DEFAULT_LIST_LIMIT) {
1350
+ const shown = items.slice(0, limit).map((item) => clipInline(oneLineJson(item), 1e3));
1351
+ const omitted = items.length - shown.length;
1352
+ if (omitted > 0)
1353
+ shown.push(`[serializer omitted ${omitted} item(s); narrow the request for more]`);
1354
+ return shown.join("\n");
1355
+ }
1356
+ function joinSections(sections) {
1357
+ return sections.map((section) => typeof section === "string" ? section.trimEnd() : void 0).filter((section) => !!section).join("\n");
1358
+ }
1359
+ function formatInlineValue(value) {
1360
+ if (Array.isArray(value)) return `[${value.map(formatInlineValue).join(",")}]`;
1361
+ if (isScalar(value)) return String(value);
1362
+ return oneLineJson(value);
1363
+ }
1364
+ function clipInline(value, max = INLINE_LIMIT) {
1365
+ const compact = value.replace(/\s+/g, " ").trim();
1366
+ return compact.length <= max ? compact : `${compact.slice(0, max - 15)}...(${compact.length} chars)`;
1367
+ }
1368
+ function oneLineJson(value) {
1369
+ try {
1370
+ return JSON.stringify(value);
1371
+ } catch {
1372
+ return String(value);
672
1373
  }
673
- return out;
1374
+ }
1375
+ function stringField(obj, key) {
1376
+ const value = obj[key];
1377
+ return typeof value === "string" ? value : void 0;
1378
+ }
1379
+ function numberField(obj, key) {
1380
+ const value = obj[key];
1381
+ return typeof value === "number" ? value : void 0;
1382
+ }
1383
+ function stringArrayField(obj, key) {
1384
+ const value = obj[key];
1385
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1386
+ }
1387
+ function stringFromInput(input, key) {
1388
+ if (!isRecord2(input)) return void 0;
1389
+ const value = input[key];
1390
+ return typeof value === "string" ? value : void 0;
1391
+ }
1392
+ function numberFromInput(input, key) {
1393
+ if (!isRecord2(input)) return void 0;
1394
+ const value = input[key];
1395
+ return typeof value === "number" ? value : void 0;
1396
+ }
1397
+ function inputListSummary(input, key) {
1398
+ if (!isRecord2(input)) return void 0;
1399
+ const value = input[key];
1400
+ if (typeof value === "string") return value;
1401
+ if (Array.isArray(value)) return value.filter((item) => typeof item === "string").join(",");
1402
+ return void 0;
1403
+ }
1404
+ function isRecord2(value) {
1405
+ return !!value && typeof value === "object" && !Array.isArray(value);
1406
+ }
1407
+ function isScalar(value) {
1408
+ return value === null || ["string", "number", "boolean"].includes(typeof value);
1409
+ }
1410
+ function wstackGlobalRoot() {
1411
+ const fromEnv = process.env["WRONGSTACK_HOME"];
1412
+ if (fromEnv && fromEnv.trim().length > 0) return path6.resolve(fromEnv);
1413
+ return path6.join(os.homedir(), ".wrongstack");
674
1414
  }
675
1415
 
676
1416
  // src/types/errors.ts
@@ -998,6 +1738,20 @@ function providerStatusToCode(status, type) {
998
1738
  return ERROR_CODES.PROVIDER_INVALID_REQUEST;
999
1739
  }
1000
1740
 
1741
+ // src/types/config.ts
1742
+ function normalizeTokenSavingTier(val) {
1743
+ if (val === void 0) return "off";
1744
+ if (typeof val === "boolean") return val ? "medium" : "off";
1745
+ const validTiers = /* @__PURE__ */ new Set([
1746
+ "off",
1747
+ "minimal",
1748
+ "light",
1749
+ "medium",
1750
+ "aggressive"
1751
+ ]);
1752
+ return validTiers.has(val) ? val : "off";
1753
+ }
1754
+
1001
1755
  // src/types/default-config.ts
1002
1756
  var DEFAULT_TOOLS_CONFIG = Object.freeze({
1003
1757
  defaultExecutionStrategy: "smart",
@@ -1005,7 +1759,8 @@ var DEFAULT_TOOLS_CONFIG = Object.freeze({
1005
1759
  iterationTimeoutMs: 3e5,
1006
1760
  sessionTimeoutMs: 18e5,
1007
1761
  perIterationOutputCapBytes: 1e5,
1008
- autoExtendLimit: true
1762
+ autoExtendLimit: true,
1763
+ restrictToProjectRoot: false
1009
1764
  });
1010
1765
  var DEFAULT_CONTEXT_CONFIG = Object.freeze({
1011
1766
  preserveK: 10,
@@ -1014,6 +1769,10 @@ var DEFAULT_CONTEXT_CONFIG = Object.freeze({
1014
1769
  var DEFAULT_AUTONOMY_CONFIG = Object.freeze({
1015
1770
  autoProceedDelayMs: 45e3
1016
1771
  });
1772
+ var DEFAULT_CIRCUIT_BREAKER_CONFIG = Object.freeze({
1773
+ enabled: false,
1774
+ autoKillResetMs: 6e4
1775
+ });
1017
1776
  var DEFAULT_SESSION_LOGGING_CONFIG = Object.freeze({
1018
1777
  auditLevel: "standard",
1019
1778
  sampling: {
@@ -1025,7 +1784,10 @@ var DEFAULT_SESSION_LOGGING_CONFIG = Object.freeze({
1025
1784
  var DEFAULT_SESSION_PRUNE_DAYS = 30;
1026
1785
 
1027
1786
  // src/types/secret-vault.ts
1028
- var ENCRYPTED_PREFIX = "enc:v1:";
1787
+ var ENCRYPTED_PREFIX_PATTERN = /^enc:v(\d+):/;
1788
+ function encryptedPrefixForVersion(version) {
1789
+ return `enc:v${version}:`;
1790
+ }
1029
1791
 
1030
1792
  // src/security/secret-vault.ts
1031
1793
  var KEY_BYTES = 32;
@@ -1033,6 +1795,8 @@ var IV_BYTES = 12;
1033
1795
  var TAG_BYTES = 16;
1034
1796
  var ALGO = "aes-256-gcm";
1035
1797
  var KEY_FILE_MODE = 384;
1798
+ var KEY_FILE_MAGIC = Buffer.from("WSKV", "ascii");
1799
+ var VERSIONED_KEY_FILE_SIZE = KEY_FILE_MAGIC.length + 1 + KEY_BYTES;
1036
1800
  function checkKeyFilePermissions(keyFile) {
1037
1801
  if (process.platform === "win32") return;
1038
1802
  try {
@@ -1055,11 +1819,17 @@ function checkKeyFilePermissions(keyFile) {
1055
1819
  var DefaultSecretVault = class {
1056
1820
  keyFile;
1057
1821
  key;
1822
+ _keyVersion = 1;
1058
1823
  constructor(opts) {
1059
1824
  this.keyFile = opts.keyFile;
1060
1825
  }
1826
+ /** Current key version. Starts at 1; incremented by rotateKey(). */
1827
+ get keyVersion() {
1828
+ if (!this.key) this.loadOrCreateKey();
1829
+ return this._keyVersion;
1830
+ }
1061
1831
  isEncrypted(value) {
1062
- return typeof value === "string" && value.startsWith(ENCRYPTED_PREFIX);
1832
+ return typeof value === "string" && ENCRYPTED_PREFIX_PATTERN.test(value);
1063
1833
  }
1064
1834
  encrypt(plaintext) {
1065
1835
  if (this.isEncrypted(plaintext)) return plaintext;
@@ -1068,11 +1838,20 @@ var DefaultSecretVault = class {
1068
1838
  const cipher = createCipheriv(ALGO, key, iv);
1069
1839
  const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
1070
1840
  const tag = cipher.getAuthTag();
1071
- return `${ENCRYPTED_PREFIX}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
1841
+ const prefix = encryptedPrefixForVersion(this._keyVersion);
1842
+ return `${prefix}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
1072
1843
  }
1073
1844
  decrypt(value) {
1074
1845
  if (!this.isEncrypted(value)) return value;
1075
- const rest = value.slice(ENCRYPTED_PREFIX.length);
1846
+ const prefixMatch = value.match(ENCRYPTED_PREFIX_PATTERN);
1847
+ if (!prefixMatch) {
1848
+ throw new ConfigError({
1849
+ message: "SecretVault: malformed encrypted value",
1850
+ code: ERROR_CODES.CONFIG_PARSE_FAILED,
1851
+ context: { field: "encrypted_value" }
1852
+ });
1853
+ }
1854
+ const rest = value.slice(prefixMatch[0].length);
1076
1855
  const parts = rest.split(":");
1077
1856
  if (parts.length !== 3) {
1078
1857
  throw new ConfigError({
@@ -1101,42 +1880,104 @@ var DefaultSecretVault = class {
1101
1880
  const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
1102
1881
  return pt.toString("utf8");
1103
1882
  }
1883
+ /**
1884
+ * Generate a new encryption key, write it to disk, and increment the key version.
1885
+ * After rotation, encrypt() emits the new version prefix (e.g. enc:v2:).
1886
+ * The caller must re-encrypt existing config values (see rotateConfigKeys()).
1887
+ */
1888
+ rotateKey() {
1889
+ const oldVersion = this._keyVersion;
1890
+ const newKey = randomBytes(KEY_BYTES);
1891
+ const newVersion = oldVersion + 1;
1892
+ const keyFileBuf = Buffer.alloc(VERSIONED_KEY_FILE_SIZE);
1893
+ KEY_FILE_MAGIC.copy(keyFileBuf, 0);
1894
+ keyFileBuf[KEY_FILE_MAGIC.length] = newVersion;
1895
+ newKey.copy(keyFileBuf, KEY_FILE_MAGIC.length + 1);
1896
+ fs2.mkdirSync(path6.dirname(this.keyFile), { recursive: true });
1897
+ fs2.writeFileSync(this.keyFile, keyFileBuf, { mode: 384 });
1898
+ checkKeyFilePermissions(this.keyFile);
1899
+ this.key = newKey;
1900
+ this._keyVersion = newVersion;
1901
+ return { oldVersion, newVersion };
1902
+ }
1104
1903
  loadOrCreateKey() {
1105
1904
  if (this.key) return this.key;
1106
1905
  try {
1107
1906
  const buf = fs2.readFileSync(this.keyFile);
1108
- if (buf.length !== KEY_BYTES) {
1109
- throw new ConfigError({
1110
- message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`,
1111
- code: ERROR_CODES.CONFIG_INVALID,
1112
- context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
1113
- });
1907
+ if (buf.length === KEY_BYTES) {
1908
+ this.key = buf;
1909
+ this._keyVersion = 1;
1910
+ checkKeyFilePermissions(this.keyFile);
1911
+ return this.key;
1912
+ }
1913
+ if (buf.length === VERSIONED_KEY_FILE_SIZE) {
1914
+ const magic = buf.subarray(0, KEY_FILE_MAGIC.length);
1915
+ if (!magic.equals(KEY_FILE_MAGIC)) {
1916
+ throw new ConfigError({
1917
+ message: `SecretVault: key file ${this.keyFile} has invalid magic header`,
1918
+ code: ERROR_CODES.CONFIG_INVALID,
1919
+ context: { keyFile: this.keyFile }
1920
+ });
1921
+ }
1922
+ const version = buf[KEY_FILE_MAGIC.length];
1923
+ const key2 = buf.subarray(KEY_FILE_MAGIC.length + 1);
1924
+ if (key2.length !== KEY_BYTES) {
1925
+ throw new ConfigError({
1926
+ message: `SecretVault: key file ${this.keyFile} has wrong key size (${key2.length} bytes, expected ${KEY_BYTES})`,
1927
+ code: ERROR_CODES.CONFIG_INVALID,
1928
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: key2.length }
1929
+ });
1930
+ }
1931
+ this.key = Buffer.from(key2);
1932
+ this._keyVersion = version;
1933
+ checkKeyFilePermissions(this.keyFile);
1934
+ return this.key;
1114
1935
  }
1115
- this.key = buf;
1116
- checkKeyFilePermissions(this.keyFile);
1117
- return this.key;
1936
+ throw new ConfigError({
1937
+ message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES} for v1 or ${VERSIONED_KEY_FILE_SIZE} for v2+). Remove it manually to generate a new key.`,
1938
+ code: ERROR_CODES.CONFIG_INVALID,
1939
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
1940
+ });
1118
1941
  } catch (err) {
1119
1942
  if (err.code !== "ENOENT") throw err;
1120
1943
  }
1121
- fs2.mkdirSync(path4.dirname(this.keyFile), { recursive: true });
1944
+ fs2.mkdirSync(path6.dirname(this.keyFile), { recursive: true });
1122
1945
  const key = randomBytes(KEY_BYTES);
1123
1946
  try {
1124
1947
  fs2.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
1125
1948
  } catch (err) {
1126
1949
  if (err.code !== "EEXIST") throw err;
1127
1950
  const buf = fs2.readFileSync(this.keyFile);
1128
- if (buf.length !== KEY_BYTES) {
1129
- throw new ConfigError({
1130
- message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`,
1131
- code: ERROR_CODES.CONFIG_INVALID,
1132
- context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
1133
- });
1951
+ if (buf.length === KEY_BYTES) {
1952
+ this.key = buf;
1953
+ this._keyVersion = 1;
1954
+ checkKeyFilePermissions(this.keyFile);
1955
+ return this.key;
1956
+ }
1957
+ if (buf.length === VERSIONED_KEY_FILE_SIZE) {
1958
+ const magic = buf.subarray(0, KEY_FILE_MAGIC.length);
1959
+ if (!magic.equals(KEY_FILE_MAGIC)) {
1960
+ throw new ConfigError({
1961
+ message: `SecretVault: key file ${this.keyFile} has invalid magic header`,
1962
+ code: ERROR_CODES.CONFIG_INVALID,
1963
+ context: { keyFile: this.keyFile }
1964
+ });
1965
+ }
1966
+ const version = buf[KEY_FILE_MAGIC.length];
1967
+ const winnerKey = buf.subarray(KEY_FILE_MAGIC.length + 1);
1968
+ this.key = Buffer.from(winnerKey);
1969
+ this._keyVersion = version;
1970
+ checkKeyFilePermissions(this.keyFile);
1971
+ return this.key;
1134
1972
  }
1135
- this.key = buf;
1136
- checkKeyFilePermissions(this.keyFile);
1137
- return this.key;
1973
+ throw new ConfigError({
1974
+ message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES} for v1 or ${VERSIONED_KEY_FILE_SIZE} for v2+). Remove it manually to generate a new key.`,
1975
+ code: ERROR_CODES.CONFIG_INVALID,
1976
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
1977
+ });
1138
1978
  }
1139
1979
  this.key = key;
1980
+ this._keyVersion = 1;
1140
1981
  return key;
1141
1982
  }
1142
1983
  };
@@ -1190,7 +2031,7 @@ async function rewriteConfigEncrypted(configPath, vault, patch) {
1190
2031
  }
1191
2032
  const merged = deepMerge(current, patch ?? {});
1192
2033
  const encrypted = encryptConfigSecrets(merged, vault);
1193
- await fs.mkdir(path4.dirname(configPath), { recursive: true });
2034
+ await fs.mkdir(path6.dirname(configPath), { recursive: true });
1194
2035
  await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
1195
2036
  await restrictFilePermissions(configPath);
1196
2037
  }
@@ -1217,6 +2058,89 @@ async function migratePlaintextSecrets(configPath, vault, logger) {
1217
2058
  );
1218
2059
  return { migrated: counter.n, file: configPath };
1219
2060
  }
2061
+ async function rotateConfigKeys(configPath, vault, logger) {
2062
+ const log = logger?.info ?? (() => {
2063
+ });
2064
+ const warn = logger?.warn ?? ((msg) => console.warn(msg));
2065
+ let raw;
2066
+ try {
2067
+ raw = await fs.readFile(configPath, "utf8");
2068
+ } catch {
2069
+ const { oldVersion: oldVersion2, newVersion: newVersion2 } = vault.rotateKey();
2070
+ log(`[secret-vault] Key rotated (v${oldVersion2} \u2192 v${newVersion2}) \u2014 no config file to re-encrypt`);
2071
+ return { rotated: 0, oldVersion: oldVersion2, newVersion: newVersion2, file: configPath };
2072
+ }
2073
+ let parsed;
2074
+ try {
2075
+ parsed = JSON.parse(raw);
2076
+ } catch {
2077
+ warn(`[secret-vault] Config file ${configPath} is not valid JSON \u2014 skipping rotation`);
2078
+ return { rotated: 0, oldVersion: vault.keyVersion, newVersion: vault.keyVersion, file: configPath };
2079
+ }
2080
+ const counter = { n: 0, failed: [] };
2081
+ const decrypted = walkDecryptCount(parsed, vault, counter);
2082
+ if (counter.failed.length > 0) {
2083
+ throw new Error(
2084
+ `[secret-vault] Aborting key rotation: ${counter.failed.length} field(s) could not be decrypted with the current key and would be permanently lost on rotation: ${counter.failed.join(", ")}. Restore or remove these fields before rotating.`
2085
+ );
2086
+ }
2087
+ if (counter.n === 0) {
2088
+ const { oldVersion: oldVersion2, newVersion: newVersion2 } = vault.rotateKey();
2089
+ log(`[secret-vault] Key rotated (v${oldVersion2} \u2192 v${newVersion2}) \u2014 no encrypted fields to re-encrypt`);
2090
+ return { rotated: 0, oldVersion: oldVersion2, newVersion: newVersion2, file: configPath };
2091
+ }
2092
+ const { oldVersion, newVersion } = vault.rotateKey();
2093
+ const reencrypted = walkReencrypt(decrypted, vault);
2094
+ await atomicWrite(configPath, JSON.stringify(reencrypted, null, 2), { mode: 384 });
2095
+ await restrictFilePermissions(configPath, { warn });
2096
+ log(`[secret-vault] Key rotated (v${oldVersion} \u2192 v${newVersion}) \u2014 re-encrypted ${counter.n} field(s)`);
2097
+ return { rotated: counter.n, oldVersion, newVersion, file: configPath };
2098
+ }
2099
+ function walkDecryptCount(node, vault, counter, pathPrefix = "") {
2100
+ if (node === null || node === void 0) return node;
2101
+ if (typeof node !== "object") return node;
2102
+ if (Array.isArray(node)) {
2103
+ return node.map(
2104
+ (item, i) => walkDecryptCount(item, vault, counter, `${pathPrefix}[${i}]`)
2105
+ );
2106
+ }
2107
+ const out = /* @__PURE__ */ Object.create(null);
2108
+ for (const [k, v] of Object.entries(node)) {
2109
+ const keyPath = pathPrefix ? `${pathPrefix}.${k}` : k;
2110
+ if (typeof v === "string" && vault.isEncrypted(v)) {
2111
+ try {
2112
+ out[k] = vault.decrypt(v);
2113
+ counter.n++;
2114
+ } catch {
2115
+ counter.failed.push(keyPath);
2116
+ out[k] = v;
2117
+ }
2118
+ } else if (typeof v === "object" && v !== null) {
2119
+ out[k] = walkDecryptCount(v, vault, counter, keyPath);
2120
+ } else {
2121
+ out[k] = v;
2122
+ }
2123
+ }
2124
+ return out;
2125
+ }
2126
+ function walkReencrypt(node, vault) {
2127
+ if (node === null || node === void 0) return node;
2128
+ if (typeof node !== "object") return node;
2129
+ if (Array.isArray(node)) {
2130
+ return node.map((item) => walkReencrypt(item, vault));
2131
+ }
2132
+ const out = /* @__PURE__ */ Object.create(null);
2133
+ for (const [k, v] of Object.entries(node)) {
2134
+ if (typeof v === "string" && isSecretField(k) && v.length > 0 && !vault.isEncrypted(v)) {
2135
+ out[k] = vault.encrypt(v);
2136
+ } else if (typeof v === "object" && v !== null) {
2137
+ out[k] = walkReencrypt(v, vault);
2138
+ } else {
2139
+ out[k] = v;
2140
+ }
2141
+ }
2142
+ return out;
2143
+ }
1220
2144
  async function restrictFilePermissions(filePath, opts) {
1221
2145
  const warn = opts?.warn ?? ((msg) => console.warn(msg));
1222
2146
  if (process.platform === "win32") {
@@ -1305,7 +2229,7 @@ var DefaultLogger = class _DefaultLogger {
1305
2229
  this.maxFileBytes = opts.maxFileBytes ?? 10 * 1024 * 1024;
1306
2230
  if (this.file) {
1307
2231
  try {
1308
- fs2.mkdirSync(path4.dirname(this.file), { recursive: true });
2232
+ fs2.mkdirSync(path6.dirname(this.file), { recursive: true });
1309
2233
  } catch {
1310
2234
  }
1311
2235
  }
@@ -1567,7 +2491,11 @@ var MEMORY_TYPE_LABELS = {
1567
2491
  };
1568
2492
 
1569
2493
  // src/execution/compaction-core.ts
2494
+ function compactionDebugEnabled() {
2495
+ return process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1";
2496
+ }
1570
2497
  function emitCompactionMetrics(event, metrics) {
2498
+ if (!compactionDebugEnabled()) return;
1571
2499
  console.log(
1572
2500
  JSON.stringify({
1573
2501
  level: "debug",
@@ -1602,38 +2530,41 @@ function findPreserveStart(messages, preserveK) {
1602
2530
  preserveStart = i;
1603
2531
  }
1604
2532
  }
1605
- let forwardWalkIterations = 0;
1606
- let forwardWalkInnerIterations = 0;
1607
- for (let i = preserveStart; i < messages.length; i++) {
1608
- forwardWalkIterations++;
1609
- const m = messages[i];
1610
- if (!m || typeof m.content === "string" || !Array.isArray(m.content)) continue;
1611
- const hasToolUse3 = m.content.some((b) => {
1612
- forwardWalkInnerIterations++;
1613
- return b.type === "tool_use";
1614
- });
1615
- if (hasToolUse3 && i + 1 < messages.length) {
1616
- const next = messages[i + 1];
1617
- if (next && next.role === "user" && typeof next.content !== "string" && Array.isArray(next.content) && next.content.some((b) => {
1618
- forwardWalkInnerIterations++;
1619
- return b.type === "tool_result";
1620
- })) {
1621
- preserveStart = i + 1;
1622
- }
2533
+ let pairRepairIterations = 0;
2534
+ let pairRepairInnerIterations = 0;
2535
+ while (preserveStart > 0) {
2536
+ pairRepairIterations++;
2537
+ const first = messages[preserveStart];
2538
+ const prev = messages[preserveStart - 1];
2539
+ if (!first || !prev || first.role !== "user" || prev.role !== "assistant") break;
2540
+ if (typeof first.content === "string" || typeof prev.content === "string") break;
2541
+ const resultIds = /* @__PURE__ */ new Set();
2542
+ for (const block of first.content) {
2543
+ pairRepairInnerIterations++;
2544
+ if (block.type === "tool_result") resultIds.add(block.tool_use_id);
1623
2545
  }
2546
+ if (resultIds.size === 0) break;
2547
+ const hasMatchingUse = prev.content.some((block) => {
2548
+ pairRepairInnerIterations++;
2549
+ return block.type === "tool_use" && resultIds.has(block.id);
2550
+ });
2551
+ if (!hasMatchingUse) break;
2552
+ preserveStart--;
2553
+ }
2554
+ if (compactionDebugEnabled()) {
2555
+ console.log(
2556
+ JSON.stringify({
2557
+ level: "debug",
2558
+ event: "compaction.find_preserve_start.ended",
2559
+ messageCount: messages.length,
2560
+ preserveK,
2561
+ preserveStart,
2562
+ pairRepairIterations,
2563
+ pairRepairInnerIterations,
2564
+ pairRepairInnerPerOuter: pairRepairIterations > 0 ? pairRepairInnerIterations / pairRepairIterations : 0
2565
+ })
2566
+ );
1624
2567
  }
1625
- console.log(
1626
- JSON.stringify({
1627
- level: "debug",
1628
- event: "compaction.find_preserve_start.ended",
1629
- messageCount: messages.length,
1630
- preserveK,
1631
- preserveStart,
1632
- forwardWalkIterations,
1633
- forwardWalkInnerIterations,
1634
- forwardWalkInnerPerOuter: forwardWalkIterations > 0 ? forwardWalkInnerIterations / forwardWalkIterations : 0
1635
- })
1636
- );
1637
2568
  return preserveStart;
1638
2569
  }
1639
2570
  function eliseOldToolResults(messages, opts) {
@@ -1647,7 +2578,8 @@ function eliseOldToolResults(messages, opts) {
1647
2578
  if (!msg || !Array.isArray(msg.content)) continue;
1648
2579
  for (const b of msg.content) {
1649
2580
  fastPathInnerIterations++;
1650
- if (b.type === "tool_result" && estimateToolResultTokens(b.content) >= opts.eliseThreshold) {
2581
+ const oversized = b.type === "tool_result" && estimateToolResultTokens(b.content) >= opts.eliseThreshold || b.type === "tool_use" && estimateToolInputTokens(b.input) >= opts.eliseThreshold;
2582
+ if (oversized) {
1651
2583
  hasOversized = true;
1652
2584
  break;
1653
2585
  }
@@ -1681,6 +2613,13 @@ function eliseOldToolResults(messages, opts) {
1681
2613
  }
1682
2614
  const original = msg.content;
1683
2615
  const newContent = original.map((b) => {
2616
+ if (b.type === "tool_use") {
2617
+ const tokens2 = estimateToolInputTokens(b.input);
2618
+ if (tokens2 < opts.eliseThreshold) return b;
2619
+ const elidedInput = summarizeToolUseInputElision(b, tokens2);
2620
+ saved += Math.max(0, tokens2 - estimateToolInputTokens(elidedInput));
2621
+ return { ...b, input: elidedInput };
2622
+ }
1684
2623
  if (b.type !== "tool_result") return b;
1685
2624
  const tokens = estimateToolResultTokens(b.content);
1686
2625
  if (tokens < opts.eliseThreshold) return b;
@@ -1688,7 +2627,7 @@ function eliseOldToolResults(messages, opts) {
1688
2627
  const elided = {
1689
2628
  type: "tool_result",
1690
2629
  tool_use_id: b.tool_use_id,
1691
- content: `[elided: ~${tokens} tokens]`,
2630
+ content: summarizeToolResultElision(b, tokens),
1692
2631
  is_error: b.is_error
1693
2632
  };
1694
2633
  return elided;
@@ -1700,7 +2639,7 @@ function eliseOldToolResults(messages, opts) {
1700
2639
  changed = true;
1701
2640
  }
1702
2641
  fullPassInnerIterations += original.length;
1703
- if (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1") {
2642
+ if (compactionDebugEnabled()) {
1704
2643
  const ratio = fullPassInnerIterations / fullPassIterations;
1705
2644
  if (ratio > 10) {
1706
2645
  console.error(
@@ -1728,6 +2667,65 @@ function eliseOldToolResults(messages, opts) {
1728
2667
  });
1729
2668
  return { messages: changed ? next : messages, saved, changed };
1730
2669
  }
2670
+ function summarizeToolUseInputElision(block, tokens) {
2671
+ const fields = {};
2672
+ for (const [key, value] of Object.entries(block.input ?? {})) {
2673
+ fields[key] = summarizeToolUseInputValue(value);
2674
+ }
2675
+ return {
2676
+ __elided_tool_input: `~${tokens} tokens; original arguments are in the session log`,
2677
+ tool: block.name,
2678
+ fields
2679
+ };
2680
+ }
2681
+ function summarizeToolUseInputValue(value) {
2682
+ if (value === null || value === void 0) return value;
2683
+ if (typeof value === "number" || typeof value === "boolean") return value;
2684
+ if (typeof value === "string") {
2685
+ const oneLine = value.replace(/\s+/g, " ").trim();
2686
+ return oneLine.length <= 160 ? oneLine : `${oneLine.slice(0, 120)}...(${oneLine.length} chars)`;
2687
+ }
2688
+ if (Array.isArray(value)) {
2689
+ return `[array:${value.length}]`;
2690
+ }
2691
+ if (typeof value === "object") {
2692
+ const keys = Object.keys(value);
2693
+ return `[object:${keys.slice(0, 8).join(",")}${keys.length > 8 ? ",..." : ""}]`;
2694
+ }
2695
+ return String(value);
2696
+ }
2697
+ function summarizeToolResultElision(block, tokens) {
2698
+ const parts = [`elided: ~${tokens} tokens`];
2699
+ if (block.name) parts.push(`tool=${block.name}`);
2700
+ const files = extractPathHints(block.content).slice(0, 5);
2701
+ if (files.length > 0) parts.push(`files=${files.join(", ")}`);
2702
+ const error = firstErrorLine(block.content);
2703
+ if (error) parts.push(`error=${error}`);
2704
+ return `[${parts.join("; ")}]`;
2705
+ }
2706
+ function extractPathHints(content) {
2707
+ const text = typeof content === "string" ? content : JSON.stringify(content);
2708
+ const out = /* @__PURE__ */ new Set();
2709
+ const re = /(?:(?:[A-Za-z]:)?[./\\]?[\w@.-]+(?:[\\/][\w@(). -]+)+\.[A-Za-z0-9]{1,12})/g;
2710
+ for (const match of text.matchAll(re)) {
2711
+ const clean = match[0]?.replace(/\\/g, "/").replace(/^["'`]+|["'`),;:]+$/g, "");
2712
+ if (clean && clean.length <= 220) out.add(clean);
2713
+ if (out.size >= 5) break;
2714
+ }
2715
+ return [...out];
2716
+ }
2717
+ function firstErrorLine(content) {
2718
+ const text = typeof content === "string" ? content : JSON.stringify(content);
2719
+ for (const line of text.split(/\r?\n/)) {
2720
+ if (!/\b(error|exception|failed|failure|fatal|panic|timeout|denied|enoent|eacces|eperm)\b/i.test(
2721
+ line
2722
+ ))
2723
+ continue;
2724
+ const trimmed = line.replace(/\s+/g, " ").trim();
2725
+ if (trimmed) return trimmed.slice(0, 180);
2726
+ }
2727
+ return void 0;
2728
+ }
1731
2729
  function buildLosslessDigest(messages) {
1732
2730
  const lines = [];
1733
2731
  for (const m of messages) {
@@ -1838,15 +2836,15 @@ function buildSmartDigest(messages) {
1838
2836
  lines.push(`[${m.role}]: ${display}${marker}`);
1839
2837
  }
1840
2838
  if (noiseCount > 0) {
1841
- lines.push(`[system]: ${noiseCount} low-importance turn(s) collapsed (repeated failures / pure tool I/O)`);
2839
+ lines.push(
2840
+ `[system]: ${noiseCount} low-importance turn(s) collapsed (repeated failures / pure tool I/O)`
2841
+ );
1842
2842
  }
1843
2843
  return lines.join("\n");
1844
2844
  }
1845
2845
  function countToolBlocks(m) {
1846
2846
  if (typeof m.content === "string") return 0;
1847
- return m.content.filter(
1848
- (b) => b.type === "tool_use" || b.type === "tool_result"
1849
- ).length;
2847
+ return m.content.filter((b) => b.type === "tool_use" || b.type === "tool_result").length;
1850
2848
  }
1851
2849
  function firstSentence(text) {
1852
2850
  const trimmed = text.trim();
@@ -1892,10 +2890,12 @@ var HybridCompactor = class {
1892
2890
  if (elide.changed) ctx.state.replaceMessages(elide.messages);
1893
2891
  if (elide.saved > 0) reductions.push({ phase: "elision", saved: elide.saved });
1894
2892
  let collapsedDigest;
2893
+ let evidenceDigest;
1895
2894
  if (opts.aggressive) {
1896
2895
  const phase2 = this.collapseAncientTurns(ctx, preserveK);
1897
2896
  if (phase2.saved > 0) reductions.push({ phase: "summary", saved: phase2.saved });
1898
2897
  collapsedDigest = phase2.digest;
2898
+ evidenceDigest = phase2.evidenceDigest;
1899
2899
  }
1900
2900
  const repaired = repairToolUseAdjacency(ctx.messages);
1901
2901
  if (repaired.report.changed) {
@@ -1903,6 +2903,11 @@ var HybridCompactor = class {
1903
2903
  }
1904
2904
  const afterTokens = estimateMessages(ctx.messages);
1905
2905
  const afterFull = this.estimateFullRequest(ctx);
2906
+ const quality = checkCompactionQuality(ctx, {
2907
+ collapsedDigest,
2908
+ evidenceDigest,
2909
+ reduced: beforeTokens > afterTokens || beforeFull > afterFull
2910
+ });
1906
2911
  return {
1907
2912
  before: beforeTokens,
1908
2913
  after: afterTokens,
@@ -1910,6 +2915,8 @@ var HybridCompactor = class {
1910
2915
  fullRequestTokensAfter: afterFull,
1911
2916
  reductions,
1912
2917
  collapsedDigest,
2918
+ evidenceDigest,
2919
+ quality,
1913
2920
  repaired: repaired.report.changed ? {
1914
2921
  removedToolUses: repaired.report.removedToolUses,
1915
2922
  removedToolResults: repaired.report.removedToolResults,
@@ -1951,7 +2958,13 @@ var HybridCompactor = class {
1951
2958
  if (boundary <= 0) return { saved: 0 };
1952
2959
  const removed = messages.slice(0, boundary);
1953
2960
  const removedTokens = estimateMessages(removed);
1954
- const digest = this.smart ? buildSmartDigest(removed) || `${removed.length} earlier turns (no textual content; tool I/O omitted \u2014 see session log)` : buildLosslessDigest(removed) || `${removed.length} earlier turns (no textual content; tool I/O omitted \u2014 see session log)`;
2961
+ const historyDigest = this.smart ? buildSmartDigest(removed) || `${removed.length} earlier turns (no textual content; tool I/O omitted; see session log)` : buildLosslessDigest(removed) || `${removed.length} earlier turns (no textual content; tool I/O omitted; see session log)`;
2962
+ const evidenceDigest = buildContextEvidenceDigest(ctx);
2963
+ const digest = evidenceDigest ? `[context_state]
2964
+ ${evidenceDigest}
2965
+
2966
+ [prior_history]
2967
+ ${historyDigest}` : historyDigest;
1955
2968
  const summaryMsg = {
1956
2969
  role: "system",
1957
2970
  content: `[prior_turns_digest: ${digest}]`
@@ -1960,10 +2973,29 @@ var HybridCompactor = class {
1960
2973
  ctx.state.replaceMessages([summaryMsg, ...tail]);
1961
2974
  return {
1962
2975
  saved: Math.max(0, removedTokens - estimateMessages([summaryMsg])),
1963
- digest
2976
+ digest,
2977
+ evidenceDigest: evidenceDigest || void 0
1964
2978
  };
1965
2979
  }
1966
2980
  };
2981
+ function checkCompactionQuality(ctx, opts) {
2982
+ const evidence = ctx.contextEvidence;
2983
+ const digest = `${opts.collapsedDigest ?? ""}
2984
+ ${opts.evidenceDigest ?? ""}`;
2985
+ const hasIntent = Boolean(evidence?.currentIntent?.text || /\b(intent|goal|session_goals)\b/i.test(digest));
2986
+ const hasPathTrail = Boolean(
2987
+ Object.keys(evidence?.fileGraph ?? {}).length > 0 || (evidence?.toolCalls.length ?? 0) > 0 || /\b(dependency_graph|tool_trail|files=)\b/i.test(digest)
2988
+ );
2989
+ const issues = [];
2990
+ if (opts.reduced && !hasIntent) issues.push("missing intent anchor");
2991
+ if (opts.reduced && !hasPathTrail) issues.push("missing tool/path trail");
2992
+ return {
2993
+ ok: issues.length === 0,
2994
+ hasIntent,
2995
+ hasPathTrail,
2996
+ issues
2997
+ };
2998
+ }
1967
2999
  function readContextWindowPolicy(ctx) {
1968
3000
  const policy = ctx.meta?.["contextWindowPolicy"];
1969
3001
  if (!policy || typeof policy !== "object") return null;
@@ -1990,52 +3022,52 @@ var DefaultPathResolver = class {
1990
3022
  projectRoot;
1991
3023
  cwd;
1992
3024
  constructor(cwd = process.cwd()) {
1993
- this.cwd = path4.resolve(cwd);
3025
+ this.cwd = path6.resolve(cwd);
1994
3026
  this.projectRoot = this.detectProjectRoot(this.cwd);
1995
3027
  }
1996
3028
  detectProjectRoot(start) {
1997
- let dir = path4.resolve(start);
1998
- const root = path4.parse(dir).root;
1999
- const home = path4.resolve(os.homedir());
2000
- const startPath = path4.resolve(start);
3029
+ let dir = path6.resolve(start);
3030
+ const root = path6.parse(dir).root;
3031
+ const home = path6.resolve(os.homedir());
3032
+ const startPath = path6.resolve(start);
2001
3033
  while (dir !== root) {
2002
3034
  if (dir === home && dir !== startPath) {
2003
3035
  break;
2004
3036
  }
2005
3037
  for (const marker of PROJECT_MARKERS) {
2006
3038
  try {
2007
- fs2.accessSync(path4.join(dir, marker));
3039
+ fs2.accessSync(path6.join(dir, marker));
2008
3040
  return dir;
2009
3041
  } catch {
2010
3042
  }
2011
3043
  }
2012
- const parent = path4.dirname(dir);
3044
+ const parent = path6.dirname(dir);
2013
3045
  if (parent === dir) break;
2014
3046
  dir = parent;
2015
3047
  }
2016
3048
  return startPath;
2017
3049
  }
2018
3050
  resolve(input) {
2019
- const abs = path4.isAbsolute(input) ? input : path4.resolve(this.cwd, input);
3051
+ const abs = path6.isAbsolute(input) ? input : path6.resolve(this.cwd, input);
2020
3052
  let real;
2021
3053
  try {
2022
3054
  real = fs2.realpathSync(abs);
2023
3055
  } catch {
2024
- real = path4.normalize(abs);
3056
+ real = path6.normalize(abs);
2025
3057
  }
2026
3058
  return real;
2027
3059
  }
2028
3060
  isInsideRoot(absPath) {
2029
- const normalized = path4.normalize(absPath);
2030
- const root = path4.normalize(this.projectRoot);
3061
+ const normalized = path6.normalize(absPath);
3062
+ const root = path6.normalize(this.projectRoot);
2031
3063
  if (normalized === root) return true;
2032
- const rel = path4.relative(root, normalized);
2033
- return !rel.startsWith("..") && !path4.isAbsolute(rel);
3064
+ const rel = path6.relative(root, normalized);
3065
+ return !rel.startsWith("..") && !path6.isAbsolute(rel);
2034
3066
  }
2035
3067
  ensureInsideRoot(absPath) {
2036
3068
  const resolved = this.resolve(absPath);
2037
3069
  if (!this.isInsideRoot(resolved)) {
2038
- const display = path4.isAbsolute(absPath) ? path4.basename(absPath) : absPath;
3070
+ const display = path6.isAbsolute(absPath) ? path6.basename(absPath) : absPath;
2039
3071
  const err = new Error(`Path "${display}" resolves outside the project root`);
2040
3072
  err.fullPath = absPath;
2041
3073
  err.projectRoot = this.projectRoot;
@@ -2234,6 +3266,27 @@ var PATTERNS = [
2234
3266
  { type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g },
2235
3267
  { type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g },
2236
3268
  { type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g },
3269
+ // AI/ML provider keys — modern LLM services with well-known prefixes
3270
+ {
3271
+ type: "huggingface_token",
3272
+ // HuggingFace tokens: hf_ followed by 34 alphanumeric chars
3273
+ regex: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{34}(?![A-Za-z0-9])/g
3274
+ },
3275
+ {
3276
+ type: "replicate_token",
3277
+ // Replicate tokens: r8_ followed by 40+ alphanumeric chars
3278
+ regex: /(?<![A-Za-z0-9])r8_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
3279
+ },
3280
+ {
3281
+ type: "perplexity_key",
3282
+ // Perplexity API keys: pplx- followed by 40+ alphanumeric chars
3283
+ regex: /(?<![A-Za-z0-9])pplx-[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
3284
+ },
3285
+ {
3286
+ type: "groq_key",
3287
+ // Groq API keys: gsk_ followed by 40+ alphanumeric chars
3288
+ regex: /(?<![A-Za-z0-9])gsk_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
3289
+ },
2237
3290
  {
2238
3291
  type: "bearer_token",
2239
3292
  // Anchored with alternation instead of negative lookahead — avoids V8
@@ -2267,6 +3320,10 @@ function hasCredentialAnchors(text) {
2267
3320
  text.includes("xox") || // Slack token (xoxa/xoxb/xoxp/xoxo/xoxs)
2268
3321
  text.includes("Bearer ") || // Bearer token (space suffix reduces false positives)
2269
3322
  text.includes("/bot") || // Telegram bot token (URL path pattern)
3323
+ text.includes("hf_") || // HuggingFace token
3324
+ text.includes("r8_") || // Replicate token
3325
+ text.includes("pplx-") || // Perplexity API key
3326
+ text.includes("gsk_") || // Groq API key
2270
3327
  text.includes("_KEY=") || // High-entropy env vars: API_KEY=, SECRET_KEY=, ...
2271
3328
  text.includes("_TOKEN=") || // ACCESS_TOKEN=, AUTH_TOKEN=, ...
2272
3329
  text.includes("_SECRET=") || // API_SECRET=, CLIENT_SECRET=, ...
@@ -2389,7 +3446,7 @@ var DefaultModelsRegistry = class {
2389
3446
  this.overlay = opts.overlay;
2390
3447
  this.overlayUrl = opts.overlayUrl;
2391
3448
  this.overlayFile = opts.overlayFile;
2392
- this.overlayCacheFile = opts.overlayCacheFile ?? (opts.overlayUrl ? path4.join(path4.dirname(opts.cacheFile), "models-overlay-cache.json") : void 0);
3449
+ this.overlayCacheFile = opts.overlayCacheFile ?? (opts.overlayUrl ? path6.join(path6.dirname(opts.cacheFile), "models-overlay-cache.json") : void 0);
2393
3450
  }
2394
3451
  async load(opts = {}) {
2395
3452
  if (this.payload && !opts.force) return this.payload;
@@ -2602,7 +3659,7 @@ var DefaultModelsRegistry = class {
2602
3659
  }
2603
3660
  /** Used by `wstack models refresh` to expose where the cache lives. */
2604
3661
  cacheLocation() {
2605
- return path4.resolve(this.cacheFile);
3662
+ return path6.resolve(this.cacheFile);
2606
3663
  }
2607
3664
  };
2608
3665
  function hasEntries(payload) {
@@ -3100,7 +4157,7 @@ var InMemoryAgentBridge = class {
3100
4157
  });
3101
4158
  }
3102
4159
  this.inflightGuards.add(correlationId);
3103
- return new Promise((resolve4, reject) => {
4160
+ return new Promise((resolve6, reject) => {
3104
4161
  const timer = setTimeout(() => {
3105
4162
  this.inflightGuards.delete(correlationId);
3106
4163
  this.pendingRequests.delete(correlationId);
@@ -3119,7 +4176,7 @@ var InMemoryAgentBridge = class {
3119
4176
  return;
3120
4177
  }
3121
4178
  this.pendingRequests.set(correlationId, {
3122
- resolve: resolve4,
4179
+ resolve: resolve6,
3123
4180
  reject,
3124
4181
  timer
3125
4182
  });
@@ -3409,7 +4466,8 @@ ${errorDetails}`,
3409
4466
  let effectivePermission = decision.permission;
3410
4467
  const policy = this.opts.permissionPolicy;
3411
4468
  const yolo = policy.getYolo?.() === true || policy.getYoloDestructive?.() === true;
3412
- if (toolDangerousCaps.length > 0 && effectivePermission === "auto" && !yolo) {
4469
+ const authoritativeAuto = decision.source === "yolo";
4470
+ if (toolDangerousCaps.length > 0 && effectivePermission === "auto" && !yolo && !authoritativeAuto) {
3413
4471
  effectivePermission = "confirm";
3414
4472
  }
3415
4473
  if (effectivePermission === "deny") {
@@ -3551,9 +4609,10 @@ ${post.additionalContext}`;
3551
4609
  });
3552
4610
  this.opts.renderer?.writeToolCall(tool.name, use.input);
3553
4611
  const output = await this.runWithTimeout(tool, use.input, ctx.signal, ctx, use.id);
3554
- const text = this.serializer.serialize(output);
4612
+ const text = this.serializer.serialize(output, { toolName: tool.name, input: use.input });
3555
4613
  const scrubbed = this.opts.secretScrubber.scrub(text);
3556
- const { text: capped, newBudget } = this.serializer.enforceCap(scrubbed, budget);
4614
+ const withArtifact = await maybePersistLargeToolOutput(tool.name, scrubbed, budget);
4615
+ const { text: capped, newBudget } = this.serializer.enforceCap(withArtifact, budget);
3557
4616
  this.opts.renderer?.writeToolResult(tool.name, capped, false);
3558
4617
  return {
3559
4618
  block: {
@@ -3578,38 +4637,27 @@ ${post.additionalContext}`;
3578
4637
  tool.timeoutMs ?? this.iterationTimeoutMs,
3579
4638
  this.maxToolTimeoutMs
3580
4639
  );
3581
- const ctrl = new AbortController();
3582
- const timer = setTimeout(() => ctrl.abort(new Error("tool timeout")), timeoutMs);
3583
- const combined = AbortSignal.any([parentSignal, ctrl.signal]);
3584
- let cleanupCalled = false;
3585
- let caught = false;
4640
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
4641
+ const combined = AbortSignal.any([parentSignal, timeoutSignal]);
4642
+ let output;
3586
4643
  try {
3587
- if (typeof tool.executeStream === "function") {
3588
- return await this.runStreamedTool(tool, input, ctx, combined, toolUseId);
3589
- }
3590
- return await tool.execute(input, ctx, { signal: combined });
4644
+ output = typeof tool.executeStream === "function" ? await this.runStreamedTool(tool, input, ctx, combined, toolUseId) : await tool.execute(input, ctx, { signal: combined });
3591
4645
  } catch (err) {
3592
- caught = true;
3593
- if (combined.aborted && typeof tool.cleanup === "function") {
3594
- cleanupCalled = true;
3595
- try {
3596
- await tool.cleanup(input, ctx);
3597
- } catch {
3598
- }
3599
- }
4646
+ if (combined.aborted) await this.runToolCleanup(tool, input, ctx);
3600
4647
  throw err;
3601
- } finally {
3602
- clearTimeout(timer);
3603
- if (combined.aborted && !caught) {
3604
- if (!cleanupCalled && typeof tool.cleanup === "function") {
3605
- try {
3606
- await tool.cleanup(input, ctx);
3607
- } catch {
3608
- }
3609
- }
3610
- const reason = combined.reason instanceof Error ? combined.reason : new Error(typeof combined.reason === "string" ? combined.reason : "aborted");
3611
- throw reason;
3612
- }
4648
+ }
4649
+ if (combined.aborted) {
4650
+ await this.runToolCleanup(tool, input, ctx);
4651
+ throw combined.reason instanceof Error ? combined.reason : new Error(typeof combined.reason === "string" ? combined.reason : "tool timeout");
4652
+ }
4653
+ return output;
4654
+ }
4655
+ /** Best-effort tool cleanup; never let it mask the original error. */
4656
+ async runToolCleanup(tool, input, ctx) {
4657
+ if (typeof tool.cleanup !== "function") return;
4658
+ try {
4659
+ await tool.cleanup(input, ctx);
4660
+ } catch {
3613
4661
  }
3614
4662
  }
3615
4663
  async runStreamedTool(tool, input, ctx, signal, toolUseId) {
@@ -3778,6 +4826,25 @@ function extractMalformedRaw(input) {
3778
4826
  return String(value);
3779
4827
  }
3780
4828
  }
4829
+ var TOOL_OUTPUT_ARTIFACT_THRESHOLD_BYTES = 64 * 1024;
4830
+ async function maybePersistLargeToolOutput(toolName, content, budget) {
4831
+ const bytes = Buffer.byteLength(content, "utf8");
4832
+ if (bytes <= Math.min(TOOL_OUTPUT_ARTIFACT_THRESHOLD_BYTES, Math.max(0, budget))) {
4833
+ return content;
4834
+ }
4835
+ try {
4836
+ const dir = path6.join(wstackGlobalRoot(), "tool-output");
4837
+ await fs.mkdir(dir, { recursive: true });
4838
+ const safeTool = toolName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
4839
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4840
+ const filePath = path6.join(dir, `${stamp}-${safeTool}-${randomUUID()}.log`);
4841
+ await fs.writeFile(filePath, content, "utf8");
4842
+ return content + `
4843
+ [full tool output: ${bytes} bytes at ${filePath}; read/grep that file selectively instead of re-running or requesting more output]`;
4844
+ } catch {
4845
+ return content;
4846
+ }
4847
+ }
3781
4848
 
3782
4849
  // src/core/conversation-state.ts
3783
4850
  var ConversationState = class {
@@ -3896,6 +4963,7 @@ var Context = class {
3896
4963
  todos = [];
3897
4964
  readFiles = /* @__PURE__ */ new Set();
3898
4965
  fileMtimes = /* @__PURE__ */ new Map();
4966
+ contextEvidence = createContextEvidenceState();
3899
4967
  systemPrompt;
3900
4968
  provider;
3901
4969
  session;
@@ -3905,6 +4973,13 @@ var Context = class {
3905
4973
  projectRoot;
3906
4974
  /** Mutable working directory — starts as `cwd`. Change via `setWorkingDir()`. */
3907
4975
  workingDir;
4976
+ /**
4977
+ * When true, file tools (via `_util.ts`) and `setWorkingDir()` reject paths
4978
+ * outside `projectRoot`. When false, those boundary checks are bypassed so
4979
+ * tools may reach paths outside the project (still gated by permission
4980
+ * tiers). Mutable so `/settings` can toggle it live on the running session.
4981
+ */
4982
+ allowOutsideProjectRoot;
3908
4983
  model;
3909
4984
  tools = [];
3910
4985
  meta = {};
@@ -3918,11 +4993,6 @@ var Context = class {
3918
4993
  * so storage operations can include it in `storage.*` events.
3919
4994
  */
3920
4995
  traceId;
3921
- /**
3922
- * When true, tools can access any path on the filesystem.
3923
- * When false or undefined, tools are restricted to the project root.
3924
- */
3925
- allowOutsideProjectRoot;
3926
4996
  /** Callbacks fired when `setWorkingDir()` changes the working directory. */
3927
4997
  _onWorkingDirChanged = [];
3928
4998
  /**
@@ -3954,12 +5024,13 @@ var Context = class {
3954
5024
  this.cwd = init.cwd;
3955
5025
  this.projectRoot = init.projectRoot;
3956
5026
  this.workingDir = init.workingDir ?? init.cwd;
5027
+ this.allowOutsideProjectRoot = init.allowOutsideProjectRoot ?? false;
3957
5028
  this.model = init.model;
3958
5029
  this.tools = init.tools ?? [];
3959
5030
  this.agentId = init.agentId ?? "unknown";
3960
5031
  this.agentName = init.agentName ?? "Unknown Agent";
3961
5032
  this.traceId = init.traceId;
3962
- this.allowOutsideProjectRoot = init.allowOutsideProjectRoot ?? true;
5033
+ this.allowOutsideProjectRoot = init.allowOutsideProjectRoot ?? false;
3963
5034
  this.session.traceId = init.traceId;
3964
5035
  }
3965
5036
  /**
@@ -4024,13 +5095,15 @@ var Context = class {
4024
5095
  * Returns the resolved absolute path.
4025
5096
  */
4026
5097
  setWorkingDir(dir) {
4027
- const resolved = path4.isAbsolute(dir) ? path4.resolve(dir) : path4.resolve(this.projectRoot, dir);
4028
- const root = path4.resolve(this.projectRoot);
4029
- const rel = path4.relative(root, resolved);
4030
- if (rel.startsWith("..") || path4.isAbsolute(rel)) {
4031
- throw new Error(
4032
- `Working directory "${resolved}" is outside project root "${root}"`
4033
- );
5098
+ const resolved = path6.isAbsolute(dir) ? path6.resolve(dir) : path6.resolve(this.projectRoot, dir);
5099
+ if (!this.allowOutsideProjectRoot) {
5100
+ const root = path6.resolve(this.projectRoot);
5101
+ const rel = path6.relative(root, resolved);
5102
+ if (rel.startsWith("..") || path6.isAbsolute(rel)) {
5103
+ throw new Error(
5104
+ `Working directory "${resolved}" is outside project root "${root}"`
5105
+ );
5106
+ }
4034
5107
  }
4035
5108
  const old = this.workingDir;
4036
5109
  this.workingDir = resolved;
@@ -4336,6 +5409,6 @@ function renderPlainText(meta, events) {
4336
5409
  return lines.join("\n");
4337
5410
  }
4338
5411
 
4339
- export { AgentError, CONTEXT_WINDOW_MODES, ConfigError, Context, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONTEXT_CONFIG, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SESSION_LOGGING_CONFIG, DEFAULT_SESSION_PRUNE_DAYS, DEFAULT_SPEC_TEMPLATE, DEFAULT_TOOLS_CONFIG, DefaultErrorHandler, DefaultLogger, DefaultModelsRegistry, DefaultPathResolver, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultTokenCounter, ERROR_CODES, FsError, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, MEMORY_TYPE_LABELS, PluginError, ProviderError, SddError, SessionError, StreamHangError, ToolError, ToolExecutor, WrongStackError, asBlocks, asText, buildRecoveryStrategies, classifyFamily, computeTaskProgress, createMessage, decryptConfigSecrets, encryptConfigSecrets, findCriticalPath, formatContextWindowModeList, getContextWindowMode, isAgentError, isConfigError, isContextWindowModeId, isFsError, isImageBlock, isPluginError, isSddError, isSecretField, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, listContextWindowModes, migratePlaintextSecrets, noOpLogger, resolveContextWindowPolicy, rewriteConfigEncrypted, toWrongStackError, topologicalSort };
5412
+ export { AgentError, CONTEXT_WINDOW_MODES, ConfigError, Context, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CIRCUIT_BREAKER_CONFIG, DEFAULT_CONTEXT_CONFIG, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SESSION_LOGGING_CONFIG, DEFAULT_SESSION_PRUNE_DAYS, DEFAULT_SPEC_TEMPLATE, DEFAULT_TOOLS_CONFIG, DefaultErrorHandler, DefaultLogger, DefaultModelsRegistry, DefaultPathResolver, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultTokenCounter, ERROR_CODES, FsError, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, MEMORY_TYPE_LABELS, PluginError, ProviderError, SddError, SessionError, StreamHangError, ToolError, ToolExecutor, WrongStackError, asBlocks, asText, buildRecoveryStrategies, classifyFamily, computeTaskProgress, createMessage, decryptConfigSecrets, encryptConfigSecrets, findCriticalPath, formatContextWindowModeList, getContextWindowMode, isAgentError, isConfigError, isContextWindowModeId, isFsError, isImageBlock, isPluginError, isSddError, isSecretField, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, listContextWindowModes, migratePlaintextSecrets, noOpLogger, normalizeTokenSavingTier, resolveContextWindowPolicy, rewriteConfigEncrypted, rotateConfigKeys, toWrongStackError, topologicalSort };
4340
5413
  //# sourceMappingURL=index.js.map
4341
5414
  //# sourceMappingURL=index.js.map