@fitlab-ai/agent-infra 0.9.5 → 0.9.7

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 (172) hide show
  1. package/dist/bin/internal-cli.js +8 -3
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
  8. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  9. package/dist/lib/defaults.json +10 -2
  10. package/dist/lib/git/workflow.js +30 -4
  11. package/dist/lib/internal/codex-lifecycle.js +169 -3
  12. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  13. package/dist/lib/internal/release-workflow.js +108 -11
  14. package/dist/lib/internal/sandbox-control.js +43 -6
  15. package/dist/lib/internal/task-create.js +12 -4
  16. package/dist/lib/internal/task-orchestration.js +40 -7
  17. package/dist/lib/platform/context.js +6 -4
  18. package/dist/lib/platform/github-client.js +55 -2
  19. package/dist/lib/platform/issue-comments.js +2 -1
  20. package/dist/lib/process-data/checkpoint.js +319 -0
  21. package/dist/lib/process-data/index.js +128 -60
  22. package/dist/lib/process-data/materialize.js +84 -0
  23. package/dist/lib/process-data/normalize.js +34 -1
  24. package/dist/lib/process-data/reconcile.js +55 -1
  25. package/dist/lib/process-data/sources.js +422 -1
  26. package/dist/lib/process-data/store.js +341 -11
  27. package/dist/lib/release/workflow.js +29 -21
  28. package/dist/lib/sandbox/commands/create.js +111 -60
  29. package/dist/lib/sandbox/commands/prune.js +40 -16
  30. package/dist/lib/sandbox/commands/rm.js +372 -94
  31. package/dist/lib/sandbox/commands/start.js +4 -3
  32. package/dist/lib/sandbox/control/client.js +138 -22
  33. package/dist/lib/sandbox/control/container-identity.js +64 -0
  34. package/dist/lib/sandbox/control/executor.js +142 -0
  35. package/dist/lib/sandbox/control/lifecycle.js +557 -0
  36. package/dist/lib/sandbox/control/protocol.js +69 -26
  37. package/dist/lib/sandbox/control/server.js +395 -99
  38. package/dist/lib/sandbox/control/state.js +191 -0
  39. package/dist/lib/sandbox/control/timing.js +8 -0
  40. package/dist/lib/sandbox/index.js +2 -2
  41. package/dist/lib/sandbox/managed-fs.js +14 -2
  42. package/dist/lib/sandbox/mounts.js +22 -19
  43. package/dist/lib/sandbox/recovery.js +284 -50
  44. package/dist/lib/sandbox/workspace-view.js +90 -10
  45. package/dist/lib/sandbox/worktree-safety.js +196 -0
  46. package/dist/lib/server/daemon.js +1 -1
  47. package/dist/lib/server/process-control.js +17 -1
  48. package/dist/lib/server/process-state.js +57 -20
  49. package/dist/lib/task/artifact-lifecycle.js +87 -15
  50. package/dist/lib/task/artifact-receipts.js +128 -0
  51. package/dist/lib/task/codex-orchestration.js +236 -0
  52. package/dist/lib/task/commands/status.js +3 -0
  53. package/dist/lib/task/commands/validate.js +289 -0
  54. package/dist/lib/task/commit-finalization.js +36 -9
  55. package/dist/lib/task/create.js +10 -2
  56. package/dist/lib/task/delegation-receipts.js +109 -2
  57. package/dist/lib/task/events.js +149 -6
  58. package/dist/lib/task/finalization-artifacts.js +2 -1
  59. package/dist/lib/task/implementation-inputs.js +1 -7
  60. package/dist/lib/task/index.js +9 -0
  61. package/dist/lib/task/ledger.js +13 -1
  62. package/dist/lib/task/orchestration.js +365 -15
  63. package/dist/lib/task/sections.js +5 -1
  64. package/dist/lib/task/task-execution-lock.js +5 -4
  65. package/dist/lib/task/verification-engine.js +78 -14
  66. package/dist/lib/task/verification.js +1 -0
  67. package/dist/lifecycle-build-manifest.json +6 -0
  68. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
  69. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  70. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
  71. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
  72. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  73. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
  74. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
  75. package/lib/agent-clients/adapters/codex.ts +1 -1
  76. package/lib/defaults.json +10 -2
  77. package/lib/git/workflow.ts +25 -3
  78. package/lib/internal/codex-lifecycle.ts +175 -4
  79. package/lib/internal/codex-sandbox-controller.ts +109 -0
  80. package/lib/internal/release-workflow.ts +107 -11
  81. package/lib/internal/sandbox-control.ts +38 -6
  82. package/lib/internal/task-create.ts +16 -4
  83. package/lib/internal/task-orchestration.ts +34 -7
  84. package/lib/platform/context.ts +8 -5
  85. package/lib/platform/github-client.ts +61 -3
  86. package/lib/platform/issue-comments.ts +2 -1
  87. package/lib/process-data/checkpoint.ts +335 -0
  88. package/lib/process-data/index.ts +112 -60
  89. package/lib/process-data/materialize.ts +83 -0
  90. package/lib/process-data/normalize.ts +29 -1
  91. package/lib/process-data/reconcile.ts +64 -2
  92. package/lib/process-data/sources.ts +449 -2
  93. package/lib/process-data/store.ts +304 -11
  94. package/lib/process-data/types.ts +124 -2
  95. package/lib/release/workflow.ts +69 -20
  96. package/lib/sandbox/commands/create.ts +70 -25
  97. package/lib/sandbox/commands/prune.ts +53 -15
  98. package/lib/sandbox/commands/rm.ts +423 -108
  99. package/lib/sandbox/commands/start.ts +4 -3
  100. package/lib/sandbox/control/client.ts +152 -28
  101. package/lib/sandbox/control/container-identity.ts +93 -0
  102. package/lib/sandbox/control/executor.ts +167 -0
  103. package/lib/sandbox/control/lifecycle.ts +573 -0
  104. package/lib/sandbox/control/protocol.ts +121 -68
  105. package/lib/sandbox/control/server.ts +398 -109
  106. package/lib/sandbox/control/state.ts +213 -0
  107. package/lib/sandbox/control/timing.ts +12 -0
  108. package/lib/sandbox/index.ts +2 -2
  109. package/lib/sandbox/managed-fs.ts +17 -2
  110. package/lib/sandbox/mounts.ts +16 -8
  111. package/lib/sandbox/recovery.ts +335 -65
  112. package/lib/sandbox/workspace-view.ts +102 -11
  113. package/lib/sandbox/worktree-safety.ts +234 -0
  114. package/lib/server/daemon.ts +1 -1
  115. package/lib/server/process-control.ts +24 -3
  116. package/lib/server/process-state.ts +82 -27
  117. package/lib/task/artifact-lifecycle.ts +78 -14
  118. package/lib/task/artifact-receipts.ts +161 -0
  119. package/lib/task/codex-orchestration.ts +319 -0
  120. package/lib/task/commands/status.ts +2 -0
  121. package/lib/task/commands/validate.ts +282 -0
  122. package/lib/task/commit-finalization.ts +52 -9
  123. package/lib/task/create.ts +11 -2
  124. package/lib/task/delegation-receipts.ts +208 -7
  125. package/lib/task/events.ts +131 -5
  126. package/lib/task/finalization-artifacts.ts +2 -1
  127. package/lib/task/implementation-inputs.ts +1 -5
  128. package/lib/task/index.ts +8 -0
  129. package/lib/task/ledger.ts +16 -1
  130. package/lib/task/orchestration.ts +552 -18
  131. package/lib/task/sections.ts +6 -2
  132. package/lib/task/task-execution-lock.ts +11 -9
  133. package/lib/task/verification-engine.ts +93 -24
  134. package/lib/task/verification.ts +2 -1
  135. package/package.json +1 -1
  136. package/templates/.agents/README.en.md +7 -0
  137. package/templates/.agents/README.zh-CN.md +5 -0
  138. package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
  139. package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
  140. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
  141. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
  142. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
  143. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  144. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  145. package/templates/.agents/rules/review-handshake.en.md +3 -1
  146. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  147. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  148. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  149. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  150. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  151. package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
  152. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
  153. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
  154. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
  155. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
  156. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
  157. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  158. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  159. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
  160. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
  161. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  162. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  163. package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
  164. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
  165. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  166. package/templates/.claude/commands/run-manual-validation.en.md +9 -0
  167. package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
  168. package/templates/.codex/hooks.json +8 -8
  169. package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
  170. package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
  171. package/templates/.agents/workspace/README.en.md +0 -26
  172. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -2,9 +2,8 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { createHash } from 'node:crypto';
5
- import { execFileSync, spawn } from 'node:child_process';
5
+ import { execFileSync } from 'node:child_process';
6
6
  import { parseArgs } from 'node:util';
7
- import { fileURLToPath } from 'node:url';
8
7
  import * as p from '@clack/prompts';
9
8
  import pc from 'picocolors';
10
9
  import { listAgentClientAdapters } from '../../agent-clients/registry.js';
@@ -17,10 +16,10 @@ import { commandForEngine, execEngine, run, runEngine, runOk, runOkEngine, runSa
17
16
  import { parseSandboxWorkspaceIdentity, resolveSandboxTarget, sameSandboxWorkspaceIdentity } from '../workspace-identity.js';
18
17
  import { createSandboxCapabilityPlan, runSandboxHooks, runBoundedSandboxHookCommand } from '../agent-client-reconciler.js';
19
18
  import { resolveTools, tmpfsSeedStagingPath, tmpfsSeedTargetPath, toolConfigDirCandidates } from '../tools.js';
20
- import { assertFreshSandboxReady, hydrateTmpfsSeedEntries, prepareTmpfsMounts } from '../recovery.js';
19
+ import { assertFreshSandboxReady, hydrateTmpfsSeedEntries, prepareTmpfsMounts, startSandboxControlBroker } from '../recovery.js';
21
20
  import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.js';
22
21
  import { sandboxCoreBindMounts } from '../mounts.js';
23
- import { assertSandboxTaskSource, materializeSandboxControl, materializeSandboxWorkspaceView } from '../workspace-view.js';
22
+ import { assertSandboxTaskSource, finalizeSandboxControlManifest, materializeSandboxControl, materializeSandboxWorkspaceView, prepareSandboxWorkspaceMountTarget, } from '../workspace-view.js';
24
23
  import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.js';
25
24
  import { validateSelinuxDisableEnv } from '../engines/selinux.js';
26
25
  import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.js';
@@ -65,11 +64,6 @@ const MANAGED_GITHUB_GIT_CONFIG = [
65
64
  '[url "https://github.com/"]',
66
65
  '\tinsteadOf = git@github.com:'
67
66
  ];
68
- function internalCliPath() {
69
- const directory = path.dirname(fileURLToPath(import.meta.url));
70
- const extension = path.extname(fileURLToPath(import.meta.url));
71
- return path.resolve(directory, '..', '..', '..', 'bin', `internal-cli${extension}`);
72
- }
73
67
  function resolveToolDirs(config, tools, branch) {
74
68
  return tools.map((tool) => {
75
69
  const candidates = toolConfigDirCandidates(tool, config.project, branch);
@@ -895,8 +889,12 @@ export async function create(args) {
895
889
  const requestedDescription = target.workspace.mode === 'task-bound'
896
890
  ? `task-bound:${target.workspace.taskId}`
897
891
  : target.workspace.mode;
892
+ const recoveryTarget = target.workspace.mode === 'task-bound'
893
+ ? target.workspace.taskId
894
+ : branch;
898
895
  throw new Error(`SANDBOX_WORKSPACE_IDENTITY_CONFLICT: container '${name}' is ${existingDescription}, `
899
- + `but this request is ${requestedDescription}. Run 'ai sandbox rm ${branch}' and retry.`);
896
+ + `but this request is ${requestedDescription}. `
897
+ + `Run 'ai sandbox start --recreate ${recoveryTarget}' on the host, then retry.`);
900
898
  }
901
899
  }
902
900
  message('Removing old container instance...');
@@ -962,6 +960,7 @@ export async function create(args) {
962
960
  container,
963
961
  identity: target.workspace
964
962
  });
963
+ prepareSandboxWorkspaceMountTarget(worktree);
965
964
  const control = materializeSandboxControl({
966
965
  base: effectiveConfig.controlBase,
967
966
  repoRoot: effectiveConfig.repoRoot,
@@ -969,7 +968,8 @@ export async function create(args) {
969
968
  project: effectiveConfig.project,
970
969
  container,
971
970
  branch,
972
- identity: target.workspace
971
+ identity: target.workspace,
972
+ engine
973
973
  });
974
974
  hostShellConfig = prepareHostShellConfig({
975
975
  home: effectiveConfig.home,
@@ -982,6 +982,7 @@ export async function create(args) {
982
982
  shellConfigHostDir: hostShellConfig.hostDir,
983
983
  workspaceViewRoot: workspaceView.root,
984
984
  controlDir: control.channelDir,
985
+ controlStatusDir: control.statusDir,
985
986
  ...(target.workspace.mode === 'task-bound'
986
987
  ? {
987
988
  taskSource: assertSandboxTaskSource(effectiveConfig.repoRoot, target.workspace.taskId),
@@ -1028,55 +1029,105 @@ export async function create(args) {
1028
1029
  : [];
1029
1030
  const hostTz = detectHostTimezone();
1030
1031
  const tzFlags = hostTz ? ['-e', `TZ=${hostTz}`] : [];
1031
- runEngineTaskCommand(engine, 'docker', [
1032
- 'run',
1033
- '-d',
1034
- '--name',
1035
- container,
1036
- '--hostname',
1037
- `${effectiveConfig.project}-sandbox`,
1038
- '--label',
1039
- sandboxLabel(effectiveConfig),
1040
- '--label',
1041
- `${sandboxBranchLabel(effectiveConfig)}=${branch}`,
1042
- '--label',
1043
- `${sandboxWorkspaceModeLabel(effectiveConfig)}=${target.workspace.mode}`,
1044
- ...(target.workspace.mode === 'task-bound'
1045
- ? [
1046
- '--label',
1047
- `${sandboxTaskIdLabel(effectiveConfig)}=${target.workspace.taskId}`,
1048
- '-e',
1049
- `AGENT_INFRA_TASK_ID=${target.workspace.taskId}`
1050
- ]
1051
- : []),
1052
- '-e',
1053
- `AGENT_INFRA_CONTROL_TOKEN=${control.token}`,
1054
- '-e',
1055
- 'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
1056
- '--label',
1057
- `${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
1058
- ...coreVolumes,
1059
- ...buildClipboardVolumeArgs(engine, effectiveConfig.home),
1060
- '-v',
1061
- volumeArg(engine, path.join(effectiveConfig.repoRoot, '.git'), `${toEnginePath(engine, effectiveConfig.repoRoot)}/.git`),
1062
- ...dotfilesMount,
1063
- ...toolVolumes,
1064
- ...tmpfsArgs,
1065
- ...tmpfsSeedPlan.flatMap(({ volumeArgs }) => volumeArgs),
1066
- ...liveMountVolumes,
1067
- ...envFile.dockerArgs,
1068
- ...tzFlags,
1069
- '-w',
1070
- '/workspace',
1071
- effectiveConfig.imageName
1072
- ]);
1073
- const broker = spawn(process.execPath, [internalCliPath(), 'sandbox-control', 'serve', '--manifest', control.manifestPath], { cwd: effectiveConfig.repoRoot, detached: true, stdio: 'ignore' });
1074
- broker.once('error', () => {
1075
- // Fresh readiness verification below remains the source of truth;
1076
- // consume detached spawn errors so they cannot crash the command.
1077
- });
1078
- broker.unref();
1079
- createdTmpfsSeedPlan = tmpfsSeedPlan;
1032
+ let createdContainerRef = null;
1033
+ try {
1034
+ const dockerRunId = runEngineTaskCommand(engine, 'docker', [
1035
+ 'run',
1036
+ '-d',
1037
+ '--init',
1038
+ '--name',
1039
+ container,
1040
+ '--hostname',
1041
+ `${effectiveConfig.project}-sandbox`,
1042
+ '--label',
1043
+ sandboxLabel(effectiveConfig),
1044
+ '--label',
1045
+ `${sandboxBranchLabel(effectiveConfig)}=${branch}`,
1046
+ '--label',
1047
+ `${sandboxWorkspaceModeLabel(effectiveConfig)}=${target.workspace.mode}`,
1048
+ ...(target.workspace.mode === 'task-bound'
1049
+ ? [
1050
+ '--label',
1051
+ `${sandboxTaskIdLabel(effectiveConfig)}=${target.workspace.taskId}`,
1052
+ '-e',
1053
+ `AGENT_INFRA_TASK_ID=${target.workspace.taskId}`
1054
+ ]
1055
+ : []),
1056
+ '-e',
1057
+ `AGENT_INFRA_CONTROL_TOKEN=${control.token}`,
1058
+ '-e',
1059
+ `AGENT_INFRA_CONTROL_GENERATION=${control.generation}`,
1060
+ '-e',
1061
+ 'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
1062
+ '-e',
1063
+ 'AGENT_INFRA_CONTROL_STATUS_DIR=/run/agent-infra/control-status',
1064
+ '--label',
1065
+ `${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
1066
+ ...coreVolumes,
1067
+ ...buildClipboardVolumeArgs(engine, effectiveConfig.home),
1068
+ '-v',
1069
+ volumeArg(engine, path.join(effectiveConfig.repoRoot, '.git'), `${toEnginePath(engine, effectiveConfig.repoRoot)}/.git`),
1070
+ ...dotfilesMount,
1071
+ ...toolVolumes,
1072
+ ...tmpfsArgs,
1073
+ ...tmpfsSeedPlan.flatMap(({ volumeArgs }) => volumeArgs),
1074
+ ...liveMountVolumes,
1075
+ ...envFile.dockerArgs,
1076
+ ...tzFlags,
1077
+ '-w',
1078
+ '/workspace',
1079
+ effectiveConfig.imageName
1080
+ ]);
1081
+ createdContainerRef = dockerRunId.trim();
1082
+ const containerId = runEngineTaskCommand(engine, 'docker', [
1083
+ 'inspect', '--format', '{{.Id}}', dockerRunId.trim()
1084
+ ]).trim();
1085
+ if (!containerId)
1086
+ throw new Error('SANDBOX_CONTROL_CONTAINER_ID_INVALID');
1087
+ const rawContainerLabels = runEngineTaskCommand(engine, 'docker', [
1088
+ 'inspect', '--format', '{{json .Config.Labels}}', containerId
1089
+ ]).trim();
1090
+ let inspectedLabels;
1091
+ try {
1092
+ inspectedLabels = JSON.parse(rawContainerLabels);
1093
+ }
1094
+ catch {
1095
+ throw new Error('SANDBOX_CONTROL_CONTAINER_LABELS_INVALID');
1096
+ }
1097
+ if (!inspectedLabels || typeof inspectedLabels !== 'object' || Array.isArray(inspectedLabels)) {
1098
+ throw new Error('SANDBOX_CONTROL_CONTAINER_LABELS_INVALID');
1099
+ }
1100
+ const expectedLabels = {
1101
+ [sandboxLabel(effectiveConfig)]: '',
1102
+ [sandboxBranchLabel(effectiveConfig)]: branch,
1103
+ [sandboxWorkspaceModeLabel(effectiveConfig)]: target.workspace.mode,
1104
+ [sandboxRuntimeCapabilityLabel(effectiveConfig)]: capabilityPlan.runtimeSignature,
1105
+ ...(target.workspace.mode === 'task-bound'
1106
+ ? { [sandboxTaskIdLabel(effectiveConfig)]: target.workspace.taskId }
1107
+ : {})
1108
+ };
1109
+ const labels = inspectedLabels;
1110
+ for (const [key, expected] of Object.entries(expectedLabels)) {
1111
+ if (labels[key] !== expected)
1112
+ throw new Error('SANDBOX_CONTROL_CONTAINER_IDENTITY_MISMATCH');
1113
+ }
1114
+ finalizeSandboxControlManifest(control, {
1115
+ engine,
1116
+ id: containerId,
1117
+ labels: expectedLabels
1118
+ });
1119
+ await startSandboxControlBroker(effectiveConfig.repoRoot, control.manifestPath);
1120
+ createdTmpfsSeedPlan = tmpfsSeedPlan;
1121
+ }
1122
+ catch (error) {
1123
+ if (createdContainerRef) {
1124
+ runSafeEngine(engine, 'docker', ['stop', createdContainerRef]);
1125
+ runSafeEngine(engine, 'docker', ['rm', createdContainerRef]);
1126
+ }
1127
+ removeDirRecursive(control.root);
1128
+ removeDirRecursive(workspaceView.root);
1129
+ throw error;
1130
+ }
1080
1131
  }
1081
1132
  finally {
1082
1133
  envFile.cleanup();
@@ -5,12 +5,13 @@ import * as p from '@clack/prompts';
5
5
  import pc from 'picocolors';
6
6
  import { loadConfig } from '../config.js';
7
7
  import { safeNameCandidates, sandboxBranchLabel, sandboxLabel } from '../constants.js';
8
- import { detectEngine } from '../engine.js';
8
+ import { detectEngine, ENGINES } from '../engine.js';
9
9
  import { hostJoin } from '../engines/wsl2-paths.js';
10
10
  import { removeManagedDir, removeWorktreeDir } from '../managed-fs.js';
11
11
  import { parseLabels } from './ls.js';
12
12
  import { runEngine, runSafe } from '../shell.js';
13
13
  import { createSandboxCapabilityPlan } from '../agent-client-reconciler.js';
14
+ import { createCleanPermit, formatWorktreeSnapshot, inspectWorktrees } from '../worktree-safety.js';
14
15
  const USAGE = `Usage: ai sandbox prune [--dry-run]`;
15
16
  function listChildDirs(base) {
16
17
  if (!fs.existsSync(base)) {
@@ -83,12 +84,15 @@ export function collectOrphanGroups(config, tools, activeBranches) {
83
84
  }
84
85
  return groups;
85
86
  }
86
- export function removeOrphanGroups(config, groups) {
87
+ export function removeOrphanGroups(config, groups, permits, allowRegisteredPathFallback = false) {
87
88
  let removedWorktrees = false;
88
89
  for (const group of groups) {
89
90
  for (const dir of group.dirs) {
90
91
  if (group.kind === 'worktree') {
91
- removeWorktreeDir(config.repoRoot, group.base, dir);
92
+ const permit = permits.get(path.resolve(dir));
93
+ if (!permit)
94
+ throw new Error(`Missing worktree removal permit: ${dir}`);
95
+ removeWorktreeDir(config.repoRoot, group.base, dir, permit, { allowRegisteredPathFallback });
92
96
  removedWorktrees = true;
93
97
  }
94
98
  else {
@@ -115,7 +119,7 @@ function writeGroups(groups) {
115
119
  }
116
120
  }
117
121
  }
118
- export async function prune(args) {
122
+ export async function prune(args, dependencies = {}) {
119
123
  const { values } = parseArgs({
120
124
  args,
121
125
  strict: true,
@@ -128,9 +132,9 @@ export async function prune(args) {
128
132
  process.stdout.write(`${USAGE}\n`);
129
133
  return;
130
134
  }
131
- const config = loadConfig();
132
- const tools = [...createSandboxCapabilityPlan(config).cleanupInventory];
133
- const engine = detectEngine(config);
135
+ const config = dependencies.config ?? loadConfig();
136
+ const tools = dependencies.tools ?? [...createSandboxCapabilityPlan(config).cleanupInventory];
137
+ const engine = dependencies.engine ?? detectEngine(config);
134
138
  const psArgs = [
135
139
  'ps',
136
140
  '-a',
@@ -139,15 +143,27 @@ export async function prune(args) {
139
143
  '--format',
140
144
  '{{.Labels}}'
141
145
  ];
142
- let labelsOutput;
143
- try {
144
- labelsOutput = runEngine(engine, 'docker', psArgs);
145
- }
146
- catch {
147
- throw new Error('Unable to determine active sandbox branches: docker ps failed');
146
+ let labelsOutput = dependencies.labelsOutput;
147
+ if (labelsOutput === undefined) {
148
+ try {
149
+ labelsOutput = runEngine(engine, 'docker', psArgs);
150
+ }
151
+ catch {
152
+ throw new Error('Unable to determine active sandbox branches: docker ps failed');
153
+ }
148
154
  }
155
+ const confirm = dependencies.confirm ?? p.confirm;
156
+ const isCancel = dependencies.isCancel ?? p.isCancel;
149
157
  const groups = collectOrphanGroups(config, tools, activeBranchesFromLabels(config, labelsOutput));
150
158
  const count = orphanCount(groups);
159
+ const worktrees = groups.filter((group) => group.kind === 'worktree').flatMap((group) => group.dirs);
160
+ const inspections = inspectWorktrees(worktrees);
161
+ const blockers = inspections.filter((inspection) => inspection.status !== 'clean');
162
+ const permits = new Map();
163
+ for (const inspection of inspections) {
164
+ if (inspection.status === 'clean')
165
+ permits.set(inspection.snapshot.worktree, createCleanPermit(inspection.snapshot));
166
+ }
151
167
  p.intro(pc.cyan(`Pruning orphaned sandbox state for ${config.project}`));
152
168
  if (count === 0) {
153
169
  p.log.success('No orphaned sandbox state dirs found');
@@ -155,19 +171,27 @@ export async function prune(args) {
155
171
  return;
156
172
  }
157
173
  writeGroups(groups);
174
+ for (const blocker of blockers) {
175
+ p.log.error(blocker.status === 'failed'
176
+ ? `${JSON.stringify(blocker.worktree)}: ${blocker.message}`
177
+ : formatWorktreeSnapshot(blocker.snapshot));
178
+ }
158
179
  if (values['dry-run']) {
159
180
  p.outro(pc.green('Dry run complete'));
160
181
  return;
161
182
  }
162
- const shouldRemove = await p.confirm({
183
+ if (blockers.length > 0) {
184
+ throw new Error('Refusing to prune because worktree preflight found blocker(s); no orphan state was deleted');
185
+ }
186
+ const shouldRemove = await confirm({
163
187
  message: `Remove ${count} orphaned sandbox state dirs?`,
164
188
  initialValue: true
165
189
  });
166
- if (p.isCancel(shouldRemove) || !shouldRemove) {
190
+ if (isCancel(shouldRemove) || !shouldRemove) {
167
191
  p.outro('Cancelled');
168
192
  return;
169
193
  }
170
- const removedWorktrees = removeOrphanGroups(config, groups);
194
+ const removedWorktrees = removeOrphanGroups(config, groups, permits, engine === ENGINES.WSL2);
171
195
  if (removedWorktrees) {
172
196
  runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
173
197
  }