@fitlab-ai/agent-infra 0.9.11 → 0.9.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/README.zh-CN.md +6 -6
- package/dist/bin/internal-cli.js +5 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +71 -10
- package/dist/lib/defaults.json +4 -0
- package/dist/lib/git/workflow.js +43 -1
- package/dist/lib/init.js +1 -0
- package/dist/lib/internal/git-workflow.js +8 -1
- package/dist/lib/internal/platform-pr.js +8 -2
- package/dist/lib/internal/task-delivery.js +59 -0
- package/dist/lib/platform/github-client.js +7 -1
- package/dist/lib/platform/pr-checks.js +8 -5
- package/dist/lib/platform/pr-summary.js +12 -8
- package/dist/lib/platform/pull-requests.js +393 -91
- package/dist/lib/platform/verification-required.js +13 -2
- package/dist/lib/platform/verification-sync.js +9 -4
- package/dist/lib/pr-review/evidence-grading.js +12 -4
- package/dist/lib/sandbox/config.js +5 -0
- package/dist/lib/sandbox/control/lifecycle.js +29 -7
- package/dist/lib/server/process-state.js +2 -2
- package/dist/lib/task/artifact-lifecycle.js +2 -2
- package/dist/lib/task/commit-intent.js +100 -0
- package/dist/lib/task/commit-operation.js +195 -101
- package/dist/lib/task/create.js +33 -7
- package/dist/lib/task/decision-details.js +3 -48
- package/dist/lib/task/delivery-target.js +97 -0
- package/dist/lib/task/delivery.js +153 -0
- package/dist/lib/task/human-override.js +5 -2
- package/dist/lib/task/lifecycle.js +106 -9
- package/dist/lib/task/orchestration.js +55 -62
- package/dist/lib/task/pr-delivery-fact.js +182 -0
- package/dist/lib/task/review-finalization.js +4 -8
- package/dist/lib/task/review-fingerprint.js +8 -0
- package/dist/lib/task/verification-engine.js +48 -24
- package/dist/lib/task/verification-review.js +1 -1
- package/dist/lib/update.js +9 -0
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +105 -10
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +12 -1
- package/lib/defaults.json +4 -0
- package/lib/git/workflow.ts +38 -1
- package/lib/init.ts +3 -0
- package/lib/internal/git-workflow.ts +7 -1
- package/lib/internal/platform-pr.ts +8 -1
- package/lib/internal/task-delivery.ts +53 -0
- package/lib/platform/github-client.ts +8 -1
- package/lib/platform/pr-checks.ts +7 -5
- package/lib/platform/pr-summary.ts +10 -7
- package/lib/platform/pull-requests.ts +425 -87
- package/lib/platform/verification-required.ts +12 -2
- package/lib/platform/verification-sync.ts +9 -4
- package/lib/pr-review/evidence-grading.ts +7 -4
- package/lib/sandbox/config.ts +7 -0
- package/lib/sandbox/control/lifecycle.ts +30 -8
- package/lib/server/process-state.ts +2 -2
- package/lib/task/artifact-lifecycle.ts +2 -2
- package/lib/task/commit-intent.ts +137 -0
- package/lib/task/commit-operation.ts +174 -87
- package/lib/task/create.ts +34 -9
- package/lib/task/decision-details.ts +2 -62
- package/lib/task/delivery-target.ts +130 -0
- package/lib/task/delivery.ts +160 -0
- package/lib/task/human-override.ts +5 -2
- package/lib/task/lifecycle.ts +93 -4
- package/lib/task/orchestration.ts +62 -65
- package/lib/task/pr-delivery-fact.ts +254 -0
- package/lib/task/review-finalization.ts +4 -14
- package/lib/task/review-fingerprint.ts +10 -0
- package/lib/task/verification-engine.ts +45 -28
- package/lib/task/verification-review.ts +2 -0
- package/lib/update.ts +10 -0
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +4 -2
- package/templates/.agents/rules/README.zh-CN.md +4 -2
- package/templates/.agents/rules/create-issue.en.md +11 -2
- package/templates/.agents/rules/create-issue.zh-CN.md +24 -2
- package/templates/.agents/rules/issue-pr-commands.en.md +4 -17
- package/templates/.agents/rules/issue-sync.en.md +26 -5
- package/templates/.agents/rules/issue-sync.zh-CN.md +62 -5
- package/templates/.agents/rules/label-milestone-setup.en.md +15 -2
- package/templates/.agents/rules/label-milestone-setup.zh-CN.md +15 -2
- package/templates/.agents/rules/local-artifact-repair.en.md +46 -0
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +46 -0
- package/templates/.agents/rules/milestone-inference.en.md +12 -2
- package/templates/.agents/rules/milestone-inference.zh-CN.md +25 -3
- package/templates/.agents/rules/pr-sync.en.md +22 -13
- package/templates/.agents/rules/pr-sync.zh-CN.md +22 -1
- package/templates/.agents/rules/release-commands.en.md +42 -3
- package/templates/.agents/rules/release-commands.zh-CN.md +42 -3
- package/templates/.agents/rules/security-alerts.en.md +17 -2
- package/templates/.agents/rules/security-alerts.zh-CN.md +17 -2
- package/templates/.agents/rules/sync-content-generation.en.md +8 -0
- package/templates/.agents/rules/sync-content-generation.zh-CN.md +8 -0
- package/templates/.agents/rules/task-management.en.md +1 -1
- package/templates/.agents/rules/task-management.zh-CN.md +1 -1
- package/templates/.agents/scripts/security-alerts.github.sh +151 -0
- package/templates/.agents/scripts/security-alerts.sh +31 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/close-codescan/SKILL.en.md +2 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/SKILL.en.md +5 -2
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +6 -3
- package/templates/.agents/skills/commit/SKILL.en.md +3 -3
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/commit/config/verify.json +1 -1
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +3 -3
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +3 -3
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -2
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -1
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +10 -10
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -10
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +2 -2
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +2 -2
- package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/init-labels/SKILL.en.md +9 -10
- package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +9 -10
- package/templates/.agents/skills/init-labels/scripts/init-labels.github.sh +57 -1
- package/templates/.agents/skills/init-labels/scripts/init-labels.sh +27 -1
- package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -5
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +3 -5
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/review-analysis/SKILL.en.md +4 -3
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +4 -3
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +18 -0
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +18 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +7 -6
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +7 -6
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +19 -1
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +19 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +4 -2
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +4 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +4 -3
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +4 -3
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +18 -0
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +18 -0
- package/templates/.agents/skills/review-pr/SKILL.en.md +1 -0
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +2 -2
- package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +2 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -0
- 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 +4 -0
- package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/templates/task.en.md +5 -1
- package/templates/.agents/templates/task.zh-CN.md +5 -1
- package/templates/.agents/workflows/bug-fix.en.yaml +9 -8
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +9 -8
- package/templates/.agents/workflows/feature-development.en.yaml +9 -8
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +9 -8
- package/templates/.agents/workflows/refactoring.en.yaml +9 -8
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +9 -8
- package/templates/.agents/rules/create-issue.github.en.md +0 -14
- package/templates/.agents/rules/create-issue.github.zh-CN.md +0 -27
- package/templates/.agents/rules/issue-pr-commands.github.en.md +0 -34
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +0 -49
- package/templates/.agents/rules/issue-sync.github.en.md +0 -40
- package/templates/.agents/rules/issue-sync.github.zh-CN.md +0 -76
- package/templates/.agents/rules/label-milestone-setup.github.en.md +0 -60
- package/templates/.agents/rules/label-milestone-setup.github.zh-CN.md +0 -60
- package/templates/.agents/rules/milestone-inference.github.en.md +0 -15
- package/templates/.agents/rules/milestone-inference.github.zh-CN.md +0 -27
- package/templates/.agents/rules/pr-checks-commands.github.en.md +0 -18
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +0 -34
- package/templates/.agents/rules/pr-sync.github.en.md +0 -43
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +0 -69
- package/templates/.agents/rules/release-commands.github.en.md +0 -44
- package/templates/.agents/rules/release-commands.github.zh-CN.md +0 -44
- package/templates/.agents/rules/security-alerts.github.en.md +0 -43
- package/templates/.agents/rules/security-alerts.github.zh-CN.md +0 -43
- /package/templates/.agents/rules/{issue-fields.github.en.md → issue-fields.en.md} +0 -0
- /package/templates/.agents/rules/{issue-fields.github.zh-CN.md → issue-fields.zh-CN.md} +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { execFileSync } from 'node:child_process';
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
+
import { parseTypedTaskFrontmatter } from '../task/frontmatter.ts';
|
|
5
6
|
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
6
7
|
import { extractSection } from '../task/sections.ts';
|
|
7
8
|
import { captureTaskWriteMetadata, writeTask } from '../task/write.ts';
|
|
@@ -20,7 +21,15 @@ import { platformResult } from './types.ts';
|
|
|
20
21
|
import type { PlatformOperation, PlatformResult } from './types.ts';
|
|
21
22
|
import { inspectCompletionArtifacts } from '../task/finalization-artifacts.ts';
|
|
22
23
|
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
|
|
24
|
+
import { resolveDeliveryTarget } from '../task/delivery-target.ts';
|
|
23
25
|
import { mergeOperationWarnings, type OperationWarning } from '../task/operation-outcome.ts';
|
|
26
|
+
import {
|
|
27
|
+
buildBoundFact,
|
|
28
|
+
buildSkippedFact,
|
|
29
|
+
factFrontmatterMutation,
|
|
30
|
+
readPrDeliveryFact
|
|
31
|
+
} from '../task/pr-delivery-fact.ts';
|
|
32
|
+
import type { CreationOutcome, PrDeliveryBindingSource, PrDeliveryFact } from '../task/pr-delivery-fact.ts';
|
|
24
33
|
|
|
25
34
|
type PullRequestSnapshot = PlatformChangeRequestSnapshot;
|
|
26
35
|
|
|
@@ -28,6 +37,7 @@ type PullRequestResult = PlatformResult & {
|
|
|
28
37
|
task: { id: string | null; issueNumber: number | null; prNumber: number | null };
|
|
29
38
|
pullRequest: PullRequestSnapshot | null;
|
|
30
39
|
result: 'pr_created' | 'pr_reused' | 'no_op' | 'pr_created_with_warnings' | 'pr_reused_with_warnings' | 'no_op_with_warnings' | 'failed' | 'blocked' | null;
|
|
40
|
+
creation: CreationOutcome | null;
|
|
31
41
|
warnings: readonly OperationWarning[];
|
|
32
42
|
};
|
|
33
43
|
type InspectionOptions = { cwd?: string; client?: unknown };
|
|
@@ -40,11 +50,13 @@ type CreateOptions = SharedOptions & {
|
|
|
40
50
|
body: string;
|
|
41
51
|
draft?: boolean;
|
|
42
52
|
dryRun?: boolean;
|
|
53
|
+
phase?: { value: CreatePhase };
|
|
43
54
|
};
|
|
44
55
|
type BindOptions = SharedOptions & { agent: string; pr: number; dryRun?: boolean };
|
|
45
56
|
type PullRequestPrimaryResult = 'pr_created' | 'pr_reused' | 'no_op';
|
|
46
57
|
type SyncOptions = SharedOptions & { agent: string; metadata?: boolean; closingIssue?: boolean; dryRun?: boolean; primaryResult: PullRequestPrimaryResult };
|
|
47
58
|
type ResolveExternalOptions = SharedOptions & { agent: string; pr?: number; dryRun?: boolean };
|
|
59
|
+
type SkipFactOptions = { cwd?: string; agent: string; dryRun?: boolean };
|
|
48
60
|
type ExternalPullRequestSelection =
|
|
49
61
|
| { status: 'normal'; candidates: PullRequestSnapshot[]; eligible: [] }
|
|
50
62
|
| { status: 'selected'; source: 'unique' | 'explicit'; selected: PullRequestSnapshot; candidates: PullRequestSnapshot[]; eligible: PullRequestSnapshot[] }
|
|
@@ -57,6 +69,8 @@ type ExternalPullRequestResult = PullRequestResult & {
|
|
|
57
69
|
selected: PullRequestSnapshot | null;
|
|
58
70
|
};
|
|
59
71
|
|
|
72
|
+
type CreatePhase = 'before-post' | 'post-dispatched' | 'post-accepted';
|
|
73
|
+
|
|
60
74
|
type RemotePullRequest = {
|
|
61
75
|
number?: number;
|
|
62
76
|
node_id?: string;
|
|
@@ -123,6 +137,7 @@ function result(
|
|
|
123
137
|
task: { id: taskId, issueNumber, prNumber },
|
|
124
138
|
pullRequest: null,
|
|
125
139
|
result: null,
|
|
140
|
+
creation: null,
|
|
126
141
|
warnings: [],
|
|
127
142
|
...overrides
|
|
128
143
|
};
|
|
@@ -144,6 +159,14 @@ function externalResult(
|
|
|
144
159
|
};
|
|
145
160
|
}
|
|
146
161
|
|
|
162
|
+
function withCreation(output: PullRequestResult, creation: CreationOutcome): PullRequestResult {
|
|
163
|
+
return { ...output, creation };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const PRECONDITION_NOT_CREATED: CreationOutcome = {
|
|
167
|
+
kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false
|
|
168
|
+
};
|
|
169
|
+
|
|
147
170
|
function normalizePullRequest(remote: RemotePullRequest, repository: string): PullRequestSnapshot | null {
|
|
148
171
|
const number = Number(remote.number);
|
|
149
172
|
const headRepository = remote.head?.repo?.full_name;
|
|
@@ -216,7 +239,7 @@ function hasCompleteExternalIdentity(pullRequest: PullRequestSnapshot): boolean
|
|
|
216
239
|
];
|
|
217
240
|
return Number.isInteger(pullRequest.number) && pullRequest.number > 0 &&
|
|
218
241
|
required.every((value) => Boolean(value?.trim())) &&
|
|
219
|
-
Boolean(pullRequest.mergedAt) === Boolean(pullRequest.mergeCommitSha);
|
|
242
|
+
(pullRequest.state === 'open' || Boolean(pullRequest.mergedAt) === Boolean(pullRequest.mergeCommitSha));
|
|
220
243
|
}
|
|
221
244
|
|
|
222
245
|
function selectExternalPullRequest(
|
|
@@ -247,6 +270,7 @@ function selectExternalPullRequest(
|
|
|
247
270
|
const eligible = unique.filter((candidate) =>
|
|
248
271
|
repositoryKey(candidate.repository) === wantedRepository &&
|
|
249
272
|
repositoryKey(candidate.base.repository) === wantedRepository &&
|
|
273
|
+
candidate.state === 'closed' &&
|
|
250
274
|
Boolean(candidate.mergedAt && candidate.mergeCommitSha)
|
|
251
275
|
);
|
|
252
276
|
if (explicitPrNumber !== null) {
|
|
@@ -421,15 +445,104 @@ function validateBoundPullRequest(
|
|
|
421
445
|
return { ok: true };
|
|
422
446
|
}
|
|
423
447
|
|
|
448
|
+
type BindingIdentityOptions = {
|
|
449
|
+
expectedHead?: string;
|
|
450
|
+
expectedHeadSha?: string;
|
|
451
|
+
errorCode?: string;
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
function resolveTaskDeliveryTarget(repoRoot: string, frontmatter: Record<string, unknown>) {
|
|
455
|
+
const existing = {
|
|
456
|
+
...(typeof frontmatter.delivery_remote === 'string' && frontmatter.delivery_remote
|
|
457
|
+
? { remote: frontmatter.delivery_remote } : {}),
|
|
458
|
+
...(typeof frontmatter.delivery_base_ref === 'string' && frontmatter.delivery_base_ref
|
|
459
|
+
? { baseRef: frontmatter.delivery_base_ref } : {})
|
|
460
|
+
};
|
|
461
|
+
const target = resolveDeliveryTarget(repoRoot, existing);
|
|
462
|
+
return target.ok
|
|
463
|
+
? { ok: true as const, value: target.value }
|
|
464
|
+
: { ok: false as const, error: { code: target.code, message: target.message, retryable: false } };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function taskDeliveryTarget(base: ReturnType<typeof resolvedContext> & { ok: true }) {
|
|
468
|
+
return resolveTaskDeliveryTarget(base.resolved.repoRoot, base.frontmatter);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function validateIdentityAgainstTarget(
|
|
472
|
+
repository: string,
|
|
473
|
+
target: { remote: string; baseRef: string },
|
|
474
|
+
pullRequest: PullRequestSnapshot | null,
|
|
475
|
+
options: BindingIdentityOptions = {}
|
|
476
|
+
): { ok: true } | { ok: false; error: { code: string; message: string; retryable: boolean } } {
|
|
477
|
+
if (!pullRequest || !hasCompleteExternalIdentity(pullRequest)) return {
|
|
478
|
+
ok: false,
|
|
479
|
+
error: { code: options.errorCode || 'PR_BIND_IDENTITY_MISMATCH', message: 'Pull request identity is incomplete', retryable: false }
|
|
480
|
+
};
|
|
481
|
+
const wantedHead = options.expectedHead ? expectedHead(repository, options.expectedHead) : null;
|
|
482
|
+
const matches = repositoryKey(pullRequest.repository) === repositoryKey(repository)
|
|
483
|
+
&& repositoryKey(pullRequest.base.repository) === repositoryKey(repository)
|
|
484
|
+
&& pullRequest.base.ref === target.baseRef
|
|
485
|
+
&& (!wantedHead || (repositoryKey(pullRequest.head.repository) === repositoryKey(wantedHead.repository) && pullRequest.head.ref === wantedHead.ref))
|
|
486
|
+
&& (!options.expectedHeadSha || pullRequest.head.sha === options.expectedHeadSha);
|
|
487
|
+
if (!matches) return {
|
|
488
|
+
ok: false,
|
|
489
|
+
error: {
|
|
490
|
+
code: options.errorCode || 'PR_BIND_IDENTITY_MISMATCH',
|
|
491
|
+
message: 'Pull request identity does not match the task branch and delivery target',
|
|
492
|
+
retryable: false
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
return { ok: true };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function validateWriterIdentity(
|
|
499
|
+
base: ReturnType<typeof resolvedContext> & { ok: true },
|
|
500
|
+
pullRequest: PullRequestSnapshot | null,
|
|
501
|
+
options: BindingIdentityOptions = {}
|
|
502
|
+
): { ok: true } | { ok: false; error: { code: string; message: string; retryable: boolean } } {
|
|
503
|
+
const target = taskDeliveryTarget(base);
|
|
504
|
+
if (!target.ok) return target;
|
|
505
|
+
return validateIdentityAgainstTarget(base.context.platform.repository!, target.value, pullRequest, options);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function sameDeliveryIdentity(left: PullRequestSnapshot, right: PullRequestSnapshot): boolean {
|
|
509
|
+
return JSON.stringify(deliveryIdentity(left)) === JSON.stringify(deliveryIdentity(right));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function validateExternalMergedEvidence(
|
|
513
|
+
initial: PullRequestSnapshot,
|
|
514
|
+
rechecked: PullRequestSnapshot
|
|
515
|
+
): { ok: true } | { ok: false; error: { code: string; message: string; retryable: boolean } } {
|
|
516
|
+
if (
|
|
517
|
+
initial.state !== 'closed'
|
|
518
|
+
|| !initial.mergedAt
|
|
519
|
+
|| !initial.mergeCommitSha
|
|
520
|
+
|| rechecked.state !== 'closed'
|
|
521
|
+
|| !rechecked.mergedAt
|
|
522
|
+
|| !rechecked.mergeCommitSha
|
|
523
|
+
|| initial.mergedAt !== rechecked.mergedAt
|
|
524
|
+
|| initial.mergeCommitSha !== rechecked.mergeCommitSha
|
|
525
|
+
) return {
|
|
526
|
+
ok: false,
|
|
527
|
+
error: { code: 'PR_EXTERNAL_IDENTITY_MISMATCH', message: 'Selected pull request merged evidence changed before task binding', retryable: false }
|
|
528
|
+
};
|
|
529
|
+
return { ok: true };
|
|
530
|
+
}
|
|
531
|
+
|
|
424
532
|
function resolvedContext(taskRef: string, options: InspectionOptions) {
|
|
425
533
|
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
426
534
|
if (!resolved.ok) return { ok: false as const, output: result('failed', resolved.taskId, null, null, {
|
|
427
535
|
error: { code: resolved.code, message: resolved.message, retryable: false }
|
|
428
536
|
}) };
|
|
429
537
|
const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
430
|
-
const frontmatter =
|
|
538
|
+
const frontmatter = parseTypedTaskFrontmatter(content);
|
|
539
|
+
const factRead = readPrDeliveryFact(frontmatter);
|
|
540
|
+
if (factRead.status === 'invalid') return { ok: false as const, output: result('failed', resolved.taskId, null, null, {
|
|
541
|
+
error: { code: 'PR_DELIVERY_FACT_INVALID', message: factRead.error.message, retryable: false }
|
|
542
|
+
}) };
|
|
543
|
+
const fact = factRead.status === 'valid' ? factRead.fact : null;
|
|
431
544
|
const issue = Number(frontmatter.issue_number);
|
|
432
|
-
const pr =
|
|
545
|
+
const pr = fact?.state === 'bound' ? fact.identity.number : NaN;
|
|
433
546
|
const issueNumber = Number.isInteger(issue) && issue > 0 ? issue : null;
|
|
434
547
|
const prNumber = Number.isInteger(pr) && pr > 0 ? pr : null;
|
|
435
548
|
const client = (options.client as GitHubClient | undefined) || createGitHubClient();
|
|
@@ -438,7 +551,7 @@ function resolvedContext(taskRef: string, options: InspectionOptions) {
|
|
|
438
551
|
if (!usable) return { ok: false as const, output: result(context.status, resolved.taskId, issueNumber, prNumber, {
|
|
439
552
|
platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
|
|
440
553
|
}) };
|
|
441
|
-
return { ok: true as const, resolved, content, frontmatter, issueNumber, prNumber, client, context };
|
|
554
|
+
return { ok: true as const, resolved, content, frontmatter, fact, issueNumber, prNumber, client, context };
|
|
442
555
|
}
|
|
443
556
|
|
|
444
557
|
function inspectGitHubPullRequest(client: GitHubClient, repository: string, number: number, cwd: string) {
|
|
@@ -582,9 +695,12 @@ function locatePullRequest(base: ReturnType<typeof resolvedContext> & { ok: true
|
|
|
582
695
|
function inspectPlatformPullRequest(taskRef: string, options: InspectionOptions = {}): PullRequestResult {
|
|
583
696
|
const base = resolvedContext(taskRef, options);
|
|
584
697
|
if (!base.ok) return base.output;
|
|
698
|
+
if (!base.fact) return result('no-op', base.resolved.taskId, base.issueNumber, null, {
|
|
699
|
+
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
700
|
+
});
|
|
585
701
|
if (!base.prNumber) return result('no-op', base.resolved.taskId, base.issueNumber, null, {
|
|
586
702
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
587
|
-
error: { code: 'PR_NOT_LINKED', message: 'Task has no
|
|
703
|
+
error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
|
|
588
704
|
});
|
|
589
705
|
const fetched = inspectPlatformChangeRequest(base.context.platform.type, {
|
|
590
706
|
cwd: base.resolved.repoRoot,
|
|
@@ -652,6 +768,38 @@ function appendActivity(content: string, line: string): string {
|
|
|
652
768
|
return `${body.replace(/\s+$/, '')}${body.trim() ? '\n' : ''}${line}`;
|
|
653
769
|
}
|
|
654
770
|
|
|
771
|
+
function deliveryIdentity(pullRequest: PullRequestSnapshot) {
|
|
772
|
+
return {
|
|
773
|
+
repository: pullRequest.repository,
|
|
774
|
+
number: pullRequest.number,
|
|
775
|
+
nodeId: pullRequest.nodeId,
|
|
776
|
+
url: pullRequest.url,
|
|
777
|
+
head: { ...pullRequest.head },
|
|
778
|
+
base: { ...pullRequest.base }
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function factTimestamp(timestamp: string): string {
|
|
783
|
+
return new Date(timestamp.replace(' ', 'T')).toISOString();
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function boundFactFor(
|
|
787
|
+
pullRequest: PullRequestSnapshot,
|
|
788
|
+
source: PrDeliveryBindingSource,
|
|
789
|
+
issueNumber: number | null,
|
|
790
|
+
verifiedAt: string
|
|
791
|
+
): PrDeliveryFact {
|
|
792
|
+
return buildBoundFact({
|
|
793
|
+
identity: deliveryIdentity(pullRequest),
|
|
794
|
+
source,
|
|
795
|
+
issueNumber,
|
|
796
|
+
verifiedAt: factTimestamp(verifiedAt),
|
|
797
|
+
remoteState: pullRequest.state,
|
|
798
|
+
mergedAt: pullRequest.state === 'closed' ? pullRequest.mergedAt : null,
|
|
799
|
+
mergeCommitSha: pullRequest.state === 'closed' ? pullRequest.mergeCommitSha : null
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
655
803
|
function writeCreateStarted(base: ReturnType<typeof resolvedContext> & { ok: true }, agent: string) {
|
|
656
804
|
const starts = (base.content.match(/\*\*Create PR \[started\]\*\*/g) || []).length;
|
|
657
805
|
const dones = (base.content.match(/\*\*Create PR\*\*/g) || []).length;
|
|
@@ -666,19 +814,40 @@ function writeCreateStarted(base: ReturnType<typeof resolvedContext> & { ok: tru
|
|
|
666
814
|
}, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
|
|
667
815
|
}
|
|
668
816
|
|
|
669
|
-
function bindIdentity(
|
|
817
|
+
function bindIdentity(
|
|
818
|
+
base: ReturnType<typeof resolvedContext> & { ok: true },
|
|
819
|
+
pullRequest: PullRequestSnapshot,
|
|
820
|
+
agent: string,
|
|
821
|
+
source: PrDeliveryBindingSource,
|
|
822
|
+
dryRun = false,
|
|
823
|
+
expectedHead?: string,
|
|
824
|
+
expectedHeadSha?: string
|
|
825
|
+
) {
|
|
826
|
+
const identity = validateWriterIdentity(base, pullRequest, {
|
|
827
|
+
expectedHead: source === 'explicit-bind' ? String(base.frontmatter.branch || '') : expectedHead,
|
|
828
|
+
expectedHeadSha,
|
|
829
|
+
errorCode: 'PR_BIND_IDENTITY_MISMATCH'
|
|
830
|
+
});
|
|
831
|
+
if (!identity.ok) return { status: 'failed' as const, error: identity.error };
|
|
670
832
|
if (base.prNumber && base.prNumber !== pullRequest.number) return { status: 'failed' as const, error: {
|
|
671
833
|
code: 'PR_BIND_CONFLICT', message: `Task is already bound to PR #${base.prNumber}`
|
|
672
834
|
} };
|
|
673
|
-
if (base.prNumber === pullRequest.number
|
|
835
|
+
if (base.prNumber === pullRequest.number && base.fact?.state === 'bound') {
|
|
836
|
+
if (JSON.stringify(base.fact.identity) !== JSON.stringify(deliveryIdentity(pullRequest))) return { status: 'failed' as const, error: {
|
|
837
|
+
code: 'PR_BIND_IDENTITY_MISMATCH', message: `Task binding for PR #${pullRequest.number} does not match the fetched pull request identity`
|
|
838
|
+
} };
|
|
839
|
+
return { status: 'no-op' as const, error: null };
|
|
840
|
+
}
|
|
674
841
|
const current = fs.readFileSync(base.resolved.taskMdPath, 'utf8');
|
|
675
842
|
const metadata = captureTaskWriteMetadata();
|
|
843
|
+
const fact = boundFactFor(pullRequest, source, base.issueNumber, metadata.timestamp);
|
|
844
|
+
const factMutation = factFrontmatterMutation(fact);
|
|
676
845
|
return writeTask({
|
|
677
846
|
taskRef: base.resolved.taskId,
|
|
678
847
|
expectedState: 'active',
|
|
679
848
|
dryRun,
|
|
680
849
|
mutations: [
|
|
681
|
-
{ kind: 'frontmatter', set: {
|
|
850
|
+
{ kind: 'frontmatter', set: { ...factMutation.set, assigned_to: agent } },
|
|
682
851
|
{ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(
|
|
683
852
|
current, `- ${metadata.timestamp} — **Create PR** by ${agent} — PR #${pullRequest.number} created → ${pullRequest.url}`
|
|
684
853
|
) }
|
|
@@ -689,6 +858,9 @@ function bindIdentity(base: ReturnType<typeof resolvedContext> & { ok: true }, p
|
|
|
689
858
|
function bindPlatformPullRequest(taskRef: string, options: BindOptions): PullRequestResult {
|
|
690
859
|
const base = resolvedContext(taskRef, options);
|
|
691
860
|
if (!base.ok) return base.output;
|
|
861
|
+
if (!base.fact) return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
862
|
+
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
863
|
+
});
|
|
692
864
|
if (!Number.isInteger(options.pr) || options.pr <= 0) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
693
865
|
error: { code: 'PR_NUMBER_INVALID', message: 'PR number must be positive', retryable: false }
|
|
694
866
|
});
|
|
@@ -697,13 +869,29 @@ function bindPlatformPullRequest(taskRef: string, options: BindOptions): PullReq
|
|
|
697
869
|
});
|
|
698
870
|
const fetched = inspectGitHubPullRequest(base.client, base.context.platform.repository!, options.pr, base.resolved.repoRoot);
|
|
699
871
|
if (!fetched.ok) return result(fetched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { error: fetched.error });
|
|
700
|
-
const
|
|
872
|
+
const initialIdentity = validateWriterIdentity(base, fetched.value, {
|
|
873
|
+
expectedHead: String(base.frontmatter.branch || ''),
|
|
874
|
+
errorCode: 'PR_BIND_IDENTITY_MISMATCH'
|
|
875
|
+
});
|
|
876
|
+
if (!initialIdentity.ok) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
877
|
+
pullRequest: fetched.value, error: initialIdentity.error
|
|
878
|
+
});
|
|
879
|
+
const rechecked = inspectGitHubPullRequest(base.client, base.context.platform.repository!, options.pr, base.resolved.repoRoot);
|
|
880
|
+
if (!rechecked.ok || !rechecked.value) return result(rechecked.ok ? 'failed' : rechecked.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
881
|
+
pullRequest: fetched.value,
|
|
882
|
+
error: rechecked.ok ? { code: 'PR_BIND_RECHECK_FAILED', message: 'Pull request recheck returned no identity', retryable: false } : rechecked.error
|
|
883
|
+
});
|
|
884
|
+
if (!sameDeliveryIdentity(fetched.value, rechecked.value)) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
885
|
+
pullRequest: rechecked.value,
|
|
886
|
+
error: { code: 'PR_BIND_IDENTITY_MISMATCH', message: 'Pull request identity changed before task binding', retryable: false }
|
|
887
|
+
});
|
|
888
|
+
const written = bindIdentity(base, rechecked.value, options.agent, 'explicit-bind', options.dryRun);
|
|
701
889
|
if (written.status === 'failed') return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
702
890
|
pullRequest: fetched.value, error: { code: written.error.code, message: written.error.message, retryable: false }
|
|
703
891
|
});
|
|
704
|
-
return result(options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber :
|
|
892
|
+
return result(options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : rechecked.value.number, {
|
|
705
893
|
changed: !options.dryRun && written.status === 'applied', platform: base.context.platform, capabilities: base.context.capabilities,
|
|
706
|
-
resource: { kind: 'pull-request', number:
|
|
894
|
+
resource: { kind: 'pull-request', number: rechecked.value.number }, pullRequest: rechecked.value,
|
|
707
895
|
operations: [{ name: 'task:bind-pr', status: options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', reasonCode: null }], error: null
|
|
708
896
|
});
|
|
709
897
|
}
|
|
@@ -737,6 +925,9 @@ function externalIdentityEvidenceNote(
|
|
|
737
925
|
function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOptions): ExternalPullRequestResult {
|
|
738
926
|
const base = resolvedContext(taskRef, options);
|
|
739
927
|
if (!base.ok) return externalResult(base.output);
|
|
928
|
+
if (!base.fact) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
929
|
+
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
930
|
+
}));
|
|
740
931
|
const inventory = inspectCompletionArtifacts(base.resolved.taskId, { repoRoot: base.resolved.repoRoot });
|
|
741
932
|
if (inventory.status === 'failed') return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
742
933
|
platform: base.context.platform,
|
|
@@ -791,32 +982,59 @@ function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOpt
|
|
|
791
982
|
error: { code: selected.code, message: selected.message, retryable: false }
|
|
792
983
|
}), { candidates: selected.candidates, eligible: selected.eligible });
|
|
793
984
|
|
|
794
|
-
const
|
|
795
|
-
|
|
985
|
+
const initialIdentity = validateWriterIdentity(base, selected.selected, { errorCode: 'PR_EXTERNAL_IDENTITY_MISMATCH' });
|
|
986
|
+
if (!initialIdentity.ok) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
987
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
988
|
+
pullRequest: selected.selected, error: initialIdentity.error
|
|
989
|
+
}), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
|
|
990
|
+
const rechecked = inspectGitHubPullRequest(base.client, base.context.platform.repository!, selected.selected.number, base.resolved.repoRoot);
|
|
991
|
+
if (!rechecked.ok || !rechecked.value) return externalResult(result(rechecked.ok ? 'failed' : rechecked.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
992
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
993
|
+
pullRequest: selected.selected,
|
|
994
|
+
error: rechecked.ok ? { code: 'PR_EXTERNAL_RECHECK_FAILED', message: 'Selected pull request recheck returned no identity', retryable: false } : rechecked.error
|
|
995
|
+
}), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
|
|
996
|
+
if (!sameDeliveryIdentity(selected.selected, rechecked.value)) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
997
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
998
|
+
pullRequest: rechecked.value,
|
|
999
|
+
error: { code: 'PR_EXTERNAL_IDENTITY_MISMATCH', message: 'Selected pull request identity changed before task binding', retryable: false }
|
|
1000
|
+
}), { candidates: selected.candidates, eligible: selected.eligible, selected: rechecked.value });
|
|
1001
|
+
const finalIdentity = validateWriterIdentity(base, rechecked.value, { errorCode: 'PR_EXTERNAL_IDENTITY_MISMATCH' });
|
|
1002
|
+
if (!finalIdentity.ok) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1003
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1004
|
+
pullRequest: rechecked.value, error: finalIdentity.error
|
|
1005
|
+
}), { candidates: selected.candidates, eligible: selected.eligible, selected: rechecked.value });
|
|
1006
|
+
const finalEvidence = validateExternalMergedEvidence(selected.selected, rechecked.value);
|
|
1007
|
+
if (!finalEvidence.ok) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1008
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1009
|
+
pullRequest: rechecked.value, error: finalEvidence.error
|
|
1010
|
+
}), { candidates: selected.candidates, eligible: selected.eligible, selected: rechecked.value });
|
|
1011
|
+
const selectedPullRequest = rechecked.value;
|
|
1012
|
+
const note = externalEvidenceNote(base.issueNumber, selected.source, selectedPullRequest);
|
|
1013
|
+
const identityNote = externalIdentityEvidenceNote(base.issueNumber, selectedPullRequest);
|
|
796
1014
|
const activityEntry = `**Bind External PR** by ${options.agent} — ${note}`;
|
|
797
1015
|
const auditLines = base.content.split(/\r?\n/).filter((line) => line.includes('**Bind External PR**'));
|
|
798
1016
|
const alreadyAudited = auditLines.some((line) => line.endsWith(identityNote));
|
|
799
1017
|
if (auditLines.length > 0 && !alreadyAudited) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
800
1018
|
platform: base.context.platform,
|
|
801
1019
|
capabilities: base.context.capabilities,
|
|
802
|
-
pullRequest:
|
|
1020
|
+
pullRequest: selectedPullRequest,
|
|
803
1021
|
error: { code: 'PR_BIND_CONFLICT', message: 'Existing external PR audit evidence conflicts with the selected identity', retryable: false }
|
|
804
|
-
}), { candidates: selected.candidates, eligible: selected.eligible, selected:
|
|
805
|
-
if (alreadyAudited && base.prNumber !==
|
|
1022
|
+
}), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
1023
|
+
if (alreadyAudited && base.prNumber !== selectedPullRequest.number) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
806
1024
|
platform: base.context.platform,
|
|
807
1025
|
capabilities: base.context.capabilities,
|
|
808
|
-
pullRequest:
|
|
1026
|
+
pullRequest: selectedPullRequest,
|
|
809
1027
|
error: { code: 'PR_BIND_CONFLICT', message: 'External PR audit evidence does not match the task binding', retryable: false }
|
|
810
|
-
}), { candidates: selected.candidates, eligible: selected.eligible, selected:
|
|
811
|
-
if (base.prNumber ===
|
|
1028
|
+
}), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
1029
|
+
if (base.prNumber === selectedPullRequest.number && alreadyAudited) {
|
|
812
1030
|
return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
813
1031
|
platform: base.context.platform,
|
|
814
1032
|
capabilities: base.context.capabilities,
|
|
815
|
-
resource: { kind: 'pull-request', number:
|
|
816
|
-
pullRequest:
|
|
1033
|
+
resource: { kind: 'pull-request', number: selectedPullRequest.number },
|
|
1034
|
+
pullRequest: selectedPullRequest,
|
|
817
1035
|
operations: [{ name: 'task:bind-external-pr', status: 'no-op', reasonCode: 'EVIDENCE_ALREADY_RECORDED' }],
|
|
818
1036
|
error: null
|
|
819
|
-
}), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected:
|
|
1037
|
+
}), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
820
1038
|
}
|
|
821
1039
|
const metadata = captureTaskWriteMetadata();
|
|
822
1040
|
const write = writeTask({
|
|
@@ -824,7 +1042,7 @@ function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOpt
|
|
|
824
1042
|
expectedState: 'active',
|
|
825
1043
|
dryRun: options.dryRun,
|
|
826
1044
|
mutations: [
|
|
827
|
-
{ kind: 'frontmatter', set: {
|
|
1045
|
+
{ kind: 'frontmatter', set: { ...factFrontmatterMutation(boundFactFor(selectedPullRequest, selected.source === 'unique' ? 'external-unique' : 'external-explicit', base.issueNumber, metadata.timestamp)).set, assigned_to: options.agent } },
|
|
828
1046
|
{ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(
|
|
829
1047
|
base.content, `- ${metadata.timestamp} — ${activityEntry}`
|
|
830
1048
|
) }
|
|
@@ -833,181 +1051,205 @@ function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOpt
|
|
|
833
1051
|
if (write.status === 'failed') return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
834
1052
|
platform: base.context.platform,
|
|
835
1053
|
capabilities: base.context.capabilities,
|
|
836
|
-
pullRequest:
|
|
1054
|
+
pullRequest: selectedPullRequest,
|
|
837
1055
|
error: { code: write.error.code, message: write.error.message, retryable: false }
|
|
838
|
-
}), { candidates: selected.candidates, eligible: selected.eligible, selected:
|
|
1056
|
+
}), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
839
1057
|
const operationStatus = options.dryRun ? 'planned' : write.status === 'applied' ? 'applied' : 'no-op';
|
|
840
|
-
return externalResult(result(operationStatus, base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber :
|
|
1058
|
+
return externalResult(result(operationStatus, base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : selectedPullRequest.number, {
|
|
841
1059
|
changed: !options.dryRun && write.changed,
|
|
842
1060
|
platform: base.context.platform,
|
|
843
1061
|
capabilities: base.context.capabilities,
|
|
844
|
-
resource: { kind: 'pull-request', number:
|
|
845
|
-
pullRequest:
|
|
1062
|
+
resource: { kind: 'pull-request', number: selectedPullRequest.number },
|
|
1063
|
+
pullRequest: selectedPullRequest,
|
|
846
1064
|
operations: [{ name: 'task:bind-external-pr', status: operationStatus, reasonCode: null }],
|
|
847
1065
|
error: null
|
|
848
|
-
}), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected:
|
|
1066
|
+
}), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
849
1067
|
}
|
|
850
1068
|
|
|
851
1069
|
function createPlatformPullRequestUnlocked(taskRef: string, options: CreateOptions): PullRequestResult {
|
|
852
1070
|
const base = resolvedContext(taskRef, options);
|
|
853
|
-
if (!base.ok) return base.output;
|
|
854
|
-
if (!
|
|
1071
|
+
if (!base.ok) return withCreation(base.output, PRECONDITION_NOT_CREATED);
|
|
1072
|
+
if (!base.fact) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1073
|
+
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
1074
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1075
|
+
if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
855
1076
|
error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
|
|
856
|
-
});
|
|
1077
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1078
|
+
const target = taskDeliveryTarget(base);
|
|
1079
|
+
if (!target.ok) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1080
|
+
error: target.error
|
|
1081
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1082
|
+
if (options.base !== target.value.baseRef) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1083
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1084
|
+
error: { code: 'PR_DELIVERY_TARGET_MISMATCH', message: `Pull request base '${options.base}' does not match delivery target '${target.value.baseRef}'`, retryable: false }
|
|
1085
|
+
}), PRECONDITION_NOT_CREATED);
|
|
857
1086
|
if (base.prNumber) {
|
|
858
1087
|
const headCheck = verifyCreateHead(base, options.head);
|
|
859
|
-
if (!headCheck.ok) return result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1088
|
+
if (!headCheck.ok) return withCreation(result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
860
1089
|
platform: base.context.platform,
|
|
861
1090
|
capabilities: base.context.capabilities,
|
|
862
1091
|
error: headCheck.error
|
|
863
|
-
});
|
|
1092
|
+
}), PRECONDITION_NOT_CREATED);
|
|
864
1093
|
const inspected = inspectPlatformPullRequest(taskRef, options);
|
|
865
1094
|
const identity = validateBoundPullRequest(base, inspected.pullRequest, options.head, options.base, headCheck.value);
|
|
866
|
-
if (!identity.ok) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1095
|
+
if (!identity.ok) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
867
1096
|
platform: base.context.platform,
|
|
868
1097
|
capabilities: base.context.capabilities,
|
|
869
1098
|
pullRequest: inspected.pullRequest,
|
|
870
1099
|
error: identity.error
|
|
871
|
-
});
|
|
872
|
-
return { ...inspected, result: 'no_op' };
|
|
1100
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1101
|
+
return withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
|
|
873
1102
|
}
|
|
874
1103
|
const headCheck = verifyCreateHead(base, options.head);
|
|
875
|
-
if (!headCheck.ok) return result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1104
|
+
if (!headCheck.ok) return withCreation(result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
|
|
876
1105
|
platform: base.context.platform,
|
|
877
1106
|
capabilities: base.context.capabilities,
|
|
878
1107
|
error: headCheck.error
|
|
879
|
-
});
|
|
1108
|
+
}), PRECONDITION_NOT_CREATED);
|
|
880
1109
|
const expectedHeadSha = headCheck.value;
|
|
881
1110
|
const located = locatePullRequest(base, options.head, options.base, expectedHeadSha);
|
|
882
|
-
if (!located.ok && located.error.code === 'PR_IDENTITY_AMBIGUOUS') return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1111
|
+
if (!located.ok && located.error.code === 'PR_IDENTITY_AMBIGUOUS') return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
883
1112
|
platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
|
|
884
|
-
});
|
|
885
|
-
if (!located.ok && located.error.code === 'PR_HEAD_SHA_MISMATCH') return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1113
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1114
|
+
if (!located.ok && located.error.code === 'PR_HEAD_SHA_MISMATCH') return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
886
1115
|
platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
|
|
887
|
-
});
|
|
888
|
-
if (
|
|
1116
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1117
|
+
if (!located.ok && located.error.code !== 'PR_NOT_FOUND') return withCreation(result(located.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1118
|
+
platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
|
|
1119
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1120
|
+
if (options.dryRun) return withCreation(result('planned', base.resolved.taskId, base.issueNumber, null, {
|
|
889
1121
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
890
1122
|
operations: [{ name: located.ok ? 'pr:reuse' : 'pr:create', status: 'planned', reasonCode: null }],
|
|
891
1123
|
result: located.ok ? 'pr_reused' : 'pr_created', error: null
|
|
892
|
-
});
|
|
1124
|
+
}), { kind: 'planned', action: located.ok ? 'reuse' : 'create', createdByCurrentOperation: false });
|
|
893
1125
|
const started = writeCreateStarted(base, options.agent);
|
|
894
|
-
if (started.status === 'failed') return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1126
|
+
if (started.status === 'failed') return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
895
1127
|
error: { code: started.error.code, message: started.error.message, retryable: false }
|
|
896
|
-
});
|
|
1128
|
+
}), PRECONDITION_NOT_CREATED);
|
|
897
1129
|
let pullRequest = located.ok ? located.value : null;
|
|
898
1130
|
let created = false;
|
|
899
1131
|
if (!pullRequest) {
|
|
900
1132
|
const repository = base.context.platform.repository!;
|
|
1133
|
+
if (options.phase) options.phase.value = 'post-dispatched';
|
|
901
1134
|
const response = base.client.json<RemotePullRequest>(['api', `repos/${repository}/pulls`, '-X', 'POST', '--input', '-'], {
|
|
902
1135
|
cwd: base.resolved.repoRoot,
|
|
903
1136
|
method: 'POST',
|
|
904
1137
|
input: JSON.stringify({ title: options.title, body: options.body, head: options.head, base: options.base, draft: Boolean(options.draft) })
|
|
905
1138
|
});
|
|
906
|
-
if (response.ok)
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1139
|
+
if (response.ok) {
|
|
1140
|
+
// The remote POST is accepted before any normalization, recheck, or task write.
|
|
1141
|
+
// From this point on the current call has created a remote resource even if
|
|
1142
|
+
// local post-processing later fails.
|
|
1143
|
+
created = true;
|
|
1144
|
+
if (options.phase) options.phase.value = 'post-accepted';
|
|
1145
|
+
pullRequest = normalizePullRequest(response.value, repository);
|
|
1146
|
+
} else if (response.error.retryable) {
|
|
1147
|
+
return withCreation(result('blocked', base.resolved.taskId, base.issueNumber, null, {
|
|
911
1148
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
912
1149
|
operations: [{ name: 'pr:create', status: 'failed', reasonCode: 'PR_CREATE_OUTCOME_UNKNOWN' }],
|
|
913
1150
|
error: { code: 'PR_CREATE_OUTCOME_UNKNOWN', message: response.error.message, retryable: true }
|
|
914
|
-
});
|
|
915
|
-
} else return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1151
|
+
}), { kind: 'unknown', errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' });
|
|
1152
|
+
} else return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
916
1153
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
917
1154
|
operations: [{ name: 'pr:create', status: 'failed', reasonCode: response.error.code }], error: response.error
|
|
918
|
-
});
|
|
919
|
-
if (!pullRequest) return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1155
|
+
}), { kind: 'not-created', reason: 'post-rejected', createdByCurrentOperation: false });
|
|
1156
|
+
if (!pullRequest) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
920
1157
|
error: { code: 'PR_CREATE_RESPONSE_INVALID', message: 'PR create response lacks validated identity', retryable: false }
|
|
921
|
-
});
|
|
922
|
-
if (pullRequest.head.sha !== expectedHeadSha) return result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1158
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1159
|
+
if (pullRequest.head.sha !== expectedHeadSha) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
923
1160
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
924
1161
|
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
|
|
925
1162
|
error: { code: 'PR_HEAD_SHA_MISMATCH', message: 'Created pull request head does not match the expected local HEAD', retryable: false }
|
|
926
|
-
});
|
|
927
|
-
created = true;
|
|
1163
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
928
1164
|
}
|
|
929
1165
|
const bindHeadCheck = verifyCreateHead(base, options.head);
|
|
930
1166
|
if (!bindHeadCheck.ok || bindHeadCheck.value !== expectedHeadSha) {
|
|
931
1167
|
const error = bindHeadCheck.ok
|
|
932
1168
|
? { code: 'PR_REMOTE_HEAD_MISMATCH', message: 'Remote head changed before task binding', retryable: false }
|
|
933
1169
|
: bindHeadCheck.error;
|
|
934
|
-
return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1170
|
+
return withCreation(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
935
1171
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
936
1172
|
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
|
|
937
|
-
});
|
|
1173
|
+
}), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
|
|
938
1174
|
}
|
|
939
1175
|
const beforeBind = inspectGitHubPullRequest(base.client, base.context.platform.repository!, pullRequest.number, base.resolved.repoRoot);
|
|
940
1176
|
if (!beforeBind.ok || !beforeBind.value) {
|
|
941
1177
|
const error = beforeBind.ok
|
|
942
1178
|
? { code: 'PR_BIND_RECHECK_FAILED', message: 'Pull request recheck returned no identity', retryable: false }
|
|
943
1179
|
: beforeBind.error;
|
|
944
|
-
return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1180
|
+
return withCreation(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
945
1181
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
946
1182
|
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
|
|
947
|
-
});
|
|
1183
|
+
}), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
|
|
948
1184
|
}
|
|
949
1185
|
if (
|
|
950
1186
|
beforeBind.value.head.sha !== expectedHeadSha
|
|
951
1187
|
|| beforeBind.value.head.ref !== expectedHead(base.context.platform.repository!, options.head).ref
|
|
952
1188
|
|| beforeBind.value.base.ref !== options.base
|
|
953
1189
|
|| beforeBind.value.base.repository !== base.context.platform.repository
|
|
954
|
-
) return result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1190
|
+
) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
955
1191
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
956
1192
|
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest: beforeBind.value,
|
|
957
1193
|
error: { code: 'PR_BIND_IDENTITY_MISMATCH', message: 'Pull request identity changed before task binding', retryable: false }
|
|
958
|
-
});
|
|
1194
|
+
}), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
|
|
959
1195
|
pullRequest = beforeBind.value;
|
|
960
1196
|
const refreshed = resolvedContext(taskRef, options);
|
|
961
|
-
if (!refreshed.ok) return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1197
|
+
if (!refreshed.ok) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
962
1198
|
pullRequest, error: { code: 'PR_CREATED_BIND_FAILED', message: pullRequest.url, retryable: false }
|
|
963
|
-
});
|
|
964
|
-
const bound = bindIdentity(refreshed, pullRequest, options.agent);
|
|
965
|
-
if (bound.status === 'failed') return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1199
|
+
}), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
|
|
1200
|
+
const bound = bindIdentity(refreshed, pullRequest, options.agent, created ? 'created' : 'reused', false, options.head, expectedHeadSha);
|
|
1201
|
+
if (bound.status === 'failed') return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
966
1202
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
967
1203
|
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
|
|
968
1204
|
operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: 'applied', reasonCode: null }, { name: 'task:bind-pr', status: 'failed', reasonCode: bound.error.code }],
|
|
969
1205
|
error: { code: 'PR_CREATED_BIND_FAILED', message: `${pullRequest.url}: ${bound.error.message}`, retryable: false }
|
|
970
|
-
});
|
|
971
|
-
return result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1206
|
+
}), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'reused', createdByCurrentOperation: false });
|
|
1207
|
+
return withCreation(result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
972
1208
|
changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
|
|
973
1209
|
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
|
|
974
1210
|
operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: created ? 'applied' : 'no-op', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }],
|
|
975
1211
|
result: created ? 'pr_created' : 'pr_reused', error: null
|
|
976
|
-
});
|
|
1212
|
+
}), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'reused', createdByCurrentOperation: false });
|
|
977
1213
|
}
|
|
978
1214
|
|
|
979
1215
|
function createPlatformPullRequest(taskRef: string, options: CreateOptions): PullRequestResult {
|
|
980
1216
|
const base = resolvedContext(taskRef, options);
|
|
981
|
-
if (!base.ok) return base.output;
|
|
1217
|
+
if (!base.ok) return withCreation(base.output, PRECONDITION_NOT_CREATED);
|
|
982
1218
|
if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) {
|
|
983
|
-
return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1219
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
984
1220
|
error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
|
|
985
|
-
});
|
|
1221
|
+
}), PRECONDITION_NOT_CREATED);
|
|
986
1222
|
}
|
|
1223
|
+
const phase = { value: 'before-post' as CreatePhase };
|
|
987
1224
|
try {
|
|
988
1225
|
return withTaskExecutionLock(
|
|
989
1226
|
base.resolved.repoRoot,
|
|
990
1227
|
base.resolved.taskId,
|
|
991
1228
|
'platform-pr.create',
|
|
992
|
-
() => createPlatformPullRequestUnlocked(taskRef, options)
|
|
1229
|
+
() => createPlatformPullRequestUnlocked(taskRef, { ...options, phase })
|
|
993
1230
|
);
|
|
994
1231
|
} catch (error) {
|
|
995
1232
|
if (error instanceof TaskExecutionLockError) {
|
|
996
|
-
return result('blocked', base.resolved.taskId, base.issueNumber, null, {
|
|
1233
|
+
return withCreation(result('blocked', base.resolved.taskId, base.issueNumber, null, {
|
|
997
1234
|
platform: base.context.platform,
|
|
998
1235
|
capabilities: base.context.capabilities,
|
|
999
1236
|
error: { code: error.code, message: error.message, retryable: true }
|
|
1000
|
-
});
|
|
1237
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1001
1238
|
}
|
|
1002
|
-
|
|
1239
|
+
const creation: CreationOutcome = phase.value === 'post-accepted'
|
|
1240
|
+
? { kind: 'created', createdByCurrentOperation: true }
|
|
1241
|
+
: phase.value === 'post-dispatched'
|
|
1242
|
+
? { kind: 'unknown', errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' }
|
|
1243
|
+
: PRECONDITION_NOT_CREATED;
|
|
1244
|
+
return withCreation(result(phase.value === 'post-dispatched' ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1003
1245
|
platform: base.context.platform,
|
|
1004
1246
|
capabilities: base.context.capabilities,
|
|
1005
1247
|
error: {
|
|
1006
|
-
code: 'PR_CREATE_FAILED',
|
|
1248
|
+
code: phase.value === 'post-dispatched' ? 'PR_CREATE_OUTCOME_UNKNOWN' : 'PR_CREATE_FAILED',
|
|
1007
1249
|
message: error instanceof Error ? error.message : String(error),
|
|
1008
|
-
retryable:
|
|
1250
|
+
retryable: phase.value === 'post-dispatched'
|
|
1009
1251
|
}
|
|
1010
|
-
});
|
|
1252
|
+
}), creation);
|
|
1011
1253
|
}
|
|
1012
1254
|
}
|
|
1013
1255
|
|
|
@@ -1031,8 +1273,11 @@ function syncPlatformPullRequest(taskRef: string, options: SyncOptions): PullReq
|
|
|
1031
1273
|
};
|
|
1032
1274
|
const base = resolvedContext(taskRef, options);
|
|
1033
1275
|
if (!base.ok) return softenFailure(base.output);
|
|
1276
|
+
if (!base.fact) return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1277
|
+
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
1278
|
+
}));
|
|
1034
1279
|
if (!base.prNumber) return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1035
|
-
error: { code: 'PR_NOT_LINKED', message: 'Task has no
|
|
1280
|
+
error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
|
|
1036
1281
|
}));
|
|
1037
1282
|
const fetched = inspectGitHubPullRequest(base.client, base.context.platform.repository!, base.prNumber, base.resolved.repoRoot);
|
|
1038
1283
|
if (!fetched.ok) return softenFailure(result(fetched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { error: fetched.error }));
|
|
@@ -1049,7 +1294,7 @@ function syncPlatformPullRequest(taskRef: string, options: SyncOptions): PullReq
|
|
|
1049
1294
|
const planned = planPullRequestMetadata({
|
|
1050
1295
|
pullRequest: fetched.value,
|
|
1051
1296
|
issue: issue.issue,
|
|
1052
|
-
taskType: base.frontmatter.type || 'task',
|
|
1297
|
+
taskType: String(base.frontmatter.type || 'task'),
|
|
1053
1298
|
issueNumber: base.issueNumber,
|
|
1054
1299
|
capabilities: base.context.capabilities
|
|
1055
1300
|
}).operations.filter((operation) => options.metadata || operation.name === 'closing-issue')
|
|
@@ -1088,6 +1333,98 @@ function syncPlatformPullRequest(taskRef: string, options: SyncOptions): PullReq
|
|
|
1088
1333
|
}));
|
|
1089
1334
|
}
|
|
1090
1335
|
|
|
1336
|
+
function readProjectPrFlow(repoRoot: string): 'required' | 'disabled' | undefined {
|
|
1337
|
+
try {
|
|
1338
|
+
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8')) as { prFlow?: unknown };
|
|
1339
|
+
return config.prFlow === 'required' || config.prFlow === 'disabled' ? config.prFlow : undefined;
|
|
1340
|
+
} catch {
|
|
1341
|
+
return undefined;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function skipPlatformPullRequestFactUnlocked(
|
|
1346
|
+
resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
|
|
1347
|
+
options: SkipFactOptions
|
|
1348
|
+
): PullRequestResult {
|
|
1349
|
+
let content: string;
|
|
1350
|
+
let frontmatter: ReturnType<typeof parseTypedTaskFrontmatter>;
|
|
1351
|
+
try {
|
|
1352
|
+
content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
1353
|
+
frontmatter = parseTypedTaskFrontmatter(content);
|
|
1354
|
+
} catch (error) {
|
|
1355
|
+
return result('failed', resolved.taskId, null, null, {
|
|
1356
|
+
error: { code: 'TASK_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
const issue = Number(frontmatter.issue_number);
|
|
1360
|
+
const issueNumber = Number.isSafeInteger(issue) && issue > 0 ? issue : null;
|
|
1361
|
+
const existing = readPrDeliveryFact(frontmatter);
|
|
1362
|
+
if (existing.status === 'invalid') return result('failed', resolved.taskId, issueNumber, null, {
|
|
1363
|
+
error: { code: 'PR_DELIVERY_FACT_INVALID', message: existing.error.message, retryable: false }
|
|
1364
|
+
});
|
|
1365
|
+
if (existing.status === 'missing') return result('failed', resolved.taskId, issueNumber, null, {
|
|
1366
|
+
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
1367
|
+
});
|
|
1368
|
+
if (existing.fact.state === 'skipped') return result('no-op', resolved.taskId, issueNumber, null, {
|
|
1369
|
+
operations: [{ name: 'task:skip-pr', status: 'no-op', reasonCode: null }],
|
|
1370
|
+
error: null
|
|
1371
|
+
});
|
|
1372
|
+
if (existing.fact.state === 'bound') return result('failed', resolved.taskId, issueNumber, existing.fact.identity.number, {
|
|
1373
|
+
error: { code: 'PR_DELIVERY_FACT_CONFLICT', message: `Task is already bound to PR #${existing.fact.identity.number}`, retryable: false }
|
|
1374
|
+
});
|
|
1375
|
+
if (readProjectPrFlow(resolved.repoRoot) === 'required') return result('failed', resolved.taskId, issueNumber, null, {
|
|
1376
|
+
error: { code: 'PR_SKIP_FORBIDDEN', message: 'Cannot skip PR delivery when prFlow is required', retryable: false }
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
const metadata = captureTaskWriteMetadata();
|
|
1380
|
+
const fact = buildSkippedFact(factTimestamp(metadata.timestamp));
|
|
1381
|
+
const write = writeTask({
|
|
1382
|
+
taskRef: resolved.taskId,
|
|
1383
|
+
expectedState: 'active',
|
|
1384
|
+
dryRun: options.dryRun,
|
|
1385
|
+
mutations: [
|
|
1386
|
+
{ kind: 'frontmatter', set: { ...factFrontmatterMutation(fact).set, assigned_to: options.agent } },
|
|
1387
|
+
{ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(
|
|
1388
|
+
content, `- ${metadata.timestamp} — **Skip PR** by ${options.agent} — PR delivery explicitly skipped`
|
|
1389
|
+
) }
|
|
1390
|
+
]
|
|
1391
|
+
}, { repoRoot: resolved.repoRoot, metadataProvider: () => metadata });
|
|
1392
|
+
if (write.status === 'failed') return result('failed', resolved.taskId, issueNumber, null, {
|
|
1393
|
+
error: { code: write.error.code, message: write.error.message, retryable: false }
|
|
1394
|
+
});
|
|
1395
|
+
const status = options.dryRun ? 'planned' : write.status === 'no-op' ? 'no-op' : 'applied';
|
|
1396
|
+
return result(status, resolved.taskId, issueNumber, null, {
|
|
1397
|
+
changed: !options.dryRun && write.changed,
|
|
1398
|
+
operations: [{ name: 'task:skip-pr', status, reasonCode: null }],
|
|
1399
|
+
error: null
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function skipPlatformPullRequestFact(taskRef: string, options: SkipFactOptions): PullRequestResult {
|
|
1404
|
+
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
1405
|
+
if (!resolved.ok) return result('failed', resolved.taskId, null, null, {
|
|
1406
|
+
error: { code: resolved.code, message: resolved.message, retryable: false }
|
|
1407
|
+
});
|
|
1408
|
+
if (resolved.state !== 'active') return result('failed', resolved.taskId, null, null, {
|
|
1409
|
+
error: { code: 'TASK_STATE_INVALID', message: 'platform-pr skip requires an active task', retryable: false }
|
|
1410
|
+
});
|
|
1411
|
+
try {
|
|
1412
|
+
return withTaskExecutionLock(
|
|
1413
|
+
resolved.repoRoot,
|
|
1414
|
+
resolved.taskId,
|
|
1415
|
+
'platform-pr.skip',
|
|
1416
|
+
() => skipPlatformPullRequestFactUnlocked(resolved, options)
|
|
1417
|
+
);
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
if (error instanceof TaskExecutionLockError) return result('blocked', resolved.taskId, null, null, {
|
|
1420
|
+
error: { code: error.code, message: error.message, retryable: true }
|
|
1421
|
+
});
|
|
1422
|
+
return result('failed', resolved.taskId, null, null, {
|
|
1423
|
+
error: { code: 'PR_SKIP_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1091
1428
|
export {
|
|
1092
1429
|
bindPlatformPullRequest,
|
|
1093
1430
|
createPlatformPullRequest,
|
|
@@ -1098,9 +1435,10 @@ export {
|
|
|
1098
1435
|
normalizePullRequest,
|
|
1099
1436
|
resolveGitHubChangeRequestGitEvidence,
|
|
1100
1437
|
resolveExternalPullRequest,
|
|
1438
|
+
skipPlatformPullRequestFact,
|
|
1101
1439
|
selectExternalPullRequest,
|
|
1102
1440
|
selectPullRequest,
|
|
1103
1441
|
syncPlatformPullRequest
|
|
1104
1442
|
};
|
|
1105
|
-
export type { BindOptions, CreateOptions, ExternalPullRequestResult, ExternalPullRequestSelection, PullRequestPrimaryResult, PullRequestResult, PullRequestSnapshot, ResolveExternalOptions, SyncOptions };
|
|
1443
|
+
export type { BindOptions, CreateOptions, ExternalPullRequestResult, ExternalPullRequestSelection, PullRequestPrimaryResult, PullRequestResult, PullRequestSnapshot, ResolveExternalOptions, SkipFactOptions, SyncOptions };
|
|
1106
1444
|
export { warningResultForPrimary };
|