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,608 @@
1
+ // `qa-status [path]`: report where a qa-run stands from its on-disk state:
2
+ // live (launching, queued, running), dead (non-terminal status document with
3
+ // a dead PID), or terminal (result document written; the state is the run's
4
+ // verdict). Classification is fail-closed: liveness is proven by the PID,
5
+ // never assumed from the status document, and the result document is
6
+ // authoritative over the status document once it exists. `--wait` reconnects
7
+ // to a detached run and exits with its verdict; `--queue` shows the
8
+ // machine-wide admission queue instead of any one run.
9
+
10
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
11
+ import { dirname, join, resolve } from "node:path";
12
+ import type { Command } from "commander";
13
+ import type { EmitContext, HarneryProgramContext } from "../commander.ts";
14
+ import { latestManagedQaRun, resolveQaRepoRoot } from "../core/qa-artifacts.ts";
15
+ import {
16
+ type AdmissionEntry,
17
+ admissionBaseDir,
18
+ admissionStatus,
19
+ pidAlive as defaultPidAlive,
20
+ listAdmissionResources,
21
+ } from "../lib/admission.ts";
22
+ import {
23
+ QA_RUN_LATEST_FILENAME,
24
+ QA_RUN_RESULT_FILENAME,
25
+ QA_RUN_STATUS_FILENAME,
26
+ } from "../lib/browser/qa-run.ts";
27
+ import {
28
+ QA_RUN_STAGES,
29
+ type QaRunStage,
30
+ type QaRunVerdict,
31
+ } from "../lib/browser/qa-run-contracts.ts";
32
+ import { QA_ADMISSION_RESOURCE } from "./qa-run.ts";
33
+
34
+ /** A running state whose heartbeat is older than this earns a staleness
35
+ * warning, but stays "running" while the PID is alive. */
36
+ export const QA_STATUS_HEARTBEAT_STALE_MS = 120_000;
37
+
38
+ /** Log file `qa-run --detach` leaves in the run directory. */
39
+ const RUNNER_LOG_FILENAME = "runner.log";
40
+
41
+ const POLL_MS = 2_000;
42
+ const DEFAULT_WAIT_TIMEOUT_MINUTES = 60;
43
+
44
+ const STATUS_DOC_STATES = new Set(["launching", "queued", "running", "completed"]);
45
+
46
+ function describeError(err: unknown): string {
47
+ return err instanceof Error ? err.message : String(err);
48
+ }
49
+
50
+ function isRecord(value: unknown): value is Record<string, unknown> {
51
+ return typeof value === "object" && value !== null && !Array.isArray(value);
52
+ }
53
+
54
+ type JsonRead = { ok: true; value: unknown } | { ok: false; error: string };
55
+
56
+ function readJsonFile(path: string): JsonRead {
57
+ let raw: string;
58
+ try {
59
+ raw = readFileSync(path, "utf8");
60
+ } catch (err: unknown) {
61
+ return { ok: false, error: `cannot read ${path}: ${describeError(err)}` };
62
+ }
63
+ try {
64
+ return { ok: true, value: JSON.parse(raw) };
65
+ } catch (err: unknown) {
66
+ return { ok: false, error: `${path} is not valid JSON: ${describeError(err)}` };
67
+ }
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Path resolution
72
+ // ---------------------------------------------------------------------------
73
+
74
+ export type RunDirResolution = { ok: true; runDir: string } | { ok: false; error: string };
75
+
76
+ function hasRunFiles(dir: string): boolean {
77
+ return (
78
+ existsSync(join(dir, QA_RUN_STATUS_FILENAME)) || existsSync(join(dir, QA_RUN_RESULT_FILENAME))
79
+ );
80
+ }
81
+
82
+ /** Best-effort started_at for one run directory: the status document's
83
+ * `started_at`, falling back to the result document's `run.started_at`. */
84
+ function startedAtOf(dir: string): string | undefined {
85
+ const statusPath = join(dir, QA_RUN_STATUS_FILENAME);
86
+ if (existsSync(statusPath)) {
87
+ const read = readJsonFile(statusPath);
88
+ if (read.ok && isRecord(read.value) && typeof read.value.started_at === "string") {
89
+ return read.value.started_at;
90
+ }
91
+ }
92
+ const resultPath = join(dir, QA_RUN_RESULT_FILENAME);
93
+ if (existsSync(resultPath)) {
94
+ const read = readJsonFile(resultPath);
95
+ if (read.ok && isRecord(read.value) && isRecord(read.value.run)) {
96
+ const startedAt = read.value.run.started_at;
97
+ if (typeof startedAt === "string") return startedAt;
98
+ }
99
+ }
100
+ return undefined;
101
+ }
102
+
103
+ /**
104
+ * Resolve `[path]` to a run directory. A file is a result document (its
105
+ * directory is the run directory); a directory carrying a status or result
106
+ * document is the run directory itself; any other directory is treated as a
107
+ * run PARENT: its `run-*` children are scanned and the one with the newest
108
+ * started_at wins, falling back to the `latest.json` pointer when no child
109
+ * carries a status or result. Without a path, the newest managed QA artifact
110
+ * under the repository's `.harnery/artifacts/` store is used.
111
+ */
112
+ export function resolveRunDir(inputPath?: string, repoRoot?: string): RunDirResolution {
113
+ if (inputPath === undefined) {
114
+ const latest = latestManagedQaRun(repoRoot ?? resolveQaRepoRoot());
115
+ if (!latest) {
116
+ return {
117
+ ok: false,
118
+ error: "no managed qa-run or qa-record output exists under .harnery/artifacts",
119
+ };
120
+ }
121
+ return { ok: true, runDir: latest };
122
+ }
123
+ const abs = resolve(inputPath);
124
+ let stats: ReturnType<typeof statSync>;
125
+ try {
126
+ stats = statSync(abs);
127
+ } catch (err: unknown) {
128
+ return { ok: false, error: `cannot access ${abs}: ${describeError(err)}` };
129
+ }
130
+ if (stats.isFile()) return { ok: true, runDir: dirname(abs) };
131
+ if (!stats.isDirectory()) {
132
+ return { ok: false, error: `${abs} is neither a file nor a directory` };
133
+ }
134
+ if (hasRunFiles(abs)) return { ok: true, runDir: abs };
135
+
136
+ // Run parent: newest run-*/ child by started_at.
137
+ let childNames: string[] = [];
138
+ try {
139
+ childNames = readdirSync(abs, { withFileTypes: true })
140
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith("run-"))
141
+ .map((entry) => entry.name)
142
+ .sort();
143
+ } catch {
144
+ childNames = [];
145
+ }
146
+ let best: { dir: string; startedAtMs: number } | undefined;
147
+ for (const name of childNames) {
148
+ const child = join(abs, name);
149
+ if (!hasRunFiles(child)) continue;
150
+ const startedAt = startedAtOf(child);
151
+ const startedAtMs = startedAt !== undefined ? Date.parse(startedAt) : Number.NaN;
152
+ if (Number.isNaN(startedAtMs)) continue;
153
+ if (!best || startedAtMs > best.startedAtMs) best = { dir: child, startedAtMs };
154
+ }
155
+ if (best) return { ok: true, runDir: best.dir };
156
+
157
+ const pointerPath = join(abs, QA_RUN_LATEST_FILENAME);
158
+ if (existsSync(pointerPath)) {
159
+ const pointer = readJsonFile(pointerPath);
160
+ if (!pointer.ok) return pointer;
161
+ if (isRecord(pointer.value)) {
162
+ const record = pointer.value;
163
+ let rel: string | undefined;
164
+ if (typeof record.dir === "string" && record.dir.length > 0) rel = record.dir;
165
+ else if (typeof record.result === "string" && record.result.length > 0) {
166
+ rel = dirname(record.result);
167
+ }
168
+ if (rel !== undefined) return { ok: true, runDir: resolve(abs, rel) };
169
+ }
170
+ return { ok: false, error: `${pointerPath} carries no run directory to follow` };
171
+ }
172
+ return {
173
+ ok: false,
174
+ error:
175
+ `${abs} carries no ${QA_RUN_STATUS_FILENAME} or ${QA_RUN_RESULT_FILENAME}, no run-*/ ` +
176
+ `child with either, and no ${QA_RUN_LATEST_FILENAME} pointer`,
177
+ };
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Classification
182
+ // ---------------------------------------------------------------------------
183
+
184
+ export type QaStatusState = QaRunVerdict | "launching" | "queued" | "running" | "dead";
185
+
186
+ export interface QaStatusReport {
187
+ state: QaStatusState;
188
+ /** The result document exists; state is the run's final verdict. */
189
+ terminal: boolean;
190
+ /** 0 completed-passed · 2 completed-failed · 4 completed-incomplete or
191
+ * dead · 5 still in progress. */
192
+ exit: 0 | 2 | 4 | 5;
193
+ run_id: string | null;
194
+ pid: number | null;
195
+ stage: QaRunStage | null;
196
+ started_at: string | null;
197
+ updated_at: string | null;
198
+ completed_at: string | null;
199
+ /** now minus the status document's updated_at; null without a heartbeat. */
200
+ heartbeat_age_ms: number | null;
201
+ verdict?: QaRunVerdict;
202
+ result_path?: string;
203
+ log_path?: string;
204
+ queue?: { resource: string; waiting_since: string };
205
+ wall_time_ms?: Record<string, number>;
206
+ warnings: string[];
207
+ }
208
+
209
+ export interface QaStatusDeps {
210
+ /** PID liveness probe (injectable for tests). Default: process.kill(pid, 0). */
211
+ pidAlive?: (pid: number) => boolean;
212
+ /** Evaluation instant in epoch milliseconds. Default: Date.now(). */
213
+ now?: () => number;
214
+ }
215
+
216
+ export type QaStatusClassification =
217
+ | { ok: true; report: QaStatusReport }
218
+ | { ok: false; error: string };
219
+
220
+ function exitForVerdict(verdict: QaRunVerdict): 0 | 2 | 4 {
221
+ if (verdict === "passed") return 0;
222
+ if (verdict === "failed") return 2;
223
+ return 4;
224
+ }
225
+
226
+ /**
227
+ * Classify one run directory, fail-closed. The result document is terminal
228
+ * and authoritative when it exists; otherwise the status document plus a PID
229
+ * liveness probe decides between a live state and "dead". Neither file is a
230
+ * usage error, not a state.
231
+ */
232
+ export function classifyRun(runDir: string, deps: QaStatusDeps = {}): QaStatusClassification {
233
+ const isAlive = deps.pidAlive ?? defaultPidAlive;
234
+ const nowMs = deps.now ? deps.now() : Date.now();
235
+ const abs = resolve(runDir);
236
+ const resultPath = join(abs, QA_RUN_RESULT_FILENAME);
237
+ const statusPath = join(abs, QA_RUN_STATUS_FILENAME);
238
+ const logPath = join(abs, RUNNER_LOG_FILENAME);
239
+ const logExists = existsSync(logPath);
240
+ const resultExists = existsSync(resultPath);
241
+
242
+ // The status document supplies pid/stage/heartbeat even for terminal runs,
243
+ // so read it best-effort first; a broken status only matters when it is
244
+ // the sole evidence.
245
+ let status: Record<string, unknown> | undefined;
246
+ if (existsSync(statusPath)) {
247
+ const read = readJsonFile(statusPath);
248
+ if (read.ok && isRecord(read.value)) status = read.value;
249
+ else if (!resultExists) {
250
+ return { ok: false, error: read.ok ? `${statusPath} is not a status object` : read.error };
251
+ }
252
+ }
253
+
254
+ let heartbeatAgeMs: number | null = null;
255
+ if (status && typeof status.updated_at === "string") {
256
+ const updatedMs = Date.parse(status.updated_at);
257
+ if (!Number.isNaN(updatedMs)) heartbeatAgeMs = Math.max(0, nowMs - updatedMs);
258
+ }
259
+
260
+ if (resultExists) {
261
+ const read = readJsonFile(resultPath);
262
+ if (!read.ok) return read;
263
+ if (!isRecord(read.value)) {
264
+ return { ok: false, error: `${resultPath} is not a result object` };
265
+ }
266
+ const result = read.value;
267
+ const verdict = result.verdict;
268
+ if (verdict !== "passed" && verdict !== "failed" && verdict !== "incomplete") {
269
+ return { ok: false, error: `${resultPath} carries no recognizable verdict` };
270
+ }
271
+ const run = isRecord(result.run) ? result.run : {};
272
+ const statusRunId = typeof status?.run_id === "string" ? status.run_id : null;
273
+ return {
274
+ ok: true,
275
+ report: {
276
+ state: verdict,
277
+ terminal: true,
278
+ exit: exitForVerdict(verdict),
279
+ run_id: typeof run.run_id === "string" ? run.run_id : statusRunId,
280
+ pid: typeof status?.pid === "number" ? status.pid : null,
281
+ stage: null,
282
+ started_at: typeof run.started_at === "string" ? run.started_at : null,
283
+ updated_at: typeof status?.updated_at === "string" ? status.updated_at : null,
284
+ completed_at: typeof run.completed_at === "string" ? run.completed_at : null,
285
+ heartbeat_age_ms: heartbeatAgeMs,
286
+ verdict,
287
+ result_path: resultPath,
288
+ ...(logExists ? { log_path: logPath } : {}),
289
+ ...(isRecord(result.wall_time_ms)
290
+ ? { wall_time_ms: result.wall_time_ms as Record<string, number> }
291
+ : {}),
292
+ warnings: [],
293
+ },
294
+ };
295
+ }
296
+
297
+ if (!status) {
298
+ return {
299
+ ok: false,
300
+ error:
301
+ `${abs} carries neither ${QA_RUN_RESULT_FILENAME} nor ${QA_RUN_STATUS_FILENAME}: ` +
302
+ "not a qa-run directory",
303
+ };
304
+ }
305
+
306
+ const docState = typeof status.state === "string" ? status.state : "";
307
+ if (!STATUS_DOC_STATES.has(docState)) {
308
+ return {
309
+ ok: false,
310
+ error: `${statusPath} carries unrecognized state ${JSON.stringify(status.state)}`,
311
+ };
312
+ }
313
+ const pid = typeof status.pid === "number" ? status.pid : 0;
314
+ const runId = typeof status.run_id === "string" ? status.run_id : null;
315
+ const stage =
316
+ typeof status.stage === "string" && (QA_RUN_STAGES as readonly string[]).includes(status.stage)
317
+ ? (status.stage as QaRunStage)
318
+ : null;
319
+ const startedAt = typeof status.started_at === "string" ? status.started_at : null;
320
+ const updatedAt = typeof status.updated_at === "string" ? status.updated_at : null;
321
+
322
+ const alive = pid > 0 && isAlive(pid);
323
+ if (!alive) {
324
+ return {
325
+ ok: true,
326
+ report: {
327
+ state: "dead",
328
+ terminal: false,
329
+ exit: 4,
330
+ run_id: runId,
331
+ pid: pid > 0 ? pid : null,
332
+ stage,
333
+ started_at: startedAt,
334
+ updated_at: updatedAt,
335
+ completed_at: null,
336
+ heartbeat_age_ms: heartbeatAgeMs,
337
+ log_path: logPath,
338
+ warnings: [],
339
+ },
340
+ };
341
+ }
342
+
343
+ const warnings: string[] = [];
344
+ let state: QaStatusState;
345
+ if (docState === "completed") {
346
+ // The status document claims completion but no result document exists.
347
+ // The PID is alive, so the runner may still be writing; stay "running"
348
+ // and say so rather than inventing a verdict.
349
+ state = "running";
350
+ warnings.push(
351
+ `${QA_RUN_STATUS_FILENAME} says completed but no ${QA_RUN_RESULT_FILENAME} exists yet; ` +
352
+ "treating as running while the PID is alive",
353
+ );
354
+ } else {
355
+ state = docState as "launching" | "queued" | "running";
356
+ }
357
+ if (
358
+ state === "running" &&
359
+ heartbeatAgeMs !== null &&
360
+ heartbeatAgeMs > QA_STATUS_HEARTBEAT_STALE_MS
361
+ ) {
362
+ warnings.push(
363
+ `heartbeat stale: last update ${Math.round(heartbeatAgeMs / 1000)}s ago ` +
364
+ `(threshold ${QA_STATUS_HEARTBEAT_STALE_MS / 1000}s), but pid ${pid} is alive`,
365
+ );
366
+ }
367
+ const queueValue = status.queue;
368
+ const queue =
369
+ isRecord(queueValue) &&
370
+ typeof queueValue.resource === "string" &&
371
+ typeof queueValue.waiting_since === "string"
372
+ ? { resource: queueValue.resource, waiting_since: queueValue.waiting_since }
373
+ : undefined;
374
+
375
+ return {
376
+ ok: true,
377
+ report: {
378
+ state,
379
+ terminal: false,
380
+ exit: 5,
381
+ run_id: runId,
382
+ pid,
383
+ stage,
384
+ started_at: startedAt,
385
+ updated_at: updatedAt,
386
+ completed_at: null,
387
+ heartbeat_age_ms: heartbeatAgeMs,
388
+ ...(queue ? { queue } : {}),
389
+ ...(logExists ? { log_path: logPath } : {}),
390
+ warnings,
391
+ },
392
+ };
393
+ }
394
+
395
+ // ---------------------------------------------------------------------------
396
+ // Rendering
397
+ // ---------------------------------------------------------------------------
398
+
399
+ function formatAge(ms: number): string {
400
+ const totalSeconds = Math.max(0, Math.round(ms / 1000));
401
+ if (totalSeconds < 60) return `${totalSeconds}s`;
402
+ const minutes = Math.floor(totalSeconds / 60);
403
+ if (minutes < 60) return `${minutes}m ${totalSeconds % 60}s`;
404
+ const hours = Math.floor(minutes / 60);
405
+ return `${hours}h ${minutes % 60}m`;
406
+ }
407
+
408
+ function renderReport(emit: EmitContext, report: QaStatusReport): void {
409
+ const runId = report.run_id ?? "(no run id)";
410
+ if (report.terminal) {
411
+ const completed = report.completed_at ? `, completed ${report.completed_at}` : "";
412
+ emit.log(
413
+ `run ${runId}: verdict ${report.verdict}${completed} (${report.result_path})`,
414
+ report.state === "passed" ? "info" : "warn",
415
+ );
416
+ if (report.wall_time_ms) {
417
+ const total = report.wall_time_ms.total;
418
+ const queueMs = report.wall_time_ms.queue;
419
+ const totalPart = total !== undefined ? `total ${total}ms` : "total unknown";
420
+ const queuePart = queueMs !== undefined ? `, queue wait ${queueMs}ms` : "";
421
+ emit.log(`wall time: ${totalPart}${queuePart}`, "info");
422
+ }
423
+ } else if (report.state === "dead") {
424
+ const bits = [
425
+ `pid ${report.pid ?? "unknown"} not running`,
426
+ ...(report.stage !== null ? [`last stage ${report.stage}`] : []),
427
+ ...(report.heartbeat_age_ms !== null
428
+ ? [`last heartbeat ${formatAge(report.heartbeat_age_ms)} ago`]
429
+ : []),
430
+ ];
431
+ emit.log(`run ${runId}: dead, ${bits.join(", ")}`, "warn");
432
+ if (report.log_path !== undefined) emit.log(`log: ${report.log_path}`, "warn");
433
+ } else {
434
+ const bits = [
435
+ ...(report.stage !== null ? [`stage ${report.stage}`] : []),
436
+ `pid ${report.pid ?? "unknown"}`,
437
+ ...(report.heartbeat_age_ms !== null
438
+ ? [`heartbeat ${formatAge(report.heartbeat_age_ms)} ago`]
439
+ : []),
440
+ ];
441
+ emit.log(`run ${runId}: ${report.state}, ${bits.join(", ")}`, "info");
442
+ if (report.queue) {
443
+ emit.log(`queued for ${report.queue.resource} since ${report.queue.waiting_since}`, "info");
444
+ }
445
+ }
446
+ for (const warning of report.warnings) emit.log(`warning: ${warning}`, "warn");
447
+ }
448
+
449
+ function describeAdmissionEntry(
450
+ entry: AdmissionEntry,
451
+ nowMs: number,
452
+ verb: "acquired" | "queued",
453
+ ): string {
454
+ const stamp = verb === "acquired" ? (entry.acquired_at ?? entry.created_at) : entry.created_at;
455
+ const ageMs = nowMs - Date.parse(stamp);
456
+ const age = Number.isNaN(ageMs) ? `${verb} at unknown time` : `${verb} ${formatAge(ageMs)} ago`;
457
+ return `${entry.label || "(no label)"} (pid ${entry.pid}), ${age}`;
458
+ }
459
+
460
+ function runQueueAction(emit: EmitContext, json: boolean): void {
461
+ const dir = admissionBaseDir();
462
+ const nowMs = Date.now();
463
+ const names = new Set(listAdmissionResources(dir));
464
+ names.add(QA_ADMISSION_RESOURCE);
465
+ // browser-qa first, everything else alphabetical.
466
+ const ordered = [...names].sort((a, b) => {
467
+ if (a === QA_ADMISSION_RESOURCE) return -1;
468
+ if (b === QA_ADMISSION_RESOURCE) return 1;
469
+ return a.localeCompare(b);
470
+ });
471
+ const resources = ordered.map((resource) => admissionStatus({ dir, resource }));
472
+ if (json) {
473
+ emit.data({ dir, resources });
474
+ return;
475
+ }
476
+ for (const entry of resources) {
477
+ const holders = entry.holders.length;
478
+ const waiters = entry.waiters.length;
479
+ emit.log(
480
+ `${entry.resource}: ${holders} holder${holders === 1 ? "" : "s"}, ${waiters} waiting`,
481
+ "info",
482
+ );
483
+ for (const holder of entry.holders) {
484
+ emit.log(` holding: ${describeAdmissionEntry(holder, nowMs, "acquired")}`, "info");
485
+ }
486
+ for (const waiter of entry.waiters) {
487
+ emit.log(` waiting: ${describeAdmissionEntry(waiter, nowMs, "queued")}`, "info");
488
+ }
489
+ }
490
+ }
491
+
492
+ // ---------------------------------------------------------------------------
493
+ // Command
494
+ // ---------------------------------------------------------------------------
495
+
496
+ interface QaStatusOpts {
497
+ json?: boolean;
498
+ wait?: boolean;
499
+ waitTimeout?: string;
500
+ queue?: boolean;
501
+ }
502
+
503
+ function sleep(ms: number): Promise<void> {
504
+ return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
505
+ }
506
+
507
+ export function registerQaStatusCommand(
508
+ program: Command,
509
+ emit: EmitContext,
510
+ context?: HarneryProgramContext,
511
+ ): void {
512
+ program
513
+ .command("qa-status [path]")
514
+ .description(
515
+ "Report where a qa-run stands from its on-disk state: live (launching, queued, " +
516
+ "running), dead (non-terminal status with a dead PID), or terminal (result " +
517
+ "document written; the state is the verdict). Fail-closed: liveness is proven " +
518
+ "by the PID, never assumed from the status document.",
519
+ )
520
+ .option("--json", "Print the status envelope as JSON.")
521
+ .option("--wait", "Poll every 2s until the run reaches a terminal verdict or is dead.")
522
+ .option(
523
+ "--wait-timeout <minutes>",
524
+ `Maximum --wait duration in minutes (default ${DEFAULT_WAIT_TIMEOUT_MINUTES}).`,
525
+ )
526
+ .option(
527
+ "--queue",
528
+ "Ignore [path] and print the machine-wide admission queue instead: holders and " +
529
+ "waiters per resource, dead entries pruned on the way.",
530
+ )
531
+ .addHelpText(
532
+ "after",
533
+ `\n[path] may be a ${QA_RUN_RESULT_FILENAME} file, a run directory (containing ` +
534
+ `${QA_RUN_STATUS_FILENAME} or ${QA_RUN_RESULT_FILENAME}), or a run parent directory ` +
535
+ `(newest run-*/ child by started_at, falling back to the ${QA_RUN_LATEST_FILENAME} ` +
536
+ `pointer). Without [path], the newest managed run under .harnery/artifacts is used.` +
537
+ "\n\nExit codes: 0 completed passed · 1 usage or unreadable state · " +
538
+ "2 completed failed · 4 completed incomplete or dead · " +
539
+ "5 still in progress (or --wait timeout).",
540
+ )
541
+ .action(async (path: string | undefined, opts: QaStatusOpts) => {
542
+ if (opts.queue) {
543
+ runQueueAction(emit, opts.json === true);
544
+ return;
545
+ }
546
+
547
+ let waitTimeoutMinutes = DEFAULT_WAIT_TIMEOUT_MINUTES;
548
+ if (opts.waitTimeout !== undefined) {
549
+ const minutes = Number.parseInt(opts.waitTimeout, 10);
550
+ if (!Number.isInteger(minutes) || minutes < 1) {
551
+ emit.error({
552
+ code: "qa_status_invalid_wait_timeout",
553
+ message: "--wait-timeout must be a positive integer number of minutes",
554
+ });
555
+ process.exitCode = 1;
556
+ return;
557
+ }
558
+ waitTimeoutMinutes = minutes;
559
+ }
560
+
561
+ const resolved = resolveRunDir(path, resolveQaRepoRoot(context));
562
+ if (!resolved.ok) {
563
+ emit.error({ code: "qa_status_unresolvable_path", message: resolved.error });
564
+ process.exitCode = 1;
565
+ return;
566
+ }
567
+ const runDir = resolved.runDir;
568
+
569
+ const deadline = Date.now() + waitTimeoutMinutes * 60_000;
570
+ let lastProgressLine = "";
571
+ while (true) {
572
+ const outcome = classifyRun(runDir);
573
+ if (!outcome.ok) {
574
+ emit.error({ code: "qa_status_unreadable", message: outcome.error });
575
+ process.exitCode = 1;
576
+ return;
577
+ }
578
+ const report = outcome.report;
579
+ const settled = report.terminal || report.state === "dead";
580
+ if (settled || !opts.wait) {
581
+ if (opts.json) emit.data(report);
582
+ renderReport(emit, report);
583
+ if (!settled && !opts.wait) {
584
+ emit.log("still in progress; rerun with --wait to block until the run settles", "info");
585
+ }
586
+ if (report.exit !== 0) process.exitCode = report.exit;
587
+ return;
588
+ }
589
+ const progressLine =
590
+ `run ${report.run_id ?? "(no run id)"}: ${report.state}` +
591
+ `${report.stage !== null ? `, stage ${report.stage}` : ""}`;
592
+ if (progressLine !== lastProgressLine) {
593
+ lastProgressLine = progressLine;
594
+ emit.log(progressLine, "info");
595
+ }
596
+ if (Date.now() >= deadline) {
597
+ if (opts.json) emit.data(report);
598
+ emit.log(
599
+ `wait timed out after ${waitTimeoutMinutes}m; run is still ${report.state}`,
600
+ "warn",
601
+ );
602
+ process.exitCode = 5;
603
+ return;
604
+ }
605
+ await sleep(POLL_MS);
606
+ }
607
+ });
608
+ }