@wrongstack/core 0.265.1 → 0.268.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 (75) hide show
  1. package/dist/{agent-bridge-DrkBxszZ.d.ts → agent-bridge-UhojbpWx.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-DM2pP-B6.d.ts → agent-subagent-runner-Bvtf1o9K.d.ts} +25 -7
  3. package/dist/{brain-BXd_61kQ.d.ts → brain-69wzMKp1.d.ts} +73 -1
  4. package/dist/{compactor-B8pOf45Y.d.ts → compactor-CBQAJoDc.d.ts} +19 -1
  5. package/dist/{config-BMCj_XDs.d.ts → config-VKfOZ-6X.d.ts} +122 -3
  6. package/dist/{context-MRk5PhNv.d.ts → context-C0U8B9NF.d.ts} +88 -1
  7. package/dist/coordination/index.d.ts +57 -161
  8. package/dist/coordination/index.js +471 -177
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/defaults/index.d.ts +26 -25
  11. package/dist/defaults/index.js +1818 -844
  12. package/dist/defaults/index.js.map +1 -1
  13. package/dist/execution/index.d.ts +72 -16
  14. package/dist/execution/index.js +1270 -265
  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 +7 -6
  18. package/dist/global-mailbox-KByEFFBa.d.ts +663 -0
  19. package/dist/{goal-preamble-DvHDSKSe.d.ts → goal-preamble-CrYjmdw4.d.ts} +28 -11
  20. package/dist/{goal-store-DtLMySNb.d.ts → goal-store-Y_zdLZ3q.d.ts} +1 -1
  21. package/dist/hq/index.d.ts +195 -0
  22. package/dist/hq/index.js +1884 -0
  23. package/dist/hq/index.js.map +1 -0
  24. package/dist/index-BfaS-f_m.d.ts +82 -0
  25. package/dist/{index-B-ch8K9C.d.ts → index-CtQnmkaS.d.ts} +8 -8
  26. package/dist/{index-CEDeNodM.d.ts → index-gCv830d7.d.ts} +5 -5
  27. package/dist/index.d.ts +124 -47
  28. package/dist/index.js +5600 -2662
  29. package/dist/index.js.map +1 -1
  30. package/dist/infrastructure/index.d.ts +6 -6
  31. package/dist/infrastructure/index.js +117 -19
  32. package/dist/infrastructure/index.js.map +1 -1
  33. package/dist/kernel/index.d.ts +10 -9
  34. package/dist/kernel/index.js.map +1 -1
  35. package/dist/{pipeline-DPDxH_7m.d.ts → mailbox-types-Ct2hJq0P.d.ts} +1 -244
  36. package/dist/{mcp-servers-2x4w6Jn9.d.ts → mcp-servers-HT3Fi7Bl.d.ts} +10 -4
  37. package/dist/models/index.d.ts +5 -5
  38. package/dist/models/index.js +33 -3
  39. package/dist/models/index.js.map +1 -1
  40. package/dist/{models-registry-DmJlKuNp.d.ts → models-registry-Bvcl3Vaa.d.ts} +1 -1
  41. package/dist/{multi-agent-coordinator-DyCkCZnU.d.ts → multi-agent-coordinator-BACjsmkC.d.ts} +1 -1
  42. package/dist/{null-fleet-bus-CG9QY2aP.d.ts → null-fleet-bus-DA7fvhUg.d.ts} +14 -9
  43. package/dist/observability/index.d.ts +2 -2
  44. package/dist/{parallel-eternal-engine-Jw9uhEoT.d.ts → parallel-eternal-engine-Ci71gYu_.d.ts} +11 -15
  45. package/dist/{path-resolver-Dy2ej-gE.d.ts → path-resolver-O1IJnmKE.d.ts} +4 -3
  46. package/dist/{permission-B9SB45lp.d.ts → permission-Bd-57Lbl.d.ts} +1 -1
  47. package/dist/{permission-policy-CkjSXabK.d.ts → permission-policy-uNXC6Kge.d.ts} +2 -3
  48. package/dist/pipeline-BDNvENyV.d.ts +245 -0
  49. package/dist/{plan-templates-CzD9GnAU.d.ts → plan-templates-EMsalEtN.d.ts} +5 -5
  50. package/dist/{llm-selector-C0tfTCUe.d.ts → provider-model-resolve-CEb9x886.d.ts} +40 -3
  51. package/dist/{provider-runner-DMa70ODu.d.ts → provider-runner-DWJbpo70.d.ts} +3 -3
  52. package/dist/{retry-policy-CN0khdlj.d.ts → retry-policy-C3s_lvdK.d.ts} +1 -1
  53. package/dist/sdd/index.d.ts +9 -8
  54. package/dist/sdd/index.js +44 -14
  55. package/dist/sdd/index.js.map +1 -1
  56. package/dist/{secret-vault-B2yw84VT.d.ts → secret-vault-Cgduf5xL.d.ts} +2 -2
  57. package/dist/security/index.d.ts +5 -67
  58. package/dist/security/index.js +129 -99
  59. package/dist/security/index.js.map +1 -1
  60. package/dist/{selector-CzHh_igB.d.ts → selector-47LBnBVk.d.ts} +1 -1
  61. package/dist/{session-event-bridge-BUI6Jf-4.d.ts → session-event-bridge-Cw7oqmW2.d.ts} +1 -1
  62. package/dist/{session-reader-CMgdMSRP.d.ts → session-reader-DD4v2Obw.d.ts} +1 -1
  63. package/dist/storage/index.d.ts +14 -12
  64. package/dist/storage/index.js +144 -120
  65. package/dist/storage/index.js.map +1 -1
  66. package/dist/tools/index.d.ts +4 -2
  67. package/dist/tools/index.js +166 -31
  68. package/dist/tools/index.js.map +1 -1
  69. package/dist/types/index.d.ts +20 -19
  70. package/dist/types/index.js +1358 -476
  71. package/dist/types/index.js.map +1 -1
  72. package/dist/utils/index.d.ts +472 -405
  73. package/dist/utils/index.js +2321 -1193
  74. package/dist/utils/index.js.map +1 -1
  75. package/package.json +5 -1
@@ -1,7 +1,7 @@
1
1
  import * as crypto2 from 'crypto';
2
2
  import { randomBytes, createCipheriv, createDecipheriv, randomUUID, createHash } from 'crypto';
3
3
  import * as fsp2 from 'fs/promises';
4
- import * as path3 from 'path';
4
+ import * as path4 from 'path';
5
5
  import { isAbsolute, resolve } from 'path';
6
6
  import * as fs4 from 'fs';
7
7
  import * as os from 'os';
@@ -33,9 +33,9 @@ __export(atomic_write_exports, {
33
33
  withFileLock: () => withFileLock
34
34
  });
35
35
  async function atomicWrite(targetPath, content, opts = {}) {
36
- const dir = path3.dirname(targetPath);
36
+ const dir = path4.dirname(targetPath);
37
37
  await fsp2.mkdir(dir, { recursive: true });
38
- const tmp = path3.join(dir, `.${path3.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
38
+ const tmp = path4.join(dir, `.${path4.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
39
39
  try {
40
40
  if (typeof content === "string") {
41
41
  await fsp2.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
@@ -74,9 +74,9 @@ async function ensureDir(dir) {
74
74
  await fsp2.mkdir(dir, { recursive: true });
75
75
  }
76
76
  async function withFileLock(targetPath, fn, opts = {}) {
77
- const dir = path3.dirname(targetPath);
77
+ const dir = path4.dirname(targetPath);
78
78
  await fsp2.mkdir(dir, { recursive: true });
79
- const lockPath = path3.join(dir, `.${path3.basename(targetPath)}.lock`);
79
+ const lockPath = path4.join(dir, `.${path4.basename(targetPath)}.lock`);
80
80
  const timeoutMs = opts.timeoutMs ?? 5e3;
81
81
  const staleMs = opts.staleMs ?? 3e4;
82
82
  const started = Date.now();
@@ -105,7 +105,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
105
105
  if (Date.now() - started >= timeoutMs) {
106
106
  throw new Error(`Timed out waiting for file lock: ${targetPath}`);
107
107
  }
108
- await new Promise((resolve5) => setTimeout(resolve5, 25));
108
+ await new Promise((resolve6) => setTimeout(resolve6, 25));
109
109
  }
110
110
  }
111
111
  try {
@@ -138,7 +138,7 @@ async function renameWithRetry(from, to) {
138
138
  if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
139
139
  throw err;
140
140
  }
141
- await new Promise((resolve5) => setTimeout(resolve5, delays[i]));
141
+ await new Promise((resolve6) => setTimeout(resolve6, delays[i]));
142
142
  }
143
143
  }
144
144
  throw lastErr;
@@ -234,7 +234,7 @@ var DefaultLogger = class _DefaultLogger {
234
234
  this.maxFileBytes = opts.maxFileBytes ?? 10 * 1024 * 1024;
235
235
  if (this.file) {
236
236
  try {
237
- fs4.mkdirSync(path3.dirname(this.file), { recursive: true });
237
+ fs4.mkdirSync(path4.dirname(this.file), { recursive: true });
238
238
  } catch {
239
239
  }
240
240
  }
@@ -436,32 +436,268 @@ function isEmptyMessage(msg) {
436
436
  return msg.content.length === 0;
437
437
  }
438
438
 
439
+ // src/utils/assert-never.ts
440
+ function assertNever(x, message) {
441
+ const err = new Error(
442
+ `Unhandled case: ${JSON.stringify(x)}`
443
+ );
444
+ err.name = "AssertNeverError";
445
+ throw err;
446
+ }
447
+
439
448
  // src/utils/index.ts
440
449
  init_atomic_write();
450
+ var MAX_DIGEST_CHARS = 4e3;
451
+ function createContextEvidenceState() {
452
+ return {
453
+ sessionGoals: [],
454
+ implicitFacts: [],
455
+ activeErrors: [],
456
+ toolCalls: [],
457
+ fileGraph: {},
458
+ repeatedReads: [],
459
+ updatedAt: Date.now()
460
+ };
461
+ }
462
+ function buildContextEvidenceDigest(ctx) {
463
+ const state = ensureEvidence(ctx);
464
+ const lines = [];
465
+ if (state.currentIntent?.text) {
466
+ lines.push(`intent: ${state.currentIntent.text}`);
467
+ }
468
+ const goals = state.sessionGoals.slice(-3);
469
+ if (goals.length > 0) {
470
+ lines.push("session_goals:");
471
+ for (const goal of goals) lines.push(`- ${goal}`);
472
+ }
473
+ const activeErrors = state.activeErrors.slice(-5);
474
+ if (activeErrors.length > 0) {
475
+ lines.push("active_errors:");
476
+ for (const err of activeErrors) lines.push(`- ${err}`);
477
+ }
478
+ 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);
479
+ if (files.length > 0) {
480
+ lines.push("dependency_graph:");
481
+ for (const file of files) {
482
+ const actions = [
483
+ file.reads > 0 ? `read ${file.reads}x` : "",
484
+ file.writes > 0 ? `write ${file.writes}x` : ""
485
+ ].filter(Boolean).join(", ");
486
+ const refs = file.referenced ? "; referenced by assistant" : "";
487
+ const via = file.lastToolUseId ? `; last via ${file.lastToolUseId}` : "";
488
+ lines.push(`- ${file.path} (${actions || "seen"}${refs}${via})`);
489
+ }
490
+ }
491
+ const referenced = state.toolCalls.filter((tool) => tool.status === "referenced").slice(-10);
492
+ const recentSeen = state.toolCalls.filter((tool) => tool.status === "seen").slice(-5);
493
+ const trail = [...referenced, ...recentSeen];
494
+ if (trail.length > 0) {
495
+ lines.push("tool_trail:");
496
+ for (const tool of trail) {
497
+ const size = tool.outputTokens ? `; ~${tool.outputTokens} tokens` : "";
498
+ const filesText = tool.files.length > 0 ? `; files=${tool.files.slice(0, 4).join(", ")}` : "";
499
+ const symbolsText = tool.symbols.length > 0 ? `; symbols=${tool.symbols.slice(0, 4).join(", ")}` : "";
500
+ lines.push(
501
+ `- ${tool.toolUseId} ${tool.toolName} ${tool.status}: ${tool.summary}${filesText}${symbolsText}${size}`
502
+ );
503
+ }
504
+ }
505
+ const facts = state.implicitFacts.slice(-8);
506
+ if (facts.length > 0) {
507
+ lines.push("implicit_facts:");
508
+ for (const fact of facts) lines.push(`- ${fact}`);
509
+ }
510
+ const digest = lines.join("\n");
511
+ if (digest.length <= MAX_DIGEST_CHARS) return digest;
512
+ return `${digest.slice(0, MAX_DIGEST_CHARS)}... [+${digest.length - MAX_DIGEST_CHARS} chars]`;
513
+ }
514
+ function repeatedReadPressure(ctx) {
515
+ return ensureEvidence(ctx).repeatedReads.reduce((max, item) => Math.max(max, item.count), 0);
516
+ }
517
+ function ensureEvidence(ctx) {
518
+ if (!ctx.contextEvidence) {
519
+ ctx.contextEvidence = createContextEvidenceState();
520
+ }
521
+ return ctx.contextEvidence;
522
+ }
523
+
524
+ // src/utils/deep-merge.ts
525
+ var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
526
+ "__proto__",
527
+ "constructor",
528
+ "prototype",
529
+ "__defineGetter__",
530
+ "__defineSetter__",
531
+ "__lookupGetter__",
532
+ "__lookupSetter__"
533
+ ]);
534
+ function isPrimitiveArray(a) {
535
+ return a.every((v) => v === null || typeof v !== "object" && typeof v !== "function");
536
+ }
537
+ function deepMerge(base, patch, options = {}) {
538
+ const {
539
+ conflictResolution = "prefer-patch",
540
+ arrayMode = "replace",
541
+ protectProto = true,
542
+ onNonPrimitiveArrayReplace
543
+ } = options;
544
+ if (typeof base !== "object" || base === null) {
545
+ return conflictResolution === "prefer-patch" ? patch : base;
546
+ }
547
+ if (typeof patch !== "object" || patch === null) {
548
+ return conflictResolution === "prefer-patch" ? patch : base;
549
+ }
550
+ if (Array.isArray(base) && Array.isArray(patch)) {
551
+ if (arrayMode === "concat-primitives" && isPrimitiveArray(base) && isPrimitiveArray(patch)) {
552
+ return [.../* @__PURE__ */ new Set([...base, ...patch])];
553
+ }
554
+ return conflictResolution === "prefer-patch" ? patch : base;
555
+ }
556
+ if (Array.isArray(base) || Array.isArray(patch)) {
557
+ return conflictResolution === "prefer-patch" ? patch : base;
558
+ }
559
+ const baseObj = base;
560
+ const patchObj = patch;
561
+ const out = { ...baseObj };
562
+ for (const [k, v] of Object.entries(patchObj)) {
563
+ if (protectProto && FORBIDDEN_PROTO_KEYS.has(k)) continue;
564
+ const existing = out[k];
565
+ if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
566
+ out[k] = deepMerge(existing, v, options);
567
+ } else if (Array.isArray(v) && Array.isArray(existing)) {
568
+ if (onNonPrimitiveArrayReplace && !isPrimitiveArray(v)) {
569
+ onNonPrimitiveArrayReplace(k, existing.length, v.length);
570
+ }
571
+ out[k] = deepMerge(existing, v, options);
572
+ } else if (v !== void 0) {
573
+ if (onNonPrimitiveArrayReplace && Array.isArray(v) && !isPrimitiveArray(v)) {
574
+ const existingLen = Array.isArray(existing) ? existing.length : 0;
575
+ onNonPrimitiveArrayReplace(k, existingLen, v.length);
576
+ }
577
+ out[k] = v;
578
+ }
579
+ }
580
+ return out;
581
+ }
441
582
 
442
583
  // src/utils/error.ts
443
584
  function toErrorMessage(err) {
444
585
  return err instanceof Error ? err.message : String(err);
445
586
  }
446
-
447
- // src/utils/safe-json.ts
448
- function safeParse(input, maxBytes = 5e6) {
449
- if (input.length > maxBytes) {
450
- return { ok: false, error: `Input exceeds limit (${maxBytes} bytes)` };
587
+ var GLOB_CHARS = /* @__PURE__ */ new Set(["*", "?", "["]);
588
+ var IS_WINDOWS = process.platform === "win32";
589
+ var SEP = IS_WINDOWS ? "\\" : "/";
590
+ function isGlob(p) {
591
+ for (const c of p) {
592
+ if (GLOB_CHARS.has(c)) return true;
451
593
  }
452
- try {
453
- return { ok: true, value: JSON.parse(input) };
454
- } catch (err) {
455
- return {
456
- ok: false,
457
- error: toErrorMessage(err)
458
- };
594
+ return false;
595
+ }
596
+ function globToRegex(pat) {
597
+ let i = 0;
598
+ let re = "^";
599
+ while (i < pat.length) {
600
+ const c = expectDefined(pat[i]);
601
+ if (c === "*") {
602
+ if (pat[i + 1] === "*") {
603
+ re += ".*";
604
+ i += 2;
605
+ if (pat[i] === "/") i++;
606
+ } else {
607
+ re += "[^/\\\\]*";
608
+ i++;
609
+ }
610
+ } else if (c === "?") {
611
+ re += "[^/\\\\]";
612
+ i++;
613
+ } else if (c === "[") {
614
+ let cls = "[";
615
+ i++;
616
+ if (pat[i] === "!" || pat[i] === "^") {
617
+ cls += "^";
618
+ i++;
619
+ }
620
+ while (i < pat.length && pat[i] !== "]") {
621
+ const ch = pat[i] ?? "";
622
+ if (ch === "\\") cls += "\\\\";
623
+ else if (ch === "]" || ch === "^") cls += `\\${ch}`;
624
+ else cls += ch;
625
+ i++;
626
+ }
627
+ cls += "]";
628
+ re += cls;
629
+ i++;
630
+ } else {
631
+ re += c.replace(/[.+^${}()|\\]/g, "\\$&");
632
+ i++;
633
+ }
459
634
  }
635
+ return new RegExp(re + "$");
460
636
  }
461
-
462
- // src/utils/string.ts
463
- function truncate(s, max) {
464
- return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
637
+ function baseDir(pat) {
638
+ let i = pat.length - 1;
639
+ while (i >= 0 && !GLOB_CHARS.has(expectDefined(pat[i])) && pat[i] !== SEP && pat[i] !== "/") i--;
640
+ const cut = i >= 0 ? pat.lastIndexOf(SEP, i) : pat.lastIndexOf("/", i);
641
+ return cut < 0 ? "." : pat.slice(0, cut);
642
+ }
643
+ async function expandGlob(pattern) {
644
+ if (!isGlob(pattern)) return [pattern];
645
+ const results = /* @__PURE__ */ new Set();
646
+ const abs = isAbsolute(pattern);
647
+ const base = abs ? baseDir(pattern) : baseDir(pattern);
648
+ const relPat = base === "." ? pattern : pattern.slice(base.length + 1);
649
+ async function walk3(dir, pat) {
650
+ let entries;
651
+ try {
652
+ entries = await fsp2.readdir(dir);
653
+ } catch {
654
+ return;
655
+ }
656
+ const firstGlob = pat.search(/[*?[[]/);
657
+ if (firstGlob < 0) {
658
+ const re = globToRegex(pat);
659
+ for (const e of entries) {
660
+ if (re.test(e)) {
661
+ const full = `${dir}${SEP}${e}`;
662
+ results.add(abs ? resolve(full) : full);
663
+ }
664
+ }
665
+ return;
666
+ }
667
+ const before = pat.slice(0, firstGlob);
668
+ const rest = pat.slice(firstGlob);
669
+ if (before.endsWith("**")) {
670
+ await walk3(dir, rest);
671
+ for (const e of entries) {
672
+ const full = `${dir}${SEP}${e}`;
673
+ try {
674
+ const stat6 = await fsp2.stat(full);
675
+ if (stat6.isDirectory()) await walk3(full, rest);
676
+ } catch {
677
+ }
678
+ }
679
+ } else if (before === "") {
680
+ const re = globToRegex(rest);
681
+ for (const e of entries) {
682
+ if (re.test(e)) {
683
+ const full = `${dir}${SEP}${e}`;
684
+ results.add(abs ? resolve(full) : full);
685
+ }
686
+ }
687
+ } else {
688
+ const seg = before.replace(/[*?[\]]/g, "").replace(/\/$/, "");
689
+ if (entries.includes(seg)) {
690
+ const full = `${dir}${SEP}${seg}`;
691
+ try {
692
+ const stat6 = await fsp2.stat(full);
693
+ if (stat6.isDirectory()) await walk3(full, rest);
694
+ } catch {
695
+ }
696
+ }
697
+ }
698
+ }
699
+ await walk3(base === "." ? "." : base, relPat);
700
+ return [...results];
465
701
  }
466
702
 
467
703
  // src/utils/glob-match.ts
@@ -539,232 +775,475 @@ function matchGlob(pattern, input) {
539
775
  function matchAny(patterns, input) {
540
776
  return patterns.some((p) => matchGlob(p, input));
541
777
  }
542
- function projectHash(absRoot) {
543
- return createHash("sha256").update(path3.resolve(absRoot)).digest("hex").slice(0, 12);
544
- }
545
- function projectSlug(absRoot) {
546
- const base = slugify(path3.basename(absRoot));
547
- const hash = createHash("sha256").update(path3.resolve(absRoot)).digest("hex").slice(0, 6);
548
- return `${base}-${hash}`;
549
- }
550
- function slugify(name) {
551
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
552
- }
553
- function wstackGlobalRoot() {
554
- const fromEnv = process.env["WRONGSTACK_HOME"];
555
- if (fromEnv && fromEnv.trim().length > 0) return path3.resolve(fromEnv);
556
- return path3.join(os.homedir(), ".wrongstack");
557
- }
558
- function resolveWstackPaths(opts) {
559
- const globalRoot = opts.globalRoot ?? (opts.userHome ? path3.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
560
- const hash = projectHash(opts.projectRoot);
561
- const slug = projectSlug(opts.projectRoot);
562
- const projectDir = path3.join(globalRoot, "projects", slug);
563
- return {
564
- globalRoot,
565
- configDir: globalRoot,
566
- globalConfig: path3.join(globalRoot, "config.json"),
567
- secretsKey: path3.join(globalRoot, ".key"),
568
- globalMemory: path3.join(globalRoot, "memory.md"),
569
- globalSkills: path3.join(globalRoot, "skills"),
570
- globalPrompts: path3.join(globalRoot, "prompts"),
571
- cacheDir: path3.join(globalRoot, "cache"),
572
- modelsCache: path3.join(globalRoot, "cache", "models.dev.json"),
573
- modelsOverlayCache: path3.join(globalRoot, "cache", "models-overlay.json"),
574
- historyFile: path3.join(globalRoot, "history"),
575
- logFile: path3.join(globalRoot, "logs", "wrongstack.log"),
576
- projectDir,
577
- projectCodebaseIndex: path3.join(projectDir, "codebase-index"),
578
- projectMemory: path3.join(projectDir, "memory.md"),
579
- projectSessions: path3.join(projectDir, "sessions"),
580
- projectTrust: path3.join(projectDir, "trust.json"),
581
- projectMeta: path3.join(projectDir, "meta.json"),
582
- projectLocalConfig: path3.join(projectDir, "config.local.json"),
583
- inProjectConfig: path3.join(opts.projectRoot, ".wrongstack", "config.json"),
584
- inProjectAgentsFile: path3.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
585
- inProjectSkills: path3.join(opts.projectRoot, ".wrongstack", "skills"),
586
- inProjectWorktrees: path3.join(opts.projectRoot, ".wrongstack", "worktrees"),
587
- projectHash: hash,
588
- projectSlug: slug,
589
- projectGoal: path3.join(projectDir, "goal.json"),
590
- projectSpecs: path3.join(projectDir, "specs"),
591
- projectTaskGraphs: path3.join(projectDir, "task-graphs"),
592
- projectSddSession: path3.join(projectDir, "sdd-session.json"),
593
- projectPlan: path3.join(projectDir, "plan.json"),
594
- projectAutophase: path3.join(projectDir, "autophase"),
595
- syncConfig: path3.join(globalRoot, "sync.json"),
596
- projectStatus: (projectHash2) => path3.join(globalRoot, "projects", projectHash2, "status.json")
597
- };
598
- }
599
-
600
- // src/utils/sleep.ts
601
- function sleep(ms) {
602
- return new Promise((resolve5) => setTimeout(resolve5, ms));
603
- }
604
778
 
605
- // src/utils/assert-never.ts
606
- function assertNever(x, message) {
607
- const err = new Error(
608
- `Unhandled case: ${JSON.stringify(x)}`
609
- );
610
- err.name = "AssertNeverError";
611
- throw err;
779
+ // src/utils/json-repair.ts
780
+ function completePartialObject(s) {
781
+ if (!s.trim().startsWith("{")) return s;
782
+ if (tryParse(s).ok) return s;
783
+ return repairTruncated(s);
612
784
  }
613
-
614
- // src/utils/deep-merge.ts
615
- var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
616
- "__proto__",
617
- "constructor",
618
- "prototype",
619
- "__defineGetter__",
620
- "__defineSetter__",
621
- "__lookupGetter__",
622
- "__lookupSetter__"
623
- ]);
624
- function isPrimitiveArray(a) {
625
- return a.every((v) => v === null || typeof v !== "object" && typeof v !== "function");
626
- }
627
- function deepMerge(base, patch, options = {}) {
628
- const {
629
- conflictResolution = "prefer-patch",
630
- arrayMode = "replace",
631
- protectProto = true,
632
- onNonPrimitiveArrayReplace
633
- } = options;
634
- if (typeof base !== "object" || base === null) {
635
- return conflictResolution === "prefer-patch" ? patch : base;
636
- }
637
- if (typeof patch !== "object" || patch === null) {
638
- return conflictResolution === "prefer-patch" ? patch : base;
785
+ function repairTruncated(s) {
786
+ const stack = [];
787
+ let inString = false;
788
+ let escaped = false;
789
+ let sawKey = false;
790
+ let prevSig = "";
791
+ let contentEnd = 0;
792
+ let stringBraceDepth = 0;
793
+ for (let i = 0; i < s.length; i++) {
794
+ const ch = expectDefined(s[i]);
795
+ if (inString) {
796
+ contentEnd = i + 1;
797
+ if (escaped) {
798
+ escaped = false;
799
+ continue;
800
+ }
801
+ if (ch === "\\") {
802
+ escaped = true;
803
+ continue;
804
+ }
805
+ if (ch === '"') {
806
+ inString = false;
807
+ prevSig = '"';
808
+ stringBraceDepth = 0;
809
+ continue;
810
+ }
811
+ if (ch === "{") stringBraceDepth++;
812
+ else if (ch === "}" && stringBraceDepth > 0) stringBraceDepth--;
813
+ continue;
814
+ }
815
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") continue;
816
+ contentEnd = i + 1;
817
+ if (ch === '"') {
818
+ inString = true;
819
+ sawKey = true;
820
+ stringBraceDepth = 0;
821
+ prevSig = '"';
822
+ } else if (ch === "{" || ch === "[") {
823
+ stack.push(ch);
824
+ prevSig = ch;
825
+ } else if (ch === "}" || ch === "]") {
826
+ stack.pop();
827
+ prevSig = ch;
828
+ } else {
829
+ prevSig = ch;
830
+ }
639
831
  }
640
- if (Array.isArray(base) && Array.isArray(patch)) {
641
- if (arrayMode === "concat-primitives" && isPrimitiveArray(base) && isPrimitiveArray(patch)) {
642
- return [.../* @__PURE__ */ new Set([...base, ...patch])];
832
+ if (!sawKey && !inString) return s;
833
+ let result = s.slice(0, contentEnd);
834
+ if (inString) {
835
+ if (escaped) {
836
+ result = result.slice(0, -1);
837
+ } else if (endsWithInvalidEscape(result)) {
838
+ result = result.slice(0, -2);
643
839
  }
644
- return conflictResolution === "prefer-patch" ? patch : base;
840
+ if (stringBraceDepth > 0) result += "}".repeat(stringBraceDepth);
841
+ result += '"';
842
+ } else if (prevSig === ":") {
843
+ result += "null";
645
844
  }
646
- if (Array.isArray(base) || Array.isArray(patch)) {
647
- return conflictResolution === "prefer-patch" ? patch : base;
845
+ for (let k = stack.length - 1; k >= 0; k--) {
846
+ result += stack[k] === "{" ? "}" : "]";
648
847
  }
649
- const baseObj = base;
650
- const patchObj = patch;
651
- const out = { ...baseObj };
652
- for (const [k, v] of Object.entries(patchObj)) {
653
- if (protectProto && FORBIDDEN_PROTO_KEYS.has(k)) continue;
654
- const existing = out[k];
655
- if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
656
- out[k] = deepMerge(existing, v, options);
657
- } else if (Array.isArray(v) && Array.isArray(existing)) {
658
- if (onNonPrimitiveArrayReplace && !isPrimitiveArray(v)) {
659
- onNonPrimitiveArrayReplace(k, existing.length, v.length);
660
- }
661
- out[k] = deepMerge(existing, v, options);
662
- } else if (v !== void 0) {
663
- if (onNonPrimitiveArrayReplace && Array.isArray(v) && !isPrimitiveArray(v)) {
664
- const existingLen = Array.isArray(existing) ? existing.length : 0;
665
- onNonPrimitiveArrayReplace(k, existingLen, v.length);
666
- }
667
- out[k] = v;
668
- }
848
+ if (!tryParse(result).ok) {
849
+ const patched = result.replace(/:(\s*)([}\]])/g, ":null$2");
850
+ if (tryParse(patched).ok) result = patched;
851
+ }
852
+ return result;
853
+ }
854
+ var VALID_ESCAPE = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
855
+ function endsWithInvalidEscape(str) {
856
+ const last = str[str.length - 1];
857
+ if (str[str.length - 2] !== "\\" || last === void 0) return false;
858
+ if (VALID_ESCAPE.has(last)) return false;
859
+ let backslashes = 0;
860
+ for (let k = str.length - 2; k >= 0 && str[k] === "\\"; k--) backslashes++;
861
+ return backslashes % 2 === 1;
862
+ }
863
+ function tryParse(s) {
864
+ try {
865
+ return { ok: true, value: JSON.parse(s) };
866
+ } catch {
867
+ return { ok: false };
669
868
  }
670
- return out;
671
869
  }
672
870
 
673
- // src/utils/tool-output-serializer.ts
674
- function createToolOutputSerializer(opts = {}) {
675
- const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
676
- function serialize(value) {
677
- if (typeof value === "string") return value;
678
- if (value === null || value === void 0) return "";
679
- if (typeof value === "object") {
680
- if (Array.isArray(value)) return value.map(serialize).join("\n");
681
- if ("text" in value) {
682
- const t = value.text;
683
- return typeof t === "string" ? t : JSON.stringify(value, null, 2);
684
- }
685
- try {
686
- return JSON.stringify(value, null, 2);
687
- } catch {
688
- return String(value);
689
- }
871
+ // src/utils/json-schema-validate.ts
872
+ function validateAgainstSchema(value, schema) {
873
+ const errors = [];
874
+ walk(value, schema, "", errors);
875
+ return { ok: errors.length === 0, errors };
876
+ }
877
+ function walk(value, schema, path21, errors) {
878
+ if (schema.enum !== void 0) {
879
+ if (!schema.enum.some((e) => deepEqual(e, value))) {
880
+ errors.push({
881
+ path: path21 || "<root>",
882
+ message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
883
+ });
884
+ return;
690
885
  }
691
- return String(value);
692
886
  }
693
- function enforceCap(text, remainingBudget) {
694
- if (remainingBudget <= 0) {
695
- return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
887
+ if (typeof schema.type === "string") {
888
+ if (!checkType(value, schema.type)) {
889
+ errors.push({
890
+ path: path21 || "<root>",
891
+ message: `expected ${schema.type}, got ${describeType(value)}`
892
+ });
893
+ return;
696
894
  }
697
- const textBytes = Buffer.byteLength(text, "utf8");
698
- if (textBytes <= remainingBudget) {
699
- return { text, newBudget: remainingBudget - textBytes };
895
+ }
896
+ if (schema.type === "object" && isPlainObject(value)) {
897
+ const obj = value;
898
+ for (const req of schema.required ?? []) {
899
+ if (!(req in obj)) {
900
+ errors.push({ path: joinPath(path21, req), message: "required property missing" });
901
+ }
700
902
  }
701
- const marker = `
702
- \u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
703
- `;
704
- const markerBytes = Buffer.byteLength(marker, "utf8");
705
- const available = remainingBudget - markerBytes;
706
- if (available <= 0) {
707
- return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
903
+ if (schema.properties) {
904
+ for (const [key, subSchema] of Object.entries(schema.properties)) {
905
+ if (key in obj) {
906
+ walk(obj[key], subSchema, joinPath(path21, key), errors);
907
+ }
908
+ }
708
909
  }
709
- const half = Math.floor(available / 2);
710
- const first = text.slice(0, half);
711
- const second = text.slice(text.length - half);
712
- return { text: `${first}${marker}${second}`, newBudget: 0 };
713
- }
714
- return { serialize, enforceCap, capBytes };
715
- }
716
-
717
- // src/utils/token-estimate.ts
718
- var RoughTokenEstimate = (text, charsPerToken = 3.5) => Math.max(1, Math.ceil(text.length / charsPerToken));
719
- var CALIBRATION_GLOBAL_KEY = "__global__";
720
- var _cals = /* @__PURE__ */ new Map();
721
- function calState(key) {
722
- let state = _cals.get(key);
723
- if (!state) {
724
- state = { ratio: 1, count: 0, prevEst: 0 };
725
- _cals.set(key, state);
726
910
  }
727
- return state;
728
- }
729
- var MIN_SAMPLES_FOR_CALIBRATION = 3;
730
- var ESTIMATE_CACHE = /* @__PURE__ */ new Map();
731
- var ESTIMATE_CACHE_MAX_SIZE = 1e4;
732
- function getCachedEstimate(key, compute) {
733
- const existing = ESTIMATE_CACHE.get(key);
734
- if (existing !== void 0) return existing;
735
- if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
736
- for (const k of ESTIMATE_CACHE.keys()) {
737
- if (ESTIMATE_CACHE.size <= Math.floor(ESTIMATE_CACHE_MAX_SIZE / 2)) break;
738
- ESTIMATE_CACHE.delete(k);
911
+ if (schema.type === "array" && Array.isArray(value) && schema.items) {
912
+ for (let i = 0; i < value.length; i++) {
913
+ walk(value[i], schema.items, `${path21}[${i}]`, errors);
739
914
  }
740
915
  }
741
- const estimate = compute(key);
742
- ESTIMATE_CACHE.set(key, estimate);
743
- return estimate;
744
916
  }
745
- function estimateToolInputTokens(input) {
746
- if (typeof input === "string") return RoughTokenEstimate(input);
747
- if (input === null || typeof input !== "object") {
748
- return RoughTokenEstimate(String(input));
917
+ function checkType(value, type) {
918
+ switch (type) {
919
+ case "string":
920
+ return typeof value === "string";
921
+ case "number":
922
+ return typeof value === "number" && !Number.isNaN(value);
923
+ case "integer":
924
+ return typeof value === "number" && Number.isInteger(value);
925
+ case "boolean":
926
+ return typeof value === "boolean";
927
+ case "null":
928
+ return value === null;
929
+ case "array":
930
+ return Array.isArray(value);
931
+ case "object":
932
+ return isPlainObject(value);
933
+ default:
934
+ return true;
749
935
  }
750
- return getCachedEstimate(JSON.stringify(input), (key) => RoughTokenEstimate(key));
751
936
  }
752
- function estimateToolResultTokens(content) {
753
- if (typeof content === "string") return RoughTokenEstimate(content);
754
- return getCachedEstimate(JSON.stringify(content), (key) => RoughTokenEstimate(key));
937
+ function isPlainObject(v) {
938
+ return typeof v === "object" && v !== null && !Array.isArray(v);
755
939
  }
756
- function estimateTextTokens(text) {
757
- return RoughTokenEstimate(text);
940
+ function describeType(v) {
941
+ if (v === null) return "null";
942
+ if (Array.isArray(v)) return "array";
943
+ return typeof v;
758
944
  }
759
- function computeMessageTokens(msg) {
760
- if (typeof msg.content === "string") return estimateTextTokens(msg.content);
761
- let total = 0;
762
- for (const b of msg.content) {
763
- if (b.type === "text") total += estimateTextTokens(b.text);
764
- else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
765
- else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
766
- else total += RoughTokenEstimate(JSON.stringify(b));
767
- }
945
+ function joinPath(parent, key) {
946
+ if (!parent) return key;
947
+ return `${parent}.${key}`;
948
+ }
949
+ function deepEqual(a, b) {
950
+ if (a === b) return true;
951
+ if (typeof a !== typeof b) return false;
952
+ if (a === null || b === null) return a === b;
953
+ if (Array.isArray(a) && Array.isArray(b)) {
954
+ return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
955
+ }
956
+ if (typeof a === "object" && typeof b === "object") {
957
+ const ak = Object.keys(a);
958
+ const bk = Object.keys(b);
959
+ if (ak.length !== bk.length) return false;
960
+ return ak.every(
961
+ (k) => deepEqual(a[k], b[k])
962
+ );
963
+ }
964
+ return false;
965
+ }
966
+
967
+ // src/utils/merge-models-payload.ts
968
+ function mergeModelsPayload(base, overlay) {
969
+ const out = {};
970
+ for (const [id, provider] of Object.entries(base)) {
971
+ out[id] = cloneProvider(provider);
972
+ }
973
+ for (const [id, ovProvider] of Object.entries(overlay)) {
974
+ const existing = out[id];
975
+ out[id] = existing ? mergeProvider(existing, ovProvider) : cloneProvider(ovProvider);
976
+ }
977
+ return out;
978
+ }
979
+ function mergeProvider(base, overlay) {
980
+ const models = {};
981
+ for (const [mid, m] of Object.entries(base.models ?? {})) {
982
+ models[mid] = { ...m };
983
+ }
984
+ for (const [mid, ovModel] of Object.entries(overlay.models ?? {})) {
985
+ const existing = models[mid];
986
+ models[mid] = existing ? mergeModel(existing, ovModel) : { ...ovModel };
987
+ }
988
+ return {
989
+ ...base,
990
+ // Overlay scalar fields win when explicitly provided; otherwise keep base.
991
+ ...stripUndefined({
992
+ id: overlay.id,
993
+ name: overlay.name,
994
+ npm: overlay.npm,
995
+ api: overlay.api,
996
+ env: overlay.env,
997
+ doc: overlay.doc
998
+ }),
999
+ models
1000
+ };
1001
+ }
1002
+ function mergeModel(base, overlay) {
1003
+ const merged = { ...base, ...overlay };
1004
+ if (base.limit || overlay.limit) {
1005
+ merged.limit = { ...base.limit, ...overlay.limit };
1006
+ }
1007
+ if (base.cost || overlay.cost) {
1008
+ merged.cost = { ...base.cost, ...overlay.cost };
1009
+ }
1010
+ if (base.modalities || overlay.modalities) {
1011
+ merged.modalities = { ...base.modalities, ...overlay.modalities };
1012
+ }
1013
+ return merged;
1014
+ }
1015
+ function cloneProvider(p) {
1016
+ const models = {};
1017
+ for (const [mid, m] of Object.entries(p.models ?? {})) {
1018
+ models[mid] = { ...m };
1019
+ }
1020
+ return { ...p, models };
1021
+ }
1022
+ function stripUndefined(obj) {
1023
+ const out = {};
1024
+ for (const [k, v] of Object.entries(obj)) {
1025
+ if (v !== void 0) out[k] = v;
1026
+ }
1027
+ return out;
1028
+ }
1029
+
1030
+ // src/utils/regex-guard.ts
1031
+ var MAX_PATTERN_LEN = 512;
1032
+ var DANGEROUS_PATTERNS = [
1033
+ /(\([^)]*[+*][^)]*\))[+*]/,
1034
+ // (a+)+, (.*)+, etc
1035
+ /(\(\?:[^)]*[+*][^)]*\))[+*]/
1036
+ // same, with non-capturing group
1037
+ ];
1038
+ function compileUserRegex(pattern, flags) {
1039
+ if (typeof pattern !== "string") {
1040
+ return { ok: false, reason: "pattern must be a string" };
1041
+ }
1042
+ if (pattern.length === 0) {
1043
+ return { ok: false, reason: "pattern is empty" };
1044
+ }
1045
+ if (pattern.length > MAX_PATTERN_LEN) {
1046
+ return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
1047
+ }
1048
+ for (const rx of DANGEROUS_PATTERNS) {
1049
+ if (rx.test(pattern)) {
1050
+ return {
1051
+ ok: false,
1052
+ reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
1053
+ };
1054
+ }
1055
+ }
1056
+ try {
1057
+ return { ok: true, regex: new RegExp(pattern, flags) };
1058
+ } catch (err) {
1059
+ return {
1060
+ ok: false,
1061
+ reason: err instanceof Error ? err.message : "invalid regex"
1062
+ };
1063
+ }
1064
+ }
1065
+
1066
+ // src/utils/safe-json.ts
1067
+ function safeParse(input, maxBytes = 5e6) {
1068
+ if (input.length > maxBytes) {
1069
+ return { ok: false, error: `Input exceeds limit (${maxBytes} bytes)` };
1070
+ }
1071
+ try {
1072
+ return { ok: true, value: JSON.parse(input) };
1073
+ } catch (err) {
1074
+ return {
1075
+ ok: false,
1076
+ error: toErrorMessage(err)
1077
+ };
1078
+ }
1079
+ }
1080
+
1081
+ // src/utils/sleep.ts
1082
+ function sleep(ms) {
1083
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
1084
+ }
1085
+
1086
+ // src/utils/string.ts
1087
+ function truncate(s, max) {
1088
+ return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
1089
+ }
1090
+
1091
+ // src/utils/tool-subject.ts
1092
+ var GLOB_METACHARACTERS = /[*?[\]]/g;
1093
+ function escapeGlobSubject(value) {
1094
+ return value.replace(GLOB_METACHARACTERS, (char) => `\\${char}`);
1095
+ }
1096
+ function normalizePathSubject(value) {
1097
+ return escapeGlobSubject(value.replace(/\\/g, "/"));
1098
+ }
1099
+ function isPathSubjectKey(subjectKey) {
1100
+ return subjectKey === "path" || subjectKey === "file" || subjectKey === "files";
1101
+ }
1102
+ function subjectForToolInput(toolName, input, subjectKey) {
1103
+ if (!input || typeof input !== "object") return void 0;
1104
+ const obj = input;
1105
+ if (subjectKey) {
1106
+ const value = obj[subjectKey];
1107
+ if (typeof value === "string") {
1108
+ return isPathSubjectKey(subjectKey) ? normalizePathSubject(value) : escapeGlobSubject(value);
1109
+ }
1110
+ }
1111
+ if (toolName === "bash" && typeof obj.command === "string") {
1112
+ return escapeGlobSubject(obj.command);
1113
+ }
1114
+ if (typeof obj.path === "string") {
1115
+ return normalizePathSubject(obj.path);
1116
+ }
1117
+ if (typeof obj.url === "string") {
1118
+ return escapeGlobSubject(obj.url);
1119
+ }
1120
+ if (typeof obj.name === "string") {
1121
+ return escapeGlobSubject(obj.name);
1122
+ }
1123
+ return void 0;
1124
+ }
1125
+
1126
+ // src/utils/tool-wire-compact.ts
1127
+ var TOOL_DESCRIPTION_MAX_CHARS = 640;
1128
+ var SCHEMA_DESCRIPTION_MAX_CHARS = 180;
1129
+ var compactCache = /* @__PURE__ */ new WeakMap();
1130
+ function compactToolDefinitionForWire(tool, opts = {}) {
1131
+ const useDefaultOptions = opts.descriptionMaxChars === void 0 && opts.schemaDescriptionMaxChars === void 0;
1132
+ if (useDefaultOptions && typeof tool === "object" && tool !== null) {
1133
+ const cached = compactCache.get(tool);
1134
+ if (cached) return cached;
1135
+ }
1136
+ const compact = {
1137
+ name: tool.name,
1138
+ description: compactDescription(
1139
+ tool.description ?? "",
1140
+ opts.descriptionMaxChars ?? TOOL_DESCRIPTION_MAX_CHARS
1141
+ ),
1142
+ inputSchema: compactSchemaDescriptions(
1143
+ tool.inputSchema,
1144
+ opts.schemaDescriptionMaxChars ?? SCHEMA_DESCRIPTION_MAX_CHARS
1145
+ )
1146
+ };
1147
+ if (useDefaultOptions && typeof tool === "object" && tool !== null) {
1148
+ compactCache.set(tool, compact);
1149
+ }
1150
+ return compact;
1151
+ }
1152
+ function compactSchemaDescriptions(schema, maxDescriptionChars = SCHEMA_DESCRIPTION_MAX_CHARS) {
1153
+ const compact = compactSchemaNode(schema, maxDescriptionChars);
1154
+ return isRecord(compact) ? compact : { type: "object", properties: {} };
1155
+ }
1156
+ function compactSchemaNode(node, maxDescriptionChars) {
1157
+ if (Array.isArray(node)) {
1158
+ return node.map((item) => compactSchemaNode(item, maxDescriptionChars));
1159
+ }
1160
+ if (!isRecord(node)) return node;
1161
+ const out = {};
1162
+ for (const [key, value] of Object.entries(node)) {
1163
+ if (key === "description" && typeof value === "string") {
1164
+ out[key] = compactDescription(value, maxDescriptionChars);
1165
+ } else {
1166
+ out[key] = compactSchemaNode(value, maxDescriptionChars);
1167
+ }
1168
+ }
1169
+ return out;
1170
+ }
1171
+ function compactDescription(text, maxChars) {
1172
+ const normalized = text.replace(/\s+/g, " ").trim();
1173
+ if (normalized.length <= maxChars) return normalized;
1174
+ if (maxChars <= 20) return normalized.slice(0, maxChars);
1175
+ const hardLimit = maxChars - 12;
1176
+ const boundary = findSemanticBoundary(normalized, hardLimit);
1177
+ const head = normalized.slice(0, boundary > 0 ? boundary : hardLimit).trimEnd();
1178
+ return `${head} ...`;
1179
+ }
1180
+ function findSemanticBoundary(text, limit) {
1181
+ const punctuation = Math.max(
1182
+ text.lastIndexOf(". ", limit),
1183
+ text.lastIndexOf("; ", limit),
1184
+ text.lastIndexOf(": ", limit)
1185
+ );
1186
+ if (punctuation >= Math.floor(limit * 0.45)) return punctuation + 1;
1187
+ const comma = text.lastIndexOf(", ", limit);
1188
+ if (comma >= Math.floor(limit * 0.6)) return comma + 1;
1189
+ const space = text.lastIndexOf(" ", limit);
1190
+ return space >= Math.floor(limit * 0.6) ? space : limit;
1191
+ }
1192
+ function isRecord(value) {
1193
+ return !!value && typeof value === "object" && !Array.isArray(value);
1194
+ }
1195
+
1196
+ // src/utils/token-estimate.ts
1197
+ var RoughTokenEstimate = (text, charsPerToken = 3.5) => Math.max(1, Math.ceil(text.length / charsPerToken));
1198
+ var CALIBRATION_GLOBAL_KEY = "__global__";
1199
+ var _cals = /* @__PURE__ */ new Map();
1200
+ function calState(key) {
1201
+ let state = _cals.get(key);
1202
+ if (!state) {
1203
+ state = { ratio: 1, count: 0, prevEst: 0 };
1204
+ _cals.set(key, state);
1205
+ }
1206
+ return state;
1207
+ }
1208
+ var MIN_SAMPLES_FOR_CALIBRATION = 3;
1209
+ var ESTIMATE_CACHE = /* @__PURE__ */ new Map();
1210
+ var ESTIMATE_CACHE_MAX_SIZE = 1e4;
1211
+ function getCachedEstimate(key, compute) {
1212
+ const existing = ESTIMATE_CACHE.get(key);
1213
+ if (existing !== void 0) return existing;
1214
+ if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
1215
+ for (const k of ESTIMATE_CACHE.keys()) {
1216
+ if (ESTIMATE_CACHE.size <= Math.floor(ESTIMATE_CACHE_MAX_SIZE / 2)) break;
1217
+ ESTIMATE_CACHE.delete(k);
1218
+ }
1219
+ }
1220
+ const estimate = compute(key);
1221
+ ESTIMATE_CACHE.set(key, estimate);
1222
+ return estimate;
1223
+ }
1224
+ function estimateToolInputTokens(input) {
1225
+ if (typeof input === "string") return RoughTokenEstimate(input);
1226
+ if (input === null || typeof input !== "object") {
1227
+ return RoughTokenEstimate(String(input));
1228
+ }
1229
+ return getCachedEstimate(JSON.stringify(input), (key) => RoughTokenEstimate(key));
1230
+ }
1231
+ function estimateToolResultTokens(content) {
1232
+ if (typeof content === "string") return RoughTokenEstimate(content);
1233
+ return getCachedEstimate(JSON.stringify(content), (key) => RoughTokenEstimate(key));
1234
+ }
1235
+ function estimateTextTokens(text) {
1236
+ return RoughTokenEstimate(text);
1237
+ }
1238
+ function computeMessageTokens(msg) {
1239
+ if (typeof msg.content === "string") return estimateTextTokens(msg.content);
1240
+ let total = 0;
1241
+ for (const b of msg.content) {
1242
+ if (b.type === "text") total += estimateTextTokens(b.text);
1243
+ else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
1244
+ else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
1245
+ else total += RoughTokenEstimate(JSON.stringify(b));
1246
+ }
768
1247
  return total;
769
1248
  }
770
1249
  function estimateMessageTokens(messages) {
@@ -781,7 +1260,8 @@ function estimateMessageTokens(messages) {
781
1260
  function estimateToolDefTokens(tool) {
782
1261
  const cached = tool._estDefTokens;
783
1262
  if (typeof cached === "number" && cached > 0) return cached;
784
- return RoughTokenEstimate(tool.name) + RoughTokenEstimate(tool.description ?? "") + RoughTokenEstimate(JSON.stringify(tool.inputSchema));
1263
+ const compact = compactToolDefinitionForWire(tool);
1264
+ return RoughTokenEstimate(tool.name) + RoughTokenEstimate(compact.description) + RoughTokenEstimate(JSON.stringify(compact.inputSchema));
785
1265
  }
786
1266
  function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
787
1267
  let messagesTokens = 0;
@@ -858,406 +1338,702 @@ function estimateRequestTokensCalibrated(messages, systemPrompt, tools, calibrat
858
1338
  return result;
859
1339
  }
860
1340
 
861
- // src/utils/json-schema-validate.ts
862
- function validateAgainstSchema(value, schema) {
863
- const errors = [];
864
- walk(value, schema, "", errors);
865
- return { ok: errors.length === 0, errors };
866
- }
867
- function walk(value, schema, path19, errors) {
868
- if (schema.enum !== void 0) {
869
- if (!schema.enum.some((e) => deepEqual(e, value))) {
870
- errors.push({
871
- path: path19 || "<root>",
872
- message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
873
- });
874
- return;
1341
+ // src/utils/tool-output-serializer.ts
1342
+ var DEFAULT_LIST_LIMIT = 500;
1343
+ var LOG_ENTRY_LIMIT = 200;
1344
+ var INLINE_LIMIT = 240;
1345
+ var GREP_FILE_LIMIT = 80;
1346
+ var GREP_MATCHES_PER_FILE = 3;
1347
+ var DIFF_INLINE_LINE_LIMIT = 260;
1348
+ var DIFF_HUNK_LIMIT = 8;
1349
+ var DIFF_HUNK_CONTEXT = 14;
1350
+ function createToolOutputSerializer(opts = {}) {
1351
+ const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
1352
+ function serialize(value, context = {}) {
1353
+ if (typeof value === "string") return value;
1354
+ if (value === null || value === void 0) return "";
1355
+ if (typeof value === "object") {
1356
+ if (Array.isArray(value)) return value.map((item) => serialize(item)).join("\n");
1357
+ if (context.toolName) {
1358
+ const compact = renderToolObject(context.toolName, value, context.input);
1359
+ if (compact !== void 0) return compact;
1360
+ return renderGenericToolObject(context.toolName, value);
1361
+ }
1362
+ if ("text" in value) {
1363
+ const t = value.text;
1364
+ return typeof t === "string" ? t : JSON.stringify(value, null, 2);
1365
+ }
1366
+ try {
1367
+ return JSON.stringify(value, null, 2);
1368
+ } catch {
1369
+ return String(value);
1370
+ }
875
1371
  }
1372
+ return String(value);
876
1373
  }
877
- if (typeof schema.type === "string") {
878
- if (!checkType(value, schema.type)) {
879
- errors.push({
880
- path: path19 || "<root>",
881
- message: `expected ${schema.type}, got ${describeType(value)}`
882
- });
883
- return;
884
- }
885
- }
886
- if (schema.type === "object" && isPlainObject(value)) {
887
- const obj = value;
888
- for (const req of schema.required ?? []) {
889
- if (!(req in obj)) {
890
- errors.push({ path: joinPath(path19, req), message: "required property missing" });
891
- }
1374
+ function enforceCap(text, remainingBudget) {
1375
+ if (remainingBudget <= 0) {
1376
+ return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
892
1377
  }
893
- if (schema.properties) {
894
- for (const [key, subSchema] of Object.entries(schema.properties)) {
895
- if (key in obj) {
896
- walk(obj[key], subSchema, joinPath(path19, key), errors);
897
- }
898
- }
1378
+ const textBytes = Buffer.byteLength(text, "utf8");
1379
+ if (textBytes <= remainingBudget) {
1380
+ return { text, newBudget: remainingBudget - textBytes };
899
1381
  }
900
- }
901
- if (schema.type === "array" && Array.isArray(value) && schema.items) {
902
- for (let i = 0; i < value.length; i++) {
903
- walk(value[i], schema.items, `${path19}[${i}]`, errors);
1382
+ const marker = `
1383
+ \u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
1384
+ `;
1385
+ const markerBytes = Buffer.byteLength(marker, "utf8");
1386
+ const available = remainingBudget - markerBytes;
1387
+ if (available <= 0) {
1388
+ return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
904
1389
  }
1390
+ const half = Math.floor(available / 2);
1391
+ const first = text.slice(0, half);
1392
+ const second = text.slice(text.length - half);
1393
+ return { text: `${first}${marker}${second}`, newBudget: 0 };
905
1394
  }
1395
+ return { serialize, enforceCap, capBytes };
906
1396
  }
907
- function checkType(value, type) {
908
- switch (type) {
909
- case "string":
910
- return typeof value === "string";
911
- case "number":
912
- return typeof value === "number" && !Number.isNaN(value);
913
- case "integer":
914
- return typeof value === "number" && Number.isInteger(value);
915
- case "boolean":
916
- return typeof value === "boolean";
917
- case "null":
918
- return value === null;
919
- case "array":
920
- return Array.isArray(value);
921
- case "object":
922
- return isPlainObject(value);
923
- default:
924
- return true;
925
- }
926
- }
927
- function isPlainObject(v) {
928
- return typeof v === "object" && v !== null && !Array.isArray(v);
929
- }
930
- function describeType(v) {
931
- if (v === null) return "null";
932
- if (Array.isArray(v)) return "array";
933
- return typeof v;
934
- }
935
- function joinPath(parent, key) {
936
- if (!parent) return key;
937
- return `${parent}.${key}`;
938
- }
939
- function deepEqual(a, b) {
940
- if (a === b) return true;
941
- if (typeof a !== typeof b) return false;
942
- if (a === null || b === null) return a === b;
943
- if (Array.isArray(a) && Array.isArray(b)) {
944
- return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
945
- }
946
- if (typeof a === "object" && typeof b === "object") {
947
- const ak = Object.keys(a);
948
- const bk = Object.keys(b);
949
- if (ak.length !== bk.length) return false;
950
- return ak.every(
951
- (k) => deepEqual(a[k], b[k])
952
- );
953
- }
954
- return false;
955
- }
956
-
957
- // src/utils/regex-guard.ts
958
- var MAX_PATTERN_LEN = 512;
959
- var DANGEROUS_PATTERNS = [
960
- /(\([^)]*[+*][^)]*\))[+*]/,
961
- // (a+)+, (.*)+, etc
962
- /(\(\?:[^)]*[+*][^)]*\))[+*]/
963
- // same, with non-capturing group
964
- ];
965
- function compileUserRegex(pattern, flags) {
966
- if (typeof pattern !== "string") {
967
- return { ok: false, reason: "pattern must be a string" };
968
- }
969
- if (pattern.length === 0) {
970
- return { ok: false, reason: "pattern is empty" };
971
- }
972
- if (pattern.length > MAX_PATTERN_LEN) {
973
- return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
974
- }
975
- for (const rx of DANGEROUS_PATTERNS) {
976
- if (rx.test(pattern)) {
977
- return {
978
- ok: false,
979
- reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
980
- };
981
- }
982
- }
983
- try {
984
- return { ok: true, regex: new RegExp(pattern, flags) };
985
- } catch (err) {
986
- return {
987
- ok: false,
988
- reason: err instanceof Error ? err.message : "invalid regex"
989
- };
1397
+ function renderToolObject(toolName, obj, input) {
1398
+ if (toolName === "read" && typeof obj["text"] === "string") {
1399
+ return joinSections([
1400
+ renderHeader(
1401
+ `read: ${stringFromInput(input, "path") ?? stringField(obj, "path") ?? "<unknown>"}`,
1402
+ {
1403
+ offset: numberFromInput(input, "offset"),
1404
+ limit: numberFromInput(input, "limit"),
1405
+ total_lines: obj["total_lines"],
1406
+ encoding: obj["encoding"],
1407
+ truncated: obj["truncated"],
1408
+ cached: obj["cached"],
1409
+ note: obj["note"]
1410
+ }
1411
+ ),
1412
+ obj["text"]
1413
+ ]);
990
1414
  }
991
- }
992
- var GLOB_CHARS = /* @__PURE__ */ new Set(["*", "?", "["]);
993
- var IS_WINDOWS = process.platform === "win32";
994
- var SEP = IS_WINDOWS ? "\\" : "/";
995
- function isGlob(p) {
996
- for (const c of p) {
997
- if (GLOB_CHARS.has(c)) return true;
1415
+ if (toolName === "grep" && Array.isArray(obj["matches"])) {
1416
+ const matches = stringArrayField(obj, "matches");
1417
+ return joinSections([
1418
+ renderHeader(`grep: ${stringFromInput(input, "pattern") ?? "<pattern>"}`, {
1419
+ path: stringFromInput(input, "path"),
1420
+ glob: stringFromInput(input, "glob"),
1421
+ mode: stringFromInput(input, "output_mode"),
1422
+ count: obj["count"],
1423
+ shown: matches.length,
1424
+ truncated: obj["truncated"],
1425
+ used: obj["used"]
1426
+ }),
1427
+ renderGrepMatches(matches, stringFromInput(input, "output_mode"))
1428
+ ]);
998
1429
  }
999
- return false;
1000
- }
1001
- function globToRegex(pat) {
1002
- let i = 0;
1003
- let re = "^";
1004
- while (i < pat.length) {
1005
- const c = expectDefined(pat[i]);
1006
- if (c === "*") {
1007
- if (pat[i + 1] === "*") {
1008
- re += ".*";
1009
- i += 2;
1010
- if (pat[i] === "/") i++;
1011
- } else {
1012
- re += "[^/\\\\]*";
1013
- i++;
1014
- }
1015
- } else if (c === "?") {
1016
- re += "[^/\\\\]";
1017
- i++;
1018
- } else if (c === "[") {
1019
- let cls = "[";
1020
- i++;
1021
- if (pat[i] === "!" || pat[i] === "^") {
1022
- cls += "^";
1023
- i++;
1024
- }
1025
- while (i < pat.length && pat[i] !== "]") {
1026
- const ch = pat[i] ?? "";
1027
- if (ch === "\\") cls += "\\\\";
1028
- else if (ch === "]" || ch === "^") cls += `\\${ch}`;
1029
- else cls += ch;
1030
- i++;
1031
- }
1032
- cls += "]";
1033
- re += cls;
1034
- i++;
1035
- } else {
1036
- re += c.replace(/[.+^${}()|\\]/g, "\\$&");
1037
- i++;
1038
- }
1430
+ if (toolName === "patch" && Array.isArray(obj["files"])) {
1431
+ const files = stringArrayField(obj, "files");
1432
+ return joinSections([
1433
+ renderHeader("patch", {
1434
+ applied: obj["applied"],
1435
+ rejected: obj["rejected"],
1436
+ files: files.length,
1437
+ dry_run: obj["dry_run"]
1438
+ }),
1439
+ typeof obj["message"] === "string" ? `message:
1440
+ ${obj["message"]}` : void 0,
1441
+ files.length > 0 ? `files:
1442
+ ${renderStringList(files)}` : void 0
1443
+ ]);
1039
1444
  }
1040
- return new RegExp(re + "$");
1041
- }
1042
- function baseDir(pat) {
1043
- let i = pat.length - 1;
1044
- while (i >= 0 && !GLOB_CHARS.has(expectDefined(pat[i])) && pat[i] !== SEP && pat[i] !== "/") i--;
1045
- const cut = i >= 0 ? pat.lastIndexOf(SEP, i) : pat.lastIndexOf("/", i);
1046
- return cut < 0 ? "." : pat.slice(0, cut);
1047
- }
1048
- async function expandGlob(pattern) {
1049
- if (!isGlob(pattern)) return [pattern];
1050
- const results = /* @__PURE__ */ new Set();
1051
- const abs = isAbsolute(pattern);
1052
- const base = abs ? baseDir(pattern) : baseDir(pattern);
1053
- const relPat = base === "." ? pattern : pattern.slice(base.length + 1);
1054
- async function walk3(dir, pat) {
1055
- let entries;
1056
- try {
1057
- entries = await fsp2.readdir(dir);
1058
- } catch {
1059
- return;
1060
- }
1061
- const firstGlob = pat.search(/[*?[[]/);
1062
- if (firstGlob < 0) {
1063
- const re = globToRegex(pat);
1064
- for (const e of entries) {
1065
- if (re.test(e)) {
1066
- const full = `${dir}${SEP}${e}`;
1067
- results.add(abs ? resolve(full) : full);
1068
- }
1069
- }
1070
- return;
1071
- }
1072
- const before = pat.slice(0, firstGlob);
1073
- const rest = pat.slice(firstGlob);
1074
- if (before.endsWith("**")) {
1075
- await walk3(dir, rest);
1076
- for (const e of entries) {
1077
- const full = `${dir}${SEP}${e}`;
1078
- try {
1079
- const stat6 = await fsp2.stat(full);
1080
- if (stat6.isDirectory()) await walk3(full, rest);
1081
- } catch {
1445
+ if (toolName === "glob" && Array.isArray(obj["files"])) {
1446
+ const files = stringArrayField(obj, "files");
1447
+ return joinSections([
1448
+ renderHeader(
1449
+ `${toolName}: ${stringFromInput(input, "pattern") ?? stringFromInput(input, "files") ?? stringFromInput(input, "path") ?? ""}`.trim(),
1450
+ {
1451
+ path: stringFromInput(input, "path"),
1452
+ files: files.length,
1453
+ truncated: obj["truncated"]
1082
1454
  }
1083
- }
1084
- } else if (before === "") {
1085
- const re = globToRegex(rest);
1086
- for (const e of entries) {
1087
- if (re.test(e)) {
1088
- const full = `${dir}${SEP}${e}`;
1089
- results.add(abs ? resolve(full) : full);
1455
+ ),
1456
+ renderStringList(files, "(no files)")
1457
+ ]);
1458
+ }
1459
+ if (toolName === "tree" && typeof obj["tree"] === "string") {
1460
+ return joinSections([
1461
+ renderHeader(
1462
+ `tree: ${stringField(obj, "path") ?? stringFromInput(input, "path") ?? "<cwd>"}`,
1463
+ {
1464
+ total_files: obj["total_files"],
1465
+ total_dirs: obj["total_dirs"],
1466
+ truncated: obj["truncated"]
1090
1467
  }
1091
- }
1092
- } else {
1093
- const seg = before.replace(/[*?[\]]/g, "").replace(/\/$/, "");
1094
- if (entries.includes(seg)) {
1095
- const full = `${dir}${SEP}${seg}`;
1096
- try {
1097
- const stat6 = await fsp2.stat(full);
1098
- if (stat6.isDirectory()) await walk3(full, rest);
1099
- } catch {
1468
+ ),
1469
+ obj["tree"]
1470
+ ]);
1471
+ }
1472
+ if (toolName === "fetch" && typeof obj["content"] === "string") {
1473
+ return joinSections([
1474
+ renderHeader(
1475
+ `fetch: ${stringField(obj, "url") ?? stringFromInput(input, "url") ?? "<url>"}`,
1476
+ {
1477
+ status: obj["status"],
1478
+ content_type: obj["content_type"]
1100
1479
  }
1101
- }
1102
- }
1480
+ ),
1481
+ obj["content"]
1482
+ ]);
1103
1483
  }
1104
- await walk3(base === "." ? "." : base, relPat);
1105
- return [...results];
1106
- }
1107
-
1108
- // src/utils/json-repair.ts
1109
- function completePartialObject(s) {
1110
- if (!s.trim().startsWith("{")) return s;
1111
- if (tryParse(s).ok) return s;
1112
- return repairTruncated(s);
1113
- }
1114
- function repairTruncated(s) {
1115
- const stack = [];
1116
- let inString = false;
1117
- let escaped = false;
1118
- let sawKey = false;
1119
- let prevSig = "";
1120
- let contentEnd = 0;
1121
- let stringBraceDepth = 0;
1122
- for (let i = 0; i < s.length; i++) {
1123
- const ch = expectDefined(s[i]);
1124
- if (inString) {
1125
- contentEnd = i + 1;
1126
- if (escaped) {
1127
- escaped = false;
1128
- continue;
1129
- }
1130
- if (ch === "\\") {
1131
- escaped = true;
1132
- continue;
1133
- }
1134
- if (ch === '"') {
1135
- inString = false;
1136
- prevSig = '"';
1137
- stringBraceDepth = 0;
1138
- continue;
1139
- }
1140
- if (ch === "{") stringBraceDepth++;
1141
- else if (ch === "}" && stringBraceDepth > 0) stringBraceDepth--;
1142
- continue;
1143
- }
1144
- if (ch === " " || ch === " " || ch === "\n" || ch === "\r") continue;
1145
- contentEnd = i + 1;
1146
- if (ch === '"') {
1147
- inString = true;
1148
- sawKey = true;
1149
- stringBraceDepth = 0;
1150
- prevSig = '"';
1151
- } else if (ch === "{" || ch === "[") {
1152
- stack.push(ch);
1153
- prevSig = ch;
1154
- } else if (ch === "}" || ch === "]") {
1155
- stack.pop();
1156
- prevSig = ch;
1157
- } else {
1158
- prevSig = ch;
1484
+ if (toolName === "replace" && Array.isArray(obj["results"])) {
1485
+ const results = obj["results"].filter(isRecord2);
1486
+ const sections = [
1487
+ renderHeader("replace", {
1488
+ files_modified: obj["files_modified"],
1489
+ total_replacements: obj["total_replacements"],
1490
+ dry_run: obj["dry_run"]
1491
+ })
1492
+ ];
1493
+ for (const r of results.slice(0, DEFAULT_LIST_LIMIT)) {
1494
+ sections.push(
1495
+ joinSections([
1496
+ renderHeader(`file: ${stringField(r, "path") ?? "<unknown>"}`, {
1497
+ replacements: r["replacements"]
1498
+ }),
1499
+ typeof r["diff"] === "string" ? r["diff"] : void 0
1500
+ ])
1501
+ );
1159
1502
  }
1503
+ if (results.length > DEFAULT_LIST_LIMIT) {
1504
+ sections.push(`[serializer omitted ${results.length - DEFAULT_LIST_LIMIT} result item(s)]`);
1505
+ }
1506
+ return joinSections(sections);
1507
+ }
1508
+ if (typeof obj["diff"] === "string") {
1509
+ const diff = obj["diff"];
1510
+ return joinSections([
1511
+ renderHeader(toolName, {
1512
+ path: obj["path"],
1513
+ replacements: obj["replacements"],
1514
+ bytes_written: obj["bytes_written"],
1515
+ created: obj["created"],
1516
+ note: obj["note"],
1517
+ files: Array.isArray(obj["files"]) ? obj["files"].length : void 0,
1518
+ truncated: obj["truncated"],
1519
+ mode: obj["mode"]
1520
+ }),
1521
+ compactDiff(diff)
1522
+ ]);
1160
1523
  }
1161
- if (!sawKey && !inString) return s;
1162
- let result = s.slice(0, contentEnd);
1163
- if (inString) {
1164
- if (escaped) {
1165
- result = result.slice(0, -1);
1166
- } else if (endsWithInvalidEscape(result)) {
1167
- result = result.slice(0, -2);
1168
- }
1169
- if (stringBraceDepth > 0) result += "}".repeat(stringBraceDepth);
1170
- result += '"';
1171
- } else if (prevSig === ":") {
1172
- result += "null";
1524
+ if (toolName === "test" && typeof obj["output"] === "string") {
1525
+ return renderTestOutput(obj, input);
1173
1526
  }
1174
- for (let k = stack.length - 1; k >= 0; k--) {
1175
- result += stack[k] === "{" ? "}" : "]";
1527
+ if ((toolName === "typecheck" || toolName === "lint" || toolName === "format") && typeof obj["output"] === "string") {
1528
+ return renderVerifierOutput(toolName, obj, input);
1176
1529
  }
1177
- if (!tryParse(result).ok) {
1178
- const patched = result.replace(/:(\s*)([}\]])/g, ":null$2");
1179
- if (tryParse(patched).ok) result = patched;
1530
+ if (hasCommandOutputShape(obj)) {
1531
+ return renderCommandOutput(toolName, obj, input);
1180
1532
  }
1181
- return result;
1182
- }
1183
- var VALID_ESCAPE = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
1184
- function endsWithInvalidEscape(str) {
1185
- const last = str[str.length - 1];
1186
- if (str[str.length - 2] !== "\\" || last === void 0) return false;
1187
- if (VALID_ESCAPE.has(last)) return false;
1188
- let backslashes = 0;
1189
- for (let k = str.length - 2; k >= 0 && str[k] === "\\"; k--) backslashes++;
1190
- return backslashes % 2 === 1;
1191
- }
1192
- function tryParse(s) {
1193
- try {
1194
- return { ok: true, value: JSON.parse(s) };
1195
- } catch {
1196
- return { ok: false };
1533
+ if (toolName === "json" && typeof obj["formatted"] === "string") {
1534
+ return joinSections([
1535
+ renderHeader("json", {
1536
+ type: obj["type"],
1537
+ keys: Array.isArray(obj["keys"]) ? obj["keys"].length : void 0,
1538
+ query: stringFromInput(input, "query"),
1539
+ error: obj["error"]
1540
+ }),
1541
+ obj["formatted"]
1542
+ ]);
1197
1543
  }
1198
- }
1199
-
1200
- // src/utils/merge-models-payload.ts
1201
- function mergeModelsPayload(base, overlay) {
1202
- const out = {};
1203
- for (const [id, provider] of Object.entries(base)) {
1204
- out[id] = cloneProvider(provider);
1544
+ if (toolName === "logs" && Array.isArray(obj["entries"])) {
1545
+ const entries = obj["entries"].filter(isRecord2);
1546
+ const lines = entries.slice(0, LOG_ENTRY_LIMIT).map((entry) => {
1547
+ const ts = stringField(entry, "timestamp") ?? "";
1548
+ const level = stringField(entry, "level") ?? "info";
1549
+ const message = stringField(entry, "message") ?? "";
1550
+ const source = stringField(entry, "source");
1551
+ return [ts, level, source, message].filter(Boolean).join(" ");
1552
+ });
1553
+ if (entries.length > LOG_ENTRY_LIMIT) {
1554
+ lines.push(`[serializer omitted ${entries.length - LOG_ENTRY_LIMIT} log entry item(s)]`);
1555
+ }
1556
+ return joinSections([
1557
+ renderHeader(`logs: ${stringField(obj, "source") ?? "<source>"}`, {
1558
+ total: obj["total"],
1559
+ shown: Math.min(entries.length, LOG_ENTRY_LIMIT),
1560
+ truncated: obj["truncated"],
1561
+ stream_mode: obj["stream_mode"]
1562
+ }),
1563
+ lines.length > 0 ? lines.join("\n") : "(no log entries)"
1564
+ ]);
1205
1565
  }
1206
- for (const [id, ovProvider] of Object.entries(overlay)) {
1207
- const existing = out[id];
1208
- out[id] = existing ? mergeProvider(existing, ovProvider) : cloneProvider(ovProvider);
1566
+ if (toolName === "audit" && Array.isArray(obj["vulnerabilities"])) {
1567
+ const vulns = obj["vulnerabilities"].filter(isRecord2);
1568
+ const lines = vulns.slice(0, DEFAULT_LIST_LIMIT).map((v) => {
1569
+ const severity = stringField(v, "severity") ?? "unknown";
1570
+ const pkg = stringField(v, "package") ?? "<package>";
1571
+ const title = stringField(v, "title") ?? "";
1572
+ const url = stringField(v, "url");
1573
+ return [severity, pkg, title, url].filter(Boolean).join(" | ");
1574
+ });
1575
+ if (vulns.length > DEFAULT_LIST_LIMIT) {
1576
+ lines.push(`[serializer omitted ${vulns.length - DEFAULT_LIST_LIMIT} vulnerability item(s)]`);
1577
+ }
1578
+ return joinSections([
1579
+ renderHeader("audit", {
1580
+ exit_code: obj["exit_code"],
1581
+ total: obj["total"],
1582
+ summary: obj["summary"],
1583
+ truncated: obj["truncated"]
1584
+ }),
1585
+ lines.length > 0 ? lines.join("\n") : stringField(obj, "output")
1586
+ ]);
1209
1587
  }
1210
- return out;
1588
+ if (toolName === "outdated" && Array.isArray(obj["packages"])) {
1589
+ const packages = obj["packages"].filter(isRecord2);
1590
+ const lines = packages.slice(0, DEFAULT_LIST_LIMIT).map(
1591
+ (p) => [
1592
+ stringField(p, "name") ?? "<package>",
1593
+ `current=${stringField(p, "current") ?? "unknown"}`,
1594
+ `wanted=${stringField(p, "wanted") ?? "unknown"}`,
1595
+ `latest=${stringField(p, "latest") ?? "unknown"}`,
1596
+ stringField(p, "type")
1597
+ ].filter(Boolean).join(" | ")
1598
+ );
1599
+ if (packages.length > DEFAULT_LIST_LIMIT) {
1600
+ lines.push(`[serializer omitted ${packages.length - DEFAULT_LIST_LIMIT} package item(s)]`);
1601
+ }
1602
+ return joinSections([
1603
+ renderHeader("outdated", {
1604
+ exit_code: obj["exit_code"],
1605
+ total: obj["total"],
1606
+ truncated: obj["truncated"]
1607
+ }),
1608
+ lines.length > 0 ? lines.join("\n") : stringField(obj, "output")
1609
+ ]);
1610
+ }
1611
+ return void 0;
1211
1612
  }
1212
- function mergeProvider(base, overlay) {
1213
- const models = {};
1214
- for (const [mid, m] of Object.entries(base.models ?? {})) {
1215
- models[mid] = { ...m };
1613
+ function renderTestOutput(obj, input) {
1614
+ const exitCode = numberField(obj, "exit_code") ?? 0;
1615
+ const failed = numberField(obj, "failed") ?? 0;
1616
+ const output = stringField(obj, "output") ?? "";
1617
+ const header = renderHeader(`test: ${stringField(obj, "runner") ?? "runner"}`, {
1618
+ exit_code: obj["exit_code"],
1619
+ tests_run: obj["tests_run"],
1620
+ passed: obj["passed"],
1621
+ failed: obj["failed"],
1622
+ duration_ms: obj["duration_ms"],
1623
+ truncated: obj["truncated"],
1624
+ files: inputListSummary(input, "files"),
1625
+ grep: stringFromInput(input, "grep")
1626
+ });
1627
+ if (exitCode === 0 && failed === 0) {
1628
+ return joinSections([
1629
+ header,
1630
+ joinSections([
1631
+ "report:",
1632
+ `status=passed`,
1633
+ `tests_run=${obj["tests_run"] ?? 0}`,
1634
+ `passed=${obj["passed"] ?? 0}`,
1635
+ `failed=${obj["failed"] ?? 0}`,
1636
+ `duration_ms=${obj["duration_ms"] ?? 0}`,
1637
+ extractSpoolNote(output)
1638
+ ])
1639
+ ]);
1216
1640
  }
1217
- for (const [mid, ovModel] of Object.entries(overlay.models ?? {})) {
1218
- const existing = models[mid];
1219
- models[mid] = existing ? mergeModel(existing, ovModel) : { ...ovModel };
1641
+ return joinSections([
1642
+ header,
1643
+ `error_context:
1644
+ ${compactFailureOutput(output || "(no runner output)")}`
1645
+ ]);
1646
+ }
1647
+ function renderVerifierOutput(toolName, obj, input) {
1648
+ const exitCode = numberField(obj, "exit_code") ?? 0;
1649
+ const errors = numberField(obj, "errors") ?? 0;
1650
+ const warnings = numberField(obj, "warnings") ?? 0;
1651
+ const output = stringField(obj, "output") ?? "";
1652
+ const changed = numberField(obj, "files_changed") ?? 0;
1653
+ const header = renderHeader(toolName, {
1654
+ exit_code: obj["exit_code"],
1655
+ errors: obj["errors"],
1656
+ warnings: obj["warnings"],
1657
+ files_checked: obj["files_checked"],
1658
+ files_changed: obj["files_changed"],
1659
+ fix_applied: obj["fix_applied"],
1660
+ fixer: obj["fixer"],
1661
+ linter: obj["linter"],
1662
+ project: obj["project"],
1663
+ truncated: obj["truncated"],
1664
+ files: inputListSummary(input, "files"),
1665
+ cwd: stringFromInput(input, "cwd")
1666
+ });
1667
+ if (exitCode === 0 && errors === 0 && (toolName !== "format" || changed === 0)) {
1668
+ return joinSections([
1669
+ header,
1670
+ joinSections([
1671
+ "report:",
1672
+ "status=passed",
1673
+ `errors=${errors}`,
1674
+ `warnings=${warnings}`,
1675
+ toolName === "format" ? `files_changed=${changed}` : void 0,
1676
+ extractSpoolNote(output)
1677
+ ])
1678
+ ]);
1220
1679
  }
1221
- return {
1222
- ...base,
1223
- // Overlay scalar fields win when explicitly provided; otherwise keep base.
1224
- ...stripUndefined({
1225
- id: overlay.id,
1226
- name: overlay.name,
1227
- npm: overlay.npm,
1228
- api: overlay.api,
1229
- env: overlay.env,
1230
- doc: overlay.doc
1231
- }),
1232
- models
1233
- };
1680
+ if (exitCode === 0 && toolName === "format") {
1681
+ return joinSections([
1682
+ header,
1683
+ joinSections([
1684
+ "report:",
1685
+ "status=changed",
1686
+ `files_changed=${changed}`,
1687
+ extractSpoolNote(output)
1688
+ ])
1689
+ ]);
1690
+ }
1691
+ return joinSections([
1692
+ header,
1693
+ `error_context:
1694
+ ${compactFailureOutput(output || "(no verifier output)")}`
1695
+ ]);
1234
1696
  }
1235
- function mergeModel(base, overlay) {
1236
- const merged = { ...base, ...overlay };
1237
- if (base.limit || overlay.limit) {
1238
- merged.limit = { ...base.limit, ...overlay.limit };
1697
+ function renderGrepMatches(matches, mode) {
1698
+ if (matches.length === 0) return "(no matches)";
1699
+ if (mode === "files_with_matches") return renderStringList(matches, "(no files)");
1700
+ if (mode === "count") return renderStringList(matches, "(no counts)");
1701
+ const groups = /* @__PURE__ */ new Map();
1702
+ const passthrough = [];
1703
+ for (const match of matches) {
1704
+ const parsed = parseGrepContentLine(match);
1705
+ if (!parsed) {
1706
+ passthrough.push(match);
1707
+ continue;
1708
+ }
1709
+ const list = groups.get(parsed.file) ?? [];
1710
+ list.push(`${parsed.line}:${parsed.text}`);
1711
+ groups.set(parsed.file, list);
1239
1712
  }
1240
- if (base.cost || overlay.cost) {
1241
- merged.cost = { ...base.cost, ...overlay.cost };
1713
+ if (groups.size === 0) return renderStringList(matches, "(no matches)");
1714
+ const sections = [];
1715
+ let fileIndex = 0;
1716
+ for (const [file, lines] of groups) {
1717
+ fileIndex++;
1718
+ if (fileIndex > GREP_FILE_LIMIT) break;
1719
+ const shown = lines.slice(0, GREP_MATCHES_PER_FILE);
1720
+ sections.push(
1721
+ `${file} (${lines.length} match(es), showing ${shown.length})
1722
+ ${shown.join("\n")}`
1723
+ );
1242
1724
  }
1243
- if (base.modalities || overlay.modalities) {
1244
- merged.modalities = { ...base.modalities, ...overlay.modalities };
1725
+ if (groups.size > GREP_FILE_LIMIT) {
1726
+ sections.push(`[serializer omitted ${groups.size - GREP_FILE_LIMIT} file group(s)]`);
1727
+ }
1728
+ if (passthrough.length > 0) {
1729
+ sections.push(`ungrouped:
1730
+ ${renderStringList(passthrough, "", 50)}`);
1731
+ }
1732
+ return sections.join("\n");
1733
+ }
1734
+ function parseGrepContentLine(line) {
1735
+ const match = /^(.+?):(\d+):(.*)$/.exec(line);
1736
+ if (!match?.[1] || !match[2]) return void 0;
1737
+ return { file: match[1], line: match[2], text: match[3] ?? "" };
1738
+ }
1739
+ function compactDiff(diff) {
1740
+ const lines = diff.split(/\r?\n/);
1741
+ if (lines.length <= DIFF_INLINE_LINE_LIMIT) return diff;
1742
+ const fileCount = Math.max(
1743
+ new Set(
1744
+ lines.map(
1745
+ (line) => /^diff --git\s+a\/(.+?)\s+b\//.exec(line)?.[1] ?? /^---\s+(.+)/.exec(line)?.[1]
1746
+ ).filter(Boolean)
1747
+ ).size,
1748
+ 0
1749
+ );
1750
+ const hunks = lines.filter((line) => line.startsWith("@@")).length;
1751
+ const added = lines.filter((line) => line.startsWith("+") && !line.startsWith("+++")).length;
1752
+ const removed = lines.filter((line) => line.startsWith("-") && !line.startsWith("---")).length;
1753
+ const selected = /* @__PURE__ */ new Set();
1754
+ let hunkCount = 0;
1755
+ for (let i = 0; i < lines.length; i++) {
1756
+ const line = lines[i] ?? "";
1757
+ if (line.startsWith("diff --git") || line.startsWith("--- ") || line.startsWith("+++ ")) {
1758
+ selected.add(i);
1759
+ continue;
1760
+ }
1761
+ if (!line.startsWith("@@")) continue;
1762
+ if (hunkCount >= DIFF_HUNK_LIMIT) continue;
1763
+ hunkCount++;
1764
+ for (let j = i; j <= Math.min(lines.length - 1, i + DIFF_HUNK_CONTEXT); j++) {
1765
+ selected.add(j);
1766
+ }
1767
+ }
1768
+ if (selected.size === 0) {
1769
+ return joinSections([
1770
+ renderHeader("diff_summary", {
1771
+ files: fileCount,
1772
+ hunks,
1773
+ added,
1774
+ removed,
1775
+ lines: lines.length
1776
+ }),
1777
+ lines.slice(0, DIFF_INLINE_LINE_LIMIT).join("\n"),
1778
+ `[serializer omitted ${Math.max(0, lines.length - DIFF_INLINE_LINE_LIMIT)} diff line(s)]`
1779
+ ]);
1245
1780
  }
1246
- return merged;
1247
- }
1248
- function cloneProvider(p) {
1249
- const models = {};
1250
- for (const [mid, m] of Object.entries(p.models ?? {})) {
1251
- models[mid] = { ...m };
1781
+ const excerpt = [];
1782
+ let previous = -1;
1783
+ for (const index of [...selected].sort((a, b) => a - b)) {
1784
+ if (index > previous + 1) {
1785
+ const omitted = previous === -1 ? index : index - previous - 1;
1786
+ excerpt.push(`[serializer omitted ${omitted} diff line(s)]`);
1787
+ }
1788
+ excerpt.push(lines[index] ?? "");
1789
+ previous = index;
1790
+ }
1791
+ const trailing = lines.length - previous - 1;
1792
+ if (trailing > 0) excerpt.push(`[serializer omitted ${trailing} trailing diff line(s)]`);
1793
+ return joinSections([
1794
+ renderHeader("diff_summary", {
1795
+ files: fileCount,
1796
+ hunks,
1797
+ shown_hunks: Math.min(hunks, DIFF_HUNK_LIMIT),
1798
+ added,
1799
+ removed,
1800
+ lines: lines.length
1801
+ }),
1802
+ excerpt.join("\n")
1803
+ ]);
1804
+ }
1805
+ function compactFailureOutput(output) {
1806
+ const lines = output.split(/\r?\n/);
1807
+ if (lines.length <= 260) return output.trimEnd();
1808
+ const selected = /* @__PURE__ */ new Set();
1809
+ const marker = /\b(fail|failed|failure|error|exception|assertionerror|expected|received|actual|timeout|stack)\b/i;
1810
+ let markerHits = 0;
1811
+ for (let i = 0; i < lines.length; i++) {
1812
+ if (!marker.test(lines[i] ?? "")) continue;
1813
+ markerHits++;
1814
+ for (let j = Math.max(0, i - 4); j <= Math.min(lines.length - 1, i + 10); j++) {
1815
+ selected.add(j);
1816
+ }
1817
+ }
1818
+ if (markerHits === 0) {
1819
+ return lines.slice(-220).join("\n").trimEnd();
1820
+ }
1821
+ const ordered = [...selected].sort((a, b) => a - b);
1822
+ const out = [];
1823
+ let previous = -1;
1824
+ for (const index of ordered) {
1825
+ if (index > previous + 1) {
1826
+ const omitted = previous === -1 ? index : index - previous - 1;
1827
+ out.push(`[serializer omitted ${omitted} line(s)]`);
1828
+ }
1829
+ out.push(lines[index] ?? "");
1830
+ previous = index;
1831
+ }
1832
+ return out.join("\n").trimEnd();
1833
+ }
1834
+ function extractSpoolNote(output) {
1835
+ return output.split(/\r?\n/).find((line) => line.startsWith("[output truncated") && line.includes("full"));
1836
+ }
1837
+ function hasCommandOutputShape(obj) {
1838
+ return typeof obj["stdout"] === "string" || typeof obj["stderr"] === "string" || typeof obj["output"] === "string" || typeof obj["exitCode"] === "number" || typeof obj["exit_code"] === "number";
1839
+ }
1840
+ function renderCommandOutput(toolName, obj, input) {
1841
+ const command = stringField(obj, "command") ?? stringFromInput(input, "command");
1842
+ const args = stringArrayField(obj, "args");
1843
+ const commandLine = command ? [command, ...args].join(" ") : void 0;
1844
+ const output = stringField(obj, "output");
1845
+ const stdout = stringField(obj, "stdout");
1846
+ const stderr = stringField(obj, "stderr");
1847
+ return joinSections([
1848
+ renderHeader(commandLine ? `${toolName}: ${commandLine}` : toolName, {
1849
+ exit_code: obj["exit_code"] ?? obj["exitCode"],
1850
+ timed_out: obj["timed_out"],
1851
+ pid: obj["pid"],
1852
+ allowed: obj["allowed"],
1853
+ truncated: obj["truncated"],
1854
+ runner: obj["runner"],
1855
+ linter: obj["linter"],
1856
+ fixer: obj["fixer"],
1857
+ project: obj["project"],
1858
+ tests_run: obj["tests_run"],
1859
+ passed: obj["passed"],
1860
+ failed: obj["failed"],
1861
+ duration_ms: obj["duration_ms"],
1862
+ errors: obj["errors"],
1863
+ warnings: obj["warnings"],
1864
+ files_checked: obj["files_checked"],
1865
+ files_changed: obj["files_changed"],
1866
+ fix_applied: obj["fix_applied"]
1867
+ }),
1868
+ stringField(obj, "error") ? `error:
1869
+ ${stringField(obj, "error")}` : void 0,
1870
+ output ? `output:
1871
+ ${output}` : void 0,
1872
+ stdout ? `stdout:
1873
+ ${stdout}` : void 0,
1874
+ stderr ? `stderr:
1875
+ ${stderr}` : void 0
1876
+ ]);
1877
+ }
1878
+ function renderGenericToolObject(toolName, obj) {
1879
+ const scalars = {};
1880
+ const blocks = [];
1881
+ for (const [key, value] of Object.entries(obj)) {
1882
+ if (value === void 0) continue;
1883
+ if (isScalar(value)) {
1884
+ const inline = String(value);
1885
+ if (inline.length <= INLINE_LIMIT && !inline.includes("\n")) {
1886
+ scalars[key] = value;
1887
+ } else {
1888
+ blocks.push(`${key}:
1889
+ ${inline}`);
1890
+ }
1891
+ continue;
1892
+ }
1893
+ if (Array.isArray(value)) {
1894
+ if (value.every((item) => typeof item === "string")) {
1895
+ blocks.push(`${key}:
1896
+ ${renderStringList(value)}`);
1897
+ } else {
1898
+ blocks.push(`${key}:
1899
+ ${renderUnknownList(value)}`);
1900
+ }
1901
+ continue;
1902
+ }
1903
+ blocks.push(`${key}: ${clipInline(oneLineJson(value))}`);
1252
1904
  }
1253
- return { ...p, models };
1905
+ return joinSections([renderHeader(toolName, scalars), ...blocks]);
1254
1906
  }
1255
- function stripUndefined(obj) {
1256
- const out = {};
1257
- for (const [k, v] of Object.entries(obj)) {
1258
- if (v !== void 0) out[k] = v;
1907
+ function renderHeader(label, fields) {
1908
+ const parts = Object.entries(fields).filter(([, value]) => value !== void 0 && value !== null && value !== "").map(([key, value]) => `${key}=${clipInline(formatInlineValue(value))}`);
1909
+ return parts.length > 0 ? `${label} (${parts.join(" ")})` : label;
1910
+ }
1911
+ function renderStringList(items, empty = "", limit = DEFAULT_LIST_LIMIT) {
1912
+ if (items.length === 0) return empty;
1913
+ const shown = items.slice(0, limit);
1914
+ const omitted = items.length - shown.length;
1915
+ return [
1916
+ ...shown,
1917
+ ...omitted > 0 ? [`[serializer omitted ${omitted} item(s); narrow the request for more]`] : []
1918
+ ].join("\n");
1919
+ }
1920
+ function renderUnknownList(items, limit = DEFAULT_LIST_LIMIT) {
1921
+ const shown = items.slice(0, limit).map((item) => clipInline(oneLineJson(item), 1e3));
1922
+ const omitted = items.length - shown.length;
1923
+ if (omitted > 0)
1924
+ shown.push(`[serializer omitted ${omitted} item(s); narrow the request for more]`);
1925
+ return shown.join("\n");
1926
+ }
1927
+ function joinSections(sections) {
1928
+ return sections.map((section) => typeof section === "string" ? section.trimEnd() : void 0).filter((section) => !!section).join("\n");
1929
+ }
1930
+ function formatInlineValue(value) {
1931
+ if (Array.isArray(value)) return `[${value.map(formatInlineValue).join(",")}]`;
1932
+ if (isScalar(value)) return String(value);
1933
+ return oneLineJson(value);
1934
+ }
1935
+ function clipInline(value, max = INLINE_LIMIT) {
1936
+ const compact = value.replace(/\s+/g, " ").trim();
1937
+ return compact.length <= max ? compact : `${compact.slice(0, max - 15)}...(${compact.length} chars)`;
1938
+ }
1939
+ function oneLineJson(value) {
1940
+ try {
1941
+ return JSON.stringify(value);
1942
+ } catch {
1943
+ return String(value);
1259
1944
  }
1260
- return out;
1945
+ }
1946
+ function stringField(obj, key) {
1947
+ const value = obj[key];
1948
+ return typeof value === "string" ? value : void 0;
1949
+ }
1950
+ function numberField(obj, key) {
1951
+ const value = obj[key];
1952
+ return typeof value === "number" ? value : void 0;
1953
+ }
1954
+ function stringArrayField(obj, key) {
1955
+ const value = obj[key];
1956
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1957
+ }
1958
+ function stringFromInput(input, key) {
1959
+ if (!isRecord2(input)) return void 0;
1960
+ const value = input[key];
1961
+ return typeof value === "string" ? value : void 0;
1962
+ }
1963
+ function numberFromInput(input, key) {
1964
+ if (!isRecord2(input)) return void 0;
1965
+ const value = input[key];
1966
+ return typeof value === "number" ? value : void 0;
1967
+ }
1968
+ function inputListSummary(input, key) {
1969
+ if (!isRecord2(input)) return void 0;
1970
+ const value = input[key];
1971
+ if (typeof value === "string") return value;
1972
+ if (Array.isArray(value)) return value.filter((item) => typeof item === "string").join(",");
1973
+ return void 0;
1974
+ }
1975
+ function isRecord2(value) {
1976
+ return !!value && typeof value === "object" && !Array.isArray(value);
1977
+ }
1978
+ function isScalar(value) {
1979
+ return value === null || ["string", "number", "boolean"].includes(typeof value);
1980
+ }
1981
+ function projectHash(absRoot) {
1982
+ return createHash("sha256").update(path4.resolve(absRoot)).digest("hex").slice(0, 12);
1983
+ }
1984
+ function projectSlug(absRoot) {
1985
+ const base = slugify(path4.basename(absRoot));
1986
+ const hash = createHash("sha256").update(path4.resolve(absRoot)).digest("hex").slice(0, 6);
1987
+ return `${base}-${hash}`;
1988
+ }
1989
+ function slugify(name) {
1990
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
1991
+ }
1992
+ function wstackGlobalRoot() {
1993
+ const fromEnv = process.env["WRONGSTACK_HOME"];
1994
+ if (fromEnv && fromEnv.trim().length > 0) return path4.resolve(fromEnv);
1995
+ return path4.join(os.homedir(), ".wrongstack");
1996
+ }
1997
+ function resolveWstackPaths(opts) {
1998
+ const globalRoot = opts.globalRoot ?? (opts.userHome ? path4.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
1999
+ const hash = projectHash(opts.projectRoot);
2000
+ const slug = projectSlug(opts.projectRoot);
2001
+ const projectDir = path4.join(globalRoot, "projects", slug);
2002
+ return {
2003
+ globalRoot,
2004
+ configDir: globalRoot,
2005
+ globalConfig: path4.join(globalRoot, "config.json"),
2006
+ secretsKey: path4.join(globalRoot, ".key"),
2007
+ globalMemory: path4.join(globalRoot, "memory.md"),
2008
+ globalSkills: path4.join(globalRoot, "skills"),
2009
+ globalPrompts: path4.join(globalRoot, "prompts"),
2010
+ cacheDir: path4.join(globalRoot, "cache"),
2011
+ modelsCache: path4.join(globalRoot, "cache", "models.dev.json"),
2012
+ modelsOverlayCache: path4.join(globalRoot, "cache", "models-overlay.json"),
2013
+ historyFile: path4.join(globalRoot, "history"),
2014
+ logFile: path4.join(globalRoot, "logs", "wrongstack.log"),
2015
+ projectDir,
2016
+ projectCodebaseIndex: path4.join(projectDir, "codebase-index"),
2017
+ projectMemory: path4.join(projectDir, "memory.md"),
2018
+ projectSessions: path4.join(projectDir, "sessions"),
2019
+ projectTrust: path4.join(projectDir, "trust.json"),
2020
+ projectMeta: path4.join(projectDir, "meta.json"),
2021
+ projectLocalConfig: path4.join(projectDir, "config.local.json"),
2022
+ inProjectConfig: path4.join(opts.projectRoot, ".wrongstack", "config.json"),
2023
+ inProjectAgentsFile: path4.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
2024
+ inProjectSkills: path4.join(opts.projectRoot, ".wrongstack", "skills"),
2025
+ inProjectWorktrees: path4.join(opts.projectRoot, ".wrongstack", "worktrees"),
2026
+ projectHash: hash,
2027
+ projectSlug: slug,
2028
+ projectGoal: path4.join(projectDir, "goal.json"),
2029
+ projectSpecs: path4.join(projectDir, "specs"),
2030
+ projectTaskGraphs: path4.join(projectDir, "task-graphs"),
2031
+ projectSddSession: path4.join(projectDir, "sdd-session.json"),
2032
+ projectPlan: path4.join(projectDir, "plan.json"),
2033
+ projectAutophase: path4.join(projectDir, "autophase"),
2034
+ syncConfig: path4.join(globalRoot, "sync.json"),
2035
+ projectStatus: (projectHash2) => path4.join(globalRoot, "projects", projectHash2, "status.json")
2036
+ };
1261
2037
  }
1262
2038
 
1263
2039
  // src/storage/session-store.ts
@@ -1339,11 +2115,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
1339
2115
  }
1340
2116
  /** Absolute path to the session index file. */
1341
2117
  get indexFile() {
1342
- return path3.join(this.dir, "_index.jsonl");
2118
+ return path4.join(this.dir, "_index.jsonl");
1343
2119
  }
1344
2120
  /** Join session ID to its absolute path within the store directory. */
1345
2121
  sessionPath(id, ext) {
1346
- return path3.join(this.dir, `${id}${ext}`);
2122
+ return path4.join(this.dir, `${id}${ext}`);
1347
2123
  }
1348
2124
  /**
1349
2125
  * Ensure the directory implied by the session ID exists. When the ID
@@ -1351,7 +2127,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
1351
2127
  * subdirectory so sessions group naturally by day.
1352
2128
  */
1353
2129
  async ensureShardDir(id) {
1354
- const dirPath = path3.dirname(path3.join(this.dir, id));
2130
+ const dirPath = path4.dirname(path4.join(this.dir, id));
1355
2131
  await ensureDir(dirPath);
1356
2132
  return dirPath;
1357
2133
  }
@@ -1359,7 +2135,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
1359
2135
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1360
2136
  const id = meta.id && meta.id.length > 0 ? meta.id : generateSessionId(startedAt, meta.model ?? meta.provider);
1361
2137
  const shardDir = await this.ensureShardDir(id);
1362
- const file = path3.join(shardDir, `${path3.basename(id)}.jsonl`);
2138
+ const file = path4.join(shardDir, `${path4.basename(id)}.jsonl`);
1363
2139
  const t0 = Date.now();
1364
2140
  let handle;
1365
2141
  try {
@@ -1421,7 +2197,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
1421
2197
  // Shard directory (sessions/<date>/) — must match create() so the
1422
2198
  // .summary.json sidecar lands next to the JSONL instead of the
1423
2199
  // sessions root (where summaryFor() would never find it).
1424
- dir: path3.dirname(file),
2200
+ dir: path4.dirname(file),
1425
2201
  filePath: file,
1426
2202
  secretScrubber: this.secretScrubber,
1427
2203
  onClose: (s) => this.appendToIndex(s)
@@ -1457,6 +2233,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
1457
2233
  const cached = this._loadCache.get(id);
1458
2234
  if (cached && cached.mtimeMs === stat6.mtimeMs && cached.size === stat6.size) {
1459
2235
  cacheHit = true;
2236
+ this._loadCache.delete(id);
2237
+ this._loadCache.set(id, cached);
1460
2238
  return cached.data;
1461
2239
  }
1462
2240
  const raw = await fsp2.readFile(file, "utf8");
@@ -1645,7 +2423,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
1645
2423
  continue;
1646
2424
  if (entry.isDirectory()) {
1647
2425
  const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
1648
- ids.push(...await this.collectSessionIds(path3.join(dir, entry.name), childPrefix, depth + 1));
2426
+ ids.push(...await this.collectSessionIds(path4.join(dir, entry.name), childPrefix, depth + 1));
1649
2427
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
1650
2428
  if (entry.name === "_index.jsonl") continue;
1651
2429
  const base = entry.name.replace(/\.jsonl$/, "");
@@ -1696,14 +2474,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
1696
2474
  async deleteSession(id) {
1697
2475
  const jsonlPath = this.sessionPath(id, ".jsonl");
1698
2476
  const summaryPath = this.sessionPath(id, ".summary.json");
1699
- const shardDir = path3.dirname(path3.join(this.dir, id));
1700
- const base = path3.basename(id);
1701
- const sessDir = path3.join(shardDir, base);
2477
+ const shardDir = path4.dirname(path4.join(this.dir, id));
2478
+ const base = path4.basename(id);
2479
+ const sessDir = path4.join(shardDir, base);
1702
2480
  const deletions = [
1703
2481
  fsp2.unlink(jsonlPath),
1704
2482
  fsp2.unlink(summaryPath),
1705
- fsp2.unlink(path3.join(shardDir, `${base}.plan.json`)),
1706
- fsp2.unlink(path3.join(shardDir, `${base}.todos.json`))
2483
+ fsp2.unlink(path4.join(shardDir, `${base}.plan.json`)),
2484
+ fsp2.unlink(path4.join(shardDir, `${base}.todos.json`))
1707
2485
  ];
1708
2486
  const results = await Promise.allSettled(deletions);
1709
2487
  for (const r of results) {
@@ -1739,14 +2517,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
1739
2517
  let deleted = 0;
1740
2518
  let activeSessionId = null;
1741
2519
  try {
1742
- const raw = await fsp2.readFile(path3.join(this.dir, "active.json"), "utf8");
2520
+ const raw = await fsp2.readFile(path4.join(this.dir, "active.json"), "utf8");
1743
2521
  const active = JSON.parse(raw);
1744
2522
  activeSessionId = active.sessionId ?? null;
1745
2523
  } catch {
1746
2524
  }
1747
2525
  const isPrunableJsonl = (name) => name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
1748
2526
  const pruneFile = async (dir, name, prefix) => {
1749
- const jsonlPath = path3.join(dir, name);
2527
+ const jsonlPath = path4.join(dir, name);
1750
2528
  try {
1751
2529
  const stat6 = await fsp2.stat(jsonlPath);
1752
2530
  if (stat6.mtimeMs >= cutoff) return;
@@ -1766,7 +2544,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
1766
2544
  continue;
1767
2545
  }
1768
2546
  if (!entry.isDirectory()) continue;
1769
- const dateDir = path3.join(this.dir, entry.name);
2547
+ const dateDir = path4.join(this.dir, entry.name);
1770
2548
  const files = await fsp2.readdir(dateDir, { withFileTypes: true }).catch(() => []);
1771
2549
  for (const file of files) {
1772
2550
  if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
@@ -1778,7 +2556,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
1778
2556
  }
1779
2557
  for (const entry of entries) {
1780
2558
  if (!entry.isDirectory()) continue;
1781
- const dateDir = path3.join(this.dir, entry.name);
2559
+ const dateDir = path4.join(this.dir, entry.name);
1782
2560
  try {
1783
2561
  const remaining = await fsp2.readdir(dateDir);
1784
2562
  if (remaining.length === 0) {
@@ -1953,7 +2731,7 @@ var FileSessionWriter = class _FileSessionWriter {
1953
2731
  this.meta = meta;
1954
2732
  this.events = events;
1955
2733
  this.resumed = opts.resumed ?? false;
1956
- this.manifestFile = opts.dir ? path3.join(opts.dir, `${path3.basename(id)}.summary.json`) : "";
2734
+ this.manifestFile = opts.dir ? path4.join(opts.dir, `${path4.basename(id)}.summary.json`) : "";
1957
2735
  this.filePath = opts.filePath ?? "";
1958
2736
  this.secretScrubber = opts.secretScrubber;
1959
2737
  this.onCloseCb = opts.onClose;
@@ -2457,7 +3235,7 @@ var QueueStore = class {
2457
3235
  events;
2458
3236
  traceId;
2459
3237
  constructor(opts) {
2460
- this.file = path3.join(opts.dir, "queue.json");
3238
+ this.file = path4.join(opts.dir, "queue.json");
2461
3239
  this.events = opts.events;
2462
3240
  this.traceId = opts.traceId;
2463
3241
  }
@@ -2644,7 +3422,7 @@ var DefaultAttachmentStore = class {
2644
3422
  let data = input.data;
2645
3423
  if (this.spoolDir && bytes >= this.spoolThreshold) {
2646
3424
  await fsp2.mkdir(this.spoolDir, { recursive: true });
2647
- spooledPath = path3.join(this.spoolDir, `${id}.bin`);
3425
+ spooledPath = path4.join(this.spoolDir, `${id}.bin`);
2648
3426
  await atomicWrite(spooledPath, input.data, {
2649
3427
  encoding: input.kind === "image" ? "base64" : "utf8"
2650
3428
  });
@@ -2856,7 +3634,7 @@ var FileMemoryBackend = class {
2856
3634
  }
2857
3635
  async remember(scope, entry, filePath) {
2858
3636
  const file = this.resolveFile(filePath, scope);
2859
- await ensureDir(path3.dirname(file));
3637
+ await ensureDir(path4.dirname(file));
2860
3638
  let existing = "";
2861
3639
  try {
2862
3640
  existing = await fsp2.readFile(file, "utf8");
@@ -3472,9 +4250,9 @@ ${body.trim()}`);
3472
4250
  if (!this.persistBackup || scope === "project-agents") return;
3473
4251
  try {
3474
4252
  const content = await this.backend.readAll(scope, this.files[scope]);
3475
- const { writeFile: writeFile4, mkdir: mkdir6 } = await import('fs/promises');
3476
- await mkdir6(this.backupDir, { recursive: true });
3477
- await writeFile4(`${this.backupDir}/${scope}.md`, content, "utf8");
4253
+ const { writeFile: writeFile5, mkdir: mkdir7 } = await import('fs/promises');
4254
+ await mkdir7(this.backupDir, { recursive: true });
4255
+ await writeFile5(`${this.backupDir}/${scope}.md`, content, "utf8");
3478
4256
  } catch {
3479
4257
  }
3480
4258
  }
@@ -3824,7 +4602,7 @@ var DefaultSecretVault = class {
3824
4602
  KEY_FILE_MAGIC.copy(keyFileBuf, 0);
3825
4603
  keyFileBuf[KEY_FILE_MAGIC.length] = newVersion;
3826
4604
  newKey.copy(keyFileBuf, KEY_FILE_MAGIC.length + 1);
3827
- fs4.mkdirSync(path3.dirname(this.keyFile), { recursive: true });
4605
+ fs4.mkdirSync(path4.dirname(this.keyFile), { recursive: true });
3828
4606
  fs4.writeFileSync(this.keyFile, keyFileBuf, { mode: 384 });
3829
4607
  checkKeyFilePermissions(this.keyFile);
3830
4608
  this.key = newKey;
@@ -3872,7 +4650,7 @@ var DefaultSecretVault = class {
3872
4650
  } catch (err) {
3873
4651
  if (err.code !== "ENOENT") throw err;
3874
4652
  }
3875
- fs4.mkdirSync(path3.dirname(this.keyFile), { recursive: true });
4653
+ fs4.mkdirSync(path4.dirname(this.keyFile), { recursive: true });
3876
4654
  const key = randomBytes(KEY_BYTES);
3877
4655
  try {
3878
4656
  fs4.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
@@ -3962,7 +4740,7 @@ async function rewriteConfigEncrypted(configPath, vault, patch) {
3962
4740
  }
3963
4741
  const merged = deepMerge(current, patch ?? {});
3964
4742
  const encrypted = encryptConfigSecrets(merged, vault);
3965
- await fsp2.mkdir(path3.dirname(configPath), { recursive: true });
4743
+ await fsp2.mkdir(path4.dirname(configPath), { recursive: true });
3966
4744
  await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
3967
4745
  await restrictFilePermissions(configPath);
3968
4746
  }
@@ -4159,6 +4937,7 @@ var BEHAVIOR_DEFAULTS = {
4159
4937
  modelsRegistry: true,
4160
4938
  skills: true
4161
4939
  },
4940
+ mcpServers: {},
4162
4941
  indexing: {
4163
4942
  onSessionStart: true,
4164
4943
  onEdit: true,
@@ -4592,7 +5371,7 @@ var RecoveryLock = class {
4592
5371
  sessionStore;
4593
5372
  probe;
4594
5373
  constructor(opts) {
4595
- this.file = path3.join(opts.dir, LOCK_FILE);
5374
+ this.file = path4.join(opts.dir, LOCK_FILE);
4596
5375
  this.pid = opts.pid ?? process.pid;
4597
5376
  this.hostname = opts.hostname ?? os.hostname();
4598
5377
  this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
@@ -4653,7 +5432,7 @@ var RecoveryLock = class {
4653
5432
  * null return before calling this.
4654
5433
  */
4655
5434
  async write(sessionId) {
4656
- await ensureDir(path3.dirname(this.file));
5435
+ await ensureDir(path4.dirname(this.file));
4657
5436
  const lock = {
4658
5437
  v: 1,
4659
5438
  sessionId,
@@ -6073,9 +6852,9 @@ function getInputString(input, key) {
6073
6852
  function pathLooksInsideProject(rawPath, projectRoot) {
6074
6853
  if (!projectRoot) return false;
6075
6854
  if (rawPath === "~" || rawPath.startsWith("~/") || rawPath.startsWith("~\\")) return false;
6076
- const resolved = path3.resolve(projectRoot, rawPath);
6077
- const relative2 = path3.relative(projectRoot, resolved);
6078
- return !!relative2 && !relative2.startsWith("..") && !path3.isAbsolute(relative2);
6855
+ const resolved = path4.resolve(projectRoot, rawPath);
6856
+ const relative3 = path4.relative(projectRoot, resolved);
6857
+ return !!relative3 && !relative3.startsWith("..") && !path4.isAbsolute(relative3);
6079
6858
  }
6080
6859
  function tokenizeShell(command) {
6081
6860
  return command.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
@@ -6085,7 +6864,7 @@ function pathTokenIsOutsideProject(token, projectRoot) {
6085
6864
  if (token === "/" || token === "~" || token === "." || token === "..") return token !== ".";
6086
6865
  if (token.includes("*")) return true;
6087
6866
  if (token.startsWith("..") || token.includes("../") || token.includes("..\\")) return true;
6088
- if (path3.isAbsolute(token) || token.startsWith("~/")) return !pathLooksInsideProject(token, projectRoot);
6867
+ if (path4.isAbsolute(token) || token.startsWith("~/")) return !pathLooksInsideProject(token, projectRoot);
6089
6868
  return false;
6090
6869
  }
6091
6870
  function hasDangerousDeleteTarget(tokens, start, projectRoot) {
@@ -6250,7 +7029,7 @@ var DefaultPermissionPolicy = class {
6250
7029
  if (!this.loaded) await this.reload();
6251
7030
  const namespaceEntry = this.findNamespaceEntry(tool.name);
6252
7031
  const entry = this.policy[tool.name] ?? namespaceEntry;
6253
- const subject = this.subjectFor(tool.name, input, tool.subjectKey);
7032
+ const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
6254
7033
  const cacheKey = `${tool.name}::${subject ?? tool.name}`;
6255
7034
  if (tool.name !== "write") {
6256
7035
  const cached = this._evalCache.get(cacheKey);
@@ -6432,32 +7211,6 @@ var DefaultPermissionPolicy = class {
6432
7211
  this.sessionAllowed.set(`${rule.tool}::${rule.pattern}`, true);
6433
7212
  this._evalCache.clear();
6434
7213
  }
6435
- subjectFor(toolName, input, subjectKey) {
6436
- if (!input || typeof input !== "object") return void 0;
6437
- const obj = input;
6438
- const globChars = /[*?[\]]/g;
6439
- const escapeGlob = (s) => s.replace(globChars, (c) => `\\${c}`);
6440
- const normalizePath = (s) => escapeGlob(s.replace(/\\/g, "/"));
6441
- if (subjectKey) {
6442
- const v = obj[subjectKey];
6443
- if (typeof v === "string") {
6444
- return subjectKey === "path" || subjectKey === "file" || subjectKey === "files" ? normalizePath(v) : escapeGlob(v);
6445
- }
6446
- }
6447
- if (toolName === "bash" && typeof obj.command === "string") {
6448
- return escapeGlob(obj.command);
6449
- }
6450
- if (typeof obj.path === "string") {
6451
- return normalizePath(obj.path);
6452
- }
6453
- if (typeof obj.url === "string") {
6454
- return escapeGlob(obj.url);
6455
- }
6456
- if (typeof obj.name === "string") {
6457
- return escapeGlob(obj.name);
6458
- }
6459
- return void 0;
6460
- }
6461
7214
  findNamespaceEntry(toolName) {
6462
7215
  for (const { pattern, value } of this.wildcardEntries) {
6463
7216
  if (matchGlob(pattern, toolName)) return value;
@@ -6480,9 +7233,13 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
6480
7233
  const caps = tool.capabilities ?? [];
6481
7234
  const hasAllowedCap = caps.some((c) => this.allowedCapabilities.includes(c));
6482
7235
  const isMcp = _AutoApprovePermissionPolicy.isMcpTool(tool.name);
6483
- const blocked = tool.permission === "deny" || isMcp || !hasAllowedCap;
7236
+ const mcpProxyAllowed = this.allowedCapabilities.includes(ToolCapabilities.MCP_PROXY);
7237
+ const dangerousNotAllowed = getDangerousCapabilities(tool).filter(
7238
+ (c) => !this.allowedCapabilities.includes(c)
7239
+ );
7240
+ const blocked = tool.permission === "deny" || isMcp && !mcpProxyAllowed || !hasAllowedCap || dangerousNotAllowed.length > 0;
6484
7241
  if (blocked) {
6485
- const reason = isMcp ? `MCP tool ${tool.name} is not auto-approved for subagents \u2014 ask the leader to allow it explicitly` : tool.permission === "deny" ? "tool default deny" : `tool lacks allowed capability (has: ${caps.join(", ") || "none"}, allowed: ${this.allowedCapabilities.join(", ")})`;
7242
+ const reason = isMcp && !mcpProxyAllowed ? `MCP tool ${tool.name} is not auto-approved for subagents \u2014 ask the leader to allow mcp.proxy explicitly` : tool.permission === "deny" ? "tool default deny" : dangerousNotAllowed.length > 0 ? `tool requires un-granted dangerous capability (needs: ${dangerousNotAllowed.join(", ")}, allowed: ${this.allowedCapabilities.join(", ")})` : `tool lacks allowed capability (has: ${caps.join(", ") || "none"}, allowed: ${this.allowedCapabilities.join(", ")})`;
6486
7243
  return {
6487
7244
  permission: "deny",
6488
7245
  source: "subagent_guard",
@@ -6742,7 +7499,7 @@ var DefaultSkillLoader = class {
6742
7499
  const entries = await fsp2.readdir(dir, { withFileTypes: true });
6743
7500
  for (const e of entries) {
6744
7501
  if (!e.isDirectory()) continue;
6745
- const skillFile = path3.join(dir, e.name, "SKILL.md");
7502
+ const skillFile = path4.join(dir, e.name, "SKILL.md");
6746
7503
  try {
6747
7504
  const raw = await fsp2.readFile(skillFile, "utf8");
6748
7505
  const meta = parseFrontmatter(raw);
@@ -6813,7 +7570,7 @@ var DefaultSkillLoader = class {
6813
7570
  if (cached !== void 0) return cached;
6814
7571
  const m = await this.find(name);
6815
7572
  if (!m) throw new Error(`Skill "${name}" not found`);
6816
- const savePath = path3.join(path3.dirname(m.path), "SKILL.save.md");
7573
+ const savePath = path4.join(path4.dirname(m.path), "SKILL.save.md");
6817
7574
  let result;
6818
7575
  try {
6819
7576
  result = await fsp2.readFile(savePath, "utf8");
@@ -7127,8 +7884,8 @@ async function streamProviderToResponse(provider, req, signal, ctx, events, logg
7127
7884
  });
7128
7885
  await Promise.race([
7129
7886
  drainPromise,
7130
- new Promise((resolve5) => {
7131
- drainTimer = setTimeout(resolve5, STREAM_DRAIN_TIMEOUT_MS);
7887
+ new Promise((resolve6) => {
7888
+ drainTimer = setTimeout(resolve6, STREAM_DRAIN_TIMEOUT_MS);
7132
7889
  })
7133
7890
  ]);
7134
7891
  } finally {
@@ -7234,26 +7991,29 @@ async function runProviderWithRetry(opts) {
7234
7991
  description
7235
7992
  });
7236
7993
  }
7237
- await new Promise((resolve5, reject) => {
7994
+ await new Promise((resolve6, reject) => {
7238
7995
  let settled = false;
7996
+ const cleanup = () => {
7997
+ clearTimeout(t);
7998
+ signal.removeEventListener("abort", onAbort);
7999
+ };
7239
8000
  const onAbort = () => {
7240
8001
  if (settled) return;
7241
8002
  settled = true;
7242
- clearTimeout(t);
8003
+ cleanup();
7243
8004
  reject(new Error("aborted"));
7244
8005
  };
7245
8006
  const t = setTimeout(() => {
7246
8007
  if (settled) return;
7247
8008
  settled = true;
7248
- clearTimeout(t);
7249
- signal.removeEventListener("abort", onAbort);
7250
- resolve5();
8009
+ cleanup();
8010
+ resolve6();
7251
8011
  }, delay);
7252
8012
  if (signal.aborted) {
7253
8013
  onAbort();
7254
8014
  return;
7255
8015
  }
7256
- signal.addEventListener("abort", onAbort, { once: true });
8016
+ signal.addEventListener("abort", onAbort);
7257
8017
  });
7258
8018
  attempt++;
7259
8019
  }
@@ -7312,25 +8072,26 @@ function findPreserveStart(messages, preserveK) {
7312
8072
  preserveStart = i;
7313
8073
  }
7314
8074
  }
7315
- let forwardWalkIterations = 0;
7316
- let forwardWalkInnerIterations = 0;
7317
- for (let i = preserveStart; i < messages.length; i++) {
7318
- forwardWalkIterations++;
7319
- const m = messages[i];
7320
- if (!m || typeof m.content === "string" || !Array.isArray(m.content)) continue;
7321
- const hasToolUse3 = m.content.some((b) => {
7322
- forwardWalkInnerIterations++;
7323
- return b.type === "tool_use";
8075
+ let pairRepairIterations = 0;
8076
+ let pairRepairInnerIterations = 0;
8077
+ while (preserveStart > 0) {
8078
+ pairRepairIterations++;
8079
+ const first = messages[preserveStart];
8080
+ const prev = messages[preserveStart - 1];
8081
+ if (!first || !prev || first.role !== "user" || prev.role !== "assistant") break;
8082
+ if (typeof first.content === "string" || typeof prev.content === "string") break;
8083
+ const resultIds = /* @__PURE__ */ new Set();
8084
+ for (const block of first.content) {
8085
+ pairRepairInnerIterations++;
8086
+ if (block.type === "tool_result") resultIds.add(block.tool_use_id);
8087
+ }
8088
+ if (resultIds.size === 0) break;
8089
+ const hasMatchingUse = prev.content.some((block) => {
8090
+ pairRepairInnerIterations++;
8091
+ return block.type === "tool_use" && resultIds.has(block.id);
7324
8092
  });
7325
- if (hasToolUse3 && i + 1 < messages.length) {
7326
- const next = messages[i + 1];
7327
- if (next && next.role === "user" && typeof next.content !== "string" && Array.isArray(next.content) && next.content.some((b) => {
7328
- forwardWalkInnerIterations++;
7329
- return b.type === "tool_result";
7330
- })) {
7331
- preserveStart = i + 1;
7332
- }
7333
- }
8093
+ if (!hasMatchingUse) break;
8094
+ preserveStart--;
7334
8095
  }
7335
8096
  if (compactionDebugEnabled()) {
7336
8097
  console.log(
@@ -7340,9 +8101,9 @@ function findPreserveStart(messages, preserveK) {
7340
8101
  messageCount: messages.length,
7341
8102
  preserveK,
7342
8103
  preserveStart,
7343
- forwardWalkIterations,
7344
- forwardWalkInnerIterations,
7345
- forwardWalkInnerPerOuter: forwardWalkIterations > 0 ? forwardWalkInnerIterations / forwardWalkIterations : 0
8104
+ pairRepairIterations,
8105
+ pairRepairInnerIterations,
8106
+ pairRepairInnerPerOuter: pairRepairIterations > 0 ? pairRepairInnerIterations / pairRepairIterations : 0
7346
8107
  })
7347
8108
  );
7348
8109
  }
@@ -7359,7 +8120,8 @@ function eliseOldToolResults(messages, opts) {
7359
8120
  if (!msg || !Array.isArray(msg.content)) continue;
7360
8121
  for (const b of msg.content) {
7361
8122
  fastPathInnerIterations++;
7362
- if (b.type === "tool_result" && estimateToolResultTokens(b.content) >= opts.eliseThreshold) {
8123
+ const oversized = b.type === "tool_result" && estimateToolResultTokens(b.content) >= opts.eliseThreshold || b.type === "tool_use" && estimateToolInputTokens(b.input) >= opts.eliseThreshold;
8124
+ if (oversized) {
7363
8125
  hasOversized = true;
7364
8126
  break;
7365
8127
  }
@@ -7393,6 +8155,13 @@ function eliseOldToolResults(messages, opts) {
7393
8155
  }
7394
8156
  const original = msg.content;
7395
8157
  const newContent = original.map((b) => {
8158
+ if (b.type === "tool_use") {
8159
+ const tokens2 = estimateToolInputTokens(b.input);
8160
+ if (tokens2 < opts.eliseThreshold) return b;
8161
+ const elidedInput = summarizeToolUseInputElision(b, tokens2);
8162
+ saved += Math.max(0, tokens2 - estimateToolInputTokens(elidedInput));
8163
+ return { ...b, input: elidedInput };
8164
+ }
7396
8165
  if (b.type !== "tool_result") return b;
7397
8166
  const tokens = estimateToolResultTokens(b.content);
7398
8167
  if (tokens < opts.eliseThreshold) return b;
@@ -7400,7 +8169,7 @@ function eliseOldToolResults(messages, opts) {
7400
8169
  const elided = {
7401
8170
  type: "tool_result",
7402
8171
  tool_use_id: b.tool_use_id,
7403
- content: `[elided: ~${tokens} tokens]`,
8172
+ content: summarizeToolResultElision(b, tokens),
7404
8173
  is_error: b.is_error
7405
8174
  };
7406
8175
  return elided;
@@ -7440,6 +8209,65 @@ function eliseOldToolResults(messages, opts) {
7440
8209
  });
7441
8210
  return { messages: changed ? next : messages, saved, changed };
7442
8211
  }
8212
+ function summarizeToolUseInputElision(block, tokens) {
8213
+ const fields = {};
8214
+ for (const [key, value] of Object.entries(block.input ?? {})) {
8215
+ fields[key] = summarizeToolUseInputValue(value);
8216
+ }
8217
+ return {
8218
+ __elided_tool_input: `~${tokens} tokens; original arguments are in the session log`,
8219
+ tool: block.name,
8220
+ fields
8221
+ };
8222
+ }
8223
+ function summarizeToolUseInputValue(value) {
8224
+ if (value === null || value === void 0) return value;
8225
+ if (typeof value === "number" || typeof value === "boolean") return value;
8226
+ if (typeof value === "string") {
8227
+ const oneLine = value.replace(/\s+/g, " ").trim();
8228
+ return oneLine.length <= 160 ? oneLine : `${oneLine.slice(0, 120)}...(${oneLine.length} chars)`;
8229
+ }
8230
+ if (Array.isArray(value)) {
8231
+ return `[array:${value.length}]`;
8232
+ }
8233
+ if (typeof value === "object") {
8234
+ const keys = Object.keys(value);
8235
+ return `[object:${keys.slice(0, 8).join(",")}${keys.length > 8 ? ",..." : ""}]`;
8236
+ }
8237
+ return String(value);
8238
+ }
8239
+ function summarizeToolResultElision(block, tokens) {
8240
+ const parts = [`elided: ~${tokens} tokens`];
8241
+ if (block.name) parts.push(`tool=${block.name}`);
8242
+ const files = extractPathHints(block.content).slice(0, 5);
8243
+ if (files.length > 0) parts.push(`files=${files.join(", ")}`);
8244
+ const error = firstErrorLine(block.content);
8245
+ if (error) parts.push(`error=${error}`);
8246
+ return `[${parts.join("; ")}]`;
8247
+ }
8248
+ function extractPathHints(content) {
8249
+ const text = typeof content === "string" ? content : JSON.stringify(content);
8250
+ const out = /* @__PURE__ */ new Set();
8251
+ const re = /(?:(?:[A-Za-z]:)?[./\\]?[\w@.-]+(?:[\\/][\w@(). -]+)+\.[A-Za-z0-9]{1,12})/g;
8252
+ for (const match of text.matchAll(re)) {
8253
+ const clean = match[0]?.replace(/\\/g, "/").replace(/^["'`]+|["'`),;:]+$/g, "");
8254
+ if (clean && clean.length <= 220) out.add(clean);
8255
+ if (out.size >= 5) break;
8256
+ }
8257
+ return [...out];
8258
+ }
8259
+ function firstErrorLine(content) {
8260
+ const text = typeof content === "string" ? content : JSON.stringify(content);
8261
+ for (const line of text.split(/\r?\n/)) {
8262
+ if (!/\b(error|exception|failed|failure|fatal|panic|timeout|denied|enoent|eacces|eperm)\b/i.test(
8263
+ line
8264
+ ))
8265
+ continue;
8266
+ const trimmed = line.replace(/\s+/g, " ").trim();
8267
+ if (trimmed) return trimmed.slice(0, 180);
8268
+ }
8269
+ return void 0;
8270
+ }
7443
8271
  function buildLosslessDigest(messages) {
7444
8272
  const lines = [];
7445
8273
  for (const m of messages) {
@@ -7550,15 +8378,15 @@ function buildSmartDigest(messages) {
7550
8378
  lines.push(`[${m.role}]: ${display}${marker}`);
7551
8379
  }
7552
8380
  if (noiseCount > 0) {
7553
- lines.push(`[system]: ${noiseCount} low-importance turn(s) collapsed (repeated failures / pure tool I/O)`);
8381
+ lines.push(
8382
+ `[system]: ${noiseCount} low-importance turn(s) collapsed (repeated failures / pure tool I/O)`
8383
+ );
7554
8384
  }
7555
8385
  return lines.join("\n");
7556
8386
  }
7557
8387
  function countToolBlocks(m) {
7558
8388
  if (typeof m.content === "string") return 0;
7559
- return m.content.filter(
7560
- (b) => b.type === "tool_use" || b.type === "tool_result"
7561
- ).length;
8389
+ return m.content.filter((b) => b.type === "tool_use" || b.type === "tool_result").length;
7562
8390
  }
7563
8391
  function firstSentence(text) {
7564
8392
  const trimmed = text.trim();
@@ -7627,10 +8455,12 @@ var HybridCompactor = class {
7627
8455
  if (elide.changed) ctx.state.replaceMessages(elide.messages);
7628
8456
  if (elide.saved > 0) reductions.push({ phase: "elision", saved: elide.saved });
7629
8457
  let collapsedDigest;
8458
+ let evidenceDigest;
7630
8459
  if (opts.aggressive) {
7631
8460
  const phase2 = this.collapseAncientTurns(ctx, preserveK);
7632
8461
  if (phase2.saved > 0) reductions.push({ phase: "summary", saved: phase2.saved });
7633
8462
  collapsedDigest = phase2.digest;
8463
+ evidenceDigest = phase2.evidenceDigest;
7634
8464
  }
7635
8465
  const repaired = repairToolUseAdjacency(ctx.messages);
7636
8466
  if (repaired.report.changed) {
@@ -7638,6 +8468,11 @@ var HybridCompactor = class {
7638
8468
  }
7639
8469
  const afterTokens = estimateMessages(ctx.messages);
7640
8470
  const afterFull = this.estimateFullRequest(ctx);
8471
+ const quality = checkCompactionQuality(ctx, {
8472
+ collapsedDigest,
8473
+ evidenceDigest,
8474
+ reduced: beforeTokens > afterTokens || beforeFull > afterFull
8475
+ });
7641
8476
  return {
7642
8477
  before: beforeTokens,
7643
8478
  after: afterTokens,
@@ -7645,6 +8480,8 @@ var HybridCompactor = class {
7645
8480
  fullRequestTokensAfter: afterFull,
7646
8481
  reductions,
7647
8482
  collapsedDigest,
8483
+ evidenceDigest,
8484
+ quality,
7648
8485
  repaired: repaired.report.changed ? {
7649
8486
  removedToolUses: repaired.report.removedToolUses,
7650
8487
  removedToolResults: repaired.report.removedToolResults,
@@ -7686,7 +8523,13 @@ var HybridCompactor = class {
7686
8523
  if (boundary <= 0) return { saved: 0 };
7687
8524
  const removed = messages.slice(0, boundary);
7688
8525
  const removedTokens = estimateMessages(removed);
7689
- 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)`;
8526
+ 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)`;
8527
+ const evidenceDigest = buildContextEvidenceDigest(ctx);
8528
+ const digest = evidenceDigest ? `[context_state]
8529
+ ${evidenceDigest}
8530
+
8531
+ [prior_history]
8532
+ ${historyDigest}` : historyDigest;
7690
8533
  const summaryMsg = {
7691
8534
  role: "system",
7692
8535
  content: `[prior_turns_digest: ${digest}]`
@@ -7695,10 +8538,29 @@ var HybridCompactor = class {
7695
8538
  ctx.state.replaceMessages([summaryMsg, ...tail]);
7696
8539
  return {
7697
8540
  saved: Math.max(0, removedTokens - estimateMessages([summaryMsg])),
7698
- digest
8541
+ digest,
8542
+ evidenceDigest: evidenceDigest || void 0
7699
8543
  };
7700
8544
  }
7701
8545
  };
8546
+ function checkCompactionQuality(ctx, opts) {
8547
+ const evidence = ctx.contextEvidence;
8548
+ const digest = `${opts.collapsedDigest ?? ""}
8549
+ ${opts.evidenceDigest ?? ""}`;
8550
+ const hasIntent = Boolean(evidence?.currentIntent?.text || /\b(intent|goal|session_goals)\b/i.test(digest));
8551
+ const hasPathTrail = Boolean(
8552
+ Object.keys(evidence?.fileGraph ?? {}).length > 0 || (evidence?.toolCalls.length ?? 0) > 0 || /\b(dependency_graph|tool_trail|files=)\b/i.test(digest)
8553
+ );
8554
+ const issues = [];
8555
+ if (opts.reduced && !hasIntent) issues.push("missing intent anchor");
8556
+ if (opts.reduced && !hasPathTrail) issues.push("missing tool/path trail");
8557
+ return {
8558
+ ok: issues.length === 0,
8559
+ hasIntent,
8560
+ hasPathTrail,
8561
+ issues
8562
+ };
8563
+ }
7702
8564
  function readContextWindowPolicy(ctx) {
7703
8565
  const policy = ctx.meta?.["contextWindowPolicy"];
7704
8566
  if (!policy || typeof policy !== "object") return null;
@@ -8057,10 +8919,10 @@ var SelectiveCompactor = class {
8057
8919
  this.maxContext = opts.maxContext ?? 128e3;
8058
8920
  this.preserveK = opts.preserveK ?? 4;
8059
8921
  this.eliseThreshold = opts.eliseThreshold ?? 500;
8060
- this.summarizerModel = opts.summarizerModel ?? opts.selectorModel ?? "unknown";
8061
- if (this.summarizerModel === "unknown" && (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1")) {
8922
+ this.summarizerModel = opts.summarizerModel ?? opts.selectorModel;
8923
+ if (this.summarizerModel === void 0 && (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1")) {
8062
8924
  console.warn(
8063
- "[SelectiveCompactor] summarizerModel not set \u2014 will use provider default. Set `summarizerModel` explicitly to silence this warning."
8925
+ "[SelectiveCompactor] summarizerModel not set \u2014 will fall back to ctx.model at summarize time. Set `summarizerModel` explicitly to silence this warning."
8064
8926
  );
8065
8927
  }
8066
8928
  this.summarizerPrompt = opts.summarizerPrompt ?? "You are a context summarizer. Given a list of messages, produce a concise summary that preserves all factual information, decisions, file changes, and state changes. Do not add commentary or opinions.";
@@ -8168,7 +9030,7 @@ var SelectiveCompactor = class {
8168
9030
  Summarize the following message range:`;
8169
9031
  const body = messages.map((m, i) => `[${i}] ${m.role}: ${this.messagePreview(m)}`).join("\n");
8170
9032
  const req = {
8171
- model: this.summarizerModel,
9033
+ model: this.summarizerModel ?? ctx.model,
8172
9034
  system: [{ type: "text", text: systemText }],
8173
9035
  messages: [{ role: "user", content: body }],
8174
9036
  maxTokens: 512
@@ -8235,27 +9097,16 @@ Summarize the following message range:`;
8235
9097
  if (typeof m.content === "string") return m.content.trim().length > 0;
8236
9098
  return m.content.some((b) => b.type === "text" && b.text.trim().length > 0);
8237
9099
  }
9100
+ /**
9101
+ * Estimate message-array tokens via the shared `estimateMessages` primitive
9102
+ * so SelectiveCompactor's before/after/load figures agree with the
9103
+ * middleware threshold math and the other compactors. Previously this used a
9104
+ * private `ceil(len/3.5)` walk that diverged from the calibrated shared
9105
+ * estimator, causing the selective `load`/`targetBudget` comparison to mix
9106
+ * two incompatible token scales.
9107
+ */
8238
9108
  estimateTokens(messages) {
8239
- let total = 0;
8240
- for (const m of messages) {
8241
- if (typeof m.content === "string") {
8242
- total += this.roughTokenEstimate(m.content);
8243
- } else {
8244
- for (const b of m.content) {
8245
- if (b.type === "text") total += this.roughTokenEstimate(b.text);
8246
- else if (b.type === "tool_use") total += this.roughTokenEstimate(JSON.stringify(b.input));
8247
- else if (b.type === "tool_result") {
8248
- total += this.roughTokenEstimate(
8249
- typeof b.content === "string" ? b.content : JSON.stringify(b.content)
8250
- );
8251
- }
8252
- }
8253
- }
8254
- }
8255
- return total;
8256
- }
8257
- roughTokenEstimate(text) {
8258
- return Math.max(1, Math.ceil(text.length / 3.5));
9109
+ return estimateMessages(messages);
8259
9110
  }
8260
9111
  };
8261
9112
 
@@ -8446,15 +9297,20 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
8446
9297
  this._cachedMsgCount = msgCount;
8447
9298
  this._cachedToolCount = toolCount;
8448
9299
  }
8449
- const load = tokens / this._maxContext;
9300
+ const budget = computeContextWindowBudget(ctx, tokens, this._maxContext);
9301
+ const load = budget.load;
8450
9302
  const policy = this.policyProvider?.(ctx);
8451
9303
  const thresholds = policy?.thresholds ?? {
8452
9304
  warn: this.warnThreshold,
8453
9305
  soft: this.softThreshold,
8454
9306
  hard: this.hardThreshold
8455
9307
  };
9308
+ const repetition = repeatedReadPressure(ctx);
9309
+ const adaptiveThresholds = adaptThresholdsForSignals(thresholds, {
9310
+ repeatedReadCount: repetition
9311
+ });
8456
9312
  const aggressiveOn = policy?.aggressiveOn ?? this.aggressiveOn;
8457
- const level = load >= thresholds.hard ? "hard" : load >= thresholds.soft ? "soft" : load >= thresholds.warn ? "warn" : null;
9313
+ const level = load >= adaptiveThresholds.hard ? "hard" : load >= adaptiveThresholds.soft ? "soft" : load >= adaptiveThresholds.warn ? "warn" : null;
8458
9314
  if (!level) {
8459
9315
  this.lastNoopAttempt = null;
8460
9316
  return next(ctx);
@@ -8463,7 +9319,13 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
8463
9319
  return next(ctx);
8464
9320
  }
8465
9321
  const aggressive = level === "hard" ? true : level === "soft" ? aggressiveOn !== "hard" : aggressiveOn === "warn";
8466
- await this.compact(ctx, aggressive, { level, tokens, load });
9322
+ await this.compact(ctx, aggressive, {
9323
+ level,
9324
+ tokens,
9325
+ load,
9326
+ budget,
9327
+ signals: { repeatedReadCount: repetition }
9328
+ });
8467
9329
  return next(ctx);
8468
9330
  };
8469
9331
  }
@@ -8516,6 +9378,8 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
8516
9378
  tokens: pressure.tokens,
8517
9379
  load: pressure.load,
8518
9380
  maxContext: this._maxContext,
9381
+ budget: pressure.budget,
9382
+ signals: pressure.signals,
8519
9383
  report,
8520
9384
  aggressive
8521
9385
  });
@@ -8527,6 +9391,8 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
8527
9391
  level: pressure.level,
8528
9392
  aggressive,
8529
9393
  reductions: report.reductions?.map((r) => ({ phase: r.phase, saved: r.saved })),
9394
+ budget: pressure.budget,
9395
+ signals: pressure.signals,
8530
9396
  // Record what was collapsed so the audit trail shows the preserved
8531
9397
  // content, not just token counts. Bounded to keep the log line small;
8532
9398
  // the full original turns are already in the session JSONL.
@@ -8542,6 +9408,8 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
8542
9408
  level: pressure.level,
8543
9409
  tokens: pressure.tokens,
8544
9410
  maxContext: this._maxContext,
9411
+ budget: pressure.budget,
9412
+ signals: pressure.signals,
8545
9413
  load: pressure.load,
8546
9414
  fatal
8547
9415
  });
@@ -8561,8 +9429,37 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
8561
9429
  }
8562
9430
  }
8563
9431
  };
8564
-
8565
- // src/execution/tool-executor.ts
9432
+ function computeContextWindowBudget(ctx, inputTokens, maxContext) {
9433
+ const reservedOutputTokens = readPositiveMetaNumber(ctx, "contextOutputReserveTokens") ?? Math.floor(Math.min(8192, maxContext * 0.08));
9434
+ const reservedSafetyTokens = readPositiveMetaNumber(ctx, "contextSafetyBufferTokens") ?? Math.floor(Math.min(4096, maxContext * 0.02));
9435
+ const availableInputTokens = Math.max(
9436
+ 1,
9437
+ maxContext - reservedOutputTokens - reservedSafetyTokens
9438
+ );
9439
+ const remainingInputTokens = availableInputTokens - inputTokens;
9440
+ return {
9441
+ maxContext,
9442
+ inputTokens,
9443
+ availableInputTokens,
9444
+ remainingInputTokens,
9445
+ reservedOutputTokens,
9446
+ reservedSafetyTokens,
9447
+ load: inputTokens / availableInputTokens,
9448
+ overflowTokens: Math.max(0, -remainingInputTokens)
9449
+ };
9450
+ }
9451
+ function readPositiveMetaNumber(ctx, key) {
9452
+ const value = ctx.meta?.[key];
9453
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : void 0;
9454
+ }
9455
+ function adaptThresholdsForSignals(thresholds, signals) {
9456
+ if (signals.repeatedReadCount < 3) return thresholds;
9457
+ return {
9458
+ warn: Math.max(0.25, thresholds.warn - 0.08),
9459
+ soft: Math.max(0.35, thresholds.soft - 0.04),
9460
+ hard: thresholds.hard
9461
+ };
9462
+ }
8566
9463
  var ToolExecutor = class _ToolExecutor {
8567
9464
  constructor(registry, opts) {
8568
9465
  this.registry = registry;
@@ -8659,7 +9556,8 @@ ${errorDetails}`,
8659
9556
  let effectivePermission = decision.permission;
8660
9557
  const policy = this.opts.permissionPolicy;
8661
9558
  const yolo = policy.getYolo?.() === true || policy.getYoloDestructive?.() === true;
8662
- if (toolDangerousCaps.length > 0 && effectivePermission === "auto" && !yolo) {
9559
+ const authoritativeAuto = decision.source === "yolo";
9560
+ if (toolDangerousCaps.length > 0 && effectivePermission === "auto" && !yolo && !authoritativeAuto) {
8663
9561
  effectivePermission = "confirm";
8664
9562
  }
8665
9563
  if (effectivePermission === "deny") {
@@ -8681,7 +9579,7 @@ ${errorDetails}`,
8681
9579
  return { result, tool, durationMs: Date.now() - start };
8682
9580
  }
8683
9581
  } else {
8684
- const suggestedPattern = this.subjectFor(tool.name, use.input, tool.subjectKey) ?? tool.name;
9582
+ const suggestedPattern = subjectForToolInput(tool.name, use.input, tool.subjectKey) ?? tool.name;
8685
9583
  const pending = {
8686
9584
  type: "tool_confirm_pending",
8687
9585
  toolUseId: use.id,
@@ -8801,9 +9699,10 @@ ${post.additionalContext}`;
8801
9699
  });
8802
9700
  this.opts.renderer?.writeToolCall(tool.name, use.input);
8803
9701
  const output = await this.runWithTimeout(tool, use.input, ctx.signal, ctx, use.id);
8804
- const text = this.serializer.serialize(output);
9702
+ const text = this.serializer.serialize(output, { toolName: tool.name, input: use.input });
8805
9703
  const scrubbed = this.opts.secretScrubber.scrub(text);
8806
- const { text: capped, newBudget } = this.serializer.enforceCap(scrubbed, budget);
9704
+ const withArtifact = await maybePersistLargeToolOutput(tool.name, scrubbed, budget);
9705
+ const { text: capped, newBudget } = this.serializer.enforceCap(withArtifact, budget);
8807
9706
  this.opts.renderer?.writeToolResult(tool.name, capped, false);
8808
9707
  return {
8809
9708
  block: {
@@ -8828,38 +9727,27 @@ ${post.additionalContext}`;
8828
9727
  tool.timeoutMs ?? this.iterationTimeoutMs,
8829
9728
  this.maxToolTimeoutMs
8830
9729
  );
8831
- const ctrl = new AbortController();
8832
- const timer = setTimeout(() => ctrl.abort(new Error("tool timeout")), timeoutMs);
8833
- const combined = AbortSignal.any([parentSignal, ctrl.signal]);
8834
- let cleanupCalled = false;
8835
- let caught = false;
9730
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
9731
+ const combined = AbortSignal.any([parentSignal, timeoutSignal]);
9732
+ let output;
8836
9733
  try {
8837
- if (typeof tool.executeStream === "function") {
8838
- return await this.runStreamedTool(tool, input, ctx, combined, toolUseId);
8839
- }
8840
- return await tool.execute(input, ctx, { signal: combined });
9734
+ output = typeof tool.executeStream === "function" ? await this.runStreamedTool(tool, input, ctx, combined, toolUseId) : await tool.execute(input, ctx, { signal: combined });
8841
9735
  } catch (err) {
8842
- caught = true;
8843
- if (combined.aborted && typeof tool.cleanup === "function") {
8844
- cleanupCalled = true;
8845
- try {
8846
- await tool.cleanup(input, ctx);
8847
- } catch {
8848
- }
8849
- }
9736
+ if (combined.aborted) await this.runToolCleanup(tool, input, ctx);
8850
9737
  throw err;
8851
- } finally {
8852
- clearTimeout(timer);
8853
- if (combined.aborted && !caught) {
8854
- if (!cleanupCalled && typeof tool.cleanup === "function") {
8855
- try {
8856
- await tool.cleanup(input, ctx);
8857
- } catch {
8858
- }
8859
- }
8860
- const reason = combined.reason instanceof Error ? combined.reason : new Error(typeof combined.reason === "string" ? combined.reason : "aborted");
8861
- throw reason;
8862
- }
9738
+ }
9739
+ if (combined.aborted) {
9740
+ await this.runToolCleanup(tool, input, ctx);
9741
+ throw combined.reason instanceof Error ? combined.reason : new Error(typeof combined.reason === "string" ? combined.reason : "tool timeout");
9742
+ }
9743
+ return output;
9744
+ }
9745
+ /** Best-effort tool cleanup; never let it mask the original error. */
9746
+ async runToolCleanup(tool, input, ctx) {
9747
+ if (typeof tool.cleanup !== "function") return;
9748
+ try {
9749
+ await tool.cleanup(input, ctx);
9750
+ } catch {
8863
9751
  }
8864
9752
  }
8865
9753
  async runStreamedTool(tool, input, ctx, signal, toolUseId) {
@@ -8970,38 +9858,6 @@ ${excerpt}`;
8970
9858
  budgetForString(content, budget) {
8971
9859
  return Math.max(0, budget - Buffer.byteLength(content, "utf8"));
8972
9860
  }
8973
- /**
8974
- * Compute the suggestedPattern string for a tool+input pair.
8975
- * Matches the logic in DefaultPermissionPolicy so the TUI shows the
8976
- * same subject that the trust file would use.
8977
- */
8978
- subjectFor(toolName, input, subjectKey) {
8979
- if (!input || typeof input !== "object") return void 0;
8980
- const obj = input;
8981
- const globChars = /[*?[\]]/g;
8982
- const escapeGlob = (s) => s.replace(globChars, (c) => `\\${c}`);
8983
- const normalizePath = (s) => escapeGlob(s.replace(/\\/g, "/"));
8984
- if (subjectKey) {
8985
- const v = obj[subjectKey];
8986
- if (typeof v === "string") {
8987
- const isPathKey = subjectKey === "path" || subjectKey === "file" || subjectKey === "files";
8988
- return isPathKey ? normalizePath(v) : escapeGlob(v);
8989
- }
8990
- }
8991
- if (toolName === "bash" && typeof obj.command === "string") {
8992
- return escapeGlob(obj.command);
8993
- }
8994
- if (typeof obj.path === "string") {
8995
- return normalizePath(obj.path);
8996
- }
8997
- if (typeof obj.url === "string") {
8998
- return escapeGlob(obj.url);
8999
- }
9000
- if (typeof obj.name === "string") {
9001
- return escapeGlob(obj.name);
9002
- }
9003
- return void 0;
9004
- }
9005
9861
  };
9006
9862
  function clampTimeoutMs(timeoutMs, maxTimeoutMs) {
9007
9863
  const fallback = 3e5;
@@ -9028,6 +9884,25 @@ function extractMalformedRaw(input) {
9028
9884
  return String(value);
9029
9885
  }
9030
9886
  }
9887
+ var TOOL_OUTPUT_ARTIFACT_THRESHOLD_BYTES = 64 * 1024;
9888
+ async function maybePersistLargeToolOutput(toolName, content, budget) {
9889
+ const bytes = Buffer.byteLength(content, "utf8");
9890
+ if (bytes <= Math.min(TOOL_OUTPUT_ARTIFACT_THRESHOLD_BYTES, Math.max(0, budget))) {
9891
+ return content;
9892
+ }
9893
+ try {
9894
+ const dir = path4.join(wstackGlobalRoot(), "tool-output");
9895
+ await fsp2.mkdir(dir, { recursive: true });
9896
+ const safeTool = toolName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
9897
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
9898
+ const filePath = path4.join(dir, `${stamp}-${safeTool}-${randomUUID()}.log`);
9899
+ await fsp2.writeFile(filePath, content, "utf8");
9900
+ return content + `
9901
+ [full tool output: ${bytes} bytes at ${filePath}; read/grep that file selectively instead of re-running or requesting more output]`;
9902
+ } catch {
9903
+ return content;
9904
+ }
9905
+ }
9031
9906
 
9032
9907
  // src/execution/autonomous-runner.ts
9033
9908
  var DoneConditionChecker = class {
@@ -10339,13 +11214,13 @@ var SubagentBudget = class _SubagentBudget {
10339
11214
  if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
10340
11215
  return Promise.resolve("stop");
10341
11216
  }
10342
- return new Promise((resolve5) => {
11217
+ return new Promise((resolve6) => {
10343
11218
  let resolved = false;
10344
11219
  const respond = (d) => {
10345
11220
  if (resolved) return;
10346
11221
  resolved = true;
10347
11222
  clearTimeout(fallback);
10348
- resolve5(d);
11223
+ resolve6(d);
10349
11224
  };
10350
11225
  const fallback = setTimeout(() => respond("stop"), _SubagentBudget.DECISION_TIMEOUT_MS);
10351
11226
  bus.emit("budget.threshold_reached", {
@@ -12808,7 +13683,27 @@ Working rules:
12808
13683
  id: "devops",
12809
13684
  name: "DevOps",
12810
13685
  role: "devops",
12811
- tools: [...TOOLS.build],
13686
+ tools: [
13687
+ ...TOOLS.build,
13688
+ "mcp__ssh__ssh_list_servers",
13689
+ "mcp__ssh__ssh_connection_status",
13690
+ "mcp__ssh__ssh_execute",
13691
+ "mcp__ssh__ssh_execute_sudo",
13692
+ "mcp__ssh__ssh_upload",
13693
+ "mcp__ssh__ssh_download",
13694
+ "mcp__ssh__ssh_sync",
13695
+ "mcp__ssh__ssh_deploy",
13696
+ "mcp__ssh__ssh_health_check",
13697
+ "mcp__ssh__ssh_service_status",
13698
+ "mcp__ssh__ssh_process_manager",
13699
+ "mcp__ssh__ssh_tunnel",
13700
+ "mcp__ssh__ssh_backup_create",
13701
+ "mcp__ssh__ssh_backup_list",
13702
+ "mcp__ssh__ssh_backup_restore",
13703
+ "mcp__ssh__ssh_db_list",
13704
+ "mcp__ssh__ssh_db_query",
13705
+ "mcp__ssh__ssh_profile"
13706
+ ],
12812
13707
  prompt: `You are the DevOps agent. Your job is CI/CD, containerization, and
12813
13708
  deployment configuration: make builds reproducible and deploys safe.
12814
13709
 
@@ -12817,6 +13712,7 @@ Scope:
12817
13712
  - Write Dockerfiles/compose and optimize image size and layer caching
12818
13713
  - Configure deployment (env, secrets handling, health checks, rollback)
12819
13714
  - Diagnose flaky/broken pipelines
13715
+ - Use optional SSH MCP tools for remote hosts when the 'ssh' MCP server is enabled: list servers, run health checks, inspect services, transfer/deploy files, and open tunnels
12820
13716
 
12821
13717
  Input format you accept:
12822
13718
  { "task": "ci | container | deploy | fix-pipeline", "platform": "github-actions | gitlab | docker | k8s", "target": "<what>" }
@@ -12831,7 +13727,8 @@ Working rules:
12831
13727
  - Never hardcode secrets in config; reference the secret store
12832
13728
  - Pin versions for reproducible builds; avoid floating :latest
12833
13729
  - Every deploy path needs a rollback and a health check
12834
- - Treat CI/CD changes as high-risk \u2014 explain blast radius before applying`
13730
+ - Treat CI/CD changes as high-risk \u2014 explain blast radius before applying
13731
+ - For remote SSH work, start with ssh_list_servers / ssh_connection_status, prefer read-only checks first, and do not run destructive commands without explicit user approval`
12835
13732
  },
12836
13733
  budget: MEDIUM_BUDGET,
12837
13734
  capability: {
@@ -12848,6 +13745,13 @@ Working rules:
12848
13745
  "kubernetes",
12849
13746
  "k8s",
12850
13747
  "deploy",
13748
+ "ssh",
13749
+ "remote ssh",
13750
+ "remote server",
13751
+ "sftp",
13752
+ "tunnel",
13753
+ "bastion",
13754
+ "jump host",
12851
13755
  "github actions",
12852
13756
  "container"
12853
13757
  ]
@@ -14156,7 +15060,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
14156
15060
  taskIds.map((id) => {
14157
15061
  const cached = this.completedResults.find((r) => r.taskId === id);
14158
15062
  if (cached) return cached;
14159
- return new Promise((resolve5, reject) => {
15063
+ return new Promise((resolve6, reject) => {
14160
15064
  const timeout = setTimeout(() => {
14161
15065
  this.off("task.completed", handler);
14162
15066
  reject(new Error(`awaitTasks timed out waiting for task "${id}"`));
@@ -14165,7 +15069,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
14165
15069
  if (result.taskId === id) {
14166
15070
  clearTimeout(timeout);
14167
15071
  this.off("task.completed", handler);
14168
- resolve5(result);
15072
+ resolve6(result);
14169
15073
  }
14170
15074
  };
14171
15075
  this.on("task.completed", handler);
@@ -14433,12 +15337,12 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
14433
15337
  }
14434
15338
  return new Promise((resolveDecision) => {
14435
15339
  let settled = false;
14436
- const resolve5 = (d) => {
15340
+ const resolve6 = (d) => {
14437
15341
  if (settled) return;
14438
15342
  settled = true;
14439
15343
  resolveDecision(d);
14440
15344
  };
14441
- const fallback = setTimeout(() => resolve5("stop"), DECISION_TIMEOUT_MS);
15345
+ const fallback = setTimeout(() => resolve6("stop"), DECISION_TIMEOUT_MS);
14442
15346
  budget._events?.emit("budget.threshold_reached", {
14443
15347
  kind: "timeout",
14444
15348
  used,
@@ -14454,11 +15358,11 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
14454
15358
  // disagreeing, resolves as a stop). Async grants still resolve.
14455
15359
  extend: (extra) => {
14456
15360
  clearTimeout(fallback);
14457
- queueMicrotask(() => resolve5({ extend: extra }));
15361
+ queueMicrotask(() => resolve6({ extend: extra }));
14458
15362
  },
14459
15363
  deny: () => {
14460
15364
  clearTimeout(fallback);
14461
- resolve5("stop");
15365
+ resolve6("stop");
14462
15366
  }
14463
15367
  });
14464
15368
  });
@@ -14619,6 +15523,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
14619
15523
  subagentId: result.subagentId,
14620
15524
  taskId: result.taskId,
14621
15525
  status: result.status,
15526
+ result: result.result,
14622
15527
  iterations: result.iterations,
14623
15528
  toolCalls: result.toolCalls,
14624
15529
  durationMs: result.durationMs
@@ -15346,7 +16251,7 @@ var InMemoryAgentBridge = class {
15346
16251
  });
15347
16252
  }
15348
16253
  this.inflightGuards.add(correlationId);
15349
- return new Promise((resolve5, reject) => {
16254
+ return new Promise((resolve6, reject) => {
15350
16255
  const timer = setTimeout(() => {
15351
16256
  this.inflightGuards.delete(correlationId);
15352
16257
  this.pendingRequests.delete(correlationId);
@@ -15365,7 +16270,7 @@ var InMemoryAgentBridge = class {
15365
16270
  return;
15366
16271
  }
15367
16272
  this.pendingRequests.set(correlationId, {
15368
- resolve: resolve5,
16273
+ resolve: resolve6,
15369
16274
  reject,
15370
16275
  timer
15371
16276
  });
@@ -16037,6 +16942,24 @@ Working rules:
16037
16942
  var DEFAULT_SUBAGENT_BASELINE = `You are a subagent operating under a Director. You were spawned to handle
16038
16943
  a specific slice of a larger plan \u2014 do that slice well and report back.
16039
16944
 
16945
+ Capabilities & operating rules:
16946
+ - You have full developer tools for your task: read, write/edit, search,
16947
+ shell + build (lint, format, typecheck, test), and dependency install.
16948
+ Use them directly to finish the task end-to-end. You run non-interactively
16949
+ \u2014 there is no human to approve individual tool calls, so routine work is
16950
+ pre-authorized; do not stop to ask for permission to read, edit, or build.
16951
+ - Stay inside the project root. Do not write files outside the repository,
16952
+ and do not touch machine config, credentials, or global state \u2014 those
16953
+ require an explicit grant you do not have.
16954
+ - Prefer the least-destructive path. Do not run irreversible or destructive
16955
+ commands (e.g. \`rm -rf\`, \`git push --force\`, history rewrites, dropping
16956
+ databases, mass deletes) unless the task explicitly requires it and names
16957
+ the target.
16958
+ - When you change code, verify it: run the relevant build / typecheck / tests
16959
+ and fix what you broke before reporting done.
16960
+ - Make only the changes the task calls for \u2014 don't refactor or reformat
16961
+ unrelated code.
16962
+
16040
16963
  Bridge contract:
16041
16964
  - You have a parent (the Director). You may call \`request\` on the
16042
16965
  parent bridge to ask a clarifying question. Use this sparingly; the
@@ -16994,6 +17917,8 @@ var Director = class _Director {
16994
17917
  sessionsRoot;
16995
17918
  /** Director run id for JSONL path resolution. */
16996
17919
  directorRunId;
17920
+ /** Optional logger for structured logging. Falls back to noop when omitted. */
17921
+ logger;
16997
17922
  /** Resolves task descriptions back from `assign()` so completion events
16998
17923
  * can also carry a human-readable title. */
16999
17924
  taskDescriptions = /* @__PURE__ */ new Map();
@@ -17082,6 +18007,7 @@ var Director = class _Director {
17082
18007
  opts.checkpointDebounceMs ?? 250
17083
18008
  ) : null;
17084
18009
  this.fleetManager = opts.fleetManager;
18010
+ this.logger = opts.logger;
17085
18011
  if (this.sharedScratchpadPath) {
17086
18012
  void fsp2.mkdir(this.sharedScratchpadPath, { recursive: true }).catch((err) => this.logShutdownError("shared_scratchpad_mkdir", err));
17087
18013
  }
@@ -17381,7 +18307,10 @@ var Director = class _Director {
17381
18307
  entry.session.cancel(reason);
17382
18308
  for (const [_role, subagentId] of entry.session.getSubagentIds()) {
17383
18309
  this.coordinator.stop(subagentId).catch((err) => {
17384
- console.debug(`[director] stop subagent ${subagentId} failed (may have already completed): ${err}`);
18310
+ this.logger?.debug(`stop subagent ${subagentId} failed (may have already completed)`, {
18311
+ subagentId,
18312
+ err: err instanceof Error ? err.message : String(err)
18313
+ });
17385
18314
  });
17386
18315
  }
17387
18316
  }
@@ -17441,7 +18370,7 @@ var Director = class _Director {
17441
18370
  * Caller-supplied `priceLookup` is optional but recommended — without
17442
18371
  * it the `cost` column in `usage.snapshot()` stays at 0.
17443
18372
  */
17444
- async spawn(config, priceLookup) {
18373
+ async spawn(callerConfig, priceLookup) {
17445
18374
  if (this.workCompleteFlag) {
17446
18375
  throw new FleetSpawnBudgetError(
17447
18376
  "max_spawns",
@@ -17450,6 +18379,7 @@ var Director = class _Director {
17450
18379
  "workComplete() has been called \u2014 director closed further spawning"
17451
18380
  );
17452
18381
  }
18382
+ const config = { ...callerConfig };
17453
18383
  if (!config.model && this.modelMatrix) {
17454
18384
  const matrix = typeof this.modelMatrix === "function" ? this.modelMatrix() : this.modelMatrix;
17455
18385
  const entry = resolveModelMatrix(matrix, config.role);
@@ -17675,7 +18605,7 @@ var Director = class _Director {
17675
18605
  })),
17676
18606
  usage: this.usage.snapshot()
17677
18607
  };
17678
- await fsp2.mkdir(path3.dirname(this.manifestPath), { recursive: true });
18608
+ await fsp2.mkdir(path4.dirname(this.manifestPath), { recursive: true });
17679
18609
  await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
17680
18610
  return this.manifestPath;
17681
18611
  }
@@ -17802,11 +18732,11 @@ var Director = class _Director {
17802
18732
  if (cached) return cached;
17803
18733
  const existing = this.taskWaiters.get(id);
17804
18734
  if (existing) return existing.promise;
17805
- let resolve5;
18735
+ let resolve6;
17806
18736
  const promise = new Promise((res) => {
17807
- resolve5 = res;
18737
+ resolve6 = res;
17808
18738
  });
17809
- this.taskWaiters.set(id, { promise, resolve: resolve5 });
18739
+ this.taskWaiters.set(id, { promise, resolve: resolve6 });
17810
18740
  return promise;
17811
18741
  })
17812
18742
  );
@@ -17889,7 +18819,7 @@ var Director = class _Director {
17889
18819
  */
17890
18820
  async readSession(subagentId, tail) {
17891
18821
  if (!this.sessionsRoot) return null;
17892
- const filePath = path3.join(this.sessionsRoot, this.directorRunId, `${subagentId}.jsonl`);
18822
+ const filePath = path4.join(this.sessionsRoot, this.directorRunId, `${subagentId}.jsonl`);
17893
18823
  let raw;
17894
18824
  try {
17895
18825
  raw = await fsp2.readFile(filePath, "utf8");
@@ -18202,7 +19132,7 @@ function createDelegateTool(opts) {
18202
19132
  subagentId
18203
19133
  });
18204
19134
  const dir = director;
18205
- const result = await new Promise((resolve5) => {
19135
+ const result = await new Promise((resolve6) => {
18206
19136
  let settled = false;
18207
19137
  let timer;
18208
19138
  const finish = (value) => {
@@ -18212,7 +19142,7 @@ function createDelegateTool(opts) {
18212
19142
  offTool();
18213
19143
  offIter();
18214
19144
  offProgress();
18215
- resolve5(value);
19145
+ resolve6(value);
18216
19146
  };
18217
19147
  const arm = () => {
18218
19148
  if (timer) clearTimeout(timer);
@@ -18419,13 +19349,13 @@ async function readSubagentPartial(opts, subagentId) {
18419
19349
  if (!opts.sessionsRoot) return void 0;
18420
19350
  const candidates = [];
18421
19351
  if (opts.directorRunId) {
18422
- candidates.push(path3.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
19352
+ candidates.push(path4.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
18423
19353
  } else {
18424
19354
  try {
18425
19355
  const entries = await fsp2.readdir(opts.sessionsRoot, { withFileTypes: true });
18426
19356
  for (const entry of entries) {
18427
19357
  if (entry.isDirectory()) {
18428
- candidates.push(path3.join(opts.sessionsRoot, entry.name, `${subagentId}.jsonl`));
19358
+ candidates.push(path4.join(opts.sessionsRoot, entry.name, `${subagentId}.jsonl`));
18429
19359
  }
18430
19360
  }
18431
19361
  } catch {
@@ -18475,9 +19405,9 @@ function makeDirectorSessionFactory(opts) {
18475
19405
  let dir;
18476
19406
  if (opts.store) {
18477
19407
  store = opts.store;
18478
- dir = opts.sessionsRoot ? path3.join(opts.sessionsRoot, runId) : "(caller-managed)";
19408
+ dir = opts.sessionsRoot ? path4.join(opts.sessionsRoot, runId) : "(caller-managed)";
18479
19409
  } else if (opts.sessionsRoot) {
18480
- dir = path3.join(opts.sessionsRoot, runId);
19410
+ dir = path4.join(opts.sessionsRoot, runId);
18481
19411
  store = new DefaultSessionStore({ dir });
18482
19412
  } else {
18483
19413
  throw new Error("makeDirectorSessionFactory requires either `store` or `sessionsRoot`");
@@ -18627,7 +19557,7 @@ var DefaultModelsRegistry = class {
18627
19557
  this.overlay = opts.overlay;
18628
19558
  this.overlayUrl = opts.overlayUrl;
18629
19559
  this.overlayFile = opts.overlayFile;
18630
- this.overlayCacheFile = opts.overlayCacheFile ?? (opts.overlayUrl ? path3.join(path3.dirname(opts.cacheFile), "models-overlay-cache.json") : void 0);
19560
+ this.overlayCacheFile = opts.overlayCacheFile ?? (opts.overlayUrl ? path4.join(path4.dirname(opts.cacheFile), "models-overlay-cache.json") : void 0);
18631
19561
  }
18632
19562
  async load(opts = {}) {
18633
19563
  if (this.payload && !opts.force) return this.payload;
@@ -18786,10 +19716,11 @@ var DefaultModelsRegistry = class {
18786
19716
  capabilities: {
18787
19717
  tools: model.tool_call ?? false,
18788
19718
  vision: Boolean(model.modalities?.input?.includes("image")),
18789
- reasoning: model.reasoning ?? false,
19719
+ reasoning: model.reasoning ?? model.reasoningConfig !== void 0,
18790
19720
  maxContext: model.limit?.context ?? 0,
18791
19721
  maxOutput: model.limit?.output,
18792
- knowledge: model.knowledge
19722
+ knowledge: model.knowledge,
19723
+ reasoningConfig: model.reasoningConfig
18793
19724
  },
18794
19725
  cost: model.cost
18795
19726
  };
@@ -18840,7 +19771,7 @@ var DefaultModelsRegistry = class {
18840
19771
  }
18841
19772
  /** Used by `wstack models refresh` to expose where the cache lives. */
18842
19773
  cacheLocation() {
18843
- return path3.resolve(this.cacheFile);
19774
+ return path4.resolve(this.cacheFile);
18844
19775
  }
18845
19776
  };
18846
19777
  function hasEntries(payload) {
@@ -19207,7 +20138,7 @@ var DefaultModeStore = class {
19207
20138
  }
19208
20139
  async loadActiveMode() {
19209
20140
  try {
19210
- const configPath = path3.join(this.configDir, "mode.json");
20141
+ const configPath = path4.join(this.configDir, "mode.json");
19211
20142
  const content = await fsp2.readFile(configPath, "utf8");
19212
20143
  const data = JSON.parse(content);
19213
20144
  this.activeModeId = data.activeMode ?? null;
@@ -19218,7 +20149,7 @@ var DefaultModeStore = class {
19218
20149
  async saveActiveMode() {
19219
20150
  try {
19220
20151
  await fsp2.mkdir(this.configDir, { recursive: true });
19221
- const configPath = path3.join(this.configDir, "mode.json");
20152
+ const configPath = path4.join(this.configDir, "mode.json");
19222
20153
  await atomicWrite(
19223
20154
  configPath,
19224
20155
  JSON.stringify({ activeMode: this.activeModeId }, null, 2)
@@ -19233,11 +20164,11 @@ async function loadProjectModes(modesDir) {
19233
20164
  const entries = await fsp2.readdir(modesDir);
19234
20165
  for (const entry of entries) {
19235
20166
  if (!entry.endsWith(".md") && !entry.endsWith(".txt")) continue;
19236
- const filePath = path3.join(modesDir, entry);
20167
+ const filePath = path4.join(modesDir, entry);
19237
20168
  const stat6 = await fsp2.stat(filePath);
19238
20169
  if (!stat6.isFile()) continue;
19239
20170
  const content = await fsp2.readFile(filePath, "utf8");
19240
- const id = path3.basename(entry, path3.extname(entry));
20171
+ const id = path4.basename(entry, path4.extname(entry));
19241
20172
  modes.push({
19242
20173
  id,
19243
20174
  name: id.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
@@ -19253,7 +20184,7 @@ async function loadProjectModes(modesDir) {
19253
20184
  async function loadUserModes(modesDir) {
19254
20185
  const modes = [];
19255
20186
  try {
19256
- const manifestPath = path3.join(modesDir, "modes.json");
20187
+ const manifestPath = path4.join(modesDir, "modes.json");
19257
20188
  const content = await fsp2.readFile(manifestPath, "utf8");
19258
20189
  const manifest = JSON.parse(content);
19259
20190
  for (const mode of manifest.modes) {
@@ -19264,6 +20195,35 @@ async function loadUserModes(modesDir) {
19264
20195
  return modes;
19265
20196
  }
19266
20197
 
20198
+ // src/models/provider-model-resolve.ts
20199
+ function describeCatalogModel(m) {
20200
+ return {
20201
+ id: m.id,
20202
+ name: m.name,
20203
+ releaseDate: m.release_date,
20204
+ contextWindow: m.limit?.context,
20205
+ inputCost: m.cost?.input,
20206
+ outputCost: m.cost?.output,
20207
+ capabilities: [
20208
+ ...m.tool_call ? ["tools"] : [],
20209
+ ...m.reasoning ? ["reasoning"] : [],
20210
+ ...m.modalities?.input?.includes("image") ? ["vision"] : [],
20211
+ ...m.open_weights ? ["open_weights"] : []
20212
+ ]
20213
+ };
20214
+ }
20215
+ function resolveProviderModelList(savedModels, catalog) {
20216
+ if (savedModels && savedModels.length > 0) {
20217
+ const byId = new Map((catalog?.models ?? []).map((m) => [m.id, m]));
20218
+ return savedModels.map((id) => {
20219
+ const hit = byId.get(id);
20220
+ return hit ? describeCatalogModel(hit) : { id, name: id, capabilities: [] };
20221
+ });
20222
+ }
20223
+ if (catalog) return catalog.models.map(describeCatalogModel);
20224
+ return [];
20225
+ }
20226
+
19267
20227
  // src/sdd/spec-parser.ts
19268
20228
  var SpecParser = class {
19269
20229
  parse(content) {
@@ -20205,7 +21165,7 @@ var SpecStore = class {
20205
21165
  indexPath;
20206
21166
  constructor(opts) {
20207
21167
  this.baseDir = opts.baseDir;
20208
- this.indexPath = path3.join(this.baseDir, "_index.json");
21168
+ this.indexPath = path4.join(this.baseDir, "_index.json");
20209
21169
  }
20210
21170
  async save(spec) {
20211
21171
  await ensureDir(this.baseDir);
@@ -20274,7 +21234,7 @@ var SpecStore = class {
20274
21234
  return updated;
20275
21235
  }
20276
21236
  filePath(id) {
20277
- return path3.join(this.baseDir, `${id}.json`);
21237
+ return path4.join(this.baseDir, `${id}.json`);
20278
21238
  }
20279
21239
  async readIndex() {
20280
21240
  try {
@@ -20331,7 +21291,7 @@ var TaskGraphStore = class {
20331
21291
  indexPath;
20332
21292
  constructor(opts) {
20333
21293
  this.baseDir = opts.baseDir;
20334
- this.indexPath = path3.join(this.baseDir, "_index.json");
21294
+ this.indexPath = path4.join(this.baseDir, "_index.json");
20335
21295
  }
20336
21296
  async save(graph) {
20337
21297
  await ensureDir(this.baseDir);
@@ -20369,7 +21329,7 @@ var TaskGraphStore = class {
20369
21329
  }
20370
21330
  }
20371
21331
  filePath(id) {
20372
- return path3.join(this.baseDir, `${id}.json`);
21332
+ return path4.join(this.baseDir, `${id}.json`);
20373
21333
  }
20374
21334
  async readIndex() {
20375
21335
  try {
@@ -20614,9 +21574,9 @@ var AISpecBuilder = class {
20614
21574
  if (!this.sessionPath) return;
20615
21575
  try {
20616
21576
  const fsp16 = await import('fs/promises');
20617
- const path19 = await import('path');
21577
+ const path21 = await import('path');
20618
21578
  const { atomicWrite: atomicWrite2 } = await Promise.resolve().then(() => (init_atomic_write(), atomic_write_exports));
20619
- await fsp16.mkdir(path19.dirname(this.sessionPath), { recursive: true });
21579
+ await fsp16.mkdir(path21.dirname(this.sessionPath), { recursive: true });
20620
21580
  await atomicWrite2(this.sessionPath, JSON.stringify(this.session, null, 2));
20621
21581
  } catch {
20622
21582
  }
@@ -21343,15 +22303,15 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
21343
22303
  maxId = id;
21344
22304
  }
21345
22305
  }
21346
- const path19 = [];
22306
+ const path21 = [];
21347
22307
  let current = maxId;
21348
22308
  const visited = /* @__PURE__ */ new Set();
21349
22309
  while (current && !visited.has(current)) {
21350
22310
  visited.add(current);
21351
- path19.unshift(current);
22311
+ path21.unshift(current);
21352
22312
  current = prev.get(current) ?? null;
21353
22313
  }
21354
- return path19;
22314
+ return path21;
21355
22315
  }
21356
22316
  function computeParallelGroups(graph, blockedByMap) {
21357
22317
  const groups = [];
@@ -22174,9 +23134,9 @@ var DefaultHealthRegistry = class {
22174
23134
  }
22175
23135
  async runOne(check) {
22176
23136
  let timer = null;
22177
- const timeout = new Promise((resolve5) => {
23137
+ const timeout = new Promise((resolve6) => {
22178
23138
  timer = setTimeout(
22179
- () => resolve5({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
23139
+ () => resolve6({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
22180
23140
  this.timeoutMs
22181
23141
  );
22182
23142
  });
@@ -22359,7 +23319,7 @@ async function startMetricsServer(opts) {
22359
23319
  const tls = opts.tls;
22360
23320
  const useHttps = !!(tls?.cert && tls?.key);
22361
23321
  const host = opts.host ?? "127.0.0.1";
22362
- const path19 = opts.path ?? "/metrics";
23322
+ const path21 = opts.path ?? "/metrics";
22363
23323
  const healthPath = opts.healthPath ?? "/healthz";
22364
23324
  const healthRegistry = opts.healthRegistry;
22365
23325
  const listener = (req, res) => {
@@ -22369,7 +23329,7 @@ async function startMetricsServer(opts) {
22369
23329
  return;
22370
23330
  }
22371
23331
  const url = req.url.split("?")[0];
22372
- if (url === path19) {
23332
+ if (url === path21) {
22373
23333
  let body;
22374
23334
  try {
22375
23335
  body = renderPrometheus(opts.sink.snapshot());
@@ -22415,14 +23375,14 @@ async function startMetricsServer(opts) {
22415
23375
  const { createServer } = await import('http');
22416
23376
  server = createServer(listener);
22417
23377
  }
22418
- await new Promise((resolve5, reject) => {
23378
+ await new Promise((resolve6, reject) => {
22419
23379
  const onError = (err) => {
22420
23380
  server.off("listening", onListening);
22421
23381
  reject(err);
22422
23382
  };
22423
23383
  const onListening = () => {
22424
23384
  server.off("error", onError);
22425
- resolve5();
23385
+ resolve6();
22426
23386
  };
22427
23387
  server.once("error", onError);
22428
23388
  server.once("listening", onListening);
@@ -22433,9 +23393,9 @@ async function startMetricsServer(opts) {
22433
23393
  const protocol = useHttps ? "https" : "http";
22434
23394
  return {
22435
23395
  port: boundPort,
22436
- url: `${protocol}://${host}:${boundPort}${path19}`,
22437
- close: () => new Promise((resolve5, reject) => {
22438
- server.close((err) => err ? reject(err) : resolve5());
23396
+ url: `${protocol}://${host}:${boundPort}${path21}`,
23397
+ close: () => new Promise((resolve6, reject) => {
23398
+ server.close((err) => err ? reject(err) : resolve6());
22439
23399
  })
22440
23400
  };
22441
23401
  }
@@ -23077,6 +24037,19 @@ var miniMaxVisionServer = () => ({
23077
24037
  allowedTools: ["understand_image"],
23078
24038
  permission: "auto"
23079
24039
  });
24040
+ var sshManagerServer = () => ({
24041
+ name: "ssh",
24042
+ description: "Remote SSH management \u2014 execute commands, transfer files, tunnels, health checks (mcp-ssh-manager)",
24043
+ transport: "stdio",
24044
+ command: "npx",
24045
+ args: ["-y", "mcp-ssh-manager"],
24046
+ env: {
24047
+ MCP_SSH_COMPACT_JSON: "true",
24048
+ MCP_SSH_DEFAULT_TIMEOUT: "120000"
24049
+ },
24050
+ permission: "confirm",
24051
+ requestTimeoutMs: 18e4
24052
+ });
23080
24053
  var allServers = () => ({
23081
24054
  filesystem: { ...filesystemServer(), enabled: false },
23082
24055
  github: { ...githubServer(), enabled: false },
@@ -23090,9 +24063,10 @@ var allServers = () => ({
23090
24063
  sentinel: { ...sentinelServer(), enabled: false },
23091
24064
  "zai-vision": { ...zaiVisionServer(), enabled: false },
23092
24065
  "minimax-vision": { ...miniMaxVisionServer(), enabled: false },
23093
- playwright: { ...playwrightServer(), enabled: false }
24066
+ playwright: { ...playwrightServer(), enabled: false },
24067
+ ssh: { ...sshManagerServer(), enabled: false }
23094
24068
  });
23095
24069
 
23096
- export { AGENTS_BY_PHASE, AGENT_CATALOG, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, ConfigMigrationError, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPermissionPolicy, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, Director, DirectorStateCheckpoint, DoneConditionChecker, EternalAutonomyEngine, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FleetBus, FleetSpawnBudgetError, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, IntelligentCompactor, LLMSelector, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, SddParallelRun, SddTaskDecomposer, SelectiveCompactor, SessionAnalyzer, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, ToolExecutor, addPlanItem, allServers, analyzeCriticalPath, applyRosterBudget, attachAutoExtend, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, clearPlan, composeDirectorPrompt, composeSubagentPrompt, context7Server, contextManagerTool, createAutoExecutor, createContextManagerTool, createDelegateTool, createMessage, createSessionEventBridge, createStrategyCompactor, decryptConfigSecrets, deriveTodosFromPlanItem, dispatchAgent, emptyPlan, encryptConfigSecrets, everArtServer, filesystemServer, formatPlan, formatPlanTemplates, getAgentDefinition, getPlanTemplate, getTemplate, githubServer, googleMapsServer, listPlanTemplates, listTemplates, loadDirectorState, loadPlan, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeRollUpTool, makeSpawnTool, makeTerminateTool, migratePlaintextSecrets, miniMaxVisionServer, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, resolveAuditLevel, resolveSessionLoggingConfig, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, savePlan, saveTodosCheckpoint, scoreAgents, sentinelServer, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, templateToMarkdown, wireMetricsToEvents, zaiVisionServer };
24070
+ export { AGENTS_BY_PHASE, AGENT_CATALOG, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, ConfigMigrationError, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPermissionPolicy, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, Director, DirectorStateCheckpoint, DoneConditionChecker, EternalAutonomyEngine, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FleetBus, FleetSpawnBudgetError, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, IntelligentCompactor, LLMSelector, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, SddParallelRun, SddTaskDecomposer, SelectiveCompactor, SessionAnalyzer, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, ToolExecutor, addPlanItem, allServers, analyzeCriticalPath, applyRosterBudget, attachAutoExtend, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, clearPlan, composeDirectorPrompt, composeSubagentPrompt, context7Server, contextManagerTool, createAutoExecutor, createContextManagerTool, createDelegateTool, createMessage, createSessionEventBridge, createStrategyCompactor, decryptConfigSecrets, deriveTodosFromPlanItem, describeCatalogModel, dispatchAgent, emptyPlan, encryptConfigSecrets, everArtServer, filesystemServer, formatPlan, formatPlanTemplates, getAgentDefinition, getPlanTemplate, getTemplate, githubServer, googleMapsServer, listPlanTemplates, listTemplates, loadDirectorState, loadPlan, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeRollUpTool, makeSpawnTool, makeTerminateTool, migratePlaintextSecrets, miniMaxVisionServer, playwrightServer, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, resolveAuditLevel, resolveProviderModelList, resolveSessionLoggingConfig, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, savePlan, saveTodosCheckpoint, scoreAgents, sentinelServer, setPlanItemStatus, slackServer, sshManagerServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, templateToMarkdown, wireMetricsToEvents, zaiVisionServer };
23097
24071
  //# sourceMappingURL=index.js.map
23098
24072
  //# sourceMappingURL=index.js.map