harnery 0.33.0 → 0.34.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 (657) hide show
  1. package/dist/commander.d.ts.map +1 -1
  2. package/dist/commander.js +4 -0
  3. package/dist/commands/adapter.js +2 -2
  4. package/dist/commands/agents.d.ts +144 -1
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +1220 -588
  7. package/dist/commands/artifacts.d.ts.map +1 -1
  8. package/dist/commands/artifacts.js +7 -6
  9. package/dist/commands/backup.d.ts +1 -1
  10. package/dist/commands/backup.d.ts.map +1 -1
  11. package/dist/commands/backup.js +29 -16
  12. package/dist/commands/browse.d.ts.map +1 -1
  13. package/dist/commands/browse.js +234 -12
  14. package/dist/commands/checkpoint.d.ts.map +1 -1
  15. package/dist/commands/checkpoint.js +3 -2
  16. package/dist/commands/claude-desktop.d.ts +1 -1
  17. package/dist/commands/claude-desktop.d.ts.map +1 -1
  18. package/dist/commands/claude-desktop.js +3 -74
  19. package/dist/commands/context.js +1 -6
  20. package/dist/commands/decision.d.ts.map +1 -1
  21. package/dist/commands/decision.js +22 -38
  22. package/dist/commands/docs.d.ts.map +1 -1
  23. package/dist/commands/docs.js +91 -28
  24. package/dist/commands/doctor.js +10 -1
  25. package/dist/commands/events.d.ts +5 -0
  26. package/dist/commands/events.d.ts.map +1 -0
  27. package/dist/commands/events.js +189 -0
  28. package/dist/commands/governor.d.ts.map +1 -1
  29. package/dist/commands/governor.js +13 -4
  30. package/dist/commands/init.d.ts +9 -2
  31. package/dist/commands/init.d.ts.map +1 -1
  32. package/dist/commands/init.js +223 -51
  33. package/dist/commands/journal.d.ts.map +1 -1
  34. package/dist/commands/journal.js +4 -13
  35. package/dist/commands/ledger-v3.d.ts +5 -0
  36. package/dist/commands/ledger-v3.d.ts.map +1 -0
  37. package/dist/commands/ledger-v3.js +91 -0
  38. package/dist/commands/presence.d.ts.map +1 -1
  39. package/dist/commands/presence.js +11 -5
  40. package/dist/commands/sync.d.ts +1 -1
  41. package/dist/commands/sync.js +1 -1
  42. package/dist/commands/web.d.ts +6 -0
  43. package/dist/commands/web.d.ts.map +1 -1
  44. package/dist/commands/web.js +67 -7
  45. package/dist/commands/work.d.ts.map +1 -1
  46. package/dist/commands/work.js +22 -5
  47. package/dist/core/adapter.d.ts +5 -0
  48. package/dist/core/adapter.d.ts.map +1 -0
  49. package/dist/core/adapter.js +1 -0
  50. package/dist/core/adapters/attest.d.ts +5 -2
  51. package/dist/core/adapters/attest.d.ts.map +1 -1
  52. package/dist/core/adapters/attest.js +8 -3
  53. package/dist/core/adapters/profiles.d.ts +6 -6
  54. package/dist/core/adapters/profiles.d.ts.map +1 -1
  55. package/dist/core/adapters/profiles.js +4 -4
  56. package/dist/core/agents/canonical-emit.d.ts +17 -15
  57. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  58. package/dist/core/agents/canonical-emit.js +22 -22
  59. package/dist/core/agents/cli.js +207 -234
  60. package/dist/core/agents/codex-archive-v3.d.ts +14 -0
  61. package/dist/core/agents/codex-archive-v3.d.ts.map +1 -0
  62. package/dist/core/agents/codex-archive-v3.js +110 -0
  63. package/dist/core/agents/coord-client.d.ts +19 -81
  64. package/dist/core/agents/coord-client.d.ts.map +1 -1
  65. package/dist/core/agents/coord-client.js +65 -155
  66. package/dist/core/agents/finalization.d.ts +4 -1
  67. package/dist/core/agents/finalization.d.ts.map +1 -1
  68. package/dist/core/agents/finalization.js +89 -32
  69. package/dist/core/agents/index.d.ts +4 -5
  70. package/dist/core/agents/index.d.ts.map +1 -1
  71. package/dist/core/agents/index.js +4 -5
  72. package/dist/core/agents/live-authority-v3.d.ts +59 -0
  73. package/dist/core/agents/live-authority-v3.d.ts.map +1 -0
  74. package/dist/core/agents/live-authority-v3.js +233 -0
  75. package/dist/core/agents/live-lifecycle-v3.d.ts +20 -0
  76. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -0
  77. package/dist/core/agents/live-lifecycle-v3.js +91 -0
  78. package/dist/core/agents/live-observation-v3.d.ts +25 -0
  79. package/dist/core/agents/live-observation-v3.d.ts.map +1 -0
  80. package/dist/core/agents/live-observation-v3.js +157 -0
  81. package/dist/core/agents/render/prompt-context.d.ts +4 -4
  82. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  83. package/dist/core/agents/render/prompt-context.js +23 -58
  84. package/dist/core/agents/render/session-context.d.ts +1 -1
  85. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  86. package/dist/core/agents/render/session-context.js +8 -32
  87. package/dist/core/agents/rules/claim-conflict.d.ts +3 -3
  88. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  89. package/dist/core/agents/rules/claim-conflict.js +79 -127
  90. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  91. package/dist/core/agents/rules/commit-conflict.js +3 -18
  92. package/dist/core/agents/rules/stop-hook.d.ts +9 -50
  93. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  94. package/dist/core/agents/rules/stop-hook.js +193 -236
  95. package/dist/core/agents/session-events.d.ts +5 -7
  96. package/dist/core/agents/session-events.d.ts.map +1 -1
  97. package/dist/core/agents/session-events.js +127 -95
  98. package/dist/core/agents/session-finalization-state-v3.d.ts +30 -0
  99. package/dist/core/agents/session-finalization-state-v3.d.ts.map +1 -0
  100. package/dist/core/agents/session-finalization-state-v3.js +40 -0
  101. package/dist/core/agents/session-finalizer-v3.d.ts +107 -0
  102. package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -0
  103. package/dist/core/agents/session-finalizer-v3.js +734 -0
  104. package/dist/core/agents/session-name-display.d.ts +40 -0
  105. package/dist/core/agents/session-name-display.d.ts.map +1 -0
  106. package/dist/core/agents/session-name-display.js +119 -0
  107. package/dist/core/agents/state/activity-log.d.ts +3 -3
  108. package/dist/core/agents/state/activity-log.d.ts.map +1 -1
  109. package/dist/core/agents/state/activity-log.js +7 -16
  110. package/dist/core/agents/state/heartbeat-writer.d.ts +23 -55
  111. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  112. package/dist/core/agents/state/heartbeat-writer.js +56 -228
  113. package/dist/core/agents/state/live-coordination-view.d.ts +31 -0
  114. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -0
  115. package/dist/core/agents/state/live-coordination-view.js +209 -0
  116. package/dist/core/agents/state/session-state.d.ts +4 -3
  117. package/dist/core/agents/state/session-state.d.ts.map +1 -1
  118. package/dist/core/agents/state/session-state.js +18 -9
  119. package/dist/core/agents/state/stale-sweep.d.ts +1 -1
  120. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  121. package/dist/core/agents/state/stale-sweep.js +26 -15
  122. package/dist/core/artifacts/index.d.ts.map +1 -1
  123. package/dist/core/artifacts/index.js +5 -7
  124. package/dist/core/config.d.ts +33 -2
  125. package/dist/core/config.d.ts.map +1 -1
  126. package/dist/core/config.js +98 -2
  127. package/dist/core/context/index.d.ts +1 -4
  128. package/dist/core/context/index.d.ts.map +1 -1
  129. package/dist/core/context/index.js +9 -9
  130. package/dist/core/events/span-parent.d.ts +15 -0
  131. package/dist/core/events/span-parent.d.ts.map +1 -0
  132. package/dist/core/events/span-parent.js +34 -0
  133. package/dist/core/events/v3/advance.d.ts +14 -0
  134. package/dist/core/events/v3/advance.d.ts.map +1 -0
  135. package/dist/core/events/v3/advance.js +134 -0
  136. package/dist/core/events/v3/authority-outbox.d.ts +78 -0
  137. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -0
  138. package/dist/core/events/v3/authority-outbox.js +399 -0
  139. package/dist/core/events/v3/base-builder.d.ts +20 -0
  140. package/dist/core/events/v3/base-builder.d.ts.map +1 -0
  141. package/dist/core/events/v3/base-builder.js +29 -0
  142. package/dist/core/events/v3/base-contract.d.ts +7576 -0
  143. package/dist/core/events/v3/base-contract.d.ts.map +1 -0
  144. package/dist/core/events/v3/base-contract.js +647 -0
  145. package/dist/core/events/v3/bootstrap.d.ts +34 -0
  146. package/dist/core/events/v3/bootstrap.d.ts.map +1 -0
  147. package/dist/core/events/v3/bootstrap.js +175 -0
  148. package/dist/core/events/v3/builder.d.ts +20 -0
  149. package/dist/core/events/v3/builder.d.ts.map +1 -0
  150. package/dist/core/events/v3/builder.js +28 -0
  151. package/dist/core/events/v3/canonical.d.ts +31 -0
  152. package/dist/core/events/v3/canonical.d.ts.map +1 -0
  153. package/dist/core/events/v3/canonical.js +100 -0
  154. package/dist/core/events/v3/capabilities.d.ts +31 -0
  155. package/dist/core/events/v3/capabilities.d.ts.map +1 -0
  156. package/dist/core/events/v3/capabilities.js +107 -0
  157. package/dist/core/events/v3/capability-drift.d.ts +22 -0
  158. package/dist/core/events/v3/capability-drift.d.ts.map +1 -0
  159. package/dist/core/events/v3/capability-drift.js +134 -0
  160. package/dist/core/events/v3/catalog.d.ts +38 -0
  161. package/dist/core/events/v3/catalog.d.ts.map +1 -0
  162. package/dist/core/events/v3/catalog.js +135 -0
  163. package/dist/core/events/v3/clock-order.d.ts +34 -0
  164. package/dist/core/events/v3/clock-order.d.ts.map +1 -0
  165. package/dist/core/events/v3/clock-order.js +35 -0
  166. package/dist/core/events/v3/contract.d.ts +171 -0
  167. package/dist/core/events/v3/contract.d.ts.map +1 -0
  168. package/dist/core/events/v3/contract.js +180 -0
  169. package/dist/core/events/v3/control.d.ts +111 -0
  170. package/dist/core/events/v3/control.d.ts.map +1 -0
  171. package/dist/core/events/v3/control.js +419 -0
  172. package/dist/core/events/v3/coordination-view.d.ts +69 -0
  173. package/dist/core/events/v3/coordination-view.d.ts.map +1 -0
  174. package/dist/core/events/v3/coordination-view.js +119 -0
  175. package/dist/core/events/v3/economics.d.ts +77 -0
  176. package/dist/core/events/v3/economics.d.ts.map +1 -0
  177. package/dist/core/events/v3/economics.js +274 -0
  178. package/dist/core/events/v3/finalization-view.d.ts +26 -0
  179. package/dist/core/events/v3/finalization-view.d.ts.map +1 -0
  180. package/dist/core/events/v3/finalization-view.js +37 -0
  181. package/dist/core/events/v3/fingerprint-keys.d.ts +25 -0
  182. package/dist/core/events/v3/fingerprint-keys.d.ts.map +1 -0
  183. package/dist/core/events/v3/fingerprint-keys.js +217 -0
  184. package/dist/core/events/v3/generated.d.ts +4 -0
  185. package/dist/core/events/v3/generated.d.ts.map +1 -0
  186. package/dist/core/events/v3/generated.js +47 -0
  187. package/dist/core/events/v3/ids.d.ts +9 -0
  188. package/dist/core/events/v3/ids.d.ts.map +1 -0
  189. package/dist/core/events/v3/ids.js +25 -0
  190. package/dist/core/events/v3/index.d.ts +35 -0
  191. package/dist/core/events/v3/index.d.ts.map +1 -0
  192. package/dist/core/events/v3/index.js +33 -0
  193. package/dist/core/events/v3/latency.d.ts +63 -0
  194. package/dist/core/events/v3/latency.d.ts.map +1 -0
  195. package/dist/core/events/v3/latency.js +412 -0
  196. package/dist/core/events/v3/live-feed.d.ts +45 -0
  197. package/dist/core/events/v3/live-feed.d.ts.map +1 -0
  198. package/dist/core/events/v3/live-feed.js +246 -0
  199. package/dist/core/events/v3/live-observation.d.ts +35 -0
  200. package/dist/core/events/v3/live-observation.d.ts.map +1 -0
  201. package/dist/core/events/v3/live-observation.js +1 -0
  202. package/dist/core/events/v3/live-routing.d.ts +57 -0
  203. package/dist/core/events/v3/live-routing.d.ts.map +1 -0
  204. package/dist/core/events/v3/live-routing.js +107 -0
  205. package/dist/core/events/v3/producers/command-recorder.d.ts +67 -0
  206. package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -0
  207. package/dist/core/events/v3/producers/command-recorder.js +477 -0
  208. package/dist/core/events/v3/producers/command.d.ts +50 -0
  209. package/dist/core/events/v3/producers/command.d.ts.map +1 -0
  210. package/dist/core/events/v3/producers/command.js +135 -0
  211. package/dist/core/events/v3/producers/coordination-recorder.d.ts +53 -0
  212. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -0
  213. package/dist/core/events/v3/producers/coordination-recorder.js +376 -0
  214. package/dist/core/events/v3/producers/coordination.d.ts +90 -0
  215. package/dist/core/events/v3/producers/coordination.d.ts.map +1 -0
  216. package/dist/core/events/v3/producers/coordination.js +229 -0
  217. package/dist/core/events/v3/producers/hook-base.d.ts +54 -0
  218. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -0
  219. package/dist/core/events/v3/producers/hook-base.js +424 -0
  220. package/dist/core/events/v3/producers/hook.d.ts +29 -0
  221. package/dist/core/events/v3/producers/hook.d.ts.map +1 -0
  222. package/dist/core/events/v3/producers/hook.js +71 -0
  223. package/dist/core/events/v3/producers/intake.d.ts +65 -0
  224. package/dist/core/events/v3/producers/intake.d.ts.map +1 -0
  225. package/dist/core/events/v3/producers/intake.js +244 -0
  226. package/dist/core/events/v3/producers/recorder.d.ts +240 -0
  227. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -0
  228. package/dist/core/events/v3/producers/recorder.js +2176 -0
  229. package/dist/core/events/v3/projection.d.ts +101 -0
  230. package/dist/core/events/v3/projection.d.ts.map +1 -0
  231. package/dist/core/events/v3/projection.js +479 -0
  232. package/dist/core/events/v3/reader.d.ts +75 -0
  233. package/dist/core/events/v3/reader.d.ts.map +1 -0
  234. package/dist/core/events/v3/reader.js +778 -0
  235. package/dist/core/events/v3/recovery-record.d.ts +47 -0
  236. package/dist/core/events/v3/recovery-record.d.ts.map +1 -0
  237. package/dist/core/events/v3/recovery-record.js +167 -0
  238. package/dist/core/events/v3/recovery.d.ts +22 -0
  239. package/dist/core/events/v3/recovery.d.ts.map +1 -0
  240. package/dist/core/events/v3/recovery.js +227 -0
  241. package/dist/core/events/v3/span-state.d.ts +52 -0
  242. package/dist/core/events/v3/span-state.d.ts.map +1 -0
  243. package/dist/core/events/v3/span-state.js +142 -0
  244. package/dist/core/events/v3/targets.d.ts +23 -0
  245. package/dist/core/events/v3/targets.d.ts.map +1 -0
  246. package/dist/core/events/v3/targets.js +134 -0
  247. package/dist/core/events/v3/turn-telemetry.d.ts +41 -0
  248. package/dist/core/events/v3/turn-telemetry.d.ts.map +1 -0
  249. package/dist/core/events/v3/turn-telemetry.js +168 -0
  250. package/dist/core/events/v3/validate.d.ts +9 -0
  251. package/dist/core/events/v3/validate.d.ts.map +1 -0
  252. package/dist/core/events/v3/validate.js +281 -0
  253. package/dist/core/events/v3/writer.d.ts +56 -0
  254. package/dist/core/events/v3/writer.d.ts.map +1 -0
  255. package/dist/core/events/v3/writer.js +335 -0
  256. package/dist/core/governor/index.d.ts +1 -1
  257. package/dist/core/governor/index.d.ts.map +1 -1
  258. package/dist/core/governor/index.js +1 -1
  259. package/dist/core/governor/plan-api.d.ts +2 -1
  260. package/dist/core/governor/plan-api.d.ts.map +1 -1
  261. package/dist/core/governor/plan-api.js +6 -3
  262. package/dist/core/governor/read.d.ts +1 -1
  263. package/dist/core/governor/read.d.ts.map +1 -1
  264. package/dist/core/governor/read.js +1 -1
  265. package/dist/core/governor/state.d.ts +9 -0
  266. package/dist/core/governor/state.d.ts.map +1 -1
  267. package/dist/core/governor/state.js +40 -3
  268. package/dist/core/guard/config.d.ts +5 -0
  269. package/dist/core/guard/config.d.ts.map +1 -0
  270. package/dist/core/guard/config.js +157 -0
  271. package/dist/core/guard/coordinator.d.ts +14 -0
  272. package/dist/core/guard/coordinator.d.ts.map +1 -0
  273. package/dist/core/guard/coordinator.js +173 -0
  274. package/dist/core/guard/evaluator.d.ts +4 -0
  275. package/dist/core/guard/evaluator.d.ts.map +1 -0
  276. package/dist/core/guard/evaluator.js +242 -0
  277. package/dist/core/guard/evidence-v3.d.ts +15 -0
  278. package/dist/core/guard/evidence-v3.d.ts.map +1 -0
  279. package/dist/core/guard/evidence-v3.js +171 -0
  280. package/dist/core/guard/index.d.ts +10 -0
  281. package/dist/core/guard/index.d.ts.map +1 -0
  282. package/dist/core/guard/index.js +6 -0
  283. package/dist/core/guard/live-source-v3.d.ts +40 -0
  284. package/dist/core/guard/live-source-v3.d.ts.map +1 -0
  285. package/dist/core/guard/live-source-v3.js +128 -0
  286. package/dist/core/guard/storage.d.ts +27 -0
  287. package/dist/core/guard/storage.d.ts.map +1 -0
  288. package/dist/core/guard/storage.js +140 -0
  289. package/dist/core/guard/types.d.ts +119 -0
  290. package/dist/core/guard/types.d.ts.map +1 -0
  291. package/dist/core/guard/types.js +1 -0
  292. package/dist/core/hooks/adapter/detect.d.ts +18 -1
  293. package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
  294. package/dist/core/hooks/adapter/detect.js +29 -0
  295. package/dist/core/hooks/adapter/events.d.ts +7 -4
  296. package/dist/core/hooks/adapter/events.d.ts.map +1 -1
  297. package/dist/core/hooks/adapter/events.js +12 -4
  298. package/dist/core/hooks/adapter/output.d.ts +2 -2
  299. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  300. package/dist/core/hooks/adapter/parse.d.ts +7 -4
  301. package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
  302. package/dist/core/hooks/adapter/parse.js +63 -25
  303. package/dist/core/hooks/adapter/wiring.d.ts +30 -4
  304. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  305. package/dist/core/hooks/adapter/wiring.js +89 -11
  306. package/dist/core/hooks/cli.d.ts +1 -1
  307. package/dist/core/hooks/cli.js +305 -444
  308. package/dist/core/hooks/effects/image-capture.d.ts +20 -34
  309. package/dist/core/hooks/effects/image-capture.d.ts.map +1 -1
  310. package/dist/core/hooks/effects/image-capture.js +212 -221
  311. package/dist/core/hooks/effects/index.d.ts +1 -9
  312. package/dist/core/hooks/effects/index.d.ts.map +1 -1
  313. package/dist/core/hooks/effects/index.js +1 -24
  314. package/dist/core/hooks/events/input-hash.d.ts +7 -0
  315. package/dist/core/hooks/events/input-hash.d.ts.map +1 -0
  316. package/dist/core/hooks/events/input-hash.js +43 -0
  317. package/dist/core/hooks/index.d.ts +4 -3
  318. package/dist/core/hooks/index.d.ts.map +1 -1
  319. package/dist/core/hooks/index.js +2 -3
  320. package/dist/core/hooks/resolve/coord-root.d.ts +1 -1
  321. package/dist/core/hooks/resolve/coord-root.js +1 -1
  322. package/dist/core/hooks/resolve/owner.d.ts +16 -15
  323. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  324. package/dist/core/hooks/resolve/owner.js +51 -42
  325. package/dist/core/hooks/resolve/transcript.d.ts +33 -3
  326. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  327. package/dist/core/hooks/resolve/transcript.js +155 -14
  328. package/dist/core/hooks/session-name-presence.d.ts +9 -10
  329. package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
  330. package/dist/core/hooks/session-name-presence.js +15 -15
  331. package/dist/core/hooks/unsafe-cross-shell.d.ts +1 -1
  332. package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -1
  333. package/dist/core/journal/index.d.ts +2 -2
  334. package/dist/core/journal/index.d.ts.map +1 -1
  335. package/dist/core/journal/index.js +9 -22
  336. package/dist/core/presence/blob.d.ts +4 -3
  337. package/dist/core/presence/blob.d.ts.map +1 -1
  338. package/dist/core/presence/blob.js +45 -44
  339. package/dist/core/presence/codec-digest.d.ts +29 -0
  340. package/dist/core/presence/codec-digest.d.ts.map +1 -0
  341. package/dist/core/presence/codec-digest.js +170 -0
  342. package/dist/core/presence/relay-client.d.ts +1 -1
  343. package/dist/core/presence/relay-client.d.ts.map +1 -1
  344. package/dist/core/presence/relay-client.js +5 -5
  345. package/dist/core/work/index.d.ts +1 -1
  346. package/dist/core/work/index.d.ts.map +1 -1
  347. package/dist/core/work/index.js +1 -1
  348. package/dist/core/work/read.d.ts +1 -1
  349. package/dist/core/work/read.d.ts.map +1 -1
  350. package/dist/core/work/read.js +1 -1
  351. package/dist/core/work/runner.d.ts.map +1 -1
  352. package/dist/core/work/runner.js +6 -0
  353. package/dist/core/work/state.d.ts +9 -0
  354. package/dist/core/work/state.d.ts.map +1 -1
  355. package/dist/core/work/state.js +17 -3
  356. package/dist/core/workflow/engine.js +24 -25
  357. package/dist/core/workflow/live-session-v3.d.ts +19 -0
  358. package/dist/core/workflow/live-session-v3.d.ts.map +1 -0
  359. package/dist/core/workflow/live-session-v3.js +86 -0
  360. package/dist/core/workflow/scope-id.d.ts +3 -0
  361. package/dist/core/workflow/scope-id.d.ts.map +1 -0
  362. package/dist/core/workflow/scope-id.js +5 -0
  363. package/dist/core/workflow/workspaces/leases.d.ts +4 -0
  364. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  365. package/dist/core/workflow/workspaces/leases.js +53 -19
  366. package/dist/index.d.ts +1 -1
  367. package/dist/index.js +1 -1
  368. package/dist/lib/browser/client.d.ts +41 -0
  369. package/dist/lib/browser/client.d.ts.map +1 -1
  370. package/dist/lib/browser/client.js +203 -0
  371. package/dist/lib/browser/critique.d.ts +28 -2
  372. package/dist/lib/browser/critique.d.ts.map +1 -1
  373. package/dist/lib/browser/critique.js +74 -18
  374. package/dist/lib/browser/geometry.d.ts +1 -1
  375. package/dist/lib/browser/geometry.d.ts.map +1 -1
  376. package/dist/lib/browser/geometry.js +130 -6
  377. package/dist/lib/browser/index.d.ts +3 -0
  378. package/dist/lib/browser/index.d.ts.map +1 -1
  379. package/dist/lib/browser/index.js +3 -0
  380. package/dist/lib/browser/qa-plan.d.ts +174 -0
  381. package/dist/lib/browser/qa-plan.d.ts.map +1 -0
  382. package/dist/lib/browser/qa-plan.js +321 -0
  383. package/dist/lib/browser/qa-reuse.d.ts +99 -0
  384. package/dist/lib/browser/qa-reuse.d.ts.map +1 -0
  385. package/dist/lib/browser/qa-reuse.js +222 -0
  386. package/dist/lib/browser/qa-snapshot.d.ts +64 -0
  387. package/dist/lib/browser/qa-snapshot.d.ts.map +1 -0
  388. package/dist/lib/browser/qa-snapshot.js +171 -0
  389. package/dist/lib/browser/runts.d.ts.map +1 -1
  390. package/dist/lib/browser/runts.js +22 -1
  391. package/dist/lib/browser/tiling.d.ts +71 -0
  392. package/dist/lib/browser/tiling.d.ts.map +1 -0
  393. package/dist/lib/browser/tiling.js +118 -0
  394. package/dist/lib/claude-desktop.d.ts +0 -47
  395. package/dist/lib/claude-desktop.d.ts.map +1 -1
  396. package/dist/lib/claude-desktop.js +1 -169
  397. package/dist/lib/context/index.d.ts +0 -3
  398. package/dist/lib/context/index.d.ts.map +1 -1
  399. package/dist/lib/context/index.js +20 -42
  400. package/dist/lib/council/index.d.ts +2 -2
  401. package/dist/lib/council/index.d.ts.map +1 -1
  402. package/dist/lib/council/index.js +9 -20
  403. package/dist/lib/docs-frontmatter.d.ts +1 -6
  404. package/dist/lib/docs-frontmatter.d.ts.map +1 -1
  405. package/dist/lib/docs-frontmatter.js +12 -78
  406. package/dist/lib/docs-index.d.ts +9 -0
  407. package/dist/lib/docs-index.d.ts.map +1 -1
  408. package/dist/lib/docs-index.js +32 -3
  409. package/dist/lib/docs-lint.d.ts.map +1 -1
  410. package/dist/lib/docs-lint.js +14 -30
  411. package/dist/lib/docs-metadata-audit.d.ts +19 -0
  412. package/dist/lib/docs-metadata-audit.d.ts.map +1 -0
  413. package/dist/lib/docs-metadata-audit.js +132 -0
  414. package/dist/lib/docs-metadata-managed.d.ts +23 -0
  415. package/dist/lib/docs-metadata-managed.d.ts.map +1 -0
  416. package/dist/lib/docs-metadata-managed.js +78 -0
  417. package/dist/lib/docs-metadata-sync.d.ts +20 -0
  418. package/dist/lib/docs-metadata-sync.d.ts.map +1 -0
  419. package/dist/lib/docs-metadata-sync.js +222 -0
  420. package/dist/lib/docs-metadata-v2-schema.d.ts +273 -0
  421. package/dist/lib/docs-metadata-v2-schema.d.ts.map +1 -0
  422. package/dist/lib/docs-metadata-v2-schema.js +134 -0
  423. package/dist/lib/docs-metadata-v2.d.ts +21 -0
  424. package/dist/lib/docs-metadata-v2.d.ts.map +1 -0
  425. package/dist/lib/docs-metadata-v2.js +424 -0
  426. package/dist/lib/docs-new.d.ts +11 -0
  427. package/dist/lib/docs-new.d.ts.map +1 -0
  428. package/dist/lib/docs-new.js +52 -0
  429. package/dist/lib/docs-sweep.d.ts.map +1 -1
  430. package/dist/lib/docs-sweep.js +45 -29
  431. package/dist/lib/headless/index.d.ts +98 -0
  432. package/dist/lib/headless/index.d.ts.map +1 -0
  433. package/dist/lib/headless/index.js +211 -0
  434. package/dist/lib/identities/assume.d.ts +4 -4
  435. package/dist/lib/identities/assume.d.ts.map +1 -1
  436. package/dist/lib/identities/assume.js +52 -68
  437. package/dist/lib/instructions/apply.d.ts +5 -4
  438. package/dist/lib/instructions/apply.d.ts.map +1 -1
  439. package/dist/lib/instructions/apply.js +32 -20
  440. package/dist/lib/instructions/bundle.d.ts +1 -1
  441. package/dist/lib/instructions/bundle.d.ts.map +1 -1
  442. package/dist/lib/instructions/bundle.js +1 -1
  443. package/dist/lib/instructions/splice.d.ts +0 -1
  444. package/dist/lib/instructions/splice.d.ts.map +1 -1
  445. package/dist/lib/instructions/splice.js +22 -6
  446. package/dist/lib/instructions/templates.d.ts +7 -5
  447. package/dist/lib/instructions/templates.d.ts.map +1 -1
  448. package/dist/lib/instructions/templates.js +79 -9
  449. package/package.json +26 -5
  450. package/schemas/config.schema.json +120 -2
  451. package/schemas/docs-metadata-v2.schema.json +441 -0
  452. package/schemas/event-v3.schema.json +24472 -0
  453. package/src/commander.ts +4 -0
  454. package/src/commands/adapter.ts +2 -2
  455. package/src/commands/agents.ts +1512 -592
  456. package/src/commands/artifacts.ts +7 -6
  457. package/src/commands/backup.ts +29 -21
  458. package/src/commands/browse.ts +349 -8
  459. package/src/commands/checkpoint.ts +4 -3
  460. package/src/commands/claude-desktop.ts +2 -101
  461. package/src/commands/context.ts +1 -6
  462. package/src/commands/decision.ts +27 -44
  463. package/src/commands/docs.ts +100 -33
  464. package/src/commands/doctor.ts +14 -1
  465. package/src/commands/events.ts +256 -0
  466. package/src/commands/governor.ts +18 -4
  467. package/src/commands/init.ts +253 -57
  468. package/src/commands/instructions.ts +1 -1
  469. package/src/commands/journal.ts +4 -13
  470. package/src/commands/ledger-v3.ts +111 -0
  471. package/src/commands/presence.ts +11 -10
  472. package/src/commands/sync.ts +1 -1
  473. package/src/commands/web.ts +67 -8
  474. package/src/commands/work.ts +28 -5
  475. package/src/core/adapter.ts +5 -0
  476. package/src/core/adapters/attest.ts +8 -3
  477. package/src/core/adapters/profiles.ts +4 -6
  478. package/src/core/agents/canonical-emit.ts +34 -25
  479. package/src/core/agents/cli.ts +236 -262
  480. package/src/core/agents/codex-archive-v3.ts +138 -0
  481. package/src/core/agents/coord-client.ts +75 -188
  482. package/src/core/agents/finalization.ts +120 -38
  483. package/src/core/agents/index.ts +4 -5
  484. package/src/core/agents/live-authority-v3.ts +375 -0
  485. package/src/core/agents/live-lifecycle-v3.ts +140 -0
  486. package/src/core/agents/live-observation-v3.ts +216 -0
  487. package/src/core/agents/render/prompt-context.ts +29 -60
  488. package/src/core/agents/render/session-context.ts +10 -31
  489. package/src/core/agents/rules/claim-conflict.ts +85 -126
  490. package/src/core/agents/rules/commit-conflict.ts +3 -14
  491. package/src/core/agents/rules/stop-hook.ts +239 -293
  492. package/src/core/agents/session-events.ts +137 -104
  493. package/src/core/agents/session-finalization-state-v3.ts +90 -0
  494. package/src/core/agents/session-finalizer-v3.ts +953 -0
  495. package/src/core/agents/session-name-display.ts +149 -0
  496. package/src/core/agents/state/activity-log.ts +6 -14
  497. package/src/core/agents/state/heartbeat-writer.ts +82 -277
  498. package/src/core/agents/state/live-coordination-view.ts +290 -0
  499. package/src/core/agents/state/session-state.ts +24 -12
  500. package/src/core/agents/state/stale-sweep.ts +40 -17
  501. package/src/core/artifacts/index.ts +4 -5
  502. package/src/core/config.ts +148 -3
  503. package/src/core/context/index.ts +9 -17
  504. package/src/core/events/span-parent.ts +46 -0
  505. package/src/core/events/v3/advance.ts +168 -0
  506. package/src/core/events/v3/authority-outbox.ts +548 -0
  507. package/src/core/events/v3/base-builder.ts +60 -0
  508. package/src/core/events/v3/base-contract.ts +882 -0
  509. package/src/core/events/v3/bootstrap.ts +226 -0
  510. package/src/core/events/v3/builder.ts +59 -0
  511. package/src/core/events/v3/canonical.ts +153 -0
  512. package/src/core/events/v3/capabilities.ts +171 -0
  513. package/src/core/events/v3/capability-drift.ts +177 -0
  514. package/src/core/events/v3/catalog.ts +202 -0
  515. package/src/core/events/v3/clock-order.ts +41 -0
  516. package/src/core/events/v3/contract.ts +365 -0
  517. package/src/core/events/v3/control.ts +576 -0
  518. package/src/core/events/v3/coordination-view.ts +193 -0
  519. package/src/core/events/v3/economics.ts +384 -0
  520. package/src/core/events/v3/finalization-view.ts +67 -0
  521. package/src/core/events/v3/fingerprint-keys.ts +260 -0
  522. package/src/core/events/v3/generated.ts +48 -0
  523. package/src/core/events/v3/ids.ts +33 -0
  524. package/src/core/events/v3/index.ts +322 -0
  525. package/src/core/events/v3/latency.ts +551 -0
  526. package/src/core/events/v3/live-feed.ts +319 -0
  527. package/src/core/events/v3/live-observation.ts +39 -0
  528. package/src/core/events/v3/live-routing.ts +135 -0
  529. package/src/core/events/v3/producers/command-recorder.ts +631 -0
  530. package/src/core/events/v3/producers/command.ts +201 -0
  531. package/src/core/events/v3/producers/coordination-recorder.ts +538 -0
  532. package/src/core/events/v3/producers/coordination.ts +355 -0
  533. package/src/core/events/v3/producers/hook-base.ts +578 -0
  534. package/src/core/events/v3/producers/hook.ts +119 -0
  535. package/src/core/events/v3/producers/intake.ts +319 -0
  536. package/src/core/events/v3/producers/recorder.ts +2805 -0
  537. package/src/core/events/v3/projection.ts +653 -0
  538. package/src/core/events/v3/reader.ts +1127 -0
  539. package/src/core/events/v3/recovery-record.ts +244 -0
  540. package/src/core/events/v3/recovery.ts +271 -0
  541. package/src/core/events/v3/span-state.ts +187 -0
  542. package/src/core/events/v3/targets.ts +182 -0
  543. package/src/core/events/v3/turn-telemetry.ts +259 -0
  544. package/src/core/events/v3/validate.ts +361 -0
  545. package/src/core/events/v3/writer.ts +419 -0
  546. package/src/core/governor/index.ts +3 -0
  547. package/src/core/governor/plan-api.ts +10 -2
  548. package/src/core/governor/read.ts +3 -0
  549. package/src/core/governor/state.ts +52 -5
  550. package/src/core/guard/config.ts +206 -0
  551. package/src/core/guard/coordinator.ts +227 -0
  552. package/src/core/guard/evaluator.ts +343 -0
  553. package/src/core/guard/evidence-v3.ts +227 -0
  554. package/src/core/guard/index.ts +35 -0
  555. package/src/core/guard/live-source-v3.ts +204 -0
  556. package/src/core/guard/storage.ts +188 -0
  557. package/src/core/guard/types.ts +152 -0
  558. package/src/core/hooks/adapter/detect.ts +28 -1
  559. package/src/core/hooks/adapter/events.ts +14 -6
  560. package/src/core/hooks/adapter/output.ts +7 -2
  561. package/src/core/hooks/adapter/parse.ts +80 -38
  562. package/src/core/hooks/adapter/wiring.ts +119 -13
  563. package/src/core/hooks/cli.ts +351 -466
  564. package/src/core/hooks/effects/image-capture.ts +228 -238
  565. package/src/core/hooks/effects/index.ts +1 -36
  566. package/src/core/hooks/events/input-hash.ts +47 -0
  567. package/src/core/hooks/index.ts +4 -11
  568. package/src/core/hooks/resolve/coord-root.ts +1 -1
  569. package/src/core/hooks/resolve/owner.ts +52 -45
  570. package/src/core/hooks/resolve/transcript.ts +165 -14
  571. package/src/core/hooks/session-name-presence.ts +14 -15
  572. package/src/core/hooks/unsafe-cross-shell.ts +1 -1
  573. package/src/core/journal/index.ts +15 -22
  574. package/src/core/presence/blob.ts +48 -41
  575. package/src/core/presence/codec-digest.ts +215 -0
  576. package/src/core/presence/relay-client.ts +5 -5
  577. package/src/core/work/index.ts +3 -0
  578. package/src/core/work/read.ts +3 -0
  579. package/src/core/work/runner.ts +6 -0
  580. package/src/core/work/state.ts +27 -3
  581. package/src/core/workflow/engine.ts +24 -24
  582. package/src/core/workflow/live-session-v3.ts +126 -0
  583. package/src/core/workflow/scope-id.ts +6 -0
  584. package/src/core/workflow/workspaces/leases.ts +56 -19
  585. package/src/index.ts +1 -1
  586. package/src/lib/browser/client.ts +230 -0
  587. package/src/lib/browser/critique.ts +104 -27
  588. package/src/lib/browser/geometry.ts +143 -14
  589. package/src/lib/browser/index.ts +34 -0
  590. package/src/lib/browser/qa-plan.ts +501 -0
  591. package/src/lib/browser/qa-reuse.ts +306 -0
  592. package/src/lib/browser/qa-snapshot.ts +223 -0
  593. package/src/lib/browser/runts.ts +27 -2
  594. package/src/lib/browser/tiling.ts +164 -0
  595. package/src/lib/claude-desktop.ts +1 -232
  596. package/src/lib/context/index.ts +22 -46
  597. package/src/lib/council/index.ts +9 -21
  598. package/src/lib/docs-frontmatter.ts +8 -79
  599. package/src/lib/docs-index.ts +33 -3
  600. package/src/lib/docs-lint.ts +15 -29
  601. package/src/lib/docs-metadata-audit.ts +166 -0
  602. package/src/lib/docs-metadata-managed.ts +83 -0
  603. package/src/lib/docs-metadata-sync.ts +262 -0
  604. package/src/lib/docs-metadata-v2-schema.ts +138 -0
  605. package/src/lib/docs-metadata-v2.ts +646 -0
  606. package/src/lib/docs-new.ts +63 -0
  607. package/src/lib/docs-sweep.ts +42 -28
  608. package/src/lib/headless/index.ts +287 -0
  609. package/src/lib/identities/assume.ts +67 -86
  610. package/src/lib/instructions/apply.ts +33 -20
  611. package/src/lib/instructions/bundle.ts +2 -2
  612. package/src/lib/instructions/splice.ts +21 -7
  613. package/src/lib/instructions/templates.ts +85 -13
  614. package/dist/core/agents/cli-emit.d.ts +0 -27
  615. package/dist/core/agents/cli-emit.d.ts.map +0 -1
  616. package/dist/core/agents/cli-emit.js +0 -57
  617. package/dist/core/agents/codex-replay.d.ts +0 -29
  618. package/dist/core/agents/codex-replay.d.ts.map +0 -1
  619. package/dist/core/agents/codex-replay.js +0 -138
  620. package/dist/core/agents/events/consume.d.ts +0 -82
  621. package/dist/core/agents/events/consume.d.ts.map +0 -1
  622. package/dist/core/agents/events/consume.js +0 -195
  623. package/dist/core/agents/events/emit.d.ts +0 -42
  624. package/dist/core/agents/events/emit.d.ts.map +0 -1
  625. package/dist/core/agents/events/emit.js +0 -75
  626. package/dist/core/agents/events/ulid.d.ts +0 -11
  627. package/dist/core/agents/events/ulid.d.ts.map +0 -1
  628. package/dist/core/agents/events/ulid.js +0 -47
  629. package/dist/core/agents/state/heartbeat-projector.d.ts +0 -76
  630. package/dist/core/agents/state/heartbeat-projector.d.ts.map +0 -1
  631. package/dist/core/agents/state/heartbeat-projector.js +0 -581
  632. package/dist/core/hooks/events/emit.d.ts +0 -31
  633. package/dist/core/hooks/events/emit.d.ts.map +0 -1
  634. package/dist/core/hooks/events/emit.js +0 -93
  635. package/dist/core/hooks/events/rotate.d.ts +0 -43
  636. package/dist/core/hooks/events/rotate.d.ts.map +0 -1
  637. package/dist/core/hooks/events/rotate.js +0 -142
  638. package/dist/core/hooks/events/schema.d.ts +0 -341
  639. package/dist/core/hooks/events/schema.d.ts.map +0 -1
  640. package/dist/core/hooks/events/schema.js +0 -12
  641. package/dist/core/hooks/events/ulid.d.ts +0 -10
  642. package/dist/core/hooks/events/ulid.d.ts.map +0 -1
  643. package/dist/core/hooks/events/ulid.js +0 -47
  644. package/dist/lib/docs-frontmatter-migrate.d.ts +0 -33
  645. package/dist/lib/docs-frontmatter-migrate.d.ts.map +0 -1
  646. package/dist/lib/docs-frontmatter-migrate.js +0 -364
  647. package/src/core/agents/cli-emit.ts +0 -64
  648. package/src/core/agents/codex-replay.ts +0 -163
  649. package/src/core/agents/events/consume.ts +0 -254
  650. package/src/core/agents/events/emit.ts +0 -115
  651. package/src/core/agents/events/ulid.ts +0 -51
  652. package/src/core/agents/state/heartbeat-projector.ts +0 -645
  653. package/src/core/hooks/events/emit.ts +0 -125
  654. package/src/core/hooks/events/rotate.ts +0 -151
  655. package/src/core/hooks/events/schema.ts +0 -582
  656. package/src/core/hooks/events/ulid.ts +0 -51
  657. package/src/lib/docs-frontmatter-migrate.ts +0 -427
@@ -4,7 +4,7 @@
4
4
  * harn agents whoami current agent's name + instance_id + claims
5
5
  * harn agents list all active agents (default: fold transients)
6
6
  * harn agents list --all include raw kind=transient rows
7
- * harn agents list --stale include heartbeats older than the freshness window
7
+ * harn agents list --stale include generations older than the freshness window
8
8
  * harn agents list --json JSON output (alias for --format json)
9
9
  * harn agents status end-of-turn status box (name + age + files + peers)
10
10
  * harn agents heal-events PIDMAP_HEAL telemetry (pid-map self-heal frequency)
@@ -17,18 +17,32 @@ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync,
17
17
  import { homedir, tmpdir } from "node:os";
18
18
  import { basename, join, resolve } from "node:path";
19
19
  import { coordBinPath } from "../core/agents/coord-bin.js";
20
- import { readStreamTailBounded } from "../core/agents/events/consume.js";
21
20
  import { checkGitFinalization, formatGitFinalizationFailure, readSessionWriteClaims, } from "../core/agents/finalization.js";
22
- import { emitCanonical, monorepoRoot, normalizeAdapter, readHeartbeat, resolveOwner, resolveOwnerBySessionEnv, resolveOwnerWithSource, } from "../core/agents/index.js";
23
- import { buildLifecycleSuggestedName, buildSuggestedName, } from "../core/agents/state/heartbeat-writer.js";
21
+ import { emitEventV3, monorepoRoot, normalizeAdapter, resolveOwner, resolveOwnerBySessionEnv, resolveOwnerWithSource, sessionIdentityFromEnv, } from "../core/agents/index.js";
22
+ import { reopenLiveCoordinationGenerationV3 } from "../core/agents/live-authority-v3.js";
23
+ import { listSessionFinalizationRequestsV3, observeHostDisappearedV3, reconcileSessionFinalizationV3, requestSessionEndExplicitV3, } from "../core/agents/session-finalizer-v3.js";
24
+ import { SESSION_NAME_DISPLAY_NOTE } from "../core/agents/session-name-display.js";
25
+ import { buildLifecycleSuggestedName, buildSuggestedName, readHeartbeat as readHeartbeatCache, } from "../core/agents/state/heartbeat-writer.js";
26
+ import { readLiveCoordinationRow, readLiveCoordinationRows, } from "../core/agents/state/live-coordination-view.js";
24
27
  import { foldSessionState, } from "../core/agents/state/session-state.js";
25
- import { coordFreshnessSeconds, resolveBinName } from "../core/config.js";
28
+ import { coordFreshnessSeconds, resolveBinName, sessionFinalizationConfig, } from "../core/config.js";
29
+ import { readEventV3ControlState } from "../core/events/v3/control.js";
30
+ import { projectCoordinationViewV3 } from "../core/events/v3/coordination-view.js";
31
+ import { liveInstanceIdV3 } from "../core/events/v3/live-routing.js";
32
+ import { listHookIntakeGroupsV3, listHookIntakeRecordsV3, } from "../core/events/v3/producers/intake.js";
33
+ import { listHookProducerStateRecordsV3, readHookProducerStateV3, } from "../core/events/v3/producers/recorder.js";
34
+ import { readLedgerV3 } from "../core/events/v3/reader.js";
35
+ import { EVENT_V3_LEDGER_RELATIVE_ROOT } from "../core/events/v3/writer.js";
36
+ import { evaluateRunQualityIfDue } from "../core/guard/index.js";
26
37
  import { readRemoteMachines } from "../core/presence/index.js";
27
38
  import { registerContextCommand } from "./context.js";
39
+ function readCurrentCoordinationRow(instanceId) {
40
+ const root = monorepoRoot();
41
+ return root ? readLiveCoordinationRow(root, instanceId) : null;
42
+ }
28
43
  /** Cap for CLI scans of the unbounded event ledger (`trace` / `health`). Well
29
44
  * under V8's ~512MB max string length so a `readFileSync` of the whole file can
30
45
  * never throw; covers ample recent history for a diagnostic scan. */
31
- const STREAM_SCAN_CAP_BYTES = 128 * 1024 * 1024; // 128 MiB
32
46
  import { parsePsChainLine } from "../core/hooks/resolve/anchor.js";
33
47
  import { appendEntry, resolveOwnerByName } from "../core/journal/index.js";
34
48
  import { buildCouncilId, buildInviteMarkdown, COUNCIL_SCHEMA_VERSION, contributorsInRound, councilBodyDir, councilsArchiveDir, deleteArchivedCouncil, effectiveSteward, findManifestByPartialId, listKnownAgents, listManifests, moveFromArchive, moveToArchive, normalizeAgentName, pendingCouncilsForMember, readArchivedManifest, readManifest, readRoundPrompts, roundDir, setCouncilSteward, writeContribution, writeManifest, writePrompt, } from "../lib/council/index.js";
@@ -43,7 +57,7 @@ import { displayName as displayAgentName, ensureIdentity, listIdentities, lookup
43
57
  function freshnessCutoffSecs() {
44
58
  return coordFreshnessSeconds();
45
59
  }
46
- const SUBAGENT_NOTE = "Bash identity is process-level in v1; if you're running inside a subagent, " +
60
+ const SUBAGENT_NOTE = "Bash identity is process-level; if you're running inside a subagent, " +
47
61
  "this resolves to the parent group's name, not the subagent's. A subagent-aware " +
48
62
  "bridge (per-shell marker file at .harnery/shells/<pid>) is out of scope.";
49
63
  /**
@@ -108,6 +122,18 @@ function activityOf(hb) {
108
122
  function taskStateOf(hb) {
109
123
  return hb.task_state ?? "active";
110
124
  }
125
+ /** Producer joins use the private native session ID; `session_id` is the canonical V3 fingerprint. */
126
+ export function nativeSessionIdentity(row, fallback) {
127
+ if (row?.native_session_id)
128
+ return row.native_session_id;
129
+ // A projection-only row carries the privacy-safe canonical session
130
+ // fingerprint, not the adapter's native session ID needed to join the hook
131
+ // producer. Until a disposable cache exists, the resolved owner is the only
132
+ // native identity available to command surfaces such as `agents status`.
133
+ if (row?.session_id && !/^sid_[a-f0-9]{64}$/.test(row.session_id))
134
+ return row.session_id;
135
+ return fallback;
136
+ }
111
137
  function lifecycleLabel(hb) {
112
138
  const state = taskStateOf(hb);
113
139
  return state === "blocked" && hb.task_state_reason ? `${state}: ${hb.task_state_reason}` : state;
@@ -130,7 +156,7 @@ export function registerAgentsCommand(program, emitParam, programContext) {
130
156
  .command("list")
131
157
  .description("List all active agents (folds kind=transient by default)")
132
158
  .option("--all", "Include raw kind=transient rows (no fold)")
133
- .option("--stale", "Include heartbeats older than the freshness window")
159
+ .option("--stale", "Include generations older than the freshness window")
134
160
  .option("--json", "JSON output (alias for --format json)")
135
161
  .action((opts) => {
136
162
  runList(opts);
@@ -140,7 +166,8 @@ export function registerAgentsCommand(program, emitParam, programContext) {
140
166
  .description("End-of-turn status box (name + session age + files held + peer count)")
141
167
  .option("--json", "JSON output instead of the box")
142
168
  .option("--end-turn", "Treat this as the turn's closing status: issue the box only when this session's held paths are committed and their repositories are pushed")
143
- .option("--session-id <id>", "Lookup heartbeat by session_id directly, bypassing the ppid walk. " +
169
+ .option("--end-session", "After the closing status, durably request an authoritative session end as soon as this exact turn closes (requires --end-turn)")
170
+ .option("--session-id <id>", "Lookup the V3 generation by session_id directly, bypassing the ppid walk. " +
144
171
  "Use this when calling from a hook (the hook's process tree may not lead back to Claude Code's session pid). " +
145
172
  "The Stop hook payload includes session_id; pass it through.")
146
173
  .action((opts) => {
@@ -153,13 +180,13 @@ export function registerAgentsCommand(program, emitParam, programContext) {
153
180
  "is set-task (it suggests a name on the first focus declaration); reach for this to reprint " +
154
181
  "or re-suggest after a topic pivot. Read-only.")
155
182
  .option("--json", "JSON output instead of the bare name")
156
- .option("--session-id <id>", "Lookup heartbeat by session_id directly, bypassing the ppid walk.")
183
+ .option("--session-id <id>", "Lookup the V3 generation by session_id directly, bypassing the ppid walk.")
157
184
  .action((description, opts) => {
158
185
  runSuggestName(description, opts);
159
186
  });
160
187
  cmd
161
188
  .command("watch")
162
- .description("Stream peer state changes in real time (file watcher on .harnery/active/). " +
189
+ .description("Stream peer state changes from the authoritative V3 coordination projection. " +
163
190
  "Prints one line per delta: started / ended / activity / file claim / task change.")
164
191
  .option("--poll-ms <n>", "Debounce window after a change event", "200")
165
192
  .action(async (opts) => {
@@ -175,14 +202,14 @@ export function registerAgentsCommand(program, emitParam, programContext) {
175
202
  });
176
203
  cmd
177
204
  .command("trace <name>")
178
- .description("Reconstruct one agent's coordination lifecycle from events.ndjson: " +
179
- "session.start → prompts → turns → tools → heals/sweeps → claims → end, " +
205
+ .description("Reconstruct one agent's coordination lifecycle from the active canonical event ledger: " +
206
+ "session.started → prompts → turns → tools → observations → session.ended, " +
180
207
  "in chronological order. The answer to 'what happened to this agent / why did " +
181
208
  "it vanish?' without hand-grepping the stream. Accepts a name (agent-Foo or Foo) " +
182
209
  "or an instance_id.")
183
210
  .option("--since <window>", "Only events newer than Nh|Nd (default: all)")
184
211
  .option("--limit <n>", "Show at most N most-recent events. Default: 200.", "200")
185
- .option("--all-tools", "Include tool.post_use + command.* (default: hidden as noise)")
212
+ .option("--all-tools", "Include tool.completed + command.* (default: hidden as noise)")
186
213
  .option("--json", "JSON envelope output")
187
214
  .action((name, opts) => {
188
215
  runTrace(name, opts);
@@ -203,6 +230,41 @@ export function registerAgentsCommand(program, emitParam, programContext) {
203
230
  .action((state, opts) => {
204
231
  runLifecycle(state, opts);
205
232
  });
233
+ cmd
234
+ .command("end")
235
+ .description("Finalize the current V3 session explicitly, or durably queue finalization until the current turn and tool spans close.")
236
+ .option("--session-id <id>", "Native adapter session id to finalize")
237
+ .option("--instance-id <id>", "Canonical V3 instance id to finalize")
238
+ .option("--outcome <outcome>", "succeeded | failed | cancelled | timed_out | denied | interrupted | unknown", "succeeded")
239
+ .action((opts) => {
240
+ runEndSession(opts);
241
+ });
242
+ cmd
243
+ .command("reconcile")
244
+ .description("Reconcile archive, idle, parent/run completion, stale, superseded, and host lifecycle signals into V3 session finalization.")
245
+ .option("--watch", "Keep reconciling until interrupted")
246
+ .option("--interval-seconds <n>", "Watch interval in seconds")
247
+ .option("--json", "JSON output")
248
+ .action(async (opts) => {
249
+ await runSessionReconcile(opts);
250
+ });
251
+ cmd
252
+ .command("observe-archive")
253
+ .description("Record an adapter archive or unarchive observation and reconcile it through the canonical V3 finalizer.")
254
+ .requiredOption("--adapter <id>", "claude-code | codex | cursor")
255
+ .requiredOption("--session-id <id>", "Native adapter session id")
256
+ .option("--unarchived", "Cancel a pending archive finalization")
257
+ .option("--observed-at <iso>", "Observation time (defaults to now)")
258
+ .action((opts) => runObserveArchive(opts));
259
+ cmd
260
+ .command("observe-host-disappeared")
261
+ .description("Record a provisional host-loss observation; finalization follows only after the configured cascade grace period.")
262
+ .requiredOption("--instance-id <id>", "Canonical V3 instance id")
263
+ .requiredOption("--generation-id <id>", "Canonical V3 generation id")
264
+ .option("--observed-at <iso>", "Observation time (defaults to now)")
265
+ .action((opts) => {
266
+ runObserveHostDisappeared(opts);
267
+ });
206
268
  cmd
207
269
  .command("release-claim <path>")
208
270
  .description("Drop a file claim from your heartbeat. Operator escape hatch when a " +
@@ -269,15 +331,13 @@ export function registerAgentsCommand(program, emitParam, programContext) {
269
331
  cmd
270
332
  .command("heal")
271
333
  .description("Force a coord-layer recovery action on a specific agent. " +
272
- "Kinds: pidmap (force PIDMAP_HEAL), heartbeat (force HEARTBEAT_HEAL), " +
273
- "kill (rm the heartbeat file). Runs through the same heartbeat flock " +
274
- "the live hooks use, so it governs the operation safely.")
334
+ "Kinds: pidmap (repair process attribution) and cache (rebuild the " +
335
+ "disposable V3 coordination cache from the authoritative ledger).")
275
336
  .requiredOption("--owner <id>", "Target agent's instance_id")
276
- .requiredOption("--kind <kind>", "pidmap | heartbeat | kill")
277
- .option("--session-id <id>", "(--kind heartbeat) session_id to stamp on the heartbeat. " +
278
- "Default: inherit from existing heartbeat if one exists. " +
279
- "Required when no heartbeat exists yet (a heartbeat without " +
280
- "session_id fails schema validation and pollutes the audit trail).")
337
+ .requiredOption("--kind <kind>", "pidmap | cache")
338
+ .option("--session-id <id>", "(--kind cache) native session id used to join the authoritative V3 generation.")
339
+ .option("--adapter <id>", "(--kind cache) adapter used to validate the authoritative V3 generation: " +
340
+ "claude-code | cursor | codex.")
281
341
  .option("--pid <pid>", "(--kind pidmap) PID to register in pid-map. Default: walk " +
282
342
  "this shell's ppid chain for a claude process. Pass explicitly " +
283
343
  "when calling from outside Claude Code's Bash tool tree (e.g. " +
@@ -288,6 +348,162 @@ export function registerAgentsCommand(program, emitParam, programContext) {
288
348
  });
289
349
  registerIdentityCommands(cmd);
290
350
  }
351
+ const SESSION_OUTCOMES = new Set([
352
+ "succeeded",
353
+ "failed",
354
+ "cancelled",
355
+ "timed_out",
356
+ "denied",
357
+ "interrupted",
358
+ "unknown",
359
+ ]);
360
+ function runEndSession(opts) {
361
+ const root = monorepoRoot();
362
+ if (!root)
363
+ return failCommand("not_in_repo", "not in an agent session");
364
+ if (!SESSION_OUTCOMES.has(opts.outcome)) {
365
+ return failCommand("invalid_outcome", "invalid session outcome");
366
+ }
367
+ const target = opts.instanceId ?? opts.sessionId ?? resolveOwner();
368
+ if (!target) {
369
+ return failCommand("session_identity_missing", "could not resolve the current session; pass --session-id or --instance-id");
370
+ }
371
+ const byInstance = listHookProducerStateRecordsV3(root, { includeTerminal: false }).filter(({ state }) => state.instance_id === target);
372
+ const byNative = ["claude-code", "codex", "cursor"].flatMap((adapter) => {
373
+ const state = readHookProducerStateV3(root, adapter, target);
374
+ return state && !state.terminal ? [{ path: "", modified_at_ms: 0, state }] : [];
375
+ });
376
+ const matches = byInstance.length > 0 ? byInstance : byNative;
377
+ if (matches.length !== 1) {
378
+ return failCommand("session_identity_ambiguous", `expected one live V3 generation for the target; found ${matches.length}`);
379
+ }
380
+ const record = matches[0];
381
+ if (!record) {
382
+ return failCommand("session_identity_missing", "live V3 generation disappeared");
383
+ }
384
+ const state = record.state;
385
+ if (state.delegations.length > 0) {
386
+ return failCommand("session_work_open", `cannot finalize while ${state.delegations.length} delegated child(ren) remain open`);
387
+ }
388
+ const history = readSessionWriteClaims(root, state.instance_id, state.session_id);
389
+ const finalized = checkGitFinalization(root, history.paths, {
390
+ claimHistoryComplete: history.complete,
391
+ });
392
+ if (!finalized.ok) {
393
+ return failCommand("git_not_finalized", formatGitFinalizationFailure(finalized, resolveBinName(root)));
394
+ }
395
+ const result = requestSessionEndExplicitV3({
396
+ coordRoot: root,
397
+ instance_id: state.instance_id,
398
+ generation_id: state.generation_id,
399
+ outcome: opts.outcome,
400
+ coordination_finalized: true,
401
+ });
402
+ if (result.state !== "recorded" &&
403
+ result.state !== "already_ended" &&
404
+ result.state !== "queued" &&
405
+ result.state !== "already_requested") {
406
+ return failCommand("session_end_failed", JSON.stringify(result));
407
+ }
408
+ const terminalEventId = result.state === "recorded"
409
+ ? result.event.event_id
410
+ : result.state === "already_ended"
411
+ ? result.event_id
412
+ : undefined;
413
+ const requestId = result.state === "queued" || result.state === "already_requested"
414
+ ? result.request.request_id
415
+ : undefined;
416
+ emit.data({
417
+ ok: true,
418
+ state: result.state,
419
+ instance_id: state.instance_id,
420
+ generation_id: state.generation_id,
421
+ ...(terminalEventId ? { terminal_event_id: terminalEventId } : {}),
422
+ ...(requestId ? { request_id: requestId } : {}),
423
+ authority: "approved",
424
+ reason: "approved_explicit_end",
425
+ });
426
+ }
427
+ async function runSessionReconcile(opts) {
428
+ const root = monorepoRoot();
429
+ if (!root)
430
+ return failCommand("not_in_repo", "not in an agent session");
431
+ if (opts.json)
432
+ emit.config({ format: "json" });
433
+ const interval = opts.intervalSeconds
434
+ ? Number.parseInt(opts.intervalSeconds, 10)
435
+ : sessionFinalizationConfig(root).reconcileIntervalSeconds;
436
+ if (!Number.isSafeInteger(interval) || interval < 1) {
437
+ return failCommand("invalid_interval", "interval-seconds must be a positive integer");
438
+ }
439
+ let stopped = false;
440
+ do {
441
+ emit.data(reconcileSessionFinalizationV3(root));
442
+ if (!opts.watch || stopped)
443
+ return;
444
+ await new Promise((resolvePromise) => {
445
+ const timer = setTimeout(resolvePromise, interval * 1_000);
446
+ const stop = () => {
447
+ stopped = true;
448
+ clearTimeout(timer);
449
+ process.off("SIGINT", stop);
450
+ process.off("SIGTERM", stop);
451
+ resolvePromise();
452
+ };
453
+ process.once("SIGINT", stop);
454
+ process.once("SIGTERM", stop);
455
+ });
456
+ } while (opts.watch && !stopped);
457
+ }
458
+ function runObserveArchive(opts) {
459
+ const root = monorepoRoot();
460
+ if (!root)
461
+ return failCommand("not_in_repo", "not in an agent session");
462
+ if (!["claude-code", "codex", "cursor"].includes(opts.adapter)) {
463
+ return failCommand("invalid_adapter", "adapter must be claude-code, codex, or cursor");
464
+ }
465
+ const adapter = opts.adapter;
466
+ const observedAt = opts.observedAt ?? new Date().toISOString();
467
+ if (Number.isNaN(Date.parse(observedAt))) {
468
+ return failCommand("invalid_observed_at", "observed-at must be an ISO timestamp");
469
+ }
470
+ emit.data(reconcileSessionFinalizationV3(root, {
471
+ now: new Date(observedAt),
472
+ archive_observations: [
473
+ {
474
+ adapter,
475
+ native_session_id: opts.sessionId,
476
+ archived: !opts.unarchived,
477
+ observed_at: observedAt,
478
+ },
479
+ ],
480
+ }));
481
+ }
482
+ function runObserveHostDisappeared(opts) {
483
+ const root = monorepoRoot();
484
+ if (!root)
485
+ return failCommand("not_in_repo", "not in an agent session");
486
+ if (!/^inst_[A-Za-z0-9._-]+$/.test(opts.instanceId)) {
487
+ return failCommand("invalid_instance_id", "invalid V3 instance id");
488
+ }
489
+ if (!/^gen_[A-Za-z0-9._-]+$/.test(opts.generationId)) {
490
+ return failCommand("invalid_generation_id", "invalid V3 generation id");
491
+ }
492
+ const observedAt = opts.observedAt ?? new Date().toISOString();
493
+ if (Number.isNaN(Date.parse(observedAt))) {
494
+ return failCommand("invalid_observed_at", "observed-at must be an ISO timestamp");
495
+ }
496
+ emit.data(observeHostDisappearedV3({
497
+ coordRoot: root,
498
+ instance_id: opts.instanceId,
499
+ generation_id: opts.generationId,
500
+ observed_at: observedAt,
501
+ }));
502
+ }
503
+ function failCommand(code, message) {
504
+ emit.error({ code, message });
505
+ process.exitCode = 1;
506
+ }
291
507
  function registerIdentityCommands(parent) {
292
508
  const identity = parent
293
509
  .command("identity")
@@ -505,7 +721,7 @@ export function registerCouncilCommands(parent) {
505
721
  "archive it first (trash-can pattern). Without --yes this prints " +
506
722
  "the paths that would be removed and exits 0 without touching " +
507
723
  "anything. Does NOT touch target_doc, close_handoff_path, or " +
508
- "session-events.ndjson, which are owned by separate authors.")
724
+ "the canonical V3 event ledger, which is owned independently.")
509
725
  .option("-y, --yes", "Required to actually delete; without this, dry-run")
510
726
  .option("--json", "JSON envelope output")
511
727
  .action((id, opts) => {
@@ -596,11 +812,11 @@ function runWhoami(opts) {
596
812
  });
597
813
  process.exit(1);
598
814
  }
599
- const hb = readHeartbeat(myOwner);
815
+ const hb = readLiveCoordinationRow(root, myOwner);
600
816
  if (!hb) {
601
817
  emit.error({
602
- code: "no_heartbeat",
603
- message: noHeartbeatMessage(myOwner),
818
+ code: "no_live_generation",
819
+ message: noLiveGenerationMessage(myOwner),
604
820
  });
605
821
  process.exit(1);
606
822
  }
@@ -611,20 +827,17 @@ function runWhoami(opts) {
611
827
  session_id: hb.session_id,
612
828
  kind: normalizeKind(hb.kind),
613
829
  relation: "self",
614
- started_at: hb.started_at,
830
+ started_at: hb.started_at ?? hb.last_heartbeat,
615
831
  last_heartbeat: hb.last_heartbeat,
616
832
  files_touched: hb.files_touched ?? [],
617
- last_tool: hb.last_tool ?? null,
618
- last_tool_target: hb.last_tool_target ?? null,
619
833
  task: hb.task ?? null,
620
834
  activity: activityOf(hb),
621
835
  activity_updated_at: hb.activity_updated_at ?? null,
622
836
  activity_source: hb.activity_source ?? null,
623
837
  task_state: taskStateOf(hb),
838
+ task_state_scope: "current",
624
839
  task_state_updated_at: hb.task_state_updated_at ?? null,
625
840
  task_state_reason: hb.task_state_reason ?? null,
626
- turn_summary: hb.turn_summary ?? null,
627
- turn_summary_updated_at: hb.turn_summary_updated_at ?? null,
628
841
  platform: hb.platform ?? "claude-code",
629
842
  };
630
843
  emit.data({ ...row, resolution_source: resolved.source, note: SUBAGENT_NOTE });
@@ -644,37 +857,17 @@ function runList(opts) {
644
857
  });
645
858
  process.exit(1);
646
859
  }
647
- const activeDir = resolve(root, ".harnery", "active");
648
- if (!existsSync(activeDir)) {
649
- emit.data({ rows: [], note: SUBAGENT_NOTE });
650
- return;
651
- }
652
860
  // Resolve self for relation column; best-effort, missing → "unknown" on every row.
653
861
  const myOwner = resolveOwner();
654
- const myHb = myOwner ? readHeartbeat(myOwner) : null;
862
+ const myHb = myOwner ? readLiveCoordinationRow(root, myOwner) : null;
655
863
  const mySession = myHb?.session_id ?? null;
656
- // Read every heartbeat.
657
- const heartbeats = [];
658
- for (const file of readdirSync(activeDir)) {
659
- if (!file.endsWith(".json"))
660
- continue;
661
- try {
662
- const raw = readFileSync(resolve(activeDir, file), "utf8");
663
- const parsed = JSON.parse(raw);
664
- if (parsed && typeof parsed.instance_id === "string") {
665
- heartbeats.push(parsed);
666
- }
667
- }
668
- catch {
669
- // skip malformed
670
- }
671
- }
864
+ const generations = readLiveCoordinationRows(root);
672
865
  // Apply staleness filter unless --stale.
673
866
  const nowSec = Math.floor(Date.now() / 1000);
674
867
  const cutoff = nowSec - freshnessCutoffSecs();
675
868
  const live = opts.stale
676
- ? heartbeats
677
- : heartbeats.filter((h) => {
869
+ ? generations
870
+ : generations.filter((h) => {
678
871
  const ts = Date.parse(h.last_heartbeat);
679
872
  return Number.isFinite(ts) && ts / 1000 >= cutoff;
680
873
  });
@@ -706,20 +899,17 @@ function runList(opts) {
706
899
  session_id: h.session_id,
707
900
  kind: "transient",
708
901
  relation: relationOf(h, myOwner ?? "", mySession),
709
- started_at: h.started_at,
902
+ started_at: h.started_at ?? h.last_heartbeat,
710
903
  last_heartbeat: h.last_heartbeat,
711
904
  files_touched: [...(h.files_touched ?? [])].sort(),
712
- last_tool: h.last_tool ?? null,
713
- last_tool_target: h.last_tool_target ?? null,
714
905
  task: h.task ?? null,
715
906
  activity: activityOf(h),
716
907
  activity_updated_at: h.activity_updated_at ?? null,
717
908
  activity_source: h.activity_source ?? null,
718
909
  task_state: taskStateOf(h),
910
+ task_state_scope: "current",
719
911
  task_state_updated_at: h.task_state_updated_at ?? null,
720
912
  task_state_reason: h.task_state_reason ?? null,
721
- turn_summary: h.turn_summary ?? null,
722
- turn_summary_updated_at: h.turn_summary_updated_at ?? null,
723
913
  platform: h.platform ?? "claude-code",
724
914
  });
725
915
  continue;
@@ -737,30 +927,26 @@ function runList(opts) {
737
927
  session_id: h.session_id,
738
928
  kind: normalizeKind(h.kind),
739
929
  relation: relationOf(h, myOwner ?? "", mySession),
740
- started_at: h.started_at,
930
+ started_at: h.started_at ?? h.last_heartbeat,
741
931
  last_heartbeat: h.last_heartbeat,
742
932
  files_touched: files,
743
- last_tool: h.last_tool ?? null,
744
- last_tool_target: h.last_tool_target ?? null,
745
933
  task: h.task ?? null,
746
934
  activity: activityOf(h),
747
935
  activity_updated_at: h.activity_updated_at ?? null,
748
936
  activity_source: h.activity_source ?? null,
749
937
  task_state: taskStateOf(h),
938
+ task_state_scope: "current",
750
939
  task_state_updated_at: h.task_state_updated_at ?? null,
751
940
  task_state_reason: h.task_state_reason ?? null,
752
- turn_summary: h.turn_summary ?? null,
753
- turn_summary_updated_at: h.turn_summary_updated_at ?? null,
754
941
  platform: h.platform ?? "claude-code",
755
942
  });
756
943
  }
757
- // Guard missing started_at: a heartbeat seeded from a stray event can lack it
758
- // (legacy zombies), and an unguarded .localeCompare throws, which is exactly
759
- // what made `harn agents list --all --stale` crash.
944
+ // Guard missing started_at so a partial observational projection cannot make
945
+ // `harn agents list --all --stale` throw while sorting.
760
946
  rows.sort((a, b) => (a.started_at ?? "").localeCompare(b.started_at ?? ""));
761
947
  // Cross-machine presence (ADR 0016): append sessions on OTHER machines from
762
948
  // the locally-fetched presence refs. Advisory rows (relation=remote) — they
763
- // don't participate in local claim blocking in v1.
949
+ // never participate in local claim blocking.
764
950
  for (const rm of readRemoteMachines(root)) {
765
951
  for (const a of rm.agents) {
766
952
  rows.push({
@@ -772,17 +958,14 @@ function runList(opts) {
772
958
  started_at: a.started_at ?? "",
773
959
  last_heartbeat: a.last_heartbeat ?? "",
774
960
  files_touched: [...(a.files_touched ?? [])].sort(),
775
- last_tool: a.last_tool ?? null,
776
- last_tool_target: null,
777
961
  task: a.task ?? null,
778
962
  activity: a.activity ?? "unknown",
779
963
  activity_updated_at: null,
780
964
  activity_source: null,
781
965
  task_state: a.task_state ?? "active",
966
+ task_state_scope: "current",
782
967
  task_state_updated_at: null,
783
968
  task_state_reason: a.task_state_reason ?? null,
784
- turn_summary: a.turn_summary ?? null,
785
- turn_summary_updated_at: null,
786
969
  platform: a.platform ?? "claude-code",
787
970
  machine: rm.machine,
788
971
  });
@@ -809,21 +992,17 @@ function normalizeKind(kind) {
809
992
  return kind;
810
993
  }
811
994
  /**
812
- * The `no_heartbeat` diagnostic, quoting the owner id in FULL.
995
+ * The `no_live_generation` diagnostic, quoting the owner id in full.
813
996
  *
814
- * This id is actionable: the reader's next move is `agents heal --kind
815
- * heartbeat --owner <id>`, and heartbeats are keyed by the whole
816
- * `instance_id` (`.harnery/active/<instance_id>.json`). An abbreviated id
817
- * here reads as complete, so it gets copy-pasted into `--owner` and mints a
818
- * heartbeat at a filename no reader resolves — an orphan that leaves the
819
- * session looking unhealable. Abbreviate ids for display elsewhere, never in
820
- * a message whose whole purpose is to hand the reader an id to pass back.
997
+ * This id is actionable: the reader may pass it to cache repair or explicit
998
+ * finalization. Abbreviate ids for display elsewhere, never in a diagnostic
999
+ * whose purpose is to hand the caller a canonical identity.
821
1000
  */
822
- function noHeartbeatMessage(owner) {
823
- return `resolved owner=${owner} but no heartbeat exists at .harnery/active/${owner}.json`;
1001
+ function noLiveGenerationMessage(owner) {
1002
+ return `resolved owner=${owner} but no authority-safe live V3 generation exists for it`;
824
1003
  }
825
1004
  /**
826
- * The instance_id of a live heartbeat that `prefix` strictly prefixes, or null.
1005
+ * The instance_id of a live V3 generation that `prefix` strictly prefixes, or null.
827
1006
  *
828
1007
  * Used to recognize an abbreviated id handed back by a reader when the caller
829
1008
  * supplied no canonical id of their own. Ambiguity is treated as "no answer":
@@ -831,14 +1010,9 @@ function noHeartbeatMessage(owner) {
831
1010
  * and refusing with the wrong id in the message would be worse than the orphan.
832
1011
  */
833
1012
  function liveIdWithPrefix(root, prefix) {
834
- const activeDir = resolve(root, ".harnery", "active");
835
- if (!existsSync(activeDir))
836
- return null;
837
1013
  const matches = new Set();
838
- for (const file of readdirSync(activeDir)) {
839
- if (!file.endsWith(".json"))
840
- continue;
841
- const id = file.slice(0, -".json".length);
1014
+ for (const row of readLiveCoordinationRows(root)) {
1015
+ const id = row.instance_id;
842
1016
  if (id !== prefix && id.startsWith(prefix))
843
1017
  matches.add(id);
844
1018
  }
@@ -853,101 +1027,68 @@ async function runWatch(pollMs) {
853
1027
  });
854
1028
  process.exit(1);
855
1029
  }
856
- const activeDir = resolve(root, ".harnery", "active");
857
- if (!existsSync(activeDir)) {
858
- emit.error({ code: "no_active_dir", message: ".harnery/active/ missing" });
859
- process.exit(1);
860
- }
861
- const fs = await import("node:fs");
862
1030
  const cache = new Map();
863
1031
  // Seed cache + print an initial roster line per live peer.
864
- const initial = listActiveHeartbeats(activeDir);
865
- process.stderr.write(`watching ${activeDir} (Ctrl-C to exit)\n`); // lint-ok-emission: banner goes to stderr, stdout is the live stream
1032
+ const initial = listLiveGenerations(root);
1033
+ process.stderr.write("watching authoritative V3 coordination state (Ctrl-C to exit)\n"); // lint-ok-emission: banner goes to stderr, stdout is the live stream
866
1034
  for (const h of initial) {
867
1035
  cache.set(h.instance_id, h);
868
- emitWatchLine(`agent-${h.name ?? "?"} present (${formatAge(secondsSince(h.started_at))} old${h.task ? `, task: "${h.task}"` : ""})`);
1036
+ emitWatchLine(`agent-${h.name ?? "?"} present (${formatAge(secondsSince(h.started_at ?? h.last_heartbeat))} old${h.task ? `, task: "${h.task}"` : ""})`);
869
1037
  }
870
- let scheduled = null;
871
1038
  const rescan = () => {
872
- if (scheduled)
873
- return;
874
- scheduled = setTimeout(() => {
875
- scheduled = null;
876
- const current = new Map();
877
- for (const h of listActiveHeartbeats(activeDir))
878
- current.set(h.instance_id, h);
879
- // Removed agents.
880
- for (const [id, old] of cache) {
881
- if (!current.has(id)) {
882
- emitWatchLine(`agent-${old.name ?? "?"} ended`);
883
- cache.delete(id);
884
- }
1039
+ const current = new Map();
1040
+ for (const h of listLiveGenerations(root))
1041
+ current.set(h.instance_id, h);
1042
+ // Removed agents.
1043
+ for (const [id, old] of cache) {
1044
+ if (!current.has(id)) {
1045
+ emitWatchLine(`agent-${old.name ?? "?"} ended`);
1046
+ cache.delete(id);
885
1047
  }
886
- // Added or changed agents.
887
- for (const [id, h] of current) {
888
- const prev = cache.get(id);
889
- if (!prev) {
890
- emitWatchLine(`agent-${h.name ?? "?"} started (${formatAge(secondsSince(h.started_at))} old${h.task ? `, task: "${h.task}"` : ""})`);
891
- cache.set(id, h);
892
- continue;
893
- }
894
- // Diff fields we care about.
895
- if ((prev.task ?? "") !== (h.task ?? "")) {
896
- emitWatchLine(`agent-${h.name ?? "?"} task: ${h.task ? `"${h.task}"` : "(cleared)"}`);
897
- }
898
- if ((prev.last_tool ?? "") !== (h.last_tool ?? "") ||
899
- (prev.last_tool_target ?? "") !== (h.last_tool_target ?? "")) {
900
- if (h.last_tool) {
901
- const target = h.last_tool_target ? ` ${truncate(h.last_tool_target, 80)}` : "";
902
- emitWatchLine(`agent-${h.name ?? "?"} ${h.last_tool}${target}`);
903
- }
904
- }
905
- // File additions/removals.
906
- const prevFiles = new Set(prev.files_touched ?? []);
907
- const currFiles = new Set(h.files_touched ?? []);
908
- for (const f of currFiles) {
909
- if (!prevFiles.has(f))
910
- emitWatchLine(`agent-${h.name ?? "?"} +claim ${f}`);
911
- }
912
- for (const f of prevFiles) {
913
- if (!currFiles.has(f))
914
- emitWatchLine(`agent-${h.name ?? "?"} -release ${f}`);
915
- }
1048
+ }
1049
+ // Added or changed agents.
1050
+ for (const [id, h] of current) {
1051
+ const prev = cache.get(id);
1052
+ if (!prev) {
1053
+ emitWatchLine(`agent-${h.name ?? "?"} started (${formatAge(secondsSince(h.started_at ?? h.last_heartbeat))} old${h.task ? `, task: "${h.task}"` : ""})`);
916
1054
  cache.set(id, h);
1055
+ continue;
917
1056
  }
918
- }, pollMs);
1057
+ // Diff fields we care about.
1058
+ if ((prev.task ?? "") !== (h.task ?? "")) {
1059
+ emitWatchLine(`agent-${h.name ?? "?"} task: ${h.task ? `"${h.task}"` : "(cleared)"}`);
1060
+ }
1061
+ // File additions/removals.
1062
+ const prevFiles = new Set(prev.files_touched ?? []);
1063
+ const currFiles = new Set(h.files_touched ?? []);
1064
+ for (const f of currFiles) {
1065
+ if (!prevFiles.has(f))
1066
+ emitWatchLine(`agent-${h.name ?? "?"} +claim ${f}`);
1067
+ }
1068
+ for (const f of prevFiles) {
1069
+ if (!currFiles.has(f))
1070
+ emitWatchLine(`agent-${h.name ?? "?"} -release ${f}`);
1071
+ }
1072
+ cache.set(id, h);
1073
+ }
919
1074
  };
920
- const watcher = fs.watch(activeDir, { persistent: true }, () => rescan());
921
1075
  await new Promise((resolveP) => {
1076
+ const timer = setInterval(rescan, Math.max(100, pollMs));
922
1077
  const stop = () => {
923
- watcher.close();
1078
+ clearInterval(timer);
924
1079
  resolveP();
925
1080
  };
926
1081
  process.on("SIGINT", stop);
927
1082
  process.on("SIGTERM", stop);
928
1083
  });
929
1084
  }
930
- function listActiveHeartbeats(activeDir) {
931
- const out = [];
1085
+ function listLiveGenerations(root) {
932
1086
  const nowSec = Math.floor(Date.now() / 1000);
933
1087
  const cutoff = nowSec - freshnessCutoffSecs();
934
- for (const file of readdirSync(activeDir)) {
935
- if (!file.endsWith(".json"))
936
- continue;
937
- try {
938
- const raw = readFileSync(resolve(activeDir, file), "utf8");
939
- const parsed = JSON.parse(raw);
940
- if (!parsed || typeof parsed.instance_id !== "string")
941
- continue;
942
- const ts = Date.parse(parsed.last_heartbeat);
943
- if (Number.isFinite(ts) && ts / 1000 >= cutoff)
944
- out.push(parsed);
945
- }
946
- catch {
947
- // skip
948
- }
949
- }
950
- return out;
1088
+ return readLiveCoordinationRows(root).filter((row) => {
1089
+ const ts = Date.parse(row.last_heartbeat);
1090
+ return Number.isFinite(ts) && ts / 1000 >= cutoff;
1091
+ });
951
1092
  }
952
1093
  function secondsSince(iso) {
953
1094
  const t = Date.parse(iso);
@@ -966,33 +1107,15 @@ async function runShow(name, opts) {
966
1107
  });
967
1108
  process.exit(1);
968
1109
  }
969
- const activeDir = resolve(root, ".harnery", "active");
970
- if (!existsSync(activeDir)) {
971
- emit.error({ code: "no_active_dir", message: ".harnery/active/ missing" });
972
- process.exit(1);
973
- }
974
- // Read all heartbeats; match by name (case-insensitive). Apply freshness filter.
975
- const matches = [];
1110
+ // Match authority-safe V3 generations by resolved display name.
976
1111
  const nowSec = Math.floor(Date.now() / 1000);
977
1112
  const cutoff = nowSec - freshnessCutoffSecs();
978
- for (const file of readdirSync(activeDir)) {
979
- if (!file.endsWith(".json"))
980
- continue;
981
- try {
982
- const raw = readFileSync(resolve(activeDir, file), "utf8");
983
- const parsed = JSON.parse(raw);
984
- if (!parsed || typeof parsed.instance_id !== "string")
985
- continue;
986
- if ((parsed.name ?? "").toLowerCase() !== name.toLowerCase())
987
- continue;
988
- const ts = Date.parse(parsed.last_heartbeat);
989
- if (Number.isFinite(ts) && ts / 1000 >= cutoff)
990
- matches.push(parsed);
991
- }
992
- catch {
993
- // skip malformed
994
- }
995
- }
1113
+ const matches = readLiveCoordinationRows(root).filter((row) => {
1114
+ if ((row.name ?? "").toLowerCase() !== name.toLowerCase())
1115
+ return false;
1116
+ const ts = Date.parse(row.last_heartbeat);
1117
+ return Number.isFinite(ts) && ts / 1000 >= cutoff;
1118
+ });
996
1119
  if (matches.length === 0) {
997
1120
  emit.error({
998
1121
  code: "no_match",
@@ -1010,7 +1133,7 @@ async function runShow(name, opts) {
1010
1133
  const hb = matches[0];
1011
1134
  const report = null;
1012
1135
  const bqError = null;
1013
- const startedAtMs = Date.parse(hb.started_at);
1136
+ const startedAtMs = Date.parse(hb.started_at ?? hb.last_heartbeat);
1014
1137
  const ageSecs = Number.isFinite(startedAtMs)
1015
1138
  ? Math.max(0, Math.floor((Date.now() - startedAtMs) / 1000))
1016
1139
  : 0;
@@ -1030,14 +1153,11 @@ async function runShow(name, opts) {
1030
1153
  activity_updated_at: hb.activity_updated_at ?? null,
1031
1154
  activity_source: hb.activity_source ?? null,
1032
1155
  task_state: taskStateOf(hb),
1156
+ task_state_scope: "current",
1033
1157
  task_state_updated_at: hb.task_state_updated_at ?? null,
1034
1158
  task_state_reason: hb.task_state_reason ?? null,
1035
- turn_summary: hb.turn_summary ?? null,
1036
- turn_summary_updated_at: hb.turn_summary_updated_at ?? null,
1037
1159
  title: report?.title ?? null,
1038
1160
  files_held: hb.files_touched ?? [],
1039
- last_tool: hb.last_tool ?? null,
1040
- last_tool_target: hb.last_tool_target ?? null,
1041
1161
  recent_prompts: report?.recent_prompts ?? [],
1042
1162
  recent_tools: report?.recent_tools ?? [],
1043
1163
  tool_counts: report?.tool_counts ?? [],
@@ -1061,10 +1181,6 @@ async function runShow(name, opts) {
1061
1181
  lines.push(` last heartbeat: ${formatAge(heartbeatAgeSecs)} ago`);
1062
1182
  lines.push(` activity: ${data.activity}`);
1063
1183
  lines.push(` lifecycle: ${data.task_state}${data.task_state === "blocked" && data.task_state_reason ? `: ${data.task_state_reason}` : ""}`);
1064
- if (data.last_tool) {
1065
- const target = data.last_tool_target ? ` ${truncate(data.last_tool_target, 80)}` : "";
1066
- lines.push(` last activity: ${data.last_tool}${target}`);
1067
- }
1068
1184
  if (data.files_held.length > 0) {
1069
1185
  lines.push(` holds ${data.files_held.length} file(s):`);
1070
1186
  for (const f of data.files_held.slice(0, 10))
@@ -1217,13 +1333,19 @@ function runSetTask(task, opts) {
1217
1333
  // Identity: prefer explicit --session-id (the ppid-walk-free escape hatch,
1218
1334
  // mirrors `status`), fall back to the ppid walk. Cursor shell tool calls
1219
1335
  // don't descend from a pid-map-registered anchor, so the walk can miss there.
1336
+ // Final fallback: the adapter/connector-stamped session id from the
1337
+ // environment. set-task is the session's REGISTRATION point — a fresh
1338
+ // (bridge) session has no heartbeat yet, so the heartbeat-validated resolver
1339
+ // returns null by design, and erroring here orphans the session's first
1340
+ // ritual command. The env id carries the same trust as an explicit
1341
+ // --session-id, and set-task mints the heartbeat exactly as that path does.
1220
1342
  if (!opts?.sessionId)
1221
1343
  ensureCursorSession(root);
1222
- const myOwner = opts?.sessionId ?? resolveOwner();
1344
+ const myOwner = opts?.sessionId ?? resolveOwner() ?? sessionIdentityFromEnv();
1223
1345
  if (!myOwner) {
1224
1346
  emit.error({
1225
1347
  code: "no_pidmap_entry",
1226
- message: "not in an agent session; ppid walk found no pid-map entry (pass --session-id to bypass)",
1348
+ message: "not in an agent session; no session-id environment identity and the ppid walk found no pid-map entry (pass --session-id to bypass)",
1227
1349
  });
1228
1350
  process.exit(1);
1229
1351
  }
@@ -1233,7 +1355,7 @@ function runSetTask(task, opts) {
1233
1355
  // naming window, and subagent/workflow kinds are never named — see
1234
1356
  // heartbeat-writer.setTask. This call is the naming call exactly when the
1235
1357
  // stamp appears across the mutation.
1236
- const priorHb = readHeartbeat(myOwner);
1358
+ const priorHb = readCurrentCoordinationRow(myOwner);
1237
1359
  const normalizedTask = task.length > 0 ? task : undefined;
1238
1360
  const lifecycleWarning = priorHb && (priorHb.task_state ?? "active") !== "active" && priorHb.task !== normalizedTask
1239
1361
  ? `Task text changed while lifecycle is ${priorHb.task_state}; run \`${resolveBinName(root)} agents lifecycle active\` to reopen it explicitly.`
@@ -1251,7 +1373,7 @@ function runSetTask(task, opts) {
1251
1373
  });
1252
1374
  process.exit(1);
1253
1375
  }
1254
- const hb = readHeartbeat(myOwner);
1376
+ const hb = readCurrentCoordinationRow(myOwner);
1255
1377
  // The naming call: the mutation just stamped `suggested_session_name` for
1256
1378
  // the first time. `first_of_session: true` means exactly "this call produced
1257
1379
  // the session name" — it is never true with a null name (an empty first
@@ -1260,18 +1382,6 @@ function runSetTask(task, opts) {
1260
1382
  ? hb.suggested_session_name
1261
1383
  : null;
1262
1384
  const firstOfSession = suggestedName !== null;
1263
- emitCanonical({
1264
- type: "state.task_set",
1265
- owner: myOwner,
1266
- session: hb?.session_id ?? myOwner,
1267
- adapter: normalizeAdapter(hb?.platform),
1268
- data: {
1269
- task,
1270
- cleared: !task || task.length === 0,
1271
- first_of_session: firstOfSession,
1272
- suggested_session_name: suggestedName,
1273
- },
1274
- });
1275
1385
  emit.data({
1276
1386
  instance_id: myOwner,
1277
1387
  name: hb?.name ?? null,
@@ -1284,7 +1394,7 @@ function runSetTask(task, opts) {
1284
1394
  // exists; this result is the moment the model holds the string.
1285
1395
  ...(suggestedName
1286
1396
  ? {
1287
- note: "Reproduce suggested_session_name verbatim, by itself, in a fenced code block at the top of your reply — the operator copies it as the session/tab title.",
1397
+ note: SESSION_NAME_DISPLAY_NOTE,
1288
1398
  }
1289
1399
  : {}),
1290
1400
  });
@@ -1310,7 +1420,7 @@ function runLifecycle(rawState, opts) {
1310
1420
  }
1311
1421
  if (!opts.sessionId)
1312
1422
  ensureCursorSession(root);
1313
- const myOwner = opts.sessionId ?? resolveOwner();
1423
+ const myOwner = opts.sessionId ?? resolveOwner() ?? sessionIdentityFromEnv();
1314
1424
  if (!myOwner) {
1315
1425
  emit.error({
1316
1426
  code: "no_pidmap_entry",
@@ -1319,11 +1429,41 @@ function runLifecycle(rawState, opts) {
1319
1429
  process.exitCode = 1;
1320
1430
  return;
1321
1431
  }
1322
- const hb = readHeartbeat(myOwner);
1432
+ let hb = readCurrentCoordinationRow(myOwner);
1433
+ let reopenedGeneration;
1323
1434
  if (!hb) {
1324
- emit.error({ code: "no_heartbeat", message: noHeartbeatMessage(myOwner) });
1325
- process.exitCode = 1;
1326
- return;
1435
+ if (state !== "active") {
1436
+ emit.error({
1437
+ code: "no_live_generation",
1438
+ message: `${noLiveGenerationMessage(myOwner)}; run \`${resolveBinName(root)} agents lifecycle active\` to open a fresh generation`,
1439
+ });
1440
+ process.exitCode = 1;
1441
+ return;
1442
+ }
1443
+ try {
1444
+ reopenedGeneration = reopenLiveCoordinationGenerationV3({
1445
+ coordRoot: root,
1446
+ owner: myOwner,
1447
+ nativeSessionId: nativeSessionIdentity(readHeartbeatCache(root, myOwner), myOwner),
1448
+ });
1449
+ }
1450
+ catch (error) {
1451
+ emit.error({
1452
+ code: "lifecycle_reopen_failed",
1453
+ message: error instanceof Error ? error.message : String(error),
1454
+ });
1455
+ process.exitCode = 1;
1456
+ return;
1457
+ }
1458
+ hb = readCurrentCoordinationRow(myOwner);
1459
+ if (!hb) {
1460
+ emit.error({
1461
+ code: "lifecycle_reopen_failed",
1462
+ message: "the fresh generation was recorded but did not project as live",
1463
+ });
1464
+ process.exitCode = 1;
1465
+ return;
1466
+ }
1327
1467
  }
1328
1468
  if (hb.kind === "subagent" || hb.kind === "transient" || hb.workflow_run_id) {
1329
1469
  emit.error({
@@ -1352,6 +1492,22 @@ function runLifecycle(rawState, opts) {
1352
1492
  }
1353
1493
  const priorState = hb.task_state ?? "active";
1354
1494
  const priorReason = hb.task_state_reason || undefined;
1495
+ if (reopenedGeneration) {
1496
+ emit.data({
1497
+ instance_id: myOwner,
1498
+ task_state: "active",
1499
+ prior_state: null,
1500
+ reason: null,
1501
+ changed: true,
1502
+ generation_reopened: true,
1503
+ prior_generation_id: reopenedGeneration.prior_generation_id,
1504
+ generation_id: reopenedGeneration.generation_id,
1505
+ name_reminted: false,
1506
+ suggested_session_name: null,
1507
+ git_finalization_checked: false,
1508
+ });
1509
+ return;
1510
+ }
1355
1511
  if (priorState === state && priorReason === reason) {
1356
1512
  emit.data({
1357
1513
  instance_id: myOwner,
@@ -1365,7 +1521,7 @@ function runLifecycle(rawState, opts) {
1365
1521
  }
1366
1522
  let finalization = null;
1367
1523
  if (state === "done") {
1368
- const history = readSessionWriteClaims(root, hb.instance_id, hb.session_id ?? myOwner);
1524
+ const history = readSessionWriteClaims(root, hb.instance_id, nativeSessionIdentity(hb, myOwner));
1369
1525
  const touchedPaths = [...new Set([...(hb.files_touched ?? []), ...history.paths])];
1370
1526
  finalization = checkGitFinalization(root, touchedPaths, {
1371
1527
  claimHistoryComplete: history.complete,
@@ -1379,19 +1535,16 @@ function runLifecycle(rawState, opts) {
1379
1535
  return;
1380
1536
  }
1381
1537
  }
1382
- const suggestedName = buildLifecycleSuggestedName(hb.name ?? "unknown", hb.task, state);
1538
+ const suggestedName = buildLifecycleSuggestedName(hb.suggested_session_name, state);
1383
1539
  const nameReminted = suggestedName !== null && suggestedName !== hb.suggested_session_name;
1384
- const emitted = emitCanonical({
1385
- type: "state.task_state",
1540
+ const emitted = emitEventV3({
1386
1541
  owner: myOwner,
1387
- session: hb.session_id ?? myOwner,
1542
+ session: nativeSessionIdentity(hb, myOwner),
1388
1543
  adapter: normalizeAdapter(hb.platform),
1389
- data: {
1390
- state,
1391
- reason: reason ?? null,
1392
- prior_state: priorState,
1393
- name_reminted: nameReminted,
1394
- git_finalization_checked: finalization !== null,
1544
+ observation: {
1545
+ event_type: "coord.lifecycle_changed",
1546
+ new_state: state,
1547
+ ...(reason ? { reason } : {}),
1395
1548
  ...(nameReminted ? { suggested_session_name: suggestedName } : {}),
1396
1549
  },
1397
1550
  });
@@ -1414,7 +1567,7 @@ function runLifecycle(rawState, opts) {
1414
1567
  git_finalization_checked: finalization !== null,
1415
1568
  ...(nameReminted
1416
1569
  ? {
1417
- note: "Reproduce suggested_session_name verbatim, by itself, in a fenced code block at the top of your reply — the operator copies it as the session/tab title.",
1570
+ note: SESSION_NAME_DISPLAY_NOTE,
1418
1571
  }
1419
1572
  : {}),
1420
1573
  });
@@ -1440,7 +1593,7 @@ function runSuggestName(descriptionParts, opts) {
1440
1593
  });
1441
1594
  process.exit(1);
1442
1595
  }
1443
- const hb = readHeartbeat(myOwner);
1596
+ const hb = readCurrentCoordinationRow(myOwner);
1444
1597
  const agentName = hb?.name || "unknown";
1445
1598
  // Description resolution: an explicit arg wins; with no arg, fall back to the
1446
1599
  // agent's current declared task, so a bare `suggest-name` reprints the running
@@ -1483,6 +1636,14 @@ function runStatus(opts) {
1483
1636
  });
1484
1637
  process.exit(1);
1485
1638
  }
1639
+ if (opts.endSession && !opts.endTurn) {
1640
+ emit.error({
1641
+ code: "end_session_requires_end_turn",
1642
+ message: "--end-session requires --end-turn so Git finalization is verified first",
1643
+ });
1644
+ process.exitCode = 1;
1645
+ return;
1646
+ }
1486
1647
  // Identity resolution: prefer explicit --session-id (hook-friendly), fall
1487
1648
  // back to ppid walk for interactive shell usage.
1488
1649
  if (!opts.sessionId)
@@ -1495,17 +1656,21 @@ function runStatus(opts) {
1495
1656
  });
1496
1657
  process.exit(1);
1497
1658
  }
1498
- const hb = readHeartbeat(myOwner);
1659
+ const hb = readCurrentCoordinationRow(myOwner);
1499
1660
  if (!hb) {
1500
1661
  emit.error({
1501
- code: "no_heartbeat",
1502
- message: noHeartbeatMessage(myOwner),
1662
+ code: "no_live_generation",
1663
+ message: noLiveGenerationMessage(myOwner),
1503
1664
  });
1504
1665
  process.exit(1);
1505
1666
  }
1667
+ // Report-only run quality: cheap config/cursor due check first, then a
1668
+ // non-blocking lazy evaluation. Shadow mode deliberately exposes no severity.
1669
+ const qualityEvaluation = evaluateRunQualityIfDue(root, new Date(), myOwner);
1670
+ const quality = qualityForStatus(qualityEvaluation.config.requested_mode, qualityEvaluation.snapshot);
1506
1671
  let finalization = null;
1507
1672
  if (opts.endTurn) {
1508
- const history = readSessionWriteClaims(root, hb.instance_id, hb.session_id ?? myOwner);
1673
+ const history = readSessionWriteClaims(root, hb.instance_id, nativeSessionIdentity(hb, myOwner));
1509
1674
  const touchedPaths = [...new Set([...(hb.files_touched ?? []), ...history.paths])];
1510
1675
  finalization = checkGitFinalization(root, touchedPaths, {
1511
1676
  claimHistoryComplete: history.complete,
@@ -1519,51 +1684,55 @@ function runStatus(opts) {
1519
1684
  return;
1520
1685
  }
1521
1686
  }
1522
- emitCanonical({
1523
- type: "state.status_checked",
1687
+ emitEventV3({
1524
1688
  owner: myOwner,
1525
- session: hb.session_id ?? myOwner,
1689
+ session: nativeSessionIdentity(hb, myOwner),
1526
1690
  adapter: normalizeAdapter(hb.platform),
1527
- data: {
1528
- format: opts.json ? "json" : "box",
1529
- git_finalization_checked: opts.endTurn === true,
1530
- agent_count: 0, // computed below, not yet available here; Phase 5 verdict reads owner-scope only
1531
- included_self: true,
1691
+ observation: {
1692
+ event_type: "coord.status_observed",
1693
+ status: opts.endTurn ? "end_turn_checked" : opts.json ? "json_checked" : "box_checked",
1532
1694
  },
1533
1695
  });
1534
- const startedAtMs = Date.parse(hb.started_at);
1696
+ let sessionEnd;
1697
+ if (opts.endSession) {
1698
+ const canonicalInstanceId = liveInstanceIdV3(hb.instance_id);
1699
+ const records = listHookProducerStateRecordsV3(root, { includeTerminal: true }).filter(({ state }) => state.instance_id === canonicalInstanceId);
1700
+ if (records.length !== 1 || !records[0]) {
1701
+ emit.error({
1702
+ code: "session_identity_ambiguous",
1703
+ message: `expected one V3 generation for ${canonicalInstanceId}; found ${records.length}`,
1704
+ });
1705
+ process.exitCode = 1;
1706
+ return;
1707
+ }
1708
+ const state = records[0].state;
1709
+ const requested = requestSessionEndExplicitV3({
1710
+ coordRoot: root,
1711
+ instance_id: state.instance_id,
1712
+ generation_id: state.generation_id,
1713
+ outcome: "succeeded",
1714
+ coordination_finalized: true,
1715
+ });
1716
+ if (requested.state === "queued" || requested.state === "already_requested") {
1717
+ sessionEnd = { state: requested.state, request_id: requested.request.request_id };
1718
+ }
1719
+ else if (requested.state === "recorded") {
1720
+ sessionEnd = { state: requested.state, terminal_event_id: requested.event.event_id };
1721
+ }
1722
+ else if (requested.state === "already_ended") {
1723
+ sessionEnd = { state: requested.state, terminal_event_id: requested.event_id };
1724
+ }
1725
+ else {
1726
+ emit.error({ code: "session_end_failed", message: JSON.stringify(requested) });
1727
+ process.exitCode = 1;
1728
+ return;
1729
+ }
1730
+ }
1731
+ const startedAtMs = Date.parse(hb.started_at ?? hb.last_heartbeat);
1535
1732
  const ageSecs = Number.isFinite(startedAtMs)
1536
1733
  ? Math.max(0, Math.floor((Date.now() - startedAtMs) / 1000))
1537
1734
  : 0;
1538
- const activeDir = resolve(root, ".harnery", "active");
1539
- const nowSec = Math.floor(Date.now() / 1000);
1540
- const cutoff = nowSec - freshnessCutoffSecs();
1541
- const livePeers = [];
1542
- let peersStale = 0;
1543
- if (existsSync(activeDir)) {
1544
- for (const file of readdirSync(activeDir)) {
1545
- if (!file.endsWith(".json"))
1546
- continue;
1547
- try {
1548
- const raw = readFileSync(resolve(activeDir, file), "utf8");
1549
- const peer = JSON.parse(raw);
1550
- if (!peer || typeof peer.instance_id !== "string")
1551
- continue;
1552
- if (peer.instance_id === myOwner)
1553
- continue;
1554
- const ts = Date.parse(peer.last_heartbeat);
1555
- if (Number.isFinite(ts) && ts / 1000 >= cutoff) {
1556
- livePeers.push(peer);
1557
- }
1558
- else {
1559
- peersStale++;
1560
- }
1561
- }
1562
- catch {
1563
- // skip malformed
1564
- }
1565
- }
1566
- }
1735
+ const { livePeers, stale: peersStale } = collectStatusPeerHealth(root, myOwner);
1567
1736
  // Sort: file-holders first (by file count desc), then idle peers by recency.
1568
1737
  livePeers.sort((a, b) => {
1569
1738
  const af = a.files_touched?.length ?? 0;
@@ -1577,7 +1746,7 @@ function runStatus(opts) {
1577
1746
  // Cross-machine presence (ADR 0016): sessions on other machines, advisory.
1578
1747
  const remoteMachines = readRemoteMachines(root);
1579
1748
  const peersStr = formatPeers(livePeers, 4, peersStale, remoteMachines);
1580
- const ctxUsage = readContextUsage(hb.session_id, hb.platform);
1749
+ const ctxUsage = readContextUsage(hb.native_session_id ?? hb.session_id, hb.platform);
1581
1750
  let ctxStr;
1582
1751
  if (!ctxUsage) {
1583
1752
  ctxStr = "unavailable";
@@ -1612,6 +1781,7 @@ function runStatus(opts) {
1612
1781
  activity_updated_at: hb.activity_updated_at ?? null,
1613
1782
  activity_source: hb.activity_source ?? null,
1614
1783
  task_state: taskStateOf(hb),
1784
+ task_state_scope: "current",
1615
1785
  task_state_updated_at: hb.task_state_updated_at ?? null,
1616
1786
  task_state_reason: hb.task_state_reason ?? null,
1617
1787
  files_held: filesHeld,
@@ -1638,7 +1808,9 @@ function runStatus(opts) {
1638
1808
  context_window: ctxUsage?.window ?? null,
1639
1809
  timestamp_iso: new Date().toISOString(),
1640
1810
  timestamp_local: timeStr,
1811
+ ...(quality ? { quality } : {}),
1641
1812
  ...(finalization ? { finalization } : {}),
1813
+ ...(sessionEnd ? { session_end: sessionEnd } : {}),
1642
1814
  };
1643
1815
  if (opts.json) {
1644
1816
  emit.config({ format: "json" });
@@ -1654,10 +1826,7 @@ function runStatus(opts) {
1654
1826
  ["peers", peersStr],
1655
1827
  ["time", timeStr],
1656
1828
  ];
1657
- // task + turn_summary get full text; formatBox word-wraps to MAX_BOX_CONTENT_WIDTH.
1658
- if (hb.turn_summary && hb.turn_summary.length > 0) {
1659
- rows.splice(1, 0, ["last turn", hb.turn_summary]);
1660
- }
1829
+ // Task gets full text; formatBox word-wraps to MAX_BOX_CONTENT_WIDTH.
1661
1830
  if (hb.task && hb.task.length > 0) {
1662
1831
  rows.splice(1, 0, ["task", hb.task]);
1663
1832
  }
@@ -1671,10 +1840,49 @@ function runStatus(opts) {
1671
1840
  : `${pendingCouncils.length} pending (${pendingCouncils[0]}, +${pendingCouncils.length - 1})`;
1672
1841
  rows.splice(idx, 0, ["council", summary]);
1673
1842
  }
1843
+ if (quality) {
1844
+ const idx = rows.findIndex((row) => row[0] === "time");
1845
+ rows.splice(idx, 0, ["quality", formatRunQuality(quality.status, quality.signal_ids)]);
1846
+ }
1847
+ if (sessionEnd) {
1848
+ const idx = rows.findIndex((row) => row[0] === "time");
1849
+ rows.splice(idx, 0, [
1850
+ "session end",
1851
+ sessionEnd.state === "queued" || sessionEnd.state === "already_requested"
1852
+ ? "queued after this turn"
1853
+ : "recorded",
1854
+ ]);
1855
+ }
1674
1856
  // Box rendering needs predictable stdout regardless of TTY/pipe detection:
1675
1857
  // agent runs this via Bash (no TTY) and pastes captured stdout into chat.
1676
1858
  process.stdout.write(`${formatBox(displayName, rows)}\n`); // lint-ok-emission: chat-paste path; emit.text() auto-suppresses non-TTY
1677
1859
  }
1860
+ function qualityForStatus(mode, snapshot) {
1861
+ if (mode !== "report")
1862
+ return null;
1863
+ const fresh = !!snapshot && Date.parse(snapshot.expires_at) > Date.now();
1864
+ if (!snapshot || !fresh) {
1865
+ return {
1866
+ status: "unknown",
1867
+ signal_ids: [],
1868
+ evaluated_at: snapshot?.evaluated_at ?? null,
1869
+ fresh: false,
1870
+ };
1871
+ }
1872
+ return {
1873
+ status: snapshot.status,
1874
+ signal_ids: snapshot.signals
1875
+ .filter((signal) => signal.state === "active" && signal.severity !== "none")
1876
+ .map((signal) => signal.id)
1877
+ .sort()
1878
+ .slice(0, 3),
1879
+ evaluated_at: snapshot.evaluated_at,
1880
+ fresh: true,
1881
+ };
1882
+ }
1883
+ function formatRunQuality(status, signalIds) {
1884
+ return signalIds.length > 0 ? `${status} (${signalIds.join(", ")})` : status;
1885
+ }
1678
1886
  function formatList(items, cap, emptyLabel) {
1679
1887
  if (items.length === 0)
1680
1888
  return emptyLabel;
@@ -1715,6 +1923,28 @@ function formatPeers(peers, cap, staleCount, remoteMachines = []) {
1715
1923
  }
1716
1924
  return main;
1717
1925
  }
1926
+ export function collectStatusPeerHealth(root, myOwner, nowMs = Date.now()) {
1927
+ let rows = [];
1928
+ try {
1929
+ rows = readLiveCoordinationRows(root);
1930
+ }
1931
+ catch {
1932
+ // V3 authority failures must not resurrect disposable cache rows.
1933
+ }
1934
+ const cutoffMs = nowMs - freshnessCutoffSecs() * 1000;
1935
+ const livePeers = [];
1936
+ let stale = 0;
1937
+ for (const peer of rows) {
1938
+ if (peer.instance_id === myOwner)
1939
+ continue;
1940
+ const observedAtMs = Date.parse(peer.last_heartbeat);
1941
+ if (Number.isFinite(observedAtMs) && observedAtMs >= cutoffMs)
1942
+ livePeers.push(peer);
1943
+ else
1944
+ stale += 1;
1945
+ }
1946
+ return { livePeers, stale };
1947
+ }
1718
1948
  function fmtTokens(n) {
1719
1949
  if (n < 1000)
1720
1950
  return `${n}`;
@@ -1968,40 +2198,49 @@ function formatAge(secs) {
1968
2198
  const h = Math.floor((secs % 86400) / 3600);
1969
2199
  return `${d}d ${h}h`;
1970
2200
  }
1971
- /**
1972
- * Read canonical events in a time window. The heal + council telemetry the
1973
- * health/heal commands report lives here. Full-file read + ts filter is fine
1974
- * for an on-demand diagnostic; events.ndjson is the canonical store.
1975
- */
1976
- function readCanonicalEventsInWindow(root, cutoffMs) {
1977
- const p = resolve(root, ".harnery", "events.ndjson");
1978
- if (!existsSync(p))
1979
- return [];
1980
- let raw;
1981
- try {
1982
- raw = readFileSync(p, "utf8");
1983
- }
1984
- catch {
1985
- return [];
1986
- }
1987
- const out = [];
1988
- for (const line of raw.split("\n")) {
1989
- if (!line)
1990
- continue;
1991
- try {
1992
- const ev = JSON.parse(line);
1993
- if (!ev.event_type || !ev.ts)
1994
- continue;
1995
- const tsMs = Date.parse(ev.ts);
2201
+ /** Read validated canonical V3 events. */
2202
+ export function readAgentDiagnosticEventsInWindow(root, cutoffMs) {
2203
+ const control = readEventV3ControlState(root);
2204
+ if (control.state === "candidate" || control.state === "active") {
2205
+ const ledger = readLedgerV3(root);
2206
+ if (!ledger.complete) {
2207
+ return {
2208
+ source: "v3",
2209
+ authoritative: false,
2210
+ reason: `V3 ledger validation failed: ${ledger.diagnostics.map((item) => item.code).join(", ") || "incomplete"}`,
2211
+ truncated: false,
2212
+ bytes: ledger.bytes,
2213
+ events: [],
2214
+ };
2215
+ }
2216
+ const events = [];
2217
+ for (const { event } of ledger.events) {
2218
+ const tsMs = Date.parse(event.time.recorded_at);
1996
2219
  if (!Number.isFinite(tsMs) || tsMs < cutoffMs)
1997
2220
  continue;
1998
- out.push(ev);
1999
- }
2000
- catch {
2001
- /* skip malformed */
2221
+ events.push({
2222
+ event_type: event.event_type,
2223
+ ts: event.time.recorded_at,
2224
+ instance_id: event.scope.instance_id,
2225
+ payload: event.payload,
2226
+ });
2002
2227
  }
2228
+ return {
2229
+ source: "v3",
2230
+ authoritative: true,
2231
+ truncated: false,
2232
+ bytes: ledger.bytes,
2233
+ events,
2234
+ };
2003
2235
  }
2004
- return out;
2236
+ return {
2237
+ source: "v3",
2238
+ authoritative: false,
2239
+ reason: `V3 control state is ${control.state}`,
2240
+ truncated: false,
2241
+ bytes: 0,
2242
+ events: [],
2243
+ };
2005
2244
  }
2006
2245
  /** Normalize adapter event data into the heartbeat platform value. */
2007
2246
  function adapterToPlatform(adapter) {
@@ -2011,18 +2250,21 @@ function adapterToPlatform(adapter) {
2011
2250
  return "cursor";
2012
2251
  if (adapter === "codex")
2013
2252
  return "codex";
2014
- return "claude-code";
2253
+ return "unknown";
2015
2254
  }
2016
2255
  /** Project a canonical health.* event into the HealEvent shape the aggregators
2017
2256
  * already consume. Returns null for non-heal events. instance_id → display name
2018
2257
  * via `nameById` (full-UUID keyed). */
2019
2258
  function canonicalToHealEvent(ev, nameById) {
2020
- if (ev.event_type !== "health.pidmap_heal" && ev.event_type !== "health.heartbeat_heal") {
2259
+ if (ev.event_type !== "health.observed") {
2021
2260
  return null;
2022
2261
  }
2023
- const kind = ev.event_type === "health.pidmap_heal" ? "pidmap" : "heartbeat";
2024
- const data = ev.data ?? {};
2025
- const reason = data.reason === "stale" ? "stale" : "missing";
2262
+ const data = ev.payload ?? {};
2263
+ const subsystem = typeof data.subsystem === "string" ? data.subsystem : "";
2264
+ if (subsystem !== "pidmap" && subsystem !== "heartbeat")
2265
+ return null;
2266
+ const kind = subsystem;
2267
+ const reason = data.condition === "stale" ? "stale" : "missing";
2026
2268
  const instanceId = ev.instance_id ?? "";
2027
2269
  const name = nameById.get(instanceId);
2028
2270
  const agent = name ? `agent-${name}` : `agent-${instanceId.slice(0, 8) || "unknown"}`;
@@ -2099,13 +2341,12 @@ function runHealEvents(opts) {
2099
2341
  });
2100
2342
  process.exit(1);
2101
2343
  }
2102
- // Heal telemetry lives in the canonical .harnery/events.ndjson stream
2103
- // (health.pidmap_heal / health.heartbeat_heal), emitted by the writer on
2104
- // actual self-heal writes.
2344
+ // Heal telemetry is read from the canonical V3 ledger.
2105
2345
  const cutoffMs = Date.now() - sinceSecs * 1000;
2106
2346
  const nameById = buildNameById(root);
2107
2347
  const events = [];
2108
- for (const ev of readCanonicalEventsInWindow(root, cutoffMs)) {
2348
+ const diagnosticEvents = readAgentDiagnosticEventsInWindow(root, cutoffMs);
2349
+ for (const ev of diagnosticEvents.events) {
2109
2350
  const heal = canonicalToHealEvent(ev, nameById);
2110
2351
  if (heal)
2111
2352
  events.push(heal);
@@ -2142,6 +2383,11 @@ function runHealEvents(opts) {
2142
2383
  const recent = events.slice(0, limit);
2143
2384
  const data = {
2144
2385
  since: opts.since,
2386
+ telemetry: {
2387
+ source: diagnosticEvents.source,
2388
+ authoritative: diagnosticEvents.authoritative,
2389
+ reason: diagnosticEvents.reason,
2390
+ },
2145
2391
  total: events.length,
2146
2392
  by_reason: byReason,
2147
2393
  by_kind: byKind,
@@ -2152,7 +2398,15 @@ function runHealEvents(opts) {
2152
2398
  };
2153
2399
  if (opts.csv) {
2154
2400
  emit.config({ format: "csv" });
2155
- emit.data(recent);
2401
+ emit.data(data.telemetry.authoritative
2402
+ ? recent
2403
+ : [
2404
+ {
2405
+ status: "unavailable",
2406
+ source: data.telemetry.source,
2407
+ reason: data.telemetry.reason,
2408
+ },
2409
+ ]);
2156
2410
  return;
2157
2411
  }
2158
2412
  if (opts.json) {
@@ -2164,6 +2418,11 @@ function runHealEvents(opts) {
2164
2418
  if (process.stdout.isTTY) {
2165
2419
  const lines = [];
2166
2420
  lines.push(`Heal events: ${events.length} total in last ${opts.since} (health.pidmap_heal + health.heartbeat_heal)`);
2421
+ if (!data.telemetry.authoritative) {
2422
+ lines.push(` unavailable: ${data.telemetry.reason ?? "diagnostic event source is incomplete"}`);
2423
+ emit.text(`${lines.join("\n")}\n`);
2424
+ return;
2425
+ }
2167
2426
  lines.push("");
2168
2427
  if (events.length === 0) {
2169
2428
  lines.push(" (none; pid-map/heartbeat drift is not happening in this window)");
@@ -2211,23 +2470,206 @@ function runHealEvents(opts) {
2211
2470
  emit.text(`${lines.join("\n")}\n`);
2212
2471
  }
2213
2472
  }
2473
+ /** Open-span soft watermark per producer state. The producer-state reader
2474
+ * hard-caps at 256 spans (a state file beyond it fails to load), so surfacing
2475
+ * pressure at half that gives room to act before reads start failing. */
2476
+ const SPAN_PRESSURE_SOFT_WATERMARK = 128;
2477
+ function errorText(error) {
2478
+ return error instanceof Error ? error.message : String(error);
2479
+ }
2480
+ /**
2481
+ * Read-only health counters for the V3 event ledger's producer surfaces. Never
2482
+ * mutates ledger state (no control repair, no spool drain) and never throws:
2483
+ * a non-live route returns `{ state: "unavailable" }`, and each sub-surface
2484
+ * that fails to read lands in `collection_errors` instead of aborting the rest.
2485
+ */
2486
+ export function collectEventLedgerHealthV3(root, nowMs = Date.now()) {
2487
+ let control;
2488
+ try {
2489
+ control = readEventV3ControlState(root);
2490
+ }
2491
+ catch (error) {
2492
+ return { state: "unavailable", reason: `control read failed: ${errorText(error)}` };
2493
+ }
2494
+ if (control.state !== "candidate" && control.state !== "active") {
2495
+ return {
2496
+ state: "unavailable",
2497
+ reason: `${control.state}: ${control.reason}`,
2498
+ };
2499
+ }
2500
+ const collectionErrors = [];
2501
+ // 1) Open tool spans per live (non-terminal) generation + span-count pressure.
2502
+ const generations = [];
2503
+ const spanPressure = [];
2504
+ try {
2505
+ for (const { state } of listHookProducerStateRecordsV3(root)) {
2506
+ if (state.spans.length >= SPAN_PRESSURE_SOFT_WATERMARK) {
2507
+ spanPressure.push({
2508
+ instance_id: state.instance_id,
2509
+ generation_id: state.generation_id,
2510
+ span_count: state.spans.length,
2511
+ });
2512
+ }
2513
+ if (state.spans.length === 0)
2514
+ continue;
2515
+ generations.push({
2516
+ instance_id: state.instance_id,
2517
+ generation_id: state.generation_id,
2518
+ adapter: state.adapter,
2519
+ span_count: state.spans.length,
2520
+ turn_open: Boolean(state.current_turn_id),
2521
+ });
2522
+ }
2523
+ }
2524
+ catch (error) {
2525
+ collectionErrors.push(`producer states unreadable: ${errorText(error)}`);
2526
+ }
2527
+ // 2) Pending finalization requests with age + trigger.
2528
+ const pending = [];
2529
+ try {
2530
+ for (const request of listSessionFinalizationRequestsV3(root)) {
2531
+ if (request.status !== "pending")
2532
+ continue;
2533
+ const observedMs = Date.parse(request.observed_at);
2534
+ pending.push({
2535
+ request_id: request.request_id,
2536
+ trigger: request.trigger,
2537
+ generation_id: request.generation_id,
2538
+ age_ms: Number.isFinite(observedMs) ? Math.max(0, nowMs - observedMs) : -1,
2539
+ allowed_open_span_count: request.allowed_open_span_ids?.length ?? 0,
2540
+ });
2541
+ }
2542
+ }
2543
+ catch (error) {
2544
+ collectionErrors.push(`finalization requests unreadable: ${errorText(error)}`);
2545
+ }
2546
+ // 3) Intake spool depth (queued hook signals awaiting a lease-holder drain).
2547
+ let intakeTotal = 0;
2548
+ const intakeGroups = [];
2549
+ try {
2550
+ for (const group of listHookIntakeGroupsV3(root)) {
2551
+ const count = listHookIntakeRecordsV3(group.directory).length;
2552
+ if (count === 0)
2553
+ continue;
2554
+ intakeTotal += count;
2555
+ intakeGroups.push({ adapter: group.adapter, session_hash: group.session_hash, count });
2556
+ }
2557
+ }
2558
+ catch (error) {
2559
+ collectionErrors.push(`intake spool unreadable: ${errorText(error)}`);
2560
+ }
2561
+ // 4) Diagnostics spool counts by category. Filenames only (never the
2562
+ // contents): `<category>-<orderkey>.json` where the orderkey leads with a
2563
+ // zero-padded epoch-ms, which also gives the last-24h split for free.
2564
+ const byCategory = {};
2565
+ const recentByCategory = {};
2566
+ let diagnosticsTotal = 0;
2567
+ let diagnostics24h = 0;
2568
+ let diagnostics1h = 0;
2569
+ let latestDiagnosticMs = Number.NEGATIVE_INFINITY;
2570
+ try {
2571
+ const diagnosticsDir = join(resolve(root), EVENT_V3_LEDGER_RELATIVE_ROOT, "diagnostics");
2572
+ if (existsSync(diagnosticsDir)) {
2573
+ const dayAgoMs = nowMs - 24 * 60 * 60 * 1000;
2574
+ const hourAgoMs = nowMs - 60 * 60 * 1000;
2575
+ for (const name of readdirSync(diagnosticsDir)) {
2576
+ const match = /^(.+)-(\d{15})-\d{20}-\d+-[0-9a-f-]+\.json$/.exec(name);
2577
+ const category = match?.[1];
2578
+ const epochMs = Number(match?.[2]);
2579
+ if (!category)
2580
+ continue;
2581
+ const entry = byCategory[category] ?? { total: 0, last_24h: 0 };
2582
+ const recent = recentByCategory[category] ?? { last_1h: 0, latest_at: null };
2583
+ byCategory[category] = entry;
2584
+ recentByCategory[category] = recent;
2585
+ entry.total += 1;
2586
+ diagnosticsTotal += 1;
2587
+ if (Number.isFinite(epochMs)) {
2588
+ const recordedAt = new Date(epochMs).toISOString();
2589
+ if (!recent.latest_at || epochMs > Date.parse(recent.latest_at)) {
2590
+ recent.latest_at = recordedAt;
2591
+ }
2592
+ latestDiagnosticMs = Math.max(latestDiagnosticMs, epochMs);
2593
+ if (epochMs >= dayAgoMs) {
2594
+ entry.last_24h += 1;
2595
+ diagnostics24h += 1;
2596
+ }
2597
+ if (epochMs >= hourAgoMs) {
2598
+ recent.last_1h += 1;
2599
+ diagnostics1h += 1;
2600
+ }
2601
+ }
2602
+ }
2603
+ }
2604
+ }
2605
+ catch (error) {
2606
+ collectionErrors.push(`diagnostics spool unreadable: ${errorText(error)}`);
2607
+ }
2608
+ return {
2609
+ state: "live",
2610
+ mode: control.state,
2611
+ open_spans: {
2612
+ total: generations.reduce((sum, generation) => sum + generation.span_count, 0),
2613
+ generations,
2614
+ },
2615
+ pending_finalizations: pending,
2616
+ intake_spool: { total: intakeTotal, groups: intakeGroups },
2617
+ diagnostics_spool: {
2618
+ total: diagnosticsTotal,
2619
+ last_24h: diagnostics24h,
2620
+ last_1h: diagnostics1h,
2621
+ latest_at: Number.isFinite(latestDiagnosticMs)
2622
+ ? new Date(latestDiagnosticMs).toISOString()
2623
+ : null,
2624
+ by_category: byCategory,
2625
+ recent_by_category: recentByCategory,
2626
+ },
2627
+ span_pressure: spanPressure,
2628
+ collection_errors: collectionErrors,
2629
+ };
2630
+ }
2214
2631
  /** Tally agent-hook failures (.harnery/debug/agent-hook.errors.ndjson) in the
2215
- * window, grouped by `phase`. Each line is {ts, error, phase, ...}. A dominant
2216
- * phase points straight at a systemic hook bug. */
2217
- function readHookErrors(root, cutoffMs) {
2632
+ * window by exact error and phase, while separating current-hour failures from
2633
+ * historical rows. Each line is {ts, error, phase, ...}. */
2634
+ export function readHookErrors(root, cutoffMs, nowMs = Date.now()) {
2218
2635
  const p = resolve(root, ".harnery", "debug", "agent-hook.errors.ndjson");
2219
2636
  const byPhase = {};
2637
+ const byError = {};
2220
2638
  const sampleByPhase = {};
2639
+ const errorPhaseCounts = new Map();
2640
+ const recentErrorPhaseCounts = new Map();
2221
2641
  let total = 0;
2222
- if (!existsSync(p))
2223
- return { total: 0, byPhase, top: [] };
2642
+ let last1h = 0;
2643
+ let latestMs = Number.NEGATIVE_INFINITY;
2644
+ if (!existsSync(p)) {
2645
+ return {
2646
+ total: 0,
2647
+ last1h: 0,
2648
+ latestAt: null,
2649
+ byPhase,
2650
+ byError,
2651
+ top: [],
2652
+ topErrors: [],
2653
+ recentTopErrors: [],
2654
+ };
2655
+ }
2224
2656
  let raw;
2225
2657
  try {
2226
2658
  raw = readFileSync(p, "utf8");
2227
2659
  }
2228
2660
  catch {
2229
- return { total: 0, byPhase, top: [] };
2661
+ return {
2662
+ total: 0,
2663
+ last1h: 0,
2664
+ latestAt: null,
2665
+ byPhase,
2666
+ byError,
2667
+ top: [],
2668
+ topErrors: [],
2669
+ recentTopErrors: [],
2670
+ };
2230
2671
  }
2672
+ const hourAgoMs = nowMs - 60 * 60 * 1000;
2231
2673
  for (const line of raw.split("\n")) {
2232
2674
  if (!line.trim())
2233
2675
  continue;
@@ -2237,9 +2679,22 @@ function readHookErrors(root, cutoffMs) {
2237
2679
  if (!Number.isFinite(tsMs) || tsMs < cutoffMs)
2238
2680
  continue;
2239
2681
  const phase = e.phase ?? "(unknown)";
2682
+ const error = e.error ?? "(unknown)";
2240
2683
  byPhase[phase] = (byPhase[phase] ?? 0) + 1;
2684
+ byError[error] = (byError[error] ?? 0) + 1;
2241
2685
  if (!sampleByPhase[phase] && e.error)
2242
2686
  sampleByPhase[phase] = e.error;
2687
+ const errorPhaseKey = `${error}\0${phase}`;
2688
+ const errorPhase = errorPhaseCounts.get(errorPhaseKey) ?? { error, phase, count: 0 };
2689
+ errorPhase.count += 1;
2690
+ errorPhaseCounts.set(errorPhaseKey, errorPhase);
2691
+ latestMs = Math.max(latestMs, tsMs);
2692
+ if (tsMs >= hourAgoMs) {
2693
+ last1h += 1;
2694
+ const recent = recentErrorPhaseCounts.get(errorPhaseKey) ?? { error, phase, count: 0 };
2695
+ recent.count += 1;
2696
+ recentErrorPhaseCounts.set(errorPhaseKey, recent);
2697
+ }
2243
2698
  total++;
2244
2699
  }
2245
2700
  catch {
@@ -2250,125 +2705,163 @@ function readHookErrors(root, cutoffMs) {
2250
2705
  .sort((a, b) => b[1] - a[1])
2251
2706
  .slice(0, 5)
2252
2707
  .map(([phase, count]) => ({ phase, count, sample: sampleByPhase[phase] ?? "" }));
2253
- return { total, byPhase, top };
2708
+ const topErrors = [...errorPhaseCounts.values()]
2709
+ .sort((left, right) => right.count - left.count || left.error.localeCompare(right.error))
2710
+ .slice(0, 5);
2711
+ const recentTopErrors = [...recentErrorPhaseCounts.values()]
2712
+ .sort((left, right) => right.count - left.count || left.error.localeCompare(right.error))
2713
+ .slice(0, 5);
2714
+ return {
2715
+ total,
2716
+ last1h,
2717
+ latestAt: Number.isFinite(latestMs) ? new Date(latestMs).toISOString() : null,
2718
+ byPhase,
2719
+ byError,
2720
+ top,
2721
+ topErrors,
2722
+ recentTopErrors,
2723
+ };
2254
2724
  }
2255
2725
  /** Canonical event stream size + drain lag (events appended after the cursor). */
2256
2726
  function readStreamStats(root) {
2257
- const streamPath = resolve(root, ".harnery", "events.ndjson");
2258
- if (!existsSync(streamPath))
2259
- return { bytes: 0, lines: 0, cursor_backlog: 0 };
2260
- let bytes = 0;
2261
- try {
2262
- bytes = statSync(streamPath).size;
2263
- }
2264
- catch {
2265
- /* ignore */
2266
- }
2267
- let cursor = null;
2268
- const cursorPath = resolve(root, ".harnery", ".events-cursor");
2269
- if (existsSync(cursorPath)) {
2270
- try {
2271
- cursor = readFileSync(cursorPath, "utf8").trim() || null;
2272
- }
2273
- catch {
2274
- /* ignore */
2275
- }
2276
- }
2277
- let lines = 0;
2278
- let backlog = 0;
2279
- let seenCursor = cursor === null; // no cursor → everything is "backlog"
2280
- try {
2281
- // Bounded tail read: the ledger grows without bound and a whole-file
2282
- // readFileSync throws past V8's ~512MB string limit. lines/backlog are then
2283
- // scoped to the scanned window; `bytes` (full size, via statSync above) still
2284
- // reflects the true stream size for the health rollup.
2285
- const { text } = readStreamTailBounded(streamPath, STREAM_SCAN_CAP_BYTES);
2286
- for (const line of text.split("\n")) {
2287
- if (!line.trim())
2288
- continue;
2289
- lines++;
2290
- if (seenCursor) {
2291
- backlog++;
2292
- }
2293
- else if (cursor && line.includes(`"event_id":"${cursor}"`)) {
2294
- seenCursor = true;
2295
- }
2296
- }
2297
- }
2298
- catch {
2299
- /* ignore */
2300
- }
2301
- return { bytes, lines, cursor_backlog: backlog };
2727
+ const read = readAgentDiagnosticEventsInWindow(root, 0);
2728
+ return {
2729
+ source: "v3",
2730
+ authoritative: read.authoritative,
2731
+ ...(read.reason ? { reason: read.reason } : {}),
2732
+ bytes: read.bytes,
2733
+ lines: read.events.length,
2734
+ cursor_backlog: 0,
2735
+ };
2736
+ }
2737
+ export function traceInstanceIdsForEventSource(nativeInstanceIds, source) {
2738
+ void source;
2739
+ return nativeInstanceIds.map((instanceId) => liveInstanceIdV3(instanceId));
2302
2740
  }
2303
2741
  /** Map a canonical event to a concise trace line, or null to drop it. */
2304
- function traceLine(ev, allTools) {
2305
- const d = (ev.data ?? {});
2742
+ export function traceLine(ev, allTools) {
2743
+ const d = (ev.payload ?? {});
2306
2744
  const s = (k) => (typeof d[k] === "string" ? d[k] : "");
2745
+ const object = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
2746
+ ? value
2747
+ : {};
2307
2748
  const clip = (v, n = 70) => (v.length <= n ? v : `${v.slice(0, n - 1)}…`);
2749
+ const recovery = typeof d.recovery === "object" && d.recovery !== null
2750
+ ? d.recovery
2751
+ : null;
2752
+ const recoveryReason = typeof recovery?.reason === "string" ? recovery.reason : "";
2753
+ const recoveryDetail = recoveryReason ? ` · RECOVERY reason=${recoveryReason}` : "";
2308
2754
  let detail = "";
2309
2755
  switch (ev.event_type) {
2310
- case "session.start":
2311
- detail = `${s("source") || "startup"}${s("model") ? ` · model=${s("model")}` : ""}${s("name") ? ` · ${s("name")}` : ""}`;
2756
+ case "session.started":
2757
+ detail = "generation started";
2758
+ break;
2759
+ case "session.resumed":
2760
+ detail = "generation resumed";
2312
2761
  break;
2313
- case "session.end":
2314
- detail = `clean_exit=${d.clean_exit ?? "?"}`;
2762
+ case "session.ended":
2763
+ detail = `outcome=${s("outcome") || "unknown"}${s("reason") ? ` · reason=${s("reason")}` : ""}`;
2315
2764
  break;
2316
- case "subagent.start":
2317
- detail = `${s("agent_type") || "subagent"}${s("name") ? ` · ${s("name")}` : ""}`;
2765
+ case "session.termination_observed":
2766
+ detail = `provisional ${s("observation") || "termination"}${s("reason") ? ` · reason=${s("reason")}` : ""}`;
2318
2767
  break;
2319
- case "subagent.stop":
2320
- detail = `clean_exit=${d.clean_exit ?? "?"}`;
2768
+ case "agent.delegated":
2769
+ detail = `delegated child=${s("child_generation_id") || "unknown"}`;
2321
2770
  break;
2322
- case "user_prompt.submit":
2323
- detail = clip(s("prompt_text") || s("prompt"));
2771
+ case "agent.started":
2772
+ detail = `child started=${s("child_generation_id") || "unknown"}`;
2324
2773
  break;
2325
- case "turn.stop":
2326
- detail = `status_box=${d.status_box_present ?? "?"}${s("turn_summary") ? ` · ${clip(s("turn_summary"), 50)}` : ""}`;
2774
+ case "agent.completed":
2775
+ detail = `child completed=${s("child_generation_id") || "unknown"} · outcome=${s("outcome") || "unknown"}`;
2327
2776
  break;
2328
- case "tool.pre_use":
2329
- detail = `${s("tool_name")}${s("tool_target") || s("intent") ? ` · ${clip(s("tool_target") || s("intent"), 60)}` : ""}`;
2777
+ case "turn.started":
2778
+ detail = `intent=${s("intent_kind") || "unknown"}`;
2330
2779
  break;
2331
- case "state.task_set":
2332
- detail = d.cleared ? "(cleared)" : clip(s("task"));
2780
+ case "turn.completed":
2781
+ detail = `outcome=${s("outcome") || "unknown"}`;
2333
2782
  break;
2334
- case "state.task_state":
2335
- detail = `${s("prior_state") || "active"} → ${s("state") || "active"}${s("reason") ? ` · ${clip(s("reason"), 55)}` : ""}`;
2783
+ case "tool.requested": {
2784
+ const tool = object(d.tool);
2785
+ detail = `${typeof tool.name === "string" ? tool.name : "tool"} requested (content omitted)${recoveryDetail}`;
2336
2786
  break;
2337
- case "interaction.input_requested":
2338
- detail = s("request_kind") || s("tool_name") || "operator input requested";
2787
+ }
2788
+ case "tool.completed": {
2789
+ const tool = object(d.tool);
2790
+ detail = `${typeof tool.name === "string" ? tool.name : "tool"} · outcome=${s("outcome") || "unknown"}${recoveryDetail}`;
2791
+ break;
2792
+ }
2793
+ case "command.started":
2794
+ if (!allTools)
2795
+ return null;
2796
+ detail = "command started (content omitted)";
2797
+ break;
2798
+ case "command.completed":
2799
+ if (!allTools && !recoveryReason)
2800
+ return null;
2801
+ detail = `outcome=${s("outcome") || "unknown"}${recoveryDetail}`;
2802
+ break;
2803
+ case "coord.task_changed":
2804
+ case "coord.lifecycle_changed":
2805
+ case "coord.presence_changed":
2806
+ detail = `${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}${s("reason") ? ` · ${clip(s("reason"), 55)}` : ""}`;
2807
+ break;
2808
+ case "coord.status_observed":
2809
+ detail = s("status") || "status observed";
2810
+ break;
2811
+ case "coord.claim_changed": {
2812
+ const target = object(d.target);
2813
+ detail = `${s("operation") || "changed"}${typeof target.display === "string" ? ` · ${clip(target.display)}` : ""}`;
2814
+ break;
2815
+ }
2816
+ case "coord.message_observed":
2817
+ detail = `${s("direction") || "observed"} · peer=${s("peer_instance_id") || "unknown"}`;
2339
2818
  break;
2340
- case "state.status_checked":
2341
- detail = "status box rendered";
2819
+ case "coord.identity_attested":
2820
+ detail = `${s("identity_id") || "identity"} · method=${s("method") || "unknown"}`;
2342
2821
  break;
2343
- case "identity.assumed":
2344
- detail = `${s("previous_name") || "unknown"} → ${s("name") || "unknown"}${s("agent_id") ? ` · persona=${s("agent_id").slice(0, 8)}…` : ""}`;
2822
+ case "wait.started":
2823
+ detail = s("kind") || "wait started";
2345
2824
  break;
2346
- case "claim.acquire":
2347
- case "claim.release":
2348
- case "claim.conflict":
2349
- detail = clip(s("path"));
2825
+ case "wait.ended":
2826
+ detail = `outcome=${s("outcome") || "unknown"}`;
2350
2827
  break;
2351
- case "health.heartbeat_heal":
2352
- case "health.pidmap_heal":
2353
- detail = `reason=${s("reason")}`;
2828
+ case "artifact.observed": {
2829
+ const artifact = object(d.artifact);
2830
+ detail = `operation=${s("operation") || "observed"}${typeof artifact.kind === "string" ? ` · kind=${artifact.kind}` : ""}`;
2354
2831
  break;
2355
- case "health.heartbeat_swept":
2356
- detail = `reason=${s("reason")}${d.age_secs !== undefined ? ` · age=${d.age_secs}s` : ""}`;
2832
+ }
2833
+ case "progress.observed":
2834
+ detail = s("kind") || "progress";
2835
+ break;
2836
+ case "health.observed":
2837
+ detail = `${s("subsystem") || "unknown"} · ${s("severity") || "unknown"} · ${s("condition") || "unknown"}`;
2838
+ break;
2839
+ case "council.state_changed":
2840
+ detail = `${s("council_id") || "council"} · ${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}`;
2841
+ break;
2842
+ case "decision.state_changed":
2843
+ detail = `${s("decision_id") || "decision"} · ${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}`;
2844
+ break;
2845
+ case "lifecycle.recovered":
2846
+ detail = `recovered=${s("recovery_kind") || "unknown"}`;
2847
+ break;
2848
+ case "lifecycle.sweep_observed":
2849
+ detail = `${s("observation") || "sweep"}${typeof d.age_ms === "number" ? ` · age=${Math.round(d.age_ms / 1000)}s` : ""}`;
2357
2850
  break;
2358
2851
  default:
2359
- // Noise unless --all-tools: per-line command.* + tool.post_use.
2360
- if (!allTools)
2361
- return null;
2362
- if (ev.event_type === "tool.post_use" ||
2363
- ev.event_type === "tool.post_use_failure" ||
2364
- ev.event_type.startsWith("command.")) {
2365
- detail = s("tool_name") || "";
2366
- break;
2367
- }
2368
2852
  return null;
2369
2853
  }
2370
2854
  return { ts: ev.ts, event_type: ev.event_type, detail };
2371
2855
  }
2856
+ export function pendingFinalizationTraceEntries(requests, instanceId) {
2857
+ return requests
2858
+ .filter((request) => request.status === "pending" && request.instance_id === instanceId)
2859
+ .map((request) => ({
2860
+ ts: request.observed_at,
2861
+ event_type: "session.finalization_pending",
2862
+ detail: `trigger=${request.trigger} · request=${request.request_id}${request.allowed_open_span_ids?.length ? ` · allowed_open_spans=${request.allowed_open_span_ids.length}` : ""}`,
2863
+ }));
2864
+ }
2372
2865
  function runTrace(name, opts) {
2373
2866
  if (opts.json)
2374
2867
  emit.config({ format: "json" });
@@ -2396,8 +2889,8 @@ function runTrace(name, opts) {
2396
2889
  targetId = candidates[0][0];
2397
2890
  else if (candidates.length > 1)
2398
2891
  targetId = candidates.map(([id]) => id).join("\x00"); // sentinel; resolved below
2399
- else if (/^[0-9a-f-]{8,}$/i.test(wanted))
2400
- targetId = wanted; // looks like an id not in history
2892
+ else if (/^(?:inst_)?[0-9a-z._-]{8,}$/i.test(wanted))
2893
+ targetId = wanted;
2401
2894
  }
2402
2895
  if (!targetId) {
2403
2896
  emit.error({
@@ -2408,33 +2901,16 @@ function runTrace(name, opts) {
2408
2901
  }
2409
2902
  const sinceMs = opts.since ? Date.now() - (parseWindowSecs(opts.since) ?? 0) * 1000 : 0;
2410
2903
  const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 200);
2411
- const candidateIds = targetId.includes("\x00") ? targetId.split("\x00") : [targetId];
2412
- // Scan the stream tail once, bucket events by instance_id for the candidates.
2413
- // Bounded read: the ledger grows without bound and a whole-file readFileSync
2414
- // throws past V8's ~512MB string limit. A trace of an agent whose events all
2415
- // predate the window is reported as truncated rather than crashing.
2416
- const streamPath = resolve(root, ".harnery", "events.ndjson");
2904
+ const diagnosticRead = readAgentDiagnosticEventsInWindow(root, sinceMs);
2905
+ const nativeCandidateIds = targetId.includes("\x00") ? targetId.split("\x00") : [targetId];
2906
+ const candidateIds = traceInstanceIdsForEventSource(nativeCandidateIds, diagnosticRead.source);
2417
2907
  const byId = new Map();
2418
- const { text: streamText, truncated: streamTruncated } = readStreamTailBounded(streamPath, STREAM_SCAN_CAP_BYTES);
2419
- for (const line of streamText.split("\n")) {
2420
- if (!line)
2908
+ for (const ev of diagnosticRead.events) {
2909
+ if (!ev.instance_id || !candidateIds.includes(ev.instance_id))
2421
2910
  continue;
2422
- try {
2423
- const ev = JSON.parse(line);
2424
- if (!ev.instance_id || !candidateIds.includes(ev.instance_id))
2425
- continue;
2426
- if (sinceMs && Date.parse(ev.ts) < sinceMs)
2427
- continue;
2428
- const arr = byId.get(ev.instance_id) ?? [];
2429
- arr.push(ev);
2430
- byId.set(ev.instance_id, arr);
2431
- }
2432
- catch {
2433
- /* skip */
2434
- }
2435
- }
2436
- if (streamTruncated) {
2437
- process.stderr.write(`note: event ledger exceeds ${Math.round(STREAM_SCAN_CAP_BYTES / 1024 / 1024)}MB; traced only the most recent window (older events omitted)\n`);
2911
+ const arr = byId.get(ev.instance_id) ?? [];
2912
+ arr.push(ev);
2913
+ byId.set(ev.instance_id, arr);
2438
2914
  }
2439
2915
  // If the name mapped to multiple instances, trace the one with the latest event.
2440
2916
  let resolvedId = candidateIds[0];
@@ -2450,24 +2926,78 @@ function runTrace(name, opts) {
2450
2926
  }
2451
2927
  }
2452
2928
  const events = byId.get(resolvedId) ?? [];
2453
- const state = foldSessionState(events, { instance_id: resolvedId });
2454
- const lines = events
2455
- .map((ev) => traceLine(ev, !!opts.allTools))
2456
- .filter((l) => l !== null)
2929
+ let state = foldSessionState(events, { instance_id: resolvedId });
2930
+ let sessionState = events.some((event) => event.event_type === "session.ended")
2931
+ ? "ended"
2932
+ : "live";
2933
+ if (diagnosticRead.source === "v3" && diagnosticRead.authoritative) {
2934
+ try {
2935
+ const view = projectCoordinationViewV3(readLedgerV3(root));
2936
+ const liveGeneration = view.instances[resolvedId];
2937
+ const terminalGeneration = Object.values(view.terminal_generations).find((candidate) => candidate.instance_id === resolvedId);
2938
+ const generation = liveGeneration ?? terminalGeneration;
2939
+ if (generation) {
2940
+ sessionState = terminalGeneration && !liveGeneration ? "ended" : "live";
2941
+ const lifecycleState = generation.lifecycle_state;
2942
+ state = {
2943
+ activity: generation.activity === "terminal" ? "idle" : generation.activity,
2944
+ activity_updated_at: generation.last_observed_at,
2945
+ activity_source: "event-v3-coordination-view",
2946
+ task_state: lifecycleState === "active" || lifecycleState === "blocked" || lifecycleState === "done"
2947
+ ? lifecycleState
2948
+ : "active",
2949
+ task_state_updated_at: generation.lifecycle_state_updated_at,
2950
+ };
2951
+ }
2952
+ }
2953
+ catch {
2954
+ diagnosticRead.authoritative = false;
2955
+ diagnosticRead.reason = "V3 coordination projection is unavailable";
2956
+ }
2957
+ }
2958
+ let pendingFinalizations = [];
2959
+ if (diagnosticRead.source === "v3") {
2960
+ try {
2961
+ pendingFinalizations = listSessionFinalizationRequestsV3(root).filter((request) => request.status === "pending" && request.instance_id === resolvedId);
2962
+ }
2963
+ catch {
2964
+ diagnosticRead.authoritative = false;
2965
+ diagnosticRead.reason = "V3 finalization requests are unreadable";
2966
+ }
2967
+ }
2968
+ const lines = [
2969
+ ...events
2970
+ .map((ev) => traceLine(ev, !!opts.allTools))
2971
+ .filter((l) => l !== null),
2972
+ ...pendingFinalizationTraceEntries(pendingFinalizations, resolvedId),
2973
+ ]
2457
2974
  // Sort by timestamp, not file order: codex replays events (original ts,
2458
2975
  // appended later), so append-order ≠ chronological order.
2459
2976
  .sort((a, b) => Date.parse(a.ts) - Date.parse(b.ts));
2460
2977
  const shown = lines.slice(-limit);
2461
- const displayName = nameById.get(resolvedId) ?? resolvedId.slice(0, 8);
2978
+ const nativeResolvedId = diagnosticRead.source === "v3"
2979
+ ? nativeCandidateIds[candidateIds.indexOf(resolvedId)]
2980
+ : resolvedId;
2981
+ const displayName = nameById.get(resolvedId) ??
2982
+ (nativeResolvedId ? nameById.get(nativeResolvedId) : undefined) ??
2983
+ resolvedId.slice(0, 8);
2462
2984
  const result = {
2985
+ event_source: {
2986
+ contract: diagnosticRead.source,
2987
+ authoritative: diagnosticRead.authoritative,
2988
+ reason: diagnosticRead.reason ?? null,
2989
+ },
2463
2990
  name: displayName,
2464
2991
  instance_id: resolvedId,
2465
2992
  other_instances: candidateIds.filter((id) => id !== resolvedId),
2993
+ pending_finalizations: pendingFinalizations,
2466
2994
  total_events: events.length,
2995
+ session_state: sessionState,
2467
2996
  activity: state.activity,
2468
2997
  activity_updated_at: state.activity_updated_at ?? null,
2469
2998
  activity_source: state.activity_source ?? null,
2470
2999
  task_state: state.task_state,
3000
+ task_state_scope: sessionState === "ended" ? "historical" : "current",
2471
3001
  task_state_updated_at: state.task_state_updated_at ?? null,
2472
3002
  task_state_reason: state.task_state_reason ?? null,
2473
3003
  shown: shown.length,
@@ -2480,7 +3010,10 @@ function runTrace(name, opts) {
2480
3010
  emit.data(result);
2481
3011
  const header = `Trace: agent-${displayName} (${resolvedId.slice(0, 8)}…) ${events.length} events${result.other_instances.length ? ` · ${result.other_instances.length} older instance(s) of this name` : ""}`;
2482
3012
  process.stdout.write(`${header}\n`); // lint-ok-emission: human trace view
2483
- process.stdout.write(` activity=${state.activity} · lifecycle=${state.task_state}${state.task_state === "blocked" && state.task_state_reason ? `: ${state.task_state_reason}` : ""}\n`); // lint-ok-emission: human trace view
3013
+ process.stdout.write(` activity=${state.activity} · session=${sessionState} · lifecycle=${sessionState === "ended" ? `historical(${state.task_state})` : state.task_state}${sessionState !== "ended" && state.task_state === "blocked" && state.task_state_reason ? `: ${state.task_state_reason}` : ""}\n`); // lint-ok-emission: human trace view
3014
+ if (!diagnosticRead.authoritative) {
3015
+ process.stdout.write(` unavailable: ${diagnosticRead.reason ?? "event source incomplete"}\n`); // lint-ok-emission: explicit no-fallback diagnostic
3016
+ }
2484
3017
  if (shown.length === 0) {
2485
3018
  process.stdout.write(" (no events)\n"); // lint-ok-emission: human trace view
2486
3019
  return;
@@ -2490,6 +3023,53 @@ function runTrace(name, opts) {
2490
3023
  process.stdout.write(` ${t} ${l.event_type.padEnd(22)} ${l.detail}\n`); // lint-ok-emission: human trace view
2491
3024
  }
2492
3025
  }
3026
+ /**
3027
+ * V3 generations, not disposable heartbeat caches, are the active-agent authority.
3028
+ */
3029
+ export function collectActiveAgentHealth(root, nowMs = Date.now()) {
3030
+ const source = "event-ledger-v3";
3031
+ let heartbeats = [];
3032
+ try {
3033
+ const control = readEventV3ControlState(root);
3034
+ if (control.state === "candidate" || control.state === "active") {
3035
+ heartbeats = readLiveCoordinationRows(root);
3036
+ }
3037
+ }
3038
+ catch {
3039
+ // The event-ledger section reports the authority read failure. Do not
3040
+ // substitute stale cache rows for a live V3 route.
3041
+ }
3042
+ const byPlatform = {};
3043
+ const byKind = {};
3044
+ const bySchema = {};
3045
+ let stale = 0;
3046
+ for (const heartbeat of heartbeats) {
3047
+ const platform = formatPlatformLabel(heartbeat.platform);
3048
+ byPlatform[platform] = (byPlatform[platform] ?? 0) + 1;
3049
+ const kind = heartbeat.kind ?? "unknown";
3050
+ byKind[kind] = (byKind[kind] ?? 0) + 1;
3051
+ // These rows exist only after an authority-safe V3 projection. The
3052
+ // disposable heartbeat file has its own cache-format version, which must
3053
+ // not be reported as the ledger schema version.
3054
+ bySchema.v3 = (bySchema.v3 ?? 0) + 1;
3055
+ const lastObservedMs = heartbeat.last_heartbeat
3056
+ ? Date.parse(heartbeat.last_heartbeat)
3057
+ : Number.NaN;
3058
+ const ageMs = Number.isFinite(lastObservedMs)
3059
+ ? nowMs - lastObservedMs
3060
+ : Number.POSITIVE_INFINITY;
3061
+ if (ageMs > freshnessCutoffSecs() * 1000)
3062
+ stale += 1;
3063
+ }
3064
+ return {
3065
+ source,
3066
+ total: heartbeats.length,
3067
+ by_platform: byPlatform,
3068
+ by_kind: byKind,
3069
+ by_schema_version: bySchema,
3070
+ stale,
3071
+ };
3072
+ }
2493
3073
  function runHealth(opts) {
2494
3074
  if (opts.json)
2495
3075
  emit.config({ format: "json" });
@@ -2509,11 +3089,12 @@ function runHealth(opts) {
2509
3089
  });
2510
3090
  process.exit(1);
2511
3091
  }
2512
- const cutoffMs = Date.now() - sinceSecs * 1000;
3092
+ const nowMs = Date.now();
3093
+ const cutoffMs = nowMs - sinceSecs * 1000;
2513
3094
  const activeDir = resolve(root, ".harnery/active");
2514
3095
  const councilsDir = resolve(root, ".harnery/councils");
2515
- // Coordination telemetry reads from the canonical events.ndjson stream.
2516
- // Heals come from health.*; council window-activity from council.*. The
3096
+ // Coordination telemetry reads from the canonical V3 ledger.
3097
+ // Heals come from health.observed; council activity from council.state_changed.
2517
3098
  const heal = [];
2518
3099
  let councilAdvanced = 0;
2519
3100
  let councilClosed = 0;
@@ -2521,32 +3102,35 @@ function runHealth(opts) {
2521
3102
  let sweptTotal = 0;
2522
3103
  const sweptByReason = {};
2523
3104
  const nameById = buildNameById(root);
2524
- for (const ev of readCanonicalEventsInWindow(root, cutoffMs)) {
3105
+ const diagnosticEvents = readAgentDiagnosticEventsInWindow(root, cutoffMs);
3106
+ for (const ev of diagnosticEvents.events) {
2525
3107
  const healEv = canonicalToHealEvent(ev, nameById);
2526
3108
  if (healEv) {
2527
3109
  heal.push(healEv);
2528
3110
  continue;
2529
3111
  }
2530
- switch (ev.event_type) {
2531
- case "council.round_open":
3112
+ if (ev.event_type === "council.state_changed") {
3113
+ const state = String(ev.payload?.new_state ?? "");
3114
+ if (state === "round_open")
2532
3115
  councilAdvanced++;
2533
- break;
2534
- case "council.close":
3116
+ if (state === "closed")
2535
3117
  councilClosed++;
2536
- break;
2537
- case "council.archive":
3118
+ if (state === "archived")
2538
3119
  councilArchived++;
2539
- break;
2540
- case "health.heartbeat_swept": {
3120
+ continue;
3121
+ }
3122
+ switch (ev.event_type) {
3123
+ case "lifecycle.sweep_observed": {
2541
3124
  sweptTotal++;
2542
- const reason = String(ev.data?.reason ?? "unknown");
3125
+ const reason = String(ev.payload?.observation ?? "unknown");
2543
3126
  sweptByReason[reason] = (sweptByReason[reason] ?? 0) + 1;
2544
3127
  break;
2545
3128
  }
2546
3129
  }
2547
3130
  }
2548
- const hookErrors = readHookErrors(root, cutoffMs);
3131
+ const hookErrors = readHookErrors(root, cutoffMs, nowMs);
2549
3132
  const stream = readStreamStats(root);
3133
+ const eventLedger = collectEventLedgerHealthV3(root);
2550
3134
  // Canonical health.* events carry the full instance_id, already resolved to
2551
3135
  // `agent-<name>` (or `agent-<hex8>` fallback) by canonicalToHealEvent via
2552
3136
  // buildNameById; no hex8→name dedup pass needed anymore.
@@ -2564,19 +3148,13 @@ function runHealth(opts) {
2564
3148
  .sort((a, b) => b[1] - a[1])
2565
3149
  .slice(0, 5)
2566
3150
  .map(([agent, count]) => ({ agent, count }));
2567
- // Active heartbeats: scan ALL files in active/, classify fresh vs stale ourselves.
2568
- const activeByPlatform = {};
2569
- const activeByKind = {};
2570
- const activeBySchema = {};
2571
- let activeTotal = 0;
2572
- let staleHeartbeats = 0;
3151
+ const activeAgents = collectActiveAgentHealth(root, nowMs);
2573
3152
  // Zombies: files in active/ that are broken: unparseable, nameless, or an
2574
3153
  // absurd (epoch-ish) last_heartbeat. These show as `agent-unknown` ghosts and
2575
3154
  // mean dead files the sweep isn't reaping.
2576
3155
  let zombieCount = 0;
2577
3156
  const zombieSamples = [];
2578
3157
  const ABSURD_AGE_MS = 24 * 60 * 60 * 1000; // > 1 day = clearly not a live, self-healing agent
2579
- const nowMs = Date.now();
2580
3158
  if (existsSync(activeDir)) {
2581
3159
  for (const file of readdirSync(activeDir)) {
2582
3160
  if (!file.endsWith(".json"))
@@ -2595,18 +3173,8 @@ function runHealth(opts) {
2595
3173
  zombieSamples.push(`${idFromFile.slice(0, 12)} (unparseable/no-id)`);
2596
3174
  continue;
2597
3175
  }
2598
- activeTotal++;
2599
- const platform = formatPlatformLabel(hb.platform);
2600
- activeByPlatform[platform] = (activeByPlatform[platform] ?? 0) + 1;
2601
- const kind = hb.kind ?? "unknown";
2602
- activeByKind[kind] = (activeByKind[kind] ?? 0) + 1;
2603
- const sv = hb.schema_version;
2604
- const schemaKey = sv === undefined ? "v0" : `v${sv}`;
2605
- activeBySchema[schemaKey] = (activeBySchema[schemaKey] ?? 0) + 1;
2606
3176
  const lastHbMs = hb.last_heartbeat ? Date.parse(hb.last_heartbeat) : Number.NaN;
2607
3177
  const ageMs = Number.isFinite(lastHbMs) ? nowMs - lastHbMs : Number.POSITIVE_INFINITY;
2608
- if (ageMs > freshnessCutoffSecs() * 1000)
2609
- staleHeartbeats++;
2610
3178
  // Zombie heuristics on a parseable heartbeat: no name, or an age so large
2611
3179
  // it can only be a broken/epoch timestamp (a real agent would have healed).
2612
3180
  if (!hb.name || hb.name === "unknown" || ageMs > ABSURD_AGE_MS) {
@@ -2636,46 +3204,65 @@ function runHealth(opts) {
2636
3204
  anomalies.push(`${agent} self-healed ${count}x in ${opts.since}; possible idle-prune loop or PID instability`);
2637
3205
  }
2638
3206
  }
2639
- if (staleHeartbeats > 0) {
2640
- anomalies.push(`${staleHeartbeats} active heartbeat(s) older than ${Math.floor(freshnessCutoffSecs() / 60)}min; heal mechanism may not be firing`);
3207
+ if (activeAgents.stale > 0) {
3208
+ const noun = activeAgents.source === "event-ledger-v3" ? "V3 generation" : "heartbeat";
3209
+ const action = activeAgents.source === "event-ledger-v3"
3210
+ ? "lifecycle reconciliation may be delayed"
3211
+ : "heal mechanism may not be firing";
3212
+ anomalies.push(`${activeAgents.stale} active ${noun}(s) without activity for ${Math.floor(freshnessCutoffSecs() / 60)}min; ${action}`);
2641
3213
  }
2642
- const unexpectedSchemas = Object.keys(activeBySchema).filter((k) => k !== "v1");
3214
+ const expectedSchema = "v3";
3215
+ const unexpectedSchemas = Object.keys(activeAgents.by_schema_version).filter((schema) => schema !== expectedSchema);
2643
3216
  if (unexpectedSchemas.length > 0) {
2644
- anomalies.push(`Unexpected heartbeat schema versions in use: ${unexpectedSchemas.join(", ")} (expected v1)`);
2645
- }
2646
- // agent-hook failures: a dominant phase is the fastest pointer to a systemic
2647
- // hook bug (this is the signal that would have surfaced the stop-projection
2648
- // crash immediately instead of after an hour of log-grepping).
2649
- if (hookErrors.total > 0) {
2650
- const top = hookErrors.top[0];
2651
- const detail = top
2652
- ? `: top phase '${top.phase}' x${top.count}${top.sample ? ` (${top.sample.slice(0, 80)})` : ""}`
2653
- : "";
2654
- anomalies.push(`agent-hook errored ${hookErrors.total}x in ${opts.since}${detail}`);
3217
+ anomalies.push(`Unexpected active-agent schema versions in use: ${unexpectedSchemas.join(", ")} (expected ${expectedSchema})`);
3218
+ }
3219
+ // Only recent hook failures are active anomalies. Historical rows remain in
3220
+ // the report with exact error signatures and their latest timestamp.
3221
+ if (hookErrors.last1h > 0) {
3222
+ const top = hookErrors.recentTopErrors[0];
3223
+ const detail = top ? `: top '${top.error.slice(0, 80)}' x${top.count} (${top.phase})` : "";
3224
+ anomalies.push(`agent-hook errored ${hookErrors.last1h}x in the last hour${detail}`);
2655
3225
  }
2656
3226
  if (stream.cursor_backlog > 500) {
2657
3227
  anomalies.push(`projection cursor is ${stream.cursor_backlog} events behind; drain lagging (stop projection may be failing)`);
2658
3228
  }
2659
- // NB: raw stream size is NOT an anomaly. events.ndjson is a deliberate
2660
- // append-only ledger (names + forensics for the life of the log), and
2661
- // consumeSince tail-reads it, so size no longer drives latency. The
2662
- // bytes/lines still surface in the summary line for visibility;
2663
- // cursor_backlog above is the real drain-lag signal.
3229
+ // Raw V3 stream size is not an anomaly; catalog rotation bounds readers.
2664
3230
  if ((sweptByReason.unparseable ?? 0) > 0) {
2665
3231
  anomalies.push(`${sweptByReason.unparseable} heartbeat(s) swept as unparseable in ${opts.since}; possible corruption or a non-atomic writer`);
2666
3232
  }
2667
3233
  if (zombieCount > 0) {
2668
3234
  anomalies.push(`${zombieCount} zombie heartbeat(s) in active/ (${zombieSamples.join(", ")}); broken files the sweep isn't reaping`);
2669
3235
  }
3236
+ if (eventLedger.state === "live") {
3237
+ // Open spans whose generation has no open turn: the turn ended without its
3238
+ // tool spans closing, so an explicit end can only queue, never finalize.
3239
+ const orphanGenerations = eventLedger.open_spans.generations.filter((generation) => !generation.turn_open);
3240
+ if (orphanGenerations.length > 0) {
3241
+ const orphanSpans = orphanGenerations.reduce((sum, generation) => sum + generation.span_count, 0);
3242
+ anomalies.push(`${orphanSpans} open tool span(s) across ${orphanGenerations.length} generation(s) with no open turn; orphaned spans block clean session end`);
3243
+ }
3244
+ for (const pressure of eventLedger.span_pressure) {
3245
+ anomalies.push(`${pressure.instance_id} (${pressure.generation_id}) holds ${pressure.span_count} open spans (soft watermark ${SPAN_PRESSURE_SOFT_WATERMARK}; the reader's 256-span cap makes the state unreadable)`);
3246
+ }
3247
+ for (const collectionError of eventLedger.collection_errors) {
3248
+ anomalies.push(`event-ledger health: ${collectionError}`);
3249
+ }
3250
+ }
2670
3251
  const report = {
2671
3252
  since: opts.since,
2672
3253
  generated_at: new Date().toISOString(),
3254
+ event_telemetry: {
3255
+ source: diagnosticEvents.source,
3256
+ authoritative: diagnosticEvents.authoritative,
3257
+ ...(diagnosticEvents.reason ? { reason: diagnosticEvents.reason } : {}),
3258
+ },
2673
3259
  active_agents: {
2674
- total: activeTotal,
2675
- by_platform: activeByPlatform,
2676
- by_kind: activeByKind,
2677
- by_schema_version: activeBySchema,
2678
- stale: staleHeartbeats,
3260
+ source: activeAgents.source,
3261
+ total: activeAgents.total,
3262
+ by_platform: activeAgents.by_platform,
3263
+ by_kind: activeAgents.by_kind,
3264
+ by_schema_version: activeAgents.by_schema_version,
3265
+ stale: activeAgents.stale,
2679
3266
  },
2680
3267
  heal_events: {
2681
3268
  total: heal.length,
@@ -2691,9 +3278,19 @@ function runHealth(opts) {
2691
3278
  closed_in_window: councilClosed,
2692
3279
  },
2693
3280
  swept_events: { total: sweptTotal, by_reason: sweptByReason },
2694
- hook_errors: { total: hookErrors.total, by_phase: hookErrors.byPhase, top: hookErrors.top },
3281
+ hook_errors: {
3282
+ total: hookErrors.total,
3283
+ last_1h: hookErrors.last1h,
3284
+ latest_at: hookErrors.latestAt,
3285
+ by_phase: hookErrors.byPhase,
3286
+ by_error: hookErrors.byError,
3287
+ top: hookErrors.top,
3288
+ top_errors: hookErrors.topErrors,
3289
+ recent_top_errors: hookErrors.recentTopErrors,
3290
+ },
2695
3291
  stream,
2696
3292
  zombies: { count: zombieCount, samples: zombieSamples },
3293
+ event_ledger: eventLedger,
2697
3294
  anomalies,
2698
3295
  };
2699
3296
  if (opts.json) {
@@ -2729,16 +3326,38 @@ function renderHealthBox(report) {
2729
3326
  councilParts.push(`${report.councils.closed_in_window} closed`);
2730
3327
  if (report.councils.archived_in_window > 0)
2731
3328
  councilParts.push(`${report.councils.archived_in_window} archived`);
2732
- const activeStr = `${report.active_agents.total}${platforms ? ` (${platforms})` : ""}${schemas ? ` · ${schemas}` : ""}${report.active_agents.stale > 0 ? ` · ${report.active_agents.stale} stale` : ""}`;
3329
+ const activeSource = report.active_agents.source === "event-ledger-v3" ? "ledger" : "heartbeat cache";
3330
+ const activeStr = `${report.active_agents.total}${platforms ? ` (${platforms})` : ""}${schemas ? ` · ${schemas}` : ""}${report.active_agents.stale > 0 ? ` · ${report.active_agents.stale} stale` : ""} · ${activeSource}`;
2733
3331
  const sweptReasonStr = Object.entries(report.swept_events.by_reason)
2734
3332
  .map(([r, n]) => `${r} ${n}`)
2735
3333
  .join(", ");
2736
3334
  const hookErrStr = report.hook_errors.total === 0
2737
3335
  ? "0"
2738
- : `${report.hook_errors.total}${report.hook_errors.top[0] ? ` (${report.hook_errors.top[0].phase} x${report.hook_errors.top[0].count})` : ""}`;
3336
+ : `${report.hook_errors.total} in window · ${report.hook_errors.last_1h} in 1h`;
2739
3337
  const streamStr = `${(report.stream.bytes / 1048576).toFixed(1)}MB · ${report.stream.lines} lines · ${report.stream.cursor_backlog} behind`;
3338
+ const ledger = report.event_ledger;
3339
+ let ledgerStr;
3340
+ if (ledger.state !== "live") {
3341
+ ledgerStr = `unavailable (${ledger.reason})`;
3342
+ }
3343
+ else {
3344
+ const orphanGenerations = ledger.open_spans.generations.filter((generation) => !generation.turn_open).length;
3345
+ const pendingAges = ledger.pending_finalizations
3346
+ .map((request) => request.age_ms < 0 ? "age?" : formatAge(Math.floor(request.age_ms / 1000)))
3347
+ .join(", ");
3348
+ ledgerStr = [
3349
+ `open spans ${ledger.open_spans.total}${orphanGenerations > 0 ? ` (${orphanGenerations} gen turn-closed)` : ""}`,
3350
+ `pending ends ${ledger.pending_finalizations.length}${pendingAges ? ` (${pendingAges})` : ""}`,
3351
+ `intake ${ledger.intake_spool.total}`,
3352
+ `diagnostics ${ledger.diagnostics_spool.total} (${ledger.diagnostics_spool.last_1h} in 1h / ${ledger.diagnostics_spool.last_24h} in 24h)`,
3353
+ ].join(" · ");
3354
+ }
2740
3355
  const rows = [
2741
3356
  ["window", `last ${report.since}`],
3357
+ [
3358
+ "event source",
3359
+ `${report.event_telemetry.source.toUpperCase()} · ${report.event_telemetry.authoritative ? "validated" : `unavailable (${report.event_telemetry.reason ?? "incomplete"})`}`,
3360
+ ],
2742
3361
  ["active", activeStr],
2743
3362
  [
2744
3363
  "heals",
@@ -2748,6 +3367,7 @@ function renderHealthBox(report) {
2748
3367
  ["swept", `${report.swept_events.total}${sweptReasonStr ? ` (${sweptReasonStr})` : ""}`],
2749
3368
  ["hook errors", hookErrStr],
2750
3369
  ["stream", streamStr],
3370
+ ["event ledger", ledgerStr],
2751
3371
  [
2752
3372
  "zombies",
2753
3373
  report.zombies.count === 0
@@ -2851,6 +3471,8 @@ function runAdapterProbe(id, opts) {
2851
3471
  replayExitCode = result.exitCode;
2852
3472
  data.samples = samples;
2853
3473
  data.samples_summary = result.summary;
3474
+ if (result.note)
3475
+ data.samples_note = result.note;
2854
3476
  }
2855
3477
  emit.data(data);
2856
3478
  if (process.stdout.isTTY && !opts.json) {
@@ -2867,7 +3489,7 @@ function runAdapterProbe(id, opts) {
2867
3489
  const summary = data.samples_summary;
2868
3490
  lines.push("");
2869
3491
  if (samples.length === 0) {
2870
- lines.push(` Sample replay: no .json fixtures found under ${sampleDir}`);
3492
+ lines.push(` Sample replay: ${String(data.samples_note ?? `no .json fixtures found under ${sampleDir}`)}`);
2871
3493
  }
2872
3494
  else {
2873
3495
  lines.push(` Sample replay (${samples.length} fixture${samples.length === 1 ? "" : "s"}):`);
@@ -2918,6 +3540,7 @@ function replayAdapterSamples(adapter, root, relativeSampleDir, filter) {
2918
3540
  samples: [],
2919
3541
  exitCode: 0,
2920
3542
  summary: { total: 0, pass: 0, fail: 0, skipped: 0 },
3543
+ note: `sample directory not found: ${relativeSampleDir} — replay verified nothing`,
2921
3544
  };
2922
3545
  }
2923
3546
  const fixtures = readdirSync(sampleDir)
@@ -2929,6 +3552,7 @@ function replayAdapterSamples(adapter, root, relativeSampleDir, filter) {
2929
3552
  samples: [],
2930
3553
  exitCode: 0,
2931
3554
  summary: { total: 0, pass: 0, fail: 0, skipped: 0 },
3555
+ note: `no .json fixtures under ${relativeSampleDir} — replay verified nothing`,
2932
3556
  };
2933
3557
  }
2934
3558
  // agent-hook is the single entry point. Replay sample payloads against it,
@@ -3143,10 +3767,24 @@ function runPing(name, message, opts) {
3143
3767
  });
3144
3768
  process.exit(1);
3145
3769
  }
3146
- const myHb = readHeartbeat(myOwner);
3770
+ const myHb = readCurrentCoordinationRow(myOwner);
3147
3771
  const fromName = myHb?.name ?? "anonymous";
3148
3772
  const body = `from agent-${fromName}: ${message.trim()}`;
3149
3773
  const doc = appendEntry(peerOwner, "handoff", body);
3774
+ // Canonical delivery record: sender is the envelope owner, recipient rides
3775
+ // in data, so read-only observers can render the communication without
3776
+ // joining journal files.
3777
+ emitEventV3({
3778
+ owner: myOwner,
3779
+ session: nativeSessionIdentity(myHb, myOwner),
3780
+ adapter: normalizeAdapter(myHb?.platform),
3781
+ observation: {
3782
+ event_type: "coord.message_observed",
3783
+ direction: "sent",
3784
+ subject: peerOwner,
3785
+ body: message.trim(),
3786
+ },
3787
+ });
3150
3788
  const data = {
3151
3789
  peer: name,
3152
3790
  peer_instance_id: peerOwner,
@@ -3195,7 +3833,7 @@ async function runWait(name, opts) {
3195
3833
  }
3196
3834
  let lastProgressMs = 0;
3197
3835
  while (true) {
3198
- const hb = readHeartbeat(peerOwner);
3836
+ const hb = readCurrentCoordinationRow(peerOwner);
3199
3837
  const now = Date.now();
3200
3838
  const elapsedMs = now - startMs;
3201
3839
  if (!hb) {
@@ -3230,9 +3868,8 @@ async function runWait(name, opts) {
3230
3868
  const progressGapMs = Math.max(pollMs, 30_000);
3231
3869
  if (!opts.quiet && now - lastProgressMs >= progressGapMs) {
3232
3870
  lastProgressMs = now;
3233
- const lastTool = hb.last_tool ? `, last=${hb.last_tool}` : "";
3234
3871
  const elapsedStr = formatAge(Math.floor(elapsedMs / 1000));
3235
- const progress = ` [${elapsedStr}] ${stillBlocking.length} file(s) blocking${lastTool}\n`;
3872
+ const progress = ` [${elapsedStr}] ${stillBlocking.length} file(s) blocking\n`;
3236
3873
  process.stderr.write(progress); // lint-ok-emission: per-poll progress heartbeat to stderr
3237
3874
  }
3238
3875
  await new Promise((r) => setTimeout(r, pollMs));
@@ -3267,10 +3904,20 @@ function runHeal(opts) {
3267
3904
  emit.error({ code: "missing_owner", message: "--owner is required" });
3268
3905
  process.exit(1);
3269
3906
  }
3270
- if (kind !== "pidmap" && kind !== "heartbeat" && kind !== "kill") {
3907
+ if (kind !== "pidmap" && kind !== "cache") {
3271
3908
  emit.error({
3272
3909
  code: "bad_kind",
3273
- message: "--kind must be one of: pidmap, heartbeat, kill",
3910
+ message: "--kind must be one of: pidmap, cache",
3911
+ });
3912
+ process.exit(1);
3913
+ }
3914
+ if (opts.adapter &&
3915
+ opts.adapter !== "claude-code" &&
3916
+ opts.adapter !== "cursor" &&
3917
+ opts.adapter !== "codex") {
3918
+ emit.error({
3919
+ code: "bad_adapter",
3920
+ message: "--adapter must be one of: claude-code, cursor, codex",
3274
3921
  });
3275
3922
  process.exit(1);
3276
3923
  }
@@ -3282,8 +3929,8 @@ function runHeal(opts) {
3282
3929
  });
3283
3930
  process.exit(1);
3284
3931
  }
3285
- const action = kind === "pidmap" ? "heal-pidmap" : kind === "heartbeat" ? "heal-heartbeat" : "kill-heartbeat";
3286
- // Refuse to mint a heartbeat at a TRUNCATED owner id.
3932
+ const action = kind === "pidmap" ? "heal-pidmap" : "repair-coordination-cache";
3933
+ // Refuse to materialize a V3 cache at a truncated owner id.
3287
3934
  //
3288
3935
  // Heartbeats are keyed by the whole instance_id, so healing at an
3289
3936
  // abbreviated id writes `.harnery/active/<prefix>.json` while every reader
@@ -3303,32 +3950,31 @@ function runHeal(opts) {
3303
3950
  // whose instance_id this id is a prefix of settles it just as well, and is
3304
3951
  // present in precisely the case that matters (the session the reader was
3305
3952
  // trying to heal is registered, just not under the abbreviated name).
3306
- if (kind === "heartbeat" && !readHeartbeat(owner)) {
3953
+ if (kind === "cache" && !readHeartbeatCache(root, owner)) {
3307
3954
  const canonical = opts.sessionId && opts.sessionId.trim() !== owner && opts.sessionId.trim().startsWith(owner)
3308
3955
  ? opts.sessionId.trim()
3309
3956
  : liveIdWithPrefix(root, owner);
3310
3957
  if (canonical) {
3311
- const source = opts.sessionId?.trim() === canonical ? "--session-id" : "a live heartbeat";
3958
+ const source = opts.sessionId?.trim() === canonical ? "--session-id" : "a live V3 generation";
3312
3959
  emit.error({
3313
3960
  code: "truncated_owner",
3314
- message: `--owner ${owner} is a prefix of ${canonical} (${source}), and no heartbeat exists ` +
3315
- `at .harnery/active/${owner}.json. Healing here would create a heartbeat no reader ` +
3316
- `resolves. Re-run with --owner ${canonical}.`,
3961
+ message: `--owner ${owner} is a prefix of ${canonical} (${source}). Cache repair requires ` +
3962
+ `the canonical instance id; re-run with --owner ${canonical}.`,
3317
3963
  });
3318
3964
  process.exit(1);
3319
3965
  }
3320
3966
  }
3321
3967
  // Build positional args. agent-coord's arg layout:
3322
3968
  // heal-pidmap <instance_id> [<pid>]
3323
- // heal-heartbeat <instance_id> [<session_id>]
3324
- // kill-heartbeat <instance_id>
3969
+ // repair-coordination-cache <instance_id> [<session_id>]
3325
3970
  const helperArgs = [action, owner];
3326
3971
  if (kind === "pidmap" && opts.pid)
3327
3972
  helperArgs.push(opts.pid);
3328
- if (kind === "heartbeat" && opts.sessionId)
3973
+ if (kind === "cache" && opts.sessionId)
3329
3974
  helperArgs.push(opts.sessionId);
3330
- // heal-pidmap / heal-heartbeat / kill-heartbeat are handled by the
3331
- // bundled agent-coord binary.
3975
+ if (kind === "cache" && opts.adapter)
3976
+ helperArgs.push(`--adapter=${opts.adapter}`);
3977
+ // Both recovery actions are handled by the bundled agent-coord binary.
3332
3978
  const helper = agentCoordOrExit(root);
3333
3979
  const proc = spawnSync(helper, helperArgs, {
3334
3980
  encoding: "utf8",
@@ -3341,26 +3987,21 @@ function runHeal(opts) {
3341
3987
  });
3342
3988
  process.exit(1);
3343
3989
  }
3344
- // Re-read the heartbeat to surface post-action state (or null if killed).
3990
+ // Read the derived cache to surface post-action state.
3345
3991
  let after = null;
3346
3992
  try {
3347
- after = readHeartbeat(owner);
3993
+ after = readHeartbeatCache(root, owner);
3348
3994
  }
3349
3995
  catch {
3350
3996
  after = null;
3351
3997
  }
3352
- // Outcome semantics differ per kind. kill-heartbeat targets the heartbeat
3353
- // file directly; heal-heartbeat upserts it; heal-pidmap touches a pid-map
3354
- // row whose existence is independent of the heartbeat. Reporting on
3355
- // "heartbeat present after" for the pidmap path was misleading: it's
3356
- // unrelated to whether the heal succeeded.
3357
3998
  const outcome = kind === "pidmap"
3358
3999
  ? proc.status === 0
3359
4000
  ? "ok"
3360
4001
  : "failed"
3361
4002
  : after
3362
- ? "heartbeat_present"
3363
- : "heartbeat_absent";
4003
+ ? "cache_present"
4004
+ : "cache_absent";
3364
4005
  emit.data({
3365
4006
  rows: [
3366
4007
  {
@@ -3383,31 +4024,35 @@ function runHeal(opts) {
3383
4024
  emit.text(`agent-coord ${action} ok\n`);
3384
4025
  }
3385
4026
  else {
3386
- emit.text(`agent-coord ${action} ok: heartbeat ${after ? "present" : "absent"} after\n`);
4027
+ emit.text(`agent-coord ${action} ok: cache ${after ? "present" : "absent"} after\n`);
3387
4028
  }
3388
4029
  }
3389
- // Canonical health.* emission is owned by the writer (heartbeat-writer.ts
3390
- // healPidmap/healHeartbeat), so it fires inside the agent-coord subprocess
4030
+ // Canonical health.* emission is owned by the cache writer, so it fires
4031
+ // inside the agent-coord subprocess
3391
4032
  // above on actual writes only: write-only telemetry, no double-emit, no
3392
4033
  // event when an already-correct heal no-ops. (Previously emitted here
3393
4034
  // unconditionally on every `harn agents heal`, which over-counted no-op heals.)
3394
4035
  }
3395
4036
  /**
3396
- * Shared emitter for council.* events. Looks up the running agent's
3397
- * heartbeat so each event carries a real instance_id / session_id; falls
3398
- * through silently if no session (CI / direct invocation).
4037
+ * Emit one V3 council audit transition keyed to the exact durable manifest.
4038
+ * Falls through silently when no live session can attest the observation.
3399
4039
  */
3400
- function emitCouncilStateEvent(type, manifest, extraData) {
4040
+ function emitCouncilStateEvent(manifest, newState, priorState) {
3401
4041
  const myOwner = resolveOwner();
3402
4042
  if (!myOwner)
3403
4043
  return;
3404
- const hb = readHeartbeat(myOwner);
3405
- emitCanonical({
3406
- type,
4044
+ const hb = readCurrentCoordinationRow(myOwner);
4045
+ emitEventV3({
3407
4046
  owner: myOwner,
3408
- session: hb?.session_id ?? myOwner,
4047
+ session: nativeSessionIdentity(hb, myOwner),
3409
4048
  adapter: normalizeAdapter(hb?.platform),
3410
- data: { council_id: manifest.council_id, ...extraData },
4049
+ observation: {
4050
+ event_type: "council.state_changed",
4051
+ council_id: manifest.council_id,
4052
+ ...(priorState ? { prior_state: priorState } : {}),
4053
+ new_state: newState,
4054
+ record: manifest,
4055
+ },
3411
4056
  });
3412
4057
  }
3413
4058
  // ──────── council subcommand impls ────────
@@ -3450,7 +4095,7 @@ function runCouncilCreate(objective, opts) {
3450
4095
  createdBy = normalizeAgentName(opts.createdBy);
3451
4096
  }
3452
4097
  else if (myOwner) {
3453
- const myHb = readHeartbeat(myOwner);
4098
+ const myHb = readCurrentCoordinationRow(myOwner);
3454
4099
  if (myHb?.name) {
3455
4100
  createdBy = normalizeAgentName(myHb.name);
3456
4101
  }
@@ -3506,17 +4151,16 @@ function runCouncilCreate(objective, opts) {
3506
4151
  writeFileSync(resolve(body, "invite.md"), buildInviteMarkdown(manifest), "utf8");
3507
4152
  writeManifest(manifest);
3508
4153
  if (myOwner) {
3509
- const myHbForEmit = readHeartbeat(myOwner);
3510
- emitCanonical({
3511
- type: "council.open",
4154
+ const myHbForEmit = readCurrentCoordinationRow(myOwner);
4155
+ emitEventV3({
3512
4156
  owner: myOwner,
3513
- session: myHbForEmit?.session_id ?? myOwner,
4157
+ session: nativeSessionIdentity(myHbForEmit, myOwner),
3514
4158
  adapter: normalizeAdapter(myHbForEmit?.platform),
3515
- data: {
3516
- council_id: councilId,
3517
- topic: trimmedObjective,
3518
- members,
3519
- target_doc: manifest.target_doc ?? undefined,
4159
+ observation: {
4160
+ event_type: "council.state_changed",
4161
+ council_id: manifest.council_id,
4162
+ new_state: "active",
4163
+ record: manifest,
3520
4164
  },
3521
4165
  });
3522
4166
  }
@@ -3528,7 +4172,7 @@ function runCouncilCreate(objective, opts) {
3528
4172
  const skippedMembers = [];
3529
4173
  for (const memberName of members) {
3530
4174
  const bareName = memberName.replace(/^agent-/, "");
3531
- if (myOwner && readHeartbeat(myOwner)?.name === bareName) {
4175
+ if (myOwner && readCurrentCoordinationRow(myOwner)?.name === bareName) {
3532
4176
  // Convener is themselves a member; skip the self-ping
3533
4177
  continue;
3534
4178
  }
@@ -3585,7 +4229,7 @@ function runCouncilList(opts) {
3585
4229
  if (opts.mine) {
3586
4230
  const myOwner = resolveOwner();
3587
4231
  if (myOwner) {
3588
- const myHb = readHeartbeat(myOwner);
4232
+ const myHb = readCurrentCoordinationRow(myOwner);
3589
4233
  if (myHb?.name)
3590
4234
  myName = normalizeAgentName(myHb.name);
3591
4235
  }
@@ -3749,7 +4393,7 @@ function runCouncilClose(id, opts) {
3749
4393
  closed_at: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
3750
4394
  };
3751
4395
  writeManifest(next);
3752
- emitCouncilStateEvent("council.close", next, { closed_at: next.closed_at });
4396
+ emitCouncilStateEvent(next, "closed", manifest.status);
3753
4397
  // Build the transcript: every round's contributions in order.
3754
4398
  const transcript = buildTranscript(next);
3755
4399
  emit.data({
@@ -3788,7 +4432,7 @@ function runCouncilArchive(id, opts) {
3788
4432
  // updated status. moveToArchive then physically relocates the artifacts.
3789
4433
  writeManifest(next);
3790
4434
  moveToArchive(next.council_id);
3791
- emitCouncilStateEvent("council.archive", next, {});
4435
+ emitCouncilStateEvent(next, "archived", manifest.status);
3792
4436
  emit.data({
3793
4437
  rows: [
3794
4438
  {
@@ -3842,7 +4486,7 @@ function runCouncilUnarchive(id, opts) {
3842
4486
  // (allows re-running for testing).
3843
4487
  moveFromArchive(next.council_id);
3844
4488
  writeManifest(next);
3845
- emitCouncilStateEvent("council.unarchive", next, { restored_status: restoredStatus });
4489
+ emitCouncilStateEvent(next, restoredStatus, "archived");
3846
4490
  emit.data({
3847
4491
  rows: [
3848
4492
  {
@@ -3895,7 +4539,7 @@ function runCouncilDelete(id, opts) {
3895
4539
  }
3896
4540
  const removed = deleteArchivedCouncil(manifest.council_id);
3897
4541
  if (removed) {
3898
- emitCouncilStateEvent("council.delete", manifest, {});
4542
+ emitCouncilStateEvent(manifest, "deleted", "archived");
3899
4543
  }
3900
4544
  emit.data({
3901
4545
  rows: [
@@ -4067,7 +4711,7 @@ function runCouncilContribute(id, opts) {
4067
4711
  try {
4068
4712
  const myOwner = resolveOwner();
4069
4713
  if (myOwner) {
4070
- const myHb = readHeartbeat(myOwner);
4714
+ const myHb = readCurrentCoordinationRow(myOwner);
4071
4715
  if (myHb?.name)
4072
4716
  actualName = normalizeAgentName(myHb.name);
4073
4717
  }
@@ -4085,7 +4729,7 @@ function runCouncilContribute(id, opts) {
4085
4729
  });
4086
4730
  process.exit(1);
4087
4731
  }
4088
- const myHb = readHeartbeat(myOwner);
4732
+ const myHb = readCurrentCoordinationRow(myOwner);
4089
4733
  if (!myHb?.name) {
4090
4734
  emit.error({
4091
4735
  code: "no_self_name",
@@ -4136,11 +4780,7 @@ function runCouncilContribute(id, opts) {
4136
4780
  process.exit(1);
4137
4781
  }
4138
4782
  const path = writeContribution(manifest.council_id, manifest.current_round, myName, body);
4139
- emitCouncilStateEvent("council.contribution", manifest, {
4140
- round_no: manifest.current_round,
4141
- member: myName,
4142
- body_summary: body.length > 1000 ? `${body.slice(0, 997)}...` : body,
4143
- });
4783
+ emitCouncilStateEvent(manifest, "contribution_recorded", manifest.round_status);
4144
4784
  // Update manifest: if all members have now contributed, flip round_status.
4145
4785
  const contributorsNow = contributorsInRound(manifest.council_id, manifest.current_round);
4146
4786
  const allIn = manifest.members.every((m) => contributorsNow.includes(m));
@@ -4149,14 +4789,10 @@ function runCouncilContribute(id, opts) {
4149
4789
  if (allIn) {
4150
4790
  nextManifest = { ...manifest, round_status: "collected" };
4151
4791
  writeManifest(nextManifest);
4152
- emitCouncilStateEvent("council.round_close", nextManifest, {
4153
- round_no: nextManifest.current_round,
4154
- });
4792
+ emitCouncilStateEvent(nextManifest, "round_closed", manifest.round_status);
4155
4793
  if (manifest.auto_advance) {
4156
4794
  nextManifest = advanceCouncil(nextManifest, /*force=*/ false);
4157
- emitCouncilStateEvent("council.round_open", nextManifest, {
4158
- round_no: nextManifest.current_round,
4159
- });
4795
+ emitCouncilStateEvent(nextManifest, "round_open", "round_closed");
4160
4796
  autoAdvanced = true;
4161
4797
  }
4162
4798
  }
@@ -4241,7 +4877,7 @@ function runCouncilPrompt(id, member, opts) {
4241
4877
  try {
4242
4878
  const myOwner = resolveOwner();
4243
4879
  if (myOwner) {
4244
- const myHb = readHeartbeat(myOwner);
4880
+ const myHb = readCurrentCoordinationRow(myOwner);
4245
4881
  if (myHb?.name)
4246
4882
  actualName = normalizeAgentName(myHb.name);
4247
4883
  }
@@ -4259,7 +4895,7 @@ function runCouncilPrompt(id, member, opts) {
4259
4895
  });
4260
4896
  process.exit(1);
4261
4897
  }
4262
- const myHb = readHeartbeat(myOwner);
4898
+ const myHb = readCurrentCoordinationRow(myOwner);
4263
4899
  if (!myHb?.name) {
4264
4900
  emit.error({
4265
4901
  code: "no_self_name",
@@ -4410,12 +5046,8 @@ function runCouncilAdvance(id, opts) {
4410
5046
  process.exit(1);
4411
5047
  }
4412
5048
  const next = advanceCouncil(manifest, !!opts.force);
4413
- emitCouncilStateEvent("council.round_close", manifest, {
4414
- round_no: manifest.current_round,
4415
- });
4416
- emitCouncilStateEvent("council.round_open", next, {
4417
- round_no: next.current_round,
4418
- });
5049
+ emitCouncilStateEvent(manifest, "round_closed", manifest.round_status);
5050
+ emitCouncilStateEvent(next, "round_open", "round_closed");
4419
5051
  emit.data({
4420
5052
  rows: [
4421
5053
  {
@@ -4457,7 +5089,7 @@ function advanceCouncil(manifest, force) {
4457
5089
  // (Convener already knows; we skip pinging them if they convened it from
4458
5090
  // their own session.)
4459
5091
  const myOwner = resolveOwner();
4460
- const myName = myOwner ? normalizeAgentName(readHeartbeat(myOwner)?.name ?? "") : "";
5092
+ const myName = myOwner ? normalizeAgentName(readCurrentCoordinationRow(myOwner)?.name ?? "") : "";
4461
5093
  for (const memberName of next.members) {
4462
5094
  if (memberName === myName)
4463
5095
  continue;
@@ -4478,7 +5110,7 @@ function advanceCouncil(manifest, force) {
4478
5110
  // Hard cap on box width. Values longer than the per-row budget word-wrap to
4479
5111
  // continuation lines (blank key column, value resumes indented). Picked to
4480
5112
  // stay readable in narrow terminals + chat clients while giving long
4481
- // turn_summary / task values room to breathe.
5113
+ // Task values need room to breathe.
4482
5114
  const MAX_BOX_CONTENT_WIDTH = 100;
4483
5115
  function formatBox(title, rows) {
4484
5116
  const titleStr = ` ${title} `;