@iowarp/clio-coder 0.3.8 → 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 (333) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +7 -3
  3. package/dist/{acp-U67UHUK2.js → acp-7LOELQFP.js} +6 -6
  4. package/dist/{agents-YU6SGALZ.js → agents-FIBG2SHA.js} +27 -25
  5. package/dist/assets/codewiki.json +1 -1
  6. package/dist/{auth-5ZPJOIVG.js → auth-OI4LIH2I.js} +11 -12
  7. package/dist/{builtins-C6JMZVV6.js → builtins-AD25UL3C.js} +5 -5
  8. package/dist/{chunk-5FR74PWO.js → chunk-2JDWVJND.js} +2 -2
  9. package/dist/chunk-3DPEIQKN.js +113 -0
  10. package/dist/{chunk-4SPRNWDE.js → chunk-3DUR4WUA.js} +15 -15
  11. package/dist/{chunk-VHN4MY6O.js → chunk-3MRC2YSQ.js} +2 -2
  12. package/dist/{chunk-5DHKRSMQ.js → chunk-3UUY7R3Z.js} +11 -7
  13. package/dist/{chunk-IGWKHNIQ.js → chunk-3V5AYSEQ.js} +8 -8
  14. package/dist/{chunk-FHJEP5SW.js → chunk-465CC7FK.js} +8 -5
  15. package/dist/{chunk-5WIGXA4T.js → chunk-47CMYGET.js} +111 -4
  16. package/dist/{chunk-A3WNZD3P.js → chunk-4H6ULJ3H.js} +67 -21
  17. package/dist/{chunk-TB5666IT.js → chunk-4LJX2PUC.js} +3 -3
  18. package/dist/{chunk-XWSF374K.js → chunk-56KB5IJP.js} +2 -2
  19. package/dist/{chunk-SPULKLCF.js → chunk-5DQRIYDZ.js} +2 -2
  20. package/dist/{chunk-2HEJ2F35.js → chunk-5HFBWUMU.js} +20 -8
  21. package/dist/{chunk-WNIJTQQK.js → chunk-5PVQ4SRS.js} +78 -6
  22. package/dist/{chunk-DYIM5TJT.js → chunk-5QKCQQ3E.js} +262 -6
  23. package/dist/{chunk-TYPGUK6W.js → chunk-5T7RBWN2.js} +111 -5
  24. package/dist/{chunk-IIZWH4XA.js → chunk-774ILSRL.js} +2 -2
  25. package/dist/chunk-7C6RYZGQ.js +391 -0
  26. package/dist/{chunk-TANS5ZJS.js → chunk-AD7Y7STJ.js} +3 -3
  27. package/dist/{chunk-RWSI4YD7.js → chunk-AEYBF3TB.js} +33 -12
  28. package/dist/{chunk-DGSYXYMX.js → chunk-AMKHQW3C.js} +2 -2
  29. package/dist/{chunk-VWZOAB7K.js → chunk-B5XRQOLB.js} +7 -7
  30. package/dist/{chunk-WXY7KU3G.js → chunk-BVDVID7E.js} +2 -2
  31. package/dist/{chunk-PMDBGQSJ.js → chunk-CA42X6KT.js} +2 -2
  32. package/dist/{chunk-HLE42MG7.js → chunk-D73KXYPF.js} +3 -3
  33. package/dist/{chunk-5Q2VVUKB.js → chunk-DG4M6ZUE.js} +3 -3
  34. package/dist/{chunk-ME6CCNFO.js → chunk-EBOC7MT3.js} +6 -6
  35. package/dist/{chunk-MXKJU4JB.js → chunk-ECUO3KDP.js} +48 -7
  36. package/dist/{chunk-26LEYJZH.js → chunk-FALJGAWU.js} +2 -2
  37. package/dist/{chunk-GWS3VEIW.js → chunk-FWDFM5ZU.js} +24 -3
  38. package/dist/{chunk-7RGZWPB6.js → chunk-GAYUJ7LE.js} +67 -13
  39. package/dist/{chunk-VCBR6CU7.js → chunk-HAY4ZE2P.js} +2 -2
  40. package/dist/{chunk-FBVTI2TJ.js → chunk-HCBCAYZU.js} +11 -130
  41. package/dist/{chunk-WSB3FPX7.js → chunk-HJB5IUKP.js} +32 -136
  42. package/dist/{chunk-J3YUBZWY.js → chunk-HKO36JWF.js} +33 -5
  43. package/dist/{chunk-E77JEWSD.js → chunk-HPCTNZM2.js} +6 -36
  44. package/dist/{chunk-FJ3H4MN5.js → chunk-IHKBWSXF.js} +2 -2
  45. package/dist/{chunk-NMPKI6XL.js → chunk-JEQQR47K.js} +37 -18
  46. package/dist/{chunk-3BINW3FP.js → chunk-KV2AOLDF.js} +24 -4
  47. package/dist/{chunk-YS5VLNH5.js → chunk-LXPJXFM5.js} +7 -7
  48. package/dist/{chunk-7RFXX52T.js → chunk-MIX5N5AC.js} +271 -41
  49. package/dist/{chunk-K4XHGFR5.js → chunk-MLOK6ZOS.js} +1297 -218
  50. package/dist/{chunk-ZNLWCMVZ.js → chunk-MV2VUEJC.js} +2 -2
  51. package/dist/{chunk-MVVUPGPW.js → chunk-MXHC5QYU.js} +6 -6
  52. package/dist/{chunk-5H3GB5BO.js → chunk-N3PBVRTZ.js} +4 -382
  53. package/dist/{chunk-2HFZQUHL.js → chunk-N5XKWMDW.js} +17 -7
  54. package/dist/{chunk-5C3AQNDW.js → chunk-NNNWO6F2.js} +124 -36
  55. package/dist/{chunk-GU2UIAFZ.js → chunk-NQ6UCCOD.js} +3 -3
  56. package/dist/{chunk-N22QMJKY.js → chunk-NZU6YDNV.js} +4 -4
  57. package/dist/{chunk-ZVJ5BLO2.js → chunk-O6I4CIEU.js} +151 -13
  58. package/dist/{chunk-XN3L4EYL.js → chunk-OEDBCISO.js} +2 -2
  59. package/dist/{chunk-VAWNZU7Z.js → chunk-P3JGPQFL.js} +2 -2
  60. package/dist/{chunk-IJ7RPIYJ.js → chunk-PNY46YEY.js} +20 -3
  61. package/dist/{chunk-U6MBIEMB.js → chunk-PZ4I4JE2.js} +56 -35
  62. package/dist/{chunk-GPIEI3LY.js → chunk-QQ7EKM72.js} +2 -2
  63. package/dist/{chunk-WLFILSD5.js → chunk-R7LNVMCS.js} +66 -28
  64. package/dist/{chunk-GOXNB3AO.js → chunk-RAPCMZL4.js} +75 -4
  65. package/dist/chunk-RKKLTLYB.js +45 -0
  66. package/dist/{chunk-TT36MB5S.js → chunk-RKRLDWD3.js} +3 -1
  67. package/dist/{chunk-TTHACPOM.js → chunk-S4COXYBG.js} +456 -18
  68. package/dist/{chunk-WWCZ5F23.js → chunk-T3Z6VAAF.js} +69 -10
  69. package/dist/{chunk-GN57SG4G.js → chunk-TD7UE2L5.js} +9 -7
  70. package/dist/{chunk-TLQJPP24.js → chunk-TEO2TLVN.js} +523 -322
  71. package/dist/{chunk-FCSXB6T2.js → chunk-UOSL25KY.js} +14 -2
  72. package/dist/{chunk-KTYTFRMB.js → chunk-VKBMFOYV.js} +17 -15
  73. package/dist/{chunk-PT7HYKEM.js → chunk-VO2LKSTM.js} +2 -2
  74. package/dist/{chunk-P43ETTHK.js → chunk-VPTUJU4P.js} +2 -2
  75. package/dist/{chunk-WEH5XRJQ.js → chunk-WIE7ZOSW.js} +2 -2
  76. package/dist/{chunk-EMYUUSFG.js → chunk-WXCJ7VME.js} +5 -5
  77. package/dist/{chunk-4DGYLA73.js → chunk-XDOQXGFO.js} +22 -7
  78. package/dist/{chunk-JOZYP4GM.js → chunk-YKOFT37S.js} +5 -5
  79. package/dist/chunk-YSEHGPCT.js +127 -0
  80. package/dist/{chunk-HFSBBKSQ.js → chunk-YW7UVM5V.js} +138 -3
  81. package/dist/cli/index.js +27 -27
  82. package/dist/{clio-QVTYJ57A.js → clio-LT5V7SSZ.js} +6 -6
  83. package/dist/{code-nav-FGGFIE7L.js → code-nav-LMW275PA.js} +4 -4
  84. package/dist/{config-LW5IJFQN.js → config-RXS5T3JT.js} +71 -43
  85. package/dist/{configure-7XIZCOU4.js → configure-2WYWSCSD.js} +14 -15
  86. package/dist/{context-Y6Y7QPR6.js → context-I3BTOTCS.js} +12 -12
  87. package/dist/{context-L3WL3X7K.js → context-MVOORGMF.js} +34 -33
  88. package/dist/{context-N52ZA626.js → context-PALKKQYL.js} +20 -20
  89. package/dist/{context-clear-MBQRLSDQ.js → context-clear-N2WOYZ2K.js} +34 -33
  90. package/dist/{context-index-HVMFQHK3.js → context-index-HNG3MOME.js} +2 -2
  91. package/dist/{context-working-set-GS6DSO7F.js → context-working-set-MIEVECVZ.js} +10 -11
  92. package/dist/{dispatch-runner-22ZCNOM3.js → dispatch-runner-VVA4SRRH.js} +34 -33
  93. package/dist/doctor-TWBWFK5V.js +165 -0
  94. package/dist/{eval-BEC2WHDA.js → eval-IJ5VEZDJ.js} +2016 -142
  95. package/dist/{evidence-REJUMSKM.js → evidence-L5APPXNV.js} +29 -28
  96. package/dist/{evolve-PY5ZBA5K.js → evolve-RGNKFJ52.js} +29 -28
  97. package/dist/{extensions-HVKU65YU.js → extensions-7WYWUX5A.js} +9 -3
  98. package/dist/{fleet-7WZEWRFA.js → fleet-6CNVBZZP.js} +87 -55
  99. package/dist/{fleet-commands-UVHWM76J.js → fleet-commands-L2SXSYEI.js} +6 -6
  100. package/dist/{fleet-graph-6ULH7PES.js → fleet-graph-2J3OOIPO.js} +14 -12
  101. package/dist/{fleet-preflight-J53T6CCE.js → fleet-preflight-CZRJ4JP5.js} +3 -4
  102. package/dist/{fleet-validate-72PC4SLA.js → fleet-validate-C5RI6DP7.js} +16 -15
  103. package/dist/{init-OG3TPGQG.js → init-VBN2ACVA.js} +50 -48
  104. package/dist/{library-CNTMPLRF.js → library-JHGUMLY2.js} +13 -11
  105. package/dist/{memory-6IS7F275.js → memory-K4OQIYWG.js} +31 -30
  106. package/dist/{models-ENRJDA5W.js → models-2NCZUWDD.js} +23 -22
  107. package/dist/{monitor-XLDVO7TN.js → monitor-MMVTJABD.js} +35 -34
  108. package/dist/{orchestrator-6KSPYRHA.js → orchestrator-ZKBPCHW6.js} +1627 -294
  109. package/dist/{reset-RZ4ER727.js → reset-DD5JGOY3.js} +3 -3
  110. package/dist/{run-Y2CNK5RU.js → run-QEGNX7FL.js} +56 -55
  111. package/dist/{share-A55GYP6Z.js → share-JKD3BQMW.js} +13 -11
  112. package/dist/{skills-ALC5J6AT.js → skills-LMQIKDOZ.js} +14 -12
  113. package/dist/{skills-eval-JPBEBYQU.js → skills-eval-I7X2774U.js} +34 -32
  114. package/dist/{steer-GGWFUJUD.js → steer-CF5TDANS.js} +3 -3
  115. package/dist/{support-MIETYA5E.js → support-I7LOJLIF.js} +4 -4
  116. package/dist/{targets-VGNXIR3S.js → targets-RUSR6B5Z.js} +58 -32
  117. package/dist/{terminal-lease-WOBR64YA.js → terminal-lease-QYVORFR4.js} +6 -4
  118. package/dist/{trace-PNCASAXC.js → trace-ODOQIVIW.js} +61 -6
  119. package/dist/{upgrade-FUSUAGHR.js → upgrade-XANW3FXB.js} +17 -16
  120. package/dist/{usage-N4MKVHKD.js → usage-4H7ZRXQT.js} +88 -44
  121. package/dist/{verifiers-YAWOJ3H2.js → verifiers-UZXNBZEB.js} +6 -6
  122. package/dist/{verify-LTDHYBGY.js → verify-BVKWTNDL.js} +5 -5
  123. package/dist/{wiki-generate-6M7GHTBJ.js → wiki-generate-MY7WV2QI.js} +49 -47
  124. package/dist/worker/entry.js +29 -33
  125. package/docs/alcf-provider.md +1 -1
  126. package/docs/architecture.md +1 -1
  127. package/docs/artifact-versions.md +6 -1
  128. package/docs/built-in-agents.md +1 -1
  129. package/docs/capacity-and-scheduling.md +23 -2
  130. package/docs/commands-and-modes.md +1 -1
  131. package/docs/configuration-and-targets.md +30 -3
  132. package/docs/context-engine.md +63 -4
  133. package/docs/documentation-coverage.md +3 -3
  134. package/docs/documentation-guide.md +1 -1
  135. package/docs/environment-variables.md +2 -0
  136. package/docs/eval-runner.md +262 -11
  137. package/docs/evals-internal.md +72 -2
  138. package/docs/evidence-and-memory.md +11 -10
  139. package/docs/evolution.md +1 -1
  140. package/docs/extensions-and-sharing.md +3 -1
  141. package/docs/fleet-dispatch.md +4 -4
  142. package/docs/installation-and-lifecycle.md +1 -1
  143. package/docs/middleware-and-components.md +1 -1
  144. package/docs/model-catalog.md +1 -1
  145. package/docs/observability.md +53 -2
  146. package/docs/proactive-memory.md +127 -14
  147. package/docs/prompt-envelope-and-tools.md +19 -1
  148. package/docs/provider-adapter-cookbook.md +1 -1
  149. package/docs/release-cut-checklist.md +19 -3
  150. package/docs/safety-model.md +1 -1
  151. package/docs/scientific-validation.md +1 -1
  152. package/docs/skills-marketplace.md +1 -1
  153. package/docs/tool-usage.md +1 -1
  154. package/docs/trace-store.md +1 -1
  155. package/docs/troubleshooting.md +87 -0
  156. package/docs/tui-design.md +1 -1
  157. package/docs/worker-dispatch-mechanics.md +1 -1
  158. package/package.json +2 -1
  159. package/src/cli/agents.ts +1 -1
  160. package/src/cli/config-inspect.ts +33 -6
  161. package/src/cli/config.ts +1 -1
  162. package/src/cli/doctor-state-size.ts +82 -0
  163. package/src/cli/doctor.ts +3 -1
  164. package/src/cli/eval.ts +80 -16
  165. package/src/cli/extensions.ts +5 -1
  166. package/src/cli/fleet.ts +32 -3
  167. package/src/cli/targets.ts +44 -13
  168. package/src/cli/trace.ts +63 -4
  169. package/src/cli/usage.ts +63 -14
  170. package/src/core/bus-events.ts +29 -1
  171. package/src/core/cache-telemetry.ts +42 -0
  172. package/src/core/config.ts +18 -0
  173. package/src/core/defaults.ts +36 -6
  174. package/src/core/endpoint-key.ts +27 -0
  175. package/src/core/residency-target-key.ts +25 -0
  176. package/src/core/response-schema.ts +36 -2
  177. package/src/domains/config/classify.ts +3 -0
  178. package/src/domains/context/codewiki/coordinator.ts +12 -4
  179. package/src/domains/dispatch/admission.ts +40 -3
  180. package/src/domains/dispatch/capacity-lease.ts +98 -9
  181. package/src/domains/dispatch/contract.ts +11 -0
  182. package/src/domains/dispatch/execution-plan.ts +44 -4
  183. package/src/domains/dispatch/extension.ts +166 -42
  184. package/src/domains/dispatch/fleet-run.ts +23 -3
  185. package/src/domains/dispatch/heartbeat.ts +32 -8
  186. package/src/domains/dispatch/index.ts +3 -0
  187. package/src/domains/dispatch/orphan-recovery.ts +5 -0
  188. package/src/domains/dispatch/reservation-store.ts +116 -8
  189. package/src/domains/dispatch/state.ts +4 -0
  190. package/src/domains/dispatch/worker-spawn.ts +25 -11
  191. package/src/domains/dispatch/write-boundary-enforcer.ts +20 -3
  192. package/src/domains/dispatch/write-boundary.ts +62 -1
  193. package/src/domains/eval/artifacts/store.ts +62 -0
  194. package/src/domains/eval/compare/behavioral.ts +224 -0
  195. package/src/domains/eval/compare/compare.ts +355 -2
  196. package/src/domains/eval/compare/envelope.ts +128 -0
  197. package/src/domains/eval/compare/gates.ts +24 -6
  198. package/src/domains/eval/compare/thresholds.ts +30 -3
  199. package/src/domains/eval/execution-provenance.ts +240 -0
  200. package/src/domains/eval/metrics/aggregate.ts +136 -0
  201. package/src/domains/eval/metrics/call-ledger-stream.ts +112 -0
  202. package/src/domains/eval/metrics/tracked.ts +413 -0
  203. package/src/domains/eval/provenance.ts +117 -0
  204. package/src/domains/eval/reports/comparison.ts +128 -0
  205. package/src/domains/eval/reports/junit.ts +17 -3
  206. package/src/domains/eval/reports/markdown.ts +3 -3
  207. package/src/domains/eval/reports/text.ts +14 -0
  208. package/src/domains/eval/run-compare.ts +20 -0
  209. package/src/domains/eval/runners/clio-run.ts +127 -0
  210. package/src/domains/eval/runners/external-command.ts +28 -3
  211. package/src/domains/eval/schema/adapter.ts +111 -0
  212. package/src/domains/eval/schema/artifact.ts +20 -0
  213. package/src/domains/eval/schema/behavioral-metrics.ts +204 -0
  214. package/src/domains/eval/schema/behavioral.ts +520 -0
  215. package/src/domains/eval/schema/execution-envelope.ts +194 -0
  216. package/src/domains/eval/schema/serving.ts +74 -0
  217. package/src/domains/eval/schema/suite.ts +38 -8
  218. package/src/domains/eval/schema/validate.ts +58 -3
  219. package/src/domains/eval/schema/verdict.ts +237 -0
  220. package/src/domains/eval/suites/resolve.ts +2 -0
  221. package/src/domains/eval/suites/run.ts +264 -33
  222. package/src/domains/eval/verifiers/command.ts +2 -1
  223. package/src/domains/eval/workspaces/temp-copy.ts +145 -13
  224. package/src/domains/evidence/build.ts +2 -13
  225. package/src/domains/evidence/eval.ts +2 -12
  226. package/src/domains/evidence/findings-markdown.ts +33 -0
  227. package/src/domains/evidence/run-trust.ts +7 -113
  228. package/src/domains/evidence/trust-projection.ts +2 -2
  229. package/src/domains/extensions/compatibility.ts +285 -0
  230. package/src/domains/extensions/discovery.ts +38 -3
  231. package/src/domains/extensions/resources.ts +1 -1
  232. package/src/domains/extensions/state.ts +12 -3
  233. package/src/domains/extensions/types.ts +2 -0
  234. package/src/domains/lifecycle/doctor.ts +69 -1
  235. package/src/domains/memory/index.ts +14 -0
  236. package/src/domains/memory/task-bank-promotion.ts +64 -0
  237. package/src/domains/memory/task-memory-policy.ts +77 -8
  238. package/src/domains/memory/task-memory-spend.ts +131 -0
  239. package/src/domains/memory/task-memory-status.ts +7 -0
  240. package/src/domains/memory/task-memory-telemetry.ts +2 -0
  241. package/src/domains/middleware/index.ts +1 -0
  242. package/src/domains/middleware/memory-intervention.ts +69 -5
  243. package/src/domains/middleware/memory-step-endpoint.ts +71 -0
  244. package/src/domains/observability/background-memory-usage.ts +140 -0
  245. package/src/domains/observability/cost.ts +1 -1
  246. package/src/domains/observability/index.ts +7 -0
  247. package/src/domains/observability/out-of-turn-usage.ts +51 -2
  248. package/src/domains/observability/trace-store.ts +192 -2
  249. package/src/domains/prompts/compiler.ts +100 -13
  250. package/src/domains/providers/endpoint-capacity.ts +96 -0
  251. package/src/domains/providers/index.ts +10 -0
  252. package/src/domains/providers/models/local-models/clio-local-coding-targets.yaml +243 -1
  253. package/src/domains/providers/runtime-resolution.ts +8 -1
  254. package/src/domains/providers/runtimes/common/probe-helpers.ts +31 -9
  255. package/src/domains/providers/runtimes/local-native/llamacpp-anthropic.ts +1 -1
  256. package/src/domains/providers/runtimes/local-native/llamacpp-completion.ts +1 -1
  257. package/src/domains/providers/runtimes/local-native/llamacpp-embed.ts +1 -1
  258. package/src/domains/providers/runtimes/local-native/llamacpp-rerank.ts +1 -1
  259. package/src/domains/providers/runtimes/local-native/llamacpp.ts +4 -1
  260. package/src/domains/providers/runtimes/local-native/lmstudio.ts +4 -1
  261. package/src/domains/providers/runtimes/local-native/ollama-native.ts +6 -1
  262. package/src/domains/providers/types/capability-flags.ts +2 -0
  263. package/src/domains/providers/types/target-descriptor.ts +2 -0
  264. package/src/domains/resources/prompts/loader.ts +95 -33
  265. package/src/domains/safety/call-target.ts +52 -0
  266. package/src/domains/safety/run-effects.ts +35 -4
  267. package/src/domains/session/context-accounting.ts +52 -1
  268. package/src/domains/session/context-ledger.ts +37 -13
  269. package/src/domains/session/index.ts +6 -0
  270. package/src/domains/session/prompt-cache.ts +140 -0
  271. package/src/domains/session/prompt-manifest.ts +42 -0
  272. package/src/engine/acp/adapter.ts +18 -3
  273. package/src/engine/ai.ts +35 -0
  274. package/src/engine/apis/llamacpp-residency.ts +55 -3
  275. package/src/engine/apis/lmstudio.ts +25 -5
  276. package/src/engine/apis/ollama-native.ts +2 -1
  277. package/src/engine/apis/openai-completions.ts +80 -17
  278. package/src/engine/apis/residency-lock.ts +3 -1
  279. package/src/engine/apis/residency.ts +34 -1
  280. package/src/engine/provider-payload.ts +29 -1
  281. package/src/entry/orchestrator.ts +176 -30
  282. package/src/interactive/chat-loop-messages.ts +26 -7
  283. package/src/interactive/chat-loop.ts +318 -41
  284. package/src/interactive/chat-panel.ts +62 -8
  285. package/src/interactive/clio-editor.ts +45 -8
  286. package/src/interactive/context-activity.ts +5 -1
  287. package/src/interactive/context-meter.ts +1 -1
  288. package/src/interactive/context-overlay.ts +40 -10
  289. package/src/interactive/cost-overlay.ts +64 -6
  290. package/src/interactive/dispatch-board.ts +84 -12
  291. package/src/interactive/fleet-run-preview.ts +41 -15
  292. package/src/interactive/handoff-round.ts +41 -2
  293. package/src/interactive/interactive-application.ts +24 -1
  294. package/src/interactive/interactive-input-runtime.ts +8 -0
  295. package/src/interactive/interactive-presentation.ts +4 -0
  296. package/src/interactive/interactive-shell.ts +20 -17
  297. package/src/interactive/interactive-slash-runtime.ts +27 -4
  298. package/src/interactive/memory-overlay.ts +8 -0
  299. package/src/interactive/mutation-preview.ts +295 -0
  300. package/src/interactive/overlay-general-openers.ts +16 -0
  301. package/src/interactive/overlay-key-routing.ts +38 -0
  302. package/src/interactive/overlay-lifecycle.ts +38 -5
  303. package/src/interactive/overlay-permission-lifecycle.ts +22 -2
  304. package/src/interactive/overlay-session-lifecycle.ts +73 -9
  305. package/src/interactive/overlays/ask-user.ts +91 -19
  306. package/src/interactive/overlays/help-reference.ts +4 -0
  307. package/src/interactive/overlays/prompts.ts +11 -1
  308. package/src/interactive/overlays/settings.ts +35 -1
  309. package/src/interactive/permission-hint.ts +34 -2
  310. package/src/interactive/permission-overlay.ts +159 -9
  311. package/src/interactive/prewarm.ts +197 -0
  312. package/src/interactive/render-trace.ts +162 -15
  313. package/src/interactive/renderers/tool-execution.ts +4 -0
  314. package/src/interactive/side-question.ts +58 -1
  315. package/src/interactive/status/controller.ts +11 -0
  316. package/src/interactive/status/state-machine.ts +54 -2
  317. package/src/interactive/status/types.ts +7 -0
  318. package/src/interactive/terminal-lease.ts +2 -0
  319. package/src/interactive/turn-context.ts +299 -31
  320. package/src/interactive/turn-persistence.ts +14 -4
  321. package/src/interactive/turn-prewarm.ts +364 -0
  322. package/src/interactive/turn-queues.ts +7 -4
  323. package/src/interactive/turn-runtime.ts +8 -1
  324. package/src/interactive/turn-state.ts +23 -0
  325. package/src/interactive/view/view-overlay.ts +28 -3
  326. package/src/tools/ask-user.ts +43 -2
  327. package/src/tools/dispatch-plan.ts +17 -9
  328. package/src/tools/dispatch-scout.ts +1 -1
  329. package/src/tools/registry.ts +16 -0
  330. package/dist/chunk-AOCYTWAV.js +0 -449
  331. package/dist/chunk-HWUFFB6L.js +0 -83
  332. package/dist/chunk-R346GLFC.js +0 -31
  333. package/dist/doctor-M7YEDGAE.js +0 -91
@@ -1,5 +1,6 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { withStateFileLockSync } from "../../core/state-file-lock.js";
3
+ import { endpointLabel } from "../providers/endpoint-capacity.js";
3
4
  import {
4
5
  acquireCapacityLease,
5
6
  activeCapacityDrainUnsafe,
@@ -7,6 +8,7 @@ import {
7
8
  type CapacityLimits,
8
9
  type CapacityStateFile,
9
10
  capacityStateLockPath,
11
+ describeEndpointCapacityHolders,
10
12
  readCapacityStateUnsafe,
11
13
  writeCapacityStateUnsafe,
12
14
  } from "./capacity-lease.js";
@@ -19,9 +21,10 @@ export type ReservationMemberStatus = "held" | "consumed" | "released";
19
21
  export type ReservationStatus = "active" | "released" | "rolled_back" | "expired";
20
22
 
21
23
  /**
22
- * A reservation holds three scarce things and nothing else: a global
23
- * concurrency slot, a per-node slot, and a budget upper bound. Node identity
24
- * matters only because slots are per-node. Agent, target, model, and runtime
24
+ * A reservation holds four scarce things and nothing else: a global
25
+ * concurrency slot, a per-node slot, an inference-endpoint slot, and a budget
26
+ * upper bound. Node and endpoint identity matter only because slots bind on
27
+ * those dimensions. Agent, target, model, and runtime
25
28
  * identity are route identity, which the failover envelope owns
26
29
  * (`assertRouteWithinApprovedEnvelope`); pinning them here made a
27
30
  * plan-approved dispatch unable to fail over at all.
@@ -30,6 +33,7 @@ export interface ReservationPlanTask {
30
33
  memberId: string;
31
34
  wave: number;
32
35
  nodeId: string;
36
+ endpointKey?: string;
33
37
  costUpperBoundUsd: number;
34
38
  }
35
39
 
@@ -54,17 +58,31 @@ export interface DispatchReservationRecord {
54
58
  export interface ReservationCapacitySnapshot {
55
59
  global: { active: number; limit: number };
56
60
  nodes: Readonly<Record<string, { active: number; limit: number }>>;
61
+ endpoints: Readonly<Record<string, { active: number; limit: number; leases?: number; foregroundStreams?: number }>>;
57
62
  budget: { currentUsd: number; ceilingUsd: number };
58
63
  }
59
64
 
60
65
  export interface ReservationAllocation {
61
66
  globalSlots: number;
62
67
  nodeSlots: Readonly<Record<string, number>>;
68
+ endpointSlots: Readonly<Record<string, number>>;
63
69
  budgetUsd: number;
64
70
  }
65
71
 
66
72
  export type ReservationPlanResult = { ok: true; allocation: ReservationAllocation } | { ok: false; reason: string };
67
73
 
74
+ function endpointCapacityDenial(input: {
75
+ endpointKey: string;
76
+ limit: number;
77
+ used: number;
78
+ requested: number;
79
+ leases: number;
80
+ reservations: number;
81
+ foregroundStreams: number;
82
+ }): string {
83
+ return `dispatch: admission denied: endpoint '${endpointLabel(input.endpointKey)}' capacity exceeded (${input.used + input.requested}/${input.limit} slots): ${describeEndpointCapacityHolders(input)}; reduce the same-wave worker count, collect in-flight runs, or point workers at a second server`;
84
+ }
85
+
68
86
  function copyRecord(record: DispatchReservationRecord): DispatchReservationRecord {
69
87
  return { ...record, members: record.members.map((member) => ({ ...member })) };
70
88
  }
@@ -96,6 +114,7 @@ function validRecord(value: unknown): value is DispatchReservationRecord {
96
114
  typeof member.memberId === "string" &&
97
115
  Number.isInteger(member.wave) &&
98
116
  typeof member.nodeId === "string" &&
117
+ (member.endpointKey === undefined || typeof member.endpointKey === "string") &&
99
118
  typeof member.costUpperBoundUsd === "number" &&
100
119
  Number.isFinite(member.costUpperBoundUsd) &&
101
120
  member.costUpperBoundUsd >= 0 &&
@@ -145,9 +164,13 @@ function reservationAllocation(tasks: ReadonlyArray<ReservationPlanTask>): Reser
145
164
  ),
146
165
  );
147
166
  const nodeSlots = peakBy(tasks.map((task) => ({ wave: task.wave, key: task.nodeId })));
167
+ const endpointSlots = peakBy(
168
+ tasks.flatMap((task) => (task.endpointKey === undefined ? [] : [{ wave: task.wave, key: task.endpointKey }])),
169
+ );
148
170
  return {
149
171
  globalSlots,
150
172
  nodeSlots,
173
+ endpointSlots,
151
174
  budgetUsd: tasks.reduce((sum, task) => sum + task.costUpperBoundUsd, 0),
152
175
  };
153
176
  }
@@ -191,6 +214,26 @@ function allocateReservation(
191
214
  return { ok: false, reason: `node '${nodeId}' capacity exceeded (${used + slots}/${node.limit})` };
192
215
  }
193
216
  }
217
+ for (const [endpointKey, slots] of Object.entries(requested.endpointSlots)) {
218
+ const endpoint = capacity.endpoints[endpointKey];
219
+ if (!endpoint || !Number.isFinite(endpoint.limit)) continue;
220
+ const heldSlots = held.reduce((sum, allocation) => sum + (allocation.endpointSlots[endpointKey] ?? 0), 0);
221
+ const used = endpoint.active + heldSlots;
222
+ if (used + slots > endpoint.limit) {
223
+ return {
224
+ ok: false,
225
+ reason: endpointCapacityDenial({
226
+ endpointKey,
227
+ limit: endpoint.limit,
228
+ used,
229
+ requested: slots,
230
+ leases: endpoint.leases ?? endpoint.active - (endpoint.foregroundStreams ?? 0),
231
+ reservations: heldSlots,
232
+ foregroundStreams: endpoint.foregroundStreams ?? 0,
233
+ }),
234
+ };
235
+ }
236
+ }
194
237
  const existingBudget = activeRecords.reduce((sum, record) => sum + outstandingBudget(record), 0);
195
238
  const projected = capacity.budget.currentUsd + existingBudget + requested.budgetUsd;
196
239
  if (projected >= capacity.budget.ceilingUsd) {
@@ -225,7 +268,9 @@ export function createDispatchReservation(input: {
225
268
  if (!planned.ok) {
226
269
  replaceReservations(state, records);
227
270
  writeCapacityStateUnsafe(state);
228
- throw new Error(`dispatch: reservation denied: ${planned.reason}`);
271
+ throw new Error(
272
+ planned.reason.startsWith("dispatch:") ? planned.reason : `dispatch: reservation denied: ${planned.reason}`,
273
+ );
229
274
  }
230
275
  const createdAt = new Date(nowMs).toISOString();
231
276
  const record: DispatchReservationRecord = {
@@ -250,6 +295,7 @@ export function transferDispatchReservationToLease(input: {
250
295
  memberId: string;
251
296
  assignmentId: string;
252
297
  nodeId: string;
298
+ endpointKey?: string;
253
299
  limits: CapacityLimits;
254
300
  nowMs?: number;
255
301
  ttlMs?: number;
@@ -258,6 +304,7 @@ export function transferDispatchReservationToLease(input: {
258
304
  return acquireCapacityLease({
259
305
  assignmentId: input.assignmentId,
260
306
  nodeId: input.nodeId,
307
+ ...(input.endpointKey !== undefined ? { endpointKey: input.endpointKey } : {}),
261
308
  limits: input.limits,
262
309
  nowMs,
263
310
  ...(input.ttlMs !== undefined ? { ttlMs: input.ttlMs } : {}),
@@ -290,6 +337,7 @@ export function rebindDispatchReservationMember(input: {
290
337
  ownerId: string;
291
338
  memberId: string;
292
339
  nodeId: string;
340
+ endpointKey?: string;
293
341
  costUpperBoundUsd: number;
294
342
  capacity: ReservationCapacitySnapshot;
295
343
  nowMs?: number;
@@ -304,11 +352,42 @@ export function rebindDispatchReservationMember(input: {
304
352
  if (member.status === "released") {
305
353
  throw new Error(`dispatch: reservation member '${input.memberId}' was already released`);
306
354
  }
307
- if (member.nodeId === input.nodeId && member.costUpperBoundUsd === input.costUpperBoundUsd) {
355
+ if (
356
+ member.nodeId === input.nodeId &&
357
+ member.endpointKey === input.endpointKey &&
358
+ member.costUpperBoundUsd === input.costUpperBoundUsd
359
+ ) {
308
360
  return copyRecord(record);
309
361
  }
310
-
311
362
  const others = records.filter((entry) => entry.status === "active" && entry.ownerId !== input.ownerId);
363
+ if (member.endpointKey !== input.endpointKey && input.endpointKey !== undefined) {
364
+ const endpointKey = input.endpointKey;
365
+ const endpoint = input.capacity.endpoints[endpointKey];
366
+ if (endpoint !== undefined && Number.isFinite(endpoint.limit)) {
367
+ const siblingSlots = record.members.filter(
368
+ (entry) => entry !== member && entry.status !== "released" && entry.endpointKey === endpointKey,
369
+ ).length;
370
+ const otherHeld = others.reduce(
371
+ (sum, entry) => sum + (reservationAllocation(outstandingTasks(entry)).endpointSlots[endpointKey] ?? 0),
372
+ 0,
373
+ );
374
+ const used = endpoint.active + otherHeld + siblingSlots;
375
+ if (used + 1 > endpoint.limit) {
376
+ throw new Error(
377
+ endpointCapacityDenial({
378
+ endpointKey,
379
+ limit: endpoint.limit,
380
+ used,
381
+ requested: 1,
382
+ leases: endpoint.leases ?? endpoint.active - (endpoint.foregroundStreams ?? 0),
383
+ reservations: otherHeld + siblingSlots,
384
+ foregroundStreams: endpoint.foregroundStreams ?? 0,
385
+ }),
386
+ );
387
+ }
388
+ }
389
+ }
390
+
312
391
  if (member.nodeId !== input.nodeId) {
313
392
  const node = input.capacity.nodes[input.nodeId];
314
393
  if (node !== undefined && Number.isFinite(node.limit)) {
@@ -342,6 +421,8 @@ export function rebindDispatchReservationMember(input: {
342
421
  }
343
422
 
344
423
  member.nodeId = input.nodeId;
424
+ if (input.endpointKey === undefined) delete member.endpointKey;
425
+ else member.endpointKey = input.endpointKey;
345
426
  member.costUpperBoundUsd = Math.max(0, input.costUpperBoundUsd);
346
427
  writeStore(records);
347
428
  return copyRecord(record);
@@ -370,6 +451,26 @@ export function releaseDispatchReservationMember(
370
451
  });
371
452
  }
372
453
 
454
+ /** Settle every member after the scheduler has finished the reserved plan. */
455
+ export function releaseDispatchReservation(ownerId: string, nowMs = Date.now()): DispatchReservationRecord | null {
456
+ return withStateFileLockSync(capacityStateLockPath(), () => {
457
+ const records = readStore();
458
+ const record = records.find((entry) => entry.ownerId === ownerId);
459
+ if (!record) return null;
460
+ if (record.status !== "active") return copyRecord(record);
461
+ const settledAt = new Date(nowMs).toISOString();
462
+ for (const member of record.members) {
463
+ if (member.status === "released") continue;
464
+ member.status = "released";
465
+ member.releasedAt = settledAt;
466
+ }
467
+ record.status = "released";
468
+ record.settledAt = settledAt;
469
+ writeStore(records);
470
+ return copyRecord(record);
471
+ });
472
+ }
473
+
373
474
  export function rollbackDispatchReservation(ownerId: string, nowMs = Date.now()): DispatchReservationRecord | null {
374
475
  return withStateFileLockSync(capacityStateLockPath(), () => {
375
476
  const records = readStore();
@@ -488,14 +589,21 @@ export function reservedBudgetUsd(): number {
488
589
  .reduce((sum, record) => sum + outstandingBudget(record), 0);
489
590
  }
490
591
 
491
- export function reservedCapacity(): { globalSlots: number; nodeSlots: Readonly<Record<string, number>> } {
592
+ export function reservedCapacity(): {
593
+ globalSlots: number;
594
+ nodeSlots: Readonly<Record<string, number>>;
595
+ endpointSlots: Readonly<Record<string, number>>;
596
+ } {
492
597
  let globalSlots = 0;
493
598
  const nodeSlots: Record<string, number> = {};
599
+ const endpointSlots: Record<string, number> = {};
494
600
  for (const record of readStore().filter((entry) => entry.status === "active")) {
495
601
  const allocation = reservationAllocation(outstandingTasks(record));
496
602
  globalSlots += allocation.globalSlots;
497
603
  for (const [nodeId, count] of Object.entries(allocation.nodeSlots))
498
604
  nodeSlots[nodeId] = (nodeSlots[nodeId] ?? 0) + count;
605
+ for (const [endpointKey, count] of Object.entries(allocation.endpointSlots))
606
+ endpointSlots[endpointKey] = (endpointSlots[endpointKey] ?? 0) + count;
499
607
  }
500
- return { globalSlots, nodeSlots };
608
+ return { globalSlots, nodeSlots, endpointSlots };
501
609
  }
@@ -107,6 +107,10 @@ export async function writeFleetRun(record: FleetRunRecord): Promise<void> {
107
107
  await atomicWrite(fleetRunPath(record.id), `${JSON.stringify(record, null, 2)}\n`);
108
108
  }
109
109
 
110
+ // `RunEnvelope.heartbeatAt` deliberately persists only the absolute wall-clock
111
+ // anchor used for operator display and crash recovery. The monotonic heartbeat
112
+ // stamp belongs to the live transport in this process; serializing it would
113
+ // give a restarted process a number from an origin it cannot share.
110
114
  function readRuns(): RunEnvelope[] {
111
115
  const path = runsPath();
112
116
  if (!existsSync(path)) return [];
@@ -26,10 +26,12 @@
26
26
 
27
27
  import { type ChildProcess, spawn } from "node:child_process";
28
28
  import { join } from "node:path";
29
+ import { performance } from "node:perf_hooks";
29
30
  import { withClioAgentEnvironment } from "../../core/agent-environment.js";
30
31
  import { enableClioCompileCache, workerCompileCacheEnvironment } from "../../core/compile-cache.js";
31
32
  import { resolvePackageRoot } from "../../core/package-root.js";
32
33
  import type { WorkerSpec } from "../../worker/spec-contract.js";
34
+ import type { HeartbeatStamp } from "./heartbeat.js";
33
35
  import {
34
36
  type AgentLedgerBody,
35
37
  type ApprovedWorkerIdentity,
@@ -71,7 +73,7 @@ export interface SpawnedWorker {
71
73
  promise: Promise<SpawnedWorkerResult>;
72
74
  events: AsyncIterableIterator<unknown>;
73
75
  abort(): void;
74
- heartbeatAt: { current: number };
76
+ heartbeatAt: HeartbeatStamp;
75
77
  /**
76
78
  * Write one JSON line to the worker's open stdin (the same line protocol
77
79
  * that carried the spec). Returns false when the worker has exited, its
@@ -134,15 +136,14 @@ export interface WorkerProcessOptions {
134
136
  /** How long bulk output may precede the announce before the peer is refused. */
135
137
  attestationGraceMs?: number;
136
138
  /**
137
- * Clock behind the heartbeat stamp, defaulting to `Date.now`. The stamp stays
138
- * a wall-clock instant in production because the ledger, the receipt, and the
139
- * evidence record all serialize it as ISO-8601 for an operator to read. That
140
- * leaves a test with no honest way to assert the stamp advanced: comparing
141
- * two wall-clock reads taken either side of an interval orders them backward
142
- * on any host that resyncs its clock in between. A test injects the steppable
143
- * clock from tests/harness/clock.ts here and asserts on that instead.
139
+ * Wall clock behind the heartbeat anchor, defaulting to `Date.now`. The
140
+ * anchor stays a wall-clock instant because the ledger and evidence record
141
+ * serialize the derived heartbeat as ISO-8601 for an operator to read. This
142
+ * clock is read once; later heartbeat instants add a monotonic span to it.
144
143
  */
145
144
  now?: () => number;
145
+ /** Monotonic clock used for heartbeat spans, defaulting to `performance.now`. */
146
+ monotonicNow?: () => number;
146
147
  }
147
148
 
148
149
  /**
@@ -192,6 +193,7 @@ export function spawnWorkerProcess(
192
193
  const shutdownGraceMs = opts?.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS;
193
194
  const attestationGraceMs = opts?.attestationGraceMs ?? DEFAULT_ATTESTATION_GRACE_MS;
194
195
  const now = opts?.now ?? Date.now;
196
+ const monotonicNow = opts?.monotonicNow ?? (() => performance.now());
195
197
  const approved = opts?.approvedIdentity ?? approvedIdentityForSpec(spec);
196
198
 
197
199
  const child: ChildProcess = spawn(command, [...args], {
@@ -204,7 +206,19 @@ export function spawnWorkerProcess(
204
206
  });
205
207
  const pid = child.pid ?? null;
206
208
 
207
- const heartbeatAt = { current: now() };
209
+ // One wall anchor plus a monotonic span keeps the persisted/display instant
210
+ // readable without ever asking a later wall-clock read how much time passed.
211
+ const heartbeatWallAnchor = now();
212
+ const heartbeatMonotonicAnchor = monotonicNow();
213
+ const heartbeatAt: HeartbeatStamp = {
214
+ current: heartbeatWallAnchor,
215
+ monotonic: heartbeatMonotonicAnchor,
216
+ };
217
+ const stampHeartbeat = (): void => {
218
+ const stamp = monotonicNow();
219
+ heartbeatAt.monotonic = stamp;
220
+ heartbeatAt.current = heartbeatWallAnchor + (stamp - heartbeatMonotonicAnchor);
221
+ };
208
222
 
209
223
  const queue = createBoundedEventQueue();
210
224
  const waiters: Array<(r: IteratorResult<unknown>) => void> = [];
@@ -233,7 +247,7 @@ export function spawnWorkerProcess(
233
247
  }
234
248
 
235
249
  function push(value: unknown): void {
236
- heartbeatAt.current = now();
250
+ stampHeartbeat();
237
251
  const w = waiters.shift();
238
252
  if (w) {
239
253
  w({ value, done: false });
@@ -378,7 +392,7 @@ export function spawnWorkerProcess(
378
392
  if (!announceAccepted) failAttestation(`Invalid worker attestation: ${frame.reason}`);
379
393
  return;
380
394
  }
381
- heartbeatAt.current = now();
395
+ stampHeartbeat();
382
396
  if (frame.value.kind === "announce") {
383
397
  if (announceAccepted || announceFailed) return;
384
398
  const verdict = verifyWorkerAttestation(frame.value.attestation, approved);
@@ -26,6 +26,7 @@ import {
26
26
  enforceWriteBoundary,
27
27
  type WorkspaceSnapshot,
28
28
  type WriteBoundaryAttribution,
29
+ type WriteBoundaryAttributionDowngrade,
29
30
  type WriteBoundaryVerdict,
30
31
  writeWriteBoundaryVerdict,
31
32
  } from "./write-boundary.js";
@@ -45,7 +46,7 @@ export interface WriteBoundaryEnforcerInput {
45
46
  * argument names. Absent entirely means the caller offers no record at all,
46
47
  * and every change outside the declaration is blamed on the window.
47
48
  */
48
- recordedWritesFor?(stepId: string): ReadonlyArray<string> | null;
49
+ recordedWritesFor?(stepId: string): ReadonlyArray<string> | WriteBoundaryAttribution | null;
49
50
  /** Called once per closed window with the sealed verdict and where it landed. */
50
51
  onVerdict?(verdict: WriteBoundaryVerdict, path: string): void;
51
52
  }
@@ -91,16 +92,32 @@ export function createWriteBoundaryEnforcer(input: WriteBoundaryEnforcerInput):
91
92
  const recordedWritesFor = input.recordedWritesFor;
92
93
  if (recordedWritesFor === undefined) return undefined;
93
94
  const recorded = new Set<string>();
95
+ const downgrades: WriteBoundaryAttributionDowngrade[] = [];
94
96
  let complete = true;
95
97
  for (const stepId of stepIds) {
96
98
  const observed = recordedWritesFor(stepId);
97
99
  if (observed === null) {
98
100
  complete = false;
101
+ downgrades.push({
102
+ reason: "write_record_unavailable",
103
+ tool: null,
104
+ toolCallId: null,
105
+ runId: null,
106
+ stepId,
107
+ });
99
108
  continue;
100
109
  }
101
- for (const target of observed) recorded.add(target);
110
+ if (!("recorded" in observed)) {
111
+ for (const target of observed) recorded.add(target);
112
+ continue;
113
+ }
114
+ for (const target of observed.recorded) recorded.add(target);
115
+ if (!observed.complete) complete = false;
116
+ for (const downgrade of observed.downgrades ?? []) {
117
+ downgrades.push({ ...downgrade, stepId: downgrade.stepId ?? stepId });
118
+ }
102
119
  }
103
- return { recorded: [...recorded].sort(), complete };
120
+ return { recorded: [...recorded].sort(), complete, downgrades };
104
121
  };
105
122
  return {
106
123
  begin(window, stepIds) {
@@ -283,6 +283,49 @@ export interface WriteBoundaryAttribution {
283
283
  * change outside the allowlist, and the verdict says it did.
284
284
  */
285
285
  complete: boolean;
286
+ /** Durable causes for an incomplete record. Empty only when complete is true. */
287
+ downgrades?: ReadonlyArray<WriteBoundaryAttributionDowngrade>;
288
+ }
289
+
290
+ export type WriteBoundaryAttributionDowngradeReason =
291
+ | "opaque_tool_succeeded"
292
+ | "incomplete_tool_telemetry"
293
+ | "code_step_untracked"
294
+ | "write_record_unavailable";
295
+
296
+ /** Why one step could not offer a closed write record. */
297
+ export interface WriteBoundaryAttributionDowngrade {
298
+ reason: WriteBoundaryAttributionDowngradeReason;
299
+ /** Tool whose successful call opened the record, or null for a non-tool cause. */
300
+ tool: string | null;
301
+ /** Exact worker call when telemetry supplied one, or null for a non-tool cause. */
302
+ toolCallId: string | null;
303
+ /** Run that observed the cause, when one reached dispatch. */
304
+ runId: string | null;
305
+ /** Fleet step whose record was downgraded. */
306
+ stepId: string | null;
307
+ }
308
+
309
+ /** One operator-facing explanation shared by live and durable surfaces. */
310
+ export function describeWriteBoundaryAttributionDowngrade(downgrade: WriteBoundaryAttributionDowngrade): string {
311
+ if (downgrade.reason === "opaque_tool_succeeded") {
312
+ const tool = downgrade.tool ?? "unknown";
313
+ const call = downgrade.toolCallId === null ? "" : ` (call ${downgrade.toolCallId})`;
314
+ return `a successful '${tool}' call${call} can write paths its arguments do not name`;
315
+ }
316
+ if (downgrade.reason === "incomplete_tool_telemetry") {
317
+ return downgrade.runId === null
318
+ ? "tool telemetry was incomplete"
319
+ : `run '${downgrade.runId}' reported incomplete tool telemetry`;
320
+ }
321
+ if (downgrade.reason === "code_step_untracked") {
322
+ return downgrade.stepId === null
323
+ ? "a code step ran through the untracked command runner"
324
+ : `code step '${downgrade.stepId}' ran through the untracked command runner`;
325
+ }
326
+ return downgrade.stepId === null
327
+ ? "no usable write record was available"
328
+ : `step '${downgrade.stepId}' supplied no usable write record`;
286
329
  }
287
330
 
288
331
  export interface WriteBoundaryVerdict {
@@ -307,6 +350,8 @@ export interface WriteBoundaryVerdict {
307
350
  unattributed: ReadonlyArray<string>;
308
351
  /** Whether every step in the window offered an enumerable write record. */
309
352
  attributionComplete: boolean;
353
+ /** Why attribution was incomplete, including the causal tool call when known. */
354
+ attributionDowngrades: ReadonlyArray<WriteBoundaryAttributionDowngrade>;
310
355
  rolledBack: ReadonlyArray<WriteBoundaryRollback>;
311
356
  unrecoverable: ReadonlyArray<WriteBoundaryUnrecoverable>;
312
357
  status: WriteBoundaryStatus;
@@ -417,6 +462,19 @@ export function enforceWriteBoundary(input: EnforceWriteBoundaryInput): WriteBou
417
462
  const changes = diffWorkspace(input.snapshot);
418
463
  const outside = changes.filter((change) => !writeBoundaryCovers(allow, change.path));
419
464
  const attributionComplete = input.attribution?.complete === true;
465
+ const attributionDowngrades: ReadonlyArray<WriteBoundaryAttributionDowngrade> = attributionComplete
466
+ ? []
467
+ : input.attribution?.downgrades !== undefined && input.attribution.downgrades.length > 0
468
+ ? input.attribution.downgrades.map((entry) => ({ ...entry }))
469
+ : [
470
+ {
471
+ reason: "write_record_unavailable",
472
+ tool: null,
473
+ toolCallId: null,
474
+ runId: null,
475
+ stepId: input.stepIds.length === 1 ? (input.stepIds[0] ?? null) : null,
476
+ },
477
+ ];
420
478
  const recorded = new Set<string>();
421
479
  for (const target of input.attribution?.recorded ?? []) {
422
480
  const relativeTarget = repoRelative(input.snapshot.root, target);
@@ -457,7 +515,9 @@ export function enforceWriteBoundary(input: EnforceWriteBoundaryInput): WriteBou
457
515
  .join("; ")}.`,
458
516
  attributionComplete
459
517
  ? "Each was blamed on this window because a run in it recorded writing that path."
460
- : "This window had no complete write record, so every change outside the declaration was blamed on it.",
518
+ : `Attribution was incomplete because ${attributionDowngrades
519
+ .map(describeWriteBoundaryAttributionDowngrade)
520
+ .join("; ")}, so every change outside the declaration was blamed on this window.`,
461
521
  "If the change was legitimate, widen the step's `writes:` declaration to cover it.",
462
522
  ];
463
523
  const concurrentDetail =
@@ -482,6 +542,7 @@ export function enforceWriteBoundary(input: EnforceWriteBoundaryInput): WriteBou
482
542
  violations: violations.map((change) => change.path),
483
543
  unattributed: unattributed.map((change) => change.path),
484
544
  attributionComplete,
545
+ attributionDowngrades,
485
546
  rolledBack,
486
547
  unrecoverable,
487
548
  status,
@@ -3,6 +3,11 @@ import { join } from "node:path";
3
3
  import { assertSafeId } from "../../../core/safe-id.js";
4
4
  import { safeResourceWrite } from "../../../core/safe-resource-write.js";
5
5
  import type { EvalArtifactV4, EvalTokenAccountingV4 } from "../schema/artifact.js";
6
+ import { assertEvalBehaviorReferencesVerdictV1, parseEvalBehaviorVerdictV1 } from "../schema/behavioral.js";
7
+ import { parseEvalBehaviorMetricsV1 } from "../schema/behavioral-metrics.js";
8
+ import { parseEvalExecutionEnvelopeV1, parseEvalExecutionMatrixDimensionsV1 } from "../schema/execution-envelope.js";
9
+ import { parseEvalServingConfigurationV1 } from "../schema/serving.js";
10
+ import { parseEvalVerdictEnvelopeV1 } from "../schema/verdict.js";
6
11
  import { evalRoot } from "../store.js";
7
12
  import { redactArtifactForStorage } from "./redact.js";
8
13
 
@@ -45,6 +50,14 @@ export function parseEvalArtifactV4(value: unknown, source: string): EvalArtifac
45
50
  const summary = asRecord(value.summary, `${source}.summary`);
46
51
  const matrix = asRecord(value.matrix, `${source}.matrix`);
47
52
  const suite = asRecord(value.suite, `${source}.suite`);
53
+ const servingConfiguration =
54
+ value.servingConfiguration === undefined
55
+ ? undefined
56
+ : parseEvalServingConfigurationV1(value.servingConfiguration, `${source}.servingConfiguration`);
57
+ const aggregates =
58
+ value.aggregates === undefined
59
+ ? undefined
60
+ : (readArray(value, source, "aggregates") as NonNullable<EvalArtifactV4["aggregates"]>);
48
61
  return {
49
62
  version: 4,
50
63
  evalId: readString(value, source, "evalId"),
@@ -62,7 +75,11 @@ export function parseEvalArtifactV4(value: unknown, source: string): EvalArtifac
62
75
  target: readString(matrix, `${source}.matrix`, "target"),
63
76
  model: readNullableString(matrix, `${source}.matrix`, "model"),
64
77
  thinking: readNullableString(matrix, `${source}.matrix`, "thinking"),
78
+ ...(matrix.dimensions === undefined
79
+ ? {}
80
+ : { dimensions: parseEvalExecutionMatrixDimensionsV1(matrix.dimensions, `${source}.matrix.dimensions`) }),
65
81
  },
82
+ ...(servingConfiguration === undefined ? {} : { servingConfiguration }),
66
83
  summary: {
67
84
  runs: readNumber(summary, `${source}.summary`, "runs"),
68
85
  passed: readNumber(summary, `${source}.summary`, "passed"),
@@ -71,6 +88,7 @@ export function parseEvalArtifactV4(value: unknown, source: string): EvalArtifac
71
88
  tokens: parseTokenAccounting(summary.tokens, `${source}.summary.tokens`),
72
89
  wallTimeMs: readNumber(summary, `${source}.summary`, "wallTimeMs"),
73
90
  },
91
+ ...(aggregates === undefined ? {} : { aggregates }),
74
92
  results: readArray(value, source, "results").map((entry, index) => parseResult(entry, `${source}.results[${index}]`)),
75
93
  };
76
94
  }
@@ -108,6 +126,46 @@ function parseTokenAccounting(value: unknown, source: string): EvalTokenAccounti
108
126
  function parseResult(value: unknown, source: string): EvalArtifactV4["results"][number] {
109
127
  const record = asRecord(value, source);
110
128
  const target = asRecord(record.target, `${source}.target`);
129
+ const verdict =
130
+ record.verdict === undefined ? undefined : parseEvalVerdictEnvelopeV1(record.verdict, `${source}.verdict`);
131
+ const behavioral =
132
+ record.behavioral === undefined ? undefined : parseEvalBehaviorVerdictV1(record.behavioral, `${source}.behavioral`);
133
+ const behavioralMetrics =
134
+ record.behavioralMetrics === undefined
135
+ ? undefined
136
+ : parseEvalBehaviorMetricsV1(record.behavioralMetrics, `${source}.behavioralMetrics`);
137
+ const executionEnvelope =
138
+ record.executionEnvelope === undefined
139
+ ? undefined
140
+ : parseEvalExecutionEnvelopeV1(record.executionEnvelope, `${source}.executionEnvelope`);
141
+ if (behavioral !== undefined && verdict === undefined) {
142
+ throw new Error(`${source}.behavioral: sibling document requires a verdict`);
143
+ }
144
+ if (behavioral !== undefined && verdict !== undefined) {
145
+ assertEvalBehaviorReferencesVerdictV1(behavioral, verdict, `${source}.behavioral`);
146
+ }
147
+ if (behavioralMetrics !== undefined) {
148
+ if (behavioral === undefined) throw new Error(`${source}.behavioralMetrics: requires a behavioral verdict`);
149
+ if (behavioralMetrics.scenarioId !== readString(record, source, "taskId")) {
150
+ throw new Error(`${source}.behavioralMetrics.scenarioId: conflicts with result taskId`);
151
+ }
152
+ if (
153
+ behavioralMetrics.target.id !== readString(target, `${source}.target`, "id") ||
154
+ behavioralMetrics.target.model !== readNullableString(target, `${source}.target`, "model")
155
+ ) {
156
+ throw new Error(`${source}.behavioralMetrics.target: conflicts with result target`);
157
+ }
158
+ }
159
+ if (executionEnvelope !== undefined) {
160
+ if (behavioral === undefined) throw new Error(`${source}.executionEnvelope: requires a behavioral verdict`);
161
+ if (
162
+ executionEnvelope.target !== readString(target, `${source}.target`, "id") ||
163
+ executionEnvelope.corpus.id !== behavioral.corpus.id ||
164
+ executionEnvelope.corpus.version !== behavioral.corpus.version
165
+ ) {
166
+ throw new Error(`${source}.executionEnvelope: conflicts with result target or behavioral corpus`);
167
+ }
168
+ }
111
169
  return {
112
170
  assignmentId: readNullableString(record, source, "assignmentId"),
113
171
  terminalReceiptDigest: readNullableDigest(record, source, "terminalReceiptDigest"),
@@ -122,6 +180,10 @@ function parseResult(value: unknown, source: string): EvalArtifactV4["results"][
122
180
  failureClass: readNullableString(record, source, "failureClass"),
123
181
  metrics: asRecord(record.metrics, `${source}.metrics`) as Record<string, number | string | boolean | null>,
124
182
  artifacts: asRecord(record.artifacts, `${source}.artifacts`) as Record<string, string | string[] | null>,
183
+ ...(verdict === undefined ? {} : { verdict }),
184
+ ...(behavioral === undefined ? {} : { behavioral }),
185
+ ...(behavioralMetrics === undefined ? {} : { behavioralMetrics }),
186
+ ...(executionEnvelope === undefined ? {} : { executionEnvelope }),
125
187
  };
126
188
  }
127
189