@wrongstack/core 0.286.0 → 0.287.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 (251) hide show
  1. package/dist/boot.d.ts.map +1 -1
  2. package/dist/coordination/brain-monitor.d.ts +15 -0
  3. package/dist/coordination/brain-monitor.d.ts.map +1 -1
  4. package/dist/coordination/delegate-tool.d.ts +8 -14
  5. package/dist/coordination/delegate-tool.d.ts.map +1 -1
  6. package/dist/coordination/director/director-btw-notes.d.ts +20 -0
  7. package/dist/coordination/director/director-btw-notes.d.ts.map +1 -0
  8. package/dist/coordination/director/director-collab.d.ts +40 -0
  9. package/dist/coordination/director/director-collab.d.ts.map +1 -0
  10. package/dist/coordination/director.d.ts +27 -8
  11. package/dist/coordination/director.d.ts.map +1 -1
  12. package/dist/coordination/fleet-manager.d.ts +21 -0
  13. package/dist/coordination/fleet-manager.d.ts.map +1 -1
  14. package/dist/coordination/fleet.d.ts +2 -3
  15. package/dist/coordination/fleet.d.ts.map +1 -1
  16. package/dist/coordination/global-mailbox.d.ts +36 -22
  17. package/dist/coordination/global-mailbox.d.ts.map +1 -1
  18. package/dist/coordination/icoordinator.d.ts +1 -3
  19. package/dist/coordination/icoordinator.d.ts.map +1 -1
  20. package/dist/coordination/index.js +708 -442
  21. package/dist/coordination/index.js.map +4 -4
  22. package/dist/coordination/mail-tools.d.ts.map +1 -1
  23. package/dist/coordination/mailbox-message-codec.d.ts +21 -1
  24. package/dist/coordination/mailbox-message-codec.d.ts.map +1 -1
  25. package/dist/coordination/mailbox-types.d.ts +42 -1
  26. package/dist/coordination/mailbox-types.d.ts.map +1 -1
  27. package/dist/core/agent-loop.d.ts.map +1 -1
  28. package/dist/core/agent-response.d.ts +10 -1
  29. package/dist/core/agent-response.d.ts.map +1 -1
  30. package/dist/core/agent-tools.d.ts.map +1 -1
  31. package/dist/core/agent.d.ts +8 -0
  32. package/dist/core/agent.d.ts.map +1 -1
  33. package/dist/core/context.d.ts +25 -0
  34. package/dist/core/context.d.ts.map +1 -1
  35. package/dist/core/conversation-state.d.ts +6 -0
  36. package/dist/core/conversation-state.d.ts.map +1 -1
  37. package/dist/core/fallback-model.d.ts.map +1 -1
  38. package/dist/core/mailbox-loop.d.ts +12 -5
  39. package/dist/core/mailbox-loop.d.ts.map +1 -1
  40. package/dist/core/system-prompt-builder.d.ts +14 -6
  41. package/dist/core/system-prompt-builder.d.ts.map +1 -1
  42. package/dist/defaults/index.d.ts +0 -1
  43. package/dist/defaults/index.d.ts.map +1 -1
  44. package/dist/defaults/index.js +834 -1465
  45. package/dist/defaults/index.js.map +4 -4
  46. package/dist/execution/auto-compaction-middleware.d.ts +8 -1
  47. package/dist/execution/auto-compaction-middleware.d.ts.map +1 -1
  48. package/dist/execution/compaction-core.d.ts.map +1 -1
  49. package/dist/execution/council-brain.d.ts.map +1 -1
  50. package/dist/execution/council-orchestrator.d.ts +32 -0
  51. package/dist/execution/council-orchestrator.d.ts.map +1 -0
  52. package/dist/execution/council-personas.d.ts +19 -0
  53. package/dist/execution/council-personas.d.ts.map +1 -0
  54. package/dist/execution/council-profiles.d.ts +34 -0
  55. package/dist/execution/council-profiles.d.ts.map +1 -0
  56. package/dist/execution/council-prompts.d.ts +21 -0
  57. package/dist/execution/council-prompts.d.ts.map +1 -0
  58. package/dist/execution/council-resolution.d.ts +57 -0
  59. package/dist/execution/council-resolution.d.ts.map +1 -0
  60. package/dist/execution/design-kit-loader.d.ts.map +1 -1
  61. package/dist/execution/enhance-recovery.d.ts +35 -0
  62. package/dist/execution/enhance-recovery.d.ts.map +1 -0
  63. package/dist/execution/error-handler.d.ts.map +1 -1
  64. package/dist/execution/index.d.ts +6 -0
  65. package/dist/execution/index.d.ts.map +1 -1
  66. package/dist/execution/index.js +1738 -161
  67. package/dist/execution/index.js.map +4 -4
  68. package/dist/execution/intelligent-compactor.d.ts +18 -7
  69. package/dist/execution/intelligent-compactor.d.ts.map +1 -1
  70. package/dist/execution/one-shot-llm.d.ts +52 -0
  71. package/dist/execution/one-shot-llm.d.ts.map +1 -0
  72. package/dist/execution/prompt-enhancer.d.ts +23 -4
  73. package/dist/execution/prompt-enhancer.d.ts.map +1 -1
  74. package/dist/execution/prompt-enhancer.js +24 -6
  75. package/dist/execution/prompt-enhancer.js.map +2 -2
  76. package/dist/execution/selective-compactor.d.ts +4 -4
  77. package/dist/execution/strategy-compactor.d.ts +7 -0
  78. package/dist/execution/strategy-compactor.d.ts.map +1 -1
  79. package/dist/execution/tool-executor.d.ts +19 -5
  80. package/dist/execution/tool-executor.d.ts.map +1 -1
  81. package/dist/hooks/registry.d.ts.map +1 -1
  82. package/dist/hooks/runner.d.ts.map +1 -1
  83. package/dist/hooks/shell-executor.d.ts.map +1 -1
  84. package/dist/hq/alerts.d.ts +5 -0
  85. package/dist/hq/alerts.d.ts.map +1 -1
  86. package/dist/hq/auth-store.d.ts +17 -7
  87. package/dist/hq/auth-store.d.ts.map +1 -1
  88. package/dist/hq/commands.d.ts +8 -4
  89. package/dist/hq/commands.d.ts.map +1 -1
  90. package/dist/hq/cost-bridge.d.ts +5 -3
  91. package/dist/hq/cost-bridge.d.ts.map +1 -1
  92. package/dist/hq/fleet-bridge.d.ts.map +1 -1
  93. package/dist/hq/index.d.ts +0 -1
  94. package/dist/hq/index.d.ts.map +1 -1
  95. package/dist/hq/index.js +471 -294
  96. package/dist/hq/index.js.map +4 -4
  97. package/dist/hq/persistence.d.ts +42 -0
  98. package/dist/hq/persistence.d.ts.map +1 -1
  99. package/dist/hq/protocol/brain.d.ts +24 -0
  100. package/dist/hq/protocol/brain.d.ts.map +1 -0
  101. package/dist/hq/protocol/browser.d.ts +12 -0
  102. package/dist/hq/protocol/browser.d.ts.map +1 -0
  103. package/dist/hq/protocol/client.d.ts +69 -0
  104. package/dist/hq/protocol/client.d.ts.map +1 -0
  105. package/dist/hq/protocol/core.d.ts +130 -0
  106. package/dist/hq/protocol/core.d.ts.map +1 -0
  107. package/dist/hq/protocol/fleet.d.ts +73 -0
  108. package/dist/hq/protocol/fleet.d.ts.map +1 -0
  109. package/dist/hq/protocol/mailbox.d.ts +76 -0
  110. package/dist/hq/protocol/mailbox.d.ts.map +1 -0
  111. package/dist/hq/protocol/mcp.d.ts +51 -0
  112. package/dist/hq/protocol/mcp.d.ts.map +1 -0
  113. package/dist/hq/protocol/project.d.ts +26 -0
  114. package/dist/hq/protocol/project.d.ts.map +1 -0
  115. package/dist/hq/protocol/session.d.ts +153 -0
  116. package/dist/hq/protocol/session.d.ts.map +1 -0
  117. package/dist/hq/protocol/tool.d.ts +31 -0
  118. package/dist/hq/protocol/tool.d.ts.map +1 -0
  119. package/dist/hq/protocol.d.ts +10 -611
  120. package/dist/hq/protocol.d.ts.map +1 -1
  121. package/dist/index.d.ts +12 -2
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +4540 -2283
  124. package/dist/index.js.map +4 -4
  125. package/dist/infrastructure/index.js +13 -11
  126. package/dist/infrastructure/index.js.map +2 -2
  127. package/dist/infrastructure/token-counter.d.ts.map +1 -1
  128. package/dist/kernel/events/agent-events.d.ts +283 -0
  129. package/dist/kernel/events/agent-events.d.ts.map +1 -0
  130. package/dist/kernel/events/brain-events.d.ts +176 -0
  131. package/dist/kernel/events/brain-events.d.ts.map +1 -0
  132. package/dist/kernel/events/fleet-events.d.ts +39 -0
  133. package/dist/kernel/events/fleet-events.d.ts.map +1 -0
  134. package/dist/kernel/events/memory-events.d.ts +165 -0
  135. package/dist/kernel/events/memory-events.d.ts.map +1 -0
  136. package/dist/kernel/events/provider-events.d.ts +98 -0
  137. package/dist/kernel/events/provider-events.d.ts.map +1 -0
  138. package/dist/kernel/events/sdd-events.d.ts +118 -0
  139. package/dist/kernel/events/sdd-events.d.ts.map +1 -0
  140. package/dist/kernel/events/session-events.d.ts +227 -0
  141. package/dist/kernel/events/session-events.d.ts.map +1 -0
  142. package/dist/kernel/events/tool-events.d.ts +185 -0
  143. package/dist/kernel/events/tool-events.d.ts.map +1 -0
  144. package/dist/kernel/events/worktree-events.d.ts +58 -0
  145. package/dist/kernel/events/worktree-events.d.ts.map +1 -0
  146. package/dist/kernel/events.d.ts +10 -1298
  147. package/dist/kernel/events.d.ts.map +1 -1
  148. package/dist/kernel/index.js +22 -2
  149. package/dist/kernel/index.js.map +2 -2
  150. package/dist/kernel/run-controller.d.ts +15 -0
  151. package/dist/kernel/run-controller.d.ts.map +1 -1
  152. package/dist/mailbox-attach.d.ts.map +1 -1
  153. package/dist/models/index.js +19 -6
  154. package/dist/models/index.js.map +2 -2
  155. package/dist/models/llm-selector.d.ts +7 -0
  156. package/dist/models/llm-selector.d.ts.map +1 -1
  157. package/dist/plugins/auto-review-plugin.d.ts +36 -0
  158. package/dist/plugins/auto-review-plugin.d.ts.map +1 -0
  159. package/dist/plugins/chimera-plugin.d.ts +8 -0
  160. package/dist/plugins/chimera-plugin.d.ts.map +1 -1
  161. package/dist/security/index.js +7 -2
  162. package/dist/security/index.js.map +2 -2
  163. package/dist/security/secret-scrubber.d.ts.map +1 -1
  164. package/dist/skills/index.js.map +2 -2
  165. package/dist/storage/config-loader.d.ts.map +1 -1
  166. package/dist/storage/goal-kanban.d.ts +69 -0
  167. package/dist/storage/goal-kanban.d.ts.map +1 -0
  168. package/dist/storage/index.d.ts +1 -1
  169. package/dist/storage/index.d.ts.map +1 -1
  170. package/dist/storage/index.js +425 -717
  171. package/dist/storage/index.js.map +4 -4
  172. package/dist/storage/memory-consolidator.d.ts +9 -1
  173. package/dist/storage/memory-consolidator.d.ts.map +1 -1
  174. package/dist/tools/council-tool.d.ts +25 -0
  175. package/dist/tools/council-tool.d.ts.map +1 -0
  176. package/dist/tools/index.d.ts +2 -0
  177. package/dist/tools/index.d.ts.map +1 -1
  178. package/dist/tools/index.js +1884 -12
  179. package/dist/tools/index.js.map +4 -4
  180. package/dist/tools/one-shot-llm-tool.d.ts +51 -0
  181. package/dist/tools/one-shot-llm-tool.d.ts.map +1 -0
  182. package/dist/types/config.d.ts +27 -3
  183. package/dist/types/config.d.ts.map +1 -1
  184. package/dist/types/context-window.d.ts.map +1 -1
  185. package/dist/types/council.d.ts +134 -0
  186. package/dist/types/council.d.ts.map +1 -0
  187. package/dist/types/default-config.d.ts +3 -3
  188. package/dist/types/hooks.d.ts +14 -0
  189. package/dist/types/hooks.d.ts.map +1 -1
  190. package/dist/types/index.d.ts +2 -0
  191. package/dist/types/index.d.ts.map +1 -1
  192. package/dist/types/index.js +29 -8
  193. package/dist/types/index.js.map +3 -3
  194. package/dist/types/one-shot-llm.d.ts +128 -0
  195. package/dist/types/one-shot-llm.d.ts.map +1 -0
  196. package/dist/types/plugin.d.ts +6 -0
  197. package/dist/types/plugin.d.ts.map +1 -1
  198. package/dist/types/provider.d.ts +17 -0
  199. package/dist/types/provider.d.ts.map +1 -1
  200. package/dist/types/system-prompt.d.ts +2 -1
  201. package/dist/types/system-prompt.d.ts.map +1 -1
  202. package/dist/types/tool-executor.d.ts +10 -0
  203. package/dist/types/tool-executor.d.ts.map +1 -1
  204. package/dist/utils/atomic-write.d.ts.map +1 -1
  205. package/dist/utils/index.js +75 -8
  206. package/dist/utils/index.js.map +3 -3
  207. package/dist/utils/tool-wire-compact.d.ts +1 -0
  208. package/dist/utils/tool-wire-compact.d.ts.map +1 -1
  209. package/dist/utils/wstack-paths.d.ts +20 -4
  210. package/dist/utils/wstack-paths.d.ts.map +1 -1
  211. package/instructions/cli/next-task-predictor.md +5 -3
  212. package/instructions/coordination/subagent-baseline.md +8 -7
  213. package/instructions/leader-after-task.md +18 -9
  214. package/instructions/llm/council-judge.md +19 -0
  215. package/instructions/llm/council-voter.md +22 -0
  216. package/instructions/modes/architect.md +14 -9
  217. package/instructions/modes/audit-lite.md +9 -7
  218. package/instructions/modes/brief-system.md +12 -12
  219. package/instructions/modes/brief.md +7 -5
  220. package/instructions/modes/code-auditor.md +13 -8
  221. package/instructions/modes/code-reviewer.md +13 -8
  222. package/instructions/modes/debug-lite.md +7 -6
  223. package/instructions/modes/debugger.md +13 -9
  224. package/instructions/modes/devops.md +13 -9
  225. package/instructions/modes/plan-lite.md +7 -6
  226. package/instructions/modes/refactor-lite.md +7 -6
  227. package/instructions/modes/refactorer.md +14 -9
  228. package/instructions/modes/research-lite.md +8 -6
  229. package/instructions/modes/research-web.md +18 -55
  230. package/instructions/modes/review-lite.md +7 -6
  231. package/instructions/modes/teach-system.md +15 -15
  232. package/instructions/modes/teach.md +13 -12
  233. package/instructions/modes/test-lite.md +6 -5
  234. package/instructions/modes/tester.md +13 -9
  235. package/instructions/modes/ui-design.md +14 -8
  236. package/instructions/sections/tool/commit-hygiene.md +1 -7
  237. package/instructions/sections/tool/common-patterns.md +5 -5
  238. package/instructions/sections/tool/delegation-full.md +3 -31
  239. package/instructions/sections/tool/mailbox-compact.md +1 -1
  240. package/instructions/sections/tool/mailbox-full.md +6 -22
  241. package/instructions/system.md +278 -39
  242. package/package.json +2 -2
  243. package/skills/mailbox-bridge/SKILL.md +16 -7
  244. package/skills/output-standards/SKILL.md +16 -16
  245. package/skills/wrongstack-mailbox/SKILL.md +6 -0
  246. package/dist/coordination/director-construction.d.ts +0 -90
  247. package/dist/coordination/director-construction.d.ts.map +0 -1
  248. package/dist/hq/agent-bridge.d.ts +0 -27
  249. package/dist/hq/agent-bridge.d.ts.map +0 -1
  250. package/dist/storage/memory-store.d.ts +0 -119
  251. package/dist/storage/memory-store.d.ts.map +0 -1
@@ -13,6 +13,7 @@ function assertNever(x, message) {
13
13
  // src/utils/atomic-write.ts
14
14
  import { randomBytes } from "node:crypto";
15
15
  import * as fs from "node:fs/promises";
16
+ import { watch as watchDir } from "node:fs";
16
17
  import * as path from "node:path";
17
18
  async function atomicWrite(targetPath, content, opts = {}) {
18
19
  const dir = path.dirname(targetPath);
@@ -35,8 +36,8 @@ async function atomicWrite(targetPath, content, opts = {}) {
35
36
  }
36
37
  let mode;
37
38
  try {
38
- const stat13 = await fs.stat(targetPath);
39
- mode = stat13.mode & 511;
39
+ const stat12 = await fs.stat(targetPath);
40
+ mode = stat12.mode & 511;
40
41
  } catch {
41
42
  mode = opts.mode;
42
43
  }
@@ -80,17 +81,18 @@ async function withFileLock(targetPath, fn, opts = {}) {
80
81
  await fs.mkdir(dir, { recursive: true });
81
82
  continue;
82
83
  }
83
- if (code !== "EEXIST") throw err;
84
+ if (code !== "EEXIST" && code !== "EPERM") throw err;
84
85
  try {
85
- const stat13 = await fs.stat(lockPath);
86
- if (Date.now() - stat13.mtimeMs > staleMs) {
86
+ const stat12 = await fs.stat(lockPath);
87
+ if (Date.now() - stat12.mtimeMs > staleMs) {
87
88
  await fs.unlink(lockPath);
88
89
  continue;
89
90
  }
90
91
  } catch {
91
92
  continue;
92
93
  }
93
- if (Date.now() - started >= timeoutMs) {
94
+ const elapsed = Date.now() - started;
95
+ if (elapsed >= timeoutMs) {
94
96
  throw new FsError({
95
97
  message: `Timed out waiting for file lock: ${targetPath}`,
96
98
  code: "FS_ATOMIC_WRITE_FAILED",
@@ -98,7 +100,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
98
100
  context: { timeoutMs }
99
101
  });
100
102
  }
101
- await new Promise((resolve12) => setTimeout(resolve12, 25));
103
+ await waitForLockRelease(lockPath, timeoutMs - elapsed);
102
104
  }
103
105
  }
104
106
  try {
@@ -114,6 +116,50 @@ async function withFileLock(targetPath, fn, opts = {}) {
114
116
  }
115
117
  }
116
118
  }
119
+ async function waitForLockRelease(lockPath, remainingMs) {
120
+ const parentDir = path.dirname(lockPath);
121
+ const lockName = path.basename(lockPath);
122
+ const intervalMs = Math.min(remainingMs, 100);
123
+ return new Promise((resolve12) => {
124
+ let settled = false;
125
+ let watcher = null;
126
+ const timer = setTimeout(() => {
127
+ settled = true;
128
+ watcher?.close();
129
+ resolve12();
130
+ }, intervalMs);
131
+ try {
132
+ watcher = watchDir(parentDir, (eventType, filename) => {
133
+ if (settled) return;
134
+ if (filename === lockName && (eventType === "rename" || eventType === "change")) {
135
+ settled = true;
136
+ clearTimeout(timer);
137
+ watcher?.close();
138
+ resolve12();
139
+ }
140
+ });
141
+ } catch {
142
+ clearTimeout(timer);
143
+ if (!settled) {
144
+ settled = true;
145
+ setTimeout(resolve12, Math.min(remainingMs, 25));
146
+ }
147
+ return;
148
+ }
149
+ fs.access(lockPath).then(
150
+ () => {
151
+ },
152
+ () => {
153
+ if (!settled) {
154
+ settled = true;
155
+ clearTimeout(timer);
156
+ watcher?.close();
157
+ resolve12();
158
+ }
159
+ }
160
+ );
161
+ });
162
+ }
117
163
  var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
118
164
  async function renameWithRetry(from, to) {
119
165
  if (process.platform !== "win32") {
@@ -572,8 +618,8 @@ async function expandGlob(pattern) {
572
618
  for (const e of entries) {
573
619
  const full = `${dir}${SEP}${e}`;
574
620
  try {
575
- const stat13 = await fsp.stat(full);
576
- if (stat13.isDirectory()) await walk3(full, rest);
621
+ const stat12 = await fsp.stat(full);
622
+ if (stat12.isDirectory()) await walk3(full, rest);
577
623
  } catch {
578
624
  }
579
625
  }
@@ -590,8 +636,8 @@ async function expandGlob(pattern) {
590
636
  if (entries.includes(seg)) {
591
637
  const full = `${dir}${SEP}${seg}`;
592
638
  try {
593
- const stat13 = await fsp.stat(full);
594
- if (stat13.isDirectory()) await walk3(full, rest);
639
+ const stat12 = await fsp.stat(full);
640
+ if (stat12.isDirectory()) await walk3(full, rest);
595
641
  } catch {
596
642
  }
597
643
  }
@@ -782,10 +828,10 @@ function validateAgainstSchema(value, schema) {
782
828
  return { ok: errors.length === 0, errors };
783
829
  }
784
830
  var MAX_SCHEMA_DEPTH = 64;
785
- function walk(value, schema, path29, errors, depth) {
831
+ function walk(value, schema, path28, errors, depth) {
786
832
  if (depth > MAX_SCHEMA_DEPTH) {
787
833
  errors.push({
788
- path: path29 || "<root>",
834
+ path: path28 || "<root>",
789
835
  message: `schema nesting exceeds maximum depth (${MAX_SCHEMA_DEPTH})`
790
836
  });
791
837
  return;
@@ -793,7 +839,7 @@ function walk(value, schema, path29, errors, depth) {
793
839
  if (schema.enum !== void 0) {
794
840
  if (!enumIncludes(schema.enum, value)) {
795
841
  errors.push({
796
- path: path29 || "<root>",
842
+ path: path28 || "<root>",
797
843
  message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
798
844
  });
799
845
  return;
@@ -802,7 +848,7 @@ function walk(value, schema, path29, errors, depth) {
802
848
  if (typeof schema.type === "string") {
803
849
  if (!checkType(value, schema.type)) {
804
850
  errors.push({
805
- path: path29 || "<root>",
851
+ path: path28 || "<root>",
806
852
  message: `expected ${schema.type}, got ${describeType(value)} (${previewValue(value)})`
807
853
  });
808
854
  return;
@@ -814,7 +860,7 @@ function walk(value, schema, path29, errors, depth) {
814
860
  if (!(req in obj)) {
815
861
  const expected = schema.properties?.[req]?.type;
816
862
  errors.push({
817
- path: joinPath(path29, req),
863
+ path: joinPath(path28, req),
818
864
  message: `required property missing${typeof expected === "string" ? ` (expected ${expected})` : ""}`
819
865
  });
820
866
  }
@@ -822,14 +868,14 @@ function walk(value, schema, path29, errors, depth) {
822
868
  if (schema.properties) {
823
869
  for (const [key, subSchema] of Object.entries(schema.properties)) {
824
870
  if (key in obj) {
825
- walk(obj[key], subSchema, joinPath(path29, key), errors, depth + 1);
871
+ walk(obj[key], subSchema, joinPath(path28, key), errors, depth + 1);
826
872
  }
827
873
  }
828
874
  }
829
875
  }
830
876
  if (schema.type === "array" && Array.isArray(value) && schema.items) {
831
877
  for (let i = 0; i < value.length; i++) {
832
- walk(value[i], schema.items, `${path29}[${i}]`, errors, depth + 1);
878
+ walk(value[i], schema.items, `${path28}[${i}]`, errors, depth + 1);
833
879
  }
834
880
  }
835
881
  }
@@ -1287,8 +1333,8 @@ function subjectForToolInput(toolName, input, subjectKey) {
1287
1333
  }
1288
1334
 
1289
1335
  // src/utils/tool-wire-compact.ts
1290
- var TOOL_DESCRIPTION_MAX_CHARS = 640;
1291
- var SCHEMA_DESCRIPTION_MAX_CHARS = 180;
1336
+ var TOOL_DESCRIPTION_MAX_CHARS = 400;
1337
+ var SCHEMA_DESCRIPTION_MAX_CHARS = 120;
1292
1338
  var compactCache = /* @__PURE__ */ new WeakMap();
1293
1339
  function compactToolDefinitionForWire(tool, opts = {}) {
1294
1340
  const useDefaultOptions = opts.descriptionMaxChars === void 0 && opts.schemaDescriptionMaxChars === void 0;
@@ -2208,14 +2254,34 @@ function resolveToolResultRenderMode(modes, toolName) {
2208
2254
 
2209
2255
  // src/utils/wstack-paths.ts
2210
2256
  import { createHash } from "node:crypto";
2257
+ import * as fs2 from "node:fs";
2211
2258
  import * as os from "node:os";
2212
2259
  import * as path3 from "node:path";
2260
+ function canonicalProjectRoot(absRoot) {
2261
+ const checkoutRoot = path3.resolve(absRoot);
2262
+ const dotGit = path3.join(checkoutRoot, ".git");
2263
+ try {
2264
+ if (!fs2.statSync(dotGit).isFile()) return checkoutRoot;
2265
+ const gitDirLine = fs2.readFileSync(dotGit, "utf8").trim();
2266
+ const match = /^gitdir:\s*(.+)$/i.exec(gitDirLine);
2267
+ if (!match?.[1]) return checkoutRoot;
2268
+ const gitDir = path3.resolve(checkoutRoot, match[1].trim());
2269
+ const commonDirFile = path3.join(gitDir, "commondir");
2270
+ if (!fs2.statSync(commonDirFile).isFile()) return checkoutRoot;
2271
+ const commonDir = path3.resolve(gitDir, fs2.readFileSync(commonDirFile, "utf8").trim());
2272
+ if (path3.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
2273
+ return path3.dirname(commonDir);
2274
+ } catch {
2275
+ return checkoutRoot;
2276
+ }
2277
+ }
2213
2278
  function projectHash(absRoot) {
2214
- return createHash("sha256").update(path3.resolve(absRoot)).digest("hex").slice(0, 12);
2279
+ return createHash("sha256").update(canonicalProjectRoot(absRoot)).digest("hex").slice(0, 12);
2215
2280
  }
2216
2281
  function projectSlug(absRoot) {
2217
- const base = slugify2(path3.basename(absRoot));
2218
- const hash = createHash("sha256").update(path3.resolve(absRoot)).digest("hex").slice(0, 6);
2282
+ const identityRoot = canonicalProjectRoot(absRoot);
2283
+ const base = slugify2(path3.basename(identityRoot));
2284
+ const hash = createHash("sha256").update(identityRoot).digest("hex").slice(0, 6);
2219
2285
  return `${base}-${hash}`;
2220
2286
  }
2221
2287
  function slugify2(name) {
@@ -3526,7 +3592,7 @@ var TOOLS = {
3526
3592
  };
3527
3593
 
3528
3594
  // src/coordination/agents/agent-prompts.ts
3529
- import { readFileSync, statSync } from "node:fs";
3595
+ import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
3530
3596
  import * as os2 from "node:os";
3531
3597
  import * as path4 from "node:path";
3532
3598
  import { fileURLToPath } from "node:url";
@@ -3541,7 +3607,7 @@ function agentPrompt(id) {
3541
3607
  let resolved = "";
3542
3608
  for (const dir of agentPromptDirCandidates(envDir)) {
3543
3609
  try {
3544
- resolved = readFileSync(path4.join(dir, fileName), "utf8").trimEnd();
3610
+ resolved = readFileSync2(path4.join(dir, fileName), "utf8").trimEnd();
3545
3611
  break;
3546
3612
  } catch {
3547
3613
  }
@@ -3571,7 +3637,7 @@ function agentPromptDirCandidates(envDir) {
3571
3637
  }
3572
3638
  function isDirectory(candidate) {
3573
3639
  try {
3574
- return statSync(candidate).isDirectory();
3640
+ return statSync2(candidate).isDirectory();
3575
3641
  } catch {
3576
3642
  return false;
3577
3643
  }
@@ -5162,19 +5228,19 @@ var FLEET_ROSTER = {
5162
5228
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.config])
5163
5229
  )
5164
5230
  };
5165
- var DEFAULT_IDLE_TIMEOUT_MS = 15 * 60 * 1e3;
5231
+ var DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
5166
5232
  var FLEET_ROSTER_BUDGETS = {
5167
- "audit-log": { timeoutMs: 7.5 * 60 * 60 * 1e3, maxIterations: 5e3, maxToolCalls: 15e3 },
5168
- "bug-hunter": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
5169
- "refactor-planner": { timeoutMs: 7.5 * 60 * 60 * 1e3, maxIterations: 6e3, maxToolCalls: 18e3 },
5170
- "security-scanner": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
5171
- critic: { timeoutMs: 5 * 60 * 60 * 1e3, maxIterations: 4e3, maxToolCalls: 12e3 },
5233
+ "audit-log": { timeoutMs: 3 * 60 * 60 * 1e3, maxIterations: 2500, maxToolCalls: 7500 },
5234
+ "bug-hunter": { timeoutMs: 5 * 60 * 60 * 1e3, maxIterations: 4e3, maxToolCalls: 1e4 },
5235
+ "refactor-planner": { timeoutMs: 4 * 60 * 60 * 1e3, maxIterations: 3e3, maxToolCalls: 9e3 },
5236
+ "security-scanner": { timeoutMs: 5 * 60 * 60 * 1e3, maxIterations: 4e3, maxToolCalls: 1e4 },
5237
+ critic: { timeoutMs: 3 * 60 * 60 * 1e3, maxIterations: 2e3, maxToolCalls: 6e3 },
5172
5238
  "shadow-agent": {
5173
5239
  idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
5174
- maxIterations: 2e3,
5175
- maxToolCalls: 5e3,
5176
- maxTokens: 6e4,
5177
- maxCostUsd: 1
5240
+ maxIterations: 1e3,
5241
+ maxToolCalls: 2500,
5242
+ maxTokens: 3e4,
5243
+ maxCostUsd: 0.5
5178
5244
  },
5179
5245
  ...Object.fromEntries(
5180
5246
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.budget])
@@ -5182,8 +5248,8 @@ var FLEET_ROSTER_BUDGETS = {
5182
5248
  };
5183
5249
  var GENERIC_SUBAGENT_BUDGET = {
5184
5250
  idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
5185
- maxIterations: 5e3,
5186
- maxToolCalls: 15e3
5251
+ maxIterations: 2500,
5252
+ maxToolCalls: 7500
5187
5253
  };
5188
5254
  function applyRosterBudget(cfg) {
5189
5255
  const roleBudget = cfg.role ? FLEET_ROSTER_BUDGETS[cfg.role] : void 0;
@@ -5239,72 +5305,71 @@ function createDelegateTool(opts) {
5239
5305
  properties: {
5240
5306
  task: {
5241
5307
  type: "string",
5242
- description: "What the subagent should do \u2014 natural language, complete sentence(s). The subagent has its own tool slice, its own LLM call, and returns when its task is done."
5308
+ description: "What the subagent should do \u2014 natural language, complete sentence(s)."
5243
5309
  },
5244
5310
  role: {
5245
5311
  type: "string",
5246
- description: rosterIds.length > 0 ? `Roster role (preferred). One of: ${rosterIds.join(", ")}. Picks a pre-tuned config (prompt, budgets, tools) for that role.` : "No roster is configured \u2014 pass `name` instead.",
5247
- enum: rosterIds.length > 0 ? rosterIds : void 0
5312
+ description: rosterIds.length > 0 ? "Roster role id. Common: bug-hunter, security-scanner, refactor-planner, critic, audit-log, executor, shadow-agent, architect." : "No roster configured \u2014 pass `name` instead."
5248
5313
  },
5249
5314
  name: {
5250
5315
  type: "string",
5251
- description: "Display name for free-form subagents (not using a roster role). The subagent gets a large default budget (3h, 5000 iter, 15000 tool calls). Required when `role` is omitted."
5316
+ description: "Display name for free-form subagents (no roster role). Required when `role` is omitted."
5252
5317
  },
5253
5318
  provider: {
5254
5319
  type: "string",
5255
- description: 'Provider id (e.g. "anthropic", "openai"). Defaults to the host provider when omitted.'
5320
+ description: 'Provider id (e.g. "anthropic", "openai"). Defaults to host provider.'
5256
5321
  },
5257
5322
  model: {
5258
5323
  type: "string",
5259
- description: "Model id within the provider. Defaults to the host model when omitted."
5324
+ description: "Model id within the provider. Defaults to host model."
5260
5325
  },
5261
5326
  systemPromptOverride: {
5262
5327
  type: "string",
5263
- description: "Optional extra prompt text appended to the role baseline."
5328
+ description: "Extra prompt text appended to the role baseline."
5264
5329
  },
5265
5330
  timeoutMs: {
5266
5331
  type: "number",
5267
5332
  minimum: 1,
5268
- description: `Wall-clock budget for this delegate in milliseconds. No hard cap \u2014 set as high as the task realistically needs (a monorepo audit can take hours, a single-file lint takes seconds). Default ${Math.round(defaultTimeoutMs / 1e3 / 60)} minutes.`
5333
+ description: `Wall-clock budget in ms (default ${Math.round(defaultTimeoutMs / 1e3 / 60)} min). No hard cap \u2014 set as high as the task needs.`
5269
5334
  },
5270
5335
  maxIterations: {
5271
5336
  type: "number",
5272
5337
  minimum: 1,
5273
- description: "Maximum LLM iterations the subagent may take. Unset = use the role/coordinator default. Raise this for tasks with many tool-think-tool cycles (deep code analysis, multi-file refactors)."
5338
+ description: "Maximum LLM iterations. Unset = role default. Raise for deep multi-step tasks."
5274
5339
  },
5275
5340
  maxToolCalls: {
5276
5341
  type: "number",
5277
5342
  minimum: 1,
5278
- description: "Maximum number of tool invocations the subagent may make. Unset = use the role/coordinator default. Raise this for tasks that touch many files (large grep + read + report)."
5343
+ description: "Maximum tool invocations. Unset = role default. Raise for file-heavy tasks."
5279
5344
  },
5280
5345
  idleTimeoutMs: {
5281
5346
  type: "number",
5282
5347
  minimum: 1,
5283
- description: "Idle timeout in ms: reap the subagent after this long with no activity. Resets on every iteration/tool call. Unset = use the role/coordinator default."
5348
+ description: "Idle timeout in ms. Resets on activity. Unset = role default."
5284
5349
  },
5285
5350
  maxTokens: {
5286
5351
  type: "number",
5287
5352
  minimum: 1,
5288
- description: "Maximum total tokens (input + output) the subagent may use. Unset = use the role/coordinator default."
5353
+ description: "Max total tokens (input+output). Unset = role default."
5289
5354
  },
5290
5355
  maxCostUsd: {
5291
5356
  type: "number",
5292
5357
  minimum: 0,
5293
- description: "Maximum estimated USD cost the subagent may incur. Unset = use the role/coordinator default."
5358
+ description: "Max estimated USD cost. Unset = role default."
5294
5359
  },
5295
5360
  maxHandoffs: {
5296
5361
  type: "number",
5297
5362
  minimum: 0,
5298
5363
  maximum: 8,
5299
- description: "Maximum fresh-worker continuations after partial completion or budget exhaustion. Default 1; set 0 to disable. Each successor receives the prior structured/partial report and is told to continue only the remaining work."
5364
+ description: "Max fresh-worker continuations after budget exhaustion. Default 1. Each gets the prior partial report."
5300
5365
  }
5301
5366
  },
5302
5367
  required: ["task"]
5303
5368
  };
5304
5369
  return {
5305
5370
  name: "delegate",
5306
- description: "Hand a discrete piece of work to a dedicated subagent and wait for its result. The subagent has its own context, its own LLM call, and a generous, auto-extending budget. If it reports explicit partial completion or still exhausts a recoverable budget, delegate can continue the remaining work with a fresh subagent (bounded by `maxHandoffs`, default 1). Workers may ask the leader to spawn a parallel helper through the mailbox, but cannot recursively spawn agents themselves. Use specialized roster roles when possible; otherwise pass `name` + `task`. Call multiple delegates in parallel through the provider's parallel-tool-call surface for independent work.",
5307
- usageHint: "Set `task` to a complete instruction. Either pick `role` from the roster or pass `name` for a free-form coding agent. Defaults are intentionally generous; override `timeoutMs`, `maxIterations`, `maxToolCalls`, or `maxHandoffs` only when the task warrants it. Returns the final TaskResult plus a `handoffs` history when fresh workers continued partial work. Requires Director mode in normal CLI sessions.",
5371
+ description: "Hand a piece of work to a subagent and wait for its result. Has own context, LLM call, auto-extending budget. Can continue remaining work with a fresh subagent on partial completion (maxHandoffs, default 1). Workers cannot recursively spawn. Use roster roles when possible; otherwise pass `name` + `task`.",
5372
+ usageHint: "Set `task` to a complete instruction. Pick `role` from roster or pass `name` for free-form. Override `timeoutMs`/`maxIterations`/`maxToolCalls` only when needed.",
5308
5373
  permission: "auto",
5309
5374
  mutating: false,
5310
5375
  managesOwnTimeout: true,
@@ -5331,10 +5396,9 @@ function createDelegateTool(opts) {
5331
5396
  director = await opts.host.promoteToDirector();
5332
5397
  }
5333
5398
  if (!director) {
5334
- const reason = opts.host.getPromotionBlockReason?.();
5335
5399
  return {
5336
5400
  ok: false,
5337
- error: reason ?? "Director could not be activated \u2014 multi-agent host already running in legacy non-director mode. Restart with `--director` for fleet support."
5401
+ error: "Director could not be activated \u2014 fleet orchestration is unavailable."
5338
5402
  };
5339
5403
  }
5340
5404
  const timeoutMs = i.timeoutMs ?? defaultTimeoutMs;
@@ -6014,6 +6078,63 @@ function resolveMaxSpawnDepth(requested) {
6014
6078
  return Math.max(0, Math.min(HARD_MAX_SPAWN_DEPTH, Math.floor(requested)));
6015
6079
  }
6016
6080
 
6081
+ // src/coordination/director/director-errors.ts
6082
+ var FleetSpawnBudgetError = class extends Error {
6083
+ kind;
6084
+ limit;
6085
+ observed;
6086
+ constructor(kind, limit, observed, message) {
6087
+ const defaultMsg = kind === "max_spawns" ? `Director spawn budget exceeded: tried to spawn #${observed} but maxSpawns is ${limit}` : `Director spawn depth budget exceeded: this director is at depth ${observed} and maxSpawnDepth is ${limit}`;
6088
+ super(message ?? defaultMsg);
6089
+ this.name = "FleetSpawnBudgetError";
6090
+ this.kind = kind;
6091
+ this.limit = limit;
6092
+ this.observed = observed;
6093
+ }
6094
+ };
6095
+ var FleetCostCapError = class extends Error {
6096
+ kind;
6097
+ limit;
6098
+ observed;
6099
+ constructor(limit, observed) {
6100
+ super(
6101
+ `Director cost cap exceeded: total fleet spend ${observed.toFixed(4)} exceeds maxCostUsd ${limit.toFixed(4)}`
6102
+ );
6103
+ this.name = "FleetCostCapError";
6104
+ this.kind = "max_cost_usd";
6105
+ this.limit = limit;
6106
+ this.observed = observed;
6107
+ }
6108
+ };
6109
+ var FleetTokenCapError = class extends Error {
6110
+ kind;
6111
+ limit;
6112
+ observed;
6113
+ constructor(limit, observed) {
6114
+ super(
6115
+ `Director token cap exceeded: total fleet usage ${Math.round(observed)} tokens meets or exceeds maxTokens ${Math.round(limit)}`
6116
+ );
6117
+ this.name = "FleetTokenCapError";
6118
+ this.kind = "max_tokens";
6119
+ this.limit = limit;
6120
+ this.observed = observed;
6121
+ }
6122
+ };
6123
+ var FleetContextOverflowError = class extends Error {
6124
+ kind;
6125
+ limit;
6126
+ observed;
6127
+ constructor(limit, observed) {
6128
+ super(
6129
+ `Leader context overflow: leader has ${observed} tokens in flight (limit: ${limit}). Compact the leader context before spawning more subagents.`
6130
+ );
6131
+ this.name = "FleetContextOverflowError";
6132
+ this.kind = "max_context_load";
6133
+ this.limit = limit;
6134
+ this.observed = observed;
6135
+ }
6136
+ };
6137
+
6017
6138
  // src/coordination/collab-debug.ts
6018
6139
  import { EventEmitter } from "node:events";
6019
6140
  import * as fsp4 from "node:fs/promises";
@@ -6230,7 +6351,7 @@ var CollabSession = class extends EventEmitter {
6230
6351
  }
6231
6352
  for (const filePath of allFiles) {
6232
6353
  try {
6233
- const [content, stat13] = await Promise.all([
6354
+ const [content, stat12] = await Promise.all([
6234
6355
  fsp4.readFile(filePath, "utf8"),
6235
6356
  fsp4.stat(filePath)
6236
6357
  ]);
@@ -6240,8 +6361,8 @@ var CollabSession = class extends EventEmitter {
6240
6361
  path: filePath,
6241
6362
  content,
6242
6363
  language,
6243
- snapshotMtimeMs: stat13.mtimeMs,
6244
- snapshotSizeBytes: stat13.size
6364
+ snapshotMtimeMs: stat12.mtimeMs,
6365
+ snapshotSizeBytes: stat12.size
6245
6366
  });
6246
6367
  } catch {
6247
6368
  this.snapshot.files.push({ path: filePath, content: "", language: void 0 });
@@ -6660,9 +6781,9 @@ Emit each evaluation immediately. Do not wait until you have read all reports.`;
6660
6781
  for (const file of this.snapshot.files) {
6661
6782
  if (file.snapshotMtimeMs === void 0 && file.snapshotSizeBytes === void 0) continue;
6662
6783
  try {
6663
- const stat13 = await fsp4.stat(file.path);
6664
- const mtimeChanged = file.snapshotMtimeMs !== void 0 && stat13.mtimeMs > file.snapshotMtimeMs + 1;
6665
- const sizeChanged = file.snapshotSizeBytes !== void 0 && stat13.size !== file.snapshotSizeBytes;
6784
+ const stat12 = await fsp4.stat(file.path);
6785
+ const mtimeChanged = file.snapshotMtimeMs !== void 0 && stat12.mtimeMs > file.snapshotMtimeMs + 1;
6786
+ const sizeChanged = file.snapshotSizeBytes !== void 0 && stat12.size !== file.snapshotSizeBytes;
6666
6787
  if (mtimeChanged || sizeChanged) {
6667
6788
  warnings.push(`${file.path} changed after the collab snapshot was captured.`);
6668
6789
  }
@@ -6737,65 +6858,86 @@ Emit each evaluation immediately. Do not wait until you have read all reports.`;
6737
6858
  }
6738
6859
  };
6739
6860
 
6740
- // src/coordination/director/director-errors.ts
6741
- var FleetSpawnBudgetError = class extends Error {
6742
- kind;
6743
- limit;
6744
- observed;
6745
- constructor(kind, limit, observed, message) {
6746
- const defaultMsg = kind === "max_spawns" ? `Director spawn budget exceeded: tried to spawn #${observed} but maxSpawns is ${limit}` : `Director spawn depth budget exceeded: this director is at depth ${observed} and maxSpawnDepth is ${limit}`;
6747
- super(message ?? defaultMsg);
6748
- this.name = "FleetSpawnBudgetError";
6749
- this.kind = kind;
6750
- this.limit = limit;
6751
- this.observed = observed;
6861
+ // src/coordination/director/director-collab.ts
6862
+ var DirectorCollabController = class {
6863
+ constructor(deps) {
6864
+ this.deps = deps;
6752
6865
  }
6753
- };
6754
- var FleetCostCapError = class extends Error {
6755
- kind;
6756
- limit;
6757
- observed;
6758
- constructor(limit, observed) {
6759
- super(
6760
- `Director cost cap exceeded: total fleet spend ${observed.toFixed(4)} exceeds maxCostUsd ${limit.toFixed(4)}`
6761
- );
6762
- this.name = "FleetCostCapError";
6763
- this.kind = "max_cost_usd";
6764
- this.limit = limit;
6765
- this.observed = observed;
6866
+ deps;
6867
+ /** Active collab sessions tracked by sessionId (see spawn).
6868
+ * The tuple holds the session and its Director-registered listener unsubs.
6869
+ * Calling the unsubs on cancel/premature-cleanup prevents listener accumulation
6870
+ * on CollabSession (EventEmitter) across many spawn() calls. */
6871
+ activeSessions = /* @__PURE__ */ new Map();
6872
+ async spawn(options) {
6873
+ const session = new CollabSession(this.deps.director, this.deps.fleet, {
6874
+ ...options,
6875
+ onBudgetWarning: (alert) => {
6876
+ return options.onBudgetWarning?.(alert) ?? "ignore";
6877
+ }
6878
+ });
6879
+ const doneHandler = () => this.activeSessions.delete(session.sessionId);
6880
+ const errorHandler = () => this.activeSessions.delete(session.sessionId);
6881
+ session.on("session.done", doneHandler);
6882
+ session.on("session.error", errorHandler);
6883
+ const unsubs = [
6884
+ () => session.off("session.done", doneHandler),
6885
+ () => session.off("session.error", errorHandler)
6886
+ ];
6887
+ this.activeSessions.set(session.sessionId, { session, unsubs });
6888
+ return session.start();
6766
6889
  }
6767
- };
6768
- var FleetTokenCapError = class extends Error {
6769
- kind;
6770
- limit;
6771
- observed;
6772
- constructor(limit, observed) {
6773
- super(
6774
- `Director token cap exceeded: total fleet usage ${Math.round(observed)} tokens meets or exceeds maxTokens ${Math.round(limit)}`
6775
- );
6776
- this.name = "FleetTokenCapError";
6777
- this.kind = "max_tokens";
6778
- this.limit = limit;
6779
- this.observed = observed;
6890
+ cancel(sessionId, reason = "Director cancelled") {
6891
+ const entry = this.activeSessions.get(sessionId);
6892
+ if (!entry || entry.session.isCancelled()) return;
6893
+ for (const unsub of entry.unsubs) unsub();
6894
+ entry.session.cancel(reason);
6895
+ for (const [_role, subagentId] of entry.session.getSubagentIds()) {
6896
+ this.deps.coordinator.stop(subagentId).catch((err) => {
6897
+ this.deps.logger?.debug(
6898
+ `stop subagent ${subagentId} failed (may have already completed)`,
6899
+ {
6900
+ subagentId,
6901
+ err: err instanceof Error ? err.message : String(err)
6902
+ }
6903
+ );
6904
+ });
6905
+ }
6906
+ }
6907
+ onAlert(handler) {
6908
+ return this.deps.fleet.filter("collab.warning", (e) => {
6909
+ handler(e.payload);
6910
+ });
6911
+ }
6912
+ activeSessionIds() {
6913
+ return Array.from(this.activeSessions.keys());
6780
6914
  }
6781
6915
  };
6782
- var FleetContextOverflowError = class extends Error {
6783
- kind;
6784
- limit;
6785
- observed;
6786
- constructor(limit, observed) {
6787
- super(
6788
- `Leader context overflow: leader has ${observed} tokens in flight (limit: ${limit}). Compact the leader context before spawning more subagents.`
6789
- );
6790
- this.name = "FleetContextOverflowError";
6791
- this.kind = "max_context_load";
6792
- this.limit = limit;
6793
- this.observed = observed;
6916
+
6917
+ // src/coordination/director/director-btw-notes.ts
6918
+ var DirectorBtwNotes = class {
6919
+ notes = [];
6920
+ /** Stash a note (trimmed; empty ignored). Returns the pending count. Cap 20. */
6921
+ add(note) {
6922
+ const trimmed = note.trim();
6923
+ if (!trimmed) return this.notes.length;
6924
+ this.notes = [...this.notes, trimmed].slice(-20);
6925
+ return this.notes.length;
6926
+ }
6927
+ /** Read and clear all pending notes, in FIFO order (empty array when none). */
6928
+ drain() {
6929
+ const notes = this.notes;
6930
+ this.notes = [];
6931
+ return notes;
6932
+ }
6933
+ /** Peek at pending notes without consuming them. */
6934
+ peek() {
6935
+ return [...this.notes];
6794
6936
  }
6795
6937
  };
6796
6938
 
6797
6939
  // src/utils/instruction-file.ts
6798
- import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
6940
+ import { readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
6799
6941
  import * as path6 from "node:path";
6800
6942
  import { fileURLToPath as fileURLToPath2 } from "node:url";
6801
6943
  var textCache = /* @__PURE__ */ new Map();
@@ -6806,7 +6948,7 @@ function readBundledInstructionText(relativePath) {
6806
6948
  let resolved = "";
6807
6949
  for (const root of instructionRootCandidates()) {
6808
6950
  try {
6809
- resolved = readFileSync2(path6.join(root, relativePath), "utf8").trimEnd();
6951
+ resolved = readFileSync3(path6.join(root, relativePath), "utf8").trimEnd();
6810
6952
  break;
6811
6953
  } catch {
6812
6954
  }
@@ -6833,7 +6975,7 @@ function instructionRootCandidates() {
6833
6975
  }
6834
6976
  function isDirectory2(candidate) {
6835
6977
  try {
6836
- return statSync2(candidate).isDirectory();
6978
+ return statSync3(candidate).isDirectory();
6837
6979
  } catch {
6838
6980
  return false;
6839
6981
  }
@@ -10190,13 +10332,12 @@ var Director = class _Director {
10190
10332
  * coordinator from keeping workers alive for tasks that will never arrive.
10191
10333
  */
10192
10334
  workCompleteFlag = false;
10193
- /** Pending /btw notes stashed by the leader agent (see setLeaderBtwNote). */
10194
- _leaderBtwNotes = [];
10195
- /** Active collab sessions tracked by sessionId (see spawnCollab).
10196
- * The tuple holds the session and its Director-registered listener unsubs.
10197
- * Calling the unsubs on cancel/premature-cleanup prevents listener accumulation
10198
- * on CollabSession (EventEmitter) across many spawnCollab() calls. */
10199
- _activeCollabSessions = /* @__PURE__ */ new Map();
10335
+ /** Pending /btw notes stashed by the leader agent (see setLeaderBtwNote).
10336
+ * Owned by DirectorBtwNotes (R4); the public btw methods delegate to it. */
10337
+ btwNotes = new DirectorBtwNotes();
10338
+ /** Owns active collab-debug sessions (spawn/cancel/alert/list). Extracted
10339
+ * from Director in R4; the public collab methods below delegate to it. */
10340
+ collab;
10200
10341
  /** Prevents large `ask_subagent` answers from bloating the leader's context window. */
10201
10342
  largeAnswerStore;
10202
10343
  constructor(opts) {
@@ -10273,92 +10414,103 @@ var Director = class _Director {
10273
10414
  );
10274
10415
  this.coordinator.setFleetBus(this.fleet);
10275
10416
  this.fleetManager?.setCoordinator(this.coordinator);
10276
- this.taskCompletedListener = (payload) => {
10277
- const r = payload.result;
10278
- const internalTask = this.internalTaskIds.delete(r.taskId);
10279
- if (!internalTask) {
10280
- this.completed.set(r.taskId, r);
10281
- if (this.completed.size > _Director.MAX_COMPLETED) {
10282
- const toDelete = this.completed.size - _Director.MAX_COMPLETED;
10283
- const keys = [...this.completed.keys()].slice(0, toDelete);
10284
- for (const k of keys) this.completed.delete(k);
10285
- }
10286
- }
10287
- const waiter = this.taskWaiters.get(r.taskId);
10288
- if (waiter) {
10289
- waiter.resolve(r);
10290
- this.taskWaiters.delete(r.taskId);
10291
- }
10292
- let anyConsumed = false;
10293
- for (const aw of [...this.anyWaiters]) {
10294
- if (!aw.ids.has(r.taskId)) continue;
10295
- if (aw.timer) clearTimeout(aw.timer);
10296
- this.anyWaiters.delete(aw);
10297
- aw.resolve(r);
10298
- anyConsumed = true;
10299
- }
10300
- const consumedInBand = !!waiter || anyConsumed;
10301
- if (internalTask) return;
10302
- const title = this.taskDescriptions.get(r.taskId) ?? payload.task.description ?? r.taskId;
10303
- if (!consumedInBand && this.taskResultNotifier) {
10304
- const resultText = typeof r.result === "string" ? r.result : r.result !== void 0 ? safeStringify(r.result) : void 0;
10305
- try {
10306
- void Promise.resolve(
10307
- this.taskResultNotifier({
10308
- taskId: r.taskId,
10309
- title,
10310
- status: r.status,
10311
- subagentId: r.subagentId,
10312
- subagentName: this.manifestEntries.get(r.subagentId)?.name,
10313
- resultText,
10314
- errorText: r.error ? `${r.error.kind}: ${r.error.message}` : void 0,
10315
- partialText: r.partial?.text,
10316
- report: r.report,
10317
- iterations: r.iterations,
10318
- toolCalls: r.toolCalls,
10319
- durationMs: r.durationMs
10320
- })
10321
- ).catch(() => {
10322
- });
10323
- } catch {
10324
- }
10417
+ this.taskCompletedListener = (payload) => this.handleTaskCompleted(payload);
10418
+ this.coordinator.on("task.completed", this.taskCompletedListener);
10419
+ this.wireBudgetPolicy();
10420
+ this.largeAnswerStore = new LargeAnswerStore(2e3);
10421
+ this.collab = new DirectorCollabController({
10422
+ director: this,
10423
+ fleet: this.fleet,
10424
+ coordinator: this.coordinator,
10425
+ logger: this.logger
10426
+ });
10427
+ }
10428
+ handleTaskCompleted(payload) {
10429
+ const r = payload.result;
10430
+ const internalTask = this.internalTaskIds.delete(r.taskId);
10431
+ if (!internalTask) {
10432
+ this.completed.set(r.taskId, r);
10433
+ if (this.completed.size > _Director.MAX_COMPLETED) {
10434
+ const toDelete = this.completed.size - _Director.MAX_COMPLETED;
10435
+ const keys = [...this.completed.keys()].slice(0, toDelete);
10436
+ for (const k of keys) this.completed.delete(k);
10437
+ }
10438
+ }
10439
+ const waiter = this.taskWaiters.get(r.taskId);
10440
+ if (waiter) {
10441
+ waiter.resolve(r);
10442
+ this.taskWaiters.delete(r.taskId);
10443
+ }
10444
+ let anyConsumed = false;
10445
+ for (const aw of [...this.anyWaiters]) {
10446
+ if (!aw.ids.has(r.taskId)) continue;
10447
+ if (aw.timer) clearTimeout(aw.timer);
10448
+ this.anyWaiters.delete(aw);
10449
+ aw.resolve(r);
10450
+ anyConsumed = true;
10451
+ }
10452
+ const consumedInBand = !!waiter || anyConsumed;
10453
+ if (internalTask) return;
10454
+ const title = this.taskDescriptions.get(r.taskId) ?? payload.task.description ?? r.taskId;
10455
+ if (!consumedInBand && this.taskResultNotifier) {
10456
+ const resultText = typeof r.result === "string" ? r.result : r.result !== void 0 ? safeStringify(r.result) : void 0;
10457
+ try {
10458
+ void Promise.resolve(
10459
+ this.taskResultNotifier({
10460
+ taskId: r.taskId,
10461
+ title,
10462
+ status: r.status,
10463
+ subagentId: r.subagentId,
10464
+ subagentName: this.manifestEntries.get(r.subagentId)?.name,
10465
+ resultText,
10466
+ errorText: r.error ? `${r.error.kind}: ${r.error.message}` : void 0,
10467
+ partialText: r.partial?.text,
10468
+ report: r.report,
10469
+ iterations: r.iterations,
10470
+ toolCalls: r.toolCalls,
10471
+ durationMs: r.durationMs
10472
+ })
10473
+ ).catch(() => {
10474
+ });
10475
+ } catch {
10325
10476
  }
10326
- const failed = r.status !== "success";
10327
- const errorString = r.error ? `${r.error.kind}: ${r.error.message}` : void 0;
10328
- this.stateCheckpoint?.recordTaskStatus(r.taskId, {
10329
- status: failed ? r.status : "completed",
10330
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
10331
- iterations: r.iterations,
10332
- toolCalls: r.toolCalls,
10333
- durationMs: r.durationMs,
10334
- error: errorString
10335
- });
10336
- this.stateCheckpoint?.setUsage(this.usage.snapshot());
10337
- void this.appendSessionEvent(
10338
- failed ? {
10339
- type: "task_failed",
10340
- ts: (/* @__PURE__ */ new Date()).toISOString(),
10341
- taskId: r.taskId,
10342
- title,
10343
- error: errorString ?? r.status
10344
- } : {
10345
- type: "task_completed",
10346
- ts: (/* @__PURE__ */ new Date()).toISOString(),
10347
- taskId: r.taskId,
10348
- title
10349
- }
10350
- );
10351
- if (this.fleetManager) {
10352
- void this.fleetManager.flushManifest();
10353
- } else {
10354
- this.scheduleManifest();
10477
+ }
10478
+ const failed = r.status !== "success";
10479
+ const errorString = r.error ? `${r.error.kind}: ${r.error.message}` : void 0;
10480
+ this.stateCheckpoint?.recordTaskStatus(r.taskId, {
10481
+ status: failed ? r.status : "completed",
10482
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
10483
+ iterations: r.iterations,
10484
+ toolCalls: r.toolCalls,
10485
+ durationMs: r.durationMs,
10486
+ error: errorString
10487
+ });
10488
+ this.stateCheckpoint?.setUsage(this.usage.snapshot());
10489
+ void this.appendSessionEvent(
10490
+ failed ? {
10491
+ type: "task_failed",
10492
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
10493
+ taskId: r.taskId,
10494
+ title,
10495
+ error: errorString ?? r.status
10496
+ } : {
10497
+ type: "task_completed",
10498
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
10499
+ taskId: r.taskId,
10500
+ title
10355
10501
  }
10356
- this.armSubagentIdleRetirement(
10357
- r.subagentId,
10358
- this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleTimeoutMs
10359
- );
10360
- };
10361
- this.coordinator.on("task.completed", this.taskCompletedListener);
10502
+ );
10503
+ if (this.fleetManager) {
10504
+ void this.fleetManager.flushManifest();
10505
+ } else {
10506
+ this.scheduleManifest();
10507
+ }
10508
+ this.armSubagentIdleRetirement(
10509
+ r.subagentId,
10510
+ this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleTimeoutMs
10511
+ );
10512
+ }
10513
+ wireBudgetPolicy() {
10362
10514
  const extendCounts = /* @__PURE__ */ new Map();
10363
10515
  const progressBySubagent = /* @__PURE__ */ new Map();
10364
10516
  const lastTimeoutProgress = /* @__PURE__ */ new Map();
@@ -10478,7 +10630,6 @@ var Director = class _Director {
10478
10630
  }
10479
10631
  grantExtension();
10480
10632
  });
10481
- this.largeAnswerStore = new LargeAnswerStore(2e3);
10482
10633
  }
10483
10634
  /**
10484
10635
  * Record a granted budget extension and broadcast it on the FleetBus so
@@ -10536,10 +10687,7 @@ var Director = class _Director {
10536
10687
  * programmatically without going through the slash-command path.
10537
10688
  */
10538
10689
  setLeaderBtwNote(note) {
10539
- const trimmed = note.trim();
10540
- if (!trimmed) return this._leaderBtwNotes.length;
10541
- this._leaderBtwNotes = [...this._leaderBtwNotes, trimmed].slice(-20);
10542
- return this._leaderBtwNotes.length;
10690
+ return this.btwNotes.add(note);
10543
10691
  }
10544
10692
  /**
10545
10693
  * Read and clear all pending /btw notes the leader has stashed.
@@ -10550,16 +10698,14 @@ var Director = class _Director {
10550
10698
  * to cancel the collab session or let it continue.
10551
10699
  */
10552
10700
  getLeaderBtwNotes() {
10553
- const notes = this._leaderBtwNotes;
10554
- this._leaderBtwNotes = [];
10555
- return notes;
10701
+ return this.btwNotes.drain();
10556
10702
  }
10557
10703
  /**
10558
10704
  * Peek at pending /btw notes without consuming them.
10559
10705
  * Useful for UI to show "N pending notes" without clearing them.
10560
10706
  */
10561
10707
  peekLeaderBtwNotes() {
10562
- return [...this._leaderBtwNotes];
10708
+ return this.btwNotes.peek();
10563
10709
  }
10564
10710
  /**
10565
10711
  * Drain (read + clear) all /btw notes in one call.
@@ -10579,18 +10725,7 @@ var Director = class _Director {
10579
10725
  * The CollabDebugReport will reflect 'cancelled' disposition when awaited.
10580
10726
  */
10581
10727
  cancelCollabSession(sessionId, reason = "Director cancelled") {
10582
- const entry = this._activeCollabSessions.get(sessionId);
10583
- if (!entry || entry.session.isCancelled()) return;
10584
- for (const unsub of entry.unsubs) unsub();
10585
- entry.session.cancel(reason);
10586
- for (const [_role, subagentId] of entry.session.getSubagentIds()) {
10587
- this.coordinator.stop(subagentId).catch((err) => {
10588
- this.logger?.debug(`stop subagent ${subagentId} failed (may have already completed)`, {
10589
- subagentId,
10590
- err: err instanceof Error ? err.message : String(err)
10591
- });
10592
- });
10593
- }
10728
+ this.collab.cancel(sessionId, reason);
10594
10729
  }
10595
10730
  /**
10596
10731
  * Subscribe a callback to be notified whenever a collab session raises
@@ -10599,9 +10734,7 @@ var Director = class _Director {
10599
10734
  * Returns an unsubscribe function.
10600
10735
  */
10601
10736
  onCollabAlert(handler) {
10602
- return this.fleet.filter("collab.warning", (e) => {
10603
- handler(e.payload);
10604
- });
10737
+ return this.collab.onAlert(handler);
10605
10738
  }
10606
10739
  /**
10607
10740
  * Returns all active (not yet settled) collab session ids.
@@ -10609,7 +10742,7 @@ var Director = class _Director {
10609
10742
  * the host to know what can be cancelled.
10610
10743
  */
10611
10744
  activeCollabSessions() {
10612
- return Array.from(this._activeCollabSessions.keys());
10745
+ return this.collab.activeSessionIds();
10613
10746
  }
10614
10747
  /** Best-effort session-writer append. Swallows failures — the director
10615
10748
  * must not break a fleet run because the session JSONL handle closed. */
@@ -10675,17 +10808,90 @@ var Director = class _Director {
10675
10808
  );
10676
10809
  }
10677
10810
  const config = { ...callerConfig };
10678
- if (!config.model && this.modelMatrix) {
10679
- const matrix = typeof this.modelMatrix === "function" ? this.modelMatrix() : this.modelMatrix;
10680
- const resolution = resolveModelMatrixResolution(matrix, config.role);
10681
- const entry = resolution?.source === "default" && roleNeedsIndependentReviewModel(config.role) ? void 0 : resolution?.entry;
10682
- if (entry?.model) {
10683
- config.model = entry.model;
10684
- if (entry.provider) config.provider = entry.provider;
10685
- if (entry.modelRuntime) config.modelRuntime = entry.modelRuntime;
10811
+ this.resolveSpawnModel(config);
10812
+ this.enforceSpawnCaps(config);
10813
+ let result;
10814
+ this.assignSpawnNickname(config);
10815
+ this.applySpawnLineage(config);
10816
+ result = await this.coordinator.spawn(config);
10817
+ if (this.fleetManager) {
10818
+ this.fleetManager.recordSpawn(result.subagentId, config, priceLookup);
10819
+ } else {
10820
+ this.spawnCount += 1;
10821
+ this.subagentMeta.set(result.subagentId, {
10822
+ provider: config.provider,
10823
+ model: config.model
10824
+ });
10825
+ if (priceLookup && config.provider && config.model) {
10826
+ this.priceLookups.set(`${config.provider}/${config.model}`, priceLookup);
10686
10827
  }
10687
10828
  }
10688
- if (this.fleetManager) {
10829
+ const subagentBridge = new InMemoryAgentBridge(
10830
+ { agentId: result.subagentId, coordinatorId: this.id },
10831
+ this.transport
10832
+ );
10833
+ this.coordinator.setSubagentBridge(result.subagentId, subagentBridge);
10834
+ this.subagentBridges.set(result.subagentId, subagentBridge);
10835
+ this.fleet.emit({
10836
+ subagentId: result.subagentId,
10837
+ ts: Date.now(),
10838
+ type: "subagent.spawned",
10839
+ payload: {
10840
+ subagentId: result.subagentId,
10841
+ taskId: "",
10842
+ // taskId will be set when assign() is called
10843
+ name: config.name,
10844
+ role: config.role,
10845
+ provider: config.provider,
10846
+ model: config.model
10847
+ }
10848
+ });
10849
+ if (!this.fleetManager) {
10850
+ this.manifestEntries.set(result.subagentId, {
10851
+ subagentId: result.subagentId,
10852
+ name: config.name,
10853
+ role: config.role,
10854
+ provider: config.provider,
10855
+ model: config.model,
10856
+ taskIds: []
10857
+ });
10858
+ const spawnedAt = (/* @__PURE__ */ new Date()).toISOString();
10859
+ this.stateCheckpoint?.recordSpawn(
10860
+ {
10861
+ id: result.subagentId,
10862
+ name: config.name,
10863
+ role: config.role,
10864
+ provider: config.provider,
10865
+ model: config.model,
10866
+ spawnedAt
10867
+ },
10868
+ this.spawnCount
10869
+ );
10870
+ void this.appendSessionEvent({
10871
+ type: "agent_spawned",
10872
+ ts: spawnedAt,
10873
+ agentId: result.subagentId,
10874
+ role: config.role ?? config.name
10875
+ });
10876
+ this.scheduleManifest();
10877
+ }
10878
+ this.armSubagentIdleRetirement(result.subagentId, this.subagentIdleTimeoutMs);
10879
+ return result.subagentId;
10880
+ }
10881
+ resolveSpawnModel(config) {
10882
+ if (!config.model && this.modelMatrix) {
10883
+ const matrix = typeof this.modelMatrix === "function" ? this.modelMatrix() : this.modelMatrix;
10884
+ const resolution = resolveModelMatrixResolution(matrix, config.role);
10885
+ const entry = resolution?.source === "default" && roleNeedsIndependentReviewModel(config.role) ? void 0 : resolution?.entry;
10886
+ if (entry?.model) {
10887
+ config.model = entry.model;
10888
+ if (entry.provider) config.provider = entry.provider;
10889
+ if (entry.modelRuntime) config.modelRuntime = entry.modelRuntime;
10890
+ }
10891
+ }
10892
+ }
10893
+ enforceSpawnCaps(config) {
10894
+ if (this.fleetManager) {
10689
10895
  const rejection = this.fleetManager.canSpawn(config);
10690
10896
  if (rejection) {
10691
10897
  if (rejection.kind === "max_spawn_depth")
@@ -10727,7 +10933,8 @@ var Director = class _Director {
10727
10933
  }
10728
10934
  }
10729
10935
  }
10730
- let result;
10936
+ }
10937
+ assignSpawnNickname(config) {
10731
10938
  const needsNickname = config.name === config.role || !config.name || config.name === "subagent" || config.name === "adhoc";
10732
10939
  if (needsNickname) {
10733
10940
  const role = config.role ?? "subagent";
@@ -10739,6 +10946,8 @@ var Director = class _Director {
10739
10946
  this._usedNicknames.add(key);
10740
10947
  }
10741
10948
  }
10949
+ }
10950
+ applySpawnLineage(config) {
10742
10951
  const budget = this.fleetManager ? this.fleetManager.budgetSnapshot() : (() => {
10743
10952
  const total = this.usage.snapshot().total;
10744
10953
  const usedTokens = (total?.input ?? 0) + (total?.output ?? 0);
@@ -10767,70 +10976,6 @@ var Director = class _Director {
10767
10976
  ...Number.isFinite(budget.remainingCostUsd) ? { remainingCostUsd: budget.remainingCostUsd } : {}
10768
10977
  }
10769
10978
  };
10770
- result = await this.coordinator.spawn(config);
10771
- if (this.fleetManager) {
10772
- this.fleetManager.recordSpawn(result.subagentId, config, priceLookup);
10773
- } else {
10774
- this.spawnCount += 1;
10775
- this.subagentMeta.set(result.subagentId, {
10776
- provider: config.provider,
10777
- model: config.model
10778
- });
10779
- if (priceLookup && config.provider && config.model) {
10780
- this.priceLookups.set(`${config.provider}/${config.model}`, priceLookup);
10781
- }
10782
- }
10783
- const subagentBridge = new InMemoryAgentBridge(
10784
- { agentId: result.subagentId, coordinatorId: this.id },
10785
- this.transport
10786
- );
10787
- this.coordinator.setSubagentBridge(result.subagentId, subagentBridge);
10788
- this.subagentBridges.set(result.subagentId, subagentBridge);
10789
- this.fleet.emit({
10790
- subagentId: result.subagentId,
10791
- ts: Date.now(),
10792
- type: "subagent.spawned",
10793
- payload: {
10794
- subagentId: result.subagentId,
10795
- taskId: "",
10796
- // taskId will be set when assign() is called
10797
- name: config.name,
10798
- role: config.role,
10799
- provider: config.provider,
10800
- model: config.model
10801
- }
10802
- });
10803
- if (!this.fleetManager) {
10804
- this.manifestEntries.set(result.subagentId, {
10805
- subagentId: result.subagentId,
10806
- name: config.name,
10807
- role: config.role,
10808
- provider: config.provider,
10809
- model: config.model,
10810
- taskIds: []
10811
- });
10812
- const spawnedAt = (/* @__PURE__ */ new Date()).toISOString();
10813
- this.stateCheckpoint?.recordSpawn(
10814
- {
10815
- id: result.subagentId,
10816
- name: config.name,
10817
- role: config.role,
10818
- provider: config.provider,
10819
- model: config.model,
10820
- spawnedAt
10821
- },
10822
- this.spawnCount
10823
- );
10824
- void this.appendSessionEvent({
10825
- type: "agent_spawned",
10826
- ts: spawnedAt,
10827
- agentId: result.subagentId,
10828
- role: config.role ?? config.name
10829
- });
10830
- this.scheduleManifest();
10831
- }
10832
- this.armSubagentIdleRetirement(result.subagentId, this.subagentIdleTimeoutMs);
10833
- return result.subagentId;
10834
10979
  }
10835
10980
  /**
10836
10981
  * Synchronously ask a subagent something via the bridge. Sends a
@@ -10955,6 +11100,23 @@ var Director = class _Director {
10955
11100
  await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
10956
11101
  return this.manifestPath;
10957
11102
  }
11103
+ /**
11104
+ * Cancel the pending manifest debounce timer and drain any in-flight
11105
+ * manifest write so that, once this resolves, no Director-owned manifest
11106
+ * write is armed or running.
11107
+ *
11108
+ * Unlike `shutdown()`, this does NOT tear down the coordinator, bridges,
11109
+ * or waiters — it is the lightweight quiesce that `MultiAgentHost.stopAll()`
11110
+ * needs. `stopAll()` flushes the FleetManager's manifest but never touched
11111
+ * the Director's own writer, leaving its armed debounce timer to fire ~2s
11112
+ * later. Under CPU starvation that late, un-awaited `atomicWrite` (temp
11113
+ * sibling + rename) races a caller that deletes the manifest directory,
11114
+ * producing an `ENOTEMPTY` on the parent `rmdir` (Windows especially).
11115
+ */
11116
+ async quiesceManifest() {
11117
+ this.clearManifestTimer();
11118
+ await this.manifestWriteChain.catch(() => void 0);
11119
+ }
10958
11120
  /**
10959
11121
  * Tear down the director: stop every subagent, close every bridge
10960
11122
  * endpoint, and (when configured) write the final manifest. Idempotent
@@ -10966,8 +11128,17 @@ var Director = class _Director {
10966
11128
  this.coordinator.off("task.completed", this.taskCompletedListener);
10967
11129
  this.taskCompletedListener = null;
10968
11130
  }
10969
- for (const aw of this.anyWaiters) {
11131
+ for (const aw of [...this.anyWaiters]) {
10970
11132
  if (aw.timer) clearTimeout(aw.timer);
11133
+ const firstId = [...aw.ids][0];
11134
+ aw.resolve({
11135
+ taskId: firstId ?? "unknown",
11136
+ subagentId: "director",
11137
+ status: "stopped",
11138
+ iterations: 0,
11139
+ toolCalls: 0,
11140
+ durationMs: 0
11141
+ });
10971
11142
  }
10972
11143
  this.anyWaiters.clear();
10973
11144
  if (this.toolExecFilter) {
@@ -10981,6 +11152,17 @@ var Director = class _Director {
10981
11152
  for (const timer of this.subagentIdleTimers.values()) clearTimeout(timer);
10982
11153
  this.subagentIdleTimers.clear();
10983
11154
  await this.coordinator.stopAll();
11155
+ for (const [id, waiter] of this.taskWaiters) {
11156
+ waiter.resolve({
11157
+ taskId: id,
11158
+ subagentId: "director",
11159
+ status: "stopped",
11160
+ iterations: 0,
11161
+ toolCalls: 0,
11162
+ durationMs: 0
11163
+ });
11164
+ }
11165
+ this.taskWaiters.clear();
10984
11166
  for (const b of this.subagentBridges.values()) {
10985
11167
  await b.stop().catch((err) => this.logShutdownError("subagent_bridge_stop", err));
10986
11168
  }
@@ -10988,6 +11170,7 @@ var Director = class _Director {
10988
11170
  await this.bridge.stop().catch((err) => this.logShutdownError("director_bridge_stop", err));
10989
11171
  if (this.fleetManager) {
10990
11172
  await this.fleetManager.flushManifest().catch((err) => this.logShutdownError("fleet_manifest_flush", err));
11173
+ await this.manifestWriteChain.catch(() => void 0);
10991
11174
  } else if (this.manifestPath) {
10992
11175
  await this.writeManifest().catch((err) => this.logShutdownError("manifest_write", err));
10993
11176
  }
@@ -11453,22 +11636,7 @@ var Director = class _Director {
11453
11636
  * three agents complete or the session times out.
11454
11637
  */
11455
11638
  async spawnCollab(options) {
11456
- const session = new CollabSession(this, this.fleet, {
11457
- ...options,
11458
- onBudgetWarning: (alert) => {
11459
- return options.onBudgetWarning?.(alert) ?? "ignore";
11460
- }
11461
- });
11462
- const doneHandler = () => this._activeCollabSessions.delete(session.sessionId);
11463
- const errorHandler = () => this._activeCollabSessions.delete(session.sessionId);
11464
- session.on("session.done", doneHandler);
11465
- session.on("session.error", errorHandler);
11466
- const unsubs = [
11467
- () => session.off("session.done", doneHandler),
11468
- () => session.off("session.error", errorHandler)
11469
- ];
11470
- this._activeCollabSessions.set(session.sessionId, { session, unsubs });
11471
- return session.start();
11639
+ return this.collab.spawn(options);
11472
11640
  }
11473
11641
  /**
11474
11642
  * Resume from a prior checkpoint snapshot (loaded via
@@ -12341,8 +12509,8 @@ var SessionCheckpointCas = class {
12341
12509
  return null;
12342
12510
  }
12343
12511
  try {
12344
- const stat13 = await fsp7.lstat(absolute);
12345
- if (stat13.isSymbolicLink()) {
12512
+ const stat12 = await fsp7.lstat(absolute);
12513
+ if (stat12.isSymbolicLink()) {
12346
12514
  const linkTarget = await fsp7.readlink(absolute);
12347
12515
  const resolvedLink = path9.resolve(path9.dirname(absolute), linkTarget);
12348
12516
  if (path9.isAbsolute(linkTarget) || !isInside(this.projectRoot, resolvedLink)) {
@@ -12354,29 +12522,29 @@ var SessionCheckpointCas = class {
12354
12522
  }
12355
12523
  return { path: relative4, state: "symlink", linkTarget };
12356
12524
  }
12357
- if (!stat13.isFile()) {
12525
+ if (!stat12.isFile()) {
12358
12526
  unresolved.push({ path: relative4, reason: "changed path is not a regular file" });
12359
12527
  return null;
12360
12528
  }
12361
- if (stat13.size > MAX_BLOB_BYTES) {
12529
+ if (stat12.size > MAX_BLOB_BYTES) {
12362
12530
  unresolved.push({
12363
12531
  path: relative4,
12364
12532
  reason: `file exceeds ${MAX_BLOB_BYTES}-byte checkpoint blob limit`
12365
12533
  });
12366
12534
  return null;
12367
12535
  }
12368
- if (scheduledBytes + stat13.size > MAX_CHECKPOINT_BYTES) {
12536
+ if (scheduledBytes + stat12.size > MAX_CHECKPOINT_BYTES) {
12369
12537
  unresolved.push({
12370
12538
  path: relative4,
12371
12539
  reason: `checkpoint exceeds ${MAX_CHECKPOINT_BYTES}-byte aggregate blob limit`
12372
12540
  });
12373
12541
  return null;
12374
12542
  }
12375
- scheduledBytes += stat13.size;
12543
+ scheduledBytes += stat12.size;
12376
12544
  const content = await fsp7.readFile(absolute);
12377
12545
  const blobHash = sha256(content);
12378
12546
  await this.putBlob(blobHash, content);
12379
- return { path: relative4, state: "file", blobHash, mode: stat13.mode & 511 };
12547
+ return { path: relative4, state: "file", blobHash, mode: stat12.mode & 511 };
12380
12548
  } catch (err) {
12381
12549
  if (err.code === "ENOENT") {
12382
12550
  return { path: relative4, state: "absent" };
@@ -12697,11 +12865,11 @@ async function validateOne(observation, lexicalRoot, realRoot) {
12697
12865
  };
12698
12866
  }
12699
12867
  try {
12700
- const stat13 = await fsp8.stat(realFile);
12701
- if (!stat13.isFile()) {
12868
+ const stat12 = await fsp8.stat(realFile);
12869
+ if (!stat12.isFile()) {
12702
12870
  return { ...base, status: "unreadable", detail: "Path is no longer a regular file." };
12703
12871
  }
12704
- if (stat13.size > MAX_REVALIDATE_BYTES) {
12872
+ if (stat12.size > MAX_REVALIDATE_BYTES) {
12705
12873
  return {
12706
12874
  ...base,
12707
12875
  status: "unreadable",
@@ -13150,9 +13318,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
13150
13318
  let cacheHit = false;
13151
13319
  try {
13152
13320
  const s = await fsp9.stat(file);
13153
- const stat13 = { mtimeMs: s.mtimeMs, size: s.size };
13321
+ const stat12 = { mtimeMs: s.mtimeMs, size: s.size };
13154
13322
  const cached = this._loadCache.get(id);
13155
- if (cached && cached.mtimeMs === stat13.mtimeMs && cached.size === stat13.size) {
13323
+ if (cached && cached.mtimeMs === stat12.mtimeMs && cached.size === stat12.size) {
13156
13324
  cacheHit = true;
13157
13325
  this._loadCache.delete(id);
13158
13326
  this._loadCache.set(id, cached);
@@ -13293,7 +13461,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
13293
13461
  this._loadCache.delete(oldest);
13294
13462
  }
13295
13463
  }
13296
- this._loadCache.set(id, { mtimeMs: stat13.mtimeMs, size: stat13.size, data });
13464
+ this._loadCache.set(id, { mtimeMs: stat12.mtimeMs, size: stat12.size, data });
13297
13465
  }
13298
13466
  return data;
13299
13467
  } catch (err) {
@@ -13335,14 +13503,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
13335
13503
  const limit = opts?.limit;
13336
13504
  const signal = opts?.signal;
13337
13505
  const out = [];
13338
- let stat13;
13506
+ let stat12;
13339
13507
  try {
13340
- stat13 = await fsp9.stat(file);
13508
+ stat12 = await fsp9.stat(file);
13341
13509
  } catch (err) {
13342
13510
  if (err.code === "ENOENT") return [];
13343
13511
  throw err;
13344
13512
  }
13345
- if (stat13.size === 0) return [];
13513
+ if (stat12.size === 0) return [];
13346
13514
  let fh;
13347
13515
  try {
13348
13516
  fh = await fsp9.open(file, "r");
@@ -13525,15 +13693,15 @@ var DefaultSessionStore = class _DefaultSessionStore {
13525
13693
  * Returns empty array when the index doesn't exist or is corrupt.
13526
13694
  */
13527
13695
  async readIndex() {
13528
- let stat13;
13696
+ let stat12;
13529
13697
  try {
13530
13698
  const s = await fsp9.stat(this.indexFile);
13531
- stat13 = { mtimeMs: s.mtimeMs, size: s.size };
13699
+ stat12 = { mtimeMs: s.mtimeMs, size: s.size };
13532
13700
  } catch {
13533
13701
  this._indexCache = null;
13534
13702
  return [];
13535
13703
  }
13536
- if (this._indexCache !== null && this._indexCache.mtimeMs === stat13.mtimeMs && this._indexCache.size === stat13.size) {
13704
+ if (this._indexCache !== null && this._indexCache.mtimeMs === stat12.mtimeMs && this._indexCache.size === stat12.size) {
13537
13705
  return [...this._indexCache.summaries];
13538
13706
  }
13539
13707
  let raw;
@@ -13566,7 +13734,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
13566
13734
  if (a.startedAt > b.startedAt) return -1;
13567
13735
  return a.id.localeCompare(b.id);
13568
13736
  });
13569
- this._indexCache = { ...stat13, summaries };
13737
+ this._indexCache = { ...stat12, summaries };
13570
13738
  return [...summaries];
13571
13739
  }
13572
13740
  /**
@@ -13733,8 +13901,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
13733
13901
  if (fromManifest) return fromManifest;
13734
13902
  try {
13735
13903
  const full = this.sessionPath(id, ".jsonl");
13736
- const stat13 = await fsp9.stat(full);
13737
- const summary = await this.summarize(id, stat13.mtime.toISOString());
13904
+ const stat12 = await fsp9.stat(full);
13905
+ const summary = await this.summarize(id, stat12.mtime.toISOString());
13738
13906
  await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
13739
13907
  const msg = toErrorMessage(err);
13740
13908
  this.emitError(id, manifest, "summary_fallback", msg, true);
@@ -13774,18 +13942,18 @@ var DefaultSessionStore = class _DefaultSessionStore {
13774
13942
  async summaryHeaderFor(ref) {
13775
13943
  let mtime = (/* @__PURE__ */ new Date(0)).toISOString();
13776
13944
  try {
13777
- const stat13 = await fsp9.stat(ref.filePath);
13778
- if (!stat13.isFile()) {
13945
+ const stat12 = await fsp9.stat(ref.filePath);
13946
+ if (!stat12.isFile()) {
13779
13947
  return {
13780
13948
  id: ref.id,
13781
13949
  title: "(damaged)",
13782
- startedAt: stat13.mtime.toISOString(),
13950
+ startedAt: stat12.mtime.toISOString(),
13783
13951
  model: "unknown",
13784
13952
  provider: "unknown",
13785
13953
  tokenTotal: 0
13786
13954
  };
13787
13955
  }
13788
- mtime = stat13.mtime.toISOString();
13956
+ mtime = stat12.mtime.toISOString();
13789
13957
  } catch {
13790
13958
  return null;
13791
13959
  }
@@ -13941,8 +14109,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
13941
14109
  const pruneFile = async (dir, name, prefix) => {
13942
14110
  const jsonlPath = path11.join(dir, name);
13943
14111
  try {
13944
- const stat13 = await fsp9.stat(jsonlPath);
13945
- if (stat13.mtimeMs >= cutoff) return;
14112
+ const stat12 = await fsp9.stat(jsonlPath);
14113
+ if (stat12.mtimeMs >= cutoff) return;
13946
14114
  } catch {
13947
14115
  return;
13948
14116
  }
@@ -14158,6 +14326,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
14158
14326
  failureMode;
14159
14327
  policyProvider;
14160
14328
  sessionBridge;
14329
+ onCompact;
14161
14330
  /**
14162
14331
  * Once a compaction attempt reduces nothing (preserveK protects everything,
14163
14332
  * no oversized tool_results remain to elide), retrying on every iteration
@@ -14204,6 +14373,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
14204
14373
  this.failureMode = opts.failureMode ?? "throw_on_hard";
14205
14374
  this.policyProvider = opts.policyProvider;
14206
14375
  this.sessionBridge = opts.sessionBridge;
14376
+ this.onCompact = opts.onCompact;
14207
14377
  }
14208
14378
  /** Allow callers (e.g. model-switch in WebUI) to update the context window
14209
14379
  * denominator when the active model changes. */
@@ -14326,6 +14496,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
14326
14496
  try {
14327
14497
  const report = await this.compactor.compact(ctx, { aggressive });
14328
14498
  this.recordAttempt(pressure.level, pressure.tokens, report);
14499
+ this.onCompact?.(report);
14329
14500
  this.events?.emit("compaction.fired", {
14330
14501
  sessionId: ctx.session.id,
14331
14502
  level: pressure.level,
@@ -14354,7 +14525,8 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
14354
14525
  });
14355
14526
  ctx.clearFileTracking();
14356
14527
  const afterTokens = report.fullRequestTokensAfter ?? report.after;
14357
- const afterLoad = this._maxContext > 0 ? afterTokens / this._maxContext : 0;
14528
+ const afterBudget = computeContextWindowBudget(ctx, afterTokens, this._maxContext);
14529
+ const afterLoad = afterBudget.load;
14358
14530
  const stillHard = afterLoad >= pressure.hardThreshold;
14359
14531
  const fatal = stillHard && (this.failureMode === "throw" || this.failureMode === "throw_on_hard" && pressure.level === "hard");
14360
14532
  if (stillHard) {
@@ -15009,7 +15181,7 @@ function eliseOldToolResults(messages, opts) {
15009
15181
  }
15010
15182
  if (newContent) {
15011
15183
  next ??= messages.slice();
15012
- next[i] = { ...msg, content: newContent };
15184
+ next[i] = { ...msg, content: newContent, _estTokens: void 0 };
15013
15185
  changed = true;
15014
15186
  }
15015
15187
  if (compactionDebugEnabled()) {
@@ -15269,7 +15441,7 @@ var DEFAULT_TOOLS_CONFIG = Object.freeze({
15269
15441
  descriptionMode: Object.freeze({}),
15270
15442
  disabledTools: Object.freeze([]),
15271
15443
  autoExtendLimit: true,
15272
- restrictToProjectRoot: false,
15444
+ restrictToProjectRoot: true,
15273
15445
  loopDetection: Object.freeze({
15274
15446
  mode: "steer-then-cut",
15275
15447
  steerThreshold: 3,
@@ -15279,8 +15451,8 @@ var DEFAULT_TOOLS_CONFIG = Object.freeze({
15279
15451
  })
15280
15452
  });
15281
15453
  var DEFAULT_CONTEXT_CONFIG = Object.freeze({
15282
- preserveK: 10,
15283
- eliseThreshold: 2e3
15454
+ preserveK: 8,
15455
+ eliseThreshold: 1e3
15284
15456
  });
15285
15457
  var DEFAULT_AUTONOMY_CONFIG = Object.freeze({
15286
15458
  autoProceedDelayMs: 45e3
@@ -15443,7 +15615,7 @@ function isDesignStack(v) {
15443
15615
 
15444
15616
  // src/execution/design-kit-loader.ts
15445
15617
  import { existsSync } from "node:fs";
15446
- import * as fs2 from "node:fs/promises";
15618
+ import * as fs3 from "node:fs/promises";
15447
15619
  import * as path13 from "node:path";
15448
15620
  import { fileURLToPath as fileURLToPath3 } from "node:url";
15449
15621
  var KIT_FILE = "KIT.md";
@@ -15483,17 +15655,17 @@ function parseKitFrontmatter(raw) {
15483
15655
  else out[key] = items;
15484
15656
  };
15485
15657
  for (const line of lines) {
15486
- const item = /^\s*-\s+(.*)$/.exec(line);
15487
- if (listKey && item) {
15488
- const parsed = parseList(item[1] ?? "");
15658
+ const bulletMatch = /^\s*-\s+(.*)$/.exec(line);
15659
+ if (listKey && bulletMatch) {
15660
+ const parsed = parseList(bulletMatch[1] ?? "");
15489
15661
  if (listKey === "stacks") out.stacks.push(...parsed.filter(isDesignStack));
15490
15662
  else out[listKey].push(...parsed);
15491
15663
  continue;
15492
15664
  }
15493
- const m = /^([a-zA-Z_]+):\s*(.*)$/.exec(line);
15494
- if (!m) continue;
15495
- const key = m[1] ?? "";
15496
- const rest = (m[2] ?? "").trim();
15665
+ const kvMatch = /^([a-zA-Z_]+):\s*(.*)$/.exec(line);
15666
+ if (!kvMatch) continue;
15667
+ const key = kvMatch[1] ?? "";
15668
+ const rest = (kvMatch[2] ?? "").trim();
15497
15669
  if (key === "tags" || key === "stacks" || key === "themes") {
15498
15670
  if (rest) {
15499
15671
  setList(key, parseList(rest));
@@ -15510,24 +15682,23 @@ function parseKitFrontmatter(raw) {
15510
15682
  }
15511
15683
  function narrowStackSections(body, stack) {
15512
15684
  if (!stack) return body;
15513
- const re = /^##\s+Stack:\s*([a-z-]+)\s*$/gim;
15514
- const matches = [];
15515
- let m;
15516
- while ((m = re.exec(body)) !== null) {
15517
- matches.push({ id: (m[1] ?? "").trim(), start: m.index });
15518
- }
15519
- if (matches.length === 0) return body;
15520
- const firstStart = matches[0]?.start ?? body.length;
15521
- let result = body.slice(0, firstStart).trimEnd();
15522
- for (let i = 0; i < matches.length; i++) {
15523
- const cur = matches[i];
15524
- if (!cur) continue;
15525
- const next = matches[i + 1];
15526
- const sectionEnd = next ? next.start : body.length;
15527
- if (cur.id === stack) {
15685
+ const stackHeading = /^##\s+Stack:\s*([a-z-]+)\s*$/gim;
15686
+ const sections = [];
15687
+ let match;
15688
+ while ((match = stackHeading.exec(body)) !== null) {
15689
+ sections.push({ id: (match[1] ?? "").trim(), start: match.index });
15690
+ }
15691
+ if (sections.length === 0) return body;
15692
+ const preambleEnd = sections[0].start;
15693
+ let result = body.slice(0, preambleEnd).trimEnd();
15694
+ let sectionEnd;
15695
+ for (let i = 0; i < sections.length; i++) {
15696
+ const current = sections[i];
15697
+ sectionEnd = i + 1 < sections.length ? sections[i + 1].start : body.length;
15698
+ if (current.id === stack) {
15528
15699
  result += `
15529
15700
 
15530
- ${body.slice(cur.start, sectionEnd).trimEnd()}`;
15701
+ ${body.slice(current.start, sectionEnd).trimEnd()}`;
15531
15702
  }
15532
15703
  }
15533
15704
  return `${result}
@@ -15552,7 +15723,7 @@ var DefaultDesignKitLoader = class {
15552
15723
  for (const { dir, source } of this.dirs) {
15553
15724
  let entries;
15554
15725
  try {
15555
- entries = await fs2.readdir(dir, { withFileTypes: true });
15726
+ entries = await fs3.readdir(dir, { withFileTypes: true });
15556
15727
  } catch {
15557
15728
  continue;
15558
15729
  }
@@ -15560,7 +15731,7 @@ var DefaultDesignKitLoader = class {
15560
15731
  if (!e.isDirectory()) continue;
15561
15732
  const kitFile = path13.join(dir, e.name, KIT_FILE);
15562
15733
  try {
15563
- const raw = await fs2.readFile(kitFile, "utf8");
15734
+ const raw = await fs3.readFile(kitFile, "utf8");
15564
15735
  const fm = parseKitFrontmatter(raw);
15565
15736
  const id = fm.id ?? e.name;
15566
15737
  if (!fm.name) continue;
@@ -15621,7 +15792,7 @@ var DefaultDesignKitLoader = class {
15621
15792
  if (cached !== void 0) return cached;
15622
15793
  const m = await this.find(id);
15623
15794
  if (!m) throw new Error(`Design kit "${id}" not found`);
15624
- const raw = await fs2.readFile(m.path, "utf8");
15795
+ const raw = await fs3.readFile(m.path, "utf8");
15625
15796
  const body = narrowStackSections(stripFrontmatter(raw), stack);
15626
15797
  this.bodyCache.set(key, body);
15627
15798
  return body;
@@ -15634,7 +15805,7 @@ var DefaultDesignKitLoader = class {
15634
15805
  if (m) {
15635
15806
  const tokensPath = path13.join(path13.dirname(m.path), TOKENS_FILE);
15636
15807
  try {
15637
- const raw = await fs2.readFile(tokensPath, "utf8");
15808
+ const raw = await fs3.readFile(tokensPath, "utf8");
15638
15809
  const parsed = JSON.parse(raw);
15639
15810
  tokens = parsed;
15640
15811
  } catch {
@@ -15692,7 +15863,7 @@ function _resetDesignKitLoaderMemo() {
15692
15863
 
15693
15864
  // src/execution/design-project-store.ts
15694
15865
  import { existsSync as existsSync2 } from "node:fs";
15695
- import * as fs3 from "node:fs/promises";
15866
+ import * as fs4 from "node:fs/promises";
15696
15867
  import * as path14 from "node:path";
15697
15868
  var DESIGN_DIR = ".design";
15698
15869
  function designProjectDir(projectRoot) {
@@ -15705,7 +15876,7 @@ async function loadProjectDesignRules(projectRoot) {
15705
15876
  let rules;
15706
15877
  for (const name of RULE_FILES) {
15707
15878
  try {
15708
- const txt = await fs3.readFile(path14.join(designProjectDir(projectRoot), name), "utf8");
15879
+ const txt = await fs4.readFile(path14.join(designProjectDir(projectRoot), name), "utf8");
15709
15880
  if (txt.trim()) {
15710
15881
  rules = txt.trim();
15711
15882
  break;
@@ -15726,7 +15897,7 @@ function parseOverrides(value) {
15726
15897
  }
15727
15898
  async function loadActiveKit(projectRoot) {
15728
15899
  try {
15729
- const raw = await fs3.readFile(path14.join(designProjectDir(projectRoot), "active.json"), "utf8");
15900
+ const raw = await fs4.readFile(path14.join(designProjectDir(projectRoot), "active.json"), "utf8");
15730
15901
  const parsed = JSON.parse(raw);
15731
15902
  if (parsed && typeof parsed.kit === "string") {
15732
15903
  return {
@@ -15759,11 +15930,11 @@ function applyTokenOverrides(tokens, overrides) {
15759
15930
  }
15760
15931
  async function ensureDesignDir(projectRoot) {
15761
15932
  const dir = designProjectDir(projectRoot);
15762
- await fs3.mkdir(dir, { recursive: true });
15933
+ await fs4.mkdir(dir, { recursive: true });
15763
15934
  const gi = path14.join(dir, ".gitignore");
15764
15935
  if (!existsSync2(gi)) {
15765
15936
  try {
15766
- await fs3.writeFile(gi, "*\n");
15937
+ await fs4.writeFile(gi, "*\n");
15767
15938
  } catch {
15768
15939
  }
15769
15940
  }
@@ -15774,11 +15945,11 @@ async function recordKitChoice(projectRoot, kit, stack, source, isoTime, overrid
15774
15945
  const dir = await ensureDesignDir(projectRoot);
15775
15946
  const record = { kit, stack: stack ?? null };
15776
15947
  if (overrides && Object.keys(overrides).length > 0) record.overrides = overrides;
15777
- await fs3.writeFile(path14.join(dir, "active.json"), `${JSON.stringify(record, null, 2)}
15948
+ await fs4.writeFile(path14.join(dir, "active.json"), `${JSON.stringify(record, null, 2)}
15778
15949
  `);
15779
15950
  const line = `- ${isoTime} \xB7 kit=${kit}${stack ? ` stack=${stack}` : ""} \xB7 via=${source}
15780
15951
  `;
15781
- await fs3.appendFile(path14.join(dir, "decisions.md"), line);
15952
+ await fs4.appendFile(path14.join(dir, "decisions.md"), line);
15782
15953
  } catch {
15783
15954
  }
15784
15955
  }
@@ -15794,10 +15965,10 @@ async function recordOverrides(projectRoot, patch, isoTime) {
15794
15965
  const dir = await ensureDesignDir(projectRoot);
15795
15966
  const record = { kit: active.kit, stack: active.stack ?? null };
15796
15967
  if (Object.keys(merged).length > 0) record.overrides = merged;
15797
- await fs3.writeFile(path14.join(dir, "active.json"), `${JSON.stringify(record, null, 2)}
15968
+ await fs4.writeFile(path14.join(dir, "active.json"), `${JSON.stringify(record, null, 2)}
15798
15969
  `);
15799
15970
  const keys = Object.keys(patch).join(",");
15800
- await fs3.appendFile(
15971
+ await fs4.appendFile(
15801
15972
  path14.join(dir, "decisions.md"),
15802
15973
  `- ${isoTime} \xB7 kit=${active.kit} \xB7 override=${keys} \xB7 via=set
15803
15974
  `
@@ -15808,7 +15979,7 @@ async function recordOverrides(projectRoot, patch, isoTime) {
15808
15979
  }
15809
15980
  async function clearPersistedActiveKit(projectRoot) {
15810
15981
  try {
15811
- await fs3.rm(path14.join(designProjectDir(projectRoot), "active.json"), { force: true });
15982
+ await fs4.rm(path14.join(designProjectDir(projectRoot), "active.json"), { force: true });
15812
15983
  } catch {
15813
15984
  }
15814
15985
  }
@@ -15924,12 +16095,12 @@ function verifyFiles(tokens, files) {
15924
16095
  const violations = [];
15925
16096
  let onPalette = 0;
15926
16097
  let offPalette = 0;
15927
- for (const { path: path29, text } of files) {
16098
+ for (const { path: path28, text } of files) {
15928
16099
  const lines = text.split("\n");
15929
16100
  lines.forEach((lineText, i) => {
15930
16101
  const lineNo = i + 1;
15931
16102
  const flag = (snippet, reason) => {
15932
- violations.push({ file: path29, line: lineNo, snippet: snippet.slice(0, 80), reason });
16103
+ violations.push({ file: path28, line: lineNo, snippet: snippet.slice(0, 80), reason });
15933
16104
  };
15934
16105
  for (const re of [HEX_RE, FUNC_COLOR_RE]) {
15935
16106
  re.lastIndex = 0;
@@ -16433,11 +16604,25 @@ function buildRecoveryStrategies(opts) {
16433
16604
  },
16434
16605
  {
16435
16606
  label: "rate_limit_backoff",
16436
- async attempt(err) {
16607
+ async attempt(err, ctx) {
16437
16608
  if (!(err instanceof ProviderError) || err.kind !== "rate_limit") return null;
16438
16609
  const delayMs = err.body?.retryAfterMs ?? 5e3;
16439
16610
  const delay = Math.min(6e4, Math.max(1e3, delayMs));
16440
- await new Promise((r) => setTimeout(r, delay));
16611
+ if (ctx?.signal) {
16612
+ await new Promise((resolve12) => {
16613
+ if (ctx.signal.aborted) {
16614
+ resolve12();
16615
+ return;
16616
+ }
16617
+ const timer = setTimeout(resolve12, delay);
16618
+ ctx.signal.addEventListener("abort", () => {
16619
+ clearTimeout(timer);
16620
+ resolve12();
16621
+ }, { once: true });
16622
+ });
16623
+ } else {
16624
+ await new Promise((r) => setTimeout(r, delay));
16625
+ }
16441
16626
  return { action: "retry", reason: "rate_limit_backoff" };
16442
16627
  }
16443
16628
  },
@@ -17170,8 +17355,8 @@ ${recentJournal}` : "No prior iterations.",
17170
17355
  });
17171
17356
  await saveGoal(this.goalPath, withEntry, this.opts.events);
17172
17357
  try {
17173
- const { unlink: unlink12 } = await import("node:fs/promises");
17174
- await unlink12(this.goalPath);
17358
+ const { unlink: unlink11 } = await import("node:fs/promises");
17359
+ await unlink11(this.goalPath);
17175
17360
  } catch {
17176
17361
  }
17177
17362
  this.opts.onEternalStop?.();
@@ -17188,8 +17373,8 @@ ${recentJournal}` : "No prior iterations.",
17188
17373
  await saveGoal(this.goalPath, abandoned, this.opts.events);
17189
17374
  }
17190
17375
  try {
17191
- const { unlink: unlink12 } = await import("node:fs/promises");
17192
- await unlink12(this.goalPath);
17376
+ const { unlink: unlink11 } = await import("node:fs/promises");
17377
+ await unlink11(this.goalPath);
17193
17378
  } catch {
17194
17379
  }
17195
17380
  this.opts.onEternalStop?.();
@@ -17318,16 +17503,18 @@ var IntelligentCompactor = class {
17318
17503
  eliseThreshold;
17319
17504
  summarizerPrompt;
17320
17505
  summarizerModel;
17506
+ oneShotOrchestrator;
17321
17507
  constructor(opts) {
17322
17508
  this.provider = opts.provider;
17323
- this.warnThreshold = opts.warnThreshold ?? 0.6;
17324
- this.softThreshold = opts.softThreshold ?? 0.75;
17325
- this.hardThreshold = opts.hardThreshold ?? 0.9;
17509
+ this.warnThreshold = opts.warnThreshold ?? 0.5;
17510
+ this.softThreshold = opts.softThreshold ?? 0.65;
17511
+ this.hardThreshold = opts.hardThreshold ?? 0.8;
17326
17512
  this.maxContext = opts.maxContext ?? 128e3;
17327
17513
  this.preserveK = opts.preserveK ?? 4;
17328
- this.eliseThreshold = opts.eliseThreshold ?? 500;
17514
+ this.eliseThreshold = opts.eliseThreshold ?? 300;
17329
17515
  this.summarizerPrompt = opts.summarizerPrompt ?? readBundledInstructionText("llm/intelligent-compactor-summarizer.md");
17330
17516
  this.summarizerModel = opts.summarizerModel;
17517
+ this.oneShotOrchestrator = opts.oneShotOrchestrator;
17331
17518
  }
17332
17519
  async compact(ctx, opts = {}) {
17333
17520
  const beforeTokens = estimateMessages(ctx.messages);
@@ -17340,7 +17527,7 @@ var IntelligentCompactor = class {
17340
17527
  let collapsedDigest;
17341
17528
  if (aggressive) {
17342
17529
  const phase2 = await this.summarizeAncientTurns(ctx);
17343
- if (phase2.saved > 0) reductions.push({ phase: "summary", saved: phase2.saved });
17530
+ reductions.push({ phase: "summary", saved: Math.max(0, phase2.saved) });
17344
17531
  collapsedDigest = phase2.digest;
17345
17532
  } else if (load >= this.warnThreshold) {
17346
17533
  const saved2 = this.elide(ctx);
@@ -17394,9 +17581,15 @@ var IntelligentCompactor = class {
17394
17581
  } catch {
17395
17582
  summaryText = buildLosslessDigest(toSummarize) || `${toSummarize.length} earlier turns (semantic content preserved)`;
17396
17583
  }
17584
+ const smartDigest = buildSmartDigest(toSummarize);
17585
+ const enriched = smartDigest ? `[CRITICAL]
17586
+ ${smartDigest}
17587
+
17588
+ [SUMMARY]
17589
+ ${summaryText}` : summaryText;
17397
17590
  const summaryMsg = {
17398
17591
  role: "system",
17399
- content: `[prior_turns_summary: ${summaryText}]`
17592
+ content: `[prior_turns_summary: ${enriched}]`
17400
17593
  };
17401
17594
  const summaryTokens = estimateMessages([summaryMsg]);
17402
17595
  const tail = ctx.messages.slice(boundary);
@@ -17404,27 +17597,41 @@ var IntelligentCompactor = class {
17404
17597
  return { saved: Math.max(0, removedTokens - summaryTokens), digest: summaryText };
17405
17598
  }
17406
17599
  async callSummarizer(messages, ctx) {
17407
- const prompt = [
17408
- { type: "text", text: this.summarizerPrompt },
17409
- { type: "text", text: "\n\nConversation to summarize:\n" },
17410
- ...this.messagesToText(messages)
17411
- ];
17412
- const req = {
17413
- model: this.summarizerModel ?? ctx.model,
17414
- system: prompt,
17415
- messages: [],
17416
- maxTokens: 1024
17417
- };
17418
- const ac = ctx.signal ? void 0 : new AbortController();
17419
- const signal = ctx.signal ?? ac?.signal;
17420
- let res;
17421
- try {
17422
- res = await this.provider.complete(req, { signal });
17423
- } finally {
17424
- ac?.abort();
17600
+ if (this.oneShotOrchestrator) {
17601
+ const result = await this.oneShotOrchestrator.call({
17602
+ system: this.summarizerPrompt,
17603
+ messages: [{ role: "user", content: this.messagesToSummary(messages) }],
17604
+ model: this.summarizerModel ?? "deepseek-chat",
17605
+ timeoutMs: 3e4,
17606
+ maxTokens: 1024,
17607
+ signal: ctx.signal,
17608
+ fallbackProfile: "summary"
17609
+ });
17610
+ if (result.text && !result.error) return result.text;
17611
+ } else {
17612
+ const prompt = [
17613
+ { type: "text", text: this.summarizerPrompt },
17614
+ { type: "text", text: "\n\nConversation to summarize:\n" },
17615
+ ...this.messagesToText(messages)
17616
+ ];
17617
+ const req = {
17618
+ model: this.summarizerModel ?? ctx.model,
17619
+ system: prompt,
17620
+ messages: [],
17621
+ maxTokens: 1024
17622
+ };
17623
+ const ac = ctx.signal ? void 0 : new AbortController();
17624
+ const signal = ctx.signal ?? ac?.signal;
17625
+ let res;
17626
+ try {
17627
+ res = await this.provider.complete(req, { signal });
17628
+ } finally {
17629
+ ac?.abort();
17630
+ }
17631
+ const textBlocks = res.content.filter(isTextBlock);
17632
+ return textBlocks.map((b) => b.text).join("\n").trim() || "(empty summary)";
17425
17633
  }
17426
- const textBlocks = res.content.filter(isTextBlock);
17427
- return textBlocks.map((b) => b.text).join("\n").trim() || "(empty summary)";
17634
+ return "(empty summary)";
17428
17635
  }
17429
17636
  messagesToText(messages) {
17430
17637
  const lines = [];
@@ -17441,6 +17648,22 @@ var IntelligentCompactor = class {
17441
17648
  }
17442
17649
  return [{ type: "text", text: lines.join("\n") }];
17443
17650
  }
17651
+ /** Compact string summary of messages (no TextBlock wrapper). */
17652
+ messagesToSummary(messages) {
17653
+ const lines = [];
17654
+ for (const m of messages) {
17655
+ const role = m.role.padEnd(10, " ");
17656
+ if (typeof m.content === "string") {
17657
+ lines.push(`[${role}]: ${m.content.slice(0, 500)}`);
17658
+ } else if (Array.isArray(m.content)) {
17659
+ const textParts = m.content.filter(isTextBlock).map((b) => b.text);
17660
+ if (textParts.length > 0) {
17661
+ lines.push(`[${role}]: ${textParts.join(" ").slice(0, 500)}`);
17662
+ }
17663
+ }
17664
+ }
17665
+ return lines.join("\n");
17666
+ }
17444
17667
  };
17445
17668
 
17446
17669
  // src/execution/parallel-eternal-engine.ts
@@ -18396,6 +18619,7 @@ var LLMSelector = class {
18396
18619
  maxContextTokens;
18397
18620
  systemPrompt;
18398
18621
  maxOutputTokens;
18622
+ oneShotOrchestrator;
18399
18623
  constructor(opts) {
18400
18624
  this.provider = opts.provider;
18401
18625
  this.model = opts.model ?? "unknown";
@@ -18407,6 +18631,7 @@ var LLMSelector = class {
18407
18631
  this.maxContextTokens = opts.maxContextTokens ?? 4e4;
18408
18632
  this.systemPrompt = opts.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
18409
18633
  this.maxOutputTokens = opts.maxOutputTokens ?? 1024;
18634
+ this.oneShotOrchestrator = opts.oneShotOrchestrator;
18410
18635
  }
18411
18636
  async select(messages, maxToKeep) {
18412
18637
  const effectiveBudget = Math.min(maxToKeep, this.maxContextTokens);
@@ -18430,12 +18655,23 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
18430
18655
  let raw;
18431
18656
  const ac = new AbortController();
18432
18657
  try {
18433
- const timeoutSignal = AbortSignal.timeout(3e4);
18434
- const res = await this.provider.complete(req, {
18435
- signal: AbortSignal.any([ac.signal, timeoutSignal])
18436
- });
18437
- const textBlocks = res.content.filter(isTextBlock);
18438
- raw = textBlocks.map((b) => b.text).join("\n").trim();
18658
+ if (this.oneShotOrchestrator) {
18659
+ const result = await this.oneShotOrchestrator.call({
18660
+ system: systemText + budgetInstruction,
18661
+ userPrompt: historyText,
18662
+ maxTokens: this.maxOutputTokens,
18663
+ timeoutMs: 3e4
18664
+ });
18665
+ if (result.error) throw new Error(result.error);
18666
+ raw = result.text;
18667
+ } else {
18668
+ const timeoutSignal = AbortSignal.timeout(3e4);
18669
+ const res = await this.provider.complete(req, {
18670
+ signal: AbortSignal.any([ac.signal, timeoutSignal])
18671
+ });
18672
+ const textBlocks = res.content.filter(isTextBlock);
18673
+ raw = textBlocks.map((b) => b.text).join("\n").trim();
18674
+ }
18439
18675
  } catch (err) {
18440
18676
  if (err instanceof Error) {
18441
18677
  console.warn(
@@ -18555,12 +18791,12 @@ var SelectiveCompactor = class {
18555
18791
  constructor(opts) {
18556
18792
  this.provider = opts.provider;
18557
18793
  this.selector = opts.selector ?? new LLMSelector({ provider: opts.provider, model: opts.selectorModel, maxOutputTokens: opts.selectorMaxOutputTokens });
18558
- this.warnThreshold = opts.warnThreshold ?? 0.6;
18559
- this.softThreshold = opts.softThreshold ?? 0.75;
18560
- this.hardThreshold = opts.hardThreshold ?? 0.9;
18794
+ this.warnThreshold = opts.warnThreshold ?? 0.5;
18795
+ this.softThreshold = opts.softThreshold ?? 0.65;
18796
+ this.hardThreshold = opts.hardThreshold ?? 0.8;
18561
18797
  this.maxContext = opts.maxContext ?? 128e3;
18562
18798
  this.preserveK = opts.preserveK ?? 4;
18563
- this.eliseThreshold = opts.eliseThreshold ?? 500;
18799
+ this.eliseThreshold = opts.eliseThreshold ?? 300;
18564
18800
  this.summarizerModel = opts.summarizerModel ?? opts.selectorModel;
18565
18801
  if (this.summarizerModel === void 0 && (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1")) {
18566
18802
  console.warn(
@@ -18753,7 +18989,7 @@ Summarize the following message range:`;
18753
18989
  };
18754
18990
 
18755
18991
  // src/execution/skill-loader.ts
18756
- import * as fs4 from "node:fs/promises";
18992
+ import * as fs5 from "node:fs/promises";
18757
18993
  import * as path15 from "node:path";
18758
18994
 
18759
18995
  // src/skills/foreign-sources.ts
@@ -18984,7 +19220,7 @@ async function entryIsDirectory(dir, entry) {
18984
19220
  if (entry.isDirectory()) return true;
18985
19221
  if (entry.isSymbolicLink()) {
18986
19222
  try {
18987
- return (await fs4.stat(path15.join(dir, entry.name))).isDirectory();
19223
+ return (await fs5.stat(path15.join(dir, entry.name))).isDirectory();
18988
19224
  } catch {
18989
19225
  return false;
18990
19226
  }
@@ -19027,12 +19263,12 @@ var DefaultSkillLoader = class {
19027
19263
  const seen = /* @__PURE__ */ new Set();
19028
19264
  for (const { dir, source, originTool } of this.dirs) {
19029
19265
  try {
19030
- const entries = await fs4.readdir(dir, { withFileTypes: true });
19266
+ const entries = await fs5.readdir(dir, { withFileTypes: true });
19031
19267
  for (const e of entries) {
19032
19268
  if (!await entryIsDirectory(dir, e)) continue;
19033
19269
  const skillFile = path15.join(dir, e.name, "SKILL.md");
19034
19270
  try {
19035
- const raw = await fs4.readFile(skillFile, "utf8");
19271
+ const raw = await fs5.readFile(skillFile, "utf8");
19036
19272
  const fm = parseSkillFrontmatter(raw);
19037
19273
  if (!fm.name || !fm.description) continue;
19038
19274
  if (!isValidSkillNameFormat(fm.name)) continue;
@@ -19104,7 +19340,7 @@ var DefaultSkillLoader = class {
19104
19340
  if (cached !== void 0) return cached;
19105
19341
  const m = await this.find(name);
19106
19342
  if (!m) throw new Error(`Skill "${name}" not found`);
19107
- const body = await fs4.readFile(m.path, "utf8");
19343
+ const body = await fs5.readFile(m.path, "utf8");
19108
19344
  this.bodyCache.set(key, body);
19109
19345
  return body;
19110
19346
  }
@@ -19117,9 +19353,9 @@ var DefaultSkillLoader = class {
19117
19353
  const savePath = path15.join(path15.dirname(m.path), "SKILL.save.md");
19118
19354
  let result;
19119
19355
  try {
19120
- result = await fs4.readFile(savePath, "utf8");
19356
+ result = await fs5.readFile(savePath, "utf8");
19121
19357
  } catch {
19122
- const full = await fs4.readFile(m.path, "utf8");
19358
+ const full = await fs5.readFile(m.path, "utf8");
19123
19359
  const body = stripFrontmatter2(full);
19124
19360
  const compact = compactSkillBody(body);
19125
19361
  if (compact) {
@@ -19147,11 +19383,11 @@ function parseDescriptionFromText(desc) {
19147
19383
  }
19148
19384
 
19149
19385
  // src/execution/prompt-loader.ts
19150
- import * as fs6 from "node:fs/promises";
19386
+ import * as fs7 from "node:fs/promises";
19151
19387
  import * as path17 from "node:path";
19152
19388
 
19153
19389
  // src/storage/prompt-store.ts
19154
- import * as fs5 from "node:fs/promises";
19390
+ import * as fs6 from "node:fs/promises";
19155
19391
  import * as path16 from "node:path";
19156
19392
  var SCHEMA_VERSION = 2;
19157
19393
  function migratePromptEntry(raw) {
@@ -19213,12 +19449,12 @@ var DefaultPromptStore = class {
19213
19449
  await ensureDir(this.dir);
19214
19450
  const entries = [];
19215
19451
  try {
19216
- const files = await fs5.readdir(this.dir);
19452
+ const files = await fs6.readdir(this.dir);
19217
19453
  for (const file of files) {
19218
19454
  if (!file.endsWith(".json")) continue;
19219
19455
  try {
19220
19456
  const raw = JSON.parse(
19221
- await fs5.readFile(path16.join(this.dir, file), "utf8")
19457
+ await fs6.readFile(path16.join(this.dir, file), "utf8")
19222
19458
  );
19223
19459
  const migrated = migratePromptEntry(raw.entry);
19224
19460
  if (migrated) entries.push(migrated);
@@ -19234,7 +19470,7 @@ var DefaultPromptStore = class {
19234
19470
  async get(id) {
19235
19471
  const file = path16.join(this.dir, `${id}.json`);
19236
19472
  try {
19237
- const raw = JSON.parse(await fs5.readFile(file, "utf8"));
19473
+ const raw = JSON.parse(await fs6.readFile(file, "utf8"));
19238
19474
  return migratePromptEntry(raw.entry);
19239
19475
  } catch {
19240
19476
  return null;
@@ -19249,7 +19485,7 @@ var DefaultPromptStore = class {
19249
19485
  async delete(id) {
19250
19486
  const file = path16.join(this.dir, `${id}.json`);
19251
19487
  try {
19252
- await fs5.unlink(file);
19488
+ await fs6.unlink(file);
19253
19489
  return true;
19254
19490
  } catch {
19255
19491
  return false;
@@ -19449,7 +19685,7 @@ var DefaultPromptLoader = class {
19449
19685
  const files = await walkJson(dir);
19450
19686
  for (const file of files) {
19451
19687
  try {
19452
- const parsed = JSON.parse(await fs6.readFile(file, "utf8"));
19688
+ const parsed = JSON.parse(await fs7.readFile(file, "utf8"));
19453
19689
  const migrated = migratePromptEntry(parsed);
19454
19690
  if (migrated) out.push({ ...migrated, source: "builtin" });
19455
19691
  } catch {
@@ -19463,7 +19699,7 @@ async function walkJson(dir) {
19463
19699
  const out = [];
19464
19700
  let entries;
19465
19701
  try {
19466
- entries = await fs6.readdir(dir, { withFileTypes: true });
19702
+ entries = await fs7.readdir(dir, { withFileTypes: true });
19467
19703
  } catch {
19468
19704
  return out;
19469
19705
  }
@@ -19640,7 +19876,8 @@ var ProviderBackedCompactor = class {
19640
19876
  }
19641
19877
  return new IntelligentCompactor({
19642
19878
  ...common,
19643
- summarizerModel: this.opts.summarizerModel
19879
+ summarizerModel: this.opts.summarizerModel,
19880
+ oneShotOrchestrator: this.opts.oneShotOrchestrator
19644
19881
  });
19645
19882
  }
19646
19883
  };
@@ -19655,9 +19892,12 @@ function readPolicy(ctx) {
19655
19892
  // src/execution/tool-executor.ts
19656
19893
  import { randomUUID as randomUUID10 } from "node:crypto";
19657
19894
  import { isDeepStrictEqual } from "node:util";
19658
- import * as fs7 from "node:fs/promises";
19895
+ import * as fs8 from "node:fs/promises";
19659
19896
  import * as path18 from "node:path";
19660
19897
 
19898
+ // src/types/tool-executor.ts
19899
+ var GOVERNED_TOOL_EXECUTOR_META_KEY = "toolExecutor.executeGoverned";
19900
+
19661
19901
  // src/types/tool-markers.ts
19662
19902
  var MALFORMED_ARG_MARKERS = ["__raw", "__raw_arguments", "_raw"];
19663
19903
 
@@ -19774,6 +20014,18 @@ var ToolExecutor = class _ToolExecutor {
19774
20014
  * Returns the execution results and the remaining output budget.
19775
20015
  */
19776
20016
  async executeBatch(toolUses, ctx, strategy) {
20017
+ return this.withGovernedExecutionBridge(
20018
+ ctx,
20019
+ () => this.executeBatchInternal(toolUses, ctx, strategy)
20020
+ );
20021
+ }
20022
+ /**
20023
+ * Internal batch implementation. Nested meta-tool calls enter here through
20024
+ * the governed bridge so they traverse the same validation, hooks,
20025
+ * permission/capability, timeout, scrubbing, and audit path without
20026
+ * replacing the bridge that is already active for the outer call.
20027
+ */
20028
+ async executeBatchInternal(toolUses, ctx, strategy) {
19777
20029
  let budget = this.opts.perIterationOutputCapBytes ?? 1e5;
19778
20030
  const runOne = async (use0) => {
19779
20031
  const start = Date.now();
@@ -19944,8 +20196,9 @@ ${errorDetails}`,
19944
20196
  );
19945
20197
  if (post.additionalContext) {
19946
20198
  if (post.contextAs === "separate") {
19947
- const block = { type: "text", text: post.additionalContext };
19948
- ctx.state.appendMessage({ role: "user", content: [block] });
20199
+ ctx.pendingPostToolContext = ctx.pendingPostToolContext ? `${ctx.pendingPostToolContext}
20200
+
20201
+ ${post.additionalContext}` : post.additionalContext;
19949
20202
  } else {
19950
20203
  const appended = `
19951
20204
 
@@ -20045,14 +20298,56 @@ ${post.additionalContext}`;
20045
20298
  };
20046
20299
  }
20047
20300
  /**
20048
- * Execute a single tool with timeout, permission check, and output capping.
20049
- * Emits `tool.started` via the injected EventBus (if any) right before
20050
- * invoking the tool closes the observability gap between "model decided
20051
- * to call a tool" and "tool.executed".
20301
+ * Install the executor-owned bridge used by meta-tools for nested calls.
20302
+ * The bridge deliberately returns the governed, scrubbed ToolResultBlock
20303
+ * content rather than the raw tool value. This prevents a meta-tool from
20304
+ * bypassing output controls while preserving structured success/failure.
20305
+ */
20306
+ async withGovernedExecutionBridge(ctx, run) {
20307
+ const previous = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY];
20308
+ if (typeof previous === "function") return run();
20309
+ const bridge = async (toolName, input) => {
20310
+ const nestedUse = {
20311
+ type: "tool_use",
20312
+ id: `nested-${randomUUID10()}`,
20313
+ name: toolName,
20314
+ input
20315
+ };
20316
+ const batch = await this.executeBatchInternal([nestedUse], ctx, "sequential");
20317
+ const output = batch.outputs[0];
20318
+ if (!output) {
20319
+ return { success: false, error: `tool "${toolName}" produced no result` };
20320
+ }
20321
+ if (output.result.type === "tool_confirm_pending") {
20322
+ return {
20323
+ success: false,
20324
+ error: `tool "${toolName}" requires separate confirmation; call it directly`
20325
+ };
20326
+ }
20327
+ if (output.result.is_error) {
20328
+ return { success: false, error: String(output.result.content) };
20329
+ }
20330
+ return { success: true, result: output.result.content };
20331
+ };
20332
+ ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY] = bridge;
20333
+ try {
20334
+ return await run();
20335
+ } finally {
20336
+ if (previous === void 0) delete ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY];
20337
+ else ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY] = previous;
20338
+ }
20339
+ }
20340
+ /**
20341
+ * Execute a single tool with timeout and output capping after its permission
20342
+ * decision has already been resolved by executeBatch/the confirmation flow.
20343
+ * Also installs the governed bridge because confirmed tools are re-run
20344
+ * through this method after a pending confirmation.
20052
20345
  */
20053
20346
  async executeTool(tool, use, ctx, budget) {
20054
- const text = await this.produceToolOutput(tool, use, ctx, budget);
20055
- return this.settleToolOutput(tool, use, text, budget);
20347
+ return this.withGovernedExecutionBridge(ctx, async () => {
20348
+ const text = await this.produceToolOutput(tool, use, ctx, budget);
20349
+ return this.settleToolOutput(tool, use, text, budget);
20350
+ });
20056
20351
  }
20057
20352
  /**
20058
20353
  * Async "produce" phase: run the tool, serialize, scrub, and spill an
@@ -20407,11 +20702,11 @@ async function maybePersistLargeToolOutput(toolName, content, budget) {
20407
20702
  }
20408
20703
  try {
20409
20704
  const dir = path18.join(wstackGlobalRoot(), "tool-output");
20410
- await fs7.mkdir(dir, { recursive: true });
20705
+ await fs8.mkdir(dir, { recursive: true });
20411
20706
  const safeTool = toolName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
20412
20707
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
20413
20708
  const filePath = path18.join(dir, `${stamp}-${safeTool}-${randomUUID10()}.log`);
20414
- await fs7.writeFile(filePath, content, "utf8");
20709
+ await fs8.writeFile(filePath, content, "utf8");
20415
20710
  const marker = `[full tool output: ${bytes} bytes at ${filePath}; read/grep that file selectively instead of re-running or requesting more output]`;
20416
20711
  const fixedBytes = Buffer.byteLength(marker + TOOL_OUTPUT_ARTIFACT_OMISSION, "utf8");
20417
20712
  const previewBytes = Math.min(
@@ -20467,9 +20762,9 @@ function roughEstimate(messages) {
20467
20762
  }
20468
20763
  function createContextManagerTool(opts = {}) {
20469
20764
  const minCompactThreshold = opts.minCompactThreshold ?? 0;
20470
- const noopRetryDeltaTokens = opts.noopRetryDeltaTokens ?? 2e3;
20765
+ const noopRetryDeltaTokens = opts.noopRetryDeltaTokens ?? 1e3;
20471
20766
  const configuredMaxContext = opts.maxContext;
20472
- const compactThresholdFraction = opts.compactThresholdFraction ?? 0.5;
20767
+ const compactThresholdFraction = opts.compactThresholdFraction ?? 0.4;
20473
20768
  let lastNoopTokens = 0;
20474
20769
  return {
20475
20770
  name: CONTEXT_MANAGER_TOOL_NAME,
@@ -21107,17 +21402,17 @@ function codexModelMeta(id) {
21107
21402
  }
21108
21403
 
21109
21404
  // src/models/mode-store.ts
21110
- import * as fs8 from "node:fs/promises";
21405
+ import * as fs9 from "node:fs/promises";
21111
21406
  import * as path21 from "node:path";
21112
21407
 
21113
21408
  // src/types/mode-prompts.ts
21114
- import { readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
21409
+ import { readFileSync as readFileSync4, statSync as statSync4 } from "node:fs";
21115
21410
  import * as path20 from "node:path";
21116
21411
  import { fileURLToPath as fileURLToPath4 } from "node:url";
21117
21412
  function modePrompt(id) {
21118
21413
  for (const dir of modePromptDirCandidates()) {
21119
21414
  try {
21120
- return readFileSync3(path20.join(dir, `${id}.md`), "utf8").trimEnd();
21415
+ return readFileSync4(path20.join(dir, `${id}.md`), "utf8").trimEnd();
21121
21416
  } catch {
21122
21417
  }
21123
21418
  }
@@ -21134,7 +21429,7 @@ function modePromptDirCandidates() {
21134
21429
  }
21135
21430
  function isDirectory3(candidate) {
21136
21431
  try {
21137
- return statSync3(candidate).isDirectory();
21432
+ return statSync4(candidate).isDirectory();
21138
21433
  } catch {
21139
21434
  return false;
21140
21435
  }
@@ -21361,7 +21656,7 @@ var DefaultModeStore = class {
21361
21656
  async loadActiveMode() {
21362
21657
  try {
21363
21658
  const configPath = path21.join(this.configDir, "mode.json");
21364
- const content = await fs8.readFile(configPath, "utf8");
21659
+ const content = await fs9.readFile(configPath, "utf8");
21365
21660
  const data = JSON.parse(content);
21366
21661
  this.activeModeId = data.activeMode ?? null;
21367
21662
  } catch {
@@ -21370,7 +21665,7 @@ var DefaultModeStore = class {
21370
21665
  }
21371
21666
  async saveActiveMode() {
21372
21667
  try {
21373
- await fs8.mkdir(this.configDir, { recursive: true });
21668
+ await fs9.mkdir(this.configDir, { recursive: true });
21374
21669
  const configPath = path21.join(this.configDir, "mode.json");
21375
21670
  await atomicWrite(
21376
21671
  configPath,
@@ -21383,13 +21678,13 @@ var DefaultModeStore = class {
21383
21678
  async function loadProjectModes(modesDir) {
21384
21679
  const modes = [];
21385
21680
  try {
21386
- const entries = await fs8.readdir(modesDir);
21681
+ const entries = await fs9.readdir(modesDir);
21387
21682
  for (const entry of entries) {
21388
21683
  if (!entry.endsWith(".md") && !entry.endsWith(".txt")) continue;
21389
21684
  const filePath = path21.join(modesDir, entry);
21390
- const stat13 = await fs8.stat(filePath);
21391
- if (!stat13.isFile()) continue;
21392
- const content = await fs8.readFile(filePath, "utf8");
21685
+ const stat12 = await fs9.stat(filePath);
21686
+ if (!stat12.isFile()) continue;
21687
+ const content = await fs9.readFile(filePath, "utf8");
21393
21688
  const id = path21.basename(entry, path21.extname(entry));
21394
21689
  modes.push({
21395
21690
  id,
@@ -21407,7 +21702,7 @@ async function loadUserModes(modesDir) {
21407
21702
  const modes = [];
21408
21703
  try {
21409
21704
  const manifestPath = path21.join(modesDir, "modes.json");
21410
- const content = await fs8.readFile(manifestPath, "utf8");
21705
+ const content = await fs9.readFile(manifestPath, "utf8");
21411
21706
  const manifest = JSON.parse(content);
21412
21707
  for (const mode of manifest.modes) {
21413
21708
  modes.push(mode);
@@ -21418,7 +21713,7 @@ async function loadUserModes(modesDir) {
21418
21713
  }
21419
21714
 
21420
21715
  // src/models/models-registry.ts
21421
- import * as fs9 from "node:fs/promises";
21716
+ import * as fs10 from "node:fs/promises";
21422
21717
  import * as path22 from "node:path";
21423
21718
  var DEFAULT_URL = "https://models.dev/api.json";
21424
21719
  var ENV_URL_KEY = "WRONGSTACK_MODELS_DEV_URL";
@@ -21660,7 +21955,7 @@ var DefaultModelsRegistry = class {
21660
21955
  async readOverlayFile() {
21661
21956
  if (!this.overlayFile) return void 0;
21662
21957
  try {
21663
- const raw = await fs9.readFile(this.overlayFile, "utf8");
21958
+ const raw = await fs10.readFile(this.overlayFile, "utf8");
21664
21959
  return JSON.parse(raw);
21665
21960
  } catch {
21666
21961
  return void 0;
@@ -21739,7 +22034,7 @@ var DefaultModelsRegistry = class {
21739
22034
  }
21740
22035
  async readCacheAt(file) {
21741
22036
  try {
21742
- const raw = await fs9.readFile(file, "utf8");
22037
+ const raw = await fs10.readFile(file, "utf8");
21743
22038
  return JSON.parse(raw);
21744
22039
  } catch {
21745
22040
  return void 0;
@@ -22171,7 +22466,7 @@ async function startMetricsServer(opts) {
22171
22466
  const tls = opts.tls;
22172
22467
  const useHttps = !!(tls?.cert && tls?.key);
22173
22468
  const host = opts.host ?? "127.0.0.1";
22174
- const path29 = opts.path ?? "/metrics";
22469
+ const path28 = opts.path ?? "/metrics";
22175
22470
  const healthPath = opts.healthPath ?? "/healthz";
22176
22471
  const healthRegistry = opts.healthRegistry;
22177
22472
  const listener = (req, res) => {
@@ -22181,7 +22476,7 @@ async function startMetricsServer(opts) {
22181
22476
  return;
22182
22477
  }
22183
22478
  const url = req.url.split("?")[0];
22184
- if (url === path29) {
22479
+ if (url === path28) {
22185
22480
  let body;
22186
22481
  try {
22187
22482
  body = renderPrometheus(opts.sink.snapshot());
@@ -22218,9 +22513,9 @@ async function startMetricsServer(opts) {
22218
22513
  let server;
22219
22514
  if (useHttps && tls) {
22220
22515
  const { createServer } = await import("node:https");
22221
- const { readFileSync: readFileSync5 } = await import("node:fs");
22516
+ const { readFileSync: readFileSync6 } = await import("node:fs");
22222
22517
  server = createServer(
22223
- { cert: readFileSync5(tls.cert), key: readFileSync5(tls.key) },
22518
+ { cert: readFileSync6(tls.cert), key: readFileSync6(tls.key) },
22224
22519
  listener
22225
22520
  );
22226
22521
  } else {
@@ -22245,7 +22540,7 @@ async function startMetricsServer(opts) {
22245
22540
  const protocol = useHttps ? "https" : "http";
22246
22541
  return {
22247
22542
  port: boundPort,
22248
- url: `${protocol}://${host}:${boundPort}${path29}`,
22543
+ url: `${protocol}://${host}:${boundPort}${path28}`,
22249
22544
  close: () => new Promise((resolve12, reject) => {
22250
22545
  server.close((err) => err ? reject(err) : resolve12());
22251
22546
  })
@@ -22517,7 +22812,7 @@ function startOtlpTraceExporter(opts) {
22517
22812
  }
22518
22813
 
22519
22814
  // src/security/permission-policy.ts
22520
- import * as fs10 from "node:fs/promises";
22815
+ import * as fs11 from "node:fs/promises";
22521
22816
 
22522
22817
  // src/security/permission-policy-schema.ts
22523
22818
  var TRUST_POLICY_LIMITS = Object.freeze({
@@ -22562,19 +22857,19 @@ var UNSAFE_PROPERTY_NAMES = /* @__PURE__ */ new Set(["__proto__", "prototype", "
22562
22857
  function isRecord5(value) {
22563
22858
  return typeof value === "object" && value !== null && !Array.isArray(value);
22564
22859
  }
22565
- function error(diagnostics, code, path29, message) {
22566
- diagnostics.push({ severity: "error", code, path: path29, message });
22860
+ function error(diagnostics, code, path28, message) {
22861
+ diagnostics.push({ severity: "error", code, path: path28, message });
22567
22862
  }
22568
- function validatePatterns(value, path29, diagnostics) {
22863
+ function validatePatterns(value, path28, diagnostics) {
22569
22864
  if (!Array.isArray(value)) {
22570
- error(diagnostics, "invalid_pattern_list", path29, "must be an array of strings");
22865
+ error(diagnostics, "invalid_pattern_list", path28, "must be an array of strings");
22571
22866
  return void 0;
22572
22867
  }
22573
22868
  if (value.length > TRUST_POLICY_LIMITS.maxPatternsPerRule) {
22574
22869
  error(
22575
22870
  diagnostics,
22576
22871
  "too_many_patterns",
22577
- path29,
22872
+ path28,
22578
22873
  `must contain at most ${TRUST_POLICY_LIMITS.maxPatternsPerRule} patterns`
22579
22874
  );
22580
22875
  return void 0;
@@ -22582,7 +22877,7 @@ function validatePatterns(value, path29, diagnostics) {
22582
22877
  const patterns = [];
22583
22878
  const seen = /* @__PURE__ */ new Set();
22584
22879
  for (const [index, pattern] of value.entries()) {
22585
- const itemPath = `${path29}[${index}]`;
22880
+ const itemPath = `${path28}[${index}]`;
22586
22881
  if (typeof pattern !== "string" || pattern.length === 0 || pattern.length > TRUST_POLICY_LIMITS.maxPatternChars) {
22587
22882
  error(
22588
22883
  diagnostics,
@@ -22906,7 +23201,7 @@ var DefaultPermissionPolicy = class {
22906
23201
  this.policyDiagnostics = [];
22907
23202
  this.policyInvalid = false;
22908
23203
  try {
22909
- const raw = await fs10.readFile(this.trustFile, "utf8");
23204
+ const raw = await fs11.readFile(this.trustFile, "utf8");
22910
23205
  const parsed = safeParse(raw);
22911
23206
  if (!parsed.ok) {
22912
23207
  this.policy = {};
@@ -23306,7 +23601,12 @@ function hasCredentialAnchors(text) {
23306
23601
  text.includes("_TOKEN=") || // ACCESS_TOKEN=, AUTH_TOKEN=, ...
23307
23602
  text.includes("_SECRET=") || // API_SECRET=, CLIENT_SECRET=, ...
23308
23603
  text.includes("_PASSWORD=") || // DB_PASSWORD=, ROOT_PASSWORD=, ...
23309
- text.includes("mongodb://") || text.includes("mongodb+srv://") || text.includes("postgres://") || text.includes("postgresql://") || text.includes("mysql://") || text.includes("redis://");
23604
+ text.includes("mongodb://") || text.includes("mongodb+srv://") || text.includes("postgres://") || text.includes("postgresql://") || text.includes("mysql://") || text.includes("redis://") || // JSON-style credential keys: tool outputs often serialise objects as
23605
+ // raw JSON strings rather than parsed objects. The value may not start
23606
+ // with a recognisable prefix (sk-, ghp_, etc.), so we anchor on common
23607
+ // key names. The `"` prefix avoids matching prose that happens to mention
23608
+ // these words — JSON serialisation always quotes string keys.
23609
+ text.includes('"apiKey"') || text.includes('"api_key"') || text.includes('"token"') || text.includes('"secret"') || text.includes('"password"') || text.includes('"authorization"') || text.includes('"bearer"') || text.includes('"private_key"') || text.includes('"access_token"') || text.includes('"refresh_token"') || text.includes('"client_secret"');
23310
23610
  }
23311
23611
  var DefaultSecretScrubber = class {
23312
23612
  scrub(text) {
@@ -23379,7 +23679,7 @@ var DefaultSecretScrubber = class {
23379
23679
 
23380
23680
  // src/security/secret-vault.ts
23381
23681
  import { createCipheriv, createDecipheriv, randomBytes as randomBytes4, scryptSync } from "node:crypto";
23382
- import * as fs11 from "node:fs";
23682
+ import * as fs12 from "node:fs";
23383
23683
  import * as fsp12 from "node:fs/promises";
23384
23684
  import * as path23 from "node:path";
23385
23685
 
@@ -23424,7 +23724,7 @@ function walk2(node, vault, transform) {
23424
23724
  }
23425
23725
  return out;
23426
23726
  }
23427
- var SECRET_KEY_PATTERN = /(?:apikey|api_key|authtoken|auth_token|bearer|secret|password|passwd|pwd|refreshtoken|refresh_token|sessionkey|session_key|access[_-]?token|private[_-]?key)/i;
23727
+ var SECRET_KEY_PATTERN = /(?:apikey|api_key|authtoken|auth_token|bearer|secret|password|passwd|pwd|refreshtoken|refresh_token|sessionkey|session_key|access[_-]?token|private[_-]?key|token\b)/i;
23428
23728
  var NON_SECRET_OVERRIDES = /* @__PURE__ */ new Set(["publickey", "public_key"]);
23429
23729
  function isSecretField(name) {
23430
23730
  const lc = name.toLowerCase();
@@ -23521,8 +23821,8 @@ function checkKeyFilePermissions(keyFile, opts) {
23521
23821
  if (process.platform === "win32") return;
23522
23822
  const warn = opts?.warn ?? ((msg) => console.warn(msg));
23523
23823
  try {
23524
- const stat13 = fs11.statSync(keyFile);
23525
- const actualMode = stat13.mode & 511;
23824
+ const stat12 = fs12.statSync(keyFile);
23825
+ const actualMode = stat12.mode & 511;
23526
23826
  if (actualMode !== KEY_FILE_MODE) {
23527
23827
  warn(
23528
23828
  `Key file ${keyFile} has mode ${actualMode.toString(8)} \u2014 expected ${KEY_FILE_MODE.toString(8)}. Run: chmod ${KEY_FILE_MODE.toString(8)} ${keyFile}`
@@ -23618,16 +23918,16 @@ var DefaultSecretVault = class {
23618
23918
  const oldVersion = this._keyVersion;
23619
23919
  const newKey = randomBytes4(KEY_BYTES);
23620
23920
  const newVersion = oldVersion + 1;
23621
- fs11.mkdirSync(path23.dirname(this.keyFile), { recursive: true });
23921
+ fs12.mkdirSync(path23.dirname(this.keyFile), { recursive: true });
23622
23922
  const passphrase = getVaultPassphrase();
23623
23923
  if (passphrase) {
23624
- fs11.writeFileSync(this.keyFile, wrapDataKey(newKey, newVersion, passphrase), { mode: 384 });
23924
+ fs12.writeFileSync(this.keyFile, wrapDataKey(newKey, newVersion, passphrase), { mode: 384 });
23625
23925
  } else {
23626
23926
  const keyFileBuf = Buffer.alloc(VERSIONED_KEY_FILE_SIZE);
23627
23927
  KEY_FILE_MAGIC.copy(keyFileBuf, 0);
23628
23928
  keyFileBuf[KEY_FILE_MAGIC.length] = newVersion;
23629
23929
  newKey.copy(keyFileBuf, KEY_FILE_MAGIC.length + 1);
23630
- fs11.writeFileSync(this.keyFile, keyFileBuf, { mode: 384 });
23930
+ fs12.writeFileSync(this.keyFile, keyFileBuf, { mode: 384 });
23631
23931
  }
23632
23932
  checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
23633
23933
  this.key = newKey;
@@ -23645,7 +23945,7 @@ var DefaultSecretVault = class {
23645
23945
  const passphrase = getVaultPassphrase();
23646
23946
  if (!passphrase || !this.key) return;
23647
23947
  try {
23648
- fs11.writeFileSync(this.keyFile, wrapDataKey(this.key, this._keyVersion, passphrase), {
23948
+ fs12.writeFileSync(this.keyFile, wrapDataKey(this.key, this._keyVersion, passphrase), {
23649
23949
  mode: 384
23650
23950
  });
23651
23951
  checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
@@ -23655,7 +23955,7 @@ var DefaultSecretVault = class {
23655
23955
  loadOrCreateKey() {
23656
23956
  if (this.key) return this.key;
23657
23957
  try {
23658
- const buf = fs11.readFileSync(this.keyFile);
23958
+ const buf = fs12.readFileSync(this.keyFile);
23659
23959
  if (isWrappedKeyFile(buf)) {
23660
23960
  const { key: key2, version } = unwrapDataKey(buf, this.keyFile);
23661
23961
  this.key = key2;
@@ -23702,15 +24002,15 @@ var DefaultSecretVault = class {
23702
24002
  } catch (err) {
23703
24003
  if (err.code !== "ENOENT") throw err;
23704
24004
  }
23705
- fs11.mkdirSync(path23.dirname(this.keyFile), { recursive: true });
24005
+ fs12.mkdirSync(path23.dirname(this.keyFile), { recursive: true });
23706
24006
  const key = randomBytes4(KEY_BYTES);
23707
24007
  const passphrase = getVaultPassphrase();
23708
24008
  const initialBytes = passphrase ? wrapDataKey(key, 1, passphrase) : key;
23709
24009
  try {
23710
- fs11.writeFileSync(this.keyFile, initialBytes, { mode: 384, flag: "wx" });
24010
+ fs12.writeFileSync(this.keyFile, initialBytes, { mode: 384, flag: "wx" });
23711
24011
  } catch (err) {
23712
24012
  if (err.code !== "EEXIST") throw err;
23713
- const buf = fs11.readFileSync(this.keyFile);
24013
+ const buf = fs12.readFileSync(this.keyFile);
23714
24014
  if (isWrappedKeyFile(buf)) {
23715
24015
  const { key: winnerKey, version } = unwrapDataKey(buf, this.keyFile);
23716
24016
  this.key = winnerKey;
@@ -23984,24 +24284,24 @@ function mergeAdjacentText(blocks) {
23984
24284
  }
23985
24285
 
23986
24286
  // src/storage/config-loader.ts
23987
- import * as fs12 from "node:fs/promises";
24287
+ import * as fs13 from "node:fs/promises";
23988
24288
  import * as path25 from "node:path";
23989
24289
 
23990
24290
  // src/types/config.ts
23991
24291
  var DEFAULT_TUI_THINKING_WORD = "thinking";
23992
24292
 
23993
24293
  // src/types/context-window.ts
23994
- var DEFAULT_CONTEXT_WINDOW_MODE_ID = "balanced";
24294
+ var DEFAULT_CONTEXT_WINDOW_MODE_ID = "frugal";
23995
24295
  var CONTEXT_WINDOW_MODES = Object.freeze([
23996
24296
  {
23997
24297
  id: "balanced",
23998
24298
  name: "Balanced",
23999
24299
  description: "Default rolling compaction: recent work stays verbatim, old tool output is trimmed.",
24000
- thresholds: { warn: 0.6, soft: 0.75, hard: 0.9 },
24300
+ thresholds: { warn: 0.5, soft: 0.65, hard: 0.8 },
24001
24301
  aggressiveOn: "soft",
24002
- preserveK: 10,
24003
- eliseThreshold: 2e3,
24004
- targetLoad: 0.65
24302
+ preserveK: 8,
24303
+ eliseThreshold: 1e3,
24304
+ targetLoad: 0.55
24005
24305
  },
24006
24306
  {
24007
24307
  id: "frugal",
@@ -24115,12 +24415,11 @@ var BEHAVIOR_DEFAULTS = {
24115
24415
  mcpServers: {},
24116
24416
  fallbackAuto: true,
24117
24417
  maxConcurrent: 4,
24118
- // YOLO on by default: auto-approve non-denied tool calls without a per-call
24119
- // prompt. Users
24120
- // who want the per-call prompts set `yolo: false`. Existing installs whose
24121
- // config lacks this key pick it up via fillMissingDefaults; an explicit
24122
- // `false` is preserved by the deep-merge (opt-out respected).
24123
- yolo: true,
24418
+ // YOLO opt-in: users who want auto-approve behaviour set `yolo: true`.
24419
+ // Off by default so prompt injection in repository content, fetched
24420
+ // content, or mailbox messages cannot induce arbitrary shell calls or
24421
+ // write operations without human confirmation.
24422
+ yolo: false,
24124
24423
  nextPrediction: false,
24125
24424
  hints: true,
24126
24425
  debugStream: false,
@@ -24145,7 +24444,7 @@ var BEHAVIOR_DEFAULTS = {
24145
24444
  terminalTitleAnimation: true,
24146
24445
  // Mirrored from the top-level yolo default so the autonomy subsystem
24147
24446
  // (which reads autonomy.yolo) stays consistent with config.yolo.
24148
- yolo: true,
24447
+ yolo: false,
24149
24448
  streamFleet: true,
24150
24449
  chime: false,
24151
24450
  confirmExit: true,
@@ -24597,7 +24896,7 @@ var DefaultConfigLoader = class {
24597
24896
  await withFileLock(fp, async () => {
24598
24897
  let parsed;
24599
24898
  try {
24600
- const raw = await fs12.readFile(fp, "utf8");
24899
+ const raw = await fs13.readFile(fp, "utf8");
24601
24900
  const result = safeParse(raw);
24602
24901
  if (!result.ok || !isPlainRecord(result.value)) {
24603
24902
  return;
@@ -24706,7 +25005,7 @@ var DefaultConfigLoader = class {
24706
25005
  const fp = this.paths.syncConfig;
24707
25006
  const t0 = Date.now();
24708
25007
  try {
24709
- const raw = await fs12.readFile(fp, "utf8");
25008
+ const raw = await fs13.readFile(fp, "utf8");
24710
25009
  const parsed = safeParse(raw);
24711
25010
  if (!parsed.ok || !parsed.value) {
24712
25011
  this.events?.emit("storage.read", {
@@ -24768,8 +25067,8 @@ var DefaultConfigLoader = class {
24768
25067
  const t0 = Date.now();
24769
25068
  let mtimeMs = null;
24770
25069
  try {
24771
- const stat13 = await fs12.stat(file);
24772
- mtimeMs = stat13.mtimeMs;
25070
+ const stat12 = await fs13.stat(file);
25071
+ mtimeMs = stat12.mtimeMs;
24773
25072
  const cached = this.jsonCache.get(file);
24774
25073
  if (cached && cached.mtimeMs === mtimeMs) {
24775
25074
  return structuredClone(cached.value);
@@ -24797,7 +25096,7 @@ var DefaultConfigLoader = class {
24797
25096
  }
24798
25097
  let raw;
24799
25098
  try {
24800
- raw = await fs12.readFile(file, "utf8");
25099
+ raw = await fs13.readFile(file, "utf8");
24801
25100
  } catch (err) {
24802
25101
  if (err.code !== "ENOENT") {
24803
25102
  this.events?.emit("storage.read", {
@@ -25027,938 +25326,9 @@ function deepFreeze(obj) {
25027
25326
  return Object.freeze(obj);
25028
25327
  }
25029
25328
 
25030
- // src/storage/memory-backend.ts
25329
+ // src/storage/plan-store.ts
25330
+ import * as fsp14 from "node:fs/promises";
25031
25331
  import { randomUUID as randomUUID11 } from "node:crypto";
25032
- import * as fs13 from "node:fs/promises";
25033
- import * as path26 from "node:path";
25034
-
25035
- // src/types/memory.ts
25036
- var MEMORY_TYPE_LABELS = {
25037
- fact: "Fact",
25038
- decision: "Decision",
25039
- convention: "Convention",
25040
- preference: "Preference",
25041
- reference: "Reference",
25042
- anti_pattern: "Anti-pattern"
25043
- };
25044
-
25045
- // src/storage/memory-backend.ts
25046
- var TYPE_PRIORITY_RE = /^\[(\w+)\|(\w+)\]\s+/;
25047
- var TAG_RE = /#([\w-]+)/g;
25048
- var MAX_MEMORY_CONSOLIDATE_BACKUPS = 5;
25049
- function formatMetadata(entry) {
25050
- const parts = [];
25051
- if (entry.type && entry.priority) {
25052
- parts.push(`[${entry.type}|${entry.priority}]`);
25053
- } else if (entry.type) {
25054
- parts.push(`[${entry.type}]`);
25055
- } else if (entry.priority) {
25056
- parts.push(`[${entry.priority}]`);
25057
- }
25058
- if (entry.tags && entry.tags.length > 0) {
25059
- parts.push(entry.tags.map((t) => `#${t}`).join(" "));
25060
- }
25061
- return parts.length > 0 ? ` ${parts.join(" ")}` : "";
25062
- }
25063
- function lineToEntry(line, scope) {
25064
- const trimmed = line.trim();
25065
- if (!trimmed.startsWith("- [")) return null;
25066
- const tsMatch = trimmed.match(/^-\s*\[([^\]]+)\]/);
25067
- if (!tsMatch) return null;
25068
- const ts = tsMatch[1] ?? "";
25069
- let rest = trimmed.slice(tsMatch[0].length).trim();
25070
- let type;
25071
- let priority;
25072
- const tpMatch = rest.match(TYPE_PRIORITY_RE);
25073
- if (tpMatch) {
25074
- const a = tpMatch[1] ?? "";
25075
- const b = tpMatch[2] ?? "";
25076
- if (isMemoryType(a)) {
25077
- type = a;
25078
- priority = isPriority(b) ? b : void 0;
25079
- } else if (isPriority(a)) {
25080
- priority = a;
25081
- }
25082
- rest = rest.slice(tpMatch[0].length).trim();
25083
- }
25084
- const idMatch = rest.match(/^mem_\d+_\w+\s+/);
25085
- let text;
25086
- if (idMatch) {
25087
- text = rest.slice(idMatch[0].length).trim();
25088
- } else {
25089
- text = rest.trim();
25090
- }
25091
- const tags = [];
25092
- let tagMatch;
25093
- TAG_RE.lastIndex = 0;
25094
- while ((tagMatch = TAG_RE.exec(text)) !== null) {
25095
- tags.push(tagMatch[1] ?? "");
25096
- }
25097
- const cleanText = text.replace(TAG_RE, "").replace(/\s{2,}/g, " ").trim();
25098
- if (!cleanText) return null;
25099
- return {
25100
- scope,
25101
- text: cleanText,
25102
- ts,
25103
- type,
25104
- priority,
25105
- tags: tags.length > 0 ? tags : void 0
25106
- };
25107
- }
25108
- function isMemoryType(s) {
25109
- return s in MEMORY_TYPE_LABELS;
25110
- }
25111
- function isPriority(s) {
25112
- return s === "critical" || s === "high" || s === "medium" || s === "low";
25113
- }
25114
- function buildInvertedIndex(entries) {
25115
- const wordMap = /* @__PURE__ */ new Map();
25116
- const tagMap = /* @__PURE__ */ new Map();
25117
- const indexed = new Array(entries.length);
25118
- for (let i = 0; i < entries.length; i++) {
25119
- const e = entries[i];
25120
- const words = e.text.toLowerCase().split(/\s+/).filter((w) => w.length > 0);
25121
- const tags = (e.tags ?? []).map((t) => t.toLowerCase());
25122
- indexed[i] = { entry: e, words, tags };
25123
- for (const w of words) {
25124
- const arr = wordMap.get(w);
25125
- if (arr) arr.push(i);
25126
- else wordMap.set(w, [i]);
25127
- }
25128
- for (const t of tags) {
25129
- const arr = tagMap.get(t);
25130
- if (arr) arr.push(i);
25131
- else tagMap.set(t, [i]);
25132
- }
25133
- }
25134
- return { wordMap, tagMap, entries: indexed, mtimeMs: 0 };
25135
- }
25136
- var MIN_SUBSTRING_NEEDLE_LEN = 3;
25137
- function searchIndex(index, query, limit) {
25138
- const needles = query.toLowerCase().split(/\s+/).filter((n) => n.length > 0);
25139
- if (needles.length === 0) return [];
25140
- const scores = /* @__PURE__ */ new Map();
25141
- for (const n of needles) {
25142
- let matched = false;
25143
- const wordExact = index.wordMap.get(n);
25144
- if (wordExact) {
25145
- for (const idx of wordExact) scores.set(idx, (scores.get(idx) ?? 0) + 1);
25146
- matched = true;
25147
- }
25148
- const tagExact = index.tagMap.get(n);
25149
- if (tagExact) {
25150
- for (const idx of tagExact) scores.set(idx, (scores.get(idx) ?? 0) + 2);
25151
- matched = true;
25152
- }
25153
- if (matched || n.length < MIN_SUBSTRING_NEEDLE_LEN) continue;
25154
- for (const [word, indices] of index.wordMap) {
25155
- if (word.includes(n) || n.includes(word)) {
25156
- for (const idx of indices) {
25157
- scores.set(idx, (scores.get(idx) ?? 0) + 1);
25158
- }
25159
- }
25160
- }
25161
- for (const [tag, indices] of index.tagMap) {
25162
- if (tag.includes(n) || n.includes(tag)) {
25163
- for (const idx of indices) {
25164
- scores.set(idx, (scores.get(idx) ?? 0) + 2);
25165
- }
25166
- }
25167
- }
25168
- }
25169
- if (scores.size === 0) return [];
25170
- const scored = Array.from(scores.entries());
25171
- scored.sort((a, b) => b[1] - a[1]);
25172
- const result = [];
25173
- const max = limit ? Math.min(limit, scored.length) : scored.length;
25174
- for (let i = 0; i < max; i++) {
25175
- result.push(index.entries[scored[i][0]].entry);
25176
- }
25177
- return result;
25178
- }
25179
- var FileMemoryBackend = class {
25180
- kind = "file";
25181
- files;
25182
- /** Cache of parsed entries per file path. */
25183
- entryCache = /* @__PURE__ */ new Map();
25184
- /** Inverted index per file path. */
25185
- indexCache = /* @__PURE__ */ new Map();
25186
- /** File mtime cache for invalidation. */
25187
- mtimeCache = /* @__PURE__ */ new Map();
25188
- constructor(opts) {
25189
- this.files = {
25190
- "project-agents": opts.paths.inProjectAgentsFile,
25191
- "project-memory": opts.paths.projectMemory,
25192
- "user-memory": opts.paths.globalMemory
25193
- };
25194
- }
25195
- resolveFile(filePath, scope) {
25196
- return filePath || this.files[scope];
25197
- }
25198
- async getMtime(file) {
25199
- try {
25200
- const stat13 = await fs13.stat(file);
25201
- return stat13.mtimeMs;
25202
- } catch {
25203
- return 0;
25204
- }
25205
- }
25206
- invalidateCache(file) {
25207
- this.entryCache.delete(file);
25208
- this.indexCache.delete(file);
25209
- this.mtimeCache.delete(file);
25210
- }
25211
- /**
25212
- * Load (and cache) the parsed entries for a file. Callers that have already
25213
- * stat'd the file this tick (e.g. `getIndex`) can pass the known `mtime` to
25214
- * avoid a redundant `fs.stat` — otherwise it's fetched here.
25215
- */
25216
- async loadEntries(file, scope, mtime) {
25217
- const resolvedMtime = mtime ?? await this.getMtime(file);
25218
- const cachedMtime = this.mtimeCache.get(file);
25219
- if (cachedMtime === resolvedMtime && this.entryCache.has(file)) {
25220
- return this.entryCache.get(file);
25221
- }
25222
- const raw = await this.readAll(scope, file);
25223
- if (!raw.trim()) {
25224
- this.entryCache.set(file, []);
25225
- this.mtimeCache.set(file, resolvedMtime);
25226
- return [];
25227
- }
25228
- const entries = parseEntries(raw, scope);
25229
- this.entryCache.set(file, entries);
25230
- this.mtimeCache.set(file, resolvedMtime);
25231
- return entries;
25232
- }
25233
- async getIndex(file, scope) {
25234
- const mtime = await this.getMtime(file);
25235
- const cached = this.indexCache.get(file);
25236
- if (cached && cached.mtimeMs === mtime) {
25237
- return cached;
25238
- }
25239
- const entries = await this.loadEntries(file, scope, mtime);
25240
- const index = buildInvertedIndex(entries);
25241
- index.mtimeMs = mtime;
25242
- this.indexCache.set(file, index);
25243
- return index;
25244
- }
25245
- async remember(scope, entry, filePath) {
25246
- const file = this.resolveFile(filePath, scope);
25247
- await ensureDir(path26.dirname(file));
25248
- let existing = "";
25249
- try {
25250
- existing = await fs13.readFile(file, "utf8");
25251
- } catch {
25252
- }
25253
- const id = `mem_${Date.now()}_${randomUUID11().slice(0, 8)}`;
25254
- const meta = formatMetadata(entry);
25255
- const line = `
25256
- - [${entry.ts}] ${id}${meta} ${entry.text.replace(/\n/g, " ")}
25257
- `;
25258
- const next = existing.trim() ? existing.replace(/\n+$/, "") + line : `# Agent Memory
25259
- ${line}`;
25260
- await atomicWrite(file, next);
25261
- this.invalidateCache(file);
25262
- }
25263
- async forget(scope, query, filePath) {
25264
- const file = this.resolveFile(filePath, scope);
25265
- return withFileLock(file, async () => {
25266
- let existing;
25267
- try {
25268
- existing = await fs13.readFile(file, "utf8");
25269
- } catch {
25270
- return 0;
25271
- }
25272
- const needle = query.toLowerCase();
25273
- const idMatcher = /mem_\d+_\w+/;
25274
- let removed = 0;
25275
- const lines = existing.split("\n").filter((line) => {
25276
- const trimmed = line.trim();
25277
- if (!trimmed.startsWith("- ")) return true;
25278
- if (idMatcher.test(query)) {
25279
- const entryIdMatch = /mem_\d+_\w+/.exec(trimmed);
25280
- if (entryIdMatch && entryIdMatch[0] === query) {
25281
- removed++;
25282
- return false;
25283
- }
25284
- }
25285
- if (trimmed.toLowerCase().includes(needle)) {
25286
- removed++;
25287
- return false;
25288
- }
25289
- return true;
25290
- });
25291
- if (removed > 0) {
25292
- if (lines.length === 0 || lines.length === 1 && !lines[0]?.trim()) {
25293
- await atomicWrite(file, "");
25294
- } else {
25295
- await atomicWrite(file, lines.join("\n"));
25296
- }
25297
- }
25298
- this.invalidateCache(file);
25299
- return removed;
25300
- });
25301
- }
25302
- async readAll(scope, filePath) {
25303
- const file = this.resolveFile(filePath, scope);
25304
- try {
25305
- return await fs13.readFile(file, "utf8");
25306
- } catch {
25307
- return "";
25308
- }
25309
- }
25310
- async list(scope, filePath, limit) {
25311
- const file = this.resolveFile(filePath, scope);
25312
- const entries = await this.loadEntries(file, scope);
25313
- return limit ? entries.slice(0, limit) : entries;
25314
- }
25315
- async search(scope, query, filePath, limit) {
25316
- const file = this.resolveFile(filePath, scope);
25317
- const index = await this.getIndex(file, scope);
25318
- return searchIndex(index, query, limit);
25319
- }
25320
- async clear(scope, filePath) {
25321
- const file = this.resolveFile(filePath, scope);
25322
- await atomicWrite(file, "");
25323
- this.invalidateCache(file);
25324
- }
25325
- async consolidate(scope, filePath) {
25326
- const file = this.resolveFile(filePath, scope);
25327
- let existing;
25328
- try {
25329
- existing = await fs13.readFile(file, "utf8");
25330
- } catch {
25331
- return 0;
25332
- }
25333
- const seen = /* @__PURE__ */ new Set();
25334
- let removed = 0;
25335
- const lines = existing.split("\n").filter((line) => {
25336
- const trimmed = line.trim();
25337
- if (!trimmed.startsWith("- ")) return true;
25338
- const norm = trimmed.replace(/\[[^\]]+\]/g, "").replace(/\bmem_\d+_\w+\s*/, "").replace(/#[\w-]+/g, "").replace(/\s+/g, " ").trim().toLowerCase();
25339
- if (seen.has(norm)) {
25340
- removed++;
25341
- return false;
25342
- }
25343
- seen.add(norm);
25344
- return true;
25345
- });
25346
- const next = lines.join("\n");
25347
- const backup = `${file}.bak.${Date.now()}`;
25348
- try {
25349
- await fs13.copyFile(file, backup);
25350
- await pruneConsolidateBackups(file);
25351
- } catch {
25352
- }
25353
- try {
25354
- await atomicWrite(file, next);
25355
- } catch {
25356
- return 0;
25357
- }
25358
- this.invalidateCache(file);
25359
- return removed;
25360
- }
25361
- };
25362
- async function pruneConsolidateBackups(file) {
25363
- const dir = path26.dirname(file);
25364
- const base = path26.basename(file);
25365
- const prefix = `${base}.bak.`;
25366
- const backups = (await fs13.readdir(dir)).filter((name) => name.startsWith(prefix)).sort().reverse();
25367
- await Promise.all(
25368
- backups.slice(MAX_MEMORY_CONSOLIDATE_BACKUPS).map(async (name) => {
25369
- try {
25370
- await fs13.unlink(path26.join(dir, name));
25371
- } catch {
25372
- }
25373
- })
25374
- );
25375
- }
25376
- function parseEntries(raw, scope = "project-memory") {
25377
- const entries = [];
25378
- for (const line of raw.split("\n")) {
25379
- const entry = lineToEntry(line, scope);
25380
- if (entry) entries.push(entry);
25381
- }
25382
- return entries.reverse();
25383
- }
25384
-
25385
- // src/storage/memory-store.ts
25386
- var MAX_BYTES_TOTAL = 32e3;
25387
- var DefaultMemoryStore = class {
25388
- files;
25389
- events;
25390
- traceId;
25391
- backend;
25392
- /**
25393
- * Per-scope serialization queue. `remember` / `forget` / `consolidate` /
25394
- * `clear` are read-modify-write against a single file; without a lock,
25395
- * two concurrent calls on the same scope can read the same baseline and
25396
- * the later write silently drops the earlier entry. We chain each
25397
- * mutation onto the prior promise for the same scope so they run in
25398
- * issue order. Different scopes still proceed in parallel.
25399
- *
25400
- * The chain tracks only the last pending write. If a write fails, its
25401
- * error is caught and swallowed so the chain stays alive for subsequent
25402
- * calls. The error is stored in `writeErrors` so callers can learn about
25403
- * it on the next read operation.
25404
- */
25405
- writeChain = /* @__PURE__ */ new Map();
25406
- /** Last write error per scope — surfaced as warnings on the next readAll(). */
25407
- writeErrors = /* @__PURE__ */ new Map();
25408
- /**
25409
- * When the global root is a temporary directory (opencode, CI sandboxes),
25410
- * memory files are also mirrored to the project tree so they survive
25411
- * session cleanup. The primary path stays in the temp root for isolation;
25412
- * this backup ensures memory is never lost.
25413
- */
25414
- persistBackup;
25415
- backupDir;
25416
- /**
25417
- * Per-scope tracked byte sizes — incremented on `remember()`, decremented on
25418
- * `forget()`, recalculated after `consolidate()`. Eliminates the redundant
25419
- * readAll() call that previously checked the file size after every write.
25420
- */
25421
- _trackedByteSizes = {};
25422
- /** Result cache for scoreRelevant() — keyed by scope + context hash, TTL 30s. */
25423
- _scoreCache = /* @__PURE__ */ new Map();
25424
- /**
25425
- * Per-entry cached lowercase strings — lazily allocated once and reused
25426
- * across scoreRelevant() calls so repeated scoring of the same entries skips
25427
- * re-lowercasing. Keyed by entry object identity; cleared (set to null) on
25428
- * every mutation (remember/forget/consolidate/clear) via invalidateScoreCaches().
25429
- */
25430
- _cachedLower = null;
25431
- constructor(opts) {
25432
- this.files = {
25433
- "project-agents": opts.paths.inProjectAgentsFile,
25434
- "project-memory": opts.paths.projectMemory,
25435
- "user-memory": opts.paths.globalMemory
25436
- };
25437
- this.events = opts.events;
25438
- this.backend = opts.backend ?? new FileMemoryBackend({ paths: opts.paths });
25439
- const root = opts.paths.globalRoot.toLowerCase();
25440
- this.persistBackup = /[/\\](tmp|temp|cache)[/\\]/.test(root);
25441
- this.backupDir = this.persistBackup ? opts.paths.inProjectAgentsFile.replace(/AGENTS\.md$/, "memory-persist") : "";
25442
- }
25443
- /** Expose the backend for plugin introspection and advanced queries. */
25444
- getBackend() {
25445
- return this.backend;
25446
- }
25447
- async runSerialized(scope, work) {
25448
- const prior = this.writeChain.get(scope) ?? Promise.resolve();
25449
- const next = prior.catch((err) => {
25450
- this.writeErrors.set(scope, err);
25451
- }).then(() => work());
25452
- this.writeChain.set(scope, next);
25453
- try {
25454
- return await next;
25455
- } catch (err) {
25456
- this.writeErrors.set(scope, err);
25457
- throw err;
25458
- } finally {
25459
- if (this.writeChain.get(scope) === next) {
25460
- this.writeChain.delete(scope);
25461
- }
25462
- }
25463
- }
25464
- /**
25465
- * Recalculate the tracked byte size for a scope by re-reading the file and
25466
- * summing the serialized byte length of each line. Called after consolidate()
25467
- * (which modifies the file) to keep the tracker accurate.
25468
- */
25469
- async _recalcTrackedByteSize(scope) {
25470
- const raw = await this.backend.readAll(scope, this.files[scope]);
25471
- let total = 0;
25472
- for (const line of raw.split("\n")) {
25473
- if (line.trim()) total += Buffer.byteLength(line, "utf8");
25474
- }
25475
- this._trackedByteSizes[scope] = total;
25476
- return total;
25477
- }
25478
- async readAll() {
25479
- const parts = [];
25480
- for (const scope of ["project-agents", "project-memory", "user-memory"]) {
25481
- const writeErr2 = this.writeErrors.get(scope);
25482
- if (writeErr2) {
25483
- parts.push(`> \u26A0\uFE0F Memory write error (${labelOf(scope)}): ${writeErr2.message}`);
25484
- }
25485
- const t0 = Date.now();
25486
- const filePath = this.files[scope];
25487
- try {
25488
- const body = await this.backend.readAll(scope, filePath);
25489
- const dur = Date.now() - t0;
25490
- this.events?.emit("storage.read", {
25491
- sessionId: "~memory~",
25492
- store: "memory",
25493
- filePath,
25494
- operation: "readAll",
25495
- outcome: "success",
25496
- durationMs: dur,
25497
- ...this.traceId !== void 0 && { traceId: this.traceId }
25498
- });
25499
- if (body.trim()) parts.push(`## ${labelOf(scope)}
25500
-
25501
- ${body.trim()}`);
25502
- } catch (err) {
25503
- const dur = Date.now() - t0;
25504
- this.events?.emit("storage.read", {
25505
- sessionId: "~memory~",
25506
- store: "memory",
25507
- filePath,
25508
- operation: "readAll",
25509
- outcome: "failure",
25510
- durationMs: dur,
25511
- error: toErrorMessage(err),
25512
- ...this.traceId !== void 0 && { traceId: this.traceId }
25513
- });
25514
- throw err;
25515
- }
25516
- }
25517
- return parts.join("\n\n");
25518
- }
25519
- async read(scope) {
25520
- const t0 = Date.now();
25521
- const filePath = this.files[scope];
25522
- try {
25523
- const body = await this.backend.readAll(scope, filePath);
25524
- const dur = Date.now() - t0;
25525
- this.events?.emit("storage.read", {
25526
- sessionId: "~memory~",
25527
- store: "memory",
25528
- filePath,
25529
- operation: "read",
25530
- outcome: "success",
25531
- durationMs: dur,
25532
- ...this.traceId !== void 0 && { traceId: this.traceId }
25533
- });
25534
- return body;
25535
- } catch (err) {
25536
- const dur = Date.now() - t0;
25537
- this.events?.emit("storage.read", {
25538
- sessionId: "~memory~",
25539
- store: "memory",
25540
- filePath,
25541
- operation: "read",
25542
- outcome: "failure",
25543
- durationMs: dur,
25544
- error: toErrorMessage(err),
25545
- ...this.traceId !== void 0 && { traceId: this.traceId }
25546
- });
25547
- throw err;
25548
- }
25549
- }
25550
- /**
25551
- * List entries from a scope, newest first. Delegates to the backend
25552
- * so graph/semantic backends can return enriched or filtered results.
25553
- */
25554
- async list(scope = "project-memory", limit) {
25555
- return this.backend.list(scope, this.files[scope], limit);
25556
- }
25557
- /**
25558
- * Find memories related to the given text via graph traversal.
25559
- * Falls back to content search when no graph backend is available.
25560
- */
25561
- async findRelated(text, scope = "project-memory", limit = 5) {
25562
- if (this.backend.findRelated) {
25563
- return this.backend.findRelated(scope, this.files[scope], text, limit);
25564
- }
25565
- return this.search(text, scope, limit);
25566
- }
25567
- async search(query, scope = "project-memory", limit) {
25568
- return this.backend.search(scope, query, this.files[scope], limit);
25569
- }
25570
- async remember(text, scope = "project-memory", metadata) {
25571
- const ts = (/* @__PURE__ */ new Date()).toISOString();
25572
- return this.runSerialized(scope, async () => {
25573
- const entry = { scope, text, ts, ...metadata };
25574
- const filePath = this.files[scope];
25575
- const t0 = Date.now();
25576
- try {
25577
- await this.backend.remember(scope, entry, filePath);
25578
- this.invalidateScoreCaches();
25579
- const dur = Date.now() - t0;
25580
- this.events?.emit("storage.write", {
25581
- sessionId: "~memory~",
25582
- store: "memory",
25583
- filePath,
25584
- operation: "remember",
25585
- outcome: "success",
25586
- durationMs: dur,
25587
- ...this.traceId !== void 0 && { traceId: this.traceId }
25588
- });
25589
- } catch (err) {
25590
- const dur = Date.now() - t0;
25591
- this.events?.emit("storage.write", {
25592
- sessionId: "~memory~",
25593
- store: "memory",
25594
- filePath,
25595
- operation: "remember",
25596
- outcome: "failure",
25597
- durationMs: dur,
25598
- error: toErrorMessage(err),
25599
- ...this.traceId !== void 0 && { traceId: this.traceId }
25600
- });
25601
- throw err;
25602
- }
25603
- let trackedSize = this._trackedByteSizes[scope];
25604
- if (trackedSize === void 0) {
25605
- trackedSize = await this._recalcTrackedByteSize(scope);
25606
- }
25607
- if (trackedSize > MAX_BYTES_TOTAL) {
25608
- const removed = await this.backend.consolidate(scope, this.files[scope]);
25609
- if (removed > 0) {
25610
- this.events?.emit("memory.consolidated", {
25611
- scope,
25612
- removed
25613
- });
25614
- await this._recalcTrackedByteSize(scope);
25615
- }
25616
- }
25617
- this._trackedByteSizes[scope] = (this._trackedByteSizes[scope] ?? 0) + Buffer.byteLength(JSON.stringify(entry), "utf8");
25618
- await this.mirrorBackup(scope);
25619
- this.events?.emit("memory.remembered", {
25620
- scope,
25621
- text,
25622
- ts,
25623
- type: entry.type,
25624
- tags: entry.tags,
25625
- priority: entry.priority
25626
- });
25627
- });
25628
- }
25629
- /**
25630
- * Drop the relevance caches after a mutation. Both the per-context score
25631
- * cache and the per-entry lowercase cache are keyed on entry objects that a
25632
- * mutation invalidates, so they must be cleared together when entries change.
25633
- */
25634
- invalidateScoreCaches() {
25635
- this._scoreCache.clear();
25636
- this._cachedLower = null;
25637
- }
25638
- /**
25639
- * Score and rank memories by relevance to the current context.
25640
- * Returns entries with score >= MIN_RELEVANCE_SCORE, sorted highest first.
25641
- */
25642
- async scoreRelevant(ctx, scope = "project-memory", limit = 8) {
25643
- const all = await this.list(scope);
25644
- if (all.length === 0) return [];
25645
- const ctxHash = `${scope}|${ctx.currentTask}|${(ctx.activeSkills ?? []).join(",")}|${(ctx.toolNames ?? []).join(",")}`;
25646
- const now = Date.now();
25647
- const TTL_MS = 3e4;
25648
- const cached = this._scoreCache.get(ctxHash);
25649
- if (cached && cached.expiresAt > now && cached.entries === all) {
25650
- return cached.scored.slice(0, Math.min(limit, 15));
25651
- }
25652
- const taskWords = ctx.currentTask.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
25653
- const skillWords = (ctx.activeSkills ?? []).flatMap((s) => s.split("-"));
25654
- const toolWords = (ctx.toolNames ?? []).flatMap((t) => t.toLowerCase().split("_"));
25655
- this._cachedLower ??= /* @__PURE__ */ new WeakMap();
25656
- const lowerCache = this._cachedLower;
25657
- const scored = [];
25658
- for (const entry of all) {
25659
- let score = 0;
25660
- const reasons = [];
25661
- let cachedLower = lowerCache.get(entry);
25662
- if (!cachedLower) {
25663
- cachedLower = {
25664
- textLower: entry.text.toLowerCase(),
25665
- tagsLower: (entry.tags ?? []).map((t) => t.toLowerCase())
25666
- };
25667
- lowerCache.set(entry, cachedLower);
25668
- }
25669
- const { textLower, tagsLower } = cachedLower;
25670
- let taskHits = 0;
25671
- for (const w of taskWords) {
25672
- if (textLower.includes(w)) {
25673
- taskHits++;
25674
- score += 2;
25675
- }
25676
- if (tagsLower.some((t) => t.includes(w))) {
25677
- taskHits++;
25678
- score += 3;
25679
- }
25680
- }
25681
- if (taskHits > 0) reasons.push(`task match (${taskHits})`);
25682
- let skillHits = 0;
25683
- for (const w of skillWords) {
25684
- if (w.length > 2 && (textLower.includes(w) || tagsLower.some((t) => t.includes(w)))) {
25685
- skillHits++;
25686
- }
25687
- }
25688
- score += skillHits;
25689
- if (skillHits > 0) reasons.push(`skill match (${skillHits})`);
25690
- for (const w of toolWords) {
25691
- if (w.length > 2 && (textLower.includes(w) || tagsLower.some((t) => t.includes(w)))) {
25692
- score += 1;
25693
- reasons.push(`tool mention: ${w}`);
25694
- }
25695
- }
25696
- switch (entry.priority) {
25697
- case "critical":
25698
- score += 5;
25699
- reasons.push("critical");
25700
- break;
25701
- case "high":
25702
- score += 3;
25703
- reasons.push("high priority");
25704
- break;
25705
- case "medium":
25706
- score += 1;
25707
- break;
25708
- case "low":
25709
- score -= 2;
25710
- reasons.push("low priority");
25711
- break;
25712
- }
25713
- switch (entry.type) {
25714
- case "decision":
25715
- score += 2;
25716
- reasons.push("decision");
25717
- break;
25718
- case "convention":
25719
- score += 2;
25720
- reasons.push("convention");
25721
- break;
25722
- case "anti_pattern":
25723
- score += 3;
25724
- reasons.push("anti-pattern");
25725
- break;
25726
- case "preference":
25727
- score += 1;
25728
- reasons.push("preference");
25729
- break;
25730
- case "reference":
25731
- break;
25732
- case "fact":
25733
- break;
25734
- }
25735
- const ageDays = (now - new Date(entry.ts).getTime()) / (1e3 * 60 * 60 * 24);
25736
- if (ageDays < 1) score += 1;
25737
- else if (ageDays > 30) score -= 1;
25738
- if (entry.confidence !== void 0 && entry.confidence < 0.5) {
25739
- score -= 2;
25740
- reasons.push("low confidence");
25741
- }
25742
- if (entry.lastAccessed) {
25743
- const hoursSinceAccess = (now - new Date(entry.lastAccessed).getTime()) / (1e3 * 60 * 60);
25744
- if (hoursSinceAccess < 1) score -= 1;
25745
- }
25746
- if (score > 0) {
25747
- scored.push({
25748
- ...entry,
25749
- score,
25750
- matchReason: reasons.join(", ") || "keyword match"
25751
- });
25752
- }
25753
- }
25754
- scored.sort((a, b) => b.score - a.score);
25755
- const threshold = 2;
25756
- const relevant = scored.filter(
25757
- (s) => s.score >= threshold || s.priority === "critical" || s.priority === "high"
25758
- );
25759
- this._scoreCache.set(ctxHash, { entries: all, scored: relevant, expiresAt: now + TTL_MS });
25760
- return relevant.slice(0, Math.min(limit, 15));
25761
- }
25762
- async forget(query, scope = "project-memory") {
25763
- return this.runSerialized(scope, async () => {
25764
- const filePath = this.files[scope];
25765
- const t0 = Date.now();
25766
- let removed = 0;
25767
- try {
25768
- removed = await this.backend.forget(scope, query, filePath);
25769
- this.invalidateScoreCaches();
25770
- const dur = Date.now() - t0;
25771
- this.events?.emit("storage.write", {
25772
- sessionId: "~memory~",
25773
- store: "memory",
25774
- filePath,
25775
- operation: "forget",
25776
- outcome: "success",
25777
- durationMs: dur,
25778
- ...this.traceId !== void 0 && { traceId: this.traceId }
25779
- });
25780
- } catch (err) {
25781
- const dur = Date.now() - t0;
25782
- this.events?.emit("storage.write", {
25783
- sessionId: "~memory~",
25784
- store: "memory",
25785
- filePath,
25786
- operation: "forget",
25787
- outcome: "failure",
25788
- durationMs: dur,
25789
- error: toErrorMessage(err),
25790
- ...this.traceId !== void 0 && { traceId: this.traceId }
25791
- });
25792
- throw err;
25793
- }
25794
- if (removed > 0) {
25795
- this.events?.emit("memory.forgotten", {
25796
- scope,
25797
- query,
25798
- removed
25799
- });
25800
- await this.mirrorBackup(scope);
25801
- await this._recalcTrackedByteSize(scope);
25802
- }
25803
- return removed;
25804
- });
25805
- }
25806
- async consolidate(scope) {
25807
- return this.runSerialized(scope, async () => {
25808
- const filePath = this.files[scope];
25809
- const t0 = Date.now();
25810
- let removed = 0;
25811
- try {
25812
- removed = await this.backend.consolidate(scope, filePath);
25813
- this.invalidateScoreCaches();
25814
- const dur = Date.now() - t0;
25815
- this.events?.emit("storage.write", {
25816
- sessionId: "~memory~",
25817
- store: "memory",
25818
- filePath,
25819
- operation: "consolidate",
25820
- outcome: "success",
25821
- durationMs: dur,
25822
- ...this.traceId !== void 0 && { traceId: this.traceId }
25823
- });
25824
- } catch (err) {
25825
- const dur = Date.now() - t0;
25826
- this.events?.emit("storage.write", {
25827
- sessionId: "~memory~",
25828
- store: "memory",
25829
- filePath,
25830
- operation: "consolidate",
25831
- outcome: "failure",
25832
- durationMs: dur,
25833
- error: toErrorMessage(err),
25834
- ...this.traceId !== void 0 && { traceId: this.traceId }
25835
- });
25836
- throw err;
25837
- }
25838
- if (removed > 0) {
25839
- this.events?.emit("memory.consolidated", {
25840
- scope,
25841
- removed
25842
- });
25843
- await this.mirrorBackup(scope);
25844
- await this._recalcTrackedByteSize(scope);
25845
- }
25846
- });
25847
- }
25848
- async clear(scope) {
25849
- if (scope) {
25850
- await this.runSerialized(scope, async () => {
25851
- const filePath = this.files[scope];
25852
- const t0 = Date.now();
25853
- try {
25854
- await this.backend.clear(scope, filePath);
25855
- this.invalidateScoreCaches();
25856
- const dur = Date.now() - t0;
25857
- this.events?.emit("storage.write", {
25858
- sessionId: "~memory~",
25859
- store: "memory",
25860
- filePath,
25861
- operation: "clear",
25862
- outcome: "success",
25863
- durationMs: dur,
25864
- ...this.traceId !== void 0 && { traceId: this.traceId }
25865
- });
25866
- } catch (err) {
25867
- const dur = Date.now() - t0;
25868
- this.events?.emit("storage.write", {
25869
- sessionId: "~memory~",
25870
- store: "memory",
25871
- filePath,
25872
- operation: "clear",
25873
- outcome: "failure",
25874
- durationMs: dur,
25875
- error: toErrorMessage(err),
25876
- ...this.traceId !== void 0 && { traceId: this.traceId }
25877
- });
25878
- throw err;
25879
- }
25880
- this.events?.emit("memory.cleared", { scope });
25881
- await this.mirrorBackup(scope);
25882
- this._trackedByteSizes[scope] = 0;
25883
- });
25884
- return;
25885
- }
25886
- await Promise.all(
25887
- ["project-agents", "project-memory", "user-memory"].map(
25888
- async (s) => this.runSerialized(s, async () => {
25889
- const filePath = this.files[s];
25890
- const t0 = Date.now();
25891
- try {
25892
- await this.backend.clear(s, filePath);
25893
- const dur = Date.now() - t0;
25894
- this.events?.emit("storage.write", {
25895
- sessionId: "~memory~",
25896
- store: "memory",
25897
- filePath,
25898
- operation: "clear",
25899
- outcome: "success",
25900
- durationMs: dur,
25901
- ...this.traceId !== void 0 && { traceId: this.traceId }
25902
- });
25903
- } catch (err) {
25904
- const dur = Date.now() - t0;
25905
- this.events?.emit("storage.write", {
25906
- sessionId: "~memory~",
25907
- store: "memory",
25908
- filePath,
25909
- operation: "clear",
25910
- outcome: "failure",
25911
- durationMs: dur,
25912
- error: toErrorMessage(err),
25913
- ...this.traceId !== void 0 && { traceId: this.traceId }
25914
- });
25915
- throw err;
25916
- }
25917
- this.events?.emit("memory.cleared", { scope: s });
25918
- await this.mirrorBackup(s);
25919
- })
25920
- )
25921
- );
25922
- this.invalidateScoreCaches();
25923
- }
25924
- /**
25925
- * Return a new MemoryStore proxy that carries `traceId` on every storage
25926
- * event. The original store is left unchanged — callers that need a
25927
- * trace-decorated view (e.g. session-run tools) receive the proxy while
25928
- * the singleton remains trace-free for boot-time use.
25929
- *
25930
- * The proxy implements the full `MemoryStore` interface; all other
25931
- * properties (backend, etc.) are delegated to the original store.
25932
- */
25933
- withTraceId(traceId) {
25934
- this.traceId = traceId;
25935
- return this;
25936
- }
25937
- async mirrorBackup(scope) {
25938
- if (!this.persistBackup || scope === "project-agents") return;
25939
- try {
25940
- const content = await this.backend.readAll(scope, this.files[scope]);
25941
- const { writeFile: writeFile7, mkdir: mkdir10 } = await import("node:fs/promises");
25942
- await mkdir10(this.backupDir, { recursive: true });
25943
- await writeFile7(`${this.backupDir}/${scope}.md`, content, "utf8");
25944
- } catch {
25945
- }
25946
- }
25947
- };
25948
- function labelOf(scope) {
25949
- switch (scope) {
25950
- case "project-agents":
25951
- return "Project AGENTS.md";
25952
- case "project-memory":
25953
- return "Project memory";
25954
- case "user-memory":
25955
- return "User memory";
25956
- }
25957
- }
25958
-
25959
- // src/storage/plan-store.ts
25960
- import * as fsp14 from "node:fs/promises";
25961
- import { randomUUID as randomUUID12 } from "node:crypto";
25962
25332
  async function loadPlan(filePath, events) {
25963
25333
  const t0 = Date.now();
25964
25334
  let raw;
@@ -26056,7 +25426,7 @@ function emptyPlan(sessionId, title) {
26056
25426
  function addPlanItem(plan, title, details) {
26057
25427
  const now = (/* @__PURE__ */ new Date()).toISOString();
26058
25428
  const item = {
26059
- id: `plan_${Date.now()}_${randomUUID12().slice(0, 6)}`,
25429
+ id: `plan_${Date.now()}_${randomUUID11().slice(0, 6)}`,
26060
25430
  title,
26061
25431
  details,
26062
25432
  status: "open",
@@ -26128,7 +25498,7 @@ function deriveTodosFromPlanItem(plan, idOrIndex, subtasks) {
26128
25498
  if (subtasks && subtasks.length > 0) {
26129
25499
  for (const st of subtasks) {
26130
25500
  todos.push({
26131
- id: `todo_${Date.now()}_${randomUUID12().slice(0, 6)}`,
25501
+ id: `todo_${Date.now()}_${randomUUID11().slice(0, 6)}`,
26132
25502
  content: st,
26133
25503
  status: "pending",
26134
25504
  promotedFromPlan: item.id
@@ -26256,7 +25626,7 @@ ${cat}:`);
26256
25626
 
26257
25627
  // src/storage/queue-store.ts
26258
25628
  import * as fsp15 from "node:fs/promises";
26259
- import * as path27 from "node:path";
25629
+ import * as path26 from "node:path";
26260
25630
  var QueueStore = class {
26261
25631
  file;
26262
25632
  // Use `| undefined` (not `?`) so exactOptionalPropertyTypes doesn't
@@ -26265,7 +25635,7 @@ var QueueStore = class {
26265
25635
  traceId;
26266
25636
  logger;
26267
25637
  constructor(opts) {
26268
- this.file = path27.join(opts.dir, "queue.json");
25638
+ this.file = path26.join(opts.dir, "queue.json");
26269
25639
  this.events = opts.events;
26270
25640
  this.traceId = opts.traceId;
26271
25641
  this.logger = opts.logger;
@@ -26423,7 +25793,7 @@ function isPersistedQueueItem(v) {
26423
25793
  // src/storage/recovery-lock.ts
26424
25794
  import * as fsp16 from "node:fs/promises";
26425
25795
  import * as os3 from "node:os";
26426
- import * as path28 from "node:path";
25796
+ import * as path27 from "node:path";
26427
25797
  var LOCK_FILE = "active.json";
26428
25798
  var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
26429
25799
  var RecoveryLock = class {
@@ -26434,7 +25804,7 @@ var RecoveryLock = class {
26434
25804
  sessionStore;
26435
25805
  probe;
26436
25806
  constructor(opts) {
26437
- this.file = path28.join(opts.dir, LOCK_FILE);
25807
+ this.file = path27.join(opts.dir, LOCK_FILE);
26438
25808
  this.pid = opts.pid ?? process.pid;
26439
25809
  this.hostname = opts.hostname ?? os3.hostname();
26440
25810
  this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
@@ -26495,7 +25865,7 @@ var RecoveryLock = class {
26495
25865
  * null return before calling this.
26496
25866
  */
26497
25867
  async write(sessionId) {
26498
- await ensureDir(path28.dirname(this.file));
25868
+ await ensureDir(path27.dirname(this.file));
26499
25869
  const lock = {
26500
25870
  v: 1,
26501
25871
  sessionId,
@@ -26800,8 +26170,8 @@ var DefaultSessionReader = class _DefaultSessionReader {
26800
26170
  const sessionPath = sessionScopedPath(rootDir, sessionId, ".jsonl");
26801
26171
  let mtimeMs = null;
26802
26172
  try {
26803
- const stat13 = await fs14.stat(sessionPath);
26804
- mtimeMs = stat13.mtimeMs;
26173
+ const stat12 = await fs14.stat(sessionPath);
26174
+ mtimeMs = stat12.mtimeMs;
26805
26175
  } catch {
26806
26176
  this.eventCache.delete(sessionId);
26807
26177
  this.eventCacheMtimes.delete(sessionId);
@@ -27319,7 +26689,6 @@ export {
27319
26689
  DefaultErrorHandler,
27320
26690
  DefaultHealthRegistry,
27321
26691
  DefaultLogger,
27322
- DefaultMemoryStore,
27323
26692
  DefaultModeStore,
27324
26693
  DefaultModelsRegistry,
27325
26694
  DefaultMultiAgentCoordinator,