@iowarp/clio-coder 0.3.6 → 0.3.8

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 (323) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +22 -6
  3. package/dist/{acp-2BEHC4DL.js → acp-U67UHUK2.js} +14 -14
  4. package/dist/{agents-LNNFTM53.js → agents-YU6SGALZ.js} +42 -35
  5. package/dist/assets/codewiki.json +1 -1
  6. package/dist/{auth-KXXFI2VS.js → auth-5ZPJOIVG.js} +32 -24
  7. package/dist/builtins-C6JMZVV6.js +17 -0
  8. package/dist/{chunk-E25LMLRW.js → chunk-26LEYJZH.js} +2 -2
  9. package/dist/{chunk-TSHXZTOQ.js → chunk-2HEJ2F35.js} +5 -5
  10. package/dist/{chunk-PBTHKCPN.js → chunk-2HFZQUHL.js} +7 -7
  11. package/dist/{chunk-FO5ZOVUY.js → chunk-3BINW3FP.js} +5 -5
  12. package/dist/{chunk-4OC57DA6.js → chunk-4DGYLA73.js} +53 -2
  13. package/dist/{chunk-CKXWIANG.js → chunk-4SPRNWDE.js} +18 -16
  14. package/dist/{chunk-E2ER4LJF.js → chunk-5C3AQNDW.js} +25 -1
  15. package/dist/{chunk-XF5N4U5A.js → chunk-5DHKRSMQ.js} +9 -8
  16. package/dist/{chunk-43AOLP7E.js → chunk-5FR74PWO.js} +2 -1
  17. package/dist/{chunk-EKY57CSP.js → chunk-5H3GB5BO.js} +68 -772
  18. package/dist/{chunk-6XXKFVSN.js → chunk-5Q2VVUKB.js} +4 -4
  19. package/dist/{chunk-DJVECN66.js → chunk-7RFXX52T.js} +295 -3705
  20. package/dist/{chunk-ZXF4XRKW.js → chunk-7RGZWPB6.js} +158 -8
  21. package/dist/{chunk-FYYLNIL5.js → chunk-A2NJGIB3.js} +2 -2
  22. package/dist/{chunk-XXQNGV4M.js → chunk-A3WNZD3P.js} +548 -72
  23. package/dist/{chunk-VEZEGCGW.js → chunk-B5CSFE7B.js} +23 -21
  24. package/dist/{chunk-2SFS6XQE.js → chunk-DGSYXYMX.js} +3 -2
  25. package/dist/chunk-DR52UMZW.js +21 -0
  26. package/dist/{chunk-ZWLZP4ZT.js → chunk-DYIM5TJT.js} +98 -12
  27. package/dist/{chunk-QKMUKYO7.js → chunk-E77JEWSD.js} +270 -125
  28. package/dist/{chunk-LYF7OHWH.js → chunk-EMYUUSFG.js} +12 -467
  29. package/dist/{chunk-4VP4KH3K.js → chunk-EQ63NRB7.js} +8 -8
  30. package/dist/chunk-FBVTI2TJ.js +518 -0
  31. package/dist/{chunk-R46L2BIR.js → chunk-FHJEP5SW.js} +19 -25
  32. package/dist/{chunk-WR67VIZY.js → chunk-GN57SG4G.js} +66 -8
  33. package/dist/{chunk-RD5U66HV.js → chunk-GPIEI3LY.js} +9 -9
  34. package/dist/{chunk-PCZJO5TI.js → chunk-GU2UIAFZ.js} +13 -178
  35. package/dist/chunk-GWS3VEIW.js +195 -0
  36. package/dist/chunk-H7IXIC72.js +103 -0
  37. package/dist/{chunk-3BPUFZDL.js → chunk-HLE42MG7.js} +3 -3
  38. package/dist/{chunk-24I7BN55.js → chunk-IFBNV6H6.js} +3 -3
  39. package/dist/{chunk-QNQHSOLF.js → chunk-IGWKHNIQ.js} +114 -55
  40. package/dist/chunk-IIZWH4XA.js +172 -0
  41. package/dist/{chunk-AD2SYQYC.js → chunk-IJ7RPIYJ.js} +124 -6
  42. package/dist/chunk-J3YUBZWY.js +382 -0
  43. package/dist/{chunk-5JGRAMKL.js → chunk-JOZYP4GM.js} +8 -6
  44. package/dist/{chunk-OH3TOQTB.js → chunk-K4XHGFR5.js} +751 -15
  45. package/dist/{chunk-EYPA3EGJ.js → chunk-KTYTFRMB.js} +178 -14
  46. package/dist/chunk-LU7P4LHA.js +33 -0
  47. package/dist/chunk-ME6CCNFO.js +108 -0
  48. package/dist/chunk-MXKJU4JB.js +1100 -0
  49. package/dist/{chunk-RY3LY4J5.js → chunk-N22QMJKY.js} +21 -18
  50. package/dist/chunk-NMPKI6XL.js +3006 -0
  51. package/dist/chunk-NUGM5KR6.js +165 -0
  52. package/dist/{chunk-22NAGB7X.js → chunk-P43ETTHK.js} +5 -94
  53. package/dist/{chunk-NILBFAPG.js → chunk-PMDBGQSJ.js} +2 -2
  54. package/dist/chunk-PT7HYKEM.js +165 -0
  55. package/dist/chunk-RVG5JXAL.js +41 -0
  56. package/dist/{chunk-GEYXPTRF.js → chunk-RWSI4YD7.js} +2 -2
  57. package/dist/{chunk-4BPJXDWC.js → chunk-TANS5ZJS.js} +35 -19
  58. package/dist/{verifiers-NCBTHHN2.js → chunk-TB5666IT.js} +67 -324
  59. package/dist/{chunk-QM3F2GKX.js → chunk-TLQJPP24.js} +7927 -8088
  60. package/dist/{chunk-G7MUEIGA.js → chunk-TT36MB5S.js} +2 -1
  61. package/dist/chunk-TTHACPOM.js +961 -0
  62. package/dist/{chunk-6US73PDB.js → chunk-TYPGUK6W.js} +7 -7
  63. package/dist/{chunk-MFFY33HR.js → chunk-U6MBIEMB.js} +554 -209
  64. package/dist/chunk-VAWNZU7Z.js +242 -0
  65. package/dist/{chunk-IR4CFBFN.js → chunk-VCBR6CU7.js} +12 -12
  66. package/dist/{chunk-WHJYKASB.js → chunk-VHN4MY6O.js} +2 -2
  67. package/dist/{chunk-XYDYPRZI.js → chunk-VWZOAB7K.js} +10 -10
  68. package/dist/{chunk-ZRGEBJ4T.js → chunk-WLFILSD5.js} +48 -48
  69. package/dist/{chunk-K7T3E2SR.js → chunk-WNIJTQQK.js} +12 -11
  70. package/dist/{chunk-CYQKWTG3.js → chunk-WSB3FPX7.js} +68 -20
  71. package/dist/{chunk-KHSFENX2.js → chunk-WWCZ5F23.js} +116 -14
  72. package/dist/{chunk-XE2VEJHX.js → chunk-WXY7KU3G.js} +2 -2
  73. package/dist/{chunk-PPAMZ32Z.js → chunk-XK56QHLX.js} +6 -1
  74. package/dist/{chunk-KOHPCX4K.js → chunk-XWSF374K.js} +5 -5
  75. package/dist/{chunk-CJUB2JJ2.js → chunk-YS5VLNH5.js} +10 -10
  76. package/dist/{chunk-ZZMN5OM4.js → chunk-ZNLWCMVZ.js} +2 -2
  77. package/dist/{chunk-WHGPSPT5.js → chunk-ZVJ5BLO2.js} +2 -2
  78. package/dist/cli/index.js +33 -31
  79. package/dist/{clio-M2KGYUFZ.js → clio-QVTYJ57A.js} +10 -10
  80. package/dist/{code-nav-GQNL7XA6.js → code-nav-FGGFIE7L.js} +5 -5
  81. package/dist/codewiki/build-worker.js +4 -4
  82. package/dist/{components-5TTYYX6G.js → components-ZFA3SAER.js} +9 -9
  83. package/dist/{config-XUUYQIWO.js → config-LW5IJFQN.js} +65 -55
  84. package/dist/{configure-IHJ7YOMV.js → configure-7XIZCOU4.js} +29 -24
  85. package/dist/{context-75MIWW3U.js → context-L3WL3X7K.js} +54 -43
  86. package/dist/{context-ZQ7SIFJV.js → context-N52ZA626.js} +30 -14
  87. package/dist/{context-74JLXAWD.js → context-Y6Y7QPR6.js} +12 -12
  88. package/dist/{context-clear-GYKWNUML.js → context-clear-MBQRLSDQ.js} +54 -43
  89. package/dist/{context-index-SSR5ECNE.js → context-index-HVMFQHK3.js} +5 -5
  90. package/dist/{context-working-set-UX5KEP4J.js → context-working-set-GS6DSO7F.js} +18 -18
  91. package/dist/{dispatch-runner-GIJBHNFL.js → dispatch-runner-22ZCNOM3.js} +367 -79
  92. package/dist/{docs-6FZSCG5B.js → docs-7LQ23DLM.js} +9 -9
  93. package/dist/{doctor-SVJ5BZCW.js → doctor-M7YEDGAE.js} +27 -23
  94. package/dist/{eval-CG6LLBLD.js → eval-BEC2WHDA.js} +73 -20
  95. package/dist/{evidence-ZYFIEN42.js → evidence-REJUMSKM.js} +70 -59
  96. package/dist/{evolve-QGEXEMDW.js → evolve-PY5ZBA5K.js} +50 -39
  97. package/dist/{extensions-ADGNCJJD.js → extensions-HVKU65YU.js} +7 -7
  98. package/dist/{fleet-S5R4ZOQY.js → fleet-7WZEWRFA.js} +236 -377
  99. package/dist/fleet-commands-UVHWM76J.js +70 -0
  100. package/dist/fleet-graph-6ULH7PES.js +125 -0
  101. package/dist/fleet-new-RDVJLHHH.js +48 -0
  102. package/dist/{fleet-preflight-BHSNPBMH.js → fleet-preflight-J53T6CCE.js} +5 -5
  103. package/dist/fleet-validate-72PC4SLA.js +79 -0
  104. package/dist/{init-5DRU55YR.js → init-OG3TPGQG.js} +69 -57
  105. package/dist/library-CNTMPLRF.js +217 -0
  106. package/dist/{memory-7YKKR6UC.js → memory-6IS7F275.js} +52 -41
  107. package/dist/{models-ZPOLRU2C.js → models-ENRJDA5W.js} +37 -31
  108. package/dist/{monitor-US5F5YGZ.js → monitor-XLDVO7TN.js} +79 -42
  109. package/dist/{orchestrator-E2AL4T5N.js → orchestrator-6KSPYRHA.js} +4126 -762
  110. package/dist/{paths-E7KYAQWE.js → paths-DBXMZMDU.js} +6 -6
  111. package/dist/registry-LG64LTF4.js +11 -0
  112. package/dist/{reset-KZ652EK6.js → reset-RZ4ER727.js} +12 -12
  113. package/dist/{run-SRNBKDWD.js → run-Y2CNK5RU.js} +89 -76
  114. package/dist/{share-CGZE33UP.js → share-A55GYP6Z.js} +37 -13
  115. package/dist/{skills-S2X4DLY5.js → skills-ALC5J6AT.js} +29 -13
  116. package/dist/{skills-eval-W2GGIC4R.js → skills-eval-JPBEBYQU.js} +60 -48
  117. package/dist/support-MIETYA5E.js +38 -0
  118. package/dist/{targets-54SWINWB.js → targets-VGNXIR3S.js} +46 -36
  119. package/dist/{terminal-lease-SAIF2OGY.js → terminal-lease-WOBR64YA.js} +4 -4
  120. package/dist/{uninstall-BVLWXKBT.js → uninstall-ZJF5H5ZN.js} +9 -9
  121. package/dist/{upgrade-JKAR27XC.js → upgrade-FUSUAGHR.js} +31 -28
  122. package/dist/{usage-MSAWCLX4.js → usage-N4MKVHKD.js} +140 -68
  123. package/dist/verifiers-YAWOJ3H2.js +336 -0
  124. package/dist/{verify-X5HDROLA.js → verify-LTDHYBGY.js} +10 -9
  125. package/dist/{wiki-generate-GUSOQ6ZP.js → wiki-generate-6M7GHTBJ.js} +75 -62
  126. package/dist/worker/entry.js +67 -57
  127. package/docs/README.md +3 -2
  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-versions.md +11 -6
  132. package/docs/built-in-agents.md +26 -2
  133. package/docs/capacity-and-scheduling.md +1 -1
  134. package/docs/commands-and-modes.md +83 -3
  135. package/docs/configuration-and-targets.md +86 -4
  136. package/docs/context-engine.md +1 -1
  137. package/docs/development-pipeline.md +1 -1
  138. package/docs/dispatch-architecture-rationale.md +1 -1
  139. package/docs/documentation-coverage.md +3 -3
  140. package/docs/documentation-guide.md +3 -3
  141. package/docs/eval-runner.md +1 -1
  142. package/docs/evals-internal.md +1 -1
  143. package/docs/evidence-and-memory.md +74 -10
  144. package/docs/evolution.md +1 -1
  145. package/docs/exit-codes-and-output.md +4 -1
  146. package/docs/extensions-and-sharing.md +6 -2
  147. package/docs/fleet-demo-runbook.md +2 -2
  148. package/docs/fleet-dispatch.md +228 -16
  149. package/docs/git-commit-provenance.md +2 -2
  150. package/docs/glossary.md +22 -2
  151. package/docs/installation-and-lifecycle.md +2 -2
  152. package/docs/middleware-and-components.md +2 -1
  153. package/docs/model-catalog.md +1 -1
  154. package/docs/observability.md +56 -9
  155. package/docs/proactive-memory.md +1 -1
  156. package/docs/prompt-envelope-and-tools.md +4 -2
  157. package/docs/provider-adapter-cookbook.md +1 -1
  158. package/docs/release-cut-checklist.md +83 -65
  159. package/docs/resource-library.md +59 -0
  160. package/docs/safety-model.md +2 -2
  161. package/docs/scientific-validation.md +3 -3
  162. package/docs/session-lifecycle.md +37 -1
  163. package/docs/skills-marketplace.md +16 -3
  164. package/docs/tool-usage.md +14 -7
  165. package/docs/trace-store.md +1 -1
  166. package/docs/troubleshooting.md +1 -1
  167. package/docs/tui-design.md +1 -1
  168. package/docs/worker-dispatch-mechanics.md +3 -3
  169. package/package.json +1 -2
  170. package/src/cli/argv.ts +5 -0
  171. package/src/cli/configure.ts +107 -23
  172. package/src/cli/doctor.ts +5 -1
  173. package/src/cli/evidence.ts +30 -25
  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 +111 -0
  178. package/src/cli/fleet-validate.ts +30 -0
  179. package/src/cli/fleet.ts +173 -335
  180. package/src/cli/index.ts +3 -1
  181. package/src/cli/library.ts +190 -0
  182. package/src/cli/share.ts +13 -1
  183. package/src/cli/shared.ts +1 -0
  184. package/src/cli/targets.ts +4 -1
  185. package/src/cli/usage.ts +111 -19
  186. package/src/cli/validate-model.ts +60 -5
  187. package/src/core/bus-events.ts +29 -0
  188. package/src/core/commit-attribution.ts +4 -4
  189. package/src/core/config.ts +130 -0
  190. package/src/core/defaults.ts +81 -0
  191. package/src/core/path-boundary.ts +100 -0
  192. package/src/domains/agents/builtins/architect.md +1 -0
  193. package/src/domains/agents/builtins/oracle.md +33 -0
  194. package/src/domains/agents/catalog.ts +13 -1
  195. package/src/domains/agents/extension.ts +2 -11
  196. package/src/domains/agents/fleet-contract.ts +304 -24
  197. package/src/domains/agents/index.ts +14 -0
  198. package/src/domains/agents/recipe.ts +7 -1
  199. package/src/domains/agents/registry.ts +73 -5
  200. package/src/domains/agents/result-contract.ts +360 -15
  201. package/src/domains/agents/write-boundary.ts +15 -50
  202. package/src/domains/config/classify.ts +4 -0
  203. package/src/domains/context/project-rules.ts +51 -1
  204. package/src/domains/dispatch/active-route-planner.ts +14 -0
  205. package/src/domains/dispatch/assignment-reconcile.ts +22 -5
  206. package/src/domains/dispatch/assignment-store.ts +151 -14
  207. package/src/domains/dispatch/backoff.ts +2 -1
  208. package/src/domains/dispatch/capability-match.ts +1 -0
  209. package/src/domains/dispatch/checkout-writer-lease.ts +175 -0
  210. package/src/domains/dispatch/contract.ts +48 -0
  211. package/src/domains/dispatch/delegation-plan.ts +164 -0
  212. package/src/domains/dispatch/execution-plan.ts +76 -5
  213. package/src/domains/dispatch/execution-role.ts +12 -2
  214. package/src/domains/dispatch/execution-scheduler.ts +183 -67
  215. package/src/domains/dispatch/extension.ts +412 -74
  216. package/src/domains/dispatch/fleet-gate.ts +14 -0
  217. package/src/domains/dispatch/fleet-plan.ts +63 -3
  218. package/src/domains/dispatch/fleet-run.ts +791 -0
  219. package/src/domains/dispatch/gate-role-prompts.ts +47 -0
  220. package/src/domains/dispatch/host-verification.ts +178 -0
  221. package/src/domains/dispatch/index.ts +41 -1
  222. package/src/domains/dispatch/intent-requirements.ts +40 -0
  223. package/src/domains/dispatch/intent.ts +235 -0
  224. package/src/domains/dispatch/path-scope.ts +370 -0
  225. package/src/domains/dispatch/receipt-integrity.ts +9 -4
  226. package/src/domains/dispatch/state.ts +36 -3
  227. package/src/domains/dispatch/types.ts +64 -12
  228. package/src/domains/dispatch/validation.ts +69 -6
  229. package/src/domains/dispatch/write-boundary-enforcer.ts +45 -0
  230. package/src/domains/dispatch/write-boundary.ts +201 -22
  231. package/src/domains/eval/metrics/evidence.ts +79 -2
  232. package/src/domains/eval/runners/clio-run.ts +12 -2
  233. package/src/domains/evidence/build.ts +69 -11
  234. package/src/domains/evidence/index.ts +21 -0
  235. package/src/domains/evidence/provenance.ts +46 -11
  236. package/src/domains/evidence/trust-projection.ts +274 -0
  237. package/src/domains/evidence/trust-status.ts +155 -18
  238. package/src/domains/evidence/types.ts +4 -0
  239. package/src/domains/extensions/discovery.ts +88 -1
  240. package/src/domains/extensions/resources.ts +20 -8
  241. package/src/domains/extensions/state.ts +6 -2
  242. package/src/domains/extensions/types.ts +4 -1
  243. package/src/domains/lifecycle/doctor.ts +140 -1
  244. package/src/domains/middleware/index.ts +15 -0
  245. package/src/domains/middleware/watchdog.ts +281 -0
  246. package/src/domains/observability/contract.ts +3 -1
  247. package/src/domains/observability/cost.ts +12 -1
  248. package/src/domains/observability/extension.ts +2 -2
  249. package/src/domains/observability/index.ts +10 -0
  250. package/src/domains/observability/out-of-turn-usage.ts +223 -0
  251. package/src/domains/prompts/contract.ts +3 -5
  252. package/src/domains/providers/extension.ts +30 -2
  253. package/src/domains/resources/common-loader.ts +3 -0
  254. package/src/domains/resources/index.ts +20 -0
  255. package/src/domains/resources/library.ts +326 -0
  256. package/src/domains/resources/prompts/loader.ts +119 -14
  257. package/src/domains/resources/skills/marketplace.ts +37 -12
  258. package/src/domains/safety/policy-engine.ts +5 -5
  259. package/src/domains/safety/run-effects.ts +64 -1
  260. package/src/domains/safety/scope.ts +7 -12
  261. package/src/domains/session/handoff.ts +629 -0
  262. package/src/domains/share/archive.ts +67 -2
  263. package/src/engine/acp/server.ts +4 -1
  264. package/src/engine/prompt-templates.ts +18 -1
  265. package/src/engine/worker-runtime.ts +6 -3
  266. package/src/entry/orchestrator.ts +37 -0
  267. package/src/interactive/bus-notices.ts +26 -0
  268. package/src/interactive/chat-loop.ts +235 -1
  269. package/src/interactive/chat-renderer.ts +22 -0
  270. package/src/interactive/cost-overlay.ts +31 -3
  271. package/src/interactive/council-dispatch.ts +30 -0
  272. package/src/interactive/council-grid.ts +213 -0
  273. package/src/interactive/council.ts +99 -0
  274. package/src/interactive/dispatch-board.ts +311 -17
  275. package/src/interactive/fleet-run-preview.ts +307 -0
  276. package/src/interactive/footer/notifications.ts +219 -0
  277. package/src/interactive/handoff-round.ts +56 -0
  278. package/src/interactive/interactive-application.ts +43 -1
  279. package/src/interactive/interactive-event-projection.ts +23 -1
  280. package/src/interactive/interactive-slash-runtime.ts +52 -2
  281. package/src/interactive/interactive-subscriptions.ts +14 -2
  282. package/src/interactive/oracle.ts +179 -0
  283. package/src/interactive/overlay-ask-user-lifecycle.ts +6 -0
  284. package/src/interactive/overlay-general-openers.ts +190 -1
  285. package/src/interactive/overlay-key-routing.ts +17 -1
  286. package/src/interactive/overlay-lifecycle.ts +41 -1
  287. package/src/interactive/overlay-permission-lifecycle.ts +10 -0
  288. package/src/interactive/overlay-resource-openers.ts +11 -3
  289. package/src/interactive/overlay-session-lifecycle.ts +234 -2
  290. package/src/interactive/overlays/fleet-run-approval.ts +208 -0
  291. package/src/interactive/overlays/handoff-review.ts +185 -0
  292. package/src/interactive/overlays/library-install-confirm.ts +151 -0
  293. package/src/interactive/overlays/list-overlay.ts +168 -2
  294. package/src/interactive/overlays/settings.ts +221 -30
  295. package/src/interactive/overlays/side-question.ts +139 -0
  296. package/src/interactive/overlays/skills-hub.ts +401 -15
  297. package/src/interactive/side-question.ts +171 -0
  298. package/src/interactive/slash-commands.ts +439 -7
  299. package/src/interactive/slash-spec.ts +19 -6
  300. package/src/interactive/theme/tokens.ts +30 -0
  301. package/src/interactive/turn-middleware.ts +15 -1
  302. package/src/interactive/view/artifacts.ts +42 -9
  303. package/src/interactive/view/view-overlay.ts +15 -3
  304. package/src/interactive/watchdog-run.ts +75 -0
  305. package/src/interactive/worker-receipts.ts +14 -2
  306. package/src/interactive/worker-share.ts +56 -1
  307. package/src/interactive/worker-stream.ts +15 -0
  308. package/src/tools/bootstrap.ts +3 -0
  309. package/src/tools/compete-worktrees.ts +13 -79
  310. package/src/tools/dispatch-admission.ts +251 -18
  311. package/src/tools/dispatch-arguments.ts +84 -1
  312. package/src/tools/dispatch-plan.ts +165 -6
  313. package/src/tools/dispatch-runner.ts +364 -23
  314. package/src/tools/dispatch-types.ts +20 -1
  315. package/src/tools/dispatch.ts +72 -2
  316. package/src/tools/monitor.ts +29 -0
  317. package/src/tools/profiles.ts +18 -4
  318. package/src/tools/task-worktree.ts +238 -0
  319. package/src/tools/verify/authoring.ts +61 -1
  320. package/src/tools/verify/scripts.ts +62 -0
  321. package/src/tools/worker-evidence.ts +21 -14
  322. package/src/worker/spec-contract.ts +3 -1
  323. package/dist/chunk-HC4CLZ2Y.js +0 -68
@@ -1,28 +1,74 @@
1
1
  import type {
2
+ LibraryDiscoveryResult,
3
+ LibraryEntry,
4
+ LibraryEntryKind,
5
+ LibraryInstallPlan,
6
+ LibraryRequirementStatus,
2
7
  MarketplaceDiscoveryResult,
3
8
  MarketplaceSkill,
4
9
  ResourceList,
5
10
  Skill,
6
11
  } from "../../domains/resources/index.js";
7
- import { discoverMarketplaceSkills, MARKETPLACE_UNCONFIGURED } from "../../domains/resources/index.js";
12
+ import {
13
+ classifyLibraryRequirements,
14
+ discoverLibrary,
15
+ discoverMarketplaceSkills,
16
+ installLibraryPlan,
17
+ libraryEntryInstalled,
18
+ libraryEntryPin,
19
+ libraryEntryRef,
20
+ libraryInstallPath,
21
+ MARKETPLACE_UNCONFIGURED,
22
+ planLibraryInstall,
23
+ } from "../../domains/resources/index.js";
8
24
  import type { OverlayHandle, TUI } from "../../engine/tui.js";
9
25
  import type { NoticeLevel } from "../command-output.js";
10
26
  import { clioTheme, GLYPH } from "../theme/index.js";
11
- import { type ListOverlayItem, openListOverlay } from "./list-overlay.js";
27
+ import type { LibraryInstallConfirmSubject } from "./library-install-confirm.js";
28
+ import { openLibraryInstallConfirmOverlay } from "./library-install-confirm.js";
29
+ import { type ListOverlayItem, type ListOverlayTab, openListOverlay } from "./list-overlay.js";
12
30
 
13
31
  /**
14
- * The Skills Hub: one multipane surface for every skill Clio can reach.
15
- * Installed skills group by scope, marketplace rows come from the same local
16
- * marketplace lookup the installer and `/skill <name>` resolve through, Enter
17
- * inserts the invocation into the editor, and `i` installs in place. The hub
18
- * lists nothing the resolver cannot resolve; when the lookup is empty the hub
19
- * says so and names the remedy instead of drawing an inventory.
32
+ * The Skills Hub: one multipane surface for every resource Clio can reach.
33
+ *
34
+ * The Skills tab is the original view. Installed skills group by scope,
35
+ * marketplace rows come from the same local marketplace lookup the installer
36
+ * and `/skill <name>` resolve through, Enter inserts the invocation into the
37
+ * editor, and `i` installs in place. The hub lists nothing the resolver cannot
38
+ * resolve; when the lookup is empty the hub says so and names the remedy
39
+ * instead of drawing an inventory.
40
+ *
41
+ * The Agents, Prompts, and Fleets tabs are the interactive half of the resource
42
+ * library. Their rows come from the same `discoverLibrary()` the CLI's
43
+ * `library list --kind` reads, their install runs the same plan-then-write pair
44
+ * `library add` runs behind the same confirmation the `--yes` gate expresses,
45
+ * and their requirement gate is the same classifier. Nothing here reaches
46
+ * around the library domain into a private path.
20
47
  */
21
48
 
22
49
  const GROUP_PROJECT = "Project";
23
50
  const GROUP_USER = "User";
24
51
  const GROUP_MARKETPLACE = "Marketplace";
25
52
  const GROUP_DIAGNOSTICS = "Diagnostics";
53
+ const GROUP_INSTALLED = "Installed";
54
+ const GROUP_AVAILABLE = "Available";
55
+
56
+ /** The hub's tabs, in the order ←/→ walks them. `skill` is the original view. */
57
+ export const LIBRARY_TABS: ReadonlyArray<{ id: LibraryEntryKind; label: string }> = [
58
+ { id: "skill", label: "Skills" },
59
+ { id: "agent", label: "Agents" },
60
+ { id: "prompt", label: "Prompts" },
61
+ { id: "fleet", label: "Fleets" },
62
+ ];
63
+
64
+ export const SKILLS_HUB_TITLE = "Skills Hub";
65
+
66
+ /** Row id prefix for a library row, which is how an action tells the tabs apart. */
67
+ export const LIBRARY_ROW_PREFIX = "library:";
68
+
69
+ export function isLibraryTab(value: string): value is LibraryEntryKind {
70
+ return LIBRARY_TABS.some((tab) => tab.id === value);
71
+ }
26
72
 
27
73
  /** @internal exported for contract tests */
28
74
  export const SKILLS_HUB_EMPTY =
@@ -37,6 +83,50 @@ export interface SkillsHubDeps {
37
83
  onClose: () => void;
38
84
  /** Injectable for tests; defaults to the local marketplace the installer uses. */
39
85
  discoverMarketplace?: () => MarketplaceDiscoveryResult;
86
+ /** Tab the hub opens on. `/skills` opens on Skills, `/library <kind>` on that kind. */
87
+ initialTab?: LibraryEntryKind;
88
+ /** Injectable for tests; defaults to the discovery `library list` reads. */
89
+ discoverLibrary?: () => LibraryDiscoveryResult;
90
+ /** Injectable for tests; defaults to the classifier `library add` gates on. */
91
+ classifyRequirements?: (entry: LibraryEntry, catalog: ReadonlyArray<LibraryEntry>) => LibraryRequirementStatus;
92
+ /** Injectable for tests; defaults to the planner `library add` prints from. */
93
+ planInstall?: (entry: LibraryEntry) => LibraryInstallPlan;
94
+ /** Injectable for tests; defaults to the writer `library add --yes` runs. */
95
+ installPlan?: (plan: LibraryInstallPlan) => void;
96
+ /** Injectable for tests; defaults to the pin-and-destination check the domain owns. */
97
+ entryInstalled?: (entry: LibraryEntry) => boolean;
98
+ /** Injectable for tests; defaults to the recorded pin for this entry. */
99
+ entryPin?: (entry: LibraryEntry) => { sha256: string; sourceUrl: string } | undefined;
100
+ /**
101
+ * The confirmation gate. Resolves true only when the operator accepted, and
102
+ * nothing is written before it does. Defaults to the framed confirmation
103
+ * overlay, which is the TUI spelling of `library add`'s `--yes`.
104
+ */
105
+ confirmInstall?: (subject: LibraryInstallConfirmSubject) => Promise<boolean>;
106
+ /** Opens the `/fleet run` approval preview for an installed fleet. */
107
+ openFleetRun?: (name: string) => void;
108
+ }
109
+
110
+ /** Row and action context for one library tab, resolved once per rebuild. */
111
+ interface LibraryTabContext {
112
+ discovery: LibraryDiscoveryResult;
113
+ installed: (entry: LibraryEntry) => boolean;
114
+ pin: (entry: LibraryEntry) => { sha256: string; sourceUrl: string } | undefined;
115
+ unresolved: (entry: LibraryEntry) => LibraryEntry[];
116
+ }
117
+
118
+ /**
119
+ * The composer text or the surface a `use` on this entry leads to.
120
+ *
121
+ * A fleet has no composer spelling because its `use` opens the `/fleet run`
122
+ * approval preview instead, so this returns null for one and the caller routes
123
+ * it there.
124
+ */
125
+ export function libraryUseInvocation(entry: Pick<LibraryEntry, "kind" | "name">): string | null {
126
+ if (entry.kind === "fleet") return null;
127
+ if (entry.kind === "agent") return `/run ${entry.name} `;
128
+ if (entry.kind === "prompt") return `/${entry.name} `;
129
+ return `/skill ${entry.name} `;
40
130
  }
41
131
 
42
132
  function groupForScope(scope: string): string {
@@ -170,8 +260,159 @@ export function buildDiagnosticItems(
170
260
  return items;
171
261
  }
172
262
 
263
+ /** @internal exported for contract tests */
264
+ export function libraryRowId(entry: Pick<LibraryEntry, "kind" | "name">): string {
265
+ return `${LIBRARY_ROW_PREFIX}${libraryEntryRef(entry)}`;
266
+ }
267
+
268
+ /**
269
+ * One kind's rows: the same columns the Skills tab shows where they apply, plus
270
+ * the requirements this entry still needs.
271
+ *
272
+ * The requires column carries the names rather than a count, because the names
273
+ * are what the refusal will say and what the second confirmation will install.
274
+ * It renders in the warning token: an entry with an unmet requirement is
275
+ * listable and readable but not installable on its own.
276
+ *
277
+ * @internal exported for contract tests
278
+ */
279
+ export function buildLibraryItems(kind: LibraryEntryKind, context: LibraryTabContext): ListOverlayItem[] {
280
+ const theme = clioTheme();
281
+ return context.discovery.entries
282
+ .filter((entry) => entry.kind === kind)
283
+ .map((entry) => {
284
+ const installed = context.installed(entry);
285
+ const pin = context.pin(entry);
286
+ const unresolved = context.unresolved(entry).map(libraryEntryRef);
287
+ const metaParts: string[] = [entry.origin];
288
+ if (entry.version) metaParts.push(`v${entry.version}`);
289
+ metaParts.push(installed ? "installed" : "available");
290
+ metaParts.push(pin ? `pin ${pin.sha256.slice(0, 8)}` : "unpinned");
291
+ if (unresolved.length > 0) metaParts.push(theme.fg("warning", `requires ${unresolved.join(", ")}`));
292
+ return {
293
+ id: libraryRowId(entry),
294
+ label: entry.name,
295
+ meta: metaParts.join(" · "),
296
+ group: installed ? GROUP_INSTALLED : GROUP_AVAILABLE,
297
+ detail: () => {
298
+ const use = libraryUseInvocation(entry);
299
+ const lines = [
300
+ `# ${entry.name}`,
301
+ `**Kind:** ${entry.kind}`,
302
+ `**Use:** ${use === null ? "`Enter` opens the /fleet run approval preview" : `\`${use.trim()}\``}`,
303
+ `**State:** ${installed ? "installed" : "available"}${pin ? ` · pinned ${pin.sha256.slice(0, 12)}` : " · unpinned"}`,
304
+ `**Destination:** \`${libraryInstallPath(entry)}\``,
305
+ `**Source:** \`${entry.sourceUrl}\``,
306
+ `**Origin:** ${entry.origin}`,
307
+ ];
308
+ if ((entry.requires ?? []).length > 0) {
309
+ lines.push(`**Requires:** ${(entry.requires ?? []).join(", ")}`);
310
+ }
311
+ if (unresolved.length > 0) {
312
+ lines.push(`**Unresolved:** ${unresolved.join(", ")}`, "", "Press `i` twice to install them with it.");
313
+ } else if (!installed) {
314
+ lines.push("", "**Install now:** press `i`");
315
+ }
316
+ lines.push("", "---", "", entry.description);
317
+ return lines;
318
+ },
319
+ };
320
+ });
321
+ }
322
+
323
+ /**
324
+ * Diagnostic rows for a library tab. `discoverLibrary` drops an entry whose
325
+ * requirements are missing, malformed, or cyclic and records why; a tab that
326
+ * silently omitted those would be the inventory this hub refuses to draw.
327
+ *
328
+ * @internal exported for contract tests
329
+ */
330
+ export function buildLibraryDiagnosticItems(discovery: LibraryDiscoveryResult): ListOverlayItem[] {
331
+ const theme = clioTheme();
332
+ return discovery.diagnostics
333
+ .filter((message) => message !== MARKETPLACE_UNCONFIGURED)
334
+ .map((message, index) => ({
335
+ id: `library-diagnostic-${index}`,
336
+ label: `${theme.fg("warning", GLYPH.warnInline)} ${message}`,
337
+ meta: "library",
338
+ group: GROUP_DIAGNOSTICS,
339
+ detail: () => ["# Library diagnostic", `**Message:** ${message}`],
340
+ }));
341
+ }
342
+
343
+ /** What one attempted library install did, in the terms the hub reports it. */
344
+ export type LibraryInstallOutcome =
345
+ | { status: "installed"; refs: string[] }
346
+ | { status: "cancelled" }
347
+ | { status: "refused"; unresolved: string[] }
348
+ | { status: "already-installed" }
349
+ | { status: "failed"; message: string };
350
+
351
+ /** The library domain calls one install needs, resolved before it runs. */
352
+ export interface LibraryInstallRunner {
353
+ discovery: LibraryDiscoveryResult;
354
+ classify: (entry: LibraryEntry, catalog: ReadonlyArray<LibraryEntry>) => LibraryRequirementStatus;
355
+ plan: (entry: LibraryEntry) => LibraryInstallPlan;
356
+ write: (plan: LibraryInstallPlan) => void;
357
+ installed: (entry: LibraryEntry) => boolean;
358
+ confirm: (subject: LibraryInstallConfirmSubject) => Promise<boolean>;
359
+ }
360
+
361
+ /**
362
+ * Install one library entry through the same sequence `library add` runs.
363
+ *
364
+ * The order is the CLI's order and the refusals are the CLI's refusals: an
365
+ * entry with an unmet requirement is refused by name and nothing is planned, a
366
+ * plan is built and shown before anything is written, and the writes happen
367
+ * only after the confirmation resolves true. A cancelled confirmation is a run
368
+ * in which no file changed.
369
+ *
370
+ * @internal exported for contract tests
371
+ */
372
+ export async function runLibraryInstall(
373
+ entry: LibraryEntry,
374
+ runner: LibraryInstallRunner,
375
+ options: { withRequirements: boolean },
376
+ ): Promise<LibraryInstallOutcome> {
377
+ if (runner.installed(entry)) return { status: "already-installed" };
378
+ let requirements: LibraryRequirementStatus;
379
+ let plans: LibraryInstallPlan[];
380
+ try {
381
+ requirements = runner.classify(entry, runner.discovery.entries);
382
+ if (requirements.unsatisfied.length > 0 && !options.withRequirements) {
383
+ return { status: "refused", unresolved: requirements.unsatisfied.map(libraryEntryRef) };
384
+ }
385
+ plans = [...(options.withRequirements ? requirements.unsatisfied : []), entry].map(runner.plan);
386
+ } catch (error) {
387
+ return { status: "failed", message: error instanceof Error ? error.message : String(error) };
388
+ }
389
+ const accepted = await runner.confirm({
390
+ entryRef: libraryEntryRef(entry),
391
+ writes: plans.map((plan) => ({ ref: libraryEntryRef(plan.entry), path: plan.path, sha256: plan.sha256 })),
392
+ requirements: options.withRequirements ? requirements.unsatisfied.map(libraryEntryRef) : [],
393
+ satisfied: requirements.satisfied.map(libraryEntryRef),
394
+ });
395
+ if (!accepted) return { status: "cancelled" };
396
+ const written: string[] = [];
397
+ try {
398
+ for (const plan of plans) {
399
+ runner.write(plan);
400
+ written.push(libraryEntryRef(plan.entry));
401
+ }
402
+ } catch (error) {
403
+ return { status: "failed", message: error instanceof Error ? error.message : String(error) };
404
+ }
405
+ return { status: "installed", refs: written };
406
+ }
407
+
173
408
  export function openSkillsHub(tui: TUI, deps: SkillsHubDeps): OverlayHandle {
174
409
  const discoverMarketplace = deps.discoverMarketplace ?? (() => discoverMarketplaceSkills());
410
+ const readLibrary = deps.discoverLibrary ?? (() => discoverLibrary());
411
+ const classify = deps.classifyRequirements ?? classifyLibraryRequirements;
412
+ const plan = deps.planInstall ?? planLibraryInstall;
413
+ const write = deps.installPlan ?? installLibraryPlan;
414
+ const installed = deps.entryInstalled ?? libraryEntryInstalled;
415
+ const pin = deps.entryPin ?? libraryEntryPin;
175
416
 
176
417
  // Rebuilt rather than mutated in place: the view memoizes its frame on the row
177
418
  // set, so a spliced array leaves an installed skill drawn as installable.
@@ -186,27 +427,170 @@ export function openSkillsHub(tui: TUI, deps: SkillsHubDeps): OverlayHandle {
186
427
  ];
187
428
  };
188
429
 
430
+ // One discovery pass feeds all three library tabs. The tab bar asks every tab
431
+ // for its rows on each rebuild, and four independent catalog reads per
432
+ // keystroke would be four answers to the same question.
433
+ let libraryCache: LibraryTabContext | null = null;
434
+ const libraryContext = (): LibraryTabContext => {
435
+ if (libraryCache) return libraryCache;
436
+ const discovery = readLibrary();
437
+ libraryCache = {
438
+ discovery,
439
+ installed,
440
+ pin,
441
+ unresolved: (entry) => {
442
+ try {
443
+ return classify(entry, discovery.entries).unsatisfied;
444
+ } catch {
445
+ // A requirement that will not resolve is already a diagnostic row on
446
+ // this tab; a row that cannot answer the question reports none rather
447
+ // than taking the whole rebuild down.
448
+ return [];
449
+ }
450
+ },
451
+ };
452
+ return libraryCache;
453
+ };
454
+ const invalidateLibrary = (): void => {
455
+ libraryCache = null;
456
+ };
457
+
458
+ const buildLibraryRows = (kind: LibraryEntryKind): ListOverlayItem[] => {
459
+ const context = libraryContext();
460
+ return [...buildLibraryItems(kind, context), ...buildLibraryDiagnosticItems(context.discovery)];
461
+ };
462
+
463
+ const entryForRow = (item: ListOverlayItem): LibraryEntry | undefined => {
464
+ if (!item.id.startsWith(LIBRARY_ROW_PREFIX)) return undefined;
465
+ const ref = item.id.slice(LIBRARY_ROW_PREFIX.length);
466
+ return libraryContext().discovery.entries.find((candidate) => libraryEntryRef(candidate) === ref);
467
+ };
468
+
469
+ const tabs: ListOverlayTab[] = LIBRARY_TABS.map((tab) => ({
470
+ id: tab.id,
471
+ label: tab.label,
472
+ items: tab.id === "skill" ? buildRows : () => buildLibraryRows(tab.id),
473
+ }));
474
+
189
475
  let closed = false;
190
476
  let installInFlight = false;
191
- const handle = openListOverlay(tui, {
192
- title: "Skills",
193
- items: buildRows(),
477
+ /**
478
+ * The row whose requirements were just refused. A second `i` on that row is
479
+ * the install-with-requirements offer, so the refusal names what is missing
480
+ * before any confirmation lists what would be written.
481
+ */
482
+ let pendingWithRequirements: string | null = null;
483
+
484
+ const columns = (): number => (typeof process.stdout.columns === "number" ? process.stdout.columns : 100);
485
+
486
+ // eslint-disable-next-line prefer-const -- the confirmation closure needs the handle it is opened from.
487
+ let handle: ReturnType<typeof openListOverlay>;
488
+
489
+ const confirmInstall =
490
+ deps.confirmInstall ??
491
+ ((subject: LibraryInstallConfirmSubject): Promise<boolean> =>
492
+ new Promise<boolean>((resolve) => {
493
+ handle.setHidden(true);
494
+ const settle = (accepted: boolean): void => {
495
+ if (!closed) {
496
+ handle.setHidden(false);
497
+ handle.focus();
498
+ }
499
+ resolve(accepted);
500
+ };
501
+ const confirmation = openLibraryInstallConfirmOverlay(tui, {
502
+ subject,
503
+ columns: columns(),
504
+ onAccept: () => {
505
+ settle(true);
506
+ confirmation.hide();
507
+ },
508
+ onCancel: () => {
509
+ settle(false);
510
+ confirmation.hide();
511
+ },
512
+ });
513
+ }));
514
+
515
+ const useEntry = (entry: LibraryEntry): void => {
516
+ if (!installed(entry)) {
517
+ deps.notice("warn", `${libraryEntryRef(entry)} is not installed; press i to install it`);
518
+ return;
519
+ }
520
+ if (entry.kind === "fleet") {
521
+ deps.openFleetRun?.(entry.name);
522
+ return;
523
+ }
524
+ const invocation = libraryUseInvocation(entry);
525
+ if (invocation === null) return;
526
+ deps.setEditorText(invocation);
527
+ deps.onClose();
528
+ };
529
+
530
+ const installEntry = (entry: LibraryEntry): void => {
531
+ const ref = libraryEntryRef(entry);
532
+ const withRequirements = pendingWithRequirements === ref;
533
+ installInFlight = true;
534
+ void (async () => {
535
+ const outcome = await runLibraryInstall(
536
+ entry,
537
+ { discovery: libraryContext().discovery, classify, plan, write, installed, confirm: confirmInstall },
538
+ { withRequirements },
539
+ );
540
+ installInFlight = false;
541
+ if (outcome.status === "refused") {
542
+ pendingWithRequirements = ref;
543
+ deps.notice("error", `${ref} needs ${outcome.unresolved.join(", ")}; press i again to install them with it`);
544
+ return;
545
+ }
546
+ pendingWithRequirements = null;
547
+ if (outcome.status === "already-installed") deps.notice("info", `${ref} is already installed`);
548
+ else if (outcome.status === "cancelled") deps.notice("info", `${ref}: cancelled; nothing was written`);
549
+ else if (outcome.status === "failed") deps.notice("error", `library install failed: ${outcome.message}`);
550
+ else deps.notice("success", `installed ${outcome.refs.join(", ")}`);
551
+ // An install that lands after the hub closed has nothing to redraw.
552
+ if (!closed && outcome.status === "installed") {
553
+ invalidateLibrary();
554
+ handle.refreshTabs();
555
+ }
556
+ })();
557
+ };
558
+
559
+ handle = openListOverlay(tui, {
560
+ title: SKILLS_HUB_TITLE,
561
+ items: [],
562
+ tabs,
563
+ ...(deps.initialTab ? { activeTabId: deps.initialTab } : {}),
564
+ onTabChange: () => {
565
+ pendingWithRequirements = null;
566
+ },
194
567
  filterable: true,
195
568
  layout: "split",
196
569
  emptyMessage: SKILLS_HUB_EMPTY,
197
570
  hints: [
198
- { key: "Enter", verb: "invoke" },
571
+ { key: "Enter", verb: "use" },
199
572
  { key: "i", verb: "install" },
200
573
  ],
201
574
  onSelect: (item) => {
202
575
  if (item.group === GROUP_DIAGNOSTICS) return;
576
+ const entry = entryForRow(item);
577
+ if (entry) {
578
+ useEntry(entry);
579
+ return;
580
+ }
203
581
  const name = item.id.startsWith("marketplace:") ? item.id.slice("marketplace:".length) : item.id;
204
582
  deps.setEditorText(`/skill ${name} `);
205
583
  deps.onClose();
206
584
  },
207
585
  actions: {
208
586
  i: (item) => {
209
- if (!item.id.startsWith("marketplace:") || installInFlight) return;
587
+ if (installInFlight) return;
588
+ const entry = entryForRow(item);
589
+ if (entry) {
590
+ installEntry(entry);
591
+ return;
592
+ }
593
+ if (!item.id.startsWith("marketplace:")) return;
210
594
  const name = item.id.slice("marketplace:".length);
211
595
  installInFlight = true;
212
596
  void (async () => {
@@ -218,8 +602,10 @@ export function openSkillsHub(tui: TUI, deps: SkillsHubDeps): OverlayHandle {
218
602
  deps.notice("error", `skill install failed: ${err instanceof Error ? err.message : String(err)}`);
219
603
  } finally {
220
604
  installInFlight = false;
221
- // An install that lands after the hub closed has nothing to redraw.
222
- if (!closed) handle.setItems(buildRows());
605
+ if (!closed) {
606
+ invalidateLibrary();
607
+ handle.refreshTabs();
608
+ }
223
609
  }
224
610
  })();
225
611
  },
@@ -0,0 +1,171 @@
1
+ /**
2
+ * `/btw`: one model round beside the session, never inside it.
3
+ *
4
+ * A fleet run briefs its workers from the transcript, so every question the
5
+ * operator asks mid-run to orient themselves ("which file did that land in
6
+ * again?") becomes context the workers inherit. This round reads the compiled
7
+ * history the next turn would send and answers into an overlay; nothing it
8
+ * produces reaches the session JSONL, the transcript panel, the context ledger,
9
+ * or the task board, and it never becomes a turn.
10
+ *
11
+ * The call goes through the same `src/engine/ai.ts` stream seam compaction uses
12
+ * for its summarization call, which is what keeps this outside the turn state
13
+ * machine without adding an engine seam of its own. Tools are never sent.
14
+ */
15
+
16
+ import { stream } from "../engine/ai.js";
17
+ import type { AgentMessage, EngineModel, Usage } from "../engine/types.js";
18
+
19
+ /**
20
+ * The whole system prompt for the round. The session's own compiled prompt is
21
+ * deliberately not reused: it carries the working agreement for a turn that
22
+ * edits files and calls tools, and this round does neither.
23
+ */
24
+ export const SIDE_QUESTION_SYSTEM_PROMPT = [
25
+ "You are answering a side question from the operator of a coding session.",
26
+ "The conversation above is read-only context. Answer the question directly and briefly.",
27
+ "Do not continue the conversation, do not propose edits, and do not call tools.",
28
+ ].join(" ");
29
+
30
+ /** Default output budget for the round. A side question wants an answer, not an essay. */
31
+ export const SIDE_QUESTION_MAX_TOKENS = 2048;
32
+
33
+ export interface SideQuestionUsage {
34
+ input: number;
35
+ output: number;
36
+ cacheRead: number;
37
+ cacheWrite: number;
38
+ reasoning: number;
39
+ totalTokens: number;
40
+ costUsd: number;
41
+ }
42
+
43
+ export interface SideQuestionInput {
44
+ /** Live model the next turn would use, taken from the session's active runtime. */
45
+ model: EngineModel;
46
+ /**
47
+ * The compiled message history the next turn would send. Treated as
48
+ * read-only: the round copies it and appends its own user message to the
49
+ * copy, so the live agent's message list is never mutated.
50
+ */
51
+ messages: ReadonlyArray<AgentMessage>;
52
+ question: string;
53
+ apiKey?: string;
54
+ signal?: AbortSignal;
55
+ maxTokens?: number;
56
+ /** Streamed answer text, delivered as the provider produces it. */
57
+ onDelta?: (partialText: string) => void;
58
+ }
59
+
60
+ /**
61
+ * One out-of-turn round against the session's live runtime. `/btw` and
62
+ * `/handoff` are both this: read the compiled history the next turn would see,
63
+ * append one instruction, send no tools, and never touch the turn state
64
+ * machine. Only the system prompt and the appended message differ.
65
+ */
66
+ export interface OutOfTurnRoundInput extends Omit<SideQuestionInput, "question"> {
67
+ systemPrompt: string;
68
+ /** The one message appended to the read-only copy of the history. */
69
+ userText: string;
70
+ }
71
+
72
+ export interface SideQuestionResult {
73
+ text: string;
74
+ usage: SideQuestionUsage | null;
75
+ /** True when the operator cancelled the round with Esc or Ctrl+C. */
76
+ aborted: boolean;
77
+ }
78
+
79
+ function positive(value: unknown): number {
80
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
81
+ }
82
+
83
+ function sideQuestionUsage(raw: unknown): SideQuestionUsage | null {
84
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
85
+ const usage = raw as Partial<Usage> & { reasoning?: number };
86
+ const input = positive(usage.input);
87
+ const output = positive(usage.output);
88
+ const cacheRead = positive(usage.cacheRead);
89
+ const cacheWrite = positive(usage.cacheWrite);
90
+ const totalTokens = positive(usage.totalTokens) || input + output + cacheRead + cacheWrite;
91
+ if (totalTokens === 0) return null;
92
+ return {
93
+ input,
94
+ output,
95
+ cacheRead,
96
+ cacheWrite,
97
+ reasoning: positive(usage.reasoning),
98
+ totalTokens,
99
+ costUsd: positive(usage.cost?.total),
100
+ };
101
+ }
102
+
103
+ function textFromMessage(message: unknown): string {
104
+ if (!message || typeof message !== "object" || !("content" in message)) return "";
105
+ const content = (message as { content: unknown }).content;
106
+ if (typeof content === "string") return content;
107
+ if (!Array.isArray(content)) return "";
108
+ return content
109
+ .filter(
110
+ (block): block is { type: "text"; text: string } =>
111
+ !!block &&
112
+ typeof block === "object" &&
113
+ (block as { type?: unknown }).type === "text" &&
114
+ typeof (block as { text?: unknown }).text === "string",
115
+ )
116
+ .map((block) => block.text)
117
+ .join("");
118
+ }
119
+
120
+ /**
121
+ * Run the side-question round. Resolves with the answer, the provider usage
122
+ * (which is still real money and is reported to `/cost`), and whether the
123
+ * operator aborted it. Provider failures reject; the caller renders the reason
124
+ * in the overlay.
125
+ */
126
+ export async function runSideQuestion(input: SideQuestionInput): Promise<SideQuestionResult> {
127
+ const { question, ...rest } = input;
128
+ return runOutOfTurnRound({ ...rest, systemPrompt: SIDE_QUESTION_SYSTEM_PROMPT, userText: question });
129
+ }
130
+
131
+ /** The shared round. See {@link OutOfTurnRoundInput}. */
132
+ export async function runOutOfTurnRound(input: OutOfTurnRoundInput): Promise<SideQuestionResult> {
133
+ const options: Record<string, unknown> = { maxTokens: input.maxTokens ?? SIDE_QUESTION_MAX_TOKENS };
134
+ if (input.apiKey !== undefined) options.apiKey = input.apiKey;
135
+ if (input.signal !== undefined) options.signal = input.signal;
136
+
137
+ const context = {
138
+ systemPrompt: input.systemPrompt,
139
+ messages: [
140
+ ...input.messages,
141
+ { role: "user", content: [{ type: "text", text: input.userText }], timestamp: Date.now() },
142
+ ],
143
+ };
144
+
145
+ const events = stream(
146
+ input.model,
147
+ context as unknown as Parameters<typeof stream>[1],
148
+ options as unknown as Parameters<typeof stream>[2],
149
+ );
150
+
151
+ let text = "";
152
+ for await (const event of events) {
153
+ if (event.type === "text_delta") {
154
+ text = textFromMessage(event.partial);
155
+ input.onDelta?.(text);
156
+ continue;
157
+ }
158
+ if (event.type === "done") {
159
+ const answer = textFromMessage(event.message).trim();
160
+ return { text: answer, usage: sideQuestionUsage((event.message as { usage?: unknown }).usage), aborted: false };
161
+ }
162
+ if (event.type === "error") {
163
+ const failed = event.error as { stopReason?: unknown; usage?: unknown; errorMessage?: unknown };
164
+ if (event.reason === "aborted" || failed.stopReason === "aborted" || input.signal?.aborted === true) {
165
+ return { text: text.trim(), usage: sideQuestionUsage(failed.usage), aborted: true };
166
+ }
167
+ throw new Error(typeof failed.errorMessage === "string" ? failed.errorMessage : "side question failed");
168
+ }
169
+ }
170
+ return { text: text.trim(), usage: null, aborted: input.signal?.aborted === true };
171
+ }