borgmcp 1.1.14 → 2.0.1

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 (459) hide show
  1. package/CONTRIBUTING.md +15 -0
  2. package/LICENSE +201 -33
  3. package/NOTICE +2 -0
  4. package/README.md +86 -37
  5. package/SECURITY.md +7 -0
  6. package/dist/agent-runtime.d.ts.map +1 -0
  7. package/dist/agent-runtime.js +63 -1
  8. package/dist/agent-runtime.js.map +1 -0
  9. package/dist/assimilate-cmd.d.ts +96 -17
  10. package/dist/assimilate-cmd.d.ts.map +1 -0
  11. package/dist/assimilate-cmd.js +1396 -48
  12. package/dist/assimilate-cmd.js.map +1 -0
  13. package/dist/assimilate-deps.d.ts.map +1 -0
  14. package/dist/assimilate-deps.js +384 -3
  15. package/dist/assimilate-deps.js.map +1 -0
  16. package/dist/assimilate-guard.d.ts +5 -7
  17. package/dist/assimilate-guard.d.ts.map +1 -0
  18. package/dist/assimilate-guard.js +60 -2
  19. package/dist/assimilate-guard.js.map +1 -0
  20. package/dist/assimilate-welcome.d.ts +1 -1
  21. package/dist/assimilate-welcome.d.ts.map +1 -0
  22. package/dist/assimilate-welcome.js +32 -2
  23. package/dist/assimilate-welcome.js.map +1 -0
  24. package/dist/auth-env.d.ts +7 -50
  25. package/dist/auth-env.d.ts.map +1 -0
  26. package/dist/auth-env.js +22 -1
  27. package/dist/auth-env.js.map +1 -0
  28. package/dist/backends/launch-all-pastelist.d.ts.map +1 -0
  29. package/dist/backends/launch-all-pastelist.js +11 -4
  30. package/dist/backends/launch-all-pastelist.js.map +1 -0
  31. package/dist/backends/launch-all-tmux.d.ts.map +1 -0
  32. package/dist/backends/launch-all-tmux.js +49 -1
  33. package/dist/backends/launch-all-tmux.js.map +1 -0
  34. package/dist/backends/launch-all-windows.d.ts.map +1 -0
  35. package/dist/backends/launch-all-windows.js +86 -12
  36. package/dist/backends/launch-all-windows.js.map +1 -0
  37. package/dist/bare-launch-menu.d.ts.map +1 -0
  38. package/dist/bare-launch-menu.js +83 -6
  39. package/dist/bare-launch-menu.js.map +1 -0
  40. package/dist/claude-launch-args.d.ts.map +1 -0
  41. package/dist/claude-launch-args.js +34 -1
  42. package/dist/claude-launch-args.js.map +1 -0
  43. package/dist/claude.d.ts.map +1 -0
  44. package/dist/claude.js +486 -14
  45. package/dist/claude.js.map +1 -0
  46. package/dist/cleanup-cmd.d.ts +5 -5
  47. package/dist/cleanup-cmd.d.ts.map +1 -0
  48. package/dist/cleanup-cmd.js +453 -14
  49. package/dist/cleanup-cmd.js.map +1 -0
  50. package/dist/clear-rewake-core.d.ts.map +1 -0
  51. package/dist/clear-rewake-core.js +23 -2
  52. package/dist/clear-rewake-core.js.map +1 -0
  53. package/dist/clear-rewake.d.ts.map +1 -0
  54. package/dist/clear-rewake.js +19 -1
  55. package/dist/clear-rewake.js.map +1 -0
  56. package/dist/cli-help.d.ts +5 -0
  57. package/dist/cli-help.d.ts.map +1 -0
  58. package/dist/cli-help.js +104 -56
  59. package/dist/cli-help.js.map +1 -0
  60. package/dist/cli-platform.d.ts.map +1 -0
  61. package/dist/cli-platform.js +106 -4
  62. package/dist/cli-platform.js.map +1 -0
  63. package/dist/cli-tool-approval.d.ts +84 -0
  64. package/dist/cli-tool-approval.d.ts.map +1 -0
  65. package/dist/cli-tool-approval.js +583 -0
  66. package/dist/cli-tool-approval.js.map +1 -0
  67. package/dist/codex-app-server.d.ts.map +1 -0
  68. package/dist/codex-app-server.js +228 -4
  69. package/dist/codex-app-server.js.map +1 -0
  70. package/dist/codex-app-wake.d.ts.map +1 -0
  71. package/dist/codex-app-wake.js +432 -2
  72. package/dist/codex-app-wake.js.map +1 -0
  73. package/dist/codex-launch.d.ts.map +1 -0
  74. package/dist/codex-launch.js +102 -2
  75. package/dist/codex-launch.js.map +1 -0
  76. package/dist/codex-remote.d.ts +6 -1
  77. package/dist/codex-remote.d.ts.map +1 -0
  78. package/dist/codex-remote.js +394 -1
  79. package/dist/codex-remote.js.map +1 -0
  80. package/dist/codex-wake-resolve.d.ts.map +1 -0
  81. package/dist/codex-wake-resolve.js +127 -1
  82. package/dist/codex-wake-resolve.js.map +1 -0
  83. package/dist/config-utils.d.ts +2 -2
  84. package/dist/config-utils.d.ts.map +1 -0
  85. package/dist/config-utils.js +766 -3
  86. package/dist/config-utils.js.map +1 -0
  87. package/dist/config.d.ts +98 -61
  88. package/dist/config.d.ts.map +1 -0
  89. package/dist/config.js +426 -1
  90. package/dist/config.js.map +1 -0
  91. package/dist/console-prefix.d.ts.map +1 -0
  92. package/dist/console-prefix.js +96 -1
  93. package/dist/console-prefix.js.map +1 -0
  94. package/dist/cube-name.d.ts.map +1 -0
  95. package/dist/cube-name.js +67 -1
  96. package/dist/cube-name.js.map +1 -0
  97. package/dist/cubes.d.ts +126 -18
  98. package/dist/cubes.d.ts.map +1 -0
  99. package/dist/cubes.js +412 -4
  100. package/dist/cubes.js.map +1 -0
  101. package/dist/debug.d.ts.map +1 -0
  102. package/dist/debug.js +71 -1
  103. package/dist/debug.js.map +1 -0
  104. package/dist/direct-log.d.ts.map +1 -0
  105. package/dist/direct-log.js +11 -1
  106. package/dist/direct-log.js.map +1 -0
  107. package/dist/docs-sections.d.ts +11 -10
  108. package/dist/docs-sections.d.ts.map +1 -0
  109. package/dist/docs-sections.js +113 -3
  110. package/dist/docs-sections.js.map +1 -0
  111. package/dist/drone-lifecycle.d.ts +5 -21
  112. package/dist/drone-lifecycle.d.ts.map +1 -0
  113. package/dist/drone-lifecycle.js +57 -8
  114. package/dist/drone-lifecycle.js.map +1 -0
  115. package/dist/drone-management.d.ts +29 -0
  116. package/dist/drone-management.d.ts.map +1 -0
  117. package/dist/drone-management.js +109 -0
  118. package/dist/drone-management.js.map +1 -0
  119. package/dist/ensure-mcp-config.d.ts.map +1 -0
  120. package/dist/ensure-mcp-config.js +36 -1
  121. package/dist/ensure-mcp-config.js.map +1 -0
  122. package/dist/evict-drone.d.ts +10 -2
  123. package/dist/evict-drone.d.ts.map +1 -0
  124. package/dist/evict-drone.js +60 -1
  125. package/dist/evict-drone.js.map +1 -0
  126. package/dist/gc-orphan-inboxes.d.ts.map +1 -0
  127. package/dist/gc-orphan-inboxes.js +177 -1
  128. package/dist/gc-orphan-inboxes.js.map +1 -0
  129. package/dist/inbox-monitor.d.ts +1 -1
  130. package/dist/inbox-monitor.d.ts.map +1 -0
  131. package/dist/inbox-monitor.js +856 -3
  132. package/dist/inbox-monitor.js.map +1 -0
  133. package/dist/index.d.ts +6 -4
  134. package/dist/index.d.ts.map +1 -0
  135. package/dist/index.js +1034 -37
  136. package/dist/index.js.map +1 -0
  137. package/dist/launch-all-cmd.d.ts.map +1 -0
  138. package/dist/launch-all-cmd.js +377 -37
  139. package/dist/launch-all-cmd.js.map +1 -0
  140. package/dist/launch-all-command.d.ts.map +1 -0
  141. package/dist/launch-all-command.js +24 -1
  142. package/dist/launch-all-command.js.map +1 -0
  143. package/dist/launch-all-deps.d.ts +3 -8
  144. package/dist/launch-all-deps.d.ts.map +1 -0
  145. package/dist/launch-all-deps.js +99 -1
  146. package/dist/launch-all-deps.js.map +1 -0
  147. package/dist/launch-all-discovery.d.ts +1 -0
  148. package/dist/launch-all-discovery.d.ts.map +1 -0
  149. package/dist/launch-all-discovery.js +90 -4
  150. package/dist/launch-all-discovery.js.map +1 -0
  151. package/dist/launch-all-locks.d.ts.map +1 -0
  152. package/dist/launch-all-locks.js +54 -1
  153. package/dist/launch-all-locks.js.map +1 -0
  154. package/dist/launch-gate.d.ts +2 -2
  155. package/dist/launch-gate.d.ts.map +1 -0
  156. package/dist/launch-gate.js +73 -1
  157. package/dist/launch-gate.js.map +1 -0
  158. package/dist/lifecycle-log-guard.d.ts.map +1 -0
  159. package/dist/lifecycle-log-guard.js +93 -2
  160. package/dist/lifecycle-log-guard.js.map +1 -0
  161. package/dist/list-roles-render.d.ts.map +1 -0
  162. package/dist/list-roles-render.js +40 -6
  163. package/dist/list-roles-render.js.map +1 -0
  164. package/dist/local-log-routing.d.ts +19 -0
  165. package/dist/local-log-routing.d.ts.map +1 -0
  166. package/dist/local-log-routing.js +67 -0
  167. package/dist/local-log-routing.js.map +1 -0
  168. package/dist/local-manage-tool-result.d.ts +8 -0
  169. package/dist/local-manage-tool-result.d.ts.map +1 -0
  170. package/dist/local-manage-tool-result.js +10 -0
  171. package/dist/local-manage-tool-result.js.map +1 -0
  172. package/dist/local-server-cursor.d.ts +34 -0
  173. package/dist/local-server-cursor.d.ts.map +1 -0
  174. package/dist/local-server-cursor.js +151 -0
  175. package/dist/local-server-cursor.js.map +1 -0
  176. package/dist/log-audit.d.ts.map +1 -0
  177. package/dist/log-audit.js +194 -3
  178. package/dist/log-audit.js.map +1 -0
  179. package/dist/log-stream.d.ts +31 -18
  180. package/dist/log-stream.d.ts.map +1 -0
  181. package/dist/log-stream.js +1214 -12
  182. package/dist/log-stream.js.map +1 -0
  183. package/dist/model-presets.d.ts.map +1 -0
  184. package/dist/model-presets.js +19 -1
  185. package/dist/model-presets.js.map +1 -0
  186. package/dist/name-validator.d.ts.map +1 -0
  187. package/dist/name-validator.js +22 -1
  188. package/dist/name-validator.js.map +1 -0
  189. package/dist/opencode-drone.d.ts.map +1 -0
  190. package/dist/opencode-drone.js +438 -4
  191. package/dist/opencode-drone.js.map +1 -0
  192. package/dist/opencode-plugin.d.ts +1 -1
  193. package/dist/opencode-plugin.d.ts.map +1 -0
  194. package/dist/opencode-plugin.js +29 -4
  195. package/dist/opencode-plugin.js.map +1 -0
  196. package/dist/parse-assimilate-args.d.ts +1 -1
  197. package/dist/parse-assimilate-args.d.ts.map +1 -0
  198. package/dist/parse-assimilate-args.js +125 -1
  199. package/dist/parse-assimilate-args.js.map +1 -0
  200. package/dist/parse-launch-all-args.d.ts.map +1 -0
  201. package/dist/parse-launch-all-args.js +70 -1
  202. package/dist/parse-launch-all-args.js.map +1 -0
  203. package/dist/postinstall-banner.d.ts.map +1 -0
  204. package/dist/postinstall-banner.js +27 -2
  205. package/dist/postinstall-banner.js.map +1 -0
  206. package/dist/postinstall.d.ts.map +1 -0
  207. package/dist/postinstall.js +32 -5
  208. package/dist/postinstall.js.map +1 -0
  209. package/dist/readiness-probe.d.ts.map +1 -0
  210. package/dist/readiness-probe.js +9 -1
  211. package/dist/readiness-probe.js.map +1 -0
  212. package/dist/regen-format.d.ts.map +1 -0
  213. package/dist/regen-format.js +545 -13
  214. package/dist/regen-format.js.map +1 -0
  215. package/dist/regen.d.ts.map +1 -0
  216. package/dist/regen.js +141 -5
  217. package/dist/regen.js.map +1 -0
  218. package/dist/remote-client.d.ts +65 -175
  219. package/dist/remote-client.d.ts.map +1 -0
  220. package/dist/remote-client.js +942 -1
  221. package/dist/remote-client.js.map +1 -0
  222. package/dist/reset-local-seat-cmd.d.ts +50 -0
  223. package/dist/reset-local-seat-cmd.d.ts.map +1 -0
  224. package/dist/reset-local-seat-cmd.js +169 -0
  225. package/dist/reset-local-seat-cmd.js.map +1 -0
  226. package/dist/role-match.d.ts.map +1 -0
  227. package/dist/role-match.js +23 -1
  228. package/dist/role-match.js.map +1 -0
  229. package/dist/role-resolver.d.ts.map +1 -0
  230. package/dist/role-resolver.js +60 -1
  231. package/dist/role-resolver.js.map +1 -0
  232. package/dist/roster-render.d.ts.map +1 -0
  233. package/dist/roster-render.js +121 -3
  234. package/dist/roster-render.js.map +1 -0
  235. package/dist/seat-probe.d.ts +32 -13
  236. package/dist/seat-probe.d.ts.map +1 -0
  237. package/dist/seat-probe.js +78 -1
  238. package/dist/seat-probe.js.map +1 -0
  239. package/dist/seat-store.d.ts +76 -0
  240. package/dist/seat-store.d.ts.map +1 -0
  241. package/dist/seat-store.js +309 -0
  242. package/dist/seat-store.js.map +1 -0
  243. package/dist/seats.d.ts +303 -0
  244. package/dist/seats.d.ts.map +1 -0
  245. package/dist/seats.js +551 -0
  246. package/dist/seats.js.map +1 -0
  247. package/dist/self-path.d.ts +27 -0
  248. package/dist/self-path.d.ts.map +1 -0
  249. package/dist/self-path.js +43 -0
  250. package/dist/self-path.js.map +1 -0
  251. package/dist/server-endpoint.d.ts +9 -0
  252. package/dist/server-endpoint.d.ts.map +1 -0
  253. package/dist/server-endpoint.js +38 -0
  254. package/dist/server-endpoint.js.map +1 -0
  255. package/dist/server-errors.d.ts +50 -0
  256. package/dist/server-errors.d.ts.map +1 -0
  257. package/dist/server-errors.js +79 -0
  258. package/dist/server-errors.js.map +1 -0
  259. package/dist/server-handshake.d.ts +197 -0
  260. package/dist/server-handshake.d.ts.map +1 -0
  261. package/dist/server-handshake.js +505 -0
  262. package/dist/server-handshake.js.map +1 -0
  263. package/dist/server-response.d.ts +3 -0
  264. package/dist/server-response.d.ts.map +1 -0
  265. package/dist/server-response.js +67 -0
  266. package/dist/server-response.js.map +1 -0
  267. package/dist/server-trust.d.ts +14 -0
  268. package/dist/server-trust.d.ts.map +1 -0
  269. package/dist/server-trust.js +225 -0
  270. package/dist/server-trust.js.map +1 -0
  271. package/dist/setup-confirm.d.ts +3 -3
  272. package/dist/setup-confirm.d.ts.map +1 -0
  273. package/dist/setup-confirm.js +126 -2
  274. package/dist/setup-confirm.js.map +1 -0
  275. package/dist/setup.d.ts +5 -3
  276. package/dist/setup.d.ts.map +1 -0
  277. package/dist/setup.js +169 -46
  278. package/dist/setup.js.map +1 -0
  279. package/dist/shell-escape.d.ts.map +1 -0
  280. package/dist/shell-escape.js +22 -1
  281. package/dist/shell-escape.js.map +1 -0
  282. package/dist/spawn.d.ts +2 -2
  283. package/dist/spawn.d.ts.map +1 -0
  284. package/dist/spawn.js +29 -10
  285. package/dist/spawn.js.map +1 -0
  286. package/dist/startup-services.d.ts +0 -1
  287. package/dist/startup-services.d.ts.map +1 -0
  288. package/dist/startup-services.js +26 -1
  289. package/dist/startup-services.js.map +1 -0
  290. package/dist/stream-owner.d.ts +6 -2
  291. package/dist/stream-owner.d.ts.map +1 -0
  292. package/dist/stream-owner.js +547 -2
  293. package/dist/stream-owner.js.map +1 -0
  294. package/dist/stream-status.d.ts +1 -1
  295. package/dist/stream-status.d.ts.map +1 -0
  296. package/dist/stream-status.js +268 -3
  297. package/dist/stream-status.js.map +1 -0
  298. package/dist/sync-roles-render.d.ts.map +1 -0
  299. package/dist/sync-roles-render.js +118 -3
  300. package/dist/sync-roles-render.js.map +1 -0
  301. package/dist/sync.d.ts +1 -1
  302. package/dist/sync.d.ts.map +1 -0
  303. package/dist/sync.js +286 -22
  304. package/dist/sync.js.map +1 -0
  305. package/dist/terminal-title.d.ts.map +1 -0
  306. package/dist/terminal-title.js +68 -1
  307. package/dist/terminal-title.js.map +1 -0
  308. package/dist/token-store.d.ts +17 -111
  309. package/dist/token-store.d.ts.map +1 -0
  310. package/dist/token-store.js +81 -1
  311. package/dist/token-store.js.map +1 -0
  312. package/dist/tool-manifest.d.ts +4 -8
  313. package/dist/tool-manifest.d.ts.map +1 -0
  314. package/dist/tool-manifest.js +554 -1
  315. package/dist/tool-manifest.js.map +1 -0
  316. package/dist/tool-scope.d.ts +8 -12
  317. package/dist/tool-scope.d.ts.map +1 -0
  318. package/dist/tool-scope.js +97 -1
  319. package/dist/tool-scope.js.map +1 -0
  320. package/dist/types.d.ts +1 -5
  321. package/dist/types.d.ts.map +1 -0
  322. package/dist/types.js +5 -0
  323. package/dist/types.js.map +1 -0
  324. package/dist/unknown-subcommand.d.ts +1 -1
  325. package/dist/unknown-subcommand.d.ts.map +1 -0
  326. package/dist/unknown-subcommand.js +37 -1
  327. package/dist/unknown-subcommand.js.map +1 -0
  328. package/dist/version.d.ts +1 -1
  329. package/dist/version.d.ts.map +1 -0
  330. package/dist/version.js +78 -2
  331. package/dist/version.js.map +1 -0
  332. package/dist/working-repo.d.ts.map +1 -0
  333. package/dist/working-repo.js +81 -1
  334. package/dist/working-repo.js.map +1 -0
  335. package/dist/worktree-lifecycle.d.ts.map +1 -0
  336. package/dist/worktree-lifecycle.js +205 -2
  337. package/dist/worktree-lifecycle.js.map +1 -0
  338. package/docs/EXTRACTION_PROVENANCE.md +40 -0
  339. package/docs/LOCAL_SERVER.md +103 -0
  340. package/docs/RELEASING.md +120 -0
  341. package/package.json +52 -34
  342. package/src/agent-runtime.ts +69 -0
  343. package/src/assimilate-cmd.ts +1864 -0
  344. package/src/assimilate-deps.ts +444 -0
  345. package/src/assimilate-guard.ts +84 -0
  346. package/src/assimilate-welcome.ts +39 -0
  347. package/src/auth-env.ts +21 -0
  348. package/src/backends/launch-all-pastelist.ts +18 -0
  349. package/src/backends/launch-all-tmux.ts +75 -0
  350. package/src/backends/launch-all-windows.ts +110 -0
  351. package/src/bare-launch-menu.ts +148 -0
  352. package/src/claude-launch-args.ts +35 -0
  353. package/src/claude.ts +561 -0
  354. package/src/cleanup-cmd.ts +581 -0
  355. package/src/clear-rewake-core.ts +34 -0
  356. package/src/clear-rewake.ts +18 -0
  357. package/src/cli-help.ts +116 -0
  358. package/src/cli-platform.ts +127 -0
  359. package/src/cli-tool-approval.ts +716 -0
  360. package/src/codex-app-server.ts +238 -0
  361. package/src/codex-app-wake.ts +512 -0
  362. package/src/codex-launch.ts +140 -0
  363. package/src/codex-remote.ts +500 -0
  364. package/src/codex-wake-resolve.ts +155 -0
  365. package/src/config-utils.ts +811 -0
  366. package/src/config.ts +595 -0
  367. package/src/console-prefix.ts +99 -0
  368. package/src/cube-name.ts +69 -0
  369. package/src/cubes.ts +583 -0
  370. package/src/debug.ts +73 -0
  371. package/src/direct-log.ts +9 -0
  372. package/src/docs-sections.ts +129 -0
  373. package/src/drone-lifecycle.ts +62 -0
  374. package/src/drone-management.ts +162 -0
  375. package/src/ensure-mcp-config.ts +54 -0
  376. package/src/evict-drone.ts +82 -0
  377. package/src/gc-orphan-inboxes.ts +229 -0
  378. package/src/inbox-monitor.ts +1005 -0
  379. package/src/index.ts +1201 -0
  380. package/src/launch-all-cmd.ts +450 -0
  381. package/src/launch-all-command.ts +30 -0
  382. package/src/launch-all-deps.ts +188 -0
  383. package/src/launch-all-discovery.ts +118 -0
  384. package/src/launch-all-locks.ts +74 -0
  385. package/src/launch-gate.ts +80 -0
  386. package/src/lifecycle-log-guard.ts +144 -0
  387. package/src/list-roles-render.ts +52 -0
  388. package/src/local-log-routing.ts +106 -0
  389. package/src/local-manage-tool-result.ts +10 -0
  390. package/src/local-server-cursor.ts +188 -0
  391. package/src/log-audit.ts +207 -0
  392. package/src/log-stream.ts +1505 -0
  393. package/src/model-presets.ts +22 -0
  394. package/src/name-validator.ts +23 -0
  395. package/src/opencode-drone.ts +528 -0
  396. package/src/opencode-plugin.ts +38 -0
  397. package/src/parse-assimilate-args.ts +125 -0
  398. package/src/parse-launch-all-args.ts +100 -0
  399. package/src/postinstall-banner.ts +37 -0
  400. package/src/postinstall.ts +36 -0
  401. package/src/readiness-probe.ts +12 -0
  402. package/src/regen-format.ts +651 -0
  403. package/src/regen.ts +150 -0
  404. package/src/remote-client.ts +1379 -0
  405. package/src/reset-local-seat-cmd.ts +227 -0
  406. package/src/role-match.ts +29 -0
  407. package/src/role-resolver.ts +93 -0
  408. package/src/roster-render.ts +196 -0
  409. package/src/seat-probe.ts +115 -0
  410. package/src/seat-store.ts +333 -0
  411. package/src/seats.ts +741 -0
  412. package/src/self-path.ts +51 -0
  413. package/src/server-endpoint.ts +38 -0
  414. package/src/server-errors.ts +93 -0
  415. package/src/server-handshake.ts +783 -0
  416. package/src/server-response.ts +76 -0
  417. package/src/server-trust.ts +245 -0
  418. package/src/setup-confirm.ts +171 -0
  419. package/src/setup.ts +186 -0
  420. package/src/shell-escape.ts +22 -0
  421. package/src/spawn.ts +31 -0
  422. package/src/startup-services.ts +36 -0
  423. package/src/stream-owner.ts +686 -0
  424. package/src/stream-status.ts +337 -0
  425. package/src/sync-roles-render.ts +163 -0
  426. package/src/sync.ts +363 -0
  427. package/src/terminal-title.ts +76 -0
  428. package/src/token-store.ts +102 -0
  429. package/src/tool-manifest.ts +610 -0
  430. package/src/tool-scope.ts +110 -0
  431. package/src/types.ts +5 -0
  432. package/src/unknown-subcommand.ts +35 -0
  433. package/src/version.ts +82 -0
  434. package/src/working-repo.ts +99 -0
  435. package/src/worktree-lifecycle.ts +254 -0
  436. package/dist/auth-recovery.d.ts +0 -33
  437. package/dist/auth-recovery.js +0 -1
  438. package/dist/auth.d.ts +0 -103
  439. package/dist/auth.js +0 -39
  440. package/dist/device-auth.d.ts +0 -75
  441. package/dist/device-auth.js +0 -1
  442. package/dist/drone-address.d.ts +0 -26
  443. package/dist/drone-address.js +0 -1
  444. package/dist/get-started.d.ts +0 -20
  445. package/dist/get-started.js +0 -2
  446. package/dist/health-beat.d.ts +0 -134
  447. package/dist/health-beat.js +0 -1
  448. package/dist/role-section.d.ts +0 -110
  449. package/dist/role-section.js +0 -8
  450. package/dist/setup-action.d.ts +0 -20
  451. package/dist/setup-action.js +0 -1
  452. package/dist/stale-version-check.d.ts +0 -40
  453. package/dist/stale-version-check.js +0 -1
  454. package/dist/subscription-retry.d.ts +0 -40
  455. package/dist/subscription-retry.js +0 -1
  456. package/dist/templates.d.ts +0 -107
  457. package/dist/templates.js +0 -359
  458. package/dist/token-crypto.d.ts +0 -50
  459. package/dist/token-crypto.js +0 -1
@@ -0,0 +1,581 @@
1
+ /**
2
+ * `borg cleanup` — reap orphaned worktrees for evicted drones (gh#882).
3
+ *
4
+ * An evicted drone (manual `borg_evict-drone`, watchdog
5
+ * `autoEvictPresumedDead`, or gh#877 graceful self-shutdown) leaves its
6
+ * worktree dir (`~/.borg/worktrees/<repo>/<name>`) + its `wt-<suffix>`
7
+ * branch on disk. Nothing reclaims them. This command finds and SAFELY
8
+ * removes worktrees orphaned by eviction — never destroying live,
9
+ * dirty, unmerged, or precious-local-state work.
10
+ *
11
+ * Pure composition over `worktree-lifecycle.ts` + the gh#877 per-seat
12
+ * `410 DRONE_EVICTED` discrimination (via the seat-probe seam). All git +
13
+ * network + filesystem effects are behind injected deps so every branch is
14
+ * unit-testable without a live repo, network, or $HOME.
15
+ *
16
+ * SEC gate (gh#882, S1–S5 — design entries 98e45fbf / acd76794):
17
+ * S1 Destructive prune authority = server 410 DRONE_EVICTED on the
18
+ * worktree's OWN saved-seat token, ONLY.
19
+ * S2 KEEP classes (any one → never prune, surface): dirty; unmerged;
20
+ * non-regenerable gitignored-local.
21
+ * S3 Report-only until gh#877 is DEPLOYED. The destructive path keys on
22
+ * the 410 CODE — pre-deploy the server returns 401 → probe resolves
23
+ * 'indeterminate' → report-only → safe. No flag / deploy coupling;
24
+ * --prune auto-unlocks the day 410 ships.
25
+ * S4 (a) gitignored-aware clean-gate, DEFAULT-DENY: block on ANY
26
+ * gitignored-local present UNLESS it matches a curated REGENERABLE
27
+ * allowlist (NOT a secret denylist — that fails open). Layered here,
28
+ * NOT in the shared isCleanTree/classifyDirty (those feed
29
+ * assimilate/sync — widening them perturbs that path).
30
+ * (b) fetch origin/main BEFORE the isMerged gate.
31
+ * S5 Auto-prune strictly under canonicalized (realpath) worktreesHome —
32
+ * reject symlink-escape / `..`. Report MAY widen to legacy siblings
33
+ * outside worktreesHome (label "legacy — manual review"); report ≠ delete.
34
+ */
35
+
36
+ import { spawnSync } from 'node:child_process';
37
+ import { realpathSync } from 'node:fs';
38
+ import { sep, basename, dirname } from 'node:path';
39
+ import { homedir } from 'node:os';
40
+ import chalk from 'chalk';
41
+ import {
42
+ classifyDirty,
43
+ isMerged,
44
+ localBranchExists,
45
+ perWorktreeBranchName,
46
+ worktreesHome,
47
+ type RunSync,
48
+ } from './worktree-lifecycle.js';
49
+ import {
50
+ readAllProjectIdentities,
51
+ type ActiveCube,
52
+ } from './cubes.js';
53
+ import { defaultProbeSeat, type SeatStatus } from './seat-probe.js';
54
+
55
+ // ------------------------------------------------------------------
56
+ // Types
57
+ // ------------------------------------------------------------------
58
+
59
+ // SeatStatus + defaultProbeSeat were moved to seat-probe.ts so `borg launch-all`
60
+ // can reuse the probe without importing cleanup-cmd's chalk/report graph.
61
+ // Re-exported here so existing `import { SeatStatus } from './cleanup-cmd.js'`
62
+ // call sites + tests keep working unchanged.
63
+ export type { SeatStatus };
64
+
65
+ /** Per-worktree classification outcome. PRUNABLE is the ONLY delete class. */
66
+ export type CleanupReason =
67
+ | 'PRUNABLE'
68
+ | 'SURVIVES-dirty'
69
+ | 'SURVIVES-clobber'
70
+ | 'SURVIVES-unmerged'
71
+ | 'SURVIVES-detached'
72
+ | 'SURVIVES-rejected'
73
+ | 'SURVIVES-live'
74
+ | 'SURVIVES-self'
75
+ | 'UNKNOWN-indeterminate'
76
+ | 'UNKNOWN-no-seat'
77
+ | 'LEGACY-manual-review';
78
+
79
+ export interface CleanupRow {
80
+ worktreePath: string;
81
+ wtBranch: string | null;
82
+ reason: CleanupReason;
83
+ /** Extra human detail (e.g. the blocking file, or the probe outcome). */
84
+ detail?: string;
85
+ /** Set after a --prune attempt on a PRUNABLE row. */
86
+ prune?: 'removed' | 'remove-failed' | 'branch-delete-failed';
87
+ }
88
+
89
+ export interface CleanupDeps {
90
+ /** Synchronous subprocess runner — `git ...`. */
91
+ runSync?: RunSync;
92
+ /** Resolved $HOME (worktreesHome anchor). Injected for tests. */
93
+ homeDir?: () => string;
94
+ /** cwd of the invocation (a git repo sharing origin). Injected for tests. */
95
+ cwd?: () => string;
96
+ /** Enumerate borg-managed seats from the central cubes.json. */
97
+ listSeats?: () => Promise<Array<{ projectPath: string; cube: ActiveCube }>>;
98
+ /** Probe ONE seat's eviction status using ITS OWN token (gh#877). */
99
+ probeSeat?: (
100
+ sessionToken: string,
101
+ apiUrl: string,
102
+ serverTrustIdentity?: string,
103
+ ) => Promise<SeatStatus>;
104
+ /** realpath resolver — injected so tests can model symlink escape. */
105
+ realpath?: (p: string) => string;
106
+ stdout?: (line: string) => void;
107
+ stderr?: (line: string) => void;
108
+ }
109
+
110
+ export interface CleanupOptions {
111
+ /** Act on PRUNABLE rows. Default false = dry-run / report only. */
112
+ prune: boolean;
113
+ }
114
+
115
+ const DEFAULT_REF = 'origin/main';
116
+
117
+ // ------------------------------------------------------------------
118
+ // S4a — gitignored-aware clean gate (DEFAULT-DENY via regenerable allowlist)
119
+ // ------------------------------------------------------------------
120
+
121
+ /**
122
+ * Regenerable build/scratch artifacts that are SAFE to delete with the
123
+ * worktree (present in nearly every worktree; never precious). DEFAULT-DENY:
124
+ * an ignored path is "regenerable" ONLY if it matches this allowlist;
125
+ * EVERYTHING else (unknown ignored path) is assumed PRECIOUS and blocks the
126
+ * prune. A secret-denylist would fail OPEN on an unanticipated precious file
127
+ * (`data/`, `.dev.vars`, `credentials.json`, a private key) — so we allowlist
128
+ * the bounded disposable set instead (SEC S4a / B24 9e79b0b1).
129
+ */
130
+ const REGENERABLE_DIRS = new Set([
131
+ 'node_modules',
132
+ 'dist',
133
+ 'build',
134
+ '.next',
135
+ 'coverage',
136
+ '.wrangler',
137
+ '.playwright-mcp',
138
+ // gh#882 follow-up: `.claude/` is Claude Code's PER-WORKTREE scaffolding
139
+ // (session state, scheduled-task locks, and a `settings.local.json` of
140
+ // worktree-local permission grants). It is present in EVERY borg worktree, so
141
+ // without this entry the S4a default-deny gate flagged it precious and `borg
142
+ // cleanup` could NEVER prune ANY worktree (0 prunable, N kept). It is a
143
+ // DELIBERATE, narrow carve-out: the gate only ever runs on an already-EVICTED,
144
+ // clean, merged worktree (410 DRONE_EVICTED stays the SOLE delete authority —
145
+ // S1), and `.claude/` is recreated on the next assimilate; its only
146
+ // arguably-precious fragment (`settings.local.json`) is worktree-local, never
147
+ // carries to the parent, and dies with the dead seat. git collapses an ignored
148
+ // dir to a single `!! .claude/` porcelain line, so segment-matching `.claude`
149
+ // here covers root + nested `<sub>/.claude/` and is the only expressible
150
+ // granularity. A truly-precious sibling (`data/`, `credentials.json`,
151
+ // `.env.test`, `.dev.vars`) emits its OWN `!!` line, still fails this
152
+ // allowlist, and still → SURVIVES-clobber (T-GITIGNORED-DEFAULTDENY intact).
153
+ // RESIDUAL RISK (accepted): any file hand-placed DIRECTLY under `.claude/` in an
154
+ // evicted worktree is sacrificial and silently removed — `git worktree remove`
155
+ // without `--force` does NOT refuse on ignored content, and a gitignore negation
156
+ // (`!.claude/keep`) is inert once the parent is excluded, so the allowlist
157
+ // decision here is the SOLE guard. Today `.claude/` is exclusively Claude Code
158
+ // scaffolding; this comment makes the assumption auditable.
159
+ '.claude',
160
+ ]);
161
+ const REGENERABLE_FILES = new Set(['.DS_Store', 'worker-configuration.d.ts']);
162
+ const REGENERABLE_SUFFIXES = ['.log', '.tsbuildinfo', '.tmp'];
163
+
164
+ /** True iff a single gitignored path is a known-regenerable artifact. */
165
+ export function isRegenerableIgnored(ignoredPath: string): boolean {
166
+ const clean = ignoredPath.replace(/\/+$/, ''); // strip trailing slash
167
+ const segments = clean.split('/').filter((s) => s.length > 0);
168
+ if (segments.some((s) => REGENERABLE_DIRS.has(s))) return true;
169
+ const base = segments[segments.length - 1] ?? clean;
170
+ if (REGENERABLE_FILES.has(base)) return true;
171
+ if (REGENERABLE_SUFFIXES.some((suf) => base.endsWith(suf))) return true;
172
+ return false;
173
+ }
174
+
175
+ /**
176
+ * Return the gitignored-local paths in `cwd` that are NOT regenerable — i.e.
177
+ * the precious-local-state files (`.env`, `.env.test`, `.dev.vars`, secrets,
178
+ * `data/`, …) that a prune would destroy forever. Non-empty → BLOCK (S4a).
179
+ *
180
+ * `git status --porcelain --ignored` emits ignored entries with a `!!` status
181
+ * prefix; git collapses an ignored directory to a single `!! dir/` entry, so
182
+ * segment-matching the reported path against the allowlist is sufficient.
183
+ */
184
+ export function clobberClassIgnored(runSync: RunSync, cwd: string): string[] {
185
+ const r = runSync('git', ['status', '--porcelain', '--ignored'], cwd);
186
+ if (r.status !== 0) {
187
+ // Cannot determine ignored state → fail SAFE: treat as "has precious"
188
+ // so the worktree survives rather than risking a clobber.
189
+ return ['<git status --ignored failed — cannot verify clean>'];
190
+ }
191
+ const precious: string[] = [];
192
+ for (const line of r.stdout.split('\n')) {
193
+ if (!line.startsWith('!!')) continue;
194
+ const path = line.slice(3).trim();
195
+ if (path.length === 0) continue;
196
+ if (!isRegenerableIgnored(path)) precious.push(path);
197
+ }
198
+ return precious;
199
+ }
200
+
201
+ // ------------------------------------------------------------------
202
+ // Defaults
203
+ // ------------------------------------------------------------------
204
+
205
+ const defaultDeps: Required<CleanupDeps> = {
206
+ runSync: (cmd, args, cwd) => {
207
+ const r = spawnSync(cmd, args, { cwd, encoding: 'utf-8' });
208
+ return { status: r.status, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
209
+ },
210
+ homeDir: () => homedir(),
211
+ cwd: () => process.cwd(),
212
+ listSeats: () => readAllProjectIdentities(),
213
+ probeSeat: defaultProbeSeat,
214
+ realpath: (p) => realpathSync(p),
215
+ stdout: (line) => process.stdout.write(line),
216
+ stderr: (line) => process.stderr.write(line),
217
+ };
218
+
219
+ // (defaultProbeSeat now lives in seat-probe.ts — imported above.)
220
+
221
+ // ------------------------------------------------------------------
222
+ // Path anchoring (S5)
223
+ // ------------------------------------------------------------------
224
+
225
+ /**
226
+ * True iff `child`'s canonicalized real path is STRICTLY under `parent`'s
227
+ * canonicalized real path — rejecting `..` traversal AND symlink escape (a
228
+ * symlinked worktree whose realpath is outside worktreesHome is NOT a prune
229
+ * candidate). Returns false (never under) if either path can't be resolved.
230
+ */
231
+ export function isStrictlyUnder(
232
+ realpath: (p: string) => string,
233
+ parent: string,
234
+ child: string
235
+ ): boolean {
236
+ let rp: string;
237
+ let rc: string;
238
+ try {
239
+ rp = realpath(parent);
240
+ rc = realpath(child);
241
+ } catch {
242
+ return false;
243
+ }
244
+ if (rc === rp) return false; // the home dir itself is not "under" itself
245
+ const prefix = rp.endsWith(sep) ? rp : rp + sep;
246
+ return rc.startsWith(prefix);
247
+ }
248
+
249
+ // ------------------------------------------------------------------
250
+ // Enumeration
251
+ // ------------------------------------------------------------------
252
+
253
+ /** Parse `git worktree list --porcelain` → absolute worktree paths (in order). */
254
+ export function parseWorktreeList(porcelain: string): string[] {
255
+ return parseWorktreeEntries(porcelain).map((e) => e.path);
256
+ }
257
+
258
+ /**
259
+ * Parse `git worktree list --porcelain` into `{path, branch}` entries (in
260
+ * order; primary first). `branch` is the ACTUAL checked-out branch name (from
261
+ * the `branch refs/heads/<name>` line) or null for a detached HEAD (no branch
262
+ * line). This is load-bearing (gh#882 CR blocker dcdd7fca): a drone cuts
263
+ * `fix/…`/`feat/…` off `wt-<suffix>` INSIDE the worktree, so the checked-out
264
+ * HEAD is commonly a feature branch with unpushed commits while `wt-<suffix>`
265
+ * stays at the merged base. The merged-gate + `git branch -d` MUST key on the
266
+ * actual HEAD branch, not the derived `wt-<suffix>`, or unmerged feature work
267
+ * is misclassified PRUNABLE.
268
+ */
269
+ export function parseWorktreeEntries(
270
+ porcelain: string
271
+ ): Array<{ path: string; branch: string | null }> {
272
+ const entries: Array<{ path: string; branch: string | null }> = [];
273
+ let cur: { path: string; branch: string | null } | null = null;
274
+ for (const line of porcelain.split('\n')) {
275
+ if (line.startsWith('worktree ')) {
276
+ if (cur) entries.push(cur);
277
+ cur = { path: line.slice('worktree '.length).trim(), branch: null };
278
+ } else if (line.startsWith('branch ') && cur) {
279
+ // `branch refs/heads/<name>` → <name>. A detached worktree emits
280
+ // `detached` (or no branch line) → branch stays null.
281
+ cur.branch = line.slice('branch '.length).trim().replace(/^refs\/heads\//, '');
282
+ }
283
+ }
284
+ if (cur) entries.push(cur);
285
+ return entries;
286
+ }
287
+
288
+ // ------------------------------------------------------------------
289
+ // Classification (per worktree)
290
+ // ------------------------------------------------------------------
291
+
292
+ /**
293
+ * Classify ONE borg-managed worktree (cheap-first gate order; the FIRST
294
+ * KEEP-class short-circuits — a transient probe failure on a dirty/unmerged
295
+ * tree never even reaches the network). Only `PRUNABLE` authorizes a delete.
296
+ */
297
+ async function classifyWorktree(
298
+ deps: Required<CleanupDeps>,
299
+ worktreePath: string,
300
+ actualBranch: string | null,
301
+ seat: ActiveCube
302
+ ): Promise<{ reason: CleanupReason; detail?: string }> {
303
+ const { runSync } = deps;
304
+
305
+ // (1) dirty — tracked staged/unstaged/untracked. KEEP.
306
+ const dirty = classifyDirty(runSync, worktreePath);
307
+ const dirtyCount =
308
+ dirty.staged.length + dirty.unstaged.length + dirty.untracked.length;
309
+ if (dirtyCount > 0) {
310
+ return { reason: 'SURVIVES-dirty', detail: `${dirtyCount} uncommitted file(s)` };
311
+ }
312
+
313
+ // (2) S4a — gitignored precious-local present (the clobber class). KEEP.
314
+ const precious = clobberClassIgnored(runSync, worktreePath);
315
+ if (precious.length > 0) {
316
+ return {
317
+ reason: 'SURVIVES-clobber',
318
+ detail: `gitignored local state: ${precious.slice(0, 3).join(', ')}${precious.length > 3 ? ` (+${precious.length - 3})` : ''}`,
319
+ };
320
+ }
321
+
322
+ // (3) detached HEAD — no branch to verify merged-state against. Cannot
323
+ // prove the work is safely on origin/main → KEEP (gh#882 CR blocker).
324
+ if (actualBranch === null) {
325
+ return { reason: 'SURVIVES-detached', detail: 'detached HEAD — cannot verify merged' };
326
+ }
327
+
328
+ // (4) unmerged — the worktree's ACTUAL checked-out branch has commits not on
329
+ // origin/main (fetch already ran ONCE globally — S4b). This MUST key on the
330
+ // real HEAD branch, not the derived `wt-<suffix>`: a drone cuts feature
331
+ // branches off wt-<suffix> inside the worktree, so checking wt-<suffix>
332
+ // (which stays at the merged base) would miss unpushed feature work and
333
+ // misclassify it PRUNABLE (gh#882 CR blocker dcdd7fca). KEEP.
334
+ if (!isMerged(runSync, worktreePath, actualBranch, DEFAULT_REF)) {
335
+ return { reason: 'SURVIVES-unmerged', detail: `${actualBranch} not merged into ${DEFAULT_REF}` };
336
+ }
337
+
338
+ // (5) ONLY now probe eviction with the worktree's OWN seat token (S1).
339
+ const status = await deps.probeSeat(
340
+ seat.sessionToken,
341
+ seat.apiUrl,
342
+ seat.serverTrustIdentity,
343
+ );
344
+ switch (status) {
345
+ case 'evicted':
346
+ return { reason: 'PRUNABLE', detail: '410 DRONE_EVICTED (clean + merged)' };
347
+ case 'rejected':
348
+ return { reason: 'SURVIVES-rejected', detail: 'pin-matched 401 (revoked/taken over) — recoverable via re-enroll, never delete' };
349
+ case 'live':
350
+ return { reason: 'SURVIVES-live', detail: 'seat resolves (drone alive)' };
351
+ case 'indeterminate':
352
+ default:
353
+ return {
354
+ reason: 'UNKNOWN-indeterminate',
355
+ detail: 'probe returned network/transient/404/trust-mismatch (or gh#877 not yet deployed) — not deleting',
356
+ };
357
+ }
358
+ }
359
+
360
+ // ------------------------------------------------------------------
361
+ // Orchestrator
362
+ // ------------------------------------------------------------------
363
+
364
+ /**
365
+ * Build the cleanup report (read-only except the single up-front
366
+ * `git fetch origin`). Exported separately from the prune action so tests +
367
+ * the dry-run path share it.
368
+ */
369
+ export async function buildCleanupReport(
370
+ deps: Required<CleanupDeps>
371
+ ): Promise<{ rows: CleanupRow[]; error?: string }> {
372
+ const { runSync, realpath } = deps;
373
+ const cwd = deps.cwd();
374
+
375
+ if (runSync('git', ['rev-parse', '--show-toplevel'], cwd).status !== 0) {
376
+ return { rows: [], error: `not in a git repository (cwd: ${cwd})` };
377
+ }
378
+
379
+ // S4b — fetch origin ONCE up front so every per-worktree isMerged gate runs
380
+ // against a fresh origin/main (the ref is shared across linked worktrees).
381
+ // Fatal: a stale origin/main makes an unpushed wt-branch look merged.
382
+ const fetch = runSync('git', ['fetch', 'origin', '--prune'], cwd);
383
+ if (fetch.status !== 0) {
384
+ return { rows: [], error: `git fetch origin failed: ${fetch.stderr.trim()}` };
385
+ }
386
+
387
+ const wtList = runSync('git', ['worktree', 'list', '--porcelain'], cwd);
388
+ if (wtList.status !== 0) {
389
+ return { rows: [], error: `git worktree list failed: ${wtList.stderr.trim()}` };
390
+ }
391
+ const wtEntries = parseWorktreeEntries(wtList.stdout);
392
+ const branchByPath = new Map(wtEntries.map((e) => [e.path, e.branch]));
393
+ const allWorktrees = wtEntries.map((e) => e.path);
394
+ const mainDir = allWorktrees[0]; // porcelain lists the primary worktree first
395
+ const home = deps.homeDir();
396
+ const wtHome = worktreesHome(home);
397
+
398
+ // Self + primary are never orphan candidates.
399
+ const selfReal = safeRealpath(realpath, cwd);
400
+ const mainReal = mainDir ? safeRealpath(realpath, mainDir) : null;
401
+
402
+ // Seat lookup keyed by realpath(projectPath) so symlinked/relative entries
403
+ // still match the enumerated worktree path.
404
+ const seats = await deps.listSeats();
405
+ const seatByReal = new Map<string, ActiveCube>();
406
+ for (const { projectPath, cube } of seats) {
407
+ const real = safeRealpath(realpath, projectPath);
408
+ if (real) seatByReal.set(real, cube);
409
+ }
410
+
411
+ const rows: CleanupRow[] = [];
412
+ for (const wt of allWorktrees) {
413
+ const real = safeRealpath(realpath, wt);
414
+ if (!real) continue; // gone from disk — git will report it as prunable itself
415
+ if (mainReal && real === mainReal) continue; // primary worktree — never a candidate
416
+ const seat = seatByReal.get(real);
417
+ const under = isStrictlyUnder(realpath, wtHome, wt);
418
+
419
+ // Self (the running worktree): surface but never prune (can't remove the
420
+ // current worktree; the seat probe would also report it live).
421
+ if (real === selfReal) {
422
+ rows.push({ worktreePath: wt, wtBranch: null, reason: 'SURVIVES-self', detail: 'current worktree' });
423
+ continue;
424
+ }
425
+
426
+ if (!under) {
427
+ // Outside worktreesHome. A seat'd worktree here is a legacy pre-gh#556
428
+ // sibling → REPORT for manual review, never auto-prune (S5). No seat →
429
+ // an arbitrary user worktree we never touch or list.
430
+ if (seat) {
431
+ rows.push({ worktreePath: wt, wtBranch: null, reason: 'LEGACY-manual-review', detail: 'borg seat outside worktreesHome (pre-gh#556 sibling)' });
432
+ }
433
+ continue;
434
+ }
435
+
436
+ // Under worktreesHome but no saved seat → not eviction-orphaned
437
+ // (pre-assimilate or hand-made). Report, never auto-prune.
438
+ if (!seat) {
439
+ rows.push({ worktreePath: wt, wtBranch: null, reason: 'UNKNOWN-no-seat', detail: 'no saved seat — manual review' });
440
+ continue;
441
+ }
442
+
443
+ // Use the worktree's ACTUAL checked-out HEAD branch (from porcelain) for
444
+ // BOTH the merged-gate and the eventual `git branch -d` — NOT the derived
445
+ // `wt-<suffix>` (gh#882 CR blocker dcdd7fca). null = detached HEAD → KEEP.
446
+ const actualBranch = branchByPath.get(wt) ?? null;
447
+ const { reason, detail } = await classifyWorktree(deps, wt, actualBranch, seat);
448
+ rows.push({ worktreePath: wt, wtBranch: actualBranch, reason, detail });
449
+ }
450
+
451
+ return { rows };
452
+ }
453
+
454
+ function safeRealpath(realpath: (p: string) => string, p: string): string | null {
455
+ try {
456
+ return realpath(p);
457
+ } catch {
458
+ return null;
459
+ }
460
+ }
461
+
462
+ /**
463
+ * `borg cleanup [--prune]` entry point. Returns the process exit code.
464
+ * Dry-run (default): print the report, delete nothing. `--prune`: additionally
465
+ * remove PRUNABLE worktrees via `git worktree remove` (NO --force) +
466
+ * `git branch -d` (NO -D) — git's own refusals are the final backstop.
467
+ */
468
+ export async function runCleanup(
469
+ deps: CleanupDeps = {},
470
+ opts: CleanupOptions = { prune: false }
471
+ ): Promise<number> {
472
+ const merged: Required<CleanupDeps> = { ...defaultDeps, ...deps };
473
+ const { stdout, stderr, runSync } = merged;
474
+
475
+ const { rows, error } = await buildCleanupReport(merged);
476
+ if (error) {
477
+ stderr(chalk.red(`◼ borg cleanup: ${error}\n`));
478
+ return 1;
479
+ }
480
+
481
+ const prunable = rows.filter((r) => r.reason === 'PRUNABLE');
482
+ const survivors = rows.filter((r) => r.reason !== 'PRUNABLE');
483
+
484
+ // --- Report (always) ---
485
+ if (rows.length === 0) {
486
+ stdout(chalk.blue('◼ borg cleanup: no borg-managed worktrees found.\n'));
487
+ return 0;
488
+ }
489
+ stdout(chalk.bold('◼ borg cleanup report:\n'));
490
+ for (const r of rows) {
491
+ const tag = r.reason === 'PRUNABLE' ? chalk.yellow(r.reason) : chalk.gray(r.reason);
492
+ stdout(` ${tag} ${r.worktreePath}${r.detail ? chalk.gray(` — ${r.detail}`) : ''}\n`);
493
+ }
494
+ stdout(chalk.gray(`◼ ${prunable.length} prunable, ${survivors.length} kept.\n`));
495
+
496
+ if (!opts.prune) {
497
+ if (prunable.length > 0) {
498
+ stdout(chalk.gray('◼ Dry-run — nothing deleted. Re-run with `--prune` to remove the PRUNABLE worktree(s).\n'));
499
+ }
500
+ return 0;
501
+ }
502
+
503
+ // --- Prune (PRUNABLE only) ---
504
+ if (prunable.length === 0) {
505
+ stdout(chalk.blue('◼ Nothing to prune.\n'));
506
+ return 0;
507
+ }
508
+ let removed = 0;
509
+ let failed = 0;
510
+ for (const r of prunable) {
511
+ // git worktree remove WITHOUT --force (refuses a dirty/locked worktree —
512
+ // backstop even past our gates).
513
+ const rm = runSync('git', ['worktree', 'remove', r.worktreePath], merged.cwd());
514
+ if (rm.status !== 0) {
515
+ r.prune = 'remove-failed';
516
+ failed++;
517
+ stderr(chalk.red(` ✗ worktree remove ${r.worktreePath}: ${rm.stderr.trim()}\n`));
518
+ continue;
519
+ }
520
+ // git branch -d (NOT -D) — refuses an unmerged branch (backstop).
521
+ if (r.wtBranch) {
522
+ const bd = runSync('git', ['branch', '-d', r.wtBranch], merged.cwd());
523
+ if (bd.status !== 0) {
524
+ r.prune = 'branch-delete-failed';
525
+ stderr(chalk.yellow(` ⚠ removed worktree but \`git branch -d ${r.wtBranch}\` refused: ${bd.stderr.trim()}\n`));
526
+ removed++;
527
+ continue;
528
+ }
529
+ }
530
+ // gh#884: r.wtBranch is the worktree's ACTUAL checked-out HEAD (often a
531
+ // feature branch the drone cut). The DERIVED per-worktree base branch
532
+ // `wt-<suffix>` — created by `borg assimilate`, the base feature branches
533
+ // were cut from — is a SEPARATE ref left dangling once the worktree is
534
+ // gone. Prune it too when it still exists and is merged into origin/main
535
+ // (skip when it IS r.wtBranch, already deleted above). `git branch -d`
536
+ // (NOT -D) backstops the merged + not-checked-out guards.
537
+ let baseNote = '';
538
+ const derivedWtBranch = perWorktreeBranchName(
539
+ basename(r.worktreePath),
540
+ basename(dirname(r.worktreePath))
541
+ );
542
+ if (
543
+ derivedWtBranch !== r.wtBranch &&
544
+ localBranchExists(runSync, merged.cwd(), derivedWtBranch) &&
545
+ isMerged(runSync, merged.cwd(), derivedWtBranch, DEFAULT_REF)
546
+ ) {
547
+ const bdBase = runSync('git', ['branch', '-d', derivedWtBranch], merged.cwd());
548
+ if (bdBase.status === 0) {
549
+ baseNote = ` + base ${derivedWtBranch}`;
550
+ } else {
551
+ stderr(chalk.yellow(` ⚠ left dangling base branch ${derivedWtBranch}: \`git branch -d\` refused: ${bdBase.stderr.trim()}\n`));
552
+ }
553
+ }
554
+ r.prune = 'removed';
555
+ removed++;
556
+ stdout(chalk.blue(` ✓ pruned ${r.worktreePath}${r.wtBranch ? ` + branch ${r.wtBranch}` : ''}${baseNote}\n`));
557
+ }
558
+ stdout(chalk.gray(`◼ Pruned ${removed} worktree(s)${failed > 0 ? `, ${failed} failed` : ''}.\n`));
559
+ return failed > 0 ? 1 : 0;
560
+ }
561
+
562
+ // ------------------------------------------------------------------
563
+ // CLI surface
564
+ // ------------------------------------------------------------------
565
+
566
+ export type ParseResult =
567
+ | { ok: true; options: CleanupOptions }
568
+ | { ok: false; error: string };
569
+
570
+ /** Parse args after `borg cleanup`. Supports `--prune`; rejects anything else. */
571
+ export function parseCleanupArgs(rawArgs: string[]): ParseResult {
572
+ let prune = false;
573
+ for (const arg of rawArgs) {
574
+ if (arg === '--prune') {
575
+ prune = true;
576
+ } else {
577
+ return { ok: false, error: `unexpected argument: ${arg}. Usage: borg cleanup [--prune]` };
578
+ }
579
+ }
580
+ return { ok: true, options: { prune } };
581
+ }
@@ -0,0 +1,34 @@
1
+ import { isBorgSession } from './launch-gate.js';
2
+
3
+ export const CLEAR_REWAKE_REMINDER =
4
+ 'Post-/clear recovery: full regen, drain the unread log; handle actionable cube entries, otherwise resume the work from before this wake.';
5
+
6
+ export interface ClearRewakeResult {
7
+ exitCode: 0 | 2;
8
+ stderr: string;
9
+ }
10
+
11
+ /**
12
+ * Resolve the Claude `/clear` async-rewake response without consulting cube
13
+ * state. The reminder is deliberately static: hook input can contain local
14
+ * paths and session metadata, none of which may cross into stderr.
15
+ */
16
+ export function evaluateClearRewake(
17
+ raw: string,
18
+ env: NodeJS.ProcessEnv = process.env
19
+ ): ClearRewakeResult {
20
+ if (!isBorgSession(env)) return { exitCode: 0, stderr: '' };
21
+
22
+ let input: any;
23
+ try {
24
+ input = raw.trim() ? JSON.parse(raw) : null;
25
+ } catch {
26
+ return { exitCode: 0, stderr: '' };
27
+ }
28
+
29
+ if (input?.hook_event_name !== 'SessionStart' || input?.source !== 'clear') {
30
+ return { exitCode: 0, stderr: '' };
31
+ }
32
+
33
+ return { exitCode: 2, stderr: `${CLEAR_REWAKE_REMINDER}\n` };
34
+ }
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { evaluateClearRewake } from './clear-rewake-core.js';
4
+
5
+ async function readStdin(): Promise<string> {
6
+ if (process.stdin.isTTY) return '';
7
+ try {
8
+ const chunks: Buffer[] = [];
9
+ for await (const chunk of process.stdin) chunks.push(chunk as Buffer);
10
+ return Buffer.concat(chunks).toString('utf-8');
11
+ } catch {
12
+ return '';
13
+ }
14
+ }
15
+
16
+ const result = evaluateClearRewake(await readStdin());
17
+ if (result.stderr) process.stderr.write(result.stderr);
18
+ process.exitCode = result.exitCode;