harnery 0.32.0 → 0.34.0

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