@iowarp/clio-coder 0.3.0 → 0.3.2

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 (1187) hide show
  1. package/CHANGELOG.md +123 -2
  2. package/CONTRIBUTING.md +23 -23
  3. package/README.md +295 -627
  4. package/dist/acp-BIYHVZIM.js +128 -0
  5. package/dist/{agents-Q4MYPMUW.js → agents-YT6SSRIT.js} +52 -27
  6. package/dist/assets/codewiki.json +1 -0
  7. package/dist/assets/grammars/notices/@vscode__tree-sitter-wasm/LICENSE +21 -0
  8. package/dist/assets/grammars/notices/@vscode__tree-sitter-wasm/cgmanifest.json +16 -0
  9. package/dist/assets/grammars/notices/tree-sitter-wasms/LICENSE +24 -0
  10. package/dist/assets/grammars/tree-sitter-c-sharp.wasm +0 -0
  11. package/dist/assets/grammars/tree-sitter-c.wasm +0 -0
  12. package/dist/assets/grammars/tree-sitter-cpp.wasm +0 -0
  13. package/dist/assets/grammars/tree-sitter-go.wasm +0 -0
  14. package/dist/assets/grammars/tree-sitter-java.wasm +0 -0
  15. package/dist/assets/grammars/tree-sitter-javascript.wasm +0 -0
  16. package/dist/assets/grammars/tree-sitter-python.wasm +0 -0
  17. package/dist/assets/grammars/tree-sitter-ruby.wasm +0 -0
  18. package/dist/assets/grammars/tree-sitter-rust.wasm +0 -0
  19. package/dist/assets/grammars/tree-sitter-tsx.wasm +0 -0
  20. package/dist/assets/grammars/tree-sitter-typescript.wasm +0 -0
  21. package/dist/assets/grammars/tree-sitter.wasm +0 -0
  22. package/dist/{auth-O6HYIJ6J.js → auth-5TWEIYDN.js} +30 -15
  23. package/dist/chunk-2EHAIA3X.js +481 -0
  24. package/dist/{chunk-ME6DNWIU.js → chunk-2IR2NMPA.js} +12 -4
  25. package/dist/chunk-2SFS6XQE.js +122 -0
  26. package/dist/{chunk-KKKPTZLM.js → chunk-2STGEDFV.js} +8 -1
  27. package/dist/chunk-2VTFPG5O.js +48 -0
  28. package/dist/chunk-3I7MS7N2.js +40 -0
  29. package/dist/chunk-3R73A4XB.js +69 -0
  30. package/dist/{chunk-JPOWPFCU.js → chunk-3ZXDFGR5.js} +52 -23
  31. package/dist/chunk-4BJ5BYCE.js +61 -0
  32. package/dist/{chunk-26BZQOAD.js → chunk-4BPJXDWC.js} +161 -260
  33. package/dist/chunk-4KLWL3UC.js +18 -0
  34. package/dist/{chunk-5LDRNKX2.js → chunk-4VP4KH3K.js} +161 -262
  35. package/dist/chunk-4ZG3XFUR.js +77 -0
  36. package/dist/chunk-5B2AEOW5.js +5407 -0
  37. package/dist/chunk-5TSRNF4G.js +510 -0
  38. package/dist/{chunk-WD6AJM35.js → chunk-5UUP6MWO.js} +48 -69
  39. package/dist/{chunk-X3BR7HWV.js → chunk-5WIGXA4T.js} +7 -1
  40. package/dist/chunk-65DEGPJ6.js +52 -0
  41. package/dist/chunk-6EJMN2Y3.js +17 -0
  42. package/dist/chunk-6EJV5X2W.js +16405 -0
  43. package/dist/chunk-6N5PTWMY.js +136 -0
  44. package/dist/chunk-6XLNIQDB.js +27 -0
  45. package/dist/{chunk-XNISANGE.js → chunk-77VKQEHF.js} +171 -74
  46. package/dist/chunk-7CR24IG7.js +242 -0
  47. package/dist/{chunk-UWIJNAOB.js → chunk-7EYHLWU7.js} +907 -638
  48. package/dist/chunk-7MNJORFF.js +22 -0
  49. package/dist/{chunk-JWHCJDCI.js → chunk-A3CYT5EX.js} +257 -79
  50. package/dist/chunk-AGYYIBLL.js +1069 -0
  51. package/dist/chunk-AO4RKG4M.js +277 -0
  52. package/dist/chunk-APJ265NV.js +7410 -0
  53. package/dist/chunk-ARBGF5F7.js +174 -0
  54. package/dist/{chunk-TFSSEXL6.js → chunk-BMEMKKIT.js} +14 -4
  55. package/dist/{chunk-X3NE4WVW.js → chunk-BNAZZHFG.js} +7 -1
  56. package/dist/chunk-CBCAPZAA.js +229 -0
  57. package/dist/{chunk-QV47YRF4.js → chunk-CGKSTWHD.js} +7 -1
  58. package/dist/chunk-CMZWFGD2.js +352 -0
  59. package/dist/chunk-ECH6PKUQ.js +39 -0
  60. package/dist/{chunk-67MTHP2E.js → chunk-ED4KHGC3.js} +13 -5
  61. package/dist/chunk-EKMEHE4H.js +340 -0
  62. package/dist/{chunk-O4EJEDHO.js → chunk-EPVUXGXG.js} +50 -35
  63. package/dist/chunk-FCSXB6T2.js +338 -0
  64. package/dist/chunk-FJ3H4MN5.js +48 -0
  65. package/dist/chunk-FQ4SKYE4.js +29 -0
  66. package/dist/chunk-G2DE3C7R.js +644 -0
  67. package/dist/chunk-G4BMMOKF.js +182 -0
  68. package/dist/{chunk-7BHLZB3A.js → chunk-GGXXDWE4.js} +190 -1218
  69. package/dist/chunk-HC4CLZ2Y.js +68 -0
  70. package/dist/{chunk-E3NYWENM.js → chunk-HFSBBKSQ.js} +55 -61
  71. package/dist/chunk-HKIYEGME.js +30 -0
  72. package/dist/chunk-I4HZDVNP.js +73 -0
  73. package/dist/chunk-IGLFWIYI.js +161 -0
  74. package/dist/chunk-IHXBNWMM.js +137 -0
  75. package/dist/chunk-IKCO5N3L.js +162 -0
  76. package/dist/chunk-IR4CFBFN.js +56 -0
  77. package/dist/{chunk-DXQNI4PC.js → chunk-IWHMRKLL.js} +8 -1
  78. package/dist/{chunk-VOO7NYPP.js → chunk-J5HN4RYU.js} +27 -9
  79. package/dist/{chunk-4UQA7NCT.js → chunk-J5Q24KAG.js} +7 -2
  80. package/dist/{chunk-KBR4MZZR.js → chunk-JVCV3ICN.js} +7 -1
  81. package/dist/chunk-KJ5LWLOE.js +1077 -0
  82. package/dist/chunk-LBMZMYH2.js +285 -0
  83. package/dist/{chunk-3DANZDGR.js → chunk-LBNRH5WM.js} +100 -161
  84. package/dist/{chunk-TKHQ4DGZ.js → chunk-LZSJBIVT.js} +7358 -7414
  85. package/dist/{chunk-UGYAX5YI.js → chunk-M6SHUN7Q.js} +13 -8
  86. package/dist/chunk-MAW544W2.js +1882 -0
  87. package/dist/chunk-MBS4V7ZP.js +217 -0
  88. package/dist/{chunk-2J63S4SF.js → chunk-MFFY33HR.js} +167 -140
  89. package/dist/chunk-MNA4JGU4.js +255 -0
  90. package/dist/chunk-MQSRRFWA.js +3428 -0
  91. package/dist/{chunk-RQDWMVRB.js → chunk-MVVUPGPW.js} +138 -137
  92. package/dist/chunk-OAO4GE4M.js +619 -0
  93. package/dist/chunk-OHHN2SO4.js +5135 -0
  94. package/dist/chunk-OKGUZO2U.js +34 -0
  95. package/dist/{chunk-BBUH4VAA.js → chunk-OOJYHWRB.js} +60 -361
  96. package/dist/{chunk-D5YD55AR.js → chunk-OQ33BKR3.js} +12 -1
  97. package/dist/chunk-OQE5J4C6.js +73 -0
  98. package/dist/{chunk-AMFR5YA3.js → chunk-ORBHGJC5.js} +26 -12
  99. package/dist/chunk-POHLU5DW.js +1186 -0
  100. package/dist/chunk-QKMUKYO7.js +4961 -0
  101. package/dist/{chunk-7RBKDI66.js → chunk-QTYWRVRA.js} +104 -12
  102. package/dist/{chunk-5N2FG33Q.js → chunk-R346GLFC.js} +7 -1
  103. package/dist/chunk-SRF2PJNW.js +184 -0
  104. package/dist/chunk-SST6Z5JA.js +80 -0
  105. package/dist/{chunk-XV4ZJ6ZM.js → chunk-STBPMHSX.js} +473 -1194
  106. package/dist/{chunk-HBU5MTAM.js → chunk-SVNXH4L2.js} +7 -1
  107. package/dist/chunk-T6YILFSB.js +80 -0
  108. package/dist/chunk-TXOTCRLG.js +15 -0
  109. package/dist/chunk-TZTZS7QK.js +227 -0
  110. package/dist/chunk-UOV2BYIW.js +107 -0
  111. package/dist/{chunk-527KG6XR.js → chunk-V4RXGQ5Q.js} +72 -185
  112. package/dist/chunk-VAKQQHWR.js +434 -0
  113. package/dist/chunk-VG7TBQIY.js +128 -0
  114. package/dist/chunk-VJWL6YS5.js +244 -0
  115. package/dist/chunk-WEH5XRJQ.js +32 -0
  116. package/dist/{chunk-VPAWTYLY.js → chunk-WEPFGWHJ.js} +7 -1
  117. package/dist/chunk-WMSVI4G2.js +2095 -0
  118. package/dist/chunk-WVO7V2QY.js +797 -0
  119. package/dist/chunk-X4RCMKVQ.js +641 -0
  120. package/dist/chunk-X75S7HFS.js +374 -0
  121. package/dist/chunk-XN3L4EYL.js +46 -0
  122. package/dist/{chunk-HMYNFFY4.js → chunk-XPLRXC72.js} +7 -2
  123. package/dist/{chunk-PIDUD6M2.js → chunk-YCWGATWI.js} +12 -5
  124. package/dist/chunk-YHZX5GEU.js +193 -0
  125. package/dist/chunk-YXLYO42X.js +91 -0
  126. package/dist/{chunk-UHTSULZS.js → chunk-ZDOOVTXZ.js} +38 -73
  127. package/dist/chunk-ZI647VB5.js +37 -0
  128. package/dist/{chunk-CLJ5HLUD.js → chunk-ZWLZP4ZT.js} +25 -6
  129. package/dist/cli/index.js +70 -56
  130. package/dist/clio-4LY5K2AC.js +25 -0
  131. package/dist/code-nav-7AX6FYE6.js +600 -0
  132. package/dist/codewiki/build-worker.js +66 -0
  133. package/dist/compile-cache-CVJMMODC.js +18 -0
  134. package/dist/{components-JVHMUBEB.js → components-KELWS457.js} +31 -9
  135. package/dist/{config-ZFCDBMDC.js → config-GTLUW2PR.js} +104 -56
  136. package/dist/configure-R6A64DHX.js +42 -0
  137. package/dist/context-5VKGUVJJ.js +866 -0
  138. package/dist/{context-CDXTP2MP.js → context-JFZEJ7W5.js} +21 -13
  139. package/dist/{context-E3KIFVXI.js → context-RW5HC47S.js} +77 -36
  140. package/dist/{context-clear-3F4PLXOS.js → context-clear-6ZHBAZZT.js} +64 -30
  141. package/dist/{context-index-Q7YSYTR3.js → context-index-BZ4UYMTC.js} +36 -24
  142. package/dist/dispatch-runner-VKBRCWQC.js +1997 -0
  143. package/dist/{docs-YIETIWZI.js → docs-2C2LTVT2.js} +32 -13
  144. package/dist/doctor-KI767GSN.js +87 -0
  145. package/dist/{eval-B3KZZESM.js → eval-XSSNATB4.js} +93 -27
  146. package/dist/{evidence-V67CHM35.js → evidence-UA6AWDQQ.js} +55 -27
  147. package/dist/{evolve-YDZSUQYA.js → evolve-QNTFGV6Z.js} +59 -26
  148. package/dist/{extensions-SRG7XCAH.js → extensions-QVDOHDGJ.js} +23 -8
  149. package/dist/{fleet-CA2CRTVG.js → fleet-Q7UOMUSG.js} +173 -56
  150. package/dist/fleet-preflight-DDN536IT.js +26 -0
  151. package/dist/{init-2OZDJE2D.js → init-WBB65ZHQ.js} +79 -34
  152. package/dist/{memory-3PIQQAKX.js → memory-MD3O64RI.js} +57 -28
  153. package/dist/{models-DY35XI7Y.js → models-BZU34YWD.js} +49 -24
  154. package/dist/monitor-MEQA5C3I.js +661 -0
  155. package/dist/{chunk-PS4PFJQP.js → orchestrator-CGFKEP27.js} +22837 -18564
  156. package/dist/{paths-5OMXW7Z4.js → paths-UXLN5YYZ.js} +17 -6
  157. package/dist/preload-P6DGH2PZ.js +13 -0
  158. package/dist/{reset-PIFYNOS3.js → reset-L2FQEE3E.js} +25 -13
  159. package/dist/{run-3VSPP24F.js → run-IV4Q6RLN.js} +115 -63
  160. package/dist/{share-D36RQCXM.js → share-S5BZQC5I.js} +23 -10
  161. package/dist/{skills-F2MRLELY.js → skills-LQEKRDTN.js} +42 -15
  162. package/dist/{skills-eval-E2ZTW4PL.js → skills-eval-3DC4HEWS.js} +90 -45
  163. package/dist/steer-GGWFUJUD.js +77 -0
  164. package/dist/{targets-DZMEZAH4.js → targets-C4SSGQOB.js} +78 -60
  165. package/dist/terminal-lease-IT5JW2NR.js +395 -0
  166. package/dist/{trace-7NYCUI2J.js → trace-PNCASAXC.js} +48 -17
  167. package/dist/tree-sitter-HGKH6LG4.js +4673 -0
  168. package/dist/{uninstall-AD3JWHBB.js → uninstall-FZCQCDKC.js} +19 -8
  169. package/dist/{upgrade-WYYBKGDY.js → upgrade-7TT7SQ3G.js} +66 -24
  170. package/dist/{usage-ULIDAGFF.js → usage-GV4PKT3M.js} +71 -32
  171. package/dist/verify-G6V4D2G7.js +716 -0
  172. package/dist/version-KGEG7EPO.js +21 -0
  173. package/dist/web-fetch-2YHJ3KTG.js +638 -0
  174. package/dist/{wiki-generate-PKFIX6OB.js → wiki-generate-DQF6Z66B.js} +98 -48
  175. package/dist/worker/entry.js +297 -54
  176. package/dist/workspace-G4ZWUIPR.js +22 -0
  177. package/docs/README.md +29 -21
  178. package/docs/acp.md +191 -16
  179. package/docs/alcf-provider.md +1 -1
  180. package/docs/architecture.md +157 -7
  181. package/docs/artifact-placement.md +101 -0
  182. package/docs/artifact-versions.md +1 -1
  183. package/docs/built-in-agents.md +4 -2
  184. package/docs/capacity-and-scheduling.md +1 -1
  185. package/docs/commands-and-modes.md +224 -97
  186. package/docs/config-knobs-audit.md +1 -3
  187. package/docs/configuration-and-targets.md +236 -38
  188. package/docs/context-engine.md +34 -7
  189. package/docs/development-pipeline.md +102 -0
  190. package/docs/dispatch-architecture-rationale.md +1 -1
  191. package/docs/documentation-coverage.md +4 -4
  192. package/docs/documentation-guide.md +3 -3
  193. package/docs/environment-variables.md +15 -6
  194. package/docs/eval-runner.md +3 -3
  195. package/docs/evals-internal.md +2 -2
  196. package/docs/evidence-and-memory.md +10 -2
  197. package/docs/evolution.md +2 -2
  198. package/docs/exit-codes-and-output.md +15 -9
  199. package/docs/extensions-and-sharing.md +25 -18
  200. package/docs/fleet-demo-runbook.md +22 -23
  201. package/docs/fleet-dispatch.md +117 -16
  202. package/docs/git-commit-provenance.md +120 -0
  203. package/docs/glossary.md +107 -15
  204. package/docs/installation-and-lifecycle.md +75 -17
  205. package/docs/middleware-and-components.md +1 -1
  206. package/docs/model-catalog.md +46 -15
  207. package/docs/observability.md +8 -5
  208. package/docs/performance-methodology.md +491 -0
  209. package/docs/pi-boundary.md +72 -0
  210. package/docs/proactive-memory.md +14 -14
  211. package/docs/prompt-envelope-and-tools.md +25 -4
  212. package/docs/provider-adapter-cookbook.md +57 -4
  213. package/docs/release-cut-checklist.md +129 -111
  214. package/docs/safety-model.md +28 -8
  215. package/docs/scientific-validation.md +3 -3
  216. package/docs/session-lifecycle.md +55 -12
  217. package/docs/skills-marketplace.md +12 -8
  218. package/docs/time-conventions.md +101 -0
  219. package/docs/tool-usage.md +6 -5
  220. package/docs/trace-store.md +1 -1
  221. package/docs/troubleshooting.md +10 -7
  222. package/docs/tui-design.md +121 -28
  223. package/docs/worker-dispatch-mechanics.md +3 -3
  224. package/package.json +33 -36
  225. package/skills/README.md +5 -6
  226. package/skills/coding/ast-grep/SKILL.md +136 -0
  227. package/skills/coding/ast-grep/evals.md +56 -0
  228. package/skills/coding/ast-grep/references/rule_reference.md +297 -0
  229. package/skills/coding/coding-standards/SKILL.md +113 -0
  230. package/skills/coding/coding-standards/evals.md +34 -0
  231. package/skills/coding/prototype/SKILL.md +86 -0
  232. package/skills/coding/prototype/evals.md +42 -0
  233. package/skills/coding/prototype/references/LOGIC.md +67 -0
  234. package/skills/coding/prototype/references/UI.md +112 -0
  235. package/skills/coding/tdd/SKILL.md +101 -0
  236. package/skills/coding/tdd/evals.md +41 -0
  237. package/skills/coding/tdd/references/mocking.md +59 -0
  238. package/skills/coding/tdd/references/tests.md +77 -0
  239. package/skills/context/context-handoff/SKILL.md +126 -0
  240. package/skills/context/context-handoff/evals.md +57 -0
  241. package/skills/context/context-handoff/scripts/new-handoff.sh +26 -0
  242. package/skills/context/context-prime/SKILL.md +95 -0
  243. package/skills/context/context-prime/evals.md +54 -0
  244. package/skills/git/file-ticket/SKILL.md +107 -0
  245. package/skills/git/file-ticket/evals.md +54 -0
  246. package/skills/git/fix-issue/SKILL.md +106 -0
  247. package/skills/git/fix-issue/evals.md +71 -0
  248. package/skills/git/ship/SKILL.md +92 -0
  249. package/skills/git/ship/evals.md +72 -0
  250. package/skills/meta/clio-dev/SKILL.md +91 -0
  251. package/skills/meta/clio-dev/evals.md +45 -0
  252. package/skills/meta/clio-test/SKILL.md +130 -0
  253. package/skills/meta/clio-test/evals.md +43 -0
  254. package/skills/meta/clio-test/references/harness.md +97 -0
  255. package/skills/meta/clio-test/references/test-map.md +59 -0
  256. package/skills/meta/credentials/SKILL.md +125 -0
  257. package/skills/meta/credentials/evals.md +104 -0
  258. package/skills/meta/find-skills/SKILL.md +72 -0
  259. package/skills/meta/find-skills/evals.md +47 -0
  260. package/skills/meta/herdr/SKILL.md +127 -0
  261. package/skills/meta/herdr/evals.md +38 -0
  262. package/skills/meta/skill-craft/SKILL.md +102 -0
  263. package/skills/meta/skill-craft/evals.md +41 -0
  264. package/skills/planning/architecture/SKILL.md +129 -0
  265. package/skills/planning/architecture/evals.md +36 -0
  266. package/skills/planning/backlog/SKILL.md +90 -0
  267. package/skills/planning/backlog/evals.md +43 -0
  268. package/skills/planning/prd/SKILL.md +82 -0
  269. package/skills/planning/prd/evals.md +49 -0
  270. package/skills/planning/product-intent/SKILL.md +112 -0
  271. package/skills/planning/product-intent/evals.md +36 -0
  272. package/skills/planning/tech-spec/SKILL.md +115 -0
  273. package/skills/planning/tech-spec/evals.md +47 -0
  274. package/skills/registry.yaml +136 -0
  275. package/skills/research/arxiv-literature/SKILL.md +104 -0
  276. package/skills/research/arxiv-literature/evals.md +58 -0
  277. package/skills/research/experiment-protocol/SKILL.md +122 -0
  278. package/skills/research/experiment-protocol/evals.md +91 -0
  279. package/skills/research/scientific-debugging/SKILL.md +119 -0
  280. package/skills/research/scientific-debugging/evals.md +138 -0
  281. package/skills/research/scientific-modernization/SKILL.md +138 -0
  282. package/skills/research/scientific-modernization/evals.md +84 -0
  283. package/skills/skill-marketplace.json +12 -20
  284. package/skills/workflow/design-council/SKILL.md +139 -0
  285. package/skills/workflow/design-council/evals.md +97 -0
  286. package/skills/workflow/grill-me/SKILL.md +186 -0
  287. package/skills/workflow/grill-me/evals.md +78 -0
  288. package/skills/workflow/workflow-distiller/SKILL.md +136 -0
  289. package/skills/workflow/workflow-distiller/evals.md +107 -0
  290. package/src/cli/acp.ts +115 -0
  291. package/src/cli/agents.ts +60 -0
  292. package/src/cli/args.ts +280 -0
  293. package/src/cli/argv.ts +170 -0
  294. package/src/cli/ask.ts +34 -0
  295. package/src/cli/auth.ts +468 -0
  296. package/src/cli/bootstrap-generate.ts +498 -0
  297. package/src/cli/clio.ts +96 -0
  298. package/src/cli/components.ts +195 -0
  299. package/src/cli/config-inspect.ts +336 -0
  300. package/src/cli/config.ts +72 -0
  301. package/src/cli/configure-interop.ts +67 -0
  302. package/src/cli/configure-layout.ts +172 -0
  303. package/src/cli/configure.ts +1525 -0
  304. package/src/cli/context-clear.ts +76 -0
  305. package/src/cli/context-index.ts +100 -0
  306. package/src/cli/context.ts +321 -0
  307. package/src/cli/default-target.ts +55 -0
  308. package/src/cli/docs.ts +346 -0
  309. package/src/cli/doctor.ts +54 -0
  310. package/src/cli/eval.ts +315 -0
  311. package/src/cli/evidence.ts +225 -0
  312. package/src/cli/evolve.ts +219 -0
  313. package/src/cli/extensions.ts +214 -0
  314. package/src/cli/fleet.ts +868 -0
  315. package/src/cli/index.ts +338 -0
  316. package/src/cli/init.ts +219 -0
  317. package/src/cli/initial-message.ts +47 -0
  318. package/src/cli/internal-dispatch.ts +55 -0
  319. package/src/cli/memory.ts +182 -0
  320. package/src/cli/models.ts +257 -0
  321. package/src/cli/modes/json-stream.ts +121 -0
  322. package/src/cli/modes/jsonl.ts +9 -0
  323. package/src/cli/modes/print.ts +604 -0
  324. package/src/cli/oauth-manual-input.ts +72 -0
  325. package/src/cli/oauth-select.ts +36 -0
  326. package/src/cli/output-guard.ts +48 -0
  327. package/src/cli/paths.ts +44 -0
  328. package/src/cli/provider-target.ts +113 -0
  329. package/src/cli/removal.ts +72 -0
  330. package/src/cli/reset.ts +253 -0
  331. package/src/cli/run.ts +497 -0
  332. package/src/cli/share.ts +243 -0
  333. package/src/cli/shared.ts +60 -0
  334. package/src/cli/skills-eval.ts +1240 -0
  335. package/src/cli/skills.ts +485 -0
  336. package/src/cli/steer-channel.ts +86 -0
  337. package/src/cli/targets.ts +1154 -0
  338. package/src/cli/text-layout.ts +112 -0
  339. package/src/cli/trace.ts +322 -0
  340. package/src/cli/uninstall.ts +405 -0
  341. package/src/cli/upgrade.ts +286 -0
  342. package/src/cli/usage.ts +876 -0
  343. package/src/cli/validate-model.ts +28 -0
  344. package/src/cli/version.ts +7 -0
  345. package/src/cli/wiki-generate.ts +510 -0
  346. package/src/core/agent-environment.ts +7 -0
  347. package/src/core/artifact-paths.ts +43 -0
  348. package/src/core/bash-exec.ts +313 -0
  349. package/src/core/boot-trace.ts +42 -0
  350. package/src/core/bus-events.ts +744 -0
  351. package/src/core/bus-trace.ts +53 -0
  352. package/src/core/c-header-language.ts +265 -0
  353. package/src/core/clio-repo.ts +78 -0
  354. package/src/core/commit-attribution.ts +157 -0
  355. package/src/core/compile-cache.ts +159 -0
  356. package/src/core/config.ts +1722 -0
  357. package/src/core/context-floor.ts +33 -0
  358. package/src/core/defaults.ts +659 -0
  359. package/src/core/domain-loader.ts +177 -0
  360. package/src/core/event-bus.ts +94 -0
  361. package/src/core/file-references.ts +282 -0
  362. package/src/core/git-commit-attribution.ts +362 -0
  363. package/src/core/guardrails.ts +185 -0
  364. package/src/core/headless-permission.ts +21 -0
  365. package/src/core/incomplete-installation.ts +45 -0
  366. package/src/core/init.ts +223 -0
  367. package/src/core/json-payload.ts +58 -0
  368. package/src/core/package-root.ts +54 -0
  369. package/src/core/path-canonical.ts +37 -0
  370. package/src/core/process-identity.ts +41 -0
  371. package/src/core/recent-models.ts +91 -0
  372. package/src/core/residency-protection.ts +56 -0
  373. package/src/core/resolve-config-value.ts +88 -0
  374. package/src/core/response-schema.ts +279 -0
  375. package/src/core/run-overrides.ts +77 -0
  376. package/src/core/safe-exec.ts +205 -0
  377. package/src/core/safe-id.ts +37 -0
  378. package/src/core/safe-resource-write.ts +162 -0
  379. package/src/core/session-routing.ts +339 -0
  380. package/src/core/settings-layers.ts +354 -0
  381. package/src/core/shared-bus.ts +8 -0
  382. package/src/core/shell-quote.ts +26 -0
  383. package/src/core/skill-activation.ts +190 -0
  384. package/src/core/startup-timer.ts +37 -0
  385. package/src/core/state-file-lock.ts +219 -0
  386. package/src/core/termination.ts +275 -0
  387. package/src/core/timers.ts +38 -0
  388. package/src/core/tool-names.ts +65 -0
  389. package/src/core/verification-scripts.ts +13 -0
  390. package/src/core/workspace-files.ts +492 -0
  391. package/src/core/xdg.ts +177 -0
  392. package/src/domains/agents/builtins/architect.md +4 -4
  393. package/src/domains/agents/builtins/coder.md +3 -2
  394. package/src/domains/agents/builtins/debugger.md +1 -1
  395. package/src/domains/agents/builtins/documenter.md +1 -1
  396. package/src/domains/agents/builtins/git-master.md +3 -3
  397. package/src/domains/agents/builtins/provenance.md +1 -1
  398. package/src/domains/agents/builtins/researcher.md +1 -1
  399. package/src/domains/agents/builtins/scout.md +1 -1
  400. package/src/domains/agents/builtins/tester.md +1 -1
  401. package/src/domains/agents/builtins/verifier.md +1 -1
  402. package/src/domains/agents/builtins/wiki-writer.md +1 -1
  403. package/src/domains/agents/catalog.ts +172 -0
  404. package/src/domains/agents/contract.ts +16 -0
  405. package/src/domains/agents/extension.ts +110 -0
  406. package/src/domains/agents/fleet-commands.ts +154 -0
  407. package/src/domains/agents/fleet-contract.ts +797 -0
  408. package/src/domains/agents/frontmatter.ts +40 -0
  409. package/src/domains/agents/index.ts +102 -0
  410. package/src/domains/agents/manifest.ts +6 -0
  411. package/src/domains/agents/recipe-schema.ts +202 -0
  412. package/src/domains/agents/recipe.ts +125 -0
  413. package/src/domains/agents/registry.ts +111 -0
  414. package/src/domains/agents/result-contract.ts +1265 -0
  415. package/src/domains/agents/spec.ts +371 -0
  416. package/src/domains/agents/write-boundary.ts +109 -0
  417. package/src/domains/components/diff.ts +99 -0
  418. package/src/domains/components/hash.ts +21 -0
  419. package/src/domains/components/index.ts +3 -0
  420. package/src/domains/components/scan.ts +304 -0
  421. package/src/domains/components/snapshot.ts +108 -0
  422. package/src/domains/components/types.ts +82 -0
  423. package/src/domains/config/agent-namespace.ts +13 -0
  424. package/src/domains/config/classify.ts +106 -0
  425. package/src/domains/config/contract.ts +22 -0
  426. package/src/domains/config/extension.ts +142 -0
  427. package/src/domains/config/index.ts +23 -0
  428. package/src/domains/config/keybindings.ts +200 -0
  429. package/src/domains/config/manifest.ts +6 -0
  430. package/src/domains/config/watcher.ts +41 -0
  431. package/src/domains/context/adoption.ts +880 -0
  432. package/src/domains/context/bootstrap-prompt.ts +273 -0
  433. package/src/domains/context/bootstrap.ts +1502 -0
  434. package/src/domains/context/clear.ts +91 -0
  435. package/src/domains/context/clio-md.ts +429 -0
  436. package/src/domains/context/codewiki/artifact.ts +400 -0
  437. package/src/domains/context/codewiki/build-worker-protocol.ts +24 -0
  438. package/src/domains/context/codewiki/build-worker.ts +54 -0
  439. package/src/domains/context/codewiki/cmake.ts +356 -0
  440. package/src/domains/context/codewiki/cooperative.ts +46 -0
  441. package/src/domains/context/codewiki/coordinator.ts +182 -0
  442. package/src/domains/context/codewiki/digest.ts +135 -0
  443. package/src/domains/context/codewiki/grammar-assets.ts +33 -0
  444. package/src/domains/context/codewiki/indexer.ts +1529 -0
  445. package/src/domains/context/codewiki/paths.ts +67 -0
  446. package/src/domains/context/codewiki/schema.ts +80 -0
  447. package/src/domains/context/codewiki/tree-sitter.ts +771 -0
  448. package/src/domains/context/contract.ts +68 -0
  449. package/src/domains/context/excluded-dirs.ts +1 -0
  450. package/src/domains/context/extension.ts +358 -0
  451. package/src/domains/context/fingerprint.ts +198 -0
  452. package/src/domains/context/index.ts +154 -0
  453. package/src/domains/context/init-options.ts +69 -0
  454. package/src/domains/context/manifest.ts +6 -0
  455. package/src/domains/context/operator-profile.ts +157 -0
  456. package/src/domains/context/project-metadata.ts +529 -0
  457. package/src/domains/context/project-rules.ts +227 -0
  458. package/src/domains/context/prompt-context.ts +61 -0
  459. package/src/domains/context/refresh.ts +178 -0
  460. package/src/domains/context/runtime.ts +17 -0
  461. package/src/domains/context/sibling-files.ts +10 -0
  462. package/src/domains/context/state.ts +237 -0
  463. package/src/domains/context/wiki/assemble.ts +303 -0
  464. package/src/domains/context/wiki/frontmatter.ts +210 -0
  465. package/src/domains/context/wiki/generate.ts +595 -0
  466. package/src/domains/context/wiki/layout.ts +111 -0
  467. package/src/domains/context/wiki/meta.ts +221 -0
  468. package/src/domains/context/wiki/plan-store.ts +217 -0
  469. package/src/domains/context/wiki/plan.ts +250 -0
  470. package/src/domains/context/wiki/prompts.ts +283 -0
  471. package/src/domains/context/wiki/staleness.ts +198 -0
  472. package/src/domains/dispatch/active-route-planner.ts +168 -0
  473. package/src/domains/dispatch/admission-queue.ts +150 -0
  474. package/src/domains/dispatch/admission.ts +334 -0
  475. package/src/domains/dispatch/agent-candidates.ts +307 -0
  476. package/src/domains/dispatch/agent-ledger-hub.ts +71 -0
  477. package/src/domains/dispatch/agent-ledger-store.ts +257 -0
  478. package/src/domains/dispatch/agent-ledger.ts +212 -0
  479. package/src/domains/dispatch/agent-plan-adapter.ts +85 -0
  480. package/src/domains/dispatch/assignment-events.ts +132 -0
  481. package/src/domains/dispatch/assignment-reconcile.ts +76 -0
  482. package/src/domains/dispatch/assignment-store.ts +162 -0
  483. package/src/domains/dispatch/assignment.ts +278 -0
  484. package/src/domains/dispatch/backoff.ts +59 -0
  485. package/src/domains/dispatch/batch-store.ts +134 -0
  486. package/src/domains/dispatch/batch-tracker.ts +66 -0
  487. package/src/domains/dispatch/capability-match.ts +155 -0
  488. package/src/domains/dispatch/capacity-lease.ts +413 -0
  489. package/src/domains/dispatch/code-step-store.ts +52 -0
  490. package/src/domains/dispatch/code-step.ts +387 -0
  491. package/src/domains/dispatch/contract.ts +278 -0
  492. package/src/domains/dispatch/dedup.ts +133 -0
  493. package/src/domains/dispatch/event-pump.ts +251 -0
  494. package/src/domains/dispatch/execution-handoff.ts +29 -0
  495. package/src/domains/dispatch/execution-plan.ts +328 -0
  496. package/src/domains/dispatch/execution-role.ts +277 -0
  497. package/src/domains/dispatch/execution-scheduler.ts +583 -0
  498. package/src/domains/dispatch/extension.ts +6006 -0
  499. package/src/domains/dispatch/failure-classification.ts +147 -0
  500. package/src/domains/dispatch/fleet-commit-attribution.ts +56 -0
  501. package/src/domains/dispatch/fleet-plan.ts +225 -0
  502. package/src/domains/dispatch/fleet-preflight.ts +394 -0
  503. package/src/domains/dispatch/gate-decisions.ts +1040 -0
  504. package/src/domains/dispatch/gate-role-prompts.ts +58 -0
  505. package/src/domains/dispatch/heartbeat.ts +27 -0
  506. package/src/domains/dispatch/index.ts +137 -0
  507. package/src/domains/dispatch/joint-route-adapter.ts +114 -0
  508. package/src/domains/dispatch/joint-route-resolver.ts +283 -0
  509. package/src/domains/dispatch/manifest.ts +6 -0
  510. package/src/domains/dispatch/orphan-recovery.ts +266 -0
  511. package/src/domains/dispatch/outcome.ts +109 -0
  512. package/src/domains/dispatch/phase-timing.ts +47 -0
  513. package/src/domains/dispatch/placement.ts +202 -0
  514. package/src/domains/dispatch/receipt-findings.ts +255 -0
  515. package/src/domains/dispatch/receipt-integrity.ts +383 -0
  516. package/src/domains/dispatch/recovery-candidates.ts +74 -0
  517. package/src/domains/dispatch/reproducibility.ts +48 -0
  518. package/src/domains/dispatch/reservation-store.ts +501 -0
  519. package/src/domains/dispatch/route-approval.ts +177 -0
  520. package/src/domains/dispatch/route-candidates.ts +57 -0
  521. package/src/domains/dispatch/route-decision.ts +1079 -0
  522. package/src/domains/dispatch/route-facts.ts +187 -0
  523. package/src/domains/dispatch/route-history.ts +238 -0
  524. package/src/domains/dispatch/route-observer.ts +488 -0
  525. package/src/domains/dispatch/route-policy.ts +327 -0
  526. package/src/domains/dispatch/route-quality.ts +256 -0
  527. package/src/domains/dispatch/route-readiness.ts +120 -0
  528. package/src/domains/dispatch/routing-intent.ts +287 -0
  529. package/src/domains/dispatch/run-identity.ts +56 -0
  530. package/src/domains/dispatch/scout-transition.ts +130 -0
  531. package/src/domains/dispatch/state.ts +302 -0
  532. package/src/domains/dispatch/tool-stats.ts +132 -0
  533. package/src/domains/dispatch/transport.ts +210 -0
  534. package/src/domains/dispatch/types.ts +799 -0
  535. package/src/domains/dispatch/validation-grounding.ts +154 -0
  536. package/src/domains/dispatch/validation.ts +721 -0
  537. package/src/domains/dispatch/worker-protocol.ts +275 -0
  538. package/src/domains/dispatch/worker-spawn.ts +585 -0
  539. package/src/domains/dispatch/write-boundary-enforcer.ts +97 -0
  540. package/src/domains/dispatch/write-boundary.ts +426 -0
  541. package/src/domains/eval/artifacts/redact.ts +42 -0
  542. package/src/domains/eval/artifacts/store.ts +173 -0
  543. package/src/domains/eval/compare/compare.ts +33 -0
  544. package/src/domains/eval/compare/gates.ts +66 -0
  545. package/src/domains/eval/compare/thresholds.ts +92 -0
  546. package/src/domains/eval/harness-metrics.ts +62 -0
  547. package/src/domains/eval/index.ts +66 -0
  548. package/src/domains/eval/metrics/chaos-stream.ts +93 -0
  549. package/src/domains/eval/metrics/context.ts +34 -0
  550. package/src/domains/eval/metrics/coverage.ts +21 -0
  551. package/src/domains/eval/metrics/evidence.ts +182 -0
  552. package/src/domains/eval/metrics/fleet-loop-stream.ts +196 -0
  553. package/src/domains/eval/metrics/invariants.ts +736 -0
  554. package/src/domains/eval/metrics/latency.ts +4 -0
  555. package/src/domains/eval/metrics/token-stream.ts +139 -0
  556. package/src/domains/eval/metrics/tokens.ts +51 -0
  557. package/src/domains/eval/metrics/tool-calls.ts +7 -0
  558. package/src/domains/eval/proactive-memory.ts +279 -0
  559. package/src/domains/eval/provenance.ts +35 -0
  560. package/src/domains/eval/report.ts +234 -0
  561. package/src/domains/eval/reports/json.ts +5 -0
  562. package/src/domains/eval/reports/junit.ts +23 -0
  563. package/src/domains/eval/reports/markdown.ts +20 -0
  564. package/src/domains/eval/reports/swe-jsonl.ts +16 -0
  565. package/src/domains/eval/reports/text.ts +25 -0
  566. package/src/domains/eval/run-compare.ts +274 -0
  567. package/src/domains/eval/runner.ts +244 -0
  568. package/src/domains/eval/runners/clio-run.ts +148 -0
  569. package/src/domains/eval/runners/context-index.ts +56 -0
  570. package/src/domains/eval/runners/context-init.ts +203 -0
  571. package/src/domains/eval/runners/external-command.ts +350 -0
  572. package/src/domains/eval/runtime-paths.ts +171 -0
  573. package/src/domains/eval/schema/artifact.ts +75 -0
  574. package/src/domains/eval/schema/suite.ts +235 -0
  575. package/src/domains/eval/schema/validate.ts +339 -0
  576. package/src/domains/eval/store.ts +392 -0
  577. package/src/domains/eval/suites/load.ts +74 -0
  578. package/src/domains/eval/suites/matrix.ts +19 -0
  579. package/src/domains/eval/suites/resolve.ts +44 -0
  580. package/src/domains/eval/suites/run.ts +325 -0
  581. package/src/domains/eval/task-file.ts +186 -0
  582. package/src/domains/eval/types.ts +140 -0
  583. package/src/domains/eval/verifiers/command.ts +27 -0
  584. package/src/domains/eval/verifiers/file-exists.ts +6 -0
  585. package/src/domains/eval/verifiers/patch.ts +22 -0
  586. package/src/domains/eval/workspaces/git.ts +41 -0
  587. package/src/domains/eval/workspaces/local.ts +17 -0
  588. package/src/domains/eval/workspaces/temp-copy.ts +29 -0
  589. package/src/domains/evidence/build.ts +1754 -0
  590. package/src/domains/evidence/eval.ts +605 -0
  591. package/src/domains/evidence/failure-attribution.ts +54 -0
  592. package/src/domains/evidence/finish-contract-map.ts +42 -0
  593. package/src/domains/evidence/index.ts +55 -0
  594. package/src/domains/evidence/provenance.ts +200 -0
  595. package/src/domains/evidence/redact.ts +81 -0
  596. package/src/domains/evidence/store.ts +291 -0
  597. package/src/domains/evidence/types.ts +325 -0
  598. package/src/domains/evolution/SELF_EDIT_GATE.md +91 -0
  599. package/src/domains/evolution/index.ts +26 -0
  600. package/src/domains/evolution/manifest.ts +154 -0
  601. package/src/domains/evolution/validate.ts +304 -0
  602. package/src/domains/extensions/contract.ts +22 -0
  603. package/src/domains/extensions/discovery.ts +193 -0
  604. package/src/domains/extensions/extension.ts +46 -0
  605. package/src/domains/extensions/index.ts +34 -0
  606. package/src/domains/extensions/manager.ts +30 -0
  607. package/src/domains/extensions/manifest.ts +6 -0
  608. package/src/domains/extensions/resources.ts +38 -0
  609. package/src/domains/extensions/state.ts +224 -0
  610. package/src/domains/extensions/types.ts +83 -0
  611. package/src/domains/interop/consent.ts +185 -0
  612. package/src/domains/interop/contract.ts +29 -0
  613. package/src/domains/interop/detect.ts +173 -0
  614. package/src/domains/interop/extension.ts +59 -0
  615. package/src/domains/interop/index.ts +34 -0
  616. package/src/domains/interop/manifest.ts +6 -0
  617. package/src/domains/interop/registry.ts +139 -0
  618. package/src/domains/interop/state.ts +92 -0
  619. package/src/domains/interop/types.ts +109 -0
  620. package/src/domains/lifecycle/contract.ts +11 -0
  621. package/src/domains/lifecycle/doctor.ts +536 -0
  622. package/src/domains/lifecycle/extension.ts +23 -0
  623. package/src/domains/lifecycle/index.ts +21 -0
  624. package/src/domains/lifecycle/install-method.ts +51 -0
  625. package/src/domains/lifecycle/manifest.ts +6 -0
  626. package/src/domains/lifecycle/migrations/2026-08-18-lmstudio-runtime-id.ts +52 -0
  627. package/src/domains/lifecycle/migrations/index.ts +114 -0
  628. package/src/domains/lifecycle/state.ts +93 -0
  629. package/src/domains/lifecycle/upgrade-notice.ts +35 -0
  630. package/src/domains/lifecycle/version.ts +35 -0
  631. package/src/domains/memory/index.ts +128 -0
  632. package/src/domains/memory/operations.ts +143 -0
  633. package/src/domains/memory/prompt-section.ts +86 -0
  634. package/src/domains/memory/proposal.ts +180 -0
  635. package/src/domains/memory/store.ts +163 -0
  636. package/src/domains/memory/task-bank.ts +229 -0
  637. package/src/domains/memory/task-memory-handoff.ts +222 -0
  638. package/src/domains/memory/task-memory-policy.ts +602 -0
  639. package/src/domains/memory/task-memory-status.ts +58 -0
  640. package/src/domains/memory/task-memory-telemetry.ts +346 -0
  641. package/src/domains/memory/task-memory-trace.ts +89 -0
  642. package/src/domains/memory/types.ts +74 -0
  643. package/src/domains/memory/validate.ts +297 -0
  644. package/src/domains/middleware/budget.ts +216 -0
  645. package/src/domains/middleware/contract.ts +35 -0
  646. package/src/domains/middleware/dispatch-nudge.ts +469 -0
  647. package/src/domains/middleware/extension.ts +129 -0
  648. package/src/domains/middleware/hook-receipts.ts +70 -0
  649. package/src/domains/middleware/hooks-io.ts +186 -0
  650. package/src/domains/middleware/hooks.ts +432 -0
  651. package/src/domains/middleware/index.ts +154 -0
  652. package/src/domains/middleware/manifest.ts +6 -0
  653. package/src/domains/middleware/memory-intervention.ts +701 -0
  654. package/src/domains/middleware/rules.ts +42 -0
  655. package/src/domains/middleware/runtime.ts +333 -0
  656. package/src/domains/middleware/skills-reminder.ts +202 -0
  657. package/src/domains/middleware/snapshot.ts +71 -0
  658. package/src/domains/middleware/stalled-turn.ts +71 -0
  659. package/src/domains/middleware/task-board-reminder.ts +87 -0
  660. package/src/domains/middleware/task-nudge.ts +77 -0
  661. package/src/domains/middleware/tool-choice-control.ts +42 -0
  662. package/src/domains/middleware/types.ts +131 -0
  663. package/src/domains/middleware/validate.ts +329 -0
  664. package/src/domains/observability/accountability.ts +67 -0
  665. package/src/domains/observability/contract.ts +168 -0
  666. package/src/domains/observability/cost.ts +206 -0
  667. package/src/domains/observability/evidence-index.ts +123 -0
  668. package/src/domains/observability/extension.ts +281 -0
  669. package/src/domains/observability/index.ts +78 -0
  670. package/src/domains/observability/manifest.ts +10 -0
  671. package/src/domains/observability/metrics.ts +37 -0
  672. package/src/domains/observability/projection.ts +477 -0
  673. package/src/domains/observability/telemetry.ts +89 -0
  674. package/src/domains/observability/trace-store.ts +1454 -0
  675. package/src/domains/prompts/compiler.ts +614 -0
  676. package/src/domains/prompts/contract.ts +46 -0
  677. package/src/domains/prompts/extension.ts +336 -0
  678. package/src/domains/prompts/fragment-loader.ts +138 -0
  679. package/src/domains/prompts/fragments/identity/clio.md +5 -6
  680. package/src/domains/prompts/fragments/identity/docs-routing.md +10 -0
  681. package/src/domains/prompts/fragments/identity/self-awareness.md +19 -0
  682. package/src/domains/prompts/fragments/operating/contract.md +4 -41
  683. package/src/domains/prompts/fragments/operating/delegation.md +42 -0
  684. package/src/domains/prompts/fragments/operating/skills.md +26 -0
  685. package/src/domains/prompts/fragments/operating/worker.md +16 -0
  686. package/src/domains/prompts/fragments/safety/auto-edit.md +5 -5
  687. package/src/domains/prompts/fragments/safety/full-auto.md +3 -3
  688. package/src/domains/prompts/fragments/safety/read-only.md +4 -4
  689. package/src/domains/prompts/fragments/safety/suggest.md +2 -2
  690. package/src/domains/prompts/fragments/wiki/page.md +10 -0
  691. package/src/domains/prompts/fragments/wiki/plan.md +10 -0
  692. package/src/domains/prompts/hash.ts +73 -0
  693. package/src/domains/prompts/index.ts +32 -0
  694. package/src/domains/prompts/manifest.ts +8 -0
  695. package/src/domains/prompts/memory-intervention.ts +57 -0
  696. package/src/domains/prompts/preload.ts +82 -0
  697. package/src/domains/providers/auth/api-key.ts +47 -0
  698. package/src/domains/providers/auth/backend-file.ts +79 -0
  699. package/src/domains/providers/auth/backend-memory.ts +72 -0
  700. package/src/domains/providers/auth/boot-status.ts +65 -0
  701. package/src/domains/providers/auth/index.ts +37 -0
  702. package/src/domains/providers/auth/oauth.ts +37 -0
  703. package/src/domains/providers/auth/storage.ts +726 -0
  704. package/src/domains/providers/capabilities.ts +58 -0
  705. package/src/domains/providers/catalog.ts +162 -0
  706. package/src/domains/providers/contract.ts +162 -0
  707. package/src/domains/providers/credentials.ts +20 -0
  708. package/src/domains/providers/eligibility.ts +24 -0
  709. package/src/domains/providers/errors.ts +64 -0
  710. package/src/domains/providers/extension.ts +548 -0
  711. package/src/domains/providers/index.ts +142 -0
  712. package/src/domains/providers/knowledge-base-path.ts +102 -0
  713. package/src/domains/providers/manifest.ts +6 -0
  714. package/src/domains/providers/model-capabilities.ts +136 -0
  715. package/src/domains/providers/model-discovery.ts +144 -0
  716. package/src/domains/providers/model-family.ts +32 -0
  717. package/src/domains/providers/model-runtime-capabilities.ts +723 -0
  718. package/src/domains/providers/models/local-models/clio-local-coding-targets.yaml +120 -37
  719. package/src/domains/providers/plugins.ts +61 -0
  720. package/src/domains/providers/probe/fingerprint.ts +64 -0
  721. package/src/domains/providers/probe/http.ts +114 -0
  722. package/src/domains/providers/probe/reasoning.ts +111 -0
  723. package/src/domains/providers/registry.ts +187 -0
  724. package/src/domains/providers/resolver.ts +156 -0
  725. package/src/domains/providers/runtime-resolution.ts +720 -0
  726. package/src/domains/providers/runtimes/antigravity/antigravity-code.ts +90 -0
  727. package/src/domains/providers/runtimes/boot-manifest.ts +55 -0
  728. package/src/domains/providers/runtimes/builtins.ts +78 -0
  729. package/src/domains/providers/runtimes/claude/claude-code.ts +38 -0
  730. package/src/domains/providers/runtimes/claude/claude-sdk.ts +38 -0
  731. package/src/domains/providers/runtimes/claude/common.ts +55 -0
  732. package/src/domains/providers/runtimes/cloud/alcf.ts +171 -0
  733. package/src/domains/providers/runtimes/cloud/anthropic-max.ts +71 -0
  734. package/src/domains/providers/runtimes/cloud/anthropic.ts +43 -0
  735. package/src/domains/providers/runtimes/cloud/bedrock.ts +35 -0
  736. package/src/domains/providers/runtimes/cloud/deepseek.ts +50 -0
  737. package/src/domains/providers/runtimes/cloud/google.ts +41 -0
  738. package/src/domains/providers/runtimes/cloud/groq.ts +46 -0
  739. package/src/domains/providers/runtimes/cloud/mistral.ts +46 -0
  740. package/src/domains/providers/runtimes/cloud/openai-codex.ts +49 -0
  741. package/src/domains/providers/runtimes/cloud/openai.ts +46 -0
  742. package/src/domains/providers/runtimes/cloud/openrouter.ts +111 -0
  743. package/src/domains/providers/runtimes/common/lmstudio-http.ts +423 -0
  744. package/src/domains/providers/runtimes/common/local-synth.ts +164 -0
  745. package/src/domains/providers/runtimes/common/probe-helpers.ts +474 -0
  746. package/src/domains/providers/runtimes/local-native/lemonade-anthropic.ts +27 -0
  747. package/src/domains/providers/runtimes/local-native/lemonade-openai.ts +26 -0
  748. package/src/domains/providers/runtimes/local-native/llamacpp-anthropic.ts +88 -0
  749. package/src/domains/providers/runtimes/local-native/llamacpp-completion.ts +207 -0
  750. package/src/domains/providers/runtimes/local-native/llamacpp-embed.ts +180 -0
  751. package/src/domains/providers/runtimes/local-native/llamacpp-rerank.ts +126 -0
  752. package/src/domains/providers/runtimes/local-native/llamacpp.ts +98 -0
  753. package/src/domains/providers/runtimes/local-native/lmstudio.ts +241 -0
  754. package/src/domains/providers/runtimes/local-native/ollama-native.ts +117 -0
  755. package/src/domains/providers/runtimes/local-native/sglang.ts +26 -0
  756. package/src/domains/providers/runtimes/local-native/vllm.ts +27 -0
  757. package/src/domains/providers/runtimes/protocol/anthropic-compat.ts +104 -0
  758. package/src/domains/providers/runtimes/protocol/anthropic-messages.ts +28 -0
  759. package/src/domains/providers/runtimes/protocol/bedrock.ts +26 -0
  760. package/src/domains/providers/runtimes/protocol/google.ts +27 -0
  761. package/src/domains/providers/runtimes/protocol/openai-compat.ts +145 -0
  762. package/src/domains/providers/support.ts +241 -0
  763. package/src/domains/providers/thinking-control-policy.ts +85 -0
  764. package/src/domains/providers/types/capability-flags.ts +114 -0
  765. package/src/domains/providers/types/cost-provenance.ts +7 -0
  766. package/src/domains/providers/types/inference.ts +41 -0
  767. package/src/domains/providers/types/knowledge-base.ts +156 -0
  768. package/src/domains/providers/types/local-model-quirks.ts +216 -0
  769. package/src/domains/providers/types/runtime-descriptor.ts +181 -0
  770. package/src/domains/providers/types/target-descriptor.ts +60 -0
  771. package/src/domains/resources/collision.ts +92 -0
  772. package/src/domains/resources/common-loader.ts +140 -0
  773. package/src/domains/resources/contract.ts +24 -0
  774. package/src/domains/resources/extension.ts +59 -0
  775. package/src/domains/resources/index.ts +74 -0
  776. package/src/domains/resources/loader.ts +85 -0
  777. package/src/domains/resources/manifest.ts +6 -0
  778. package/src/domains/resources/prompts/loader.ts +246 -0
  779. package/src/domains/resources/prompts/substitute.ts +1 -0
  780. package/src/domains/resources/skills/content-hash.ts +107 -0
  781. package/src/domains/resources/skills/evals.ts +175 -0
  782. package/src/domains/resources/skills/install.ts +427 -0
  783. package/src/domains/resources/skills/loader.ts +985 -0
  784. package/src/domains/resources/skills/marketplace.ts +301 -0
  785. package/src/domains/resources/skills/provenance-pin.ts +115 -0
  786. package/src/domains/safety/action-classifier.ts +270 -0
  787. package/src/domains/safety/approval-axis.ts +23 -0
  788. package/src/domains/safety/audit.ts +493 -0
  789. package/src/domains/safety/autonomy.ts +174 -0
  790. package/src/domains/safety/call-target.ts +59 -0
  791. package/src/domains/safety/contract.ts +65 -0
  792. package/src/domains/safety/damage-control.ts +44 -0
  793. package/src/domains/safety/default-path-policy.ts +117 -0
  794. package/src/domains/safety/extension.ts +383 -0
  795. package/src/domains/safety/finish-contract-registration.ts +128 -0
  796. package/src/domains/safety/finish-contract.ts +554 -0
  797. package/src/domains/safety/index.ts +44 -0
  798. package/src/domains/safety/loop-detector.ts +143 -0
  799. package/src/domains/safety/manifest.ts +6 -0
  800. package/src/domains/safety/path-policy.ts +164 -0
  801. package/src/domains/safety/policy-engine.ts +994 -0
  802. package/src/domains/safety/project-policy.ts +391 -0
  803. package/src/domains/safety/protected-artifacts-registration.ts +189 -0
  804. package/src/domains/safety/protected-artifacts.ts +993 -0
  805. package/src/domains/safety/rejection-feedback.ts +102 -0
  806. package/src/domains/safety/rigor.ts +72 -0
  807. package/src/domains/safety/rule-compiler.ts +48 -0
  808. package/src/domains/safety/rule-pack-loader.ts +92 -0
  809. package/src/domains/safety/run-effects.ts +197 -0
  810. package/src/domains/safety/scope.ts +61 -0
  811. package/src/domains/scheduling/budget.ts +35 -0
  812. package/src/domains/scheduling/cluster.ts +175 -0
  813. package/src/domains/scheduling/contract.ts +30 -0
  814. package/src/domains/scheduling/extension.ts +91 -0
  815. package/src/domains/scheduling/index.ts +12 -0
  816. package/src/domains/scheduling/manifest.ts +6 -0
  817. package/src/domains/session/archive-readers.ts +208 -0
  818. package/src/domains/session/checkpoint.ts +38 -0
  819. package/src/domains/session/compaction/auto.ts +89 -0
  820. package/src/domains/session/compaction/branch-summary.ts +187 -0
  821. package/src/domains/session/compaction/compact.ts +493 -0
  822. package/src/domains/session/compaction/cut-point.ts +172 -0
  823. package/src/domains/session/compaction/defaults.ts +34 -0
  824. package/src/domains/session/compaction/mask-observations.ts +265 -0
  825. package/src/domains/session/compaction/session-entries.ts +18 -0
  826. package/src/domains/session/compaction/tokens.ts +148 -0
  827. package/src/domains/session/context-accounting.ts +583 -0
  828. package/src/domains/session/context-ledger.ts +362 -0
  829. package/src/domains/session/contract.ts +159 -0
  830. package/src/domains/session/cwd-fallback.ts +65 -0
  831. package/src/domains/session/decision-board.ts +190 -0
  832. package/src/domains/session/entries.ts +572 -0
  833. package/src/domains/session/extension.ts +403 -0
  834. package/src/domains/session/history.ts +172 -0
  835. package/src/domains/session/index.ts +84 -0
  836. package/src/domains/session/manager.ts +128 -0
  837. package/src/domains/session/manifest.ts +6 -0
  838. package/src/domains/session/migrations/index.ts +27 -0
  839. package/src/domains/session/prompt-manifest.ts +173 -0
  840. package/src/domains/session/protected-artifact-journal.ts +241 -0
  841. package/src/domains/session/protected-artifacts.ts +90 -0
  842. package/src/domains/session/retry.ts +230 -0
  843. package/src/domains/session/session-artifacts.ts +107 -0
  844. package/src/domains/session/task-board.ts +569 -0
  845. package/src/domains/session/tree/active-path.ts +137 -0
  846. package/src/domains/session/tree/fork.ts +159 -0
  847. package/src/domains/session/tree/manager.ts +128 -0
  848. package/src/domains/session/tree/navigator.ts +177 -0
  849. package/src/domains/session/tree/preview.ts +259 -0
  850. package/src/domains/session/usage.ts +148 -0
  851. package/src/domains/session/workspace/git-probe.ts +167 -0
  852. package/src/domains/session/workspace/index.ts +4 -0
  853. package/src/domains/session/workspace/project-type.ts +217 -0
  854. package/src/domains/session/workspace/snapshot.ts +74 -0
  855. package/src/domains/share/archive.ts +684 -0
  856. package/src/domains/share/contract.ts +9 -0
  857. package/src/domains/share/extension.ts +26 -0
  858. package/src/domains/share/index.ts +28 -0
  859. package/src/domains/share/manifest.ts +6 -0
  860. package/src/domains/user-tasks/store.ts +297 -0
  861. package/src/engine/acp/adapter.ts +397 -0
  862. package/src/engine/acp/errors.ts +128 -0
  863. package/src/engine/acp/event-mapper.ts +154 -0
  864. package/src/engine/acp/server.ts +2238 -0
  865. package/src/engine/acp/tool-mediator.ts +686 -0
  866. package/src/engine/acp/transport.ts +775 -0
  867. package/src/engine/acp/types.ts +196 -0
  868. package/src/engine/agent.ts +29 -0
  869. package/src/engine/ai.ts +207 -0
  870. package/src/engine/alcf-oauth.ts +205 -0
  871. package/src/engine/antigravity/subprocess-runtime.ts +205 -0
  872. package/src/engine/api-registry.ts +223 -0
  873. package/src/engine/apis/degraded-inference.ts +95 -0
  874. package/src/engine/apis/index.ts +16 -0
  875. package/src/engine/apis/llamacpp-residency.ts +292 -0
  876. package/src/engine/apis/lmstudio-residency.ts +76 -0
  877. package/src/engine/apis/lmstudio.ts +243 -0
  878. package/src/engine/apis/ollama-native.ts +528 -0
  879. package/src/engine/apis/openai-completions.ts +795 -0
  880. package/src/engine/apis/output-budget.ts +99 -0
  881. package/src/engine/apis/residency-lock.ts +29 -0
  882. package/src/engine/apis/residency.ts +776 -0
  883. package/src/engine/apis/resident-models.ts +42 -0
  884. package/src/engine/apis/sampling-overrides.ts +38 -0
  885. package/src/engine/apis/types.ts +16 -0
  886. package/src/engine/claude/sdk-runtime.ts +685 -0
  887. package/src/engine/claude/session-id.ts +5 -0
  888. package/src/engine/claude/subprocess-runtime.ts +316 -0
  889. package/src/engine/claude/tool-safety.ts +400 -0
  890. package/src/engine/env-api-keys.ts +98 -0
  891. package/src/engine/external-subprocess.ts +23 -0
  892. package/src/engine/gemma-channel-filter.ts +223 -0
  893. package/src/engine/harmony-response.ts +165 -0
  894. package/src/engine/instrumented-tui.ts +192 -0
  895. package/src/engine/loop-guard.ts +1127 -0
  896. package/src/engine/messages.ts +14 -0
  897. package/src/engine/models.ts +42 -0
  898. package/src/engine/oauth.ts +196 -0
  899. package/src/engine/pi-mono-names.ts +13 -0
  900. package/src/engine/prompt-templates.ts +1 -0
  901. package/src/engine/provider-payload.ts +272 -0
  902. package/src/engine/select-list.ts +53 -0
  903. package/src/engine/session.ts +834 -0
  904. package/src/engine/strip-tokenizer-sentinels.ts +125 -0
  905. package/src/engine/truncate.ts +9 -0
  906. package/src/engine/tui.ts +111 -0
  907. package/src/engine/types.ts +63 -0
  908. package/src/engine/worker-events.ts +100 -0
  909. package/src/engine/worker-runtime-capabilities.ts +5 -0
  910. package/src/engine/worker-runtime.ts +1022 -0
  911. package/src/engine/worker-tools.ts +208 -0
  912. package/src/entry/boot-options.ts +50 -0
  913. package/src/entry/orchestrator.ts +2011 -0
  914. package/src/interactive/application-controller.ts +433 -0
  915. package/src/interactive/bus-notices.ts +265 -0
  916. package/src/interactive/chat-loop-messages.ts +626 -0
  917. package/src/interactive/chat-loop-policy.ts +18 -0
  918. package/src/interactive/chat-loop.ts +1090 -0
  919. package/src/interactive/chat-panel.ts +1820 -0
  920. package/src/interactive/chat-renderer.ts +1228 -0
  921. package/src/interactive/clio-editor.ts +161 -0
  922. package/src/interactive/command-fallbacks.ts +23 -0
  923. package/src/interactive/command-output.ts +85 -0
  924. package/src/interactive/context-activity.ts +203 -0
  925. package/src/interactive/context-meter.ts +223 -0
  926. package/src/interactive/context-overlay.ts +246 -0
  927. package/src/interactive/cost-overlay.ts +279 -0
  928. package/src/interactive/dispatch-board.ts +1319 -0
  929. package/src/interactive/dispatch-steering.ts +86 -0
  930. package/src/interactive/editor-bash.ts +46 -0
  931. package/src/interactive/editor-steer.ts +51 -0
  932. package/src/interactive/editor-submit.ts +507 -0
  933. package/src/interactive/export-html/ansi-to-html.ts +161 -0
  934. package/src/interactive/export-html/index.ts +51 -0
  935. package/src/interactive/export-html/template.ts +45 -0
  936. package/src/interactive/export-html/tool-renderer.ts +54 -0
  937. package/src/interactive/external-editor.ts +67 -0
  938. package/src/interactive/follow-up-queue-panel.ts +75 -0
  939. package/src/interactive/footer/dashboard.ts +538 -0
  940. package/src/interactive/footer/notifications.ts +275 -0
  941. package/src/interactive/footer/widgets.ts +1264 -0
  942. package/src/interactive/footer-panel.ts +187 -0
  943. package/src/interactive/format-time.ts +97 -0
  944. package/src/interactive/index.ts +11 -0
  945. package/src/interactive/interactive-application.ts +939 -0
  946. package/src/interactive/interactive-event-projection.ts +385 -0
  947. package/src/interactive/interactive-input-runtime.ts +307 -0
  948. package/src/interactive/interactive-presentation.ts +584 -0
  949. package/src/interactive/interactive-shell.ts +388 -0
  950. package/src/interactive/interactive-slash-runtime.ts +554 -0
  951. package/src/interactive/interactive-subscriptions.ts +111 -0
  952. package/src/interactive/interactive-tickers.ts +183 -0
  953. package/src/interactive/keybinding-manager.ts +496 -0
  954. package/src/interactive/layout.ts +57 -0
  955. package/src/interactive/leader-key.ts +143 -0
  956. package/src/interactive/loop-guard-interrupt.ts +101 -0
  957. package/src/interactive/memory-overlay.ts +280 -0
  958. package/src/interactive/overlay-ask-user-lifecycle.ts +110 -0
  959. package/src/interactive/overlay-auth-lifecycle.ts +308 -0
  960. package/src/interactive/overlay-frame.ts +502 -0
  961. package/src/interactive/overlay-general-openers.ts +274 -0
  962. package/src/interactive/overlay-key-routing.ts +216 -0
  963. package/src/interactive/overlay-lifecycle.ts +426 -0
  964. package/src/interactive/overlay-model-selectors.ts +127 -0
  965. package/src/interactive/overlay-permission-lifecycle.ts +345 -0
  966. package/src/interactive/overlay-resource-openers.ts +112 -0
  967. package/src/interactive/overlay-session-lifecycle.ts +327 -0
  968. package/src/interactive/overlay-transitions.ts +57 -0
  969. package/src/interactive/overlays/agents.ts +69 -0
  970. package/src/interactive/overlays/ask-user.ts +921 -0
  971. package/src/interactive/overlays/auth-dialog.ts +221 -0
  972. package/src/interactive/overlays/context-reset.ts +108 -0
  973. package/src/interactive/overlays/cwd-fallback.ts +141 -0
  974. package/src/interactive/overlays/decisions.ts +300 -0
  975. package/src/interactive/overlays/extensions.ts +54 -0
  976. package/src/interactive/overlays/help-reference.ts +145 -0
  977. package/src/interactive/overlays/interop.ts +139 -0
  978. package/src/interactive/overlays/keybinding-detail.ts +90 -0
  979. package/src/interactive/overlays/list-overlay.ts +606 -0
  980. package/src/interactive/overlays/message-picker.ts +155 -0
  981. package/src/interactive/overlays/model-selector.ts +1109 -0
  982. package/src/interactive/overlays/prompts.ts +72 -0
  983. package/src/interactive/overlays/session-selector-search.ts +31 -0
  984. package/src/interactive/overlays/session-selector.ts +284 -0
  985. package/src/interactive/overlays/settings.ts +3451 -0
  986. package/src/interactive/overlays/skills-hub.ts +237 -0
  987. package/src/interactive/overlays/tree-selector.ts +422 -0
  988. package/src/interactive/permission-overlay.ts +170 -0
  989. package/src/interactive/render-trace.ts +534 -0
  990. package/src/interactive/renderers/branch-summary.ts +86 -0
  991. package/src/interactive/renderers/code-ink.ts +420 -0
  992. package/src/interactive/renderers/compaction-summary.ts +112 -0
  993. package/src/interactive/renderers/diff.ts +137 -0
  994. package/src/interactive/renderers/mermaid.ts +53 -0
  995. package/src/interactive/renderers/notice.ts +28 -0
  996. package/src/interactive/renderers/retry-status.ts +31 -0
  997. package/src/interactive/renderers/structured.ts +89 -0
  998. package/src/interactive/renderers/tool-execution.ts +1357 -0
  999. package/src/interactive/renderers/worker-entry.ts +280 -0
  1000. package/src/interactive/session-last-turn.ts +186 -0
  1001. package/src/interactive/session-switch-settlement.ts +10 -0
  1002. package/src/interactive/session-transcript.ts +105 -0
  1003. package/src/interactive/session-usage-reseed.ts +74 -0
  1004. package/src/interactive/slash-autocomplete.ts +256 -0
  1005. package/src/interactive/slash-commands.ts +1407 -0
  1006. package/src/interactive/slash-spec.ts +424 -0
  1007. package/src/interactive/status/controller.ts +260 -0
  1008. package/src/interactive/status/index.ts +23 -0
  1009. package/src/interactive/status/state-machine.ts +530 -0
  1010. package/src/interactive/status/summary.ts +170 -0
  1011. package/src/interactive/status/types.ts +121 -0
  1012. package/src/interactive/status/verbs.ts +163 -0
  1013. package/src/interactive/status/watchdog.ts +24 -0
  1014. package/src/interactive/stdout-backpressure.ts +99 -0
  1015. package/src/interactive/stream-pacer.ts +530 -0
  1016. package/src/interactive/stream-pacing-policy.ts +66 -0
  1017. package/src/interactive/tasks-overlay.ts +540 -0
  1018. package/src/interactive/terminal-lease.ts +485 -0
  1019. package/src/interactive/theme/components.ts +55 -0
  1020. package/src/interactive/theme/glyphs.ts +70 -0
  1021. package/src/interactive/theme/index.ts +15 -0
  1022. package/src/interactive/theme/labels.ts +88 -0
  1023. package/src/interactive/theme/rules.ts +138 -0
  1024. package/src/interactive/theme/segments.ts +88 -0
  1025. package/src/interactive/theme/tokens.ts +121 -0
  1026. package/src/interactive/thinking-level-policy.ts +6 -0
  1027. package/src/interactive/tool-prose-loop.ts +122 -0
  1028. package/src/interactive/tool-prose-registration.ts +48 -0
  1029. package/src/interactive/turn-context.ts +855 -0
  1030. package/src/interactive/turn-middleware.ts +283 -0
  1031. package/src/interactive/turn-persistence.ts +458 -0
  1032. package/src/interactive/turn-queues.ts +181 -0
  1033. package/src/interactive/turn-recovery.ts +323 -0
  1034. package/src/interactive/turn-runtime.ts +869 -0
  1035. package/src/interactive/turn-state.ts +133 -0
  1036. package/src/interactive/view/artifacts.ts +1452 -0
  1037. package/src/interactive/view/view-overlay.ts +735 -0
  1038. package/src/interactive/welcome-dashboard.ts +537 -0
  1039. package/src/interactive/worker-receipts.ts +143 -0
  1040. package/src/interactive/worker-replay.ts +111 -0
  1041. package/src/interactive/worker-share.ts +100 -0
  1042. package/src/interactive/worker-stream.ts +446 -0
  1043. package/src/interactive/workspace-facts.ts +141 -0
  1044. package/src/tools/agent-tools.ts +402 -0
  1045. package/src/tools/artifact.ts +103 -0
  1046. package/src/tools/ask-user.ts +666 -0
  1047. package/src/tools/bash.ts +204 -0
  1048. package/src/tools/bootstrap.ts +77 -0
  1049. package/src/tools/builtin-tool-catalog.ts +265 -0
  1050. package/src/tools/codewiki/code-nav-surface.ts +29 -0
  1051. package/src/tools/codewiki/code-nav.ts +549 -0
  1052. package/src/tools/codewiki/shared.ts +118 -0
  1053. package/src/tools/compete-worktrees.ts +898 -0
  1054. package/src/tools/context/docs-engine.ts +630 -0
  1055. package/src/tools/context/index.ts +514 -0
  1056. package/src/tools/context/surface.ts +19 -0
  1057. package/src/tools/core-bootstrap.ts +168 -0
  1058. package/src/tools/credential-present.ts +151 -0
  1059. package/src/tools/dispatch-admission.ts +533 -0
  1060. package/src/tools/dispatch-arguments.ts +228 -0
  1061. package/src/tools/dispatch-background.ts +54 -0
  1062. package/src/tools/dispatch-event-text.ts +6 -0
  1063. package/src/tools/dispatch-plan.ts +746 -0
  1064. package/src/tools/dispatch-run-events.ts +238 -0
  1065. package/src/tools/dispatch-runner.ts +2370 -0
  1066. package/src/tools/dispatch-scout-admission.ts +295 -0
  1067. package/src/tools/dispatch-scout.ts +461 -0
  1068. package/src/tools/dispatch-types.ts +77 -0
  1069. package/src/tools/dispatch.ts +244 -0
  1070. package/src/tools/edit-diff.ts +534 -0
  1071. package/src/tools/edit.ts +128 -0
  1072. package/src/tools/executables.ts +22 -0
  1073. package/src/tools/file-mutation-queue.ts +35 -0
  1074. package/src/tools/find.ts +294 -0
  1075. package/src/tools/grep.ts +495 -0
  1076. package/src/tools/ignore-policy.ts +169 -0
  1077. package/src/tools/lazy-tool.ts +60 -0
  1078. package/src/tools/ledger.ts +206 -0
  1079. package/src/tools/ls.ts +110 -0
  1080. package/src/tools/monitor-surface.ts +36 -0
  1081. package/src/tools/monitor.ts +767 -0
  1082. package/src/tools/network-policy.ts +29 -0
  1083. package/src/tools/observation.ts +421 -0
  1084. package/src/tools/observers.ts +99 -0
  1085. package/src/tools/path-utils.ts +79 -0
  1086. package/src/tools/policy.ts +154 -0
  1087. package/src/tools/profiles.ts +108 -0
  1088. package/src/tools/read.ts +173 -0
  1089. package/src/tools/registry.ts +1169 -0
  1090. package/src/tools/result-shaping.ts +204 -0
  1091. package/src/tools/safe-exec.ts +128 -0
  1092. package/src/tools/spawn-hygiene.ts +182 -0
  1093. package/src/tools/steer-surface.ts +17 -0
  1094. package/src/tools/steer.ts +77 -0
  1095. package/src/tools/tasks.ts +265 -0
  1096. package/src/tools/truncate-utf8.ts +17 -0
  1097. package/src/tools/truncate.ts +43 -0
  1098. package/src/tools/verify/frontend.ts +687 -0
  1099. package/src/tools/verify/index.ts +31 -0
  1100. package/src/tools/verify/scripts.ts +108 -0
  1101. package/src/tools/verify/surface.ts +46 -0
  1102. package/src/tools/web-fetch-surface.ts +23 -0
  1103. package/src/tools/web-fetch.ts +759 -0
  1104. package/src/tools/worker-evidence.ts +215 -0
  1105. package/src/tools/write.ts +57 -0
  1106. package/src/utils/exif-orientation.ts +192 -0
  1107. package/src/utils/git.ts +24 -0
  1108. package/src/utils/image-resize.ts +148 -0
  1109. package/src/utils/photon.ts +103 -0
  1110. package/src/worker/control-lane.ts +22 -0
  1111. package/src/worker/entry.ts +294 -0
  1112. package/src/worker/event-projection.ts +56 -0
  1113. package/src/worker/heartbeat.ts +24 -0
  1114. package/src/worker/ledger-mirror.ts +95 -0
  1115. package/src/worker/ndjson.ts +40 -0
  1116. package/src/worker/protocol.ts +529 -0
  1117. package/src/worker/resource-facts.ts +79 -0
  1118. package/src/worker/runtime-registry.ts +12 -0
  1119. package/src/worker/spec-contract.ts +675 -0
  1120. package/src/worker/stdin-demux.ts +316 -0
  1121. package/dist/acp-UMLFVA3F.js +0 -92
  1122. package/dist/chunk-262G75JS.js +0 -35
  1123. package/dist/chunk-6CWDTGUC.js +0 -20
  1124. package/dist/chunk-BYEU76JP.js +0 -899
  1125. package/dist/chunk-GNGDQYDU.js +0 -34688
  1126. package/dist/chunk-GOTUR54M.js +0 -9
  1127. package/dist/chunk-NI4DEJMC.js +0 -88
  1128. package/dist/chunk-TPOCL34A.js +0 -2876
  1129. package/dist/chunk-UU3R62TT.js +0 -128
  1130. package/dist/clio-KIQ5SNDS.js +0 -53
  1131. package/dist/configure-G4E3A2PG.js +0 -27
  1132. package/dist/doctor-M5HJJZOL.js +0 -61
  1133. package/dist/domains/agents/builtins/architect.md +0 -33
  1134. package/dist/domains/agents/builtins/coder.md +0 -31
  1135. package/dist/domains/agents/builtins/context-bootstrap.md +0 -38
  1136. package/dist/domains/agents/builtins/debugger.md +0 -30
  1137. package/dist/domains/agents/builtins/documenter.md +0 -31
  1138. package/dist/domains/agents/builtins/git-master.md +0 -30
  1139. package/dist/domains/agents/builtins/provenance.md +0 -30
  1140. package/dist/domains/agents/builtins/researcher.md +0 -71
  1141. package/dist/domains/agents/builtins/scout.md +0 -42
  1142. package/dist/domains/agents/builtins/tester.md +0 -31
  1143. package/dist/domains/agents/builtins/verifier.md +0 -30
  1144. package/dist/domains/agents/builtins/wiki-writer.md +0 -41
  1145. package/dist/fleet-preflight-CLIAX7YR.js +0 -21
  1146. package/dist/preload-KZVHET2B.js +0 -11
  1147. package/dist/version-ROZ6CZKH.js +0 -16
  1148. package/docs/html/agents_blueprint.html +0 -936
  1149. package/docs/html/alcf_blueprint.html +0 -324
  1150. package/docs/html/architecture_blueprint.html +0 -850
  1151. package/docs/html/commands_blueprint.html +0 -794
  1152. package/docs/html/config_knobs_audit_blueprint.html +0 -178
  1153. package/docs/html/configuration_blueprint.html +0 -1080
  1154. package/docs/html/context_blueprint.html +0 -603
  1155. package/docs/html/documentation_blueprint.html +0 -832
  1156. package/docs/html/environment_blueprint.html +0 -404
  1157. package/docs/html/eval_blueprint.html +0 -743
  1158. package/docs/html/evals_internal_blueprint.html +0 -190
  1159. package/docs/html/evolution_blueprint.html +0 -674
  1160. package/docs/html/extensions_blueprint.html +0 -2065
  1161. package/docs/html/fleet_dispatch_blueprint.html +0 -286
  1162. package/docs/html/index.html +0 -919
  1163. package/docs/html/lifecycle_blueprint.html +0 -723
  1164. package/docs/html/memory_blueprint.html +0 -699
  1165. package/docs/html/middleware_blueprint.html +0 -664
  1166. package/docs/html/models_blueprint.html +0 -2366
  1167. package/docs/html/observability_blueprint.html +0 -683
  1168. package/docs/html/provider_adapter_blueprint.html +0 -245
  1169. package/docs/html/safety_blueprint.html +0 -1386
  1170. package/docs/html/shared.css +0 -571
  1171. package/docs/html/shared.js +0 -143
  1172. package/docs/html/skills_blueprint.html +0 -671
  1173. package/docs/html/soak_blueprint.html +0 -182
  1174. package/docs/html/tool_usage_blueprint.html +0 -350
  1175. package/docs/html/tools_blueprint.html +0 -2249
  1176. package/docs/html/trace_blueprint.html +0 -235
  1177. package/docs/html/tui_design_blueprint.html +0 -314
  1178. package/docs/html/validation_blueprint.html +0 -961
  1179. package/docs/html/worker_dispatch_blueprint.html +0 -231
  1180. package/skills/git/commit-crafting/SKILL.md +0 -79
  1181. package/skills/git/commit-crafting/evals.md +0 -92
  1182. package/skills/git/create-pr/SKILL.md +0 -116
  1183. package/skills/git/create-pr/evals.md +0 -114
  1184. package/skills/git/investigate-issue/SKILL.md +0 -139
  1185. package/skills/git/investigate-issue/evals.md +0 -94
  1186. package/skills/git/review-changes/SKILL.md +0 -103
  1187. package/skills/git/review-changes/evals.md +0 -85
@@ -0,0 +1,1820 @@
1
+ import { performance } from "node:perf_hooks";
2
+ import type { OutputVerbosity } from "../core/defaults.js";
3
+ import { SKILL_SUGGESTION_PREFIX } from "../core/skill-activation.js";
4
+ import { rawDurationMs } from "../core/timers.js";
5
+ import { estimateReasoningTextTokens, extractReasoningTokens } from "../domains/session/context-accounting.js";
6
+ import { type Component, Markdown, truncateToWidth, wrapTextWithAnsi } from "../engine/tui.js";
7
+ import type { AgentMessage } from "../engine/types.js";
8
+ import type { ChatLoopEvent, RetryStatusPayload } from "./chat-loop.js";
9
+ import { extractText, isSelfExplainingAbort } from "./chat-loop-messages.js";
10
+ import type { ApprovalRequestView } from "./permission-overlay.js";
11
+ import { codeInk } from "./renderers/code-ink.js";
12
+ import { createMermaidMarkdownTransform } from "./renderers/mermaid.js";
13
+ import { styleTaggedNotice } from "./renderers/notice.js";
14
+ import { formatRetryStatus } from "./renderers/retry-status.js";
15
+ import {
16
+ classifyResourceRead,
17
+ renderToolAwaitingApproval,
18
+ renderToolCallHeader,
19
+ renderToolExecution,
20
+ renderToolRunningStatus,
21
+ renderToolStreamingExecution,
22
+ renderToolSubline,
23
+ } from "./renderers/tool-execution.js";
24
+ import { renderWorkerEntryLines } from "./renderers/worker-entry.js";
25
+ import { INLINE_STATUS_INDENT_COLS, type StatusPhase, type VerbRender } from "./status/index.js";
26
+ import { clioTheme, fgSequence, GLYPH, markdownTheme, SGR_DIM, SGR_RESET } from "./theme/index.js";
27
+ import { type WorkerEntryState, workerAskedByModel } from "./worker-stream.js";
28
+
29
+ // Fenced code reaches the screen through pi-tui's Markdown component, which
30
+ // exposes the MarkdownTheme.highlightCode hook: it hands over the raw fence
31
+ // text plus its language tag before pi-tui draws the fence borders and indent.
32
+ // Wiring code ink through that hook colors only the ink, so the fence frame,
33
+ // indentation, and width behavior stay pi-tui's and nothing post-processes
34
+ // already-rendered output.
35
+ const CHAT_MARKDOWN_THEME = markdownTheme(clioTheme(), (code, lang) => codeInk(lang, code.split("\n")));
36
+ const CHAT_MARKDOWN_OPTIONS = {
37
+ transform: createMermaidMarkdownTransform(clioTheme()),
38
+ renderLatex: true,
39
+ } as const;
40
+ // TuiAltScreen uses Pi's OSC 133 prompt-start marker for semantic prompt
41
+ // navigation. The sequence is zero-width and stripped before terminal output.
42
+ const OSC133_PROMPT_START = "\x1b]133;A\x07";
43
+
44
+ // Prefix and rail SGR constants, previously re-exported by the deleted
45
+ // palette.ts. Composing them from fgSequence/GLYPH here yields byte-identical
46
+ // sequences to what palette.js produced, so the transcript renders unchanged.
47
+ const RESET = SGR_RESET;
48
+ const DIM = SGR_DIM;
49
+ const TEAL = fgSequence("accent");
50
+ const BLUE_REASON = fgSequence("reason");
51
+ const GREEN_OK = fgSequence("success");
52
+ const AMBER = fgSequence("warning");
53
+ const RED_CRIT = fgSequence("error");
54
+ const AGENT_GLYPH = GLYPH.agent;
55
+ const USER_GLYPH = GLYPH.user;
56
+
57
+ /**
58
+ * An assistant turn is a sequence of text and tool segments interleaved in
59
+ * pi-agent-core event order. A tool turn emits assistant `message_update`
60
+ * events carrying `toolcall_*` formation, then `message_end`, then the
61
+ * `tool_execution_*` lifecycle before the next assistant message. Tool calls
62
+ * therefore sit BETWEEN the assistant's pre-tool narration and the post-tool summary. Storing a flat
63
+ * `text` buffer + `tools[]` array (pre-refactor) collapsed that order: all
64
+ * text across the turn concatenated into one line with every tool block
65
+ * appended at the end. The segment list preserves the stream order instead.
66
+ *
67
+ * Each text segment tracks whether it has been finalized by a `message_end`.
68
+ * Streaming deltas render as plain lines; only finalized text is piped
69
+ * through the Markdown renderer. Partial markdown (unclosed fence, half-typed
70
+ * bullet) would otherwise paint garbage at ~60 fps under streaming.
71
+ */
72
+ export type ReasoningTokenProvenance = "provider" | "estimated" | "mixed";
73
+
74
+ export interface ChatPanelTurnUsage {
75
+ inputTokens: number;
76
+ outputTokens: number;
77
+ cacheReadTokens: number;
78
+ cacheWriteTokens: number;
79
+ reasoningTokens?: number;
80
+ reasoningTokenProvenance?: ReasoningTokenProvenance;
81
+ /** Model calls the totals were summed over; absent when the count is unknown. */
82
+ modelCalls?: number;
83
+ }
84
+
85
+ export interface ChatPanelRenderMetrics {
86
+ durationMs: number;
87
+ cacheHit: boolean;
88
+ entriesRendered: number;
89
+ }
90
+
91
+ type TextSegment = {
92
+ kind: "text";
93
+ text: string;
94
+ finalized: boolean;
95
+ /**
96
+ * Lazy pi-tui Markdown instance owned by the segment. Markdown caches its
97
+ * output by (text, width) internally, so reusing the instance keeps the
98
+ * per-frame cost O(1) for stable segments and lets the active entry
99
+ * invalidate only the tail segment's cache on re-canonicalization.
100
+ */
101
+ md?: Markdown;
102
+ /**
103
+ * Wrapped output for every source line except the last, plus the width and
104
+ * source-line count it was built at. A streaming segment only ever grows at
105
+ * its tail: source lines before the last one are terminated by a newline and
106
+ * can never change, yet every frame re-wrapped all of them. On a 16k-char
107
+ * answer that was 5-22 ms per frame to reproduce identical rows.
108
+ */
109
+ wrapCache?: { width: number; completedLines: number; lines: string[] };
110
+ /**
111
+ * A protocol suggestion line and the answer prose beneath it, when the model
112
+ * opened its reply with both in one segment (which is what the skills prompt
113
+ * asks for). The answer half is a segment of its own so it keeps its own
114
+ * Markdown and wrap caches; it is cached here rather than rebuilt per frame.
115
+ */
116
+ suggestionSplit?: { suggestion: string; answer: TextSegment };
117
+ };
118
+ type ToolSegment = {
119
+ kind: "tool";
120
+ id: string;
121
+ name: string;
122
+ args: unknown;
123
+ /** Final result from `tool_execution_end`; undefined while the call is in flight. */
124
+ result?: unknown;
125
+ /** True once `tool_execution_end` has landed (success or error). */
126
+ finished: boolean;
127
+ /** Pi streamed the call row before execution and later starts this same row. */
128
+ executionStarted: boolean;
129
+ /** The assistant stream closed this call's argument block. */
130
+ argsComplete: boolean;
131
+ /**
132
+ * True when the segment was force-settled without its own end event
133
+ * (blocked at admission, aborted mid-batch, id reused). A late
134
+ * `tool_execution_end` may upgrade such a segment with the call's true
135
+ * result; a segment finished by its own end event is never overwritten.
136
+ * The explicit `| undefined` allows the upgrade path to clear the flag
137
+ * under `exactOptionalPropertyTypes: true`.
138
+ */
139
+ settledWithoutResult?: boolean | undefined;
140
+ /** True when the finished result was an error. Meaningful only after `finished`. */
141
+ isError: boolean;
142
+ /** When true, render the full structured block instead of the collapsed subline. */
143
+ expanded: boolean;
144
+ /** Wall-clock start time captured by the chat panel for live duration display. */
145
+ startedAtMs?: number;
146
+ /** Completed call duration in milliseconds (event-supplied or measured locally). */
147
+ durationMs?: number;
148
+ /** Persisted result summary (bytes, truncated, offloadPath, observation) from the chat loop. */
149
+ resultSummary?: Record<string, unknown> | undefined;
150
+ /**
151
+ * Latest cumulative Pi result from `tool_execution_update`, including the
152
+ * display content and structured progress details. Cleared
153
+ * back to `undefined` on `tool_execution_end` so the finished `result`
154
+ * takes over. Only consumed when `!finished && expanded`. The explicit
155
+ * `| undefined` is required under `exactOptionalPropertyTypes: true` so
156
+ * the clear path can re-assign `undefined` without a `delete`.
157
+ */
158
+ partialResult?: unknown;
159
+ /**
160
+ * True while the call is parked at the permission gate. Set/cleared by
161
+ * `tool_approval_state` events and cleared by any settle so a denied or
162
+ * resumed call never keeps the awaiting-approval styling. Meaningful only
163
+ * while `!finished`.
164
+ */
165
+ awaitingApproval?: boolean | undefined;
166
+ /** Live, redacted approval facts. Never reconstructed during replay. */
167
+ approvalView?: ApprovalRequestView | undefined;
168
+ settlement?: "blocked" | "aborted" | "orphaned" | undefined;
169
+ /**
170
+ * The admission verdict's short reason, present only on a settlement the
171
+ * registry actually rejected. A blocked row without it states that something
172
+ * was refused and leaves the operator no way to learn why.
173
+ */
174
+ blockReason?: string | undefined;
175
+ /** View-only marker: historical calls render mutation diffs without live color. */
176
+ replayed?: true;
177
+ };
178
+ /**
179
+ * A turn's terminal-error marker (`[error] ...`, `[aborted] ...`,
180
+ * `[stopped: length] ...`) carried as its own segment so it renders in the
181
+ * error token instead of being piped through Markdown as plain prose. Kept
182
+ * distinct from streamed text: the error text is not model output, it is Clio
183
+ * reporting why the turn ended.
184
+ */
185
+ type ErrorSegment = {
186
+ kind: "error";
187
+ text: string;
188
+ };
189
+ type AssistantSegment = TextSegment | ToolSegment | ErrorSegment;
190
+ type ReplayBlockRenderer = (width: number) => string[];
191
+ type AssistantStatusLine = { phase: StatusPhase; verb: string; toneHint: VerbRender["toneHint"] };
192
+
193
+ type TranscriptEntry =
194
+ | { role: "user"; text: string }
195
+ | { role: "retryStatus"; status: RetryStatusPayload }
196
+ | {
197
+ role: "assistant";
198
+ segments: AssistantSegment[];
199
+ /**
200
+ * Raw thinking content from `thinking_delta` events plus
201
+ * `thinking` blocks captured on `message_end`. Renders live while
202
+ * the turn is pending: a folded `Thinking (N tokens)…` marker by
203
+ * default, or the tail of the reasoning down a dim `│ ` rail if
204
+ * expanded via `toggleLastThinking()` (Ctrl+T). Once the turn
205
+ * settles it collapses to a static `Thinking...` marker (folded) or
206
+ * a head-anchored rail (expanded), mirroring the pi-coding-agent
207
+ * reference which streams thinking from the partial message.
208
+ */
209
+ thinking: string;
210
+ /**
211
+ * Whether the thinking block renders as the full body (true) or
212
+ * the one-line dim marker (false/undefined). Toggled by
213
+ * `toggleLastThinking()`. New thinking inherits the panel-level
214
+ * visibility mode until Ctrl+T toggles it again.
215
+ */
216
+ expandedThinking?: boolean;
217
+ pending: boolean;
218
+ statusLine?: AssistantStatusLine | null | undefined;
219
+ isError: boolean;
220
+ turnUsage?: ChatPanelTurnUsage;
221
+ }
222
+ /**
223
+ * A dispatched worker's attributed block. The panel owns only the fold
224
+ * state; `state` is the live object the worker-stream reducer mutates, so a
225
+ * streaming delta reaches the screen without copying the entry per frame.
226
+ * The panel is told when that happened through `applyWorkerState`.
227
+ */
228
+ | { role: "worker"; state: WorkerEntryState; folded: boolean }
229
+ /**
230
+ * A block the caller renders itself. Most are settled the moment they are
231
+ * appended, but a few (the operator's `!` bash row) keep mutating the state
232
+ * their closure reads until the work behind them finishes. Such a block
233
+ * declares `isLive`, which keeps it out of the frozen prefix and keeps the
234
+ * panel's time-keyed tick running while it is unsettled.
235
+ */
236
+ | { role: "replayBlock"; renderBlock: ReplayBlockRenderer; isLive?: (() => boolean) | undefined };
237
+
238
+ type WorkerTranscriptEntry = Extract<TranscriptEntry, { role: "worker" }>;
239
+
240
+ export interface ChatPanel extends Component {
241
+ appendUser(text: string): void;
242
+ /**
243
+ * Append a caller-rendered block. Pass `isLive` when the closure reads state
244
+ * that keeps changing after the append, so the panel keeps re-rendering it
245
+ * instead of treating the first frame as final.
246
+ */
247
+ appendReplayBlock(renderBlock: ReplayBlockRenderer, isLive?: () => boolean): void;
248
+ applyEvent(event: ChatLoopEvent): void;
249
+ /** Mark a just-rehydrated tool segment so its mutation diff remains plain. */
250
+ markToolReplayed?(toolCallId: string): void;
251
+ /**
252
+ * Place or refresh a worker's block. The first call for an assignment
253
+ * inserts the entry (agent origin nests under the tool segment named by
254
+ * `state.parentToolCallId`, everything else appends); later calls only
255
+ * invalidate the render, because the reducer mutates the same state object.
256
+ */
257
+ applyWorkerState(state: WorkerEntryState): void;
258
+ /**
259
+ * Every worker block on the transcript, oldest first, live or replayed.
260
+ * `/share` selects from this rather than from the reducer's routing table,
261
+ * so what the operator can share is exactly what the operator can see.
262
+ */
263
+ workerStates(): ReadonlyArray<WorkerEntryState>;
264
+ setStatusLine(line: AssistantStatusLine | null): void;
265
+ toggleLastToolExpanded(): boolean;
266
+ toggleAllToolsExpanded(): boolean;
267
+ /**
268
+ * Force every tool segment into its collapsed one-line form. Replay
269
+ * (`rehydrateChatPanelFromTurns`) calls this so a resumed or forked
270
+ * transcript uses compact ledger summaries instead of auto-expanding every
271
+ * historical non-resource call. Idempotent.
272
+ */
273
+ collapseAllTools(): void;
274
+ /**
275
+ * Flip thinking-bearing assistant turns between the one-line dim marker
276
+ * and the full rail-prefixed body. The target visibility is panel-level
277
+ * sticky state, then applied to current thinking history so Ctrl+T behaves
278
+ * like a transcript-level thinking visibility toggle.
279
+ */
280
+ toggleLastThinking(): boolean;
281
+ toggleAllThinking(): boolean;
282
+ /** Current panel-level live-thinking visibility used by presentation pacing. */
283
+ isThinkingExpanded(): boolean;
284
+ /** Toggle whether expanded live tool bodies include cumulative partial output. */
285
+ toggleLiveToolOutput(): boolean;
286
+ /** Clears the visible transcript. /new uses this after rotating the session. */
287
+ reset(): void;
288
+ }
289
+
290
+ export interface ChatPanelOptions {
291
+ /**
292
+ * Resolves the user-visible key string for the `clio.tool.expand`
293
+ * action, which folds and unfolds the newest tool call or worker block.
294
+ * Returning a non-empty string surfaces a dim ` (<key>)` hint on the one
295
+ * surface the key would act on: the latest finished collapsed tool subline,
296
+ * or the newest folded worker card. Returning undefined or an empty string
297
+ * suppresses the hint. Called per render so live keybinding changes flow
298
+ * through.
299
+ */
300
+ getToolExpandKey?: () => string | undefined;
301
+ /** Live transcript detail mode. Settings changes take effect on the next frame. */
302
+ getOutputVerbosity?: () => OutputVerbosity;
303
+ /** Receives measured panel render cost; no FPS claim is made by the panel. */
304
+ onRenderMetrics?: (metrics: ChatPanelRenderMetrics) => void;
305
+ /** Clock injection for deterministic duration tests. Defaults to Date.now. */
306
+ now?: () => number;
307
+ /**
308
+ * Render every expanded tool body in full, without the live view's
309
+ * middle-elision or character truncation. `/export` builds a throwaway panel
310
+ * with this set so the written transcript reproduces the complete tool output
311
+ * instead of the terminal's bounded view.
312
+ */
313
+ unboundedToolBodies?: boolean;
314
+ }
315
+
316
+ /**
317
+ * An abort reaches the registry as a rejection, so it arrives here as a block
318
+ * whose reason names it (`run aborted before the operator decided`). Matching
319
+ * that is safe in a way that matching the tool's own output is not: this string
320
+ * is composed by Clio, never by the command that ran.
321
+ */
322
+ const ABORTED_REASON_RE = /\babort(?:ed)?\b/i;
323
+
324
+ /**
325
+ * Classify a finished tool call from the registry's admission verdict, which
326
+ * the turn runtime stamps onto the event as `outcome`. Only a call the registry
327
+ * refused settles as blocked; a call that executed and failed is an ordinary
328
+ * error and carries no settlement. Events with no verdict (replayed history, a
329
+ * surface that resolves tools without telemetry) settle as nothing rather than
330
+ * guessing.
331
+ */
332
+ function toolSettlement(event: {
333
+ outcome?: unknown;
334
+ blockReason?: unknown;
335
+ }): { settlement: "blocked" | "aborted"; reason?: string } | undefined {
336
+ if (event.outcome !== "blocked") return undefined;
337
+ const reason = typeof event.blockReason === "string" && event.blockReason.trim().length > 0 ? event.blockReason : null;
338
+ const settlement = reason !== null && ABORTED_REASON_RE.test(reason) ? "aborted" : "blocked";
339
+ return reason === null ? { settlement } : { settlement, reason };
340
+ }
341
+
342
+ function extractAssistantText(message: unknown): string {
343
+ if (!message || typeof message !== "object" || !("role" in message) || message.role !== "assistant") return "";
344
+ if (!("content" in message) || !Array.isArray(message.content)) return "";
345
+ return message.content
346
+ .filter((item): item is { type: "text"; text: string } => item?.type === "text" && typeof item.text === "string")
347
+ .map((item) => item.text)
348
+ .join("");
349
+ }
350
+
351
+ function extractAssistantThinking(message: unknown): string {
352
+ if (!message || typeof message !== "object" || !("role" in message) || message.role !== "assistant") return "";
353
+ if (!("content" in message) || !Array.isArray(message.content)) return "";
354
+ return message.content
355
+ .filter(
356
+ (item): item is { type: "thinking"; thinking: string } =>
357
+ item?.type === "thinking" && typeof item.thinking === "string",
358
+ )
359
+ .map((item) => item.thinking)
360
+ .join("");
361
+ }
362
+
363
+ function finiteToken(value: unknown): number {
364
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : 0;
365
+ }
366
+
367
+ function assistantUsage(message: unknown): ChatPanelTurnUsage | undefined {
368
+ if (!message || typeof message !== "object" || (message as { role?: unknown }).role !== "assistant") return undefined;
369
+ const record = message as Record<string, unknown>;
370
+ const usage = record.usage && typeof record.usage === "object" ? (record.usage as Record<string, unknown>) : undefined;
371
+ const thinking = extractAssistantThinking(message);
372
+ const reportedReasoning = usage ? extractReasoningTokens(usage) : null;
373
+ const estimatedReasoning = reportedReasoning === null ? estimateReasoningTextTokens(thinking) : null;
374
+ const inputTokens = finiteToken(usage?.input);
375
+ const outputTokens = finiteToken(usage?.output);
376
+ const cacheReadTokens = finiteToken(usage?.cacheRead);
377
+ const cacheWriteTokens = finiteToken(usage?.cacheWrite);
378
+ if (
379
+ inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens === 0 &&
380
+ reportedReasoning === null &&
381
+ estimatedReasoning === null
382
+ ) {
383
+ return undefined;
384
+ }
385
+ const turnUsage: ChatPanelTurnUsage = { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, modelCalls: 1 };
386
+ if (reportedReasoning !== null) {
387
+ turnUsage.reasoningTokens = reportedReasoning;
388
+ turnUsage.reasoningTokenProvenance = "provider";
389
+ } else if (estimatedReasoning !== null) {
390
+ turnUsage.reasoningTokens = estimatedReasoning;
391
+ turnUsage.reasoningTokenProvenance = "estimated";
392
+ }
393
+ return turnUsage;
394
+ }
395
+
396
+ function aggregateAssistantUsage(messages: unknown): ChatPanelTurnUsage | undefined {
397
+ if (!Array.isArray(messages)) return undefined;
398
+ const total: ChatPanelTurnUsage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
399
+ let found = false;
400
+ let provider = false;
401
+ let estimated = false;
402
+ for (const message of messages) {
403
+ const usage = assistantUsage(message);
404
+ if (!usage) continue;
405
+ found = true;
406
+ total.modelCalls = (total.modelCalls ?? 0) + (usage.modelCalls ?? 1);
407
+ total.inputTokens += usage.inputTokens;
408
+ total.outputTokens += usage.outputTokens;
409
+ total.cacheReadTokens += usage.cacheReadTokens;
410
+ total.cacheWriteTokens += usage.cacheWriteTokens;
411
+ if (usage.reasoningTokens !== undefined) {
412
+ total.reasoningTokens = (total.reasoningTokens ?? 0) + usage.reasoningTokens;
413
+ if (usage.reasoningTokenProvenance === "provider") provider = true;
414
+ else estimated = true;
415
+ }
416
+ }
417
+ if (!found) return undefined;
418
+ if (provider || estimated)
419
+ total.reasoningTokenProvenance = provider && estimated ? "mixed" : provider ? "provider" : "estimated";
420
+ return total;
421
+ }
422
+
423
+ function extractAssistantTerminalError(message: unknown): string {
424
+ if (!message || typeof message !== "object" || !("role" in message) || message.role !== "assistant") return "";
425
+ const stopReason = (message as { stopReason?: unknown }).stopReason;
426
+ if (stopReason !== "error" && stopReason !== "aborted" && stopReason !== "length") return "";
427
+ if (stopReason === "length") {
428
+ return "[stopped: length] Model target hit its generation/output limit before a complete response. This is not a safety denial. Continue with a shorter answer or lower thinking; use /context compact if the context meter is also near full.";
429
+ }
430
+ const raw = (message as { errorMessage?: unknown }).errorMessage;
431
+ if (isSelfExplainingAbort({ stopReason, errorMessage: raw, text: extractText(message as AgentMessage) })) return "";
432
+ const reason = typeof raw === "string" && raw.length > 0 ? raw : "unknown error";
433
+ return stopReason === "aborted" ? `[aborted] ${reason}` : `[error] ${reason}`;
434
+ }
435
+
436
+ function scopeTerminalErrorAfterSuccessfulTool(
437
+ entry: Extract<TranscriptEntry, { role: "assistant" }>,
438
+ terminalError: string,
439
+ ): string {
440
+ if (!terminalError.startsWith("[error] ")) return terminalError;
441
+ const successfulTools = entry.segments.filter(
442
+ (segment): segment is ToolSegment => segment.kind === "tool" && segment.finished && !segment.isError,
443
+ );
444
+ if (successfulTools.length === 0) return terminalError;
445
+
446
+ const reason = terminalError.slice("[error] ".length);
447
+ const timedOut = /\b(?:timed?\s*out|timeout)\b/i.test(reason);
448
+ const modelFailure = timedOut
449
+ ? "main model response timed out after successful tool result"
450
+ : `main model response failed after successful tool result: ${reason}`;
451
+ const detachedDispatchSucceeded = successfulTools.some(
452
+ (segment) =>
453
+ segment.name === "dispatch" &&
454
+ segment.args !== null &&
455
+ typeof segment.args === "object" &&
456
+ (segment.args as { detach?: unknown }).detach === true,
457
+ );
458
+ return `[error] ${modelFailure}${detachedDispatchSucceeded ? "; detached runs continue" : ""}`;
459
+ }
460
+
461
+ function hasVisibleOutput(entry: Extract<TranscriptEntry, { role: "assistant" }>): boolean {
462
+ for (const seg of entry.segments) {
463
+ if (seg.kind === "tool") return true;
464
+ if (seg.kind === "text" && seg.text.trim().length > 0) return true;
465
+ if (seg.kind === "error" && seg.text.trim().length > 0) return true;
466
+ }
467
+ return false;
468
+ }
469
+
470
+ /** Index of the newest assistant entry, optionally restricted to ones that rendered something. */
471
+ function lastAssistantIndex(
472
+ transcript: ReadonlyArray<TranscriptEntry>,
473
+ options: { withOutput?: boolean } = {},
474
+ ): number | null {
475
+ for (let index = transcript.length - 1; index >= 0; index -= 1) {
476
+ const entry = transcript[index];
477
+ if (entry?.role !== "assistant") continue;
478
+ if (options.withOutput === true && !hasVisibleOutput(entry)) continue;
479
+ return index;
480
+ }
481
+ return null;
482
+ }
483
+
484
+ function hasStreamingText(entry: Extract<TranscriptEntry, { role: "assistant" }>): boolean {
485
+ const tail = entry.segments[entry.segments.length - 1];
486
+ return tail?.kind === "text" && !tail.finalized && tail.text.trim().length > 0;
487
+ }
488
+
489
+ /**
490
+ * Split a reply that opens with the skill-suggestion protocol line into that
491
+ * line and the answer beneath it.
492
+ *
493
+ * The prompt asks the model to *begin its reply* with the suggestion, so the
494
+ * usual shape is one segment holding both. Classifying that whole segment as
495
+ * advisory left the turn with no voice glyph at all: the suggestion did not
496
+ * claim it and the answer never got the chance. Returns null when the segment
497
+ * is only a suggestion, which stays advisory in full.
498
+ */
499
+ function skillSuggestionSplit(seg: TextSegment): { suggestion: string; answer: TextSegment } | null {
500
+ if (!seg.text.startsWith(SKILL_SUGGESTION_PREFIX)) return null;
501
+ const breakIndex = seg.text.indexOf("\n");
502
+ if (breakIndex < 0) return null;
503
+ // A model that puts a blank line between the suggestion and the answer left
504
+ // the remainder opening with a newline, so the first rendered row was empty
505
+ // and took the glyph the answer's own text row was owed.
506
+ const answerText = seg.text.slice(breakIndex + 1).replace(/^\n+/, "");
507
+ if (answerText.trim().length === 0) return null;
508
+ const split = seg.suggestionSplit ?? {
509
+ suggestion: "",
510
+ answer: { kind: "text", text: answerText, finalized: seg.finalized } as TextSegment,
511
+ };
512
+ seg.suggestionSplit = split;
513
+ split.suggestion = seg.text.slice(0, breakIndex);
514
+ const answer = split.answer;
515
+ if (answer.text === answerText && answer.finalized === seg.finalized) return split;
516
+ // The answer half follows the same cache rules as any other segment: the
517
+ // streaming wrap cache assumes append-only text, so a rewrite or a
518
+ // finalization drops it and a delta keeps it.
519
+ const appendOnly = !seg.finalized && answer.finalized === seg.finalized && answerText.startsWith(answer.text);
520
+ answer.text = answerText;
521
+ answer.finalized = seg.finalized;
522
+ if (!appendOnly) delete answer.wrapCache;
523
+ if (answer.md) answer.md.setText(answerText);
524
+ return split;
525
+ }
526
+
527
+ function renderTextSegmentLines(seg: TextSegment, width: number): string[] {
528
+ if (!seg.finalized) {
529
+ const source = seg.text.split("\n");
530
+ // The final element is the live tail; everything before it is newline-
531
+ // terminated and frozen.
532
+ const completedCount = source.length - 1;
533
+ const cache = seg.wrapCache;
534
+ const reusable = cache !== undefined && cache.width === width && cache.completedLines <= completedCount;
535
+ const completed = reusable ? cache.lines.slice() : [];
536
+ for (let i = reusable ? cache.completedLines : 0; i < completedCount; i += 1) {
537
+ for (const line of wrapTextWithAnsi(source[i] ?? "", width)) completed.push(line);
538
+ }
539
+ seg.wrapCache = { width, completedLines: completedCount, lines: completed };
540
+ const wrapped = completed.slice();
541
+ for (const line of wrapTextWithAnsi(source[completedCount] ?? "", width)) wrapped.push(line);
542
+ return wrapped;
543
+ }
544
+ if (!seg.md) {
545
+ seg.md = new Markdown(seg.text, 0, 0, CHAT_MARKDOWN_THEME, undefined, CHAT_MARKDOWN_OPTIONS);
546
+ }
547
+ // pi-tui Markdown right-pads lines to the render width. If a long streaming
548
+ // reply has already scrolled, flipping the finalized segment from unpadded
549
+ // plain text to padded Markdown changes historical rows and forces a full
550
+ // redraw on terminals that cannot clear scrollback. Trim only that render
551
+ // padding so finalized prose remains byte-stable with the streamed shape.
552
+ return seg.md.render(width).map((line) => line.replace(/ +$/, ""));
553
+ }
554
+
555
+ /**
556
+ * Render a terminal-error segment in the error token. Terminal markers such as
557
+ * `[error] ...`, `[aborted] ...`, and `[stopped: length] ...` render as red
558
+ * message text rather than plain markdown, so a failed turn is visibly a
559
+ * failure. Each source line wraps to width and carries the error color.
560
+ */
561
+ function renderErrorSegmentLines(seg: ErrorSegment, width: number): string[] {
562
+ const out: string[] = [];
563
+ for (const line of seg.text.split("\n")) {
564
+ for (const wrapped of wrapTextWithAnsi(line, width)) {
565
+ out.push(`${RED_CRIT}${wrapped}${RESET}`);
566
+ }
567
+ }
568
+ return out;
569
+ }
570
+
571
+ const CLIO_PREFIX = `${TEAL}${AGENT_GLYPH}${RESET} `;
572
+ const CLIO_PREFIX_ERROR = `${RED_CRIT}${AGENT_GLYPH}${RESET} `;
573
+ const USER_PREFIX = `${TEAL}${USER_GLYPH}${RESET} `;
574
+ const PROSE_GUTTER = " ";
575
+ const PROSE_GUTTER_WIDTH = 2;
576
+
577
+ /**
578
+ * Give transcript prose a fixed two-cell ownership gutter. Callers render the
579
+ * content at `width - 2`, then this function spends those reserved cells on
580
+ * either the turn glyph or a hanging indent. Tool ledgers never pass through
581
+ * here, so their existing full-width grammar remains untouched.
582
+ */
583
+ function hangProseLines(lines: string[], firstPrefix?: string): string[] {
584
+ return lines.map((line, index) => `${index === 0 && firstPrefix !== undefined ? firstPrefix : PROSE_GUTTER}${line}`);
585
+ }
586
+
587
+ /**
588
+ * Static marker used when thinking is folded. This matches pi-coding-agent's
589
+ * hidden-thinking presentation and avoids previewing reasoning content.
590
+ */
591
+ const THINKING_HIDDEN_LABEL = `Thinking${GLYPH.ellipsis}`;
592
+ const THINKING_LINE_LIMIT = 12;
593
+ const REASONING_CHARS_PER_TOKEN = 4;
594
+
595
+ function estimateThinkingTokens(thinking: string): number {
596
+ return estimateReasoningTextTokens(thinking) ?? Math.max(1, Math.round(thinking.length / REASONING_CHARS_PER_TOKEN));
597
+ }
598
+
599
+ function reasoningDisplay(usage: ChatPanelTurnUsage | undefined, thinking: string): { tokens: number; marker: string } {
600
+ if (usage?.reasoningTokens !== undefined) {
601
+ return {
602
+ tokens: usage.reasoningTokens,
603
+ marker: usage.reasoningTokenProvenance === "provider" ? "provider-reported" : "mixed/estimated",
604
+ };
605
+ }
606
+ return { tokens: estimateThinkingTokens(thinking), marker: "estimated" };
607
+ }
608
+
609
+ /**
610
+ * Render the assistant turn's thinking block. Collapsed (default) returns a
611
+ * single dim `Thinking...` marker. Expanded returns the full body dimmed and
612
+ * prefixed with a dim `│ ` rail, capped at `THINKING_LINE_LIMIT` lines with a
613
+ * tail `... N more lines hidden` overflow message. Mirrors the tool toggle's
614
+ * lab-notebook minimalism: no colored glyphs, no boxes.
615
+ */
616
+ function renderThinkingLines(
617
+ thinking: string,
618
+ expanded: boolean,
619
+ width: number,
620
+ streaming: boolean,
621
+ usage?: ChatPanelTurnUsage,
622
+ ): string[] {
623
+ if (thinking.length === 0) return [];
624
+ const dimWrap = (s: string): string => `${DIM}${s}${RESET}`;
625
+ if (!expanded) {
626
+ const lineBudget = Math.max(1, width);
627
+ const display = reasoningDisplay(usage, thinking);
628
+ const label = streaming
629
+ ? `Thinking (${display.tokens} tokens)${display.marker === "provider-reported" ? "" : " ≈ estimated"}…`
630
+ : THINKING_HIDDEN_LABEL;
631
+ return [dimWrap(truncateToWidth(label, lineBudget, GLYPH.ellipsis, false))];
632
+ }
633
+ const splitLines = thinking.split("\n");
634
+ let visible: string[];
635
+ if (streaming) {
636
+ if (splitLines.length > THINKING_LINE_LIMIT) {
637
+ const hiddenCount = splitLines.length - THINKING_LINE_LIMIT;
638
+ visible = [`… ${hiddenCount} earlier lines hidden`, ...splitLines.slice(-THINKING_LINE_LIMIT)];
639
+ } else {
640
+ visible = splitLines;
641
+ }
642
+ } else {
643
+ visible =
644
+ splitLines.length > THINKING_LINE_LIMIT
645
+ ? [...splitLines.slice(0, THINKING_LINE_LIMIT), `... ${splitLines.length - THINKING_LINE_LIMIT} more lines hidden`]
646
+ : splitLines;
647
+ }
648
+ const out: string[] = [];
649
+ const bodyWidth = Math.max(1, width - 2);
650
+ for (const raw of visible) {
651
+ const wrappedLines = raw.length === 0 ? [""] : wrapTextWithAnsi(raw, bodyWidth);
652
+ for (const wrapped of wrappedLines) {
653
+ out.push(`${BLUE_REASON}│ ${RESET}${DIM}${wrapped}${RESET}`);
654
+ }
655
+ }
656
+ return out;
657
+ }
658
+
659
+ /**
660
+ * `in` is the sum of every model call the turn made, not the size of one
661
+ * prompt. A long agentic turn makes dozens of calls that each resend a growing
662
+ * context, so the total runs far past the model's context window and reads as
663
+ * one impossible request unless the call count is beside it. One live turn
664
+ * reported 717676 input tokens against a 500k window; it was 65 calls of about
665
+ * 11k, which the preceding one-call turns had already shown.
666
+ */
667
+ function renderTurnUsageLine(usage: ChatPanelTurnUsage, width: number, verbosity: OutputVerbosity): string[] {
668
+ if (verbosity === "minimal") return [];
669
+ if (verbosity === "default") {
670
+ const receipt = truncateToWidth(
671
+ ` turn · in ${usage.inputTokens} · out ${usage.outputTokens}`,
672
+ width,
673
+ GLYPH.ellipsis,
674
+ false,
675
+ );
676
+ return [`${DIM}${receipt}${RESET}`];
677
+ }
678
+ const calls = usage.modelCalls !== undefined && usage.modelCalls > 1 ? ` over ${usage.modelCalls} calls` : "";
679
+ // The label stays separated from the count in both provenances. Deriving the
680
+ // separator from the `≈` marker glued them together whenever the provider
681
+ // reported a total, which is the common case, and rendered `reason0 provider`.
682
+ // The field is named for reasoning tokens rather than `reason`, which the
683
+ // memory step rows already use for a fixed decision vocabulary.
684
+ //
685
+ // A turn that spent no reasoning tokens states nothing by naming the
686
+ // provenance of zero, and at narrow widths `reasoning 0 provider` orphaned
687
+ // the word `provider` on its own line. Zero suppresses the whole suffix, the
688
+ // same rule the caveat below already follows.
689
+ const reason =
690
+ usage.reasoningTokens !== undefined && usage.reasoningTokens > 0
691
+ ? ` reasoning ${usage.reasoningTokenProvenance === "provider" ? `${usage.reasoningTokens} provider` : `≈${usage.reasoningTokens} estimated`}`
692
+ : "";
693
+ const cache =
694
+ usage.cacheReadTokens > 0 || usage.cacheWriteTokens > 0
695
+ ? ` cache ${usage.cacheReadTokens}/${usage.cacheWriteTokens}`
696
+ : "";
697
+ // The caveat is about reasoning text the panel displayed. A turn that spent
698
+ // no reasoning tokens displayed none, so appending it there warned about
699
+ // something absent and cost a wrapped line per turn at narrow widths.
700
+ const caveat =
701
+ usage.reasoningTokens !== undefined && usage.reasoningTokens > 0
702
+ ? " · reasoning text is a UI excerpt, not a verification"
703
+ : "";
704
+ return wrapTextWithAnsi(
705
+ `${DIM} turn · in ${usage.inputTokens}${calls} · out ${usage.outputTokens}${cache}${reason}${caveat}${RESET}`,
706
+ width,
707
+ );
708
+ }
709
+
710
+ /** The indent resolveInlineVerb budgets against when it fits the verb to the terminal. */
711
+ const STATUS_INDENT = " ".repeat(INLINE_STATUS_INDENT_COLS);
712
+
713
+ function styleStatusVerb(text: string, toneHint: VerbRender["toneHint"]): string {
714
+ if (toneHint === "error") return `${RED_CRIT}${text}${RESET}`;
715
+ if (toneHint === "warn") return `${AMBER}${text}${RESET}`;
716
+ if (toneHint === "ok") return `${GREEN_OK}${text}${RESET}`;
717
+ return `${DIM}${text}${RESET}`;
718
+ }
719
+
720
+ function renderToolSegmentLines(
721
+ seg: ToolSegment,
722
+ width: number,
723
+ expandKey: string | undefined,
724
+ latestHintToolId: string | null,
725
+ nowMs: number,
726
+ unboundedToolBodies: boolean,
727
+ verbosity: OutputVerbosity,
728
+ liveToolOutput: boolean,
729
+ ): string[] {
730
+ const hintKey = seg.id === latestHintToolId ? expandKey : undefined;
731
+ const expanded = verbosity === "verbose" || (verbosity !== "minimal" && seg.expanded);
732
+ const elapsedMs = seg.startedAtMs !== undefined ? Math.max(0, rawDurationMs(seg.startedAtMs, nowMs)) : undefined;
733
+ const phase: "forming" | "ready" | "running" = seg.executionStarted
734
+ ? "running"
735
+ : seg.argsComplete
736
+ ? "ready"
737
+ : "forming";
738
+ // A parked call is not executing: the awaiting-approval line replaces the
739
+ // counting elapsed spinner in both collapsed and expanded form (there is no
740
+ // body or partial output to expand while the call sits at the gate).
741
+ if (!seg.finished && seg.awaitingApproval === true) {
742
+ return renderToolAwaitingApproval(
743
+ { toolCallId: seg.id, toolName: seg.name, args: seg.args },
744
+ width,
745
+ seg.approvalView,
746
+ );
747
+ }
748
+ if (!expanded) {
749
+ return renderToolSubline(
750
+ seg.finished
751
+ ? {
752
+ toolCallId: seg.id,
753
+ toolName: seg.name,
754
+ args: seg.args,
755
+ result: seg.result,
756
+ isError: seg.isError,
757
+ durationMs: seg.durationMs,
758
+ resultSummary: seg.resultSummary,
759
+ outcome: seg.settlement,
760
+ blockReason: seg.blockReason,
761
+ }
762
+ : { toolCallId: seg.id, toolName: seg.name, args: seg.args, elapsedMs, phase },
763
+ width,
764
+ hintKey,
765
+ );
766
+ }
767
+ if (!seg.finished) {
768
+ if (liveToolOutput && seg.partialResult !== undefined) {
769
+ return renderToolStreamingExecution(
770
+ { toolCallId: seg.id, toolName: seg.name, args: seg.args, elapsedMs, phase },
771
+ width,
772
+ seg.partialResult,
773
+ );
774
+ }
775
+ const call = { toolCallId: seg.id, toolName: seg.name, args: seg.args, elapsedMs, phase };
776
+ return liveToolOutput ? renderToolCallHeader(call, width) : renderToolRunningStatus(call, width);
777
+ }
778
+ return renderToolExecution(
779
+ {
780
+ toolCallId: seg.id,
781
+ toolName: seg.name,
782
+ args: seg.args,
783
+ result: seg.result,
784
+ isError: seg.isError,
785
+ durationMs: seg.durationMs,
786
+ resultSummary: seg.resultSummary,
787
+ outcome: seg.settlement,
788
+ blockReason: seg.blockReason,
789
+ },
790
+ width,
791
+ { unbounded: unboundedToolBodies, diffStyle: seg.replayed === true ? "plain" : "color" },
792
+ );
793
+ }
794
+
795
+ /**
796
+ * Whether a worker block draws its one-line card this frame. `/output verbose`
797
+ * opens every block and `/output minimal` folds every block, so the entry's own
798
+ * fold decides only in between.
799
+ */
800
+ function workerEntryFolded(entry: WorkerTranscriptEntry, verbosity: OutputVerbosity): boolean {
801
+ if (verbosity === "verbose") return false;
802
+ if (verbosity === "minimal") return true;
803
+ return entry.folded;
804
+ }
805
+
806
+ function renderEntryLines(
807
+ entry: TranscriptEntry,
808
+ width: number,
809
+ expandKey: string | undefined,
810
+ latestHintToolId: string | null,
811
+ latestFoldedWorkerId: string | null,
812
+ nowMs: number,
813
+ unboundedToolBodies: boolean,
814
+ verbosity: OutputVerbosity,
815
+ liveToolOutput: boolean,
816
+ ): string[] {
817
+ if (entry.role === "replayBlock") {
818
+ return entry.renderBlock(width);
819
+ }
820
+ if (entry.role === "user") {
821
+ const contentWidth = Math.max(1, width - PROSE_GUTTER_WIDTH);
822
+ const lines: string[] = [];
823
+ for (const sourceLine of entry.text.split("\n")) lines.push(...wrapTextWithAnsi(sourceLine, contentWidth));
824
+ const rendered = hangProseLines(lines, USER_PREFIX);
825
+ if (rendered[0] !== undefined) rendered[0] = `${OSC133_PROMPT_START}${rendered[0]}`;
826
+ return rendered;
827
+ }
828
+ if (entry.role === "retryStatus") {
829
+ return wrapTextWithAnsi(formatRetryStatus(entry.status), width);
830
+ }
831
+ if (entry.role === "worker") {
832
+ return renderWorkerEntryLines(entry.state, width, {
833
+ folded: workerEntryFolded(entry, verbosity),
834
+ ...(expandKey !== undefined && entry.state.assignmentId === latestFoldedWorkerId ? { expandKey } : {}),
835
+ unbounded: unboundedToolBodies,
836
+ });
837
+ }
838
+ // A settled assistant entry that rendered nothing at all contributes nothing.
839
+ // A mid-turn notice splits the transcript, so the events after it open a
840
+ // fresh entry that a stopped turn never fills; that entry used to reach the
841
+ // tail below and print a lone agent bubble under the notice.
842
+ if (
843
+ !entry.pending &&
844
+ entry.thinking.length === 0 &&
845
+ entry.turnUsage === undefined &&
846
+ !hasVisibleOutput(entry) &&
847
+ entry.segments.length === 0
848
+ ) {
849
+ return [];
850
+ }
851
+ const lines: string[] = [];
852
+ // Thinking renders BEFORE assistant text/tool segments so the folded marker
853
+ // or expanded rail sits above the response, matching the order the
854
+ // pi-coding-agent reference uses. It streams live while `pending === true`
855
+ // (folded shows a dynamic token count; expanded tail-anchors the tail) and
856
+ // collapses to a static marker / head-anchored rail once the turn settles.
857
+ // The generic "thinking" status verb is suppressed while this marker is
858
+ // active so only one indicator shows (see `shouldRenderStatus` below).
859
+ if (entry.thinking.length > 0) {
860
+ lines.push(
861
+ ...renderThinkingLines(
862
+ entry.thinking,
863
+ verbosity === "verbose" || (verbosity !== "minimal" && entry.expandedThinking === true),
864
+ width,
865
+ entry.pending,
866
+ entry.turnUsage,
867
+ ),
868
+ );
869
+ }
870
+ const clioPrefix = entry.isError ? CLIO_PREFIX_ERROR : CLIO_PREFIX;
871
+ const proseWidth = Math.max(1, width - PROSE_GUTTER_WIDTH);
872
+ let labeled = false;
873
+ for (const seg of entry.segments) {
874
+ if (seg.kind === "tool") {
875
+ lines.push(
876
+ ...renderToolSegmentLines(
877
+ seg,
878
+ width,
879
+ expandKey,
880
+ latestHintToolId,
881
+ nowMs,
882
+ unboundedToolBodies,
883
+ verbosity,
884
+ liveToolOutput,
885
+ ),
886
+ );
887
+ continue;
888
+ }
889
+ // Text and error segments share the reply-prefix bookkeeping: the first
890
+ // substantive one carries the agent glyph and every later one hangs plain.
891
+ // A leading skill-suggestion protocol line is advisory rather than the
892
+ // answer, so it renders in place without claiming the turn's voice glyph.
893
+ if (seg.kind === "text" && seg.text.length === 0) continue;
894
+ const split = seg.kind === "text" ? skillSuggestionSplit(seg) : null;
895
+ if (split) {
896
+ // The suggestion line hangs plain and the answer beneath it is ordinary
897
+ // prose, so it claims the glyph when nothing else has.
898
+ lines.push(...hangProseLines(wrapTextWithAnsi(split.suggestion, proseWidth)));
899
+ const answerLines = renderTextSegmentLines(split.answer, proseWidth);
900
+ if (answerLines.length === 0) continue;
901
+ lines.push(...(labeled ? hangProseLines(answerLines) : hangProseLines(answerLines, clioPrefix)));
902
+ labeled = true;
903
+ continue;
904
+ }
905
+ const rendered =
906
+ seg.kind === "text" ? renderTextSegmentLines(seg, proseWidth) : renderErrorSegmentLines(seg, proseWidth);
907
+ if (rendered.length === 0) continue;
908
+ const isSkillSuggestion = seg.kind === "text" && seg.text.startsWith(SKILL_SUGGESTION_PREFIX);
909
+ if (!labeled && !isSkillSuggestion) {
910
+ lines.push(...hangProseLines(rendered, clioPrefix));
911
+ labeled = true;
912
+ } else {
913
+ lines.push(...hangProseLines(rendered));
914
+ }
915
+ }
916
+ if (entry.turnUsage && !entry.pending) lines.push(...renderTurnUsageLine(entry.turnUsage, width, verbosity));
917
+ const shouldRenderStatus =
918
+ entry.pending &&
919
+ entry.statusLine !== null &&
920
+ entry.statusLine !== undefined &&
921
+ !(entry.statusLine.phase === "writing" && hasStreamingText(entry)) &&
922
+ !(entry.statusLine.phase === "thinking" && entry.thinking.length > 0);
923
+ if (!labeled && !hasVisibleOutput(entry)) {
924
+ lines.push(clioPrefix.trimEnd());
925
+ if (shouldRenderStatus) {
926
+ lines.push(
927
+ `${STATUS_INDENT}${styleStatusVerb(entry.statusLine?.verb ?? "", entry.statusLine?.toneHint ?? "muted")}`,
928
+ );
929
+ }
930
+ } else if (shouldRenderStatus) {
931
+ lines.push(`${STATUS_INDENT}${styleStatusVerb(entry.statusLine?.verb ?? "", entry.statusLine?.toneHint ?? "muted")}`);
932
+ }
933
+ return lines;
934
+ }
935
+
936
+ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
937
+ const transcript: TranscriptEntry[] = [];
938
+ /** Assignment to its placed block, in placement order, so a streaming delta is O(1) to route. */
939
+ const workerEntries = new Map<string, WorkerTranscriptEntry>();
940
+ let dirty = true;
941
+ let cachedWidth: number | undefined;
942
+ let cachedLines: string[] = [];
943
+ let cachedExpandKey: string | undefined;
944
+ let cachedVerbosity: OutputVerbosity | undefined;
945
+ let cachedLiveToolOutput: boolean | undefined;
946
+ let cachedTick = 0;
947
+ /**
948
+ * Did the last executed render put a counting elapsed line on screen? It is
949
+ * what decides whether the render key carries a time tick at all, so a
950
+ * transcript with nothing running keeps the old mutation-only invalidation.
951
+ */
952
+ let renderedRunningTool = false;
953
+ const entryRenderCache = new Map<TranscriptEntry, { key: string; lines: string[] }>();
954
+ /**
955
+ * The cache follows the transcript instead of stopping at a fixed 256
956
+ * entries, which re-rendered everything past the cap on every dirty frame
957
+ * (10 ms/frame at 800 entries). The ceiling bounds worst-case memory at
958
+ * roughly 4096 rendered entries; past it, the excess only re-renders on
959
+ * full-rebuild events (width change, expand-all), which the frozen prefix
960
+ * below makes rare rather than per-frame.
961
+ */
962
+ const MIN_ENTRY_RENDER_CACHE = 256;
963
+ const MAX_ENTRY_RENDER_CACHE = 4096;
964
+ const entryCacheCapacity = (): number =>
965
+ Math.max(MIN_ENTRY_RENDER_CACHE, Math.min(MAX_ENTRY_RENDER_CACHE, transcript.length));
966
+ /**
967
+ * Regular-screen windowed-tail build: the lines of every settled
968
+ * leading entry are baked into one frozen prefix, so a dirty frame re-renders
969
+ * only the live tail and re-emits the prefix by reference. TuiMainScreen still
970
+ * receives the full line array, deliberately: the renderer keeps every
971
+ * line in `previousLines` and full-redraws (clearing scrollback) when the
972
+ * head shrinks. Fullscreen mode instead gives the transcript its own pi-tui
973
+ * ScrollView. The freeze is dropped whenever a frozen entry is invalidated or
974
+ * the render key changes.
975
+ */
976
+ let frozen: { lines: string[]; through: number; key: string } | null = null;
977
+ let thinkingExpanded = false;
978
+ let liveToolOutput = true;
979
+ const unboundedToolBodies = options.unboundedToolBodies === true;
980
+
981
+ const markDirty = (): void => {
982
+ dirty = true;
983
+ };
984
+ const invalidateEntryCache = (entry: TranscriptEntry): void => {
985
+ entryRenderCache.delete(entry);
986
+ if (frozen !== null && transcript.indexOf(entry) < frozen.through) frozen = null;
987
+ };
988
+ /** Full drop of both render caches; used by the toggle paths that touch many entries. */
989
+ const clearRenderCaches = (): void => {
990
+ entryRenderCache.clear();
991
+ frozen = null;
992
+ };
993
+ /**
994
+ * A mutation is about to land on the tail entry without an explicit
995
+ * invalidation. If the freeze extends over the whole transcript, that tail
996
+ * entry is frozen and the freeze must go.
997
+ */
998
+ const unfreezeTail = (): void => {
999
+ if (frozen !== null && frozen.through >= transcript.length) frozen = null;
1000
+ };
1001
+
1002
+ const resolveExpandKey = (): string | undefined => {
1003
+ const key = options.getToolExpandKey?.();
1004
+ if (typeof key !== "string" || key.length === 0) return undefined;
1005
+ return key;
1006
+ };
1007
+
1008
+ const now = (): number => options.now?.() ?? Date.now();
1009
+
1010
+ /**
1011
+ * Force an in-flight tool segment to a settled error line. A call blocked at
1012
+ * admission (loop guard, safety) or one whose `tool_execution_end` never
1013
+ * arrives (aborted mid-batch, a model that reuses a tool-call id) would
1014
+ * otherwise stay a counting `· N.Ns` running line forever. Settling it with
1015
+ * its OWN elapsed gives it the same visual grammar as any other error
1016
+ * (`✗ · <ms>`), so a blocked call reads like the failure it is.
1017
+ */
1018
+ const settleUnfinishedToolSegment = (seg: ToolSegment, settlement: "aborted" | "orphaned" = "orphaned"): void => {
1019
+ if (seg.finished) return;
1020
+ seg.finished = true;
1021
+ seg.isError = true;
1022
+ seg.settledWithoutResult = true;
1023
+ if (seg.durationMs === undefined && seg.startedAtMs !== undefined) {
1024
+ seg.durationMs = Math.max(1, now() - seg.startedAtMs);
1025
+ }
1026
+ if (seg.result === undefined)
1027
+ seg.result = "(no result: the call did not complete; execution was aborted, blocked, or orphaned)";
1028
+ seg.settlement = settlement;
1029
+ seg.partialResult = undefined;
1030
+ seg.awaitingApproval = undefined;
1031
+ seg.approvalView = undefined;
1032
+ };
1033
+
1034
+ /**
1035
+ * Locate the most recent tool segment with this call id anywhere in the
1036
+ * transcript. A mid-turn notice entry (safety-net block, approval parked,
1037
+ * context-engine notice) splits the transcript, so an in-flight call's
1038
+ * segment can live in an earlier assistant entry than the tail. Unfinished
1039
+ * segments win over finished ones so an id the model reuses binds to the
1040
+ * live call, not the settled one. Among finished segments only a
1041
+ * force-settled one (no end event of its own) is returned: a late true
1042
+ * result may upgrade the synthetic settle, but a segment that finished
1043
+ * with its own result is never rewritten after the fact.
1044
+ */
1045
+ const findToolSegmentOwner = (toolCallId: string): { segment: ToolSegment; entry: TranscriptEntry } | undefined => {
1046
+ let settledMatch: { segment: ToolSegment; entry: TranscriptEntry } | undefined;
1047
+ for (let entryIndex = transcript.length - 1; entryIndex >= 0; entryIndex -= 1) {
1048
+ const entry = transcript[entryIndex];
1049
+ if (entry?.role !== "assistant") continue;
1050
+ for (let segIndex = entry.segments.length - 1; segIndex >= 0; segIndex -= 1) {
1051
+ const seg = entry.segments[segIndex];
1052
+ if (seg?.kind !== "tool" || seg.id !== toolCallId) continue;
1053
+ if (!seg.finished) return { segment: seg, entry };
1054
+ if (seg.settledWithoutResult === true) settledMatch ??= { segment: seg, entry };
1055
+ }
1056
+ }
1057
+ return settledMatch;
1058
+ };
1059
+
1060
+ const ensureAssistant = (): Extract<TranscriptEntry, { role: "assistant" }> => {
1061
+ const last = transcript[transcript.length - 1];
1062
+ if (last && last.role === "assistant") {
1063
+ // Callers mutate the returned entry (pending, thinking, segments)
1064
+ // without always invalidating; a fully-frozen transcript would keep
1065
+ // serving the settled render of this tail entry.
1066
+ unfreezeTail();
1067
+ return last;
1068
+ }
1069
+ const entry: Extract<TranscriptEntry, { role: "assistant" }> = {
1070
+ role: "assistant",
1071
+ segments: [],
1072
+ thinking: "",
1073
+ expandedThinking: thinkingExpanded,
1074
+ pending: false,
1075
+ isError: false,
1076
+ };
1077
+ transcript.push(entry);
1078
+ return entry;
1079
+ };
1080
+
1081
+ const appendTextDelta = (entry: Extract<TranscriptEntry, { role: "assistant" }>, delta: string): void => {
1082
+ if (delta.length === 0) return;
1083
+ invalidateEntryCache(entry);
1084
+ const tail = entry.segments[entry.segments.length - 1];
1085
+ if (tail && tail.kind === "text" && !tail.finalized) {
1086
+ tail.text += delta;
1087
+ return;
1088
+ }
1089
+ entry.segments.push({ kind: "text", text: delta, finalized: false });
1090
+ };
1091
+
1092
+ /**
1093
+ * Canonicalize a streamed text segment from a completed assistant message.
1094
+ * When streaming produced a prefix of the final text (the common case),
1095
+ * the tail segment is overwritten and flipped to finalized so the next
1096
+ * render pipes it through Markdown. When the message arrived fully formed
1097
+ * with no deltas (non-streaming test path, synthetic notices), a fresh
1098
+ * finalized text segment is appended after any tool segments that may
1099
+ * have landed in this turn already. `replaceTail` forces the overwrite for
1100
+ * messages the chat loop rewrote after streaming (locked-turn markup
1101
+ * sanitation): the streamed tail is dead text there, not a prefix.
1102
+ */
1103
+ const canonicalizeMessageText = (
1104
+ entry: Extract<TranscriptEntry, { role: "assistant" }>,
1105
+ text: string,
1106
+ replaceTail = false,
1107
+ ): void => {
1108
+ if (text.length === 0) return;
1109
+ const tail = entry.segments[entry.segments.length - 1];
1110
+ if (tail?.kind === "text" && !tail.finalized && (replaceTail || text.startsWith(tail.text))) {
1111
+ tail.text = text;
1112
+ tail.finalized = true;
1113
+ // The streaming wrap cache assumes append-only text. This is the one
1114
+ // path that rewrites it wholesale, and finalized segments render through
1115
+ // Markdown instead, so the cache is dead here either way.
1116
+ delete tail.wrapCache;
1117
+ if (tail.md) tail.md.setText(text);
1118
+ return;
1119
+ }
1120
+ entry.segments.push({ kind: "text", text, finalized: true });
1121
+ };
1122
+
1123
+ /**
1124
+ * Append the turn's terminal-error marker as its own error segment so the
1125
+ * render path styles it in the error token rather than piping it through
1126
+ * Markdown as prose. Guards against a duplicate when the same marker arrives
1127
+ * twice for one settled turn.
1128
+ */
1129
+ const appendErrorSegment = (entry: Extract<TranscriptEntry, { role: "assistant" }>, text: string): void => {
1130
+ const tail = entry.segments[entry.segments.length - 1];
1131
+ if (tail?.kind === "error" && tail.text === text) return;
1132
+ entry.segments.push({ kind: "error", text });
1133
+ };
1134
+
1135
+ /**
1136
+ * Who advertises the fold key this frame: the newest folded worker card, or
1137
+ * the newest finished collapsed tool subline with no worker card behind it.
1138
+ * One surface at most, because the key reaches the newest foldable thing of
1139
+ * either kind, and a chord shown anywhere else would open something the
1140
+ * operator was not looking at. An already-open newest card advertises
1141
+ * nothing, since folding it again needs no invitation.
1142
+ */
1143
+ const expandHintOwner = (): { toolId: string | null; workerId: string | null } => {
1144
+ let workerMayOwn = true;
1145
+ for (let entryIndex = transcript.length - 1; entryIndex >= 0; entryIndex -= 1) {
1146
+ const entry = transcript[entryIndex];
1147
+ if (entry?.role === "worker") {
1148
+ return { toolId: null, workerId: workerMayOwn && entry.folded ? entry.state.assignmentId : null };
1149
+ }
1150
+ if (entry?.role !== "assistant") continue;
1151
+ for (let segIndex = entry.segments.length - 1; segIndex >= 0; segIndex -= 1) {
1152
+ const seg = entry.segments[segIndex];
1153
+ if (seg?.kind !== "tool") continue;
1154
+ if (seg.finished && !seg.expanded) return { toolId: seg.id, workerId: null };
1155
+ workerMayOwn = false;
1156
+ }
1157
+ }
1158
+ return { toolId: null, workerId: null };
1159
+ };
1160
+
1161
+ /**
1162
+ * Where a newly seen worker block belongs. An agent-origin run nests under
1163
+ * the assistant entry holding the tool call that spawned it, behind any
1164
+ * sibling blocks the same call already placed, so a fan-out reads top to
1165
+ * bottom in spawn order. Everything else, including a run whose parent call
1166
+ * is no longer in the transcript (a detached collect landing turns later),
1167
+ * appends at the tail. null means "append".
1168
+ */
1169
+ const workerInsertionIndex = (state: WorkerEntryState): number | null => {
1170
+ const parentToolCallId = state.parentToolCallId;
1171
+ if (parentToolCallId === undefined) return null;
1172
+ const owner = findToolSegmentOwner(parentToolCallId);
1173
+ if (owner === undefined) return null;
1174
+ const parentIndex = transcript.indexOf(owner.entry);
1175
+ if (parentIndex < 0) return null;
1176
+ let index = parentIndex + 1;
1177
+ while (transcript[index]?.role === "worker") index += 1;
1178
+ return index >= transcript.length ? null : index;
1179
+ };
1180
+
1181
+ /** True when the entry owns the tool the expand hint currently points at. */
1182
+ const entryContainsHint = (entry: TranscriptEntry, latestHintToolId: string | null): boolean =>
1183
+ latestHintToolId !== null &&
1184
+ entry.role === "assistant" &&
1185
+ entry.segments.some((segment) => segment.kind === "tool" && segment.id === latestHintToolId);
1186
+
1187
+ /** True when the entry renders at least one counting elapsed line this frame. */
1188
+ const entryHasRunningTool = (entry: TranscriptEntry): boolean =>
1189
+ (entry.role === "assistant" &&
1190
+ entry.segments.some(
1191
+ (segment) => segment.kind === "tool" && !segment.finished && segment.startedAtMs !== undefined,
1192
+ )) ||
1193
+ (entry.role === "replayBlock" && entry.isLive?.() === true);
1194
+
1195
+ /**
1196
+ * Settled entries whose render is a pure function of the base key. A live
1197
+ * worker block is excluded for the same reason a running tool is: the
1198
+ * reducer mutates its state object in place, so a cached render would keep
1199
+ * serving the answer as it looked several deltas ago. A replay block that
1200
+ * declares itself live is excluded on the same grounds.
1201
+ */
1202
+ const entryIsStable = (entry: TranscriptEntry): boolean =>
1203
+ entry.role === "user" ||
1204
+ (entry.role === "replayBlock" && entry.isLive?.() !== true) ||
1205
+ (entry.role === "worker" && !entry.state.pending) ||
1206
+ (entry.role === "assistant" &&
1207
+ !entry.pending &&
1208
+ !entry.segments.some((segment) => segment.kind === "tool" && !segment.finished));
1209
+
1210
+ const render = (width: number): string[] => {
1211
+ const startedAt = performance.now();
1212
+ const expandKey = resolveExpandKey();
1213
+ const verbosity = options.getOutputVerbosity?.() ?? "default";
1214
+ const nowMs = now();
1215
+ // `dirty` is set on mutation and never on a tick, so without time in the
1216
+ // key a running tool's elapsed counter advanced only when something
1217
+ // unrelated invalidated the panel. The tick is the same 100 ms bucket
1218
+ // dispatch-board.ts uses for running rows, taken off the injectable
1219
+ // clock so a fixed clock still produces byte-stable output, and it is
1220
+ // pinned to 0 whenever nothing is counting so a settled transcript
1221
+ // re-renders no more often than it did before.
1222
+ const tick = renderedRunningTool ? Math.floor(nowMs / 100) : 0;
1223
+ // The hit guard runs before any transcript scan. expandHintOwner
1224
+ // walks the whole transcript when the newest tool is expanded or absent, and
1225
+ // it is not part of the panel-level key, so computing it above the guard cost
1226
+ // a full scan per frame for a value the early return discards.
1227
+ if (
1228
+ !dirty &&
1229
+ cachedWidth === width &&
1230
+ cachedExpandKey === expandKey &&
1231
+ cachedVerbosity === verbosity &&
1232
+ cachedLiveToolOutput === liveToolOutput &&
1233
+ cachedTick === tick
1234
+ ) {
1235
+ options.onRenderMetrics?.({ durationMs: performance.now() - startedAt, cacheHit: true, entriesRendered: 0 });
1236
+ return cachedLines;
1237
+ }
1238
+ const { toolId: latestHintToolId, workerId: latestFoldedWorkerId } = expandHintOwner();
1239
+ // The hint id is deliberately NOT part of the shared key: it changes on
1240
+ // every finished collapsed tool, and keying every entry on it re-rendered
1241
+ // the entire transcript per tool completion. Only the entry that contains
1242
+ // the hint tool renders differently, so only that entry's key carries it.
1243
+ // The tick stays out of the entry key: a settled entry renders the same
1244
+ // bytes at every tick, and keying it on time would drop the entry cache
1245
+ // and the frozen prefix ten times a second. Only the panel-level guard
1246
+ // above is time-keyed, so a tick re-renders the live tail and nothing else.
1247
+ const baseKey = `${width}|${expandKey ?? ""}|${verbosity}|${liveToolOutput}`;
1248
+ const capacity = entryCacheCapacity();
1249
+ if (frozen !== null && frozen.key !== baseKey) frozen = null;
1250
+ const out: string[] = frozen === null ? [] : frozen.lines.slice();
1251
+ const startIndex = frozen === null ? 0 : frozen.through;
1252
+ // The freeze extends over the contiguous run of stable, hint-free leading
1253
+ // entries; it is captured after the loop from what this frame rendered.
1254
+ let freezeThrough = startIndex;
1255
+ let freezeLineCount = out.length;
1256
+ let freezeOpen = frozen !== null || startIndex === 0;
1257
+ let entriesRendered = 0;
1258
+ // Only entries at or past `startIndex` can hold a running tool: the frozen
1259
+ // prefix is by construction a run of stable entries, and stable means no
1260
+ // unfinished tool segment.
1261
+ let sawRunningTool = false;
1262
+ for (let i = startIndex; i < transcript.length; i += 1) {
1263
+ const entry = transcript[i];
1264
+ if (!entry) continue;
1265
+ if (!sawRunningTool && entryHasRunningTool(entry)) sawRunningTool = true;
1266
+ // Folded worker cards are a list, not a series of blocks: a fan-out of
1267
+ // five scouts costs five rows, which is what makes the folded default
1268
+ // worth having. Anything else keeps the blank line between entries.
1269
+ const previous = i > 0 ? transcript[i - 1] : undefined;
1270
+ const stacksOnPrevious =
1271
+ entry.role === "worker" &&
1272
+ previous?.role === "worker" &&
1273
+ workerEntryFolded(entry, verbosity) &&
1274
+ workerEntryFolded(previous, verbosity);
1275
+ if (i > 0 && !stacksOnPrevious) out.push("");
1276
+ const containsHint =
1277
+ entryContainsHint(entry, latestHintToolId) ||
1278
+ (entry.role === "worker" && entry.state.assignmentId === latestFoldedWorkerId);
1279
+ const entryKey = containsHint ? `${baseKey}|hint:${latestHintToolId}|${latestFoldedWorkerId}` : baseKey;
1280
+ const cached = entryRenderCache.get(entry);
1281
+ const cacheable = i >= transcript.length - capacity && entry.role !== "replayBlock" && entryIsStable(entry);
1282
+ if (cacheable && cached?.key === entryKey) {
1283
+ // A spread here is slower than a loop for large arrays and blows the
1284
+ // stack outright for a single entry that renders enough lines.
1285
+ for (const line of cached.lines) out.push(line);
1286
+ } else {
1287
+ entriesRendered += 1;
1288
+ const renderedEntry = renderEntryLines(
1289
+ entry,
1290
+ width,
1291
+ expandKey,
1292
+ latestHintToolId,
1293
+ latestFoldedWorkerId,
1294
+ nowMs,
1295
+ unboundedToolBodies,
1296
+ verbosity,
1297
+ liveToolOutput,
1298
+ );
1299
+ for (const line of renderedEntry) out.push(line);
1300
+ if (cacheable) {
1301
+ entryRenderCache.set(entry, { key: entryKey, lines: renderedEntry });
1302
+ while (entryRenderCache.size > capacity) {
1303
+ const oldest = entryRenderCache.keys().next().value;
1304
+ if (oldest === undefined) break;
1305
+ entryRenderCache.delete(oldest);
1306
+ }
1307
+ }
1308
+ }
1309
+ if (freezeOpen && i === freezeThrough && entryIsStable(entry) && !containsHint) {
1310
+ freezeThrough = i + 1;
1311
+ freezeLineCount = out.length;
1312
+ } else {
1313
+ freezeOpen = false;
1314
+ }
1315
+ }
1316
+ frozen = freezeThrough > 0 ? { lines: out.slice(0, freezeLineCount), through: freezeThrough, key: baseKey } : null;
1317
+ cachedLines = out;
1318
+ cachedWidth = width;
1319
+ cachedExpandKey = expandKey;
1320
+ cachedVerbosity = verbosity;
1321
+ cachedLiveToolOutput = liveToolOutput;
1322
+ cachedTick = tick;
1323
+ renderedRunningTool = sawRunningTool;
1324
+ dirty = false;
1325
+ options.onRenderMetrics?.({ durationMs: performance.now() - startedAt, cacheHit: false, entriesRendered });
1326
+ return out;
1327
+ };
1328
+
1329
+ return {
1330
+ appendUser(text: string): void {
1331
+ transcript.push({ role: "user", text });
1332
+ markDirty();
1333
+ },
1334
+ appendReplayBlock(renderBlock: ReplayBlockRenderer, isLive?: () => boolean): void {
1335
+ transcript.push({ role: "replayBlock", renderBlock, isLive });
1336
+ markDirty();
1337
+ },
1338
+ applyWorkerState(state: WorkerEntryState): void {
1339
+ const existing = workerEntries.get(state.assignmentId);
1340
+ if (existing !== undefined) {
1341
+ // The reducer mutated the same state object this entry already holds,
1342
+ // so nothing is re-linked; only the cached render is now stale.
1343
+ invalidateEntryCache(existing);
1344
+ markDirty();
1345
+ return;
1346
+ }
1347
+ const entry: WorkerTranscriptEntry = { role: "worker", state, folded: workerAskedByModel(state) };
1348
+ workerEntries.set(state.assignmentId, entry);
1349
+ const at = workerInsertionIndex(state);
1350
+ if (at === null) {
1351
+ transcript.push(entry);
1352
+ } else {
1353
+ transcript.splice(at, 0, entry);
1354
+ // A frozen prefix is a run of indices. Inserting inside it renumbers
1355
+ // every entry behind the cut, so the freeze has to go.
1356
+ if (frozen !== null && at < frozen.through) frozen = null;
1357
+ }
1358
+ markDirty();
1359
+ },
1360
+ workerStates(): ReadonlyArray<WorkerEntryState> {
1361
+ return [...workerEntries.values()].map((entry) => entry.state);
1362
+ },
1363
+ toggleLastToolExpanded(): boolean {
1364
+ // Ctrl+O owns the newest foldable thing, whichever kind it is. A worker
1365
+ // block the operator just watched land is what they mean by "expand
1366
+ // that", not the tool call two screens up that spawned it.
1367
+ for (let entryIndex = transcript.length - 1; entryIndex >= 0; entryIndex -= 1) {
1368
+ const entry = transcript[entryIndex];
1369
+ if (entry?.role === "worker") {
1370
+ entry.folded = !entry.folded;
1371
+ clearRenderCaches();
1372
+ markDirty();
1373
+ return true;
1374
+ }
1375
+ if (entry?.role !== "assistant") continue;
1376
+ for (let segIndex = entry.segments.length - 1; segIndex >= 0; segIndex -= 1) {
1377
+ const seg = entry.segments[segIndex];
1378
+ if (seg?.kind !== "tool") continue;
1379
+ seg.expanded = !seg.expanded;
1380
+ clearRenderCaches();
1381
+ markDirty();
1382
+ return true;
1383
+ }
1384
+ }
1385
+ return false;
1386
+ },
1387
+ toggleAllToolsExpanded(): boolean {
1388
+ const tools: ToolSegment[] = [];
1389
+ const workers: WorkerTranscriptEntry[] = [];
1390
+ for (const entry of transcript) {
1391
+ if (entry.role === "worker") {
1392
+ workers.push(entry);
1393
+ continue;
1394
+ }
1395
+ if (entry.role !== "assistant") continue;
1396
+ for (const seg of entry.segments) {
1397
+ if (seg.kind === "tool") tools.push(seg);
1398
+ }
1399
+ }
1400
+ if (tools.length === 0 && workers.length === 0) return false;
1401
+ const expand = tools.some((seg) => !seg.expanded) || workers.some((entry) => entry.folded);
1402
+ for (const seg of tools) seg.expanded = expand;
1403
+ for (const entry of workers) entry.folded = !expand;
1404
+ clearRenderCaches();
1405
+ markDirty();
1406
+ return true;
1407
+ },
1408
+ collapseAllTools(): void {
1409
+ for (const entry of transcript) {
1410
+ if (entry.role === "worker") {
1411
+ // The settled view for a worker is its origin default, not
1412
+ // universally folded: the operator's own run is the one block
1413
+ // /run exists to show them.
1414
+ entry.folded = workerAskedByModel(entry.state);
1415
+ continue;
1416
+ }
1417
+ if (entry.role !== "assistant") continue;
1418
+ for (const seg of entry.segments) {
1419
+ if (seg.kind === "tool") seg.expanded = false;
1420
+ }
1421
+ }
1422
+ clearRenderCaches();
1423
+ markDirty();
1424
+ },
1425
+ toggleLastThinking(): boolean {
1426
+ for (let entryIndex = transcript.length - 1; entryIndex >= 0; entryIndex -= 1) {
1427
+ const entry = transcript[entryIndex];
1428
+ if (entry?.role !== "assistant") continue;
1429
+ if (entry.thinking.length === 0) continue;
1430
+ entry.expandedThinking = entry.expandedThinking !== true;
1431
+ thinkingExpanded = entry.expandedThinking === true;
1432
+ clearRenderCaches();
1433
+ markDirty();
1434
+ return true;
1435
+ }
1436
+ thinkingExpanded = !thinkingExpanded;
1437
+ return true;
1438
+ },
1439
+ toggleAllThinking(): boolean {
1440
+ const entries: Array<Extract<TranscriptEntry, { role: "assistant" }>> = [];
1441
+ for (const entry of transcript) {
1442
+ if (entry.role === "assistant" && entry.thinking.length > 0) entries.push(entry);
1443
+ }
1444
+ if (entries.length === 0) {
1445
+ thinkingExpanded = !thinkingExpanded;
1446
+ return true;
1447
+ }
1448
+ const expand = entries.some((entry) => entry.expandedThinking !== true);
1449
+ for (const entry of entries) entry.expandedThinking = expand;
1450
+ thinkingExpanded = expand;
1451
+ clearRenderCaches();
1452
+ markDirty();
1453
+ return true;
1454
+ },
1455
+ isThinkingExpanded: () => thinkingExpanded,
1456
+ toggleLiveToolOutput(): boolean {
1457
+ liveToolOutput = !liveToolOutput;
1458
+ markDirty();
1459
+ return liveToolOutput;
1460
+ },
1461
+ reset(): void {
1462
+ transcript.length = 0;
1463
+ workerEntries.clear();
1464
+ clearRenderCaches();
1465
+ markDirty();
1466
+ },
1467
+ markToolReplayed(toolCallId: string): void {
1468
+ const owner = findToolSegmentOwner(toolCallId);
1469
+ if (!owner || owner.segment.finished) return;
1470
+ invalidateEntryCache(owner.entry);
1471
+ owner.segment.replayed = true;
1472
+ markDirty();
1473
+ },
1474
+ applyEvent(event: ChatLoopEvent): void {
1475
+ if (event.type === "agent_status") {
1476
+ return;
1477
+ }
1478
+ if (event.type === "notice") {
1479
+ // Transcript notices are first-class advisory lines, not assistant
1480
+ // messages: render with the bracketed-tag treatment replay lines get.
1481
+ if (event.surface !== "transcript") return;
1482
+ const text = event.text;
1483
+ transcript.push({
1484
+ role: "replayBlock",
1485
+ renderBlock: (width) => wrapTextWithAnsi(styleTaggedNotice(text), width),
1486
+ });
1487
+ markDirty();
1488
+ return;
1489
+ }
1490
+ if (event.type === "queued_user_turn") {
1491
+ // A queued steer or follow-up the engine just injected. Rendering it
1492
+ // here, at injection time, keeps the transcript in the order the
1493
+ // model saw: enqueue time shows the text only in the queue panel.
1494
+ transcript.push({ role: "user", text: event.text });
1495
+ markDirty();
1496
+ return;
1497
+ }
1498
+ if (event.type === "message_update") {
1499
+ const assistantEvent = event.assistantMessageEvent as {
1500
+ type?: unknown;
1501
+ contentIndex?: unknown;
1502
+ partial?: { content?: unknown };
1503
+ };
1504
+ if (
1505
+ assistantEvent.type !== "toolcall_start" &&
1506
+ assistantEvent.type !== "toolcall_delta" &&
1507
+ assistantEvent.type !== "toolcall_end"
1508
+ ) {
1509
+ return;
1510
+ }
1511
+ const index = typeof assistantEvent.contentIndex === "number" ? assistantEvent.contentIndex : -1;
1512
+ const content = Array.isArray(assistantEvent.partial?.content) ? assistantEvent.partial.content : [];
1513
+ const block = content[index];
1514
+ if (block === null || typeof block !== "object" || Array.isArray(block)) return;
1515
+ const streamed = block as { type?: unknown; id?: unknown; name?: unknown; arguments?: unknown };
1516
+ if (streamed.type !== "toolCall" || typeof streamed.id !== "string" || streamed.id.length === 0) return;
1517
+ const owner = findToolSegmentOwner(streamed.id);
1518
+ const existing = owner?.segment;
1519
+ if (existing && !existing.finished && !existing.executionStarted) {
1520
+ if (owner) invalidateEntryCache(owner.entry);
1521
+ existing.name = typeof streamed.name === "string" && streamed.name.length > 0 ? streamed.name : existing.name;
1522
+ existing.args = streamed.arguments ?? existing.args;
1523
+ existing.argsComplete = assistantEvent.type === "toolcall_end";
1524
+ } else if (existing === undefined) {
1525
+ const assistant = ensureAssistant();
1526
+ assistant.pending = true;
1527
+ assistant.segments.push({
1528
+ kind: "tool",
1529
+ id: streamed.id,
1530
+ name: typeof streamed.name === "string" && streamed.name.length > 0 ? streamed.name : "tool",
1531
+ args: streamed.arguments ?? {},
1532
+ finished: false,
1533
+ executionStarted: false,
1534
+ argsComplete: assistantEvent.type === "toolcall_end",
1535
+ isError: false,
1536
+ expanded: false,
1537
+ });
1538
+ }
1539
+ markDirty();
1540
+ return;
1541
+ }
1542
+ if (event.type === "text_delta") {
1543
+ const assistant = ensureAssistant();
1544
+ assistant.pending = true;
1545
+ appendTextDelta(assistant, event.delta);
1546
+ markDirty();
1547
+ return;
1548
+ }
1549
+ if (event.type === "thinking_delta") {
1550
+ // Capture for downstream consumers but never render inline.
1551
+ const assistant = ensureAssistant();
1552
+ assistant.pending = true;
1553
+ assistant.thinking += event.delta;
1554
+ assistant.expandedThinking = thinkingExpanded;
1555
+ markDirty();
1556
+ return;
1557
+ }
1558
+ if (event.type === "message_start" && event.message.role === "assistant") {
1559
+ ensureAssistant().pending = true;
1560
+ markDirty();
1561
+ return;
1562
+ }
1563
+ if (event.type === "tool_execution_start") {
1564
+ const streamedOwner = findToolSegmentOwner(event.toolCallId);
1565
+ if (streamedOwner !== undefined && !streamedOwner.segment.finished && !streamedOwner.segment.executionStarted) {
1566
+ invalidateEntryCache(streamedOwner.entry);
1567
+ const streamed = streamedOwner.segment;
1568
+ streamed.name = event.toolName;
1569
+ streamed.args = event.args;
1570
+ streamed.executionStarted = true;
1571
+ streamed.argsComplete = true;
1572
+ streamed.startedAtMs = now();
1573
+ streamed.expanded = classifyResourceRead(event.toolName, event.args) === null;
1574
+ markDirty();
1575
+ return;
1576
+ }
1577
+ // A model that reuses a tool-call id across calls would leave the
1578
+ // prior same-id segment unsettled (its end matches the first segment
1579
+ // on lookup). Settle any such orphan now, wherever it lives, so it
1580
+ // does not linger as a counting running line while this call runs.
1581
+ for (const entry of transcript) {
1582
+ if (entry.role !== "assistant") continue;
1583
+ for (const seg of entry.segments) {
1584
+ if (seg.kind === "tool" && seg.id === event.toolCallId && !seg.finished) settleUnfinishedToolSegment(seg);
1585
+ }
1586
+ }
1587
+ const assistant = ensureAssistant();
1588
+ // Compact resource reads (SKILL.md, CLIO-CODER.md, AGENTS.md, docs/) stay
1589
+ // collapsed to one labeled line until explicitly expanded.
1590
+ const expanded = classifyResourceRead(event.toolName, event.args) === null;
1591
+ assistant.pending = true;
1592
+ assistant.segments.push({
1593
+ kind: "tool",
1594
+ id: event.toolCallId,
1595
+ name: event.toolName,
1596
+ args: event.args,
1597
+ finished: false,
1598
+ executionStarted: true,
1599
+ argsComplete: true,
1600
+ isError: false,
1601
+ expanded,
1602
+ startedAtMs: now(),
1603
+ });
1604
+ markDirty();
1605
+ return;
1606
+ }
1607
+ if (event.type === "tool_approval_state") {
1608
+ // Only the entry that owns the segment can change. Clearing the whole
1609
+ // 256-entry cache re-rendered every settled turn in the transcript;
1610
+ // on a 400-turn session that was 26.7 ms per event, and
1611
+ // tool_execution_update fires on every output tick of a running command.
1612
+ const owner = findToolSegmentOwner(event.toolCallId);
1613
+ if (owner) invalidateEntryCache(owner.entry);
1614
+ const tool = owner?.segment;
1615
+ if (tool && !tool.finished) {
1616
+ tool.awaitingApproval = event.state === "awaiting-approval" ? true : undefined;
1617
+ tool.approvalView = event.state === "awaiting-approval" ? event.view : undefined;
1618
+ markDirty();
1619
+ }
1620
+ return;
1621
+ }
1622
+ if (event.type === "tool_execution_update") {
1623
+ // pi-agent emits `partialResult` as a cumulative AgentToolResult.
1624
+ // Preserve that full envelope so the renderer can use structured
1625
+ // progress details as well as content. Replace rather than append:
1626
+ // Pi's update semantics are cumulative, and appending would duplicate
1627
+ // every earlier snapshot.
1628
+ const owner = findToolSegmentOwner(event.toolCallId);
1629
+ if (owner) invalidateEntryCache(owner.entry);
1630
+ const tool = owner?.segment;
1631
+ if (tool && !tool.finished) {
1632
+ tool.partialResult = event.partialResult;
1633
+ }
1634
+ markDirty();
1635
+ return;
1636
+ }
1637
+ if (event.type === "tool_execution_end") {
1638
+ const owner = findToolSegmentOwner(event.toolCallId);
1639
+ if (owner) invalidateEntryCache(owner.entry);
1640
+ const tool = owner?.segment;
1641
+ if (tool) {
1642
+ tool.result = event.result;
1643
+ tool.isError = event.isError;
1644
+ // The chat loop enriches tool_execution_end with durationMs, the
1645
+ // persisted resultSummary (bytes, truncated, offloadPath,
1646
+ // observation counts), and the registry's admission verdict;
1647
+ // carry them so the ledger line and replay render identical facts.
1648
+ const enriched = event as {
1649
+ durationMs?: unknown;
1650
+ resultSummary?: unknown;
1651
+ outcome?: unknown;
1652
+ blockReason?: unknown;
1653
+ };
1654
+ // Settlement is that verdict, never an inference from result text.
1655
+ // The text of a tool result is the tool's own output: `node --test`
1656
+ // prints `cancelled 0` on every run and a linter can print
1657
+ // "blocked" for its own reasons, so matching those words there
1658
+ // labelled ordinary command failures as permission blocks and
1659
+ // suppressed the output that would have explained them.
1660
+ const settled = toolSettlement(enriched);
1661
+ tool.settlement = settled?.settlement;
1662
+ tool.blockReason = settled?.reason;
1663
+ tool.finished = true;
1664
+ // The true result replaces a synthetic settle; from here on the
1665
+ // segment is model-finished and immutable to later end events.
1666
+ tool.settledWithoutResult = undefined;
1667
+ if (typeof enriched.durationMs === "number" && Number.isFinite(enriched.durationMs)) {
1668
+ tool.durationMs = enriched.durationMs;
1669
+ } else if (tool.startedAtMs !== undefined) {
1670
+ const elapsed = Math.max(0, now() - tool.startedAtMs);
1671
+ if (elapsed > 0) tool.durationMs = elapsed;
1672
+ }
1673
+ if (
1674
+ enriched.resultSummary !== null &&
1675
+ typeof enriched.resultSummary === "object" &&
1676
+ !Array.isArray(enriched.resultSummary)
1677
+ ) {
1678
+ tool.resultSummary = enriched.resultSummary as Record<string, unknown>;
1679
+ }
1680
+ // Drop the streaming buffer once the final result has landed; the
1681
+ // expanded render switches to `renderToolExecution` and stays
1682
+ // stable instead of churning through partial-frame layout. A
1683
+ // denied park settles here too, so the awaiting styling must go.
1684
+ tool.partialResult = undefined;
1685
+ tool.awaitingApproval = undefined;
1686
+ tool.approvalView = undefined;
1687
+ }
1688
+ markDirty();
1689
+ return;
1690
+ }
1691
+ if (event.type === "message_end") {
1692
+ const current = transcript[transcript.length - 1];
1693
+ let completedStreamedArgs = false;
1694
+ if (current?.role === "assistant") {
1695
+ for (const segment of current.segments) {
1696
+ if (segment.kind !== "tool" || segment.finished || segment.executionStarted || segment.argsComplete) continue;
1697
+ segment.argsComplete = true;
1698
+ completedStreamedArgs = true;
1699
+ }
1700
+ if (completedStreamedArgs) invalidateEntryCache(current);
1701
+ }
1702
+ const text = extractAssistantText(event.message);
1703
+ const thinking = extractAssistantThinking(event.message);
1704
+ const extractedTerminalError = extractAssistantTerminalError(event.message);
1705
+ const terminalError =
1706
+ current?.role === "assistant"
1707
+ ? scopeTerminalErrorAfterSuccessfulTool(current, extractedTerminalError)
1708
+ : extractedTerminalError;
1709
+ const usage = assistantUsage(event.message);
1710
+ if (text.length === 0 && thinking.length === 0 && terminalError.length === 0 && usage === undefined) {
1711
+ if (completedStreamedArgs) markDirty();
1712
+ return;
1713
+ }
1714
+ const assistant = ensureAssistant();
1715
+ // message_end rewrites exactly one entry: the assistant it lands on.
1716
+ invalidateEntryCache(assistant);
1717
+ if (usage !== undefined) assistant.turnUsage = usage;
1718
+ if (terminalError.length > 0) assistant.isError = true;
1719
+ if (thinking.length > 0) {
1720
+ assistant.thinking = thinking;
1721
+ assistant.expandedThinking = thinkingExpanded;
1722
+ }
1723
+ // The chat loop marks messages it sanitized after streaming (dead
1724
+ // tool-call markup on a synthesis-locked turn); the streamed tail
1725
+ // must be replaced, not kept alongside a duplicate segment.
1726
+ const sanitized = (event as { lockedSynthesisSanitized?: unknown }).lockedSynthesisSanitized === true;
1727
+ if (text.length > 0) canonicalizeMessageText(assistant, text, sanitized);
1728
+ if (terminalError.length > 0) appendErrorSegment(assistant, terminalError);
1729
+ markDirty();
1730
+ return;
1731
+ }
1732
+ if (event.type === "retry_status") {
1733
+ const last = transcript[transcript.length - 1];
1734
+ if (last?.role === "retryStatus" && last.status.attempt === event.status.attempt) {
1735
+ last.status = event.status;
1736
+ } else {
1737
+ transcript.push({ role: "retryStatus", status: event.status });
1738
+ }
1739
+ markDirty();
1740
+ return;
1741
+ }
1742
+ if (event.type === "agent_end") {
1743
+ // agent_end can touch many entries, but it names every one it touches:
1744
+ // the usage caption's target, the later entries whose caption it
1745
+ // removes, and any entry it settles or un-pends below. Each is
1746
+ // invalidated at the point of mutation instead of dropping the whole
1747
+ // cache and re-rendering settled history.
1748
+ const runUsage = aggregateAssistantUsage(event.messages);
1749
+ if (runUsage !== undefined) {
1750
+ // The usage line is a caption on rendered output, so it goes on the
1751
+ // last entry that rendered any. A mid-turn notice splits entries, and
1752
+ // a turn that stops after one leaves an empty tail entry behind: the
1753
+ // run total landed there while the entry above kept its own
1754
+ // message_end line, so one turn printed the identical line twice, once
1755
+ // on each side of the notice. Entries after the caption rendered
1756
+ // nothing and must not carry a second copy.
1757
+ const index = lastAssistantIndex(transcript, { withOutput: true }) ?? lastAssistantIndex(transcript);
1758
+ const target = index === null ? undefined : transcript[index];
1759
+ if (target?.role === "assistant") {
1760
+ invalidateEntryCache(target);
1761
+ target.turnUsage = runUsage;
1762
+ }
1763
+ for (let after = (index ?? -1) + 1; after < transcript.length; after += 1) {
1764
+ const later = transcript[after];
1765
+ if (later?.role !== "assistant") continue;
1766
+ if (later.turnUsage !== undefined) invalidateEntryCache(later);
1767
+ delete later.turnUsage;
1768
+ }
1769
+ }
1770
+ // The run is over: no tool can still be executing anywhere in the
1771
+ // transcript, not just in the tail entry (a mid-turn notice splits
1772
+ // entries). Settle any tool segment whose `tool_execution_end` never
1773
+ // arrived (blocked at admission, or cut off by an abort) so the
1774
+ // ledger never leaves a running line counting past the turn's end,
1775
+ // and clear `pending` everywhere so no earlier entry keeps rendering
1776
+ // live thinking or status.
1777
+ const runWasAborted =
1778
+ Array.isArray(event.messages) &&
1779
+ event.messages.some(
1780
+ (message) =>
1781
+ message && typeof message === "object" && (message as { stopReason?: unknown }).stopReason === "aborted",
1782
+ );
1783
+ for (const entry of transcript) {
1784
+ if (entry.role !== "assistant") continue;
1785
+ for (const seg of entry.segments) {
1786
+ if (seg.kind === "tool" && !seg.finished) {
1787
+ invalidateEntryCache(entry);
1788
+ settleUnfinishedToolSegment(seg, runWasAborted ? "aborted" : "orphaned");
1789
+ }
1790
+ }
1791
+ if (entry.pending) {
1792
+ invalidateEntryCache(entry);
1793
+ entry.pending = false;
1794
+ entry.statusLine = null;
1795
+ }
1796
+ }
1797
+ markDirty();
1798
+ }
1799
+ },
1800
+ setStatusLine(line): void {
1801
+ if (line) {
1802
+ const assistant = ensureAssistant();
1803
+ assistant.pending = true;
1804
+ assistant.statusLine = line;
1805
+ markDirty();
1806
+ return;
1807
+ }
1808
+ const last = transcript[transcript.length - 1];
1809
+ if (last && last.role === "assistant") {
1810
+ unfreezeTail();
1811
+ last.statusLine = null;
1812
+ markDirty();
1813
+ }
1814
+ },
1815
+ render,
1816
+ invalidate(): void {
1817
+ markDirty();
1818
+ },
1819
+ };
1820
+ }