@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
@@ -187,6 +187,32 @@ export function middlewareHookFailedSessionNotice(payload: unknown, seenBudgetWa
187
187
  return middlewareHookFailedNotice(payload, { noteBudgetWarningSuppression: key !== null });
188
188
  }
189
189
 
190
+ /** How many failed checks a watchdog notice names before it counts the rest. */
191
+ export const WATCHDOG_NOTICE_CHECK_LIMIT = 3;
192
+
193
+ /**
194
+ * The watchdog's finding, as the one transcript line it is allowed.
195
+ *
196
+ * A passing report renders nothing: the operator asked for a warning, not a
197
+ * receipt, and a green line after every mutating turn is noise that trains
198
+ * people to stop reading the channel. Only the failed checks of the
199
+ * `verifier-report` contract reach here, and only their names: the evidence
200
+ * body belongs to the receipt, which the operator can open. Returning null
201
+ * means the turn passed or the report carried nothing usable.
202
+ */
203
+ export function watchdogBlockersNotice(checks: ReadonlyArray<{ name: string; passed: boolean }>): BusNotice | null {
204
+ const failed = checks.filter((check) => !check.passed && check.name.trim().length > 0);
205
+ if (failed.length === 0) return null;
206
+ const named = failed.slice(0, WATCHDOG_NOTICE_CHECK_LIMIT).map((check) => check.name.trim());
207
+ const rest = failed.length - named.length;
208
+ const more = rest > 0 ? `, and ${rest} more` : "";
209
+ const blockers = failed.length === 1 ? "blocker" : "blockers";
210
+ return {
211
+ level: "warn",
212
+ text: `[watchdog] ${failed.length} ${blockers} after this turn: ${named.join("; ")}${more}. Nothing was changed or queued.`,
213
+ };
214
+ }
215
+
190
216
  /**
191
217
  * A tool call parked for one-shot approval (sd-01 §3.3). The text names the
192
218
  * axis that produced the ask: a safety-net rail asks at every level, while an
@@ -7,6 +7,13 @@
7
7
 
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { settingsPath } from "../core/config.js";
10
+ import {
11
+ addResponseModelIdObservationCount,
12
+ emptyResponseModelIdObservationCounts,
13
+ type ResponseModelIdObservation,
14
+ type ResponseModelIdObservationCounts,
15
+ responseModelIdObservationFromRecord,
16
+ } from "../core/response-model-id.js";
10
17
  import type { PendingSkillRequest, PendingSkillToolPolicy, SkillDeclaredToolPolicy } from "../core/skill-activation.js";
11
18
  import { ToolNames } from "../core/tool-names.js";
12
19
  import { canonicalJson, sha256 } from "../domains/prompts/hash.js";
@@ -334,6 +341,7 @@ export function createAskUserToolPolicy(activeTools: ReadonlyArray<{ name: strin
334
341
  status: "idle",
335
342
  startedAt: now,
336
343
  updatedAt: now,
344
+ exposure: "local",
337
345
  rounds: [],
338
346
  decisions: [],
339
347
  inFlight: false,
@@ -356,6 +364,9 @@ export interface RunUsageSummary {
356
364
  apiCalls: number;
357
365
  hadReasoning: boolean;
358
366
  hadUsage: boolean;
367
+ responseModelIdObservationCounts: ResponseModelIdObservationCounts;
368
+ lastResponseModelIdObservation: ResponseModelIdObservation;
369
+ lastDifferingResponseModelId: string | null;
359
370
  }
360
371
 
361
372
  /**
@@ -380,6 +391,9 @@ export function sumRunUsage(messages: ReadonlyArray<AgentMessage>): RunUsageSumm
380
391
  apiCalls: 0,
381
392
  hadReasoning: false,
382
393
  hadUsage: false,
394
+ responseModelIdObservationCounts: emptyResponseModelIdObservationCounts(),
395
+ lastResponseModelIdObservation: { state: "not-observed" },
396
+ lastDifferingResponseModelId: null,
383
397
  };
384
398
  for (const raw of messages) {
385
399
  const message = raw as
@@ -397,8 +411,15 @@ export function sumRunUsage(messages: ReadonlyArray<AgentMessage>): RunUsageSumm
397
411
  };
398
412
  if (!message || typeof message !== "object") continue;
399
413
  if (message.role !== "assistant") continue;
414
+ const record = message as unknown as Record<string, unknown>;
400
415
  const usage = message.usage;
401
416
  if (!usage || typeof usage !== "object") continue;
417
+ summary.lastResponseModelIdObservation = responseModelIdObservationFromRecord(record, "not-observed");
418
+ addResponseModelIdObservationCount(summary.responseModelIdObservationCounts, summary.lastResponseModelIdObservation);
419
+ summary.lastDifferingResponseModelId =
420
+ typeof record.responseModel === "string" && record.responseModel.trim().length > 0
421
+ ? record.responseModel.trim()
422
+ : null;
402
423
  summary.hadUsage = true;
403
424
  summary.apiCalls += 1;
404
425
  const input = typeof usage.input === "number" ? usage.input : 0;
@@ -459,6 +480,7 @@ export function assistantSessionPayload(
459
480
  const raw = message as unknown as Record<string, unknown>;
460
481
  if (Array.isArray(raw.content)) payload.content = raw.content;
461
482
  if (thinking.length > 0) payload.thinking = thinking;
483
+ payload.responseModelIdObservation = responseModelIdObservationFromRecord(raw, "not-observed");
462
484
  for (const key of ["usage", "api", "provider", "model", "responseModel", "responseId", "diagnostics"]) {
463
485
  if (raw[key] !== undefined) payload[key] = raw[key];
464
486
  }
@@ -17,16 +17,19 @@ import { BusChannels, type RunAbortSource } from "../core/bus-events.js";
17
17
  import type { ClioSettings } from "../core/config.js";
18
18
  import type { SafeEventBus } from "../core/event-bus.js";
19
19
  import type { PendingSkillRequest } from "../core/skill-activation.js";
20
+ import { clioStateDir } from "../core/xdg.js";
20
21
  import {
21
22
  createMiddlewareToolChoiceControl,
22
23
  type MiddlewareContract,
23
24
  type MiddlewareToolChoiceControl,
24
25
  } from "../domains/middleware/index.js";
25
26
  import type { ObservabilityContract } from "../domains/observability/contract.js";
27
+ import type { CostEntryLabel } from "../domains/observability/cost.js";
28
+ import { appendOutOfTurnUsageRow, type OutOfTurnUsageRow } from "../domains/observability/out-of-turn-usage.js";
26
29
  import type { PromptsContract } from "../domains/prompts/contract.js";
27
30
  import { toContextOverflowError } from "../domains/providers/errors.js";
28
31
  import type { ProvidersContract } from "../domains/providers/index.js";
29
- import { runtimeTargetSnapshot } from "../domains/providers/index.js";
32
+ import { normalizeCostProvenance, runtimeTargetSnapshot, targetRequiresAuth } from "../domains/providers/index.js";
30
33
  import type { ProtectedArtifactState } from "../domains/safety/protected-artifacts.js";
31
34
  import type { CompactResult } from "../domains/session/compaction/compact.js";
32
35
  import type { ContextSnapshot, ContextUsageSnapshot } from "../domains/session/context-accounting.js";
@@ -38,6 +41,7 @@ import { protectedArtifactStateFromSessionEntries } from "../domains/session/pro
38
41
  import { isRetryableErrorMessage, type RetrySettings } from "../domains/session/retry.js";
39
42
  import { createEngineAgent } from "../engine/agent.js";
40
43
  import { resolveReservedOutputTokens } from "../engine/apis/output-budget.js";
44
+ import { cwdHash } from "../engine/session.js";
41
45
  import type { AgentEvent, AgentMessage, ImageContent } from "../engine/types.js";
42
46
  import { resolveSessionTools } from "../tools/agent-tools.js";
43
47
  import { finalizeAskUserInterviewForHost } from "../tools/ask-user.js";
@@ -53,7 +57,9 @@ import {
53
57
  toolSignatureFromState,
54
58
  } from "./chat-loop-messages.js";
55
59
  import { normalizeRetrySettings } from "./chat-loop-policy.js";
60
+ import { runHandoffRound } from "./handoff-round.js";
56
61
  import type { ApprovalRequestView } from "./permission-overlay.js";
62
+ import { runSideQuestion, type SideQuestionResult } from "./side-question.js";
57
63
  import type { AgentStatusEvent } from "./status/types.js";
58
64
  import { createTurnContext } from "./turn-context.js";
59
65
  import { createTurnMiddleware } from "./turn-middleware.js";
@@ -184,6 +190,13 @@ export interface ChatSubmitOptions {
184
190
  onAdmitted?: () => void;
185
191
  }
186
192
 
193
+ /**
194
+ * Placeholder key a target that needs no auth still has to be handed. Mirrors
195
+ * the turn runtime's own local fallback, so a `/btw` round against a local
196
+ * server authenticates exactly the way a turn against it does.
197
+ */
198
+ const LOCAL_SIDE_QUESTION_API_KEY = "clio-local-target";
199
+
187
200
  /** Closing notice an operator interrupt leaves in the transcript and the ledger. */
188
201
  const INTERRUPT_CANCEL_REASON = "[Clio Coder] run interrupted by operator; delivering the new message now.";
189
202
  const ENGINE_ACTIVE_PROMPT_ERROR =
@@ -212,6 +225,24 @@ export interface ChatCancelOptions {
212
225
  auditReason?: string;
213
226
  }
214
227
 
228
+ export interface SideQuestionOptions {
229
+ /** Cancels the round. Esc and Ctrl+C in the overlay abort through it. */
230
+ signal?: AbortSignal;
231
+ /** Streamed answer text so the overlay fills as the provider produces it. */
232
+ onDelta?: (partialText: string) => void;
233
+ }
234
+
235
+ /**
236
+ * How a `/btw` round ended. `refused` is a round that never started (a turn was
237
+ * in flight, or no orchestrator target is configured); `failed` is a round that
238
+ * started and the provider rejected.
239
+ */
240
+ export type SideQuestionOutcome =
241
+ | { status: "answered"; text: string }
242
+ | { status: "aborted"; text: string }
243
+ | { status: "refused"; reason: string }
244
+ | { status: "failed"; reason: string };
245
+
215
246
  export interface ChatLoop {
216
247
  submit(text: string, options?: ChatSubmitOptions): Promise<void>;
217
248
  steer(text: string): boolean;
@@ -250,6 +281,28 @@ export interface ChatLoop {
250
281
  * notice so the `/context compact` handler does not have to mirror the logic.
251
282
  */
252
283
  compact(instructions?: string): Promise<void>;
284
+ /**
285
+ * `/btw`: answer one side question against the session's active target,
286
+ * model, and compiled message history without starting a turn.
287
+ *
288
+ * Nothing this produces reaches the session ledger, the transcript, the
289
+ * context ledger, or the footer token counters; the message history is read,
290
+ * never mutated. The round's provider usage is still reported to `/cost`,
291
+ * labeled as a side question, because money was spent. Refused outright
292
+ * while a turn is in flight rather than queued.
293
+ */
294
+ askSideQuestion(question: string, options?: SideQuestionOptions): Promise<SideQuestionOutcome>;
295
+ /**
296
+ * `/handoff`: run the extraction round for a goal against the same target,
297
+ * model, and compiled message history, and return its raw JSON answer.
298
+ *
299
+ * Like a side question this is out of turn: no tools are sent, the message
300
+ * history is read and never mutated, and nothing the round produces reaches
301
+ * the ledger. Validating, bounding, and reviewing the answer belong to the
302
+ * caller; this method only owns the provider call. Refused outright while a
303
+ * turn is in flight rather than queued.
304
+ */
305
+ extractHandoff(goal: string, options?: SideQuestionOptions): Promise<SideQuestionOutcome>;
253
306
  /**
254
307
  * Drop or replace the chat-loop's in-memory state after a session switch
255
308
  * (/resume, /fork, /new). `leafTurnId` is the id the next user turn
@@ -292,6 +345,20 @@ export interface CreateChatLoopDeps {
292
345
  */
293
346
  prompts?: PromptsContract;
294
347
  createAgent?: typeof createEngineAgent;
348
+ /**
349
+ * The `/btw` round. Defaults to the real provider call; contracts inject a
350
+ * stub so they can assert what a side question does to the session without
351
+ * standing up a provider, exactly as `createAgent` does for a turn.
352
+ */
353
+ runSideQuestion?: typeof runSideQuestion;
354
+ /** The `/handoff` extraction round. Injectable for the same reason. */
355
+ runHandoffRound?: typeof runHandoffRound;
356
+ /**
357
+ * Append one priced out-of-turn call to the durable out-of-turn usage store.
358
+ * Defaults to the real writer under the state dir. Contracts inject a spy so
359
+ * they can assert the row was written without touching a real state dir.
360
+ */
361
+ recordOutOfTurnUsageRow?: (row: OutOfTurnUsageRow) => void;
295
362
  /**
296
363
  * Return the current session's entries for token estimation. The chat-loop
297
364
  * calls this on every submit so the auto-compaction threshold sees the
@@ -369,6 +436,12 @@ export interface CreateChatLoopDeps {
369
436
  * composition; the loop owns the buffer the reminder lands in.
370
437
  */
371
438
  registerDeferredReminderSink?: (sink: (message: string) => void) => void;
439
+ /**
440
+ * The same seam for findings that are for the operator rather than the model.
441
+ * The watchdog uses it: its run settles after the turn it reviewed, and its
442
+ * blockers become one transcript notice that never enters model context.
443
+ */
444
+ registerDeferredNoticeSink?: (sink: (text: string) => void) => void;
372
445
  /**
373
446
  * Host-finalizer seam for branch-anchored interview snapshots. Called once,
374
447
  * after the ask-user host finalizer has settled the policy and its transcript.
@@ -399,6 +472,8 @@ export function reloadProtectedArtifactsForSession(
399
472
  export function createChatLoop(deps: CreateChatLoopDeps): ChatLoop {
400
473
  const listeners = new Set<(event: ChatLoopEvent) => void>();
401
474
  const createAgent = deps.createAgent ?? createEngineAgent;
475
+ const sideQuestionRound = deps.runSideQuestion ?? runSideQuestion;
476
+ const handoffRound = deps.runHandoffRound ?? runHandoffRound;
402
477
  const middlewareToolChoice = deps.middlewareToolChoice ?? createMiddlewareToolChoiceControl();
403
478
  const state = createTurnState(deps.getSettings().orchestrator.thinkingLevel ?? "off");
404
479
  const toolStartTimes = new Map<string, number>();
@@ -485,6 +560,7 @@ export function createChatLoop(deps: CreateChatLoopDeps): ChatLoop {
485
560
 
486
561
  try {
487
562
  deps.registerDeferredReminderSink?.((message) => middleware.injectDeferredReminder(message));
563
+ deps.registerDeferredNoticeSink?.((text) => middleware.emitDeferredNotice(text));
488
564
  } catch {
489
565
  // A background observer losing its delivery path must not stop the loop
490
566
  // from starting; it simply stays silent.
@@ -549,6 +625,12 @@ export function createChatLoop(deps: CreateChatLoopDeps): ChatLoop {
549
625
  emitNotice,
550
626
  toolStartTimes,
551
627
  });
628
+ // A fresh ledger and footer render before the first submit. Start the same
629
+ // live capability probe that submit and resume use so those boot surfaces
630
+ // refresh to the probed model window as soon as provider discovery lands.
631
+ // The turn runtime coalesces the first submit onto this request, then its
632
+ // target and model TTL keeps later submits from repeating it.
633
+ void turnRuntime.ensureLiveCapabilitiesForSelectedModel().catch(() => {});
552
634
 
553
635
  // --- bus subscriptions --------------------------------------------------
554
636
 
@@ -586,6 +668,102 @@ export function createChatLoop(deps: CreateChatLoopDeps): ChatLoop {
586
668
  return null;
587
669
  };
588
670
 
671
+ /**
672
+ * Everything an out-of-turn round needs, or the reason it cannot run.
673
+ *
674
+ * `/btw` and `/handoff` are the two callers. Both read the compiled history
675
+ * the next turn would see, both authenticate exactly the way a turn against
676
+ * the same target does, and both are refused rather than queued while a turn
677
+ * is in flight, so the admission decision is made once here.
678
+ */
679
+ type OutOfTurnPreparation =
680
+ | { ok: true; runtime: AgentRuntime; apiKey: string | undefined }
681
+ | { ok: false; reason: string };
682
+
683
+ const prepareOutOfTurnRound = async (inFlightRefusal: string, signal?: AbortSignal): Promise<OutOfTurnPreparation> => {
684
+ if (state.streaming) return { ok: false, reason: inFlightRefusal };
685
+ let agentRuntime: AgentRuntime | null;
686
+ try {
687
+ agentRuntime = turnRuntime.ensureRuntime();
688
+ } catch (err) {
689
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
690
+ }
691
+ if (!agentRuntime) return { ok: false, reason: notConfiguredNotice() };
692
+ const resolution = agentRuntime.runtimeResolution;
693
+ try {
694
+ const apiKey = targetRequiresAuth(resolution.target, resolution.runtime)
695
+ ? (
696
+ await deps.providers.auth.resolveForTarget(resolution.target, resolution.runtime, signal ? { signal } : undefined)
697
+ ).apiKey
698
+ : LOCAL_SIDE_QUESTION_API_KEY;
699
+ return { ok: true, runtime: agentRuntime, apiKey };
700
+ } catch (err) {
701
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
702
+ }
703
+ };
704
+
705
+ const writeOutOfTurnUsageRow =
706
+ deps.recordOutOfTurnUsageRow ?? ((row: OutOfTurnUsageRow): void => appendOutOfTurnUsageRow(clioStateDir(), row));
707
+
708
+ /**
709
+ * Report an out-of-turn round's provider usage. Money was spent, so `/cost`
710
+ * says so under its own label; turn persistence, the working-set ledger,
711
+ * compaction inputs, and the footer counters never see it.
712
+ *
713
+ * The same call is also appended to the out-of-turn usage store under the
714
+ * state dir. That store exists because `/cost` only knows what this process
715
+ * spent: the round appends nothing to the session JSONL by design, so an
716
+ * archive reader such as `clio-coder usage report` had no record of the
717
+ * spend at all once the process exited. The session ledger stays untouched.
718
+ */
719
+ const recordOutOfTurnUsage = (
720
+ runtime: AgentRuntime,
721
+ usage: SideQuestionResult["usage"],
722
+ label: CostEntryLabel,
723
+ ): void => {
724
+ if (!usage) return;
725
+ const costProvenance = runtime.runtimeResolution.costProvenance;
726
+ deps.observability?.recordTokens(
727
+ runtime.targetId,
728
+ runtime.wireModelId,
729
+ usage.totalTokens,
730
+ usage.costUsd,
731
+ {
732
+ input: usage.input,
733
+ output: usage.output,
734
+ cacheRead: usage.cacheRead,
735
+ cacheWrite: usage.cacheWrite,
736
+ reasoningTokens: usage.reasoning,
737
+ totalTokens: usage.totalTokens,
738
+ apiCalls: 1,
739
+ },
740
+ costProvenance,
741
+ undefined,
742
+ label,
743
+ );
744
+ const meta = deps.session?.current() ?? null;
745
+ writeOutOfTurnUsageRow({
746
+ label,
747
+ sessionId: meta?.id ?? null,
748
+ // The identity the session ledger is filed under, so `usage report
749
+ // --repo` selects these rows with the same hash it selects ledgers with.
750
+ repoIdentity: meta ? meta.cwdHash || cwdHash(meta.cwd || process.cwd()) : null,
751
+ timestamp: new Date().toISOString(),
752
+ target: runtime.targetId,
753
+ attributedModelId: runtime.wireModelId,
754
+ usage: {
755
+ input: usage.input,
756
+ output: usage.output,
757
+ cacheRead: usage.cacheRead,
758
+ cacheWrite: usage.cacheWrite,
759
+ reasoning: usage.reasoning,
760
+ totalTokens: usage.totalTokens,
761
+ costUsd: usage.costUsd,
762
+ costProvenance: normalizeCostProvenance(costProvenance),
763
+ },
764
+ });
765
+ };
766
+
589
767
  const api: ChatLoop = {
590
768
  steer: (text) => queues.steer(text),
591
769
  queueFollowUp: (text) => queues.queueFollowUp(text),
@@ -664,6 +842,7 @@ export function createChatLoop(deps: CreateChatLoopDeps): ChatLoop {
664
842
  // 1. Accept the prompt: reset per-turn accounting, freeze the tool
665
843
  // surface, fire turn_start, and assemble the submitted text.
666
844
  state.turnToolCalls = 0;
845
+ state.turnToolNames = [];
667
846
  state.turnSharedWorkerNote = isWorkerShareNote(text);
668
847
  middlewareToolChoice.reset();
669
848
  if (options.requestContinuation !== true) state.stalledTurnNudgeSpent = false;
@@ -988,6 +1167,12 @@ export function createChatLoop(deps: CreateChatLoopDeps): ChatLoop {
988
1167
  middlewareToolChoice.reset();
989
1168
  state.lastTurnId = leafTurnId;
990
1169
  context.resetForSession();
1170
+ // The resumed ledger renders before the first new turn (issue #189),
1171
+ // and the window it renders against should be the probed one rather
1172
+ // than the catalog's, so the live capability probe the first submit
1173
+ // would run runs now, under the same TTL; the footer refreshes on the
1174
+ // provider-health event the probe publishes.
1175
+ void turnRuntime.ensureLiveCapabilitiesForSelectedModel().catch(() => {});
991
1176
  state.replayedContextMessages = replayMessages ? [...replayMessages] : [];
992
1177
  if (state.runtime) {
993
1178
  state.runtime.agent.state.messages = [...state.replayedContextMessages];
@@ -1011,6 +1196,68 @@ export function createChatLoop(deps: CreateChatLoopDeps): ChatLoop {
1011
1196
  middlewareToolChoice.reset();
1012
1197
  },
1013
1198
 
1199
+ async askSideQuestion(question: string, options: SideQuestionOptions = {}): Promise<SideQuestionOutcome> {
1200
+ const text = question.trim();
1201
+ if (text.length === 0) {
1202
+ return { status: "refused", reason: "a side question needs a question" };
1203
+ }
1204
+ // Never queued. A side question exists to be answered now, beside a run
1205
+ // the operator is watching; holding it until the run settles would
1206
+ // deliver it after the moment it was asked in had passed.
1207
+ const prepared = await prepareOutOfTurnRound(
1208
+ "a turn is in flight; /btw runs beside the session, not in its queue",
1209
+ options.signal,
1210
+ );
1211
+ if (!prepared.ok) return { status: "refused", reason: prepared.reason };
1212
+ let result: SideQuestionResult;
1213
+ try {
1214
+ result = await sideQuestionRound({
1215
+ model: prepared.runtime.agent.state.model,
1216
+ // Read-only: runSideQuestion copies before appending its own
1217
+ // message, so the live agent's history is untouched.
1218
+ messages: prepared.runtime.agent.state.messages,
1219
+ question: text,
1220
+ ...(prepared.apiKey !== undefined ? { apiKey: prepared.apiKey } : {}),
1221
+ ...(options.signal ? { signal: options.signal } : {}),
1222
+ ...(options.onDelta ? { onDelta: options.onDelta } : {}),
1223
+ });
1224
+ } catch (err) {
1225
+ return { status: "failed", reason: err instanceof Error ? err.message : String(err) };
1226
+ }
1227
+ recordOutOfTurnUsage(prepared.runtime, result.usage, "side-question");
1228
+ return result.aborted ? { status: "aborted", text: result.text } : { status: "answered", text: result.text };
1229
+ },
1230
+
1231
+ async extractHandoff(goal: string, options: SideQuestionOptions = {}): Promise<SideQuestionOutcome> {
1232
+ const text = goal.trim();
1233
+ if (text.length === 0) {
1234
+ return { status: "refused", reason: "a handoff needs a goal" };
1235
+ }
1236
+ // Refused, never queued: a handoff describes a session that has stopped
1237
+ // working, and a turn still in flight is about to change what the
1238
+ // document would say.
1239
+ const prepared = await prepareOutOfTurnRound(
1240
+ "a turn is in flight; /handoff cannot summarize a session that is still moving",
1241
+ options.signal,
1242
+ );
1243
+ if (!prepared.ok) return { status: "refused", reason: prepared.reason };
1244
+ let result: SideQuestionResult;
1245
+ try {
1246
+ result = await handoffRound({
1247
+ model: prepared.runtime.agent.state.model,
1248
+ // Read-only, exactly as the side-question round treats it.
1249
+ messages: prepared.runtime.agent.state.messages,
1250
+ goal: text,
1251
+ ...(prepared.apiKey !== undefined ? { apiKey: prepared.apiKey } : {}),
1252
+ ...(options.signal ? { signal: options.signal } : {}),
1253
+ });
1254
+ } catch (err) {
1255
+ return { status: "failed", reason: err instanceof Error ? err.message : String(err) };
1256
+ }
1257
+ recordOutOfTurnUsage(prepared.runtime, result.usage, "handoff");
1258
+ return result.aborted ? { status: "aborted", text: result.text } : { status: "answered", text: result.text };
1259
+ },
1260
+
1014
1261
  async compact(instructions?: string): Promise<void> {
1015
1262
  // Session check runs BEFORE orchestrator-configuration so a fresh
1016
1263
  // TUI with nothing configured still reports the actionable "no
@@ -13,6 +13,7 @@
13
13
  * `isTransparentAssistantWrapper`.
14
14
  */
15
15
 
16
+ import { existsSync } from "node:fs";
16
17
  import { ToolNames } from "../core/tool-names.js";
17
18
  import { foldWorkingSet } from "../domains/context/working-set/fold.js";
18
19
  import { compactionCut } from "../domains/context/working-set/visible.js";
@@ -30,6 +31,13 @@ import type {
30
31
  ThinkingLevelChangeEntry,
31
32
  WorkerRunEntry,
32
33
  } from "../domains/session/entries.js";
34
+ import {
35
+ HANDOFF_NOTE_CUSTOM_TYPE,
36
+ HANDOFF_SEED_CUSTOM_TYPE,
37
+ handoffSeedContextText,
38
+ isHandoffNoteData,
39
+ isHandoffSeedData,
40
+ } from "../domains/session/handoff.js";
33
41
  import { filterEntriesToActivePath } from "../domains/session/tree/active-path.js";
34
42
  import {
35
43
  type BashExecutionMessage,
@@ -44,7 +52,7 @@ import type { AgentMessage } from "../engine/types.js";
44
52
  import { toolPresentationPolicy } from "../tools/presentation.js";
45
53
  import { toolResultPresentationText } from "../tools/result-disposition.js";
46
54
  import type { ChatLoopEvent, RetryStatusPayload } from "./chat-loop.js";
47
- import { isSelfExplainingAbort } from "./chat-loop-messages.js";
55
+ import { isSelfExplainingAbort, toolResultSummary } from "./chat-loop-messages.js";
48
56
  import type { ChatPanel } from "./chat-panel.js";
49
57
  import { renderBranchSummaryEntry } from "./renderers/branch-summary.js";
50
58
  import { renderCompactionSummaryEntry } from "./renderers/compaction-summary.js";
@@ -440,6 +448,7 @@ function richMessageFromEntry(entry: MessageEntry, maxTextChars?: number): Agent
440
448
  "api",
441
449
  "provider",
442
450
  "model",
451
+ "responseModelIdObservation",
443
452
  "responseModel",
444
453
  "responseId",
445
454
  "diagnostics",
@@ -671,6 +680,19 @@ interface ReplayToolResult {
671
680
  blockReason?: string;
672
681
  }
673
682
 
683
+ /**
684
+ * Resolve a replayed offload pointer once, when the ledger row becomes a
685
+ * display event. Terminal repaints consume the recorded fact and never touch
686
+ * the filesystem.
687
+ */
688
+ function replayResultSummary(result: ReplayToolResult): Record<string, unknown> | undefined {
689
+ const summary = result.resultSummary ?? toolResultSummary(result.result);
690
+ const offloadPath =
691
+ typeof summary.offloadPath === "string" && summary.offloadPath.length > 0 ? summary.offloadPath : null;
692
+ if (offloadPath === null) return result.resultSummary;
693
+ return { ...summary, offloadFileMissing: !existsSync(offloadPath) };
694
+ }
695
+
674
696
  function extractToolResult(entry: MessageEntry): ReplayToolResult {
675
697
  const payload = entry.payload;
676
698
  const obj = payloadObject(payload);
@@ -780,11 +802,18 @@ function rendersCustomEntry(entry: CustomEntry): boolean {
780
802
  if (entry.display === false) return false;
781
803
  if (entry.customType === "retryStatus") return true;
782
804
  if (entry.customType === "finishContractAdvisory" || entry.customType === "middlewareReminder") return true;
805
+ if (entry.customType === HANDOFF_SEED_CUSTOM_TYPE || entry.customType === HANDOFF_NOTE_CUSTOM_TYPE) return true;
783
806
  return entry.display === true;
784
807
  }
785
808
 
786
809
  function renderCustomEntry(entry: CustomEntry, width: number): string[] {
787
810
  if (entry.customType === "retryStatus") return renderRetryStatusEntry(entry, width);
811
+ if (entry.customType === HANDOFF_SEED_CUSTOM_TYPE && isHandoffSeedData(entry.data)) {
812
+ return wrapTextWithAnsi(styleTaggedNotice(`[handoff] carried from session ${entry.data.fromSessionId}`), width);
813
+ }
814
+ if (entry.customType === HANDOFF_NOTE_CUSTOM_TYPE && isHandoffNoteData(entry.data)) {
815
+ return wrapTextWithAnsi(styleTaggedNotice(`[handoff] handed off to session ${entry.data.toSessionId}`), width);
816
+ }
788
817
  // "finishContractAdvisory" is the pre-middleware name for the same entry
789
818
  // shape; older session ledgers still carry it.
790
819
  if (entry.customType === "finishContractAdvisory" || entry.customType === "middlewareReminder") {
@@ -1024,7 +1053,15 @@ export function buildReplayAgentMessagesFromTurns(
1024
1053
  case "skillActivation":
1025
1054
  appendContextMessage(out, "user", skillActivationContextText(entry), entry.timestamp);
1026
1055
  break;
1056
+ // The one custom entry that becomes a model message. `/handoff` seeds a
1057
+ // new session with a reviewed document, and the seed is the first thing
1058
+ // the model reads there; it is labelled by its origin session and
1059
+ // carried as data, never as a user turn the operator did not write.
1027
1060
  case "custom":
1061
+ if (entry.customType === HANDOFF_SEED_CUSTOM_TYPE && isHandoffSeedData(entry.data)) {
1062
+ appendContextMessage(out, "user", handoffSeedContextText(entry.data), entry.timestamp);
1063
+ }
1064
+ break;
1028
1065
  case "modelChange":
1029
1066
  case "thinkingLevelChange":
1030
1067
  case "fileEntry":
@@ -1131,6 +1168,7 @@ export function rehydrateChatPanelFromTurns(
1131
1168
  }
1132
1169
  if (entry.role === "tool_result") {
1133
1170
  const result = extractToolResult(entry);
1171
+ const resultSummary = replayResultSummary(result);
1134
1172
  const evictedReason = workingSet.evicted.get(entry.turnId)?.reason;
1135
1173
  const fallbackId = result.id ?? pendingToolIds.pop() ?? null;
1136
1174
  if (fallbackId) {
@@ -1143,7 +1181,7 @@ export function rehydrateChatPanelFromTurns(
1143
1181
  result: displayReplayToolResult(result.result, options.unboundedToolBodies === true),
1144
1182
  isError: result.isError,
1145
1183
  ...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}),
1146
- ...(result.resultSummary !== undefined ? { resultSummary: result.resultSummary } : {}),
1184
+ ...(resultSummary !== undefined ? { resultSummary } : {}),
1147
1185
  ...(result.outcome !== undefined ? { outcome: result.outcome } : {}),
1148
1186
  ...(result.blockReason !== undefined ? { blockReason: result.blockReason } : {}),
1149
1187
  ...(evictedReason !== undefined ? { evictedReason } : {}),
@@ -1157,7 +1195,7 @@ export function rehydrateChatPanelFromTurns(
1157
1195
  result: displayReplayToolResult(result.result, options.unboundedToolBodies === true),
1158
1196
  isError: result.isError,
1159
1197
  ...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}),
1160
- ...(result.resultSummary !== undefined ? { resultSummary: result.resultSummary } : {}),
1198
+ ...(resultSummary !== undefined ? { resultSummary } : {}),
1161
1199
  ...(result.outcome === "blocked" ? { outcome: "blocked" as const } : {}),
1162
1200
  ...(result.blockReason !== undefined ? { blockReason: result.blockReason } : {}),
1163
1201
  ...(evictedReason !== undefined ? { evictedReason } : {}),