@iowarp/clio-coder 0.3.4 → 0.3.7

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 (376) hide show
  1. package/CHANGELOG.md +65 -2
  2. package/CONTRIBUTING.md +6 -6
  3. package/README.md +16 -5
  4. package/dist/{acp-S5R4RR5B.js → acp-SK4MD6MM.js} +11 -11
  5. package/dist/{agents-P6DMMVZY.js → agents-2FN2K6ME.js} +33 -26
  6. package/dist/assets/codewiki.json +1 -1
  7. package/dist/{auth-2XCZLPKS.js → auth-QIYZWM5I.js} +15 -15
  8. package/dist/{chunk-EKMEHE4H.js → chunk-33YXPOE3.js} +2 -3
  9. package/dist/{chunk-VAWWTKDP.js → chunk-3HAPLH5M.js} +11 -11
  10. package/dist/{chunk-YCWGATWI.js → chunk-465YSENW.js} +3 -3
  11. package/dist/{chunk-4OC57DA6.js → chunk-4DGYLA73.js} +53 -2
  12. package/dist/{chunk-UZHIZC5S.js → chunk-4DWFMQDR.js} +61 -76
  13. package/dist/{chunk-ZWMF7253.js → chunk-5C3AQNDW.js} +328 -9
  14. package/dist/{chunk-22NAGB7X.js → chunk-5C77SEEY.js} +5 -94
  15. package/dist/{chunk-WPQLXFOZ.js → chunk-5FR74PWO.js} +3 -2
  16. package/dist/{chunk-35MKKU5R.js → chunk-5UJ6ECTS.js} +18 -10
  17. package/dist/{chunk-BRXQQJFP.js → chunk-6M7VS3J3.js} +571 -50
  18. package/dist/{chunk-QQK64KLB.js → chunk-6TUKSZVF.js} +141 -23
  19. package/dist/{chunk-N4CZJQRK.js → chunk-AB4XIIVB.js} +8 -6
  20. package/dist/{chunk-KRPY7NTG.js → chunk-BMWK7ZIZ.js} +14 -20
  21. package/dist/{chunk-4BPJXDWC.js → chunk-C4JBQ5SR.js} +30 -14
  22. package/dist/{chunk-ZYKPLLNQ.js → chunk-CEYBNUGC.js} +821 -83
  23. package/dist/{chunk-VEZEGCGW.js → chunk-D4MDIG46.js} +20 -18
  24. package/dist/chunk-DJNLUABN.js +843 -0
  25. package/dist/{chunk-BP4OYD6A.js → chunk-DMD2AGVS.js} +21 -2
  26. package/dist/{chunk-KOHPCX4K.js → chunk-DOOEX22V.js} +2 -2
  27. package/dist/chunk-DQA7QLMD.js +123 -0
  28. package/dist/chunk-DR52UMZW.js +21 -0
  29. package/dist/{chunk-3HZ5RWN2.js → chunk-EBEFWSGL.js} +9 -7
  30. package/dist/{chunk-EDRHSCIE.js → chunk-EELBMBT6.js} +128 -13
  31. package/dist/{chunk-HV5X7OR2.js → chunk-EOOQZZDE.js} +16 -14
  32. package/dist/{chunk-WR67VIZY.js → chunk-FOT2FX5J.js} +63 -5
  33. package/dist/{chunk-BPGS2WCQ.js → chunk-GEYXPTRF.js} +2 -1
  34. package/dist/{chunk-FYYLNIL5.js → chunk-GH5622CP.js} +2 -2
  35. package/dist/{chunk-BEY543CS.js → chunk-GOXNB3AO.js} +5 -2
  36. package/dist/chunk-GWS3VEIW.js +195 -0
  37. package/dist/{chunk-G4BMMOKF.js → chunk-HVDIIIQW.js} +2 -2
  38. package/dist/chunk-HWUFFB6L.js +83 -0
  39. package/dist/{chunk-X6COSD2O.js → chunk-J7PIKKWC.js} +8 -436
  40. package/dist/{chunk-NILBFAPG.js → chunk-JNXPYBB4.js} +2 -2
  41. package/dist/{chunk-4VP4KH3K.js → chunk-JRIO5UD2.js} +4 -4
  42. package/dist/{chunk-K6WL7QZT.js → chunk-JTSEDYVQ.js} +7 -7
  43. package/dist/{chunk-QKMUKYO7.js → chunk-KCMKRQX4.js} +236 -84
  44. package/dist/chunk-KZ2H5X4G.js +1026 -0
  45. package/dist/{chunk-A2GZF7DC.js → chunk-LADCF22A.js} +13 -13
  46. package/dist/chunk-LCGCVYZ4.js +57 -0
  47. package/dist/chunk-M4AKACEO.js +382 -0
  48. package/dist/{chunk-POHLU5DW.js → chunk-M6L6IDJG.js} +3 -3
  49. package/dist/{chunk-4JUF2NNX.js → chunk-MXI6J5JF.js} +7 -7
  50. package/dist/{chunk-X4RCMKVQ.js → chunk-NDINPTJ4.js} +2 -2
  51. package/dist/{chunk-TTNYS3EA.js → chunk-OB5HIGJY.js} +1 -1
  52. package/dist/{chunk-7RXG6QRZ.js → chunk-OBMAI2DP.js} +61 -840
  53. package/dist/{chunk-5M54SPOL.js → chunk-ODFEOB4F.js} +161 -5
  54. package/dist/chunk-PD3MESLB.js +242 -0
  55. package/dist/{chunk-ED4KHGC3.js → chunk-PPAMZ32Z.js} +9 -2
  56. package/dist/{chunk-VMNQ6OZA.js → chunk-QCTRSGHQ.js} +963 -786
  57. package/dist/chunk-RVG5JXAL.js +41 -0
  58. package/dist/{chunk-RD5U66HV.js → chunk-SROCI7ZU.js} +7 -7
  59. package/dist/{chunk-MFFY33HR.js → chunk-THKY7CD7.js} +466 -205
  60. package/dist/{chunk-34475P3I.js → chunk-TSHXZTOQ.js} +5 -4
  61. package/dist/{chunk-PCZJO5TI.js → chunk-UFQ3F4FW.js} +13 -178
  62. package/dist/{chunk-AD2SYQYC.js → chunk-UHXRNZ2J.js} +121 -3
  63. package/dist/chunk-UND3GU2L.js +103 -0
  64. package/dist/{chunk-QQL5RT5M.js → chunk-UUANF5CR.js} +2323 -2114
  65. package/dist/{chunk-VJWL6YS5.js → chunk-UUVG37B4.js} +2 -2
  66. package/dist/chunk-UVDSQ6LW.js +472 -0
  67. package/dist/{chunk-QWU7ZBO7.js → chunk-VQNODYQ4.js} +215 -56
  68. package/dist/chunk-VREKEFLL.js +37 -0
  69. package/dist/{chunk-2TZWSW76.js → chunk-WHGPSPT5.js} +2 -2
  70. package/dist/{chunk-TW3WDMVS.js → chunk-WHJYKASB.js} +2 -2
  71. package/dist/{chunk-MEQ45TQ4.js → chunk-WJHBC77E.js} +21 -7
  72. package/dist/{chunk-HXG4IURW.js → chunk-X2KV5FXT.js} +2 -2
  73. package/dist/{chunk-YHZX5GEU.js → chunk-XAKHZX5N.js} +2 -2
  74. package/dist/{chunk-2LZI5CAG.js → chunk-XEGB6BCN.js} +228 -36
  75. package/dist/{chunk-E25LMLRW.js → chunk-YD734TPH.js} +2 -2
  76. package/dist/{verifiers-4UUM6TEE.js → chunk-YTYFXUI3.js} +121 -372
  77. package/dist/{chunk-3JLKSKD7.js → chunk-ZGH7FGS5.js} +17 -7
  78. package/dist/{chunk-VSNATDE6.js → chunk-ZZMN5OM4.js} +2 -2
  79. package/dist/cli/index.js +34 -32
  80. package/dist/{clio-J5JIOIDS.js → clio-WBVQEBKO.js} +7 -7
  81. package/dist/{code-nav-AXCXSBHX.js → code-nav-FGGFIE7L.js} +7 -7
  82. package/dist/codewiki/build-worker.js +4 -4
  83. package/dist/{components-KELWS457.js → components-F7OEATSO.js} +5 -5
  84. package/dist/{config-OEBMIN2U.js → config-TRBL3RCF.js} +48 -41
  85. package/dist/{configure-PUQOSIXQ.js → configure-OLCVPHNM.js} +17 -17
  86. package/dist/{context-URSXPBCK.js → context-MJIJ6GOX.js} +12 -12
  87. package/dist/{context-EKDCKUUZ.js → context-WFPKQSM6.js} +26 -9
  88. package/dist/{context-MGSE4Z2T.js → context-XEWE3MOJ.js} +44 -37
  89. package/dist/{context-clear-KDAJRNUK.js → context-clear-KNOS2JPB.js} +44 -37
  90. package/dist/{context-index-BZ4UYMTC.js → context-index-SSR5ECNE.js} +3 -3
  91. package/dist/{context-working-set-SBKMPPI2.js → context-working-set-EUXAZI6N.js} +14 -13
  92. package/dist/{dispatch-runner-MSWN72NK.js → dispatch-runner-B7MTOVKL.js} +321 -60
  93. package/dist/{docs-2C2LTVT2.js → docs-FLJTIDSE.js} +5 -5
  94. package/dist/{doctor-7BSE27PJ.js → doctor-RN4YKO2X.js} +15 -15
  95. package/dist/{eval-IZGDOO4H.js → eval-RUBJVSNQ.js} +52 -236
  96. package/dist/{evidence-SR7WXB5B.js → evidence-JZNBUOQZ.js} +39 -33
  97. package/dist/{evolve-K7VE2CBX.js → evolve-FJVC4KKI.js} +39 -33
  98. package/dist/{extensions-QVDOHDGJ.js → extensions-IQL36S7K.js} +5 -5
  99. package/dist/{fleet-7XMJNQNF.js → fleet-BDKYJFCP.js} +243 -370
  100. package/dist/fleet-commands-ZFIWZSB3.js +70 -0
  101. package/dist/fleet-graph-Y6HPXIVF.js +125 -0
  102. package/dist/fleet-new-RDVJLHHH.js +48 -0
  103. package/dist/{fleet-preflight-AQNAH644.js → fleet-preflight-BHSNPBMH.js} +2 -2
  104. package/dist/fleet-validate-BIYREGIK.js +79 -0
  105. package/dist/{init-JGNPAYXT.js → init-LQUB5COQ.js} +57 -48
  106. package/dist/library-NJAHIGG4.js +217 -0
  107. package/dist/memory-OG6HOYKM.js +472 -0
  108. package/dist/{models-ZMMLFJNN.js → models-5ZG5XY7J.js} +23 -22
  109. package/dist/{monitor-2F3T5KHP.js → monitor-TJ7AMTGB.js} +69 -35
  110. package/dist/{orchestrator-ORHT43JB.js → orchestrator-WZYB54DM.js} +4868 -1189
  111. package/dist/{paths-UXLN5YYZ.js → paths-XUC7GS6E.js} +5 -5
  112. package/dist/{reset-NXGTYNUO.js → reset-PXQT45IY.js} +8 -8
  113. package/dist/{run-RF4WJGMT.js → run-FQ74YF62.js} +82 -62
  114. package/dist/{share-UT3W6E4M.js → share-FW7SVCL3.js} +34 -10
  115. package/dist/{skills-PSACKC5Q.js → skills-7E7IRB3R.js} +25 -9
  116. package/dist/{skills-eval-WJSI55RZ.js → skills-eval-LI75W6OK.js} +43 -35
  117. package/dist/{targets-PIIRAOYS.js → targets-4CIFKCTW.js} +27 -24
  118. package/dist/{terminal-lease-ULWXWNVY.js → terminal-lease-WUZY7ZV5.js} +5 -4
  119. package/dist/{uninstall-FZCQCDKC.js → uninstall-7FV7IP4E.js} +5 -5
  120. package/dist/{upgrade-346TZ6AV.js → upgrade-K2HVIVMQ.js} +21 -20
  121. package/dist/{usage-6KKXR32N.js → usage-GTZELZQX.js} +159 -59
  122. package/dist/verifiers-RLAHT27O.js +336 -0
  123. package/dist/{verify-X5HDROLA.js → verify-BX3BRKH5.js} +7 -6
  124. package/dist/{wiki-generate-7STOCIFZ.js → wiki-generate-ASIFASCN.js} +58 -48
  125. package/dist/worker/entry.js +98 -84
  126. package/dist/{workspace-G4ZWUIPR.js → workspace-ZJ6BFM3Q.js} +4 -4
  127. package/docs/README.md +4 -3
  128. package/docs/acp.md +1 -1
  129. package/docs/alcf-provider.md +1 -1
  130. package/docs/architecture.md +2 -2
  131. package/docs/artifact-placement.md +1 -2
  132. package/docs/artifact-versions.md +10 -6
  133. package/docs/built-in-agents.md +26 -2
  134. package/docs/capacity-and-scheduling.md +1 -1
  135. package/docs/commands-and-modes.md +90 -8
  136. package/docs/configuration-and-targets.md +90 -2
  137. package/docs/context-engine.md +4 -2
  138. package/docs/context-working-set.md +4 -4
  139. package/docs/development-pipeline.md +1 -1
  140. package/docs/dispatch-architecture-rationale.md +1 -1
  141. package/docs/documentation-coverage.md +4 -4
  142. package/docs/documentation-guide.md +4 -4
  143. package/docs/eval-runner.md +1 -1
  144. package/docs/evals-internal.md +4 -45
  145. package/docs/evidence-and-memory.md +70 -10
  146. package/docs/evolution.md +1 -1
  147. package/docs/exit-codes-and-output.md +4 -1
  148. package/docs/extensions-and-sharing.md +6 -2
  149. package/docs/fleet-demo-runbook.md +2 -2
  150. package/docs/fleet-dispatch.md +224 -11
  151. package/docs/git-commit-provenance.md +2 -2
  152. package/docs/glossary.md +1 -1
  153. package/docs/installation-and-lifecycle.md +2 -2
  154. package/docs/middleware-and-components.md +20 -2
  155. package/docs/model-catalog.md +1 -1
  156. package/docs/observability.md +55 -8
  157. package/docs/proactive-memory.md +26 -16
  158. package/docs/prompt-envelope-and-tools.md +4 -2
  159. package/docs/provider-adapter-cookbook.md +1 -1
  160. package/docs/release-cut-checklist.md +83 -65
  161. package/docs/resource-library.md +59 -0
  162. package/docs/safety-model.md +29 -7
  163. package/docs/scientific-validation.md +3 -3
  164. package/docs/session-lifecycle.md +37 -1
  165. package/docs/skills-marketplace.md +16 -3
  166. package/docs/tool-usage.md +14 -7
  167. package/docs/trace-store.md +1 -1
  168. package/docs/troubleshooting.md +1 -1
  169. package/docs/tui-design.md +38 -4
  170. package/docs/worker-dispatch-mechanics.md +3 -3
  171. package/package.json +7 -4
  172. package/src/cli/agents.ts +2 -3
  173. package/src/cli/argv.ts +14 -1
  174. package/src/cli/fleet-commands.ts +37 -0
  175. package/src/cli/fleet-graph.ts +102 -0
  176. package/src/cli/fleet-new.ts +36 -0
  177. package/src/cli/fleet-preflight.ts +121 -0
  178. package/src/cli/fleet-validate.ts +30 -0
  179. package/src/cli/fleet.ts +188 -335
  180. package/src/cli/index.ts +4 -2
  181. package/src/cli/library.ts +190 -0
  182. package/src/cli/memory.ts +272 -10
  183. package/src/cli/modes/json-stream.ts +2 -2
  184. package/src/cli/modes/print.ts +12 -1
  185. package/src/cli/run.ts +22 -2
  186. package/src/cli/share.ts +13 -1
  187. package/src/cli/targets.ts +12 -3
  188. package/src/cli/usage.ts +160 -20
  189. package/src/core/bus-events.ts +7 -0
  190. package/src/core/commit-attribution.ts +4 -4
  191. package/src/core/config.ts +130 -0
  192. package/src/core/defaults.ts +81 -0
  193. package/src/core/response-model-id.ts +134 -0
  194. package/src/core/toml.ts +62 -0
  195. package/src/core/workspace-files.ts +0 -1
  196. package/src/domains/agents/builtins/architect.md +2 -1
  197. package/src/domains/agents/builtins/oracle.md +33 -0
  198. package/src/domains/agents/catalog.ts +18 -5
  199. package/src/domains/agents/fleet-contract.ts +278 -16
  200. package/src/domains/agents/index.ts +14 -0
  201. package/src/domains/agents/recipe.ts +54 -14
  202. package/src/domains/agents/result-contract.ts +242 -5
  203. package/src/domains/config/classify.ts +4 -0
  204. package/src/domains/context/bootstrap.ts +36 -27
  205. package/src/domains/context/project-metadata.ts +19 -63
  206. package/src/domains/context/prompt-context.ts +8 -0
  207. package/src/domains/context/working-set/policies/index.ts +3 -4
  208. package/src/domains/dispatch/active-route-planner.ts +14 -0
  209. package/src/domains/dispatch/backoff.ts +2 -1
  210. package/src/domains/dispatch/budget-envelope.ts +396 -0
  211. package/src/domains/dispatch/capability-match.ts +1 -0
  212. package/src/domains/dispatch/checkout-writer-lease.ts +175 -0
  213. package/src/domains/dispatch/contract.ts +36 -0
  214. package/src/domains/dispatch/delegation-plan.ts +167 -0
  215. package/src/domains/dispatch/execution-plan.ts +76 -5
  216. package/src/domains/dispatch/execution-role.ts +3 -1
  217. package/src/domains/dispatch/execution-scheduler.ts +183 -67
  218. package/src/domains/dispatch/extension.ts +339 -36
  219. package/src/domains/dispatch/fleet-gate.ts +14 -0
  220. package/src/domains/dispatch/fleet-plan.ts +63 -3
  221. package/src/domains/dispatch/fleet-run.ts +737 -0
  222. package/src/domains/dispatch/gate-role-prompts.ts +9 -0
  223. package/src/domains/dispatch/host-verification.ts +178 -0
  224. package/src/domains/dispatch/index.ts +38 -0
  225. package/src/domains/dispatch/intent.ts +159 -0
  226. package/src/domains/dispatch/orphan-recovery.ts +1 -0
  227. package/src/domains/dispatch/receipt-integrity.ts +12 -4
  228. package/src/domains/dispatch/state.ts +37 -3
  229. package/src/domains/dispatch/types.ts +61 -9
  230. package/src/domains/dispatch/validation.ts +80 -6
  231. package/src/domains/dispatch/worker-spawn.ts +14 -3
  232. package/src/domains/eval/metrics/evidence.ts +0 -116
  233. package/src/domains/eval/metrics/invariants.ts +1 -1
  234. package/src/domains/eval/runners/clio-run.ts +1 -10
  235. package/src/domains/eval/runners/external-command.ts +2 -29
  236. package/src/domains/eval/schema/suite.ts +0 -7
  237. package/src/domains/eval/suites/run.ts +1 -7
  238. package/src/domains/evidence/trust-status.ts +10 -1
  239. package/src/domains/memory/index.ts +22 -0
  240. package/src/domains/memory/operations.ts +58 -1
  241. package/src/domains/memory/promotion.ts +281 -0
  242. package/src/domains/memory/prompt-section.ts +25 -5
  243. package/src/domains/memory/proposal.ts +51 -7
  244. package/src/domains/memory/task-bank.ts +3 -2
  245. package/src/domains/memory/task-memory-handoff.ts +181 -24
  246. package/src/domains/memory/task-memory-policy.ts +3 -1
  247. package/src/domains/memory/types.ts +37 -0
  248. package/src/domains/memory/validate.ts +178 -0
  249. package/src/domains/middleware/index.ts +15 -0
  250. package/src/domains/middleware/memory-intervention.ts +35 -25
  251. package/src/domains/middleware/runtime.ts +6 -0
  252. package/src/domains/middleware/skills-reminder.ts +19 -4
  253. package/src/domains/middleware/stalled-turn.ts +43 -1
  254. package/src/domains/middleware/types.ts +10 -0
  255. package/src/domains/middleware/watchdog.ts +281 -0
  256. package/src/domains/observability/contract.ts +9 -2
  257. package/src/domains/observability/cost.ts +31 -4
  258. package/src/domains/observability/extension.ts +2 -2
  259. package/src/domains/observability/index.ts +10 -0
  260. package/src/domains/observability/out-of-turn-usage.ts +223 -0
  261. package/src/domains/providers/index.ts +3 -0
  262. package/src/domains/providers/model-discovery.ts +9 -0
  263. package/src/domains/providers/runtime-resolution.ts +38 -1
  264. package/src/domains/providers/runtimes/common/probe-helpers.ts +97 -16
  265. package/src/domains/providers/types/context-window-slots.ts +18 -0
  266. package/src/domains/providers/types/runtime-descriptor.ts +3 -1
  267. package/src/domains/resources/index.ts +20 -0
  268. package/src/domains/resources/library.ts +326 -0
  269. package/src/domains/resources/skills/marketplace.ts +37 -12
  270. package/src/domains/safety/call-target.ts +211 -14
  271. package/src/domains/safety/decision-presentation.ts +268 -0
  272. package/src/domains/safety/redaction.ts +73 -0
  273. package/src/domains/session/context-ledger.ts +10 -1
  274. package/src/domains/session/decision-board.ts +4 -0
  275. package/src/domains/session/entries.ts +3 -0
  276. package/src/domains/session/handoff.ts +629 -0
  277. package/src/domains/session/history.ts +68 -19
  278. package/src/domains/session/usage.ts +24 -7
  279. package/src/domains/share/archive.ts +67 -2
  280. package/src/engine/acp/event-mapper.ts +7 -0
  281. package/src/engine/acp/server.ts +29 -2
  282. package/src/engine/apis/lmstudio.ts +25 -4
  283. package/src/engine/apis/openai-completions.ts +147 -22
  284. package/src/engine/claude/sdk-runtime.ts +8 -2
  285. package/src/engine/claude/tool-safety.ts +13 -0
  286. package/src/engine/loop-guard.ts +27 -3
  287. package/src/engine/worker-events.ts +4 -3
  288. package/src/engine/worker-runtime.ts +59 -54
  289. package/src/entry/orchestrator.ts +55 -1
  290. package/src/interactive/bus-notices.ts +26 -0
  291. package/src/interactive/chat-loop-messages.ts +22 -0
  292. package/src/interactive/chat-loop.ts +248 -1
  293. package/src/interactive/chat-renderer.ts +41 -3
  294. package/src/interactive/clio-editor.ts +44 -7
  295. package/src/interactive/context-overlay.ts +43 -5
  296. package/src/interactive/cost-overlay.ts +70 -11
  297. package/src/interactive/council-dispatch.ts +30 -0
  298. package/src/interactive/council-grid.ts +213 -0
  299. package/src/interactive/council.ts +99 -0
  300. package/src/interactive/dispatch-board.ts +471 -50
  301. package/src/interactive/fleet-run-preview.ts +307 -0
  302. package/src/interactive/footer/notifications.ts +219 -0
  303. package/src/interactive/footer/widgets.ts +13 -0
  304. package/src/interactive/handoff-round.ts +56 -0
  305. package/src/interactive/interactive-application.ts +49 -2
  306. package/src/interactive/interactive-event-projection.ts +9 -1
  307. package/src/interactive/interactive-input-runtime.ts +11 -1
  308. package/src/interactive/interactive-presentation.ts +11 -1
  309. package/src/interactive/interactive-slash-runtime.ts +52 -2
  310. package/src/interactive/interactive-subscriptions.ts +14 -2
  311. package/src/interactive/memory-overlay.ts +89 -4
  312. package/src/interactive/oracle.ts +179 -0
  313. package/src/interactive/overlay-ask-user-lifecycle.ts +7 -1
  314. package/src/interactive/overlay-frame.ts +5 -2
  315. package/src/interactive/overlay-general-openers.ts +230 -2
  316. package/src/interactive/overlay-key-routing.ts +58 -2
  317. package/src/interactive/overlay-lifecycle.ts +52 -5
  318. package/src/interactive/overlay-permission-lifecycle.ts +33 -8
  319. package/src/interactive/overlay-resource-openers.ts +11 -3
  320. package/src/interactive/overlay-session-lifecycle.ts +234 -2
  321. package/src/interactive/overlay-transitions.ts +11 -0
  322. package/src/interactive/overlays/ask-user.ts +74 -30
  323. package/src/interactive/overlays/decisions.ts +3 -1
  324. package/src/interactive/overlays/fleet-run-approval.ts +208 -0
  325. package/src/interactive/overlays/handoff-review.ts +185 -0
  326. package/src/interactive/overlays/library-install-confirm.ts +151 -0
  327. package/src/interactive/overlays/list-overlay.ts +168 -2
  328. package/src/interactive/overlays/settings.ts +101 -4
  329. package/src/interactive/overlays/side-question.ts +139 -0
  330. package/src/interactive/overlays/skills-hub.ts +401 -15
  331. package/src/interactive/permission-hint.ts +35 -0
  332. package/src/interactive/permission-overlay.ts +95 -45
  333. package/src/interactive/renderers/tool-execution.ts +19 -49
  334. package/src/interactive/session-last-turn.ts +8 -1
  335. package/src/interactive/session-usage-reseed.ts +36 -10
  336. package/src/interactive/side-question.ts +171 -0
  337. package/src/interactive/slash-commands.ts +434 -7
  338. package/src/interactive/slash-spec.ts +19 -6
  339. package/src/interactive/status/summary.ts +5 -0
  340. package/src/interactive/status/types.ts +5 -0
  341. package/src/interactive/terminal-lease.ts +1 -0
  342. package/src/interactive/theme/tokens.ts +30 -0
  343. package/src/interactive/turn-context.ts +96 -23
  344. package/src/interactive/turn-middleware.ts +16 -1
  345. package/src/interactive/turn-runtime.ts +37 -8
  346. package/src/interactive/turn-state.ts +3 -0
  347. package/src/interactive/watchdog-run.ts +75 -0
  348. package/src/interactive/worker-progress.ts +440 -0
  349. package/src/interactive/worker-share.ts +56 -1
  350. package/src/interactive/worker-stream.ts +58 -110
  351. package/src/tools/agent-tools.ts +28 -3
  352. package/src/tools/ask-user.ts +21 -1
  353. package/src/tools/bootstrap.ts +3 -0
  354. package/src/tools/compete-worktrees.ts +13 -79
  355. package/src/tools/context/index.ts +2 -2
  356. package/src/tools/dispatch-admission.ts +242 -8
  357. package/src/tools/dispatch-arguments.ts +65 -1
  358. package/src/tools/dispatch-event-text.ts +19 -0
  359. package/src/tools/dispatch-plan.ts +136 -6
  360. package/src/tools/dispatch-runner.ts +319 -13
  361. package/src/tools/dispatch-types.ts +20 -1
  362. package/src/tools/dispatch.ts +96 -3
  363. package/src/tools/monitor.ts +31 -0
  364. package/src/tools/profiles.ts +18 -4
  365. package/src/tools/registry.ts +15 -5
  366. package/src/tools/result-disposition.ts +156 -0
  367. package/src/tools/result-shaping.ts +59 -1
  368. package/src/tools/task-worktree.ts +238 -0
  369. package/src/tools/verify/authoring.ts +116 -55
  370. package/src/tools/verify/scripts.ts +62 -0
  371. package/src/tools/worker-evidence.ts +21 -1
  372. package/src/worker/spec-contract.ts +44 -3
  373. package/dist/chunk-EFADSJET.js +0 -18
  374. package/dist/chunk-HC4CLZ2Y.js +0 -68
  375. package/dist/memory-4ALKDJ4Q.js +0 -246
  376. package/src/domains/eval/metrics/chaos-stream.ts +0 -93
@@ -9,6 +9,14 @@ import {
9
9
  import type { SafeEventBus } from "../core/event-bus.js";
10
10
  import { rawDurationMs } from "../core/timers.js";
11
11
  import type { AgentAudience } from "../domains/agents/spec.js";
12
+ import {
13
+ cloneRunToolBudgetEnvelope,
14
+ formatBudgetPolicy,
15
+ formatBudgetReasons,
16
+ formatBudgetRequest,
17
+ formatEffectiveBudget,
18
+ type RunToolBudgetEnvelope,
19
+ } from "../domains/dispatch/budget-envelope.js";
12
20
  import type { DispatchSnapshot } from "../domains/dispatch/contract.js";
13
21
  import {
14
22
  type DispatchRequestOrigin,
@@ -25,8 +33,15 @@ import {
25
33
  } from "../domains/observability/index.js";
26
34
  import type { CostProvenance } from "../domains/providers/index.js";
27
35
  import { sanitizeCallTargetText } from "../domains/safety/call-target.js";
28
- import { type Component, truncateToWidth, visibleWidth } from "../engine/tui.js";
36
+ import { type Component, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../engine/tui.js";
29
37
  import { formatWorkerContextMeter } from "./context-meter.js";
38
+ import {
39
+ COUNCIL_SYNTHESIS_LABEL,
40
+ type CouncilGroupView,
41
+ type CouncilMemberView,
42
+ councilGroupBody,
43
+ councilIslandLines,
44
+ } from "./council-grid.js";
30
45
  import { formatFooterTokens } from "./footer-panel.js";
31
46
  import {
32
47
  type ClioTheme,
@@ -42,6 +57,13 @@ import {
42
57
  screenTitle,
43
58
  spinnerFrame,
44
59
  } from "./theme/index.js";
60
+ import {
61
+ createWorkerProgressFold,
62
+ type WorkerAction,
63
+ type WorkerProgressFold,
64
+ type WorkerProgressSnapshot,
65
+ } from "./worker-progress.js";
66
+ import type { WorkerReceiptReader } from "./worker-stream.js";
45
67
 
46
68
  export type DispatchBoardStatus =
47
69
  | Extract<RunStatus, "running" | "completed" | "failed" | "stale" | "dead">
@@ -72,6 +94,8 @@ export interface DispatchBoardRow {
72
94
  wireModelId: string;
73
95
  /** Sanitized, one-line summary of the task assigned to this run. */
74
96
  taskSummary?: string;
97
+ /** Immutable budget admission provenance for this run. */
98
+ budget?: RunToolBudgetEnvelope;
75
99
  status: DispatchBoardStatus;
76
100
  elapsedMs: number;
77
101
  tokenCount: number;
@@ -86,6 +110,9 @@ export interface DispatchBoardRow {
86
110
  node?: string;
87
111
  /** Review/compete gate badge (role + cycle). */
88
112
  gate?: { role: string; cycle: number };
113
+ council?: { group: string; label: string; color?: string; round: number };
114
+ /** Orchestrator-executed declared verification status. */
115
+ hostVerification?: "verified" | "rejected" | "skipped";
89
116
  /** Dead-node failover hops recorded on this run's chain. */
90
117
  rerouteCount?: number;
91
118
  /** Assignment retry attempts observed on the assignment event stream. */
@@ -94,10 +121,17 @@ export interface DispatchBoardRow {
94
121
  contextWindow?: number;
95
122
  /** Last assistant message's input+cacheRead+output: current context occupancy. */
96
123
  lastContextTokens?: number;
97
- /** Tool currently executing in the worker; null between calls. */
124
+ /** Tool currently executing in the worker; null between calls. Projected from `progress`. */
98
125
  currentTool?: string | null;
99
- /** Recently finished tools, newest first, bounded. */
126
+ /** Recently finished tools, newest first, bounded. Projected from `progress`. */
100
127
  recentTools?: ReadonlyArray<string>;
128
+ /**
129
+ * The canonical worker-progress projection for this run: the bounded answer
130
+ * tail, the phase, and the redacted action descriptors. The same projection
131
+ * the transcript's worker block reads, so opening a run here shows what the
132
+ * worker is doing rather than a richer spinner.
133
+ */
134
+ progress?: WorkerProgressSnapshot;
101
135
  /**
102
136
  * Id of the receipt sealed for this run (`receipts/<runId>.json`, the id
103
137
  * `clio-coder trace` takes). Set only once a terminal dispatch event has
@@ -110,8 +144,39 @@ export interface DispatchBoardRow {
110
144
  retry?: DispatchRetryPresentation;
111
145
  /** Most recent worker delivery acknowledgement for an operator steer. */
112
146
  steerAcknowledgement?: DispatchSteerAcknowledgement;
147
+ /**
148
+ * Where this run sits in a fleet plan: the wave the step was scheduled in
149
+ * and the step id it executes. Present only for runs a fleet plan
150
+ * dispatched, so a `/run`, a delegation, or a dispatch-tool run leaves the
151
+ * board's phase column empty rather than claiming a position it has none of.
152
+ */
153
+ phase?: DispatchBoardPhase;
113
154
  }
114
155
 
156
+ /** One fleet plan position: the wave index and the step the run executes. */
157
+ export interface DispatchBoardPhase {
158
+ wave: number;
159
+ stepId: string;
160
+ }
161
+
162
+ /**
163
+ * The phase cell, or null for a run that is not a fleet step. Fixed width so
164
+ * the column lines up across rows; a long step id is clipped with a marker
165
+ * rather than pushing the row's other facts out of position.
166
+ */
167
+ export function formatDispatchPhaseCell(
168
+ phase: DispatchBoardPhase | undefined,
169
+ width = PHASE_COLUMN_WIDTH,
170
+ ): string | null {
171
+ if (phase === undefined) return null;
172
+ return truncateToWidth(`w${phase.wave} ${phase.stepId}`, Math.max(1, width), "…", false);
173
+ }
174
+
175
+ /** Columns the compact island reserves for the phase cell when it can afford them. */
176
+ export const PHASE_COLUMN_WIDTH = 12;
177
+ /** Below this much room for the agent label, the compact row drops the column entirely. */
178
+ const PHASE_COLUMN_MIN_LABEL_WIDTH = 8;
179
+
115
180
  /** Live HTTP/SDK rows are the only rows whose runtime can consume operator guidance. */
116
181
  export function isDispatchBoardRowSteerable(row: DispatchBoardRow): boolean {
117
182
  return (
@@ -126,7 +191,10 @@ export function isDispatchBoardRowCancellable(row: DispatchBoardRow): boolean {
126
191
  }
127
192
 
128
193
  interface DispatchBoardEntry
129
- extends Omit<DispatchBoardRow, "elapsedMs" | "recentTools" | "lastContextTokens" | "currentTool" | "retry"> {
194
+ extends Omit<
195
+ DispatchBoardRow,
196
+ "elapsedMs" | "recentTools" | "lastContextTokens" | "currentTool" | "retry" | "progress"
197
+ > {
130
198
  sequence: number;
131
199
  enqueuedAtMs: number;
132
200
  startedAtMs: number | null;
@@ -139,15 +207,19 @@ interface DispatchBoardEntry
139
207
  finishedAtMs: number | null;
140
208
  durationMs: number | null;
141
209
  lastContextTokens: number;
142
- currentTool: string | null;
143
- recentTools: string[];
210
+ /**
211
+ * The one fold that reads this run's worker events. Tool activity and the
212
+ * answer tail are its output, so the board no longer interprets the stream
213
+ * alongside the transcript.
214
+ */
215
+ progress: WorkerProgressFold;
144
216
  retry?: DispatchRetryPresentation;
145
217
  }
146
218
 
147
- /** Bound on the recent-tool trail rendered on a card. */
148
- const RECENT_TOOL_TRAIL_LIMIT = 4;
149
219
  /** Keep raw task text out of long-lived TUI rows and bound hostile/user-sized input. */
150
220
  const TASK_SUMMARY_MAX_WIDTH = 240;
221
+ /** Rows of worker prose an expanded card shows before it defers to `/view`. */
222
+ const WORKER_PROGRESS_CARD_ROWS = 6;
151
223
 
152
224
  interface WorkerEventShape {
153
225
  type?: unknown;
@@ -433,11 +505,92 @@ function retryCountdown(retry: DispatchRetryPresentation, now = Date.now()): str
433
505
  return remainingMs === 0 ? "now" : `in ${formatCompactMs(remainingMs)}`;
434
506
  }
435
507
 
508
+ /**
509
+ * One action as a phrase: the tool name, then the verb and object of its
510
+ * redacted descriptor. The descriptor was bounded and scrubbed at the worker
511
+ * seam, so this composes text and never inspects an argument.
512
+ */
513
+ function actionPhrase(action: WorkerAction): string {
514
+ const descriptor = action.descriptor;
515
+ if (descriptor === undefined) return action.tool;
516
+ const object = descriptor.object === undefined ? "" : ` ${descriptor.object}${descriptor.truncated ? "…" : ""}`;
517
+ return `${action.tool} ${descriptor.verb}${object}`;
518
+ }
519
+
520
+ /** The phase word an expanded card names, or null for a phase that says nothing new. */
521
+ function progressPhaseUnit(theme: ClioTheme, progress: WorkerProgressSnapshot): string | null {
522
+ switch (progress.phase) {
523
+ case "thinking":
524
+ // The phase only. Reasoning content never reaches an operator surface.
525
+ return theme.fg("reason", `${GLYPH.phaseThinking} thinking`);
526
+ case "writing":
527
+ return theme.fg("accent", `${GLYPH.phaseWriting} writing`);
528
+ case "tool":
529
+ return theme.fg("action", `${GLYPH.phaseTool} tool`);
530
+ case "waiting":
531
+ return theme.fg("info", `${GLYPH.phaseWaiting} waiting`);
532
+ case "settled":
533
+ case "starting":
534
+ return null;
535
+ }
536
+ }
537
+
538
+ /** The `doing` row: the phase, then the running call or the most recent one, with its object. */
539
+ function progressActionLine(theme: ClioTheme, progress: WorkerProgressSnapshot, contentWidth: number): string | null {
540
+ const phase = progressPhaseUnit(theme, progress);
541
+ const current = progress.currentAction;
542
+ const recent = progress.recentActions[0];
543
+ const units: string[] = [];
544
+ if (phase !== null) units.push(phase);
545
+ if (current !== null) units.push(theme.fg("muted", actionPhrase(current)));
546
+ else if (recent !== undefined) units.push(theme.fg("dim", `last ${actionPhrase(recent)}`));
547
+ if (units.length === 0) return null;
548
+ return cardUnitsLine(theme, "doing", units, contentWidth);
549
+ }
550
+
551
+ /**
552
+ * The `answer` block: the newest rows of the worker's bounded prose on a rail,
553
+ * then what is not shown and where to read it. Wrapping happens before the row
554
+ * cap, so the block is at most {@link WORKER_PROGRESS_CARD_ROWS} rows tall
555
+ * whatever the terminal width, and a streaming answer cannot make the card grow
556
+ * under the operator.
557
+ */
558
+ function progressAnswerLines(
559
+ theme: ClioTheme,
560
+ progress: WorkerProgressSnapshot,
561
+ runId: string,
562
+ contentWidth: number,
563
+ ): string[] {
564
+ if (progress.tailText.length === 0) return [];
565
+ const gutter = CARD_KV_KEY_WIDTH + 1;
566
+ const railWidth = Math.max(1, contentWidth - gutter - 2);
567
+ const wrapped: string[] = [];
568
+ for (const line of progress.tailText.split("\n")) {
569
+ for (const row of wrapTextWithAnsi(sanitizeCallTargetText(line), railWidth)) wrapped.push(row);
570
+ }
571
+ const shown = wrapped.slice(Math.max(0, wrapped.length - WORKER_PROGRESS_CARD_ROWS));
572
+ const hiddenRows = wrapped.length - shown.length;
573
+ const rail = theme.fg("dim", `${GLYPH.rail} `);
574
+ const body = shown.map((row) => `${rail}${theme.fg("muted", row)}`);
575
+ const hiddenLines = progress.droppedLines + hiddenRows;
576
+ if (hiddenLines > 0 || progress.droppedBytes > 0) {
577
+ const facts = [
578
+ ...(hiddenLines > 0 ? [`${hiddenLines} more line${hiddenLines === 1 ? "" : "s"}`] : []),
579
+ ...(progress.droppedBytes > 0 ? [`${progress.droppedBytes} bytes outran the view`] : []),
580
+ `/view dispatch:${runId}`,
581
+ ];
582
+ body.push(`${rail}${theme.fg("dim", truncateToWidth(facts.join(" · "), railWidth, "…", false))}`);
583
+ }
584
+ // The key labels the block once and the rest hangs under it, which is the
585
+ // card's key-value grammar applied to a body rather than to one value.
586
+ return body.map((row, index) => `${index === 0 ? cardKvKey(theme, "answer") : " ".repeat(gutter)}${row}`);
587
+ }
588
+
436
589
  export function renderDispatchCard(
437
590
  row: DispatchBoardRow,
438
591
  width: number,
439
592
  evidence?: RunEvidencePresentation,
440
- options: { selected?: boolean } = {},
593
+ options: { selected?: boolean; expanded?: boolean } = {},
441
594
  ): string[] {
442
595
  const theme = clioTheme();
443
596
  const contentWidth = Math.max(0, width - 4);
@@ -498,14 +651,42 @@ export function renderDispatchCard(
498
651
  `${theme.fg("dim", "ttft")} ${theme.fg("muted", ttft)}`,
499
652
  `${theme.fg("dim", "cost")} ${theme.fg("muted", cost)}`,
500
653
  ];
654
+ const hostVerification = theme.fg("info", `host_verification=${row.hostVerification ?? "not_requested"}`);
501
655
  const contextUnit = formatWorkerContextMeter(row.lastContextTokens ?? 0, row.contextWindow, theme);
656
+ // The phase column: a fleet step says which wave it belongs to and which
657
+ // step it is; every other run leaves the cell empty rather than inventing a
658
+ // position, so the column reads as "not a fleet step" and not as wave zero.
659
+ const phaseCell = formatDispatchPhaseCell(row.phase, contentWidth);
502
660
  const bodyLines = [
503
661
  cardUnitsLine(theme, "run", [theme.fg("dim", row.runId)], contentWidth),
662
+ cardUnitsLine(
663
+ theme,
664
+ "phase",
665
+ [phaseCell === null ? theme.fg("dim", "—") : theme.fg("info", phaseCell)],
666
+ contentWidth,
667
+ ),
504
668
  targetLine,
505
669
  ...(row.taskSummary
506
670
  ? [truncateToWidth(`${cardKvKey(theme, "task")}${theme.fg("muted", row.taskSummary)}`, contentWidth, "…", false)]
507
671
  : []),
508
672
  cardUnitsLine(theme, "status", statusUnits, contentWidth),
673
+ cardUnitsLine(theme, "evidence", [hostVerification], contentWidth),
674
+ ...(row.budget !== undefined
675
+ ? [
676
+ truncateToWidth(
677
+ `${cardKvKey(theme, "policy")}${theme.fg("muted", `${formatBudgetPolicy(row.budget)}; requested ${formatBudgetRequest(row.budget)}`)}`,
678
+ contentWidth,
679
+ "…",
680
+ false,
681
+ ),
682
+ truncateToWidth(
683
+ `${cardKvKey(theme, "budget")}${theme.fg("muted", `${formatEffectiveBudget(row.budget)}; reason ${formatBudgetReasons(row.budget)}`)}`,
684
+ contentWidth,
685
+ "…",
686
+ false,
687
+ ),
688
+ ]
689
+ : []),
509
690
  cardUnitsLine(theme, "telemetry", [up, down, total, ...(contextUnit !== null ? [contextUnit] : [])], contentWidth),
510
691
  ];
511
692
  if (row.retry) {
@@ -533,6 +714,13 @@ export function renderDispatchCard(
533
714
  ];
534
715
  bodyLines.push(cardUnitsLine(theme, "tools", toolUnits, contentWidth));
535
716
  }
717
+ // Live worker progress is expanded detail, never a default row: five running
718
+ // scouts must stay five compact cards until the operator opens one.
719
+ if (options.expanded === true && row.progress) {
720
+ const doing = progressActionLine(theme, row.progress, contentWidth);
721
+ if (doing !== null) bodyLines.push(doing);
722
+ bodyLines.push(...progressAnswerLines(theme, row.progress, row.runId, contentWidth));
723
+ }
536
724
  if (row.steerAcknowledgement) {
537
725
  bodyLines.push(
538
726
  cardUnitsLine(
@@ -580,11 +768,25 @@ function renderTaskIslandRow(row: DispatchBoardRow, width: number): string[] {
580
768
  visibleWidth(presentation.label) +
581
769
  3 +
582
770
  visibleWidth(elapsed);
583
- const clampedLabel = truncateToWidth(agentLabel, Math.max(1, width - labelChrome), "…", false);
771
+ // The phase column rides the compact row only when the row can still show a
772
+ // readable agent label beside it. TASK_ISLAND_WIDTH is fixed, so a row that
773
+ // cannot afford the cell drops the column here and shows the phase in the
774
+ // expanded card instead of clipping the label to make room.
775
+ const phaseCell = formatDispatchPhaseCell(row.phase);
776
+ const phaseColumn = phaseCell === null ? "" : `${dot}${theme.fg("info", padAnsi(phaseCell, PHASE_COLUMN_WIDTH))}`;
777
+ const phaseChrome = phaseCell === null ? 0 : PHASE_COLUMN_WIDTH + 3;
778
+ const labelWidth = width - labelChrome - phaseChrome;
779
+ const showPhase = phaseCell !== null && labelWidth >= PHASE_COLUMN_MIN_LABEL_WIDTH;
780
+ const clampedLabel = truncateToWidth(
781
+ agentLabel,
782
+ Math.max(1, showPhase ? labelWidth : width - labelChrome),
783
+ "…",
784
+ false,
785
+ );
584
786
 
585
787
  // The agent label drops its accent color for plain bold; the status word is
586
788
  // the only status-colored element on the row, with dim middot separators.
587
- const line1 = `${glyph} ${rowPrefix.text}${theme.paint(clampedLabel, { bold: true })}${dot}${statusStr}${dot}${theme.fg("muted", elapsed)}`;
789
+ const line1 = `${glyph} ${rowPrefix.text}${theme.paint(clampedLabel, { bold: true })}${dot}${statusStr}${dot}${theme.fg("muted", elapsed)}${showPhase ? phaseColumn : ""}`;
588
790
 
589
791
  const elapsedSec = row.elapsedMs / 1000;
590
792
  const tokensPerSec = elapsedSec > 0.1 ? Math.round(row.outputTokens / elapsedSec) : 0;
@@ -605,11 +807,138 @@ function renderTaskIslandRow(row: DispatchBoardRow, width: number): string[] {
605
807
  return [padAnsi(line1, width), ...(task ? [padAnsi(task, width)] : []), padAnsi(telemetry, width)];
606
808
  }
607
809
 
810
+ /**
811
+ * One member column's facts, drawn from the row the board already keeps. The
812
+ * route, status, and answer tail are the same values the run's own card shows,
813
+ * so a council column and a council card can never disagree.
814
+ */
815
+ function councilMemberView(
816
+ row: DispatchBoardRow,
817
+ council: NonNullable<DispatchBoardRow["council"]>,
818
+ ): CouncilMemberView {
819
+ const presentation = dispatchStatusPresentation(row.status, {
820
+ compact: true,
821
+ ...(row.status === "running" ? { tick: Math.floor(Date.now() / 100) } : {}),
822
+ });
823
+ return {
824
+ runId: row.runId,
825
+ label: council.label,
826
+ ...(council.color !== undefined ? { color: council.color } : {}),
827
+ round: council.round,
828
+ route: `${row.targetId}/${row.wireModelId}`,
829
+ status: { glyph: presentation.glyph, label: presentation.label, token: presentation.token },
830
+ tailText: sanitizeCallTargetText(row.progress?.tailText ?? ""),
831
+ droppedLines: row.progress?.droppedLines ?? 0,
832
+ };
833
+ }
834
+
835
+ /**
836
+ * Fold one council group's rows into the view the grid renders.
837
+ *
838
+ * A council of N members over R rounds seals N x R member runs, and the
839
+ * operator is looking at a council, not at its history: each label keeps its
840
+ * newest round, so the group shows one column per member however many rounds
841
+ * it ran. Columns are ordered by label so a repaint cannot reshuffle them under
842
+ * the cursor while runs settle in whatever order they finish.
843
+ */
844
+ function foldCouncilGroup(
845
+ group: string,
846
+ rows: ReadonlyArray<DispatchBoardRow>,
847
+ selectedRunId?: string | null,
848
+ ): CouncilGroupView {
849
+ const newest = new Map<string, DispatchBoardRow>();
850
+ let synthesisRow: DispatchBoardRow | null = null;
851
+ let rank = Number.POSITIVE_INFINITY;
852
+ let elapsedMs = 0;
853
+ let round = 1;
854
+ for (const row of rows) {
855
+ const council = row.council;
856
+ if (council === undefined) continue;
857
+ rank = Math.min(rank, STATUS_ORDER[row.status]);
858
+ elapsedMs = Math.max(elapsedMs, row.elapsedMs);
859
+ if (council.label === COUNCIL_SYNTHESIS_LABEL) {
860
+ synthesisRow = row;
861
+ continue;
862
+ }
863
+ round = Math.max(round, council.round);
864
+ const previous = newest.get(council.label);
865
+ if (previous === undefined || (previous.council?.round ?? 0) <= council.round) newest.set(council.label, row);
866
+ }
867
+ const aggregate = (Object.entries(STATUS_ORDER) as Array<[DispatchBoardStatus, number]>).find(
868
+ ([, value]) => value === rank,
869
+ );
870
+ const presentation = dispatchStatusPresentation(aggregate?.[0] ?? "running", {
871
+ compact: true,
872
+ ...(aggregate?.[0] === "running" ? { tick: Math.floor(Date.now() / 100) } : {}),
873
+ });
874
+ const members = [...newest.entries()]
875
+ .sort(([left], [right]) => left.localeCompare(right))
876
+ // biome-ignore lint/style/noNonNullAssertion: every entry was inserted with its own council badge.
877
+ .map(([, row]) => councilMemberView(row, row.council!));
878
+ const selectedInGroup =
879
+ selectedRunId != null && rows.some((row) => row.runId === selectedRunId) ? selectedRunId : undefined;
880
+ return {
881
+ group,
882
+ members,
883
+ // biome-ignore lint/style/noNonNullAssertion: the synthesis row was matched on its own council badge.
884
+ synthesis: synthesisRow === null ? null : councilMemberView(synthesisRow, synthesisRow.council!),
885
+ status: { glyph: presentation.glyph, label: presentation.label, token: presentation.token },
886
+ round,
887
+ elapsed: formatCompactMs(elapsedMs),
888
+ ...(selectedInGroup !== undefined ? { selectedRunId: selectedInGroup } : {}),
889
+ };
890
+ }
891
+
892
+ /** A board entry: either one ordinary run, or one whole council in the position of its first row. */
893
+ export type DispatchBoardItem = { kind: "run"; row: DispatchBoardRow } | { kind: "council"; group: CouncilGroupView };
894
+
895
+ /**
896
+ * Board order with councils folded in place. A council occupies the position of
897
+ * its first row and its remaining rows are consumed, so the board never shows a
898
+ * member both inside its group and beside it.
899
+ */
900
+ export function dispatchBoardItems(
901
+ rows: ReadonlyArray<DispatchBoardRow>,
902
+ selectedRunId?: string | null,
903
+ ): DispatchBoardItem[] {
904
+ const byGroup = new Map<string, DispatchBoardRow[]>();
905
+ for (const row of rows) {
906
+ const group = row.council?.group;
907
+ if (group === undefined) continue;
908
+ const bucket = byGroup.get(group);
909
+ if (bucket === undefined) byGroup.set(group, [row]);
910
+ else bucket.push(row);
911
+ }
912
+ const emitted = new Set<string>();
913
+ const items: DispatchBoardItem[] = [];
914
+ for (const row of rows) {
915
+ const group = row.council?.group;
916
+ if (group === undefined) {
917
+ items.push({ kind: "run", row });
918
+ continue;
919
+ }
920
+ if (emitted.has(group)) continue;
921
+ emitted.add(group);
922
+ items.push({ kind: "council", group: foldCouncilGroup(group, byGroup.get(group) ?? [row], selectedRunId) });
923
+ }
924
+ return items;
925
+ }
926
+
927
+ /** The framed council card: the member grid or stack, with the synthesis run full width under it. */
928
+ export function renderCouncilCard(group: CouncilGroupView, width: number): string[] {
929
+ const theme = clioTheme();
930
+ const contentWidth = Math.max(0, width - 4);
931
+ const title = `${theme.fg("info", "council")} ${theme.paint(group.group, { bold: true })}`;
932
+ return frame(theme, title, councilGroupBody(theme, group, contentWidth), width, { rightMeta: group.elapsed });
933
+ }
934
+
608
935
  export function formatDispatchBoardLines(
609
936
  rows: ReadonlyArray<DispatchBoardRow>,
610
937
  width = 76,
611
938
  observability?: ObservabilitySnapshot,
612
939
  selectedRunId?: string | null,
940
+ /** Whether the selected row renders its worker-progress detail. Off by default. */
941
+ detailExpanded = false,
613
942
  ): string[] {
614
943
  if (rows.length === 0) {
615
944
  const theme = clioTheme();
@@ -620,10 +949,15 @@ export function formatDispatchBoardLines(
620
949
  });
621
950
  }
622
951
 
623
- const cards = rows.map((row) =>
624
- renderDispatchCard(row, width, deriveRunEvidenceState(observability, row.runId), {
625
- selected: row.runId === selectedRunId,
626
- }),
952
+ // A council is one question asked of several members, so its rows render as
953
+ // one card holding the whole group rather than as unrelated neighbours.
954
+ const cards = dispatchBoardItems(rows, selectedRunId).map((item) =>
955
+ item.kind === "council"
956
+ ? renderCouncilCard(item.group, width)
957
+ : renderDispatchCard(item.row, width, deriveRunEvidenceState(observability, item.row.runId), {
958
+ selected: item.row.runId === selectedRunId,
959
+ expanded: detailExpanded && item.row.runId === selectedRunId,
960
+ }),
627
961
  );
628
962
  const body: string[] = [];
629
963
  for (const card of cards) {
@@ -648,6 +982,9 @@ export interface DispatchBoardView extends Component {
648
982
  selectPrevious(): void;
649
983
  selectNext(): void;
650
984
  resetSelection(): void;
985
+ /** Open or close the selected row's worker-progress detail. */
986
+ toggleDetail(): void;
987
+ detailExpanded(): boolean;
651
988
  }
652
989
 
653
990
  export function createDispatchBoardView(
@@ -655,6 +992,9 @@ export function createDispatchBoardView(
655
992
  observability: () => ObservabilitySnapshot | undefined,
656
993
  ): DispatchBoardView {
657
994
  let selectedRunId: string | null = null;
995
+ // Detail is a property of the operator's attention, not of a run, so it
996
+ // follows the cursor rather than pinning to the row it was opened on.
997
+ let expanded = false;
658
998
 
659
999
  const normalizeSelection = (currentRows: ReadonlyArray<DispatchBoardRow>): DispatchBoardRow | null => {
660
1000
  if (currentRows.length === 0) {
@@ -679,7 +1019,7 @@ export function createDispatchBoardView(
679
1019
  render(width: number): string[] {
680
1020
  const currentRows = rows();
681
1021
  normalizeSelection(currentRows);
682
- return formatDispatchBoardLines(currentRows, Math.max(1, width), observability(), selectedRunId);
1022
+ return formatDispatchBoardLines(currentRows, Math.max(1, width), observability(), selectedRunId, expanded);
683
1023
  },
684
1024
  selectedRow(): DispatchBoardRow | null {
685
1025
  return normalizeSelection(rows());
@@ -692,30 +1032,44 @@ export function createDispatchBoardView(
692
1032
  },
693
1033
  resetSelection(): void {
694
1034
  selectedRunId = null;
1035
+ expanded = false;
695
1036
  normalizeSelection(rows());
696
1037
  },
1038
+ toggleDetail(): void {
1039
+ expanded = !expanded;
1040
+ },
1041
+ detailExpanded(): boolean {
1042
+ return expanded;
1043
+ },
697
1044
  invalidate(): void {},
698
1045
  };
699
1046
  }
700
1047
 
701
1048
  export function formatTaskIslandLines(rows: ReadonlyArray<DispatchBoardRow>, maxRows = 4): string[] {
702
- const visibleRows = rows.slice(0, Math.max(1, maxRows));
1049
+ // Councils are folded before the row cap, so a five-member council costs the
1050
+ // island one card and never crowds out the runs beside it.
1051
+ const items = dispatchBoardItems(rows);
1052
+ const visibleItems = items.slice(0, Math.max(1, maxRows));
703
1053
  const body: string[] = [];
704
1054
 
705
- if (visibleRows.length === 0) {
1055
+ if (visibleItems.length === 0) {
706
1056
  const theme = clioTheme();
707
1057
  body.push(theme.fg("dim", "No active fleet runs."));
708
1058
  body.push(theme.fg("dim", "Use /run or /delegate to spawn agents."));
709
1059
  } else {
710
- for (let i = 0; i < visibleRows.length; i++) {
711
- const row = visibleRows[i];
712
- if (!row) continue;
1060
+ for (let i = 0; i < visibleItems.length; i++) {
1061
+ const item = visibleItems[i];
1062
+ if (!item) continue;
713
1063
  if (i > 0) {
714
1064
  body.push(innerDivider(clioTheme(), TASK_ISLAND_WIDTH));
715
1065
  }
716
- body.push(...renderTaskIslandRow(row, TASK_ISLAND_WIDTH));
1066
+ body.push(
1067
+ ...(item.kind === "council"
1068
+ ? councilIslandLines(clioTheme(), item.group, TASK_ISLAND_WIDTH)
1069
+ : renderTaskIslandRow(item.row, TASK_ISLAND_WIDTH)),
1070
+ );
717
1071
  }
718
- const hidden = rows.length - visibleRows.length;
1072
+ const hidden = items.length - visibleItems.length;
719
1073
  if (hidden > 0) {
720
1074
  body.push(innerDivider(clioTheme(), TASK_ISLAND_WIDTH));
721
1075
  body.push(clioTheme().fg("dim", `+ ${hidden} more`));
@@ -781,6 +1135,21 @@ function parseGateBadge(value: unknown): { role: string; cycle: number } | undef
781
1135
  return { role: record.role, cycle };
782
1136
  }
783
1137
 
1138
+ function parseCouncilBadge(value: unknown): DispatchBoardRow["council"] | undefined {
1139
+ if (typeof value !== "object" || value === null) return undefined;
1140
+ const record = value as { group?: unknown; label?: unknown; color?: unknown; round?: unknown };
1141
+ const group = parseNonEmptyString(record.group);
1142
+ const label = parseNonEmptyString(record.label);
1143
+ const round = parsePositiveInt(record.round);
1144
+ if (group === undefined || label === undefined || round === undefined) return undefined;
1145
+ return {
1146
+ group,
1147
+ label,
1148
+ ...(typeof record.color === "string" && record.color.length > 0 ? { color: record.color } : {}),
1149
+ round,
1150
+ };
1151
+ }
1152
+
784
1153
  function parseFiniteNumberOrZero(value: unknown): number {
785
1154
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
786
1155
  }
@@ -879,6 +1248,7 @@ function readRunningSnapshot(snapshot: DispatchSnapshot): Map<
879
1248
  costUsd: number;
880
1249
  costProvenance: CostProvenance;
881
1250
  outcomePhase: string;
1251
+ budget?: RunToolBudgetEnvelope;
882
1252
  }
883
1253
  > {
884
1254
  const running = new Map<
@@ -890,12 +1260,14 @@ function readRunningSnapshot(snapshot: DispatchSnapshot): Map<
890
1260
  costUsd: number;
891
1261
  costProvenance: CostProvenance;
892
1262
  outcomePhase: string;
1263
+ budget?: RunToolBudgetEnvelope;
893
1264
  }
894
1265
  >();
895
1266
  try {
896
1267
  for (const value of snapshot.running) {
897
1268
  const runId = parseRunId(value.runId);
898
1269
  if (!runId) continue;
1270
+ const budget = cloneRunToolBudgetEnvelope(value.budget);
899
1271
  running.set(runId, {
900
1272
  inputTokens: parseFiniteNumberOrZero(value.tokens.input),
901
1273
  outputTokens: parseFiniteNumberOrZero(value.tokens.output),
@@ -903,6 +1275,7 @@ function readRunningSnapshot(snapshot: DispatchSnapshot): Map<
903
1275
  costUsd: parseFiniteNumberOrZero(value.costUsd),
904
1276
  costProvenance: value.costProvenance ?? "unknown",
905
1277
  outcomePhase: value.outcomePhase,
1278
+ ...(budget !== undefined ? { budget } : {}),
906
1279
  });
907
1280
  }
908
1281
  } catch {
@@ -920,7 +1293,9 @@ function resolveElapsedMs(entry: DispatchBoardEntry, now: number): number {
920
1293
 
921
1294
  function toRow(entry: DispatchBoardEntry, now: number): DispatchBoardRow {
922
1295
  const retry = entry.retry;
1296
+ const progress = entry.progress.snapshot();
923
1297
  return {
1298
+ progress,
924
1299
  runId: entry.runId,
925
1300
  agentId: entry.agentId,
926
1301
  ...(entry.agentAudience !== undefined ? { agentAudience: entry.agentAudience } : {}),
@@ -930,6 +1305,7 @@ function toRow(entry: DispatchBoardEntry, now: number): DispatchBoardRow {
930
1305
  targetId: entry.targetId,
931
1306
  wireModelId: entry.wireModelId,
932
1307
  ...(entry.taskSummary !== undefined ? { taskSummary: entry.taskSummary } : {}),
1308
+ ...(entry.budget !== undefined ? { budget: entry.budget } : {}),
933
1309
  status: retry ? "retrying" : entry.status,
934
1310
  elapsedMs: resolveElapsedMs(entry, now),
935
1311
  tokenCount: entry.tokenCount,
@@ -941,13 +1317,18 @@ function toRow(entry: DispatchBoardEntry, now: number): DispatchBoardRow {
941
1317
  ...(entry.outcomeDetail !== undefined ? { outcomeDetail: entry.outcomeDetail } : {}),
942
1318
  ...(entry.node !== undefined ? { node: entry.node } : {}),
943
1319
  ...(entry.gate !== undefined ? { gate: { ...entry.gate } } : {}),
1320
+ ...(entry.council !== undefined ? { council: { ...entry.council } } : {}),
1321
+ ...(entry.hostVerification !== undefined ? { hostVerification: entry.hostVerification } : {}),
944
1322
  ...(entry.rerouteCount !== undefined ? { rerouteCount: entry.rerouteCount } : {}),
945
1323
  ...(entry.failoverHops !== undefined ? { failoverHops: entry.failoverHops } : {}),
946
1324
  ...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {}),
947
1325
  lastContextTokens: entry.lastContextTokens,
948
1326
  ...(entry.receiptId !== undefined ? { receiptId: entry.receiptId } : {}),
949
- currentTool: retry ? null : entry.currentTool,
950
- recentTools: [...entry.recentTools],
1327
+ ...(entry.phase !== undefined ? { phase: { ...entry.phase } } : {}),
1328
+ // Both tool fields are views of the one projection, so the compact card and
1329
+ // the expanded detail can never disagree about what is running.
1330
+ currentTool: retry ? null : (progress.currentAction?.tool ?? null),
1331
+ recentTools: progress.recentActions.map((action) => action.tool),
951
1332
  ...(retry ? { retry: { ...retry } } : {}),
952
1333
  ...(entry.steerAcknowledgement ? { steerAcknowledgement: { ...entry.steerAcknowledgement } } : {}),
953
1334
  };
@@ -964,7 +1345,7 @@ function sortEntries(a: DispatchBoardEntry, b: DispatchBoardEntry): number {
964
1345
  return a.sequence - b.sequence;
965
1346
  }
966
1347
 
967
- function pruneEntries(entries: Map<string, DispatchBoardEntry>): void {
1348
+ function pruneEntries(entries: Map<string, DispatchBoardEntry>, onEvict?: (runId: string) => void): void {
968
1349
  if (entries.size <= MAX_DISPATCH_BOARD_ROWS) return;
969
1350
  const terminalEntries = [...entries.values()]
970
1351
  .filter((entry) => !entry.retry && isTerminalStatus(entry.status))
@@ -974,22 +1355,49 @@ function pruneEntries(entries: Map<string, DispatchBoardEntry>): void {
974
1355
  for (const entry of evictionQueue) {
975
1356
  if (entries.size <= MAX_DISPATCH_BOARD_ROWS) break;
976
1357
  entries.delete(entry.runId);
1358
+ onEvict?.(entry.runId);
977
1359
  }
978
1360
  }
979
1361
 
980
1362
  export function createDispatchBoardStore(
981
1363
  bus: SafeEventBus,
982
1364
  snapshot?: () => DispatchSnapshot,
1365
+ /**
1366
+ * Sealed terminal facts for a finished run. Settlement replaces the
1367
+ * provisional live tail with the receipt's answer where one can be read; the
1368
+ * run's own durable message stands in where it cannot.
1369
+ */
1370
+ readReceipt?: WorkerReceiptReader,
983
1371
  ): {
984
1372
  rows(): ReadonlyArray<DispatchBoardRow>;
985
1373
  activeRows(): ReadonlyArray<DispatchBoardRow>;
986
1374
  reconcile(): void;
1375
+ /**
1376
+ * Record the fleet-plan position of a run the caller just dispatched. The
1377
+ * board never infers a position: a run carries one only because whoever
1378
+ * dispatched the plan said so, which is why a non-fleet run can never grow
1379
+ * a phase cell.
1380
+ */
1381
+ setFleetPhase(runId: string, phase: DispatchBoardPhase): void;
987
1382
  unsubscribe(): void;
988
1383
  } {
989
1384
  const entries = new Map<string, DispatchBoardEntry>();
1385
+ // Positions arrive from the fleet-run path, which may hand one over before
1386
+ // the run's first lifecycle event creates the entry.
1387
+ const fleetPhases = new Map<string, DispatchBoardPhase>();
990
1388
  let nextSequence = 0;
991
1389
  let reconciledAtMs = Date.now();
992
1390
 
1391
+ /**
1392
+ * Seal a run's projection on the sealed receipt's answer. Only for the
1393
+ * terminal bus events: the dispatch domain writes `receipts/<runId>.json`
1394
+ * before it publishes one, so this is the first moment the file exists.
1395
+ */
1396
+ const settleFromReceipt = (entry: DispatchBoardEntry): void => {
1397
+ const text = readReceipt?.(entry.runId)?.text;
1398
+ entry.progress.settle(typeof text === "string" && text.trim().length > 0 ? text : undefined);
1399
+ };
1400
+
993
1401
  // Payloads arrive typed off the bus, but the board keeps its runtime
994
1402
  // parsing (parse* helpers) because events are not validated at runtime.
995
1403
  const upsertBase = (
@@ -1004,9 +1412,12 @@ export function createDispatchBoardStore(
1004
1412
  const requestOrigin = parseRequestOrigin(raw.requestOrigin, previous?.requestOrigin);
1005
1413
  const node = parseNonEmptyString(raw.node) ?? previous?.node;
1006
1414
  const gate = parseGateBadge(raw.gate) ?? previous?.gate;
1415
+ const council = parseCouncilBadge(raw.council) ?? previous?.council;
1007
1416
  const rerouteCount = parsePositiveInt(raw.rerouteCount) ?? previous?.rerouteCount;
1008
1417
  const contextWindow = parsePositiveInt(raw.contextWindow) ?? previous?.contextWindow;
1009
1418
  const taskSummary = parseTaskSummary(raw, previous?.taskSummary);
1419
+ const budget = cloneRunToolBudgetEnvelope(raw.budget) ?? previous?.budget;
1420
+ const phase = fleetPhases.get(runId) ?? previous?.phase;
1010
1421
  const entry: DispatchBoardEntry = {
1011
1422
  runId,
1012
1423
  agentId: parseText(raw.agentId, previous?.agentId ?? "-"),
@@ -1017,6 +1428,7 @@ export function createDispatchBoardStore(
1017
1428
  targetId: parseText(raw.targetId, previous?.targetId ?? "-"),
1018
1429
  wireModelId: parseText(raw.wireModelId, previous?.wireModelId ?? "-"),
1019
1430
  ...(taskSummary !== undefined ? { taskSummary } : {}),
1431
+ ...(budget !== undefined ? { budget } : {}),
1020
1432
  status,
1021
1433
  tokenCount: previous?.tokenCount ?? 0,
1022
1434
  costUsd: previous?.costUsd ?? 0,
@@ -1033,17 +1445,19 @@ export function createDispatchBoardStore(
1033
1445
  outcomeDetail: previous?.outcomeDetail ?? null,
1034
1446
  ...(node !== undefined ? { node } : {}),
1035
1447
  ...(gate !== undefined ? { gate } : {}),
1448
+ ...(council !== undefined ? { council } : {}),
1449
+ ...(previous?.hostVerification !== undefined ? { hostVerification: previous.hostVerification } : {}),
1036
1450
  ...(rerouteCount !== undefined ? { rerouteCount } : {}),
1037
1451
  ...(contextWindow !== undefined ? { contextWindow } : {}),
1038
1452
  lastContextTokens: previous?.lastContextTokens ?? 0,
1039
1453
  ...(previous?.receiptId !== undefined ? { receiptId: previous.receiptId } : {}),
1040
- currentTool: previous?.currentTool ?? null,
1041
- recentTools: previous?.recentTools ?? [],
1454
+ ...(phase !== undefined ? { phase } : {}),
1455
+ progress: previous?.progress ?? createWorkerProgressFold(),
1042
1456
  ...(previous?.retry ? { retry: { ...previous.retry } } : {}),
1043
1457
  ...(previous?.steerAcknowledgement ? { steerAcknowledgement: { ...previous.steerAcknowledgement } } : {}),
1044
1458
  };
1045
1459
  entries.set(runId, entry);
1046
- pruneEntries(entries);
1460
+ pruneEntries(entries, (evicted) => fleetPhases.delete(evicted));
1047
1461
  return entry;
1048
1462
  };
1049
1463
 
@@ -1074,7 +1488,8 @@ export function createDispatchBoardStore(
1074
1488
  entry.costUsd = parseFiniteNumber(payload.costUsd, entry.costUsd);
1075
1489
  entry.costProvenance = payload.costProvenance ?? "unknown";
1076
1490
  entry.outcomeDetail = null;
1077
- entry.currentTool = null;
1491
+ if (payload.hostVerification !== undefined) entry.hostVerification = payload.hostVerification;
1492
+ settleFromReceipt(entry);
1078
1493
  // A terminal dispatch event is published only after the run's receipt is
1079
1494
  // sealed at receipts/<runId>.json, so the run id is the receipt id here.
1080
1495
  entry.receiptId = entry.runId;
@@ -1102,7 +1517,8 @@ export function createDispatchBoardStore(
1102
1517
  entry.costUsd = parseFiniteNumber(payload.costUsd, entry.costUsd);
1103
1518
  entry.costProvenance = payload.costProvenance ?? "unknown";
1104
1519
  entry.outcomeDetail = resolveFailureDetail(payload, entry.outcomeDetail);
1105
- entry.currentTool = null;
1520
+ if (payload.hostVerification !== undefined) entry.hostVerification = payload.hostVerification;
1521
+ settleFromReceipt(entry);
1106
1522
  // A denied retry never reached a run, so no receipt was sealed for it;
1107
1523
  // every other failure finalized through recordReceipt like a success.
1108
1524
  if (payload.reason !== "retry_denied") entry.receiptId = entry.runId;
@@ -1126,7 +1542,7 @@ export function createDispatchBoardStore(
1126
1542
  // wind down, so the history row can become terminal immediately.
1127
1543
  entry.status = "aborted";
1128
1544
  entry.finishedAtMs = Date.now();
1129
- entry.currentTool = null;
1545
+ entry.progress.settle();
1130
1546
  } else {
1131
1547
  if (isTerminalStatus(entry.status) && !wasRetrying) return;
1132
1548
  entry.status = "cancelling";
@@ -1148,7 +1564,10 @@ export function createDispatchBoardStore(
1148
1564
  entry.status = status;
1149
1565
  if (status === "dead") {
1150
1566
  entry.finishedAtMs ??= Date.now();
1151
- entry.currentTool = null;
1567
+ // A worker-side terminal signal arrives before any receipt is
1568
+ // sealed, so the run's own last durable message is the best truth
1569
+ // there is; the terminal bus event replaces it with the sealed one.
1570
+ entry.progress.settle();
1152
1571
  delete entry.retry;
1153
1572
  }
1154
1573
  return;
@@ -1159,7 +1578,9 @@ export function createDispatchBoardStore(
1159
1578
  entry.failoverHops = (entry.failoverHops ?? 0) + 1;
1160
1579
  entry.status = "running";
1161
1580
  entry.finishedAtMs = null;
1162
- entry.currentTool = null;
1581
+ // The projection keeps the tail the operator is reading and drops the
1582
+ // finished attempt's live state, exactly as the transcript block does.
1583
+ entry.progress.restart();
1163
1584
  delete entry.retry;
1164
1585
  return;
1165
1586
  }
@@ -1183,20 +1604,10 @@ export function createDispatchBoardStore(
1183
1604
  entry.ttftMs = Math.round(performance.now() - entry.startedAtClockMs);
1184
1605
  }
1185
1606
  }
1186
- if (type === "clio_tool_start") {
1187
- const payload = (workerEvent as { payload?: { tool?: unknown } }).payload;
1188
- const tool = parseNonEmptyString(payload?.tool);
1189
- if (tool !== undefined) entry.currentTool = tool;
1190
- }
1191
- if (type === "clio_tool_finish") {
1192
- const payload = (workerEvent as { payload?: { tool?: unknown } }).payload;
1193
- const tool = parseNonEmptyString(payload?.tool);
1194
- entry.currentTool = null;
1195
- if (tool !== undefined) {
1196
- entry.recentTools.unshift(tool);
1197
- if (entry.recentTools.length > RECENT_TOOL_TRAIL_LIMIT) entry.recentTools.length = RECENT_TOOL_TRAIL_LIMIT;
1198
- }
1199
- }
1607
+ // The one fold that reads worker prose and tool activity. Everything the
1608
+ // board says about what a worker is saying or touching comes from here,
1609
+ // so the board and the transcript block cannot tell two stories.
1610
+ entry.progress.observe(payload.event);
1200
1611
  if (type === "clio_steer_received") {
1201
1612
  const steerPayload = (workerEvent as { payload?: { chars?: unknown } }).payload;
1202
1613
  entry.steerAcknowledgement = {
@@ -1221,7 +1632,10 @@ export function createDispatchBoardStore(
1221
1632
  if (!status) return;
1222
1633
  entry.status = status;
1223
1634
  entry.finishedAtMs ??= Date.now();
1224
- entry.currentTool = null;
1635
+ // The worker says it is done before the domain seals its receipt. Settle
1636
+ // on the durable message now so the card stops reading as live; the
1637
+ // terminal bus event seals it on the receipt shortly after.
1638
+ entry.progress.settle();
1225
1639
  delete entry.retry;
1226
1640
  }
1227
1641
  }),
@@ -1270,7 +1684,8 @@ export function createDispatchBoardStore(
1270
1684
  now,
1271
1685
  );
1272
1686
  if (entry) {
1273
- entry.currentTool = null;
1687
+ // A waiting retry has no live worker; the row projection already
1688
+ // renders a retrying row with no current tool.
1274
1689
  entry.retry = { ...projection.retry };
1275
1690
  if (entry.taskSummary === undefined && projection.taskSummary !== undefined) {
1276
1691
  entry.taskSummary = projection.taskSummary;
@@ -1286,6 +1701,7 @@ export function createDispatchBoardStore(
1286
1701
  entry.tokenCount = live.tokenCount;
1287
1702
  entry.costUsd = live.costUsd;
1288
1703
  entry.costProvenance = live.costProvenance;
1704
+ if (live.budget !== undefined) entry.budget = live.budget;
1289
1705
  if (live.outcomePhase === "aborting") {
1290
1706
  if (entry.status !== "completed" && entry.status !== "aborted") {
1291
1707
  entry.status = "cancelling";
@@ -1310,6 +1726,11 @@ export function createDispatchBoardStore(
1310
1726
  return projectRows(true);
1311
1727
  },
1312
1728
  reconcile,
1729
+ setFleetPhase(runId: string, phase: DispatchBoardPhase): void {
1730
+ fleetPhases.set(runId, phase);
1731
+ const entry = entries.get(runId);
1732
+ if (entry) entry.phase = { ...phase };
1733
+ },
1313
1734
  unsubscribe() {
1314
1735
  if (closed) return;
1315
1736
  closed = true;