harnery 0.36.0 → 0.37.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 (311) hide show
  1. package/dist/commander.js +4 -0
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +87 -46
  4. package/dist/commands/artifacts.d.ts.map +1 -1
  5. package/dist/commands/artifacts.js +26 -1
  6. package/dist/commands/browse.d.ts.map +1 -1
  7. package/dist/commands/browse.js +11 -0
  8. package/dist/commands/diagnostics.d.ts +4 -0
  9. package/dist/commands/diagnostics.d.ts.map +1 -0
  10. package/dist/commands/diagnostics.js +229 -0
  11. package/dist/commands/files.d.ts +12 -0
  12. package/dist/commands/files.d.ts.map +1 -1
  13. package/dist/commands/files.js +54 -1
  14. package/dist/commands/governor.d.ts.map +1 -1
  15. package/dist/commands/governor.js +4 -0
  16. package/dist/commands/ledger-v3.d.ts.map +1 -1
  17. package/dist/commands/ledger-v3.js +71 -2
  18. package/dist/commands/qa-run.d.ts +4 -0
  19. package/dist/commands/qa-run.d.ts.map +1 -0
  20. package/dist/commands/qa-run.js +141 -0
  21. package/dist/commands/resources.d.ts +4 -0
  22. package/dist/commands/resources.d.ts.map +1 -0
  23. package/dist/commands/resources.js +29 -0
  24. package/dist/commands/supervisor.d.ts +4 -0
  25. package/dist/commands/supervisor.d.ts.map +1 -0
  26. package/dist/commands/supervisor.js +101 -0
  27. package/dist/commands/web.d.ts.map +1 -1
  28. package/dist/commands/web.js +17 -0
  29. package/dist/commands/workflow.d.ts.map +1 -1
  30. package/dist/commands/workflow.js +4 -0
  31. package/dist/core/agents/cli.js +7 -3
  32. package/dist/core/agents/health.d.ts +17 -0
  33. package/dist/core/agents/health.d.ts.map +1 -0
  34. package/dist/core/agents/health.js +109 -0
  35. package/dist/core/agents/mailbox.d.ts +92 -0
  36. package/dist/core/agents/mailbox.d.ts.map +1 -0
  37. package/dist/core/agents/mailbox.js +325 -0
  38. package/dist/core/agents/reconcile-coordination-v3.d.ts +44 -0
  39. package/dist/core/agents/reconcile-coordination-v3.d.ts.map +1 -0
  40. package/dist/core/agents/reconcile-coordination-v3.js +58 -0
  41. package/dist/core/agents/render/prompt-context.d.ts +5 -0
  42. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  43. package/dist/core/agents/render/prompt-context.js +34 -13
  44. package/dist/core/agents/render/session-context.d.ts +8 -0
  45. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  46. package/dist/core/agents/render/session-context.js +34 -3
  47. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  48. package/dist/core/agents/rules/commit-conflict.js +50 -1
  49. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  50. package/dist/core/agents/rules/stop-hook.js +6 -10
  51. package/dist/core/agents/session-finalizer-v3.js +4 -1
  52. package/dist/core/artifacts/index.d.ts +26 -1
  53. package/dist/core/artifacts/index.d.ts.map +1 -1
  54. package/dist/core/artifacts/index.js +146 -6
  55. package/dist/core/config.d.ts +12 -0
  56. package/dist/core/config.d.ts.map +1 -1
  57. package/dist/core/config.js +43 -0
  58. package/dist/core/diagnostics/advice.d.ts +9 -0
  59. package/dist/core/diagnostics/advice.d.ts.map +1 -0
  60. package/dist/core/diagnostics/advice.js +112 -0
  61. package/dist/core/diagnostics/bundle.d.ts +35 -0
  62. package/dist/core/diagnostics/bundle.d.ts.map +1 -0
  63. package/dist/core/diagnostics/bundle.js +874 -0
  64. package/dist/core/diagnostics/comparison.d.ts +5 -0
  65. package/dist/core/diagnostics/comparison.d.ts.map +1 -0
  66. package/dist/core/diagnostics/comparison.js +444 -0
  67. package/dist/core/diagnostics/contract.d.ts +245 -0
  68. package/dist/core/diagnostics/contract.d.ts.map +1 -0
  69. package/dist/core/diagnostics/contract.js +18 -0
  70. package/dist/core/diagnostics/identity.d.ts +3 -0
  71. package/dist/core/diagnostics/identity.d.ts.map +1 -0
  72. package/dist/core/diagnostics/identity.js +10 -0
  73. package/dist/core/diagnostics/index.d.ts +8 -0
  74. package/dist/core/diagnostics/index.d.ts.map +1 -0
  75. package/dist/core/diagnostics/index.js +7 -0
  76. package/dist/core/diagnostics/replay.d.ts +8 -0
  77. package/dist/core/diagnostics/replay.d.ts.map +1 -0
  78. package/dist/core/diagnostics/replay.js +213 -0
  79. package/dist/core/diagnostics/sanitize.d.ts +9 -0
  80. package/dist/core/diagnostics/sanitize.d.ts.map +1 -0
  81. package/dist/core/diagnostics/sanitize.js +84 -0
  82. package/dist/core/events/legacy-storage/compression.d.ts +13 -0
  83. package/dist/core/events/legacy-storage/compression.d.ts.map +1 -0
  84. package/dist/core/events/legacy-storage/compression.js +107 -0
  85. package/dist/core/events/legacy-storage/index.d.ts +1 -0
  86. package/dist/core/events/legacy-storage/index.d.ts.map +1 -1
  87. package/dist/core/events/legacy-storage/index.js +1 -0
  88. package/dist/core/events/v3/archive-retention.d.ts +33 -0
  89. package/dist/core/events/v3/archive-retention.d.ts.map +1 -0
  90. package/dist/core/events/v3/archive-retention.js +195 -0
  91. package/dist/core/events/v3/capabilities.d.ts +1 -1
  92. package/dist/core/events/v3/capabilities.d.ts.map +1 -1
  93. package/dist/core/events/v3/capabilities.js +1 -0
  94. package/dist/core/events/v3/index.d.ts +1 -0
  95. package/dist/core/events/v3/index.d.ts.map +1 -1
  96. package/dist/core/events/v3/index.js +1 -0
  97. package/dist/core/events/v3/live-routing.d.ts.map +1 -1
  98. package/dist/core/events/v3/live-routing.js +1 -0
  99. package/dist/core/events/v3/producers/hook-base.d.ts +1 -1
  100. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
  101. package/dist/core/events/v3/producers/hook-base.js +13 -2
  102. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  103. package/dist/core/events/v3/producers/intake.js +1 -0
  104. package/dist/core/events/v3/producers/recorder.d.ts +16 -0
  105. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  106. package/dist/core/events/v3/producers/recorder.js +88 -1
  107. package/dist/core/hooks/adapter/output.d.ts +8 -0
  108. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  109. package/dist/core/hooks/adapter/output.js +11 -1
  110. package/dist/core/hooks/cli.js +183 -192
  111. package/dist/core/hooks/health.d.ts +59 -0
  112. package/dist/core/hooks/health.d.ts.map +1 -0
  113. package/dist/core/hooks/health.js +84 -0
  114. package/dist/core/resources/contract.d.ts +90 -0
  115. package/dist/core/resources/contract.d.ts.map +1 -0
  116. package/dist/core/resources/contract.js +2 -0
  117. package/dist/core/resources/index.d.ts +6 -0
  118. package/dist/core/resources/index.d.ts.map +1 -0
  119. package/dist/core/resources/index.js +5 -0
  120. package/dist/core/resources/sampler.d.ts +27 -0
  121. package/dist/core/resources/sampler.d.ts.map +1 -0
  122. package/dist/core/resources/sampler.js +357 -0
  123. package/dist/core/resources/service-status.d.ts +3 -0
  124. package/dist/core/resources/service-status.d.ts.map +1 -0
  125. package/dist/core/resources/service-status.js +48 -0
  126. package/dist/core/resources/service.d.ts +26 -0
  127. package/dist/core/resources/service.d.ts.map +1 -0
  128. package/dist/core/resources/service.js +292 -0
  129. package/dist/core/resources/storage.d.ts +13 -0
  130. package/dist/core/resources/storage.d.ts.map +1 -0
  131. package/dist/core/resources/storage.js +30 -0
  132. package/dist/core/storage/atomic-json.d.ts +3 -0
  133. package/dist/core/storage/atomic-json.d.ts.map +1 -0
  134. package/dist/core/storage/atomic-json.js +15 -0
  135. package/dist/core/storage/builtins.d.ts.map +1 -1
  136. package/dist/core/storage/builtins.js +45 -6
  137. package/dist/core/storage/logger.d.ts +2 -0
  138. package/dist/core/storage/logger.d.ts.map +1 -1
  139. package/dist/core/storage/logger.js +2 -0
  140. package/dist/core/storage/query.d.ts +14 -0
  141. package/dist/core/storage/query.d.ts.map +1 -1
  142. package/dist/core/storage/query.js +51 -0
  143. package/dist/core/supervisor/activity.d.ts +4 -0
  144. package/dist/core/supervisor/activity.d.ts.map +1 -0
  145. package/dist/core/supervisor/activity.js +141 -0
  146. package/dist/core/supervisor/contract.d.ts +268 -0
  147. package/dist/core/supervisor/contract.d.ts.map +1 -0
  148. package/dist/core/supervisor/contract.js +35 -0
  149. package/dist/core/supervisor/explanations.d.ts +4 -0
  150. package/dist/core/supervisor/explanations.d.ts.map +1 -0
  151. package/dist/core/supervisor/explanations.js +71 -0
  152. package/dist/core/supervisor/findings.d.ts +20 -0
  153. package/dist/core/supervisor/findings.d.ts.map +1 -0
  154. package/dist/core/supervisor/findings.js +399 -0
  155. package/dist/core/supervisor/history.d.ts +9 -0
  156. package/dist/core/supervisor/history.d.ts.map +1 -0
  157. package/dist/core/supervisor/history.js +46 -0
  158. package/dist/core/supervisor/hook-health-alerts.d.ts +14 -0
  159. package/dist/core/supervisor/hook-health-alerts.d.ts.map +1 -0
  160. package/dist/core/supervisor/hook-health-alerts.js +70 -0
  161. package/dist/core/supervisor/hook-health-storage.d.ts +5 -0
  162. package/dist/core/supervisor/hook-health-storage.d.ts.map +1 -0
  163. package/dist/core/supervisor/hook-health-storage.js +24 -0
  164. package/dist/core/supervisor/hook-health.d.ts +72 -0
  165. package/dist/core/supervisor/hook-health.d.ts.map +1 -0
  166. package/dist/core/supervisor/hook-health.js +203 -0
  167. package/dist/core/supervisor/hooks.d.ts +10 -0
  168. package/dist/core/supervisor/hooks.d.ts.map +1 -0
  169. package/dist/core/supervisor/hooks.js +28 -0
  170. package/dist/core/supervisor/index.d.ts +15 -0
  171. package/dist/core/supervisor/index.d.ts.map +1 -0
  172. package/dist/core/supervisor/index.js +14 -0
  173. package/dist/core/supervisor/log-feed.d.ts +9 -0
  174. package/dist/core/supervisor/log-feed.d.ts.map +1 -0
  175. package/dist/core/supervisor/log-feed.js +103 -0
  176. package/dist/core/supervisor/service.d.ts +29 -0
  177. package/dist/core/supervisor/service.d.ts.map +1 -0
  178. package/dist/core/supervisor/service.js +450 -0
  179. package/dist/core/supervisor/services.d.ts +11 -0
  180. package/dist/core/supervisor/services.d.ts.map +1 -0
  181. package/dist/core/supervisor/services.js +155 -0
  182. package/dist/core/supervisor/status.d.ts +3 -0
  183. package/dist/core/supervisor/status.d.ts.map +1 -0
  184. package/dist/core/supervisor/status.js +48 -0
  185. package/dist/core/supervisor/storage.d.ts +30 -0
  186. package/dist/core/supervisor/storage.d.ts.map +1 -0
  187. package/dist/core/supervisor/storage.js +73 -0
  188. package/dist/core/supervisor/timeline.d.ts +3 -0
  189. package/dist/core/supervisor/timeline.d.ts.map +1 -0
  190. package/dist/core/supervisor/timeline.js +87 -0
  191. package/dist/core/workflow/admission.d.ts +23 -0
  192. package/dist/core/workflow/admission.d.ts.map +1 -0
  193. package/dist/core/workflow/admission.js +136 -0
  194. package/dist/core/workflow/engine.d.ts.map +1 -1
  195. package/dist/core/workflow/engine.js +77 -2
  196. package/dist/core/workflow/index.d.ts +3 -2
  197. package/dist/core/workflow/index.d.ts.map +1 -1
  198. package/dist/core/workflow/index.js +2 -1
  199. package/dist/core/workflow/proof.d.ts +2 -1
  200. package/dist/core/workflow/proof.d.ts.map +1 -1
  201. package/dist/core/workflow/proof.js +39 -1
  202. package/dist/core/workflow/run-state.d.ts +3 -2
  203. package/dist/core/workflow/run-state.d.ts.map +1 -1
  204. package/dist/core/workflow/run-state.js +8 -1
  205. package/dist/core/workflow/types.d.ts +33 -1
  206. package/dist/core/workflow/types.d.ts.map +1 -1
  207. package/dist/core/workflow/types.js +2 -1
  208. package/dist/lib/browser/client.d.ts +5 -0
  209. package/dist/lib/browser/client.d.ts.map +1 -1
  210. package/dist/lib/browser/client.js +1 -0
  211. package/dist/lib/browser/index.d.ts +2 -0
  212. package/dist/lib/browser/index.d.ts.map +1 -1
  213. package/dist/lib/browser/index.js +2 -0
  214. package/dist/lib/browser/qa-run-contracts.d.ts +173 -0
  215. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -0
  216. package/dist/lib/browser/qa-run-contracts.js +226 -0
  217. package/dist/lib/browser/qa-run.d.ts +51 -0
  218. package/dist/lib/browser/qa-run.d.ts.map +1 -0
  219. package/dist/lib/browser/qa-run.js +684 -0
  220. package/dist/lib/coord-root-id.d.ts +9 -0
  221. package/dist/lib/coord-root-id.d.ts.map +1 -0
  222. package/dist/lib/coord-root-id.js +17 -0
  223. package/dist/lib/instructions/apply.d.ts +3 -1
  224. package/dist/lib/instructions/apply.d.ts.map +1 -1
  225. package/dist/lib/instructions/apply.js +74 -3
  226. package/dist/lib/instructions/templates.d.ts.map +1 -1
  227. package/dist/lib/instructions/templates.js +6 -0
  228. package/package.json +12 -1
  229. package/src/commander.ts +26 -0
  230. package/src/commands/agents.ts +102 -49
  231. package/src/commands/artifacts.ts +40 -2
  232. package/src/commands/browse.ts +15 -0
  233. package/src/commands/diagnostics.ts +271 -0
  234. package/src/commands/files.ts +102 -2
  235. package/src/commands/governor.ts +8 -0
  236. package/src/commands/ledger-v3.ts +82 -0
  237. package/src/commands/qa-run.ts +184 -0
  238. package/src/commands/resources.ts +38 -0
  239. package/src/commands/supervisor.ts +141 -0
  240. package/src/commands/web.ts +19 -0
  241. package/src/commands/workflow.ts +8 -0
  242. package/src/core/agents/cli.ts +7 -3
  243. package/src/core/agents/health.ts +142 -0
  244. package/src/core/agents/mailbox.ts +373 -0
  245. package/src/core/agents/reconcile-coordination-v3.ts +77 -0
  246. package/src/core/agents/render/prompt-context.ts +42 -12
  247. package/src/core/agents/render/session-context.ts +37 -3
  248. package/src/core/agents/rules/commit-conflict.ts +58 -1
  249. package/src/core/agents/rules/stop-hook.ts +6 -12
  250. package/src/core/agents/session-finalizer-v3.ts +4 -1
  251. package/src/core/artifacts/index.ts +196 -10
  252. package/src/core/config.ts +111 -2
  253. package/src/core/diagnostics/advice.ts +147 -0
  254. package/src/core/diagnostics/bundle.ts +1142 -0
  255. package/src/core/diagnostics/comparison.ts +565 -0
  256. package/src/core/diagnostics/contract.ts +311 -0
  257. package/src/core/diagnostics/identity.ts +11 -0
  258. package/src/core/diagnostics/index.ts +7 -0
  259. package/src/core/diagnostics/replay.ts +314 -0
  260. package/src/core/diagnostics/sanitize.ts +97 -0
  261. package/src/core/events/legacy-storage/compression.ts +130 -0
  262. package/src/core/events/legacy-storage/index.ts +1 -0
  263. package/src/core/events/v3/archive-retention.ts +251 -0
  264. package/src/core/events/v3/capabilities.ts +2 -0
  265. package/src/core/events/v3/index.ts +9 -0
  266. package/src/core/events/v3/live-routing.ts +1 -0
  267. package/src/core/events/v3/producers/hook-base.ts +14 -2
  268. package/src/core/events/v3/producers/intake.ts +1 -0
  269. package/src/core/events/v3/producers/recorder.ts +110 -1
  270. package/src/core/hooks/adapter/output.ts +12 -1
  271. package/src/core/hooks/cli.ts +192 -215
  272. package/src/core/hooks/health.ts +146 -0
  273. package/src/core/resources/contract.ts +98 -0
  274. package/src/core/resources/index.ts +5 -0
  275. package/src/core/resources/sampler.ts +458 -0
  276. package/src/core/resources/service-status.ts +62 -0
  277. package/src/core/resources/service.ts +337 -0
  278. package/src/core/resources/storage.ts +44 -0
  279. package/src/core/storage/atomic-json.ts +15 -0
  280. package/src/core/storage/builtins.ts +62 -6
  281. package/src/core/storage/logger.ts +2 -0
  282. package/src/core/storage/query.ts +58 -0
  283. package/src/core/supervisor/activity.ts +193 -0
  284. package/src/core/supervisor/contract.ts +312 -0
  285. package/src/core/supervisor/explanations.ts +104 -0
  286. package/src/core/supervisor/findings.ts +788 -0
  287. package/src/core/supervisor/history.ts +61 -0
  288. package/src/core/supervisor/hook-health-alerts.ts +108 -0
  289. package/src/core/supervisor/hook-health-storage.ts +25 -0
  290. package/src/core/supervisor/hook-health.ts +301 -0
  291. package/src/core/supervisor/hooks.ts +35 -0
  292. package/src/core/supervisor/index.ts +14 -0
  293. package/src/core/supervisor/log-feed.ts +131 -0
  294. package/src/core/supervisor/service.ts +511 -0
  295. package/src/core/supervisor/services.ts +203 -0
  296. package/src/core/supervisor/status.ts +59 -0
  297. package/src/core/supervisor/storage.ts +126 -0
  298. package/src/core/supervisor/timeline.ts +133 -0
  299. package/src/core/workflow/admission.ts +215 -0
  300. package/src/core/workflow/engine.ts +93 -1
  301. package/src/core/workflow/index.ts +10 -0
  302. package/src/core/workflow/proof.ts +54 -1
  303. package/src/core/workflow/run-state.ts +13 -1
  304. package/src/core/workflow/types.ts +39 -1
  305. package/src/lib/browser/client.ts +6 -0
  306. package/src/lib/browser/index.ts +29 -0
  307. package/src/lib/browser/qa-run-contracts.ts +380 -0
  308. package/src/lib/browser/qa-run.ts +784 -0
  309. package/src/lib/coord-root-id.ts +23 -0
  310. package/src/lib/instructions/apply.ts +81 -2
  311. package/src/lib/instructions/templates.ts +6 -0
@@ -0,0 +1,184 @@
1
+ // `qa-run <target>`: run the whole page-QA matrix (planner, deterministic
2
+ // gates, interactions, critique, snapshot) in one command. The agent's loop
3
+ // collapses to: build the job, run this, read page-qa-result.json.
4
+
5
+ import { readFileSync } from "node:fs";
6
+ import { join, resolve } from "node:path";
7
+ import type { Command } from "commander";
8
+ import type { EmitContext } from "../commander.ts";
9
+ import { QA_RUN_RESULT_FILENAME, runQaMatrix } from "../lib/browser/qa-run.ts";
10
+ import {
11
+ QA_RUN_JOB_SCHEMA_VERSION,
12
+ type QaRunJob,
13
+ validateQaRunJob,
14
+ } from "../lib/browser/qa-run-contracts.ts";
15
+
16
+ interface QaRunOpts {
17
+ job?: string;
18
+ mode: string;
19
+ concurrency?: string;
20
+ allowMetered?: boolean;
21
+ outDir?: string;
22
+ json?: boolean;
23
+ }
24
+
25
+ export function registerQaRunCommand(program: Command, emit: EmitContext): void {
26
+ program
27
+ .command("qa-run <target>")
28
+ .description(
29
+ "Run the whole page-QA matrix in one command: QA planner, deterministic " +
30
+ "gates per viewport/theme/state through a bounded process pool, serial " +
31
+ "interaction assertions, manifest-required vision critique, and the QA " +
32
+ "snapshot. Writes page-qa-result.json with a fail-closed verdict.",
33
+ )
34
+ .option(
35
+ "--job <file>",
36
+ "QA job document (JSON). Validated before any browser starts — secret-bearing " +
37
+ "fields are refused, and the job may widen but never narrow the planner's coverage. " +
38
+ "Absent: a minimal implicit job for <target> is used.",
39
+ )
40
+ .option(
41
+ "--mode <mode>",
42
+ "signoff persists a QA snapshot on a passing run; review never does.",
43
+ "signoff",
44
+ )
45
+ .option(
46
+ "--concurrency <n>",
47
+ "Concurrent deterministic captures (1-8; default 2). Interactions and critique " +
48
+ "always run serially.",
49
+ )
50
+ .option(
51
+ "--allow-metered",
52
+ "Permit the critique provider's metered-API fallback. Default: subscription-backed " +
53
+ "headless harnesses only — exhaustion becomes an incomplete blocker, never a metered call.",
54
+ )
55
+ .option(
56
+ "--out-dir <dir>",
57
+ "Artifact + result directory (default: .qa-run/<timestamp> under the current directory).",
58
+ )
59
+ .option("--json", "Print the full QaRunResult JSON to stdout.")
60
+ .addHelpText(
61
+ "after",
62
+ "\nExit codes: 0 verdict passed · 1 usage or job-validation error · " +
63
+ "2 verdict failed · 4 verdict incomplete.",
64
+ )
65
+ .action(async (target: string, opts: QaRunOpts, command: Command) => {
66
+ if (opts.mode !== "signoff" && opts.mode !== "review") {
67
+ emit.error({
68
+ code: "qa_run_invalid_mode",
69
+ message: '--mode must be "signoff" or "review"',
70
+ });
71
+ process.exitCode = 1;
72
+ return;
73
+ }
74
+ const modeExplicit = command.getOptionValueSource("mode") === "cli";
75
+
76
+ let jobDocument: unknown;
77
+ if (opts.job) {
78
+ let rawText: string;
79
+ try {
80
+ rawText = readFileSync(opts.job, "utf8");
81
+ } catch (err: unknown) {
82
+ emit.error({
83
+ code: "qa_run_job_unreadable",
84
+ message: `cannot read --job file ${opts.job}: ${err instanceof Error ? err.message : String(err)}`,
85
+ });
86
+ process.exitCode = 1;
87
+ return;
88
+ }
89
+ let parsed: unknown;
90
+ try {
91
+ parsed = JSON.parse(rawText);
92
+ } catch (err: unknown) {
93
+ emit.error({
94
+ code: "qa_run_job_invalid_json",
95
+ message: `--job file ${opts.job} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
96
+ });
97
+ process.exitCode = 1;
98
+ return;
99
+ }
100
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
101
+ const raw = parsed as Record<string, unknown>;
102
+ // The CLI target argument is authoritative; a job file may omit
103
+ // target and mode. An explicit --mode overrides the job's mode.
104
+ jobDocument = {
105
+ ...raw,
106
+ target,
107
+ ...(raw.mode === undefined || modeExplicit ? { mode: opts.mode } : {}),
108
+ };
109
+ } else {
110
+ jobDocument = parsed;
111
+ }
112
+ } else {
113
+ jobDocument = { schema_version: QA_RUN_JOB_SCHEMA_VERSION, target, mode: opts.mode };
114
+ }
115
+
116
+ // All validation errors print at once, before any browser starts.
117
+ const validation = validateQaRunJob(jobDocument);
118
+ if (!validation.ok) {
119
+ for (const message of validation.errors) emit.log(`job: ${message}`, "error");
120
+ emit.error({
121
+ code: "qa_run_invalid_job",
122
+ message: `job validation failed with ${validation.errors.length} error${validation.errors.length === 1 ? "" : "s"}`,
123
+ });
124
+ process.exitCode = 1;
125
+ return;
126
+ }
127
+
128
+ const policy = { ...validation.job.policy };
129
+ if (opts.concurrency !== undefined) {
130
+ const n = Number.parseInt(opts.concurrency, 10);
131
+ if (!Number.isInteger(n) || n < 1 || n > 8) {
132
+ emit.error({
133
+ code: "qa_run_invalid_concurrency",
134
+ message: "--concurrency must be an integer between 1 and 8",
135
+ });
136
+ process.exitCode = 1;
137
+ return;
138
+ }
139
+ policy.command_concurrency = n;
140
+ }
141
+ if (opts.allowMetered) policy.allow_metered_critique = true;
142
+ const job: QaRunJob = { ...validation.job, policy };
143
+
144
+ const outDir = resolve(
145
+ opts.outDir ?? join(".qa-run", new Date().toISOString().replace(/[:.]/g, "-")),
146
+ );
147
+ const cliScript = process.argv[1];
148
+ if (!cliScript) {
149
+ emit.error({
150
+ code: "qa_run_no_cli_script",
151
+ message: "cannot resolve the host CLI script path for child browse invocations",
152
+ });
153
+ process.exitCode = 1;
154
+ return;
155
+ }
156
+ // Children run the same host CLI, so they inherit the host's browse
157
+ // wiring (critique provider, cookie jar, profiles) unchanged.
158
+ const browseArgv = [process.execPath, cliScript, "browse"];
159
+
160
+ const result = await runQaMatrix({
161
+ job,
162
+ outDir,
163
+ browseArgv,
164
+ onLog: (message) => emit.log(message, "info"),
165
+ });
166
+
167
+ if (opts.json) emit.data(result);
168
+ const resultPath = join(outDir, QA_RUN_RESULT_FILENAME);
169
+ for (const blocker of result.blockers) {
170
+ emit.log(
171
+ `blocker [${blocker.stage}${blocker.context_id ? ` ${blocker.context_id}` : ""}]: ${blocker.reason}`,
172
+ "warn",
173
+ );
174
+ }
175
+ emit.log(
176
+ `verdict: ${result.verdict} — ${result.contexts.length} context${result.contexts.length === 1 ? "" : "s"}, ` +
177
+ `${result.commands.length} command${result.commands.length === 1 ? "" : "s"}, ` +
178
+ `${result.blockers.length} blocker${result.blockers.length === 1 ? "" : "s"}; result: ${resultPath}`,
179
+ result.verdict === "passed" ? "info" : "warn",
180
+ );
181
+ if (result.verdict === "failed") process.exitCode = 2;
182
+ else if (result.verdict === "incomplete") process.exitCode = 4;
183
+ });
184
+ }
@@ -0,0 +1,38 @@
1
+ import { resolve } from "node:path";
2
+ import type { Command } from "commander";
3
+ import type { EmitContext, HarneryProgramContext } from "../commander.ts";
4
+ import { sampleResources } from "../core/resources/index.ts";
5
+
6
+ export function registerResourcesCommand(
7
+ program: Command,
8
+ emit: EmitContext,
9
+ context?: HarneryProgramContext,
10
+ ): void {
11
+ const resources = program
12
+ .command("resources")
13
+ .description("Observe local CPU and memory pressure by agent and process tree");
14
+
15
+ resources
16
+ .command("snapshot")
17
+ .description("Capture one machine and process-resource snapshot")
18
+ .option("--root <path>", "Explicit coordination root")
19
+ .action((options: { root?: string }) => {
20
+ try {
21
+ emit.data(sampleResources(resolve(options.root ?? coordRoot(context))).snapshot);
22
+ } catch (error) {
23
+ fail(emit, "resource_snapshot_failed", error);
24
+ }
25
+ });
26
+ }
27
+
28
+ function coordRoot(context?: HarneryProgramContext): string {
29
+ return context?.resolveCoordRoot?.() ?? context?.repoRoot ?? process.cwd();
30
+ }
31
+
32
+ function fail(emit: EmitContext, code: string, error: unknown): void {
33
+ emit.error({
34
+ code,
35
+ message: error instanceof Error ? error.message : String(error),
36
+ });
37
+ emit.setExitCode(1);
38
+ }
@@ -0,0 +1,141 @@
1
+ import { resolve } from "node:path";
2
+ import type { Command } from "commander";
3
+ import type { EmitContext, HarneryProgramContext } from "../commander.ts";
4
+ import { readSupervisorHookHealth } from "../core/supervisor/hook-health-storage.ts";
5
+ import {
6
+ requestSupervisorStop,
7
+ runSupervisor,
8
+ spawnSupervisor,
9
+ } from "../core/supervisor/service.ts";
10
+ import { readSupervisorStatus } from "../core/supervisor/status.ts";
11
+ import {
12
+ readSupervisorFindings,
13
+ readSupervisorHistory,
14
+ readSupervisorLogFeed,
15
+ readSupervisorSnapshot,
16
+ } from "../core/supervisor/storage.ts";
17
+
18
+ export function registerSupervisorCommand(
19
+ program: Command,
20
+ emit: EmitContext,
21
+ context?: HarneryProgramContext,
22
+ ): void {
23
+ const supervisor = program
24
+ .command("supervisor")
25
+ .description("Run Harnery's optional local diagnostic supervisor");
26
+
27
+ supervisor
28
+ .command("start")
29
+ .description("Start the detached local supervisor")
30
+ .option("--root <path>", "Explicit coordination root")
31
+ .option("--interval-ms <milliseconds>", "Collection interval from 500 to 60000 ms", integer)
32
+ .option(
33
+ "--idle-exit-ms <milliseconds>",
34
+ "Exit after this long without a live dashboard consumer or attributed agent",
35
+ integer,
36
+ )
37
+ .option("--keep-alive", "Remain resident until explicitly stopped")
38
+ .action(
39
+ async (options: {
40
+ root?: string;
41
+ intervalMs?: number;
42
+ idleExitMs?: number;
43
+ keepAlive?: boolean;
44
+ }) => {
45
+ try {
46
+ emit.data(
47
+ await spawnSupervisor(resolve(options.root ?? coordRoot(context)), {
48
+ intervalMs: options.intervalMs,
49
+ idleExitMs: options.idleExitMs,
50
+ keepAlive: options.keepAlive,
51
+ }),
52
+ );
53
+ } catch (error) {
54
+ fail(emit, "supervisor_start_failed", error);
55
+ }
56
+ },
57
+ );
58
+
59
+ supervisor
60
+ .command("status")
61
+ .description("Show supervisor liveness and its latest bounded projections")
62
+ .option("--root <path>", "Explicit coordination root")
63
+ .action((options: { root?: string }) => {
64
+ try {
65
+ const root = resolve(options.root ?? coordRoot(context));
66
+ emit.data({
67
+ service: readSupervisorStatus(root),
68
+ snapshot: readSupervisorSnapshot(root),
69
+ history: readSupervisorHistory(root),
70
+ findings: readSupervisorFindings(root),
71
+ hook_health: readSupervisorHookHealth(root),
72
+ log_feed: readSupervisorLogFeed(root),
73
+ });
74
+ } catch (error) {
75
+ fail(emit, "supervisor_status_failed", error);
76
+ }
77
+ });
78
+
79
+ supervisor
80
+ .command("stop")
81
+ .description("Request a graceful supervisor stop")
82
+ .option("--root <path>", "Explicit coordination root")
83
+ .action(async (options: { root?: string }) => {
84
+ try {
85
+ const root = resolve(options.root ?? coordRoot(context));
86
+ let status = requestSupervisorStop(root);
87
+ const deadline = Date.now() + 5_000;
88
+ while (status.running && Date.now() < deadline) {
89
+ await new Promise((done) => setTimeout(done, 50));
90
+ status = readSupervisorStatus(root);
91
+ }
92
+ emit.data(status);
93
+ } catch (error) {
94
+ fail(emit, "supervisor_stop_failed", error);
95
+ }
96
+ });
97
+
98
+ supervisor
99
+ .command("daemon", { hidden: true })
100
+ .description("Internal detached local supervisor entrypoint")
101
+ .option("--root <path>", "Explicit coordination root")
102
+ .option("--interval-ms <milliseconds>", "Collection interval", integer)
103
+ .option("--idle-exit-ms <milliseconds>", "Idle exit interval", integer)
104
+ .option("--keep-alive", "Remain resident until explicitly stopped")
105
+ .action(
106
+ async (options: {
107
+ root?: string;
108
+ intervalMs?: number;
109
+ idleExitMs?: number;
110
+ keepAlive?: boolean;
111
+ }) => {
112
+ try {
113
+ emit.data(
114
+ await runSupervisor({
115
+ coordRoot: resolve(options.root ?? coordRoot(context)),
116
+ intervalMs: options.intervalMs,
117
+ idleExitMs: options.idleExitMs,
118
+ keepAlive: options.keepAlive,
119
+ }),
120
+ );
121
+ } catch (error) {
122
+ fail(emit, "supervisor_daemon_failed", error);
123
+ }
124
+ },
125
+ );
126
+ }
127
+
128
+ function coordRoot(context?: HarneryProgramContext): string {
129
+ return context?.resolveCoordRoot?.() ?? context?.repoRoot ?? process.cwd();
130
+ }
131
+
132
+ function integer(value: string): number {
133
+ const parsed = Number(value);
134
+ if (!Number.isInteger(parsed)) throw new Error(`expected an integer, got ${value}`);
135
+ return parsed;
136
+ }
137
+
138
+ function fail(emit: EmitContext, code: string, error: unknown): void {
139
+ emit.error({ code, message: error instanceof Error ? error.message : String(error) });
140
+ emit.setExitCode(1);
141
+ }
@@ -8,6 +8,11 @@ import type { EmitContext } from "../commander.ts";
8
8
  import { DEFAULT_WEB_PORT, resolveWebPort } from "../core/config.ts";
9
9
  import { ensureSemanticServiceRunning } from "../core/semantic/service.ts";
10
10
  import { processLogDestination } from "../core/storage/process-log.ts";
11
+ import { ensureSupervisorRunning } from "../core/supervisor/service.ts";
12
+ import {
13
+ registerSupervisorConsumer,
14
+ unregisterSupervisorConsumer,
15
+ } from "../core/supervisor/services.ts";
11
16
  import {
12
17
  parsePerformanceWindow,
13
18
  readWebPerformanceReport,
@@ -109,6 +114,16 @@ async function startSemanticServiceWithWeb(coordRoot: string, emit: EmitContext)
109
114
  }
110
115
  }
111
116
 
117
+ async function startSupervisorWithWeb(coordRoot: string, emit: EmitContext): Promise<void> {
118
+ registerSupervisorConsumer(coordRoot, { id: "dashboard" });
119
+ const result = await ensureSupervisorRunning(coordRoot);
120
+ if (result.state === "started") emit.log("local supervisor · started automatically", "info");
121
+ if (result.state === "running") emit.log("local supervisor · already running", "info");
122
+ if (result.state === "unavailable") {
123
+ emit.log(`local supervisor · unavailable (${result.error})`, "warn");
124
+ }
125
+ }
126
+
112
127
  /**
113
128
  * Resolve the web app root. A local `web/` (source checkout / submodule) wins.
114
129
  * Otherwise, on an npm install, lazy-fetch it (clone + install) unless the
@@ -233,6 +248,7 @@ export function registerWebCommand(program: Command, emit: EmitContext): void {
233
248
 
234
249
  if (!(await requireAvailablePort(emit, port))) return;
235
250
 
251
+ await startSupervisorWithWeb(coordRoot, emit);
236
252
  await startSemanticServiceWithWeb(coordRoot, emit);
237
253
 
238
254
  const heapMb = resolveMaxOldSpaceMb(opts.maxOldSpace);
@@ -264,6 +280,7 @@ export function registerWebCommand(program: Command, emit: EmitContext): void {
264
280
  process.on("SIGTERM", () => cleanup("SIGTERM"));
265
281
 
266
282
  child.on("exit", (code, sig) => {
283
+ unregisterSupervisorConsumer(coordRoot, "dashboard");
267
284
  if (sig) {
268
285
  emit.log(`web exited on ${sig}`, "info");
269
286
  } else if (code !== 0) {
@@ -326,6 +343,7 @@ export function registerWebCommand(program: Command, emit: EmitContext): void {
326
343
  return;
327
344
  }
328
345
  if (!(await requireAvailablePort(emit, port))) return;
346
+ await startSupervisorWithWeb(coordRoot, emit);
329
347
  await startSemanticServiceWithWeb(coordRoot, emit);
330
348
  const heapMb = resolveMaxOldSpaceMb(opts.maxOldSpace);
331
349
  const nodeOptions = nodeOptionsWithWebDiagnostics(root, heapMb);
@@ -355,6 +373,7 @@ export function registerWebCommand(program: Command, emit: EmitContext): void {
355
373
  process.on("SIGTERM", () => cleanup("SIGTERM"));
356
374
 
357
375
  child.on("exit", (code) => {
376
+ unregisterSupervisorConsumer(coordRoot, "dashboard");
358
377
  process.exitCode = code ?? 0;
359
378
  });
360
379
  });
@@ -28,6 +28,7 @@ import type { WorkspaceProvider } from "../core/workflow/index.ts";
28
28
  interface WorkflowRunOpts {
29
29
  maxAgents?: string;
30
30
  concurrency?: string;
31
+ observePressure?: boolean;
31
32
  cwd?: string;
32
33
  adapter?: string;
33
34
  resumeFrom?: string;
@@ -98,6 +99,10 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
98
99
  )
99
100
  .option("--max-agents <n>", "Total-agent ceiling for the run (default 50)")
100
101
  .option("--concurrency <n>", "Concurrent-subagent cap (default 4)")
102
+ .option(
103
+ "--observe-pressure",
104
+ "Record observer-only local pressure before the first real child dispatch",
105
+ )
101
106
  .option("--cwd <dir>", "Working directory children spawn in (default: coord root)")
102
107
  .option(
103
108
  "--adapter <name>",
@@ -194,6 +199,9 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
194
199
  allowApiBilling: opts.allowApiBilling,
195
200
  maxAgents: opts.maxAgents ? Number.parseInt(opts.maxAgents, 10) : undefined,
196
201
  concurrency: opts.concurrency ? Number.parseInt(opts.concurrency, 10) : undefined,
202
+ diagnosticAdmission: opts.observePressure
203
+ ? { schema_version: 1, mode: "shadow" }
204
+ : undefined,
197
205
  cwd: opts.cwd,
198
206
  ...adapterProofInputs(
199
207
  registry.list().map((adapter) => adapter.profile),
@@ -708,8 +708,10 @@ async function handleStaleSweep(root: string, _rest: string[]): Promise<number>
708
708
  }
709
709
 
710
710
  async function handleReconcileFinalization(root: string): Promise<number> {
711
- const { reconcileSessionFinalizationV3 } = await import("./session-finalizer-v3.ts");
712
- const result = reconcileSessionFinalizationV3(root);
711
+ // Shares the supervisor composition with `agents reconcile` so session start
712
+ // sweeps the stale cache in the same pass, as ADR 0077 specified.
713
+ const { reconcileCoordinationV3 } = await import("./reconcile-coordination-v3.ts");
714
+ const result = reconcileCoordinationV3(root);
713
715
  process.stdout.write(`${JSON.stringify(result)}\n`);
714
716
  return result.diagnostics.some((item) => item === "ledger_not_authority_safe") ? 2 : 0;
715
717
  }
@@ -759,6 +761,7 @@ async function handlePromptContext(root: string, rest: string[]): Promise<number
759
761
  const instanceId = args.instance;
760
762
  const sessionId = args.session ?? instanceId;
761
763
  const agentName = args.name;
764
+ const adapter = args.adapter;
762
765
  const sessionNameNudge = args["session-name-nudge"] === "true";
763
766
  const taskNudge = args["task-nudge"] === "true";
764
767
  const hostPromptReminder = args["host-prompt-reminder"] === "true";
@@ -769,7 +772,7 @@ async function handlePromptContext(root: string, rest: string[]): Promise<number
769
772
  args["turn-ritual-nudge"] === "true" ? "generic" : args["turn-ritual-nudge"];
770
773
  if (!instanceId) {
771
774
  process.stderr.write(
772
- "agent-coord prompt-context --instance <id> [--session <id>] [--name <agent-name>] [--session-name-nudge] [--task-nudge] [--host-prompt-reminder] [--status-footer-nudge] [--turn-ritual-nudge <adapter>]\n",
775
+ "agent-coord prompt-context --instance <id> [--session <id>] [--name <agent-name>] [--adapter <id>] [--session-name-nudge] [--task-nudge] [--host-prompt-reminder] [--status-footer-nudge] [--turn-ritual-nudge <adapter>]\n",
773
776
  );
774
777
  return 2;
775
778
  }
@@ -784,6 +787,7 @@ async function handlePromptContext(root: string, rest: string[]): Promise<number
784
787
  hostPromptReminder,
785
788
  statusFooterNudge,
786
789
  turnRitualNudge,
790
+ adapter,
787
791
  });
788
792
  process.stdout.write(text);
789
793
  return 0;
@@ -0,0 +1,142 @@
1
+ import { closeSync, existsSync, lstatSync, openSync, readSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import type { SupervisorCapability, SupervisorSourceReference } from "../supervisor/contract.ts";
4
+
5
+ export interface CoordinationHealthDiagnostic {
6
+ code: "malformed_json" | "invalid_contract";
7
+ byte_offset: number;
8
+ segment_ordinal: number;
9
+ event_id?: string;
10
+ }
11
+
12
+ export interface CoordinationHealthSnapshot {
13
+ observed_at: string;
14
+ capability: SupervisorCapability;
15
+ recent_events: readonly SupervisorSourceReference[];
16
+ diagnostics: readonly CoordinationHealthDiagnostic[];
17
+ omitted_event_count: number;
18
+ }
19
+
20
+ const MAX_EVENT_REFERENCES = 64;
21
+ const MAX_DIAGNOSTICS = 16;
22
+ const MAX_TAIL_BYTES = 256 * 1_024;
23
+
24
+ /** Read bounded V3 references without retaining event payloads or the full ledger reader. */
25
+ export function collectCoordinationHealthSnapshot(
26
+ coordRoot: string,
27
+ now = new Date(),
28
+ ): CoordinationHealthSnapshot {
29
+ const observedAt = now.toISOString();
30
+ const active = join(resolve(coordRoot), ".harnery", "ledgers", "v3", "active.ndjson");
31
+ if (!existsSync(active)) return unavailable(observedAt, "ledger-missing");
32
+
33
+ let handle: number | undefined;
34
+ try {
35
+ const stat = lstatSync(active);
36
+ if (stat.isSymbolicLink() || !stat.isFile()) return unavailable(observedAt, "ledger-unsafe");
37
+ if (stat.size === 0) return available(observedAt, [], [], 0);
38
+ const start = Math.max(0, stat.size - MAX_TAIL_BYTES);
39
+ const bytes = Buffer.allocUnsafe(stat.size - start);
40
+ handle = openSync(active, "r");
41
+ const count = readSync(handle, bytes, 0, bytes.length, start);
42
+ const lines = bytes.subarray(0, count).toString("utf8").split("\n");
43
+ if (start > 0) lines.shift();
44
+ const completeLines = lines.filter((line) => line.length > 0);
45
+ const diagnostics: CoordinationHealthDiagnostic[] = [];
46
+ const references: SupervisorSourceReference[] = [];
47
+ let byteOffset = start;
48
+ for (const line of completeLines) {
49
+ const reference = parseReference(line);
50
+ if (reference) references.push(reference);
51
+ else if (diagnostics.length < MAX_DIAGNOSTICS) {
52
+ diagnostics.push({
53
+ code: line.trimStart().startsWith("{") ? "invalid_contract" : "malformed_json",
54
+ byte_offset: byteOffset,
55
+ segment_ordinal: 1,
56
+ });
57
+ }
58
+ byteOffset += Buffer.byteLength(line) + 1;
59
+ }
60
+ const recent = references.slice(-MAX_EVENT_REFERENCES);
61
+ return available(
62
+ observedAt,
63
+ recent,
64
+ diagnostics,
65
+ Math.max(0, references.length - recent.length) + (start > 0 ? 1 : 0),
66
+ );
67
+ } catch {
68
+ return unavailable(observedAt, "tail-read-failed", "error");
69
+ } finally {
70
+ if (handle !== undefined) closeSync(handle);
71
+ }
72
+ }
73
+
74
+ function parseReference(line: string): SupervisorSourceReference | undefined {
75
+ try {
76
+ const value = JSON.parse(line) as Record<string, unknown>;
77
+ const contract = object(value.contract);
78
+ const time = object(value.time);
79
+ const producer = object(value.producer);
80
+ if (
81
+ contract.major !== 3 ||
82
+ typeof value.event_id !== "string" ||
83
+ typeof value.event_type !== "string" ||
84
+ typeof time.observed_at !== "string" ||
85
+ typeof producer.sequence !== "number" ||
86
+ !Number.isSafeInteger(producer.sequence)
87
+ )
88
+ return undefined;
89
+ return {
90
+ id: `v3:${value.event_id}`,
91
+ source_kind: "coordination.v3",
92
+ source_id: value.event_type,
93
+ record_id: value.event_id,
94
+ sequence: producer.sequence,
95
+ schema_version: 3,
96
+ observed_at: time.observed_at,
97
+ capability: "supported",
98
+ };
99
+ } catch {
100
+ return undefined;
101
+ }
102
+ }
103
+
104
+ function object(value: unknown): Record<string, unknown> {
105
+ return value && typeof value === "object" && !Array.isArray(value)
106
+ ? (value as Record<string, unknown>)
107
+ : {};
108
+ }
109
+
110
+ function available(
111
+ observedAt: string,
112
+ recentEvents: readonly SupervisorSourceReference[],
113
+ diagnostics: readonly CoordinationHealthDiagnostic[],
114
+ omittedEventCount: number,
115
+ ): CoordinationHealthSnapshot {
116
+ const state = diagnostics.length === 0 ? "supported" : "partial";
117
+ return {
118
+ observed_at: observedAt,
119
+ capability: {
120
+ source_kind: "coordination.v3.references",
121
+ state,
122
+ ...(state === "partial" ? { reason_code: diagnostics[0]?.code ?? "tail-invalid" } : {}),
123
+ },
124
+ recent_events: recentEvents.map((reference) => ({ ...reference, capability: state })),
125
+ diagnostics,
126
+ omitted_event_count: omittedEventCount,
127
+ };
128
+ }
129
+
130
+ function unavailable(
131
+ observedAt: string,
132
+ reasonCode: string,
133
+ state: "unsupported" | "error" = "unsupported",
134
+ ): CoordinationHealthSnapshot {
135
+ return {
136
+ observed_at: observedAt,
137
+ capability: { source_kind: "coordination.v3.references", state, reason_code: reasonCode },
138
+ recent_events: [],
139
+ diagnostics: [],
140
+ omitted_event_count: 0,
141
+ };
142
+ }