harnery 0.43.0 → 0.45.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 (323) hide show
  1. package/README.md +3 -3
  2. package/dist/commander.js +1 -0
  3. package/dist/commands/agents.d.ts.map +1 -1
  4. package/dist/commands/agents.js +41 -48
  5. package/dist/commands/browse.d.ts.map +1 -1
  6. package/dist/commands/browse.js +38 -11
  7. package/dist/commands/diagnostics.d.ts.map +1 -1
  8. package/dist/commands/diagnostics.js +10 -0
  9. package/dist/commands/doctor.d.ts +12 -0
  10. package/dist/commands/doctor.d.ts.map +1 -1
  11. package/dist/commands/doctor.js +22 -7
  12. package/dist/commands/files.d.ts +2 -2
  13. package/dist/commands/files.d.ts.map +1 -1
  14. package/dist/commands/files.js +7 -4
  15. package/dist/commands/governor.js +22 -10
  16. package/dist/commands/inbox.d.ts +1 -1
  17. package/dist/commands/inbox.d.ts.map +1 -1
  18. package/dist/commands/inbox.js +55 -2
  19. package/dist/commands/rm.d.ts.map +1 -1
  20. package/dist/commands/rm.js +28 -4
  21. package/dist/commands/servers.d.ts.map +1 -1
  22. package/dist/commands/servers.js +26 -17
  23. package/dist/commands/supervisor.d.ts.map +1 -1
  24. package/dist/commands/supervisor.js +7 -2
  25. package/dist/commands/toc.d.ts +14 -0
  26. package/dist/commands/toc.d.ts.map +1 -1
  27. package/dist/commands/toc.js +7 -4
  28. package/dist/commands/tunnel.d.ts +15 -0
  29. package/dist/commands/tunnel.d.ts.map +1 -1
  30. package/dist/commands/tunnel.js +453 -153
  31. package/dist/commands/turn-rollback.d.ts +5 -0
  32. package/dist/commands/turn-rollback.d.ts.map +1 -0
  33. package/dist/commands/turn-rollback.js +76 -0
  34. package/dist/commands/workflow.d.ts.map +1 -1
  35. package/dist/commands/workflow.js +68 -30
  36. package/dist/core/agents/coord-client.d.ts.map +1 -1
  37. package/dist/core/agents/coord-client.js +18 -0
  38. package/dist/core/agents/live-authority-v3.d.ts +1 -0
  39. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  40. package/dist/core/agents/live-authority-v3.js +11 -0
  41. package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
  42. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  43. package/dist/core/agents/live-lifecycle-v3.js +28 -7
  44. package/dist/core/agents/mailbox.d.ts +9 -8
  45. package/dist/core/agents/mailbox.d.ts.map +1 -1
  46. package/dist/core/agents/mailbox.js +24 -17
  47. package/dist/core/agents/render/prompt-context.d.ts +2 -0
  48. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  49. package/dist/core/agents/render/prompt-context.js +8 -12
  50. package/dist/core/agents/render/session-context.d.ts +2 -8
  51. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  52. package/dist/core/agents/render/session-context.js +7 -30
  53. package/dist/core/agents/rules/stop-hook.js +3 -3
  54. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  55. package/dist/core/agents/state/heartbeat-writer.js +25 -14
  56. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  57. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  58. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  59. package/dist/core/agents/state/pidmap.js +5 -0
  60. package/dist/core/agents/state/proc-start.d.ts +19 -1
  61. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  62. package/dist/core/agents/state/proc-start.js +46 -1
  63. package/dist/core/agents/state/process-handle.d.ts +26 -0
  64. package/dist/core/agents/state/process-handle.d.ts.map +1 -0
  65. package/dist/core/agents/state/process-handle.js +121 -0
  66. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  67. package/dist/core/agents/state/stale-sweep.js +180 -70
  68. package/dist/core/agents/state/unchanged-file.d.ts +44 -0
  69. package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
  70. package/dist/core/agents/state/unchanged-file.js +390 -0
  71. package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
  72. package/dist/core/artifacts/delivery-card.js +2 -1
  73. package/dist/core/context/index.d.ts +22 -10
  74. package/dist/core/context/index.d.ts.map +1 -1
  75. package/dist/core/context/index.js +381 -119
  76. package/dist/core/context/observation.d.ts +26 -0
  77. package/dist/core/context/observation.d.ts.map +1 -0
  78. package/dist/core/context/observation.js +11 -0
  79. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  80. package/dist/core/diagnostics/bundle.js +25 -14
  81. package/dist/core/diagnostics/path-identity.d.ts +3 -0
  82. package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
  83. package/dist/core/diagnostics/path-identity.js +12 -0
  84. package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
  85. package/dist/core/diagnostics/sanitize.js +4 -3
  86. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  87. package/dist/core/events/v3/authority-outbox.js +29 -0
  88. package/dist/core/events/v3/generation-custody.d.ts +5 -0
  89. package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
  90. package/dist/core/events/v3/generation-custody.js +50 -0
  91. package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
  92. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  93. package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
  94. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  95. package/dist/core/events/v3/producers/recorder.js +77 -64
  96. package/dist/core/governor/planning.d.ts.map +1 -1
  97. package/dist/core/governor/planning.js +14 -1
  98. package/dist/core/governor/service-read.d.ts +10 -2
  99. package/dist/core/governor/service-read.d.ts.map +1 -1
  100. package/dist/core/governor/service-read.js +105 -22
  101. package/dist/core/governor/service.d.ts +20 -2
  102. package/dist/core/governor/service.d.ts.map +1 -1
  103. package/dist/core/governor/service.js +72 -47
  104. package/dist/core/hooks/cli.js +33 -5
  105. package/dist/core/inbox/contract.d.ts +2 -1
  106. package/dist/core/inbox/contract.d.ts.map +1 -1
  107. package/dist/core/inbox/index.d.ts +2 -0
  108. package/dist/core/inbox/index.d.ts.map +1 -1
  109. package/dist/core/inbox/index.js +2 -0
  110. package/dist/core/inbox/peer-messages.d.ts +37 -0
  111. package/dist/core/inbox/peer-messages.d.ts.map +1 -0
  112. package/dist/core/inbox/peer-messages.js +72 -0
  113. package/dist/core/inbox/service.d.ts +8 -1
  114. package/dist/core/inbox/service.d.ts.map +1 -1
  115. package/dist/core/inbox/service.js +67 -21
  116. package/dist/core/inbox/targets.d.ts +22 -0
  117. package/dist/core/inbox/targets.d.ts.map +1 -0
  118. package/dist/core/inbox/targets.js +108 -0
  119. package/dist/core/inbox/watcher.js +7 -3
  120. package/dist/core/journal/index.d.ts.map +1 -1
  121. package/dist/core/journal/index.js +17 -2
  122. package/dist/core/resources/contract.d.ts +3 -0
  123. package/dist/core/resources/contract.d.ts.map +1 -1
  124. package/dist/core/resources/service-status.d.ts +14 -1
  125. package/dist/core/resources/service-status.d.ts.map +1 -1
  126. package/dist/core/resources/service-status.js +23 -13
  127. package/dist/core/resources/service.d.ts +7 -13
  128. package/dist/core/resources/service.d.ts.map +1 -1
  129. package/dist/core/resources/service.js +34 -91
  130. package/dist/core/resources/status.d.ts +2 -5
  131. package/dist/core/resources/status.d.ts.map +1 -1
  132. package/dist/core/resources/status.js +36 -7
  133. package/dist/core/semantic/service.d.ts.map +1 -1
  134. package/dist/core/semantic/service.js +2 -9
  135. package/dist/core/servers/index.d.ts +34 -22
  136. package/dist/core/servers/index.d.ts.map +1 -1
  137. package/dist/core/servers/index.js +297 -144
  138. package/dist/core/servers/tunnels.d.ts +7 -7
  139. package/dist/core/servers/tunnels.d.ts.map +1 -1
  140. package/dist/core/servers/tunnels.js +47 -10
  141. package/dist/core/supervisor/contract.d.ts +12 -0
  142. package/dist/core/supervisor/contract.d.ts.map +1 -1
  143. package/dist/core/supervisor/explanations.d.ts.map +1 -1
  144. package/dist/core/supervisor/explanations.js +2 -1
  145. package/dist/core/supervisor/findings.d.ts.map +1 -1
  146. package/dist/core/supervisor/findings.js +54 -9
  147. package/dist/core/supervisor/service.d.ts +7 -1
  148. package/dist/core/supervisor/service.d.ts.map +1 -1
  149. package/dist/core/supervisor/service.js +59 -29
  150. package/dist/core/supervisor/services.d.ts.map +1 -1
  151. package/dist/core/supervisor/services.js +22 -1
  152. package/dist/core/supervisor/status.d.ts +17 -2
  153. package/dist/core/supervisor/status.d.ts.map +1 -1
  154. package/dist/core/supervisor/status.js +70 -12
  155. package/dist/core/supervisor/timeline.d.ts.map +1 -1
  156. package/dist/core/supervisor/timeline.js +2 -1
  157. package/dist/core/turn-rollback/content.d.ts +53 -0
  158. package/dist/core/turn-rollback/content.d.ts.map +1 -0
  159. package/dist/core/turn-rollback/content.js +396 -0
  160. package/dist/core/turn-rollback/index.d.ts +72 -0
  161. package/dist/core/turn-rollback/index.d.ts.map +1 -0
  162. package/dist/core/turn-rollback/index.js +443 -0
  163. package/dist/core/work/state.js +7 -7
  164. package/dist/core/workflow/engine.d.ts.map +1 -1
  165. package/dist/core/workflow/engine.js +105 -32
  166. package/dist/core/workflow/failure-evidence.d.ts +16 -0
  167. package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
  168. package/dist/core/workflow/failure-evidence.js +114 -0
  169. package/dist/core/workflow/index.d.ts +5 -3
  170. package/dist/core/workflow/index.d.ts.map +1 -1
  171. package/dist/core/workflow/index.js +3 -2
  172. package/dist/core/workflow/live-session-v3.d.ts +6 -0
  173. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  174. package/dist/core/workflow/live-session-v3.js +126 -5
  175. package/dist/core/workflow/proof.d.ts +5 -5
  176. package/dist/core/workflow/proof.d.ts.map +1 -1
  177. package/dist/core/workflow/proof.js +115 -20
  178. package/dist/core/workflow/recovery-audit.d.ts +30 -0
  179. package/dist/core/workflow/recovery-audit.d.ts.map +1 -0
  180. package/dist/core/workflow/recovery-audit.js +116 -0
  181. package/dist/core/workflow/run-state.d.ts +20 -1
  182. package/dist/core/workflow/run-state.d.ts.map +1 -1
  183. package/dist/core/workflow/run-state.js +141 -32
  184. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  185. package/dist/core/workflow/spawn-claude.js +19 -0
  186. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  187. package/dist/core/workflow/spawn-codex.js +13 -0
  188. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  189. package/dist/core/workflow/spawn-cursor.js +19 -0
  190. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  191. package/dist/core/workflow/spawn-opencode.js +14 -0
  192. package/dist/core/workflow/types.d.ts +64 -17
  193. package/dist/core/workflow/types.d.ts.map +1 -1
  194. package/dist/core/workflow/workspaces/index.d.ts +0 -1
  195. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  196. package/dist/core/workflow/workspaces/index.js +0 -1
  197. package/dist/core/workflow/workspaces/leases.d.ts +3 -1
  198. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  199. package/dist/core/workflow/workspaces/leases.js +30 -16
  200. package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
  201. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  202. package/dist/core/workflow/workspaces/local-git.js +328 -8
  203. package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
  204. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
  205. package/dist/core/workflow/workspaces/ownership.js +194 -0
  206. package/dist/core/workflow/workspaces/reclaim.d.ts +40 -0
  207. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  208. package/dist/core/workflow/workspaces/reclaim.js +14 -0
  209. package/dist/core/workflow/workspaces/recovery.d.ts +2 -0
  210. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  211. package/dist/core/workflow/workspaces/recovery.js +42 -10
  212. package/dist/core/workflow/workspaces/state.d.ts +1 -0
  213. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  214. package/dist/core/workflow/workspaces/state.js +21 -1
  215. package/dist/lib/browser/windows-chrome.d.ts +100 -12
  216. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  217. package/dist/lib/browser/windows-chrome.js +262 -73
  218. package/dist/lib/docs-links.d.ts.map +1 -1
  219. package/dist/lib/docs-links.js +45 -2
  220. package/dist/lib/guarded-remove.d.ts +7 -0
  221. package/dist/lib/guarded-remove.d.ts.map +1 -1
  222. package/dist/lib/guarded-remove.js +106 -12
  223. package/dist/lib/identities/assume.d.ts.map +1 -1
  224. package/dist/lib/identities/assume.js +4 -0
  225. package/dist/lib/tunnel/state.d.ts +13 -6
  226. package/dist/lib/tunnel/state.d.ts.map +1 -1
  227. package/dist/lib/tunnel/state.js +58 -12
  228. package/dist/lib/zip/index.d.ts +41 -0
  229. package/dist/lib/zip/index.d.ts.map +1 -0
  230. package/dist/lib/zip/index.js +398 -0
  231. package/package.json +12 -1
  232. package/src/commander.ts +10 -0
  233. package/src/commands/agents.ts +54 -63
  234. package/src/commands/browse.ts +50 -14
  235. package/src/commands/diagnostics.ts +10 -0
  236. package/src/commands/doctor.ts +26 -7
  237. package/src/commands/files.ts +13 -5
  238. package/src/commands/governor.ts +23 -10
  239. package/src/commands/inbox.ts +52 -2
  240. package/src/commands/rm.ts +43 -4
  241. package/src/commands/servers.ts +27 -26
  242. package/src/commands/supervisor.ts +7 -2
  243. package/src/commands/toc.ts +14 -6
  244. package/src/commands/tunnel.ts +533 -166
  245. package/src/commands/turn-rollback.ts +111 -0
  246. package/src/commands/workflow.ts +89 -30
  247. package/src/core/agents/coord-client.ts +20 -0
  248. package/src/core/agents/live-authority-v3.ts +17 -0
  249. package/src/core/agents/live-lifecycle-v3.ts +52 -14
  250. package/src/core/agents/mailbox.ts +26 -17
  251. package/src/core/agents/render/prompt-context.ts +10 -12
  252. package/src/core/agents/render/session-context.ts +9 -32
  253. package/src/core/agents/rules/stop-hook.ts +3 -3
  254. package/src/core/agents/state/heartbeat-writer.ts +29 -12
  255. package/src/core/agents/state/live-coordination-writer.ts +29 -0
  256. package/src/core/agents/state/pidmap.ts +13 -0
  257. package/src/core/agents/state/proc-start.ts +62 -2
  258. package/src/core/agents/state/process-handle.ts +165 -0
  259. package/src/core/agents/state/stale-sweep.ts +247 -82
  260. package/src/core/agents/state/unchanged-file.ts +456 -0
  261. package/src/core/artifacts/delivery-card.ts +7 -1
  262. package/src/core/context/index.ts +446 -136
  263. package/src/core/context/observation.ts +29 -0
  264. package/src/core/diagnostics/bundle.ts +23 -18
  265. package/src/core/diagnostics/path-identity.ts +15 -0
  266. package/src/core/diagnostics/sanitize.ts +4 -3
  267. package/src/core/events/v3/authority-outbox.ts +56 -0
  268. package/src/core/events/v3/generation-custody.ts +59 -0
  269. package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
  270. package/src/core/events/v3/producers/recorder.ts +94 -68
  271. package/src/core/governor/planning.ts +20 -8
  272. package/src/core/governor/service-read.ts +128 -20
  273. package/src/core/governor/service.ts +119 -42
  274. package/src/core/hooks/cli.ts +33 -5
  275. package/src/core/inbox/contract.ts +2 -1
  276. package/src/core/inbox/index.ts +2 -0
  277. package/src/core/inbox/peer-messages.ts +94 -0
  278. package/src/core/inbox/service.ts +88 -22
  279. package/src/core/inbox/targets.ts +148 -0
  280. package/src/core/inbox/watcher.ts +8 -9
  281. package/src/core/journal/index.ts +19 -2
  282. package/src/core/resources/contract.ts +4 -0
  283. package/src/core/resources/service-status.ts +35 -12
  284. package/src/core/resources/service.ts +57 -95
  285. package/src/core/resources/status.ts +57 -8
  286. package/src/core/semantic/service.ts +2 -8
  287. package/src/core/servers/index.ts +376 -141
  288. package/src/core/servers/tunnels.ts +57 -13
  289. package/src/core/supervisor/contract.ts +13 -0
  290. package/src/core/supervisor/explanations.ts +4 -1
  291. package/src/core/supervisor/findings.ts +68 -10
  292. package/src/core/supervisor/service.ts +87 -26
  293. package/src/core/supervisor/services.ts +26 -8
  294. package/src/core/supervisor/status.ts +89 -12
  295. package/src/core/supervisor/timeline.ts +2 -1
  296. package/src/core/turn-rollback/content.ts +499 -0
  297. package/src/core/turn-rollback/index.ts +580 -0
  298. package/src/core/work/state.ts +7 -7
  299. package/src/core/workflow/engine.ts +131 -49
  300. package/src/core/workflow/failure-evidence.ts +141 -0
  301. package/src/core/workflow/index.ts +12 -2
  302. package/src/core/workflow/live-session-v3.ts +146 -5
  303. package/src/core/workflow/proof.ts +134 -26
  304. package/src/core/workflow/recovery-audit.ts +181 -0
  305. package/src/core/workflow/run-state.ts +165 -34
  306. package/src/core/workflow/spawn-claude.ts +18 -0
  307. package/src/core/workflow/spawn-codex.ts +13 -0
  308. package/src/core/workflow/spawn-cursor.ts +18 -0
  309. package/src/core/workflow/spawn-opencode.ts +14 -0
  310. package/src/core/workflow/types.ts +51 -17
  311. package/src/core/workflow/workspaces/index.ts +0 -1
  312. package/src/core/workflow/workspaces/leases.ts +48 -17
  313. package/src/core/workflow/workspaces/local-git.ts +388 -7
  314. package/src/core/workflow/workspaces/ownership.ts +232 -0
  315. package/src/core/workflow/workspaces/reclaim.ts +62 -0
  316. package/src/core/workflow/workspaces/recovery.ts +45 -10
  317. package/src/core/workflow/workspaces/state.ts +20 -0
  318. package/src/lib/browser/windows-chrome.ts +346 -86
  319. package/src/lib/docs-links.ts +49 -2
  320. package/src/lib/guarded-remove.ts +132 -14
  321. package/src/lib/identities/assume.ts +4 -0
  322. package/src/lib/tunnel/state.ts +70 -15
  323. package/src/lib/zip/index.ts +483 -0
@@ -9,7 +9,10 @@ import {
9
9
  } from "node:fs";
10
10
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
11
11
  import { stateDirMode, stateFileMode } from "../../storage/modes.ts";
12
+ import { beginWorkflowRecovery, validateWorkflowRecoveryAuthorization } from "../recovery-audit.ts";
13
+ import { acquireWorkflowResumeLease } from "../run-state.ts";
12
14
  import { WorkspaceAttestationError } from "./attestation-error.ts";
15
+ import { cleanupWorkspace } from "./cleanup.ts";
13
16
  import {
14
17
  git,
15
18
  gitMaybe,
@@ -22,6 +25,11 @@ import {
22
25
  worktreeInventory,
23
26
  } from "./git.ts";
24
27
  import { acquireNoClobberLease } from "./leases.ts";
28
+ import {
29
+ claimOverlapsWorkspace,
30
+ reserveWorkspaceReclaim,
31
+ type WorkspaceReclaimReservation,
32
+ } from "./ownership.ts";
25
33
  import {
26
34
  assertContainedExisting,
27
35
  assertPathIdentity,
@@ -36,13 +44,24 @@ import {
36
44
  type ValidatedRoot,
37
45
  validateConfiguredRoot,
38
46
  } from "./paths.ts";
39
- import { readWorkflowLiveness } from "./recovery.ts";
47
+ import {
48
+ prepareReclaim,
49
+ type ReclaimLeaseHolder,
50
+ type ReclaimMode,
51
+ type ReclaimPreparation,
52
+ WorkflowReclaimError,
53
+ type WorkspaceReclaimOptions,
54
+ type WorkspaceReclaimPreview,
55
+ type WorkspaceReclaimResult,
56
+ } from "./reclaim.ts";
57
+ import { hasActiveWorkflowEvidence, readWorkflowLiveness } from "./recovery.ts";
40
58
  import {
41
59
  appendWorkspaceEvent,
42
60
  fileSha256,
43
61
  readWorkflowSupplement,
44
62
  readWorkspaceBinding,
45
63
  readWorkspaceClaim,
64
+ readWorkspaceClaims,
46
65
  readWorkspaceEvents,
47
66
  stableDigest,
48
67
  type WorkspaceClaim,
@@ -86,11 +105,18 @@ export interface LocalGitWorktreeProviderOptions {
86
105
  descriptorPathsSupported?: () => boolean;
87
106
  }
88
107
 
108
+ export interface LocalGitWorktreeProvider extends WorkspaceProvider {
109
+ reclaim(
110
+ binding: WorkspaceBinding,
111
+ options: WorkspaceReclaimOptions,
112
+ ): Promise<WorkspaceReclaimResult>;
113
+ }
114
+
89
115
  export function createLocalGitWorktreeProvider(
90
116
  options: LocalGitWorktreeProviderOptions,
91
- ): WorkspaceProvider {
117
+ ): LocalGitWorktreeProvider {
92
118
  const coordRoot = resolve(options.coordRoot);
93
- return {
119
+ const provider: LocalGitWorktreeProvider = {
94
120
  probe: (input) => probe(input, options.descriptorPathsSupported),
95
121
  allocate: (request) => allocate(coordRoot, request),
96
122
  readBinding: async (binding) => {
@@ -103,7 +129,9 @@ export function createLocalGitWorktreeProvider(
103
129
  previewIntegration: (input) => previewIntegration(coordRoot, input),
104
130
  applyAuthorizedIntegration: (input) => applyAuthorizedIntegration(coordRoot, input),
105
131
  cleanup: (binding, intent) => cleanup(coordRoot, binding, intent),
132
+ reclaim: (binding, reclaimOptions) => reclaim(coordRoot, provider, binding, reclaimOptions),
106
133
  };
134
+ return provider;
107
135
  }
108
136
 
109
137
  export async function probe(
@@ -783,6 +811,325 @@ function cancelUnderLease(
783
811
  return { ...base, status: "cancelled" };
784
812
  }
785
813
 
814
+ function reclaimPreview(
815
+ coordRoot: string,
816
+ binding: WorkspaceBinding,
817
+ mode: ReclaimMode,
818
+ ): WorkspaceReclaimPreview {
819
+ const preview: WorkspaceReclaimPreview = {
820
+ action: "preview",
821
+ run_id: binding.run_id,
822
+ mode,
823
+ liveness: readWorkflowLiveness(coordRoot, binding.run_id),
824
+ evidence_sha256: reclaimEvidenceDigest(coordRoot, binding),
825
+ ownership: "unproven",
826
+ dirty_paths: null,
827
+ lease_holder: reclaimLeaseHolders(coordRoot, binding),
828
+ };
829
+ try {
830
+ const current = attestSync(coordRoot, binding);
831
+ if (current.status === "ok") {
832
+ preview.ownership = "ok";
833
+ preview.dirty_paths = [...(current.repository?.dirty_paths ?? [])];
834
+ } else {
835
+ preview.ownership_detail =
836
+ current.provider_drift.join("; ") || "ownership attestation failed";
837
+ }
838
+ } catch (error) {
839
+ preview.ownership_detail = (error as Error).message;
840
+ }
841
+ return preview;
842
+ }
843
+
844
+ function reclaimEvidenceDigest(coordRoot: string, binding: WorkspaceBinding): string {
845
+ const evidence = (name: string): string | null => {
846
+ try {
847
+ return readFileSync(join(coordRoot, ".harnery", "workflows", binding.run_id, name), "utf8");
848
+ } catch (error) {
849
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
850
+ throw error;
851
+ }
852
+ };
853
+ return stableDigest({
854
+ binding,
855
+ proof: evidence("proof.json"),
856
+ transcript: evidence("transcript.jsonl"),
857
+ });
858
+ }
859
+
860
+ function assertReclaimLiveness(
861
+ coordRoot: string,
862
+ binding: WorkspaceBinding,
863
+ recovery?: WorkspaceReclaimOptions["unknownLiveness"],
864
+ ): void {
865
+ const liveness = readWorkflowLiveness(coordRoot, binding.run_id);
866
+ if (liveness === "active" || (recovery && hasActiveWorkflowEvidence(coordRoot, binding.run_id))) {
867
+ throw new WorkflowReclaimError(
868
+ "workflow_reclaim_run_live",
869
+ `run ${binding.run_id} has active evidence; its workspace was left untouched`,
870
+ );
871
+ }
872
+ if (recovery && reclaimEvidenceDigest(coordRoot, binding) !== recovery.evidenceSha256) {
873
+ throw new WorkflowReclaimError(
874
+ "workflow_reclaim_recovery_mismatch",
875
+ "workflow reclaim requires the exact current preview evidence digest",
876
+ );
877
+ }
878
+ if (liveness === "inactive") return;
879
+ if (liveness === "unknown" && recovery) return;
880
+ throw new WorkflowReclaimError(
881
+ "workflow_reclaim_liveness_unknown",
882
+ `run ${binding.run_id} has missing, corrupt, or ambiguous liveness evidence; its workspace was left untouched`,
883
+ );
884
+ }
885
+
886
+ function assertReclaimOwnership(coordRoot: string, binding: WorkspaceBinding): WorkspaceClaim {
887
+ try {
888
+ const claim = requiredClaim(coordRoot, binding);
889
+ const current = attestSync(coordRoot, binding);
890
+ if (current.status !== "ok") {
891
+ throw new Error(current.provider_drift.join("; ") || "ownership attestation failed");
892
+ }
893
+ return claim;
894
+ } catch (error) {
895
+ throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", (error as Error).message);
896
+ }
897
+ }
898
+
899
+ function assertPeerWorkspaceAuthority(coordRoot: string, claim: WorkspaceClaim): void {
900
+ try {
901
+ for (const peer of readWorkspaceClaims(coordRoot, PROVIDER_ID)) {
902
+ if (peer.binding_id === claim.binding_id) continue;
903
+ const binding = readWorkspaceBinding(coordRoot, PROVIDER_ID, peer.binding_id);
904
+ const events = readWorkspaceEvents(coordRoot, PROVIDER_ID, peer.binding_id);
905
+ if (binding) requiredClaim(coordRoot, binding);
906
+ else if (events.some((event) => event.event === "bound")) {
907
+ throw new Error("retained peer binding authority is missing");
908
+ }
909
+ if (!claimOverlapsWorkspace(peer, claim.workspace_root)) continue;
910
+ // Process termination, archival and an expired reservation do not release
911
+ // a resource. Require the provider's release record AND current absence.
912
+ if (binding && events.some((event) => event.event === "released")) {
913
+ const current = attestSync(coordRoot, binding);
914
+ if (current.status === "ok" && current.resource_state === "released") continue;
915
+ }
916
+ throw new Error(`retained peer workspace ${peer.binding_id} overlaps the reclaim path`);
917
+ }
918
+ } catch (error) {
919
+ throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", (error as Error).message);
920
+ }
921
+ }
922
+
923
+ function reclaimAuthorityGuard(
924
+ coordRoot: string,
925
+ binding: WorkspaceBinding,
926
+ claim: WorkspaceClaim,
927
+ reservation: WorkspaceReclaimReservation,
928
+ recovery?: WorkspaceReclaimOptions["unknownLiveness"],
929
+ ): void {
930
+ try {
931
+ reservation.assertCurrent();
932
+ assertReclaimLiveness(coordRoot, binding, recovery);
933
+ assertReclaimOwnership(coordRoot, binding);
934
+ assertPeerWorkspaceAuthority(coordRoot, claim);
935
+ } catch (error) {
936
+ if (error instanceof WorkflowReclaimError) throw error;
937
+ throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", (error as Error).message);
938
+ }
939
+ }
940
+
941
+ async function reclaim(
942
+ coordRoot: string,
943
+ provider: WorkspaceProvider,
944
+ binding: WorkspaceBinding,
945
+ options: WorkspaceReclaimOptions,
946
+ ): Promise<WorkspaceReclaimResult> {
947
+ if (options.mode !== "salvage" && options.mode !== "discard") {
948
+ throw new Error("unsupported workspace reclaim mode");
949
+ }
950
+ if (!options.yes) return reclaimPreview(coordRoot, binding, options.mode);
951
+ if (options.unknownLiveness) validateWorkflowRecoveryAuthorization(options.unknownLiveness);
952
+
953
+ // Liveness comes before terminal-proof cleanup helpers. A live run may not
954
+ // have a proof yet, and a missing proof must never be discovered after reset.
955
+ assertReclaimLiveness(coordRoot, binding, options.unknownLiveness);
956
+ const claim = assertReclaimOwnership(coordRoot, binding);
957
+ let reservation: WorkspaceReclaimReservation;
958
+ try {
959
+ reservation = reserveWorkspaceReclaim(coordRoot, claim);
960
+ } catch (error) {
961
+ throw new WorkflowReclaimError("workflow_reclaim_lease_held", (error as Error).message);
962
+ }
963
+ try {
964
+ reclaimAuthorityGuard(coordRoot, binding, claim, reservation, options.unknownLiveness);
965
+ let releaseResume: () => void;
966
+ try {
967
+ releaseResume = acquireWorkflowResumeLease(coordRoot, binding.run_id);
968
+ } catch (error) {
969
+ throw new WorkflowReclaimError("workflow_reclaim_lease_held", (error as Error).message);
970
+ }
971
+ try {
972
+ let releaseBinding: () => void;
973
+ try {
974
+ releaseBinding = acquireLease(
975
+ coordRoot,
976
+ workspaceLockPath(coordRoot, PROVIDER_ID, binding.binding_id),
977
+ claim,
978
+ "binding",
979
+ );
980
+ } catch (error) {
981
+ throw new WorkflowReclaimError("workflow_reclaim_lease_held", (error as Error).message);
982
+ }
983
+ try {
984
+ let releaseRepository: () => void;
985
+ try {
986
+ releaseRepository = acquireRepositoryLease(coordRoot, claim);
987
+ } catch (error) {
988
+ throw new WorkflowReclaimError("workflow_reclaim_lease_held", (error as Error).message);
989
+ }
990
+ try {
991
+ assertReclaimLiveness(coordRoot, binding, options.unknownLiveness);
992
+ assertReclaimOwnership(coordRoot, binding);
993
+ const preview = reclaimPreview(coordRoot, binding, options.mode);
994
+ const audit = options.unknownLiveness
995
+ ? beginWorkflowRecovery(coordRoot, {
996
+ run_id: binding.run_id,
997
+ action: "reclaim_unknown_liveness",
998
+ actor: options.unknownLiveness.actor,
999
+ reason: options.unknownLiveness.reason,
1000
+ target_sha256: options.unknownLiveness.evidenceSha256,
1001
+ target: {
1002
+ binding_id: binding.binding_id,
1003
+ workspace_root: binding.workspace_root,
1004
+ mode: options.mode,
1005
+ liveness: preview.liveness,
1006
+ },
1007
+ })
1008
+ : undefined;
1009
+ let preparation: ReclaimPreparation | undefined;
1010
+ // Keep ordinary cleanup's terminal-evidence checks, intent and receipts.
1011
+ // Its provider callback uses the leases already held here instead of
1012
+ // trying to acquire them again. Preparation occurs only after all of
1013
+ // cleanup's path, ownership, ref and registration checks have succeeded.
1014
+ const cleanupResult = await cleanupWorkspace({
1015
+ coordRoot,
1016
+ runId: binding.run_id,
1017
+ provider: {
1018
+ ...provider,
1019
+ cleanup: async (candidate, intent) => {
1020
+ assertReclaimLiveness(coordRoot, candidate, options.unknownLiveness);
1021
+ assertReclaimOwnership(coordRoot, candidate);
1022
+ return cleanupUnderLease(coordRoot, candidate, claim, intent, {
1023
+ mode: options.mode,
1024
+ recovery: options.unknownLiveness,
1025
+ beforeEffect: () => {
1026
+ audit?.assertCurrent();
1027
+ reclaimAuthorityGuard(
1028
+ coordRoot,
1029
+ candidate,
1030
+ claim,
1031
+ reservation,
1032
+ options.unknownLiveness,
1033
+ );
1034
+ },
1035
+ beforeRefEffect: () => {
1036
+ audit?.assertCurrent();
1037
+ reservation.assertCurrent();
1038
+ assertReclaimLiveness(coordRoot, candidate, options.unknownLiveness);
1039
+ requiredClaim(coordRoot, candidate);
1040
+ assertPeerWorkspaceAuthority(coordRoot, claim);
1041
+ },
1042
+ prepared: (value) => {
1043
+ preparation = value;
1044
+ },
1045
+ });
1046
+ },
1047
+ },
1048
+ });
1049
+ if (
1050
+ !preparation &&
1051
+ cleanupResult.status !== "released" &&
1052
+ cleanupResult.status !== "already_released"
1053
+ ) {
1054
+ throw new WorkflowReclaimError(
1055
+ "workflow_reclaim_ownership_unproven",
1056
+ ("reason" in cleanupResult ? cleanupResult.reason : undefined) ??
1057
+ "workspace cleanup preconditions could not be proved",
1058
+ );
1059
+ }
1060
+ if (cleanupResult.status === "released" || cleanupResult.status === "already_released")
1061
+ audit?.complete();
1062
+ return {
1063
+ ...preview,
1064
+ action: "reclaimed",
1065
+ lease_holder: [],
1066
+ preparation: preparation ?? {
1067
+ action: "already_gone",
1068
+ detail: "the workspace is already released",
1069
+ },
1070
+ cleanup: cleanupResult,
1071
+ };
1072
+ } finally {
1073
+ releaseRepository();
1074
+ }
1075
+ } finally {
1076
+ releaseBinding();
1077
+ }
1078
+ } finally {
1079
+ releaseResume();
1080
+ }
1081
+ } finally {
1082
+ reservation.release();
1083
+ }
1084
+ }
1085
+
1086
+ function reclaimLeaseHolders(coordRoot: string, binding: WorkspaceBinding): ReclaimLeaseHolder[] {
1087
+ const paths: Array<[ReclaimLeaseHolder["scope"], string]> = [
1088
+ ["binding", workspaceLockPath(coordRoot, PROVIDER_ID, binding.binding_id)],
1089
+ ];
1090
+ if (binding.repository) {
1091
+ const key = stableDigest({ common: binding.repository.common_dir.identity });
1092
+ paths.push([
1093
+ "repository",
1094
+ join(coordRoot, ".harnery", "workspaces", PROVIDER_ID, ".leases", `repository-${key}.lock`),
1095
+ ]);
1096
+ }
1097
+ const holders: ReclaimLeaseHolder[] = [];
1098
+ try {
1099
+ const owner = JSON.parse(
1100
+ readFileSync(join(coordRoot, ".harnery", "workflows", binding.run_id, "resume.lock"), "utf8"),
1101
+ ) as Record<string, unknown>;
1102
+ holders.push({
1103
+ scope: "resume",
1104
+ pid: typeof owner.pid === "number" ? owner.pid : undefined,
1105
+ host: typeof owner.host === "string" ? owner.host : undefined,
1106
+ });
1107
+ } catch (error) {
1108
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
1109
+ holders.push({ scope: "resume", detail: "lease holder could not be read" });
1110
+ }
1111
+ }
1112
+ for (const [scope, path] of paths) {
1113
+ try {
1114
+ const owner = JSON.parse(readFileSync(join(path, "current"), "utf8")) as Record<
1115
+ string,
1116
+ unknown
1117
+ >;
1118
+ holders.push({
1119
+ scope,
1120
+ owner_id: typeof owner.owner_id === "string" ? owner.owner_id : undefined,
1121
+ pid: typeof owner.pid === "number" ? owner.pid : undefined,
1122
+ host: typeof owner.host === "string" ? owner.host : undefined,
1123
+ });
1124
+ } catch (error) {
1125
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
1126
+ holders.push({ scope, detail: "lease holder could not be read" });
1127
+ }
1128
+ }
1129
+ }
1130
+ return holders;
1131
+ }
1132
+
786
1133
  async function cleanup(
787
1134
  coordRoot: string,
788
1135
  binding: WorkspaceBinding,
@@ -799,7 +1146,17 @@ async function cleanup(
799
1146
  try {
800
1147
  const releaseRepository = acquireRepositoryLease(coordRoot, claim);
801
1148
  try {
802
- return cleanupUnderLease(coordRoot, binding, claim, intent);
1149
+ const reservation = reserveWorkspaceReclaim(coordRoot, claim);
1150
+ try {
1151
+ const authorityGuard = () => {
1152
+ reservation.assertCurrent();
1153
+ assertPeerWorkspaceAuthority(coordRoot, claim);
1154
+ };
1155
+ authorityGuard();
1156
+ return cleanupUnderLease(coordRoot, binding, claim, intent, undefined, authorityGuard);
1157
+ } finally {
1158
+ reservation.release();
1159
+ }
803
1160
  } finally {
804
1161
  releaseRepository();
805
1162
  }
@@ -813,6 +1170,14 @@ function cleanupUnderLease(
813
1170
  binding: WorkspaceBinding,
814
1171
  claim: WorkspaceClaim,
815
1172
  intent: WorkspaceCleanupIntent,
1173
+ reclaimRequest?: {
1174
+ mode: ReclaimMode;
1175
+ recovery?: WorkspaceReclaimOptions["unknownLiveness"];
1176
+ beforeEffect: () => void;
1177
+ beforeRefEffect: () => void;
1178
+ prepared: (value: ReclaimPreparation) => void;
1179
+ },
1180
+ beforeRemoval?: () => void,
816
1181
  ): WorkspaceCleanupResult {
817
1182
  const repository = binding.repository;
818
1183
  if (
@@ -857,7 +1222,7 @@ function cleanupUnderLease(
857
1222
  };
858
1223
  }
859
1224
  const liveness = readWorkflowLiveness(coordRoot, binding.run_id);
860
- if (liveness !== "inactive") {
1225
+ if (liveness !== "inactive" && !(liveness === "unknown" && reclaimRequest?.recovery)) {
861
1226
  return {
862
1227
  ...base,
863
1228
  status: "blocked",
@@ -871,11 +1236,11 @@ function cleanupUnderLease(
871
1236
  return { ...base, status: "blocked", reason: "ownership attestation failed" };
872
1237
  }
873
1238
  if (
874
- (before.repository?.dirty_paths.length ?? 0) > 0 ||
1239
+ (!reclaimRequest && (before.repository?.dirty_paths.length ?? 0) > 0) ||
875
1240
  (before.repository?.conflicts.length ?? 0) > 0 ||
876
1241
  (before.repository?.operation_in_progress.length ?? 0) > 0
877
1242
  ) {
878
- appendWorkspaceEvent(coordRoot, claim, "preserved_dirty");
1243
+ if (!reclaimRequest) appendWorkspaceEvent(coordRoot, claim, "preserved_dirty");
879
1244
  return {
880
1245
  ...base,
881
1246
  status: "preserved_dirty",
@@ -949,6 +1314,19 @@ function cleanupUnderLease(
949
1314
  };
950
1315
  }
951
1316
 
1317
+ if (reclaimRequest) {
1318
+ assertReclaimLiveness(coordRoot, binding, reclaimRequest.recovery);
1319
+ assertReclaimOwnership(coordRoot, binding);
1320
+ revalidateClaimResources(claim, root);
1321
+ reclaimRequest.prepared(
1322
+ prepareReclaim({
1323
+ worktreePath: binding.workspace_root,
1324
+ mode: reclaimRequest.mode,
1325
+ runId: binding.run_id,
1326
+ beforeEffect: reclaimRequest.beforeEffect,
1327
+ }),
1328
+ );
1329
+ }
952
1330
  appendWorkspaceEvent(coordRoot, claim, "cleanup_pending", {
953
1331
  worktree_path: binding.workspace_root,
954
1332
  gitdir: repository.gitdir,
@@ -958,6 +1336,8 @@ function cleanupUnderLease(
958
1336
  target_ref_oid: targetRef.out,
959
1337
  });
960
1338
  if (before.workspace_exists) {
1339
+ reclaimRequest?.beforeEffect();
1340
+ beforeRemoval?.();
961
1341
  revalidateClaimResources(claim, root);
962
1342
  assertContainedExisting(root, binding.workspace_root, "workspace cleanup path");
963
1343
  git(repository.source_root.realpath, ["worktree", "remove", binding.workspace_root]);
@@ -983,6 +1363,7 @@ function cleanupUnderLease(
983
1363
  return { ...base, status: "blocked", reason: "workspace ref changed during cleanup" };
984
1364
  }
985
1365
  revalidateClaimResources(claim, root);
1366
+ reclaimRequest?.beforeRefEffect();
986
1367
  const deleted = gitMaybe(repository.source_root.realpath, [
987
1368
  "update-ref",
988
1369
  "-d",