@wrongstack/core 0.265.1 → 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 (69) hide show
  1. package/dist/{agent-bridge-DrkBxszZ.d.ts → agent-bridge-STJ3JwwK.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-DM2pP-B6.d.ts → agent-subagent-runner-CzPGP3jA.d.ts} +25 -7
  3. package/dist/{brain-BXd_61kQ.d.ts → brain-Cdg77tVN.d.ts} +73 -1
  4. package/dist/{compactor-B8pOf45Y.d.ts → compactor-iMZ84CXq.d.ts} +19 -1
  5. package/dist/{config-BMCj_XDs.d.ts → config-Du3pYYln.d.ts} +54 -3
  6. package/dist/{context-MRk5PhNv.d.ts → context-dT5Ueund.d.ts} +65 -1
  7. package/dist/coordination/index.d.ts +17 -17
  8. package/dist/coordination/index.js +138 -114
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/defaults/index.d.ts +25 -25
  11. package/dist/defaults/index.js +1729 -781
  12. package/dist/defaults/index.js.map +1 -1
  13. package/dist/execution/index.d.ts +15 -15
  14. package/dist/execution/index.js +1119 -229
  15. package/dist/execution/index.js.map +1 -1
  16. package/dist/execution/prompt-enhancer.d.ts +1 -1
  17. package/dist/extension/index.d.ts +6 -6
  18. package/dist/{goal-preamble-DvHDSKSe.d.ts → goal-preamble-SulMTowG.d.ts} +28 -11
  19. package/dist/{goal-store-DtLMySNb.d.ts → goal-store-CABDwdFE.d.ts} +1 -1
  20. package/dist/{index-CEDeNodM.d.ts → index-Bms0m4oy.d.ts} +5 -5
  21. package/dist/{index-B-ch8K9C.d.ts → index-DtCVWel4.d.ts} +8 -8
  22. package/dist/index-IEuxQd-E.d.ts +82 -0
  23. package/dist/index.d.ts +118 -45
  24. package/dist/index.js +3083 -1602
  25. package/dist/index.js.map +1 -1
  26. package/dist/infrastructure/index.d.ts +6 -6
  27. package/dist/infrastructure/index.js +72 -1
  28. package/dist/infrastructure/index.js.map +1 -1
  29. package/dist/kernel/index.d.ts +9 -9
  30. package/dist/kernel/index.js.map +1 -1
  31. package/dist/{mcp-servers-2x4w6Jn9.d.ts → mcp-servers-C2cBTxUR.d.ts} +3 -3
  32. package/dist/models/index.d.ts +5 -5
  33. package/dist/models/index.js +30 -1
  34. package/dist/models/index.js.map +1 -1
  35. package/dist/{models-registry-DmJlKuNp.d.ts → models-registry-BqGZNJQ-.d.ts} +1 -1
  36. package/dist/{multi-agent-coordinator-DyCkCZnU.d.ts → multi-agent-coordinator-B8R43uPz.d.ts} +1 -1
  37. package/dist/{null-fleet-bus-CG9QY2aP.d.ts → null-fleet-bus-CnXa5oTH.d.ts} +14 -9
  38. package/dist/observability/index.d.ts +2 -2
  39. package/dist/{parallel-eternal-engine-Jw9uhEoT.d.ts → parallel-eternal-engine-DdNnw9BQ.d.ts} +11 -9
  40. package/dist/{path-resolver-Dy2ej-gE.d.ts → path-resolver-COIMLCQL.d.ts} +3 -3
  41. package/dist/{permission-B9SB45lp.d.ts → permission-B75JAi3-.d.ts} +1 -1
  42. package/dist/{permission-policy-CkjSXabK.d.ts → permission-policy-DlR9eJAM.d.ts} +2 -2
  43. package/dist/{pipeline-DPDxH_7m.d.ts → pipeline-BfD2k1rT.d.ts} +2 -2
  44. package/dist/{plan-templates-CzD9GnAU.d.ts → plan-templates-DSIKCXZN.d.ts} +5 -5
  45. package/dist/{llm-selector-C0tfTCUe.d.ts → provider-model-resolve-BNRsNuJx.d.ts} +40 -3
  46. package/dist/{provider-runner-DMa70ODu.d.ts → provider-runner-CX7iIvox.d.ts} +3 -3
  47. package/dist/{retry-policy-CN0khdlj.d.ts → retry-policy-BilV1ujH.d.ts} +1 -1
  48. package/dist/sdd/index.d.ts +8 -8
  49. package/dist/sdd/index.js +12 -12
  50. package/dist/sdd/index.js.map +1 -1
  51. package/dist/{secret-vault-B2yw84VT.d.ts → secret-vault-gkvEZZfE.d.ts} +2 -2
  52. package/dist/security/index.d.ts +5 -67
  53. package/dist/security/index.js +96 -76
  54. package/dist/security/index.js.map +1 -1
  55. package/dist/{selector-CzHh_igB.d.ts → selector-Bc7eWtT3.d.ts} +1 -1
  56. package/dist/{session-event-bridge-BUI6Jf-4.d.ts → session-event-bridge-D-araDEz.d.ts} +1 -1
  57. package/dist/{session-reader-CMgdMSRP.d.ts → session-reader-D7Dapswh.d.ts} +1 -1
  58. package/dist/storage/index.d.ts +11 -11
  59. package/dist/storage/index.js +81 -84
  60. package/dist/storage/index.js.map +1 -1
  61. package/dist/tools/index.d.ts +4 -2
  62. package/dist/tools/index.js.map +1 -1
  63. package/dist/types/index.d.ts +19 -19
  64. package/dist/types/index.js +1265 -400
  65. package/dist/types/index.js.map +1 -1
  66. package/dist/utils/index.d.ts +454 -406
  67. package/dist/utils/index.js +2191 -1201
  68. package/dist/utils/index.js.map +1 -1
  69. package/package.json +1 -1
@@ -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,48 +268,381 @@ 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);
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;
300
+ }
301
+ }
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;
309
+ }
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" });
232
316
  }
233
- try {
234
- return JSON.stringify(value, null, 2);
235
- } catch {
236
- return String(value);
317
+ }
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
+ }
237
323
  }
238
324
  }
239
- return String(value);
240
325
  }
241
- function enforceCap(text, remainingBudget) {
242
- if (remainingBudget <= 0) {
243
- return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
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);
244
329
  }
245
- const textBytes = Buffer.byteLength(text, "utf8");
246
- if (textBytes <= remainingBudget) {
247
- return { text, newBudget: remainingBudget - textBytes };
330
+ }
331
+ }
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;
350
+ }
351
+ }
352
+ function isPlainObject(v) {
353
+ return typeof v === "object" && v !== null && !Array.isArray(v);
354
+ }
355
+ function describeType(v) {
356
+ if (v === null) return "null";
357
+ if (Array.isArray(v)) return "array";
358
+ return typeof v;
359
+ }
360
+ function joinPath(parent, key) {
361
+ if (!parent) return key;
362
+ return `${parent}.${key}`;
363
+ }
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]));
370
+ }
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;
380
+ }
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);
387
+ }
388
+ for (const [id, ovProvider] of Object.entries(overlay)) {
389
+ const existing = out[id];
390
+ out[id] = existing ? mergeProvider(existing, ovProvider) : cloneProvider(ovProvider);
391
+ }
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 };
402
+ }
403
+ return {
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
415
+ };
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
+ }
444
+
445
+ // src/utils/message-invariants.ts
446
+ function repairToolUseAdjacency(messages) {
447
+ const removedToolUses = [];
448
+ const removedToolResults = [];
449
+ let removedMessages = 0;
450
+ let changed = false;
451
+ const out = [];
452
+ for (let i = 0; i < messages.length; i++) {
453
+ const original = expectDefined(messages[i]);
454
+ let msg = original;
455
+ if (hasToolUse(msg)) {
456
+ const nextIds = toolResultIds(messages[i + 1]);
457
+ const filtered = mapContent(msg, (blocks) => {
458
+ const next = [];
459
+ for (const block of blocks) {
460
+ if (block.type === "tool_use" && !nextIds.has(block.id)) {
461
+ removedToolUses.push(block.id);
462
+ changed = true;
463
+ continue;
464
+ }
465
+ next.push(block);
466
+ }
467
+ return next;
468
+ });
469
+ msg = filtered ?? msg;
248
470
  }
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 };
471
+ if (hasToolResult(msg)) {
472
+ const allowed = toolUseIds(out[out.length - 1]);
473
+ const filtered = mapContent(msg, (blocks) => {
474
+ const next = [];
475
+ for (const block of blocks) {
476
+ if (block.type === "tool_result" && !allowed.has(block.tool_use_id)) {
477
+ removedToolResults.push(block.tool_use_id);
478
+ changed = true;
479
+ continue;
480
+ }
481
+ next.push(block);
482
+ }
483
+ return next;
484
+ });
485
+ msg = filtered ?? msg;
256
486
  }
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 };
487
+ if (isEmptyMessage(msg)) {
488
+ removedMessages++;
489
+ changed = true;
490
+ continue;
491
+ }
492
+ out.push(msg);
261
493
  }
262
- return { serialize, enforceCap, capBytes };
494
+ return {
495
+ messages: changed ? out : messages,
496
+ report: { changed, removedToolUses, removedToolResults, removedMessages }
497
+ };
498
+ }
499
+ function hasToolUse(msg) {
500
+ return contentBlocks(msg).some((b) => b.type === "tool_use");
501
+ }
502
+ function hasToolResult(msg) {
503
+ return contentBlocks(msg).some((b) => b.type === "tool_result");
504
+ }
505
+ function toolUseIds(msg) {
506
+ const ids = /* @__PURE__ */ new Set();
507
+ if (!msg || msg.role !== "assistant") return ids;
508
+ for (const block of contentBlocks(msg)) {
509
+ if (block.type === "tool_use") ids.add(block.id);
510
+ }
511
+ return ids;
512
+ }
513
+ function toolResultIds(msg) {
514
+ const ids = /* @__PURE__ */ new Set();
515
+ if (!msg || msg.role !== "user") return ids;
516
+ for (const block of contentBlocks(msg)) {
517
+ if (block.type === "tool_result") ids.add(block.tool_use_id);
518
+ }
519
+ return ids;
520
+ }
521
+ function contentBlocks(msg) {
522
+ return msg && Array.isArray(msg.content) ? msg.content : [];
523
+ }
524
+ function mapContent(msg, fn) {
525
+ if (!Array.isArray(msg.content)) return msg;
526
+ const next = fn(msg.content);
527
+ if (next.length === msg.content.length && next.every((b, idx) => b === msg.content[idx])) {
528
+ return msg;
529
+ }
530
+ return { ...msg, content: next };
531
+ }
532
+ function isEmptyMessage(msg) {
533
+ if (typeof msg.content === "string") return msg.content.trim().length === 0;
534
+ return msg.content.length === 0;
535
+ }
536
+
537
+ // src/utils/regex-guard.ts
538
+ var MAX_PATTERN_LEN = 512;
539
+ var DANGEROUS_PATTERNS = [
540
+ /(\([^)]*[+*][^)]*\))[+*]/,
541
+ // (a+)+, (.*)+, etc
542
+ /(\(\?:[^)]*[+*][^)]*\))[+*]/
543
+ // same, with non-capturing group
544
+ ];
545
+ function compileUserRegex(pattern, flags) {
546
+ if (typeof pattern !== "string") {
547
+ return { ok: false, reason: "pattern must be a string" };
548
+ }
549
+ if (pattern.length === 0) {
550
+ return { ok: false, reason: "pattern is empty" };
551
+ }
552
+ if (pattern.length > MAX_PATTERN_LEN) {
553
+ return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
554
+ }
555
+ for (const rx of DANGEROUS_PATTERNS) {
556
+ if (rx.test(pattern)) {
557
+ return {
558
+ ok: false,
559
+ reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
560
+ };
561
+ }
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);
263
646
  }
264
647
 
265
648
  // src/utils/token-estimate.ts
@@ -328,7 +711,8 @@ function estimateMessageTokens(messages) {
328
711
  function estimateToolDefTokens(tool) {
329
712
  const cached = tool._estDefTokens;
330
713
  if (typeof cached === "number" && cached > 0) return cached;
331
- return RoughTokenEstimate(tool.name) + RoughTokenEstimate(tool.description ?? "") + RoughTokenEstimate(JSON.stringify(tool.inputSchema));
714
+ const compact = compactToolDefinitionForWire(tool);
715
+ return RoughTokenEstimate(tool.name) + RoughTokenEstimate(compact.description) + RoughTokenEstimate(JSON.stringify(compact.inputSchema));
332
716
  }
333
717
  function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
334
718
  let messagesTokens = 0;
@@ -383,291 +767,650 @@ function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = C
383
767
  };
384
768
  }
385
769
 
386
- // src/utils/message-invariants.ts
387
- function repairToolUseAdjacency(messages) {
388
- const removedToolUses = [];
389
- const removedToolResults = [];
390
- let removedMessages = 0;
391
- let changed = false;
392
- const out = [];
393
- for (let i = 0; i < messages.length; i++) {
394
- const original = expectDefined(messages[i]);
395
- let msg = original;
396
- if (hasToolUse(msg)) {
397
- const nextIds = toolResultIds(messages[i + 1]);
398
- const filtered = mapContent(msg, (blocks) => {
399
- const next = [];
400
- for (const block of blocks) {
401
- if (block.type === "tool_use" && !nextIds.has(block.id)) {
402
- removedToolUses.push(block.id);
403
- changed = true;
404
- continue;
405
- }
406
- next.push(block);
407
- }
408
- return next;
409
- });
410
- msg = filtered ?? msg;
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
+ }
411
800
  }
412
- if (hasToolResult(msg)) {
413
- const allowed = toolUseIds(out[out.length - 1]);
414
- const filtered = mapContent(msg, (blocks) => {
415
- const next = [];
416
- for (const block of blocks) {
417
- if (block.type === "tool_result" && !allowed.has(block.tool_use_id)) {
418
- removedToolResults.push(block.tool_use_id);
419
- changed = true;
420
- continue;
421
- }
422
- next.push(block);
423
- }
424
- return next;
425
- });
426
- msg = filtered ?? msg;
801
+ return String(value);
802
+ }
803
+ function enforceCap(text, remainingBudget) {
804
+ if (remainingBudget <= 0) {
805
+ return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
427
806
  }
428
- if (isEmptyMessage(msg)) {
429
- removedMessages++;
430
- changed = true;
431
- continue;
807
+ const textBytes = Buffer.byteLength(text, "utf8");
808
+ if (textBytes <= remainingBudget) {
809
+ return { text, newBudget: remainingBudget - textBytes };
432
810
  }
433
- out.push(msg);
434
- }
435
- return {
436
- messages: changed ? out : messages,
437
- report: { changed, removedToolUses, removedToolResults, removedMessages }
438
- };
439
- }
440
- function hasToolUse(msg) {
441
- return contentBlocks(msg).some((b) => b.type === "tool_use");
442
- }
443
- function hasToolResult(msg) {
444
- return contentBlocks(msg).some((b) => b.type === "tool_result");
445
- }
446
- function toolUseIds(msg) {
447
- const ids = /* @__PURE__ */ new Set();
448
- if (!msg || msg.role !== "assistant") return ids;
449
- for (const block of contentBlocks(msg)) {
450
- if (block.type === "tool_use") ids.add(block.id);
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 };
451
823
  }
452
- return ids;
824
+ return { serialize, enforceCap, capBytes };
453
825
  }
454
- function toolResultIds(msg) {
455
- const ids = /* @__PURE__ */ new Set();
456
- if (!msg || msg.role !== "user") return ids;
457
- for (const block of contentBlocks(msg)) {
458
- if (block.type === "tool_result") ids.add(block.tool_use_id);
459
- }
460
- return ids;
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;
461
1041
  }
462
- function contentBlocks(msg) {
463
- return msg && Array.isArray(msg.content) ? msg.content : [];
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
+ ]);
464
1075
  }
465
- function mapContent(msg, fn) {
466
- if (!Array.isArray(msg.content)) return msg;
467
- const next = fn(msg.content);
468
- if (next.length === msg.content.length && next.every((b, idx) => b === msg.content[idx])) {
469
- return msg;
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
+ );
470
1153
  }
471
- return { ...msg, content: next };
1154
+ if (groups.size > GREP_FILE_LIMIT) {
1155
+ sections.push(`[serializer omitted ${groups.size - GREP_FILE_LIMIT} file group(s)]`);
1156
+ }
1157
+ if (passthrough.length > 0) {
1158
+ sections.push(`ungrouped:
1159
+ ${renderStringList(passthrough, "", 50)}`);
1160
+ }
1161
+ return sections.join("\n");
472
1162
  }
473
- function isEmptyMessage(msg) {
474
- if (typeof msg.content === "string") return msg.content.trim().length === 0;
475
- return msg.content.length === 0;
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] ?? "" };
476
1167
  }
477
-
478
- // src/utils/json-schema-validate.ts
479
- function validateAgainstSchema(value, schema) {
480
- const errors = [];
481
- walk(value, schema, "", errors);
482
- return { ok: errors.length === 0, errors };
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
+ }
1196
+ }
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
1230
+ }),
1231
+ excerpt.join("\n")
1232
+ ]);
483
1233
  }
484
- function walk(value, schema, path7, errors) {
485
- if (schema.enum !== void 0) {
486
- if (!schema.enum.some((e) => deepEqual(e, value))) {
487
- errors.push({
488
- path: path7 || "<root>",
489
- message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
490
- });
491
- return;
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);
492
1245
  }
493
1246
  }
494
- if (typeof schema.type === "string") {
495
- if (!checkType(value, schema.type)) {
496
- errors.push({
497
- path: path7 || "<root>",
498
- message: `expected ${schema.type}, got ${describeType(value)}`
499
- });
500
- return;
1247
+ if (markerHits === 0) {
1248
+ return lines.slice(-220).join("\n").trimEnd();
1249
+ }
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)]`);
501
1257
  }
1258
+ out.push(lines[index] ?? "");
1259
+ previous = index;
502
1260
  }
503
- if (schema.type === "object" && isPlainObject(value)) {
504
- const obj = value;
505
- for (const req of schema.required ?? []) {
506
- if (!(req in obj)) {
507
- errors.push({ path: joinPath(path7, req), message: "required property missing" });
1261
+ return out.join("\n").trimEnd();
1262
+ }
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}`);
508
1319
  }
1320
+ continue;
509
1321
  }
510
- if (schema.properties) {
511
- for (const [key, subSchema] of Object.entries(schema.properties)) {
512
- if (key in obj) {
513
- walk(obj[key], subSchema, joinPath(path7, key), errors);
514
- }
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)}`);
515
1329
  }
1330
+ continue;
516
1331
  }
1332
+ blocks.push(`${key}: ${clipInline(oneLineJson(value))}`);
517
1333
  }
518
- if (schema.type === "array" && Array.isArray(value) && schema.items) {
519
- for (let i = 0; i < value.length; i++) {
520
- walk(value[i], schema.items, `${path7}[${i}]`, errors);
521
- }
522
- }
1334
+ return joinSections([renderHeader(toolName, scalars), ...blocks]);
523
1335
  }
524
- function checkType(value, type) {
525
- switch (type) {
526
- case "string":
527
- return typeof value === "string";
528
- case "number":
529
- return typeof value === "number" && !Number.isNaN(value);
530
- case "integer":
531
- return typeof value === "number" && Number.isInteger(value);
532
- case "boolean":
533
- return typeof value === "boolean";
534
- case "null":
535
- return value === null;
536
- case "array":
537
- return Array.isArray(value);
538
- case "object":
539
- return isPlainObject(value);
540
- default:
541
- return true;
542
- }
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;
543
1339
  }
544
- function isPlainObject(v) {
545
- return typeof v === "object" && v !== null && !Array.isArray(v);
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");
546
1348
  }
547
- function describeType(v) {
548
- if (v === null) return "null";
549
- if (Array.isArray(v)) return "array";
550
- return typeof v;
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");
551
1355
  }
552
- function joinPath(parent, key) {
553
- if (!parent) return key;
554
- return `${parent}.${key}`;
1356
+ function joinSections(sections) {
1357
+ return sections.map((section) => typeof section === "string" ? section.trimEnd() : void 0).filter((section) => !!section).join("\n");
555
1358
  }
556
- function deepEqual(a, b) {
557
- if (a === b) return true;
558
- if (typeof a !== typeof b) return false;
559
- if (a === null || b === null) return a === b;
560
- if (Array.isArray(a) && Array.isArray(b)) {
561
- return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
562
- }
563
- if (typeof a === "object" && typeof b === "object") {
564
- const ak = Object.keys(a);
565
- const bk = Object.keys(b);
566
- if (ak.length !== bk.length) return false;
567
- return ak.every(
568
- (k) => deepEqual(a[k], b[k])
569
- );
570
- }
571
- return false;
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);
572
1363
  }
573
-
574
- // src/utils/regex-guard.ts
575
- var MAX_PATTERN_LEN = 512;
576
- var DANGEROUS_PATTERNS = [
577
- /(\([^)]*[+*][^)]*\))[+*]/,
578
- // (a+)+, (.*)+, etc
579
- /(\(\?:[^)]*[+*][^)]*\))[+*]/
580
- // same, with non-capturing group
581
- ];
582
- function compileUserRegex(pattern, flags) {
583
- if (typeof pattern !== "string") {
584
- return { ok: false, reason: "pattern must be a string" };
585
- }
586
- if (pattern.length === 0) {
587
- return { ok: false, reason: "pattern is empty" };
588
- }
589
- if (pattern.length > MAX_PATTERN_LEN) {
590
- return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
591
- }
592
- for (const rx of DANGEROUS_PATTERNS) {
593
- if (rx.test(pattern)) {
594
- return {
595
- ok: false,
596
- reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
597
- };
598
- }
599
- }
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) {
600
1369
  try {
601
- return { ok: true, regex: new RegExp(pattern, flags) };
602
- } catch (err) {
603
- return {
604
- ok: false,
605
- reason: err instanceof Error ? err.message : "invalid regex"
606
- };
1370
+ return JSON.stringify(value);
1371
+ } catch {
1372
+ return String(value);
607
1373
  }
608
1374
  }
609
-
610
- // src/utils/merge-models-payload.ts
611
- function mergeModelsPayload(base, overlay) {
612
- const out = {};
613
- for (const [id, provider] of Object.entries(base)) {
614
- out[id] = cloneProvider(provider);
615
- }
616
- for (const [id, ovProvider] of Object.entries(overlay)) {
617
- const existing = out[id];
618
- out[id] = existing ? mergeProvider(existing, ovProvider) : cloneProvider(ovProvider);
619
- }
620
- return out;
1375
+ function stringField(obj, key) {
1376
+ const value = obj[key];
1377
+ return typeof value === "string" ? value : void 0;
621
1378
  }
622
- function mergeProvider(base, overlay) {
623
- const models = {};
624
- for (const [mid, m] of Object.entries(base.models ?? {})) {
625
- models[mid] = { ...m };
626
- }
627
- for (const [mid, ovModel] of Object.entries(overlay.models ?? {})) {
628
- const existing = models[mid];
629
- models[mid] = existing ? mergeModel(existing, ovModel) : { ...ovModel };
630
- }
631
- return {
632
- ...base,
633
- // Overlay scalar fields win when explicitly provided; otherwise keep base.
634
- ...stripUndefined({
635
- id: overlay.id,
636
- name: overlay.name,
637
- npm: overlay.npm,
638
- api: overlay.api,
639
- env: overlay.env,
640
- doc: overlay.doc
641
- }),
642
- models
643
- };
1379
+ function numberField(obj, key) {
1380
+ const value = obj[key];
1381
+ return typeof value === "number" ? value : void 0;
644
1382
  }
645
- function mergeModel(base, overlay) {
646
- const merged = { ...base, ...overlay };
647
- if (base.limit || overlay.limit) {
648
- merged.limit = { ...base.limit, ...overlay.limit };
649
- }
650
- if (base.cost || overlay.cost) {
651
- merged.cost = { ...base.cost, ...overlay.cost };
652
- }
653
- if (base.modalities || overlay.modalities) {
654
- merged.modalities = { ...base.modalities, ...overlay.modalities };
655
- }
656
- return merged;
1383
+ function stringArrayField(obj, key) {
1384
+ const value = obj[key];
1385
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
657
1386
  }
658
- function cloneProvider(p) {
659
- const models = {};
660
- for (const [mid, m] of Object.entries(p.models ?? {})) {
661
- models[mid] = { ...m };
662
- }
663
- return { ...p, models };
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;
664
1391
  }
665
- function stripUndefined(obj) {
666
- const out = {};
667
- for (const [k, v] of Object.entries(obj)) {
668
- if (v !== void 0) out[k] = v;
669
- }
670
- return out;
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");
671
1414
  }
672
1415
 
673
1416
  // src/types/errors.ts
@@ -1150,7 +1893,7 @@ var DefaultSecretVault = class {
1150
1893
  KEY_FILE_MAGIC.copy(keyFileBuf, 0);
1151
1894
  keyFileBuf[KEY_FILE_MAGIC.length] = newVersion;
1152
1895
  newKey.copy(keyFileBuf, KEY_FILE_MAGIC.length + 1);
1153
- fs2.mkdirSync(path4.dirname(this.keyFile), { recursive: true });
1896
+ fs2.mkdirSync(path6.dirname(this.keyFile), { recursive: true });
1154
1897
  fs2.writeFileSync(this.keyFile, keyFileBuf, { mode: 384 });
1155
1898
  checkKeyFilePermissions(this.keyFile);
1156
1899
  this.key = newKey;
@@ -1198,7 +1941,7 @@ var DefaultSecretVault = class {
1198
1941
  } catch (err) {
1199
1942
  if (err.code !== "ENOENT") throw err;
1200
1943
  }
1201
- fs2.mkdirSync(path4.dirname(this.keyFile), { recursive: true });
1944
+ fs2.mkdirSync(path6.dirname(this.keyFile), { recursive: true });
1202
1945
  const key = randomBytes(KEY_BYTES);
1203
1946
  try {
1204
1947
  fs2.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
@@ -1288,7 +2031,7 @@ async function rewriteConfigEncrypted(configPath, vault, patch) {
1288
2031
  }
1289
2032
  const merged = deepMerge(current, patch ?? {});
1290
2033
  const encrypted = encryptConfigSecrets(merged, vault);
1291
- await fs.mkdir(path4.dirname(configPath), { recursive: true });
2034
+ await fs.mkdir(path6.dirname(configPath), { recursive: true });
1292
2035
  await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
1293
2036
  await restrictFilePermissions(configPath);
1294
2037
  }
@@ -1334,8 +2077,13 @@ async function rotateConfigKeys(configPath, vault, logger) {
1334
2077
  warn(`[secret-vault] Config file ${configPath} is not valid JSON \u2014 skipping rotation`);
1335
2078
  return { rotated: 0, oldVersion: vault.keyVersion, newVersion: vault.keyVersion, file: configPath };
1336
2079
  }
1337
- const counter = { n: 0 };
2080
+ const counter = { n: 0, failed: [] };
1338
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
+ }
1339
2087
  if (counter.n === 0) {
1340
2088
  const { oldVersion: oldVersion2, newVersion: newVersion2 } = vault.rotateKey();
1341
2089
  log(`[secret-vault] Key rotated (v${oldVersion2} \u2192 v${newVersion2}) \u2014 no encrypted fields to re-encrypt`);
@@ -1348,23 +2096,27 @@ async function rotateConfigKeys(configPath, vault, logger) {
1348
2096
  log(`[secret-vault] Key rotated (v${oldVersion} \u2192 v${newVersion}) \u2014 re-encrypted ${counter.n} field(s)`);
1349
2097
  return { rotated: counter.n, oldVersion, newVersion, file: configPath };
1350
2098
  }
1351
- function walkDecryptCount(node, vault, counter) {
2099
+ function walkDecryptCount(node, vault, counter, pathPrefix = "") {
1352
2100
  if (node === null || node === void 0) return node;
1353
2101
  if (typeof node !== "object") return node;
1354
2102
  if (Array.isArray(node)) {
1355
- return node.map((item) => walkDecryptCount(item, vault, counter));
2103
+ return node.map(
2104
+ (item, i) => walkDecryptCount(item, vault, counter, `${pathPrefix}[${i}]`)
2105
+ );
1356
2106
  }
1357
2107
  const out = /* @__PURE__ */ Object.create(null);
1358
2108
  for (const [k, v] of Object.entries(node)) {
2109
+ const keyPath = pathPrefix ? `${pathPrefix}.${k}` : k;
1359
2110
  if (typeof v === "string" && vault.isEncrypted(v)) {
1360
2111
  try {
1361
2112
  out[k] = vault.decrypt(v);
1362
2113
  counter.n++;
1363
2114
  } catch {
2115
+ counter.failed.push(keyPath);
1364
2116
  out[k] = v;
1365
2117
  }
1366
2118
  } else if (typeof v === "object" && v !== null) {
1367
- out[k] = walkDecryptCount(v, vault, counter);
2119
+ out[k] = walkDecryptCount(v, vault, counter, keyPath);
1368
2120
  } else {
1369
2121
  out[k] = v;
1370
2122
  }
@@ -1477,7 +2229,7 @@ var DefaultLogger = class _DefaultLogger {
1477
2229
  this.maxFileBytes = opts.maxFileBytes ?? 10 * 1024 * 1024;
1478
2230
  if (this.file) {
1479
2231
  try {
1480
- fs2.mkdirSync(path4.dirname(this.file), { recursive: true });
2232
+ fs2.mkdirSync(path6.dirname(this.file), { recursive: true });
1481
2233
  } catch {
1482
2234
  }
1483
2235
  }
@@ -1778,25 +2530,26 @@ function findPreserveStart(messages, preserveK) {
1778
2530
  preserveStart = i;
1779
2531
  }
1780
2532
  }
1781
- let forwardWalkIterations = 0;
1782
- let forwardWalkInnerIterations = 0;
1783
- for (let i = preserveStart; i < messages.length; i++) {
1784
- forwardWalkIterations++;
1785
- const m = messages[i];
1786
- if (!m || typeof m.content === "string" || !Array.isArray(m.content)) continue;
1787
- const hasToolUse3 = m.content.some((b) => {
1788
- forwardWalkInnerIterations++;
1789
- return b.type === "tool_use";
1790
- });
1791
- if (hasToolUse3 && i + 1 < messages.length) {
1792
- const next = messages[i + 1];
1793
- if (next && next.role === "user" && typeof next.content !== "string" && Array.isArray(next.content) && next.content.some((b) => {
1794
- forwardWalkInnerIterations++;
1795
- return b.type === "tool_result";
1796
- })) {
1797
- preserveStart = i + 1;
1798
- }
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);
1799
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--;
1800
2553
  }
1801
2554
  if (compactionDebugEnabled()) {
1802
2555
  console.log(
@@ -1806,9 +2559,9 @@ function findPreserveStart(messages, preserveK) {
1806
2559
  messageCount: messages.length,
1807
2560
  preserveK,
1808
2561
  preserveStart,
1809
- forwardWalkIterations,
1810
- forwardWalkInnerIterations,
1811
- forwardWalkInnerPerOuter: forwardWalkIterations > 0 ? forwardWalkInnerIterations / forwardWalkIterations : 0
2562
+ pairRepairIterations,
2563
+ pairRepairInnerIterations,
2564
+ pairRepairInnerPerOuter: pairRepairIterations > 0 ? pairRepairInnerIterations / pairRepairIterations : 0
1812
2565
  })
1813
2566
  );
1814
2567
  }
@@ -1825,7 +2578,8 @@ function eliseOldToolResults(messages, opts) {
1825
2578
  if (!msg || !Array.isArray(msg.content)) continue;
1826
2579
  for (const b of msg.content) {
1827
2580
  fastPathInnerIterations++;
1828
- 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) {
1829
2583
  hasOversized = true;
1830
2584
  break;
1831
2585
  }
@@ -1859,6 +2613,13 @@ function eliseOldToolResults(messages, opts) {
1859
2613
  }
1860
2614
  const original = msg.content;
1861
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
+ }
1862
2623
  if (b.type !== "tool_result") return b;
1863
2624
  const tokens = estimateToolResultTokens(b.content);
1864
2625
  if (tokens < opts.eliseThreshold) return b;
@@ -1866,7 +2627,7 @@ function eliseOldToolResults(messages, opts) {
1866
2627
  const elided = {
1867
2628
  type: "tool_result",
1868
2629
  tool_use_id: b.tool_use_id,
1869
- content: `[elided: ~${tokens} tokens]`,
2630
+ content: summarizeToolResultElision(b, tokens),
1870
2631
  is_error: b.is_error
1871
2632
  };
1872
2633
  return elided;
@@ -1906,6 +2667,65 @@ function eliseOldToolResults(messages, opts) {
1906
2667
  });
1907
2668
  return { messages: changed ? next : messages, saved, changed };
1908
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
+ }
1909
2729
  function buildLosslessDigest(messages) {
1910
2730
  const lines = [];
1911
2731
  for (const m of messages) {
@@ -2016,15 +2836,15 @@ function buildSmartDigest(messages) {
2016
2836
  lines.push(`[${m.role}]: ${display}${marker}`);
2017
2837
  }
2018
2838
  if (noiseCount > 0) {
2019
- 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
+ );
2020
2842
  }
2021
2843
  return lines.join("\n");
2022
2844
  }
2023
2845
  function countToolBlocks(m) {
2024
2846
  if (typeof m.content === "string") return 0;
2025
- return m.content.filter(
2026
- (b) => b.type === "tool_use" || b.type === "tool_result"
2027
- ).length;
2847
+ return m.content.filter((b) => b.type === "tool_use" || b.type === "tool_result").length;
2028
2848
  }
2029
2849
  function firstSentence(text) {
2030
2850
  const trimmed = text.trim();
@@ -2070,10 +2890,12 @@ var HybridCompactor = class {
2070
2890
  if (elide.changed) ctx.state.replaceMessages(elide.messages);
2071
2891
  if (elide.saved > 0) reductions.push({ phase: "elision", saved: elide.saved });
2072
2892
  let collapsedDigest;
2893
+ let evidenceDigest;
2073
2894
  if (opts.aggressive) {
2074
2895
  const phase2 = this.collapseAncientTurns(ctx, preserveK);
2075
2896
  if (phase2.saved > 0) reductions.push({ phase: "summary", saved: phase2.saved });
2076
2897
  collapsedDigest = phase2.digest;
2898
+ evidenceDigest = phase2.evidenceDigest;
2077
2899
  }
2078
2900
  const repaired = repairToolUseAdjacency(ctx.messages);
2079
2901
  if (repaired.report.changed) {
@@ -2081,6 +2903,11 @@ var HybridCompactor = class {
2081
2903
  }
2082
2904
  const afterTokens = estimateMessages(ctx.messages);
2083
2905
  const afterFull = this.estimateFullRequest(ctx);
2906
+ const quality = checkCompactionQuality(ctx, {
2907
+ collapsedDigest,
2908
+ evidenceDigest,
2909
+ reduced: beforeTokens > afterTokens || beforeFull > afterFull
2910
+ });
2084
2911
  return {
2085
2912
  before: beforeTokens,
2086
2913
  after: afterTokens,
@@ -2088,6 +2915,8 @@ var HybridCompactor = class {
2088
2915
  fullRequestTokensAfter: afterFull,
2089
2916
  reductions,
2090
2917
  collapsedDigest,
2918
+ evidenceDigest,
2919
+ quality,
2091
2920
  repaired: repaired.report.changed ? {
2092
2921
  removedToolUses: repaired.report.removedToolUses,
2093
2922
  removedToolResults: repaired.report.removedToolResults,
@@ -2129,7 +2958,13 @@ var HybridCompactor = class {
2129
2958
  if (boundary <= 0) return { saved: 0 };
2130
2959
  const removed = messages.slice(0, boundary);
2131
2960
  const removedTokens = estimateMessages(removed);
2132
- 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;
2133
2968
  const summaryMsg = {
2134
2969
  role: "system",
2135
2970
  content: `[prior_turns_digest: ${digest}]`
@@ -2138,10 +2973,29 @@ var HybridCompactor = class {
2138
2973
  ctx.state.replaceMessages([summaryMsg, ...tail]);
2139
2974
  return {
2140
2975
  saved: Math.max(0, removedTokens - estimateMessages([summaryMsg])),
2141
- digest
2976
+ digest,
2977
+ evidenceDigest: evidenceDigest || void 0
2142
2978
  };
2143
2979
  }
2144
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
+ }
2145
2999
  function readContextWindowPolicy(ctx) {
2146
3000
  const policy = ctx.meta?.["contextWindowPolicy"];
2147
3001
  if (!policy || typeof policy !== "object") return null;
@@ -2168,52 +3022,52 @@ var DefaultPathResolver = class {
2168
3022
  projectRoot;
2169
3023
  cwd;
2170
3024
  constructor(cwd = process.cwd()) {
2171
- this.cwd = path4.resolve(cwd);
3025
+ this.cwd = path6.resolve(cwd);
2172
3026
  this.projectRoot = this.detectProjectRoot(this.cwd);
2173
3027
  }
2174
3028
  detectProjectRoot(start) {
2175
- let dir = path4.resolve(start);
2176
- const root = path4.parse(dir).root;
2177
- const home = path4.resolve(os.homedir());
2178
- 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);
2179
3033
  while (dir !== root) {
2180
3034
  if (dir === home && dir !== startPath) {
2181
3035
  break;
2182
3036
  }
2183
3037
  for (const marker of PROJECT_MARKERS) {
2184
3038
  try {
2185
- fs2.accessSync(path4.join(dir, marker));
3039
+ fs2.accessSync(path6.join(dir, marker));
2186
3040
  return dir;
2187
3041
  } catch {
2188
3042
  }
2189
3043
  }
2190
- const parent = path4.dirname(dir);
3044
+ const parent = path6.dirname(dir);
2191
3045
  if (parent === dir) break;
2192
3046
  dir = parent;
2193
3047
  }
2194
3048
  return startPath;
2195
3049
  }
2196
3050
  resolve(input) {
2197
- const abs = path4.isAbsolute(input) ? input : path4.resolve(this.cwd, input);
3051
+ const abs = path6.isAbsolute(input) ? input : path6.resolve(this.cwd, input);
2198
3052
  let real;
2199
3053
  try {
2200
3054
  real = fs2.realpathSync(abs);
2201
3055
  } catch {
2202
- real = path4.normalize(abs);
3056
+ real = path6.normalize(abs);
2203
3057
  }
2204
3058
  return real;
2205
3059
  }
2206
3060
  isInsideRoot(absPath) {
2207
- const normalized = path4.normalize(absPath);
2208
- const root = path4.normalize(this.projectRoot);
3061
+ const normalized = path6.normalize(absPath);
3062
+ const root = path6.normalize(this.projectRoot);
2209
3063
  if (normalized === root) return true;
2210
- const rel = path4.relative(root, normalized);
2211
- return !rel.startsWith("..") && !path4.isAbsolute(rel);
3064
+ const rel = path6.relative(root, normalized);
3065
+ return !rel.startsWith("..") && !path6.isAbsolute(rel);
2212
3066
  }
2213
3067
  ensureInsideRoot(absPath) {
2214
3068
  const resolved = this.resolve(absPath);
2215
3069
  if (!this.isInsideRoot(resolved)) {
2216
- const display = path4.isAbsolute(absPath) ? path4.basename(absPath) : absPath;
3070
+ const display = path6.isAbsolute(absPath) ? path6.basename(absPath) : absPath;
2217
3071
  const err = new Error(`Path "${display}" resolves outside the project root`);
2218
3072
  err.fullPath = absPath;
2219
3073
  err.projectRoot = this.projectRoot;
@@ -2592,7 +3446,7 @@ var DefaultModelsRegistry = class {
2592
3446
  this.overlay = opts.overlay;
2593
3447
  this.overlayUrl = opts.overlayUrl;
2594
3448
  this.overlayFile = opts.overlayFile;
2595
- 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);
2596
3450
  }
2597
3451
  async load(opts = {}) {
2598
3452
  if (this.payload && !opts.force) return this.payload;
@@ -2805,7 +3659,7 @@ var DefaultModelsRegistry = class {
2805
3659
  }
2806
3660
  /** Used by `wstack models refresh` to expose where the cache lives. */
2807
3661
  cacheLocation() {
2808
- return path4.resolve(this.cacheFile);
3662
+ return path6.resolve(this.cacheFile);
2809
3663
  }
2810
3664
  };
2811
3665
  function hasEntries(payload) {
@@ -3303,7 +4157,7 @@ var InMemoryAgentBridge = class {
3303
4157
  });
3304
4158
  }
3305
4159
  this.inflightGuards.add(correlationId);
3306
- return new Promise((resolve4, reject) => {
4160
+ return new Promise((resolve6, reject) => {
3307
4161
  const timer = setTimeout(() => {
3308
4162
  this.inflightGuards.delete(correlationId);
3309
4163
  this.pendingRequests.delete(correlationId);
@@ -3322,7 +4176,7 @@ var InMemoryAgentBridge = class {
3322
4176
  return;
3323
4177
  }
3324
4178
  this.pendingRequests.set(correlationId, {
3325
- resolve: resolve4,
4179
+ resolve: resolve6,
3326
4180
  reject,
3327
4181
  timer
3328
4182
  });
@@ -3612,7 +4466,8 @@ ${errorDetails}`,
3612
4466
  let effectivePermission = decision.permission;
3613
4467
  const policy = this.opts.permissionPolicy;
3614
4468
  const yolo = policy.getYolo?.() === true || policy.getYoloDestructive?.() === true;
3615
- if (toolDangerousCaps.length > 0 && effectivePermission === "auto" && !yolo) {
4469
+ const authoritativeAuto = decision.source === "yolo";
4470
+ if (toolDangerousCaps.length > 0 && effectivePermission === "auto" && !yolo && !authoritativeAuto) {
3616
4471
  effectivePermission = "confirm";
3617
4472
  }
3618
4473
  if (effectivePermission === "deny") {
@@ -3754,9 +4609,10 @@ ${post.additionalContext}`;
3754
4609
  });
3755
4610
  this.opts.renderer?.writeToolCall(tool.name, use.input);
3756
4611
  const output = await this.runWithTimeout(tool, use.input, ctx.signal, ctx, use.id);
3757
- const text = this.serializer.serialize(output);
4612
+ const text = this.serializer.serialize(output, { toolName: tool.name, input: use.input });
3758
4613
  const scrubbed = this.opts.secretScrubber.scrub(text);
3759
- 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);
3760
4616
  this.opts.renderer?.writeToolResult(tool.name, capped, false);
3761
4617
  return {
3762
4618
  block: {
@@ -3781,38 +4637,27 @@ ${post.additionalContext}`;
3781
4637
  tool.timeoutMs ?? this.iterationTimeoutMs,
3782
4638
  this.maxToolTimeoutMs
3783
4639
  );
3784
- const ctrl = new AbortController();
3785
- const timer = setTimeout(() => ctrl.abort(new Error("tool timeout")), timeoutMs);
3786
- const combined = AbortSignal.any([parentSignal, ctrl.signal]);
3787
- let cleanupCalled = false;
3788
- let caught = false;
4640
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
4641
+ const combined = AbortSignal.any([parentSignal, timeoutSignal]);
4642
+ let output;
3789
4643
  try {
3790
- if (typeof tool.executeStream === "function") {
3791
- return await this.runStreamedTool(tool, input, ctx, combined, toolUseId);
3792
- }
3793
- 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 });
3794
4645
  } catch (err) {
3795
- caught = true;
3796
- if (combined.aborted && typeof tool.cleanup === "function") {
3797
- cleanupCalled = true;
3798
- try {
3799
- await tool.cleanup(input, ctx);
3800
- } catch {
3801
- }
3802
- }
4646
+ if (combined.aborted) await this.runToolCleanup(tool, input, ctx);
3803
4647
  throw err;
3804
- } finally {
3805
- clearTimeout(timer);
3806
- if (combined.aborted && !caught) {
3807
- if (!cleanupCalled && typeof tool.cleanup === "function") {
3808
- try {
3809
- await tool.cleanup(input, ctx);
3810
- } catch {
3811
- }
3812
- }
3813
- const reason = combined.reason instanceof Error ? combined.reason : new Error(typeof combined.reason === "string" ? combined.reason : "aborted");
3814
- throw reason;
3815
- }
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 {
3816
4661
  }
3817
4662
  }
3818
4663
  async runStreamedTool(tool, input, ctx, signal, toolUseId) {
@@ -3981,6 +4826,25 @@ function extractMalformedRaw(input) {
3981
4826
  return String(value);
3982
4827
  }
3983
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
+ }
3984
4848
 
3985
4849
  // src/core/conversation-state.ts
3986
4850
  var ConversationState = class {
@@ -4099,6 +4963,7 @@ var Context = class {
4099
4963
  todos = [];
4100
4964
  readFiles = /* @__PURE__ */ new Set();
4101
4965
  fileMtimes = /* @__PURE__ */ new Map();
4966
+ contextEvidence = createContextEvidenceState();
4102
4967
  systemPrompt;
4103
4968
  provider;
4104
4969
  session;
@@ -4230,11 +5095,11 @@ var Context = class {
4230
5095
  * Returns the resolved absolute path.
4231
5096
  */
4232
5097
  setWorkingDir(dir) {
4233
- const resolved = path4.isAbsolute(dir) ? path4.resolve(dir) : path4.resolve(this.projectRoot, dir);
5098
+ const resolved = path6.isAbsolute(dir) ? path6.resolve(dir) : path6.resolve(this.projectRoot, dir);
4234
5099
  if (!this.allowOutsideProjectRoot) {
4235
- const root = path4.resolve(this.projectRoot);
4236
- const rel = path4.relative(root, resolved);
4237
- if (rel.startsWith("..") || path4.isAbsolute(rel)) {
5100
+ const root = path6.resolve(this.projectRoot);
5101
+ const rel = path6.relative(root, resolved);
5102
+ if (rel.startsWith("..") || path6.isAbsolute(rel)) {
4238
5103
  throw new Error(
4239
5104
  `Working directory "${resolved}" is outside project root "${root}"`
4240
5105
  );