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
@@ -1 +1 @@
1
- {"version":3,"file":"leases.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/leases.ts"],"names":[],"mappings":"AAwBA,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,CAAC,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,iBAAiB,GACzB,eAAe,GACf,cAAc,GACd,iBAAiB,GACjB,oBAAoB,CAAC;AAEzB,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,OAAO,CAAC;IAC7D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IACpC,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACpD;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,mBAAmB,CAAC;IAC3B,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,OAAO,IAAI,IAAI,CAAC;CACjB;AAUD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,0BAA0B,GAAG,cAAc,CA+GvF"}
1
+ {"version":3,"file":"leases.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/leases.ts"],"names":[],"mappings":"AAwBA,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,CAAC,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,iBAAiB,GACzB,eAAe,GACf,cAAc,GACd,iBAAiB,GACjB,oBAAoB,GACpB,kBAAkB,CAAC;AAEvB,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,OAAO,CAAC;IAC7D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,8EAA8E;IAC9E,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IACpC,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACpD;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,mBAAmB,CAAC;IAC3B,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,OAAO,IAAI,IAAI,CAAC;CACjB;AAUD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,0BAA0B,GAAG,cAAc,CAgHvF"}
@@ -71,8 +71,8 @@ export function acquireNoClobberLease(input) {
71
71
  if (observed === undefined) {
72
72
  throw new Error(`lease ${input.scope} changed repeatedly during contention`);
73
73
  }
74
- if (leaseIsLive(observed.owner, now(), input.staleAfterMs)) {
75
- throw new Error(`lease ${input.scope} is held by a live or unexpired owner`);
74
+ if (leaseIsLive(observed.owner, now(), input.staleAfterMs, input.probePid)) {
75
+ throw new Error(`lease ${input.scope} is held by a live or unexpired owner, or its liveness is unknown`);
76
76
  }
77
77
  if (observed.owner.scope !== input.scope ||
78
78
  (input.validateStaleOwner !== undefined && !input.validateStaleOwner(observed.owner))) {
@@ -87,7 +87,7 @@ export function acquireNoClobberLease(input) {
87
87
  assertSameInodeAndBytes(currentPath, pinPath, observed.bytes);
88
88
  }
89
89
  catch {
90
- abortUnchangedRecoveryClaim(leaseDir, recoveryPath, pinPath, owner);
90
+ abortUnchangedRecoveryClaim(leaseDir, recoveryPath, owner);
91
91
  recoveryClaimed = false;
92
92
  throw new Error(`lease ${input.scope} recovery raced with another contender`);
93
93
  }
@@ -102,10 +102,7 @@ export function acquireNoClobberLease(input) {
102
102
  fsyncDirectory(leaseDir);
103
103
  input.onRecoveryStep?.("new_current_linked");
104
104
  removePinnedOwner(leaseDir, pinPath, observed.owner);
105
- unlinkSync(pinPath);
106
- unlinkSync(join(recoveryPath, "claim.json"));
107
- rmdirSync(recoveryPath);
108
- fsyncDirectory(leaseDir);
105
+ retireRecoveryClaim(leaseDir, recoveryPath, owner, () => input.onRecoveryStep?.("recovery_retired"));
109
106
  return {
110
107
  ...leaseHandle(leaseDir, ownerPath, ownerBytes, owner),
111
108
  recovered_owner: observed.owner,
@@ -184,7 +181,7 @@ function createRecoveryClaim(leaseDir, recoveryPath, claimant, observed) {
184
181
  }
185
182
  function reclaimAbandonedRecovery(leaseDir, recoveryPath, input, now) {
186
183
  const claim = readRecoveryClaim(recoveryPath);
187
- if (leaseIsLive(claim.claimant, now, input.staleAfterMs)) {
184
+ if (leaseIsLive(claim.claimant, now, input.staleAfterMs, input.probePid)) {
188
185
  throw new Error(`lease ${input.scope} recovery is already in progress`);
189
186
  }
190
187
  if (claim.claimant.scope !== input.scope ||
@@ -231,14 +228,27 @@ function reclaimAbandonedRecovery(leaseDir, recoveryPath, input, now) {
231
228
  rmdirSync(quarantine);
232
229
  fsyncDirectory(leaseDir);
233
230
  }
234
- function abortUnchangedRecoveryClaim(leaseDir, recoveryPath, pinPath, claimant) {
231
+ function abortUnchangedRecoveryClaim(leaseDir, recoveryPath, claimant) {
235
232
  const claim = readRecoveryClaim(recoveryPath);
236
233
  if (claim.claimant.owner_id !== claimant.owner_id) {
237
234
  throw new Error(`lease ${claimant.scope} recovery ownership changed`);
238
235
  }
239
- safeUnlink(pinPath);
240
- unlinkSync(join(recoveryPath, "claim.json"));
241
- rmdirSync(recoveryPath);
236
+ retireRecoveryClaim(leaseDir, recoveryPath, claimant);
237
+ }
238
+ /**
239
+ * Move this claimant's recovery directory to a private name before emptying
240
+ * it. Renaming a directory onto an existing empty directory replaces it, so
241
+ * emptying `recovery` in place would let a contender's claim land there and
242
+ * fail this claimant's final rmdir after it already holds the lease.
243
+ */
244
+ function retireRecoveryClaim(leaseDir, recoveryPath, claimant, onRetired) {
245
+ const retired = join(leaseDir, `recovery-retired-${claimant.owner_id}`);
246
+ renameSync(recoveryPath, retired);
247
+ fsyncDirectory(leaseDir);
248
+ onRetired?.();
249
+ safeUnlink(join(retired, "stale-pin"));
250
+ unlinkSync(join(retired, "claim.json"));
251
+ rmdirSync(retired);
242
252
  fsyncDirectory(leaseDir);
243
253
  }
244
254
  function readRecoveryClaim(recoveryPath) {
@@ -355,7 +365,9 @@ function isStringRecord(value) {
355
365
  !Array.isArray(value) &&
356
366
  Object.entries(value).every(([key, item]) => SAFE_COMPONENT.test(key) && typeof item === "string" && item.length <= 1_000));
357
367
  }
358
- function leaseIsLive(owner, now, staleAfterMs) {
368
+ function leaseIsLive(owner, now, staleAfterMs, probePid = (pid) => {
369
+ process.kill(pid, 0);
370
+ }) {
359
371
  const created = Date.parse(owner.created_at);
360
372
  const expires = Date.parse(owner.expires_at);
361
373
  if (!Number.isFinite(created) ||
@@ -365,14 +377,16 @@ function leaseIsLive(owner, now, staleAfterMs) {
365
377
  now < expires) {
366
378
  return true;
367
379
  }
380
+ // Expiry only makes a same-host owner eligible for a process check. Neither
381
+ // a remote owner nor an unreadable probe proves that its work has stopped.
368
382
  if (owner.host !== hostname())
369
- return false;
383
+ return true;
370
384
  try {
371
- process.kill(owner.pid, 0);
385
+ probePid(owner.pid);
372
386
  return true;
373
387
  }
374
388
  catch (error) {
375
- return error.code === "EPERM";
389
+ return error.code !== "ESRCH";
376
390
  }
377
391
  }
378
392
  function assertExactOwner(currentPath, ownerPath, expectedBytes, owner) {
@@ -1,8 +1,12 @@
1
- import type { WorkspaceProbeInput, WorkspaceProbeResult, WorkspaceProvider } from "./types.js";
1
+ import { type WorkspaceReclaimOptions, type WorkspaceReclaimResult } from "./reclaim.js";
2
+ import type { WorkspaceBinding, WorkspaceProbeInput, WorkspaceProbeResult, WorkspaceProvider } from "./types.js";
2
3
  export interface LocalGitWorktreeProviderOptions {
3
4
  coordRoot: string;
4
5
  descriptorPathsSupported?: () => boolean;
5
6
  }
6
- export declare function createLocalGitWorktreeProvider(options: LocalGitWorktreeProviderOptions): WorkspaceProvider;
7
+ export interface LocalGitWorktreeProvider extends WorkspaceProvider {
8
+ reclaim(binding: WorkspaceBinding, options: WorkspaceReclaimOptions): Promise<WorkspaceReclaimResult>;
9
+ }
10
+ export declare function createLocalGitWorktreeProvider(options: LocalGitWorktreeProviderOptions): LocalGitWorktreeProvider;
7
11
  export declare function probe(input: WorkspaceProbeInput, descriptorPathsSupported?: () => boolean): Promise<WorkspaceProbeResult>;
8
12
  //# sourceMappingURL=local-git.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"local-git.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/local-git.ts"],"names":[],"mappings":"AAqDA,OAAO,KAAK,EAgBV,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAUpB,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,wBAAwB,CAAC,EAAE,MAAM,OAAO,CAAC;CAC1C;AAED,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,+BAA+B,GACvC,iBAAiB,CAgBnB;AAED,wBAAsB,KAAK,CACzB,KAAK,EAAE,mBAAmB,EAC1B,wBAAwB,GAAE,MAAM,OAAwC,GACvE,OAAO,CAAC,oBAAoB,CAAC,CA4E/B"}
1
+ {"version":3,"file":"local-git.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/local-git.ts"],"names":[],"mappings":"AA8CA,OAAO,EAML,KAAK,uBAAuB,EAE5B,KAAK,sBAAsB,EAC5B,MAAM,cAAc,CAAC;AAiBtB,OAAO,KAAK,EAYV,gBAAgB,EAIhB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAUpB,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,wBAAwB,CAAC,EAAE,MAAM,OAAO,CAAC;CAC1C;AAED,MAAM,WAAW,wBAAyB,SAAQ,iBAAiB;IACjE,OAAO,CACL,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACpC;AAED,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,+BAA+B,GACvC,wBAAwB,CAkB1B;AAED,wBAAsB,KAAK,CACzB,KAAK,EAAE,mBAAmB,EAC1B,wBAAwB,GAAE,MAAM,OAAwC,GACvE,OAAO,CAAC,oBAAoB,CAAC,CA4E/B"}
@@ -2,12 +2,17 @@ import { randomBytes } from "node:crypto";
2
2
  import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync, } from "node:fs";
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { stateDirMode, stateFileMode } from "../../storage/modes.js";
5
+ import { beginWorkflowRecovery, validateWorkflowRecoveryAuthorization } from "../recovery-audit.js";
6
+ import { acquireWorkflowResumeLease } from "../run-state.js";
5
7
  import { WorkspaceAttestationError } from "./attestation-error.js";
8
+ import { cleanupWorkspace } from "./cleanup.js";
6
9
  import { git, gitMaybe, gitMaybeWithDirectoryDescriptor, gitOperations, isAncestor, nulEntries, overwriteRiskPaths, parseWorktreeInventory, worktreeInventory, } from "./git.js";
7
10
  import { acquireNoClobberLease } from "./leases.js";
11
+ import { claimOverlapsWorkspace, reserveWorkspaceReclaim, } from "./ownership.js";
8
12
  import { assertContainedExisting, assertPathIdentity, candidateUnderRoot, containsPath, createContainedDirectories, descriptorBackedPathsSupported, descriptorPath, filesystemIdentity, openContainedDirectory, sameFilesystemIdentity, validateConfiguredRoot, } from "./paths.js";
9
- import { readWorkflowLiveness } from "./recovery.js";
10
- import { appendWorkspaceEvent, fileSha256, readWorkflowSupplement, readWorkspaceBinding, readWorkspaceClaim, readWorkspaceEvents, stableDigest, workspaceClaimPath, workspaceLockPath, writeWorkspaceBinding, writeWorkspaceClaim, } from "./state.js";
13
+ import { prepareReclaim, WorkflowReclaimError, } from "./reclaim.js";
14
+ import { hasActiveWorkflowEvidence, readWorkflowLiveness } from "./recovery.js";
15
+ import { appendWorkspaceEvent, fileSha256, readWorkflowSupplement, readWorkspaceBinding, readWorkspaceClaim, readWorkspaceClaims, readWorkspaceEvents, stableDigest, workspaceClaimPath, workspaceLockPath, writeWorkspaceBinding, writeWorkspaceClaim, } from "./state.js";
11
16
  import { isWorkspaceAttestation } from "./validate.js";
12
17
  const PROVIDER_ID = "local-git-worktree";
13
18
  const PROVIDER_VERSION = "1";
@@ -17,7 +22,7 @@ const OBJECT_FORMAT = /^[a-f0-9]{40,64}$/;
17
22
  const REF_FORMAT = /^refs\/heads\/[A-Za-z0-9][A-Za-z0-9._/-]{0,180}$/;
18
23
  export function createLocalGitWorktreeProvider(options) {
19
24
  const coordRoot = resolve(options.coordRoot);
20
- return {
25
+ const provider = {
21
26
  probe: (input) => probe(input, options.descriptorPathsSupported),
22
27
  allocate: (request) => allocate(coordRoot, request),
23
28
  readBinding: async (binding) => {
@@ -30,7 +35,9 @@ export function createLocalGitWorktreeProvider(options) {
30
35
  previewIntegration: (input) => previewIntegration(coordRoot, input),
31
36
  applyAuthorizedIntegration: (input) => applyAuthorizedIntegration(coordRoot, input),
32
37
  cleanup: (binding, intent) => cleanup(coordRoot, binding, intent),
38
+ reclaim: (binding, reclaimOptions) => reclaim(coordRoot, provider, binding, reclaimOptions),
33
39
  };
40
+ return provider;
34
41
  }
35
42
  export async function probe(input, descriptorPathsSupported = descriptorBackedPathsSupported) {
36
43
  const unsupported = [];
@@ -585,6 +592,293 @@ function cancelUnderLease(coordRoot, binding, claim) {
585
592
  appendWorkspaceEvent(coordRoot, claim, "cancellation_outcome", { status: "cancelled" });
586
593
  return { ...base, status: "cancelled" };
587
594
  }
595
+ function reclaimPreview(coordRoot, binding, mode) {
596
+ const preview = {
597
+ action: "preview",
598
+ run_id: binding.run_id,
599
+ mode,
600
+ liveness: readWorkflowLiveness(coordRoot, binding.run_id),
601
+ evidence_sha256: reclaimEvidenceDigest(coordRoot, binding),
602
+ ownership: "unproven",
603
+ dirty_paths: null,
604
+ lease_holder: reclaimLeaseHolders(coordRoot, binding),
605
+ };
606
+ try {
607
+ const current = attestSync(coordRoot, binding);
608
+ if (current.status === "ok") {
609
+ preview.ownership = "ok";
610
+ preview.dirty_paths = [...(current.repository?.dirty_paths ?? [])];
611
+ }
612
+ else {
613
+ preview.ownership_detail =
614
+ current.provider_drift.join("; ") || "ownership attestation failed";
615
+ }
616
+ }
617
+ catch (error) {
618
+ preview.ownership_detail = error.message;
619
+ }
620
+ return preview;
621
+ }
622
+ function reclaimEvidenceDigest(coordRoot, binding) {
623
+ const evidence = (name) => {
624
+ try {
625
+ return readFileSync(join(coordRoot, ".harnery", "workflows", binding.run_id, name), "utf8");
626
+ }
627
+ catch (error) {
628
+ if (error.code === "ENOENT")
629
+ return null;
630
+ throw error;
631
+ }
632
+ };
633
+ return stableDigest({
634
+ binding,
635
+ proof: evidence("proof.json"),
636
+ transcript: evidence("transcript.jsonl"),
637
+ });
638
+ }
639
+ function assertReclaimLiveness(coordRoot, binding, recovery) {
640
+ const liveness = readWorkflowLiveness(coordRoot, binding.run_id);
641
+ if (liveness === "active" || (recovery && hasActiveWorkflowEvidence(coordRoot, binding.run_id))) {
642
+ throw new WorkflowReclaimError("workflow_reclaim_run_live", `run ${binding.run_id} has active evidence; its workspace was left untouched`);
643
+ }
644
+ if (recovery && reclaimEvidenceDigest(coordRoot, binding) !== recovery.evidenceSha256) {
645
+ throw new WorkflowReclaimError("workflow_reclaim_recovery_mismatch", "workflow reclaim requires the exact current preview evidence digest");
646
+ }
647
+ if (liveness === "inactive")
648
+ return;
649
+ if (liveness === "unknown" && recovery)
650
+ return;
651
+ throw new WorkflowReclaimError("workflow_reclaim_liveness_unknown", `run ${binding.run_id} has missing, corrupt, or ambiguous liveness evidence; its workspace was left untouched`);
652
+ }
653
+ function assertReclaimOwnership(coordRoot, binding) {
654
+ try {
655
+ const claim = requiredClaim(coordRoot, binding);
656
+ const current = attestSync(coordRoot, binding);
657
+ if (current.status !== "ok") {
658
+ throw new Error(current.provider_drift.join("; ") || "ownership attestation failed");
659
+ }
660
+ return claim;
661
+ }
662
+ catch (error) {
663
+ throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", error.message);
664
+ }
665
+ }
666
+ function assertPeerWorkspaceAuthority(coordRoot, claim) {
667
+ try {
668
+ for (const peer of readWorkspaceClaims(coordRoot, PROVIDER_ID)) {
669
+ if (peer.binding_id === claim.binding_id)
670
+ continue;
671
+ const binding = readWorkspaceBinding(coordRoot, PROVIDER_ID, peer.binding_id);
672
+ const events = readWorkspaceEvents(coordRoot, PROVIDER_ID, peer.binding_id);
673
+ if (binding)
674
+ requiredClaim(coordRoot, binding);
675
+ else if (events.some((event) => event.event === "bound")) {
676
+ throw new Error("retained peer binding authority is missing");
677
+ }
678
+ if (!claimOverlapsWorkspace(peer, claim.workspace_root))
679
+ continue;
680
+ // Process termination, archival and an expired reservation do not release
681
+ // a resource. Require the provider's release record AND current absence.
682
+ if (binding && events.some((event) => event.event === "released")) {
683
+ const current = attestSync(coordRoot, binding);
684
+ if (current.status === "ok" && current.resource_state === "released")
685
+ continue;
686
+ }
687
+ throw new Error(`retained peer workspace ${peer.binding_id} overlaps the reclaim path`);
688
+ }
689
+ }
690
+ catch (error) {
691
+ throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", error.message);
692
+ }
693
+ }
694
+ function reclaimAuthorityGuard(coordRoot, binding, claim, reservation, recovery) {
695
+ try {
696
+ reservation.assertCurrent();
697
+ assertReclaimLiveness(coordRoot, binding, recovery);
698
+ assertReclaimOwnership(coordRoot, binding);
699
+ assertPeerWorkspaceAuthority(coordRoot, claim);
700
+ }
701
+ catch (error) {
702
+ if (error instanceof WorkflowReclaimError)
703
+ throw error;
704
+ throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", error.message);
705
+ }
706
+ }
707
+ async function reclaim(coordRoot, provider, binding, options) {
708
+ if (options.mode !== "salvage" && options.mode !== "discard") {
709
+ throw new Error("unsupported workspace reclaim mode");
710
+ }
711
+ if (!options.yes)
712
+ return reclaimPreview(coordRoot, binding, options.mode);
713
+ if (options.unknownLiveness)
714
+ validateWorkflowRecoveryAuthorization(options.unknownLiveness);
715
+ // Liveness comes before terminal-proof cleanup helpers. A live run may not
716
+ // have a proof yet, and a missing proof must never be discovered after reset.
717
+ assertReclaimLiveness(coordRoot, binding, options.unknownLiveness);
718
+ const claim = assertReclaimOwnership(coordRoot, binding);
719
+ let reservation;
720
+ try {
721
+ reservation = reserveWorkspaceReclaim(coordRoot, claim);
722
+ }
723
+ catch (error) {
724
+ throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
725
+ }
726
+ try {
727
+ reclaimAuthorityGuard(coordRoot, binding, claim, reservation, options.unknownLiveness);
728
+ let releaseResume;
729
+ try {
730
+ releaseResume = acquireWorkflowResumeLease(coordRoot, binding.run_id);
731
+ }
732
+ catch (error) {
733
+ throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
734
+ }
735
+ try {
736
+ let releaseBinding;
737
+ try {
738
+ releaseBinding = acquireLease(coordRoot, workspaceLockPath(coordRoot, PROVIDER_ID, binding.binding_id), claim, "binding");
739
+ }
740
+ catch (error) {
741
+ throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
742
+ }
743
+ try {
744
+ let releaseRepository;
745
+ try {
746
+ releaseRepository = acquireRepositoryLease(coordRoot, claim);
747
+ }
748
+ catch (error) {
749
+ throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
750
+ }
751
+ try {
752
+ assertReclaimLiveness(coordRoot, binding, options.unknownLiveness);
753
+ assertReclaimOwnership(coordRoot, binding);
754
+ const preview = reclaimPreview(coordRoot, binding, options.mode);
755
+ const audit = options.unknownLiveness
756
+ ? beginWorkflowRecovery(coordRoot, {
757
+ run_id: binding.run_id,
758
+ action: "reclaim_unknown_liveness",
759
+ actor: options.unknownLiveness.actor,
760
+ reason: options.unknownLiveness.reason,
761
+ target_sha256: options.unknownLiveness.evidenceSha256,
762
+ target: {
763
+ binding_id: binding.binding_id,
764
+ workspace_root: binding.workspace_root,
765
+ mode: options.mode,
766
+ liveness: preview.liveness,
767
+ },
768
+ })
769
+ : undefined;
770
+ let preparation;
771
+ // Keep ordinary cleanup's terminal-evidence checks, intent and receipts.
772
+ // Its provider callback uses the leases already held here instead of
773
+ // trying to acquire them again. Preparation occurs only after all of
774
+ // cleanup's path, ownership, ref and registration checks have succeeded.
775
+ const cleanupResult = await cleanupWorkspace({
776
+ coordRoot,
777
+ runId: binding.run_id,
778
+ provider: {
779
+ ...provider,
780
+ cleanup: async (candidate, intent) => {
781
+ assertReclaimLiveness(coordRoot, candidate, options.unknownLiveness);
782
+ assertReclaimOwnership(coordRoot, candidate);
783
+ return cleanupUnderLease(coordRoot, candidate, claim, intent, {
784
+ mode: options.mode,
785
+ recovery: options.unknownLiveness,
786
+ beforeEffect: () => {
787
+ audit?.assertCurrent();
788
+ reclaimAuthorityGuard(coordRoot, candidate, claim, reservation, options.unknownLiveness);
789
+ },
790
+ beforeRefEffect: () => {
791
+ audit?.assertCurrent();
792
+ reservation.assertCurrent();
793
+ assertReclaimLiveness(coordRoot, candidate, options.unknownLiveness);
794
+ requiredClaim(coordRoot, candidate);
795
+ assertPeerWorkspaceAuthority(coordRoot, claim);
796
+ },
797
+ prepared: (value) => {
798
+ preparation = value;
799
+ },
800
+ });
801
+ },
802
+ },
803
+ });
804
+ if (!preparation &&
805
+ cleanupResult.status !== "released" &&
806
+ cleanupResult.status !== "already_released") {
807
+ throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", ("reason" in cleanupResult ? cleanupResult.reason : undefined) ??
808
+ "workspace cleanup preconditions could not be proved");
809
+ }
810
+ if (cleanupResult.status === "released" || cleanupResult.status === "already_released")
811
+ audit?.complete();
812
+ return {
813
+ ...preview,
814
+ action: "reclaimed",
815
+ lease_holder: [],
816
+ preparation: preparation ?? {
817
+ action: "already_gone",
818
+ detail: "the workspace is already released",
819
+ },
820
+ cleanup: cleanupResult,
821
+ };
822
+ }
823
+ finally {
824
+ releaseRepository();
825
+ }
826
+ }
827
+ finally {
828
+ releaseBinding();
829
+ }
830
+ }
831
+ finally {
832
+ releaseResume();
833
+ }
834
+ }
835
+ finally {
836
+ reservation.release();
837
+ }
838
+ }
839
+ function reclaimLeaseHolders(coordRoot, binding) {
840
+ const paths = [
841
+ ["binding", workspaceLockPath(coordRoot, PROVIDER_ID, binding.binding_id)],
842
+ ];
843
+ if (binding.repository) {
844
+ const key = stableDigest({ common: binding.repository.common_dir.identity });
845
+ paths.push([
846
+ "repository",
847
+ join(coordRoot, ".harnery", "workspaces", PROVIDER_ID, ".leases", `repository-${key}.lock`),
848
+ ]);
849
+ }
850
+ const holders = [];
851
+ try {
852
+ const owner = JSON.parse(readFileSync(join(coordRoot, ".harnery", "workflows", binding.run_id, "resume.lock"), "utf8"));
853
+ holders.push({
854
+ scope: "resume",
855
+ pid: typeof owner.pid === "number" ? owner.pid : undefined,
856
+ host: typeof owner.host === "string" ? owner.host : undefined,
857
+ });
858
+ }
859
+ catch (error) {
860
+ if (error.code !== "ENOENT") {
861
+ holders.push({ scope: "resume", detail: "lease holder could not be read" });
862
+ }
863
+ }
864
+ for (const [scope, path] of paths) {
865
+ try {
866
+ const owner = JSON.parse(readFileSync(join(path, "current"), "utf8"));
867
+ holders.push({
868
+ scope,
869
+ owner_id: typeof owner.owner_id === "string" ? owner.owner_id : undefined,
870
+ pid: typeof owner.pid === "number" ? owner.pid : undefined,
871
+ host: typeof owner.host === "string" ? owner.host : undefined,
872
+ });
873
+ }
874
+ catch (error) {
875
+ if (error.code !== "ENOENT") {
876
+ holders.push({ scope, detail: "lease holder could not be read" });
877
+ }
878
+ }
879
+ }
880
+ return holders;
881
+ }
588
882
  async function cleanup(coordRoot, binding, intent) {
589
883
  if (intent.mode !== "normal")
590
884
  throw new Error("unsupported workspace cleanup mode");
@@ -593,7 +887,18 @@ async function cleanup(coordRoot, binding, intent) {
593
887
  try {
594
888
  const releaseRepository = acquireRepositoryLease(coordRoot, claim);
595
889
  try {
596
- return cleanupUnderLease(coordRoot, binding, claim, intent);
890
+ const reservation = reserveWorkspaceReclaim(coordRoot, claim);
891
+ try {
892
+ const authorityGuard = () => {
893
+ reservation.assertCurrent();
894
+ assertPeerWorkspaceAuthority(coordRoot, claim);
895
+ };
896
+ authorityGuard();
897
+ return cleanupUnderLease(coordRoot, binding, claim, intent, undefined, authorityGuard);
898
+ }
899
+ finally {
900
+ reservation.release();
901
+ }
597
902
  }
598
903
  finally {
599
904
  releaseRepository();
@@ -603,7 +908,7 @@ async function cleanup(coordRoot, binding, intent) {
603
908
  releaseBinding();
604
909
  }
605
910
  }
606
- function cleanupUnderLease(coordRoot, binding, claim, intent) {
911
+ function cleanupUnderLease(coordRoot, binding, claim, intent, reclaimRequest, beforeRemoval) {
607
912
  const repository = binding.repository;
608
913
  if (!repository ||
609
914
  intent.schema_version !== 1 ||
@@ -641,7 +946,7 @@ function cleanupUnderLease(coordRoot, binding, claim, intent) {
641
946
  };
642
947
  }
643
948
  const liveness = readWorkflowLiveness(coordRoot, binding.run_id);
644
- if (liveness !== "inactive") {
949
+ if (liveness !== "inactive" && !(liveness === "unknown" && reclaimRequest?.recovery)) {
645
950
  return {
646
951
  ...base,
647
952
  status: "blocked",
@@ -653,10 +958,11 @@ function cleanupUnderLease(coordRoot, binding, claim, intent) {
653
958
  if (before.workspace_exists && before.status !== "ok") {
654
959
  return { ...base, status: "blocked", reason: "ownership attestation failed" };
655
960
  }
656
- if ((before.repository?.dirty_paths.length ?? 0) > 0 ||
961
+ if ((!reclaimRequest && (before.repository?.dirty_paths.length ?? 0) > 0) ||
657
962
  (before.repository?.conflicts.length ?? 0) > 0 ||
658
963
  (before.repository?.operation_in_progress.length ?? 0) > 0) {
659
- appendWorkspaceEvent(coordRoot, claim, "preserved_dirty");
964
+ if (!reclaimRequest)
965
+ appendWorkspaceEvent(coordRoot, claim, "preserved_dirty");
660
966
  return {
661
967
  ...base,
662
968
  status: "preserved_dirty",
@@ -710,6 +1016,17 @@ function cleanupUnderLease(coordRoot, binding, claim, intent) {
710
1016
  reason: "missing worktree still has ambiguous Git registration",
711
1017
  };
712
1018
  }
1019
+ if (reclaimRequest) {
1020
+ assertReclaimLiveness(coordRoot, binding, reclaimRequest.recovery);
1021
+ assertReclaimOwnership(coordRoot, binding);
1022
+ revalidateClaimResources(claim, root);
1023
+ reclaimRequest.prepared(prepareReclaim({
1024
+ worktreePath: binding.workspace_root,
1025
+ mode: reclaimRequest.mode,
1026
+ runId: binding.run_id,
1027
+ beforeEffect: reclaimRequest.beforeEffect,
1028
+ }));
1029
+ }
713
1030
  appendWorkspaceEvent(coordRoot, claim, "cleanup_pending", {
714
1031
  worktree_path: binding.workspace_root,
715
1032
  gitdir: repository.gitdir,
@@ -719,6 +1036,8 @@ function cleanupUnderLease(coordRoot, binding, claim, intent) {
719
1036
  target_ref_oid: targetRef.out,
720
1037
  });
721
1038
  if (before.workspace_exists) {
1039
+ reclaimRequest?.beforeEffect();
1040
+ beforeRemoval?.();
722
1041
  revalidateClaimResources(claim, root);
723
1042
  assertContainedExisting(root, binding.workspace_root, "workspace cleanup path");
724
1043
  git(repository.source_root.realpath, ["worktree", "remove", binding.workspace_root]);
@@ -738,6 +1057,7 @@ function cleanupUnderLease(coordRoot, binding, claim, intent) {
738
1057
  return { ...base, status: "blocked", reason: "workspace ref changed during cleanup" };
739
1058
  }
740
1059
  revalidateClaimResources(claim, root);
1060
+ reclaimRequest?.beforeRefEffect();
741
1061
  const deleted = gitMaybe(repository.source_root.realpath, [
742
1062
  "update-ref",
743
1063
  "-d",
@@ -0,0 +1,11 @@
1
+ import type { WorkspaceClaim } from "./state.js";
2
+ export interface WorkspaceReclaimReservation {
3
+ assertCurrent(): void;
4
+ release(): void;
5
+ }
6
+ /** The immutable claim is the authority; the lease reserves its destructive path. */
7
+ export declare function reserveWorkspaceReclaim(coordRoot: string, claim: WorkspaceClaim): WorkspaceReclaimReservation;
8
+ export declare function publishWorkspaceClaim<T>(coordRoot: string, claim: WorkspaceClaim, publish: () => T): T;
9
+ export declare function assertClaimAuthority(claim: WorkspaceClaim, providerId: string, id: string): void;
10
+ export declare function claimOverlapsWorkspace(claim: WorkspaceClaim, workspaceRoot: string): boolean;
11
+ //# sourceMappingURL=ownership.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ownership.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/ownership.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AA8CjD,MAAM,WAAW,2BAA2B;IAC1C,aAAa,IAAI,IAAI,CAAC;IACtB,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,qFAAqF;AACrF,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,cAAc,GACpB,2BAA2B,CAgC7B;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EACrC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,MAAM,CAAC,GACf,CAAC,CA8DH;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAwBhG;AAmCD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAU5F"}