@fitlab-ai/agent-infra 0.9.13 → 0.9.15

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 (428) hide show
  1. package/README.md +10 -0
  2. package/README.zh-CN.md +10 -0
  3. package/bin/cli.ts +26 -9
  4. package/bin/internal-cli.sh +45 -0
  5. package/dist/bin/cli.js +173 -157
  6. package/dist/bin/internal-cli.js +126 -42
  7. package/dist/lib/agent-client.js +5 -0
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -2
  10. package/dist/lib/defaults.json +103 -103
  11. package/dist/lib/fs/durable-write.js +37 -0
  12. package/dist/lib/fs/file-lock.js +111 -0
  13. package/dist/lib/host-control/audit.js +19 -0
  14. package/dist/lib/host-control/client.js +142 -0
  15. package/dist/lib/host-control/command.js +49 -0
  16. package/dist/lib/host-control/path.js +160 -0
  17. package/dist/lib/host-control/secure-fs.js +140 -0
  18. package/dist/lib/host-control/server.js +159 -0
  19. package/dist/lib/host-control/service.js +112 -0
  20. package/dist/lib/init.js +11 -2
  21. package/dist/lib/internal/agent-client.js +5 -2
  22. package/dist/lib/internal/cli-route-inventory.js +154 -0
  23. package/dist/lib/internal/codex-lifecycle.js +15 -12
  24. package/dist/lib/internal/codex-sandbox-controller.js +8 -2
  25. package/dist/lib/internal/demo-promotion.js +322 -0
  26. package/dist/lib/internal/demo-transcript.js +454 -0
  27. package/dist/lib/internal/git-workflow.js +10 -6
  28. package/dist/lib/internal/host-control.js +28 -0
  29. package/dist/lib/internal/manual-validation.js +322 -0
  30. package/dist/lib/internal/platform-checks.js +16 -4
  31. package/dist/lib/internal/platform-comment.js +16 -4
  32. package/dist/lib/internal/platform-context.js +4 -1
  33. package/dist/lib/internal/platform-issue.js +12 -4
  34. package/dist/lib/internal/platform-metadata.js +88 -0
  35. package/dist/lib/internal/platform-pr-review.js +16 -3
  36. package/dist/lib/internal/platform-pr.js +42 -14
  37. package/dist/lib/internal/platform-release-notes.js +6 -3
  38. package/dist/lib/internal/platform-security.js +93 -0
  39. package/dist/lib/internal/pr-review-grade.js +14 -3
  40. package/dist/lib/internal/release-workflow.js +82 -89
  41. package/dist/lib/internal/sandbox-control.js +171 -7
  42. package/dist/lib/internal/task-activity.js +13 -4
  43. package/dist/lib/internal/task-artifact.js +19 -107
  44. package/dist/lib/internal/task-context.js +4 -1
  45. package/dist/lib/internal/task-create.js +48 -14
  46. package/dist/lib/internal/task-delivery.js +4 -1
  47. package/dist/lib/internal/task-event.js +38 -35
  48. package/dist/lib/internal/task-finalization.js +4 -1
  49. package/dist/lib/internal/task-invalidation.js +4 -1
  50. package/dist/lib/internal/task-ledger.js +12 -1
  51. package/dist/lib/internal/task-lifecycle.js +4 -1
  52. package/dist/lib/internal/task-operation-registry.js +519 -0
  53. package/dist/lib/internal/task-orchestration.js +12 -0
  54. package/dist/lib/internal/task-override.js +10 -3
  55. package/dist/lib/internal/task-qualification.js +90 -0
  56. package/dist/lib/internal/task-review.js +10 -52
  57. package/dist/lib/internal/task-short-id.js +11 -1
  58. package/dist/lib/internal/task-snapshot.js +4 -1
  59. package/dist/lib/internal/task-validate.js +37 -12
  60. package/dist/lib/internal/task-verify.js +5 -1
  61. package/dist/lib/internal/task-warning.js +8 -1
  62. package/dist/lib/owned-path.js +33 -0
  63. package/dist/lib/platform/check-status.js +11 -0
  64. package/dist/lib/platform/comment-safety.js +229 -0
  65. package/dist/lib/platform/completion-backfill.js +1 -1
  66. package/dist/lib/platform/github-data.js +241 -0
  67. package/dist/lib/platform/github-provider.js +248 -26
  68. package/dist/lib/platform/issue-comments.js +146 -34
  69. package/dist/lib/platform/issues.js +1 -179
  70. package/dist/lib/platform/pr-change-report.js +6 -12
  71. package/dist/lib/platform/pr-checks.js +12 -65
  72. package/dist/lib/platform/pr-summary.js +83 -10
  73. package/dist/lib/platform/provider-contract.d.ts +67 -1
  74. package/dist/lib/platform/provider-contract.js +6 -2
  75. package/dist/lib/platform/provider-validation.js +76 -3
  76. package/dist/lib/platform/pull-requests.js +117 -275
  77. package/dist/lib/platform/repository-metadata.js +377 -0
  78. package/dist/lib/platform/security-alerts.js +155 -0
  79. package/dist/lib/platform/snapshots.js +2 -0
  80. package/dist/lib/platform/verification-sync.js +154 -162
  81. package/dist/lib/process-data/index.js +7 -0
  82. package/dist/lib/release/workflow.js +1 -1
  83. package/dist/lib/run/index.js +5 -0
  84. package/dist/lib/sandbox/commands/control-status.js +22 -0
  85. package/dist/lib/sandbox/commands/create.js +39 -8
  86. package/dist/lib/sandbox/commands/ls.js +5 -0
  87. package/dist/lib/sandbox/commands/rm.js +3 -807
  88. package/dist/lib/sandbox/commands/show.js +16 -0
  89. package/dist/lib/sandbox/control/audit.js +203 -0
  90. package/dist/lib/sandbox/control/client.js +125 -12
  91. package/dist/lib/sandbox/control/completed-reentry.js +85 -0
  92. package/dist/lib/sandbox/control/container-identity.js +28 -31
  93. package/dist/lib/sandbox/control/executor.js +233 -34
  94. package/dist/lib/sandbox/control/finalization-response.js +27 -0
  95. package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
  96. package/dist/lib/sandbox/control/lifecycle.js +656 -128
  97. package/dist/lib/sandbox/control/native-file-lock.js +85 -0
  98. package/dist/lib/sandbox/control/protocol.js +55 -4
  99. package/dist/lib/sandbox/control/server.js +720 -79
  100. package/dist/lib/sandbox/control/state.js +63 -3
  101. package/dist/lib/sandbox/control/task-view.js +183 -0
  102. package/dist/lib/sandbox/control/task-workflow.js +74 -0
  103. package/dist/lib/sandbox/control/workflow-executor.js +99 -0
  104. package/dist/lib/sandbox/engines/authority.js +289 -0
  105. package/dist/lib/sandbox/engines/docker-exact-discovery.js +97 -0
  106. package/dist/lib/sandbox/index.js +7 -2
  107. package/dist/lib/sandbox/recovery.js +49 -5
  108. package/dist/lib/sandbox/removal.js +1717 -0
  109. package/dist/lib/sandbox/task-cleanup.js +217 -0
  110. package/dist/lib/sandbox/workspace-view.js +15 -15
  111. package/dist/lib/sandbox/worktree-safety.js +4 -1
  112. package/dist/lib/server/index.js +8 -0
  113. package/dist/lib/task/activity-intent.js +2 -1
  114. package/dist/lib/task/activity-log.js +30 -24
  115. package/dist/lib/task/artifact-command.js +101 -0
  116. package/dist/lib/task/artifact-lifecycle.js +71 -40
  117. package/dist/lib/task/artifact-name.js +36 -0
  118. package/dist/lib/task/artifact-operations.js +474 -0
  119. package/dist/lib/task/artifact-receipts.js +3 -15
  120. package/dist/lib/task/artifact-repair-intent.js +130 -0
  121. package/dist/lib/task/artifact-schema.js +127 -0
  122. package/dist/lib/task/capabilities.js +83 -19
  123. package/dist/lib/task/codex-orchestration.js +71 -25
  124. package/dist/lib/task/commit-identity.js +28 -0
  125. package/dist/lib/task/commit-operation.js +2 -16
  126. package/dist/lib/task/control-authority.js +375 -5
  127. package/dist/lib/task/control-recovery.js +128 -0
  128. package/dist/lib/task/create-service.js +103 -4
  129. package/dist/lib/task/create.js +80 -2
  130. package/dist/lib/task/decision-details.js +2 -46
  131. package/dist/lib/task/events.js +306 -76
  132. package/dist/lib/task/finalization-state.js +9 -0
  133. package/dist/lib/task/human-override.js +3 -6
  134. package/dist/lib/task/implementation-inputs.js +3 -3
  135. package/dist/lib/task/index.js +11 -3
  136. package/dist/lib/task/intermediate-cleanup.js +383 -0
  137. package/dist/lib/task/invalidation.js +18 -65
  138. package/dist/lib/task/ledger-intents.js +7 -22
  139. package/dist/lib/task/ledger.js +1 -3
  140. package/dist/lib/task/lifecycle.js +19 -1
  141. package/dist/lib/task/local-artifact-finalization.js +190 -290
  142. package/dist/lib/task/manual-validation-completion.js +26 -0
  143. package/dist/lib/task/manual-validation-evidence.js +130 -0
  144. package/dist/lib/task/manual-validation-receipt.js +101 -0
  145. package/dist/lib/task/manual-validation-shared.js +40 -0
  146. package/dist/lib/task/manual-validation-transaction.js +243 -0
  147. package/dist/lib/task/markdown.js +47 -0
  148. package/dist/lib/task/orchestration.js +206 -69
  149. package/dist/lib/task/qualification-audit.js +476 -0
  150. package/dist/lib/task/qualification-confirmation.js +109 -0
  151. package/dist/lib/task/qualification-intents.js +148 -0
  152. package/dist/lib/task/review-artifacts.js +23 -40
  153. package/dist/lib/task/review-command.js +25 -0
  154. package/dist/lib/task/review-finalization.js +146 -97
  155. package/dist/lib/task/review-fingerprint.js +2 -2
  156. package/dist/lib/task/sections.js +34 -54
  157. package/dist/lib/task/verification-config.js +11 -0
  158. package/dist/lib/task/verification-engine.js +170 -327
  159. package/dist/lib/task/verification.js +1 -1
  160. package/dist/lib/task/workflow-command.js +17 -0
  161. package/dist/lib/task/workflow-warning-intents.js +0 -16
  162. package/dist/lib/task/workflow-warnings.js +28 -2
  163. package/dist/lib/update.js +2 -0
  164. package/dist/lifecycle-build-manifest.json +3 -3
  165. package/lib/agent-client.ts +9 -0
  166. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
  167. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -2
  168. package/lib/fs/durable-write.ts +29 -0
  169. package/lib/fs/file-lock.ts +113 -0
  170. package/lib/host-control/audit.ts +22 -0
  171. package/lib/host-control/client.ts +172 -0
  172. package/lib/host-control/command.ts +60 -0
  173. package/lib/host-control/path.ts +167 -0
  174. package/lib/host-control/secure-fs.ts +135 -0
  175. package/lib/host-control/server.ts +180 -0
  176. package/lib/host-control/service.ts +133 -0
  177. package/lib/init.ts +15 -2
  178. package/lib/internal/agent-client.ts +4 -2
  179. package/lib/internal/cli-route-inventory.ts +142 -0
  180. package/lib/internal/codex-lifecycle.ts +14 -12
  181. package/lib/internal/codex-sandbox-controller.ts +7 -2
  182. package/lib/internal/demo-promotion.ts +354 -0
  183. package/lib/internal/demo-transcript.ts +419 -0
  184. package/lib/internal/git-workflow.ts +9 -6
  185. package/lib/internal/host-control.ts +28 -0
  186. package/lib/internal/manual-validation.ts +306 -0
  187. package/lib/internal/platform-checks.ts +12 -4
  188. package/lib/internal/platform-comment.ts +12 -4
  189. package/lib/internal/platform-context.ts +3 -1
  190. package/lib/internal/platform-issue.ts +11 -4
  191. package/lib/internal/platform-metadata.ts +70 -0
  192. package/lib/internal/platform-pr-review.ts +14 -3
  193. package/lib/internal/platform-pr.ts +38 -14
  194. package/lib/internal/platform-release-notes.ts +5 -3
  195. package/lib/internal/platform-security.ts +83 -0
  196. package/lib/internal/pr-review-grade.ts +10 -3
  197. package/lib/internal/release-workflow.ts +91 -87
  198. package/lib/internal/sandbox-control.ts +181 -6
  199. package/lib/internal/task-activity.ts +12 -4
  200. package/lib/internal/task-artifact.ts +15 -79
  201. package/lib/internal/task-context.ts +3 -1
  202. package/lib/internal/task-create.ts +76 -15
  203. package/lib/internal/task-delivery.ts +3 -1
  204. package/lib/internal/task-event.ts +21 -10
  205. package/lib/internal/task-finalization.ts +3 -1
  206. package/lib/internal/task-invalidation.ts +3 -1
  207. package/lib/internal/task-ledger.ts +11 -1
  208. package/lib/internal/task-lifecycle.ts +3 -1
  209. package/lib/internal/task-operation-registry.ts +590 -0
  210. package/lib/internal/task-orchestration.ts +12 -0
  211. package/lib/internal/task-override.ts +9 -3
  212. package/lib/internal/task-qualification.ts +61 -0
  213. package/lib/internal/task-review.ts +7 -50
  214. package/lib/internal/task-short-id.ts +10 -1
  215. package/lib/internal/task-snapshot.ts +3 -1
  216. package/lib/internal/task-validate.ts +32 -12
  217. package/lib/internal/task-verify.ts +4 -1
  218. package/lib/internal/task-warning.ts +7 -1
  219. package/lib/owned-path.ts +26 -0
  220. package/lib/platform/check-status.ts +9 -0
  221. package/lib/platform/comment-safety.ts +255 -0
  222. package/lib/platform/completion-backfill.ts +1 -1
  223. package/lib/platform/github-data.ts +322 -0
  224. package/lib/platform/github-provider.ts +244 -27
  225. package/lib/platform/issue-comments.ts +159 -35
  226. package/lib/platform/issues.ts +4 -245
  227. package/lib/platform/pr-change-report.ts +6 -12
  228. package/lib/platform/pr-checks.ts +13 -77
  229. package/lib/platform/pr-summary.ts +88 -11
  230. package/lib/platform/provider-contract.ts +78 -3
  231. package/lib/platform/provider-validation.ts +85 -3
  232. package/lib/platform/pull-requests.ts +129 -348
  233. package/lib/platform/repository-metadata.ts +453 -0
  234. package/lib/platform/security-alerts.ts +191 -0
  235. package/lib/platform/snapshots.ts +53 -0
  236. package/lib/platform/verification-sync.ts +171 -177
  237. package/lib/process-data/index.ts +7 -0
  238. package/lib/release/workflow.ts +3 -3
  239. package/lib/run/index.ts +5 -0
  240. package/lib/sandbox/commands/control-status.ts +26 -0
  241. package/lib/sandbox/commands/create.ts +53 -12
  242. package/lib/sandbox/commands/ls.ts +7 -0
  243. package/lib/sandbox/commands/rm.ts +3 -996
  244. package/lib/sandbox/commands/show.ts +16 -0
  245. package/lib/sandbox/control/audit.ts +253 -0
  246. package/lib/sandbox/control/client.ts +150 -13
  247. package/lib/sandbox/control/completed-reentry.ts +98 -0
  248. package/lib/sandbox/control/container-identity.ts +27 -43
  249. package/lib/sandbox/control/executor.ts +262 -37
  250. package/lib/sandbox/control/finalization-response.ts +29 -0
  251. package/lib/sandbox/control/identity-sentinel.ts +137 -0
  252. package/lib/sandbox/control/lifecycle.ts +672 -18
  253. package/lib/sandbox/control/native-file-lock.ts +108 -0
  254. package/lib/sandbox/control/protocol.ts +80 -8
  255. package/lib/sandbox/control/server.ts +779 -82
  256. package/lib/sandbox/control/state.ts +84 -3
  257. package/lib/sandbox/control/task-view.ts +240 -0
  258. package/lib/sandbox/control/task-workflow.ts +95 -0
  259. package/lib/sandbox/control/workflow-executor.ts +102 -0
  260. package/lib/sandbox/engines/authority.ts +365 -0
  261. package/lib/sandbox/engines/docker-exact-discovery.ts +132 -0
  262. package/lib/sandbox/index.ts +9 -2
  263. package/lib/sandbox/recovery.ts +49 -6
  264. package/lib/sandbox/removal.ts +2144 -0
  265. package/lib/sandbox/task-cleanup.ts +209 -0
  266. package/lib/sandbox/workspace-view.ts +26 -18
  267. package/lib/sandbox/worktree-safety.ts +4 -1
  268. package/lib/server/index.ts +10 -0
  269. package/lib/task/activity-intent.ts +2 -2
  270. package/lib/task/activity-log.ts +27 -19
  271. package/lib/task/artifact-command.ts +104 -0
  272. package/lib/task/artifact-lifecycle.ts +66 -39
  273. package/lib/task/artifact-name.ts +55 -0
  274. package/lib/task/artifact-operations.ts +580 -0
  275. package/lib/task/artifact-receipts.ts +3 -14
  276. package/lib/task/artifact-repair-intent.ts +152 -0
  277. package/lib/task/artifact-schema.ts +163 -0
  278. package/lib/task/capabilities.ts +82 -22
  279. package/lib/task/codex-orchestration.ts +85 -24
  280. package/lib/task/commit-identity.ts +31 -0
  281. package/lib/task/commit-operation.ts +2 -16
  282. package/lib/task/control-authority.ts +513 -6
  283. package/lib/task/control-recovery.ts +174 -0
  284. package/lib/task/create-service.ts +131 -6
  285. package/lib/task/create.ts +83 -1
  286. package/lib/task/decision-details.ts +3 -71
  287. package/lib/task/events.ts +292 -75
  288. package/lib/task/finalization-state.ts +11 -0
  289. package/lib/task/human-override.ts +3 -8
  290. package/lib/task/implementation-inputs.ts +3 -3
  291. package/lib/task/index.ts +16 -3
  292. package/lib/task/intermediate-cleanup.ts +455 -0
  293. package/lib/task/invalidation.ts +18 -62
  294. package/lib/task/ledger-intents.ts +7 -24
  295. package/lib/task/ledger.ts +1 -3
  296. package/lib/task/lifecycle.ts +17 -1
  297. package/lib/task/local-artifact-finalization.ts +223 -388
  298. package/lib/task/manual-validation-completion.ts +40 -0
  299. package/lib/task/manual-validation-evidence.ts +209 -0
  300. package/lib/task/manual-validation-receipt.ts +152 -0
  301. package/lib/task/manual-validation-shared.ts +50 -0
  302. package/lib/task/manual-validation-transaction.ts +304 -0
  303. package/lib/task/markdown.ts +70 -0
  304. package/lib/task/orchestration.ts +191 -68
  305. package/lib/task/qualification-audit.ts +588 -0
  306. package/lib/task/qualification-confirmation.ts +117 -0
  307. package/lib/task/qualification-intents.ts +150 -0
  308. package/lib/task/review-artifacts.ts +21 -41
  309. package/lib/task/review-command.ts +23 -0
  310. package/lib/task/review-finalization.ts +154 -153
  311. package/lib/task/review-fingerprint.ts +2 -2
  312. package/lib/task/sections.ts +33 -52
  313. package/lib/task/verification-config.ts +12 -0
  314. package/lib/task/verification-engine.ts +165 -370
  315. package/lib/task/verification-types.ts +17 -0
  316. package/lib/task/verification.ts +1 -1
  317. package/lib/task/workflow-command.ts +18 -0
  318. package/lib/task/workflow-warning-intents.ts +0 -13
  319. package/lib/task/workflow-warnings.ts +21 -2
  320. package/lib/update.ts +6 -0
  321. package/package.json +7 -2
  322. package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
  323. package/templates/.agents/rules/README.en.md +2 -0
  324. package/templates/.agents/rules/README.zh-CN.md +2 -0
  325. package/templates/.agents/rules/decision-qualification.en.md +22 -0
  326. package/templates/.agents/rules/decision-qualification.zh-CN.md +22 -0
  327. package/templates/.agents/rules/evidence-reporting.en.md +28 -0
  328. package/templates/.agents/rules/evidence-reporting.zh-CN.md +28 -0
  329. package/templates/.agents/rules/label-milestone-setup.en.md +7 -7
  330. package/templates/.agents/rules/label-milestone-setup.zh-CN.md +7 -7
  331. package/templates/.agents/rules/local-artifact-repair.en.md +5 -1
  332. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +5 -1
  333. package/templates/.agents/rules/security-alerts.en.md +8 -8
  334. package/templates/.agents/rules/security-alerts.zh-CN.md +8 -8
  335. package/templates/.agents/rules/testing-discipline.en.md +2 -0
  336. package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
  337. package/templates/.agents/scripts/package.json +3 -0
  338. package/templates/.agents/skills/analyze-task/SKILL.en.md +15 -2
  339. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +19 -2
  340. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -12
  341. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -12
  342. package/templates/.agents/skills/close-codescan/SKILL.en.md +2 -2
  343. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
  344. package/templates/.agents/skills/close-dependabot/SKILL.en.md +2 -2
  345. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
  346. package/templates/.agents/skills/code-task/SKILL.en.md +14 -1
  347. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +14 -1
  348. package/templates/.agents/skills/code-task/config/verify.en.json +5 -14
  349. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +5 -14
  350. package/templates/.agents/skills/code-task/reference/code-rules.en.md +5 -4
  351. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +5 -4
  352. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
  353. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
  354. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +3 -1
  355. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +3 -1
  356. package/templates/.agents/skills/code-task/reference/report-template.en.md +42 -15
  357. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +39 -12
  358. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +37 -21
  359. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +37 -21
  360. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
  361. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
  362. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +4 -0
  363. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +4 -0
  364. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
  365. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
  366. package/templates/.agents/skills/complete-task/SKILL.en.md +10 -23
  367. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -23
  368. package/templates/.agents/skills/create-task/SKILL.en.md +8 -0
  369. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +7 -0
  370. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  371. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  372. package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
  373. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  374. package/templates/.agents/skills/init-labels/SKILL.en.md +8 -8
  375. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +8 -8
  376. package/templates/.agents/skills/init-milestones/SKILL.en.md +9 -11
  377. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +9 -11
  378. package/templates/.agents/skills/plan-task/SKILL.en.md +14 -1
  379. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +16 -1
  380. package/templates/.agents/skills/plan-task/config/verify.en.json +4 -14
  381. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +4 -14
  382. package/templates/.agents/skills/release/SKILL.en.md +7 -3
  383. package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
  384. package/templates/.agents/skills/review-analysis/SKILL.en.md +14 -1
  385. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +14 -1
  386. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -18
  387. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -18
  388. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +50 -23
  389. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +45 -18
  390. package/templates/.agents/skills/review-code/SKILL.en.md +14 -1
  391. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +14 -1
  392. package/templates/.agents/skills/review-code/config/verify.en.json +1 -23
  393. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -23
  394. package/templates/.agents/skills/review-code/reference/report-template.en.md +49 -22
  395. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +45 -18
  396. package/templates/.agents/skills/review-plan/SKILL.en.md +14 -1
  397. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +14 -1
  398. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -18
  399. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -18
  400. package/templates/.agents/skills/review-plan/reference/report-template.en.md +49 -22
  401. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +45 -18
  402. package/templates/.agents/skills/review-pr/SKILL.en.md +5 -1
  403. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +5 -1
  404. package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -3
  405. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -3
  406. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +25 -4
  407. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +25 -4
  408. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
  409. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
  410. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +5 -1
  411. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +5 -1
  412. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  413. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  414. package/templates/.agents/skills/test/SKILL.en.md +29 -7
  415. package/templates/.agents/skills/test/SKILL.zh-CN.md +29 -7
  416. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +14 -0
  417. package/templates/.agents/templates/task.en.md +6 -0
  418. package/templates/.agents/templates/task.zh-CN.md +6 -0
  419. package/templates/.claude/commands/run-manual-validation.en.md +1 -1
  420. package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
  421. package/dist/lib/platform/adapters.js +0 -56
  422. package/lib/platform/adapters.ts +0 -197
  423. package/templates/.agents/scripts/security-alerts.github.sh +0 -151
  424. package/templates/.agents/scripts/security-alerts.sh +0 -31
  425. package/templates/.agents/skills/init-labels/scripts/init-labels.github.sh +0 -146
  426. package/templates/.agents/skills/init-labels/scripts/init-labels.sh +0 -32
  427. package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +0 -294
  428. package/templates/.agents/skills/init-milestones/scripts/init-milestones.sh +0 -6
@@ -1,1004 +1,13 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
1
  import { parseArgs } from 'node:util';
4
- import * as p from '@clack/prompts';
5
- import pc from 'picocolors';
6
2
  import { loadConfig } from '../config.ts';
7
- import type { SandboxConfig } from '../config.ts';
8
- import {
9
- assertValidBranchName,
10
- containerNameCandidates,
11
- sandboxBranchLabel,
12
- sandboxLabel,
13
- sandboxTaskIdLabel,
14
- sandboxWorkspaceModeLabel,
15
- shareBranchDir,
16
- shellConfigDirCandidates,
17
- worktreeDirCandidates
18
- } from '../constants.ts';
19
- import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from '../engine.ts';
20
- import { pruneSandboxDanglingImages } from '../image-prune.ts';
21
- import { assertManagedPath, removeManagedDir, removeWorktreeDir } from '../managed-fs.ts';
22
- import { runEngine, runOk, runOkEngine, runSafe, runSafeEngine } from '../shell.ts';
23
- import {
24
- parseSandboxWorkspaceIdentity,
25
- resolveSandboxCleanupTarget,
26
- sameSandboxWorkspaceIdentity,
27
- type SandboxCleanupTarget,
28
- type SandboxWorkspaceKey
29
- } from '../workspace-identity.ts';
30
- import { sandboxControlPaths, sandboxWorkspaceViewPaths } from '../workspace-view.ts';
31
- import { removeSandboxControlRoot, readSandboxControlManifest } from '../control/lifecycle.ts';
32
- import { inspectSandboxControlContainer } from '../control/container-identity.ts';
33
- import { toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.ts';
34
3
  import { createSandboxCapabilityPlan } from '../agent-client-reconciler.ts';
35
- import type { SandboxTool } from '../tools.ts';
36
- import { fetchSandboxRows, type SandboxRow } from './list-running.ts';
37
- import {
38
- createCleanPermit,
39
- createDiscardPermit,
40
- formatWorktreeSnapshot,
41
- inspectRecoveredWorktree,
42
- inspectWorktrees,
43
- verifyWorktreePermit
44
- } from '../worktree-safety.ts';
45
- import type {
46
- WorktreeInspection,
47
- WorktreeRecoveryContext,
48
- WorktreeRemovalPermit
49
- } from '../worktree-safety.ts';
4
+ import { resolveSandboxCleanupTarget } from '../workspace-identity.ts';
5
+ import { rmOne, rmPurge, rmUnbound } from '../removal.ts';
50
6
 
51
7
  const USAGE = `Usage:
52
8
  ai sandbox rm <branch | TASK-id | short id> Remove one sandbox; use a full TASK-id for a task-bound sandbox and a branch for branch-only sandboxes
53
- ai sandbox rm --unbound [--dry-run] [--yes] Remove completed task-bound and branch-only sandboxes; active, blocked, and archive tasks are protected
9
+ ai sandbox rm --unbound [--dry-run] [--yes] Remove completed task-bound and branch-only sandboxes and verified auxiliary state; active, blocked, and archive tasks are protected
54
10
  ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
55
- export { assertManagedPath } from '../managed-fs.ts';
56
-
57
- function projectToolDirs(config: SandboxConfig, tools: SandboxTool[]): string[] {
58
- return tools.flatMap((tool) => toolProjectDirCandidates(tool, config.project));
59
- }
60
-
61
- function isMissingPathError(error: unknown): boolean {
62
- return Boolean(error && typeof error === 'object' && 'code' in error
63
- && (error as { code?: unknown }).code === 'ENOENT');
64
- }
65
-
66
- export function sandboxManagedPathKey(
67
- candidate: string,
68
- platform: NodeJS.Platform = process.platform,
69
- resolveExistingPath: (resolved: string) => string = (resolved) => fs.realpathSync.native(resolved)
70
- ): string {
71
- const resolved = path.resolve(candidate);
72
- let identity = resolved;
73
- try {
74
- identity = resolveExistingPath(resolved);
75
- } catch (error) {
76
- if (!isMissingPathError(error)) {
77
- throw new Error(
78
- `SANDBOX_CLEANUP_BATCH_PREFLIGHT_FAILED: unable to determine managed path identity for '${resolved}'`
79
- );
80
- }
81
- }
82
- return platform === 'win32' ? identity.toLowerCase() : identity;
83
- }
84
-
85
- type RmTarget = {
86
- branch: string;
87
- effectiveBranch: string;
88
- engine: string;
89
- matchedContainers: string[];
90
- existingWorktrees: string[];
91
- toolCandidates: Array<{ tool: SandboxTool; candidates: string[] }>;
92
- managedPathCandidates?: string[];
93
- workspace: SandboxWorkspaceKey;
94
- controlRoots: string[];
95
- workspaceViewRoots: string[];
96
- };
97
-
98
- type CleanupCandidate = Readonly<{
99
- row: SandboxRow;
100
- cleanupTarget: SandboxCleanupTarget;
101
- }>;
102
-
103
- type CleanupGroup = Readonly<{
104
- candidates: readonly CleanupCandidate[];
105
- cleanupTarget: SandboxCleanupTarget;
106
- target: RmTarget;
107
- }>;
108
-
109
- type RmOneOptions = {
110
- assumeYes?: boolean;
111
- interactive?: boolean;
112
- quiet?: boolean;
113
- target?: RmTarget;
114
- cleanupTarget?: SandboxCleanupTarget;
115
- permits?: ReadonlyMap<string, WorktreeRemovalPermit>;
116
- allowDirtyDiscard?: boolean;
117
- prompt?: PromptDependencies;
118
- };
119
-
120
- type PromptDependencies = {
121
- confirm?: typeof p.confirm;
122
- isCancel?: typeof p.isCancel;
123
- };
124
-
125
- function recoveryContexts(
126
- config: SandboxConfig,
127
- target: RmTarget,
128
- worktrees: readonly string[]
129
- ): Map<string, WorktreeRecoveryContext> {
130
- const candidates = worktreeDirCandidates(config, target.effectiveBranch).map((candidate) => path.resolve(candidate));
131
- const existingCandidates = candidates.filter((candidate) => fs.existsSync(candidate));
132
- if (existingCandidates.length !== 1) return new Map();
133
-
134
- const contexts = new Map<string, WorktreeRecoveryContext>();
135
- for (const worktree of worktrees) {
136
- const resolvedWorktree = path.resolve(worktree);
137
- if (resolvedWorktree !== existingCandidates[0]) continue;
138
- for (const root of target.controlRoots.filter((candidate) => fs.existsSync(candidate))) {
139
- assertLegacyCandidateEvidence(root, config.controlBase, config, target.effectiveBranch, target.matchedContainers);
140
- assertControlRootMatchesTarget(root, target.effectiveBranch, target.workspace);
141
- }
142
- contexts.set(resolvedWorktree, {
143
- repoRoot: config.repoRoot,
144
- worktreeBase: config.worktreeBase,
145
- branch: target.effectiveBranch,
146
- identitySource: target.workspace.mode,
147
- taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null
148
- });
149
- }
150
- return contexts;
151
- }
152
-
153
- function resolveRmTarget(
154
- config: SandboxConfig,
155
- tools: SandboxTool[],
156
- cleanupTarget: SandboxCleanupTarget,
157
- options: Readonly<{ discoveredContainers?: readonly string[] }> = {}
158
- ): RmTarget {
159
- assertValidBranchName(cleanupTarget.branch);
160
- const engine = detectEngine(config);
161
- const branch = cleanupTarget.branch;
162
- const effectiveBranch = branch;
163
- const worktreeCandidates = worktreeDirCandidates(config, branch);
164
- const toolCandidates = tools.map((tool) => ({
165
- tool,
166
- candidates: toolConfigDirCandidates(tool, config.project, branch)
167
- }));
168
- const shellCandidates = shellConfigDirCandidates(config, branch);
169
- const existing = runEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
170
- const matchedContainers = options.discoveredContainers
171
- ? options.discoveredContainers.map((container) => {
172
- if (!existing.includes(container)) {
173
- throw new Error(
174
- `SANDBOX_CLEANUP_BATCH_PREFLIGHT_FAILED: discovered container '${container}' is no longer present`
175
- );
176
- }
177
- return container;
178
- })
179
- : containerNameCandidates(config, branch).filter((name) => existing.includes(name));
180
-
181
- const workspace = cleanupTarget.workspace;
182
- const identities: SandboxWorkspaceKey[] = [workspace];
183
- const containers = options.discoveredContainers
184
- ? matchedContainers
185
- : [...new Set([...containerNameCandidates(config, effectiveBranch), ...matchedContainers])];
186
- const controlRoots = containers.flatMap((container) => identities.map((identity) => sandboxControlPaths({
187
- base: config.controlBase, project: config.project, container, identity
188
- }).root));
189
- const workspaceViewRoots = containers.flatMap((container) => identities.map((identity) => sandboxWorkspaceViewPaths({
190
- base: config.workspaceViewBase, project: config.project, container, identity
191
- }).root));
192
- const managedPathCandidates = [...new Set([
193
- ...worktreeCandidates,
194
- ...toolCandidates.flatMap(({ candidates }) => candidates),
195
- ...shellCandidates,
196
- shareBranchDir(config, branch),
197
- ...controlRoots,
198
- ...workspaceViewRoots
199
- ].map((candidate) => path.resolve(candidate)))];
200
-
201
- return {
202
- branch,
203
- effectiveBranch,
204
- engine,
205
- matchedContainers,
206
- existingWorktrees: worktreeCandidates.filter((candidate) => fs.existsSync(candidate)),
207
- toolCandidates,
208
- managedPathCandidates,
209
- workspace,
210
- controlRoots: [...new Set(controlRoots)],
211
- workspaceViewRoots: [...new Set(workspaceViewRoots)]
212
- };
213
- }
214
-
215
- type CurrentContainerIdentity = Readonly<{ id: string; labels: Record<string, string> }>;
216
-
217
- function inspectNamedContainerIdentity(target: RmTarget, container: string): CurrentContainerIdentity {
218
- const raw = runSafeEngine(target.engine, 'docker', [
219
- 'inspect', '--format', '{{json .}}', container
220
- ]).trim();
221
- let parsed: unknown;
222
- try {
223
- parsed = JSON.parse(raw);
224
- } catch {
225
- throw new Error(`SANDBOX_WORKSPACE_IDENTITY_UNKNOWN: container '${container}' inspection is invalid`);
226
- }
227
- const value = Array.isArray(parsed) && parsed.length === 1 ? parsed[0] : parsed;
228
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
229
- throw new Error(`SANDBOX_WORKSPACE_IDENTITY_UNKNOWN: container '${container}' inspection is incomplete`);
230
- }
231
- const candidate = value as {
232
- Id?: unknown;
233
- Config?: { Labels?: unknown };
234
- };
235
- if (typeof candidate.Id !== 'string' || !candidate.Id
236
- || !candidate.Config?.Labels || typeof candidate.Config.Labels !== 'object'
237
- || Array.isArray(candidate.Config.Labels)
238
- || Object.values(candidate.Config.Labels).some((value) => typeof value !== 'string')) {
239
- throw new Error(`SANDBOX_WORKSPACE_IDENTITY_UNKNOWN: container '${container}' inspection is incomplete`);
240
- }
241
- return { id: candidate.Id, labels: candidate.Config.Labels as Record<string, string> };
242
- }
243
-
244
- function assertMatchedContainerIdentities(config: SandboxConfig, target: RmTarget): Map<string, CurrentContainerIdentity> {
245
- const current = new Map<string, CurrentContainerIdentity>();
246
- for (const container of target.matchedContainers) {
247
- const observation = inspectNamedContainerIdentity(target, container);
248
- const stringLabels = observation.labels;
249
- const branch = stringLabels[sandboxBranchLabel(config)];
250
- if (branch !== target.branch) {
251
- throw new Error(
252
- `SANDBOX_WORKSPACE_IDENTITY_CONFLICT: container '${container}' branch is ${JSON.stringify(branch ?? null)}, `
253
- + `but this request is ${JSON.stringify(target.branch)}`
254
- );
255
- }
256
- const identity = parseSandboxWorkspaceIdentity(stringLabels, {
257
- mode: sandboxWorkspaceModeLabel(config),
258
- taskId: sandboxTaskIdLabel(config)
259
- });
260
- if (identity.mode === 'legacy-invalid' || !sameSandboxWorkspaceIdentity(identity, target.workspace)) {
261
- const existingDescription = identity.mode === 'task-bound'
262
- ? `task-bound:${identity.taskId}`
263
- : identity.mode;
264
- const requestedDescription = target.workspace.mode === 'task-bound'
265
- ? `task-bound:${target.workspace.taskId}`
266
- : target.workspace.mode;
267
- throw new Error(
268
- `SANDBOX_WORKSPACE_IDENTITY_CONFLICT: container '${container}' is ${existingDescription}, `
269
- + `but this request is ${requestedDescription}`
270
- );
271
- }
272
- current.set(container, observation);
273
- }
274
- return current;
275
- }
276
-
277
- function controlRootContainer(config: SandboxConfig, root: string): string {
278
- const relative = path.relative(path.join(config.controlBase, config.project), root);
279
- const [container, identity, ...extra] = relative.split(path.sep);
280
- if (!container || !identity || extra.length > 0) {
281
- throw new Error(`SANDBOX_CONTROL_TARGET_EVIDENCE_INVALID: ${root}`);
282
- }
283
- return container;
284
- }
285
-
286
- function assertControlRootMatchesTarget(root: string, effectiveBranch: string, workspace: SandboxWorkspaceKey): void {
287
- const manifestPath = path.join(root, 'manifest.json');
288
- if (!fs.existsSync(manifestPath)) {
289
- if (workspace.mode === 'task-bound') {
290
- throw new Error(`SANDBOX_CONTROL_TARGET_EVIDENCE_MISSING: ${root}`);
291
- }
292
- return;
293
- }
294
- const manifest = readSandboxControlManifest(manifestPath);
295
- const identityMatches = workspace.mode === 'task-bound'
296
- ? manifest.mode === 'task-bound' && manifest.taskId === workspace.taskId
297
- : manifest.mode === 'branch-only';
298
- if (manifest.branch !== effectiveBranch || !identityMatches) {
299
- throw new Error(`SANDBOX_CONTROL_TARGET_MISMATCH: ${root}`);
300
- }
301
- }
302
-
303
- function preflightRmTarget(
304
- config: SandboxConfig,
305
- target: RmTarget
306
- ): void {
307
- const current = assertMatchedContainerIdentities(config, target);
308
- const manifests: ReturnType<typeof readSandboxControlManifest>[] = [];
309
- for (const root of target.controlRoots.filter((candidate) => fs.existsSync(candidate))) {
310
- assertLegacyCandidateEvidence(root, config.controlBase, config, target.effectiveBranch, target.matchedContainers);
311
- assertControlRootMatchesTarget(root, target.effectiveBranch, target.workspace);
312
- const manifestPath = path.join(root, 'manifest.json');
313
- if (!fs.existsSync(manifestPath)) continue;
314
- const manifest = readSandboxControlManifest(manifestPath);
315
- const rootContainer = controlRootContainer(config, root);
316
- if (manifest.container !== rootContainer) {
317
- throw new Error(`SANDBOX_CONTROL_TARGET_MISMATCH: ${root}`);
318
- }
319
- const observed = current.get(manifest.container);
320
- if (observed && (observed.id !== manifest.containerIdentity.id
321
- || Object.entries(manifest.containerIdentity.labels).some(([key, value]) => observed.labels[key] !== value))) {
322
- throw new Error(`SANDBOX_CONTROL_TARGET_MISMATCH: ${root}`);
323
- }
324
- manifests.push(manifest);
325
- }
326
-
327
- if (target.workspace.mode !== 'task-bound') return;
328
- const matched = new Set(target.matchedContainers);
329
- const manifestContainers = manifests.map((manifest) => manifest.container);
330
- if (matched.size > 0 && manifestContainers.some((container) => !matched.has(container))) {
331
- throw new Error('SANDBOX_CONTROL_TARGET_MISMATCH: manifest container is outside the cleanup target');
332
- }
333
- for (const container of matched) {
334
- const matches = manifests.filter((manifest) => manifest.container === container);
335
- if (matches.length !== 1) {
336
- throw new Error(`SANDBOX_CONTROL_TARGET_EVIDENCE_MISSING: container '${container}'`);
337
- }
338
- }
339
- if (matched.size === 0 && manifests.length > 0) {
340
- const supported = new Set(containerNameCandidates(config, target.effectiveBranch));
341
- if (manifests.some((manifest) => !supported.has(manifest.container))) {
342
- throw new Error('SANDBOX_CONTROL_TARGET_MISMATCH: manifest container is outside the cleanup target');
343
- }
344
- }
345
- }
346
-
347
- function assertCleanupGroupPathsDoNotOverlap(groups: readonly CleanupGroup[]): void {
348
- const owners = new Map<string, string>();
349
- for (const group of groups) {
350
- for (const candidate of group.target.managedPathCandidates ?? []) {
351
- const key = sandboxManagedPathKey(candidate);
352
- const existingBranch = owners.get(key);
353
- if (existingBranch && existingBranch !== group.cleanupTarget.branch) {
354
- throw new Error(
355
- `SANDBOX_CLEANUP_BATCH_PREFLIGHT_FAILED: managed path '${candidate}' is shared by branches `
356
- + `${JSON.stringify(existingBranch)} and ${JSON.stringify(group.cleanupTarget.branch)}`
357
- );
358
- }
359
- owners.set(key, group.cleanupTarget.branch);
360
- }
361
- }
362
- }
363
-
364
- function assertWorktreeBranchesMatchGroups(
365
- groups: readonly CleanupGroup[],
366
- inspections: readonly WorktreeInspection[]
367
- ): void {
368
- const byPath = new Map(
369
- inspections.flatMap((inspection) => inspection.status === 'failed'
370
- ? []
371
- : [[sandboxManagedPathKey(inspection.snapshot.worktree), inspection.snapshot.branch] as const])
372
- );
373
- for (const group of groups) {
374
- for (const worktree of group.target.existingWorktrees) {
375
- const registeredBranch = byPath.get(sandboxManagedPathKey(worktree));
376
- if (registeredBranch && registeredBranch !== group.cleanupTarget.branch) {
377
- throw new Error(
378
- `SANDBOX_CLEANUP_BATCH_PREFLIGHT_FAILED: worktree '${worktree}' is registered to branch `
379
- + `${JSON.stringify(registeredBranch)}, but cleanup targets ${JSON.stringify(group.cleanupTarget.branch)}`
380
- );
381
- }
382
- }
383
- }
384
- }
385
-
386
- function assertLegacyCandidateEvidence(
387
- candidate: string,
388
- base: string,
389
- config: SandboxConfig,
390
- effectiveBranch: string,
391
- matchedContainers: readonly string[]
392
- ): void {
393
- const relative = path.relative(path.join(base, config.project), candidate);
394
- const [container, identity] = relative.split(path.sep);
395
- const [canonical, legacy] = containerNameCandidates(config, effectiveBranch);
396
- if (!container || !identity || legacy === canonical || container !== legacy || matchedContainers.includes(container)) return;
397
- const manifestPath = path.join(config.controlBase, config.project, container, identity, 'manifest.json');
398
- if (!fs.existsSync(manifestPath)) {
399
- throw new Error(`SANDBOX_CONTROL_TARGET_EVIDENCE_MISSING: ${candidate}`);
400
- }
401
- }
402
-
403
- function assertRemoved(target: string, label: string): void {
404
- if (fs.existsSync(target)) throw new Error(`${label} still exists after removal: ${target}`);
405
- }
406
-
407
- function removeEmptyManagedParent(base: string, directory: string): void {
408
- const parent = path.dirname(directory);
409
- if (path.resolve(parent) === path.resolve(base)) return;
410
- assertManagedPath(base, parent);
411
- try {
412
- fs.rmdirSync(parent);
413
- } catch (error) {
414
- if (['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes((error as NodeJS.ErrnoException).code ?? '')) return;
415
- throw error;
416
- }
417
- assertRemoved(parent, 'Empty sandbox container directory');
418
- }
419
-
420
- async function removeExactSandboxContainer(
421
- engine: string,
422
- manifest: ReturnType<typeof readSandboxControlManifest>,
423
- timeoutMs: number
424
- ): Promise<void> {
425
- const deadlineAt = Date.now() + timeoutMs;
426
- const remaining = (): number => Math.max(1, deadlineAt - Date.now());
427
- const inspect = () => inspectSandboxControlContainer(manifest, { timeoutMs: remaining() });
428
- const before = await inspect();
429
- if (before.state === 'unknown') throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${before.reason}`);
430
- if (before.state === 'absent') return;
431
- if (before.running && !runOkEngine(
432
- engine,
433
- 'docker',
434
- ['stop', '--timeout', '1', manifest.containerIdentity.id],
435
- { timeout: remaining() }
436
- )) {
437
- const afterStop = await inspect();
438
- if (afterStop.state !== 'absent' && afterStop.state !== 'found') {
439
- throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterStop.reason}`);
440
- }
441
- if (afterStop.state === 'found' && afterStop.running) {
442
- throw new Error(`Failed to stop sandbox container: ${manifest.containerIdentity.id}`);
443
- }
444
- }
445
- const afterStop = await inspect();
446
- if (afterStop.state === 'unknown') throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterStop.reason}`);
447
- if (afterStop.state === 'found' && !runOkEngine(engine, 'docker', ['rm', manifest.containerIdentity.id], { timeout: remaining() })) {
448
- const afterRemove = await inspect();
449
- if (afterRemove.state !== 'absent') {
450
- throw new Error(`Failed to remove sandbox container: ${manifest.containerIdentity.id}`);
451
- }
452
- }
453
- }
454
-
455
- async function removeProjectControlRoots(config: SandboxConfig, engine: string): Promise<Set<string>> {
456
- const containers = new Set<string>();
457
- const projectRoot = path.join(config.controlBase, config.project);
458
- if (!fs.existsSync(projectRoot)) return containers;
459
- assertManagedPath(config.controlBase, projectRoot);
460
- const projectStat = fs.lstatSync(projectRoot);
461
- if (!projectStat.isDirectory() || projectStat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
462
- for (const container of fs.readdirSync(projectRoot, { withFileTypes: true })) {
463
- if (!container.isDirectory() || container.isSymbolicLink()) continue;
464
- const containerRoot = path.join(projectRoot, container.name);
465
- for (const identity of fs.readdirSync(containerRoot, { withFileTypes: true })) {
466
- if (!identity.isDirectory() || identity.isSymbolicLink()) continue;
467
- const root = path.join(containerRoot, identity.name);
468
- const manifestPath = path.join(root, 'manifest.json');
469
- if (!fs.existsSync(manifestPath)) throw new Error(`SANDBOX_CONTROL_TARGET_EVIDENCE_MISSING: ${root}`);
470
- const manifest = readSandboxControlManifest(manifestPath);
471
- containers.add(manifest.container);
472
- await removeSandboxControlRoot(root, {
473
- inspectContainer: (timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }),
474
- removeContainer: (timeoutMs) => removeExactSandboxContainer(engine, manifest, timeoutMs)
475
- });
476
- }
477
- }
478
- return containers;
479
- }
480
-
481
- function inspectionBlockers(inspections: readonly WorktreeInspection[]): WorktreeInspection[] {
482
- return inspections.filter((inspection) => inspection.status !== 'clean');
483
- }
484
-
485
- function blockerMessage(blockers: readonly WorktreeInspection[]): string {
486
- return blockers.map((blocker) => {
487
- if (blocker.status === 'failed') return `${JSON.stringify(blocker.worktree)}: ${blocker.message}`;
488
- return `${formatWorktreeSnapshot(blocker.snapshot)}\n Action: commit, stash, clean, or remove this sandbox interactively.`;
489
- }).join('\n');
490
- }
491
-
492
- function cleanPermits(inspections: readonly WorktreeInspection[]): Map<string, WorktreeRemovalPermit> {
493
- const permits = new Map<string, WorktreeRemovalPermit>();
494
- for (const inspection of inspections) {
495
- if (inspection.status === 'clean') permits.set(inspection.snapshot.worktree, createCleanPermit(inspection.snapshot));
496
- }
497
- return permits;
498
- }
499
-
500
- async function authorizeWorktrees(
501
- worktrees: readonly string[],
502
- { allowDirtyDiscard, assumeYes }: { allowDirtyDiscard: boolean; assumeYes: boolean },
503
- {
504
- interactive = Boolean(process.stdin.isTTY),
505
- confirm = p.confirm,
506
- isCancel = p.isCancel,
507
- recovery = new Map<string, WorktreeRecoveryContext>()
508
- }: PromptDependencies & { interactive?: boolean; recovery?: ReadonlyMap<string, WorktreeRecoveryContext> } = {}
509
- ): Promise<Map<string, WorktreeRemovalPermit>> {
510
- const inspections = worktrees.map((worktree) => {
511
- const inspection = inspectWorktrees([worktree])[0]!;
512
- const context = recovery.get(path.resolve(worktree));
513
- return inspection.status === 'failed' && context
514
- ? inspectRecoveredWorktree(worktree, context)
515
- : inspection;
516
- });
517
- const failures = inspections.filter((inspection) => inspection.status === 'failed');
518
- if (failures.length > 0) throw new Error(`Unable to inspect worktree(s):\n${blockerMessage(failures)}`);
519
- const permits = cleanPermits(inspections);
520
- for (const inspection of inspections) {
521
- if (inspection.status !== 'dirty') continue;
522
- if (!allowDirtyDiscard || assumeYes || !interactive) {
523
- throw new Error(`Refusing to remove dirty worktree(s):\n${blockerMessage([inspection])}`);
524
- }
525
- p.log.warn(formatWorktreeSnapshot(inspection.snapshot));
526
- const confirmed = await confirm({
527
- message: 'Discard these exact uncommitted changes?',
528
- initialValue: false
529
- });
530
- if (isCancel(confirmed) || !confirmed) throw new Error('Dirty worktree removal cancelled; nothing was deleted');
531
- permits.set(inspection.snapshot.worktree, createDiscardPermit(inspection.snapshot));
532
- }
533
- return permits;
534
- }
535
-
536
- async function rmOne(
537
- config: SandboxConfig,
538
- tools: SandboxTool[],
539
- branch: string,
540
- options: RmOneOptions = {}
541
- ): Promise<void> {
542
- const target = options.target ?? resolveRmTarget(
543
- config,
544
- tools,
545
- options.cleanupTarget ?? resolveSandboxCleanupTarget(branch, config.repoRoot)
546
- );
547
- const { effectiveBranch, engine, matchedContainers, existingWorktrees, toolCandidates } = target;
548
- const { workspace, controlRoots, workspaceViewRoots } = target;
549
- preflightRmTarget(config, target);
550
- const confirm = options.prompt?.confirm ?? p.confirm;
551
- const isCancel = options.prompt?.isCancel ?? p.isCancel;
552
-
553
- if (!options.quiet) {
554
- p.intro(pc.cyan(`Removing sandbox for ${branch}`));
555
- }
556
-
557
- const recovery = options.permits
558
- ? new Map<string, WorktreeRecoveryContext>()
559
- : recoveryContexts(config, target, existingWorktrees);
560
- const permits = options.permits
561
- ? new Map(options.permits)
562
- : await authorizeWorktrees(existingWorktrees, {
563
- allowDirtyDiscard: options.allowDirtyDiscard ?? true,
564
- assumeYes: Boolean(options.assumeYes)
565
- }, {
566
- ...options.prompt,
567
- interactive: options.interactive ?? Boolean(process.stdin.isTTY),
568
- recovery
569
- });
570
- for (const worktree of existingWorktrees) {
571
- const permit = permits.get(path.resolve(worktree));
572
- if (!permit) throw new Error(`Missing worktree removal permit: ${worktree}`);
573
- verifyWorktreePermit(permit);
574
- }
575
-
576
- const shouldRemoveWorktree = existingWorktrees.length === 0
577
- ? false
578
- : options.assumeYes
579
- ? true
580
- : await confirm({
581
- message: `Remove worktree(s): ${existingWorktrees.join(', ')}?`,
582
- initialValue: true
583
- });
584
- if (isCancel(shouldRemoveWorktree)) {
585
- p.outro('Cancelled');
586
- return;
587
- }
588
-
589
- preflightRmTarget(config, target);
590
- const coordinatedContainers = new Set<string>();
591
- for (const root of controlRoots.filter((candidate) => fs.existsSync(candidate))) {
592
- assertLegacyCandidateEvidence(root, config.controlBase, config, effectiveBranch, matchedContainers);
593
- assertControlRootMatchesTarget(root, effectiveBranch, workspace);
594
- const manifestPath = path.join(root, 'manifest.json');
595
- if (!fs.existsSync(manifestPath)) continue;
596
- const manifest = readSandboxControlManifest(manifestPath);
597
- coordinatedContainers.add(manifest.container);
598
- await removeSandboxControlRoot(root, {
599
- inspectContainer: (timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }),
600
- removeContainer: (timeoutMs) => removeExactSandboxContainer(engine, manifest, timeoutMs)
601
- });
602
- removeEmptyManagedParent(path.join(config.controlBase, config.project), root);
603
- }
604
-
605
- const uncoordinatedContainers = matchedContainers.filter((name) => !coordinatedContainers.has(name));
606
- if (uncoordinatedContainers.length > 0) {
607
- if (workspace.mode === 'task-bound') {
608
- throw new Error(`SANDBOX_CONTROL_TARGET_EVIDENCE_MISSING: container '${uncoordinatedContainers[0]}'`);
609
- }
610
- const spinner = p.spinner();
611
- spinner.start(`Stopping container(s): ${uncoordinatedContainers.join(', ')}`);
612
- for (const name of uncoordinatedContainers) {
613
- if (!runOkEngine(engine, 'docker', ['stop', name])) throw new Error(`Failed to stop sandbox container: ${name}`);
614
- if (!runOkEngine(engine, 'docker', ['rm', name])) throw new Error(`Failed to remove sandbox container: ${name}`);
615
- }
616
- const remaining = runEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
617
- const leftovers = uncoordinatedContainers.filter((name) => remaining.includes(name));
618
- if (leftovers.length > 0) throw new Error(`Sandbox container(s) still exist after removal: ${leftovers.join(', ')}`);
619
- spinner.stop(pc.green(`Removed container(s): ${uncoordinatedContainers.join(', ')}`));
620
- } else {
621
- p.log.warn(`No sandbox container found for '${branch}'`);
622
- }
623
-
624
- for (const [roots, base, label] of [
625
- [workspaceViewRoots, path.join(config.workspaceViewBase, config.project), 'Workspace view'],
626
- [controlRoots, path.join(config.controlBase, config.project), 'Control channel']
627
- ] as const) {
628
- for (const directory of roots.filter((candidate) => fs.existsSync(candidate))) {
629
- assertLegacyCandidateEvidence(directory, base === path.join(config.controlBase, config.project)
630
- ? config.controlBase : config.workspaceViewBase, config, effectiveBranch, matchedContainers);
631
- removeManagedDir(base, directory);
632
- assertRemoved(directory, label);
633
- removeEmptyManagedParent(base, directory);
634
- if (!options.quiet) p.log.success(`${label} removed: ${directory}`);
635
- }
636
- }
637
-
638
- if (shouldRemoveWorktree) {
639
- for (const worktree of existingWorktrees) {
640
- const permit = permits.get(path.resolve(worktree));
641
- if (!permit) throw new Error(`Missing worktree removal permit: ${worktree}`);
642
- removeWorktreeDir(config.repoRoot, config.worktreeBase, worktree, permit, {
643
- allowRegisteredPathFallback: engine === ENGINES.WSL2
644
- });
645
- assertRemoved(worktree, 'Worktree');
646
- const registered = runSafe('git', ['-C', config.repoRoot, 'worktree', 'list', '--porcelain'])
647
- .split('\n').filter((line) => line.startsWith('worktree ')).map((line) => path.resolve(line.slice(9)));
648
- if (registered.includes(path.resolve(worktree))) throw new Error(`Worktree is still registered after removal: ${worktree}`);
649
- }
650
-
651
- const shouldDeleteBranch = options.assumeYes
652
- ? true
653
- : await confirm({
654
- message: `Also delete local branch '${effectiveBranch}'?`,
655
- initialValue: true
656
- });
657
-
658
- if (!isCancel(shouldDeleteBranch) && shouldDeleteBranch) {
659
- if (!runOk('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch])) {
660
- throw new Error(`Local branch '${effectiveBranch}' was not deleted`);
661
- }
662
- if (runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', `refs/heads/${effectiveBranch}`])) {
663
- throw new Error(`Local branch '${effectiveBranch}' still exists after removal`);
664
- }
665
- }
666
- }
667
-
668
- for (const { tool, candidates } of toolCandidates) {
669
- for (const dir of candidates.filter((candidate) => fs.existsSync(candidate))) {
670
- removeManagedDir(tool.sandboxBase, dir);
671
- assertRemoved(dir, `${tool.name} state`);
672
- p.log.success(`${tool.name} state removed: ${dir}`);
673
- }
674
- }
675
-
676
- for (const dir of shellConfigDirCandidates(config, effectiveBranch).filter((candidate) => fs.existsSync(candidate))) {
677
- removeManagedDir(config.shellConfigBase, dir);
678
- assertRemoved(dir, 'Shell config');
679
- p.log.success(`Shell config removed: ${dir}`);
680
- }
681
-
682
- const shareBranch = shareBranchDir(config, effectiveBranch);
683
- if (fs.existsSync(shareBranch)) {
684
- const shouldRemoveShare = options.assumeYes
685
- ? true
686
- : await confirm({
687
- message: `Remove share dir for branch '${effectiveBranch}' (${shareBranch})?`,
688
- initialValue: true
689
- });
690
- if (!isCancel(shouldRemoveShare) && shouldRemoveShare) {
691
- removeManagedDir(config.shareBase, shareBranch);
692
- assertRemoved(shareBranch, 'Share dir');
693
- p.log.success(`Share dir removed: ${shareBranch}`);
694
- }
695
- }
696
-
697
- if (!options.quiet) {
698
- p.outro(pc.green('Sandbox removed'));
699
- }
700
- }
701
-
702
- async function rmPurge(
703
- config: SandboxConfig,
704
- tools: SandboxTool[],
705
- prompt: PromptDependencies = {}
706
- ): Promise<void> {
707
- const engine = detectEngine(config);
708
- const confirm = prompt.confirm ?? p.confirm;
709
- const isCancel = prompt.isCancel ?? p.isCancel;
710
- p.intro(pc.cyan(`Removing all sandboxes for ${config.project}`));
711
-
712
- const worktrees = fs.existsSync(config.worktreeBase)
713
- ? fs.readdirSync(config.worktreeBase)
714
- .map((entry) => path.join(config.worktreeBase, entry))
715
- .filter((entry) => {
716
- try { return fs.statSync(entry).isDirectory(); } catch { return false; }
717
- })
718
- : [];
719
- const inspections = inspectWorktrees(worktrees);
720
- const blockers = inspectionBlockers(inspections);
721
- if (blockers.length > 0) {
722
- throw new Error(`Refusing to purge because worktree preflight found blocker(s):\n${blockerMessage(blockers)}`);
723
- }
724
- const permits = cleanPermits(inspections);
725
-
726
- const coordinatedContainers = await removeProjectControlRoots(config, engine);
727
-
728
- const containers = runEngine(engine, 'docker', [
729
- 'ps',
730
- '-a',
731
- '--filter',
732
- `label=${sandboxLabel(config)}`,
733
- '--format',
734
- '{{.Names}}'
735
- ]);
736
- const uncoordinatedContainers = containers.split('\n').filter((name) => name && !coordinatedContainers.has(name));
737
- if (uncoordinatedContainers.length > 0) {
738
- const spinner = p.spinner();
739
- spinner.start('Stopping project sandbox containers...');
740
- for (const name of uncoordinatedContainers) {
741
- if (!runOkEngine(engine, 'docker', ['stop', name])) throw new Error(`Failed to stop sandbox container: ${name}`);
742
- if (!runOkEngine(engine, 'docker', ['rm', name])) throw new Error(`Failed to remove sandbox container: ${name}`);
743
- }
744
- const remaining = runEngine(engine, 'docker', [
745
- 'ps', '-a', '--filter', `label=${sandboxLabel(config)}`, '--format', '{{.Names}}'
746
- ]);
747
- const leftovers = remaining.split('\n').filter((name) => name && !coordinatedContainers.has(name));
748
- if (leftovers.length > 0) throw new Error(`Project sandbox container(s) still exist after removal: ${leftovers.join(', ')}`);
749
- spinner.stop(pc.green('Project sandbox containers removed'));
750
- } else {
751
- p.log.warn('No project sandbox containers found');
752
- }
753
-
754
- if (worktrees.length > 0) {
755
- const shouldRemoveWorktrees = await confirm({
756
- message: `Remove all worktrees in ${config.worktreeBase}?`,
757
- initialValue: true
758
- });
759
-
760
- if (!isCancel(shouldRemoveWorktrees) && shouldRemoveWorktrees) {
761
- for (const dir of worktrees) {
762
- const permit = permits.get(path.resolve(dir));
763
- if (!permit) throw new Error(`Missing worktree removal permit: ${dir}`);
764
- removeWorktreeDir(config.repoRoot, config.worktreeBase, dir, permit, {
765
- allowRegisteredPathFallback: engine === ENGINES.WSL2
766
- });
767
- assertRemoved(dir, 'Worktree');
768
- }
769
- runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
770
- const registered = new Set(runSafe('git', ['-C', config.repoRoot, 'worktree', 'list', '--porcelain'])
771
- .split('\n').filter((line) => line.startsWith('worktree ')).map((line) => path.resolve(line.slice(9))));
772
- const leftovers = worktrees.filter((dir) => registered.has(path.resolve(dir)));
773
- if (leftovers.length > 0) throw new Error(`Worktree(s) still registered after removal: ${leftovers.join(', ')}`);
774
- }
775
- }
776
-
777
- for (const dir of projectToolDirs(config, tools)) {
778
- if (fs.existsSync(dir)) {
779
- removeManagedDir(path.dirname(dir), dir);
780
- assertRemoved(dir, 'Tool state');
781
- p.log.success(`Removed tool state: ${dir}`);
782
- }
783
- }
784
-
785
- if (fs.existsSync(config.shellConfigBase) && fs.readdirSync(config.shellConfigBase).length > 0) {
786
- const shouldRemoveShellConfigs = await confirm({
787
- message: `Remove all shell config dirs in ${config.shellConfigBase}?`,
788
- initialValue: true
789
- });
790
-
791
- if (!isCancel(shouldRemoveShellConfigs) && shouldRemoveShellConfigs) {
792
- for (const entry of fs.readdirSync(config.shellConfigBase)) {
793
- const dir = path.join(config.shellConfigBase, entry);
794
- removeManagedDir(config.shellConfigBase, dir);
795
- assertRemoved(dir, 'Shell config');
796
- }
797
- p.log.success(`Project shell config dirs removed: ${config.shellConfigBase}`);
798
- }
799
- }
800
-
801
- if (fs.existsSync(config.shareBase) && fs.readdirSync(config.shareBase).length > 0) {
802
- const shouldRemoveAllShares = await confirm({
803
- message: `Remove all share dirs for project (${config.shareBase})?`,
804
- initialValue: true
805
- });
806
- if (!isCancel(shouldRemoveAllShares) && shouldRemoveAllShares) {
807
- removeManagedDir(path.dirname(config.shareBase), config.shareBase);
808
- assertRemoved(config.shareBase, 'Project share dir');
809
- p.log.success(`Project share dirs removed: ${config.shareBase}`);
810
- }
811
- }
812
-
813
- for (const [base, label] of [
814
- [config.workspaceViewBase, 'workspace views'],
815
- [config.controlBase, 'control channels']
816
- ] as const) {
817
- const projectDir = path.join(base, config.project);
818
- if (fs.existsSync(projectDir)) {
819
- removeManagedDir(base, projectDir);
820
- assertRemoved(projectDir, `Project ${label}`);
821
- p.log.success(`Removed project ${label}: ${projectDir}`);
822
- }
823
- }
824
-
825
- const shouldRemoveImage = await confirm({
826
- message: `Remove image ${config.imageName}?`,
827
- initialValue: false
828
- });
829
- if (!isCancel(shouldRemoveImage) && shouldRemoveImage) {
830
- runSafeEngine(engine, 'docker', ['rmi', config.imageName]);
831
- }
832
-
833
- pruneSandboxDanglingImages(config, engine);
834
-
835
- if (isManagedEngine(engine)) {
836
- if (engine === ENGINES.WSL2) {
837
- p.log.warn('Windows uses Docker Desktop with WSL2. Stop it from Docker Desktop or run "wsl --shutdown" manually.');
838
- p.outro(pc.green('All project sandboxes removed'));
839
- return;
840
- }
841
-
842
- const name = engineDisplayName(engine);
843
- const shouldStopVm = await confirm({
844
- message: `Stop ${name} VM?`,
845
- initialValue: false
846
- });
847
- if (!isCancel(shouldStopVm) && shouldStopVm) {
848
- stopManagedVm(config);
849
- }
850
- }
851
-
852
- p.outro(pc.green('All project sandboxes removed'));
853
- }
854
-
855
- async function rmUnbound(
856
- config: SandboxConfig,
857
- tools: SandboxTool[],
858
- options: { dryRun: boolean; assumeYes: boolean }
859
- ): Promise<void> {
860
- const engine = detectEngine(config);
861
- const { running, nonRunning } = fetchSandboxRows(
862
- engine,
863
- sandboxLabel(config),
864
- sandboxBranchLabel(config),
865
- { mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) }
866
- );
867
- const rows = [...running, ...nonRunning];
868
-
869
- p.intro(pc.cyan(`Removing sandboxes not bound to an active task for ${config.project}`));
870
-
871
- if (rows.length === 0) {
872
- p.outro('No removable sandboxes: every container is bound to an active task (or none exist)');
873
- return;
874
- }
875
-
876
- const candidates: CleanupCandidate[] = rows.map((row) => {
877
- if (!row.branch || !row.workspaceMode || row.workspaceMode === 'legacy-invalid') {
878
- throw new Error(`SANDBOX_CLEANUP_BATCH_PREFLIGHT_FAILED: container '${row.name}' has incomplete workspace identity`);
879
- }
880
- if (row.workspaceMode === 'task-bound') {
881
- if (!row.taskId) {
882
- throw new Error(`SANDBOX_CLEANUP_BATCH_PREFLIGHT_FAILED: container '${row.name}' has no task id`);
883
- }
884
- const cleanupTarget = resolveSandboxCleanupTarget(row.taskId, config.repoRoot, { allowProtected: true });
885
- if (cleanupTarget.branch !== row.branch
886
- || cleanupTarget.workspace.mode !== 'task-bound'
887
- || cleanupTarget.workspace.taskId !== row.taskId) {
888
- throw new Error(`SANDBOX_CLEANUP_BATCH_PREFLIGHT_FAILED: container '${row.name}' task identity conflicts with task.md`);
889
- }
890
- return { row, cleanupTarget };
891
- }
892
- return {
893
- row,
894
- cleanupTarget: {
895
- requestedRef: row.branch,
896
- branch: row.branch,
897
- workspace: { mode: 'branch-only' },
898
- taskState: 'branch-only'
899
- } satisfies SandboxCleanupTarget
900
- };
901
- });
902
- const branchIdentities = new Map<string, string>();
903
- const groupedCandidates = new Map<string, CleanupCandidate[]>();
904
- for (const candidate of candidates) {
905
- const identity = candidate.cleanupTarget.workspace.mode === 'task-bound'
906
- ? `task-bound:${candidate.cleanupTarget.workspace.taskId}`
907
- : 'branch-only';
908
- const existingIdentity = branchIdentities.get(candidate.cleanupTarget.branch);
909
- if (existingIdentity && existingIdentity !== identity) {
910
- throw new Error(
911
- `SANDBOX_CLEANUP_BATCH_PREFLIGHT_FAILED: branch '${candidate.cleanupTarget.branch}' has conflicting workspace identities`
912
- );
913
- }
914
- branchIdentities.set(candidate.cleanupTarget.branch, identity);
915
- const groupKey = `${candidate.cleanupTarget.branch}\0${identity}`;
916
- const group = groupedCandidates.get(groupKey) ?? [];
917
- group.push(candidate);
918
- groupedCandidates.set(groupKey, group);
919
- }
920
- const groups: CleanupGroup[] = [...groupedCandidates.values()].map((groupCandidates) => {
921
- const cleanupTarget = groupCandidates[0]!.cleanupTarget;
922
- const target = resolveRmTarget(config, tools, cleanupTarget, {
923
- discoveredContainers: groupCandidates.map(({ row }) => row.name)
924
- });
925
- preflightRmTarget(config, target);
926
- return { candidates: groupCandidates, cleanupTarget, target };
927
- });
928
- assertCleanupGroupPathsDoNotOverlap(groups);
929
-
930
- const removableGroups = groups.filter(({ cleanupTarget }) =>
931
- cleanupTarget.taskState === 'completed' || cleanupTarget.taskState === 'branch-only'
932
- );
933
- const removable = removableGroups.flatMap(({ candidates: groupCandidates }) => groupCandidates);
934
- for (const { candidates: groupCandidates, cleanupTarget } of groups.filter(({ cleanupTarget }) =>
935
- cleanupTarget.taskState !== 'completed' && cleanupTarget.taskState !== 'branch-only'
936
- )) {
937
- for (const { row } of groupCandidates) {
938
- p.log.message(`Skipped protected sandbox ${row.name} (${cleanupTarget.taskState})`);
939
- }
940
- }
941
-
942
- if (removableGroups.length === 0) {
943
- p.outro('No removable sandboxes: every container is bound to a protected task (or none exist)');
944
- return;
945
- }
946
-
947
- const inspections = inspectWorktrees(removableGroups.flatMap(({ target }) => target.existingWorktrees));
948
- assertWorktreeBranchesMatchGroups(removableGroups, inspections);
949
- const blockers = inspectionBlockers(inspections);
950
- const permits = cleanPermits(inspections);
951
-
952
- for (const { row } of removable) {
953
- p.log.message(`${row.name} ${row.branch}`);
954
- }
955
- if (blockers.length > 0) p.log.error(blockerMessage(blockers));
956
-
957
- if (options.dryRun) {
958
- p.outro(`Dry run: ${removable.length} sandbox(es) inspected, nothing deleted`);
959
- return;
960
- }
961
-
962
- if (blockers.length > 0) {
963
- throw new Error(`Refusing batch removal because worktree preflight found blocker(s):\n${blockerMessage(blockers)}`);
964
- }
965
-
966
- if (!options.assumeYes && !process.stdin.isTTY) {
967
- throw new Error(
968
- 'Refusing to remove sandboxes without confirmation in a non-interactive shell; pass --yes to proceed.'
969
- );
970
- }
971
-
972
- const failures: { branch: string; message: string }[] = [];
973
- let failedRows = 0;
974
- for (const group of removableGroups) {
975
- try {
976
- await rmOne(config, tools, group.cleanupTarget.branch, {
977
- assumeYes: options.assumeYes,
978
- quiet: true,
979
- target: group.target,
980
- cleanupTarget: group.cleanupTarget,
981
- permits,
982
- allowDirtyDiscard: false
983
- });
984
- } catch (error) {
985
- failedRows += group.candidates.length;
986
- failures.push({ branch: group.cleanupTarget.branch, message: error instanceof Error ? error.message : String(error) });
987
- }
988
- }
989
-
990
- if (failures.length > 0) {
991
- for (const failure of failures) {
992
- p.log.error(`Failed to remove '${failure.branch}': ${failure.message}`);
993
- }
994
- throw new Error(
995
- `Removed ${removable.length - failedRows}/${removable.length} sandbox(es); ${failures.length} failed`
996
- );
997
- }
998
-
999
- p.outro(pc.green(`Removed ${removable.length} sandbox(es)`));
1000
- }
1001
-
1002
11
  export async function rm(args: string[]): Promise<void> {
1003
12
  const { values, positionals } = parseArgs({
1004
13
  args,
@@ -1058,5 +67,3 @@ export async function rm(args: string[]): Promise<void> {
1058
67
  const cleanupTarget = resolveSandboxCleanupTarget(positionals[0] ?? '', config.repoRoot);
1059
68
  await rmOne(config, tools, cleanupTarget.branch, { cleanupTarget });
1060
69
  }
1061
-
1062
- export { authorizeWorktrees, rmOne, rmPurge };