@iowarp/clio-coder 0.4.1 → 0.4.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 (437) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/CONTRIBUTING.md +59 -36
  3. package/README.md +404 -472
  4. package/SECURITY.md +2 -1
  5. package/dist/{acp-ZILU3AUO.js → acp-TMDQZDIG.js} +7 -7
  6. package/dist/{agents-HYWGBGQR.js → agents-5N5NG3XG.js} +28 -28
  7. package/dist/assets/codewiki.json +1 -1
  8. package/dist/{auth-N3QT7CBO.js → auth-Z5CCBXKQ.js} +8 -9
  9. package/dist/{builtins-UJLMOVOV.js → builtins-K6TNDT24.js} +4 -4
  10. package/dist/{chunk-GVQJ5CCZ.js → chunk-2HFQNRV3.js} +7 -7
  11. package/dist/{chunk-QMXC4JB7.js → chunk-2NHR3NAY.js} +163 -1401
  12. package/dist/chunk-2X4RYJTJ.js +39 -0
  13. package/dist/{chunk-Y45G3AXC.js → chunk-2Z2IKEXI.js} +6 -10
  14. package/dist/{chunk-EIMVLWB3.js → chunk-34BHNEE3.js} +7 -3
  15. package/dist/{chunk-GIZNH63R.js → chunk-35MSIRKH.js} +9 -4
  16. package/dist/chunk-3EBYEESD.js +314 -0
  17. package/dist/{chunk-CTJ4RNAA.js → chunk-3F7VUY77.js} +2 -2
  18. package/dist/{chunk-AP73CFDC.js → chunk-3KIPBMUA.js} +2 -2
  19. package/dist/{chunk-J5LZHVIT.js → chunk-3M6DQK6S.js} +113 -35
  20. package/dist/{chunk-VEGN6WIQ.js → chunk-462T4EGZ.js} +2 -2
  21. package/dist/{chunk-AFKWHWXF.js → chunk-4JDLP6ZS.js} +33 -16
  22. package/dist/{chunk-6FN3E6KX.js → chunk-4O6MANBS.js} +2 -2
  23. package/dist/{chunk-AKB4GYDL.js → chunk-54ODD65L.js} +5 -5
  24. package/dist/{chunk-BBTJOK6Y.js → chunk-5KW52TEP.js} +3 -3
  25. package/dist/{chunk-6CCS4G3W.js → chunk-5PFYMY2V.js} +2 -2
  26. package/dist/chunk-77QIVUZB.js +1334 -0
  27. package/dist/{chunk-7OBGU7UB.js → chunk-7BHIY2MW.js} +7 -13
  28. package/dist/{chunk-3QSOM6PA.js → chunk-AZ4WMN4W.js} +2 -2
  29. package/dist/{chunk-6NJQITNH.js → chunk-B74PXLU7.js} +6 -3
  30. package/dist/{chunk-R23Z6K6I.js → chunk-B7HM5Z7T.js} +15 -15
  31. package/dist/{chunk-R32CLGZ6.js → chunk-BO7Y52RY.js} +81 -20
  32. package/dist/{chunk-UEDMSP56.js → chunk-BYMNWQ7O.js} +123 -148
  33. package/dist/{chunk-ZJLUDYFY.js → chunk-CRFOIAX3.js} +4 -4
  34. package/dist/{chunk-2NM363SV.js → chunk-CYZW7JHJ.js} +7 -7
  35. package/dist/{chunk-6HMJX2VU.js → chunk-DYHAXKHD.js} +38 -10
  36. package/dist/{chunk-THYWACCR.js → chunk-DZAW46HP.js} +3 -3
  37. package/dist/{chunk-FYUN5KZ3.js → chunk-DZEK6CJN.js} +17 -17
  38. package/dist/{chunk-3I5NY75V.js → chunk-E7GT7O5N.js} +5 -5
  39. package/dist/{chunk-VKFQTNDV.js → chunk-F2I26BDK.js} +4 -4
  40. package/dist/{chunk-HLW2MRKE.js → chunk-F4EKGO4N.js} +3 -1
  41. package/dist/{chunk-IXJT6DCX.js → chunk-FVDGR2ZL.js} +3 -3
  42. package/dist/{chunk-TZSKNMZG.js → chunk-GTUD2WMY.js} +2 -1
  43. package/dist/{chunk-7EPLI7VL.js → chunk-HIICAHCJ.js} +2 -2
  44. package/dist/{chunk-E67WX76H.js → chunk-HKMD33FO.js} +29 -80
  45. package/dist/chunk-HLAFFSEK.js +360 -0
  46. package/dist/{chunk-UAPGZHYC.js → chunk-I64IFBLB.js} +9 -2
  47. package/dist/{chunk-XKA2ICR3.js → chunk-I66ZTYNP.js} +440 -175
  48. package/dist/{chunk-7PWAODYW.js → chunk-I7XBWTYH.js} +2 -2
  49. package/dist/{chunk-PVAMAVBB.js → chunk-IDNA72AH.js} +102 -2
  50. package/dist/{chunk-GCSMB2KY.js → chunk-IKOZFYBN.js} +1 -1
  51. package/dist/{chunk-2VG7KLYV.js → chunk-IKSLQ4XV.js} +5460 -3241
  52. package/dist/{chunk-QKIFBZKT.js → chunk-IMXMHHMQ.js} +166 -25
  53. package/dist/{chunk-74YWRRU5.js → chunk-JBCS7CRR.js} +2 -2
  54. package/dist/{chunk-BDPT6GTK.js → chunk-JWJGP5DQ.js} +2 -2
  55. package/dist/{chunk-K6BF4U2H.js → chunk-KKOJXO6R.js} +62 -14
  56. package/dist/chunk-KPXDY6QF.js +47 -0
  57. package/dist/{chunk-ABLSQ6JX.js → chunk-LJID3DYZ.js} +7 -1
  58. package/dist/{chunk-VKRH2TCS.js → chunk-M2DAX4F6.js} +2 -2
  59. package/dist/{chunk-6I5ILFOF.js → chunk-M2WXEHER.js} +2 -2
  60. package/dist/{chunk-YPI3QQCF.js → chunk-MCEPRMZW.js} +2 -4
  61. package/dist/{chunk-N5UK64DP.js → chunk-MCMZMDAC.js} +2 -2
  62. package/dist/{chunk-Y4CAGMM6.js → chunk-MNJGS2IN.js} +5 -6
  63. package/dist/{chunk-TVHHYFHE.js → chunk-NEDJ26B5.js} +2 -2
  64. package/dist/{chunk-U2WB7TZS.js → chunk-NMJXSHBJ.js} +97 -85
  65. package/dist/{chunk-HUAS7ITX.js → chunk-O3YUNJZ2.js} +13 -21
  66. package/dist/{chunk-MA3H6DM5.js → chunk-P75RZCJW.js} +25 -3
  67. package/dist/{chunk-IG7BCQBA.js → chunk-PGF63K6I.js} +2 -2
  68. package/dist/chunk-PJX3WQUQ.js +42 -0
  69. package/dist/{chunk-6DWBAZ5U.js → chunk-Q4XWMHX6.js} +4 -6
  70. package/dist/{chunk-OJTRZGR3.js → chunk-QQLGQY2A.js} +8 -8
  71. package/dist/{chunk-J4HBWF6Y.js → chunk-RLYRBIYQ.js} +115 -20
  72. package/dist/{chunk-NLFAQR7Z.js → chunk-S66XZJOF.js} +3 -23
  73. package/dist/{chunk-C537JADH.js → chunk-SSEYRH53.js} +6 -7
  74. package/dist/chunk-SZAA6XDG.js +30 -0
  75. package/dist/{chunk-MOPSG2X7.js → chunk-TPEQIQIE.js} +6 -6
  76. package/dist/{chunk-JA5QWE4Z.js → chunk-UBRFI4HS.js} +1879 -1650
  77. package/dist/{chunk-BTGG6BG2.js → chunk-UH347SHR.js} +154 -15
  78. package/dist/{chunk-5YHDIDBP.js → chunk-UH632ZYL.js} +2 -2
  79. package/dist/{chunk-BWW4HLO4.js → chunk-UXCU4E3T.js} +8 -6
  80. package/dist/{chunk-6VC4OV3Z.js → chunk-VIA6RFQZ.js} +3 -11
  81. package/dist/{chunk-ZAZB4JMW.js → chunk-VKPAQYEB.js} +27 -8
  82. package/dist/{chunk-UXN6JT4W.js → chunk-W4YEMFBX.js} +2 -2
  83. package/dist/{chunk-TD3PGPQA.js → chunk-W6NIE6OW.js} +2 -2
  84. package/dist/{chunk-TVH4ONAM.js → chunk-X7IARSHT.js} +3 -3
  85. package/dist/{chunk-PJJ6MY27.js → chunk-XE3PCIXH.js} +3 -3
  86. package/dist/{chunk-FEFIFZTL.js → chunk-XGDPUNND.js} +2 -2
  87. package/dist/{chunk-SCYB3HA4.js → chunk-XOXV5GKE.js} +51 -16
  88. package/dist/{chunk-QTFGO774.js → chunk-XQRY4DTA.js} +24 -11
  89. package/dist/{chunk-BJGUKIG4.js → chunk-YJISEZKC.js} +2 -2
  90. package/dist/{chunk-GPPB3JBE.js → chunk-ZGNYYXQ6.js} +2 -2
  91. package/dist/{chunk-SINK3QR6.js → chunk-ZNT2M6TG.js} +7 -7
  92. package/dist/{chunk-7RY5VZPH.js → chunk-ZW4HH5JJ.js} +6 -6
  93. package/dist/cli/index.js +33 -32
  94. package/dist/{clio-IT3G3VQH.js → clio-7VB377CC.js} +7 -7
  95. package/dist/{code-nav-RK6S7F6E.js → code-nav-YVLCYA7V.js} +85 -17
  96. package/dist/{config-3QZRWZJF.js → config-4HVOS65E.js} +88 -43
  97. package/dist/{configure-FL7Y3KJF.js → configure-PIWO7B24.js} +10 -10
  98. package/dist/{context-5HE7ODYK.js → context-IYEHL3WQ.js} +33 -31
  99. package/dist/{context-XNHL75JV.js → context-KQYIWPWT.js} +47 -34
  100. package/dist/{context-KYQFRVDC.js → context-N6ZE3LGJ.js} +11 -11
  101. package/dist/{context-clear-N545L53A.js → context-clear-G4OGZJDS.js} +33 -31
  102. package/dist/{context-working-set-QHKXSV2F.js → context-working-set-BWLF6LJP.js} +7 -7
  103. package/dist/{dispatch-runner-RGIE5PCT.js → dispatch-runner-2QQAITS3.js} +38 -38
  104. package/dist/{docs-5NAF6AU7.js → docs-PD3EXDKU.js} +21 -20
  105. package/dist/{doctor-ZGPEGHIP.js → doctor-LHBD36VU.js} +23 -22
  106. package/dist/{eval-GXLL44RD.js → eval-C45FYRJ6.js} +21 -20
  107. package/dist/{eval-inventory-HBWSWQOK.js → eval-inventory-6DEJPLBF.js} +2 -2
  108. package/dist/{evidence-HWLBRH3Q.js → evidence-6SHONYAF.js} +30 -28
  109. package/dist/{evolve-FTZBMNVW.js → evolve-KRKMV72X.js} +30 -28
  110. package/dist/{extensions-VHRBEID7.js → extensions-KPZ2UHBB.js} +5 -3
  111. package/dist/{fleet-CKZHJWZJ.js → fleet-IVTCKDHT.js} +62 -61
  112. package/dist/{fleet-commands-EXDXBMV6.js → fleet-commands-EDWL3IT7.js} +5 -5
  113. package/dist/{fleet-decisions-OTHB6KRL.js → fleet-decisions-YP3YEFGK.js} +4 -4
  114. package/dist/{fleet-graph-YTEZUCUT.js → fleet-graph-ZFWKHY2M.js} +16 -14
  115. package/dist/{fleet-inspect-SS6YMDCK.js → fleet-inspect-FVUNCBML.js} +31 -29
  116. package/dist/{fleet-preflight-PBY4VYOM.js → fleet-preflight-UN5XED4R.js} +2 -2
  117. package/dist/{fleet-validate-KMEM5L3S.js → fleet-validate-XOWC4HSX.js} +17 -15
  118. package/dist/{fleet-verify-QD5M7E7Q.js → fleet-verify-UN3SODEL.js} +30 -28
  119. package/dist/{fleet-view-WAMJYNDT.js → fleet-view-TWHJKCN6.js} +31 -29
  120. package/dist/{init-5XQRBOFV.js → init-T2QORQ3Y.js} +50 -49
  121. package/dist/{interop-34TVO25M.js → interop-IN5I2A66.js} +5 -5
  122. package/dist/{library-3QY6KF57.js → library-LSCATDLZ.js} +15 -13
  123. package/dist/{memory-L4UTIIIW.js → memory-HYOKAGGJ.js} +31 -29
  124. package/dist/{models-ZVX3QOWE.js → models-2GPMFYCM.js} +22 -21
  125. package/dist/{monitor-CEKVSYTS.js → monitor-E4ASVUJH.js} +34 -32
  126. package/dist/{orchestrator-77BAP6BC.js → orchestrator-DDMPR3PY.js} +984 -583
  127. package/dist/{panes-7STHOAUJ.js → panes-E3RUXOW5.js} +4 -4
  128. package/dist/{panes-SHAUIRXY.js → panes-IXKLOKA2.js} +23 -8
  129. package/dist/{reset-EOLM7GVE.js → reset-OAQP3W4O.js} +4 -4
  130. package/dist/{resources-74GKTLSF.js → resources-OTRSN34L.js} +15 -13
  131. package/dist/{run-HBAUJNNZ.js → run-5DEYH5QK.js} +60 -59
  132. package/dist/{share-G3APVLVP.js → share-IHWTLO3M.js} +19 -15
  133. package/dist/{skills-35HHUKCR.js → skills-IYMXMKW4.js} +17 -15
  134. package/dist/{skills-eval-QN4HSHDC.js → skills-eval-DROHSJAR.js} +36 -36
  135. package/dist/{skills-inventory-J357J34F.js → skills-inventory-D7X4L4ZX.js} +15 -13
  136. package/dist/{slash-commands-JZZCQA32.js → slash-commands-QBM7UZ3B.js} +21 -18
  137. package/dist/{steer-XAVHJM22.js → steer-Z5DO23FJ.js} +2 -2
  138. package/dist/{targets-DSM6CY3M.js → targets-P2FUC4IL.js} +25 -28
  139. package/dist/{terminal-lease-JOPFUVEM.js → terminal-lease-YREJ3JX2.js} +5 -5
  140. package/dist/{tools-MKNWVPBH.js → tools-5B7RO6MV.js} +4 -4
  141. package/dist/{trace-ECQ7TIYZ.js → trace-YMGMUM6A.js} +55 -7
  142. package/dist/{upgrade-H7TOM7YL.js → upgrade-PXK3S2YM.js} +11 -9
  143. package/dist/{usage-X52N3IDJ.js → usage-ME5MPXGX.js} +36 -34
  144. package/dist/{verifiers-EJTVVSMA.js → verifiers-BVZ7IWOO.js} +5 -5
  145. package/dist/{verify-YJL6XET2.js → verify-5K7ZKQFC.js} +4 -4
  146. package/dist/{web-fetch-MPIFL3LL.js → web-fetch-MPARV2K7.js} +2 -2
  147. package/dist/{wiki-generate-4NDZTQ4B.js → wiki-generate-F5W5QTYY.js} +48 -47
  148. package/dist/{with-panes-OBOBFIIR.js → with-panes-BYOJCLAM.js} +51 -255
  149. package/dist/worker/entry.js +45 -30
  150. package/docs/README.md +176 -81
  151. package/docs/{acp.md → architecture/acp.md} +36 -20
  152. package/docs/{alcf-provider.md → architecture/alcf-provider.md} +8 -5
  153. package/docs/{architecture.md → architecture/architecture.md} +43 -22
  154. package/docs/{artifact-placement.md → architecture/artifact-placement.md} +26 -23
  155. package/docs/architecture/artifact-versions.md +90 -0
  156. package/docs/{capacity-and-scheduling.md → architecture/capacity-and-scheduling.md} +26 -13
  157. package/docs/{context-engine.md → architecture/context-engine.md} +25 -25
  158. package/docs/{context-working-set.md → architecture/context-working-set.md} +13 -10
  159. package/docs/{dispatch-architecture-rationale.md → architecture/dispatch-architecture-rationale.md} +12 -9
  160. package/docs/{dispatch-typed-intent.md → architecture/dispatch-typed-intent.md} +68 -46
  161. package/docs/{evidence-and-memory.md → architecture/evidence-and-memory.md} +23 -16
  162. package/docs/{middleware-and-components.md → architecture/middleware-and-components.md} +11 -5
  163. package/docs/{model-catalog.md → architecture/model-catalog.md} +40 -17
  164. package/docs/{observability.md → architecture/observability.md} +26 -13
  165. package/docs/{pi-boundary.md → architecture/pi-boundary.md} +24 -11
  166. package/docs/{prompt-envelope-and-tools.md → architecture/prompt-envelope-and-tools.md} +55 -20
  167. package/docs/{provider-adapter-cookbook.md → architecture/provider-adapter-cookbook.md} +35 -24
  168. package/docs/{safety-model.md → architecture/safety-model.md} +20 -15
  169. package/docs/{session-lifecycle.md → architecture/session-lifecycle.md} +8 -5
  170. package/docs/architecture/time-conventions.md +125 -0
  171. package/docs/{trace-store.md → architecture/trace-store.md} +13 -5
  172. package/docs/{tui-design.md → architecture/tui-design.md} +13 -13
  173. package/docs/{worker-dispatch-mechanics.md → architecture/worker-dispatch-mechanics.md} +27 -30
  174. package/docs/{built-in-agents.md → guide/built-in-agents.md} +50 -34
  175. package/docs/{commands-and-modes.md → guide/commands-and-modes.md} +65 -60
  176. package/docs/{configuration-and-targets.md → guide/configuration-and-targets.md} +227 -289
  177. package/docs/guide/configuration-reference.md +1158 -0
  178. package/docs/{environment-variables.md → guide/environment-variables.md} +31 -28
  179. package/docs/{exit-codes-and-output.md → guide/exit-codes-and-output.md} +6 -3
  180. package/docs/{extensions-and-sharing.md → guide/extensions-and-sharing.md} +41 -14
  181. package/docs/{fleet-dispatch.md → guide/fleet-dispatch.md} +39 -43
  182. package/docs/{glossary.md → guide/glossary.md} +14 -11
  183. package/docs/{installation-and-lifecycle.md → guide/installation-and-lifecycle.md} +44 -13
  184. package/docs/guide/panes-and-files.md +290 -0
  185. package/docs/{proactive-memory.md → guide/proactive-memory.md} +79 -66
  186. package/docs/{resource-library.md → guide/resource-library.md} +13 -4
  187. package/docs/{skills-marketplace.md → guide/skills-marketplace.md} +7 -3
  188. package/docs/{tool-usage.md → guide/tool-usage.md} +87 -23
  189. package/docs/{troubleshooting.md → guide/troubleshooting.md} +9 -4
  190. package/docs/{config-knobs-audit.md → history/config-knobs-audit.md} +11 -11
  191. package/docs/{release-cut-checklist.md → history/release-cut-checklist.md} +29 -2
  192. package/docs/{development-pipeline.md → process/development-pipeline.md} +24 -26
  193. package/docs/process/documentation-coverage.md +100 -0
  194. package/docs/process/documentation-guide.md +187 -0
  195. package/docs/{eval-runner.md → process/eval-runner.md} +41 -50
  196. package/docs/{evals-internal.md → process/evals-internal.md} +10 -10
  197. package/docs/{evolution.md → process/evolution.md} +2 -2
  198. package/docs/{fleet-demo-runbook.md → process/fleet-demo-runbook.md} +11 -7
  199. package/docs/{git-commit-provenance.md → process/git-commit-provenance.md} +11 -4
  200. package/docs/{performance-methodology.md → process/performance-methodology.md} +87 -69
  201. package/docs/{scientific-validation.md → process/scientific-validation.md} +4 -4
  202. package/evals/README.md +2 -2
  203. package/package.json +9 -7
  204. package/skills/README.md +46 -37
  205. package/skills/coding/ast-grep/SKILL.md +2 -2
  206. package/skills/coding/coding-standards/SKILL.md +2 -2
  207. package/skills/coding/prototype/SKILL.md +2 -2
  208. package/skills/coding/tdd/SKILL.md +2 -2
  209. package/skills/context/context-handoff/SKILL.md +2 -2
  210. package/skills/context/context-prime/SKILL.md +2 -2
  211. package/skills/git/file-ticket/SKILL.md +2 -2
  212. package/skills/git/fix-issue/SKILL.md +3 -3
  213. package/skills/git/resolve-merge-conflicts/SKILL.md +2 -2
  214. package/skills/git/ship/SKILL.md +2 -2
  215. package/skills/git/worktree-create/SKILL.md +2 -2
  216. package/skills/git/worktree-merge/SKILL.md +2 -2
  217. package/skills/meta/clio-coder-dev/SKILL.md +9 -5
  218. package/skills/meta/clio-coder-dev/evals.md +3 -2
  219. package/skills/meta/clio-coder-test/SKILL.md +102 -95
  220. package/skills/meta/clio-coder-test/evals.md +9 -4
  221. package/skills/meta/clio-coder-test/references/harness.md +100 -124
  222. package/skills/meta/clio-coder-test/references/test-map.md +77 -50
  223. package/skills/meta/credentials/SKILL.md +2 -2
  224. package/skills/meta/find-skills/SKILL.md +2 -2
  225. package/skills/meta/herdr/SKILL.md +2 -2
  226. package/skills/meta/skill-craft/SKILL.md +22 -16
  227. package/skills/planning/architecture/SKILL.md +2 -2
  228. package/skills/planning/backlog/SKILL.md +2 -2
  229. package/skills/planning/prd/SKILL.md +2 -2
  230. package/skills/planning/product-intent/SKILL.md +2 -2
  231. package/skills/planning/tech-spec/SKILL.md +2 -2
  232. package/skills/registry.yaml +62 -62
  233. package/skills/research/arxiv-literature/SKILL.md +2 -2
  234. package/skills/research/experiment-protocol/SKILL.md +2 -2
  235. package/skills/research/scientific-debugging/SKILL.md +2 -2
  236. package/skills/research/scientific-modernization/SKILL.md +2 -2
  237. package/skills/skill-marketplace.json +62 -62
  238. package/skills/workflow/cut-it/SKILL.md +2 -2
  239. package/skills/workflow/design-council/SKILL.md +2 -2
  240. package/skills/workflow/grill-me/SKILL.md +2 -2
  241. package/skills/workflow/workflow-distiller/SKILL.md +2 -2
  242. package/src/cli/args.ts +2 -2
  243. package/src/cli/bootstrap-generate.ts +1 -1
  244. package/src/cli/config-inspect.ts +65 -12
  245. package/src/cli/configure.ts +0 -4
  246. package/src/cli/docs.ts +22 -14
  247. package/src/cli/doctor-naming.ts +5 -5
  248. package/src/cli/doctor-toolchain.ts +3 -3
  249. package/src/cli/eval.ts +1 -2
  250. package/src/cli/extensions.ts +2 -1
  251. package/src/cli/fleet.ts +1 -1
  252. package/src/cli/index.ts +2 -1
  253. package/src/cli/internal-dispatch.ts +3 -4
  254. package/src/cli/panes.ts +19 -5
  255. package/src/cli/run.ts +2 -2
  256. package/src/cli/share.ts +5 -1
  257. package/src/cli/skills-eval.ts +3 -3
  258. package/src/cli/targets.ts +2 -6
  259. package/src/cli/trace.ts +55 -4
  260. package/src/cli/wiki-generate.ts +1 -1
  261. package/src/core/artifact-paths.ts +1 -1
  262. package/src/core/bash-exec.ts +131 -86
  263. package/src/core/bus-events.ts +51 -6
  264. package/src/core/config.ts +5 -1
  265. package/src/core/defaults.ts +7 -4
  266. package/src/core/dispatch-outcome.ts +16 -0
  267. package/src/core/guardrails.ts +10 -49
  268. package/src/core/prompt-hint.ts +9 -0
  269. package/src/domains/agents/builtins/architect.md +2 -3
  270. package/src/domains/agents/builtins/coder.md +3 -2
  271. package/src/domains/agents/builtins/debugger.md +2 -2
  272. package/src/domains/agents/builtins/documenter.md +2 -2
  273. package/src/domains/agents/builtins/git-master.md +1 -1
  274. package/src/domains/agents/builtins/oracle.md +1 -1
  275. package/src/domains/agents/builtins/provenance.md +1 -1
  276. package/src/domains/agents/builtins/researcher.md +1 -1
  277. package/src/domains/agents/builtins/scout.md +1 -1
  278. package/src/domains/agents/builtins/tester.md +2 -2
  279. package/src/domains/agents/builtins/verifier.md +2 -2
  280. package/src/domains/agents/builtins/wiki-writer.md +1 -1
  281. package/src/domains/agents/catalog.ts +12 -14
  282. package/src/domains/agents/contract.ts +2 -0
  283. package/src/domains/agents/extension.ts +23 -1
  284. package/src/domains/config/keybindings.ts +8 -0
  285. package/src/domains/context/extension.ts +0 -3
  286. package/src/domains/context/working-set/path-index.ts +1 -0
  287. package/src/domains/dispatch/capability-match.ts +10 -0
  288. package/src/domains/dispatch/extension.ts +105 -22
  289. package/src/domains/dispatch/host-verification.ts +435 -39
  290. package/src/domains/dispatch/intent-requirements.ts +10 -0
  291. package/src/domains/dispatch/intent.ts +18 -1
  292. package/src/domains/dispatch/path-scope.ts +235 -24
  293. package/src/domains/dispatch/run-event-journal.ts +4 -15
  294. package/src/domains/dispatch/state.ts +2 -3
  295. package/src/domains/dispatch/transport.ts +45 -21
  296. package/src/domains/dispatch/types.ts +55 -3
  297. package/src/domains/eval/artifacts/store.ts +5 -0
  298. package/src/domains/eval/store.ts +8 -1
  299. package/src/domains/evidence/trust-status.ts +10 -1
  300. package/src/domains/extensions/contract.ts +15 -1
  301. package/src/domains/extensions/discovery.ts +238 -41
  302. package/src/domains/extensions/extension.ts +105 -6
  303. package/src/domains/extensions/index.ts +24 -0
  304. package/src/domains/extensions/integrity.ts +189 -0
  305. package/src/domains/extensions/manager.ts +17 -1
  306. package/src/domains/extensions/resource-path.ts +27 -0
  307. package/src/domains/extensions/resources.ts +18 -38
  308. package/src/domains/extensions/snapshot-store.ts +39 -0
  309. package/src/domains/extensions/snapshot.ts +180 -0
  310. package/src/domains/extensions/state.ts +385 -57
  311. package/src/domains/extensions/types.ts +118 -1
  312. package/src/domains/lifecycle/migrations/2026-09-01-extension-install-digests.ts +27 -0
  313. package/src/domains/lifecycle/migrations/index.ts +2 -0
  314. package/src/domains/lifecycle/naming-resources.ts +19 -4
  315. package/src/domains/lifecycle/naming-yazi.ts +10 -5
  316. package/src/domains/middleware/contract.ts +26 -0
  317. package/src/domains/middleware/extension.ts +24 -24
  318. package/src/domains/middleware/hook-receipts.ts +27 -4
  319. package/src/domains/middleware/hooks-io.ts +65 -32
  320. package/src/domains/middleware/hooks.ts +64 -0
  321. package/src/domains/middleware/index.ts +28 -4
  322. package/src/domains/middleware/registrations.ts +326 -0
  323. package/src/domains/middleware/runtime.ts +28 -0
  324. package/src/domains/middleware/snapshot.ts +20 -7
  325. package/src/domains/mux/contract.ts +38 -0
  326. package/src/domains/mux/detect.ts +6 -13
  327. package/src/domains/mux/index.ts +1 -1
  328. package/src/domains/mux/operations.ts +44 -5
  329. package/src/domains/mux/yazi/assets/yazi.toml +2 -2
  330. package/src/domains/mux/yazi/session.ts +53 -4
  331. package/src/domains/mux/yazi/theme.ts +117 -17
  332. package/src/domains/observability/contract.ts +10 -11
  333. package/src/domains/observability/extension.ts +11 -3
  334. package/src/domains/observability/projection.ts +14 -90
  335. package/src/domains/observability/trace-store.ts +43 -7
  336. package/src/domains/prompts/compiler.ts +73 -53
  337. package/src/domains/prompts/contract.ts +15 -3
  338. package/src/domains/prompts/extension.ts +97 -9
  339. package/src/domains/prompts/fragments/identity/clio-worker.md +1 -3
  340. package/src/domains/prompts/fragments/identity/clio.md +6 -12
  341. package/src/domains/prompts/fragments/identity/docs-routing.md +1 -2
  342. package/src/domains/prompts/fragments/identity/self-awareness.md +3 -11
  343. package/src/domains/prompts/fragments/operating/contract.md +7 -15
  344. package/src/domains/prompts/fragments/operating/delegation.md +32 -34
  345. package/src/domains/prompts/fragments/operating/skills.md +10 -24
  346. package/src/domains/prompts/fragments/operating/worker.md +1 -8
  347. package/src/domains/providers/index.ts +1 -1
  348. package/src/domains/providers/model-runtime-capabilities.ts +85 -21
  349. package/src/domains/providers/models/local-models/clio-coder-local-coding-targets.yaml +669 -104
  350. package/src/domains/providers/runtime-resolution.ts +31 -0
  351. package/src/domains/providers/runtimes/common/probe-helpers.ts +7 -2
  352. package/src/domains/providers/runtimes/local-native/llamacpp.ts +9 -1
  353. package/src/domains/providers/types/cost-provenance.ts +19 -0
  354. package/src/domains/providers/types/local-model-quirks.ts +85 -37
  355. package/src/domains/resources/skills/loader.ts +16 -19
  356. package/src/domains/safety/call-target.ts +1 -1
  357. package/src/domains/safety/loop-detector.ts +7 -4
  358. package/src/domains/session/task-board.ts +10 -9
  359. package/src/domains/share/archive.ts +164 -7
  360. package/src/engine/acp/server.ts +62 -9
  361. package/src/engine/apis/llamacpp-residency.ts +3 -4
  362. package/src/engine/apis/lmstudio.ts +3 -3
  363. package/src/engine/apis/ollama-native.ts +6 -6
  364. package/src/engine/apis/openai-completions.ts +28 -25
  365. package/src/engine/apis/output-budget.ts +8 -18
  366. package/src/engine/apis/residency.ts +8 -27
  367. package/src/engine/gemma-channel-filter.ts +19 -0
  368. package/src/engine/loop-guard.ts +92 -12
  369. package/src/engine/worker-runtime.ts +40 -11
  370. package/src/engine/worker-tools.ts +3 -1
  371. package/src/entry/extension-hook-sources.ts +28 -0
  372. package/src/entry/extension-reload.ts +309 -0
  373. package/src/entry/orchestrator.ts +59 -35
  374. package/src/interactive/application-controller.ts +2 -1
  375. package/src/interactive/bus-notices.ts +8 -1
  376. package/src/interactive/chat-loop-messages.ts +3 -13
  377. package/src/interactive/chat-loop.ts +10 -1
  378. package/src/interactive/chat-panel.ts +36 -13
  379. package/src/interactive/chat-renderer.ts +71 -7
  380. package/src/interactive/dispatch-board.ts +6 -11
  381. package/src/interactive/footer/widgets.ts +13 -0
  382. package/src/interactive/interactive-application.ts +39 -4
  383. package/src/interactive/interactive-input-runtime.ts +4 -0
  384. package/src/interactive/interactive-presentation.ts +2 -2
  385. package/src/interactive/interactive-slash-runtime.ts +2 -0
  386. package/src/interactive/overlays/extensions.ts +9 -1
  387. package/src/interactive/overlays/help-reference.ts +13 -0
  388. package/src/interactive/overlays/settings.ts +27 -16
  389. package/src/interactive/panes-runtime.ts +111 -35
  390. package/src/interactive/prompt-cache-identity.ts +88 -0
  391. package/src/interactive/slash-commands.ts +129 -14
  392. package/src/interactive/stream-pacing-policy.ts +0 -23
  393. package/src/interactive/turn-context.ts +30 -15
  394. package/src/interactive/yazi-bridge.ts +60 -6
  395. package/src/tools/agent-tools.ts +30 -1
  396. package/src/tools/artifact.ts +2 -2
  397. package/src/tools/ask-user.ts +3 -3
  398. package/src/tools/bash.ts +1 -1
  399. package/src/tools/bootstrap.ts +4 -0
  400. package/src/tools/builtin-tool-catalog.ts +52 -22
  401. package/src/tools/codewiki/code-nav-surface.ts +6 -0
  402. package/src/tools/codewiki/code-nav.ts +99 -13
  403. package/src/tools/context/docs-engine.ts +20 -7
  404. package/src/tools/context/index.ts +29 -12
  405. package/src/tools/core-bootstrap.ts +28 -6
  406. package/src/tools/credential-present.ts +1 -2
  407. package/src/tools/dispatch-arguments.ts +5 -1
  408. package/src/tools/dispatch-plan.ts +48 -4
  409. package/src/tools/dispatch-run-events.ts +1 -1
  410. package/src/tools/dispatch-schema.ts +338 -0
  411. package/src/tools/dispatch-types.ts +3 -0
  412. package/src/tools/dispatch.ts +9 -254
  413. package/src/tools/ledger.ts +3 -5
  414. package/src/tools/monitor-surface.ts +5 -13
  415. package/src/tools/observation.ts +4 -5
  416. package/src/tools/panes-surface.ts +4 -11
  417. package/src/tools/panes.ts +4 -2
  418. package/src/tools/policy.ts +15 -2
  419. package/src/tools/read.ts +5 -6
  420. package/src/tools/registry.ts +30 -7
  421. package/src/tools/result-shaping.ts +18 -14
  422. package/src/tools/steer-surface.ts +1 -1
  423. package/src/tools/tasks.ts +1 -1
  424. package/src/tools/truncate.ts +6 -5
  425. package/src/tools/verify/surface.ts +6 -12
  426. package/src/tools/web-fetch-surface.ts +1 -3
  427. package/dist/chunk-5QIAJV2D.js +0 -48
  428. package/dist/chunk-JZWT5J3Y.js +0 -814
  429. package/dist/chunk-K7VKOLQQ.js +0 -15
  430. package/dist/chunk-PMZCIOCJ.js +0 -25
  431. package/dist/chunk-SUW5DORT.js +0 -819
  432. package/dist/chunk-UOV2BYIW.js +0 -107
  433. package/dist/chunk-WR6U3OVP.js +0 -45
  434. package/docs/artifact-versions.md +0 -67
  435. package/docs/documentation-coverage.md +0 -46
  436. package/docs/documentation-guide.md +0 -167
  437. package/docs/time-conventions.md +0 -101
@@ -1,7 +1,4 @@
1
1
  import { createRequire as __clioCreateRequire } from "node:module"; const require = __clioCreateRequire(import.meta.url);
2
- import {
3
- hashToolCall
4
- } from "./chunk-UOV2BYIW.js";
5
2
  import {
6
3
  TASK_MEMORY_DEFAULT_PROCEDURAL_CAP,
7
4
  TASK_MEMORY_POLICY_DEFAULT_TIMEOUT_MS,
@@ -10,10 +7,13 @@ import {
10
7
  } from "./chunk-I66EAJFY.js";
11
8
  import {
12
9
  isTerminalRunEnvelope
13
- } from "./chunk-2VG7KLYV.js";
10
+ } from "./chunk-IKSLQ4XV.js";
14
11
  import {
15
12
  taskBoardCounts
16
- } from "./chunk-6DWBAZ5U.js";
13
+ } from "./chunk-Q4XWMHX6.js";
14
+ import {
15
+ hashToolCall
16
+ } from "./chunk-IDNA72AH.js";
17
17
  import {
18
18
  SKILL_INSTALL_OFFER_OPTION_NEVER,
19
19
  SKILL_INSTALL_OFFER_OPTION_NOT_NOW,
@@ -25,19 +25,19 @@ import {
25
25
  import {
26
26
  legacyToolResultDigest,
27
27
  sanitizeToolResultDigest
28
- } from "./chunk-R23Z6K6I.js";
28
+ } from "./chunk-B7HM5Z7T.js";
29
29
  import {
30
30
  ToolNames
31
31
  } from "./chunk-K6BSR66V.js";
32
32
  import {
33
33
  registerForegroundStream
34
- } from "./chunk-UEDMSP56.js";
34
+ } from "./chunk-BYMNWQ7O.js";
35
35
  import {
36
36
  ceilChars
37
37
  } from "./chunk-CWVRRIEI.js";
38
38
  import {
39
39
  BusChannels
40
- } from "./chunk-HLW2MRKE.js";
40
+ } from "./chunk-F4EKGO4N.js";
41
41
  import {
42
42
  reportCommitAttributionDiagnostic,
43
43
  withManagedGitCommitAttributionEnvironment
@@ -436,6 +436,11 @@ function writeMiddlewareDiagnosticToStderr(diagnostic) {
436
436
  );
437
437
  return;
438
438
  }
439
+ if (diagnostic.kind === "registration_conflict") {
440
+ process.stderr.write(`[clio-coder:middleware] ${formatRegistrationConflict(diagnostic)}
441
+ `);
442
+ return;
443
+ }
439
444
  if (!diagnostic.steadyStateWarn && process.env.CLIO_CODER_HOOK_BUDGET_DEBUG !== "1") return;
440
445
  const stats = diagnostic.stats;
441
446
  const trend = stats.window > 0 ? ` (slow on ${stats.overCount}/${stats.window} recent ${diagnostic.hook} calls, p95 ${stats.p95Ms.toFixed(1)}ms)` : "";
@@ -444,6 +449,9 @@ function writeMiddlewareDiagnosticToStderr(diagnostic) {
444
449
  `
445
450
  );
446
451
  }
452
+ function formatRegistrationConflict(diagnostic) {
453
+ return diagnostic.action === "dropped" ? `${diagnostic.owner} registration '${diagnostic.registrationId}' (generation ${diagnostic.generation}) dropped: id is held by a ${diagnostic.conflictsWith} registration` : `${diagnostic.owner} registration '${diagnostic.registrationId}' (generation ${diagnostic.generation}) evicted by a ${diagnostic.conflictsWith} registration with the same id`;
454
+ }
447
455
  function registrationFromRuleDefinition(definition) {
448
456
  const registration = {
449
457
  id: definition.rule.id,
@@ -906,6 +914,7 @@ function createDetachedDispatchNudgeRegistration(options) {
906
914
 
907
915
  // src/domains/middleware/hook-receipts.ts
908
916
  init_esm_shims();
917
+ import { readFileSync } from "node:fs";
909
918
  var HOOK_RECEIPT_LOG_CAPACITY = 200;
910
919
  var PERSIST_THROTTLE_MS = 2e3;
911
920
  function createHookReceiptLog(options = {}) {
@@ -943,1806 +952,2023 @@ function createHookReceiptLog(options = {}) {
943
952
  }
944
953
  };
945
954
  }
955
+ function readPersistedHookReceipts(persistPath) {
956
+ let raw;
957
+ try {
958
+ raw = readFileSync(persistPath, "utf8");
959
+ } catch (error) {
960
+ if (error.code === "ENOENT") return [];
961
+ throw error;
962
+ }
963
+ const parsed = JSON.parse(raw);
964
+ if (parsed.version !== 1 || !Array.isArray(parsed.receipts)) {
965
+ throw new Error(`${persistPath}: unrecognized hook receipt log shape`);
966
+ }
967
+ return parsed.receipts;
968
+ }
946
969
 
947
- // src/domains/middleware/hooks-io.ts
970
+ // src/domains/middleware/skills-reminder.ts
948
971
  init_esm_shims();
949
- var import_yaml = __toESM(require_dist(), 1);
950
- import { spawnSync } from "node:child_process";
951
- import { readFileSync } from "node:fs";
952
- import { join } from "node:path";
972
+ var SKILLS_REMINDER_REGISTRATION_ID = "observer.skills-reminder";
973
+ function skillsReminderMessage(installed, installable = 0) {
974
+ const counts = installable > 0 ? `${installed} installed, ${installable} installable from the marketplace` : `${installed} installed`;
975
+ return `[Skills] ${counts}. Start this task by listing them with context(scope="skills") and checking for a match; if one matches, open your reply with the line \`${SKILL_SUGGESTION_ANCHOR}\` (a comma-separated sequence, in order, when several compose) and then continue the task in the same turn without the skill. Only the operator loads a skill, and a marketplace skill is offered for install when the operator runs it. If none match, do not mention skills and continue with the task.`;
976
+ }
977
+ var NO_EFFECTS = [];
978
+ var GREETING_WORDS = /* @__PURE__ */ new Set([
979
+ "hi",
980
+ "hello",
981
+ "hey",
982
+ "heya",
983
+ "hiya",
984
+ "howdy",
985
+ "yo",
986
+ "sup",
987
+ "hola",
988
+ "greetings",
989
+ "gm",
990
+ "gn",
991
+ "morning",
992
+ "afternoon",
993
+ "evening",
994
+ "good",
995
+ "day",
996
+ "night",
997
+ "there",
998
+ "you",
999
+ "clio",
1000
+ "thanks",
1001
+ "thank",
1002
+ "thankyou",
1003
+ "thx",
1004
+ "ty",
1005
+ "ok",
1006
+ "okay",
1007
+ "k",
1008
+ "cool",
1009
+ "nice",
1010
+ "test",
1011
+ "testing",
1012
+ "ping",
1013
+ "pong"
1014
+ ]);
1015
+ function isSubstantiveUserTurn(text) {
1016
+ const normalized = (text ?? "").toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
1017
+ if (normalized.length === 0) return false;
1018
+ const tokens = normalized.split(" ");
1019
+ if (tokens.length > 4) return true;
1020
+ return tokens.some((token) => !GREETING_WORDS.has(token));
1021
+ }
1022
+ function createSkillsReminderRegistration(deps) {
1023
+ let lastSeenSessionId;
1024
+ let spentForActiveSession = false;
1025
+ let observedFreshStart = false;
1026
+ let observedAnyTurn = false;
1027
+ return {
1028
+ id: SKILLS_REMINDER_REGISTRATION_ID,
1029
+ description: "once per session, on the first substantive turn, teaches the skill-suggestion reply protocol; keeps a turn going that stopped on the suggestion",
1030
+ hooks: ["turn_start", "turn_end"],
1031
+ evaluate(input) {
1032
+ if (input.hook === "turn_end") {
1033
+ return isSkillSuggestionWait(input) ? [{ kind: "request_continuation", message: SKILL_SUGGESTION_WAIT_CONTINUATION_MESSAGE }] : NO_EFFECTS;
1034
+ }
1035
+ const sessionId = input.sessionId ?? null;
1036
+ if (lastSeenSessionId === void 0) {
1037
+ lastSeenSessionId = sessionId;
1038
+ } else if (lastSeenSessionId === null && sessionId !== null) {
1039
+ lastSeenSessionId = sessionId;
1040
+ } else if (sessionId !== lastSeenSessionId) {
1041
+ lastSeenSessionId = sessionId;
1042
+ spentForActiveSession = false;
1043
+ observedFreshStart = false;
1044
+ observedAnyTurn = false;
1045
+ }
1046
+ if (spentForActiveSession) return NO_EFFECTS;
1047
+ const conversationMessages = metadataNumber(input, "conversationMessages");
1048
+ if (!observedAnyTurn) {
1049
+ observedAnyTurn = true;
1050
+ if (conversationMessages === null || conversationMessages > 0) {
1051
+ spentForActiveSession = true;
1052
+ return NO_EFFECTS;
1053
+ }
1054
+ observedFreshStart = true;
1055
+ }
1056
+ if (!observedFreshStart) {
1057
+ spentForActiveSession = true;
1058
+ return NO_EFFECTS;
1059
+ }
1060
+ if (!isSubstantiveUserTurn(input.text)) return NO_EFFECTS;
1061
+ spentForActiveSession = true;
1062
+ const pendingSkillRequests = metadataNumber(input, "pendingSkillRequests");
1063
+ if (pendingSkillRequests !== null && pendingSkillRequests > 0) return NO_EFFECTS;
1064
+ let installed = 0;
1065
+ let installable = 0;
1066
+ try {
1067
+ installed = deps.countModelVisibleSkills();
1068
+ installable = deps.countInstallableSkills?.() ?? 0;
1069
+ } catch {
1070
+ return NO_EFFECTS;
1071
+ }
1072
+ if (installed <= 0 && installable <= 0) return NO_EFFECTS;
1073
+ return [{ kind: "inject_reminder", severity: "info", message: skillsReminderMessage(installed, installable) }];
1074
+ }
1075
+ };
1076
+ }
953
1077
 
954
- // src/domains/middleware/hooks.ts
1078
+ // src/domains/middleware/marketplace-offer.ts
955
1079
  init_esm_shims();
956
- import { createHash } from "node:crypto";
957
- import { isAbsolute, resolve } from "node:path";
1080
+ import { randomUUID } from "node:crypto";
958
1081
 
959
- // src/domains/middleware/validate.ts
1082
+ // src/domains/resources/skills/promotion.ts
960
1083
  init_esm_shims();
961
- function validateMiddlewareEffect(value, source = "$") {
962
- const issues = [];
963
- const effect = readMiddlewareEffect(value, source, issues);
964
- if (issues.length > 0 || effect === null) return { valid: false, issues };
965
- return { valid: true, effect, issues: [] };
966
- }
967
- function readMiddlewareEffect(value, path2, issues) {
968
- if (!isRecord(value)) {
969
- issues.push({ path: path2, message: "expected middleware effect object" });
970
- return null;
971
- }
972
- const kindValue = value.kind;
973
- if (typeof kindValue !== "string" || !isMiddlewareEffectKind(kindValue)) {
974
- issues.push({ path: `${path2}.kind`, message: "expected known middleware effect kind" });
975
- return null;
976
- }
977
- switch (kindValue) {
978
- case "inject_reminder":
979
- return readInjectReminder(value, path2, issues);
980
- case "annotate_tool_result":
981
- return readAnnotateToolResult(value, path2, issues);
982
- case "block_tool":
983
- return readBlockTool(value, path2, issues);
984
- case "protect_path":
985
- return readProtectPath(value, path2, issues);
986
- case "request_continuation":
987
- return readRequestContinuation(value, path2, issues);
988
- case "require_tool":
989
- return readRequireTool(value, path2, issues);
990
- case "lock_tools":
991
- rejectUnexpectedFields(value, path2, ["kind"], issues);
992
- return { kind: "lock_tools" };
993
- }
1084
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
1085
+ import path from "node:path";
1086
+ var TOKEN_STOPWORDS = /* @__PURE__ */ new Set([
1087
+ "the",
1088
+ "a",
1089
+ "an",
1090
+ "and",
1091
+ "or",
1092
+ "for",
1093
+ "with",
1094
+ "this",
1095
+ "that",
1096
+ "when",
1097
+ "what",
1098
+ "how",
1099
+ "into",
1100
+ "from",
1101
+ "use",
1102
+ "used",
1103
+ "using",
1104
+ "one",
1105
+ "not",
1106
+ "skill",
1107
+ "skills",
1108
+ "clio",
1109
+ "please",
1110
+ "can",
1111
+ "you",
1112
+ "should",
1113
+ "would",
1114
+ "about",
1115
+ "need",
1116
+ "want",
1117
+ "help",
1118
+ "make",
1119
+ "file",
1120
+ "files",
1121
+ "code"
1122
+ ]);
1123
+ function normalize(text) {
1124
+ return text.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
994
1125
  }
995
- function readRequireTool(record, path2, issues) {
996
- rejectUnexpectedFields(record, path2, ["kind", "toolName"], issues);
997
- const toolName = readRequiredString(record, `${path2}.toolName`, issues);
998
- return toolName === null ? null : { kind: "require_tool", toolName };
1126
+ function distinctiveTokens(text) {
1127
+ return new Set(
1128
+ normalize(text).split(" ").filter((token) => token.length >= 4 && !TOKEN_STOPWORDS.has(token))
1129
+ );
999
1130
  }
1000
- function readInjectReminder(record, path2, issues) {
1001
- rejectUnexpectedFields(record, path2, ["kind", "message", "severity"], issues);
1002
- const message = readRequiredString(record, `${path2}.message`, issues);
1003
- const severity = readOptionalString(record, `${path2}.severity`, issues);
1004
- if (severity !== void 0 && !isMiddlewareReminderSeverity(severity)) {
1005
- issues.push({ path: `${path2}.severity`, message: "expected info, advisory, warn, or hard-block" });
1131
+ var TRIGGER_SCORE = 4;
1132
+ var TOKEN_FIRE_THRESHOLD = 3;
1133
+ function scorePromotionEntry(userText, entry) {
1134
+ const haystack = ` ${normalize(userText)} `;
1135
+ if (haystack.trim().length === 0) return null;
1136
+ for (const trigger of entry.triggers ?? []) {
1137
+ const phrase = normalize(trigger);
1138
+ if (phrase.length >= 4 && haystack.includes(` ${phrase} `)) {
1139
+ return { entry, score: TRIGGER_SCORE, matchedTrigger: trigger };
1140
+ }
1006
1141
  }
1007
- if (message === null) return null;
1008
- const effect = { kind: "inject_reminder", message };
1009
- if (severity !== void 0 && isMiddlewareReminderSeverity(severity)) effect.severity = severity;
1010
- return effect;
1142
+ const requestTokens = distinctiveTokens(userText);
1143
+ if (requestTokens.size === 0) return null;
1144
+ const entryTokens = distinctiveTokens(`${entry.name.replace(/-/g, " ")} ${entry.description}`);
1145
+ let overlap = 0;
1146
+ for (const token of requestTokens) if (entryTokens.has(token)) overlap += 1;
1147
+ return overlap >= TOKEN_FIRE_THRESHOLD ? { entry, score: overlap } : null;
1011
1148
  }
1012
- function readAnnotateToolResult(record, path2, issues) {
1013
- rejectUnexpectedFields(record, path2, ["kind", "message", "severity"], issues);
1014
- const message = readRequiredString(record, `${path2}.message`, issues);
1015
- const severity = readOptionalString(record, `${path2}.severity`, issues);
1016
- if (severity !== void 0 && !isMiddlewareAnnotationSeverity(severity)) {
1017
- issues.push({ path: `${path2}.severity`, message: "expected info or warn" });
1149
+ function matchMarketplaceSkills(userText, entries, excludedNames) {
1150
+ const matches = [];
1151
+ for (const entry of entries) {
1152
+ if (entry.kind !== "skill" || excludedNames.has(entry.name)) continue;
1153
+ const match = scorePromotionEntry(userText, entry);
1154
+ if (match) matches.push(match);
1018
1155
  }
1019
- if (message === null) return null;
1020
- const effect = { kind: "annotate_tool_result", message };
1021
- if (severity !== void 0 && isMiddlewareAnnotationSeverity(severity)) effect.severity = severity;
1022
- return effect;
1156
+ return matches.sort((a, b) => b.score - a.score || a.entry.name.localeCompare(b.entry.name));
1023
1157
  }
1024
- function readBlockTool(record, path2, issues) {
1025
- rejectUnexpectedFields(record, path2, ["kind", "reason", "severity"], issues);
1026
- const reason = readRequiredString(record, `${path2}.reason`, issues);
1027
- const severity = readRequiredString(record, `${path2}.severity`, issues);
1028
- if (severity !== null && severity !== "hard-block") {
1029
- issues.push({ path: `${path2}.severity`, message: "expected hard-block" });
1030
- }
1031
- if (reason === null || severity !== "hard-block") return null;
1032
- return { kind: "block_tool", reason, severity };
1158
+ var OWN_MARKETPLACE_URL_PREFIX = "https://github.com/iowarp/clio-coder/";
1159
+ function isOwnMarketplaceSource(entry) {
1160
+ const source = entry.sourceUrl.trim();
1161
+ if (/^(?:https?:\/\/|git@)/.test(source)) return source.startsWith(OWN_MARKETPLACE_URL_PREFIX);
1162
+ return entry.origin === "catalog" && path.isAbsolute(source);
1033
1163
  }
1034
- function readProtectPath(record, path2, issues) {
1035
- rejectUnexpectedFields(record, path2, ["kind", "path", "reason"], issues);
1036
- const protectedPath = readRequiredString(record, `${path2}.path`, issues);
1037
- const reason = readRequiredString(record, `${path2}.reason`, issues);
1038
- if (protectedPath === null || reason === null) return null;
1039
- return { kind: "protect_path", path: protectedPath, reason };
1164
+ function assertPromotionInstallSource(entry) {
1165
+ if (isOwnMarketplaceSource(entry)) return;
1166
+ throw new Error(
1167
+ `skill promotion: refusing to install "${entry.name}" from ${entry.sourceUrl}; promotion installs only from Clio's own marketplace (local catalog or ${OWN_MARKETPLACE_URL_PREFIX}). Install manually with clio-coder skills install if intended.`
1168
+ );
1040
1169
  }
1041
- function readRequestContinuation(record, path2, issues) {
1042
- rejectUnexpectedFields(record, path2, ["kind", "message"], issues);
1043
- const message = readRequiredString(record, `${path2}.message`, issues);
1044
- if (message === null) return null;
1045
- return { kind: "request_continuation", message };
1170
+ var PROMOTION_DECLINES_FILE = "skill-promotion-declines.json";
1171
+ function declineKey(name, version) {
1172
+ return `${name}@${version ?? ""}`;
1046
1173
  }
1047
- function readRequiredString(record, path2, issues) {
1048
- const value = record[pathField(path2)];
1049
- if (typeof value !== "string" || value.trim().length === 0) {
1050
- issues.push({ path: path2, message: "expected non-empty string" });
1051
- return null;
1052
- }
1053
- return value;
1174
+ function declinesPath(configDir) {
1175
+ return path.join(configDir ?? clioConfigDir(), PROMOTION_DECLINES_FILE);
1054
1176
  }
1055
- function readOptionalString(record, path2, issues) {
1056
- const field = pathField(path2);
1057
- if (!Object.hasOwn(record, field)) return void 0;
1058
- const value = record[field];
1059
- if (typeof value !== "string" || value.trim().length === 0) {
1060
- issues.push({ path: path2, message: "expected non-empty string" });
1061
- return void 0;
1177
+ function readPromotionDeclines(configDir) {
1178
+ const file = declinesPath(configDir);
1179
+ if (!existsSync(file)) return { never: {} };
1180
+ try {
1181
+ const parsed = JSON.parse(readFileSync2(file, "utf8"));
1182
+ const never = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed.never : void 0;
1183
+ if (never && typeof never === "object" && !Array.isArray(never)) {
1184
+ const entries = Object.entries(never).filter(
1185
+ (pair) => typeof pair[1] === "string"
1186
+ );
1187
+ return { never: Object.fromEntries(entries) };
1188
+ }
1189
+ } catch {
1062
1190
  }
1063
- return value;
1191
+ return { never: {} };
1064
1192
  }
1065
- function rejectUnexpectedFields(record, path2, allowedFields, issues) {
1066
- const allowed = new Set(allowedFields);
1067
- for (const field of Object.keys(record).sort((a, b) => a.localeCompare(b))) {
1068
- if (!allowed.has(field)) issues.push({ path: `${path2}.${field}`, message: "unexpected field" });
1069
- }
1193
+ function recordPromotionNeverDecline(name, version, configDir) {
1194
+ const store = readPromotionDeclines(configDir);
1195
+ store.never[declineKey(name, version)] = (/* @__PURE__ */ new Date()).toISOString();
1196
+ safeResourceWrite(declinesPath(configDir), `${JSON.stringify(store, null, 2)}
1197
+ `, { encoding: "utf8" });
1070
1198
  }
1071
- function pathField(path2) {
1072
- return path2.slice(path2.lastIndexOf(".") + 1);
1199
+
1200
+ // src/domains/middleware/marketplace-offer.ts
1201
+ var MARKETPLACE_OFFER_REGISTRATION_ID = "observer.marketplace-offer";
1202
+ var NO_EFFECTS2 = [];
1203
+ function offerBindingTag(offerTag) {
1204
+ return `[clio-install:${offerTag}]`;
1073
1205
  }
1074
- function isRecord(value) {
1075
- return typeof value === "object" && value !== null && !Array.isArray(value);
1206
+ function marketplaceOfferReminder(entry, offerTag) {
1207
+ return `[Marketplace] This request may need expertise no installed skill covers. The local marketplace has "${entry.name}" (not installed): ${entry.description} First check the installed side with context(scope="skills"); if ${entry.name} is genuinely the right fit and nothing installed serves, ask the operator with ask_user (mode=single_question, header "Install skill") whether to install ${entry.name}, offering exactly these options in this order: "${SKILL_INSTALL_OFFER_OPTION_PROJECT}", "${SKILL_INSTALL_OFFER_OPTION_USER}", "${SKILL_INSTALL_OFFER_OPTION_NOT_NOW}", "${SKILL_INSTALL_OFFER_OPTION_NEVER}". Include the exact tag ${offerBindingTag(offerTag)} verbatim in that question's text so the harness can bind the answer to this offer; the harness acts only on an answer carrying this tag. Then continue the task in the same turn. The harness handles the answer; never install or load a skill yourself. If it is not actually needed, do not mention it.`;
1076
1208
  }
1077
-
1078
- // src/domains/middleware/hooks.ts
1079
- var USER_HOOK_ORIGIN_ORDER = ["extension", "user", "project", "project.local"];
1080
- var USER_HOOK_COMMAND_TIMEOUT_DEFAULT_MS = 2e3;
1081
- var USER_HOOK_COMMAND_TIMEOUT_MIN_MS = 100;
1082
- var USER_HOOK_COMMAND_TIMEOUT_MAX_MS = 5e3;
1083
- var USER_HOOK_COMMAND_OUTPUT_MAX_CHARS = 4e3;
1084
- var USER_HOOK_PROMPT_MAX_CHARS = 2e3;
1085
- function isRecord2(value) {
1086
- return value !== null && typeof value === "object" && !Array.isArray(value);
1209
+ function marketplaceAutoInstallReminder(entry, result) {
1210
+ return `[Marketplace] Installed skill "${entry.name}" from Clio's own local marketplace (full-auto) to ${result.path}. Source ${result.sourceUrl}, sha256 ${result.installedHash}. ${entry.description} It is installed but not active \u2014 activation stays operator-gated. If it fits this task, suggest /skill ${entry.name} to the operator; never load it yourself.`;
1087
1211
  }
1088
- function stableHash(value) {
1089
- return createHash("sha256").update(canonicalJson(value)).digest("hex").slice(0, 16);
1212
+ function answersFromDetails(details) {
1213
+ const raw = details?.answers;
1214
+ if (!Array.isArray(raw)) return [];
1215
+ return raw.filter(
1216
+ (entry) => typeof entry === "object" && entry !== null && typeof entry.question === "string" && typeof entry.answer === "string"
1217
+ );
1090
1218
  }
1091
- function canonicalJson(value) {
1092
- if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
1093
- if (isRecord2(value)) {
1094
- const keys = Object.keys(value).sort();
1095
- return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
1219
+ function chosenOfferOption(answer) {
1220
+ const labels = [
1221
+ SKILL_INSTALL_OFFER_OPTION_PROJECT,
1222
+ SKILL_INSTALL_OFFER_OPTION_USER,
1223
+ SKILL_INSTALL_OFFER_OPTION_NOT_NOW,
1224
+ SKILL_INSTALL_OFFER_OPTION_NEVER
1225
+ ];
1226
+ for (const label of labels) {
1227
+ if (answer.options?.includes(label)) return label;
1096
1228
  }
1097
- return JSON.stringify(value) ?? "null";
1098
- }
1099
- function readStringArray(value) {
1100
- if (!Array.isArray(value)) return null;
1101
- const out = [];
1102
- for (const item of value) {
1103
- if (typeof item !== "string" || item.length === 0) return null;
1104
- out.push(item);
1229
+ for (const label of labels) {
1230
+ if (answer.answer.includes(label)) return label;
1105
1231
  }
1106
- return out;
1232
+ return null;
1107
1233
  }
1108
- function normalizeCommandSpec(raw, options, issues) {
1109
- const argv = readStringArray(raw.argv);
1110
- if (!argv || argv.length === 0) {
1111
- issues.push("command.argv must be a non-empty array of non-empty strings");
1112
- return null;
1113
- }
1114
- const spec = {
1115
- kind: "command",
1116
- argv,
1117
- cwd: resolve(options.workspaceRoot),
1118
- timeoutMs: USER_HOOK_COMMAND_TIMEOUT_DEFAULT_MS,
1119
- as: "annotate"
1234
+ function createMarketplaceOfferRegistration(deps) {
1235
+ const declines = deps.declines ?? {
1236
+ readNever: () => readPromotionDeclines().never,
1237
+ recordNever: (name, version) => recordPromotionNeverDecline(name, version)
1120
1238
  };
1121
- if (raw.cwd !== void 0) {
1122
- if (typeof raw.cwd !== "string" || raw.cwd.length === 0) {
1123
- issues.push("command.cwd must be a non-empty string");
1124
- return null;
1239
+ const newOfferTag = deps.newOfferTag ?? (() => randomUUID());
1240
+ let lastSeenSessionId;
1241
+ let offeredNames = /* @__PURE__ */ new Set();
1242
+ let sessionDeclines = /* @__PURE__ */ new Set();
1243
+ let installedNamesCache = null;
1244
+ let marketplaceEntriesCache = null;
1245
+ let pendingOffer = null;
1246
+ const trackSession = (input) => {
1247
+ const sessionId = input.sessionId ?? null;
1248
+ if (lastSeenSessionId === void 0 || lastSeenSessionId === null && sessionId !== null) {
1249
+ lastSeenSessionId = sessionId;
1250
+ return;
1125
1251
  }
1126
- const resolved = isAbsolute(raw.cwd) ? resolve(raw.cwd) : resolve(options.workspaceRoot, raw.cwd);
1127
- if (!isWithin(resolved, options.workspaceRoot)) {
1128
- issues.push("command.cwd must resolve under the workspace root");
1129
- return null;
1252
+ if (sessionId !== lastSeenSessionId) {
1253
+ lastSeenSessionId = sessionId;
1254
+ offeredNames = /* @__PURE__ */ new Set();
1255
+ sessionDeclines = /* @__PURE__ */ new Set();
1256
+ pendingOffer = null;
1257
+ installedNamesCache = null;
1258
+ marketplaceEntriesCache = null;
1130
1259
  }
1131
- spec.cwd = resolved;
1132
- }
1133
- if (raw.timeoutMs !== void 0) {
1134
- if (typeof raw.timeoutMs !== "number" || !Number.isFinite(raw.timeoutMs)) {
1135
- issues.push("command.timeoutMs must be a number");
1260
+ };
1261
+ const bestMatch = (text) => {
1262
+ const installedWasCached = installedNamesCache !== null;
1263
+ let never;
1264
+ try {
1265
+ installedNamesCache ??= new Set(deps.listInstalledSkillNames());
1266
+ marketplaceEntriesCache ??= deps.listMarketplaceEntries();
1267
+ never = declines.readNever();
1268
+ } catch {
1136
1269
  return null;
1137
1270
  }
1138
- spec.timeoutMs = Math.min(
1139
- USER_HOOK_COMMAND_TIMEOUT_MAX_MS,
1140
- Math.max(USER_HOOK_COMMAND_TIMEOUT_MIN_MS, Math.floor(raw.timeoutMs))
1141
- );
1142
- }
1143
- if (raw.as !== void 0) {
1144
- if (raw.as !== "annotate" && raw.as !== "reminder") {
1145
- issues.push('command.as must be "annotate" or "reminder"');
1146
- return null;
1271
+ const declinedKeys = /* @__PURE__ */ new Set([...Object.keys(never), ...sessionDeclines]);
1272
+ const findMatch = () => matchMarketplaceSkills(
1273
+ text,
1274
+ marketplaceEntriesCache ?? [],
1275
+ /* @__PURE__ */ new Set([...installedNamesCache ?? [], ...offeredNames])
1276
+ ).find((candidate) => !declinedKeys.has(declineKey(candidate.entry.name, candidate.entry.version))) ?? null;
1277
+ let match = findMatch();
1278
+ if (match && installedWasCached) {
1279
+ try {
1280
+ installedNamesCache = new Set(deps.listInstalledSkillNames());
1281
+ } catch {
1282
+ return null;
1283
+ }
1284
+ match = findMatch();
1147
1285
  }
1148
- spec.as = raw.as;
1149
- }
1150
- return spec;
1151
- }
1152
- function normalizePromptSpec(raw, issues) {
1153
- if (typeof raw.message !== "string" || raw.message.trim().length === 0) {
1154
- issues.push("prompt.message must be a non-empty string");
1155
- return null;
1156
- }
1157
- let severity = "info";
1158
- if (raw.severity !== void 0) {
1159
- if (typeof raw.severity !== "string" || !isMiddlewareReminderSeverity(raw.severity)) {
1160
- issues.push("prompt.severity must be info, advisory, warn, or hard-block");
1161
- return null;
1286
+ return match;
1287
+ };
1288
+ const installGated = (entry, scope) => {
1289
+ assertPromotionInstallSource(entry);
1290
+ const result = deps.installEntry(entry, scope);
1291
+ installedNamesCache?.add(entry.name);
1292
+ return result;
1293
+ };
1294
+ const evaluateAskUserAnswers = (input) => {
1295
+ const offer = pendingOffer;
1296
+ if (!offer) return NO_EFFECTS2;
1297
+ const marker = offerBindingTag(offer.tag);
1298
+ if (input.toolResultDetails?.cancelled === true) {
1299
+ return NO_EFFECTS2;
1162
1300
  }
1163
- severity = raw.severity;
1164
- }
1165
- return { kind: "prompt", message: raw.message.slice(0, USER_HOOK_PROMPT_MAX_CHARS), severity };
1166
- }
1167
- function normalizeEffectSpec(raw, issues) {
1168
- const result = validateMiddlewareEffect(raw.effect ?? raw, "effect");
1169
- if (!result.valid) {
1170
- for (const issue of result.issues) issues.push(`effect: ${issue.message}`);
1171
- return null;
1172
- }
1173
- return { kind: "effect", effect: result.effect };
1174
- }
1175
- function isWithin(child, parent) {
1176
- const rel = resolve(child);
1177
- const base = resolve(parent);
1178
- return rel === base || rel.startsWith(`${base}/`);
1179
- }
1180
- function normalizeUserHook(raw, source, options) {
1181
- const issues = [];
1182
- if (!isRecord2(raw)) {
1183
- return { issues: ["hook declaration must be an object"] };
1184
- }
1185
- if (raw.on === void 0 || typeof raw.on !== "string" || !isMiddlewareHook(raw.on)) {
1186
- issues.push("on must be one of before_tool, after_tool, turn_start, turn_end, on_compaction");
1187
- }
1188
- let tools;
1189
- if (raw.tools !== void 0) {
1190
- const parsed = readStringArray(raw.tools);
1191
- if (!parsed) issues.push("tools must be an array of non-empty tool names");
1192
- else tools = parsed;
1193
- }
1194
- let enabled = true;
1195
- if (raw.enabled !== void 0) {
1196
- if (typeof raw.enabled !== "boolean") issues.push("enabled must be a boolean");
1197
- else enabled = raw.enabled;
1198
- }
1199
- let spec = null;
1200
- if (raw.kind === "command") spec = normalizeCommandSpec(raw, options, issues);
1201
- else if (raw.kind === "prompt") spec = normalizePromptSpec(raw, issues);
1202
- else if (raw.kind === "effect") spec = normalizeEffectSpec(raw, issues);
1203
- else issues.push('kind must be "command", "prompt", or "effect"');
1204
- if (issues.length > 0 || spec === null || raw.on === void 0 || typeof raw.on !== "string") {
1205
- return { issues };
1206
- }
1207
- const on = raw.on;
1208
- const hash = stableHash({ on, tools: tools ?? null, spec });
1209
- const id = typeof raw.id === "string" && raw.id.length > 0 ? raw.id : `${source.sourceId ?? source.origin}.${spec.kind}.${hash}`;
1210
- const hook = { id, source, hash, on, enabled, spec };
1211
- if (tools !== void 0) hook.tools = tools;
1212
- return { hook, issues };
1213
- }
1214
- function declarationsArray(declarations) {
1215
- if (Array.isArray(declarations)) return declarations;
1216
- if (isRecord2(declarations) && Array.isArray(declarations.hooks)) return declarations.hooks;
1217
- return [];
1301
+ for (const answer of answersFromDetails(input.toolResultDetails)) {
1302
+ if (!answer.question.includes(marker)) continue;
1303
+ const option = chosenOfferOption(answer);
1304
+ if (option === null) continue;
1305
+ pendingOffer = null;
1306
+ const offerKey = declineKey(offer.entry.name, offer.entry.version);
1307
+ if (option === SKILL_INSTALL_OFFER_OPTION_NOT_NOW) {
1308
+ sessionDeclines.add(offerKey);
1309
+ return NO_EFFECTS2;
1310
+ }
1311
+ if (option === SKILL_INSTALL_OFFER_OPTION_NEVER) {
1312
+ sessionDeclines.add(offerKey);
1313
+ try {
1314
+ declines.recordNever(offer.entry.name, offer.entry.version);
1315
+ } catch {
1316
+ }
1317
+ return NO_EFFECTS2;
1318
+ }
1319
+ const scope = option === SKILL_INSTALL_OFFER_OPTION_USER ? "user" : "project";
1320
+ try {
1321
+ const result = installGated(offer.entry, scope);
1322
+ return [
1323
+ {
1324
+ kind: "annotate_tool_result",
1325
+ severity: "info",
1326
+ message: `[Marketplace] Installed skill "${offer.entry.name}" (${scope} scope) to ${result.path}. It is installed but not active; the operator activates it with /skill ${offer.entry.name}.`
1327
+ }
1328
+ ];
1329
+ } catch (error) {
1330
+ sessionDeclines.add(offerKey);
1331
+ return [
1332
+ {
1333
+ kind: "annotate_tool_result",
1334
+ severity: "warn",
1335
+ message: `[Marketplace] Install of "${offer.entry.name}" failed: ${error instanceof Error ? error.message : String(error)}. Continue the task without it.`
1336
+ }
1337
+ ];
1338
+ }
1339
+ }
1340
+ return NO_EFFECTS2;
1341
+ };
1342
+ return {
1343
+ id: MARKETPLACE_OFFER_REGISTRATION_ID,
1344
+ description: "offers an uninstalled marketplace skill via ask_user when the request matches it; consented installs run gated to Clio's own marketplace, declines are remembered",
1345
+ hooks: ["turn_start", "after_tool"],
1346
+ evaluate(input) {
1347
+ trackSession(input);
1348
+ if (input.hook === "after_tool") {
1349
+ return input.toolName === ToolNames.AskUser ? evaluateAskUserAnswers(input) : NO_EFFECTS2;
1350
+ }
1351
+ if (input.hook !== "turn_start") return NO_EFFECTS2;
1352
+ if (!isSubstantiveUserTurn(input.text)) return NO_EFFECTS2;
1353
+ const pendingSkillRequests = metadataNumber(input, "pendingSkillRequests");
1354
+ if (pendingSkillRequests !== null && pendingSkillRequests > 0) return NO_EFFECTS2;
1355
+ const match = bestMatch(input.text ?? "");
1356
+ if (!match) return NO_EFFECTS2;
1357
+ offeredNames.add(match.entry.name);
1358
+ if (deps.getAutonomy() === "full-auto") {
1359
+ try {
1360
+ const result = installGated(match.entry, "project");
1361
+ return [
1362
+ {
1363
+ kind: "inject_reminder",
1364
+ severity: "info",
1365
+ message: marketplaceAutoInstallReminder(match.entry, result)
1366
+ }
1367
+ ];
1368
+ } catch {
1369
+ }
1370
+ }
1371
+ const tag = newOfferTag();
1372
+ pendingOffer = { entry: match.entry, tag };
1373
+ return [{ kind: "inject_reminder", severity: "info", message: marketplaceOfferReminder(match.entry, tag) }];
1374
+ }
1375
+ };
1218
1376
  }
1219
- function loadUserHooks(batches, options) {
1220
- const ordered = [...batches].sort(
1221
- (a, b) => USER_HOOK_ORIGIN_ORDER.indexOf(a.source.origin) - USER_HOOK_ORIGIN_ORDER.indexOf(b.source.origin)
1222
- );
1223
- const byId = /* @__PURE__ */ new Map();
1224
- const overridden = [];
1225
- const issues = [];
1226
- for (const batch of ordered) {
1227
- const declarations = declarationsArray(batch.declarations);
1228
- declarations.forEach((raw, index) => {
1229
- const result = normalizeUserHook(raw, batch.source, options);
1230
- if (!result.hook) {
1231
- issues.push({ source: batch.source, index, issues: result.issues });
1232
- return;
1377
+
1378
+ // src/domains/middleware/memory-intervention.ts
1379
+ init_esm_shims();
1380
+ import { createHash } from "node:crypto";
1381
+ var MEMORY_INTERVENTION_REGISTRATION_ID = "observer.memory-intervention";
1382
+ var MEMORY_INTERVENTION_DEFAULT_WINDOW_STEPS = 8;
1383
+ var MEMORY_INTERVENTION_DEFAULT_MAX_TOKENS = 2e3;
1384
+ var MEMORY_INTERVENTION_DEFAULT_EVERY_N_TOOLS = 10;
1385
+ var MEMORY_INTERVENTION_TIMEOUT_BACKOFF_THRESHOLD = 2;
1386
+ var MEMORY_INTERVENTION_ACTIVITY_LIMIT = 20;
1387
+ var CALL_DESCRIPTION_MAX_CHARS = 180;
1388
+ var NO_EFFECTS3 = [];
1389
+ function createMemoryInterventionRegistration(deps) {
1390
+ const pending = /* @__PURE__ */ new Map();
1391
+ const trajectory = [];
1392
+ const failures = /* @__PURE__ */ new Map();
1393
+ let toolStep = 0;
1394
+ let lastTurnEndStep = 0;
1395
+ let reactivateAfterCompaction = false;
1396
+ let lastInjectedOperationFingerprint = null;
1397
+ let currentTask = "(current task unavailable)";
1398
+ let toolsSinceMemoryStep = 0;
1399
+ let consecutiveErrors = 0;
1400
+ let lastPromptedBoundary = null;
1401
+ let lastInjectedMessage = null;
1402
+ let lastDecision = null;
1403
+ const pendingTriggers = /* @__PURE__ */ new Set();
1404
+ const activity = [];
1405
+ const annotatedThisTurn = /* @__PURE__ */ new Set();
1406
+ let telemetryBankSnapshot = deps.bank.snapshot();
1407
+ let promptedStepInFlight = false;
1408
+ let promptedStepTier = "rules";
1409
+ let outstandingStep = Promise.resolve();
1410
+ let rulesInjectedSincePromptedStep = false;
1411
+ let annotatedSinceTurnEnd = false;
1412
+ let consecutiveLlmTimeouts = 0;
1413
+ return {
1414
+ id: MEMORY_INTERVENTION_REGISTRATION_ID,
1415
+ description: "maintain bounded task execution memory and selectively remind after repeated failures or compaction",
1416
+ hooks: ["before_tool", "after_tool", "turn_start", "turn_end", "on_compaction"],
1417
+ evaluate(input) {
1418
+ if (!settings().enabled) return NO_EFFECTS3;
1419
+ try {
1420
+ switch (input.hook) {
1421
+ case "before_tool":
1422
+ observeBeforeTool(input);
1423
+ return NO_EFFECTS3;
1424
+ case "after_tool":
1425
+ return observeAfterTool(input);
1426
+ case "turn_end": {
1427
+ if (toolStep <= lastTurnEndStep) return NO_EFFECTS3;
1428
+ const alreadyReported = annotatedSinceTurnEnd;
1429
+ annotatedSinceTurnEnd = false;
1430
+ const started = process.hrtime.bigint();
1431
+ const effects = decideRepeatedFailure();
1432
+ if (alreadyReported && effects.length === 0) return effects;
1433
+ emitTelemetry(
1434
+ [effects.length > 0 ? "repeated_failure" : "turn_end"],
1435
+ "rules",
1436
+ effects.length > 0 ? "injected" : "silent",
1437
+ effects.length > 0 ? "intervened" : "no_repeated_failure",
1438
+ citedEntryCount(effects[0]?.kind === "inject_reminder" ? effects[0].message : null),
1439
+ 0,
1440
+ 0,
1441
+ started
1442
+ );
1443
+ return effects;
1444
+ }
1445
+ case "on_compaction":
1446
+ if (input.metadata?.stage === "working_set_recall") return NO_EFFECTS3;
1447
+ reactivateAfterCompaction = true;
1448
+ return NO_EFFECTS3;
1449
+ case "turn_start": {
1450
+ if (input.text?.trim()) currentTask = shortText(input.text, 2e3);
1451
+ annotatedThisTurn.clear();
1452
+ const shouldReactivate = reactivateAfterCompaction;
1453
+ const started = process.hrtime.bigint();
1454
+ const effects = reactivateKnowledge();
1455
+ if (shouldReactivate) {
1456
+ emitTelemetry(
1457
+ ["post_compaction"],
1458
+ "rules",
1459
+ effects.length > 0 ? "injected" : "silent",
1460
+ effects.length > 0 ? "intervened" : "bank_empty",
1461
+ citedEntryCount(effects[0]?.kind === "inject_reminder" ? effects[0].message : null),
1462
+ 0,
1463
+ 0,
1464
+ started
1465
+ );
1466
+ }
1467
+ return effects;
1468
+ }
1469
+ }
1470
+ } catch {
1471
+ return NO_EFFECTS3;
1233
1472
  }
1234
- const existing = byId.get(result.hook.id);
1235
- if (existing) overridden.push({ winner: batch.source, loser: existing });
1236
- byId.set(result.hook.id, result.hook);
1237
- });
1473
+ },
1474
+ /**
1475
+ * Kicks off the background memory step and returns immediately. The pi
1476
+ * agent does not go idle until every `agent_end` listener settles, so
1477
+ * awaiting a local memory model here would stall the visible turn for its
1478
+ * full latency. Small local models measure in tens of seconds, which is far
1479
+ * past any tolerable end-of-turn pause and past the policy timeout itself.
1480
+ * The reminder is delivered through `onDeferredReminder` instead, landing
1481
+ * in the next submitted turn, which is where an awaited turn_end reminder
1482
+ * would have been buffered anyway.
1483
+ */
1484
+ async evaluateAsync(input, context) {
1485
+ if (!settings().enabled || input.hook !== "turn_end" || pendingTriggers.size === 0) return NO_EFFECTS3;
1486
+ const boundary = input.turnId ?? input.metadata?.userTurnId?.toString() ?? `tool-step:${toolStep}`;
1487
+ if (boundary === lastPromptedBoundary) return NO_EFFECTS3;
1488
+ if (deps.deliversDeferredReminders === false) {
1489
+ emitTelemetry([...pendingTriggers], "rules", "silent", "no_consumer", 0, 0, 0, process.hrtime.bigint());
1490
+ pendingTriggers.clear();
1491
+ return NO_EFFECTS3;
1492
+ }
1493
+ if (promptedStepInFlight) {
1494
+ emitTelemetry(
1495
+ [...pendingTriggers],
1496
+ promptedStepTier,
1497
+ "dropped",
1498
+ "step_in_flight",
1499
+ 0,
1500
+ 0,
1501
+ 0,
1502
+ process.hrtime.bigint()
1503
+ );
1504
+ return NO_EFFECTS3;
1505
+ }
1506
+ lastPromptedBoundary = boundary;
1507
+ const triggers = [...pendingTriggers];
1508
+ pendingTriggers.clear();
1509
+ toolsSinceMemoryStep = 0;
1510
+ consecutiveErrors = 0;
1511
+ const rulesAlreadySpoke = rulesInjectedSincePromptedStep || (context?.priorEffects.some(
1512
+ (effect) => effect.kind === "inject_reminder" && effect.message.startsWith("Memory:")
1513
+ ) ?? false);
1514
+ rulesInjectedSincePromptedStep = false;
1515
+ promptedStepInFlight = true;
1516
+ outstandingStep = runPromptedStep({
1517
+ deterministicTrigger: triggers.some((trigger) => trigger !== "interval"),
1518
+ suppressIntervention: rulesAlreadySpoke,
1519
+ triggerReasons: triggers
1520
+ }).then((result) => {
1521
+ if (rulesAlreadySpoke && result.decision === "silent") lastDecision = "injected";
1522
+ if (result.reminder === null) return;
1523
+ lastInjectedMessage = result.reminder;
1524
+ deps.onDeferredReminder?.(result.reminder);
1525
+ }).catch(() => {
1526
+ }).finally(() => {
1527
+ promptedStepInFlight = false;
1528
+ });
1529
+ return NO_EFFECTS3;
1530
+ },
1531
+ runPromptedStep,
1532
+ signalLoop() {
1533
+ if (settings().enabled) pendingTriggers.add("loop_signal");
1534
+ },
1535
+ lastDecision: () => lastDecision,
1536
+ recentActivity: () => [...activity],
1537
+ stepInFlight: () => promptedStepInFlight,
1538
+ whenIdle: () => outstandingStep
1539
+ };
1540
+ function settings() {
1541
+ const live = deps.getSettings?.();
1542
+ return {
1543
+ enabled: live?.enabled ?? deps.enabled ?? true,
1544
+ everyNTools: Math.max(
1545
+ 2,
1546
+ positiveInteger(live?.everyNTools ?? deps.everyNTools, MEMORY_INTERVENTION_DEFAULT_EVERY_N_TOOLS)
1547
+ ),
1548
+ windowSteps: positiveInteger(live?.windowSteps ?? deps.windowSteps, MEMORY_INTERVENTION_DEFAULT_WINDOW_STEPS),
1549
+ maxTokens: positiveInteger(live?.maxTokens ?? deps.maxTokens, MEMORY_INTERVENTION_DEFAULT_MAX_TOKENS),
1550
+ timeoutMs: positiveInteger(live?.timeoutMs ?? deps.timeoutMs, TASK_MEMORY_POLICY_DEFAULT_TIMEOUT_MS)
1551
+ };
1238
1552
  }
1239
- return { hooks: [...byId.values()], overridden, issues };
1240
- }
1241
- function reminderEffect(message, severity) {
1242
- return severity === "info" ? { kind: "inject_reminder", message } : { kind: "inject_reminder", message, severity };
1243
- }
1244
- function commandEffect(hook, output) {
1245
- const message = output.slice(0, USER_HOOK_COMMAND_OUTPUT_MAX_CHARS);
1246
- return hook.as === "reminder" ? { kind: "inject_reminder", message } : { kind: "annotate_tool_result", message };
1247
- }
1248
- function userHookToRegistration(hook, deps) {
1249
- const now = deps.now ?? Date.now;
1250
- const baseReceipt = (input) => ({
1251
- at: now(),
1252
- hookId: hook.id,
1253
- origin: hook.source.origin,
1254
- sourcePath: hook.source.sourcePath,
1255
- hash: hook.hash,
1256
- hook: hook.on,
1257
- kind: hook.spec.kind,
1258
- ...input.toolName !== void 0 ? { toolName: input.toolName } : {}
1259
- });
1260
- const registration = {
1261
- id: hook.id,
1262
- description: `${hook.spec.kind} hook from ${hook.source.origin} (${hook.source.sourcePath})`,
1263
- hooks: [hook.on],
1264
- evaluate: (input) => {
1265
- if (!hook.enabled) return [];
1553
+ async function runPromptedStep(input) {
1554
+ const silent = (reason) => ({
1555
+ decision: "silent",
1556
+ reason,
1557
+ bankOperations: 0,
1558
+ droppedOperations: 0,
1559
+ reminder: null,
1560
+ inputTokens: 0,
1561
+ outputTokens: 0,
1562
+ usage: null,
1563
+ effects: NO_EFFECTS3
1564
+ });
1565
+ const live = settings();
1566
+ if (!live.enabled) {
1567
+ const result = silent("no_client");
1568
+ lastDecision = result.decision;
1569
+ return result;
1570
+ }
1571
+ const started = process.hrtime.bigint();
1572
+ const triggers = input.triggerReasons?.length ? input.triggerReasons : ["manual"];
1573
+ let tier = "rules";
1574
+ promptedStepTier = tier;
1575
+ let promptedResult;
1576
+ let telemetryDecision = null;
1577
+ try {
1578
+ if (consecutiveLlmTimeouts >= MEMORY_INTERVENTION_TIMEOUT_BACKOFF_THRESHOLD) {
1579
+ promptedResult = silent("llm_timeout_backoff");
1580
+ } else {
1581
+ const client = deps.getModelClient?.() ?? null;
1582
+ if (client === null) {
1583
+ promptedResult = silent("no_client");
1584
+ } else if (deps.backgroundEndpointBusy?.() === true) {
1585
+ tier = "llm";
1586
+ promptedStepTier = tier;
1587
+ promptedResult = silent("endpoint_busy");
1588
+ telemetryDecision = "dropped";
1589
+ } else {
1590
+ tier = "llm";
1591
+ promptedStepTier = tier;
1592
+ promptedResult = await runTaskMemoryPolicy(deps.bank, client, {
1593
+ task: input.task?.trim() || currentTask,
1594
+ trajectory: [...trajectory],
1595
+ deterministicTrigger: input.deterministicTrigger,
1596
+ maxTokens: live.maxTokens,
1597
+ modelMaxTokens: positiveInteger(deps.getModelMaxTokens?.(live.maxTokens), live.maxTokens),
1598
+ ...input.suppressIntervention === void 0 ? {} : { suppressIntervention: input.suppressIntervention },
1599
+ previousReminder: lastInjectedMessage,
1600
+ timeoutMs: live.timeoutMs,
1601
+ ...deps.onEnvelope === void 0 ? {} : { onEnvelope: deps.onEnvelope }
1602
+ });
1603
+ }
1604
+ }
1605
+ } catch (error) {
1606
+ promptedResult = silent("client_error");
1607
+ deps.onEnvelope?.({
1608
+ systemPrompt: "",
1609
+ userPrompt: "",
1610
+ response: "",
1611
+ decision: "silent",
1612
+ reason: "client_error",
1613
+ bankOperations: 0,
1614
+ droppedOperations: 0,
1615
+ reminder: null,
1616
+ error: error instanceof Error ? error.message : "resolving the background model client failed"
1617
+ });
1618
+ }
1619
+ if (tier === "llm" && promptedResult.decision === "timeout") consecutiveLlmTimeouts += 1;
1620
+ else if (tier === "llm" && telemetryDecision !== "dropped") consecutiveLlmTimeouts = 0;
1621
+ lastDecision = promptedResult.decision;
1622
+ if (promptedResult.usage !== null) {
1266
1623
  try {
1267
- return evaluateHook(hook, deps, baseReceipt(input));
1624
+ deps.onStepUsage?.(promptedResult.usage);
1268
1625
  } catch {
1269
- deps.recordReceipt({ ...baseReceipt(input), outcome: "skipped" });
1270
- return [];
1271
1626
  }
1272
1627
  }
1273
- };
1274
- if (hook.tools !== void 0) registration.toolNames = [...hook.tools];
1275
- return registration;
1276
- }
1277
- function evaluateHook(hook, deps, base) {
1278
- if (hook.spec.kind === "prompt") {
1279
- const effect2 = reminderEffect(hook.spec.message, hook.spec.severity);
1280
- deps.recordReceipt({ ...base, outcome: "emitted", effectKinds: ["inject_reminder"] });
1281
- return [effect2];
1628
+ if (promptedResult.decision === "injected") reportInjectedEntries(promptedResult.reminder);
1629
+ emitTelemetry(
1630
+ triggers,
1631
+ tier,
1632
+ telemetryDecision ?? promptedResult.decision,
1633
+ promptedResult.reason,
1634
+ citedEntryCount(promptedResult.reminder),
1635
+ promptedResult.inputTokens,
1636
+ promptedResult.outputTokens,
1637
+ started,
1638
+ { bankOperations: promptedResult.bankOperations, droppedOperations: promptedResult.droppedOperations }
1639
+ );
1640
+ return {
1641
+ ...promptedResult,
1642
+ effects: promptedResult.reminder === null ? NO_EFFECTS3 : [{ kind: "inject_reminder", message: promptedResult.reminder, severity: "advisory" }]
1643
+ };
1644
+ }
1645
+ function emitTelemetry(triggerReasons, tier, decision, reason, citedEntries2, inputTokens, outputTokens, started, operations = { bankOperations: 0, droppedOperations: 0 }) {
1646
+ const next = deps.bank.snapshot();
1647
+ const bankDelta = taskMemoryBankDelta(telemetryBankSnapshot, next);
1648
+ telemetryBankSnapshot = next;
1649
+ const latencyMs = Number(process.hrtime.bigint() - started) / 1e6;
1650
+ try {
1651
+ deps.telemetry?.record({
1652
+ triggerReasons,
1653
+ tier,
1654
+ bankDelta,
1655
+ decision,
1656
+ reason,
1657
+ bankOperations: operations.bankOperations,
1658
+ droppedOperations: operations.droppedOperations,
1659
+ citedEntries: citedEntries2,
1660
+ inputTokens,
1661
+ outputTokens,
1662
+ latencyMs
1663
+ });
1664
+ } catch {
1665
+ }
1666
+ const event = {
1667
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1668
+ triggerReasons: [...new Set(triggerReasons)],
1669
+ tier,
1670
+ decision,
1671
+ reason,
1672
+ citedEntries: citedEntries2,
1673
+ bankWrites: countBankWrites(bankDelta),
1674
+ latencyMs
1675
+ };
1676
+ activity.unshift(event);
1677
+ if (activity.length > MEMORY_INTERVENTION_ACTIVITY_LIMIT) activity.length = MEMORY_INTERVENTION_ACTIVITY_LIMIT;
1282
1678
  }
1283
- if (hook.spec.kind === "effect") {
1284
- deps.recordReceipt({ ...base, outcome: "emitted", effectKinds: [hook.spec.effect.kind] });
1285
- return [hook.spec.effect];
1679
+ function citedEntries(message) {
1680
+ if (message === null) return [];
1681
+ const snapshot = deps.bank.snapshot();
1682
+ return [...snapshot.knowledge, ...snapshot.procedural].filter((entry) => message.includes(`[${entry.id}]`));
1286
1683
  }
1287
- const result = deps.runCommand(hook.spec.argv, {
1288
- timeoutMs: hook.spec.timeoutMs,
1289
- ...hook.spec.cwd !== void 0 ? { cwd: hook.spec.cwd } : {}
1290
- });
1291
- const output = (result.stdout || result.stderr || "").trim();
1292
- if (result.timedOut) {
1293
- deps.recordReceipt({ ...base, outcome: "command-timeout", exitCode: result.code ?? -1, outputChars: output.length });
1294
- return [];
1684
+ function citedEntryCount(message) {
1685
+ return citedEntries(message).length;
1295
1686
  }
1296
- const outcome = result.code === 0 ? "command-ok" : "command-failed";
1297
- deps.recordReceipt({ ...base, outcome, exitCode: result.code ?? -1, outputChars: output.length });
1298
- if (output.length === 0) return [];
1299
- const effect = commandEffect(hook.spec, output);
1300
- return [effect];
1301
- }
1302
-
1303
- // src/domains/middleware/hooks-io.ts
1304
- var COMMAND_OUTPUT_MAX_BYTES = 1024 * 1024;
1305
- function readBatch(source, filePath, fileIssues) {
1306
- let text;
1307
- try {
1308
- text = readFileSync(filePath, "utf8");
1309
- } catch {
1310
- return null;
1687
+ function reportInjectedEntries(reminder) {
1688
+ if (deps.onInjectedEntries === void 0) return;
1689
+ const entries = citedEntries(reminder);
1690
+ if (entries.length === 0) return;
1691
+ try {
1692
+ deps.onInjectedEntries(entries);
1693
+ } catch {
1694
+ }
1311
1695
  }
1312
- try {
1313
- const parsed = (0, import_yaml.parse)(text);
1314
- return { source, declarations: parsed ?? [] };
1315
- } catch (err) {
1316
- fileIssues.push({
1317
- source,
1318
- message: `failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`
1319
- });
1320
- return null;
1696
+ function observeBeforeTool(input) {
1697
+ const prepared = prepareToolStep(input);
1698
+ if (prepared === null) return;
1699
+ setBounded(pending, pendingKey(input, prepared.operationFingerprint), prepared, settings().windowSteps);
1321
1700
  }
1322
- }
1323
- function readHookSources(options) {
1324
- const fileIssues = [];
1325
- const batches = [];
1326
- for (const extension of options.extensions ?? []) {
1327
- const source = {
1328
- origin: "extension",
1329
- sourcePath: `${extension.id}:hooks.yaml`,
1330
- sourceId: extension.id
1701
+ function observeAfterTool(input) {
1702
+ const fallback = prepareToolStep(input);
1703
+ if (fallback === null) return NO_EFFECTS3;
1704
+ const key = pendingKey(input, fallback.operationFingerprint);
1705
+ const prepared = pending.get(key) ?? fallback;
1706
+ pending.delete(key);
1707
+ const outcome = input.metadata?.resultKind === "error" ? "error" : "ok";
1708
+ toolStep += 1;
1709
+ toolsSinceMemoryStep += 1;
1710
+ const live = settings();
1711
+ if (toolsSinceMemoryStep >= live.everyNTools) pendingTriggers.add("interval");
1712
+ if (outcome === "error") {
1713
+ consecutiveErrors += 1;
1714
+ if (consecutiveErrors >= 2) pendingTriggers.add("tool_error_streak");
1715
+ } else {
1716
+ consecutiveErrors = 0;
1717
+ }
1718
+ const digest = resultDigest(input, outcome);
1719
+ const step = {
1720
+ ...prepared,
1721
+ step: toolStep,
1722
+ outcome,
1723
+ resultDigest: digest.text,
1724
+ resultDigestProvenance: digest.provenance
1331
1725
  };
1332
- const batch = readBatch(source, join(extension.rootPath, "hooks.yaml"), fileIssues);
1333
- if (batch) batches.push(batch);
1726
+ trajectory.push(step);
1727
+ if (trajectory.length > live.windowSteps) trajectory.splice(0, trajectory.length - live.windowSteps);
1728
+ if (outcome !== "error") return NO_EFFECTS3;
1729
+ rememberFailure(step);
1730
+ return annotateRepeatedFailure(step);
1334
1731
  }
1335
- const projectBatch = readBatch(
1336
- { origin: "project", sourcePath: ".clio-coder/hooks.yaml" },
1337
- join(options.cwd, ".clio-coder", "hooks.yaml"),
1338
- fileIssues
1339
- );
1340
- if (projectBatch) batches.push(projectBatch);
1341
- const localBatch = readBatch(
1342
- { origin: "project.local", sourcePath: ".clio-coder/hooks.local.yaml" },
1343
- join(options.cwd, ".clio-coder", "hooks.local.yaml"),
1344
- fileIssues
1345
- );
1346
- if (localBatch) batches.push(localBatch);
1347
- return { batches, fileIssues };
1348
- }
1349
- function installUserHooks(options) {
1350
- const workspaceRoot = options.workspaceRoot ?? options.cwd;
1351
- const readOptions = { cwd: options.cwd };
1352
- if (options.extensions !== void 0) readOptions.extensions = options.extensions;
1353
- const { batches, fileIssues } = readHookSources(readOptions);
1354
- const loaded = loadUserHooks(batches, { workspaceRoot });
1355
- const runCommand = options.runCommand ?? spawnSyncCommandRunner();
1356
- for (const hook of loaded.hooks) {
1357
- options.registerHook(
1358
- userHookToRegistration(hook, {
1359
- recordReceipt: options.recordReceipt,
1360
- runCommand,
1361
- ...options.now !== void 0 ? { now: options.now } : {}
1362
- })
1732
+ function annotateRepeatedFailure(step) {
1733
+ if (annotatedThisTurn.has(step.operationFingerprint)) return NO_EFFECTS3;
1734
+ const occurrences = trajectory.filter(
1735
+ (candidate) => candidate.outcome === "error" && candidate.operationFingerprint === step.operationFingerprint
1736
+ ).length;
1737
+ const failure = failures.get(step.operationFingerprint);
1738
+ if (occurrences < 2 || failure === void 0) return NO_EFFECTS3;
1739
+ const message = boundedReminder(
1740
+ `Memory: [${failure.entryId}] you already tried ${failure.callDescription} at step ${failure.firstStep} and it failed with ${failure.errorDigest}. Change the approach rather than repeating it.`,
1741
+ settings().maxTokens
1363
1742
  );
1364
- }
1365
- return { ...loaded, fileIssues };
1366
- }
1367
- function spawnSyncCommandRunner() {
1368
- return (argv, options) => {
1369
- const [command, ...args] = argv;
1370
- const attribution = withManagedGitCommitAttributionEnvironment(process.env, {
1371
- cwd: options.cwd ?? process.cwd(),
1372
- evidence: {}
1373
- });
1374
- reportCommitAttributionDiagnostic(attribution.diagnostic);
1375
- const result = spawnSync(command ?? "", args, {
1376
- ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
1377
- timeout: options.timeoutMs,
1378
- maxBuffer: COMMAND_OUTPUT_MAX_BYTES,
1379
- encoding: "utf8",
1380
- shell: false,
1381
- env: attribution.env
1382
- });
1383
- const timedOut = result.error !== void 0 && result.error.code === "ETIMEDOUT";
1384
- return {
1385
- code: result.status,
1386
- timedOut,
1387
- stdout: typeof result.stdout === "string" ? result.stdout : "",
1388
- stderr: typeof result.stderr === "string" ? result.stderr : ""
1389
- };
1390
- };
1391
- }
1392
-
1393
- // src/domains/middleware/skills-reminder.ts
1394
- init_esm_shims();
1395
- var SKILLS_REMINDER_REGISTRATION_ID = "observer.skills-reminder";
1396
- function skillsReminderMessage(installed, installable = 0) {
1397
- const counts = installable > 0 ? `${installed} installed, ${installable} installable from the marketplace` : `${installed} installed`;
1398
- return `[Skills] ${counts}. Start this task by listing them with context(scope="skills") and checking for a match; if one matches, open your reply with the line \`${SKILL_SUGGESTION_ANCHOR}\` (a comma-separated sequence, in order, when several compose) and then continue the task in the same turn without the skill. Only the operator loads a skill, and a marketplace skill is offered for install when the operator runs it. If none match, do not mention skills and continue with the task.`;
1399
- }
1400
- var NO_EFFECTS = [];
1401
- var GREETING_WORDS = /* @__PURE__ */ new Set([
1402
- "hi",
1403
- "hello",
1404
- "hey",
1405
- "heya",
1406
- "hiya",
1407
- "howdy",
1408
- "yo",
1409
- "sup",
1410
- "hola",
1411
- "greetings",
1412
- "gm",
1413
- "gn",
1414
- "morning",
1415
- "afternoon",
1416
- "evening",
1417
- "good",
1418
- "day",
1419
- "night",
1420
- "there",
1421
- "you",
1422
- "clio",
1423
- "thanks",
1424
- "thank",
1425
- "thankyou",
1426
- "thx",
1427
- "ty",
1428
- "ok",
1429
- "okay",
1430
- "k",
1431
- "cool",
1432
- "nice",
1433
- "test",
1434
- "testing",
1435
- "ping",
1436
- "pong"
1437
- ]);
1438
- function isSubstantiveUserTurn(text) {
1439
- const normalized = (text ?? "").toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
1440
- if (normalized.length === 0) return false;
1441
- const tokens = normalized.split(" ");
1442
- if (tokens.length > 4) return true;
1443
- return tokens.some((token) => !GREETING_WORDS.has(token));
1444
- }
1445
- function createSkillsReminderRegistration(deps) {
1446
- let lastSeenSessionId;
1447
- let spentForActiveSession = false;
1448
- let observedFreshStart = false;
1449
- let observedAnyTurn = false;
1450
- return {
1451
- id: SKILLS_REMINDER_REGISTRATION_ID,
1452
- description: "once per session, on the first substantive turn, teaches the skill-suggestion reply protocol; keeps a turn going that stopped on the suggestion",
1453
- hooks: ["turn_start", "turn_end"],
1454
- evaluate(input) {
1455
- if (input.hook === "turn_end") {
1456
- return isSkillSuggestionWait(input) ? [{ kind: "request_continuation", message: SKILL_SUGGESTION_WAIT_CONTINUATION_MESSAGE }] : NO_EFFECTS;
1457
- }
1458
- const sessionId = input.sessionId ?? null;
1459
- if (lastSeenSessionId === void 0) {
1460
- lastSeenSessionId = sessionId;
1461
- } else if (lastSeenSessionId === null && sessionId !== null) {
1462
- lastSeenSessionId = sessionId;
1463
- } else if (sessionId !== lastSeenSessionId) {
1464
- lastSeenSessionId = sessionId;
1465
- spentForActiveSession = false;
1466
- observedFreshStart = false;
1467
- observedAnyTurn = false;
1468
- }
1469
- if (spentForActiveSession) return NO_EFFECTS;
1470
- const conversationMessages = metadataNumber(input, "conversationMessages");
1471
- if (!observedAnyTurn) {
1472
- observedAnyTurn = true;
1473
- if (conversationMessages === null || conversationMessages > 0) {
1474
- spentForActiveSession = true;
1475
- return NO_EFFECTS;
1476
- }
1477
- observedFreshStart = true;
1478
- }
1479
- if (!observedFreshStart) {
1480
- spentForActiveSession = true;
1481
- return NO_EFFECTS;
1482
- }
1483
- if (!isSubstantiveUserTurn(input.text)) return NO_EFFECTS;
1484
- spentForActiveSession = true;
1485
- const pendingSkillRequests = metadataNumber(input, "pendingSkillRequests");
1486
- if (pendingSkillRequests !== null && pendingSkillRequests > 0) return NO_EFFECTS;
1487
- let installed = 0;
1488
- let installable = 0;
1743
+ if (message.length === 0) return NO_EFFECTS3;
1744
+ annotatedThisTurn.add(step.operationFingerprint);
1745
+ lastInjectedOperationFingerprint = step.operationFingerprint;
1746
+ lastDecision = "injected";
1747
+ rulesInjectedSincePromptedStep = true;
1748
+ annotatedSinceTurnEnd = true;
1749
+ deps.bank.recordInjection([failure.entryId]);
1750
+ emitTelemetry(["repeated_failure"], "rules", "injected", "intervened", 1, 0, 0, process.hrtime.bigint());
1751
+ return [{ kind: "annotate_tool_result", message, severity: "warn" }];
1752
+ }
1753
+ function rememberFailure(step) {
1754
+ const previous = failures.get(step.operationFingerprint);
1755
+ const attempts = (previous?.attempts ?? 0) + 1;
1756
+ const firstStep = previous?.firstStep ?? step.step;
1757
+ const content = proceduralContent(step.callDescription, attempts, firstStep, step.resultDigest);
1758
+ let entryId;
1759
+ if (previous === void 0) {
1760
+ entryId = deps.bank.saveProcedural(content).id;
1761
+ } else {
1489
1762
  try {
1490
- installed = deps.countModelVisibleSkills();
1491
- installable = deps.countInstallableSkills?.() ?? 0;
1763
+ entryId = deps.bank.saveProcedural(content, { id: previous.entryId }).id;
1492
1764
  } catch {
1493
- return NO_EFFECTS;
1765
+ entryId = deps.bank.saveProcedural(content).id;
1494
1766
  }
1495
- if (installed <= 0 && installable <= 0) return NO_EFFECTS;
1496
- return [{ kind: "inject_reminder", severity: "info", message: skillsReminderMessage(installed, installable) }];
1497
1767
  }
1498
- };
1499
- }
1500
-
1501
- // src/domains/middleware/marketplace-offer.ts
1502
- init_esm_shims();
1503
- import { randomUUID } from "node:crypto";
1504
-
1505
- // src/domains/resources/skills/promotion.ts
1506
- init_esm_shims();
1507
- import { existsSync, readFileSync as readFileSync2 } from "node:fs";
1508
- import path from "node:path";
1509
- var TOKEN_STOPWORDS = /* @__PURE__ */ new Set([
1510
- "the",
1511
- "a",
1512
- "an",
1513
- "and",
1514
- "or",
1515
- "for",
1516
- "with",
1517
- "this",
1518
- "that",
1519
- "when",
1520
- "what",
1521
- "how",
1522
- "into",
1523
- "from",
1524
- "use",
1525
- "used",
1526
- "using",
1527
- "one",
1528
- "not",
1529
- "skill",
1530
- "skills",
1531
- "clio",
1532
- "please",
1533
- "can",
1534
- "you",
1535
- "should",
1536
- "would",
1537
- "about",
1538
- "need",
1539
- "want",
1540
- "help",
1541
- "make",
1542
- "file",
1543
- "files",
1544
- "code"
1545
- ]);
1546
- function normalize(text) {
1547
- return text.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
1548
- }
1549
- function distinctiveTokens(text) {
1550
- return new Set(
1551
- normalize(text).split(" ").filter((token) => token.length >= 4 && !TOKEN_STOPWORDS.has(token))
1552
- );
1553
- }
1554
- var TRIGGER_SCORE = 4;
1555
- var TOKEN_FIRE_THRESHOLD = 3;
1556
- function scorePromotionEntry(userText, entry) {
1557
- const haystack = ` ${normalize(userText)} `;
1558
- if (haystack.trim().length === 0) return null;
1559
- for (const trigger of entry.triggers ?? []) {
1560
- const phrase = normalize(trigger);
1561
- if (phrase.length >= 4 && haystack.includes(` ${phrase} `)) {
1562
- return { entry, score: TRIGGER_SCORE, matchedTrigger: trigger };
1768
+ setBounded(
1769
+ failures,
1770
+ step.operationFingerprint,
1771
+ {
1772
+ entryId,
1773
+ attempts,
1774
+ firstStep,
1775
+ callDescription: step.callDescription,
1776
+ errorDigest: step.resultDigest
1777
+ },
1778
+ TASK_MEMORY_DEFAULT_PROCEDURAL_CAP
1779
+ );
1780
+ }
1781
+ function decideRepeatedFailure() {
1782
+ try {
1783
+ for (let index = trajectory.length - 1; index >= 0; index -= 1) {
1784
+ const step = trajectory[index];
1785
+ if (step === void 0 || step.outcome !== "error" || step.step <= lastTurnEndStep || step.operationFingerprint === lastInjectedOperationFingerprint) {
1786
+ continue;
1787
+ }
1788
+ const occurrences = trajectory.filter(
1789
+ (candidate) => candidate.outcome === "error" && candidate.operationFingerprint === step.operationFingerprint
1790
+ ).length;
1791
+ const failure = failures.get(step.operationFingerprint);
1792
+ if (occurrences < 2 || failure === void 0) continue;
1793
+ const message = boundedReminder(
1794
+ `Memory: [${failure.entryId}] you already tried ${failure.callDescription} at step ${failure.firstStep} and it failed with ${failure.errorDigest}.`,
1795
+ settings().maxTokens
1796
+ );
1797
+ if (message.length === 0) return NO_EFFECTS3;
1798
+ lastInjectedOperationFingerprint = step.operationFingerprint;
1799
+ lastInjectedMessage = message;
1800
+ lastDecision = "injected";
1801
+ rulesInjectedSincePromptedStep = true;
1802
+ deps.bank.recordInjection([failure.entryId]);
1803
+ return [{ kind: "inject_reminder", message, severity: "advisory" }];
1804
+ }
1805
+ lastDecision = "silent";
1806
+ return NO_EFFECTS3;
1807
+ } finally {
1808
+ lastTurnEndStep = toolStep;
1563
1809
  }
1564
1810
  }
1565
- const requestTokens = distinctiveTokens(userText);
1566
- if (requestTokens.size === 0) return null;
1567
- const entryTokens = distinctiveTokens(`${entry.name.replace(/-/g, " ")} ${entry.description}`);
1568
- let overlap = 0;
1569
- for (const token of requestTokens) if (entryTokens.has(token)) overlap += 1;
1570
- return overlap >= TOKEN_FIRE_THRESHOLD ? { entry, score: overlap } : null;
1571
- }
1572
- function matchMarketplaceSkills(userText, entries, excludedNames) {
1573
- const matches = [];
1574
- for (const entry of entries) {
1575
- if (entry.kind !== "skill" || excludedNames.has(entry.name)) continue;
1576
- const match = scorePromotionEntry(userText, entry);
1577
- if (match) matches.push(match);
1811
+ function reactivateKnowledge() {
1812
+ if (!reactivateAfterCompaction) return NO_EFFECTS3;
1813
+ reactivateAfterCompaction = false;
1814
+ const prefix = "Memory: execution state restored after compaction:\n";
1815
+ const maxTokens = settings().maxTokens;
1816
+ const prefixTokens = ceilChars(prefix.length);
1817
+ const rendered = deps.bank.renderRestoredState(Math.max(0, maxTokens - prefixTokens));
1818
+ if (rendered.length === 0) {
1819
+ lastDecision = "silent";
1820
+ return NO_EFFECTS3;
1821
+ }
1822
+ const message = boundedReminder(`${prefix}${rendered}`, maxTokens);
1823
+ if (message.length === 0) {
1824
+ lastDecision = "silent";
1825
+ return NO_EFFECTS3;
1826
+ }
1827
+ const citedIds = [...message.matchAll(/\[([^\]]+)\]/gu)].map((match) => match[1]).filter((id) => id !== void 0);
1828
+ deps.bank.recordInjection(citedIds);
1829
+ lastInjectedMessage = message;
1830
+ lastDecision = "injected";
1831
+ return [{ kind: "inject_reminder", message, severity: "advisory" }];
1578
1832
  }
1579
- return matches.sort((a, b) => b.score - a.score || a.entry.name.localeCompare(b.entry.name));
1580
- }
1581
- var OWN_MARKETPLACE_URL_PREFIX = "https://github.com/iowarp/clio-coder/";
1582
- function isOwnMarketplaceSource(entry) {
1583
- const source = entry.sourceUrl.trim();
1584
- if (/^(?:https?:\/\/|git@)/.test(source)) return source.startsWith(OWN_MARKETPLACE_URL_PREFIX);
1585
- return entry.origin === "catalog" && path.isAbsolute(source);
1586
- }
1587
- function assertPromotionInstallSource(entry) {
1588
- if (isOwnMarketplaceSource(entry)) return;
1589
- throw new Error(
1590
- `skill promotion: refusing to install "${entry.name}" from ${entry.sourceUrl}; promotion installs only from Clio's own marketplace (local catalog or ${OWN_MARKETPLACE_URL_PREFIX}). Install manually with clio-coder skills install if intended.`
1591
- );
1592
1833
  }
1593
- var PROMOTION_DECLINES_FILE = "skill-promotion-declines.json";
1594
- function declineKey(name, version) {
1595
- return `${name}@${version ?? ""}`;
1834
+ function prepareToolStep(input) {
1835
+ const toolName = input.toolName?.trim();
1836
+ if (!toolName) return null;
1837
+ const canonical = hashToolCall(toolName, input.toolArgs ?? {});
1838
+ return {
1839
+ toolName,
1840
+ operationFingerprint: createHash("sha256").update(canonical).digest("hex").slice(0, 16),
1841
+ callDescription: shortText(canonical, CALL_DESCRIPTION_MAX_CHARS)
1842
+ };
1596
1843
  }
1597
- function declinesPath(configDir) {
1598
- return path.join(configDir ?? clioConfigDir(), PROMOTION_DECLINES_FILE);
1844
+ function pendingKey(input, operationFingerprint) {
1845
+ return input.toolCallId ?? `anonymous:${operationFingerprint}`;
1599
1846
  }
1600
- function readPromotionDeclines(configDir) {
1601
- const file = declinesPath(configDir);
1602
- if (!existsSync(file)) return { never: {} };
1603
- try {
1604
- const parsed = JSON.parse(readFileSync2(file, "utf8"));
1605
- const never = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed.never : void 0;
1606
- if (never && typeof never === "object" && !Array.isArray(never)) {
1607
- const entries = Object.entries(never).filter(
1608
- (pair) => typeof pair[1] === "string"
1609
- );
1610
- return { never: Object.fromEntries(entries) };
1847
+ function resultDigest(input, outcome) {
1848
+ if (input.toolResultDigest !== void 0) return sanitizeToolResultDigest(input.toolResultDigest);
1849
+ if (outcome === "error") {
1850
+ const metadataMessage = input.metadata?.errorMessage;
1851
+ if (typeof metadataMessage === "string" && metadataMessage.trim().length > 0) {
1852
+ return legacyToolResultDigest(diagnosticLine(metadataMessage), { outcome });
1611
1853
  }
1612
- } catch {
1854
+ for (const key of ["error", "message"]) {
1855
+ const value = input.toolResultDetails?.[key];
1856
+ if (typeof value === "string" && value.trim().length > 0) {
1857
+ return legacyToolResultDigest(diagnosticLine(value), { outcome });
1858
+ }
1859
+ }
1860
+ return legacyToolResultDigest("", { outcome });
1613
1861
  }
1614
- return { never: {} };
1862
+ const resultFingerprint = input.metadata?.resultFingerprint;
1863
+ return legacyToolResultDigest(
1864
+ typeof resultFingerprint === "string" ? `ok result ${resultFingerprint.slice(0, 16)}` : "ok",
1865
+ { outcome }
1866
+ );
1615
1867
  }
1616
- function recordPromotionNeverDecline(name, version, configDir) {
1617
- const store = readPromotionDeclines(configDir);
1618
- store.never[declineKey(name, version)] = (/* @__PURE__ */ new Date()).toISOString();
1619
- safeResourceWrite(declinesPath(configDir), `${JSON.stringify(store, null, 2)}
1620
- `, { encoding: "utf8" });
1868
+ var DIAGNOSTIC_HINT = /\b(error|failed|failure|cannot|unable|expected|refused|denied|missing|not found|timed out|timeout|fatal)\b/iu;
1869
+ function diagnosticLine(value) {
1870
+ const lines = value.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0);
1871
+ return lines.find((line) => DIAGNOSTIC_HINT.test(line)) ?? lines[0] ?? "";
1621
1872
  }
1622
-
1623
- // src/domains/middleware/marketplace-offer.ts
1624
- var MARKETPLACE_OFFER_REGISTRATION_ID = "observer.marketplace-offer";
1625
- var NO_EFFECTS2 = [];
1626
- function offerBindingTag(offerTag) {
1627
- return `[clio-install:${offerTag}]`;
1873
+ function proceduralContent(call, attempts, firstStep, error) {
1874
+ return `${call} failed ${attempts} time${attempts === 1 ? "" : "s"}; first observed at step ${firstStep}: ${error}.`;
1628
1875
  }
1629
- function marketplaceOfferReminder(entry, offerTag) {
1630
- return `[Marketplace] This request may need expertise no installed skill covers. The local marketplace has "${entry.name}" (not installed): ${entry.description} First check the installed side with context(scope="skills"); if ${entry.name} is genuinely the right fit and nothing installed serves, ask the operator with ask_user (mode=single_question, header "Install skill") whether to install ${entry.name}, offering exactly these options in this order: "${SKILL_INSTALL_OFFER_OPTION_PROJECT}", "${SKILL_INSTALL_OFFER_OPTION_USER}", "${SKILL_INSTALL_OFFER_OPTION_NOT_NOW}", "${SKILL_INSTALL_OFFER_OPTION_NEVER}". Include the exact tag ${offerBindingTag(offerTag)} verbatim in that question's text so the harness can bind the answer to this offer; the harness acts only on an answer carrying this tag. Then continue the task in the same turn. The harness handles the answer; never install or load a skill yourself. If it is not actually needed, do not mention it.`;
1876
+ function boundedReminder(message, maxTokens) {
1877
+ const maxChars = maxTokens * 4;
1878
+ if (maxChars <= 0) return "";
1879
+ const normalized = message.replace(/[^\S\n]+/gu, " ").trim();
1880
+ return normalized.length <= maxChars ? normalized : normalized.slice(0, maxChars).trimEnd();
1631
1881
  }
1632
- function marketplaceAutoInstallReminder(entry, result) {
1633
- return `[Marketplace] Installed skill "${entry.name}" from Clio's own local marketplace (full-auto) to ${result.path}. Source ${result.sourceUrl}, sha256 ${result.installedHash}. ${entry.description} It is installed but not active \u2014 activation stays operator-gated. If it fits this task, suggest /skill ${entry.name} to the operator; never load it yourself.`;
1882
+ function shortText(value, maxChars) {
1883
+ const normalized = value.replace(/\s+/gu, " ").trim();
1884
+ return normalized.length <= maxChars ? normalized : `${normalized.slice(0, Math.max(0, maxChars - 1)).trimEnd()}\u2026`;
1885
+ }
1886
+ function positiveInteger(value, fallback) {
1887
+ return value !== void 0 && Number.isInteger(value) && value > 0 ? value : fallback;
1634
1888
  }
1635
- function answersFromDetails(details) {
1636
- const raw = details?.answers;
1637
- if (!Array.isArray(raw)) return [];
1638
- return raw.filter(
1639
- (entry) => typeof entry === "object" && entry !== null && typeof entry.question === "string" && typeof entry.answer === "string"
1889
+ function countBankWrites(delta) {
1890
+ return [delta.status, delta.knowledge, delta.procedural].reduce(
1891
+ (total, entry) => total + entry.added + entry.updated + entry.deleted,
1892
+ 0
1640
1893
  );
1641
1894
  }
1642
- function chosenOfferOption(answer) {
1643
- const labels = [
1644
- SKILL_INSTALL_OFFER_OPTION_PROJECT,
1645
- SKILL_INSTALL_OFFER_OPTION_USER,
1646
- SKILL_INSTALL_OFFER_OPTION_NOT_NOW,
1647
- SKILL_INSTALL_OFFER_OPTION_NEVER
1648
- ];
1649
- for (const label of labels) {
1650
- if (answer.options?.includes(label)) return label;
1651
- }
1652
- for (const label of labels) {
1653
- if (answer.answer.includes(label)) return label;
1895
+ function setBounded(map, key, value, capacity) {
1896
+ if (!map.has(key) && map.size >= capacity) {
1897
+ const oldest = map.keys().next().value;
1898
+ if (oldest !== void 0) map.delete(oldest);
1654
1899
  }
1655
- return null;
1900
+ map.set(key, value);
1656
1901
  }
1657
- function createMarketplaceOfferRegistration(deps) {
1658
- const declines = deps.declines ?? {
1659
- readNever: () => readPromotionDeclines().never,
1660
- recordNever: (name, version) => recordPromotionNeverDecline(name, version)
1661
- };
1662
- const newOfferTag = deps.newOfferTag ?? (() => randomUUID());
1663
- let lastSeenSessionId;
1664
- let offeredNames = /* @__PURE__ */ new Set();
1665
- let sessionDeclines = /* @__PURE__ */ new Set();
1666
- let installedNamesCache = null;
1667
- let marketplaceEntriesCache = null;
1668
- let pendingOffer = null;
1669
- const trackSession = (input) => {
1670
- const sessionId = input.sessionId ?? null;
1671
- if (lastSeenSessionId === void 0 || lastSeenSessionId === null && sessionId !== null) {
1672
- lastSeenSessionId = sessionId;
1673
- return;
1674
- }
1675
- if (sessionId !== lastSeenSessionId) {
1676
- lastSeenSessionId = sessionId;
1677
- offeredNames = /* @__PURE__ */ new Set();
1678
- sessionDeclines = /* @__PURE__ */ new Set();
1679
- pendingOffer = null;
1680
- installedNamesCache = null;
1681
- marketplaceEntriesCache = null;
1682
- }
1683
- };
1684
- const bestMatch = (text) => {
1685
- const installedWasCached = installedNamesCache !== null;
1686
- let never;
1902
+
1903
+ // src/domains/middleware/memory-step-endpoint.ts
1904
+ init_esm_shims();
1905
+ function announceMemoryStepEndpoint(deps, complete) {
1906
+ const { bus, endpointKey, targetId } = deps;
1907
+ if (endpointKey === null) return complete;
1908
+ return async (request) => {
1909
+ const releaseEndpointSlot = registerForegroundStream(endpointKey);
1687
1910
  try {
1688
- installedNamesCache ??= new Set(deps.listInstalledSkillNames());
1689
- marketplaceEntriesCache ??= deps.listMarketplaceEntries();
1690
- never = declines.readNever();
1691
- } catch {
1692
- return null;
1693
- }
1694
- const declinedKeys = /* @__PURE__ */ new Set([...Object.keys(never), ...sessionDeclines]);
1695
- const findMatch = () => matchMarketplaceSkills(
1696
- text,
1697
- marketplaceEntriesCache ?? [],
1698
- /* @__PURE__ */ new Set([...installedNamesCache ?? [], ...offeredNames])
1699
- ).find((candidate) => !declinedKeys.has(declineKey(candidate.entry.name, candidate.entry.version))) ?? null;
1700
- let match = findMatch();
1701
- if (match && installedWasCached) {
1702
- try {
1703
- installedNamesCache = new Set(deps.listInstalledSkillNames());
1704
- } catch {
1705
- return null;
1706
- }
1707
- match = findMatch();
1708
- }
1709
- return match;
1710
- };
1711
- const installGated = (entry, scope) => {
1712
- assertPromotionInstallSource(entry);
1713
- const result = deps.installEntry(entry, scope);
1714
- installedNamesCache?.add(entry.name);
1715
- return result;
1716
- };
1717
- const evaluateAskUserAnswers = (input) => {
1718
- const offer = pendingOffer;
1719
- if (!offer) return NO_EFFECTS2;
1720
- const marker = offerBindingTag(offer.tag);
1721
- if (input.toolResultDetails?.cancelled === true) {
1722
- return NO_EFFECTS2;
1723
- }
1724
- for (const answer of answersFromDetails(input.toolResultDetails)) {
1725
- if (!answer.question.includes(marker)) continue;
1726
- const option = chosenOfferOption(answer);
1727
- if (option === null) continue;
1728
- pendingOffer = null;
1729
- const offerKey = declineKey(offer.entry.name, offer.entry.version);
1730
- if (option === SKILL_INSTALL_OFFER_OPTION_NOT_NOW) {
1731
- sessionDeclines.add(offerKey);
1732
- return NO_EFFECTS2;
1733
- }
1734
- if (option === SKILL_INSTALL_OFFER_OPTION_NEVER) {
1735
- sessionDeclines.add(offerKey);
1911
+ return await complete(request);
1912
+ } finally {
1913
+ releaseEndpointSlot();
1914
+ if (bus !== null) {
1736
1915
  try {
1737
- declines.recordNever(offer.entry.name, offer.entry.version);
1916
+ bus.emit(BusChannels.MemoryStepCompleted, { endpointKey, targetId });
1738
1917
  } catch {
1739
1918
  }
1740
- return NO_EFFECTS2;
1741
- }
1742
- const scope = option === SKILL_INSTALL_OFFER_OPTION_USER ? "user" : "project";
1743
- try {
1744
- const result = installGated(offer.entry, scope);
1745
- return [
1746
- {
1747
- kind: "annotate_tool_result",
1748
- severity: "info",
1749
- message: `[Marketplace] Installed skill "${offer.entry.name}" (${scope} scope) to ${result.path}. It is installed but not active; the operator activates it with /skill ${offer.entry.name}.`
1750
- }
1751
- ];
1752
- } catch (error) {
1753
- sessionDeclines.add(offerKey);
1754
- return [
1755
- {
1756
- kind: "annotate_tool_result",
1757
- severity: "warn",
1758
- message: `[Marketplace] Install of "${offer.entry.name}" failed: ${error instanceof Error ? error.message : String(error)}. Continue the task without it.`
1759
- }
1760
- ];
1761
1919
  }
1762
1920
  }
1763
- return NO_EFFECTS2;
1764
1921
  };
1922
+ }
1923
+
1924
+ // src/domains/middleware/task-board-reminder.ts
1925
+ init_esm_shims();
1926
+ var TASK_BOARD_REMINDER_REGISTRATION_ID = "observer.task-board-reminder";
1927
+ function taskBoardReminderMessage(steps) {
1928
+ return `[Tasks] This request enumerates ${steps} steps. Before the first edit, declare the task board: tasks action="plan" with a title and one task per step, then work them one at a time ("start", then "done" with an evidence note).`;
1929
+ }
1930
+ function countEnumeratedSteps(text) {
1931
+ const value = text ?? "";
1932
+ if (value.trim().length === 0) return 0;
1933
+ const markers = [];
1934
+ const markerPattern = /(?:^|[\s(])(?:step\s+)?([1-9])\s*[).:]\s/gi;
1935
+ let match = markerPattern.exec(value);
1936
+ while (match !== null) {
1937
+ const digit = Number(match[1]);
1938
+ if (Number.isInteger(digit)) markers.push(digit);
1939
+ match = markerPattern.exec(value);
1940
+ }
1941
+ let run = 0;
1942
+ for (const digit of markers) {
1943
+ if (digit === run + 1) run += 1;
1944
+ else if (digit === 1) run = 1;
1945
+ }
1946
+ if (run >= 3) return run;
1947
+ const bulletLines = value.split(/\r?\n/).filter((line) => /^\s*[-*]\s+\S/.test(line)).length;
1948
+ return bulletLines >= 3 ? bulletLines : 0;
1949
+ }
1950
+ var NO_EFFECTS4 = [];
1951
+ function createTaskBoardReminderRegistration() {
1952
+ let lastSeenSessionId;
1953
+ let spentForActiveSession = false;
1765
1954
  return {
1766
- id: MARKETPLACE_OFFER_REGISTRATION_ID,
1767
- description: "offers an uninstalled marketplace skill via ask_user when the request matches it; consented installs run gated to Clio's own marketplace, declines are remembered",
1768
- hooks: ["turn_start", "after_tool"],
1955
+ id: TASK_BOARD_REMINDER_REGISTRATION_ID,
1956
+ description: "once per session, on the first explicitly enumerated multi-step turn, teaches the board-first protocol",
1957
+ hooks: ["turn_start"],
1769
1958
  evaluate(input) {
1770
- trackSession(input);
1771
- if (input.hook === "after_tool") {
1772
- return input.toolName === ToolNames.AskUser ? evaluateAskUserAnswers(input) : NO_EFFECTS2;
1959
+ const sessionId = input.sessionId ?? null;
1960
+ if (lastSeenSessionId === void 0 || lastSeenSessionId === null && sessionId !== null) {
1961
+ lastSeenSessionId = sessionId;
1962
+ } else if (sessionId !== lastSeenSessionId) {
1963
+ lastSeenSessionId = sessionId;
1964
+ spentForActiveSession = false;
1773
1965
  }
1774
- if (input.hook !== "turn_start") return NO_EFFECTS2;
1775
- if (!isSubstantiveUserTurn(input.text)) return NO_EFFECTS2;
1966
+ if (spentForActiveSession) return NO_EFFECTS4;
1967
+ const steps = countEnumeratedSteps(input.text);
1968
+ if (steps < 3) return NO_EFFECTS4;
1969
+ spentForActiveSession = true;
1776
1970
  const pendingSkillRequests = metadataNumber(input, "pendingSkillRequests");
1777
- if (pendingSkillRequests !== null && pendingSkillRequests > 0) return NO_EFFECTS2;
1778
- const match = bestMatch(input.text ?? "");
1779
- if (!match) return NO_EFFECTS2;
1780
- offeredNames.add(match.entry.name);
1781
- if (deps.getAutonomy() === "full-auto") {
1782
- try {
1783
- const result = installGated(match.entry, "project");
1784
- return [
1785
- {
1786
- kind: "inject_reminder",
1787
- severity: "info",
1788
- message: marketplaceAutoInstallReminder(match.entry, result)
1789
- }
1790
- ];
1791
- } catch {
1792
- }
1971
+ if (pendingSkillRequests !== null && pendingSkillRequests > 0) return NO_EFFECTS4;
1972
+ return [{ kind: "inject_reminder", severity: "info", message: taskBoardReminderMessage(steps) }];
1973
+ }
1974
+ };
1975
+ }
1976
+
1977
+ // src/domains/middleware/task-nudge.ts
1978
+ init_esm_shims();
1979
+ var TASK_NUDGE_REGISTRATION_ID = "nudge.open-tasks";
1980
+ function openTasks(board) {
1981
+ return board.tasks.filter((task) => task.status === "pending" || task.status === "active");
1982
+ }
1983
+ function buildOpenTasksMessage(board) {
1984
+ const open = openTasks(board);
1985
+ const counts = taskBoardCounts(board);
1986
+ const rows = open.map((task) => ` ${task.status === "active" ? "[>]" : "[ ]"} ${task.id} ${task.title}`);
1987
+ return `[Clio Coder] task board "${board.title}" still has ${open.length} of ${counts.total} task(s) open:
1988
+ ${rows.join("\n")}
1989
+ Continue working them, or record the honest state on the board: tasks action="done" with an evidence note, action="block" with a reason, or action="drop". Do not end the turn with a stale board.`;
1990
+ }
1991
+ function createTaskNudgeRegistration(options) {
1992
+ return {
1993
+ id: TASK_NUDGE_REGISTRATION_ID,
1994
+ description: "carry the turn onward when a work turn ends with open task-board tasks",
1995
+ hooks: ["turn_end"],
1996
+ evaluate(input) {
1997
+ if (input.hook !== "turn_end") return [];
1998
+ const stopReason = input.metadata?.stopReason;
1999
+ if (stopReason !== void 0 && stopReason !== "stop") return [];
2000
+ const turnToolCalls = input.metadata?.turnToolCalls;
2001
+ if (typeof turnToolCalls !== "number" || turnToolCalls <= 0) return [];
2002
+ const activeToolNames = input.metadata?.activeToolNames;
2003
+ if (typeof activeToolNames === "string" && !activeToolNames.split(",").includes(ToolNames.Tasks)) return [];
2004
+ let board;
2005
+ try {
2006
+ board = options.getBoard();
2007
+ } catch {
2008
+ return [];
1793
2009
  }
1794
- const tag = newOfferTag();
1795
- pendingOffer = { entry: match.entry, tag };
1796
- return [{ kind: "inject_reminder", severity: "info", message: marketplaceOfferReminder(match.entry, tag) }];
2010
+ if (board === null || openTasks(board).length === 0) return [];
2011
+ const message = buildOpenTasksMessage(board);
2012
+ return [
2013
+ { kind: "request_continuation", message },
2014
+ { kind: "inject_reminder", message, severity: "warn" }
2015
+ ];
1797
2016
  }
1798
2017
  };
1799
2018
  }
1800
2019
 
1801
- // src/domains/middleware/memory-intervention.ts
2020
+ // src/domains/middleware/tool-choice-control.ts
1802
2021
  init_esm_shims();
1803
- import { createHash as createHash2 } from "node:crypto";
1804
- var MEMORY_INTERVENTION_REGISTRATION_ID = "observer.memory-intervention";
1805
- var MEMORY_INTERVENTION_DEFAULT_WINDOW_STEPS = 8;
1806
- var MEMORY_INTERVENTION_DEFAULT_MAX_TOKENS = 2e3;
1807
- var MEMORY_INTERVENTION_DEFAULT_EVERY_N_TOOLS = 10;
1808
- var MEMORY_INTERVENTION_TIMEOUT_BACKOFF_THRESHOLD = 2;
1809
- var MEMORY_INTERVENTION_ACTIVITY_LIMIT = 20;
1810
- var CALL_DESCRIPTION_MAX_CHARS = 180;
1811
- var NO_EFFECTS3 = [];
1812
- function createMemoryInterventionRegistration(deps) {
1813
- const pending = /* @__PURE__ */ new Map();
1814
- const trajectory = [];
1815
- const failures = /* @__PURE__ */ new Map();
1816
- let toolStep = 0;
1817
- let lastTurnEndStep = 0;
1818
- let reactivateAfterCompaction = false;
1819
- let lastInjectedOperationFingerprint = null;
1820
- let currentTask = "(current task unavailable)";
1821
- let toolsSinceMemoryStep = 0;
1822
- let consecutiveErrors = 0;
1823
- let lastPromptedBoundary = null;
1824
- let lastInjectedMessage = null;
1825
- let lastDecision = null;
1826
- const pendingTriggers = /* @__PURE__ */ new Set();
1827
- const activity = [];
1828
- const annotatedThisTurn = /* @__PURE__ */ new Set();
1829
- let telemetryBankSnapshot = deps.bank.snapshot();
1830
- let promptedStepInFlight = false;
1831
- let promptedStepTier = "rules";
1832
- let outstandingStep = Promise.resolve();
1833
- let rulesInjectedSincePromptedStep = false;
1834
- let annotatedSinceTurnEnd = false;
1835
- let consecutiveLlmTimeouts = 0;
2022
+ function createMiddlewareToolChoiceControl() {
2023
+ let choice = { kind: "auto" };
1836
2024
  return {
1837
- id: MEMORY_INTERVENTION_REGISTRATION_ID,
1838
- description: "maintain bounded task execution memory and selectively remind after repeated failures or compaction",
1839
- hooks: ["before_tool", "after_tool", "turn_start", "turn_end", "on_compaction"],
1840
- evaluate(input) {
1841
- if (!settings().enabled) return NO_EFFECTS3;
1842
- try {
1843
- switch (input.hook) {
1844
- case "before_tool":
1845
- observeBeforeTool(input);
1846
- return NO_EFFECTS3;
1847
- case "after_tool":
1848
- return observeAfterTool(input);
1849
- case "turn_end": {
1850
- if (toolStep <= lastTurnEndStep) return NO_EFFECTS3;
1851
- const alreadyReported = annotatedSinceTurnEnd;
1852
- annotatedSinceTurnEnd = false;
1853
- const started = process.hrtime.bigint();
1854
- const effects = decideRepeatedFailure();
1855
- if (alreadyReported && effects.length === 0) return effects;
1856
- emitTelemetry(
1857
- [effects.length > 0 ? "repeated_failure" : "turn_end"],
1858
- "rules",
1859
- effects.length > 0 ? "injected" : "silent",
1860
- effects.length > 0 ? "intervened" : "no_repeated_failure",
1861
- citedEntryCount(effects[0]?.kind === "inject_reminder" ? effects[0].message : null),
1862
- 0,
1863
- 0,
1864
- started
1865
- );
1866
- return effects;
1867
- }
1868
- case "on_compaction":
1869
- if (input.metadata?.stage === "working_set_recall") return NO_EFFECTS3;
1870
- reactivateAfterCompaction = true;
1871
- return NO_EFFECTS3;
1872
- case "turn_start": {
1873
- if (input.text?.trim()) currentTask = shortText(input.text, 2e3);
1874
- annotatedThisTurn.clear();
1875
- const shouldReactivate = reactivateAfterCompaction;
1876
- const started = process.hrtime.bigint();
1877
- const effects = reactivateKnowledge();
1878
- if (shouldReactivate) {
1879
- emitTelemetry(
1880
- ["post_compaction"],
1881
- "rules",
1882
- effects.length > 0 ? "injected" : "silent",
1883
- effects.length > 0 ? "intervened" : "bank_empty",
1884
- citedEntryCount(effects[0]?.kind === "inject_reminder" ? effects[0].message : null),
1885
- 0,
1886
- 0,
1887
- started
1888
- );
1889
- }
1890
- return effects;
1891
- }
2025
+ apply(effects) {
2026
+ for (const effect of effects) {
2027
+ if (effect.kind === "lock_tools") {
2028
+ choice = { kind: "none" };
2029
+ continue;
1892
2030
  }
1893
- } catch {
1894
- return NO_EFFECTS3;
1895
- }
1896
- },
1897
- /**
1898
- * Kicks off the background memory step and returns immediately. The pi
1899
- * agent does not go idle until every `agent_end` listener settles, so
1900
- * awaiting a local memory model here would stall the visible turn for its
1901
- * full latency. Small local models measure in tens of seconds, which is far
1902
- * past any tolerable end-of-turn pause and past the policy timeout itself.
1903
- * The reminder is delivered through `onDeferredReminder` instead, landing
1904
- * in the next submitted turn, which is where an awaited turn_end reminder
1905
- * would have been buffered anyway.
1906
- */
1907
- async evaluateAsync(input, context) {
1908
- if (!settings().enabled || input.hook !== "turn_end" || pendingTriggers.size === 0) return NO_EFFECTS3;
1909
- const boundary = input.turnId ?? input.metadata?.userTurnId?.toString() ?? `tool-step:${toolStep}`;
1910
- if (boundary === lastPromptedBoundary) return NO_EFFECTS3;
1911
- if (deps.deliversDeferredReminders === false) {
1912
- emitTelemetry([...pendingTriggers], "rules", "silent", "no_consumer", 0, 0, 0, process.hrtime.bigint());
1913
- pendingTriggers.clear();
1914
- return NO_EFFECTS3;
1915
- }
1916
- if (promptedStepInFlight) {
1917
- emitTelemetry(
1918
- [...pendingTriggers],
1919
- promptedStepTier,
1920
- "dropped",
1921
- "step_in_flight",
1922
- 0,
1923
- 0,
1924
- 0,
1925
- process.hrtime.bigint()
1926
- );
1927
- return NO_EFFECTS3;
1928
- }
1929
- lastPromptedBoundary = boundary;
1930
- const triggers = [...pendingTriggers];
1931
- pendingTriggers.clear();
1932
- toolsSinceMemoryStep = 0;
1933
- consecutiveErrors = 0;
1934
- const rulesAlreadySpoke = rulesInjectedSincePromptedStep || (context?.priorEffects.some(
1935
- (effect) => effect.kind === "inject_reminder" && effect.message.startsWith("Memory:")
1936
- ) ?? false);
1937
- rulesInjectedSincePromptedStep = false;
1938
- promptedStepInFlight = true;
1939
- outstandingStep = runPromptedStep({
1940
- deterministicTrigger: triggers.some((trigger) => trigger !== "interval"),
1941
- suppressIntervention: rulesAlreadySpoke,
1942
- triggerReasons: triggers
1943
- }).then((result) => {
1944
- if (rulesAlreadySpoke && result.decision === "silent") lastDecision = "injected";
1945
- if (result.reminder === null) return;
1946
- lastInjectedMessage = result.reminder;
1947
- deps.onDeferredReminder?.(result.reminder);
1948
- }).catch(() => {
1949
- }).finally(() => {
1950
- promptedStepInFlight = false;
1951
- });
1952
- return NO_EFFECTS3;
1953
- },
1954
- runPromptedStep,
1955
- signalLoop() {
1956
- if (settings().enabled) pendingTriggers.add("loop_signal");
1957
- },
1958
- lastDecision: () => lastDecision,
1959
- recentActivity: () => [...activity],
1960
- stepInFlight: () => promptedStepInFlight,
1961
- whenIdle: () => outstandingStep
1962
- };
1963
- function settings() {
1964
- const live = deps.getSettings?.();
1965
- return {
1966
- enabled: live?.enabled ?? deps.enabled ?? true,
1967
- everyNTools: Math.max(
1968
- 2,
1969
- positiveInteger(live?.everyNTools ?? deps.everyNTools, MEMORY_INTERVENTION_DEFAULT_EVERY_N_TOOLS)
1970
- ),
1971
- windowSteps: positiveInteger(live?.windowSteps ?? deps.windowSteps, MEMORY_INTERVENTION_DEFAULT_WINDOW_STEPS),
1972
- maxTokens: positiveInteger(live?.maxTokens ?? deps.maxTokens, MEMORY_INTERVENTION_DEFAULT_MAX_TOKENS),
1973
- timeoutMs: positiveInteger(live?.timeoutMs ?? deps.timeoutMs, TASK_MEMORY_POLICY_DEFAULT_TIMEOUT_MS)
1974
- };
1975
- }
1976
- async function runPromptedStep(input) {
1977
- const silent = (reason) => ({
1978
- decision: "silent",
1979
- reason,
1980
- bankOperations: 0,
1981
- droppedOperations: 0,
1982
- reminder: null,
1983
- inputTokens: 0,
1984
- outputTokens: 0,
1985
- usage: null,
1986
- effects: NO_EFFECTS3
1987
- });
1988
- const live = settings();
1989
- if (!live.enabled) {
1990
- const result = silent("no_client");
1991
- lastDecision = result.decision;
1992
- return result;
1993
- }
1994
- const started = process.hrtime.bigint();
1995
- const triggers = input.triggerReasons?.length ? input.triggerReasons : ["manual"];
1996
- let tier = "rules";
1997
- promptedStepTier = tier;
1998
- let promptedResult;
1999
- let telemetryDecision = null;
2000
- try {
2001
- if (consecutiveLlmTimeouts >= MEMORY_INTERVENTION_TIMEOUT_BACKOFF_THRESHOLD) {
2002
- promptedResult = silent("llm_timeout_backoff");
2003
- } else {
2004
- const client = deps.getModelClient?.() ?? null;
2005
- if (client === null) {
2006
- promptedResult = silent("no_client");
2007
- } else if (deps.backgroundEndpointBusy?.() === true) {
2008
- tier = "llm";
2009
- promptedStepTier = tier;
2010
- promptedResult = silent("endpoint_busy");
2011
- telemetryDecision = "dropped";
2012
- } else {
2013
- tier = "llm";
2014
- promptedStepTier = tier;
2015
- promptedResult = await runTaskMemoryPolicy(deps.bank, client, {
2016
- task: input.task?.trim() || currentTask,
2017
- trajectory: [...trajectory],
2018
- deterministicTrigger: input.deterministicTrigger,
2019
- maxTokens: live.maxTokens,
2020
- modelMaxTokens: positiveInteger(deps.getModelMaxTokens?.(live.maxTokens), live.maxTokens),
2021
- ...input.suppressIntervention === void 0 ? {} : { suppressIntervention: input.suppressIntervention },
2022
- previousReminder: lastInjectedMessage,
2023
- timeoutMs: live.timeoutMs,
2024
- ...deps.onEnvelope === void 0 ? {} : { onEnvelope: deps.onEnvelope }
2025
- });
2031
+ if (effect.kind === "require_tool" && choice.kind !== "none") {
2032
+ choice = { kind: "required", toolName: effect.toolName };
2026
2033
  }
2027
2034
  }
2028
- } catch (error) {
2029
- promptedResult = silent("client_error");
2030
- deps.onEnvelope?.({
2031
- systemPrompt: "",
2032
- userPrompt: "",
2033
- response: "",
2034
- decision: "silent",
2035
- reason: "client_error",
2036
- bankOperations: 0,
2037
- droppedOperations: 0,
2038
- reminder: null,
2039
- error: error instanceof Error ? error.message : "resolving the background model client failed"
2040
- });
2041
- }
2042
- if (tier === "llm" && promptedResult.decision === "timeout") consecutiveLlmTimeouts += 1;
2043
- else if (tier === "llm" && telemetryDecision !== "dropped") consecutiveLlmTimeouts = 0;
2044
- lastDecision = promptedResult.decision;
2045
- if (promptedResult.usage !== null) {
2046
- try {
2047
- deps.onStepUsage?.(promptedResult.usage);
2048
- } catch {
2049
- }
2035
+ },
2036
+ current: () => choice,
2037
+ toolStarted(toolName) {
2038
+ if (choice.kind === "required" && choice.toolName === toolName) choice = { kind: "auto" };
2039
+ },
2040
+ reset() {
2041
+ choice = { kind: "auto" };
2050
2042
  }
2051
- if (promptedResult.decision === "injected") reportInjectedEntries(promptedResult.reminder);
2052
- emitTelemetry(
2053
- triggers,
2054
- tier,
2055
- telemetryDecision ?? promptedResult.decision,
2056
- promptedResult.reason,
2057
- citedEntryCount(promptedResult.reminder),
2058
- promptedResult.inputTokens,
2059
- promptedResult.outputTokens,
2060
- started,
2061
- { bankOperations: promptedResult.bankOperations, droppedOperations: promptedResult.droppedOperations }
2062
- );
2063
- return {
2064
- ...promptedResult,
2065
- effects: promptedResult.reminder === null ? NO_EFFECTS3 : [{ kind: "inject_reminder", message: promptedResult.reminder, severity: "advisory" }]
2066
- };
2043
+ };
2044
+ }
2045
+
2046
+ // src/domains/middleware/watchdog.ts
2047
+ init_esm_shims();
2048
+ var WATCHDOG_REGISTRATION_ID = "observer.watchdog";
2049
+ var WATCHDOG_DIFF_MAX_BYTES = 12 * 1024;
2050
+ var WATCHDOG_PATHS_MAX = 40;
2051
+ var WATCHDOG_TRUNCATION_MARKER = "\n[diff truncated]";
2052
+ var MUTATING_TOOL_NAMES = /* @__PURE__ */ new Set([ToolNames.Write, ToolNames.Edit, ToolNames.Artifact]);
2053
+ var NO_EFFECTS5 = [];
2054
+ function stringValue2(value) {
2055
+ return typeof value === "string" && value.length > 0 ? value : null;
2056
+ }
2057
+ function firstPath(details) {
2058
+ const paths = details?.paths;
2059
+ if (!Array.isArray(paths)) return null;
2060
+ for (const entry of paths) {
2061
+ const path2 = stringValue2(entry);
2062
+ if (path2 !== null) return path2;
2067
2063
  }
2068
- function emitTelemetry(triggerReasons, tier, decision, reason, citedEntries2, inputTokens, outputTokens, started, operations = { bankOperations: 0, droppedOperations: 0 }) {
2069
- const next = deps.bank.snapshot();
2070
- const bankDelta = taskMemoryBankDelta(telemetryBankSnapshot, next);
2071
- telemetryBankSnapshot = next;
2072
- const latencyMs = Number(process.hrtime.bigint() - started) / 1e6;
2064
+ return null;
2065
+ }
2066
+ function coalesceTurnDiff(byPath, maxBytes = WATCHDOG_DIFF_MAX_BYTES) {
2067
+ const blocks = [];
2068
+ let index = 0;
2069
+ for (const [path2, diff] of byPath) {
2070
+ if (index >= WATCHDOG_PATHS_MAX) {
2071
+ blocks.push(`[${byPath.size - WATCHDOG_PATHS_MAX} more changed path(s) omitted]`);
2072
+ break;
2073
+ }
2074
+ blocks.push(`--- ${path2}
2075
+ ${diff}`);
2076
+ index += 1;
2077
+ }
2078
+ const whole = blocks.join("\n\n");
2079
+ if (Buffer.byteLength(whole, "utf8") <= maxBytes) return whole;
2080
+ const room = Math.max(0, maxBytes - Buffer.byteLength(WATCHDOG_TRUNCATION_MARKER, "utf8"));
2081
+ const buffer = Buffer.from(whole, "utf8");
2082
+ let cut = Math.min(room, buffer.byteLength);
2083
+ while (cut > 0) {
2084
+ const next = buffer[cut];
2085
+ if (next === void 0 || (next & 192) !== 128) break;
2086
+ cut -= 1;
2087
+ }
2088
+ return `${buffer.subarray(0, cut).toString("utf8")}${WATCHDOG_TRUNCATION_MARKER}`;
2089
+ }
2090
+ var WATCHDOG_TASK = "Review the coalesced diff in the briefing against the scope it names. Report one check per concern: a change outside the stated scope, a change the scope required that the diff does not make, or a defect visible in the diff itself. Run no commands and change nothing.";
2091
+ function watchdogBriefing(trigger) {
2092
+ const scope = trigger.scope === null ? "(no task board; scope is unstated)" : trigger.scope;
2093
+ const paths = trigger.paths.length === 0 ? "(none)" : trigger.paths.join("\n");
2094
+ return [
2095
+ "## Current scope",
2096
+ scope,
2097
+ "",
2098
+ "## Changed paths",
2099
+ paths,
2100
+ "",
2101
+ "## Coalesced diff for this turn",
2102
+ trigger.diff
2103
+ ].join("\n");
2104
+ }
2105
+ function createWatchdogRegistration(deps) {
2106
+ const diffByPath = /* @__PURE__ */ new Map();
2107
+ let toolCalls = 0;
2108
+ let dropped = 0;
2109
+ let inFlight = false;
2110
+ let outstanding = Promise.resolve();
2111
+ const active = () => {
2112
+ if (deps.firesOnThisSurface === false) return null;
2113
+ let settings;
2073
2114
  try {
2074
- deps.telemetry?.record({
2075
- triggerReasons,
2076
- tier,
2077
- bankDelta,
2078
- decision,
2079
- reason,
2080
- bankOperations: operations.bankOperations,
2081
- droppedOperations: operations.droppedOperations,
2082
- citedEntries: citedEntries2,
2083
- inputTokens,
2084
- outputTokens,
2085
- latencyMs
2086
- });
2115
+ settings = deps.getSettings();
2087
2116
  } catch {
2117
+ return null;
2088
2118
  }
2089
- const event = {
2090
- at: (/* @__PURE__ */ new Date()).toISOString(),
2091
- triggerReasons: [...new Set(triggerReasons)],
2092
- tier,
2093
- decision,
2119
+ return settings.enabled ? settings : null;
2120
+ };
2121
+ const resetTurn = () => {
2122
+ diffByPath.clear();
2123
+ toolCalls = 0;
2124
+ };
2125
+ const fire = (reason) => {
2126
+ if (diffByPath.size === 0) return;
2127
+ if (inFlight) {
2128
+ dropped += 1;
2129
+ return;
2130
+ }
2131
+ const trigger = {
2094
2132
  reason,
2095
- citedEntries: citedEntries2,
2096
- bankWrites: countBankWrites(bankDelta),
2097
- latencyMs
2133
+ diff: coalesceTurnDiff(diffByPath),
2134
+ paths: [...diffByPath.keys()].slice(0, WATCHDOG_PATHS_MAX),
2135
+ scope: scopeNow(),
2136
+ toolCalls
2098
2137
  };
2099
- activity.unshift(event);
2100
- if (activity.length > MEMORY_INTERVENTION_ACTIVITY_LIMIT) activity.length = MEMORY_INTERVENTION_ACTIVITY_LIMIT;
2101
- }
2102
- function citedEntries(message) {
2103
- if (message === null) return [];
2104
- const snapshot = deps.bank.snapshot();
2105
- return [...snapshot.knowledge, ...snapshot.procedural].filter((entry) => message.includes(`[${entry.id}]`));
2106
- }
2107
- function citedEntryCount(message) {
2108
- return citedEntries(message).length;
2109
- }
2110
- function reportInjectedEntries(reminder) {
2111
- if (deps.onInjectedEntries === void 0) return;
2112
- const entries = citedEntries(reminder);
2113
- if (entries.length === 0) return;
2138
+ inFlight = true;
2139
+ let started;
2114
2140
  try {
2115
- deps.onInjectedEntries(entries);
2141
+ started = deps.run(trigger);
2116
2142
  } catch {
2143
+ inFlight = false;
2144
+ return;
2117
2145
  }
2118
- }
2119
- function observeBeforeTool(input) {
2120
- const prepared = prepareToolStep(input);
2121
- if (prepared === null) return;
2122
- setBounded(pending, pendingKey(input, prepared.operationFingerprint), prepared, settings().windowSteps);
2123
- }
2124
- function observeAfterTool(input) {
2125
- const fallback = prepareToolStep(input);
2126
- if (fallback === null) return NO_EFFECTS3;
2127
- const key = pendingKey(input, fallback.operationFingerprint);
2128
- const prepared = pending.get(key) ?? fallback;
2129
- pending.delete(key);
2130
- const outcome = input.metadata?.resultKind === "error" ? "error" : "ok";
2131
- toolStep += 1;
2132
- toolsSinceMemoryStep += 1;
2133
- const live = settings();
2134
- if (toolsSinceMemoryStep >= live.everyNTools) pendingTriggers.add("interval");
2135
- if (outcome === "error") {
2136
- consecutiveErrors += 1;
2137
- if (consecutiveErrors >= 2) pendingTriggers.add("tool_error_streak");
2138
- } else {
2139
- consecutiveErrors = 0;
2146
+ outstanding = started.catch(() => {
2147
+ }).finally(() => {
2148
+ inFlight = false;
2149
+ });
2150
+ };
2151
+ const scopeNow = () => {
2152
+ try {
2153
+ return deps.getScope?.() ?? null;
2154
+ } catch {
2155
+ return null;
2140
2156
  }
2141
- const digest = resultDigest(input, outcome);
2142
- const step = {
2143
- ...prepared,
2144
- step: toolStep,
2145
- outcome,
2146
- resultDigest: digest.text,
2147
- resultDigestProvenance: digest.provenance
2148
- };
2149
- trajectory.push(step);
2150
- if (trajectory.length > live.windowSteps) trajectory.splice(0, trajectory.length - live.windowSteps);
2151
- if (outcome !== "error") return NO_EFFECTS3;
2152
- rememberFailure(step);
2153
- return annotateRepeatedFailure(step);
2154
- }
2155
- function annotateRepeatedFailure(step) {
2156
- if (annotatedThisTurn.has(step.operationFingerprint)) return NO_EFFECTS3;
2157
- const occurrences = trajectory.filter(
2158
- (candidate) => candidate.outcome === "error" && candidate.operationFingerprint === step.operationFingerprint
2159
- ).length;
2160
- const failure = failures.get(step.operationFingerprint);
2161
- if (occurrences < 2 || failure === void 0) return NO_EFFECTS3;
2162
- const message = boundedReminder(
2163
- `Memory: [${failure.entryId}] you already tried ${failure.callDescription} at step ${failure.firstStep} and it failed with ${failure.errorDigest}. Change the approach rather than repeating it.`,
2164
- settings().maxTokens
2165
- );
2166
- if (message.length === 0) return NO_EFFECTS3;
2167
- annotatedThisTurn.add(step.operationFingerprint);
2168
- lastInjectedOperationFingerprint = step.operationFingerprint;
2169
- lastDecision = "injected";
2170
- rulesInjectedSincePromptedStep = true;
2171
- annotatedSinceTurnEnd = true;
2172
- deps.bank.recordInjection([failure.entryId]);
2173
- emitTelemetry(["repeated_failure"], "rules", "injected", "intervened", 1, 0, 0, process.hrtime.bigint());
2174
- return [{ kind: "annotate_tool_result", message, severity: "warn" }];
2157
+ };
2158
+ return {
2159
+ id: WATCHDOG_REGISTRATION_ID,
2160
+ description: "review a mutating turn's coalesced diff against the board scope with a read-only verifier run",
2161
+ hooks: ["after_tool", "turn_end"],
2162
+ evaluate(input) {
2163
+ const settings = active();
2164
+ if (settings === null) {
2165
+ if (input.hook === "turn_end") resetTurn();
2166
+ return NO_EFFECTS5;
2167
+ }
2168
+ if (input.hook === "after_tool") {
2169
+ toolCalls += 1;
2170
+ recordMutation(input);
2171
+ const cadence = settings.cadenceToolCalls;
2172
+ if (cadence !== void 0 && cadence >= 1 && toolCalls % cadence === 0) fire("cadence");
2173
+ return NO_EFFECTS5;
2174
+ }
2175
+ if (input.hook !== "turn_end") return NO_EFFECTS5;
2176
+ fire("turn_end");
2177
+ resetTurn();
2178
+ return NO_EFFECTS5;
2179
+ },
2180
+ droppedTriggers: () => dropped,
2181
+ runInFlight: () => inFlight,
2182
+ whenIdle: () => outstanding
2183
+ };
2184
+ function recordMutation(input) {
2185
+ if (input.metadata?.resultKind !== "ok") return;
2186
+ if (input.toolName === void 0 || !MUTATING_TOOL_NAMES.has(input.toolName)) return;
2187
+ const diff = stringValue2(input.toolResultDetails?.diff);
2188
+ if (diff === null) return;
2189
+ diffByPath.set(firstPath(input.toolResultDetails) ?? input.toolName, diff);
2175
2190
  }
2176
- function rememberFailure(step) {
2177
- const previous = failures.get(step.operationFingerprint);
2178
- const attempts = (previous?.attempts ?? 0) + 1;
2179
- const firstStep = previous?.firstStep ?? step.step;
2180
- const content = proceduralContent(step.callDescription, attempts, firstStep, step.resultDigest);
2181
- let entryId;
2182
- if (previous === void 0) {
2183
- entryId = deps.bank.saveProcedural(content).id;
2184
- } else {
2185
- try {
2186
- entryId = deps.bank.saveProcedural(content, { id: previous.entryId }).id;
2187
- } catch {
2188
- entryId = deps.bank.saveProcedural(content).id;
2191
+ }
2192
+
2193
+ // src/domains/middleware/index.ts
2194
+ init_esm_shims();
2195
+
2196
+ // src/domains/middleware/extension.ts
2197
+ init_esm_shims();
2198
+
2199
+ // src/domains/middleware/registrations.ts
2200
+ init_esm_shims();
2201
+ var MIDDLEWARE_REGISTRATION_OWNERS = ["user-hooks"];
2202
+ function isRegistrationOwner(value) {
2203
+ return MIDDLEWARE_REGISTRATION_OWNERS.includes(value);
2204
+ }
2205
+ function createMiddlewareRegistrationTable(options) {
2206
+ const fixed = Object.freeze([...options.fixed]);
2207
+ const fixedIds = new Set(fixed.map((registration) => registration.id));
2208
+ const buildList = (host, slots) => {
2209
+ const list = [...fixed];
2210
+ const slotAt = (index) => {
2211
+ for (const owner of MIDDLEWARE_REGISTRATION_OWNERS) {
2212
+ const slot = slots.get(owner);
2213
+ if (slot !== void 0 && slot.anchor === index) list.push(...slot.registrations);
2189
2214
  }
2215
+ };
2216
+ for (let index = 0; index < host.length; index += 1) {
2217
+ slotAt(index);
2218
+ list.push(host[index]);
2190
2219
  }
2191
- setBounded(
2192
- failures,
2193
- step.operationFingerprint,
2194
- {
2195
- entryId,
2196
- attempts,
2197
- firstStep,
2198
- callDescription: step.callDescription,
2199
- errorDigest: step.resultDigest
2200
- },
2201
- TASK_MEMORY_DEFAULT_PROCEDURAL_CAP
2202
- );
2203
- }
2204
- function decideRepeatedFailure() {
2220
+ slotAt(host.length);
2221
+ return Object.freeze(list);
2222
+ };
2223
+ const makeState = (host, slots) => Object.freeze({
2224
+ host: Object.freeze([...host]),
2225
+ hostIds: new Set(host.map((registration) => registration.id)),
2226
+ slots: new Map(slots),
2227
+ list: buildList(host, slots)
2228
+ });
2229
+ let state = makeState([], /* @__PURE__ */ new Map());
2230
+ const emit = (diagnostic) => {
2205
2231
  try {
2206
- for (let index = trajectory.length - 1; index >= 0; index -= 1) {
2207
- const step = trajectory[index];
2208
- if (step === void 0 || step.outcome !== "error" || step.step <= lastTurnEndStep || step.operationFingerprint === lastInjectedOperationFingerprint) {
2232
+ options.diagnosticSink()(diagnostic);
2233
+ } catch {
2234
+ }
2235
+ };
2236
+ const slotFor = (base, owner) => base.slots.get(owner) ?? { generation: 0, registrations: [], anchor: base.host.length };
2237
+ const disposeGeneration = (owner, generation) => {
2238
+ const base = state;
2239
+ const held = base.slots.get(owner);
2240
+ if (held === void 0 || held.generation !== generation || held.registrations.length === 0) return;
2241
+ const slots = new Map(base.slots);
2242
+ slots.set(owner, { ...held, registrations: Object.freeze([]) });
2243
+ state = makeState(base.host, slots);
2244
+ };
2245
+ const table = {
2246
+ list: () => state.list,
2247
+ registerHook(registration) {
2248
+ const base = state;
2249
+ if (fixedIds.has(registration.id) || base.hostIds.has(registration.id)) return;
2250
+ const evictions = [];
2251
+ const slots = new Map(base.slots);
2252
+ for (const [owner, slot] of base.slots) {
2253
+ if (!slot.registrations.some((entry) => entry.id === registration.id)) continue;
2254
+ slots.set(owner, {
2255
+ ...slot,
2256
+ registrations: Object.freeze(slot.registrations.filter((entry) => entry.id !== registration.id))
2257
+ });
2258
+ evictions.push({
2259
+ kind: "registration_conflict",
2260
+ registrationId: registration.id,
2261
+ owner,
2262
+ generation: slot.generation,
2263
+ conflictsWith: "host",
2264
+ action: "evicted"
2265
+ });
2266
+ }
2267
+ state = makeState([...base.host, registration], slots);
2268
+ for (const eviction of evictions) emit(eviction);
2269
+ },
2270
+ prepareReplacement(owner, generation, registrations) {
2271
+ if (!isRegistrationOwner(owner)) {
2272
+ return { status: "rejected", owner, reason: "unknown-owner", activeGeneration: 0 };
2273
+ }
2274
+ const base = state;
2275
+ const slot = slotFor(base, owner);
2276
+ if (!Number.isInteger(generation) || generation <= slot.generation) {
2277
+ return { status: "rejected", owner, reason: "stale", activeGeneration: slot.generation };
2278
+ }
2279
+ const dropped = [];
2280
+ const conflicts = [];
2281
+ const accepted = [];
2282
+ const seen = /* @__PURE__ */ new Set();
2283
+ for (const registration of registrations) {
2284
+ const conflictsWith = fixedIds.has(registration.id) ? "builtin" : base.hostIds.has(registration.id) ? "host" : seen.has(registration.id) ? "owned" : null;
2285
+ if (conflictsWith !== null) {
2286
+ dropped.push({ id: registration.id, conflictsWith });
2287
+ conflicts.push({
2288
+ kind: "registration_conflict",
2289
+ registrationId: registration.id,
2290
+ owner,
2291
+ generation,
2292
+ conflictsWith,
2293
+ action: "dropped"
2294
+ });
2209
2295
  continue;
2210
2296
  }
2211
- const occurrences = trajectory.filter(
2212
- (candidate) => candidate.outcome === "error" && candidate.operationFingerprint === step.operationFingerprint
2213
- ).length;
2214
- const failure = failures.get(step.operationFingerprint);
2215
- if (occurrences < 2 || failure === void 0) continue;
2216
- const message = boundedReminder(
2217
- `Memory: [${failure.entryId}] you already tried ${failure.callDescription} at step ${failure.firstStep} and it failed with ${failure.errorDigest}.`,
2218
- settings().maxTokens
2219
- );
2220
- if (message.length === 0) return NO_EFFECTS3;
2221
- lastInjectedOperationFingerprint = step.operationFingerprint;
2222
- lastInjectedMessage = message;
2223
- lastDecision = "injected";
2224
- rulesInjectedSincePromptedStep = true;
2225
- deps.bank.recordInjection([failure.entryId]);
2226
- return [{ kind: "inject_reminder", message, severity: "advisory" }];
2297
+ seen.add(registration.id);
2298
+ accepted.push(registration);
2227
2299
  }
2228
- lastDecision = "silent";
2229
- return NO_EFFECTS3;
2230
- } finally {
2231
- lastTurnEndStep = toolStep;
2232
- }
2233
- }
2234
- function reactivateKnowledge() {
2235
- if (!reactivateAfterCompaction) return NO_EFFECTS3;
2236
- reactivateAfterCompaction = false;
2237
- const prefix = "Memory: execution state restored after compaction:\n";
2238
- const maxTokens = settings().maxTokens;
2239
- const prefixTokens = ceilChars(prefix.length);
2240
- const rendered = deps.bank.renderRestoredState(Math.max(0, maxTokens - prefixTokens));
2241
- if (rendered.length === 0) {
2242
- lastDecision = "silent";
2243
- return NO_EFFECTS3;
2244
- }
2245
- const message = boundedReminder(`${prefix}${rendered}`, maxTokens);
2246
- if (message.length === 0) {
2247
- lastDecision = "silent";
2248
- return NO_EFFECTS3;
2300
+ const slots = new Map(base.slots);
2301
+ slots.set(owner, { generation, registrations: Object.freeze(accepted), anchor: slot.anchor });
2302
+ const prepared = makeState(base.host, slots);
2303
+ let settled = false;
2304
+ const frozenDropped = Object.freeze(dropped);
2305
+ const frozenConflicts = Object.freeze(conflicts);
2306
+ const replacement = {
2307
+ owner,
2308
+ generation,
2309
+ dropped: frozenDropped,
2310
+ conflicts: frozenConflicts,
2311
+ size: accepted.length,
2312
+ // Reference identity on the base state covers every kind of
2313
+ // intervening change: host appends, other replacements, disposals.
2314
+ current: () => !settled && state === base,
2315
+ publish() {
2316
+ settled = true;
2317
+ state = prepared;
2318
+ },
2319
+ emitConflicts() {
2320
+ for (const conflict of frozenConflicts) emit(conflict);
2321
+ },
2322
+ dispose() {
2323
+ disposeGeneration(owner, generation);
2324
+ },
2325
+ discard() {
2326
+ settled = true;
2327
+ }
2328
+ };
2329
+ return { status: "prepared", replacement };
2330
+ },
2331
+ replaceRegistrations(owner, generation, registrations) {
2332
+ const prepared = table.prepareReplacement(owner, generation, registrations);
2333
+ if (prepared.status === "rejected") {
2334
+ return {
2335
+ applied: false,
2336
+ owner: prepared.owner,
2337
+ activeGeneration: prepared.activeGeneration,
2338
+ reason: prepared.reason,
2339
+ dropped: [],
2340
+ dispose: () => void 0
2341
+ };
2342
+ }
2343
+ const replacement = prepared.replacement;
2344
+ if (!replacement.current()) {
2345
+ replacement.discard();
2346
+ return {
2347
+ applied: false,
2348
+ owner,
2349
+ activeGeneration: table.ownedGeneration(owner),
2350
+ reason: "stale",
2351
+ dropped: replacement.dropped,
2352
+ dispose: () => void 0
2353
+ };
2354
+ }
2355
+ replacement.publish();
2356
+ replacement.emitConflicts();
2357
+ return {
2358
+ applied: true,
2359
+ owner,
2360
+ activeGeneration: generation,
2361
+ dropped: replacement.dropped,
2362
+ dispose: () => replacement.dispose()
2363
+ };
2364
+ },
2365
+ ownedGeneration(owner) {
2366
+ return state.slots.get(owner)?.generation ?? 0;
2249
2367
  }
2250
- const citedIds = [...message.matchAll(/\[([^\]]+)\]/gu)].map((match) => match[1]).filter((id) => id !== void 0);
2251
- deps.bank.recordInjection(citedIds);
2252
- lastInjectedMessage = message;
2253
- lastDecision = "injected";
2254
- return [{ kind: "inject_reminder", message, severity: "advisory" }];
2255
- }
2368
+ };
2369
+ return table;
2256
2370
  }
2257
- function prepareToolStep(input) {
2258
- const toolName = input.toolName?.trim();
2259
- if (!toolName) return null;
2260
- const canonical = hashToolCall(toolName, input.toolArgs ?? {});
2371
+
2372
+ // src/domains/middleware/snapshot.ts
2373
+ init_esm_shims();
2374
+ function createMiddlewareSnapshot(rules = listMiddlewareRules()) {
2261
2375
  return {
2262
- toolName,
2263
- operationFingerprint: createHash2("sha256").update(canonical).digest("hex").slice(0, 16),
2264
- callDescription: shortText(canonical, CALL_DESCRIPTION_MAX_CHARS)
2376
+ version: 1,
2377
+ rules: rules.map(cloneMiddlewareRule)
2265
2378
  };
2266
2379
  }
2267
- function pendingKey(input, operationFingerprint) {
2268
- return input.toolCallId ?? `anonymous:${operationFingerprint}`;
2269
- }
2270
- function resultDigest(input, outcome) {
2271
- if (input.toolResultDigest !== void 0) return sanitizeToolResultDigest(input.toolResultDigest);
2272
- if (outcome === "error") {
2273
- const metadataMessage = input.metadata?.errorMessage;
2274
- if (typeof metadataMessage === "string" && metadataMessage.trim().length > 0) {
2275
- return legacyToolResultDigest(diagnosticLine(metadataMessage), { outcome });
2276
- }
2277
- for (const key of ["error", "message"]) {
2278
- const value = input.toolResultDetails?.[key];
2279
- if (typeof value === "string" && value.trim().length > 0) {
2280
- return legacyToolResultDigest(diagnosticLine(value), { outcome });
2281
- }
2380
+ function createMiddlewareContractFromSnapshot(snapshot) {
2381
+ const builtinById = new Map(listMiddlewareRuleDefinitions().map((definition) => [definition.rule.id, definition]));
2382
+ const definitions = snapshot.rules.map((rule) => {
2383
+ const builtin = builtinById.get(rule.id);
2384
+ const definition = {
2385
+ rule: cloneMiddlewareRule(rule),
2386
+ effects: builtin === void 0 ? [] : builtin.effects.map(cloneMiddlewareEffect)
2387
+ };
2388
+ if (builtin?.toolNames !== void 0) definition.toolNames = [...builtin.toolNames];
2389
+ if (builtin?.predicate !== void 0) definition.predicate = builtin.predicate;
2390
+ return definition;
2391
+ });
2392
+ let diagnosticSink;
2393
+ const table = createMiddlewareRegistrationTable({
2394
+ fixed: definitions.map(registrationFromRuleDefinition),
2395
+ diagnosticSink: () => diagnosticSink ?? writeMiddlewareDiagnosticToStderr
2396
+ });
2397
+ const budgetTracker = createHookBudgetTracker({
2398
+ budgets: resolveHookBudgetsFromEnv(),
2399
+ ...resolveHookBudgetTunablesFromEnv()
2400
+ });
2401
+ return {
2402
+ runHook(input) {
2403
+ const registrations = table.list();
2404
+ return runMiddlewareRegistrations(input, registrations, {
2405
+ budgetTracker,
2406
+ ...diagnosticSink !== void 0 ? { onDiagnostic: diagnosticSink } : {}
2407
+ });
2408
+ },
2409
+ listRules() {
2410
+ return definitions.map((definition) => cloneMiddlewareRule(definition.rule));
2411
+ },
2412
+ snapshot() {
2413
+ return createMiddlewareSnapshot(definitions.map((definition) => definition.rule));
2414
+ },
2415
+ registerHook(registration) {
2416
+ table.registerHook(registration);
2417
+ },
2418
+ setDiagnosticSink(sink) {
2419
+ diagnosticSink = sink;
2420
+ },
2421
+ prepareRegistrationReplacement(owner, generation, registrations) {
2422
+ return table.prepareReplacement(owner, generation, registrations);
2423
+ },
2424
+ replaceRegistrations(owner, generation, registrations) {
2425
+ return table.replaceRegistrations(owner, generation, registrations);
2426
+ },
2427
+ ownedGeneration(owner) {
2428
+ return table.ownedGeneration(owner);
2282
2429
  }
2283
- return legacyToolResultDigest("", { outcome });
2284
- }
2285
- const resultFingerprint = input.metadata?.resultFingerprint;
2286
- return legacyToolResultDigest(
2287
- typeof resultFingerprint === "string" ? `ok result ${resultFingerprint.slice(0, 16)}` : "ok",
2288
- { outcome }
2289
- );
2290
- }
2291
- var DIAGNOSTIC_HINT = /\b(error|failed|failure|cannot|unable|expected|refused|denied|missing|not found|timed out|timeout|fatal)\b/iu;
2292
- function diagnosticLine(value) {
2293
- const lines = value.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0);
2294
- return lines.find((line) => DIAGNOSTIC_HINT.test(line)) ?? lines[0] ?? "";
2295
- }
2296
- function proceduralContent(call, attempts, firstStep, error) {
2297
- return `${call} failed ${attempts} time${attempts === 1 ? "" : "s"}; first observed at step ${firstStep}: ${error}.`;
2298
- }
2299
- function boundedReminder(message, maxTokens) {
2300
- const maxChars = maxTokens * 4;
2301
- if (maxChars <= 0) return "";
2302
- const normalized = message.replace(/[^\S\n]+/gu, " ").trim();
2303
- return normalized.length <= maxChars ? normalized : normalized.slice(0, maxChars).trimEnd();
2304
- }
2305
- function shortText(value, maxChars) {
2306
- const normalized = value.replace(/\s+/gu, " ").trim();
2307
- return normalized.length <= maxChars ? normalized : `${normalized.slice(0, Math.max(0, maxChars - 1)).trimEnd()}\u2026`;
2430
+ };
2308
2431
  }
2309
- function positiveInteger(value, fallback) {
2310
- return value !== void 0 && Number.isInteger(value) && value > 0 ? value : fallback;
2432
+
2433
+ // src/domains/middleware/extension.ts
2434
+ function createEnvHookBudgetTracker(env = process.env) {
2435
+ return createHookBudgetTracker({ budgets: resolveHookBudgetsFromEnv(env), ...resolveHookBudgetTunablesFromEnv(env) });
2311
2436
  }
2312
- function countBankWrites(delta) {
2313
- return [delta.status, delta.knowledge, delta.procedural].reduce(
2314
- (total, entry) => total + entry.added + entry.updated + entry.deleted,
2315
- 0
2316
- );
2437
+ function createMiddlewareBundle(options = {}) {
2438
+ const ruleDefinitions = combineRuleDefinitions(listMiddlewareRuleDefinitions(), options.ruleDefinitions ?? []);
2439
+ let diagnosticSink = options.onDiagnostic;
2440
+ const table = createMiddlewareRegistrationTable({
2441
+ fixed: ruleDefinitions.map(registrationFromRuleDefinition),
2442
+ diagnosticSink: () => diagnosticSink ?? writeMiddlewareDiagnosticToStderr
2443
+ });
2444
+ for (const registration of options.registrations ?? []) table.registerHook(registration);
2445
+ const budgetTracker = options.budgetTracker ?? createEnvHookBudgetTracker();
2446
+ const contract = {
2447
+ runHook(input) {
2448
+ const registrations = table.list();
2449
+ return runMiddlewareRegistrations(input, registrations, {
2450
+ budgetTracker,
2451
+ ...diagnosticSink !== void 0 ? { onDiagnostic: diagnosticSink } : {}
2452
+ });
2453
+ },
2454
+ runAsyncHook(input, priorEffects = []) {
2455
+ const registrations = table.list();
2456
+ return runMiddlewareAsyncRegistrations(input, registrations, priorEffects, {
2457
+ ...diagnosticSink !== void 0 ? { onDiagnostic: diagnosticSink } : {}
2458
+ });
2459
+ },
2460
+ listRules() {
2461
+ return ruleDefinitions.map((definition) => cloneMiddlewareRule(definition.rule));
2462
+ },
2463
+ snapshot() {
2464
+ return createMiddlewareSnapshot(ruleDefinitions.map((definition) => definition.rule));
2465
+ },
2466
+ registerHook(registration) {
2467
+ table.registerHook(registration);
2468
+ },
2469
+ setDiagnosticSink(sink) {
2470
+ diagnosticSink = sink;
2471
+ },
2472
+ prepareRegistrationReplacement(owner, generation, registrations) {
2473
+ return table.prepareReplacement(owner, generation, registrations);
2474
+ },
2475
+ replaceRegistrations(owner, generation, registrations) {
2476
+ return table.replaceRegistrations(owner, generation, registrations);
2477
+ },
2478
+ ownedGeneration(owner) {
2479
+ return table.ownedGeneration(owner);
2480
+ }
2481
+ };
2482
+ return {
2483
+ extension: {
2484
+ start() {
2485
+ return void 0;
2486
+ }
2487
+ },
2488
+ contract
2489
+ };
2317
2490
  }
2318
- function setBounded(map, key, value, capacity) {
2319
- if (!map.has(key) && map.size >= capacity) {
2320
- const oldest = map.keys().next().value;
2321
- if (oldest !== void 0) map.delete(oldest);
2491
+ function combineRuleDefinitions(builtin, registered) {
2492
+ const seen = /* @__PURE__ */ new Set();
2493
+ const combined = [];
2494
+ for (const definition of [...builtin, ...registered]) {
2495
+ if (seen.has(definition.rule.id)) continue;
2496
+ seen.add(definition.rule.id);
2497
+ combined.push(definition);
2322
2498
  }
2323
- map.set(key, value);
2499
+ return combined;
2324
2500
  }
2325
2501
 
2326
- // src/domains/middleware/memory-step-endpoint.ts
2502
+ // src/domains/middleware/manifest.ts
2327
2503
  init_esm_shims();
2328
- function announceMemoryStepEndpoint(deps, complete) {
2329
- const { bus, endpointKey, targetId } = deps;
2330
- if (endpointKey === null) return complete;
2331
- return async (request) => {
2332
- const releaseEndpointSlot = registerForegroundStream(endpointKey);
2333
- try {
2334
- return await complete(request);
2335
- } finally {
2336
- releaseEndpointSlot();
2337
- if (bus !== null) {
2338
- try {
2339
- bus.emit(BusChannels.MemoryStepCompleted, { endpointKey, targetId });
2340
- } catch {
2341
- }
2342
- }
2343
- }
2344
- };
2345
- }
2504
+ var MiddlewareManifest = {
2505
+ name: "middleware",
2506
+ dependsOn: []
2507
+ };
2346
2508
 
2347
- // src/domains/middleware/task-board-reminder.ts
2509
+ // src/domains/middleware/hooks.ts
2348
2510
  init_esm_shims();
2349
- var TASK_BOARD_REMINDER_REGISTRATION_ID = "observer.task-board-reminder";
2350
- function taskBoardReminderMessage(steps) {
2351
- return `[Tasks] This request enumerates ${steps} steps. Before the first edit, declare the task board: tasks action="plan" with a title and one task per step, then work them one at a time ("start", then "done" with an evidence note).`;
2511
+ import { createHash as createHash2 } from "node:crypto";
2512
+ import { isAbsolute, resolve } from "node:path";
2513
+
2514
+ // src/domains/middleware/validate.ts
2515
+ init_esm_shims();
2516
+ function validateMiddlewareEffect(value, source = "$") {
2517
+ const issues = [];
2518
+ const effect = readMiddlewareEffect(value, source, issues);
2519
+ if (issues.length > 0 || effect === null) return { valid: false, issues };
2520
+ return { valid: true, effect, issues: [] };
2352
2521
  }
2353
- function countEnumeratedSteps(text) {
2354
- const value = text ?? "";
2355
- if (value.trim().length === 0) return 0;
2356
- const markers = [];
2357
- const markerPattern = /(?:^|[\s(])(?:step\s+)?([1-9])\s*[).:]\s/gi;
2358
- let match = markerPattern.exec(value);
2359
- while (match !== null) {
2360
- const digit = Number(match[1]);
2361
- if (Number.isInteger(digit)) markers.push(digit);
2362
- match = markerPattern.exec(value);
2522
+ function readMiddlewareEffect(value, path2, issues) {
2523
+ if (!isRecord(value)) {
2524
+ issues.push({ path: path2, message: "expected middleware effect object" });
2525
+ return null;
2363
2526
  }
2364
- let run = 0;
2365
- for (const digit of markers) {
2366
- if (digit === run + 1) run += 1;
2367
- else if (digit === 1) run = 1;
2527
+ const kindValue = value.kind;
2528
+ if (typeof kindValue !== "string" || !isMiddlewareEffectKind(kindValue)) {
2529
+ issues.push({ path: `${path2}.kind`, message: "expected known middleware effect kind" });
2530
+ return null;
2531
+ }
2532
+ switch (kindValue) {
2533
+ case "inject_reminder":
2534
+ return readInjectReminder(value, path2, issues);
2535
+ case "annotate_tool_result":
2536
+ return readAnnotateToolResult(value, path2, issues);
2537
+ case "block_tool":
2538
+ return readBlockTool(value, path2, issues);
2539
+ case "protect_path":
2540
+ return readProtectPath(value, path2, issues);
2541
+ case "request_continuation":
2542
+ return readRequestContinuation(value, path2, issues);
2543
+ case "require_tool":
2544
+ return readRequireTool(value, path2, issues);
2545
+ case "lock_tools":
2546
+ rejectUnexpectedFields(value, path2, ["kind"], issues);
2547
+ return { kind: "lock_tools" };
2368
2548
  }
2369
- if (run >= 3) return run;
2370
- const bulletLines = value.split(/\r?\n/).filter((line) => /^\s*[-*]\s+\S/.test(line)).length;
2371
- return bulletLines >= 3 ? bulletLines : 0;
2372
2549
  }
2373
- var NO_EFFECTS4 = [];
2374
- function createTaskBoardReminderRegistration() {
2375
- let lastSeenSessionId;
2376
- let spentForActiveSession = false;
2377
- return {
2378
- id: TASK_BOARD_REMINDER_REGISTRATION_ID,
2379
- description: "once per session, on the first explicitly enumerated multi-step turn, teaches the board-first protocol",
2380
- hooks: ["turn_start"],
2381
- evaluate(input) {
2382
- const sessionId = input.sessionId ?? null;
2383
- if (lastSeenSessionId === void 0 || lastSeenSessionId === null && sessionId !== null) {
2384
- lastSeenSessionId = sessionId;
2385
- } else if (sessionId !== lastSeenSessionId) {
2386
- lastSeenSessionId = sessionId;
2387
- spentForActiveSession = false;
2388
- }
2389
- if (spentForActiveSession) return NO_EFFECTS4;
2390
- const steps = countEnumeratedSteps(input.text);
2391
- if (steps < 3) return NO_EFFECTS4;
2392
- spentForActiveSession = true;
2393
- const pendingSkillRequests = metadataNumber(input, "pendingSkillRequests");
2394
- if (pendingSkillRequests !== null && pendingSkillRequests > 0) return NO_EFFECTS4;
2395
- return [{ kind: "inject_reminder", severity: "info", message: taskBoardReminderMessage(steps) }];
2396
- }
2397
- };
2550
+ function readRequireTool(record, path2, issues) {
2551
+ rejectUnexpectedFields(record, path2, ["kind", "toolName"], issues);
2552
+ const toolName = readRequiredString(record, `${path2}.toolName`, issues);
2553
+ return toolName === null ? null : { kind: "require_tool", toolName };
2398
2554
  }
2399
-
2400
- // src/domains/middleware/task-nudge.ts
2401
- init_esm_shims();
2402
- var TASK_NUDGE_REGISTRATION_ID = "nudge.open-tasks";
2403
- function openTasks(board) {
2404
- return board.tasks.filter((task) => task.status === "pending" || task.status === "active");
2555
+ function readInjectReminder(record, path2, issues) {
2556
+ rejectUnexpectedFields(record, path2, ["kind", "message", "severity"], issues);
2557
+ const message = readRequiredString(record, `${path2}.message`, issues);
2558
+ const severity = readOptionalString(record, `${path2}.severity`, issues);
2559
+ if (severity !== void 0 && !isMiddlewareReminderSeverity(severity)) {
2560
+ issues.push({ path: `${path2}.severity`, message: "expected info, advisory, warn, or hard-block" });
2561
+ }
2562
+ if (message === null) return null;
2563
+ const effect = { kind: "inject_reminder", message };
2564
+ if (severity !== void 0 && isMiddlewareReminderSeverity(severity)) effect.severity = severity;
2565
+ return effect;
2405
2566
  }
2406
- function buildOpenTasksMessage(board) {
2407
- const open = openTasks(board);
2408
- const counts = taskBoardCounts(board);
2409
- const rows = open.map((task) => ` ${task.status === "active" ? "[>]" : "[ ]"} ${task.id} ${task.title}`);
2410
- return `[Clio Coder] task board "${board.title}" still has ${open.length} of ${counts.total} task(s) open:
2411
- ${rows.join("\n")}
2412
- Continue working them, or record the honest state on the board: tasks action="done" with an evidence note, action="block" with a reason, or action="drop". Do not end the turn with a stale board.`;
2567
+ function readAnnotateToolResult(record, path2, issues) {
2568
+ rejectUnexpectedFields(record, path2, ["kind", "message", "severity"], issues);
2569
+ const message = readRequiredString(record, `${path2}.message`, issues);
2570
+ const severity = readOptionalString(record, `${path2}.severity`, issues);
2571
+ if (severity !== void 0 && !isMiddlewareAnnotationSeverity(severity)) {
2572
+ issues.push({ path: `${path2}.severity`, message: "expected info or warn" });
2573
+ }
2574
+ if (message === null) return null;
2575
+ const effect = { kind: "annotate_tool_result", message };
2576
+ if (severity !== void 0 && isMiddlewareAnnotationSeverity(severity)) effect.severity = severity;
2577
+ return effect;
2413
2578
  }
2414
- function createTaskNudgeRegistration(options) {
2415
- return {
2416
- id: TASK_NUDGE_REGISTRATION_ID,
2417
- description: "carry the turn onward when a work turn ends with open task-board tasks",
2418
- hooks: ["turn_end"],
2419
- evaluate(input) {
2420
- if (input.hook !== "turn_end") return [];
2421
- const stopReason = input.metadata?.stopReason;
2422
- if (stopReason !== void 0 && stopReason !== "stop") return [];
2423
- const turnToolCalls = input.metadata?.turnToolCalls;
2424
- if (typeof turnToolCalls !== "number" || turnToolCalls <= 0) return [];
2425
- const activeToolNames = input.metadata?.activeToolNames;
2426
- if (typeof activeToolNames === "string" && !activeToolNames.split(",").includes(ToolNames.Tasks)) return [];
2427
- let board;
2428
- try {
2429
- board = options.getBoard();
2430
- } catch {
2431
- return [];
2432
- }
2433
- if (board === null || openTasks(board).length === 0) return [];
2434
- const message = buildOpenTasksMessage(board);
2435
- return [
2436
- { kind: "request_continuation", message },
2437
- { kind: "inject_reminder", message, severity: "warn" }
2438
- ];
2439
- }
2440
- };
2579
+ function readBlockTool(record, path2, issues) {
2580
+ rejectUnexpectedFields(record, path2, ["kind", "reason", "severity"], issues);
2581
+ const reason = readRequiredString(record, `${path2}.reason`, issues);
2582
+ const severity = readRequiredString(record, `${path2}.severity`, issues);
2583
+ if (severity !== null && severity !== "hard-block") {
2584
+ issues.push({ path: `${path2}.severity`, message: "expected hard-block" });
2585
+ }
2586
+ if (reason === null || severity !== "hard-block") return null;
2587
+ return { kind: "block_tool", reason, severity };
2441
2588
  }
2442
-
2443
- // src/domains/middleware/tool-choice-control.ts
2444
- init_esm_shims();
2445
- function createMiddlewareToolChoiceControl() {
2446
- let choice = { kind: "auto" };
2447
- return {
2448
- apply(effects) {
2449
- for (const effect of effects) {
2450
- if (effect.kind === "lock_tools") {
2451
- choice = { kind: "none" };
2452
- continue;
2453
- }
2454
- if (effect.kind === "require_tool" && choice.kind !== "none") {
2455
- choice = { kind: "required", toolName: effect.toolName };
2456
- }
2457
- }
2458
- },
2459
- current: () => choice,
2460
- toolStarted(toolName) {
2461
- if (choice.kind === "required" && choice.toolName === toolName) choice = { kind: "auto" };
2462
- },
2463
- reset() {
2464
- choice = { kind: "auto" };
2465
- }
2466
- };
2589
+ function readProtectPath(record, path2, issues) {
2590
+ rejectUnexpectedFields(record, path2, ["kind", "path", "reason"], issues);
2591
+ const protectedPath = readRequiredString(record, `${path2}.path`, issues);
2592
+ const reason = readRequiredString(record, `${path2}.reason`, issues);
2593
+ if (protectedPath === null || reason === null) return null;
2594
+ return { kind: "protect_path", path: protectedPath, reason };
2595
+ }
2596
+ function readRequestContinuation(record, path2, issues) {
2597
+ rejectUnexpectedFields(record, path2, ["kind", "message"], issues);
2598
+ const message = readRequiredString(record, `${path2}.message`, issues);
2599
+ if (message === null) return null;
2600
+ return { kind: "request_continuation", message };
2601
+ }
2602
+ function readRequiredString(record, path2, issues) {
2603
+ const value = record[pathField(path2)];
2604
+ if (typeof value !== "string" || value.trim().length === 0) {
2605
+ issues.push({ path: path2, message: "expected non-empty string" });
2606
+ return null;
2607
+ }
2608
+ return value;
2609
+ }
2610
+ function readOptionalString(record, path2, issues) {
2611
+ const field = pathField(path2);
2612
+ if (!Object.hasOwn(record, field)) return void 0;
2613
+ const value = record[field];
2614
+ if (typeof value !== "string" || value.trim().length === 0) {
2615
+ issues.push({ path: path2, message: "expected non-empty string" });
2616
+ return void 0;
2617
+ }
2618
+ return value;
2619
+ }
2620
+ function rejectUnexpectedFields(record, path2, allowedFields, issues) {
2621
+ const allowed = new Set(allowedFields);
2622
+ for (const field of Object.keys(record).sort((a, b) => a.localeCompare(b))) {
2623
+ if (!allowed.has(field)) issues.push({ path: `${path2}.${field}`, message: "unexpected field" });
2624
+ }
2625
+ }
2626
+ function pathField(path2) {
2627
+ return path2.slice(path2.lastIndexOf(".") + 1);
2628
+ }
2629
+ function isRecord(value) {
2630
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2467
2631
  }
2468
2632
 
2469
- // src/domains/middleware/watchdog.ts
2470
- init_esm_shims();
2471
- var WATCHDOG_REGISTRATION_ID = "observer.watchdog";
2472
- var WATCHDOG_DIFF_MAX_BYTES = 12 * 1024;
2473
- var WATCHDOG_PATHS_MAX = 40;
2474
- var WATCHDOG_TRUNCATION_MARKER = "\n[diff truncated]";
2475
- var MUTATING_TOOL_NAMES = /* @__PURE__ */ new Set([ToolNames.Write, ToolNames.Edit, ToolNames.Artifact]);
2476
- var NO_EFFECTS5 = [];
2477
- function stringValue2(value) {
2478
- return typeof value === "string" && value.length > 0 ? value : null;
2633
+ // src/domains/middleware/hooks.ts
2634
+ var USER_HOOK_ORIGIN_ORDER = ["extension", "user", "project", "project.local"];
2635
+ var USER_HOOK_COMMAND_TIMEOUT_DEFAULT_MS = 2e3;
2636
+ var USER_HOOK_COMMAND_TIMEOUT_MIN_MS = 100;
2637
+ var USER_HOOK_COMMAND_TIMEOUT_MAX_MS = 5e3;
2638
+ var USER_HOOK_COMMAND_OUTPUT_MAX_CHARS = 4e3;
2639
+ var USER_HOOK_PROMPT_MAX_CHARS = 2e3;
2640
+ function isRecord2(value) {
2641
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2479
2642
  }
2480
- function firstPath(details) {
2481
- const paths = details?.paths;
2482
- if (!Array.isArray(paths)) return null;
2483
- for (const entry of paths) {
2484
- const path2 = stringValue2(entry);
2485
- if (path2 !== null) return path2;
2486
- }
2487
- return null;
2643
+ function stableHash(value) {
2644
+ return createHash2("sha256").update(canonicalJson(value)).digest("hex").slice(0, 16);
2488
2645
  }
2489
- function coalesceTurnDiff(byPath, maxBytes = WATCHDOG_DIFF_MAX_BYTES) {
2490
- const blocks = [];
2491
- let index = 0;
2492
- for (const [path2, diff] of byPath) {
2493
- if (index >= WATCHDOG_PATHS_MAX) {
2494
- blocks.push(`[${byPath.size - WATCHDOG_PATHS_MAX} more changed path(s) omitted]`);
2495
- break;
2496
- }
2497
- blocks.push(`--- ${path2}
2498
- ${diff}`);
2499
- index += 1;
2500
- }
2501
- const whole = blocks.join("\n\n");
2502
- if (Buffer.byteLength(whole, "utf8") <= maxBytes) return whole;
2503
- const room = Math.max(0, maxBytes - Buffer.byteLength(WATCHDOG_TRUNCATION_MARKER, "utf8"));
2504
- const buffer = Buffer.from(whole, "utf8");
2505
- let cut = Math.min(room, buffer.byteLength);
2506
- while (cut > 0) {
2507
- const next = buffer[cut];
2508
- if (next === void 0 || (next & 192) !== 128) break;
2509
- cut -= 1;
2646
+ function canonicalJson(value) {
2647
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
2648
+ if (isRecord2(value)) {
2649
+ const keys = Object.keys(value).sort();
2650
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
2510
2651
  }
2511
- return `${buffer.subarray(0, cut).toString("utf8")}${WATCHDOG_TRUNCATION_MARKER}`;
2652
+ return JSON.stringify(value) ?? "null";
2512
2653
  }
2513
- var WATCHDOG_TASK = "Review the coalesced diff in the briefing against the scope it names. Report one check per concern: a change outside the stated scope, a change the scope required that the diff does not make, or a defect visible in the diff itself. Run no commands and change nothing.";
2514
- function watchdogBriefing(trigger) {
2515
- const scope = trigger.scope === null ? "(no task board; scope is unstated)" : trigger.scope;
2516
- const paths = trigger.paths.length === 0 ? "(none)" : trigger.paths.join("\n");
2517
- return [
2518
- "## Current scope",
2519
- scope,
2520
- "",
2521
- "## Changed paths",
2522
- paths,
2523
- "",
2524
- "## Coalesced diff for this turn",
2525
- trigger.diff
2526
- ].join("\n");
2654
+ function readStringArray(value) {
2655
+ if (!Array.isArray(value)) return null;
2656
+ const out = [];
2657
+ for (const item of value) {
2658
+ if (typeof item !== "string" || item.length === 0) return null;
2659
+ out.push(item);
2660
+ }
2661
+ return out;
2527
2662
  }
2528
- function createWatchdogRegistration(deps) {
2529
- const diffByPath = /* @__PURE__ */ new Map();
2530
- let toolCalls = 0;
2531
- let dropped = 0;
2532
- let inFlight = false;
2533
- let outstanding = Promise.resolve();
2534
- const active = () => {
2535
- if (deps.firesOnThisSurface === false) return null;
2536
- let settings;
2537
- try {
2538
- settings = deps.getSettings();
2539
- } catch {
2663
+ function normalizeCommandSpec(raw, options, issues) {
2664
+ const argv = readStringArray(raw.argv);
2665
+ if (!argv || argv.length === 0) {
2666
+ issues.push("command.argv must be a non-empty array of non-empty strings");
2667
+ return null;
2668
+ }
2669
+ const spec = {
2670
+ kind: "command",
2671
+ argv,
2672
+ cwd: resolve(options.workspaceRoot),
2673
+ timeoutMs: USER_HOOK_COMMAND_TIMEOUT_DEFAULT_MS,
2674
+ as: "annotate"
2675
+ };
2676
+ if (raw.cwd !== void 0) {
2677
+ if (typeof raw.cwd !== "string" || raw.cwd.length === 0) {
2678
+ issues.push("command.cwd must be a non-empty string");
2540
2679
  return null;
2541
2680
  }
2542
- return settings.enabled ? settings : null;
2543
- };
2544
- const resetTurn = () => {
2545
- diffByPath.clear();
2546
- toolCalls = 0;
2547
- };
2548
- const fire = (reason) => {
2549
- if (diffByPath.size === 0) return;
2550
- if (inFlight) {
2551
- dropped += 1;
2552
- return;
2681
+ const resolved = isAbsolute(raw.cwd) ? resolve(raw.cwd) : resolve(options.workspaceRoot, raw.cwd);
2682
+ if (!isWithin(resolved, options.workspaceRoot)) {
2683
+ issues.push("command.cwd must resolve under the workspace root");
2684
+ return null;
2553
2685
  }
2554
- const trigger = {
2555
- reason,
2556
- diff: coalesceTurnDiff(diffByPath),
2557
- paths: [...diffByPath.keys()].slice(0, WATCHDOG_PATHS_MAX),
2558
- scope: scopeNow(),
2559
- toolCalls
2560
- };
2561
- inFlight = true;
2562
- let started;
2563
- try {
2564
- started = deps.run(trigger);
2565
- } catch {
2566
- inFlight = false;
2567
- return;
2686
+ spec.cwd = resolved;
2687
+ }
2688
+ if (raw.timeoutMs !== void 0) {
2689
+ if (typeof raw.timeoutMs !== "number" || !Number.isFinite(raw.timeoutMs)) {
2690
+ issues.push("command.timeoutMs must be a number");
2691
+ return null;
2568
2692
  }
2569
- outstanding = started.catch(() => {
2570
- }).finally(() => {
2571
- inFlight = false;
2572
- });
2573
- };
2574
- const scopeNow = () => {
2575
- try {
2576
- return deps.getScope?.() ?? null;
2577
- } catch {
2693
+ spec.timeoutMs = Math.min(
2694
+ USER_HOOK_COMMAND_TIMEOUT_MAX_MS,
2695
+ Math.max(USER_HOOK_COMMAND_TIMEOUT_MIN_MS, Math.floor(raw.timeoutMs))
2696
+ );
2697
+ }
2698
+ if (raw.as !== void 0) {
2699
+ if (raw.as !== "annotate" && raw.as !== "reminder") {
2700
+ issues.push('command.as must be "annotate" or "reminder"');
2578
2701
  return null;
2579
2702
  }
2580
- };
2581
- return {
2582
- id: WATCHDOG_REGISTRATION_ID,
2583
- description: "review a mutating turn's coalesced diff against the board scope with a read-only verifier run",
2584
- hooks: ["after_tool", "turn_end"],
2585
- evaluate(input) {
2586
- const settings = active();
2587
- if (settings === null) {
2588
- if (input.hook === "turn_end") resetTurn();
2589
- return NO_EFFECTS5;
2590
- }
2591
- if (input.hook === "after_tool") {
2592
- toolCalls += 1;
2593
- recordMutation(input);
2594
- const cadence = settings.cadenceToolCalls;
2595
- if (cadence !== void 0 && cadence >= 1 && toolCalls % cadence === 0) fire("cadence");
2596
- return NO_EFFECTS5;
2703
+ spec.as = raw.as;
2704
+ }
2705
+ return spec;
2706
+ }
2707
+ function normalizePromptSpec(raw, issues) {
2708
+ if (typeof raw.message !== "string" || raw.message.trim().length === 0) {
2709
+ issues.push("prompt.message must be a non-empty string");
2710
+ return null;
2711
+ }
2712
+ let severity = "info";
2713
+ if (raw.severity !== void 0) {
2714
+ if (typeof raw.severity !== "string" || !isMiddlewareReminderSeverity(raw.severity)) {
2715
+ issues.push("prompt.severity must be info, advisory, warn, or hard-block");
2716
+ return null;
2717
+ }
2718
+ severity = raw.severity;
2719
+ }
2720
+ return { kind: "prompt", message: raw.message.slice(0, USER_HOOK_PROMPT_MAX_CHARS), severity };
2721
+ }
2722
+ function normalizeEffectSpec(raw, issues) {
2723
+ const result = validateMiddlewareEffect(raw.effect ?? raw, "effect");
2724
+ if (!result.valid) {
2725
+ for (const issue of result.issues) issues.push(`effect: ${issue.message}`);
2726
+ return null;
2727
+ }
2728
+ return { kind: "effect", effect: result.effect };
2729
+ }
2730
+ function isWithin(child, parent) {
2731
+ const rel = resolve(child);
2732
+ const base = resolve(parent);
2733
+ return rel === base || rel.startsWith(`${base}/`);
2734
+ }
2735
+ function normalizeUserHook(raw, source, options) {
2736
+ const issues = [];
2737
+ if (!isRecord2(raw)) {
2738
+ return { issues: ["hook declaration must be an object"] };
2739
+ }
2740
+ if (raw.on === void 0 || typeof raw.on !== "string" || !isMiddlewareHook(raw.on)) {
2741
+ issues.push("on must be one of before_tool, after_tool, turn_start, turn_end, on_compaction");
2742
+ }
2743
+ let tools;
2744
+ if (raw.tools !== void 0) {
2745
+ const parsed = readStringArray(raw.tools);
2746
+ if (!parsed) issues.push("tools must be an array of non-empty tool names");
2747
+ else tools = parsed;
2748
+ }
2749
+ let enabled = true;
2750
+ if (raw.enabled !== void 0) {
2751
+ if (typeof raw.enabled !== "boolean") issues.push("enabled must be a boolean");
2752
+ else enabled = raw.enabled;
2753
+ }
2754
+ let spec = null;
2755
+ if (raw.kind === "command") spec = normalizeCommandSpec(raw, options, issues);
2756
+ else if (raw.kind === "prompt") spec = normalizePromptSpec(raw, issues);
2757
+ else if (raw.kind === "effect") spec = normalizeEffectSpec(raw, issues);
2758
+ else issues.push('kind must be "command", "prompt", or "effect"');
2759
+ if (issues.length > 0 || spec === null || raw.on === void 0 || typeof raw.on !== "string") {
2760
+ return { issues };
2761
+ }
2762
+ const on = raw.on;
2763
+ const hash = stableHash({ on, tools: tools ?? null, spec });
2764
+ const id = typeof raw.id === "string" && raw.id.length > 0 ? raw.id : `${source.sourceId ?? source.origin}.${spec.kind}.${hash}`;
2765
+ const hook = { id, source, hash, on, enabled, spec };
2766
+ if (tools !== void 0) hook.tools = tools;
2767
+ return { hook, issues };
2768
+ }
2769
+ function declarationsArray(declarations) {
2770
+ if (Array.isArray(declarations)) return declarations;
2771
+ if (isRecord2(declarations) && Array.isArray(declarations.hooks)) return declarations.hooks;
2772
+ return [];
2773
+ }
2774
+ function loadUserHooks(batches, options) {
2775
+ const ordered = [...batches].sort(
2776
+ (a, b) => USER_HOOK_ORIGIN_ORDER.indexOf(a.source.origin) - USER_HOOK_ORIGIN_ORDER.indexOf(b.source.origin)
2777
+ );
2778
+ const byId = /* @__PURE__ */ new Map();
2779
+ const overridden = [];
2780
+ const issues = [];
2781
+ for (const batch of ordered) {
2782
+ const declarations = declarationsArray(batch.declarations);
2783
+ declarations.forEach((raw, index) => {
2784
+ const result = normalizeUserHook(raw, batch.source, options);
2785
+ if (!result.hook) {
2786
+ issues.push({ source: batch.source, index, issues: result.issues });
2787
+ return;
2597
2788
  }
2598
- if (input.hook !== "turn_end") return NO_EFFECTS5;
2599
- fire("turn_end");
2600
- resetTurn();
2601
- return NO_EFFECTS5;
2602
- },
2603
- droppedTriggers: () => dropped,
2604
- runInFlight: () => inFlight,
2605
- whenIdle: () => outstanding
2606
- };
2607
- function recordMutation(input) {
2608
- if (input.metadata?.resultKind !== "ok") return;
2609
- if (input.toolName === void 0 || !MUTATING_TOOL_NAMES.has(input.toolName)) return;
2610
- const diff = stringValue2(input.toolResultDetails?.diff);
2611
- if (diff === null) return;
2612
- diffByPath.set(firstPath(input.toolResultDetails) ?? input.toolName, diff);
2789
+ const existing = byId.get(result.hook.id);
2790
+ if (existing) overridden.push({ winner: batch.source, loser: existing });
2791
+ byId.set(result.hook.id, result.hook);
2792
+ });
2613
2793
  }
2794
+ return { hooks: [...byId.values()], overridden, issues };
2614
2795
  }
2615
-
2616
- // src/domains/middleware/index.ts
2617
- init_esm_shims();
2618
-
2619
- // src/domains/middleware/extension.ts
2620
- init_esm_shims();
2621
-
2622
- // src/domains/middleware/snapshot.ts
2623
- init_esm_shims();
2624
- function createMiddlewareSnapshot(rules = listMiddlewareRules()) {
2625
- return {
2626
- version: 1,
2627
- rules: rules.map(cloneMiddlewareRule)
2628
- };
2796
+ function reminderEffect(message, severity) {
2797
+ return severity === "info" ? { kind: "inject_reminder", message } : { kind: "inject_reminder", message, severity };
2629
2798
  }
2630
- function createMiddlewareContractFromSnapshot(snapshot) {
2631
- const builtinById = new Map(listMiddlewareRuleDefinitions().map((definition) => [definition.rule.id, definition]));
2632
- const definitions = snapshot.rules.map((rule) => {
2633
- const builtin = builtinById.get(rule.id);
2634
- const definition = {
2635
- rule: cloneMiddlewareRule(rule),
2636
- effects: builtin === void 0 ? [] : builtin.effects.map(cloneMiddlewareEffect)
2637
- };
2638
- if (builtin?.toolNames !== void 0) definition.toolNames = [...builtin.toolNames];
2639
- if (builtin?.predicate !== void 0) definition.predicate = builtin.predicate;
2640
- return definition;
2641
- });
2642
- const registrations = definitions.map(registrationFromRuleDefinition);
2643
- const registeredIds = new Set(registrations.map((registration) => registration.id));
2644
- const budgetTracker = createHookBudgetTracker({
2645
- budgets: resolveHookBudgetsFromEnv(),
2646
- ...resolveHookBudgetTunablesFromEnv()
2799
+ function commandEffect(hook, output) {
2800
+ const message = output.slice(0, USER_HOOK_COMMAND_OUTPUT_MAX_CHARS);
2801
+ return hook.as === "reminder" ? { kind: "inject_reminder", message } : { kind: "annotate_tool_result", message };
2802
+ }
2803
+ function userHookToRegistration(hook, deps) {
2804
+ const now = deps.now ?? Date.now;
2805
+ const extension = hook.source.extension === void 0 ? void 0 : receiptExtension(hook.source.extension);
2806
+ const baseReceipt = (input) => ({
2807
+ at: now(),
2808
+ hookId: hook.id,
2809
+ origin: hook.source.origin,
2810
+ sourcePath: hook.source.sourcePath,
2811
+ hash: hook.hash,
2812
+ hook: hook.on,
2813
+ kind: hook.spec.kind,
2814
+ ...extension !== void 0 ? { extension: { ...extension } } : {},
2815
+ ...input.toolName !== void 0 ? { toolName: input.toolName } : {}
2647
2816
  });
2648
- let diagnosticSink;
2649
- return {
2650
- runHook(input) {
2651
- return runMiddlewareRegistrations(input, registrations, {
2652
- budgetTracker,
2653
- ...diagnosticSink !== void 0 ? { onDiagnostic: diagnosticSink } : {}
2654
- });
2655
- },
2656
- listRules() {
2657
- return definitions.map((definition) => cloneMiddlewareRule(definition.rule));
2658
- },
2659
- snapshot() {
2660
- return createMiddlewareSnapshot(definitions.map((definition) => definition.rule));
2661
- },
2662
- registerHook(registration) {
2663
- if (registeredIds.has(registration.id)) return;
2664
- registeredIds.add(registration.id);
2665
- registrations.push(registration);
2666
- },
2667
- setDiagnosticSink(sink) {
2668
- diagnosticSink = sink;
2817
+ const registration = {
2818
+ id: hook.id,
2819
+ description: `${hook.spec.kind} hook from ${hook.source.origin} (${hook.source.sourcePath})`,
2820
+ hooks: [hook.on],
2821
+ evaluate: (input) => {
2822
+ if (!hook.enabled) return [];
2823
+ try {
2824
+ return evaluateHook(hook, deps, baseReceipt(input));
2825
+ } catch {
2826
+ deps.recordReceipt({ ...baseReceipt(input), outcome: "skipped" });
2827
+ return [];
2828
+ }
2669
2829
  }
2670
2830
  };
2831
+ if (hook.tools !== void 0) registration.toolNames = [...hook.tools];
2832
+ return registration;
2671
2833
  }
2672
-
2673
- // src/domains/middleware/extension.ts
2674
- function createEnvHookBudgetTracker(env = process.env) {
2675
- return createHookBudgetTracker({ budgets: resolveHookBudgetsFromEnv(env), ...resolveHookBudgetTunablesFromEnv(env) });
2676
- }
2677
- function createMiddlewareBundle(options = {}) {
2678
- const ruleDefinitions = combineRuleDefinitions(listMiddlewareRuleDefinitions(), options.ruleDefinitions ?? []);
2679
- const registrations = combineRegistrations(ruleDefinitions, options.registrations ?? []);
2680
- const registeredIds = new Set(registrations.map((registration) => registration.id));
2681
- let diagnosticSink = options.onDiagnostic;
2682
- const budgetTracker = options.budgetTracker ?? createEnvHookBudgetTracker();
2683
- const contract = {
2684
- runHook(input) {
2685
- return runMiddlewareRegistrations(input, registrations, {
2686
- budgetTracker,
2687
- ...diagnosticSink !== void 0 ? { onDiagnostic: diagnosticSink } : {}
2688
- });
2689
- },
2690
- runAsyncHook(input, priorEffects = []) {
2691
- return runMiddlewareAsyncRegistrations(input, registrations, priorEffects, {
2692
- ...diagnosticSink !== void 0 ? { onDiagnostic: diagnosticSink } : {}
2693
- });
2694
- },
2695
- listRules() {
2696
- return ruleDefinitions.map((definition) => cloneMiddlewareRule(definition.rule));
2697
- },
2698
- snapshot() {
2699
- return createMiddlewareSnapshot(ruleDefinitions.map((definition) => definition.rule));
2700
- },
2701
- registerHook(registration) {
2702
- if (registeredIds.has(registration.id)) return;
2703
- registeredIds.add(registration.id);
2704
- registrations.push(registration);
2705
- },
2706
- setDiagnosticSink(sink) {
2707
- diagnosticSink = sink;
2708
- }
2709
- };
2834
+ function receiptExtension(source) {
2710
2835
  return {
2711
- extension: {
2712
- start() {
2713
- return void 0;
2714
- }
2715
- },
2716
- contract
2836
+ id: source.provenance.id,
2837
+ scope: source.provenance.scope,
2838
+ canonicalRoot: source.provenance.canonicalRoot,
2839
+ manifestDigest: source.provenance.manifestDigest,
2840
+ contentDigest: source.provenance.contentDigest,
2841
+ declarationsDigest: source.declarationsDigest,
2842
+ generation: source.generation
2717
2843
  };
2718
2844
  }
2719
- function combineRuleDefinitions(builtin, registered) {
2720
- const seen = /* @__PURE__ */ new Set();
2721
- const combined = [];
2722
- for (const definition of [...builtin, ...registered]) {
2723
- if (seen.has(definition.rule.id)) continue;
2724
- seen.add(definition.rule.id);
2725
- combined.push(definition);
2845
+ function evaluateHook(hook, deps, base) {
2846
+ if (hook.spec.kind === "prompt") {
2847
+ const effect2 = reminderEffect(hook.spec.message, hook.spec.severity);
2848
+ deps.recordReceipt({ ...base, outcome: "emitted", effectKinds: ["inject_reminder"] });
2849
+ return [effect2];
2726
2850
  }
2727
- return combined;
2728
- }
2729
- function combineRegistrations(ruleDefinitions, coded) {
2730
- const seen = new Set(ruleDefinitions.map((definition) => definition.rule.id));
2731
- const combined = ruleDefinitions.map(registrationFromRuleDefinition);
2732
- for (const registration of coded) {
2733
- if (seen.has(registration.id)) continue;
2734
- seen.add(registration.id);
2735
- combined.push(registration);
2851
+ if (hook.spec.kind === "effect") {
2852
+ deps.recordReceipt({ ...base, outcome: "emitted", effectKinds: [hook.spec.effect.kind] });
2853
+ return [hook.spec.effect];
2736
2854
  }
2737
- return combined;
2855
+ const result = deps.runCommand(hook.spec.argv, {
2856
+ timeoutMs: hook.spec.timeoutMs,
2857
+ ...hook.spec.cwd !== void 0 ? { cwd: hook.spec.cwd } : {}
2858
+ });
2859
+ const output = (result.stdout || result.stderr || "").trim();
2860
+ if (result.timedOut) {
2861
+ deps.recordReceipt({ ...base, outcome: "command-timeout", exitCode: result.code ?? -1, outputChars: output.length });
2862
+ return [];
2863
+ }
2864
+ const outcome = result.code === 0 ? "command-ok" : "command-failed";
2865
+ deps.recordReceipt({ ...base, outcome, exitCode: result.code ?? -1, outputChars: output.length });
2866
+ if (output.length === 0) return [];
2867
+ const effect = commandEffect(hook.spec, output);
2868
+ return [effect];
2738
2869
  }
2739
2870
 
2740
- // src/domains/middleware/manifest.ts
2871
+ // src/domains/middleware/hooks-io.ts
2741
2872
  init_esm_shims();
2742
- var MiddlewareManifest = {
2743
- name: "middleware",
2744
- dependsOn: []
2745
- };
2873
+ var import_yaml = __toESM(require_dist(), 1);
2874
+ import { spawnSync } from "node:child_process";
2875
+ import { readFileSync as readFileSync3 } from "node:fs";
2876
+ import { join } from "node:path";
2877
+ var COMMAND_OUTPUT_MAX_BYTES = 1024 * 1024;
2878
+ function readBatch(source, filePath, fileIssues) {
2879
+ let text;
2880
+ try {
2881
+ text = readFileSync3(filePath, "utf8");
2882
+ } catch {
2883
+ return null;
2884
+ }
2885
+ try {
2886
+ const parsed = (0, import_yaml.parse)(text);
2887
+ return { source, declarations: parsed ?? [] };
2888
+ } catch (err) {
2889
+ fileIssues.push({
2890
+ source,
2891
+ message: `failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`
2892
+ });
2893
+ return null;
2894
+ }
2895
+ }
2896
+ function readHookSources(options) {
2897
+ const fileIssues = [];
2898
+ const batches = [];
2899
+ const captured = options.capturedSources;
2900
+ for (const entry of captured?.sources ?? []) {
2901
+ const source = {
2902
+ origin: "extension",
2903
+ sourcePath: `${entry.provenance.id}:hooks.yaml`,
2904
+ sourceId: entry.provenance.id,
2905
+ extension: {
2906
+ provenance: entry.provenance,
2907
+ generation: captured?.generation ?? 0,
2908
+ declarationsDigest: entry.declarationsDigest
2909
+ }
2910
+ };
2911
+ if (entry.parseError !== void 0) {
2912
+ fileIssues.push({ source, message: `failed to parse ${source.sourcePath}: ${entry.parseError}` });
2913
+ continue;
2914
+ }
2915
+ batches.push({ source, declarations: entry.declarations });
2916
+ }
2917
+ const projectBatch = readBatch(
2918
+ { origin: "project", sourcePath: ".clio-coder/hooks.yaml" },
2919
+ join(options.cwd, ".clio-coder", "hooks.yaml"),
2920
+ fileIssues
2921
+ );
2922
+ if (projectBatch) batches.push(projectBatch);
2923
+ const localBatch = readBatch(
2924
+ { origin: "project.local", sourcePath: ".clio-coder/hooks.local.yaml" },
2925
+ join(options.cwd, ".clio-coder", "hooks.local.yaml"),
2926
+ fileIssues
2927
+ );
2928
+ if (localBatch) batches.push(localBatch);
2929
+ return { batches, fileIssues };
2930
+ }
2931
+ function buildUserHookRegistrations(options) {
2932
+ const workspaceRoot = options.workspaceRoot ?? options.cwd;
2933
+ const readOptions = { cwd: options.cwd };
2934
+ if (options.capturedSources !== void 0) readOptions.capturedSources = options.capturedSources;
2935
+ const { batches, fileIssues } = readHookSources(readOptions);
2936
+ const loaded = loadUserHooks(batches, { workspaceRoot });
2937
+ const runCommand = options.runCommand ?? spawnSyncCommandRunner();
2938
+ const registrations = loaded.hooks.map(
2939
+ (hook) => userHookToRegistration(hook, {
2940
+ recordReceipt: options.recordReceipt,
2941
+ runCommand,
2942
+ ...options.now !== void 0 ? { now: options.now } : {}
2943
+ })
2944
+ );
2945
+ return { ...loaded, fileIssues, registrations };
2946
+ }
2947
+ function spawnSyncCommandRunner() {
2948
+ return (argv, options) => {
2949
+ const [command, ...args] = argv;
2950
+ const attribution = withManagedGitCommitAttributionEnvironment(process.env, {
2951
+ cwd: options.cwd ?? process.cwd(),
2952
+ evidence: {}
2953
+ });
2954
+ reportCommitAttributionDiagnostic(attribution.diagnostic);
2955
+ const result = spawnSync(command ?? "", args, {
2956
+ ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
2957
+ timeout: options.timeoutMs,
2958
+ maxBuffer: COMMAND_OUTPUT_MAX_BYTES,
2959
+ encoding: "utf8",
2960
+ shell: false,
2961
+ env: attribution.env
2962
+ });
2963
+ const timedOut = result.error !== void 0 && result.error.code === "ETIMEDOUT";
2964
+ return {
2965
+ code: result.status,
2966
+ timedOut,
2967
+ stdout: typeof result.stdout === "string" ? result.stdout : "",
2968
+ stderr: typeof result.stderr === "string" ? result.stderr : ""
2969
+ };
2970
+ };
2971
+ }
2746
2972
 
2747
2973
  // src/domains/middleware/index.ts
2748
2974
  var MiddlewareDomainModule = {
@@ -2754,15 +2980,18 @@ export {
2754
2980
  MIDDLEWARE_HOOK_TEXT_MAX_CHARS,
2755
2981
  shouldRequestStalledTurnContinuation,
2756
2982
  writeMiddlewareDiagnosticToStderr,
2983
+ formatRegistrationConflict,
2757
2984
  createMiddlewareContractFromSnapshot,
2758
2985
  createReadOnlyExplorationNudgeRegistration,
2759
2986
  createUnbackedWorkerClaimRegistration,
2760
2987
  openDetachedBatchViews,
2761
2988
  createDetachedDispatchNudgeRegistration,
2989
+ HOOK_RECEIPT_LOG_CAPACITY,
2762
2990
  createHookReceiptLog,
2991
+ readPersistedHookReceipts,
2763
2992
  loadUserHooks,
2764
2993
  readHookSources,
2765
- installUserHooks,
2994
+ buildUserHookRegistrations,
2766
2995
  createSkillsReminderRegistration,
2767
2996
  createMarketplaceOfferRegistration,
2768
2997
  createMemoryInterventionRegistration,
@@ -2775,4 +3004,4 @@ export {
2775
3004
  createWatchdogRegistration,
2776
3005
  MiddlewareDomainModule
2777
3006
  };
2778
- //# sourceMappingURL=chunk-JA5QWE4Z.js.map
3007
+ //# sourceMappingURL=chunk-UBRFI4HS.js.map