@wrongstack/core 0.313.1 → 0.316.1

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 (290) hide show
  1. package/dist/chronicle/index.js +31 -31
  2. package/dist/chronicle/legacy-journal-import.d.ts +0 -6
  3. package/dist/chronicle/metrics-ingest.d.ts +14 -0
  4. package/dist/chronicle/metrics-schema.d.ts +10 -1
  5. package/dist/chronicle/metrics-store.d.ts +20 -2
  6. package/dist/chronicle/project-server.js +39 -31
  7. package/dist/chronicle/sink.d.ts +13 -0
  8. package/dist/chronicle/sqlite-journal.d.ts +20 -2
  9. package/dist/{chunk-WOEZV3Q4.js → chunk-25LSLC3G.js} +2 -1
  10. package/dist/{chunk-AI4LLQ2U.js → chunk-27QIURLN.js} +229 -12
  11. package/dist/{chunk-I45KAW2N.js → chunk-3AGWVSS7.js} +38 -450
  12. package/dist/chunk-3BDV7RL3.js +211 -0
  13. package/dist/{chunk-NFAYHPRH.js → chunk-3ECENVHL.js} +10 -8
  14. package/dist/{chunk-FW5LDDMJ.js → chunk-45WI72ZB.js} +1576 -1156
  15. package/dist/{chunk-AT4V4XGI.js → chunk-4BMVPOHO.js} +539 -313
  16. package/dist/{chunk-YBUJCZ57.js → chunk-4I7HGVVA.js} +17 -5
  17. package/dist/{chunk-JVGPR3TS.js → chunk-4LCZVJVF.js} +7 -55
  18. package/dist/{chunk-4ZHTKJHM.js → chunk-4SJNS7OB.js} +36 -2
  19. package/dist/{chunk-AD7ZFZCS.js → chunk-5BMIE7QG.js} +3 -3
  20. package/dist/{chunk-IQ35HMQL.js → chunk-5YPSXQXM.js} +71 -24
  21. package/dist/chunk-6FHWXLPT.js +10 -0
  22. package/dist/{chunk-FIV4IJL7.js → chunk-6H5GEOET.js} +163 -20
  23. package/dist/{chunk-VB3H5WWB.js → chunk-6NWEVFEL.js} +2 -2
  24. package/dist/{chunk-E6I5RXVK.js → chunk-AYW5YINX.js} +2 -2
  25. package/dist/{chunk-DEIQMBD2.js → chunk-B7F4WVP4.js} +2 -2
  26. package/dist/{chunk-XQ2XGIBF.js → chunk-BPBKKBUK.js} +6 -7
  27. package/dist/{chunk-K4YUNKU7.js → chunk-BYD345OX.js} +2 -2
  28. package/dist/{chunk-LZDDLNCZ.js → chunk-C27KDLOX.js} +4 -4
  29. package/dist/{chunk-6AKOTGLM.js → chunk-CU546ESW.js} +7 -7
  30. package/dist/{chunk-ZZ4V3WIZ.js → chunk-DLHHXUAM.js} +10 -1
  31. package/dist/{chunk-BAKVJLEV.js → chunk-DOTHQSJ2.js} +28 -16
  32. package/dist/{chunk-KBZGE6LN.js → chunk-DYLPMV4Q.js} +4 -3
  33. package/dist/{chunk-52RTVH4M.js → chunk-F3JWNST7.js} +2 -2
  34. package/dist/{chunk-WEBMSQP2.js → chunk-FC2ZGRF3.js} +21 -4
  35. package/dist/{chunk-FHAMQS7Q.js → chunk-FCBTO76Z.js} +76 -146
  36. package/dist/{chunk-4LJJYBAU.js → chunk-FHZVFYSL.js} +9 -1
  37. package/dist/{chunk-W4A6QA3D.js → chunk-FWNLEZYG.js} +4 -3
  38. package/dist/{chunk-OJZEX73F.js → chunk-GDQ7CZRQ.js} +5 -1
  39. package/dist/{chunk-QVOVMXJR.js → chunk-GJJGJB2R.js} +32 -9
  40. package/dist/{chunk-4XV6IR37.js → chunk-HI3JGZR2.js} +7 -6
  41. package/dist/{chunk-FSWCWSTS.js → chunk-HMLIGAU7.js} +26 -26
  42. package/dist/{chunk-PFXFQQRT.js → chunk-HXKZ7OBQ.js} +7 -43
  43. package/dist/{chunk-2FQL4PDC.js → chunk-I7DALG7J.js} +5 -5
  44. package/dist/{chunk-GM54GJBU.js → chunk-IRUTEHCE.js} +3 -3
  45. package/dist/{chunk-TYW2GI5I.js → chunk-J2WRWR37.js} +40 -13
  46. package/dist/{chunk-ZEHUCJHY.js → chunk-JFPJ6LLQ.js} +2 -2
  47. package/dist/chunk-JMULFFLN.js +9 -0
  48. package/dist/{chunk-RD6DEE36.js → chunk-JO4MGB6Z.js} +94 -8
  49. package/dist/{chunk-5DSMEVA4.js → chunk-K6BXGITA.js} +7 -7
  50. package/dist/{chunk-O74MJHXU.js → chunk-KZMSZV32.js} +2 -2
  51. package/dist/{chunk-GFJOMSHG.js → chunk-LA7CLDRW.js} +6 -2
  52. package/dist/{chunk-M2WF6AXG.js → chunk-LSMMOS2Z.js} +7 -3
  53. package/dist/{chunk-ASX6AW36.js → chunk-LXWCMQNJ.js} +3 -3
  54. package/dist/{chunk-WZO23AWO.js → chunk-M27LIEKV.js} +72 -22
  55. package/dist/{chunk-JTHKOYWU.js → chunk-MJPHJRAL.js} +15 -15
  56. package/dist/{chunk-G5AAKEEZ.js → chunk-MTL4O5YR.js} +5 -5
  57. package/dist/chunk-NHJ75HXY.js +1 -0
  58. package/dist/{chunk-ZYU2YOJ6.js → chunk-NIOAGFPB.js} +29 -15
  59. package/dist/{chunk-Y2GE5TSI.js → chunk-NS52ISWS.js} +2 -2
  60. package/dist/{chunk-UIWONWVG.js → chunk-NSOFPKGT.js} +541 -148
  61. package/dist/{chunk-RCQYT2I4.js → chunk-NVIZG63R.js} +2 -2
  62. package/dist/{chunk-EPC3GZD4.js → chunk-OOYZPOXM.js} +2 -2
  63. package/dist/{chunk-7ZHZE5EO.js → chunk-OSGLYW5J.js} +2 -2
  64. package/dist/{chunk-CE4ZOCOI.js → chunk-PFCMSSUF.js} +9 -9
  65. package/dist/{chunk-GFXZTHNI.js → chunk-PGRVXIMY.js} +5 -5
  66. package/dist/{chunk-JJPMWPB6.js → chunk-PISBFBJN.js} +6 -6
  67. package/dist/chunk-POOOIWE5.js +9 -0
  68. package/dist/{chunk-5RP3QTRL.js → chunk-PQNS3U7Z.js} +20 -3
  69. package/dist/{chunk-3E244UDA.js → chunk-QSTSJOCK.js} +52 -6
  70. package/dist/{chunk-YUPMXOGP.js → chunk-RFQMOKWK.js} +3 -3
  71. package/dist/chunk-RTOTNK3P.js +484 -0
  72. package/dist/{chunk-SN2DQRL6.js → chunk-RXKSDUAU.js} +7 -7
  73. package/dist/{chunk-T3KGZBU5.js → chunk-S45JPKQN.js} +189 -43
  74. package/dist/{chunk-YNCVSFGF.js → chunk-SAFHACFA.js} +4 -1
  75. package/dist/{chunk-KI4L2DUL.js → chunk-TCCHINEF.js} +114 -19
  76. package/dist/{chunk-AWNQ6ED6.js → chunk-THEGTRAG.js} +7 -2
  77. package/dist/{chunk-2KKODIQU.js → chunk-TR4LQE7L.js} +15 -15
  78. package/dist/{chunk-GPQ3NJPR.js → chunk-TWJMGXCI.js} +3 -3
  79. package/dist/{chunk-H72H4XDU.js → chunk-U45AXT5D.js} +9 -9
  80. package/dist/{chunk-62V2AFVG.js → chunk-U4FWBSO3.js} +160 -73
  81. package/dist/{chunk-A455ATZB.js → chunk-UBGDX6FI.js} +8 -1
  82. package/dist/chunk-UJILKYES.js +75 -0
  83. package/dist/{chunk-KSJ5UTP7.js → chunk-ULMAQV3T.js} +84 -4
  84. package/dist/{chunk-CB2HFKTP.js → chunk-VVBP6KGT.js} +1 -1
  85. package/dist/{chunk-RPOA7IAE.js → chunk-W5VITDM2.js} +43 -25
  86. package/dist/{chunk-QRTG4XTI.js → chunk-WAHQVNWT.js} +7 -1
  87. package/dist/{chunk-SQP64RCS.js → chunk-WGGYRBXH.js} +3019 -2976
  88. package/dist/{chunk-K3G6JBUC.js → chunk-WLYKU45P.js} +679 -141
  89. package/dist/chunk-WP7BVANJ.js +53 -0
  90. package/dist/{chunk-GAZEWQEZ.js → chunk-XF4VUMKW.js} +2 -2
  91. package/dist/{chunk-PAAWLLZX.js → chunk-Y5YXDM7D.js} +2 -2
  92. package/dist/{chunk-65RDIOR2.js → chunk-YOPAUVU4.js} +162 -44
  93. package/dist/{chunk-FFSX2QDQ.js → chunk-YPT4A46F.js} +3 -3
  94. package/dist/chunk-YWXT3JNM.js +425 -0
  95. package/dist/{chunk-EFEHDQGY.js → chunk-ZHZ2Y2DH.js} +64 -10
  96. package/dist/{chunk-CQQL4ZOQ.js → chunk-ZT7JNIGX.js} +4 -3
  97. package/dist/coordination/agent-monitor.d.ts +9 -1
  98. package/dist/coordination/agent-subagent-runner.d.ts +1 -1
  99. package/dist/coordination/agents/agent-prompts.d.ts +8 -0
  100. package/dist/coordination/agents/index.d.ts +33 -1
  101. package/dist/coordination/agents/index.js +52 -22
  102. package/dist/coordination/agents/phase3-wave1-platform.d.ts +1 -3
  103. package/dist/coordination/agents/phase3-wave2-meta.d.ts +1 -3
  104. package/dist/coordination/agents/phase8-wave3-products.d.ts +1 -3
  105. package/dist/coordination/agents/phase9-wave4-platform-meta.d.ts +1 -3
  106. package/dist/coordination/agents/project-agent-identity.d.ts +11 -11
  107. package/dist/coordination/agents/types.d.ts +13 -2
  108. package/dist/coordination/brain-monitor.d.ts +30 -7
  109. package/dist/coordination/collab-debug.d.ts +1 -1
  110. package/dist/coordination/collab-pause.d.ts +1 -1
  111. package/dist/coordination/delegate-tool.d.ts +7 -5
  112. package/dist/coordination/director-kanban-queue-helpers.d.ts +1 -0
  113. package/dist/coordination/director-options.d.ts +7 -0
  114. package/dist/coordination/director-spawn-model.d.ts +17 -0
  115. package/dist/coordination/director-tools.d.ts +2 -2
  116. package/dist/coordination/director.d.ts +12 -0
  117. package/dist/coordination/dispatcher.d.ts +8 -0
  118. package/dist/coordination/fleet-bus.d.ts +35 -4
  119. package/dist/coordination/fleet-supervisor.d.ts +10 -2
  120. package/dist/coordination/fleet.d.ts +16 -7
  121. package/dist/coordination/index.d.ts +4 -0
  122. package/dist/coordination/index.js +86 -49
  123. package/dist/coordination/kanban-dispatch-port.d.ts +2 -2
  124. package/dist/coordination/mailbox-http-router.d.ts +4 -3
  125. package/dist/coordination/mailbox-http-validation.d.ts +1 -1
  126. package/dist/coordination/mailbox-project-server.js +15 -15
  127. package/dist/coordination/mailbox-types.d.ts +24 -6
  128. package/dist/coordination/model-tier-leader.d.ts +122 -0
  129. package/dist/coordination/model-tier.d.ts +122 -0
  130. package/dist/coordination/multi-agent-coordinator.d.ts +24 -0
  131. package/dist/coordination/origin-session.d.ts +16 -0
  132. package/dist/coordination/provider-status-tracker.d.ts +37 -5
  133. package/dist/coordination/session-note-hub.d.ts +44 -0
  134. package/dist/coordination/session-note-tool.d.ts +10 -0
  135. package/dist/core/agent.d.ts +1 -1
  136. package/dist/core/context.d.ts +22 -3
  137. package/dist/core/index.d.ts +2 -0
  138. package/dist/core/index.js +130 -38
  139. package/dist/core/request-conversation-binding.d.ts +37 -0
  140. package/dist/core/session-notes.d.ts +25 -0
  141. package/dist/core/system-prompt-builder.d.ts +11 -2
  142. package/dist/core/system-prompt-variants.d.ts +78 -0
  143. package/dist/design/index.js +3 -3
  144. package/dist/execution/auto-compaction-middleware.d.ts +11 -0
  145. package/dist/execution/autonomy-brain.d.ts +2 -1
  146. package/dist/execution/autonomy-prompt-contributor.d.ts +7 -2
  147. package/dist/execution/compaction-core.d.ts +4 -4
  148. package/dist/execution/eternal-autonomy.d.ts +1 -1
  149. package/dist/execution/index.js +138 -129
  150. package/dist/execution/model-runtime.d.ts +0 -7
  151. package/dist/execution/parallel-eternal-engine.d.ts +7 -7
  152. package/dist/extension/index.js +3 -3
  153. package/dist/goal/index.js +6 -6
  154. package/dist/hooks/index.js +3 -3
  155. package/dist/hq/index.js +14 -12
  156. package/dist/hq/persistence/event-log.d.ts +0 -1
  157. package/dist/hq/persistence.d.ts +4 -4
  158. package/dist/hq/publisher.d.ts +2 -0
  159. package/dist/index.d.ts +14 -11
  160. package/dist/index.js +728 -635
  161. package/dist/infrastructure/index.d.ts +4 -4
  162. package/dist/infrastructure/index.js +281 -260
  163. package/dist/infrastructure/token-counter.d.ts +9 -0
  164. package/dist/kernel/events/agent-events.d.ts +77 -3
  165. package/dist/kernel/events/brain-events.d.ts +12 -0
  166. package/dist/kernel/events/provider-events.d.ts +7 -1
  167. package/dist/kernel/events/session-events.d.ts +14 -0
  168. package/dist/kernel/events/tool-events.d.ts +13 -0
  169. package/dist/kernel/index.js +4 -4
  170. package/dist/mailbox-attach.d.ts +1 -1
  171. package/dist/models/index.js +10 -9
  172. package/dist/observability/event-bridge.d.ts +25 -5
  173. package/dist/observability/index.d.ts +9 -8
  174. package/dist/observability/index.js +459 -351
  175. package/dist/observability/tool-usage-source.d.ts +65 -0
  176. package/dist/plugin/index.d.ts +5 -0
  177. package/dist/plugin/index.js +66 -50
  178. package/dist/plugins/auto-review-plugin.d.ts +3 -2
  179. package/dist/plugins/review-finding-commands.d.ts +0 -8
  180. package/dist/{review-finding-store-DKAV3AS6.js → plugins/review-finding-store.js} +7 -6
  181. package/dist/plugins/review-finding-types.js +19 -0
  182. package/dist/plugins/review-report-integration.js +21 -0
  183. package/dist/{review-report-store-OWY3EC3N.js → plugins/review-report-store.js} +7 -6
  184. package/dist/plugins/review-report-types.js +13 -0
  185. package/dist/prompts/index.js +5 -5
  186. package/dist/registry/index.js +3 -3
  187. package/dist/registry/tool-registry.d.ts +59 -9
  188. package/dist/{registry-R23UBE7H.js → registry-7N4O3E7W.js} +5 -5
  189. package/dist/security/capabilities.d.ts +6 -0
  190. package/dist/security/file-permissions.d.ts +1 -1
  191. package/dist/security/index.js +47 -33
  192. package/dist/security/permission-policy.d.ts +12 -1
  193. package/dist/security/secret-scrubber.d.ts +15 -0
  194. package/dist/session-catalog/index.d.ts +1 -0
  195. package/dist/session-catalog/index.js +19 -14
  196. package/dist/session-catalog/project-server.js +30 -13
  197. package/dist/session-catalog/protocol.d.ts +32 -1
  198. package/dist/session-catalog/registry.d.ts +17 -1
  199. package/dist/session-catalog/session-agents.d.ts +56 -0
  200. package/dist/session-catalog/store-rebuild.d.ts +14 -0
  201. package/dist/session-catalog/store-schema.d.ts +26 -0
  202. package/dist/session-catalog/store.d.ts +27 -1
  203. package/dist/session-note-attach.d.ts +9 -0
  204. package/dist/skills/index.js +5 -5
  205. package/dist/statusline/index.d.ts +43 -0
  206. package/dist/statusline/index.js +102 -0
  207. package/dist/storage/board-store-port.d.ts +2 -2
  208. package/dist/storage/config-loader/in-project-policy.d.ts +29 -0
  209. package/dist/storage/config-loader.d.ts +3 -2
  210. package/dist/storage/file-session-writer.d.ts +4 -2
  211. package/dist/storage/goal-coordination.d.ts +6 -1
  212. package/dist/storage/goal-kanban.d.ts +4 -1
  213. package/dist/storage/index.d.ts +12 -4
  214. package/dist/storage/index.js +76 -61
  215. package/dist/storage/input-history-store.d.ts +2 -4
  216. package/dist/storage/memory-curator.d.ts +133 -0
  217. package/dist/storage/session-agent-attribution.d.ts +39 -0
  218. package/dist/storage/session-doctor.d.ts +109 -0
  219. package/dist/storage/session-event-bridge.d.ts +5 -7
  220. package/dist/storage/session-outcome.d.ts +42 -0
  221. package/dist/storage/session-recovery.d.ts +55 -0
  222. package/dist/storage/session-store/load-session-data.d.ts +11 -0
  223. package/dist/storage/session-store/prune-helpers.d.ts +1 -1
  224. package/dist/storage/session-store/resume-session.d.ts +0 -2
  225. package/dist/storage/session-store/types.d.ts +8 -0
  226. package/dist/storage/session-store.d.ts +3 -3
  227. package/dist/storage/session-summary-tracker.d.ts +8 -1
  228. package/dist/storage/session-write-buffer.d.ts +18 -0
  229. package/dist/storage/session-writer-truncate.d.ts +2 -0
  230. package/dist/tasking/index.js +2 -2
  231. package/dist/tools/fallback-manage-tools.d.ts +6 -4
  232. package/dist/tools/index.js +21 -21
  233. package/dist/tools/model-tier-set-tool.d.ts +66 -0
  234. package/dist/types/agent-bridge.d.ts +2 -1
  235. package/dist/types/config/mcp-features.d.ts +7 -0
  236. package/dist/types/config/model-tiers.d.ts +148 -0
  237. package/dist/types/config/root.d.ts +9 -0
  238. package/dist/types/config/tools.d.ts +73 -0
  239. package/dist/types/config.d.ts +1 -0
  240. package/dist/types/context.d.ts +2 -0
  241. package/dist/types/default-config.d.ts +6 -0
  242. package/dist/types/errors.d.ts +2 -1
  243. package/dist/types/index.d.ts +11 -9
  244. package/dist/types/index.js +25 -15
  245. package/dist/types/multi-agent.d.ts +43 -0
  246. package/dist/types/plugin.d.ts +1 -1
  247. package/dist/types/provider.d.ts +15 -0
  248. package/dist/types/runtime-capability-manifest.d.ts +1 -1
  249. package/dist/types/secret-scrubber.d.ts +15 -0
  250. package/dist/types/session-markers.d.ts +37 -10
  251. package/dist/types/session-markers.js +1 -1
  252. package/dist/types/session-timeline.d.ts +197 -0
  253. package/dist/types/session-timeline.js +12 -0
  254. package/dist/types/session.d.ts +172 -11
  255. package/dist/types/system-prompt.d.ts +27 -0
  256. package/dist/utils/config-backup.d.ts +9 -1
  257. package/dist/utils/heap-watchdog.js +2 -2
  258. package/dist/utils/index.js +50 -48
  259. package/dist/utils/message-invariants.d.ts +4 -1
  260. package/dist/utils/regex-guard.d.ts +1 -1
  261. package/dist/utils/socket-path.d.ts +1 -1
  262. package/dist/utils/token-estimate.d.ts +6 -2
  263. package/dist/utils/wstack-paths.d.ts +2 -0
  264. package/dist/wiring/proxy-rewrite.d.ts +28 -0
  265. package/dist/wiring/proxy-rewrite.js +7 -1
  266. package/dist/worktree/index.js +2 -2
  267. package/instructions/agents/explore-companion.md +45 -18
  268. package/instructions/agents/memory-curator.md +34 -22
  269. package/instructions/coordination/director-preamble.md +12 -7
  270. package/instructions/coordination/subagent-baseline.md +26 -5
  271. package/instructions/leader-after-task.md +4 -1
  272. package/instructions/llm/chimera-review.md +30 -9
  273. package/instructions/llm/memory-curator.md +31 -0
  274. package/instructions/sections/tool/delegation-compact.md +2 -0
  275. package/instructions/sections/tool/delegation-full.md +5 -1
  276. package/instructions/sections/tool/mailbox-compact.md +5 -1
  277. package/instructions/sections/tool/mailbox-full.md +3 -0
  278. package/instructions/sections/tool/session-note-compact.md +3 -0
  279. package/instructions/sections/tool/session-note-full.md +15 -0
  280. package/instructions/system-lite.md +9 -4
  281. package/instructions/system-pro.md +66 -143
  282. package/instructions/system.md +73 -175
  283. package/package.json +32 -4
  284. package/dist/chunk-2BUUIMQB.js +0 -225
  285. package/dist/chunk-FAYRX2AD.js +0 -13
  286. package/dist/chunk-H5E77KUJ.js +0 -15
  287. package/dist/chunk-M7BS2K6U.js +0 -1
  288. package/dist/coordination/agents.d.ts +0 -8
  289. package/dist/coordination/transport.d.ts +0 -2
  290. package/dist/storage/session-writer/session-writer-summary-tracker.d.ts +0 -28
@@ -1,11 +1,15 @@
1
1
  import {
2
2
  DefaultSessionStore,
3
3
  DirectorStateCheckpoint
4
- } from "./chunk-UIWONWVG.js";
4
+ } from "./chunk-NSOFPKGT.js";
5
5
  import {
6
+ applyTierToSubagentConfig,
7
+ classifyTier,
8
+ listTierIds,
6
9
  resolveModelMatrixResolution,
10
+ resolveTier,
7
11
  roleNeedsIndependentReviewModel
8
- } from "./chunk-2BUUIMQB.js";
12
+ } from "./chunk-RTOTNK3P.js";
9
13
  import {
10
14
  DefaultMultiAgentCoordinator,
11
15
  FLEET_ROSTER_BUDGETS,
@@ -14,23 +18,34 @@ import {
14
18
  dispatchAgent,
15
19
  formatSubagentStructuredReport,
16
20
  nicknameKeyFromDisplay
17
- } from "./chunk-T3KGZBU5.js";
21
+ } from "./chunk-S45JPKQN.js";
18
22
  import {
19
23
  getAgentDefinition
20
- } from "./chunk-FHAMQS7Q.js";
24
+ } from "./chunk-FCBTO76Z.js";
25
+ import {
26
+ expandGlob
27
+ } from "./chunk-J2WRWR37.js";
28
+ import {
29
+ safeParse,
30
+ safeStringify
31
+ } from "./chunk-W5VITDM2.js";
32
+ import {
33
+ readBundledInstructionText
34
+ } from "./chunk-5S437PRC.js";
21
35
  import {
22
36
  applyMailboxSendPolicy,
23
37
  defaultResolveProjectDir,
24
38
  getSharedProjectMailbox,
39
+ postSessionNote,
25
40
  resolveMailboxIdentity
26
- } from "./chunk-RD6DEE36.js";
41
+ } from "./chunk-JO4MGB6Z.js";
27
42
  import {
28
43
  MAILBOX_MAX_ACK_BATCH,
29
44
  MAILBOX_MAX_QUERY_LIMIT,
30
45
  UNREAD_CHECK_MIN_INTERVAL_MS,
31
46
  resolveSendType,
32
47
  resolveSendTypeSafe
33
- } from "./chunk-GM54GJBU.js";
48
+ } from "./chunk-IRUTEHCE.js";
34
49
  import {
35
50
  MAILBOX_TYPE_PROPERTIES,
36
51
  hasMailboxCapability,
@@ -41,57 +56,51 @@ import {
41
56
  mailboxIdentityBase,
42
57
  normalizeRecipient,
43
58
  sessionRecipient
44
- } from "./chunk-W4A6QA3D.js";
59
+ } from "./chunk-FWNLEZYG.js";
45
60
  import {
46
61
  scrubErrorText
47
- } from "./chunk-PAAWLLZX.js";
48
- import {
49
- ToolCapabilities
50
- } from "./chunk-A455ATZB.js";
62
+ } from "./chunk-Y5YXDM7D.js";
51
63
  import {
52
64
  SECRET_FILE_MODE
53
- } from "./chunk-H5E77KUJ.js";
54
- import {
55
- QUOTA_EXHAUSTED_RE,
56
- ROUTE_SCOPED_QUOTA_RE,
57
- parseResetHintMs
58
- } from "./chunk-NFAYHPRH.js";
59
- import {
60
- expandGlob
61
- } from "./chunk-TYW2GI5I.js";
65
+ } from "./chunk-6FHWXLPT.js";
62
66
  import {
63
- safeParse,
64
- safeStringify
65
- } from "./chunk-RPOA7IAE.js";
66
- import {
67
- readBundledInstructionText
68
- } from "./chunk-5S437PRC.js";
67
+ ToolCapabilities
68
+ } from "./chunk-UBGDX6FI.js";
69
69
  import {
70
70
  renderInstructionLayer
71
- } from "./chunk-WZO23AWO.js";
71
+ } from "./chunk-M27LIEKV.js";
72
72
  import {
73
73
  buildChildEnv
74
- } from "./chunk-VB3H5WWB.js";
74
+ } from "./chunk-6NWEVFEL.js";
75
+ import {
76
+ resolveOwningSessionId
77
+ } from "./chunk-ZHZ2Y2DH.js";
75
78
  import {
76
79
  isPidAlive
77
80
  } from "./chunk-TRT2HRWZ.js";
78
81
  import {
79
82
  atomicWrite,
80
83
  withFileLock
81
- } from "./chunk-7ZHZE5EO.js";
84
+ } from "./chunk-OSGLYW5J.js";
82
85
  import {
83
86
  resolveWstackPaths
84
- } from "./chunk-YNCVSFGF.js";
87
+ } from "./chunk-SAFHACFA.js";
88
+ import {
89
+ MAX_RESET_HINT_MS,
90
+ QUOTA_EXHAUSTED_RE,
91
+ ROUTE_SCOPED_QUOTA_RE,
92
+ parseResetHintMs
93
+ } from "./chunk-3ECENVHL.js";
94
+ import {
95
+ expectDefined
96
+ } from "./chunk-PFZO3C6H.js";
85
97
  import {
86
98
  AgentError,
87
99
  ERROR_CODES
88
- } from "./chunk-WOEZV3Q4.js";
100
+ } from "./chunk-25LSLC3G.js";
89
101
  import {
90
102
  toErrorMessage
91
103
  } from "./chunk-TBRL2RCC.js";
92
- import {
93
- expectDefined
94
- } from "./chunk-PFZO3C6H.js";
95
104
 
96
105
  // src/coordination/agent-bridge.ts
97
106
  import { randomUUID } from "node:crypto";
@@ -302,6 +311,10 @@ function attachAutoExtend(events, policy = {}) {
302
311
  Math.ceil(limit * (1 + factor)),
303
312
  ceiling.timeoutMs ?? DEFAULT_CEILING.timeoutMs
304
313
  );
314
+ if (next2 <= limit) {
315
+ deny();
316
+ return;
317
+ }
305
318
  extend({ timeoutMs: next2 });
306
319
  } else {
307
320
  deny();
@@ -317,6 +330,10 @@ function attachAutoExtend(events, policy = {}) {
317
330
  const field = FIELD_BY_KIND[kind];
318
331
  const cap = ceiling[field] ?? DEFAULT_CEILING[field];
319
332
  const next = Math.min(Math.ceil(limit * (1 + factor)), cap);
333
+ if (next <= limit) {
334
+ deny();
335
+ return;
336
+ }
320
337
  extend({ [field]: next });
321
338
  })
322
339
  ];
@@ -341,20 +358,16 @@ function editedPath(input) {
341
358
  const candidate = r["file_path"] ?? r["path"] ?? r["filePath"] ?? r["file"];
342
359
  return typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
343
360
  }
361
+ var UNATTRIBUTED_SESSION = "";
362
+ var MAX_TRACKED_SESSIONS = 16;
344
363
  var BrainMonitor = class {
345
364
  constructor(opts) {
346
365
  this.opts = opts;
347
366
  this.applyTunables(opts);
348
367
  }
349
368
  opts;
350
- failStreaks = /* @__PURE__ */ new Map();
351
- errorTimestamps = [];
352
- editTimestamps = /* @__PURE__ */ new Map();
353
- lastEngagedAt = /* @__PURE__ */ new Map();
369
+ bySession = /* @__PURE__ */ new Map();
354
370
  unsubscribers = [];
355
- engaging = false;
356
- activeRuns = 0;
357
- lastProgressAt = 0;
358
371
  stallTimer;
359
372
  // Mutable, not readonly: `reconfigure()` re-applies these live. Every knob
360
373
  // on the Brain except these used to be live-editable, so `/brain monitor …`
@@ -385,6 +398,69 @@ var BrainMonitor = class {
385
398
  const id = this.opts.leaderSessionId;
386
399
  return typeof id === "function" ? id() : id;
387
400
  }
401
+ /**
402
+ * The signal state for one session, created on first sight.
403
+ *
404
+ * A host that pins `leaderSessionId` (the CLI, one session per process) only
405
+ * ever reaches one bucket. A host that does not (the WebUI, four tabs on one
406
+ * runtime) gets one bucket per tab, which is the point.
407
+ */
408
+ stateFor(sessionId) {
409
+ const key = sessionId || UNATTRIBUTED_SESSION;
410
+ let state = this.bySession.get(key);
411
+ if (!state) {
412
+ this.pruneIdleSessions();
413
+ state = {
414
+ failStreaks: /* @__PURE__ */ new Map(),
415
+ errorTimestamps: [],
416
+ editTimestamps: /* @__PURE__ */ new Map(),
417
+ lastEngagedAt: /* @__PURE__ */ new Map(),
418
+ engaging: false,
419
+ activeRuns: 0,
420
+ lastProgressAt: 0,
421
+ touchedAt: Date.now()
422
+ };
423
+ this.bySession.set(key, state);
424
+ }
425
+ state.touchedAt = Date.now();
426
+ return state;
427
+ }
428
+ /**
429
+ * Drop buckets for sessions that have been quiet longer than any signal
430
+ * window. Without this a long-lived host accumulates one bucket per session
431
+ * it has ever seen — cheap individually, unbounded in aggregate.
432
+ *
433
+ * Cooldowns are the one thing worth losing sleep over here: pruning a bucket
434
+ * resets its rate limit. That is acceptable only because the prune horizon is
435
+ * strictly longer than the cooldown, so a pruned session could have engaged
436
+ * again anyway.
437
+ */
438
+ pruneIdleSessions() {
439
+ if (this.bySession.size < MAX_TRACKED_SESSIONS) return;
440
+ const now = Date.now();
441
+ const horizon = Math.max(
442
+ this.fileChurnWindowMs,
443
+ this.errorStormWindowMs,
444
+ this.stallMs,
445
+ this.cooldownMs
446
+ );
447
+ for (const [key, state] of this.bySession) {
448
+ if (state.engaging) continue;
449
+ if (now - state.touchedAt > horizon) this.bySession.delete(key);
450
+ }
451
+ if (this.bySession.size >= MAX_TRACKED_SESSIONS) {
452
+ let oldestKey;
453
+ let oldestAt = Number.POSITIVE_INFINITY;
454
+ for (const [key, state] of this.bySession) {
455
+ if (state.engaging) continue;
456
+ if (state.touchedAt < oldestAt) {
457
+ oldestAt = state.touchedAt;
458
+ oldestKey = key;
459
+ }
460
+ }
461
+ if (oldestKey !== void 0) this.bySession.delete(oldestKey);
462
+ }
463
+ }
388
464
  /** Resolve the live-tunable subset onto the instance. Shared by the constructor and `reconfigure`. */
389
465
  applyTunables(next) {
390
466
  this.toolFailureStreak = next.toolFailureStreak ?? 3;
@@ -469,18 +545,19 @@ var BrainMonitor = class {
469
545
  this.opts.events.on("tool.executed", (e) => {
470
546
  const leaderSid = this.resolveLeaderSessionId();
471
547
  if (leaderSid && e.sessionId && e.sessionId !== leaderSid) return;
472
- this.lastProgressAt = Date.now();
473
- this.trackFileChurn(e.name, e.ok, e.input);
548
+ const state = this.stateFor(e.sessionId);
549
+ state.lastProgressAt = Date.now();
550
+ this.trackFileChurn(state, e.sessionId, e.name, e.ok, e.input);
474
551
  if (!this.signals.toolFailureStreak) return;
475
552
  if (e.ok) {
476
- this.failStreaks.delete(e.name);
553
+ state.failStreaks.delete(e.name);
477
554
  return;
478
555
  }
479
- const streak = (this.failStreaks.get(e.name) ?? 0) + 1;
480
- this.failStreaks.set(e.name, streak);
556
+ const streak = (state.failStreaks.get(e.name) ?? 0) + 1;
557
+ state.failStreaks.set(e.name, streak);
481
558
  if (streak >= this.toolFailureStreak) {
482
- this.failStreaks.delete(e.name);
483
- void this.engage("tool_failure_streak", {
559
+ state.failStreaks.delete(e.name);
560
+ void this.engage("tool_failure_streak", e.sessionId, {
484
561
  question: `The tool "${e.name}" has failed ${streak} times in a row. Should the agent be steered to a different approach?`,
485
562
  context: [
486
563
  `Tool: ${e.name}`,
@@ -496,8 +573,9 @@ var BrainMonitor = class {
496
573
  this.opts.events.on("agent.run.started", (e) => {
497
574
  const lsid = this.resolveLeaderSessionId();
498
575
  if (lsid && e.sessionId && e.sessionId !== lsid) return;
499
- this.activeRuns += 1;
500
- this.lastProgressAt = Date.now();
576
+ const state = this.stateFor(e.sessionId);
577
+ state.activeRuns += 1;
578
+ state.lastProgressAt = Date.now();
501
579
  }),
502
580
  // `agent.run.completed` is the only terminator. `Agent.run` emits it on
503
581
  // every exit path — the success path and, unconditionally, the catch
@@ -512,27 +590,30 @@ var BrainMonitor = class {
512
590
  this.opts.events.on("agent.run.completed", (e) => {
513
591
  const lsid = this.resolveLeaderSessionId();
514
592
  if (lsid && e.sessionId && e.sessionId !== lsid) return;
515
- this.activeRuns = Math.max(0, this.activeRuns - 1);
593
+ const state = this.stateFor(e.sessionId);
594
+ state.activeRuns = Math.max(0, state.activeRuns - 1);
516
595
  }),
517
596
  this.opts.events.on("iteration.started", (e) => {
518
597
  const lsid = this.resolveLeaderSessionId();
519
598
  if (lsid && e.sessionId && e.sessionId !== lsid) return;
520
- this.lastProgressAt = Date.now();
599
+ this.stateFor(e.sessionId).lastProgressAt = Date.now();
521
600
  })
522
601
  );
523
602
  this.stallTimer = setInterval(() => {
524
- if (this.activeRuns === 0 || this.lastProgressAt === 0) return;
525
- const idleMs = Date.now() - this.lastProgressAt;
526
- if (idleMs < this.stallMs) return;
527
- this.lastProgressAt = Date.now();
528
- void this.engage("agent_stall", {
529
- question: `An active run has made no observable progress (no tool call or iteration) for ${Math.round(idleMs / 6e4)} minute(s). Should the agent be steered before more time is wasted?`,
530
- context: [
531
- `Active runs: ${this.activeRuns}`,
532
- `Idle for: ${Math.round(idleMs / 1e3)}s`,
533
- `Stall threshold: ${Math.round(this.stallMs / 1e3)}s`
534
- ].join("\n")
535
- });
603
+ for (const [key, state] of this.bySession) {
604
+ if (state.activeRuns === 0 || state.lastProgressAt === 0) continue;
605
+ const idleMs = Date.now() - state.lastProgressAt;
606
+ if (idleMs < this.stallMs) continue;
607
+ state.lastProgressAt = Date.now();
608
+ void this.engage("agent_stall", key || void 0, {
609
+ question: `An active run has made no observable progress (no tool call or iteration) for ${Math.round(idleMs / 6e4)} minute(s). Should the agent be steered before more time is wasted?`,
610
+ context: [
611
+ `Active runs: ${state.activeRuns}`,
612
+ `Idle for: ${Math.round(idleMs / 1e3)}s`,
613
+ `Stall threshold: ${Math.round(this.stallMs / 1e3)}s`
614
+ ].join("\n")
615
+ });
616
+ }
536
617
  }, this.stallCheckIntervalMs);
537
618
  this.stallTimer.unref?.();
538
619
  }
@@ -542,15 +623,16 @@ var BrainMonitor = class {
542
623
  const lsid = this.resolveLeaderSessionId();
543
624
  if (lsid && e.sessionId && e.sessionId !== lsid) return;
544
625
  const now = Date.now();
545
- this.errorTimestamps.push(now);
546
- this.errorTimestamps = this.errorTimestamps.filter(
626
+ const state = this.stateFor(e.sessionId);
627
+ state.errorTimestamps.push(now);
628
+ state.errorTimestamps = state.errorTimestamps.filter(
547
629
  (t) => now - t <= this.errorStormWindowMs
548
630
  );
549
- if (this.errorTimestamps.length >= this.errorStormCount) {
550
- const count = this.errorTimestamps.length;
551
- this.errorTimestamps = [];
631
+ if (state.errorTimestamps.length >= this.errorStormCount) {
632
+ const count = state.errorTimestamps.length;
633
+ state.errorTimestamps = [];
552
634
  const message = e.err instanceof Error ? e.err.message : String(e.err);
553
- void this.engage("error_storm", {
635
+ void this.engage("error_storm", e.sessionId, {
554
636
  question: `${count} errors occurred within ${Math.round(this.errorStormWindowMs / 1e3)}s (phase: ${e.phase}). Should the agent be steered before more work is wasted?`,
555
637
  context: `Latest error: ${message.slice(0, 400)}`
556
638
  });
@@ -572,30 +654,32 @@ var BrainMonitor = class {
572
654
  this.running = false;
573
655
  for (const off of this.unsubscribers) off();
574
656
  this.unsubscribers.length = 0;
575
- this.failStreaks.clear();
576
- this.errorTimestamps = [];
577
- this.editTimestamps.clear();
657
+ for (const state of this.bySession.values()) {
658
+ state.failStreaks.clear();
659
+ state.errorTimestamps = [];
660
+ state.editTimestamps.clear();
661
+ state.activeRuns = 0;
662
+ state.lastProgressAt = 0;
663
+ }
578
664
  if (this.stallTimer) {
579
665
  clearInterval(this.stallTimer);
580
666
  this.stallTimer = void 0;
581
667
  }
582
- this.activeRuns = 0;
583
- this.lastProgressAt = 0;
584
668
  }
585
669
  /** Sliding-window count of successful edits per file → churn signal. */
586
- trackFileChurn(toolName, ok, input) {
670
+ trackFileChurn(state, sessionId, toolName, ok, input) {
587
671
  if (!this.signals.fileChurn) return;
588
672
  if (!ok || !this.fileEditTools.has(toolName.toLowerCase())) return;
589
673
  const path12 = editedPath(input);
590
674
  if (!path12) return;
591
675
  const now = Date.now();
592
- const stamps = (this.editTimestamps.get(path12) ?? []).filter(
676
+ const stamps = (state.editTimestamps.get(path12) ?? []).filter(
593
677
  (t) => now - t <= this.fileChurnWindowMs
594
678
  );
595
679
  stamps.push(now);
596
680
  if (stamps.length >= this.fileChurnThreshold) {
597
- this.editTimestamps.delete(path12);
598
- void this.engage("file_churn", {
681
+ state.editTimestamps.delete(path12);
682
+ void this.engage("file_churn", sessionId, {
599
683
  question: `The file "${path12}" has been edited ${stamps.length} times within ${Math.round(this.fileChurnWindowMs / 6e4)} minutes \u2014 the agent may be oscillating (edit/revert loop) instead of converging. Should it be steered?`,
600
684
  context: [
601
685
  `File: ${path12}`,
@@ -605,28 +689,32 @@ var BrainMonitor = class {
605
689
  });
606
690
  return;
607
691
  }
608
- if (this.editTimestamps.size >= 500 && !this.editTimestamps.has(path12)) {
609
- for (const [p, times] of this.editTimestamps) {
692
+ if (state.editTimestamps.size >= 500 && !state.editTimestamps.has(path12)) {
693
+ for (const [p, times] of state.editTimestamps) {
610
694
  if (times.every((t) => now - t > this.fileChurnWindowMs)) {
611
- this.editTimestamps.delete(p);
695
+ state.editTimestamps.delete(p);
612
696
  }
613
697
  }
614
- if (this.editTimestamps.size >= 500) {
615
- const oldest = this.editTimestamps.keys().next().value;
616
- if (oldest !== void 0) this.editTimestamps.delete(oldest);
698
+ if (state.editTimestamps.size >= 500) {
699
+ const oldest = state.editTimestamps.keys().next().value;
700
+ if (oldest !== void 0) state.editTimestamps.delete(oldest);
617
701
  }
618
702
  }
619
- this.editTimestamps.set(path12, stamps);
703
+ state.editTimestamps.set(path12, stamps);
620
704
  }
621
- async engage(kind, input) {
622
- const last = this.lastEngagedAt.get(kind) ?? 0;
623
- if (this.engaging || Date.now() - last < this.cooldownMs) return;
624
- this.engaging = true;
625
- this.lastEngagedAt.set(kind, Date.now());
705
+ async engage(kind, sessionId, input) {
706
+ const state = this.stateFor(sessionId);
707
+ const last = state.lastEngagedAt.get(kind) ?? 0;
708
+ if (state.engaging || Date.now() - last < this.cooldownMs) return;
709
+ state.engaging = true;
710
+ state.lastEngagedAt.set(kind, Date.now());
626
711
  try {
627
712
  const request = {
628
713
  id: `brainmon-${randomUUID2()}`,
629
- sessionId: this.opts.sessionId?.(),
714
+ // The session whose evidence triggered this — NOT the host's current
715
+ // one. They differ the moment two sessions run under one host, and
716
+ // taking the host's sent the steer to the wrong leader.
717
+ sessionId: sessionId ?? this.opts.sessionId?.(),
630
718
  source: "system",
631
719
  question: input.question,
632
720
  context: input.context,
@@ -684,7 +772,7 @@ var BrainMonitor = class {
684
772
  });
685
773
  } catch {
686
774
  } finally {
687
- this.engaging = false;
775
+ state.engaging = false;
688
776
  }
689
777
  }
690
778
  async maybeIntervene(kind, request, decision) {
@@ -695,6 +783,7 @@ var BrainMonitor = class {
695
783
  const guidance = decision.rationale?.trim() || decision.text.trim();
696
784
  try {
697
785
  await this.opts.intervene({
786
+ sessionId: request.sessionId,
698
787
  subject: `Brain intervention: ${kind.replace(/_/g, " ")}`,
699
788
  body: [
700
789
  `The Brain engaged after detecting: ${request.question}`,
@@ -1846,10 +1935,10 @@ async function compactLog(opts, keepCount = 2e3) {
1846
1935
  const archived = log.entries.slice(0, log.entries.length - keepCount);
1847
1936
  const kept = log.entries.slice(-keepCount);
1848
1937
  const archivePath = path.join(opts.storageDir, `file-authors-archive-${Date.now()}.json`);
1849
- await fs.writeFile(
1938
+ await atomicWrite(
1850
1939
  archivePath,
1851
- JSON.stringify({ projectRoot: opts.projectRoot, entries: archived }, null, 2) + "\n",
1852
- "utf-8"
1940
+ `${JSON.stringify({ projectRoot: opts.projectRoot, entries: archived }, null, 2)}
1941
+ `
1853
1942
  );
1854
1943
  log.entries = kept;
1855
1944
  log.lastCompactedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -2034,6 +2123,16 @@ import { randomUUID as randomUUID4 } from "node:crypto";
2034
2123
  import * as fsp2 from "node:fs/promises";
2035
2124
  import * as path3 from "node:path";
2036
2125
 
2126
+ // src/coordination/origin-session.ts
2127
+ function callerSessionId(ctx, fallback) {
2128
+ const c = ctx;
2129
+ const pinned = c?.activeRunSessionId;
2130
+ if (typeof pinned === "string" && pinned.length > 0) return pinned;
2131
+ const live = c?.session?.id;
2132
+ if (typeof live === "string" && live.length > 0) return live;
2133
+ return fallback;
2134
+ }
2135
+
2037
2136
  // src/coordination/task-boundary.ts
2038
2137
  var PLACEHOLDER_VALUES = /* @__PURE__ */ new Set([
2039
2138
  "n/a",
@@ -2112,6 +2211,14 @@ var taskBoundarySchemaProperties = {
2112
2211
  function createDelegateTool(opts) {
2113
2212
  const defaultTimeoutMs = opts.defaultTimeoutMs ?? 4 * 60 * 60 * 1e3;
2114
2213
  const rosterIds = opts.roster ? Object.keys(opts.roster) : [];
2214
+ const emitDelegateCompleted = (payload) => {
2215
+ opts.events?.emit("delegate.completed", payload);
2216
+ opts.events?.emit("subagent.done", {
2217
+ sessionId: payload.sessionId,
2218
+ summary: payload.summary,
2219
+ ok: payload.ok
2220
+ });
2221
+ };
2115
2222
  const inputSchema = {
2116
2223
  type: "object",
2117
2224
  properties: {
@@ -2136,6 +2243,10 @@ function createDelegateTool(opts) {
2136
2243
  type: "string",
2137
2244
  description: "Model id within the provider. Defaults to host model."
2138
2245
  },
2246
+ tier: {
2247
+ type: "string",
2248
+ description: "Cost/capability level for this worker: 'budget' (cheap + fast, for mechanical or well-specified work), 'standard' (the default), or 'premium' (expensive + most capable, for work where being wrong is costly). Resolved deterministically into a model, a failover chain, and a spend budget from `modelTiers` config, so you do NOT need to know any model id. Omit to let the routing table decide by role. An explicit `model` always wins over the tier.\n\nA CHEAPER TIER DOES NOT MAKE `delegate` CHEAPER FOR YOU. The leader stays fully blocked for the entire run either way \u2014 a cheap model is usually a SLOWER one, so `tier: 'budget'` on `delegate` can cost more of your wall-clock than it saves in dollars. Tier is about the worker's spend; `delegate` vs `spawn_subagent` is about your own time, and they are separate decisions. If the job is anything but short, pass the tier to `spawn_subagent` instead and keep working."
2249
+ },
2139
2250
  systemPromptOverride: {
2140
2251
  type: "string",
2141
2252
  description: "Extra prompt text appended to the role baseline."
@@ -2181,7 +2292,7 @@ function createDelegateTool(opts) {
2181
2292
  };
2182
2293
  return {
2183
2294
  name: "delegate",
2184
- description: "Hand a piece of work to a subagent and block until it returns. This call is synchronous: the leader's iteration pauses for the full duration of the subagent's run. (Multiple `delegate` calls fired in the same assistant turn still parallelize through the provider's parallel-tool-call surface, but each one eats wall-clock time \u2014 so for fan-out you actually control, reach for the async path below.) Use `delegate` when your next step genuinely needs the subagent's verdict \u2014 a review, a fact-check, a sign-off. Has own context, own LLM call, auto-extending budget, and a partial-completion handoff path (maxHandoffs, default 1). Workers cannot recursively spawn.\n\n**Do NOT use `delegate` for long-running work.** While `delegate` is in flight, the leader is fully blocked \u2014 it cannot act on other tools, read mail, or react to the user. If the work might run for tens of minutes or hours (multi-file refactor, monorepo audit, long-running build/test, sweeping migration), the blocking call wastes the leader's time. Use the async tool family instead: `spawn_subagent` to create each worker (returns a `subagentId` immediately), `assign_task` to queue work on it (returns a `taskId` immediately), then `await_tasks` to retrieve results later. The leader keeps doing other work while the worker churns, and a worker that realizes its task will run long can mail the leader (type `steer` or `ask` via `mail_send`) saying *\"my task is going to run long, please spawn a subagent instead\"* so the leader re-dispatches asynchronously instead of waiting.\n\n**Do NOT use `delegate` for fan-out you control.** Multiple sequential `delegate` calls each block the leader, wasting wall-clock time. For independent investigations you want to run in parallel \u2014 security scan + bug hunt + perf review on the same PR \u2014 use the async tool family: `spawn_subagent` to create each worker (returns a `subagentId` immediately), `assign_task` to queue work on it (returns a `taskId` immediately), then the `await_tasks` tool with `{mode: 'any'}` to fold the first useful result into the next decision while the rest keep churning. Reach for `delegate` only when the result gates your next move AND the work is short enough that blocking the leader is acceptable.",
2295
+ description: "Hand a piece of work to a subagent and block until it returns. This call is synchronous: the leader's iteration pauses for the full duration of the subagent's run. (Multiple `delegate` calls fired in the same assistant turn still parallelize through the provider's parallel-tool-call surface, but each one eats wall-clock time \u2014 so for fan-out you actually control, reach for the async path below.) Use `delegate` when your next step genuinely needs the subagent's verdict \u2014 a review, a fact-check, a sign-off. Has own context, own LLM call, auto-extending budget, and a partial-completion handoff path (maxHandoffs, default 1). Workers cannot recursively spawn.\n\n**Do NOT use `delegate` for long-running work.** While `delegate` is in flight, the leader is fully blocked \u2014 it cannot act on other tools, read mail, or react to the user. If the work might run for tens of minutes or hours (multi-file refactor, monorepo audit, long-running build/test, sweeping migration), the blocking call wastes the leader's time. Use the async tool family instead: `spawn_subagent` to create each worker (returns a `subagentId` immediately), `assign_task` to queue work on it (returns a `taskId` immediately), then `await_tasks` to retrieve results later. The leader keeps doing other work while the worker churns, and a worker that realizes its task will run long can tell the leader (type `steer` or `ask` via `session_note`, otherwise `mail_send`) saying *\"my task is going to run long, please spawn a subagent instead\"* so the leader re-dispatches asynchronously instead of waiting.\n\n**Do NOT use `delegate` for fan-out you control.** Multiple sequential `delegate` calls each block the leader, wasting wall-clock time. For independent investigations you want to run in parallel \u2014 security scan + bug hunt + perf review on the same PR \u2014 use the async tool family: `spawn_subagent` to create each worker (returns a `subagentId` immediately), `assign_task` to queue work on it (returns a `taskId` immediately), then the `await_tasks` tool with `{mode: 'any'}` to fold the first useful result into the next decision while the rest keep churning. Reach for `delegate` only when the result gates your next move AND the work is short enough that blocking the leader is acceptable.",
2185
2296
  usageHint: "Set `task` to the objective, then make the edges explicit: `scope` (what the work covers) and `outOfScope` (at least one concrete non-goal) are REQUIRED \u2014 the call is rejected without them, and the worker treats the rendered boundary block as a hard contract. Pick `role` from roster or pass `name` for free-form. Reach for `delegate` only when the result gates your next move AND the work is short enough that blocking the leader is acceptable (minutes, not hours). For long-running work or fan-out you control, use `spawn_subagent` + `assign_task` + `await_tasks` instead. Raise `maxHandoffs` (default 1, cap 8) for multi-day or multi-refactor tasks; pass larger `timeoutMs`/`maxIterations`/`maxToolCalls` only when needed.",
2186
2297
  permission: "auto",
2187
2298
  mutating: false,
@@ -2189,7 +2300,7 @@ function createDelegateTool(opts) {
2189
2300
  capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
2190
2301
  inputSchema,
2191
2302
  async execute(input, _ctx, execOpts) {
2192
- const sessionId = opts.directorRunId;
2303
+ const sessionId = callerSessionId(_ctx, opts.directorRunId) ?? opts.directorRunId;
2193
2304
  const abortSignal = execOpts?.signal;
2194
2305
  const i = input ?? {};
2195
2306
  if (typeof i.task !== "string" || !i.task.trim()) {
@@ -2214,7 +2325,7 @@ function createDelegateTool(opts) {
2214
2325
  const launchModePreface = [
2215
2326
  "Launch-mode guidance (delegate): you were launched via the synchronous `delegate` tool, so the leader is blocked on this call for the full duration of your run.",
2216
2327
  "If, after inspecting the task, you judge it will run for tens of minutes or hours (multi-file refactor, monorepo audit, long-running build/test, sweeping migration), do NOT silently grind through it under the blocking call.",
2217
- 'Escalate to the leader using whatever mail-style tool your role exposes \u2014 `mail_send` if available, otherwise `mailbox action=send` (some inspect-only roles expose `mailbox` rather than `mail_send`). Send a `steer` or `ask`, e.g. *"my task is going to run long, please spawn a subagent instead"*, so the leader can re-dispatch asynchronously via `spawn_subagent` + `assign_task`.',
2328
+ 'Escalate to the leader with `session_note to="leader"` when that tool is registered (same-session, next iteration). Fall back to `mail_send` or `mailbox action=send` only for cross-session mail. Send a `steer` or `ask`, e.g. *"my task is going to run long, please spawn a subagent instead"*, so the leader can re-dispatch asynchronously via `spawn_subagent` + `assign_task`.',
2218
2329
  'Then return a clean checkpoint with `completion:"partial"` and a concrete `remaining_work`.',
2219
2330
  "If the task is short and bounded, just do it end-to-end \u2014 do not over-trigger the escalation for normal work."
2220
2331
  ].join("\n\n");
@@ -2258,6 +2369,13 @@ function createDelegateTool(opts) {
2258
2369
  };
2259
2370
  cfg = applyRosterBudget({ ...cfg, name: i.name });
2260
2371
  }
2372
+ if (i.tier) cfg.tier = i.tier;
2373
+ const budgetPins = [];
2374
+ if (typeof i.maxIterations === "number") budgetPins.push("maxIterations");
2375
+ if (typeof i.maxToolCalls === "number") budgetPins.push("maxToolCalls");
2376
+ if (typeof i.maxTokens === "number") budgetPins.push("maxTokens");
2377
+ if (typeof i.maxCostUsd === "number") budgetPins.push("maxCostUsd");
2378
+ if (budgetPins.length) cfg.budgetPins = budgetPins;
2261
2379
  if (typeof i.maxIterations === "number") {
2262
2380
  cfg.maxIterations = i.maxIterations;
2263
2381
  }
@@ -2301,7 +2419,7 @@ function createDelegateTool(opts) {
2301
2419
  systemPromptOverride: segments.join("\n\n")
2302
2420
  };
2303
2421
  })();
2304
- const subagentId = await dir.spawn(attemptConfig);
2422
+ const subagentId = await dir.spawn({ ...attemptConfig, originSessionId: sessionId });
2305
2423
  if (handoffCount === 0) {
2306
2424
  opts.events?.emit("delegate.started", {
2307
2425
  sessionId,
@@ -2329,7 +2447,7 @@ function createDelegateTool(opts) {
2329
2447
  } catch {
2330
2448
  }
2331
2449
  const partial2 = await readSubagentPartial(opts, subagentId);
2332
- opts.events?.emit("delegate.completed", {
2450
+ emitDelegateCompleted({
2333
2451
  sessionId,
2334
2452
  target,
2335
2453
  task: i.task,
@@ -2357,7 +2475,7 @@ function createDelegateTool(opts) {
2357
2475
  } catch {
2358
2476
  }
2359
2477
  const partial2 = await readSubagentPartial(opts, subagentId);
2360
- opts.events?.emit("delegate.completed", {
2478
+ emitDelegateCompleted({
2361
2479
  sessionId,
2362
2480
  target,
2363
2481
  task: i.task,
@@ -2382,7 +2500,7 @@ function createDelegateTool(opts) {
2382
2500
  }
2383
2501
  if ("__emptyResult" in result) {
2384
2502
  const partial2 = await readSubagentPartial(opts, subagentId);
2385
- opts.events?.emit("delegate.completed", {
2503
+ emitDelegateCompleted({
2386
2504
  sessionId,
2387
2505
  target,
2388
2506
  task: i.task,
@@ -2436,7 +2554,7 @@ function createDelegateTool(opts) {
2436
2554
  } catch {
2437
2555
  costUsd = void 0;
2438
2556
  }
2439
- opts.events?.emit("delegate.completed", {
2557
+ emitDelegateCompleted({
2440
2558
  sessionId,
2441
2559
  target,
2442
2560
  task: i.task,
@@ -2474,7 +2592,7 @@ function createDelegateTool(opts) {
2474
2592
  }
2475
2593
  } catch (err) {
2476
2594
  const message = toErrorMessage(err);
2477
- opts.events?.emit("delegate.completed", {
2595
+ emitDelegateCompleted({
2478
2596
  sessionId,
2479
2597
  target,
2480
2598
  task: i.task,
@@ -2570,7 +2688,7 @@ function buildHandoffTask(originalTask, continuation, handoffCount, maxHandoffs)
2570
2688
  `Continue an oversized delegated task as fresh worker ${handoffCount} of ${maxHandoffs}.`,
2571
2689
  "Do not blindly repeat completed actions. Inspect the current workspace, git diff, tests, and any files named below before changing anything.",
2572
2690
  'If the remaining work is still too large, stop at a clean checkpoint and call submit_result with completion="partial" plus concrete remaining_work.',
2573
- "If parallel help would materially improve the outcome and mail_send is available, send the leader an `ask` that names the exact helper task; do not spawn agents yourself.",
2691
+ 'If parallel help would materially improve the outcome, `session_note to="leader" kind="ask"` (or `mail_send` if you must reach another session) naming the exact helper task; do not spawn agents yourself.',
2574
2692
  `Original task:
2575
2693
  ${originalTask}`,
2576
2694
  `Prior checkpoint summary:
@@ -2733,12 +2851,16 @@ var DEFAULT_EXPLORE_EDIT_TOOLS = [
2733
2851
  "str_replace"
2734
2852
  ];
2735
2853
  var DEFAULT_EXPLORE_SEARCH_TOOLS = [
2736
- "search",
2737
2854
  "grep",
2738
2855
  "codebase-search"
2739
2856
  ];
2740
2857
  function buildProbeTaskText(probe) {
2741
- const payload = { probe: probe.probe };
2858
+ const payload = {
2859
+ probe: probe.probe,
2860
+ // Repeated on every assign so a long-lived resident cannot treat a
2861
+ // later probe as permission to keep mapping the previous subject.
2862
+ scope: "Help the leader, then stop. Answer only this probe with codebase-* tools first. Do not map adjacent files, features, tests, or docs unless named in probe/hint. Do not reindex. Deliver via submit_result only."
2863
+ };
2742
2864
  if (probe.hint) payload.hint = probe.hint;
2743
2865
  if (probe.context) payload.context = probe.context;
2744
2866
  return JSON.stringify(payload, null, 2);
@@ -2903,7 +3025,7 @@ var ExploreCompanion = class {
2903
3025
  if (!this.readSet.has(path12)) {
2904
3026
  this.engage({
2905
3027
  id: randomUUID5(),
2906
- probe: `Map file ${path12}: role, exports, dependencies, and callers \u2014 the leader is about to edit it.`,
3028
+ probe: `Map file ${path12} for the leader: role, exports, incoming/outgoing calls, and blast radius if they edit it.`,
2907
3029
  hint: { file: path12 },
2908
3030
  context: `Leader edited ${path12} without reading it first.`,
2909
3031
  source: "edit_unread_file",
@@ -2918,7 +3040,7 @@ var ExploreCompanion = class {
2918
3040
  this.readSet.add(path12);
2919
3041
  this.engage({
2920
3042
  id: randomUUID5(),
2921
- probe: `Skeleton + callers + dependents of ${path12}: what it exports, who imports it, and how it fits the feature flow.`,
3043
+ probe: `Give the leader a skeleton of ${path12} plus callers and dependents \u2014 what it exports, who imports it.`,
2922
3044
  hint: { file: path12 },
2923
3045
  context: `Leader read unfamiliar file ${path12}.`,
2924
3046
  source: "unfamiliar_read",
@@ -2933,7 +3055,7 @@ var ExploreCompanion = class {
2933
3055
  const query = typeof input["query"] === "string" ? input["query"] : typeof input["pattern"] === "string" ? input["pattern"] : "";
2934
3056
  this.engage({
2935
3057
  id: randomUUID5(),
2936
- probe: query ? `Locate "${query}" \u2014 the leader's ${e.name} returned no hits. Try synonyms, a refreshed index, and lexical fallbacks.` : `The leader's ${e.name} returned no results. Find where the concept actually lives.`,
3058
+ probe: query ? `Locate "${query}" for the leader \u2014 ${e.name} returned no hits. Try codebase-search synonyms, then grep/glob. Do not reindex.` : `The leader's ${e.name} returned no results. Find where the concept actually lives via codebase-search, then grep/glob. Do not reindex.`,
2937
3059
  hint: query ? { symbol: query } : void 0,
2938
3060
  context: `${e.name} for "${query}" returned zero results.`,
2939
3061
  source: "search_zero_hits",
@@ -2954,7 +3076,7 @@ var ExploreCompanion = class {
2954
3076
  const first = mentions[0];
2955
3077
  this.engage({
2956
3078
  id: randomUUID5(),
2957
- probe: `Pre-map the files/symbols behind this in-progress todo: "${todo.content.slice(0, 160)}".`,
3079
+ probe: `Pre-map files/symbols for the leader's in-progress todo so they can start already oriented: "${todo.content.slice(0, 160)}".`,
2958
3080
  hint: first ? { [first.kind]: first.value } : void 0,
2959
3081
  context: `Todo "${todo.content.slice(0, 120)}" flipped to in_progress.`,
2960
3082
  source: "todo_in_progress",
@@ -2970,7 +3092,7 @@ var ExploreCompanion = class {
2970
3092
  for (const token of tokens.slice(0, 2)) {
2971
3093
  this.engage({
2972
3094
  id: randomUUID5(),
2973
- probe: `What is ${token.value}, where does it live, and who uses it? The leader hit an error naming it.`,
3095
+ probe: `What is ${token.value}, where does it live, and who uses it? Answer so the leader can recover from the error that named it.`,
2974
3096
  hint: { [token.kind]: token.value },
2975
3097
  context: `Error: ${err.message.slice(0, 300)}`,
2976
3098
  source: "error_symbol",
@@ -3339,11 +3461,7 @@ var DirectorBudgetPolicy = class {
3339
3461
  }
3340
3462
  const payload = event.payload;
3341
3463
  if (payload.kind === "timeout" || payload.kind === "idle_timeout") {
3342
- this.handleTimeoutThreshold(
3343
- event.subagentId,
3344
- event.taskId,
3345
- payload
3346
- );
3464
+ this.handleTimeoutThreshold(event.subagentId, event.taskId, payload);
3347
3465
  return;
3348
3466
  }
3349
3467
  const guardKey = `${event.subagentId}:${payload.kind}`;
@@ -3360,13 +3478,7 @@ var DirectorBudgetPolicy = class {
3360
3478
  return;
3361
3479
  }
3362
3480
  }
3363
- const grant = () => this.grantBoundedExtension(
3364
- event.subagentId,
3365
- event.taskId,
3366
- payload,
3367
- guardKey,
3368
- prior
3369
- );
3481
+ const grant = () => this.grantBoundedExtension(event.subagentId, event.taskId, payload, guardKey, prior);
3370
3482
  if (payload.kind !== "cost" || !this.deps.brain) {
3371
3483
  grant();
3372
3484
  return;
@@ -3490,12 +3602,18 @@ var DirectorBudgetPolicy = class {
3490
3602
  recordExtension(subagentId, taskId, kind, newLimit) {
3491
3603
  const total = (this.extendTotals.get(subagentId) ?? 0) + 1;
3492
3604
  this.extendTotals.set(subagentId, total);
3605
+ const sessionId = this.deps.currentSessionId?.();
3493
3606
  this.deps.fleet.emit({
3494
3607
  subagentId,
3495
3608
  taskId,
3496
3609
  ts: Date.now(),
3497
3610
  type: "budget.extended",
3498
- payload: { kind, newLimit, totalExtensions: total }
3611
+ payload: {
3612
+ ...sessionId ? { sessionId } : {},
3613
+ kind,
3614
+ newLimit,
3615
+ totalExtensions: total
3616
+ }
3499
3617
  });
3500
3618
  }
3501
3619
  isCollabAgent(subagentId) {
@@ -3909,21 +4027,6 @@ ${JSON.stringify(result.result, null, 2)}
3909
4027
  // src/coordination/director-tools.ts
3910
4028
  import { randomUUID as randomUUID11 } from "node:crypto";
3911
4029
 
3912
- // src/coordination/kanban-dispatch-port.ts
3913
- var notWired = () => {
3914
- throw new Error(
3915
- "Kanban dispatch port is not wired \u2014 register the implementation at the CLI composition root (see setKanbanDispatch)."
3916
- );
3917
- };
3918
- var port = void 0;
3919
- function setKanbanDispatch(impl) {
3920
- port = impl;
3921
- }
3922
- function kanbanDispatch() {
3923
- if (!port) notWired();
3924
- return port;
3925
- }
3926
-
3927
4030
  // src/coordination/director-input-helpers.ts
3928
4031
  import { randomUUID as randomUUID7 } from "node:crypto";
3929
4032
  function stringArray(value) {
@@ -3944,17 +4047,17 @@ function instantiateRosterConfig2(role, base) {
3944
4047
  }
3945
4048
 
3946
4049
  // src/coordination/kanban-ops-port.ts
3947
- var notWired2 = () => {
4050
+ var notWired = () => {
3948
4051
  throw new Error(
3949
4052
  "KanbanBoundaryOpsPort is not wired \u2014 register the implementation at the CLI composition root (see setKanbanBoundaryOps)."
3950
4053
  );
3951
4054
  };
3952
- var port2 = void 0;
4055
+ var port;
3953
4056
  function setKanbanBoundaryOps(impl) {
3954
- port2 = impl;
4057
+ port = impl;
3955
4058
  }
3956
4059
  function kanbanBoundaryOps() {
3957
- return port2 ?? notWired2();
4060
+ return port ?? notWired();
3958
4061
  }
3959
4062
 
3960
4063
  // src/coordination/director-kanban-queue-helpers.ts
@@ -3974,6 +4077,7 @@ function normalizeKanbanQueueInput(input) {
3974
4077
  role: typeof raw.role === "string" ? raw.role : void 0,
3975
4078
  provider: typeof raw.provider === "string" ? raw.provider : void 0,
3976
4079
  model: typeof raw.model === "string" ? raw.model : void 0,
4080
+ tier: typeof raw.tier === "string" ? raw.tier : void 0,
3977
4081
  fallbackModels: stringArray(raw.fallbackModels),
3978
4082
  tools: stringArray(raw.tools),
3979
4083
  allowedCapabilities: stringArray(raw.allowedCapabilities),
@@ -3998,7 +4102,10 @@ function buildKanbanSubagentConfig(task, input, roster, instantiateRosterConfig3
3998
4102
  ...tools ? { tools: ensureKanbanTool(tools) } : {},
3999
4103
  ...input.allowedCapabilities ?? assignment?.allowedCapabilities ? { allowedCapabilities: input.allowedCapabilities ?? assignment?.allowedCapabilities } : {},
4000
4104
  ...input.worktree !== void 0 ? { worktree: input.worktree } : {},
4001
- ...assignment?.costCeilingUsd !== void 0 ? { maxCostUsd: assignment.costCeilingUsd } : {}
4105
+ ...input.tier ?? assignment?.tier ? { tier: input.tier ?? assignment?.tier } : {},
4106
+ // A board's cost ceiling is an explicit decision by whoever queued the task,
4107
+ // so it is pinned: the tier layer may tighten a roster default, never this.
4108
+ ...assignment?.costCeilingUsd !== void 0 ? { maxCostUsd: assignment.costCeilingUsd, budgetPins: ["maxCostUsd"] } : {}
4002
4109
  };
4003
4110
  }
4004
4111
  function matchesKanbanQueueQuery(task, query) {
@@ -4125,6 +4232,21 @@ function ensureKanbanTool(tools) {
4125
4232
  return tools.includes("kanban") ? [...tools] : [...tools, "kanban"];
4126
4233
  }
4127
4234
 
4235
+ // src/coordination/kanban-dispatch-port.ts
4236
+ var notWired2 = () => {
4237
+ throw new Error(
4238
+ "Kanban dispatch port is not wired \u2014 register the implementation at the CLI composition root (see setKanbanDispatch)."
4239
+ );
4240
+ };
4241
+ var port2;
4242
+ function setKanbanDispatch(impl) {
4243
+ port2 = impl;
4244
+ }
4245
+ function kanbanDispatch() {
4246
+ if (!port2) notWired2();
4247
+ return port2;
4248
+ }
4249
+
4128
4250
  // src/coordination/director-basic-tools.ts
4129
4251
  import { randomUUID as randomUUID8 } from "node:crypto";
4130
4252
  function makeAssignTool(director) {
@@ -4584,998 +4706,1008 @@ function makeWorkCompleteTool(director) {
4584
4706
  };
4585
4707
  }
4586
4708
 
4587
- // src/coordination/director-quality-gate-tool.ts
4709
+ // src/coordination/director-mutation-test-tool.ts
4588
4710
  import { randomUUID as randomUUID9 } from "node:crypto";
4589
- function makeQualityGateTool(director, roster) {
4590
- return {
4591
- name: "quality_gate",
4592
- description: "Run a first-class implementation quality gate. It can await implementer task ids, spawn independent verifier/reviewer agents, summarize their verdicts, and optionally send must-fix feedback back to an implementer until the gate passes or the repair-attempt limit is reached.",
4593
- usageHint: "Use after code-changing work. Provide implementerTaskIds when available. Add repairSubagentId to iterate fixes automatically. Verdict only passes when every enabled reviewer/verifier explicitly passes.",
4594
- permission: "auto",
4595
- mutating: false,
4596
- capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
4597
- inputSchema: {
4598
- type: "object",
4599
- properties: {
4600
- task: {
4601
- type: "string",
4602
- description: "Original implementation task or acceptance goal being gated."
4603
- },
4604
- implementerTaskIds: {
4605
- type: "array",
4606
- items: { type: "string" },
4607
- description: "Optional completed or in-flight implementer task ids to await and include as implementation evidence."
4608
- },
4609
- repairSubagentId: {
4610
- type: "string",
4611
- description: "Optional implementer subagent id. When set and the gate fails, quality_gate assigns a repair task with reviewer/verifier feedback and reruns the gate."
4612
- },
4613
- maxRepairAttempts: {
4614
- type: "number",
4615
- minimum: 0,
4616
- maximum: 5,
4617
- description: "Maximum automatic repair iterations. Default: 2 when repairSubagentId is set, otherwise 0."
4618
- },
4619
- targets: {
4620
- type: "array",
4621
- items: { type: "string" },
4622
- description: "Files, packages, or paths that reviewer/verifier should focus on."
4623
- },
4624
- commands: {
4625
- type: "array",
4626
- items: { type: "string" },
4627
- description: 'Verification commands that should pass, e.g. ["pnpm --filter @wrongstack/core typecheck"].'
4628
- },
4629
- expected: {
4630
- type: "string",
4631
- description: "Expected behavior or acceptance criteria."
4632
- },
4633
- evidence: {
4634
- type: "string",
4635
- description: "Known implementation notes, diff summary, or commands already run."
4636
- },
4637
- reviewer: {
4638
- type: "boolean",
4639
- description: "Whether to run the reviewer lane. Default true."
4640
- },
4641
- verifier: {
4642
- type: "boolean",
4643
- description: "Whether to run the verifier lane. Default true."
4644
- },
4645
- timeoutMs: {
4646
- type: "number",
4647
- minimum: 1,
4648
- description: "Optional per reviewer/verifier/repair task timeout."
4649
- },
4650
- reviewerWorktree: {
4651
- anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
4652
- description: "Reviewer worktree override. Default off because reviewer is read-only."
4653
- },
4654
- verifierWorktree: {
4655
- anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
4656
- description: "Verifier worktree override. Default auto so test artifacts stay isolated when fleet policy wants it."
4711
+ import { readFileSync } from "node:fs";
4712
+ import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
4713
+
4714
+ // src/coordination/mutation-engine.ts
4715
+ var TOKEN_PATTERNS = [
4716
+ {
4717
+ kind: "relax-boundary",
4718
+ // `>` not followed by `=` and not part of `=>` or `>>`; require code-ish
4719
+ // context on both sides so generic text (JSX, strings) is not touched.
4720
+ regex: /(?<=[\w)\]}'"`])\s*\x20?(?<op>>(?!=|>))/g,
4721
+ replace: () => ">="
4722
+ },
4723
+ {
4724
+ kind: "tighten-boundary",
4725
+ regex: /(?<=[\w)\]}'"`])\s*\x20?(?<op>>=)/g,
4726
+ replace: () => ">"
4727
+ },
4728
+ {
4729
+ kind: "arith-plus-to-minus",
4730
+ // `+` between operands (binary), not `++`, unary `+x`, or `+=`.
4731
+ regex: /(?<=[\w)\]}'"`])\s*\x20?(?<op>\+(?!\+|=))/g,
4732
+ replace: () => "-"
4733
+ },
4734
+ {
4735
+ kind: "arith-minus-to-plus",
4736
+ // Binary `-` between operands, not `--`, `-=` or negative-number literal.
4737
+ regex: /(?<=[\w)\]}'"`])\s*\x20?(?<op>-(?!-|=))/g,
4738
+ replace: () => "+"
4739
+ },
4740
+ {
4741
+ kind: "negate-boolean",
4742
+ // Standalone boolean literals used as values, not property names.
4743
+ regex: /(?<![.\w$])(?<op>true|false)(?![\w$])/g,
4744
+ replace: (m) => m === "true" ? "false" : "true"
4745
+ },
4746
+ {
4747
+ kind: "return-null",
4748
+ // `return <expr>;` where expr is not already null/undefined/void.
4749
+ regex: /(?<indent>\breturn\b)(?<expr>\s+[^;{}\n]+?)\s*;/g,
4750
+ replace: () => "return null;",
4751
+ endpointsInCode: true
4752
+ }
4753
+ ];
4754
+ function planMutations(file, source, opts = {}) {
4755
+ const maxPerFile = opts.maxPerFile ?? 25;
4756
+ const out = [];
4757
+ const lines = source.split("\n");
4758
+ const masks = computeLineMasks(source);
4759
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
4760
+ const line = lines[lineIdx];
4761
+ const t = line.trim();
4762
+ if (t.startsWith("//")) continue;
4763
+ const codeRanges = masks[lineIdx];
4764
+ const inCode = (start) => codeRanges.some(([s, e]) => start >= s && start < e);
4765
+ for (const pattern of TOKEN_PATTERNS) {
4766
+ pattern.regex.lastIndex = 0;
4767
+ let m = pattern.regex.exec(line);
4768
+ while (m !== null) {
4769
+ const token = m.groups?.["op"] ?? m[0];
4770
+ const tokenStart = m.index + m[0].indexOf(token);
4771
+ if (!inCode(tokenStart)) {
4772
+ m = pattern.regex.exec(line);
4773
+ continue;
4657
4774
  }
4775
+ if (pattern.endpointsInCode && !inCode(tokenStart + token.length - 1)) {
4776
+ m = pattern.regex.exec(line);
4777
+ continue;
4778
+ }
4779
+ const original = line.slice(tokenStart, tokenStart + token.length);
4780
+ const replacement = pattern.replace(token);
4781
+ if (replacement === original) {
4782
+ m = pattern.regex.exec(line);
4783
+ continue;
4784
+ }
4785
+ out.push({
4786
+ id: `${pattern.kind}#${lineIdx + 1}#${tokenStart + 1}`,
4787
+ kind: pattern.kind,
4788
+ file,
4789
+ line: lineIdx + 1,
4790
+ column: tokenStart + 1,
4791
+ original,
4792
+ replacement
4793
+ });
4794
+ m = pattern.regex.exec(line);
4658
4795
  }
4659
- },
4660
- async execute(input) {
4661
- const i = normalizeQualityGateInput(input);
4662
- const runReviewer = i.reviewer !== false;
4663
- const runVerifier = i.verifier !== false;
4664
- if (!runReviewer && !runVerifier) {
4665
- return {
4666
- verdict: "inconclusive",
4667
- passed: false,
4668
- error: "quality_gate requires reviewer, verifier, or both."
4669
- };
4796
+ }
4797
+ if (out.length >= maxPerFile) break;
4798
+ }
4799
+ return out.slice(0, maxPerFile);
4800
+ }
4801
+ function computeLineMasks(source) {
4802
+ const lines = source.split("\n");
4803
+ const masks = lines.map(() => []);
4804
+ const stack = [{ kind: "code", depth: 0, parens: [] }];
4805
+ let inBlockComment = false;
4806
+ let lastToken = null;
4807
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
4808
+ const line = lines[lineIdx];
4809
+ const ranges = masks[lineIdx];
4810
+ let runStart = null;
4811
+ const closeRun = (end) => {
4812
+ if (runStart !== null && end > runStart) ranges.push([runStart, end]);
4813
+ runStart = null;
4814
+ };
4815
+ let i = 0;
4816
+ if (inBlockComment) {
4817
+ const close = line.indexOf("*/");
4818
+ if (close === -1) continue;
4819
+ inBlockComment = false;
4820
+ i = close + 2;
4821
+ }
4822
+ while (i < line.length) {
4823
+ const top = stack[stack.length - 1];
4824
+ const c = line[i];
4825
+ if (top.kind === "template") {
4826
+ if (c === "\\") {
4827
+ i += 2;
4828
+ continue;
4829
+ }
4830
+ if (c === "`") {
4831
+ stack.pop();
4832
+ lastToken = "`";
4833
+ i++;
4834
+ continue;
4835
+ }
4836
+ if (c === "$" && line[i + 1] === "{") {
4837
+ stack.push({ kind: "code", depth: 0, parens: [] });
4838
+ lastToken = "${";
4839
+ i += 2;
4840
+ continue;
4841
+ }
4842
+ i++;
4843
+ continue;
4670
4844
  }
4671
- const implementerResults = i.implementerTaskIds && i.implementerTaskIds.length > 0 ? await director.awaitTasks(i.implementerTaskIds) : [];
4672
- const maxRepairAttempts = clampRepairAttempts(
4673
- i.maxRepairAttempts ?? (i.repairSubagentId ? 2 : 0)
4674
- );
4675
- const repairResults = [];
4676
- const attempts = [];
4677
- for (let attempt = 1; ; attempt++) {
4678
- const gateTaskIds = [];
4679
- const taskRoleById = /* @__PURE__ */ new Map();
4680
- if (runVerifier) {
4681
- const subagentId = await director.spawn(
4682
- makeQualityGateSubagentConfig("verifier", roster, i.verifierWorktree ?? "auto")
4683
- );
4684
- const taskId = await director.assign({
4685
- id: randomUUID9(),
4686
- subagentId,
4687
- description: buildVerifierTask(i, {
4688
- attempt,
4689
- implementerResults,
4690
- repairResults,
4691
- priorAttempts: attempts
4692
- }),
4693
- timeoutMs: i.timeoutMs
4694
- });
4695
- gateTaskIds.push(taskId);
4696
- taskRoleById.set(taskId, "verifier");
4845
+ if (/[\w$]/.test(c)) {
4846
+ let j = i + 1;
4847
+ while (j < line.length && /[\w$]/.test(line[j])) j++;
4848
+ lastToken = line.slice(i, j);
4849
+ if (runStart === null) runStart = i;
4850
+ i = j;
4851
+ continue;
4852
+ }
4853
+ if (c === "'" || c === '"') {
4854
+ closeRun(i);
4855
+ i++;
4856
+ while (i < line.length && line[i] !== c) {
4857
+ if (line[i] === "\\") i++;
4858
+ i++;
4697
4859
  }
4698
- if (runReviewer) {
4699
- const subagentId = await director.spawn(
4700
- makeQualityGateSubagentConfig("reviewer", roster, i.reviewerWorktree ?? "off")
4701
- );
4702
- const taskId = await director.assign({
4703
- id: randomUUID9(),
4704
- subagentId,
4705
- description: buildReviewerTask(i, {
4706
- attempt,
4707
- implementerResults,
4708
- repairResults,
4709
- priorAttempts: attempts
4710
- }),
4711
- timeoutMs: i.timeoutMs
4712
- });
4713
- gateTaskIds.push(taskId);
4714
- taskRoleById.set(taskId, "reviewer");
4860
+ i++;
4861
+ lastToken = c;
4862
+ continue;
4863
+ }
4864
+ if (c === "`") {
4865
+ closeRun(i);
4866
+ stack.push({ kind: "template", depth: 0, parens: [] });
4867
+ i++;
4868
+ continue;
4869
+ }
4870
+ if (c === "/" && line[i + 1] === "/") {
4871
+ closeRun(i);
4872
+ break;
4873
+ }
4874
+ if (c === "/" && line[i + 1] === "*") {
4875
+ closeRun(i);
4876
+ const close = line.indexOf("*/", i + 2);
4877
+ if (close === -1) {
4878
+ inBlockComment = true;
4879
+ break;
4715
4880
  }
4716
- const gateResults = await director.awaitTasks(gateTaskIds);
4717
- const reports = gateResults.map((r) => assessRoleResult(taskRoleById.get(r.taskId), r));
4718
- const assessment = assessQualityGate(reports);
4719
- attempts.push({ attempt, reports, ...assessment });
4720
- if (assessment.passed || !i.repairSubagentId || attempt > maxRepairAttempts) {
4721
- return {
4722
- verdict: assessment.verdict,
4723
- passed: assessment.passed,
4724
- attempts,
4725
- repairAttemptsUsed: repairResults.length,
4726
- implementerResults: implementerResults.map(summarizeTaskResult),
4727
- nextAction: assessment.passed ? "accept" : i.repairSubagentId && attempt > maxRepairAttempts ? "manual_intervention_or_raise_repair_limit" : "inspect_failures"
4728
- };
4881
+ i = close + 2;
4882
+ continue;
4883
+ }
4884
+ if (c === "/") {
4885
+ if (!tokenCanEndOperand(lastToken)) {
4886
+ closeRun(i);
4887
+ const next = skipRegexLiteral(line, i);
4888
+ lastToken = next > i + 1 ? "regex" : "/";
4889
+ i = next;
4890
+ continue;
4729
4891
  }
4730
- const repairTaskId = await director.assign({
4731
- id: randomUUID9(),
4732
- subagentId: i.repairSubagentId,
4733
- description: buildRepairTask(i, attempts[attempts.length - 1], attempt),
4734
- timeoutMs: i.timeoutMs
4735
- });
4736
- const [repairResult] = await director.awaitTasks([repairTaskId]);
4737
- if (repairResult) repairResults.push(repairResult);
4738
- if (repairResult?.status !== "success") {
4739
- return {
4740
- verdict: "fail",
4741
- passed: false,
4742
- attempts,
4743
- repairAttemptsUsed: repairResults.length,
4744
- repairResult: repairResult ? summarizeTaskResult(repairResult) : void 0,
4745
- implementerResults: implementerResults.map(summarizeTaskResult),
4746
- nextAction: "repair_failed"
4747
- };
4892
+ }
4893
+ if (c === "(") {
4894
+ top.parens.push(CONTROL_KEYWORDS.has(lastToken ?? "") ? "control" : "expr");
4895
+ lastToken = c;
4896
+ } else if (c === ")") {
4897
+ const kind = top.parens.pop() ?? "expr";
4898
+ lastToken = kind === "control" ? "control-paren-close" : ")";
4899
+ } else if (c === "{") {
4900
+ top.depth++;
4901
+ lastToken = c;
4902
+ } else if (c === "}") {
4903
+ if (top.depth > 0) {
4904
+ top.depth--;
4905
+ lastToken = c;
4906
+ } else if (stack.length > 1) {
4907
+ closeRun(i);
4908
+ stack.pop();
4909
+ i++;
4910
+ continue;
4911
+ } else {
4912
+ lastToken = c;
4748
4913
  }
4914
+ } else if (c !== " " && c !== " " && c !== "\r") {
4915
+ lastToken = c;
4749
4916
  }
4917
+ if (runStart === null) runStart = i;
4918
+ i++;
4750
4919
  }
4751
- };
4752
- }
4753
- function normalizeQualityGateInput(input) {
4754
- const raw = input ?? {};
4755
- return {
4756
- task: typeof raw.task === "string" ? raw.task : void 0,
4757
- implementerTaskIds: stringArray(raw.implementerTaskIds),
4758
- repairSubagentId: typeof raw.repairSubagentId === "string" && raw.repairSubagentId.trim() ? raw.repairSubagentId.trim() : void 0,
4759
- maxRepairAttempts: typeof raw.maxRepairAttempts === "number" ? raw.maxRepairAttempts : void 0,
4760
- targets: stringArray(raw.targets),
4761
- commands: stringArray(raw.commands),
4762
- expected: typeof raw.expected === "string" ? raw.expected : void 0,
4763
- evidence: typeof raw.evidence === "string" ? raw.evidence : void 0,
4764
- reviewer: typeof raw.reviewer === "boolean" ? raw.reviewer : void 0,
4765
- verifier: typeof raw.verifier === "boolean" ? raw.verifier : void 0,
4766
- timeoutMs: typeof raw.timeoutMs === "number" ? raw.timeoutMs : void 0,
4767
- reviewerWorktree: normalizeWorktreeOverride(raw.reviewerWorktree),
4768
- verifierWorktree: normalizeWorktreeOverride(raw.verifierWorktree)
4769
- };
4770
- }
4771
- function clampRepairAttempts(value) {
4772
- if (!Number.isFinite(value)) return 0;
4773
- return Math.max(0, Math.min(5, Math.floor(value)));
4920
+ closeRun(line.length);
4921
+ }
4922
+ return masks;
4774
4923
  }
4775
- function makeQualityGateSubagentConfig(role, roster, worktree) {
4776
- const base = roster?.[role] ?? getAgentDefinition(role)?.config ?? { name: role, role };
4777
- return {
4778
- ...instantiateRosterConfig2(role, base),
4779
- worktree
4780
- };
4924
+ var KEYWORDS_BEFORE_REGEX = /* @__PURE__ */ new Set([
4925
+ "return",
4926
+ "typeof",
4927
+ "instanceof",
4928
+ "in",
4929
+ "of",
4930
+ "new",
4931
+ "delete",
4932
+ "void",
4933
+ "throw",
4934
+ "case",
4935
+ "do",
4936
+ "else",
4937
+ "yield",
4938
+ "await"
4939
+ ]);
4940
+ var CONTROL_KEYWORDS = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "catch", "with", "await"]);
4941
+ function tokenCanEndOperand(token) {
4942
+ if (token === null) return false;
4943
+ if (/^[\w$]+$/.test(token)) return !KEYWORDS_BEFORE_REGEX.has(token);
4944
+ return token === ")" || token === "]" || token === "." || token === '"' || token === "'" || token === "`";
4781
4945
  }
4782
- function buildVerifierTask(input, state) {
4783
- return [
4784
- "Run the independent verification gate for this implementation.",
4785
- "Return Markdown with `## Verdict` and make the first verdict word exactly `pass`, `fail`, or `blocked`.",
4786
- "Do not edit code. Run the smallest meaningful command set and include exact failures.",
4787
- "",
4788
- `Gate attempt: ${state.attempt}`,
4789
- input.task ? `Original task:
4790
- ${input.task}` : void 0,
4791
- input.targets?.length ? `Targets:
4792
- ${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
4793
- input.commands?.length ? `Required or suggested commands:
4794
- ${input.commands.map((c) => `- ${c}`).join("\n")}` : void 0,
4795
- input.expected ? `Expected behavior:
4796
- ${input.expected}` : void 0,
4797
- input.evidence ? `Known evidence:
4798
- ${input.evidence}` : void 0,
4799
- taskResultsBlock("Implementer results", state.implementerResults),
4800
- taskResultsBlock("Repair results so far", state.repairResults),
4801
- priorAttemptsBlock(state.priorAttempts)
4802
- ].filter((part) => !!part).join("\n\n");
4803
- }
4804
- function buildReviewerTask(input, state) {
4805
- return [
4806
- "Run independent code review for this implementation.",
4807
- "Return Markdown with `## Verdict` and make the first verdict phrase exactly `approve`, `request changes`, or `needs verification`.",
4808
- "Do not edit code. Treat missing proof, vague tests, and uncertainty as blocking until verifier evidence exists.",
4809
- "",
4810
- `Gate attempt: ${state.attempt}`,
4811
- input.task ? `Original task:
4812
- ${input.task}` : void 0,
4813
- input.targets?.length ? `Targets:
4814
- ${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
4815
- input.expected ? `Expected behavior:
4816
- ${input.expected}` : void 0,
4817
- input.evidence ? `Known evidence:
4818
- ${input.evidence}` : void 0,
4819
- taskResultsBlock("Implementer results", state.implementerResults),
4820
- taskResultsBlock("Repair results so far", state.repairResults),
4821
- priorAttemptsBlock(state.priorAttempts)
4822
- ].filter((part) => !!part).join("\n\n");
4823
- }
4824
- function buildRepairTask(input, attempt, attemptNumber) {
4825
- return [
4826
- `Repair the implementation after quality gate attempt ${attemptNumber} failed.`,
4827
- "Address every must-fix item. Run relevant checks before returning.",
4828
- "Do not claim done unless verifier/reviewer feedback is resolved.",
4829
- "",
4830
- input.task ? `Original task:
4831
- ${input.task}` : void 0,
4832
- input.targets?.length ? `Targets:
4833
- ${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
4834
- input.commands?.length ? `Commands expected to pass:
4835
- ${input.commands.map((c) => `- ${c}`).join("\n")}` : void 0,
4836
- input.expected ? `Expected behavior:
4837
- ${input.expected}` : void 0,
4838
- attempt.mustFix.length ? `Must fix:
4839
- ${attempt.mustFix.map((f) => `- ${f}`).join("\n")}` : void 0,
4840
- attempt.uncertaintyFlags.length ? `Uncertainty flags to resolve:
4841
- ${attempt.uncertaintyFlags.map((f) => `- ${f}`).join("\n")}` : void 0,
4842
- `Reviewer/verifier reports:
4843
- ${attempt.reports.map((r) => `### ${r.role} (${r.verdict})
4844
- ${r.summary}`).join("\n\n")}`
4845
- ].filter((part) => !!part).join("\n\n");
4846
- }
4847
- function taskResultsBlock(title, results) {
4848
- if (results.length === 0) return void 0;
4849
- return `${title}:
4850
- ${results.map((r) => `### ${r.subagentId}/${r.taskId}
4851
- ${summarizeTaskResult(r).summary}`).join("\n\n")}`;
4852
- }
4853
- function priorAttemptsBlock(attempts) {
4854
- if (attempts.length === 0) return void 0;
4855
- return `Prior quality gate attempts:
4856
- ${attempts.map(
4857
- (a) => `### Attempt ${a.attempt}
4858
- ${a.reports.map((r) => `- ${r.role}: ${r.verdict}${r.error ? ` (${r.error})` : ""}`).join("\n")}`
4859
- ).join("\n\n")}`;
4860
- }
4861
- function summarizeTaskResult(result) {
4862
- const text = typeof result.result === "string" ? result.result : result.result !== void 0 ? JSON.stringify(result.result, null, 2) : "";
4863
- const error = result.error ? `${result.error.kind}: ${result.error.message}` : void 0;
4864
- return {
4865
- taskId: result.taskId,
4866
- subagentId: result.subagentId,
4867
- status: result.status,
4868
- summary: excerpt(text || error || "(no output)", 4e3),
4869
- error
4870
- };
4871
- }
4872
- function assessRoleResult(role, result) {
4873
- const resolvedRole = role ?? (result.subagentId.includes("review") ? "reviewer" : "verifier");
4874
- const summary = summarizeTaskResult(result);
4875
- const text = summary.summary;
4876
- const uncertaintyFlags = extractSection(text, "Uncertainty Flags");
4877
- if (result.status !== "success") {
4878
- return {
4879
- role: resolvedRole,
4880
- subagentId: result.subagentId,
4881
- taskId: result.taskId,
4882
- status: result.status,
4883
- verdict: "fail",
4884
- summary: text,
4885
- uncertaintyFlags,
4886
- error: summary.error ?? result.status
4887
- };
4946
+ function skipRegexLiteral(line, start) {
4947
+ let i = start + 1;
4948
+ let inClass = false;
4949
+ while (i < line.length) {
4950
+ const ch = line[i];
4951
+ if (ch === "\\") {
4952
+ i += 2;
4953
+ continue;
4954
+ }
4955
+ if (inClass) {
4956
+ if (ch === "]") inClass = false;
4957
+ i++;
4958
+ continue;
4959
+ }
4960
+ if (ch === "[") {
4961
+ inClass = true;
4962
+ i++;
4963
+ continue;
4964
+ }
4965
+ if (ch === "/") {
4966
+ i++;
4967
+ break;
4968
+ }
4969
+ if (ch === "\n" || ch === "\r") return line.length;
4970
+ i++;
4888
4971
  }
4889
- return {
4890
- role: resolvedRole,
4891
- subagentId: result.subagentId,
4892
- taskId: result.taskId,
4893
- status: result.status,
4894
- verdict: parseQualityVerdict(resolvedRole, text),
4895
- summary: text,
4896
- uncertaintyFlags
4897
- };
4972
+ while (i < line.length && /[a-z]/.test(line[i])) i++;
4973
+ return i;
4898
4974
  }
4899
- function assessQualityGate(reports) {
4900
- const mustFix = [];
4901
- const uncertaintyFlags = [];
4902
- let hasFail = false;
4903
- let hasInconclusive = false;
4904
- for (const report of reports) {
4905
- if (report.verdict === "fail") hasFail = true;
4906
- if (report.verdict === "inconclusive") hasInconclusive = true;
4907
- const blocking = extractSection(report.summary, "Must Fix") || extractSection(report.summary, "Failures") || extractSection(report.summary, "Verification Gaps");
4908
- if (blocking) mustFix.push(`${report.role}: ${excerpt(blocking, 1e3)}`);
4909
- if (report.uncertaintyFlags) {
4910
- uncertaintyFlags.push(`${report.role}: ${excerpt(report.uncertaintyFlags, 1e3)}`);
4975
+ function parseMutationReport(text) {
4976
+ const candidates = [];
4977
+ const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/);
4978
+ if (fence?.[1]) candidates.push(fence[1].trim());
4979
+ const firstBrace = text.indexOf("{");
4980
+ if (firstBrace >= 0) candidates.push(extractBalancedObject(text, firstBrace));
4981
+ for (const candidate of candidates) {
4982
+ if (!candidate) continue;
4983
+ try {
4984
+ const parsed = JSON.parse(candidate);
4985
+ if (!Array.isArray(parsed.mutants)) continue;
4986
+ return {
4987
+ mutants: parsed.mutants.map(normalizeMutantEntry).filter((x) => Boolean(x)),
4988
+ summary: typeof parsed.summary === "string" ? parsed.summary : void 0
4989
+ };
4990
+ } catch {
4911
4991
  }
4912
- if (report.error) mustFix.push(`${report.role}: ${report.error}`);
4913
- }
4914
- if (hasFail) return { verdict: "fail", passed: false, mustFix, uncertaintyFlags };
4915
- if (hasInconclusive || reports.length === 0) {
4916
- return { verdict: "inconclusive", passed: false, mustFix, uncertaintyFlags };
4917
4992
  }
4918
- return { verdict: "pass", passed: true, mustFix, uncertaintyFlags };
4993
+ return void 0;
4919
4994
  }
4920
- function parseQualityVerdict(role, text) {
4921
- const normalized = text.toLowerCase();
4922
- const verdictBlock = normalized.match(/(?:^|\n)\s*(?:#+\s*)?verdict\b[^\n]*(?:\n|:|-)?([\s\S]{0,500})/)?.[0] ?? normalized.slice(0, 1e3);
4923
- if (role === "reviewer") {
4924
- if (/\b(request\s+changes|needs\s+verification|reject|rejected|fail|failed|blocked)\b/.test(
4925
- verdictBlock
4926
- )) {
4927
- return "fail";
4995
+ function extractBalancedObject(text, start) {
4996
+ let depth = 0;
4997
+ let inString = false;
4998
+ let escaped = false;
4999
+ for (let i = start; i < text.length; i++) {
5000
+ const c = text[i];
5001
+ if (escaped) {
5002
+ escaped = false;
5003
+ continue;
4928
5004
  }
4929
- if (/\b(approve|approved|pass|passed)\b/.test(verdictBlock)) return "pass";
4930
- if (sectionHasBlockingContent(text, "Must Fix") || sectionHasBlockingContent(text, "Verification Gaps")) {
4931
- return "fail";
5005
+ if (c === "\\") {
5006
+ escaped = true;
5007
+ continue;
5008
+ }
5009
+ if (c === '"') inString = !inString;
5010
+ if (inString) continue;
5011
+ if (c === "{") depth++;
5012
+ else if (c === "}") {
5013
+ depth--;
5014
+ if (depth === 0) return text.slice(start, i + 1);
4932
5015
  }
4933
- return "inconclusive";
4934
5016
  }
4935
- if (/\b(fail|failed|blocked|red)\b/.test(verdictBlock)) return "fail";
4936
- if (/\b(pass|passed|green|approve|approved)\b/.test(verdictBlock)) return "pass";
4937
- if (sectionHasBlockingContent(text, "Failures")) return "fail";
4938
- return "inconclusive";
4939
- }
4940
- function sectionHasBlockingContent(text, heading) {
4941
- const section = extractSection(text, heading);
4942
- if (!section) return false;
4943
- return !/^\s*(none|n\/a|no\b|no issues|empty|\(none\))\s*\.?\s*$/i.test(section.trim());
4944
- }
4945
- function extractSection(text, heading) {
4946
- const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4947
- const pattern = new RegExp(
4948
- `(?:^|\\n)\\s*#{1,6}\\s*${escaped}\\s*\\n([\\s\\S]*?)(?=\\n\\s*#{1,6}\\s+|$)`,
4949
- "i"
4950
- );
4951
- const match = text.match(pattern);
4952
- const body = match?.[1]?.trim();
4953
- return body ? body : void 0;
5017
+ return text.slice(start);
4954
5018
  }
4955
- function excerpt(text, max) {
4956
- if (text.length <= max) return text;
4957
- return `${text.slice(0, max - 20).trimEnd()}
4958
- ...(truncated)`;
5019
+ function normalizeMutantEntry(value) {
5020
+ if (typeof value !== "object" || value === null) return void 0;
5021
+ const rec = value;
5022
+ const id = typeof rec["id"] === "string" ? rec["id"] : void 0;
5023
+ const status = rec["status"];
5024
+ if (!id || status !== "killed" && status !== "survived" && status !== "skipped" && status !== "killed-by-hang") {
5025
+ return void 0;
5026
+ }
5027
+ return {
5028
+ id,
5029
+ file: typeof rec["file"] === "string" ? rec["file"] : "",
5030
+ line: typeof rec["line"] === "number" ? rec["line"] : 0,
5031
+ kind: typeof rec["kind"] === "string" ? rec["kind"] : "",
5032
+ status,
5033
+ evidence: typeof rec["evidence"] === "string" ? rec["evidence"] : void 0
5034
+ };
4959
5035
  }
4960
5036
 
4961
5037
  // src/coordination/director-mutation-test-tool.ts
4962
- import { randomUUID as randomUUID10 } from "node:crypto";
4963
- import { readFileSync } from "node:fs";
4964
- import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
4965
-
4966
- // src/coordination/mutation-engine.ts
4967
- var TOKEN_PATTERNS = [
4968
- {
4969
- kind: "relax-boundary",
4970
- // `>` not followed by `=` and not part of `=>` or `>>`; require code-ish
4971
- // context on both sides so generic text (JSX, strings) is not touched.
4972
- regex: /(?<=[\w\)\]\}'"`])\s*\x20?(?<op>>(?!=|>))/g,
4973
- replace: () => ">="
4974
- },
4975
- {
4976
- kind: "tighten-boundary",
4977
- regex: /(?<=[\w\)\]\}'"`])\s*\x20?(?<op>>=)/g,
4978
- replace: () => ">"
4979
- },
4980
- {
4981
- kind: "arith-plus-to-minus",
4982
- // `+` between operands (binary), not `++`, unary `+x`, or `+=`.
4983
- regex: /(?<=[\w\)\]\}'"`])\s*\x20?(?<op>\+(?!\+|=))/g,
4984
- replace: () => "-"
4985
- },
4986
- {
4987
- kind: "arith-minus-to-plus",
4988
- // Binary `-` between operands, not `--`, `-=` or negative-number literal.
4989
- regex: /(?<=[\w\)\]\}'"`])\s*\x20?(?<op>-(?!-|=))/g,
4990
- replace: () => "+"
4991
- },
4992
- {
4993
- kind: "negate-boolean",
4994
- // Standalone boolean literals used as values, not property names.
4995
- regex: /(?<![.\w$])(?<op>true|false)(?![\w$])/g,
4996
- replace: (m) => m === "true" ? "false" : "true"
4997
- },
4998
- {
4999
- kind: "return-null",
5000
- // `return <expr>;` where expr is not already null/undefined/void.
5001
- regex: /(?<indent>\breturn\b)(?<expr>\s+[^;{}\n]+?)\s*;/g,
5002
- replace: () => "return null;",
5003
- endpointsInCode: true
5004
- }
5005
- ];
5006
- function planMutations(file, source, opts = {}) {
5007
- const maxPerFile = opts.maxPerFile ?? 25;
5008
- const out = [];
5009
- const lines = source.split("\n");
5010
- const masks = computeLineMasks(source);
5011
- for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
5012
- const line = lines[lineIdx];
5013
- const t = line.trim();
5014
- if (t.startsWith("//")) continue;
5015
- const codeRanges = masks[lineIdx];
5016
- const inCode = (start) => codeRanges.some(([s, e]) => start >= s && start < e);
5017
- for (const pattern of TOKEN_PATTERNS) {
5018
- pattern.regex.lastIndex = 0;
5019
- let m;
5020
- while ((m = pattern.regex.exec(line)) !== null) {
5021
- const token = m.groups?.["op"] ?? m[0];
5022
- const tokenStart = m.index + m[0].indexOf(token);
5023
- if (!inCode(tokenStart)) continue;
5024
- if (pattern.endpointsInCode && !inCode(tokenStart + token.length - 1)) continue;
5025
- const original = line.slice(tokenStart, tokenStart + token.length);
5026
- const replacement = pattern.replace(token);
5027
- if (replacement === original) continue;
5028
- out.push({
5029
- id: `${pattern.kind}#${lineIdx + 1}#${tokenStart + 1}`,
5030
- kind: pattern.kind,
5031
- file,
5032
- line: lineIdx + 1,
5033
- column: tokenStart + 1,
5034
- original,
5035
- replacement
5036
- });
5037
- }
5038
- }
5039
- if (out.length >= maxPerFile) break;
5040
- }
5041
- return out.slice(0, maxPerFile);
5042
- }
5043
- function computeLineMasks(source) {
5044
- const lines = source.split("\n");
5045
- const masks = lines.map(() => []);
5046
- const stack = [{ kind: "code", depth: 0, parens: [] }];
5047
- let inBlockComment = false;
5048
- let lastToken = null;
5049
- for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
5050
- const line = lines[lineIdx];
5051
- const ranges = masks[lineIdx];
5052
- let runStart = null;
5053
- const closeRun = (end) => {
5054
- if (runStart !== null && end > runStart) ranges.push([runStart, end]);
5055
- runStart = null;
5056
- };
5057
- let i = 0;
5058
- if (inBlockComment) {
5059
- const close = line.indexOf("*/");
5060
- if (close === -1) continue;
5061
- inBlockComment = false;
5062
- i = close + 2;
5063
- }
5064
- while (i < line.length) {
5065
- const top = stack[stack.length - 1];
5066
- const c = line[i];
5067
- if (top.kind === "template") {
5068
- if (c === "\\") {
5069
- i += 2;
5070
- continue;
5071
- }
5072
- if (c === "`") {
5073
- stack.pop();
5074
- lastToken = "`";
5075
- i++;
5076
- continue;
5077
- }
5078
- if (c === "$" && line[i + 1] === "{") {
5079
- stack.push({ kind: "code", depth: 0, parens: [] });
5080
- lastToken = "${";
5081
- i += 2;
5082
- continue;
5083
- }
5084
- i++;
5085
- continue;
5086
- }
5087
- if (/[\w$]/.test(c)) {
5088
- let j = i + 1;
5089
- while (j < line.length && /[\w$]/.test(line[j])) j++;
5090
- lastToken = line.slice(i, j);
5091
- if (runStart === null) runStart = i;
5092
- i = j;
5093
- continue;
5094
- }
5095
- if (c === "'" || c === '"') {
5096
- closeRun(i);
5097
- i++;
5098
- while (i < line.length && line[i] !== c) {
5099
- if (line[i] === "\\") i++;
5100
- i++;
5038
+ var DEFAULT_MAX_PER_FILE = 10;
5039
+ var DEFAULT_MAX_STRENGTHEN_ATTEMPTS = 2;
5040
+ var CHAOS_ROLE = "chaos-monkey";
5041
+ function makeMutationTestTool(director, roster, opts = {}) {
5042
+ return {
5043
+ name: "mutation_test",
5044
+ description: "Chaos Monkey mutation testing: deterministically sabotage boundary conditions in the target code (> to >=, + to -, boolean flips, return null), re-run the tests per mutant, and report which mutants were killed. Surviving mutants mean the tests are weak \u2014 optionally loop a strengthen-tests repair until they die.",
5045
+ usageHint: "Use after writing new code AND its tests, before delivering. Pass targets (files) and testCommand. Provide repairSubagentId to auto-strengthen weak tests. Survivors that persist are reported as suspected-equivalent.",
5046
+ permission: "auto",
5047
+ mutating: false,
5048
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
5049
+ inputSchema: {
5050
+ type: "object",
5051
+ properties: {
5052
+ targets: {
5053
+ type: "array",
5054
+ items: { type: "string" },
5055
+ description: "Project-relative (or absolute) source files to mutate. Keep to files changed by the current task."
5056
+ },
5057
+ testCommand: {
5058
+ type: "string",
5059
+ description: 'Exact command that runs the relevant tests, e.g. "pnpm exec vitest run packages/core/tests/coordination/mutation-engine.test.ts".'
5060
+ },
5061
+ cwd: { type: "string", description: "Working directory for the test command." },
5062
+ maxPerFile: {
5063
+ type: "number",
5064
+ minimum: 1,
5065
+ maximum: 25,
5066
+ description: "Mutant cap per file per pass. Default 10."
5067
+ },
5068
+ maxStrengthenAttempts: {
5069
+ type: "number",
5070
+ minimum: 0,
5071
+ maximum: 5,
5072
+ description: "Strengthen\u2192re-verify rounds. Default 2 when repairSubagentId is set, else 0."
5073
+ },
5074
+ repairSubagentId: {
5075
+ type: "string",
5076
+ description: "Subagent that owns the tests. When set and mutants survive, it receives a strengthen-tests task and the survivors are re-verified."
5077
+ },
5078
+ chaosWorktree: {
5079
+ anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
5080
+ description: "Worktree override for the chaos agent. Defaults to the roster policy for chaos-monkey ('off'), because mutation targets are usually freshly written and uncommitted \u2014 a worktree from HEAD would not contain them and every mutant would drift to skipped. Only pass 'auto' or 'required' when the targets are committed."
5081
+ },
5082
+ timeoutMs: { type: "number", minimum: 1, description: "Per-task timeout for chaos/strengthen/rerun tasks." },
5083
+ reportOnly: {
5084
+ type: "boolean",
5085
+ description: "Skip the strengthen loop even when survivors exist. Default false."
5101
5086
  }
5102
- i++;
5103
- lastToken = c;
5104
- continue;
5105
- }
5106
- if (c === "`") {
5107
- closeRun(i);
5108
- stack.push({ kind: "template", depth: 0, parens: [] });
5109
- i++;
5110
- continue;
5087
+ },
5088
+ required: ["targets", "testCommand"],
5089
+ additionalProperties: false
5090
+ },
5091
+ async execute(input, ctx) {
5092
+ const i = normalizeMutationTestInput(input);
5093
+ const root = opts.projectRoot ?? ctx.projectRoot;
5094
+ const plan = buildPlan(i, root);
5095
+ if (plan.length === 0) {
5096
+ return {
5097
+ verdict: "inconclusive",
5098
+ passed: false,
5099
+ error: "No mutable sites found in the given targets (after comment/string filtering)."
5100
+ };
5111
5101
  }
5112
- if (c === "/" && line[i + 1] === "/") {
5113
- closeRun(i);
5114
- break;
5102
+ const chaosBase = roster?.[CHAOS_ROLE];
5103
+ if (!chaosBase) {
5104
+ return {
5105
+ verdict: "inconclusive",
5106
+ passed: false,
5107
+ error: "chaos-monkey role missing from the roster \u2014 refusing to spawn a saboteur without its prompt/tools contract. Build the toolset with a roster that includes 'chaos-monkey' (FLEET_ROSTER does)."
5108
+ };
5115
5109
  }
5116
- if (c === "/" && line[i + 1] === "*") {
5117
- closeRun(i);
5118
- const close = line.indexOf("*/", i + 2);
5119
- if (close === -1) {
5120
- inBlockComment = true;
5110
+ const chaosSubagentId = await director.spawn(
5111
+ makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
5112
+ );
5113
+ const chaosTaskId = await director.assign({
5114
+ id: randomUUID9(),
5115
+ subagentId: chaosSubagentId,
5116
+ description: buildChaosTask(plan, i, 1, []),
5117
+ timeoutMs: i.timeoutMs
5118
+ });
5119
+ const [chaosResult] = await director.awaitTasks([chaosTaskId]);
5120
+ const pass1 = collectOutcomes(chaosResult, plan);
5121
+ const survivors = pass1.filter((m) => m.status === "survived");
5122
+ const maxAttempts = clamp(
5123
+ i.maxStrengthenAttempts ?? (i.repairSubagentId && !i.reportOnly ? DEFAULT_MAX_STRENGTHEN_ATTEMPTS : 0),
5124
+ 0,
5125
+ 5
5126
+ );
5127
+ const attempts = [];
5128
+ let current = survivors;
5129
+ let rerunUnknowns = [];
5130
+ while (current.length > 0 && attempts.length < maxAttempts && i.repairSubagentId) {
5131
+ const attemptNo = attempts.length + 1;
5132
+ const strengthenTaskId = await director.assign({
5133
+ id: randomUUID9(),
5134
+ subagentId: i.repairSubagentId,
5135
+ description: buildStrengthenTask(current, i, attemptNo),
5136
+ timeoutMs: i.timeoutMs
5137
+ });
5138
+ const [strengthenResult] = await director.awaitTasks([strengthenTaskId]);
5139
+ if (strengthenResult?.status !== "success") {
5140
+ attempts.push({
5141
+ attempt: attemptNo,
5142
+ survivorsBefore: current,
5143
+ strengthenResult: strengthenResult ? { taskId: strengthenResult.taskId, status: strengthenResult.status } : void 0,
5144
+ survivorsAfter: current,
5145
+ suspectedEquivalent: []
5146
+ });
5121
5147
  break;
5122
5148
  }
5123
- i = close + 2;
5124
- continue;
5125
- }
5126
- if (c === "/") {
5127
- if (!tokenCanEndOperand(lastToken)) {
5128
- closeRun(i);
5129
- const next = skipRegexLiteral(line, i);
5130
- lastToken = next > i + 1 ? "regex" : "/";
5131
- i = next;
5132
- continue;
5133
- }
5134
- }
5135
- if (c === "(") {
5136
- top.parens.push(CONTROL_KEYWORDS.has(lastToken ?? "") ? "control" : "expr");
5137
- lastToken = c;
5138
- } else if (c === ")") {
5139
- const kind = top.parens.pop() ?? "expr";
5140
- lastToken = kind === "control" ? "control-paren-close" : ")";
5141
- } else if (c === "{") {
5142
- top.depth++;
5143
- lastToken = c;
5144
- } else if (c === "}") {
5145
- if (top.depth > 0) {
5146
- top.depth--;
5147
- lastToken = c;
5148
- } else if (stack.length > 1) {
5149
- closeRun(i);
5150
- stack.pop();
5151
- i++;
5152
- continue;
5153
- } else {
5154
- lastToken = c;
5155
- }
5156
- } else if (c !== " " && c !== " " && c !== "\r") {
5157
- lastToken = c;
5149
+ const survivorPlan = plan.filter((p) => current.some((s) => s.id === p.id));
5150
+ const rerunSubagentId = await director.spawn(
5151
+ makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
5152
+ );
5153
+ const rerunTaskId = await director.assign({
5154
+ id: randomUUID9(),
5155
+ subagentId: rerunSubagentId,
5156
+ description: buildChaosTask(survivorPlan, i, attemptNo + 1, current),
5157
+ timeoutMs: i.timeoutMs
5158
+ });
5159
+ const [rerunResult] = await director.awaitTasks([rerunTaskId]);
5160
+ const passN = collectOutcomes(rerunResult, survivorPlan);
5161
+ const stillSurviving = passN.filter((m) => !isKill(m.status));
5162
+ rerunUnknowns = passN.filter((m) => m.status === "skipped");
5163
+ attempts.push({
5164
+ attempt: attemptNo,
5165
+ survivorsBefore: current,
5166
+ strengthenResult: { taskId: strengthenResult.taskId, status: strengthenResult.status },
5167
+ rerunResult: { taskId: rerunTaskId, status: rerunResult?.status ?? "unknown" },
5168
+ survivorsAfter: stillSurviving,
5169
+ suspectedEquivalent: stillSurviving.filter((m) => m.status === "survived" && current.some((c) => c.id === m.id)).map((m) => m.id)
5170
+ });
5171
+ current = stillSurviving;
5158
5172
  }
5159
- if (runStart === null) runStart = i;
5160
- i++;
5173
+ const finalSurvivors = current.filter((m) => m.status === "survived");
5174
+ const verifiedCount = pass1.filter((m) => m.status !== "skipped").length;
5175
+ const skippedCount = pass1.filter((m) => m.status === "skipped").length;
5176
+ const rerunUnknownCount = rerunUnknowns.length;
5177
+ const score = plan.length === 0 ? 0 : pass1.filter((m) => isKill(m.status)).length / plan.length;
5178
+ const verdict = verifiedCount === 0 ? "inconclusive" : finalSurvivors.length === 0 ? skippedCount > 0 || rerunUnknownCount > 0 ? "partial" : "pass" : score >= 0.8 ? "partial" : "fail";
5179
+ return {
5180
+ verdict,
5181
+ passed: verdict === "pass",
5182
+ mutationScore: Number.parseFloat(score.toFixed(3)),
5183
+ planned: plan.length,
5184
+ killed: pass1.filter((m) => isKill(m.status)).length,
5185
+ // Breakout of `killed`: how many kills were detected by the test
5186
+ // command hanging rather than by a failing assertion. A subset of
5187
+ // `killed`, surfaced so a director can distinguish a hang-heavy
5188
+ // suite (mutants breaking termination, not assertions) from an
5189
+ // assertion-strong one. hangHeavy = killedByHang === killed.
5190
+ killedByHang: pass1.filter((m) => m.status === "killed-by-hang").length,
5191
+ survived: pass1.filter((m) => m.status === "survived").length,
5192
+ skipped: pass1.filter((m) => m.status === "skipped").length,
5193
+ finalSurvivors: finalSurvivors.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
5194
+ suspectedEquivalent: attempts.flatMap((a) => a.suspectedEquivalent),
5195
+ strengthenAttempts: attempts.length,
5196
+ attempts,
5197
+ chaosTaskId,
5198
+ // Unverified leftovers from the strengthen loop: surfaced so the
5199
+ // caller can see WHICH mutants lack kill evidence, and counted by
5200
+ // the verdict gate above.
5201
+ unverifiedFromRerun: rerunUnknowns.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
5202
+ nextAction: finalSurvivors.length === 0 && rerunUnknownCount === 0 && skippedCount === 0 ? "accept" : attempts.length >= maxAttempts && i.repairSubagentId ? "manual_review_survivors" : "strengthen_tests"
5203
+ };
5161
5204
  }
5162
- closeRun(line.length);
5163
- }
5164
- return masks;
5205
+ };
5165
5206
  }
5166
- var KEYWORDS_BEFORE_REGEX = /* @__PURE__ */ new Set([
5167
- "return",
5168
- "typeof",
5169
- "instanceof",
5170
- "in",
5171
- "of",
5172
- "new",
5173
- "delete",
5174
- "void",
5175
- "throw",
5176
- "case",
5177
- "do",
5178
- "else",
5179
- "yield",
5180
- "await"
5181
- ]);
5182
- var CONTROL_KEYWORDS = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "catch", "with", "await"]);
5183
- function tokenCanEndOperand(token) {
5184
- if (token === null) return false;
5185
- if (/^[\w$]+$/.test(token)) return !KEYWORDS_BEFORE_REGEX.has(token);
5186
- return token === ")" || token === "]" || token === "." || token === '"' || token === "'" || token === "`";
5207
+ function normalizeMutationTestInput(input) {
5208
+ const raw = input ?? {};
5209
+ const targets = stringArray(raw["targets"]) ?? [];
5210
+ const testCommand = typeof raw["testCommand"] === "string" ? raw["testCommand"].trim() : "";
5211
+ return {
5212
+ targets: targets.filter(Boolean),
5213
+ testCommand,
5214
+ cwd: typeof raw["cwd"] === "string" && raw["cwd"].trim() ? raw["cwd"].trim() : void 0,
5215
+ maxPerFile: typeof raw["maxPerFile"] === "number" ? raw["maxPerFile"] : void 0,
5216
+ maxStrengthenAttempts: typeof raw["maxStrengthenAttempts"] === "number" ? raw["maxStrengthenAttempts"] : void 0,
5217
+ repairSubagentId: typeof raw["repairSubagentId"] === "string" && raw["repairSubagentId"].trim() ? raw["repairSubagentId"].trim() : void 0,
5218
+ chaosWorktree: normalizeWorktreeOverride(raw["chaosWorktree"]),
5219
+ timeoutMs: typeof raw["timeoutMs"] === "number" ? raw["timeoutMs"] : void 0,
5220
+ reportOnly: raw["reportOnly"] === true
5221
+ };
5187
5222
  }
5188
- function skipRegexLiteral(line, start) {
5189
- let i = start + 1;
5190
- let inClass = false;
5191
- while (i < line.length) {
5192
- const ch = line[i];
5193
- if (ch === "\\") {
5194
- i += 2;
5195
- continue;
5196
- }
5197
- if (inClass) {
5198
- if (ch === "]") inClass = false;
5199
- i++;
5200
- continue;
5201
- }
5202
- if (ch === "[") {
5203
- inClass = true;
5204
- i++;
5205
- continue;
5206
- }
5207
- if (ch === "/") {
5208
- i++;
5209
- break;
5210
- }
5211
- if (ch === "\n" || ch === "\r") return line.length;
5212
- i++;
5213
- }
5214
- while (i < line.length && /[a-z]/.test(line[i])) i++;
5215
- return i;
5223
+ function clamp(n, lo, hi) {
5224
+ return Math.min(hi, Math.max(lo, n));
5216
5225
  }
5217
- function parseMutationReport(text) {
5218
- const candidates = [];
5219
- const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/);
5220
- if (fence?.[1]) candidates.push(fence[1].trim());
5221
- const firstBrace = text.indexOf("{");
5222
- if (firstBrace >= 0) candidates.push(extractBalancedObject(text, firstBrace));
5223
- for (const candidate of candidates) {
5224
- if (!candidate) continue;
5226
+ function buildPlan(i, projectRoot) {
5227
+ const plan = [];
5228
+ for (const target of i.targets) {
5229
+ const abs = isAbsolute2(target) ? target : join3(projectRoot ?? process.cwd(), target);
5230
+ let source;
5225
5231
  try {
5226
- const parsed = JSON.parse(candidate);
5227
- if (!Array.isArray(parsed.mutants)) continue;
5228
- return {
5229
- mutants: parsed.mutants.map(normalizeMutantEntry).filter((x) => Boolean(x)),
5230
- summary: typeof parsed.summary === "string" ? parsed.summary : void 0
5231
- };
5232
+ source = readFileSync(abs, "utf8");
5232
5233
  } catch {
5234
+ continue;
5233
5235
  }
5236
+ plan.push(...planMutations(target, source, { maxPerFile: i.maxPerFile ?? DEFAULT_MAX_PER_FILE }));
5234
5237
  }
5235
- return void 0;
5238
+ return plan;
5236
5239
  }
5237
- function extractBalancedObject(text, start) {
5238
- let depth = 0;
5239
- let inString = false;
5240
- let escaped = false;
5241
- for (let i = start; i < text.length; i++) {
5242
- const c = text[i];
5243
- if (escaped) {
5244
- escaped = false;
5245
- continue;
5246
- }
5247
- if (c === "\\") {
5248
- escaped = true;
5249
- continue;
5240
+ function makeChaosConfig(base, worktree) {
5241
+ return { ...instantiateRosterConfig2(CHAOS_ROLE, base), worktree };
5242
+ }
5243
+ function buildChaosTask(plan, i, pass, priorSurvivors) {
5244
+ const mutants = plan.map(
5245
+ (m) => `- ${m.id} | ${m.file}:${m.line}:${m.column} | ${m.kind} | "${m.original}" -> "${m.replacement}"`
5246
+ ).join("\n");
5247
+ const prior = priorSurvivors.length > 0 ? `
5248
+ These mutants survived a previous pass (pass ${pass - 1}) \u2014 re-verify them against the STRENGTHENED tests:
5249
+ ${priorSurvivors.map((s) => `- ${s.id} (${s.kind} @ ${s.file}:${s.line})`).join("\n")}` : "";
5250
+ return [
5251
+ "Execute this deterministic mutation plan against the current checkout.",
5252
+ "",
5253
+ "For each mutant, in order:",
5254
+ "1. Apply ONLY that mutation at its exact (file, line, column).",
5255
+ `2. Run the test command: ${i.testCommand}${i.cwd ? ` (cwd: ${i.cwd})` : ""}`,
5256
+ "3. Record killed (tests failed \u2014 quote first failing assertion), survived (suite green), or killed-by-hang (the test command timed out or was aborted \u2014 the mutation broke the suite by non-termination; record the timeout as evidence, do NOT report it as survived).",
5257
+ "4. Restore the file byte-for-byte before the next mutant.",
5258
+ "",
5259
+ "Mutants:",
5260
+ mutants,
5261
+ prior,
5262
+ "",
5263
+ "Rules: one mutation at a time; never stack; if the anchored token no longer matches, mark skipped with the drift as evidence; do not fix or refactor anything; stay inside the plan.",
5264
+ "Finish with submit_result, then repeat the same JSON as your final text."
5265
+ ].join("\n");
5266
+ }
5267
+ function buildStrengthenTask(survivors, i, attempt) {
5268
+ const confirmed = survivors.filter((s) => s.status === "survived");
5269
+ const unverified = survivors.filter((s) => s.status === "skipped");
5270
+ const row = (s) => `- ${s.id} | ${s.file}:${s.line} | ${s.kind}${s.evidence ? ` | ${s.evidence}` : ""}`;
5271
+ return [
5272
+ `Strengthen the tests so the mutants below die (attempt ${attempt}).`,
5273
+ "",
5274
+ ...confirmed.length > 0 ? [
5275
+ "CONFIRMED SURVIVORS \u2014 each was a deliberate sabotage of production code that the current suite did NOT catch:",
5276
+ ...confirmed.map(row),
5277
+ ""
5278
+ ] : [],
5279
+ ...unverified.length > 0 ? [
5280
+ "UNVERIFIED \u2014 these mutations were never actually re-tested (the re-verify pass skipped or did not report them). Do NOT assume the suite misses them: first apply each mutation, run the tests, and confirm it really survives; if the tests already fail, report that instead of writing new assertions.",
5281
+ ...unverified.map(row),
5282
+ ""
5283
+ ] : [],
5284
+ `Test command that must fail under each CONFIRMED mutant: ${i.testCommand}`,
5285
+ "",
5286
+ "For each CONFIRMED survivor add or tighten exactly one assertion that pins the sabotaged boundary/behavior. Do not change production code. Do not weaken other tests. Run the suite green on clean code before finishing."
5287
+ ].join("\n");
5288
+ }
5289
+ function collectOutcomes(result, plan) {
5290
+ const fromText = parseTextOutcomes(result);
5291
+ if (fromText.length > 0) {
5292
+ const remaining = [...plan];
5293
+ const matched = [];
5294
+ for (const m of fromText) {
5295
+ const idx = remaining.findIndex((p) => p.id === m.id);
5296
+ if (idx === -1) continue;
5297
+ remaining.splice(idx, 1);
5298
+ matched.push(m);
5250
5299
  }
5251
- if (c === '"') inString = !inString;
5252
- if (inString) continue;
5253
- if (c === "{") depth++;
5254
- else if (c === "}") {
5255
- depth--;
5256
- if (depth === 0) return text.slice(start, i + 1);
5300
+ if (matched.length > 0) {
5301
+ const missing = remaining.map((p) => ({
5302
+ id: p.id,
5303
+ file: p.file,
5304
+ line: p.line,
5305
+ kind: p.kind,
5306
+ status: "skipped",
5307
+ evidence: "not reported by chaos task"
5308
+ }));
5309
+ return [...matched, ...missing];
5257
5310
  }
5258
5311
  }
5259
- return text.slice(start);
5312
+ return plan.map((p) => ({
5313
+ id: p.id,
5314
+ file: p.file,
5315
+ line: p.line,
5316
+ kind: p.kind,
5317
+ status: "skipped",
5318
+ evidence: result ? `chaos task ended ${result.status}` : "chaos task produced no result"
5319
+ }));
5260
5320
  }
5261
- function normalizeMutantEntry(value) {
5262
- if (typeof value !== "object" || value === null) return void 0;
5263
- const rec = value;
5264
- const id = typeof rec["id"] === "string" ? rec["id"] : void 0;
5265
- const status = rec["status"];
5266
- if (!id || status !== "killed" && status !== "survived" && status !== "skipped" && status !== "killed-by-hang") {
5267
- return void 0;
5268
- }
5269
- return {
5270
- id,
5271
- file: typeof rec["file"] === "string" ? rec["file"] : "",
5272
- line: typeof rec["line"] === "number" ? rec["line"] : 0,
5273
- kind: typeof rec["kind"] === "string" ? rec["kind"] : "",
5274
- status,
5275
- evidence: typeof rec["evidence"] === "string" ? rec["evidence"] : void 0
5276
- };
5321
+ function isKill(status) {
5322
+ return status === "killed" || status === "killed-by-hang";
5323
+ }
5324
+ function parseTextOutcomes(result) {
5325
+ const text = typeof result?.result === "string" ? result.result : void 0;
5326
+ if (!text) return [];
5327
+ const parsed = parseMutationReport(text);
5328
+ if (!parsed) return [];
5329
+ return parsed.mutants.map((m) => ({
5330
+ id: m.id,
5331
+ file: m.file,
5332
+ line: m.line,
5333
+ kind: m.kind,
5334
+ status: m.status,
5335
+ evidence: m.evidence
5336
+ }));
5277
5337
  }
5278
5338
 
5279
- // src/coordination/director-mutation-test-tool.ts
5280
- var DEFAULT_MAX_PER_FILE = 10;
5281
- var DEFAULT_MAX_STRENGTHEN_ATTEMPTS = 2;
5282
- var CHAOS_ROLE = "chaos-monkey";
5283
- function makeMutationTestTool(director, roster, opts = {}) {
5339
+ // src/coordination/director-quality-gate-tool.ts
5340
+ import { randomUUID as randomUUID10 } from "node:crypto";
5341
+ function makeQualityGateTool(director, roster) {
5284
5342
  return {
5285
- name: "mutation_test",
5286
- description: "Chaos Monkey mutation testing: deterministically sabotage boundary conditions in the target code (> to >=, + to -, boolean flips, return null), re-run the tests per mutant, and report which mutants were killed. Surviving mutants mean the tests are weak \u2014 optionally loop a strengthen-tests repair until they die.",
5287
- usageHint: "Use after writing new code AND its tests, before delivering. Pass targets (files) and testCommand. Provide repairSubagentId to auto-strengthen weak tests. Survivors that persist are reported as suspected-equivalent.",
5343
+ name: "quality_gate",
5344
+ description: "Run a first-class implementation quality gate. It can await implementer task ids, spawn independent verifier/reviewer agents, summarize their verdicts, and optionally send must-fix feedback back to an implementer until the gate passes or the repair-attempt limit is reached.",
5345
+ usageHint: "Use after code-changing work. Provide implementerTaskIds when available. Add repairSubagentId to iterate fixes automatically. Verdict only passes when every enabled reviewer/verifier explicitly passes.",
5288
5346
  permission: "auto",
5289
5347
  mutating: false,
5290
5348
  capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
5291
5349
  inputSchema: {
5292
5350
  type: "object",
5293
5351
  properties: {
5352
+ task: {
5353
+ type: "string",
5354
+ description: "Original implementation task or acceptance goal being gated."
5355
+ },
5356
+ implementerTaskIds: {
5357
+ type: "array",
5358
+ items: { type: "string" },
5359
+ description: "Optional completed or in-flight implementer task ids to await and include as implementation evidence."
5360
+ },
5361
+ repairSubagentId: {
5362
+ type: "string",
5363
+ description: "Optional implementer subagent id. When set and the gate fails, quality_gate assigns a repair task with reviewer/verifier feedback and reruns the gate."
5364
+ },
5365
+ maxRepairAttempts: {
5366
+ type: "number",
5367
+ minimum: 0,
5368
+ maximum: 5,
5369
+ description: "Maximum automatic repair iterations. Default: 2 when repairSubagentId is set, otherwise 0."
5370
+ },
5294
5371
  targets: {
5295
5372
  type: "array",
5296
5373
  items: { type: "string" },
5297
- description: "Project-relative (or absolute) source files to mutate. Keep to files changed by the current task."
5374
+ description: "Files, packages, or paths that reviewer/verifier should focus on."
5298
5375
  },
5299
- testCommand: {
5376
+ commands: {
5377
+ type: "array",
5378
+ items: { type: "string" },
5379
+ description: 'Verification commands that should pass, e.g. ["pnpm --filter @wrongstack/core typecheck"].'
5380
+ },
5381
+ expected: {
5300
5382
  type: "string",
5301
- description: 'Exact command that runs the relevant tests, e.g. "pnpm exec vitest run packages/core/tests/coordination/mutation-engine.test.ts".'
5383
+ description: "Expected behavior or acceptance criteria."
5302
5384
  },
5303
- cwd: { type: "string", description: "Working directory for the test command." },
5304
- maxPerFile: {
5305
- type: "number",
5306
- minimum: 1,
5307
- maximum: 25,
5308
- description: "Mutant cap per file per pass. Default 10."
5385
+ evidence: {
5386
+ type: "string",
5387
+ description: "Known implementation notes, diff summary, or commands already run."
5309
5388
  },
5310
- maxStrengthenAttempts: {
5311
- type: "number",
5312
- minimum: 0,
5313
- maximum: 5,
5314
- description: "Strengthen\u2192re-verify rounds. Default 2 when repairSubagentId is set, else 0."
5389
+ reviewer: {
5390
+ type: "boolean",
5391
+ description: "Whether to run the reviewer lane. Default true."
5315
5392
  },
5316
- repairSubagentId: {
5317
- type: "string",
5318
- description: "Subagent that owns the tests. When set and mutants survive, it receives a strengthen-tests task and the survivors are re-verified."
5393
+ verifier: {
5394
+ type: "boolean",
5395
+ description: "Whether to run the verifier lane. Default true."
5319
5396
  },
5320
- chaosWorktree: {
5397
+ timeoutMs: {
5398
+ type: "number",
5399
+ minimum: 1,
5400
+ description: "Optional per reviewer/verifier/repair task timeout."
5401
+ },
5402
+ reviewerWorktree: {
5321
5403
  anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
5322
- description: "Worktree override for the chaos agent. Defaults to the roster policy for chaos-monkey ('off'), because mutation targets are usually freshly written and uncommitted \u2014 a worktree from HEAD would not contain them and every mutant would drift to skipped. Only pass 'auto' or 'required' when the targets are committed."
5404
+ description: "Reviewer worktree override. Default off because reviewer is read-only."
5323
5405
  },
5324
- timeoutMs: { type: "number", minimum: 1, description: "Per-task timeout for chaos/strengthen/rerun tasks." },
5325
- reportOnly: {
5326
- type: "boolean",
5327
- description: "Skip the strengthen loop even when survivors exist. Default false."
5406
+ verifierWorktree: {
5407
+ anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
5408
+ description: "Verifier worktree override. Default auto so test artifacts stay isolated when fleet policy wants it."
5328
5409
  }
5329
- },
5330
- required: ["targets", "testCommand"],
5331
- additionalProperties: false
5332
- },
5333
- async execute(input, ctx) {
5334
- const i = normalizeMutationTestInput(input);
5335
- const root = opts.projectRoot ?? ctx.projectRoot;
5336
- const plan = buildPlan(i, root);
5337
- if (plan.length === 0) {
5338
- return {
5339
- verdict: "inconclusive",
5340
- passed: false,
5341
- error: "No mutable sites found in the given targets (after comment/string filtering)."
5342
- };
5343
5410
  }
5344
- const chaosBase = roster?.[CHAOS_ROLE];
5345
- if (!chaosBase) {
5411
+ },
5412
+ async execute(input) {
5413
+ const i = normalizeQualityGateInput(input);
5414
+ const runReviewer = i.reviewer !== false;
5415
+ const runVerifier = i.verifier !== false;
5416
+ if (!runReviewer && !runVerifier) {
5346
5417
  return {
5347
5418
  verdict: "inconclusive",
5348
5419
  passed: false,
5349
- error: "chaos-monkey role missing from the roster \u2014 refusing to spawn a saboteur without its prompt/tools contract. Build the toolset with a roster that includes 'chaos-monkey' (FLEET_ROSTER does)."
5420
+ error: "quality_gate requires reviewer, verifier, or both."
5350
5421
  };
5351
5422
  }
5352
- const chaosSubagentId = await director.spawn(
5353
- makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
5354
- );
5355
- const chaosTaskId = await director.assign({
5356
- id: randomUUID10(),
5357
- subagentId: chaosSubagentId,
5358
- description: buildChaosTask(plan, i, 1, []),
5359
- timeoutMs: i.timeoutMs
5360
- });
5361
- const [chaosResult] = await director.awaitTasks([chaosTaskId]);
5362
- const pass1 = collectOutcomes(chaosResult, plan);
5363
- const survivors = pass1.filter((m) => m.status === "survived");
5364
- const maxAttempts = clamp(
5365
- i.maxStrengthenAttempts ?? (i.repairSubagentId && !i.reportOnly ? DEFAULT_MAX_STRENGTHEN_ATTEMPTS : 0),
5366
- 0,
5367
- 5
5423
+ const implementerResults = i.implementerTaskIds && i.implementerTaskIds.length > 0 ? await director.awaitTasks(i.implementerTaskIds) : [];
5424
+ const maxRepairAttempts = clampRepairAttempts(
5425
+ i.maxRepairAttempts ?? (i.repairSubagentId ? 2 : 0)
5368
5426
  );
5427
+ const repairResults = [];
5369
5428
  const attempts = [];
5370
- let current = survivors;
5371
- let rerunUnknowns = [];
5372
- while (current.length > 0 && attempts.length < maxAttempts && i.repairSubagentId) {
5373
- const attemptNo = attempts.length + 1;
5374
- const strengthenTaskId = await director.assign({
5375
- id: randomUUID10(),
5376
- subagentId: i.repairSubagentId,
5377
- description: buildStrengthenTask(current, i, attemptNo),
5378
- timeoutMs: i.timeoutMs
5379
- });
5380
- const [strengthenResult] = await director.awaitTasks([strengthenTaskId]);
5381
- if (strengthenResult?.status !== "success") {
5382
- attempts.push({
5383
- attempt: attemptNo,
5384
- survivorsBefore: current,
5385
- strengthenResult: strengthenResult ? { taskId: strengthenResult.taskId, status: strengthenResult.status } : void 0,
5386
- survivorsAfter: current,
5387
- suspectedEquivalent: []
5429
+ for (let attempt = 1; ; attempt++) {
5430
+ const gateTaskIds = [];
5431
+ const taskRoleById = /* @__PURE__ */ new Map();
5432
+ if (runVerifier) {
5433
+ const subagentId = await director.spawn(
5434
+ makeQualityGateSubagentConfig("verifier", roster, i.verifierWorktree ?? "auto")
5435
+ );
5436
+ const taskId = await director.assign({
5437
+ id: randomUUID10(),
5438
+ subagentId,
5439
+ description: buildVerifierTask(i, {
5440
+ attempt,
5441
+ implementerResults,
5442
+ repairResults,
5443
+ priorAttempts: attempts
5444
+ }),
5445
+ timeoutMs: i.timeoutMs
5388
5446
  });
5389
- break;
5447
+ gateTaskIds.push(taskId);
5448
+ taskRoleById.set(taskId, "verifier");
5390
5449
  }
5391
- const survivorPlan = plan.filter((p) => current.some((s) => s.id === p.id));
5392
- const rerunSubagentId = await director.spawn(
5393
- makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
5394
- );
5395
- const rerunTaskId = await director.assign({
5450
+ if (runReviewer) {
5451
+ const subagentId = await director.spawn(
5452
+ makeQualityGateSubagentConfig("reviewer", roster, i.reviewerWorktree ?? "off")
5453
+ );
5454
+ const taskId = await director.assign({
5455
+ id: randomUUID10(),
5456
+ subagentId,
5457
+ description: buildReviewerTask(i, {
5458
+ attempt,
5459
+ implementerResults,
5460
+ repairResults,
5461
+ priorAttempts: attempts
5462
+ }),
5463
+ timeoutMs: i.timeoutMs
5464
+ });
5465
+ gateTaskIds.push(taskId);
5466
+ taskRoleById.set(taskId, "reviewer");
5467
+ }
5468
+ const gateResults = await director.awaitTasks(gateTaskIds);
5469
+ const reports = gateResults.map((r) => assessRoleResult(taskRoleById.get(r.taskId), r));
5470
+ const assessment = assessQualityGate(reports);
5471
+ attempts.push({ attempt, reports, ...assessment });
5472
+ if (assessment.passed || !i.repairSubagentId || attempt > maxRepairAttempts) {
5473
+ return {
5474
+ verdict: assessment.verdict,
5475
+ passed: assessment.passed,
5476
+ attempts,
5477
+ repairAttemptsUsed: repairResults.length,
5478
+ implementerResults: implementerResults.map(summarizeTaskResult),
5479
+ nextAction: assessment.passed ? "accept" : i.repairSubagentId && attempt > maxRepairAttempts ? "manual_intervention_or_raise_repair_limit" : "inspect_failures"
5480
+ };
5481
+ }
5482
+ const repairTaskId = await director.assign({
5396
5483
  id: randomUUID10(),
5397
- subagentId: rerunSubagentId,
5398
- description: buildChaosTask(survivorPlan, i, attemptNo + 1, current),
5484
+ subagentId: i.repairSubagentId,
5485
+ description: buildRepairTask(i, attempts[attempts.length - 1], attempt),
5399
5486
  timeoutMs: i.timeoutMs
5400
5487
  });
5401
- const [rerunResult] = await director.awaitTasks([rerunTaskId]);
5402
- const passN = collectOutcomes(rerunResult, survivorPlan);
5403
- const stillSurviving = passN.filter((m) => !isKill(m.status));
5404
- rerunUnknowns = passN.filter((m) => m.status === "skipped");
5405
- attempts.push({
5406
- attempt: attemptNo,
5407
- survivorsBefore: current,
5408
- strengthenResult: { taskId: strengthenResult.taskId, status: strengthenResult.status },
5409
- rerunResult: { taskId: rerunTaskId, status: rerunResult?.status ?? "unknown" },
5410
- survivorsAfter: stillSurviving,
5411
- suspectedEquivalent: stillSurviving.filter((m) => m.status === "survived" && current.some((c) => c.id === m.id)).map((m) => m.id)
5412
- });
5413
- current = stillSurviving;
5488
+ const [repairResult] = await director.awaitTasks([repairTaskId]);
5489
+ if (repairResult) repairResults.push(repairResult);
5490
+ if (repairResult?.status !== "success") {
5491
+ return {
5492
+ verdict: "fail",
5493
+ passed: false,
5494
+ attempts,
5495
+ repairAttemptsUsed: repairResults.length,
5496
+ repairResult: repairResult ? summarizeTaskResult(repairResult) : void 0,
5497
+ implementerResults: implementerResults.map(summarizeTaskResult),
5498
+ nextAction: "repair_failed"
5499
+ };
5500
+ }
5414
5501
  }
5415
- const finalSurvivors = current.filter((m) => m.status === "survived");
5416
- const verifiedCount = pass1.filter((m) => m.status !== "skipped").length;
5417
- const skippedCount = pass1.filter((m) => m.status === "skipped").length;
5418
- const rerunUnknownCount = rerunUnknowns.length;
5419
- const score = plan.length === 0 ? 0 : pass1.filter((m) => isKill(m.status)).length / plan.length;
5420
- const verdict = verifiedCount === 0 ? "inconclusive" : finalSurvivors.length === 0 ? skippedCount > 0 || rerunUnknownCount > 0 ? "partial" : "pass" : score >= 0.8 ? "partial" : "fail";
5421
- return {
5422
- verdict,
5423
- passed: verdict === "pass",
5424
- mutationScore: Number.parseFloat(score.toFixed(3)),
5425
- planned: plan.length,
5426
- killed: pass1.filter((m) => isKill(m.status)).length,
5427
- // Breakout of `killed`: how many kills were detected by the test
5428
- // command hanging rather than by a failing assertion. A subset of
5429
- // `killed`, surfaced so a director can distinguish a hang-heavy
5430
- // suite (mutants breaking termination, not assertions) from an
5431
- // assertion-strong one. hangHeavy = killedByHang === killed.
5432
- killedByHang: pass1.filter((m) => m.status === "killed-by-hang").length,
5433
- survived: pass1.filter((m) => m.status === "survived").length,
5434
- skipped: pass1.filter((m) => m.status === "skipped").length,
5435
- finalSurvivors: finalSurvivors.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
5436
- suspectedEquivalent: attempts.flatMap((a) => a.suspectedEquivalent),
5437
- strengthenAttempts: attempts.length,
5438
- attempts,
5439
- chaosTaskId,
5440
- // Unverified leftovers from the strengthen loop: surfaced so the
5441
- // caller can see WHICH mutants lack kill evidence, and counted by
5442
- // the verdict gate above.
5443
- unverifiedFromRerun: rerunUnknowns.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
5444
- nextAction: finalSurvivors.length === 0 && rerunUnknownCount === 0 && skippedCount === 0 ? "accept" : attempts.length >= maxAttempts && i.repairSubagentId ? "manual_review_survivors" : "strengthen_tests"
5445
- };
5446
5502
  }
5447
5503
  };
5448
5504
  }
5449
- function normalizeMutationTestInput(input) {
5505
+ function normalizeQualityGateInput(input) {
5450
5506
  const raw = input ?? {};
5451
- const targets = stringArray(raw["targets"]) ?? [];
5452
- const testCommand = typeof raw["testCommand"] === "string" ? raw["testCommand"].trim() : "";
5453
5507
  return {
5454
- targets: targets.filter(Boolean),
5455
- testCommand,
5456
- cwd: typeof raw["cwd"] === "string" && raw["cwd"].trim() ? raw["cwd"].trim() : void 0,
5457
- maxPerFile: typeof raw["maxPerFile"] === "number" ? raw["maxPerFile"] : void 0,
5458
- maxStrengthenAttempts: typeof raw["maxStrengthenAttempts"] === "number" ? raw["maxStrengthenAttempts"] : void 0,
5459
- repairSubagentId: typeof raw["repairSubagentId"] === "string" && raw["repairSubagentId"].trim() ? raw["repairSubagentId"].trim() : void 0,
5460
- chaosWorktree: normalizeWorktreeOverride(raw["chaosWorktree"]),
5461
- timeoutMs: typeof raw["timeoutMs"] === "number" ? raw["timeoutMs"] : void 0,
5462
- reportOnly: raw["reportOnly"] === true
5508
+ task: typeof raw.task === "string" ? raw.task : void 0,
5509
+ implementerTaskIds: stringArray(raw.implementerTaskIds),
5510
+ repairSubagentId: typeof raw.repairSubagentId === "string" && raw.repairSubagentId.trim() ? raw.repairSubagentId.trim() : void 0,
5511
+ maxRepairAttempts: typeof raw.maxRepairAttempts === "number" ? raw.maxRepairAttempts : void 0,
5512
+ targets: stringArray(raw.targets),
5513
+ commands: stringArray(raw.commands),
5514
+ expected: typeof raw.expected === "string" ? raw.expected : void 0,
5515
+ evidence: typeof raw.evidence === "string" ? raw.evidence : void 0,
5516
+ reviewer: typeof raw.reviewer === "boolean" ? raw.reviewer : void 0,
5517
+ verifier: typeof raw.verifier === "boolean" ? raw.verifier : void 0,
5518
+ timeoutMs: typeof raw.timeoutMs === "number" ? raw.timeoutMs : void 0,
5519
+ reviewerWorktree: normalizeWorktreeOverride(raw.reviewerWorktree),
5520
+ verifierWorktree: normalizeWorktreeOverride(raw.verifierWorktree)
5463
5521
  };
5464
5522
  }
5465
- function clamp(n, lo, hi) {
5466
- return Math.min(hi, Math.max(lo, n));
5467
- }
5468
- function buildPlan(i, projectRoot) {
5469
- const plan = [];
5470
- for (const target of i.targets) {
5471
- const abs = isAbsolute2(target) ? target : join3(projectRoot ?? process.cwd(), target);
5472
- let source;
5473
- try {
5474
- source = readFileSync(abs, "utf8");
5475
- } catch {
5476
- continue;
5477
- }
5478
- plan.push(...planMutations(target, source, { maxPerFile: i.maxPerFile ?? DEFAULT_MAX_PER_FILE }));
5479
- }
5480
- return plan;
5523
+ function clampRepairAttempts(value) {
5524
+ if (!Number.isFinite(value)) return 0;
5525
+ return Math.max(0, Math.min(5, Math.floor(value)));
5481
5526
  }
5482
- function makeChaosConfig(base, worktree) {
5483
- return { ...instantiateRosterConfig2(CHAOS_ROLE, base), worktree };
5527
+ function makeQualityGateSubagentConfig(role, roster, worktree) {
5528
+ const base = roster?.[role] ?? getAgentDefinition(role)?.config ?? { name: role, role };
5529
+ return {
5530
+ ...instantiateRosterConfig2(role, base),
5531
+ worktree
5532
+ };
5484
5533
  }
5485
- function buildChaosTask(plan, i, pass, priorSurvivors) {
5486
- const mutants = plan.map(
5487
- (m) => `- ${m.id} | ${m.file}:${m.line}:${m.column} | ${m.kind} | "${m.original}" -> "${m.replacement}"`
5488
- ).join("\n");
5489
- const prior = priorSurvivors.length > 0 ? `
5490
- These mutants survived a previous pass (pass ${pass - 1}) \u2014 re-verify them against the STRENGTHENED tests:
5491
- ${priorSurvivors.map((s) => `- ${s.id} (${s.kind} @ ${s.file}:${s.line})`).join("\n")}` : "";
5534
+ function buildVerifierTask(input, state) {
5492
5535
  return [
5493
- "Execute this deterministic mutation plan against the current checkout.",
5494
- "",
5495
- "For each mutant, in order:",
5496
- "1. Apply ONLY that mutation at its exact (file, line, column).",
5497
- `2. Run the test command: ${i.testCommand}${i.cwd ? ` (cwd: ${i.cwd})` : ""}`,
5498
- "3. Record killed (tests failed \u2014 quote first failing assertion), survived (suite green), or killed-by-hang (the test command timed out or was aborted \u2014 the mutation broke the suite by non-termination; record the timeout as evidence, do NOT report it as survived).",
5499
- "4. Restore the file byte-for-byte before the next mutant.",
5500
- "",
5501
- "Mutants:",
5502
- mutants,
5503
- prior,
5536
+ "Run the independent verification gate for this implementation.",
5537
+ "Return Markdown with `## Verdict` and make the first verdict word exactly `pass`, `fail`, or `blocked`.",
5538
+ "Do not edit code. Run the smallest meaningful command set and include exact failures.",
5504
5539
  "",
5505
- "Rules: one mutation at a time; never stack; if the anchored token no longer matches, mark skipped with the drift as evidence; do not fix or refactor anything; stay inside the plan.",
5506
- "Finish with submit_result, then repeat the same JSON as your final text."
5507
- ].join("\n");
5540
+ `Gate attempt: ${state.attempt}`,
5541
+ input.task ? `Original task:
5542
+ ${input.task}` : void 0,
5543
+ input.targets?.length ? `Targets:
5544
+ ${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
5545
+ input.commands?.length ? `Required or suggested commands:
5546
+ ${input.commands.map((c) => `- ${c}`).join("\n")}` : void 0,
5547
+ input.expected ? `Expected behavior:
5548
+ ${input.expected}` : void 0,
5549
+ input.evidence ? `Known evidence:
5550
+ ${input.evidence}` : void 0,
5551
+ taskResultsBlock("Implementer results", state.implementerResults),
5552
+ taskResultsBlock("Repair results so far", state.repairResults),
5553
+ priorAttemptsBlock(state.priorAttempts)
5554
+ ].filter((part) => !!part).join("\n\n");
5508
5555
  }
5509
- function buildStrengthenTask(survivors, i, attempt) {
5510
- const confirmed = survivors.filter((s) => s.status === "survived");
5511
- const unverified = survivors.filter((s) => s.status === "skipped");
5512
- const row = (s) => `- ${s.id} | ${s.file}:${s.line} | ${s.kind}${s.evidence ? ` | ${s.evidence}` : ""}`;
5556
+ function buildReviewerTask(input, state) {
5513
5557
  return [
5514
- `Strengthen the tests so the mutants below die (attempt ${attempt}).`,
5558
+ "Run independent code review for this implementation.",
5559
+ "Return Markdown with `## Verdict` and make the first verdict phrase exactly `approve`, `request changes`, or `needs verification`.",
5560
+ "Do not edit code. Treat missing proof, vague tests, and uncertainty as blocking until verifier evidence exists.",
5515
5561
  "",
5516
- ...confirmed.length > 0 ? [
5517
- "CONFIRMED SURVIVORS \u2014 each was a deliberate sabotage of production code that the current suite did NOT catch:",
5518
- ...confirmed.map(row),
5519
- ""
5520
- ] : [],
5521
- ...unverified.length > 0 ? [
5522
- "UNVERIFIED \u2014 these mutations were never actually re-tested (the re-verify pass skipped or did not report them). Do NOT assume the suite misses them: first apply each mutation, run the tests, and confirm it really survives; if the tests already fail, report that instead of writing new assertions.",
5523
- ...unverified.map(row),
5524
- ""
5525
- ] : [],
5526
- `Test command that must fail under each CONFIRMED mutant: ${i.testCommand}`,
5562
+ `Gate attempt: ${state.attempt}`,
5563
+ input.task ? `Original task:
5564
+ ${input.task}` : void 0,
5565
+ input.targets?.length ? `Targets:
5566
+ ${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
5567
+ input.expected ? `Expected behavior:
5568
+ ${input.expected}` : void 0,
5569
+ input.evidence ? `Known evidence:
5570
+ ${input.evidence}` : void 0,
5571
+ taskResultsBlock("Implementer results", state.implementerResults),
5572
+ taskResultsBlock("Repair results so far", state.repairResults),
5573
+ priorAttemptsBlock(state.priorAttempts)
5574
+ ].filter((part) => !!part).join("\n\n");
5575
+ }
5576
+ function buildRepairTask(input, attempt, attemptNumber) {
5577
+ return [
5578
+ `Repair the implementation after quality gate attempt ${attemptNumber} failed.`,
5579
+ "Address every must-fix item. Run relevant checks before returning.",
5580
+ "Do not claim done unless verifier/reviewer feedback is resolved.",
5527
5581
  "",
5528
- "For each CONFIRMED survivor add or tighten exactly one assertion that pins the sabotaged boundary/behavior. Do not change production code. Do not weaken other tests. Run the suite green on clean code before finishing."
5529
- ].join("\n");
5582
+ input.task ? `Original task:
5583
+ ${input.task}` : void 0,
5584
+ input.targets?.length ? `Targets:
5585
+ ${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
5586
+ input.commands?.length ? `Commands expected to pass:
5587
+ ${input.commands.map((c) => `- ${c}`).join("\n")}` : void 0,
5588
+ input.expected ? `Expected behavior:
5589
+ ${input.expected}` : void 0,
5590
+ attempt.mustFix.length ? `Must fix:
5591
+ ${attempt.mustFix.map((f) => `- ${f}`).join("\n")}` : void 0,
5592
+ attempt.uncertaintyFlags.length ? `Uncertainty flags to resolve:
5593
+ ${attempt.uncertaintyFlags.map((f) => `- ${f}`).join("\n")}` : void 0,
5594
+ `Reviewer/verifier reports:
5595
+ ${attempt.reports.map((r) => `### ${r.role} (${r.verdict})
5596
+ ${r.summary}`).join("\n\n")}`
5597
+ ].filter((part) => !!part).join("\n\n");
5530
5598
  }
5531
- function collectOutcomes(result, plan) {
5532
- const fromText = parseTextOutcomes(result);
5533
- if (fromText.length > 0) {
5534
- const remaining = [...plan];
5535
- const matched = [];
5536
- for (const m of fromText) {
5537
- const idx = remaining.findIndex((p) => p.id === m.id);
5538
- if (idx === -1) continue;
5539
- remaining.splice(idx, 1);
5540
- matched.push(m);
5599
+ function taskResultsBlock(title, results) {
5600
+ if (results.length === 0) return void 0;
5601
+ return `${title}:
5602
+ ${results.map((r) => `### ${r.subagentId}/${r.taskId}
5603
+ ${summarizeTaskResult(r).summary}`).join("\n\n")}`;
5604
+ }
5605
+ function priorAttemptsBlock(attempts) {
5606
+ if (attempts.length === 0) return void 0;
5607
+ return `Prior quality gate attempts:
5608
+ ${attempts.map(
5609
+ (a) => `### Attempt ${a.attempt}
5610
+ ${a.reports.map((r) => `- ${r.role}: ${r.verdict}${r.error ? ` (${r.error})` : ""}`).join("\n")}`
5611
+ ).join("\n\n")}`;
5612
+ }
5613
+ function summarizeTaskResult(result) {
5614
+ const text = typeof result.result === "string" ? result.result : result.result !== void 0 ? JSON.stringify(result.result, null, 2) : "";
5615
+ const error = result.error ? `${result.error.kind}: ${result.error.message}` : void 0;
5616
+ return {
5617
+ taskId: result.taskId,
5618
+ subagentId: result.subagentId,
5619
+ status: result.status,
5620
+ summary: excerpt(text || error || "(no output)", 4e3),
5621
+ error
5622
+ };
5623
+ }
5624
+ function assessRoleResult(role, result) {
5625
+ const resolvedRole = role ?? (result.subagentId.includes("review") ? "reviewer" : "verifier");
5626
+ const summary = summarizeTaskResult(result);
5627
+ const text = summary.summary;
5628
+ const uncertaintyFlags = extractSection(text, "Uncertainty Flags");
5629
+ if (result.status !== "success") {
5630
+ return {
5631
+ role: resolvedRole,
5632
+ subagentId: result.subagentId,
5633
+ taskId: result.taskId,
5634
+ status: result.status,
5635
+ verdict: "fail",
5636
+ summary: text,
5637
+ uncertaintyFlags,
5638
+ error: summary.error ?? result.status
5639
+ };
5640
+ }
5641
+ return {
5642
+ role: resolvedRole,
5643
+ subagentId: result.subagentId,
5644
+ taskId: result.taskId,
5645
+ status: result.status,
5646
+ verdict: parseQualityVerdict(resolvedRole, text),
5647
+ summary: text,
5648
+ uncertaintyFlags
5649
+ };
5650
+ }
5651
+ function assessQualityGate(reports) {
5652
+ const mustFix = [];
5653
+ const uncertaintyFlags = [];
5654
+ let hasFail = false;
5655
+ let hasInconclusive = false;
5656
+ for (const report of reports) {
5657
+ if (report.verdict === "fail") hasFail = true;
5658
+ if (report.verdict === "inconclusive") hasInconclusive = true;
5659
+ const blocking = extractSection(report.summary, "Must Fix") || extractSection(report.summary, "Failures") || extractSection(report.summary, "Verification Gaps");
5660
+ if (blocking) mustFix.push(`${report.role}: ${excerpt(blocking, 1e3)}`);
5661
+ if (report.uncertaintyFlags) {
5662
+ uncertaintyFlags.push(`${report.role}: ${excerpt(report.uncertaintyFlags, 1e3)}`);
5541
5663
  }
5542
- if (matched.length > 0) {
5543
- const missing = remaining.map((p) => ({
5544
- id: p.id,
5545
- file: p.file,
5546
- line: p.line,
5547
- kind: p.kind,
5548
- status: "skipped",
5549
- evidence: "not reported by chaos task"
5550
- }));
5551
- return [...matched, ...missing];
5664
+ if (report.error) mustFix.push(`${report.role}: ${report.error}`);
5665
+ }
5666
+ if (hasFail) return { verdict: "fail", passed: false, mustFix, uncertaintyFlags };
5667
+ if (hasInconclusive || reports.length === 0) {
5668
+ return { verdict: "inconclusive", passed: false, mustFix, uncertaintyFlags };
5669
+ }
5670
+ return { verdict: "pass", passed: true, mustFix, uncertaintyFlags };
5671
+ }
5672
+ function parseQualityVerdict(role, text) {
5673
+ const normalized = text.toLowerCase();
5674
+ const verdictBlock = normalized.match(/(?:^|\n)\s*(?:#+\s*)?verdict\b[^\n]*(?:\n|:|-)?([\s\S]{0,500})/)?.[0] ?? normalized.slice(0, 1e3);
5675
+ if (role === "reviewer") {
5676
+ if (/\b(request\s+changes|needs\s+verification|reject|rejected|fail|failed|blocked)\b/.test(
5677
+ verdictBlock
5678
+ )) {
5679
+ return "fail";
5680
+ }
5681
+ if (/\b(approve|approved|pass|passed)\b/.test(verdictBlock)) return "pass";
5682
+ if (sectionHasBlockingContent(text, "Must Fix") || sectionHasBlockingContent(text, "Verification Gaps")) {
5683
+ return "fail";
5552
5684
  }
5685
+ return "inconclusive";
5553
5686
  }
5554
- return plan.map((p) => ({
5555
- id: p.id,
5556
- file: p.file,
5557
- line: p.line,
5558
- kind: p.kind,
5559
- status: "skipped",
5560
- evidence: result ? `chaos task ended ${result.status}` : "chaos task produced no result"
5561
- }));
5687
+ if (/\b(fail|failed|blocked|red)\b/.test(verdictBlock)) return "fail";
5688
+ if (/\b(pass|passed|green|approve|approved)\b/.test(verdictBlock)) return "pass";
5689
+ if (sectionHasBlockingContent(text, "Failures")) return "fail";
5690
+ return "inconclusive";
5562
5691
  }
5563
- function isKill(status) {
5564
- return status === "killed" || status === "killed-by-hang";
5692
+ function sectionHasBlockingContent(text, heading) {
5693
+ const section = extractSection(text, heading);
5694
+ if (!section) return false;
5695
+ return !/^\s*(none|n\/a|no\b|no issues|empty|\(none\))\s*\.?\s*$/i.test(section.trim());
5696
+ }
5697
+ function extractSection(text, heading) {
5698
+ const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5699
+ const pattern = new RegExp(
5700
+ `(?:^|\\n)\\s*#{1,6}\\s*${escaped}\\s*\\n([\\s\\S]*?)(?=\\n\\s*#{1,6}\\s+|$)`,
5701
+ "i"
5702
+ );
5703
+ const match = text.match(pattern);
5704
+ const body = match?.[1]?.trim();
5705
+ return body ? body : void 0;
5565
5706
  }
5566
- function parseTextOutcomes(result) {
5567
- const text = typeof result?.result === "string" ? result.result : void 0;
5568
- if (!text) return [];
5569
- const parsed = parseMutationReport(text);
5570
- if (!parsed) return [];
5571
- return parsed.mutants.map((m) => ({
5572
- id: m.id,
5573
- file: m.file,
5574
- line: m.line,
5575
- kind: m.kind,
5576
- status: m.status,
5577
- evidence: m.evidence
5578
- }));
5707
+ function excerpt(text, max) {
5708
+ if (text.length <= max) return text;
5709
+ return `${text.slice(0, max - 20).trimEnd()}
5710
+ ...(truncated)`;
5579
5711
  }
5580
5712
 
5581
5713
  // src/coordination/director-tools.ts
@@ -5630,6 +5762,10 @@ function makeSpawnTool(director, roster) {
5630
5762
  type: "string",
5631
5763
  description: "Model id within the provider. Defaults to the leader model when omitted."
5632
5764
  },
5765
+ tier: {
5766
+ type: "string",
5767
+ description: "Cost/capability level for this worker: 'budget' (cheap + fast, for mechanical or well-specified work), 'standard' (the default), or 'premium' (expensive + most capable, for work where being wrong is costly). Resolved deterministically into a model, a failover chain, and a spend budget from `modelTiers` config, so you do NOT need to know any model id. Omit to let the routing table decide by role. An explicit `model` always wins over the tier.\n\nThis is the RIGHT place to spend a cheap tier. Because this spawn is non-blocking, a budget-tier worker being slower costs you nothing \u2014 you keep working while it runs. The same tier on `delegate` would just make you wait longer."
5768
+ },
5633
5769
  systemPromptOverride: {
5634
5770
  type: "string",
5635
5771
  description: "Extra prompt text appended after the role-base prompt."
@@ -5667,7 +5803,7 @@ function makeSpawnTool(director, roster) {
5667
5803
  mutating: false,
5668
5804
  capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
5669
5805
  inputSchema,
5670
- async execute(input) {
5806
+ async execute(input, ctx) {
5671
5807
  const i = input ?? {};
5672
5808
  const role = typeof i.role === "string" ? i.role : void 0;
5673
5809
  const description = typeof i.description === "string" ? i.description : void 0;
@@ -5707,11 +5843,20 @@ function makeSpawnTool(director, roster) {
5707
5843
  if (typeof i.timeoutMs === "number") cfg.timeoutMs = i.timeoutMs;
5708
5844
  if (typeof i.idleTimeoutMs === "number") cfg.idleTimeoutMs = i.idleTimeoutMs;
5709
5845
  if (typeof i.maxTokens === "number") cfg.maxTokens = i.maxTokens;
5846
+ if (typeof i.tier === "string" && i.tier) cfg.tier = i.tier;
5847
+ const budgetPins = [];
5848
+ if (typeof i.maxIterations === "number") budgetPins.push("maxIterations");
5849
+ if (typeof i.maxToolCalls === "number") budgetPins.push("maxToolCalls");
5850
+ if (typeof i.maxCostUsd === "number") budgetPins.push("maxCostUsd");
5851
+ if (typeof i.maxTokens === "number") budgetPins.push("maxTokens");
5852
+ if (typeof i.timeoutMs === "number") budgetPins.push("timeoutMs");
5853
+ if (budgetPins.length) cfg.budgetPins = budgetPins;
5710
5854
  if (typeof i.worktree === "boolean" || i.worktree === "auto" || i.worktree === "required" || i.worktree === "off") {
5711
5855
  cfg.worktree = i.worktree;
5712
5856
  }
5713
5857
  try {
5714
- const subagentId = await director.spawn(cfg);
5858
+ const origin = callerSessionId(ctx);
5859
+ const subagentId = await director.spawn(origin ? { ...cfg, originSessionId: origin } : cfg);
5715
5860
  return {
5716
5861
  subagentId,
5717
5862
  provider: cfg.provider,
@@ -5784,6 +5929,10 @@ function makeKanbanQueueTool(director, roster) {
5784
5929
  role: { type: "string" },
5785
5930
  provider: { type: "string" },
5786
5931
  model: { type: "string" },
5932
+ tier: {
5933
+ type: "string",
5934
+ description: "Cost level for the dispatched workers ('budget' | 'standard' | 'premium'). Resolved from `modelTiers`; an explicit provider/model still wins."
5935
+ },
5787
5936
  fallbackModels: { type: "array", items: { type: "string" } },
5788
5937
  tools: { type: "array", items: { type: "string" } },
5789
5938
  allowedCapabilities: { type: "array", items: { type: "string" } },
@@ -5800,6 +5949,7 @@ function makeKanbanQueueTool(director, roster) {
5800
5949
  }
5801
5950
  const projectRoot = ctx.projectRoot;
5802
5951
  if (!projectRoot) return { error: "kanban_queue requires ctx.projectRoot." };
5952
+ const sessionId = ctx.eventSessionId();
5803
5953
  const maxTasks = Math.max(1, Math.min(20, Math.floor(i.maxTasks ?? 1)));
5804
5954
  const baseLeaseTtlMs = Math.max(1e3, Math.floor(i.leaseTtlMs ?? 5 * 60 * 1e3));
5805
5955
  const leaseTtlMs = i.awaitCompletion === true && i.timeoutMs !== void 0 ? Math.max(baseLeaseTtlMs, i.timeoutMs + 6e4) : baseLeaseTtlMs;
@@ -5820,6 +5970,7 @@ function makeKanbanQueueTool(director, roster) {
5820
5970
  if (candidateTaskIds && !candidateTaskId) break;
5821
5971
  if (candidateTaskId && budgetRejectedTaskIds.has(candidateTaskId)) continue;
5822
5972
  const reserved = await kanbanDispatch().reserveKanbanDispatch(projectRoot, {
5973
+ sessionId,
5823
5974
  ...i.boardId !== void 0 ? { boardId: i.boardId } : {},
5824
5975
  ...candidateTaskId !== void 0 ? { taskId: candidateTaskId } : {},
5825
5976
  routing: {
@@ -5828,6 +5979,7 @@ function makeKanbanQueueTool(director, roster) {
5828
5979
  ...i.role !== void 0 ? { role: i.role } : {},
5829
5980
  ...i.provider !== void 0 ? { provider: i.provider } : {},
5830
5981
  ...i.model !== void 0 ? { model: i.model } : {},
5982
+ ...i.tier !== void 0 ? { tier: i.tier } : {},
5831
5983
  ...i.fallbackModels !== void 0 ? { fallbackModels: i.fallbackModels } : {},
5832
5984
  ...i.tools !== void 0 ? { tools: i.tools } : {},
5833
5985
  ...i.allowedCapabilities !== void 0 ? { allowedCapabilities: i.allowedCapabilities } : {}
@@ -5858,7 +6010,7 @@ function makeKanbanQueueTool(director, roster) {
5858
6010
  status: "failed",
5859
6011
  error: budgetError
5860
6012
  },
5861
- { expectedLeaseId: ourLeaseId }
6013
+ { sessionId, expectedLeaseId: ourLeaseId }
5862
6014
  );
5863
6015
  errors.push({
5864
6016
  taskId: claim.task.id,
@@ -5901,6 +6053,7 @@ function makeKanbanQueueTool(director, roster) {
5901
6053
  }
5902
6054
  };
5903
6055
  const started = await kanbanDispatch().startKanbanDispatch(projectRoot, {
6056
+ sessionId,
5904
6057
  boardId: claim.board.id,
5905
6058
  taskId: claim.task.id,
5906
6059
  leaseId: ourLeaseId,
@@ -5946,6 +6099,7 @@ function makeKanbanQueueTool(director, roster) {
5946
6099
  }
5947
6100
  }
5948
6101
  await kanbanDispatch().failKanbanDispatch(projectRoot, {
6102
+ sessionId,
5949
6103
  boardId: claim.board.id,
5950
6104
  taskId: claim.task.id,
5951
6105
  leaseId: ourLeaseId,
@@ -5973,6 +6127,7 @@ function makeKanbanQueueTool(director, roster) {
5973
6127
  for (const dispatch of dispatches) {
5974
6128
  if (!disposers.has(dispatch.runTaskId)) continue;
5975
6129
  void renewAndRevokeLease({
6130
+ sessionId,
5976
6131
  projectRoot,
5977
6132
  boardId: dispatch.boardId,
5978
6133
  taskId: dispatch.taskId,
@@ -6027,7 +6182,8 @@ function makeKanbanQueueTool(director, roster) {
6027
6182
  {
6028
6183
  leaseExpiresAt: refreshedExpiry,
6029
6184
  expectedLeaseId: dispatch.leaseId
6030
- }
6185
+ },
6186
+ { sessionId, expectedLeaseId: dispatch.leaseId }
6031
6187
  );
6032
6188
  } catch {
6033
6189
  }
@@ -6052,6 +6208,7 @@ function makeKanbanQueueTool(director, roster) {
6052
6208
  runTaskIds.delete(dispatch.runTaskId);
6053
6209
  if (result.status === "success") {
6054
6210
  const completed = await kanbanDispatch().completeKanbanDispatch(projectRoot, {
6211
+ sessionId,
6055
6212
  boardId: dispatch.boardId,
6056
6213
  taskId: dispatch.taskId,
6057
6214
  leaseId: dispatch.leaseId,
@@ -6068,6 +6225,7 @@ function makeKanbanQueueTool(director, roster) {
6068
6225
  }
6069
6226
  } else {
6070
6227
  const failed = await kanbanDispatch().failKanbanDispatch(projectRoot, {
6228
+ sessionId,
6071
6229
  boardId: dispatch.boardId,
6072
6230
  taskId: dispatch.taskId,
6073
6231
  leaseId: dispatch.leaseId,
@@ -6114,6 +6272,7 @@ async function renewAndRevokeLease(opts) {
6114
6272
  runTaskId,
6115
6273
  ourLeaseId,
6116
6274
  refreshedExpiry,
6275
+ sessionId,
6117
6276
  director,
6118
6277
  disposers
6119
6278
  } = opts;
@@ -6132,10 +6291,13 @@ async function renewAndRevokeLease(opts) {
6132
6291
  } catch {
6133
6292
  }
6134
6293
  if (!revoked) {
6135
- await kanbanDispatch().heartbeatTaskAssignment(projectRoot, boardId, taskId, {
6136
- leaseExpiresAt: refreshedExpiry,
6137
- expectedLeaseId: ourLeaseId
6138
- }).catch(() => {
6294
+ await kanbanDispatch().heartbeatTaskAssignment(
6295
+ projectRoot,
6296
+ boardId,
6297
+ taskId,
6298
+ { leaseExpiresAt: refreshedExpiry, expectedLeaseId: ourLeaseId },
6299
+ { sessionId, expectedLeaseId: ourLeaseId }
6300
+ ).catch(() => {
6139
6301
  });
6140
6302
  }
6141
6303
  }
@@ -6320,6 +6482,28 @@ function resolveDirectorSpawnModel(config, opts) {
6320
6482
  if (entry.modelRuntime) config.modelRuntime = entry.modelRuntime;
6321
6483
  }
6322
6484
  }
6485
+ if (opts.config) {
6486
+ if (opts.tier) {
6487
+ const decision = classifyTier(opts.config, { role: config.role, tier: opts.tier });
6488
+ if (decision && !decision.configured) {
6489
+ const available = listTierIds(opts.config);
6490
+ opts.logger?.warn(
6491
+ `spawn: tier "${opts.tier}" is not a configured level${available.length ? ` (available: ${available.join(", ")})` : " (modelTiers.levels is empty)"} \u2014 falling through to matrix/session resolution.`
6492
+ );
6493
+ }
6494
+ }
6495
+ const resolvedTier = resolveTier(opts.config, {
6496
+ role: config.role,
6497
+ tier: opts.tier
6498
+ });
6499
+ if (resolvedTier) {
6500
+ applyTierToSubagentConfig(config, resolvedTier);
6501
+ opts.onTierResolved?.(resolvedTier);
6502
+ opts.logger?.info(
6503
+ `spawn: tier="${resolvedTier.tier}" (via ${resolvedTier.source}) applied for role "${config.role ?? "?"}"`
6504
+ );
6505
+ }
6506
+ }
6323
6507
  if (!config.provider && opts.sessionProvider) {
6324
6508
  config.provider = opts.sessionProvider;
6325
6509
  opts.logger?.info(
@@ -6447,22 +6631,35 @@ var FleetUsageAggregator = class {
6447
6631
  metaLookup;
6448
6632
  perSubagent = /* @__PURE__ */ new Map();
6449
6633
  total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
6634
+ retired = {
6635
+ subagents: 0,
6636
+ input: 0,
6637
+ output: 0,
6638
+ cacheRead: 0,
6639
+ cacheWrite: 0,
6640
+ cost: 0
6641
+ };
6450
6642
  unsub = [];
6451
6643
  /**
6452
- * Remove a terminated subagent's data from the aggregator and subtract its
6453
- * contribution from the running totals. Call this when a subagent is removed
6454
- * from the fleet so the aggregator doesn't accumulate unbounded data for
6455
- * entities that will never emit events again.
6644
+ * Drop a terminated subagent's per-agent entry, folding its spend into
6645
+ * {@link FleetUsage.retired} so the run total stays whole.
6646
+ *
6647
+ * The per-agent map is what needed bounding — one entry per spawn, for
6648
+ * entities that will never emit again. Deducting from `total` as well (the
6649
+ * original behavior) also un-spent real tokens: a run that retires each
6650
+ * worker on completion reported a total that shrank as it made progress, and
6651
+ * the director budgets against that number.
6456
6652
  */
6457
6653
  removeSubagent(subagentId) {
6458
6654
  const snap = this.perSubagent.get(subagentId);
6459
6655
  if (!snap) return;
6460
6656
  this.perSubagent.delete(subagentId);
6461
- this.total.input -= snap.input;
6462
- this.total.output -= snap.output;
6463
- this.total.cacheRead -= snap.cacheRead;
6464
- this.total.cacheWrite -= snap.cacheWrite;
6465
- this.total.cost -= snap.cost;
6657
+ this.retired.subagents += 1;
6658
+ this.retired.input += snap.input;
6659
+ this.retired.output += snap.output;
6660
+ this.retired.cacheRead += snap.cacheRead;
6661
+ this.retired.cacheWrite += snap.cacheWrite;
6662
+ this.retired.cost += snap.cost;
6466
6663
  }
6467
6664
  /** Disposes all fleet-bus subscriptions. Call when the aggregator is no longer needed. */
6468
6665
  dispose() {
@@ -6475,7 +6672,8 @@ var FleetUsageAggregator = class {
6475
6672
  total: { ...this.total },
6476
6673
  perSubagent: Object.fromEntries(
6477
6674
  Array.from(this.perSubagent.entries()).map(([k, v]) => [k, { ...v }])
6478
- )
6675
+ ),
6676
+ retired: { ...this.retired }
6479
6677
  };
6480
6678
  }
6481
6679
  ensure(subagentId) {
@@ -6664,11 +6862,15 @@ async function spawn2(host, config, priceLookup) {
6664
6862
  );
6665
6863
  host.coordinator.setSubagentBridge(result.subagentId, subagentBridge);
6666
6864
  host.subagentBridges.set(result.subagentId, subagentBridge);
6865
+ const currentSessionId = typeof host.coordinator.sessionOf === "function" ? host.coordinator.sessionOf(
6866
+ result.subagentId
6867
+ ) : void 0;
6667
6868
  host.fleet.emit({
6668
6869
  subagentId: result.subagentId,
6669
6870
  ts: Date.now(),
6670
6871
  type: "subagent.spawned",
6671
6872
  payload: {
6873
+ ...currentSessionId ? { sessionId: currentSessionId } : {},
6672
6874
  subagentId: result.subagentId,
6673
6875
  taskId: "",
6674
6876
  // taskId will be set when assign() is called
@@ -6740,7 +6942,12 @@ var LargeAnswerStore = class _LargeAnswerStore {
6740
6942
  if (value === void 0 || value === null) {
6741
6943
  return { summary: String(value), inline: true };
6742
6944
  }
6743
- const serialized = typeof value === "string" ? value : JSON.stringify(value);
6945
+ let serialized;
6946
+ try {
6947
+ serialized = typeof value === "string" ? value : JSON.stringify(value) ?? String(value);
6948
+ } catch {
6949
+ serialized = String(value);
6950
+ }
6744
6951
  const size = serialized.length;
6745
6952
  const bytes = Buffer.byteLength(serialized, "utf8");
6746
6953
  if (size <= this.sizeThreshold) {
@@ -7117,6 +7324,7 @@ var Director = class _Director {
7117
7324
  leaderContextPressure = 0;
7118
7325
  maxLeaderContextLoad;
7119
7326
  maxContext;
7327
+ appConfig;
7120
7328
  modelMatrix;
7121
7329
  workCompleteFlag = false;
7122
7330
  btwNotes = new DirectorBtwNotes();
@@ -7146,6 +7354,7 @@ var Director = class _Director {
7146
7354
  this.maxFleetTokens = opts.directorBudget?.maxTokens ?? Number.POSITIVE_INFINITY;
7147
7355
  this.maxLeaderContextLoad = opts.maxLeaderContextLoad ?? 0.85;
7148
7356
  this.maxContext = opts.maxContext ?? 128e3;
7357
+ this.appConfig = opts.appConfig;
7149
7358
  this.modelMatrix = opts.modelMatrix;
7150
7359
  this.sessionsRoot = opts.sessionsRoot;
7151
7360
  this.directorRunId = opts.directorRunId ?? this.id;
@@ -7412,8 +7621,14 @@ var Director = class _Director {
7412
7621
  return subagentId;
7413
7622
  }
7414
7623
  resolveSpawnModel(config) {
7624
+ const appConfig = typeof this.appConfig === "function" ? this.appConfig() : this.appConfig;
7415
7625
  resolveDirectorSpawnModel(config, {
7416
7626
  modelMatrix: this.modelMatrix,
7627
+ ...appConfig ? { config: appConfig } : {},
7628
+ ...config.tier ? { tier: config.tier } : {},
7629
+ onTierResolved: (resolved) => {
7630
+ config.tier = resolved.tier;
7631
+ },
7417
7632
  sessionProvider: this.sessionProvider,
7418
7633
  sessionModel: this.sessionModel,
7419
7634
  statusTracker: this.statusTracker,
@@ -7518,6 +7733,25 @@ var Director = class _Director {
7518
7733
  void this.remove(id).catch((err) => this.logShutdownError("terminate_all_remove", err));
7519
7734
  }
7520
7735
  }
7736
+ /**
7737
+ * Terminate every subagent spawned by ONE session.
7738
+ *
7739
+ * What a tab's Stop button needs. `terminateAll()` is the wrong tool once
7740
+ * several sessions share a director: it would kill three other tabs' fleets
7741
+ * along with this one's. Aborting the leader's run only unwinds workers the
7742
+ * leader is BLOCKED on (the delegate tool terminates those itself); anything
7743
+ * started with `spawn_subagent` + `assign_task` keeps running because nobody
7744
+ * asked it to stop. This is that ask, scoped to the session that owns them.
7745
+ */
7746
+ async terminateSession(sessionId) {
7747
+ if (!sessionId) return;
7748
+ const ids = this.coordinator.subagentIdsForSession(sessionId);
7749
+ if (ids.length === 0) return;
7750
+ await this.coordinator.stopSession(sessionId);
7751
+ for (const id of ids) {
7752
+ void this.remove(id).catch((err) => this.logShutdownError("terminate_session_remove", err));
7753
+ }
7754
+ }
7521
7755
  async remove(subagentId) {
7522
7756
  this.clearSubagentIdleRetirement(subagentId);
7523
7757
  this.subagentIdleDelayMs.delete(subagentId);
@@ -8195,7 +8429,7 @@ function makeFleetStatusTool(opts = {}) {
8195
8429
  const resolveMailbox = opts.resolveMailbox ?? ((ctx) => getSharedProjectMailbox(opts.projectDir ?? defaultResolveProjectDir(ctx), opts.events));
8196
8430
  return {
8197
8431
  name: "fleet_status",
8198
- description: "Live snapshot of every agent working on this canonical project (all clients, processes, sessions, branches, and linked Git worktrees): who is online, what task each is on, which tool is running, and progress counters. Check it before starting work that might overlap with a peer, when deciding whether to wait for someone or proceed, or when a task mentions another agent. Read-only. To talk to a peer, use mail_send with the returned id.",
8432
+ description: "Live snapshot of every agent working on this canonical project (all clients, processes, sessions, branches, and linked Git worktrees): who is online, what task each is on, which tool is running, and progress counters. Check it before starting work that might overlap with a peer, when deciding whether to wait for someone or proceed, or when a task mentions another agent. Read-only. Same-session talk: session_note with the returned id. Durable cross-session mail: mail_send.",
8199
8433
  usageHint: "fleet_status (optionally: onlineOnly=false to include recently-offline agents)",
8200
8434
  category: "Coordination",
8201
8435
  permission: "auto",
@@ -8626,7 +8860,8 @@ Idle workers: ${idleWorkerIds.join(", ")}`,
8626
8860
  });
8627
8861
  await this.opts.actions.notifyLeader(
8628
8862
  "Fleet rebalanced",
8629
- `Supervisor moved ${moved.length} queued task(s) off busy worker ${fromWorkerId}: ${moved.join(", ")}.`
8863
+ `Supervisor moved ${moved.length} queued task(s) off busy worker ${fromWorkerId}: ${moved.join(", ")}.`,
8864
+ fromWorkerId
8630
8865
  ).catch(() => {
8631
8866
  });
8632
8867
  }
@@ -8683,7 +8918,8 @@ Idle workers: ${idleWorkerIds.join(", ")}`,
8683
8918
  });
8684
8919
  await this.opts.actions.notifyLeader(
8685
8920
  "Fleet helper spawned",
8686
- `Supervisor spawned helper ${res.subagentId} to drain a ${pendingCount}-task backlog.`
8921
+ `Supervisor spawned helper ${res.subagentId} to drain a ${pendingCount}-task backlog.`,
8922
+ res.subagentId
8687
8923
  ).catch(() => {
8688
8924
  });
8689
8925
  } else {
@@ -8748,7 +8984,8 @@ Idle workers: ${idleWorkerIds.join(", ")}`,
8748
8984
  this.emitAction("steer", true, "stuck nudge", s.id);
8749
8985
  await this.opts.actions.notifyLeader(
8750
8986
  `Worker ${s.name} may be stuck`,
8751
- `No observable activity for ${Math.round(this.cfg.stuckMs / 1e3)}s. Supervisor nudged it; consider terminate_subagent + reassigning if it stays silent.`
8987
+ `No observable activity for ${Math.round(this.cfg.stuckMs / 1e3)}s. Supervisor nudged it; consider terminate_subagent + reassigning if it stays silent.`,
8988
+ s.id
8752
8989
  ).catch(() => {
8753
8990
  });
8754
8991
  this.record({
@@ -8799,7 +9036,8 @@ Idle workers: ${idleWorkerIds.join(", ")}`,
8799
9036
  this.emitAction("terminate", true, `${streak} consecutive failures`, subagentId);
8800
9037
  await this.opts.actions.notifyLeader(
8801
9038
  `Worker ${subagentId} terminated`,
8802
- `Supervisor terminated ${subagentId} after ${streak} consecutive failures. Reassign its pending work.`
9039
+ `Supervisor terminated ${subagentId} after ${streak} consecutive failures. Reassign its pending work.`,
9040
+ subagentId
8803
9041
  ).catch(() => {
8804
9042
  });
8805
9043
  this.record({
@@ -8821,7 +9059,8 @@ Idle workers: ${idleWorkerIds.join(", ")}`,
8821
9059
  this.emitAction("steer", true, "failure streak", subagentId);
8822
9060
  await this.opts.actions.notifyLeader(
8823
9061
  `Worker ${subagentId} failing repeatedly`,
8824
- `${streak} consecutive failures. Supervisor steered it; consider reassigning its work if the next task also fails.`
9062
+ `${streak} consecutive failures. Supervisor steered it; consider reassigning its work if the next task also fails.`,
9063
+ subagentId
8825
9064
  ).catch(() => {
8826
9065
  });
8827
9066
  this.record({
@@ -9101,6 +9340,12 @@ function assertCapability(actor, cap, op) {
9101
9340
  }
9102
9341
 
9103
9342
  // src/coordination/mail-tools.ts
9343
+ function scopeAgentMailToOwningSession(ctx, to, sessionId) {
9344
+ const owning = ctx.meta["sessionId"];
9345
+ if (typeof owning !== "string" || owning.length === 0) return {};
9346
+ if (to.trim().toLowerCase() !== "leader") return {};
9347
+ return { sessionAffinity: { sessionId } };
9348
+ }
9104
9349
  function makeResolver(opts) {
9105
9350
  return opts.resolveMailbox ?? ((ctx) => getSharedProjectMailbox(opts.projectDir ?? defaultResolveProjectDir(ctx), opts.events));
9106
9351
  }
@@ -9204,7 +9449,8 @@ function makeMailSendTool(opts = {}) {
9204
9449
  body: parsed.body,
9205
9450
  priority: parsed.priority,
9206
9451
  replyTo: parsed.replyTo,
9207
- senderSessionId: identity.sessionId
9452
+ senderSessionId: identity.sessionId,
9453
+ ...scopeAgentMailToOwningSession(ctx, delivery.to, identity.sessionId)
9208
9454
  });
9209
9455
  return {
9210
9456
  ok: true,
@@ -9301,6 +9547,61 @@ function makeMailInboxTool(opts = {}) {
9301
9547
  };
9302
9548
  }
9303
9549
 
9550
+ // src/coordination/session-note-tool.ts
9551
+ var KINDS = ["note", "result", "ask", "steer"];
9552
+ function isKind(value) {
9553
+ return KINDS.includes(value);
9554
+ }
9555
+ function makeSessionNoteTool() {
9556
+ return {
9557
+ name: "session_note",
9558
+ description: 'Send an ephemeral note to the leader or another agent in THIS session. Delivered at their next iteration. Use it for same-session talk (findings, a short ask, a steer). Durable mail remains the durable cross-session channel. to="leader" reaches the session leader; to="@session" fans out to every other live agent in the session; an exact agent id reaches one peer. You never receive your own note.',
9559
+ usageHint: 'session_note to="leader" kind="result" body="file:line \u2014 what it is"',
9560
+ category: "Coordination",
9561
+ permission: "auto",
9562
+ mutating: false,
9563
+ capabilities: [ToolCapabilities.SESSION_NOTE],
9564
+ inputSchema: {
9565
+ type: "object",
9566
+ properties: {
9567
+ to: {
9568
+ type: "string",
9569
+ description: 'Recipient: "leader", "@session", or a live agent id.'
9570
+ },
9571
+ body: { type: "string", description: "The note. Keep it compact." },
9572
+ kind: {
9573
+ type: "string",
9574
+ enum: [...KINDS],
9575
+ description: "note (default), result, ask, or steer."
9576
+ },
9577
+ subject: { type: "string", description: "Optional short subject (e.g. [explore])." }
9578
+ },
9579
+ required: ["to", "body"]
9580
+ },
9581
+ async execute(input, ctx) {
9582
+ const rec = input && typeof input === "object" ? input : {};
9583
+ const to = typeof rec["to"] === "string" ? rec["to"].trim() : "";
9584
+ const body = typeof rec["body"] === "string" ? rec["body"] : "";
9585
+ const subject = typeof rec["subject"] === "string" ? rec["subject"] : void 0;
9586
+ const kindRaw = typeof rec["kind"] === "string" ? rec["kind"].trim().toLowerCase() : "note";
9587
+ const kind = isKind(kindRaw) ? kindRaw : "note";
9588
+ if (!to || !body.trim()) {
9589
+ return { ok: false, delivered: 0, error: "to and body are required" };
9590
+ }
9591
+ const from = typeof ctx.meta["globalAgentId"] === "string" && ctx.meta["globalAgentId"] || ctx.agentId || mailboxIdentityBase(ctx.agentId);
9592
+ const { delivered } = postSessionNote({
9593
+ sessionId: resolveOwningSessionId(ctx),
9594
+ from,
9595
+ to,
9596
+ kind,
9597
+ body,
9598
+ subject
9599
+ });
9600
+ return { ok: delivered > 0, delivered, to, kind };
9601
+ }
9602
+ };
9603
+ }
9604
+
9304
9605
  // src/coordination/mailbox-actions.ts
9305
9606
  function actionToAckInput(action, input) {
9306
9607
  switch (action) {
@@ -9587,60 +9888,6 @@ function createMailboxHooks(opts) {
9587
9888
  };
9588
9889
  }
9589
9890
 
9590
- // src/coordination/mailbox-http-auth.ts
9591
- import { timingSafeEqual } from "node:crypto";
9592
- function authorizeMailboxBearerToken(request, expectedToken) {
9593
- const header = request.headers.authorization;
9594
- if (typeof header !== "string") return { allowed: false };
9595
- const match = /^Bearer\s+(.+)$/i.exec(header);
9596
- if (match === null) return { allowed: false };
9597
- const presented = Buffer.from(match[1] ?? "", "utf8");
9598
- const expected = Buffer.from(expectedToken, "utf8");
9599
- if (presented.length !== expected.length || !timingSafeEqual(presented, expected)) {
9600
- return { allowed: false };
9601
- }
9602
- return { allowed: true, rateLimitKey: expectedToken };
9603
- }
9604
- async function authorizePersistedMailboxCredential(request, store) {
9605
- const parsed = parseCredentialAuthorization(request);
9606
- if (parsed === void 0) return { allowed: false };
9607
- return credentialDecision(
9608
- parsed.credentialId,
9609
- await store.verifyPersisted(parsed.credentialId, parsed.secret)
9610
- );
9611
- }
9612
- function credentialDecision(credentialId, result) {
9613
- const credential = result.credential;
9614
- const projectId = credential?.projectId;
9615
- if (!result.valid || credential === void 0 || projectId === void 0) {
9616
- return { allowed: false };
9617
- }
9618
- const identitySeparator = credential.principalId.indexOf("@");
9619
- const role = credential.kind === "agent" ? credential.principalId.slice(0, identitySeparator === -1 ? void 0 : identitySeparator) : void 0;
9620
- const sessionId = identitySeparator === -1 ? void 0 : credential.principalId.slice(identitySeparator + 1) || void 0;
9621
- return {
9622
- allowed: true,
9623
- rateLimitKey: `cred:${credentialId}`,
9624
- actor: {
9625
- actorId: credential.principalId,
9626
- projectId,
9627
- kind: credential.kind,
9628
- capabilities: new Set(credential.capabilities),
9629
- authMode: "identity-token",
9630
- recipientAliases: role === void 0 ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([role]),
9631
- ...role !== void 0 ? { role } : {},
9632
- ...sessionId !== void 0 ? { sessionId } : {}
9633
- }
9634
- };
9635
- }
9636
- function parseCredentialAuthorization(request) {
9637
- const header = request.headers.authorization;
9638
- if (typeof header !== "string") return void 0;
9639
- const match = /^Credential\s+([^:]+):(.+)$/i.exec(header);
9640
- if (match === null || match[1] === void 0 || match[2] === void 0) return void 0;
9641
- return { credentialId: match[1], secret: match[2] };
9642
- }
9643
-
9644
9891
  // src/coordination/mailbox-http-validation.ts
9645
9892
  var MAILBOX_HTTP_MAX_AGE_CEILING_MS = 7 * 24 * 60 * 60 * 1e3;
9646
9893
  var MailboxHttpValidationError = class extends Error {
@@ -9986,13 +10233,19 @@ function validateAgentRegistration(body, actor) {
9986
10233
  }
9987
10234
  return result;
9988
10235
  }
9989
- function validateAgentHeartbeat(body, actorId) {
10236
+ function validateAgentHeartbeat(body, actor) {
9990
10237
  if (typeof body !== "object" || body === null) {
9991
10238
  throw validationError("expected JSON object body");
9992
10239
  }
9993
10240
  const object = body;
9994
- if (actorId !== void 0) rejectConflictingIdentity(object, "agentId", actorId);
9995
- const result = { agentId: actorId ?? requireString2(object, "agentId") };
10241
+ if (actor !== void 0) {
10242
+ rejectConflictingIdentity(object, "agentId", actor.actorId);
10243
+ rejectConflictingOptionalIdentity(object, "sessionId", actor.sessionId);
10244
+ rejectConflictingOptionalIdentity(object, "role", actor.role);
10245
+ }
10246
+ const agentId = actor?.actorId ?? requireString2(object, "agentId");
10247
+ if (actor === void 0) validateReaderId(agentId);
10248
+ const result = { agentId };
9996
10249
  const status = optionalString2(object, "status");
9997
10250
  const currentTool = optionalString2(object, "currentTool");
9998
10251
  const currentTask = optionalString2(object, "currentTask");
@@ -10013,6 +10266,25 @@ function validateAgentHeartbeat(body, actorId) {
10013
10266
  }
10014
10267
  result.toolCalls = toolCalls;
10015
10268
  }
10269
+ const name = optionalString2(object, "name");
10270
+ const pid = optionalNumber(object, "pid");
10271
+ if (name !== void 0) result.name = name;
10272
+ if (pid !== void 0) {
10273
+ if (!Number.isInteger(pid) || pid < 1) {
10274
+ throw validationError('field "pid" must be a positive integer when present');
10275
+ }
10276
+ result.pid = pid;
10277
+ }
10278
+ if (actor === void 0) {
10279
+ const sessionId = optionalString2(object, "sessionId");
10280
+ const role = optionalString2(object, "role");
10281
+ if (sessionId !== void 0) result.sessionId = sessionId;
10282
+ if (role !== void 0) result.role = role;
10283
+ } else {
10284
+ if (actor.sessionId !== void 0) result.sessionId = actor.sessionId;
10285
+ if (actor.role !== void 0) result.role = actor.role;
10286
+ }
10287
+ result.source = "http";
10016
10288
  return result;
10017
10289
  }
10018
10290
  function validateClientRegistration(body) {
@@ -10192,6 +10464,60 @@ async function checkMailbox(mailbox, input, minTimestampIso, includeReceiptState
10192
10464
  return { data, count: data.length };
10193
10465
  }
10194
10466
 
10467
+ // src/coordination/mailbox-http-auth.ts
10468
+ import { timingSafeEqual } from "node:crypto";
10469
+ function authorizeMailboxBearerToken(request, expectedToken) {
10470
+ const header = request.headers.authorization;
10471
+ if (typeof header !== "string") return { allowed: false };
10472
+ const match = /^Bearer\s+(.+)$/i.exec(header);
10473
+ if (match === null) return { allowed: false };
10474
+ const presented = Buffer.from(match[1] ?? "", "utf8");
10475
+ const expected = Buffer.from(expectedToken, "utf8");
10476
+ if (presented.length !== expected.length || !timingSafeEqual(presented, expected)) {
10477
+ return { allowed: false };
10478
+ }
10479
+ return { allowed: true, rateLimitKey: expectedToken };
10480
+ }
10481
+ async function authorizePersistedMailboxCredential(request, store) {
10482
+ const parsed = parseCredentialAuthorization(request);
10483
+ if (parsed === void 0) return { allowed: false };
10484
+ return credentialDecision(
10485
+ parsed.credentialId,
10486
+ await store.verifyPersisted(parsed.credentialId, parsed.secret)
10487
+ );
10488
+ }
10489
+ function credentialDecision(credentialId, result) {
10490
+ const credential = result.credential;
10491
+ const projectId = credential?.projectId;
10492
+ if (!result.valid || credential === void 0 || projectId === void 0) {
10493
+ return { allowed: false };
10494
+ }
10495
+ const identitySeparator = credential.principalId.indexOf("@");
10496
+ const role = credential.kind === "agent" ? credential.principalId.slice(0, identitySeparator === -1 ? void 0 : identitySeparator) : void 0;
10497
+ const sessionId = identitySeparator === -1 ? void 0 : credential.principalId.slice(identitySeparator + 1) || void 0;
10498
+ return {
10499
+ allowed: true,
10500
+ rateLimitKey: `cred:${credentialId}`,
10501
+ actor: {
10502
+ actorId: credential.principalId,
10503
+ projectId,
10504
+ kind: credential.kind,
10505
+ capabilities: new Set(credential.capabilities),
10506
+ authMode: "identity-token",
10507
+ recipientAliases: role === void 0 ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([role]),
10508
+ ...role !== void 0 ? { role } : {},
10509
+ ...sessionId !== void 0 ? { sessionId } : {}
10510
+ }
10511
+ };
10512
+ }
10513
+ function parseCredentialAuthorization(request) {
10514
+ const header = request.headers.authorization;
10515
+ if (typeof header !== "string") return void 0;
10516
+ const match = /^Credential\s+([^:]+):(.+)$/i.exec(header);
10517
+ if (match === null || match[1] === void 0 || match[2] === void 0) return void 0;
10518
+ return { credentialId: match[1], secret: match[2] };
10519
+ }
10520
+
10195
10521
  // src/coordination/mailbox-http-sse.ts
10196
10522
  var MAX_SSE_BUFFER_BYTES = 8 * 1024 * 1024;
10197
10523
  function mailboxEventRecords(event) {
@@ -10422,7 +10748,10 @@ function createMailboxHttpRouter(options) {
10422
10748
  response,
10423
10749
  access3.status ?? 401,
10424
10750
  access3.body ?? {
10425
- error: { code: "UNAUTHORIZED", message: "invalid or missing authorization credential" }
10751
+ error: {
10752
+ code: "UNAUTHORIZED",
10753
+ message: "invalid or missing authorization credential"
10754
+ }
10426
10755
  }
10427
10756
  );
10428
10757
  return;
@@ -10480,7 +10809,10 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
10480
10809
  const requiredCapability = requiredCredentialCapability(method, path12);
10481
10810
  if (requiredCapability === void 0) {
10482
10811
  writeJson(response, 403, {
10483
- error: { code: "FORBIDDEN", message: `credential access is not permitted for ${method} ${path12}` }
10812
+ error: {
10813
+ code: "FORBIDDEN",
10814
+ message: `credential access is not permitted for ${method} ${path12}`
10815
+ }
10484
10816
  });
10485
10817
  return;
10486
10818
  }
@@ -10613,7 +10945,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
10613
10945
  return;
10614
10946
  }
10615
10947
  if (method === "POST" && path12 === "/mailbox/agents/heartbeat") {
10616
- const input = validateAgentHeartbeat(await readJsonBody(request, maxBodyBytes), actor?.actorId);
10948
+ const input = validateAgentHeartbeat(await readJsonBody(request, maxBodyBytes), actor);
10617
10949
  if (actor !== void 0) input.agentId = actor.actorId;
10618
10950
  await mailbox.heartbeat(input);
10619
10951
  writeJson(response, 200, { ok: true });
@@ -10786,7 +11118,8 @@ async function getPackagesByAgent(opts, agentId) {
10786
11118
  const map = /* @__PURE__ */ new Map();
10787
11119
  for (const e of log.entries) {
10788
11120
  if (e.agentId === agentId) {
10789
- const key = `${e.manifestPath}|${e.packageName}`;
11121
+ const normPath2 = e.manifestPath.replace(/\\/g, "/");
11122
+ const key = `${normPath2}|${e.packageName}`;
10790
11123
  map.set(key, e);
10791
11124
  }
10792
11125
  }
@@ -11001,12 +11334,14 @@ var DEFAULTS2 = {
11001
11334
  degradedDurationMs: 3e4,
11002
11335
  blockAfterRateLimitHits: 1,
11003
11336
  blockAfterFailures: 5,
11004
- blockDurationMs: 3e5,
11337
+ blockDurationMs: 12e4,
11005
11338
  quotaBlockDurationMs: 9e5,
11339
+ quotaBlockEscalationMs: [18e5, 36e5],
11006
11340
  recoverAfterSuccesses: 3,
11007
11341
  maxErrorHistory: 50,
11008
11342
  quarantineSiblingsOnQuotaExhausted: true
11009
11343
  };
11344
+ var NON_QUOTA_HINT_CAP_FACTOR = 3;
11010
11345
  var ProviderModelStatusTracker = class {
11011
11346
  cfg;
11012
11347
  map = /* @__PURE__ */ new Map();
@@ -11015,6 +11350,7 @@ var ProviderModelStatusTracker = class {
11015
11350
  events;
11016
11351
  constructor(opts) {
11017
11352
  this.cfg = { ...DEFAULTS2, ...opts?.config };
11353
+ this.cfg.quotaBlockEscalationMs = [...this.cfg.quotaBlockEscalationMs];
11018
11354
  this.events = opts?.events;
11019
11355
  }
11020
11356
  // ── Public API ──────────────────────────────────────────────────────────
@@ -11032,6 +11368,7 @@ var ProviderModelStatusTracker = class {
11032
11368
  const key = pairKey(providerId, model);
11033
11369
  const s = this.getOrCreate(key, providerId, model);
11034
11370
  s.consecutiveFailures = 0;
11371
+ s.quotaBlockStreak = 0;
11035
11372
  s.consecutiveSuccesses += 1;
11036
11373
  s.totalSuccesses += 1;
11037
11374
  s.lastSuccessAt = Date.now();
@@ -11085,7 +11422,13 @@ var ProviderModelStatusTracker = class {
11085
11422
  const quotaExhausted = kind === "quota_exhausted" || isQuotaExhausted(kind, status, message);
11086
11423
  const providerWideQuota = quotaExhausted && !ROUTE_SCOPED_QUOTA_RE.test(message);
11087
11424
  const proseHintMs = quotaExhausted || kind === "rate_limit" ? parseResetHintMs(message, now) : void 0;
11088
- const effectiveRetryAfterMs = meta?.retryAfterMs && meta.retryAfterMs > 0 ? meta.retryAfterMs : proseHintMs;
11425
+ const rawHintMs = meta?.retryAfterMs && meta.retryAfterMs > 0 ? meta.retryAfterMs : proseHintMs;
11426
+ const effectiveRetryAfterMs = rawHintMs && rawHintMs > 0 ? quotaExhausted ? (
11427
+ // Quota keeps the provider-published reset, but a corrupt or
11428
+ // absurd structured Retry-After still cannot park a model
11429
+ // beyond the prose-hint maximum.
11430
+ Math.min(rawHintMs, MAX_RESET_HINT_MS)
11431
+ ) : Math.min(rawHintMs, this.cfg.blockDurationMs * NON_QUOTA_HINT_CAP_FACTOR) : void 0;
11089
11432
  const entry = Object.freeze({
11090
11433
  timestamp: now,
11091
11434
  kind,
@@ -11105,7 +11448,8 @@ var ProviderModelStatusTracker = class {
11105
11448
  if (quotaExhausted) {
11106
11449
  newState = "blocked";
11107
11450
  reason = "quota_exhausted";
11108
- s.stateExpiresAt = now + this.cfg.quotaBlockDurationMs;
11451
+ s.quotaBlockStreak += 1;
11452
+ s.stateExpiresAt = now + this.quotaBlockDurationForStreak(s.quotaBlockStreak);
11109
11453
  } else if (endpointUnreachable) {
11110
11454
  newState = "blocked";
11111
11455
  reason = "endpoint_unreachable";
@@ -11131,13 +11475,18 @@ var ProviderModelStatusTracker = class {
11131
11475
  }
11132
11476
  if (newState !== "healthy" && effectiveRetryAfterMs && effectiveRetryAfterMs > 0) {
11133
11477
  const hintExpiry = now + effectiveRetryAfterMs;
11134
- if (s.stateExpiresAt === null || hintExpiry > s.stateExpiresAt) {
11478
+ if (quotaExhausted) {
11479
+ s.stateExpiresAt = hintExpiry;
11480
+ } else if (s.stateExpiresAt === null || hintExpiry > s.stateExpiresAt) {
11135
11481
  s.stateExpiresAt = hintExpiry;
11136
11482
  }
11137
11483
  }
11138
11484
  if (this.cfg.quarantineSiblingsOnQuotaExhausted && providerWideQuota && s.stateExpiresAt !== null) {
11139
11485
  const previous = this.providerQuotaBlocks.get(providerId) ?? 0;
11140
- this.providerQuotaBlocks.set(providerId, Math.max(previous, s.stateExpiresAt));
11486
+ this.providerQuotaBlocks.set(
11487
+ providerId,
11488
+ Math.max(previous, this.fanOutExpiryFor(now, s.stateExpiresAt))
11489
+ );
11141
11490
  }
11142
11491
  if (s.state !== newState) {
11143
11492
  const oldState = s.state;
@@ -11147,10 +11496,26 @@ var ProviderModelStatusTracker = class {
11147
11496
  this.emitStatusChanged(providerId, model, s.state, s.state, "cooldown_extended");
11148
11497
  }
11149
11498
  if (this.cfg.quarantineSiblingsOnQuotaExhausted && providerWideQuota && newState === "blocked") {
11150
- this.quarantineSiblings(providerId, model, status, now, s.stateExpiresAt ?? 0);
11499
+ this.quarantineSiblings(
11500
+ providerId,
11501
+ model,
11502
+ status,
11503
+ now,
11504
+ this.fanOutExpiryFor(now, s.stateExpiresAt ?? now)
11505
+ );
11151
11506
  }
11152
11507
  return newState;
11153
11508
  }
11509
+ /**
11510
+ * Quarantine fan-out expiry: the fixed quota cooldown, never the
11511
+ * hint-extended trigger expiry. A weekly-cap reset time applies to the
11512
+ * model that published it; sibling models (and unseen pairs behind the
11513
+ * provider-wide gate) re-open after the base cooldown so one misclassified
11514
+ * or hint-stretched failure cannot silence a whole provider for hours.
11515
+ */
11516
+ fanOutExpiryFor(now, triggerExpiry) {
11517
+ return Math.min(triggerExpiry, now + this.cfg.quotaBlockDurationMs);
11518
+ }
11154
11519
  /**
11155
11520
  * Mark every other tracked pair on the same provider as blocked with the
11156
11521
  * same expiry as the triggering pair. Only pairs already known to the
@@ -11247,6 +11612,7 @@ var ProviderModelStatusTracker = class {
11247
11612
  s.state = "healthy";
11248
11613
  s.stateExpiresAt = null;
11249
11614
  s.consecutiveFailures = 0;
11615
+ s.quotaBlockStreak = 0;
11250
11616
  s.rateLimitHits = 0;
11251
11617
  s.overloadedHits = 0;
11252
11618
  s.serverErrors = 0;
@@ -11328,6 +11694,8 @@ var ProviderModelStatusTracker = class {
11328
11694
  /**
11329
11695
  * Release one entry for an immediate half-open probe on its next real use.
11330
11696
  * History and totals are retained so operators do not lose diagnostics.
11697
+ * The quota escalation streak is also retained on purpose: a probe that
11698
+ * fails with quota exhaustion again keeps climbing the ladder.
11331
11699
  */
11332
11700
  retryNow(providerId, model) {
11333
11701
  ({ providerId, model } = statusIdentity(providerId, model));
@@ -11470,13 +11838,29 @@ var ProviderModelStatusTracker = class {
11470
11838
  );
11471
11839
  if (this.cfg.quarantineSiblingsOnQuotaExhausted && providerWideQuota && !ROUTE_SCOPED_QUOTA_RE.test(s.lastErrorMessage ?? "") && s.state === "blocked" && s.stateExpiresAt !== null) {
11472
11840
  const previous = this.providerQuotaBlocks.get(providerId) ?? 0;
11473
- this.providerQuotaBlocks.set(providerId, Math.max(previous, s.stateExpiresAt));
11841
+ this.providerQuotaBlocks.set(
11842
+ providerId,
11843
+ Math.max(previous, this.fanOutExpiryFor(now, s.stateExpiresAt))
11844
+ );
11474
11845
  }
11475
11846
  restored += 1;
11476
11847
  }
11477
11848
  return restored;
11478
11849
  }
11479
11850
  // ── Private helpers ─────────────────────────────────────────────────────
11851
+ /**
11852
+ * Cooldown for the Nth consecutive quota block: the base duration for the
11853
+ * first block, then the escalation ladder for repeats, capped at the last
11854
+ * ladder entry. A provider-published reset hint bypasses this entirely
11855
+ * (the pair is closed until that exact time instead).
11856
+ */
11857
+ quotaBlockDurationForStreak(streak) {
11858
+ if (streak <= 1) return this.cfg.quotaBlockDurationMs;
11859
+ const ladder = this.cfg.quotaBlockEscalationMs;
11860
+ if (ladder.length === 0) return this.cfg.quotaBlockDurationMs;
11861
+ const tier = Math.min(streak - 2, ladder.length - 1);
11862
+ return ladder[tier] ?? this.cfg.quotaBlockDurationMs;
11863
+ }
11480
11864
  getOrCreate(key, _providerId, _model) {
11481
11865
  let s = this.map.get(key);
11482
11866
  if (!s) {
@@ -11494,6 +11878,7 @@ var ProviderModelStatusTracker = class {
11494
11878
  firstFailureAt: null,
11495
11879
  lastFailureAt: null,
11496
11880
  stateExpiresAt: null,
11881
+ quotaBlockStreak: 0,
11497
11882
  lastErrorKind: null,
11498
11883
  lastErrorMessage: null,
11499
11884
  lastErrorStatus: null,
@@ -11534,6 +11919,7 @@ var ProviderModelStatusTracker = class {
11534
11919
  emitStatusChanged(providerId, model, oldState, newState, reason) {
11535
11920
  if (!this.events) return;
11536
11921
  try {
11922
+ const entry = this.map.get(pairKey(providerId, model));
11537
11923
  this.events.emit("provider.status_changed", {
11538
11924
  providerId,
11539
11925
  model,
@@ -11541,7 +11927,15 @@ var ProviderModelStatusTracker = class {
11541
11927
  newState,
11542
11928
  reason,
11543
11929
  timestamp: Date.now(),
11544
- stateExpiresAt: this.map.get(pairKey(providerId, model))?.stateExpiresAt ?? void 0
11930
+ stateExpiresAt: entry?.stateExpiresAt ?? void 0,
11931
+ // Error context for durable audit logs (who/what/why): the failure
11932
+ // that led to this state, with the session/agent that hit it. Nulls
11933
+ // normalize to undefined so JSON serialization drops them.
11934
+ lastErrorKind: entry?.lastErrorKind ?? void 0,
11935
+ lastErrorStatus: entry?.lastErrorStatus ?? void 0,
11936
+ lastErrorMessage: entry?.lastErrorMessage ?? void 0,
11937
+ lastSessionId: entry?.lastSessionId ?? void 0,
11938
+ lastAgentId: entry?.lastAgentId ?? void 0
11545
11939
  });
11546
11940
  } catch {
11547
11941
  }
@@ -11686,7 +12080,7 @@ async function finalize(projectDir, tentative, boundPort) {
11686
12080
  url: `http://${tentative.host}:${boundPort}`
11687
12081
  };
11688
12082
  await atomicWriteJson(lockPath, finalized);
11689
- await fsp7.writeFile(tokenPath, finalized.token, { mode: 384 });
12083
+ await atomicWriteString(tokenPath, finalized.token);
11690
12084
  return finalized;
11691
12085
  }
11692
12086
  async function release(projectDir, generation) {
@@ -11735,12 +12129,11 @@ async function readLiveLock(projectDir) {
11735
12129
  }
11736
12130
  return { kind: "absent" };
11737
12131
  }
11738
- async function atomicWriteJson(targetPath, value) {
12132
+ async function atomicWriteString(targetPath, content) {
11739
12133
  const dir = path8.dirname(targetPath);
11740
12134
  await fsp7.mkdir(dir, { recursive: true });
11741
12135
  const tmp = `${targetPath}.tmp.${process.pid}.${randomBytes(4).toString("hex")}`;
11742
- const body = JSON.stringify(value, null, 2) + "\n";
11743
- await fsp7.writeFile(tmp, body, { mode: 384 });
12136
+ await fsp7.writeFile(tmp, content, { mode: 384 });
11744
12137
  try {
11745
12138
  await fsp7.rename(tmp, targetPath);
11746
12139
  } catch (err) {
@@ -11748,6 +12141,9 @@ async function atomicWriteJson(targetPath, value) {
11748
12141
  throw err;
11749
12142
  }
11750
12143
  }
12144
+ async function atomicWriteJson(targetPath, value) {
12145
+ await atomicWriteString(targetPath, JSON.stringify(value, null, 2) + "\n");
12146
+ }
11751
12147
  async function isProcessAlive(pid) {
11752
12148
  if (!Number.isInteger(pid) || pid <= 0) return false;
11753
12149
  if (pid === process.pid) return true;
@@ -11780,6 +12176,7 @@ async function probeHealthz(url) {
11780
12176
  try {
11781
12177
  const ctrl = new AbortController();
11782
12178
  const t = setTimeout(() => ctrl.abort(), 500);
12179
+ if (typeof t.unref === "function") t.unref();
11783
12180
  const res = await fetch(`${url}/healthz`, {
11784
12181
  signal: ctrl.signal,
11785
12182
  redirect: "manual"
@@ -12212,8 +12609,11 @@ var AdaptiveConcurrencyController = class {
12212
12609
  }
12213
12610
  notifyStateChange() {
12214
12611
  const state = this.getState();
12215
- for (const handler of this.stateChangeHandlers) {
12216
- handler(state);
12612
+ for (const handler of [...this.stateChangeHandlers]) {
12613
+ try {
12614
+ handler(state);
12615
+ } catch {
12616
+ }
12217
12617
  }
12218
12618
  }
12219
12619
  /**
@@ -12309,14 +12709,28 @@ var AgentMonitorService = class _AgentMonitorService {
12309
12709
  *
12310
12710
  * Entries already in the ring win: a live subagent's in-memory segment is
12311
12711
  * newer than its JSONL, which is only written when a segment closes.
12712
+ *
12713
+ * `only` restricts BOTH the disk scan and the returned set to a known list of
12714
+ * subagent ids. This is how a session gets ITS OWN subagents back: the
12715
+ * transcripts directory is shared by every session of the project, so the
12716
+ * unfiltered form hands each of four open tabs the union of all four tabs'
12717
+ * workers. The caller derives the list from the session's own journal
12718
+ * (`deriveSessionAgents`), which is the only record that says which agents
12719
+ * belong to which session.
12312
12720
  */
12313
- async loadSessionsFromDisk() {
12721
+ async loadSessionsFromDisk(only) {
12722
+ const wanted = only === void 0 ? void 0 : new Set(only);
12723
+ if (wanted?.size === 0) return [];
12314
12724
  let subagentIds;
12315
- try {
12316
- const dirents = await fs3.readdir(this._transcriptsDir, { withFileTypes: true });
12317
- subagentIds = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
12318
- } catch {
12319
- return this.getAllSessions();
12725
+ if (wanted) {
12726
+ subagentIds = [...wanted];
12727
+ } else {
12728
+ try {
12729
+ const dirents = await fs3.readdir(this._transcriptsDir, { withFileTypes: true });
12730
+ subagentIds = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
12731
+ } catch {
12732
+ return this.getAllSessions();
12733
+ }
12320
12734
  }
12321
12735
  for (const subagentId of subagentIds) {
12322
12736
  if (this._sessions.has(subagentId)) continue;
@@ -12334,7 +12748,8 @@ var AgentMonitorService = class _AgentMonitorService {
12334
12748
  transcript: transcript.slice(-this._maxEntries)
12335
12749
  });
12336
12750
  }
12337
- return this.getAllSessions();
12751
+ const all = this.getAllSessions();
12752
+ return wanted ? all.filter((session) => wanted.has(session.subagentId)) : all;
12338
12753
  }
12339
12754
  async _readTranscriptFile(subagentId) {
12340
12755
  const file = path9.join(this._transcriptsDir, subagentId, "transcript.jsonl");
@@ -12678,7 +13093,8 @@ var AgentMonitorService = class _AgentMonitorService {
12678
13093
  this._writeDrain = drain;
12679
13094
  }
12680
13095
  async _appendToFile(subagentId, line) {
12681
- const dir = path9.join(this._transcriptsDir, subagentId);
13096
+ const cleanId = subagentId.replace(/\\/g, "/");
13097
+ const dir = path9.join(this._transcriptsDir, cleanId);
12682
13098
  if (!this._ensuredDirs.has(dir)) {
12683
13099
  await fs3.mkdir(dir, { recursive: true });
12684
13100
  this._ensuredDirs.add(dir);
@@ -13416,6 +13832,10 @@ var KnowledgeGraph = class _KnowledgeGraph {
13416
13832
  this._trackSeq(parsed.node.id);
13417
13833
  this._addToIndex(parsed.node, this._indexKeys(parsed.node));
13418
13834
  } else {
13835
+ const oldNode = this.nodes.get(parsed.id);
13836
+ if (oldNode) {
13837
+ this._removeFromIndex(oldNode, this._indexKeys(oldNode));
13838
+ }
13419
13839
  this.nodes.set(parsed.id, parsed);
13420
13840
  this._trackSeq(parsed.id);
13421
13841
  this._addToIndex(parsed, this._indexKeys(parsed));
@@ -14570,7 +14990,6 @@ var ChangeManager = class {
14570
14990
  });
14571
14991
  this.appliedChanges.set(appliedChangeId, rollback.id);
14572
14992
  await this.graph.update(appliedChangeId, {
14573
- rolledBackAt: (/* @__PURE__ */ new Date()).toISOString(),
14574
14993
  rollbackReason: reason
14575
14994
  });
14576
14995
  this._emit("change:rollback_proposed", {
@@ -16332,6 +16751,8 @@ export {
16332
16751
  BrainTraceRecorder,
16333
16752
  readBrainTrace,
16334
16753
  CollaborationBus,
16754
+ collabPauseMiddleware,
16755
+ collabInjectMiddleware,
16335
16756
  recordFileAction,
16336
16757
  getLastAuthor,
16337
16758
  getFileHistory,
@@ -16350,10 +16771,10 @@ export {
16350
16771
  FleetSpawnBudgetError,
16351
16772
  FleetCostCapError,
16352
16773
  FleetTokenCapError,
16353
- setKanbanDispatch,
16354
- kanbanDispatch,
16355
16774
  setKanbanBoundaryOps,
16356
16775
  kanbanBoundaryOps,
16776
+ setKanbanDispatch,
16777
+ kanbanDispatch,
16357
16778
  parseTaskBoundary,
16358
16779
  renderTaskBoundaryBlock,
16359
16780
  composeBoundedTaskDescription,
@@ -16369,8 +16790,8 @@ export {
16369
16790
  makeCollabDebugTool,
16370
16791
  makeFleetEmitTool,
16371
16792
  makeWorkCompleteTool,
16372
- makeQualityGateTool,
16373
16793
  makeMutationTestTool,
16794
+ makeQualityGateTool,
16374
16795
  makeSpawnTool,
16375
16796
  makeKanbanQueueTool,
16376
16797
  DEFAULT_DIRECTOR_PREAMBLE,
@@ -16408,6 +16829,7 @@ export {
16408
16829
  parseMailboxAckInput,
16409
16830
  makeMailSendTool,
16410
16831
  makeMailInboxTool,
16832
+ makeSessionNoteTool,
16411
16833
  actionToAckInput,
16412
16834
  MAILBOX_HEALTH_DEFAULT_INTERVAL_MS,
16413
16835
  MAILBOX_HEALTH_DEFAULT_TIMEOUT_MS,
@@ -16418,9 +16840,9 @@ export {
16418
16840
  buildRecoveryAlert,
16419
16841
  validateWatchdogOptions,
16420
16842
  createMailboxHooks,
16843
+ MAILBOX_HTTP_MAX_AGE_CEILING_MS,
16421
16844
  authorizeMailboxBearerToken,
16422
16845
  authorizePersistedMailboxCredential,
16423
- MAILBOX_HTTP_MAX_AGE_CEILING_MS,
16424
16846
  MAILBOX_HTTP_RATE_LIMIT_PER_MINUTE,
16425
16847
  MAILBOX_HTTP_RATE_LIMIT_WINDOW_MS,
16426
16848
  MailboxHttpRateLimiter,
@@ -16445,8 +16867,6 @@ export {
16445
16867
  release,
16446
16868
  readLiveLock,
16447
16869
  startTechStackConsumer,
16448
- collabPauseMiddleware,
16449
- collabInjectMiddleware,
16450
16870
  AdaptiveConcurrencyController,
16451
16871
  AgentMonitorService,
16452
16872
  createAgentMonitorService,
@@ -16461,4 +16881,4 @@ export {
16461
16881
  AgentStatusTracker,
16462
16882
  FleetNotifier
16463
16883
  };
16464
- //# sourceMappingURL=chunk-FW5LDDMJ.js.map
16884
+ //# sourceMappingURL=chunk-45WI72ZB.js.map