@fitlab-ai/agent-infra 0.11.4 → 0.11.6
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 +0 -5
- package/dist/lib/decide.js +4 -41
- package/dist/lib/git/branch-worktree.js +45 -0
- package/dist/lib/internal/cli-route-inventory.js +0 -3
- package/dist/lib/internal/task-activity.js +7 -40
- package/dist/lib/internal/task-event.js +4 -37
- package/dist/lib/internal/task-finalization.js +9 -2
- package/dist/lib/internal/task-ledger.js +5 -43
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +1 -4
- package/dist/lib/internal/task-review.js +5 -37
- package/dist/lib/internal/task-verify.js +3 -47
- package/dist/lib/internal/task-warning.js +4 -40
- package/dist/lib/merge.js +62 -10
- package/dist/lib/platform/github-provider.js +9 -14
- package/dist/lib/platform/github-release-notes.js +80 -11
- package/dist/lib/platform/none-provider.js +1 -1
- package/dist/lib/platform/pr-summary.js +4 -23
- package/dist/lib/platform/provider-contract.d.ts +39 -16
- package/dist/lib/platform/provider-contract.js +1 -1
- package/dist/lib/platform/provider-validation.js +83 -13
- package/dist/lib/platform/release-notes.js +7 -8
- package/dist/lib/platform/verification-sync.js +12 -49
- package/dist/lib/sandbox/capture.js +1 -1
- package/dist/lib/sandbox/commands/create.js +46 -1
- package/dist/lib/sandbox/commands/enter.js +1 -1
- package/dist/lib/sandbox/commands/start.js +1 -1
- package/dist/lib/sandbox/config.js +10 -0
- package/dist/lib/sandbox/control/client.js +4 -1
- package/dist/lib/sandbox/control/executor.js +12 -4
- package/dist/lib/sandbox/control/lifecycle.js +18 -13
- package/dist/lib/sandbox/control/protocol.js +2 -1
- package/dist/lib/sandbox/removal.js +15 -8
- package/dist/lib/sandbox/task-resolver.js +62 -2
- package/dist/lib/task/activity-intent.js +1 -2
- package/dist/lib/task/artifact-operations.js +36 -2
- package/dist/lib/task/artifact-schema.js +7 -7
- package/dist/lib/task/control-authority.js +9 -166
- package/dist/lib/task/decision-intents.js +2 -4
- package/dist/lib/task/events.js +7 -28
- package/dist/lib/task/finalization-handoff.js +92 -0
- package/dist/lib/task/finalization.js +123 -12
- package/dist/lib/task/intermediate-cleanup.js +9 -148
- package/dist/lib/task/ledger-intents.js +3 -6
- package/dist/lib/task/lifecycle.js +50 -11
- package/dist/lib/task/local-artifact-finalization.js +13 -28
- package/dist/lib/task/resolve-ref.js +3 -3
- package/dist/lib/task/review-command.js +1 -2
- package/dist/lib/task/review-finalization.js +1 -2
- package/dist/lib/task/snapshot.js +6 -6
- package/dist/lib/task/verification-engine.js +16 -11
- package/dist/lib/task/verification.js +1 -13
- package/dist/lib/task/workflow-warning-intents.js +2 -4
- package/dist/lib/task/write.js +122 -3
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/decide.ts +5 -35
- package/lib/git/branch-worktree.ts +41 -0
- package/lib/internal/cli-route-inventory.ts +0 -2
- package/lib/internal/task-activity.ts +6 -34
- package/lib/internal/task-event.ts +4 -37
- package/lib/internal/task-finalization.ts +9 -2
- package/lib/internal/task-ledger.ts +5 -38
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +1 -4
- package/lib/internal/task-review.ts +5 -31
- package/lib/internal/task-verify.ts +3 -39
- package/lib/internal/task-warning.ts +3 -32
- package/lib/merge.ts +59 -10
- package/lib/platform/github-provider.ts +11 -18
- package/lib/platform/github-release-notes.ts +89 -14
- package/lib/platform/none-provider.ts +1 -1
- package/lib/platform/pr-summary.ts +4 -25
- package/lib/platform/provider-contract.ts +38 -12
- package/lib/platform/provider-validation.ts +83 -12
- package/lib/platform/release-notes.ts +11 -12
- package/lib/platform/verification-sync.ts +13 -53
- package/lib/sandbox/capture.ts +1 -1
- package/lib/sandbox/commands/create.ts +52 -1
- package/lib/sandbox/commands/enter.ts +1 -1
- package/lib/sandbox/commands/start.ts +1 -1
- package/lib/sandbox/config.ts +12 -0
- package/lib/sandbox/control/client.ts +5 -1
- package/lib/sandbox/control/executor.ts +13 -4
- package/lib/sandbox/control/lifecycle.ts +22 -13
- package/lib/sandbox/control/protocol.ts +3 -2
- package/lib/sandbox/removal.ts +23 -7
- package/lib/sandbox/task-resolver.ts +51 -2
- package/lib/task/activity-intent.ts +1 -2
- package/lib/task/artifact-operations.ts +58 -2
- package/lib/task/artifact-schema.ts +9 -7
- package/lib/task/control-authority.ts +14 -188
- package/lib/task/decision-intents.ts +2 -4
- package/lib/task/events.ts +8 -27
- package/lib/task/finalization-handoff.ts +104 -0
- package/lib/task/finalization.ts +120 -12
- package/lib/task/intermediate-cleanup.ts +9 -125
- package/lib/task/ledger-intents.ts +3 -6
- package/lib/task/lifecycle.ts +45 -13
- package/lib/task/local-artifact-finalization.ts +16 -35
- package/lib/task/resolve-ref.ts +3 -3
- package/lib/task/review-command.ts +2 -5
- package/lib/task/review-finalization.ts +1 -4
- package/lib/task/snapshot.ts +6 -6
- package/lib/task/verification-engine.ts +20 -10
- package/lib/task/verification.ts +1 -15
- package/lib/task/workflow-warning-intents.ts +2 -4
- package/lib/task/write.ts +116 -5
- package/package.json +1 -1
- package/templates/.agents/rules/evidence-reporting.en.md +1 -2
- package/templates/.agents/rules/evidence-reporting.zh-CN.md +1 -2
- package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -2
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -2
- package/templates/.agents/skills/archive-tasks/SKILL.en.md +2 -2
- package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +109 -6
- package/templates/.agents/skills/code-task/reference/report-template.en.md +6 -6
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +6 -6
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/create-release-note/SKILL.en.md +17 -11
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +17 -11
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
- package/dist/lib/internal/task-override.js +0 -138
- package/dist/lib/task/artifact-recovery.js +0 -476
- package/dist/lib/task/artifact-repair-intent.js +0 -181
- package/dist/lib/task/guard-override.js +0 -31
- package/dist/lib/task/human-override.js +0 -1259
- package/lib/internal/task-override.ts +0 -144
- package/lib/task/artifact-recovery.ts +0 -572
- package/lib/task/artifact-repair-intent.ts +0 -227
- package/lib/task/guard-override.ts +0 -50
- package/lib/task/human-override.ts +0 -1522
|
@@ -7,6 +7,7 @@ import { resolvePlatformProviderContext } from './context.ts';
|
|
|
7
7
|
import type { PlatformClient } from './context.ts';
|
|
8
8
|
import { providerError, providerOperationContext, providerStatus, unsupportedProviderOperation } from './provider-bridge.ts';
|
|
9
9
|
import { resourceIdentityNumber } from './resource-identity.ts';
|
|
10
|
+
import type { ReleaseNoteAuthor } from './provider-contract.ts';
|
|
10
11
|
import type { ResourceIdentity } from './resource-identity.ts';
|
|
11
12
|
|
|
12
13
|
type ReleaseNoteOptions = {
|
|
@@ -15,10 +16,9 @@ type ReleaseNoteOptions = {
|
|
|
15
16
|
client?: PlatformClient;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
|
-
type
|
|
19
|
-
type
|
|
20
|
-
type
|
|
21
|
-
type ReleaseNotePullRequest = { number: number; identity?: ResourceIdentity; title: string; body: string; url: string; mergedAt: string; labels: string[]; author: { login: string; bot: boolean } | null; closingIssues: ReleaseNoteIssue[] };
|
|
19
|
+
type ReleaseNoteCommit = { oid: string; title: string; url: string | null; pullRequestNumbers: number[]; authors: ReleaseNoteAuthor[] };
|
|
20
|
+
type ReleaseNoteIssue = { number: number; identity?: ResourceIdentity; title: string; url: string; author: ReleaseNoteAuthor | null };
|
|
21
|
+
type ReleaseNotePullRequest = { number: number; identity?: ResourceIdentity; title: string; body: string; url: string; mergedAt: string; labels: string[]; author: ReleaseNoteAuthor | null; closingIssues: ReleaseNoteIssue[] };
|
|
22
22
|
|
|
23
23
|
const unsupportedError = {
|
|
24
24
|
code: 'PLATFORM_RELEASE_NOTES_UNSUPPORTED',
|
|
@@ -180,15 +180,15 @@ async function releaseNoteContext(
|
|
|
180
180
|
if (!platform.ok) return { ...baseResult(platform.error.retryable ? 'blocked' : 'failed'), error: providerError(platform.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') };
|
|
181
181
|
const commits = commitFacts.map((commit) => {
|
|
182
182
|
const seen = new Set<string>();
|
|
183
|
-
const
|
|
184
|
-
|
|
183
|
+
const facts = platform.value.commits.find((item) => item.sha === commit.oid);
|
|
184
|
+
const authors = (facts?.authors || [])
|
|
185
185
|
.filter((actor) => {
|
|
186
|
-
const key = actor.
|
|
186
|
+
const key = actor.login || actor.name;
|
|
187
187
|
if (seen.has(key)) return false;
|
|
188
188
|
seen.add(key);
|
|
189
189
|
return true;
|
|
190
190
|
});
|
|
191
|
-
return { ...commit, authors };
|
|
191
|
+
return { ...commit, url: facts?.url || null, pullRequestNumbers: facts?.pullRequestNumbers || [], authors };
|
|
192
192
|
});
|
|
193
193
|
const pullRequests: ReleaseNotePullRequest[] = platform.value.mergedPullRequests
|
|
194
194
|
.map((item) => ({
|
|
@@ -199,14 +199,13 @@ async function releaseNoteContext(
|
|
|
199
199
|
url: item.displayUrl || '',
|
|
200
200
|
mergedAt: item.mergedAt || '',
|
|
201
201
|
labels: item.labels || [],
|
|
202
|
-
author: item.author
|
|
203
|
-
closingIssues:
|
|
202
|
+
author: item.author,
|
|
203
|
+
closingIssues: item.closingIssues.map((issue) => ({
|
|
204
204
|
number: issue.number || resourceIdentityNumber(issue.identity) || 0,
|
|
205
205
|
...(issue.identity ? { identity: issue.identity } : {}),
|
|
206
206
|
title: issue.title,
|
|
207
207
|
url: issue.displayUrl || '',
|
|
208
|
-
|
|
209
|
-
author: issue.author ? { login: issue.author.name || issue.author.id || '', bot: false } : null
|
|
208
|
+
author: issue.author
|
|
210
209
|
}))
|
|
211
210
|
}))
|
|
212
211
|
.sort((left, right) => left.number - right.number);
|
|
@@ -5,6 +5,8 @@ import path from "node:path";
|
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
|
|
8
|
+
import { resolveBranchWorktree } from "../git/branch-worktree.ts";
|
|
9
|
+
|
|
8
10
|
import { resolvePlatformProviderContext } from "./context.ts";
|
|
9
11
|
import { hasCheckedRequirement, resolveRequirementSection } from "./issue-metadata.ts";
|
|
10
12
|
import { requirementSectionAnchors } from "./issues.ts";
|
|
@@ -1054,7 +1056,10 @@ function computeExpectedInLabels(taskDir: any, repositoryLabels: string[], mappi
|
|
|
1054
1056
|
return { ok: false, type: "check_failed", message: "Task has no delivery_base_ref for in-label evidence" };
|
|
1055
1057
|
}
|
|
1056
1058
|
const branch = typeof task.metadata?.branch === "string" ? task.metadata.branch.trim() : "";
|
|
1057
|
-
const gitCwd = branch ?
|
|
1059
|
+
const gitCwd = branch ? resolveBranchWorktree(shared.repoRoot, branch) : taskDir;
|
|
1060
|
+
if (!gitCwd) {
|
|
1061
|
+
return { ok: false, type: "check_failed", message: `Task branch '${branch}' is not checked out in a usable worktree` };
|
|
1062
|
+
}
|
|
1058
1063
|
const changedFilesResult = gitText(["diff", `${baseRef}...HEAD`, "--name-only"], gitCwd);
|
|
1059
1064
|
if (!changedFilesResult.ok) {
|
|
1060
1065
|
return { ...changedFilesResult, type: "network_error" };
|
|
@@ -1082,25 +1087,6 @@ function computeExpectedInLabels(taskDir: any, repositoryLabels: string[], mappi
|
|
|
1082
1087
|
return { ok: true, labels: planned.target, mode: "mapped" };
|
|
1083
1088
|
}
|
|
1084
1089
|
|
|
1085
|
-
function worktreeForBranch(repositoryRoot: string, branch: string): string | null {
|
|
1086
|
-
try {
|
|
1087
|
-
const records = execFileSync("git", ["worktree", "list", "--porcelain"], {
|
|
1088
|
-
cwd: repositoryRoot,
|
|
1089
|
-
encoding: "utf8",
|
|
1090
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1091
|
-
}).trim().split(/\r?\n\r?\n/);
|
|
1092
|
-
const expectedRef = `refs/heads/${branch}`;
|
|
1093
|
-
for (const record of records) {
|
|
1094
|
-
const worktree = /^worktree (.+)$/m.exec(record)?.[1];
|
|
1095
|
-
const branchRef = /^branch (.+)$/m.exec(record)?.[1];
|
|
1096
|
-
if (worktree && branchRef === expectedRef) return worktree;
|
|
1097
|
-
}
|
|
1098
|
-
} catch {
|
|
1099
|
-
// Fall back to taskDir so ordinary in-repository task workspaces retain their behavior.
|
|
1100
|
-
}
|
|
1101
|
-
return null;
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
1090
|
function loadInLabelMapping(shared: VerificationShared): any {
|
|
1105
1091
|
const configPath = path.join(shared.repoRoot, ".agents", ".airc.json");
|
|
1106
1092
|
if (!fs.existsSync(configPath)) {
|
|
@@ -1141,42 +1127,16 @@ function resolvePrHeadSha(context: any): any {
|
|
|
1141
1127
|
return fallback();
|
|
1142
1128
|
}
|
|
1143
1129
|
|
|
1144
|
-
const
|
|
1145
|
-
if (!worktreeList.ok) {
|
|
1146
|
-
return fallback();
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
const matchedWorktree = findWorktreeForBranch(worktreeList.value, branch);
|
|
1130
|
+
const matchedWorktree = resolveBranchWorktree(context.taskDir, branch);
|
|
1150
1131
|
if (!matchedWorktree) {
|
|
1151
|
-
return
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
return fallback();
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
return headInWorktree;
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
function findWorktreeForBranch(porcelainOutput: any, branch: any): any {
|
|
1163
|
-
let currentWorktree = "";
|
|
1164
|
-
for (const rawLine of String(porcelainOutput || "").split("\n")) {
|
|
1165
|
-
const line = rawLine.trimEnd();
|
|
1166
|
-
if (line.startsWith("worktree ")) {
|
|
1167
|
-
currentWorktree = line.slice("worktree ".length).trim();
|
|
1168
|
-
continue;
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
if (line.startsWith("branch refs/heads/")) {
|
|
1172
|
-
const usedBranch = line.slice("branch refs/heads/".length).trim();
|
|
1173
|
-
if (usedBranch === branch && currentWorktree) {
|
|
1174
|
-
return currentWorktree;
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1132
|
+
return {
|
|
1133
|
+
ok: false,
|
|
1134
|
+
type: "check_failed",
|
|
1135
|
+
message: `Task branch '${branch}' is not checked out in a usable worktree`
|
|
1136
|
+
};
|
|
1177
1137
|
}
|
|
1178
1138
|
|
|
1179
|
-
return
|
|
1139
|
+
return gitText(["rev-parse", "HEAD"], matchedWorktree);
|
|
1180
1140
|
}
|
|
1181
1141
|
|
|
1182
1142
|
function interpolate(template: any, taskDir: any, artifactFile: any): any {
|
package/lib/sandbox/capture.ts
CHANGED
|
@@ -219,7 +219,7 @@ export async function runInSandbox(
|
|
|
219
219
|
allowRecreate: request.recreate,
|
|
220
220
|
recreate: async () => {
|
|
221
221
|
const { create } = await import('./commands/create.ts');
|
|
222
|
-
await create([request.taskRef, '--no-refresh']);
|
|
222
|
+
await create([request.taskRef, '--no-refresh'], { runProjectInitCommand: false });
|
|
223
223
|
}
|
|
224
224
|
})).container;
|
|
225
225
|
} else if (!found.running) {
|
|
@@ -79,6 +79,7 @@ import {
|
|
|
79
79
|
recoverSandboxControlReplacement,
|
|
80
80
|
readSandboxControlManifest
|
|
81
81
|
} from '../control/lifecycle.ts';
|
|
82
|
+
import { atomicWriteJson } from '../control/state.ts';
|
|
82
83
|
import { inspectSandboxControlContainer } from '../control/container-identity.ts';
|
|
83
84
|
import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
|
|
84
85
|
import { sandboxCoreBindMounts } from '../mounts.ts';
|
|
@@ -1008,7 +1009,27 @@ function readImageLabels(config: Pick<SandboxCreateConfig, 'imageName'> & Pick<S
|
|
|
1008
1009
|
]));
|
|
1009
1010
|
}
|
|
1010
1011
|
|
|
1011
|
-
|
|
1012
|
+
function readProjectInitCommandDigest(controlRoot: string): string | null {
|
|
1013
|
+
const markerPath = path.join(controlRoot, 'project-init.json');
|
|
1014
|
+
if (!fs.existsSync(markerPath)) return null;
|
|
1015
|
+
try {
|
|
1016
|
+
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8')) as { version?: unknown; commandSha256?: unknown };
|
|
1017
|
+
return marker.version === 1 && typeof marker.commandSha256 === 'string' && /^[a-f0-9]{64}$/.test(marker.commandSha256)
|
|
1018
|
+
? marker.commandSha256
|
|
1019
|
+
: null;
|
|
1020
|
+
} catch {
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function writeProjectInitCommandDigest(controlRoot: string, commandSha256: string): void {
|
|
1026
|
+
atomicWriteJson(path.join(controlRoot, 'project-init.json'), { version: 1, commandSha256 });
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
export async function create(
|
|
1030
|
+
args: string[],
|
|
1031
|
+
{ runProjectInitCommand = true }: { runProjectInitCommand?: boolean } = {}
|
|
1032
|
+
): Promise<void> {
|
|
1012
1033
|
const { values, positionals } = parseArgs({
|
|
1013
1034
|
args,
|
|
1014
1035
|
allowPositionals: true,
|
|
@@ -1079,6 +1100,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1079
1100
|
identity: target.workspace
|
|
1080
1101
|
});
|
|
1081
1102
|
const worktree = worktreeCandidates.find((candidate) => fs.existsSync(candidate)) ?? worktreeCandidates[0] ?? '';
|
|
1103
|
+
let worktreeCreated = false;
|
|
1082
1104
|
const shareCommon = shareCommonDir(effectiveConfig);
|
|
1083
1105
|
const shareBranch = shareBranchDir(effectiveConfig, branch);
|
|
1084
1106
|
const preparedDockerfile = prepareDockerfile(effectiveConfig, capabilityPlan.image);
|
|
@@ -1203,6 +1225,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1203
1225
|
toEnginePath(engine, worktree),
|
|
1204
1226
|
branch
|
|
1205
1227
|
]);
|
|
1228
|
+
worktreeCreated = true;
|
|
1206
1229
|
} else {
|
|
1207
1230
|
message(`Creating branch '${branch}' from '${baseBranch}'...`);
|
|
1208
1231
|
runEngineTaskCommand(engine, 'git', [
|
|
@@ -1215,6 +1238,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1215
1238
|
toEnginePath(engine, worktree),
|
|
1216
1239
|
baseBranch
|
|
1217
1240
|
]);
|
|
1241
|
+
worktreeCreated = true;
|
|
1218
1242
|
}
|
|
1219
1243
|
|
|
1220
1244
|
return `Worktree ready at ${worktree}`;
|
|
@@ -1272,6 +1296,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1272
1296
|
let replacementCutover: ReturnType<typeof beginSandboxControlReplacement> | null = null;
|
|
1273
1297
|
try {
|
|
1274
1298
|
const recoveryResult = await recoverSandboxControlReplacement(controlPaths.root, replacementLease);
|
|
1299
|
+
const previousProjectInitCommandDigest = readProjectInitCommandDigest(controlPaths.root);
|
|
1275
1300
|
const hadExistingControlRoot = hadControlRootBeforeAcquire || recoveryResult === 'restored';
|
|
1276
1301
|
const previousManifest = fs.existsSync(controlPaths.manifestPath)
|
|
1277
1302
|
? readSandboxControlManifest(controlPaths.manifestPath)
|
|
@@ -1728,6 +1753,32 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1728
1753
|
);
|
|
1729
1754
|
}
|
|
1730
1755
|
|
|
1756
|
+
const initCommand = effectiveConfig.initCommand;
|
|
1757
|
+
const initCommandDigest = initCommand === null ? null : createHash('sha256').update(initCommand).digest('hex');
|
|
1758
|
+
const shouldRunProjectInitCommand = runProjectInitCommand
|
|
1759
|
+
&& initCommand !== null
|
|
1760
|
+
&& initCommandDigest !== null
|
|
1761
|
+
&& (worktreeCreated || previousProjectInitCommandDigest !== initCommandDigest);
|
|
1762
|
+
let projectInitCompleted = !worktreeCreated
|
|
1763
|
+
&& initCommandDigest !== null
|
|
1764
|
+
&& previousProjectInitCommandDigest === initCommandDigest;
|
|
1765
|
+
|
|
1766
|
+
if (shouldRunProjectInitCommand && initCommand !== null) {
|
|
1767
|
+
try {
|
|
1768
|
+
runVerboseEngine(engine, 'docker', [
|
|
1769
|
+
'exec', '--workdir', '/workspace', container, 'bash', '-lc', initCommand
|
|
1770
|
+
]);
|
|
1771
|
+
projectInitCompleted = true;
|
|
1772
|
+
} catch (error) {
|
|
1773
|
+
throw new Error(
|
|
1774
|
+
`Project initialization failed for '${branch}': ${error instanceof Error ? error.message : String(error)}`
|
|
1775
|
+
);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
if (projectInitCompleted && initCommandDigest !== null) {
|
|
1779
|
+
writeProjectInitCommandDigest(controlPaths.root, initCommandDigest);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1731
1782
|
replacementLease.release();
|
|
1732
1783
|
replacementLeaseHeld = false;
|
|
1733
1784
|
return 'Container started';
|
|
@@ -169,7 +169,7 @@ export async function enter(args: string[]): Promise<number> {
|
|
|
169
169
|
allowRecreate: parsed.recreate,
|
|
170
170
|
recreate: async () => {
|
|
171
171
|
const { create } = await import('./create.ts');
|
|
172
|
-
await create([target.requestedRef, '--no-refresh']);
|
|
172
|
+
await create([target.requestedRef, '--no-refresh'], { runProjectInitCommand: false });
|
|
173
173
|
}
|
|
174
174
|
});
|
|
175
175
|
const container = ready.container;
|
|
@@ -86,7 +86,7 @@ export async function start(args: string[]): Promise<void> {
|
|
|
86
86
|
forceRecreate: parsed.recreate,
|
|
87
87
|
recreate: async () => {
|
|
88
88
|
const { create } = await import('./create.ts');
|
|
89
|
-
await create([target.requestedRef, '--no-refresh']);
|
|
89
|
+
await create([target.requestedRef, '--no-refresh'], { runProjectInitCommand: false });
|
|
90
90
|
}
|
|
91
91
|
});
|
|
92
92
|
const message = ready.path === 'healthy'
|
package/lib/sandbox/config.ts
CHANGED
|
@@ -18,6 +18,7 @@ const DEFAULTS = Object.freeze({
|
|
|
18
18
|
tools: ['agent-infra'],
|
|
19
19
|
refreshIntervalDays: 7,
|
|
20
20
|
dockerfile: null,
|
|
21
|
+
initCommand: null,
|
|
21
22
|
vm: {
|
|
22
23
|
cpu: null,
|
|
23
24
|
memory: null,
|
|
@@ -35,6 +36,7 @@ type SandboxConfigInput = {
|
|
|
35
36
|
customTools?: unknown;
|
|
36
37
|
refreshIntervalDays?: unknown;
|
|
37
38
|
dockerfile?: string | null;
|
|
39
|
+
initCommand?: unknown;
|
|
38
40
|
vm?: Record<string, unknown>;
|
|
39
41
|
};
|
|
40
42
|
|
|
@@ -66,6 +68,7 @@ export type SandboxConfig = {
|
|
|
66
68
|
delivery?: { remote: string; baseRef: string };
|
|
67
69
|
refreshIntervalDays: number;
|
|
68
70
|
dockerfile: string | null;
|
|
71
|
+
initCommand: string | null;
|
|
69
72
|
vm: SandboxVmConfig;
|
|
70
73
|
};
|
|
71
74
|
|
|
@@ -103,6 +106,7 @@ function cloneDefaults(): SandboxConfigInput & { vm: SandboxVmConfig; runtimes:
|
|
|
103
106
|
tools: [...DEFAULTS.tools],
|
|
104
107
|
refreshIntervalDays: DEFAULTS.refreshIntervalDays,
|
|
105
108
|
dockerfile: DEFAULTS.dockerfile,
|
|
109
|
+
initCommand: DEFAULTS.initCommand,
|
|
106
110
|
vm: { ...DEFAULTS.vm }
|
|
107
111
|
};
|
|
108
112
|
}
|
|
@@ -149,6 +153,13 @@ export function loadConfig({
|
|
|
149
153
|
? [...sandbox.runtimes]
|
|
150
154
|
: defaults.runtimes;
|
|
151
155
|
const dockerfile = typeof sandbox.dockerfile === 'string' ? sandbox.dockerfile : defaults.dockerfile ?? null;
|
|
156
|
+
if (sandbox.initCommand !== undefined &&
|
|
157
|
+
(typeof sandbox.initCommand !== 'string' || sandbox.initCommand.trim().length === 0)) {
|
|
158
|
+
throw new Error('sandbox.initCommand must be a non-empty string when configured');
|
|
159
|
+
}
|
|
160
|
+
const initCommand = typeof sandbox.initCommand === 'string'
|
|
161
|
+
? sandbox.initCommand
|
|
162
|
+
: null;
|
|
152
163
|
|
|
153
164
|
if (!dockerfile) {
|
|
154
165
|
let enginesNode: string | undefined;
|
|
@@ -200,6 +211,7 @@ export function loadConfig({
|
|
|
200
211
|
defaults.refreshIntervalDays
|
|
201
212
|
),
|
|
202
213
|
dockerfile,
|
|
214
|
+
initCommand,
|
|
203
215
|
vm: {
|
|
204
216
|
cpu: asPositiveNumberOrNull(sandbox.vm?.cpu) ?? defaults.vm.cpu,
|
|
205
217
|
memory: asPositiveNumberOrNull(sandbox.vm?.memory) ?? defaults.vm.memory,
|
|
@@ -393,6 +393,7 @@ export function requestSandboxControl(params: Readonly<{
|
|
|
393
393
|
|
|
394
394
|
export function requestSandboxTaskFinalization(params: Readonly<{
|
|
395
395
|
agent: string;
|
|
396
|
+
prepareHandoff?: (binding: Readonly<{ generation: string; requestId: string }>) => string;
|
|
396
397
|
channelDir?: string;
|
|
397
398
|
statusDir?: string;
|
|
398
399
|
token?: string;
|
|
@@ -403,15 +404,18 @@ export function requestSandboxTaskFinalization(params: Readonly<{
|
|
|
403
404
|
if (!agent) clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization agent is invalid', false);
|
|
404
405
|
const auth = authority(params);
|
|
405
406
|
const issuedAt = Date.now();
|
|
407
|
+
const id = randomUUID();
|
|
408
|
+
const handoffSha256 = (params.prepareHandoff ?? (() => '0'.repeat(64)))({ generation: auth.generation, requestId: id });
|
|
406
409
|
const request: SandboxTaskFinalizationRequest = {
|
|
407
410
|
version: 3,
|
|
408
|
-
id
|
|
411
|
+
id,
|
|
409
412
|
...auth,
|
|
410
413
|
issuedAt,
|
|
411
414
|
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
412
415
|
family: 'task-finalization',
|
|
413
416
|
operation: 'complete',
|
|
414
417
|
agent,
|
|
418
|
+
handoffSha256,
|
|
415
419
|
args: [],
|
|
416
420
|
controllerProcess: null,
|
|
417
421
|
controllerProof: null
|
|
@@ -2,6 +2,7 @@ import { spawn, type ChildProcess } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
5
6
|
import { getProcessStartTime } from '../../server/process-state.ts';
|
|
6
7
|
import {
|
|
7
8
|
createTask,
|
|
@@ -41,6 +42,11 @@ import {
|
|
|
41
42
|
openCodexControllerRegistration,
|
|
42
43
|
resolveCodexControllerBinding
|
|
43
44
|
} from './controller-registration.ts';
|
|
45
|
+
import { shareBranchDir } from '../constants.ts';
|
|
46
|
+
|
|
47
|
+
function handoffDirectory(manifest: SandboxControlManifest): string {
|
|
48
|
+
return shareBranchDir({ shareBase: path.join(homedir(), '.agent-infra', 'share', manifest.project) }, manifest.branch);
|
|
49
|
+
}
|
|
44
50
|
|
|
45
51
|
export type SandboxControlExecutionResult = {
|
|
46
52
|
exitCode: number;
|
|
@@ -373,10 +379,11 @@ async function executeRequestInner(
|
|
|
373
379
|
}
|
|
374
380
|
}
|
|
375
381
|
if (request.family === 'task-finalization') {
|
|
376
|
-
const
|
|
382
|
+
const parsed = parseTaskControlOperation(
|
|
377
383
|
'task-finalization', [manifest.taskId!, 'complete', '--agent', request.agent]
|
|
378
384
|
);
|
|
379
|
-
if (
|
|
385
|
+
if (parsed.family !== 'task-finalization') throw new Error('SANDBOX_CONTROL_FINALIZATION_OPERATION_INVALID');
|
|
386
|
+
const operation = { ...parsed, request: { ...parsed.request, handoffSha256: request.handoffSha256 } };
|
|
380
387
|
const result = dispatchTaskControlOperation(
|
|
381
388
|
createSandboxExecutorExecutionContext({
|
|
382
389
|
repoRoot: manifest.repoRoot,
|
|
@@ -385,7 +392,8 @@ async function executeRequestInner(
|
|
|
385
392
|
taskId: manifest.taskId!,
|
|
386
393
|
generation: manifest.generation,
|
|
387
394
|
manifestPath,
|
|
388
|
-
requestId: request.id
|
|
395
|
+
requestId: request.id,
|
|
396
|
+
handoffDirectory: handoffDirectory(manifest)
|
|
389
397
|
}),
|
|
390
398
|
operation
|
|
391
399
|
);
|
|
@@ -407,7 +415,8 @@ async function executeRequestInner(
|
|
|
407
415
|
taskId: manifest.taskId!,
|
|
408
416
|
generation: manifest.generation,
|
|
409
417
|
manifestPath,
|
|
410
|
-
requestId: request.id
|
|
418
|
+
requestId: request.id,
|
|
419
|
+
handoffDirectory: handoffDirectory(manifest)
|
|
411
420
|
});
|
|
412
421
|
const format = (value: unknown): SandboxControlExecutionResult => {
|
|
413
422
|
const result = value as { status?: string };
|
|
@@ -1298,6 +1298,10 @@ export async function quiesceSandboxControlRoot(
|
|
|
1298
1298
|
timeoutMs?: number;
|
|
1299
1299
|
timing?: SandboxControlTimingPolicy;
|
|
1300
1300
|
identityProbe?: ProcessIdentityProbe;
|
|
1301
|
+
executionProcessControl?: Readonly<{
|
|
1302
|
+
isAlive: (execution: SandboxControlExecution) => boolean;
|
|
1303
|
+
terminate: (execution: SandboxControlExecution, phase: 'graceful' | 'force') => boolean;
|
|
1304
|
+
}>;
|
|
1301
1305
|
} = {}
|
|
1302
1306
|
): Promise<'missing' | 'stale' | 'stopped'> {
|
|
1303
1307
|
const resolvedRoot = path.resolve(root);
|
|
@@ -1338,58 +1342,63 @@ export async function quiesceSandboxControlRoot(
|
|
|
1338
1342
|
if (!manifest && brokerLive) throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
1339
1343
|
|
|
1340
1344
|
const platform = options.platform ?? process.platform;
|
|
1345
|
+
const processControl = options.executionProcessControl ?? {
|
|
1346
|
+
isAlive: (execution: SandboxControlExecution) => executionAlive(execution, platform, identityProbe),
|
|
1347
|
+
terminate: (execution: SandboxControlExecution, phase: 'graceful' | 'force') => phase === 'graceful'
|
|
1348
|
+
? terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe })
|
|
1349
|
+
: terminateSandboxControlExecution(execution, {
|
|
1350
|
+
platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now(), identityProbe
|
|
1351
|
+
})
|
|
1352
|
+
};
|
|
1341
1353
|
let executions = manifest ? readExecutions(manifest) : [];
|
|
1342
1354
|
const owner: OwnerIdentity | null = brokerLive ? broker : statusLive ? statusOwner : null;
|
|
1343
1355
|
if (!owner) {
|
|
1344
1356
|
if (manifest && !broker && !status) throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
1345
1357
|
for (const execution of executions) {
|
|
1346
|
-
|
|
1358
|
+
processControl.terminate(execution, 'graceful');
|
|
1347
1359
|
}
|
|
1348
1360
|
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
1349
1361
|
while (Date.now() < softDeadlineAt) {
|
|
1350
|
-
if (!executions.some((execution) =>
|
|
1362
|
+
if (!executions.some((execution) => processControl.isAlive(execution))) break;
|
|
1351
1363
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1352
1364
|
}
|
|
1353
1365
|
if (manifest) executions = mergeExecutions(executions, readExecutions(manifest));
|
|
1354
1366
|
if (Date.now() >= deadlineAt) throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
|
|
1355
1367
|
for (const execution of executions) {
|
|
1356
|
-
if (!
|
|
1357
|
-
platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now(), identityProbe
|
|
1358
|
-
})) {
|
|
1368
|
+
if (!processControl.terminate(execution, 'force')) {
|
|
1359
1369
|
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
1360
1370
|
}
|
|
1361
1371
|
}
|
|
1362
|
-
if (executions.some((execution) =>
|
|
1372
|
+
if (executions.some((execution) => processControl.isAlive(execution))) {
|
|
1363
1373
|
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
1364
1374
|
}
|
|
1365
1375
|
return broker || status ? 'stale' : 'missing';
|
|
1366
1376
|
}
|
|
1367
1377
|
|
|
1368
|
-
const remaining = (): number => Math.max(0, deadlineAt - Date.now());
|
|
1369
1378
|
signalOwner(owner, platform, false);
|
|
1370
1379
|
for (const execution of executions) {
|
|
1371
|
-
|
|
1380
|
+
processControl.terminate(execution, 'graceful');
|
|
1372
1381
|
}
|
|
1373
1382
|
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
1374
1383
|
while (Date.now() < softDeadlineAt) {
|
|
1375
|
-
if (!ownerLive(owner, identityProbe) && !executions.some((execution) =>
|
|
1384
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => processControl.isAlive(execution))) break;
|
|
1376
1385
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1377
1386
|
}
|
|
1378
|
-
if (!ownerLive(owner, identityProbe) && !executions.some((execution) =>
|
|
1387
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => processControl.isAlive(execution))) {
|
|
1379
1388
|
return 'stopped';
|
|
1380
1389
|
}
|
|
1381
1390
|
if (manifest) executions = mergeExecutions(executions, readExecutions(manifest));
|
|
1382
1391
|
if (Date.now() >= deadlineAt) throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
|
|
1383
1392
|
for (const execution of executions) {
|
|
1384
|
-
|
|
1393
|
+
processControl.terminate(execution, 'force');
|
|
1385
1394
|
}
|
|
1386
1395
|
signalOwner(owner, platform, true);
|
|
1387
1396
|
while (Date.now() < deadlineAt) {
|
|
1388
|
-
if (!ownerLive(owner, identityProbe) && !executions.some((execution) =>
|
|
1397
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => processControl.isAlive(execution))) return 'stopped';
|
|
1389
1398
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1390
1399
|
}
|
|
1391
1400
|
if (ownerLive(owner, identityProbe)) throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
1392
|
-
if (executions.some((execution) =>
|
|
1401
|
+
if (executions.some((execution) => processControl.isAlive(execution))) throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
1393
1402
|
return 'stopped';
|
|
1394
1403
|
}
|
|
1395
1404
|
|
|
@@ -65,7 +65,7 @@ export type SandboxTaskCommandRequest = RequestBase & Readonly<{
|
|
|
65
65
|
family: 'task-lifecycle'; args: string[];
|
|
66
66
|
}>;
|
|
67
67
|
export type SandboxTaskFinalizationRequest = RequestBase & Readonly<{
|
|
68
|
-
family: 'task-finalization'; operation: 'complete'; agent: string; args: [];
|
|
68
|
+
family: 'task-finalization'; operation: 'complete'; agent: string; handoffSha256: string; args: [];
|
|
69
69
|
}>;
|
|
70
70
|
export type SandboxTaskCreateRequest = RequestBase & Readonly<{
|
|
71
71
|
family: 'task-create'; candidate: TaskCreateCandidateV1;
|
|
@@ -279,11 +279,12 @@ export function validateSandboxControlRequest(
|
|
|
279
279
|
return request as SandboxCodexControllerRequest;
|
|
280
280
|
}
|
|
281
281
|
if (request.family === 'task-finalization') {
|
|
282
|
-
const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'token', 'version'];
|
|
282
|
+
const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'handoffSha256', 'id', 'issuedAt', 'operation', 'token', 'version'];
|
|
283
283
|
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
|
|
284
284
|
|| request.operation !== 'complete'
|
|
285
285
|
|| !Array.isArray(request.args) || request.args.length !== 0
|
|
286
286
|
|| typeof request.agent !== 'string' || normalizeAgentToken(request.agent) !== request.agent
|
|
287
|
+
|| typeof request.handoffSha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(request.handoffSha256)
|
|
287
288
|
|| request.controllerProcess !== null || request.controllerProof !== null) {
|
|
288
289
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization request schema or authorization is invalid');
|
|
289
290
|
}
|
package/lib/sandbox/removal.ts
CHANGED
|
@@ -47,6 +47,7 @@ import { toolConfigDirCandidates, toolProjectDirCandidates } from './tools.ts';
|
|
|
47
47
|
import type { SandboxTool } from './tools.ts';
|
|
48
48
|
import { getProcessStartTime } from '../server/process-state.ts';
|
|
49
49
|
import { releaseStaleShortIdRegistry } from '../task/short-id.ts';
|
|
50
|
+
import { withRepositoryMutationLock } from '../task/task-execution-lock.ts';
|
|
50
51
|
import { fetchSandboxRows, type SandboxRow } from './commands/list-running.ts';
|
|
51
52
|
import {
|
|
52
53
|
formatIntermediateCleanupReport,
|
|
@@ -959,7 +960,10 @@ async function rmOne(
|
|
|
959
960
|
branch: string,
|
|
960
961
|
options: RmOneOptions = {}
|
|
961
962
|
): Promise<void> {
|
|
962
|
-
await
|
|
963
|
+
await withRepositoryMutationLock(
|
|
964
|
+
config.repoRoot,
|
|
965
|
+
() => runRmOneUnderRepositoryLock(config, tools, branch, options)
|
|
966
|
+
);
|
|
963
967
|
}
|
|
964
968
|
|
|
965
969
|
async function removeUncheckedSandbox(
|
|
@@ -998,7 +1002,16 @@ async function removeUncheckedSandbox(
|
|
|
998
1002
|
return null;
|
|
999
1003
|
}
|
|
1000
1004
|
|
|
1005
|
+
const worktreePermits = options.permits ?? await authorizeWorktrees(
|
|
1006
|
+
existingWorktrees,
|
|
1007
|
+
{ allowDirtyDiscard: true, assumeYes: Boolean(options.assumeYes) },
|
|
1008
|
+
{ ...options.prompt, interactive: options.interactive }
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1001
1011
|
for (const container of matchedContainers) runSafeEngine(engine, 'docker', ['rm', '-f', container]);
|
|
1012
|
+
if (!sandboxContainersRemoved(engine, matchedContainers)) {
|
|
1013
|
+
throw new Error(`SANDBOX_REMOVAL_CONTAINER_STILL_PRESENT: ${matchedContainers.join(', ')}`);
|
|
1014
|
+
}
|
|
1002
1015
|
for (const root of controlRoots) {
|
|
1003
1016
|
fs.rmSync(root, { recursive: true, force: true });
|
|
1004
1017
|
removeEmptyManagedParent(path.join(config.controlBase, config.project), root);
|
|
@@ -1011,12 +1024,15 @@ async function removeUncheckedSandbox(
|
|
|
1011
1024
|
for (const shell of shellConfigDirCandidates(config, effectiveBranch)) fs.rmSync(shell, { recursive: true, force: true });
|
|
1012
1025
|
if (shouldRemoveShare) fs.rmSync(sharePath, { recursive: true, force: true });
|
|
1013
1026
|
if (shouldRemoveWorktree) {
|
|
1014
|
-
for (const worktree of existingWorktrees)
|
|
1015
|
-
|
|
1027
|
+
for (const worktree of existingWorktrees) {
|
|
1028
|
+
const permit = worktreePermits.get(path.resolve(worktree));
|
|
1029
|
+
if (!permit) throw new Error(`SANDBOX_WORKTREE_REMOVAL_PERMIT_MISSING: ${worktree}`);
|
|
1030
|
+
removeWorktreeDir(config.repoRoot, config.worktreeBase, worktree, permit);
|
|
1031
|
+
}
|
|
1016
1032
|
}
|
|
1017
|
-
if (shouldDeleteBranch)
|
|
1018
|
-
|
|
1019
|
-
|
|
1033
|
+
if (shouldDeleteBranch) {
|
|
1034
|
+
assertBranchRemovalIdentity(config, effectiveBranch, worktreePermits);
|
|
1035
|
+
runSafe('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch]);
|
|
1020
1036
|
}
|
|
1021
1037
|
if (target.workspace.mode === 'task-bound') {
|
|
1022
1038
|
releaseStaleShortIdRegistry(config.repoRoot, target.workspace.taskId);
|
|
@@ -1045,7 +1061,7 @@ async function rmPurge(
|
|
|
1045
1061
|
tools: SandboxTool[],
|
|
1046
1062
|
prompt: PromptDependencies = {}
|
|
1047
1063
|
): Promise<void> {
|
|
1048
|
-
return rmPurgeCore(config, tools, prompt);
|
|
1064
|
+
return withRepositoryMutationLock(config.repoRoot, () => rmPurgeCore(config, tools, prompt));
|
|
1049
1065
|
}
|
|
1050
1066
|
|
|
1051
1067
|
async function rmPurgeCore(
|