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
@@ -260,7 +260,10 @@ async function handleStateAction(root: string, action: string, rest: string[]):
260
260
  // same durable, idempotent pool path used by normal SessionStart.
261
261
  if (task && before && humanFacing && !before.name?.trim()) {
262
262
  const { assignName } = await import("./state/names.ts");
263
- const name = assignName(root, owner, "session");
263
+ const { coordFreshnessSeconds } = await import("../config.ts");
264
+ const name = assignName(root, owner, "session", {
265
+ freshnessSecs: coordFreshnessSeconds(root),
266
+ });
264
267
  before = writer.setAssignedNameCache(root, owner, name, "session");
265
268
  if (!before) {
266
269
  process.stderr.write(
@@ -302,7 +305,10 @@ async function handleStateAction(root: string, action: string, rest: string[]):
302
305
  (!hb.name?.trim() || /^Agent unknown - /.test(hb.suggested_session_name ?? ""))
303
306
  ) {
304
307
  const { assignName } = await import("./state/names.ts");
305
- const name = hb.name?.trim() || assignName(root, owner, "session");
308
+ const { coordFreshnessSeconds } = await import("../config.ts");
309
+ const name =
310
+ hb.name?.trim() ||
311
+ assignName(root, owner, "session", { freshnessSecs: coordFreshnessSeconds(root) });
306
312
  hb = writer.setAssignedNameCache(root, owner, name, "session");
307
313
  }
308
314
  } catch (error) {
@@ -640,7 +646,11 @@ async function handleAssignName(root: string, rest: string[]): Promise<number> {
640
646
  process.stderr.write(`agent-coord assign-name: invalid kind ${kindArg}\n`);
641
647
  return 2;
642
648
  }
643
- const name = assignName(root, owner, kindArg, forkedFrom ? { forkedFrom } : undefined);
649
+ const { coordFreshnessSeconds } = await import("../config.ts");
650
+ const name = assignName(root, owner, kindArg, {
651
+ freshnessSecs: coordFreshnessSeconds(root),
652
+ ...(forkedFrom ? { forkedFrom } : {}),
653
+ });
644
654
  process.stdout.write(
645
655
  `${JSON.stringify({
646
656
  instance_id: owner,
@@ -761,7 +771,6 @@ async function handlePromptContext(root: string, rest: string[]): Promise<number
761
771
  const instanceId = args.instance;
762
772
  const sessionId = args.session ?? instanceId;
763
773
  const agentName = args.name;
764
- const adapter = args.adapter;
765
774
  const sessionNameNudge = args["session-name-nudge"] === "true";
766
775
  const taskNudge = args["task-nudge"] === "true";
767
776
  const hostPromptReminder = args["host-prompt-reminder"] === "true";
@@ -772,7 +781,7 @@ async function handlePromptContext(root: string, rest: string[]): Promise<number
772
781
  args["turn-ritual-nudge"] === "true" ? "generic" : args["turn-ritual-nudge"];
773
782
  if (!instanceId) {
774
783
  process.stderr.write(
775
- "agent-coord prompt-context --instance <id> [--session <id>] [--name <agent-name>] [--adapter <id>] [--session-name-nudge] [--task-nudge] [--host-prompt-reminder] [--status-footer-nudge] [--turn-ritual-nudge <adapter>]\n",
784
+ "agent-coord prompt-context --instance <id> [--session <id>] [--name <agent-name>] [--session-name-nudge] [--task-nudge] [--host-prompt-reminder] [--status-footer-nudge] [--turn-ritual-nudge <adapter>]\n",
776
785
  );
777
786
  return 2;
778
787
  }
@@ -787,7 +796,6 @@ async function handlePromptContext(root: string, rest: string[]): Promise<number
787
796
  hostPromptReminder,
788
797
  statusFooterNudge,
789
798
  turnRitualNudge,
790
- adapter,
791
799
  });
792
800
  process.stdout.write(text);
793
801
  return 0;
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { Adapter } from "../adapter.ts";
3
+ import { EVENT_ADAPTER_IDS_V3 } from "../events/v3/adapter-id.ts";
3
4
  import type { AuthorityMutationV3 } from "../events/v3/authority-outbox.ts";
4
5
  import { canonicalJsonV3, normalizeNativeIdV3, sha256V3 } from "../events/v3/canonical.ts";
5
6
  import { type EventV3WriteMode, readEventV3ControlState } from "../events/v3/control.ts";
@@ -246,9 +247,9 @@ function readSessionProducerStatesForBootstrap(
246
247
  nativeSessionId: string,
247
248
  ): HookProducerStateV3[] {
248
249
  try {
249
- return (["claude-code", "codex", "cursor"] as const)
250
- .map((adapter) => readHookProducerStateV3(coordRoot, adapter, nativeSessionId))
251
- .filter((state): state is HookProducerStateV3 => state !== undefined);
250
+ return EVENT_ADAPTER_IDS_V3.map((adapter) =>
251
+ readHookProducerStateV3(coordRoot, adapter, nativeSessionId),
252
+ ).filter((state): state is HookProducerStateV3 => state !== undefined);
252
253
  } catch {
253
254
  throw new LiveCoordinationAuthorityV3Error("producer_state_unsafe");
254
255
  }
@@ -424,6 +425,9 @@ export function reopenLiveCoordinationGenerationV3(input: {
424
425
  if (!terminalView || terminalView.instance_id !== instanceId) {
425
426
  throw new LiveCoordinationAuthorityV3Error("terminal_generation_authority_missing");
426
427
  }
428
+ if (terminal.adapter === "openclaw") {
429
+ throw new LiveCoordinationAuthorityV3Error("lifecycle_not_human_facing");
430
+ }
427
431
  if (terminalView.parent_generation_id || terminalView.delegation_id || terminalView.workflow_id) {
428
432
  throw new LiveCoordinationAuthorityV3Error("lifecycle_not_human_facing");
429
433
  }
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import type { Adapter } from "../adapter.ts";
2
+ import type { EventAdapterIdV3 } from "../events/v3/adapter-id.ts";
3
3
  import { buildEventV3 } from "../events/v3/builder.ts";
4
4
  import { normalizeNativeIdV3 } from "../events/v3/canonical.ts";
5
5
  import type { EventV3 } from "../events/v3/contract.ts";
@@ -21,7 +21,7 @@ interface LiveLifecycleObservationBaseV3 {
21
21
  coordRoot: string;
22
22
  owner: string;
23
23
  nativeSessionId: string;
24
- adapter: Adapter;
24
+ adapter: EventAdapterIdV3;
25
25
  observedAt?: string;
26
26
  }
27
27
 
@@ -46,12 +46,13 @@ import {
46
46
  import { join } from "node:path";
47
47
 
48
48
  import { coordEnv } from "../../../lib/env.ts";
49
- import type { Adapter } from "../../adapter.ts";
50
49
  import { endOfTurnStatusCommand, hostPromptReminder, resolveBinName } from "../../config.ts";
51
- import { canReceiveContext } from "../../hooks/adapter/output.ts";
50
+ import type { PressureWorkloadClass } from "../../diagnostics/contract.ts";
52
51
  import { type RemoteMachine, readRemoteMachines } from "../../presence/index.ts";
52
+ import { readResourceStatus } from "../../resources/status.ts";
53
53
  import { drainMailbox, formatMailboxDelivery } from "../mailbox.ts";
54
54
  import { sessionNameDisplayBlock, sessionNameDisplayPending } from "../session-name-display.ts";
55
+ import { heartbeatPath } from "../state/heartbeat-reader.ts";
55
56
  import {
56
57
  readLiveCoordinationRow,
57
58
  readLiveCoordinationRows,
@@ -95,11 +96,6 @@ export interface PromptContextOpts {
95
96
  /** When true, append the non-deduplicated Codex status-footer reminder.
96
97
  * The caller enables this only for human-facing Codex sessions. */
97
98
  statusFooterNudge?: boolean;
98
- /** Adapter id this prompt is being rendered for. Gates the mailbox drain:
99
- * an adapter that cannot receive UserPromptSubmit context must not consume
100
- * messages here, or they are removed from the queue and never shown. When
101
- * omitted, delivery is attempted (the default route can receive). */
102
- adapter?: string;
103
99
  /** Adapter id whose Stop hook ENFORCES the end-of-turn ritual (task_set +
104
100
  * status check). When set, append a fresh per-prompt reminder of that
105
101
  * contract so the model satisfies it before ending the turn instead of
@@ -127,7 +123,6 @@ export function renderPromptContext(opts: PromptContextOpts): string {
127
123
  hostPromptReminder: promptReminderEnabled,
128
124
  statusFooterNudge,
129
125
  turnRitualNudge,
130
- adapter,
131
126
  } = opts;
132
127
  const sections: string[] = [];
133
128
 
@@ -140,12 +135,7 @@ export function renderPromptContext(opts: PromptContextOpts): string {
140
135
  // exactly once, and it is the one section here that carries new information
141
136
  // from another agent rather than a reminder.
142
137
  //
143
- // Draining is gated on the adapter being able to receive this event's
144
- // context. Cursor cannot, so draining there would empty the queue into output
145
- // that is discarded; those sessions get their messages at SessionStart, which
146
- // Cursor does deliver.
147
- const canDeliverHere = !adapter || canReceiveContext(adapter as Adapter, "UserPromptSubmit");
148
- if (resolvedName && canDeliverHere) {
138
+ if (resolvedName) {
149
139
  const delivered = drainMailbox(coordRoot, resolvedName);
150
140
  if (delivered.length > 0) {
151
141
  recordDeliveredMessages(instanceId, delivered);
@@ -157,6 +147,9 @@ export function renderPromptContext(opts: PromptContextOpts): string {
157
147
  const peerTable = computePeerTableIfChanged(coordRoot, instanceId, sessionId);
158
148
  if (peerTable) sections.push(peerTable);
159
149
 
150
+ const resources = resourceWarningIfChanged(coordRoot, instanceId);
151
+ if (resources) sections.push(resources);
152
+
160
153
  // 3. Council pending with hash dedup.
161
154
  if (resolvedName) {
162
155
  const councilMsg = computeCouncilPendingIfChanged(coordRoot, instanceId, resolvedName);
@@ -203,6 +196,67 @@ export function renderPromptContext(opts: PromptContextOpts): string {
203
196
  return sections.join("\n\n");
204
197
  }
205
198
 
199
+ /**
200
+ * Prompts receive the published assessment, and only when it changes in a way
201
+ * that changes what an agent should do. The dedupe key is scope, state, and
202
+ * recommended action, so a moving number or a different contributor does not
203
+ * re-emit the same advice.
204
+ */
205
+ export function resourceWarningIfChanged(coordRoot: string, instanceId: string): string {
206
+ try {
207
+ heartbeatPath(coordRoot, instanceId);
208
+ const status = readResourceStatus(coordRoot);
209
+ const assessment = status.assessment;
210
+ const hashFile = join(coordRoot, ".harnery", `.last-resource-hash.${instanceId}`);
211
+ const previous = safeRead(hashFile);
212
+ // Existing users without an observer should retain ordinary coordination.
213
+ if (status.assessment_capability.reason_code === "pressure_record_missing" && !previous) {
214
+ return "";
215
+ }
216
+ const key = `${assessment.scope}:${assessment.state}:${assessment.recommended_action}`;
217
+ if (previous === key) return "";
218
+ writeHashFile(hashFile, key);
219
+ const warning = assessment.state === "elevated" || assessment.state === "critical";
220
+ const wasWarning =
221
+ !!previous && (previous.includes(":elevated:") || previous.includes(":critical:"));
222
+ if (!warning && !wasWarning) return "";
223
+ const lines = [`Resource update: ${assessment.summary}`];
224
+ if (assessment.state === "unknown") {
225
+ lines.push(
226
+ "Resource headroom cannot be measured right now, so continue normal coordination and judge heavy work yourself.",
227
+ );
228
+ } else if (!warning && wasWarning) {
229
+ lines.push("The previously reported resource warning has cleared.");
230
+ }
231
+ for (const item of assessment.guidance) {
232
+ lines.push(`${WORKLOAD_LABEL[item.workload_class]}: ${item.summary}`);
233
+ }
234
+ const owners = assessment.contributors
235
+ .filter((row) => row.attribution_confidence === "exact" && row.owner_id)
236
+ .slice(0, 3)
237
+ .map((row) => `${row.owner_kind ?? "owner"} ${row.owner_id}`);
238
+ if (owners.length) {
239
+ lines.push(
240
+ `Measured owners of the contended resource: ${owners.join(", ")}. That says who is holding it, not what the machine as a whole can take.`,
241
+ );
242
+ }
243
+ lines.push(
244
+ `Read \`${resolveBinName(coordRoot)} resources status --json\` for the evidence behind this.`,
245
+ );
246
+ return lines.join("\n");
247
+ } catch {
248
+ return "";
249
+ }
250
+ }
251
+
252
+ /** How an agent recognizes the work each guidance entry is about. */
253
+ const WORKLOAD_LABEL: Readonly<Record<PressureWorkloadClass, string>> = {
254
+ lightweight: "Reads and small edits",
255
+ "cpu-heavy": "Builds and test runs",
256
+ "memory-heavy": "Browser captures and page QA",
257
+ "storage-heavy": "Large writes and exports",
258
+ };
259
+
206
260
  function renderHostPromptReminder(coordRoot: string, selfInstanceId: string): string {
207
261
  const hb = readLiveCoordinationRow(coordRoot, selfInstanceId);
208
262
  if (!hb || hb.kind === "subagent" || hb.kind === "transient" || hb.workflow_run_id) return "";
@@ -12,8 +12,8 @@ import {
12
12
  } from "node:fs";
13
13
  import { hostname } from "node:os";
14
14
  import { join, resolve } from "node:path";
15
- import type { Adapter } from "../adapter.ts";
16
15
  import { sessionFinalizationConfig } from "../config.ts";
16
+ import type { EventAdapterIdV3 } from "../events/v3/adapter-id.ts";
17
17
  import { buildEventV3 } from "../events/v3/builder.ts";
18
18
  import { normalizeNativeIdV3 } from "../events/v3/canonical.ts";
19
19
  import type { EventV3 } from "../events/v3/contract.ts";
@@ -55,7 +55,7 @@ export {
55
55
  const EXPLICIT_END_PENDING_EXPIRY_MS = 24 * 60 * 60 * 1000;
56
56
 
57
57
  export interface SessionArchiveObservationV3 {
58
- adapter: Adapter;
58
+ adapter: EventAdapterIdV3;
59
59
  native_session_id: string;
60
60
  archived: boolean;
61
61
  observed_at: string;
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import type { Adapter } from "../../adapter.ts";
3
+ import { EVENT_ADAPTER_IDS_V3, type EventAdapterIdV3 } from "../../events/v3/adapter-id.ts";
4
4
  import {
5
5
  type CoordinationGenerationViewV3,
6
6
  type CoordinationViewV3,
@@ -94,7 +94,7 @@ export function readLiveCoordinationRow(
94
94
  export function liveCoordinationAdapterV3(
95
95
  coordRoot: string,
96
96
  nativeInstanceId: string,
97
- ): Adapter | null {
97
+ ): EventAdapterIdV3 | null {
98
98
  const route = observeLiveEventLedgerRouteV3(coordRoot);
99
99
  if (route.state === "blocked") throw new Error(`event_v3_coordination_view:${route.reason}`);
100
100
  const view = requireAuthoritySafeCoordinationViewV3(readCoordinationViewV3(coordRoot));
@@ -213,8 +213,8 @@ export function observedAdapterV3(generation: CoordinationGenerationViewV3): str
213
213
  return observation.state === "observed" ? observation.value.id : undefined;
214
214
  }
215
215
 
216
- export function isAdapterV3(value: string | undefined): value is Adapter {
217
- return value === "claude-code" || value === "cursor" || value === "codex";
216
+ export function isAdapterV3(value: string | undefined): value is EventAdapterIdV3 {
217
+ return (EVENT_ADAPTER_IDS_V3 as readonly string[]).includes(value ?? "");
218
218
  }
219
219
 
220
220
  function observedModel(generation: CoordinationGenerationViewV3): string | undefined {
@@ -25,9 +25,23 @@
25
25
  * new binding instead of rewriting history; readers therefore resolve from the
26
26
  * end of the file. Older one-row histories retain their original behavior.
27
27
  * 3. Else: new assignment, consume a counter slot.
28
+ *
29
+ * Live-name skip: the pool holds 260 names and a busy repo consumes ~100 a
30
+ * day, so the counter wraps roughly every three days while sessions that
31
+ * resume under a stable instance_id keep a name for far longer. A counter-only
32
+ * pick therefore hands a live agent's name to a new one (observed 2026-09-06:
33
+ * two concurrent "Maya" agents, their pool slots exactly 520 apart). Assignment
34
+ * now probes forward past any name a fresh heartbeat still holds.
28
35
  */
29
36
 
30
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
37
+ import {
38
+ existsSync,
39
+ mkdirSync,
40
+ readdirSync,
41
+ readFileSync,
42
+ renameSync,
43
+ writeFileSync,
44
+ } from "node:fs";
31
45
  import { dirname, join } from "node:path";
32
46
 
33
47
  /** All 260 names. */
@@ -308,7 +322,7 @@ if (COORD_NAMES.length !== 260) {
308
322
  throw new Error(`COORD_NAMES table corrupt: expected 260 entries, got ${COORD_NAMES.length}`);
309
323
  }
310
324
 
311
- export type NameKind = "session" | "subagent" | "transient";
325
+ export type NameKind = "session" | "subagent" | "transient" | "workflow-child";
312
326
 
313
327
  export interface NameHistoryRow {
314
328
  instance_id: string;
@@ -441,15 +455,68 @@ export function recordNameAssumption(
441
455
  return { changed: true, previous, current };
442
456
  }
443
457
 
458
+ /**
459
+ * Names a live agent currently holds, read from the generation-bound heartbeat
460
+ * cache (`.harnery/active/<instance>.json`).
461
+ *
462
+ * Deliberately reads the cache directly rather than the V3 coordination view:
463
+ * that view imports `resolveName` from this module, so depending on it here
464
+ * would build an import cycle. The cache is the same set the view materializes
465
+ * from and is pruned as sessions end, so it answers "is this name taken right
466
+ * now" without one.
467
+ *
468
+ * Both error directions are safe for the caller. A stale row that slips past
469
+ * the freshness window costs one skipped pool slot; a live agent missing from
470
+ * the cache just restores the old counter-only behavior for that assignment.
471
+ */
472
+ export function readLiveNames(
473
+ coordRoot: string,
474
+ opts: { freshnessSecs: number; nowMs?: number },
475
+ ): Set<string> {
476
+ const live = new Set<string>();
477
+ const dir = join(coordRoot, ".harnery", "active");
478
+ let entries: string[];
479
+ try {
480
+ entries = readdirSync(dir);
481
+ } catch {
482
+ return live; // No cache yet (fresh checkout) → nothing is live.
483
+ }
484
+
485
+ const nowMs = opts.nowMs ?? Date.now();
486
+ const cutoffMs = nowMs - opts.freshnessSecs * 1000;
487
+
488
+ for (const entry of entries) {
489
+ if (!entry.endsWith(".json")) continue;
490
+ let row: { name?: unknown; last_heartbeat?: unknown };
491
+ try {
492
+ row = JSON.parse(readFileSync(join(dir, entry), "utf8"));
493
+ } catch {
494
+ continue; // A torn or half-written row cannot prove a name is held.
495
+ }
496
+ if (typeof row.name !== "string" || row.name.length === 0) continue;
497
+ if (typeof row.last_heartbeat !== "string") continue;
498
+ const beatMs = Date.parse(row.last_heartbeat);
499
+ if (!Number.isFinite(beatMs) || beatMs < cutoffMs) continue;
500
+ live.add(row.name);
501
+ }
502
+ return live;
503
+ }
504
+
444
505
  /**
445
506
  * Assign a name to <instanceId> with the given <kind>. Counter-consuming when
446
507
  * the owner is new. Idempotent: returns existing name on resume.
508
+ *
509
+ * Slot choice probes forward from the counter and takes the first name no live
510
+ * agent holds, then advances the counter past every slot it skipped so the skip
511
+ * is durable rather than re-probed on the next assignment. A full lap without a
512
+ * free name means all 260 are genuinely live, so the raw counter slot is used:
513
+ * a duplicate name beats refusing to name an agent at all.
447
514
  */
448
515
  export function assignName(
449
516
  coordRoot: string,
450
517
  instanceId: string,
451
518
  kind: NameKind,
452
- opts?: { forkedFrom?: string },
519
+ opts: { freshnessSecs: number; forkedFrom?: string; nowMs?: number },
453
520
  ): string {
454
521
  // Check 1: existing history row → original name. A resume re-enters here,
455
522
  // which also makes fork stamping naturally idempotent: lineage lands only on
@@ -457,16 +524,34 @@ export function assignName(
457
524
  const existing = resolveName(coordRoot, instanceId);
458
525
  if (existing) return existing.name;
459
526
 
460
- // New owner: consume a counter slot.
527
+ // New owner: consume a counter slot. The live set is gathered BEFORE the
528
+ // counter read on purpose. Counter read-modify-write is already unlocked, so
529
+ // concurrent assignments can collide on one slot; doing the directory scan
530
+ // first keeps that window exactly as narrow as it was before the skip existed.
531
+ const live = readLiveNames(coordRoot, {
532
+ freshnessSecs: opts.freshnessSecs,
533
+ ...(opts.nowMs !== undefined ? { nowMs: opts.nowMs } : {}),
534
+ });
535
+
461
536
  const cPath = counterPath(coordRoot);
462
537
  let counter = 0;
463
538
  if (existsSync(cPath)) {
464
539
  const raw = readFileSync(cPath, "utf8").trim();
465
540
  if (/^\d+$/.test(raw)) counter = Number.parseInt(raw, 10);
466
541
  }
467
- const name = COORD_NAMES[counter % 260]!;
468
- atomicWrite(cPath, String(counter + 1));
469
- const forkedFrom = opts?.forkedFrom;
542
+
543
+ let chosen = counter;
544
+ for (let probe = 0; probe < 260; probe++) {
545
+ const candidate = COORD_NAMES[(counter + probe) % 260]!;
546
+ if (!live.has(candidate)) {
547
+ chosen = counter + probe;
548
+ break;
549
+ }
550
+ }
551
+
552
+ const name = COORD_NAMES[chosen % 260]!;
553
+ atomicWrite(cPath, String(chosen + 1));
554
+ const forkedFrom = opts.forkedFrom;
470
555
  appendHistory(coordRoot, {
471
556
  instance_id: instanceId,
472
557
  name,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Stale-sweep: prune dead heartbeats + orphaned pid-map + .last-peer-hash
2
+ * Stale-sweep: prune dead heartbeats + orphaned pid-map + prompt hash
3
3
  * files.
4
4
  *
5
5
  * Runs after session.started to clean up crashed-peer detritus before the new
@@ -13,13 +13,15 @@
13
13
  import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
  import { coordFreshnessSeconds } from "../../config.ts";
16
+ import type { EventAdapterIdV3 } from "../../events/v3/adapter-id.ts";
16
17
  import { writeProducerDiagnosticV3 } from "../../events/v3/producers/intake.ts";
17
18
  import { recordLiveSweepObservationV3 } from "../live-lifecycle-v3.ts";
18
19
 
19
20
  /** platform → adapter, for the swept-event envelope (mirrors heartbeat-writer's adapterOf). */
20
- function adapterFromPlatform(platform: unknown): "claude-code" | "cursor" | "codex" {
21
+ function adapterFromPlatform(platform: unknown): EventAdapterIdV3 {
21
22
  if (platform === "cursor") return "cursor";
22
23
  if (platform === "codex") return "codex";
24
+ if (platform === "openclaw") return "openclaw";
23
25
  return "claude-code";
24
26
  }
25
27
 
@@ -35,7 +37,7 @@ function adapterFromPlatform(platform: unknown): "claude-code" | "cursor" | "cod
35
37
  function emitSwept(
36
38
  coordRoot: string,
37
39
  instanceId: string,
38
- adapter: "claude-code" | "cursor" | "codex",
40
+ adapter: EventAdapterIdV3,
39
41
  sessionId: string,
40
42
  reason: "stale" | "unparseable" | "missing_ts",
41
43
  ageSecs?: number,
@@ -83,10 +85,12 @@ export function staleSweep(coordRoot: string): {
83
85
  heartbeatsRemoved: string[];
84
86
  pidmapsRemoved: number;
85
87
  peerHashesRemoved: number;
88
+ resourceHashesRemoved: number;
86
89
  } {
87
90
  const heartbeatsRemoved: string[] = [];
88
91
  let pidmapsRemoved = 0;
89
92
  let peerHashesRemoved = 0;
93
+ let resourceHashesRemoved = 0;
90
94
 
91
95
  const freshness = coordFreshnessSeconds(coordRoot);
92
96
  const nowSec = Math.floor(Date.now() / 1000);
@@ -207,16 +211,26 @@ export function staleSweep(coordRoot: string): {
207
211
  }
208
212
  }
209
213
 
210
- // 3. Prune .last-peer-hash files for dead owners.
214
+ // 3. Prune prompt hashes for dead owners. Resource hashes retain their own
215
+ // count so existing peer-hash telemetry keeps its original meaning.
211
216
  const agentsDir = join(coordRoot, ".harnery");
212
217
  if (existsSync(agentsDir)) {
213
218
  for (const f of readdirSync(agentsDir)) {
214
- if (!f.startsWith(".last-peer-hash.")) continue;
215
- const owner = f.slice(".last-peer-hash.".length);
219
+ const resourceHash = f.startsWith(".last-resource-hash.");
220
+ const prefix = resourceHash ? ".last-resource-hash." : ".last-peer-hash.";
221
+ if (!f.startsWith(prefix)) continue;
222
+ const owner = f.slice(prefix.length);
223
+ if (resourceHash) {
224
+ // Ignore atomic-write temporary files and preserve hashes when a
225
+ // retained heartbeat's liveness could not be determined safely.
226
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(owner)) continue;
227
+ if (existsSync(join(d, `${owner}.json`))) continue;
228
+ }
216
229
  if (!liveInstanceIds.has(owner)) {
217
230
  try {
218
231
  unlinkSync(join(agentsDir, f));
219
- peerHashesRemoved += 1;
232
+ if (resourceHash) resourceHashesRemoved += 1;
233
+ else peerHashesRemoved += 1;
220
234
  } catch {
221
235
  /* swallow */
222
236
  }
@@ -224,5 +238,5 @@ export function staleSweep(coordRoot: string): {
224
238
  }
225
239
  }
226
240
 
227
- return { heartbeatsRemoved, pidmapsRemoved, peerHashesRemoved };
241
+ return { heartbeatsRemoved, pidmapsRemoved, peerHashesRemoved, resourceHashesRemoved };
228
242
  }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * How long the current turn has been running, for the closing status box.
3
+ *
4
+ * The box already reports `session`, which is cumulative and says nothing
5
+ * about the work just done: a 9-minute session hides whether this turn took
6
+ * twelve seconds or eight minutes. `turn` measures from the operator's prompt
7
+ * (`turn.started`) to the moment the box renders, so the operator reads the
8
+ * cost of the turn they are about to be handed.
9
+ *
10
+ * Every read here is best-effort by contract. A missing ledger, an
11
+ * unvalidated one, or no `turn.started` for this owner renders no row rather
12
+ * than failing the status command.
13
+ */
14
+ import type { EventV3 } from "../events/v3/contract.ts";
15
+ import { liveInstanceIdV3 } from "../events/v3/live-routing.ts";
16
+ import { readLedgerV3 } from "../events/v3/reader.ts";
17
+
18
+ /** Stop-hook remediation restarts to walk back through before giving up. */
19
+ const MAX_REMEDIATION_WALK_BACK = 8;
20
+
21
+ export interface TurnElapsed {
22
+ /** Seconds from the turn's start to its terminal, or to `now` while open. */
23
+ secs: number;
24
+ /** True once a `turn.completed` closed the turn this measures. */
25
+ complete: boolean;
26
+ /** Stop-hook restarts folded into this measurement. */
27
+ remediation_restarts: number;
28
+ }
29
+
30
+ function timeMs(event: EventV3 | undefined): number {
31
+ if (!event) return Number.NaN;
32
+ return Date.parse(event.time.observed_at);
33
+ }
34
+
35
+ function generationOf(event: EventV3): string | undefined {
36
+ return "generation_id" in event.scope ? event.scope.generation_id : undefined;
37
+ }
38
+
39
+ /**
40
+ * Measure the newest turn for `instanceId` from an already-read event stream.
41
+ *
42
+ * A Stop-hook bounce ends one `turn.started` and opens another carrying
43
+ * `stop_remediation`. The operator experiences those as one turn, so the walk
44
+ * back over remediation restarts reports elapsed time since their prompt, not
45
+ * since the last retry. The walk stays inside one generation: a resumed
46
+ * session must not absorb the previous generation's turn into its own.
47
+ */
48
+ export function resolveTurnElapsed(
49
+ events: readonly EventV3[],
50
+ instanceId: string,
51
+ now: number = Date.now(),
52
+ ): TurnElapsed | null {
53
+ const owner = liveInstanceIdV3(instanceId);
54
+ const ownerEvents = events.filter((event) => event.scope.instance_id === owner);
55
+ const starts = ownerEvents.filter(
56
+ (event): event is Extract<EventV3, { event_type: "turn.started" }> =>
57
+ event.event_type === "turn.started" && timeMs(event) <= now,
58
+ );
59
+ if (starts.length === 0) return null;
60
+
61
+ let index = starts.length - 1;
62
+ const generation = generationOf(starts[index]!);
63
+ let restarts = 0;
64
+ while (index > 0 && restarts < MAX_REMEDIATION_WALK_BACK) {
65
+ const turn = starts[index];
66
+ if (turn?.payload.stop_remediation !== true) break;
67
+ const previous = starts[index - 1];
68
+ if (!previous || generationOf(previous) !== generation) break;
69
+ index -= 1;
70
+ restarts += 1;
71
+ }
72
+
73
+ const startMs = timeMs(starts[index]);
74
+ if (!Number.isFinite(startMs)) return null;
75
+
76
+ // A terminal only closes the measurement when nothing restarted the turn
77
+ // after it, so match against the newest start rather than the walked-back
78
+ // one. Otherwise the `turn.completed` that a Stop-hook bounce produced would
79
+ // end the measurement mid-remediation.
80
+ const newestStartMs = timeMs(starts[starts.length - 1]);
81
+ const terminal = ownerEvents.find(
82
+ (event) =>
83
+ event.event_type === "turn.completed" &&
84
+ generationOf(event) === generation &&
85
+ timeMs(event) >= newestStartMs,
86
+ );
87
+ const terminalMs = timeMs(terminal);
88
+ const endMs = Number.isFinite(terminalMs) ? terminalMs : now;
89
+ return {
90
+ secs: Math.max(0, Math.floor((endMs - startMs) / 1000)),
91
+ complete: Number.isFinite(terminalMs),
92
+ remediation_restarts: restarts,
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Render a turn duration for the box. Seconds stay visible below an hour
98
+ * because most turns land there and a bare `4m` cannot be compared with the
99
+ * turn before it.
100
+ */
101
+ export function formatTurnElapsed(secs: number): string {
102
+ if (secs < 60) return `${secs}s`;
103
+ if (secs < 3600) return `${Math.floor(secs / 60)}m ${secs % 60}s`;
104
+ const hours = Math.floor(secs / 3600);
105
+ const minutes = Math.floor((secs % 3600) / 60);
106
+ return `${hours}h ${minutes}m`;
107
+ }
108
+
109
+ export interface TurnElapsedTarget {
110
+ coordRoot: string;
111
+ instanceId: string;
112
+ }
113
+
114
+ /**
115
+ * The whole status-box contribution in one best-effort call: read the ledger
116
+ * and render this session's `turn` row. Null means render no row.
117
+ */
118
+ export function turnElapsedStatusRow(
119
+ target: TurnElapsedTarget,
120
+ now: number = Date.now(),
121
+ ): { value: string; elapsed: TurnElapsed } | null {
122
+ try {
123
+ const ledger = readLedgerV3(target.coordRoot);
124
+ if (!ledger.complete) return null;
125
+ const elapsed = resolveTurnElapsed(
126
+ ledger.events.map((positioned) => positioned.event),
127
+ target.instanceId,
128
+ now,
129
+ );
130
+ if (!elapsed) return null;
131
+ return { value: formatTurnElapsed(elapsed.secs), elapsed };
132
+ } catch {
133
+ return null;
134
+ }
135
+ }