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
@@ -14,9 +14,11 @@
14
14
 
15
15
  import { createHash } from "node:crypto";
16
16
  import {
17
+ copyFileSync,
17
18
  existsSync,
18
19
  lstatSync,
19
20
  mkdirSync,
21
+ mkdtempSync,
20
22
  readdirSync,
21
23
  readFileSync,
22
24
  renameSync,
@@ -28,9 +30,19 @@ import { basename, dirname, join, relative, resolve } from "node:path";
28
30
  import { gunzipSync, gzipSync } from "node:zlib";
29
31
  import { PNG } from "pngjs";
30
32
  import type { CritiqueCoverage, CritiqueFinding, CritiqueTile } from "./critique.js";
33
+ import type {
34
+ PageReviewBudgetCoverage,
35
+ PageReviewCapturePlan,
36
+ PageReviewEvidenceContext,
37
+ PageReviewNativeTile,
38
+ PageReviewRect,
39
+ } from "./page-review-contracts.js";
40
+ import { machineEvidenceDigest, PAGE_REVIEW_CRITIQUE_SCHEMA } from "./page-review-evidence.js";
31
41
  import type { QaContext, QaSignature } from "./qa-plan.js";
32
42
 
33
- export const PAGE_REVIEW_PACK_SCHEMA = "harnery-page-review/v1";
43
+ export type { PageReviewRect } from "./page-review-contracts.js";
44
+
45
+ export const PAGE_REVIEW_PACK_SCHEMA = "harnery-page-review/v2";
34
46
  export const PAGE_REVIEW_FINDINGS_SCHEMA = "harnery-page-review-findings/v3";
35
47
 
36
48
  /** Directory name a qa-run gives its pack inside the run directory. */
@@ -127,6 +139,7 @@ export interface PageReviewFindingsDocument {
127
139
  target: string;
128
140
  reviewer: string | null;
129
141
  reviewed_at: string | null;
142
+ machine_evidence_digest?: string | null;
130
143
  delegated_reviews: PageReviewDelegatedReviewRecord[];
131
144
  findings: PageReviewReviewerFinding[];
132
145
  dispositions?: PageReviewDispositionRecord[];
@@ -249,6 +262,14 @@ export interface PageReviewContextRecord {
249
262
  title?: string;
250
263
  captured_at: string;
251
264
  page: { width: number; height: number };
265
+ viewport_size: { width: number; height: number };
266
+ dpr: number;
267
+ recipe_version: string;
268
+ full_page_scale: 0.5;
269
+ capture_plan?: PageReviewCapturePlan;
270
+ allocation_coverage?: PageReviewBudgetCoverage;
271
+ evidence_context?: PageReviewEvidenceContext;
272
+ capture_incomplete?: boolean;
252
273
  /** Full-page band coverage (what the tiler kept of the page). */
253
274
  coverage: CritiqueCoverage;
254
275
  scopes: Array<{ selector: string; tiles: number }>;
@@ -277,6 +298,12 @@ export interface PageReviewContextCapture {
277
298
  fullPage: Buffer;
278
299
  pageWidth: number;
279
300
  pageHeight: number;
301
+ viewportSize: { width: number; height: number };
302
+ dpr: number;
303
+ recipeVersion: string;
304
+ capturePlan?: PageReviewCapturePlan;
305
+ allocationCoverage?: PageReviewBudgetCoverage;
306
+ captureIncomplete?: boolean;
280
307
  tiles: CritiqueTile[];
281
308
  coverage: CritiqueCoverage;
282
309
  scopeTiles?: Array<{ selector: string; tiles: CritiqueTile[] }>;
@@ -302,13 +329,11 @@ export interface PageReviewCritiqueRecord {
302
329
  findings: Array<CritiqueFinding & { tile_id: string }>;
303
330
  coverage: CritiqueCoverage;
304
331
  error?: string;
305
- }
306
-
307
- export interface PageReviewRect {
308
- x: number;
309
- y: number;
310
- width: number;
311
- height: number;
332
+ evidence?: {
333
+ schema: string;
334
+ context: PageReviewEvidenceContext;
335
+ tiles: Array<Omit<PageReviewNativeTile, "png">>;
336
+ };
312
337
  }
313
338
 
314
339
  /** One gate finding that carries a document-space rectangle (CSS px at the
@@ -515,6 +540,7 @@ export function writePackExpandedTile(
515
540
  const paths = packPaths(packDir);
516
541
  const dir = paths.contextDir(contextId);
517
542
  const file = join(dir, "tiles", expandedTileFilename(tile.id, input.dpr));
543
+ const scale = input.dpr / record.dpr;
518
544
  const { png, width, height } = input.region
519
545
  ? {
520
546
  png: input.region,
@@ -522,10 +548,10 @@ export function writePackExpandedTile(
522
548
  height: input.region.readUInt32BE(20),
523
549
  }
524
550
  : cropPngRegion(input.fullPage, {
525
- x: tile.x * input.dpr,
526
- y: tile.scrollY * input.dpr,
527
- width: tile.width * input.dpr,
528
- height: tile.height * input.dpr,
551
+ x: tile.x * scale,
552
+ y: tile.scrollY * scale,
553
+ width: tile.width * scale,
554
+ height: tile.height * scale,
529
555
  });
530
556
  mkdirSync(join(dir, "tiles"), { recursive: true });
531
557
  writeFileSync(file, png);
@@ -557,6 +583,17 @@ export function writePackContext(
557
583
  packDir: string,
558
584
  capture: PageReviewContextCapture,
559
585
  ): PageReviewContextRecord {
586
+ if (
587
+ !capture.viewportSize ||
588
+ ![capture.viewportSize.width, capture.viewportSize.height, capture.dpr].every(
589
+ (n) => Number.isFinite(n) && n > 0,
590
+ ) ||
591
+ !capture.recipeVersion?.trim()
592
+ ) {
593
+ throw new Error(
594
+ "Native pack capture requires viewportSize, positive DPR, and a capture recipe version",
595
+ );
596
+ }
560
597
  const contextId =
561
598
  capture.context.id ??
562
599
  `${capture.context.viewport}-${capture.context.theme}-${capture.context.state}`;
@@ -571,7 +608,14 @@ export function writePackContext(
571
608
  const signaturePath = join(dir, "signature.json");
572
609
  const tilesPath = join(dir, "tiles.json");
573
610
  const contextPath = join(dir, "context.json");
574
- writeFileSync(fullPagePath, capture.fullPage);
611
+ // Full-page pixels are transient. The saved image is orientation only.
612
+ const native = PNG.sync.read(capture.fullPage);
613
+ const overview = boxDownscale(
614
+ native,
615
+ Math.max(1, Math.round(native.width / 2)),
616
+ Math.max(1, Math.round(native.height / 2)),
617
+ );
618
+ writeFileSync(fullPagePath, PNG.sync.write(overview));
575
619
  // The serialized DOM compresses roughly 10:1; `dom_sha256` stays the digest
576
620
  // of the uncompressed bytes so a reader can verify what `readPackDom` returns.
577
621
  writeFileSync(domPath, gzipSync(Buffer.from(capture.domHtml, "utf8")));
@@ -615,7 +659,17 @@ export function writePackContext(
615
659
  url: capture.url,
616
660
  ...(capture.title !== undefined ? { title: capture.title } : {}),
617
661
  captured_at: capture.capturedAt ?? new Date().toISOString(),
618
- page: { width: capture.pageWidth, height: capture.pageHeight },
662
+ page: {
663
+ width: Math.round(capture.pageWidth * capture.dpr),
664
+ height: Math.round(capture.pageHeight * capture.dpr),
665
+ },
666
+ viewport_size: capture.viewportSize,
667
+ dpr: capture.dpr,
668
+ recipe_version: capture.recipeVersion,
669
+ full_page_scale: 0.5,
670
+ ...(capture.capturePlan ? { capture_plan: capture.capturePlan } : {}),
671
+ ...(capture.allocationCoverage ? { allocation_coverage: capture.allocationCoverage } : {}),
672
+ ...(capture.captureIncomplete ? { capture_incomplete: true } : {}),
619
673
  coverage: capture.coverage,
620
674
  scopes: (capture.scopeTiles ?? []).map((entry) => ({
621
675
  selector: entry.selector,
@@ -862,7 +916,9 @@ export function readPackContext(packDir: string, contextId: string): PageReviewC
862
916
  const path = join(packPaths(packDir).contextDir(contextId), "context.json");
863
917
  const { schema, ...record } = readJson<PageReviewContextRecord & { schema?: string }>(path);
864
918
  if (schema !== PAGE_REVIEW_PACK_SCHEMA) {
865
- throw new Error(`${path}: expected schema ${PAGE_REVIEW_PACK_SCHEMA}, found ${schema}`);
919
+ throw new Error(
920
+ `${path}: expected schema ${PAGE_REVIEW_PACK_SCHEMA}, found ${schema}; recapture the target to create a v2 pack`,
921
+ );
866
922
  }
867
923
  return record;
868
924
  }
@@ -897,7 +953,8 @@ export function readPackTiles(
897
953
  });
898
954
  }
899
955
 
900
- export function readPackFullPage(packDir: string, record: PageReviewContextRecord): Buffer {
956
+ /** Half-scale navigation image; never native evidence. */
957
+ export function readPackOverview(packDir: string, record: PageReviewContextRecord): Buffer {
901
958
  return readFileSync(join(packDir, record.files.full_page));
902
959
  }
903
960
 
@@ -917,7 +974,7 @@ export function readPackManifest(packDir: string): PageReviewPackManifest {
917
974
  const manifest = readJson<PageReviewPackManifest>(packPaths(packDir).manifest);
918
975
  if (manifest.schema !== PAGE_REVIEW_PACK_SCHEMA) {
919
976
  throw new Error(
920
- `${packPaths(packDir).manifest}: expected schema ${PAGE_REVIEW_PACK_SCHEMA}, found ${manifest.schema}`,
977
+ `${packPaths(packDir).manifest}: expected schema ${PAGE_REVIEW_PACK_SCHEMA}, found ${manifest.schema}; recapture the target to create a v2 pack`,
921
978
  );
922
979
  }
923
980
  return manifest;
@@ -974,6 +1031,7 @@ export function validateFindingsDocument(doc: unknown): string[] {
974
1031
  "target",
975
1032
  "reviewer",
976
1033
  "reviewed_at",
1034
+ "machine_evidence_digest",
977
1035
  "delegated_reviews",
978
1036
  "findings",
979
1037
  "dispositions",
@@ -1238,7 +1296,8 @@ export function machineFindingTargets(
1238
1296
  * critique: `evidence/critique.json` stays the machine record.
1239
1297
  */
1240
1298
  export function resolvePackVerdict(
1241
- manifest: Pick<PageReviewPackManifest, "critique">,
1299
+ manifest: Pick<PageReviewPackManifest, "critique"> &
1300
+ Partial<Pick<PageReviewPackManifest, "gates" | "contexts">>,
1242
1301
  inspectionPlan: PageReviewInspectionPlan,
1243
1302
  findings: PageReviewFindingsDocument,
1244
1303
  ): PageReviewVerdict {
@@ -1271,7 +1330,8 @@ export function resolvePackVerdict(
1271
1330
  high_total += 1;
1272
1331
  const disposition = byTarget.get(target)?.disposition;
1273
1332
  if (disposition === undefined) high_open += 1;
1274
- else if (disposition === "confirmed") high_confirmed += 1;
1333
+ else if (disposition === "confirmed" || finding.category === "provider-error")
1334
+ high_confirmed += 1;
1275
1335
  else high_dismissed += 1;
1276
1336
  }
1277
1337
  const reviewer_high = findings.findings.filter(
@@ -1292,8 +1352,20 @@ export function resolvePackVerdict(
1292
1352
  .filter((tile) => !completedTiles.has(tile))
1293
1353
  .sort();
1294
1354
 
1295
- const blocking = high_confirmed + high_open + reviewer_high;
1296
- const anyIncomplete = critique?.some((row) => row.outcome === "incomplete") ?? false;
1355
+ const blocking =
1356
+ high_confirmed +
1357
+ high_open +
1358
+ reviewer_high +
1359
+ (manifest.gates?.filter((g) => g.outcome === "failed").length ?? 0);
1360
+ const anyIncomplete =
1361
+ (critique?.some(
1362
+ (row) =>
1363
+ row.outcome === "incomplete" ||
1364
+ (row.outcome === "skipped" && machine_outcome !== "skipped"),
1365
+ ) ??
1366
+ false) ||
1367
+ manifest.contexts?.some((c) => c.capture_incomplete || c.coverage.capped) ||
1368
+ false;
1297
1369
  const reviewed_outcome: PageReviewVerdict["reviewed_outcome"] =
1298
1370
  blocking > 0
1299
1371
  ? "fail"
@@ -1359,6 +1431,7 @@ export function findingsSchemaDocument(): Record<string, unknown> {
1359
1431
  additionalProperties: false,
1360
1432
  required: ["schema", "target", "reviewer", "reviewed_at", "delegated_reviews", "findings"],
1361
1433
  properties: {
1434
+ machine_evidence_digest: { type: ["string", "null"], pattern: "^[a-f0-9]{64}$" },
1362
1435
  schema: { const: PAGE_REVIEW_FINDINGS_SCHEMA },
1363
1436
  schema_path: { type: "string" },
1364
1437
  target: { type: "string" },
@@ -1495,11 +1568,12 @@ function str(value: unknown, fallback = "?"): string {
1495
1568
  */
1496
1569
  export function gateHitsFromEnvelope(
1497
1570
  envelope: Record<string, unknown> | undefined,
1571
+ maxHits = GATE_HITS_PER_ENVELOPE,
1498
1572
  ): PageReviewGateHit[] {
1499
1573
  if (!envelope) return [];
1500
1574
  const hits: PageReviewGateHit[] = [];
1501
1575
  const push = (rule: string, label: string, rect: PageReviewRect | undefined): void => {
1502
- if (rect && hits.length < GATE_HITS_PER_ENVELOPE) hits.push({ rule, label, rect });
1576
+ if (rect && hits.length < maxHits) hits.push({ rule, label, rect });
1503
1577
  };
1504
1578
  const runts = envelope.runts as Record<string, unknown> | undefined;
1505
1579
  for (const hit of asArray(runts?.runts)) {
@@ -1663,8 +1737,7 @@ export function buildInspectionPlan(
1663
1737
  add(last.id, "page bottom: footer and final call to action");
1664
1738
  for (const tile of ctx.tiles) {
1665
1739
  if (tile.scope !== undefined) add(tile.id, `scoped tile for ${tile.scope}`);
1666
- if (tile.kind === "hit-band")
1667
- add(tile.id, "hit band: captured past the tile cap for a gate hit");
1740
+ if (tile.kind === "hit-band") add(tile.id, "hit band: selected for a gate hit");
1668
1741
  }
1669
1742
  const row = critique?.find((entry) => entry.context_id === ctx.id);
1670
1743
  for (const finding of row?.findings ?? []) {
@@ -1710,9 +1783,9 @@ function walkFiles(root: string, dir: string, out: Array<{ path: string; bytes:
1710
1783
 
1711
1784
  /**
1712
1785
  * Write the pack's navigation layer: manifest, evidence files, the findings
1713
- * skeleton and schema (never overwriting a findings file a reviewer already
1714
- * filled), and `review.md`. Safe to call twice: once after capture, again
1715
- * after the judge so machine findings land in the plan and the review.
1786
+ * skeleton and schema, and `review.md`. An unchanged judge result preserves
1787
+ * the review; changed machine evidence archives it before starting a fresh
1788
+ * review, so a previous finding position cannot acquire a new meaning.
1716
1789
  */
1717
1790
  export function finalizePageReviewPack(input: FinalizePageReviewPackInput): {
1718
1791
  manifest: string;
@@ -1726,6 +1799,52 @@ export function finalizePageReviewPack(input: FinalizePageReviewPackInput): {
1726
1799
  const gates = input.gates ?? [];
1727
1800
  const notChecked = [...(input.not_checked ?? [])];
1728
1801
  const warnings = [...(input.warnings ?? [])];
1802
+ const reviewDigest = critique === null ? null : machineEvidenceDigest(input.target, critique);
1803
+ const freshFindings = (): PageReviewFindingsDocument => ({
1804
+ schema: PAGE_REVIEW_FINDINGS_SCHEMA,
1805
+ schema_path: PAGE_REVIEW_FINDINGS_SCHEMA_FILENAME,
1806
+ target: input.target,
1807
+ machine_evidence_digest: reviewDigest,
1808
+ reviewer: null,
1809
+ reviewed_at: null,
1810
+ delegated_reviews: [],
1811
+ findings: [],
1812
+ dispositions: [],
1813
+ });
1814
+
1815
+ // Preserve the old machine evidence before any navigation files are replaced.
1816
+ // Archive failure aborts finalization without clearing the current review.
1817
+ if (critique !== null && existsSync(paths.findings)) {
1818
+ const previous = readPackFindings(input.packDir);
1819
+ if (previous.machine_evidence_digest !== reviewDigest) {
1820
+ if (
1821
+ previous.machine_evidence_digest ||
1822
+ previous.reviewer ||
1823
+ previous.reviewed_at ||
1824
+ previous.delegated_reviews.length ||
1825
+ previous.findings.length ||
1826
+ previous.dispositions?.length
1827
+ ) {
1828
+ const historyRoot = join(input.packDir, "review-history");
1829
+ mkdirSync(historyRoot, { recursive: true });
1830
+ const historyDir = mkdtempSync(join(historyRoot, "review-"));
1831
+ for (const source of [
1832
+ paths.findings,
1833
+ paths.critique,
1834
+ paths.inspectionPlan,
1835
+ paths.verdict,
1836
+ paths.manifest,
1837
+ ]) {
1838
+ if (existsSync(source)) copyFileSync(source, join(historyDir, basename(source)));
1839
+ }
1840
+ warnings.push(
1841
+ `Changed machine evidence requires a new review. Previous review archived at ${rel(historyDir)}.`,
1842
+ );
1843
+ }
1844
+ rmSync(paths.verdict, { force: true });
1845
+ writePackFindings(input.packDir, freshFindings());
1846
+ }
1847
+ }
1729
1848
 
1730
1849
  if (critique === null) {
1731
1850
  notChecked.push({
@@ -1750,7 +1869,7 @@ export function finalizePageReviewPack(input: FinalizePageReviewPackInput): {
1750
1869
  }
1751
1870
  if (ctx.hit_bands !== undefined && ctx.hit_bands > 0) {
1752
1871
  warnings.push(
1753
- `${ctx.id}: ${ctx.hit_bands} band(s) below the tile cap were captured because gate hits land in them.`,
1872
+ `${ctx.id}: ${ctx.hit_bands} band(s) were selected because gate hits land in them.`,
1754
1873
  );
1755
1874
  }
1756
1875
  }
@@ -1800,7 +1919,7 @@ export function finalizePageReviewPack(input: FinalizePageReviewPackInput): {
1800
1919
  });
1801
1920
  if (critique !== null) {
1802
1921
  writeJson(paths.critique, {
1803
- schema: PAGE_REVIEW_PACK_SCHEMA,
1922
+ schema: PAGE_REVIEW_CRITIQUE_SCHEMA,
1804
1923
  ...(input.pool ? { pool: input.pool } : {}),
1805
1924
  contexts: critique,
1806
1925
  });
@@ -1808,16 +1927,7 @@ export function finalizePageReviewPack(input: FinalizePageReviewPackInput): {
1808
1927
 
1809
1928
  writeJson(paths.findingsSchema, findingsSchemaDocument());
1810
1929
  if (!existsSync(paths.findings)) {
1811
- writeJson(paths.findings, {
1812
- schema: PAGE_REVIEW_FINDINGS_SCHEMA,
1813
- schema_path: PAGE_REVIEW_FINDINGS_SCHEMA_FILENAME,
1814
- target: input.target,
1815
- reviewer: null,
1816
- reviewed_at: null,
1817
- delegated_reviews: [],
1818
- findings: [],
1819
- dispositions: [],
1820
- });
1930
+ writeJson(paths.findings, freshFindings());
1821
1931
  }
1822
1932
 
1823
1933
  // Retention: what the caller says, else what the manifest already carries.
@@ -1880,9 +1990,8 @@ export function finalizePageReviewPack(input: FinalizePageReviewPackInput): {
1880
1990
  ),
1881
1991
  },
1882
1992
  });
1883
- // A verdict written by `review-pack verdict` survives every refinalize so
1884
- // review.md keeps its "Reviewed outcome" section; the section itself notes
1885
- // when the verdict predates the current critique.
1993
+ // A verdict survives an unchanged refinalize. Changed machine evidence
1994
+ // archives and removes it above, before starting the next review.
1886
1995
  let verdict: PageReviewVerdictDocument | undefined;
1887
1996
  try {
1888
1997
  verdict = readPackVerdict(input.packDir);
@@ -1930,7 +2039,7 @@ export function renderReviewMarkdown(
1930
2039
  lines.push("## Delegated review protocol");
1931
2040
  lines.push("");
1932
2041
  lines.push(
1933
- "1. The coordinating agent reads the context table and coverage below. A capped context has unreviewed page below its last tile.",
2042
+ "1. The coordinating agent reads the context table and coverage below. A budgeted context can have unreviewed gaps between selected tiles; use its recorded coverage intervals.",
1934
2043
  );
1935
2044
  lines.push(
1936
2045
  "2. Read `evidence/inspection-plan.json`, then dispatch review subagents with disjoint assignments covering every `primary_tiles` entry. Choose the first model available in this exact order: GPT-5.6 Luna (`gpt-5.6-luna` where an id is required), Composer 2.5, Haiku 4.5. Use that model for every tile-review subagent; do not substitute another model. If none exists, the review is incomplete. Each tile must be opened at native pixels by at least one completed subagent. The coordinating agent must not open tile images itself.",
@@ -2155,6 +2264,14 @@ export interface PageReviewExpiredStub {
2155
2264
  contexts: number;
2156
2265
  /** Aggregate of the manifest's critique outcomes; null when no judge ran. */
2157
2266
  machine_outcome: PageReviewCritiqueRecord["outcome"] | null;
2267
+ result: PageReviewVerdictDocument | null;
2268
+ findings_summary: {
2269
+ machine_high: number;
2270
+ machine_medium: number;
2271
+ machine_low: number;
2272
+ reviewer_findings: number;
2273
+ dispositions: number;
2274
+ } | null;
2158
2275
  }
2159
2276
 
2160
2277
  /** One pack as seen by the expiry sweep. `expires_at`, `size_bytes`, and
@@ -2295,6 +2412,25 @@ export function aggregateMachineOutcome(
2295
2412
  * `retention.expires_at` in the past is eligible, and an unmanaged pack
2296
2413
  * (explicit `--out`) only when `includeUnmanaged` is set.
2297
2414
  */
2415
+ function expiredFindingsSummary(
2416
+ dir: string,
2417
+ manifest: PageReviewPackManifest,
2418
+ ): PageReviewExpiredStub["findings_summary"] {
2419
+ try {
2420
+ const findings = readPackFindings(dir);
2421
+ const machine = manifest.critique?.flatMap((row) => row.findings) ?? [];
2422
+ return {
2423
+ machine_high: machine.filter((f) => f.severity === "high").length,
2424
+ machine_medium: machine.filter((f) => f.severity === "medium").length,
2425
+ machine_low: machine.filter((f) => f.severity === "low").length,
2426
+ reviewer_findings: findings.findings.length,
2427
+ dispositions: findings.dispositions?.length ?? 0,
2428
+ };
2429
+ } catch {
2430
+ return null;
2431
+ }
2432
+ }
2433
+
2298
2434
  export function deleteExpiredPacks(input: DeleteExpiredPacksInput): DeleteExpiredPacksResult {
2299
2435
  const now = input.now ?? new Date();
2300
2436
  const nowMs = now.getTime();
@@ -2329,6 +2465,8 @@ export function deleteExpiredPacks(input: DeleteExpiredPacksInput): DeleteExpire
2329
2465
  deleted_at: now.toISOString(),
2330
2466
  contexts: Array.isArray(manifest.contexts) ? manifest.contexts.length : 0,
2331
2467
  machine_outcome: aggregateMachineOutcome(manifest.critique),
2468
+ result: readPackVerdict(dir) ?? null,
2469
+ findings_summary: expiredFindingsSummary(dir, manifest),
2332
2470
  };
2333
2471
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
2334
2472
  if (entry.name === ARTIFACT_WORKSPACE_MANIFEST_FILENAME) continue;
@@ -0,0 +1,30 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import type { PageReviewSourceEvidence } from "./page-review-contracts.ts";
5
+
6
+ /** The caller supplies the already-canonicalized DOM. Keep every identity input
7
+ * and its order: this receipt diagnoses differences without accepting them. */
8
+ export function retainPageReviewSourceIdentity(
9
+ input: { nodes: unknown[]; stylesheets: unknown[]; dom: string },
10
+ artifactPrefix?: string,
11
+ ): { digest: string; evidence?: PageReviewSourceEvidence } {
12
+ const bytes = Buffer.from(
13
+ JSON.stringify({ nodes: input.nodes, stylesheets: input.stylesheets, dom: input.dom }),
14
+ "utf8",
15
+ );
16
+ const digest = createHash("sha256").update(bytes).digest("hex");
17
+ if (!artifactPrefix) return { digest };
18
+
19
+ // Raw source can contain session metadata. Keep it out of stdout and create
20
+ // a private directory per attempt, preserving both sides of a failed retry.
21
+ const prefix = resolve(artifactPrefix);
22
+ mkdirSync(dirname(prefix), { recursive: true, mode: 0o700 });
23
+ const dir = mkdtempSync(`${prefix}.source-identity-`);
24
+ const path = join(dir, `${digest}.json`);
25
+ writeFileSync(path, bytes, { mode: 0o600, flag: "wx" });
26
+ return {
27
+ digest,
28
+ evidence: { path, sha256: digest, bytes: bytes.length, encoding: "utf8" },
29
+ };
30
+ }
@@ -83,11 +83,13 @@ export interface QaRunPolicy {
83
83
  * recorded, and the result finalizes as incomplete — releasing the
84
84
  * admission slot instead of holding it open-endedly. */
85
85
  run_deadline_ms?: number;
86
- /** Full-page critique bands per context (browse `--check-critique-max-tiles`,
87
- * default 24). Raise it when a tall page must be reviewed end to end and the
88
- * per-tile cost is accepted; each critique row's `coverage` records what the
89
- * run actually saw, which is why this knob can stay out of the job digest. */
86
+ /** Explicit per-context ceiling. When absent, a pack context may use the
87
+ * whole shared budget; independent browse commands keep their own default. */
90
88
  critique_max_tiles?: number;
89
+ /** Maximum native tiles across the complete pack (8-400, default 96). */
90
+ critique_tile_budget?: number;
91
+ /** Apply exact-match, fully reviewed prior dispositions. Default false. */
92
+ accept_dispositions?: boolean;
91
93
  /** Vision calls in flight during the judge stage, across every context of
92
94
  * the run (1 to 16). Default: the host provider's own concurrency. The
93
95
  * capture stage closes every browser before judging starts, so this knob
@@ -154,6 +156,11 @@ export interface QaRunCritiqueCoverage {
154
156
  bands_total: number;
155
157
  bands_reviewed: number;
156
158
  capped: boolean;
159
+ selected_rectangles?: Array<{ x: number; y: number; width: number; height: number }>;
160
+ reviewed_intervals?: Array<{ start: number; end: number }>;
161
+ uncovered_intervals?: Array<{ start: number; end: number }>;
162
+ omitted_gate_hits?: string[];
163
+ omitted_scopes?: string[];
157
164
  }
158
165
 
159
166
  export interface QaRunCritiqueOutcome {
@@ -164,6 +171,8 @@ export interface QaRunCritiqueOutcome {
164
171
  tiles_reused: number;
165
172
  outcome: "passed" | "failed" | "unknown";
166
173
  findings: Array<{ severity: string; summary: string; selector?: string; tile?: string }>;
174
+ /** Applied reviewer decisions; the machine findings above remain unchanged. */
175
+ dismissed?: Array<{ finding_key: string; target: string; disposition: string }>;
167
176
  /** Tile coverage of the page. Absent when the capture died or reported none. */
168
177
  coverage?: QaRunCritiqueCoverage;
169
178
  }
@@ -490,6 +499,18 @@ export function validateQaRunJob(value: unknown): QaRunJobValidation {
490
499
 
491
500
  if (job.policy !== undefined) {
492
501
  const p = job.policy as Record<string, unknown>;
502
+ if (p === null || typeof p !== "object" || Array.isArray(p)) {
503
+ errors.push("policy must be an object");
504
+ }
505
+ if (p?.accept_dispositions !== undefined && typeof p.accept_dispositions !== "boolean") {
506
+ errors.push("policy.accept_dispositions must be a boolean");
507
+ }
508
+ if (p?.critique_tile_budget !== undefined) {
509
+ const n = p.critique_tile_budget;
510
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 8 || n > 400) {
511
+ errors.push("policy.critique_tile_budget must be an integer between 8 and 400");
512
+ }
513
+ }
493
514
  if (p?.command_concurrency !== undefined) {
494
515
  const n = p.command_concurrency;
495
516
  if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > 8) {
@@ -510,8 +531,8 @@ export function validateQaRunJob(value: unknown): QaRunJobValidation {
510
531
  }
511
532
  if (p?.critique_max_tiles !== undefined) {
512
533
  const n = p.critique_max_tiles;
513
- if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > 200) {
514
- errors.push("policy.critique_max_tiles must be an integer between 1 and 200");
534
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > 400) {
535
+ errors.push("policy.critique_max_tiles must be an integer between 1 and 400");
515
536
  }
516
537
  }
517
538
  if (p?.critique_pool !== undefined) {
@@ -545,7 +566,10 @@ export function contextIdFor(context: QaContext): string {
545
566
  /**
546
567
  * Merge planner-manifest coverage with the job's extra contexts. The manifest
547
568
  * is the floor: its contexts always run, in manifest order, and a job can only
548
- * append. The union construction makes narrowing impossible by design; the
569
+ * append coverage or supply setup arguments for an exactly matching context.
570
+ * IDs and rendering identities must agree; aliases are rejected rather than
571
+ * silently losing a setup or leaving a check pointed at an absent context.
572
+ * The union construction makes narrowing impossible by design; the
549
573
  * returned list is what the runner executes and what the result reports.
550
574
  */
551
575
  export function mergeCoverage(manifest: QaManifest, job: QaRunJob): QaRunContext[] {
@@ -555,13 +579,30 @@ export function mergeCoverage(manifest: QaManifest, job: QaRunJob): QaRunContext
555
579
  theme: context.theme,
556
580
  state: context.state,
557
581
  }));
558
- const seen = new Set(merged.map((context) => context.id));
582
+ const identity = (context: QaRunContext) =>
583
+ JSON.stringify([context.viewport, context.theme, context.state]);
584
+ const byId = new Map(merged.map((context) => [context.id, context]));
585
+ const byRender = new Map(merged.map((context) => [identity(context), context]));
586
+ const jobIds = new Set<string>();
559
587
  for (const context of job.contexts ?? []) {
560
- const canonical = contextIdFor(context);
561
- if (seen.has(canonical) || seen.has(context.id)) continue;
562
- seen.add(canonical);
563
- seen.add(context.id);
564
- merged.push(context);
588
+ if (jobIds.has(context.id)) throw new Error(`Duplicate QA context ID: ${context.id}`);
589
+ jobIds.add(context.id);
590
+ const existingId = byId.get(context.id);
591
+ const existingRender = byRender.get(identity(context));
592
+ if (existingId || existingRender) {
593
+ if (!existingId || existingId !== existingRender) {
594
+ throw new Error(`Conflicting QA context ID or rendering identity: ${context.id}`);
595
+ }
596
+ if (context.args !== undefined) existingId.args = [...context.args];
597
+ continue;
598
+ }
599
+ const added = {
600
+ ...context,
601
+ ...(context.args !== undefined ? { args: [...context.args] } : {}),
602
+ };
603
+ byId.set(added.id, added);
604
+ byRender.set(identity(added), added);
605
+ merged.push(added);
565
606
  }
566
607
  return merged;
567
608
  }
@@ -599,12 +640,15 @@ function canonicalize(value: unknown): unknown {
599
640
  }
600
641
 
601
642
  /** SHA-256 hex digest over the effective validated job (after the CLI merges
602
- * its authoritative `target` and `mode`). `policy` is excluded: concurrency,
603
- * timeout, and metered-critique knobs change how the run executes, not what
604
- * it proves, and CLI flags mutate them after the job file is read — including
605
- * them would make the same job file verify differently across invocations. */
643
+ * its authoritative `target` and `mode`). Runtime policy knobs are excluded.
644
+ * Disposition acceptance changes what can establish a pass, so enabling it
645
+ * is identity-bearing; an ordinary run cannot verify as an accepting run. */
606
646
  export function computeJobDigest(job: QaRunJob): string {
607
- const { policy: _policy, ...identityBearing } = job;
647
+ const { policy, ...rest } = job;
648
+ const identityBearing =
649
+ policy?.accept_dispositions === true
650
+ ? { ...rest, policy: { accept_dispositions: true } }
651
+ : rest;
608
652
  return createHash("sha256")
609
653
  .update(JSON.stringify(canonicalize(identityBearing)))
610
654
  .digest("hex");