harnery 0.42.0 → 0.44.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 (343) hide show
  1. package/README.md +3 -3
  2. package/dist/commander.d.ts +16 -0
  3. package/dist/commander.d.ts.map +1 -1
  4. package/dist/commander.js +18 -0
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +95 -52
  7. package/dist/commands/browse.d.ts.map +1 -1
  8. package/dist/commands/browse.js +38 -11
  9. package/dist/commands/diagnostics.d.ts.map +1 -1
  10. package/dist/commands/diagnostics.js +10 -0
  11. package/dist/commands/doctor.d.ts +12 -0
  12. package/dist/commands/doctor.d.ts.map +1 -1
  13. package/dist/commands/doctor.js +22 -7
  14. package/dist/commands/governor.js +22 -10
  15. package/dist/commands/inbox.d.ts +1 -1
  16. package/dist/commands/inbox.d.ts.map +1 -1
  17. package/dist/commands/inbox.js +55 -2
  18. package/dist/commands/rm.d.ts.map +1 -1
  19. package/dist/commands/rm.js +28 -4
  20. package/dist/commands/servers.d.ts.map +1 -1
  21. package/dist/commands/servers.js +16 -1
  22. package/dist/commands/supervisor.d.ts.map +1 -1
  23. package/dist/commands/supervisor.js +7 -2
  24. package/dist/commands/toc.d.ts +14 -0
  25. package/dist/commands/toc.d.ts.map +1 -1
  26. package/dist/commands/toc.js +7 -4
  27. package/dist/commands/tunnel.d.ts +65 -0
  28. package/dist/commands/tunnel.d.ts.map +1 -1
  29. package/dist/commands/tunnel.js +290 -101
  30. package/dist/commands/web-fetch.d.ts +3 -2
  31. package/dist/commands/web-fetch.d.ts.map +1 -1
  32. package/dist/commands/web-fetch.js +25 -19
  33. package/dist/commands/web.d.ts +7 -1
  34. package/dist/commands/web.d.ts.map +1 -1
  35. package/dist/commands/web.js +9 -3
  36. package/dist/commands/workflow.js +24 -30
  37. package/dist/core/agents/live-authority-v3.d.ts +1 -0
  38. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  39. package/dist/core/agents/live-authority-v3.js +11 -0
  40. package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
  41. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  42. package/dist/core/agents/live-lifecycle-v3.js +28 -7
  43. package/dist/core/agents/mailbox.d.ts +9 -8
  44. package/dist/core/agents/mailbox.d.ts.map +1 -1
  45. package/dist/core/agents/mailbox.js +24 -17
  46. package/dist/core/agents/render/prompt-context.d.ts +2 -0
  47. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  48. package/dist/core/agents/render/prompt-context.js +19 -17
  49. package/dist/core/agents/render/session-context.d.ts +2 -8
  50. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  51. package/dist/core/agents/render/session-context.js +64 -43
  52. package/dist/core/agents/rules/stop-hook.js +3 -3
  53. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  54. package/dist/core/agents/state/heartbeat-writer.js +25 -14
  55. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  56. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  57. package/dist/core/agents/state/names.d.ts +43 -3
  58. package/dist/core/agents/state/names.d.ts.map +1 -1
  59. package/dist/core/agents/state/names.js +87 -3
  60. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  61. package/dist/core/agents/state/pidmap.js +5 -0
  62. package/dist/core/agents/state/proc-start.d.ts +19 -0
  63. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  64. package/dist/core/agents/state/proc-start.js +41 -0
  65. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  66. package/dist/core/agents/state/stale-sweep.js +180 -70
  67. package/dist/core/agents/state/unchanged-file.d.ts +44 -0
  68. package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
  69. package/dist/core/agents/state/unchanged-file.js +390 -0
  70. package/dist/core/context/index.d.ts +22 -10
  71. package/dist/core/context/index.d.ts.map +1 -1
  72. package/dist/core/context/index.js +381 -119
  73. package/dist/core/context/observation.d.ts +26 -0
  74. package/dist/core/context/observation.d.ts.map +1 -0
  75. package/dist/core/context/observation.js +11 -0
  76. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  77. package/dist/core/diagnostics/bundle.js +25 -14
  78. package/dist/core/diagnostics/path-identity.d.ts +3 -0
  79. package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
  80. package/dist/core/diagnostics/path-identity.js +12 -0
  81. package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
  82. package/dist/core/diagnostics/sanitize.js +4 -3
  83. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  84. package/dist/core/events/v3/authority-outbox.js +29 -0
  85. package/dist/core/events/v3/generation-custody.d.ts +5 -0
  86. package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
  87. package/dist/core/events/v3/generation-custody.js +50 -0
  88. package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
  89. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  90. package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
  91. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  92. package/dist/core/events/v3/producers/recorder.js +77 -64
  93. package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
  94. package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
  95. package/dist/core/governor/planning.d.ts.map +1 -1
  96. package/dist/core/governor/planning.js +14 -1
  97. package/dist/core/governor/service-read.d.ts +10 -2
  98. package/dist/core/governor/service-read.d.ts.map +1 -1
  99. package/dist/core/governor/service-read.js +105 -22
  100. package/dist/core/governor/service.d.ts +20 -2
  101. package/dist/core/governor/service.d.ts.map +1 -1
  102. package/dist/core/governor/service.js +72 -47
  103. package/dist/core/hooks/cli.js +120 -23
  104. package/dist/core/hooks/fork-lineage.d.ts +40 -0
  105. package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
  106. package/dist/core/hooks/fork-lineage.js +62 -0
  107. package/dist/core/hooks/resolve/transcript.d.ts +16 -0
  108. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  109. package/dist/core/hooks/resolve/transcript.js +84 -0
  110. package/dist/core/hooks/shell-rm.d.ts +2 -0
  111. package/dist/core/hooks/shell-rm.d.ts.map +1 -0
  112. package/dist/core/hooks/shell-rm.js +105 -0
  113. package/dist/core/hooks/shell-tokens.d.ts +12 -0
  114. package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
  115. package/dist/core/hooks/shell-tokens.js +97 -0
  116. package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
  117. package/dist/core/hooks/shell-waiter.js +3 -93
  118. package/dist/core/inbox/contract.d.ts +2 -1
  119. package/dist/core/inbox/contract.d.ts.map +1 -1
  120. package/dist/core/inbox/index.d.ts +2 -0
  121. package/dist/core/inbox/index.d.ts.map +1 -1
  122. package/dist/core/inbox/index.js +2 -0
  123. package/dist/core/inbox/peer-messages.d.ts +37 -0
  124. package/dist/core/inbox/peer-messages.d.ts.map +1 -0
  125. package/dist/core/inbox/peer-messages.js +72 -0
  126. package/dist/core/inbox/service.d.ts +8 -1
  127. package/dist/core/inbox/service.d.ts.map +1 -1
  128. package/dist/core/inbox/service.js +67 -21
  129. package/dist/core/inbox/targets.d.ts +22 -0
  130. package/dist/core/inbox/targets.d.ts.map +1 -0
  131. package/dist/core/inbox/targets.js +108 -0
  132. package/dist/core/inbox/watcher.js +7 -3
  133. package/dist/core/journal/index.d.ts.map +1 -1
  134. package/dist/core/journal/index.js +17 -2
  135. package/dist/core/resources/contract.d.ts +3 -0
  136. package/dist/core/resources/contract.d.ts.map +1 -1
  137. package/dist/core/resources/service-status.d.ts +14 -1
  138. package/dist/core/resources/service-status.d.ts.map +1 -1
  139. package/dist/core/resources/service-status.js +23 -13
  140. package/dist/core/resources/service.d.ts +9 -1
  141. package/dist/core/resources/service.d.ts.map +1 -1
  142. package/dist/core/resources/service.js +47 -27
  143. package/dist/core/resources/status.d.ts +2 -5
  144. package/dist/core/resources/status.d.ts.map +1 -1
  145. package/dist/core/resources/status.js +36 -7
  146. package/dist/core/semantic/contract.d.ts +5 -5
  147. package/dist/core/servers/index.d.ts +24 -13
  148. package/dist/core/servers/index.d.ts.map +1 -1
  149. package/dist/core/servers/index.js +132 -92
  150. package/dist/core/servers/tunnels.d.ts +3 -5
  151. package/dist/core/servers/tunnels.d.ts.map +1 -1
  152. package/dist/core/servers/tunnels.js +3 -9
  153. package/dist/core/storage/process-log.d.ts +12 -0
  154. package/dist/core/storage/process-log.d.ts.map +1 -1
  155. package/dist/core/storage/process-log.js +10 -2
  156. package/dist/core/supervisor/contract.d.ts +12 -0
  157. package/dist/core/supervisor/contract.d.ts.map +1 -1
  158. package/dist/core/supervisor/explanations.d.ts.map +1 -1
  159. package/dist/core/supervisor/explanations.js +2 -1
  160. package/dist/core/supervisor/findings.d.ts.map +1 -1
  161. package/dist/core/supervisor/findings.js +54 -9
  162. package/dist/core/supervisor/service.d.ts +9 -1
  163. package/dist/core/supervisor/service.d.ts.map +1 -1
  164. package/dist/core/supervisor/service.js +59 -29
  165. package/dist/core/supervisor/services.d.ts.map +1 -1
  166. package/dist/core/supervisor/services.js +22 -1
  167. package/dist/core/supervisor/status.d.ts +17 -2
  168. package/dist/core/supervisor/status.d.ts.map +1 -1
  169. package/dist/core/supervisor/status.js +70 -12
  170. package/dist/core/supervisor/timeline.d.ts.map +1 -1
  171. package/dist/core/supervisor/timeline.js +2 -1
  172. package/dist/core/work/state.js +7 -7
  173. package/dist/core/workflow/engine.d.ts.map +1 -1
  174. package/dist/core/workflow/engine.js +105 -32
  175. package/dist/core/workflow/failure-evidence.d.ts +16 -0
  176. package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
  177. package/dist/core/workflow/failure-evidence.js +114 -0
  178. package/dist/core/workflow/index.d.ts +1 -1
  179. package/dist/core/workflow/index.d.ts.map +1 -1
  180. package/dist/core/workflow/index.js +1 -1
  181. package/dist/core/workflow/live-session-v3.d.ts +6 -0
  182. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  183. package/dist/core/workflow/live-session-v3.js +126 -5
  184. package/dist/core/workflow/proof.d.ts +5 -5
  185. package/dist/core/workflow/proof.d.ts.map +1 -1
  186. package/dist/core/workflow/proof.js +115 -20
  187. package/dist/core/workflow/run-state.d.ts +3 -1
  188. package/dist/core/workflow/run-state.d.ts.map +1 -1
  189. package/dist/core/workflow/run-state.js +31 -27
  190. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  191. package/dist/core/workflow/spawn-claude.js +19 -0
  192. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  193. package/dist/core/workflow/spawn-codex.js +13 -0
  194. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  195. package/dist/core/workflow/spawn-cursor.js +19 -0
  196. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  197. package/dist/core/workflow/spawn-opencode.js +14 -0
  198. package/dist/core/workflow/types.d.ts +64 -17
  199. package/dist/core/workflow/types.d.ts.map +1 -1
  200. package/dist/core/workflow/workspaces/index.d.ts +0 -1
  201. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  202. package/dist/core/workflow/workspaces/index.js +0 -1
  203. package/dist/core/workflow/workspaces/leases.d.ts +3 -1
  204. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  205. package/dist/core/workflow/workspaces/leases.js +30 -16
  206. package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
  207. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  208. package/dist/core/workflow/workspaces/local-git.js +270 -6
  209. package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
  210. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
  211. package/dist/core/workflow/workspaces/ownership.js +194 -0
  212. package/dist/core/workflow/workspaces/reclaim.d.ts +35 -0
  213. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  214. package/dist/core/workflow/workspaces/reclaim.js +14 -0
  215. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  216. package/dist/core/workflow/workspaces/recovery.js +21 -13
  217. package/dist/core/workflow/workspaces/state.d.ts +1 -0
  218. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  219. package/dist/core/workflow/workspaces/state.js +21 -1
  220. package/dist/lib/browser/windows-chrome.d.ts +100 -12
  221. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  222. package/dist/lib/browser/windows-chrome.js +262 -73
  223. package/dist/lib/docs-links.d.ts.map +1 -1
  224. package/dist/lib/docs-links.js +45 -2
  225. package/dist/lib/guarded-remove.d.ts +7 -0
  226. package/dist/lib/guarded-remove.d.ts.map +1 -1
  227. package/dist/lib/guarded-remove.js +106 -12
  228. package/dist/lib/identities/assume.d.ts.map +1 -1
  229. package/dist/lib/identities/assume.js +4 -0
  230. package/dist/lib/tunnel/allowlist.d.ts +44 -0
  231. package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
  232. package/dist/lib/tunnel/allowlist.js +204 -0
  233. package/dist/lib/tunnel/current-address.d.ts +41 -0
  234. package/dist/lib/tunnel/current-address.d.ts.map +1 -0
  235. package/dist/lib/tunnel/current-address.js +123 -0
  236. package/dist/lib/tunnel/gate.js +13 -6
  237. package/dist/lib/tunnel/processes.d.ts +39 -0
  238. package/dist/lib/tunnel/processes.d.ts.map +1 -0
  239. package/dist/lib/tunnel/processes.js +121 -0
  240. package/dist/lib/tunnel/state.d.ts +28 -3
  241. package/dist/lib/tunnel/state.d.ts.map +1 -1
  242. package/dist/lib/tunnel/state.js +98 -27
  243. package/dist/lib/zip/index.d.ts +41 -0
  244. package/dist/lib/zip/index.d.ts.map +1 -0
  245. package/dist/lib/zip/index.js +398 -0
  246. package/package.json +8 -1
  247. package/src/commander.ts +32 -0
  248. package/src/commands/agents.ts +151 -74
  249. package/src/commands/browse.ts +50 -14
  250. package/src/commands/diagnostics.ts +10 -0
  251. package/src/commands/doctor.ts +26 -7
  252. package/src/commands/governor.ts +23 -10
  253. package/src/commands/inbox.ts +52 -2
  254. package/src/commands/rm.ts +43 -4
  255. package/src/commands/servers.ts +16 -4
  256. package/src/commands/supervisor.ts +7 -2
  257. package/src/commands/toc.ts +14 -6
  258. package/src/commands/tunnel.ts +394 -116
  259. package/src/commands/web-fetch.ts +30 -19
  260. package/src/commands/web.ts +9 -3
  261. package/src/commands/workflow.ts +30 -30
  262. package/src/core/agents/live-authority-v3.ts +17 -0
  263. package/src/core/agents/live-lifecycle-v3.ts +52 -14
  264. package/src/core/agents/mailbox.ts +26 -17
  265. package/src/core/agents/render/prompt-context.ts +35 -20
  266. package/src/core/agents/render/session-context.ts +92 -48
  267. package/src/core/agents/rules/stop-hook.ts +3 -3
  268. package/src/core/agents/state/heartbeat-writer.ts +29 -12
  269. package/src/core/agents/state/live-coordination-writer.ts +29 -0
  270. package/src/core/agents/state/names.ts +117 -6
  271. package/src/core/agents/state/pidmap.ts +13 -0
  272. package/src/core/agents/state/proc-start.ts +57 -0
  273. package/src/core/agents/state/stale-sweep.ts +247 -82
  274. package/src/core/agents/state/unchanged-file.ts +456 -0
  275. package/src/core/context/index.ts +446 -136
  276. package/src/core/context/observation.ts +29 -0
  277. package/src/core/diagnostics/bundle.ts +23 -18
  278. package/src/core/diagnostics/path-identity.ts +15 -0
  279. package/src/core/diagnostics/sanitize.ts +4 -3
  280. package/src/core/events/v3/authority-outbox.ts +56 -0
  281. package/src/core/events/v3/generation-custody.ts +59 -0
  282. package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
  283. package/src/core/events/v3/producers/recorder.ts +94 -68
  284. package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
  285. package/src/core/governor/planning.ts +20 -8
  286. package/src/core/governor/service-read.ts +128 -20
  287. package/src/core/governor/service.ts +119 -42
  288. package/src/core/hooks/cli.ts +139 -22
  289. package/src/core/hooks/fork-lineage.ts +82 -0
  290. package/src/core/hooks/resolve/transcript.ts +88 -0
  291. package/src/core/hooks/shell-rm.ts +103 -0
  292. package/src/core/hooks/shell-tokens.ts +96 -0
  293. package/src/core/hooks/shell-waiter.ts +4 -91
  294. package/src/core/inbox/contract.ts +2 -1
  295. package/src/core/inbox/index.ts +2 -0
  296. package/src/core/inbox/peer-messages.ts +94 -0
  297. package/src/core/inbox/service.ts +88 -22
  298. package/src/core/inbox/targets.ts +148 -0
  299. package/src/core/inbox/watcher.ts +8 -9
  300. package/src/core/journal/index.ts +19 -2
  301. package/src/core/resources/contract.ts +4 -0
  302. package/src/core/resources/service-status.ts +35 -12
  303. package/src/core/resources/service.ts +71 -24
  304. package/src/core/resources/status.ts +57 -8
  305. package/src/core/servers/index.ts +177 -91
  306. package/src/core/servers/tunnels.ts +3 -11
  307. package/src/core/storage/process-log.ts +17 -2
  308. package/src/core/supervisor/contract.ts +13 -0
  309. package/src/core/supervisor/explanations.ts +4 -1
  310. package/src/core/supervisor/findings.ts +68 -10
  311. package/src/core/supervisor/service.ts +88 -26
  312. package/src/core/supervisor/services.ts +26 -8
  313. package/src/core/supervisor/status.ts +89 -12
  314. package/src/core/supervisor/timeline.ts +2 -1
  315. package/src/core/work/state.ts +7 -7
  316. package/src/core/workflow/engine.ts +131 -49
  317. package/src/core/workflow/failure-evidence.ts +141 -0
  318. package/src/core/workflow/index.ts +0 -1
  319. package/src/core/workflow/live-session-v3.ts +146 -5
  320. package/src/core/workflow/proof.ts +134 -26
  321. package/src/core/workflow/run-state.ts +31 -29
  322. package/src/core/workflow/spawn-claude.ts +18 -0
  323. package/src/core/workflow/spawn-codex.ts +13 -0
  324. package/src/core/workflow/spawn-cursor.ts +18 -0
  325. package/src/core/workflow/spawn-opencode.ts +14 -0
  326. package/src/core/workflow/types.ts +51 -17
  327. package/src/core/workflow/workspaces/index.ts +0 -1
  328. package/src/core/workflow/workspaces/leases.ts +48 -17
  329. package/src/core/workflow/workspaces/local-git.ts +315 -5
  330. package/src/core/workflow/workspaces/ownership.ts +232 -0
  331. package/src/core/workflow/workspaces/reclaim.ts +58 -0
  332. package/src/core/workflow/workspaces/recovery.ts +25 -13
  333. package/src/core/workflow/workspaces/state.ts +20 -0
  334. package/src/lib/browser/windows-chrome.ts +346 -86
  335. package/src/lib/docs-links.ts +49 -2
  336. package/src/lib/guarded-remove.ts +132 -14
  337. package/src/lib/identities/assume.ts +4 -0
  338. package/src/lib/tunnel/allowlist.ts +208 -0
  339. package/src/lib/tunnel/current-address.ts +158 -0
  340. package/src/lib/tunnel/gate.ts +16 -8
  341. package/src/lib/tunnel/processes.ts +149 -0
  342. package/src/lib/tunnel/state.ts +123 -27
  343. package/src/lib/zip/index.ts +483 -0
@@ -50,13 +50,6 @@ import {
50
50
  LiveCoordinationAuthorityV3Error,
51
51
  reopenLiveCoordinationGenerationV3,
52
52
  } from "../core/agents/live-authority-v3.ts";
53
- import {
54
- isAddressableName,
55
- MailboxCapacityError,
56
- type QueueResult,
57
- queueMailboxMessage,
58
- suggestNames,
59
- } from "../core/agents/mailbox.ts";
60
53
  import { qaSignalStatusRow } from "../core/agents/qa-signal.ts";
61
54
  import { reconcileCoordinationV3 } from "../core/agents/reconcile-coordination-v3.ts";
62
55
  import {
@@ -84,6 +77,7 @@ import {
84
77
  readLiveCoordinationRow,
85
78
  readLiveCoordinationRows,
86
79
  } from "../core/agents/state/live-coordination-view.ts";
80
+ import { forkRelationLabels, resolveForkAncestry } from "../core/agents/state/names.ts";
87
81
  import {
88
82
  type AgentActivity,
89
83
  foldSessionState,
@@ -107,6 +101,7 @@ import {
107
101
  projectCoordinationViewV3,
108
102
  readCoordinationViewV3,
109
103
  } from "../core/events/v3/coordination-view.ts";
104
+ import { nativeInstanceIdV3 } from "../core/events/v3/live-route-observer.ts";
110
105
  import { liveInstanceIdV3 } from "../core/events/v3/live-routing.ts";
111
106
  import {
112
107
  countSummarizedSinceV3,
@@ -128,9 +123,12 @@ import {
128
123
  readRuntimeContextTelemetry,
129
124
  readRuntimeContextUsage,
130
125
  } from "../core/hooks/adapter/runtime-telemetry.ts";
126
+ import { HarneryInboxCapacityError } from "../core/inbox/contract.ts";
127
+ import { peerInbox, sendPeerMessage } from "../core/inbox/peer-messages.ts";
131
128
  import { type RemoteMachine, readRemoteMachines } from "../core/presence/index.ts";
132
129
  import { formatResourceSummary, readResourceStatus } from "../core/resources/status.ts";
133
130
  import { registerContextCommand } from "./context.ts";
131
+ import { registerInboxCommand } from "./inbox.ts";
134
132
 
135
133
  type LiveCoordinationRow = ReturnType<typeof readLiveCoordinationRows>[number];
136
134
 
@@ -311,6 +309,11 @@ export function registerAgentsCommand(
311
309
  );
312
310
 
313
311
  registerContextCommand(cmd, emitParam, programContext);
312
+ registerInboxCommand(cmd, emitParam, peerInbox(monorepoRoot() ?? process.cwd()), () => {
313
+ const owner = resolveOwner();
314
+ if (!owner) throw new Error("recipient instance is required outside an agent session");
315
+ return owner;
316
+ });
314
317
 
315
318
  cmd
316
319
  .command("whoami")
@@ -437,9 +440,15 @@ export function registerAgentsCommand(
437
440
  "--session-id <id>",
438
441
  "Target the heartbeat with this session_id directly, bypassing the ppid walk.",
439
442
  )
440
- .action((state: string, opts: { reason?: string; sessionId?: string }) => {
441
- runLifecycle(state, opts);
442
- });
443
+ .option(
444
+ "--force-ancestor",
445
+ "Allow a fork to change its fork ancestor's lifecycle (only on explicit operator instruction)",
446
+ )
447
+ .action(
448
+ (state: string, opts: { reason?: string; sessionId?: string; forceAncestor?: boolean }) => {
449
+ runLifecycle(state, opts);
450
+ },
451
+ );
443
452
 
444
453
  cmd
445
454
  .command("end")
@@ -453,9 +462,20 @@ export function registerAgentsCommand(
453
462
  "succeeded | failed | cancelled | timed_out | denied | interrupted | unknown",
454
463
  "succeeded",
455
464
  )
456
- .action((opts: { sessionId?: string; instanceId?: string; outcome: string }) => {
457
- runEndSession(opts);
458
- });
465
+ .option(
466
+ "--force-ancestor",
467
+ "Allow a fork to finalize its fork ancestor's session (only on explicit operator instruction)",
468
+ )
469
+ .action(
470
+ (opts: {
471
+ sessionId?: string;
472
+ instanceId?: string;
473
+ outcome: string;
474
+ forceAncestor?: boolean;
475
+ }) => {
476
+ runEndSession(opts);
477
+ },
478
+ );
459
479
 
460
480
  cmd
461
481
  .command("reconcile")
@@ -511,15 +531,22 @@ export function registerAgentsCommand(
511
531
  cmd
512
532
  .command("ping <name> <message...>")
513
533
  .description(
514
- "Send a message to another agent by name. Works whether or not that agent " +
515
- "is running: a live peer sees it on their next prompt, and a name with no " +
516
- "live session gets it queued until a session of that name next starts. " +
517
- "Only an unknown name is refused.",
534
+ "Queue a durable message for one known task instance by name. Stale or " +
535
+ "done tasks receive it when that same task resumes. Ambiguous names " +
536
+ "require an exact instance; receipts do not confirm attention.",
518
537
  )
538
+ .option("--instance-id <id>", "Select the exact known recipient instance")
539
+ .option("--require-live", "Refuse a recipient outside live presence")
519
540
  .option("--json", "JSON output")
520
- .action((name: string, message: string[], opts: { json?: boolean }) => {
521
- runPing(name, message.join(" "), opts);
522
- });
541
+ .action(
542
+ (
543
+ name: string,
544
+ message: string[],
545
+ opts: { json?: boolean; instanceId?: string; requireLive?: boolean },
546
+ ) => {
547
+ runPing(name, message.join(" "), opts);
548
+ },
549
+ );
523
550
 
524
551
  cmd
525
552
  .command("wait <name>")
@@ -666,7 +693,40 @@ const SESSION_OUTCOMES = new Set([
666
693
  "unknown",
667
694
  ]);
668
695
 
669
- function runEndSession(opts: { sessionId?: string; instanceId?: string; outcome: string }) {
696
+ /**
697
+ * Refuse when the calling session targets one of its recorded fork ancestors.
698
+ * A fork's transcript is full of its parent's name, task, and claims, so a
699
+ * fork that "cleans up its earlier self" is usually cutting off a live parent
700
+ * (2026-10-07: a fork offered to close its running parent). Returns the
701
+ * refusal message, or null when the call may proceed.
702
+ */
703
+ function forkAncestorRefusal(
704
+ root: string,
705
+ targetIds: Array<string | undefined>,
706
+ force: boolean | undefined,
707
+ action: string,
708
+ ): string | null {
709
+ if (force) return null;
710
+ const ids = new Set(targetIds.filter((id): id is string => !!id));
711
+ if (ids.size === 0) return null;
712
+ const self = resolveOwner() ?? sessionIdentityFromEnv();
713
+ if (!self || ids.has(self)) return null;
714
+ const ancestor = resolveForkAncestry(root, self).find((a) => ids.has(a.instance_id));
715
+ if (!ancestor) return null;
716
+ const label = ancestor.name ? `agent-${ancestor.name}` : ancestor.instance_id;
717
+ return (
718
+ `${label} is this session's fork ancestor: this conversation was branched from it, so its ` +
719
+ `name, task, and files in your context are its own, and it may still be running. Refusing to ` +
720
+ `${action} it. Rerun with --force-ancestor only when the operator explicitly asked for that.`
721
+ );
722
+ }
723
+
724
+ function runEndSession(opts: {
725
+ sessionId?: string;
726
+ instanceId?: string;
727
+ outcome: string;
728
+ forceAncestor?: boolean;
729
+ }) {
670
730
  const root = monorepoRoot();
671
731
  if (!root) return failCommand("not_in_repo", "not in an agent session");
672
732
  if (!SESSION_OUTCOMES.has(opts.outcome)) {
@@ -698,6 +758,15 @@ function runEndSession(opts: { sessionId?: string; instanceId?: string; outcome:
698
758
  return failCommand("session_identity_missing", "live V3 generation disappeared");
699
759
  }
700
760
  const state = record.state;
761
+ if (opts.instanceId || opts.sessionId) {
762
+ const refusal = forkAncestorRefusal(
763
+ root,
764
+ [target, state.session_id, nativeInstanceIdV3(state.instance_id)],
765
+ opts.forceAncestor,
766
+ "finalize",
767
+ );
768
+ if (refusal) return failCommand("session_is_fork_ancestor", refusal);
769
+ }
701
770
  if (state.delegations.length > 0) {
702
771
  return failCommand(
703
772
  "session_work_open",
@@ -1992,7 +2061,10 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
1992
2061
  });
1993
2062
  }
1994
2063
 
1995
- function runLifecycle(rawState: string, opts: { reason?: string; sessionId?: string }): void {
2064
+ function runLifecycle(
2065
+ rawState: string,
2066
+ opts: { reason?: string; sessionId?: string; forceAncestor?: boolean },
2067
+ ): void {
1996
2068
  const state = rawState.trim().toLowerCase();
1997
2069
  if (state !== "active" && state !== "blocked" && state !== "done") {
1998
2070
  emit.error({
@@ -2013,6 +2085,19 @@ function runLifecycle(rawState: string, opts: { reason?: string; sessionId?: str
2013
2085
  return;
2014
2086
  }
2015
2087
  if (!opts.sessionId) ensureAdapterSession(root);
2088
+ if (opts.sessionId) {
2089
+ const refusal = forkAncestorRefusal(
2090
+ root,
2091
+ [opts.sessionId],
2092
+ opts.forceAncestor,
2093
+ "change the lifecycle of",
2094
+ );
2095
+ if (refusal) {
2096
+ emit.error({ code: "session_is_fork_ancestor", message: refusal });
2097
+ process.exitCode = 1;
2098
+ return;
2099
+ }
2100
+ }
2016
2101
  const myOwner = opts.sessionId ?? resolveOwner() ?? sessionIdentityFromEnv();
2017
2102
  if (!myOwner) {
2018
2103
  emit.error(
@@ -2390,8 +2475,15 @@ function runStatus(opts: {
2390
2475
 
2391
2476
  const { livePeers, stale: peersStale } = collectStatusPeerHealth(root, myOwner);
2392
2477
 
2393
- // Sort: file-holders first (by file count desc), then idle peers by recency.
2478
+ // Recorded fork relations label the parent and forks so a fork cannot read
2479
+ // its live parent as its own earlier session; they sort first.
2480
+ const relations = forkRelationLabels(root, myOwner, new Set(livePeers.map((p) => p.instance_id)));
2481
+ // Sort: related sessions, then file-holders (by file count desc), then idle
2482
+ // peers by recency.
2394
2483
  livePeers.sort((a, b) => {
2484
+ const ar = relations.has(a.instance_id) ? 1 : 0;
2485
+ const br = relations.has(b.instance_id) ? 1 : 0;
2486
+ if (ar !== br) return br - ar;
2395
2487
  const af = a.files_touched?.length ?? 0;
2396
2488
  const bf = b.files_touched?.length ?? 0;
2397
2489
  if (af !== bf) return bf - af;
@@ -2406,7 +2498,7 @@ function runStatus(opts: {
2406
2498
  );
2407
2499
  // Cross-machine presence (ADR 0016): sessions on other machines, advisory.
2408
2500
  const remoteMachines = readRemoteMachines(root);
2409
- const peersStr = formatPeers(livePeers, 4, peersStale, remoteMachines);
2501
+ const peersStr = formatPeers(livePeers, 4, peersStale, remoteMachines, relations);
2410
2502
 
2411
2503
  const ctxUsage = readContextUsage(hb.native_session_id ?? hb.session_id, hb.platform);
2412
2504
  let ctxStr: string;
@@ -2600,13 +2692,15 @@ function formatPeers(
2600
2692
  cap: number,
2601
2693
  staleCount: number,
2602
2694
  remoteMachines: RemoteMachine[] = [],
2695
+ relations: Map<string, string> = new Map(),
2603
2696
  ): string {
2604
2697
  if (peers.length === 0 && staleCount === 0 && remoteMachines.length === 0) return "none";
2605
2698
  const labels = peers.map((p) => {
2606
2699
  const name = p.name || "unnamed";
2607
2700
  const plat = formatPlatformLabel(p.platform);
2608
2701
  const files = p.files_touched?.length ?? 0;
2609
- const base = `${name} (${plat})`;
2702
+ const relation = relations.get(p.instance_id);
2703
+ const base = relation ? `${name} (${plat}, ${relation})` : `${name} (${plat})`;
2610
2704
  return files > 0 ? `${base}, ${files} files` : base;
2611
2705
  });
2612
2706
  let main: string;
@@ -4784,7 +4878,11 @@ function formatDuration(ms: number): string {
4784
4878
  return h > 0 ? `${d}d${h}h` : `${d}d`;
4785
4879
  }
4786
4880
 
4787
- function runPing(name: string, message: string, opts: { json?: boolean }): void {
4881
+ function runPing(
4882
+ name: string,
4883
+ message: string,
4884
+ opts: { json?: boolean; instanceId?: string; requireLive?: boolean },
4885
+ ): void {
4788
4886
  if (!message || message.trim().length === 0) {
4789
4887
  emit.error({ code: "empty_message", message: "message is required (and non-empty)" });
4790
4888
  process.exit(1);
@@ -4805,52 +4903,33 @@ function runPing(name: string, message: string, opts: { json?: boolean }): void
4805
4903
  );
4806
4904
  process.exit(1);
4807
4905
  }
4808
- const coordRoot = monorepoRoot();
4809
- const peerOwner = resolveOwnerByName(name);
4810
-
4811
- // A name with no live session is the common case, not an error: the agent it
4812
- // refers to may have ended minutes ago and may come back under the same name.
4813
- // Refuse only a name that no agent has ever held, which is a typo — queueing
4814
- // that would swallow the message silently.
4815
- if (!peerOwner && coordRoot && !isAddressableName(coordRoot, name)) {
4816
- const suggestions = suggestNames(coordRoot, name);
4817
- emit.error({
4818
- code: "unknown_agent",
4819
- message:
4820
- `no agent has ever been named "${name}". ` +
4821
- (suggestions.length > 0 ? `Did you mean ${suggestions.join(", ")}? ` : "") +
4822
- `Run \`${resolveBinName()} agents list\` for live sessions.`,
4823
- });
4824
- process.exit(1);
4825
- }
4826
-
4906
+ const coordRoot = monorepoRoot() ?? process.cwd();
4827
4907
  const myHb = readCurrentCoordinationRow(myOwner);
4828
4908
  const fromName = myHb?.name ?? "anonymous";
4829
- const body = `from agent-${fromName}: ${message.trim()}`;
4830
-
4831
- // A live recipient gets the journal entry immediately, so a peer reading
4832
- // their own journal mid-session sees it without waiting for a prompt. A
4833
- // dormant recipient has no journal to write to yet; the drain writes it into
4834
- // whichever session picks the message up.
4835
- const doc = peerOwner ? appendEntry(peerOwner, "handoff", body) : null;
4836
-
4837
- let queued: QueueResult;
4909
+ let sent: ReturnType<typeof sendPeerMessage>;
4838
4910
  try {
4839
- queued = queueMailboxMessage({
4840
- coordRoot: coordRoot ?? process.cwd(),
4841
- toName: name,
4842
- fromName,
4843
- fromInstanceId: myOwner,
4911
+ sent = sendPeerMessage({
4912
+ coordRoot,
4913
+ target: { name, instanceId: opts.instanceId, requireLive: opts.requireLive },
4914
+ senderName: fromName,
4915
+ senderInstanceId: myOwner,
4844
4916
  body: message.trim(),
4845
- journaled: Boolean(peerOwner),
4846
4917
  });
4847
4918
  } catch (error) {
4848
- if (error instanceof MailboxCapacityError) {
4849
- emit.error({ code: `mailbox_${error.reason_code}`, message: error.message });
4919
+ if (error instanceof HarneryInboxCapacityError) {
4920
+ emit.error({ code: `inbox_${error.reason_code}`, message: error.message });
4850
4921
  process.exit(1);
4851
4922
  }
4852
4923
  throw error;
4853
4924
  }
4925
+ if (!sent.ok) {
4926
+ emit.error({
4927
+ code: sent.code,
4928
+ message: sent.message,
4929
+ ...(sent.candidates ? { candidates: sent.candidates } : {}),
4930
+ });
4931
+ process.exit(1);
4932
+ }
4854
4933
 
4855
4934
  // Canonical delivery record: sender is the envelope owner, recipient rides
4856
4935
  // in data, so read-only observers can render the communication without
@@ -4862,21 +4941,20 @@ function runPing(name: string, message: string, opts: { json?: boolean }): void
4862
4941
  observation: {
4863
4942
  event_type: "coord.message_observed",
4864
4943
  direction: "sent",
4865
- subject: peerOwner ?? `name:${queued.message.to_name}`,
4944
+ subject: sent.target.instance_id,
4945
+ message_id: sent.message_id,
4866
4946
  body: message.trim(),
4867
4947
  },
4868
4948
  });
4869
4949
 
4870
4950
  const data = {
4871
- peer: queued.message.to_name,
4872
- peer_instance_id: peerOwner,
4873
- delivery: peerOwner ? ("live" as const) : ("queued" as const),
4874
- message_id: queued.message.message_id,
4875
- pending: queued.pending,
4951
+ peer: sent.target.name,
4952
+ peer_instance_id: sent.target.instance_id,
4953
+ state: sent.state,
4954
+ message_id: sent.message_id,
4955
+ synced: sent.synced,
4956
+ body_bytes: sent.body_bytes,
4876
4957
  from: fromName,
4877
- body,
4878
- journal_path: doc?.path ?? null,
4879
- journal_bytes: doc?.bytes ?? null,
4880
4958
  };
4881
4959
 
4882
4960
  if (opts.json) {
@@ -4886,9 +4964,8 @@ function runPing(name: string, message: string, opts: { json?: boolean }): void
4886
4964
  }
4887
4965
  emit.data(data);
4888
4966
  emit.text(
4889
- peerOwner
4890
- ? `delivered to agent-${data.peer} (live; they see it on their next prompt): "${truncate(message.trim(), 80)}"\n`
4891
- : `queued for agent-${data.peer} (no live session; delivered when a session of that name next starts): "${truncate(message.trim(), 80)}"\n`,
4967
+ `${data.state} for agent-${data.peer} (${data.peer_instance_id}); ` +
4968
+ `stored as ${data.message_id}, pending until surfaced to that task.\n`,
4892
4969
  );
4893
4970
  }
4894
4971
 
@@ -134,6 +134,7 @@ import {
134
134
  import {
135
135
  harvestDomain,
136
136
  harvestWindowsChromeCookies,
137
+ keepWindowsChromeSession,
137
138
  stopWindowsChromeProfile,
138
139
  windowsChromePath,
139
140
  windowsLocalAppData,
@@ -3465,15 +3466,18 @@ function useWindowsPlainLogin(): boolean {
3465
3466
 
3466
3467
  /**
3467
3468
  * The WSL form of `--login`: the person signs in through the Windows
3468
- * Chrome on a dedicated Windows profile with nothing attached. When the window
3469
- * closes, the profile's cookies for the sign-in site are read and merged into
3470
- * the shared cookie store, which later `browse` runs attach.
3469
+ * Chrome on a dedicated Windows profile with nothing attached. Before the
3470
+ * window opens, the profile is set to continue where it left off, so Chrome
3471
+ * keeps browser-session cookies when the window closes. When the window
3472
+ * closes, the profile's cookies for the sign-in site are read and merged
3473
+ * into the shared cookie store, which later `browse` runs attach, and the
3474
+ * last page open on that site is reported so an agent can resume from it.
3471
3475
  */
3472
3476
  async function runWindowsPlainLogin(
3473
3477
  url: string,
3474
3478
  storePath: string,
3475
3479
  loginCloseFile: string | null,
3476
- emit: { log: (message: string, level: "info" | "warn" | "error" | "debug") => void },
3480
+ emit: Pick<EmitContext, "log" | "data">,
3477
3481
  ): Promise<void> {
3478
3482
  const chromeLinux = windowsChromePath();
3479
3483
  const localAppData = windowsLocalAppData();
@@ -3484,6 +3488,13 @@ async function runWindowsPlainLogin(
3484
3488
  }
3485
3489
  const chromeWindows = wslPathToWindows(chromeLinux);
3486
3490
  const profileWindows = windowsPlainProfile(localAppData);
3491
+ const keep = keepWindowsChromeSession({ chromeWindows, profileWindows });
3492
+ if (!keep.kept) {
3493
+ emit.log(
3494
+ "[--login] Could not set the sign-in profile to keep browser-session cookies. A sign-in that relies on them survives only if you end it from the terminal instead of closing the window.",
3495
+ "warn",
3496
+ );
3497
+ }
3487
3498
  const child = spawn(
3488
3499
  chromeLinux,
3489
3500
  [`--user-data-dir=${profileWindows}`, "--no-first-run", "--no-default-browser-check", url],
@@ -3507,24 +3518,49 @@ async function runWindowsPlainLogin(
3507
3518
  );
3508
3519
  await Promise.race(waits);
3509
3520
  lifecycleAbort.abort();
3510
- stopWindowsChromeProfile(profileWindows);
3521
+ const stop = stopWindowsChromeProfile(profileWindows);
3522
+ if (stop.forced > 0) {
3523
+ emit.log(
3524
+ "[--login] Chrome did not close when asked and was stopped. Cookies set in its last 30 seconds may be missing; run the sign-in again if later pages are signed out.",
3525
+ "warn",
3526
+ );
3527
+ }
3511
3528
  await Promise.race([exited, new Promise((r) => setTimeout(r, 5000))]);
3512
3529
  const domain = harvestDomain(url);
3513
- const cookies = harvestWindowsChromeCookies({ chromeWindows, profileWindows, domain });
3530
+ const { cookies, pages } = harvestWindowsChromeCookies({ chromeWindows, profileWindows, domain });
3531
+ const lastPage = pages[0] ?? null;
3532
+ const names = [...new Set(cookies.map((cookie) => cookie.name))].sort();
3533
+ const sessionCookies = cookies.filter((cookie) => cookie.expires <= 0).length;
3514
3534
  if (cookies.length === 0) {
3515
3535
  emit.log(
3516
3536
  `[--login] No ${domain} cookies were found in the Windows profile; the sign-in may not have completed.`,
3517
3537
  "warn",
3518
3538
  );
3519
- return;
3539
+ } else {
3540
+ const jar = new CookieJar({ path: storePath, source: "harn-browse" });
3541
+ jar.save(mergeCookies(jar.load(), cookies));
3542
+ emit.log(
3543
+ `[--login] Copied ${cookies.length} ${domain} cookies (${sessionCookies} browser-session) into ${storePath}: ${names.join(", ")}.`,
3544
+ "info",
3545
+ );
3520
3546
  }
3521
- const jar = new CookieJar({ path: storePath, source: "harn-browse" });
3522
- jar.save(mergeCookies(jar.load(), cookies));
3523
- const names = [...new Set(cookies.map((cookie) => cookie.name))].sort().join(", ");
3524
- emit.log(
3525
- `[--login] Copied ${cookies.length} ${domain} cookies into ${storePath}: ${names}.`,
3526
- "info",
3527
- );
3547
+ if (lastPage) {
3548
+ emit.log(
3549
+ `[--login] Last page: ${lastPage} (the last page you had open on ${domain}; open it to continue where the sign-in left off, since its address may carry a sign-in token that cookies alone do not replace).`,
3550
+ "info",
3551
+ );
3552
+ }
3553
+ emit.data({
3554
+ login: {
3555
+ domain,
3556
+ store: storePath,
3557
+ cookies: cookies.length,
3558
+ sessionCookies,
3559
+ names,
3560
+ lastPage,
3561
+ pages,
3562
+ },
3563
+ });
3528
3564
  }
3529
3565
 
3530
3566
  async function waitForLoginCloseFile(path: string, signal?: AbortSignal): Promise<void> {
@@ -50,6 +50,7 @@ export function registerDiagnosticsCommand(
50
50
  findings,
51
51
  bundles,
52
52
  capability,
53
+ supervisor: readSupervisorStatus(repoRoot),
53
54
  total_findings: findings.length,
54
55
  total_bundles: bundles.length,
55
56
  });
@@ -68,6 +69,7 @@ export function registerDiagnosticsCommand(
68
69
  schema_version: DIAGNOSTIC_COMMAND_SCHEMA_VERSION,
69
70
  kind: "diagnostic_finding",
70
71
  finding,
72
+ supervisor: readSupervisorStatus(repoRoot),
71
73
  timeline:
72
74
  readSupervisorTimeline(repoRoot, finding.id) ?? buildSupervisorTimeline(finding),
73
75
  explanation:
@@ -118,6 +120,7 @@ export function registerDiagnosticsCommand(
118
120
  schema_version: DIAGNOSTIC_COMMAND_SCHEMA_VERSION,
119
121
  kind: "diagnostic_advice",
120
122
  source: { mode: "live" },
123
+ supervisor: readSupervisorStatus(repoRoot),
121
124
  advice: buildDiagnosticAdvice({
122
125
  assessment: published.assessment,
123
126
  priorHysteresis: published.prior_hysteresis,
@@ -204,6 +207,13 @@ function liveFindingsCapability(repoRoot: string, reportPresent: boolean): Super
204
207
  }
205
208
  const status = readSupervisorStatus(repoRoot);
206
209
  if (status.running) return { source_kind: "supervisor.findings", state: "supported" };
210
+ if (status.liveness === "unknown")
211
+ return {
212
+ source_kind: "supervisor.findings",
213
+ state: "error",
214
+ reason_code: "supervisor_generation_unknown",
215
+ detail: status.build_status,
216
+ };
207
217
  return {
208
218
  source_kind: "supervisor.findings",
209
219
  state: "expired",
@@ -421,15 +421,34 @@ function checkWorkflowAdapter(adapter: AdapterName): { check: Check; installed:
421
421
  }
422
422
 
423
423
  function checkNode(): Check {
424
- const v = process.versions.node;
424
+ return nodeCheck(whichVersion("node"), process.versions);
425
+ }
426
+
427
+ /**
428
+ * Report the `node` on PATH, which is what Node-based tools run. Under Bun,
429
+ * `process.versions.node` is the Node version Bun emulates, not an installed
430
+ * Node, so it is used only when doctor itself runs on Node and PATH has none.
431
+ */
432
+ export function nodeCheck(
433
+ onPath: { ok: boolean; out: string },
434
+ runtime: { node: string; bun?: string },
435
+ ): Check {
436
+ const hint = "https://nodejs.org/en/download";
437
+ let v = onPath.ok ? onPath.out.replace(/^v/, "") : "";
438
+ if (!v) {
439
+ if (runtime.bun) {
440
+ return {
441
+ name: "node",
442
+ severity: "warn",
443
+ detail: "not on PATH (Bun runs harnery; Node-based tools need it)",
444
+ hint,
445
+ };
446
+ }
447
+ v = runtime.node;
448
+ }
425
449
  const major = Number.parseInt(v.split(".")[0], 10);
426
450
  if (Number.isNaN(major) || major < 20) {
427
- return {
428
- name: "node",
429
- severity: "fail",
430
- detail: `${v} (need ≥ 20)`,
431
- hint: "https://nodejs.org/en/download",
432
- };
451
+ return { name: "node", severity: "fail", detail: `${v} (need ≥ 20)`, hint };
433
452
  }
434
453
  return { name: "node", severity: "ok", detail: v };
435
454
  }
@@ -385,12 +385,13 @@ function registerServiceCommand(
385
385
  .action(async (opts: { json?: boolean }) => {
386
386
  await withGovernorRootAsync(emit, async (coordRoot) => {
387
387
  let status = requestGovernorServiceStop(coordRoot);
388
+ const signal = { signal_sent: status.signal_sent, signal_verdict: status.signal_verdict };
388
389
  const deadline = Date.now() + 5_000;
389
390
  while (status.running && Date.now() < deadline) {
390
391
  await delay(50);
391
392
  status = readGovernorServiceStatus(coordRoot);
392
393
  }
393
- emitServiceStatus(status, opts.json, emit);
394
+ emitServiceStatus({ ...status, ...signal }, opts.json, emit);
394
395
  });
395
396
  });
396
397
 
@@ -534,19 +535,31 @@ function emitServiceStatus(
534
535
  emit.data(status);
535
536
  return;
536
537
  }
537
- if (!status.config && !status.record) {
538
- emit.text("governor service: unconfigured\n");
539
- return;
540
- }
541
- const state = status.running
542
- ? (status.record?.state ?? "running")
543
- : status.stale
544
- ? "stale"
545
- : "stopped";
538
+ const state =
539
+ status.liveness === "unknown"
540
+ ? "unknown"
541
+ : !status.config && !status.record
542
+ ? "unconfigured"
543
+ : status.running
544
+ ? (status.record?.state ?? "running")
545
+ : status.stale
546
+ ? "stale"
547
+ : "stopped";
546
548
  const lines = [
547
549
  `governor service: ${state}`,
550
+ `generation: ${status.generation}`,
551
+ `liveness: ${status.liveness}`,
552
+ `running build: ${status.running_build ? `${status.running_build.version} (${status.running_build.revision})` : "unknown"}`,
553
+ `installed build: ${status.installed_build.version} (${status.installed_build.revision})`,
554
+ `build: ${status.build_status}`,
548
555
  `goals: ${status.config?.goal_ids.join(", ") || "none"}`,
549
556
  ];
557
+ if (status.signal_sent !== undefined) {
558
+ lines.push(`signal sent: ${status.signal_sent ? "yes" : "no"} (${status.signal_verdict})`);
559
+ if (!status.signal_sent && status.record?.state !== "stopped") {
560
+ lines.push("The durable stop request remains for the service to read.");
561
+ }
562
+ }
550
563
  if (status.record) {
551
564
  lines.push(`pid: ${status.record.pid} on ${status.record.host}`);
552
565
  lines.push(`heartbeat: ${status.record.heartbeat_at}`);