@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';
@@ -8,11 +9,13 @@ import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, san
8
9
  import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from '../engine.js';
9
10
  import { pruneSandboxDanglingImages } from '../image-prune.js';
10
11
  import { assertManagedPath, removeManagedDir, removeWorktreeDir } from '../managed-fs.js';
11
- import { runEngine, runOk, runOkEngine, runSafe, runSafeEngine } from '../shell.js';
12
+ import { run, runEngine, runOk, runOkEngine, runSafe, runSafeEngine } from '../shell.js';
12
13
  import { parseSandboxWorkspaceIdentity, resolveSandboxCleanupTarget, sameSandboxWorkspaceIdentity } from '../workspace-identity.js';
13
14
  import { sandboxControlPaths, sandboxWorkspaceViewPaths } from '../workspace-view.js';
14
- import { removeSandboxControlRoot, readSandboxControlManifest } from '../control/lifecycle.js';
15
+ import { SANDBOX_REMOVAL_JOURNAL_PHASES, advanceSandboxRemovalJournalPhase, claimSandboxRemovalJournal, clearSandboxRemovalJournalRecord, listSandboxRemovalJournals, removeSandboxControlRoot, readSandboxControlManifest, sandboxRemovalPhaseIndex } from '../control/lifecycle.js';
15
16
  import { inspectSandboxControlContainer } from '../control/container-identity.js';
17
+ import { commandForSandboxAuthority } from '../engines/authority.js';
18
+ import { acquireSandboxResourceLock } from '../control/native-file-lock.js';
16
19
  import { toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.js';
17
20
  import { createSandboxCapabilityPlan } from '../agent-client-reconciler.js';
18
21
  import { fetchSandboxRows } from './list-running.js';
@@ -29,6 +32,518 @@ function isMissingPathError(error) {
29
32
  return Boolean(error && typeof error === 'object' && 'code' in error
30
33
  && error.code === 'ENOENT');
31
34
  }
35
+ function digestCleanupValue(value) {
36
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex');
37
+ }
38
+ function removalTargetDigest(config, target) {
39
+ return digestCleanupValue({
40
+ project: config.project,
41
+ branch: target.effectiveBranch,
42
+ workspace: target.workspace,
43
+ controlRoots: target.controlRoots.map((candidate) => path.resolve(candidate)),
44
+ workspaceViewRoots: target.workspaceViewRoots.map((candidate) => path.resolve(candidate)),
45
+ managedPathCandidates: (target.managedPathCandidates ?? []).map((candidate) => path.resolve(candidate)).sort()
46
+ });
47
+ }
48
+ function removalPermitDigest(permits) {
49
+ return digestCleanupValue([...permits.entries()]
50
+ .map(([worktree, permit]) => [path.resolve(worktree), permit.mode, permit.snapshot.identity])
51
+ .sort(([left], [right]) => String(left).localeCompare(String(right))));
52
+ }
53
+ function removalPermitCommits(permits) {
54
+ return [...permits.entries()]
55
+ .map(([worktree, permit]) => ({
56
+ path: path.resolve(worktree),
57
+ mode: permit.mode,
58
+ snapshot: permit.snapshot
59
+ }))
60
+ .sort((left, right) => left.path.localeCompare(right.path));
61
+ }
62
+ function removalTargetCommit(config, target, permits, removeWorktree, removeBranch, removeShare) {
63
+ return {
64
+ branch: target.effectiveBranch,
65
+ project: config.project,
66
+ controlRoot: path.resolve(target.controlRoots[0] ?? path.join(config.controlBase, config.project)),
67
+ targetDigest: removalTargetDigest(config, target),
68
+ permitDigest: removalPermitDigest(permits),
69
+ removeWorktree,
70
+ removeBranch,
71
+ removeShare,
72
+ worktreePaths: target.existingWorktrees.map((candidate) => path.resolve(candidate)).sort(),
73
+ workspaceViewPaths: target.workspaceViewRoots.map((candidate) => path.resolve(candidate)).sort(),
74
+ toolPaths: target.toolCandidates.flatMap(({ candidates }) => candidates.map((candidate) => path.resolve(candidate))).sort(),
75
+ shellPaths: shellConfigDirCandidates(config, target.effectiveBranch).map((candidate) => path.resolve(candidate)).sort(),
76
+ sharePath: path.resolve(shareBranchDir(config, target.effectiveBranch)),
77
+ permits: removalPermitCommits(permits)
78
+ };
79
+ }
80
+ function assertRemovalSelectionMatches(expected, actual) {
81
+ const normalize = (value) => ({
82
+ ...value,
83
+ controlRoot: '',
84
+ permits: value.permits
85
+ });
86
+ if (JSON.stringify(normalize(expected)) !== JSON.stringify(normalize(actual))) {
87
+ throw new Error('SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH');
88
+ }
89
+ }
90
+ function advanceRemovalJournals(project, target, targetDigest, phase, resourceLocks) {
91
+ const targetIndex = sandboxRemovalPhaseIndex(phase);
92
+ return listSandboxRemovalJournals({
93
+ branch: target.effectiveBranch,
94
+ project,
95
+ targetDigest
96
+ }).map((journal) => {
97
+ let current = journal;
98
+ while (sandboxRemovalPhaseIndex(current.phase) < targetIndex) {
99
+ if (current.phase === 'target-committed' && phase === 'container-absent') {
100
+ const lock = resourceLocks.get(current.target.controlRoot);
101
+ if (!lock)
102
+ throw new Error('SANDBOX_CONTROL_REMOVAL_LOCK_MISMATCH');
103
+ current = advanceSandboxRemovalJournalPhase(current, phase, { resourceLock: lock });
104
+ continue;
105
+ }
106
+ const next = SANDBOX_REMOVAL_JOURNAL_PHASES[sandboxRemovalPhaseIndex(current.phase) + 1];
107
+ if (!next)
108
+ throw new Error('SANDBOX_CONTROL_REMOVAL_PHASE_TRANSITION_INVALID');
109
+ const lock = resourceLocks.get(current.target.controlRoot);
110
+ if (!lock)
111
+ throw new Error('SANDBOX_CONTROL_REMOVAL_LOCK_MISMATCH');
112
+ current = advanceSandboxRemovalJournalPhase(current, next, { resourceLock: lock });
113
+ }
114
+ return current;
115
+ });
116
+ }
117
+ function prepareRemovalAction(project, target, targetDigest, startPhase, completedPhase, resourceLocks) {
118
+ const journals = listSandboxRemovalJournals({
119
+ branch: target.effectiveBranch,
120
+ project,
121
+ targetDigest
122
+ });
123
+ const startIndex = sandboxRemovalPhaseIndex(startPhase);
124
+ const completedIndex = sandboxRemovalPhaseIndex(completedPhase);
125
+ const shouldRun = journals.length === 0
126
+ || journals.some((journal) => sandboxRemovalPhaseIndex(journal.phase) < completedIndex);
127
+ const recovering = journals.some((journal) => {
128
+ const index = sandboxRemovalPhaseIndex(journal.phase);
129
+ return index >= startIndex && index < completedIndex;
130
+ });
131
+ advanceRemovalJournals(project, target, targetDigest, startPhase, resourceLocks);
132
+ return { run: shouldRun, recovering };
133
+ }
134
+ function assertRemovalPathsAbsent(paths) {
135
+ const remaining = paths.filter((candidate) => fs.existsSync(candidate));
136
+ if (remaining.length > 0) {
137
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${remaining[0]}`);
138
+ }
139
+ }
140
+ function assertBranchRemovalIdentity(config, branch, permits) {
141
+ if (!runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', `refs/heads/${branch}`]))
142
+ return;
143
+ const expectedHeads = new Set([...permits.values()]
144
+ .filter((permit) => permit.snapshot.branch === branch)
145
+ .map((permit) => permit.snapshot.head));
146
+ if (expectedHeads.size === 0) {
147
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
148
+ }
149
+ const actualHead = runSafe('git', ['-C', config.repoRoot, 'rev-parse', `refs/heads/${branch}`]);
150
+ if (!expectedHeads.has(actualHead)) {
151
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
152
+ }
153
+ }
154
+ function removalTombstonePath(targetDigest, kind, source) {
155
+ const sourceDigest = createHash('sha256')
156
+ .update(`${kind}\0${path.resolve(source)}`)
157
+ .digest('hex')
158
+ .slice(0, 24);
159
+ return path.join(path.dirname(path.resolve(source)), `.agent-infra-removal-${targetDigest.slice(0, 16)}-${kind}-${sourceDigest}`);
160
+ }
161
+ const REMOVAL_TOMBSTONE_MARKER = '.agent-infra-removal-ownership.json';
162
+ const REMOVAL_TOMBSTONE_PAYLOAD = 'payload';
163
+ function removalSourceClaimPath(source, ownership) {
164
+ const claimDigest = createHash('sha256')
165
+ .update(JSON.stringify(ownership))
166
+ .digest('hex')
167
+ .slice(0, 24);
168
+ return path.join(source, `.agent-infra-removal-source-${claimDigest}.json`);
169
+ }
170
+ function removalSourceIdentity(source) {
171
+ const stat = fs.lstatSync(source, { bigint: true });
172
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.dev === 0n || stat.ino === 0n) {
173
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
174
+ }
175
+ return { dev: String(stat.dev), ino: String(stat.ino) };
176
+ }
177
+ function sameRemovalSourceIdentity(left, right) {
178
+ return left.dev === right.dev && left.ino === right.ino;
179
+ }
180
+ function removalSourceClaimMatches(source, ownership, identity) {
181
+ try {
182
+ const claimPath = removalSourceClaimPath(source, ownership);
183
+ const claimStat = fs.lstatSync(claimPath);
184
+ if (!claimStat.isFile() || claimStat.isSymbolicLink())
185
+ return false;
186
+ const record = JSON.parse(fs.readFileSync(claimPath, 'utf8'));
187
+ return record.version === ownership.version
188
+ && record.targetDigest === ownership.targetDigest
189
+ && record.permitDigest === ownership.permitDigest
190
+ && record.kind === ownership.kind
191
+ && typeof record.source === 'string'
192
+ && path.resolve(record.source) === path.resolve(ownership.source)
193
+ && record.sourceIdentity?.dev === identity.dev
194
+ && record.sourceIdentity?.ino === identity.ino;
195
+ }
196
+ catch {
197
+ return false;
198
+ }
199
+ }
200
+ function isOwnedRemovalSourceClaim(source, ownership) {
201
+ try {
202
+ const claimPath = removalSourceClaimPath(source, ownership);
203
+ const claimStat = fs.lstatSync(claimPath);
204
+ if (!claimStat.isFile() || claimStat.isSymbolicLink())
205
+ return false;
206
+ const record = JSON.parse(fs.readFileSync(claimPath, 'utf8'));
207
+ return record.version === ownership.version
208
+ && record.targetDigest === ownership.targetDigest
209
+ && record.permitDigest === ownership.permitDigest
210
+ && record.kind === ownership.kind
211
+ && typeof record.source === 'string'
212
+ && path.resolve(record.source) === path.resolve(ownership.source);
213
+ }
214
+ catch {
215
+ return false;
216
+ }
217
+ }
218
+ function removeOwnedSourceClaim(source, ownership) {
219
+ try {
220
+ const claimPath = removalSourceClaimPath(source, ownership);
221
+ if (isOwnedRemovalSourceClaim(source, ownership))
222
+ fs.unlinkSync(claimPath);
223
+ }
224
+ catch {
225
+ // Preserve the source when the claim cannot be revalidated.
226
+ }
227
+ }
228
+ function readRemovalTombstoneRecord(root, tombstone) {
229
+ assertManagedPath(root, tombstone);
230
+ try {
231
+ const tombstoneStat = fs.lstatSync(tombstone);
232
+ const markerStat = fs.lstatSync(removalTombstoneMarker(tombstone));
233
+ if (!tombstoneStat.isDirectory() || tombstoneStat.isSymbolicLink()
234
+ || !markerStat.isFile() || markerStat.isSymbolicLink())
235
+ return null;
236
+ const record = JSON.parse(fs.readFileSync(removalTombstoneMarker(tombstone), 'utf8'));
237
+ if (record.version !== 1
238
+ || typeof record.targetDigest !== 'string'
239
+ || typeof record.permitDigest !== 'string'
240
+ || typeof record.kind !== 'string'
241
+ || typeof record.source !== 'string'
242
+ || record.sourceIdentity?.dev === undefined
243
+ || record.sourceIdentity.ino === undefined)
244
+ return null;
245
+ return record;
246
+ }
247
+ catch {
248
+ return null;
249
+ }
250
+ }
251
+ function removalTombstonePayload(tombstone) {
252
+ return path.join(tombstone, REMOVAL_TOMBSTONE_PAYLOAD);
253
+ }
254
+ function removalTombstoneMarker(tombstone) {
255
+ return path.join(tombstone, REMOVAL_TOMBSTONE_MARKER);
256
+ }
257
+ function isOwnedRemovalTombstone(root, tombstone, ownership) {
258
+ const record = readRemovalTombstoneRecord(root, tombstone);
259
+ return record !== null
260
+ && record.version === 1
261
+ && record.targetDigest === ownership.targetDigest
262
+ && record.permitDigest === ownership.permitDigest
263
+ && record.kind === ownership.kind
264
+ && typeof record.source === 'string'
265
+ && path.resolve(record.source) === path.resolve(ownership.source);
266
+ }
267
+ function isOwnedRemovalPayload(root, tombstone, ownership) {
268
+ const record = readRemovalTombstoneRecord(root, tombstone);
269
+ if (!record || record.targetDigest !== ownership.targetDigest
270
+ || record.permitDigest !== ownership.permitDigest
271
+ || record.kind !== ownership.kind
272
+ || path.resolve(record.source) !== path.resolve(ownership.source))
273
+ return false;
274
+ try {
275
+ const payload = removalTombstonePayload(tombstone);
276
+ const payloadIdentity = removalSourceIdentity(payload);
277
+ return sameRemovalSourceIdentity(payloadIdentity, record.sourceIdentity)
278
+ && removalSourceClaimMatches(payload, ownership, record.sourceIdentity);
279
+ }
280
+ catch {
281
+ return false;
282
+ }
283
+ }
284
+ function assertOwnedRemovalTombstone(root, tombstone, ownership) {
285
+ if (!isOwnedRemovalTombstone(root, tombstone, ownership)) {
286
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${ownership.source}`);
287
+ }
288
+ }
289
+ function shouldStageManagedRemoval(source, tombstone, recovering) {
290
+ return fs.existsSync(source) || (recovering && fs.existsSync(tombstone));
291
+ }
292
+ function stageManagedRemoval(root, source, tombstone, recovering, ownership) {
293
+ assertManagedPath(root, source);
294
+ assertManagedPath(root, tombstone);
295
+ const sourceExists = fs.existsSync(source);
296
+ const tombstoneExists = fs.existsSync(tombstone);
297
+ if (tombstoneExists) {
298
+ assertOwnedRemovalTombstone(root, tombstone, ownership);
299
+ if (sourceExists) {
300
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
301
+ }
302
+ if (recovering) {
303
+ if (!isOwnedRemovalPayload(root, tombstone, ownership)) {
304
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
305
+ }
306
+ return;
307
+ }
308
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
309
+ }
310
+ if (recovering) {
311
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_ACTION_OUTCOME_UNKNOWN: ${source}`);
312
+ }
313
+ if (!sourceExists)
314
+ return;
315
+ // The operator authorizes cleanup of this managed target after execution stops.
316
+ // Path checks detect conflicts; they do not provide an atomic source-instance move.
317
+ const expectedIdentity = removalSourceIdentity(source);
318
+ const claimPath = removalSourceClaimPath(source, ownership);
319
+ try {
320
+ fs.writeFileSync(claimPath, `${JSON.stringify({ ...ownership, sourceIdentity: expectedIdentity })}\n`, {
321
+ encoding: 'utf8',
322
+ mode: 0o600,
323
+ flag: 'wx'
324
+ });
325
+ }
326
+ catch {
327
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
328
+ }
329
+ if (!sameRemovalSourceIdentity(expectedIdentity, removalSourceIdentity(source))) {
330
+ removeOwnedSourceClaim(source, ownership);
331
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
332
+ }
333
+ let moved = false;
334
+ try {
335
+ fs.mkdirSync(tombstone, { mode: 0o700 });
336
+ fs.writeFileSync(removalTombstoneMarker(tombstone), `${JSON.stringify({ ...ownership, sourceIdentity: expectedIdentity })}\n`, {
337
+ encoding: 'utf8',
338
+ mode: 0o600,
339
+ flag: 'wx'
340
+ });
341
+ fs.renameSync(source, removalTombstonePayload(tombstone));
342
+ moved = true;
343
+ if (!isOwnedRemovalPayload(root, tombstone, ownership)) {
344
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
345
+ }
346
+ }
347
+ catch (error) {
348
+ if (!moved)
349
+ removeOwnedSourceClaim(source, ownership);
350
+ throw error;
351
+ }
352
+ }
353
+ function cleanupManagedTombstones(root, targetDigest, permitDigest, kind, sources) {
354
+ for (const source of sources) {
355
+ const tombstone = removalTombstonePath(targetDigest, kind, source);
356
+ if (!fs.existsSync(tombstone))
357
+ continue;
358
+ const ownership = { version: 1, targetDigest, permitDigest, kind, source };
359
+ if (!isOwnedRemovalTombstone(root, tombstone, ownership))
360
+ continue;
361
+ if (!isOwnedRemovalPayload(root, tombstone, ownership)) {
362
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
363
+ }
364
+ removeManagedDir(root, tombstone);
365
+ }
366
+ }
367
+ function branchRemovalTombstoneRef(targetDigest) {
368
+ return `refs/agent-infra/sandbox-removal/${targetDigest}/branch`;
369
+ }
370
+ function branchPermitHead(branch, permits) {
371
+ const heads = new Set([...permits.values()]
372
+ .filter((permit) => permit.snapshot.branch === branch)
373
+ .map((permit) => permit.snapshot.head));
374
+ return heads.size === 1 ? [...heads][0] : null;
375
+ }
376
+ function stageBranchRemoval(config, branch, targetDigest, permits, recovering) {
377
+ const expectedHead = branchPermitHead(branch, permits);
378
+ if (!expectedHead)
379
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
380
+ const branchName = `refs/heads/${branch}`;
381
+ const tombstone = branchRemovalTombstoneRef(targetDigest);
382
+ const branchExists = runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', branchName]);
383
+ const tombstoneExists = runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', tombstone]);
384
+ if (branchExists && tombstoneExists) {
385
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
386
+ }
387
+ if (recovering) {
388
+ if (branchExists && !tombstoneExists) {
389
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_ACTION_OUTCOME_UNKNOWN: ${branch}`);
390
+ }
391
+ if (tombstoneExists && runSafe('git', ['-C', config.repoRoot, 'rev-parse', tombstone]) !== expectedHead) {
392
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
393
+ }
394
+ return;
395
+ }
396
+ if (!branchExists)
397
+ return;
398
+ assertBranchRemovalIdentity(config, branch, permits);
399
+ const actualHead = runSafe('git', ['-C', config.repoRoot, 'rev-parse', branchName]);
400
+ if (!runOk('git', ['-C', config.repoRoot, 'update-ref', tombstone, actualHead, '0'.repeat(40)])) {
401
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
402
+ }
403
+ if (!runOk('git', ['-C', config.repoRoot, 'update-ref', '-d', branchName, actualHead])) {
404
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
405
+ }
406
+ }
407
+ function cleanupBranchTombstone(config, branch, targetDigest, permits) {
408
+ const tombstone = branchRemovalTombstoneRef(targetDigest);
409
+ if (!runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', tombstone]))
410
+ return;
411
+ const expectedHead = branchPermitHead(branch, permits);
412
+ if (!expectedHead || runSafe('git', ['-C', config.repoRoot, 'rev-parse', tombstone]) !== expectedHead) {
413
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
414
+ }
415
+ if (!runOk('git', ['-C', config.repoRoot, 'update-ref', '-d', tombstone, expectedHead])) {
416
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
417
+ }
418
+ }
419
+ function worktreeRegistration(config, worktree) {
420
+ const lines = runSafe('git', ['-C', config.repoRoot, 'worktree', 'list', '--porcelain']).split('\n');
421
+ let current = null;
422
+ const flush = () => {
423
+ if (!current || path.resolve(current.path) !== path.resolve(worktree))
424
+ return null;
425
+ return { head: current.head, branch: current.branch };
426
+ };
427
+ for (const line of lines) {
428
+ if (line.startsWith('worktree ')) {
429
+ const previous = flush();
430
+ if (previous)
431
+ return previous;
432
+ current = { path: line.slice('worktree '.length), head: '', branch: null };
433
+ }
434
+ else if (current && line.startsWith('HEAD ')) {
435
+ current.head = line.slice('HEAD '.length);
436
+ }
437
+ else if (current && line.startsWith('branch ')) {
438
+ current.branch = line.slice('branch '.length);
439
+ }
440
+ }
441
+ return flush();
442
+ }
443
+ function assertWorktreeRegistrationIdentity(config, worktree, permit) {
444
+ const registration = worktreeRegistration(config, worktree);
445
+ if (!registration)
446
+ return;
447
+ if (registration.head !== permit.snapshot.head
448
+ || registration.branch !== `refs/heads/${permit.snapshot.branch}`) {
449
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${worktree}`);
450
+ }
451
+ }
452
+ function pruneWorktreeRegistration(config, worktree) {
453
+ runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
454
+ if (worktreeRegistration(config, worktree)) {
455
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_ACTION_OUTCOME_UNKNOWN: ${worktree}`);
456
+ }
457
+ }
458
+ function stageWorktreeRemoval(config, worktree, permit, tombstone, recovering, ownership) {
459
+ assertManagedPath(config.worktreeBase, worktree);
460
+ assertManagedPath(config.worktreeBase, tombstone);
461
+ const worktreeExists = fs.existsSync(worktree);
462
+ const tombstoneExists = fs.existsSync(tombstone);
463
+ if (worktreeExists || !recovering)
464
+ verifyWorktreePermit(permit);
465
+ if (recovering) {
466
+ if (worktreeExists || !tombstoneExists) {
467
+ if (worktreeExists && tombstoneExists) {
468
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${worktree}`);
469
+ }
470
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_ACTION_OUTCOME_UNKNOWN: ${worktree}`);
471
+ }
472
+ assertOwnedRemovalTombstone(config.worktreeBase, tombstone, ownership);
473
+ assertWorktreeRegistrationIdentity(config, worktree, permit);
474
+ pruneWorktreeRegistration(config, worktree);
475
+ return;
476
+ }
477
+ if (!worktreeExists)
478
+ return;
479
+ stageManagedRemoval(config.worktreeBase, worktree, tombstone, false, ownership);
480
+ pruneWorktreeRegistration(config, worktree);
481
+ }
482
+ function cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest) {
483
+ const journals = listSandboxRemovalJournals({
484
+ branch: target.effectiveBranch,
485
+ project: config.project,
486
+ targetDigest
487
+ });
488
+ if (journals.length === 0)
489
+ return;
490
+ const completed = (phase) => journals.every((journal) => (sandboxRemovalPhaseIndex(journal.phase) >= sandboxRemovalPhaseIndex(phase)));
491
+ if (completed('workspace-removed')) {
492
+ cleanupManagedTombstones(config.workspaceViewBase, targetDigest, committedTarget.permitDigest, 'workspace', target.workspaceViewRoots);
493
+ cleanupManagedTombstones(path.join(config.controlBase, config.project), targetDigest, committedTarget.permitDigest, 'workspace', target.controlRoots);
494
+ cleanupManagedTombstones(config.worktreeBase, targetDigest, committedTarget.permitDigest, 'worktree', committedTarget.worktreePaths);
495
+ }
496
+ if (completed('branch-removed')) {
497
+ cleanupBranchTombstone(config, target.effectiveBranch, targetDigest, new Map(committedTarget.permits.map((permit) => [path.resolve(permit.path), {
498
+ mode: permit.mode,
499
+ snapshot: permit.snapshot
500
+ }])));
501
+ }
502
+ if (completed('tool-removed')) {
503
+ cleanupManagedTombstones(config.home, targetDigest, committedTarget.permitDigest, 'tool', committedTarget.toolPaths);
504
+ }
505
+ if (completed('shell-removed')) {
506
+ cleanupManagedTombstones(config.shellConfigBase, targetDigest, committedTarget.permitDigest, 'shell', committedTarget.shellPaths);
507
+ }
508
+ if (completed('share-removed')) {
509
+ cleanupManagedTombstones(config.shareBase, targetDigest, committedTarget.permitDigest, 'share', [committedTarget.sharePath]);
510
+ }
511
+ }
512
+ function removalActionPaths(target, config, phase) {
513
+ if (phase === 'workspace-removed') {
514
+ return [...target.workspaceViewRoots, ...target.controlRoots];
515
+ }
516
+ if (phase === 'tool-removed') {
517
+ return target.toolCandidates.flatMap(({ candidates }) => candidates);
518
+ }
519
+ if (phase === 'shell-removed') {
520
+ return shellConfigDirCandidates(config, target.effectiveBranch);
521
+ }
522
+ if (phase === 'share-removed') {
523
+ return [shareBranchDir(config, target.effectiveBranch)];
524
+ }
525
+ return [];
526
+ }
527
+ function permitsFromJournal(journals, target, expectedTargetDigest) {
528
+ const permits = new Map();
529
+ for (const journal of journals) {
530
+ if (journal.target.targetDigest !== expectedTargetDigest)
531
+ continue;
532
+ if (journal.target.branch !== target.effectiveBranch) {
533
+ throw new Error('SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH');
534
+ }
535
+ for (const entry of journal.target.permits) {
536
+ if (path.resolve(entry.path) !== path.resolve(entry.snapshot.worktree)) {
537
+ throw new Error('SANDBOX_CONTROL_REMOVAL_PERMIT_MISMATCH');
538
+ }
539
+ permits.set(path.resolve(entry.path), {
540
+ mode: entry.mode,
541
+ snapshot: entry.snapshot
542
+ });
543
+ }
544
+ }
545
+ return permits;
546
+ }
32
547
  export function sandboxManagedPathKey(candidate, platform = process.platform, resolveExistingPath = (resolved) => fs.realpathSync.native(resolved)) {
33
548
  const resolved = path.resolve(candidate);
34
549
  let identity = resolved;
@@ -297,12 +812,19 @@ async function removeExactSandboxContainer(engine, manifest, timeoutMs) {
297
812
  const deadlineAt = Date.now() + timeoutMs;
298
813
  const remaining = () => Math.max(1, deadlineAt - Date.now());
299
814
  const inspect = () => inspectSandboxControlContainer(manifest, { timeoutMs: remaining() });
815
+ const runAuthorityCommand = (args) => {
816
+ const command = manifest.authorityEvidence
817
+ ? commandForSandboxAuthority(manifest.authorityEvidence, 'docker', args)
818
+ : null;
819
+ return command ? runOk(command.cmd, command.args, { timeout: remaining() })
820
+ : runOkEngine(engine, 'docker', args, { timeout: remaining() });
821
+ };
300
822
  const before = await inspect();
301
823
  if (before.state === 'unknown')
302
824
  throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${before.reason}`);
303
825
  if (before.state === 'absent')
304
826
  return;
305
- if (before.running && !runOkEngine(engine, 'docker', ['stop', '--timeout', '1', manifest.containerIdentity.id], { timeout: remaining() })) {
827
+ if (before.running && !runAuthorityCommand(['stop', '--timeout', '1', manifest.containerIdentity.id])) {
306
828
  const afterStop = await inspect();
307
829
  if (afterStop.state !== 'absent' && afterStop.state !== 'found') {
308
830
  throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterStop.reason}`);
@@ -314,7 +836,7 @@ async function removeExactSandboxContainer(engine, manifest, timeoutMs) {
314
836
  const afterStop = await inspect();
315
837
  if (afterStop.state === 'unknown')
316
838
  throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterStop.reason}`);
317
- if (afterStop.state === 'found' && !runOkEngine(engine, 'docker', ['rm', manifest.containerIdentity.id], { timeout: remaining() })) {
839
+ if (afterStop.state === 'found' && !runAuthorityCommand(['rm', manifest.containerIdentity.id])) {
318
840
  const afterRemove = await inspect();
319
841
  if (afterRemove.state !== 'absent') {
320
842
  throw new Error(`Failed to remove sandbox container: ${manifest.containerIdentity.id}`);
@@ -389,7 +911,7 @@ async function authorizeWorktrees(worktrees, { allowDirtyDiscard, assumeYes }, {
389
911
  }
390
912
  p.log.warn(formatWorktreeSnapshot(inspection.snapshot));
391
913
  const confirmed = await confirm({
392
- message: 'Discard these exact uncommitted changes?',
914
+ message: 'Discard this worktree and all its uncommitted changes, including later changes?',
393
915
  initialValue: false
394
916
  });
395
917
  if (isCancel(confirmed) || !confirmed)
@@ -408,146 +930,331 @@ async function rmOne(config, tools, branch, options = {}) {
408
930
  if (!options.quiet) {
409
931
  p.intro(pc.cyan(`Removing sandbox for ${branch}`));
410
932
  }
933
+ const targetDigest = removalTargetDigest(config, target);
934
+ const existingJournals = listSandboxRemovalJournals({
935
+ branch: effectiveBranch,
936
+ project: config.project,
937
+ targetDigest
938
+ });
939
+ const persistedTarget = existingJournals[0]?.target;
940
+ for (const journal of existingJournals) {
941
+ if (!persistedTarget)
942
+ break;
943
+ assertRemovalSelectionMatches(persistedTarget, journal.target);
944
+ }
411
945
  const recovery = options.permits
412
946
  ? new Map()
413
947
  : recoveryContexts(config, target, existingWorktrees);
948
+ const journalPermits = options.permits ? new Map()
949
+ : permitsFromJournal(existingJournals, target, targetDigest);
414
950
  const permits = options.permits
415
951
  ? new Map(options.permits)
416
- : await authorizeWorktrees(existingWorktrees, {
417
- allowDirtyDiscard: options.allowDirtyDiscard ?? true,
418
- assumeYes: Boolean(options.assumeYes)
419
- }, {
420
- ...options.prompt,
421
- interactive: options.interactive ?? Boolean(process.stdin.isTTY),
422
- recovery
423
- });
952
+ : journalPermits.size > 0
953
+ ? journalPermits
954
+ : await authorizeWorktrees(existingWorktrees, {
955
+ allowDirtyDiscard: options.allowDirtyDiscard ?? true,
956
+ assumeYes: Boolean(options.assumeYes)
957
+ }, {
958
+ ...options.prompt,
959
+ interactive: options.interactive ?? Boolean(process.stdin.isTTY),
960
+ recovery
961
+ });
424
962
  for (const worktree of existingWorktrees) {
425
963
  const permit = permits.get(path.resolve(worktree));
426
964
  if (!permit)
427
965
  throw new Error(`Missing worktree removal permit: ${worktree}`);
428
- verifyWorktreePermit(permit);
966
+ const worktreeTombstone = removalTombstonePath(targetDigest, 'worktree', worktree);
967
+ const workspacePhasePending = existingJournals.some((journal) => (sandboxRemovalPhaseIndex(journal.phase) < sandboxRemovalPhaseIndex('workspace-removed')));
968
+ if (fs.existsSync(worktree)
969
+ || (workspacePhasePending
970
+ && !isOwnedRemovalTombstone(config.worktreeBase, worktreeTombstone, {
971
+ version: 1,
972
+ targetDigest,
973
+ permitDigest: removalPermitDigest(permits),
974
+ kind: 'worktree',
975
+ source: worktree
976
+ }))) {
977
+ verifyWorktreePermit(permit);
978
+ }
429
979
  }
430
- const shouldRemoveWorktree = existingWorktrees.length === 0
431
- ? false
432
- : options.assumeYes
433
- ? true
434
- : await confirm({
435
- message: `Remove worktree(s): ${existingWorktrees.join(', ')}?`,
436
- initialValue: true
437
- });
980
+ const recoveredRemovalChoice = persistedTarget;
981
+ const shouldRemoveWorktree = recoveredRemovalChoice
982
+ ? recoveredRemovalChoice.removeWorktree
983
+ : existingWorktrees.length === 0
984
+ ? false
985
+ : options.assumeYes
986
+ ? true
987
+ : await confirm({
988
+ message: `Remove worktree(s): ${existingWorktrees.join(', ')}?`,
989
+ initialValue: true
990
+ });
438
991
  if (isCancel(shouldRemoveWorktree)) {
439
992
  p.outro('Cancelled');
440
993
  return;
441
994
  }
442
- preflightRmTarget(config, target);
443
- const coordinatedContainers = new Set();
444
- for (const root of controlRoots.filter((candidate) => fs.existsSync(candidate))) {
445
- assertLegacyCandidateEvidence(root, config.controlBase, config, effectiveBranch, matchedContainers);
446
- assertControlRootMatchesTarget(root, effectiveBranch, workspace);
447
- const manifestPath = path.join(root, 'manifest.json');
448
- if (!fs.existsSync(manifestPath))
449
- continue;
450
- const manifest = readSandboxControlManifest(manifestPath);
451
- coordinatedContainers.add(manifest.container);
452
- await removeSandboxControlRoot(root, {
453
- inspectContainer: (timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }),
454
- removeContainer: (timeoutMs) => removeExactSandboxContainer(engine, manifest, timeoutMs)
455
- });
456
- removeEmptyManagedParent(path.join(config.controlBase, config.project), root);
995
+ const shouldDeleteBranch = recoveredRemovalChoice
996
+ ? recoveredRemovalChoice.removeBranch
997
+ : shouldRemoveWorktree && existingWorktrees.length > 0
998
+ ? options.assumeYes
999
+ ? true
1000
+ : await confirm({
1001
+ message: `Also delete local branch '${effectiveBranch}'?`,
1002
+ initialValue: true
1003
+ })
1004
+ : false;
1005
+ if (isCancel(shouldDeleteBranch)) {
1006
+ p.outro('Cancelled');
1007
+ return;
457
1008
  }
458
- const uncoordinatedContainers = matchedContainers.filter((name) => !coordinatedContainers.has(name));
459
- if (uncoordinatedContainers.length > 0) {
460
- if (workspace.mode === 'task-bound') {
461
- throw new Error(`SANDBOX_CONTROL_TARGET_EVIDENCE_MISSING: container '${uncoordinatedContainers[0]}'`);
1009
+ const sharePath = path.resolve(shareBranchDir(config, effectiveBranch));
1010
+ const shouldRemoveShare = recoveredRemovalChoice
1011
+ ? recoveredRemovalChoice.removeShare
1012
+ : fs.existsSync(sharePath)
1013
+ ? options.assumeYes
1014
+ ? true
1015
+ : await confirm({
1016
+ message: `Remove share dir for branch '${effectiveBranch}' (${sharePath})?`,
1017
+ initialValue: true
1018
+ })
1019
+ : false;
1020
+ if (isCancel(shouldRemoveShare)) {
1021
+ p.outro('Cancelled');
1022
+ return;
1023
+ }
1024
+ const committedTarget = persistedTarget
1025
+ ? {
1026
+ ...persistedTarget,
1027
+ permitDigest: removalPermitDigest(permits),
1028
+ permits: removalPermitCommits(permits)
462
1029
  }
463
- const spinner = p.spinner();
464
- spinner.start(`Stopping container(s): ${uncoordinatedContainers.join(', ')}`);
465
- for (const name of uncoordinatedContainers) {
466
- if (!runOkEngine(engine, 'docker', ['stop', name]))
467
- throw new Error(`Failed to stop sandbox container: ${name}`);
468
- if (!runOkEngine(engine, 'docker', ['rm', name]))
469
- throw new Error(`Failed to remove sandbox container: ${name}`);
1030
+ : removalTargetCommit(config, target, permits, Boolean(shouldRemoveWorktree), Boolean(shouldDeleteBranch), Boolean(shouldRemoveShare));
1031
+ if (persistedTarget)
1032
+ assertRemovalSelectionMatches(persistedTarget, committedTarget);
1033
+ const coordinatedManifests = controlRoots
1034
+ .filter((candidate) => fs.existsSync(candidate))
1035
+ .flatMap((root) => {
1036
+ const manifestPath = path.join(root, 'manifest.json');
1037
+ return fs.existsSync(manifestPath)
1038
+ ? [[root, readSandboxControlManifest(manifestPath), fs.readFileSync(manifestPath, 'utf8')]]
1039
+ : [];
1040
+ });
1041
+ const resourceLocks = new Map();
1042
+ const recoveredContainerNames = new Set();
1043
+ try {
1044
+ for (const [root, manifest] of [...coordinatedManifests].sort(([left], [right]) => left.localeCompare(right))) {
1045
+ resourceLocks.set(root, acquireSandboxResourceLock(`${manifest.engine}:${manifest.containerIdentity.id}`, { lockDomain: manifest.authorityEvidence.lockDomain }));
470
1046
  }
471
- const remaining = runEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
472
- const leftovers = uncoordinatedContainers.filter((name) => remaining.includes(name));
473
- if (leftovers.length > 0)
474
- throw new Error(`Sandbox container(s) still exist after removal: ${leftovers.join(', ')}`);
475
- spinner.stop(pc.green(`Removed container(s): ${uncoordinatedContainers.join(', ')}`));
476
- }
477
- else {
478
- p.log.warn(`No sandbox container found for '${branch}'`);
479
- }
480
- for (const [roots, base, label] of [
481
- [workspaceViewRoots, path.join(config.workspaceViewBase, config.project), 'Workspace view'],
482
- [controlRoots, path.join(config.controlBase, config.project), 'Control channel']
483
- ]) {
484
- for (const directory of roots.filter((candidate) => fs.existsSync(candidate))) {
485
- assertLegacyCandidateEvidence(directory, base === path.join(config.controlBase, config.project)
486
- ? config.controlBase : config.workspaceViewBase, config, effectiveBranch, matchedContainers);
487
- removeManagedDir(base, directory);
488
- assertRemoved(directory, label);
489
- removeEmptyManagedParent(base, directory);
490
- if (!options.quiet)
491
- p.log.success(`${label} removed: ${directory}`);
492
- }
493
- }
494
- if (shouldRemoveWorktree) {
495
- for (const worktree of existingWorktrees) {
496
- const permit = permits.get(path.resolve(worktree));
497
- if (!permit)
498
- throw new Error(`Missing worktree removal permit: ${worktree}`);
499
- removeWorktreeDir(config.repoRoot, config.worktreeBase, worktree, permit, {
500
- allowRegisteredPathFallback: engine === ENGINES.WSL2
1047
+ for (const journal of existingJournals.filter((candidate) => !fs.existsSync(candidate.target.controlRoot))) {
1048
+ if (!target.controlRoots.some((root) => path.resolve(root) === path.resolve(journal.target.controlRoot))) {
1049
+ throw new Error('SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH');
1050
+ }
1051
+ assertRemovalSelectionMatches(committedTarget, journal.target);
1052
+ if (journal.target.permitDigest !== removalPermitDigest(permits)) {
1053
+ throw new Error('SANDBOX_CONTROL_REMOVAL_PERMIT_MISMATCH');
1054
+ }
1055
+ const lock = acquireSandboxResourceLock(`${journal.engine}:${journal.containerId}`, {
1056
+ lockDomain: journal.lockDomain
501
1057
  });
502
- assertRemoved(worktree, 'Worktree');
503
- const registered = runSafe('git', ['-C', config.repoRoot, 'worktree', 'list', '--porcelain'])
504
- .split('\n').filter((line) => line.startsWith('worktree ')).map((line) => path.resolve(line.slice(9)));
505
- if (registered.includes(path.resolve(worktree)))
506
- throw new Error(`Worktree is still registered after removal: ${worktree}`);
507
- }
508
- const shouldDeleteBranch = options.assumeYes
509
- ? true
510
- : await confirm({
511
- message: `Also delete local branch '${effectiveBranch}'?`,
512
- initialValue: true
1058
+ resourceLocks.set(journal.target.controlRoot, lock);
1059
+ const current = listSandboxRemovalJournals({
1060
+ branch: journal.target.branch,
1061
+ project: journal.target.project,
1062
+ targetDigest: journal.target.targetDigest
1063
+ }).find((candidate) => candidate.carrierIdentityDigest === journal.carrierIdentityDigest);
1064
+ if (!current || current.revision !== journal.revision || current.handoffId !== journal.handoffId) {
1065
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
1066
+ }
1067
+ const claimed = claimSandboxRemovalJournal(current, { resourceLock: lock });
1068
+ if (!['carrier-finalizing', 'carrier-removed', 'workspace-finalizing', 'workspace-removed',
1069
+ 'branch-finalizing', 'branch-removed', 'tool-finalizing', 'tool-removed',
1070
+ 'shell-finalizing', 'shell-removed', 'share-finalizing', 'share-removed', 'completed']
1071
+ .includes(claimed.phase)) {
1072
+ throw new Error('SANDBOX_CONTROL_REMOVE_RECOVERY_PENDING');
1073
+ }
1074
+ const recovered = claimed.phase === 'carrier-finalizing'
1075
+ ? advanceSandboxRemovalJournalPhase(claimed, 'carrier-removed', { resourceLock: lock })
1076
+ : claimed;
1077
+ recoveredContainerNames.add(controlRootContainer(config, journal.target.controlRoot));
1078
+ }
1079
+ cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest);
1080
+ preflightRmTarget(config, target);
1081
+ const coordinatedContainers = new Set(recoveredContainerNames);
1082
+ for (const root of controlRoots.filter((candidate) => fs.existsSync(candidate))) {
1083
+ assertLegacyCandidateEvidence(root, config.controlBase, config, effectiveBranch, matchedContainers);
1084
+ assertControlRootMatchesTarget(root, effectiveBranch, workspace);
1085
+ const manifestPath = path.join(root, 'manifest.json');
1086
+ if (!fs.existsSync(manifestPath))
1087
+ continue;
1088
+ const currentRaw = fs.readFileSync(manifestPath, 'utf8');
1089
+ const manifest = readSandboxControlManifest(manifestPath);
1090
+ const initial = coordinatedManifests.find(([candidate]) => candidate === root);
1091
+ if (!initial || currentRaw !== initial[2])
1092
+ throw new Error('SANDBOX_CONTROL_MANIFEST_CHANGED');
1093
+ coordinatedContainers.add(manifest.container);
1094
+ await removeSandboxControlRoot(root, {
1095
+ inspectContainer: (timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }),
1096
+ removeContainer: (timeoutMs) => removeExactSandboxContainer(engine, manifest, timeoutMs),
1097
+ resourceLock: resourceLocks.get(root),
1098
+ retainRemovalJournal: true,
1099
+ removalTarget: { ...committedTarget, controlRoot: path.resolve(root) }
513
1100
  });
514
- if (!isCancel(shouldDeleteBranch) && shouldDeleteBranch) {
515
- if (!runOk('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch])) {
516
- throw new Error(`Local branch '${effectiveBranch}' was not deleted`);
1101
+ removeEmptyManagedParent(path.join(config.controlBase, config.project), root);
1102
+ }
1103
+ if ([...recoveredContainerNames].some((name) => matchedContainers.includes(name))) {
1104
+ throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
1105
+ }
1106
+ const uncoordinatedContainers = matchedContainers.filter((name) => !coordinatedContainers.has(name));
1107
+ if (uncoordinatedContainers.length > 0) {
1108
+ if (workspace.mode === 'task-bound') {
1109
+ throw new Error(`SANDBOX_CONTROL_TARGET_EVIDENCE_MISSING: container '${uncoordinatedContainers[0]}'`);
517
1110
  }
518
- if (runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', `refs/heads/${effectiveBranch}`])) {
519
- throw new Error(`Local branch '${effectiveBranch}' still exists after removal`);
1111
+ const spinner = p.spinner();
1112
+ spinner.start(`Stopping container(s): ${uncoordinatedContainers.join(', ')}`);
1113
+ for (const name of uncoordinatedContainers) {
1114
+ if (!runOkEngine(engine, 'docker', ['stop', name]))
1115
+ throw new Error(`Failed to stop sandbox container: ${name}`);
1116
+ if (!runOkEngine(engine, 'docker', ['rm', name]))
1117
+ throw new Error(`Failed to remove sandbox container: ${name}`);
520
1118
  }
1119
+ const remaining = runEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
1120
+ const leftovers = uncoordinatedContainers.filter((name) => remaining.includes(name));
1121
+ if (leftovers.length > 0)
1122
+ throw new Error(`Sandbox container(s) still exist after removal: ${leftovers.join(', ')}`);
1123
+ spinner.stop(pc.green(`Removed container(s): ${uncoordinatedContainers.join(', ')}`));
521
1124
  }
522
- }
523
- for (const { tool, candidates } of toolCandidates) {
524
- for (const dir of candidates.filter((candidate) => fs.existsSync(candidate))) {
525
- removeManagedDir(tool.sandboxBase, dir);
526
- assertRemoved(dir, `${tool.name} state`);
527
- p.log.success(`${tool.name} state removed: ${dir}`);
1125
+ else {
1126
+ p.log.warn(`No sandbox container found for '${branch}'`);
528
1127
  }
529
- }
530
- for (const dir of shellConfigDirCandidates(config, effectiveBranch).filter((candidate) => fs.existsSync(candidate))) {
531
- removeManagedDir(config.shellConfigBase, dir);
532
- assertRemoved(dir, 'Shell config');
533
- p.log.success(`Shell config removed: ${dir}`);
534
- }
535
- const shareBranch = shareBranchDir(config, effectiveBranch);
536
- if (fs.existsSync(shareBranch)) {
537
- const shouldRemoveShare = options.assumeYes
538
- ? true
539
- : await confirm({
540
- message: `Remove share dir for branch '${effectiveBranch}' (${shareBranch})?`,
541
- initialValue: true
542
- });
543
- if (!isCancel(shouldRemoveShare) && shouldRemoveShare) {
544
- removeManagedDir(config.shareBase, shareBranch);
545
- assertRemoved(shareBranch, 'Share dir');
546
- p.log.success(`Share dir removed: ${shareBranch}`);
1128
+ const runWorkspaceCleanup = prepareRemovalAction(config.project, target, targetDigest, 'workspace-finalizing', 'workspace-removed', resourceLocks);
1129
+ if (runWorkspaceCleanup.run) {
1130
+ for (const [roots, base, label] of [
1131
+ [workspaceViewRoots, path.join(config.workspaceViewBase, config.project), 'Workspace view'],
1132
+ [controlRoots, path.join(config.controlBase, config.project), 'Control channel']
1133
+ ]) {
1134
+ for (const directory of roots.filter((candidate) => shouldStageManagedRemoval(candidate, removalTombstonePath(targetDigest, 'workspace', candidate), runWorkspaceCleanup.recovering))) {
1135
+ assertLegacyCandidateEvidence(directory, base === path.join(config.controlBase, config.project)
1136
+ ? config.controlBase : config.workspaceViewBase, config, effectiveBranch, matchedContainers);
1137
+ stageManagedRemoval(base, directory, removalTombstonePath(targetDigest, 'workspace', directory), runWorkspaceCleanup.recovering, {
1138
+ version: 1,
1139
+ targetDigest,
1140
+ permitDigest: committedTarget.permitDigest,
1141
+ kind: 'workspace',
1142
+ source: directory
1143
+ });
1144
+ if (!options.quiet)
1145
+ p.log.success(`${label} removed: ${directory}`);
1146
+ }
1147
+ }
1148
+ if (shouldRemoveWorktree) {
1149
+ for (const worktree of existingWorktrees) {
1150
+ const permit = permits.get(path.resolve(worktree));
1151
+ if (!permit)
1152
+ throw new Error(`Missing worktree removal permit: ${worktree}`);
1153
+ stageWorktreeRemoval(config, worktree, permit, removalTombstonePath(targetDigest, 'worktree', worktree), runWorkspaceCleanup.recovering, {
1154
+ version: 1,
1155
+ targetDigest,
1156
+ permitDigest: committedTarget.permitDigest,
1157
+ kind: 'worktree',
1158
+ source: worktree
1159
+ });
1160
+ }
1161
+ }
1162
+ }
1163
+ else {
1164
+ assertRemovalPathsAbsent([
1165
+ ...removalActionPaths(target, config, 'workspace-removed'),
1166
+ ...(shouldRemoveWorktree ? existingWorktrees : [])
1167
+ ]);
1168
+ }
1169
+ advanceRemovalJournals(config.project, target, targetDigest, 'workspace-removed', resourceLocks);
1170
+ cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest);
1171
+ for (const [roots, base] of [
1172
+ [workspaceViewRoots, path.join(config.workspaceViewBase, config.project)],
1173
+ [controlRoots, path.join(config.controlBase, config.project)]
1174
+ ]) {
1175
+ for (const directory of roots) {
1176
+ if (!fs.existsSync(directory))
1177
+ removeEmptyManagedParent(base, directory);
1178
+ }
1179
+ }
1180
+ const runBranchCleanup = prepareRemovalAction(config.project, target, targetDigest, 'branch-finalizing', 'branch-removed', resourceLocks);
1181
+ if (runBranchCleanup.run) {
1182
+ if (shouldRemoveWorktree && shouldDeleteBranch) {
1183
+ stageBranchRemoval(config, effectiveBranch, targetDigest, permits, runBranchCleanup.recovering);
1184
+ }
1185
+ }
1186
+ else if (shouldRemoveWorktree && shouldDeleteBranch
1187
+ && runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', `refs/heads/${effectiveBranch}`])) {
1188
+ throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${effectiveBranch}`);
1189
+ }
1190
+ advanceRemovalJournals(config.project, target, targetDigest, 'branch-removed', resourceLocks);
1191
+ cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest);
1192
+ const runToolCleanup = prepareRemovalAction(config.project, target, targetDigest, 'tool-finalizing', 'tool-removed', resourceLocks);
1193
+ if (runToolCleanup.run) {
1194
+ for (const { tool, candidates } of toolCandidates) {
1195
+ for (const dir of candidates.filter((candidate) => shouldStageManagedRemoval(candidate, removalTombstonePath(targetDigest, 'tool', candidate), runToolCleanup.recovering))) {
1196
+ stageManagedRemoval(tool.sandboxBase, dir, removalTombstonePath(targetDigest, 'tool', dir), runToolCleanup.recovering, {
1197
+ version: 1,
1198
+ targetDigest,
1199
+ permitDigest: committedTarget.permitDigest,
1200
+ kind: 'tool',
1201
+ source: dir
1202
+ });
1203
+ p.log.success(`${tool.name} state removed: ${dir}`);
1204
+ }
1205
+ }
1206
+ }
1207
+ else {
1208
+ assertRemovalPathsAbsent(removalActionPaths(target, config, 'tool-removed'));
1209
+ }
1210
+ advanceRemovalJournals(config.project, target, targetDigest, 'tool-removed', resourceLocks);
1211
+ cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest);
1212
+ const runShellCleanup = prepareRemovalAction(config.project, target, targetDigest, 'shell-finalizing', 'shell-removed', resourceLocks);
1213
+ if (runShellCleanup.run) {
1214
+ for (const dir of shellConfigDirCandidates(config, effectiveBranch).filter((candidate) => shouldStageManagedRemoval(candidate, removalTombstonePath(targetDigest, 'shell', candidate), runShellCleanup.recovering))) {
1215
+ stageManagedRemoval(config.shellConfigBase, dir, removalTombstonePath(targetDigest, 'shell', dir), runShellCleanup.recovering, {
1216
+ version: 1,
1217
+ targetDigest,
1218
+ permitDigest: committedTarget.permitDigest,
1219
+ kind: 'shell',
1220
+ source: dir
1221
+ });
1222
+ p.log.success(`Shell config removed: ${dir}`);
1223
+ }
1224
+ }
1225
+ else {
1226
+ assertRemovalPathsAbsent(removalActionPaths(target, config, 'shell-removed'));
1227
+ }
1228
+ advanceRemovalJournals(config.project, target, targetDigest, 'shell-removed', resourceLocks);
1229
+ cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest);
1230
+ const runShareCleanup = prepareRemovalAction(config.project, target, targetDigest, 'share-finalizing', 'share-removed', resourceLocks);
1231
+ if (runShareCleanup.run) {
1232
+ if (shouldRemoveShare && shouldStageManagedRemoval(sharePath, removalTombstonePath(targetDigest, 'share', sharePath), runShareCleanup.recovering)) {
1233
+ stageManagedRemoval(config.shareBase, sharePath, removalTombstonePath(targetDigest, 'share', sharePath), runShareCleanup.recovering, {
1234
+ version: 1,
1235
+ targetDigest,
1236
+ permitDigest: committedTarget.permitDigest,
1237
+ kind: 'share',
1238
+ source: sharePath
1239
+ });
1240
+ p.log.success(`Share dir removed: ${sharePath}`);
1241
+ }
1242
+ }
1243
+ else if (shouldRemoveShare) {
1244
+ assertRemovalPathsAbsent(removalActionPaths(target, config, 'share-removed'));
1245
+ }
1246
+ advanceRemovalJournals(config.project, target, targetDigest, 'share-removed', resourceLocks);
1247
+ cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest);
1248
+ const completedJournals = advanceRemovalJournals(config.project, target, targetDigest, 'completed', resourceLocks);
1249
+ for (const journal of completedJournals)
1250
+ clearSandboxRemovalJournalRecord(journal);
1251
+ if (!options.quiet) {
1252
+ p.outro(pc.green('Sandbox removed'));
547
1253
  }
548
1254
  }
549
- if (!options.quiet) {
550
- p.outro(pc.green('Sandbox removed'));
1255
+ finally {
1256
+ for (const lock of [...resourceLocks.values()].reverse())
1257
+ lock.release();
551
1258
  }
552
1259
  }
553
1260
  async function rmPurge(config, tools, prompt = {}) {