@iowarp/clio-coder 0.3.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (552) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/NOTICE +33 -0
  3. package/README.md +12 -3
  4. package/dist/{acp-U67UHUK2.js → acp-G5WJBNCT.js} +13 -12
  5. package/dist/{agents-YU6SGALZ.js → agents-FMV2Q5G4.js} +41 -36
  6. package/dist/assets/codewiki.json +1 -1
  7. package/dist/{auth-5ZPJOIVG.js → auth-3IDSJEIK.js} +18 -18
  8. package/dist/{builtins-C6JMZVV6.js → builtins-XCZWXSC7.js} +5 -5
  9. package/dist/{chunk-IFBNV6H6.js → chunk-2ANTL7MR.js} +3 -3
  10. package/dist/{chunk-5FR74PWO.js → chunk-2JDWVJND.js} +2 -2
  11. package/dist/{chunk-XWSF374K.js → chunk-2OQE55CK.js} +3 -3
  12. package/dist/{chunk-5H3GB5BO.js → chunk-32KWKNSF.js} +8 -384
  13. package/dist/{chunk-KTYTFRMB.js → chunk-36EJLSQQ.js} +34 -36
  14. package/dist/{chunk-FHJEP5SW.js → chunk-3BT2XMV4.js} +19 -13
  15. package/dist/chunk-3DPEIQKN.js +113 -0
  16. package/dist/{chunk-GU2UIAFZ.js → chunk-3URVFKWK.js} +7 -7
  17. package/dist/{chunk-WEH5XRJQ.js → chunk-3XML7CDN.js} +3 -3
  18. package/dist/chunk-42FMPA75.js +101 -0
  19. package/dist/{chunk-A2NJGIB3.js → chunk-5LXZXPKX.js} +2 -2
  20. package/dist/{chunk-VCBR6CU7.js → chunk-5PSMVOLM.js} +2 -2
  21. package/dist/{chunk-NMPKI6XL.js → chunk-6DB53AJS.js} +203 -25
  22. package/dist/{chunk-3BINW3FP.js → chunk-76ONBSIA.js} +2 -2
  23. package/dist/{chunk-26LEYJZH.js → chunk-7MCTRUCE.js} +2 -2
  24. package/dist/{chunk-TYPGUK6W.js → chunk-AB44T6BB.js} +111 -5
  25. package/dist/{chunk-K4XHGFR5.js → chunk-B7OBL7PK.js} +317 -721
  26. package/dist/{chunk-B5CSFE7B.js → chunk-BBVJUZHB.js} +2 -2
  27. package/dist/{chunk-EQ63NRB7.js → chunk-BBVYXMFO.js} +2 -2
  28. package/dist/{chunk-ZVJ5BLO2.js → chunk-BKFJHQCA.js} +154 -16
  29. package/dist/chunk-BKFM6EJV.js +462 -0
  30. package/dist/chunk-BMS5RKQY.js +27 -0
  31. package/dist/{chunk-ZNLWCMVZ.js → chunk-BPKCPIL7.js} +2 -2
  32. package/dist/{chunk-TLQJPP24.js → chunk-BUMFYQFY.js} +1394 -1349
  33. package/dist/{chunk-BNAZZHFG.js → chunk-BYP5D4HI.js} +1 -1
  34. package/dist/chunk-C2LTL2W6.js +2447 -0
  35. package/dist/{chunk-ME6CCNFO.js → chunk-CODPRO7Q.js} +8 -8
  36. package/dist/{chunk-E77JEWSD.js → chunk-CTJ4RNAA.js} +7 -37
  37. package/dist/{chunk-ODFEOB4F.js → chunk-CY6FY24N.js} +26 -8
  38. package/dist/{chunk-7RFXX52T.js → chunk-DQITNCXG.js} +642 -172
  39. package/dist/{chunk-TTHACPOM.js → chunk-DYJP44XW.js} +578 -117
  40. package/dist/{chunk-2HFZQUHL.js → chunk-F4CKPOEQ.js} +18 -8
  41. package/dist/{chunk-DGSYXYMX.js → chunk-FEFIFZTL.js} +3 -3
  42. package/dist/{chunk-GWS3VEIW.js → chunk-FWDFM5ZU.js} +24 -3
  43. package/dist/{chunk-MV3K5QF2.js → chunk-GCSMB2KY.js} +2 -2
  44. package/dist/chunk-GKF55TAZ.js +391 -0
  45. package/dist/{chunk-VWZOAB7K.js → chunk-GR5G2PVF.js} +9 -8
  46. package/dist/{chunk-4DGYLA73.js → chunk-GXNLGKAB.js} +80 -9
  47. package/dist/chunk-HHV2GANA.js +88 -0
  48. package/dist/{chunk-IIZWH4XA.js → chunk-HI63TFOG.js} +5 -4
  49. package/dist/{chunk-WLFILSD5.js → chunk-HJJTYUHX.js} +113 -83
  50. package/dist/{chunk-TT36MB5S.js → chunk-HLW2MRKE.js} +3 -1
  51. package/dist/{chunk-PMDBGQSJ.js → chunk-HWHKMHUA.js} +7 -7
  52. package/dist/chunk-HZHHCK24.js +1631 -0
  53. package/dist/{chunk-WSB3FPX7.js → chunk-I5VEOC6I.js} +39 -143
  54. package/dist/chunk-IBEBSCYA.js +564 -0
  55. package/dist/chunk-IQ7KR472.js +362 -0
  56. package/dist/{chunk-A3WNZD3P.js → chunk-J4W7KFM7.js} +949 -972
  57. package/dist/{chunk-TB5666IT.js → chunk-JDG2WCRO.js} +5 -5
  58. package/dist/{chunk-N22QMJKY.js → chunk-K5C3NCBD.js} +4 -4
  59. package/dist/{chunk-CGKSTWHD.js → chunk-K6BSR66V.js} +2 -1
  60. package/dist/{chunk-5C3AQNDW.js → chunk-KFZI4NIL.js} +216 -38
  61. package/dist/chunk-KMVISBZR.js +132 -0
  62. package/dist/{chunk-WXY7KU3G.js → chunk-LDQ2ZF2M.js} +2 -2
  63. package/dist/{chunk-XN3L4EYL.js → chunk-LQ3DZAMX.js} +3 -3
  64. package/dist/{chunk-U6MBIEMB.js → chunk-LY4S7GJC.js} +173 -144
  65. package/dist/{chunk-4SPRNWDE.js → chunk-MLKNTWH2.js} +19 -19
  66. package/dist/{chunk-MVVUPGPW.js → chunk-MXHC5QYU.js} +6 -6
  67. package/dist/chunk-NEKRRTYW.js +56 -0
  68. package/dist/{chunk-SPULKLCF.js → chunk-NHCZP4K7.js} +3 -3
  69. package/dist/chunk-NHLBIGRH.js +1506 -0
  70. package/dist/chunk-NQQH3YT7.js +302 -0
  71. package/dist/chunk-NYS75XW5.js +15 -0
  72. package/dist/{chunk-GN57SG4G.js → chunk-O4XIVISU.js} +10 -8
  73. package/dist/{chunk-EMYUUSFG.js → chunk-O6TL7WWY.js} +6 -6
  74. package/dist/chunk-OQBA45DZ.js +97 -0
  75. package/dist/{chunk-LU7P4LHA.js → chunk-P3FOHJT4.js} +2 -2
  76. package/dist/chunk-PMZCIOCJ.js +25 -0
  77. package/dist/{chunk-I4HZDVNP.js → chunk-PQEFIJ36.js} +2 -2
  78. package/dist/{chunk-J3YUBZWY.js → chunk-QBJA7R7N.js} +62 -6
  79. package/dist/chunk-QDC3K2U3.js +262 -0
  80. package/dist/{chunk-2HEJ2F35.js → chunk-QLFS5GO2.js} +22 -10
  81. package/dist/{chunk-7RGZWPB6.js → chunk-QLL7ILRG.js} +95 -32
  82. package/dist/{chunk-YS5VLNH5.js → chunk-QREDIESB.js} +6 -6
  83. package/dist/chunk-QSNYB6ZV.js +195 -0
  84. package/dist/{chunk-GOXNB3AO.js → chunk-RAPCMZL4.js} +75 -4
  85. package/dist/chunk-RKKLTLYB.js +45 -0
  86. package/dist/{chunk-P43ETTHK.js → chunk-SJ5ZKQ4S.js} +2 -2
  87. package/dist/{chunk-GPIEI3LY.js → chunk-SP2RXXYO.js} +6 -54
  88. package/dist/chunk-SUCTJL45.js +45 -0
  89. package/dist/{chunk-DYIM5TJT.js → chunk-SUW5DORT.js} +263 -7
  90. package/dist/chunk-T56WDKA5.js +183 -0
  91. package/dist/chunk-TVHHYFHE.js +255 -0
  92. package/dist/{chunk-FJ3H4MN5.js → chunk-TZ3SGWZZ.js} +3 -3
  93. package/dist/{chunk-MXKJU4JB.js → chunk-U77AMWDL.js} +91 -10
  94. package/dist/{chunk-5DHKRSMQ.js → chunk-ULC6OTWO.js} +11 -7
  95. package/dist/{chunk-RWSI4YD7.js → chunk-UM7N4G5A.js} +33 -12
  96. package/dist/{chunk-FCSXB6T2.js → chunk-UOSL25KY.js} +14 -2
  97. package/dist/{chunk-IGWKHNIQ.js → chunk-UXMFQ54G.js} +44 -37
  98. package/dist/{chunk-HFSBBKSQ.js → chunk-V5DHCITQ.js} +171 -3
  99. package/dist/{chunk-5WIGXA4T.js → chunk-VAZSBTKF.js} +111 -4
  100. package/dist/{chunk-VHN4MY6O.js → chunk-VEO4AP2K.js} +2 -2
  101. package/dist/{chunk-IJ7RPIYJ.js → chunk-VFA6GDY5.js} +65 -4
  102. package/dist/{chunk-PT7HYKEM.js → chunk-VO2LKSTM.js} +2 -2
  103. package/dist/chunk-VO67MWHC.js +75 -0
  104. package/dist/{chunk-XK56QHLX.js → chunk-VPKWYKEY.js} +19 -5
  105. package/dist/{chunk-TANS5ZJS.js → chunk-VYMXRQI6.js} +36 -22
  106. package/dist/{chunk-WWCZ5F23.js → chunk-W5VSYASO.js} +77 -16
  107. package/dist/{chunk-WNIJTQQK.js → chunk-WZR7K7ZX.js} +72 -116
  108. package/dist/{chunk-5Q2VVUKB.js → chunk-X3YGUTOB.js} +4 -4
  109. package/dist/chunk-X75E3D2N.js +686 -0
  110. package/dist/{chunk-VAWNZU7Z.js → chunk-YDFRH54B.js} +4 -4
  111. package/dist/chunk-YJX4SHTD.js +40 -0
  112. package/dist/{chunk-ZI647VB5.js → chunk-YPI3QQCF.js} +2 -2
  113. package/dist/chunk-Z2RR6MAK.js +127 -0
  114. package/dist/{chunk-FBVTI2TJ.js → chunk-Z4TXYIEG.js} +12 -131
  115. package/dist/cli/index.js +47 -36
  116. package/dist/{clio-QVTYJ57A.js → clio-2JXHBBY5.js} +7 -7
  117. package/dist/{code-nav-FGGFIE7L.js → code-nav-3YYRMYNF.js} +8 -8
  118. package/dist/{compile-cache-CVJMMODC.js → compile-cache-7FPE6PS3.js} +3 -3
  119. package/dist/{components-ZFA3SAER.js → components-RYZV4JGP.js} +5 -5
  120. package/dist/{config-LW5IJFQN.js → config-QZPCMYSO.js} +99 -63
  121. package/dist/{configure-7XIZCOU4.js → configure-TEGEBYCA.js} +23 -22
  122. package/dist/{context-Y6Y7QPR6.js → context-AV7OEZ4D.js} +12 -12
  123. package/dist/{context-L3WL3X7K.js → context-E6H5RNMC.js} +56 -47
  124. package/dist/{context-N52ZA626.js → context-GSXUE4CT.js} +29 -27
  125. package/dist/{context-clear-MBQRLSDQ.js → context-clear-SHIBYK6T.js} +55 -46
  126. package/dist/{context-index-HVMFQHK3.js → context-index-HNG3MOME.js} +2 -2
  127. package/dist/{context-working-set-GS6DSO7F.js → context-working-set-5ZGKPGZQ.js} +13 -13
  128. package/dist/{dispatch-runner-22ZCNOM3.js → dispatch-runner-EFMJT4LD.js} +93 -64
  129. package/dist/{docs-7LQ23DLM.js → docs-23KQS3XK.js} +5 -5
  130. package/dist/doctor-QOA5FNY5.js +313 -0
  131. package/dist/{eval-BEC2WHDA.js → eval-TFBYQH4H.js} +2032 -156
  132. package/dist/eval-inventory-SXH7PDKX.js +316 -0
  133. package/dist/{evidence-REJUMSKM.js → evidence-ERGESKGN.js} +203 -48
  134. package/dist/{evolve-PY5ZBA5K.js → evolve-VDXTSYCJ.js} +52 -43
  135. package/dist/{extensions-HVKU65YU.js → extensions-7BGBHN57.js} +13 -7
  136. package/dist/{fleet-7WZEWRFA.js → fleet-2RRVDF2V.js} +228 -108
  137. package/dist/{fleet-commands-UVHWM76J.js → fleet-commands-VJ726XIA.js} +11 -11
  138. package/dist/fleet-decisions-EPAPM3XJ.js +157 -0
  139. package/dist/{fleet-graph-6ULH7PES.js → fleet-graph-JF5QOATM.js} +18 -15
  140. package/dist/fleet-inspect-VLY4S7QM.js +442 -0
  141. package/dist/{fleet-preflight-J53T6CCE.js → fleet-preflight-AIZUEJOY.js} +5 -5
  142. package/dist/{fleet-validate-72PC4SLA.js → fleet-validate-AJRPDMDV.js} +22 -19
  143. package/dist/fleet-verify-JFEL2L3H.js +175 -0
  144. package/dist/fleet-view-ZCON35AG.js +102 -0
  145. package/dist/{init-OG3TPGQG.js → init-DN2WWLFE.js} +72 -62
  146. package/dist/install-XGLBQY5E.js +13 -0
  147. package/dist/interop-OZBKXAYL.js +114 -0
  148. package/dist/{library-CNTMPLRF.js → library-YWZG7IMW.js} +21 -18
  149. package/dist/{memory-6IS7F275.js → memory-I4C4HMLW.js} +54 -45
  150. package/dist/{models-ENRJDA5W.js → models-CEYXJBO6.js} +33 -30
  151. package/dist/{monitor-XLDVO7TN.js → monitor-NZ6GCI3P.js} +59 -52
  152. package/dist/{orchestrator-6KSPYRHA.js → orchestrator-GCGQ4N5I.js} +7827 -7328
  153. package/dist/panes-HMABYVO4.js +58 -0
  154. package/dist/panes-KY6W3V2E.js +103 -0
  155. package/dist/{paths-DBXMZMDU.js → paths-II4K7DNR.js} +5 -5
  156. package/dist/{reset-RZ4ER727.js → reset-DQ6FGCSH.js} +13 -11
  157. package/dist/resources-BB3MVJMD.js +111 -0
  158. package/dist/{run-Y2CNK5RU.js → run-H2GQDUER.js} +119 -87
  159. package/dist/{share-A55GYP6Z.js → share-GTJN6A5O.js} +20 -17
  160. package/dist/{skills-ALC5J6AT.js → skills-L55TEW6R.js} +33 -24
  161. package/dist/{skills-eval-JPBEBYQU.js → skills-eval-XVXPH2JI.js} +67 -56
  162. package/dist/skills-inventory-S4MXPJFV.js +126 -0
  163. package/dist/slash-commands-ZSGASKJC.js +77 -0
  164. package/dist/{steer-GGWFUJUD.js → steer-RZGSCY4R.js} +4 -4
  165. package/dist/{support-MIETYA5E.js → support-PKEUNNQL.js} +6 -6
  166. package/dist/{targets-VGNXIR3S.js → targets-NCPZ644J.js} +68 -40
  167. package/dist/{terminal-lease-WOBR64YA.js → terminal-lease-44SV3YCN.js} +6 -4
  168. package/dist/tools-DAF3DI3C.js +27 -0
  169. package/dist/{trace-PNCASAXC.js → trace-FYVW2MQA.js} +207 -10
  170. package/dist/tui-primitives-2AKXQNZK.js +13 -0
  171. package/dist/{uninstall-ZJF5H5ZN.js → uninstall-DW2PNOIC.js} +5 -5
  172. package/dist/{upgrade-FUSUAGHR.js → upgrade-3XPP6OQL.js} +27 -24
  173. package/dist/{usage-N4MKVHKD.js → usage-3NLHGTU2.js} +114 -62
  174. package/dist/{verifiers-YAWOJ3H2.js → verifiers-SSQONKRT.js} +172 -13
  175. package/dist/{verify-LTDHYBGY.js → verify-3U6J7FZI.js} +10 -10
  176. package/dist/{web-fetch-2YHJ3KTG.js → web-fetch-S7RR6GZ7.js} +3 -3
  177. package/dist/{wiki-generate-6M7GHTBJ.js → wiki-generate-CEHYGPGQ.js} +75 -65
  178. package/dist/with-panes-MKB46MPQ.js +782 -0
  179. package/dist/worker/entry.js +106 -75
  180. package/docs/README.md +3 -2
  181. package/docs/acp.md +24 -3
  182. package/docs/alcf-provider.md +1 -1
  183. package/docs/architecture.md +2 -2
  184. package/docs/artifact-versions.md +9 -1
  185. package/docs/built-in-agents.md +1 -1
  186. package/docs/capacity-and-scheduling.md +62 -3
  187. package/docs/commands-and-modes.md +31 -2
  188. package/docs/configuration-and-targets.md +69 -12
  189. package/docs/context-engine.md +63 -4
  190. package/docs/development-pipeline.md +19 -0
  191. package/docs/dispatch-typed-intent.md +385 -0
  192. package/docs/documentation-coverage.md +5 -5
  193. package/docs/documentation-guide.md +1 -1
  194. package/docs/environment-variables.md +3 -0
  195. package/docs/eval-runner.md +262 -11
  196. package/docs/evals-internal.md +72 -2
  197. package/docs/evidence-and-memory.md +12 -11
  198. package/docs/evolution.md +1 -1
  199. package/docs/exit-codes-and-output.md +1 -1
  200. package/docs/extensions-and-sharing.md +27 -1
  201. package/docs/fleet-dispatch.md +25 -4
  202. package/docs/installation-and-lifecycle.md +15 -2
  203. package/docs/middleware-and-components.md +1 -1
  204. package/docs/model-catalog.md +10 -1
  205. package/docs/observability.md +55 -4
  206. package/docs/proactive-memory.md +127 -14
  207. package/docs/prompt-envelope-and-tools.md +19 -1
  208. package/docs/provider-adapter-cookbook.md +1 -1
  209. package/docs/release-cut-checklist.md +19 -3
  210. package/docs/safety-model.md +2 -2
  211. package/docs/scientific-validation.md +3 -3
  212. package/docs/session-lifecycle.md +1 -1
  213. package/docs/skills-marketplace.md +1 -1
  214. package/docs/tool-usage.md +18 -8
  215. package/docs/trace-store.md +1 -1
  216. package/docs/troubleshooting.md +88 -1
  217. package/docs/tui-design.md +1 -1
  218. package/docs/worker-dispatch-mechanics.md +1 -1
  219. package/package.json +5 -2
  220. package/src/cli/acp.ts +6 -2
  221. package/src/cli/agents.ts +1 -1
  222. package/src/cli/argv.ts +25 -0
  223. package/src/cli/config-inspect.ts +33 -6
  224. package/src/cli/config.ts +1 -1
  225. package/src/cli/configure.ts +23 -21
  226. package/src/cli/doctor-panes.ts +124 -0
  227. package/src/cli/doctor-state-size.ts +82 -0
  228. package/src/cli/doctor-toolchain.ts +57 -0
  229. package/src/cli/doctor.ts +22 -1
  230. package/src/cli/eval-inventory.ts +436 -0
  231. package/src/cli/eval.ts +93 -16
  232. package/src/cli/evidence-detail.ts +88 -0
  233. package/src/cli/evidence-inventory.ts +183 -0
  234. package/src/cli/evidence.ts +30 -5
  235. package/src/cli/extensions.ts +5 -1
  236. package/src/cli/fleet-decisions.ts +69 -0
  237. package/src/cli/fleet-inspect.ts +334 -0
  238. package/src/cli/fleet-verify.ts +133 -0
  239. package/src/cli/fleet-view.ts +810 -0
  240. package/src/cli/fleet.ts +179 -39
  241. package/src/cli/index.ts +14 -2
  242. package/src/cli/interop-inspect.ts +128 -0
  243. package/src/cli/interop.ts +34 -0
  244. package/src/cli/panes.ts +35 -0
  245. package/src/cli/reset.ts +5 -2
  246. package/src/cli/run.ts +58 -0
  247. package/src/cli/skills-inventory.ts +185 -0
  248. package/src/cli/skills.ts +16 -13
  249. package/src/cli/targets.ts +44 -13
  250. package/src/cli/tools.ts +321 -0
  251. package/src/cli/trace-inspect.ts +252 -0
  252. package/src/cli/trace.ts +85 -5
  253. package/src/cli/usage.ts +63 -14
  254. package/src/cli/verifiers-inspect.ts +347 -0
  255. package/src/cli/verifiers.ts +9 -0
  256. package/src/core/bus-events.ts +33 -1
  257. package/src/core/cache-telemetry.ts +42 -0
  258. package/src/core/config.ts +67 -0
  259. package/src/core/defaults.ts +124 -8
  260. package/src/core/endpoint-key.ts +27 -0
  261. package/src/core/residency-target-key.ts +25 -0
  262. package/src/core/response-schema.ts +80 -6
  263. package/src/core/theme-token-hex.ts +43 -0
  264. package/src/core/tool-names.ts +2 -1
  265. package/src/core/xdg.ts +1 -1
  266. package/src/domains/agents/fleets/build-review.md +0 -3
  267. package/src/domains/agents/fleets/build-test.md +0 -3
  268. package/src/domains/agents/result-contract-filesystem.ts +32 -0
  269. package/src/domains/agents/result-contract.ts +164 -35
  270. package/src/domains/config/classify.ts +6 -0
  271. package/src/domains/context/codewiki/coordinator.ts +12 -4
  272. package/src/domains/dispatch/admission-error.ts +9 -0
  273. package/src/domains/dispatch/admission.ts +52 -14
  274. package/src/domains/dispatch/capacity-lease.ts +118 -9
  275. package/src/domains/dispatch/contract.ts +11 -0
  276. package/src/domains/dispatch/council-topology.ts +398 -0
  277. package/src/domains/dispatch/execution-plan.ts +44 -4
  278. package/src/domains/dispatch/extension.ts +378 -82
  279. package/src/domains/dispatch/fleet-node-prompt.ts +62 -0
  280. package/src/domains/dispatch/fleet-plan.ts +7 -2
  281. package/src/domains/dispatch/fleet-run.ts +87 -4
  282. package/src/domains/dispatch/gate-decisions.ts +11 -1
  283. package/src/domains/dispatch/gate-role-prompts.ts +9 -0
  284. package/src/domains/dispatch/gate-topology.ts +289 -0
  285. package/src/domains/dispatch/heartbeat.ts +32 -8
  286. package/src/domains/dispatch/index.ts +22 -0
  287. package/src/domains/dispatch/intent-compatibility.ts +330 -0
  288. package/src/domains/dispatch/intent.ts +85 -1
  289. package/src/domains/dispatch/orphan-recovery.ts +5 -0
  290. package/src/domains/dispatch/reservation-store.ts +139 -11
  291. package/src/domains/dispatch/run-event-journal-bridge.ts +149 -0
  292. package/src/domains/dispatch/run-event-journal.ts +598 -0
  293. package/src/domains/dispatch/state.ts +49 -1
  294. package/src/domains/dispatch/types.ts +13 -0
  295. package/src/domains/dispatch/validation.ts +33 -8
  296. package/src/domains/dispatch/worker-spawn.ts +25 -11
  297. package/src/domains/dispatch/write-boundary-enforcer.ts +20 -3
  298. package/src/domains/dispatch/write-boundary.ts +62 -1
  299. package/src/domains/eval/artifacts/store.ts +62 -0
  300. package/src/domains/eval/compare/behavioral.ts +224 -0
  301. package/src/domains/eval/compare/compare.ts +342 -2
  302. package/src/domains/eval/compare/envelope.ts +128 -0
  303. package/src/domains/eval/compare/gates.ts +24 -6
  304. package/src/domains/eval/compare/thresholds.ts +30 -3
  305. package/src/domains/eval/execution-provenance.ts +240 -0
  306. package/src/domains/eval/inventory.ts +113 -0
  307. package/src/domains/eval/metrics/aggregate.ts +136 -0
  308. package/src/domains/eval/metrics/call-ledger-stream.ts +112 -0
  309. package/src/domains/eval/metrics/tracked.ts +413 -0
  310. package/src/domains/eval/provenance.ts +117 -0
  311. package/src/domains/eval/reports/comparison.ts +128 -0
  312. package/src/domains/eval/reports/junit.ts +17 -3
  313. package/src/domains/eval/reports/markdown.ts +3 -3
  314. package/src/domains/eval/reports/text.ts +14 -0
  315. package/src/domains/eval/run-compare.ts +20 -0
  316. package/src/domains/eval/runners/clio-run.ts +127 -0
  317. package/src/domains/eval/runners/external-command.ts +28 -3
  318. package/src/domains/eval/schema/adapter.ts +111 -0
  319. package/src/domains/eval/schema/artifact.ts +20 -0
  320. package/src/domains/eval/schema/behavioral-metrics.ts +204 -0
  321. package/src/domains/eval/schema/behavioral.ts +520 -0
  322. package/src/domains/eval/schema/execution-envelope.ts +194 -0
  323. package/src/domains/eval/schema/serving.ts +105 -0
  324. package/src/domains/eval/schema/suite.ts +38 -8
  325. package/src/domains/eval/schema/validate.ts +58 -3
  326. package/src/domains/eval/schema/verdict.ts +237 -0
  327. package/src/domains/eval/suites/resolve.ts +2 -0
  328. package/src/domains/eval/suites/run.ts +264 -33
  329. package/src/domains/eval/verifiers/command.ts +2 -1
  330. package/src/domains/eval/workspaces/temp-copy.ts +145 -13
  331. package/src/domains/evidence/build.ts +2 -13
  332. package/src/domains/evidence/eval.ts +2 -12
  333. package/src/domains/evidence/findings-markdown.ts +33 -0
  334. package/src/domains/evidence/run-trust.ts +7 -113
  335. package/src/domains/evidence/store.ts +6 -0
  336. package/src/domains/evidence/trust-projection.ts +2 -2
  337. package/src/domains/extensions/compatibility.ts +285 -0
  338. package/src/domains/extensions/discovery.ts +38 -3
  339. package/src/domains/extensions/resources.ts +1 -1
  340. package/src/domains/extensions/state.ts +12 -3
  341. package/src/domains/extensions/types.ts +2 -0
  342. package/src/domains/lifecycle/doctor.ts +69 -1
  343. package/src/domains/memory/index.ts +14 -1
  344. package/src/domains/memory/task-bank-promotion.ts +64 -0
  345. package/src/domains/memory/task-memory-policy.ts +82 -17
  346. package/src/domains/memory/task-memory-spend.ts +131 -0
  347. package/src/domains/memory/task-memory-status.ts +7 -0
  348. package/src/domains/memory/task-memory-telemetry.ts +3 -0
  349. package/src/domains/middleware/index.ts +1 -0
  350. package/src/domains/middleware/memory-intervention.ts +97 -21
  351. package/src/domains/middleware/memory-step-endpoint.ts +71 -0
  352. package/src/domains/mux/contract.ts +434 -0
  353. package/src/domains/mux/detect.ts +158 -0
  354. package/src/domains/mux/extension.ts +47 -0
  355. package/src/domains/mux/index.ts +96 -0
  356. package/src/domains/mux/manifest.ts +6 -0
  357. package/src/domains/mux/operations.ts +164 -0
  358. package/src/domains/mux/pane-registry.ts +90 -0
  359. package/src/domains/mux/protocol.ts +49 -0
  360. package/src/domains/mux/socket-client.ts +816 -0
  361. package/src/domains/mux/types.ts +222 -0
  362. package/src/domains/mux/viewer-command.ts +59 -0
  363. package/src/domains/mux/yazi/assets/init.lua +2 -0
  364. package/src/domains/mux/yazi/assets/plugins/git.yazi/LICENSE +21 -0
  365. package/src/domains/mux/yazi/assets/plugins/git.yazi/README.md +78 -0
  366. package/src/domains/mux/yazi/assets/plugins/git.yazi/main.lua +255 -0
  367. package/src/domains/mux/yazi/assets/plugins/git.yazi/types.lua +12 -0
  368. package/src/domains/mux/yazi/assets/yazi.toml +17 -0
  369. package/src/domains/mux/yazi/event-stream.ts +180 -0
  370. package/src/domains/mux/yazi/profile.ts +299 -0
  371. package/src/domains/mux/yazi/session.ts +228 -0
  372. package/src/domains/mux/yazi/theme.ts +30 -0
  373. package/src/domains/observability/background-memory-usage.ts +140 -0
  374. package/src/domains/observability/cost.ts +22 -1
  375. package/src/domains/observability/index.ts +9 -0
  376. package/src/domains/observability/out-of-turn-usage.ts +51 -2
  377. package/src/domains/observability/trace-store.ts +234 -2
  378. package/src/domains/prompts/compiler.ts +100 -13
  379. package/src/domains/providers/endpoint-capacity.ts +228 -0
  380. package/src/domains/providers/endpoint-slots-store.ts +189 -0
  381. package/src/domains/providers/extension.ts +20 -3
  382. package/src/domains/providers/index.ts +32 -0
  383. package/src/domains/providers/model-runtime-capabilities.ts +32 -0
  384. package/src/domains/providers/models/local-models/clio-local-coding-targets.yaml +243 -1
  385. package/src/domains/providers/runtime-resolution.ts +8 -1
  386. package/src/domains/providers/runtimes/boot-manifest.ts +1 -0
  387. package/src/domains/providers/runtimes/builtins.ts +2 -0
  388. package/src/domains/providers/runtimes/common/probe-helpers.ts +31 -9
  389. package/src/domains/providers/runtimes/local-native/llamacpp-anthropic.ts +1 -1
  390. package/src/domains/providers/runtimes/local-native/llamacpp-completion.ts +1 -1
  391. package/src/domains/providers/runtimes/local-native/llamacpp-embed.ts +1 -1
  392. package/src/domains/providers/runtimes/local-native/llamacpp-rerank.ts +1 -1
  393. package/src/domains/providers/runtimes/local-native/llamacpp.ts +4 -1
  394. package/src/domains/providers/runtimes/local-native/lmstudio.ts +4 -1
  395. package/src/domains/providers/runtimes/local-native/ollama-native.ts +6 -1
  396. package/src/domains/providers/runtimes/protocol/litellm.ts +375 -0
  397. package/src/domains/providers/support.ts +1 -0
  398. package/src/domains/providers/target-model-cache.ts +124 -0
  399. package/src/domains/providers/types/capability-flags.ts +2 -0
  400. package/src/domains/providers/types/target-descriptor.ts +2 -0
  401. package/src/domains/resources/index.ts +3 -0
  402. package/src/domains/resources/prompts/loader.ts +95 -33
  403. package/src/domains/resources/skills/loader.ts +33 -0
  404. package/src/domains/safety/action-classifier.ts +6 -0
  405. package/src/domains/safety/call-target.ts +52 -0
  406. package/src/domains/safety/run-effects.ts +35 -4
  407. package/src/domains/session/context-accounting.ts +52 -1
  408. package/src/domains/session/context-ledger.ts +37 -13
  409. package/src/domains/session/index.ts +6 -0
  410. package/src/domains/session/prompt-cache.ts +140 -0
  411. package/src/domains/session/prompt-manifest.ts +42 -0
  412. package/src/domains/toolchain/archive.ts +175 -0
  413. package/src/domains/toolchain/contract.ts +28 -0
  414. package/src/domains/toolchain/extension.ts +47 -0
  415. package/src/domains/toolchain/index.ts +39 -0
  416. package/src/domains/toolchain/install.ts +327 -0
  417. package/src/domains/toolchain/manifest.ts +8 -0
  418. package/src/domains/toolchain/paths.ts +34 -0
  419. package/src/domains/toolchain/registry.ts +265 -0
  420. package/src/domains/toolchain/remove.ts +218 -0
  421. package/src/domains/toolchain/resolve.ts +182 -0
  422. package/src/domains/toolchain/types.ts +113 -0
  423. package/src/domains/toolchain/version.ts +88 -0
  424. package/src/engine/acp/adapter.ts +18 -3
  425. package/src/engine/acp/server.ts +413 -70
  426. package/src/engine/acp/types.ts +19 -1
  427. package/src/engine/ai.ts +35 -0
  428. package/src/engine/apis/llamacpp-residency.ts +55 -3
  429. package/src/engine/apis/lmstudio.ts +25 -5
  430. package/src/engine/apis/ollama-native.ts +2 -1
  431. package/src/engine/apis/openai-completions.ts +80 -17
  432. package/src/engine/apis/residency-lock.ts +3 -1
  433. package/src/engine/apis/residency.ts +34 -1
  434. package/src/engine/claude/sdk-module.ts +98 -0
  435. package/src/engine/claude/sdk-runtime.ts +19 -11
  436. package/src/engine/provider-payload.ts +29 -1
  437. package/src/engine/tui-primitives.ts +21 -0
  438. package/src/engine/tui.ts +1 -0
  439. package/src/engine/worker-runtime.ts +2 -13
  440. package/src/entry/boot-options.ts +2 -0
  441. package/src/entry/orchestrator.ts +279 -35
  442. package/src/entry/panes-activation.ts +31 -0
  443. package/src/entry/with-panes.ts +20 -0
  444. package/src/interactive/chat-loop-messages.ts +26 -7
  445. package/src/interactive/chat-loop.ts +318 -41
  446. package/src/interactive/chat-panel.ts +62 -8
  447. package/src/interactive/clio-editor.ts +45 -8
  448. package/src/interactive/context-activity.ts +5 -1
  449. package/src/interactive/context-meter.ts +1 -1
  450. package/src/interactive/context-overlay.ts +41 -10
  451. package/src/interactive/cost-overlay.ts +66 -6
  452. package/src/interactive/council-grid.ts +1 -3
  453. package/src/interactive/council.ts +11 -0
  454. package/src/interactive/dispatch-board.ts +102 -20
  455. package/src/interactive/fleet-run-preview.ts +41 -15
  456. package/src/interactive/handoff-round.ts +41 -2
  457. package/src/interactive/interactive-application.ts +177 -7
  458. package/src/interactive/interactive-input-runtime.ts +15 -0
  459. package/src/interactive/interactive-presentation.ts +4 -0
  460. package/src/interactive/interactive-shell.ts +20 -17
  461. package/src/interactive/interactive-slash-runtime.ts +63 -10
  462. package/src/interactive/memory-overlay.ts +9 -0
  463. package/src/interactive/modal-marker.ts +170 -0
  464. package/src/interactive/mutation-preview.ts +295 -0
  465. package/src/interactive/mux-bridge.ts +214 -0
  466. package/src/interactive/overlay-frame.ts +58 -2
  467. package/src/interactive/overlay-general-openers.ts +17 -0
  468. package/src/interactive/overlay-key-routing.ts +52 -3
  469. package/src/interactive/overlay-lifecycle.ts +56 -7
  470. package/src/interactive/overlay-model-selectors.ts +40 -3
  471. package/src/interactive/overlay-permission-lifecycle.ts +112 -24
  472. package/src/interactive/overlay-session-lifecycle.ts +73 -9
  473. package/src/interactive/overlay-transitions.ts +18 -4
  474. package/src/interactive/overlays/agents.ts +1 -0
  475. package/src/interactive/overlays/ask-user.ts +227 -49
  476. package/src/interactive/overlays/auth-dialog.ts +1 -0
  477. package/src/interactive/overlays/context-reset.ts +1 -0
  478. package/src/interactive/overlays/cwd-fallback.ts +1 -0
  479. package/src/interactive/overlays/decisions.ts +11 -11
  480. package/src/interactive/overlays/extensions.ts +1 -0
  481. package/src/interactive/overlays/fleet-run-approval.ts +1 -0
  482. package/src/interactive/overlays/handoff-review.ts +1 -0
  483. package/src/interactive/overlays/help-reference.ts +6 -0
  484. package/src/interactive/overlays/interop.ts +1 -0
  485. package/src/interactive/overlays/library-install-confirm.ts +1 -0
  486. package/src/interactive/overlays/library-tabs.ts +28 -0
  487. package/src/interactive/overlays/list-overlay.ts +10 -1
  488. package/src/interactive/overlays/message-picker.ts +1 -0
  489. package/src/interactive/overlays/model-scope.ts +86 -0
  490. package/src/interactive/overlays/model-selector.ts +1 -0
  491. package/src/interactive/overlays/prompts.ts +12 -1
  492. package/src/interactive/overlays/session-selector.ts +1 -0
  493. package/src/interactive/overlays/settings-sections.ts +30 -0
  494. package/src/interactive/overlays/settings.ts +614 -45
  495. package/src/interactive/overlays/side-question.ts +1 -0
  496. package/src/interactive/overlays/skills-hub.ts +3 -11
  497. package/src/interactive/overlays/tree-selector.ts +1 -0
  498. package/src/interactive/pane-policy.ts +46 -0
  499. package/src/interactive/panes-runtime.ts +292 -0
  500. package/src/interactive/permission-hint.ts +34 -2
  501. package/src/interactive/permission-overlay.ts +159 -9
  502. package/src/interactive/prewarm.ts +197 -0
  503. package/src/interactive/render-trace.ts +162 -15
  504. package/src/interactive/renderers/compaction-summary.ts +29 -0
  505. package/src/interactive/renderers/tool-execution.ts +4 -0
  506. package/src/interactive/renderers/worker-entry.ts +122 -14
  507. package/src/interactive/side-question.ts +58 -1
  508. package/src/interactive/slash-commands.ts +251 -15
  509. package/src/interactive/status/controller.ts +11 -0
  510. package/src/interactive/status/state-machine.ts +54 -2
  511. package/src/interactive/status/types.ts +7 -0
  512. package/src/interactive/tasks-overlay.ts +1 -0
  513. package/src/interactive/terminal-lease.ts +2 -0
  514. package/src/interactive/theme/tokens.ts +3 -14
  515. package/src/interactive/turn-context.ts +346 -33
  516. package/src/interactive/turn-persistence.ts +14 -4
  517. package/src/interactive/turn-prewarm.ts +364 -0
  518. package/src/interactive/turn-queues.ts +7 -4
  519. package/src/interactive/turn-runtime.ts +8 -1
  520. package/src/interactive/turn-state.ts +23 -0
  521. package/src/interactive/view/artifacts.ts +109 -1
  522. package/src/interactive/view/view-overlay.ts +29 -3
  523. package/src/interactive/watch-pane.ts +152 -0
  524. package/src/interactive/worker-progress.ts +7 -1
  525. package/src/interactive/worker-receipts.ts +19 -1
  526. package/src/interactive/worker-stream.ts +5 -0
  527. package/src/interactive/yazi-bridge.ts +444 -0
  528. package/src/tools/ask-user.ts +43 -2
  529. package/src/tools/bootstrap.ts +26 -2
  530. package/src/tools/builtin-tool-catalog.ts +15 -0
  531. package/src/tools/compete-worktrees.ts +83 -2
  532. package/src/tools/core-bootstrap.ts +2 -1
  533. package/src/tools/dispatch-admission.ts +14 -3
  534. package/src/tools/dispatch-arguments.ts +20 -20
  535. package/src/tools/dispatch-plan.ts +17 -9
  536. package/src/tools/dispatch-run-events.ts +134 -19
  537. package/src/tools/dispatch-runner.ts +29 -7
  538. package/src/tools/dispatch-scout.ts +1 -1
  539. package/src/tools/dispatch-types.ts +15 -3
  540. package/src/tools/dispatch.ts +1 -1
  541. package/src/tools/executables.ts +17 -14
  542. package/src/tools/observation.ts +54 -4
  543. package/src/tools/panes-surface.ts +38 -0
  544. package/src/tools/panes.ts +112 -0
  545. package/src/tools/policy.ts +10 -1
  546. package/src/tools/presentation.ts +1 -0
  547. package/src/tools/registry.ts +16 -0
  548. package/dist/chunk-AOCYTWAV.js +0 -449
  549. package/dist/chunk-HLE42MG7.js +0 -37
  550. package/dist/chunk-HWUFFB6L.js +0 -83
  551. package/dist/chunk-JOZYP4GM.js +0 -279
  552. package/dist/doctor-M7YEDGAE.js +0 -91
@@ -6,6 +6,7 @@ import { BIRTH_TOKEN_SOURCE_AVAILABLE, processAlive, processBirthToken } from ".
6
6
  import { FILE_LOCK_ACQUIRE_TIMEOUT_MS, withStateFileLockSync } from "../../core/state-file-lock.js";
7
7
  import { clioStateDir } from "../../core/xdg.js";
8
8
  import { atomicWrite } from "../../engine/session.js";
9
+ import { endpointLabel, foregroundStreamUsage } from "../providers/endpoint-capacity.js";
9
10
 
10
11
  export { processBirthToken } from "../../core/process-identity.js";
11
12
 
@@ -30,6 +31,8 @@ export interface CapacityLease {
30
31
  leaseId: string;
31
32
  assignmentId: string;
32
33
  nodeId: string;
34
+ /** Canonical inference endpoint. Absent only on leases written before endpoint capacity existed. */
35
+ endpointKey?: string;
33
36
  /** Owner host. Absent only on records written before the field existed; those are read as this host. */
34
37
  host?: string;
35
38
  ownerPid: number;
@@ -43,6 +46,7 @@ export interface CapacityLease {
43
46
  export interface CapacityLimits {
44
47
  global: number;
45
48
  nodes: Readonly<Record<string, number>>;
49
+ endpoints: Readonly<Record<string, number>>;
46
50
  }
47
51
  export interface CapacityDrain {
48
52
  requestedByPid: number;
@@ -81,6 +85,7 @@ function valid(value: unknown): value is CapacityLease {
81
85
  typeof lease.leaseId === "string" &&
82
86
  typeof lease.assignmentId === "string" &&
83
87
  typeof lease.nodeId === "string" &&
88
+ (lease.endpointKey === undefined || typeof lease.endpointKey === "string") &&
84
89
  (lease.host === undefined || typeof lease.host === "string") &&
85
90
  Number.isInteger(lease.ownerPid) &&
86
91
  typeof lease.processBirthToken === "string" &&
@@ -182,9 +187,14 @@ const MEMBER_STATUSES = new Set(["held", "consumed", "released"]);
182
187
  * rejected here as well: silently skipping an unreadable record would under-count
183
188
  * capacity and admit past the cap.
184
189
  */
185
- function heldReservationUsage(values: ReadonlyArray<unknown>): { global: number; nodes: Record<string, number> } {
190
+ function heldReservationUsage(values: ReadonlyArray<unknown>): {
191
+ global: number;
192
+ nodes: Record<string, number>;
193
+ endpoints: Record<string, number>;
194
+ } {
186
195
  let global = 0;
187
196
  const nodes: Record<string, number> = {};
197
+ const endpoints: Record<string, number> = {};
188
198
  for (const value of values) {
189
199
  if (typeof value !== "object" || value === null)
190
200
  throw new Error("dispatch capacity store has invalid reservation records");
@@ -194,10 +204,11 @@ function heldReservationUsage(values: ReadonlyArray<unknown>): { global: number;
194
204
  if (record.status !== "active") continue;
195
205
  const byWave = new Map<number, number>();
196
206
  const byNodeWave = new Map<string, Map<number, number>>();
207
+ const byEndpointWave = new Map<string, Map<number, number>>();
197
208
  for (const raw of record.members) {
198
209
  if (typeof raw !== "object" || raw === null)
199
210
  throw new Error("dispatch capacity store has invalid reservation records");
200
- const member = raw as { status?: unknown; wave?: unknown; nodeId?: unknown };
211
+ const member = raw as { status?: unknown; wave?: unknown; nodeId?: unknown; endpointKey?: unknown };
201
212
  if (
202
213
  typeof member.status !== "string" ||
203
214
  !MEMBER_STATUSES.has(member.status) ||
@@ -211,13 +222,73 @@ function heldReservationUsage(values: ReadonlyArray<unknown>): { global: number;
211
222
  const nodeWaves = byNodeWave.get(member.nodeId) ?? new Map<number, number>();
212
223
  nodeWaves.set(wave, (nodeWaves.get(wave) ?? 0) + 1);
213
224
  byNodeWave.set(member.nodeId, nodeWaves);
225
+ if (member.endpointKey !== undefined && typeof member.endpointKey !== "string")
226
+ throw new Error("dispatch capacity store has invalid reservation records");
227
+ if (typeof member.endpointKey === "string") {
228
+ const endpointWaves = byEndpointWave.get(member.endpointKey) ?? new Map<number, number>();
229
+ endpointWaves.set(wave, (endpointWaves.get(wave) ?? 0) + 1);
230
+ byEndpointWave.set(member.endpointKey, endpointWaves);
231
+ }
214
232
  }
215
233
  global += Math.max(0, ...byWave.values());
216
234
  for (const [id, waves] of byNodeWave) nodes[id] = (nodes[id] ?? 0) + Math.max(0, ...waves.values());
235
+ for (const [key, waves] of byEndpointWave) endpoints[key] = (endpoints[key] ?? 0) + Math.max(0, ...waves.values());
217
236
  }
218
- return { global, nodes };
237
+ return { global, nodes, endpoints };
238
+ }
239
+
240
+ export interface EndpointCapacityHolders {
241
+ leases: number;
242
+ reservations: number;
243
+ foregroundStreams: number;
219
244
  }
220
- function assertCapacity(file: CapacityStateFile, nodeId: string, limits: CapacityLimits): void {
245
+
246
+ /** Name the endpoint-capacity addends that are actually nonzero. */
247
+ export function describeEndpointCapacityHolders(holders: EndpointCapacityHolders): string {
248
+ const parts: string[] = [];
249
+ const add = (count: number, singular: string, plural: string): void => {
250
+ if (count > 0) parts.push(`${count} ${count === 1 ? singular : plural}`);
251
+ };
252
+ add(holders.leases, "active lease", "active leases");
253
+ add(holders.reservations, "held reservation", "held reservations");
254
+ add(holders.foregroundStreams, "foreground stream", "foreground streams");
255
+ if (parts.length === 0) return "no active lease, held reservation, or foreground stream currently holds a slot";
256
+ const named =
257
+ parts.length === 1
258
+ ? parts[0]
259
+ : parts.length === 2
260
+ ? `${parts[0]} and ${parts[1]}`
261
+ : `${parts.slice(0, -1).join(", ")}, and ${parts.at(-1)}`;
262
+ const total = holders.leases + holders.reservations + holders.foregroundStreams;
263
+ return `${named} ${total === 1 ? "holds" : "hold"} ${total === 1 ? "the slot" : "the slots"}`;
264
+ }
265
+
266
+ /**
267
+ * The ways past a saturated inference endpoint, in the order an operator can
268
+ * act on them.
269
+ *
270
+ * `maxConcurrentRequests` is named because the conservative default of one slot
271
+ * is a guess about an unprobed server, and on a server started with
272
+ * `--parallel 4` it is simply wrong. Without that sentence the denial reads as
273
+ * a hardware limit and the operator has no reason to look for a setting.
274
+ *
275
+ * `rosterFloor` is the smallest same-wave width the caller's topology can have.
276
+ * A council runs its whole roster in one wave, so "dispatch fewer at once" is
277
+ * not a move it has; saying so keeps the remedy honest.
278
+ */
279
+ export function endpointCapacityRemedy(input?: { rosterFloor?: number }): string {
280
+ const ways =
281
+ "set this target's maxConcurrentRequests to the slot count the server was started with, collect in-flight runs, or point workers at a second server";
282
+ if (input?.rosterFloor === undefined) return `reduce the same-wave worker count, ${ways}`;
283
+ return `a council runs its whole roster in one wave and cannot go below ${input.rosterFloor} members, so ${ways}`;
284
+ }
285
+
286
+ function assertCapacity(
287
+ file: CapacityStateFile,
288
+ nodeId: string,
289
+ endpointKey: string | undefined,
290
+ limits: CapacityLimits,
291
+ ): void {
221
292
  const held = heldReservationUsage(file.reservations);
222
293
  const globalUsed = file.leases.length + held.global;
223
294
  // The denial names the compliant next move, not just the gate. A model told
@@ -235,10 +306,28 @@ function assertCapacity(file: CapacityStateFile, nodeId: string, limits: Capacit
235
306
  if (used >= nodeLimit)
236
307
  throw new Error(`dispatch: admission denied: node '${nodeId}' capacity reached (${used}/${nodeLimit})`);
237
308
  }
309
+ if (endpointKey !== undefined) {
310
+ const endpointLimit = limits.endpoints[endpointKey];
311
+ if (endpointLimit !== undefined) {
312
+ if (endpointLimit < 1)
313
+ throw new Error(`dispatch: admission denied: endpoint '${endpointLabel(endpointKey)}' capacity unavailable`);
314
+ const holders: EndpointCapacityHolders = {
315
+ leases: file.leases.filter((lease) => lease.endpointKey === endpointKey).length,
316
+ reservations: held.endpoints[endpointKey] ?? 0,
317
+ foregroundStreams: foregroundStreamUsage()[endpointKey] ?? 0,
318
+ };
319
+ const used = holders.leases + holders.reservations + holders.foregroundStreams;
320
+ if (used >= endpointLimit)
321
+ throw new Error(
322
+ `dispatch: admission denied: endpoint '${endpointLabel(endpointKey)}' capacity reached (${used}/${endpointLimit} slots): ${describeEndpointCapacityHolders(holders)}; ${endpointCapacityRemedy()}`,
323
+ );
324
+ }
325
+ }
238
326
  }
239
327
  export function acquireCapacityLease(input: {
240
328
  assignmentId: string;
241
329
  nodeId: string;
330
+ endpointKey?: string;
242
331
  limits: CapacityLimits;
243
332
  nowMs?: number;
244
333
  ttlMs?: number;
@@ -259,10 +348,12 @@ export function acquireCapacityLease(input: {
259
348
  );
260
349
  const existing = file.leases.find((lease) => lease.assignmentId === input.assignmentId);
261
350
  if (existing) {
262
- if (existing.nodeId !== input.nodeId) {
351
+ if (existing.nodeId !== input.nodeId || existing.endpointKey !== input.endpointKey) {
263
352
  const without = { ...file, leases: file.leases.filter((lease) => lease !== existing) };
264
- assertCapacity(without, input.nodeId, input.limits);
353
+ assertCapacity(without, input.nodeId, input.endpointKey, input.limits);
265
354
  existing.nodeId = input.nodeId;
355
+ if (input.endpointKey === undefined) delete existing.endpointKey;
356
+ else existing.endpointKey = input.endpointKey;
266
357
  }
267
358
  existing.heartbeatAt = new Date(nowMs).toISOString();
268
359
  existing.expiresAt = new Date(nowMs + (input.ttlMs ?? DEFAULT_CAPACITY_LEASE_TTL_MS)).toISOString();
@@ -270,7 +361,7 @@ export function acquireCapacityLease(input: {
270
361
  return clone(existing);
271
362
  }
272
363
  input.onAcquiredUnderLock?.(file);
273
- assertCapacity(file, input.nodeId, input.limits);
364
+ assertCapacity(file, input.nodeId, input.endpointKey, input.limits);
274
365
  const token = input.processBirthToken ?? processBirthToken(input.ownerPid ?? process.pid);
275
366
  if (!token) throw new Error("dispatch: cannot establish process birth token");
276
367
  const at = new Date(nowMs).toISOString();
@@ -278,6 +369,7 @@ export function acquireCapacityLease(input: {
278
369
  leaseId: `lease-${nowMs.toString(36)}-${randomBytes(6).toString("hex")}`,
279
370
  assignmentId: input.assignmentId,
280
371
  nodeId: input.nodeId,
372
+ ...(input.endpointKey !== undefined ? { endpointKey: input.endpointKey } : {}),
281
373
  host: hostname(),
282
374
  ownerPid: input.ownerPid ?? process.pid,
283
375
  processBirthToken: token,
@@ -365,11 +457,28 @@ export function capacityDrain(nowMs = Date.now()): CapacityDrain | null {
365
457
  export function capacityLeaseUsage(options?: { nowMs?: number; probe?: LeaseOwnerProbe }): {
366
458
  global: number;
367
459
  nodes: Readonly<Record<string, number>>;
460
+ endpoints: Readonly<Record<string, number>>;
461
+ endpointHolders: Readonly<Record<string, EndpointCapacityHolders>>;
368
462
  } {
369
463
  const leases = listCapacityLeases(options);
370
464
  const nodes: Record<string, number> = {};
371
- for (const lease of leases) nodes[lease.nodeId] = (nodes[lease.nodeId] ?? 0) + 1;
372
- return { global: leases.length, nodes };
465
+ const foreground = foregroundStreamUsage();
466
+ const endpoints: Record<string, number> = { ...foreground };
467
+ const endpointHolders: Record<string, EndpointCapacityHolders> = Object.fromEntries(
468
+ Object.entries(foreground).map(([endpointKey, foregroundStreams]) => [
469
+ endpointKey,
470
+ { leases: 0, reservations: 0, foregroundStreams },
471
+ ]),
472
+ );
473
+ for (const lease of leases) {
474
+ nodes[lease.nodeId] = (nodes[lease.nodeId] ?? 0) + 1;
475
+ if (lease.endpointKey !== undefined) {
476
+ endpoints[lease.endpointKey] = (endpoints[lease.endpointKey] ?? 0) + 1;
477
+ const holders = endpointHolders[lease.endpointKey] ?? { leases: 0, reservations: 0, foregroundStreams: 0 };
478
+ endpointHolders[lease.endpointKey] = { ...holders, leases: holders.leases + 1 };
479
+ }
480
+ }
481
+ return { global: leases.length, nodes, endpoints, endpointHolders };
373
482
  }
374
483
 
375
484
  /**
@@ -13,6 +13,7 @@ import type { ApprovedAssignmentRoute } from "./route-approval.js";
13
13
  import type { RouteDecisionV1 } from "./route-decision.js";
14
14
  import type { RunEnvelope, RunLineage, RunNodeIdentity, RunPhaseDurations, RunReceipt, RunStatus } from "./types.js";
15
15
  import type { DispatchFailoverCandidate, JobSpec } from "./validation.js";
16
+ import type { WriteBoundaryAttribution } from "./write-boundary.js";
16
17
 
17
18
  export interface DispatchRequest extends JobSpec {
18
19
  /** Coordinator-owned result contract override for a fleet plan step. */
@@ -93,6 +94,8 @@ export interface DispatchPlanTaskResolution {
93
94
  targetId: string;
94
95
  wireModelId: string;
95
96
  runtimeId: string;
97
+ /** Canonical inference scheduler identity and its independent request-slot bound. */
98
+ endpoint?: { key: string; label: string; limit: number };
96
99
  node: RunNodeIdentity;
97
100
  /** Effective thinking control for this route; null when the runtime has none. */
98
101
  thinkingLevel: string | null;
@@ -209,6 +212,7 @@ export interface DispatchContract {
209
212
  topology: ReservationTopology;
210
213
  tasks: ReadonlyArray<{ memberId: string; wave: number; resolution: DispatchPlanTaskResolution }>;
211
214
  }): DispatchReservationRecord;
215
+ release(ownerId: string): DispatchReservationRecord | null;
212
216
  rollback(ownerId: string): DispatchReservationRecord | null;
213
217
  rollbackUnconsumed(ownerId: string): DispatchReservationRecord | null;
214
218
  get(ownerId: string): DispatchReservationRecord | null;
@@ -266,6 +270,13 @@ export interface DispatchContract {
266
270
  */
267
271
  observedRunWrites?(runId: string): ReadonlyArray<string> | null;
268
272
 
273
+ /**
274
+ * Full write-record witness, including durable downgrade causes. New callers
275
+ * use this surface; observedRunWrites remains as the closed-list compatibility
276
+ * view for integrations that only know paths or null.
277
+ */
278
+ observedRunWriteAttribution?(runId: string): WriteBoundaryAttribution | null;
279
+
269
280
  /** In-memory logical assignment state and complete finalized-attempt history. */
270
281
  readonly assignments?: {
271
282
  /** Live assignment, including its terminal promise and detailed attempt refs. */
@@ -0,0 +1,398 @@
1
+ /**
2
+ * Read-only reconstruction of council topology from the ordinary run ledger.
3
+ *
4
+ * A council has no durable record of its own. Every one of its runs is an
5
+ * ordinary ledger row, and what makes it a council is the provenance the
6
+ * scheduler stamps on each row: a shared {@link RunCouncilProvenance.group},
7
+ * the seated member's `label`, the round it spoke in, and a `gate.role` of
8
+ * `member` or `synthesis`. So the topology is not stored anywhere; it is a
9
+ * grouping over rows that already exist, which is what this module computes.
10
+ *
11
+ * Everything here is a fact the scheduler wrote. Member answers, the judge's
12
+ * synthesis text, and the vote tally live in receipt output as free model prose
13
+ * and are deliberately outside this projection: what crosses is who was seated,
14
+ * where they ran, how many rounds they took, and whether the council reached a
15
+ * synthesis. This module is pure. It reads no file, opens no ledger, and takes
16
+ * the rows its caller already has.
17
+ */
18
+
19
+ import type { ExecutionRole } from "./execution-role.js";
20
+ import { COUNCIL_MAX_MEMBERS } from "./gate-role-prompts.js";
21
+ import type { RunEnvelope, RunOutcome, RunStatus } from "./types.js";
22
+
23
+ /** How many councils one projection reports, newest first. */
24
+ export const COUNCIL_TOPOLOGY_MAX_COUNCILS = 4;
25
+
26
+ /** Seated members named per council. Mirrors the harness's own roster ceiling. */
27
+ export const COUNCIL_TOPOLOGY_MAX_MEMBERS = COUNCIL_MAX_MEMBERS;
28
+
29
+ /**
30
+ * Turns reported per member. Admission caps a council at three rounds; the
31
+ * bound is restated rather than imported because raising the harness ceiling
32
+ * must widen this window deliberately, and `turnsTruncated` reports the gap in
33
+ * the meantime.
34
+ */
35
+ export const COUNCIL_TOPOLOGY_MAX_TURNS = 3;
36
+
37
+ /**
38
+ * Ledger rows one projection will read.
39
+ *
40
+ * A council is at most five members over three rounds plus a judge and a sealed
41
+ * report, so seventeen rows; four councils fit inside a fraction of this scan
42
+ * even when ordinary dispatch rows are interleaved. The cap is what keeps the
43
+ * projection proportional on an installation whose ring holds a thousand runs.
44
+ */
45
+ export const COUNCIL_TOPOLOGY_MAX_SCAN = 256;
46
+
47
+ /**
48
+ * The synthesis label every council files its verdict under.
49
+ *
50
+ * It is a scheduler constant, not an operator string: member labels are matched
51
+ * against {@link COUNCIL_MEMBER_LABEL}, which this value cannot satisfy anyway,
52
+ * so the two namespaces cannot collide.
53
+ */
54
+ const SYNTHESIS_LABEL = "synthesis";
55
+
56
+ /** The agent id the ledger files a sealed council report under. */
57
+ const SEALED_REPORT_AGENT_ID = "council-synthesis";
58
+
59
+ /**
60
+ * The exact task text {@link sealCouncilSynthesis} mints, per synthesis kind.
61
+ *
62
+ * A run's task is operator or model prose everywhere else in this ledger, so it
63
+ * is never repeated by this projection. The sealed report is the one row whose
64
+ * task the harness writes itself, from a closed set of three, which makes it the
65
+ * only durable record of which synthesis a council was configured for. It is
66
+ * classified against that set, not quoted: an unrecognised string yields no kind
67
+ * rather than crossing as text.
68
+ */
69
+ const SEALED_REPORT_TASKS: ReadonlyArray<readonly [string, CouncilSynthesisKind]> = [
70
+ ["Council none synthesis", "none"],
71
+ ["Council vote synthesis", "vote"],
72
+ ["Council judge synthesis", "judge"],
73
+ ];
74
+
75
+ /**
76
+ * The label shape both council entry paths already enforce.
77
+ *
78
+ * `dispatch` admission tests member labels against this pattern and settings
79
+ * validation rejects a roster member that fails it, so a stored label outside it
80
+ * was not written by either. Repeating it here makes the projection's own
81
+ * guarantee structural rather than inherited: whatever wrote the row, the label
82
+ * this module reports can carry no path, URL, quote, space, or control byte.
83
+ */
84
+ const COUNCIL_MEMBER_LABEL = /^[a-z][a-z0-9_-]{0,31}$/u;
85
+
86
+ export type CouncilSynthesisKind = "none" | "vote" | "judge";
87
+
88
+ /** How a council's dispatch plan was admitted, when the row records one. */
89
+ export type CouncilApproval = "operator" | "full-auto";
90
+
91
+ /** Who asked for the council: the operator's `/council`, the model, or the runtime. */
92
+ export type CouncilOrigin = "user" | "agent" | "internal";
93
+
94
+ /** One member's contribution in one round. */
95
+ export interface CouncilMemberTurn {
96
+ readonly round: number;
97
+ readonly runId: string;
98
+ readonly status: RunStatus;
99
+ /** Null while the run has not finalized; absent on pre-taxonomy rows. */
100
+ readonly outcome: RunOutcome | null;
101
+ readonly terminal: boolean;
102
+ }
103
+
104
+ /** One seated voice, with every round it spoke in, oldest round first. */
105
+ export interface CouncilMember {
106
+ readonly label: string;
107
+ readonly agentId: string;
108
+ readonly targetId: string;
109
+ readonly wireModelId: string;
110
+ /** The role its route statistics were filed under; a council seats researchers. */
111
+ readonly executionRole: ExecutionRole;
112
+ readonly turns: readonly CouncilMemberTurn[];
113
+ readonly turnsTruncated: boolean;
114
+ }
115
+
116
+ /** The judge dispatch a `judge` synthesis runs, as an ordinary ledger row. */
117
+ export interface CouncilJudge {
118
+ readonly runId: string;
119
+ readonly agentId: string;
120
+ readonly targetId: string;
121
+ readonly wireModelId: string;
122
+ readonly status: RunStatus;
123
+ readonly outcome: RunOutcome | null;
124
+ }
125
+
126
+ export interface CouncilSynthesisRecord {
127
+ /**
128
+ * Which synthesis the council was configured for, classified from the sealed
129
+ * report's harness-minted task. Null when no sealed report is in the window,
130
+ * which is what an aborted council and an aged-out one both look like.
131
+ */
132
+ readonly kind: CouncilSynthesisKind | null;
133
+ /** The sealed council-report row, whose text stays host-side. */
134
+ readonly sealedRunId: string | null;
135
+ /** Present only for a judge synthesis, and only while its row is in the window. */
136
+ readonly judge: CouncilJudge | null;
137
+ }
138
+
139
+ export interface CouncilTopology {
140
+ readonly group: string;
141
+ readonly startedAt: string;
142
+ /** Null while any row of the council is still running. */
143
+ readonly endedAt: string | null;
144
+ readonly running: boolean;
145
+ /**
146
+ * Rounds the council was configured for, recovered from the sealed report's
147
+ * round stamp. Null when no sealed report is in the window.
148
+ */
149
+ readonly roundsPlanned: number | null;
150
+ /** Highest round any member row records. Never exceeds `roundsPlanned`. */
151
+ readonly roundsObserved: number;
152
+ readonly origin: CouncilOrigin | null;
153
+ readonly approval: CouncilApproval | null;
154
+ readonly members: readonly CouncilMember[];
155
+ /** True when the council seated more members than this projection names. */
156
+ readonly membersTruncated: boolean;
157
+ /**
158
+ * Member rows whose stored label failed {@link COUNCIL_MEMBER_LABEL} and were
159
+ * therefore left unnamed rather than repeated. Non-zero means something other
160
+ * than council admission wrote a row into this group.
161
+ */
162
+ readonly membersRejected: number;
163
+ readonly synthesis: CouncilSynthesisRecord;
164
+ }
165
+
166
+ function isRunStatus(value: unknown): value is RunStatus {
167
+ return (
168
+ value === "queued" ||
169
+ value === "running" ||
170
+ value === "completed" ||
171
+ value === "failed" ||
172
+ value === "interrupted" ||
173
+ value === "stale" ||
174
+ value === "dead"
175
+ );
176
+ }
177
+
178
+ function isRunOutcome(value: unknown): value is RunOutcome {
179
+ return (
180
+ value === "succeeded" ||
181
+ value === "failed" ||
182
+ value === "timed_out" ||
183
+ value === "stalled" ||
184
+ value === "canceled" ||
185
+ value === "denied_by_policy" ||
186
+ value === "spawn_failed"
187
+ );
188
+ }
189
+
190
+ function outcomeOf(row: RunEnvelope): RunOutcome | null {
191
+ return isRunOutcome(row.outcome) ? row.outcome : null;
192
+ }
193
+
194
+ function statusOf(row: RunEnvelope): RunStatus {
195
+ // A row written by another build can carry a status this one does not know.
196
+ // Reporting it as `stale` is the closest honest answer: the projection has a
197
+ // row it cannot classify, which is exactly what that state already means.
198
+ return isRunStatus(row.status) ? row.status : "stale";
199
+ }
200
+
201
+ function synthesisKindOf(task: unknown): CouncilSynthesisKind | null {
202
+ return SEALED_REPORT_TASKS.find(([text]) => text === task)?.[1] ?? null;
203
+ }
204
+
205
+ /** Oldest round first, then by run id so a duplicate round still orders stably. */
206
+ function byRound(a: CouncilMemberTurn, b: CouncilMemberTurn): number {
207
+ return a.round - b.round || (a.runId < b.runId ? -1 : a.runId > b.runId ? 1 : 0);
208
+ }
209
+
210
+ interface CouncilRows {
211
+ readonly group: string;
212
+ readonly members: RunEnvelope[];
213
+ readonly judges: RunEnvelope[];
214
+ readonly sealed: RunEnvelope[];
215
+ }
216
+
217
+ /**
218
+ * Split one group's rows by the part they played.
219
+ *
220
+ * A row is a member unless its council label is the synthesis constant, and a
221
+ * synthesis row is the sealed report only when its agent id is the one the
222
+ * ledger writer uses. That distinction matters: a judge council writes two
223
+ * synthesis rows, one an ordinary dispatch that consumed a model and one a
224
+ * zero-cost record of the council's own report, and conflating them would double
225
+ * the council's apparent judge.
226
+ */
227
+ function partition(group: string, rows: ReadonlyArray<RunEnvelope>): CouncilRows {
228
+ const members: RunEnvelope[] = [];
229
+ const judges: RunEnvelope[] = [];
230
+ const sealed: RunEnvelope[] = [];
231
+ for (const row of rows) {
232
+ if (row.council?.label !== SYNTHESIS_LABEL) {
233
+ members.push(row);
234
+ continue;
235
+ }
236
+ if (row.agentId === SEALED_REPORT_AGENT_ID) sealed.push(row);
237
+ else judges.push(row);
238
+ }
239
+ return { group, members, judges, sealed };
240
+ }
241
+
242
+ function projectMembers(rows: ReadonlyArray<RunEnvelope>): {
243
+ members: CouncilMember[];
244
+ truncated: boolean;
245
+ rejected: number;
246
+ } {
247
+ const byLabel = new Map<string, RunEnvelope[]>();
248
+ let rejected = 0;
249
+ for (const row of rows) {
250
+ const label = row.council?.label ?? "";
251
+ if (!COUNCIL_MEMBER_LABEL.test(label)) {
252
+ rejected += 1;
253
+ continue;
254
+ }
255
+ const bucket = byLabel.get(label);
256
+ if (bucket === undefined) byLabel.set(label, [row]);
257
+ else bucket.push(row);
258
+ }
259
+ const labels = [...byLabel.keys()].sort();
260
+ const named = labels.slice(0, COUNCIL_TOPOLOGY_MAX_MEMBERS);
261
+ const members: CouncilMember[] = [];
262
+ for (const label of named) {
263
+ const bucket = byLabel.get(label) ?? [];
264
+ // The rows arrive newest first, so the head is the member's latest round
265
+ // and carries the route it most recently ran on. A council can reroute a
266
+ // member between rounds, and the current route is the one an operator is
267
+ // reading the grid to check.
268
+ const head = bucket[0];
269
+ if (head === undefined) continue;
270
+ const turns = bucket
271
+ .map(
272
+ (row): CouncilMemberTurn => ({
273
+ round: row.council?.round ?? 1,
274
+ runId: row.id,
275
+ status: statusOf(row),
276
+ outcome: outcomeOf(row),
277
+ terminal: row.endedAt !== null,
278
+ }),
279
+ )
280
+ .sort(byRound);
281
+ const visible = turns.slice(0, COUNCIL_TOPOLOGY_MAX_TURNS);
282
+ members.push({
283
+ label,
284
+ agentId: head.agentId,
285
+ targetId: head.targetId,
286
+ wireModelId: head.wireModelId,
287
+ executionRole: head.executionRole,
288
+ turns: visible,
289
+ turnsTruncated: turns.length > visible.length,
290
+ });
291
+ }
292
+ return { members, truncated: labels.length > named.length, rejected };
293
+ }
294
+
295
+ function projectSynthesis(rows: CouncilRows): CouncilSynthesisRecord {
296
+ const sealed = rows.sealed[0] ?? null;
297
+ const judge = rows.judges[0] ?? null;
298
+ return {
299
+ kind: sealed === null ? null : synthesisKindOf(sealed.task),
300
+ sealedRunId: sealed?.id ?? null,
301
+ judge:
302
+ judge === null
303
+ ? null
304
+ : {
305
+ runId: judge.id,
306
+ agentId: judge.agentId,
307
+ targetId: judge.targetId,
308
+ wireModelId: judge.wireModelId,
309
+ status: statusOf(judge),
310
+ outcome: outcomeOf(judge),
311
+ },
312
+ };
313
+ }
314
+
315
+ function originOf(rows: ReadonlyArray<RunEnvelope>): CouncilOrigin | null {
316
+ const value = rows.find((row) => row.requestOrigin !== undefined)?.requestOrigin;
317
+ return value === "user" || value === "agent" || value === "internal" ? value : null;
318
+ }
319
+
320
+ function approvalOf(rows: ReadonlyArray<RunEnvelope>): CouncilApproval | null {
321
+ const value = rows.find((row) => row.plan !== undefined)?.plan?.approval;
322
+ return value === "operator" || value === "full-auto" ? value : null;
323
+ }
324
+
325
+ /** Earliest start over the council's rows, as an ISO stamp. */
326
+ function firstStart(rows: ReadonlyArray<RunEnvelope>): string {
327
+ let earliest = rows[0]?.startedAt ?? new Date(0).toISOString();
328
+ for (const row of rows) if (row.startedAt < earliest) earliest = row.startedAt;
329
+ return earliest;
330
+ }
331
+
332
+ /** Latest end, or null when any row has not finished. */
333
+ function lastEnd(rows: ReadonlyArray<RunEnvelope>): string | null {
334
+ let latest: string | null = null;
335
+ for (const row of rows) {
336
+ if (row.endedAt === null) return null;
337
+ if (latest === null || row.endedAt > latest) latest = row.endedAt;
338
+ }
339
+ return latest;
340
+ }
341
+
342
+ export interface CouncilTopologyResult {
343
+ readonly councils: readonly CouncilTopology[];
344
+ /** True when the scanned window held councils older than the reported ones. */
345
+ readonly truncated: boolean;
346
+ }
347
+
348
+ /**
349
+ * Group a newest-first ledger window into bounded council topologies.
350
+ *
351
+ * The rows must arrive in the order the ledger lists them, newest start first,
352
+ * because that ordering is what makes the first council encountered the newest
353
+ * one. Nothing is read from disk and no row is mutated.
354
+ */
355
+ export function councilTopologies(rows: ReadonlyArray<RunEnvelope>): CouncilTopologyResult {
356
+ const grouped = new Map<string, RunEnvelope[]>();
357
+ for (const row of rows.slice(0, COUNCIL_TOPOLOGY_MAX_SCAN)) {
358
+ const group = row.council?.group;
359
+ if (typeof group !== "string" || group.length === 0) continue;
360
+ const bucket = grouped.get(group);
361
+ if (bucket === undefined) grouped.set(group, [row]);
362
+ else bucket.push(row);
363
+ }
364
+ const groups = [...grouped.entries()];
365
+ const window = groups.slice(0, COUNCIL_TOPOLOGY_MAX_COUNCILS);
366
+ const councils: CouncilTopology[] = [];
367
+ for (const [group, rowsInGroup] of window) {
368
+ const parts = partition(group, rowsInGroup);
369
+ const { members, truncated, rejected } = projectMembers(parts.members);
370
+ const synthesis = projectSynthesis(parts);
371
+ const roundsObserved = members.reduce(
372
+ (highest, member) => member.turns.reduce((inner, turn) => Math.max(inner, turn.round), highest),
373
+ 0,
374
+ );
375
+ const sealedRound = parts.sealed[0]?.council?.round;
376
+ const roundsPlanned =
377
+ typeof sealedRound === "number" && Number.isInteger(sealedRound) && sealedRound > 0 ? sealedRound : null;
378
+ councils.push({
379
+ group,
380
+ startedAt: firstStart(rowsInGroup),
381
+ endedAt: lastEnd(rowsInGroup),
382
+ running: rowsInGroup.some((row) => row.endedAt === null),
383
+ // A sealed report stamps the configured round count, so a member seen in
384
+ // a later round than the report claims means the two disagree about the
385
+ // same council. Reporting the higher of the two keeps the invariant that
386
+ // observed never exceeds planned without hiding the member row.
387
+ roundsPlanned: roundsPlanned === null ? null : Math.max(roundsPlanned, roundsObserved),
388
+ roundsObserved,
389
+ origin: originOf(rowsInGroup),
390
+ approval: approvalOf(rowsInGroup),
391
+ members,
392
+ membersTruncated: truncated,
393
+ membersRejected: rejected,
394
+ synthesis,
395
+ });
396
+ }
397
+ return { councils, truncated: groups.length > window.length };
398
+ }