@fitlab-ai/agent-infra 0.11.7 → 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/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 +11 -0
- package/dist/lib/sandbox/removal.js +58 -20
- package/dist/lib/sandbox/tools.js +29 -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 +157 -24
- 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/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/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 -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/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 +154 -21
- 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/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 +1 -1
- package/templates/.agents/rules/README.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/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/complete-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -0
- 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-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/scripts/sync-templates.js +19 -6
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { recoverPlatformOperations } from '../task/platform-operation-recovery.js';
|
|
2
|
+
import { recordPlatformOperation } from '../task/platform-operation-journal.js';
|
|
3
|
+
async function coordinatePlatformWrite(input) {
|
|
4
|
+
let operationId;
|
|
5
|
+
try {
|
|
6
|
+
const queued = recordPlatformOperation({ ...input.operation, state: 'queued' });
|
|
7
|
+
operationId = queued.id;
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
const value = error;
|
|
11
|
+
return input.persistenceFailure({
|
|
12
|
+
code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
|
|
13
|
+
message: value.message || 'Unable to queue platform operation',
|
|
14
|
+
retryable: true
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
const selection = input.operation.kind === 'issue-metadata' && input.operation.issueMetadata?.state === 'closed'
|
|
18
|
+
? 'all' : input.operation.dependency;
|
|
19
|
+
const drained = input.drain
|
|
20
|
+
? await input.drain(input.operation.taskRef, operationId, input.agent, input.operation.cwd)
|
|
21
|
+
: await recoverPlatformOperations(input.operation.taskRef, selection, {
|
|
22
|
+
agent: input.agent,
|
|
23
|
+
client: input.client,
|
|
24
|
+
cwd: input.operation.cwd,
|
|
25
|
+
excludeId: operationId
|
|
26
|
+
});
|
|
27
|
+
if (drained.status !== 'applied' && drained.status !== 'no-op') {
|
|
28
|
+
return input.block(drained.error ?? {
|
|
29
|
+
code: 'PLATFORM_OPERATION_QUEUE_BLOCKED',
|
|
30
|
+
message: 'A previous platform operation is still unresolved; the current write remains queued',
|
|
31
|
+
retryable: true
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
recordPlatformOperation({ ...input.operation, state: 'pending' });
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const value = error;
|
|
39
|
+
return input.persistenceFailure({
|
|
40
|
+
code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
|
|
41
|
+
message: value.message || 'Unable to mark platform operation as started',
|
|
42
|
+
retryable: true
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
let output;
|
|
46
|
+
try {
|
|
47
|
+
output = await input.execute();
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const value = error;
|
|
51
|
+
output = input.block({ code: value.code || 'PLATFORM_WRITE_FAILED', message: value.message || String(error), retryable: true });
|
|
52
|
+
}
|
|
53
|
+
const succeeded = (output.status === 'applied' || output.status === 'no-op')
|
|
54
|
+
&& !output.error && !(output.warnings?.length);
|
|
55
|
+
const state = succeeded ? 'succeeded' : output.status === 'failed' && output.error?.retryable === false ? 'failed' : 'unknown';
|
|
56
|
+
try {
|
|
57
|
+
recordPlatformOperation({
|
|
58
|
+
...input.operation,
|
|
59
|
+
state,
|
|
60
|
+
lastCode: output.error?.code ?? output.warnings?.[0]?.code ?? null
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
const value = error;
|
|
65
|
+
return input.persistenceFailure({
|
|
66
|
+
code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
|
|
67
|
+
message: value.message || 'Unable to persist platform operation outcome',
|
|
68
|
+
retryable: true
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return output;
|
|
72
|
+
}
|
|
73
|
+
export { coordinatePlatformWrite };
|
|
74
|
+
//# sourceMappingURL=operation-coordinator.js.map
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { coordinatePlatformWrite } from './operation-coordinator.js';
|
|
1
5
|
import { resolvePlatformProviderContext } from './context.js';
|
|
2
6
|
import { platformResult } from './types.js';
|
|
3
7
|
import { providerError, providerOperationContext, providerStatus, providerResourceToken, unsupportedProviderOperation } from './provider-bridge.js';
|
|
4
|
-
import { isResourceIdentity, resourceIdentityNumber, reviewMarker as resourceReviewMarker } from './resource-identity.js';
|
|
8
|
+
import { isResourceIdentity, resourceIdentityEquals, resourceIdentityNumber, reviewMarker as resourceReviewMarker } from './resource-identity.js';
|
|
9
|
+
import { resolveTaskRef } from '../task/resolve-ref.js';
|
|
5
10
|
const REVIEW_EVENTS = ['COMMENT', 'APPROVE', 'REQUEST_CHANGES'];
|
|
6
11
|
// Marker-safe scope: a task id (`TASK-YYYYMMDD-HHMMSS`) or a bare PR number
|
|
7
12
|
// (`pr{N}`). Any other value (e.g. one containing `\r\n` or `-->`) would break
|
|
@@ -19,6 +24,34 @@ export function reviewedCommitMarker(sha) {
|
|
|
19
24
|
function normalizeBody(body) {
|
|
20
25
|
return String(body || '').replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
|
|
21
26
|
}
|
|
27
|
+
function canonicalReviewBody(body) {
|
|
28
|
+
return normalizeBody(body).replace(/\n$/u, '');
|
|
29
|
+
}
|
|
30
|
+
function reviewArtifactName(round, artifactFile) {
|
|
31
|
+
return path.basename(artifactFile) === artifactFile
|
|
32
|
+
&& (artifactFile === 'pr-review.md' && round === 1 || artifactFile === `pr-review-r${round}.md`);
|
|
33
|
+
}
|
|
34
|
+
export function readReviewBodyFile(taskRef, round, artifactFile, bodyDigest, cwd) {
|
|
35
|
+
if (!/^TASK-\d{8}-\d{6}$/u.test(taskRef) || path.basename(artifactFile) !== artifactFile
|
|
36
|
+
|| !reviewArtifactName(round, artifactFile)) {
|
|
37
|
+
throw Object.assign(new Error('Pull-request review artifact identity is invalid'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
|
|
38
|
+
}
|
|
39
|
+
const task = resolveTaskRef(taskRef, cwd ? { repoRoot: cwd } : {});
|
|
40
|
+
if (!task.ok)
|
|
41
|
+
throw Object.assign(new Error(task.message), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
|
|
42
|
+
const bodyFile = artifactFile.replace(/^pr-review/u, 'pr-review-body');
|
|
43
|
+
const report = fs.readFileSync(path.join(task.taskDir, artifactFile), 'utf8');
|
|
44
|
+
const bodyReferences = [...report.matchAll(/^\*\*(?:正文文件|Body file)\*\*(?::|:)\s*`([^`]+)`[^\r\n]*$/gmu)]
|
|
45
|
+
.map((match) => match[1]);
|
|
46
|
+
if (bodyReferences.length !== 1 || bodyReferences[0] !== bodyFile) {
|
|
47
|
+
throw Object.assign(new Error('Pull-request review artifact does not reference its canonical body file'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
|
|
48
|
+
}
|
|
49
|
+
const body = canonicalReviewBody(fs.readFileSync(path.join(task.taskDir, bodyFile), 'utf8'));
|
|
50
|
+
if (!body || createHash('sha256').update(body).digest('hex') !== bodyDigest) {
|
|
51
|
+
throw Object.assign(new Error('Canonical pull-request review body does not match its persisted digest'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
|
|
52
|
+
}
|
|
53
|
+
return body;
|
|
54
|
+
}
|
|
22
55
|
function firstLine(body) {
|
|
23
56
|
return normalizeBody(String(body || '')).split('\n', 1)[0] || '';
|
|
24
57
|
}
|
|
@@ -74,6 +107,13 @@ export async function publishPrReview(options) {
|
|
|
74
107
|
if (!hasUsableContext(context)) {
|
|
75
108
|
return platformResult(context.status, { platform: context.platform, capabilities: context.capabilities, error: context.error });
|
|
76
109
|
}
|
|
110
|
+
const providerScopeId = loaded.ok ? loaded.value.snapshot.scope.id : null;
|
|
111
|
+
if (options.expectedProviderScopeId && providerScopeId !== options.expectedProviderScopeId) {
|
|
112
|
+
return platformResult('failed', {
|
|
113
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
114
|
+
error: { code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Current provider scope differs from the persisted operation target', retryable: false }
|
|
115
|
+
});
|
|
116
|
+
}
|
|
77
117
|
const identity = loaded.ok ? (() => { try {
|
|
78
118
|
return providerResourceToken(loaded.value.provider, 'pull-request', String(options.prNumber));
|
|
79
119
|
}
|
|
@@ -87,6 +127,13 @@ export async function publishPrReview(options) {
|
|
|
87
127
|
});
|
|
88
128
|
}
|
|
89
129
|
const identityNumber = resourceIdentityNumber(identity);
|
|
130
|
+
if (options.expectedResource && !resourceIdentityEquals(identity, options.expectedResource)) {
|
|
131
|
+
return platformResult('failed', {
|
|
132
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
133
|
+
resource: { kind: 'pull-request', number: identityNumber, identity },
|
|
134
|
+
error: { code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Current pull request identity differs from the persisted operation target', retryable: false }
|
|
135
|
+
});
|
|
136
|
+
}
|
|
90
137
|
if (!REVIEW_EVENTS.includes(options.event)) {
|
|
91
138
|
return platformResult('failed', {
|
|
92
139
|
platform: context.platform, capabilities: context.capabilities,
|
|
@@ -101,6 +148,65 @@ export async function publishPrReview(options) {
|
|
|
101
148
|
error: { code: 'REVIEW_IDENTITY_INVALID', message: 'review scope must be a task id or pr{N}; commitSha is required', retryable: false }
|
|
102
149
|
});
|
|
103
150
|
}
|
|
151
|
+
if (/^TASK-\d{8}-\d{6}$/u.test(options.identity.scope) && !options.skipQueue && !options.dryRun) {
|
|
152
|
+
if (!options.agent)
|
|
153
|
+
return platformResult('failed', {
|
|
154
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
155
|
+
resource: { kind: 'pull-request', number: identityNumber, identity },
|
|
156
|
+
error: { code: 'PR_REVIEW_AGENT_REQUIRED', message: 'task-scoped review publication requires an agent token', retryable: false }
|
|
157
|
+
});
|
|
158
|
+
if (!options.recoveryArtifact || !reviewArtifactName(options.identity.round, options.recoveryArtifact))
|
|
159
|
+
return platformResult('failed', {
|
|
160
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
161
|
+
resource: { kind: 'pull-request', number: identityNumber, identity },
|
|
162
|
+
error: { code: 'PR_REVIEW_ARTIFACT_REQUIRED', message: 'task-scoped review publication requires its canonical review artifact', retryable: false }
|
|
163
|
+
});
|
|
164
|
+
const body = canonicalReviewBody(options.body);
|
|
165
|
+
const bodyDigest = createHash('sha256').update(body).digest('hex');
|
|
166
|
+
try {
|
|
167
|
+
if (readReviewBodyFile(options.identity.scope, options.identity.round, options.recoveryArtifact, bodyDigest, options.cwd) !== body)
|
|
168
|
+
throw new Error('body mismatch');
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return platformResult('failed', {
|
|
172
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
173
|
+
resource: { kind: 'pull-request', number: identityNumber, identity },
|
|
174
|
+
error: { code: 'PR_REVIEW_ARTIFACT_MISMATCH', message: 'Review body must match the canonical review body file', retryable: false }
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const pullRequestReview = {
|
|
178
|
+
prNumber: String(options.prNumber),
|
|
179
|
+
resource: identity,
|
|
180
|
+
providerScopeId: providerScopeId,
|
|
181
|
+
scope: options.identity.scope,
|
|
182
|
+
round: options.identity.round,
|
|
183
|
+
commitSha: options.identity.commitSha,
|
|
184
|
+
event: options.event,
|
|
185
|
+
artifactFile: options.recoveryArtifact,
|
|
186
|
+
bodyDigest
|
|
187
|
+
};
|
|
188
|
+
return coordinatePlatformWrite({
|
|
189
|
+
operation: {
|
|
190
|
+
taskRef: options.identity.scope,
|
|
191
|
+
cwd: options.cwd || process.cwd(),
|
|
192
|
+
kind: 'pull-request-review',
|
|
193
|
+
target: JSON.stringify(identity),
|
|
194
|
+
expectedDigest: createHash('sha256').update(JSON.stringify(pullRequestReview)).digest('hex'),
|
|
195
|
+
dependency: 'deferred',
|
|
196
|
+
pullRequestReview
|
|
197
|
+
},
|
|
198
|
+
agent: options.agent,
|
|
199
|
+
execute: () => publishPrReview({ ...options, expectedProviderScopeId: providerScopeId, skipQueue: true }),
|
|
200
|
+
block: (error) => platformResult('blocked', {
|
|
201
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
202
|
+
resource: { kind: 'pull-request', number: identityNumber, identity }, error
|
|
203
|
+
}),
|
|
204
|
+
persistenceFailure: (error) => platformResult('failed', {
|
|
205
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
206
|
+
resource: { kind: 'pull-request', number: identityNumber, identity }, error
|
|
207
|
+
})
|
|
208
|
+
});
|
|
209
|
+
}
|
|
104
210
|
const marker = reviewMarker({ ...options.identity, resource: identity });
|
|
105
211
|
const listed = await listPrReviews(options.prNumber, { cwd: options.cwd, client: options.client });
|
|
106
212
|
if (listed.status === 'failed' || listed.status === 'blocked')
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
import { enumerateArtifacts } from '../task/artifacts.js';
|
|
4
5
|
import { parseTypedTaskFrontmatter } from '../task/frontmatter.js';
|
|
5
6
|
import { resolveTaskRef } from '../task/resolve-ref.js';
|
|
@@ -9,6 +10,7 @@ import { resolvePlatformProviderContext } from './context.js';
|
|
|
9
10
|
import { normalizeCommentContent } from './issue-comments.js';
|
|
10
11
|
import { CONTROL_MARKER_PATTERN, sanitizeMarkdownDocument, splitDocumentPlaceholder } from './comment-safety.js';
|
|
11
12
|
import { platformResult } from './types.js';
|
|
13
|
+
import { coordinatePlatformWrite } from './operation-coordinator.js';
|
|
12
14
|
import { providerError, providerOperationContext, providerStatus, providerResourceToken, resourceIdentityNumber, unsupportedProviderOperation } from './provider-bridge.js';
|
|
13
15
|
import { resourceIdentityEquals } from './resource-identity.js';
|
|
14
16
|
import { buildPrChangeReport, CANONICAL_REPORT_PLACEHOLDER, readPrChangeReport, replaceCanonicalReportPlaceholder, runMechanicalChangeReport, taskIntentDigest, validateMechanicalReport, validatePrecheckCandidate, writePrChangeReportAtomic } from './pr-change-report.js';
|
|
@@ -609,74 +611,94 @@ async function syncPullRequestSummary(taskRef, options) {
|
|
|
609
611
|
operations: [{ name: `summary:${reconciliation.action}`, status: 'planned', reasonCode: null }],
|
|
610
612
|
result: null, warnings: [], ...info
|
|
611
613
|
};
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
context: providerOperationContext(loaded.value),
|
|
618
|
-
parent: prIdentity,
|
|
619
|
-
body: desired,
|
|
620
|
-
...(existingComment ? { existingComment } : {}),
|
|
621
|
-
mutation: { idempotencyKey: `pr-summary:${resolved.taskId}` }
|
|
622
|
-
})
|
|
623
|
-
: unsupportedProviderOperation(loaded.value.provider, 'comments.write');
|
|
624
|
-
if (!written.ok)
|
|
625
|
-
return fail(providerStatus(written.error) === 'blocked' ? 'blocked' : 'failed', context, providerError(written.error, 'PLATFORM_PROVIDER_OPERATION_FAILED'), prNumber);
|
|
626
|
-
const after = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
|
|
627
|
-
const id = /^\d+$/.test(written.value.remoteId) ? Number(written.value.remoteId) : written.value.remoteId;
|
|
628
|
-
const compensateWrittenSummary = async () => reconciliation.action === 'create'
|
|
629
|
-
? written.value.remoteId && loaded.value.provider.comments?.delete
|
|
630
|
-
? await loaded.value.provider.comments.delete({
|
|
631
|
-
context: providerOperationContext(loaded.value),
|
|
632
|
-
parent: prIdentity,
|
|
633
|
-
comment: providerResourceToken(loaded.value.provider, 'comment', written.value.remoteId),
|
|
634
|
-
mutation: { idempotencyKey: `pr-summary-delete:${resolved.taskId}:${written.value.remoteId}` }
|
|
635
|
-
})
|
|
636
|
-
: { ok: false, error: { code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED', message: 'Created summary comment has no valid id', retryable: true } }
|
|
637
|
-
: reconciliation.commentId && previousBody !== null && loaded.value.provider.comments?.write
|
|
614
|
+
const publish = async () => {
|
|
615
|
+
const existingComment = reconciliation.commentId === null
|
|
616
|
+
? undefined
|
|
617
|
+
: providerResourceToken(loaded.value.provider, 'comment', String(reconciliation.commentId));
|
|
618
|
+
const written = loaded.value.provider.comments?.write
|
|
638
619
|
? await loaded.value.provider.comments.write({
|
|
639
620
|
context: providerOperationContext(loaded.value),
|
|
640
621
|
parent: prIdentity,
|
|
641
|
-
body:
|
|
642
|
-
existingComment
|
|
643
|
-
mutation: { idempotencyKey: `pr-summary
|
|
622
|
+
body: desired,
|
|
623
|
+
...(existingComment ? { existingComment } : {}),
|
|
624
|
+
mutation: { idempotencyKey: `pr-summary:${resolved.taskId}` }
|
|
644
625
|
})
|
|
645
|
-
:
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
626
|
+
: unsupportedProviderOperation(loaded.value.provider, 'comments.write');
|
|
627
|
+
if (!written.ok)
|
|
628
|
+
return fail(providerStatus(written.error) === 'blocked' ? 'blocked' : 'failed', context, providerError(written.error, 'PLATFORM_PROVIDER_OPERATION_FAILED'), prNumber);
|
|
629
|
+
const after = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
|
|
630
|
+
const id = /^\d+$/.test(written.value.remoteId) ? Number(written.value.remoteId) : written.value.remoteId;
|
|
631
|
+
const compensateWrittenSummary = async () => reconciliation.action === 'create'
|
|
632
|
+
? written.value.remoteId && loaded.value.provider.comments?.delete
|
|
633
|
+
? await loaded.value.provider.comments.delete({
|
|
634
|
+
context: providerOperationContext(loaded.value),
|
|
635
|
+
parent: prIdentity,
|
|
636
|
+
comment: providerResourceToken(loaded.value.provider, 'comment', written.value.remoteId),
|
|
637
|
+
mutation: { idempotencyKey: `pr-summary-delete:${resolved.taskId}:${written.value.remoteId}` }
|
|
638
|
+
})
|
|
639
|
+
: { ok: false, error: { code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED', message: 'Created summary comment has no valid id', retryable: true } }
|
|
640
|
+
: reconciliation.commentId && previousBody !== null && loaded.value.provider.comments?.write
|
|
641
|
+
? await loaded.value.provider.comments.write({
|
|
642
|
+
context: providerOperationContext(loaded.value),
|
|
643
|
+
parent: prIdentity,
|
|
644
|
+
body: previousBody,
|
|
645
|
+
existingComment: providerResourceToken(loaded.value.provider, 'comment', String(reconciliation.commentId)),
|
|
646
|
+
mutation: { idempotencyKey: `pr-summary-restore:${resolved.taskId}` }
|
|
647
|
+
})
|
|
648
|
+
: { ok: false, error: { code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED', message: 'Previous summary body is unavailable for restoration', retryable: true } };
|
|
649
|
+
if (!after.ok) {
|
|
650
|
+
const compensated = await compensateWrittenSummary();
|
|
651
|
+
if (!compensated.ok)
|
|
652
|
+
return fail('blocked', context, {
|
|
653
|
+
code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED',
|
|
654
|
+
message: `Summary post-write verification failed and compensation failed: ${compensated.error.message}`,
|
|
655
|
+
retryable: true
|
|
656
|
+
}, prNumber);
|
|
649
657
|
return fail('blocked', context, {
|
|
650
|
-
code: '
|
|
651
|
-
message: `Summary
|
|
658
|
+
code: 'PR_SUMMARY_POSTWRITE_VERIFY_FAILED',
|
|
659
|
+
message: `Summary was written but pull-request verification failed: ${after.error?.message || 'unknown verification error'}; the previous summary was restored or the new comment was deleted`,
|
|
652
660
|
retryable: true
|
|
653
661
|
}, prNumber);
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
662
|
+
}
|
|
663
|
+
if (!sameIdentity(initial.value, after.value)) {
|
|
664
|
+
const compensated = await compensateWrittenSummary();
|
|
665
|
+
if (!compensated.ok)
|
|
666
|
+
return fail('blocked', context, {
|
|
667
|
+
code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED',
|
|
668
|
+
message: `Pull-request head changed and summary compensation failed: ${compensated.error.message}`,
|
|
669
|
+
retryable: true
|
|
670
|
+
}, prNumber);
|
|
663
671
|
return fail('blocked', context, {
|
|
664
|
-
code: '
|
|
665
|
-
message:
|
|
672
|
+
code: 'PR_SUMMARY_HEAD_RACE',
|
|
673
|
+
message: 'Pull-request head changed while publishing; the previous summary was restored or the new comment was deleted; rerun report and summary sync',
|
|
666
674
|
retryable: true
|
|
667
675
|
}, prNumber);
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
676
|
+
}
|
|
677
|
+
return {
|
|
678
|
+
...basePlatformResult('applied', context, resolved.taskId, prNumber),
|
|
679
|
+
comment: { kind: 'summary', marker: summaryMarker(resolved.taskId), ids: Number.isInteger(id) ? [id] : [], parts: 1 },
|
|
680
|
+
operations: [{ name: `summary:${reconciliation.action}`, status: 'applied', reasonCode: null }],
|
|
681
|
+
result: null, warnings: [], ...info
|
|
682
|
+
};
|
|
683
|
+
};
|
|
684
|
+
const operation = {
|
|
685
|
+
taskRef: resolved.taskId,
|
|
686
|
+
cwd: resolved.repoRoot,
|
|
687
|
+
kind: 'pull-request-summary',
|
|
688
|
+
target: JSON.stringify(prIdentity),
|
|
689
|
+
expectedDigest: createHash('sha256').update(desired).digest('hex'),
|
|
690
|
+
dependency: 'deferred',
|
|
691
|
+
pullRequestSummary: { body: options.body, changeReportFile: options.changeReportFile }
|
|
679
692
|
};
|
|
693
|
+
if (options.skipQueue)
|
|
694
|
+
return publish();
|
|
695
|
+
return coordinatePlatformWrite({
|
|
696
|
+
operation,
|
|
697
|
+
agent: options.agent,
|
|
698
|
+
execute: publish,
|
|
699
|
+
block: (error) => fail('blocked', context, error, prNumber),
|
|
700
|
+
persistenceFailure: (error) => fail('failed', context, error, prNumber)
|
|
701
|
+
});
|
|
680
702
|
};
|
|
681
703
|
return await (options.lockAlreadyHeld
|
|
682
704
|
? execute()
|
|
@@ -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, {
|
|
@@ -1104,7 +1190,7 @@ async function createExternalPullRequest(taskRef, options, base) {
|
|
|
1104
1190
|
error: null
|
|
1105
1191
|
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1106
1192
|
}
|
|
1107
|
-
async function
|
|
1193
|
+
async function syncPlatformPullRequestImpl(taskRef, options) {
|
|
1108
1194
|
const warningResult = warningResultForPrimary(options.primaryResult);
|
|
1109
1195
|
const softenFailure = (output) => {
|
|
1110
1196
|
const authorityError = output.error?.code.startsWith('IN_LABEL_SYNC') || output.error?.code.startsWith('PR_IDENTITY');
|
|
@@ -1288,6 +1374,12 @@ async function syncPlatformPullRequest(taskRef, options) {
|
|
|
1288
1374
|
}));
|
|
1289
1375
|
}
|
|
1290
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
|
+
}
|
|
1291
1383
|
function readProjectPrFlow(repoRoot) {
|
|
1292
1384
|
try {
|
|
1293
1385
|
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
|
|
@@ -1386,6 +1478,6 @@ async function skipPlatformPullRequestFact(taskRef, options) {
|
|
|
1386
1478
|
});
|
|
1387
1479
|
}
|
|
1388
1480
|
}
|
|
1389
|
-
export { bindPlatformPullRequest, createPlatformPullRequest, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, resolveExternalPullRequest, skipPlatformPullRequestFact, selectExternalPullRequest, selectPullRequest, syncPlatformPullRequest, syncPlatformPullRequestInLabels };
|
|
1481
|
+
export { bindPlatformPullRequest, createPlatformPullRequest, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, recoverCreatedPullRequest, resolveExternalPullRequest, skipPlatformPullRequestFact, selectExternalPullRequest, selectPullRequest, syncPlatformPullRequest, syncPlatformPullRequestInLabels };
|
|
1390
1482
|
export { warningResultForPrimary };
|
|
1391
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);
|