harnery 0.43.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 (278) hide show
  1. package/README.md +3 -3
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +41 -48
  4. package/dist/commands/browse.d.ts.map +1 -1
  5. package/dist/commands/browse.js +38 -11
  6. package/dist/commands/diagnostics.d.ts.map +1 -1
  7. package/dist/commands/diagnostics.js +10 -0
  8. package/dist/commands/doctor.d.ts +12 -0
  9. package/dist/commands/doctor.d.ts.map +1 -1
  10. package/dist/commands/doctor.js +22 -7
  11. package/dist/commands/governor.js +22 -10
  12. package/dist/commands/inbox.d.ts +1 -1
  13. package/dist/commands/inbox.d.ts.map +1 -1
  14. package/dist/commands/inbox.js +55 -2
  15. package/dist/commands/rm.d.ts.map +1 -1
  16. package/dist/commands/rm.js +28 -4
  17. package/dist/commands/servers.d.ts.map +1 -1
  18. package/dist/commands/servers.js +16 -1
  19. package/dist/commands/supervisor.d.ts.map +1 -1
  20. package/dist/commands/supervisor.js +7 -2
  21. package/dist/commands/toc.d.ts +14 -0
  22. package/dist/commands/toc.d.ts.map +1 -1
  23. package/dist/commands/toc.js +7 -4
  24. package/dist/commands/workflow.js +24 -30
  25. package/dist/core/agents/live-authority-v3.d.ts +1 -0
  26. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  27. package/dist/core/agents/live-authority-v3.js +11 -0
  28. package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
  29. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  30. package/dist/core/agents/live-lifecycle-v3.js +28 -7
  31. package/dist/core/agents/mailbox.d.ts +9 -8
  32. package/dist/core/agents/mailbox.d.ts.map +1 -1
  33. package/dist/core/agents/mailbox.js +24 -17
  34. package/dist/core/agents/render/prompt-context.d.ts +2 -0
  35. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  36. package/dist/core/agents/render/prompt-context.js +8 -12
  37. package/dist/core/agents/render/session-context.d.ts +2 -8
  38. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  39. package/dist/core/agents/render/session-context.js +7 -30
  40. package/dist/core/agents/rules/stop-hook.js +3 -3
  41. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  42. package/dist/core/agents/state/heartbeat-writer.js +25 -14
  43. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  44. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  45. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  46. package/dist/core/agents/state/pidmap.js +5 -0
  47. package/dist/core/agents/state/proc-start.d.ts +19 -0
  48. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  49. package/dist/core/agents/state/proc-start.js +41 -0
  50. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  51. package/dist/core/agents/state/stale-sweep.js +180 -70
  52. package/dist/core/agents/state/unchanged-file.d.ts +44 -0
  53. package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
  54. package/dist/core/agents/state/unchanged-file.js +390 -0
  55. package/dist/core/context/index.d.ts +22 -10
  56. package/dist/core/context/index.d.ts.map +1 -1
  57. package/dist/core/context/index.js +381 -119
  58. package/dist/core/context/observation.d.ts +26 -0
  59. package/dist/core/context/observation.d.ts.map +1 -0
  60. package/dist/core/context/observation.js +11 -0
  61. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  62. package/dist/core/diagnostics/bundle.js +25 -14
  63. package/dist/core/diagnostics/path-identity.d.ts +3 -0
  64. package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
  65. package/dist/core/diagnostics/path-identity.js +12 -0
  66. package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
  67. package/dist/core/diagnostics/sanitize.js +4 -3
  68. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  69. package/dist/core/events/v3/authority-outbox.js +29 -0
  70. package/dist/core/events/v3/generation-custody.d.ts +5 -0
  71. package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
  72. package/dist/core/events/v3/generation-custody.js +50 -0
  73. package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
  74. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  75. package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
  76. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  77. package/dist/core/events/v3/producers/recorder.js +77 -64
  78. package/dist/core/governor/planning.d.ts.map +1 -1
  79. package/dist/core/governor/planning.js +14 -1
  80. package/dist/core/governor/service-read.d.ts +10 -2
  81. package/dist/core/governor/service-read.d.ts.map +1 -1
  82. package/dist/core/governor/service-read.js +105 -22
  83. package/dist/core/governor/service.d.ts +20 -2
  84. package/dist/core/governor/service.d.ts.map +1 -1
  85. package/dist/core/governor/service.js +72 -47
  86. package/dist/core/hooks/cli.js +33 -5
  87. package/dist/core/inbox/contract.d.ts +2 -1
  88. package/dist/core/inbox/contract.d.ts.map +1 -1
  89. package/dist/core/inbox/index.d.ts +2 -0
  90. package/dist/core/inbox/index.d.ts.map +1 -1
  91. package/dist/core/inbox/index.js +2 -0
  92. package/dist/core/inbox/peer-messages.d.ts +37 -0
  93. package/dist/core/inbox/peer-messages.d.ts.map +1 -0
  94. package/dist/core/inbox/peer-messages.js +72 -0
  95. package/dist/core/inbox/service.d.ts +8 -1
  96. package/dist/core/inbox/service.d.ts.map +1 -1
  97. package/dist/core/inbox/service.js +67 -21
  98. package/dist/core/inbox/targets.d.ts +22 -0
  99. package/dist/core/inbox/targets.d.ts.map +1 -0
  100. package/dist/core/inbox/targets.js +108 -0
  101. package/dist/core/inbox/watcher.js +7 -3
  102. package/dist/core/journal/index.d.ts.map +1 -1
  103. package/dist/core/journal/index.js +17 -2
  104. package/dist/core/resources/contract.d.ts +3 -0
  105. package/dist/core/resources/contract.d.ts.map +1 -1
  106. package/dist/core/resources/service-status.d.ts +14 -1
  107. package/dist/core/resources/service-status.d.ts.map +1 -1
  108. package/dist/core/resources/service-status.js +23 -13
  109. package/dist/core/resources/service.d.ts +9 -1
  110. package/dist/core/resources/service.d.ts.map +1 -1
  111. package/dist/core/resources/service.js +47 -27
  112. package/dist/core/resources/status.d.ts +2 -5
  113. package/dist/core/resources/status.d.ts.map +1 -1
  114. package/dist/core/resources/status.js +36 -7
  115. package/dist/core/servers/index.d.ts +24 -13
  116. package/dist/core/servers/index.d.ts.map +1 -1
  117. package/dist/core/servers/index.js +132 -92
  118. package/dist/core/servers/tunnels.d.ts +3 -5
  119. package/dist/core/servers/tunnels.d.ts.map +1 -1
  120. package/dist/core/servers/tunnels.js +3 -9
  121. package/dist/core/supervisor/contract.d.ts +12 -0
  122. package/dist/core/supervisor/contract.d.ts.map +1 -1
  123. package/dist/core/supervisor/explanations.d.ts.map +1 -1
  124. package/dist/core/supervisor/explanations.js +2 -1
  125. package/dist/core/supervisor/findings.d.ts.map +1 -1
  126. package/dist/core/supervisor/findings.js +54 -9
  127. package/dist/core/supervisor/service.d.ts +9 -1
  128. package/dist/core/supervisor/service.d.ts.map +1 -1
  129. package/dist/core/supervisor/service.js +59 -29
  130. package/dist/core/supervisor/services.d.ts.map +1 -1
  131. package/dist/core/supervisor/services.js +22 -1
  132. package/dist/core/supervisor/status.d.ts +17 -2
  133. package/dist/core/supervisor/status.d.ts.map +1 -1
  134. package/dist/core/supervisor/status.js +70 -12
  135. package/dist/core/supervisor/timeline.d.ts.map +1 -1
  136. package/dist/core/supervisor/timeline.js +2 -1
  137. package/dist/core/work/state.js +7 -7
  138. package/dist/core/workflow/engine.d.ts.map +1 -1
  139. package/dist/core/workflow/engine.js +105 -32
  140. package/dist/core/workflow/failure-evidence.d.ts +16 -0
  141. package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
  142. package/dist/core/workflow/failure-evidence.js +114 -0
  143. package/dist/core/workflow/index.d.ts +1 -1
  144. package/dist/core/workflow/index.d.ts.map +1 -1
  145. package/dist/core/workflow/index.js +1 -1
  146. package/dist/core/workflow/live-session-v3.d.ts +6 -0
  147. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  148. package/dist/core/workflow/live-session-v3.js +126 -5
  149. package/dist/core/workflow/proof.d.ts +5 -5
  150. package/dist/core/workflow/proof.d.ts.map +1 -1
  151. package/dist/core/workflow/proof.js +115 -20
  152. package/dist/core/workflow/run-state.d.ts +3 -1
  153. package/dist/core/workflow/run-state.d.ts.map +1 -1
  154. package/dist/core/workflow/run-state.js +31 -27
  155. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  156. package/dist/core/workflow/spawn-claude.js +19 -0
  157. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  158. package/dist/core/workflow/spawn-codex.js +13 -0
  159. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  160. package/dist/core/workflow/spawn-cursor.js +19 -0
  161. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  162. package/dist/core/workflow/spawn-opencode.js +14 -0
  163. package/dist/core/workflow/types.d.ts +64 -17
  164. package/dist/core/workflow/types.d.ts.map +1 -1
  165. package/dist/core/workflow/workspaces/index.d.ts +0 -1
  166. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  167. package/dist/core/workflow/workspaces/index.js +0 -1
  168. package/dist/core/workflow/workspaces/leases.d.ts +3 -1
  169. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  170. package/dist/core/workflow/workspaces/leases.js +30 -16
  171. package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
  172. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  173. package/dist/core/workflow/workspaces/local-git.js +270 -6
  174. package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
  175. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
  176. package/dist/core/workflow/workspaces/ownership.js +194 -0
  177. package/dist/core/workflow/workspaces/reclaim.d.ts +35 -0
  178. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  179. package/dist/core/workflow/workspaces/reclaim.js +14 -0
  180. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  181. package/dist/core/workflow/workspaces/recovery.js +21 -13
  182. package/dist/core/workflow/workspaces/state.d.ts +1 -0
  183. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  184. package/dist/core/workflow/workspaces/state.js +21 -1
  185. package/dist/lib/browser/windows-chrome.d.ts +100 -12
  186. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  187. package/dist/lib/browser/windows-chrome.js +262 -73
  188. package/dist/lib/docs-links.d.ts.map +1 -1
  189. package/dist/lib/docs-links.js +45 -2
  190. package/dist/lib/guarded-remove.d.ts +7 -0
  191. package/dist/lib/guarded-remove.d.ts.map +1 -1
  192. package/dist/lib/guarded-remove.js +106 -12
  193. package/dist/lib/identities/assume.d.ts.map +1 -1
  194. package/dist/lib/identities/assume.js +4 -0
  195. package/dist/lib/zip/index.d.ts +41 -0
  196. package/dist/lib/zip/index.d.ts.map +1 -0
  197. package/dist/lib/zip/index.js +398 -0
  198. package/package.json +7 -1
  199. package/src/commands/agents.ts +54 -63
  200. package/src/commands/browse.ts +50 -14
  201. package/src/commands/diagnostics.ts +10 -0
  202. package/src/commands/doctor.ts +26 -7
  203. package/src/commands/governor.ts +23 -10
  204. package/src/commands/inbox.ts +52 -2
  205. package/src/commands/rm.ts +43 -4
  206. package/src/commands/servers.ts +16 -4
  207. package/src/commands/supervisor.ts +7 -2
  208. package/src/commands/toc.ts +14 -6
  209. package/src/commands/workflow.ts +30 -30
  210. package/src/core/agents/live-authority-v3.ts +17 -0
  211. package/src/core/agents/live-lifecycle-v3.ts +52 -14
  212. package/src/core/agents/mailbox.ts +26 -17
  213. package/src/core/agents/render/prompt-context.ts +10 -12
  214. package/src/core/agents/render/session-context.ts +9 -32
  215. package/src/core/agents/rules/stop-hook.ts +3 -3
  216. package/src/core/agents/state/heartbeat-writer.ts +29 -12
  217. package/src/core/agents/state/live-coordination-writer.ts +29 -0
  218. package/src/core/agents/state/pidmap.ts +13 -0
  219. package/src/core/agents/state/proc-start.ts +57 -0
  220. package/src/core/agents/state/stale-sweep.ts +247 -82
  221. package/src/core/agents/state/unchanged-file.ts +456 -0
  222. package/src/core/context/index.ts +446 -136
  223. package/src/core/context/observation.ts +29 -0
  224. package/src/core/diagnostics/bundle.ts +23 -18
  225. package/src/core/diagnostics/path-identity.ts +15 -0
  226. package/src/core/diagnostics/sanitize.ts +4 -3
  227. package/src/core/events/v3/authority-outbox.ts +56 -0
  228. package/src/core/events/v3/generation-custody.ts +59 -0
  229. package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
  230. package/src/core/events/v3/producers/recorder.ts +94 -68
  231. package/src/core/governor/planning.ts +20 -8
  232. package/src/core/governor/service-read.ts +128 -20
  233. package/src/core/governor/service.ts +119 -42
  234. package/src/core/hooks/cli.ts +33 -5
  235. package/src/core/inbox/contract.ts +2 -1
  236. package/src/core/inbox/index.ts +2 -0
  237. package/src/core/inbox/peer-messages.ts +94 -0
  238. package/src/core/inbox/service.ts +88 -22
  239. package/src/core/inbox/targets.ts +148 -0
  240. package/src/core/inbox/watcher.ts +8 -9
  241. package/src/core/journal/index.ts +19 -2
  242. package/src/core/resources/contract.ts +4 -0
  243. package/src/core/resources/service-status.ts +35 -12
  244. package/src/core/resources/service.ts +71 -24
  245. package/src/core/resources/status.ts +57 -8
  246. package/src/core/servers/index.ts +177 -91
  247. package/src/core/servers/tunnels.ts +3 -11
  248. package/src/core/supervisor/contract.ts +13 -0
  249. package/src/core/supervisor/explanations.ts +4 -1
  250. package/src/core/supervisor/findings.ts +68 -10
  251. package/src/core/supervisor/service.ts +88 -26
  252. package/src/core/supervisor/services.ts +26 -8
  253. package/src/core/supervisor/status.ts +89 -12
  254. package/src/core/supervisor/timeline.ts +2 -1
  255. package/src/core/work/state.ts +7 -7
  256. package/src/core/workflow/engine.ts +131 -49
  257. package/src/core/workflow/failure-evidence.ts +141 -0
  258. package/src/core/workflow/index.ts +0 -1
  259. package/src/core/workflow/live-session-v3.ts +146 -5
  260. package/src/core/workflow/proof.ts +134 -26
  261. package/src/core/workflow/run-state.ts +31 -29
  262. package/src/core/workflow/spawn-claude.ts +18 -0
  263. package/src/core/workflow/spawn-codex.ts +13 -0
  264. package/src/core/workflow/spawn-cursor.ts +18 -0
  265. package/src/core/workflow/spawn-opencode.ts +14 -0
  266. package/src/core/workflow/types.ts +51 -17
  267. package/src/core/workflow/workspaces/index.ts +0 -1
  268. package/src/core/workflow/workspaces/leases.ts +48 -17
  269. package/src/core/workflow/workspaces/local-git.ts +315 -5
  270. package/src/core/workflow/workspaces/ownership.ts +232 -0
  271. package/src/core/workflow/workspaces/reclaim.ts +58 -0
  272. package/src/core/workflow/workspaces/recovery.ts +25 -13
  273. package/src/core/workflow/workspaces/state.ts +20 -0
  274. package/src/lib/browser/windows-chrome.ts +346 -86
  275. package/src/lib/docs-links.ts +49 -2
  276. package/src/lib/guarded-remove.ts +132 -14
  277. package/src/lib/identities/assume.ts +4 -0
  278. package/src/lib/zip/index.ts +483 -0
@@ -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}`);
@@ -1,18 +1,21 @@
1
1
  import type { Command } from "commander";
2
2
  import type { EmitContext } from "../commander.ts";
3
3
  import type { HarneryInboxService } from "../core/inbox/service.ts";
4
+ import { watchInbox } from "../core/inbox/watcher.ts";
4
5
 
5
6
  export function registerInboxCommand(
6
7
  program: Command,
7
8
  emit: EmitContext,
8
9
  service?: HarneryInboxService,
10
+ defaultRecipient?: () => string,
9
11
  ): void {
10
12
  const inbox = program.command("inbox").description("Inspect private coordination inbox state");
11
13
  inbox
12
- .command("list <recipient>")
14
+ .command("list [recipient]")
13
15
  .option("--json", "Emit stable JSON")
14
- .action((recipient: string, options: { json?: boolean }) => {
16
+ .action((recipient: string | undefined, options: { json?: boolean }) => {
15
17
  run(emit, () => {
18
+ recipient = requireRecipient(recipient, defaultRecipient);
16
19
  const rows = requireInboxService(service)
17
20
  .pending(recipient)
18
21
  .map((record) => ({
@@ -22,6 +25,7 @@ export function registerInboxCommand(
22
25
  created_at: record.created_at,
23
26
  body_bytes: record.body_bytes,
24
27
  body: record.body,
28
+ state: "pending",
25
29
  }));
26
30
  if (options.json) {
27
31
  emit.config({ format: "json" });
@@ -41,6 +45,52 @@ export function registerInboxCommand(
41
45
  } else emit.rows([status as unknown as Record<string, unknown>]);
42
46
  });
43
47
  });
48
+ inbox
49
+ .command("watch [recipient]")
50
+ .description("Surface pending messages for this instance; poll history until timeout")
51
+ .option("--timeout <seconds>", "Stop after this many seconds", "60")
52
+ .option("--json", "Emit one JSON record per surfaced message")
53
+ .action(async (recipient: string | undefined, options: { json?: boolean; timeout: string }) => {
54
+ let controller: AbortController | undefined;
55
+ let timer: ReturnType<typeof setTimeout> | undefined;
56
+ try {
57
+ const instanceId = requireRecipient(recipient, defaultRecipient);
58
+ const inboxService = requireInboxService(service);
59
+ const seconds = Number(options.timeout);
60
+ if (!Number.isFinite(seconds) || seconds <= 0 || seconds > 3600)
61
+ throw new Error("--timeout must be greater than 0 and at most 3600 seconds");
62
+ controller = new AbortController();
63
+ timer = setTimeout(() => controller!.abort(), seconds * 1000);
64
+ if (options.json) emit.config({ format: "json" });
65
+ for await (const message of watchInbox(inboxService, instanceId, {
66
+ signal: controller.signal,
67
+ })) {
68
+ const row = { ...message, state: "surfaced" };
69
+ if (options.json) emit.data(row);
70
+ else emit.rows([row]);
71
+ // An output error leaves this ID pending for prompt recovery.
72
+ await new Promise<void>((resolve, reject) =>
73
+ process.stdout.write("", (error) => (error ? reject(error) : resolve())),
74
+ );
75
+ inboxService.acknowledgeSurface(instanceId, [message.message_id]);
76
+ }
77
+ } catch (error) {
78
+ emit.error({
79
+ code: "inbox_failed",
80
+ message: error instanceof Error ? error.message : String(error),
81
+ });
82
+ emit.setExitCode(1);
83
+ } finally {
84
+ if (timer) clearTimeout(timer);
85
+ controller?.abort();
86
+ }
87
+ });
88
+ }
89
+
90
+ function requireRecipient(recipient: string | undefined, fallback?: () => string): string {
91
+ const resolved = recipient ?? fallback?.();
92
+ if (!resolved) throw new Error("recipient instance is required outside an agent session");
93
+ return resolved;
44
94
  }
45
95
 
46
96
  function requireInboxService(service?: HarneryInboxService): HarneryInboxService {
@@ -1,9 +1,10 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { existsSync } from "node:fs";
3
- import { isAbsolute, relative, resolve, sep } from "node:path";
2
+ import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
3
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
4
4
  import type { Command } from "commander";
5
5
  import type { EmitContext, HarneryProgramContext } from "../commander.ts";
6
6
  import { resolveCoordRoot } from "../core/agents/coord-client.ts";
7
+ import { artifactsRoot, parseArtifactManifest } from "../core/artifacts/index.ts";
7
8
  import {
8
9
  readCoordinationViewV3,
9
10
  requireAuthoritySafeCoordinationViewV3,
@@ -25,6 +26,15 @@ export function registerRmCommand(
25
26
  "Allowed directory; each target must be strictly inside it",
26
27
  )
27
28
  .option("-r, --recursive", "Permit directory removal after inspecting its complete contents")
29
+ .option(
30
+ "--clean-clone",
31
+ "Remove only clean standalone clones inside one owned artifact; refuse local-only commits and worktrees",
32
+ )
33
+ .option(
34
+ "--entry-limit <n>",
35
+ "Maximum inspected entries, 1-100000 (default 10000); larger limits require one current caller-owned managed artifact as --root",
36
+ (value: string) => Number(value),
37
+ )
28
38
  .option(
29
39
  "--allow-protected <path>",
30
40
  "Remove this one protected path (Git metadata, credentials, Harnery state, keys) inside a target; repeat per path",
@@ -65,7 +75,34 @@ export function registerRmCommand(
65
75
  const liveRoots = [coordRoot, gitTopLevel(process.cwd())].filter((dir): dir is string =>
66
76
  Boolean(dir),
67
77
  );
68
- const report = guardedRemove(paths, { ...options, liveRoots }, checkClaims);
78
+ const assertOwnedArtifact = (root: string): void => {
79
+ if (!coordRoot || dirname(root) !== realpathSync(artifactsRoot(coordRoot)))
80
+ throw new Error("Copied state requires --root to name one owned managed artifact");
81
+ const view = requireAuthoritySafeCoordinationViewV3(readCoordinationViewV3(coordRoot));
82
+ const owner = resolveOwner({ payload: null, coordRoot });
83
+ const ownId = owner ? liveInstanceIdV3(owner.instance_id) : undefined;
84
+ if (!ownId || !view.instances[ownId]?.authority_eligible)
85
+ throw new Error("Copied state requires an authenticated current artifact owner");
86
+ const record = resolve(root, ".harnery-artifact.json");
87
+ const stat = lstatSync(record);
88
+ if (!stat.isFile() || stat.isSymbolicLink())
89
+ throw new Error("An owned artifact requires a regular manifest file");
90
+ const parsed = parseArtifactManifest(JSON.parse(readFileSync(record, "utf8")));
91
+ if (
92
+ !parsed.ok ||
93
+ parsed.manifest.released_at ||
94
+ !parsed.manifest.created_by?.instance_id ||
95
+ liveInstanceIdV3(parsed.manifest.created_by.instance_id) !== ownId
96
+ )
97
+ throw new Error(
98
+ "Copied state requires a valid unreleased artifact owned by the caller",
99
+ );
100
+ };
101
+ const report = guardedRemove(
102
+ paths,
103
+ { ...options, liveRoots, assertOwnedArtifact },
104
+ checkClaims,
105
+ );
69
106
  if (options.json) {
70
107
  emit.config({ format: "json" });
71
108
  emit.data(report);
@@ -76,7 +113,9 @@ export function registerRmCommand(
76
113
  ];
77
114
  if (report.protected.length)
78
115
  lines.push(
79
- "Protected paths named with --allow-protected:",
116
+ options.cleanClone
117
+ ? "Verified clone metadata:"
118
+ : "Protected paths named with --allow-protected:",
80
119
  ...report.protected.map((path) => ` ${path}`),
81
120
  );
82
121
  if (report.links.length)
@@ -120,6 +120,7 @@ export function registerServersCommand(
120
120
  type: "unregistered",
121
121
  label: stray.command,
122
122
  pid: stray.pid,
123
+ ...(stray.start_token ? { start_token: stray.start_token } : {}),
123
124
  host: hostname(),
124
125
  started_at: new Date().toISOString(),
125
126
  registered_at: new Date().toISOString(),
@@ -207,10 +208,20 @@ export function registerServersCommand(
207
208
  emit.config({ format: "json" });
208
209
  emit.data({ applied: Boolean(options.yes), pruned: report.pruned, decisions, results });
209
210
  } else {
210
- const lines = decisions.map(
211
- (d) =>
212
- `${d.action === "stop" ? (options.yes ? "stopped" : "would stop") : "keep"} ${d.id} ${d.reason}`,
213
- );
211
+ const lines = decisions.map((decision) => {
212
+ const result = results.find((item) => item.id === decision.id);
213
+ const action =
214
+ decision.action === "keep"
215
+ ? "keep"
216
+ : !options.yes
217
+ ? "would stop"
218
+ : result?.stopped
219
+ ? "stopped"
220
+ : result?.method === "not-running"
221
+ ? "already stopped"
222
+ : "could not stop";
223
+ return `${action} ${decision.id} ${result?.detail ?? decision.reason}`;
224
+ });
214
225
  if (report.pruned.length)
215
226
  lines.push(`Removed records of exited servers: ${report.pruned.join(", ")}`);
216
227
  if (!decisions.length) lines.push("No registered servers.");
@@ -218,6 +229,7 @@ export function registerServersCommand(
218
229
  lines.push("Preview only. Re-run with --yes to stop.");
219
230
  emit.text(lines.join("\n"));
220
231
  }
232
+ if (results.some((result) => result.method === "refused")) emit.setExitCode(1);
221
233
  } catch (error) {
222
234
  fail(error);
223
235
  }
@@ -83,11 +83,16 @@ export function registerSupervisorCommand(
83
83
  .action(async (options: { root?: string }) => {
84
84
  try {
85
85
  const root = resolve(options.root ?? coordRoot(context));
86
- let status = requestSupervisorStop(root);
86
+ const requested = requestSupervisorStop(root);
87
+ let status = requested;
87
88
  const deadline = Date.now() + 5_000;
88
89
  while (status.running && Date.now() < deadline) {
89
90
  await new Promise((done) => setTimeout(done, 50));
90
- status = readSupervisorStatus(root);
91
+ status = {
92
+ ...readSupervisorStatus(root),
93
+ signal_sent: requested.signal_sent,
94
+ signal_verdict: requested.signal_verdict,
95
+ };
91
96
  }
92
97
  emit.data(status);
93
98
  } catch (error) {
@@ -172,17 +172,25 @@ function renderToc(r: TocResult): string {
172
172
  return lines.join("\n");
173
173
  }
174
174
 
175
- async function runSection(file: string, query: string, opts: SectionOpts): Promise<SectionResult> {
175
+ export async function runSection(
176
+ file: string,
177
+ query: string,
178
+ opts: SectionOpts,
179
+ ): Promise<SectionResult> {
176
180
  if (!query.trim()) throw new Error("header query required");
177
181
  const tocResult = await runToc(file, {});
178
182
  const absPath = resolve(file);
179
183
  const lines = readFileSync(absPath, "utf8").split("\n");
180
184
 
181
- const matches = tocResult.headers.filter((h) =>
182
- opts.caseSensitive
183
- ? h.text.includes(query)
184
- : h.text.toLowerCase().includes(query.toLowerCase()),
185
- );
185
+ const same = (a: string, b: string) =>
186
+ opts.caseSensitive ? a === b : a.toLowerCase() === b.toLowerCase();
187
+ const contains = (a: string, b: string) =>
188
+ opts.caseSensitive ? a.includes(b) : a.toLowerCase().includes(b.toLowerCase());
189
+ // A header that equals the query wins over headers that merely contain it,
190
+ // so "Media" can still select "## Media" beside "## Media recipes".
191
+ const exact = tocResult.headers.filter((h) => same(h.text, query.trim()));
192
+ const matches =
193
+ exact.length > 0 ? exact : tocResult.headers.filter((h) => contains(h.text, query));
186
194
  if (matches.length === 0) {
187
195
  throw new Error(
188
196
  `no header matching "${query}". Run \`${resolveBinName()} toc ${file}\` to list available headers.`,
@@ -618,11 +618,11 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
618
618
  workflow
619
619
  .command("reclaim <run-id>")
620
620
  .description(
621
- "Resolve a workspace preserved because it was dirty: salvage the work to its branch, then release.",
621
+ "Preview reclaim of an inactive owned workspace, or confirm salvage or discard and release.",
622
622
  )
623
623
  .option("--yes", "Confirm that this commits or discards uncommitted work, then releases")
624
624
  .option("--discard", "Throw the uncommitted work away instead of salvaging it")
625
- .option("--json", "Emit the reclaim preparation and cleanup result as JSON")
625
+ .option("--json", "Emit the reclaim preview or confirmed result as JSON")
626
626
  .action(async (runId: string, opts: WorkflowReclaimOpts) => {
627
627
  const coordRoot = resolveCoordRoot();
628
628
  if (!coordRoot) {
@@ -632,53 +632,53 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
632
632
  });
633
633
  process.exit(1);
634
634
  }
635
- if (!opts.yes) {
636
- emit.error({
637
- code: "reclaim_confirmation_required",
638
- message: opts.discard
639
- ? "reclaim --discard permanently destroys uncommitted work; pass --yes to confirm"
640
- : "workspace reclaim commits uncommitted work to its branch and then releases the worktree; pass --yes to confirm",
641
- });
642
- process.exit(1);
643
- }
644
635
  try {
645
- const { inspectWorkflowWorkspace, prepareReclaim, cleanupWorkspace } = await import(
636
+ const { createLocalGitWorktreeProvider, readWorkflowRunManifest } = await import(
646
637
  "../core/workflow/index.ts"
647
638
  );
648
- const inspection = inspectWorkflowWorkspace(coordRoot, runId);
649
- if (!inspection.ok) {
650
- emit.error({ code: "workflow_reclaim_failed", message: inspection.error });
651
- process.exit(1);
652
- }
653
- const allocation = inspection.value.allocation;
654
- if (!allocation) {
639
+ const binding = readWorkflowRunManifest(coordRoot, runId).execution.workspace_binding;
640
+ if (binding?.provider.id !== "local-git-worktree") {
655
641
  emit.error({
656
642
  code: "workflow_reclaim_unavailable",
657
643
  message: `run ${runId} has no provider-owned Git workspace to reclaim`,
658
644
  });
659
645
  process.exit(1);
660
646
  }
661
- const preparation = prepareReclaim({
662
- worktreePath: allocation.active_root,
647
+ const provider = createLocalGitWorktreeProvider({ coordRoot });
648
+ const result = await provider.reclaim(binding, {
663
649
  mode: opts.discard ? "discard" : "salvage",
664
- runId,
650
+ yes: opts.yes,
665
651
  });
666
- // Cleanup still owns removal. Reclaim only changes whether the tree it
667
- // finds is dirty, so there is one audited release path, not two.
668
- const provider = await builtInProviderForRun(coordRoot, runId);
669
- const cleanup = await cleanupWorkspace({ coordRoot, runId, provider });
670
652
  if (opts.json) {
671
653
  emit.config({ format: "json" });
672
- emit.data({ preparation, cleanup });
654
+ emit.data(result);
655
+ return;
656
+ }
657
+ if (result.action === "preview") {
658
+ const holders = result.lease_holder.map(
659
+ (holder) =>
660
+ `${holder.scope}: ${holder.owner_id ?? "unknown"}${holder.pid ? ` (pid ${holder.pid})` : ""}${holder.detail ? `; ${holder.detail}` : ""}`,
661
+ );
662
+ emit.text(
663
+ `reclaim preview: ${result.run_id}\n` +
664
+ `mode: ${result.mode}\nliveness: ${result.liveness}\nownership: ${result.ownership}\n` +
665
+ `lease holder: ${holders.length > 0 ? holders.join(", ") : "none observed"}\n` +
666
+ `dirty paths: ${result.dirty_paths === null ? "unknown" : result.dirty_paths.length > 0 ? result.dirty_paths.join(", ") : "none"}\n` +
667
+ (result.ownership_detail ? `ownership detail: ${result.ownership_detail}\n` : "") +
668
+ `Pass --yes to confirm ${result.mode} and release after all checks pass.\n`,
669
+ );
673
670
  return;
674
671
  }
675
672
  emit.text(
676
- `reclaim ${preparation.action}: ${preparation.detail}\n` +
677
- `workspace cleanup ${cleanup.status}: ${cleanup.binding_id}\n`,
673
+ `reclaim ${result.preparation.action}: ${result.preparation.detail}\n` +
674
+ `workspace cleanup ${result.cleanup.status}: ${result.cleanup.binding_id}\n`,
678
675
  );
679
676
  } catch (error) {
680
677
  emit.error({
681
- code: "workflow_reclaim_failed",
678
+ code:
679
+ error instanceof Error && "code" in error && typeof error.code === "string"
680
+ ? error.code
681
+ : "workflow_reclaim_failed",
682
682
  message: error instanceof Error ? error.message : String(error),
683
683
  });
684
684
  process.exit(1);
@@ -88,6 +88,7 @@ interface LiveAuthorityBaseV3 {
88
88
  nativeSessionId: string;
89
89
  adapter: Adapter;
90
90
  observationId?: string;
91
+ expectedGenerationId?: `gen_${string}`;
91
92
  }
92
93
 
93
94
  /**
@@ -648,6 +649,13 @@ function recordLiveAuthority<S extends CoordinationAuthoritySignalV3>(
648
649
  platform: livePlatformV3(),
649
650
  expected_prior_state_digest: coordinationAuthorityStateDigestV3(expected),
650
651
  desired_state_digest: coordinationAuthorityStateDigestV3(desired),
652
+ expected_generation_id:
653
+ input.expectedGenerationId ??
654
+ ((subject === input.owner
655
+ ? expected.v3_generation_id
656
+ : readLiveCoordinationRow(input.coordRoot, input.owner)?.v3_generation_id) as
657
+ | `gen_${string}`
658
+ | undefined),
651
659
  reconciler: {
652
660
  readStateDigest: () => {
653
661
  const heartbeat = readHeartbeat(input.coordRoot, subject);
@@ -681,6 +689,12 @@ export function liveCoordinationWriteModeV3(coordRoot: string): EventV3WriteMode
681
689
  }
682
690
 
683
691
  function requireHeartbeat(input: LiveAuthorityBaseV3, owner: string): Heartbeat {
692
+ if (
693
+ input.expectedGenerationId &&
694
+ readLiveCoordinationRow(input.coordRoot, owner)?.v3_generation_id !== input.expectedGenerationId
695
+ ) {
696
+ throw new LiveCoordinationAuthorityV3Error("expected_generation_changed");
697
+ }
684
698
  const heartbeat = ensureLiveCoordinationHeartbeat(
685
699
  input.coordRoot,
686
700
  owner,
@@ -688,6 +702,9 @@ function requireHeartbeat(input: LiveAuthorityBaseV3, owner: string): Heartbeat
688
702
  input.adapter,
689
703
  );
690
704
  if (!heartbeat) throw new LiveCoordinationAuthorityV3Error(`heartbeat_missing:${owner}`);
705
+ if (input.expectedGenerationId && heartbeat.v3_generation_id !== input.expectedGenerationId) {
706
+ throw new LiveCoordinationAuthorityV3Error("expected_generation_changed");
707
+ }
691
708
  return heartbeat;
692
709
  }
693
710