harnery 0.33.0 → 0.34.1

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 +1237 -590
  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 +306 -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 +161 -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 +1530 -593
  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 +352 -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 +171 -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,40 +1373,43 @@ 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
1258
1380
  // declaration or a `--session-id` relay both keep the window open instead).
1259
- const suggestedName = !priorHb?.suggested_session_name && hb?.suggested_session_name
1381
+ const firstSuggestedName = !priorHb?.suggested_session_name && hb?.suggested_session_name
1260
1382
  ? hb.suggested_session_name
1261
1383
  : null;
1262
- 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
- });
1384
+ // A readable but misclassified first display would otherwise leave the
1385
+ // session permanently latched: set-task is allowed through the gate, but a
1386
+ // normal repeat used to return no name and therefore could not establish a
1387
+ // fresh ordered transcript boundary. Re-emit the unchanged pending name as
1388
+ // an explicit retry. This does not rename the session or claim that the
1389
+ // display was seen; it only gives the next exact block new mint evidence.
1390
+ const retriedSuggestedName = !firstSuggestedName &&
1391
+ priorHb?.suggested_session_name &&
1392
+ priorHb.session_name_seen_for !== priorHb.suggested_session_name &&
1393
+ hb?.suggested_session_name === priorHb.suggested_session_name
1394
+ ? hb.suggested_session_name
1395
+ : null;
1396
+ const suggestedName = firstSuggestedName ?? retriedSuggestedName;
1397
+ const firstOfSession = firstSuggestedName !== null;
1398
+ const sessionNameRetry = retriedSuggestedName !== null;
1275
1399
  emit.data({
1276
1400
  instance_id: myOwner,
1277
1401
  name: hb?.name ?? null,
1278
1402
  task: hb?.task ?? null,
1279
1403
  cleared: !task || task.length === 0,
1280
1404
  first_of_session: firstOfSession,
1405
+ session_name_retry: sessionNameRetry,
1281
1406
  suggested_session_name: suggestedName,
1282
1407
  ...(lifecycleWarning ? { warning: lifecycleWarning } : {}),
1283
1408
  // Right-time instruction: the UserPromptSubmit nudge fires before the name
1284
1409
  // exists; this result is the moment the model holds the string.
1285
1410
  ...(suggestedName
1286
1411
  ? {
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.",
1412
+ note: SESSION_NAME_DISPLAY_NOTE,
1288
1413
  }
1289
1414
  : {}),
1290
1415
  });
@@ -1310,7 +1435,7 @@ function runLifecycle(rawState, opts) {
1310
1435
  }
1311
1436
  if (!opts.sessionId)
1312
1437
  ensureCursorSession(root);
1313
- const myOwner = opts.sessionId ?? resolveOwner();
1438
+ const myOwner = opts.sessionId ?? resolveOwner() ?? sessionIdentityFromEnv();
1314
1439
  if (!myOwner) {
1315
1440
  emit.error({
1316
1441
  code: "no_pidmap_entry",
@@ -1319,11 +1444,41 @@ function runLifecycle(rawState, opts) {
1319
1444
  process.exitCode = 1;
1320
1445
  return;
1321
1446
  }
1322
- const hb = readHeartbeat(myOwner);
1447
+ let hb = readCurrentCoordinationRow(myOwner);
1448
+ let reopenedGeneration;
1323
1449
  if (!hb) {
1324
- emit.error({ code: "no_heartbeat", message: noHeartbeatMessage(myOwner) });
1325
- process.exitCode = 1;
1326
- return;
1450
+ if (state !== "active") {
1451
+ emit.error({
1452
+ code: "no_live_generation",
1453
+ message: `${noLiveGenerationMessage(myOwner)}; run \`${resolveBinName(root)} agents lifecycle active\` to open a fresh generation`,
1454
+ });
1455
+ process.exitCode = 1;
1456
+ return;
1457
+ }
1458
+ try {
1459
+ reopenedGeneration = reopenLiveCoordinationGenerationV3({
1460
+ coordRoot: root,
1461
+ owner: myOwner,
1462
+ nativeSessionId: nativeSessionIdentity(readHeartbeatCache(root, myOwner), myOwner),
1463
+ });
1464
+ }
1465
+ catch (error) {
1466
+ emit.error({
1467
+ code: "lifecycle_reopen_failed",
1468
+ message: error instanceof Error ? error.message : String(error),
1469
+ });
1470
+ process.exitCode = 1;
1471
+ return;
1472
+ }
1473
+ hb = readCurrentCoordinationRow(myOwner);
1474
+ if (!hb) {
1475
+ emit.error({
1476
+ code: "lifecycle_reopen_failed",
1477
+ message: "the fresh generation was recorded but did not project as live",
1478
+ });
1479
+ process.exitCode = 1;
1480
+ return;
1481
+ }
1327
1482
  }
1328
1483
  if (hb.kind === "subagent" || hb.kind === "transient" || hb.workflow_run_id) {
1329
1484
  emit.error({
@@ -1352,6 +1507,22 @@ function runLifecycle(rawState, opts) {
1352
1507
  }
1353
1508
  const priorState = hb.task_state ?? "active";
1354
1509
  const priorReason = hb.task_state_reason || undefined;
1510
+ if (reopenedGeneration) {
1511
+ emit.data({
1512
+ instance_id: myOwner,
1513
+ task_state: "active",
1514
+ prior_state: null,
1515
+ reason: null,
1516
+ changed: true,
1517
+ generation_reopened: true,
1518
+ prior_generation_id: reopenedGeneration.prior_generation_id,
1519
+ generation_id: reopenedGeneration.generation_id,
1520
+ name_reminted: false,
1521
+ suggested_session_name: null,
1522
+ git_finalization_checked: false,
1523
+ });
1524
+ return;
1525
+ }
1355
1526
  if (priorState === state && priorReason === reason) {
1356
1527
  emit.data({
1357
1528
  instance_id: myOwner,
@@ -1365,7 +1536,7 @@ function runLifecycle(rawState, opts) {
1365
1536
  }
1366
1537
  let finalization = null;
1367
1538
  if (state === "done") {
1368
- const history = readSessionWriteClaims(root, hb.instance_id, hb.session_id ?? myOwner);
1539
+ const history = readSessionWriteClaims(root, hb.instance_id, nativeSessionIdentity(hb, myOwner));
1369
1540
  const touchedPaths = [...new Set([...(hb.files_touched ?? []), ...history.paths])];
1370
1541
  finalization = checkGitFinalization(root, touchedPaths, {
1371
1542
  claimHistoryComplete: history.complete,
@@ -1379,19 +1550,16 @@ function runLifecycle(rawState, opts) {
1379
1550
  return;
1380
1551
  }
1381
1552
  }
1382
- const suggestedName = buildLifecycleSuggestedName(hb.name ?? "unknown", hb.task, state);
1553
+ const suggestedName = buildLifecycleSuggestedName(hb.suggested_session_name, state);
1383
1554
  const nameReminted = suggestedName !== null && suggestedName !== hb.suggested_session_name;
1384
- const emitted = emitCanonical({
1385
- type: "state.task_state",
1555
+ const emitted = emitEventV3({
1386
1556
  owner: myOwner,
1387
- session: hb.session_id ?? myOwner,
1557
+ session: nativeSessionIdentity(hb, myOwner),
1388
1558
  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,
1559
+ observation: {
1560
+ event_type: "coord.lifecycle_changed",
1561
+ new_state: state,
1562
+ ...(reason ? { reason } : {}),
1395
1563
  ...(nameReminted ? { suggested_session_name: suggestedName } : {}),
1396
1564
  },
1397
1565
  });
@@ -1414,7 +1582,7 @@ function runLifecycle(rawState, opts) {
1414
1582
  git_finalization_checked: finalization !== null,
1415
1583
  ...(nameReminted
1416
1584
  ? {
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.",
1585
+ note: SESSION_NAME_DISPLAY_NOTE,
1418
1586
  }
1419
1587
  : {}),
1420
1588
  });
@@ -1440,7 +1608,7 @@ function runSuggestName(descriptionParts, opts) {
1440
1608
  });
1441
1609
  process.exit(1);
1442
1610
  }
1443
- const hb = readHeartbeat(myOwner);
1611
+ const hb = readCurrentCoordinationRow(myOwner);
1444
1612
  const agentName = hb?.name || "unknown";
1445
1613
  // Description resolution: an explicit arg wins; with no arg, fall back to the
1446
1614
  // agent's current declared task, so a bare `suggest-name` reprints the running
@@ -1483,6 +1651,14 @@ function runStatus(opts) {
1483
1651
  });
1484
1652
  process.exit(1);
1485
1653
  }
1654
+ if (opts.endSession && !opts.endTurn) {
1655
+ emit.error({
1656
+ code: "end_session_requires_end_turn",
1657
+ message: "--end-session requires --end-turn so Git finalization is verified first",
1658
+ });
1659
+ process.exitCode = 1;
1660
+ return;
1661
+ }
1486
1662
  // Identity resolution: prefer explicit --session-id (hook-friendly), fall
1487
1663
  // back to ppid walk for interactive shell usage.
1488
1664
  if (!opts.sessionId)
@@ -1495,17 +1671,21 @@ function runStatus(opts) {
1495
1671
  });
1496
1672
  process.exit(1);
1497
1673
  }
1498
- const hb = readHeartbeat(myOwner);
1674
+ const hb = readCurrentCoordinationRow(myOwner);
1499
1675
  if (!hb) {
1500
1676
  emit.error({
1501
- code: "no_heartbeat",
1502
- message: noHeartbeatMessage(myOwner),
1677
+ code: "no_live_generation",
1678
+ message: noLiveGenerationMessage(myOwner),
1503
1679
  });
1504
1680
  process.exit(1);
1505
1681
  }
1682
+ // Report-only run quality: cheap config/cursor due check first, then a
1683
+ // non-blocking lazy evaluation. Shadow mode deliberately exposes no severity.
1684
+ const qualityEvaluation = evaluateRunQualityIfDue(root, new Date(), myOwner);
1685
+ const quality = qualityForStatus(qualityEvaluation.config.requested_mode, qualityEvaluation.snapshot);
1506
1686
  let finalization = null;
1507
1687
  if (opts.endTurn) {
1508
- const history = readSessionWriteClaims(root, hb.instance_id, hb.session_id ?? myOwner);
1688
+ const history = readSessionWriteClaims(root, hb.instance_id, nativeSessionIdentity(hb, myOwner));
1509
1689
  const touchedPaths = [...new Set([...(hb.files_touched ?? []), ...history.paths])];
1510
1690
  finalization = checkGitFinalization(root, touchedPaths, {
1511
1691
  claimHistoryComplete: history.complete,
@@ -1519,51 +1699,55 @@ function runStatus(opts) {
1519
1699
  return;
1520
1700
  }
1521
1701
  }
1522
- emitCanonical({
1523
- type: "state.status_checked",
1702
+ emitEventV3({
1524
1703
  owner: myOwner,
1525
- session: hb.session_id ?? myOwner,
1704
+ session: nativeSessionIdentity(hb, myOwner),
1526
1705
  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,
1706
+ observation: {
1707
+ event_type: "coord.status_observed",
1708
+ status: opts.endTurn ? "end_turn_checked" : opts.json ? "json_checked" : "box_checked",
1532
1709
  },
1533
1710
  });
1534
- const startedAtMs = Date.parse(hb.started_at);
1711
+ let sessionEnd;
1712
+ if (opts.endSession) {
1713
+ const canonicalInstanceId = liveInstanceIdV3(hb.instance_id);
1714
+ const records = listHookProducerStateRecordsV3(root, { includeTerminal: true }).filter(({ state }) => state.instance_id === canonicalInstanceId);
1715
+ if (records.length !== 1 || !records[0]) {
1716
+ emit.error({
1717
+ code: "session_identity_ambiguous",
1718
+ message: `expected one V3 generation for ${canonicalInstanceId}; found ${records.length}`,
1719
+ });
1720
+ process.exitCode = 1;
1721
+ return;
1722
+ }
1723
+ const state = records[0].state;
1724
+ const requested = requestSessionEndExplicitV3({
1725
+ coordRoot: root,
1726
+ instance_id: state.instance_id,
1727
+ generation_id: state.generation_id,
1728
+ outcome: "succeeded",
1729
+ coordination_finalized: true,
1730
+ });
1731
+ if (requested.state === "queued" || requested.state === "already_requested") {
1732
+ sessionEnd = { state: requested.state, request_id: requested.request.request_id };
1733
+ }
1734
+ else if (requested.state === "recorded") {
1735
+ sessionEnd = { state: requested.state, terminal_event_id: requested.event.event_id };
1736
+ }
1737
+ else if (requested.state === "already_ended") {
1738
+ sessionEnd = { state: requested.state, terminal_event_id: requested.event_id };
1739
+ }
1740
+ else {
1741
+ emit.error({ code: "session_end_failed", message: JSON.stringify(requested) });
1742
+ process.exitCode = 1;
1743
+ return;
1744
+ }
1745
+ }
1746
+ const startedAtMs = Date.parse(hb.started_at ?? hb.last_heartbeat);
1535
1747
  const ageSecs = Number.isFinite(startedAtMs)
1536
1748
  ? Math.max(0, Math.floor((Date.now() - startedAtMs) / 1000))
1537
1749
  : 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
- }
1750
+ const { livePeers, stale: peersStale } = collectStatusPeerHealth(root, myOwner);
1567
1751
  // Sort: file-holders first (by file count desc), then idle peers by recency.
1568
1752
  livePeers.sort((a, b) => {
1569
1753
  const af = a.files_touched?.length ?? 0;
@@ -1577,7 +1761,7 @@ function runStatus(opts) {
1577
1761
  // Cross-machine presence (ADR 0016): sessions on other machines, advisory.
1578
1762
  const remoteMachines = readRemoteMachines(root);
1579
1763
  const peersStr = formatPeers(livePeers, 4, peersStale, remoteMachines);
1580
- const ctxUsage = readContextUsage(hb.session_id, hb.platform);
1764
+ const ctxUsage = readContextUsage(hb.native_session_id ?? hb.session_id, hb.platform);
1581
1765
  let ctxStr;
1582
1766
  if (!ctxUsage) {
1583
1767
  ctxStr = "unavailable";
@@ -1612,6 +1796,7 @@ function runStatus(opts) {
1612
1796
  activity_updated_at: hb.activity_updated_at ?? null,
1613
1797
  activity_source: hb.activity_source ?? null,
1614
1798
  task_state: taskStateOf(hb),
1799
+ task_state_scope: "current",
1615
1800
  task_state_updated_at: hb.task_state_updated_at ?? null,
1616
1801
  task_state_reason: hb.task_state_reason ?? null,
1617
1802
  files_held: filesHeld,
@@ -1638,7 +1823,9 @@ function runStatus(opts) {
1638
1823
  context_window: ctxUsage?.window ?? null,
1639
1824
  timestamp_iso: new Date().toISOString(),
1640
1825
  timestamp_local: timeStr,
1826
+ ...(quality ? { quality } : {}),
1641
1827
  ...(finalization ? { finalization } : {}),
1828
+ ...(sessionEnd ? { session_end: sessionEnd } : {}),
1642
1829
  };
1643
1830
  if (opts.json) {
1644
1831
  emit.config({ format: "json" });
@@ -1654,10 +1841,7 @@ function runStatus(opts) {
1654
1841
  ["peers", peersStr],
1655
1842
  ["time", timeStr],
1656
1843
  ];
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
- }
1844
+ // Task gets full text; formatBox word-wraps to MAX_BOX_CONTENT_WIDTH.
1661
1845
  if (hb.task && hb.task.length > 0) {
1662
1846
  rows.splice(1, 0, ["task", hb.task]);
1663
1847
  }
@@ -1671,10 +1855,49 @@ function runStatus(opts) {
1671
1855
  : `${pendingCouncils.length} pending (${pendingCouncils[0]}, +${pendingCouncils.length - 1})`;
1672
1856
  rows.splice(idx, 0, ["council", summary]);
1673
1857
  }
1858
+ if (quality) {
1859
+ const idx = rows.findIndex((row) => row[0] === "time");
1860
+ rows.splice(idx, 0, ["quality", formatRunQuality(quality.status, quality.signal_ids)]);
1861
+ }
1862
+ if (sessionEnd) {
1863
+ const idx = rows.findIndex((row) => row[0] === "time");
1864
+ rows.splice(idx, 0, [
1865
+ "session end",
1866
+ sessionEnd.state === "queued" || sessionEnd.state === "already_requested"
1867
+ ? "queued after this turn"
1868
+ : "recorded",
1869
+ ]);
1870
+ }
1674
1871
  // Box rendering needs predictable stdout regardless of TTY/pipe detection:
1675
1872
  // agent runs this via Bash (no TTY) and pastes captured stdout into chat.
1676
1873
  process.stdout.write(`${formatBox(displayName, rows)}\n`); // lint-ok-emission: chat-paste path; emit.text() auto-suppresses non-TTY
1677
1874
  }
1875
+ function qualityForStatus(mode, snapshot) {
1876
+ if (mode !== "report")
1877
+ return null;
1878
+ const fresh = !!snapshot && Date.parse(snapshot.expires_at) > Date.now();
1879
+ if (!snapshot || !fresh) {
1880
+ return {
1881
+ status: "unknown",
1882
+ signal_ids: [],
1883
+ evaluated_at: snapshot?.evaluated_at ?? null,
1884
+ fresh: false,
1885
+ };
1886
+ }
1887
+ return {
1888
+ status: snapshot.status,
1889
+ signal_ids: snapshot.signals
1890
+ .filter((signal) => signal.state === "active" && signal.severity !== "none")
1891
+ .map((signal) => signal.id)
1892
+ .sort()
1893
+ .slice(0, 3),
1894
+ evaluated_at: snapshot.evaluated_at,
1895
+ fresh: true,
1896
+ };
1897
+ }
1898
+ function formatRunQuality(status, signalIds) {
1899
+ return signalIds.length > 0 ? `${status} (${signalIds.join(", ")})` : status;
1900
+ }
1678
1901
  function formatList(items, cap, emptyLabel) {
1679
1902
  if (items.length === 0)
1680
1903
  return emptyLabel;
@@ -1715,6 +1938,28 @@ function formatPeers(peers, cap, staleCount, remoteMachines = []) {
1715
1938
  }
1716
1939
  return main;
1717
1940
  }
1941
+ export function collectStatusPeerHealth(root, myOwner, nowMs = Date.now()) {
1942
+ let rows = [];
1943
+ try {
1944
+ rows = readLiveCoordinationRows(root);
1945
+ }
1946
+ catch {
1947
+ // V3 authority failures must not resurrect disposable cache rows.
1948
+ }
1949
+ const cutoffMs = nowMs - freshnessCutoffSecs() * 1000;
1950
+ const livePeers = [];
1951
+ let stale = 0;
1952
+ for (const peer of rows) {
1953
+ if (peer.instance_id === myOwner)
1954
+ continue;
1955
+ const observedAtMs = Date.parse(peer.last_heartbeat);
1956
+ if (Number.isFinite(observedAtMs) && observedAtMs >= cutoffMs)
1957
+ livePeers.push(peer);
1958
+ else
1959
+ stale += 1;
1960
+ }
1961
+ return { livePeers, stale };
1962
+ }
1718
1963
  function fmtTokens(n) {
1719
1964
  if (n < 1000)
1720
1965
  return `${n}`;
@@ -1968,40 +2213,49 @@ function formatAge(secs) {
1968
2213
  const h = Math.floor((secs % 86400) / 3600);
1969
2214
  return `${d}d ${h}h`;
1970
2215
  }
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);
2216
+ /** Read validated canonical V3 events. */
2217
+ export function readAgentDiagnosticEventsInWindow(root, cutoffMs) {
2218
+ const control = readEventV3ControlState(root);
2219
+ if (control.state === "candidate" || control.state === "active") {
2220
+ const ledger = readLedgerV3(root);
2221
+ if (!ledger.complete) {
2222
+ return {
2223
+ source: "v3",
2224
+ authoritative: false,
2225
+ reason: `V3 ledger validation failed: ${ledger.diagnostics.map((item) => item.code).join(", ") || "incomplete"}`,
2226
+ truncated: false,
2227
+ bytes: ledger.bytes,
2228
+ events: [],
2229
+ };
2230
+ }
2231
+ const events = [];
2232
+ for (const { event } of ledger.events) {
2233
+ const tsMs = Date.parse(event.time.recorded_at);
1996
2234
  if (!Number.isFinite(tsMs) || tsMs < cutoffMs)
1997
2235
  continue;
1998
- out.push(ev);
1999
- }
2000
- catch {
2001
- /* skip malformed */
2236
+ events.push({
2237
+ event_type: event.event_type,
2238
+ ts: event.time.recorded_at,
2239
+ instance_id: event.scope.instance_id,
2240
+ payload: event.payload,
2241
+ });
2002
2242
  }
2243
+ return {
2244
+ source: "v3",
2245
+ authoritative: true,
2246
+ truncated: false,
2247
+ bytes: ledger.bytes,
2248
+ events,
2249
+ };
2003
2250
  }
2004
- return out;
2251
+ return {
2252
+ source: "v3",
2253
+ authoritative: false,
2254
+ reason: `V3 control state is ${control.state}`,
2255
+ truncated: false,
2256
+ bytes: 0,
2257
+ events: [],
2258
+ };
2005
2259
  }
2006
2260
  /** Normalize adapter event data into the heartbeat platform value. */
2007
2261
  function adapterToPlatform(adapter) {
@@ -2011,18 +2265,21 @@ function adapterToPlatform(adapter) {
2011
2265
  return "cursor";
2012
2266
  if (adapter === "codex")
2013
2267
  return "codex";
2014
- return "claude-code";
2268
+ return "unknown";
2015
2269
  }
2016
2270
  /** Project a canonical health.* event into the HealEvent shape the aggregators
2017
2271
  * already consume. Returns null for non-heal events. instance_id → display name
2018
2272
  * via `nameById` (full-UUID keyed). */
2019
2273
  function canonicalToHealEvent(ev, nameById) {
2020
- if (ev.event_type !== "health.pidmap_heal" && ev.event_type !== "health.heartbeat_heal") {
2274
+ if (ev.event_type !== "health.observed") {
2021
2275
  return null;
2022
2276
  }
2023
- const kind = ev.event_type === "health.pidmap_heal" ? "pidmap" : "heartbeat";
2024
- const data = ev.data ?? {};
2025
- const reason = data.reason === "stale" ? "stale" : "missing";
2277
+ const data = ev.payload ?? {};
2278
+ const subsystem = typeof data.subsystem === "string" ? data.subsystem : "";
2279
+ if (subsystem !== "pidmap" && subsystem !== "heartbeat")
2280
+ return null;
2281
+ const kind = subsystem;
2282
+ const reason = data.condition === "stale" ? "stale" : "missing";
2026
2283
  const instanceId = ev.instance_id ?? "";
2027
2284
  const name = nameById.get(instanceId);
2028
2285
  const agent = name ? `agent-${name}` : `agent-${instanceId.slice(0, 8) || "unknown"}`;
@@ -2099,13 +2356,12 @@ function runHealEvents(opts) {
2099
2356
  });
2100
2357
  process.exit(1);
2101
2358
  }
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.
2359
+ // Heal telemetry is read from the canonical V3 ledger.
2105
2360
  const cutoffMs = Date.now() - sinceSecs * 1000;
2106
2361
  const nameById = buildNameById(root);
2107
2362
  const events = [];
2108
- for (const ev of readCanonicalEventsInWindow(root, cutoffMs)) {
2363
+ const diagnosticEvents = readAgentDiagnosticEventsInWindow(root, cutoffMs);
2364
+ for (const ev of diagnosticEvents.events) {
2109
2365
  const heal = canonicalToHealEvent(ev, nameById);
2110
2366
  if (heal)
2111
2367
  events.push(heal);
@@ -2142,6 +2398,11 @@ function runHealEvents(opts) {
2142
2398
  const recent = events.slice(0, limit);
2143
2399
  const data = {
2144
2400
  since: opts.since,
2401
+ telemetry: {
2402
+ source: diagnosticEvents.source,
2403
+ authoritative: diagnosticEvents.authoritative,
2404
+ reason: diagnosticEvents.reason,
2405
+ },
2145
2406
  total: events.length,
2146
2407
  by_reason: byReason,
2147
2408
  by_kind: byKind,
@@ -2152,7 +2413,15 @@ function runHealEvents(opts) {
2152
2413
  };
2153
2414
  if (opts.csv) {
2154
2415
  emit.config({ format: "csv" });
2155
- emit.data(recent);
2416
+ emit.data(data.telemetry.authoritative
2417
+ ? recent
2418
+ : [
2419
+ {
2420
+ status: "unavailable",
2421
+ source: data.telemetry.source,
2422
+ reason: data.telemetry.reason,
2423
+ },
2424
+ ]);
2156
2425
  return;
2157
2426
  }
2158
2427
  if (opts.json) {
@@ -2164,6 +2433,11 @@ function runHealEvents(opts) {
2164
2433
  if (process.stdout.isTTY) {
2165
2434
  const lines = [];
2166
2435
  lines.push(`Heal events: ${events.length} total in last ${opts.since} (health.pidmap_heal + health.heartbeat_heal)`);
2436
+ if (!data.telemetry.authoritative) {
2437
+ lines.push(` unavailable: ${data.telemetry.reason ?? "diagnostic event source is incomplete"}`);
2438
+ emit.text(`${lines.join("\n")}\n`);
2439
+ return;
2440
+ }
2167
2441
  lines.push("");
2168
2442
  if (events.length === 0) {
2169
2443
  lines.push(" (none; pid-map/heartbeat drift is not happening in this window)");
@@ -2211,23 +2485,206 @@ function runHealEvents(opts) {
2211
2485
  emit.text(`${lines.join("\n")}\n`);
2212
2486
  }
2213
2487
  }
2488
+ /** Open-span soft watermark per producer state. The producer-state reader
2489
+ * hard-caps at 256 spans (a state file beyond it fails to load), so surfacing
2490
+ * pressure at half that gives room to act before reads start failing. */
2491
+ const SPAN_PRESSURE_SOFT_WATERMARK = 128;
2492
+ function errorText(error) {
2493
+ return error instanceof Error ? error.message : String(error);
2494
+ }
2495
+ /**
2496
+ * Read-only health counters for the V3 event ledger's producer surfaces. Never
2497
+ * mutates ledger state (no control repair, no spool drain) and never throws:
2498
+ * a non-live route returns `{ state: "unavailable" }`, and each sub-surface
2499
+ * that fails to read lands in `collection_errors` instead of aborting the rest.
2500
+ */
2501
+ export function collectEventLedgerHealthV3(root, nowMs = Date.now()) {
2502
+ let control;
2503
+ try {
2504
+ control = readEventV3ControlState(root);
2505
+ }
2506
+ catch (error) {
2507
+ return { state: "unavailable", reason: `control read failed: ${errorText(error)}` };
2508
+ }
2509
+ if (control.state !== "candidate" && control.state !== "active") {
2510
+ return {
2511
+ state: "unavailable",
2512
+ reason: `${control.state}: ${control.reason}`,
2513
+ };
2514
+ }
2515
+ const collectionErrors = [];
2516
+ // 1) Open tool spans per live (non-terminal) generation + span-count pressure.
2517
+ const generations = [];
2518
+ const spanPressure = [];
2519
+ try {
2520
+ for (const { state } of listHookProducerStateRecordsV3(root)) {
2521
+ if (state.spans.length >= SPAN_PRESSURE_SOFT_WATERMARK) {
2522
+ spanPressure.push({
2523
+ instance_id: state.instance_id,
2524
+ generation_id: state.generation_id,
2525
+ span_count: state.spans.length,
2526
+ });
2527
+ }
2528
+ if (state.spans.length === 0)
2529
+ continue;
2530
+ generations.push({
2531
+ instance_id: state.instance_id,
2532
+ generation_id: state.generation_id,
2533
+ adapter: state.adapter,
2534
+ span_count: state.spans.length,
2535
+ turn_open: Boolean(state.current_turn_id),
2536
+ });
2537
+ }
2538
+ }
2539
+ catch (error) {
2540
+ collectionErrors.push(`producer states unreadable: ${errorText(error)}`);
2541
+ }
2542
+ // 2) Pending finalization requests with age + trigger.
2543
+ const pending = [];
2544
+ try {
2545
+ for (const request of listSessionFinalizationRequestsV3(root)) {
2546
+ if (request.status !== "pending")
2547
+ continue;
2548
+ const observedMs = Date.parse(request.observed_at);
2549
+ pending.push({
2550
+ request_id: request.request_id,
2551
+ trigger: request.trigger,
2552
+ generation_id: request.generation_id,
2553
+ age_ms: Number.isFinite(observedMs) ? Math.max(0, nowMs - observedMs) : -1,
2554
+ allowed_open_span_count: request.allowed_open_span_ids?.length ?? 0,
2555
+ });
2556
+ }
2557
+ }
2558
+ catch (error) {
2559
+ collectionErrors.push(`finalization requests unreadable: ${errorText(error)}`);
2560
+ }
2561
+ // 3) Intake spool depth (queued hook signals awaiting a lease-holder drain).
2562
+ let intakeTotal = 0;
2563
+ const intakeGroups = [];
2564
+ try {
2565
+ for (const group of listHookIntakeGroupsV3(root)) {
2566
+ const count = listHookIntakeRecordsV3(group.directory).length;
2567
+ if (count === 0)
2568
+ continue;
2569
+ intakeTotal += count;
2570
+ intakeGroups.push({ adapter: group.adapter, session_hash: group.session_hash, count });
2571
+ }
2572
+ }
2573
+ catch (error) {
2574
+ collectionErrors.push(`intake spool unreadable: ${errorText(error)}`);
2575
+ }
2576
+ // 4) Diagnostics spool counts by category. Filenames only (never the
2577
+ // contents): `<category>-<orderkey>.json` where the orderkey leads with a
2578
+ // zero-padded epoch-ms, which also gives the last-24h split for free.
2579
+ const byCategory = {};
2580
+ const recentByCategory = {};
2581
+ let diagnosticsTotal = 0;
2582
+ let diagnostics24h = 0;
2583
+ let diagnostics1h = 0;
2584
+ let latestDiagnosticMs = Number.NEGATIVE_INFINITY;
2585
+ try {
2586
+ const diagnosticsDir = join(resolve(root), EVENT_V3_LEDGER_RELATIVE_ROOT, "diagnostics");
2587
+ if (existsSync(diagnosticsDir)) {
2588
+ const dayAgoMs = nowMs - 24 * 60 * 60 * 1000;
2589
+ const hourAgoMs = nowMs - 60 * 60 * 1000;
2590
+ for (const name of readdirSync(diagnosticsDir)) {
2591
+ const match = /^(.+)-(\d{15})-\d{20}-\d+-[0-9a-f-]+\.json$/.exec(name);
2592
+ const category = match?.[1];
2593
+ const epochMs = Number(match?.[2]);
2594
+ if (!category)
2595
+ continue;
2596
+ const entry = byCategory[category] ?? { total: 0, last_24h: 0 };
2597
+ const recent = recentByCategory[category] ?? { last_1h: 0, latest_at: null };
2598
+ byCategory[category] = entry;
2599
+ recentByCategory[category] = recent;
2600
+ entry.total += 1;
2601
+ diagnosticsTotal += 1;
2602
+ if (Number.isFinite(epochMs)) {
2603
+ const recordedAt = new Date(epochMs).toISOString();
2604
+ if (!recent.latest_at || epochMs > Date.parse(recent.latest_at)) {
2605
+ recent.latest_at = recordedAt;
2606
+ }
2607
+ latestDiagnosticMs = Math.max(latestDiagnosticMs, epochMs);
2608
+ if (epochMs >= dayAgoMs) {
2609
+ entry.last_24h += 1;
2610
+ diagnostics24h += 1;
2611
+ }
2612
+ if (epochMs >= hourAgoMs) {
2613
+ recent.last_1h += 1;
2614
+ diagnostics1h += 1;
2615
+ }
2616
+ }
2617
+ }
2618
+ }
2619
+ }
2620
+ catch (error) {
2621
+ collectionErrors.push(`diagnostics spool unreadable: ${errorText(error)}`);
2622
+ }
2623
+ return {
2624
+ state: "live",
2625
+ mode: control.state,
2626
+ open_spans: {
2627
+ total: generations.reduce((sum, generation) => sum + generation.span_count, 0),
2628
+ generations,
2629
+ },
2630
+ pending_finalizations: pending,
2631
+ intake_spool: { total: intakeTotal, groups: intakeGroups },
2632
+ diagnostics_spool: {
2633
+ total: diagnosticsTotal,
2634
+ last_24h: diagnostics24h,
2635
+ last_1h: diagnostics1h,
2636
+ latest_at: Number.isFinite(latestDiagnosticMs)
2637
+ ? new Date(latestDiagnosticMs).toISOString()
2638
+ : null,
2639
+ by_category: byCategory,
2640
+ recent_by_category: recentByCategory,
2641
+ },
2642
+ span_pressure: spanPressure,
2643
+ collection_errors: collectionErrors,
2644
+ };
2645
+ }
2214
2646
  /** 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) {
2647
+ * window by exact error and phase, while separating current-hour failures from
2648
+ * historical rows. Each line is {ts, error, phase, ...}. */
2649
+ export function readHookErrors(root, cutoffMs, nowMs = Date.now()) {
2218
2650
  const p = resolve(root, ".harnery", "debug", "agent-hook.errors.ndjson");
2219
2651
  const byPhase = {};
2652
+ const byError = {};
2220
2653
  const sampleByPhase = {};
2654
+ const errorPhaseCounts = new Map();
2655
+ const recentErrorPhaseCounts = new Map();
2221
2656
  let total = 0;
2222
- if (!existsSync(p))
2223
- return { total: 0, byPhase, top: [] };
2657
+ let last1h = 0;
2658
+ let latestMs = Number.NEGATIVE_INFINITY;
2659
+ if (!existsSync(p)) {
2660
+ return {
2661
+ total: 0,
2662
+ last1h: 0,
2663
+ latestAt: null,
2664
+ byPhase,
2665
+ byError,
2666
+ top: [],
2667
+ topErrors: [],
2668
+ recentTopErrors: [],
2669
+ };
2670
+ }
2224
2671
  let raw;
2225
2672
  try {
2226
2673
  raw = readFileSync(p, "utf8");
2227
2674
  }
2228
2675
  catch {
2229
- return { total: 0, byPhase, top: [] };
2676
+ return {
2677
+ total: 0,
2678
+ last1h: 0,
2679
+ latestAt: null,
2680
+ byPhase,
2681
+ byError,
2682
+ top: [],
2683
+ topErrors: [],
2684
+ recentTopErrors: [],
2685
+ };
2230
2686
  }
2687
+ const hourAgoMs = nowMs - 60 * 60 * 1000;
2231
2688
  for (const line of raw.split("\n")) {
2232
2689
  if (!line.trim())
2233
2690
  continue;
@@ -2237,9 +2694,22 @@ function readHookErrors(root, cutoffMs) {
2237
2694
  if (!Number.isFinite(tsMs) || tsMs < cutoffMs)
2238
2695
  continue;
2239
2696
  const phase = e.phase ?? "(unknown)";
2697
+ const error = e.error ?? "(unknown)";
2240
2698
  byPhase[phase] = (byPhase[phase] ?? 0) + 1;
2699
+ byError[error] = (byError[error] ?? 0) + 1;
2241
2700
  if (!sampleByPhase[phase] && e.error)
2242
2701
  sampleByPhase[phase] = e.error;
2702
+ const errorPhaseKey = `${error}\0${phase}`;
2703
+ const errorPhase = errorPhaseCounts.get(errorPhaseKey) ?? { error, phase, count: 0 };
2704
+ errorPhase.count += 1;
2705
+ errorPhaseCounts.set(errorPhaseKey, errorPhase);
2706
+ latestMs = Math.max(latestMs, tsMs);
2707
+ if (tsMs >= hourAgoMs) {
2708
+ last1h += 1;
2709
+ const recent = recentErrorPhaseCounts.get(errorPhaseKey) ?? { error, phase, count: 0 };
2710
+ recent.count += 1;
2711
+ recentErrorPhaseCounts.set(errorPhaseKey, recent);
2712
+ }
2243
2713
  total++;
2244
2714
  }
2245
2715
  catch {
@@ -2250,125 +2720,163 @@ function readHookErrors(root, cutoffMs) {
2250
2720
  .sort((a, b) => b[1] - a[1])
2251
2721
  .slice(0, 5)
2252
2722
  .map(([phase, count]) => ({ phase, count, sample: sampleByPhase[phase] ?? "" }));
2253
- return { total, byPhase, top };
2723
+ const topErrors = [...errorPhaseCounts.values()]
2724
+ .sort((left, right) => right.count - left.count || left.error.localeCompare(right.error))
2725
+ .slice(0, 5);
2726
+ const recentTopErrors = [...recentErrorPhaseCounts.values()]
2727
+ .sort((left, right) => right.count - left.count || left.error.localeCompare(right.error))
2728
+ .slice(0, 5);
2729
+ return {
2730
+ total,
2731
+ last1h,
2732
+ latestAt: Number.isFinite(latestMs) ? new Date(latestMs).toISOString() : null,
2733
+ byPhase,
2734
+ byError,
2735
+ top,
2736
+ topErrors,
2737
+ recentTopErrors,
2738
+ };
2254
2739
  }
2255
2740
  /** Canonical event stream size + drain lag (events appended after the cursor). */
2256
2741
  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 };
2742
+ const read = readAgentDiagnosticEventsInWindow(root, 0);
2743
+ return {
2744
+ source: "v3",
2745
+ authoritative: read.authoritative,
2746
+ ...(read.reason ? { reason: read.reason } : {}),
2747
+ bytes: read.bytes,
2748
+ lines: read.events.length,
2749
+ cursor_backlog: 0,
2750
+ };
2751
+ }
2752
+ export function traceInstanceIdsForEventSource(nativeInstanceIds, source) {
2753
+ void source;
2754
+ return nativeInstanceIds.map((instanceId) => liveInstanceIdV3(instanceId));
2302
2755
  }
2303
2756
  /** Map a canonical event to a concise trace line, or null to drop it. */
2304
- function traceLine(ev, allTools) {
2305
- const d = (ev.data ?? {});
2757
+ export function traceLine(ev, allTools) {
2758
+ const d = (ev.payload ?? {});
2306
2759
  const s = (k) => (typeof d[k] === "string" ? d[k] : "");
2760
+ const object = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
2761
+ ? value
2762
+ : {};
2307
2763
  const clip = (v, n = 70) => (v.length <= n ? v : `${v.slice(0, n - 1)}…`);
2764
+ const recovery = typeof d.recovery === "object" && d.recovery !== null
2765
+ ? d.recovery
2766
+ : null;
2767
+ const recoveryReason = typeof recovery?.reason === "string" ? recovery.reason : "";
2768
+ const recoveryDetail = recoveryReason ? ` · RECOVERY reason=${recoveryReason}` : "";
2308
2769
  let detail = "";
2309
2770
  switch (ev.event_type) {
2310
- case "session.start":
2311
- detail = `${s("source") || "startup"}${s("model") ? ` · model=${s("model")}` : ""}${s("name") ? ` · ${s("name")}` : ""}`;
2771
+ case "session.started":
2772
+ detail = "generation started";
2312
2773
  break;
2313
- case "session.end":
2314
- detail = `clean_exit=${d.clean_exit ?? "?"}`;
2774
+ case "session.resumed":
2775
+ detail = "generation resumed";
2315
2776
  break;
2316
- case "subagent.start":
2317
- detail = `${s("agent_type") || "subagent"}${s("name") ? ` · ${s("name")}` : ""}`;
2777
+ case "session.ended":
2778
+ detail = `outcome=${s("outcome") || "unknown"}${s("reason") ? ` · reason=${s("reason")}` : ""}`;
2318
2779
  break;
2319
- case "subagent.stop":
2320
- detail = `clean_exit=${d.clean_exit ?? "?"}`;
2780
+ case "session.termination_observed":
2781
+ detail = `provisional ${s("observation") || "termination"}${s("reason") ? ` · reason=${s("reason")}` : ""}`;
2321
2782
  break;
2322
- case "user_prompt.submit":
2323
- detail = clip(s("prompt_text") || s("prompt"));
2783
+ case "agent.delegated":
2784
+ detail = `delegated child=${s("child_generation_id") || "unknown"}`;
2324
2785
  break;
2325
- case "turn.stop":
2326
- detail = `status_box=${d.status_box_present ?? "?"}${s("turn_summary") ? ` · ${clip(s("turn_summary"), 50)}` : ""}`;
2786
+ case "agent.started":
2787
+ detail = `child started=${s("child_generation_id") || "unknown"}`;
2327
2788
  break;
2328
- case "tool.pre_use":
2329
- detail = `${s("tool_name")}${s("tool_target") || s("intent") ? ` · ${clip(s("tool_target") || s("intent"), 60)}` : ""}`;
2789
+ case "agent.completed":
2790
+ detail = `child completed=${s("child_generation_id") || "unknown"} · outcome=${s("outcome") || "unknown"}`;
2330
2791
  break;
2331
- case "state.task_set":
2332
- detail = d.cleared ? "(cleared)" : clip(s("task"));
2792
+ case "turn.started":
2793
+ detail = `intent=${s("intent_kind") || "unknown"}`;
2333
2794
  break;
2334
- case "state.task_state":
2335
- detail = `${s("prior_state") || "active"} → ${s("state") || "active"}${s("reason") ? ` · ${clip(s("reason"), 55)}` : ""}`;
2795
+ case "turn.completed":
2796
+ detail = `outcome=${s("outcome") || "unknown"}`;
2336
2797
  break;
2337
- case "interaction.input_requested":
2338
- detail = s("request_kind") || s("tool_name") || "operator input requested";
2798
+ case "tool.requested": {
2799
+ const tool = object(d.tool);
2800
+ detail = `${typeof tool.name === "string" ? tool.name : "tool"} requested (content omitted)${recoveryDetail}`;
2339
2801
  break;
2340
- case "state.status_checked":
2341
- detail = "status box rendered";
2802
+ }
2803
+ case "tool.completed": {
2804
+ const tool = object(d.tool);
2805
+ detail = `${typeof tool.name === "string" ? tool.name : "tool"} · outcome=${s("outcome") || "unknown"}${recoveryDetail}`;
2342
2806
  break;
2343
- case "identity.assumed":
2344
- detail = `${s("previous_name") || "unknown"} → ${s("name") || "unknown"}${s("agent_id") ? ` · persona=${s("agent_id").slice(0, 8)}…` : ""}`;
2807
+ }
2808
+ case "command.started":
2809
+ if (!allTools)
2810
+ return null;
2811
+ detail = "command started (content omitted)";
2812
+ break;
2813
+ case "command.completed":
2814
+ if (!allTools && !recoveryReason)
2815
+ return null;
2816
+ detail = `outcome=${s("outcome") || "unknown"}${recoveryDetail}`;
2817
+ break;
2818
+ case "coord.task_changed":
2819
+ case "coord.lifecycle_changed":
2820
+ case "coord.presence_changed":
2821
+ detail = `${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}${s("reason") ? ` · ${clip(s("reason"), 55)}` : ""}`;
2822
+ break;
2823
+ case "coord.status_observed":
2824
+ detail = s("status") || "status observed";
2825
+ break;
2826
+ case "coord.claim_changed": {
2827
+ const target = object(d.target);
2828
+ detail = `${s("operation") || "changed"}${typeof target.display === "string" ? ` · ${clip(target.display)}` : ""}`;
2829
+ break;
2830
+ }
2831
+ case "coord.message_observed":
2832
+ detail = `${s("direction") || "observed"} · peer=${s("peer_instance_id") || "unknown"}`;
2833
+ break;
2834
+ case "coord.identity_attested":
2835
+ detail = `${s("identity_id") || "identity"} · method=${s("method") || "unknown"}`;
2836
+ break;
2837
+ case "wait.started":
2838
+ detail = s("kind") || "wait started";
2345
2839
  break;
2346
- case "claim.acquire":
2347
- case "claim.release":
2348
- case "claim.conflict":
2349
- detail = clip(s("path"));
2840
+ case "wait.ended":
2841
+ detail = `outcome=${s("outcome") || "unknown"}`;
2350
2842
  break;
2351
- case "health.heartbeat_heal":
2352
- case "health.pidmap_heal":
2353
- detail = `reason=${s("reason")}`;
2843
+ case "artifact.observed": {
2844
+ const artifact = object(d.artifact);
2845
+ detail = `operation=${s("operation") || "observed"}${typeof artifact.kind === "string" ? ` · kind=${artifact.kind}` : ""}`;
2354
2846
  break;
2355
- case "health.heartbeat_swept":
2356
- detail = `reason=${s("reason")}${d.age_secs !== undefined ? ` · age=${d.age_secs}s` : ""}`;
2847
+ }
2848
+ case "progress.observed":
2849
+ detail = s("kind") || "progress";
2850
+ break;
2851
+ case "health.observed":
2852
+ detail = `${s("subsystem") || "unknown"} · ${s("severity") || "unknown"} · ${s("condition") || "unknown"}`;
2853
+ break;
2854
+ case "council.state_changed":
2855
+ detail = `${s("council_id") || "council"} · ${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}`;
2856
+ break;
2857
+ case "decision.state_changed":
2858
+ detail = `${s("decision_id") || "decision"} · ${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}`;
2859
+ break;
2860
+ case "lifecycle.recovered":
2861
+ detail = `recovered=${s("recovery_kind") || "unknown"}`;
2862
+ break;
2863
+ case "lifecycle.sweep_observed":
2864
+ detail = `${s("observation") || "sweep"}${typeof d.age_ms === "number" ? ` · age=${Math.round(d.age_ms / 1000)}s` : ""}`;
2357
2865
  break;
2358
2866
  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
2867
  return null;
2369
2868
  }
2370
2869
  return { ts: ev.ts, event_type: ev.event_type, detail };
2371
2870
  }
2871
+ export function pendingFinalizationTraceEntries(requests, instanceId) {
2872
+ return requests
2873
+ .filter((request) => request.status === "pending" && request.instance_id === instanceId)
2874
+ .map((request) => ({
2875
+ ts: request.observed_at,
2876
+ event_type: "session.finalization_pending",
2877
+ detail: `trigger=${request.trigger} · request=${request.request_id}${request.allowed_open_span_ids?.length ? ` · allowed_open_spans=${request.allowed_open_span_ids.length}` : ""}`,
2878
+ }));
2879
+ }
2372
2880
  function runTrace(name, opts) {
2373
2881
  if (opts.json)
2374
2882
  emit.config({ format: "json" });
@@ -2396,8 +2904,8 @@ function runTrace(name, opts) {
2396
2904
  targetId = candidates[0][0];
2397
2905
  else if (candidates.length > 1)
2398
2906
  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
2907
+ else if (/^(?:inst_)?[0-9a-z._-]{8,}$/i.test(wanted))
2908
+ targetId = wanted;
2401
2909
  }
2402
2910
  if (!targetId) {
2403
2911
  emit.error({
@@ -2408,33 +2916,16 @@ function runTrace(name, opts) {
2408
2916
  }
2409
2917
  const sinceMs = opts.since ? Date.now() - (parseWindowSecs(opts.since) ?? 0) * 1000 : 0;
2410
2918
  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");
2919
+ const diagnosticRead = readAgentDiagnosticEventsInWindow(root, sinceMs);
2920
+ const nativeCandidateIds = targetId.includes("\x00") ? targetId.split("\x00") : [targetId];
2921
+ const candidateIds = traceInstanceIdsForEventSource(nativeCandidateIds, diagnosticRead.source);
2417
2922
  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)
2923
+ for (const ev of diagnosticRead.events) {
2924
+ if (!ev.instance_id || !candidateIds.includes(ev.instance_id))
2421
2925
  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`);
2926
+ const arr = byId.get(ev.instance_id) ?? [];
2927
+ arr.push(ev);
2928
+ byId.set(ev.instance_id, arr);
2438
2929
  }
2439
2930
  // If the name mapped to multiple instances, trace the one with the latest event.
2440
2931
  let resolvedId = candidateIds[0];
@@ -2450,24 +2941,78 @@ function runTrace(name, opts) {
2450
2941
  }
2451
2942
  }
2452
2943
  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)
2944
+ let state = foldSessionState(events, { instance_id: resolvedId });
2945
+ let sessionState = events.some((event) => event.event_type === "session.ended")
2946
+ ? "ended"
2947
+ : "live";
2948
+ if (diagnosticRead.source === "v3" && diagnosticRead.authoritative) {
2949
+ try {
2950
+ const view = projectCoordinationViewV3(readLedgerV3(root));
2951
+ const liveGeneration = view.instances[resolvedId];
2952
+ const terminalGeneration = Object.values(view.terminal_generations).find((candidate) => candidate.instance_id === resolvedId);
2953
+ const generation = liveGeneration ?? terminalGeneration;
2954
+ if (generation) {
2955
+ sessionState = terminalGeneration && !liveGeneration ? "ended" : "live";
2956
+ const lifecycleState = generation.lifecycle_state;
2957
+ state = {
2958
+ activity: generation.activity === "terminal" ? "idle" : generation.activity,
2959
+ activity_updated_at: generation.last_observed_at,
2960
+ activity_source: "event-v3-coordination-view",
2961
+ task_state: lifecycleState === "active" || lifecycleState === "blocked" || lifecycleState === "done"
2962
+ ? lifecycleState
2963
+ : "active",
2964
+ task_state_updated_at: generation.lifecycle_state_updated_at,
2965
+ };
2966
+ }
2967
+ }
2968
+ catch {
2969
+ diagnosticRead.authoritative = false;
2970
+ diagnosticRead.reason = "V3 coordination projection is unavailable";
2971
+ }
2972
+ }
2973
+ let pendingFinalizations = [];
2974
+ if (diagnosticRead.source === "v3") {
2975
+ try {
2976
+ pendingFinalizations = listSessionFinalizationRequestsV3(root).filter((request) => request.status === "pending" && request.instance_id === resolvedId);
2977
+ }
2978
+ catch {
2979
+ diagnosticRead.authoritative = false;
2980
+ diagnosticRead.reason = "V3 finalization requests are unreadable";
2981
+ }
2982
+ }
2983
+ const lines = [
2984
+ ...events
2985
+ .map((ev) => traceLine(ev, !!opts.allTools))
2986
+ .filter((l) => l !== null),
2987
+ ...pendingFinalizationTraceEntries(pendingFinalizations, resolvedId),
2988
+ ]
2457
2989
  // Sort by timestamp, not file order: codex replays events (original ts,
2458
2990
  // appended later), so append-order ≠ chronological order.
2459
2991
  .sort((a, b) => Date.parse(a.ts) - Date.parse(b.ts));
2460
2992
  const shown = lines.slice(-limit);
2461
- const displayName = nameById.get(resolvedId) ?? resolvedId.slice(0, 8);
2993
+ const nativeResolvedId = diagnosticRead.source === "v3"
2994
+ ? nativeCandidateIds[candidateIds.indexOf(resolvedId)]
2995
+ : resolvedId;
2996
+ const displayName = nameById.get(resolvedId) ??
2997
+ (nativeResolvedId ? nameById.get(nativeResolvedId) : undefined) ??
2998
+ resolvedId.slice(0, 8);
2462
2999
  const result = {
3000
+ event_source: {
3001
+ contract: diagnosticRead.source,
3002
+ authoritative: diagnosticRead.authoritative,
3003
+ reason: diagnosticRead.reason ?? null,
3004
+ },
2463
3005
  name: displayName,
2464
3006
  instance_id: resolvedId,
2465
3007
  other_instances: candidateIds.filter((id) => id !== resolvedId),
3008
+ pending_finalizations: pendingFinalizations,
2466
3009
  total_events: events.length,
3010
+ session_state: sessionState,
2467
3011
  activity: state.activity,
2468
3012
  activity_updated_at: state.activity_updated_at ?? null,
2469
3013
  activity_source: state.activity_source ?? null,
2470
3014
  task_state: state.task_state,
3015
+ task_state_scope: sessionState === "ended" ? "historical" : "current",
2471
3016
  task_state_updated_at: state.task_state_updated_at ?? null,
2472
3017
  task_state_reason: state.task_state_reason ?? null,
2473
3018
  shown: shown.length,
@@ -2480,7 +3025,10 @@ function runTrace(name, opts) {
2480
3025
  emit.data(result);
2481
3026
  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
3027
  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
3028
+ 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
3029
+ if (!diagnosticRead.authoritative) {
3030
+ process.stdout.write(` unavailable: ${diagnosticRead.reason ?? "event source incomplete"}\n`); // lint-ok-emission: explicit no-fallback diagnostic
3031
+ }
2484
3032
  if (shown.length === 0) {
2485
3033
  process.stdout.write(" (no events)\n"); // lint-ok-emission: human trace view
2486
3034
  return;
@@ -2490,6 +3038,53 @@ function runTrace(name, opts) {
2490
3038
  process.stdout.write(` ${t} ${l.event_type.padEnd(22)} ${l.detail}\n`); // lint-ok-emission: human trace view
2491
3039
  }
2492
3040
  }
3041
+ /**
3042
+ * V3 generations, not disposable heartbeat caches, are the active-agent authority.
3043
+ */
3044
+ export function collectActiveAgentHealth(root, nowMs = Date.now()) {
3045
+ const source = "event-ledger-v3";
3046
+ let heartbeats = [];
3047
+ try {
3048
+ const control = readEventV3ControlState(root);
3049
+ if (control.state === "candidate" || control.state === "active") {
3050
+ heartbeats = readLiveCoordinationRows(root);
3051
+ }
3052
+ }
3053
+ catch {
3054
+ // The event-ledger section reports the authority read failure. Do not
3055
+ // substitute stale cache rows for a live V3 route.
3056
+ }
3057
+ const byPlatform = {};
3058
+ const byKind = {};
3059
+ const bySchema = {};
3060
+ let stale = 0;
3061
+ for (const heartbeat of heartbeats) {
3062
+ const platform = formatPlatformLabel(heartbeat.platform);
3063
+ byPlatform[platform] = (byPlatform[platform] ?? 0) + 1;
3064
+ const kind = heartbeat.kind ?? "unknown";
3065
+ byKind[kind] = (byKind[kind] ?? 0) + 1;
3066
+ // These rows exist only after an authority-safe V3 projection. The
3067
+ // disposable heartbeat file has its own cache-format version, which must
3068
+ // not be reported as the ledger schema version.
3069
+ bySchema.v3 = (bySchema.v3 ?? 0) + 1;
3070
+ const lastObservedMs = heartbeat.last_heartbeat
3071
+ ? Date.parse(heartbeat.last_heartbeat)
3072
+ : Number.NaN;
3073
+ const ageMs = Number.isFinite(lastObservedMs)
3074
+ ? nowMs - lastObservedMs
3075
+ : Number.POSITIVE_INFINITY;
3076
+ if (ageMs > freshnessCutoffSecs() * 1000)
3077
+ stale += 1;
3078
+ }
3079
+ return {
3080
+ source,
3081
+ total: heartbeats.length,
3082
+ by_platform: byPlatform,
3083
+ by_kind: byKind,
3084
+ by_schema_version: bySchema,
3085
+ stale,
3086
+ };
3087
+ }
2493
3088
  function runHealth(opts) {
2494
3089
  if (opts.json)
2495
3090
  emit.config({ format: "json" });
@@ -2509,11 +3104,12 @@ function runHealth(opts) {
2509
3104
  });
2510
3105
  process.exit(1);
2511
3106
  }
2512
- const cutoffMs = Date.now() - sinceSecs * 1000;
3107
+ const nowMs = Date.now();
3108
+ const cutoffMs = nowMs - sinceSecs * 1000;
2513
3109
  const activeDir = resolve(root, ".harnery/active");
2514
3110
  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
3111
+ // Coordination telemetry reads from the canonical V3 ledger.
3112
+ // Heals come from health.observed; council activity from council.state_changed.
2517
3113
  const heal = [];
2518
3114
  let councilAdvanced = 0;
2519
3115
  let councilClosed = 0;
@@ -2521,32 +3117,35 @@ function runHealth(opts) {
2521
3117
  let sweptTotal = 0;
2522
3118
  const sweptByReason = {};
2523
3119
  const nameById = buildNameById(root);
2524
- for (const ev of readCanonicalEventsInWindow(root, cutoffMs)) {
3120
+ const diagnosticEvents = readAgentDiagnosticEventsInWindow(root, cutoffMs);
3121
+ for (const ev of diagnosticEvents.events) {
2525
3122
  const healEv = canonicalToHealEvent(ev, nameById);
2526
3123
  if (healEv) {
2527
3124
  heal.push(healEv);
2528
3125
  continue;
2529
3126
  }
2530
- switch (ev.event_type) {
2531
- case "council.round_open":
3127
+ if (ev.event_type === "council.state_changed") {
3128
+ const state = String(ev.payload?.new_state ?? "");
3129
+ if (state === "round_open")
2532
3130
  councilAdvanced++;
2533
- break;
2534
- case "council.close":
3131
+ if (state === "closed")
2535
3132
  councilClosed++;
2536
- break;
2537
- case "council.archive":
3133
+ if (state === "archived")
2538
3134
  councilArchived++;
2539
- break;
2540
- case "health.heartbeat_swept": {
3135
+ continue;
3136
+ }
3137
+ switch (ev.event_type) {
3138
+ case "lifecycle.sweep_observed": {
2541
3139
  sweptTotal++;
2542
- const reason = String(ev.data?.reason ?? "unknown");
3140
+ const reason = String(ev.payload?.observation ?? "unknown");
2543
3141
  sweptByReason[reason] = (sweptByReason[reason] ?? 0) + 1;
2544
3142
  break;
2545
3143
  }
2546
3144
  }
2547
3145
  }
2548
- const hookErrors = readHookErrors(root, cutoffMs);
3146
+ const hookErrors = readHookErrors(root, cutoffMs, nowMs);
2549
3147
  const stream = readStreamStats(root);
3148
+ const eventLedger = collectEventLedgerHealthV3(root);
2550
3149
  // Canonical health.* events carry the full instance_id, already resolved to
2551
3150
  // `agent-<name>` (or `agent-<hex8>` fallback) by canonicalToHealEvent via
2552
3151
  // buildNameById; no hex8→name dedup pass needed anymore.
@@ -2564,19 +3163,13 @@ function runHealth(opts) {
2564
3163
  .sort((a, b) => b[1] - a[1])
2565
3164
  .slice(0, 5)
2566
3165
  .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;
3166
+ const activeAgents = collectActiveAgentHealth(root, nowMs);
2573
3167
  // Zombies: files in active/ that are broken: unparseable, nameless, or an
2574
3168
  // absurd (epoch-ish) last_heartbeat. These show as `agent-unknown` ghosts and
2575
3169
  // mean dead files the sweep isn't reaping.
2576
3170
  let zombieCount = 0;
2577
3171
  const zombieSamples = [];
2578
3172
  const ABSURD_AGE_MS = 24 * 60 * 60 * 1000; // > 1 day = clearly not a live, self-healing agent
2579
- const nowMs = Date.now();
2580
3173
  if (existsSync(activeDir)) {
2581
3174
  for (const file of readdirSync(activeDir)) {
2582
3175
  if (!file.endsWith(".json"))
@@ -2595,18 +3188,8 @@ function runHealth(opts) {
2595
3188
  zombieSamples.push(`${idFromFile.slice(0, 12)} (unparseable/no-id)`);
2596
3189
  continue;
2597
3190
  }
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
3191
  const lastHbMs = hb.last_heartbeat ? Date.parse(hb.last_heartbeat) : Number.NaN;
2607
3192
  const ageMs = Number.isFinite(lastHbMs) ? nowMs - lastHbMs : Number.POSITIVE_INFINITY;
2608
- if (ageMs > freshnessCutoffSecs() * 1000)
2609
- staleHeartbeats++;
2610
3193
  // Zombie heuristics on a parseable heartbeat: no name, or an age so large
2611
3194
  // it can only be a broken/epoch timestamp (a real agent would have healed).
2612
3195
  if (!hb.name || hb.name === "unknown" || ageMs > ABSURD_AGE_MS) {
@@ -2636,46 +3219,65 @@ function runHealth(opts) {
2636
3219
  anomalies.push(`${agent} self-healed ${count}x in ${opts.since}; possible idle-prune loop or PID instability`);
2637
3220
  }
2638
3221
  }
2639
- if (staleHeartbeats > 0) {
2640
- anomalies.push(`${staleHeartbeats} active heartbeat(s) older than ${Math.floor(freshnessCutoffSecs() / 60)}min; heal mechanism may not be firing`);
3222
+ if (activeAgents.stale > 0) {
3223
+ const noun = activeAgents.source === "event-ledger-v3" ? "V3 generation" : "heartbeat";
3224
+ const action = activeAgents.source === "event-ledger-v3"
3225
+ ? "lifecycle reconciliation may be delayed"
3226
+ : "heal mechanism may not be firing";
3227
+ anomalies.push(`${activeAgents.stale} active ${noun}(s) without activity for ${Math.floor(freshnessCutoffSecs() / 60)}min; ${action}`);
2641
3228
  }
2642
- const unexpectedSchemas = Object.keys(activeBySchema).filter((k) => k !== "v1");
3229
+ const expectedSchema = "v3";
3230
+ const unexpectedSchemas = Object.keys(activeAgents.by_schema_version).filter((schema) => schema !== expectedSchema);
2643
3231
  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}`);
3232
+ anomalies.push(`Unexpected active-agent schema versions in use: ${unexpectedSchemas.join(", ")} (expected ${expectedSchema})`);
3233
+ }
3234
+ // Only recent hook failures are active anomalies. Historical rows remain in
3235
+ // the report with exact error signatures and their latest timestamp.
3236
+ if (hookErrors.last1h > 0) {
3237
+ const top = hookErrors.recentTopErrors[0];
3238
+ const detail = top ? `: top '${top.error.slice(0, 80)}' x${top.count} (${top.phase})` : "";
3239
+ anomalies.push(`agent-hook errored ${hookErrors.last1h}x in the last hour${detail}`);
2655
3240
  }
2656
3241
  if (stream.cursor_backlog > 500) {
2657
3242
  anomalies.push(`projection cursor is ${stream.cursor_backlog} events behind; drain lagging (stop projection may be failing)`);
2658
3243
  }
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.
3244
+ // Raw V3 stream size is not an anomaly; catalog rotation bounds readers.
2664
3245
  if ((sweptByReason.unparseable ?? 0) > 0) {
2665
3246
  anomalies.push(`${sweptByReason.unparseable} heartbeat(s) swept as unparseable in ${opts.since}; possible corruption or a non-atomic writer`);
2666
3247
  }
2667
3248
  if (zombieCount > 0) {
2668
3249
  anomalies.push(`${zombieCount} zombie heartbeat(s) in active/ (${zombieSamples.join(", ")}); broken files the sweep isn't reaping`);
2669
3250
  }
3251
+ if (eventLedger.state === "live") {
3252
+ // Open spans whose generation has no open turn: the turn ended without its
3253
+ // tool spans closing, so an explicit end can only queue, never finalize.
3254
+ const orphanGenerations = eventLedger.open_spans.generations.filter((generation) => !generation.turn_open);
3255
+ if (orphanGenerations.length > 0) {
3256
+ const orphanSpans = orphanGenerations.reduce((sum, generation) => sum + generation.span_count, 0);
3257
+ anomalies.push(`${orphanSpans} open tool span(s) across ${orphanGenerations.length} generation(s) with no open turn; orphaned spans block clean session end`);
3258
+ }
3259
+ for (const pressure of eventLedger.span_pressure) {
3260
+ 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)`);
3261
+ }
3262
+ for (const collectionError of eventLedger.collection_errors) {
3263
+ anomalies.push(`event-ledger health: ${collectionError}`);
3264
+ }
3265
+ }
2670
3266
  const report = {
2671
3267
  since: opts.since,
2672
3268
  generated_at: new Date().toISOString(),
3269
+ event_telemetry: {
3270
+ source: diagnosticEvents.source,
3271
+ authoritative: diagnosticEvents.authoritative,
3272
+ ...(diagnosticEvents.reason ? { reason: diagnosticEvents.reason } : {}),
3273
+ },
2673
3274
  active_agents: {
2674
- total: activeTotal,
2675
- by_platform: activeByPlatform,
2676
- by_kind: activeByKind,
2677
- by_schema_version: activeBySchema,
2678
- stale: staleHeartbeats,
3275
+ source: activeAgents.source,
3276
+ total: activeAgents.total,
3277
+ by_platform: activeAgents.by_platform,
3278
+ by_kind: activeAgents.by_kind,
3279
+ by_schema_version: activeAgents.by_schema_version,
3280
+ stale: activeAgents.stale,
2679
3281
  },
2680
3282
  heal_events: {
2681
3283
  total: heal.length,
@@ -2691,9 +3293,19 @@ function runHealth(opts) {
2691
3293
  closed_in_window: councilClosed,
2692
3294
  },
2693
3295
  swept_events: { total: sweptTotal, by_reason: sweptByReason },
2694
- hook_errors: { total: hookErrors.total, by_phase: hookErrors.byPhase, top: hookErrors.top },
3296
+ hook_errors: {
3297
+ total: hookErrors.total,
3298
+ last_1h: hookErrors.last1h,
3299
+ latest_at: hookErrors.latestAt,
3300
+ by_phase: hookErrors.byPhase,
3301
+ by_error: hookErrors.byError,
3302
+ top: hookErrors.top,
3303
+ top_errors: hookErrors.topErrors,
3304
+ recent_top_errors: hookErrors.recentTopErrors,
3305
+ },
2695
3306
  stream,
2696
3307
  zombies: { count: zombieCount, samples: zombieSamples },
3308
+ event_ledger: eventLedger,
2697
3309
  anomalies,
2698
3310
  };
2699
3311
  if (opts.json) {
@@ -2729,16 +3341,38 @@ function renderHealthBox(report) {
2729
3341
  councilParts.push(`${report.councils.closed_in_window} closed`);
2730
3342
  if (report.councils.archived_in_window > 0)
2731
3343
  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` : ""}`;
3344
+ const activeSource = report.active_agents.source === "event-ledger-v3" ? "ledger" : "heartbeat cache";
3345
+ const activeStr = `${report.active_agents.total}${platforms ? ` (${platforms})` : ""}${schemas ? ` · ${schemas}` : ""}${report.active_agents.stale > 0 ? ` · ${report.active_agents.stale} stale` : ""} · ${activeSource}`;
2733
3346
  const sweptReasonStr = Object.entries(report.swept_events.by_reason)
2734
3347
  .map(([r, n]) => `${r} ${n}`)
2735
3348
  .join(", ");
2736
3349
  const hookErrStr = report.hook_errors.total === 0
2737
3350
  ? "0"
2738
- : `${report.hook_errors.total}${report.hook_errors.top[0] ? ` (${report.hook_errors.top[0].phase} x${report.hook_errors.top[0].count})` : ""}`;
3351
+ : `${report.hook_errors.total} in window · ${report.hook_errors.last_1h} in 1h`;
2739
3352
  const streamStr = `${(report.stream.bytes / 1048576).toFixed(1)}MB · ${report.stream.lines} lines · ${report.stream.cursor_backlog} behind`;
3353
+ const ledger = report.event_ledger;
3354
+ let ledgerStr;
3355
+ if (ledger.state !== "live") {
3356
+ ledgerStr = `unavailable (${ledger.reason})`;
3357
+ }
3358
+ else {
3359
+ const orphanGenerations = ledger.open_spans.generations.filter((generation) => !generation.turn_open).length;
3360
+ const pendingAges = ledger.pending_finalizations
3361
+ .map((request) => request.age_ms < 0 ? "age?" : formatAge(Math.floor(request.age_ms / 1000)))
3362
+ .join(", ");
3363
+ ledgerStr = [
3364
+ `open spans ${ledger.open_spans.total}${orphanGenerations > 0 ? ` (${orphanGenerations} gen turn-closed)` : ""}`,
3365
+ `pending ends ${ledger.pending_finalizations.length}${pendingAges ? ` (${pendingAges})` : ""}`,
3366
+ `intake ${ledger.intake_spool.total}`,
3367
+ `diagnostics ${ledger.diagnostics_spool.total} (${ledger.diagnostics_spool.last_1h} in 1h / ${ledger.diagnostics_spool.last_24h} in 24h)`,
3368
+ ].join(" · ");
3369
+ }
2740
3370
  const rows = [
2741
3371
  ["window", `last ${report.since}`],
3372
+ [
3373
+ "event source",
3374
+ `${report.event_telemetry.source.toUpperCase()} · ${report.event_telemetry.authoritative ? "validated" : `unavailable (${report.event_telemetry.reason ?? "incomplete"})`}`,
3375
+ ],
2742
3376
  ["active", activeStr],
2743
3377
  [
2744
3378
  "heals",
@@ -2748,6 +3382,7 @@ function renderHealthBox(report) {
2748
3382
  ["swept", `${report.swept_events.total}${sweptReasonStr ? ` (${sweptReasonStr})` : ""}`],
2749
3383
  ["hook errors", hookErrStr],
2750
3384
  ["stream", streamStr],
3385
+ ["event ledger", ledgerStr],
2751
3386
  [
2752
3387
  "zombies",
2753
3388
  report.zombies.count === 0
@@ -2851,6 +3486,8 @@ function runAdapterProbe(id, opts) {
2851
3486
  replayExitCode = result.exitCode;
2852
3487
  data.samples = samples;
2853
3488
  data.samples_summary = result.summary;
3489
+ if (result.note)
3490
+ data.samples_note = result.note;
2854
3491
  }
2855
3492
  emit.data(data);
2856
3493
  if (process.stdout.isTTY && !opts.json) {
@@ -2867,7 +3504,7 @@ function runAdapterProbe(id, opts) {
2867
3504
  const summary = data.samples_summary;
2868
3505
  lines.push("");
2869
3506
  if (samples.length === 0) {
2870
- lines.push(` Sample replay: no .json fixtures found under ${sampleDir}`);
3507
+ lines.push(` Sample replay: ${String(data.samples_note ?? `no .json fixtures found under ${sampleDir}`)}`);
2871
3508
  }
2872
3509
  else {
2873
3510
  lines.push(` Sample replay (${samples.length} fixture${samples.length === 1 ? "" : "s"}):`);
@@ -2918,6 +3555,7 @@ function replayAdapterSamples(adapter, root, relativeSampleDir, filter) {
2918
3555
  samples: [],
2919
3556
  exitCode: 0,
2920
3557
  summary: { total: 0, pass: 0, fail: 0, skipped: 0 },
3558
+ note: `sample directory not found: ${relativeSampleDir} — replay verified nothing`,
2921
3559
  };
2922
3560
  }
2923
3561
  const fixtures = readdirSync(sampleDir)
@@ -2929,6 +3567,7 @@ function replayAdapterSamples(adapter, root, relativeSampleDir, filter) {
2929
3567
  samples: [],
2930
3568
  exitCode: 0,
2931
3569
  summary: { total: 0, pass: 0, fail: 0, skipped: 0 },
3570
+ note: `no .json fixtures under ${relativeSampleDir} — replay verified nothing`,
2932
3571
  };
2933
3572
  }
2934
3573
  // agent-hook is the single entry point. Replay sample payloads against it,
@@ -3143,10 +3782,24 @@ function runPing(name, message, opts) {
3143
3782
  });
3144
3783
  process.exit(1);
3145
3784
  }
3146
- const myHb = readHeartbeat(myOwner);
3785
+ const myHb = readCurrentCoordinationRow(myOwner);
3147
3786
  const fromName = myHb?.name ?? "anonymous";
3148
3787
  const body = `from agent-${fromName}: ${message.trim()}`;
3149
3788
  const doc = appendEntry(peerOwner, "handoff", body);
3789
+ // Canonical delivery record: sender is the envelope owner, recipient rides
3790
+ // in data, so read-only observers can render the communication without
3791
+ // joining journal files.
3792
+ emitEventV3({
3793
+ owner: myOwner,
3794
+ session: nativeSessionIdentity(myHb, myOwner),
3795
+ adapter: normalizeAdapter(myHb?.platform),
3796
+ observation: {
3797
+ event_type: "coord.message_observed",
3798
+ direction: "sent",
3799
+ subject: peerOwner,
3800
+ body: message.trim(),
3801
+ },
3802
+ });
3150
3803
  const data = {
3151
3804
  peer: name,
3152
3805
  peer_instance_id: peerOwner,
@@ -3195,7 +3848,7 @@ async function runWait(name, opts) {
3195
3848
  }
3196
3849
  let lastProgressMs = 0;
3197
3850
  while (true) {
3198
- const hb = readHeartbeat(peerOwner);
3851
+ const hb = readCurrentCoordinationRow(peerOwner);
3199
3852
  const now = Date.now();
3200
3853
  const elapsedMs = now - startMs;
3201
3854
  if (!hb) {
@@ -3230,9 +3883,8 @@ async function runWait(name, opts) {
3230
3883
  const progressGapMs = Math.max(pollMs, 30_000);
3231
3884
  if (!opts.quiet && now - lastProgressMs >= progressGapMs) {
3232
3885
  lastProgressMs = now;
3233
- const lastTool = hb.last_tool ? `, last=${hb.last_tool}` : "";
3234
3886
  const elapsedStr = formatAge(Math.floor(elapsedMs / 1000));
3235
- const progress = ` [${elapsedStr}] ${stillBlocking.length} file(s) blocking${lastTool}\n`;
3887
+ const progress = ` [${elapsedStr}] ${stillBlocking.length} file(s) blocking\n`;
3236
3888
  process.stderr.write(progress); // lint-ok-emission: per-poll progress heartbeat to stderr
3237
3889
  }
3238
3890
  await new Promise((r) => setTimeout(r, pollMs));
@@ -3267,10 +3919,20 @@ function runHeal(opts) {
3267
3919
  emit.error({ code: "missing_owner", message: "--owner is required" });
3268
3920
  process.exit(1);
3269
3921
  }
3270
- if (kind !== "pidmap" && kind !== "heartbeat" && kind !== "kill") {
3922
+ if (kind !== "pidmap" && kind !== "cache") {
3271
3923
  emit.error({
3272
3924
  code: "bad_kind",
3273
- message: "--kind must be one of: pidmap, heartbeat, kill",
3925
+ message: "--kind must be one of: pidmap, cache",
3926
+ });
3927
+ process.exit(1);
3928
+ }
3929
+ if (opts.adapter &&
3930
+ opts.adapter !== "claude-code" &&
3931
+ opts.adapter !== "cursor" &&
3932
+ opts.adapter !== "codex") {
3933
+ emit.error({
3934
+ code: "bad_adapter",
3935
+ message: "--adapter must be one of: claude-code, cursor, codex",
3274
3936
  });
3275
3937
  process.exit(1);
3276
3938
  }
@@ -3282,8 +3944,8 @@ function runHeal(opts) {
3282
3944
  });
3283
3945
  process.exit(1);
3284
3946
  }
3285
- const action = kind === "pidmap" ? "heal-pidmap" : kind === "heartbeat" ? "heal-heartbeat" : "kill-heartbeat";
3286
- // Refuse to mint a heartbeat at a TRUNCATED owner id.
3947
+ const action = kind === "pidmap" ? "heal-pidmap" : "repair-coordination-cache";
3948
+ // Refuse to materialize a V3 cache at a truncated owner id.
3287
3949
  //
3288
3950
  // Heartbeats are keyed by the whole instance_id, so healing at an
3289
3951
  // abbreviated id writes `.harnery/active/<prefix>.json` while every reader
@@ -3303,32 +3965,31 @@ function runHeal(opts) {
3303
3965
  // whose instance_id this id is a prefix of settles it just as well, and is
3304
3966
  // present in precisely the case that matters (the session the reader was
3305
3967
  // trying to heal is registered, just not under the abbreviated name).
3306
- if (kind === "heartbeat" && !readHeartbeat(owner)) {
3968
+ if (kind === "cache" && !readHeartbeatCache(root, owner)) {
3307
3969
  const canonical = opts.sessionId && opts.sessionId.trim() !== owner && opts.sessionId.trim().startsWith(owner)
3308
3970
  ? opts.sessionId.trim()
3309
3971
  : liveIdWithPrefix(root, owner);
3310
3972
  if (canonical) {
3311
- const source = opts.sessionId?.trim() === canonical ? "--session-id" : "a live heartbeat";
3973
+ const source = opts.sessionId?.trim() === canonical ? "--session-id" : "a live V3 generation";
3312
3974
  emit.error({
3313
3975
  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}.`,
3976
+ message: `--owner ${owner} is a prefix of ${canonical} (${source}). Cache repair requires ` +
3977
+ `the canonical instance id; re-run with --owner ${canonical}.`,
3317
3978
  });
3318
3979
  process.exit(1);
3319
3980
  }
3320
3981
  }
3321
3982
  // Build positional args. agent-coord's arg layout:
3322
3983
  // heal-pidmap <instance_id> [<pid>]
3323
- // heal-heartbeat <instance_id> [<session_id>]
3324
- // kill-heartbeat <instance_id>
3984
+ // repair-coordination-cache <instance_id> [<session_id>]
3325
3985
  const helperArgs = [action, owner];
3326
3986
  if (kind === "pidmap" && opts.pid)
3327
3987
  helperArgs.push(opts.pid);
3328
- if (kind === "heartbeat" && opts.sessionId)
3988
+ if (kind === "cache" && opts.sessionId)
3329
3989
  helperArgs.push(opts.sessionId);
3330
- // heal-pidmap / heal-heartbeat / kill-heartbeat are handled by the
3331
- // bundled agent-coord binary.
3990
+ if (kind === "cache" && opts.adapter)
3991
+ helperArgs.push(`--adapter=${opts.adapter}`);
3992
+ // Both recovery actions are handled by the bundled agent-coord binary.
3332
3993
  const helper = agentCoordOrExit(root);
3333
3994
  const proc = spawnSync(helper, helperArgs, {
3334
3995
  encoding: "utf8",
@@ -3341,26 +4002,21 @@ function runHeal(opts) {
3341
4002
  });
3342
4003
  process.exit(1);
3343
4004
  }
3344
- // Re-read the heartbeat to surface post-action state (or null if killed).
4005
+ // Read the derived cache to surface post-action state.
3345
4006
  let after = null;
3346
4007
  try {
3347
- after = readHeartbeat(owner);
4008
+ after = readHeartbeatCache(root, owner);
3348
4009
  }
3349
4010
  catch {
3350
4011
  after = null;
3351
4012
  }
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
4013
  const outcome = kind === "pidmap"
3358
4014
  ? proc.status === 0
3359
4015
  ? "ok"
3360
4016
  : "failed"
3361
4017
  : after
3362
- ? "heartbeat_present"
3363
- : "heartbeat_absent";
4018
+ ? "cache_present"
4019
+ : "cache_absent";
3364
4020
  emit.data({
3365
4021
  rows: [
3366
4022
  {
@@ -3383,31 +4039,35 @@ function runHeal(opts) {
3383
4039
  emit.text(`agent-coord ${action} ok\n`);
3384
4040
  }
3385
4041
  else {
3386
- emit.text(`agent-coord ${action} ok: heartbeat ${after ? "present" : "absent"} after\n`);
4042
+ emit.text(`agent-coord ${action} ok: cache ${after ? "present" : "absent"} after\n`);
3387
4043
  }
3388
4044
  }
3389
- // Canonical health.* emission is owned by the writer (heartbeat-writer.ts
3390
- // healPidmap/healHeartbeat), so it fires inside the agent-coord subprocess
4045
+ // Canonical health.* emission is owned by the cache writer, so it fires
4046
+ // inside the agent-coord subprocess
3391
4047
  // above on actual writes only: write-only telemetry, no double-emit, no
3392
4048
  // event when an already-correct heal no-ops. (Previously emitted here
3393
4049
  // unconditionally on every `harn agents heal`, which over-counted no-op heals.)
3394
4050
  }
3395
4051
  /**
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).
4052
+ * Emit one V3 council audit transition keyed to the exact durable manifest.
4053
+ * Falls through silently when no live session can attest the observation.
3399
4054
  */
3400
- function emitCouncilStateEvent(type, manifest, extraData) {
4055
+ function emitCouncilStateEvent(manifest, newState, priorState) {
3401
4056
  const myOwner = resolveOwner();
3402
4057
  if (!myOwner)
3403
4058
  return;
3404
- const hb = readHeartbeat(myOwner);
3405
- emitCanonical({
3406
- type,
4059
+ const hb = readCurrentCoordinationRow(myOwner);
4060
+ emitEventV3({
3407
4061
  owner: myOwner,
3408
- session: hb?.session_id ?? myOwner,
4062
+ session: nativeSessionIdentity(hb, myOwner),
3409
4063
  adapter: normalizeAdapter(hb?.platform),
3410
- data: { council_id: manifest.council_id, ...extraData },
4064
+ observation: {
4065
+ event_type: "council.state_changed",
4066
+ council_id: manifest.council_id,
4067
+ ...(priorState ? { prior_state: priorState } : {}),
4068
+ new_state: newState,
4069
+ record: manifest,
4070
+ },
3411
4071
  });
3412
4072
  }
3413
4073
  // ──────── council subcommand impls ────────
@@ -3450,7 +4110,7 @@ function runCouncilCreate(objective, opts) {
3450
4110
  createdBy = normalizeAgentName(opts.createdBy);
3451
4111
  }
3452
4112
  else if (myOwner) {
3453
- const myHb = readHeartbeat(myOwner);
4113
+ const myHb = readCurrentCoordinationRow(myOwner);
3454
4114
  if (myHb?.name) {
3455
4115
  createdBy = normalizeAgentName(myHb.name);
3456
4116
  }
@@ -3506,17 +4166,16 @@ function runCouncilCreate(objective, opts) {
3506
4166
  writeFileSync(resolve(body, "invite.md"), buildInviteMarkdown(manifest), "utf8");
3507
4167
  writeManifest(manifest);
3508
4168
  if (myOwner) {
3509
- const myHbForEmit = readHeartbeat(myOwner);
3510
- emitCanonical({
3511
- type: "council.open",
4169
+ const myHbForEmit = readCurrentCoordinationRow(myOwner);
4170
+ emitEventV3({
3512
4171
  owner: myOwner,
3513
- session: myHbForEmit?.session_id ?? myOwner,
4172
+ session: nativeSessionIdentity(myHbForEmit, myOwner),
3514
4173
  adapter: normalizeAdapter(myHbForEmit?.platform),
3515
- data: {
3516
- council_id: councilId,
3517
- topic: trimmedObjective,
3518
- members,
3519
- target_doc: manifest.target_doc ?? undefined,
4174
+ observation: {
4175
+ event_type: "council.state_changed",
4176
+ council_id: manifest.council_id,
4177
+ new_state: "active",
4178
+ record: manifest,
3520
4179
  },
3521
4180
  });
3522
4181
  }
@@ -3528,7 +4187,7 @@ function runCouncilCreate(objective, opts) {
3528
4187
  const skippedMembers = [];
3529
4188
  for (const memberName of members) {
3530
4189
  const bareName = memberName.replace(/^agent-/, "");
3531
- if (myOwner && readHeartbeat(myOwner)?.name === bareName) {
4190
+ if (myOwner && readCurrentCoordinationRow(myOwner)?.name === bareName) {
3532
4191
  // Convener is themselves a member; skip the self-ping
3533
4192
  continue;
3534
4193
  }
@@ -3585,7 +4244,7 @@ function runCouncilList(opts) {
3585
4244
  if (opts.mine) {
3586
4245
  const myOwner = resolveOwner();
3587
4246
  if (myOwner) {
3588
- const myHb = readHeartbeat(myOwner);
4247
+ const myHb = readCurrentCoordinationRow(myOwner);
3589
4248
  if (myHb?.name)
3590
4249
  myName = normalizeAgentName(myHb.name);
3591
4250
  }
@@ -3749,7 +4408,7 @@ function runCouncilClose(id, opts) {
3749
4408
  closed_at: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
3750
4409
  };
3751
4410
  writeManifest(next);
3752
- emitCouncilStateEvent("council.close", next, { closed_at: next.closed_at });
4411
+ emitCouncilStateEvent(next, "closed", manifest.status);
3753
4412
  // Build the transcript: every round's contributions in order.
3754
4413
  const transcript = buildTranscript(next);
3755
4414
  emit.data({
@@ -3788,7 +4447,7 @@ function runCouncilArchive(id, opts) {
3788
4447
  // updated status. moveToArchive then physically relocates the artifacts.
3789
4448
  writeManifest(next);
3790
4449
  moveToArchive(next.council_id);
3791
- emitCouncilStateEvent("council.archive", next, {});
4450
+ emitCouncilStateEvent(next, "archived", manifest.status);
3792
4451
  emit.data({
3793
4452
  rows: [
3794
4453
  {
@@ -3842,7 +4501,7 @@ function runCouncilUnarchive(id, opts) {
3842
4501
  // (allows re-running for testing).
3843
4502
  moveFromArchive(next.council_id);
3844
4503
  writeManifest(next);
3845
- emitCouncilStateEvent("council.unarchive", next, { restored_status: restoredStatus });
4504
+ emitCouncilStateEvent(next, restoredStatus, "archived");
3846
4505
  emit.data({
3847
4506
  rows: [
3848
4507
  {
@@ -3895,7 +4554,7 @@ function runCouncilDelete(id, opts) {
3895
4554
  }
3896
4555
  const removed = deleteArchivedCouncil(manifest.council_id);
3897
4556
  if (removed) {
3898
- emitCouncilStateEvent("council.delete", manifest, {});
4557
+ emitCouncilStateEvent(manifest, "deleted", "archived");
3899
4558
  }
3900
4559
  emit.data({
3901
4560
  rows: [
@@ -4067,7 +4726,7 @@ function runCouncilContribute(id, opts) {
4067
4726
  try {
4068
4727
  const myOwner = resolveOwner();
4069
4728
  if (myOwner) {
4070
- const myHb = readHeartbeat(myOwner);
4729
+ const myHb = readCurrentCoordinationRow(myOwner);
4071
4730
  if (myHb?.name)
4072
4731
  actualName = normalizeAgentName(myHb.name);
4073
4732
  }
@@ -4085,7 +4744,7 @@ function runCouncilContribute(id, opts) {
4085
4744
  });
4086
4745
  process.exit(1);
4087
4746
  }
4088
- const myHb = readHeartbeat(myOwner);
4747
+ const myHb = readCurrentCoordinationRow(myOwner);
4089
4748
  if (!myHb?.name) {
4090
4749
  emit.error({
4091
4750
  code: "no_self_name",
@@ -4136,11 +4795,7 @@ function runCouncilContribute(id, opts) {
4136
4795
  process.exit(1);
4137
4796
  }
4138
4797
  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
- });
4798
+ emitCouncilStateEvent(manifest, "contribution_recorded", manifest.round_status);
4144
4799
  // Update manifest: if all members have now contributed, flip round_status.
4145
4800
  const contributorsNow = contributorsInRound(manifest.council_id, manifest.current_round);
4146
4801
  const allIn = manifest.members.every((m) => contributorsNow.includes(m));
@@ -4149,14 +4804,10 @@ function runCouncilContribute(id, opts) {
4149
4804
  if (allIn) {
4150
4805
  nextManifest = { ...manifest, round_status: "collected" };
4151
4806
  writeManifest(nextManifest);
4152
- emitCouncilStateEvent("council.round_close", nextManifest, {
4153
- round_no: nextManifest.current_round,
4154
- });
4807
+ emitCouncilStateEvent(nextManifest, "round_closed", manifest.round_status);
4155
4808
  if (manifest.auto_advance) {
4156
4809
  nextManifest = advanceCouncil(nextManifest, /*force=*/ false);
4157
- emitCouncilStateEvent("council.round_open", nextManifest, {
4158
- round_no: nextManifest.current_round,
4159
- });
4810
+ emitCouncilStateEvent(nextManifest, "round_open", "round_closed");
4160
4811
  autoAdvanced = true;
4161
4812
  }
4162
4813
  }
@@ -4241,7 +4892,7 @@ function runCouncilPrompt(id, member, opts) {
4241
4892
  try {
4242
4893
  const myOwner = resolveOwner();
4243
4894
  if (myOwner) {
4244
- const myHb = readHeartbeat(myOwner);
4895
+ const myHb = readCurrentCoordinationRow(myOwner);
4245
4896
  if (myHb?.name)
4246
4897
  actualName = normalizeAgentName(myHb.name);
4247
4898
  }
@@ -4259,7 +4910,7 @@ function runCouncilPrompt(id, member, opts) {
4259
4910
  });
4260
4911
  process.exit(1);
4261
4912
  }
4262
- const myHb = readHeartbeat(myOwner);
4913
+ const myHb = readCurrentCoordinationRow(myOwner);
4263
4914
  if (!myHb?.name) {
4264
4915
  emit.error({
4265
4916
  code: "no_self_name",
@@ -4410,12 +5061,8 @@ function runCouncilAdvance(id, opts) {
4410
5061
  process.exit(1);
4411
5062
  }
4412
5063
  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
- });
5064
+ emitCouncilStateEvent(manifest, "round_closed", manifest.round_status);
5065
+ emitCouncilStateEvent(next, "round_open", "round_closed");
4419
5066
  emit.data({
4420
5067
  rows: [
4421
5068
  {
@@ -4457,7 +5104,7 @@ function advanceCouncil(manifest, force) {
4457
5104
  // (Convener already knows; we skip pinging them if they convened it from
4458
5105
  // their own session.)
4459
5106
  const myOwner = resolveOwner();
4460
- const myName = myOwner ? normalizeAgentName(readHeartbeat(myOwner)?.name ?? "") : "";
5107
+ const myName = myOwner ? normalizeAgentName(readCurrentCoordinationRow(myOwner)?.name ?? "") : "";
4461
5108
  for (const memberName of next.members) {
4462
5109
  if (memberName === myName)
4463
5110
  continue;
@@ -4478,7 +5125,7 @@ function advanceCouncil(manifest, force) {
4478
5125
  // Hard cap on box width. Values longer than the per-row budget word-wrap to
4479
5126
  // continuation lines (blank key column, value resumes indented). Picked to
4480
5127
  // stay readable in narrow terminals + chat clients while giving long
4481
- // turn_summary / task values room to breathe.
5128
+ // Task values need room to breathe.
4482
5129
  const MAX_BOX_CONTENT_WIDTH = 100;
4483
5130
  function formatBox(title, rows) {
4484
5131
  const titleStr = ` ${title} `;