@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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { recoverPlatformOperations } from '../task/platform-operation-recovery.ts';
|
|
2
|
+
import { recordPlatformOperation } from '../task/platform-operation-journal.ts';
|
|
3
|
+
import type { RecordOperationInput } from '../task/platform-operation-journal.ts';
|
|
4
|
+
import type { PlatformError, PlatformStatus } from './types.ts';
|
|
5
|
+
import type { PlatformClient } from './context.ts';
|
|
6
|
+
|
|
7
|
+
type OperationResult = Readonly<{
|
|
8
|
+
status: PlatformStatus;
|
|
9
|
+
error?: PlatformError | null;
|
|
10
|
+
warnings?: readonly { code: string }[];
|
|
11
|
+
}>;
|
|
12
|
+
|
|
13
|
+
type CoordinateInput<T extends OperationResult> = Readonly<{
|
|
14
|
+
operation: Omit<RecordOperationInput, 'state' | 'lastCode'>;
|
|
15
|
+
agent: string;
|
|
16
|
+
client?: PlatformClient;
|
|
17
|
+
drain?: (taskRef: string, excludeId: string, agent: string, cwd?: string) => Promise<{
|
|
18
|
+
status: 'applied' | 'no-op' | 'blocked' | 'failed';
|
|
19
|
+
error: PlatformError | null;
|
|
20
|
+
}>;
|
|
21
|
+
execute: () => Promise<T>;
|
|
22
|
+
block: (error: PlatformError) => T;
|
|
23
|
+
persistenceFailure: (error: PlatformError) => T;
|
|
24
|
+
}>;
|
|
25
|
+
|
|
26
|
+
async function coordinatePlatformWrite<T extends OperationResult>(input: CoordinateInput<T>): Promise<T> {
|
|
27
|
+
let operationId: string;
|
|
28
|
+
try {
|
|
29
|
+
const queued = recordPlatformOperation({ ...input.operation, state: 'queued' });
|
|
30
|
+
operationId = queued.id;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
const value = error as { code?: string; message?: string };
|
|
33
|
+
return input.persistenceFailure({
|
|
34
|
+
code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
|
|
35
|
+
message: value.message || 'Unable to queue platform operation',
|
|
36
|
+
retryable: true
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const selection = input.operation.kind === 'issue-metadata' && input.operation.issueMetadata?.state === 'closed'
|
|
41
|
+
? 'all' : input.operation.dependency;
|
|
42
|
+
const drained = input.drain
|
|
43
|
+
? await input.drain(input.operation.taskRef, operationId, input.agent, input.operation.cwd)
|
|
44
|
+
: await recoverPlatformOperations(input.operation.taskRef, selection, {
|
|
45
|
+
agent: input.agent,
|
|
46
|
+
client: input.client,
|
|
47
|
+
cwd: input.operation.cwd,
|
|
48
|
+
excludeId: operationId
|
|
49
|
+
});
|
|
50
|
+
if (drained.status !== 'applied' && drained.status !== 'no-op') {
|
|
51
|
+
return input.block(drained.error ?? {
|
|
52
|
+
code: 'PLATFORM_OPERATION_QUEUE_BLOCKED',
|
|
53
|
+
message: 'A previous platform operation is still unresolved; the current write remains queued',
|
|
54
|
+
retryable: true
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
recordPlatformOperation({ ...input.operation, state: 'pending' });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
const value = error as { code?: string; message?: string };
|
|
62
|
+
return input.persistenceFailure({
|
|
63
|
+
code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
|
|
64
|
+
message: value.message || 'Unable to mark platform operation as started',
|
|
65
|
+
retryable: true
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let output: T;
|
|
70
|
+
try {
|
|
71
|
+
output = await input.execute();
|
|
72
|
+
} catch (error) {
|
|
73
|
+
const value = error as { code?: string; message?: string };
|
|
74
|
+
output = input.block({ code: value.code || 'PLATFORM_WRITE_FAILED', message: value.message || String(error), retryable: true });
|
|
75
|
+
}
|
|
76
|
+
const succeeded = (output.status === 'applied' || output.status === 'no-op')
|
|
77
|
+
&& !output.error && !(output.warnings?.length);
|
|
78
|
+
const state = succeeded ? 'succeeded' : output.status === 'failed' && output.error?.retryable === false ? 'failed' : 'unknown';
|
|
79
|
+
try {
|
|
80
|
+
recordPlatformOperation({
|
|
81
|
+
...input.operation,
|
|
82
|
+
state,
|
|
83
|
+
lastCode: output.error?.code ?? output.warnings?.[0]?.code ?? null
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const value = error as { code?: string; message?: string };
|
|
87
|
+
return input.persistenceFailure({
|
|
88
|
+
code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
|
|
89
|
+
message: value.message || 'Unable to persist platform operation outcome',
|
|
90
|
+
retryable: true
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return output;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { coordinatePlatformWrite };
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { coordinatePlatformWrite } from './operation-coordinator.ts';
|
|
1
6
|
import { resolvePlatformProviderContext } from './context.ts';
|
|
2
7
|
import type { PlatformClient } from './context.ts';
|
|
3
8
|
import { platformResult } from './types.ts';
|
|
@@ -9,8 +14,10 @@ import {
|
|
|
9
14
|
providerResourceToken,
|
|
10
15
|
unsupportedProviderOperation
|
|
11
16
|
} from './provider-bridge.ts';
|
|
12
|
-
import { isResourceIdentity, resourceIdentityNumber, reviewMarker as resourceReviewMarker } from './resource-identity.ts';
|
|
17
|
+
import { isResourceIdentity, resourceIdentityEquals, resourceIdentityNumber, reviewMarker as resourceReviewMarker } from './resource-identity.ts';
|
|
13
18
|
import type { ResourceIdentity } from './resource-identity.ts';
|
|
19
|
+
import type { PlatformPullRequestReviewIntent } from '../task/platform-operation-journal.ts';
|
|
20
|
+
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
14
21
|
|
|
15
22
|
export type PrReviewEvent = 'COMMENT' | 'APPROVE' | 'REQUEST_CHANGES';
|
|
16
23
|
export type PrReviewIdentity = { scope: string; round: number; commitSha: string; resource?: ResourceIdentity };
|
|
@@ -38,6 +45,36 @@ function normalizeBody(body: string): string {
|
|
|
38
45
|
return String(body || '').replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
|
|
39
46
|
}
|
|
40
47
|
|
|
48
|
+
function canonicalReviewBody(body: string): string {
|
|
49
|
+
return normalizeBody(body).replace(/\n$/u, '');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function reviewArtifactName(round: number, artifactFile: string): boolean {
|
|
53
|
+
return path.basename(artifactFile) === artifactFile
|
|
54
|
+
&& (artifactFile === 'pr-review.md' && round === 1 || artifactFile === `pr-review-r${round}.md`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function readReviewBodyFile(taskRef: string, round: number, artifactFile: string, bodyDigest: string, cwd?: string): string {
|
|
58
|
+
if (!/^TASK-\d{8}-\d{6}$/u.test(taskRef) || path.basename(artifactFile) !== artifactFile
|
|
59
|
+
|| !reviewArtifactName(round, artifactFile)) {
|
|
60
|
+
throw Object.assign(new Error('Pull-request review artifact identity is invalid'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
|
|
61
|
+
}
|
|
62
|
+
const task = resolveTaskRef(taskRef, cwd ? { repoRoot: cwd } : {});
|
|
63
|
+
if (!task.ok) throw Object.assign(new Error(task.message), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
|
|
64
|
+
const bodyFile = artifactFile.replace(/^pr-review/u, 'pr-review-body');
|
|
65
|
+
const report = fs.readFileSync(path.join(task.taskDir, artifactFile), 'utf8');
|
|
66
|
+
const bodyReferences = [...report.matchAll(/^\*\*(?:正文文件|Body file)\*\*(?::|:)\s*`([^`]+)`[^\r\n]*$/gmu)]
|
|
67
|
+
.map((match) => match[1]);
|
|
68
|
+
if (bodyReferences.length !== 1 || bodyReferences[0] !== bodyFile) {
|
|
69
|
+
throw Object.assign(new Error('Pull-request review artifact does not reference its canonical body file'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
|
|
70
|
+
}
|
|
71
|
+
const body = canonicalReviewBody(fs.readFileSync(path.join(task.taskDir, bodyFile), 'utf8'));
|
|
72
|
+
if (!body || createHash('sha256').update(body).digest('hex') !== bodyDigest) {
|
|
73
|
+
throw Object.assign(new Error('Canonical pull-request review body does not match its persisted digest'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
|
|
74
|
+
}
|
|
75
|
+
return body;
|
|
76
|
+
}
|
|
77
|
+
|
|
41
78
|
function firstLine(body: string): string {
|
|
42
79
|
return normalizeBody(String(body || '')).split('\n', 1)[0] || '';
|
|
43
80
|
}
|
|
@@ -90,6 +127,11 @@ export async function publishPrReview(options: {
|
|
|
90
127
|
cwd?: string;
|
|
91
128
|
client?: PlatformClient;
|
|
92
129
|
dryRun?: boolean;
|
|
130
|
+
agent?: string;
|
|
131
|
+
skipQueue?: boolean;
|
|
132
|
+
expectedResource?: ResourceIdentity;
|
|
133
|
+
expectedProviderScopeId?: string;
|
|
134
|
+
recoveryArtifact?: string;
|
|
93
135
|
prNumber: PrToken;
|
|
94
136
|
identity: PrReviewIdentity;
|
|
95
137
|
event: PrReviewEvent;
|
|
@@ -100,6 +142,13 @@ export async function publishPrReview(options: {
|
|
|
100
142
|
if (!hasUsableContext(context)) {
|
|
101
143
|
return platformResult(context.status, { platform: context.platform, capabilities: context.capabilities, error: context.error });
|
|
102
144
|
}
|
|
145
|
+
const providerScopeId = loaded.ok ? loaded.value.snapshot.scope.id : null;
|
|
146
|
+
if (options.expectedProviderScopeId && providerScopeId !== options.expectedProviderScopeId) {
|
|
147
|
+
return platformResult('failed', {
|
|
148
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
149
|
+
error: { code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Current provider scope differs from the persisted operation target', retryable: false }
|
|
150
|
+
});
|
|
151
|
+
}
|
|
103
152
|
const identity = loaded.ok ? (() => { try { return providerResourceToken(loaded.value.provider, 'pull-request', String(options.prNumber)); } catch { return null; } })() : null;
|
|
104
153
|
if (!identity) {
|
|
105
154
|
return platformResult('failed', {
|
|
@@ -108,6 +157,13 @@ export async function publishPrReview(options: {
|
|
|
108
157
|
});
|
|
109
158
|
}
|
|
110
159
|
const identityNumber = resourceIdentityNumber(identity);
|
|
160
|
+
if (options.expectedResource && !resourceIdentityEquals(identity, options.expectedResource)) {
|
|
161
|
+
return platformResult('failed', {
|
|
162
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
163
|
+
resource: { kind: 'pull-request', number: identityNumber, identity },
|
|
164
|
+
error: { code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Current pull request identity differs from the persisted operation target', retryable: false }
|
|
165
|
+
});
|
|
166
|
+
}
|
|
111
167
|
if (!REVIEW_EVENTS.includes(options.event)) {
|
|
112
168
|
return platformResult('failed', {
|
|
113
169
|
platform: context.platform, capabilities: context.capabilities,
|
|
@@ -123,6 +179,62 @@ export async function publishPrReview(options: {
|
|
|
123
179
|
});
|
|
124
180
|
}
|
|
125
181
|
|
|
182
|
+
if (/^TASK-\d{8}-\d{6}$/u.test(options.identity.scope) && !options.skipQueue && !options.dryRun) {
|
|
183
|
+
if (!options.agent) return platformResult('failed', {
|
|
184
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
185
|
+
resource: { kind: 'pull-request', number: identityNumber, identity },
|
|
186
|
+
error: { code: 'PR_REVIEW_AGENT_REQUIRED', message: 'task-scoped review publication requires an agent token', retryable: false }
|
|
187
|
+
});
|
|
188
|
+
if (!options.recoveryArtifact || !reviewArtifactName(options.identity.round, options.recoveryArtifact)) return platformResult('failed', {
|
|
189
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
190
|
+
resource: { kind: 'pull-request', number: identityNumber, identity },
|
|
191
|
+
error: { code: 'PR_REVIEW_ARTIFACT_REQUIRED', message: 'task-scoped review publication requires its canonical review artifact', retryable: false }
|
|
192
|
+
});
|
|
193
|
+
const body = canonicalReviewBody(options.body);
|
|
194
|
+
const bodyDigest = createHash('sha256').update(body).digest('hex');
|
|
195
|
+
try {
|
|
196
|
+
if (readReviewBodyFile(options.identity.scope, options.identity.round, options.recoveryArtifact, bodyDigest, options.cwd) !== body) throw new Error('body mismatch');
|
|
197
|
+
} catch {
|
|
198
|
+
return platformResult('failed', {
|
|
199
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
200
|
+
resource: { kind: 'pull-request', number: identityNumber, identity },
|
|
201
|
+
error: { code: 'PR_REVIEW_ARTIFACT_MISMATCH', message: 'Review body must match the canonical review body file', retryable: false }
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const pullRequestReview: PlatformPullRequestReviewIntent = {
|
|
205
|
+
prNumber: String(options.prNumber),
|
|
206
|
+
resource: identity,
|
|
207
|
+
providerScopeId: providerScopeId!,
|
|
208
|
+
scope: options.identity.scope,
|
|
209
|
+
round: options.identity.round,
|
|
210
|
+
commitSha: options.identity.commitSha,
|
|
211
|
+
event: options.event,
|
|
212
|
+
artifactFile: options.recoveryArtifact,
|
|
213
|
+
bodyDigest
|
|
214
|
+
};
|
|
215
|
+
return coordinatePlatformWrite({
|
|
216
|
+
operation: {
|
|
217
|
+
taskRef: options.identity.scope,
|
|
218
|
+
cwd: options.cwd || process.cwd(),
|
|
219
|
+
kind: 'pull-request-review',
|
|
220
|
+
target: JSON.stringify(identity),
|
|
221
|
+
expectedDigest: createHash('sha256').update(JSON.stringify(pullRequestReview)).digest('hex'),
|
|
222
|
+
dependency: 'deferred',
|
|
223
|
+
pullRequestReview
|
|
224
|
+
},
|
|
225
|
+
agent: options.agent,
|
|
226
|
+
execute: () => publishPrReview({ ...options, expectedProviderScopeId: providerScopeId!, skipQueue: true }),
|
|
227
|
+
block: (error) => platformResult('blocked', {
|
|
228
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
229
|
+
resource: { kind: 'pull-request', number: identityNumber, identity }, error
|
|
230
|
+
}),
|
|
231
|
+
persistenceFailure: (error) => platformResult('failed', {
|
|
232
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
233
|
+
resource: { kind: 'pull-request', number: identityNumber, identity }, error
|
|
234
|
+
})
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
126
238
|
const marker = reviewMarker({ ...options.identity, resource: identity });
|
|
127
239
|
const listed = await listPrReviews(options.prNumber, { cwd: options.cwd, client: options.client });
|
|
128
240
|
if (listed.status === 'failed' || listed.status === 'blocked') return listed;
|
|
@@ -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
|
|
|
4
5
|
import { enumerateArtifacts } from '../task/artifacts.ts';
|
|
5
6
|
import { parseTypedTaskFrontmatter } from '../task/frontmatter.ts';
|
|
@@ -13,6 +14,7 @@ import { normalizeCommentContent } from './issue-comments.ts';
|
|
|
13
14
|
import { CONTROL_MARKER_PATTERN, sanitizeMarkdownDocument, splitDocumentPlaceholder } from './comment-safety.ts';
|
|
14
15
|
import { platformResult } from './types.ts';
|
|
15
16
|
import type { PlatformResult } from './types.ts';
|
|
17
|
+
import { coordinatePlatformWrite } from './operation-coordinator.ts';
|
|
16
18
|
import type { OperationWarning } from '../task/operation-outcome.ts';
|
|
17
19
|
import { providerError, providerOperationContext, providerStatus, providerResourceToken, resourceIdentityNumber, unsupportedProviderOperation } from './provider-bridge.ts';
|
|
18
20
|
import { resourceIdentityEquals } from './resource-identity.ts';
|
|
@@ -491,7 +493,7 @@ async function reportWrite(taskRef: string, options: ReportWriteOptions): Promis
|
|
|
491
493
|
|
|
492
494
|
async function syncPullRequestSummary(
|
|
493
495
|
taskRef: string,
|
|
494
|
-
options: { agent: string; body: string; changeReportFile?: string; cwd?: string; client?: PlatformClient; dryRun?: boolean; strict?: boolean; primaryResult: PullRequestPrimaryResult; runtimeVersion?: string; manualValidation?: ManualValidationSummaryOptions; lockAlreadyHeld?: boolean }
|
|
496
|
+
options: { agent: string; body: string; changeReportFile?: string; cwd?: string; client?: PlatformClient; dryRun?: boolean; strict?: boolean; primaryResult: PullRequestPrimaryResult; runtimeVersion?: string; manualValidation?: ManualValidationSummaryOptions; lockAlreadyHeld?: boolean; skipQueue?: boolean }
|
|
495
497
|
): Promise<PullRequestSummaryResult> {
|
|
496
498
|
const warningResult = warningResultForPrimary(options.primaryResult);
|
|
497
499
|
let knownPrNumber: number | null = null;
|
|
@@ -657,6 +659,7 @@ async function syncPullRequestSummary(
|
|
|
657
659
|
operations: [{ name: `summary:${reconciliation.action}`, status: 'planned', reasonCode: null }],
|
|
658
660
|
result: null, warnings: [], ...info
|
|
659
661
|
};
|
|
662
|
+
const publish = async (): Promise<PullRequestSummaryResult> => {
|
|
660
663
|
const existingComment = reconciliation.commentId === null
|
|
661
664
|
? undefined
|
|
662
665
|
: providerResourceToken(loaded.value.provider, 'comment', String(reconciliation.commentId));
|
|
@@ -722,6 +725,24 @@ async function syncPullRequestSummary(
|
|
|
722
725
|
operations: [{ name: `summary:${reconciliation.action}`, status: 'applied', reasonCode: null }],
|
|
723
726
|
result: null, warnings: [], ...info
|
|
724
727
|
};
|
|
728
|
+
};
|
|
729
|
+
const operation = {
|
|
730
|
+
taskRef: resolved.taskId,
|
|
731
|
+
cwd: resolved.repoRoot,
|
|
732
|
+
kind: 'pull-request-summary' as const,
|
|
733
|
+
target: JSON.stringify(prIdentity),
|
|
734
|
+
expectedDigest: createHash('sha256').update(desired).digest('hex'),
|
|
735
|
+
dependency: 'deferred' as const,
|
|
736
|
+
pullRequestSummary: { body: options.body, changeReportFile: options.changeReportFile! }
|
|
737
|
+
};
|
|
738
|
+
if (options.skipQueue) return publish();
|
|
739
|
+
return coordinatePlatformWrite({
|
|
740
|
+
operation,
|
|
741
|
+
agent: options.agent,
|
|
742
|
+
execute: publish,
|
|
743
|
+
block: (error) => fail('blocked', context, error, prNumber),
|
|
744
|
+
persistenceFailure: (error) => fail('failed', context, error, prNumber)
|
|
745
|
+
});
|
|
725
746
|
};
|
|
726
747
|
return await (options.lockAlreadyHeld
|
|
727
748
|
? execute()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { normalizePullRequest, type RemotePullRequest } from './github-data.ts';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
4
5
|
import { execFileSync } from 'node:child_process';
|
|
5
6
|
|
|
6
7
|
import { parseTypedTaskFrontmatter } from '../task/frontmatter.ts';
|
|
@@ -41,6 +42,8 @@ import {
|
|
|
41
42
|
import { isResourceIdentity, resourceIdentityEquals, resourceIdentityNumber, serializeResourceIdentity } from './resource-identity.ts';
|
|
42
43
|
import type { ResourceIdentity } from './resource-identity.ts';
|
|
43
44
|
import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.ts';
|
|
45
|
+
import { coordinatePlatformWrite } from './operation-coordinator.ts';
|
|
46
|
+
import type { PlatformPullRequestIntent } from '../task/platform-operation-journal.ts';
|
|
44
47
|
import type { ChangeRequestSnapshot as ProviderChangeRequestSnapshot, IssueSnapshot as ProviderIssueSnapshot } from './provider-contract.ts';
|
|
45
48
|
|
|
46
49
|
type PullRequestSnapshot = PlatformChangeRequestSnapshot;
|
|
@@ -63,7 +66,7 @@ type PullRequestResult = PlatformResult & {
|
|
|
63
66
|
}>;
|
|
64
67
|
};
|
|
65
68
|
type InspectionOptions = { cwd?: string; client?: PlatformClient; runtimeVersion?: string };
|
|
66
|
-
type SharedOptions = { cwd?: string; client?: PlatformClient; runtimeVersion?: string };
|
|
69
|
+
type SharedOptions = { cwd?: string; client?: PlatformClient; runtimeVersion?: string; skipQueue?: boolean };
|
|
67
70
|
type CreateOptions = SharedOptions & {
|
|
68
71
|
agent: string;
|
|
69
72
|
base: string;
|
|
@@ -137,6 +140,30 @@ function withCreation(output: PullRequestResult, creation: CreationOutcome): Pul
|
|
|
137
140
|
return { ...output, creation };
|
|
138
141
|
}
|
|
139
142
|
|
|
143
|
+
async function journalPullRequestOperation(
|
|
144
|
+
taskRef: string,
|
|
145
|
+
options: SharedOptions & { agent: string; dryRun?: boolean },
|
|
146
|
+
intent: PlatformPullRequestIntent,
|
|
147
|
+
target: string,
|
|
148
|
+
identityInput: unknown,
|
|
149
|
+
execute: () => Promise<PullRequestResult>
|
|
150
|
+
): Promise<PullRequestResult> {
|
|
151
|
+
if (options.dryRun) return execute();
|
|
152
|
+
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
153
|
+
if (!resolved.ok) return execute();
|
|
154
|
+
const expectedDigest = createHash('sha256').update(JSON.stringify({ taskId: resolved.taskId, intent, identityInput })).digest('hex');
|
|
155
|
+
const operation = { kind: 'pull-request' as const, target: intent.action === 'sync' ? `bound-pr:${resolved.taskId}` : target, expectedDigest };
|
|
156
|
+
const dependency = intent.action === 'sync' ? 'deferred' : 'required';
|
|
157
|
+
if (options.skipQueue) return execute();
|
|
158
|
+
return coordinatePlatformWrite({
|
|
159
|
+
operation: { taskRef: resolved.taskId, cwd: resolved.repoRoot, ...operation, pullRequest: intent, dependency },
|
|
160
|
+
agent: options.agent,
|
|
161
|
+
execute,
|
|
162
|
+
block: (error) => result('blocked', resolved.taskId, null, null, { error }),
|
|
163
|
+
persistenceFailure: (error) => result('failed', resolved.taskId, null, null, { error })
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
140
167
|
const PRECONDITION_NOT_CREATED: CreationOutcome = {
|
|
141
168
|
kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false
|
|
142
169
|
};
|
|
@@ -851,7 +878,7 @@ function bindIdentity(
|
|
|
851
878
|
}, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
|
|
852
879
|
}
|
|
853
880
|
|
|
854
|
-
async function
|
|
881
|
+
async function bindPlatformPullRequestImpl(taskRef: string, options: BindOptions): Promise<PullRequestResult> {
|
|
855
882
|
const base = await resolvedContext(taskRef, options);
|
|
856
883
|
if (!base.ok) return base.output;
|
|
857
884
|
if (!base.fact) return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
@@ -884,6 +911,12 @@ async function bindPlatformPullRequest(taskRef: string, options: BindOptions): P
|
|
|
884
911
|
});
|
|
885
912
|
}
|
|
886
913
|
|
|
914
|
+
async function bindPlatformPullRequest(taskRef: string, options: BindOptions): Promise<PullRequestResult> {
|
|
915
|
+
const intent: PlatformPullRequestIntent = { action: 'bind', prToken: String(options.pr) };
|
|
916
|
+
return journalPullRequestOperation(taskRef, options, intent, `pr:${options.pr}`, { pr: String(options.pr) },
|
|
917
|
+
() => bindPlatformPullRequestImpl(taskRef, options));
|
|
918
|
+
}
|
|
919
|
+
|
|
887
920
|
function externalEvidenceNote(
|
|
888
921
|
issueIdentity: ResourceIdentity | null,
|
|
889
922
|
source: 'unique' | 'explicit',
|
|
@@ -1077,7 +1110,7 @@ async function resolveExternalPullRequest(taskRef: string, options: ResolveExter
|
|
|
1077
1110
|
}), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
1078
1111
|
}
|
|
1079
1112
|
|
|
1080
|
-
async function
|
|
1113
|
+
async function createPlatformPullRequestImpl(taskRef: string, options: CreateOptions): Promise<PullRequestResult> {
|
|
1081
1114
|
const base = await resolvedContext(taskRef, options);
|
|
1082
1115
|
if (!base.ok) return withCreation(base.output, PRECONDITION_NOT_CREATED);
|
|
1083
1116
|
if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) {
|
|
@@ -1112,6 +1145,66 @@ async function createPlatformPullRequest(taskRef: string, options: CreateOptions
|
|
|
1112
1145
|
}
|
|
1113
1146
|
}
|
|
1114
1147
|
|
|
1148
|
+
async function recoverCreatedPullRequest(
|
|
1149
|
+
taskRef: string,
|
|
1150
|
+
options: SharedOptions & { agent: string; base: string; head: string }
|
|
1151
|
+
): Promise<PullRequestResult> {
|
|
1152
|
+
const base = await resolvedContext(taskRef, options);
|
|
1153
|
+
if (!base.ok) return base.output;
|
|
1154
|
+
if (!base.issueIdentity) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1155
|
+
error: { code: 'PLATFORM_OPERATION_ISSUE_REQUIRED', message: 'Pull-request recovery requires a bound Issue identity', retryable: false }
|
|
1156
|
+
});
|
|
1157
|
+
const head = base.provider.changeRequests?.verifyHead
|
|
1158
|
+
? await base.provider.changeRequests.verifyHead({ context: providerOperationContext(base.loadedContext), head: options.head })
|
|
1159
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.verifyHead');
|
|
1160
|
+
if (!head.ok) return providerPullRequestError(base, head.error, base.prNumber);
|
|
1161
|
+
if (base.prIdentity) {
|
|
1162
|
+
const inspected = await inspectExternalPullRequest(base, base.prIdentity);
|
|
1163
|
+
if (!inspected.pullRequest || inspected.status === 'failed' || inspected.status === 'blocked'
|
|
1164
|
+
|| inspected.pullRequest.base.repository !== base.context.platform.repository
|
|
1165
|
+
|| inspected.pullRequest.base.ref !== options.base || inspected.pullRequest.head.ref !== options.head
|
|
1166
|
+
|| inspected.pullRequest.head.sha !== head.value.sha) {
|
|
1167
|
+
return result('blocked', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1168
|
+
error: inspected.error ?? { code: 'PLATFORM_OPERATION_PR_NOT_CONFIRMED', message: 'Bound pull request does not match the pending create intent', retryable: true }
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
return inspected;
|
|
1172
|
+
}
|
|
1173
|
+
const candidates = base.provider.changeRequests?.listClosing
|
|
1174
|
+
? await base.provider.changeRequests.listClosing({ context: providerOperationContext(base.loadedContext), issue: base.issueIdentity })
|
|
1175
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.listClosing');
|
|
1176
|
+
if (!candidates.ok) return providerPullRequestError(base, candidates.error, null);
|
|
1177
|
+
const matches = candidates.value.map((candidate) => normalizeProviderPullRequest(candidate, base.context.platform.repository!, candidate.number || 0))
|
|
1178
|
+
.filter((candidate) => candidate.base.repository === base.context.platform.repository
|
|
1179
|
+
&& candidate.base.ref === options.base
|
|
1180
|
+
&& candidate.head.repository === base.context.platform.repository
|
|
1181
|
+
&& candidate.head.ref === options.head
|
|
1182
|
+
&& candidate.head.sha === head.value.sha);
|
|
1183
|
+
if (matches.length !== 1 || !matches[0]?.number) return result('blocked', base.resolved.taskId, base.issueNumber, null, {
|
|
1184
|
+
error: { code: matches.length > 1 ? 'PLATFORM_OPERATION_PR_AMBIGUOUS' : 'PLATFORM_OPERATION_PR_NOT_CONFIRMED',
|
|
1185
|
+
message: 'Pending pull-request creation does not resolve to one exact repository/base/head identity', retryable: true }
|
|
1186
|
+
});
|
|
1187
|
+
const bound = await bindPlatformPullRequest(base.resolved.taskId, { agent: options.agent, cwd: base.resolved.repoRoot, pr: matches[0].number });
|
|
1188
|
+
if (!bound.pullRequest || (bound.status !== 'applied' && bound.status !== 'no-op') || bound.error) return bound;
|
|
1189
|
+
if (bound.pullRequest.base.repository !== base.context.platform.repository || bound.pullRequest.base.ref !== options.base
|
|
1190
|
+
|| bound.pullRequest.head.repository !== base.context.platform.repository || bound.pullRequest.head.ref !== options.head
|
|
1191
|
+
|| bound.pullRequest.head.sha !== head.value.sha) return result('blocked', base.resolved.taskId, base.issueNumber, bound.task.prNumber, {
|
|
1192
|
+
error: { code: 'PLATFORM_OPERATION_PR_NOT_CONFIRMED', message: 'Bound pull request changed while recovering the create intent', retryable: true }
|
|
1193
|
+
});
|
|
1194
|
+
return bound;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
async function createPlatformPullRequest(taskRef: string, options: CreateOptions): Promise<PullRequestResult> {
|
|
1198
|
+
const intent: PlatformPullRequestIntent = { action: 'create', baseRef: options.base, headRef: options.head };
|
|
1199
|
+
const identityInput = {
|
|
1200
|
+
title: createHash('sha256').update(options.title).digest('hex'),
|
|
1201
|
+
body: createHash('sha256').update(options.body).digest('hex'),
|
|
1202
|
+
draft: options.draft === true
|
|
1203
|
+
};
|
|
1204
|
+
return journalPullRequestOperation(taskRef, options, intent, `head:${options.head}:base:${options.base}`, identityInput,
|
|
1205
|
+
() => createPlatformPullRequestImpl(taskRef, options));
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1115
1208
|
async function createExternalPullRequest(
|
|
1116
1209
|
taskRef: string,
|
|
1117
1210
|
options: CreateOptions,
|
|
@@ -1140,9 +1233,33 @@ async function createExternalPullRequest(
|
|
|
1140
1233
|
}
|
|
1141
1234
|
if (base.prIdentity) {
|
|
1142
1235
|
const inspected = await inspectExternalPullRequest(base, base.prIdentity!);
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1236
|
+
if (inspected.status === 'failed' || inspected.status === 'blocked') {
|
|
1237
|
+
return withCreation(inspected, PRECONDITION_NOT_CREATED);
|
|
1238
|
+
}
|
|
1239
|
+
const deliveredHeadSha = typeof base.frontmatter.delivery_remote_head === 'string'
|
|
1240
|
+
? base.frontmatter.delivery_remote_head
|
|
1241
|
+
: undefined;
|
|
1242
|
+
const expectedHeadSha = verifiedHeadSha ?? deliveredHeadSha;
|
|
1243
|
+
if (!expectedHeadSha) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1244
|
+
platform: base.context.platform,
|
|
1245
|
+
capabilities: base.context.capabilities,
|
|
1246
|
+
resource: { kind: 'pull-request', number: base.prNumber, identity: base.prIdentity },
|
|
1247
|
+
pullRequest: inspected.pullRequest,
|
|
1248
|
+
error: { code: 'PR_HEAD_UNVERIFIED', message: 'Unable to verify the bound pull request head SHA', retryable: false }
|
|
1249
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1250
|
+
const identity = validateWriterIdentity(base, inspected.pullRequest ?? null, {
|
|
1251
|
+
expectedHead: options.head,
|
|
1252
|
+
expectedHeadSha,
|
|
1253
|
+
errorCode: 'PR_BIND_IDENTITY_MISMATCH'
|
|
1254
|
+
});
|
|
1255
|
+
if (!identity.ok) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1256
|
+
platform: base.context.platform,
|
|
1257
|
+
capabilities: base.context.capabilities,
|
|
1258
|
+
resource: { kind: 'pull-request', number: base.prNumber, identity: base.prIdentity },
|
|
1259
|
+
pullRequest: inspected.pullRequest,
|
|
1260
|
+
error: identity.error
|
|
1261
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1262
|
+
return withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
|
|
1146
1263
|
}
|
|
1147
1264
|
if (options.dryRun) return withCreation(result('planned', base.resolved.taskId, base.issueNumber, null, {
|
|
1148
1265
|
platform: base.context.platform,
|
|
@@ -1242,7 +1359,7 @@ async function createExternalPullRequest(
|
|
|
1242
1359
|
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1243
1360
|
}
|
|
1244
1361
|
|
|
1245
|
-
async function
|
|
1362
|
+
async function syncPlatformPullRequestImpl(taskRef: string, options: SyncOptions): Promise<PullRequestResult> {
|
|
1246
1363
|
const warningResult = warningResultForPrimary(options.primaryResult);
|
|
1247
1364
|
const softenFailure = (output: PullRequestResult): PullRequestResult => {
|
|
1248
1365
|
const authorityError = output.error?.code.startsWith('IN_LABEL_SYNC') || output.error?.code.startsWith('PR_IDENTITY');
|
|
@@ -1406,6 +1523,13 @@ async function syncPlatformPullRequest(taskRef: string, options: SyncOptions): P
|
|
|
1406
1523
|
}
|
|
1407
1524
|
}
|
|
1408
1525
|
|
|
1526
|
+
async function syncPlatformPullRequest(taskRef: string, options: SyncOptions): Promise<PullRequestResult> {
|
|
1527
|
+
const intent: PlatformPullRequestIntent = { action: 'sync', metadata: options.metadata === true, closingIssue: options.closingIssue === true };
|
|
1528
|
+
return journalPullRequestOperation(taskRef, options, intent, `bound-pr:${taskRef}`, {
|
|
1529
|
+
metadata: options.metadata === true, closingIssue: options.closingIssue === true
|
|
1530
|
+
}, () => syncPlatformPullRequestImpl(taskRef, options));
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1409
1533
|
function readProjectPrFlow(repoRoot: string): 'required' | 'disabled' | undefined {
|
|
1410
1534
|
try {
|
|
1411
1535
|
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8')) as { prFlow?: unknown };
|
|
@@ -1503,6 +1627,6 @@ async function skipPlatformPullRequestFact(taskRef: string, options: SkipFactOpt
|
|
|
1503
1627
|
}
|
|
1504
1628
|
}
|
|
1505
1629
|
|
|
1506
|
-
export { bindPlatformPullRequest, createPlatformPullRequest, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, resolveExternalPullRequest, skipPlatformPullRequestFact, selectExternalPullRequest, selectPullRequest, syncPlatformPullRequest, syncPlatformPullRequestInLabels };
|
|
1630
|
+
export { bindPlatformPullRequest, createPlatformPullRequest, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, recoverCreatedPullRequest, resolveExternalPullRequest, skipPlatformPullRequestFact, selectExternalPullRequest, selectPullRequest, syncPlatformPullRequest, syncPlatformPullRequestInLabels };
|
|
1507
1631
|
export type { BindOptions, CreateOptions, ExternalPullRequestResult, ExternalPullRequestSelection, PullRequestPrimaryResult, PullRequestResult, PullRequestSnapshot, ResolveExternalOptions, SkipFactOptions, SyncOptions };
|
|
1508
1632
|
export { warningResultForPrimary };
|
|
@@ -83,6 +83,7 @@ import { atomicWriteJson } from '../control/state.ts';
|
|
|
83
83
|
import { inspectSandboxControlContainer } from '../control/container-identity.ts';
|
|
84
84
|
import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
|
|
85
85
|
import { sandboxCoreBindMounts } from '../mounts.ts';
|
|
86
|
+
import { materializeSandboxGitMetadata } from '../git-metadata.ts';
|
|
86
87
|
import {
|
|
87
88
|
assertSandboxTaskSource,
|
|
88
89
|
finalizeSandboxControlManifest,
|
|
@@ -950,9 +951,10 @@ function runSandboxAuthoritySafeCommand(
|
|
|
950
951
|
|
|
951
952
|
// `docker run` args for mounting a tool's containerMount as an in-container
|
|
952
953
|
// tmpfs. containerMount is an in-container path, so it is NOT engine-converted.
|
|
953
|
-
export function buildTmpfsRunArgs(containerMount: string, tmpfs: { size?: string }): string[] {
|
|
954
|
+
export function buildTmpfsRunArgs(containerMount: string, tmpfs: { size?: string; exec?: boolean }): string[] {
|
|
954
955
|
const size = tmpfs.size ?? '512m';
|
|
955
|
-
|
|
956
|
+
const options = ['rw', ...(tmpfs.exec ? ['exec'] : []), `size=${size}`];
|
|
957
|
+
return ['--tmpfs', `${containerMount}:${options.join(',')}`];
|
|
956
958
|
}
|
|
957
959
|
|
|
958
960
|
export function buildImage(
|
|
@@ -1459,6 +1461,7 @@ export async function create(
|
|
|
1459
1461
|
engine,
|
|
1460
1462
|
replacementLease,
|
|
1461
1463
|
});
|
|
1464
|
+
const gitMetadata = materializeSandboxGitMetadata(worktree, control.channelDir);
|
|
1462
1465
|
hostShellConfig = prepareHostShellConfig({
|
|
1463
1466
|
home: effectiveConfig.home,
|
|
1464
1467
|
project: effectiveConfig.project,
|
|
@@ -1466,6 +1469,8 @@ export async function create(
|
|
|
1466
1469
|
repoRoot: effectiveConfig.repoRoot
|
|
1467
1470
|
});
|
|
1468
1471
|
const coreVolumes = sandboxCoreBindMounts(effectiveConfig, branch, {
|
|
1472
|
+
gitCommonDir: gitMetadata.commonDir,
|
|
1473
|
+
worktreeGitFile: gitMetadata.worktreeGitFile,
|
|
1469
1474
|
worktree,
|
|
1470
1475
|
shellConfigHostDir: hostShellConfig.hostDir,
|
|
1471
1476
|
workspaceViewRoot: workspaceView.root,
|
|
@@ -1572,12 +1577,6 @@ export async function create(
|
|
|
1572
1577
|
`${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
|
|
1573
1578
|
...coreVolumes,
|
|
1574
1579
|
...buildClipboardVolumeArgs(engine, effectiveConfig.home),
|
|
1575
|
-
'-v',
|
|
1576
|
-
volumeArg(
|
|
1577
|
-
engine,
|
|
1578
|
-
path.join(effectiveConfig.repoRoot, '.git'),
|
|
1579
|
-
`${toEnginePath(engine, effectiveConfig.repoRoot)}/.git`
|
|
1580
|
-
),
|
|
1581
1580
|
...dotfilesMount,
|
|
1582
1581
|
...toolVolumes,
|
|
1583
1582
|
...tmpfsArgs,
|
|
@@ -22,8 +22,9 @@ display-only row number; the 'SHORT' column shows the active task short
|
|
|
22
22
|
id bound to each container's branch (via
|
|
23
23
|
.agents/workspace/active/.short-ids.json), or '-' if no active task is
|
|
24
24
|
bound. Pass the SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
|
|
25
|
-
A '-' means no active task
|
|
26
|
-
|
|
25
|
+
A SHORT value of '-' means this container has no active task short id and may
|
|
26
|
+
be eligible for "ai sandbox rm --unbound" cleanup. Use a full TASK-id to
|
|
27
|
+
remove a specific task-bound sandbox.
|
|
27
28
|
|
|
28
29
|
Use "ai sandbox show <ref>" for a single sandbox's worktree and per-tool
|
|
29
30
|
state paths.`;
|
|
@@ -97,7 +98,7 @@ export function ls(args: string[] = []): void {
|
|
|
97
98
|
}
|
|
98
99
|
if (tableRows.some((r) => r.shortId === '-')) {
|
|
99
100
|
process.stdout.write(
|
|
100
|
-
` SHORT '-' = no active task
|
|
101
|
+
` SHORT '-' = no active task short id is displayed; --unbound may clean this sandbox if its branch is not tied to an active task.\n`
|
|
101
102
|
);
|
|
102
103
|
}
|
|
103
104
|
}
|
|
@@ -14,7 +14,7 @@ import { rmOne, rmPurge, rmUnbound } from '../removal.ts';
|
|
|
14
14
|
|
|
15
15
|
const USAGE = `Usage:
|
|
16
16
|
ai sandbox rm <branch | TASK-id | short id> Remove one sandbox; use a full TASK-id for a task-bound sandbox and a branch for branch-only sandboxes
|
|
17
|
-
ai sandbox rm --unbound [--dry-run] [--yes] Remove
|
|
17
|
+
ai sandbox rm --unbound [--dry-run] [--yes] Remove sandboxes not bound to an active task short id
|
|
18
18
|
ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
|
|
19
19
|
|
|
20
20
|
function resolveMissingTaskSandboxBranch(taskId: string, config: ReturnType<typeof loadConfig>): string | null {
|