@iowarp/clio-coder 0.3.4 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (376) hide show
  1. package/CHANGELOG.md +65 -2
  2. package/CONTRIBUTING.md +6 -6
  3. package/README.md +16 -5
  4. package/dist/{acp-S5R4RR5B.js → acp-SK4MD6MM.js} +11 -11
  5. package/dist/{agents-P6DMMVZY.js → agents-2FN2K6ME.js} +33 -26
  6. package/dist/assets/codewiki.json +1 -1
  7. package/dist/{auth-2XCZLPKS.js → auth-QIYZWM5I.js} +15 -15
  8. package/dist/{chunk-EKMEHE4H.js → chunk-33YXPOE3.js} +2 -3
  9. package/dist/{chunk-VAWWTKDP.js → chunk-3HAPLH5M.js} +11 -11
  10. package/dist/{chunk-YCWGATWI.js → chunk-465YSENW.js} +3 -3
  11. package/dist/{chunk-4OC57DA6.js → chunk-4DGYLA73.js} +53 -2
  12. package/dist/{chunk-UZHIZC5S.js → chunk-4DWFMQDR.js} +61 -76
  13. package/dist/{chunk-ZWMF7253.js → chunk-5C3AQNDW.js} +328 -9
  14. package/dist/{chunk-22NAGB7X.js → chunk-5C77SEEY.js} +5 -94
  15. package/dist/{chunk-WPQLXFOZ.js → chunk-5FR74PWO.js} +3 -2
  16. package/dist/{chunk-35MKKU5R.js → chunk-5UJ6ECTS.js} +18 -10
  17. package/dist/{chunk-BRXQQJFP.js → chunk-6M7VS3J3.js} +571 -50
  18. package/dist/{chunk-QQK64KLB.js → chunk-6TUKSZVF.js} +141 -23
  19. package/dist/{chunk-N4CZJQRK.js → chunk-AB4XIIVB.js} +8 -6
  20. package/dist/{chunk-KRPY7NTG.js → chunk-BMWK7ZIZ.js} +14 -20
  21. package/dist/{chunk-4BPJXDWC.js → chunk-C4JBQ5SR.js} +30 -14
  22. package/dist/{chunk-ZYKPLLNQ.js → chunk-CEYBNUGC.js} +821 -83
  23. package/dist/{chunk-VEZEGCGW.js → chunk-D4MDIG46.js} +20 -18
  24. package/dist/chunk-DJNLUABN.js +843 -0
  25. package/dist/{chunk-BP4OYD6A.js → chunk-DMD2AGVS.js} +21 -2
  26. package/dist/{chunk-KOHPCX4K.js → chunk-DOOEX22V.js} +2 -2
  27. package/dist/chunk-DQA7QLMD.js +123 -0
  28. package/dist/chunk-DR52UMZW.js +21 -0
  29. package/dist/{chunk-3HZ5RWN2.js → chunk-EBEFWSGL.js} +9 -7
  30. package/dist/{chunk-EDRHSCIE.js → chunk-EELBMBT6.js} +128 -13
  31. package/dist/{chunk-HV5X7OR2.js → chunk-EOOQZZDE.js} +16 -14
  32. package/dist/{chunk-WR67VIZY.js → chunk-FOT2FX5J.js} +63 -5
  33. package/dist/{chunk-BPGS2WCQ.js → chunk-GEYXPTRF.js} +2 -1
  34. package/dist/{chunk-FYYLNIL5.js → chunk-GH5622CP.js} +2 -2
  35. package/dist/{chunk-BEY543CS.js → chunk-GOXNB3AO.js} +5 -2
  36. package/dist/chunk-GWS3VEIW.js +195 -0
  37. package/dist/{chunk-G4BMMOKF.js → chunk-HVDIIIQW.js} +2 -2
  38. package/dist/chunk-HWUFFB6L.js +83 -0
  39. package/dist/{chunk-X6COSD2O.js → chunk-J7PIKKWC.js} +8 -436
  40. package/dist/{chunk-NILBFAPG.js → chunk-JNXPYBB4.js} +2 -2
  41. package/dist/{chunk-4VP4KH3K.js → chunk-JRIO5UD2.js} +4 -4
  42. package/dist/{chunk-K6WL7QZT.js → chunk-JTSEDYVQ.js} +7 -7
  43. package/dist/{chunk-QKMUKYO7.js → chunk-KCMKRQX4.js} +236 -84
  44. package/dist/chunk-KZ2H5X4G.js +1026 -0
  45. package/dist/{chunk-A2GZF7DC.js → chunk-LADCF22A.js} +13 -13
  46. package/dist/chunk-LCGCVYZ4.js +57 -0
  47. package/dist/chunk-M4AKACEO.js +382 -0
  48. package/dist/{chunk-POHLU5DW.js → chunk-M6L6IDJG.js} +3 -3
  49. package/dist/{chunk-4JUF2NNX.js → chunk-MXI6J5JF.js} +7 -7
  50. package/dist/{chunk-X4RCMKVQ.js → chunk-NDINPTJ4.js} +2 -2
  51. package/dist/{chunk-TTNYS3EA.js → chunk-OB5HIGJY.js} +1 -1
  52. package/dist/{chunk-7RXG6QRZ.js → chunk-OBMAI2DP.js} +61 -840
  53. package/dist/{chunk-5M54SPOL.js → chunk-ODFEOB4F.js} +161 -5
  54. package/dist/chunk-PD3MESLB.js +242 -0
  55. package/dist/{chunk-ED4KHGC3.js → chunk-PPAMZ32Z.js} +9 -2
  56. package/dist/{chunk-VMNQ6OZA.js → chunk-QCTRSGHQ.js} +963 -786
  57. package/dist/chunk-RVG5JXAL.js +41 -0
  58. package/dist/{chunk-RD5U66HV.js → chunk-SROCI7ZU.js} +7 -7
  59. package/dist/{chunk-MFFY33HR.js → chunk-THKY7CD7.js} +466 -205
  60. package/dist/{chunk-34475P3I.js → chunk-TSHXZTOQ.js} +5 -4
  61. package/dist/{chunk-PCZJO5TI.js → chunk-UFQ3F4FW.js} +13 -178
  62. package/dist/{chunk-AD2SYQYC.js → chunk-UHXRNZ2J.js} +121 -3
  63. package/dist/chunk-UND3GU2L.js +103 -0
  64. package/dist/{chunk-QQL5RT5M.js → chunk-UUANF5CR.js} +2323 -2114
  65. package/dist/{chunk-VJWL6YS5.js → chunk-UUVG37B4.js} +2 -2
  66. package/dist/chunk-UVDSQ6LW.js +472 -0
  67. package/dist/{chunk-QWU7ZBO7.js → chunk-VQNODYQ4.js} +215 -56
  68. package/dist/chunk-VREKEFLL.js +37 -0
  69. package/dist/{chunk-2TZWSW76.js → chunk-WHGPSPT5.js} +2 -2
  70. package/dist/{chunk-TW3WDMVS.js → chunk-WHJYKASB.js} +2 -2
  71. package/dist/{chunk-MEQ45TQ4.js → chunk-WJHBC77E.js} +21 -7
  72. package/dist/{chunk-HXG4IURW.js → chunk-X2KV5FXT.js} +2 -2
  73. package/dist/{chunk-YHZX5GEU.js → chunk-XAKHZX5N.js} +2 -2
  74. package/dist/{chunk-2LZI5CAG.js → chunk-XEGB6BCN.js} +228 -36
  75. package/dist/{chunk-E25LMLRW.js → chunk-YD734TPH.js} +2 -2
  76. package/dist/{verifiers-4UUM6TEE.js → chunk-YTYFXUI3.js} +121 -372
  77. package/dist/{chunk-3JLKSKD7.js → chunk-ZGH7FGS5.js} +17 -7
  78. package/dist/{chunk-VSNATDE6.js → chunk-ZZMN5OM4.js} +2 -2
  79. package/dist/cli/index.js +34 -32
  80. package/dist/{clio-J5JIOIDS.js → clio-WBVQEBKO.js} +7 -7
  81. package/dist/{code-nav-AXCXSBHX.js → code-nav-FGGFIE7L.js} +7 -7
  82. package/dist/codewiki/build-worker.js +4 -4
  83. package/dist/{components-KELWS457.js → components-F7OEATSO.js} +5 -5
  84. package/dist/{config-OEBMIN2U.js → config-TRBL3RCF.js} +48 -41
  85. package/dist/{configure-PUQOSIXQ.js → configure-OLCVPHNM.js} +17 -17
  86. package/dist/{context-URSXPBCK.js → context-MJIJ6GOX.js} +12 -12
  87. package/dist/{context-EKDCKUUZ.js → context-WFPKQSM6.js} +26 -9
  88. package/dist/{context-MGSE4Z2T.js → context-XEWE3MOJ.js} +44 -37
  89. package/dist/{context-clear-KDAJRNUK.js → context-clear-KNOS2JPB.js} +44 -37
  90. package/dist/{context-index-BZ4UYMTC.js → context-index-SSR5ECNE.js} +3 -3
  91. package/dist/{context-working-set-SBKMPPI2.js → context-working-set-EUXAZI6N.js} +14 -13
  92. package/dist/{dispatch-runner-MSWN72NK.js → dispatch-runner-B7MTOVKL.js} +321 -60
  93. package/dist/{docs-2C2LTVT2.js → docs-FLJTIDSE.js} +5 -5
  94. package/dist/{doctor-7BSE27PJ.js → doctor-RN4YKO2X.js} +15 -15
  95. package/dist/{eval-IZGDOO4H.js → eval-RUBJVSNQ.js} +52 -236
  96. package/dist/{evidence-SR7WXB5B.js → evidence-JZNBUOQZ.js} +39 -33
  97. package/dist/{evolve-K7VE2CBX.js → evolve-FJVC4KKI.js} +39 -33
  98. package/dist/{extensions-QVDOHDGJ.js → extensions-IQL36S7K.js} +5 -5
  99. package/dist/{fleet-7XMJNQNF.js → fleet-BDKYJFCP.js} +243 -370
  100. package/dist/fleet-commands-ZFIWZSB3.js +70 -0
  101. package/dist/fleet-graph-Y6HPXIVF.js +125 -0
  102. package/dist/fleet-new-RDVJLHHH.js +48 -0
  103. package/dist/{fleet-preflight-AQNAH644.js → fleet-preflight-BHSNPBMH.js} +2 -2
  104. package/dist/fleet-validate-BIYREGIK.js +79 -0
  105. package/dist/{init-JGNPAYXT.js → init-LQUB5COQ.js} +57 -48
  106. package/dist/library-NJAHIGG4.js +217 -0
  107. package/dist/memory-OG6HOYKM.js +472 -0
  108. package/dist/{models-ZMMLFJNN.js → models-5ZG5XY7J.js} +23 -22
  109. package/dist/{monitor-2F3T5KHP.js → monitor-TJ7AMTGB.js} +69 -35
  110. package/dist/{orchestrator-ORHT43JB.js → orchestrator-WZYB54DM.js} +4868 -1189
  111. package/dist/{paths-UXLN5YYZ.js → paths-XUC7GS6E.js} +5 -5
  112. package/dist/{reset-NXGTYNUO.js → reset-PXQT45IY.js} +8 -8
  113. package/dist/{run-RF4WJGMT.js → run-FQ74YF62.js} +82 -62
  114. package/dist/{share-UT3W6E4M.js → share-FW7SVCL3.js} +34 -10
  115. package/dist/{skills-PSACKC5Q.js → skills-7E7IRB3R.js} +25 -9
  116. package/dist/{skills-eval-WJSI55RZ.js → skills-eval-LI75W6OK.js} +43 -35
  117. package/dist/{targets-PIIRAOYS.js → targets-4CIFKCTW.js} +27 -24
  118. package/dist/{terminal-lease-ULWXWNVY.js → terminal-lease-WUZY7ZV5.js} +5 -4
  119. package/dist/{uninstall-FZCQCDKC.js → uninstall-7FV7IP4E.js} +5 -5
  120. package/dist/{upgrade-346TZ6AV.js → upgrade-K2HVIVMQ.js} +21 -20
  121. package/dist/{usage-6KKXR32N.js → usage-GTZELZQX.js} +159 -59
  122. package/dist/verifiers-RLAHT27O.js +336 -0
  123. package/dist/{verify-X5HDROLA.js → verify-BX3BRKH5.js} +7 -6
  124. package/dist/{wiki-generate-7STOCIFZ.js → wiki-generate-ASIFASCN.js} +58 -48
  125. package/dist/worker/entry.js +98 -84
  126. package/dist/{workspace-G4ZWUIPR.js → workspace-ZJ6BFM3Q.js} +4 -4
  127. package/docs/README.md +4 -3
  128. package/docs/acp.md +1 -1
  129. package/docs/alcf-provider.md +1 -1
  130. package/docs/architecture.md +2 -2
  131. package/docs/artifact-placement.md +1 -2
  132. package/docs/artifact-versions.md +10 -6
  133. package/docs/built-in-agents.md +26 -2
  134. package/docs/capacity-and-scheduling.md +1 -1
  135. package/docs/commands-and-modes.md +90 -8
  136. package/docs/configuration-and-targets.md +90 -2
  137. package/docs/context-engine.md +4 -2
  138. package/docs/context-working-set.md +4 -4
  139. package/docs/development-pipeline.md +1 -1
  140. package/docs/dispatch-architecture-rationale.md +1 -1
  141. package/docs/documentation-coverage.md +4 -4
  142. package/docs/documentation-guide.md +4 -4
  143. package/docs/eval-runner.md +1 -1
  144. package/docs/evals-internal.md +4 -45
  145. package/docs/evidence-and-memory.md +70 -10
  146. package/docs/evolution.md +1 -1
  147. package/docs/exit-codes-and-output.md +4 -1
  148. package/docs/extensions-and-sharing.md +6 -2
  149. package/docs/fleet-demo-runbook.md +2 -2
  150. package/docs/fleet-dispatch.md +224 -11
  151. package/docs/git-commit-provenance.md +2 -2
  152. package/docs/glossary.md +1 -1
  153. package/docs/installation-and-lifecycle.md +2 -2
  154. package/docs/middleware-and-components.md +20 -2
  155. package/docs/model-catalog.md +1 -1
  156. package/docs/observability.md +55 -8
  157. package/docs/proactive-memory.md +26 -16
  158. package/docs/prompt-envelope-and-tools.md +4 -2
  159. package/docs/provider-adapter-cookbook.md +1 -1
  160. package/docs/release-cut-checklist.md +83 -65
  161. package/docs/resource-library.md +59 -0
  162. package/docs/safety-model.md +29 -7
  163. package/docs/scientific-validation.md +3 -3
  164. package/docs/session-lifecycle.md +37 -1
  165. package/docs/skills-marketplace.md +16 -3
  166. package/docs/tool-usage.md +14 -7
  167. package/docs/trace-store.md +1 -1
  168. package/docs/troubleshooting.md +1 -1
  169. package/docs/tui-design.md +38 -4
  170. package/docs/worker-dispatch-mechanics.md +3 -3
  171. package/package.json +7 -4
  172. package/src/cli/agents.ts +2 -3
  173. package/src/cli/argv.ts +14 -1
  174. package/src/cli/fleet-commands.ts +37 -0
  175. package/src/cli/fleet-graph.ts +102 -0
  176. package/src/cli/fleet-new.ts +36 -0
  177. package/src/cli/fleet-preflight.ts +121 -0
  178. package/src/cli/fleet-validate.ts +30 -0
  179. package/src/cli/fleet.ts +188 -335
  180. package/src/cli/index.ts +4 -2
  181. package/src/cli/library.ts +190 -0
  182. package/src/cli/memory.ts +272 -10
  183. package/src/cli/modes/json-stream.ts +2 -2
  184. package/src/cli/modes/print.ts +12 -1
  185. package/src/cli/run.ts +22 -2
  186. package/src/cli/share.ts +13 -1
  187. package/src/cli/targets.ts +12 -3
  188. package/src/cli/usage.ts +160 -20
  189. package/src/core/bus-events.ts +7 -0
  190. package/src/core/commit-attribution.ts +4 -4
  191. package/src/core/config.ts +130 -0
  192. package/src/core/defaults.ts +81 -0
  193. package/src/core/response-model-id.ts +134 -0
  194. package/src/core/toml.ts +62 -0
  195. package/src/core/workspace-files.ts +0 -1
  196. package/src/domains/agents/builtins/architect.md +2 -1
  197. package/src/domains/agents/builtins/oracle.md +33 -0
  198. package/src/domains/agents/catalog.ts +18 -5
  199. package/src/domains/agents/fleet-contract.ts +278 -16
  200. package/src/domains/agents/index.ts +14 -0
  201. package/src/domains/agents/recipe.ts +54 -14
  202. package/src/domains/agents/result-contract.ts +242 -5
  203. package/src/domains/config/classify.ts +4 -0
  204. package/src/domains/context/bootstrap.ts +36 -27
  205. package/src/domains/context/project-metadata.ts +19 -63
  206. package/src/domains/context/prompt-context.ts +8 -0
  207. package/src/domains/context/working-set/policies/index.ts +3 -4
  208. package/src/domains/dispatch/active-route-planner.ts +14 -0
  209. package/src/domains/dispatch/backoff.ts +2 -1
  210. package/src/domains/dispatch/budget-envelope.ts +396 -0
  211. package/src/domains/dispatch/capability-match.ts +1 -0
  212. package/src/domains/dispatch/checkout-writer-lease.ts +175 -0
  213. package/src/domains/dispatch/contract.ts +36 -0
  214. package/src/domains/dispatch/delegation-plan.ts +167 -0
  215. package/src/domains/dispatch/execution-plan.ts +76 -5
  216. package/src/domains/dispatch/execution-role.ts +3 -1
  217. package/src/domains/dispatch/execution-scheduler.ts +183 -67
  218. package/src/domains/dispatch/extension.ts +339 -36
  219. package/src/domains/dispatch/fleet-gate.ts +14 -0
  220. package/src/domains/dispatch/fleet-plan.ts +63 -3
  221. package/src/domains/dispatch/fleet-run.ts +737 -0
  222. package/src/domains/dispatch/gate-role-prompts.ts +9 -0
  223. package/src/domains/dispatch/host-verification.ts +178 -0
  224. package/src/domains/dispatch/index.ts +38 -0
  225. package/src/domains/dispatch/intent.ts +159 -0
  226. package/src/domains/dispatch/orphan-recovery.ts +1 -0
  227. package/src/domains/dispatch/receipt-integrity.ts +12 -4
  228. package/src/domains/dispatch/state.ts +37 -3
  229. package/src/domains/dispatch/types.ts +61 -9
  230. package/src/domains/dispatch/validation.ts +80 -6
  231. package/src/domains/dispatch/worker-spawn.ts +14 -3
  232. package/src/domains/eval/metrics/evidence.ts +0 -116
  233. package/src/domains/eval/metrics/invariants.ts +1 -1
  234. package/src/domains/eval/runners/clio-run.ts +1 -10
  235. package/src/domains/eval/runners/external-command.ts +2 -29
  236. package/src/domains/eval/schema/suite.ts +0 -7
  237. package/src/domains/eval/suites/run.ts +1 -7
  238. package/src/domains/evidence/trust-status.ts +10 -1
  239. package/src/domains/memory/index.ts +22 -0
  240. package/src/domains/memory/operations.ts +58 -1
  241. package/src/domains/memory/promotion.ts +281 -0
  242. package/src/domains/memory/prompt-section.ts +25 -5
  243. package/src/domains/memory/proposal.ts +51 -7
  244. package/src/domains/memory/task-bank.ts +3 -2
  245. package/src/domains/memory/task-memory-handoff.ts +181 -24
  246. package/src/domains/memory/task-memory-policy.ts +3 -1
  247. package/src/domains/memory/types.ts +37 -0
  248. package/src/domains/memory/validate.ts +178 -0
  249. package/src/domains/middleware/index.ts +15 -0
  250. package/src/domains/middleware/memory-intervention.ts +35 -25
  251. package/src/domains/middleware/runtime.ts +6 -0
  252. package/src/domains/middleware/skills-reminder.ts +19 -4
  253. package/src/domains/middleware/stalled-turn.ts +43 -1
  254. package/src/domains/middleware/types.ts +10 -0
  255. package/src/domains/middleware/watchdog.ts +281 -0
  256. package/src/domains/observability/contract.ts +9 -2
  257. package/src/domains/observability/cost.ts +31 -4
  258. package/src/domains/observability/extension.ts +2 -2
  259. package/src/domains/observability/index.ts +10 -0
  260. package/src/domains/observability/out-of-turn-usage.ts +223 -0
  261. package/src/domains/providers/index.ts +3 -0
  262. package/src/domains/providers/model-discovery.ts +9 -0
  263. package/src/domains/providers/runtime-resolution.ts +38 -1
  264. package/src/domains/providers/runtimes/common/probe-helpers.ts +97 -16
  265. package/src/domains/providers/types/context-window-slots.ts +18 -0
  266. package/src/domains/providers/types/runtime-descriptor.ts +3 -1
  267. package/src/domains/resources/index.ts +20 -0
  268. package/src/domains/resources/library.ts +326 -0
  269. package/src/domains/resources/skills/marketplace.ts +37 -12
  270. package/src/domains/safety/call-target.ts +211 -14
  271. package/src/domains/safety/decision-presentation.ts +268 -0
  272. package/src/domains/safety/redaction.ts +73 -0
  273. package/src/domains/session/context-ledger.ts +10 -1
  274. package/src/domains/session/decision-board.ts +4 -0
  275. package/src/domains/session/entries.ts +3 -0
  276. package/src/domains/session/handoff.ts +629 -0
  277. package/src/domains/session/history.ts +68 -19
  278. package/src/domains/session/usage.ts +24 -7
  279. package/src/domains/share/archive.ts +67 -2
  280. package/src/engine/acp/event-mapper.ts +7 -0
  281. package/src/engine/acp/server.ts +29 -2
  282. package/src/engine/apis/lmstudio.ts +25 -4
  283. package/src/engine/apis/openai-completions.ts +147 -22
  284. package/src/engine/claude/sdk-runtime.ts +8 -2
  285. package/src/engine/claude/tool-safety.ts +13 -0
  286. package/src/engine/loop-guard.ts +27 -3
  287. package/src/engine/worker-events.ts +4 -3
  288. package/src/engine/worker-runtime.ts +59 -54
  289. package/src/entry/orchestrator.ts +55 -1
  290. package/src/interactive/bus-notices.ts +26 -0
  291. package/src/interactive/chat-loop-messages.ts +22 -0
  292. package/src/interactive/chat-loop.ts +248 -1
  293. package/src/interactive/chat-renderer.ts +41 -3
  294. package/src/interactive/clio-editor.ts +44 -7
  295. package/src/interactive/context-overlay.ts +43 -5
  296. package/src/interactive/cost-overlay.ts +70 -11
  297. package/src/interactive/council-dispatch.ts +30 -0
  298. package/src/interactive/council-grid.ts +213 -0
  299. package/src/interactive/council.ts +99 -0
  300. package/src/interactive/dispatch-board.ts +471 -50
  301. package/src/interactive/fleet-run-preview.ts +307 -0
  302. package/src/interactive/footer/notifications.ts +219 -0
  303. package/src/interactive/footer/widgets.ts +13 -0
  304. package/src/interactive/handoff-round.ts +56 -0
  305. package/src/interactive/interactive-application.ts +49 -2
  306. package/src/interactive/interactive-event-projection.ts +9 -1
  307. package/src/interactive/interactive-input-runtime.ts +11 -1
  308. package/src/interactive/interactive-presentation.ts +11 -1
  309. package/src/interactive/interactive-slash-runtime.ts +52 -2
  310. package/src/interactive/interactive-subscriptions.ts +14 -2
  311. package/src/interactive/memory-overlay.ts +89 -4
  312. package/src/interactive/oracle.ts +179 -0
  313. package/src/interactive/overlay-ask-user-lifecycle.ts +7 -1
  314. package/src/interactive/overlay-frame.ts +5 -2
  315. package/src/interactive/overlay-general-openers.ts +230 -2
  316. package/src/interactive/overlay-key-routing.ts +58 -2
  317. package/src/interactive/overlay-lifecycle.ts +52 -5
  318. package/src/interactive/overlay-permission-lifecycle.ts +33 -8
  319. package/src/interactive/overlay-resource-openers.ts +11 -3
  320. package/src/interactive/overlay-session-lifecycle.ts +234 -2
  321. package/src/interactive/overlay-transitions.ts +11 -0
  322. package/src/interactive/overlays/ask-user.ts +74 -30
  323. package/src/interactive/overlays/decisions.ts +3 -1
  324. package/src/interactive/overlays/fleet-run-approval.ts +208 -0
  325. package/src/interactive/overlays/handoff-review.ts +185 -0
  326. package/src/interactive/overlays/library-install-confirm.ts +151 -0
  327. package/src/interactive/overlays/list-overlay.ts +168 -2
  328. package/src/interactive/overlays/settings.ts +101 -4
  329. package/src/interactive/overlays/side-question.ts +139 -0
  330. package/src/interactive/overlays/skills-hub.ts +401 -15
  331. package/src/interactive/permission-hint.ts +35 -0
  332. package/src/interactive/permission-overlay.ts +95 -45
  333. package/src/interactive/renderers/tool-execution.ts +19 -49
  334. package/src/interactive/session-last-turn.ts +8 -1
  335. package/src/interactive/session-usage-reseed.ts +36 -10
  336. package/src/interactive/side-question.ts +171 -0
  337. package/src/interactive/slash-commands.ts +434 -7
  338. package/src/interactive/slash-spec.ts +19 -6
  339. package/src/interactive/status/summary.ts +5 -0
  340. package/src/interactive/status/types.ts +5 -0
  341. package/src/interactive/terminal-lease.ts +1 -0
  342. package/src/interactive/theme/tokens.ts +30 -0
  343. package/src/interactive/turn-context.ts +96 -23
  344. package/src/interactive/turn-middleware.ts +16 -1
  345. package/src/interactive/turn-runtime.ts +37 -8
  346. package/src/interactive/turn-state.ts +3 -0
  347. package/src/interactive/watchdog-run.ts +75 -0
  348. package/src/interactive/worker-progress.ts +440 -0
  349. package/src/interactive/worker-share.ts +56 -1
  350. package/src/interactive/worker-stream.ts +58 -110
  351. package/src/tools/agent-tools.ts +28 -3
  352. package/src/tools/ask-user.ts +21 -1
  353. package/src/tools/bootstrap.ts +3 -0
  354. package/src/tools/compete-worktrees.ts +13 -79
  355. package/src/tools/context/index.ts +2 -2
  356. package/src/tools/dispatch-admission.ts +242 -8
  357. package/src/tools/dispatch-arguments.ts +65 -1
  358. package/src/tools/dispatch-event-text.ts +19 -0
  359. package/src/tools/dispatch-plan.ts +136 -6
  360. package/src/tools/dispatch-runner.ts +319 -13
  361. package/src/tools/dispatch-types.ts +20 -1
  362. package/src/tools/dispatch.ts +96 -3
  363. package/src/tools/monitor.ts +31 -0
  364. package/src/tools/profiles.ts +18 -4
  365. package/src/tools/registry.ts +15 -5
  366. package/src/tools/result-disposition.ts +156 -0
  367. package/src/tools/result-shaping.ts +59 -1
  368. package/src/tools/task-worktree.ts +238 -0
  369. package/src/tools/verify/authoring.ts +116 -55
  370. package/src/tools/verify/scripts.ts +62 -0
  371. package/src/tools/worker-evidence.ts +21 -1
  372. package/src/worker/spec-contract.ts +44 -3
  373. package/dist/chunk-EFADSJET.js +0 -18
  374. package/dist/chunk-HC4CLZ2Y.js +0 -68
  375. package/dist/memory-4ALKDJ4Q.js +0 -246
  376. package/src/domains/eval/metrics/chaos-stream.ts +0 -93
@@ -1,3 +1,8 @@
1
+ import {
2
+ classifyDecisionPresentation,
3
+ type DecisionPresentation,
4
+ decisionFactsForAnswer,
5
+ } from "../../domains/safety/decision-presentation.js";
1
6
  import {
2
7
  type Component,
3
8
  Input,
@@ -18,16 +23,14 @@ import { type ClioToken, clioTheme, dotSep, GLYPH, screenTitle } from "../theme/
18
23
  import { nextContentScrollOffset, type ViewScrollAction } from "../view/view-overlay.js";
19
24
 
20
25
  /**
21
- * The border and title token while a decision is pending.
22
- *
23
- * Orange is the token that means Clio is acting, and a prompt holding the
24
- * keyboard is the moment the operator has to act with it. Everything
25
- * informational keeps the teal frame, so the one border that is not teal is the
26
- * one asking for an answer. All three surfaces carry it: what changes between
27
- * them is the size and the place, never the signal.
26
+ * The default title and border token for a local conversational answer.
27
+ * Outward and other consequence tiers replace both values with their typed
28
+ * presentation. All ask_user surfaces carry the same classified signal.
28
29
  */
29
- export const ASK_USER_DECISION_TONE: ClioToken = "action";
30
- export const ASK_USER_DECISION_TITLE = "Decision required";
30
+ const DEFAULT_ASK_USER_PRESENTATION = classifyDecisionPresentation(decisionFactsForAnswer("local"));
31
+
32
+ export const ASK_USER_DECISION_TONE: ClioToken = DEFAULT_ASK_USER_PRESENTATION.semanticToken;
33
+ export const ASK_USER_DECISION_TITLE = DEFAULT_ASK_USER_PRESENTATION.title;
31
34
  export const ASK_USER_WAITING_TITLE = "Ask User";
32
35
 
33
36
  /**
@@ -73,7 +76,7 @@ interface AskUserSurfaceGeometry {
73
76
  * `ASK_USER_FRAME_AND_MARGIN_ROWS` counts alongside the two border rows.
74
77
  */
75
78
  export const ASK_USER_SURFACE_GEOMETRY: Record<AskUserSurface, AskUserSurfaceGeometry> = {
76
- compact: { width: 68, maxInnerRows: 10, anchor: "bottom-center", margin: { top: 1, right: 2, bottom: 1, left: 2 } },
79
+ compact: { width: 68, maxInnerRows: 14, anchor: "bottom-center", margin: { top: 1, right: 2, bottom: 1, left: 2 } },
77
80
  panel: { width: 88, maxInnerRows: 18, anchor: "center", margin: 1 },
78
81
  // The interview is a workspace, not a modal. A cap of 42 turned a 66-row
79
82
  // terminal into a centered box with twenty blank rows around it, so the
@@ -111,7 +114,7 @@ export interface OpenAskUserOverlayDeps {
111
114
  }
112
115
 
113
116
  export interface AskUserOverlaySession extends OverlayHandle {
114
- ask(questions: ReadonlyArray<AskUserQuestion>): Promise<AskUserResult>;
117
+ ask(questions: ReadonlyArray<AskUserQuestion>, presentation?: DecisionPresentation): Promise<AskUserResult>;
115
118
  cancel(): void;
116
119
  close(): void;
117
120
  isWaiting(): boolean;
@@ -242,16 +245,21 @@ class AskUserOverlayView implements Component {
242
245
  private contentRegionHeight = 1;
243
246
  /** Rows the scroll region wanted on the last render. */
244
247
  private contentTotalRows = 0;
248
+ private presentation: DecisionPresentation = DEFAULT_ASK_USER_PRESENTATION;
245
249
 
246
250
  constructor(private readonly deps: AskUserOverlayViewDeps) {}
247
251
 
248
- begin(questions: ReadonlyArray<AskUserQuestion>): Promise<AskUserResult> {
252
+ begin(
253
+ questions: ReadonlyArray<AskUserQuestion>,
254
+ presentation: DecisionPresentation = DEFAULT_ASK_USER_PRESENTATION,
255
+ ): Promise<AskUserResult> {
249
256
  if (this.phase === "closed") return Promise.resolve(cancelledAskUserResult());
250
257
  if (this.resolveCurrent) return Promise.resolve(cancelledAskUserResult());
251
258
  this.phase = "asking";
252
259
  this.index = 0;
253
260
  this.status = "";
254
261
  this.questions = [...questions];
262
+ this.presentation = presentation;
255
263
  this.states = this.questions.map((question) => createQuestionState(question));
256
264
  this.list = null;
257
265
  this.input = null;
@@ -288,6 +296,14 @@ class AskUserOverlayView implements Component {
288
296
  return this.surface;
289
297
  }
290
298
 
299
+ decisionTitle(): string {
300
+ return this.presentation.title;
301
+ }
302
+
303
+ decisionTone(): ClioToken {
304
+ return this.presentation.semanticToken;
305
+ }
306
+
291
307
  invalidate(): void {
292
308
  this.list?.invalidate();
293
309
  this.input?.invalidate();
@@ -358,10 +374,17 @@ class AskUserOverlayView implements Component {
358
374
  const statusLines = this.status.length > 0 ? ["", fitLine(clioTheme().fg("dim", this.status), safeWidth)] : [];
359
375
  const headerLine = this.renderQuestionHeader(question, safeWidth);
360
376
  const headerLines = headerLine.length > 0 ? [headerLine] : [];
361
- const spent = headerLines.length + statusLines.length + 1 + controlLines.length;
362
- const questionLines = wrapTextWithAnsi(question.question, safeWidth).slice(0, Math.max(1, maxRows - spent));
363
-
364
- return [...headerLines, ...questionLines, ...statusLines, "", ...controlLines].slice(0, maxRows);
377
+ const bottom = [...statusLines, "", ...controlLines];
378
+ const topBudget = Math.max(1, maxRows - bottom.length);
379
+ const contextLines = this.renderDecisionContext(safeWidth);
380
+ const top = [
381
+ contextLines[0] ?? "",
382
+ ...headerLines,
383
+ ...wrapTextWithAnsi(question.question, safeWidth),
384
+ ...contextLines.slice(1),
385
+ ].slice(0, topBudget);
386
+
387
+ return [...top, ...bottom].slice(0, maxRows);
365
388
  }
366
389
 
367
390
  /**
@@ -380,7 +403,12 @@ class AskUserOverlayView implements Component {
380
403
  const headerLine = this.renderQuestionHeader(question, width);
381
404
  const headerLines = headerLine.length > 0 ? [headerLine] : [];
382
405
  const stripLines = [this.renderQuestionStrip(width), ""];
383
- const content = [...wrapTextWithAnsi(question.question, width), ...this.renderAnswerLedger(width)];
406
+ const content = [
407
+ ...wrapTextWithAnsi(question.question, width),
408
+ "",
409
+ ...this.renderDecisionContext(width),
410
+ ...this.renderAnswerLedger(width),
411
+ ];
384
412
 
385
413
  // Chrome sheds from the outside in when the terminal is too short to carry
386
414
  // all of it: the round strip first, then the header, then the status, and
@@ -460,36 +488,52 @@ class AskUserOverlayView implements Component {
460
488
  const question = this.currentQuestion();
461
489
  const state = this.currentState();
462
490
  if (!question || !state) return buildHint([]);
491
+ const recordAnswer =
492
+ this.presentation.requiredActions.find((action) => action.id === "record-answer")?.label.toLowerCase() ??
493
+ "record answer";
463
494
  if (state.mode === "text") {
464
495
  return this.questions.length > 1
465
496
  ? this.withScrollHint([
466
- { key: "Enter", verb: "submit" },
497
+ { key: "Enter", verb: recordAnswer },
467
498
  { key: "Alt+Left/Right", verb: "question" },
468
499
  ])
469
- : this.withScrollHint([{ key: "Enter", verb: "submit" }]);
500
+ : this.withScrollHint([{ key: "Enter", verb: recordAnswer }]);
470
501
  }
471
- // `accept` rather than `select` or `commit`: the footer of a decision prompt
472
- // has one job, which is to name the key that answers it. Esc keeps the
473
- // product's one word for the way out, since it closes the prompt and the
474
- // interview together.
502
+ // The classified action label distinguishes recording an answer from
503
+ // granting authority. Esc keeps the product's one word for the way out,
504
+ // since it closes the prompt and the interview together.
475
505
  if (question.multi_select === true) {
476
506
  return this.questions.length > 1
477
507
  ? this.withScrollHint([
478
508
  { key: "Left/Right", verb: "question" },
479
509
  { key: "Space", verb: "toggle" },
480
- { key: "Enter", verb: "accept" },
510
+ { key: "Enter", verb: recordAnswer },
481
511
  ])
482
512
  : this.withScrollHint([
483
513
  { key: "Space", verb: "toggle" },
484
- { key: "Enter", verb: "accept" },
514
+ { key: "Enter", verb: recordAnswer },
485
515
  ]);
486
516
  }
487
517
  return this.questions.length > 1
488
518
  ? this.withScrollHint([
489
519
  { key: "Left/Right", verb: "question" },
490
- { key: "Enter", verb: "accept" },
520
+ { key: "Enter", verb: recordAnswer },
491
521
  ])
492
- : this.withScrollHint([{ key: "Enter", verb: "accept" }]);
522
+ : this.withScrollHint([{ key: "Enter", verb: recordAnswer }]);
523
+ }
524
+
525
+ private renderDecisionContext(width: number): string[] {
526
+ const theme = clioTheme();
527
+ const tier = theme.style(this.presentation.semanticToken, this.presentation.tierLabel, { bold: true });
528
+ const requested = theme.fg("dim", `requested by ${this.presentation.requestedByCopy}`);
529
+ const effect = `${theme.fg("dim", "Effect:")} ${theme.fg("muted", this.presentation.authorizationCopy)}`;
530
+ return [
531
+ fitLine(`${tier}${dotSep(theme)}${requested}`, width),
532
+ ...wrapTextWithAnsi(effect, width).map((line) => fitLine(line, width)),
533
+ ...wrapTextWithAnsi(theme.fg("muted", this.presentation.reversibilityCopy), width).map((line) =>
534
+ fitLine(line, width),
535
+ ),
536
+ ];
493
537
  }
494
538
 
495
539
  /**
@@ -1012,8 +1056,8 @@ export function openAskUserOverlay(tui: TUI, deps: OpenAskUserOverlayDeps): AskU
1012
1056
  // the terminal has left after the margins.
1013
1057
  ...(geometry.maxInnerRows > 0 ? { maxHeight: geometry.maxInnerRows + ASK_USER_FRAME_ROWS } : {}),
1014
1058
  margin: geometry.margin,
1015
- title: () => (view.isDecisionPending() ? ASK_USER_DECISION_TITLE : ASK_USER_WAITING_TITLE),
1016
- tone: () => (view.isDecisionPending() ? ASK_USER_DECISION_TONE : undefined),
1059
+ title: () => (view.isDecisionPending() ? view.decisionTitle() : ASK_USER_WAITING_TITLE),
1060
+ tone: () => (view.isDecisionPending() ? view.decisionTone() : undefined),
1017
1061
  footerHint: () => view.footerHint(),
1018
1062
  });
1019
1063
  mounted = surface;
@@ -1034,7 +1078,7 @@ export function openAskUserOverlay(tui: TUI, deps: OpenAskUserOverlayDeps): AskU
1034
1078
  focus: () => handle?.focus(),
1035
1079
  unfocus: (options) => (options ? handle?.unfocus(options) : handle?.unfocus()),
1036
1080
  isFocused: () => handle?.isFocused() ?? false,
1037
- ask: (questions) => view.begin(questions),
1081
+ ask: (questions, presentation) => view.begin(questions, presentation),
1038
1082
  cancel: () => view.cancel(),
1039
1083
  close,
1040
1084
  hide: close,
@@ -1,3 +1,4 @@
1
+ import { classifyDecisionPresentation, decisionFactsForAnswer } from "../../domains/safety/decision-presentation.js";
1
2
  import type { DecisionLedgerEntry, DecisionRecord } from "../../domains/session/entries.js";
2
3
  import {
3
4
  type Component,
@@ -73,10 +74,11 @@ function selectableRows(interviews: ReadonlyArray<DecisionLedgerEntry>): Selecta
73
74
 
74
75
  function interviewHeader(interview: DecisionLedgerEntry, width: number, now: number): string[] {
75
76
  const theme = clioTheme();
77
+ const presentation = classifyDecisionPresentation(decisionFactsForAnswer(interview.exposure ?? "local"));
76
78
  const rounds = `${interview.roundCount} round${interview.roundCount === 1 ? "" : "s"}`;
77
79
  const status =
78
80
  interview.interviewStatus === "complete" ? theme.fg("success", "complete") : theme.fg("warning", "cancelled");
79
- const heading = `${theme.fg("accent", relativeTime(interview.endedAt, now))}${theme.fg("dim", " · ")}${theme.fg("muted", rounds)}${theme.fg("dim", " · ")}${status}`;
81
+ const heading = `${theme.style(presentation.semanticToken, presentation.tierLabel, { bold: true })}${theme.fg("dim", " · ")}${theme.fg("accent", relativeTime(interview.endedAt, now))}${theme.fg("dim", " · ")}${theme.fg("muted", rounds)}${theme.fg("dim", " · ")}${status}`;
80
82
  const lines = [fitLine(heading, width)];
81
83
  if (interview.summary) {
82
84
  lines.push(...wrapTextWithAnsi(theme.fg("dim", interview.summary), width).map((line) => fitLine(line, width)));
@@ -0,0 +1,208 @@
1
+ /**
2
+ * `/fleet run <name>` approval: the whole plan, before anything dispatches.
3
+ *
4
+ * The operator reads the compiled plan here and decides. Enter accepts and the
5
+ * run starts; Esc cancels and nothing is dispatched, nothing is written, and no
6
+ * ledger row exists. A contract that failed preflight opens the same overlay
7
+ * with its diagnostics and no accept key, so the only way out of a broken plan
8
+ * is to leave it.
9
+ *
10
+ * The body is a pure function of the projection so the layout is testable
11
+ * without a TUI.
12
+ */
13
+
14
+ import {
15
+ type Component,
16
+ isKeyRelease,
17
+ matchesKey,
18
+ type OverlayHandle,
19
+ type TUI,
20
+ truncateToWidth,
21
+ wrapTextWithAnsi,
22
+ } from "../../engine/tui.js";
23
+ import type { FleetRunPreview, FleetRunPreviewStep } from "../fleet-run-preview.js";
24
+ import { buildResponsiveHint, FocusBox, showClioOverlayFrame } from "../overlay-frame.js";
25
+ import { clioTheme, rule } from "../theme/index.js";
26
+
27
+ export const FLEET_RUN_APPROVAL_OVERLAY_TITLE = "Fleet run approval";
28
+
29
+ const FLEET_RUN_APPROVAL_MIN_WIDTH = 48;
30
+ const FLEET_RUN_APPROVAL_MAX_WIDTH = 110;
31
+ /** Rows of plan shown at once. Anything longer scrolls. */
32
+ export const FLEET_RUN_APPROVAL_VISIBLE_ROWS = 22;
33
+
34
+ function fleetRunApprovalOverlayWidth(columns: number): number {
35
+ return Math.max(FLEET_RUN_APPROVAL_MIN_WIDTH, Math.min(FLEET_RUN_APPROVAL_MAX_WIDTH, columns - 4));
36
+ }
37
+
38
+ /** What the overlay renders: an accepted-shaped plan or the reasons there is none. */
39
+ export type FleetRunApprovalSubject =
40
+ | { ok: true; preview: FleetRunPreview }
41
+ | { ok: false; name: string; diagnostics: ReadonlyArray<string> };
42
+
43
+ export interface OpenFleetRunApprovalOverlayOptions {
44
+ subject: FleetRunApprovalSubject;
45
+ /** Live terminal width, so the box tracks the window it opened in. */
46
+ columns: number;
47
+ /** Enter: dispatch this exact plan. Never called for a failed preflight. */
48
+ onAccept: () => void;
49
+ /** Esc: nothing is dispatched and nothing is written. */
50
+ onCancel: () => void;
51
+ }
52
+
53
+ /** The declared boundary as one phrase: an empty allowlist is a claim, not an absence. */
54
+ export function formatWriteBoundary(writes: ReadonlyArray<string> | undefined): string {
55
+ if (writes === undefined) return "writes unenforced";
56
+ if (writes.length === 0) return "writes nothing";
57
+ return `writes ${writes.join(", ")}`;
58
+ }
59
+
60
+ /** One step as its facts: kind, who runs it, where, its scope, and its boundary. */
61
+ export function formatFleetRunPreviewStep(step: FleetRunPreviewStep): string {
62
+ const loop = step.loop === undefined ? "" : ` (${step.loop.loopId} ${step.loop.role} ${step.loop.attempt})`;
63
+ if (step.kind === "code") {
64
+ const gate = step.gate === undefined ? "" : ` · gate path ${step.gate.path}`;
65
+ return `code ${step.stepId}${loop} · command ${step.commandId ?? "?"}${gate} · ${step.scope} · ${formatWriteBoundary(step.writes)}`;
66
+ }
67
+ const route = step.route === undefined ? "route unresolved" : `${step.route.targetId} ▸ ${step.route.wireModelId}`;
68
+ const node = step.route === undefined ? "" : ` · node ${step.route.nodeId}`;
69
+ const declaredRoute =
70
+ step.target !== undefined
71
+ ? ` · target ${step.target}`
72
+ : step.profile !== undefined
73
+ ? ` · profile ${step.profile}`
74
+ : "";
75
+ const gate =
76
+ step.gate !== undefined
77
+ ? ` · gate path ${step.gate.path} · run ${"commandId" in step.gate ? step.gate.commandId : "?"} · baseline ${"commandId" in step.gate ? step.gate.commandId : "?"}`
78
+ : "";
79
+ const dynamicPlan =
80
+ step.plan === undefined
81
+ ? ""
82
+ : ` · roster ${step.plan.roster.join(", ")} · maxTasks ${step.plan.maxTasks}${step.plan.proposals ? " · proposals" : ""}`;
83
+ return `agent ${step.stepId}${loop} · ${step.agentId ?? "?"} · ${route}${node}${declaredRoute}${gate}${dynamicPlan} · ${step.scope} · ${formatWriteBoundary(step.writes)}`;
84
+ }
85
+
86
+ function formatBudgetLine(preview: FleetRunPreview): string {
87
+ const contract =
88
+ preview.budget.contractUsd === null
89
+ ? "contract declares no ceiling"
90
+ : `contract ceiling $${preview.budget.contractUsd.toFixed(2)}`;
91
+ return `budget: admitted under $${preview.budget.ceilingUsd.toFixed(2)} session ceiling, $${preview.budget.currentUsd.toFixed(4)} spent, ${contract}`;
92
+ }
93
+
94
+ /**
95
+ * Render the body. Waves in order, each step with its facts, then the argv a
96
+ * code step will actually run, and last the budget the run is admitted under.
97
+ */
98
+ export function formatFleetRunApprovalBody(subject: FleetRunApprovalSubject, width: number, scroll: number): string[] {
99
+ const theme = clioTheme();
100
+ const contentWidth = Math.max(1, Math.floor(width));
101
+ const rows: string[] = [];
102
+
103
+ if (!subject.ok) {
104
+ rows.push(theme.fg("error", `${subject.name}: preflight failed; nothing will be dispatched`));
105
+ rows.push(rule(theme, contentWidth));
106
+ for (const diagnostic of subject.diagnostics) {
107
+ for (const line of diagnostic.split("\n")) {
108
+ for (const wrapped of wrapTextWithAnsi(theme.fg("muted", line), contentWidth)) rows.push(wrapped);
109
+ }
110
+ }
111
+ } else {
112
+ const { preview } = subject;
113
+ rows.push(theme.fg("dim", `${preview.name} · plan ${preview.planHash.slice(0, 12)}`));
114
+ rows.push(rule(theme, contentWidth));
115
+ for (const wave of preview.waves) {
116
+ rows.push(theme.fg("accent", `wave ${wave.index}`));
117
+ for (const step of wave.steps) {
118
+ for (const line of wrapTextWithAnsi(theme.fg("muted", ` ${formatFleetRunPreviewStep(step)}`), contentWidth)) {
119
+ rows.push(line);
120
+ }
121
+ if (step.argv !== undefined) {
122
+ rows.push(theme.fg("dim", truncateToWidth(` argv ${step.argv.join(" ")}`, contentWidth, "…", false)));
123
+ }
124
+ }
125
+ }
126
+ rows.push(rule(theme, contentWidth));
127
+ for (const line of wrapTextWithAnsi(theme.fg("info", formatBudgetLine(preview)), contentWidth)) rows.push(line);
128
+ }
129
+
130
+ const maxScroll = Math.max(0, rows.length - FLEET_RUN_APPROVAL_VISIBLE_ROWS);
131
+ const start = Math.max(0, Math.min(scroll, maxScroll));
132
+ const shown = rows.slice(start, start + FLEET_RUN_APPROVAL_VISIBLE_ROWS);
133
+ if (rows.length > FLEET_RUN_APPROVAL_VISIBLE_ROWS) {
134
+ shown.push(theme.fg("dim", `(${start + 1}-${start + shown.length} of ${rows.length} lines)`));
135
+ }
136
+ return shown;
137
+ }
138
+
139
+ class FleetRunApprovalBody implements Component {
140
+ scroll = 0;
141
+
142
+ constructor(private readonly subject: FleetRunApprovalSubject) {}
143
+
144
+ render(width: number): string[] {
145
+ return formatFleetRunApprovalBody(this.subject, width, this.scroll);
146
+ }
147
+
148
+ invalidate(): void {}
149
+ }
150
+
151
+ export function openFleetRunApprovalOverlay(tui: TUI, options: OpenFleetRunApprovalOverlayOptions): OverlayHandle {
152
+ const body = new FleetRunApprovalBody(options.subject);
153
+ const acceptable = options.subject.ok;
154
+ let settled = false;
155
+
156
+ const accept = (): void => {
157
+ if (settled || !acceptable) return;
158
+ settled = true;
159
+ options.onAccept();
160
+ };
161
+ const cancel = (): void => {
162
+ if (settled) return;
163
+ settled = true;
164
+ options.onCancel();
165
+ };
166
+
167
+ const focus = new FocusBox(body, {
168
+ // Keys are matched by name, never by raw bytes: under the kitty keyboard
169
+ // protocol Esc arrives as CSI 27 u, and a byte comparison against "\x1b"
170
+ // left the overlay unanswerable. Everything unmatched is swallowed.
171
+ onInput: (data: string): void => {
172
+ if (settled || isKeyRelease(data)) return;
173
+ if (matchesKey(data, "up")) {
174
+ body.scroll = Math.max(0, body.scroll - 1);
175
+ tui.requestRender();
176
+ return;
177
+ }
178
+ if (matchesKey(data, "down")) {
179
+ body.scroll += 1;
180
+ tui.requestRender();
181
+ return;
182
+ }
183
+ if (matchesKey(data, "enter")) {
184
+ accept();
185
+ return;
186
+ }
187
+ if (matchesKey(data, "escape")) cancel();
188
+ },
189
+ });
190
+
191
+ const handle = showClioOverlayFrame(tui, focus, {
192
+ anchor: "center",
193
+ width: fleetRunApprovalOverlayWidth(options.columns),
194
+ title: FLEET_RUN_APPROVAL_OVERLAY_TITLE,
195
+ footerHint: buildResponsiveHint(
196
+ [...(acceptable ? [{ key: "Enter", verb: "dispatch" }] : []), { key: "↑↓", verb: "scroll" }],
197
+ { key: "Esc", verb: "cancel" },
198
+ ),
199
+ });
200
+
201
+ return {
202
+ ...handle,
203
+ hide(): void {
204
+ cancel();
205
+ handle.hide();
206
+ },
207
+ };
208
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * `/handoff` review: the document, before anything is written anywhere.
3
+ *
4
+ * The operator reads the rendered Markdown here and chooses. Enter accepts the
5
+ * document as it stands, `e` hands it to `$EDITOR` through the same external
6
+ * editor flow the composer uses and comes back with whatever was saved, and Esc
7
+ * cancels the whole handoff. Cancel is the important one: nothing has been
8
+ * written at the point this overlay opens, so leaving it costs the extraction
9
+ * round and nothing else.
10
+ */
11
+
12
+ import {
13
+ type Component,
14
+ isKeyRelease,
15
+ matchesKey,
16
+ type OverlayHandle,
17
+ type TUI,
18
+ wrapTextWithAnsi,
19
+ } from "../../engine/tui.js";
20
+ import { buildResponsiveHint, FocusBox, showClioOverlayFrame } from "../overlay-frame.js";
21
+ import { clioTheme, rule } from "../theme/index.js";
22
+
23
+ export const HANDOFF_REVIEW_OVERLAY_TITLE = "Handoff review";
24
+
25
+ const HANDOFF_REVIEW_MIN_WIDTH = 44;
26
+ const HANDOFF_REVIEW_MAX_WIDTH = 100;
27
+ /** Rows of document shown at once. Anything longer scrolls. */
28
+ export const HANDOFF_REVIEW_VISIBLE_ROWS = 20;
29
+
30
+ export function handoffReviewOverlayWidth(columns: number): number {
31
+ return Math.max(HANDOFF_REVIEW_MIN_WIDTH, Math.min(HANDOFF_REVIEW_MAX_WIDTH, columns - 4));
32
+ }
33
+
34
+ export interface OpenHandoffReviewOverlayOptions {
35
+ document: string;
36
+ goal: string;
37
+ /** Live terminal width, so the box tracks the window it opened in. */
38
+ columns: number;
39
+ /**
40
+ * Hand the current document to `$EDITOR` and return what came back, or null
41
+ * when the edit was cancelled or no editor is configured. The caller owns
42
+ * stopping and restarting the TUI around the child process.
43
+ */
44
+ onEdit: (current: string) => string | null;
45
+ /** Enter: the operator accepts this exact text. */
46
+ onAccept: (document: string) => void;
47
+ /** Esc: the whole handoff is abandoned and nothing is written. */
48
+ onCancel: () => void;
49
+ }
50
+
51
+ export interface HandoffReviewOverlaySession extends OverlayHandle {
52
+ /** The document as it currently stands, including any external edit. */
53
+ document(): string;
54
+ }
55
+
56
+ /**
57
+ * Render the body. Pure so the layout is testable without a TUI: the goal, a
58
+ * rule, then the scrolled document window.
59
+ */
60
+ export function formatHandoffReviewBody(goal: string, document: string, width: number, scroll: number): string[] {
61
+ const theme = clioTheme();
62
+ const contentWidth = Math.max(1, Math.floor(width));
63
+ const lines: string[] = [];
64
+ for (const line of wrapTextWithAnsi(theme.fg("dim", `goal: ${goal}`), contentWidth)) lines.push(line);
65
+ lines.push(rule(theme, contentWidth));
66
+
67
+ const wrapped: string[] = [];
68
+ for (const raw of document.split("\n")) {
69
+ if (raw.length === 0) {
70
+ wrapped.push("");
71
+ continue;
72
+ }
73
+ for (const line of wrapTextWithAnsi(theme.fg("muted", raw), contentWidth)) wrapped.push(line);
74
+ }
75
+ const maxScroll = Math.max(0, wrapped.length - HANDOFF_REVIEW_VISIBLE_ROWS);
76
+ const start = Math.max(0, Math.min(scroll, maxScroll));
77
+ const window = wrapped.slice(start, start + HANDOFF_REVIEW_VISIBLE_ROWS);
78
+ for (const line of window) lines.push(line);
79
+ if (wrapped.length > HANDOFF_REVIEW_VISIBLE_ROWS) {
80
+ lines.push(theme.fg("dim", `(${start + 1}-${start + window.length} of ${wrapped.length} lines)`));
81
+ }
82
+ return lines;
83
+ }
84
+
85
+ class HandoffReviewBody implements Component {
86
+ scroll = 0;
87
+
88
+ constructor(
89
+ private readonly goal: string,
90
+ private text: string,
91
+ ) {}
92
+
93
+ setDocument(next: string): void {
94
+ this.text = next;
95
+ this.scroll = 0;
96
+ }
97
+
98
+ document(): string {
99
+ return this.text;
100
+ }
101
+
102
+ lineCount(): number {
103
+ return this.text.split("\n").length;
104
+ }
105
+
106
+ render(width: number): string[] {
107
+ return formatHandoffReviewBody(this.goal, this.text, width, this.scroll);
108
+ }
109
+
110
+ invalidate(): void {}
111
+ }
112
+
113
+ export function openHandoffReviewOverlay(
114
+ tui: TUI,
115
+ options: OpenHandoffReviewOverlayOptions,
116
+ ): HandoffReviewOverlaySession {
117
+ const body = new HandoffReviewBody(options.goal, options.document);
118
+ let settled = false;
119
+
120
+ const accept = (): void => {
121
+ if (settled) return;
122
+ settled = true;
123
+ options.onAccept(body.document());
124
+ };
125
+ const cancel = (): void => {
126
+ if (settled) return;
127
+ settled = true;
128
+ options.onCancel();
129
+ };
130
+
131
+ const focus = new FocusBox(body, {
132
+ // Keys are matched by name, never by raw bytes: under the kitty keyboard
133
+ // protocol Esc arrives as CSI 27 u, and a byte comparison against "\x1b"
134
+ // left the overlay unanswerable. Everything unmatched is swallowed.
135
+ onInput: (data: string): void => {
136
+ if (settled || isKeyRelease(data)) return;
137
+ if (matchesKey(data, "up")) {
138
+ body.scroll = Math.max(0, body.scroll - 1);
139
+ tui.requestRender();
140
+ return;
141
+ }
142
+ if (matchesKey(data, "down")) {
143
+ body.scroll = Math.min(body.lineCount(), body.scroll + 1);
144
+ tui.requestRender();
145
+ return;
146
+ }
147
+ if (matchesKey(data, "enter")) {
148
+ accept();
149
+ return;
150
+ }
151
+ if (matchesKey(data, "escape")) {
152
+ cancel();
153
+ return;
154
+ }
155
+ if (matchesKey(data, "e") || matchesKey(data, "shift+e")) {
156
+ const edited = options.onEdit(body.document());
157
+ if (edited !== null) body.setDocument(edited);
158
+ tui.requestRender(true);
159
+ }
160
+ },
161
+ });
162
+
163
+ const handle = showClioOverlayFrame(tui, focus, {
164
+ anchor: "center",
165
+ width: handoffReviewOverlayWidth(options.columns),
166
+ title: HANDOFF_REVIEW_OVERLAY_TITLE,
167
+ footerHint: buildResponsiveHint(
168
+ [
169
+ { key: "Enter", verb: "accept" },
170
+ { key: "e", verb: "edit" },
171
+ { key: "↑↓", verb: "scroll" },
172
+ ],
173
+ { key: "Esc", verb: "cancel" },
174
+ ),
175
+ });
176
+
177
+ return {
178
+ ...handle,
179
+ document: () => body.document(),
180
+ hide(): void {
181
+ cancel();
182
+ handle.hide();
183
+ },
184
+ };
185
+ }