harnery 0.45.1 → 0.46.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 (332) hide show
  1. package/dist/commander.d.ts +13 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commands/admission.d.ts.map +1 -1
  4. package/dist/commands/admission.js +214 -27
  5. package/dist/commands/agents.d.ts +4 -0
  6. package/dist/commands/agents.d.ts.map +1 -1
  7. package/dist/commands/agents.js +102 -25
  8. package/dist/commands/browse.d.ts +3 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +172 -8
  11. package/dist/commands/fetch.js +1 -0
  12. package/dist/commands/ledger-v3.d.ts.map +1 -1
  13. package/dist/commands/ledger-v3.js +8 -2
  14. package/dist/commands/presence.d.ts.map +1 -1
  15. package/dist/commands/presence.js +40 -0
  16. package/dist/commands/qa-run.d.ts.map +1 -1
  17. package/dist/commands/qa-run.js +4 -3
  18. package/dist/commands/review-pack.d.ts.map +1 -1
  19. package/dist/commands/review-pack.js +33 -6
  20. package/dist/commands/workflow.d.ts.map +1 -1
  21. package/dist/commands/workflow.js +6 -45
  22. package/dist/core/agents/coord-client.d.ts.map +1 -1
  23. package/dist/core/agents/coord-client.js +0 -18
  24. package/dist/core/agents/index.d.ts +1 -0
  25. package/dist/core/agents/index.d.ts.map +1 -1
  26. package/dist/core/agents/index.js +1 -0
  27. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  28. package/dist/core/agents/live-authority-v3.js +11 -4
  29. package/dist/core/agents/render/session-context.d.ts +0 -6
  30. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  31. package/dist/core/agents/render/session-context.js +1 -6
  32. package/dist/core/agents/session-events.d.ts.map +1 -1
  33. package/dist/core/agents/session-events.js +12 -4
  34. package/dist/core/agents/state/heartbeat-reader.d.ts +3 -0
  35. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  36. package/dist/core/agents/state/live-coordination-view.d.ts +3 -1
  37. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  38. package/dist/core/agents/state/live-coordination-view.js +6 -2
  39. package/dist/core/agents/state/parent-reference-contract.d.ts +16 -0
  40. package/dist/core/agents/state/parent-reference-contract.d.ts.map +1 -0
  41. package/dist/core/agents/state/parent-reference-contract.js +1 -0
  42. package/dist/core/agents/state/parent-reference.d.ts +18 -0
  43. package/dist/core/agents/state/parent-reference.d.ts.map +1 -0
  44. package/dist/core/agents/state/parent-reference.js +119 -0
  45. package/dist/core/agents/state/pidmap.js +2 -2
  46. package/dist/core/agents/state/proc-start.d.ts +20 -31
  47. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  48. package/dist/core/agents/state/proc-start.js +37 -70
  49. package/dist/core/agents/state/service-process.d.ts +61 -0
  50. package/dist/core/agents/state/service-process.d.ts.map +1 -0
  51. package/dist/core/agents/state/service-process.js +143 -0
  52. package/dist/core/config.d.ts +39 -0
  53. package/dist/core/config.d.ts.map +1 -1
  54. package/dist/core/config.js +63 -0
  55. package/dist/core/context/index.d.ts +6 -4
  56. package/dist/core/context/index.d.ts.map +1 -1
  57. package/dist/core/context/index.js +194 -256
  58. package/dist/core/context/observation.d.ts +9 -9
  59. package/dist/core/context/observation.d.ts.map +1 -1
  60. package/dist/core/context/observation.js +37 -6
  61. package/dist/core/diagnostics/path-identity.d.ts +1 -1
  62. package/dist/core/diagnostics/path-identity.d.ts.map +1 -1
  63. package/dist/core/diagnostics/path-identity.js +51 -6
  64. package/dist/core/events/v3/canonical.js +43 -14
  65. package/dist/core/events/v3/index.d.ts +1 -1
  66. package/dist/core/events/v3/index.d.ts.map +1 -1
  67. package/dist/core/events/v3/index.js +1 -1
  68. package/dist/core/events/v3/producers/recorder.d.ts +34 -0
  69. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  70. package/dist/core/events/v3/producers/recorder.js +117 -13
  71. package/dist/core/events/v3/reader.d.ts.map +1 -1
  72. package/dist/core/events/v3/reader.js +53 -13
  73. package/dist/core/events/v3/runtime-build.d.ts +14 -0
  74. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  75. package/dist/core/events/v3/runtime-build.js +44 -0
  76. package/dist/core/events/v3/validate.d.ts.map +1 -1
  77. package/dist/core/events/v3/validate.js +36 -1
  78. package/dist/core/governor/index.d.ts +2 -1
  79. package/dist/core/governor/index.d.ts.map +1 -1
  80. package/dist/core/governor/index.js +2 -1
  81. package/dist/core/governor/service-read.d.ts +3 -10
  82. package/dist/core/governor/service-read.d.ts.map +1 -1
  83. package/dist/core/governor/service-read.js +10 -94
  84. package/dist/core/governor/service.d.ts +3 -7
  85. package/dist/core/governor/service.d.ts.map +1 -1
  86. package/dist/core/governor/service.js +17 -182
  87. package/dist/core/hooks/adapter/native-parent.d.ts +23 -0
  88. package/dist/core/hooks/adapter/native-parent.d.ts.map +1 -0
  89. package/dist/core/hooks/adapter/native-parent.js +91 -0
  90. package/dist/core/hooks/cli.js +79 -4
  91. package/dist/core/hooks/resolve/owner.d.ts +0 -11
  92. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  93. package/dist/core/hooks/resolve/owner.js +2 -25
  94. package/dist/core/hooks/shell-admission.d.ts +84 -0
  95. package/dist/core/hooks/shell-admission.d.ts.map +1 -0
  96. package/dist/core/hooks/shell-admission.js +622 -0
  97. package/dist/core/hooks/shell-commit-tree.d.ts +2 -0
  98. package/dist/core/hooks/shell-commit-tree.d.ts.map +1 -0
  99. package/dist/core/hooks/shell-commit-tree.js +104 -0
  100. package/dist/core/inbox/contract.d.ts +12 -0
  101. package/dist/core/inbox/contract.d.ts.map +1 -1
  102. package/dist/core/inbox/contract.js +16 -0
  103. package/dist/core/inbox/peer-messages.d.ts +41 -16
  104. package/dist/core/inbox/peer-messages.d.ts.map +1 -1
  105. package/dist/core/inbox/peer-messages.js +372 -15
  106. package/dist/core/inbox/service.d.ts +8 -1
  107. package/dist/core/inbox/service.d.ts.map +1 -1
  108. package/dist/core/inbox/service.js +83 -3
  109. package/dist/core/inbox/targets.d.ts +17 -3
  110. package/dist/core/inbox/targets.d.ts.map +1 -1
  111. package/dist/core/inbox/targets.js +77 -2
  112. package/dist/core/presence/git.d.ts +18 -12
  113. package/dist/core/presence/git.d.ts.map +1 -1
  114. package/dist/core/presence/git.js +32 -38
  115. package/dist/core/presence/index.d.ts +2 -2
  116. package/dist/core/presence/index.d.ts.map +1 -1
  117. package/dist/core/presence/index.js +8 -6
  118. package/dist/core/presence/mail.d.ts +133 -0
  119. package/dist/core/presence/mail.d.ts.map +1 -0
  120. package/dist/core/presence/mail.js +464 -0
  121. package/dist/core/resources/sampler.js +2 -2
  122. package/dist/core/resources/service-status.d.ts +2 -14
  123. package/dist/core/resources/service-status.d.ts.map +1 -1
  124. package/dist/core/resources/service-status.js +3 -23
  125. package/dist/core/resources/service.d.ts +3 -7
  126. package/dist/core/resources/service.d.ts.map +1 -1
  127. package/dist/core/resources/service.js +12 -101
  128. package/dist/core/resources/status.d.ts.map +1 -1
  129. package/dist/core/resources/status.js +3 -2
  130. package/dist/core/semantic/contract.d.ts +5 -5
  131. package/dist/core/semantic/service.d.ts.map +1 -1
  132. package/dist/core/semantic/service.js +9 -2
  133. package/dist/core/servers/index.d.ts +0 -2
  134. package/dist/core/servers/index.d.ts.map +1 -1
  135. package/dist/core/servers/index.js +3 -18
  136. package/dist/core/storage/modes.d.ts +4 -0
  137. package/dist/core/storage/modes.d.ts.map +1 -1
  138. package/dist/core/storage/modes.js +4 -0
  139. package/dist/core/storage/segments.d.ts.map +1 -1
  140. package/dist/core/storage/segments.js +5 -4
  141. package/dist/core/supervisor/service.d.ts +4 -7
  142. package/dist/core/supervisor/service.d.ts.map +1 -1
  143. package/dist/core/supervisor/service.js +23 -122
  144. package/dist/core/supervisor/services.js +2 -2
  145. package/dist/core/supervisor/status.d.ts +3 -17
  146. package/dist/core/supervisor/status.d.ts.map +1 -1
  147. package/dist/core/supervisor/status.js +7 -70
  148. package/dist/core/workflow/engine.js +2 -1
  149. package/dist/core/workflow/index.d.ts +3 -4
  150. package/dist/core/workflow/index.d.ts.map +1 -1
  151. package/dist/core/workflow/index.js +2 -2
  152. package/dist/core/workflow/proof.d.ts.map +1 -1
  153. package/dist/core/workflow/proof.js +30 -74
  154. package/dist/core/workflow/resume-lock.d.ts +17 -0
  155. package/dist/core/workflow/resume-lock.d.ts.map +1 -0
  156. package/dist/core/workflow/resume-lock.js +158 -0
  157. package/dist/core/workflow/run-state.d.ts +0 -21
  158. package/dist/core/workflow/run-state.d.ts.map +1 -1
  159. package/dist/core/workflow/run-state.js +3 -174
  160. package/dist/core/workflow/workspaces/leases.d.ts +4 -0
  161. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  162. package/dist/core/workflow/workspaces/leases.js +1 -1
  163. package/dist/core/workflow/workspaces/local-git.js +91 -210
  164. package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
  165. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
  166. package/dist/core/workflow/workspaces/ownership.js +3 -130
  167. package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
  168. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  169. package/dist/core/workflow/workspaces/reclaim.js +0 -4
  170. package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
  171. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  172. package/dist/core/workflow/workspaces/recovery.js +1 -1
  173. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  174. package/dist/core/workflow/workspaces/state.js +1 -4
  175. package/dist/lib/admission-history.d.ts +96 -0
  176. package/dist/lib/admission-history.d.ts.map +1 -0
  177. package/dist/lib/admission-history.js +196 -0
  178. package/dist/lib/admission.d.ts +56 -7
  179. package/dist/lib/admission.d.ts.map +1 -1
  180. package/dist/lib/admission.js +161 -19
  181. package/dist/lib/browser/client.d.ts +18 -0
  182. package/dist/lib/browser/client.d.ts.map +1 -1
  183. package/dist/lib/browser/client.js +100 -12
  184. package/dist/lib/browser/content-checks.d.ts +3 -1
  185. package/dist/lib/browser/content-checks.d.ts.map +1 -1
  186. package/dist/lib/browser/content-checks.js +10 -2
  187. package/dist/lib/browser/critique.d.ts +32 -0
  188. package/dist/lib/browser/critique.d.ts.map +1 -1
  189. package/dist/lib/browser/critique.js +72 -3
  190. package/dist/lib/browser/geometry.d.ts +2 -0
  191. package/dist/lib/browser/geometry.d.ts.map +1 -1
  192. package/dist/lib/browser/geometry.js +61 -15
  193. package/dist/lib/browser/index.d.ts +1 -1
  194. package/dist/lib/browser/index.d.ts.map +1 -1
  195. package/dist/lib/browser/index.js +1 -1
  196. package/dist/lib/browser/page-review-contracts.d.ts +6 -1
  197. package/dist/lib/browser/page-review-contracts.d.ts.map +1 -1
  198. package/dist/lib/browser/page-review-contracts.js +6 -1
  199. package/dist/lib/browser/page-review-judge.d.ts +3 -1
  200. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  201. package/dist/lib/browser/page-review-judge.js +41 -11
  202. package/dist/lib/browser/page-review-pack.d.ts +3 -0
  203. package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
  204. package/dist/lib/browser/page-review-pack.js +5 -2
  205. package/dist/lib/browser/qa-run-contracts.d.ts +11 -0
  206. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  207. package/dist/lib/browser/qa-run-contracts.js +11 -4
  208. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  209. package/dist/lib/browser/qa-run.js +73 -16
  210. package/dist/lib/browser/runts.d.ts +4 -2
  211. package/dist/lib/browser/runts.d.ts.map +1 -1
  212. package/dist/lib/browser/runts.js +84 -33
  213. package/dist/lib/browser/windows-chrome.d.ts +14 -0
  214. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  215. package/dist/lib/browser/windows-chrome.js +42 -0
  216. package/dist/lib/browser/windows-login-sync.d.ts +110 -0
  217. package/dist/lib/browser/windows-login-sync.d.ts.map +1 -0
  218. package/dist/lib/browser/windows-login-sync.js +171 -0
  219. package/dist/lib/cookies/client.d.ts +30 -5
  220. package/dist/lib/cookies/client.d.ts.map +1 -1
  221. package/dist/lib/cookies/client.js +55 -10
  222. package/dist/lib/cookies/index.d.ts +1 -1
  223. package/dist/lib/cookies/index.d.ts.map +1 -1
  224. package/dist/lib/cookies/index.js +1 -1
  225. package/dist/lib/docs-metadata-sync.d.ts.map +1 -1
  226. package/dist/lib/docs-metadata-sync.js +41 -2
  227. package/dist/lib/durable-job.d.ts +8 -0
  228. package/dist/lib/durable-job.d.ts.map +1 -1
  229. package/dist/lib/durable-job.js +10 -0
  230. package/dist/lib/http/client.d.ts +9 -8
  231. package/dist/lib/http/client.d.ts.map +1 -1
  232. package/dist/lib/http/client.js +44 -2
  233. package/dist/lib/instructions/templates.js +4 -4
  234. package/package.json +2 -2
  235. package/src/commander.ts +13 -0
  236. package/src/commands/admission.ts +272 -29
  237. package/src/commands/agents.ts +132 -27
  238. package/src/commands/browse.ts +208 -6
  239. package/src/commands/fetch.ts +1 -0
  240. package/src/commands/ledger-v3.ts +8 -2
  241. package/src/commands/presence.ts +49 -0
  242. package/src/commands/qa-run.ts +4 -3
  243. package/src/commands/review-pack.ts +41 -5
  244. package/src/commands/workflow.ts +8 -58
  245. package/src/core/agents/coord-client.ts +0 -20
  246. package/src/core/agents/index.ts +6 -0
  247. package/src/core/agents/live-authority-v3.ts +11 -4
  248. package/src/core/agents/render/session-context.ts +1 -7
  249. package/src/core/agents/session-events.ts +18 -3
  250. package/src/core/agents/state/heartbeat-reader.ts +3 -0
  251. package/src/core/agents/state/live-coordination-view.ts +13 -6
  252. package/src/core/agents/state/parent-reference-contract.ts +14 -0
  253. package/src/core/agents/state/parent-reference.ts +179 -0
  254. package/src/core/agents/state/pidmap.ts +2 -2
  255. package/src/core/agents/state/proc-start.ts +41 -69
  256. package/src/core/agents/state/service-process.ts +217 -0
  257. package/src/core/config.ts +85 -0
  258. package/src/core/context/index.ts +207 -272
  259. package/src/core/context/observation.ts +40 -9
  260. package/src/core/diagnostics/path-identity.ts +52 -6
  261. package/src/core/events/v3/canonical.ts +46 -13
  262. package/src/core/events/v3/index.ts +3 -0
  263. package/src/core/events/v3/producers/recorder.ts +151 -13
  264. package/src/core/events/v3/reader.ts +55 -11
  265. package/src/core/events/v3/runtime-build.ts +62 -0
  266. package/src/core/events/v3/validate.ts +42 -1
  267. package/src/core/governor/index.ts +1 -2
  268. package/src/core/governor/service-read.ts +15 -114
  269. package/src/core/governor/service.ts +36 -220
  270. package/src/core/hooks/adapter/native-parent.ts +109 -0
  271. package/src/core/hooks/cli.ts +83 -4
  272. package/src/core/hooks/resolve/owner.ts +2 -25
  273. package/src/core/hooks/shell-admission.ts +634 -0
  274. package/src/core/hooks/shell-commit-tree.ts +102 -0
  275. package/src/core/inbox/contract.ts +18 -0
  276. package/src/core/inbox/peer-messages.ts +501 -15
  277. package/src/core/inbox/service.ts +108 -3
  278. package/src/core/inbox/targets.ts +94 -3
  279. package/src/core/presence/git.ts +45 -37
  280. package/src/core/presence/index.ts +16 -8
  281. package/src/core/presence/mail.ts +620 -0
  282. package/src/core/resources/sampler.ts +3 -2
  283. package/src/core/resources/service-status.ts +6 -35
  284. package/src/core/resources/service.ts +23 -133
  285. package/src/core/resources/status.ts +3 -2
  286. package/src/core/semantic/service.ts +8 -2
  287. package/src/core/servers/index.ts +3 -23
  288. package/src/core/storage/modes.ts +6 -0
  289. package/src/core/storage/segments.ts +5 -4
  290. package/src/core/supervisor/service.ts +33 -155
  291. package/src/core/supervisor/services.ts +2 -2
  292. package/src/core/supervisor/status.ts +10 -85
  293. package/src/core/workflow/engine.ts +1 -1
  294. package/src/core/workflow/index.ts +2 -13
  295. package/src/core/workflow/proof.ts +35 -83
  296. package/src/core/workflow/resume-lock.ts +179 -0
  297. package/src/core/workflow/run-state.ts +3 -207
  298. package/src/core/workflow/workspaces/leases.ts +1 -1
  299. package/src/core/workflow/workspaces/local-git.ts +120 -239
  300. package/src/core/workflow/workspaces/ownership.ts +3 -158
  301. package/src/core/workflow/workspaces/reclaim.ts +3 -9
  302. package/src/core/workflow/workspaces/recovery.ts +1 -1
  303. package/src/core/workflow/workspaces/state.ts +1 -5
  304. package/src/lib/admission-history.ts +267 -0
  305. package/src/lib/admission.ts +217 -19
  306. package/src/lib/browser/client.ts +147 -35
  307. package/src/lib/browser/content-checks.ts +14 -3
  308. package/src/lib/browser/critique.ts +113 -3
  309. package/src/lib/browser/geometry.ts +63 -14
  310. package/src/lib/browser/index.ts +5 -0
  311. package/src/lib/browser/page-review-contracts.ts +6 -1
  312. package/src/lib/browser/page-review-judge.ts +58 -13
  313. package/src/lib/browser/page-review-pack.ts +13 -2
  314. package/src/lib/browser/qa-run-contracts.ts +30 -4
  315. package/src/lib/browser/qa-run.ts +78 -17
  316. package/src/lib/browser/runts.ts +85 -37
  317. package/src/lib/browser/windows-chrome.ts +50 -0
  318. package/src/lib/browser/windows-login-sync.ts +275 -0
  319. package/src/lib/cookies/client.ts +62 -10
  320. package/src/lib/cookies/index.ts +3 -0
  321. package/src/lib/docs-metadata-sync.ts +44 -2
  322. package/src/lib/durable-job.ts +15 -0
  323. package/src/lib/http/client.ts +63 -1
  324. package/src/lib/instructions/templates.ts +4 -4
  325. package/dist/core/agents/state/process-handle.d.ts +0 -26
  326. package/dist/core/agents/state/process-handle.d.ts.map +0 -1
  327. package/dist/core/agents/state/process-handle.js +0 -121
  328. package/dist/core/workflow/recovery-audit.d.ts +0 -30
  329. package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
  330. package/dist/core/workflow/recovery-audit.js +0 -116
  331. package/src/core/agents/state/process-handle.ts +0 -165
  332. package/src/core/workflow/recovery-audit.ts +0 -181
@@ -18,6 +18,8 @@
18
18
  // Toolkit tier: this module must not import src/core (layering check).
19
19
 
20
20
  import { createHash } from "node:crypto";
21
+ import type { CritiqueUsage, CritiqueUsageTotals } from "./critique.js";
22
+ import { PAGE_REVIEW_MAX_TILE_BUDGET } from "./page-review-contracts.js";
21
23
  import type { QaContext, QaManifest } from "./qa-plan.js";
22
24
 
23
25
  export const QA_RUN_JOB_SCHEMA_VERSION = 1 as const;
@@ -152,6 +154,9 @@ export interface QaRunCritiqueTileTiming {
152
154
  tile_id: string;
153
155
  duration_ms: number;
154
156
  outcome: "passed" | "failed";
157
+ /** Tokens the provider reported for this tile, retries included. Absent
158
+ * when the backend reports none; that is unknown, not zero. */
159
+ usage?: CritiqueUsage;
155
160
  }
156
161
 
157
162
  /** What share of the page the critique tiles covered, lifted from the browse
@@ -184,6 +189,11 @@ export interface QaRunCritiqueOutcome {
184
189
  dismissed?: Array<{ finding_key: string; target: string; disposition: string }>;
185
190
  /** Tile coverage of the page. Absent when the capture died or reported none. */
186
191
  coverage?: QaRunCritiqueCoverage;
192
+ /** `full`: every band reviewed. `capped`: bands left out; a signoff blocker.
193
+ * `sampled`: signoff on a page whose complete review would exceed the
194
+ * per-run tile ceiling; every edge band, gate hit, and required scope was
195
+ * reviewed and the uncovered intervals are listed in `coverage` (ADR 0224). */
196
+ coverage_mode?: "full" | "capped" | "sampled";
187
197
  }
188
198
 
189
199
  /** The judge stage as one unit: every context's tiles through one bounded
@@ -199,6 +209,8 @@ export interface QaRunCritiquePool {
199
209
  latency_ms?: Record<string, QaRunCritiqueLatency>;
200
210
  /** One entry per attempted tile, in context and tile order. */
201
211
  tile_timings?: QaRunCritiqueTileTiming[];
212
+ /** Tokens summed over the attempted tiles. */
213
+ usage?: CritiqueUsageTotals;
202
214
  }
203
215
 
204
216
  /** Where the run's page review pack lives: the on-disk evidence an agent can
@@ -518,8 +530,15 @@ export function validateQaRunJob(value: unknown): QaRunJobValidation {
518
530
  }
519
531
  if (p?.critique_tile_budget !== undefined) {
520
532
  const n = p.critique_tile_budget;
521
- if (typeof n !== "number" || !Number.isInteger(n) || n < 8 || n > 400) {
522
- errors.push("policy.critique_tile_budget must be an integer between 8 and 400");
533
+ if (
534
+ typeof n !== "number" ||
535
+ !Number.isInteger(n) ||
536
+ n < 8 ||
537
+ n > PAGE_REVIEW_MAX_TILE_BUDGET
538
+ ) {
539
+ errors.push(
540
+ `policy.critique_tile_budget must be an integer between 8 and ${PAGE_REVIEW_MAX_TILE_BUDGET}`,
541
+ );
523
542
  }
524
543
  }
525
544
  if (p?.command_concurrency !== undefined) {
@@ -542,8 +561,15 @@ export function validateQaRunJob(value: unknown): QaRunJobValidation {
542
561
  }
543
562
  if (p?.critique_max_tiles !== undefined) {
544
563
  const n = p.critique_max_tiles;
545
- if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > 400) {
546
- errors.push("policy.critique_max_tiles must be an integer between 1 and 400");
564
+ if (
565
+ typeof n !== "number" ||
566
+ !Number.isInteger(n) ||
567
+ n < 1 ||
568
+ n > PAGE_REVIEW_MAX_TILE_BUDGET
569
+ ) {
570
+ errors.push(
571
+ `policy.critique_max_tiles must be an integer between 1 and ${PAGE_REVIEW_MAX_TILE_BUDGET}`,
572
+ );
547
573
  }
548
574
  }
549
575
  if (p?.critique_pool !== undefined) {
@@ -24,6 +24,7 @@ import {
24
24
  } from "./page-review-budget.js";
25
25
  import {
26
26
  PAGE_REVIEW_DEFAULT_TILE_BUDGET,
27
+ PAGE_REVIEW_MAX_TILE_BUDGET,
27
28
  type PageReviewCapturePlan,
28
29
  type PageReviewContextAllocation,
29
30
  } from "./page-review-contracts.js";
@@ -911,10 +912,29 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
911
912
  log(`critique provider failed to load: ${err instanceof Error ? err.message : String(err)}`);
912
913
  }
913
914
  }
914
- const bandArgs =
915
- provider?.tileBudgetPx !== undefined
916
- ? ["--check-critique-band", String(Math.max(200, Math.min(1400, provider.tileBudgetPx)))]
917
- : [];
915
+ const bandArgs = (ctx: QaRunContext): string[] => {
916
+ const args = [
917
+ ...contextRenderArgs(ctx),
918
+ ...(job.checks ?? [])
919
+ .filter((check) => check.contexts === undefined || check.contexts.includes(ctx.id))
920
+ .flatMap((check) => check.args),
921
+ ];
922
+ let requested = "1400";
923
+ for (let i = 0; i < args.length; i++) {
924
+ if (args[i] === "--check-critique-band") requested = args[i + 1] ?? "";
925
+ else if (args[i]?.startsWith("--check-critique-band=")) {
926
+ requested = args[i]!.slice("--check-critique-band=".length);
927
+ }
928
+ }
929
+ // A larger explicit request needs a declared route budget. Missing or
930
+ // invalid metadata must not turn capture planning into an unbounded path.
931
+ const budget = provider?.tileBudgetPx;
932
+ const cap = budget !== undefined && Number.isFinite(budget) && budget >= 200 ? budget : 1400;
933
+ return [
934
+ "--check-critique-band",
935
+ String(Math.max(200, Math.min(Number.parseInt(requested, 10), cap))),
936
+ ];
937
+ };
918
938
  const scopeArgs =
919
939
  visual === "full-page"
920
940
  ? []
@@ -961,7 +981,7 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
961
981
  "--qa-state",
962
982
  ctx.state,
963
983
  ...scopeArgs,
964
- ...bandArgs,
984
+ ...bandArgs(ctx),
965
985
  ...critiqueMaxTilesArgs,
966
986
  ]
967
987
  : []),
@@ -1185,6 +1205,10 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
1185
1205
 
1186
1206
  enterStage("capture");
1187
1207
  const captureStart = Date.now();
1208
+ // Candidate tiles a complete review of every context would need. Above the
1209
+ // per-run ceiling, signoff accepts a sampled critique (ADR 0224).
1210
+ let fullCoverageTiles = 0;
1211
+ const sampledContexts = new Set<string>();
1188
1212
  if (runsVisual) {
1189
1213
  writeFileSync(
1190
1214
  join(outDir, "preliminary-gates.json"),
@@ -1205,6 +1229,7 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
1205
1229
  );
1206
1230
  return candidate;
1207
1231
  });
1232
+ fullCoverageTiles = plans.reduce((sum, plan) => sum + plan.candidates.length, 0);
1208
1233
  const ceilings =
1209
1234
  job.policy?.critique_max_tiles === undefined
1210
1235
  ? undefined
@@ -1263,7 +1288,7 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
1263
1288
  .flatMap((check) => check.args),
1264
1289
  ...scopeArgs,
1265
1290
  ...critiqueMaxTilesArgs,
1266
- ...bandArgs,
1291
+ ...bandArgs(ctx),
1267
1292
  ];
1268
1293
  log(
1269
1294
  `capture ${ctx.id}${scopeArgs.length > 0 ? ` [${manifest.scopes.map((s) => s.selector).join(",")}]` : ""}`,
@@ -1398,6 +1423,7 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
1398
1423
  provider: judged.pool.provider,
1399
1424
  ...(latency ? { latency_ms: latency } : {}),
1400
1425
  tile_timings: judged.tile_timings,
1426
+ ...(judged.usage ? { usage: judged.usage } : {}),
1401
1427
  };
1402
1428
  for (const row of judged.contexts) {
1403
1429
  judgedById.set(row.context_id, row);
@@ -1477,17 +1503,43 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
1477
1503
  });
1478
1504
  }
1479
1505
  // Selected bands may leave gaps anywhere. Signoff needs complete
1480
- // coverage, while review mode reports what was actually captured.
1506
+ // coverage when the whole page fits under the per-run ceiling. Past
1507
+ // it, complete coverage is unavailable by policy, so signoff accepts a
1508
+ // sampled critique that still reviewed every edge band, gate hit, and
1509
+ // required scope; the deterministic gates covered the whole page.
1510
+ let coverageMode: QaRunCritiqueOutcome["coverage_mode"] = row.coverage
1511
+ ? row.coverage.capped
1512
+ ? "capped"
1513
+ : "full"
1514
+ : undefined;
1481
1515
  if (job.mode === "signoff" && row.coverage.capped && contextOutcome !== "failed") {
1482
- contextOutcome = "unknown";
1483
- blockers.push({
1484
- stage: "critique",
1485
- context_id: row.context_id,
1486
- reason:
1487
- `critique coverage capped: ${row.coverage.bands_reviewed} of ${row.coverage.bands_total} bands ` +
1488
- `reviewed (${row.coverage.reviewed_height_px} of ${row.coverage.page_height_px} px); raise ` +
1489
- "policy.critique_tile_budget (and any explicit per-context ceiling) for full signoff coverage",
1490
- });
1516
+ // Without the allocation record nothing proves the required regions
1517
+ // were selected, so a sampled signoff is refused.
1518
+ const allocated = row.record.allocation_coverage;
1519
+ const requiredOmitted = allocated
1520
+ ? [
1521
+ ...allocated.omitted_gate_hits.map((id) => `gate hit ${id}`),
1522
+ ...allocated.omitted_scopes.map((scope) => `scope ${scope}`),
1523
+ ]
1524
+ : ["its allocation record (recapture this context)"];
1525
+ if (fullCoverageTiles > PAGE_REVIEW_MAX_TILE_BUDGET && requiredOmitted.length === 0) {
1526
+ coverageMode = "sampled";
1527
+ sampledContexts.add(row.context_id);
1528
+ } else {
1529
+ contextOutcome = "unknown";
1530
+ blockers.push({
1531
+ stage: "critique",
1532
+ context_id: row.context_id,
1533
+ reason:
1534
+ `critique coverage capped: ${row.coverage.bands_reviewed} of ${row.coverage.bands_total} bands ` +
1535
+ `reviewed (${row.coverage.reviewed_height_px} of ${row.coverage.page_height_px} px)` +
1536
+ (requiredOmitted.length > 0
1537
+ ? `; the budget left out ${requiredOmitted.join(", ")}. Raise ` +
1538
+ `policy.critique_tile_budget (at most ${PAGE_REVIEW_MAX_TILE_BUDGET}) so every gate hit and scope is reviewed`
1539
+ : `; the full page needs ${fullCoverageTiles} tiles, so raise ` +
1540
+ `policy.critique_tile_budget (and any explicit per-context ceiling) to ${fullCoverageTiles}`),
1541
+ });
1542
+ }
1491
1543
  }
1492
1544
  const scopeById = new Map(row.record.tiles.map((tile) => [tile.id, tile.scope]));
1493
1545
  critique.push({
@@ -1497,6 +1549,7 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
1497
1549
  tiles_reviewed: row.tiles_reviewed,
1498
1550
  tiles_reused: row.tiles_reused,
1499
1551
  outcome: contextOutcome,
1552
+ ...(coverageMode ? { coverage_mode: coverageMode } : {}),
1500
1553
  ...(dismissed.length > 0 ? { dismissed } : {}),
1501
1554
  findings: row.findings.map((finding) => {
1502
1555
  const selector = scopeById.get(finding.tile_id);
@@ -1510,7 +1563,15 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
1510
1563
  coverage: row.coverage,
1511
1564
  });
1512
1565
  }
1513
- finalizePack("critique", toCritiqueRecords(judged), judged.pool);
1566
+ finalizePack(
1567
+ "critique",
1568
+ toCritiqueRecords(judged).map((record) =>
1569
+ sampledContexts.has(record.context_id)
1570
+ ? { ...record, coverage_mode: "sampled" as const }
1571
+ : record,
1572
+ ),
1573
+ judged.pool,
1574
+ );
1514
1575
  }
1515
1576
  if (runsVisual) restoreHeadlessOnly();
1516
1577
  wall.critique = Date.now() - critiqueStart;
@@ -39,7 +39,7 @@ export interface RuntsResult {
39
39
  outcome: "pass" | "fail" | "unknown";
40
40
  /** Text blocks scanned (post length/inline-children filters). */
41
41
  scannedBlocks: number;
42
- /** True when the block sweep hit the internal cap (very large page). */
42
+ /** True when the sweep reached its findings limit or the internal block cap. */
43
43
  truncated: boolean;
44
44
  runts: RuntHit[];
45
45
  }
@@ -47,13 +47,20 @@ export interface RuntsResult {
47
47
  /**
48
48
  * Build the JS function passed to `page.evaluate`. `scope` narrows the sweep
49
49
  * to one container (null = whole body); `minChars` filters out tiny labels
50
- * that can't meaningfully wrap (default 40).
50
+ * that can't meaningfully wrap (default 40). `maxFindings` bounds the retained
51
+ * failure list (default 50; integer 1..5000), not the separate 5000-block cap.
51
52
  */
52
53
  export function buildRuntsCheck(): (args: {
53
54
  scope: string | null;
54
55
  minChars: number;
56
+ maxFindings?: number;
55
57
  }) => RuntsResult {
56
- return ({ scope, minChars }) => {
58
+ return ({ scope, minChars, maxFindings = 50 }) => {
59
+ // page.evaluate has no module closure: validate this boundary too, even
60
+ // when a caller evaluates the built function without the Browser client.
61
+ if (!Number.isInteger(maxFindings) || maxFindings < 1 || maxFindings > 5000) {
62
+ throw new Error("maxFindings must be an integer from 1 to 5000.");
63
+ }
57
64
  const INLINE = new Set([
58
65
  "B",
59
66
  "I",
@@ -74,7 +81,7 @@ export function buildRuntsCheck(): (args: {
74
81
  ]);
75
82
  const ATOMIC = /@|https?:|\.(com|org|net|io|co)\b|\d-\d{3}/;
76
83
  const BLOCK_CAP = 5000;
77
- const RUNT_CAP = 50;
84
+ const RUNT_CAP = maxFindings;
78
85
 
79
86
  const root = scope ? document.querySelector(scope) : document.body;
80
87
  if (!root) {
@@ -128,34 +135,78 @@ export function buildRuntsCheck(): (args: {
128
135
  }
129
136
  scanned++;
130
137
 
131
- // Map every word to its visual line by rounding the Range rect top.
132
- const lines = new Map<number, { words: string[]; last: DOMRect; node: Node }>();
133
- const tw = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
138
+ // Inline markup does not create word boundaries. Keep offsets into the
139
+ // original text nodes so one Range can measure a word split by emphasis
140
+ // or followed by punctuation in a separate node. BR still separates words.
141
+ const segments: Array<{ node: Text; start: number; end: number }> = [];
142
+ let content = "";
143
+ const tw = document.createTreeWalker(el, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
134
144
  for (let n = tw.nextNode(); n; n = tw.nextNode()) {
145
+ if (n instanceof Element && n.tagName === "BR") content += "\n";
146
+ if (n.nodeType !== Node.TEXT_NODE) continue;
135
147
  const value = n.nodeValue ?? "";
136
- for (const m of value.matchAll(/\S+/g)) {
137
- const r = document.createRange();
138
- r.setStart(n, m.index);
139
- r.setEnd(n, m.index + m[0].length);
140
- const rect = r.getBoundingClientRect();
141
- if (!rect.width) continue;
142
- const top = Math.round(rect.top);
143
- const entry = lines.get(top);
144
- if (entry) {
145
- entry.words.push(m[0]);
146
- entry.last = rect;
147
- entry.node = n;
148
- } else {
149
- lines.set(top, { words: [m[0]], last: rect, node: n });
148
+ segments.push({
149
+ node: n as Text,
150
+ start: content.length,
151
+ end: content.length + value.length,
152
+ });
153
+ content += value;
154
+ }
155
+ type Word = { value: string; rect: DOMRect; node: Node };
156
+ const lines: Array<{ top: number; bottom: number; words: Map<number, Word> }> = [];
157
+ let segmentIndex = 0;
158
+ for (const m of content.matchAll(/\S+/g)) {
159
+ const lexical = /[\p{L}\p{N}]/u.exec(m[0]);
160
+ if (!lexical) continue; // punctuation alone is not an orphan word
161
+ const end = m.index + m[0].length;
162
+ while (segments[segmentIndex] && segments[segmentIndex]!.end <= m.index) segmentIndex++;
163
+ const first = segments[segmentIndex];
164
+ if (!first) continue;
165
+ let endIndex = segmentIndex;
166
+ while (segments[endIndex] && segments[endIndex]!.end < end) endIndex++;
167
+ const final = segments[endIndex];
168
+ if (!final) continue;
169
+ let wordIndex = segmentIndex;
170
+ while (segments[wordIndex]!.end <= m.index + lexical.index) wordIndex++;
171
+ const wordNode = segments[wordIndex]!.node;
172
+ const range = document.createRange();
173
+ range.setStart(first.node, m.index - first.start);
174
+ range.setEnd(final.node, end - final.start);
175
+ for (const rect of range.getClientRects()) {
176
+ if (!rect.width || !rect.height) continue;
177
+ // Different inline fonts have different glyph tops on the same line.
178
+ // Require substantial vertical overlap rather than exact rounded tops.
179
+ let line = lines.find(
180
+ (candidate) =>
181
+ Math.min(candidate.bottom, rect.bottom) - Math.max(candidate.top, rect.top) >
182
+ Math.min(candidate.bottom - candidate.top, rect.height) / 2,
183
+ );
184
+ if (!line) {
185
+ line = { top: rect.top, bottom: rect.bottom, words: new Map() };
186
+ lines.push(line);
150
187
  }
188
+ line.top = Math.min(line.top, rect.top);
189
+ line.bottom = Math.max(line.bottom, rect.bottom);
190
+ const previous = line.words.get(m.index)?.rect;
191
+ const box = previous
192
+ ? new DOMRect(
193
+ Math.min(previous.left, rect.left),
194
+ Math.min(previous.top, rect.top),
195
+ Math.max(previous.right, rect.right) - Math.min(previous.left, rect.left),
196
+ Math.max(previous.bottom, rect.bottom) - Math.min(previous.top, rect.top),
197
+ )
198
+ : rect;
199
+ line.words.set(m.index, { value: m[0], rect: box, node: wordNode });
151
200
  }
152
201
  }
153
- const tops = [...lines.keys()].sort((a, b) => a - b);
154
- if (tops.length < 2) continue; // single line, no runt risk
155
- const lastTop = tops[tops.length - 1];
156
- const last = lastTop === undefined ? undefined : lines.get(lastTop);
157
- if (last?.words.length !== 1) continue;
158
- const word = last.words[0] ?? "";
202
+ lines.sort((a, b) => a.top - b.top);
203
+ if (lines.length < 2) continue; // single line, no runt risk
204
+ const lastLine = lines[lines.length - 1];
205
+ if (lastLine?.words.size !== 1) continue;
206
+ const [wordOffset, last] = [...lastLine.words.entries()][0]!;
207
+ // A word split across visual lines is not a separate final orphan word.
208
+ if (lines.slice(0, -1).some((line) => line.words.has(wordOffset))) continue;
209
+ const word = last.value;
159
210
  if (ATOMIC.test(word)) continue;
160
211
  // A chip is not prose. A lone token whose glyphs live inside a padded,
161
212
  // boxed inline element (a <code> chip, pill, or badge) is a UI control:
@@ -165,11 +216,7 @@ export function buildRuntsCheck(): (args: {
165
216
  // inline ancestry (tag-based, since flex/grid parents blockify computed
166
217
  // display) and skip the finding when a chip hosts it.
167
218
  const chipHosted = (() => {
168
- for (
169
- let e = last.node.parentElement;
170
- e && e !== el && INLINE.has(e.tagName);
171
- e = e.parentElement
172
- ) {
219
+ for (let e = last.node.parentElement; e && INLINE.has(e.tagName); e = e.parentElement) {
173
220
  const cs = getComputedStyle(e);
174
221
  const padX =
175
222
  (Number.parseFloat(cs.paddingLeft) || 0) + (Number.parseFloat(cs.paddingRight) || 0);
@@ -177,6 +224,7 @@ export function buildRuntsCheck(): (args: {
177
224
  (cs.backgroundColor !== "rgba(0, 0, 0, 0)" && cs.backgroundColor !== "transparent") ||
178
225
  (Number.parseFloat(cs.borderTopWidth) || 0) > 0;
179
226
  if (padX > 0 && boxed) return true;
227
+ if (e === el) break;
180
228
  }
181
229
  return false;
182
230
  })();
@@ -187,12 +235,12 @@ export function buildRuntsCheck(): (args: {
187
235
  word,
188
236
  snippet: text.replace(/\s+/g, " ").trim().slice(0, 80),
189
237
  rect: {
190
- x: Math.round(last.last.x + sx),
191
- y: Math.round(last.last.y + sy),
192
- width: Math.round(last.last.width),
193
- height: Math.round(last.last.height),
238
+ x: Math.round(last.rect.x + sx),
239
+ y: Math.round(last.rect.y + sy),
240
+ width: Math.round(last.rect.width),
241
+ height: Math.round(last.rect.height),
194
242
  },
195
- lines: tops.length,
243
+ lines: lines.length,
196
244
  });
197
245
  if (runts.length >= RUNT_CAP) {
198
246
  truncated = true;
@@ -479,6 +479,56 @@ export function stopWindowsChromeProfile(
479
479
  return parseStopOutput(result.stdout, profileWindows);
480
480
  }
481
481
 
482
+ /**
483
+ * PowerShell that lists the Chrome processes running on the profile matched
484
+ * by `$Pattern` and prints their ids as a JSON array. It never closes them.
485
+ */
486
+ export function runningScript(): string {
487
+ return [
488
+ "param([string]$Pattern)",
489
+ "$ErrorActionPreference = 'Stop'",
490
+ PROFILE_PROCESS_FUNCTION,
491
+ "$ids = @(Get-ProfileChrome | ForEach-Object { [int]$_.ProcessId })",
492
+ "[Console]::Out.Write((ConvertTo-Json -Compress -InputObject $ids))",
493
+ ].join("\r\n");
494
+ }
495
+
496
+ /** Parse the running script's stdout into process ids. */
497
+ export function parseRunningOutput(stdout: string): number[] {
498
+ const trimmed = stdout.trim();
499
+ if (!trimmed) return [];
500
+ const parsed = JSON.parse(trimmed) as number | number[] | null;
501
+ if (parsed === null) return [];
502
+ return (Array.isArray(parsed) ? parsed : [parsed]).map(Number);
503
+ }
504
+
505
+ /**
506
+ * Ids of the Chrome processes running on `profileWindows`, for callers that
507
+ * must leave an open profile alone. Throws when the check cannot run.
508
+ */
509
+ export function windowsChromeProfileProcesses(profileWindows: string): number[] {
510
+ const result = runPowerShellScript(
511
+ "profile-running",
512
+ runningScript(),
513
+ ["-Pattern", profileCommandLinePattern(profileWindows)],
514
+ 30_000,
515
+ );
516
+ if (result.status !== 0) {
517
+ throw new Error(
518
+ `Checking for Chrome on ${profileWindows} failed: ${(result.stderr || result.stdout || result.error?.message || "no output").trim().slice(0, 400)}`,
519
+ );
520
+ }
521
+ return parseRunningOutput(result.stdout);
522
+ }
523
+
524
+ /** Linux form of a drive-letter Windows path (`C:\x` becomes `/mnt/c/x`). */
525
+ export function windowsPathToWsl(windowsPath: string): string {
526
+ const match = /^([A-Za-z]):\\(.*)$/.exec(windowsPath);
527
+ if (!match) throw new Error(`Not a drive-letter path: ${windowsPath}`);
528
+ const rest = (match[2] ?? "").replace(/\\+/g, "/").replace(/\/+$/, "");
529
+ return `/mnt/${(match[1] as string).toLowerCase()}${rest ? `/${rest}` : ""}`;
530
+ }
531
+
482
532
  /** Windows form of a `/mnt/<drive>/...` path. */
483
533
  export function wslPathToWindows(linuxPath: string): string {
484
534
  const match = /^\/mnt\/([a-z])\/(.*)$/.exec(linuxPath);