harnery 0.38.0 → 0.40.0

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 (378) hide show
  1. package/dist/commands/agents.d.ts.map +1 -1
  2. package/dist/commands/agents.js +32 -8
  3. package/dist/commands/artifacts.d.ts.map +1 -1
  4. package/dist/commands/artifacts.js +97 -6
  5. package/dist/commands/backup.d.ts +16 -0
  6. package/dist/commands/backup.d.ts.map +1 -1
  7. package/dist/commands/backup.js +239 -33
  8. package/dist/commands/browse.d.ts +8 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +466 -231
  11. package/dist/commands/diagnostics.d.ts.map +1 -1
  12. package/dist/commands/diagnostics.js +6 -3
  13. package/dist/commands/ledger-v3.d.ts.map +1 -1
  14. package/dist/commands/ledger-v3.js +20 -4
  15. package/dist/commands/qa-run.d.ts.map +1 -1
  16. package/dist/commands/qa-run.js +26 -3
  17. package/dist/commands/qa-status.d.ts.map +1 -1
  18. package/dist/commands/qa-status.js +7 -2
  19. package/dist/commands/resources.d.ts.map +1 -1
  20. package/dist/commands/resources.js +23 -3
  21. package/dist/commands/review-pack.d.ts.map +1 -1
  22. package/dist/commands/review-pack.js +98 -23
  23. package/dist/commands/tunnel.d.ts.map +1 -1
  24. package/dist/commands/tunnel.js +2 -2
  25. package/dist/core/adapters/profiles.d.ts +1 -1
  26. package/dist/core/agents/canonical-emit.d.ts +3 -2
  27. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  28. package/dist/core/agents/canonical-emit.js +2 -0
  29. package/dist/core/agents/cli.js +13 -6
  30. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  31. package/dist/core/agents/live-authority-v3.js +5 -3
  32. package/dist/core/agents/live-lifecycle-v3.d.ts +2 -2
  33. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  34. package/dist/core/agents/render/prompt-context.d.ts +7 -5
  35. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  36. package/dist/core/agents/render/prompt-context.js +63 -8
  37. package/dist/core/agents/session-finalizer-v3.d.ts +2 -2
  38. package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -1
  39. package/dist/core/agents/state/live-coordination-view.d.ts +3 -3
  40. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  41. package/dist/core/agents/state/live-coordination-view.js +2 -1
  42. package/dist/core/agents/state/names.d.ts +35 -2
  43. package/dist/core/agents/state/names.d.ts.map +1 -1
  44. package/dist/core/agents/state/names.js +80 -5
  45. package/dist/core/agents/state/stale-sweep.d.ts +2 -1
  46. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  47. package/dist/core/agents/state/stale-sweep.js +23 -6
  48. package/dist/core/agents/turn-elapsed.d.ts +51 -0
  49. package/dist/core/agents/turn-elapsed.d.ts.map +1 -0
  50. package/dist/core/agents/turn-elapsed.js +91 -0
  51. package/dist/core/artifacts/activity.d.ts +14 -0
  52. package/dist/core/artifacts/activity.d.ts.map +1 -0
  53. package/dist/core/artifacts/activity.js +52 -0
  54. package/dist/core/artifacts/delivery-card.d.ts +39 -0
  55. package/dist/core/artifacts/delivery-card.d.ts.map +1 -0
  56. package/dist/core/artifacts/delivery-card.js +282 -0
  57. package/dist/core/artifacts/index.d.ts +42 -7
  58. package/dist/core/artifacts/index.d.ts.map +1 -1
  59. package/dist/core/artifacts/index.js +288 -76
  60. package/dist/core/artifacts/mutation-lock.d.ts +2 -0
  61. package/dist/core/artifacts/mutation-lock.d.ts.map +1 -0
  62. package/dist/core/artifacts/mutation-lock.js +91 -0
  63. package/dist/core/backup/host-snapshot.d.ts +18 -0
  64. package/dist/core/backup/host-snapshot.d.ts.map +1 -0
  65. package/dist/core/backup/host-snapshot.js +64 -0
  66. package/dist/core/config.d.ts +20 -5
  67. package/dist/core/config.d.ts.map +1 -1
  68. package/dist/core/config.js +44 -10
  69. package/dist/core/diagnostics/advice.d.ts +38 -3
  70. package/dist/core/diagnostics/advice.d.ts.map +1 -1
  71. package/dist/core/diagnostics/advice.js +103 -99
  72. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  73. package/dist/core/diagnostics/bundle.js +111 -47
  74. package/dist/core/diagnostics/comparison.d.ts.map +1 -1
  75. package/dist/core/diagnostics/comparison.js +3 -2
  76. package/dist/core/diagnostics/contract.d.ts +15 -31
  77. package/dist/core/diagnostics/contract.d.ts.map +1 -1
  78. package/dist/core/diagnostics/contract.js +2 -5
  79. package/dist/core/diagnostics/index.d.ts +2 -0
  80. package/dist/core/diagnostics/index.d.ts.map +1 -1
  81. package/dist/core/diagnostics/index.js +2 -0
  82. package/dist/core/diagnostics/pressure-contract.d.ts +260 -0
  83. package/dist/core/diagnostics/pressure-contract.d.ts.map +1 -0
  84. package/dist/core/diagnostics/pressure-contract.js +70 -0
  85. package/dist/core/diagnostics/pressure.d.ts +25 -0
  86. package/dist/core/diagnostics/pressure.d.ts.map +1 -0
  87. package/dist/core/diagnostics/pressure.js +1138 -0
  88. package/dist/core/diagnostics/replay.d.ts.map +1 -1
  89. package/dist/core/diagnostics/replay.js +90 -10
  90. package/dist/core/events/v3/adapter-id.d.ts +6 -0
  91. package/dist/core/events/v3/adapter-id.d.ts.map +1 -0
  92. package/dist/core/events/v3/adapter-id.js +7 -0
  93. package/dist/core/events/v3/archive-retention.d.ts.map +1 -1
  94. package/dist/core/events/v3/archive-retention.js +3 -2
  95. package/dist/core/events/v3/bootstrap.d.ts +41 -4
  96. package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
  97. package/dist/core/events/v3/bootstrap.js +120 -17
  98. package/dist/core/events/v3/capabilities.d.ts +16 -9
  99. package/dist/core/events/v3/capabilities.d.ts.map +1 -1
  100. package/dist/core/events/v3/capabilities.js +37 -2
  101. package/dist/core/events/v3/capability-drift.d.ts +2 -2
  102. package/dist/core/events/v3/capability-drift.d.ts.map +1 -1
  103. package/dist/core/events/v3/control-witness.d.ts +48 -5
  104. package/dist/core/events/v3/control-witness.d.ts.map +1 -1
  105. package/dist/core/events/v3/control-witness.js +91 -36
  106. package/dist/core/events/v3/control.d.ts.map +1 -1
  107. package/dist/core/events/v3/control.js +22 -2
  108. package/dist/core/events/v3/index.d.ts +2 -1
  109. package/dist/core/events/v3/index.d.ts.map +1 -1
  110. package/dist/core/events/v3/index.js +2 -1
  111. package/dist/core/events/v3/live-feed.d.ts +7 -1
  112. package/dist/core/events/v3/live-feed.d.ts.map +1 -1
  113. package/dist/core/events/v3/live-feed.js +21 -3
  114. package/dist/core/events/v3/live-routing.d.ts.map +1 -1
  115. package/dist/core/events/v3/live-routing.js +10 -2
  116. package/dist/core/events/v3/producers/command-recorder.d.ts +2 -2
  117. package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
  118. package/dist/core/events/v3/producers/command-recorder.js +2 -1
  119. package/dist/core/events/v3/producers/coordination-recorder.d.ts +2 -2
  120. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  121. package/dist/core/events/v3/producers/coordination-recorder.js +2 -1
  122. package/dist/core/events/v3/producers/hook-base.d.ts +2 -2
  123. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
  124. package/dist/core/events/v3/producers/hook-base.js +15 -5
  125. package/dist/core/events/v3/producers/hook.d.ts.map +1 -1
  126. package/dist/core/events/v3/producers/hook.js +17 -4
  127. package/dist/core/events/v3/producers/intake.d.ts +4 -4
  128. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  129. package/dist/core/events/v3/producers/intake.js +3 -2
  130. package/dist/core/events/v3/producers/producer-state-observer.d.ts.map +1 -1
  131. package/dist/core/events/v3/producers/producer-state-observer.js +2 -1
  132. package/dist/core/events/v3/producers/recorder.d.ts +16 -5
  133. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  134. package/dist/core/events/v3/producers/recorder.js +31 -9
  135. package/dist/core/events/v3/reader.d.ts.map +1 -1
  136. package/dist/core/events/v3/reader.js +10 -13
  137. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  138. package/dist/core/events/v3/runtime-build.js +3 -2
  139. package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts +2 -2
  140. package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts.map +1 -1
  141. package/dist/core/events/v3/turn-telemetry.d.ts +2 -2
  142. package/dist/core/events/v3/turn-telemetry.d.ts.map +1 -1
  143. package/dist/core/hooks/adapter/output.d.ts +16 -14
  144. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  145. package/dist/core/hooks/adapter/output.js +36 -38
  146. package/dist/core/hooks/backup-schedule.d.ts +34 -0
  147. package/dist/core/hooks/backup-schedule.d.ts.map +1 -0
  148. package/dist/core/hooks/backup-schedule.js +114 -0
  149. package/dist/core/hooks/cli.js +61 -18
  150. package/dist/core/hooks/codex-wsl-bridge.d.ts +2 -0
  151. package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -1
  152. package/dist/core/hooks/codex-wsl-bridge.js +18 -4
  153. package/dist/core/hooks/index.d.ts +1 -0
  154. package/dist/core/hooks/index.d.ts.map +1 -1
  155. package/dist/core/hooks/index.js +1 -0
  156. package/dist/core/hooks/prompt-context/contract.d.ts +61 -0
  157. package/dist/core/hooks/prompt-context/contract.d.ts.map +1 -0
  158. package/dist/core/hooks/prompt-context/contract.js +102 -0
  159. package/dist/core/hooks/prompt-context/index.d.ts +5 -0
  160. package/dist/core/hooks/prompt-context/index.d.ts.map +1 -0
  161. package/dist/core/hooks/prompt-context/index.js +2 -0
  162. package/dist/core/hooks/prompt-context/runner.d.ts +24 -0
  163. package/dist/core/hooks/prompt-context/runner.d.ts.map +1 -0
  164. package/dist/core/hooks/prompt-context/runner.js +249 -0
  165. package/dist/core/qa-artifacts.d.ts +3 -0
  166. package/dist/core/qa-artifacts.d.ts.map +1 -1
  167. package/dist/core/qa-artifacts.js +25 -3
  168. package/dist/core/resources/contract.d.ts +69 -3
  169. package/dist/core/resources/contract.d.ts.map +1 -1
  170. package/dist/core/resources/contract.js +1 -1
  171. package/dist/core/resources/index.d.ts +1 -0
  172. package/dist/core/resources/index.d.ts.map +1 -1
  173. package/dist/core/resources/index.js +1 -0
  174. package/dist/core/resources/native.d.ts +36 -0
  175. package/dist/core/resources/native.d.ts.map +1 -0
  176. package/dist/core/resources/native.js +418 -0
  177. package/dist/core/resources/sampler.d.ts +2 -0
  178. package/dist/core/resources/sampler.d.ts.map +1 -1
  179. package/dist/core/resources/sampler.js +31 -1
  180. package/dist/core/resources/status.d.ts +61 -0
  181. package/dist/core/resources/status.d.ts.map +1 -0
  182. package/dist/core/resources/status.js +536 -0
  183. package/dist/core/resources/system.d.ts +27 -0
  184. package/dist/core/resources/system.d.ts.map +1 -0
  185. package/dist/core/resources/system.js +439 -0
  186. package/dist/core/semantic/contract.d.ts +13 -13
  187. package/dist/core/semantic/service.d.ts +2 -0
  188. package/dist/core/semantic/service.d.ts.map +1 -1
  189. package/dist/core/semantic/service.js +27 -4
  190. package/dist/core/storage/builtins.js +41 -0
  191. package/dist/core/supervisor/contract.d.ts +41 -3
  192. package/dist/core/supervisor/contract.d.ts.map +1 -1
  193. package/dist/core/supervisor/contract.js +3 -2
  194. package/dist/core/supervisor/findings.d.ts +3 -0
  195. package/dist/core/supervisor/findings.d.ts.map +1 -1
  196. package/dist/core/supervisor/findings.js +88 -0
  197. package/dist/core/supervisor/history.d.ts.map +1 -1
  198. package/dist/core/supervisor/history.js +14 -0
  199. package/dist/core/supervisor/ledger-state.d.ts +55 -0
  200. package/dist/core/supervisor/ledger-state.d.ts.map +1 -0
  201. package/dist/core/supervisor/ledger-state.js +0 -0
  202. package/dist/core/supervisor/service.d.ts.map +1 -1
  203. package/dist/core/supervisor/service.js +120 -7
  204. package/dist/core/supervisor/storage.d.ts +3 -1
  205. package/dist/core/supervisor/storage.d.ts.map +1 -1
  206. package/dist/core/supervisor/storage.js +4 -0
  207. package/dist/core/workflow/admission.d.ts +3 -0
  208. package/dist/core/workflow/admission.d.ts.map +1 -1
  209. package/dist/core/workflow/admission.js +28 -16
  210. package/dist/core/workflow/engine.js +2 -2
  211. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  212. package/dist/core/workflow/live-session-v3.js +6 -1
  213. package/dist/core/workflow/proof.d.ts.map +1 -1
  214. package/dist/core/workflow/proof.js +4 -3
  215. package/dist/lib/browser/capture-fidelity.d.ts +7 -0
  216. package/dist/lib/browser/capture-fidelity.d.ts.map +1 -1
  217. package/dist/lib/browser/capture-fidelity.js +17 -0
  218. package/dist/lib/browser/client.d.ts +9 -0
  219. package/dist/lib/browser/client.d.ts.map +1 -1
  220. package/dist/lib/browser/client.js +85 -37
  221. package/dist/lib/browser/index.d.ts +3 -1
  222. package/dist/lib/browser/index.d.ts.map +1 -1
  223. package/dist/lib/browser/index.js +2 -1
  224. package/dist/lib/browser/page-review-budget.d.ts +13 -0
  225. package/dist/lib/browser/page-review-budget.d.ts.map +1 -0
  226. package/dist/lib/browser/page-review-budget.js +251 -0
  227. package/dist/lib/browser/page-review-contracts.d.ts +127 -0
  228. package/dist/lib/browser/page-review-contracts.d.ts.map +1 -0
  229. package/dist/lib/browser/page-review-contracts.js +5 -0
  230. package/dist/lib/browser/page-review-evidence.d.ts +48 -0
  231. package/dist/lib/browser/page-review-evidence.d.ts.map +1 -0
  232. package/dist/lib/browser/page-review-evidence.js +340 -0
  233. package/dist/lib/browser/page-review-judge.d.ts +1 -1
  234. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  235. package/dist/lib/browser/page-review-judge.js +46 -38
  236. package/dist/lib/browser/page-review-pack.d.ts +44 -22
  237. package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
  238. package/dist/lib/browser/page-review-pack.js +116 -36
  239. package/dist/lib/browser/page-review-source.d.ts +12 -0
  240. package/dist/lib/browser/page-review-source.d.ts.map +1 -0
  241. package/dist/lib/browser/page-review-source.js +22 -0
  242. package/dist/lib/browser/qa-run-contracts.d.ts +35 -9
  243. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  244. package/dist/lib/browser/qa-run-contracts.js +48 -14
  245. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  246. package/dist/lib/browser/qa-run.js +215 -68
  247. package/dist/lib/browser/qa-snapshot.d.ts +6 -0
  248. package/dist/lib/browser/qa-snapshot.d.ts.map +1 -1
  249. package/dist/lib/browser/qa-snapshot.js +259 -48
  250. package/dist/lib/browser/review-capture-transaction.d.ts +13 -0
  251. package/dist/lib/browser/review-capture-transaction.d.ts.map +1 -0
  252. package/dist/lib/browser/review-capture-transaction.js +79 -0
  253. package/dist/lib/browser/standalone-html.d.ts.map +1 -1
  254. package/dist/lib/browser/standalone-html.js +4 -2
  255. package/dist/lib/browser/thumbnail-association.d.ts +34 -0
  256. package/dist/lib/browser/thumbnail-association.d.ts.map +1 -0
  257. package/dist/lib/browser/thumbnail-association.js +158 -0
  258. package/dist/lib/browser/tiling.d.ts +30 -0
  259. package/dist/lib/browser/tiling.d.ts.map +1 -1
  260. package/dist/lib/browser/tiling.js +38 -0
  261. package/dist/lib/docs-metadata-managed.d.ts.map +1 -1
  262. package/dist/lib/docs-metadata-managed.js +4 -1
  263. package/dist/lib/identities/assume.d.ts.map +1 -1
  264. package/dist/lib/identities/assume.js +7 -1
  265. package/dist/lib/instructions/templates.d.ts.map +1 -1
  266. package/dist/lib/instructions/templates.js +1 -0
  267. package/dist/lib/local-file-url.d.ts +12 -0
  268. package/dist/lib/local-file-url.d.ts.map +1 -1
  269. package/dist/lib/local-file-url.js +16 -2
  270. package/dist/lib/tunnel/state.d.ts +19 -4
  271. package/dist/lib/tunnel/state.d.ts.map +1 -1
  272. package/dist/lib/tunnel/state.js +53 -13
  273. package/package.json +7 -4
  274. package/schemas/config.schema.json +69 -2
  275. package/src/commands/agents.ts +42 -9
  276. package/src/commands/artifacts.ts +130 -6
  277. package/src/commands/backup.ts +287 -34
  278. package/src/commands/browse.ts +564 -267
  279. package/src/commands/diagnostics.ts +6 -3
  280. package/src/commands/ledger-v3.ts +20 -3
  281. package/src/commands/qa-run.ts +37 -3
  282. package/src/commands/qa-status.ts +11 -2
  283. package/src/commands/resources.ts +23 -3
  284. package/src/commands/review-pack.ts +165 -62
  285. package/src/commands/tunnel.ts +2 -1
  286. package/src/core/agents/canonical-emit.ts +4 -2
  287. package/src/core/agents/cli.ts +14 -6
  288. package/src/core/agents/live-authority-v3.ts +7 -3
  289. package/src/core/agents/live-lifecycle-v3.ts +2 -2
  290. package/src/core/agents/render/prompt-context.ts +68 -14
  291. package/src/core/agents/session-finalizer-v3.ts +2 -2
  292. package/src/core/agents/state/live-coordination-view.ts +4 -4
  293. package/src/core/agents/state/names.ts +92 -7
  294. package/src/core/agents/state/stale-sweep.ts +22 -8
  295. package/src/core/agents/turn-elapsed.ts +135 -0
  296. package/src/core/artifacts/activity.ts +71 -0
  297. package/src/core/artifacts/delivery-card.ts +375 -0
  298. package/src/core/artifacts/index.ts +385 -91
  299. package/src/core/artifacts/mutation-lock.ts +106 -0
  300. package/src/core/backup/host-snapshot.ts +76 -0
  301. package/src/core/config.ts +66 -13
  302. package/src/core/diagnostics/advice.ts +148 -120
  303. package/src/core/diagnostics/bundle.ts +138 -52
  304. package/src/core/diagnostics/comparison.ts +6 -2
  305. package/src/core/diagnostics/contract.ts +39 -42
  306. package/src/core/diagnostics/index.ts +2 -0
  307. package/src/core/diagnostics/pressure-contract.ts +294 -0
  308. package/src/core/diagnostics/pressure.ts +1361 -0
  309. package/src/core/diagnostics/replay.ts +116 -11
  310. package/src/core/events/v3/adapter-id.ts +12 -0
  311. package/src/core/events/v3/archive-retention.ts +3 -6
  312. package/src/core/events/v3/bootstrap.ts +163 -16
  313. package/src/core/events/v3/capabilities.ts +75 -25
  314. package/src/core/events/v3/capability-drift.ts +2 -2
  315. package/src/core/events/v3/control-witness.ts +172 -41
  316. package/src/core/events/v3/control.ts +34 -2
  317. package/src/core/events/v3/index.ts +3 -0
  318. package/src/core/events/v3/live-feed.ts +23 -5
  319. package/src/core/events/v3/live-routing.ts +14 -2
  320. package/src/core/events/v3/producers/command-recorder.ts +4 -4
  321. package/src/core/events/v3/producers/coordination-recorder.ts +4 -4
  322. package/src/core/events/v3/producers/hook-base.ts +21 -16
  323. package/src/core/events/v3/producers/hook.ts +21 -4
  324. package/src/core/events/v3/producers/intake.ts +6 -6
  325. package/src/core/events/v3/producers/producer-state-observer.ts +2 -1
  326. package/src/core/events/v3/producers/recorder.ts +50 -21
  327. package/src/core/events/v3/reader.ts +14 -13
  328. package/src/core/events/v3/runtime-build.ts +3 -2
  329. package/src/core/events/v3/runtime-telemetry-capabilities.ts +3 -3
  330. package/src/core/events/v3/turn-telemetry.ts +4 -4
  331. package/src/core/hooks/adapter/output.ts +50 -36
  332. package/src/core/hooks/backup-schedule.ts +147 -0
  333. package/src/core/hooks/cli.ts +65 -14
  334. package/src/core/hooks/codex-wsl-bridge.ts +21 -5
  335. package/src/core/hooks/index.ts +1 -0
  336. package/src/core/hooks/prompt-context/contract.ts +177 -0
  337. package/src/core/hooks/prompt-context/index.ts +22 -0
  338. package/src/core/hooks/prompt-context/runner.ts +303 -0
  339. package/src/core/qa-artifacts.ts +29 -3
  340. package/src/core/resources/contract.ts +76 -3
  341. package/src/core/resources/index.ts +1 -0
  342. package/src/core/resources/native.ts +481 -0
  343. package/src/core/resources/sampler.ts +46 -1
  344. package/src/core/resources/status.ts +717 -0
  345. package/src/core/resources/system.ts +530 -0
  346. package/src/core/semantic/service.ts +37 -3
  347. package/src/core/storage/builtins.ts +41 -0
  348. package/src/core/supervisor/contract.ts +45 -3
  349. package/src/core/supervisor/findings.ts +159 -0
  350. package/src/core/supervisor/history.ts +15 -0
  351. package/src/core/supervisor/ledger-state.ts +0 -0
  352. package/src/core/supervisor/service.ts +133 -5
  353. package/src/core/supervisor/storage.ts +7 -0
  354. package/src/core/workflow/admission.ts +33 -17
  355. package/src/core/workflow/engine.ts +2 -2
  356. package/src/core/workflow/live-session-v3.ts +6 -8
  357. package/src/core/workflow/proof.ts +4 -3
  358. package/src/lib/browser/capture-fidelity.ts +23 -0
  359. package/src/lib/browser/client.ts +96 -44
  360. package/src/lib/browser/index.ts +15 -1
  361. package/src/lib/browser/page-review-budget.ts +309 -0
  362. package/src/lib/browser/page-review-contracts.ts +129 -0
  363. package/src/lib/browser/page-review-evidence.ts +465 -0
  364. package/src/lib/browser/page-review-judge.ts +56 -40
  365. package/src/lib/browser/page-review-pack.ts +181 -43
  366. package/src/lib/browser/page-review-source.ts +30 -0
  367. package/src/lib/browser/qa-run-contracts.ts +62 -18
  368. package/src/lib/browser/qa-run.ts +259 -71
  369. package/src/lib/browser/qa-snapshot.ts +289 -48
  370. package/src/lib/browser/review-capture-transaction.ts +106 -0
  371. package/src/lib/browser/standalone-html.ts +4 -2
  372. package/src/lib/browser/thumbnail-association.ts +190 -0
  373. package/src/lib/browser/tiling.ts +63 -0
  374. package/src/lib/docs-metadata-managed.ts +4 -1
  375. package/src/lib/identities/assume.ts +12 -4
  376. package/src/lib/instructions/templates.ts +1 -0
  377. package/src/lib/local-file-url.ts +17 -2
  378. package/src/lib/tunnel/state.ts +70 -13
@@ -17,6 +17,17 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "
17
17
  import { cpus, freemem, loadavg, totalmem } from "node:os";
18
18
  import { basename, join } from "node:path";
19
19
  import { type CritiqueProvider, DEFAULT_CRITIQUE_RUBRIC } from "./critique.js";
20
+ import {
21
+ allocateTileBudget,
22
+ capturePlanDigest,
23
+ validatePageReviewCapturePlan,
24
+ } from "./page-review-budget.js";
25
+ import {
26
+ PAGE_REVIEW_DEFAULT_TILE_BUDGET,
27
+ type PageReviewCapturePlan,
28
+ type PageReviewContextAllocation,
29
+ } from "./page-review-contracts.js";
30
+ import { applyPackReviewDecisions, buildPackNativeEvidence } from "./page-review-evidence.js";
20
31
  import { type JudgedContext, judgePageReviewPack, toCritiqueRecords } from "./page-review-judge.js";
21
32
  import {
22
33
  finalizePageReviewPack,
@@ -31,12 +42,12 @@ import {
31
42
  type PageReviewGateRecord,
32
43
  readPackContext,
33
44
  readPackDom,
34
- readPackFullPage,
35
45
  readPackManifest,
36
46
  readPackSignature,
47
+ readPackTiles,
37
48
  } from "./page-review-pack.js";
38
49
  import type { QaManifest } from "./qa-plan.js";
39
- import { type PersistedCritique, QA_CRITIQUE_CONTRACT_VERSION, rubricDigest } from "./qa-reuse.js";
50
+ import type { PersistedCritique } from "./qa-reuse.js";
40
51
  import {
41
52
  computeJobDigest,
42
53
  computeVerdict,
@@ -57,7 +68,7 @@ import {
57
68
  type QaRunStatusDocument,
58
69
  type QaRunStatusState,
59
70
  } from "./qa-run-contracts.js";
60
- import { type QaSnapshotStoreOptions, saveQaSnapshot } from "./qa-snapshot.js";
71
+ import { loadReviewDecisions, type QaSnapshotStoreOptions, saveQaSnapshot } from "./qa-snapshot.js";
61
72
 
62
73
  /** Set on the runner's own environment before the host's critique provider is
63
74
  * loaded for the judge stage, unless the job permits metered critique: the
@@ -130,7 +141,6 @@ const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
130
141
  const DEFAULT_RUN_DEADLINE_MS = 900_000;
131
142
  const DEFAULT_COMMAND_CONCURRENCY = 2;
132
143
  /** Gate-hit rectangles forwarded to one context's capture child (`--review-pack-hit-rect`). */
133
- const REVIEW_PACK_HIT_RECTS_PER_CONTEXT = 50;
134
144
  const EXEC_MAX_BUFFER = 16 * 1024 * 1024;
135
145
 
136
146
  /** The planner's deterministic vocabulary is an executable contract, not a
@@ -775,14 +785,25 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
775
785
  ctx.viewport,
776
786
  ...(ctx.theme === "dark" ? ["--color-scheme", "dark"] : []),
777
787
  ...(ctx.args ?? []),
788
+ ...(job.interaction_states ?? [])
789
+ .filter((state) => state.name === ctx.state)
790
+ .flatMap((state) => [
791
+ ...state.setup,
792
+ ...state.assertions.flatMap((assertion) => ["--assert", assertion]),
793
+ "--assert-fail",
794
+ ]),
778
795
  ];
779
796
 
780
797
  // The planner's tile ceiling and every critique child must agree on the
781
798
  // per-context band cap, or the predicted cost and the real coverage drift.
782
- const critiqueMaxTilesArgs =
783
- job.policy?.critique_max_tiles !== undefined
784
- ? ["--check-critique-max-tiles", String(job.policy.critique_max_tiles)]
785
- : [];
799
+ const critiqueMaxTilesArgs = [
800
+ "--check-critique-max-tiles",
801
+ String(
802
+ job.policy?.critique_max_tiles ??
803
+ job.policy?.critique_tile_budget ??
804
+ PAGE_REVIEW_DEFAULT_TILE_BUDGET,
805
+ ),
806
+ ];
786
807
 
787
808
  // ------------------------------------------------------------------ plan
788
809
  const planArgv = [
@@ -870,6 +891,35 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
870
891
  }
871
892
  return finalize();
872
893
  }
894
+ // The planning render and capture must use the same native band geometry.
895
+ // Load the provider's pixel budget before gates, without making model calls.
896
+ const visual = manifest.checks.visual;
897
+ const runsVisual = visual !== "none";
898
+ const priorHeadlessOnly = process.env[QA_RUN_HEADLESS_ONLY_ENV];
899
+ const restoreHeadlessOnly = (): void => {
900
+ if (priorHeadlessOnly === undefined) delete process.env[QA_RUN_HEADLESS_ONLY_ENV];
901
+ else process.env[QA_RUN_HEADLESS_ONLY_ENV] = priorHeadlessOnly;
902
+ };
903
+ let provider: CritiqueProvider | undefined;
904
+ if (runsVisual) {
905
+ if (!job.policy?.allow_metered_critique) process.env[QA_RUN_HEADLESS_ONLY_ENV] = "1";
906
+ try {
907
+ provider =
908
+ options.critiqueProvider ??
909
+ (options.critiqueProviderLoader ? await options.critiqueProviderLoader() : undefined);
910
+ } catch (err: unknown) {
911
+ log(`critique provider failed to load: ${err instanceof Error ? err.message : String(err)}`);
912
+ }
913
+ }
914
+ const bandArgs =
915
+ provider?.tileBudgetPx !== undefined
916
+ ? ["--check-critique-band", String(Math.max(200, Math.min(1400, provider.tileBudgetPx)))]
917
+ : [];
918
+ const scopeArgs =
919
+ visual === "full-page"
920
+ ? []
921
+ : manifest.scopes.flatMap((s) => ["--review-pack-scope", s.selector]);
922
+ const gateCapturePlans = new Map<string, PageReviewCapturePlan>();
873
923
  const manifestGateArgs = manifest.checks.deterministic.flatMap(
874
924
  (check) => MANIFEST_DETERMINISTIC_ARGS[check] ?? [],
875
925
  );
@@ -901,6 +951,20 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
901
951
  ...(manifest.checks.visual !== "none" ? ["--no-screenshot"] : []),
902
952
  ...manifestGateArgs,
903
953
  ...applicable.flatMap((check) => check.args),
954
+ ...(runsVisual
955
+ ? [
956
+ "--review-pack-plan",
957
+ "--review-pack-context",
958
+ ctx.id,
959
+ "--qa-theme",
960
+ ctx.theme,
961
+ "--qa-state",
962
+ ctx.state,
963
+ ...scopeArgs,
964
+ ...bandArgs,
965
+ ...critiqueMaxTilesArgs,
966
+ ]
967
+ : []),
904
968
  ];
905
969
  log(`gate ${ctx.id}: ${checkId}`);
906
970
  const { res, wallTimeMs } = await timedExec(argv, baseEnv);
@@ -908,6 +972,26 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
908
972
  const failures: string[] = [];
909
973
  let outcome: QaRunCommandOutcome["outcome"];
910
974
  const envelope = existsSync(jsonPath) ? readEnvelope(jsonPath) : undefined;
975
+ if (runsVisual) {
976
+ try {
977
+ const capturePlan = validatePageReviewCapturePlan(envelope?.review_pack_capture_plan);
978
+ if (
979
+ capturePlan.context_id !== ctx.id ||
980
+ capturePlan.viewport !== ctx.viewport ||
981
+ capturePlan.theme !== ctx.theme ||
982
+ capturePlan.state !== ctx.state
983
+ ) {
984
+ throw new Error("capture plan belongs to a different render context");
985
+ }
986
+ gateCapturePlans.set(ctx.id, capturePlan);
987
+ } catch (err: unknown) {
988
+ blockers.push({
989
+ stage: "capture",
990
+ context_id: ctx.id,
991
+ reason: `capture plan unavailable: ${err instanceof Error ? err.message : String(err)}`,
992
+ });
993
+ }
994
+ }
911
995
  const consoleFailures = enforceConsole ? parseConsoleFailures(envelope) : [];
912
996
  if (!res.error && res.exitCode === 0 && existsSync(jsonPath)) {
913
997
  if (consoleFailures.length > 0) {
@@ -1030,9 +1114,6 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
1030
1114
  // Each context is rendered ONCE into the run's page review pack (full-page
1031
1115
  // screenshot, tiles as PNG files, DOM, signature) through the same bounded
1032
1116
  // pool the gates used, and its browser closes before any vision call.
1033
- const visual = manifest.checks.visual;
1034
- const cleanSoFar =
1035
- blockers.length === 0 && commands.every((command) => command.outcome === "passed");
1036
1117
  const packDir = join(outDir, PAGE_REVIEW_PACK_DIRNAME);
1037
1118
  const capturedRecords: PageReviewContextRecord[] = [];
1038
1119
  const judgeCommand = options.reviewPackJudgeCommand?.(packDir);
@@ -1102,61 +1183,62 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
1102
1183
  }
1103
1184
  };
1104
1185
 
1105
- // The provider is loaded once, before capture, so the capture children can
1106
- // clamp band height to the routed model's vision budget (what browse does
1107
- // for --check-critique) and the judge reuses the same instance.
1108
- const priorHeadlessOnly = process.env[QA_RUN_HEADLESS_ONLY_ENV];
1109
- const restoreHeadlessOnly = (): void => {
1110
- if (priorHeadlessOnly === undefined) delete process.env[QA_RUN_HEADLESS_ONLY_ENV];
1111
- else process.env[QA_RUN_HEADLESS_ONLY_ENV] = priorHeadlessOnly;
1112
- };
1113
- let provider: CritiqueProvider | undefined;
1114
- const runsVisual = cleanSoFar && visual !== "none";
1186
+ enterStage("capture");
1187
+ const captureStart = Date.now();
1115
1188
  if (runsVisual) {
1116
- if (!job.policy?.allow_metered_critique) process.env[QA_RUN_HEADLESS_ONLY_ENV] = "1";
1189
+ writeFileSync(
1190
+ join(outDir, "preliminary-gates.json"),
1191
+ `${JSON.stringify(gateOutcomes, null, 2)}\n`,
1192
+ );
1193
+ for (const gate of gateOutcomes)
1194
+ if (gate) {
1195
+ gate.outcome = "unknown";
1196
+ gate.failures = ["Authoritative same-visit gates and capture have not completed."];
1197
+ }
1198
+ const allocations = new Map<string, PageReviewContextAllocation>();
1117
1199
  try {
1118
- provider =
1119
- options.critiqueProvider ??
1120
- (options.critiqueProviderLoader ? await options.critiqueProviderLoader() : undefined);
1121
- } catch (err: unknown) {
1122
- provider = undefined;
1123
- log(`critique provider failed to load: ${err instanceof Error ? err.message : String(err)}`);
1124
- }
1125
- }
1126
- const bandArgs =
1127
- provider?.tileBudgetPx !== undefined
1128
- ? ["--check-critique-band", String(Math.max(200, Math.min(1400, provider.tileBudgetPx)))]
1129
- : [];
1130
- const scopeArgs =
1131
- visual === "full-page"
1132
- ? []
1133
- : manifest.scopes.flatMap((s) => ["--review-pack-scope", s.selector]);
1134
- // Gate-hit rectangles ride into each context's capture child so the bands
1135
- // they land in are cut even past the tile cap (`hit band N` tiles). The
1136
- // gates ran first, so the rects are known before the page is captured.
1137
- const hitRectsByContext = new Map<string, string[]>();
1138
- for (const gate of gateRecords()) {
1139
- for (const hit of gate.hits ?? []) {
1140
- const list = hitRectsByContext.get(gate.context_id) ?? [];
1141
- if (list.length >= REVIEW_PACK_HIT_RECTS_PER_CONTEXT) break;
1142
- const r = hit.rect;
1143
- list.push(
1144
- [r.x, r.y, Math.max(0, r.width), Math.max(0, r.height)].map((n) => Math.round(n)).join(","),
1200
+ const plans = contexts.map((ctx) => {
1201
+ const candidate = gateCapturePlans.get(ctx.id);
1202
+ if (!candidate)
1203
+ throw new Error(
1204
+ `no valid capture plan for ${ctx.id}; rerun after fixing its gate capture`,
1205
+ );
1206
+ return candidate;
1207
+ });
1208
+ const ceilings =
1209
+ job.policy?.critique_max_tiles === undefined
1210
+ ? undefined
1211
+ : Object.fromEntries(
1212
+ contexts.map((ctx) => [ctx.id, job.policy?.critique_max_tiles as number]),
1213
+ );
1214
+ const allocation = allocateTileBudget(
1215
+ plans,
1216
+ job.policy?.critique_tile_budget ?? PAGE_REVIEW_DEFAULT_TILE_BUDGET,
1217
+ ceilings,
1145
1218
  );
1146
- hitRectsByContext.set(gate.context_id, list);
1219
+ for (const row of allocation.contexts) allocations.set(row.context_id, row);
1220
+ writeFileSync(
1221
+ join(outDir, "tile-allocation.json"),
1222
+ `${JSON.stringify(allocation, null, 2)}\n`,
1223
+ );
1224
+ log(
1225
+ `tile budget: ${allocation.used}/${allocation.budget} allocated across ${allocations.size} contexts`,
1226
+ );
1227
+ } catch (err: unknown) {
1228
+ blockers.push({
1229
+ stage: "capture",
1230
+ reason: `tile allocation failed: ${err instanceof Error ? err.message : String(err)}`,
1231
+ });
1147
1232
  }
1148
- }
1149
- const hitRectArgs = (contextId: string): string[] =>
1150
- (hitRectsByContext.get(contextId) ?? []).flatMap((spec) => ["--review-pack-hit-rect", spec]);
1151
-
1152
- enterStage("capture");
1153
- const captureStart = Date.now();
1154
- if (runsVisual) {
1155
1233
  const captureOutcomes = new Array<QaRunCommandOutcome>(contexts.length);
1156
1234
  const captureRecords = new Array<PageReviewContextRecord | undefined>(contexts.length);
1157
1235
  await runPool(contexts, concurrency, async (ctx, index) => {
1158
1236
  if (pastDeadline()) return;
1237
+ const allocation = allocations.get(ctx.id);
1238
+ if (!allocation) return;
1159
1239
  const outPrefix = join(outDir, `${ctx.id}-capture`);
1240
+ const allocationFile = `${outPrefix}-allocation.json`;
1241
+ writeFileSync(allocationFile, `${JSON.stringify(allocation)}\n`);
1160
1242
  const argv = [
1161
1243
  ...browseArgv,
1162
1244
  job.target,
@@ -1172,11 +1254,16 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
1172
1254
  ctx.theme,
1173
1255
  "--qa-state",
1174
1256
  ctx.state,
1175
- ...(visual === "scoped" ? ["--no-review-pack-bands"] : []),
1257
+ "--review-pack-reservation",
1258
+ allocationFile,
1259
+ "--review-pack-plan",
1260
+ ...manifestGateArgs,
1261
+ ...checks
1262
+ .filter((check) => check.contexts === undefined || check.contexts.includes(ctx.id))
1263
+ .flatMap((check) => check.args),
1176
1264
  ...scopeArgs,
1177
1265
  ...critiqueMaxTilesArgs,
1178
1266
  ...bandArgs,
1179
- ...hitRectArgs(ctx.id),
1180
1267
  ];
1181
1268
  log(
1182
1269
  `capture ${ctx.id}${scopeArgs.length > 0 ? ` [${manifest.scopes.map((s) => s.selector).join(",")}]` : ""}`,
@@ -1188,15 +1275,54 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
1188
1275
  const failures: string[] = [];
1189
1276
  let outcome: QaRunCommandOutcome["outcome"];
1190
1277
  let record: PageReviewContextRecord | undefined;
1191
- if (!res.error && res.exitCode === 0 && envelope && report?.context_id) {
1278
+ if (
1279
+ !res.error &&
1280
+ (res.exitCode === 0 || res.exitCode === 2) &&
1281
+ envelope &&
1282
+ report?.context_id
1283
+ ) {
1192
1284
  try {
1193
1285
  record = readPackContext(packDir, report.context_id);
1194
- outcome = "passed";
1286
+ const currentPlan = validatePageReviewCapturePlan(envelope.review_pack_capture_plan);
1287
+ const transaction = envelope.review_pack_transaction as
1288
+ | { source_digest?: string; attempts?: Array<{ status?: string }> }
1289
+ | undefined;
1290
+ if (
1291
+ !record.capture_plan ||
1292
+ record.id !== ctx.id ||
1293
+ capturePlanDigest(record.capture_plan) !== capturePlanDigest(currentPlan) ||
1294
+ transaction?.source_digest !== currentPlan.source_digest ||
1295
+ transaction.attempts?.at(-1)?.status !== "captured" ||
1296
+ record.tiles.length > allocation.selected_ids.length
1297
+ ) {
1298
+ throw new Error(
1299
+ "Fresh gates, source identity and native capture are not bound to the same transaction.",
1300
+ );
1301
+ }
1302
+ const freshFailures = [
1303
+ ...parseGateFailures(envelope),
1304
+ ...(enforceConsole ? parseConsoleFailures(envelope) : []),
1305
+ ];
1306
+ if (res.exitCode === 2 && !freshFailures.length)
1307
+ freshFailures.push("Fresh gate command returned exit code 2.");
1308
+ const preliminaryGate = gateOutcomes[index];
1309
+ if (!preliminaryGate) throw new Error("Missing context gate identity.");
1310
+ Object.assign(preliminaryGate, {
1311
+ argv,
1312
+ exit_code: res.exitCode,
1313
+ outcome: freshFailures.length ? "failed" : "passed",
1314
+ failures: freshFailures,
1315
+ artifacts: gatherArtifacts(outPrefix),
1316
+ wall_time_ms: wallTimeMs,
1317
+ });
1318
+ outcome = freshFailures.length ? "failed" : "passed";
1319
+ failures.push(...freshFailures);
1195
1320
  } catch (err: unknown) {
1196
1321
  outcome = "unknown";
1197
1322
  const reason = `pack context unreadable: ${err instanceof Error ? err.message : String(err)}`;
1198
1323
  failures.push(reason);
1199
1324
  blockers.push({ stage: "capture", context_id: ctx.id, reason });
1325
+ record = undefined;
1200
1326
  }
1201
1327
  } else {
1202
1328
  outcome = "unknown";
@@ -1274,8 +1400,60 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
1274
1400
  };
1275
1401
  for (const row of judged.contexts) {
1276
1402
  judgedById.set(row.context_id, row);
1403
+ const captured = capturedRecords.find((record) => record.id === row.context_id);
1404
+ if (captured) captured.evidence_context = row.record.evidence_context;
1277
1405
  let contextOutcome: QaRunCritiqueOutcome["outcome"] =
1278
1406
  row.outcome === "pass" ? "passed" : row.outcome === "fail" ? "failed" : "unknown";
1407
+ let dismissed: NonNullable<QaRunCritiqueOutcome["dismissed"]> = [];
1408
+ if (
1409
+ job.policy?.accept_dispositions === true &&
1410
+ (row.outcome === "pass" || row.outcome === "fail") &&
1411
+ row.tiles_unjudged === 0 &&
1412
+ !row.record.capture_incomplete &&
1413
+ !row.coverage.capped
1414
+ ) {
1415
+ try {
1416
+ const evidence = buildPackNativeEvidence(
1417
+ row.record,
1418
+ readPackTiles(packDir, row.record),
1419
+ DEFAULT_CRITIQUE_RUBRIC,
1420
+ );
1421
+ const slot = loadReviewDecisions(
1422
+ job.target,
1423
+ { viewport: row.record.viewport, theme: row.record.theme, state: row.record.state },
1424
+ options.snapshotStore ?? {},
1425
+ );
1426
+ const reviewed = applyPackReviewDecisions({
1427
+ target: job.target,
1428
+ evidence,
1429
+ findings: row.findings,
1430
+ slot,
1431
+ enabled: true,
1432
+ });
1433
+ dismissed = reviewed.applied;
1434
+ if (
1435
+ dismissed.length > 0 &&
1436
+ reviewed.findings.every((finding) => finding.severity !== "high")
1437
+ ) {
1438
+ contextOutcome = "passed";
1439
+ }
1440
+ } catch (err: unknown) {
1441
+ contextOutcome = "unknown";
1442
+ blockers.push({
1443
+ stage: "critique",
1444
+ context_id: row.context_id,
1445
+ reason: `reviewed disposition evidence unavailable: ${err instanceof Error ? err.message : String(err)}`,
1446
+ });
1447
+ }
1448
+ }
1449
+ if (row.record.capture_incomplete) {
1450
+ if (contextOutcome !== "failed") contextOutcome = "unknown";
1451
+ blockers.push({
1452
+ stage: "capture",
1453
+ context_id: row.context_id,
1454
+ reason: "native capture evidence is incomplete; recapture this context before signoff",
1455
+ });
1456
+ }
1279
1457
  if (row.outcome === "skipped") {
1280
1458
  const detail = row.error ?? "critique reported no conclusive outcome";
1281
1459
  blockers.push({
@@ -1292,13 +1470,13 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
1292
1470
  stage: "critique",
1293
1471
  context_id: row.context_id,
1294
1472
  reason:
1473
+ row.error ??
1295
1474
  `judge did not review ${row.tiles_unjudged} of ${row.tiles_total} tile(s) before the ` +
1296
- "run deadline; nothing is proven for them",
1475
+ "run deadline; nothing is proven for them",
1297
1476
  });
1298
1477
  }
1299
- // A capped capture holds the top of the page and nothing below it.
1300
- // Signoff cannot rest on that; review mode keeps the row honest via
1301
- // `coverage` and leaves the verdict to the tiles that were seen.
1478
+ // Selected bands may leave gaps anywhere. Signoff needs complete
1479
+ // coverage, while review mode reports what was actually captured.
1302
1480
  if (job.mode === "signoff" && row.coverage.capped && contextOutcome !== "failed") {
1303
1481
  contextOutcome = "unknown";
1304
1482
  blockers.push({
@@ -1307,7 +1485,7 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
1307
1485
  reason:
1308
1486
  `critique coverage capped: ${row.coverage.bands_reviewed} of ${row.coverage.bands_total} bands ` +
1309
1487
  `reviewed (${row.coverage.reviewed_height_px} of ${row.coverage.page_height_px} px); raise ` +
1310
- "policy.critique_max_tiles to review the whole page in signoff mode",
1488
+ "policy.critique_tile_budget (and any explicit per-context ceiling) for full signoff coverage",
1311
1489
  });
1312
1490
  }
1313
1491
  const scopeById = new Map(row.record.tiles.map((tile) => [tile.id, tile.scope]));
@@ -1318,6 +1496,7 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
1318
1496
  tiles_reviewed: row.tiles_reviewed,
1319
1497
  tiles_reused: row.tiles_reused,
1320
1498
  outcome: contextOutcome,
1499
+ ...(dismissed.length > 0 ? { dismissed } : {}),
1321
1500
  findings: row.findings.map((finding) => {
1322
1501
  const selector = scopeById.get(finding.tile_id);
1323
1502
  return {
@@ -1339,7 +1518,7 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
1339
1518
  // -------------------------------------------------------------- snapshot
1340
1519
  // Signoff persists each context's baseline from the pack's own files (no
1341
1520
  // browser). The critique rides along only when the whole tile set was
1342
- // freshly judged, uncapped, and unscoped — a partial or scoped review must
1521
+ // judged or safely reused, uncapped, and unscoped — a partial or scoped review must
1343
1522
  // never become the next baseline's finding record. When the manifest
1344
1523
  // required no visual pass, a dedicated browse --qa-snapshot pass still runs.
1345
1524
  enterStage("snapshot");
@@ -1352,15 +1531,15 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
1352
1531
  const persistCritique =
1353
1532
  row !== undefined &&
1354
1533
  (row.outcome === "pass" || row.outcome === "fail") &&
1534
+ !record.capture_incomplete &&
1355
1535
  !record.coverage.capped &&
1356
- row.tiles_reused === 0 &&
1357
1536
  row.tiles_unjudged === 0 &&
1358
1537
  record.scopes.length === 0;
1359
1538
  const persisted: PersistedCritique | undefined =
1360
1539
  persistCritique && row
1361
1540
  ? {
1362
- contract_version: QA_CRITIQUE_CONTRACT_VERSION,
1363
- rubric_digest: rubricDigest(DEFAULT_CRITIQUE_RUBRIC),
1541
+ contract_version: 2,
1542
+ rubric_digest: "",
1364
1543
  outcome: row.outcome as "pass" | "fail",
1365
1544
  findings: row.findings.map(({ tile_id: _tileId, ...finding }) => finding),
1366
1545
  tiles: record.tiles.map((tile) => ({
@@ -1374,13 +1553,22 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
1374
1553
  }
1375
1554
  : undefined;
1376
1555
  try {
1556
+ const tileEvidence = buildPackNativeEvidence(
1557
+ record,
1558
+ readPackTiles(packDir, record),
1559
+ DEFAULT_CRITIQUE_RUBRIC,
1560
+ );
1561
+ if (persisted) {
1562
+ persisted.contract_version = tileEvidence.context.critique_contract_version;
1563
+ persisted.rubric_digest = tileEvidence.context.rubric_digest;
1564
+ }
1377
1565
  const saved = saveQaSnapshot(
1378
1566
  job.target,
1379
1567
  { viewport: record.viewport, theme: record.theme, state: record.state },
1380
1568
  {
1381
1569
  signature: readPackSignature(packDir, record),
1382
1570
  domHtml: readPackDom(packDir, record),
1383
- screenshotPng: readPackFullPage(packDir, record),
1571
+ tileEvidence,
1384
1572
  ...(persisted ? { critique: persisted } : {}),
1385
1573
  },
1386
1574
  options.snapshotStore ?? {},