@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.
- package/dist/bin/internal-cli.js +8 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -4
- package/dist/lib/internal/codex-lifecycle.js +169 -3
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +12 -4
- package/dist/lib/internal/task-orchestration.js +40 -7
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/process-data/checkpoint.js +319 -0
- package/dist/lib/process-data/index.js +128 -60
- package/dist/lib/process-data/materialize.js +84 -0
- package/dist/lib/process-data/normalize.js +34 -1
- package/dist/lib/process-data/reconcile.js +55 -1
- package/dist/lib/process-data/sources.js +422 -1
- package/dist/lib/process-data/store.js +341 -11
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +111 -60
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +372 -94
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/client.js +138 -22
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +142 -0
- package/dist/lib/sandbox/control/lifecycle.js +557 -0
- package/dist/lib/sandbox/control/protocol.js +69 -26
- package/dist/lib/sandbox/control/server.js +395 -99
- package/dist/lib/sandbox/control/state.js +191 -0
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +22 -19
- package/dist/lib/sandbox/recovery.js +284 -50
- package/dist/lib/sandbox/workspace-view.js +90 -10
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +17 -1
- package/dist/lib/server/process-state.js +57 -20
- package/dist/lib/task/artifact-lifecycle.js +87 -15
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +236 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +36 -9
- package/dist/lib/task/create.js +10 -2
- package/dist/lib/task/delegation-receipts.js +109 -2
- package/dist/lib/task/events.js +149 -6
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/ledger.js +13 -1
- package/dist/lib/task/orchestration.js +365 -15
- package/dist/lib/task/sections.js +5 -1
- package/dist/lib/task/task-execution-lock.js +5 -4
- package/dist/lib/task/verification-engine.js +78 -14
- package/dist/lib/task/verification.js +1 -0
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -3
- package/lib/internal/codex-lifecycle.ts +175 -4
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +16 -4
- package/lib/internal/task-orchestration.ts +34 -7
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/process-data/checkpoint.ts +335 -0
- package/lib/process-data/index.ts +112 -60
- package/lib/process-data/materialize.ts +83 -0
- package/lib/process-data/normalize.ts +29 -1
- package/lib/process-data/reconcile.ts +64 -2
- package/lib/process-data/sources.ts +449 -2
- package/lib/process-data/store.ts +304 -11
- package/lib/process-data/types.ts +124 -2
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +70 -25
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +423 -108
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/control/client.ts +152 -28
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +167 -0
- package/lib/sandbox/control/lifecycle.ts +573 -0
- package/lib/sandbox/control/protocol.ts +121 -68
- package/lib/sandbox/control/server.ts +398 -109
- package/lib/sandbox/control/state.ts +213 -0
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +16 -8
- package/lib/sandbox/recovery.ts +335 -65
- package/lib/sandbox/workspace-view.ts +102 -11
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +24 -3
- package/lib/server/process-state.ts +82 -27
- package/lib/task/artifact-lifecycle.ts +78 -14
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +319 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +52 -9
- package/lib/task/create.ts +11 -2
- package/lib/task/delegation-receipts.ts +208 -7
- package/lib/task/events.ts +131 -5
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/ledger.ts +16 -1
- package/lib/task/orchestration.ts +552 -18
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +93 -24
- package/lib/task/verification.ts +2 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +7 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
- package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +3 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +8 -8
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
- package/templates/.agents/workspace/README.en.md +0 -26
- 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
|
|
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}.
|
|
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
|
-
|
|
1032
|
-
|
|
1033
|
-
'
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
}
|