@north-light/crouter 0.3.197 → 0.3.199

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 (339) hide show
  1. package/dist/api/client.d.ts +19 -19
  2. package/dist/api/client.js +29 -37
  3. package/dist/api/dto/broker-ops.d.ts +3 -1
  4. package/dist/api/dto/broker.d.ts +13 -2
  5. package/dist/api/dto/broker.js +4 -4
  6. package/dist/api/dto/human.d.ts +6 -5
  7. package/dist/api/dto/inbox.d.ts +126 -33
  8. package/dist/api/dto/nodes.d.ts +8 -2
  9. package/dist/api/dto/profiles.d.ts +23 -0
  10. package/dist/api/dto/profiles.js +2 -2
  11. package/dist/api/dto/worktree.d.ts +3 -5
  12. package/dist/api/routes.d.ts +3 -5
  13. package/dist/api/routes.js +4 -6
  14. package/dist/builtin-memory/00-runtime-base.md +8 -8
  15. package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
  16. package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -3
  17. package/dist/builtin-memory/04-orchestration-kernel.md +6 -6
  18. package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
  19. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +2 -2
  20. package/dist/builtin-memory/05-kinds/review/00-base.md +2 -2
  21. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
  22. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -1
  23. package/dist/builtin-memory/insights/capture.md +2 -2
  24. package/dist/builtin-memory/insights/init.md +3 -3
  25. package/dist/builtin-memory/internal/INDEX.md +1 -1
  26. package/dist/builtin-memory/internal/memory-loading.md +1 -1
  27. package/dist/builtin-memory/internal/nodes-and-canvas.md +3 -3
  28. package/dist/builtin-memory/internal/plugins.md +59 -2
  29. package/dist/builtin-memory/internal/storage-tiers.md +3 -1
  30. package/dist/builtin-memory/spec/roadmap.md +2 -2
  31. package/dist/builtin-memory/testing.md +1 -1
  32. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
  33. package/dist/clients/attach/chrome/bash-jobs.js +3 -3
  34. package/dist/clients/attach/render/chat-view.d.ts +29 -9
  35. package/dist/clients/attach/render/chat-view.js +64 -35
  36. package/dist/clients/attach/render/markdown-source.js +0 -6
  37. package/dist/clients/attach/render/page-block.d.ts +34 -0
  38. package/dist/clients/attach/render/page-block.js +226 -0
  39. package/dist/clients/attach/render/tool-calls.js +16 -2
  40. package/dist/clients/attach/session/whip.d.ts +2 -0
  41. package/dist/clients/attach/session/whip.js +2 -0
  42. package/dist/clients/attach/viewer.js +1419 -982
  43. package/dist/clients/conversation/projection.js +18 -3
  44. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
  45. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -14
  46. package/dist/clients/inbox/controller.js +10 -16
  47. package/dist/clients/inbox/page-adapter.d.ts +3 -4
  48. package/dist/clients/inbox/page-adapter.js +3 -4
  49. package/dist/clients/inbox/resolve.d.ts +1 -1
  50. package/dist/clients/inbox/resolve.js +3 -3
  51. package/dist/clients/inbox/review/document-surface.js +3 -1
  52. package/dist/clients/inbox/review/state.d.ts +2 -1
  53. package/dist/clients/inbox/review/state.js +5 -16
  54. package/dist/clients/inbox/tui/input.js +37 -19
  55. package/dist/clients/inbox/tui/panel.js +5 -31
  56. package/dist/clients/inbox/tui/render.d.ts +11 -0
  57. package/dist/clients/inbox/tui/render.js +101 -36
  58. package/dist/clients/inbox/tui/slots.d.ts +4 -4
  59. package/dist/clients/inbox/tui/slots.js +19 -44
  60. package/dist/clients/inbox/tui/types.d.ts +6 -10
  61. package/dist/clients/inbox/tui.js +2 -2
  62. package/dist/commands/__tests__/human.test.js +118 -24
  63. package/dist/commands/__tests__/node-message.test.js +14 -0
  64. package/dist/commands/attention.js +4 -4
  65. package/dist/commands/canvas-browse.js +1 -1
  66. package/dist/commands/canvas-snapshot.js +1 -1
  67. package/dist/commands/canvas.js +1 -1
  68. package/dist/commands/dashboard.js +1 -1
  69. package/dist/commands/human/{doc.d.ts → components.d.ts} +1 -1
  70. package/dist/commands/human/components.js +77 -0
  71. package/dist/commands/human/feedback.d.ts +2 -0
  72. package/dist/commands/human/feedback.js +108 -0
  73. package/dist/commands/human/prompts.d.ts +17 -8
  74. package/dist/commands/human/prompts.js +217 -226
  75. package/dist/commands/human/queue.js +57 -88
  76. package/dist/commands/human/shared.d.ts +4 -15
  77. package/dist/commands/human/shared.js +19 -61
  78. package/dist/commands/human.js +32 -19
  79. package/dist/commands/memory/origin.js +2 -2
  80. package/dist/commands/node/bash.js +4 -4
  81. package/dist/commands/node/inspect.js +3 -3
  82. package/dist/commands/node/lifecycle.js +1 -1
  83. package/dist/commands/node/message.js +2 -2
  84. package/dist/commands/node-worktree.js +8 -8
  85. package/dist/commands/pkg/browse.js +1 -1
  86. package/dist/commands/pkg/plugin-manage.js +129 -10
  87. package/dist/commands/profile/delete.js +41 -15
  88. package/dist/commands/profile.js +1 -1
  89. package/dist/commands/push.js +1 -1
  90. package/dist/commands/surface/node/focus.js +29 -14
  91. package/dist/commands/surface-edit.js +3 -3
  92. package/dist/commands/surface-inbox.d.ts +2 -0
  93. package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
  94. package/dist/commands/surface.js +2 -1
  95. package/dist/commands/sys/doctor.js +60 -3
  96. package/dist/commands/sys/feedback.js +79 -25
  97. package/dist/commands/sys/settings.js +1 -1
  98. package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
  99. package/dist/core/__tests__/daemon-boot.test.js +7 -4
  100. package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
  101. package/dist/core/__tests__/extension-abort.test.js +5 -2
  102. package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
  103. package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
  104. package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
  105. package/dist/core/__tests__/human-deliver.test.js +41 -45
  106. package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
  107. package/dist/core/__tests__/plugin-page-components.test.js +157 -0
  108. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
  109. package/dist/core/__tests__/seam/dormancy-release.test.js +103 -4
  110. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
  111. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -16
  112. package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
  113. package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
  114. package/dist/core/__tests__/serial/worktree.test.js +24 -198
  115. package/dist/core/__tests__/session-cycles.test.js +22 -0
  116. package/dist/core/__tests__/stop-guard.test.js +4 -4
  117. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
  118. package/dist/core/bash-jobs.d.ts +6 -0
  119. package/dist/core/bash-jobs.js +10 -0
  120. package/dist/core/canvas/__tests__/attention.test.js +8 -5
  121. package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
  122. package/dist/core/canvas/attention.js +8 -6
  123. package/dist/core/canvas/browse/app.d.ts +1 -1
  124. package/dist/core/canvas/browse/app.js +21 -3
  125. package/dist/core/canvas/browse/model.d.ts +8 -2
  126. package/dist/core/canvas/browse/model.js +11 -4
  127. package/dist/core/canvas/browse/render.d.ts +4 -0
  128. package/dist/core/canvas/browse/render.js +5 -1
  129. package/dist/core/canvas/canvas.d.ts +33 -0
  130. package/dist/core/canvas/canvas.js +92 -0
  131. package/dist/core/canvas/crons.d.ts +9 -0
  132. package/dist/core/canvas/crons.js +51 -0
  133. package/dist/core/canvas/node-order.d.ts +22 -1
  134. package/dist/core/canvas/node-order.js +26 -1
  135. package/dist/core/canvas/render-source.d.ts +5 -0
  136. package/dist/core/canvas/render-source.js +1 -0
  137. package/dist/core/canvas/types.d.ts +9 -3
  138. package/dist/core/command-plugins/bundle.d.ts +6 -1
  139. package/dist/core/command-plugins/bundle.js +26 -13
  140. package/dist/core/command.js +8 -12
  141. package/dist/core/config.d.ts +47 -1
  142. package/dist/core/config.js +138 -4
  143. package/dist/core/help.d.ts +3 -0
  144. package/dist/core/help.js +1 -1
  145. package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
  146. package/dist/core/human/__tests__/page-catalog.test.js +20 -0
  147. package/dist/core/human/__tests__/page-tickets.test.js +68 -34
  148. package/dist/core/human/__tests__/page.test.js +60 -119
  149. package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -16
  150. package/dist/core/human/answer-text.d.ts +3 -0
  151. package/dist/core/human/answer-text.js +98 -0
  152. package/dist/core/human/answer.d.ts +53 -0
  153. package/dist/core/human/answer.js +119 -0
  154. package/dist/core/human/component-docs.d.ts +18 -0
  155. package/dist/core/human/component-docs.js +434 -0
  156. package/dist/core/human/convention.d.ts +17 -3
  157. package/dist/core/human/convention.js +36 -5
  158. package/dist/core/human/feedback-companion.d.ts +20 -0
  159. package/dist/core/human/feedback-companion.js +98 -0
  160. package/dist/core/human/feedback.d.ts +65 -0
  161. package/dist/core/human/feedback.js +116 -0
  162. package/dist/core/human/page-catalog.d.ts +56 -5
  163. package/dist/core/human/page-catalog.js +151 -22
  164. package/dist/core/human/page-errors.d.ts +4 -0
  165. package/dist/core/human/page-errors.js +7 -0
  166. package/dist/core/human/page-eval.d.ts +20 -0
  167. package/dist/core/human/page-eval.js +123 -0
  168. package/dist/core/human/page-schema.d.ts +48 -116
  169. package/dist/core/human/page-schema.js +100 -72
  170. package/dist/core/human/page-synth.d.ts +2 -4
  171. package/dist/core/human/page-synth.js +20 -7
  172. package/dist/core/human/page.d.ts +13 -21
  173. package/dist/core/human/page.js +147 -391
  174. package/dist/core/human/review-schema.d.ts +1 -0
  175. package/dist/core/human/review-schema.js +1 -1
  176. package/dist/core/human/root.d.ts +6 -2
  177. package/dist/core/human/root.js +12 -4
  178. package/dist/core/human/scan.d.ts +3 -3
  179. package/dist/core/human/scan.js +21 -21
  180. package/dist/core/human/summary.d.ts +2 -6
  181. package/dist/core/human/summary.js +3 -59
  182. package/dist/core/human/tickets.d.ts +22 -15
  183. package/dist/core/human/tickets.js +90 -28
  184. package/dist/core/human/types.d.ts +14 -2
  185. package/dist/core/keybindings/catalog.d.ts +2 -2
  186. package/dist/core/keybindings/catalog.js +1 -0
  187. package/dist/core/preview-registry.js +8 -10
  188. package/dist/core/profiles/default-binding.d.ts +4 -0
  189. package/dist/core/profiles/default-binding.js +24 -0
  190. package/dist/core/profiles/deletion-reservation.d.ts +8 -0
  191. package/dist/core/profiles/deletion-reservation.js +38 -0
  192. package/dist/core/profiles/manifest.d.ts +3 -0
  193. package/dist/core/profiles/manifest.js +12 -0
  194. package/dist/core/profiles/select.js +2 -2
  195. package/dist/core/review/realize.js +12 -4
  196. package/dist/core/review/store.d.ts +6 -0
  197. package/dist/core/review/store.js +31 -0
  198. package/dist/core/runtime/bearings.d.ts +0 -4
  199. package/dist/core/runtime/bearings.js +4 -62
  200. package/dist/core/runtime/bin-contributions.d.ts +44 -0
  201. package/dist/core/runtime/bin-contributions.js +186 -0
  202. package/dist/core/runtime/boot-root.js +6 -1
  203. package/dist/core/runtime/broker/client-registry.d.ts +4 -0
  204. package/dist/core/runtime/broker/client-registry.js +13 -1
  205. package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
  206. package/dist/core/runtime/broker/extension-abort.js +4 -2
  207. package/dist/core/runtime/broker/fault-retry.js +4 -0
  208. package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
  209. package/dist/core/runtime/broker/frame-dispatch.js +5 -6
  210. package/dist/core/runtime/broker/read-ops.d.ts +1 -1
  211. package/dist/core/runtime/broker/read-ops.js +2 -2
  212. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  213. package/dist/core/runtime/broker/rebind.js +6 -1
  214. package/dist/core/runtime/broker-extension-render.js +19 -0
  215. package/dist/core/runtime/broker-protocol.d.ts +14 -0
  216. package/dist/core/runtime/broker.d.ts +1 -1
  217. package/dist/core/runtime/broker.js +25 -12
  218. package/dist/core/runtime/busy.d.ts +4 -3
  219. package/dist/core/runtime/busy.js +4 -3
  220. package/dist/core/runtime/front-door-env.d.ts +5 -0
  221. package/dist/core/runtime/front-door-env.js +16 -0
  222. package/dist/core/runtime/front-door.d.ts +1 -5
  223. package/dist/core/runtime/front-door.js +2 -5
  224. package/dist/core/runtime/host.d.ts +1 -1
  225. package/dist/core/runtime/invocation.d.ts +6 -0
  226. package/dist/core/runtime/invocation.js +10 -0
  227. package/dist/core/runtime/kickoff.js +34 -0
  228. package/dist/core/runtime/launch.d.ts +2 -6
  229. package/dist/core/runtime/node-read.js +4 -1
  230. package/dist/core/runtime/nodes.js +2 -0
  231. package/dist/core/runtime/promote.d.ts +1 -0
  232. package/dist/core/runtime/promote.js +3 -1
  233. package/dist/core/runtime/session-cycles.d.ts +6 -0
  234. package/dist/core/runtime/session-cycles.js +15 -1
  235. package/dist/core/runtime/session-visibility.d.ts +17 -8
  236. package/dist/core/runtime/session-visibility.js +19 -10
  237. package/dist/core/runtime/spawn-env.d.ts +9 -1
  238. package/dist/core/runtime/spawn-env.js +62 -2
  239. package/dist/core/runtime/spawn.js +4 -4
  240. package/dist/core/runtime/stop-guard.d.ts +2 -2
  241. package/dist/core/runtime/stop-guard.js +2 -2
  242. package/dist/core/runtime/stop-signals.d.ts +1 -1
  243. package/dist/core/runtime/stop-signals.js +2 -2
  244. package/dist/core/runtime/tmux-bindings.js +1 -1
  245. package/dist/core/session-model/session-state.d.ts +4 -3
  246. package/dist/core/session-model/session-state.js +14 -1
  247. package/dist/core/user-settings.d.ts +36 -2
  248. package/dist/core/user-settings.js +56 -14
  249. package/dist/core/worktree.d.ts +15 -45
  250. package/dist/core/worktree.js +59 -261
  251. package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
  252. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
  253. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  254. package/dist/daemon/api/handlers/broker-ops.js +9 -5
  255. package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
  256. package/dist/daemon/api/handlers/feedback-comments.js +207 -0
  257. package/dist/daemon/api/handlers/human.js +24 -20
  258. package/dist/daemon/api/handlers/inbox.d.ts +10 -0
  259. package/dist/daemon/api/handlers/inbox.js +48 -109
  260. package/dist/daemon/api/handlers/profiles.js +11 -29
  261. package/dist/daemon/api/map.d.ts +6 -1
  262. package/dist/daemon/api/map.js +23 -0
  263. package/dist/daemon/api/server.js +3 -1
  264. package/dist/daemon/companion-retire.d.ts +2 -0
  265. package/dist/daemon/companion-retire.js +33 -0
  266. package/dist/daemon/cron-run.js +29 -17
  267. package/dist/daemon/crtrd.js +1 -1
  268. package/dist/daemon/fleet.d.ts +22 -10
  269. package/dist/daemon/fleet.js +43 -24
  270. package/dist/daemon/human/finish.d.ts +12 -6
  271. package/dist/daemon/human/finish.js +134 -54
  272. package/dist/daemon/human/sweep.js +9 -3
  273. package/dist/daemon/profile-delete.d.ts +7 -0
  274. package/dist/daemon/profile-delete.js +306 -0
  275. package/dist/daemon/reconcilers/broker-supervision.js +14 -9
  276. package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
  277. package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
  278. package/dist/daemon/reconcilers/live-obligation.js +30 -2
  279. package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
  280. package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
  281. package/dist/daemon/review/deliver.js +3 -3
  282. package/dist/daemon/review/finish.d.ts +0 -2
  283. package/dist/daemon/review/finish.js +1 -29
  284. package/dist/daemon/review/sweep.js +12 -1
  285. package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
  286. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
  287. package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
  288. package/dist/pi-extensions/canvas-bash-valve.js +31 -13
  289. package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
  290. package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
  291. package/dist/pi-extensions/canvas-review-boundary.js +18 -9
  292. package/dist/pi-extensions/canvas-stophook.js +4 -4
  293. package/dist/shared/generated-context.js +2 -2
  294. package/dist/types.d.ts +61 -5
  295. package/dist/types.js +8 -2
  296. package/package.json +4 -4
  297. package/runtime.lock.json +5 -33
  298. package/scripts/install-runtime.mjs +19 -24
  299. package/dist/builtin-memory/init.md +0 -38
  300. package/dist/builtin-memory/plan.md +0 -19
  301. package/dist/builtin-memory/spec.md +0 -19
  302. package/dist/clients/attach/render/html-markdown.d.ts +0 -13
  303. package/dist/clients/attach/render/html-markdown.js +0 -206
  304. package/dist/commands/human/doc.js +0 -144
  305. package/dist/commands/human/inbox.d.ts +0 -2
  306. package/dist/core/human/__tests__/page-render.test.js +0 -49
  307. package/dist/core/human/markdown-html.d.ts +0 -8
  308. package/dist/core/human/markdown-html.js +0 -581
  309. package/dist/core/human/page-render.d.ts +0 -3
  310. package/dist/core/human/page-render.js +0 -131
  311. package/dist/pages/bundle.css +0 -1
  312. package/dist/pages/bundle.js +0 -1347
  313. package/dist/pages/comments.d.ts +0 -43
  314. package/dist/pages/comments.js +0 -125
  315. package/dist/pages/elements/cards.d.ts +0 -17
  316. package/dist/pages/elements/cards.js +0 -802
  317. package/dist/pages/elements/chart.js +0 -648
  318. package/dist/pages/elements/options.d.ts +0 -21
  319. package/dist/pages/elements/options.js +0 -629
  320. package/dist/pages/elements/pages.d.ts +0 -24
  321. package/dist/pages/elements/pages.js +0 -355
  322. package/dist/pages/elements/slot.d.ts +0 -21
  323. package/dist/pages/elements/slot.js +0 -105
  324. package/dist/pages/elements/table.d.ts +0 -19
  325. package/dist/pages/elements/table.js +0 -935
  326. package/dist/pages/elements/text.d.ts +0 -25
  327. package/dist/pages/elements/text.js +0 -968
  328. package/dist/pages/entry.d.ts +0 -8
  329. package/dist/pages/entry.js +0 -8
  330. package/dist/pages/host.d.ts +0 -158
  331. package/dist/pages/host.js +0 -182
  332. package/dist/pages/register.d.ts +0 -2
  333. package/dist/pages/register.js +0 -3
  334. package/dist/pages/slot-config.d.ts +0 -36
  335. package/dist/pages/slot-config.js +0 -50
  336. package/dist/pages/types.d.ts +0 -98
  337. package/dist/pages/types.js +0 -23
  338. /package/dist/{core/human/__tests__/page-render.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
  339. /package/dist/{pages/elements/chart.d.ts → core/__tests__/plugin-page-components.test.d.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, rmdirSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { dirname, join, resolve } from 'node:path';
4
4
  import { gitSync } from './git.js';
@@ -76,7 +76,6 @@ function checkoutPathForBranchInList(porcelain, branch) {
76
76
  return null;
77
77
  }
78
78
  const WORKTREE_LOCK_WAIT_MS = 30_000;
79
- const WORKTREE_CLEANUP_TIMEOUT_MS = 5_000;
80
79
  function commonGitDir(repoRoot) {
81
80
  const commonDir = runGit(repoRoot, ['rev-parse', '--git-common-dir'], 'git_common_dir_failed', 'Inspect the repository git metadata, then retry.');
82
81
  return realpathSync(resolve(repoRoot, commonDir));
@@ -240,18 +239,23 @@ export function isRebaseInProgress(cwd) {
240
239
  /** The ONE authoritative closed-worktree state transition, shared by both
241
240
  * closing paths (explicit `closeManagedWorktreeLocked` land, and the clean
242
241
  * `autoDropCleanManagedWorktreeLocked` push-final drop): persist
243
- * `managed_worktree.state: 'closed'` AND repoint the node's launch `cwd` to
244
- * the durable base checkout, atomically, in the same `updateNode` write.
245
- * The worktree dir is torn down by cleanup around this call (at best left
246
- * as an empty tombstone so an in-flight `push final` can still run) and
247
- * must never remain the node's recorded launch directory — an existing
248
- * tombstone directory still passes a naive `existsSync(cwd)` revive
249
- * preflight, so a caller that forgets this repoint lets a later park/revive
250
- * launch into a directory that no longer holds a real checkout. Both
251
- * closing paths MUST go through this helper instead of hand-rolling the
252
- * `updateNode` call, so neither can drift out of sync with the other. */
253
- function persistClosedWorktreeState(nodeId, wt) {
254
- updateNode(nodeId, { cwd: wt.repo_root, managed_worktree: { ...wt, state: 'closed', closed: new Date().toISOString() } });
242
+ * `managed_worktree.state: 'closed'`, name its cleanup state, and repoint the
243
+ * node's launch `cwd` to the durable base checkout atomically in the same
244
+ * `updateNode` write. A live node keeps its checkout intact for explicit
245
+ * post-exit cleanup: `git worktree remove` recursively deletes the caller's
246
+ * cwd and cannot safely run against it. Both closing paths MUST go through this helper instead of
247
+ * hand-rolling the `updateNode` call, so neither can drift out of sync. */
248
+ function persistClosedWorktreeState(nodeId, wt, cleanup = 'pending') {
249
+ updateNode(nodeId, { cwd: wt.repo_root, managed_worktree: { ...wt, state: 'closed', cleanup, closed: new Date().toISOString() } });
250
+ }
251
+ function deferredCleanupResult(wt) {
252
+ const remove = `git -C ${shellQuote(wt.repo_root)} worktree remove ${shellQuote(wt.path)}`;
253
+ return {
254
+ worktree_removed: false,
255
+ worktree_remove_error: `cleanup pending: the caller checkout at ${wt.path} remains intact. After its node exits, run \`${remove}\` after confirming it is safe to delete.`,
256
+ branch_deleted: false,
257
+ branch_delete_error: `cleanup pending: local branch ${wt.branch} remains checked out at ${wt.path}; delete it only after that checkout is removed and its landed commit is still contained in ${wt.base_ref}.`,
258
+ };
255
259
  }
256
260
  function closeManagedWorktreeLocked(nodeId) {
257
261
  const node = getNode(nodeId);
@@ -289,18 +293,18 @@ function closeManagedWorktreeLocked(nodeId) {
289
293
  }
290
294
  const dirty = statusPorcelain(wt.path);
291
295
  if (dirty !== '') {
292
- throw new WorktreeError('dirty_worktree', 'managed worktree has uncommitted changes', `Commit the work in ${wt.path} (or ask the human with \`crtr human ask\` if you do not know whether to keep it), then rerun \`crtr node worktree close\`.`);
296
+ throw new WorktreeError('dirty_worktree', 'managed worktree has uncommitted changes', `Commit the work in ${wt.path} (or ask the user with \`crtr human send\` if you do not know whether to keep it), then rerun \`crtr node worktree close\`.`);
293
297
  }
294
298
  const branch = currentBranch(wt.path);
295
299
  if (branch !== wt.branch) {
296
300
  const found = branch === 'HEAD' ? 'detached HEAD' : branch;
297
- throw new WorktreeError('wrong_branch', `managed worktree is not on its recorded branch (expected ${wt.branch}, found ${found})`, `Check out ${wt.branch} in ${wt.path} before closing, or ask the human with \`crtr human ask\` if you are unsure why it changed, then rerun \`crtr node worktree close\`.`);
301
+ throw new WorktreeError('wrong_branch', `managed worktree is not on its recorded branch (expected ${wt.branch}, found ${found})`, `Check out ${wt.branch} in ${wt.path} before closing, or ask the user with \`crtr human send\` if you are unsure why it changed, then rerun \`crtr node worktree close\`.`);
298
302
  }
299
303
  const baseSha = resolveLandableBase(wt.repo_root, wt.base_ref);
300
304
  const rebase = gitSync(['rebase', baseSha], wt.path);
301
305
  if (rebase.status !== 0) {
302
306
  const detail = (rebase.stderr.trim() || rebase.stdout.trim()).trim();
303
- throw new WorktreeError('rebase_failed', `rebase onto local base branch '${wt.base_ref}' did not complete`, `Resolve conflicts in ${wt.path}, run \`git rebase --continue\` there, then rerun \`crtr node worktree close\`. If the blocker is not mechanical, ask the human with \`crtr human ask\`.`, detail);
307
+ throw new WorktreeError('rebase_failed', `rebase onto local base branch '${wt.base_ref}' did not complete`, `Resolve conflicts in ${wt.path}, run \`git rebase --continue\` there, then rerun \`crtr node worktree close\`. If the blocker is not mechanical, ask the user with \`crtr human send\`.`, detail);
304
308
  }
305
309
  const landedSha = runGit(wt.path, ['rev-parse', 'HEAD'], 'landed_sha_failed', 'Inspect the worktree checkout manually, then rerun `crtr node worktree close`.');
306
310
  // Fast-forward the local base branch to the worktree tip rebased onto the
@@ -309,7 +313,7 @@ function closeManagedWorktreeLocked(nodeId) {
309
313
  // captured tip, so neither can overwrite a concurrent base change.
310
314
  const fastForward = gitSync(['merge-base', '--is-ancestor', baseSha, landedSha], wt.repo_root);
311
315
  if (fastForward.status !== 0) {
312
- throw new WorktreeError('land_base_failed', `rebased worktree is not a fast-forward of local base branch '${wt.base_ref}'`, 'Ask the human with `crtr human ask` how to land this managed worktree.', (fastForward.stderr.trim() || fastForward.stdout.trim()).trim());
316
+ throw new WorktreeError('land_base_failed', `rebased worktree is not a fast-forward of local base branch '${wt.base_ref}'`, 'Ask the user with `crtr human send` how to land this managed worktree.', (fastForward.stderr.trim() || fastForward.stdout.trim()).trim());
313
317
  }
314
318
  const baseCheckout = checkoutPathForBranch(wt.repo_root, wt.base_ref);
315
319
  if (baseCheckout !== null) {
@@ -319,7 +323,7 @@ function closeManagedWorktreeLocked(nodeId) {
319
323
  const detail = checkoutHead.status !== 0
320
324
  ? (checkoutHead.stderr.trim() || actualHead).trim()
321
325
  : `expected ${baseSha}, found ${actualHead}`;
322
- throw new WorktreeError('land_base_failed', `local base branch '${wt.base_ref}' changed while this worktree was closing`, `Reconcile the base checkout at ${baseCheckout}, then rerun \`crtr node worktree close\`. If you are unsure, ask the human with \`crtr human ask\`.`, detail);
326
+ throw new WorktreeError('land_base_failed', `local base branch '${wt.base_ref}' changed while this worktree was closing`, `Reconcile the base checkout at ${baseCheckout}, then rerun \`crtr node worktree close\`. If you are unsure, ask the user with \`crtr human send\`.`, detail);
323
327
  }
324
328
  }
325
329
  const land = baseCheckout !== null
@@ -328,46 +332,21 @@ function closeManagedWorktreeLocked(nodeId) {
328
332
  if (land.status !== 0) {
329
333
  const detail = (land.stderr.trim() || land.stdout.trim()).trim();
330
334
  throw new WorktreeError('land_base_failed', `could not fast-forward local base branch '${wt.base_ref}' to this worktree`, baseCheckout !== null
331
- ? `Local base branch '${wt.base_ref}' is checked out at ${baseCheckout} in a state that blocks a fast-forward (uncommitted changes, or it has diverged). Reconcile it there, then rerun \`crtr node worktree close\`. If you are unsure, ask the human with \`crtr human ask\`.`
332
- : 'Ask the human with `crtr human ask` how to land this managed worktree.', detail);
333
- }
334
- // Landing on the local base branch IS the point of no return: persist `state: 'closed'`
335
- // IMMEDIATELY, before attempting any cleanup below. Worktree removal and
336
- // branch deletion are best-effort from here on — their failure is cosmetic
337
- // (a stray checkout / local ref) and must never re-strand `state: 'open'`
338
- // behind a dead end that permanently blocks push-final. See
339
- // `persistClosedWorktreeState` for why `cwd` is repointed in the same write.
340
- persistClosedWorktreeState(nodeId, wt);
341
- const { worktreeRemoved, worktreeRemoveError } = bestEffortRemoveWorktreeCheckout(wt, { timeoutMs: WORKTREE_CLEANUP_TIMEOUT_MS });
342
- // Delete via the atomic base+branch transaction, keyed on `landedSha` for
343
- // BOTH refs: the fast-forward above just moved `refs/heads/<wt.base_ref>`
344
- // to `landedSha`, and `wt.branch`'s tip (as of `landedSha`) is provably
345
- // contained in it. Binding both refs into one transaction closes the
346
- // window a branch-only check leaves open: if a concurrent commit lands a
347
- // new tip on this branch, or a concurrent force-reset moves the base off
348
- // `landedSha`, before this call runs, the transaction fails closed and
349
- // preserves the branch — it cannot ever delete the last ref to work that
350
- // is no longer contained in the base. A refusal here is cosmetic and
351
- // non-fatal exactly as before: the land already happened (state is closed
352
- // above), only cleanup of the source branch is best-effort. Skip the
353
- // attempt entirely when the checkout itself could not be removed — this
354
- // branch is still checked out somewhere, so deleting its ref underneath
355
- // that checkout must never be attempted.
356
- const { branchDeleted, branchDeleteError } = worktreeRemoved
357
- ? bestEffortDeleteBranchWithBaseProof(wt, landedSha, landedSha)
358
- : {
359
- branchDeleted: false,
360
- branchDeleteError: `local branch ${wt.branch} was left in place because its checkout at ${wt.path} could not be removed automatically; resolve the checkout, then run \`git -C ${shellQuote(wt.repo_root)} branch -D ${shellQuote(wt.branch)}\` after confirming it is merged.`,
361
- };
335
+ ? `Local base branch '${wt.base_ref}' is checked out at ${baseCheckout} in a state that blocks a fast-forward (uncommitted changes, or it has diverged). Reconcile it there, then rerun \`crtr node worktree close\`. If you are unsure, ask the user with \`crtr human send\`.`
336
+ : 'Ask the user with `crtr human send` how to land this managed worktree.', detail);
337
+ }
338
+ // The caller broker still runs from wt.path after this API response. Do not
339
+ // invoke `git worktree remove`: it recursively deletes the caller's cwd and
340
+ // a timeout can kill Git between individual unlink operations. Keep the
341
+ // checkout and its checked-out branch intact in the named pending-cleanup
342
+ // state; the successful land is still the point of no return for this node.
343
+ persistClosedWorktreeState(nodeId, wt, 'pending');
362
344
  return {
363
345
  node_id: nodeId,
364
346
  branch: wt.branch,
365
347
  worktree_path: wt.path,
366
348
  landed_sha: landedSha,
367
- worktree_removed: worktreeRemoved,
368
- ...(worktreeRemoveError !== undefined ? { worktree_remove_error: worktreeRemoveError } : {}),
369
- branch_deleted: branchDeleted,
370
- ...(branchDeleteError !== undefined ? { branch_delete_error: branchDeleteError } : {}),
349
+ ...deferredCleanupResult(wt),
371
350
  };
372
351
  }
373
352
  export function closeManagedWorktree(nodeId) {
@@ -378,114 +357,9 @@ export function closeManagedWorktree(nodeId) {
378
357
  return closeManagedWorktreeLocked(nodeId);
379
358
  return withRepositoryWorktreeLock(wt.repo_root, () => closeManagedWorktreeLocked(nodeId));
380
359
  }
381
- const defaultWorktreeCleanupRunner = (args, cwd, options) => gitSync(args, cwd, undefined, options);
382
- let worktreeCleanupRunner = defaultWorktreeCleanupRunner;
383
- /** Test seam for the bounded post-land cleanup regression. */
384
- export function setWorktreeCleanupRunnerForTest(runner) {
385
- worktreeCleanupRunner = runner ?? defaultWorktreeCleanupRunner;
386
- }
387
- function isValidRegisteredCheckout(output, wt) {
388
- let path = null;
389
- let prunable = false;
390
- const samePath = (candidate) => {
391
- try {
392
- return realpathSync(candidate) === realpathSync(wt.path);
393
- }
394
- catch {
395
- return resolve(candidate) === resolve(wt.path);
396
- }
397
- };
398
- const matches = () => path !== null
399
- && samePath(path)
400
- && existsSync(path)
401
- && existsSync(join(path, '.git'))
402
- && !prunable;
403
- for (const line of output.split('\n')) {
404
- if (line.startsWith('worktree ')) {
405
- if (matches())
406
- return true;
407
- path = line.slice('worktree '.length).trim();
408
- prunable = false;
409
- }
410
- else if (path !== null && line.startsWith('prunable')) {
411
- prunable = true;
412
- }
413
- }
414
- return matches();
415
- }
416
- function timeoutRecoveryGuidance(wt, options) {
417
- const force = `git -C ${shellQuote(wt.repo_root)} worktree remove --force ${shellQuote(wt.path)}`;
418
- const prune = `git -C ${shellQuote(wt.repo_root)} worktree prune`;
419
- const removeResidual = `rm -rf ${shellQuote(wt.path)}`;
420
- try {
421
- const inspection = worktreeCleanupRunner(['worktree', 'list', '--porcelain'], wt.repo_root, options);
422
- if (inspection.status === 0) {
423
- return isValidRegisteredCheckout(inspection.stdout, wt)
424
- ? `run \`${force}\` after confirming it is safe to delete.`
425
- : `run \`${prune}\`, then run \`${removeResidual}\` after confirming the leftover directory is safe to delete.`;
426
- }
427
- }
428
- catch {
429
- // Inspection is advisory only. Emit both recovery shapes when Git could
430
- // not tell us which residual state remains.
431
- }
432
- return `if ${shellQuote(wt.path)} is a valid non-prunable checkout (its .git exists and its worktree-list record is not prunable), run \`${force}\`; otherwise run \`${prune}\` and then \`${removeResidual}\` after confirming it is safe to delete.`;
433
- }
434
- /** Best-effort `git worktree remove` shared by a real land (close) and an
435
- * auto-drop (nothing was ever landed). Either way `wt.path` IS the calling
436
- * node's own pi-process cwd — a process can never chdir itself out of a
437
- * directory that vanishes out from under it, so once removal deletes it,
438
- * every subsequent bash/chdir in this node fails permanently and the
439
- * mandated post-close `crtr push final` can never run. Recreate it as an
440
- * empty tombstone directory so the path still resolves regardless. */
441
- function bestEffortRemoveWorktreeCheckout(wt, options = {}) {
442
- const remove = worktreeCleanupRunner(['worktree', 'remove', wt.path], wt.repo_root, options);
443
- const worktreeRemoved = remove.status === 0;
444
- if (worktreeRemoved && existsSync(wt.path)) {
445
- // `git worktree remove` can leave the (now empty) mount dir behind; tidy it best-effort.
446
- try {
447
- rmdirSync(wt.path);
448
- }
449
- catch { /* non-empty or busy — leave it */ }
450
- }
451
- if (!existsSync(wt.path)) {
452
- try {
453
- mkdirSync(wt.path, { recursive: true });
454
- }
455
- catch { /* best-effort; leaves cwd broken only if this also fails */ }
456
- }
457
- if (worktreeRemoved)
458
- return { worktreeRemoved };
459
- const detail = (remove.stderr.trim() || remove.stdout.trim()).trim();
460
- const timeoutDetail = remove.timedOut
461
- ? `cleanup timed out after ${options.timeoutMs}ms`
462
- : `could not be removed automatically${detail ? ` (${detail})` : ''}`;
463
- const manualGuidance = remove.timedOut
464
- ? timeoutRecoveryGuidance(wt, options)
465
- : `run \`git -C ${shellQuote(wt.repo_root)} worktree remove ${shellQuote(wt.path)}\` after confirming it is safe to delete.`;
466
- return {
467
- worktreeRemoved,
468
- worktreeRemoveError: `the checkout at ${wt.path} ${timeoutDetail} ${manualGuidance}`,
469
- };
470
- }
471
- /** Atomic multi-ref transaction shared by close and auto-drop: as ONE Git ref
472
- * transaction, verifies `refs/heads/<wt.base_ref>` still equals `baseSha`
473
- * AND deletes `refs/heads/<wt.branch>` only if it still equals `branchSha`
474
- * — both preconditions and the delete are checked and applied together via
475
- * `git update-ref --stdin`, which commits every command in its input
476
- * atomically (any failed precondition aborts the whole transaction; see
477
- * `rollbackManagedWorktreeLocked` above for the single-ref precursor of
478
- * this pattern). A single-ref compare-and-delete on the branch alone is not
479
- * enough: both callers prove containment against a specific base SHA, and
480
- * that base can move (an external force-reset) in the window between the
481
- * proof and this call just as easily as the branch can. Binding the base's
482
- * live value into the SAME transaction as the branch delete closes that
483
- * window — the delete can only succeed at the instant both refs are
484
- * simultaneously exactly what was proven, so a base rewrite can never leave
485
- * a proven commit unreachable from any head. */
486
- function bestEffortDeleteBranchWithBaseProof(wt, branchSha, baseSha) {
487
- return bestEffortDeleteBranchWithContainmentProof(wt, branchSha, `refs/heads/${wt.base_ref}`, baseSha);
488
- }
360
+ // ---------------------------------------------------------------------------
361
+ // Push-final auto-drop of a clean, zero-commit managed worktree
362
+ // ---------------------------------------------------------------------------
489
363
  /** Delete a source branch only while the ref that proved it already delivered
490
364
  * is still at the exact inspected SHA. `containingRef` can be the recorded
491
365
  * local base or that base's remote-tracking upstream. */
@@ -573,7 +447,7 @@ function proveAbsentPathManagedWorktree(wt) {
573
447
  * work is delivered. This deliberately never recommends `worktree close`:
574
448
  * that command cannot operate on the absent checkout. */
575
449
  export function missingWorktreeRecoveryHint(wt) {
576
- return `The recorded checkout path is gone, but crtr cannot prove ${wt.branch} is already delivered. Push or land ${wt.branch}, then rerun \`crtr push final\`; if you cannot establish what happened to the branch, ask the human with \`crtr human ask\`.`;
450
+ return `The recorded checkout path is gone, but crtr cannot prove ${wt.branch} is already delivered. Push or land ${wt.branch}, then rerun \`crtr push final\`; if you cannot establish what happened to the branch, ask the user with \`crtr human send\`.`;
577
451
  }
578
452
  /** Fix B's shared "nothing to land" test — the SAME clean-worktree definition
579
453
  * `close`'s own dirty check now naturally satisfies once `.git/info/exclude`
@@ -582,20 +456,11 @@ export function missingWorktreeRecoveryHint(wt) {
582
456
  * be a git-ancestor of (or equal to) the CURRENT tip of its recorded base
583
457
  * branch — i.e. landing it would be a pure no-op. Any ambiguity (missing
584
458
  * path, mid-rebase, wrong branch, an unresolvable base, or a real ancestry
585
- * divergence) returns null: this function only ever proves "safe to drop"
459
+ * divergence) returns null: this function only ever proves a no-op land
586
460
  * when a live git check establishes it, never on a heuristic guess, so real
587
- * unlanded work always falls through to the existing explicit-close guard.
588
- * This proof is NOT itself the safety boundary — it only decides whether
589
- * auto-drop is even attempted. `autoDropCleanManagedWorktreeLocked` below
590
- * re-proves containment against the LIVE base immediately before the
591
- * destructive delete, refuses outright if the checkout cannot be removed,
592
- * and conditions the delete itself on BOTH `branchSha` and that same live
593
- * base SHA via `bestEffortDeleteBranchWithBaseProof`'s atomic multi-ref
594
- * transaction — those checks, not this one, are what close the TOCTOU gap.
595
- * Exported (alongside
596
- * `autoDropCleanManagedWorktreeLocked`) purely so regression tests can
597
- * inject a race in the window between proving and cleanup; production code
598
- * must always go through `autoDropCleanManagedWorktree`. */
461
+ * unlanded work always falls through to the explicit-close guard. The locked
462
+ * path re-checks this predicate before recording deferred cleanup; production
463
+ * code must always go through `autoDropCleanManagedWorktree`. */
599
464
  export function proveCleanEmptyManagedWorktree(wt) {
600
465
  if (wt.state !== 'open')
601
466
  return null;
@@ -627,13 +492,13 @@ export function proveCleanEmptyManagedWorktree(wt) {
627
492
  }
628
493
  /** Conclude an absent-path record only after `proveAbsentPathManagedWorktree`
629
494
  * has established the same no-unlanded-work property required by auto-drop.
630
- * There is no checkout left to remove; persistClosedWorktreeState writes the
631
- * closed record and repoints cwd exactly like the normal tombstone path. */
495
+ * There is no checkout left to remove; persistClosedWorktreeState records the
496
+ * completed cleanup and repoints cwd to the durable base checkout. */
632
497
  function autoDropAbsentPathManagedWorktreeLocked(nodeId, wt, proof) {
633
498
  if (existsSync(wt.path))
634
499
  return null;
635
500
  if (proof.branchSha === null) {
636
- persistClosedWorktreeState(nodeId, wt);
501
+ persistClosedWorktreeState(nodeId, wt, 'complete');
637
502
  return { node_id: nodeId, branch: wt.branch, worktree_path: wt.path, worktree_removed: true, branch_deleted: true };
638
503
  }
639
504
  if (proof.containingRef === undefined || proof.containingSha === undefined)
@@ -641,93 +506,26 @@ function autoDropAbsentPathManagedWorktreeLocked(nodeId, wt, proof) {
641
506
  const { branchDeleted } = bestEffortDeleteBranchWithContainmentProof(wt, proof.branchSha, proof.containingRef, proof.containingSha);
642
507
  if (!branchDeleted)
643
508
  return null;
644
- persistClosedWorktreeState(nodeId, wt);
509
+ persistClosedWorktreeState(nodeId, wt, 'complete');
645
510
  return { node_id: nodeId, branch: wt.branch, worktree_path: wt.path, worktree_removed: true, branch_deleted: true };
646
511
  }
647
- /** Exported (see `proveCleanEmptyManagedWorktree`'s doc) so regression tests
648
- * can call it directly with a proof captured before injecting a race.
649
- * Returns null — persisting NO state change — when the immediate base
650
- * re-verification, the checkout removal, or the atomic base+branch delete
651
- * refuses: the caller (`autoDropCleanManagedWorktree`, and `push final`
652
- * above it) then falls through to the existing `open_managed_worktree`
653
- * guard exactly as if this worktree had never looked droppable. In every
654
- * refusal case the branch, and every commit reachable from it, is
655
- * unconditionally preserved — that is the actual safety property this
656
- * function guarantees. A checkout-removal refusal in particular must be
657
- * treated as a full refusal, not a partial success: if the checkout cannot
658
- * be removed (e.g. a file reappeared after the clean predicate ran), the
659
- * branch is still checked out somewhere, so this function must never reach
660
- * the ref-deletion transaction or mutate managed-worktree state — both the
661
- * branch and the live checkout stay fully recoverable and the caller's
662
- * `open_managed_worktree` guard fires. */
512
+ /** Re-check a clean auto-drop before closing its record. `push final` runs
513
+ * inside the owner broker, so it has the same destructive-cwd boundary as
514
+ * explicit close: removal and branch deletion are deferred until that owner
515
+ * has exited. */
663
516
  export function autoDropCleanManagedWorktreeLocked(nodeId, wt, proof) {
664
- // Re-verify containment against the LIVE base immediately before doing
665
- // anything destructive: the proof above may already be stale by the time
666
- // we get here — `withRepositoryWorktreeLock` only serializes crtr's OWN
667
- // worktree mutations, not ordinary `git commit`/`git reset` run by any
668
- // other process in the checkout, nor an external force-reset of the base
669
- // branch. Re-resolving the base and re-running the ancestor check fresh
670
- // (rather than trusting `proof.baseSha`) means a base fast-forward keeps a
671
- // genuinely-contained tip provably contained, while any other base
672
- // rewrite (or the base vanishing) fails this closed before we touch
673
- // anything. This SAME `liveBaseSha` is what the ref transaction below
674
- // verifies again at the moment of the delete — re-checking it here only
675
- // decides whether to attempt cleanup at all; the transaction is what
676
- // actually closes the race.
677
- let liveBaseSha;
678
- try {
679
- liveBaseSha = resolveLandableBase(wt.repo_root, wt.base_ref);
680
- }
681
- catch {
682
- return null;
683
- }
684
- const stillContained = gitSync(['merge-base', '--is-ancestor', proof.branchSha, liveBaseSha], wt.repo_root);
685
- if (stillContained.status !== 0)
686
- return null;
687
- // Checkout removal has to come BEFORE the ref-deletion transaction, not
688
- // after: `git worktree remove`'s cleanliness check needs the checked-out
689
- // branch's ref to still resolve to compute a diff, so deleting the ref
690
- // first turns every tracked file into what looks like an unresolvable path
691
- // and `worktree remove` refuses outright (verified directly against git).
692
- // This ordering is still safe: removal alone never touches
693
- // `refs/heads/<branch>` or anything reachable from it — a commit that
694
- // lands here is fully preserved as an ordinary (no longer checked-out)
695
- // local branch, exactly as droppable-or-not as it was, until the
696
- // transaction below either consumes it or refuses. A removal REFUSAL,
697
- // though, must stop cleanup here entirely: low-level ref deletion bypasses
698
- // Git's own checked-out-branch protection, so deleting the branch while
699
- // its checkout is still live would strand that checkout with no branch to
700
- // point at it.
701
- const { worktreeRemoved } = bestEffortRemoveWorktreeCheckout(wt);
702
- if (!worktreeRemoved)
703
- return null;
704
- // THE safety boundary: this only succeeds if `refs/heads/<wt.base_ref>` is
705
- // still exactly `liveBaseSha` AND `refs/heads/<branch>` is still exactly
706
- // `proof.branchSha`, verified and applied as one atomic transaction. A
707
- // commit made anywhere in the checkout, or an external rewrite of the
708
- // base, at any point up to and including this instant makes the whole
709
- // transaction fail closed instead of silently destroying new work. On
710
- // refusal the branch, and everything reachable from it, is fully intact
711
- // and enumerable; only the convenience checkout may already be gone
712
- // (tombstoned by the removal above), recoverable with a fresh
713
- // `git worktree add` against the still-live branch. `state` is left
714
- // untouched ('open') below specifically so this refusal is never mistaken
715
- // for a completed drop.
716
- const { branchDeleted } = bestEffortDeleteBranchWithBaseProof(wt, proof.branchSha, liveBaseSha);
717
- if (!branchDeleted)
517
+ const current = proveCleanEmptyManagedWorktree(wt);
518
+ if (current === null || current.branchSha !== proof.branchSha)
718
519
  return null;
719
- // Only now — with the branch ref provably gone at the proven SHA and the
720
- // base provably unchanged — is it safe to commit to the drop. Reuse the
721
- // shared closed-worktree transition (see `persistClosedWorktreeState`) so
722
- // this path repoints `cwd` to the base checkout exactly like an explicit
723
- // close — a later revive's tombstone-vs-missing-cwd check depends on it.
724
- persistClosedWorktreeState(nodeId, wt);
520
+ // The owner is still running in this checkout while it publishes final.
521
+ // Preserve its files and checked-out branch; physical cleanup is explicitly
522
+ // recorded as pending rather than racing Git's recursive removal.
523
+ persistClosedWorktreeState(nodeId, wt, 'pending');
725
524
  return {
726
525
  node_id: nodeId,
727
526
  branch: wt.branch,
728
527
  worktree_path: wt.path,
729
- worktree_removed: true,
730
- branch_deleted: true,
528
+ ...deferredCleanupResult(wt),
731
529
  };
732
530
  }
733
531
  /** `push final`'s escape hatch: a managed worktree that has
@@ -737,7 +535,7 @@ export function autoDropCleanManagedWorktreeLocked(nodeId, wt, proof) {
737
535
  * that never had anything to close in the first place (a read-only
738
536
  * review node). Returns null and changes nothing when the worktree is not
739
537
  * PROVABLY empty-and-clean by `proveCleanEmptyManagedWorktree`, OR when a
740
- * concurrent mutation invalidates that proof before the destructive delete
538
+ * concurrent mutation invalidates that proof before the record is closed
741
539
  * (see `autoDropCleanManagedWorktreeLocked`); the caller's existing
742
540
  * `open_managed_worktree` guard then still applies unchanged — real
743
541
  * commits, real uncommitted changes, and a lost race all require the
@@ -757,8 +555,8 @@ export function autoDropCleanManagedWorktree(nodeId) {
757
555
  const locked = openManagedWorktreeForNode(nodeId);
758
556
  if (locked === null)
759
557
  return null;
760
- // Re-prove the matching predicate under the lock before any state or ref
761
- // mutation. A path that reappeared follows the ordinary checkout path.
558
+ // Re-prove the matching predicate under the lock before changing state.
559
+ // A path that reappeared follows the ordinary checkout path.
762
560
  if (!existsSync(locked.path)) {
763
561
  const proof = proveAbsentPathManagedWorktree(locked);
764
562
  return proof === null ? null : autoDropAbsentPathManagedWorktreeLocked(nodeId, locked, proof);