@iowarp/clio-coder 0.5.0 → 0.5.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 (312) hide show
  1. package/.claude-plugin/marketplace.json +4 -4
  2. package/CHANGELOG.md +35 -0
  3. package/CONTRIBUTING.md +9 -0
  4. package/README.md +12 -9
  5. package/ROADMAP.md +30 -5
  6. package/dist/{acp-KS7ARGV6.js → acp-F6PGOFZ4.js} +2 -2
  7. package/dist/{agents-YOGJ2YEJ.js → agents-ZHP4R6EX.js} +22 -21
  8. package/dist/assets/codewiki.json +1 -1
  9. package/dist/{auth-CEDCO6O6.js → auth-N75U2H3Z.js} +4 -4
  10. package/dist/{chunk-D3KWFOUT.js → chunk-23UIXH6Z.js} +3 -3
  11. package/dist/{chunk-ZR5YA6ZF.js → chunk-36XVK5P5.js} +5 -5
  12. package/dist/{chunk-AVSR2HKI.js → chunk-3DZ4LPLS.js} +6 -6
  13. package/dist/{chunk-LYJJ6546.js → chunk-3GSPCWFX.js} +12 -12
  14. package/dist/{chunk-CRHPCPZP.js → chunk-4KC4CHKJ.js} +2 -2
  15. package/dist/{chunk-4VDHLCF5.js → chunk-4WJBWKMI.js} +2 -2
  16. package/dist/{chunk-KBUTSJSR.js → chunk-4XBYHXGK.js} +2 -2
  17. package/dist/{chunk-6W75D37F.js → chunk-53P775GE.js} +6 -3
  18. package/dist/{chunk-XZWSP67B.js → chunk-5APYAXCB.js} +2 -2
  19. package/dist/{chunk-D65E25JG.js → chunk-67HPYNTJ.js} +2 -2
  20. package/dist/{chunk-5JQR5B4D.js → chunk-6KV4KCQZ.js} +4 -4
  21. package/dist/{chunk-V4XJWDHR.js → chunk-6UKFRTXI.js} +2 -1
  22. package/dist/{chunk-NDPF2N2L.js → chunk-7DW7CJW2.js} +2 -2
  23. package/dist/{chunk-5KQEUIWM.js → chunk-7LUEQODV.js} +4 -4
  24. package/dist/{chunk-LNOXCGN2.js → chunk-7UFDIOBT.js} +3 -3
  25. package/dist/{chunk-DXSETN72.js → chunk-7WUGNN43.js} +2 -2
  26. package/dist/{chunk-V35KZDUZ.js → chunk-AA3GRZWS.js} +2 -2
  27. package/dist/{chunk-X4CEFOSG.js → chunk-ADO3OQIC.js} +4 -4
  28. package/dist/{chunk-UX2L5CUD.js → chunk-AI36V3ON.js} +3 -3
  29. package/dist/{chunk-B7GY6QER.js → chunk-B26OUMJW.js} +2 -2
  30. package/dist/{chunk-LAR7DX5E.js → chunk-B5TASTCK.js} +153 -62
  31. package/dist/{chunk-TGWZXJ7L.js → chunk-BEMQK5Q7.js} +92 -84
  32. package/dist/{chunk-RNDCROSG.js → chunk-CNXU7TR2.js} +3 -3
  33. package/dist/{chunk-3JLTRPN7.js → chunk-CVMUXT64.js} +35 -66
  34. package/dist/{chunk-733ZBT5P.js → chunk-E74MJF6G.js} +4 -4
  35. package/dist/{chunk-T2MDZES3.js → chunk-EBMTBE76.js} +3 -3
  36. package/dist/{chunk-OZ76TLGU.js → chunk-EEITU7S6.js} +2 -2
  37. package/dist/{chunk-GRHOXDKB.js → chunk-ELDJWJKM.js} +97 -14
  38. package/dist/{chunk-GCQFN7YI.js → chunk-EMYEQLWO.js} +2 -2
  39. package/dist/{chunk-LTWH2ACR.js → chunk-EQPFEDFV.js} +222 -127
  40. package/dist/{chunk-TDGYTWM6.js → chunk-F4I3H56H.js} +2 -2
  41. package/dist/chunk-F6KCJO3U.js +523 -0
  42. package/dist/{chunk-FGSQHUCK.js → chunk-F6VKWTTY.js} +5 -5
  43. package/dist/{chunk-UJD5MPP6.js → chunk-FLNXQQ5B.js} +2 -2
  44. package/dist/{chunk-EX57VVWW.js → chunk-FOSRJ2VZ.js} +4 -4
  45. package/dist/{chunk-ZARZ4POR.js → chunk-G5ZDQQE3.js} +3 -3
  46. package/dist/{chunk-HQVN7G4F.js → chunk-G62JBPXN.js} +7 -7
  47. package/dist/{chunk-ZJPORZTC.js → chunk-GIAI7A2K.js} +2 -2
  48. package/dist/chunk-GPTR6OTU.js +2551 -0
  49. package/dist/{chunk-EZD2BXGP.js → chunk-GUFQPTQK.js} +2 -2
  50. package/dist/{chunk-OPD7GL6F.js → chunk-H6FQVNZC.js} +8 -7
  51. package/dist/chunk-H7REJ7L5.js +80 -0
  52. package/dist/{chunk-EYTLV3W3.js → chunk-HTWTORF7.js} +2 -2
  53. package/dist/{chunk-DF6PI6GN.js → chunk-I6UKQPGS.js} +3 -3
  54. package/dist/{chunk-ISHUS7HC.js → chunk-JRW236JT.js} +3 -3
  55. package/dist/{chunk-DE2LU267.js → chunk-KKAJGT3B.js} +3 -2
  56. package/dist/{chunk-OV62D6KO.js → chunk-KQMYL5GR.js} +3 -3
  57. package/dist/{chunk-QDUUOZ3S.js → chunk-KWMFC7FN.js} +3 -3
  58. package/dist/{chunk-R7FGRZNF.js → chunk-LXYHCCIZ.js} +9 -9
  59. package/dist/{chunk-NFSK2VNU.js → chunk-LZMTVVN4.js} +2 -2
  60. package/dist/{chunk-62UK7AGW.js → chunk-M5PMRZSR.js} +397 -44
  61. package/dist/chunk-MMZMM6SW.js +131 -0
  62. package/dist/{chunk-FFYTC2KQ.js → chunk-MNABNEBX.js} +2 -2
  63. package/dist/{chunk-XMDBTYCH.js → chunk-N3CWOGVF.js} +3 -3
  64. package/dist/{chunk-VYRQHORQ.js → chunk-NMQSO6Z6.js} +4 -3
  65. package/dist/{chunk-R2UUNSNQ.js → chunk-O3ZAF7UY.js} +18 -5
  66. package/dist/{chunk-4SYHGVDE.js → chunk-OQNCLZI4.js} +4 -4
  67. package/dist/{chunk-EG4ARXPC.js → chunk-OX6QTA4N.js} +3 -3
  68. package/dist/{chunk-P4GNZPXO.js → chunk-OYUS3UZP.js} +2 -2
  69. package/dist/{chunk-OQ2EOAHA.js → chunk-PF36KOGI.js} +291 -112
  70. package/dist/{chunk-55RMDDFM.js → chunk-PGQG7ZMW.js} +2 -2
  71. package/dist/{chunk-CBAQPTDU.js → chunk-PIU6BXXW.js} +2 -2
  72. package/dist/{chunk-EKV4UBCM.js → chunk-PZBQUJ2F.js} +2 -2
  73. package/dist/{chunk-7RET77VB.js → chunk-Q2HKY32Y.js} +2 -2
  74. package/dist/{chunk-U6YTUVOX.js → chunk-Q4RMNWMZ.js} +4 -4
  75. package/dist/chunk-Q6FRR3AQ.js +48 -0
  76. package/dist/{chunk-6RJMYYIE.js → chunk-QJSIAJBY.js} +3 -3
  77. package/dist/{chunk-DOUISWEE.js → chunk-QOJ4ERMU.js} +5 -5
  78. package/dist/{chunk-IFPDAD7R.js → chunk-RWSRKXLT.js} +3 -3
  79. package/dist/{chunk-T33IYTZM.js → chunk-SAASVMIY.js} +3 -3
  80. package/dist/{chunk-2DTIWSAW.js → chunk-SEQDH6JC.js} +3 -3
  81. package/dist/{chunk-BRCOSE7O.js → chunk-SZUO5BSE.js} +2 -2
  82. package/dist/{chunk-HOEDA42N.js → chunk-T7HZ7KJF.js} +2 -2
  83. package/dist/{chunk-OJXOA6YU.js → chunk-TL6LNERH.js} +2 -2
  84. package/dist/{chunk-UCINIRHW.js → chunk-TPCZXWLT.js} +3 -3
  85. package/dist/{chunk-ZUKUCZYZ.js → chunk-TQ2KTH4A.js} +2 -2
  86. package/dist/{chunk-4WOOTKFD.js → chunk-U3FETNQB.js} +341 -20
  87. package/dist/{chunk-5B3RTYAK.js → chunk-UGDSB4AN.js} +2 -2
  88. package/dist/{chunk-VKHLUZNO.js → chunk-UZ7YBL43.js} +3 -3
  89. package/dist/{chunk-QNUQ7K7D.js → chunk-VKMSEO7Y.js} +4 -4
  90. package/dist/{chunk-IN7DGBVS.js → chunk-VLX5VZ35.js} +5 -5
  91. package/dist/{chunk-SJGS3GDI.js → chunk-WAGBMMNX.js} +5 -5
  92. package/dist/{chunk-BFOSV5EZ.js → chunk-WX2YCH7F.js} +2 -2
  93. package/dist/{chunk-3LT34CAM.js → chunk-XBIGUILU.js} +32 -17
  94. package/dist/{chunk-XDHUDE5K.js → chunk-XKYBFRWR.js} +5 -5
  95. package/dist/{chunk-U5QU5ZOD.js → chunk-XYPWFSU5.js} +3 -3
  96. package/dist/{chunk-YBUECLAF.js → chunk-YEJQDODA.js} +2 -2
  97. package/dist/{chunk-YQ6XEFVK.js → chunk-Z74OGONW.js} +39 -7
  98. package/dist/{chunk-I4ELN5BX.js → chunk-ZTNOEJRI.js} +2 -2
  99. package/dist/cli/index.js +26 -26
  100. package/dist/{clio-K2PBVCEM.js → clio-4IKFE25F.js} +2 -2
  101. package/dist/{clio-context-tools-GR4OHFSO.js → clio-context-tools-OHJXYX4R.js} +25 -23
  102. package/dist/{code-nav-4X3OI6DK.js → code-nav-XPM3MY5I.js} +7 -7
  103. package/dist/{config-Q5J7UBAB.js → config-343YS25T.js} +43 -41
  104. package/dist/{config-graph-P3555574.js → config-graph-WI263KDX.js} +43 -41
  105. package/dist/{configure-KIJ32ZO6.js → configure-GZDG4HER.js} +21 -21
  106. package/dist/{context-LFBQOSXT.js → context-4OFO7N7Y.js} +12 -12
  107. package/dist/{context-WBH3KSVR.js → context-JLK2RLOG.js} +42 -40
  108. package/dist/{context-NNLBES2Y.js → context-O6DIWCF6.js} +23 -21
  109. package/dist/{context-clear-YOHMQ6SQ.js → context-clear-MR7KOGQI.js} +42 -40
  110. package/dist/{context-working-set-MRAEVDNV.js → context-working-set-YZAD3KWO.js} +14 -13
  111. package/dist/{data-tool-JJPBGMQJ.js → data-tool-EKD7DFHG.js} +7 -7
  112. package/dist/{detail-RHJOTGN4.js → detail-YZPHFEVJ.js} +43 -41
  113. package/dist/{dispatch-runner-NC7PYNIO.js → dispatch-runner-VKVXVTWJ.js} +44 -42
  114. package/dist/{doctor-S5KZ2GTN.js → doctor-53AVQXAO.js} +17 -16
  115. package/dist/{doctor-deep-2EJQSHIZ.js → doctor-deep-BLRB2NCG.js} +5 -5
  116. package/dist/{eval-3D6M36N7.js → eval-P3BQSLQE.js} +21 -20
  117. package/dist/{evidence-XNVPPLVK.js → evidence-6LLI6SZH.js} +44 -42
  118. package/dist/{evidence-6IYN52ET.js → evidence-HTN3JLUO.js} +42 -40
  119. package/dist/{evidence-JVECPI3F.js → evidence-NJRE3R5H.js} +42 -40
  120. package/dist/{evolve-TLPUDJDM.js → evolve-WWZHJVJP.js} +42 -40
  121. package/dist/{fleet-WKVBF5SZ.js → fleet-GGZ4BVJE.js} +64 -62
  122. package/dist/{fleet-6RJWIOEO.js → fleet-QG3IIWWM.js} +44 -42
  123. package/dist/{fleet-commands-L6WFT7KZ.js → fleet-commands-J2ZIKISQ.js} +7 -7
  124. package/dist/{fleet-decisions-BTQMWU5A.js → fleet-decisions-Y2Q7375Q.js} +8 -8
  125. package/dist/{fleet-graph-553DZ67Q.js → fleet-graph-TCBCCCBP.js} +11 -11
  126. package/dist/{fleet-inspect-236FZPRB.js → fleet-inspect-KOLKBC56.js} +44 -42
  127. package/dist/{fleet-preflight-NZQHABWO.js → fleet-preflight-44UNCC4Y.js} +27 -25
  128. package/dist/{fleet-validate-7NLAE26R.js → fleet-validate-K2FXRZVA.js} +13 -13
  129. package/dist/{fleet-verify-E3RO6A5W.js → fleet-verify-AS444WDW.js} +42 -40
  130. package/dist/{fleet-view-KMZQY7YJ.js → fleet-view-X4JU4J6O.js} +43 -41
  131. package/dist/gui/ops-worker.js +9 -9
  132. package/dist/gui/reads-worker.js +9 -8
  133. package/dist/{init-DV5B6HQG.js → init-VSOWFHVL.js} +54 -52
  134. package/dist/{interactive-OXGYZYAS.js → interactive-JRCBENLZ.js} +1733 -577
  135. package/dist/{interop-J7UOBOSF.js → interop-JR6BCY7R.js} +10 -10
  136. package/dist/{inventory-OEGIL6PY.js → inventory-3JUGLVJI.js} +43 -41
  137. package/dist/{library-ROZAMUIL.js → library-7BOH3Y7N.js} +15 -15
  138. package/dist/{library-B3UPGY4V.js → library-7PFONXUI.js} +9 -9
  139. package/dist/{library-MXQA5DLT.js → library-TUYZMFIA.js} +7 -7
  140. package/dist/{library-import-2LTWOVBD.js → library-import-TJVXKLDK.js} +10 -10
  141. package/dist/{library-inventory-LJ3XHLWO.js → library-inventory-MD3AYTP4.js} +9 -9
  142. package/dist/{library-validation-DARHHUNW.js → library-validation-ZDLITEA3.js} +7 -7
  143. package/dist/{mcp-2GSPYQ54.js → mcp-EFCWUPUR.js} +3 -3
  144. package/dist/{memory-J3STUUIK.js → memory-Q2YVOKK5.js} +42 -40
  145. package/dist/{models-Y3GMFJ2V.js → models-M7BOCW7F.js} +17 -16
  146. package/dist/{monitor-LHNH577J.js → monitor-2HD3MC67.js} +47 -45
  147. package/dist/{orchestrator-DTAPXULA.js → orchestrator-OUW3ZN6Y.js} +1894 -226
  148. package/dist/{panes-A743ONZ5.js → panes-TTPPSYCL.js} +3 -3
  149. package/dist/{preload-OJBMAIXF.js → preload-A47C2NUF.js} +42 -40
  150. package/dist/{providers-CMETPZNS.js → providers-T43W7EL4.js} +4 -4
  151. package/dist/{resources-FY6XYFN2.js → resources-TP6X2V6W.js} +20 -11
  152. package/dist/{run-H64MEVTA.js → run-LDSBPMCS.js} +61 -59
  153. package/dist/{share-CAFMMQLM.js → share-URHKKMHR.js} +9 -9
  154. package/dist/{skills-ERKQUC7L.js → skills-E477MOOO.js} +13 -11
  155. package/dist/{skills-eval-CQ23MYQK.js → skills-eval-AIBR2L7H.js} +583 -93
  156. package/dist/{skills-inventory-TVUYJDVN.js → skills-inventory-HLMKY3R5.js} +13 -11
  157. package/dist/{slash-commands-ROG3KIFJ.js → slash-commands-LIIZKA5Y.js} +28 -26
  158. package/dist/{startup-background-KDWCQMWT.js → startup-background-JR2KDK6R.js} +43 -41
  159. package/dist/{steer-KKYXJSH4.js → steer-D7RTAPCM.js} +3 -3
  160. package/dist/{system-ZDW44ZGX.js → system-X2P5N4IY.js} +14 -13
  161. package/dist/{targets-AJWA6E7Z.js → targets-VFFAKUFM.js} +25 -24
  162. package/dist/{tasks-6L2JJRC5.js → tasks-ERAWZA22.js} +6 -6
  163. package/dist/{terminal-lease-YFUSDPAD.js → terminal-lease-V2N2BIKQ.js} +2 -2
  164. package/dist/{trace-LA5MVR4Y.js → trace-MA5BT7XY.js} +3 -3
  165. package/dist/{usage-XSILDHKF.js → usage-WYFY2L6M.js} +50 -48
  166. package/dist/{verifiers-ECA5GIHL.js → verifiers-TYIW2XKF.js} +7 -7
  167. package/dist/{verify-R6QQCCEB.js → verify-242UFYTP.js} +6 -6
  168. package/dist/{web-fetch-PVZNMBOB.js → web-fetch-LRAGYU6Y.js} +3 -3
  169. package/dist/{wiki-generate-RP22WP5K.js → wiki-generate-OFKOX2T6.js} +53 -51
  170. package/dist/worker/entry.js +31 -29
  171. package/docs/architecture/architecture.md +1 -0
  172. package/docs/architecture/context-engine.md +4 -2
  173. package/docs/architecture/observability.md +6 -4
  174. package/docs/architecture/prompt-envelope-and-tools.md +1 -1
  175. package/docs/architecture/session-lifecycle.md +1 -1
  176. package/docs/architecture/tui-design.md +12 -9
  177. package/docs/gui/parity/02-slash-and-surfaces.md +1 -1
  178. package/docs/guide/commands-and-modes.md +91 -5
  179. package/docs/guide/configuration-and-targets.md +2 -2
  180. package/docs/guide/context-continuity.md +49 -0
  181. package/docs/guide/proactive-memory.md +6 -3
  182. package/library/registry.yaml +292 -32
  183. package/library/skills/README.md +27 -2
  184. package/library/skills/context/context-handoff/SKILL.md +10 -4
  185. package/library/skills/meta/clio-coder-dev/SKILL.md +92 -88
  186. package/library/skills/meta/clio-coder-dev/evals.md +51 -46
  187. package/library/skills/meta/clio-coder-dev/plugin.json +2 -2
  188. package/library/skills/meta/clio-coder-dev/references/change-map.md +49 -0
  189. package/library/skills/meta/clio-coder-test/SKILL.md +89 -142
  190. package/library/skills/meta/clio-coder-test/evals.md +58 -62
  191. package/library/skills/meta/clio-coder-test/plugin.json +2 -2
  192. package/library/skills/meta/clio-coder-test/references/harness.md +3 -3
  193. package/library/skills/meta/clio-coder-test/references/lifecycle-validation.md +30 -0
  194. package/library/skills/meta/clio-coder-test/references/test-map.md +60 -89
  195. package/library/skills/registry.yaml +5 -5
  196. package/library/skills/skill-marketplace.json +12 -14
  197. package/package.json +1 -1
  198. package/src/cli/skills-eval.ts +1064 -54
  199. package/src/cli/usage.ts +2 -2
  200. package/src/core/clio-repo.ts +3 -0
  201. package/src/core/tool-names.ts +1 -0
  202. package/src/domains/context/budget/inspection.ts +9 -0
  203. package/src/domains/context/budget/live-view.ts +418 -0
  204. package/src/domains/context/budget/pressure.ts +393 -0
  205. package/src/domains/context/budget/request-fit.ts +15 -0
  206. package/src/domains/evidence/build.ts +24 -0
  207. package/src/domains/gateway/mcp/client.ts +1 -1
  208. package/src/domains/memory/commit-state.ts +160 -0
  209. package/src/domains/memory/operations.ts +18 -10
  210. package/src/domains/memory/prompt-cache.ts +95 -0
  211. package/src/domains/memory/prompt-section.ts +50 -7
  212. package/src/domains/memory/relevance.ts +93 -0
  213. package/src/domains/memory/restoration.ts +78 -0
  214. package/src/domains/memory/store.ts +39 -1
  215. package/src/domains/middleware/memory-intervention.ts +116 -24
  216. package/src/domains/observability/background-memory-usage.ts +1 -1
  217. package/src/domains/observability/cost.ts +3 -3
  218. package/src/domains/observability/extension.ts +1 -1
  219. package/src/domains/observability/metrics.ts +1 -1
  220. package/src/domains/prompts/compiler.ts +1 -1
  221. package/src/domains/prompts/extension.ts +38 -1
  222. package/src/domains/quota/anthropic-max-provider.ts +134 -0
  223. package/src/domains/quota/anthropic-usage.ts +220 -0
  224. package/src/domains/quota/antigravity-provider.ts +339 -0
  225. package/src/domains/quota/cache.ts +87 -0
  226. package/src/domains/quota/claude-code-provider.ts +169 -0
  227. package/src/domains/quota/codex-provider.ts +237 -0
  228. package/src/domains/quota/presentation.ts +240 -0
  229. package/src/domains/quota/registry.ts +23 -0
  230. package/src/domains/quota/service.ts +93 -0
  231. package/src/domains/quota/summary-feed.ts +86 -0
  232. package/src/domains/quota/types.ts +84 -0
  233. package/src/domains/resources/index.ts +11 -0
  234. package/src/domains/resources/skills/catalog-view.ts +571 -0
  235. package/src/domains/resources/skills/lexical-match.ts +136 -0
  236. package/src/domains/resources/skills/loader.ts +38 -0
  237. package/src/domains/resources/skills/promotion.ts +1 -55
  238. package/src/domains/resources/skills/provenance-pin.ts +50 -20
  239. package/src/domains/safety/action-classifier.ts +1 -0
  240. package/src/domains/session/compaction/branch-summary.ts +4 -1
  241. package/src/domains/session/compaction/compact.ts +18 -2
  242. package/src/domains/session/compaction/cut-point.ts +20 -1
  243. package/src/domains/session/compaction/tokens.ts +48 -3
  244. package/src/domains/session/context-accounting.ts +8 -1
  245. package/src/domains/session/continuity/carry.ts +59 -0
  246. package/src/domains/session/continuity/contract.ts +592 -0
  247. package/src/domains/session/continuity/evidence.ts +290 -0
  248. package/src/domains/session/continuity/fold.ts +1075 -0
  249. package/src/domains/session/continuity/note.ts +79 -0
  250. package/src/domains/session/continuity/operator-request.ts +104 -0
  251. package/src/domains/session/continuity/persistence.ts +408 -0
  252. package/src/domains/session/continuity/ports.ts +231 -0
  253. package/src/domains/session/continuity/projection.ts +538 -0
  254. package/src/domains/session/continuity/validate.ts +352 -0
  255. package/src/domains/session/entries.ts +50 -3
  256. package/src/domains/session/index.ts +48 -0
  257. package/src/domains/session/migrations/index.ts +7 -3
  258. package/src/domains/session/tree/fork.ts +15 -1
  259. package/src/domains/session/usage.ts +2 -2
  260. package/src/engine/acp/commands.ts +1 -1
  261. package/src/engine/agent.ts +133 -9
  262. package/src/engine/session.ts +13 -6
  263. package/src/entry/orchestrator.ts +116 -33
  264. package/src/interactive/chat-loop-messages.ts +2 -2
  265. package/src/interactive/chat-loop.ts +219 -63
  266. package/src/interactive/chat-renderer.ts +99 -8
  267. package/src/interactive/context-overlay.ts +24 -3
  268. package/src/interactive/continuity-controller.ts +526 -0
  269. package/src/interactive/dispatch-board.ts +21 -3
  270. package/src/interactive/footer/dashboard.ts +25 -1
  271. package/src/interactive/footer/key-hints.ts +2 -2
  272. package/src/interactive/footer/pages.ts +70 -27
  273. package/src/interactive/footer/widgets.ts +32 -7
  274. package/src/interactive/footer-panel.ts +1 -1
  275. package/src/interactive/interactive-application.ts +7 -2
  276. package/src/interactive/interactive-input-runtime.ts +2 -2
  277. package/src/interactive/interactive-presentation.ts +15 -0
  278. package/src/interactive/interactive-slash-runtime.ts +45 -8
  279. package/src/interactive/interactive-tickers.ts +7 -1
  280. package/src/interactive/model-session-replay.ts +130 -3
  281. package/src/interactive/output-reserve.ts +35 -0
  282. package/src/interactive/overlay-general-openers.ts +12 -8
  283. package/src/interactive/overlay-key-routing.ts +6 -9
  284. package/src/interactive/overlay-lifecycle.ts +10 -6
  285. package/src/interactive/overlay-session-lifecycle.ts +21 -14
  286. package/src/interactive/quota-view.ts +229 -0
  287. package/src/interactive/session-last-turn.ts +1 -1
  288. package/src/interactive/session-transcript.ts +2 -1
  289. package/src/interactive/session-usage-reseed.ts +2 -2
  290. package/src/interactive/side-question.ts +2 -2
  291. package/src/interactive/slash-commands.ts +28 -9
  292. package/src/interactive/turn-context.ts +686 -63
  293. package/src/interactive/turn-middleware.ts +39 -6
  294. package/src/interactive/turn-persistence.ts +15 -2
  295. package/src/interactive/turn-prewarm.ts +1 -1
  296. package/src/interactive/turn-runtime.ts +48 -5
  297. package/src/interactive/{cost-overlay.ts → usage-overlay.ts} +121 -33
  298. package/src/interactive/welcome-dashboard.ts +26 -3
  299. package/src/tools/agent-tools.ts +3 -1
  300. package/src/tools/bootstrap.ts +6 -0
  301. package/src/tools/builtin-tool-catalog.ts +9 -0
  302. package/src/tools/context/index.ts +148 -113
  303. package/src/tools/context/surface.ts +11 -7
  304. package/src/tools/core-bootstrap.ts +4 -0
  305. package/src/tools/observation.ts +8 -2
  306. package/src/tools/policy.ts +3 -0
  307. package/src/tools/self-compact.ts +31 -0
  308. package/src/tools/surface.ts +1 -0
  309. package/src/tools/tasks.ts +1 -1
  310. package/dist/chunk-CTFPFW3H.js +0 -44
  311. package/dist/chunk-EOJPDNUP.js +0 -1325
  312. package/dist/chunk-I4Y4WKZR.js +0 -386
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Read-only data model for provider subscription quota.
3
+ *
4
+ * The shapes mirror QuotaBubble's `providers/base.py` (`ProviderStatus`,
5
+ * `UsageWindow`, `UsageSnapshot`) so that adapters written against either
6
+ * project describe the same usage facts. Timestamps are ISO 8601 strings
7
+ * rather than `Date` objects so a snapshot stays structurally cloneable
8
+ * across the worker and transport boundaries.
9
+ */
10
+
11
+ /** Outcome of one usage read, including the reasons a read produced no data. */
12
+ export type QuotaStatus = "ok" | "no_credentials" | "expired" | "error" | "loading";
13
+
14
+ /** One rate-limit window reported by a provider. */
15
+ export interface UsageWindow {
16
+ /** Stable identity for ordering and lookup, for example "session" or "weekly". */
17
+ key: string;
18
+ /** Human label such as "5h" or "Weekly". */
19
+ label: string;
20
+ /** Canonical percentage consumed, 0 to 100. Adapters convert remaining fractions here. */
21
+ usedPct: number;
22
+ /** ISO 8601 instant the window resets, or null when the provider omitted it. */
23
+ resetsAt: string | null;
24
+ /** Compact label for a narrow surface such as a footer indicator. */
25
+ short?: string;
26
+ /** Model or product scope when the window covers less than the whole plan. */
27
+ scope?: string;
28
+ /** Provider-supplied severity word; Clio does not invent one. */
29
+ severity?: string;
30
+ /** True when the provider marks this window as the currently binding limit. */
31
+ active?: boolean;
32
+ }
33
+
34
+ /** Prepaid credit balance for providers that bill credits instead of windows. */
35
+ export interface UsageCredits {
36
+ display: string;
37
+ usedPct: number | null;
38
+ }
39
+
40
+ /** Everything one provider reported about quota at a single point in time. */
41
+ export interface UsageSnapshot {
42
+ providerId: string;
43
+ displayName: string;
44
+ status: QuotaStatus;
45
+ windows: UsageWindow[];
46
+ credits?: UsageCredits | null;
47
+ plan?: string | null;
48
+ message?: string | null;
49
+ /** Seconds to wait, captured from a 429 `Retry-After` header. */
50
+ retryAfterSeconds?: number | null;
51
+ /** True when the snapshot is a cached last-good value rather than a fresh read. */
52
+ stale?: boolean;
53
+ /** ISO 8601 instant of the read that produced this snapshot. */
54
+ fetchedAt: string | null;
55
+ }
56
+
57
+ /**
58
+ * One runtime's quota adapter. Implementations stay pure data plumbing:
59
+ * no UI, no dispatch admission, no credential writes.
60
+ */
61
+ export interface QuotaProvider {
62
+ id: string;
63
+ displayName: string;
64
+ /** True when usable credentials are present, without spending a network call. */
65
+ detect(): Promise<boolean>;
66
+ /** Read current usage, reporting failure as a snapshot status rather than throwing. */
67
+ fetch(): Promise<UsageSnapshot>;
68
+ }
69
+
70
+ /** Parse a numeric `Retry-After` value into non-negative seconds. */
71
+ export function parseRetryAfterSeconds(value: string | null | undefined): number | null {
72
+ if (!value) return null;
73
+ const seconds = Number.parseFloat(value.trim());
74
+ if (!Number.isFinite(seconds)) return null;
75
+ return Math.max(0, seconds);
76
+ }
77
+
78
+ /** Turn a raw plan identifier such as "max_5x" into a display plan such as "Max 5X". */
79
+ export function formatPlan(value: string | null | undefined): string | null {
80
+ if (!value) return null;
81
+ const words = value.replace(/_/g, " ").trim();
82
+ if (!words) return null;
83
+ return words.replace(/\S+/g, (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
84
+ }
@@ -115,6 +115,14 @@ export {
115
115
  } from "./prompts/loader.js";
116
116
  export { parseCommandArgs, substituteArgs } from "./prompts/substitute.js";
117
117
  export { installedSkillNames, installedSkillPackages } from "./skills/availability.js";
118
+ export {
119
+ buildSkillCatalogView,
120
+ type SkillCatalogPackage,
121
+ type SkillCatalogRow,
122
+ type SkillCatalogRowKind,
123
+ type SkillCatalogView,
124
+ type SkillCatalogViewInput,
125
+ } from "./skills/catalog-view.js";
118
126
  export { parseSkillEvals, type SkillEvalParseResult, type SkillEvalScenario } from "./skills/evals.js";
119
127
  export {
120
128
  type InstallSkillInput,
@@ -127,6 +135,7 @@ export {
127
135
  type UpdateSkillsInput,
128
136
  updateSkills,
129
137
  } from "./skills/install.js";
138
+ export { type LexicalMatchMode, lexicalMatches } from "./skills/lexical-match.js";
130
139
  export {
131
140
  defaultSkillRoots,
132
141
  expandSkillInvocationInput,
@@ -163,6 +172,8 @@ export {
163
172
  } from "./skills/marketplace.js";
164
173
  export {
165
174
  checkSkillDrift,
175
+ checkSkillDriftBatch,
176
+ type SkillDriftSubject,
166
177
  type SkillDriftVerdict,
167
178
  type SkillPinEntry,
168
179
  } from "./skills/provenance-pin.js";
@@ -0,0 +1,571 @@
1
+ import {
2
+ SKILL_INSTALL_OFFER_OPTION_NEVER,
3
+ SKILL_INSTALL_OFFER_OPTION_NOT_NOW,
4
+ SKILL_INSTALL_OFFER_OPTION_PROJECT,
5
+ SKILL_INSTALL_OFFER_OPTION_USER,
6
+ SKILL_SUGGESTION_ANCHOR,
7
+ } from "../../../core/skill-activation.js";
8
+ import { type LexicalMatchMode, lexicalMatches, normalize } from "./lexical-match.js";
9
+ import type { Skill } from "./loader.js";
10
+ import type { MarketplaceSkill } from "./marketplace.js";
11
+
12
+ /**
13
+ * The model-facing skills listing: which rows exist, which of them the caller
14
+ * asked for, and how many of those fit the byte budget.
15
+ *
16
+ * It lives here rather than in the context tool for two reasons. The row model
17
+ * and the query filter are resource concerns, and the tool should hold only the
18
+ * call site. And the budget arithmetic wants to be testable without standing up
19
+ * an observation reservation.
20
+ *
21
+ * Two properties this module exists to guarantee.
22
+ *
23
+ * A listing that is neither filtered nor cut renders byte-identical to what the
24
+ * tool emitted before any of this existed. Narrowing is something the caller
25
+ * opts into; the default view still shows every skill, because a ranked or
26
+ * clipped default would decide for the model which skills it is allowed to
27
+ * know about.
28
+ *
29
+ * The footer always survives. The listing used to be rendered whole and then
30
+ * head-truncated against the per-call cap, which keeps the head and drops the
31
+ * tail. The tail is the reply protocol, the one line a model actually acts on.
32
+ * Here the footer is measured and reserved first and rows are fitted into what
33
+ * is left, so an over-budget catalog loses catalog rows and says so, rather
34
+ * than losing the instruction that tells the model what to do with them.
35
+ */
36
+
37
+ /** Ordered lowest-value-last, which is also the order overflow drops them in. */
38
+ export type SkillCatalogRowKind = "ready" | "session" | "package" | "marketplace";
39
+
40
+ export interface SkillCatalogRow {
41
+ kind: SkillCatalogRowKind;
42
+ name: string;
43
+ /** The rendered bullet, exactly as it appears in the listing. */
44
+ line: string;
45
+ /** Typed drift state. Never re-derived by reading the rendered line back. */
46
+ drifted: boolean;
47
+ /**
48
+ * Stable identity for this row, unique across the whole listing.
49
+ *
50
+ * A name is not an identity here. One installed standalone skill produces a
51
+ * ready row and a package row under the same name, and the same package id
52
+ * can be installed at both user and project scope. Callers that reconcile a
53
+ * page against their own inventories have to match on this, not on the name.
54
+ */
55
+ key: string;
56
+ }
57
+
58
+ /** Disk-install state for one package, as {@link installedSkillPackages} reports it. */
59
+ export interface SkillCatalogPackage {
60
+ name: string;
61
+ names: string[];
62
+ scope: string;
63
+ state: string;
64
+ origin: string;
65
+ path: string;
66
+ }
67
+
68
+ export interface SkillCatalogViewInput {
69
+ /** Model-visible skills, already filtered by trust and disable-model-invocation. */
70
+ skills: ReadonlyArray<Skill>;
71
+ packages: ReadonlyArray<SkillCatalogPackage>;
72
+ marketplace: ReadonlyArray<MarketplaceSkill>;
73
+ /** Names of ready skills whose content no longer matches its recorded hash. */
74
+ drifted?: ReadonlySet<string>;
75
+ /** False on a registry that never offers the marketplace, such as a worker. */
76
+ marketplaceOffered: boolean;
77
+ modelActivation: boolean;
78
+ /** Operator/model query; empty means no filtering. */
79
+ query?: string;
80
+ /** Maximum rows on this page; undefined means every row the budget allows. */
81
+ limit?: number | undefined;
82
+ offset?: number;
83
+ /** Per-call observation cap in bytes. */
84
+ capBytes: number;
85
+ }
86
+
87
+ export interface SkillCatalogView {
88
+ text: string;
89
+ /** The rows actually rendered. */
90
+ rows: SkillCatalogRow[];
91
+ /** Rows matching the query across every kind, before paging. */
92
+ total: number;
93
+ shown: number;
94
+ /** Offset to continue from, when rows remain. */
95
+ nextOffset: number | undefined;
96
+ /** True when a query was applied, whatever it matched. */
97
+ filtered: boolean;
98
+ /** Which token rule produced the matches; null when no query was applied. */
99
+ matchMode: LexicalMatchMode | null;
100
+ /** True when the byte budget, rather than the query or the limit, cut the page. */
101
+ budgetLimited: boolean;
102
+ /** Ready-row names reported as drifted in this result set. */
103
+ driftedNames: string[];
104
+ }
105
+
106
+ const LISTING_HEADER = "Available skills.";
107
+ const SESSION_NOTE =
108
+ "These skills were explicitly supplied for this session. Preserve their source; session availability does not mean Clio installed or copied them.";
109
+ const INTEROP_NOTE =
110
+ "Other-agent skill folders are discovery-only. Explicitly import into Clio before use; the trust-imports setting does not install or activate loose files.";
111
+ const MARKETPLACE_HEADER = "Marketplace (additional skills available to install; /skill <name> offers to install):";
112
+ const SESSION_HEADER = "Explicitly supplied session skills (not installed packages):";
113
+ const EMPTY_WITH_MARKETPLACE = "No skills are available in Clio and no additional marketplace skills were found.";
114
+ const EMPTY_WITHOUT_MARKETPLACE = "No skills are available in Clio.";
115
+
116
+ /** Marks a ready row whose content no longer matches the hash recorded for it. */
117
+ const DRIFT_MARKER = " [drifted]";
118
+
119
+ /** Ceiling on an explicit `limit`; the byte budget is the real bound. */
120
+ const MAX_LIMIT = 200;
121
+
122
+ /** Code points of a caller-supplied query echoed back in the note. */
123
+ const MAX_QUERY_ECHO = 48;
124
+ /** Code points of a name echoed back in the note. */
125
+ const MAX_NAME_ECHO = 64;
126
+ /** Drifted skills named individually before the notice reports a remainder. */
127
+ const MAX_DRIFT_NAMES = 6;
128
+
129
+ function byteLength(text: string): number {
130
+ return Buffer.byteLength(text, "utf8");
131
+ }
132
+
133
+ /**
134
+ * Clip by code point, not by UTF-16 unit, so a surrogate pair is never split
135
+ * into a lone half and a multi-byte script is bounded by what it reads as
136
+ * rather than by how it happens to encode.
137
+ */
138
+ function boundText(text: string, maxCodePoints: number): string {
139
+ const points = [...text];
140
+ if (points.length <= maxCodePoints) return text;
141
+ return `${points.slice(0, maxCodePoints - 1).join("")}…`;
142
+ }
143
+
144
+ /** Echoing the caller's query must not let one argument size the response. */
145
+ function boundQuery(query: string): string {
146
+ return boundText(query.replace(/\s+/g, " ").trim(), MAX_QUERY_ECHO);
147
+ }
148
+
149
+ function boundName(name: string): string {
150
+ return boundText(name, MAX_NAME_ECHO);
151
+ }
152
+
153
+ function activationProtocolLine(modelActivation: boolean): string {
154
+ return modelActivation
155
+ ? `If one skill above matches the current task, load it now with context(scope="skills", name="<name>") and continue in the same turn; at this autonomy level you activate installed skills yourself and do not wait for the operator. Marketplace additions still require operator approval. If none match, do not mention skills.`
156
+ : `If one skill above matches the current task, begin your reply with the line \`${SKILL_SUGGESTION_ANCHOR}\` (a comma-separated sequence, in order, when several compose), then continue the task in the same turn without it; only the operator can run it. If none match, do not mention skills.`;
157
+ }
158
+
159
+ function marketplaceOfferClause(): string {
160
+ return `When no installed skill serves the task but a marketplace skill above genuinely does, you may instead ask the operator with ask_user (mode=single_question, header "Install skill") whether to install it, offering exactly: "${SKILL_INSTALL_OFFER_OPTION_PROJECT}", "${SKILL_INSTALL_OFFER_OPTION_USER}", "${SKILL_INSTALL_OFFER_OPTION_NOT_NOW}", "${SKILL_INSTALL_OFFER_OPTION_NEVER}". The harness handles those exact offer options. An explicit operator request or approval also authorizes the documented library install CLI. After installation, refresh the inventory; distinguish installed from ready and report /library reload when required.`;
161
+ }
162
+
163
+ /**
164
+ * One sentence, only when something actually drifted.
165
+ *
166
+ * Visibility and nothing else. The skill stays listed, stays ready and stays
167
+ * loadable; the operator owns the installed copy and this does not reinstall,
168
+ * repair, suppress or reorder anything. It exists because the drift warning
169
+ * used to arrive only after the model had already spent a turn loading the
170
+ * skill, which is too late to choose a different one.
171
+ */
172
+ function driftNotice(names: ReadonlyArray<string>): string {
173
+ if (names.length === 0) return "";
174
+ // Bounded: a catalog where everything drifted would otherwise put every name
175
+ // in one sentence and size the response by how much is wrong.
176
+ const shown = names.slice(0, MAX_DRIFT_NAMES).map(boundName);
177
+ const omitted = names.length - shown.length;
178
+ const list = omitted > 0 ? `${shown.join(", ")} and ${omitted} more` : shown.join(", ");
179
+ const subject = names.length === 1 ? `${list} no longer matches` : `${list} no longer match`;
180
+ return `Marked [drifted]: ${subject} the content hash recorded for it; the skill still loads unchanged, and /library shows the installed copy.`;
181
+ }
182
+
183
+ function readySkills(skills: ReadonlyArray<Skill>): Skill[] {
184
+ return skills.filter((skill) => skill.source === "clio-coder" || skill.source === "plugin");
185
+ }
186
+
187
+ function sessionSkills(skills: ReadonlyArray<Skill>): Skill[] {
188
+ return skills.filter((skill) => skill.source !== "clio-coder" && skill.source !== "plugin");
189
+ }
190
+
191
+ /** The text a query is matched against, per row kind. */
192
+ function haystackForSkill(skill: Skill): string {
193
+ const triggers = skill.metadata.triggers;
194
+ const triggerText = Array.isArray(triggers)
195
+ ? triggers.filter((entry): entry is string => typeof entry === "string").join(" ")
196
+ : "";
197
+ return `${skill.name.replace(/-/g, " ")} ${skill.description} ${triggerText}`;
198
+ }
199
+
200
+ function haystackForMarketplace(entry: MarketplaceSkill): string {
201
+ return `${entry.name.replace(/-/g, " ")} ${entry.description} ${entry.category ?? ""} ${(entry.triggers ?? []).join(" ")}`;
202
+ }
203
+
204
+ function haystackForPackage(record: SkillCatalogPackage): string {
205
+ return `${record.name.replace(/-/g, " ")} ${record.names.join(" ").replace(/-/g, " ")}`;
206
+ }
207
+
208
+ interface CandidateRow extends SkillCatalogRow {
209
+ haystack: string;
210
+ }
211
+
212
+ function buildRows(input: SkillCatalogViewInput): CandidateRow[] {
213
+ const drifted = input.drifted ?? new Set<string>();
214
+ const rows: CandidateRow[] = [];
215
+ for (const skill of readySkills(input.skills)) {
216
+ const isDrifted = drifted.has(skill.name);
217
+ rows.push({
218
+ kind: "ready",
219
+ name: skill.name,
220
+ key: `ready:${skill.filePath}`,
221
+ drifted: isDrifted,
222
+ line: `- ${skill.name}${isDrifted ? DRIFT_MARKER : ""} (source: ${skill.source}; scope: ${skill.scope}): ${skill.description}`,
223
+ haystack: haystackForSkill(skill),
224
+ });
225
+ }
226
+ for (const skill of sessionSkills(input.skills)) {
227
+ rows.push({
228
+ kind: "session",
229
+ name: skill.name,
230
+ key: `session:${skill.filePath}`,
231
+ drifted: false,
232
+ line: `- ${skill.name} (source: ${skill.source}; scope: ${skill.scope}; file: ${skill.filePath}): ${skill.description}`,
233
+ haystack: haystackForSkill(skill),
234
+ });
235
+ }
236
+ for (const record of input.packages) {
237
+ rows.push({
238
+ kind: "package",
239
+ name: record.name,
240
+ key: `package:${record.scope}:${record.path}`,
241
+ drifted: false,
242
+ line: `- ${record.name} (scope: ${record.scope}; origin: ${record.origin === "catalog" ? "marketplace catalog" : record.origin}; state: ${record.state}; path: ${record.path})`,
243
+ haystack: haystackForPackage(record),
244
+ });
245
+ }
246
+ for (const entry of input.marketplace) {
247
+ const category = entry.category ? ` [${entry.category}]` : "";
248
+ rows.push({
249
+ kind: "marketplace",
250
+ name: entry.name,
251
+ key: `marketplace:${entry.sourceUrl}`,
252
+ drifted: false,
253
+ line: `- ${entry.name}${category}: ${entry.description}`,
254
+ haystack: haystackForMarketplace(entry),
255
+ });
256
+ }
257
+ return rows;
258
+ }
259
+
260
+ /**
261
+ * Narrow to the rows the query asked for.
262
+ *
263
+ * `all` first, then `any`. A precise query ("worktree merge") should narrow to
264
+ * the rows carrying both words, but a query written as a sentence would then
265
+ * match nothing, and an empty page is a worse answer than a broad one. The
266
+ * chosen mode is reported so the payload can say which happened.
267
+ */
268
+ function selectRows(
269
+ rows: ReadonlyArray<CandidateRow>,
270
+ query: string,
271
+ ): { rows: CandidateRow[]; mode: LexicalMatchMode } {
272
+ const strict = rows.filter((row) => lexicalMatches(query, row.haystack, "all"));
273
+ if (strict.length > 0) return { rows: strict, mode: "all" };
274
+ return { rows: rows.filter((row) => lexicalMatches(query, row.haystack, "any")), mode: "any" };
275
+ }
276
+
277
+ function renderSections(rows: ReadonlyArray<SkillCatalogRow>, input: SkillCatalogViewInput, drift: string): string {
278
+ const ready = rows.filter((row) => row.kind === "ready");
279
+ const session = rows.filter((row) => row.kind === "session");
280
+ const packages = rows.filter((row) => row.kind === "package");
281
+ const marketplace = rows.filter((row) => row.kind === "marketplace");
282
+
283
+ const lines = [LISTING_HEADER, ""];
284
+ lines.push(`Ready skills in Clio (${ready.length}):`);
285
+ if (ready.length === 0) lines.push("- none");
286
+ for (const row of ready) lines.push(row.line);
287
+ if (session.length > 0) {
288
+ lines.push("", SESSION_HEADER);
289
+ for (const row of session) lines.push(row.line);
290
+ lines.push(SESSION_NOTE);
291
+ }
292
+ if (packages.length > 0) {
293
+ lines.push("", `Installed packages providing skills (${packages.length}):`);
294
+ for (const row of packages) lines.push(row.line);
295
+ }
296
+ lines.push("", INTEROP_NOTE);
297
+ if (marketplace.length > 0) {
298
+ lines.push("", MARKETPLACE_HEADER);
299
+ for (const row of marketplace) lines.push(row.line);
300
+ }
301
+ if (drift.length > 0) lines.push("", drift);
302
+ lines.push("", activationProtocolLine(input.modelActivation));
303
+ if (marketplace.length > 0) lines.push(marketplaceOfferClause());
304
+ return lines.join("\n");
305
+ }
306
+
307
+ function clampLimit(limit: number | undefined, fallback: number): number {
308
+ if (limit === undefined || !Number.isFinite(limit)) return fallback;
309
+ return Math.max(1, Math.min(MAX_LIMIT, Math.floor(limit)));
310
+ }
311
+
312
+ function clampOffset(offset: number | undefined): number {
313
+ if (offset === undefined || !Number.isFinite(offset) || offset <= 0) return 0;
314
+ return Math.floor(offset);
315
+ }
316
+
317
+ /**
318
+ * The listing, narrowed and fitted.
319
+ *
320
+ * A catalog with nothing in it keeps the exact sentence the tool emitted
321
+ * before, including the distinction between "no marketplace was offered" and
322
+ * "the marketplace was offered and had nothing", because a worker registry must
323
+ * not claim a marketplace is unconfigured when it simply has none of its own.
324
+ */
325
+ export function buildSkillCatalogView(input: SkillCatalogViewInput): SkillCatalogView {
326
+ const rawQuery = (input.query ?? "").trim();
327
+ // A query of punctuation or emoji normalizes to nothing searchable. Filtering
328
+ // on it would return an empty page for a request that carried no terms, so it
329
+ // is treated as no query and the note says the terms were dropped.
330
+ const query = normalize(rawQuery).length > 0 ? rawQuery : "";
331
+ const emptyTerms = rawQuery.length > 0 && query.length === 0;
332
+ const filtered = query.length > 0;
333
+ const all = buildRows(input);
334
+
335
+ if (all.length === 0 && !filtered) {
336
+ return {
337
+ text: input.marketplaceOffered ? EMPTY_WITH_MARKETPLACE : EMPTY_WITHOUT_MARKETPLACE,
338
+ rows: [],
339
+ total: 0,
340
+ shown: 0,
341
+ nextOffset: undefined,
342
+ filtered: false,
343
+ matchMode: null,
344
+ budgetLimited: false,
345
+ driftedNames: [],
346
+ };
347
+ }
348
+
349
+ const selected = filtered ? selectRows(all, query) : { rows: [...all], mode: null as LexicalMatchMode | null };
350
+ const matching = selected.rows;
351
+ const total = matching.length;
352
+ const offset = clampOffset(input.offset);
353
+ const limit = clampLimit(input.limit, total);
354
+ const window = matching.slice(offset, offset + limit);
355
+
356
+ // The drift notice describes the matched result set, not the page: a drifted
357
+ // skill the caller filtered to is worth naming even when paging pushed its
358
+ // row to the next offset. Read from the typed flag, never from the rendered
359
+ // line, so a description that happens to contain the marker text cannot give
360
+ // a healthy skill a drift footer.
361
+ const driftedNames = matching.filter((row) => row.drifted).map((row) => row.name);
362
+
363
+ const context: RenderContext = {
364
+ input,
365
+ drift: driftNotice(driftedNames),
366
+ filtered,
367
+ emptyTerms,
368
+ query,
369
+ matchMode: selected.mode,
370
+ total,
371
+ offset,
372
+ catalogTotal: all.length,
373
+ window,
374
+ };
375
+
376
+ // The whole window first, so an inventory that exactly fits is never cut. The
377
+ // previous version reserved scaffolding for sections that did not exist and
378
+ // dropped complete listings that fitted with room to spare.
379
+ const whole = renderCandidate(context, window.length, false);
380
+ if (byteLength(whole) <= input.capBytes) {
381
+ return finishView(context, window, whole, false);
382
+ }
383
+
384
+ // Bisection is valid only over row counts of one or more.
385
+ //
386
+ // For k >= 1 the rendered size is non-decreasing in k: each extra row adds
387
+ // its line and, at a section's first row, that section's heading, while the
388
+ // note only grows by the digits of a larger count. A zero-row page is NOT on
389
+ // that curve, because it alone carries the "this row is too large, open it
390
+ // directly" sentence, which a one-row page does not. Including 0 in the
391
+ // search let a midpoint of 0 test larger than a one-row page and report that
392
+ // nothing fits while the first row would have fitted comfortably.
393
+ let low = 1;
394
+ let high = window.length - 1;
395
+ let best = -1;
396
+ let bestText = "";
397
+ while (low <= high) {
398
+ const mid = (low + high) >> 1;
399
+ const text = renderCandidate(context, mid, true);
400
+ if (byteLength(text) <= input.capBytes) {
401
+ best = mid;
402
+ bestText = text;
403
+ low = mid + 1;
404
+ } else {
405
+ high = mid - 1;
406
+ }
407
+ }
408
+ if (best >= 1) return finishView(context, window.slice(0, best), bestText, true);
409
+
410
+ // No row fits. The zero-row page is measured on its own, off the monotonic
411
+ // curve, because it says something the others do not.
412
+ const blocked = renderCandidate(context, 0, true);
413
+ if (byteLength(blocked) <= input.capBytes) {
414
+ return finishView(context, [], blocked, true);
415
+ }
416
+ // Not even the scaffolding fits. Say what is there and how to reach it, in a
417
+ // line short enough to survive any usable reservation.
418
+ return {
419
+ text: compactFallback(context),
420
+ rows: [],
421
+ total,
422
+ shown: 0,
423
+ nextOffset: undefined,
424
+ filtered,
425
+ matchMode: selected.mode,
426
+ budgetLimited: true,
427
+ driftedNames,
428
+ };
429
+ }
430
+
431
+ interface RenderContext {
432
+ input: SkillCatalogViewInput;
433
+ drift: string;
434
+ filtered: boolean;
435
+ emptyTerms: boolean;
436
+ query: string;
437
+ matchMode: LexicalMatchMode | null;
438
+ total: number;
439
+ offset: number;
440
+ catalogTotal: number;
441
+ window: ReadonlyArray<SkillCatalogRow>;
442
+ }
443
+
444
+ /** Where paging resumes, and whether a row too large to carry was stepped over. */
445
+ function continuation(context: RenderContext, shown: number): { nextOffset: number | undefined; blocked: boolean } {
446
+ const consumed = context.offset + shown;
447
+ if (consumed >= context.total) return { nextOffset: undefined, blocked: false };
448
+ // A page that carried nothing must not hand back the offset it was given:
449
+ // that is a loop, not a continuation. Stepping over the oversized row keeps
450
+ // the rows behind it reachable, and the note names what was stepped over.
451
+ if (shown === 0) return { nextOffset: context.offset + 1, blocked: true };
452
+ return { nextOffset: consumed, blocked: false };
453
+ }
454
+
455
+ function renderCandidate(context: RenderContext, shown: number, cut: boolean): string {
456
+ const page = context.window.slice(0, shown);
457
+ const body = renderSections(page, context.input, context.drift);
458
+ const { nextOffset, blocked } = continuation(context, shown);
459
+ const note = pageNote({
460
+ filtered: context.filtered,
461
+ emptyTerms: context.emptyTerms,
462
+ query: context.query,
463
+ matchMode: context.matchMode,
464
+ total: context.total,
465
+ shown,
466
+ nextOffset,
467
+ budgetLimited: cut,
468
+ blockedRow: blocked ? (context.window[0] ?? null) : null,
469
+ catalogTotal: context.catalogTotal,
470
+ });
471
+ return note.length > 0 ? `${body}\n${note}` : body;
472
+ }
473
+
474
+ function finishView(
475
+ context: RenderContext,
476
+ page: ReadonlyArray<SkillCatalogRow>,
477
+ text: string,
478
+ budgetLimited: boolean,
479
+ ): SkillCatalogView {
480
+ const { nextOffset } = continuation(context, page.length);
481
+ return {
482
+ text,
483
+ rows: page.map((row) => ({ ...row })),
484
+ total: context.total,
485
+ shown: page.length,
486
+ nextOffset,
487
+ filtered: context.filtered,
488
+ matchMode: context.matchMode,
489
+ budgetLimited,
490
+ driftedNames: context.window.filter((row) => row.drifted).map((row) => row.name),
491
+ };
492
+ }
493
+
494
+ /**
495
+ * What to say when the reservation cannot carry the listing's own scaffolding.
496
+ *
497
+ * One short line naming the size of the thing and the one argument that makes
498
+ * it smaller. Deliberately free of the reply protocol: a page that shows no
499
+ * skills has nothing for the protocol to point at, and repeating it here would
500
+ * be the same overrun in a smaller font.
501
+ */
502
+ function compactFallback(context: RenderContext): string {
503
+ const scope = context.filtered ? `${context.total} matching` : `${context.total}`;
504
+ return `Available skills: ${scope}. This call's budget cannot carry the listing; narrow it with context(scope="skills", query="<terms>") or raise the budget in a fresh turn.`;
505
+ }
506
+
507
+ interface PageNoteInput {
508
+ filtered: boolean;
509
+ emptyTerms: boolean;
510
+ query: string;
511
+ matchMode: LexicalMatchMode | null;
512
+ total: number;
513
+ shown: number;
514
+ nextOffset: number | undefined;
515
+ budgetLimited: boolean;
516
+ blockedRow: SkillCatalogRow | null;
517
+ catalogTotal: number;
518
+ }
519
+
520
+ /**
521
+ * Emitted only when the view is narrowed or cut.
522
+ *
523
+ * A complete unfiltered listing gains no new text at all, which is what keeps
524
+ * the default view byte-identical to the one this module replaced. Everything
525
+ * here is a statement about what the caller is *not* seeing, so a view that
526
+ * hides nothing has nothing to say.
527
+ *
528
+ * Every value interpolated here is bounded. The query is a caller-supplied
529
+ * string with no length limit in the schema, and the drift notice can name a
530
+ * whole catalog, so echoing either whole would let one argument decide the size
531
+ * of a response this module exists to keep inside its budget.
532
+ */
533
+ function pageNote(input: PageNoteInput): string {
534
+ const clauses: string[] = [];
535
+ if (input.emptyTerms) {
536
+ clauses.push("The query carried no searchable terms, so the full list is shown.");
537
+ }
538
+ if (input.filtered) {
539
+ const broadened = input.matchMode === "any" ? ", matched on any query word after no row matched them all" : "";
540
+ clauses.push(
541
+ input.total === 0
542
+ ? `No skill matches "${boundQuery(input.query)}"; ${input.catalogTotal} are available unfiltered, so drop query to list them.`
543
+ : `Filtered by "${boundQuery(input.query)}"${broadened}: ${input.total} of ${input.catalogTotal} rows match.`,
544
+ );
545
+ }
546
+ if (input.shown < input.total) {
547
+ clauses.push(
548
+ input.budgetLimited
549
+ ? `Showing ${input.shown} of ${input.total} matching rows; the rest did not fit this call's budget.`
550
+ : `Showing ${input.shown} of ${input.total} matching rows.`,
551
+ );
552
+ }
553
+ if (input.blockedRow !== null) {
554
+ clauses.push(
555
+ `"${boundName(input.blockedRow.name)}" is too large to carry in this call's budget and was stepped over; open it directly with context(scope="skills", name="${boundName(input.blockedRow.name)}").`,
556
+ );
557
+ }
558
+ if (input.nextOffset !== undefined) {
559
+ clauses.push(
560
+ input.filtered
561
+ ? // Offsets index the FILTERED result set. A continuation that dropped
562
+ // the query would apply this cursor to the unfiltered catalog, which
563
+ // repeats rows and skips matches.
564
+ `Continue with the same query at offset=${input.nextOffset}; the offset indexes the filtered rows, so dropping query changes which rows it selects.`
565
+ : `Continue with context(scope="skills", offset=${input.nextOffset}) for the remainder.`,
566
+ );
567
+ } else if (input.shown < input.total) {
568
+ clauses.push("The remainder cannot be carried this turn; narrow with query or continue in a follow-up turn.");
569
+ }
570
+ return clauses.join(" ");
571
+ }