harnery 0.38.0 → 0.40.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 (378) hide show
  1. package/dist/commands/agents.d.ts.map +1 -1
  2. package/dist/commands/agents.js +32 -8
  3. package/dist/commands/artifacts.d.ts.map +1 -1
  4. package/dist/commands/artifacts.js +97 -6
  5. package/dist/commands/backup.d.ts +16 -0
  6. package/dist/commands/backup.d.ts.map +1 -1
  7. package/dist/commands/backup.js +239 -33
  8. package/dist/commands/browse.d.ts +8 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +466 -231
  11. package/dist/commands/diagnostics.d.ts.map +1 -1
  12. package/dist/commands/diagnostics.js +6 -3
  13. package/dist/commands/ledger-v3.d.ts.map +1 -1
  14. package/dist/commands/ledger-v3.js +20 -4
  15. package/dist/commands/qa-run.d.ts.map +1 -1
  16. package/dist/commands/qa-run.js +26 -3
  17. package/dist/commands/qa-status.d.ts.map +1 -1
  18. package/dist/commands/qa-status.js +7 -2
  19. package/dist/commands/resources.d.ts.map +1 -1
  20. package/dist/commands/resources.js +23 -3
  21. package/dist/commands/review-pack.d.ts.map +1 -1
  22. package/dist/commands/review-pack.js +98 -23
  23. package/dist/commands/tunnel.d.ts.map +1 -1
  24. package/dist/commands/tunnel.js +2 -2
  25. package/dist/core/adapters/profiles.d.ts +1 -1
  26. package/dist/core/agents/canonical-emit.d.ts +3 -2
  27. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  28. package/dist/core/agents/canonical-emit.js +2 -0
  29. package/dist/core/agents/cli.js +13 -6
  30. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  31. package/dist/core/agents/live-authority-v3.js +5 -3
  32. package/dist/core/agents/live-lifecycle-v3.d.ts +2 -2
  33. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  34. package/dist/core/agents/render/prompt-context.d.ts +7 -5
  35. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  36. package/dist/core/agents/render/prompt-context.js +63 -8
  37. package/dist/core/agents/session-finalizer-v3.d.ts +2 -2
  38. package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -1
  39. package/dist/core/agents/state/live-coordination-view.d.ts +3 -3
  40. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  41. package/dist/core/agents/state/live-coordination-view.js +2 -1
  42. package/dist/core/agents/state/names.d.ts +35 -2
  43. package/dist/core/agents/state/names.d.ts.map +1 -1
  44. package/dist/core/agents/state/names.js +80 -5
  45. package/dist/core/agents/state/stale-sweep.d.ts +2 -1
  46. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  47. package/dist/core/agents/state/stale-sweep.js +23 -6
  48. package/dist/core/agents/turn-elapsed.d.ts +51 -0
  49. package/dist/core/agents/turn-elapsed.d.ts.map +1 -0
  50. package/dist/core/agents/turn-elapsed.js +91 -0
  51. package/dist/core/artifacts/activity.d.ts +14 -0
  52. package/dist/core/artifacts/activity.d.ts.map +1 -0
  53. package/dist/core/artifacts/activity.js +52 -0
  54. package/dist/core/artifacts/delivery-card.d.ts +39 -0
  55. package/dist/core/artifacts/delivery-card.d.ts.map +1 -0
  56. package/dist/core/artifacts/delivery-card.js +282 -0
  57. package/dist/core/artifacts/index.d.ts +42 -7
  58. package/dist/core/artifacts/index.d.ts.map +1 -1
  59. package/dist/core/artifacts/index.js +288 -76
  60. package/dist/core/artifacts/mutation-lock.d.ts +2 -0
  61. package/dist/core/artifacts/mutation-lock.d.ts.map +1 -0
  62. package/dist/core/artifacts/mutation-lock.js +91 -0
  63. package/dist/core/backup/host-snapshot.d.ts +18 -0
  64. package/dist/core/backup/host-snapshot.d.ts.map +1 -0
  65. package/dist/core/backup/host-snapshot.js +64 -0
  66. package/dist/core/config.d.ts +20 -5
  67. package/dist/core/config.d.ts.map +1 -1
  68. package/dist/core/config.js +44 -10
  69. package/dist/core/diagnostics/advice.d.ts +38 -3
  70. package/dist/core/diagnostics/advice.d.ts.map +1 -1
  71. package/dist/core/diagnostics/advice.js +103 -99
  72. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  73. package/dist/core/diagnostics/bundle.js +111 -47
  74. package/dist/core/diagnostics/comparison.d.ts.map +1 -1
  75. package/dist/core/diagnostics/comparison.js +3 -2
  76. package/dist/core/diagnostics/contract.d.ts +15 -31
  77. package/dist/core/diagnostics/contract.d.ts.map +1 -1
  78. package/dist/core/diagnostics/contract.js +2 -5
  79. package/dist/core/diagnostics/index.d.ts +2 -0
  80. package/dist/core/diagnostics/index.d.ts.map +1 -1
  81. package/dist/core/diagnostics/index.js +2 -0
  82. package/dist/core/diagnostics/pressure-contract.d.ts +260 -0
  83. package/dist/core/diagnostics/pressure-contract.d.ts.map +1 -0
  84. package/dist/core/diagnostics/pressure-contract.js +70 -0
  85. package/dist/core/diagnostics/pressure.d.ts +25 -0
  86. package/dist/core/diagnostics/pressure.d.ts.map +1 -0
  87. package/dist/core/diagnostics/pressure.js +1138 -0
  88. package/dist/core/diagnostics/replay.d.ts.map +1 -1
  89. package/dist/core/diagnostics/replay.js +90 -10
  90. package/dist/core/events/v3/adapter-id.d.ts +6 -0
  91. package/dist/core/events/v3/adapter-id.d.ts.map +1 -0
  92. package/dist/core/events/v3/adapter-id.js +7 -0
  93. package/dist/core/events/v3/archive-retention.d.ts.map +1 -1
  94. package/dist/core/events/v3/archive-retention.js +3 -2
  95. package/dist/core/events/v3/bootstrap.d.ts +41 -4
  96. package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
  97. package/dist/core/events/v3/bootstrap.js +120 -17
  98. package/dist/core/events/v3/capabilities.d.ts +16 -9
  99. package/dist/core/events/v3/capabilities.d.ts.map +1 -1
  100. package/dist/core/events/v3/capabilities.js +37 -2
  101. package/dist/core/events/v3/capability-drift.d.ts +2 -2
  102. package/dist/core/events/v3/capability-drift.d.ts.map +1 -1
  103. package/dist/core/events/v3/control-witness.d.ts +48 -5
  104. package/dist/core/events/v3/control-witness.d.ts.map +1 -1
  105. package/dist/core/events/v3/control-witness.js +91 -36
  106. package/dist/core/events/v3/control.d.ts.map +1 -1
  107. package/dist/core/events/v3/control.js +22 -2
  108. package/dist/core/events/v3/index.d.ts +2 -1
  109. package/dist/core/events/v3/index.d.ts.map +1 -1
  110. package/dist/core/events/v3/index.js +2 -1
  111. package/dist/core/events/v3/live-feed.d.ts +7 -1
  112. package/dist/core/events/v3/live-feed.d.ts.map +1 -1
  113. package/dist/core/events/v3/live-feed.js +21 -3
  114. package/dist/core/events/v3/live-routing.d.ts.map +1 -1
  115. package/dist/core/events/v3/live-routing.js +10 -2
  116. package/dist/core/events/v3/producers/command-recorder.d.ts +2 -2
  117. package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
  118. package/dist/core/events/v3/producers/command-recorder.js +2 -1
  119. package/dist/core/events/v3/producers/coordination-recorder.d.ts +2 -2
  120. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  121. package/dist/core/events/v3/producers/coordination-recorder.js +2 -1
  122. package/dist/core/events/v3/producers/hook-base.d.ts +2 -2
  123. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
  124. package/dist/core/events/v3/producers/hook-base.js +15 -5
  125. package/dist/core/events/v3/producers/hook.d.ts.map +1 -1
  126. package/dist/core/events/v3/producers/hook.js +17 -4
  127. package/dist/core/events/v3/producers/intake.d.ts +4 -4
  128. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  129. package/dist/core/events/v3/producers/intake.js +3 -2
  130. package/dist/core/events/v3/producers/producer-state-observer.d.ts.map +1 -1
  131. package/dist/core/events/v3/producers/producer-state-observer.js +2 -1
  132. package/dist/core/events/v3/producers/recorder.d.ts +16 -5
  133. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  134. package/dist/core/events/v3/producers/recorder.js +31 -9
  135. package/dist/core/events/v3/reader.d.ts.map +1 -1
  136. package/dist/core/events/v3/reader.js +10 -13
  137. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  138. package/dist/core/events/v3/runtime-build.js +3 -2
  139. package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts +2 -2
  140. package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts.map +1 -1
  141. package/dist/core/events/v3/turn-telemetry.d.ts +2 -2
  142. package/dist/core/events/v3/turn-telemetry.d.ts.map +1 -1
  143. package/dist/core/hooks/adapter/output.d.ts +16 -14
  144. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  145. package/dist/core/hooks/adapter/output.js +36 -38
  146. package/dist/core/hooks/backup-schedule.d.ts +34 -0
  147. package/dist/core/hooks/backup-schedule.d.ts.map +1 -0
  148. package/dist/core/hooks/backup-schedule.js +114 -0
  149. package/dist/core/hooks/cli.js +61 -18
  150. package/dist/core/hooks/codex-wsl-bridge.d.ts +2 -0
  151. package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -1
  152. package/dist/core/hooks/codex-wsl-bridge.js +18 -4
  153. package/dist/core/hooks/index.d.ts +1 -0
  154. package/dist/core/hooks/index.d.ts.map +1 -1
  155. package/dist/core/hooks/index.js +1 -0
  156. package/dist/core/hooks/prompt-context/contract.d.ts +61 -0
  157. package/dist/core/hooks/prompt-context/contract.d.ts.map +1 -0
  158. package/dist/core/hooks/prompt-context/contract.js +102 -0
  159. package/dist/core/hooks/prompt-context/index.d.ts +5 -0
  160. package/dist/core/hooks/prompt-context/index.d.ts.map +1 -0
  161. package/dist/core/hooks/prompt-context/index.js +2 -0
  162. package/dist/core/hooks/prompt-context/runner.d.ts +24 -0
  163. package/dist/core/hooks/prompt-context/runner.d.ts.map +1 -0
  164. package/dist/core/hooks/prompt-context/runner.js +249 -0
  165. package/dist/core/qa-artifacts.d.ts +3 -0
  166. package/dist/core/qa-artifacts.d.ts.map +1 -1
  167. package/dist/core/qa-artifacts.js +25 -3
  168. package/dist/core/resources/contract.d.ts +69 -3
  169. package/dist/core/resources/contract.d.ts.map +1 -1
  170. package/dist/core/resources/contract.js +1 -1
  171. package/dist/core/resources/index.d.ts +1 -0
  172. package/dist/core/resources/index.d.ts.map +1 -1
  173. package/dist/core/resources/index.js +1 -0
  174. package/dist/core/resources/native.d.ts +36 -0
  175. package/dist/core/resources/native.d.ts.map +1 -0
  176. package/dist/core/resources/native.js +418 -0
  177. package/dist/core/resources/sampler.d.ts +2 -0
  178. package/dist/core/resources/sampler.d.ts.map +1 -1
  179. package/dist/core/resources/sampler.js +31 -1
  180. package/dist/core/resources/status.d.ts +61 -0
  181. package/dist/core/resources/status.d.ts.map +1 -0
  182. package/dist/core/resources/status.js +536 -0
  183. package/dist/core/resources/system.d.ts +27 -0
  184. package/dist/core/resources/system.d.ts.map +1 -0
  185. package/dist/core/resources/system.js +439 -0
  186. package/dist/core/semantic/contract.d.ts +13 -13
  187. package/dist/core/semantic/service.d.ts +2 -0
  188. package/dist/core/semantic/service.d.ts.map +1 -1
  189. package/dist/core/semantic/service.js +27 -4
  190. package/dist/core/storage/builtins.js +41 -0
  191. package/dist/core/supervisor/contract.d.ts +41 -3
  192. package/dist/core/supervisor/contract.d.ts.map +1 -1
  193. package/dist/core/supervisor/contract.js +3 -2
  194. package/dist/core/supervisor/findings.d.ts +3 -0
  195. package/dist/core/supervisor/findings.d.ts.map +1 -1
  196. package/dist/core/supervisor/findings.js +88 -0
  197. package/dist/core/supervisor/history.d.ts.map +1 -1
  198. package/dist/core/supervisor/history.js +14 -0
  199. package/dist/core/supervisor/ledger-state.d.ts +55 -0
  200. package/dist/core/supervisor/ledger-state.d.ts.map +1 -0
  201. package/dist/core/supervisor/ledger-state.js +0 -0
  202. package/dist/core/supervisor/service.d.ts.map +1 -1
  203. package/dist/core/supervisor/service.js +120 -7
  204. package/dist/core/supervisor/storage.d.ts +3 -1
  205. package/dist/core/supervisor/storage.d.ts.map +1 -1
  206. package/dist/core/supervisor/storage.js +4 -0
  207. package/dist/core/workflow/admission.d.ts +3 -0
  208. package/dist/core/workflow/admission.d.ts.map +1 -1
  209. package/dist/core/workflow/admission.js +28 -16
  210. package/dist/core/workflow/engine.js +2 -2
  211. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  212. package/dist/core/workflow/live-session-v3.js +6 -1
  213. package/dist/core/workflow/proof.d.ts.map +1 -1
  214. package/dist/core/workflow/proof.js +4 -3
  215. package/dist/lib/browser/capture-fidelity.d.ts +7 -0
  216. package/dist/lib/browser/capture-fidelity.d.ts.map +1 -1
  217. package/dist/lib/browser/capture-fidelity.js +17 -0
  218. package/dist/lib/browser/client.d.ts +9 -0
  219. package/dist/lib/browser/client.d.ts.map +1 -1
  220. package/dist/lib/browser/client.js +85 -37
  221. package/dist/lib/browser/index.d.ts +3 -1
  222. package/dist/lib/browser/index.d.ts.map +1 -1
  223. package/dist/lib/browser/index.js +2 -1
  224. package/dist/lib/browser/page-review-budget.d.ts +13 -0
  225. package/dist/lib/browser/page-review-budget.d.ts.map +1 -0
  226. package/dist/lib/browser/page-review-budget.js +251 -0
  227. package/dist/lib/browser/page-review-contracts.d.ts +127 -0
  228. package/dist/lib/browser/page-review-contracts.d.ts.map +1 -0
  229. package/dist/lib/browser/page-review-contracts.js +5 -0
  230. package/dist/lib/browser/page-review-evidence.d.ts +48 -0
  231. package/dist/lib/browser/page-review-evidence.d.ts.map +1 -0
  232. package/dist/lib/browser/page-review-evidence.js +340 -0
  233. package/dist/lib/browser/page-review-judge.d.ts +1 -1
  234. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  235. package/dist/lib/browser/page-review-judge.js +46 -38
  236. package/dist/lib/browser/page-review-pack.d.ts +44 -22
  237. package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
  238. package/dist/lib/browser/page-review-pack.js +116 -36
  239. package/dist/lib/browser/page-review-source.d.ts +12 -0
  240. package/dist/lib/browser/page-review-source.d.ts.map +1 -0
  241. package/dist/lib/browser/page-review-source.js +22 -0
  242. package/dist/lib/browser/qa-run-contracts.d.ts +35 -9
  243. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  244. package/dist/lib/browser/qa-run-contracts.js +48 -14
  245. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  246. package/dist/lib/browser/qa-run.js +215 -68
  247. package/dist/lib/browser/qa-snapshot.d.ts +6 -0
  248. package/dist/lib/browser/qa-snapshot.d.ts.map +1 -1
  249. package/dist/lib/browser/qa-snapshot.js +259 -48
  250. package/dist/lib/browser/review-capture-transaction.d.ts +13 -0
  251. package/dist/lib/browser/review-capture-transaction.d.ts.map +1 -0
  252. package/dist/lib/browser/review-capture-transaction.js +79 -0
  253. package/dist/lib/browser/standalone-html.d.ts.map +1 -1
  254. package/dist/lib/browser/standalone-html.js +4 -2
  255. package/dist/lib/browser/thumbnail-association.d.ts +34 -0
  256. package/dist/lib/browser/thumbnail-association.d.ts.map +1 -0
  257. package/dist/lib/browser/thumbnail-association.js +158 -0
  258. package/dist/lib/browser/tiling.d.ts +30 -0
  259. package/dist/lib/browser/tiling.d.ts.map +1 -1
  260. package/dist/lib/browser/tiling.js +38 -0
  261. package/dist/lib/docs-metadata-managed.d.ts.map +1 -1
  262. package/dist/lib/docs-metadata-managed.js +4 -1
  263. package/dist/lib/identities/assume.d.ts.map +1 -1
  264. package/dist/lib/identities/assume.js +7 -1
  265. package/dist/lib/instructions/templates.d.ts.map +1 -1
  266. package/dist/lib/instructions/templates.js +1 -0
  267. package/dist/lib/local-file-url.d.ts +12 -0
  268. package/dist/lib/local-file-url.d.ts.map +1 -1
  269. package/dist/lib/local-file-url.js +16 -2
  270. package/dist/lib/tunnel/state.d.ts +19 -4
  271. package/dist/lib/tunnel/state.d.ts.map +1 -1
  272. package/dist/lib/tunnel/state.js +53 -13
  273. package/package.json +7 -4
  274. package/schemas/config.schema.json +69 -2
  275. package/src/commands/agents.ts +42 -9
  276. package/src/commands/artifacts.ts +130 -6
  277. package/src/commands/backup.ts +287 -34
  278. package/src/commands/browse.ts +564 -267
  279. package/src/commands/diagnostics.ts +6 -3
  280. package/src/commands/ledger-v3.ts +20 -3
  281. package/src/commands/qa-run.ts +37 -3
  282. package/src/commands/qa-status.ts +11 -2
  283. package/src/commands/resources.ts +23 -3
  284. package/src/commands/review-pack.ts +165 -62
  285. package/src/commands/tunnel.ts +2 -1
  286. package/src/core/agents/canonical-emit.ts +4 -2
  287. package/src/core/agents/cli.ts +14 -6
  288. package/src/core/agents/live-authority-v3.ts +7 -3
  289. package/src/core/agents/live-lifecycle-v3.ts +2 -2
  290. package/src/core/agents/render/prompt-context.ts +68 -14
  291. package/src/core/agents/session-finalizer-v3.ts +2 -2
  292. package/src/core/agents/state/live-coordination-view.ts +4 -4
  293. package/src/core/agents/state/names.ts +92 -7
  294. package/src/core/agents/state/stale-sweep.ts +22 -8
  295. package/src/core/agents/turn-elapsed.ts +135 -0
  296. package/src/core/artifacts/activity.ts +71 -0
  297. package/src/core/artifacts/delivery-card.ts +375 -0
  298. package/src/core/artifacts/index.ts +385 -91
  299. package/src/core/artifacts/mutation-lock.ts +106 -0
  300. package/src/core/backup/host-snapshot.ts +76 -0
  301. package/src/core/config.ts +66 -13
  302. package/src/core/diagnostics/advice.ts +148 -120
  303. package/src/core/diagnostics/bundle.ts +138 -52
  304. package/src/core/diagnostics/comparison.ts +6 -2
  305. package/src/core/diagnostics/contract.ts +39 -42
  306. package/src/core/diagnostics/index.ts +2 -0
  307. package/src/core/diagnostics/pressure-contract.ts +294 -0
  308. package/src/core/diagnostics/pressure.ts +1361 -0
  309. package/src/core/diagnostics/replay.ts +116 -11
  310. package/src/core/events/v3/adapter-id.ts +12 -0
  311. package/src/core/events/v3/archive-retention.ts +3 -6
  312. package/src/core/events/v3/bootstrap.ts +163 -16
  313. package/src/core/events/v3/capabilities.ts +75 -25
  314. package/src/core/events/v3/capability-drift.ts +2 -2
  315. package/src/core/events/v3/control-witness.ts +172 -41
  316. package/src/core/events/v3/control.ts +34 -2
  317. package/src/core/events/v3/index.ts +3 -0
  318. package/src/core/events/v3/live-feed.ts +23 -5
  319. package/src/core/events/v3/live-routing.ts +14 -2
  320. package/src/core/events/v3/producers/command-recorder.ts +4 -4
  321. package/src/core/events/v3/producers/coordination-recorder.ts +4 -4
  322. package/src/core/events/v3/producers/hook-base.ts +21 -16
  323. package/src/core/events/v3/producers/hook.ts +21 -4
  324. package/src/core/events/v3/producers/intake.ts +6 -6
  325. package/src/core/events/v3/producers/producer-state-observer.ts +2 -1
  326. package/src/core/events/v3/producers/recorder.ts +50 -21
  327. package/src/core/events/v3/reader.ts +14 -13
  328. package/src/core/events/v3/runtime-build.ts +3 -2
  329. package/src/core/events/v3/runtime-telemetry-capabilities.ts +3 -3
  330. package/src/core/events/v3/turn-telemetry.ts +4 -4
  331. package/src/core/hooks/adapter/output.ts +50 -36
  332. package/src/core/hooks/backup-schedule.ts +147 -0
  333. package/src/core/hooks/cli.ts +65 -14
  334. package/src/core/hooks/codex-wsl-bridge.ts +21 -5
  335. package/src/core/hooks/index.ts +1 -0
  336. package/src/core/hooks/prompt-context/contract.ts +177 -0
  337. package/src/core/hooks/prompt-context/index.ts +22 -0
  338. package/src/core/hooks/prompt-context/runner.ts +303 -0
  339. package/src/core/qa-artifacts.ts +29 -3
  340. package/src/core/resources/contract.ts +76 -3
  341. package/src/core/resources/index.ts +1 -0
  342. package/src/core/resources/native.ts +481 -0
  343. package/src/core/resources/sampler.ts +46 -1
  344. package/src/core/resources/status.ts +717 -0
  345. package/src/core/resources/system.ts +530 -0
  346. package/src/core/semantic/service.ts +37 -3
  347. package/src/core/storage/builtins.ts +41 -0
  348. package/src/core/supervisor/contract.ts +45 -3
  349. package/src/core/supervisor/findings.ts +159 -0
  350. package/src/core/supervisor/history.ts +15 -0
  351. package/src/core/supervisor/ledger-state.ts +0 -0
  352. package/src/core/supervisor/service.ts +133 -5
  353. package/src/core/supervisor/storage.ts +7 -0
  354. package/src/core/workflow/admission.ts +33 -17
  355. package/src/core/workflow/engine.ts +2 -2
  356. package/src/core/workflow/live-session-v3.ts +6 -8
  357. package/src/core/workflow/proof.ts +4 -3
  358. package/src/lib/browser/capture-fidelity.ts +23 -0
  359. package/src/lib/browser/client.ts +96 -44
  360. package/src/lib/browser/index.ts +15 -1
  361. package/src/lib/browser/page-review-budget.ts +309 -0
  362. package/src/lib/browser/page-review-contracts.ts +129 -0
  363. package/src/lib/browser/page-review-evidence.ts +465 -0
  364. package/src/lib/browser/page-review-judge.ts +56 -40
  365. package/src/lib/browser/page-review-pack.ts +181 -43
  366. package/src/lib/browser/page-review-source.ts +30 -0
  367. package/src/lib/browser/qa-run-contracts.ts +62 -18
  368. package/src/lib/browser/qa-run.ts +259 -71
  369. package/src/lib/browser/qa-snapshot.ts +289 -48
  370. package/src/lib/browser/review-capture-transaction.ts +106 -0
  371. package/src/lib/browser/standalone-html.ts +4 -2
  372. package/src/lib/browser/thumbnail-association.ts +190 -0
  373. package/src/lib/browser/tiling.ts +63 -0
  374. package/src/lib/docs-metadata-managed.ts +4 -1
  375. package/src/lib/identities/assume.ts +12 -4
  376. package/src/lib/instructions/templates.ts +1 -0
  377. package/src/lib/local-file-url.ts +17 -2
  378. package/src/lib/tunnel/state.ts +70 -13
@@ -0,0 +1,190 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ closeSync,
4
+ constants,
5
+ fstatSync,
6
+ openSync,
7
+ readSync,
8
+ realpathSync,
9
+ renameSync,
10
+ statSync,
11
+ unlinkSync,
12
+ writeSync,
13
+ } from "node:fs";
14
+ import path from "node:path";
15
+
16
+ const MAX_HTML = 2 * 1024 * 1024;
17
+ const MAX_PNG = 32 * 1024 * 1024;
18
+ export interface CaptureFingerprint {
19
+ version: string;
20
+ sha256: string;
21
+ }
22
+ export interface CaptureThumbnailAssociation {
23
+ schema_version: 2;
24
+ producer: "browser-standalone-capture";
25
+ source: CaptureFingerprint & { path: string };
26
+ preview: CaptureFingerprint & { path: string };
27
+ }
28
+ function version(fd: number): string {
29
+ const s = fstatSync(fd);
30
+ return `${s.dev}:${s.ino}:${s.size}:${s.mtimeMs}:${s.ctimeMs}`;
31
+ }
32
+ function fingerprint(filename: string, maxBytes: number): CaptureFingerprint | null {
33
+ const canonical = realpathSync(filename);
34
+ const before = statSync(canonical);
35
+ if (!before.isFile() || before.size > maxBytes) return null;
36
+ const fd = openSync(canonical, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
37
+ try {
38
+ const opened = fstatSync(fd);
39
+ if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino) return null;
40
+ const initial = version(fd);
41
+ const hash = createHash("sha256");
42
+ const bytes = Buffer.alloc(64 * 1024);
43
+ let offset = 0;
44
+ while (offset < before.size) {
45
+ const count = readSync(fd, bytes, 0, Math.min(bytes.length, before.size - offset), offset);
46
+ if (!count) return null;
47
+ hash.update(bytes.subarray(0, count));
48
+ offset += count;
49
+ }
50
+ if (version(fd) !== initial || realpathSync(filename) !== canonical) return null;
51
+ return { version: initial, sha256: hash.digest("hex") };
52
+ } finally {
53
+ closeSync(fd);
54
+ }
55
+ }
56
+
57
+ /** Take immediately after the screenshot writer completes, before asynchronous
58
+ * HTML serialization. A later writer cannot silently substitute its PNG. */
59
+ export function captureThumbnailFingerprint(pngPath: string): CaptureFingerprint | null {
60
+ try {
61
+ return fingerprint(pngPath, MAX_PNG);
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+ function workspaceFor(filename: string): { root: string; workspace: string } | null {
67
+ for (
68
+ let current = path.dirname(filename);
69
+ path.dirname(current) !== current;
70
+ current = path.dirname(current)
71
+ ) {
72
+ const artifacts = path.dirname(current);
73
+ const state = path.dirname(artifacts);
74
+ if (path.basename(artifacts) === "artifacts" && path.basename(state) === ".harnery")
75
+ return { root: path.dirname(state), workspace: current };
76
+ }
77
+ return null;
78
+ }
79
+ function inside(root: string, filename: string): boolean {
80
+ const relative = path.relative(root, filename);
81
+ return (
82
+ relative !== "" &&
83
+ relative !== ".." &&
84
+ !relative.startsWith(`..${path.sep}`) &&
85
+ !path.isAbsolute(relative)
86
+ );
87
+ }
88
+
89
+ /** Associate only the full-document, standalone HTML and exact PNG produced by
90
+ * this capture operation. No basename inference and no reads of source pages.
91
+ * A linked asset, selector fragment, changed output, oversize file or output
92
+ * outside a managed artifact workspace safely skips registration.
93
+ * The hidden sidecar binds canonical repository paths, checked file versions
94
+ * and SHA256 receipts. Web readers also require an empty local asset graph;
95
+ * manual registrations bind their own complete dependency graph instead. */
96
+ export function registerCaptureThumbnail(input: {
97
+ htmlPath: string;
98
+ pngPath: string;
99
+ html: string;
100
+ preview: CaptureFingerprint;
101
+ fullDocument: boolean;
102
+ stylesheetsLinked: number;
103
+ resourcesLinked: number;
104
+ }): string | null {
105
+ if (!input.fullDocument || input.stylesheetsLinked !== 0 || input.resourcesLinked !== 0)
106
+ return null;
107
+ try {
108
+ const htmlPath = realpathSync(input.htmlPath);
109
+ const pngPath = realpathSync(input.pngPath);
110
+ if (!/\.html?$/i.test(htmlPath) || !/\.png$/i.test(pngPath)) return null;
111
+ const owner = workspaceFor(htmlPath);
112
+ if (!owner || !inside(owner.workspace, pngPath)) return null;
113
+ const source = fingerprint(htmlPath, MAX_HTML);
114
+ const preview = fingerprint(pngPath, MAX_PNG);
115
+ if (
116
+ !source ||
117
+ !preview ||
118
+ source.sha256 !== createHash("sha256").update(input.html).digest("hex") ||
119
+ preview.version !== input.preview.version ||
120
+ preview.sha256 !== input.preview.sha256
121
+ )
122
+ return null;
123
+ const sourceRelative = path.relative(owner.root, htmlPath).split(path.sep).join("/");
124
+ const previewRelative = path.relative(owner.root, pngPath).split(path.sep).join("/");
125
+ const association: CaptureThumbnailAssociation = {
126
+ schema_version: 2,
127
+ producer: "browser-standalone-capture",
128
+ source: { path: sourceRelative, ...source },
129
+ preview: { path: previewRelative, ...preview },
130
+ };
131
+ const name = `.thumbnail-preview-${createHash("sha256").update(sourceRelative).digest("hex").slice(0, 24)}.json`;
132
+ const directoryFd = openSync(
133
+ owner.workspace,
134
+ constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
135
+ );
136
+ try {
137
+ let anchor: string | undefined;
138
+ for (const prefix of ["/proc/self/fd", "/dev/fd"]) {
139
+ const candidate = `${prefix}/${directoryFd}`;
140
+ try {
141
+ if (realpathSync(candidate) === owner.workspace) {
142
+ anchor = candidate;
143
+ break;
144
+ }
145
+ } catch {
146
+ /* unsupported descriptor namespace */
147
+ }
148
+ }
149
+ if (!anchor) return null;
150
+ const target = path.join(anchor, name);
151
+ const temporary = `${target}.${randomUUID()}.tmp`;
152
+ const fd = openSync(
153
+ temporary,
154
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
155
+ 0o600,
156
+ );
157
+ try {
158
+ const bytes = Buffer.from(`${JSON.stringify(association)}\n`);
159
+ if (bytes.length > 16 * 1024) return null;
160
+ let offset = 0;
161
+ while (offset < bytes.length) offset += writeSync(fd, bytes, offset, bytes.length - offset);
162
+ if (
163
+ realpathSync(anchor) !== owner.workspace ||
164
+ realpathSync(owner.workspace) !== owner.workspace
165
+ )
166
+ return null;
167
+ // Stat again after the write, without reading another potentially large
168
+ // image. ctime detects same-size edits with a restored mtime.
169
+ const matches = (filename: string, expected: string) => {
170
+ const s = statSync(filename);
171
+ return `${s.dev}:${s.ino}:${s.size}:${s.mtimeMs}:${s.ctimeMs}` === expected;
172
+ };
173
+ if (!matches(htmlPath, source.version) || !matches(pngPath, preview.version)) return null;
174
+ renameSync(temporary, target);
175
+ } finally {
176
+ closeSync(fd);
177
+ try {
178
+ unlinkSync(temporary);
179
+ } catch {
180
+ /* renamed */
181
+ }
182
+ }
183
+ } finally {
184
+ closeSync(directoryFd);
185
+ }
186
+ return path.relative(owner.root, path.join(owner.workspace, name)).split(path.sep).join("/");
187
+ } catch {
188
+ return null;
189
+ }
190
+ }
@@ -194,3 +194,66 @@ export function bandsCoveringRects(
194
194
  }
195
195
  return out;
196
196
  }
197
+
198
+ /** All native-pixel candidates, before any per-pack allocation or cropping. */
199
+ export function reviewCandidateRects(args: {
200
+ width: number;
201
+ height: number;
202
+ bandHeight: number;
203
+ overlap: number;
204
+ atoms?: VisualAtom[];
205
+ scopes?: Array<{
206
+ selector: string;
207
+ rects: Array<{ label: string; x: number; y: number; width: number; height: number }>;
208
+ }>;
209
+ }): Array<{
210
+ id: string;
211
+ index: number;
212
+ label: string;
213
+ rect: { x: number; y: number; width: number; height: number };
214
+ scope?: string;
215
+ gate_hits: [];
216
+ }> {
217
+ const opts = { bandHeight: args.bandHeight, overlap: args.overlap };
218
+ const bands = args.atoms?.length
219
+ ? snappedBandRects(0, args.height, 0, args.width, args.atoms, opts).rects
220
+ : Array.from(
221
+ {
222
+ length:
223
+ Math.ceil(
224
+ Math.max(0, args.height - args.bandHeight) /
225
+ Math.max(1, args.bandHeight - args.overlap),
226
+ ) + 1,
227
+ },
228
+ (_, i) => {
229
+ const y = i * Math.max(1, args.bandHeight - args.overlap);
230
+ return { x: 0, y, width: args.width, height: Math.min(args.bandHeight, args.height - y) };
231
+ },
232
+ );
233
+ const out: ReturnType<typeof reviewCandidateRects> = bands.map((r, i) => ({
234
+ id: `band-${i + 1}`,
235
+ index: i,
236
+ label: `band ${i + 1}`,
237
+ rect: { x: r.x, y: r.y, width: r.width, height: r.height },
238
+ gate_hits: [],
239
+ }));
240
+ for (const [scopeIndex, scope] of (args.scopes ?? []).entries()) {
241
+ const expanded = scope.rects.flatMap((r) => bandOversizedRect(r, args.atoms ?? [], opts));
242
+ for (const [i, r] of expanded.entries()) {
243
+ const x = Math.max(0, Math.round(r.x)),
244
+ y = Math.max(0, Math.round(r.y));
245
+ const width = Math.min(Math.round(r.width), args.width - x),
246
+ height = Math.min(Math.round(r.height), args.height - y);
247
+ if (width <= 0 || height <= 0) continue;
248
+ out.push({
249
+ id: `scope-${scopeIndex + 1}-${i + 1}`,
250
+ index: out.length,
251
+ label: r.label,
252
+ scope: scope.selector,
253
+ rect: { x, y, width, height },
254
+ gate_hits: [],
255
+ });
256
+ }
257
+ }
258
+ return out;
259
+ }
@@ -18,7 +18,10 @@ export function isExcludedDocsMetadataPath(path: string): boolean {
18
18
  // Dashboard surfaces render from copies of other repositories' documents.
19
19
  /(?:^|\/)content\/dashboards\//.test(rel) ||
20
20
  /(?:^|\/)docs\/shopify\/admin-api\//.test(rel) ||
21
- /(?:^|\/)docs\/templates\//.test(rel)
21
+ // Template skeletons carry placeholder frontmatter by design. Hosts keep
22
+ // them under docs/templates/ or another documentation root such as
23
+ // meta-docs/templates/, so match the directory name rather than one root.
24
+ /(?:^|\/)templates\//.test(rel)
22
25
  );
23
26
  }
24
27
 
@@ -16,7 +16,6 @@
16
16
  import { randomUUID } from "node:crypto";
17
17
  import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
18
18
  import { join } from "node:path";
19
-
20
19
  import { recordLiveIdentityChangeV3 } from "../../core/agents/live-authority-v3.ts";
21
20
  import { recordLiveSweepObservationV3 } from "../../core/agents/live-lifecycle-v3.ts";
22
21
  import type { Heartbeat } from "../../core/agents/state/heartbeat-writer.ts";
@@ -27,6 +26,7 @@ import {
27
26
  import { resolveForkAncestry } from "../../core/agents/state/names.ts";
28
27
  import { instanceHasLivePid, removePidmapRowsForInstance } from "../../core/agents/state/pidmap.ts";
29
28
  import { coordFreshnessSeconds } from "../../core/config.ts";
29
+ import type { EventAdapterIdV3 } from "../../core/events/v3/adapter-id.ts";
30
30
  import { readRemoteMachines } from "../../core/presence/index.ts";
31
31
  import { type AgentIdentity, bareName, ensureIdentity, lookupById, lookupByName } from "./index.ts";
32
32
 
@@ -133,7 +133,7 @@ export function reclaimAbandonedLocalConflict(
133
133
  if (instanceHasLivePid(coordRoot, conflict.instance_id)) return false;
134
134
 
135
135
  const hbPath = join(coordRoot, ".harnery", "active", `${conflict.instance_id}.json`);
136
- let adapter: "claude-code" | "cursor" | "codex" = "claude-code";
136
+ let adapter: EventAdapterIdV3 = "claude-code";
137
137
  let sessionId = conflict.instance_id;
138
138
  let ageSecs: number | undefined;
139
139
  const liveRow = readLiveCoordinationRow(coordRoot, conflict.instance_id);
@@ -236,9 +236,10 @@ function acquireLock(coordRoot: string): () => void {
236
236
  );
237
237
  }
238
238
 
239
- function adapterOf(platform: string | undefined): "claude-code" | "cursor" | "codex" {
239
+ function adapterOf(platform: string | undefined): EventAdapterIdV3 {
240
240
  if (platform === "cursor") return "cursor";
241
241
  if (platform === "codex") return "codex";
242
+ if (platform === "openclaw") return "openclaw";
242
243
  return "claude-code";
243
244
  }
244
245
 
@@ -328,11 +329,18 @@ export function assumeIdentity(
328
329
  };
329
330
  }
330
331
 
332
+ const adapter = adapterOf(hb.platform);
333
+ if (adapter === "openclaw") {
334
+ throw new IdentityAssumeError(
335
+ "not_session",
336
+ "identity assumption is unavailable for observer-only OpenClaw generations",
337
+ );
338
+ }
331
339
  const emitted = recordLiveIdentityChangeV3({
332
340
  coordRoot,
333
341
  owner: instanceId,
334
342
  nativeSessionId: hb.native_session_id ?? hb.session_id,
335
- adapter: adapterOf(hb.platform),
343
+ adapter,
336
344
  name: identity.name,
337
345
  identityId: identity.agent_id,
338
346
  });
@@ -150,6 +150,7 @@ Use it for anything future-you or a peer will need to pick up your thread.
150
150
  and other untracked files that must survive a session, create a managed workspace
151
151
  with \`${b} artifacts create <slug> --purpose "<why>"\`. Write files under the
152
152
  returned path, then run \`${b} artifacts release <id>\` when active work is done.
153
+ After review, use \`${b} artifacts discard <id> --reason "<why no longer needed>"\` for disposable or superseded evidence: it releases ownership and shortens retention to at most 60 minutes (\`--minutes\` overrides). Keep unresolved failures, handoffs, final evidence, and files awaiting review; use a hold when needed. A passing check cannot establish whether another reader needs the files, so the reviewing agent decides. For known short-lived work, \`${b} artifacts create --minutes <n>\` sets the initial window.
153
154
  Do not create a repo-root temp directory; \`${b} artifacts clean\` previews
154
155
  expired cleanup and requires \`--yes\` to delete anything.
155
156
 
@@ -4,13 +4,28 @@ export const DEFAULT_WEB_PORT = 4276;
4
4
  /** Cookie-isolated host for navigable repo files. */
5
5
  export const FILES_ORIGIN_HOST = "harnery-files.localhost";
6
6
 
7
+ /**
8
+ * Percent-encode one URL component so the finished URL is safe as a bare
9
+ * Markdown destination.
10
+ *
11
+ * encodeURIComponent leaves parentheses raw, and a raw ")" ends a `[text](url)`
12
+ * destination early. The alternative is the angle-bracket destination form,
13
+ * which is valid CommonMark but is not implemented by every renderer, and one
14
+ * that skips it can style the text as a link while dropping the href, leaving
15
+ * something that looks clickable and does nothing. These URLs are written into
16
+ * Markdown for humans to click, so encode the parentheses instead.
17
+ */
18
+ export function encodeLinkSafeComponent(value: string): string {
19
+ return encodeURIComponent(value).replace(/[()]/g, (char) => (char === "(" ? "%28" : "%29"));
20
+ }
21
+
7
22
  /** Encode each repo-path segment while preserving slash-based URL resolution. */
8
23
  export function encodedRepoPath(relPath: string): string {
9
24
  return relPath
10
25
  .replace(/^\/+/, "")
11
26
  .split("/")
12
27
  .filter(Boolean)
13
- .map((segment) => encodeURIComponent(segment))
28
+ .map((segment) => encodeLinkSafeComponent(segment))
14
29
  .join("/");
15
30
  }
16
31
 
@@ -27,5 +42,5 @@ export function localFileViewerUrl(
27
42
  relPath: string,
28
43
  port: number | string = DEFAULT_WEB_PORT,
29
44
  ): string {
30
- return `http://localhost:${port}/files?path=${encodeURIComponent(relPath)}`;
45
+ return `http://localhost:${port}/files?path=${encodeLinkSafeComponent(relPath)}`;
31
46
  }
@@ -1,4 +1,4 @@
1
- // Tunnel config + state persistence + provider helpers. Lives under
1
+ // Tunnel config + state persistence + provider helpers. Commands default to
2
2
  // <cwd>/.cache/tunnel/; gitignored, so the allowlist is per-machine.
3
3
 
4
4
  import { execSync } from "node:child_process";
@@ -12,9 +12,10 @@ import {
12
12
  } from "node:fs";
13
13
  import { resolve } from "node:path";
14
14
 
15
- // Inline cachePath: tunnel state under <cwd>/.cache/tunnel/.
16
- function cachePath(tool: string, filename: string): string {
17
- const dir = resolve(process.cwd(), ".cache", tool);
15
+ // Tunnel state lives under <root>/.cache/tunnel/. Root defaults to cwd for the
16
+ // command surface and can be supplied by callers that already resolved a repo.
17
+ function cachePath(tool: string, filename: string, root: string = process.cwd()): string {
18
+ const dir = resolve(root, ".cache", tool);
18
19
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
19
20
  return resolve(dir, filename);
20
21
  }
@@ -107,8 +108,11 @@ export function writeConfig(cfg: TunnelConfig): void {
107
108
  writeFileSync(cachePath("tunnel", CONFIG_FILE), JSON.stringify(cfg, null, 2));
108
109
  }
109
110
 
110
- export function readState(name: string = DEFAULT_INSTANCE): TunnelState | null {
111
- const p = cachePath("tunnel", stateFile(name));
111
+ export function readState(
112
+ name: string = DEFAULT_INSTANCE,
113
+ root: string = process.cwd(),
114
+ ): TunnelState | null {
115
+ const p = cachePath("tunnel", stateFile(name), root);
112
116
  if (!existsSync(p)) return null;
113
117
  try {
114
118
  return normalizeState(JSON.parse(readFileSync(p, "utf-8")) as TunnelState, name);
@@ -117,12 +121,12 @@ export function readState(name: string = DEFAULT_INSTANCE): TunnelState | null {
117
121
  }
118
122
  }
119
123
 
120
- export function writeState(state: TunnelState): void {
121
- writeFileSync(cachePath("tunnel", stateFile(state.name)), JSON.stringify(state, null, 2));
124
+ export function writeState(state: TunnelState, root: string = process.cwd()): void {
125
+ writeFileSync(cachePath("tunnel", stateFile(state.name), root), JSON.stringify(state, null, 2));
122
126
  }
123
127
 
124
- export function clearState(name: string = DEFAULT_INSTANCE): void {
125
- const p = cachePath("tunnel", stateFile(name));
128
+ export function clearState(name: string = DEFAULT_INSTANCE, root: string = process.cwd()): void {
129
+ const p = cachePath("tunnel", stateFile(name), root);
126
130
  if (existsSync(p)) unlinkSync(p);
127
131
  }
128
132
 
@@ -130,14 +134,14 @@ export function clearState(name: string = DEFAULT_INSTANCE): void {
130
134
  * Every persisted tunnel instance, newest-started first. Reads every
131
135
  * `state*.json` under `.cache/tunnel/`; tolerates missing/corrupt files.
132
136
  */
133
- export function listStates(): TunnelState[] {
134
- const dir = resolve(process.cwd(), ".cache", "tunnel");
137
+ export function listStates(root: string = process.cwd()): TunnelState[] {
138
+ const dir = resolve(root, ".cache", "tunnel");
135
139
  if (!existsSync(dir)) return [];
136
140
  const out: TunnelState[] = [];
137
141
  for (const file of readdirSync(dir)) {
138
142
  const name = nameFromStateFile(file);
139
143
  if (name === null) continue;
140
- const state = readState(name);
144
+ const state = readState(name, root);
141
145
  if (state) out.push(state);
142
146
  }
143
147
  return out.sort((a, b) => (b.started_at ?? "").localeCompare(a.started_at ?? ""));
@@ -152,6 +156,59 @@ export function isProcessAlive(pid: number): boolean {
152
156
  }
153
157
  }
154
158
 
159
+ export type ProcessAliveCheck = (pid: number) => boolean;
160
+
161
+ /** True when both the local gate and its provider-side process are live. */
162
+ export function isTunnelStateLive(
163
+ state: TunnelState,
164
+ processAlive: ProcessAliveCheck = isProcessAlive,
165
+ ): boolean {
166
+ if (!processAlive(state.gate_pid)) return false;
167
+ if (state.provider === "tailscale") return true;
168
+ const providerPid = state.cloudflared_pid ?? state.provider_pid;
169
+ return typeof providerPid === "number" && processAlive(providerPid);
170
+ }
171
+
172
+ function tunnelTargetPort(state: TunnelState): number | null {
173
+ const value = /^[a-z][a-z\d+.-]*:\/\//i.test(state.target)
174
+ ? state.target
175
+ : `http://${state.target}`;
176
+ try {
177
+ const url = new URL(value);
178
+ if (url.port) return Number.parseInt(url.port, 10);
179
+ return url.protocol === "https:" ? 443 : 80;
180
+ } catch {
181
+ return null;
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Newest live tunnel that serves `vhost` on the requested local web port.
187
+ *
188
+ * The port alone does not identify a tunnel. Several tunnels can forward to one
189
+ * upstream port and be told apart only by the Host header they send, and a
190
+ * server that routes on Host then serves each of them a different site. Picking
191
+ * by port alone returns whichever such tunnel started last, so a caller that
192
+ * builds a URL for one site can hand back a host that serves another and every
193
+ * link 400s. Matching the vhost too keeps the returned tunnel the one that
194
+ * actually serves the caller's origin.
195
+ */
196
+ export function findLiveTunnelForOrigin(
197
+ port: number,
198
+ vhost: string,
199
+ root: string = process.cwd(),
200
+ processAlive: ProcessAliveCheck = isProcessAlive,
201
+ ): TunnelState | null {
202
+ return (
203
+ listStates(root).find(
204
+ (state) =>
205
+ tunnelTargetPort(state) === port &&
206
+ state.vhost === vhost &&
207
+ isTunnelStateLive(state, processAlive),
208
+ ) ?? null
209
+ );
210
+ }
211
+
155
212
  /**
156
213
  * Ensure cloudflared is on PATH or installed at ~/.local/bin/cloudflared.
157
214
  * Auto-downloads on Linux; throws on other platforms with brew hint.