@fitlab-ai/agent-infra 0.9.12 → 0.9.14

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 (402) hide show
  1. package/README.md +11 -11
  2. package/README.zh-CN.md +11 -11
  3. package/bin/cli.ts +26 -9
  4. package/dist/bin/cli.js +173 -157
  5. package/dist/bin/internal-cli.js +66 -40
  6. package/dist/lib/agent-client.js +5 -0
  7. package/dist/lib/agent-clients/next-steps.js +6 -1
  8. package/dist/lib/decide.js +3 -3
  9. package/dist/lib/internal/agent-client.js +5 -2
  10. package/dist/lib/internal/cli-route-inventory.js +152 -0
  11. package/dist/lib/internal/codex-lifecycle.js +9 -6
  12. package/dist/lib/internal/codex-sandbox-controller.js +8 -2
  13. package/dist/lib/internal/git-workflow.js +10 -6
  14. package/dist/lib/internal/platform-checks.js +19 -7
  15. package/dist/lib/internal/platform-comment.js +25 -13
  16. package/dist/lib/internal/platform-context.js +6 -3
  17. package/dist/lib/internal/platform-issue.js +20 -13
  18. package/dist/lib/internal/platform-metadata.js +88 -0
  19. package/dist/lib/internal/platform-pr-review.js +26 -13
  20. package/dist/lib/internal/platform-pr.js +84 -31
  21. package/dist/lib/internal/platform-release-notes.js +9 -6
  22. package/dist/lib/internal/platform-security.js +93 -0
  23. package/dist/lib/internal/pr-review-grade.js +25 -12
  24. package/dist/lib/internal/release-workflow.js +23 -9
  25. package/dist/lib/internal/sandbox-control.js +50 -7
  26. package/dist/lib/internal/task-activity.js +16 -7
  27. package/dist/lib/internal/task-artifact.js +235 -24
  28. package/dist/lib/internal/task-context.js +4 -1
  29. package/dist/lib/internal/task-create.js +50 -17
  30. package/dist/lib/internal/task-delivery.js +4 -1
  31. package/dist/lib/internal/task-event.js +10 -4
  32. package/dist/lib/internal/task-finalization.js +6 -3
  33. package/dist/lib/internal/task-invalidation.js +63 -0
  34. package/dist/lib/internal/task-ledger.js +20 -6
  35. package/dist/lib/internal/task-lifecycle.js +6 -3
  36. package/dist/lib/internal/task-operation-registry.js +421 -0
  37. package/dist/lib/internal/task-orchestration.js +12 -0
  38. package/dist/lib/internal/task-override.js +14 -7
  39. package/dist/lib/internal/task-qualification.js +90 -0
  40. package/dist/lib/internal/task-review.js +7 -4
  41. package/dist/lib/internal/task-short-id.js +11 -1
  42. package/dist/lib/internal/task-snapshot.js +4 -1
  43. package/dist/lib/internal/task-validate.js +7 -2
  44. package/dist/lib/internal/task-verify.js +11 -7
  45. package/dist/lib/internal/task-warning.js +11 -4
  46. package/dist/lib/platform/adapters.js +1 -3
  47. package/dist/lib/platform/change-request-git-evidence.js +27 -7
  48. package/dist/lib/platform/comment-safety.js +226 -0
  49. package/dist/lib/platform/completion-backfill.js +6 -5
  50. package/dist/lib/platform/context.js +70 -130
  51. package/dist/lib/platform/github-client.js +1 -1
  52. package/dist/lib/platform/github-provider.js +1130 -0
  53. package/dist/lib/platform/in-label-sync.js +143 -0
  54. package/dist/lib/platform/issue-comments.js +237 -72
  55. package/dist/lib/platform/issues.js +335 -189
  56. package/dist/lib/platform/metadata-labels.js +6 -2
  57. package/dist/lib/platform/none-provider.js +28 -0
  58. package/dist/lib/platform/pr-change-report.js +545 -0
  59. package/dist/lib/platform/pr-checks.js +119 -104
  60. package/dist/lib/platform/pr-review.js +88 -72
  61. package/dist/lib/platform/pr-summary.js +541 -50
  62. package/dist/lib/platform/provider-bridge.js +45 -0
  63. package/dist/lib/platform/provider-contract.d.ts +553 -0
  64. package/dist/lib/platform/provider-contract.js +99 -0
  65. package/dist/lib/platform/provider-loader.js +251 -0
  66. package/dist/lib/platform/provider-validation.js +577 -0
  67. package/dist/lib/platform/pull-request-metadata.js +8 -6
  68. package/dist/lib/platform/pull-requests.js +734 -543
  69. package/dist/lib/platform/release-notes.js +78 -55
  70. package/dist/lib/platform/releases.js +79 -50
  71. package/dist/lib/platform/repository-metadata.js +377 -0
  72. package/dist/lib/platform/resource-identity.d.ts +26 -0
  73. package/dist/lib/platform/resource-identity.js +92 -0
  74. package/dist/lib/platform/security-alerts.js +155 -0
  75. package/dist/lib/platform/task-identities.js +31 -0
  76. package/dist/lib/platform/verification-required.js +11 -12
  77. package/dist/lib/platform/verification-sync.js +129 -248
  78. package/dist/lib/pr-review/evidence-grading.js +19 -6
  79. package/dist/lib/process-data/index.js +7 -0
  80. package/dist/lib/run/index.js +68 -23
  81. package/dist/lib/sandbox/commands/control-status.js +22 -0
  82. package/dist/lib/sandbox/commands/create.js +32 -6
  83. package/dist/lib/sandbox/commands/ls.js +5 -0
  84. package/dist/lib/sandbox/commands/rm.js +827 -120
  85. package/dist/lib/sandbox/commands/show.js +16 -0
  86. package/dist/lib/sandbox/control/client.js +174 -19
  87. package/dist/lib/sandbox/control/completed-reentry.js +85 -0
  88. package/dist/lib/sandbox/control/container-identity.js +28 -31
  89. package/dist/lib/sandbox/control/executor.js +151 -16
  90. package/dist/lib/sandbox/control/lifecycle.js +621 -127
  91. package/dist/lib/sandbox/control/native-file-lock.js +177 -0
  92. package/dist/lib/sandbox/control/protocol.js +77 -0
  93. package/dist/lib/sandbox/control/server.js +677 -34
  94. package/dist/lib/sandbox/control/state.js +209 -4
  95. package/dist/lib/sandbox/control/task-view.js +183 -0
  96. package/dist/lib/sandbox/engines/authority.js +289 -0
  97. package/dist/lib/sandbox/engines/docker-exact-discovery.js +97 -0
  98. package/dist/lib/sandbox/index.js +5 -0
  99. package/dist/lib/sandbox/recovery.js +21 -1
  100. package/dist/lib/sandbox/workspace-view.js +2 -1
  101. package/dist/lib/sandbox/worktree-safety.js +4 -1
  102. package/dist/lib/server/index.js +8 -0
  103. package/dist/lib/server/protocol.js +38 -1
  104. package/dist/lib/task/activity-log.js +8 -1
  105. package/dist/lib/task/artifact-lifecycle.js +75 -18
  106. package/dist/lib/task/artifact-operations.js +520 -0
  107. package/dist/lib/task/artifact-repair-intent.js +59 -0
  108. package/dist/lib/task/artifact-schema.js +135 -0
  109. package/dist/lib/task/capabilities.js +310 -0
  110. package/dist/lib/task/commands/status.js +32 -0
  111. package/dist/lib/task/commit-operation.js +12 -1
  112. package/dist/lib/task/control-authority.js +12 -5
  113. package/dist/lib/task/create-service.js +111 -12
  114. package/dist/lib/task/create.js +25 -0
  115. package/dist/lib/task/current-contract.js +5 -1
  116. package/dist/lib/task/decision-details.js +1 -1
  117. package/dist/lib/task/events.js +416 -17
  118. package/dist/lib/task/finalization.js +37 -13
  119. package/dist/lib/task/human-override.js +58 -36
  120. package/dist/lib/task/index.js +11 -3
  121. package/dist/lib/task/invalidation-command.js +86 -0
  122. package/dist/lib/task/invalidation.js +260 -0
  123. package/dist/lib/task/ledger-intents.js +37 -0
  124. package/dist/lib/task/local-artifact-finalization.js +299 -0
  125. package/dist/lib/task/orchestration.js +240 -144
  126. package/dist/lib/task/pr-delivery-fact.js +102 -101
  127. package/dist/lib/task/qualification-audit.js +490 -0
  128. package/dist/lib/task/qualification-confirmation.js +109 -0
  129. package/dist/lib/task/qualification-intents.js +148 -0
  130. package/dist/lib/task/review-artifacts.js +22 -23
  131. package/dist/lib/task/review-finalization.js +121 -4
  132. package/dist/lib/task/review-fingerprint.js +24 -2
  133. package/dist/lib/task/rework-intent.js +138 -0
  134. package/dist/lib/task/sections.js +6 -4
  135. package/dist/lib/task/skill-scope.js +23 -0
  136. package/dist/lib/task/task-execution-lock.js +27 -17
  137. package/dist/lib/task/verification-config.js +11 -0
  138. package/dist/lib/task/verification-engine.js +93 -16
  139. package/dist/lib/task/verification.js +3 -3
  140. package/dist/lib/task/workflow-warning-intents.js +1 -1
  141. package/dist/lib/task/write.js +11 -0
  142. package/dist/lib/version.js +11 -1
  143. package/dist/lifecycle-build-manifest.json +3 -3
  144. package/lib/agent-client.ts +9 -0
  145. package/lib/agent-clients/next-steps.ts +6 -1
  146. package/lib/decide.ts +3 -3
  147. package/lib/init.ts +1 -1
  148. package/lib/internal/agent-client.ts +4 -2
  149. package/lib/internal/cli-route-inventory.ts +140 -0
  150. package/lib/internal/codex-lifecycle.ts +8 -6
  151. package/lib/internal/codex-sandbox-controller.ts +7 -2
  152. package/lib/internal/git-workflow.ts +9 -6
  153. package/lib/internal/platform-checks.ts +14 -6
  154. package/lib/internal/platform-comment.ts +20 -12
  155. package/lib/internal/platform-context.ts +5 -3
  156. package/lib/internal/platform-issue.ts +17 -11
  157. package/lib/internal/platform-metadata.ts +70 -0
  158. package/lib/internal/platform-pr-review.ts +23 -12
  159. package/lib/internal/platform-pr.ts +65 -27
  160. package/lib/internal/platform-release-notes.ts +8 -6
  161. package/lib/internal/platform-security.ts +83 -0
  162. package/lib/internal/pr-review-grade.ts +20 -11
  163. package/lib/internal/release-workflow.ts +21 -9
  164. package/lib/internal/sandbox-control.ts +44 -6
  165. package/lib/internal/task-activity.ts +15 -7
  166. package/lib/internal/task-artifact.ts +204 -22
  167. package/lib/internal/task-context.ts +3 -1
  168. package/lib/internal/task-create.ts +78 -17
  169. package/lib/internal/task-delivery.ts +3 -1
  170. package/lib/internal/task-event.ts +9 -4
  171. package/lib/internal/task-finalization.ts +6 -4
  172. package/lib/internal/task-invalidation.ts +41 -0
  173. package/lib/internal/task-ledger.ts +21 -8
  174. package/lib/internal/task-lifecycle.ts +5 -3
  175. package/lib/internal/task-operation-registry.ts +482 -0
  176. package/lib/internal/task-orchestration.ts +12 -0
  177. package/lib/internal/task-override.ts +15 -9
  178. package/lib/internal/task-qualification.ts +61 -0
  179. package/lib/internal/task-review.ts +6 -4
  180. package/lib/internal/task-short-id.ts +10 -1
  181. package/lib/internal/task-snapshot.ts +3 -1
  182. package/lib/internal/task-validate.ts +6 -2
  183. package/lib/internal/task-verify.ts +10 -7
  184. package/lib/internal/task-warning.ts +10 -4
  185. package/lib/platform/adapters.ts +3 -2
  186. package/lib/platform/change-request-git-evidence.ts +36 -9
  187. package/lib/platform/comment-safety.ts +250 -0
  188. package/lib/platform/completion-backfill.ts +9 -8
  189. package/lib/platform/context.ts +97 -134
  190. package/lib/platform/github-client.ts +2 -2
  191. package/lib/platform/github-provider.ts +1132 -0
  192. package/lib/platform/in-label-sync.ts +208 -0
  193. package/lib/platform/issue-comments.ts +255 -80
  194. package/lib/platform/issues.ts +344 -174
  195. package/lib/platform/metadata-labels.ts +5 -2
  196. package/lib/platform/none-provider.ts +35 -0
  197. package/lib/platform/pr-change-report.ts +639 -0
  198. package/lib/platform/pr-checks.ts +126 -106
  199. package/lib/platform/pr-review.ts +88 -85
  200. package/lib/platform/pr-summary.ts +581 -53
  201. package/lib/platform/provider-bridge.ts +82 -0
  202. package/lib/platform/provider-contract.ts +611 -0
  203. package/lib/platform/provider-loader.ts +307 -0
  204. package/lib/platform/provider-validation.ts +611 -0
  205. package/lib/platform/pull-request-metadata.ts +10 -7
  206. package/lib/platform/pull-requests.ts +781 -550
  207. package/lib/platform/release-notes.ts +76 -59
  208. package/lib/platform/releases.ts +77 -43
  209. package/lib/platform/repository-metadata.ts +453 -0
  210. package/lib/platform/resource-identity.ts +121 -0
  211. package/lib/platform/security-alerts.ts +191 -0
  212. package/lib/platform/task-identities.ts +40 -0
  213. package/lib/platform/types.ts +4 -2
  214. package/lib/platform/verification-required.ts +11 -13
  215. package/lib/platform/verification-sync.ts +128 -304
  216. package/lib/pr-review/evidence-grading.ts +25 -8
  217. package/lib/process-data/index.ts +7 -0
  218. package/lib/run/index.ts +62 -22
  219. package/lib/sandbox/commands/control-status.ts +26 -0
  220. package/lib/sandbox/commands/create.ts +46 -6
  221. package/lib/sandbox/commands/ls.ts +7 -0
  222. package/lib/sandbox/commands/rm.ts +1000 -87
  223. package/lib/sandbox/commands/show.ts +16 -0
  224. package/lib/sandbox/control/client.ts +203 -19
  225. package/lib/sandbox/control/completed-reentry.ts +98 -0
  226. package/lib/sandbox/control/container-identity.ts +27 -43
  227. package/lib/sandbox/control/executor.ts +180 -15
  228. package/lib/sandbox/control/lifecycle.ts +629 -17
  229. package/lib/sandbox/control/native-file-lock.ts +209 -0
  230. package/lib/sandbox/control/protocol.ts +118 -2
  231. package/lib/sandbox/control/server.ts +724 -34
  232. package/lib/sandbox/control/state.ts +257 -4
  233. package/lib/sandbox/control/task-view.ts +240 -0
  234. package/lib/sandbox/engines/authority.ts +365 -0
  235. package/lib/sandbox/engines/docker-exact-discovery.ts +132 -0
  236. package/lib/sandbox/index.ts +7 -0
  237. package/lib/sandbox/recovery.ts +21 -1
  238. package/lib/sandbox/workspace-view.ts +9 -3
  239. package/lib/sandbox/worktree-safety.ts +4 -1
  240. package/lib/server/index.ts +10 -0
  241. package/lib/server/protocol.ts +36 -1
  242. package/lib/task/activity-log.ts +8 -0
  243. package/lib/task/artifact-lifecycle.ts +70 -22
  244. package/lib/task/artifact-operations.ts +626 -0
  245. package/lib/task/artifact-repair-intent.ts +71 -0
  246. package/lib/task/artifact-schema.ts +192 -0
  247. package/lib/task/capabilities.ts +331 -0
  248. package/lib/task/commands/status.ts +38 -0
  249. package/lib/task/commit-operation.ts +12 -1
  250. package/lib/task/control-authority.ts +22 -9
  251. package/lib/task/create-service.ts +139 -14
  252. package/lib/task/create.ts +27 -0
  253. package/lib/task/current-contract.ts +6 -1
  254. package/lib/task/decision-details.ts +6 -2
  255. package/lib/task/events.ts +392 -16
  256. package/lib/task/finalization.ts +50 -14
  257. package/lib/task/human-override.ts +70 -46
  258. package/lib/task/index.ts +16 -3
  259. package/lib/task/invalidation-command.ts +86 -0
  260. package/lib/task/invalidation.ts +332 -0
  261. package/lib/task/ledger-intents.ts +35 -1
  262. package/lib/task/local-artifact-finalization.ts +432 -0
  263. package/lib/task/orchestration.ts +223 -133
  264. package/lib/task/pr-delivery-fact.ts +131 -148
  265. package/lib/task/qualification-audit.ts +599 -0
  266. package/lib/task/qualification-confirmation.ts +117 -0
  267. package/lib/task/qualification-intents.ts +150 -0
  268. package/lib/task/review-artifacts.ts +21 -22
  269. package/lib/task/review-finalization.ts +133 -4
  270. package/lib/task/review-fingerprint.ts +23 -2
  271. package/lib/task/rework-intent.ts +165 -0
  272. package/lib/task/sections.ts +6 -4
  273. package/lib/task/skill-scope.ts +24 -0
  274. package/lib/task/task-execution-lock.ts +23 -14
  275. package/lib/task/verification-config.ts +12 -0
  276. package/lib/task/verification-engine.ts +103 -17
  277. package/lib/task/verification.ts +4 -4
  278. package/lib/task/workflow-warning-intents.ts +1 -1
  279. package/lib/task/write.ts +19 -0
  280. package/lib/update.ts +1 -1
  281. package/lib/version.ts +15 -1
  282. package/package.json +12 -1
  283. package/templates/.agents/rules/README.en.md +3 -1
  284. package/templates/.agents/rules/README.zh-CN.md +3 -1
  285. package/templates/.agents/rules/decision-qualification.en.md +22 -0
  286. package/templates/.agents/rules/decision-qualification.zh-CN.md +22 -0
  287. package/templates/.agents/rules/evidence-reporting.en.md +28 -0
  288. package/templates/.agents/rules/evidence-reporting.zh-CN.md +28 -0
  289. package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
  290. package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
  291. package/templates/.agents/rules/issue-sync.en.md +2 -0
  292. package/templates/.agents/rules/issue-sync.zh-CN.md +2 -0
  293. package/templates/.agents/rules/label-milestone-setup.en.md +7 -7
  294. package/templates/.agents/rules/label-milestone-setup.zh-CN.md +7 -7
  295. package/templates/.agents/rules/local-artifact-repair.en.md +13 -2
  296. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -2
  297. package/templates/.agents/rules/pr-sync.en.md +10 -4
  298. package/templates/.agents/rules/pr-sync.zh-CN.md +17 -4
  299. package/templates/.agents/rules/security-alerts.en.md +8 -8
  300. package/templates/.agents/rules/security-alerts.zh-CN.md +8 -8
  301. package/templates/.agents/rules/task-short-id.en.md +1 -4
  302. package/templates/.agents/rules/task-short-id.zh-CN.md +1 -3
  303. package/templates/.agents/skills/analyze-task/SKILL.en.md +28 -5
  304. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +29 -5
  305. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -12
  306. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -12
  307. package/templates/.agents/skills/check-task/SKILL.en.md +16 -37
  308. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +16 -37
  309. package/templates/.agents/skills/close-codescan/SKILL.en.md +2 -2
  310. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
  311. package/templates/.agents/skills/close-dependabot/SKILL.en.md +2 -2
  312. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
  313. package/templates/.agents/skills/code-task/SKILL.en.md +41 -9
  314. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +43 -11
  315. package/templates/.agents/skills/code-task/config/verify.en.json +6 -14
  316. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +6 -14
  317. package/templates/.agents/skills/code-task/reference/code-rules.en.md +5 -4
  318. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +5 -4
  319. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
  320. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
  321. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +3 -1
  322. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +3 -1
  323. package/templates/.agents/skills/code-task/reference/report-template.en.md +42 -15
  324. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +39 -12
  325. package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +2 -0
  326. package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +2 -0
  327. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +3 -3
  328. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +3 -3
  329. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +10 -4
  330. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +10 -4
  331. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
  332. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
  333. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +4 -3
  334. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +4 -3
  335. package/templates/.agents/skills/complete-task/SKILL.en.md +26 -21
  336. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +25 -21
  337. package/templates/.agents/skills/create-pr/SKILL.en.md +7 -6
  338. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +7 -7
  339. package/templates/.agents/skills/create-pr/config/verify.json +1 -0
  340. package/templates/.agents/skills/create-pr/reference/change-report.en.md +16 -4
  341. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +16 -4
  342. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +5 -3
  343. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +5 -3
  344. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
  345. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
  346. package/templates/.agents/skills/create-pr/scripts/change-report.mjs +3 -0
  347. package/templates/.agents/skills/create-task/SKILL.en.md +8 -0
  348. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +7 -0
  349. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  350. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  351. package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
  352. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  353. package/templates/.agents/skills/init-labels/SKILL.en.md +8 -8
  354. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +8 -8
  355. package/templates/.agents/skills/init-milestones/SKILL.en.md +9 -11
  356. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +9 -11
  357. package/templates/.agents/skills/plan-task/SKILL.en.md +27 -4
  358. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +26 -4
  359. package/templates/.agents/skills/plan-task/config/verify.en.json +4 -14
  360. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +4 -14
  361. package/templates/.agents/skills/review-analysis/SKILL.en.md +17 -4
  362. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +17 -4
  363. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -18
  364. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -18
  365. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +50 -23
  366. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +45 -18
  367. package/templates/.agents/skills/review-code/SKILL.en.md +18 -5
  368. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +18 -5
  369. package/templates/.agents/skills/review-code/config/verify.en.json +1 -23
  370. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -23
  371. package/templates/.agents/skills/review-code/reference/report-template.en.md +49 -22
  372. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +45 -18
  373. package/templates/.agents/skills/review-plan/SKILL.en.md +17 -4
  374. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +17 -4
  375. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -18
  376. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -18
  377. package/templates/.agents/skills/review-plan/reference/report-template.en.md +49 -22
  378. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +45 -18
  379. package/templates/.agents/skills/review-pr/SKILL.en.md +5 -1
  380. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +5 -1
  381. package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -3
  382. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -3
  383. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +9 -3
  384. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +9 -3
  385. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +3 -1
  386. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +3 -1
  387. package/templates/.agents/skills/test/SKILL.en.md +13 -2
  388. package/templates/.agents/skills/test/SKILL.zh-CN.md +13 -2
  389. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +14 -0
  390. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  391. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  392. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +6 -0
  393. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +6 -0
  394. package/templates/.agents/templates/task.en.md +24 -1
  395. package/templates/.agents/templates/task.zh-CN.md +24 -1
  396. package/templates/.github/workflows/pr-label.yml +7 -26
  397. package/templates/.agents/scripts/security-alerts.github.sh +0 -151
  398. package/templates/.agents/scripts/security-alerts.sh +0 -31
  399. package/templates/.agents/skills/init-labels/scripts/init-labels.github.sh +0 -146
  400. package/templates/.agents/skills/init-labels/scripts/init-labels.sh +0 -32
  401. package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +0 -294
  402. package/templates/.agents/skills/init-milestones/scripts/init-milestones.sh +0 -6
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
3
4
  import { parseArgs } from 'node:util';
4
5
  import * as p from '@clack/prompts';
5
6
  import pc from 'picocolors';
@@ -19,7 +20,7 @@ import {
19
20
  import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from '../engine.ts';
20
21
  import { pruneSandboxDanglingImages } from '../image-prune.ts';
21
22
  import { assertManagedPath, removeManagedDir, removeWorktreeDir } from '../managed-fs.ts';
22
- import { runEngine, runOk, runOkEngine, runSafe, runSafeEngine } from '../shell.ts';
23
+ import { run, runEngine, runOk, runOkEngine, runSafe, runSafeEngine } from '../shell.ts';
23
24
  import {
24
25
  parseSandboxWorkspaceIdentity,
25
26
  resolveSandboxCleanupTarget,
@@ -28,8 +29,21 @@ import {
28
29
  type SandboxWorkspaceKey
29
30
  } from '../workspace-identity.ts';
30
31
  import { sandboxControlPaths, sandboxWorkspaceViewPaths } from '../workspace-view.ts';
31
- import { removeSandboxControlRoot, readSandboxControlManifest } from '../control/lifecycle.ts';
32
+ import {
33
+ SANDBOX_REMOVAL_JOURNAL_PHASES,
34
+ advanceSandboxRemovalJournalPhase,
35
+ claimSandboxRemovalJournal,
36
+ clearSandboxRemovalJournalRecord,
37
+ listSandboxRemovalJournals,
38
+ removeSandboxControlRoot,
39
+ readSandboxControlManifest,
40
+ sandboxRemovalPhaseIndex,
41
+ type SandboxRemovalJournal,
42
+ type SandboxRemovalTargetCommit
43
+ } from '../control/lifecycle.ts';
32
44
  import { inspectSandboxControlContainer } from '../control/container-identity.ts';
45
+ import { commandForSandboxAuthority } from '../engines/authority.ts';
46
+ import { acquireSandboxResourceLock, type SandboxResourceLock } from '../control/native-file-lock.ts';
33
47
  import { toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.ts';
34
48
  import { createSandboxCapabilityPlan } from '../agent-client-reconciler.ts';
35
49
  import type { SandboxTool } from '../tools.ts';
@@ -63,6 +77,649 @@ function isMissingPathError(error: unknown): boolean {
63
77
  && (error as { code?: unknown }).code === 'ENOENT');
64
78
  }
65
79
 
80
+ function digestCleanupValue(value: unknown): string {
81
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex');
82
+ }
83
+
84
+ function removalTargetDigest(config: SandboxConfig, target: RmTarget): string {
85
+ return digestCleanupValue({
86
+ project: config.project,
87
+ branch: target.effectiveBranch,
88
+ workspace: target.workspace,
89
+ controlRoots: target.controlRoots.map((candidate) => path.resolve(candidate)),
90
+ workspaceViewRoots: target.workspaceViewRoots.map((candidate) => path.resolve(candidate)),
91
+ managedPathCandidates: (target.managedPathCandidates ?? []).map((candidate) => path.resolve(candidate)).sort()
92
+ });
93
+ }
94
+
95
+ function removalPermitDigest(permits: ReadonlyMap<string, WorktreeRemovalPermit>): string {
96
+ return digestCleanupValue([...permits.entries()]
97
+ .map(([worktree, permit]) => [path.resolve(worktree), permit.mode, permit.snapshot.identity])
98
+ .sort(([left], [right]) => String(left).localeCompare(String(right))));
99
+ }
100
+
101
+ function removalPermitCommits(permits: ReadonlyMap<string, WorktreeRemovalPermit>): SandboxRemovalTargetCommit['permits'] {
102
+ return [...permits.entries()]
103
+ .map(([worktree, permit]) => ({
104
+ path: path.resolve(worktree),
105
+ mode: permit.mode,
106
+ snapshot: permit.snapshot
107
+ }))
108
+ .sort((left, right) => left.path.localeCompare(right.path));
109
+ }
110
+
111
+ function removalTargetCommit(
112
+ config: SandboxConfig,
113
+ target: RmTarget,
114
+ permits: ReadonlyMap<string, WorktreeRemovalPermit>,
115
+ removeWorktree: boolean,
116
+ removeBranch: boolean,
117
+ removeShare: boolean
118
+ ): SandboxRemovalTargetCommit {
119
+ return {
120
+ branch: target.effectiveBranch,
121
+ project: config.project,
122
+ controlRoot: path.resolve(target.controlRoots[0] ?? path.join(config.controlBase, config.project)),
123
+ targetDigest: removalTargetDigest(config, target),
124
+ permitDigest: removalPermitDigest(permits),
125
+ removeWorktree,
126
+ removeBranch,
127
+ removeShare,
128
+ worktreePaths: target.existingWorktrees.map((candidate) => path.resolve(candidate)).sort(),
129
+ workspaceViewPaths: target.workspaceViewRoots.map((candidate) => path.resolve(candidate)).sort(),
130
+ toolPaths: target.toolCandidates.flatMap(({ candidates }) => candidates.map((candidate) => path.resolve(candidate))).sort(),
131
+ shellPaths: shellConfigDirCandidates(config, target.effectiveBranch).map((candidate) => path.resolve(candidate)).sort(),
132
+ sharePath: path.resolve(shareBranchDir(config, target.effectiveBranch)),
133
+ permits: removalPermitCommits(permits)
134
+ };
135
+ }
136
+
137
+ function assertRemovalSelectionMatches(
138
+ expected: SandboxRemovalTargetCommit,
139
+ actual: SandboxRemovalTargetCommit
140
+ ): void {
141
+ const normalize = (value: SandboxRemovalTargetCommit): unknown => ({
142
+ ...value,
143
+ controlRoot: '',
144
+ permits: value.permits
145
+ });
146
+ if (JSON.stringify(normalize(expected)) !== JSON.stringify(normalize(actual))) {
147
+ throw new Error('SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH');
148
+ }
149
+ }
150
+
151
+ function advanceRemovalJournals(
152
+ project: string,
153
+ target: RmTarget,
154
+ targetDigest: string,
155
+ phase: SandboxRemovalJournal['phase'],
156
+ resourceLocks: ReadonlyMap<string, SandboxResourceLock>
157
+ ): SandboxRemovalJournal[] {
158
+ const targetIndex = sandboxRemovalPhaseIndex(phase);
159
+ return listSandboxRemovalJournals({
160
+ branch: target.effectiveBranch,
161
+ project,
162
+ targetDigest
163
+ }).map((journal) => {
164
+ let current = journal;
165
+ while (sandboxRemovalPhaseIndex(current.phase) < targetIndex) {
166
+ if (current.phase === 'target-committed' && phase === 'container-absent') {
167
+ const lock = resourceLocks.get(current.target.controlRoot);
168
+ if (!lock) throw new Error('SANDBOX_CONTROL_REMOVAL_LOCK_MISMATCH');
169
+ current = advanceSandboxRemovalJournalPhase(current, phase, { resourceLock: lock });
170
+ continue;
171
+ }
172
+ const next = SANDBOX_REMOVAL_JOURNAL_PHASES[sandboxRemovalPhaseIndex(current.phase) + 1];
173
+ if (!next) throw new Error('SANDBOX_CONTROL_REMOVAL_PHASE_TRANSITION_INVALID');
174
+ const lock = resourceLocks.get(current.target.controlRoot);
175
+ if (!lock) throw new Error('SANDBOX_CONTROL_REMOVAL_LOCK_MISMATCH');
176
+ current = advanceSandboxRemovalJournalPhase(current, next, { resourceLock: lock });
177
+ }
178
+ return current;
179
+ });
180
+ }
181
+
182
+ function prepareRemovalAction(
183
+ project: string,
184
+ target: RmTarget,
185
+ targetDigest: string,
186
+ startPhase: SandboxRemovalJournal['phase'],
187
+ completedPhase: SandboxRemovalJournal['phase'],
188
+ resourceLocks: ReadonlyMap<string, SandboxResourceLock>
189
+ ): RemovalActionPreparation {
190
+ const journals = listSandboxRemovalJournals({
191
+ branch: target.effectiveBranch,
192
+ project,
193
+ targetDigest
194
+ });
195
+ const startIndex = sandboxRemovalPhaseIndex(startPhase);
196
+ const completedIndex = sandboxRemovalPhaseIndex(completedPhase);
197
+ const shouldRun = journals.length === 0
198
+ || journals.some((journal) => sandboxRemovalPhaseIndex(journal.phase) < completedIndex);
199
+ const recovering = journals.some((journal) => {
200
+ const index = sandboxRemovalPhaseIndex(journal.phase);
201
+ return index >= startIndex && index < completedIndex;
202
+ });
203
+ advanceRemovalJournals(project, target, targetDigest, startPhase, resourceLocks);
204
+ return { run: shouldRun, recovering };
205
+ }
206
+
207
+ function assertRemovalPathsAbsent(paths: readonly string[]): void {
208
+ const remaining = paths.filter((candidate) => fs.existsSync(candidate));
209
+ if (remaining.length > 0) {
210
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${remaining[0]}`);
211
+ }
212
+ }
213
+
214
+ function assertBranchRemovalIdentity(
215
+ config: SandboxConfig,
216
+ branch: string,
217
+ permits: ReadonlyMap<string, WorktreeRemovalPermit>
218
+ ): void {
219
+ if (!runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', `refs/heads/${branch}`])) return;
220
+ const expectedHeads = new Set(
221
+ [...permits.values()]
222
+ .filter((permit) => permit.snapshot.branch === branch)
223
+ .map((permit) => permit.snapshot.head)
224
+ );
225
+ if (expectedHeads.size === 0) {
226
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
227
+ }
228
+ const actualHead = runSafe('git', ['-C', config.repoRoot, 'rev-parse', `refs/heads/${branch}`]);
229
+ if (!expectedHeads.has(actualHead)) {
230
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
231
+ }
232
+ }
233
+
234
+ function removalTombstonePath(targetDigest: string, kind: RemovalActionKind, source: string): string {
235
+ const sourceDigest = createHash('sha256')
236
+ .update(`${kind}\0${path.resolve(source)}`)
237
+ .digest('hex')
238
+ .slice(0, 24);
239
+ return path.join(
240
+ path.dirname(path.resolve(source)),
241
+ `.agent-infra-removal-${targetDigest.slice(0, 16)}-${kind}-${sourceDigest}`
242
+ );
243
+ }
244
+
245
+ type RemovalTombstoneOwnership = Readonly<{
246
+ version: 1;
247
+ targetDigest: string;
248
+ permitDigest: string;
249
+ kind: RemovalActionKind;
250
+ source: string;
251
+ }>;
252
+
253
+ type RemovalSourceIdentity = Readonly<{
254
+ dev: string;
255
+ ino: string;
256
+ }>;
257
+
258
+ type RemovalTombstoneRecord = RemovalTombstoneOwnership & Readonly<{
259
+ sourceIdentity: RemovalSourceIdentity;
260
+ }>;
261
+
262
+ const REMOVAL_TOMBSTONE_MARKER = '.agent-infra-removal-ownership.json';
263
+ const REMOVAL_TOMBSTONE_PAYLOAD = 'payload';
264
+
265
+ function removalSourceClaimPath(source: string, ownership: RemovalTombstoneOwnership): string {
266
+ const claimDigest = createHash('sha256')
267
+ .update(JSON.stringify(ownership))
268
+ .digest('hex')
269
+ .slice(0, 24);
270
+ return path.join(source, `.agent-infra-removal-source-${claimDigest}.json`);
271
+ }
272
+
273
+ function removalSourceIdentity(source: string): RemovalSourceIdentity {
274
+ const stat = fs.lstatSync(source, { bigint: true });
275
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.dev === 0n || stat.ino === 0n) {
276
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
277
+ }
278
+ return { dev: String(stat.dev), ino: String(stat.ino) };
279
+ }
280
+
281
+ function sameRemovalSourceIdentity(left: RemovalSourceIdentity, right: RemovalSourceIdentity): boolean {
282
+ return left.dev === right.dev && left.ino === right.ino;
283
+ }
284
+
285
+ function removalSourceClaimMatches(
286
+ source: string,
287
+ ownership: RemovalTombstoneOwnership,
288
+ identity: RemovalSourceIdentity
289
+ ): boolean {
290
+ try {
291
+ const claimPath = removalSourceClaimPath(source, ownership);
292
+ const claimStat = fs.lstatSync(claimPath);
293
+ if (!claimStat.isFile() || claimStat.isSymbolicLink()) return false;
294
+ const record = JSON.parse(fs.readFileSync(claimPath, 'utf8')) as Partial<RemovalTombstoneRecord>;
295
+ return record.version === ownership.version
296
+ && record.targetDigest === ownership.targetDigest
297
+ && record.permitDigest === ownership.permitDigest
298
+ && record.kind === ownership.kind
299
+ && typeof record.source === 'string'
300
+ && path.resolve(record.source) === path.resolve(ownership.source)
301
+ && record.sourceIdentity?.dev === identity.dev
302
+ && record.sourceIdentity?.ino === identity.ino;
303
+ } catch {
304
+ return false;
305
+ }
306
+ }
307
+
308
+ function isOwnedRemovalSourceClaim(source: string, ownership: RemovalTombstoneOwnership): boolean {
309
+ try {
310
+ const claimPath = removalSourceClaimPath(source, ownership);
311
+ const claimStat = fs.lstatSync(claimPath);
312
+ if (!claimStat.isFile() || claimStat.isSymbolicLink()) return false;
313
+ const record = JSON.parse(fs.readFileSync(claimPath, 'utf8')) as Partial<RemovalTombstoneRecord>;
314
+ return record.version === ownership.version
315
+ && record.targetDigest === ownership.targetDigest
316
+ && record.permitDigest === ownership.permitDigest
317
+ && record.kind === ownership.kind
318
+ && typeof record.source === 'string'
319
+ && path.resolve(record.source) === path.resolve(ownership.source);
320
+ } catch {
321
+ return false;
322
+ }
323
+ }
324
+
325
+ function removeOwnedSourceClaim(source: string, ownership: RemovalTombstoneOwnership): void {
326
+ try {
327
+ const claimPath = removalSourceClaimPath(source, ownership);
328
+ if (isOwnedRemovalSourceClaim(source, ownership)) fs.unlinkSync(claimPath);
329
+ } catch {
330
+ // Preserve the source when the claim cannot be revalidated.
331
+ }
332
+ }
333
+
334
+ function readRemovalTombstoneRecord(root: string, tombstone: string): RemovalTombstoneRecord | null {
335
+ assertManagedPath(root, tombstone);
336
+ try {
337
+ const tombstoneStat = fs.lstatSync(tombstone);
338
+ const markerStat = fs.lstatSync(removalTombstoneMarker(tombstone));
339
+ if (!tombstoneStat.isDirectory() || tombstoneStat.isSymbolicLink()
340
+ || !markerStat.isFile() || markerStat.isSymbolicLink()) return null;
341
+ const record = JSON.parse(fs.readFileSync(removalTombstoneMarker(tombstone), 'utf8')) as Partial<RemovalTombstoneRecord>;
342
+ if (record.version !== 1
343
+ || typeof record.targetDigest !== 'string'
344
+ || typeof record.permitDigest !== 'string'
345
+ || typeof record.kind !== 'string'
346
+ || typeof record.source !== 'string'
347
+ || record.sourceIdentity?.dev === undefined
348
+ || record.sourceIdentity.ino === undefined) return null;
349
+ return record as RemovalTombstoneRecord;
350
+ } catch {
351
+ return null;
352
+ }
353
+ }
354
+
355
+ function removalTombstonePayload(tombstone: string): string {
356
+ return path.join(tombstone, REMOVAL_TOMBSTONE_PAYLOAD);
357
+ }
358
+
359
+ function removalTombstoneMarker(tombstone: string): string {
360
+ return path.join(tombstone, REMOVAL_TOMBSTONE_MARKER);
361
+ }
362
+
363
+ function isOwnedRemovalTombstone(
364
+ root: string,
365
+ tombstone: string,
366
+ ownership: RemovalTombstoneOwnership
367
+ ): boolean {
368
+ const record = readRemovalTombstoneRecord(root, tombstone);
369
+ return record !== null
370
+ && record.version === 1
371
+ && record.targetDigest === ownership.targetDigest
372
+ && record.permitDigest === ownership.permitDigest
373
+ && record.kind === ownership.kind
374
+ && typeof record.source === 'string'
375
+ && path.resolve(record.source) === path.resolve(ownership.source);
376
+ }
377
+
378
+ function isOwnedRemovalPayload(
379
+ root: string,
380
+ tombstone: string,
381
+ ownership: RemovalTombstoneOwnership
382
+ ): boolean {
383
+ const record = readRemovalTombstoneRecord(root, tombstone);
384
+ if (!record || record.targetDigest !== ownership.targetDigest
385
+ || record.permitDigest !== ownership.permitDigest
386
+ || record.kind !== ownership.kind
387
+ || path.resolve(record.source) !== path.resolve(ownership.source)) return false;
388
+ try {
389
+ const payload = removalTombstonePayload(tombstone);
390
+ const payloadIdentity = removalSourceIdentity(payload);
391
+ return sameRemovalSourceIdentity(payloadIdentity, record.sourceIdentity)
392
+ && removalSourceClaimMatches(payload, ownership, record.sourceIdentity);
393
+ } catch {
394
+ return false;
395
+ }
396
+ }
397
+
398
+ function assertOwnedRemovalTombstone(
399
+ root: string,
400
+ tombstone: string,
401
+ ownership: RemovalTombstoneOwnership
402
+ ): void {
403
+ if (!isOwnedRemovalTombstone(root, tombstone, ownership)) {
404
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${ownership.source}`);
405
+ }
406
+ }
407
+
408
+ function shouldStageManagedRemoval(source: string, tombstone: string, recovering: boolean): boolean {
409
+ return fs.existsSync(source) || (recovering && fs.existsSync(tombstone));
410
+ }
411
+
412
+ function stageManagedRemoval(
413
+ root: string,
414
+ source: string,
415
+ tombstone: string,
416
+ recovering: boolean,
417
+ ownership: RemovalTombstoneOwnership
418
+ ): void {
419
+ assertManagedPath(root, source);
420
+ assertManagedPath(root, tombstone);
421
+ const sourceExists = fs.existsSync(source);
422
+ const tombstoneExists = fs.existsSync(tombstone);
423
+ if (tombstoneExists) {
424
+ assertOwnedRemovalTombstone(root, tombstone, ownership);
425
+ if (sourceExists) {
426
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
427
+ }
428
+ if (recovering) {
429
+ if (!isOwnedRemovalPayload(root, tombstone, ownership)) {
430
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
431
+ }
432
+ return;
433
+ }
434
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
435
+ }
436
+ if (recovering) {
437
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_ACTION_OUTCOME_UNKNOWN: ${source}`);
438
+ }
439
+ if (!sourceExists) return;
440
+ // The operator authorizes cleanup of this managed target after execution stops.
441
+ // Path checks detect conflicts; they do not provide an atomic source-instance move.
442
+ const expectedIdentity = removalSourceIdentity(source);
443
+ const claimPath = removalSourceClaimPath(source, ownership);
444
+ try {
445
+ fs.writeFileSync(claimPath, `${JSON.stringify({ ...ownership, sourceIdentity: expectedIdentity })}\n`, {
446
+ encoding: 'utf8',
447
+ mode: 0o600,
448
+ flag: 'wx'
449
+ });
450
+ } catch {
451
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
452
+ }
453
+ if (!sameRemovalSourceIdentity(expectedIdentity, removalSourceIdentity(source))) {
454
+ removeOwnedSourceClaim(source, ownership);
455
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
456
+ }
457
+ let moved = false;
458
+ try {
459
+ fs.mkdirSync(tombstone, { mode: 0o700 });
460
+ fs.writeFileSync(removalTombstoneMarker(tombstone), `${JSON.stringify({ ...ownership, sourceIdentity: expectedIdentity })}\n`, {
461
+ encoding: 'utf8',
462
+ mode: 0o600,
463
+ flag: 'wx'
464
+ });
465
+ fs.renameSync(source, removalTombstonePayload(tombstone));
466
+ moved = true;
467
+ if (!isOwnedRemovalPayload(root, tombstone, ownership)) {
468
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
469
+ }
470
+ } catch (error) {
471
+ if (!moved) removeOwnedSourceClaim(source, ownership);
472
+ throw error;
473
+ }
474
+ }
475
+
476
+ function cleanupManagedTombstones(
477
+ root: string,
478
+ targetDigest: string,
479
+ permitDigest: string,
480
+ kind: RemovalActionKind,
481
+ sources: readonly string[]
482
+ ): void {
483
+ for (const source of sources) {
484
+ const tombstone = removalTombstonePath(targetDigest, kind, source);
485
+ if (!fs.existsSync(tombstone)) continue;
486
+ const ownership = { version: 1, targetDigest, permitDigest, kind, source } as const;
487
+ if (!isOwnedRemovalTombstone(root, tombstone, ownership)) continue;
488
+ if (!isOwnedRemovalPayload(root, tombstone, ownership)) {
489
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
490
+ }
491
+ removeManagedDir(root, tombstone);
492
+ }
493
+ }
494
+
495
+ function branchRemovalTombstoneRef(targetDigest: string): string {
496
+ return `refs/agent-infra/sandbox-removal/${targetDigest}/branch`;
497
+ }
498
+
499
+ function branchPermitHead(
500
+ branch: string,
501
+ permits: ReadonlyMap<string, WorktreeRemovalPermit>
502
+ ): string | null {
503
+ const heads = new Set(
504
+ [...permits.values()]
505
+ .filter((permit) => permit.snapshot.branch === branch)
506
+ .map((permit) => permit.snapshot.head)
507
+ );
508
+ return heads.size === 1 ? [...heads][0]! : null;
509
+ }
510
+
511
+ function stageBranchRemoval(
512
+ config: SandboxConfig,
513
+ branch: string,
514
+ targetDigest: string,
515
+ permits: ReadonlyMap<string, WorktreeRemovalPermit>,
516
+ recovering: boolean
517
+ ): void {
518
+ const expectedHead = branchPermitHead(branch, permits);
519
+ if (!expectedHead) throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
520
+ const branchName = `refs/heads/${branch}`;
521
+ const tombstone = branchRemovalTombstoneRef(targetDigest);
522
+ const branchExists = runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', branchName]);
523
+ const tombstoneExists = runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', tombstone]);
524
+ if (branchExists && tombstoneExists) {
525
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
526
+ }
527
+ if (recovering) {
528
+ if (branchExists && !tombstoneExists) {
529
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_ACTION_OUTCOME_UNKNOWN: ${branch}`);
530
+ }
531
+ if (tombstoneExists && runSafe('git', ['-C', config.repoRoot, 'rev-parse', tombstone]) !== expectedHead) {
532
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
533
+ }
534
+ return;
535
+ }
536
+ if (!branchExists) return;
537
+ assertBranchRemovalIdentity(config, branch, permits);
538
+ const actualHead = runSafe('git', ['-C', config.repoRoot, 'rev-parse', branchName]);
539
+ if (!runOk('git', ['-C', config.repoRoot, 'update-ref', tombstone, actualHead, '0'.repeat(40)])) {
540
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
541
+ }
542
+ if (!runOk('git', ['-C', config.repoRoot, 'update-ref', '-d', branchName, actualHead])) {
543
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
544
+ }
545
+ }
546
+
547
+ function cleanupBranchTombstone(
548
+ config: SandboxConfig,
549
+ branch: string,
550
+ targetDigest: string,
551
+ permits: ReadonlyMap<string, WorktreeRemovalPermit>
552
+ ): void {
553
+ const tombstone = branchRemovalTombstoneRef(targetDigest);
554
+ if (!runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', tombstone])) return;
555
+ const expectedHead = branchPermitHead(branch, permits);
556
+ if (!expectedHead || runSafe('git', ['-C', config.repoRoot, 'rev-parse', tombstone]) !== expectedHead) {
557
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
558
+ }
559
+ if (!runOk('git', ['-C', config.repoRoot, 'update-ref', '-d', tombstone, expectedHead])) {
560
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
561
+ }
562
+ }
563
+
564
+ function worktreeRegistration(
565
+ config: SandboxConfig,
566
+ worktree: string
567
+ ): { head: string; branch: string | null } | null {
568
+ const lines = runSafe('git', ['-C', config.repoRoot, 'worktree', 'list', '--porcelain']).split('\n');
569
+ let current: { path: string; head: string; branch: string | null } | null = null;
570
+ const flush = (): { head: string; branch: string | null } | null => {
571
+ if (!current || path.resolve(current.path) !== path.resolve(worktree)) return null;
572
+ return { head: current.head, branch: current.branch };
573
+ };
574
+ for (const line of lines) {
575
+ if (line.startsWith('worktree ')) {
576
+ const previous = flush();
577
+ if (previous) return previous;
578
+ current = { path: line.slice('worktree '.length), head: '', branch: null };
579
+ } else if (current && line.startsWith('HEAD ')) {
580
+ current.head = line.slice('HEAD '.length);
581
+ } else if (current && line.startsWith('branch ')) {
582
+ current.branch = line.slice('branch '.length);
583
+ }
584
+ }
585
+ return flush();
586
+ }
587
+
588
+ function assertWorktreeRegistrationIdentity(
589
+ config: SandboxConfig,
590
+ worktree: string,
591
+ permit: WorktreeRemovalPermit
592
+ ): void {
593
+ const registration = worktreeRegistration(config, worktree);
594
+ if (!registration) return;
595
+ if (registration.head !== permit.snapshot.head
596
+ || registration.branch !== `refs/heads/${permit.snapshot.branch}`) {
597
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${worktree}`);
598
+ }
599
+ }
600
+
601
+ function pruneWorktreeRegistration(config: SandboxConfig, worktree: string): void {
602
+ runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
603
+ if (worktreeRegistration(config, worktree)) {
604
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_ACTION_OUTCOME_UNKNOWN: ${worktree}`);
605
+ }
606
+ }
607
+
608
+ function stageWorktreeRemoval(
609
+ config: SandboxConfig,
610
+ worktree: string,
611
+ permit: WorktreeRemovalPermit,
612
+ tombstone: string,
613
+ recovering: boolean,
614
+ ownership: RemovalTombstoneOwnership
615
+ ): void {
616
+ assertManagedPath(config.worktreeBase, worktree);
617
+ assertManagedPath(config.worktreeBase, tombstone);
618
+ const worktreeExists = fs.existsSync(worktree);
619
+ const tombstoneExists = fs.existsSync(tombstone);
620
+ if (worktreeExists || !recovering) verifyWorktreePermit(permit);
621
+ if (recovering) {
622
+ if (worktreeExists || !tombstoneExists) {
623
+ if (worktreeExists && tombstoneExists) {
624
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${worktree}`);
625
+ }
626
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_ACTION_OUTCOME_UNKNOWN: ${worktree}`);
627
+ }
628
+ assertOwnedRemovalTombstone(config.worktreeBase, tombstone, ownership);
629
+ assertWorktreeRegistrationIdentity(config, worktree, permit);
630
+ pruneWorktreeRegistration(config, worktree);
631
+ return;
632
+ }
633
+ if (!worktreeExists) return;
634
+ stageManagedRemoval(config.worktreeBase, worktree, tombstone, false, ownership);
635
+ pruneWorktreeRegistration(config, worktree);
636
+ }
637
+
638
+ function cleanupCompletedRemovalArtifacts(
639
+ config: SandboxConfig,
640
+ target: RmTarget,
641
+ committedTarget: SandboxRemovalTargetCommit,
642
+ targetDigest: string
643
+ ): void {
644
+ const journals = listSandboxRemovalJournals({
645
+ branch: target.effectiveBranch,
646
+ project: config.project,
647
+ targetDigest
648
+ });
649
+ if (journals.length === 0) return;
650
+ const completed = (phase: SandboxRemovalJournal['phase']): boolean => journals.every((journal) => (
651
+ sandboxRemovalPhaseIndex(journal.phase) >= sandboxRemovalPhaseIndex(phase)
652
+ ));
653
+ if (completed('workspace-removed')) {
654
+ cleanupManagedTombstones(
655
+ config.workspaceViewBase, targetDigest, committedTarget.permitDigest, 'workspace', target.workspaceViewRoots
656
+ );
657
+ cleanupManagedTombstones(
658
+ path.join(config.controlBase, config.project), targetDigest, committedTarget.permitDigest, 'workspace', target.controlRoots
659
+ );
660
+ cleanupManagedTombstones(
661
+ config.worktreeBase, targetDigest, committedTarget.permitDigest, 'worktree', committedTarget.worktreePaths
662
+ );
663
+ }
664
+ if (completed('branch-removed')) {
665
+ cleanupBranchTombstone(config, target.effectiveBranch, targetDigest, new Map(
666
+ committedTarget.permits.map((permit) => [path.resolve(permit.path), {
667
+ mode: permit.mode,
668
+ snapshot: permit.snapshot
669
+ }])
670
+ ));
671
+ }
672
+ if (completed('tool-removed')) {
673
+ cleanupManagedTombstones(config.home, targetDigest, committedTarget.permitDigest, 'tool', committedTarget.toolPaths);
674
+ }
675
+ if (completed('shell-removed')) {
676
+ cleanupManagedTombstones(config.shellConfigBase, targetDigest, committedTarget.permitDigest, 'shell', committedTarget.shellPaths);
677
+ }
678
+ if (completed('share-removed')) {
679
+ cleanupManagedTombstones(config.shareBase, targetDigest, committedTarget.permitDigest, 'share', [committedTarget.sharePath]);
680
+ }
681
+ }
682
+
683
+ function removalActionPaths(target: RmTarget, config: SandboxConfig, phase: SandboxRemovalJournal['phase']): string[] {
684
+ if (phase === 'workspace-removed') {
685
+ return [...target.workspaceViewRoots, ...target.controlRoots];
686
+ }
687
+ if (phase === 'tool-removed') {
688
+ return target.toolCandidates.flatMap(({ candidates }) => candidates);
689
+ }
690
+ if (phase === 'shell-removed') {
691
+ return shellConfigDirCandidates(config, target.effectiveBranch);
692
+ }
693
+ if (phase === 'share-removed') {
694
+ return [shareBranchDir(config, target.effectiveBranch)];
695
+ }
696
+ return [];
697
+ }
698
+
699
+ function permitsFromJournal(
700
+ journals: readonly SandboxRemovalJournal[],
701
+ target: RmTarget,
702
+ expectedTargetDigest: string
703
+ ): Map<string, WorktreeRemovalPermit> {
704
+ const permits = new Map<string, WorktreeRemovalPermit>();
705
+ for (const journal of journals) {
706
+ if (journal.target.targetDigest !== expectedTargetDigest) continue;
707
+ if (journal.target.branch !== target.effectiveBranch) {
708
+ throw new Error('SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH');
709
+ }
710
+ for (const entry of journal.target.permits) {
711
+ if (path.resolve(entry.path) !== path.resolve(entry.snapshot.worktree)) {
712
+ throw new Error('SANDBOX_CONTROL_REMOVAL_PERMIT_MISMATCH');
713
+ }
714
+ permits.set(path.resolve(entry.path), {
715
+ mode: entry.mode,
716
+ snapshot: entry.snapshot
717
+ });
718
+ }
719
+ }
720
+ return permits;
721
+ }
722
+
66
723
  export function sandboxManagedPathKey(
67
724
  candidate: string,
68
725
  platform: NodeJS.Platform = process.platform,
@@ -95,6 +752,13 @@ type RmTarget = {
95
752
  workspaceViewRoots: string[];
96
753
  };
97
754
 
755
+ type RemovalActionKind = 'workspace' | 'worktree' | 'tool' | 'shell' | 'share';
756
+
757
+ type RemovalActionPreparation = Readonly<{
758
+ run: boolean;
759
+ recovering: boolean;
760
+ }>;
761
+
98
762
  type CleanupCandidate = Readonly<{
99
763
  row: SandboxRow;
100
764
  cleanupTarget: SandboxCleanupTarget;
@@ -425,15 +1089,17 @@ async function removeExactSandboxContainer(
425
1089
  const deadlineAt = Date.now() + timeoutMs;
426
1090
  const remaining = (): number => Math.max(1, deadlineAt - Date.now());
427
1091
  const inspect = () => inspectSandboxControlContainer(manifest, { timeoutMs: remaining() });
1092
+ const runAuthorityCommand = (args: string[]): boolean => {
1093
+ const command = manifest.authorityEvidence
1094
+ ? commandForSandboxAuthority(manifest.authorityEvidence, 'docker', args)
1095
+ : null;
1096
+ return command ? runOk(command.cmd, command.args, { timeout: remaining() })
1097
+ : runOkEngine(engine, 'docker', args, { timeout: remaining() });
1098
+ };
428
1099
  const before = await inspect();
429
1100
  if (before.state === 'unknown') throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${before.reason}`);
430
1101
  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
- )) {
1102
+ if (before.running && !runAuthorityCommand(['stop', '--timeout', '1', manifest.containerIdentity.id])) {
437
1103
  const afterStop = await inspect();
438
1104
  if (afterStop.state !== 'absent' && afterStop.state !== 'found') {
439
1105
  throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterStop.reason}`);
@@ -444,7 +1110,7 @@ async function removeExactSandboxContainer(
444
1110
  }
445
1111
  const afterStop = await inspect();
446
1112
  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() })) {
1113
+ if (afterStop.state === 'found' && !runAuthorityCommand(['rm', manifest.containerIdentity.id])) {
448
1114
  const afterRemove = await inspect();
449
1115
  if (afterRemove.state !== 'absent') {
450
1116
  throw new Error(`Failed to remove sandbox container: ${manifest.containerIdentity.id}`);
@@ -524,7 +1190,7 @@ async function authorizeWorktrees(
524
1190
  }
525
1191
  p.log.warn(formatWorktreeSnapshot(inspection.snapshot));
526
1192
  const confirmed = await confirm({
527
- message: 'Discard these exact uncommitted changes?',
1193
+ message: 'Discard this worktree and all its uncommitted changes, including later changes?',
528
1194
  initialValue: false
529
1195
  });
530
1196
  if (isCancel(confirmed) || !confirmed) throw new Error('Dirty worktree removal cancelled; nothing was deleted');
@@ -554,54 +1220,193 @@ async function rmOne(
554
1220
  p.intro(pc.cyan(`Removing sandbox for ${branch}`));
555
1221
  }
556
1222
 
1223
+ const targetDigest = removalTargetDigest(config, target);
1224
+ const existingJournals = listSandboxRemovalJournals({
1225
+ branch: effectiveBranch,
1226
+ project: config.project,
1227
+ targetDigest
1228
+ });
1229
+ const persistedTarget = existingJournals[0]?.target;
1230
+ for (const journal of existingJournals) {
1231
+ if (!persistedTarget) break;
1232
+ assertRemovalSelectionMatches(persistedTarget, journal.target);
1233
+ }
557
1234
  const recovery = options.permits
558
1235
  ? new Map<string, WorktreeRecoveryContext>()
559
1236
  : recoveryContexts(config, target, existingWorktrees);
1237
+ const journalPermits = options.permits ? new Map<string, WorktreeRemovalPermit>()
1238
+ : permitsFromJournal(existingJournals, target, targetDigest);
560
1239
  const permits = options.permits
561
1240
  ? 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
- });
1241
+ : journalPermits.size > 0
1242
+ ? journalPermits
1243
+ : await authorizeWorktrees(existingWorktrees, {
1244
+ allowDirtyDiscard: options.allowDirtyDiscard ?? true,
1245
+ assumeYes: Boolean(options.assumeYes)
1246
+ }, {
1247
+ ...options.prompt,
1248
+ interactive: options.interactive ?? Boolean(process.stdin.isTTY),
1249
+ recovery
1250
+ });
570
1251
  for (const worktree of existingWorktrees) {
571
1252
  const permit = permits.get(path.resolve(worktree));
572
1253
  if (!permit) throw new Error(`Missing worktree removal permit: ${worktree}`);
573
- verifyWorktreePermit(permit);
1254
+ const worktreeTombstone = removalTombstonePath(targetDigest, 'worktree', worktree);
1255
+ const workspacePhasePending = existingJournals.some((journal) => (
1256
+ sandboxRemovalPhaseIndex(journal.phase) < sandboxRemovalPhaseIndex('workspace-removed')
1257
+ ));
1258
+ if (fs.existsSync(worktree)
1259
+ || (workspacePhasePending
1260
+ && !isOwnedRemovalTombstone(config.worktreeBase, worktreeTombstone, {
1261
+ version: 1,
1262
+ targetDigest,
1263
+ permitDigest: removalPermitDigest(permits),
1264
+ kind: 'worktree',
1265
+ source: worktree
1266
+ }))) {
1267
+ verifyWorktreePermit(permit);
1268
+ }
574
1269
  }
575
1270
 
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
- });
1271
+ const recoveredRemovalChoice = persistedTarget;
1272
+ const shouldRemoveWorktree = recoveredRemovalChoice
1273
+ ? recoveredRemovalChoice.removeWorktree
1274
+ : existingWorktrees.length === 0
1275
+ ? false
1276
+ : options.assumeYes
1277
+ ? true
1278
+ : await confirm({
1279
+ message: `Remove worktree(s): ${existingWorktrees.join(', ')}?`,
1280
+ initialValue: true
1281
+ });
584
1282
  if (isCancel(shouldRemoveWorktree)) {
585
1283
  p.outro('Cancelled');
586
1284
  return;
587
1285
  }
588
1286
 
589
- preflightRmTarget(config, target);
590
- const coordinatedContainers = new Set<string>();
1287
+ const shouldDeleteBranch = recoveredRemovalChoice
1288
+ ? recoveredRemovalChoice.removeBranch
1289
+ : shouldRemoveWorktree && existingWorktrees.length > 0
1290
+ ? options.assumeYes
1291
+ ? true
1292
+ : await confirm({
1293
+ message: `Also delete local branch '${effectiveBranch}'?`,
1294
+ initialValue: true
1295
+ })
1296
+ : false;
1297
+ if (isCancel(shouldDeleteBranch)) {
1298
+ p.outro('Cancelled');
1299
+ return;
1300
+ }
1301
+
1302
+ const sharePath = path.resolve(shareBranchDir(config, effectiveBranch));
1303
+ const shouldRemoveShare = recoveredRemovalChoice
1304
+ ? recoveredRemovalChoice.removeShare
1305
+ : fs.existsSync(sharePath)
1306
+ ? options.assumeYes
1307
+ ? true
1308
+ : await confirm({
1309
+ message: `Remove share dir for branch '${effectiveBranch}' (${sharePath})?`,
1310
+ initialValue: true
1311
+ })
1312
+ : false;
1313
+ if (isCancel(shouldRemoveShare)) {
1314
+ p.outro('Cancelled');
1315
+ return;
1316
+ }
1317
+
1318
+ const committedTarget = persistedTarget
1319
+ ? {
1320
+ ...persistedTarget,
1321
+ permitDigest: removalPermitDigest(permits),
1322
+ permits: removalPermitCommits(permits)
1323
+ }
1324
+ : removalTargetCommit(
1325
+ config,
1326
+ target,
1327
+ permits,
1328
+ Boolean(shouldRemoveWorktree),
1329
+ Boolean(shouldDeleteBranch),
1330
+ Boolean(shouldRemoveShare)
1331
+ );
1332
+ if (persistedTarget) assertRemovalSelectionMatches(persistedTarget, committedTarget);
1333
+
1334
+ const coordinatedManifests = controlRoots
1335
+ .filter((candidate) => fs.existsSync(candidate))
1336
+ .flatMap((root) => {
1337
+ const manifestPath = path.join(root, 'manifest.json');
1338
+ return fs.existsSync(manifestPath)
1339
+ ? [[root, readSandboxControlManifest(manifestPath), fs.readFileSync(manifestPath, 'utf8')] as const]
1340
+ : [];
1341
+ });
1342
+ const resourceLocks = new Map<string, SandboxResourceLock>();
1343
+ const recoveredContainerNames = new Set<string>();
1344
+ try {
1345
+ for (const [root, manifest] of [...coordinatedManifests].sort(([left], [right]) => left.localeCompare(right))) {
1346
+ resourceLocks.set(root, acquireSandboxResourceLock(
1347
+ `${manifest.engine}:${manifest.containerIdentity.id}`,
1348
+ { lockDomain: manifest.authorityEvidence.lockDomain }
1349
+ ));
1350
+ }
1351
+ for (const journal of existingJournals.filter((candidate) => !fs.existsSync(candidate.target.controlRoot))) {
1352
+ if (!target.controlRoots.some((root) => path.resolve(root) === path.resolve(journal.target.controlRoot))) {
1353
+ throw new Error('SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH');
1354
+ }
1355
+ assertRemovalSelectionMatches(committedTarget, journal.target);
1356
+ if (journal.target.permitDigest !== removalPermitDigest(permits)) {
1357
+ throw new Error('SANDBOX_CONTROL_REMOVAL_PERMIT_MISMATCH');
1358
+ }
1359
+ const lock = acquireSandboxResourceLock(`${journal.engine}:${journal.containerId}`, {
1360
+ lockDomain: journal.lockDomain
1361
+ });
1362
+ resourceLocks.set(journal.target.controlRoot, lock);
1363
+ const current = listSandboxRemovalJournals({
1364
+ branch: journal.target.branch,
1365
+ project: journal.target.project,
1366
+ targetDigest: journal.target.targetDigest
1367
+ }).find((candidate) => candidate.carrierIdentityDigest === journal.carrierIdentityDigest);
1368
+ if (!current || current.revision !== journal.revision || current.handoffId !== journal.handoffId) {
1369
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
1370
+ }
1371
+ const claimed = claimSandboxRemovalJournal(current, { resourceLock: lock });
1372
+ if (!['carrier-finalizing', 'carrier-removed', 'workspace-finalizing', 'workspace-removed',
1373
+ 'branch-finalizing', 'branch-removed', 'tool-finalizing', 'tool-removed',
1374
+ 'shell-finalizing', 'shell-removed', 'share-finalizing', 'share-removed', 'completed']
1375
+ .includes(claimed.phase)) {
1376
+ throw new Error('SANDBOX_CONTROL_REMOVE_RECOVERY_PENDING');
1377
+ }
1378
+ const recovered = claimed.phase === 'carrier-finalizing'
1379
+ ? advanceSandboxRemovalJournalPhase(claimed, 'carrier-removed', { resourceLock: lock })
1380
+ : claimed;
1381
+ recoveredContainerNames.add(controlRootContainer(config, journal.target.controlRoot));
1382
+ }
1383
+ cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest);
1384
+ preflightRmTarget(config, target);
1385
+ const coordinatedContainers = new Set(recoveredContainerNames);
591
1386
  for (const root of controlRoots.filter((candidate) => fs.existsSync(candidate))) {
592
1387
  assertLegacyCandidateEvidence(root, config.controlBase, config, effectiveBranch, matchedContainers);
593
1388
  assertControlRootMatchesTarget(root, effectiveBranch, workspace);
594
1389
  const manifestPath = path.join(root, 'manifest.json');
595
1390
  if (!fs.existsSync(manifestPath)) continue;
1391
+ const currentRaw = fs.readFileSync(manifestPath, 'utf8');
596
1392
  const manifest = readSandboxControlManifest(manifestPath);
1393
+ const initial = coordinatedManifests.find(([candidate]) => candidate === root);
1394
+ if (!initial || currentRaw !== initial[2]) throw new Error('SANDBOX_CONTROL_MANIFEST_CHANGED');
597
1395
  coordinatedContainers.add(manifest.container);
598
1396
  await removeSandboxControlRoot(root, {
599
1397
  inspectContainer: (timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }),
600
- removeContainer: (timeoutMs) => removeExactSandboxContainer(engine, manifest, timeoutMs)
1398
+ removeContainer: (timeoutMs) => removeExactSandboxContainer(engine, manifest, timeoutMs),
1399
+ resourceLock: resourceLocks.get(root),
1400
+ retainRemovalJournal: true,
1401
+ removalTarget: { ...committedTarget, controlRoot: path.resolve(root) }
601
1402
  });
602
1403
  removeEmptyManagedParent(path.join(config.controlBase, config.project), root);
603
1404
  }
604
1405
 
1406
+ if ([...recoveredContainerNames].some((name) => matchedContainers.includes(name))) {
1407
+ throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
1408
+ }
1409
+
605
1410
  const uncoordinatedContainers = matchedContainers.filter((name) => !coordinatedContainers.has(name));
606
1411
  if (uncoordinatedContainers.length > 0) {
607
1412
  if (workspace.mode === 'task-bound') {
@@ -621,82 +1426,190 @@ async function rmOne(
621
1426
  p.log.warn(`No sandbox container found for '${branch}'`);
622
1427
  }
623
1428
 
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']
1429
+ const runWorkspaceCleanup = prepareRemovalAction(
1430
+ config.project, target, targetDigest, 'workspace-finalizing', 'workspace-removed', resourceLocks
1431
+ );
1432
+ if (runWorkspaceCleanup.run) {
1433
+ for (const [roots, base, label] of [
1434
+ [workspaceViewRoots, path.join(config.workspaceViewBase, config.project), 'Workspace view'],
1435
+ [controlRoots, path.join(config.controlBase, config.project), 'Control channel']
1436
+ ] as const) {
1437
+ for (const directory of roots.filter((candidate) => shouldStageManagedRemoval(
1438
+ candidate,
1439
+ removalTombstonePath(targetDigest, 'workspace', candidate),
1440
+ runWorkspaceCleanup.recovering
1441
+ ))) {
1442
+ assertLegacyCandidateEvidence(directory, base === path.join(config.controlBase, config.project)
1443
+ ? config.controlBase : config.workspaceViewBase, config, effectiveBranch, matchedContainers);
1444
+ stageManagedRemoval(
1445
+ base,
1446
+ directory,
1447
+ removalTombstonePath(targetDigest, 'workspace', directory),
1448
+ runWorkspaceCleanup.recovering,
1449
+ {
1450
+ version: 1,
1451
+ targetDigest,
1452
+ permitDigest: committedTarget.permitDigest,
1453
+ kind: 'workspace',
1454
+ source: directory
1455
+ }
1456
+ );
1457
+ if (!options.quiet) p.log.success(`${label} removed: ${directory}`);
1458
+ }
1459
+ }
1460
+
1461
+ if (shouldRemoveWorktree) {
1462
+ for (const worktree of existingWorktrees) {
1463
+ const permit = permits.get(path.resolve(worktree));
1464
+ if (!permit) throw new Error(`Missing worktree removal permit: ${worktree}`);
1465
+ stageWorktreeRemoval(
1466
+ config,
1467
+ worktree,
1468
+ permit,
1469
+ removalTombstonePath(targetDigest, 'worktree', worktree),
1470
+ runWorkspaceCleanup.recovering,
1471
+ {
1472
+ version: 1,
1473
+ targetDigest,
1474
+ permitDigest: committedTarget.permitDigest,
1475
+ kind: 'worktree',
1476
+ source: worktree
1477
+ }
1478
+ );
1479
+ }
1480
+ }
1481
+ } else {
1482
+ assertRemovalPathsAbsent([
1483
+ ...removalActionPaths(target, config, 'workspace-removed'),
1484
+ ...(shouldRemoveWorktree ? existingWorktrees : [])
1485
+ ]);
1486
+ }
1487
+ advanceRemovalJournals(config.project, target, targetDigest, 'workspace-removed', resourceLocks);
1488
+ cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest);
1489
+ for (const [roots, base] of [
1490
+ [workspaceViewRoots, path.join(config.workspaceViewBase, config.project)],
1491
+ [controlRoots, path.join(config.controlBase, config.project)]
627
1492
  ] 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}`);
1493
+ for (const directory of roots) {
1494
+ if (!fs.existsSync(directory)) removeEmptyManagedParent(base, directory);
635
1495
  }
636
1496
  }
637
1497
 
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}`);
1498
+ const runBranchCleanup = prepareRemovalAction(
1499
+ config.project, target, targetDigest, 'branch-finalizing', 'branch-removed', resourceLocks
1500
+ );
1501
+ if (runBranchCleanup.run) {
1502
+ if (shouldRemoveWorktree && shouldDeleteBranch) {
1503
+ stageBranchRemoval(config, effectiveBranch, targetDigest, permits, runBranchCleanup.recovering);
649
1504
  }
1505
+ } else if (shouldRemoveWorktree && shouldDeleteBranch
1506
+ && runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', `refs/heads/${effectiveBranch}`])) {
1507
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${effectiveBranch}`);
1508
+ }
1509
+ advanceRemovalJournals(config.project, target, targetDigest, 'branch-removed', resourceLocks);
1510
+ cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest);
650
1511
 
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`);
1512
+ const runToolCleanup = prepareRemovalAction(
1513
+ config.project, target, targetDigest, 'tool-finalizing', 'tool-removed', resourceLocks
1514
+ );
1515
+ if (runToolCleanup.run) {
1516
+ for (const { tool, candidates } of toolCandidates) {
1517
+ for (const dir of candidates.filter((candidate) => shouldStageManagedRemoval(
1518
+ candidate,
1519
+ removalTombstonePath(targetDigest, 'tool', candidate),
1520
+ runToolCleanup.recovering
1521
+ ))) {
1522
+ stageManagedRemoval(
1523
+ tool.sandboxBase,
1524
+ dir,
1525
+ removalTombstonePath(targetDigest, 'tool', dir),
1526
+ runToolCleanup.recovering,
1527
+ {
1528
+ version: 1,
1529
+ targetDigest,
1530
+ permitDigest: committedTarget.permitDigest,
1531
+ kind: 'tool',
1532
+ source: dir
1533
+ }
1534
+ );
1535
+ p.log.success(`${tool.name} state removed: ${dir}`);
664
1536
  }
665
1537
  }
1538
+ } else {
1539
+ assertRemovalPathsAbsent(removalActionPaths(target, config, 'tool-removed'));
666
1540
  }
1541
+ advanceRemovalJournals(config.project, target, targetDigest, 'tool-removed', resourceLocks);
1542
+ cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest);
667
1543
 
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}`);
1544
+ const runShellCleanup = prepareRemovalAction(
1545
+ config.project, target, targetDigest, 'shell-finalizing', 'shell-removed', resourceLocks
1546
+ );
1547
+ if (runShellCleanup.run) {
1548
+ for (const dir of shellConfigDirCandidates(config, effectiveBranch).filter((candidate) => shouldStageManagedRemoval(
1549
+ candidate,
1550
+ removalTombstonePath(targetDigest, 'shell', candidate),
1551
+ runShellCleanup.recovering
1552
+ ))) {
1553
+ stageManagedRemoval(
1554
+ config.shellConfigBase,
1555
+ dir,
1556
+ removalTombstonePath(targetDigest, 'shell', dir),
1557
+ runShellCleanup.recovering,
1558
+ {
1559
+ version: 1,
1560
+ targetDigest,
1561
+ permitDigest: committedTarget.permitDigest,
1562
+ kind: 'shell',
1563
+ source: dir
1564
+ }
1565
+ );
1566
+ p.log.success(`Shell config removed: ${dir}`);
673
1567
  }
1568
+ } else {
1569
+ assertRemovalPathsAbsent(removalActionPaths(target, config, 'shell-removed'));
674
1570
  }
1571
+ advanceRemovalJournals(config.project, target, targetDigest, 'shell-removed', resourceLocks);
1572
+ cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest);
675
1573
 
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}`);
1574
+ const runShareCleanup = prepareRemovalAction(
1575
+ config.project, target, targetDigest, 'share-finalizing', 'share-removed', resourceLocks
1576
+ );
1577
+ if (runShareCleanup.run) {
1578
+ if (shouldRemoveShare && shouldStageManagedRemoval(
1579
+ sharePath,
1580
+ removalTombstonePath(targetDigest, 'share', sharePath),
1581
+ runShareCleanup.recovering
1582
+ )) {
1583
+ stageManagedRemoval(
1584
+ config.shareBase,
1585
+ sharePath,
1586
+ removalTombstonePath(targetDigest, 'share', sharePath),
1587
+ runShareCleanup.recovering,
1588
+ {
1589
+ version: 1,
1590
+ targetDigest,
1591
+ permitDigest: committedTarget.permitDigest,
1592
+ kind: 'share',
1593
+ source: sharePath
1594
+ }
1595
+ );
1596
+ p.log.success(`Share dir removed: ${sharePath}`);
694
1597
  }
1598
+ } else if (shouldRemoveShare) {
1599
+ assertRemovalPathsAbsent(removalActionPaths(target, config, 'share-removed'));
695
1600
  }
1601
+ advanceRemovalJournals(config.project, target, targetDigest, 'share-removed', resourceLocks);
1602
+ cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest);
1603
+
1604
+ const completedJournals = advanceRemovalJournals(config.project, target, targetDigest, 'completed', resourceLocks);
1605
+ for (const journal of completedJournals) clearSandboxRemovalJournalRecord(journal);
696
1606
 
697
1607
  if (!options.quiet) {
698
1608
  p.outro(pc.green('Sandbox removed'));
699
1609
  }
1610
+ } finally {
1611
+ for (const lock of [...resourceLocks.values()].reverse()) lock.release();
1612
+ }
700
1613
  }
701
1614
 
702
1615
  async function rmPurge(