harnery 0.29.0 → 0.31.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 (512) hide show
  1. package/README.md +4 -4
  2. package/dist/commander.d.ts +8 -0
  3. package/dist/commander.d.ts.map +1 -1
  4. package/dist/commander.js +14 -13
  5. package/dist/commands/adapter.d.ts +12 -0
  6. package/dist/commands/adapter.d.ts.map +1 -0
  7. package/dist/commands/{harness.js → adapter.js} +35 -35
  8. package/dist/commands/agents.d.ts +3 -2
  9. package/dist/commands/agents.d.ts.map +1 -1
  10. package/dist/commands/agents.js +61 -60
  11. package/dist/commands/artifacts.d.ts +4 -0
  12. package/dist/commands/artifacts.d.ts.map +1 -0
  13. package/dist/commands/artifacts.js +143 -0
  14. package/dist/commands/backup.d.ts +1 -1
  15. package/dist/commands/backup.d.ts.map +1 -1
  16. package/dist/commands/backup.js +5 -2
  17. package/dist/commands/browse.d.ts.map +1 -1
  18. package/dist/commands/browse.js +310 -7
  19. package/dist/commands/checkpoint.d.ts +12 -0
  20. package/dist/commands/checkpoint.d.ts.map +1 -0
  21. package/dist/commands/checkpoint.js +162 -0
  22. package/dist/commands/context.d.ts +2 -2
  23. package/dist/commands/context.d.ts.map +1 -1
  24. package/dist/commands/context.js +3 -151
  25. package/dist/commands/decision.d.ts.map +1 -1
  26. package/dist/commands/decision.js +31 -7
  27. package/dist/commands/deinit.d.ts +2 -2
  28. package/dist/commands/deinit.js +15 -15
  29. package/dist/commands/doctor.js +23 -23
  30. package/dist/commands/governor.d.ts +4 -0
  31. package/dist/commands/governor.d.ts.map +1 -0
  32. package/dist/commands/{supervisor.js → governor.js} +133 -108
  33. package/dist/commands/init.d.ts +12 -12
  34. package/dist/commands/init.d.ts.map +1 -1
  35. package/dist/commands/init.js +44 -30
  36. package/dist/commands/{session.d.ts → journal.d.ts} +2 -2
  37. package/dist/commands/journal.d.ts.map +1 -0
  38. package/dist/commands/{scratch.js → journal.js} +47 -47
  39. package/dist/commands/policy.js +1 -1
  40. package/dist/commands/presence.js +2 -2
  41. package/dist/commands/sync.d.ts +1 -1
  42. package/dist/commands/sync.js +3 -3
  43. package/dist/commands/tunnel.d.ts +16 -0
  44. package/dist/commands/tunnel.d.ts.map +1 -1
  45. package/dist/commands/tunnel.js +193 -23
  46. package/dist/commands/web-fetch.js +1 -1
  47. package/dist/commands/web.d.ts +19 -0
  48. package/dist/commands/web.d.ts.map +1 -1
  49. package/dist/commands/web.js +51 -0
  50. package/dist/commands/work.d.ts +2 -1
  51. package/dist/commands/work.d.ts.map +1 -1
  52. package/dist/commands/work.js +55 -25
  53. package/dist/commands/workflow.d.ts.map +1 -1
  54. package/dist/commands/workflow.js +35 -28
  55. package/dist/core/{harnesses → adapters}/attest-projection.d.ts +2 -2
  56. package/dist/core/adapters/attest-projection.d.ts.map +1 -0
  57. package/dist/core/{harnesses → adapters}/attest.d.ts +16 -16
  58. package/dist/core/adapters/attest.d.ts.map +1 -0
  59. package/dist/core/{harnesses → adapters}/attest.js +9 -9
  60. package/dist/core/{harnesses → adapters}/attestation.d.ts +16 -16
  61. package/dist/core/adapters/attestation.d.ts.map +1 -0
  62. package/dist/core/{harnesses → adapters}/attestation.js +20 -20
  63. package/dist/core/{harnesses → adapters}/bench.d.ts +12 -12
  64. package/dist/core/adapters/bench.d.ts.map +1 -0
  65. package/dist/core/{harnesses → adapters}/bench.js +12 -12
  66. package/dist/core/adapters/index.d.ts +13 -0
  67. package/dist/core/adapters/index.d.ts.map +1 -0
  68. package/dist/core/adapters/index.js +6 -0
  69. package/dist/core/{harnesses → adapters}/profiles.d.ts +11 -11
  70. package/dist/core/adapters/profiles.d.ts.map +1 -0
  71. package/dist/core/{harnesses → adapters}/profiles.js +11 -11
  72. package/dist/core/adapters/registry.d.ts +15 -0
  73. package/dist/core/adapters/registry.d.ts.map +1 -0
  74. package/dist/core/{harnesses → adapters}/registry.js +11 -11
  75. package/dist/core/{harnesses → adapters}/types.d.ts +28 -23
  76. package/dist/core/adapters/types.d.ts.map +1 -0
  77. package/dist/core/{harnesses → adapters}/types.js +1 -1
  78. package/dist/core/agents/canonical-emit.d.ts +4 -4
  79. package/dist/core/agents/canonical-emit.js +5 -5
  80. package/dist/core/agents/cli-emit.js +1 -1
  81. package/dist/core/agents/cli.js +30 -26
  82. package/dist/core/agents/codex-replay.d.ts +1 -1
  83. package/dist/core/agents/codex-replay.js +6 -6
  84. package/dist/core/agents/coord-client.d.ts +21 -2
  85. package/dist/core/agents/coord-client.d.ts.map +1 -1
  86. package/dist/core/agents/coord-client.js +122 -27
  87. package/dist/core/agents/events/consume.d.ts +1 -1
  88. package/dist/core/agents/events/emit.d.ts +5 -6
  89. package/dist/core/agents/events/emit.d.ts.map +1 -1
  90. package/dist/core/agents/events/emit.js +2 -2
  91. package/dist/core/agents/render/prompt-context.d.ts +21 -9
  92. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  93. package/dist/core/agents/render/prompt-context.js +82 -30
  94. package/dist/core/agents/render/session-context.d.ts +1 -1
  95. package/dist/core/agents/render/session-context.js +5 -5
  96. package/dist/core/agents/rules/claim-conflict.d.ts +2 -6
  97. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  98. package/dist/core/agents/rules/claim-conflict.js +1 -1
  99. package/dist/core/agents/rules/commit-conflict.js +98 -22
  100. package/dist/core/agents/rules/stop-hook.d.ts +9 -9
  101. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  102. package/dist/core/agents/rules/stop-hook.js +23 -5
  103. package/dist/core/agents/rules/verdict.d.ts +14 -0
  104. package/dist/core/agents/rules/verdict.d.ts.map +1 -0
  105. package/dist/core/agents/rules/verdict.js +1 -0
  106. package/dist/core/agents/session-events.js +3 -3
  107. package/dist/core/agents/state/council.d.ts +1 -1
  108. package/dist/core/agents/state/council.js +1 -1
  109. package/dist/core/agents/state/heartbeat-projector.d.ts +3 -2
  110. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  111. package/dist/core/agents/state/heartbeat-projector.js +14 -10
  112. package/dist/core/agents/state/heartbeat-writer.d.ts +1 -1
  113. package/dist/core/agents/state/heartbeat-writer.js +12 -12
  114. package/dist/core/agents/state/{scratch.d.ts → journal.d.ts} +7 -7
  115. package/dist/core/agents/state/{scratch.d.ts.map → journal.d.ts.map} +1 -1
  116. package/dist/core/agents/state/{scratch.js → journal.js} +12 -12
  117. package/dist/core/agents/state/pidmap.d.ts +28 -4
  118. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  119. package/dist/core/agents/state/pidmap.js +130 -15
  120. package/dist/core/agents/state/proc-start.d.ts +95 -0
  121. package/dist/core/agents/state/proc-start.d.ts.map +1 -0
  122. package/dist/core/agents/state/proc-start.js +199 -0
  123. package/dist/core/agents/state/shell-mutation.d.ts +1 -1
  124. package/dist/core/agents/state/shell-mutation.js +1 -1
  125. package/dist/core/agents/state/stale-sweep.js +7 -7
  126. package/dist/core/artifacts/index.d.ts +86 -0
  127. package/dist/core/artifacts/index.d.ts.map +1 -0
  128. package/dist/core/artifacts/index.js +421 -0
  129. package/dist/core/config.d.ts +9 -2
  130. package/dist/core/config.d.ts.map +1 -1
  131. package/dist/core/config.js +26 -2
  132. package/dist/core/context/index.d.ts +12 -12
  133. package/dist/core/context/index.js +10 -10
  134. package/dist/core/governor/adapter-spread.d.ts +52 -0
  135. package/dist/core/governor/adapter-spread.d.ts.map +1 -0
  136. package/dist/core/governor/adapter-spread.js +52 -0
  137. package/dist/core/governor/index.d.ts +7 -0
  138. package/dist/core/governor/index.d.ts.map +1 -0
  139. package/dist/core/governor/index.js +6 -0
  140. package/dist/core/governor/plan-api.d.ts +38 -0
  141. package/dist/core/governor/plan-api.d.ts.map +1 -0
  142. package/dist/core/governor/plan-api.js +83 -0
  143. package/dist/core/governor/plan-read.d.ts +5 -0
  144. package/dist/core/governor/plan-read.d.ts.map +1 -0
  145. package/dist/core/{supervisor → governor}/plan-read.js +71 -51
  146. package/dist/core/governor/plan-types.d.ts +181 -0
  147. package/dist/core/governor/plan-types.d.ts.map +1 -0
  148. package/dist/core/{supervisor → governor}/plan-types.js +4 -4
  149. package/dist/core/governor/planning.d.ts +48 -0
  150. package/dist/core/governor/planning.d.ts.map +1 -0
  151. package/dist/core/{supervisor → governor}/planning.js +143 -94
  152. package/dist/core/governor/read.d.ts +6 -0
  153. package/dist/core/governor/read.d.ts.map +1 -0
  154. package/dist/core/governor/read.js +4 -0
  155. package/dist/core/governor/runner.d.ts +37 -0
  156. package/dist/core/governor/runner.d.ts.map +1 -0
  157. package/dist/core/{supervisor → governor}/runner.js +15 -15
  158. package/dist/core/governor/service-read.d.ts +6 -0
  159. package/dist/core/governor/service-read.d.ts.map +1 -0
  160. package/dist/core/{supervisor → governor}/service-read.js +12 -12
  161. package/dist/core/governor/service.d.ts +114 -0
  162. package/dist/core/governor/service.d.ts.map +1 -0
  163. package/dist/core/{supervisor → governor}/service.js +57 -57
  164. package/dist/core/governor/state.d.ts +116 -0
  165. package/dist/core/governor/state.d.ts.map +1 -0
  166. package/dist/core/{supervisor → governor}/state.js +290 -116
  167. package/dist/core/hooks/adapter/detect.d.ts +9 -0
  168. package/dist/core/hooks/{harness → adapter}/detect.d.ts.map +1 -1
  169. package/dist/core/hooks/{harness → adapter}/detect.js +8 -8
  170. package/dist/core/hooks/{harness → adapter}/events.d.ts +15 -14
  171. package/dist/core/hooks/adapter/events.d.ts.map +1 -0
  172. package/dist/core/hooks/{harness → adapter}/events.js +5 -5
  173. package/dist/core/hooks/{harness → adapter}/output.d.ts +20 -14
  174. package/dist/core/hooks/adapter/output.d.ts.map +1 -0
  175. package/dist/core/hooks/{harness → adapter}/output.js +30 -22
  176. package/dist/core/hooks/{harness → adapter}/parse.d.ts +5 -5
  177. package/dist/core/hooks/{harness → adapter}/parse.d.ts.map +1 -1
  178. package/dist/core/hooks/{harness → adapter}/parse.js +4 -4
  179. package/dist/core/hooks/{harness → adapter}/wiring.d.ts +15 -15
  180. package/dist/core/hooks/{harness → adapter}/wiring.d.ts.map +1 -1
  181. package/dist/core/hooks/{harness → adapter}/wiring.js +13 -13
  182. package/dist/core/hooks/cli.d.ts +3 -3
  183. package/dist/core/hooks/cli.js +175 -151
  184. package/dist/core/hooks/effects/image-capture.d.ts +5 -5
  185. package/dist/core/hooks/effects/image-capture.d.ts.map +1 -1
  186. package/dist/core/hooks/effects/image-capture.js +3 -3
  187. package/dist/core/hooks/effects/index.d.ts +12 -12
  188. package/dist/core/hooks/effects/index.d.ts.map +1 -1
  189. package/dist/core/hooks/effects/index.js +17 -17
  190. package/dist/core/hooks/events/emit.d.ts +2 -2
  191. package/dist/core/hooks/events/emit.d.ts.map +1 -1
  192. package/dist/core/hooks/events/emit.js +1 -1
  193. package/dist/core/hooks/events/rotate.js +1 -1
  194. package/dist/core/hooks/events/schema.d.ts +13 -5
  195. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  196. package/dist/core/hooks/guard-path.d.ts +2 -2
  197. package/dist/core/hooks/guard-path.js +2 -2
  198. package/dist/core/hooks/index.d.ts +1 -1
  199. package/dist/core/hooks/index.d.ts.map +1 -1
  200. package/dist/core/hooks/resolve/anchor.d.ts +14 -5
  201. package/dist/core/hooks/resolve/anchor.d.ts.map +1 -1
  202. package/dist/core/hooks/resolve/anchor.js +64 -7
  203. package/dist/core/hooks/resolve/coord-root.d.ts +3 -3
  204. package/dist/core/hooks/resolve/coord-root.js +4 -4
  205. package/dist/core/hooks/resolve/intent.js +1 -1
  206. package/dist/core/hooks/resolve/owner.d.ts +1 -1
  207. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  208. package/dist/core/hooks/resolve/owner.js +11 -5
  209. package/dist/core/{scratch → journal}/index.d.ts +25 -25
  210. package/dist/core/{scratch → journal}/index.d.ts.map +1 -1
  211. package/dist/core/{scratch → journal}/index.js +38 -38
  212. package/dist/core/policy/evaluate.d.ts +1 -1
  213. package/dist/core/policy/evaluate.d.ts.map +1 -1
  214. package/dist/core/policy/evaluate.js +7 -7
  215. package/dist/core/policy/types.d.ts +3 -3
  216. package/dist/core/policy/types.d.ts.map +1 -1
  217. package/dist/core/work/runner.d.ts.map +1 -1
  218. package/dist/core/work/runner.js +11 -2
  219. package/dist/core/work/state.d.ts +28 -3
  220. package/dist/core/work/state.d.ts.map +1 -1
  221. package/dist/core/work/state.js +148 -16
  222. package/dist/core/workflow/adapters.d.ts +17 -0
  223. package/dist/core/workflow/adapters.d.ts.map +1 -0
  224. package/dist/core/workflow/adapters.js +21 -0
  225. package/dist/core/workflow/approvals.js +6 -6
  226. package/dist/core/workflow/billing.d.ts +9 -9
  227. package/dist/core/workflow/billing.js +9 -9
  228. package/dist/core/workflow/child-env.d.ts +10 -2
  229. package/dist/core/workflow/child-env.d.ts.map +1 -1
  230. package/dist/core/workflow/child-env.js +12 -4
  231. package/dist/core/workflow/engine.d.ts +15 -5
  232. package/dist/core/workflow/engine.d.ts.map +1 -1
  233. package/dist/core/workflow/engine.js +151 -83
  234. package/dist/core/workflow/evidence-preflight.d.ts +24 -0
  235. package/dist/core/workflow/evidence-preflight.d.ts.map +1 -0
  236. package/dist/core/workflow/evidence-preflight.js +134 -0
  237. package/dist/core/workflow/index.d.ts +3 -2
  238. package/dist/core/workflow/index.d.ts.map +1 -1
  239. package/dist/core/workflow/index.js +2 -1
  240. package/dist/core/workflow/proof.d.ts +28 -5
  241. package/dist/core/workflow/proof.d.ts.map +1 -1
  242. package/dist/core/workflow/proof.js +77 -34
  243. package/dist/core/workflow/run-state.d.ts +1 -1
  244. package/dist/core/workflow/run-state.js +7 -7
  245. package/dist/core/workflow/sandbox-projection.d.ts +6 -6
  246. package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
  247. package/dist/core/workflow/sandbox-projection.js +11 -11
  248. package/dist/core/workflow/spawn-claude.d.ts +3 -3
  249. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  250. package/dist/core/workflow/spawn-claude.js +28 -8
  251. package/dist/core/workflow/spawn-codex.d.ts +4 -4
  252. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  253. package/dist/core/workflow/spawn-codex.js +26 -8
  254. package/dist/core/workflow/spawn-cursor.d.ts +4 -4
  255. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  256. package/dist/core/workflow/spawn-cursor.js +27 -8
  257. package/dist/core/workflow/spawn-failure.d.ts +15 -1
  258. package/dist/core/workflow/spawn-failure.d.ts.map +1 -1
  259. package/dist/core/workflow/spawn-failure.js +35 -1
  260. package/dist/core/workflow/specialists.js +2 -2
  261. package/dist/core/workflow/{journal.d.ts → transcript.d.ts} +8 -8
  262. package/dist/core/workflow/transcript.d.ts.map +1 -0
  263. package/dist/core/workflow/{journal.js → transcript.js} +14 -14
  264. package/dist/core/workflow/types.d.ts +119 -46
  265. package/dist/core/workflow/types.d.ts.map +1 -1
  266. package/dist/core/workflow/types.js +11 -1
  267. package/dist/core/workflow/validate.d.ts +1 -1
  268. package/dist/core/workflow/validate.js +1 -1
  269. package/dist/core/workflow/workspaces/cancellation.js +2 -2
  270. package/dist/core/workflow/workspaces/cleanup.js +2 -2
  271. package/dist/core/workflow/workspaces/execution.js +6 -6
  272. package/dist/core/workflow/workspaces/inspect.js +9 -9
  273. package/dist/core/workflow/workspaces/integration.js +14 -14
  274. package/dist/core/workflow/workspaces/lifecycle.d.ts +1 -1
  275. package/dist/core/workflow/workspaces/lifecycle.d.ts.map +1 -1
  276. package/dist/core/workflow/workspaces/lifecycle.js +9 -9
  277. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  278. package/dist/core/workflow/workspaces/local-git.js +39 -5
  279. package/dist/core/workflow/workspaces/recovery.js +3 -3
  280. package/dist/core/workflow/workspaces/state.d.ts +1 -1
  281. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  282. package/dist/core/workflow/workspaces/state.js +3 -3
  283. package/dist/core/workflow/workspaces/types.d.ts +1 -1
  284. package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
  285. package/dist/lib/browser/asserts.d.ts +28 -0
  286. package/dist/lib/browser/asserts.d.ts.map +1 -0
  287. package/dist/lib/browser/asserts.js +152 -0
  288. package/dist/lib/browser/client.d.ts +49 -0
  289. package/dist/lib/browser/client.d.ts.map +1 -1
  290. package/dist/lib/browser/client.js +155 -14
  291. package/dist/lib/browser/content-checks.d.ts +108 -0
  292. package/dist/lib/browser/content-checks.d.ts.map +1 -0
  293. package/dist/lib/browser/content-checks.js +474 -0
  294. package/dist/lib/browser/critique.d.ts +91 -0
  295. package/dist/lib/browser/critique.d.ts.map +1 -0
  296. package/dist/lib/browser/critique.js +167 -0
  297. package/dist/lib/browser/geometry.d.ts +32 -0
  298. package/dist/lib/browser/geometry.d.ts.map +1 -1
  299. package/dist/lib/browser/geometry.js +244 -1
  300. package/dist/lib/browser/index.d.ts +4 -1
  301. package/dist/lib/browser/index.d.ts.map +1 -1
  302. package/dist/lib/browser/index.js +2 -0
  303. package/dist/lib/claude-desktop.d.ts.map +1 -1
  304. package/dist/lib/claude-desktop.js +4 -1
  305. package/dist/lib/council/index.d.ts +6 -6
  306. package/dist/lib/council/index.d.ts.map +1 -1
  307. package/dist/lib/council/index.js +8 -8
  308. package/dist/lib/exec.d.ts +7 -0
  309. package/dist/lib/exec.d.ts.map +1 -1
  310. package/dist/lib/exec.js +6 -3
  311. package/dist/lib/identities/assume.d.ts +2 -2
  312. package/dist/lib/identities/assume.js +7 -7
  313. package/dist/lib/instructions/apply.d.ts +3 -3
  314. package/dist/lib/instructions/apply.d.ts.map +1 -1
  315. package/dist/lib/instructions/apply.js +75 -27
  316. package/dist/lib/instructions/host-addendum.d.ts +54 -0
  317. package/dist/lib/instructions/host-addendum.d.ts.map +1 -0
  318. package/dist/lib/instructions/host-addendum.js +107 -0
  319. package/dist/lib/instructions/templates.d.ts +10 -3
  320. package/dist/lib/instructions/templates.d.ts.map +1 -1
  321. package/dist/lib/instructions/templates.js +41 -12
  322. package/dist/lib/machine.d.ts +2 -2
  323. package/dist/lib/machine.js +2 -2
  324. package/package.json +26 -21
  325. package/schemas/config.schema.json +26 -1
  326. package/src/commander.ts +22 -13
  327. package/src/commands/{harness.ts → adapter.ts} +46 -46
  328. package/src/commands/agents.ts +71 -64
  329. package/src/commands/artifacts.ts +168 -0
  330. package/src/commands/backup.ts +47 -33
  331. package/src/commands/browse.ts +438 -2
  332. package/src/commands/checkpoint.ts +196 -0
  333. package/src/commands/context.ts +3 -176
  334. package/src/commands/decision.ts +41 -7
  335. package/src/commands/deinit.ts +16 -16
  336. package/src/commands/doctor.ts +28 -28
  337. package/src/commands/{supervisor.ts → governor.ts} +184 -154
  338. package/src/commands/init.ts +50 -34
  339. package/src/commands/{scratch.ts → journal.ts} +60 -60
  340. package/src/commands/policy.ts +1 -1
  341. package/src/commands/presence.ts +2 -2
  342. package/src/commands/sync.ts +3 -3
  343. package/src/commands/tunnel.ts +237 -34
  344. package/src/commands/web-fetch.ts +1 -1
  345. package/src/commands/web.ts +56 -1
  346. package/src/commands/work.ts +72 -38
  347. package/src/commands/workflow.ts +43 -34
  348. package/src/core/{harnesses → adapters}/attest-projection.ts +2 -2
  349. package/src/core/{harnesses → adapters}/attest.ts +27 -27
  350. package/src/core/{harnesses → adapters}/attestation.ts +41 -41
  351. package/src/core/{harnesses → adapters}/bench.ts +34 -43
  352. package/src/core/adapters/index.ts +56 -0
  353. package/src/core/{harnesses → adapters}/profiles.ts +16 -16
  354. package/src/core/{harnesses → adapters}/registry.ts +23 -23
  355. package/src/core/{harnesses → adapters}/types.ts +28 -23
  356. package/src/core/agents/canonical-emit.ts +6 -6
  357. package/src/core/agents/cli-emit.ts +1 -1
  358. package/src/core/agents/cli.ts +30 -28
  359. package/src/core/agents/codex-replay.ts +6 -6
  360. package/src/core/agents/coord-client.ts +119 -28
  361. package/src/core/agents/events/consume.ts +1 -1
  362. package/src/core/agents/events/emit.ts +7 -6
  363. package/src/core/agents/render/prompt-context.ts +107 -31
  364. package/src/core/agents/render/session-context.ts +6 -6
  365. package/src/core/agents/rules/claim-conflict.ts +4 -7
  366. package/src/core/agents/rules/commit-conflict.ts +103 -22
  367. package/src/core/agents/rules/stop-hook.ts +33 -17
  368. package/src/core/agents/rules/verdict.ts +13 -0
  369. package/src/core/agents/session-events.ts +4 -4
  370. package/src/core/agents/state/council.ts +1 -1
  371. package/src/core/agents/state/heartbeat-projector.ts +16 -12
  372. package/src/core/agents/state/heartbeat-writer.ts +12 -12
  373. package/src/core/agents/state/{scratch.ts → journal.ts} +12 -12
  374. package/src/core/agents/state/pidmap.ts +130 -15
  375. package/src/core/agents/state/proc-start.ts +208 -0
  376. package/src/core/agents/state/shell-mutation.ts +1 -1
  377. package/src/core/agents/state/stale-sweep.ts +7 -7
  378. package/src/core/artifacts/index.ts +557 -0
  379. package/src/core/config.ts +30 -2
  380. package/src/core/context/index.ts +15 -15
  381. package/src/core/governor/adapter-spread.ts +82 -0
  382. package/src/core/governor/index.ts +91 -0
  383. package/src/core/governor/plan-api.ts +115 -0
  384. package/src/core/{supervisor → governor}/plan-read.ts +120 -96
  385. package/src/core/{supervisor → governor}/plan-types.ts +64 -50
  386. package/src/core/{supervisor → governor}/planning.ts +240 -188
  387. package/src/core/governor/read.ts +59 -0
  388. package/src/core/{supervisor → governor}/runner.ts +39 -39
  389. package/src/core/{supervisor → governor}/service-read.ts +22 -22
  390. package/src/core/{supervisor → governor}/service.ts +117 -119
  391. package/src/core/{supervisor → governor}/state.ts +389 -174
  392. package/src/core/hooks/adapter/detect.ts +30 -0
  393. package/src/core/hooks/{harness → adapter}/events.ts +16 -14
  394. package/src/core/hooks/{harness → adapter}/output.ts +31 -23
  395. package/src/core/hooks/{harness → adapter}/parse.ts +5 -5
  396. package/src/core/hooks/{harness → adapter}/wiring.ts +21 -21
  397. package/src/core/hooks/cli.ts +185 -162
  398. package/src/core/hooks/effects/image-capture.ts +6 -6
  399. package/src/core/hooks/effects/index.ts +17 -17
  400. package/src/core/hooks/events/emit.ts +3 -3
  401. package/src/core/hooks/events/rotate.ts +1 -1
  402. package/src/core/hooks/events/schema.ts +15 -7
  403. package/src/core/hooks/guard-path.ts +2 -2
  404. package/src/core/hooks/index.ts +1 -1
  405. package/src/core/hooks/resolve/anchor.ts +63 -8
  406. package/src/core/hooks/resolve/coord-root.ts +4 -4
  407. package/src/core/hooks/resolve/intent.ts +1 -1
  408. package/src/core/hooks/resolve/owner.ts +11 -5
  409. package/src/core/{scratch → journal}/index.ts +53 -53
  410. package/src/core/policy/evaluate.ts +8 -8
  411. package/src/core/policy/types.ts +3 -3
  412. package/src/core/work/runner.ts +11 -2
  413. package/src/core/work/state.ts +185 -21
  414. package/src/core/workflow/adapters.ts +33 -0
  415. package/src/core/workflow/approvals.ts +7 -7
  416. package/src/core/workflow/billing.ts +12 -12
  417. package/src/core/workflow/child-env.ts +13 -4
  418. package/src/core/workflow/engine.ts +166 -90
  419. package/src/core/workflow/evidence-preflight.ts +165 -0
  420. package/src/core/workflow/index.ts +6 -3
  421. package/src/core/workflow/proof.ts +94 -48
  422. package/src/core/workflow/run-state.ts +7 -7
  423. package/src/core/workflow/sandbox-projection.ts +16 -16
  424. package/src/core/workflow/spawn-claude.ts +34 -12
  425. package/src/core/workflow/spawn-codex.ts +30 -12
  426. package/src/core/workflow/spawn-cursor.ts +31 -12
  427. package/src/core/workflow/spawn-failure.ts +41 -1
  428. package/src/core/workflow/specialists.ts +2 -2
  429. package/src/core/workflow/{journal.ts → transcript.ts} +16 -16
  430. package/src/core/workflow/types.ts +130 -46
  431. package/src/core/workflow/validate.ts +1 -1
  432. package/src/core/workflow/workspaces/cancellation.ts +2 -2
  433. package/src/core/workflow/workspaces/cleanup.ts +2 -2
  434. package/src/core/workflow/workspaces/execution.ts +6 -6
  435. package/src/core/workflow/workspaces/inspect.ts +14 -11
  436. package/src/core/workflow/workspaces/integration.ts +18 -14
  437. package/src/core/workflow/workspaces/lifecycle.ts +10 -10
  438. package/src/core/workflow/workspaces/local-git.ts +47 -5
  439. package/src/core/workflow/workspaces/recovery.ts +3 -3
  440. package/src/core/workflow/workspaces/state.ts +4 -3
  441. package/src/core/workflow/workspaces/types.ts +1 -1
  442. package/src/lib/browser/asserts.ts +177 -0
  443. package/src/lib/browser/client.ts +189 -14
  444. package/src/lib/browser/content-checks.ts +571 -0
  445. package/src/lib/browser/critique.ts +231 -0
  446. package/src/lib/browser/geometry.ts +297 -1
  447. package/src/lib/browser/index.ts +34 -0
  448. package/src/lib/claude-desktop.ts +4 -1
  449. package/src/lib/council/index.ts +9 -9
  450. package/src/lib/exec.ts +13 -3
  451. package/src/lib/identities/assume.ts +7 -7
  452. package/src/lib/instructions/apply.ts +87 -25
  453. package/src/lib/instructions/host-addendum.ts +126 -0
  454. package/src/lib/instructions/templates.ts +44 -15
  455. package/src/lib/machine.ts +2 -2
  456. package/dist/commands/harness.d.ts +0 -12
  457. package/dist/commands/harness.d.ts.map +0 -1
  458. package/dist/commands/scratch.d.ts +0 -4
  459. package/dist/commands/scratch.d.ts.map +0 -1
  460. package/dist/commands/session.d.ts.map +0 -1
  461. package/dist/commands/session.js +0 -162
  462. package/dist/commands/supervisor.d.ts +0 -4
  463. package/dist/commands/supervisor.d.ts.map +0 -1
  464. package/dist/core/harnesses/attest-projection.d.ts.map +0 -1
  465. package/dist/core/harnesses/attest.d.ts.map +0 -1
  466. package/dist/core/harnesses/attestation.d.ts.map +0 -1
  467. package/dist/core/harnesses/bench.d.ts.map +0 -1
  468. package/dist/core/harnesses/index.d.ts +0 -13
  469. package/dist/core/harnesses/index.d.ts.map +0 -1
  470. package/dist/core/harnesses/index.js +0 -6
  471. package/dist/core/harnesses/profiles.d.ts.map +0 -1
  472. package/dist/core/harnesses/registry.d.ts +0 -15
  473. package/dist/core/harnesses/registry.d.ts.map +0 -1
  474. package/dist/core/harnesses/types.d.ts.map +0 -1
  475. package/dist/core/hooks/harness/detect.d.ts +0 -9
  476. package/dist/core/hooks/harness/events.d.ts.map +0 -1
  477. package/dist/core/hooks/harness/output.d.ts.map +0 -1
  478. package/dist/core/supervisor/index.d.ts +0 -7
  479. package/dist/core/supervisor/index.d.ts.map +0 -1
  480. package/dist/core/supervisor/index.js +0 -6
  481. package/dist/core/supervisor/plan-api.d.ts +0 -23
  482. package/dist/core/supervisor/plan-api.d.ts.map +0 -1
  483. package/dist/core/supervisor/plan-api.js +0 -50
  484. package/dist/core/supervisor/plan-read.d.ts +0 -5
  485. package/dist/core/supervisor/plan-read.d.ts.map +0 -1
  486. package/dist/core/supervisor/plan-types.d.ts +0 -170
  487. package/dist/core/supervisor/plan-types.d.ts.map +0 -1
  488. package/dist/core/supervisor/planning.d.ts +0 -33
  489. package/dist/core/supervisor/planning.d.ts.map +0 -1
  490. package/dist/core/supervisor/read.d.ts +0 -6
  491. package/dist/core/supervisor/read.d.ts.map +0 -1
  492. package/dist/core/supervisor/read.js +0 -4
  493. package/dist/core/supervisor/runner.d.ts +0 -37
  494. package/dist/core/supervisor/runner.d.ts.map +0 -1
  495. package/dist/core/supervisor/service-read.d.ts +0 -6
  496. package/dist/core/supervisor/service-read.d.ts.map +0 -1
  497. package/dist/core/supervisor/service.d.ts +0 -114
  498. package/dist/core/supervisor/service.d.ts.map +0 -1
  499. package/dist/core/supervisor/state.d.ts +0 -95
  500. package/dist/core/supervisor/state.d.ts.map +0 -1
  501. package/dist/core/workflow/harnesses.d.ts +0 -17
  502. package/dist/core/workflow/harnesses.d.ts.map +0 -1
  503. package/dist/core/workflow/harnesses.js +0 -21
  504. package/dist/core/workflow/journal.d.ts.map +0 -1
  505. package/src/commands/session.ts +0 -187
  506. package/src/core/harnesses/index.ts +0 -56
  507. package/src/core/hooks/harness/detect.ts +0 -30
  508. package/src/core/supervisor/index.ts +0 -89
  509. package/src/core/supervisor/plan-api.ts +0 -73
  510. package/src/core/supervisor/read.ts +0 -59
  511. package/src/core/workflow/harnesses.ts +0 -33
  512. /package/dist/core/{harnesses → adapters}/attest-projection.js +0 -0
@@ -135,7 +135,7 @@ export interface WorkspaceLifecycleProjectionInput {
135
135
  binding?: WorkspaceBinding;
136
136
  provider_events?: readonly WorkspaceProviderEvent[];
137
137
  manifest?: WorkflowRunManifest;
138
- workflow_journal?: readonly {
138
+ workflow_transcript?: readonly {
139
139
  event: string;
140
140
  ok?: boolean;
141
141
  status?: "blocked" | "lost" | WorkspaceCancellationResult["status"];
@@ -207,22 +207,22 @@ export function deriveWorkspaceLifecycle(
207
207
  transition(next);
208
208
  }
209
209
 
210
- const journal = input.workflow_journal ?? [];
211
- for (const event of journal) {
210
+ const transcript = input.workflow_transcript ?? [];
211
+ for (const event of transcript) {
212
212
  if (input.binding && (event.event === "run.start" || event.event === "run.resume")) {
213
213
  transition("running");
214
214
  }
215
215
  if (event.event === "run.parked") transition("parked");
216
216
  }
217
- const failedReattachment = [...journal]
217
+ const failedReattachment = [...transcript]
218
218
  .reverse()
219
219
  .find((event) => event.event === "workspace.reattach.failed");
220
220
  if (failedReattachment) transition(failedReattachment.status === "blocked" ? "blocked" : "lost");
221
221
  let workflowOutcome = proofOutcome(input.proof, input.binding);
222
- const terminalJournal = [...journal].reverse().find((event) => event.event === "run.end");
222
+ const terminalTranscript = [...transcript].reverse().find((event) => event.event === "run.end");
223
223
  if (input.proof && !failedReattachment) transition(workflowOutcome!);
224
- if (terminalJournal && !input.proof && !failedReattachment) {
225
- workflowOutcome = terminalJournal.ok === false ? "failed_retained" : null;
224
+ if (terminalTranscript && !input.proof && !failedReattachment) {
225
+ workflowOutcome = terminalTranscript.ok === false ? "failed_retained" : null;
226
226
  transition("blocked");
227
227
  }
228
228
  if (projectedIntegrationState !== "none") transition("integration_requested");
@@ -269,7 +269,7 @@ export function deriveWorkspaceLifecycle(
269
269
  ((input.cancellation_receipt?.status === "cancelled" &&
270
270
  input.cancellation_receipt.work_event_seq === latestGovernance.seq &&
271
271
  input.cancellation_receipt.work_event_sha256 === workEventSha256) ||
272
- journal.some(
272
+ transcript.some(
273
273
  (event) =>
274
274
  event.event === "workspace.cancel" &&
275
275
  event.status === "cancelled" &&
@@ -459,8 +459,8 @@ function validateIntegrationAuthorization(
459
459
  !digest(authorization.policy_sha256) ||
460
460
  authorization.decision?.verdict !== "allow" ||
461
461
  authorization.decision_sha256 !== stableDigest(authorization.decision) ||
462
- authorization.journal_anchor?.event !== "integration.plan" ||
463
- authorization.journal_anchor.plan_sha256 !== planSha256 ||
462
+ authorization.transcript_anchor?.event !== "integration.plan" ||
463
+ authorization.transcript_anchor.plan_sha256 !== planSha256 ||
464
464
  !validTimestamp(authorization.authorized_at) ||
465
465
  (authorization.approval_id !== undefined &&
466
466
  (typeof authorization.approval_actor !== "string" || !digest(authorization.approval_sha256)))
@@ -1,6 +1,13 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
3
- import { dirname, isAbsolute, join, relative, resolve } from "node:path";
2
+ import {
3
+ existsSync,
4
+ lstatSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ realpathSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
11
  import { WorkspaceAttestationError } from "./attestation-error.ts";
5
12
  import {
6
13
  git,
@@ -68,6 +75,8 @@ import { isWorkspaceAttestation } from "./validate.ts";
68
75
 
69
76
  const PROVIDER_ID = "local-git-worktree";
70
77
  const PROVIDER_VERSION = "1";
78
+ const WORKSPACE_PARENT_SEGMENT = ".harnery-workspaces";
79
+ const LEGACY_WORKSPACE_PARENT_SEGMENT = "harnery-workspaces";
71
80
  const LOCK_STALE_MS = 5 * 60 * 1_000;
72
81
  const OBJECT_FORMAT = /^[a-f0-9]{40,64}$/;
73
82
  const REF_FORMAT = /^refs\/heads\/[A-Za-z0-9][A-Za-z0-9._/-]{0,180}$/;
@@ -221,7 +230,11 @@ async function allocate(
221
230
  );
222
231
  const authorityOutside = describeAuthorityOutsideRoot(repo, root.realpath);
223
232
  if (authorityOutside) throw new Error(authorityOutside);
224
- const workspaceRoot = candidateUnderRoot(root, ["harnery-workspaces", bindingId]);
233
+ // Claims are immutable, so a retry must reproduce the path the first attempt
234
+ // froze. Only a genuinely new allocation picks the current parent.
235
+ const priorClaim = readWorkspaceClaim(coordRoot, PROVIDER_ID, bindingId);
236
+ const parentSegment = priorClaim ? claimedParentSegment(priorClaim) : WORKSPACE_PARENT_SEGMENT;
237
+ const workspaceRoot = candidateUnderRoot(root, [parentSegment, bindingId]);
225
238
  const activeRoot = resolve(workspaceRoot, repo.requestedRelativePath);
226
239
  if (!containsPath(workspaceRoot, activeRoot)) {
227
240
  throw new Error("requested working directory escapes the isolated workspace");
@@ -287,6 +300,33 @@ async function allocate(
287
300
  }
288
301
  }
289
302
 
303
+ /**
304
+ * A claim freezes its workspace path at allocation time, so reconciliation must
305
+ * follow the parent recorded there rather than the parent this version would
306
+ * choose. Only the names this provider has ever allocated under are accepted, so
307
+ * a tampered claim cannot redirect creation into an unrelated directory.
308
+ */
309
+ function claimedParentSegment(claim: WorkspaceClaim): string {
310
+ const segment = basename(dirname(claim.workspace_root));
311
+ if (segment !== WORKSPACE_PARENT_SEGMENT && segment !== LEGACY_WORKSPACE_PARENT_SEGMENT) {
312
+ throw new Error("claimed workspace path is not under a provider-owned workspace parent");
313
+ }
314
+ return segment;
315
+ }
316
+
317
+ /**
318
+ * The parent holds real checkouts that would otherwise surface as untracked
319
+ * content in the surrounding repository. A directory whose every entry is
320
+ * ignored, including the ignore file itself, does not appear in `git status` at
321
+ * all, which keeps the guarantee with the provider instead of asking each
322
+ * consumer to maintain a rule.
323
+ */
324
+ function hideWorkspaceParentFromGit(parent: string): void {
325
+ const ignoreFile = join(parent, ".gitignore");
326
+ if (existsSync(ignoreFile)) return;
327
+ writeFileSync(ignoreFile, "*\n", { mode: 0o600 });
328
+ }
329
+
290
330
  function reconcileAllocation(
291
331
  coordRoot: string,
292
332
  claim: WorkspaceClaim,
@@ -328,7 +368,9 @@ function reconcileAllocation(
328
368
  throw new Error("provider-owned workspace branch no longer matches its frozen base");
329
369
  }
330
370
 
331
- const workspaceParent = createContainedDirectories(root, ["harnery-workspaces"]);
371
+ const parentSegment = claimedParentSegment(claim);
372
+ const workspaceParent = createContainedDirectories(root, [parentSegment]);
373
+ hideWorkspaceParentFromGit(workspaceParent);
332
374
  const workspaceParentIdentity = filesystemIdentity(workspaceParent);
333
375
  const registered = worktreeInventory(claim.repository.source_root.realpath);
334
376
  const exact = registered.find((item) => resolve(item.path) === claim.workspace_root);
@@ -385,7 +427,7 @@ function reconcileAllocation(
385
427
  parent_identity: workspaceParentIdentity,
386
428
  });
387
429
  }
388
- const openedWorkspace = openContainedDirectory(root, ["harnery-workspaces", claim.binding_id]);
430
+ const openedWorkspace = openContainedDirectory(root, [parentSegment, claim.binding_id]);
389
431
  if (openedWorkspace.path !== claim.workspace_root) {
390
432
  openedWorkspace.close();
391
433
  throw new Error("descriptor-backed workspace directory differs from the claim");
@@ -16,11 +16,11 @@ export function readWorkflowLiveness(coordRoot: string, runId: string): Workflow
16
16
  }
17
17
  }
18
18
 
19
- const journalPath = join(root, ".harnery", "workflows", runId, "journal.jsonl");
20
- if (!existsSync(journalPath)) return "unknown";
19
+ const transcriptPath = join(root, ".harnery", "workflows", runId, "transcript.jsonl");
20
+ if (!existsSync(transcriptPath)) return "unknown";
21
21
  let events: Array<{ event?: string; run_id?: string; ok?: unknown; status?: unknown }>;
22
22
  try {
23
- events = readFileSync(journalPath, "utf8")
23
+ events = readFileSync(transcriptPath, "utf8")
24
24
  .split("\n")
25
25
  .filter(Boolean)
26
26
  .map(
@@ -18,7 +18,7 @@ import {
18
18
  writeImmutableJson,
19
19
  } from "../durable-record.ts";
20
20
 
21
- export { appendWorkflowJournalEvent } from "../journal.ts";
21
+ export { appendWorkflowTranscriptEvent } from "../transcript.ts";
22
22
 
23
23
  import type {
24
24
  GitRepositoryBinding,
@@ -232,7 +232,8 @@ export function readWorkspaceEvents(
232
232
  ): WorkspaceProviderEvent[] {
233
233
  const path = workspaceEventsPath(coordRoot, providerId, bindingId);
234
234
  if (!existsSync(path)) return [];
235
- if (statSync(path).size > JSON_LIMIT * 4) throw new Error("workspace event journal is too large");
235
+ if (statSync(path).size > JSON_LIMIT * 4)
236
+ throw new Error("workspace event transcript is too large");
236
237
  const claim = readWorkspaceClaim(coordRoot, providerId, bindingId);
237
238
  if (!claim) throw new Error("workspace event authority claim is missing");
238
239
  const records = readFileSync(path, "utf8")
@@ -391,7 +392,7 @@ function readChainedRecords<
391
392
  >(path: string, label: string): T[] {
392
393
  if (!existsSync(path)) return [];
393
394
  if (statSync(path).size > JSON_LIMIT * 4) {
394
- throw new Error(`${label} journal is too large`);
395
+ throw new Error(`${label} transcript is too large`);
395
396
  }
396
397
  const records = readFileSync(path, "utf8")
397
398
  .split("\n")
@@ -353,7 +353,7 @@ export interface IntegrationAuthorization {
353
353
  approval_id?: string;
354
354
  approval_actor?: string;
355
355
  approval_sha256?: string;
356
- journal_anchor: {
356
+ transcript_anchor: {
357
357
  event: "integration.plan";
358
358
  plan_sha256: string;
359
359
  };
@@ -0,0 +1,177 @@
1
+ // Value assertions for `harn browse --assert`.
2
+ //
3
+ // The layout + content checks answer "does the page look right"; asserts answer
4
+ // "does the page SAY the right thing" — the h1 text, a price, how many cards
5
+ // rendered, whether an error banner is absent. It lets an agent confirm the
6
+ // values it expects without a human reading the page back.
7
+ //
8
+ // One repeatable flag with a small grammar:
9
+ // --assert 'text h1 => Welcome' first match's trimmed text equals
10
+ // --assert 'contains .total => $49' first match's text contains
11
+ // --assert 'matches .sku => ^BN-\d+$' first match's text matches a regex
12
+ // --assert 'count .card => >=3' match count vs a number or comparator
13
+ // --assert 'exists .cta' at least one match
14
+ // --assert 'absent .error' zero matches
15
+ //
16
+ // `op selector => expected`; `exists`/`absent` take no `=> expected`.
17
+
18
+ export type AssertOp = "text" | "contains" | "matches" | "count" | "exists" | "absent";
19
+
20
+ export interface AssertSpec {
21
+ raw: string;
22
+ op: AssertOp;
23
+ selector: string;
24
+ expected: string;
25
+ }
26
+
27
+ export interface AssertResult {
28
+ raw: string;
29
+ op: AssertOp;
30
+ selector: string;
31
+ expected: string;
32
+ /** Observed value: the text, or the match count as a string. */
33
+ actual: string;
34
+ found: boolean;
35
+ outcome: "pass" | "fail";
36
+ /** Populated when the spec itself was malformed (e.g. bad regex, bad count). */
37
+ error?: string;
38
+ }
39
+
40
+ const OPS: readonly AssertOp[] = ["text", "contains", "matches", "count", "exists", "absent"];
41
+
42
+ /** Parse one `--assert` expression. Throws with a clear message on malformed input. */
43
+ export function parseAssertSpec(raw: string): AssertSpec {
44
+ const trimmed = raw.trim();
45
+ const spaceIdx = trimmed.indexOf(" ");
46
+ if (spaceIdx < 0) {
47
+ throw new Error(`--assert needs '<op> <selector> [=> expected]' (got: '${raw}')`);
48
+ }
49
+ const op = trimmed.slice(0, spaceIdx).toLowerCase() as AssertOp;
50
+ if (!OPS.includes(op)) {
51
+ throw new Error(`--assert op must be one of ${OPS.join(", ")} (got: '${op}')`);
52
+ }
53
+ const rest = trimmed.slice(spaceIdx + 1).trim();
54
+ const sep = rest.indexOf("=>");
55
+ if (op === "exists" || op === "absent") {
56
+ return { raw, op, selector: (sep < 0 ? rest : rest.slice(0, sep)).trim(), expected: "" };
57
+ }
58
+ if (sep < 0) {
59
+ throw new Error(`--assert '${op}' needs 'selector => expected' (got: '${raw}')`);
60
+ }
61
+ return {
62
+ raw,
63
+ op,
64
+ selector: rest.slice(0, sep).trim(),
65
+ expected: rest.slice(sep + 2).trim(),
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Build the page evaluator. Serialized into the page by Playwright, so helpers
71
+ * are nested. Comparison logic that does not touch the DOM (count comparators,
72
+ * regex) also runs in-page to keep it one round trip.
73
+ */
74
+ export function buildAssertCheck(): (specs: AssertSpec[]) => AssertResult[] {
75
+ return (specs) => {
76
+ const textOf = (el: Element): string => (el.textContent ?? "").replace(/\s+/g, " ").trim();
77
+
78
+ const compareCount = (count: number, expected: string): { ok: boolean; error?: string } => {
79
+ const m = /^(>=|<=|>|<|=)?\s*(\d+)$/.exec(expected.trim());
80
+ if (!m) return { ok: false, error: `bad count expected '${expected}'` };
81
+ const op = m[1] || "=";
82
+ const n = Number.parseInt(m[2]!, 10);
83
+ switch (op) {
84
+ case ">=":
85
+ return { ok: count >= n };
86
+ case "<=":
87
+ return { ok: count <= n };
88
+ case ">":
89
+ return { ok: count > n };
90
+ case "<":
91
+ return { ok: count < n };
92
+ default:
93
+ return { ok: count === n };
94
+ }
95
+ };
96
+
97
+ return specs.map((spec): AssertResult => {
98
+ const base = {
99
+ raw: spec.raw,
100
+ op: spec.op,
101
+ selector: spec.selector,
102
+ expected: spec.expected,
103
+ };
104
+ let nodes: Element[];
105
+ try {
106
+ nodes = Array.from(document.querySelectorAll(spec.selector));
107
+ } catch {
108
+ return { ...base, actual: "", found: false, outcome: "fail", error: "invalid selector" };
109
+ }
110
+ const first = nodes[0] ?? null;
111
+ const firstText = first ? textOf(first) : "";
112
+
113
+ switch (spec.op) {
114
+ case "exists":
115
+ return {
116
+ ...base,
117
+ actual: String(nodes.length),
118
+ found: nodes.length > 0,
119
+ outcome: nodes.length > 0 ? "pass" : "fail",
120
+ };
121
+ case "absent":
122
+ return {
123
+ ...base,
124
+ actual: String(nodes.length),
125
+ found: nodes.length > 0,
126
+ outcome: nodes.length === 0 ? "pass" : "fail",
127
+ };
128
+ case "count": {
129
+ const { ok, error } = compareCount(nodes.length, spec.expected);
130
+ return {
131
+ ...base,
132
+ actual: String(nodes.length),
133
+ found: true,
134
+ outcome: ok ? "pass" : "fail",
135
+ ...(error ? { error } : {}),
136
+ };
137
+ }
138
+ case "text":
139
+ return {
140
+ ...base,
141
+ actual: firstText,
142
+ found: !!first,
143
+ outcome: first && firstText === spec.expected ? "pass" : "fail",
144
+ };
145
+ case "contains":
146
+ return {
147
+ ...base,
148
+ actual: firstText,
149
+ found: !!first,
150
+ outcome: first && firstText.includes(spec.expected) ? "pass" : "fail",
151
+ };
152
+ case "matches": {
153
+ let re: RegExp;
154
+ try {
155
+ re = new RegExp(spec.expected);
156
+ } catch {
157
+ return {
158
+ ...base,
159
+ actual: firstText,
160
+ found: !!first,
161
+ outcome: "fail",
162
+ error: `invalid regex '${spec.expected}'`,
163
+ };
164
+ }
165
+ return {
166
+ ...base,
167
+ actual: firstText,
168
+ found: !!first,
169
+ outcome: first && re.test(firstText) ? "pass" : "fail",
170
+ };
171
+ }
172
+ default:
173
+ return { ...base, actual: "", found: false, outcome: "fail", error: "unknown op" };
174
+ }
175
+ });
176
+ };
177
+ }
@@ -11,6 +11,15 @@ import {
11
11
  type Request,
12
12
  } from "playwright";
13
13
  import type { CookieJar, Cookie as JarCookie } from "../cookies/index.ts";
14
+ import { type AssertResult, type AssertSpec, buildAssertCheck } from "./asserts.js";
15
+ import {
16
+ buildClearContentAnnotationsScript,
17
+ buildContentAnnotateScript,
18
+ buildContentChecks,
19
+ type ContentAnnotationBox,
20
+ type ContentChecksRequest,
21
+ type ContentChecksResult,
22
+ } from "./content-checks.js";
14
23
  import {
15
24
  buildClearLayoutLintAnnotationsScript,
16
25
  buildLayoutLintAnnotateScript,
@@ -141,6 +150,38 @@ export interface Diagnostics {
141
150
 
142
151
  const DEFAULT_PROFILE = resolve(homedir(), ".cache", "harnery", "browser-profile");
143
152
 
153
+ // Bound how many Chromium processes SPAWN at the same moment. When many Browser
154
+ // instances open at once — a full test suite, a fan-out of browse calls — the
155
+ // simultaneous `child_process.spawn`s exhaust the OS's stdio-pipe/socket
156
+ // resources and Chromium launch dies with an unhandled ENOENT that a per-call
157
+ // retry can't catch. Serializing only the brief launch phase (never the
158
+ // browser's lifetime, so N browsers still run concurrently) removes the burst
159
+ // that causes it. Overridable via HARNERY_MAX_BROWSER_LAUNCHES.
160
+ const MAX_CONCURRENT_LAUNCHES = Math.max(
161
+ 1,
162
+ Number.parseInt(process.env.HARNERY_MAX_BROWSER_LAUNCHES ?? "3", 10) || 3,
163
+ );
164
+ let activeLaunches = 0;
165
+ const launchWaiters: Array<() => void> = [];
166
+
167
+ async function acquireLaunchSlot(): Promise<void> {
168
+ if (activeLaunches < MAX_CONCURRENT_LAUNCHES) {
169
+ activeLaunches++;
170
+ return;
171
+ }
172
+ // Queue; the releaser transfers its slot to us (activeLaunches unchanged).
173
+ await new Promise<void>((resolve) => launchWaiters.push(resolve));
174
+ }
175
+
176
+ function releaseLaunchSlot(): void {
177
+ const next = launchWaiters.shift();
178
+ if (next) {
179
+ next();
180
+ } else {
181
+ activeLaunches = Math.max(0, activeLaunches - 1);
182
+ }
183
+ }
184
+
144
185
  export class Browser {
145
186
  private context: BrowserContext | null = null;
146
187
  private page: Page | null = null;
@@ -161,20 +202,27 @@ export class Browser {
161
202
  return this.page;
162
203
  }
163
204
 
164
- async open(): Promise<void> {
165
- if (this.context) return;
166
- mkdirSync(this.profileDir, { recursive: true });
167
-
168
- this.context = await chromium.launchPersistentContext(this.profileDir, {
169
- headless: !this.opts.headed,
170
- viewport: this.opts.viewport ?? { width: 1280, height: 800 },
171
- ...(this.opts.launchArgs && this.opts.launchArgs.length > 0
172
- ? { args: this.opts.launchArgs }
173
- : {}),
174
- ...(this.opts.recordHarPath
175
- ? { recordHar: { path: this.opts.recordHarPath, mode: "full" as const } }
176
- : {}),
177
- });
205
+ /**
206
+ * Launch the context and wire up the first page. Factored out of `open()` so
207
+ * the whole sequence — not just the launch — can be retried as a unit.
208
+ */
209
+ private async openOnce(): Promise<void> {
210
+ // Hold a launch slot only across the spawn itself, not the browser's life.
211
+ await acquireLaunchSlot();
212
+ try {
213
+ this.context = await chromium.launchPersistentContext(this.profileDir, {
214
+ headless: !this.opts.headed,
215
+ viewport: this.opts.viewport ?? { width: 1280, height: 800 },
216
+ ...(this.opts.launchArgs && this.opts.launchArgs.length > 0
217
+ ? { args: this.opts.launchArgs }
218
+ : {}),
219
+ ...(this.opts.recordHarPath
220
+ ? { recordHar: { path: this.opts.recordHarPath, mode: "full" as const } }
221
+ : {}),
222
+ });
223
+ } finally {
224
+ releaseLaunchSlot();
225
+ }
178
226
  this.context.setDefaultNavigationTimeout(this.opts.navigationTimeout ?? 30_000);
179
227
 
180
228
  if (this.opts.jar) {
@@ -202,6 +250,54 @@ export class Browser {
202
250
  this.attachDiagnosticListeners(this.page);
203
251
  }
204
252
 
253
+ /**
254
+ * Open the browser, retrying a few times on a transient startup failure.
255
+ * Chromium occasionally fails to hand off its CDP port ("Failed to connect",
256
+ * "Target closed") when many instances launch at once — a busy CI box or a
257
+ * full test suite — and the failure can surface on the launch OR on a
258
+ * follow-up call (addCookies, newPage) when the process dies right after
259
+ * spawning. So the entire open sequence retries as a unit, tearing down any
260
+ * half-built context between attempts. A genuinely broken config fails every
261
+ * attempt and throws with the underlying message.
262
+ */
263
+ async open(): Promise<void> {
264
+ if (this.context) return;
265
+ mkdirSync(this.profileDir, { recursive: true });
266
+
267
+ const maxAttempts = 3;
268
+ let lastErr: unknown;
269
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
270
+ try {
271
+ await this.openOnce();
272
+ return;
273
+ } catch (err) {
274
+ lastErr = err;
275
+ // Tear down a partial context + reset diagnostics collected on the
276
+ // failed attempt, so the retry starts clean. (openOnce() reassigns
277
+ // this.context, which TS can't see across the call, so re-widen.)
278
+ const partial = this.context as BrowserContext | null;
279
+ if (partial) {
280
+ try {
281
+ await partial.close();
282
+ } catch {
283
+ // ignore — the process is likely already gone
284
+ }
285
+ }
286
+ this.context = null;
287
+ this.page = null;
288
+ this.consoleEvents = [];
289
+ this.pageErrors = [];
290
+ this.failedRequests = [];
291
+ if (attempt < maxAttempts) {
292
+ await new Promise((resolve) => setTimeout(resolve, attempt * 250));
293
+ }
294
+ }
295
+ }
296
+ throw new Error(
297
+ `Failed to open browser after ${maxAttempts} attempts: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`,
298
+ );
299
+ }
300
+
205
301
  /**
206
302
  * Hook console + pageerror + requestfailed events. Called on `open()`
207
303
  * before any navigation so we don't miss early-fired events.
@@ -295,6 +391,65 @@ export class Browser {
295
391
  return buf.length;
296
392
  }
297
393
 
394
+ /** Full document + viewport dimensions, for tiling a page into bands. */
395
+ async pageMetrics(): Promise<{
396
+ scrollWidth: number;
397
+ scrollHeight: number;
398
+ viewportWidth: number;
399
+ viewportHeight: number;
400
+ }> {
401
+ return await this.currentPage.evaluate(() => ({
402
+ scrollWidth: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0),
403
+ scrollHeight: Math.max(
404
+ document.documentElement.scrollHeight,
405
+ document.body?.scrollHeight ?? 0,
406
+ ),
407
+ viewportWidth: window.innerWidth,
408
+ viewportHeight: window.innerHeight,
409
+ }));
410
+ }
411
+
412
+ /** Full-page PNG as a Buffer, for cropping critique tiles in pixel space. */
413
+ async fullPageScreenshotBuffer(): Promise<Buffer> {
414
+ return await this.currentPage.screenshot({ type: "png", fullPage: true });
415
+ }
416
+
417
+ /** Base64 PNG of a document-space clip rect. Used to capture one critique tile. */
418
+ async screenshotClipBase64(rect: {
419
+ x: number;
420
+ y: number;
421
+ width: number;
422
+ height: number;
423
+ }): Promise<string> {
424
+ const buf = await this.currentPage.screenshot({ type: "png", clip: rect });
425
+ return buf.toString("base64");
426
+ }
427
+
428
+ /** Document-space rects + labels for each element matching `selector` (semantic tiling). */
429
+ async elementTiles(
430
+ selector: string,
431
+ ): Promise<Array<{ label: string; x: number; y: number; width: number; height: number }>> {
432
+ return await this.currentPage.evaluate((sel) => {
433
+ const out: Array<{ label: string; x: number; y: number; width: number; height: number }> = [];
434
+ const sx = window.scrollX;
435
+ const sy = window.scrollY;
436
+ for (const el of Array.from(document.querySelectorAll(sel))) {
437
+ const r = el.getBoundingClientRect();
438
+ if (r.width <= 0 || r.height <= 0) continue;
439
+ const tag = el.tagName.toLowerCase();
440
+ const cls = typeof el.className === "string" ? el.className.trim().split(/\s+/)[0] : "";
441
+ out.push({
442
+ label: el.id ? `${tag}#${el.id}` : cls ? `${tag}.${cls}` : tag,
443
+ x: Math.max(0, r.x + sx),
444
+ y: Math.max(0, r.y + sy),
445
+ width: r.width,
446
+ height: r.height,
447
+ });
448
+ }
449
+ return out;
450
+ }, selector);
451
+ }
452
+
298
453
  /**
299
454
  * Capture a full-page screenshot from an explicit capture viewport and
300
455
  * evaluate the caller's final evidence expression immediately before the
@@ -600,6 +755,26 @@ export class Browser {
600
755
  await this.currentPage.evaluate(buildClearLayoutLintAnnotationsScript());
601
756
  }
602
757
 
758
+ /** Run the requested content checks (placeholder/image/truncation/contrast) in one evaluation. */
759
+ async checkContent(request: ContentChecksRequest): Promise<ContentChecksResult> {
760
+ return await this.currentPage.evaluate(buildContentChecks(), request);
761
+ }
762
+
763
+ /** Evaluate value assertions (text/contains/matches/count/exists/absent) against the page. */
764
+ async checkAsserts(specs: AssertSpec[]): Promise<AssertResult[]> {
765
+ return await this.currentPage.evaluate(buildAssertCheck(), specs);
766
+ }
767
+
768
+ /** Draw one document-space annotation layer for content-check hits. */
769
+ async annotateContent(boxes: ContentAnnotationBox[]): Promise<void> {
770
+ await this.currentPage.evaluate(buildContentAnnotateScript(), { boxes });
771
+ }
772
+
773
+ /** Remove content-check annotations. */
774
+ async clearContentAnnotations(): Promise<void> {
775
+ await this.currentPage.evaluate(buildClearContentAnnotationsScript());
776
+ }
777
+
603
778
  /**
604
779
  * Run the target-size rule against the page or explicit scopes. The
605
780
  * dependency's browser bundle is evaluated directly in the page context so