@fitlab-ai/agent-infra 0.11.7 → 0.11.9
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +8 -2
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/agent-clients/config.js +11 -3
- package/dist/lib/agent-clients/custom-tool-invocations.js +92 -0
- package/dist/lib/agent-clients/next-steps.js +1 -1
- package/dist/lib/agent-clients/reconcile.js +3 -3
- package/dist/lib/agent-clients/schema.js +18 -10
- package/dist/lib/defaults.json +6 -3
- package/dist/lib/init.js +2 -2
- package/dist/lib/internal/agent-client.js +3 -3
- package/dist/lib/internal/cli-route-inventory.js +3 -0
- package/dist/lib/internal/platform-pr-review.js +20 -3
- package/dist/lib/internal/task-operation-registry.js +3 -1
- package/dist/lib/internal/task-platform-recovery.js +56 -0
- package/dist/lib/merge.js +25 -5
- package/dist/lib/platform/completion-backfill.js +2 -1
- package/dist/lib/platform/in-label-sync.js +12 -1
- package/dist/lib/platform/issue-comments.js +58 -4
- package/dist/lib/platform/issues.js +105 -5
- package/dist/lib/platform/operation-coordinator.js +74 -0
- package/dist/lib/platform/pr-review.js +107 -1
- package/dist/lib/platform/pr-summary.js +79 -57
- package/dist/lib/platform/pull-requests.js +96 -4
- package/dist/lib/sandbox/commands/create.js +6 -3
- package/dist/lib/sandbox/commands/ls.js +4 -3
- package/dist/lib/sandbox/commands/rm.js +1 -1
- package/dist/lib/sandbox/config.js +22 -4
- package/dist/lib/sandbox/git-metadata.js +50 -0
- package/dist/lib/sandbox/index.js +1 -1
- package/dist/lib/sandbox/mounts.js +12 -0
- package/dist/lib/sandbox/recovery.js +12 -22
- package/dist/lib/sandbox/removal.js +58 -20
- package/dist/lib/sandbox/tools.js +31 -7
- package/dist/lib/task/activity-intent.js +4 -3
- package/dist/lib/task/create-service.js +5 -3
- package/dist/lib/task/create.js +22 -5
- package/dist/lib/task/finalization-artifacts.js +4 -1
- package/dist/lib/task/finalization.js +243 -113
- package/dist/lib/task/lifecycle.js +38 -2
- package/dist/lib/task/platform-operation-journal.js +152 -0
- package/dist/lib/task/platform-operation-recovery.js +459 -0
- package/dist/lib/task/verification.js +0 -1
- package/dist/lib/task/workflow-warning-intents.js +7 -1
- package/dist/lib/update.js +2 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/agent-clients/config.ts +9 -3
- package/lib/agent-clients/custom-tool-invocations.ts +129 -0
- package/lib/agent-clients/next-steps.ts +3 -3
- package/lib/agent-clients/reconcile.ts +3 -3
- package/lib/agent-clients/schema.ts +18 -10
- package/lib/defaults.json +6 -3
- package/lib/init.ts +2 -2
- package/lib/internal/agent-client.ts +3 -3
- package/lib/internal/cli-route-inventory.ts +2 -0
- package/lib/internal/platform-pr-review.ts +14 -3
- package/lib/internal/task-operation-registry.ts +3 -1
- package/lib/internal/task-platform-recovery.ts +53 -0
- package/lib/merge.ts +26 -6
- package/lib/platform/completion-backfill.ts +2 -1
- package/lib/platform/in-label-sync.ts +12 -0
- package/lib/platform/issue-comments.ts +59 -3
- package/lib/platform/issues.ts +103 -5
- package/lib/platform/operation-coordinator.ts +96 -0
- package/lib/platform/pr-review.ts +113 -1
- package/lib/platform/pr-summary.ts +22 -1
- package/lib/platform/pull-requests.ts +105 -5
- package/lib/sandbox/commands/create.ts +7 -8
- package/lib/sandbox/commands/ls.ts +4 -3
- package/lib/sandbox/commands/rm.ts +1 -1
- package/lib/sandbox/config.ts +23 -5
- package/lib/sandbox/git-metadata.ts +66 -0
- package/lib/sandbox/index.ts +1 -1
- package/lib/sandbox/mounts.ts +14 -0
- package/lib/sandbox/recovery.ts +13 -25
- package/lib/sandbox/removal.ts +57 -19
- package/lib/sandbox/tool-types.ts +2 -1
- package/lib/sandbox/tools.ts +38 -7
- package/lib/task/activity-intent.ts +4 -3
- package/lib/task/create-service.ts +5 -3
- package/lib/task/create.ts +17 -5
- package/lib/task/finalization-artifacts.ts +4 -1
- package/lib/task/finalization.ts +242 -106
- package/lib/task/lifecycle.ts +35 -3
- package/lib/task/platform-operation-journal.ts +230 -0
- package/lib/task/platform-operation-recovery.ts +435 -0
- package/lib/task/verification.ts +1 -2
- package/lib/task/workflow-warning-intents.ts +9 -1
- package/lib/update.ts +2 -3
- package/package.json +1 -1
- package/templates/.agents/README.en.md +20 -66
- package/templates/.agents/README.zh-CN.md +19 -65
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/next-step-output.en.md +1 -1
- package/templates/.agents/rules/next-step-output.zh-CN.md +1 -1
- package/templates/.agents/rules/task-management.en.md +12 -0
- package/templates/.agents/rules/task-management.zh-CN.md +12 -0
- package/templates/.agents/scripts/lib/agent-infra-package.js +0 -7
- package/templates/.agents/skills/cancel-task/SKILL.en.md +4 -2
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +4 -2
- package/templates/.agents/skills/code-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/commit/SKILL.en.md +1 -1
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +15 -6
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +15 -6
- package/templates/.agents/skills/review-pr/SKILL.en.md +11 -11
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +11 -11
- package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +3 -3
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -0
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -0
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +1 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +21 -274
- package/dist/lib/agent-clients/custom-tuis.js +0 -99
- package/lib/agent-clients/custom-tuis.ts +0 -149
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { normalizePullRequest, type RemotePullRequest } from './github-data.ts';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
4
5
|
import { execFileSync } from 'node:child_process';
|
|
5
6
|
|
|
6
7
|
import { parseTypedTaskFrontmatter } from '../task/frontmatter.ts';
|
|
@@ -41,6 +42,8 @@ import {
|
|
|
41
42
|
import { isResourceIdentity, resourceIdentityEquals, resourceIdentityNumber, serializeResourceIdentity } from './resource-identity.ts';
|
|
42
43
|
import type { ResourceIdentity } from './resource-identity.ts';
|
|
43
44
|
import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.ts';
|
|
45
|
+
import { coordinatePlatformWrite } from './operation-coordinator.ts';
|
|
46
|
+
import type { PlatformPullRequestIntent } from '../task/platform-operation-journal.ts';
|
|
44
47
|
import type { ChangeRequestSnapshot as ProviderChangeRequestSnapshot, IssueSnapshot as ProviderIssueSnapshot } from './provider-contract.ts';
|
|
45
48
|
|
|
46
49
|
type PullRequestSnapshot = PlatformChangeRequestSnapshot;
|
|
@@ -63,7 +66,7 @@ type PullRequestResult = PlatformResult & {
|
|
|
63
66
|
}>;
|
|
64
67
|
};
|
|
65
68
|
type InspectionOptions = { cwd?: string; client?: PlatformClient; runtimeVersion?: string };
|
|
66
|
-
type SharedOptions = { cwd?: string; client?: PlatformClient; runtimeVersion?: string };
|
|
69
|
+
type SharedOptions = { cwd?: string; client?: PlatformClient; runtimeVersion?: string; skipQueue?: boolean };
|
|
67
70
|
type CreateOptions = SharedOptions & {
|
|
68
71
|
agent: string;
|
|
69
72
|
base: string;
|
|
@@ -137,6 +140,30 @@ function withCreation(output: PullRequestResult, creation: CreationOutcome): Pul
|
|
|
137
140
|
return { ...output, creation };
|
|
138
141
|
}
|
|
139
142
|
|
|
143
|
+
async function journalPullRequestOperation(
|
|
144
|
+
taskRef: string,
|
|
145
|
+
options: SharedOptions & { agent: string; dryRun?: boolean },
|
|
146
|
+
intent: PlatformPullRequestIntent,
|
|
147
|
+
target: string,
|
|
148
|
+
identityInput: unknown,
|
|
149
|
+
execute: () => Promise<PullRequestResult>
|
|
150
|
+
): Promise<PullRequestResult> {
|
|
151
|
+
if (options.dryRun) return execute();
|
|
152
|
+
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
153
|
+
if (!resolved.ok) return execute();
|
|
154
|
+
const expectedDigest = createHash('sha256').update(JSON.stringify({ taskId: resolved.taskId, intent, identityInput })).digest('hex');
|
|
155
|
+
const operation = { kind: 'pull-request' as const, target: intent.action === 'sync' ? `bound-pr:${resolved.taskId}` : target, expectedDigest };
|
|
156
|
+
const dependency = intent.action === 'sync' ? 'deferred' : 'required';
|
|
157
|
+
if (options.skipQueue) return execute();
|
|
158
|
+
return coordinatePlatformWrite({
|
|
159
|
+
operation: { taskRef: resolved.taskId, cwd: resolved.repoRoot, ...operation, pullRequest: intent, dependency },
|
|
160
|
+
agent: options.agent,
|
|
161
|
+
execute,
|
|
162
|
+
block: (error) => result('blocked', resolved.taskId, null, null, { error }),
|
|
163
|
+
persistenceFailure: (error) => result('failed', resolved.taskId, null, null, { error })
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
140
167
|
const PRECONDITION_NOT_CREATED: CreationOutcome = {
|
|
141
168
|
kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false
|
|
142
169
|
};
|
|
@@ -851,7 +878,7 @@ function bindIdentity(
|
|
|
851
878
|
}, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
|
|
852
879
|
}
|
|
853
880
|
|
|
854
|
-
async function
|
|
881
|
+
async function bindPlatformPullRequestImpl(taskRef: string, options: BindOptions): Promise<PullRequestResult> {
|
|
855
882
|
const base = await resolvedContext(taskRef, options);
|
|
856
883
|
if (!base.ok) return base.output;
|
|
857
884
|
if (!base.fact) return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
@@ -884,6 +911,12 @@ async function bindPlatformPullRequest(taskRef: string, options: BindOptions): P
|
|
|
884
911
|
});
|
|
885
912
|
}
|
|
886
913
|
|
|
914
|
+
async function bindPlatformPullRequest(taskRef: string, options: BindOptions): Promise<PullRequestResult> {
|
|
915
|
+
const intent: PlatformPullRequestIntent = { action: 'bind', prToken: String(options.pr) };
|
|
916
|
+
return journalPullRequestOperation(taskRef, options, intent, `pr:${options.pr}`, { pr: String(options.pr) },
|
|
917
|
+
() => bindPlatformPullRequestImpl(taskRef, options));
|
|
918
|
+
}
|
|
919
|
+
|
|
887
920
|
function externalEvidenceNote(
|
|
888
921
|
issueIdentity: ResourceIdentity | null,
|
|
889
922
|
source: 'unique' | 'explicit',
|
|
@@ -1077,7 +1110,7 @@ async function resolveExternalPullRequest(taskRef: string, options: ResolveExter
|
|
|
1077
1110
|
}), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
1078
1111
|
}
|
|
1079
1112
|
|
|
1080
|
-
async function
|
|
1113
|
+
async function createPlatformPullRequestImpl(taskRef: string, options: CreateOptions): Promise<PullRequestResult> {
|
|
1081
1114
|
const base = await resolvedContext(taskRef, options);
|
|
1082
1115
|
if (!base.ok) return withCreation(base.output, PRECONDITION_NOT_CREATED);
|
|
1083
1116
|
if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) {
|
|
@@ -1112,6 +1145,66 @@ async function createPlatformPullRequest(taskRef: string, options: CreateOptions
|
|
|
1112
1145
|
}
|
|
1113
1146
|
}
|
|
1114
1147
|
|
|
1148
|
+
async function recoverCreatedPullRequest(
|
|
1149
|
+
taskRef: string,
|
|
1150
|
+
options: SharedOptions & { agent: string; base: string; head: string }
|
|
1151
|
+
): Promise<PullRequestResult> {
|
|
1152
|
+
const base = await resolvedContext(taskRef, options);
|
|
1153
|
+
if (!base.ok) return base.output;
|
|
1154
|
+
if (!base.issueIdentity) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1155
|
+
error: { code: 'PLATFORM_OPERATION_ISSUE_REQUIRED', message: 'Pull-request recovery requires a bound Issue identity', retryable: false }
|
|
1156
|
+
});
|
|
1157
|
+
const head = base.provider.changeRequests?.verifyHead
|
|
1158
|
+
? await base.provider.changeRequests.verifyHead({ context: providerOperationContext(base.loadedContext), head: options.head })
|
|
1159
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.verifyHead');
|
|
1160
|
+
if (!head.ok) return providerPullRequestError(base, head.error, base.prNumber);
|
|
1161
|
+
if (base.prIdentity) {
|
|
1162
|
+
const inspected = await inspectExternalPullRequest(base, base.prIdentity);
|
|
1163
|
+
if (!inspected.pullRequest || inspected.status === 'failed' || inspected.status === 'blocked'
|
|
1164
|
+
|| inspected.pullRequest.base.repository !== base.context.platform.repository
|
|
1165
|
+
|| inspected.pullRequest.base.ref !== options.base || inspected.pullRequest.head.ref !== options.head
|
|
1166
|
+
|| inspected.pullRequest.head.sha !== head.value.sha) {
|
|
1167
|
+
return result('blocked', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1168
|
+
error: inspected.error ?? { code: 'PLATFORM_OPERATION_PR_NOT_CONFIRMED', message: 'Bound pull request does not match the pending create intent', retryable: true }
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
return inspected;
|
|
1172
|
+
}
|
|
1173
|
+
const candidates = base.provider.changeRequests?.listClosing
|
|
1174
|
+
? await base.provider.changeRequests.listClosing({ context: providerOperationContext(base.loadedContext), issue: base.issueIdentity })
|
|
1175
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.listClosing');
|
|
1176
|
+
if (!candidates.ok) return providerPullRequestError(base, candidates.error, null);
|
|
1177
|
+
const matches = candidates.value.map((candidate) => normalizeProviderPullRequest(candidate, base.context.platform.repository!, candidate.number || 0))
|
|
1178
|
+
.filter((candidate) => candidate.base.repository === base.context.platform.repository
|
|
1179
|
+
&& candidate.base.ref === options.base
|
|
1180
|
+
&& candidate.head.repository === base.context.platform.repository
|
|
1181
|
+
&& candidate.head.ref === options.head
|
|
1182
|
+
&& candidate.head.sha === head.value.sha);
|
|
1183
|
+
if (matches.length !== 1 || !matches[0]?.number) return result('blocked', base.resolved.taskId, base.issueNumber, null, {
|
|
1184
|
+
error: { code: matches.length > 1 ? 'PLATFORM_OPERATION_PR_AMBIGUOUS' : 'PLATFORM_OPERATION_PR_NOT_CONFIRMED',
|
|
1185
|
+
message: 'Pending pull-request creation does not resolve to one exact repository/base/head identity', retryable: true }
|
|
1186
|
+
});
|
|
1187
|
+
const bound = await bindPlatformPullRequest(base.resolved.taskId, { agent: options.agent, cwd: base.resolved.repoRoot, pr: matches[0].number });
|
|
1188
|
+
if (!bound.pullRequest || (bound.status !== 'applied' && bound.status !== 'no-op') || bound.error) return bound;
|
|
1189
|
+
if (bound.pullRequest.base.repository !== base.context.platform.repository || bound.pullRequest.base.ref !== options.base
|
|
1190
|
+
|| bound.pullRequest.head.repository !== base.context.platform.repository || bound.pullRequest.head.ref !== options.head
|
|
1191
|
+
|| bound.pullRequest.head.sha !== head.value.sha) return result('blocked', base.resolved.taskId, base.issueNumber, bound.task.prNumber, {
|
|
1192
|
+
error: { code: 'PLATFORM_OPERATION_PR_NOT_CONFIRMED', message: 'Bound pull request changed while recovering the create intent', retryable: true }
|
|
1193
|
+
});
|
|
1194
|
+
return bound;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
async function createPlatformPullRequest(taskRef: string, options: CreateOptions): Promise<PullRequestResult> {
|
|
1198
|
+
const intent: PlatformPullRequestIntent = { action: 'create', baseRef: options.base, headRef: options.head };
|
|
1199
|
+
const identityInput = {
|
|
1200
|
+
title: createHash('sha256').update(options.title).digest('hex'),
|
|
1201
|
+
body: createHash('sha256').update(options.body).digest('hex'),
|
|
1202
|
+
draft: options.draft === true
|
|
1203
|
+
};
|
|
1204
|
+
return journalPullRequestOperation(taskRef, options, intent, `head:${options.head}:base:${options.base}`, identityInput,
|
|
1205
|
+
() => createPlatformPullRequestImpl(taskRef, options));
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1115
1208
|
async function createExternalPullRequest(
|
|
1116
1209
|
taskRef: string,
|
|
1117
1210
|
options: CreateOptions,
|
|
@@ -1266,7 +1359,7 @@ async function createExternalPullRequest(
|
|
|
1266
1359
|
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1267
1360
|
}
|
|
1268
1361
|
|
|
1269
|
-
async function
|
|
1362
|
+
async function syncPlatformPullRequestImpl(taskRef: string, options: SyncOptions): Promise<PullRequestResult> {
|
|
1270
1363
|
const warningResult = warningResultForPrimary(options.primaryResult);
|
|
1271
1364
|
const softenFailure = (output: PullRequestResult): PullRequestResult => {
|
|
1272
1365
|
const authorityError = output.error?.code.startsWith('IN_LABEL_SYNC') || output.error?.code.startsWith('PR_IDENTITY');
|
|
@@ -1430,6 +1523,13 @@ async function syncPlatformPullRequest(taskRef: string, options: SyncOptions): P
|
|
|
1430
1523
|
}
|
|
1431
1524
|
}
|
|
1432
1525
|
|
|
1526
|
+
async function syncPlatformPullRequest(taskRef: string, options: SyncOptions): Promise<PullRequestResult> {
|
|
1527
|
+
const intent: PlatformPullRequestIntent = { action: 'sync', metadata: options.metadata === true, closingIssue: options.closingIssue === true };
|
|
1528
|
+
return journalPullRequestOperation(taskRef, options, intent, `bound-pr:${taskRef}`, {
|
|
1529
|
+
metadata: options.metadata === true, closingIssue: options.closingIssue === true
|
|
1530
|
+
}, () => syncPlatformPullRequestImpl(taskRef, options));
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1433
1533
|
function readProjectPrFlow(repoRoot: string): 'required' | 'disabled' | undefined {
|
|
1434
1534
|
try {
|
|
1435
1535
|
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8')) as { prFlow?: unknown };
|
|
@@ -1527,6 +1627,6 @@ async function skipPlatformPullRequestFact(taskRef: string, options: SkipFactOpt
|
|
|
1527
1627
|
}
|
|
1528
1628
|
}
|
|
1529
1629
|
|
|
1530
|
-
export { bindPlatformPullRequest, createPlatformPullRequest, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, resolveExternalPullRequest, skipPlatformPullRequestFact, selectExternalPullRequest, selectPullRequest, syncPlatformPullRequest, syncPlatformPullRequestInLabels };
|
|
1630
|
+
export { bindPlatformPullRequest, createPlatformPullRequest, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, recoverCreatedPullRequest, resolveExternalPullRequest, skipPlatformPullRequestFact, selectExternalPullRequest, selectPullRequest, syncPlatformPullRequest, syncPlatformPullRequestInLabels };
|
|
1531
1631
|
export type { BindOptions, CreateOptions, ExternalPullRequestResult, ExternalPullRequestSelection, PullRequestPrimaryResult, PullRequestResult, PullRequestSnapshot, ResolveExternalOptions, SkipFactOptions, SyncOptions };
|
|
1532
1632
|
export { warningResultForPrimary };
|
|
@@ -83,6 +83,7 @@ import { atomicWriteJson } from '../control/state.ts';
|
|
|
83
83
|
import { inspectSandboxControlContainer } from '../control/container-identity.ts';
|
|
84
84
|
import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
|
|
85
85
|
import { sandboxCoreBindMounts } from '../mounts.ts';
|
|
86
|
+
import { materializeSandboxGitMetadata } from '../git-metadata.ts';
|
|
86
87
|
import {
|
|
87
88
|
assertSandboxTaskSource,
|
|
88
89
|
finalizeSandboxControlManifest,
|
|
@@ -950,9 +951,10 @@ function runSandboxAuthoritySafeCommand(
|
|
|
950
951
|
|
|
951
952
|
// `docker run` args for mounting a tool's containerMount as an in-container
|
|
952
953
|
// tmpfs. containerMount is an in-container path, so it is NOT engine-converted.
|
|
953
|
-
export function buildTmpfsRunArgs(containerMount: string, tmpfs: { size?: string }): string[] {
|
|
954
|
+
export function buildTmpfsRunArgs(containerMount: string, tmpfs: { size?: string; exec?: boolean }): string[] {
|
|
954
955
|
const size = tmpfs.size ?? '512m';
|
|
955
|
-
|
|
956
|
+
const options = ['rw', ...(tmpfs.exec ? ['exec'] : []), `size=${size}`];
|
|
957
|
+
return ['--tmpfs', `${containerMount}:${options.join(',')}`];
|
|
956
958
|
}
|
|
957
959
|
|
|
958
960
|
export function buildImage(
|
|
@@ -1459,6 +1461,7 @@ export async function create(
|
|
|
1459
1461
|
engine,
|
|
1460
1462
|
replacementLease,
|
|
1461
1463
|
});
|
|
1464
|
+
const gitMetadata = materializeSandboxGitMetadata(worktree, control.channelDir);
|
|
1462
1465
|
hostShellConfig = prepareHostShellConfig({
|
|
1463
1466
|
home: effectiveConfig.home,
|
|
1464
1467
|
project: effectiveConfig.project,
|
|
@@ -1466,6 +1469,8 @@ export async function create(
|
|
|
1466
1469
|
repoRoot: effectiveConfig.repoRoot
|
|
1467
1470
|
});
|
|
1468
1471
|
const coreVolumes = sandboxCoreBindMounts(effectiveConfig, branch, {
|
|
1472
|
+
gitCommonDir: gitMetadata.commonDir,
|
|
1473
|
+
worktreeGitFile: gitMetadata.worktreeGitFile,
|
|
1469
1474
|
worktree,
|
|
1470
1475
|
shellConfigHostDir: hostShellConfig.hostDir,
|
|
1471
1476
|
workspaceViewRoot: workspaceView.root,
|
|
@@ -1572,12 +1577,6 @@ export async function create(
|
|
|
1572
1577
|
`${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
|
|
1573
1578
|
...coreVolumes,
|
|
1574
1579
|
...buildClipboardVolumeArgs(engine, effectiveConfig.home),
|
|
1575
|
-
'-v',
|
|
1576
|
-
volumeArg(
|
|
1577
|
-
engine,
|
|
1578
|
-
path.join(effectiveConfig.repoRoot, '.git'),
|
|
1579
|
-
`${toEnginePath(engine, effectiveConfig.repoRoot)}/.git`
|
|
1580
|
-
),
|
|
1581
1580
|
...dotfilesMount,
|
|
1582
1581
|
...toolVolumes,
|
|
1583
1582
|
...tmpfsArgs,
|
|
@@ -22,8 +22,9 @@ display-only row number; the 'SHORT' column shows the active task short
|
|
|
22
22
|
id bound to each container's branch (via
|
|
23
23
|
.agents/workspace/active/.short-ids.json), or '-' if no active task is
|
|
24
24
|
bound. Pass the SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
|
|
25
|
-
A '-' means no active task
|
|
26
|
-
|
|
25
|
+
A SHORT value of '-' means this container has no active task short id and may
|
|
26
|
+
be eligible for "ai sandbox rm --unbound" cleanup. Use a full TASK-id to
|
|
27
|
+
remove a specific task-bound sandbox.
|
|
27
28
|
|
|
28
29
|
Use "ai sandbox show <ref>" for a single sandbox's worktree and per-tool
|
|
29
30
|
state paths.`;
|
|
@@ -97,7 +98,7 @@ export function ls(args: string[] = []): void {
|
|
|
97
98
|
}
|
|
98
99
|
if (tableRows.some((r) => r.shortId === '-')) {
|
|
99
100
|
process.stdout.write(
|
|
100
|
-
` SHORT '-' = no active task
|
|
101
|
+
` SHORT '-' = no active task short id is displayed; --unbound may clean this sandbox if its branch is not tied to an active task.\n`
|
|
101
102
|
);
|
|
102
103
|
}
|
|
103
104
|
}
|
|
@@ -14,7 +14,7 @@ import { rmOne, rmPurge, rmUnbound } from '../removal.ts';
|
|
|
14
14
|
|
|
15
15
|
const USAGE = `Usage:
|
|
16
16
|
ai sandbox rm <branch | TASK-id | short id> Remove one sandbox; use a full TASK-id for a task-bound sandbox and a branch for branch-only sandboxes
|
|
17
|
-
ai sandbox rm --unbound [--dry-run] [--yes] Remove
|
|
17
|
+
ai sandbox rm --unbound [--dry-run] [--yes] Remove sandboxes not bound to an active task short id
|
|
18
18
|
ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
|
|
19
19
|
|
|
20
20
|
function resolveMissingTaskSandboxBranch(taskId: string, config: ReturnType<typeof loadConfig>): string | null {
|
package/lib/sandbox/config.ts
CHANGED
|
@@ -32,7 +32,7 @@ type WriteStderr = (chunk: string) => unknown;
|
|
|
32
32
|
type SandboxConfigInput = {
|
|
33
33
|
engine?: SandboxEngineInput;
|
|
34
34
|
runtimes?: string[];
|
|
35
|
-
tools?:
|
|
35
|
+
tools?: unknown;
|
|
36
36
|
customTools?: unknown;
|
|
37
37
|
refreshIntervalDays?: unknown;
|
|
38
38
|
dockerfile?: string | null;
|
|
@@ -115,10 +115,22 @@ function resolveSandboxToolIds(
|
|
|
115
115
|
sandbox: SandboxConfigInput,
|
|
116
116
|
defaults: readonly string[]
|
|
117
117
|
): string[] {
|
|
118
|
-
if (
|
|
119
|
-
return
|
|
118
|
+
if (sandbox.tools === undefined) {
|
|
119
|
+
return [...defaults];
|
|
120
120
|
}
|
|
121
|
-
|
|
121
|
+
if (typeof sandbox.tools !== 'object' || sandbox.tools === null || Array.isArray(sandbox.tools)) {
|
|
122
|
+
throw new Error('sandbox.tools must be an object when configured');
|
|
123
|
+
}
|
|
124
|
+
const tools = sandbox.tools as Record<string, unknown>;
|
|
125
|
+
if (!Array.isArray(tools.ids)) {
|
|
126
|
+
throw new Error('sandbox.tools.ids must be an array');
|
|
127
|
+
}
|
|
128
|
+
return tools.ids.map((id, index) => {
|
|
129
|
+
if (typeof id !== 'string') {
|
|
130
|
+
throw new Error(`sandbox.tools.ids[${index}] must be a string`);
|
|
131
|
+
}
|
|
132
|
+
return id;
|
|
133
|
+
});
|
|
122
134
|
}
|
|
123
135
|
|
|
124
136
|
export function loadConfig({
|
|
@@ -180,7 +192,13 @@ export function loadConfig({
|
|
|
180
192
|
}
|
|
181
193
|
}
|
|
182
194
|
|
|
183
|
-
const
|
|
195
|
+
const toolsConfig = sandbox.tools && typeof sandbox.tools === 'object' && !Array.isArray(sandbox.tools)
|
|
196
|
+
? sandbox.tools as Record<string, unknown>
|
|
197
|
+
: undefined;
|
|
198
|
+
if (sandbox.customTools !== undefined) {
|
|
199
|
+
throw new Error('sandbox.customTools is no longer supported; use sandbox.tools.definitions');
|
|
200
|
+
}
|
|
201
|
+
const customTools = parseCustomTools(toolsConfig?.definitions, { home });
|
|
184
202
|
const tools = resolveSandboxToolIds(sandbox, defaults.tools);
|
|
185
203
|
|
|
186
204
|
return {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const SANDBOX_GIT_COMMON_DIR = '/run/agent-infra/git';
|
|
6
|
+
|
|
7
|
+
export type SandboxGitMetadata = Readonly<{
|
|
8
|
+
commonDir: string;
|
|
9
|
+
worktreeGitFile: string;
|
|
10
|
+
worktreeGitFileContent: string;
|
|
11
|
+
}>;
|
|
12
|
+
|
|
13
|
+
function gitText(worktreeRoot: string, args: string[]): string {
|
|
14
|
+
const env = { ...process.env };
|
|
15
|
+
for (const key of [
|
|
16
|
+
'GIT_DIR',
|
|
17
|
+
'GIT_COMMON_DIR',
|
|
18
|
+
'GIT_WORK_TREE',
|
|
19
|
+
'GIT_INDEX_FILE',
|
|
20
|
+
'GIT_PREFIX',
|
|
21
|
+
'GIT_NAMESPACE',
|
|
22
|
+
'GIT_OBJECT_DIRECTORY',
|
|
23
|
+
'GIT_CEILING_DIRECTORIES',
|
|
24
|
+
'GIT_DISCOVERY_ACROSS_FILESYSTEM'
|
|
25
|
+
]) {
|
|
26
|
+
delete env[key];
|
|
27
|
+
}
|
|
28
|
+
return execFileSync('git', ['-C', worktreeRoot, ...args], {
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
31
|
+
env
|
|
32
|
+
}).trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveSandboxGitMetadata(
|
|
36
|
+
worktreeRoot: string,
|
|
37
|
+
controlDir: string
|
|
38
|
+
): SandboxGitMetadata {
|
|
39
|
+
const commonDir = fs.realpathSync.native(gitText(worktreeRoot, [
|
|
40
|
+
'rev-parse', '--path-format=absolute', '--git-common-dir'
|
|
41
|
+
]));
|
|
42
|
+
const gitDir = fs.realpathSync.native(gitText(worktreeRoot, ['rev-parse', '--absolute-git-dir']));
|
|
43
|
+
const relativeGitDir = path.relative(commonDir, gitDir);
|
|
44
|
+
if (path.isAbsolute(relativeGitDir) || relativeGitDir.startsWith(`..${path.sep}`) || relativeGitDir === '..') {
|
|
45
|
+
throw new Error('SANDBOX_GIT_METADATA_INVALID: worktree git directory is outside its common directory');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const containerGitDir = relativeGitDir === '.'
|
|
49
|
+
? SANDBOX_GIT_COMMON_DIR
|
|
50
|
+
: path.posix.join(SANDBOX_GIT_COMMON_DIR, ...relativeGitDir.split(path.sep));
|
|
51
|
+
return {
|
|
52
|
+
commonDir,
|
|
53
|
+
worktreeGitFile: path.join(controlDir, 'workspace.git'),
|
|
54
|
+
worktreeGitFileContent: `gitdir: ${containerGitDir}\n`
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function materializeSandboxGitMetadata(
|
|
59
|
+
worktreeRoot: string,
|
|
60
|
+
controlDir: string
|
|
61
|
+
): SandboxGitMetadata {
|
|
62
|
+
const metadata = resolveSandboxGitMetadata(worktreeRoot, controlDir);
|
|
63
|
+
fs.mkdirSync(controlDir, { recursive: true });
|
|
64
|
+
fs.writeFileSync(metadata.worktreeGitFile, metadata.worktreeGitFileContent, 'utf8');
|
|
65
|
+
return metadata;
|
|
66
|
+
}
|
package/lib/sandbox/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ Commands:
|
|
|
20
20
|
rm <branch | TASK-id> | --unbound | --purge
|
|
21
21
|
Remove one sandbox; use a full TASK-id for task-bound
|
|
22
22
|
cleanup and a branch for branch-only cleanup. --unbound
|
|
23
|
-
removes
|
|
23
|
+
removes sandboxes not bound to an active task short id.
|
|
24
24
|
start [--recreate] <branch | TASK-id | N>
|
|
25
25
|
Verify or recover an existing sandbox container;
|
|
26
26
|
optionally replace only the container on failure
|
package/lib/sandbox/mounts.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from './constants.ts';
|
|
8
8
|
import type { SandboxConfig } from './config.ts';
|
|
9
9
|
import { sandboxWorkspaceViewStatePaths } from './workspace-view.ts';
|
|
10
|
+
import { SANDBOX_GIT_COMMON_DIR } from './git-metadata.ts';
|
|
10
11
|
|
|
11
12
|
export type SandboxBindMountDeclaration = {
|
|
12
13
|
hostPaths: string[];
|
|
@@ -26,6 +27,8 @@ export function sandboxCoreBindMounts(
|
|
|
26
27
|
workspaceViewRoot: string;
|
|
27
28
|
controlDir: string;
|
|
28
29
|
controlStatusDir: string;
|
|
30
|
+
gitCommonDir?: string;
|
|
31
|
+
worktreeGitFile?: string;
|
|
29
32
|
runtimeDir?: string;
|
|
30
33
|
taskSources?: string[];
|
|
31
34
|
taskId?: string;
|
|
@@ -46,7 +49,17 @@ export function sandboxCoreBindMounts(
|
|
|
46
49
|
readOnly: true
|
|
47
50
|
}
|
|
48
51
|
);
|
|
52
|
+
if (Boolean(overrides.gitCommonDir) !== Boolean(overrides.worktreeGitFile)) {
|
|
53
|
+
throw new Error('SANDBOX_GIT_METADATA_INCOMPLETE');
|
|
54
|
+
}
|
|
55
|
+
const gitCommonMount = overrides.gitCommonDir
|
|
56
|
+
? { hostPaths: [overrides.gitCommonDir], containerPath: SANDBOX_GIT_COMMON_DIR, readOnly: false }
|
|
57
|
+
: null;
|
|
58
|
+
const worktreeGitFileMount = overrides.worktreeGitFile
|
|
59
|
+
? { hostPaths: [overrides.worktreeGitFile], containerPath: '/workspace/.git', readOnly: true }
|
|
60
|
+
: null;
|
|
49
61
|
const mounts: SandboxBindMountDeclaration[] = [
|
|
62
|
+
...(gitCommonMount ? [gitCommonMount] : []),
|
|
50
63
|
{
|
|
51
64
|
hostPaths: overrides.worktree
|
|
52
65
|
? [overrides.worktree]
|
|
@@ -54,6 +67,7 @@ export function sandboxCoreBindMounts(
|
|
|
54
67
|
containerPath: '/workspace',
|
|
55
68
|
readOnly: false
|
|
56
69
|
},
|
|
70
|
+
...(worktreeGitFileMount ? [worktreeGitFileMount] : []),
|
|
57
71
|
...workspaceMounts,
|
|
58
72
|
];
|
|
59
73
|
if (taskBound) {
|
package/lib/sandbox/recovery.ts
CHANGED
|
@@ -8,12 +8,10 @@ import {
|
|
|
8
8
|
containerNameCandidates,
|
|
9
9
|
sandboxBranchLabel,
|
|
10
10
|
sandboxLabel,
|
|
11
|
-
sandboxRuntimeCapabilityLabel,
|
|
12
11
|
sandboxTaskIdLabel,
|
|
13
12
|
sandboxWorkspaceModeLabel,
|
|
14
13
|
worktreeDirCandidates
|
|
15
14
|
} from './constants.ts';
|
|
16
|
-
import { isAgentClientId } from '../agent-clients/types.ts';
|
|
17
15
|
import {
|
|
18
16
|
createSandboxCapabilityPlan
|
|
19
17
|
} from './agent-client-reconciler.ts';
|
|
@@ -24,6 +22,7 @@ import type {
|
|
|
24
22
|
import type { SandboxConfig } from './config.ts';
|
|
25
23
|
import { toEnginePath } from './engines/wsl2-paths.ts';
|
|
26
24
|
import { sandboxCoreBindMounts } from './mounts.ts';
|
|
25
|
+
import { resolveSandboxGitMetadata } from './git-metadata.ts';
|
|
27
26
|
import {
|
|
28
27
|
assertSandboxTaskSource,
|
|
29
28
|
sandboxControlPaths,
|
|
@@ -99,8 +98,6 @@ export type SandboxRecoverySnapshot = {
|
|
|
99
98
|
workspaceTopology?: 'legacy-parent' | 'per-state' | 'unknown';
|
|
100
99
|
taskView?: { path: string; readable: boolean };
|
|
101
100
|
runtimeStoreOk?: boolean;
|
|
102
|
-
runtimeCapabilityOk?: boolean;
|
|
103
|
-
unexpectedCapabilityMounts?: string[];
|
|
104
101
|
mounts: Array<{
|
|
105
102
|
path: string;
|
|
106
103
|
expectedType: string;
|
|
@@ -451,12 +448,6 @@ export function classifySandboxRecovery(snapshot: SandboxRecoverySnapshot): Sand
|
|
|
451
448
|
: `Container identity ${actualIdentity} on branch ${snapshot.actualBranch ?? 'unknown'} does not match requested ${expectedIdentity} on branch ${snapshot.expectedBranch}.`
|
|
452
449
|
});
|
|
453
450
|
}
|
|
454
|
-
if (snapshot.runtimeCapabilityOk === false) {
|
|
455
|
-
findings.push({
|
|
456
|
-
repairKind: 'hard-failure',
|
|
457
|
-
message: 'Container runtime capability signature does not match the current sandbox plan.'
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
451
|
if (snapshot.workspaceTopology === 'legacy-parent') {
|
|
461
452
|
findings.push({
|
|
462
453
|
repairKind: 'hard-failure',
|
|
@@ -481,14 +472,6 @@ export function classifySandboxRecovery(snapshot: SandboxRecoverySnapshot): Sand
|
|
|
481
472
|
path: '/run/agent-infra/runtime'
|
|
482
473
|
});
|
|
483
474
|
}
|
|
484
|
-
for (const mountPath of snapshot.unexpectedCapabilityMounts ?? []) {
|
|
485
|
-
findings.push({
|
|
486
|
-
repairKind: 'hard-failure',
|
|
487
|
-
message: `Disabled Agent Client capability remains mounted at ${mountPath}.`,
|
|
488
|
-
path: mountPath
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
|
|
492
475
|
for (const mount of snapshot.mounts) {
|
|
493
476
|
if (
|
|
494
477
|
snapshot.workspaceTopology === 'legacy-parent'
|
|
@@ -764,7 +747,19 @@ function expectedMounts(params: {
|
|
|
764
747
|
? recoveryTaskSources(config.repoRoot, params.workspace.taskId)
|
|
765
748
|
: null;
|
|
766
749
|
const taskId = params.workspace.mode === 'task-bound' ? params.workspace.taskId : null;
|
|
750
|
+
const worktrees = worktreeDirCandidates(config, branch).filter((candidate) =>
|
|
751
|
+
fs.existsSync(path.join(candidate, '.git'))
|
|
752
|
+
);
|
|
753
|
+
const gitMetadata = worktrees.length === 1
|
|
754
|
+
? resolveSandboxGitMetadata(worktrees[0]!, control.channelDir)
|
|
755
|
+
: null;
|
|
767
756
|
const core = sandboxCoreBindMounts(config, branch, {
|
|
757
|
+
...(gitMetadata === null
|
|
758
|
+
? {}
|
|
759
|
+
: {
|
|
760
|
+
gitCommonDir: gitMetadata.commonDir,
|
|
761
|
+
worktreeGitFile: gitMetadata.worktreeGitFile
|
|
762
|
+
}),
|
|
768
763
|
workspaceViewRoot: view.root,
|
|
769
764
|
controlDir: control.channelDir,
|
|
770
765
|
controlStatusDir: control.statusDir,
|
|
@@ -951,11 +946,6 @@ export function collectSandboxRecoverySnapshot(params: {
|
|
|
951
946
|
mode: sandboxWorkspaceModeLabel(params.config),
|
|
952
947
|
taskId: sandboxTaskIdLabel(params.config)
|
|
953
948
|
});
|
|
954
|
-
const selectedToolIds = new Set(tools.map((tool) => tool.id));
|
|
955
|
-
const unexpectedCapabilityMounts = capabilityPlan.cleanupInventory
|
|
956
|
-
.filter((tool) => isAgentClientId(tool.id) && !selectedToolIds.has(tool.id))
|
|
957
|
-
.map((tool) => tool.containerMount)
|
|
958
|
-
.filter((mountPath) => mountsByDestination.has(mountPath));
|
|
959
949
|
const mountSnapshots = expectedMounts({
|
|
960
950
|
config: params.config,
|
|
961
951
|
branch: params.branch,
|
|
@@ -1022,8 +1012,6 @@ export function collectSandboxRecoverySnapshot(params: {
|
|
|
1022
1012
|
identityOk: typeof inspection.Id === 'string' && inspection.Id.length > 0
|
|
1023
1013
|
&& branchLabel === params.branch
|
|
1024
1014
|
&& sameSandboxWorkspaceIdentity(containerWorkspace, workspace),
|
|
1025
|
-
runtimeCapabilityOk: labels[sandboxRuntimeCapabilityLabel(params.config)] === capabilityPlan.runtimeSignature,
|
|
1026
|
-
unexpectedCapabilityMounts,
|
|
1027
1015
|
mounts: mountSnapshots,
|
|
1028
1016
|
tmpfs,
|
|
1029
1017
|
seeds,
|
package/lib/sandbox/removal.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { run, runEngine, runOk, runOkEngine, runSafe, runSafeEngine } from './sh
|
|
|
22
22
|
import {
|
|
23
23
|
parseSandboxWorkspaceIdentity,
|
|
24
24
|
resolveSandboxCleanupTarget,
|
|
25
|
+
resolveSandboxTarget,
|
|
25
26
|
sameSandboxWorkspaceIdentity,
|
|
26
27
|
type SandboxCleanupTarget,
|
|
27
28
|
type SandboxWorkspaceKey
|
|
@@ -1112,7 +1113,10 @@ async function rmUnbound(
|
|
|
1112
1113
|
tools: SandboxTool[],
|
|
1113
1114
|
options: { dryRun: boolean; assumeYes: boolean }
|
|
1114
1115
|
): Promise<void> {
|
|
1115
|
-
return
|
|
1116
|
+
return withRepositoryMutationLock(
|
|
1117
|
+
config.repoRoot,
|
|
1118
|
+
() => rmUnboundCore(config, tools, options)
|
|
1119
|
+
);
|
|
1116
1120
|
}
|
|
1117
1121
|
|
|
1118
1122
|
async function rmUnboundCore(
|
|
@@ -1127,32 +1131,66 @@ async function rmUnboundCore(
|
|
|
1127
1131
|
sandboxBranchLabel(config),
|
|
1128
1132
|
{ mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) }
|
|
1129
1133
|
);
|
|
1130
|
-
const
|
|
1134
|
+
const discovered = [...listed.running, ...listed.nonRunning];
|
|
1135
|
+
const branchMismatches: Array<{ container: string; labelBranch: string; taskBranch: string }> = [];
|
|
1136
|
+
const classified = discovered.map((row) => {
|
|
1137
|
+
if (!row.branch) return { row, protected: true, cleanupTarget: null };
|
|
1138
|
+
if (row.workspaceMode === 'task-bound' && row.taskId) {
|
|
1139
|
+
try {
|
|
1140
|
+
resolveSandboxTarget(row.taskId, config.repoRoot);
|
|
1141
|
+
return { row, protected: true, cleanupTarget: null };
|
|
1142
|
+
} catch {
|
|
1143
|
+
try {
|
|
1144
|
+
const cleanupTarget = resolveSandboxCleanupTarget(row.taskId, config.repoRoot, { allowProtected: true });
|
|
1145
|
+
if (cleanupTarget.taskState === 'blocked'
|
|
1146
|
+
|| cleanupTarget.taskState === 'completed'
|
|
1147
|
+
|| cleanupTarget.taskState === 'archive') {
|
|
1148
|
+
if (row.branch !== cleanupTarget.branch) {
|
|
1149
|
+
branchMismatches.push({ container: row.name, labelBranch: row.branch, taskBranch: cleanupTarget.branch });
|
|
1150
|
+
return { row, protected: true, cleanupTarget: null };
|
|
1151
|
+
}
|
|
1152
|
+
const activeBranchTarget = resolveSandboxTarget(cleanupTarget.branch, config.repoRoot);
|
|
1153
|
+
if (activeBranchTarget.workspace.mode === 'branch-only') {
|
|
1154
|
+
return { row, protected: false, cleanupTarget };
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
} catch {
|
|
1158
|
+
// An unresolved task identity has no active short-id evidence, so keep it out of bulk cleanup.
|
|
1159
|
+
}
|
|
1160
|
+
return { row, protected: true, cleanupTarget: null };
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
if (row.workspaceMode !== 'branch-only') return { row, protected: true, cleanupTarget: null };
|
|
1164
|
+
const cleanupTarget = resolveSandboxCleanupTarget(row.branch, config.repoRoot);
|
|
1165
|
+
return cleanupTarget.workspace.mode === 'branch-only'
|
|
1166
|
+
? { row, protected: false, cleanupTarget }
|
|
1167
|
+
: { row, protected: true, cleanupTarget: null };
|
|
1168
|
+
});
|
|
1169
|
+
const protectedBranches = new Set(classified
|
|
1170
|
+
.filter((entry) => entry.protected)
|
|
1171
|
+
.map(({ row }) => row.branch));
|
|
1172
|
+
const targets = classified
|
|
1173
|
+
.filter((entry) => !entry.protected && !protectedBranches.has(entry.row.branch) && entry.cleanupTarget)
|
|
1174
|
+
.map(({ row, cleanupTarget }) => ({
|
|
1175
|
+
row,
|
|
1176
|
+
cleanupTarget: cleanupTarget!,
|
|
1177
|
+
target: resolveRmTarget(config, tools, cleanupTarget!, { discoveredContainers: [row.name] })
|
|
1178
|
+
}));
|
|
1131
1179
|
p.intro(pc.cyan(`Removing sandboxes for ${config.project}`));
|
|
1180
|
+
for (const mismatch of branchMismatches) {
|
|
1181
|
+
p.log.warn(`Skipping task-bound sandbox '${mismatch.container}': container branch '${mismatch.labelBranch}' differs from task record branch '${mismatch.taskBranch}'. Inspect the task record and container labels before retrying cleanup.`);
|
|
1182
|
+
}
|
|
1132
1183
|
if (options.dryRun) {
|
|
1133
|
-
p.outro(`Dry run: ${
|
|
1184
|
+
p.outro(`Dry run: ${targets.length} sandbox(es) found, nothing deleted`);
|
|
1134
1185
|
return;
|
|
1135
1186
|
}
|
|
1136
|
-
for (const row of
|
|
1137
|
-
|
|
1138
|
-
runSafeEngine(engine, 'docker', ['rm', '-f', row.name]);
|
|
1139
|
-
continue;
|
|
1140
|
-
}
|
|
1141
|
-
const cleanupTarget: SandboxCleanupTarget = {
|
|
1142
|
-
requestedRef: row.branch,
|
|
1143
|
-
branch: row.branch,
|
|
1144
|
-
workspace: row.workspaceMode === 'task-bound' && row.taskId
|
|
1145
|
-
? { mode: 'task-bound', taskId: row.taskId }
|
|
1146
|
-
: { mode: 'branch-only' },
|
|
1147
|
-
taskState: 'branch-only'
|
|
1148
|
-
};
|
|
1149
|
-
const target = resolveRmTarget(config, tools, cleanupTarget, { discoveredContainers: [row.name] });
|
|
1150
|
-
await rmOne(config, tools, cleanupTarget.branch, {
|
|
1187
|
+
for (const { row, cleanupTarget, target } of targets) {
|
|
1188
|
+
await runRmOneUnderRepositoryLock(config, tools, cleanupTarget.branch, {
|
|
1151
1189
|
assumeYes: options.assumeYes,
|
|
1152
1190
|
target,
|
|
1153
1191
|
cleanupTarget
|
|
1154
1192
|
});
|
|
1155
1193
|
}
|
|
1156
|
-
p.outro(pc.green(`Removed ${
|
|
1194
|
+
p.outro(pc.green(`Removed ${targets.length} sandbox(es)`));
|
|
1157
1195
|
}
|
|
1158
1196
|
export { authorizeWorktrees, rmOne, rmPurge, rmUnbound };
|
|
@@ -10,13 +10,14 @@ type SandboxTool = {
|
|
|
10
10
|
containerMount: string;
|
|
11
11
|
versionCmd: string;
|
|
12
12
|
setupHint: string;
|
|
13
|
+
invoke?: string;
|
|
13
14
|
envVars?: Record<string, string>;
|
|
14
15
|
hostPreSeedFiles?: Array<{ hostPath: string; sandboxName: string }>;
|
|
15
16
|
hostPreSeedDirs?: Array<{ hostDir: string; sandboxSubdir: string }>;
|
|
16
17
|
pathRewriteFiles?: string[];
|
|
17
18
|
hostLiveMounts?: Array<{ hostPath: string; containerSubpath: string }>;
|
|
18
19
|
postSetupCmds?: string[];
|
|
19
|
-
tmpfs?: { size?: string; seed?: string[] };
|
|
20
|
+
tmpfs?: { size?: string; seed?: string[]; exec?: boolean };
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
type SandboxAlias = Readonly<{
|