@iowarp/clio-coder 0.3.7 → 0.3.9

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 (400) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +13 -4
  3. package/dist/{acp-SK4MD6MM.js → acp-7LOELQFP.js} +13 -13
  4. package/dist/{agents-2FN2K6ME.js → agents-FIBG2SHA.js} +41 -37
  5. package/dist/assets/codewiki.json +1 -1
  6. package/dist/{auth-QIYZWM5I.js → auth-OI4LIH2I.js} +31 -24
  7. package/dist/builtins-AD25UL3C.js +17 -0
  8. package/dist/{chunk-5FR74PWO.js → chunk-2JDWVJND.js} +2 -2
  9. package/dist/chunk-3DPEIQKN.js +113 -0
  10. package/dist/{chunk-EOOQZZDE.js → chunk-3DUR4WUA.js} +19 -19
  11. package/dist/{chunk-WHJYKASB.js → chunk-3MRC2YSQ.js} +2 -2
  12. package/dist/{chunk-EBEFWSGL.js → chunk-3UUY7R3Z.js} +14 -10
  13. package/dist/{chunk-LADCF22A.js → chunk-3V5AYSEQ.js} +113 -54
  14. package/dist/{chunk-BMWK7ZIZ.js → chunk-465CC7FK.js} +16 -13
  15. package/dist/{chunk-5WIGXA4T.js → chunk-47CMYGET.js} +111 -4
  16. package/dist/{chunk-CEYBNUGC.js → chunk-4H6ULJ3H.js} +378 -36
  17. package/dist/{chunk-YTYFXUI3.js → chunk-4LJX2PUC.js} +9 -9
  18. package/dist/{chunk-DOOEX22V.js → chunk-56KB5IJP.js} +5 -5
  19. package/dist/{chunk-SPULKLCF.js → chunk-5DQRIYDZ.js} +2 -2
  20. package/dist/{chunk-TSHXZTOQ.js → chunk-5HFBWUMU.js} +23 -11
  21. package/dist/{chunk-5UJ6ECTS.js → chunk-5PVQ4SRS.js} +80 -8
  22. package/dist/{chunk-ZWLZP4ZT.js → chunk-5QKCQQ3E.js} +359 -17
  23. package/dist/{chunk-6M7VS3J3.js → chunk-5T7RBWN2.js} +111 -5
  24. package/dist/chunk-774ILSRL.js +172 -0
  25. package/dist/chunk-7C6RYZGQ.js +391 -0
  26. package/dist/{chunk-GH5622CP.js → chunk-A2NJGIB3.js} +2 -2
  27. package/dist/{chunk-C4JBQ5SR.js → chunk-AD7Y7STJ.js} +6 -6
  28. package/dist/{chunk-GEYXPTRF.js → chunk-AEYBF3TB.js} +33 -12
  29. package/dist/{chunk-2SFS6XQE.js → chunk-AMKHQW3C.js} +3 -2
  30. package/dist/{chunk-D4MDIG46.js → chunk-B5CSFE7B.js} +7 -7
  31. package/dist/{chunk-MXI6J5JF.js → chunk-B5XRQOLB.js} +10 -10
  32. package/dist/{chunk-X2KV5FXT.js → chunk-BVDVID7E.js} +2 -2
  33. package/dist/{chunk-JNXPYBB4.js → chunk-CA42X6KT.js} +3 -3
  34. package/dist/{chunk-VREKEFLL.js → chunk-D73KXYPF.js} +3 -3
  35. package/dist/{chunk-JTSEDYVQ.js → chunk-DG4M6ZUE.js} +7 -7
  36. package/dist/{chunk-DQA7QLMD.js → chunk-EBOC7MT3.js} +10 -25
  37. package/dist/{chunk-KZ2H5X4G.js → chunk-ECUO3KDP.js} +129 -14
  38. package/dist/{chunk-JRIO5UD2.js → chunk-EQ63NRB7.js} +5 -5
  39. package/dist/{chunk-YD734TPH.js → chunk-FALJGAWU.js} +2 -2
  40. package/dist/{chunk-GWS3VEIW.js → chunk-FWDFM5ZU.js} +24 -3
  41. package/dist/{chunk-XEGB6BCN.js → chunk-GAYUJ7LE.js} +68 -14
  42. package/dist/{chunk-UND3GU2L.js → chunk-H7IXIC72.js} +2 -2
  43. package/dist/{chunk-IR4CFBFN.js → chunk-HAY4ZE2P.js} +12 -12
  44. package/dist/{chunk-UVDSQ6LW.js → chunk-HCBCAYZU.js} +74 -147
  45. package/dist/{chunk-4DWFMQDR.js → chunk-HJB5IUKP.js} +89 -145
  46. package/dist/{chunk-M4AKACEO.js → chunk-HKO36JWF.js} +33 -5
  47. package/dist/{chunk-KCMKRQX4.js → chunk-HPCTNZM2.js} +45 -82
  48. package/dist/{chunk-465YSENW.js → chunk-IFBNV6H6.js} +3 -3
  49. package/dist/{chunk-FJ3H4MN5.js → chunk-IHKBWSXF.js} +2 -2
  50. package/dist/chunk-JEQQR47K.js +3025 -0
  51. package/dist/{chunk-FO5ZOVUY.js → chunk-KV2AOLDF.js} +27 -7
  52. package/dist/chunk-LU7P4LHA.js +33 -0
  53. package/dist/{chunk-6TUKSZVF.js → chunk-LXPJXFM5.js} +11 -11
  54. package/dist/{chunk-VQNODYQ4.js → chunk-MIX5N5AC.js} +488 -3668
  55. package/dist/chunk-MLOK6ZOS.js +2888 -0
  56. package/dist/{chunk-ZZMN5OM4.js → chunk-MV2VUEJC.js} +2 -2
  57. package/dist/{chunk-MVVUPGPW.js → chunk-MXHC5QYU.js} +6 -6
  58. package/dist/{chunk-OBMAI2DP.js → chunk-N3PBVRTZ.js} +12 -388
  59. package/dist/{chunk-WJHBC77E.js → chunk-N5XKWMDW.js} +17 -7
  60. package/dist/{chunk-5C3AQNDW.js → chunk-NNNWO6F2.js} +124 -36
  61. package/dist/{chunk-UFQ3F4FW.js → chunk-NQ6UCCOD.js} +4 -4
  62. package/dist/chunk-NUGM5KR6.js +165 -0
  63. package/dist/{chunk-DMD2AGVS.js → chunk-NZU6YDNV.js} +20 -18
  64. package/dist/{chunk-WHGPSPT5.js → chunk-O6I4CIEU.js} +151 -13
  65. package/dist/{chunk-XN3L4EYL.js → chunk-OEDBCISO.js} +2 -2
  66. package/dist/{chunk-PD3MESLB.js → chunk-P3JGPQFL.js} +4 -4
  67. package/dist/{chunk-UHXRNZ2J.js → chunk-PNY46YEY.js} +23 -6
  68. package/dist/{chunk-THKY7CD7.js → chunk-PZ4I4JE2.js} +134 -29
  69. package/dist/{chunk-SROCI7ZU.js → chunk-QQ7EKM72.js} +5 -5
  70. package/dist/{chunk-QCTRSGHQ.js → chunk-R7LNVMCS.js} +91 -53
  71. package/dist/{chunk-GOXNB3AO.js → chunk-RAPCMZL4.js} +75 -4
  72. package/dist/chunk-RKKLTLYB.js +45 -0
  73. package/dist/{chunk-OB5HIGJY.js → chunk-RKRLDWD3.js} +4 -1
  74. package/dist/{chunk-DJNLUABN.js → chunk-S4COXYBG.js} +588 -32
  75. package/dist/{chunk-3HAPLH5M.js → chunk-T3Z6VAAF.js} +172 -11
  76. package/dist/{chunk-FOT2FX5J.js → chunk-TD7UE2L5.js} +12 -10
  77. package/dist/{chunk-UUANF5CR.js → chunk-TEO2TLVN.js} +856 -967
  78. package/dist/{chunk-FCSXB6T2.js → chunk-UOSL25KY.js} +14 -2
  79. package/dist/{chunk-EELBMBT6.js → chunk-VKBMFOYV.js} +74 -15
  80. package/dist/chunk-VO2LKSTM.js +165 -0
  81. package/dist/{chunk-5C77SEEY.js → chunk-VPTUJU4P.js} +3 -3
  82. package/dist/{chunk-WEH5XRJQ.js → chunk-WIE7ZOSW.js} +2 -2
  83. package/dist/{chunk-J7PIKKWC.js → chunk-WXCJ7VME.js} +8 -8
  84. package/dist/{chunk-4DGYLA73.js → chunk-XDOQXGFO.js} +22 -7
  85. package/dist/{chunk-PPAMZ32Z.js → chunk-XK56QHLX.js} +6 -1
  86. package/dist/{chunk-AB4XIIVB.js → chunk-YKOFT37S.js} +6 -6
  87. package/dist/chunk-YSEHGPCT.js +127 -0
  88. package/dist/{chunk-HFSBBKSQ.js → chunk-YW7UVM5V.js} +138 -3
  89. package/dist/cli/index.js +32 -32
  90. package/dist/{clio-WBVQEBKO.js → clio-LT5V7SSZ.js} +9 -9
  91. package/dist/{code-nav-FGGFIE7L.js → code-nav-LMW275PA.js} +5 -5
  92. package/dist/codewiki/build-worker.js +4 -4
  93. package/dist/{components-F7OEATSO.js → components-ZFA3SAER.js} +8 -8
  94. package/dist/{config-TRBL3RCF.js → config-RXS5T3JT.js} +98 -65
  95. package/dist/{configure-OLCVPHNM.js → configure-2WYWSCSD.js} +26 -22
  96. package/dist/{context-MJIJ6GOX.js → context-I3BTOTCS.js} +12 -12
  97. package/dist/{context-XEWE3MOJ.js → context-MVOORGMF.js} +54 -47
  98. package/dist/{context-WFPKQSM6.js → context-PALKKQYL.js} +28 -28
  99. package/dist/{context-clear-KNOS2JPB.js → context-clear-N2WOYZ2K.js} +53 -46
  100. package/dist/{context-index-SSR5ECNE.js → context-index-HNG3MOME.js} +6 -6
  101. package/dist/{context-working-set-EUXAZI6N.js → context-working-set-MIEVECVZ.js} +17 -18
  102. package/dist/{dispatch-runner-B7MTOVKL.js → dispatch-runner-VVA4SRRH.js} +90 -61
  103. package/dist/{docs-FLJTIDSE.js → docs-7LQ23DLM.js} +8 -8
  104. package/dist/doctor-TWBWFK5V.js +165 -0
  105. package/dist/eval-IJ5VEZDJ.js +4483 -0
  106. package/dist/{evidence-JZNBUOQZ.js → evidence-L5APPXNV.js} +68 -61
  107. package/dist/{evolve-FJVC4KKI.js → evolve-RGNKFJ52.js} +47 -40
  108. package/dist/{extensions-IQL36S7K.js → extensions-7WYWUX5A.js} +13 -7
  109. package/dist/{fleet-BDKYJFCP.js → fleet-6CNVBZZP.js} +113 -76
  110. package/dist/{fleet-commands-ZFIWZSB3.js → fleet-commands-L2SXSYEI.js} +10 -10
  111. package/dist/{fleet-graph-Y6HPXIVF.js → fleet-graph-2J3OOIPO.js} +17 -15
  112. package/dist/{fleet-preflight-BHSNPBMH.js → fleet-preflight-CZRJ4JP5.js} +5 -6
  113. package/dist/{fleet-validate-BIYREGIK.js → fleet-validate-C5RI6DP7.js} +20 -19
  114. package/dist/{init-LQUB5COQ.js → init-VBN2ACVA.js} +70 -63
  115. package/dist/{library-NJAHIGG4.js → library-JHGUMLY2.js} +22 -20
  116. package/dist/{memory-OG6HOYKM.js → memory-K4OQIYWG.js} +49 -42
  117. package/dist/{models-5ZG5XY7J.js → models-2NCZUWDD.js} +35 -29
  118. package/dist/{monitor-TJ7AMTGB.js → monitor-MMVTJABD.js} +64 -45
  119. package/dist/{orchestrator-WZYB54DM.js → orchestrator-ZKBPCHW6.js} +1971 -520
  120. package/dist/{paths-XUC7GS6E.js → paths-DBXMZMDU.js} +5 -5
  121. package/dist/registry-LG64LTF4.js +11 -0
  122. package/dist/{reset-PXQT45IY.js → reset-DD5JGOY3.js} +11 -11
  123. package/dist/{run-FQ74YF62.js → run-QEGNX7FL.js} +89 -83
  124. package/dist/{share-FW7SVCL3.js → share-JKD3BQMW.js} +20 -18
  125. package/dist/{skills-7E7IRB3R.js → skills-LMQIKDOZ.js} +23 -21
  126. package/dist/{skills-eval-LI75W6OK.js → skills-eval-I7X2774U.js} +59 -52
  127. package/dist/{steer-GGWFUJUD.js → steer-CF5TDANS.js} +3 -3
  128. package/dist/support-I7LOJLIF.js +38 -0
  129. package/dist/{targets-4CIFKCTW.js → targets-RUSR6B5Z.js} +77 -42
  130. package/dist/{terminal-lease-WUZY7ZV5.js → terminal-lease-QYVORFR4.js} +6 -4
  131. package/dist/{trace-PNCASAXC.js → trace-ODOQIVIW.js} +61 -6
  132. package/dist/{uninstall-7FV7IP4E.js → uninstall-ZJF5H5ZN.js} +8 -8
  133. package/dist/{upgrade-K2HVIVMQ.js → upgrade-XANW3FXB.js} +29 -26
  134. package/dist/{usage-GTZELZQX.js → usage-4H7ZRXQT.js} +110 -61
  135. package/dist/{verifiers-RLAHT27O.js → verifiers-UZXNBZEB.js} +13 -13
  136. package/dist/{verify-BX3BRKH5.js → verify-BVKWTNDL.js} +9 -9
  137. package/dist/{wiki-generate-ASIFASCN.js → wiki-generate-MY7WV2QI.js} +76 -69
  138. package/dist/worker/entry.js +69 -66
  139. package/docs/alcf-provider.md +1 -1
  140. package/docs/architecture.md +1 -1
  141. package/docs/artifact-versions.md +11 -5
  142. package/docs/built-in-agents.md +1 -1
  143. package/docs/capacity-and-scheduling.md +23 -2
  144. package/docs/commands-and-modes.md +2 -2
  145. package/docs/configuration-and-targets.md +37 -5
  146. package/docs/context-engine.md +63 -4
  147. package/docs/documentation-coverage.md +3 -3
  148. package/docs/documentation-guide.md +1 -1
  149. package/docs/environment-variables.md +2 -0
  150. package/docs/eval-runner.md +262 -11
  151. package/docs/evals-internal.md +72 -2
  152. package/docs/evidence-and-memory.md +77 -12
  153. package/docs/evolution.md +1 -1
  154. package/docs/extensions-and-sharing.md +3 -1
  155. package/docs/fleet-dispatch.md +34 -9
  156. package/docs/glossary.md +21 -1
  157. package/docs/installation-and-lifecycle.md +1 -1
  158. package/docs/middleware-and-components.md +1 -1
  159. package/docs/model-catalog.md +1 -1
  160. package/docs/observability.md +54 -3
  161. package/docs/proactive-memory.md +127 -14
  162. package/docs/prompt-envelope-and-tools.md +22 -2
  163. package/docs/provider-adapter-cookbook.md +1 -1
  164. package/docs/release-cut-checklist.md +60 -41
  165. package/docs/safety-model.md +1 -1
  166. package/docs/scientific-validation.md +1 -1
  167. package/docs/skills-marketplace.md +1 -1
  168. package/docs/tool-usage.md +1 -1
  169. package/docs/trace-store.md +1 -1
  170. package/docs/troubleshooting.md +87 -0
  171. package/docs/tui-design.md +1 -1
  172. package/docs/worker-dispatch-mechanics.md +1 -1
  173. package/package.json +2 -2
  174. package/src/cli/agents.ts +1 -1
  175. package/src/cli/argv.ts +5 -0
  176. package/src/cli/config-inspect.ts +33 -6
  177. package/src/cli/config.ts +1 -1
  178. package/src/cli/configure.ts +107 -23
  179. package/src/cli/doctor-state-size.ts +82 -0
  180. package/src/cli/doctor.ts +7 -1
  181. package/src/cli/eval.ts +80 -16
  182. package/src/cli/evidence.ts +30 -25
  183. package/src/cli/extensions.ts +5 -1
  184. package/src/cli/fleet-preflight.ts +2 -12
  185. package/src/cli/fleet.ts +32 -3
  186. package/src/cli/shared.ts +1 -0
  187. package/src/cli/targets.ts +45 -11
  188. package/src/cli/trace.ts +63 -4
  189. package/src/cli/usage.ts +63 -14
  190. package/src/cli/validate-model.ts +60 -5
  191. package/src/core/bus-events.ts +54 -1
  192. package/src/core/cache-telemetry.ts +42 -0
  193. package/src/core/commit-attribution.ts +4 -4
  194. package/src/core/config.ts +18 -0
  195. package/src/core/defaults.ts +36 -6
  196. package/src/core/endpoint-key.ts +27 -0
  197. package/src/core/path-boundary.ts +100 -0
  198. package/src/core/residency-target-key.ts +25 -0
  199. package/src/core/response-schema.ts +36 -2
  200. package/src/domains/agents/extension.ts +2 -11
  201. package/src/domains/agents/fleet-contract.ts +30 -12
  202. package/src/domains/agents/recipe.ts +7 -1
  203. package/src/domains/agents/registry.ts +73 -5
  204. package/src/domains/agents/result-contract.ts +128 -17
  205. package/src/domains/agents/write-boundary.ts +15 -50
  206. package/src/domains/config/classify.ts +3 -0
  207. package/src/domains/context/codewiki/coordinator.ts +12 -4
  208. package/src/domains/context/project-rules.ts +51 -1
  209. package/src/domains/dispatch/admission.ts +40 -3
  210. package/src/domains/dispatch/assignment-reconcile.ts +22 -5
  211. package/src/domains/dispatch/assignment-store.ts +151 -14
  212. package/src/domains/dispatch/capacity-lease.ts +98 -9
  213. package/src/domains/dispatch/contract.ts +26 -1
  214. package/src/domains/dispatch/delegation-plan.ts +2 -5
  215. package/src/domains/dispatch/execution-plan.ts +44 -4
  216. package/src/domains/dispatch/execution-role.ts +9 -1
  217. package/src/domains/dispatch/extension.ts +309 -96
  218. package/src/domains/dispatch/fleet-run.ts +78 -4
  219. package/src/domains/dispatch/gate-role-prompts.ts +38 -0
  220. package/src/domains/dispatch/heartbeat.ts +32 -8
  221. package/src/domains/dispatch/index.ts +6 -1
  222. package/src/domains/dispatch/intent-requirements.ts +40 -0
  223. package/src/domains/dispatch/intent.ts +84 -8
  224. package/src/domains/dispatch/orphan-recovery.ts +5 -0
  225. package/src/domains/dispatch/path-scope.ts +370 -0
  226. package/src/domains/dispatch/receipt-integrity.ts +2 -1
  227. package/src/domains/dispatch/reservation-store.ts +116 -8
  228. package/src/domains/dispatch/state.ts +4 -0
  229. package/src/domains/dispatch/types.ts +14 -7
  230. package/src/domains/dispatch/validation.ts +6 -3
  231. package/src/domains/dispatch/worker-spawn.ts +25 -11
  232. package/src/domains/dispatch/write-boundary-enforcer.ts +62 -0
  233. package/src/domains/dispatch/write-boundary.ts +262 -22
  234. package/src/domains/eval/artifacts/store.ts +62 -0
  235. package/src/domains/eval/compare/behavioral.ts +224 -0
  236. package/src/domains/eval/compare/compare.ts +355 -2
  237. package/src/domains/eval/compare/envelope.ts +128 -0
  238. package/src/domains/eval/compare/gates.ts +24 -6
  239. package/src/domains/eval/compare/thresholds.ts +30 -3
  240. package/src/domains/eval/execution-provenance.ts +240 -0
  241. package/src/domains/eval/metrics/aggregate.ts +136 -0
  242. package/src/domains/eval/metrics/call-ledger-stream.ts +112 -0
  243. package/src/domains/eval/metrics/evidence.ts +79 -2
  244. package/src/domains/eval/metrics/tracked.ts +413 -0
  245. package/src/domains/eval/provenance.ts +117 -0
  246. package/src/domains/eval/reports/comparison.ts +128 -0
  247. package/src/domains/eval/reports/junit.ts +17 -3
  248. package/src/domains/eval/reports/markdown.ts +3 -3
  249. package/src/domains/eval/reports/text.ts +14 -0
  250. package/src/domains/eval/run-compare.ts +20 -0
  251. package/src/domains/eval/runners/clio-run.ts +139 -2
  252. package/src/domains/eval/runners/external-command.ts +28 -3
  253. package/src/domains/eval/schema/adapter.ts +111 -0
  254. package/src/domains/eval/schema/artifact.ts +20 -0
  255. package/src/domains/eval/schema/behavioral-metrics.ts +204 -0
  256. package/src/domains/eval/schema/behavioral.ts +520 -0
  257. package/src/domains/eval/schema/execution-envelope.ts +194 -0
  258. package/src/domains/eval/schema/serving.ts +74 -0
  259. package/src/domains/eval/schema/suite.ts +38 -8
  260. package/src/domains/eval/schema/validate.ts +58 -3
  261. package/src/domains/eval/schema/verdict.ts +237 -0
  262. package/src/domains/eval/suites/resolve.ts +2 -0
  263. package/src/domains/eval/suites/run.ts +264 -33
  264. package/src/domains/eval/verifiers/command.ts +2 -1
  265. package/src/domains/eval/workspaces/temp-copy.ts +145 -13
  266. package/src/domains/evidence/build.ts +68 -21
  267. package/src/domains/evidence/eval.ts +2 -12
  268. package/src/domains/evidence/findings-markdown.ts +33 -0
  269. package/src/domains/evidence/index.ts +21 -0
  270. package/src/domains/evidence/provenance.ts +46 -11
  271. package/src/domains/evidence/run-trust.ts +7 -113
  272. package/src/domains/evidence/trust-projection.ts +274 -0
  273. package/src/domains/evidence/trust-status.ts +145 -17
  274. package/src/domains/evidence/types.ts +4 -0
  275. package/src/domains/extensions/compatibility.ts +285 -0
  276. package/src/domains/extensions/discovery.ts +126 -4
  277. package/src/domains/extensions/resources.ts +21 -9
  278. package/src/domains/extensions/state.ts +18 -5
  279. package/src/domains/extensions/types.ts +6 -1
  280. package/src/domains/lifecycle/doctor.ts +209 -2
  281. package/src/domains/memory/index.ts +14 -0
  282. package/src/domains/memory/task-bank-promotion.ts +64 -0
  283. package/src/domains/memory/task-memory-policy.ts +77 -8
  284. package/src/domains/memory/task-memory-spend.ts +131 -0
  285. package/src/domains/memory/task-memory-status.ts +7 -0
  286. package/src/domains/memory/task-memory-telemetry.ts +2 -0
  287. package/src/domains/middleware/index.ts +1 -0
  288. package/src/domains/middleware/memory-intervention.ts +69 -5
  289. package/src/domains/middleware/memory-step-endpoint.ts +71 -0
  290. package/src/domains/observability/background-memory-usage.ts +140 -0
  291. package/src/domains/observability/cost.ts +1 -1
  292. package/src/domains/observability/index.ts +7 -0
  293. package/src/domains/observability/out-of-turn-usage.ts +51 -2
  294. package/src/domains/observability/trace-store.ts +192 -2
  295. package/src/domains/prompts/compiler.ts +100 -13
  296. package/src/domains/prompts/contract.ts +3 -5
  297. package/src/domains/providers/endpoint-capacity.ts +96 -0
  298. package/src/domains/providers/extension.ts +30 -2
  299. package/src/domains/providers/index.ts +10 -0
  300. package/src/domains/providers/models/local-models/clio-local-coding-targets.yaml +243 -1
  301. package/src/domains/providers/runtime-resolution.ts +8 -1
  302. package/src/domains/providers/runtimes/common/probe-helpers.ts +31 -9
  303. package/src/domains/providers/runtimes/local-native/llamacpp-anthropic.ts +1 -1
  304. package/src/domains/providers/runtimes/local-native/llamacpp-completion.ts +1 -1
  305. package/src/domains/providers/runtimes/local-native/llamacpp-embed.ts +1 -1
  306. package/src/domains/providers/runtimes/local-native/llamacpp-rerank.ts +1 -1
  307. package/src/domains/providers/runtimes/local-native/llamacpp.ts +4 -1
  308. package/src/domains/providers/runtimes/local-native/lmstudio.ts +4 -1
  309. package/src/domains/providers/runtimes/local-native/ollama-native.ts +6 -1
  310. package/src/domains/providers/types/capability-flags.ts +2 -0
  311. package/src/domains/providers/types/target-descriptor.ts +2 -0
  312. package/src/domains/resources/common-loader.ts +3 -0
  313. package/src/domains/resources/prompts/loader.ts +184 -17
  314. package/src/domains/safety/call-target.ts +52 -0
  315. package/src/domains/safety/policy-engine.ts +5 -5
  316. package/src/domains/safety/run-effects.ts +96 -2
  317. package/src/domains/safety/scope.ts +7 -12
  318. package/src/domains/session/context-accounting.ts +52 -1
  319. package/src/domains/session/context-ledger.ts +37 -13
  320. package/src/domains/session/index.ts +6 -0
  321. package/src/domains/session/prompt-cache.ts +140 -0
  322. package/src/domains/session/prompt-manifest.ts +42 -0
  323. package/src/engine/acp/adapter.ts +18 -3
  324. package/src/engine/acp/server.ts +4 -1
  325. package/src/engine/ai.ts +35 -0
  326. package/src/engine/apis/llamacpp-residency.ts +55 -3
  327. package/src/engine/apis/lmstudio.ts +25 -5
  328. package/src/engine/apis/ollama-native.ts +2 -1
  329. package/src/engine/apis/openai-completions.ts +80 -17
  330. package/src/engine/apis/residency-lock.ts +3 -1
  331. package/src/engine/apis/residency.ts +34 -1
  332. package/src/engine/prompt-templates.ts +18 -1
  333. package/src/engine/provider-payload.ts +29 -1
  334. package/src/engine/worker-runtime.ts +6 -3
  335. package/src/entry/orchestrator.ts +176 -30
  336. package/src/interactive/chat-loop-messages.ts +26 -7
  337. package/src/interactive/chat-loop.ts +318 -41
  338. package/src/interactive/chat-panel.ts +62 -8
  339. package/src/interactive/clio-editor.ts +45 -8
  340. package/src/interactive/context-activity.ts +5 -1
  341. package/src/interactive/context-meter.ts +1 -1
  342. package/src/interactive/context-overlay.ts +40 -10
  343. package/src/interactive/cost-overlay.ts +64 -6
  344. package/src/interactive/dispatch-board.ts +127 -5
  345. package/src/interactive/fleet-run-preview.ts +41 -15
  346. package/src/interactive/handoff-round.ts +41 -2
  347. package/src/interactive/interactive-application.ts +24 -1
  348. package/src/interactive/interactive-event-projection.ts +14 -0
  349. package/src/interactive/interactive-input-runtime.ts +8 -0
  350. package/src/interactive/interactive-presentation.ts +4 -0
  351. package/src/interactive/interactive-shell.ts +20 -17
  352. package/src/interactive/interactive-slash-runtime.ts +27 -4
  353. package/src/interactive/memory-overlay.ts +8 -0
  354. package/src/interactive/mutation-preview.ts +295 -0
  355. package/src/interactive/overlay-general-openers.ts +16 -0
  356. package/src/interactive/overlay-key-routing.ts +38 -0
  357. package/src/interactive/overlay-lifecycle.ts +38 -5
  358. package/src/interactive/overlay-permission-lifecycle.ts +22 -2
  359. package/src/interactive/overlay-session-lifecycle.ts +73 -9
  360. package/src/interactive/overlays/ask-user.ts +91 -19
  361. package/src/interactive/overlays/help-reference.ts +4 -0
  362. package/src/interactive/overlays/prompts.ts +11 -1
  363. package/src/interactive/overlays/settings.ts +176 -48
  364. package/src/interactive/permission-hint.ts +34 -2
  365. package/src/interactive/permission-overlay.ts +159 -9
  366. package/src/interactive/prewarm.ts +197 -0
  367. package/src/interactive/render-trace.ts +162 -15
  368. package/src/interactive/renderers/tool-execution.ts +4 -0
  369. package/src/interactive/side-question.ts +58 -1
  370. package/src/interactive/slash-commands.ts +7 -2
  371. package/src/interactive/status/controller.ts +11 -0
  372. package/src/interactive/status/state-machine.ts +54 -2
  373. package/src/interactive/status/types.ts +7 -0
  374. package/src/interactive/terminal-lease.ts +2 -0
  375. package/src/interactive/turn-context.ts +299 -31
  376. package/src/interactive/turn-persistence.ts +14 -4
  377. package/src/interactive/turn-prewarm.ts +364 -0
  378. package/src/interactive/turn-queues.ts +7 -4
  379. package/src/interactive/turn-runtime.ts +8 -1
  380. package/src/interactive/turn-state.ts +23 -0
  381. package/src/interactive/view/artifacts.ts +42 -9
  382. package/src/interactive/view/view-overlay.ts +43 -6
  383. package/src/interactive/worker-receipts.ts +14 -2
  384. package/src/interactive/worker-stream.ts +8 -0
  385. package/src/tools/ask-user.ts +43 -2
  386. package/src/tools/dispatch-admission.ts +12 -13
  387. package/src/tools/dispatch-arguments.ts +27 -0
  388. package/src/tools/dispatch-plan.ts +46 -9
  389. package/src/tools/dispatch-runner.ts +48 -13
  390. package/src/tools/dispatch-scout.ts +1 -1
  391. package/src/tools/monitor.ts +13 -0
  392. package/src/tools/registry.ts +16 -0
  393. package/src/tools/worker-evidence.ts +19 -13
  394. package/src/worker/spec-contract.ts +2 -1
  395. package/dist/chunk-AOCYTWAV.js +0 -449
  396. package/dist/chunk-HWUFFB6L.js +0 -83
  397. package/dist/chunk-R346GLFC.js +0 -31
  398. package/dist/chunk-ZGH7FGS5.js +0 -1079
  399. package/dist/doctor-RN4YKO2X.js +0 -87
  400. package/dist/eval-RUBJVSNQ.js +0 -2557
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import { lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import path from "node:path";
4
4
  import { parseCommandArgs, substituteArgs } from "../../../engine/prompt-templates.js";
@@ -13,7 +13,6 @@ import {
13
13
  import {
14
14
  COMPAT_RESOURCE_PRECEDENCE,
15
15
  defaultScopedResourceRoots,
16
- readRootEntries,
17
16
  sourceInfoForRoot,
18
17
  splitYamlFrontmatter,
19
18
  stringField,
@@ -28,11 +27,21 @@ export interface PromptTemplate {
28
27
  sourceInfo: ResourceSourceInfo;
29
28
  /** False for a project compatibility root until the operator opts in; such a template refuses to expand. */
30
29
  trusted: boolean;
30
+ /**
31
+ * Why this template cannot be expanded, when a load-time failure left it
32
+ * with no usable body. It is loaded anyway, so the namespaced command is
33
+ * recognized and invoking it reports this reason instead of "not a command"
34
+ * (issue #245). `content` is empty on such a template and nothing may send
35
+ * it to a model.
36
+ */
37
+ unavailable?: string;
31
38
  argumentHint?: string;
32
39
  }
33
40
 
34
41
  export interface PromptTemplateRoot {
35
42
  path: string;
43
+ /** Present only for an installed extension resource root. */
44
+ rootPath?: string;
36
45
  scope: ResourceScope;
37
46
  source?: string;
38
47
  precedence?: number;
@@ -139,6 +148,95 @@ function fallbackDescription(body: string): string {
139
148
  return normalized.length > 60 ? `${normalized.slice(0, 57)}...` : normalized;
140
149
  }
141
150
 
151
+ // Keep the suffix deliberately conservative. Prompt prose commonly places a
152
+ // comma, parenthesis, or full stop immediately after an @path; treating that
153
+ // punctuation as part of the filesystem reference makes a valid template look
154
+ // unresolved. Extension bundles use portable path components, so these are the
155
+ // only characters needed in a reference while still allowing `..` to be
156
+ // detected and rejected by the containment check below.
157
+ const EXTENSION_ROOT_REFERENCE = /\$\{extensionRoot\}(\/[A-Za-z0-9._~%+@/-]*)?/g;
158
+ const EXTENSION_ROOT_TOKEN = "$" + "{extensionRoot}";
159
+
160
+ function contained(root: string, candidate: string): boolean {
161
+ const relative = path.relative(root, candidate);
162
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
163
+ }
164
+
165
+ /**
166
+ * The outcome of a step that may leave the template with no usable body.
167
+ * `reason` is the one sentence that becomes both the diagnostic the `/prompts`
168
+ * overlay renders and the refusal the operator gets when they invoke the
169
+ * command, so the two can never disagree.
170
+ */
171
+ type BodyResolution = { body: string } | { reason: string };
172
+
173
+ /**
174
+ * Resolve only existing paths contained by the declaring extension package.
175
+ *
176
+ * The safety half is unchanged: a missing or escaping reference never expands
177
+ * and never reads outside the package. What changed is what the caller does
178
+ * with the failure, which used to be dropping the template entirely.
179
+ */
180
+ function resolveExtensionReferences(body: string, root: PromptTemplateRoot): BodyResolution {
181
+ if (root.rootPath === undefined || !body.includes(EXTENSION_ROOT_TOKEN)) return { body };
182
+ let canonicalRoot: string;
183
+ try {
184
+ canonicalRoot = realpathSync(root.rootPath);
185
+ } catch (error) {
186
+ return { reason: `extension root could not be resolved: ${error instanceof Error ? error.message : String(error)}` };
187
+ }
188
+ let invalid: string | null = null;
189
+ const resolved = body.replace(EXTENSION_ROOT_REFERENCE, (reference, suffix: string | undefined) => {
190
+ const candidate = path.resolve(canonicalRoot, `.${suffix ?? ""}`);
191
+ try {
192
+ const canonicalCandidate = realpathSync(candidate);
193
+ if (!contained(canonicalRoot, canonicalCandidate)) invalid = reference;
194
+ } catch {
195
+ invalid = reference;
196
+ }
197
+ return candidate;
198
+ });
199
+ if (invalid !== null) {
200
+ return { reason: `prompt template has an unresolved or escaping extension reference: ${invalid}` };
201
+ }
202
+ return { body: resolved };
203
+ }
204
+
205
+ /**
206
+ * A template that exists and cannot run.
207
+ *
208
+ * Dropping it made the operator's namespaced command answer "not a command",
209
+ * so an extension author's single bad `${extensionRoot}` path read to their
210
+ * users as "the prompt was never installed" (issue #245). The template is
211
+ * loaded with an empty body and the reason attached instead, which is the same
212
+ * shape an untrusted project template already had, so the existing refusal
213
+ * path carries it to the operator with no new machinery.
214
+ */
215
+ function unavailableTemplate(
216
+ name: string,
217
+ filePath: string,
218
+ root: PromptTemplateRoot,
219
+ reason: string,
220
+ diagnostics: ResourceDiagnostic[],
221
+ description?: string,
222
+ ): ResourceCandidate<PromptTemplate> {
223
+ diagnostics.push({ type: "warning", message: reason, path: filePath });
224
+ const sourceInfo: ResourceSourceInfo = sourceInfoForRoot(root, filePath);
225
+ return {
226
+ name,
227
+ value: {
228
+ name,
229
+ description: description ?? "Prompt template (unavailable)",
230
+ content: "",
231
+ filePath,
232
+ sourceInfo,
233
+ trusted: root.trusted !== false,
234
+ unavailable: `${reason} (${filePath})`,
235
+ },
236
+ source: sourceInfo,
237
+ };
238
+ }
239
+
142
240
  function loadPromptFile(
143
241
  filePath: string,
144
242
  root: PromptTemplateRoot,
@@ -146,25 +244,45 @@ function loadPromptFile(
146
244
  ): ResourceCandidate<PromptTemplate> | null {
147
245
  const ext = path.extname(filePath).toLowerCase();
148
246
  if (ext !== ".md") return null;
247
+
248
+ // The name comes before the read, so a file that exists but cannot be read
249
+ // still has a command the operator can invoke and be told why.
250
+ const relativePath = path.relative(root.path, filePath);
251
+ if (
252
+ relativePath === "" ||
253
+ relativePath === ".." ||
254
+ relativePath.startsWith(`..${path.sep}`) ||
255
+ path.isAbsolute(relativePath)
256
+ ) {
257
+ // The one failure that stays a drop: the path is outside the discovery
258
+ // root, so there is no name here to offer a command under.
259
+ diagnostics.push({ type: "warning", message: "prompt template escaped its discovery root", path: filePath });
260
+ return null;
261
+ }
262
+ const name = relativePath.slice(0, -ext.length).split(path.sep).join(":");
263
+ if (name.trim().length === 0) return null;
264
+
149
265
  let raw: string;
150
266
  try {
151
267
  raw = readFileSync(filePath, "utf8");
152
268
  } catch (err) {
153
269
  const reason = err instanceof Error ? err.message : String(err);
154
- diagnostics.push({ type: "warning", message: `prompt template could not be read: ${reason}`, path: filePath });
155
- return null;
270
+ return unavailableTemplate(name, filePath, root, `prompt template could not be read: ${reason}`, diagnostics);
156
271
  }
157
272
 
158
- const name = path.basename(filePath, ext);
159
- if (name.trim().length === 0) return null;
160
273
  const { frontmatter, body } = splitOptionalFrontmatter(raw, filePath, diagnostics);
161
- const description = stringField(frontmatter, "description") ?? fallbackDescription(body);
274
+ const declaredDescription = stringField(frontmatter, "description");
275
+ const resolution = resolveExtensionReferences(body, root);
276
+ if ("reason" in resolution) {
277
+ return unavailableTemplate(name, filePath, root, resolution.reason, diagnostics, declaredDescription ?? undefined);
278
+ }
279
+ const description = declaredDescription ?? fallbackDescription(resolution.body);
162
280
  const argumentHint = stringField(frontmatter, "argument-hint") ?? stringField(frontmatter, "argumentHint");
163
281
  const sourceInfo: ResourceSourceInfo = sourceInfoForRoot(root, filePath);
164
282
  const template: PromptTemplate = {
165
283
  name,
166
284
  description,
167
- content: body.trim(),
285
+ content: resolution.body.trim(),
168
286
  filePath,
169
287
  sourceInfo,
170
288
  trusted: root.trusted !== false,
@@ -178,10 +296,42 @@ function loadPromptRoot(
178
296
  diagnostics: ResourceDiagnostic[],
179
297
  ): ResourceCandidate<PromptTemplate>[] {
180
298
  const candidates: ResourceCandidate<PromptTemplate>[] = [];
181
- for (const entry of readRootEntries(root, "prompt template", diagnostics)) {
182
- if (!entry.isFile()) continue;
183
- const candidate = loadPromptFile(path.join(root.path, entry.name), root, diagnostics);
184
- if (candidate) candidates.push(candidate);
299
+ const pending = [root.path];
300
+ while (pending.length > 0) {
301
+ const directory = pending.pop();
302
+ if (!directory) continue;
303
+ let entries: import("node:fs").Dirent[];
304
+ try {
305
+ const stat = lstatSync(directory);
306
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
307
+ diagnostics.push({
308
+ type: "warning",
309
+ message: "prompt template root contains a non-directory or symbolic link",
310
+ path: directory,
311
+ });
312
+ continue;
313
+ }
314
+ entries = readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
315
+ } catch (error) {
316
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
317
+ diagnostics.push({
318
+ type: "warning",
319
+ message: `prompt template directory could not be read: ${error instanceof Error ? error.message : String(error)}`,
320
+ path: directory,
321
+ });
322
+ continue;
323
+ }
324
+ for (const entry of [...entries].reverse()) {
325
+ const filePath = path.join(directory, entry.name);
326
+ if (entry.isSymbolicLink()) {
327
+ diagnostics.push({ type: "warning", message: "prompt template symbolic link was ignored", path: filePath });
328
+ } else if (entry.isDirectory()) {
329
+ pending.push(filePath);
330
+ } else if (entry.isFile()) {
331
+ const candidate = loadPromptFile(filePath, root, diagnostics);
332
+ if (candidate) candidates.push(candidate);
333
+ }
334
+ }
185
335
  }
186
336
  return candidates;
187
337
  }
@@ -210,13 +360,18 @@ export function loadPromptTemplates(input: LoadPromptTemplatesInput = {}): Promp
210
360
  }
211
361
 
212
362
  function parsePromptCommand(input: string): { name: string; rest: string } | null {
213
- const trimmed = input.trim();
214
- if (!trimmed.startsWith("/")) return null;
215
- const withoutSlash = trimmed.slice(1);
363
+ const framed = input.trimStart();
364
+ if (!framed.startsWith("/")) return null;
365
+ const withoutSlash = framed.slice(1);
216
366
  const separator = withoutSlash.search(/\s/);
217
367
  const name = separator === -1 ? withoutSlash : withoutSlash.slice(0, separator);
218
368
  if (name.length === 0 || name.includes("/")) return null;
219
- const rest = separator === -1 ? "" : withoutSlash.slice(separator).trim();
369
+ // Leading whitespace before `/` and the first whitespace after the command
370
+ // are framing. A CRLF pair is one delimiter. Every subsequent byte belongs
371
+ // to the argument payload, including indentation, blank lines, and trailing
372
+ // whitespace, so raw `$ARGUMENTS` substitution can be exact at its boundary.
373
+ const delimiterWidth = separator !== -1 && withoutSlash.slice(separator, separator + 2) === "\r\n" ? 2 : 1;
374
+ const rest = separator === -1 ? "" : withoutSlash.slice(separator + delimiterWidth);
220
375
  return { name, rest };
221
376
  }
222
377
 
@@ -225,6 +380,18 @@ export function expandPromptTemplateInput(input: string, templates: PromptTempla
225
380
  if (!command) return { expanded: false, text: input, args: [], diagnostics: templates.diagnostics };
226
381
  const template = templates.items.find((entry) => entry.name === command.name);
227
382
  if (!template) return { expanded: false, text: input, args: [], diagnostics: templates.diagnostics };
383
+ // A load-time failure refuses before anything else, and before the trust
384
+ // check, because there is no body here to be trusted or untrusted.
385
+ if (template.unavailable !== undefined) {
386
+ const message = `prompt template /${template.name} cannot run: ${template.unavailable}`;
387
+ return {
388
+ expanded: false,
389
+ text: input,
390
+ args: [],
391
+ diagnostics: templates.diagnostics,
392
+ refusal: { template, message },
393
+ };
394
+ }
228
395
  if (!template.trusted) {
229
396
  const message = `prompt template ${template.name} comes from an untrusted project root; set skills.trustProjectCompatRoots to use it`;
230
397
  return {
@@ -238,7 +405,7 @@ export function expandPromptTemplateInput(input: string, templates: PromptTempla
238
405
  const args = parseCommandArgs(command.rest);
239
406
  return {
240
407
  expanded: true,
241
- text: substituteArgs(template.content, args),
408
+ text: substituteArgs(template.content, args, command.rest),
242
409
  args,
243
410
  template,
244
411
  diagnostics: templates.diagnostics,
@@ -31,6 +31,58 @@ function oneLine(value: string): string {
31
31
  return value.replace(/\s+/g, " ").trim();
32
32
  }
33
33
 
34
+ /** Columns one tab occupies when a multi-line surface expands it. */
35
+ const DISPLAY_TAB_WIDTH = 4;
36
+
37
+ /** Stands in for a control byte that was neutralized, so the cut is visible. */
38
+ const CONTROL_BYTE_PLACEHOLDER = "·";
39
+
40
+ export interface SanitizedDisplayText {
41
+ text: string;
42
+ /** A control byte or escape sequence was neutralized. */
43
+ neutralized: boolean;
44
+ /** A tab was expanded, so column positions are display positions. */
45
+ tabsExpanded: boolean;
46
+ }
47
+
48
+ /**
49
+ * Neutralize escape sequences and control bytes while keeping line structure.
50
+ * {@link sanitizeCallTargetText} is the one-line form for a card; this is the
51
+ * form a multi-line operator surface needs, where the newlines are the content
52
+ * rather than whitespace to collapse. Tabs become spaces because a bordered
53
+ * frame measures visible width and a literal tab makes that measurement a lie.
54
+ * Every other C0 byte and DEL becomes a visible placeholder, so a payload
55
+ * cannot style, reposition, or spoof the surface that is approving it.
56
+ */
57
+ export function sanitizeMultilineDisplayText(value: string): SanitizedDisplayText {
58
+ const escapesStripped = value.replace(OSC_PATTERN, "").replace(CSI_PATTERN, "");
59
+ let neutralized = escapesStripped.length !== value.length;
60
+ // CRLF is line structure, not an injection, so it normalizes rather than
61
+ // showing a placeholder at the end of every line of a Windows-authored file.
62
+ const stripped = escapesStripped.replace(/\r\n/g, "\n");
63
+ let tabsExpanded = false;
64
+ let out = "";
65
+ for (const ch of stripped) {
66
+ if (ch === "\n") {
67
+ out += ch;
68
+ continue;
69
+ }
70
+ if (ch === "\t") {
71
+ out += " ".repeat(DISPLAY_TAB_WIDTH);
72
+ tabsExpanded = true;
73
+ continue;
74
+ }
75
+ const code = ch.codePointAt(0) ?? 0;
76
+ if (code < 0x20 || code === 0x7f) {
77
+ out += CONTROL_BYTE_PLACEHOLDER;
78
+ neutralized = true;
79
+ continue;
80
+ }
81
+ out += ch;
82
+ }
83
+ return { text: out, neutralized, tabsExpanded };
84
+ }
85
+
34
86
  /**
35
87
  * Neutralize escape sequences and control bytes in a pre-composed target
36
88
  * string and collapse it to one line. Used where a description crosses a
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import path from "node:path";
3
3
  import { artifactDefaultPath } from "../../core/artifact-paths.js";
4
+ import { pathBoundaryCovers, resolvePathBoundary } from "../../core/path-boundary.js";
4
5
  import { canonicalizeExistingPath } from "../../core/path-canonical.js";
5
6
  import { ToolNames } from "../../core/tool-names.js";
6
7
  import { clioConfigDir } from "../../core/xdg.js";
@@ -89,7 +90,8 @@ export interface SafetyPolicyEngineOptions {
89
90
  rulePacks?: RulePacks;
90
91
  projectPolicy?: LoadedProjectSafetyPolicy;
91
92
  /**
92
- * Absolute directories a write-class tool call is confined to for this run.
93
+ * Absolute path boundaries a write-class tool call is confined to for this run.
94
+ * Exact files omit a trailing slash and subtrees retain one.
93
95
  * When present and non-empty, a write/edit target outside every root is a
94
96
  * final BLOCK (reason code "write-root"). Empty or absent disables the check.
95
97
  * Enforced at the worker safety seam so both the native worker registry and
@@ -182,9 +184,7 @@ function evaluateWriteRoots(roots: ReadonlyArray<string>, writeRootCwd: string,
182
184
  const target = writeRootTargetPath(call);
183
185
  if (target === null) return null;
184
186
  const resolved = path.resolve(writeRootCwd, target);
185
- for (const root of roots) {
186
- if (resolved === root || resolved.startsWith(`${root}${path.sep}`)) return null;
187
- }
187
+ if (pathBoundaryCovers(roots, resolved)) return null;
188
188
  return `write target '${target}' resolves to '${resolved}', which is outside the permitted write roots for this run: ${roots.join(", ")}`;
189
189
  }
190
190
 
@@ -193,7 +193,7 @@ export function createSafetyPolicyEngine(options: SafetyPolicyEngineOptions = {}
193
193
  // Write-root containment resolves lexically, so it keeps its own un-canonicalized
194
194
  // cwd and roots to compare like against like (the design mandates no symlink chasing).
195
195
  const writeRootCwd = path.resolve(options.cwd ?? process.cwd());
196
- const writeRoots = (options.writeRoots ?? []).map((root) => path.resolve(root));
196
+ const writeRoots = (options.writeRoots ?? []).map((root) => resolvePathBoundary(writeRootCwd, root));
197
197
  const packs = options.rulePacks ?? getCachedDefaultRulePacks();
198
198
  const projectPolicy = options.projectPolicy ?? loadProjectSafetyPolicy(cwd);
199
199
  const projectPolicyRoot =
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { ToolNames } from "../../core/tool-names.js";
3
+ import { classify } from "./action-classifier.js";
3
4
  import { typedValidationSummary } from "./finish-contract.js";
4
5
  import {
5
6
  detectValidationCommand,
@@ -25,6 +26,13 @@ import {
25
26
  * bash command that writes through a script) leaves the path absent from the
26
27
  * set and the validator falls back to the filesystem rather than to a failed
27
28
  * conformance.
29
+ *
30
+ * `writeRecordComplete` is the other half of that sentence, and it exists for
31
+ * the reader that cannot fall back to the filesystem. The write boundary uses
32
+ * this record the other way round, to decide that a path the run did *not*
33
+ * write is somebody else's and must survive. Reading an under-reporting set as
34
+ * a closed list would clear a step of a write it really made, so the recorder
35
+ * says outright whether the set is closed.
28
36
  */
29
37
  export interface RunEffects {
30
38
  /** Absolute paths the run's successful tool calls aimed a mutation at. */
@@ -56,6 +64,35 @@ export interface RunEffects {
56
64
  * wider set.
57
65
  */
58
66
  verificationCommands: ReadonlySet<string>;
67
+ /**
68
+ * False once the run made a successful call to a tool that can mutate a path
69
+ * its own arguments do not name. After such a call `mutatedPaths` is a lower
70
+ * bound rather than the whole list, so a reader that would treat an absent
71
+ * path as proof the run did not write it has to stop treating it that way.
72
+ *
73
+ * The downgrade covers the whole run rather than the one call, because an
74
+ * opaque call's effect is unbounded. A shell command, a project script, or a
75
+ * spawned worker can write anywhere the process can reach, so once one has
76
+ * run there is no path left in the checkout that the record can clear.
77
+ */
78
+ writeRecordComplete: boolean;
79
+ /**
80
+ * Successful opaque calls that made the write record incomplete. The call id
81
+ * and tool name are retained so a later write-boundary verdict can explain
82
+ * exactly why attribution was downgraded instead of exposing only a boolean.
83
+ */
84
+ writeRecordDowngrades: ReadonlyArray<WriteRecordDowngrade>;
85
+ }
86
+
87
+ export interface WriteRecordDowngrade {
88
+ reason: "opaque_tool_succeeded";
89
+ tool: string;
90
+ toolCallId: string;
91
+ }
92
+
93
+ export interface RunEffectsRecorderOptions {
94
+ /** Fires once, when the first successful opaque call opens the record. */
95
+ onWriteRecordDowngraded?(downgrade: WriteRecordDowngrade): void;
59
96
  }
60
97
 
61
98
  export interface RunEffectsRecorder {
@@ -67,9 +104,44 @@ export interface RunEffectsRecorder {
67
104
  }
68
105
 
69
106
  interface PendingEffects {
107
+ tool: string;
108
+ toolCallId: string;
70
109
  paths: ReadonlyArray<string>;
71
110
  validationCommand: string | null;
72
111
  verificationCommand: string | null;
112
+ opaque: boolean;
113
+ }
114
+
115
+ /**
116
+ * Action classes whose successful calls write only paths their own arguments
117
+ * name. `read` covers the observe, retrieve, interact, and orchestrate-bookkeeping
118
+ * tools, none of which touch a source-workspace file: `git` is a closed
119
+ * status/diff/log surface, and `tasks`, `ledger`, and `context` write Clio's own
120
+ * state. `write` covers `write`, `edit`, and `artifact`, whose target
121
+ * `toolMutationPaths` resolves from the call itself, including the artifact
122
+ * tool's default path.
123
+ */
124
+ const ENUMERABLE_ACTION_CLASSES: ReadonlySet<string> = new Set(["read", "write"]);
125
+
126
+ /**
127
+ * Whether a successful call to this tool can mutate a path its own recorded
128
+ * arguments do not name.
129
+ *
130
+ * Asked of the classifier rather than of a hand-written list, because
131
+ * `src/tools/policy.ts` asserts at bootstrap that every registered tool's
132
+ * `baseActionClass` equals what this classifier returns and what the plane
133
+ * table declares. A tool added to the execute or orchestrate-dispatch plane is
134
+ * therefore opaque the day it is registered, with no list to remember to update.
135
+ *
136
+ * The classifier is asked about the tool name alone. Bash's argument scan can
137
+ * escalate a single call to `system_modify` or `git_destructive`, and those are
138
+ * still bash; the question here is what the tool can do, not what one call was
139
+ * caught doing. An unregistered name (a dynamic or MCP tool) classifies as
140
+ * `unknown` and is opaque, which is the fail-closed answer for a tool whose
141
+ * arguments this process has no schema for.
142
+ */
143
+ export function toolWritesOpaquely(toolName: string): boolean {
144
+ return !ENUMERABLE_ACTION_CLASSES.has(classify({ tool: toolName }).actionClass);
73
145
  }
74
146
 
75
147
  /**
@@ -151,24 +223,31 @@ function validationCommandOf(
151
223
  * when its result comes back clean: a blocked write and a red command are not
152
224
  * evidence that the workspace changed or that a check passed.
153
225
  */
154
- export function createRunEffectsRecorder(cwd: string): RunEffectsRecorder {
226
+ export function createRunEffectsRecorder(cwd: string, options: RunEffectsRecorderOptions = {}): RunEffectsRecorder {
155
227
  const pending = new Map<string, PendingEffects>();
156
228
  const mutatedPaths = new Set<string>();
157
229
  const failedMutationPaths = new Set<string>();
158
230
  const validationCommands = new Set<string>();
159
231
  const verificationCommands = new Set<string>();
232
+ const writeRecordDowngrades = new Map<string, WriteRecordDowngrade>();
160
233
  return {
161
234
  start(toolCallId, toolName, args) {
162
235
  const paths = mutationTargets(toolName, args);
163
236
  const validationCommand = validationCommandOf(toolName, args, "finish-contract");
164
237
  const verificationCommand = validationCommandOf(toolName, args, "grounding");
165
- if (paths.length === 0 && validationCommand === null && verificationCommand === null) return;
238
+ const opaque = toolWritesOpaquely(toolName);
239
+ // An opaque call is tracked even when it exposed no path and no
240
+ // command, because the fact that it ran at all is the finding.
241
+ if (!opaque && paths.length === 0 && validationCommand === null && verificationCommand === null) return;
166
242
  // A bash call may re-base its relative paths with its own cwd argument.
167
243
  const base = typeof args?.cwd === "string" && args.cwd.length > 0 ? path.resolve(cwd, args.cwd) : cwd;
168
244
  pending.set(toolCallId, {
245
+ tool: toolName,
246
+ toolCallId,
169
247
  paths: paths.map((target) => path.resolve(base, target)),
170
248
  validationCommand,
171
249
  verificationCommand,
250
+ opaque,
172
251
  });
173
252
  },
174
253
  finish(toolCallId, failed) {
@@ -179,6 +258,19 @@ export function createRunEffectsRecorder(cwd: string): RunEffectsRecorder {
179
258
  for (const target of effects.paths) failedMutationPaths.add(target);
180
259
  return;
181
260
  }
261
+ // Success-gated on purpose. A shell command the safety policy blocked,
262
+ // or one that came back an error, never reached the filesystem, so it
263
+ // leaves the record closed.
264
+ if (effects.opaque) {
265
+ const downgrade: WriteRecordDowngrade = {
266
+ reason: "opaque_tool_succeeded",
267
+ tool: effects.tool,
268
+ toolCallId: effects.toolCallId,
269
+ };
270
+ const recordWasComplete = writeRecordDowngrades.size === 0;
271
+ writeRecordDowngrades.set(effects.toolCallId, downgrade);
272
+ if (recordWasComplete) options.onWriteRecordDowngraded?.({ ...downgrade });
273
+ }
182
274
  for (const target of effects.paths) mutatedPaths.add(target);
183
275
  if (effects.validationCommand !== null) validationCommands.add(effects.validationCommand);
184
276
  if (effects.verificationCommand !== null) verificationCommands.add(effects.verificationCommand);
@@ -191,6 +283,8 @@ export function createRunEffectsRecorder(cwd: string): RunEffectsRecorder {
191
283
  failedMutationPaths: new Set([...failedMutationPaths].filter((target) => !mutatedPaths.has(target))),
192
284
  validationCommands: new Set(validationCommands),
193
285
  verificationCommands: new Set(verificationCommands),
286
+ writeRecordComplete: writeRecordDowngrades.size === 0,
287
+ writeRecordDowngrades: [...writeRecordDowngrades.values()].map((entry) => ({ ...entry })),
194
288
  };
195
289
  },
196
290
  };
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { asDirectoryPathBoundary, pathBoundaryEntryCovers, resolvePathBoundary } from "../../core/path-boundary.js";
2
3
  import type { ActionClass } from "./action-classifier.js";
3
4
 
4
5
  /**
@@ -15,15 +16,7 @@ export interface ScopeSpec {
15
16
  }
16
17
 
17
18
  function normalizeRoot(root: string): string {
18
- return path.resolve(root);
19
- }
20
-
21
- function isUnder(child: string, parent: string): boolean {
22
- const c = normalizeRoot(child);
23
- const p = normalizeRoot(parent);
24
- if (c === p) return true;
25
- const rel = path.relative(p, c);
26
- return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
19
+ return resolvePathBoundary(process.cwd(), root);
27
20
  }
28
21
 
29
22
  export function isSubset(worker: ScopeSpec, orchestrator: ScopeSpec): boolean {
@@ -31,7 +24,9 @@ export function isSubset(worker: ScopeSpec, orchestrator: ScopeSpec): boolean {
31
24
  if (!orchestrator.allowedActions.has(action)) return false;
32
25
  }
33
26
  for (const root of worker.allowedWriteRoots) {
34
- const covered = orchestrator.allowedWriteRoots.some((outer) => isUnder(root, outer));
27
+ const covered = orchestrator.allowedWriteRoots.some((outer) =>
28
+ pathBoundaryEntryCovers(normalizeRoot(outer), normalizeRoot(root)),
29
+ );
35
30
  if (!covered) return false;
36
31
  }
37
32
  if (worker.allowNetwork && !orchestrator.allowNetwork) return false;
@@ -48,14 +43,14 @@ export const READONLY_SCOPE: ScopeSpec = {
48
43
 
49
44
  export const WORKSPACE_SCOPE: ScopeSpec = {
50
45
  allowedActions: new Set<ActionClass>(["read", "write", "execute", "dispatch"]),
51
- allowedWriteRoots: [process.cwd()],
46
+ allowedWriteRoots: [asDirectoryPathBoundary(path.resolve(process.cwd()))],
52
47
  allowNetwork: true,
53
48
  allowDispatch: true,
54
49
  };
55
50
 
56
51
  export const CONFIRMED_SCOPE: ScopeSpec = {
57
52
  allowedActions: new Set<ActionClass>(["read", "write", "execute", "dispatch", "system_modify"]),
58
- allowedWriteRoots: [process.cwd()],
53
+ allowedWriteRoots: [asDirectoryPathBoundary(path.resolve(process.cwd()))],
59
54
  allowNetwork: true,
60
55
  allowDispatch: true,
61
56
  };
@@ -72,6 +72,19 @@ export interface ContextSnapshot {
72
72
  splits: Record<string, "estimated" | "exact" | "reconciled">;
73
73
  };
74
74
 
75
+ /**
76
+ * Divergence record. `estimatedTokens` is the chars/4 prompt-side total this
77
+ * snapshot was captured with and is never rewritten by a reconcile, so the
78
+ * two figures stay comparable. `reconciledTokens` is the provider's own
79
+ * prompt token count for the call this snapshot was reconciled against
80
+ * (cached prompt tokens folded back in), and `divergenceRatio` is
81
+ * `reconciledTokens / estimatedTokens`. A ratio above 1 is the estimator
82
+ * under-counting, which is what issue #227 recorded as a provider overflow.
83
+ */
84
+ estimatedTokens?: number | undefined;
85
+ reconciledTokens?: number | undefined;
86
+ divergenceRatio?: number | undefined;
87
+
75
88
  promptHash?: string | undefined;
76
89
  toolSignature?: string | undefined;
77
90
  messageRange?: { start: number; end: number } | undefined;
@@ -332,6 +345,31 @@ export function getLatestContextSnapshot(meta: SessionMeta): ContextSnapshot | n
332
345
  return list.at(-1) ?? null;
333
346
  }
334
347
 
348
+ /**
349
+ * The window a previous process measured this session's prompts against, when
350
+ * the backend itself reported it as the one it had open (issue #227).
351
+ *
352
+ * A resumed session re-resolves its target before discovery has reported a
353
+ * loaded window, so the first turn budgets against the probed server-wide
354
+ * figure, which on a multi-slot backend is a multiple of what the model is
355
+ * actually serving. The snapshot ledger already recorded the number the
356
+ * backend named, so the resume carries it forward instead of re-probing.
357
+ * Scoped to the same target and model: a different selection has no claim on
358
+ * that measurement, and a live loaded window always outranks this one, so a
359
+ * model reloaded at a new size corrects on the next resolution.
360
+ */
361
+ export function lastLoadedContextWindow(meta: SessionMeta, providerId: string, modelId: string): number | null {
362
+ const snapshots = getContextSnapshots(meta);
363
+ for (let i = snapshots.length - 1; i >= 0; i -= 1) {
364
+ const snapshot = snapshots[i];
365
+ if (snapshot?.contextWindowSource !== "loaded") continue;
366
+ if (snapshot.providerId !== providerId || snapshot.modelId !== modelId) continue;
367
+ const window = snapshot.effectiveContextWindow;
368
+ if (typeof window === "number" && Number.isFinite(window) && window > 0) return window;
369
+ }
370
+ return null;
371
+ }
372
+
335
373
  export function buildSnapshotCategories(inputs: {
336
374
  systemPrompt?: string | undefined;
337
375
  promptSegments?: ReadonlyArray<{ id: string; tokenEstimate: number }> | undefined;
@@ -456,6 +494,7 @@ export function captureContextSnapshot(input: CaptureContextSnapshotInput): Cont
456
494
  compactionThreshold: input.compactionThreshold,
457
495
  });
458
496
  return {
497
+ estimatedTokens: sumSplitTokens(categories),
459
498
  snapshotId: nextSnapshotId(),
460
499
  sessionId: input.sessionId,
461
500
  turnId: input.turnId,
@@ -480,9 +519,13 @@ export function captureContextSnapshot(input: CaptureContextSnapshotInput): Cont
480
519
  };
481
520
  }
482
521
 
522
+ function sumSplitTokens(categories: ContextSnapshot["categories"]): number {
523
+ return SNAPSHOT_SPLIT_KEYS.reduce((sum, key) => sum + (categories[key] ?? 0), 0);
524
+ }
525
+
483
526
  /** Sum of the prompt-side categories (everything except reserve/free/streaming). */
484
527
  export function snapshotInputTokens(snapshot: ContextSnapshot): number {
485
- return SNAPSHOT_SPLIT_KEYS.reduce((sum, key) => sum + (snapshot.categories[key] ?? 0), 0);
528
+ return sumSplitTokens(snapshot.categories);
486
529
  }
487
530
 
488
531
  export function reconcileSnapshot(snapshot: ContextSnapshot, usage: Usage): ContextSnapshot {
@@ -572,9 +615,17 @@ export function reconcileSnapshot(snapshot: ContextSnapshot, usage: Usage): Cont
572
615
  {} as Record<string, "estimated" | "exact" | "reconciled">,
573
616
  );
574
617
 
618
+ // The estimate this snapshot was captured with, kept whole across repeated
619
+ // reconciles so the recorded divergence always compares the provider count
620
+ // against chars/4 rather than against a previously normalized total.
621
+ const estimatedTokens = snapshot.estimatedTokens ?? sumSplitTokens(snapshot.categories);
622
+
575
623
  return {
576
624
  ...snapshot,
577
625
  categories: updatedCategories,
626
+ estimatedTokens,
627
+ reconciledTokens: exactInput,
628
+ divergenceRatio: estimatedTokens > 0 ? Math.round((exactInput / estimatedTokens) * 1000) / 1000 : undefined,
578
629
  sources: {
579
630
  total: "reconciled",
580
631
  splits: splitsSources,