@fitlab-ai/agent-infra 0.11.6 → 0.11.8
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 -2
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/agent-clients/config.js +11 -3
- package/dist/lib/defaults.json +6 -3
- package/dist/lib/git/primary-worktree.js +58 -0
- 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 +125 -7
- 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/control/server.js +6 -2
- 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 +11 -0
- package/dist/lib/sandbox/removal.js +58 -20
- package/dist/lib/sandbox/tools.js +29 -7
- package/dist/lib/server/config.js +7 -27
- package/dist/lib/server/daemon.js +4 -3
- package/dist/lib/server/process-control.js +4 -2
- package/dist/lib/server/runner.js +4 -3
- package/dist/lib/task/activity-intent.js +4 -3
- package/dist/lib/task/artifact-lifecycle.js +42 -10
- package/dist/lib/task/artifact-receipts.js +62 -22
- package/dist/lib/task/capabilities.js +22 -4
- package/dist/lib/task/completion-facts.js +56 -0
- package/dist/lib/task/create-service.js +11 -5
- package/dist/lib/task/create.js +60 -79
- package/dist/lib/task/events.js +268 -195
- package/dist/lib/task/finalization-artifacts.js +4 -1
- package/dist/lib/task/finalization-handoff.js +2 -1
- package/dist/lib/task/finalization.js +157 -24
- package/dist/lib/task/invalidation-command.js +4 -2
- package/dist/lib/task/invalidation.js +38 -9
- package/dist/lib/task/lifecycle.js +38 -2
- package/dist/lib/task/local-artifact-finalization.js +17 -3
- package/dist/lib/task/platform-operation-journal.js +152 -0
- package/dist/lib/task/platform-operation-recovery.js +459 -0
- package/dist/lib/task/qualification-audit.js +23 -105
- package/dist/lib/task/review-finalization.js +7 -0
- package/dist/lib/task/review-identity.js +69 -0
- package/dist/lib/task/sections.js +6 -5
- package/dist/lib/task/task-execution-lock.js +6 -3
- package/dist/lib/task/verification-engine.js +14 -63
- package/dist/lib/task/verification.js +35 -38
- package/dist/lib/task/workflow-warning-intents.js +7 -1
- 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/defaults.json +6 -3
- package/lib/git/primary-worktree.ts +62 -0
- 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 +132 -8
- 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/control/server.ts +5 -2
- 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 -0
- package/lib/sandbox/removal.ts +57 -19
- package/lib/sandbox/tool-types.ts +1 -1
- package/lib/sandbox/tools.ts +36 -7
- package/lib/server/config.ts +7 -29
- package/lib/server/daemon.ts +5 -3
- package/lib/server/process-control.ts +4 -2
- package/lib/server/runner.ts +7 -4
- package/lib/server/server.schema.json +0 -4
- package/lib/task/activity-intent.ts +4 -3
- package/lib/task/artifact-lifecycle.ts +35 -11
- package/lib/task/artifact-receipts.ts +55 -15
- package/lib/task/capabilities.ts +17 -3
- package/lib/task/completion-facts.ts +66 -0
- package/lib/task/create-service.ts +12 -6
- package/lib/task/create.ts +54 -69
- package/lib/task/events.ts +248 -182
- package/lib/task/finalization-artifacts.ts +4 -1
- package/lib/task/finalization-handoff.ts +2 -1
- package/lib/task/finalization.ts +154 -21
- package/lib/task/invalidation-command.ts +4 -2
- package/lib/task/invalidation.ts +35 -10
- package/lib/task/lifecycle.ts +35 -3
- package/lib/task/local-artifact-finalization.ts +13 -3
- package/lib/task/platform-operation-journal.ts +230 -0
- package/lib/task/platform-operation-recovery.ts +435 -0
- package/lib/task/qualification-audit.ts +21 -114
- package/lib/task/review-finalization.ts +7 -0
- package/lib/task/review-identity.ts +85 -0
- package/lib/task/sections.ts +6 -6
- package/lib/task/task-execution-lock.ts +7 -3
- package/lib/task/verification-engine.ts +13 -86
- package/lib/task/verification.ts +34 -38
- package/lib/task/workflow-warning-intents.ts +9 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +19 -17
- package/templates/.agents/README.zh-CN.md +18 -16
- package/templates/.agents/rules/README.en.md +2 -1
- package/templates/.agents/rules/README.zh-CN.md +2 -1
- package/templates/.agents/rules/decision-qualification.en.md +4 -3
- package/templates/.agents/rules/decision-qualification.zh-CN.md +4 -3
- package/templates/.agents/rules/task-management.en.md +12 -0
- package/templates/.agents/rules/task-management.zh-CN.md +12 -0
- package/templates/.agents/rules/validation-output.en.md +19 -0
- package/templates/.agents/rules/validation-output.zh-CN.md +19 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +6 -2
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/code-task/SKILL.en.md +5 -3
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -8
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -8
- 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-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -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/create-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- 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-analysis/SKILL.en.md +3 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -8
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -8
- package/templates/.agents/skills/review-code/SKILL.en.md +13 -12
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -12
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -3
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -3
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +23 -46
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +27 -48
- package/templates/.agents/skills/review-code/reference/report-template.en.md +8 -11
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +8 -11
- package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -2
- package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +3 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +5 -9
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +5 -9
- package/templates/.agents/skills/review-pr/SKILL.en.md +13 -11
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +13 -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/run-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +19 -6
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { normalizePullRequest } from './github-data.js';
|
|
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
|
import { parseTypedTaskFrontmatter } from '../task/frontmatter.js';
|
|
6
7
|
import { resolveTaskRef } from '../task/resolve-ref.js';
|
|
@@ -19,6 +20,7 @@ import { buildBoundFact, buildSkippedFact, factFrontmatterMutation, readPrDelive
|
|
|
19
20
|
import { providerError, providerOperationContext, providerStatus, providerResourceToken, providerResourceIdentity, resourceIdentity, unsupportedProviderOperation } from './provider-bridge.js';
|
|
20
21
|
import { isResourceIdentity, resourceIdentityEquals, resourceIdentityNumber, serializeResourceIdentity } from './resource-identity.js';
|
|
21
22
|
import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.js';
|
|
23
|
+
import { coordinatePlatformWrite } from './operation-coordinator.js';
|
|
22
24
|
function warningResultForPrimary(primaryResult) {
|
|
23
25
|
if (primaryResult === 'pr_created')
|
|
24
26
|
return 'pr_created_with_warnings';
|
|
@@ -52,6 +54,25 @@ function externalResult(base, overrides = {}) {
|
|
|
52
54
|
function withCreation(output, creation) {
|
|
53
55
|
return { ...output, creation };
|
|
54
56
|
}
|
|
57
|
+
async function journalPullRequestOperation(taskRef, options, intent, target, identityInput, execute) {
|
|
58
|
+
if (options.dryRun)
|
|
59
|
+
return execute();
|
|
60
|
+
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
61
|
+
if (!resolved.ok)
|
|
62
|
+
return execute();
|
|
63
|
+
const expectedDigest = createHash('sha256').update(JSON.stringify({ taskId: resolved.taskId, intent, identityInput })).digest('hex');
|
|
64
|
+
const operation = { kind: 'pull-request', target: intent.action === 'sync' ? `bound-pr:${resolved.taskId}` : target, expectedDigest };
|
|
65
|
+
const dependency = intent.action === 'sync' ? 'deferred' : 'required';
|
|
66
|
+
if (options.skipQueue)
|
|
67
|
+
return execute();
|
|
68
|
+
return coordinatePlatformWrite({
|
|
69
|
+
operation: { taskRef: resolved.taskId, cwd: resolved.repoRoot, ...operation, pullRequest: intent, dependency },
|
|
70
|
+
agent: options.agent,
|
|
71
|
+
execute,
|
|
72
|
+
block: (error) => result('blocked', resolved.taskId, null, null, { error }),
|
|
73
|
+
persistenceFailure: (error) => result('failed', resolved.taskId, null, null, { error })
|
|
74
|
+
});
|
|
75
|
+
}
|
|
55
76
|
const PRECONDITION_NOT_CREATED = {
|
|
56
77
|
kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false
|
|
57
78
|
};
|
|
@@ -681,7 +702,7 @@ function bindIdentity(base, pullRequest, agent, source, dryRun = false, expected
|
|
|
681
702
|
]
|
|
682
703
|
}, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
|
|
683
704
|
}
|
|
684
|
-
async function
|
|
705
|
+
async function bindPlatformPullRequestImpl(taskRef, options) {
|
|
685
706
|
const base = await resolvedContext(taskRef, options);
|
|
686
707
|
if (!base.ok)
|
|
687
708
|
return base.output;
|
|
@@ -723,6 +744,10 @@ async function bindPlatformPullRequest(taskRef, options) {
|
|
|
723
744
|
operations: [{ name: 'task:bind-pr', status: options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', reasonCode: null }], error: null
|
|
724
745
|
});
|
|
725
746
|
}
|
|
747
|
+
async function bindPlatformPullRequest(taskRef, options) {
|
|
748
|
+
const intent = { action: 'bind', prToken: String(options.pr) };
|
|
749
|
+
return journalPullRequestOperation(taskRef, options, intent, `pr:${options.pr}`, { pr: String(options.pr) }, () => bindPlatformPullRequestImpl(taskRef, options));
|
|
750
|
+
}
|
|
726
751
|
function externalEvidenceNote(issueIdentity, source, pullRequest) {
|
|
727
752
|
return [
|
|
728
753
|
`authorization=${source}`,
|
|
@@ -915,7 +940,7 @@ async function resolveExternalPullRequest(taskRef, options) {
|
|
|
915
940
|
error: null
|
|
916
941
|
}), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
917
942
|
}
|
|
918
|
-
async function
|
|
943
|
+
async function createPlatformPullRequestImpl(taskRef, options) {
|
|
919
944
|
const base = await resolvedContext(taskRef, options);
|
|
920
945
|
if (!base.ok)
|
|
921
946
|
return withCreation(base.output, PRECONDITION_NOT_CREATED);
|
|
@@ -946,6 +971,67 @@ async function createPlatformPullRequest(taskRef, options) {
|
|
|
946
971
|
}), PRECONDITION_NOT_CREATED);
|
|
947
972
|
}
|
|
948
973
|
}
|
|
974
|
+
async function recoverCreatedPullRequest(taskRef, options) {
|
|
975
|
+
const base = await resolvedContext(taskRef, options);
|
|
976
|
+
if (!base.ok)
|
|
977
|
+
return base.output;
|
|
978
|
+
if (!base.issueIdentity)
|
|
979
|
+
return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
980
|
+
error: { code: 'PLATFORM_OPERATION_ISSUE_REQUIRED', message: 'Pull-request recovery requires a bound Issue identity', retryable: false }
|
|
981
|
+
});
|
|
982
|
+
const head = base.provider.changeRequests?.verifyHead
|
|
983
|
+
? await base.provider.changeRequests.verifyHead({ context: providerOperationContext(base.loadedContext), head: options.head })
|
|
984
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.verifyHead');
|
|
985
|
+
if (!head.ok)
|
|
986
|
+
return providerPullRequestError(base, head.error, base.prNumber);
|
|
987
|
+
if (base.prIdentity) {
|
|
988
|
+
const inspected = await inspectExternalPullRequest(base, base.prIdentity);
|
|
989
|
+
if (!inspected.pullRequest || inspected.status === 'failed' || inspected.status === 'blocked'
|
|
990
|
+
|| inspected.pullRequest.base.repository !== base.context.platform.repository
|
|
991
|
+
|| inspected.pullRequest.base.ref !== options.base || inspected.pullRequest.head.ref !== options.head
|
|
992
|
+
|| inspected.pullRequest.head.sha !== head.value.sha) {
|
|
993
|
+
return result('blocked', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
994
|
+
error: inspected.error ?? { code: 'PLATFORM_OPERATION_PR_NOT_CONFIRMED', message: 'Bound pull request does not match the pending create intent', retryable: true }
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
return inspected;
|
|
998
|
+
}
|
|
999
|
+
const candidates = base.provider.changeRequests?.listClosing
|
|
1000
|
+
? await base.provider.changeRequests.listClosing({ context: providerOperationContext(base.loadedContext), issue: base.issueIdentity })
|
|
1001
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.listClosing');
|
|
1002
|
+
if (!candidates.ok)
|
|
1003
|
+
return providerPullRequestError(base, candidates.error, null);
|
|
1004
|
+
const matches = candidates.value.map((candidate) => normalizeProviderPullRequest(candidate, base.context.platform.repository, candidate.number || 0))
|
|
1005
|
+
.filter((candidate) => candidate.base.repository === base.context.platform.repository
|
|
1006
|
+
&& candidate.base.ref === options.base
|
|
1007
|
+
&& candidate.head.repository === base.context.platform.repository
|
|
1008
|
+
&& candidate.head.ref === options.head
|
|
1009
|
+
&& candidate.head.sha === head.value.sha);
|
|
1010
|
+
if (matches.length !== 1 || !matches[0]?.number)
|
|
1011
|
+
return result('blocked', base.resolved.taskId, base.issueNumber, null, {
|
|
1012
|
+
error: { code: matches.length > 1 ? 'PLATFORM_OPERATION_PR_AMBIGUOUS' : 'PLATFORM_OPERATION_PR_NOT_CONFIRMED',
|
|
1013
|
+
message: 'Pending pull-request creation does not resolve to one exact repository/base/head identity', retryable: true }
|
|
1014
|
+
});
|
|
1015
|
+
const bound = await bindPlatformPullRequest(base.resolved.taskId, { agent: options.agent, cwd: base.resolved.repoRoot, pr: matches[0].number });
|
|
1016
|
+
if (!bound.pullRequest || (bound.status !== 'applied' && bound.status !== 'no-op') || bound.error)
|
|
1017
|
+
return bound;
|
|
1018
|
+
if (bound.pullRequest.base.repository !== base.context.platform.repository || bound.pullRequest.base.ref !== options.base
|
|
1019
|
+
|| bound.pullRequest.head.repository !== base.context.platform.repository || bound.pullRequest.head.ref !== options.head
|
|
1020
|
+
|| bound.pullRequest.head.sha !== head.value.sha)
|
|
1021
|
+
return result('blocked', base.resolved.taskId, base.issueNumber, bound.task.prNumber, {
|
|
1022
|
+
error: { code: 'PLATFORM_OPERATION_PR_NOT_CONFIRMED', message: 'Bound pull request changed while recovering the create intent', retryable: true }
|
|
1023
|
+
});
|
|
1024
|
+
return bound;
|
|
1025
|
+
}
|
|
1026
|
+
async function createPlatformPullRequest(taskRef, options) {
|
|
1027
|
+
const intent = { action: 'create', baseRef: options.base, headRef: options.head };
|
|
1028
|
+
const identityInput = {
|
|
1029
|
+
title: createHash('sha256').update(options.title).digest('hex'),
|
|
1030
|
+
body: createHash('sha256').update(options.body).digest('hex'),
|
|
1031
|
+
draft: options.draft === true
|
|
1032
|
+
};
|
|
1033
|
+
return journalPullRequestOperation(taskRef, options, intent, `head:${options.head}:base:${options.base}`, identityInput, () => createPlatformPullRequestImpl(taskRef, options));
|
|
1034
|
+
}
|
|
949
1035
|
async function createExternalPullRequest(taskRef, options, base) {
|
|
950
1036
|
if (!base.fact)
|
|
951
1037
|
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
@@ -974,9 +1060,35 @@ async function createExternalPullRequest(taskRef, options, base) {
|
|
|
974
1060
|
}
|
|
975
1061
|
if (base.prIdentity) {
|
|
976
1062
|
const inspected = await inspectExternalPullRequest(base, base.prIdentity);
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1063
|
+
if (inspected.status === 'failed' || inspected.status === 'blocked') {
|
|
1064
|
+
return withCreation(inspected, PRECONDITION_NOT_CREATED);
|
|
1065
|
+
}
|
|
1066
|
+
const deliveredHeadSha = typeof base.frontmatter.delivery_remote_head === 'string'
|
|
1067
|
+
? base.frontmatter.delivery_remote_head
|
|
1068
|
+
: undefined;
|
|
1069
|
+
const expectedHeadSha = verifiedHeadSha ?? deliveredHeadSha;
|
|
1070
|
+
if (!expectedHeadSha)
|
|
1071
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1072
|
+
platform: base.context.platform,
|
|
1073
|
+
capabilities: base.context.capabilities,
|
|
1074
|
+
resource: { kind: 'pull-request', number: base.prNumber, identity: base.prIdentity },
|
|
1075
|
+
pullRequest: inspected.pullRequest,
|
|
1076
|
+
error: { code: 'PR_HEAD_UNVERIFIED', message: 'Unable to verify the bound pull request head SHA', retryable: false }
|
|
1077
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1078
|
+
const identity = validateWriterIdentity(base, inspected.pullRequest ?? null, {
|
|
1079
|
+
expectedHead: options.head,
|
|
1080
|
+
expectedHeadSha,
|
|
1081
|
+
errorCode: 'PR_BIND_IDENTITY_MISMATCH'
|
|
1082
|
+
});
|
|
1083
|
+
if (!identity.ok)
|
|
1084
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1085
|
+
platform: base.context.platform,
|
|
1086
|
+
capabilities: base.context.capabilities,
|
|
1087
|
+
resource: { kind: 'pull-request', number: base.prNumber, identity: base.prIdentity },
|
|
1088
|
+
pullRequest: inspected.pullRequest,
|
|
1089
|
+
error: identity.error
|
|
1090
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1091
|
+
return withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
|
|
980
1092
|
}
|
|
981
1093
|
if (options.dryRun)
|
|
982
1094
|
return withCreation(result('planned', base.resolved.taskId, base.issueNumber, null, {
|
|
@@ -1078,7 +1190,7 @@ async function createExternalPullRequest(taskRef, options, base) {
|
|
|
1078
1190
|
error: null
|
|
1079
1191
|
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1080
1192
|
}
|
|
1081
|
-
async function
|
|
1193
|
+
async function syncPlatformPullRequestImpl(taskRef, options) {
|
|
1082
1194
|
const warningResult = warningResultForPrimary(options.primaryResult);
|
|
1083
1195
|
const softenFailure = (output) => {
|
|
1084
1196
|
const authorityError = output.error?.code.startsWith('IN_LABEL_SYNC') || output.error?.code.startsWith('PR_IDENTITY');
|
|
@@ -1262,6 +1374,12 @@ async function syncPlatformPullRequest(taskRef, options) {
|
|
|
1262
1374
|
}));
|
|
1263
1375
|
}
|
|
1264
1376
|
}
|
|
1377
|
+
async function syncPlatformPullRequest(taskRef, options) {
|
|
1378
|
+
const intent = { action: 'sync', metadata: options.metadata === true, closingIssue: options.closingIssue === true };
|
|
1379
|
+
return journalPullRequestOperation(taskRef, options, intent, `bound-pr:${taskRef}`, {
|
|
1380
|
+
metadata: options.metadata === true, closingIssue: options.closingIssue === true
|
|
1381
|
+
}, () => syncPlatformPullRequestImpl(taskRef, options));
|
|
1382
|
+
}
|
|
1265
1383
|
function readProjectPrFlow(repoRoot) {
|
|
1266
1384
|
try {
|
|
1267
1385
|
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
|
|
@@ -1360,6 +1478,6 @@ async function skipPlatformPullRequestFact(taskRef, options) {
|
|
|
1360
1478
|
});
|
|
1361
1479
|
}
|
|
1362
1480
|
}
|
|
1363
|
-
export { bindPlatformPullRequest, createPlatformPullRequest, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, resolveExternalPullRequest, skipPlatformPullRequestFact, selectExternalPullRequest, selectPullRequest, syncPlatformPullRequest, syncPlatformPullRequestInLabels };
|
|
1481
|
+
export { bindPlatformPullRequest, createPlatformPullRequest, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, recoverCreatedPullRequest, resolveExternalPullRequest, skipPlatformPullRequestFact, selectExternalPullRequest, selectPullRequest, syncPlatformPullRequest, syncPlatformPullRequestInLabels };
|
|
1364
1482
|
export { warningResultForPrimary };
|
|
1365
1483
|
//# sourceMappingURL=pull-requests.js.map
|
|
@@ -22,6 +22,7 @@ import { atomicWriteJson } from '../control/state.js';
|
|
|
22
22
|
import { inspectSandboxControlContainer } from '../control/container-identity.js';
|
|
23
23
|
import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.js';
|
|
24
24
|
import { sandboxCoreBindMounts } from '../mounts.js';
|
|
25
|
+
import { materializeSandboxGitMetadata } from '../git-metadata.js';
|
|
25
26
|
import { assertSandboxTaskSource, finalizeSandboxControlManifest, materializeSandboxControl, materializeSandboxWorkspaceView, prepareSandboxWorkspaceMountTargets, sandboxControlPaths, } from '../workspace-view.js';
|
|
26
27
|
import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.js';
|
|
27
28
|
import { validateSelinuxDisableEnv } from '../engines/selinux.js';
|
|
@@ -630,7 +631,8 @@ function runSandboxAuthoritySafeCommand(authority, args) {
|
|
|
630
631
|
// tmpfs. containerMount is an in-container path, so it is NOT engine-converted.
|
|
631
632
|
export function buildTmpfsRunArgs(containerMount, tmpfs) {
|
|
632
633
|
const size = tmpfs.size ?? '512m';
|
|
633
|
-
|
|
634
|
+
const options = ['rw', ...(tmpfs.exec ? ['exec'] : []), `size=${size}`];
|
|
635
|
+
return ['--tmpfs', `${containerMount}:${options.join(',')}`];
|
|
634
636
|
}
|
|
635
637
|
export function buildImage(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, runVerboseFn = runVerboseEngine, env = process.env, refresh = false, lastRefresh, buildProxyArgs = [], buildEnv } = {}) {
|
|
636
638
|
const selectedEngine = engine ?? detectEngine({ engine: config.engine });
|
|
@@ -1050,6 +1052,7 @@ export async function create(args, { runProjectInitCommand = true } = {}) {
|
|
|
1050
1052
|
engine,
|
|
1051
1053
|
replacementLease,
|
|
1052
1054
|
});
|
|
1055
|
+
const gitMetadata = materializeSandboxGitMetadata(worktree, control.channelDir);
|
|
1053
1056
|
hostShellConfig = prepareHostShellConfig({
|
|
1054
1057
|
home: effectiveConfig.home,
|
|
1055
1058
|
project: effectiveConfig.project,
|
|
@@ -1057,6 +1060,8 @@ export async function create(args, { runProjectInitCommand = true } = {}) {
|
|
|
1057
1060
|
repoRoot: effectiveConfig.repoRoot
|
|
1058
1061
|
});
|
|
1059
1062
|
const coreVolumes = sandboxCoreBindMounts(effectiveConfig, branch, {
|
|
1063
|
+
gitCommonDir: gitMetadata.commonDir,
|
|
1064
|
+
worktreeGitFile: gitMetadata.worktreeGitFile,
|
|
1060
1065
|
worktree,
|
|
1061
1066
|
shellConfigHostDir: hostShellConfig.hostDir,
|
|
1062
1067
|
workspaceViewRoot: workspaceView.root,
|
|
@@ -1153,8 +1158,6 @@ export async function create(args, { runProjectInitCommand = true } = {}) {
|
|
|
1153
1158
|
`${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
|
|
1154
1159
|
...coreVolumes,
|
|
1155
1160
|
...buildClipboardVolumeArgs(engine, effectiveConfig.home),
|
|
1156
|
-
'-v',
|
|
1157
|
-
volumeArg(engine, path.join(effectiveConfig.repoRoot, '.git'), `${toEnginePath(engine, effectiveConfig.repoRoot)}/.git`),
|
|
1158
1161
|
...dotfilesMount,
|
|
1159
1162
|
...toolVolumes,
|
|
1160
1163
|
...tmpfsArgs,
|
|
@@ -15,8 +15,9 @@ display-only row number; the 'SHORT' column shows the active task short
|
|
|
15
15
|
id bound to each container's branch (via
|
|
16
16
|
.agents/workspace/active/.short-ids.json), or '-' if no active task is
|
|
17
17
|
bound. Pass the SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
|
|
18
|
-
A '-' means no active task
|
|
19
|
-
|
|
18
|
+
A SHORT value of '-' means this container has no active task short id and may
|
|
19
|
+
be eligible for "ai sandbox rm --unbound" cleanup. Use a full TASK-id to
|
|
20
|
+
remove a specific task-bound sandbox.
|
|
20
21
|
|
|
21
22
|
Use "ai sandbox show <ref>" for a single sandbox's worktree and per-tool
|
|
22
23
|
state paths.`;
|
|
@@ -63,7 +64,7 @@ export function ls(args = []) {
|
|
|
63
64
|
process.stdout.write(` ${row.name}: health=${controlStatus?.state ?? 'unavailable'} task-view=${controlStatus?.taskView.state ?? 'unavailable'}\n`);
|
|
64
65
|
}
|
|
65
66
|
if (tableRows.some((r) => r.shortId === '-')) {
|
|
66
|
-
process.stdout.write(` SHORT '-' = no active task
|
|
67
|
+
process.stdout.write(` SHORT '-' = no active task short id is displayed; --unbound may clean this sandbox if its branch is not tied to an active task.\n`);
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
}
|
|
@@ -8,7 +8,7 @@ import { fetchSandboxRows } from './list-running.js';
|
|
|
8
8
|
import { rmOne, rmPurge, rmUnbound } from '../removal.js';
|
|
9
9
|
const USAGE = `Usage:
|
|
10
10
|
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
|
|
11
|
-
ai sandbox rm --unbound [--dry-run] [--yes] Remove
|
|
11
|
+
ai sandbox rm --unbound [--dry-run] [--yes] Remove sandboxes not bound to an active task short id
|
|
12
12
|
ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
|
|
13
13
|
function resolveMissingTaskSandboxBranch(taskId, config) {
|
|
14
14
|
const engine = detectEngine(config);
|
|
@@ -50,10 +50,22 @@ function cloneDefaults() {
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
function resolveSandboxToolIds(sandbox, defaults) {
|
|
53
|
-
if (
|
|
54
|
-
return
|
|
53
|
+
if (sandbox.tools === undefined) {
|
|
54
|
+
return [...defaults];
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
if (typeof sandbox.tools !== 'object' || sandbox.tools === null || Array.isArray(sandbox.tools)) {
|
|
57
|
+
throw new Error('sandbox.tools must be an object when configured');
|
|
58
|
+
}
|
|
59
|
+
const tools = sandbox.tools;
|
|
60
|
+
if (!Array.isArray(tools.ids)) {
|
|
61
|
+
throw new Error('sandbox.tools.ids must be an array');
|
|
62
|
+
}
|
|
63
|
+
return tools.ids.map((id, index) => {
|
|
64
|
+
if (typeof id !== 'string') {
|
|
65
|
+
throw new Error(`sandbox.tools.ids[${index}] must be a string`);
|
|
66
|
+
}
|
|
67
|
+
return id;
|
|
68
|
+
});
|
|
57
69
|
}
|
|
58
70
|
export function loadConfig({ platformFn = platform, writeStderr = (chunk) => process.stderr.write(chunk) } = {}) {
|
|
59
71
|
const repoRoot = detectRepoRoot();
|
|
@@ -100,7 +112,13 @@ export function loadConfig({ platformFn = platform, writeStderr = (chunk) => pro
|
|
|
100
112
|
' Update "sandbox.runtimes" in .agents/.airc.json (e.g. "node22"), or relax "engines.node".\n'));
|
|
101
113
|
}
|
|
102
114
|
}
|
|
103
|
-
const
|
|
115
|
+
const toolsConfig = sandbox.tools && typeof sandbox.tools === 'object' && !Array.isArray(sandbox.tools)
|
|
116
|
+
? sandbox.tools
|
|
117
|
+
: undefined;
|
|
118
|
+
if (sandbox.customTools !== undefined) {
|
|
119
|
+
throw new Error('sandbox.customTools is no longer supported; use sandbox.tools.definitions');
|
|
120
|
+
}
|
|
121
|
+
const customTools = parseCustomTools(toolsConfig?.definitions, { home });
|
|
104
122
|
const tools = resolveSandboxToolIds(sandbox, defaults.tools);
|
|
105
123
|
return {
|
|
106
124
|
repoRoot,
|
|
@@ -171,13 +171,17 @@ function taskCreateDomainEvidence(manifest, output) {
|
|
|
171
171
|
? output.task : null;
|
|
172
172
|
const taskId = typeof task?.id === 'string' ? task.id : null;
|
|
173
173
|
const shortId = typeof task?.shortId === 'string' ? task.shortId : null;
|
|
174
|
-
|
|
174
|
+
const state = task?.state;
|
|
175
|
+
if (!taskId || !['active', 'blocked', 'completed', 'archive'].includes(String(state)))
|
|
176
|
+
return { consistent: false };
|
|
177
|
+
if (state === 'active' ? !shortId : output.status !== 'no-op' || shortId !== null)
|
|
175
178
|
return { consistent: false };
|
|
176
179
|
try {
|
|
177
180
|
const resolved = resolveTaskRef(taskId, { repoRoot: manifest.repoRoot });
|
|
178
181
|
const shortIds = loadShortIdByTaskId(manifest.repoRoot);
|
|
179
182
|
return {
|
|
180
|
-
consistent: resolved.ok && resolved.taskId === taskId && resolved.state ===
|
|
183
|
+
consistent: resolved.ok && resolved.taskId === taskId && resolved.state === state
|
|
184
|
+
&& (state === 'active' ? shortIds.get(taskId) === shortId : !shortIds.has(taskId)),
|
|
181
185
|
taskId,
|
|
182
186
|
shortId
|
|
183
187
|
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export const SANDBOX_GIT_COMMON_DIR = '/run/agent-infra/git';
|
|
5
|
+
function gitText(worktreeRoot, args) {
|
|
6
|
+
const env = { ...process.env };
|
|
7
|
+
for (const key of [
|
|
8
|
+
'GIT_DIR',
|
|
9
|
+
'GIT_COMMON_DIR',
|
|
10
|
+
'GIT_WORK_TREE',
|
|
11
|
+
'GIT_INDEX_FILE',
|
|
12
|
+
'GIT_PREFIX',
|
|
13
|
+
'GIT_NAMESPACE',
|
|
14
|
+
'GIT_OBJECT_DIRECTORY',
|
|
15
|
+
'GIT_CEILING_DIRECTORIES',
|
|
16
|
+
'GIT_DISCOVERY_ACROSS_FILESYSTEM'
|
|
17
|
+
]) {
|
|
18
|
+
delete env[key];
|
|
19
|
+
}
|
|
20
|
+
return execFileSync('git', ['-C', worktreeRoot, ...args], {
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
23
|
+
env
|
|
24
|
+
}).trim();
|
|
25
|
+
}
|
|
26
|
+
export function resolveSandboxGitMetadata(worktreeRoot, controlDir) {
|
|
27
|
+
const commonDir = fs.realpathSync.native(gitText(worktreeRoot, [
|
|
28
|
+
'rev-parse', '--path-format=absolute', '--git-common-dir'
|
|
29
|
+
]));
|
|
30
|
+
const gitDir = fs.realpathSync.native(gitText(worktreeRoot, ['rev-parse', '--absolute-git-dir']));
|
|
31
|
+
const relativeGitDir = path.relative(commonDir, gitDir);
|
|
32
|
+
if (path.isAbsolute(relativeGitDir) || relativeGitDir.startsWith(`..${path.sep}`) || relativeGitDir === '..') {
|
|
33
|
+
throw new Error('SANDBOX_GIT_METADATA_INVALID: worktree git directory is outside its common directory');
|
|
34
|
+
}
|
|
35
|
+
const containerGitDir = relativeGitDir === '.'
|
|
36
|
+
? SANDBOX_GIT_COMMON_DIR
|
|
37
|
+
: path.posix.join(SANDBOX_GIT_COMMON_DIR, ...relativeGitDir.split(path.sep));
|
|
38
|
+
return {
|
|
39
|
+
commonDir,
|
|
40
|
+
worktreeGitFile: path.join(controlDir, 'workspace.git'),
|
|
41
|
+
worktreeGitFileContent: `gitdir: ${containerGitDir}\n`
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function materializeSandboxGitMetadata(worktreeRoot, controlDir) {
|
|
45
|
+
const metadata = resolveSandboxGitMetadata(worktreeRoot, controlDir);
|
|
46
|
+
fs.mkdirSync(controlDir, { recursive: true });
|
|
47
|
+
fs.writeFileSync(metadata.worktreeGitFile, metadata.worktreeGitFileContent, 'utf8');
|
|
48
|
+
return metadata;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=git-metadata.js.map
|
|
@@ -19,7 +19,7 @@ Commands:
|
|
|
19
19
|
rm <branch | TASK-id> | --unbound | --purge
|
|
20
20
|
Remove one sandbox; use a full TASK-id for task-bound
|
|
21
21
|
cleanup and a branch for branch-only cleanup. --unbound
|
|
22
|
-
removes
|
|
22
|
+
removes sandboxes not bound to an active task short id.
|
|
23
23
|
start [--recreate] <branch | TASK-id | N>
|
|
24
24
|
Verify or recover an existing sandbox container;
|
|
25
25
|
optionally replace only the container on failure
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { shareBranchDir, shareCommonDir, shellConfigDirCandidates, worktreeDirCandidates } from './constants.js';
|
|
3
3
|
import { sandboxWorkspaceViewStatePaths } from './workspace-view.js';
|
|
4
|
+
import { SANDBOX_GIT_COMMON_DIR } from './git-metadata.js';
|
|
4
5
|
export function sandboxCoreBindMounts(config, branch, overrides) {
|
|
5
6
|
const taskSources = overrides.taskSources ?? [];
|
|
6
7
|
const taskBound = Boolean(taskSources.length > 0 && overrides.taskId);
|
|
@@ -15,7 +16,17 @@ export function sandboxCoreBindMounts(config, branch, overrides) {
|
|
|
15
16
|
containerPath: path.posix.join('/workspace/.agents/workspace', state),
|
|
16
17
|
readOnly: true
|
|
17
18
|
});
|
|
19
|
+
if (Boolean(overrides.gitCommonDir) !== Boolean(overrides.worktreeGitFile)) {
|
|
20
|
+
throw new Error('SANDBOX_GIT_METADATA_INCOMPLETE');
|
|
21
|
+
}
|
|
22
|
+
const gitCommonMount = overrides.gitCommonDir
|
|
23
|
+
? { hostPaths: [overrides.gitCommonDir], containerPath: SANDBOX_GIT_COMMON_DIR, readOnly: false }
|
|
24
|
+
: null;
|
|
25
|
+
const worktreeGitFileMount = overrides.worktreeGitFile
|
|
26
|
+
? { hostPaths: [overrides.worktreeGitFile], containerPath: '/workspace/.git', readOnly: true }
|
|
27
|
+
: null;
|
|
18
28
|
const mounts = [
|
|
29
|
+
...(gitCommonMount ? [gitCommonMount] : []),
|
|
19
30
|
{
|
|
20
31
|
hostPaths: overrides.worktree
|
|
21
32
|
? [overrides.worktree]
|
|
@@ -23,6 +34,7 @@ export function sandboxCoreBindMounts(config, branch, overrides) {
|
|
|
23
34
|
containerPath: '/workspace',
|
|
24
35
|
readOnly: false
|
|
25
36
|
},
|
|
37
|
+
...(worktreeGitFileMount ? [worktreeGitFileMount] : []),
|
|
26
38
|
...workspaceMounts,
|
|
27
39
|
];
|
|
28
40
|
if (taskBound) {
|
|
@@ -9,6 +9,7 @@ import { isAgentClientId } from '../agent-clients/types.js';
|
|
|
9
9
|
import { createSandboxCapabilityPlan } from './agent-client-reconciler.js';
|
|
10
10
|
import { toEnginePath } from './engines/wsl2-paths.js';
|
|
11
11
|
import { sandboxCoreBindMounts } from './mounts.js';
|
|
12
|
+
import { resolveSandboxGitMetadata } from './git-metadata.js';
|
|
12
13
|
import { assertSandboxTaskSource, sandboxControlPaths, sandboxWorkspaceViewPaths } from './workspace-view.js';
|
|
13
14
|
import { resolveTaskWorkspace } from './task-resolver.js';
|
|
14
15
|
import { parseSandboxWorkspaceIdentity, sameSandboxWorkspaceIdentity } from './workspace-identity.js';
|
|
@@ -530,7 +531,17 @@ function expectedMounts(params) {
|
|
|
530
531
|
? recoveryTaskSources(config.repoRoot, params.workspace.taskId)
|
|
531
532
|
: null;
|
|
532
533
|
const taskId = params.workspace.mode === 'task-bound' ? params.workspace.taskId : null;
|
|
534
|
+
const worktrees = worktreeDirCandidates(config, branch).filter((candidate) => fs.existsSync(path.join(candidate, '.git')));
|
|
535
|
+
const gitMetadata = worktrees.length === 1
|
|
536
|
+
? resolveSandboxGitMetadata(worktrees[0], control.channelDir)
|
|
537
|
+
: null;
|
|
533
538
|
const core = sandboxCoreBindMounts(config, branch, {
|
|
539
|
+
...(gitMetadata === null
|
|
540
|
+
? {}
|
|
541
|
+
: {
|
|
542
|
+
gitCommonDir: gitMetadata.commonDir,
|
|
543
|
+
worktreeGitFile: gitMetadata.worktreeGitFile
|
|
544
|
+
}),
|
|
534
545
|
workspaceViewRoot: view.root,
|
|
535
546
|
controlDir: control.channelDir,
|
|
536
547
|
controlStatusDir: control.statusDir,
|
|
@@ -8,7 +8,7 @@ import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedV
|
|
|
8
8
|
import { pruneSandboxDanglingImages } from './image-prune.js';
|
|
9
9
|
import { assertManagedPath, removeManagedDir, removeWorktreeDir } from './managed-fs.js';
|
|
10
10
|
import { run, runEngine, runOk, runOkEngine, runSafe, runSafeEngine } from './shell.js';
|
|
11
|
-
import { parseSandboxWorkspaceIdentity, resolveSandboxCleanupTarget, sameSandboxWorkspaceIdentity } from './workspace-identity.js';
|
|
11
|
+
import { parseSandboxWorkspaceIdentity, resolveSandboxCleanupTarget, resolveSandboxTarget, sameSandboxWorkspaceIdentity } from './workspace-identity.js';
|
|
12
12
|
import { sandboxControlPaths, sandboxWorkspaceViewPaths } from './workspace-view.js';
|
|
13
13
|
import { advanceSandboxRemovalJournalToPhase, advanceSandboxRemovalJournalPhase, claimSandboxRemovalJournal, clearSandboxRemovalJournalRecord, isDefaultSandboxRemovalJournal, listSandboxRemovalJournals, removeSandboxControlRoot, readSandboxControlManifest, sandboxRemovalPhaseIndex } from './control/lifecycle.js';
|
|
14
14
|
import { inspectSandboxControlContainer } from './control/container-identity.js';
|
|
@@ -792,38 +792,76 @@ async function rmPurgeCore(config, tools, prompt = {}) {
|
|
|
792
792
|
p.outro(pc.green('All project sandboxes removed'));
|
|
793
793
|
}
|
|
794
794
|
async function rmUnbound(config, tools, options) {
|
|
795
|
-
return rmUnboundCore(config, tools, options);
|
|
795
|
+
return withRepositoryMutationLock(config.repoRoot, () => rmUnboundCore(config, tools, options));
|
|
796
796
|
}
|
|
797
797
|
async function rmUnboundCore(config, tools, options) {
|
|
798
798
|
const engine = detectEngine(config);
|
|
799
799
|
const listed = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config), { mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) });
|
|
800
|
-
const
|
|
800
|
+
const discovered = [...listed.running, ...listed.nonRunning];
|
|
801
|
+
const branchMismatches = [];
|
|
802
|
+
const classified = discovered.map((row) => {
|
|
803
|
+
if (!row.branch)
|
|
804
|
+
return { row, protected: true, cleanupTarget: null };
|
|
805
|
+
if (row.workspaceMode === 'task-bound' && row.taskId) {
|
|
806
|
+
try {
|
|
807
|
+
resolveSandboxTarget(row.taskId, config.repoRoot);
|
|
808
|
+
return { row, protected: true, cleanupTarget: null };
|
|
809
|
+
}
|
|
810
|
+
catch {
|
|
811
|
+
try {
|
|
812
|
+
const cleanupTarget = resolveSandboxCleanupTarget(row.taskId, config.repoRoot, { allowProtected: true });
|
|
813
|
+
if (cleanupTarget.taskState === 'blocked'
|
|
814
|
+
|| cleanupTarget.taskState === 'completed'
|
|
815
|
+
|| cleanupTarget.taskState === 'archive') {
|
|
816
|
+
if (row.branch !== cleanupTarget.branch) {
|
|
817
|
+
branchMismatches.push({ container: row.name, labelBranch: row.branch, taskBranch: cleanupTarget.branch });
|
|
818
|
+
return { row, protected: true, cleanupTarget: null };
|
|
819
|
+
}
|
|
820
|
+
const activeBranchTarget = resolveSandboxTarget(cleanupTarget.branch, config.repoRoot);
|
|
821
|
+
if (activeBranchTarget.workspace.mode === 'branch-only') {
|
|
822
|
+
return { row, protected: false, cleanupTarget };
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
catch {
|
|
827
|
+
// An unresolved task identity has no active short-id evidence, so keep it out of bulk cleanup.
|
|
828
|
+
}
|
|
829
|
+
return { row, protected: true, cleanupTarget: null };
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (row.workspaceMode !== 'branch-only')
|
|
833
|
+
return { row, protected: true, cleanupTarget: null };
|
|
834
|
+
const cleanupTarget = resolveSandboxCleanupTarget(row.branch, config.repoRoot);
|
|
835
|
+
return cleanupTarget.workspace.mode === 'branch-only'
|
|
836
|
+
? { row, protected: false, cleanupTarget }
|
|
837
|
+
: { row, protected: true, cleanupTarget: null };
|
|
838
|
+
});
|
|
839
|
+
const protectedBranches = new Set(classified
|
|
840
|
+
.filter((entry) => entry.protected)
|
|
841
|
+
.map(({ row }) => row.branch));
|
|
842
|
+
const targets = classified
|
|
843
|
+
.filter((entry) => !entry.protected && !protectedBranches.has(entry.row.branch) && entry.cleanupTarget)
|
|
844
|
+
.map(({ row, cleanupTarget }) => ({
|
|
845
|
+
row,
|
|
846
|
+
cleanupTarget: cleanupTarget,
|
|
847
|
+
target: resolveRmTarget(config, tools, cleanupTarget, { discoveredContainers: [row.name] })
|
|
848
|
+
}));
|
|
801
849
|
p.intro(pc.cyan(`Removing sandboxes for ${config.project}`));
|
|
850
|
+
for (const mismatch of branchMismatches) {
|
|
851
|
+
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.`);
|
|
852
|
+
}
|
|
802
853
|
if (options.dryRun) {
|
|
803
|
-
p.outro(`Dry run: ${
|
|
854
|
+
p.outro(`Dry run: ${targets.length} sandbox(es) found, nothing deleted`);
|
|
804
855
|
return;
|
|
805
856
|
}
|
|
806
|
-
for (const row of
|
|
807
|
-
|
|
808
|
-
runSafeEngine(engine, 'docker', ['rm', '-f', row.name]);
|
|
809
|
-
continue;
|
|
810
|
-
}
|
|
811
|
-
const cleanupTarget = {
|
|
812
|
-
requestedRef: row.branch,
|
|
813
|
-
branch: row.branch,
|
|
814
|
-
workspace: row.workspaceMode === 'task-bound' && row.taskId
|
|
815
|
-
? { mode: 'task-bound', taskId: row.taskId }
|
|
816
|
-
: { mode: 'branch-only' },
|
|
817
|
-
taskState: 'branch-only'
|
|
818
|
-
};
|
|
819
|
-
const target = resolveRmTarget(config, tools, cleanupTarget, { discoveredContainers: [row.name] });
|
|
820
|
-
await rmOne(config, tools, cleanupTarget.branch, {
|
|
857
|
+
for (const { row, cleanupTarget, target } of targets) {
|
|
858
|
+
await runRmOneUnderRepositoryLock(config, tools, cleanupTarget.branch, {
|
|
821
859
|
assumeYes: options.assumeYes,
|
|
822
860
|
target,
|
|
823
861
|
cleanupTarget
|
|
824
862
|
});
|
|
825
863
|
}
|
|
826
|
-
p.outro(pc.green(`Removed ${
|
|
864
|
+
p.outro(pc.green(`Removed ${targets.length} sandbox(es)`));
|
|
827
865
|
}
|
|
828
866
|
export { authorizeWorktrees, rmOne, rmPurge, rmUnbound };
|
|
829
867
|
//# sourceMappingURL=removal.js.map
|
|
@@ -56,6 +56,15 @@ function asOptionalNonEmptyString(value, field, context) {
|
|
|
56
56
|
}
|
|
57
57
|
return value;
|
|
58
58
|
}
|
|
59
|
+
function asOptionalBoolean(value, field, context) {
|
|
60
|
+
if (value === undefined) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
if (typeof value !== 'boolean') {
|
|
64
|
+
throw new Error(`${context}: field "${field}" must be a boolean when provided`);
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
59
68
|
function asStringRecord(value, field, context) {
|
|
60
69
|
if (value === undefined) {
|
|
61
70
|
return undefined;
|
|
@@ -158,13 +167,14 @@ function parseTmpfs(value, context) {
|
|
|
158
167
|
if (!isPlainObject(value)) {
|
|
159
168
|
throw new Error(`${context}: field "tmpfs" must be an object when provided`);
|
|
160
169
|
}
|
|
170
|
+
const exec = asOptionalBoolean(value.exec, 'tmpfs.exec', context);
|
|
161
171
|
return {
|
|
162
172
|
size: asOptionalNonEmptyString(value.size, 'tmpfs.size', context),
|
|
163
|
-
seed: asStringArray(value.seed, 'tmpfs.seed', context)
|
|
173
|
+
seed: asStringArray(value.seed, 'tmpfs.seed', context),
|
|
174
|
+
...(exec === undefined ? {} : { exec })
|
|
164
175
|
};
|
|
165
176
|
}
|
|
166
|
-
export function parseCustomTool(entry, index, options) {
|
|
167
|
-
const context = `customTools[${index}]`;
|
|
177
|
+
export function parseCustomTool(entry, index, options, context = `customTools[${index}]`) {
|
|
168
178
|
if (!isPlainObject(entry)) {
|
|
169
179
|
throw new Error(`${context} must be an object`);
|
|
170
180
|
}
|
|
@@ -198,13 +208,25 @@ export function parseCustomTool(entry, index, options) {
|
|
|
198
208
|
return tool;
|
|
199
209
|
}
|
|
200
210
|
export function parseCustomTools(value, options) {
|
|
201
|
-
if (value === undefined
|
|
211
|
+
if (value === undefined) {
|
|
202
212
|
return [];
|
|
203
213
|
}
|
|
204
|
-
if (!
|
|
205
|
-
throw new Error('sandbox
|
|
214
|
+
if (!isPlainObject(value)) {
|
|
215
|
+
throw new Error('sandbox.tools.definitions must be an object');
|
|
206
216
|
}
|
|
207
|
-
return value.map((
|
|
217
|
+
return Object.entries(value).map(([id, definition], index) => {
|
|
218
|
+
const context = `sandbox.tools.definitions.${id}`;
|
|
219
|
+
if (!TOOL_ID_PATTERN.test(id)) {
|
|
220
|
+
throw new Error(`${context}: tool id must match ${TOOL_ID_PATTERN.source}`);
|
|
221
|
+
}
|
|
222
|
+
if (!isPlainObject(definition)) {
|
|
223
|
+
throw new Error(`${context} must be an object`);
|
|
224
|
+
}
|
|
225
|
+
if (Object.hasOwn(definition, 'id')) {
|
|
226
|
+
throw new Error(`${context}.id is not allowed; the definition key is the tool id`);
|
|
227
|
+
}
|
|
228
|
+
return parseCustomTool({ ...definition, id }, index, options, context);
|
|
229
|
+
});
|
|
208
230
|
}
|
|
209
231
|
export function resolveTools(config) {
|
|
210
232
|
return [...createSandboxCapabilityPlan(config).tools];
|