@iowarp/clio-coder 0.3.6 → 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 (258) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +16 -5
  3. package/dist/{acp-2BEHC4DL.js → acp-SK4MD6MM.js} +10 -10
  4. package/dist/{agents-LNNFTM53.js → agents-2FN2K6ME.js} +30 -25
  5. package/dist/assets/codewiki.json +1 -1
  6. package/dist/{auth-KXXFI2VS.js → auth-QIYZWM5I.js} +13 -13
  7. package/dist/{chunk-KHSFENX2.js → chunk-3HAPLH5M.js} +10 -10
  8. package/dist/{chunk-24I7BN55.js → chunk-465YSENW.js} +2 -2
  9. package/dist/{chunk-4OC57DA6.js → chunk-4DGYLA73.js} +53 -2
  10. package/dist/{chunk-CYQKWTG3.js → chunk-4DWFMQDR.js} +4 -4
  11. package/dist/{chunk-E2ER4LJF.js → chunk-5C3AQNDW.js} +25 -1
  12. package/dist/{chunk-22NAGB7X.js → chunk-5C77SEEY.js} +5 -94
  13. package/dist/{chunk-43AOLP7E.js → chunk-5FR74PWO.js} +2 -1
  14. package/dist/{chunk-K7T3E2SR.js → chunk-5UJ6ECTS.js} +10 -9
  15. package/dist/{chunk-6US73PDB.js → chunk-6M7VS3J3.js} +5 -5
  16. package/dist/{chunk-CJUB2JJ2.js → chunk-6TUKSZVF.js} +5 -5
  17. package/dist/{chunk-5JGRAMKL.js → chunk-AB4XIIVB.js} +8 -6
  18. package/dist/{chunk-R46L2BIR.js → chunk-BMWK7ZIZ.js} +14 -20
  19. package/dist/{chunk-4BPJXDWC.js → chunk-C4JBQ5SR.js} +30 -14
  20. package/dist/{chunk-XXQNGV4M.js → chunk-CEYBNUGC.js} +243 -63
  21. package/dist/{chunk-VEZEGCGW.js → chunk-D4MDIG46.js} +20 -18
  22. package/dist/chunk-DJNLUABN.js +843 -0
  23. package/dist/{chunk-RY3LY4J5.js → chunk-DMD2AGVS.js} +5 -4
  24. package/dist/{chunk-KOHPCX4K.js → chunk-DOOEX22V.js} +2 -2
  25. package/dist/chunk-DQA7QLMD.js +123 -0
  26. package/dist/chunk-DR52UMZW.js +21 -0
  27. package/dist/{chunk-XF5N4U5A.js → chunk-EBEFWSGL.js} +6 -5
  28. package/dist/{chunk-EYPA3EGJ.js → chunk-EELBMBT6.js} +120 -13
  29. package/dist/{chunk-CKXWIANG.js → chunk-EOOQZZDE.js} +16 -14
  30. package/dist/{chunk-WR67VIZY.js → chunk-FOT2FX5J.js} +63 -5
  31. package/dist/{chunk-FYYLNIL5.js → chunk-GH5622CP.js} +2 -2
  32. package/dist/chunk-GWS3VEIW.js +195 -0
  33. package/dist/{chunk-LYF7OHWH.js → chunk-J7PIKKWC.js} +8 -463
  34. package/dist/{chunk-NILBFAPG.js → chunk-JNXPYBB4.js} +2 -2
  35. package/dist/{chunk-4VP4KH3K.js → chunk-JRIO5UD2.js} +4 -4
  36. package/dist/{chunk-6XXKFVSN.js → chunk-JTSEDYVQ.js} +7 -7
  37. package/dist/{chunk-QKMUKYO7.js → chunk-KCMKRQX4.js} +236 -84
  38. package/dist/chunk-KZ2H5X4G.js +1026 -0
  39. package/dist/{chunk-QNQHSOLF.js → chunk-LADCF22A.js} +12 -12
  40. package/dist/chunk-M4AKACEO.js +382 -0
  41. package/dist/{chunk-XYDYPRZI.js → chunk-MXI6J5JF.js} +7 -7
  42. package/dist/{chunk-G7MUEIGA.js → chunk-OB5HIGJY.js} +1 -1
  43. package/dist/{chunk-EKY57CSP.js → chunk-OBMAI2DP.js} +61 -767
  44. package/dist/chunk-PD3MESLB.js +242 -0
  45. package/dist/{chunk-ZRGEBJ4T.js → chunk-QCTRSGHQ.js} +21 -21
  46. package/dist/chunk-RVG5JXAL.js +41 -0
  47. package/dist/{chunk-RD5U66HV.js → chunk-SROCI7ZU.js} +7 -7
  48. package/dist/{chunk-MFFY33HR.js → chunk-THKY7CD7.js} +466 -205
  49. package/dist/{chunk-PCZJO5TI.js → chunk-UFQ3F4FW.js} +13 -178
  50. package/dist/{chunk-AD2SYQYC.js → chunk-UHXRNZ2J.js} +121 -3
  51. package/dist/chunk-UND3GU2L.js +103 -0
  52. package/dist/{chunk-QM3F2GKX.js → chunk-UUANF5CR.js} +2247 -2096
  53. package/dist/chunk-UVDSQ6LW.js +472 -0
  54. package/dist/{chunk-DJVECN66.js → chunk-VQNODYQ4.js} +14 -14
  55. package/dist/{chunk-3BPUFZDL.js → chunk-VREKEFLL.js} +3 -3
  56. package/dist/{chunk-PBTHKCPN.js → chunk-WJHBC77E.js} +6 -6
  57. package/dist/{chunk-XE2VEJHX.js → chunk-X2KV5FXT.js} +2 -2
  58. package/dist/{chunk-ZXF4XRKW.js → chunk-XEGB6BCN.js} +157 -7
  59. package/dist/{chunk-E25LMLRW.js → chunk-YD734TPH.js} +2 -2
  60. package/dist/{verifiers-NCBTHHN2.js → chunk-YTYFXUI3.js} +65 -322
  61. package/dist/{chunk-OH3TOQTB.js → chunk-ZGH7FGS5.js} +13 -7
  62. package/dist/cli/index.js +32 -30
  63. package/dist/{clio-M2KGYUFZ.js → clio-WBVQEBKO.js} +7 -7
  64. package/dist/{code-nav-GQNL7XA6.js → code-nav-FGGFIE7L.js} +3 -3
  65. package/dist/{components-5TTYYX6G.js → components-F7OEATSO.js} +4 -4
  66. package/dist/{config-XUUYQIWO.js → config-TRBL3RCF.js} +34 -29
  67. package/dist/{configure-IHJ7YOMV.js → configure-OLCVPHNM.js} +15 -15
  68. package/dist/{context-74JLXAWD.js → context-MJIJ6GOX.js} +11 -11
  69. package/dist/{context-ZQ7SIFJV.js → context-WFPKQSM6.js} +19 -3
  70. package/dist/{context-75MIWW3U.js → context-XEWE3MOJ.js} +31 -26
  71. package/dist/{context-clear-GYKWNUML.js → context-clear-KNOS2JPB.js} +31 -26
  72. package/dist/{context-working-set-UX5KEP4J.js → context-working-set-EUXAZI6N.js} +8 -8
  73. package/dist/{dispatch-runner-GIJBHNFL.js → dispatch-runner-B7MTOVKL.js} +313 -53
  74. package/dist/{docs-6FZSCG5B.js → docs-FLJTIDSE.js} +4 -4
  75. package/dist/{doctor-SVJ5BZCW.js → doctor-RN4YKO2X.js} +14 -14
  76. package/dist/{eval-CG6LLBLD.js → eval-RUBJVSNQ.js} +8 -7
  77. package/dist/{evidence-ZYFIEN42.js → evidence-JZNBUOQZ.js} +30 -25
  78. package/dist/{evolve-QGEXEMDW.js → evolve-FJVC4KKI.js} +30 -25
  79. package/dist/{extensions-ADGNCJJD.js → extensions-IQL36S7K.js} +4 -4
  80. package/dist/{fleet-S5R4ZOQY.js → fleet-BDKYJFCP.js} +214 -360
  81. package/dist/fleet-commands-ZFIWZSB3.js +70 -0
  82. package/dist/fleet-graph-Y6HPXIVF.js +125 -0
  83. package/dist/fleet-new-RDVJLHHH.js +48 -0
  84. package/dist/fleet-validate-BIYREGIK.js +79 -0
  85. package/dist/{init-5DRU55YR.js → init-LQUB5COQ.js} +44 -37
  86. package/dist/library-NJAHIGG4.js +217 -0
  87. package/dist/{memory-7YKKR6UC.js → memory-OG6HOYKM.js} +31 -26
  88. package/dist/{models-ZPOLRU2C.js → models-5ZG5XY7J.js} +21 -20
  89. package/dist/{monitor-US5F5YGZ.js → monitor-TJ7AMTGB.js} +49 -30
  90. package/dist/{orchestrator-E2AL4T5N.js → orchestrator-WZYB54DM.js} +3827 -581
  91. package/dist/{paths-E7KYAQWE.js → paths-XUC7GS6E.js} +4 -4
  92. package/dist/{reset-KZ652EK6.js → reset-PXQT45IY.js} +7 -7
  93. package/dist/{run-SRNBKDWD.js → run-FQ74YF62.js} +53 -45
  94. package/dist/{share-CGZE33UP.js → share-FW7SVCL3.js} +33 -9
  95. package/dist/{skills-S2X4DLY5.js → skills-7E7IRB3R.js} +24 -8
  96. package/dist/{skills-eval-W2GGIC4R.js → skills-eval-LI75W6OK.js} +34 -27
  97. package/dist/{targets-54SWINWB.js → targets-4CIFKCTW.js} +23 -22
  98. package/dist/{terminal-lease-SAIF2OGY.js → terminal-lease-WUZY7ZV5.js} +4 -4
  99. package/dist/{uninstall-BVLWXKBT.js → uninstall-7FV7IP4E.js} +4 -4
  100. package/dist/{upgrade-JKAR27XC.js → upgrade-K2HVIVMQ.js} +20 -19
  101. package/dist/{usage-MSAWCLX4.js → usage-GTZELZQX.js} +116 -49
  102. package/dist/verifiers-RLAHT27O.js +336 -0
  103. package/dist/{verify-X5HDROLA.js → verify-BX3BRKH5.js} +7 -6
  104. package/dist/{wiki-generate-GUSOQ6ZP.js → wiki-generate-ASIFASCN.js} +45 -37
  105. package/dist/worker/entry.js +38 -35
  106. package/docs/README.md +3 -2
  107. package/docs/acp.md +1 -1
  108. package/docs/alcf-provider.md +1 -1
  109. package/docs/architecture.md +2 -2
  110. package/docs/artifact-versions.md +10 -6
  111. package/docs/built-in-agents.md +26 -2
  112. package/docs/capacity-and-scheduling.md +1 -1
  113. package/docs/commands-and-modes.md +82 -2
  114. package/docs/configuration-and-targets.md +79 -2
  115. package/docs/context-engine.md +1 -1
  116. package/docs/development-pipeline.md +1 -1
  117. package/docs/dispatch-architecture-rationale.md +1 -1
  118. package/docs/documentation-coverage.md +3 -3
  119. package/docs/documentation-guide.md +3 -3
  120. package/docs/eval-runner.md +1 -1
  121. package/docs/evals-internal.md +1 -1
  122. package/docs/evidence-and-memory.md +5 -5
  123. package/docs/evolution.md +1 -1
  124. package/docs/exit-codes-and-output.md +4 -1
  125. package/docs/extensions-and-sharing.md +6 -2
  126. package/docs/fleet-demo-runbook.md +2 -2
  127. package/docs/fleet-dispatch.md +197 -10
  128. package/docs/git-commit-provenance.md +2 -2
  129. package/docs/glossary.md +1 -1
  130. package/docs/installation-and-lifecycle.md +2 -2
  131. package/docs/middleware-and-components.md +2 -1
  132. package/docs/model-catalog.md +1 -1
  133. package/docs/observability.md +55 -8
  134. package/docs/proactive-memory.md +1 -1
  135. package/docs/prompt-envelope-and-tools.md +1 -1
  136. package/docs/provider-adapter-cookbook.md +1 -1
  137. package/docs/release-cut-checklist.md +79 -64
  138. package/docs/resource-library.md +59 -0
  139. package/docs/safety-model.md +2 -2
  140. package/docs/scientific-validation.md +3 -3
  141. package/docs/session-lifecycle.md +37 -1
  142. package/docs/skills-marketplace.md +16 -3
  143. package/docs/tool-usage.md +14 -7
  144. package/docs/trace-store.md +1 -1
  145. package/docs/troubleshooting.md +1 -1
  146. package/docs/tui-design.md +1 -1
  147. package/docs/worker-dispatch-mechanics.md +3 -3
  148. package/package.json +1 -1
  149. package/src/cli/fleet-commands.ts +37 -0
  150. package/src/cli/fleet-graph.ts +102 -0
  151. package/src/cli/fleet-new.ts +36 -0
  152. package/src/cli/fleet-preflight.ts +121 -0
  153. package/src/cli/fleet-validate.ts +30 -0
  154. package/src/cli/fleet.ts +173 -335
  155. package/src/cli/index.ts +3 -1
  156. package/src/cli/library.ts +190 -0
  157. package/src/cli/share.ts +13 -1
  158. package/src/cli/usage.ts +111 -19
  159. package/src/core/bus-events.ts +4 -0
  160. package/src/core/commit-attribution.ts +4 -4
  161. package/src/core/config.ts +130 -0
  162. package/src/core/defaults.ts +81 -0
  163. package/src/domains/agents/builtins/architect.md +1 -0
  164. package/src/domains/agents/builtins/oracle.md +33 -0
  165. package/src/domains/agents/catalog.ts +13 -1
  166. package/src/domains/agents/fleet-contract.ts +278 -16
  167. package/src/domains/agents/index.ts +14 -0
  168. package/src/domains/agents/result-contract.ts +235 -1
  169. package/src/domains/config/classify.ts +4 -0
  170. package/src/domains/dispatch/active-route-planner.ts +14 -0
  171. package/src/domains/dispatch/backoff.ts +2 -1
  172. package/src/domains/dispatch/capability-match.ts +1 -0
  173. package/src/domains/dispatch/checkout-writer-lease.ts +175 -0
  174. package/src/domains/dispatch/contract.ts +34 -0
  175. package/src/domains/dispatch/delegation-plan.ts +167 -0
  176. package/src/domains/dispatch/execution-plan.ts +76 -5
  177. package/src/domains/dispatch/execution-role.ts +3 -1
  178. package/src/domains/dispatch/execution-scheduler.ts +183 -67
  179. package/src/domains/dispatch/extension.ts +258 -9
  180. package/src/domains/dispatch/fleet-gate.ts +14 -0
  181. package/src/domains/dispatch/fleet-plan.ts +63 -3
  182. package/src/domains/dispatch/fleet-run.ts +737 -0
  183. package/src/domains/dispatch/gate-role-prompts.ts +9 -0
  184. package/src/domains/dispatch/host-verification.ts +178 -0
  185. package/src/domains/dispatch/index.ts +38 -0
  186. package/src/domains/dispatch/intent.ts +159 -0
  187. package/src/domains/dispatch/receipt-integrity.ts +8 -4
  188. package/src/domains/dispatch/state.ts +36 -3
  189. package/src/domains/dispatch/types.ts +51 -6
  190. package/src/domains/dispatch/validation.ts +66 -6
  191. package/src/domains/evidence/trust-status.ts +10 -1
  192. package/src/domains/middleware/index.ts +15 -0
  193. package/src/domains/middleware/watchdog.ts +281 -0
  194. package/src/domains/observability/contract.ts +3 -1
  195. package/src/domains/observability/cost.ts +12 -1
  196. package/src/domains/observability/extension.ts +2 -2
  197. package/src/domains/observability/index.ts +10 -0
  198. package/src/domains/observability/out-of-turn-usage.ts +223 -0
  199. package/src/domains/resources/index.ts +20 -0
  200. package/src/domains/resources/library.ts +326 -0
  201. package/src/domains/resources/skills/marketplace.ts +37 -12
  202. package/src/domains/session/handoff.ts +629 -0
  203. package/src/domains/share/archive.ts +67 -2
  204. package/src/entry/orchestrator.ts +37 -0
  205. package/src/interactive/bus-notices.ts +26 -0
  206. package/src/interactive/chat-loop.ts +235 -1
  207. package/src/interactive/chat-renderer.ts +22 -0
  208. package/src/interactive/cost-overlay.ts +31 -3
  209. package/src/interactive/council-dispatch.ts +30 -0
  210. package/src/interactive/council-grid.ts +213 -0
  211. package/src/interactive/council.ts +99 -0
  212. package/src/interactive/dispatch-board.ts +260 -16
  213. package/src/interactive/fleet-run-preview.ts +307 -0
  214. package/src/interactive/footer/notifications.ts +219 -0
  215. package/src/interactive/handoff-round.ts +56 -0
  216. package/src/interactive/interactive-application.ts +43 -1
  217. package/src/interactive/interactive-event-projection.ts +9 -1
  218. package/src/interactive/interactive-slash-runtime.ts +52 -2
  219. package/src/interactive/interactive-subscriptions.ts +14 -2
  220. package/src/interactive/oracle.ts +179 -0
  221. package/src/interactive/overlay-ask-user-lifecycle.ts +6 -0
  222. package/src/interactive/overlay-general-openers.ts +190 -1
  223. package/src/interactive/overlay-key-routing.ts +17 -1
  224. package/src/interactive/overlay-lifecycle.ts +41 -1
  225. package/src/interactive/overlay-permission-lifecycle.ts +10 -0
  226. package/src/interactive/overlay-resource-openers.ts +11 -3
  227. package/src/interactive/overlay-session-lifecycle.ts +234 -2
  228. package/src/interactive/overlays/fleet-run-approval.ts +208 -0
  229. package/src/interactive/overlays/handoff-review.ts +185 -0
  230. package/src/interactive/overlays/library-install-confirm.ts +151 -0
  231. package/src/interactive/overlays/list-overlay.ts +168 -2
  232. package/src/interactive/overlays/settings.ts +101 -4
  233. package/src/interactive/overlays/side-question.ts +139 -0
  234. package/src/interactive/overlays/skills-hub.ts +401 -15
  235. package/src/interactive/side-question.ts +171 -0
  236. package/src/interactive/slash-commands.ts +432 -5
  237. package/src/interactive/slash-spec.ts +19 -6
  238. package/src/interactive/theme/tokens.ts +30 -0
  239. package/src/interactive/turn-middleware.ts +15 -1
  240. package/src/interactive/watchdog-run.ts +75 -0
  241. package/src/interactive/worker-share.ts +56 -1
  242. package/src/interactive/worker-stream.ts +7 -0
  243. package/src/tools/bootstrap.ts +3 -0
  244. package/src/tools/compete-worktrees.ts +13 -79
  245. package/src/tools/dispatch-admission.ts +242 -8
  246. package/src/tools/dispatch-arguments.ts +57 -1
  247. package/src/tools/dispatch-plan.ts +136 -6
  248. package/src/tools/dispatch-runner.ts +319 -13
  249. package/src/tools/dispatch-types.ts +20 -1
  250. package/src/tools/dispatch.ts +72 -2
  251. package/src/tools/monitor.ts +16 -0
  252. package/src/tools/profiles.ts +18 -4
  253. package/src/tools/task-worktree.ts +238 -0
  254. package/src/tools/verify/authoring.ts +61 -1
  255. package/src/tools/verify/scripts.ts +62 -0
  256. package/src/tools/worker-evidence.ts +2 -1
  257. package/src/worker/spec-contract.ts +1 -0
  258. package/dist/chunk-HC4CLZ2Y.js +0 -68
@@ -35,6 +35,13 @@ import type { CostProvenance } from "../domains/providers/index.js";
35
35
  import { sanitizeCallTargetText } from "../domains/safety/call-target.js";
36
36
  import { type Component, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../engine/tui.js";
37
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";
38
45
  import { formatFooterTokens } from "./footer-panel.js";
39
46
  import {
40
47
  type ClioTheme,
@@ -103,6 +110,9 @@ export interface DispatchBoardRow {
103
110
  node?: string;
104
111
  /** Review/compete gate badge (role + cycle). */
105
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";
106
116
  /** Dead-node failover hops recorded on this run's chain. */
107
117
  rerouteCount?: number;
108
118
  /** Assignment retry attempts observed on the assignment event stream. */
@@ -134,8 +144,39 @@ export interface DispatchBoardRow {
134
144
  retry?: DispatchRetryPresentation;
135
145
  /** Most recent worker delivery acknowledgement for an operator steer. */
136
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;
154
+ }
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);
137
173
  }
138
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
+
139
180
  /** Live HTTP/SDK rows are the only rows whose runtime can consume operator guidance. */
140
181
  export function isDispatchBoardRowSteerable(row: DispatchBoardRow): boolean {
141
182
  return (
@@ -610,14 +651,26 @@ export function renderDispatchCard(
610
651
  `${theme.fg("dim", "ttft")} ${theme.fg("muted", ttft)}`,
611
652
  `${theme.fg("dim", "cost")} ${theme.fg("muted", cost)}`,
612
653
  ];
654
+ const hostVerification = theme.fg("info", `host_verification=${row.hostVerification ?? "not_requested"}`);
613
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);
614
660
  const bodyLines = [
615
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
+ ),
616
668
  targetLine,
617
669
  ...(row.taskSummary
618
670
  ? [truncateToWidth(`${cardKvKey(theme, "task")}${theme.fg("muted", row.taskSummary)}`, contentWidth, "…", false)]
619
671
  : []),
620
672
  cardUnitsLine(theme, "status", statusUnits, contentWidth),
673
+ cardUnitsLine(theme, "evidence", [hostVerification], contentWidth),
621
674
  ...(row.budget !== undefined
622
675
  ? [
623
676
  truncateToWidth(
@@ -715,11 +768,25 @@ function renderTaskIslandRow(row: DispatchBoardRow, width: number): string[] {
715
768
  visibleWidth(presentation.label) +
716
769
  3 +
717
770
  visibleWidth(elapsed);
718
- 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
+ );
719
786
 
720
787
  // The agent label drops its accent color for plain bold; the status word is
721
788
  // the only status-colored element on the row, with dim middot separators.
722
- 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 : ""}`;
723
790
 
724
791
  const elapsedSec = row.elapsedMs / 1000;
725
792
  const tokensPerSec = elapsedSec > 0.1 ? Math.round(row.outputTokens / elapsedSec) : 0;
@@ -740,6 +807,131 @@ function renderTaskIslandRow(row: DispatchBoardRow, width: number): string[] {
740
807
  return [padAnsi(line1, width), ...(task ? [padAnsi(task, width)] : []), padAnsi(telemetry, width)];
741
808
  }
742
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
+
743
935
  export function formatDispatchBoardLines(
744
936
  rows: ReadonlyArray<DispatchBoardRow>,
745
937
  width = 76,
@@ -757,11 +949,15 @@ export function formatDispatchBoardLines(
757
949
  });
758
950
  }
759
951
 
760
- const cards = rows.map((row) =>
761
- renderDispatchCard(row, width, deriveRunEvidenceState(observability, row.runId), {
762
- selected: row.runId === selectedRunId,
763
- expanded: detailExpanded && row.runId === selectedRunId,
764
- }),
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
+ }),
765
961
  );
766
962
  const body: string[] = [];
767
963
  for (const card of cards) {
@@ -850,23 +1046,30 @@ export function createDispatchBoardView(
850
1046
  }
851
1047
 
852
1048
  export function formatTaskIslandLines(rows: ReadonlyArray<DispatchBoardRow>, maxRows = 4): string[] {
853
- 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));
854
1053
  const body: string[] = [];
855
1054
 
856
- if (visibleRows.length === 0) {
1055
+ if (visibleItems.length === 0) {
857
1056
  const theme = clioTheme();
858
1057
  body.push(theme.fg("dim", "No active fleet runs."));
859
1058
  body.push(theme.fg("dim", "Use /run or /delegate to spawn agents."));
860
1059
  } else {
861
- for (let i = 0; i < visibleRows.length; i++) {
862
- const row = visibleRows[i];
863
- if (!row) continue;
1060
+ for (let i = 0; i < visibleItems.length; i++) {
1061
+ const item = visibleItems[i];
1062
+ if (!item) continue;
864
1063
  if (i > 0) {
865
1064
  body.push(innerDivider(clioTheme(), TASK_ISLAND_WIDTH));
866
1065
  }
867
- 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
+ );
868
1071
  }
869
- const hidden = rows.length - visibleRows.length;
1072
+ const hidden = items.length - visibleItems.length;
870
1073
  if (hidden > 0) {
871
1074
  body.push(innerDivider(clioTheme(), TASK_ISLAND_WIDTH));
872
1075
  body.push(clioTheme().fg("dim", `+ ${hidden} more`));
@@ -932,6 +1135,21 @@ function parseGateBadge(value: unknown): { role: string; cycle: number } | undef
932
1135
  return { role: record.role, cycle };
933
1136
  }
934
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
+
935
1153
  function parseFiniteNumberOrZero(value: unknown): number {
936
1154
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
937
1155
  }
@@ -1099,11 +1317,14 @@ function toRow(entry: DispatchBoardEntry, now: number): DispatchBoardRow {
1099
1317
  ...(entry.outcomeDetail !== undefined ? { outcomeDetail: entry.outcomeDetail } : {}),
1100
1318
  ...(entry.node !== undefined ? { node: entry.node } : {}),
1101
1319
  ...(entry.gate !== undefined ? { gate: { ...entry.gate } } : {}),
1320
+ ...(entry.council !== undefined ? { council: { ...entry.council } } : {}),
1321
+ ...(entry.hostVerification !== undefined ? { hostVerification: entry.hostVerification } : {}),
1102
1322
  ...(entry.rerouteCount !== undefined ? { rerouteCount: entry.rerouteCount } : {}),
1103
1323
  ...(entry.failoverHops !== undefined ? { failoverHops: entry.failoverHops } : {}),
1104
1324
  ...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {}),
1105
1325
  lastContextTokens: entry.lastContextTokens,
1106
1326
  ...(entry.receiptId !== undefined ? { receiptId: entry.receiptId } : {}),
1327
+ ...(entry.phase !== undefined ? { phase: { ...entry.phase } } : {}),
1107
1328
  // Both tool fields are views of the one projection, so the compact card and
1108
1329
  // the expanded detail can never disagree about what is running.
1109
1330
  currentTool: retry ? null : (progress.currentAction?.tool ?? null),
@@ -1124,7 +1345,7 @@ function sortEntries(a: DispatchBoardEntry, b: DispatchBoardEntry): number {
1124
1345
  return a.sequence - b.sequence;
1125
1346
  }
1126
1347
 
1127
- function pruneEntries(entries: Map<string, DispatchBoardEntry>): void {
1348
+ function pruneEntries(entries: Map<string, DispatchBoardEntry>, onEvict?: (runId: string) => void): void {
1128
1349
  if (entries.size <= MAX_DISPATCH_BOARD_ROWS) return;
1129
1350
  const terminalEntries = [...entries.values()]
1130
1351
  .filter((entry) => !entry.retry && isTerminalStatus(entry.status))
@@ -1134,6 +1355,7 @@ function pruneEntries(entries: Map<string, DispatchBoardEntry>): void {
1134
1355
  for (const entry of evictionQueue) {
1135
1356
  if (entries.size <= MAX_DISPATCH_BOARD_ROWS) break;
1136
1357
  entries.delete(entry.runId);
1358
+ onEvict?.(entry.runId);
1137
1359
  }
1138
1360
  }
1139
1361
 
@@ -1150,9 +1372,19 @@ export function createDispatchBoardStore(
1150
1372
  rows(): ReadonlyArray<DispatchBoardRow>;
1151
1373
  activeRows(): ReadonlyArray<DispatchBoardRow>;
1152
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;
1153
1382
  unsubscribe(): void;
1154
1383
  } {
1155
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>();
1156
1388
  let nextSequence = 0;
1157
1389
  let reconciledAtMs = Date.now();
1158
1390
 
@@ -1180,10 +1412,12 @@ export function createDispatchBoardStore(
1180
1412
  const requestOrigin = parseRequestOrigin(raw.requestOrigin, previous?.requestOrigin);
1181
1413
  const node = parseNonEmptyString(raw.node) ?? previous?.node;
1182
1414
  const gate = parseGateBadge(raw.gate) ?? previous?.gate;
1415
+ const council = parseCouncilBadge(raw.council) ?? previous?.council;
1183
1416
  const rerouteCount = parsePositiveInt(raw.rerouteCount) ?? previous?.rerouteCount;
1184
1417
  const contextWindow = parsePositiveInt(raw.contextWindow) ?? previous?.contextWindow;
1185
1418
  const taskSummary = parseTaskSummary(raw, previous?.taskSummary);
1186
1419
  const budget = cloneRunToolBudgetEnvelope(raw.budget) ?? previous?.budget;
1420
+ const phase = fleetPhases.get(runId) ?? previous?.phase;
1187
1421
  const entry: DispatchBoardEntry = {
1188
1422
  runId,
1189
1423
  agentId: parseText(raw.agentId, previous?.agentId ?? "-"),
@@ -1211,16 +1445,19 @@ export function createDispatchBoardStore(
1211
1445
  outcomeDetail: previous?.outcomeDetail ?? null,
1212
1446
  ...(node !== undefined ? { node } : {}),
1213
1447
  ...(gate !== undefined ? { gate } : {}),
1448
+ ...(council !== undefined ? { council } : {}),
1449
+ ...(previous?.hostVerification !== undefined ? { hostVerification: previous.hostVerification } : {}),
1214
1450
  ...(rerouteCount !== undefined ? { rerouteCount } : {}),
1215
1451
  ...(contextWindow !== undefined ? { contextWindow } : {}),
1216
1452
  lastContextTokens: previous?.lastContextTokens ?? 0,
1217
1453
  ...(previous?.receiptId !== undefined ? { receiptId: previous.receiptId } : {}),
1454
+ ...(phase !== undefined ? { phase } : {}),
1218
1455
  progress: previous?.progress ?? createWorkerProgressFold(),
1219
1456
  ...(previous?.retry ? { retry: { ...previous.retry } } : {}),
1220
1457
  ...(previous?.steerAcknowledgement ? { steerAcknowledgement: { ...previous.steerAcknowledgement } } : {}),
1221
1458
  };
1222
1459
  entries.set(runId, entry);
1223
- pruneEntries(entries);
1460
+ pruneEntries(entries, (evicted) => fleetPhases.delete(evicted));
1224
1461
  return entry;
1225
1462
  };
1226
1463
 
@@ -1251,6 +1488,7 @@ export function createDispatchBoardStore(
1251
1488
  entry.costUsd = parseFiniteNumber(payload.costUsd, entry.costUsd);
1252
1489
  entry.costProvenance = payload.costProvenance ?? "unknown";
1253
1490
  entry.outcomeDetail = null;
1491
+ if (payload.hostVerification !== undefined) entry.hostVerification = payload.hostVerification;
1254
1492
  settleFromReceipt(entry);
1255
1493
  // A terminal dispatch event is published only after the run's receipt is
1256
1494
  // sealed at receipts/<runId>.json, so the run id is the receipt id here.
@@ -1279,6 +1517,7 @@ export function createDispatchBoardStore(
1279
1517
  entry.costUsd = parseFiniteNumber(payload.costUsd, entry.costUsd);
1280
1518
  entry.costProvenance = payload.costProvenance ?? "unknown";
1281
1519
  entry.outcomeDetail = resolveFailureDetail(payload, entry.outcomeDetail);
1520
+ if (payload.hostVerification !== undefined) entry.hostVerification = payload.hostVerification;
1282
1521
  settleFromReceipt(entry);
1283
1522
  // A denied retry never reached a run, so no receipt was sealed for it;
1284
1523
  // every other failure finalized through recordReceipt like a success.
@@ -1487,6 +1726,11 @@ export function createDispatchBoardStore(
1487
1726
  return projectRows(true);
1488
1727
  },
1489
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
+ },
1490
1734
  unsubscribe() {
1491
1735
  if (closed) return;
1492
1736
  closed = true;