@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,25 +1,37 @@
1
- import type { Component } from "../engine/tui.js";
2
- import { buildResponsiveHint } from "./overlay-frame.js";
1
+ import type { ActionClass } from "../domains/safety/action-classifier.js";
2
+ import type { AutonomyExposure } from "../domains/safety/autonomy.js";
3
+ import {
4
+ classifyDecisionPresentation,
5
+ type DecisionPresentation,
6
+ decisionFactsForPermission,
7
+ } from "../domains/safety/decision-presentation.js";
8
+ import type { Component, OverlayOptions, TUI } from "../engine/tui.js";
9
+ import { fitHintEntries } from "./overlay-frame.js";
10
+ import { permissionHintEntries } from "./permission-hint.js";
11
+ import type { ClioToken } from "./theme/index.js";
3
12
 
4
13
  export { type AskAxis, askAxis } from "../domains/safety/approval-axis.js";
5
14
  export { describeCallTarget, sanitizeCallTargetText } from "../domains/safety/call-target.js";
15
+ export { permissionHintEntries } from "./permission-hint.js";
6
16
 
7
17
  export interface ApprovalRequestView {
8
18
  requestId: string;
9
19
  tool: string;
10
- actionClass: string;
20
+ actionClass: ActionClass;
11
21
  axis: { kind: "net"; ruleId: string } | { kind: "autonomy"; level: string };
12
22
  origin: { kind: "main" } | { kind: "worker"; agentId: string; runId: string };
23
+ /** Admission-normalized exposure. Caller prose never supplies presentation fields. */
24
+ exposure?: AutonomyExposure;
13
25
  reason: string;
14
26
  /** Typed, sanitized multi-line artifact that this one approval authorizes. */
15
27
  artifact?: { kind: "dispatch-plan"; text: string };
16
28
  /**
17
- * One-line preview of the call's object: the command for bash, the path
18
- * for file tools, else a compact args preview. The operator is deciding
19
- * whether to allow this exact call, so the overlay must show what the
20
- * call will touch, not just the tool name. Main-agent asks derive it from
21
- * the parked call's args; worker escalations carry it in the escalation
22
- * payload. Absent only when nothing meaningful is derivable.
29
+ * One-line preview of the call's allowlisted object fields. The operator is
30
+ * deciding whether to allow this exact call, so the overlay must show what
31
+ * the call will touch, not just the tool name. Unlisted fields appear only as
32
+ * type-and-size summaries. Main-agent asks derive it from the parked call's
33
+ * args; worker escalations carry it in the escalation payload. Absent only
34
+ * when nothing meaningful is derivable.
23
35
  */
24
36
  target?: string;
25
37
  queueDepth?: number;
@@ -29,6 +41,44 @@ const PERMISSION_OVERLAY_CONTENT_WIDTH = 78;
29
41
 
30
42
  export const PERMISSION_OVERLAY_WIDTH = PERMISSION_OVERLAY_CONTENT_WIDTH + 4;
31
43
 
44
+ /**
45
+ * Place a permission frame immediately above the live composer in both TUI
46
+ * modes. The editor grows with wrapped and multiline drafts, while the footer
47
+ * can add notice rows, so their rendered heights are the bottom dock rather
48
+ * than a fixed clearance. Regular mode preserves terminal scrollback and may
49
+ * leave that dock above the viewport bottom when the transcript is short.
50
+ * Recompute its viewport row on every frame so draft changes and terminal
51
+ * resizes move the dialog with the composer.
52
+ */
53
+ export function permissionOverlayPlacement(
54
+ tui: Pick<TUI, "mode" | "render">,
55
+ editor: Pick<Component, "render">,
56
+ footer: Pick<Component, "render">,
57
+ ): OverlayOptions {
58
+ const margin = { bottom: 0 };
59
+ return {
60
+ anchor: "bottom-center",
61
+ margin,
62
+ visible: (termWidth, termHeight) => {
63
+ const dockHeight = editor.render(termWidth).length + footer.render(termWidth).length;
64
+ if (tui.mode === "fullscreen") {
65
+ margin.bottom = dockHeight;
66
+ return true;
67
+ }
68
+
69
+ const baseHeight = tui.render(termWidth).length;
70
+ const composerTop = Math.max(0, baseHeight - dockHeight);
71
+ const viewportStart = Math.max(0, baseHeight - termHeight);
72
+ const composerViewportRow = composerTop - viewportStart;
73
+ margin.bottom =
74
+ composerViewportRow >= 0 && composerViewportRow < termHeight
75
+ ? Math.max(0, termHeight - composerViewportRow)
76
+ : dockHeight;
77
+ return true;
78
+ },
79
+ };
80
+ }
81
+
32
82
  class PermissionOverlayBody implements Component {
33
83
  constructor(private readonly view: ApprovalRequestView) {}
34
84
 
@@ -89,46 +139,41 @@ function wrapArtifactLine(value: string, max: number): string[] {
89
139
  return lines;
90
140
  }
91
141
 
92
- function axisLabel(axis: ApprovalRequestView["axis"], style: ApprovalRequestView["origin"]["kind"]): string {
93
- if (axis.kind === "net") return `safety-net rail ${axis.ruleId}`;
94
- return style === "worker" ? `autonomy level ${axis.level}` : `autonomy level (${axis.level})`;
142
+ export function permissionDecisionPresentation(view: ApprovalRequestView): DecisionPresentation {
143
+ return classifyDecisionPresentation(
144
+ decisionFactsForPermission({
145
+ tool: view.tool,
146
+ actionClass: view.actionClass,
147
+ axis:
148
+ view.axis.kind === "net"
149
+ ? { kind: "safety-net", ruleId: view.axis.ruleId }
150
+ : { kind: "autonomy", level: view.axis.level },
151
+ origin: view.origin,
152
+ ...(view.exposure !== undefined ? { exposure: view.exposure } : {}),
153
+ }),
154
+ );
95
155
  }
96
156
 
97
- function askedBy(view: ApprovalRequestView): string {
98
- if (view.origin.kind === "worker") {
99
- return `worker ${view.origin.agentId} (run ${view.origin.runId}), ${axisLabel(view.axis, "worker")}`;
100
- }
101
- return axisLabel(view.axis, "main");
157
+ export function permissionOverlayTitle(view: ApprovalRequestView): string {
158
+ return permissionDecisionPresentation(view).title;
102
159
  }
103
160
 
104
- export function permissionOverlayTitle(): string {
105
- return "Allow this action once?";
161
+ export function permissionOverlayTone(view: ApprovalRequestView): ClioToken {
162
+ return permissionDecisionPresentation(view).semanticToken;
106
163
  }
107
164
 
108
165
  /**
109
- * The footer for a box `innerWidth` columns wide inside its borders.
110
- *
111
- * At 40 columns the old positional elider removed `[s] stop turn` and left
112
- * "allow once" and an ambiguous "close" in front of an operator trying to
113
- * refuse. The key kept working, so the layout was hiding a live safety action.
114
- * This surface used to answer that with three hand-written width tiers; it now
115
- * states the same policy as data. Allow and stop are marked critical, Esc is
116
- * marked droppable, and `fitHintEntries` shortens every label before it drops
117
- * anything, which reproduces the three tiers exactly.
166
+ * The footer for a box `innerWidth` columns wide inside its borders. The
167
+ * three columns the bottom border spends on `─ ` and the trailing space come
168
+ * off the top, which is what `buildResponsiveHint` does for every other frame.
118
169
  */
119
- export const permissionOverlayHint = buildResponsiveHint(
120
- [
121
- { key: "Enter", verb: "allow once", short: "allow", critical: true },
122
- { key: "s", verb: "stop turn", short: "stop", critical: true },
123
- ],
124
- { key: "Esc", verb: "close", critical: false },
125
- );
126
-
127
- const SAFETY_SENTENCES: ReadonlyArray<string> = [
128
- "Parked until you decide; allow or deny applies to this call only.",
129
- "Stopping the turn denies it and ends the run, so nothing asks again.",
130
- "Hard-blocked actions remain blocked.",
131
- ];
170
+ export function permissionOverlayHint(innerWidth: number, composerHasDraft = false): string {
171
+ return fitHintEntries(permissionHintEntries(composerHasDraft), innerWidth - 3);
172
+ }
173
+
174
+ function actionConsequence(presentation: DecisionPresentation, id: "deny" | "stop"): string {
175
+ return presentation.requiredActions.find((action) => action.id === id)?.consequence ?? "";
176
+ }
132
177
 
133
178
  /**
134
179
  * The overlay's body, laid out for the width the frame gives it.
@@ -140,15 +185,15 @@ const SAFETY_SENTENCES: ReadonlyArray<string> = [
140
185
  */
141
186
  export function permissionOverlayLines(view: ApprovalRequestView, width: number): string[] {
142
187
  const content = Math.max(8, Math.floor(width));
188
+ const presentation = permissionDecisionPresentation(view);
143
189
  // The parked call is awaiting a decision, not blocked: the raw rejection
144
190
  // short ("<tool> blocked: <class>") is never rendered here because its
145
191
  // wording contradicts the ask. Tool, Target, Action, and the asking axis
146
192
  // carry everything the operator needs to decide.
147
193
  const lines = [
148
- field("Tool: ", view.tool, content),
194
+ field("Tool: ", `${view.tool} · Action: ${view.actionClass}`, content),
149
195
  ...(view.target !== undefined && view.target.length > 0 ? [field("Target: ", view.target, content)] : []),
150
- field("Action: ", view.actionClass, content),
151
- field("Asked by: ", askedBy(view), content),
196
+ ...wrapSentence(`Requested by: ${presentation.requestedByCopy}`, content),
152
197
  ...(view.artifact !== undefined
153
198
  ? [
154
199
  "",
@@ -157,7 +202,12 @@ export function permissionOverlayLines(view: ApprovalRequestView, width: number)
157
202
  ]
158
203
  : []),
159
204
  "",
160
- ...SAFETY_SENTENCES.flatMap((sentence) => wrapSentence(sentence, content)),
205
+ ...wrapSentence(`Approval: ${presentation.authorizationCopy}`, content),
206
+ ...wrapSentence(`Consequence: ${presentation.consequenceCopy}`, content),
207
+ ...wrapSentence(presentation.reversibilityCopy, content),
208
+ ...wrapSentence(`Deny: ${actionConsequence(presentation, "deny")}`, content),
209
+ ...wrapSentence(`Stop: ${actionConsequence(presentation, "stop")}`, content),
210
+ ...wrapSentence("Hard-blocked actions remain blocked.", content),
161
211
  ];
162
212
  if (view.queueDepth !== undefined && view.queueDepth > 1) {
163
213
  lines.splice(lines.indexOf(""), 0, `1 of ${view.queueDepth} parked`);
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import { sanitizeCallTargetText } from "../../domains/safety/call-target.js";
16
+ import { redactSecretString, redactToolArgs } from "../../domains/safety/redaction.js";
16
17
  import { formatSize } from "../../engine/truncate.js";
17
18
  import { visibleWidth, wrapTextWithAnsi } from "../../engine/tui.js";
18
19
  import { classifyResourceRead, toolPresentationPolicy } from "../../tools/presentation.js";
@@ -22,7 +23,10 @@ import { clioTheme, formatCompactMs, GLYPH } from "../theme/index.js";
22
23
  import { renderDiffLines } from "./diff.js";
23
24
  import { tryRenderJson, tryRenderXml } from "./structured.js";
24
25
 
25
- export { classifyResourceRead };
26
+ // The argument projection lives in the safety domain so the worker tool seam
27
+ // scrubs by the same rules; re-exported here because this renderer is where
28
+ // callers and tests have always reached for it.
29
+ export { classifyResourceRead, redactToolArgs };
26
30
 
27
31
  const theme = clioTheme();
28
32
  const dim = (text: string): string => theme.fg("dim", text);
@@ -127,52 +131,6 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
127
131
  return value !== null && typeof value === "object" && !Array.isArray(value);
128
132
  }
129
133
 
130
- const SECRET_KEY_RE =
131
- /(?:api[-_]?key|access[-_]?key|auth(?:orization)?|credential|password|private[-_]?key|secret|token)/iu;
132
- const ENV_KEY_RE = /^env(?:ironment)?$/iu;
133
- const SECRET_STRING_RE = /(https?:\/\/[^\s/@]+:)[^\s/@]+@/giu;
134
- const SECRET_URL_PARAM_RE =
135
- /([?&](?:api[-_]?key|access[-_]?token|auth(?:orization)?|credential|password|secret|token)[^=\s]*=)[^&#\s]+/giu;
136
- const SECRET_ASSIGNMENT_RE =
137
- /(?<![A-Z0-9_])((?:[A-Z0-9_]*(?:ACCESS|API|AUTH|CREDENTIAL|KEY|PASS(?:WORD)?|PRIVATE|SECRET|TOKEN)[A-Z0-9_]*)=)[^\s;&|]+/giu;
138
- const SECRET_FLAG_RE =
139
- /(--?(?:[a-z0-9-]*(?:api[-_]?key|access[-_]?token|auth(?:orization)?|credential|password|secret|token)[a-z0-9-]*))(=|\s+)([^\s;&|]+)/giu;
140
-
141
- function redactSecretString(value: string): string {
142
- return value
143
- .replace(SECRET_STRING_RE, "$1[redacted]@")
144
- .replace(SECRET_URL_PARAM_RE, "$1[redacted]")
145
- .replace(SECRET_ASSIGNMENT_RE, "$1[redacted]")
146
- .replace(SECRET_FLAG_RE, "$1$2[redacted]");
147
- }
148
-
149
- function redactEnvironmentValue(value: unknown, depth: number): unknown {
150
- if (depth > 8) return "[redacted nested values]";
151
- if (typeof value === "string") return "[redacted]";
152
- if (Array.isArray(value)) return value.map((item) => redactEnvironmentValue(item, depth + 1));
153
- if (!isPlainObject(value)) return value;
154
- const out: Record<string, unknown> = {};
155
- for (const [childKey, childValue] of Object.entries(value)) {
156
- out[childKey] = redactEnvironmentValue(childValue, depth + 1);
157
- }
158
- return out;
159
- }
160
-
161
- /** Project tool arguments for display only; execution and receipts retain the original values. */
162
- export function redactToolArgs(value: unknown, key = "", depth = 0): unknown {
163
- if (SECRET_KEY_RE.test(key)) return "[redacted]";
164
- if (ENV_KEY_RE.test(key)) return redactEnvironmentValue(value, depth);
165
- if (typeof value === "string") return redactSecretString(value);
166
- if (depth > 8) return "[redacted nested values]";
167
- if (Array.isArray(value)) return value.map((item) => redactToolArgs(item, key, depth + 1));
168
- if (!isPlainObject(value)) return value;
169
- const out: Record<string, unknown> = {};
170
- for (const [childKey, childValue] of Object.entries(value)) {
171
- out[childKey] = redactToolArgs(childValue, childKey, depth + 1);
172
- }
173
- return out;
174
- }
175
-
176
134
  function readStringField(args: unknown, key: string): string | null {
177
135
  if (!isPlainObject(args)) return null;
178
136
  const value = args[key];
@@ -389,6 +347,10 @@ function offloadPathOf(finished: ToolExecutionFinished): string | null {
389
347
  return isPlainObject(resultSize) ? stringField(resultSize, "offloadPath") : null;
390
348
  }
391
349
 
350
+ function offloadFileMissing(finished: ToolExecutionFinished): boolean {
351
+ return finished.resultSummary?.offloadFileMissing === true;
352
+ }
353
+
392
354
  function shownBytesOf(finished: ToolExecutionFinished): number | null {
393
355
  const observation = observationOf(finished);
394
356
  const fromObservation = numberField(observation, "shownBytes");
@@ -468,7 +430,12 @@ function ledgerTail(finished: ToolExecutionFinished): { facts: string; offload:
468
430
  const offloadPath = executed ? offloadPathOf(finished) : null;
469
431
  return {
470
432
  facts: parts.length > 0 ? dim(` · ${parts.join(" · ")}`) : "",
471
- offload: offloadPath !== null ? dim(` · full: ${offloadPath}`) : "",
433
+ offload:
434
+ offloadPath === null
435
+ ? ""
436
+ : offloadFileMissing(finished)
437
+ ? dim(" · full: gone after the 14-day retention sweep")
438
+ : dim(` · full: ${offloadPath}`),
472
439
  };
473
440
  }
474
441
 
@@ -1072,7 +1039,10 @@ function renderOutputMeta(
1072
1039
  function renderOutputFooter(finished: ToolExecutionFinished, width: number, isError: boolean): string[] {
1073
1040
  const out: string[] = [];
1074
1041
  const offloadPath = isNonExecutedOutcome(finished.outcome) ? null : offloadPathOf(finished);
1075
- if (offloadPath !== null) out.push(...indentAndWrap(`${yellow("full output")} ${offloadPath}`, width, isError));
1042
+ if (offloadPath !== null) {
1043
+ const pointer = offloadFileMissing(finished) ? "gone after the 14-day retention sweep" : offloadPath;
1044
+ out.push(...indentAndWrap(`${yellow("full output")} ${pointer}`, width, isError));
1045
+ }
1076
1046
  const hint =
1077
1047
  stringField(resultSizeOf(finished), "followUpHint") ?? stringField(finished.resultSummary ?? null, "followUpHint");
1078
1048
  if (hint !== null) out.push(...indentAndWrap(`${dim("next")} ${hint}`, width, isError));
@@ -17,6 +17,7 @@
17
17
  * back off replayed thinking text.
18
18
  */
19
19
 
20
+ import { type ResponseModelIdObservation, responseModelIdObservationFromRecord } from "../core/response-model-id.js";
20
21
  import { rawDurationMs } from "../core/timers.js";
21
22
  import { extractReasoningTokens } from "../domains/session/context-accounting.js";
22
23
  import type { MessageEntry, SessionEntry, SessionUsageDefaults } from "../domains/session/index.js";
@@ -133,6 +134,10 @@ export function lastTurnSummaryFromLedger(
133
134
  let toolErrorCount = 0;
134
135
  let targetId = turn.targetId;
135
136
  let modelId = turn.modelId;
137
+ let responseModelIdObservation: ResponseModelIdObservation = {
138
+ state: "legacy-difference-only",
139
+ differingModelId: null,
140
+ };
136
141
 
137
142
  for (const row of turn.rows) {
138
143
  const record = payloadRecord(row.payload);
@@ -148,7 +153,8 @@ export function lastTurnSummaryFromLedger(
148
153
  const reason = stopReasonOf(record.stopReason);
149
154
  if (reason && reason !== "stop") stopReason = reason;
150
155
  targetId = targetId ?? nonEmptyString(record, "provider", "api");
151
- modelId = modelId ?? nonEmptyString(record, "responseModel", "model");
156
+ modelId = modelId ?? nonEmptyString(record, "model");
157
+ responseModelIdObservation = responseModelIdObservationFromRecord(record, "legacy-difference-only");
152
158
  const usage = payloadRecord(record.usage);
153
159
  if (!usage) continue;
154
160
  inputTokens += positiveNumber(usage.input);
@@ -177,6 +183,7 @@ export function lastTurnSummaryFromLedger(
177
183
  stopReason,
178
184
  watchdogPeak: 0,
179
185
  truncated: false,
186
+ responseModelIdObservation,
180
187
  };
181
188
  if (sawReasoning) {
182
189
  summary.reasoningTokens = reasoningTokens;
@@ -14,6 +14,11 @@
14
14
  * only the sink wiring between that fold and the observability projection.
15
15
  */
16
16
 
17
+ import {
18
+ addResponseModelIdObservationCount,
19
+ emptyResponseModelIdObservationCounts,
20
+ type ResponseModelIdObservationCounts,
21
+ } from "../core/response-model-id.js";
17
22
  import { ledgerUsageCalls, type SessionEntry, type SessionUsageDefaults } from "../domains/session/index.js";
18
23
  import { filterEntriesToActivePath } from "../domains/session/tree/active-path.js";
19
24
 
@@ -22,7 +27,7 @@ export interface SessionUsageSink {
22
27
  resetSession(): void;
23
28
  recordTokens(
24
29
  providerId: string,
25
- modelId: string,
30
+ attributedModelId: string,
26
31
  tokens: number,
27
32
  costUsd?: number,
28
33
  breakdown?: {
@@ -35,6 +40,10 @@ export interface SessionUsageSink {
35
40
  apiCalls?: number;
36
41
  },
37
42
  costProvenance?: never,
43
+ modelIdFacts?: {
44
+ requestedModelIds: ReadonlyArray<string>;
45
+ responseModelIdObservationCounts: Readonly<ResponseModelIdObservationCounts>;
46
+ },
38
47
  ): void;
39
48
  }
40
49
 
@@ -61,14 +70,31 @@ export function reseedSessionUsageFromLedger(
61
70
  sink.resetSession();
62
71
  const scoped = filterEntriesToActivePath(entries, activeLeafTurnId ?? undefined);
63
72
  for (const call of ledgerUsageCalls(scoped, defaults)) {
64
- sink.recordTokens(call.providerId, call.modelId, call.totalTokens, call.costUsd, {
65
- input: call.input,
66
- output: call.output,
67
- cacheRead: call.cacheRead,
68
- cacheWrite: call.cacheWrite,
69
- reasoningTokens: call.reasoningTokens,
70
- totalTokens: call.totalTokens,
71
- apiCalls: call.apiCalls ?? 1,
72
- });
73
+ const responseModelIdObservationCounts = emptyResponseModelIdObservationCounts();
74
+ addResponseModelIdObservationCount(
75
+ responseModelIdObservationCounts,
76
+ call.responseModelIdObservation,
77
+ call.apiCalls ?? 1,
78
+ );
79
+ sink.recordTokens(
80
+ call.providerId,
81
+ call.attributedModelId,
82
+ call.totalTokens,
83
+ call.costUsd,
84
+ {
85
+ input: call.input,
86
+ output: call.output,
87
+ cacheRead: call.cacheRead,
88
+ cacheWrite: call.cacheWrite,
89
+ reasoningTokens: call.reasoningTokens,
90
+ totalTokens: call.totalTokens,
91
+ apiCalls: call.apiCalls ?? 1,
92
+ },
93
+ undefined,
94
+ {
95
+ requestedModelIds: [call.requestedModelId],
96
+ responseModelIdObservationCounts,
97
+ },
98
+ );
73
99
  }
74
100
  }
@@ -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
+ }