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
@@ -0,0 +1,682 @@
1
+ // `qa-record <target>`: record hand-performed page QA as a standard result
2
+ // document. When the matrix runner cannot run — host thrash, a lost bridge, an
3
+ // interaction no runner can drive — an agent does the checks by hand. Today
4
+ // that produces prose, which no consumer can verify. This command turns those
5
+ // hand-performed checks into the SAME artifact qa-run emits, so qa-verify and
6
+ // qa-status work on it identically.
7
+ //
8
+ // One rule is load-bearing and enforced here, not in prose: a manual result
9
+ // can report a defect but can never read `passed`. Every record carries a
10
+ // `validate` blocker naming the reason the runner was unavailable, and the
11
+ // verdict is computed with evidenceSource "manual", which caps at incomplete.
12
+
13
+ import { execFileSync } from "node:child_process";
14
+ import { randomUUID } from "node:crypto";
15
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
16
+ import { cpus, freemem, loadavg, totalmem } from "node:os";
17
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
18
+ import type { Command } from "commander";
19
+ import type { EmitContext, HarneryProgramContext } from "../commander.ts";
20
+ import { recordQaSignal } from "../core/agents/qa-signal.ts";
21
+ import { createManagedQaOutParent, resolveQaRepoRoot } from "../core/qa-artifacts.ts";
22
+ import {
23
+ QA_RUN_RESULT_FILENAME,
24
+ QA_RUN_STATUS_FILENAME,
25
+ writeLatestPointer,
26
+ } from "../lib/browser/qa-run.ts";
27
+ import {
28
+ computeJobDigest,
29
+ computeVerdict,
30
+ QA_RUN_JOB_SCHEMA_VERSION,
31
+ QA_RUN_RESULT_SCHEMA_VERSION,
32
+ QA_RUN_STATUS_SCHEMA_VERSION,
33
+ type QaRunBlocker,
34
+ type QaRunCommandOutcome,
35
+ type QaRunContext,
36
+ type QaRunHostSample,
37
+ type QaRunJob,
38
+ type QaRunResult,
39
+ type QaRunStatusDocument,
40
+ validateQaRunJob,
41
+ } from "../lib/browser/qa-run-contracts.ts";
42
+
43
+ export const QA_RECORD_EVIDENCE_SCHEMA_VERSION = 1 as const;
44
+
45
+ /** The evidence document is copied into the run directory beside the result:
46
+ * the run identity's job_digest is taken over it, so without the copy nothing
47
+ * downstream could re-derive the digest. qa-run writes `job.json` for the same
48
+ * reason; the manual name keeps the two from being confused. */
49
+ export const QA_RECORD_EVIDENCE_FILENAME = "manual-evidence.json";
50
+
51
+ /** argv marker for a hand-performed check. The contract types argv as a string
52
+ * array and consumers print it, so an empty array would render as a command
53
+ * that ran with no arguments. A single explicit marker reads as "no process
54
+ * executed" and can never be mistaken for a re-runnable command line. */
55
+ export const QA_RECORD_MANUAL_ARGV = ["<manual>"] as const;
56
+
57
+ export type QaManualOutcome = "passed" | "failed" | "unknown";
58
+
59
+ /** One check a human or agent performed by hand. */
60
+ export interface QaManualCheck {
61
+ /** Context the check was performed in; matches a `contexts[].id` when the
62
+ * document declares contexts. */
63
+ context_id: string;
64
+ /** Stable check identifier. Prefix with `manual:` by convention. */
65
+ check_id: string;
66
+ outcome: QaManualOutcome;
67
+ /** What the recorder observed. Carried into the result's `failures` field. */
68
+ notes?: string[];
69
+ /** Files that back the observation. Every path must exist on disk. */
70
+ artifacts?: { png?: string; html?: string; json?: string };
71
+ /** Time the check took, when the recorder measured it. */
72
+ wall_time_ms?: number;
73
+ }
74
+
75
+ export interface QaManualEvidence {
76
+ schema_version: typeof QA_RECORD_EVIDENCE_SCHEMA_VERSION;
77
+ /** Who performed the checks (agent name or operator identity). */
78
+ recorded_by: string;
79
+ /** Why the runner could not be used. Required: a hand record with no stated
80
+ * reason is an assertion, not evidence. */
81
+ reason: string;
82
+ checks: QaManualCheck[];
83
+ /** Declared contexts. Absent: derived from the checks' context IDs. */
84
+ contexts?: QaRunContext[];
85
+ }
86
+
87
+ export type QaManualEvidenceValidation =
88
+ | { ok: true; evidence: QaManualEvidence }
89
+ | { ok: false; errors: string[] };
90
+
91
+ const ARTIFACT_KINDS = ["png", "html", "json"] as const;
92
+
93
+ /** Artifact paths are resolved against the directory holding the evidence
94
+ * document, not the working directory: an evidence file names the captures
95
+ * sitting beside it, and that stays true wherever it is recorded from. */
96
+ export function resolveArtifactPath(baseDir: string, path: string): string {
97
+ return isAbsolute(path) ? path : resolve(baseDir, path);
98
+ }
99
+
100
+ function isStringArray(value: unknown): value is string[] {
101
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
102
+ }
103
+
104
+ /** Reuse the job validator's credential scan rather than duplicating its
105
+ * patterns: the evidence document is nested under a minimal valid job, so
106
+ * every error the validator can return for that probe comes from the secret
107
+ * scan. Paths are rewritten back to the evidence document's own namespace. */
108
+ function scanEvidenceForSecrets(document: unknown): string[] {
109
+ const probe = {
110
+ schema_version: QA_RUN_JOB_SCHEMA_VERSION,
111
+ target: "qa-record",
112
+ mode: "review",
113
+ evidence: document,
114
+ };
115
+ const validation = validateQaRunJob(probe);
116
+ if (validation.ok) return [];
117
+ return validation.errors.map((message) => message.replace(/^job\.evidence/, "evidence"));
118
+ }
119
+
120
+ /**
121
+ * Validate an untrusted evidence document. Every structural problem, every
122
+ * missing artifact, and every secret-bearing field is reported at once, before
123
+ * anything is written: a caller fixes the document in one pass, and a
124
+ * half-valid record never reaches disk.
125
+ */
126
+ export function validateManualEvidence(
127
+ value: unknown,
128
+ options: { baseDir: string },
129
+ ): QaManualEvidenceValidation {
130
+ const errors: string[] = [];
131
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
132
+ return { ok: false, errors: ["evidence must be a JSON object"] };
133
+ }
134
+ const document = value as Record<string, unknown>;
135
+
136
+ if (document.schema_version !== QA_RECORD_EVIDENCE_SCHEMA_VERSION) {
137
+ errors.push(`schema_version must be ${QA_RECORD_EVIDENCE_SCHEMA_VERSION}`);
138
+ }
139
+ if (typeof document.recorded_by !== "string" || document.recorded_by.length === 0) {
140
+ errors.push("recorded_by is required (the agent or operator who performed the checks)");
141
+ }
142
+ if (typeof document.reason !== "string" || document.reason.length === 0) {
143
+ errors.push(
144
+ "reason is required: name why the runner could not be used — a hand record with " +
145
+ "no stated reason is an assertion, not evidence",
146
+ );
147
+ }
148
+
149
+ const declaredContextIds = new Set<string>();
150
+ if (document.contexts !== undefined) {
151
+ if (!Array.isArray(document.contexts)) {
152
+ errors.push("contexts must be an array");
153
+ } else {
154
+ document.contexts.forEach((entry, i) => {
155
+ const context = entry as Record<string, unknown>;
156
+ if (typeof context?.id !== "string" || context.id.length === 0) {
157
+ errors.push(`contexts[${i}].id is required`);
158
+ } else if (declaredContextIds.has(context.id)) {
159
+ errors.push(`contexts[${i}].id duplicates "${context.id}"`);
160
+ } else {
161
+ declaredContextIds.add(context.id);
162
+ }
163
+ if (typeof context?.viewport !== "string" || context.viewport.length === 0) {
164
+ errors.push(`contexts[${i}].viewport is required`);
165
+ }
166
+ if (context?.theme !== "light" && context?.theme !== "dark") {
167
+ errors.push(`contexts[${i}].theme must be "light" or "dark"`);
168
+ }
169
+ if (typeof context?.state !== "string" || context.state.length === 0) {
170
+ errors.push(`contexts[${i}].state is required ("default" for the plain page)`);
171
+ }
172
+ });
173
+ }
174
+ }
175
+
176
+ if (!Array.isArray(document.checks) || document.checks.length === 0) {
177
+ errors.push(
178
+ "checks must be a non-empty array: a record with no checks proves nothing about the page",
179
+ );
180
+ } else {
181
+ const seen = new Set<string>();
182
+ document.checks.forEach((entry, i) => {
183
+ const check = entry as Record<string, unknown>;
184
+ const contextId = check?.context_id;
185
+ if (typeof contextId !== "string" || contextId.length === 0) {
186
+ errors.push(`checks[${i}].context_id is required`);
187
+ } else if (declaredContextIds.size > 0 && !declaredContextIds.has(contextId)) {
188
+ errors.push(`checks[${i}].context_id "${contextId}" is not a declared context`);
189
+ }
190
+ const checkId = check?.check_id;
191
+ if (typeof checkId !== "string" || checkId.length === 0) {
192
+ errors.push(`checks[${i}].check_id is required`);
193
+ } else if (typeof contextId === "string") {
194
+ const key = `${contextId}${checkId}`;
195
+ if (seen.has(key)) {
196
+ errors.push(`checks[${i}] duplicates "${checkId}" in context "${contextId}"`);
197
+ }
198
+ seen.add(key);
199
+ }
200
+ if (
201
+ check?.outcome !== "passed" &&
202
+ check?.outcome !== "failed" &&
203
+ check?.outcome !== "unknown"
204
+ ) {
205
+ errors.push(`checks[${i}].outcome must be "passed", "failed", or "unknown"`);
206
+ }
207
+ if (check?.notes !== undefined && !isStringArray(check.notes)) {
208
+ errors.push(`checks[${i}].notes must be an array of strings`);
209
+ }
210
+ if (check?.wall_time_ms !== undefined) {
211
+ const ms = check.wall_time_ms;
212
+ if (typeof ms !== "number" || !Number.isFinite(ms) || ms < 0) {
213
+ errors.push(`checks[${i}].wall_time_ms must be a non-negative number of milliseconds`);
214
+ }
215
+ }
216
+ if (check?.artifacts !== undefined) {
217
+ if (
218
+ !check.artifacts ||
219
+ typeof check.artifacts !== "object" ||
220
+ Array.isArray(check.artifacts)
221
+ ) {
222
+ errors.push(`checks[${i}].artifacts must be an object`);
223
+ } else {
224
+ for (const [kind, path] of Object.entries(check.artifacts as Record<string, unknown>)) {
225
+ if (!(ARTIFACT_KINDS as readonly string[]).includes(kind)) {
226
+ errors.push(
227
+ `checks[${i}].artifacts.${kind} is not a recognised artifact kind ` +
228
+ `(${ARTIFACT_KINDS.join(", ")})`,
229
+ );
230
+ continue;
231
+ }
232
+ if (typeof path !== "string" || path.length === 0) {
233
+ errors.push(`checks[${i}].artifacts.${kind} must be a file path`);
234
+ continue;
235
+ }
236
+ // A named artifact that is not on disk is a validation error, not
237
+ // a warning: an unresolvable path makes the record unverifiable,
238
+ // which is the failure mode this command exists to remove.
239
+ const abs = resolveArtifactPath(options.baseDir, path);
240
+ if (!existsSync(abs)) {
241
+ errors.push(`checks[${i}].artifacts.${kind} does not exist on disk: ${abs}`);
242
+ }
243
+ }
244
+ }
245
+ }
246
+ });
247
+ }
248
+
249
+ errors.push(...scanEvidenceForSecrets(document));
250
+
251
+ if (errors.length > 0) return { ok: false, errors };
252
+ return { ok: true, evidence: value as QaManualEvidence };
253
+ }
254
+
255
+ /** Contexts the result reports: the document's own declarations when it has
256
+ * them, otherwise one derived per distinct check context ID. Derivation reads
257
+ * the runner's `<viewport>-<theme>-<state>` convention when the ID follows it
258
+ * and records `unknown` rather than guessing when it does not. */
259
+ export function deriveContexts(evidence: QaManualEvidence): QaRunContext[] {
260
+ if (evidence.contexts && evidence.contexts.length > 0) return evidence.contexts;
261
+ const contexts: QaRunContext[] = [];
262
+ const seen = new Set<string>();
263
+ for (const check of evidence.checks) {
264
+ if (seen.has(check.context_id)) continue;
265
+ seen.add(check.context_id);
266
+ const parts = check.context_id.split("-");
267
+ const theme = parts[1];
268
+ if (parts.length >= 3 && (theme === "light" || theme === "dark")) {
269
+ contexts.push({
270
+ id: check.context_id,
271
+ viewport: parts[0] as string,
272
+ theme,
273
+ state: parts.slice(2).join("-"),
274
+ });
275
+ } else {
276
+ contexts.push({
277
+ id: check.context_id,
278
+ viewport: "unknown",
279
+ theme: "light",
280
+ state: "default",
281
+ });
282
+ }
283
+ }
284
+ return contexts;
285
+ }
286
+
287
+ function hostSample(): QaRunHostSample {
288
+ return {
289
+ captured_at: new Date().toISOString(),
290
+ loadavg_1m: loadavg()[0] ?? 0,
291
+ free_mem_bytes: freemem(),
292
+ total_mem_bytes: totalmem(),
293
+ cpu_count: cpus().length,
294
+ };
295
+ }
296
+
297
+ /** One git probe for the identity block, matching qa-run. Failure is not an
298
+ * error: work outside any repository legitimately records "unknown". */
299
+ export function probeRevision():
300
+ | { tested_revision?: string; worktree_dirty?: boolean }
301
+ | undefined {
302
+ try {
303
+ const head = execFileSync("git", ["rev-parse", "HEAD"], {
304
+ encoding: "utf8",
305
+ stdio: ["ignore", "pipe", "ignore"],
306
+ }).trim();
307
+ const porcelain = execFileSync("git", ["status", "--porcelain"], {
308
+ encoding: "utf8",
309
+ stdio: ["ignore", "pipe", "ignore"],
310
+ });
311
+ return { tested_revision: head, worktree_dirty: porcelain.trim().length > 0 };
312
+ } catch {
313
+ return undefined;
314
+ }
315
+ }
316
+
317
+ export interface BuildManualResultInput {
318
+ evidence: QaManualEvidence;
319
+ target: string;
320
+ mode: "signoff" | "review";
321
+ /** Parent directory; the result lands in `<outParent>/run-<run_id>/`. */
322
+ outParent: string;
323
+ /** Pre-minted run ID (tests); default crypto.randomUUID. */
324
+ runId?: string;
325
+ /** Directory the artifact paths resolve against (the evidence file's dir). */
326
+ baseDir: string;
327
+ revisionProbe?: { tested_revision?: string; worktree_dirty?: boolean };
328
+ /** Completion instant (tests); default now. */
329
+ completedAt?: Date;
330
+ }
331
+
332
+ /**
333
+ * Build the QaRunResult for a validated evidence document. Pure apart from the
334
+ * host sample and the clock, so the shape is testable without touching disk.
335
+ */
336
+ export function buildManualResult(input: BuildManualResultInput): QaRunResult {
337
+ const runId = input.runId ?? randomUUID();
338
+ const outDir = join(resolve(input.outParent), `run-${runId}`);
339
+
340
+ const commands: QaRunCommandOutcome[] = input.evidence.checks.map((check) => {
341
+ const artifacts: QaRunCommandOutcome["artifacts"] = {};
342
+ for (const kind of ARTIFACT_KINDS) {
343
+ const path = check.artifacts?.[kind];
344
+ if (path !== undefined) artifacts[kind] = resolveArtifactPath(input.baseDir, path);
345
+ }
346
+ return {
347
+ context_id: check.context_id,
348
+ check_id: check.check_id,
349
+ argv: [...QA_RECORD_MANUAL_ARGV],
350
+ exit_code: null,
351
+ outcome: check.outcome,
352
+ // `failures` is the contract's only free-text field per command, so the
353
+ // recorder's notes land there whatever the outcome. Dropping the notes
354
+ // of a passing check would discard the observation that backs it.
355
+ failures: check.notes ?? [],
356
+ artifacts,
357
+ wall_time_ms: check.wall_time_ms ?? 0,
358
+ };
359
+ });
360
+
361
+ const total = commands.reduce((sum, command) => sum + command.wall_time_ms, 0);
362
+ const completedAt = input.completedAt ?? new Date();
363
+ // The identity interval covers the hand-performed work rather than the few
364
+ // milliseconds this command took: a reader comparing started_at against a
365
+ // freshness floor should see when the checking happened, not when it was
366
+ // typed up. Stage buckets stay zero — no runner stage executed.
367
+ const startedAt = new Date(completedAt.getTime() - total);
368
+
369
+ const revision: Pick<
370
+ QaRunResult["run"],
371
+ "tested_revision" | "revision_source" | "worktree_dirty"
372
+ > =
373
+ input.revisionProbe?.tested_revision !== undefined
374
+ ? {
375
+ tested_revision: input.revisionProbe.tested_revision,
376
+ revision_source: "git",
377
+ ...(input.revisionProbe.worktree_dirty !== undefined
378
+ ? { worktree_dirty: input.revisionProbe.worktree_dirty }
379
+ : {}),
380
+ }
381
+ : { revision_source: "unknown" };
382
+
383
+ // The digest is taken over the same canonical hashing qa-run uses, with the
384
+ // evidence document standing in for the job: the record's identity is the
385
+ // document it was built from.
386
+ const jobDigest = computeJobDigest({
387
+ schema_version: QA_RUN_JOB_SCHEMA_VERSION,
388
+ target: input.target,
389
+ mode: input.mode,
390
+ evidence: input.evidence,
391
+ } as unknown as QaRunJob);
392
+
393
+ const blockers: QaRunBlocker[] = [
394
+ {
395
+ stage: "validate",
396
+ reason:
397
+ `evidence was recorded by hand (${input.evidence.reason}); no re-executable ` +
398
+ "proof exists, so this result cannot read passed",
399
+ },
400
+ ];
401
+
402
+ const start = hostSample();
403
+ return {
404
+ schema_version: QA_RUN_RESULT_SCHEMA_VERSION,
405
+ evidence_source: "manual",
406
+ run: {
407
+ run_id: runId,
408
+ started_at: startedAt.toISOString(),
409
+ completed_at: completedAt.toISOString(),
410
+ ...revision,
411
+ job_digest: jobDigest,
412
+ out_dir: outDir,
413
+ },
414
+ host: { start, finish: hostSample() },
415
+ last_completed_stage: null,
416
+ target: input.target,
417
+ ...(revision.tested_revision !== undefined
418
+ ? { tested_revision: revision.tested_revision }
419
+ : {}),
420
+ mode: input.mode,
421
+ qa_plan: null,
422
+ contexts: deriveContexts(input.evidence),
423
+ commands,
424
+ critique: [],
425
+ snapshot: { saved: false },
426
+ wall_time_ms: {
427
+ plan: 0,
428
+ gates: 0,
429
+ interactions: 0,
430
+ capture: 0,
431
+ critique: 0,
432
+ snapshot: 0,
433
+ total,
434
+ },
435
+ blockers,
436
+ verdict: computeVerdict({
437
+ mode: input.mode,
438
+ blockers,
439
+ commands,
440
+ critique: [],
441
+ snapshotSaved: false,
442
+ evidenceSource: "manual",
443
+ }),
444
+ };
445
+ }
446
+
447
+ export interface QaRecordWritePaths {
448
+ runDir: string;
449
+ resultPath: string;
450
+ evidencePath: string;
451
+ statusPath: string;
452
+ latestPath: string;
453
+ }
454
+
455
+ /**
456
+ * Write the run directory exactly the way the runner does: the result, the
457
+ * source document, a terminal status document, and the parent's latest.json
458
+ * pointer written temp-then-rename so a reader never sees a torn write.
459
+ */
460
+ export function writeManualRun(
461
+ result: QaRunResult,
462
+ evidence: QaManualEvidence,
463
+ ): QaRecordWritePaths {
464
+ const runDir = result.run.out_dir;
465
+ const outParent = dirname(runDir);
466
+ mkdirSync(runDir, { recursive: true });
467
+
468
+ const evidencePath = join(runDir, QA_RECORD_EVIDENCE_FILENAME);
469
+ writeFileSync(evidencePath, `${JSON.stringify(evidence, null, 2)}\n`);
470
+ const resultPath = join(runDir, QA_RUN_RESULT_FILENAME);
471
+ writeFileSync(resultPath, `${JSON.stringify(result, null, 2)}\n`);
472
+
473
+ const status: QaRunStatusDocument = {
474
+ schema_version: QA_RUN_STATUS_SCHEMA_VERSION,
475
+ run_id: result.run.run_id,
476
+ pid: process.pid,
477
+ state: "completed",
478
+ stage: null,
479
+ started_at: result.run.started_at,
480
+ updated_at: result.run.completed_at,
481
+ verdict: result.verdict,
482
+ };
483
+ const statusPath = join(runDir, QA_RUN_STATUS_FILENAME);
484
+ const statusTmp = join(runDir, `.${QA_RUN_STATUS_FILENAME}.tmp`);
485
+ writeFileSync(statusTmp, `${JSON.stringify(status, null, 2)}\n`);
486
+ renameSync(statusTmp, statusPath);
487
+
488
+ const latestPath = writeLatestPointer(outParent, {
489
+ run_id: result.run.run_id,
490
+ dir: basename(runDir),
491
+ completed_at: result.run.completed_at,
492
+ verdict: result.verdict,
493
+ });
494
+
495
+ return { runDir, resultPath, evidencePath, statusPath, latestPath };
496
+ }
497
+
498
+ export interface QaRecordOutcome {
499
+ /** 1 usage or validation error · 2 verdict failed · 4 verdict incomplete.
500
+ * Never 0: a hand-recorded result cannot report a pass. */
501
+ exit: 1 | 2 | 4;
502
+ result?: QaRunResult;
503
+ paths?: QaRecordWritePaths;
504
+ error?: string;
505
+ /** Individual validation errors, all of them, when the document was bad. */
506
+ evidenceErrors?: string[];
507
+ }
508
+
509
+ export interface QaRecordInput {
510
+ target: string;
511
+ evidencePath: string;
512
+ mode?: string;
513
+ outDir?: string;
514
+ /** Lazy default so invalid evidence does not create an empty artifact workspace. */
515
+ defaultOutDir?: () => string;
516
+ runId?: string;
517
+ completedAt?: Date;
518
+ /** Injectable for tests; default: the real git probe. */
519
+ revisionProbe?: () => { tested_revision?: string; worktree_dirty?: boolean } | undefined;
520
+ }
521
+
522
+ /**
523
+ * Pure core of qa-record: read the evidence document, validate it completely,
524
+ * build the result, and write the run directory. The commander action is a
525
+ * thin wrapper over this so the whole path tests without a CLI harness.
526
+ */
527
+ export function recordManualQa(input: QaRecordInput): QaRecordOutcome {
528
+ const mode = input.mode ?? "review";
529
+ if (mode !== "signoff" && mode !== "review") {
530
+ return { exit: 1, error: '--mode must be "signoff" or "review"' };
531
+ }
532
+ const evidenceAbs = resolve(input.evidencePath);
533
+ let raw: string;
534
+ try {
535
+ raw = readFileSync(evidenceAbs, "utf8");
536
+ } catch (err: unknown) {
537
+ return {
538
+ exit: 1,
539
+ error: `cannot read --evidence file ${evidenceAbs}: ${err instanceof Error ? err.message : String(err)}`,
540
+ };
541
+ }
542
+ let parsed: unknown;
543
+ try {
544
+ parsed = JSON.parse(raw);
545
+ } catch (err: unknown) {
546
+ return {
547
+ exit: 1,
548
+ error: `--evidence file ${evidenceAbs} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
549
+ };
550
+ }
551
+
552
+ const baseDir = dirname(evidenceAbs);
553
+ const validation = validateManualEvidence(parsed, { baseDir });
554
+ if (!validation.ok) {
555
+ return {
556
+ exit: 1,
557
+ error: `evidence validation failed with ${validation.errors.length} error${validation.errors.length === 1 ? "" : "s"}`,
558
+ evidenceErrors: validation.errors,
559
+ };
560
+ }
561
+
562
+ const probe = (input.revisionProbe ?? probeRevision)();
563
+ let outParent = input.outDir;
564
+ if (!outParent && input.defaultOutDir) {
565
+ try {
566
+ outParent = input.defaultOutDir();
567
+ } catch (err: unknown) {
568
+ return {
569
+ exit: 1,
570
+ error: `cannot create managed QA output: ${err instanceof Error ? err.message : String(err)}`,
571
+ };
572
+ }
573
+ }
574
+ if (!outParent) {
575
+ return { exit: 1, error: "qa-record requires outDir or defaultOutDir" };
576
+ }
577
+
578
+ const result = buildManualResult({
579
+ evidence: validation.evidence,
580
+ target: input.target,
581
+ mode,
582
+ outParent,
583
+ baseDir,
584
+ ...(input.runId !== undefined ? { runId: input.runId } : {}),
585
+ ...(probe ? { revisionProbe: probe } : {}),
586
+ ...(input.completedAt !== undefined ? { completedAt: input.completedAt } : {}),
587
+ });
588
+ const paths = writeManualRun(result, validation.evidence);
589
+ return { exit: result.verdict === "failed" ? 2 : 4, result, paths };
590
+ }
591
+
592
+ interface QaRecordOpts {
593
+ evidence?: string;
594
+ mode: string;
595
+ outDir?: string;
596
+ json?: boolean;
597
+ }
598
+
599
+ export function registerQaRecordCommand(
600
+ program: Command,
601
+ emit: EmitContext,
602
+ context?: HarneryProgramContext,
603
+ ): void {
604
+ program
605
+ .command("qa-record <target>")
606
+ .description(
607
+ "Record hand-performed page QA as a standard result document. Writes the " +
608
+ "same page-qa-result.json shape, run identity, and timing block the matrix " +
609
+ "runner writes, marked evidence_source manual, so qa-verify and qa-status " +
610
+ "treat it like any other result. A manual record can report a defect but " +
611
+ "never a pass.",
612
+ )
613
+ .option(
614
+ "--evidence <file>",
615
+ "Evidence document (JSON) describing the checks that were performed by hand, " +
616
+ "who performed them, and why the runner could not be used. Required.",
617
+ )
618
+ .option(
619
+ "--mode <mode>",
620
+ "signoff or review. A manual record never saves a QA snapshot either way.",
621
+ "review",
622
+ )
623
+ .option(
624
+ "--out-dir <dir>",
625
+ "PARENT output directory (default: a managed .harnery/artifacts workspace). The " +
626
+ "record lands in its own run-<run_id>/ beneath it and updates the parent's " +
627
+ "latest.json pointer, exactly like a runner invocation.",
628
+ )
629
+ .option("--json", "Print the full result JSON to stdout.")
630
+ .addHelpText(
631
+ "after",
632
+ "\nReach for this only when the matrix runner cannot run: host thrash, a lost " +
633
+ "bridge, or an interaction no runner can drive. Artifact paths in the evidence " +
634
+ "document must exist on disk, and secret-bearing fields are refused." +
635
+ "\n\nExit codes: 1 usage or evidence-validation error · 2 verdict failed · " +
636
+ "4 verdict incomplete. Never 0 — hand-recorded evidence can prove a defect " +
637
+ "but never its absence, so the verdict caps at incomplete.",
638
+ )
639
+ .action((target: string, opts: QaRecordOpts) => {
640
+ if (!opts.evidence) {
641
+ emit.error({
642
+ code: "qa_record_missing_evidence",
643
+ message: "--evidence <file> is required: the record IS the evidence document",
644
+ });
645
+ process.exitCode = 1;
646
+ return;
647
+ }
648
+ const outcome = recordManualQa({
649
+ target,
650
+ evidencePath: opts.evidence,
651
+ mode: opts.mode,
652
+ ...(opts.outDir !== undefined ? { outDir: opts.outDir } : {}),
653
+ defaultOutDir: () => createManagedQaOutParent(resolveQaRepoRoot(context), "qa-record"),
654
+ });
655
+ if (outcome.exit === 1 || !outcome.result || !outcome.paths) {
656
+ for (const message of outcome.evidenceErrors ?? [])
657
+ emit.log(`evidence: ${message}`, "error");
658
+ emit.error({
659
+ code: "qa_record_invalid_evidence",
660
+ message: outcome.error ?? "qa-record could not write the result",
661
+ });
662
+ process.exitCode = 1;
663
+ return;
664
+ }
665
+ const { result, paths } = outcome;
666
+ // Same status surface as a runner result: an operator should see that
667
+ // the latest evidence was hand-recorded, not silently see nothing.
668
+ recordQaSignal(result);
669
+ if (opts.json) emit.data(result);
670
+ for (const blocker of result.blockers) {
671
+ emit.log(`blocker [${blocker.stage}]: ${blocker.reason}`, "warn");
672
+ }
673
+ emit.log(
674
+ `verdict: ${result.verdict} — manual record ${result.run.run_id}, ` +
675
+ `${result.contexts.length} context${result.contexts.length === 1 ? "" : "s"}, ` +
676
+ `${result.commands.length} check${result.commands.length === 1 ? "" : "s"}; ` +
677
+ `result: ${paths.resultPath}`,
678
+ "warn",
679
+ );
680
+ process.exitCode = outcome.exit;
681
+ });
682
+ }