@fitlab-ai/agent-infra 0.12.0 → 0.12.2

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 (331) hide show
  1. package/README.md +0 -2
  2. package/README.zh-CN.md +0 -2
  3. package/dist/bin/internal-cli.js +2 -17
  4. package/dist/lib/agent-clients/adapter.js +0 -13
  5. package/dist/lib/agent-clients/adapters/antigravity-cli.js +0 -5
  6. package/dist/lib/agent-clients/adapters/claude-code-credentials.js +0 -17
  7. package/dist/lib/agent-clients/adapters/claude-code.js +0 -9
  8. package/dist/lib/agent-clients/adapters/codex.js +0 -52
  9. package/dist/lib/agent-clients/adapters/opencode.js +0 -5
  10. package/dist/lib/agent-clients/adapters/traecli.js +0 -5
  11. package/dist/lib/agent-clients/config.js +19 -54
  12. package/dist/lib/agent-clients/registry.js +1 -12
  13. package/dist/lib/agent-clients/schema.js +0 -18
  14. package/dist/lib/agent-clients/tokens.js +1 -2
  15. package/dist/lib/agent-clients/types.js +0 -1
  16. package/dist/lib/decide.js +2 -0
  17. package/dist/lib/git/worktree-identity.js +1 -9
  18. package/dist/lib/internal/agent-client.js +2 -62
  19. package/dist/lib/internal/cli-route-inventory.js +1 -5
  20. package/dist/lib/internal/manual-validation.js +29 -27
  21. package/dist/lib/internal/platform-comment.js +56 -7
  22. package/dist/lib/internal/task-create.js +1 -1
  23. package/dist/lib/internal/task-event.js +1 -8
  24. package/dist/lib/internal/task-finalization.js +0 -1
  25. package/dist/lib/internal/task-lifecycle.js +2 -2
  26. package/dist/lib/internal/task-operation-registry.js +4 -20
  27. package/dist/lib/internal/task-review.js +4 -4
  28. package/dist/lib/platform/comment-safety.js +1 -7
  29. package/dist/lib/platform/in-label-sync.js +1 -14
  30. package/dist/lib/platform/issue-comments.js +373 -282
  31. package/dist/lib/platform/issue-summary-content.js +112 -0
  32. package/dist/lib/platform/issues.js +1 -1
  33. package/dist/lib/platform/operation-coordinator.js +4 -2
  34. package/dist/lib/platform/pr-change-report.js +176 -30
  35. package/dist/lib/platform/pr-checks.js +2 -8
  36. package/dist/lib/platform/pr-summary-content.js +54 -0
  37. package/dist/lib/platform/pr-summary.js +318 -284
  38. package/dist/lib/platform/provider-validation.js +0 -20
  39. package/dist/lib/platform/pull-requests.js +73 -84
  40. package/dist/lib/platform/releases.js +1 -43
  41. package/dist/lib/platform/summary-content-pipeline.js +18 -0
  42. package/dist/lib/platform/verification-required.js +0 -2
  43. package/dist/lib/platform/verification-sync.js +1 -79
  44. package/dist/lib/process-data/normalize.js +1 -185
  45. package/dist/lib/process-data/privacy.js +1 -16
  46. package/dist/lib/process-data/sources.js +2 -279
  47. package/dist/lib/render.js +0 -12
  48. package/dist/lib/run/index.js +15 -10
  49. package/dist/lib/run/skills.js +0 -3
  50. package/dist/lib/sandbox/commands/create.js +40 -53
  51. package/dist/lib/sandbox/commands/list-running.js +0 -18
  52. package/dist/lib/sandbox/commands/start.js +7 -8
  53. package/dist/lib/sandbox/constants.js +0 -6
  54. package/dist/lib/sandbox/control/client.js +0 -1
  55. package/dist/lib/sandbox/control/container-identity.js +1 -16
  56. package/dist/lib/sandbox/control/executor.js +0 -4
  57. package/dist/lib/sandbox/control/identity-sentinel.js +0 -7
  58. package/dist/lib/sandbox/control/lifecycle.js +21 -37
  59. package/dist/lib/sandbox/control/native-file-lock.js +0 -3
  60. package/dist/lib/sandbox/control/server.js +18 -38
  61. package/dist/lib/sandbox/control/state.js +1 -9
  62. package/dist/lib/sandbox/dockerfile.js +0 -6
  63. package/dist/lib/sandbox/engine.js +0 -14
  64. package/dist/lib/sandbox/engines/docker-exact-discovery.js +1 -4
  65. package/dist/lib/sandbox/engines/wsl2-paths.js +0 -3
  66. package/dist/lib/sandbox/recovery.js +24 -26
  67. package/dist/lib/sandbox/removal.js +67 -518
  68. package/dist/lib/sandbox/task-resolver.js +0 -41
  69. package/dist/lib/sandbox/tools.js +1 -4
  70. package/dist/lib/sandbox/workspace-view.js +1 -2
  71. package/dist/lib/server/display.js +0 -6
  72. package/dist/lib/task/activity-intent.js +45 -40
  73. package/dist/lib/task/activity-log.js +9 -16
  74. package/dist/lib/task/artifact-command.js +1 -1
  75. package/dist/lib/task/artifact-lifecycle.js +61 -63
  76. package/dist/lib/task/artifact-name.js +0 -6
  77. package/dist/lib/task/artifact-operations.js +3 -3
  78. package/dist/lib/task/capabilities.js +4 -5
  79. package/dist/lib/task/commands/log.js +0 -6
  80. package/dist/lib/task/commands/status.js +17 -60
  81. package/dist/lib/task/commit-operation.js +28 -39
  82. package/dist/lib/task/completion-facts.js +56 -43
  83. package/dist/lib/task/control-authority.js +23 -256
  84. package/dist/lib/task/control-recovery.js +3 -5
  85. package/dist/lib/task/create.js +2 -4
  86. package/dist/lib/task/decision-intents.js +8 -9
  87. package/dist/lib/task/delivery-target.js +1 -40
  88. package/dist/lib/task/events.js +84 -107
  89. package/dist/lib/task/finalization.js +60 -15
  90. package/dist/lib/task/ledger-intents.js +1 -1
  91. package/dist/lib/task/lifecycle-execution.js +38 -33
  92. package/dist/lib/task/lifecycle.js +49 -41
  93. package/dist/lib/task/local-artifact-finalization.js +1 -4
  94. package/dist/lib/task/manual-validation-evidence.js +1 -37
  95. package/dist/lib/task/platform-operation-recovery.js +151 -108
  96. package/dist/lib/task/pr-delivery-fact.js +2 -5
  97. package/dist/lib/task/qualification-confirmation.js +5 -7
  98. package/dist/lib/task/resolve-ref.js +1 -25
  99. package/dist/lib/task/review-command.js +2 -2
  100. package/dist/lib/task/review-finalization.js +1 -21
  101. package/dist/lib/task/review-fingerprint.js +0 -4
  102. package/dist/lib/task/secure-fs.js +0 -56
  103. package/dist/lib/task/short-id.js +1 -39
  104. package/dist/lib/task/skill-scope.js +0 -1
  105. package/dist/lib/task/verification-engine.js +58 -282
  106. package/dist/lib/task/verification.js +6 -3
  107. package/dist/lib/task/write.js +11 -1
  108. package/lib/agent-clients/adapter.ts +0 -76
  109. package/lib/agent-clients/adapters/antigravity-cli.ts +0 -5
  110. package/lib/agent-clients/adapters/claude-code-credentials.ts +0 -19
  111. package/lib/agent-clients/adapters/claude-code.ts +0 -9
  112. package/lib/agent-clients/adapters/codex.ts +0 -56
  113. package/lib/agent-clients/adapters/opencode.ts +0 -5
  114. package/lib/agent-clients/adapters/traecli.ts +0 -5
  115. package/lib/agent-clients/config.ts +20 -61
  116. package/lib/agent-clients/registry.ts +1 -34
  117. package/lib/agent-clients/schema.ts +0 -18
  118. package/lib/agent-clients/tokens.ts +1 -2
  119. package/lib/agent-clients/types.ts +0 -15
  120. package/lib/decide.ts +1 -0
  121. package/lib/git/worktree-identity.ts +0 -10
  122. package/lib/internal/agent-client.ts +2 -70
  123. package/lib/internal/cli-route-inventory.ts +1 -4
  124. package/lib/internal/manual-validation.ts +28 -27
  125. package/lib/internal/platform-comment.ts +46 -4
  126. package/lib/internal/task-create.ts +0 -1
  127. package/lib/internal/task-event.ts +1 -5
  128. package/lib/internal/task-finalization.ts +0 -1
  129. package/lib/internal/task-lifecycle.ts +3 -5
  130. package/lib/internal/task-operation-registry.ts +3 -19
  131. package/lib/internal/task-review.ts +4 -4
  132. package/lib/platform/comment-safety.ts +0 -10
  133. package/lib/platform/github-provider.ts +0 -2
  134. package/lib/platform/in-label-sync.ts +0 -16
  135. package/lib/platform/issue-comments.ts +478 -301
  136. package/lib/platform/issue-summary-content.ts +132 -0
  137. package/lib/platform/issues.ts +0 -2
  138. package/lib/platform/operation-coordinator.ts +4 -2
  139. package/lib/platform/pr-change-report.ts +165 -25
  140. package/lib/platform/pr-checks.ts +1 -10
  141. package/lib/platform/pr-review.ts +1 -1
  142. package/lib/platform/pr-summary-content.ts +60 -0
  143. package/lib/platform/pr-summary.ts +364 -257
  144. package/lib/platform/provider-bridge.ts +0 -1
  145. package/lib/platform/provider-loader.ts +1 -5
  146. package/lib/platform/provider-validation.ts +1 -49
  147. package/lib/platform/pull-requests.ts +64 -99
  148. package/lib/platform/releases.ts +1 -40
  149. package/lib/platform/summary-content-pipeline.ts +23 -0
  150. package/lib/platform/verification-required.ts +0 -2
  151. package/lib/platform/verification-sync.ts +0 -94
  152. package/lib/process-data/normalize.ts +1 -186
  153. package/lib/process-data/privacy.ts +2 -19
  154. package/lib/process-data/sources.ts +3 -283
  155. package/lib/render.ts +0 -15
  156. package/lib/run/index.ts +13 -7
  157. package/lib/run/skills.ts +0 -4
  158. package/lib/sandbox/commands/create.ts +37 -74
  159. package/lib/sandbox/commands/list-running.ts +0 -19
  160. package/lib/sandbox/commands/start.ts +7 -10
  161. package/lib/sandbox/constants.ts +0 -8
  162. package/lib/sandbox/control/client.ts +0 -1
  163. package/lib/sandbox/control/container-identity.ts +1 -16
  164. package/lib/sandbox/control/executor.ts +1 -5
  165. package/lib/sandbox/control/identity-sentinel.ts +0 -8
  166. package/lib/sandbox/control/lifecycle.ts +18 -51
  167. package/lib/sandbox/control/native-file-lock.ts +0 -8
  168. package/lib/sandbox/control/protocol.ts +0 -2
  169. package/lib/sandbox/control/server.ts +13 -39
  170. package/lib/sandbox/control/state.ts +1 -10
  171. package/lib/sandbox/dockerfile.ts +0 -11
  172. package/lib/sandbox/engine.ts +0 -17
  173. package/lib/sandbox/engines/docker-exact-discovery.ts +1 -5
  174. package/lib/sandbox/engines/wsl2-paths.ts +0 -4
  175. package/lib/sandbox/recovery.ts +24 -26
  176. package/lib/sandbox/removal.ts +58 -687
  177. package/lib/sandbox/shell.ts +1 -1
  178. package/lib/sandbox/task-resolver.ts +0 -48
  179. package/lib/sandbox/tools.ts +1 -5
  180. package/lib/sandbox/workspace-view.ts +2 -4
  181. package/lib/server/display.ts +0 -14
  182. package/lib/server/process-control.ts +1 -1
  183. package/lib/task/activity-intent.ts +36 -44
  184. package/lib/task/activity-log.ts +9 -22
  185. package/lib/task/artifact-command.ts +1 -1
  186. package/lib/task/artifact-lifecycle.ts +58 -67
  187. package/lib/task/artifact-name.ts +0 -7
  188. package/lib/task/artifact-operations.ts +3 -3
  189. package/lib/task/capabilities.ts +5 -6
  190. package/lib/task/commands/log.ts +0 -7
  191. package/lib/task/commands/status.ts +14 -79
  192. package/lib/task/commit-operation.ts +29 -51
  193. package/lib/task/completion-facts.ts +45 -42
  194. package/lib/task/control-authority.ts +27 -317
  195. package/lib/task/control-recovery.ts +2 -5
  196. package/lib/task/create.ts +2 -5
  197. package/lib/task/decision-intents.ts +5 -5
  198. package/lib/task/delivery-target.ts +0 -40
  199. package/lib/task/events.ts +89 -123
  200. package/lib/task/finalization.ts +52 -15
  201. package/lib/task/ledger-intents.ts +1 -1
  202. package/lib/task/lifecycle-execution.ts +37 -89
  203. package/lib/task/lifecycle.ts +48 -44
  204. package/lib/task/local-artifact-finalization.ts +0 -7
  205. package/lib/task/manual-validation-evidence.ts +1 -47
  206. package/lib/task/platform-operation-recovery.ts +117 -104
  207. package/lib/task/pr-delivery-fact.ts +2 -11
  208. package/lib/task/qualification-confirmation.ts +6 -6
  209. package/lib/task/resolve-ref.ts +0 -23
  210. package/lib/task/review-command.ts +2 -2
  211. package/lib/task/review-finalization.ts +1 -27
  212. package/lib/task/review-fingerprint.ts +0 -8
  213. package/lib/task/secure-fs.ts +0 -42
  214. package/lib/task/short-id.ts +0 -43
  215. package/lib/task/skill-scope.ts +0 -1
  216. package/lib/task/verification-engine.ts +56 -287
  217. package/lib/task/verification.ts +5 -5
  218. package/lib/task/write.ts +12 -1
  219. package/package.json +2 -2
  220. package/templates/.agents/README.en.md +1 -3
  221. package/templates/.agents/README.zh-CN.md +0 -2
  222. package/templates/.agents/rules/README.en.md +0 -1
  223. package/templates/.agents/rules/README.zh-CN.md +0 -1
  224. package/templates/.agents/rules/commit-and-pr.en.md +1 -1
  225. package/templates/.agents/rules/commit-and-pr.zh-CN.md +1 -1
  226. package/templates/.agents/rules/no-mid-flow-questions.en.md +0 -10
  227. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +0 -10
  228. package/templates/.agents/rules/pr-sync.en.md +4 -2
  229. package/templates/.agents/rules/pr-sync.zh-CN.md +6 -9
  230. package/templates/.agents/rules/task-management.en.md +3 -3
  231. package/templates/.agents/rules/task-management.zh-CN.md +3 -3
  232. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
  233. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  234. package/templates/.agents/skills/cancel-task/SKILL.en.md +2 -2
  235. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
  236. package/templates/.agents/skills/code-task/SKILL.en.md +2 -2
  237. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -4
  238. package/templates/.agents/skills/commit/SKILL.en.md +2 -2
  239. package/templates/.agents/skills/commit/SKILL.zh-CN.md +2 -2
  240. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +6 -6
  241. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +6 -6
  242. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  243. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  244. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +2 -2
  245. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +2 -2
  246. package/templates/.agents/skills/complete-task/SKILL.en.md +10 -25
  247. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -26
  248. package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
  249. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  250. package/templates/.agents/skills/create-pr/reference/change-report.en.md +9 -2
  251. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +9 -2
  252. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  253. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  254. package/templates/.agents/skills/create-task/SKILL.en.md +0 -18
  255. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +0 -18
  256. package/templates/.agents/skills/plan-task/SKILL.en.md +2 -2
  257. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
  258. package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
  259. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
  260. package/templates/.agents/skills/review-code/SKILL.en.md +5 -5
  261. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -5
  262. package/templates/.agents/skills/review-plan/SKILL.en.md +3 -3
  263. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
  264. package/templates/.agents/skills/review-pr/SKILL.en.md +4 -4
  265. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +4 -4
  266. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
  267. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
  268. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +3 -75
  269. package/templates/.agents/skills/watch-pr/SKILL.en.md +1 -1
  270. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
  271. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +3 -3
  272. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +3 -3
  273. package/templates/.agents/templates/task.en.md +1 -0
  274. package/templates/.agents/templates/task.zh-CN.md +1 -0
  275. package/templates/.claude/settings.json +0 -24
  276. package/templates/.codex/hooks.json +0 -50
  277. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +0 -582
  278. package/dist/lib/agent-clients/adapters/codex-lifecycle/binding.js +0 -114
  279. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +0 -237
  280. package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +0 -443
  281. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +0 -236
  282. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +0 -292
  283. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +0 -267
  284. package/dist/lib/agent-clients/adapters/codex-orchestration.js +0 -466
  285. package/dist/lib/internal/task-orchestration.js +0 -71
  286. package/dist/lib/platform/verification.js +0 -6
  287. package/dist/lib/run/prompt.js +0 -2
  288. package/dist/lib/sandbox/engines/authority.js +0 -289
  289. package/dist/lib/task/delegation-receipts.js +0 -410
  290. package/dist/lib/task/orchestration.js +0 -1233
  291. package/dist/lib/task/verification-checks.js +0 -9
  292. package/dist/lib/task/workspace-snapshot.js +0 -121
  293. package/dist/lifecycle-build-manifest.json +0 -6
  294. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +0 -688
  295. package/lib/agent-clients/adapters/codex-lifecycle/binding.ts +0 -147
  296. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +0 -349
  297. package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +0 -506
  298. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +0 -385
  299. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +0 -41
  300. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +0 -418
  301. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +0 -354
  302. package/lib/agent-clients/adapters/codex-orchestration.ts +0 -630
  303. package/lib/internal/task-orchestration.ts +0 -74
  304. package/lib/platform/verification.ts +0 -11
  305. package/lib/run/prompt.ts +0 -1
  306. package/lib/sandbox/engines/authority.ts +0 -365
  307. package/lib/task/delegation-receipts.ts +0 -547
  308. package/lib/task/orchestration.ts +0 -1651
  309. package/lib/task/verification-checks.ts +0 -10
  310. package/lib/task/workspace-snapshot.ts +0 -144
  311. package/templates/.agents/hooks/lifecycle-delegation.js +0 -140
  312. package/templates/.agents/rules/lifecycle-orchestration.en.md +0 -44
  313. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +0 -44
  314. package/templates/.agents/skills/run-task/SKILL.en.md +0 -29
  315. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +0 -29
  316. package/templates/.agents/skills/run-task/config/verify.en.json +0 -10
  317. package/templates/.agents/skills/run-task/config/verify.zh-CN.json +0 -10
  318. package/templates/.agents/skills/run-task/reference/host-validation.en.md +0 -24
  319. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +0 -24
  320. package/templates/.claude/agents/agent-infra-lifecycle-executor.en.md +0 -6
  321. package/templates/.claude/agents/agent-infra-lifecycle-executor.zh-CN.md +0 -6
  322. package/templates/.claude/agents/agent-infra-lifecycle-reviewer.en.md +0 -6
  323. package/templates/.claude/agents/agent-infra-lifecycle-reviewer.zh-CN.md +0 -6
  324. package/templates/.claude/commands/run-task.en.md +0 -8
  325. package/templates/.claude/commands/run-task.zh-CN.md +0 -8
  326. package/templates/.codex/agents/agent-infra-lifecycle-executor.en.toml +0 -3
  327. package/templates/.codex/agents/agent-infra-lifecycle-executor.zh-CN.toml +0 -3
  328. package/templates/.codex/agents/agent-infra-lifecycle-reviewer.en.toml +0 -3
  329. package/templates/.codex/agents/agent-infra-lifecycle-reviewer.zh-CN.toml +0 -3
  330. package/templates/.opencode/commands/run-task.en.md +0 -11
  331. package/templates/.opencode/commands/run-task.zh-CN.md +0 -11
@@ -1,6 +1,5 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { createHash } from 'node:crypto';
4
3
  import * as p from '@clack/prompts';
5
4
  import pc from 'picocolors';
6
5
  import type { SandboxConfig } from './config.ts';
@@ -15,48 +14,31 @@ import {
15
14
  shellConfigDirCandidates,
16
15
  worktreeDirCandidates
17
16
  } from './constants.ts';
18
- import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from './engine.ts';
19
- import { pruneSandboxDanglingImages } from './image-prune.ts';
20
- import { assertManagedPath, removeManagedDir, removeWorktreeDir } from './managed-fs.ts';
21
- import { run, runEngine, runOk, runOkEngine, runSafe, runSafeEngine } from './shell.ts';
17
+ import { detectEngine } from './engine.ts';
18
+ import './image-prune.ts';
19
+ import { assertManagedPath, removeWorktreeDir } from './managed-fs.ts';
20
+ import { runEngine, runOk, runSafe, runSafeEngine } from './shell.ts';
22
21
  import {
23
- parseSandboxWorkspaceIdentity,
24
22
  resolveSandboxCleanupTarget,
25
23
  resolveSandboxTarget,
26
- sameSandboxWorkspaceIdentity,
27
24
  type SandboxCleanupTarget,
28
25
  type SandboxWorkspaceKey
29
26
  } from './workspace-identity.ts';
30
27
  import { sandboxControlPaths, sandboxWorkspaceViewPaths } from './workspace-view.ts';
31
- import {
32
- advanceSandboxRemovalJournalToPhase,
33
- advanceSandboxRemovalJournalPhase,
34
- claimSandboxRemovalJournal,
35
- clearSandboxRemovalJournalRecord,
36
- isDefaultSandboxRemovalJournal,
37
- listSandboxRemovalJournals,
38
- removeSandboxControlRoot,
39
- readSandboxControlManifest,
40
- sandboxRemovalPhaseIndex,
41
- type SandboxRemovalJournal,
42
- type SandboxRemovalTargetCommit
43
- } from './control/lifecycle.ts';
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';
28
+ import './control/lifecycle.ts';
29
+ import './control/container-identity.ts';
47
30
  import { toolConfigDirCandidates, toolProjectDirCandidates } from './tools.ts';
48
31
  import type { SandboxTool } from './tools.ts';
49
- import { getProcessStartTime } from '../server/process-state.ts';
32
+ import '../server/process-state.ts';
50
33
  import { releaseStaleShortIdRegistry } from '../task/short-id.ts';
51
- import { fetchSandboxRows, type SandboxRow } from './commands/list-running.ts';
52
- import { createSandboxControlBindingEvidence } from './task-cleanup.ts';
34
+ import { fetchSandboxRows } from './commands/list-running.ts';
35
+ import './task-cleanup.ts';
53
36
  import {
54
37
  createCleanPermit,
55
38
  createDiscardPermit,
56
39
  formatWorktreeSnapshot,
57
40
  inspectRecoveredWorktree,
58
- inspectWorktrees,
59
- verifyWorktreePermit
41
+ inspectWorktrees
60
42
  } from './worktree-safety.ts';
61
43
  import type {
62
44
  WorktreeInspection,
@@ -73,131 +55,39 @@ function isMissingPathError(error: unknown): boolean {
73
55
  && (error as { code?: unknown }).code === 'ENOENT');
74
56
  }
75
57
 
76
- function canonicalPath(input: string): string {
77
- try { return fs.realpathSync.native(input); }
78
- catch { return path.resolve(input); }
79
- }
80
-
81
- function isMissingTaskRecordError(error: unknown): boolean {
82
- return error instanceof Error && /^Task not found: TASK-\d{8}-\d{6}$/.test(error.message);
58
+ function removalErrorCode(error: unknown): string | null {
59
+ return error && typeof error === 'object' && 'code' in error
60
+ && typeof (error as { code?: unknown }).code === 'string'
61
+ ? (error as { code: string }).code
62
+ : null;
83
63
  }
84
64
 
85
- function digestCleanupValue(value: unknown): string {
86
- return createHash('sha256').update(JSON.stringify(value)).digest('hex');
65
+ function removalErrorMessage(error: unknown): string {
66
+ const code = removalErrorCode(error);
67
+ const message = error instanceof Error ? error.message : String(error);
68
+ return code ? `${code}: ${message}` : message;
87
69
  }
88
70
 
89
- function removalTargetDigest(config: SandboxConfig, target: RmTarget): string {
90
- return digestCleanupValue({
91
- project: config.project,
92
- branch: target.effectiveBranch,
93
- workspace: target.workspace,
94
- controlRoots: target.controlRoots.map((candidate) => path.resolve(candidate)),
95
- workspaceViewRoots: target.workspaceViewRoots.map((candidate) => path.resolve(candidate)),
96
- managedPathCandidates: (target.managedPathCandidates ?? []).map((candidate) => path.resolve(candidate)).sort()
97
- });
98
- }
99
-
100
- function removalPermitDigest(permits: ReadonlyMap<string, WorktreeRemovalPermit>): string {
101
- return digestCleanupValue([...permits.entries()]
102
- .map(([worktree, permit]) => [path.resolve(worktree), permit.mode, permit.snapshot.identity])
103
- .sort(([left], [right]) => String(left).localeCompare(String(right))));
104
- }
105
-
106
- function removalPermitCommits(permits: ReadonlyMap<string, WorktreeRemovalPermit>): SandboxRemovalTargetCommit['permits'] {
107
- return [...permits.entries()]
108
- .map(([worktree, permit]) => ({
109
- path: path.resolve(worktree),
110
- mode: permit.mode,
111
- snapshot: permit.snapshot
112
- }))
113
- .sort((left, right) => left.path.localeCompare(right.path));
114
- }
115
-
116
- function removalTargetCommit(
117
- config: SandboxConfig,
118
- target: RmTarget,
119
- permits: ReadonlyMap<string, WorktreeRemovalPermit>,
120
- removeWorktree: boolean,
121
- removeBranch: boolean,
122
- removeShare: boolean
123
- ): SandboxRemovalTargetCommit {
124
- return {
125
- branch: target.effectiveBranch,
126
- project: config.project,
127
- controlRoot: path.resolve(target.controlRoots[0] ?? path.join(config.controlBase, config.project)),
128
- targetDigest: removalTargetDigest(config, target),
129
- permitDigest: removalPermitDigest(permits),
130
- removeWorktree,
131
- removeBranch,
132
- removeShare,
133
- worktreePaths: target.existingWorktrees.map((candidate) => path.resolve(candidate)).sort(),
134
- workspaceViewPaths: target.workspaceViewRoots.map((candidate) => path.resolve(candidate)).sort(),
135
- toolPaths: target.toolCandidates.flatMap(({ candidates }) => candidates.map((candidate) => path.resolve(candidate))).sort(),
136
- shellPaths: shellConfigDirCandidates(config, target.effectiveBranch).map((candidate) => path.resolve(candidate)).sort(),
137
- sharePath: path.resolve(shareBranchDir(config, target.effectiveBranch)),
138
- permits: removalPermitCommits(permits)
139
- };
140
- }
71
+ function addOwnerWriteToDirectories(root: string): void {
72
+ const pending = [root];
73
+ while (pending.length > 0) {
74
+ const current = pending.pop()!;
75
+ let stat: fs.Stats;
76
+ try {
77
+ stat = fs.lstatSync(current);
78
+ } catch (error) {
79
+ if (isMissingPathError(error)) continue;
80
+ throw error;
81
+ }
82
+ if (!stat.isDirectory()) continue;
141
83
 
142
- function assertRemovalSelectionMatches(
143
- expected: SandboxRemovalTargetCommit,
144
- actual: SandboxRemovalTargetCommit
145
- ): void {
146
- const normalize = (value: SandboxRemovalTargetCommit): unknown => ({
147
- ...value,
148
- controlRoot: '',
149
- permits: value.permits
150
- });
151
- if (JSON.stringify(normalize(expected)) !== JSON.stringify(normalize(actual))) {
152
- throw new Error('SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH');
84
+ fs.chmodSync(current, stat.mode | 0o200);
85
+ for (const entry of fs.readdirSync(current)) pending.push(path.join(current, entry));
153
86
  }
154
87
  }
155
88
 
156
- function advanceRemovalJournals(
157
- project: string,
158
- target: RmTarget,
159
- targetDigest: string,
160
- phase: SandboxRemovalJournal['phase'],
161
- resourceLocks: ReadonlyMap<string, SandboxResourceLock>
162
- ): SandboxRemovalJournal[] {
163
- return listSandboxRemovalJournals({ branch: target.effectiveBranch, project, targetDigest }).map((journal) => {
164
- if (sandboxRemovalPhaseIndex(journal.phase) >= sandboxRemovalPhaseIndex(phase)) return journal;
165
- const lock = resourceLocks.get(journal.target.controlRoot);
166
- if (!lock) throw new Error('SANDBOX_CONTROL_REMOVAL_LOCK_MISMATCH');
167
- return advanceSandboxRemovalJournalToPhase(journal, phase, lock);
168
- });
169
- }
170
-
171
- function prepareRemovalAction(
172
- project: string,
173
- target: RmTarget,
174
- targetDigest: string,
175
- startPhase: SandboxRemovalJournal['phase'],
176
- completedPhase: SandboxRemovalJournal['phase'],
177
- resourceLocks: ReadonlyMap<string, SandboxResourceLock>
178
- ): RemovalActionPreparation {
179
- const journals = listSandboxRemovalJournals({
180
- branch: target.effectiveBranch,
181
- project,
182
- targetDigest
183
- });
184
- const startIndex = sandboxRemovalPhaseIndex(startPhase);
185
- const completedIndex = sandboxRemovalPhaseIndex(completedPhase);
186
- const shouldRun = journals.length === 0
187
- || journals.some((journal) => sandboxRemovalPhaseIndex(journal.phase) < completedIndex);
188
- const recovering = journals.some((journal) => {
189
- const index = sandboxRemovalPhaseIndex(journal.phase);
190
- return index >= startIndex && index < completedIndex;
191
- });
192
- advanceRemovalJournals(project, target, targetDigest, startPhase, resourceLocks);
193
- return { run: shouldRun, recovering };
194
- }
195
-
196
- function assertRemovalPathsAbsent(paths: readonly string[]): void {
197
- const remaining = paths.filter((candidate) => fs.existsSync(candidate));
198
- if (remaining.length > 0) {
199
- throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${remaining[0]}`);
200
- }
89
+ function isPermissionRemovalError(error: unknown): boolean {
90
+ return ['EACCES', 'EPERM', 'ENOTEMPTY'].includes(removalErrorCode(error) ?? '');
201
91
  }
202
92
 
203
93
  function assertBranchRemovalIdentity(
@@ -220,514 +110,6 @@ function assertBranchRemovalIdentity(
220
110
  }
221
111
  }
222
112
 
223
- function removalTombstonePath(targetDigest: string, kind: RemovalActionKind, source: string): string {
224
- const sourceDigest = createHash('sha256')
225
- .update(`${kind}\0${path.resolve(source)}`)
226
- .digest('hex')
227
- .slice(0, 24);
228
- return path.join(
229
- path.dirname(path.resolve(source)),
230
- `.agent-infra-removal-${targetDigest.slice(0, 16)}-${kind}-${sourceDigest}`
231
- );
232
- }
233
-
234
- type RemovalTombstoneOwnership = Readonly<{
235
- version: 1;
236
- targetDigest: string;
237
- permitDigest: string;
238
- kind: RemovalActionKind;
239
- source: string;
240
- }>;
241
-
242
- type RemovalSourceIdentity = Readonly<{
243
- dev: string;
244
- ino: string;
245
- }>;
246
-
247
- type RemovalTombstoneRecord = RemovalTombstoneOwnership & Readonly<{
248
- sourceIdentity: RemovalSourceIdentity;
249
- }>;
250
-
251
- const REMOVAL_TOMBSTONE_MARKER = '.agent-infra-removal-ownership.json';
252
- const REMOVAL_TOMBSTONE_PAYLOAD = 'payload';
253
-
254
- function removalSourceClaimPath(source: string, ownership: RemovalTombstoneOwnership): string {
255
- const claimDigest = createHash('sha256')
256
- .update(JSON.stringify(ownership))
257
- .digest('hex')
258
- .slice(0, 24);
259
- return path.join(source, `.agent-infra-removal-source-${claimDigest}.json`);
260
- }
261
-
262
- function removalSourceIdentity(source: string): RemovalSourceIdentity {
263
- const stat = fs.lstatSync(source, { bigint: true });
264
- if (!stat.isDirectory() || stat.isSymbolicLink() || stat.dev === 0n || stat.ino === 0n) {
265
- throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
266
- }
267
- return { dev: String(stat.dev), ino: String(stat.ino) };
268
- }
269
-
270
- function sameRemovalSourceIdentity(left: RemovalSourceIdentity, right: RemovalSourceIdentity): boolean {
271
- return left.dev === right.dev && left.ino === right.ino;
272
- }
273
-
274
- function removalSourceClaimMatches(
275
- source: string,
276
- ownership: RemovalTombstoneOwnership,
277
- identity: RemovalSourceIdentity
278
- ): boolean {
279
- try {
280
- const claimPath = removalSourceClaimPath(source, ownership);
281
- const claimStat = fs.lstatSync(claimPath);
282
- if (!claimStat.isFile() || claimStat.isSymbolicLink()) return false;
283
- const record = JSON.parse(fs.readFileSync(claimPath, 'utf8')) as Partial<RemovalTombstoneRecord>;
284
- return record.version === ownership.version
285
- && record.targetDigest === ownership.targetDigest
286
- && record.permitDigest === ownership.permitDigest
287
- && record.kind === ownership.kind
288
- && typeof record.source === 'string'
289
- && path.resolve(record.source) === path.resolve(ownership.source)
290
- && record.sourceIdentity?.dev === identity.dev
291
- && record.sourceIdentity?.ino === identity.ino;
292
- } catch {
293
- return false;
294
- }
295
- }
296
-
297
- function isOwnedRemovalSourceClaim(source: string, ownership: RemovalTombstoneOwnership): boolean {
298
- try {
299
- const claimPath = removalSourceClaimPath(source, ownership);
300
- const claimStat = fs.lstatSync(claimPath);
301
- if (!claimStat.isFile() || claimStat.isSymbolicLink()) return false;
302
- const record = JSON.parse(fs.readFileSync(claimPath, 'utf8')) as Partial<RemovalTombstoneRecord>;
303
- return record.version === ownership.version
304
- && record.targetDigest === ownership.targetDigest
305
- && record.permitDigest === ownership.permitDigest
306
- && record.kind === ownership.kind
307
- && typeof record.source === 'string'
308
- && path.resolve(record.source) === path.resolve(ownership.source);
309
- } catch {
310
- return false;
311
- }
312
- }
313
-
314
- function removeOwnedSourceClaim(source: string, ownership: RemovalTombstoneOwnership): void {
315
- try {
316
- const claimPath = removalSourceClaimPath(source, ownership);
317
- if (isOwnedRemovalSourceClaim(source, ownership)) fs.unlinkSync(claimPath);
318
- } catch {
319
- // Preserve the source when the claim cannot be revalidated.
320
- }
321
- }
322
-
323
- function readRemovalTombstoneRecord(root: string, tombstone: string): RemovalTombstoneRecord | null {
324
- assertManagedPath(root, tombstone);
325
- try {
326
- const tombstoneStat = fs.lstatSync(tombstone);
327
- const markerStat = fs.lstatSync(removalTombstoneMarker(tombstone));
328
- if (!tombstoneStat.isDirectory() || tombstoneStat.isSymbolicLink()
329
- || !markerStat.isFile() || markerStat.isSymbolicLink()) return null;
330
- const record = JSON.parse(fs.readFileSync(removalTombstoneMarker(tombstone), 'utf8')) as Partial<RemovalTombstoneRecord>;
331
- if (record.version !== 1
332
- || typeof record.targetDigest !== 'string'
333
- || typeof record.permitDigest !== 'string'
334
- || typeof record.kind !== 'string'
335
- || typeof record.source !== 'string'
336
- || record.sourceIdentity?.dev === undefined
337
- || record.sourceIdentity.ino === undefined) return null;
338
- return record as RemovalTombstoneRecord;
339
- } catch {
340
- return null;
341
- }
342
- }
343
-
344
- function removalTombstonePayload(tombstone: string): string {
345
- return path.join(tombstone, REMOVAL_TOMBSTONE_PAYLOAD);
346
- }
347
-
348
- function removalTombstoneMarker(tombstone: string): string {
349
- return path.join(tombstone, REMOVAL_TOMBSTONE_MARKER);
350
- }
351
-
352
- function isOwnedRemovalTombstone(
353
- root: string,
354
- tombstone: string,
355
- ownership: RemovalTombstoneOwnership
356
- ): boolean {
357
- const record = readRemovalTombstoneRecord(root, tombstone);
358
- return record !== null
359
- && record.version === 1
360
- && record.targetDigest === ownership.targetDigest
361
- && record.permitDigest === ownership.permitDigest
362
- && record.kind === ownership.kind
363
- && typeof record.source === 'string'
364
- && path.resolve(record.source) === path.resolve(ownership.source);
365
- }
366
-
367
- function isOwnedRemovalPayload(
368
- root: string,
369
- tombstone: string,
370
- ownership: RemovalTombstoneOwnership
371
- ): boolean {
372
- const record = readRemovalTombstoneRecord(root, tombstone);
373
- if (!record || record.targetDigest !== ownership.targetDigest
374
- || record.permitDigest !== ownership.permitDigest
375
- || record.kind !== ownership.kind
376
- || path.resolve(record.source) !== path.resolve(ownership.source)) return false;
377
- try {
378
- const payload = removalTombstonePayload(tombstone);
379
- const payloadIdentity = removalSourceIdentity(payload);
380
- return sameRemovalSourceIdentity(payloadIdentity, record.sourceIdentity)
381
- && removalSourceClaimMatches(payload, ownership, record.sourceIdentity);
382
- } catch {
383
- return false;
384
- }
385
- }
386
-
387
- function assertOwnedRemovalTombstone(
388
- root: string,
389
- tombstone: string,
390
- ownership: RemovalTombstoneOwnership
391
- ): void {
392
- if (!isOwnedRemovalTombstone(root, tombstone, ownership)) {
393
- throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${ownership.source}`);
394
- }
395
- }
396
-
397
- function shouldStageManagedRemoval(source: string, tombstone: string, recovering: boolean): boolean {
398
- return fs.existsSync(source) || (recovering && fs.existsSync(tombstone));
399
- }
400
-
401
- function stageManagedRemoval(
402
- root: string,
403
- source: string,
404
- tombstone: string,
405
- recovering: boolean,
406
- ownership: RemovalTombstoneOwnership
407
- ): void {
408
- assertManagedPath(root, source);
409
- assertManagedPath(root, tombstone);
410
- const sourceExists = fs.existsSync(source);
411
- const tombstoneExists = fs.existsSync(tombstone);
412
- if (tombstoneExists) {
413
- assertOwnedRemovalTombstone(root, tombstone, ownership);
414
- if (sourceExists) {
415
- throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
416
- }
417
- if (recovering) {
418
- if (!isOwnedRemovalPayload(root, tombstone, ownership)) {
419
- throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
420
- }
421
- return;
422
- }
423
- throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
424
- }
425
- if (recovering) {
426
- throw new Error(`SANDBOX_CONTROL_REMOVAL_ACTION_OUTCOME_UNKNOWN: ${source}`);
427
- }
428
- if (!sourceExists) return;
429
- // The operator authorizes cleanup of this managed target after execution stops.
430
- // Path checks detect conflicts; they do not provide an atomic source-instance move.
431
- const expectedIdentity = removalSourceIdentity(source);
432
- const claimPath = removalSourceClaimPath(source, ownership);
433
- try {
434
- fs.writeFileSync(claimPath, `${JSON.stringify({ ...ownership, sourceIdentity: expectedIdentity })}\n`, {
435
- encoding: 'utf8',
436
- mode: 0o600,
437
- flag: 'wx'
438
- });
439
- } catch {
440
- throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
441
- }
442
- if (!sameRemovalSourceIdentity(expectedIdentity, removalSourceIdentity(source))) {
443
- removeOwnedSourceClaim(source, ownership);
444
- throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
445
- }
446
- let moved = false;
447
- try {
448
- fs.mkdirSync(tombstone, { mode: 0o700 });
449
- fs.writeFileSync(removalTombstoneMarker(tombstone), `${JSON.stringify({ ...ownership, sourceIdentity: expectedIdentity })}\n`, {
450
- encoding: 'utf8',
451
- mode: 0o600,
452
- flag: 'wx'
453
- });
454
- fs.renameSync(source, removalTombstonePayload(tombstone));
455
- moved = true;
456
- if (!isOwnedRemovalPayload(root, tombstone, ownership)) {
457
- throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
458
- }
459
- } catch (error) {
460
- if (!moved) removeOwnedSourceClaim(source, ownership);
461
- throw error;
462
- }
463
- }
464
-
465
- function cleanupManagedTombstones(
466
- root: string,
467
- targetDigest: string,
468
- permitDigest: string,
469
- kind: RemovalActionKind,
470
- sources: readonly string[]
471
- ): void {
472
- for (const source of sources) {
473
- const tombstone = removalTombstonePath(targetDigest, kind, source);
474
- if (!fs.existsSync(tombstone)) continue;
475
- const ownership = { version: 1, targetDigest, permitDigest, kind, source } as const;
476
- if (!isOwnedRemovalTombstone(root, tombstone, ownership)) continue;
477
- if (!isOwnedRemovalPayload(root, tombstone, ownership)) {
478
- throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
479
- }
480
- removeManagedDir(root, tombstone);
481
- }
482
- }
483
-
484
- function branchRemovalTombstoneRef(targetDigest: string): string {
485
- return `refs/agent-infra/sandbox-removal/${targetDigest}/branch`;
486
- }
487
-
488
- function branchPermitHead(
489
- branch: string,
490
- permits: ReadonlyMap<string, WorktreeRemovalPermit>
491
- ): string | null {
492
- const heads = new Set(
493
- [...permits.values()]
494
- .filter((permit) => permit.snapshot.branch === branch)
495
- .map((permit) => permit.snapshot.head)
496
- );
497
- return heads.size === 1 ? [...heads][0]! : null;
498
- }
499
-
500
- function stageBranchRemoval(
501
- config: SandboxConfig,
502
- branch: string,
503
- targetDigest: string,
504
- permits: ReadonlyMap<string, WorktreeRemovalPermit>,
505
- recovering: boolean
506
- ): void {
507
- const expectedHead = branchPermitHead(branch, permits);
508
- if (!expectedHead) throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
509
- const branchName = `refs/heads/${branch}`;
510
- const tombstone = branchRemovalTombstoneRef(targetDigest);
511
- const branchExists = runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', branchName]);
512
- const tombstoneExists = runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', tombstone]);
513
- if (branchExists && tombstoneExists) {
514
- throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
515
- }
516
- if (recovering) {
517
- if (branchExists && !tombstoneExists) {
518
- throw new Error(`SANDBOX_CONTROL_REMOVAL_ACTION_OUTCOME_UNKNOWN: ${branch}`);
519
- }
520
- if (tombstoneExists && runSafe('git', ['-C', config.repoRoot, 'rev-parse', tombstone]) !== expectedHead) {
521
- throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
522
- }
523
- return;
524
- }
525
- if (!branchExists) return;
526
- assertBranchRemovalIdentity(config, branch, permits);
527
- const actualHead = runSafe('git', ['-C', config.repoRoot, 'rev-parse', branchName]);
528
- if (!runOk('git', ['-C', config.repoRoot, 'update-ref', tombstone, actualHead, '0'.repeat(40)])) {
529
- throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
530
- }
531
- if (!runOk('git', ['-C', config.repoRoot, 'update-ref', '-d', branchName, actualHead])) {
532
- throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
533
- }
534
- }
535
-
536
- function cleanupBranchTombstone(
537
- config: SandboxConfig,
538
- branch: string,
539
- targetDigest: string,
540
- permits: ReadonlyMap<string, WorktreeRemovalPermit>
541
- ): void {
542
- const tombstone = branchRemovalTombstoneRef(targetDigest);
543
- if (!runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', tombstone])) return;
544
- const expectedHead = branchPermitHead(branch, permits);
545
- if (!expectedHead || runSafe('git', ['-C', config.repoRoot, 'rev-parse', tombstone]) !== expectedHead) {
546
- throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
547
- }
548
- if (!runOk('git', ['-C', config.repoRoot, 'update-ref', '-d', tombstone, expectedHead])) {
549
- throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
550
- }
551
- }
552
-
553
- function worktreeRegistration(
554
- config: SandboxConfig,
555
- worktree: string
556
- ): { head: string; branch: string | null } | null {
557
- const lines = runSafe('git', ['-C', config.repoRoot, 'worktree', 'list', '--porcelain']).split('\n');
558
- let current: { path: string; head: string; branch: string | null } | null = null;
559
- const flush = (): { head: string; branch: string | null } | null => {
560
- if (!current || path.resolve(current.path) !== path.resolve(worktree)) return null;
561
- return { head: current.head, branch: current.branch };
562
- };
563
- for (const line of lines) {
564
- if (line.startsWith('worktree ')) {
565
- const previous = flush();
566
- if (previous) return previous;
567
- current = { path: line.slice('worktree '.length), head: '', branch: null };
568
- } else if (current && line.startsWith('HEAD ')) {
569
- current.head = line.slice('HEAD '.length);
570
- } else if (current && line.startsWith('branch ')) {
571
- current.branch = line.slice('branch '.length);
572
- }
573
- }
574
- return flush();
575
- }
576
-
577
- function assertWorktreeRegistrationIdentity(
578
- config: SandboxConfig,
579
- worktree: string,
580
- permit: WorktreeRemovalPermit
581
- ): void {
582
- const registration = worktreeRegistration(config, worktree);
583
- if (!registration) return;
584
- if (registration.head !== permit.snapshot.head
585
- || registration.branch !== `refs/heads/${permit.snapshot.branch}`) {
586
- throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${worktree}`);
587
- }
588
- }
589
-
590
- function pruneWorktreeRegistration(config: SandboxConfig, worktree: string): void {
591
- runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
592
- if (worktreeRegistration(config, worktree)) {
593
- throw new Error(`SANDBOX_CONTROL_REMOVAL_ACTION_OUTCOME_UNKNOWN: ${worktree}`);
594
- }
595
- }
596
-
597
- function stageWorktreeRemoval(
598
- config: SandboxConfig,
599
- worktree: string,
600
- permit: WorktreeRemovalPermit,
601
- tombstone: string,
602
- recovering: boolean,
603
- ownership: RemovalTombstoneOwnership
604
- ): void {
605
- assertManagedPath(config.worktreeBase, worktree);
606
- assertManagedPath(config.worktreeBase, tombstone);
607
- const worktreeExists = fs.existsSync(worktree);
608
- const tombstoneExists = fs.existsSync(tombstone);
609
- if (worktreeExists || !recovering) verifyWorktreePermit(permit);
610
- if (recovering) {
611
- if (worktreeExists || !tombstoneExists) {
612
- if (worktreeExists && tombstoneExists) {
613
- throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${worktree}`);
614
- }
615
- throw new Error(`SANDBOX_CONTROL_REMOVAL_ACTION_OUTCOME_UNKNOWN: ${worktree}`);
616
- }
617
- assertOwnedRemovalTombstone(config.worktreeBase, tombstone, ownership);
618
- assertWorktreeRegistrationIdentity(config, worktree, permit);
619
- pruneWorktreeRegistration(config, worktree);
620
- return;
621
- }
622
- if (!worktreeExists) return;
623
- stageManagedRemoval(config.worktreeBase, worktree, tombstone, false, ownership);
624
- pruneWorktreeRegistration(config, worktree);
625
- }
626
-
627
- function cleanupCompletedRemovalArtifacts(
628
- config: SandboxConfig,
629
- target: RmTarget,
630
- committedTarget: SandboxRemovalTargetCommit,
631
- targetDigest: string
632
- ): void {
633
- const journals = listSandboxRemovalJournals({
634
- branch: target.effectiveBranch,
635
- project: config.project,
636
- targetDigest
637
- });
638
- if (journals.length === 0) return;
639
- const completed = (phase: SandboxRemovalJournal['phase']): boolean => journals.every((journal) => (
640
- sandboxRemovalPhaseIndex(journal.phase) >= sandboxRemovalPhaseIndex(phase)
641
- ));
642
- if (completed('workspace-removed')) {
643
- cleanupManagedTombstones(
644
- config.workspaceViewBase, targetDigest, committedTarget.permitDigest, 'workspace', target.workspaceViewRoots
645
- );
646
- cleanupManagedTombstones(
647
- path.join(config.controlBase, config.project), targetDigest, committedTarget.permitDigest, 'workspace', target.controlRoots
648
- );
649
- cleanupManagedTombstones(
650
- config.worktreeBase, targetDigest, committedTarget.permitDigest, 'worktree', committedTarget.worktreePaths
651
- );
652
- }
653
- if (completed('branch-removed')) {
654
- cleanupBranchTombstone(config, target.effectiveBranch, targetDigest, new Map(
655
- committedTarget.permits.map((permit) => [path.resolve(permit.path), {
656
- mode: permit.mode,
657
- snapshot: permit.snapshot
658
- }])
659
- ));
660
- }
661
- if (completed('tool-removed')) {
662
- cleanupManagedTombstones(config.home, targetDigest, committedTarget.permitDigest, 'tool', committedTarget.toolPaths);
663
- }
664
- if (completed('shell-removed')) {
665
- cleanupManagedTombstones(config.shellConfigBase, targetDigest, committedTarget.permitDigest, 'shell', committedTarget.shellPaths);
666
- }
667
- if (completed('share-removed')) {
668
- cleanupManagedTombstones(config.shareBase, targetDigest, committedTarget.permitDigest, 'share', [committedTarget.sharePath]);
669
- }
670
- }
671
-
672
- function removalActionPaths(target: RmTarget, config: SandboxConfig, phase: SandboxRemovalJournal['phase']): string[] {
673
- if (phase === 'workspace-removed') {
674
- return [...target.workspaceViewRoots, ...target.controlRoots];
675
- }
676
- if (phase === 'tool-removed') {
677
- return target.toolCandidates.flatMap(({ candidates }) => candidates);
678
- }
679
- if (phase === 'shell-removed') {
680
- return shellConfigDirCandidates(config, target.effectiveBranch);
681
- }
682
- if (phase === 'share-removed') {
683
- return [shareBranchDir(config, target.effectiveBranch)];
684
- }
685
- return [];
686
- }
687
-
688
- function permitsFromJournal(
689
- journals: readonly SandboxRemovalJournal[],
690
- target: RmTarget,
691
- expectedTargetDigest: string
692
- ): Map<string, WorktreeRemovalPermit> {
693
- const permits = new Map<string, WorktreeRemovalPermit>();
694
- for (const journal of journals) {
695
- if (journal.target.targetDigest !== expectedTargetDigest) continue;
696
- if (journal.target.branch !== target.effectiveBranch) {
697
- throw new Error('SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH');
698
- }
699
- for (const entry of journal.target.permits) {
700
- if (path.resolve(entry.path) !== path.resolve(entry.snapshot.worktree)) {
701
- throw new Error('SANDBOX_CONTROL_REMOVAL_PERMIT_MISMATCH');
702
- }
703
- permits.set(path.resolve(entry.path), {
704
- mode: entry.mode,
705
- snapshot: entry.snapshot
706
- });
707
- }
708
- }
709
- return permits;
710
- }
711
-
712
- export function sandboxManagedPathKey(
713
- candidate: string,
714
- platform: NodeJS.Platform = process.platform,
715
- resolveExistingPath: (resolved: string) => string = (resolved) => fs.realpathSync.native(resolved)
716
- ): string {
717
- const resolved = path.resolve(candidate);
718
- let identity = resolved;
719
- try {
720
- identity = resolveExistingPath(resolved);
721
- } catch (error) {
722
- if (!isMissingPathError(error)) {
723
- throw new Error(
724
- `SANDBOX_CLEANUP_BATCH_PREFLIGHT_FAILED: unable to determine managed path identity for '${resolved}'`
725
- );
726
- }
727
- }
728
- return platform === 'win32' ? identity.toLowerCase() : identity;
729
- }
730
-
731
113
  type RmTarget = {
732
114
  branch: string;
733
115
  effectiveBranch: string;
@@ -753,38 +135,6 @@ export type SandboxRemovalResourceDisclosure = Readonly<{
753
135
  preserve: readonly SandboxRemovalResource[];
754
136
  }>;
755
137
 
756
- type RemovalDisclosureTarget = Readonly<Pick<
757
- RmTarget,
758
- 'branch' | 'effectiveBranch' | 'existingWorktrees' | 'toolCandidates' | 'controlRoots' | 'workspaceViewRoots'
759
- >>;
760
-
761
- type RemovalSelection = Pick<SandboxRemovalTargetCommit, 'removeWorktree' | 'removeBranch' | 'removeShare'>;
762
-
763
- type RemovalActionKind = 'workspace' | 'worktree' | 'tool' | 'shell' | 'share';
764
-
765
- type RemovalActionPreparation = Readonly<{
766
- run: boolean;
767
- recovering: boolean;
768
- }>;
769
-
770
- type CleanupCandidate = Readonly<{
771
- row: SandboxRow;
772
- cleanupTarget: SandboxCleanupTarget;
773
- }>;
774
-
775
- type ProtectedCleanupCandidate = Readonly<{
776
- row: SandboxRow;
777
- branch: string;
778
- identity: string;
779
- reason: string;
780
- }>;
781
-
782
- type CleanupGroup = Readonly<{
783
- candidates: readonly CleanupCandidate[];
784
- cleanupTarget: SandboxCleanupTarget;
785
- target: RmTarget;
786
- }>;
787
-
788
138
  type RmOneOptions = {
789
139
  assumeYes?: boolean;
790
140
  interactive?: boolean;
@@ -1011,7 +361,25 @@ async function removeUncheckedSandbox(
1011
361
  fs.rmSync(root, { recursive: true, force: true });
1012
362
  removeEmptyManagedParent(path.join(config.workspaceViewBase, config.project), root);
1013
363
  }
1014
- for (const candidate of toolCandidates.flatMap(({ candidates }) => candidates)) fs.rmSync(candidate, { recursive: true, force: true });
364
+ const toolRemovalFailures: string[] = [];
365
+ for (const candidate of toolCandidates.flatMap(({ candidates }) => candidates)) {
366
+ try {
367
+ fs.rmSync(candidate, { recursive: true, force: true });
368
+ continue;
369
+ } catch (initialError) {
370
+ if (isPermissionRemovalError(initialError)) {
371
+ try {
372
+ addOwnerWriteToDirectories(candidate);
373
+ fs.rmSync(candidate, { recursive: true, force: true });
374
+ continue;
375
+ } catch (retryError) {
376
+ toolRemovalFailures.push(`${candidate}: ${removalErrorMessage(initialError)}; retry failed: ${removalErrorMessage(retryError)}`);
377
+ }
378
+ } else {
379
+ toolRemovalFailures.push(`${candidate}: ${removalErrorMessage(initialError)}`);
380
+ }
381
+ }
382
+ }
1015
383
  for (const shell of shellConfigDirCandidates(config, effectiveBranch)) fs.rmSync(shell, { recursive: true, force: true });
1016
384
  if (shouldRemoveShare) fs.rmSync(sharePath, { recursive: true, force: true });
1017
385
  if (shouldRemoveWorktree) {
@@ -1025,6 +393,9 @@ async function removeUncheckedSandbox(
1025
393
  assertBranchRemovalIdentity(config, effectiveBranch, worktreePermits);
1026
394
  runSafe('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch]);
1027
395
  }
396
+ if (toolRemovalFailures.length > 0) {
397
+ throw new Error(`SANDBOX_TOOL_CLEANUP_INCOMPLETE:\n${toolRemovalFailures.map((failure) => ` - ${failure}`).join('\n')}`);
398
+ }
1028
399
  releaseTaskBoundSandboxIdentity(config.repoRoot, target.workspace);
1029
400
 
1030
401
  if (!options.quiet) p.outro('Sandbox removed');
@@ -1191,7 +562,7 @@ async function rmUnbound(
1191
562
  p.outro(`Dry run: ${targets.length} sandbox(es) found, nothing deleted`);
1192
563
  return;
1193
564
  }
1194
- for (const { row, cleanupTarget, target } of targets) {
565
+ for (const { cleanupTarget, target } of targets) {
1195
566
  await runRmOne(config, tools, cleanupTarget.branch, {
1196
567
  assumeYes: options.assumeYes,
1197
568
  target,