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