harnery 0.36.0 → 0.38.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 (432) hide show
  1. package/dist/commander.d.ts +9 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +10 -1
  4. package/dist/commands/admission.d.ts +21 -0
  5. package/dist/commands/admission.d.ts.map +1 -0
  6. package/dist/commands/admission.js +565 -0
  7. package/dist/commands/agents.d.ts +7 -0
  8. package/dist/commands/agents.d.ts.map +1 -1
  9. package/dist/commands/agents.js +148 -47
  10. package/dist/commands/artifacts.d.ts.map +1 -1
  11. package/dist/commands/artifacts.js +73 -2
  12. package/dist/commands/browse-ai.d.ts +2 -2
  13. package/dist/commands/browse-ai.d.ts.map +1 -1
  14. package/dist/commands/browse-ai.js +6 -4
  15. package/dist/commands/browse.d.ts.map +1 -1
  16. package/dist/commands/browse.js +360 -21
  17. package/dist/commands/diagnostics.d.ts +4 -0
  18. package/dist/commands/diagnostics.d.ts.map +1 -0
  19. package/dist/commands/diagnostics.js +229 -0
  20. package/dist/commands/fetch.js +1 -0
  21. package/dist/commands/files.d.ts +12 -0
  22. package/dist/commands/files.d.ts.map +1 -1
  23. package/dist/commands/files.js +54 -1
  24. package/dist/commands/governor.d.ts.map +1 -1
  25. package/dist/commands/governor.js +4 -0
  26. package/dist/commands/ledger-v3.d.ts.map +1 -1
  27. package/dist/commands/ledger-v3.js +71 -2
  28. package/dist/commands/qa-record.d.ts +144 -0
  29. package/dist/commands/qa-record.d.ts.map +1 -0
  30. package/dist/commands/qa-record.js +0 -0
  31. package/dist/commands/qa-run.d.ts +8 -0
  32. package/dist/commands/qa-run.d.ts.map +1 -0
  33. package/dist/commands/qa-run.js +396 -0
  34. package/dist/commands/qa-status.d.ts +71 -0
  35. package/dist/commands/qa-status.d.ts.map +1 -0
  36. package/dist/commands/qa-status.js +490 -0
  37. package/dist/commands/qa-verify.d.ts +40 -0
  38. package/dist/commands/qa-verify.d.ts.map +1 -0
  39. package/dist/commands/qa-verify.js +180 -0
  40. package/dist/commands/resources.d.ts +4 -0
  41. package/dist/commands/resources.d.ts.map +1 -0
  42. package/dist/commands/resources.js +29 -0
  43. package/dist/commands/review-pack.d.ts +4 -0
  44. package/dist/commands/review-pack.d.ts.map +1 -0
  45. package/dist/commands/review-pack.js +1001 -0
  46. package/dist/commands/supervisor.d.ts +4 -0
  47. package/dist/commands/supervisor.d.ts.map +1 -0
  48. package/dist/commands/supervisor.js +101 -0
  49. package/dist/commands/web.d.ts.map +1 -1
  50. package/dist/commands/web.js +17 -0
  51. package/dist/commands/workflow.d.ts.map +1 -1
  52. package/dist/commands/workflow.js +4 -0
  53. package/dist/core/agents/cli.js +7 -3
  54. package/dist/core/agents/health.d.ts +17 -0
  55. package/dist/core/agents/health.d.ts.map +1 -0
  56. package/dist/core/agents/health.js +109 -0
  57. package/dist/core/agents/mailbox.d.ts +92 -0
  58. package/dist/core/agents/mailbox.d.ts.map +1 -0
  59. package/dist/core/agents/mailbox.js +325 -0
  60. package/dist/core/agents/qa-signal.d.ts +111 -0
  61. package/dist/core/agents/qa-signal.d.ts.map +1 -0
  62. package/dist/core/agents/qa-signal.js +231 -0
  63. package/dist/core/agents/reconcile-coordination-v3.d.ts +44 -0
  64. package/dist/core/agents/reconcile-coordination-v3.d.ts.map +1 -0
  65. package/dist/core/agents/reconcile-coordination-v3.js +58 -0
  66. package/dist/core/agents/render/prompt-context.d.ts +5 -0
  67. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  68. package/dist/core/agents/render/prompt-context.js +34 -13
  69. package/dist/core/agents/render/session-context.d.ts +8 -0
  70. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  71. package/dist/core/agents/render/session-context.js +34 -3
  72. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  73. package/dist/core/agents/rules/commit-conflict.js +50 -1
  74. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  75. package/dist/core/agents/rules/stop-hook.js +6 -10
  76. package/dist/core/agents/session-finalizer-v3.js +4 -1
  77. package/dist/core/agents/session-name-display.d.ts +20 -5
  78. package/dist/core/agents/session-name-display.d.ts.map +1 -1
  79. package/dist/core/agents/session-name-display.js +67 -7
  80. package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
  81. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  82. package/dist/core/agents/state/heartbeat-writer.d.ts +11 -0
  83. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  84. package/dist/core/agents/state/heartbeat-writer.js +17 -0
  85. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  86. package/dist/core/agents/state/live-coordination-view.js +1 -0
  87. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  88. package/dist/core/artifacts/constants.d.ts +1 -1
  89. package/dist/core/artifacts/constants.js +1 -1
  90. package/dist/core/artifacts/index.d.ts +82 -6
  91. package/dist/core/artifacts/index.d.ts.map +1 -1
  92. package/dist/core/artifacts/index.js +410 -15
  93. package/dist/core/config.d.ts +20 -0
  94. package/dist/core/config.d.ts.map +1 -1
  95. package/dist/core/config.js +59 -0
  96. package/dist/core/diagnostics/advice.d.ts +9 -0
  97. package/dist/core/diagnostics/advice.d.ts.map +1 -0
  98. package/dist/core/diagnostics/advice.js +112 -0
  99. package/dist/core/diagnostics/bundle.d.ts +51 -0
  100. package/dist/core/diagnostics/bundle.d.ts.map +1 -0
  101. package/dist/core/diagnostics/bundle.js +968 -0
  102. package/dist/core/diagnostics/comparison.d.ts +5 -0
  103. package/dist/core/diagnostics/comparison.d.ts.map +1 -0
  104. package/dist/core/diagnostics/comparison.js +444 -0
  105. package/dist/core/diagnostics/contract.d.ts +245 -0
  106. package/dist/core/diagnostics/contract.d.ts.map +1 -0
  107. package/dist/core/diagnostics/contract.js +18 -0
  108. package/dist/core/diagnostics/identity.d.ts +3 -0
  109. package/dist/core/diagnostics/identity.d.ts.map +1 -0
  110. package/dist/core/diagnostics/identity.js +10 -0
  111. package/dist/core/diagnostics/index.d.ts +8 -0
  112. package/dist/core/diagnostics/index.d.ts.map +1 -0
  113. package/dist/core/diagnostics/index.js +7 -0
  114. package/dist/core/diagnostics/replay.d.ts +8 -0
  115. package/dist/core/diagnostics/replay.d.ts.map +1 -0
  116. package/dist/core/diagnostics/replay.js +213 -0
  117. package/dist/core/diagnostics/sanitize.d.ts +9 -0
  118. package/dist/core/diagnostics/sanitize.d.ts.map +1 -0
  119. package/dist/core/diagnostics/sanitize.js +84 -0
  120. package/dist/core/events/legacy-storage/compression.d.ts +13 -0
  121. package/dist/core/events/legacy-storage/compression.d.ts.map +1 -0
  122. package/dist/core/events/legacy-storage/compression.js +107 -0
  123. package/dist/core/events/legacy-storage/index.d.ts +1 -0
  124. package/dist/core/events/legacy-storage/index.d.ts.map +1 -1
  125. package/dist/core/events/legacy-storage/index.js +1 -0
  126. package/dist/core/events/v3/archive-retention.d.ts +33 -0
  127. package/dist/core/events/v3/archive-retention.d.ts.map +1 -0
  128. package/dist/core/events/v3/archive-retention.js +195 -0
  129. package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
  130. package/dist/core/events/v3/bootstrap.js +10 -0
  131. package/dist/core/events/v3/capabilities.d.ts +1 -1
  132. package/dist/core/events/v3/capabilities.d.ts.map +1 -1
  133. package/dist/core/events/v3/capabilities.js +1 -0
  134. package/dist/core/events/v3/coordination-view.d.ts +3 -0
  135. package/dist/core/events/v3/coordination-view.d.ts.map +1 -1
  136. package/dist/core/events/v3/coordination-view.js +69 -8
  137. package/dist/core/events/v3/index.d.ts +1 -0
  138. package/dist/core/events/v3/index.d.ts.map +1 -1
  139. package/dist/core/events/v3/index.js +1 -0
  140. package/dist/core/events/v3/live-routing.d.ts.map +1 -1
  141. package/dist/core/events/v3/live-routing.js +1 -0
  142. package/dist/core/events/v3/producers/hook-base.d.ts +1 -1
  143. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
  144. package/dist/core/events/v3/producers/hook-base.js +13 -2
  145. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  146. package/dist/core/events/v3/producers/intake.js +10 -2
  147. package/dist/core/events/v3/producers/recorder.d.ts +39 -0
  148. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  149. package/dist/core/events/v3/producers/recorder.js +350 -17
  150. package/dist/core/hooks/adapter/output.d.ts +8 -0
  151. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  152. package/dist/core/hooks/adapter/output.js +11 -1
  153. package/dist/core/hooks/cli.js +303 -219
  154. package/dist/core/hooks/health.d.ts +59 -0
  155. package/dist/core/hooks/health.d.ts.map +1 -0
  156. package/dist/core/hooks/health.js +84 -0
  157. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  158. package/dist/core/hooks/resolve/transcript.js +10 -3
  159. package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
  160. package/dist/core/hooks/session-name-presence.js +4 -1
  161. package/dist/core/qa-artifacts.d.ts +20 -0
  162. package/dist/core/qa-artifacts.d.ts.map +1 -0
  163. package/dist/core/qa-artifacts.js +110 -0
  164. package/dist/core/resources/contract.d.ts +96 -0
  165. package/dist/core/resources/contract.d.ts.map +1 -0
  166. package/dist/core/resources/contract.js +2 -0
  167. package/dist/core/resources/index.d.ts +6 -0
  168. package/dist/core/resources/index.d.ts.map +1 -0
  169. package/dist/core/resources/index.js +5 -0
  170. package/dist/core/resources/sampler.d.ts +34 -0
  171. package/dist/core/resources/sampler.d.ts.map +1 -0
  172. package/dist/core/resources/sampler.js +458 -0
  173. package/dist/core/resources/service-status.d.ts +3 -0
  174. package/dist/core/resources/service-status.d.ts.map +1 -0
  175. package/dist/core/resources/service-status.js +48 -0
  176. package/dist/core/resources/service.d.ts +26 -0
  177. package/dist/core/resources/service.d.ts.map +1 -0
  178. package/dist/core/resources/service.js +292 -0
  179. package/dist/core/resources/storage.d.ts +13 -0
  180. package/dist/core/resources/storage.d.ts.map +1 -0
  181. package/dist/core/resources/storage.js +30 -0
  182. package/dist/core/storage/atomic-json.d.ts +3 -0
  183. package/dist/core/storage/atomic-json.d.ts.map +1 -0
  184. package/dist/core/storage/atomic-json.js +15 -0
  185. package/dist/core/storage/builtins.d.ts.map +1 -1
  186. package/dist/core/storage/builtins.js +45 -6
  187. package/dist/core/storage/logger.d.ts +2 -0
  188. package/dist/core/storage/logger.d.ts.map +1 -1
  189. package/dist/core/storage/logger.js +2 -0
  190. package/dist/core/storage/query.d.ts +14 -0
  191. package/dist/core/storage/query.d.ts.map +1 -1
  192. package/dist/core/storage/query.js +51 -0
  193. package/dist/core/supervisor/activity.d.ts +4 -0
  194. package/dist/core/supervisor/activity.d.ts.map +1 -0
  195. package/dist/core/supervisor/activity.js +141 -0
  196. package/dist/core/supervisor/contract.d.ts +268 -0
  197. package/dist/core/supervisor/contract.d.ts.map +1 -0
  198. package/dist/core/supervisor/contract.js +35 -0
  199. package/dist/core/supervisor/explanations.d.ts +4 -0
  200. package/dist/core/supervisor/explanations.d.ts.map +1 -0
  201. package/dist/core/supervisor/explanations.js +71 -0
  202. package/dist/core/supervisor/findings.d.ts +20 -0
  203. package/dist/core/supervisor/findings.d.ts.map +1 -0
  204. package/dist/core/supervisor/findings.js +399 -0
  205. package/dist/core/supervisor/history.d.ts +9 -0
  206. package/dist/core/supervisor/history.d.ts.map +1 -0
  207. package/dist/core/supervisor/history.js +46 -0
  208. package/dist/core/supervisor/hook-health-alerts.d.ts +14 -0
  209. package/dist/core/supervisor/hook-health-alerts.d.ts.map +1 -0
  210. package/dist/core/supervisor/hook-health-alerts.js +70 -0
  211. package/dist/core/supervisor/hook-health-storage.d.ts +5 -0
  212. package/dist/core/supervisor/hook-health-storage.d.ts.map +1 -0
  213. package/dist/core/supervisor/hook-health-storage.js +24 -0
  214. package/dist/core/supervisor/hook-health.d.ts +72 -0
  215. package/dist/core/supervisor/hook-health.d.ts.map +1 -0
  216. package/dist/core/supervisor/hook-health.js +203 -0
  217. package/dist/core/supervisor/hooks.d.ts +10 -0
  218. package/dist/core/supervisor/hooks.d.ts.map +1 -0
  219. package/dist/core/supervisor/hooks.js +28 -0
  220. package/dist/core/supervisor/index.d.ts +15 -0
  221. package/dist/core/supervisor/index.d.ts.map +1 -0
  222. package/dist/core/supervisor/index.js +14 -0
  223. package/dist/core/supervisor/log-feed.d.ts +9 -0
  224. package/dist/core/supervisor/log-feed.d.ts.map +1 -0
  225. package/dist/core/supervisor/log-feed.js +103 -0
  226. package/dist/core/supervisor/service.d.ts +29 -0
  227. package/dist/core/supervisor/service.d.ts.map +1 -0
  228. package/dist/core/supervisor/service.js +450 -0
  229. package/dist/core/supervisor/services.d.ts +11 -0
  230. package/dist/core/supervisor/services.d.ts.map +1 -0
  231. package/dist/core/supervisor/services.js +155 -0
  232. package/dist/core/supervisor/status.d.ts +3 -0
  233. package/dist/core/supervisor/status.d.ts.map +1 -0
  234. package/dist/core/supervisor/status.js +48 -0
  235. package/dist/core/supervisor/storage.d.ts +30 -0
  236. package/dist/core/supervisor/storage.d.ts.map +1 -0
  237. package/dist/core/supervisor/storage.js +73 -0
  238. package/dist/core/supervisor/timeline.d.ts +3 -0
  239. package/dist/core/supervisor/timeline.d.ts.map +1 -0
  240. package/dist/core/supervisor/timeline.js +87 -0
  241. package/dist/core/workflow/admission.d.ts +23 -0
  242. package/dist/core/workflow/admission.d.ts.map +1 -0
  243. package/dist/core/workflow/admission.js +136 -0
  244. package/dist/core/workflow/engine.d.ts.map +1 -1
  245. package/dist/core/workflow/engine.js +77 -2
  246. package/dist/core/workflow/index.d.ts +3 -2
  247. package/dist/core/workflow/index.d.ts.map +1 -1
  248. package/dist/core/workflow/index.js +2 -1
  249. package/dist/core/workflow/proof.d.ts +2 -1
  250. package/dist/core/workflow/proof.d.ts.map +1 -1
  251. package/dist/core/workflow/proof.js +39 -1
  252. package/dist/core/workflow/run-state.d.ts +3 -2
  253. package/dist/core/workflow/run-state.d.ts.map +1 -1
  254. package/dist/core/workflow/run-state.js +8 -1
  255. package/dist/core/workflow/types.d.ts +33 -1
  256. package/dist/core/workflow/types.d.ts.map +1 -1
  257. package/dist/core/workflow/types.js +2 -1
  258. package/dist/lib/admission.d.ts +71 -0
  259. package/dist/lib/admission.d.ts.map +1 -0
  260. package/dist/lib/admission.js +264 -0
  261. package/dist/lib/agent-browser/client.d.ts +1 -1
  262. package/dist/lib/agent-browser/client.d.ts.map +1 -1
  263. package/dist/lib/agent-browser/client.js +1 -5
  264. package/dist/lib/browser/capture-fidelity.d.ts +39 -0
  265. package/dist/lib/browser/capture-fidelity.d.ts.map +1 -0
  266. package/dist/lib/browser/capture-fidelity.js +84 -0
  267. package/dist/lib/browser/client.d.ts +46 -1
  268. package/dist/lib/browser/client.d.ts.map +1 -1
  269. package/dist/lib/browser/client.js +168 -9
  270. package/dist/lib/browser/critique.d.ts +38 -1
  271. package/dist/lib/browser/critique.d.ts.map +1 -1
  272. package/dist/lib/browser/critique.js +34 -6
  273. package/dist/lib/browser/index.d.ts +5 -1
  274. package/dist/lib/browser/index.d.ts.map +1 -1
  275. package/dist/lib/browser/index.js +4 -0
  276. package/dist/lib/browser/page-review-judge.d.ts +64 -0
  277. package/dist/lib/browser/page-review-judge.d.ts.map +1 -0
  278. package/dist/lib/browser/page-review-judge.js +270 -0
  279. package/dist/lib/browser/page-review-pack.d.ts +613 -0
  280. package/dist/lib/browser/page-review-pack.d.ts.map +1 -0
  281. package/dist/lib/browser/page-review-pack.js +1751 -0
  282. package/dist/lib/browser/qa-run-contracts.d.ts +377 -0
  283. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -0
  284. package/dist/lib/browser/qa-run-contracts.js +361 -0
  285. package/dist/lib/browser/qa-run.d.ts +141 -0
  286. package/dist/lib/browser/qa-run.d.ts.map +1 -0
  287. package/dist/lib/browser/qa-run.js +1283 -0
  288. package/dist/lib/browser/request-diagnostics.d.ts +13 -0
  289. package/dist/lib/browser/request-diagnostics.d.ts.map +1 -0
  290. package/dist/lib/browser/request-diagnostics.js +18 -0
  291. package/dist/lib/browser/tiling.d.ts +19 -0
  292. package/dist/lib/browser/tiling.d.ts.map +1 -1
  293. package/dist/lib/browser/tiling.js +28 -0
  294. package/dist/lib/cookies/client.d.ts +9 -0
  295. package/dist/lib/cookies/client.d.ts.map +1 -1
  296. package/dist/lib/cookies/client.js +197 -44
  297. package/dist/lib/cookies/extra.d.ts +18 -0
  298. package/dist/lib/cookies/extra.d.ts.map +1 -0
  299. package/dist/lib/cookies/extra.js +14 -0
  300. package/dist/lib/cookies/index.d.ts +2 -1
  301. package/dist/lib/cookies/index.d.ts.map +1 -1
  302. package/dist/lib/cookies/index.js +2 -1
  303. package/dist/lib/coord-root-id.d.ts +9 -0
  304. package/dist/lib/coord-root-id.d.ts.map +1 -0
  305. package/dist/lib/coord-root-id.js +17 -0
  306. package/dist/lib/durable-job.d.ts +124 -0
  307. package/dist/lib/durable-job.d.ts.map +1 -0
  308. package/dist/lib/durable-job.js +296 -0
  309. package/dist/lib/http/client.d.ts +7 -1
  310. package/dist/lib/http/client.d.ts.map +1 -1
  311. package/dist/lib/http/client.js +2 -0
  312. package/dist/lib/instructions/apply.d.ts +3 -1
  313. package/dist/lib/instructions/apply.d.ts.map +1 -1
  314. package/dist/lib/instructions/apply.js +74 -3
  315. package/dist/lib/instructions/templates.d.ts.map +1 -1
  316. package/dist/lib/instructions/templates.js +11 -2
  317. package/package.json +19 -2
  318. package/src/commander.ts +75 -1
  319. package/src/commands/admission.ts +699 -0
  320. package/src/commands/agents.ts +189 -50
  321. package/src/commands/artifacts.ts +132 -18
  322. package/src/commands/browse-ai.ts +10 -5
  323. package/src/commands/browse.ts +496 -20
  324. package/src/commands/diagnostics.ts +271 -0
  325. package/src/commands/fetch.ts +1 -0
  326. package/src/commands/files.ts +102 -2
  327. package/src/commands/governor.ts +8 -0
  328. package/src/commands/ledger-v3.ts +82 -0
  329. package/src/commands/qa-record.ts +682 -0
  330. package/src/commands/qa-run.ts +503 -0
  331. package/src/commands/qa-status.ts +608 -0
  332. package/src/commands/qa-verify.ts +238 -0
  333. package/src/commands/resources.ts +38 -0
  334. package/src/commands/review-pack.ts +1281 -0
  335. package/src/commands/supervisor.ts +141 -0
  336. package/src/commands/web.ts +19 -0
  337. package/src/commands/workflow.ts +8 -0
  338. package/src/core/agents/cli.ts +7 -3
  339. package/src/core/agents/health.ts +142 -0
  340. package/src/core/agents/mailbox.ts +373 -0
  341. package/src/core/agents/qa-signal.ts +261 -0
  342. package/src/core/agents/reconcile-coordination-v3.ts +77 -0
  343. package/src/core/agents/render/prompt-context.ts +42 -12
  344. package/src/core/agents/render/session-context.ts +37 -3
  345. package/src/core/agents/rules/commit-conflict.ts +58 -1
  346. package/src/core/agents/rules/stop-hook.ts +6 -12
  347. package/src/core/agents/session-finalizer-v3.ts +4 -1
  348. package/src/core/agents/session-name-display.ts +78 -7
  349. package/src/core/agents/state/heartbeat-reader.ts +7 -0
  350. package/src/core/agents/state/heartbeat-writer.ts +23 -0
  351. package/src/core/agents/state/live-coordination-view.ts +1 -0
  352. package/src/core/agents/state/live-coordination-writer.ts +5 -0
  353. package/src/core/artifacts/constants.ts +1 -1
  354. package/src/core/artifacts/index.ts +565 -30
  355. package/src/core/config.ts +134 -2
  356. package/src/core/diagnostics/advice.ts +147 -0
  357. package/src/core/diagnostics/bundle.ts +1250 -0
  358. package/src/core/diagnostics/comparison.ts +565 -0
  359. package/src/core/diagnostics/contract.ts +311 -0
  360. package/src/core/diagnostics/identity.ts +11 -0
  361. package/src/core/diagnostics/index.ts +7 -0
  362. package/src/core/diagnostics/replay.ts +314 -0
  363. package/src/core/diagnostics/sanitize.ts +97 -0
  364. package/src/core/events/legacy-storage/compression.ts +130 -0
  365. package/src/core/events/legacy-storage/index.ts +1 -0
  366. package/src/core/events/v3/archive-retention.ts +251 -0
  367. package/src/core/events/v3/bootstrap.ts +10 -0
  368. package/src/core/events/v3/capabilities.ts +2 -0
  369. package/src/core/events/v3/coordination-view.ts +100 -11
  370. package/src/core/events/v3/index.ts +9 -0
  371. package/src/core/events/v3/live-routing.ts +1 -0
  372. package/src/core/events/v3/producers/hook-base.ts +14 -2
  373. package/src/core/events/v3/producers/intake.ts +10 -2
  374. package/src/core/events/v3/producers/recorder.ts +420 -17
  375. package/src/core/hooks/adapter/output.ts +12 -1
  376. package/src/core/hooks/cli.ts +332 -247
  377. package/src/core/hooks/health.ts +146 -0
  378. package/src/core/hooks/resolve/transcript.ts +10 -3
  379. package/src/core/hooks/session-name-presence.ts +6 -1
  380. package/src/core/qa-artifacts.ts +126 -0
  381. package/src/core/resources/contract.ts +105 -0
  382. package/src/core/resources/index.ts +5 -0
  383. package/src/core/resources/sampler.ts +590 -0
  384. package/src/core/resources/service-status.ts +62 -0
  385. package/src/core/resources/service.ts +337 -0
  386. package/src/core/resources/storage.ts +44 -0
  387. package/src/core/storage/atomic-json.ts +15 -0
  388. package/src/core/storage/builtins.ts +62 -6
  389. package/src/core/storage/logger.ts +2 -0
  390. package/src/core/storage/query.ts +58 -0
  391. package/src/core/supervisor/activity.ts +193 -0
  392. package/src/core/supervisor/contract.ts +312 -0
  393. package/src/core/supervisor/explanations.ts +104 -0
  394. package/src/core/supervisor/findings.ts +788 -0
  395. package/src/core/supervisor/history.ts +61 -0
  396. package/src/core/supervisor/hook-health-alerts.ts +108 -0
  397. package/src/core/supervisor/hook-health-storage.ts +25 -0
  398. package/src/core/supervisor/hook-health.ts +301 -0
  399. package/src/core/supervisor/hooks.ts +35 -0
  400. package/src/core/supervisor/index.ts +14 -0
  401. package/src/core/supervisor/log-feed.ts +131 -0
  402. package/src/core/supervisor/service.ts +511 -0
  403. package/src/core/supervisor/services.ts +203 -0
  404. package/src/core/supervisor/status.ts +59 -0
  405. package/src/core/supervisor/storage.ts +126 -0
  406. package/src/core/supervisor/timeline.ts +133 -0
  407. package/src/core/workflow/admission.ts +215 -0
  408. package/src/core/workflow/engine.ts +93 -1
  409. package/src/core/workflow/index.ts +10 -0
  410. package/src/core/workflow/proof.ts +54 -1
  411. package/src/core/workflow/run-state.ts +13 -1
  412. package/src/core/workflow/types.ts +39 -1
  413. package/src/lib/admission.ts +347 -0
  414. package/src/lib/agent-browser/client.ts +2 -10
  415. package/src/lib/browser/capture-fidelity.ts +98 -0
  416. package/src/lib/browser/client.ts +212 -10
  417. package/src/lib/browser/critique.ts +62 -7
  418. package/src/lib/browser/index.ts +65 -0
  419. package/src/lib/browser/page-review-judge.ts +360 -0
  420. package/src/lib/browser/page-review-pack.ts +2384 -0
  421. package/src/lib/browser/qa-run-contracts.ts +743 -0
  422. package/src/lib/browser/qa-run.ts +1462 -0
  423. package/src/lib/browser/request-diagnostics.ts +27 -0
  424. package/src/lib/browser/tiling.ts +32 -0
  425. package/src/lib/cookies/client.ts +228 -42
  426. package/src/lib/cookies/extra.ts +28 -0
  427. package/src/lib/cookies/index.ts +2 -0
  428. package/src/lib/coord-root-id.ts +23 -0
  429. package/src/lib/durable-job.ts +407 -0
  430. package/src/lib/http/client.ts +13 -1
  431. package/src/lib/instructions/apply.ts +81 -2
  432. package/src/lib/instructions/templates.ts +11 -2
@@ -2,8 +2,16 @@ import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSyn
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, isAbsolute, resolve } from "node:path";
4
4
  import type { Command } from "commander";
5
+ import { PNG } from "pngjs";
5
6
  import type { EmitContext, HarneryProgramContext } from "../commander.ts";
6
7
  import { resolveBinName } from "../core/config.ts";
8
+ import {
9
+ CAPTURE_FIDELITY_MISMATCH_THRESHOLD,
10
+ compareBand,
11
+ decideFidelity,
12
+ type FidelityProbe,
13
+ pngDimensions,
14
+ } from "../lib/browser/capture-fidelity.ts";
7
15
  import {
8
16
  type AssertResult,
9
17
  Browser,
@@ -11,6 +19,7 @@ import {
11
19
  browserProxyGateFromEnv,
12
20
  type ContentAnnotationBox,
13
21
  type ContentChecksResult,
22
+ type CritiqueCoverage,
14
23
  type CritiqueResult,
15
24
  type CritiqueTile,
16
25
  captureDevOverlay,
@@ -34,6 +43,16 @@ import {
34
43
  writeNetscapeCookieFile,
35
44
  wslHeadedLaunchArgs,
36
45
  } from "../lib/browser/index.ts";
46
+ import {
47
+ findPackTile,
48
+ type PageReviewCaptureFidelity,
49
+ type PageReviewContextRecord,
50
+ type PageReviewExpandedTileRecord,
51
+ readPackContext,
52
+ tileId,
53
+ writePackContext,
54
+ writePackExpandedTile,
55
+ } from "../lib/browser/page-review-pack.ts";
37
56
  import {
38
57
  buildQaManifest,
39
58
  classifySignatures,
@@ -62,7 +81,7 @@ import {
62
81
  type SaveBaselineResult,
63
82
  saveBaseline,
64
83
  } from "../lib/browser/visual-diff.ts";
65
- import { CookieJar } from "../lib/cookies/index.ts";
84
+ import { applyExtraCookies, CookieJar } from "../lib/cookies/index.ts";
66
85
 
67
86
  /**
68
87
  * `harn browse <url>`: Playwright-backed page navigation with shared
@@ -117,6 +136,8 @@ interface BrowseOpts {
117
136
  store?: string;
118
137
  profile?: string;
119
138
  viewport?: string;
139
+ deviceScaleFactor?: string;
140
+ colorScheme?: string;
120
141
  waitUntil: string;
121
142
  timeout: string;
122
143
  json?: boolean;
@@ -210,6 +231,16 @@ interface BrowseOpts {
210
231
  qaStates?: string[];
211
232
  qaReuse?: boolean;
212
233
  qaReuseThreshold?: string;
234
+ // Page review pack capture (page-review-pack.ts): tiles + DOM to disk, no
235
+ // vision call; the judge stage reviews the pack after the browser closed.
236
+ reviewPack?: string;
237
+ reviewPackContext?: string;
238
+ reviewPackScope?: string[];
239
+ reviewPackBands?: boolean;
240
+ /** Re-capture ONE existing tile of --review-pack-context at --device-scale-factor. */
241
+ reviewPackExpand?: string;
242
+ /** Gate-hit rectangles (`x,y,w,h` strings) whose bands are captured past the tile cap. */
243
+ reviewPackHitRect?: string[];
213
244
  // Next.js dev-overlay capture (auto-on; --no-dev-overlay opts out)
214
245
  devOverlay?: boolean;
215
246
  }
@@ -275,6 +306,12 @@ export function registerBrowseCommand(
275
306
  "Viewport: mobile (390x844), tablet (820x1180), desktop (1280x800), hd (1920x1080), or explicit '1920x1080'",
276
307
  "desktop",
277
308
  )
309
+ .option(
310
+ "--color-scheme <scheme>",
311
+ "Emulate prefers-color-scheme for the whole session: light | dark. " +
312
+ "Renders theme-aware pages in that scheme without page-specific toggles. " +
313
+ "Absent = browser default (unchanged behavior).",
314
+ )
278
315
  .option("--login", "Headed mode for one-time auth flow (cookies persist in profile)")
279
316
  .option(
280
317
  "--login-close-file <path>",
@@ -681,6 +718,48 @@ export function registerBrowseCommand(
681
718
  "Band-diff mismatch ratio at or below which a clean tile region is reused " +
682
719
  `(default ${DEFAULT_REUSE_MISMATCH_RATIO}; lower it toward 0 for stricter reuse).`,
683
720
  )
721
+ .option(
722
+ "--review-pack <dir>",
723
+ "Capture this render into a page review pack at <dir> (contexts/<id>/: full-page " +
724
+ "screenshot, critique tiles as PNG files, serialized DOM, QA signature) and make no " +
725
+ "vision call. A judge stage (`review-pack judge`, or qa-run) reviews the tiles from disk " +
726
+ "after every browser has closed. Tiling knobs are the --check-critique-* flags.",
727
+ )
728
+ .option(
729
+ "--review-pack-context <id>",
730
+ "Context id inside the pack (default <viewport>-<theme>-<state> from --viewport, " +
731
+ "--qa-theme, --qa-state).",
732
+ )
733
+ .option(
734
+ "--review-pack-scope <selector>",
735
+ "Also tile one screenshot per element matching <selector> into the pack (repeatable).",
736
+ (value: string, previous: string[] = []) => [...previous, value],
737
+ [] as string[],
738
+ )
739
+ .option(
740
+ "--no-review-pack-bands",
741
+ "Skip full-page bands in the pack and keep only --review-pack-scope tiles (a scoped " +
742
+ "visual pass).",
743
+ )
744
+ .option(
745
+ "--review-pack-expand <tile-id>",
746
+ "Instead of capturing a context, re-render ONE existing tile of --review-pack-context " +
747
+ "(e.g. T012) at --device-scale-factor and write it beside the original as " +
748
+ "tiles/<tile>@<dpr>x.png. Existing tiles are untouched.",
749
+ )
750
+ .option(
751
+ "--review-pack-hit-rect <x,y,w,h>",
752
+ "Document-space rectangle (integer px) a deterministic gate already flagged. Every " +
753
+ "full-page band that contains it is captured even past --check-critique-max-tiles, " +
754
+ `labelled \`hit band N\` (repeatable, at most ${REVIEW_PACK_HIT_RECT_MAX}).`,
755
+ (value: string, previous: string[] = []) => [...previous, value],
756
+ [] as string[],
757
+ )
758
+ .option(
759
+ "--device-scale-factor <n>",
760
+ "Device scale factor for the browser context (default 1). 2 renders every screenshot " +
761
+ "at twice the pixels; pair with --review-pack-expand for a sharper look at one tile.",
762
+ )
684
763
  .option(
685
764
  "--no-dev-overlay",
686
765
  "Skip auto-capture of Next.js dev-overlay issues. Default: capture every queued error (kind/code/message/stack) when a <nextjs-portal> shadow root is present. Necessary because Next.js 16 + React 19 route hydration errors + most React warnings through onCaughtError → next-devtools' errorQueue, NOT through console.error, so Playwright's standard listener doesn't see them. Surfaces them in the JSON envelope under `devOverlay`.",
@@ -732,8 +811,26 @@ async function runBrowse(
732
811
  opts.cookies === false
733
812
  ? null
734
813
  : new CookieJar({ path: opts.store ?? DEFAULT_STORE, source: "harn-browse" });
814
+ if (jar) applyExtraCookies(url, jar, context?.extraCookies);
735
815
  const headed = opts.login || opts.headed;
736
816
  const viewport = parseViewport(opts.viewport ?? "desktop");
817
+ const deviceScaleFactor = parseDeviceScaleFactor(opts.deviceScaleFactor);
818
+ if (opts.reviewPackExpand !== undefined) {
819
+ if (opts.reviewPack === undefined || opts.reviewPackContext === undefined) {
820
+ throw new Error(
821
+ "--review-pack-expand requires --review-pack <dir> and --review-pack-context <id>.",
822
+ );
823
+ }
824
+ }
825
+ // Malformed hit rects fail here, before a browser launches.
826
+ parseReviewPackHitRects(opts.reviewPackHitRect);
827
+ let colorScheme: "light" | "dark" | undefined;
828
+ if (opts.colorScheme !== undefined) {
829
+ if (opts.colorScheme !== "light" && opts.colorScheme !== "dark") {
830
+ throw new Error(`--color-scheme must be light or dark (got: ${opts.colorScheme}).`);
831
+ }
832
+ colorScheme = opts.colorScheme;
833
+ }
737
834
  const proxy = opts.proxyFromEnv ? browserProxyFromEnv() : undefined;
738
835
  const proxyGate = opts.proxyFromEnv ? browserProxyGateFromEnv() : null;
739
836
  if (opts.loginCloseFile && !opts.login) {
@@ -754,6 +851,8 @@ async function runBrowse(
754
851
  headed,
755
852
  jar,
756
853
  viewport,
854
+ ...(deviceScaleFactor !== undefined ? { deviceScaleFactor } : {}),
855
+ ...(colorScheme ? { colorScheme } : {}),
757
856
  navigationTimeout: Number.parseInt(opts.timeout, 10),
758
857
  waitUntil: opts.waitUntil as BrowseOpts["waitUntil"] as never,
759
858
  recordHarPath: opts.networkHar ? resolve(opts.networkHar) : undefined,
@@ -979,12 +1078,25 @@ async function runBrowse(
979
1078
  "info",
980
1079
  );
981
1080
  }
982
- critique = await runCritique({
983
- url: navResult.url,
984
- rubric,
985
- tiles: tilesToReview,
986
- provider: critiqueProvider,
987
- });
1081
+ critique = {
1082
+ ...(await runCritique({
1083
+ url: navResult.url,
1084
+ rubric,
1085
+ tiles: tilesToReview,
1086
+ provider: critiqueProvider,
1087
+ })),
1088
+ // Coverage rides on every envelope, skipped or not, so a reader can
1089
+ // tell a pass over the whole page from a pass over its first N bands.
1090
+ coverage: captured.coverage,
1091
+ };
1092
+ }
1093
+ // Page review pack capture: tiles, DOM, and signature to disk, no vision
1094
+ // call. Also before annotations, for the same reason as the critique.
1095
+ let reviewPack: ReviewPackReport | undefined;
1096
+ if (opts.reviewPack !== undefined && opts.reviewPackExpand !== undefined) {
1097
+ reviewPack = await expandReviewPackTile(browser, opts, deviceScaleFactor ?? 1);
1098
+ } else if (opts.reviewPack !== undefined) {
1099
+ reviewPack = await captureReviewPackContext(browser, url, navResult, opts, qaCapture);
988
1100
  }
989
1101
  // Persist the QA baseline AFTER critique so a passing run's verdicts ride
990
1102
  // along with the snapshot (still before annotations mutate the page).
@@ -997,6 +1109,7 @@ async function runBrowse(
997
1109
  critique?.provider &&
998
1110
  critique.outcome !== "skipped" &&
999
1111
  critiqueTiles !== undefined &&
1112
+ !critique.coverage?.capped &&
1000
1113
  (!qaReuse || qaReuse.tiles_reused === 0);
1001
1114
  const persistedCritique: PersistedCritique | undefined =
1002
1115
  fullCoverage && critique && critiqueTiles
@@ -1160,6 +1273,7 @@ async function runBrowse(
1160
1273
  batchResult,
1161
1274
  qaPlan,
1162
1275
  qaReuse ? summarizeReuse(qaReuse) : undefined,
1276
+ reviewPack,
1163
1277
  );
1164
1278
  } else {
1165
1279
  await runTrioMode(
@@ -1180,6 +1294,7 @@ async function runBrowse(
1180
1294
  batchResult,
1181
1295
  qaPlan,
1182
1296
  qaReuse ? summarizeReuse(qaReuse) : undefined,
1297
+ reviewPack,
1183
1298
  );
1184
1299
  }
1185
1300
 
@@ -1425,6 +1540,7 @@ async function runPrintMode(
1425
1540
  batchResult: BatchResult | undefined,
1426
1541
  qaPlan: QaPlanReport | undefined,
1427
1542
  qaReuse: Record<string, unknown> | undefined,
1543
+ reviewPack: ReviewPackReport | undefined,
1428
1544
  ): Promise<void> {
1429
1545
  let body: string | null = null;
1430
1546
  if (opts.html) {
@@ -1461,6 +1577,7 @@ async function runPrintMode(
1461
1577
  if (devOverlay) result.devOverlay = devOverlay;
1462
1578
  if (qaPlan) result.qaPlan = qaPlan;
1463
1579
  if (qaReuse) result.qaReuse = qaReuse;
1580
+ if (reviewPack) result.reviewPack = reviewPack;
1464
1581
  if (batchResult && batchResult.clipboardReads.length > 0) {
1465
1582
  result.batchClipboardReads = batchResult.clipboardReads;
1466
1583
  }
@@ -1499,6 +1616,7 @@ async function runTrioMode(
1499
1616
  batchResult: BatchResult | undefined,
1500
1617
  qaPlan: QaPlanReport | undefined,
1501
1618
  qaReuse: Record<string, unknown> | undefined,
1619
+ reviewPack: ReviewPackReport | undefined,
1502
1620
  ): Promise<void> {
1503
1621
  const prefix = resolveOutPrefix(opts.out);
1504
1622
  mkdirSync(dirname(prefix), { recursive: true });
@@ -1595,6 +1713,7 @@ async function runTrioMode(
1595
1713
  if (devOverlay) envelope.devOverlay = devOverlay;
1596
1714
  if (qaPlan) envelope.qaPlan = qaPlan;
1597
1715
  if (qaReuse) envelope.qaReuse = qaReuse;
1716
+ if (reviewPack) envelope.reviewPack = reviewPack;
1598
1717
  if (batchResult && batchResult.clipboardReads.length > 0) {
1599
1718
  envelope.batchClipboardReads = batchResult.clipboardReads;
1600
1719
  }
@@ -2088,12 +2207,14 @@ interface QaCaptureState {
2088
2207
  * supplies the live-page inputs (signature, page height, explicit scope match
2089
2208
  * counts). Classification and manifest math live in lib/browser/qa-plan.ts.
2090
2209
  */
2091
- async function runQaPlanning(
2210
+ /** Capture the live page's QA signature and serialized DOM for the store key
2211
+ * `--qa-target` (default: the url argument) and the labelled context. */
2212
+ async function captureQaState(
2092
2213
  browser: Browser,
2093
2214
  targetArg: string,
2094
2215
  renderedUrl: string,
2095
2216
  opts: BrowseOpts,
2096
- ): Promise<{ report: QaPlanReport; capture: QaCaptureState }> {
2217
+ ): Promise<QaCaptureState> {
2097
2218
  const context: QaContext = {
2098
2219
  viewport: opts.viewport ?? "desktop",
2099
2220
  theme: opts.qaTheme === "dark" ? "dark" : "light",
@@ -2108,11 +2229,22 @@ async function runQaPlanning(
2108
2229
  stylesheets: captured.stylesheets,
2109
2230
  ...(captured.truncated ? { truncated: true } : {}),
2110
2231
  };
2232
+ return { target, context, signature, domHtml: captured.domHtml };
2233
+ }
2234
+
2235
+ async function runQaPlanning(
2236
+ browser: Browser,
2237
+ targetArg: string,
2238
+ renderedUrl: string,
2239
+ opts: BrowseOpts,
2240
+ ): Promise<{ report: QaPlanReport; capture: QaCaptureState }> {
2241
+ const capture = await captureQaState(browser, targetArg, renderedUrl, opts);
2242
+ const { target, context, signature } = capture;
2111
2243
  const report: QaPlanReport = {
2112
2244
  signature: {
2113
- nodes: captured.nodes.length,
2114
- stylesheets: captured.stylesheets.length,
2115
- truncated: captured.truncated,
2245
+ nodes: signature.nodes.length,
2246
+ stylesheets: signature.stylesheets.length,
2247
+ truncated: Boolean(signature.truncated),
2116
2248
  },
2117
2249
  };
2118
2250
 
@@ -2150,20 +2282,324 @@ async function runQaPlanning(
2150
2282
  });
2151
2283
  }
2152
2284
 
2153
- return { report, capture: { target, context, signature, domHtml: captured.domHtml } };
2285
+ return { report, capture };
2154
2286
  }
2155
2287
 
2156
- async function captureCritiqueTiles(
2157
- browser: Browser,
2288
+ /** Tiling knobs shared by the critique and the review-pack capture. The
2289
+ * routed provider's vision long-edge budget caps band height so tiles are
2290
+ * never silently downscaled by the model. */
2291
+ function critiqueTilingKnobs(
2158
2292
  opts: BrowseOpts,
2159
2293
  tileBudgetPx?: number,
2160
- ): Promise<{ tiles: CritiqueTile[]; fullPage: Buffer }> {
2294
+ ): { maxTiles: number; band: number; overlap: number } {
2161
2295
  const maxTiles = Math.max(1, Number.parseInt(opts.checkCritiqueMaxTiles ?? "24", 10));
2162
- // The routed provider's vision long-edge budget caps band height so tiles
2163
- // are never silently downscaled by the model.
2164
2296
  const bandFlag = Math.max(200, Number.parseInt(opts.checkCritiqueBand ?? "1400", 10));
2165
2297
  const band = tileBudgetPx ? Math.max(200, Math.min(bandFlag, tileBudgetPx)) : bandFlag;
2166
2298
  const overlap = Math.max(0, Number.parseInt(opts.checkCritiqueOverlap ?? "120", 10));
2299
+ return { maxTiles, band, overlap };
2300
+ }
2301
+
2302
+ /** What `--review-pack` puts in the JSON envelope. */
2303
+ interface ReviewPackReport {
2304
+ dir: string;
2305
+ context_id: string;
2306
+ tiles: number;
2307
+ scopes: Array<{ selector: string; tiles: number }>;
2308
+ coverage: CritiqueCoverage;
2309
+ files: PageReviewContextRecord["files"];
2310
+ /** Bands captured past the tile cap for --review-pack-hit-rect rectangles. */
2311
+ hit_bands?: number;
2312
+ /** Where the tiles came from and what the fidelity probe saw. */
2313
+ capture_fidelity?: PageReviewCaptureFidelity;
2314
+ /** Capture warnings worth surfacing beside coverage (fidelity fallback, failed probes). */
2315
+ warnings?: string[];
2316
+ /** Set by --review-pack-expand: the one region that was re-captured. */
2317
+ expanded?: PageReviewExpandedTileRecord;
2318
+ }
2319
+
2320
+ /**
2321
+ * `--review-pack-expand`: re-render one existing tile of a pack context at
2322
+ * the context's device scale factor. The tile's CSS-pixel rect is captured by
2323
+ * scrolling the viewport to it and clipping (`captureRegionByScroll`), which
2324
+ * renders the region the way a reader sees it and yields tile size × DPR
2325
+ * pixels. A second full-page screenshot is not taken: it would reproduce the
2326
+ * very raster artifact the expand is meant to check. No other pack file
2327
+ * changes.
2328
+ */
2329
+ async function expandReviewPackTile(
2330
+ browser: Browser,
2331
+ opts: BrowseOpts,
2332
+ dpr: number,
2333
+ ): Promise<ReviewPackReport> {
2334
+ const packDir = opts.reviewPack as string;
2335
+ const contextId = opts.reviewPackContext as string;
2336
+ const tileId = opts.reviewPackExpand as string;
2337
+ const { tile } = findPackTile([readPackContext(packDir, contextId)], tileId, contextId);
2338
+ const region = await browser.captureRegionByScroll({
2339
+ x: tile.x,
2340
+ y: tile.scrollY,
2341
+ width: tile.width,
2342
+ height: tile.height,
2343
+ });
2344
+ const { record, expanded } = writePackExpandedTile(packDir, contextId, {
2345
+ tileId,
2346
+ dpr,
2347
+ region,
2348
+ });
2349
+ emit.log(
2350
+ `review-pack: ${record.id}/${tileId} expanded at ${dpr}× → ${expanded.width}×${expanded.height} px, ${expanded.file}`,
2351
+ "info",
2352
+ );
2353
+ return {
2354
+ dir: packDir,
2355
+ context_id: record.id,
2356
+ tiles: record.tiles.length,
2357
+ scopes: record.scopes,
2358
+ coverage: record.coverage,
2359
+ files: record.files,
2360
+ expanded,
2361
+ };
2362
+ }
2363
+
2364
+ /**
2365
+ * Capture-fidelity probe (capture-fidelity.ts). Every band is re-shot by
2366
+ * scrolling the viewport (`Browser.captureRegionByScroll`) and pixel compared
2367
+ * with the crop the tiler took from the full-page screenshot. When they all
2368
+ * agree the tiles stand and the record says `full-page`; when any disagrees,
2369
+ * every band is re-cut from its scrolled capture (same rects, same ids, only
2370
+ * the pixels change) and the record says `scrolled-bands`. A probe that
2371
+ * cannot run, or a re-capture that comes back the wrong size (a page wider
2372
+ * than the viewport), keeps the full-page crop for that band and says so in a
2373
+ * warning. Runs while the capture browser is open; the judge never opens one.
2374
+ */
2375
+ async function reconcileCaptureFidelity(
2376
+ browser: Browser,
2377
+ tiles: CritiqueTile[],
2378
+ ): Promise<{ tiles: CritiqueTile[]; fidelity: PageReviewCaptureFidelity; warnings: string[] }> {
2379
+ const threshold = CAPTURE_FIDELITY_MISMATCH_THRESHOLD;
2380
+ const warnings: string[] = [];
2381
+ const dpr = await browser.currentPage.evaluate(() => window.devicePixelRatio || 1);
2382
+ // Tile rects live in the full-page PNG's pixel space; the scroll capture
2383
+ // takes CSS px and renders at the page's DPR, so the two line up again.
2384
+ const cssRect = (tile: CritiqueTile) => ({
2385
+ x: (tile.x ?? 0) / dpr,
2386
+ y: tile.scrollY / dpr,
2387
+ width: tile.width / dpr,
2388
+ height: tile.height / dpr,
2389
+ });
2390
+ const shots = new Map<number, Buffer | undefined>();
2391
+ const shoot = async (position: number): Promise<Buffer | undefined> => {
2392
+ if (shots.has(position)) return shots.get(position);
2393
+ const tile = tiles[position] as CritiqueTile;
2394
+ let shot: Buffer | undefined;
2395
+ try {
2396
+ const captured = await browser.captureRegionByScroll(cssRect(tile));
2397
+ const size = pngDimensions(captured);
2398
+ if (Math.abs(size.width - tile.width) > 1 || Math.abs(size.height - tile.height) > 1) {
2399
+ warnings.push(
2400
+ `review-pack: scrolled capture of ${tileId(position)} came back ` +
2401
+ `${size.width}×${size.height} px for a ${tile.width}×${tile.height} px band; ` +
2402
+ "its full-page crop was kept",
2403
+ );
2404
+ } else {
2405
+ shot = captured;
2406
+ }
2407
+ } catch (err: unknown) {
2408
+ warnings.push(
2409
+ `review-pack: scrolled capture of ${tileId(position)} failed ` +
2410
+ `(${err instanceof Error ? err.message : String(err)}); its full-page crop was kept`,
2411
+ );
2412
+ }
2413
+ shots.set(position, shot);
2414
+ return shot;
2415
+ };
2416
+ // Every band is probed. A two-band sample (top and middle) missed a raster
2417
+ // artifact that sat elsewhere on the page, and once every band has been
2418
+ // shot by scrolling, the fallback below costs nothing extra: the same shots
2419
+ // become the tiles.
2420
+ const probes: FidelityProbe[] = [];
2421
+ for (let position = 0; position < tiles.length; position++) {
2422
+ const tile = tiles[position] as CritiqueTile;
2423
+ const shot = await shoot(position);
2424
+ if (!shot) continue;
2425
+ const cmp = compareBand(
2426
+ PNG.sync.read(Buffer.from(tile.pngBase64, "base64")),
2427
+ PNG.sync.read(shot),
2428
+ threshold,
2429
+ );
2430
+ probes.push({
2431
+ tile_id: tileId(position),
2432
+ scrollY: tile.scrollY,
2433
+ height: tile.height,
2434
+ mismatch_ratio: cmp.mismatch_ratio,
2435
+ });
2436
+ }
2437
+ const fidelity = decideFidelity(probes, threshold);
2438
+ if (fidelity.source === "full-page") return { tiles, fidelity, warnings };
2439
+ const recut: CritiqueTile[] = [];
2440
+ for (let position = 0; position < tiles.length; position++) {
2441
+ const tile = tiles[position] as CritiqueTile;
2442
+ const shot = await shoot(position);
2443
+ recut.push(shot ? { ...tile, pngBase64: shot.toString("base64") } : tile);
2444
+ }
2445
+ warnings.unshift(
2446
+ `review-pack: the full-page screenshot disagreed with the viewport render on ` +
2447
+ `${fidelity.mismatched.join(", ")} (mismatch ratio above ${threshold}); every band was ` +
2448
+ "re-captured by scrolling the viewport",
2449
+ );
2450
+ return { tiles: recut, fidelity, warnings };
2451
+ }
2452
+
2453
+ /**
2454
+ * Capture this render into a page review pack: one full-page screenshot,
2455
+ * banded tiles (unless --no-review-pack-bands), one tile set per
2456
+ * --review-pack-scope selector, the serialized DOM, and the QA signature —
2457
+ * all written to disk, with no vision call. The judge stage reads them back
2458
+ * once the browser is gone.
2459
+ */
2460
+ async function captureReviewPackContext(
2461
+ browser: Browser,
2462
+ targetArg: string,
2463
+ navResult: { url: string; title: string },
2464
+ opts: BrowseOpts,
2465
+ qaCapture: QaCaptureState | undefined,
2466
+ ): Promise<ReviewPackReport> {
2467
+ const packDir = opts.reviewPack as string;
2468
+ const { maxTiles, band, overlap } = critiqueTilingKnobs(opts);
2469
+ const buffer = await browser.fullPageScreenshotBuffer();
2470
+ let atoms: Awaited<ReturnType<Browser["visualAtoms"]>> | undefined;
2471
+ try {
2472
+ atoms = await browser.visualAtoms();
2473
+ } catch {
2474
+ atoms = undefined;
2475
+ }
2476
+ const tiling = { bandHeight: band, overlap, maxTiles, atoms };
2477
+ const withBands = opts.reviewPackBands !== false;
2478
+ // Gate-hit rectangles (qa-run lifts them from the gate stage, which runs
2479
+ // first) pull the bands they land in past the cap; those tiles follow the
2480
+ // kept bands as `hit band N`. Coverage still describes the kept prefix.
2481
+ const hitRects = parseReviewPackHitRects(opts.reviewPackHitRect);
2482
+ const banded = withBands
2483
+ ? tilesFromFullPage(buffer, { ...tiling, hitRects, maxHitBands: REVIEW_PACK_HIT_RECT_MAX })
2484
+ : undefined;
2485
+ if (banded?.coverage.capped) {
2486
+ emit.log(
2487
+ `review-pack: page banded to the ${maxTiles}-tile cap (raise --check-critique-max-tiles for full coverage)`,
2488
+ "warn",
2489
+ );
2490
+ }
2491
+ if (banded && banded.hitBands > 0) {
2492
+ emit.log(
2493
+ `review-pack: ${banded.hitBands} band(s) below the tile cap captured for ${hitRects.length} gate-hit rect(s)`,
2494
+ "info",
2495
+ );
2496
+ }
2497
+ // Fidelity: prove the full-page screenshot against scrolled viewport
2498
+ // renders while the browser is still open; on disagreement the bands are
2499
+ // re-cut from scrolled captures (see reconcileCaptureFidelity).
2500
+ let bandTiles = banded?.tiles ?? [];
2501
+ let captureFidelity: PageReviewCaptureFidelity | undefined;
2502
+ const fidelityWarnings: string[] = [];
2503
+ if (bandTiles.length > 0) {
2504
+ const reconciled = await reconcileCaptureFidelity(browser, bandTiles);
2505
+ bandTiles = reconciled.tiles;
2506
+ captureFidelity = reconciled.fidelity;
2507
+ fidelityWarnings.push(...reconciled.warnings);
2508
+ for (const warning of reconciled.warnings) emit.log(warning, "warn");
2509
+ emit.log(
2510
+ `review-pack: capture fidelity ${captureFidelity.source} ` +
2511
+ `(${captureFidelity.probed.length} band(s) probed, ${captureFidelity.mismatched.length} mismatched)`,
2512
+ "info",
2513
+ );
2514
+ }
2515
+ const scopeTiles: Array<{ selector: string; tiles: CritiqueTile[]; coverage: CritiqueCoverage }> =
2516
+ [];
2517
+ for (const selector of opts.reviewPackScope ?? []) {
2518
+ const elementRects = await browser.elementTiles(selector);
2519
+ const scoped = tilesFromFullPage(buffer, { ...tiling, elementRects });
2520
+ if (elementRects.length > maxTiles) {
2521
+ emit.log(
2522
+ `review-pack: ${elementRects.length} elements matched ${selector}, capped to ${maxTiles} tiles`,
2523
+ "warn",
2524
+ );
2525
+ }
2526
+ scopeTiles.push({ selector, tiles: scoped.tiles, coverage: scoped.coverage });
2527
+ }
2528
+ // Coverage: the bands when present, else the scopes folded together
2529
+ // (heights worst-case, band counts add, capped sticky).
2530
+ let coverage: CritiqueCoverage | undefined = banded?.coverage;
2531
+ if (!coverage) {
2532
+ for (const scope of scopeTiles) {
2533
+ coverage = coverage
2534
+ ? {
2535
+ page_height_px: Math.max(coverage.page_height_px, scope.coverage.page_height_px),
2536
+ reviewed_height_px: Math.min(
2537
+ coverage.reviewed_height_px,
2538
+ scope.coverage.reviewed_height_px,
2539
+ ),
2540
+ bands_total: coverage.bands_total + scope.coverage.bands_total,
2541
+ bands_reviewed: coverage.bands_reviewed + scope.coverage.bands_reviewed,
2542
+ capped: coverage.capped || scope.coverage.capped,
2543
+ }
2544
+ : scope.coverage;
2545
+ }
2546
+ }
2547
+ const page = await browser.currentPage.evaluate(() => ({
2548
+ width: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0),
2549
+ height: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0),
2550
+ }));
2551
+ const capture = qaCapture ?? (await captureQaState(browser, targetArg, navResult.url, opts));
2552
+ const record = writePackContext(packDir, {
2553
+ context: {
2554
+ ...capture.context,
2555
+ ...(opts.reviewPackContext ? { id: opts.reviewPackContext } : {}),
2556
+ },
2557
+ url: navResult.url,
2558
+ title: navResult.title,
2559
+ fullPage: buffer,
2560
+ pageWidth: page.width,
2561
+ pageHeight: page.height,
2562
+ tiles: bandTiles,
2563
+ coverage: coverage ?? {
2564
+ page_height_px: page.height,
2565
+ reviewed_height_px: 0,
2566
+ bands_total: 0,
2567
+ bands_reviewed: 0,
2568
+ capped: false,
2569
+ },
2570
+ scopeTiles: scopeTiles.map(({ selector, tiles }) => ({ selector, tiles })),
2571
+ signature: capture.signature,
2572
+ domHtml: capture.domHtml,
2573
+ ...(captureFidelity ? { captureFidelity } : {}),
2574
+ hitBands: banded?.hitBands ?? 0,
2575
+ });
2576
+ emit.log(
2577
+ `review-pack: ${record.id} → ${record.tiles.length} tile(s)` +
2578
+ (record.scopes.length > 0
2579
+ ? ` (${record.scopes.map((s) => `${s.selector}: ${s.tiles}`).join(", ")})`
2580
+ : "") +
2581
+ ` written to ${packDir}`,
2582
+ "info",
2583
+ );
2584
+ return {
2585
+ dir: packDir,
2586
+ context_id: record.id,
2587
+ tiles: record.tiles.length,
2588
+ scopes: record.scopes,
2589
+ coverage: record.coverage,
2590
+ files: record.files,
2591
+ ...(record.hit_bands !== undefined ? { hit_bands: record.hit_bands } : {}),
2592
+ ...(record.capture_fidelity ? { capture_fidelity: record.capture_fidelity } : {}),
2593
+ ...(fidelityWarnings.length > 0 ? { warnings: fidelityWarnings } : {}),
2594
+ };
2595
+ }
2596
+
2597
+ async function captureCritiqueTiles(
2598
+ browser: Browser,
2599
+ opts: BrowseOpts,
2600
+ tileBudgetPx?: number,
2601
+ ): Promise<{ tiles: CritiqueTile[]; fullPage: Buffer; coverage: CritiqueCoverage }> {
2602
+ const { maxTiles, band, overlap } = critiqueTilingKnobs(opts, tileBudgetPx);
2167
2603
 
2168
2604
  // Capture the whole page ONCE, then crop tiles from the pixels. Playwright's
2169
2605
  // clip is viewport-relative, so a below-fold band clip throws ("clipped area
@@ -2183,7 +2619,7 @@ async function captureCritiqueTiles(
2183
2619
  } catch {
2184
2620
  atoms = undefined;
2185
2621
  }
2186
- const tiles = tilesFromFullPage(buffer, {
2622
+ const { tiles, coverage } = tilesFromFullPage(buffer, {
2187
2623
  elementRects,
2188
2624
  bandHeight: band,
2189
2625
  overlap,
@@ -2202,7 +2638,7 @@ async function captureCritiqueTiles(
2202
2638
  "warn",
2203
2639
  );
2204
2640
  }
2205
- return { tiles, fullPage: buffer };
2641
+ return { tiles, fullPage: buffer, coverage };
2206
2642
  }
2207
2643
  function logCritiqueSummary(critique: CritiqueResult): void {
2208
2644
  if (critique.outcome === "skipped") {
@@ -2299,6 +2735,46 @@ async function waitForTerminalEnter(signal: AbortSignal): Promise<void> {
2299
2735
  });
2300
2736
  }
2301
2737
 
2738
+ /** Upper bound on --review-pack-hit-rect values (and on hit bands cut past the cap). */
2739
+ const REVIEW_PACK_HIT_RECT_MAX = 50;
2740
+
2741
+ /** `x,y,w,h` integer rectangles from repeated --review-pack-hit-rect flags. */
2742
+ function parseReviewPackHitRects(
2743
+ specs: string[] | undefined,
2744
+ ): Array<{ x: number; y: number; width: number; height: number }> {
2745
+ const list = specs ?? [];
2746
+ if (list.length > REVIEW_PACK_HIT_RECT_MAX) {
2747
+ throw new Error(
2748
+ `--review-pack-hit-rect accepts at most ${REVIEW_PACK_HIT_RECT_MAX} rectangles (got ${list.length}).`,
2749
+ );
2750
+ }
2751
+ return list.map((spec) => {
2752
+ const parts = spec.split(",").map((part) => part.trim());
2753
+ const nums = parts.map((part) => (/^-?\d+$/.test(part) ? Number.parseInt(part, 10) : NaN));
2754
+ if (parts.length !== 4 || nums.some((n) => !Number.isFinite(n))) {
2755
+ throw new Error(
2756
+ `--review-pack-hit-rect must be four integers x,y,w,h in document px (got: ${spec}).`,
2757
+ );
2758
+ }
2759
+ const [x, y, width, height] = nums as [number, number, number, number];
2760
+ if (width < 0 || height < 0) {
2761
+ throw new Error(
2762
+ `--review-pack-hit-rect width and height must not be negative (got: ${spec}).`,
2763
+ );
2764
+ }
2765
+ return { x, y, width, height };
2766
+ });
2767
+ }
2768
+
2769
+ function parseDeviceScaleFactor(spec: string | undefined): number | undefined {
2770
+ if (spec === undefined) return undefined;
2771
+ const value = Number.parseFloat(spec);
2772
+ if (!Number.isFinite(value) || value < 1 || value > 4) {
2773
+ throw new Error(`--device-scale-factor must be a number between 1 and 4 (got: ${spec})`);
2774
+ }
2775
+ return value;
2776
+ }
2777
+
2302
2778
  function parseViewport(spec: string): { width: number; height: number } {
2303
2779
  const preset = VIEWPORT_PRESETS[spec.toLowerCase()];
2304
2780
  if (preset) return preset;