@fitlab-ai/agent-infra 0.9.11 → 0.9.13
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 +13 -13
- package/README.zh-CN.md +13 -13
- package/dist/bin/internal-cli.js +29 -19
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +71 -10
- package/dist/lib/agent-clients/next-steps.js +6 -1
- package/dist/lib/decide.js +3 -3
- 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-checks.js +3 -3
- package/dist/lib/internal/platform-comment.js +9 -9
- package/dist/lib/internal/platform-context.js +2 -2
- package/dist/lib/internal/platform-issue.js +8 -9
- package/dist/lib/internal/platform-pr-review.js +10 -10
- package/dist/lib/internal/platform-pr.js +68 -23
- package/dist/lib/internal/platform-release-notes.js +3 -3
- package/dist/lib/internal/pr-review-grade.js +11 -9
- package/dist/lib/internal/release-workflow.js +4 -4
- package/dist/lib/internal/sandbox-control.js +30 -4
- package/dist/lib/internal/task-activity.js +3 -3
- package/dist/lib/internal/task-artifact.js +76 -24
- package/dist/lib/internal/task-create.js +2 -2
- package/dist/lib/internal/task-delivery.js +59 -0
- package/dist/lib/internal/task-event.js +6 -3
- package/dist/lib/internal/task-finalization.js +2 -2
- package/dist/lib/internal/task-invalidation.js +60 -0
- package/dist/lib/internal/task-ledger.js +9 -6
- package/dist/lib/internal/task-lifecycle.js +2 -2
- package/dist/lib/internal/task-override.js +4 -4
- package/dist/lib/internal/task-review.js +3 -3
- package/dist/lib/internal/task-verify.js +6 -6
- package/dist/lib/internal/task-warning.js +3 -3
- package/dist/lib/platform/adapters.js +1 -3
- package/dist/lib/platform/change-request-git-evidence.js +27 -7
- package/dist/lib/platform/completion-backfill.js +6 -5
- package/dist/lib/platform/context.js +70 -130
- package/dist/lib/platform/github-client.js +8 -2
- package/dist/lib/platform/github-provider.js +903 -0
- package/dist/lib/platform/in-label-sync.js +143 -0
- package/dist/lib/platform/issue-comments.js +94 -41
- package/dist/lib/platform/issues.js +335 -189
- package/dist/lib/platform/metadata-labels.js +6 -2
- package/dist/lib/platform/none-provider.js +28 -0
- package/dist/lib/platform/pr-change-report.js +551 -0
- package/dist/lib/platform/pr-checks.js +124 -106
- package/dist/lib/platform/pr-review.js +88 -72
- package/dist/lib/platform/pr-summary.js +523 -49
- package/dist/lib/platform/provider-bridge.js +45 -0
- package/dist/lib/platform/provider-contract.d.ts +487 -0
- package/dist/lib/platform/provider-contract.js +95 -0
- package/dist/lib/platform/provider-loader.js +251 -0
- package/dist/lib/platform/provider-validation.js +504 -0
- package/dist/lib/platform/pull-request-metadata.js +8 -6
- package/dist/lib/platform/pull-requests.js +1050 -502
- package/dist/lib/platform/release-notes.js +78 -55
- package/dist/lib/platform/releases.js +79 -50
- package/dist/lib/platform/resource-identity.d.ts +26 -0
- package/dist/lib/platform/resource-identity.js +92 -0
- package/dist/lib/platform/task-identities.js +31 -0
- package/dist/lib/platform/verification-required.js +22 -12
- package/dist/lib/platform/verification-sync.js +118 -239
- package/dist/lib/pr-review/evidence-grading.js +30 -9
- package/dist/lib/run/index.js +63 -23
- package/dist/lib/sandbox/config.js +5 -0
- package/dist/lib/sandbox/control/client.js +96 -15
- package/dist/lib/sandbox/control/executor.js +3 -3
- package/dist/lib/sandbox/control/lifecycle.js +29 -7
- package/dist/lib/sandbox/control/protocol.js +77 -0
- package/dist/lib/sandbox/control/server.js +398 -25
- package/dist/lib/sandbox/control/state.js +204 -1
- package/dist/lib/server/process-state.js +2 -2
- package/dist/lib/server/protocol.js +38 -1
- package/dist/lib/task/activity-log.js +8 -1
- package/dist/lib/task/artifact-lifecycle.js +35 -7
- package/dist/lib/task/capabilities.js +239 -0
- package/dist/lib/task/commands/status.js +32 -0
- package/dist/lib/task/commit-intent.js +100 -0
- package/dist/lib/task/commit-operation.js +207 -102
- package/dist/lib/task/control-authority.js +8 -5
- package/dist/lib/task/create-service.js +8 -8
- package/dist/lib/task/create.js +33 -7
- package/dist/lib/task/current-contract.js +5 -1
- 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/events.js +297 -17
- package/dist/lib/task/finalization.js +37 -13
- package/dist/lib/task/human-override.js +61 -36
- package/dist/lib/task/invalidation-command.js +86 -0
- package/dist/lib/task/invalidation.js +260 -0
- package/dist/lib/task/ledger-intents.js +37 -0
- package/dist/lib/task/lifecycle.js +106 -9
- package/dist/lib/task/local-artifact-finalization.js +374 -0
- package/dist/lib/task/orchestration.js +100 -151
- package/dist/lib/task/pr-delivery-fact.js +183 -0
- package/dist/lib/task/review-finalization.js +4 -8
- package/dist/lib/task/review-fingerprint.js +32 -2
- package/dist/lib/task/rework-intent.js +138 -0
- package/dist/lib/task/skill-scope.js +23 -0
- package/dist/lib/task/task-execution-lock.js +27 -17
- package/dist/lib/task/verification-engine.js +89 -38
- package/dist/lib/task/verification-review.js +1 -1
- package/dist/lib/task/verification.js +3 -3
- package/dist/lib/task/workflow-warning-intents.js +1 -1
- package/dist/lib/task/write.js +11 -0
- package/dist/lib/update.js +9 -0
- package/dist/lib/version.js +11 -1
- 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/agent-clients/next-steps.ts +6 -1
- package/lib/decide.ts +3 -3
- package/lib/defaults.json +4 -0
- package/lib/git/workflow.ts +38 -1
- package/lib/init.ts +4 -1
- package/lib/internal/git-workflow.ts +7 -1
- package/lib/internal/platform-checks.ts +3 -3
- package/lib/internal/platform-comment.ts +8 -8
- package/lib/internal/platform-context.ts +2 -2
- package/lib/internal/platform-issue.ts +7 -8
- package/lib/internal/platform-pr-review.ts +9 -9
- package/lib/internal/platform-pr.ts +53 -21
- package/lib/internal/platform-release-notes.ts +3 -3
- package/lib/internal/pr-review-grade.ts +10 -8
- package/lib/internal/release-workflow.ts +4 -4
- package/lib/internal/sandbox-control.ts +27 -3
- package/lib/internal/task-activity.ts +3 -3
- package/lib/internal/task-artifact.ts +65 -22
- package/lib/internal/task-create.ts +2 -2
- package/lib/internal/task-delivery.ts +53 -0
- package/lib/internal/task-event.ts +6 -3
- package/lib/internal/task-finalization.ts +3 -3
- package/lib/internal/task-invalidation.ts +39 -0
- package/lib/internal/task-ledger.ts +11 -8
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-override.ts +6 -6
- package/lib/internal/task-review.ts +3 -3
- package/lib/internal/task-verify.ts +6 -6
- package/lib/internal/task-warning.ts +3 -3
- package/lib/platform/adapters.ts +3 -2
- package/lib/platform/change-request-git-evidence.ts +36 -9
- package/lib/platform/completion-backfill.ts +9 -8
- package/lib/platform/context.ts +97 -134
- package/lib/platform/github-client.ts +10 -3
- package/lib/platform/github-provider.ts +910 -0
- package/lib/platform/in-label-sync.ts +208 -0
- package/lib/platform/issue-comments.ts +97 -47
- package/lib/platform/issues.ts +344 -174
- package/lib/platform/metadata-labels.ts +5 -2
- package/lib/platform/none-provider.ts +35 -0
- package/lib/platform/pr-change-report.ts +645 -0
- package/lib/platform/pr-checks.ts +129 -107
- package/lib/platform/pr-review.ts +88 -85
- package/lib/platform/pr-summary.ts +562 -52
- package/lib/platform/provider-bridge.ts +82 -0
- package/lib/platform/provider-contract.ts +536 -0
- package/lib/platform/provider-loader.ts +307 -0
- package/lib/platform/provider-validation.ts +529 -0
- package/lib/platform/pull-request-metadata.ts +10 -7
- package/lib/platform/pull-requests.ts +1126 -505
- package/lib/platform/release-notes.ts +76 -59
- package/lib/platform/releases.ts +77 -43
- package/lib/platform/resource-identity.ts +121 -0
- package/lib/platform/task-identities.ts +40 -0
- package/lib/platform/types.ts +4 -2
- package/lib/platform/verification-required.ts +21 -13
- package/lib/platform/verification-sync.ts +118 -293
- package/lib/pr-review/evidence-grading.ts +31 -11
- package/lib/run/index.ts +57 -22
- package/lib/sandbox/config.ts +7 -0
- package/lib/sandbox/control/client.ts +105 -15
- package/lib/sandbox/control/executor.ts +4 -4
- package/lib/sandbox/control/lifecycle.ts +30 -8
- package/lib/sandbox/control/protocol.ts +113 -0
- package/lib/sandbox/control/server.ts +431 -23
- package/lib/sandbox/control/state.ts +251 -1
- package/lib/server/process-state.ts +2 -2
- package/lib/server/protocol.ts +36 -1
- package/lib/task/activity-log.ts +8 -0
- package/lib/task/artifact-lifecycle.ts +34 -8
- package/lib/task/capabilities.ts +262 -0
- package/lib/task/commands/status.ts +38 -0
- package/lib/task/commit-intent.ts +137 -0
- package/lib/task/commit-operation.ts +186 -88
- package/lib/task/control-authority.ts +11 -8
- package/lib/task/create-service.ts +8 -8
- package/lib/task/create.ts +34 -9
- package/lib/task/current-contract.ts +6 -1
- package/lib/task/decision-details.ts +8 -64
- package/lib/task/delivery-target.ts +130 -0
- package/lib/task/delivery.ts +160 -0
- package/lib/task/events.ts +296 -16
- package/lib/task/finalization.ts +50 -14
- package/lib/task/human-override.ts +73 -46
- package/lib/task/invalidation-command.ts +86 -0
- package/lib/task/invalidation.ts +332 -0
- package/lib/task/ledger-intents.ts +35 -1
- package/lib/task/lifecycle.ts +93 -4
- package/lib/task/local-artifact-finalization.ts +563 -0
- package/lib/task/orchestration.ts +110 -146
- package/lib/task/pr-delivery-fact.ts +237 -0
- package/lib/task/review-finalization.ts +4 -14
- package/lib/task/review-fingerprint.ts +33 -2
- package/lib/task/rework-intent.ts +165 -0
- package/lib/task/skill-scope.ts +24 -0
- package/lib/task/task-execution-lock.ts +23 -14
- package/lib/task/verification-engine.ts +95 -43
- package/lib/task/verification-review.ts +2 -0
- package/lib/task/verification.ts +4 -4
- package/lib/task/workflow-warning-intents.ts +1 -1
- package/lib/task/write.ts +19 -0
- package/lib/update.ts +11 -1
- package/lib/version.ts +15 -1
- package/package.json +7 -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-pr-commands.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +28 -5
- package/templates/.agents/rules/issue-sync.zh-CN.md +64 -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 +53 -0
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +53 -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 +28 -13
- package/templates/.agents/rules/pr-sync.zh-CN.md +39 -5
- 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/rules/task-short-id.en.md +1 -4
- package/templates/.agents/rules/task-short-id.zh-CN.md +1 -3
- 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 +14 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -3
- package/templates/.agents/skills/check-task/SKILL.en.md +16 -37
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +16 -37
- 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 +32 -10
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +35 -13
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -0
- 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/issue-metadata-sync.en.md +2 -0
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +2 -0
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +4 -4
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +4 -4
- 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 +7 -4
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +7 -4
- 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 +5 -4
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +5 -4
- package/templates/.agents/skills/complete-task/SKILL.en.md +31 -13
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +30 -13
- 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 +11 -10
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +11 -11
- package/templates/.agents/skills/create-pr/config/verify.json +1 -0
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +16 -4
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +16 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +5 -3
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +5 -3
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/scripts/change-report.mjs +3 -0
- 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 +14 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +11 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +6 -5
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +6 -5
- 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 +10 -9
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +10 -9
- 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 +6 -5
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +6 -5
- 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 +5 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +5 -2
- 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 +6 -6
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +6 -0
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +6 -0
- package/templates/.agents/templates/task.en.md +23 -2
- package/templates/.agents/templates/task.zh-CN.md +23 -2
- 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/.github/workflows/pr-label.yml +7 -26
- 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,18 +1,25 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import {
|
|
4
|
+
import { parseTypedTaskFrontmatter } from '../task/frontmatter.js';
|
|
4
5
|
import { resolveTaskRef } from '../task/resolve-ref.js';
|
|
5
6
|
import { extractSection } from '../task/sections.js';
|
|
6
7
|
import { captureTaskWriteMetadata, writeTask } from '../task/write.js';
|
|
7
|
-
import {
|
|
8
|
-
import { parseGitHubRemote, resolvePlatformContext } from './context.js';
|
|
8
|
+
import { parseGitHubRemote, resolvePlatformContext, resolvePlatformProviderContext } from './context.js';
|
|
9
9
|
import { createGitHubClient } from './github-client.js';
|
|
10
10
|
import { inspectPlatformIssue } from './issues.js';
|
|
11
|
+
import { inspectGitHubIssue } from './issues.js';
|
|
11
12
|
import { planPullRequestMetadata } from './pull-request-metadata.js';
|
|
13
|
+
import { extractPullRequestFileNames, planInLabelUpdate, syncLabelDelta, validateInLabelMapping, validateRepositoryLabelPayload } from './in-label-sync.js';
|
|
12
14
|
import { platformResult } from './types.js';
|
|
13
15
|
import { inspectCompletionArtifacts } from '../task/finalization-artifacts.js';
|
|
14
16
|
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
|
|
17
|
+
import { resolveDeliveryTarget } from '../task/delivery-target.js';
|
|
15
18
|
import { mergeOperationWarnings } from '../task/operation-outcome.js';
|
|
19
|
+
import { buildBoundFact, buildSkippedFact, factFrontmatterMutation, readPrDeliveryFact } from '../task/pr-delivery-fact.js';
|
|
20
|
+
import { providerError, providerOperationContext, providerStatus, providerResourceToken, providerResourceIdentity, resourceIdentity, unsupportedProviderOperation } from './provider-bridge.js';
|
|
21
|
+
import { isResourceIdentity, resourceIdentityEquals, resourceIdentityNumber } from './resource-identity.js';
|
|
22
|
+
import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.js';
|
|
16
23
|
function warningResultForPrimary(primaryResult) {
|
|
17
24
|
if (primaryResult === 'pr_created')
|
|
18
25
|
return 'pr_created_with_warnings';
|
|
@@ -26,6 +33,7 @@ function result(status, taskId, issueNumber, prNumber, overrides = {}) {
|
|
|
26
33
|
task: { id: taskId, issueNumber, prNumber },
|
|
27
34
|
pullRequest: null,
|
|
28
35
|
result: null,
|
|
36
|
+
creation: null,
|
|
29
37
|
warnings: [],
|
|
30
38
|
...overrides
|
|
31
39
|
};
|
|
@@ -42,6 +50,12 @@ function externalResult(base, overrides = {}) {
|
|
|
42
50
|
...overrides
|
|
43
51
|
};
|
|
44
52
|
}
|
|
53
|
+
function withCreation(output, creation) {
|
|
54
|
+
return { ...output, creation };
|
|
55
|
+
}
|
|
56
|
+
const PRECONDITION_NOT_CREATED = {
|
|
57
|
+
kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false
|
|
58
|
+
};
|
|
45
59
|
function normalizePullRequest(remote, repository) {
|
|
46
60
|
const number = Number(remote.number);
|
|
47
61
|
const headRepository = remote.head?.repo?.full_name;
|
|
@@ -95,9 +109,15 @@ function normalizeClosingPullRequest(node, repository) {
|
|
|
95
109
|
function repositoryKey(repository) {
|
|
96
110
|
return repository.trim().toLowerCase();
|
|
97
111
|
}
|
|
112
|
+
function isUsableResourceIdentity(identity) {
|
|
113
|
+
return isResourceIdentity(identity);
|
|
114
|
+
}
|
|
115
|
+
function identityLabel(identity) {
|
|
116
|
+
return identity.kind === 'number' ? `PR #${identity.value}` : `PR ${identity.kind}:${identity.value}`;
|
|
117
|
+
}
|
|
98
118
|
function identitySignature(pullRequest) {
|
|
99
119
|
return [
|
|
100
|
-
pullRequest.repository, pullRequest
|
|
120
|
+
pullRequest.repository, JSON.stringify(pullRequestIdentity(pullRequest)), pullRequest.nodeId, pullRequest.url,
|
|
101
121
|
pullRequest.head.repository, pullRequest.head.ref, pullRequest.head.sha,
|
|
102
122
|
pullRequest.base.repository, pullRequest.base.ref, pullRequest.base.sha,
|
|
103
123
|
pullRequest.mergedAt || '', pullRequest.mergeCommitSha || ''
|
|
@@ -109,47 +129,52 @@ function hasCompleteExternalIdentity(pullRequest) {
|
|
|
109
129
|
pullRequest.head.repository, pullRequest.head.ref, pullRequest.head.sha,
|
|
110
130
|
pullRequest.base.repository, pullRequest.base.ref, pullRequest.base.sha
|
|
111
131
|
];
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
Boolean(pullRequest.mergedAt) === Boolean(pullRequest.mergeCommitSha);
|
|
132
|
+
return isUsableResourceIdentity(pullRequestIdentity(pullRequest)) && required.every((value) => Boolean(value?.trim())) &&
|
|
133
|
+
(pullRequest.state === 'open' || Boolean(pullRequest.mergedAt) === Boolean(pullRequest.mergeCommitSha));
|
|
115
134
|
}
|
|
116
|
-
function selectExternalPullRequest(candidates, repository,
|
|
135
|
+
function selectExternalPullRequest(candidates, repository, existingPrIdentity, explicitPrIdentity) {
|
|
136
|
+
const existingIdentity = typeof existingPrIdentity === 'number'
|
|
137
|
+
? resourceIdentity(existingPrIdentity) : existingPrIdentity;
|
|
138
|
+
const explicitIdentity = typeof explicitPrIdentity === 'number'
|
|
139
|
+
? resourceIdentity(explicitPrIdentity) : explicitPrIdentity;
|
|
117
140
|
const invalid = candidates.find((candidate) => !hasCompleteExternalIdentity(candidate));
|
|
118
141
|
if (invalid)
|
|
119
142
|
return {
|
|
120
143
|
status: 'failed', code: 'PR_IDENTITY_INVALID',
|
|
121
144
|
message: `Closing PR #${invalid.number} lacks required identity`, candidates, eligible: []
|
|
122
145
|
};
|
|
123
|
-
const
|
|
146
|
+
const byIdentity = new Map();
|
|
124
147
|
for (const candidate of candidates) {
|
|
125
148
|
const signature = identitySignature(candidate);
|
|
126
|
-
const
|
|
149
|
+
const identityKey = JSON.stringify(pullRequestIdentity(candidate));
|
|
150
|
+
const prior = byIdentity.get(identityKey);
|
|
127
151
|
if (prior && prior !== signature)
|
|
128
152
|
return {
|
|
129
153
|
status: 'failed', code: 'PR_IDENTITY_INVALID',
|
|
130
154
|
message: `Closing PR #${candidate.number} has conflicting identities`, candidates, eligible: []
|
|
131
155
|
};
|
|
132
|
-
|
|
156
|
+
byIdentity.set(identityKey, signature);
|
|
133
157
|
}
|
|
134
|
-
const unique = candidates.filter((candidate, index) => candidates.findIndex((other) => other
|
|
158
|
+
const unique = candidates.filter((candidate, index) => candidates.findIndex((other) => resourceIdentityEquals(pullRequestIdentity(other), pullRequestIdentity(candidate))) === index);
|
|
135
159
|
const wantedRepository = repositoryKey(repository);
|
|
136
160
|
const eligible = unique.filter((candidate) => repositoryKey(candidate.repository) === wantedRepository &&
|
|
137
161
|
repositoryKey(candidate.base.repository) === wantedRepository &&
|
|
162
|
+
candidate.state === 'closed' &&
|
|
138
163
|
Boolean(candidate.mergedAt && candidate.mergeCommitSha));
|
|
139
|
-
if (
|
|
140
|
-
const selected = unique.find((candidate) => candidate
|
|
164
|
+
if (explicitIdentity !== null) {
|
|
165
|
+
const selected = unique.find((candidate) => resourceIdentityEquals(pullRequestIdentity(candidate), explicitIdentity));
|
|
141
166
|
if (!selected)
|
|
142
|
-
return { status: 'failed', code: 'PR_NOT_FOUND', message: `PR
|
|
143
|
-
if (!eligible.some((candidate) => candidate
|
|
144
|
-
return { status: 'failed', code: 'PR_IDENTITY_INVALID', message: `PR
|
|
167
|
+
return { status: 'failed', code: 'PR_NOT_FOUND', message: `PR ${identityLabel(explicitIdentity)} is not a closing PR`, candidates, eligible };
|
|
168
|
+
if (!eligible.some((candidate) => resourceIdentityEquals(pullRequestIdentity(candidate), explicitIdentity))) {
|
|
169
|
+
return { status: 'failed', code: 'PR_IDENTITY_INVALID', message: `PR ${identityLabel(explicitIdentity)} is not an eligible merged PR`, candidates, eligible };
|
|
145
170
|
}
|
|
146
|
-
if (
|
|
147
|
-
return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to
|
|
171
|
+
if (existingIdentity !== null && !resourceIdentityEquals(existingIdentity, explicitIdentity)) {
|
|
172
|
+
return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to ${identityLabel(existingIdentity)}`, candidates, eligible };
|
|
148
173
|
}
|
|
149
174
|
return { status: 'selected', source: 'explicit', selected, candidates, eligible };
|
|
150
175
|
}
|
|
151
|
-
if (
|
|
152
|
-
return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to
|
|
176
|
+
if (existingIdentity !== null && eligible.length === 1 && !resourceIdentityEquals(existingIdentity, pullRequestIdentity(eligible[0]))) {
|
|
177
|
+
return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to ${identityLabel(existingIdentity)}`, candidates, eligible };
|
|
153
178
|
}
|
|
154
179
|
if (eligible.length === 0)
|
|
155
180
|
return { status: 'normal', candidates, eligible: [] };
|
|
@@ -227,64 +252,6 @@ function selectPullRequest(remotes, repository, head, base, expectedSha) {
|
|
|
227
252
|
return { status: 'resolved', pullRequest: matches[0] };
|
|
228
253
|
return { status: matches.length === 0 ? 'missing' : 'ambiguous', pullRequest: null };
|
|
229
254
|
}
|
|
230
|
-
function repositoryHead(cwd) {
|
|
231
|
-
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
232
|
-
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
|
|
233
|
-
}).trim();
|
|
234
|
-
}
|
|
235
|
-
function remoteBranchHead(cwd, repository, head) {
|
|
236
|
-
const wanted = expectedHead(repository, head);
|
|
237
|
-
if (!wanted.ref || !/^[A-Za-z0-9._/-]+$/.test(wanted.ref))
|
|
238
|
-
return {
|
|
239
|
-
ok: false,
|
|
240
|
-
error: { code: 'PR_HEAD_INVALID', message: 'Pull request head ref is invalid', retryable: false }
|
|
241
|
-
};
|
|
242
|
-
const remotes = configuredGitRemotes(cwd);
|
|
243
|
-
const remote = remotes.find((item) => repositoryKey(item.repository) === repositoryKey(wanted.repository))
|
|
244
|
-
?? remotes.find((item) => item.name === 'origin');
|
|
245
|
-
if (!remote)
|
|
246
|
-
return {
|
|
247
|
-
ok: false,
|
|
248
|
-
error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `No configured remote can verify ${wanted.repository}:${wanted.ref}`, retryable: false }
|
|
249
|
-
};
|
|
250
|
-
try {
|
|
251
|
-
const output = execFileSync('git', ['ls-remote', '--refs', remote.name, `refs/heads/${wanted.ref}`], {
|
|
252
|
-
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
|
|
253
|
-
}).trim();
|
|
254
|
-
const match = output.split(/\r?\n/).map((line) => line.trim()).find((line) => line.endsWith(`refs/heads/${wanted.ref}`));
|
|
255
|
-
const sha = match?.split(/\s+/)[0] || null;
|
|
256
|
-
if (!sha || !/^[a-f0-9]{40}$/i.test(sha))
|
|
257
|
-
return {
|
|
258
|
-
ok: false,
|
|
259
|
-
error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `Remote branch ${wanted.repository}:${wanted.ref} does not exist`, retryable: false }
|
|
260
|
-
};
|
|
261
|
-
return { ok: true, value: sha };
|
|
262
|
-
}
|
|
263
|
-
catch (error) {
|
|
264
|
-
return {
|
|
265
|
-
ok: false,
|
|
266
|
-
error: { code: 'PR_REMOTE_BRANCH_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: true }
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
function verifyCreateHead(base, head) {
|
|
271
|
-
let localHead;
|
|
272
|
-
try {
|
|
273
|
-
localHead = repositoryHead(base.resolved.repoRoot);
|
|
274
|
-
}
|
|
275
|
-
catch (error) {
|
|
276
|
-
return { ok: false, error: { code: 'PR_LOCAL_HEAD_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: false } };
|
|
277
|
-
}
|
|
278
|
-
const remote = remoteBranchHead(base.resolved.repoRoot, base.context.platform.repository, head);
|
|
279
|
-
if (!remote.ok)
|
|
280
|
-
return remote;
|
|
281
|
-
if (remote.value !== localHead)
|
|
282
|
-
return {
|
|
283
|
-
ok: false,
|
|
284
|
-
error: { code: 'PR_REMOTE_HEAD_MISMATCH', message: `Remote head ${remote.value} does not match local HEAD ${localHead}`, retryable: false }
|
|
285
|
-
};
|
|
286
|
-
return { ok: true, value: localHead };
|
|
287
|
-
}
|
|
288
255
|
function validateBoundPullRequest(base, pullRequest, head, baseRef, expectedSha) {
|
|
289
256
|
if (!pullRequest)
|
|
290
257
|
return {
|
|
@@ -303,26 +270,157 @@ function validateBoundPullRequest(base, pullRequest, head, baseRef, expectedSha)
|
|
|
303
270
|
};
|
|
304
271
|
return { ok: true };
|
|
305
272
|
}
|
|
306
|
-
function
|
|
273
|
+
function resolveTaskDeliveryTarget(repoRoot, frontmatter) {
|
|
274
|
+
const existing = {
|
|
275
|
+
...(typeof frontmatter.delivery_remote === 'string' && frontmatter.delivery_remote
|
|
276
|
+
? { remote: frontmatter.delivery_remote } : {}),
|
|
277
|
+
...(typeof frontmatter.delivery_base_ref === 'string' && frontmatter.delivery_base_ref
|
|
278
|
+
? { baseRef: frontmatter.delivery_base_ref } : {})
|
|
279
|
+
};
|
|
280
|
+
const target = resolveDeliveryTarget(repoRoot, existing);
|
|
281
|
+
return target.ok
|
|
282
|
+
? { ok: true, value: target.value }
|
|
283
|
+
: { ok: false, error: { code: target.code, message: target.message, retryable: false } };
|
|
284
|
+
}
|
|
285
|
+
function taskDeliveryTarget(base) {
|
|
286
|
+
return resolveTaskDeliveryTarget(base.resolved.repoRoot, base.frontmatter);
|
|
287
|
+
}
|
|
288
|
+
function validateIdentityAgainstTarget(repository, target, pullRequest, options = {}) {
|
|
289
|
+
if (!pullRequest || !hasCompleteExternalIdentity(pullRequest))
|
|
290
|
+
return {
|
|
291
|
+
ok: false,
|
|
292
|
+
error: { code: options.errorCode || 'PR_BIND_IDENTITY_MISMATCH', message: 'Pull request identity is incomplete', retryable: false }
|
|
293
|
+
};
|
|
294
|
+
const wantedHead = options.expectedHead ? expectedHead(repository, options.expectedHead) : null;
|
|
295
|
+
const matches = repositoryKey(pullRequest.repository) === repositoryKey(repository)
|
|
296
|
+
&& repositoryKey(pullRequest.base.repository) === repositoryKey(repository)
|
|
297
|
+
&& pullRequest.base.ref === target.baseRef
|
|
298
|
+
&& (!wantedHead || (repositoryKey(pullRequest.head.repository) === repositoryKey(wantedHead.repository) && pullRequest.head.ref === wantedHead.ref))
|
|
299
|
+
&& (!options.expectedHeadSha || pullRequest.head.sha === options.expectedHeadSha);
|
|
300
|
+
if (!matches)
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
error: {
|
|
304
|
+
code: options.errorCode || 'PR_BIND_IDENTITY_MISMATCH',
|
|
305
|
+
message: 'Pull request identity does not match the task branch and delivery target',
|
|
306
|
+
retryable: false
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
return { ok: true };
|
|
310
|
+
}
|
|
311
|
+
function validateWriterIdentity(base, pullRequest, options = {}) {
|
|
312
|
+
const target = taskDeliveryTarget(base);
|
|
313
|
+
if (!target.ok)
|
|
314
|
+
return target;
|
|
315
|
+
return validateIdentityAgainstTarget(base.context.platform.repository, target.value, pullRequest, options);
|
|
316
|
+
}
|
|
317
|
+
function sameDeliveryIdentity(left, right) {
|
|
318
|
+
return JSON.stringify(deliveryIdentity(left)) === JSON.stringify(deliveryIdentity(right));
|
|
319
|
+
}
|
|
320
|
+
function validateExternalMergedEvidence(initial, rechecked) {
|
|
321
|
+
if (initial.state !== 'closed'
|
|
322
|
+
|| !initial.mergedAt
|
|
323
|
+
|| !initial.mergeCommitSha
|
|
324
|
+
|| rechecked.state !== 'closed'
|
|
325
|
+
|| !rechecked.mergedAt
|
|
326
|
+
|| !rechecked.mergeCommitSha
|
|
327
|
+
|| initial.mergedAt !== rechecked.mergedAt
|
|
328
|
+
|| initial.mergeCommitSha !== rechecked.mergeCommitSha)
|
|
329
|
+
return {
|
|
330
|
+
ok: false,
|
|
331
|
+
error: { code: 'PR_EXTERNAL_IDENTITY_MISMATCH', message: 'Selected pull request merged evidence changed before task binding', retryable: false }
|
|
332
|
+
};
|
|
333
|
+
return { ok: true };
|
|
334
|
+
}
|
|
335
|
+
async function resolvedContext(taskRef, options) {
|
|
307
336
|
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
308
337
|
if (!resolved.ok)
|
|
309
338
|
return { ok: false, output: result('failed', resolved.taskId, null, null, {
|
|
310
339
|
error: { code: resolved.code, message: resolved.message, retryable: false }
|
|
311
340
|
}) };
|
|
312
341
|
const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
313
|
-
const frontmatter =
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const
|
|
342
|
+
const frontmatter = parseTypedTaskFrontmatter(content);
|
|
343
|
+
const factRead = readPrDeliveryFact(frontmatter, options.runtimeVersion);
|
|
344
|
+
if (factRead.status === 'invalid')
|
|
345
|
+
return { ok: false, output: result('failed', resolved.taskId, null, null, {
|
|
346
|
+
error: { code: factRead.error.code, message: factRead.error.message, retryable: false }
|
|
347
|
+
}) };
|
|
348
|
+
const fact = factRead.status === 'valid' ? factRead.fact : null;
|
|
349
|
+
let issueIdentity;
|
|
350
|
+
try {
|
|
351
|
+
issueIdentity = taskIssueIdentity(frontmatter, undefined, options.runtimeVersion);
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
return { ok: false, output: result('failed', resolved.taskId, null, null, { error: { ...taskIssueIdentityError(error), retryable: false } }) };
|
|
355
|
+
}
|
|
356
|
+
const issueNumber = resourceIdentityNumber(issueIdentity);
|
|
357
|
+
const prIdentity = fact?.state === 'bound' ? fact.identity.resource : null;
|
|
358
|
+
const prNumber = resourceIdentityNumber(prIdentity);
|
|
359
|
+
const client = options.client;
|
|
360
|
+
const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client });
|
|
361
|
+
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
320
362
|
const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
|
|
321
|
-
if (!usable)
|
|
363
|
+
if (!usable || !loaded.ok)
|
|
322
364
|
return { ok: false, output: result(context.status, resolved.taskId, issueNumber, prNumber, {
|
|
323
365
|
platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
|
|
324
366
|
}) };
|
|
325
|
-
return { ok: true, resolved, content, frontmatter, issueNumber, prNumber, client, context };
|
|
367
|
+
return { ok: true, resolved, content, frontmatter, fact, issueIdentity, issueNumber, prIdentity, prNumber, client, context, provider: loaded.value.provider, providerType: loaded.value.providerType, loadedContext: loaded.value };
|
|
368
|
+
}
|
|
369
|
+
function normalizeProviderPullRequest(remote, repository, fallbackNumber, fallback) {
|
|
370
|
+
const head = remote.head || fallback?.head || { repository, ref: '', sha: remote.headSha || '' };
|
|
371
|
+
const base = remote.base || fallback?.base || { repository, ref: '', sha: remote.baseSha || '' };
|
|
372
|
+
const identity = remote.identity || fallback?.identity || (remote.number ? { kind: 'number', value: remote.number } : { kind: 'id', value: remote.id });
|
|
373
|
+
const snapshot = {
|
|
374
|
+
repository,
|
|
375
|
+
number: remote.number ?? resourceIdentityNumber(identity) ?? fallbackNumber,
|
|
376
|
+
nodeId: remote.id,
|
|
377
|
+
url: remote.displayUrl || fallback?.url || '',
|
|
378
|
+
state: remote.state === 'closed' ? 'closed' : 'open',
|
|
379
|
+
title: remote.title,
|
|
380
|
+
body: remote.body,
|
|
381
|
+
draft: remote.draft ?? fallback?.draft ?? false,
|
|
382
|
+
head: { ...head, sha: remote.headSha || head.sha },
|
|
383
|
+
base: { ...base, sha: remote.baseSha || base.sha },
|
|
384
|
+
mergedAt: remote.mergedAt ?? fallback?.mergedAt ?? null,
|
|
385
|
+
mergeCommitSha: remote.mergeCommitSha ?? fallback?.mergeCommitSha ?? null,
|
|
386
|
+
labels: [...(remote.labels || fallback?.labels || [])].sort(),
|
|
387
|
+
assignees: [...(remote.assignees || fallback?.assignees || [])].sort(),
|
|
388
|
+
milestone: remote.milestone ?? fallback?.milestone ?? null,
|
|
389
|
+
mergeability: remote.mergeability || fallback?.mergeability || { state: 'unknown', detail: null }
|
|
390
|
+
};
|
|
391
|
+
Object.defineProperty(snapshot, 'identity', { value: identity, enumerable: false, configurable: true });
|
|
392
|
+
return snapshot;
|
|
393
|
+
}
|
|
394
|
+
function pullRequestIdentity(pullRequest) {
|
|
395
|
+
return pullRequest.identity || { kind: 'number', value: pullRequest.number };
|
|
396
|
+
}
|
|
397
|
+
function providerPullRequestError(base, error, prNumber) {
|
|
398
|
+
return result(error.code.startsWith('IN_LABEL_SYNC') ? 'blocked' : providerStatus(error), base.resolved.taskId, base.issueNumber, prNumber, {
|
|
399
|
+
changed: error.code.startsWith('IN_LABEL_SYNC'),
|
|
400
|
+
platform: base.context.platform,
|
|
401
|
+
capabilities: base.context.capabilities,
|
|
402
|
+
resource: { kind: 'pull-request', number: prNumber },
|
|
403
|
+
error: providerError(error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
async function inspectExternalPullRequest(base, prIdentity) {
|
|
407
|
+
const prNumber = resourceIdentityNumber(prIdentity);
|
|
408
|
+
const fallback = base.fact?.state === 'bound' && resourceIdentityEquals(base.fact.identity.resource, prIdentity)
|
|
409
|
+
? base.fact.identity
|
|
410
|
+
: null;
|
|
411
|
+
const inspected = base.provider.changeRequests?.inspect
|
|
412
|
+
? await base.provider.changeRequests.inspect({ context: providerOperationContext(base.loadedContext), target: prIdentity })
|
|
413
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
|
|
414
|
+
if (!inspected.ok)
|
|
415
|
+
return providerPullRequestError(base, inspected.error, prNumber);
|
|
416
|
+
const pullRequest = normalizeProviderPullRequest(inspected.value, base.context.platform.repository, prNumber || 0, fallback);
|
|
417
|
+
return result('no-op', base.resolved.taskId, base.issueNumber, prNumber, {
|
|
418
|
+
platform: base.context.platform,
|
|
419
|
+
capabilities: base.context.capabilities,
|
|
420
|
+
resource: { kind: 'pull-request', number: prNumber, identity: prIdentity },
|
|
421
|
+
pullRequest,
|
|
422
|
+
error: null
|
|
423
|
+
});
|
|
326
424
|
}
|
|
327
425
|
function inspectGitHubPullRequest(client, repository, number, cwd) {
|
|
328
426
|
const fetched = client.json(['api', `repos/${repository}/pulls/${number}`], { cwd });
|
|
@@ -333,191 +431,369 @@ function inspectGitHubPullRequest(client, repository, number, cwd) {
|
|
|
333
431
|
? { ok: true, value: pullRequest }
|
|
334
432
|
: { ok: false, error: { code: 'PR_IDENTITY_INVALID', message: 'Remote resource is not a valid pull request', retryable: false } };
|
|
335
433
|
}
|
|
336
|
-
function
|
|
434
|
+
async function inspectPlatformPullRequest(taskRef, options = {}) {
|
|
435
|
+
const base = await resolvedContext(taskRef, options);
|
|
436
|
+
if (!base.ok)
|
|
437
|
+
return base.output;
|
|
438
|
+
if (!base.fact)
|
|
439
|
+
return result('no-op', base.resolved.taskId, base.issueNumber, null, {
|
|
440
|
+
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
441
|
+
});
|
|
442
|
+
if (!base.prIdentity)
|
|
443
|
+
return result('no-op', base.resolved.taskId, base.issueNumber, null, {
|
|
444
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
445
|
+
error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
|
|
446
|
+
});
|
|
447
|
+
return inspectExternalPullRequest(base, base.prIdentity);
|
|
448
|
+
}
|
|
449
|
+
async function inspectPlatformPullRequestByNumber(prNumber, options = {}) {
|
|
450
|
+
const loaded = await resolvePlatformProviderContext({ cwd: options.cwd || process.cwd(), client: options.client });
|
|
451
|
+
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
452
|
+
const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
|
|
453
|
+
if (!usable) {
|
|
454
|
+
return result(context.status, null, null, null, {
|
|
455
|
+
platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
let identity;
|
|
337
459
|
try {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}).trim();
|
|
345
|
-
const repository = parseGitHubRemote(url);
|
|
346
|
-
return repository ? [{ name, url, repository }] : [];
|
|
347
|
-
}
|
|
348
|
-
catch {
|
|
349
|
-
return [];
|
|
350
|
-
}
|
|
460
|
+
identity = loaded.ok ? providerResourceToken(loaded.value.provider, 'pull-request', String(prNumber)) : resourceIdentity(prNumber);
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
return result('failed', null, null, null, {
|
|
464
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
465
|
+
error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
351
466
|
});
|
|
352
467
|
}
|
|
353
|
-
|
|
354
|
-
|
|
468
|
+
if (loaded.ok) {
|
|
469
|
+
const inspected = loaded.value.provider.changeRequests?.inspect
|
|
470
|
+
? await loaded.value.provider.changeRequests.inspect({ context: providerOperationContext(loaded.value), target: identity })
|
|
471
|
+
: unsupportedProviderOperation(loaded.value.provider, 'changeRequests.inspect');
|
|
472
|
+
const number = resourceIdentityNumber(identity);
|
|
473
|
+
if (!inspected.ok)
|
|
474
|
+
return result(providerStatus(inspected.error), null, null, number, {
|
|
475
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
476
|
+
resource: { kind: 'pull-request', number, identity }, error: providerError(inspected.error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
|
|
477
|
+
});
|
|
478
|
+
return result('no-op', null, null, number, {
|
|
479
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
480
|
+
resource: { kind: 'pull-request', number, identity },
|
|
481
|
+
pullRequest: normalizeProviderPullRequest(inspected.value, context.platform.repository, number || 0), error: null
|
|
482
|
+
});
|
|
355
483
|
}
|
|
484
|
+
return result('failed', null, null, null, {
|
|
485
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
486
|
+
resource: { kind: 'pull-request', number: null },
|
|
487
|
+
error: { code: 'PLATFORM_PROVIDER_LOAD_FAILED', message: 'Selected provider failed to load', retryable: false }
|
|
488
|
+
});
|
|
356
489
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
490
|
+
const CLOSING_ISSUES_QUERY = 'query($owner:String!,$name:String!,$number:Int!,$cursor:String){repository(owner:$owner,name:$name){pullRequest(number:$number){closingIssuesReferences(first:100,after:$cursor){nodes{number} pageInfo{hasNextPage endCursor}}}}}';
|
|
491
|
+
function readInLabelMapping(repoRoot) {
|
|
492
|
+
try {
|
|
493
|
+
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
|
|
494
|
+
const mapping = validateInLabelMapping(config.labels?.in);
|
|
495
|
+
return mapping.ok ? { ok: true, value: mapping.value } : mapping;
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
return {
|
|
499
|
+
ok: false,
|
|
500
|
+
error: { code: 'IN_LABEL_SYNC_CONFIG_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
501
|
+
};
|
|
502
|
+
}
|
|
360
503
|
}
|
|
361
|
-
function
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
pullRequest.number <= 0) {
|
|
504
|
+
function taskInLabelTarget(repoRoot, frontmatter, currentLabels, repositoryLabels) {
|
|
505
|
+
const baseRef = typeof frontmatter.delivery_base_ref === 'string' ? frontmatter.delivery_base_ref.trim() : '';
|
|
506
|
+
if (!baseRef)
|
|
365
507
|
return {
|
|
366
508
|
ok: false,
|
|
367
|
-
error: {
|
|
368
|
-
code: 'PR_MERGE_EVIDENCE_SOURCE_UNAVAILABLE',
|
|
369
|
-
message: 'Pull request repository identity is inconsistent',
|
|
370
|
-
retryable: false
|
|
371
|
-
}
|
|
509
|
+
error: { code: 'IN_LABEL_SYNC_BASE_MISSING', message: 'Task has no delivery_base_ref for in-label evidence', retryable: false }
|
|
372
510
|
};
|
|
511
|
+
let changedFiles;
|
|
512
|
+
try {
|
|
513
|
+
changedFiles = execFileSync('git', ['diff', `${baseRef}...HEAD`, '--name-only'], {
|
|
514
|
+
cwd: repoRoot, encoding: 'utf8'
|
|
515
|
+
}).trim().split(/\r?\n/).filter(Boolean);
|
|
373
516
|
}
|
|
374
|
-
|
|
375
|
-
const targetHeadRef = `refs/heads/${pullRequest.base.ref}`;
|
|
376
|
-
const refValid = (ref) => {
|
|
377
|
-
try {
|
|
378
|
-
execFileSync('git', ['check-ref-format', ref], {
|
|
379
|
-
cwd, encoding: 'utf8', stdio: ['ignore', 'ignore', 'ignore']
|
|
380
|
-
});
|
|
381
|
-
return true;
|
|
382
|
-
}
|
|
383
|
-
catch {
|
|
384
|
-
return false;
|
|
385
|
-
}
|
|
386
|
-
};
|
|
387
|
-
if (!refValid(reviewedHeadRef) || !refValid(targetHeadRef)) {
|
|
517
|
+
catch (error) {
|
|
388
518
|
return {
|
|
389
519
|
ok: false,
|
|
390
|
-
error: {
|
|
391
|
-
code: 'PR_MERGE_EVIDENCE_SOURCE_UNAVAILABLE',
|
|
392
|
-
message: 'Pull request Git refs are invalid',
|
|
393
|
-
retryable: false
|
|
394
|
-
}
|
|
520
|
+
error: { code: 'IN_LABEL_SYNC_EVIDENCE_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
395
521
|
};
|
|
396
522
|
}
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
523
|
+
const mapping = readInLabelMapping(repoRoot);
|
|
524
|
+
if (!mapping.ok)
|
|
525
|
+
return mapping;
|
|
526
|
+
const planned = planInLabelUpdate({ changedFiles, currentLabels, mapping: mapping.value, repositoryLabels });
|
|
527
|
+
if (planned.error)
|
|
528
|
+
return { ok: false, error: planned.error };
|
|
529
|
+
return { ok: true, value: planned.target };
|
|
530
|
+
}
|
|
531
|
+
function inspectClosingIssueNumbers(client, repository, pullRequest, cwd) {
|
|
532
|
+
const [owner, name] = repository.split('/');
|
|
533
|
+
if (!owner || !name)
|
|
405
534
|
return {
|
|
406
535
|
ok: false,
|
|
407
|
-
error: {
|
|
408
|
-
code: 'PR_MERGE_EVIDENCE_SOURCE_UNAVAILABLE',
|
|
409
|
-
message: `No GitHub remote can provide evidence for ${repository}`,
|
|
410
|
-
retryable: false
|
|
411
|
-
}
|
|
536
|
+
error: { code: 'IN_LABEL_SYNC_REPOSITORY_INVALID', message: 'Repository identity is invalid', retryable: false }
|
|
412
537
|
};
|
|
538
|
+
let cursor = null;
|
|
539
|
+
const numbers = [];
|
|
540
|
+
for (;;) {
|
|
541
|
+
const args = ['api', 'graphql', '-f', `query=${CLOSING_ISSUES_QUERY}`, '-F', `owner=${owner}`, '-F', `name=${name}`, '-F', `number=${pullRequest}`];
|
|
542
|
+
if (cursor)
|
|
543
|
+
args.push('-F', `cursor=${cursor}`);
|
|
544
|
+
const response = client.json(args, { cwd });
|
|
545
|
+
if (!response.ok)
|
|
546
|
+
return response;
|
|
547
|
+
const connection = response.value?.data?.repository?.pullRequest?.closingIssuesReferences;
|
|
548
|
+
if (!connection || !Array.isArray(connection.nodes) || !connection.pageInfo)
|
|
549
|
+
return {
|
|
550
|
+
ok: false,
|
|
551
|
+
error: { code: 'IN_LABEL_SYNC_ASSOCIATION_INVALID', message: 'Closing Issue response is incomplete', retryable: false }
|
|
552
|
+
};
|
|
553
|
+
for (const node of connection.nodes) {
|
|
554
|
+
const issue = Number(node?.number);
|
|
555
|
+
if (!Number.isSafeInteger(issue) || issue <= 0)
|
|
556
|
+
return {
|
|
557
|
+
ok: false,
|
|
558
|
+
error: { code: 'IN_LABEL_SYNC_ASSOCIATION_INVALID', message: 'Closing Issue identity is invalid', retryable: false }
|
|
559
|
+
};
|
|
560
|
+
numbers.push(issue);
|
|
561
|
+
}
|
|
562
|
+
if (!connection.pageInfo.hasNextPage)
|
|
563
|
+
return { ok: true, value: [...new Set(numbers)] };
|
|
564
|
+
if (!connection.pageInfo.endCursor || connection.pageInfo.endCursor === cursor)
|
|
565
|
+
return {
|
|
566
|
+
ok: false,
|
|
567
|
+
error: { code: 'IN_LABEL_SYNC_ASSOCIATION_INVALID', message: 'Closing Issue pagination cursor is invalid', retryable: false }
|
|
568
|
+
};
|
|
569
|
+
cursor = connection.pageInfo.endCursor;
|
|
413
570
|
}
|
|
414
|
-
return {
|
|
415
|
-
ok: true,
|
|
416
|
-
value: { remoteUrl, reviewedHeadRef, targetHeadRef }
|
|
417
|
-
};
|
|
418
571
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
},
|
|
426
|
-
resolveChangeRequestGitEvidence(context) {
|
|
427
|
-
return resolveGitHubChangeRequestGitEvidence(context);
|
|
428
|
-
}
|
|
429
|
-
});
|
|
430
|
-
function locatePullRequest(base, head, target, expectedSha) {
|
|
431
|
-
const repository = base.context.platform.repository;
|
|
432
|
-
const listed = base.client.json([
|
|
433
|
-
'api', `repos/${repository}/pulls?state=open&base=${encodeURIComponent(target)}&per_page=100`
|
|
434
|
-
], { cwd: base.resolved.repoRoot });
|
|
435
|
-
if (!listed.ok)
|
|
436
|
-
return listed;
|
|
437
|
-
const selected = selectPullRequest(listed.value, repository, head, target, expectedSha);
|
|
438
|
-
return selected.status === 'resolved'
|
|
439
|
-
? { ok: true, value: selected.pullRequest }
|
|
440
|
-
: { ok: false, error: {
|
|
441
|
-
code: selected.status === 'ambiguous' ? 'PR_IDENTITY_AMBIGUOUS'
|
|
442
|
-
: selected.status === 'head-mismatch' ? 'PR_HEAD_SHA_MISMATCH' : 'PR_NOT_FOUND',
|
|
443
|
-
message: selected.status === 'ambiguous' ? 'Multiple pull requests match the exact head/base identity'
|
|
444
|
-
: selected.status === 'head-mismatch' ? 'A pull request matches head/base but not the expected head SHA'
|
|
445
|
-
: 'No pull request matches the exact head/base identity',
|
|
446
|
-
retryable: false
|
|
447
|
-
} };
|
|
572
|
+
function inspectRepositoryLabels(client, repository, cwd) {
|
|
573
|
+
const response = client.json(['api', '--paginate', '--slurp', `repos/${repository}/labels?per_page=100`], { cwd });
|
|
574
|
+
if (!response.ok)
|
|
575
|
+
return response;
|
|
576
|
+
const labels = validateRepositoryLabelPayload(response.value);
|
|
577
|
+
return labels.ok ? { ok: true, value: new Set(labels.value) } : labels;
|
|
448
578
|
}
|
|
449
|
-
function
|
|
450
|
-
const
|
|
451
|
-
if (!
|
|
452
|
-
return
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
579
|
+
function inspectPullRequestFiles(client, repository, pullRequest, cwd) {
|
|
580
|
+
const response = client.json(['api', '--paginate', '--slurp', `repos/${repository}/pulls/${pullRequest}/files?per_page=100`], { cwd });
|
|
581
|
+
if (!response.ok)
|
|
582
|
+
return response;
|
|
583
|
+
const files = extractPullRequestFileNames(response.value);
|
|
584
|
+
return files ? { ok: true, value: files } : {
|
|
585
|
+
ok: false,
|
|
586
|
+
error: { code: 'IN_LABEL_SYNC_FILES_INVALID', message: 'Pull request files response is incomplete', retryable: false }
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function inspectTaskInLabelTarget(repoRoot, frontmatter, pullRequest, repository, client) {
|
|
590
|
+
const baseRef = typeof frontmatter.delivery_base_ref === 'string' ? frontmatter.delivery_base_ref.trim() : '';
|
|
591
|
+
if (!baseRef)
|
|
592
|
+
return {
|
|
593
|
+
ok: false,
|
|
594
|
+
error: { code: 'IN_LABEL_SYNC_BASE_MISSING', message: 'Task has no delivery_base_ref for in-label evidence', retryable: false }
|
|
595
|
+
};
|
|
596
|
+
let changedFiles;
|
|
597
|
+
try {
|
|
598
|
+
changedFiles = execFileSync('git', ['diff', `${baseRef}...HEAD`, '--name-only'], {
|
|
599
|
+
cwd: repoRoot, encoding: 'utf8'
|
|
600
|
+
}).trim().split(/\r?\n/).filter(Boolean);
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
return {
|
|
604
|
+
ok: false,
|
|
605
|
+
error: { code: 'IN_LABEL_SYNC_EVIDENCE_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
469
606
|
};
|
|
470
|
-
return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
471
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
472
|
-
resource: { kind: 'pull-request', number: base.prNumber }, error
|
|
473
|
-
});
|
|
474
607
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
608
|
+
const mapping = readInLabelMapping(repoRoot);
|
|
609
|
+
if (!mapping.ok)
|
|
610
|
+
return mapping;
|
|
611
|
+
const labels = inspectRepositoryLabels(client, repository, repoRoot);
|
|
612
|
+
if (!labels.ok)
|
|
613
|
+
return labels;
|
|
614
|
+
const planned = planInLabelUpdate({
|
|
615
|
+
changedFiles, currentLabels: pullRequest.labels, mapping: mapping.value, repositoryLabels: labels.value
|
|
478
616
|
});
|
|
617
|
+
if (planned.error)
|
|
618
|
+
return { ok: false, error: planned.error };
|
|
619
|
+
return { ok: true, value: { changedFiles, target: planned.target } };
|
|
479
620
|
}
|
|
480
|
-
function
|
|
621
|
+
function inLabelResource(kind, number, before, expected, after, effect) {
|
|
622
|
+
return { kind, number, before: [...before].sort(), expected: [...expected].sort(), after: after ? [...after].sort() : null, effect };
|
|
623
|
+
}
|
|
624
|
+
async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
|
|
625
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
481
626
|
const client = options.client || createGitHubClient();
|
|
482
|
-
const context = resolvePlatformContext({ cwd
|
|
627
|
+
const context = await resolvePlatformContext({ cwd, client });
|
|
483
628
|
const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
|
|
484
|
-
if (!usable)
|
|
485
|
-
return result(context.status, null, null,
|
|
629
|
+
if (!usable)
|
|
630
|
+
return result(context.status, null, null, prNumber, {
|
|
486
631
|
platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
|
|
487
632
|
});
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
return result('failed', null, null, null, {
|
|
633
|
+
if (!Number.isSafeInteger(prNumber) || prNumber <= 0)
|
|
634
|
+
return result('failed', null, null, prNumber, {
|
|
491
635
|
platform: context.platform, capabilities: context.capabilities,
|
|
492
636
|
error: { code: 'PR_NUMBER_INVALID', message: 'PR number must be positive', retryable: false }
|
|
493
637
|
});
|
|
494
|
-
|
|
495
|
-
const fetched =
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
638
|
+
const repository = context.platform.repository;
|
|
639
|
+
const fetched = inspectGitHubPullRequest(client, repository, prNumber, cwd);
|
|
640
|
+
if (!fetched.ok)
|
|
641
|
+
return result(fetched.error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
|
|
642
|
+
platform: context.platform, capabilities: context.capabilities, resource: { kind: 'pull-request', number: prNumber }, error: fetched.error
|
|
643
|
+
});
|
|
644
|
+
const files = inspectPullRequestFiles(client, repository, prNumber, cwd);
|
|
645
|
+
if (!files.ok)
|
|
646
|
+
return result(files.error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
|
|
647
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
648
|
+
resource: { kind: 'pull-request', number: prNumber }, error: files.error
|
|
649
|
+
});
|
|
650
|
+
const mapping = readInLabelMapping(cwd);
|
|
651
|
+
if (!mapping.ok)
|
|
652
|
+
return result('failed', null, null, prNumber, {
|
|
653
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
654
|
+
resource: { kind: 'pull-request', number: prNumber }, error: mapping.error
|
|
655
|
+
});
|
|
656
|
+
const repositoryLabels = inspectRepositoryLabels(client, repository, cwd);
|
|
657
|
+
if (!repositoryLabels.ok)
|
|
658
|
+
return result(repositoryLabels.error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
|
|
659
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
660
|
+
resource: { kind: 'pull-request', number: prNumber }, error: repositoryLabels.error
|
|
661
|
+
});
|
|
662
|
+
const plannedTarget = planInLabelUpdate({
|
|
663
|
+
changedFiles: files.value, currentLabels: fetched.value.labels,
|
|
664
|
+
mapping: mapping.value, repositoryLabels: repositoryLabels.value
|
|
500
665
|
});
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
666
|
+
if (plannedTarget.error)
|
|
667
|
+
return result('failed', null, null, prNumber, {
|
|
668
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
669
|
+
resource: { kind: 'pull-request', number: prNumber }, error: plannedTarget.error
|
|
670
|
+
});
|
|
671
|
+
const target = plannedTarget.target;
|
|
672
|
+
const association = inspectClosingIssueNumbers(client, repository, prNumber, cwd);
|
|
673
|
+
if (!association.ok)
|
|
674
|
+
return result(association.error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
|
|
675
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
676
|
+
resource: { kind: 'pull-request', number: prNumber }, evidence: { kind: 'pull-request-files', pullRequestFiles: files.value }, error: association.error
|
|
510
677
|
});
|
|
678
|
+
const issueNumber = association.value.length === 1 ? association.value[0] : null;
|
|
679
|
+
let issue = null;
|
|
680
|
+
if (issueNumber) {
|
|
681
|
+
const fetchedIssue = inspectGitHubIssue(client, repository, issueNumber, cwd);
|
|
682
|
+
if (!fetchedIssue.ok)
|
|
683
|
+
return result(fetchedIssue.error.retryable ? 'blocked' : 'failed', null, issueNumber, prNumber, {
|
|
684
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
685
|
+
resource: { kind: 'issue', number: issueNumber }, evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value }, error: fetchedIssue.error
|
|
686
|
+
});
|
|
687
|
+
issue = fetchedIssue.value;
|
|
511
688
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
689
|
+
const resources = issue
|
|
690
|
+
? [
|
|
691
|
+
{ kind: 'issue', number: issue.number, snapshot: issue },
|
|
692
|
+
{ kind: 'pull-request', number: prNumber, snapshot: fetched.value }
|
|
693
|
+
]
|
|
694
|
+
: [{ kind: 'pull-request', number: prNumber, snapshot: fetched.value }];
|
|
695
|
+
const plans = resources.map((resource) => ({
|
|
696
|
+
...resource,
|
|
697
|
+
plan: planInLabelUpdate({
|
|
698
|
+
changedFiles: files.value, currentLabels: resource.snapshot.labels,
|
|
699
|
+
mapping: mapping.value, repositoryLabels: repositoryLabels.value
|
|
700
|
+
})
|
|
701
|
+
}));
|
|
702
|
+
const operations = [
|
|
703
|
+
{ name: 'closing-issues', status: 'no-op', reasonCode: association.value.length === 1 ? null : association.value.length === 0 ? 'ISSUE_ASSOCIATION_NONE' : 'ISSUE_ASSOCIATION_AMBIGUOUS' },
|
|
704
|
+
...plans.map((item) => ({
|
|
705
|
+
name: `labels:in:${item.kind}`,
|
|
706
|
+
status: !context.capabilities.triage ? 'skipped' : item.plan.changed ? 'planned' : 'no-op',
|
|
707
|
+
reasonCode: !context.capabilities.triage ? 'TRIAGE_REQUIRED' : null
|
|
708
|
+
}))
|
|
709
|
+
];
|
|
710
|
+
const willWrite = context.capabilities.triage && !options.dryRun;
|
|
711
|
+
const resourcesState = plans.map((item) => inLabelResource(item.kind, item.number, item.snapshot.labels, target, item.plan.changed && willWrite ? null : item.snapshot.labels, item.plan.changed && willWrite ? 'unknown' : 'no-op'));
|
|
712
|
+
if (options.dryRun || !context.capabilities.triage)
|
|
713
|
+
return result(!context.capabilities.triage || association.value.length !== 1 ? 'degraded' : plans.some((item) => item.plan.changed) ? 'planned' : 'no-op', null, issueNumber, prNumber, {
|
|
714
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
715
|
+
resource: { kind: 'pull-request', number: prNumber }, operations, resources: resourcesState,
|
|
716
|
+
evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value }, error: null
|
|
717
|
+
});
|
|
718
|
+
let currentPullRequest = fetched.value;
|
|
719
|
+
let successfulWrites = 0;
|
|
720
|
+
for (const item of plans) {
|
|
721
|
+
if (!item.plan.changed)
|
|
722
|
+
continue;
|
|
723
|
+
const synced = syncLabelDelta(client, repository, item.number, cwd, item.snapshot.labels, item.plan.target);
|
|
724
|
+
if (synced.status === 'failed' || synced.status === 'blocked') {
|
|
725
|
+
const state = resourcesState.find((resource) => resource.kind === item.kind && resource.number === item.number);
|
|
726
|
+
state.effect = synced.status === 'blocked' ? 'unknown' : 'no-op';
|
|
727
|
+
state.after = synced.status === 'blocked' ? null : item.snapshot.labels;
|
|
728
|
+
const partial = successfulWrites > 0 || synced.status === 'blocked';
|
|
729
|
+
const error = partial && synced.error
|
|
730
|
+
? { ...synced.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${synced.error.message}` }
|
|
731
|
+
: synced.error;
|
|
732
|
+
return result(partial ? 'blocked' : synced.status, null, issueNumber, prNumber, {
|
|
733
|
+
changed: successfulWrites > 0 || synced.changed, platform: context.platform, capabilities: context.capabilities,
|
|
734
|
+
pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber }, operations, resources: resourcesState,
|
|
735
|
+
evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value }, error
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
successfulWrites += synced.changed ? 1 : 0;
|
|
739
|
+
const reread = item.kind === 'issue'
|
|
740
|
+
? inspectGitHubIssue(client, repository, item.number, cwd)
|
|
741
|
+
: inspectGitHubPullRequest(client, repository, item.number, cwd);
|
|
742
|
+
if (!reread.ok)
|
|
743
|
+
return result('blocked', null, issueNumber, prNumber, {
|
|
744
|
+
changed: true, platform: context.platform, capabilities: context.capabilities,
|
|
745
|
+
pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber }, operations, resources: resourcesState,
|
|
746
|
+
evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value },
|
|
747
|
+
error: { ...reread.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${reread.error.message}` }
|
|
748
|
+
});
|
|
749
|
+
const after = reread.value.labels;
|
|
750
|
+
const state = resourcesState.find((resource) => resource.kind === item.kind && resource.number === item.number);
|
|
751
|
+
state.after = after;
|
|
752
|
+
state.effect = after.filter((label) => label.startsWith('in:')).sort().join('\0') === target.join('\0') ? 'applied' : 'unknown';
|
|
753
|
+
if (state.effect === 'unknown')
|
|
754
|
+
return result('blocked', null, issueNumber, prNumber, {
|
|
755
|
+
changed: true, platform: context.platform, capabilities: context.capabilities,
|
|
756
|
+
pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber }, operations, resources: resourcesState,
|
|
757
|
+
evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value },
|
|
758
|
+
error: { code: 'IN_LABEL_SYNC_PARTIAL', message: `${item.kind} #${item.number} did not converge to the expected in: labels`, retryable: true }
|
|
759
|
+
});
|
|
760
|
+
if (item.kind === 'pull-request')
|
|
761
|
+
currentPullRequest = reread.value;
|
|
762
|
+
}
|
|
763
|
+
return result(association.value.length !== 1 ? 'degraded' : successfulWrites > 0 ? 'applied' : 'no-op', null, issueNumber, prNumber, {
|
|
764
|
+
changed: successfulWrites > 0, platform: context.platform, capabilities: context.capabilities,
|
|
765
|
+
pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber },
|
|
766
|
+
operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation),
|
|
767
|
+
resources: resourcesState, evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value }, error: null
|
|
515
768
|
});
|
|
516
769
|
}
|
|
517
770
|
function appendActivity(content, line) {
|
|
518
771
|
const body = extractSection(content, ['活动日志', 'Activity Log']);
|
|
519
772
|
return `${body.replace(/\s+$/, '')}${body.trim() ? '\n' : ''}${line}`;
|
|
520
773
|
}
|
|
774
|
+
function deliveryIdentity(pullRequest) {
|
|
775
|
+
return {
|
|
776
|
+
resource: pullRequestIdentity(pullRequest),
|
|
777
|
+
repository: pullRequest.repository,
|
|
778
|
+
url: pullRequest.url,
|
|
779
|
+
head: { ...pullRequest.head },
|
|
780
|
+
base: { ...pullRequest.base }
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
function factTimestamp(timestamp) {
|
|
784
|
+
return new Date(timestamp.replace(' ', 'T')).toISOString();
|
|
785
|
+
}
|
|
786
|
+
function boundFactFor(pullRequest, source, issue, verifiedAt) {
|
|
787
|
+
return buildBoundFact({
|
|
788
|
+
identity: deliveryIdentity(pullRequest),
|
|
789
|
+
source,
|
|
790
|
+
...(typeof issue === 'object' ? { issueIdentity: issue } : { issueNumber: issue }),
|
|
791
|
+
verifiedAt: factTimestamp(verifiedAt),
|
|
792
|
+
remoteState: pullRequest.state,
|
|
793
|
+
mergedAt: pullRequest.state === 'closed' ? pullRequest.mergedAt : null,
|
|
794
|
+
mergeCommitSha: pullRequest.state === 'closed' ? pullRequest.mergeCommitSha : null
|
|
795
|
+
});
|
|
796
|
+
}
|
|
521
797
|
function writeCreateStarted(base, agent) {
|
|
522
798
|
const starts = (base.content.match(/\*\*Create PR \[started\]\*\*/g) || []).length;
|
|
523
799
|
const dones = (base.content.match(/\*\*Create PR\*\*/g) || []).length;
|
|
@@ -530,60 +806,93 @@ function writeCreateStarted(base, agent) {
|
|
|
530
806
|
mutations: [{ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(base.content, `- ${metadata.timestamp} — **Create PR [started]** by ${agent} — started`) }]
|
|
531
807
|
}, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
|
|
532
808
|
}
|
|
533
|
-
function bindIdentity(base, pullRequest, agent, dryRun = false) {
|
|
534
|
-
|
|
809
|
+
function bindIdentity(base, pullRequest, agent, source, dryRun = false, expectedHead, expectedHeadSha) {
|
|
810
|
+
const identity = validateWriterIdentity(base, pullRequest, {
|
|
811
|
+
expectedHead: source === 'explicit-bind' ? String(base.frontmatter.branch || '') : expectedHead,
|
|
812
|
+
expectedHeadSha,
|
|
813
|
+
errorCode: 'PR_BIND_IDENTITY_MISMATCH'
|
|
814
|
+
});
|
|
815
|
+
if (!identity.ok)
|
|
816
|
+
return { status: 'failed', error: identity.error };
|
|
817
|
+
if (base.prIdentity && !resourceIdentityEquals(base.prIdentity, pullRequestIdentity(pullRequest)))
|
|
535
818
|
return { status: 'failed', error: {
|
|
536
|
-
code: 'PR_BIND_CONFLICT', message:
|
|
819
|
+
code: 'PR_BIND_CONFLICT', message: 'Task is already bound to a different pull request identity'
|
|
537
820
|
} };
|
|
538
|
-
if (base.
|
|
821
|
+
if (base.prIdentity && resourceIdentityEquals(base.prIdentity, pullRequestIdentity(pullRequest)) && base.fact?.state === 'bound') {
|
|
822
|
+
if (JSON.stringify(base.fact.identity) !== JSON.stringify(deliveryIdentity(pullRequest)))
|
|
823
|
+
return { status: 'failed', error: {
|
|
824
|
+
code: 'PR_BIND_IDENTITY_MISMATCH', message: `Task binding for PR #${pullRequest.number} does not match the fetched pull request identity`
|
|
825
|
+
} };
|
|
539
826
|
return { status: 'no-op', error: null };
|
|
827
|
+
}
|
|
540
828
|
const current = fs.readFileSync(base.resolved.taskMdPath, 'utf8');
|
|
541
829
|
const metadata = captureTaskWriteMetadata();
|
|
830
|
+
const fact = boundFactFor(pullRequest, source, taskIssueIdentity(base.frontmatter), metadata.timestamp);
|
|
831
|
+
const factMutation = factFrontmatterMutation(fact);
|
|
542
832
|
return writeTask({
|
|
543
833
|
taskRef: base.resolved.taskId,
|
|
544
834
|
expectedState: 'active',
|
|
545
835
|
dryRun,
|
|
546
836
|
mutations: [
|
|
547
|
-
{ kind: 'frontmatter', set: {
|
|
837
|
+
{ kind: 'frontmatter', set: { ...factMutation.set, assigned_to: agent } },
|
|
548
838
|
{ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(current, `- ${metadata.timestamp} — **Create PR** by ${agent} — PR #${pullRequest.number} created → ${pullRequest.url}`) }
|
|
549
839
|
]
|
|
550
840
|
}, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
|
|
551
841
|
}
|
|
552
|
-
function bindPlatformPullRequest(taskRef, options) {
|
|
553
|
-
const base = resolvedContext(taskRef, options);
|
|
842
|
+
async function bindPlatformPullRequest(taskRef, options) {
|
|
843
|
+
const base = await resolvedContext(taskRef, options);
|
|
554
844
|
if (!base.ok)
|
|
555
845
|
return base.output;
|
|
556
|
-
if (!
|
|
557
|
-
return result('failed', base.resolved.taskId, base.issueNumber,
|
|
558
|
-
error: { code: '
|
|
846
|
+
if (!base.fact)
|
|
847
|
+
return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
848
|
+
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
559
849
|
});
|
|
560
|
-
|
|
850
|
+
let prIdentity;
|
|
851
|
+
try {
|
|
852
|
+
prIdentity = providerResourceToken(base.provider, 'pull-request', String(options.pr));
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
855
|
+
return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, { error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false } });
|
|
856
|
+
}
|
|
857
|
+
const requestedNumber = resourceIdentityNumber(prIdentity);
|
|
858
|
+
if (base.prIdentity && !resourceIdentityEquals(base.prIdentity, prIdentity))
|
|
561
859
|
return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
562
|
-
error: { code: 'PR_BIND_CONFLICT', message:
|
|
860
|
+
error: { code: 'PR_BIND_CONFLICT', message: 'Task is already bound to a different pull request identity', retryable: false }
|
|
563
861
|
});
|
|
564
|
-
const
|
|
565
|
-
if (!
|
|
566
|
-
return
|
|
567
|
-
const
|
|
862
|
+
const inspected = await inspectExternalPullRequest(base, prIdentity);
|
|
863
|
+
if (inspected.status === 'failed' || inspected.status === 'blocked' || !inspected.pullRequest)
|
|
864
|
+
return inspected;
|
|
865
|
+
const initialIdentity = validateWriterIdentity(base, inspected.pullRequest, {
|
|
866
|
+
expectedHead: String(base.frontmatter.branch || ''),
|
|
867
|
+
errorCode: 'PR_BIND_IDENTITY_MISMATCH'
|
|
868
|
+
});
|
|
869
|
+
if (!initialIdentity.ok)
|
|
870
|
+
return result('failed', base.resolved.taskId, base.issueNumber, requestedNumber, {
|
|
871
|
+
pullRequest: inspected.pullRequest, error: initialIdentity.error
|
|
872
|
+
});
|
|
873
|
+
const written = bindIdentity(base, inspected.pullRequest, options.agent, 'explicit-bind', options.dryRun);
|
|
568
874
|
if (written.status === 'failed')
|
|
569
|
-
return result('failed', base.resolved.taskId, base.issueNumber,
|
|
570
|
-
pullRequest:
|
|
875
|
+
return result('failed', base.resolved.taskId, base.issueNumber, requestedNumber, {
|
|
876
|
+
pullRequest: inspected.pullRequest, error: { code: written.error.code, message: written.error.message, retryable: false }
|
|
571
877
|
});
|
|
572
|
-
return result(options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber :
|
|
878
|
+
return result(options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : inspected.pullRequest.number, {
|
|
573
879
|
changed: !options.dryRun && written.status === 'applied', platform: base.context.platform, capabilities: base.context.capabilities,
|
|
574
|
-
resource: { kind: 'pull-request', number:
|
|
880
|
+
resource: { kind: 'pull-request', number: inspected.pullRequest.number, identity: pullRequestIdentity(inspected.pullRequest) }, pullRequest: inspected.pullRequest,
|
|
575
881
|
operations: [{ name: 'task:bind-pr', status: options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', reasonCode: null }], error: null
|
|
576
882
|
});
|
|
577
883
|
}
|
|
578
|
-
function externalEvidenceNote(
|
|
884
|
+
function externalEvidenceNote(issueIdentity, source, pullRequest) {
|
|
579
885
|
return [
|
|
580
886
|
`authorization=${source}`,
|
|
581
|
-
externalIdentityEvidenceNote(
|
|
887
|
+
externalIdentityEvidenceNote(issueIdentity, pullRequest)
|
|
582
888
|
].join('; ');
|
|
583
889
|
}
|
|
584
|
-
function externalIdentityEvidenceNote(
|
|
890
|
+
function externalIdentityEvidenceNote(issueIdentity, pullRequest) {
|
|
891
|
+
const issueLabel = issueIdentity
|
|
892
|
+
? issueIdentity.kind === 'number' ? `#${issueIdentity.value}` : `${issueIdentity.kind}:${issueIdentity.value}`
|
|
893
|
+
: 'unknown';
|
|
585
894
|
return [
|
|
586
|
-
`issue
|
|
895
|
+
`issue=${issueLabel}`,
|
|
587
896
|
`pr=#${pullRequest.number}`,
|
|
588
897
|
`url=${pullRequest.url}`,
|
|
589
898
|
`base=${pullRequest.base.repository}:${pullRequest.base.ref}@${pullRequest.base.sha}`,
|
|
@@ -592,10 +901,14 @@ function externalIdentityEvidenceNote(issueNumber, pullRequest) {
|
|
|
592
901
|
`mergeCommitSha=${pullRequest.mergeCommitSha}`
|
|
593
902
|
].join('; ');
|
|
594
903
|
}
|
|
595
|
-
function resolveExternalPullRequest(taskRef, options) {
|
|
596
|
-
const base = resolvedContext(taskRef, options);
|
|
904
|
+
async function resolveExternalPullRequest(taskRef, options) {
|
|
905
|
+
const base = await resolvedContext(taskRef, options);
|
|
597
906
|
if (!base.ok)
|
|
598
907
|
return externalResult(base.output);
|
|
908
|
+
if (!base.fact)
|
|
909
|
+
return externalResult(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
910
|
+
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
911
|
+
}));
|
|
599
912
|
const inventory = inspectCompletionArtifacts(base.resolved.taskId, { repoRoot: base.resolved.repoRoot });
|
|
600
913
|
if (inventory.status === 'failed')
|
|
601
914
|
return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
@@ -617,25 +930,40 @@ function resolveExternalPullRequest(taskRef, options) {
|
|
|
617
930
|
error: null
|
|
618
931
|
}), { mode: 'normal' });
|
|
619
932
|
}
|
|
620
|
-
if (!base.
|
|
933
|
+
if (!base.issueIdentity)
|
|
621
934
|
return externalResult(result('failed', base.resolved.taskId, null, base.prNumber, {
|
|
622
935
|
platform: base.context.platform,
|
|
623
936
|
capabilities: base.context.capabilities,
|
|
624
937
|
error: { code: 'EXTERNAL_DELIVERY_ISSUE_REQUIRED', message: 'External delivery requires a valid issue_number', retryable: false }
|
|
625
938
|
}));
|
|
626
|
-
const inspected =
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
939
|
+
const inspected = base.provider.changeRequests?.listClosing
|
|
940
|
+
? await base.provider.changeRequests.listClosing({
|
|
941
|
+
context: providerOperationContext(base.loadedContext),
|
|
942
|
+
issue: base.issueIdentity
|
|
943
|
+
})
|
|
944
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.listClosing');
|
|
632
945
|
if (!inspected.ok || !inspected.value) {
|
|
633
|
-
const error =
|
|
946
|
+
const error = 'error' in inspected && inspected.error
|
|
947
|
+
? inspected.error
|
|
948
|
+
: { code: 'PR_IDENTITY_INVALID', message: 'Closing PR inspection returned no candidates', retryable: false };
|
|
634
949
|
return externalResult(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
635
950
|
platform: base.context.platform, capabilities: base.context.capabilities, error
|
|
636
951
|
}));
|
|
637
952
|
}
|
|
638
|
-
const
|
|
953
|
+
const candidates = inspected.value.map((item) => normalizeProviderPullRequest(item, base.context.platform.repository, item.number || 0));
|
|
954
|
+
let explicitPrIdentity = null;
|
|
955
|
+
if (options.pr !== undefined) {
|
|
956
|
+
try {
|
|
957
|
+
explicitPrIdentity = providerResourceToken(base.provider, 'pull-request', String(options.pr));
|
|
958
|
+
}
|
|
959
|
+
catch (error) {
|
|
960
|
+
return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
961
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
962
|
+
error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
963
|
+
}));
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
const selected = selectExternalPullRequest(candidates, base.context.platform.repository, base.prIdentity, explicitPrIdentity);
|
|
639
967
|
if (selected.status === 'normal')
|
|
640
968
|
return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
641
969
|
platform: base.context.platform,
|
|
@@ -649,8 +977,47 @@ function resolveExternalPullRequest(taskRef, options) {
|
|
|
649
977
|
capabilities: base.context.capabilities,
|
|
650
978
|
error: { code: selected.code, message: selected.message, retryable: false }
|
|
651
979
|
}), { candidates: selected.candidates, eligible: selected.eligible });
|
|
652
|
-
const
|
|
653
|
-
|
|
980
|
+
const initialIdentity = validateWriterIdentity(base, selected.selected, { errorCode: 'PR_EXTERNAL_IDENTITY_MISMATCH' });
|
|
981
|
+
if (!initialIdentity.ok)
|
|
982
|
+
return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
983
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
984
|
+
pullRequest: selected.selected, error: initialIdentity.error
|
|
985
|
+
}), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
|
|
986
|
+
const rechecked = base.provider.changeRequests?.inspect
|
|
987
|
+
? await base.provider.changeRequests.inspect({
|
|
988
|
+
context: providerOperationContext(base.loadedContext),
|
|
989
|
+
target: pullRequestIdentity(selected.selected)
|
|
990
|
+
}).then((response) => response.ok
|
|
991
|
+
? { ok: true, value: normalizeProviderPullRequest(response.value, base.context.platform.repository, selected.selected.number, selected.selected) }
|
|
992
|
+
: response)
|
|
993
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
|
|
994
|
+
if (!rechecked.ok || !rechecked.value)
|
|
995
|
+
return externalResult(result(rechecked.ok ? 'failed' : rechecked.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
996
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
997
|
+
pullRequest: selected.selected,
|
|
998
|
+
error: rechecked.ok ? { code: 'PR_EXTERNAL_RECHECK_FAILED', message: 'Selected pull request recheck returned no identity', retryable: false } : rechecked.error
|
|
999
|
+
}), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
|
|
1000
|
+
if (!sameDeliveryIdentity(selected.selected, rechecked.value))
|
|
1001
|
+
return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1002
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1003
|
+
pullRequest: rechecked.value,
|
|
1004
|
+
error: { code: 'PR_EXTERNAL_IDENTITY_MISMATCH', message: 'Selected pull request identity changed before task binding', retryable: false }
|
|
1005
|
+
}), { candidates: selected.candidates, eligible: selected.eligible, selected: rechecked.value });
|
|
1006
|
+
const finalIdentity = validateWriterIdentity(base, rechecked.value, { errorCode: 'PR_EXTERNAL_IDENTITY_MISMATCH' });
|
|
1007
|
+
if (!finalIdentity.ok)
|
|
1008
|
+
return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1009
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1010
|
+
pullRequest: rechecked.value, error: finalIdentity.error
|
|
1011
|
+
}), { candidates: selected.candidates, eligible: selected.eligible, selected: rechecked.value });
|
|
1012
|
+
const finalEvidence = validateExternalMergedEvidence(selected.selected, rechecked.value);
|
|
1013
|
+
if (!finalEvidence.ok)
|
|
1014
|
+
return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1015
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1016
|
+
pullRequest: rechecked.value, error: finalEvidence.error
|
|
1017
|
+
}), { candidates: selected.candidates, eligible: selected.eligible, selected: rechecked.value });
|
|
1018
|
+
const selectedPullRequest = rechecked.value;
|
|
1019
|
+
const note = externalEvidenceNote(base.issueIdentity, selected.source, selectedPullRequest);
|
|
1020
|
+
const identityNote = externalIdentityEvidenceNote(base.issueIdentity, selectedPullRequest);
|
|
654
1021
|
const activityEntry = `**Bind External PR** by ${options.agent} — ${note}`;
|
|
655
1022
|
const auditLines = base.content.split(/\r?\n/).filter((line) => line.includes('**Bind External PR**'));
|
|
656
1023
|
const alreadyAudited = auditLines.some((line) => line.endsWith(identityNote));
|
|
@@ -658,25 +1025,25 @@ function resolveExternalPullRequest(taskRef, options) {
|
|
|
658
1025
|
return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
659
1026
|
platform: base.context.platform,
|
|
660
1027
|
capabilities: base.context.capabilities,
|
|
661
|
-
pullRequest:
|
|
1028
|
+
pullRequest: selectedPullRequest,
|
|
662
1029
|
error: { code: 'PR_BIND_CONFLICT', message: 'Existing external PR audit evidence conflicts with the selected identity', retryable: false }
|
|
663
|
-
}), { candidates: selected.candidates, eligible: selected.eligible, selected:
|
|
664
|
-
if (alreadyAudited && base.
|
|
1030
|
+
}), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
1031
|
+
if (alreadyAudited && base.prIdentity && !resourceIdentityEquals(base.prIdentity, pullRequestIdentity(selectedPullRequest)))
|
|
665
1032
|
return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
666
1033
|
platform: base.context.platform,
|
|
667
1034
|
capabilities: base.context.capabilities,
|
|
668
|
-
pullRequest:
|
|
1035
|
+
pullRequest: selectedPullRequest,
|
|
669
1036
|
error: { code: 'PR_BIND_CONFLICT', message: 'External PR audit evidence does not match the task binding', retryable: false }
|
|
670
|
-
}), { candidates: selected.candidates, eligible: selected.eligible, selected:
|
|
671
|
-
if (base.
|
|
1037
|
+
}), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
1038
|
+
if (base.prIdentity && resourceIdentityEquals(base.prIdentity, pullRequestIdentity(selectedPullRequest)) && alreadyAudited) {
|
|
672
1039
|
return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
673
1040
|
platform: base.context.platform,
|
|
674
1041
|
capabilities: base.context.capabilities,
|
|
675
|
-
resource: { kind: 'pull-request', number:
|
|
676
|
-
pullRequest:
|
|
1042
|
+
resource: { kind: 'pull-request', number: selectedPullRequest.number },
|
|
1043
|
+
pullRequest: selectedPullRequest,
|
|
677
1044
|
operations: [{ name: 'task:bind-external-pr', status: 'no-op', reasonCode: 'EVIDENCE_ALREADY_RECORDED' }],
|
|
678
1045
|
error: null
|
|
679
|
-
}), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected:
|
|
1046
|
+
}), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
680
1047
|
}
|
|
681
1048
|
const metadata = captureTaskWriteMetadata();
|
|
682
1049
|
const write = writeTask({
|
|
@@ -684,7 +1051,7 @@ function resolveExternalPullRequest(taskRef, options) {
|
|
|
684
1051
|
expectedState: 'active',
|
|
685
1052
|
dryRun: options.dryRun,
|
|
686
1053
|
mutations: [
|
|
687
|
-
{ kind: 'frontmatter', set: {
|
|
1054
|
+
{ kind: 'frontmatter', set: { ...factFrontmatterMutation(boundFactFor(selectedPullRequest, selected.source === 'unique' ? 'external-unique' : 'external-explicit', base.issueNumber, metadata.timestamp)).set, assigned_to: options.agent } },
|
|
688
1055
|
{ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(base.content, `- ${metadata.timestamp} — ${activityEntry}`) }
|
|
689
1056
|
]
|
|
690
1057
|
}, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
|
|
@@ -692,185 +1059,41 @@ function resolveExternalPullRequest(taskRef, options) {
|
|
|
692
1059
|
return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
693
1060
|
platform: base.context.platform,
|
|
694
1061
|
capabilities: base.context.capabilities,
|
|
695
|
-
pullRequest:
|
|
1062
|
+
pullRequest: selectedPullRequest,
|
|
696
1063
|
error: { code: write.error.code, message: write.error.message, retryable: false }
|
|
697
|
-
}), { candidates: selected.candidates, eligible: selected.eligible, selected:
|
|
1064
|
+
}), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
698
1065
|
const operationStatus = options.dryRun ? 'planned' : write.status === 'applied' ? 'applied' : 'no-op';
|
|
699
|
-
return externalResult(result(operationStatus, base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber :
|
|
1066
|
+
return externalResult(result(operationStatus, base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : selectedPullRequest.number, {
|
|
700
1067
|
changed: !options.dryRun && write.changed,
|
|
701
1068
|
platform: base.context.platform,
|
|
702
1069
|
capabilities: base.context.capabilities,
|
|
703
|
-
resource: { kind: 'pull-request', number:
|
|
704
|
-
pullRequest:
|
|
1070
|
+
resource: { kind: 'pull-request', number: selectedPullRequest.number },
|
|
1071
|
+
pullRequest: selectedPullRequest,
|
|
705
1072
|
operations: [{ name: 'task:bind-external-pr', status: operationStatus, reasonCode: null }],
|
|
706
1073
|
error: null
|
|
707
|
-
}), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected:
|
|
708
|
-
}
|
|
709
|
-
function createPlatformPullRequestUnlocked(taskRef, options) {
|
|
710
|
-
const base = resolvedContext(taskRef, options);
|
|
711
|
-
if (!base.ok)
|
|
712
|
-
return base.output;
|
|
713
|
-
if (!options.base || !options.head || !options.title.trim() || !options.body.trim())
|
|
714
|
-
return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
715
|
-
error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
|
|
716
|
-
});
|
|
717
|
-
if (base.prNumber) {
|
|
718
|
-
const headCheck = verifyCreateHead(base, options.head);
|
|
719
|
-
if (!headCheck.ok)
|
|
720
|
-
return result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
721
|
-
platform: base.context.platform,
|
|
722
|
-
capabilities: base.context.capabilities,
|
|
723
|
-
error: headCheck.error
|
|
724
|
-
});
|
|
725
|
-
const inspected = inspectPlatformPullRequest(taskRef, options);
|
|
726
|
-
const identity = validateBoundPullRequest(base, inspected.pullRequest, options.head, options.base, headCheck.value);
|
|
727
|
-
if (!identity.ok)
|
|
728
|
-
return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
729
|
-
platform: base.context.platform,
|
|
730
|
-
capabilities: base.context.capabilities,
|
|
731
|
-
pullRequest: inspected.pullRequest,
|
|
732
|
-
error: identity.error
|
|
733
|
-
});
|
|
734
|
-
return { ...inspected, result: 'no_op' };
|
|
735
|
-
}
|
|
736
|
-
const headCheck = verifyCreateHead(base, options.head);
|
|
737
|
-
if (!headCheck.ok)
|
|
738
|
-
return result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
|
|
739
|
-
platform: base.context.platform,
|
|
740
|
-
capabilities: base.context.capabilities,
|
|
741
|
-
error: headCheck.error
|
|
742
|
-
});
|
|
743
|
-
const expectedHeadSha = headCheck.value;
|
|
744
|
-
const located = locatePullRequest(base, options.head, options.base, expectedHeadSha);
|
|
745
|
-
if (!located.ok && located.error.code === 'PR_IDENTITY_AMBIGUOUS')
|
|
746
|
-
return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
747
|
-
platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
|
|
748
|
-
});
|
|
749
|
-
if (!located.ok && located.error.code === 'PR_HEAD_SHA_MISMATCH')
|
|
750
|
-
return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
751
|
-
platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
|
|
752
|
-
});
|
|
753
|
-
if (options.dryRun)
|
|
754
|
-
return result('planned', base.resolved.taskId, base.issueNumber, null, {
|
|
755
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
756
|
-
operations: [{ name: located.ok ? 'pr:reuse' : 'pr:create', status: 'planned', reasonCode: null }],
|
|
757
|
-
result: located.ok ? 'pr_reused' : 'pr_created', error: null
|
|
758
|
-
});
|
|
759
|
-
const started = writeCreateStarted(base, options.agent);
|
|
760
|
-
if (started.status === 'failed')
|
|
761
|
-
return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
762
|
-
error: { code: started.error.code, message: started.error.message, retryable: false }
|
|
763
|
-
});
|
|
764
|
-
let pullRequest = located.ok ? located.value : null;
|
|
765
|
-
let created = false;
|
|
766
|
-
if (!pullRequest) {
|
|
767
|
-
const repository = base.context.platform.repository;
|
|
768
|
-
const response = base.client.json(['api', `repos/${repository}/pulls`, '-X', 'POST', '--input', '-'], {
|
|
769
|
-
cwd: base.resolved.repoRoot,
|
|
770
|
-
method: 'POST',
|
|
771
|
-
input: JSON.stringify({ title: options.title, body: options.body, head: options.head, base: options.base, draft: Boolean(options.draft) })
|
|
772
|
-
});
|
|
773
|
-
if (response.ok)
|
|
774
|
-
pullRequest = normalizePullRequest(response.value, repository);
|
|
775
|
-
else if (response.error.retryable) {
|
|
776
|
-
const recovered = locatePullRequest(base, options.head, options.base, expectedHeadSha);
|
|
777
|
-
if (recovered.ok)
|
|
778
|
-
pullRequest = recovered.value;
|
|
779
|
-
else
|
|
780
|
-
return result('blocked', base.resolved.taskId, base.issueNumber, null, {
|
|
781
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
782
|
-
operations: [{ name: 'pr:create', status: 'failed', reasonCode: 'PR_CREATE_OUTCOME_UNKNOWN' }],
|
|
783
|
-
error: { code: 'PR_CREATE_OUTCOME_UNKNOWN', message: response.error.message, retryable: true }
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
else
|
|
787
|
-
return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
788
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
789
|
-
operations: [{ name: 'pr:create', status: 'failed', reasonCode: response.error.code }], error: response.error
|
|
790
|
-
});
|
|
791
|
-
if (!pullRequest)
|
|
792
|
-
return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
793
|
-
error: { code: 'PR_CREATE_RESPONSE_INVALID', message: 'PR create response lacks validated identity', retryable: false }
|
|
794
|
-
});
|
|
795
|
-
if (pullRequest.head.sha !== expectedHeadSha)
|
|
796
|
-
return result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
797
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
798
|
-
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
|
|
799
|
-
error: { code: 'PR_HEAD_SHA_MISMATCH', message: 'Created pull request head does not match the expected local HEAD', retryable: false }
|
|
800
|
-
});
|
|
801
|
-
created = true;
|
|
802
|
-
}
|
|
803
|
-
const bindHeadCheck = verifyCreateHead(base, options.head);
|
|
804
|
-
if (!bindHeadCheck.ok || bindHeadCheck.value !== expectedHeadSha) {
|
|
805
|
-
const error = bindHeadCheck.ok
|
|
806
|
-
? { code: 'PR_REMOTE_HEAD_MISMATCH', message: 'Remote head changed before task binding', retryable: false }
|
|
807
|
-
: bindHeadCheck.error;
|
|
808
|
-
return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
809
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
810
|
-
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
|
|
811
|
-
});
|
|
812
|
-
}
|
|
813
|
-
const beforeBind = inspectGitHubPullRequest(base.client, base.context.platform.repository, pullRequest.number, base.resolved.repoRoot);
|
|
814
|
-
if (!beforeBind.ok || !beforeBind.value) {
|
|
815
|
-
const error = beforeBind.ok
|
|
816
|
-
? { code: 'PR_BIND_RECHECK_FAILED', message: 'Pull request recheck returned no identity', retryable: false }
|
|
817
|
-
: beforeBind.error;
|
|
818
|
-
return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
819
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
820
|
-
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
if (beforeBind.value.head.sha !== expectedHeadSha
|
|
824
|
-
|| beforeBind.value.head.ref !== expectedHead(base.context.platform.repository, options.head).ref
|
|
825
|
-
|| beforeBind.value.base.ref !== options.base
|
|
826
|
-
|| beforeBind.value.base.repository !== base.context.platform.repository)
|
|
827
|
-
return result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
828
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
829
|
-
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest: beforeBind.value,
|
|
830
|
-
error: { code: 'PR_BIND_IDENTITY_MISMATCH', message: 'Pull request identity changed before task binding', retryable: false }
|
|
831
|
-
});
|
|
832
|
-
pullRequest = beforeBind.value;
|
|
833
|
-
const refreshed = resolvedContext(taskRef, options);
|
|
834
|
-
if (!refreshed.ok)
|
|
835
|
-
return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
836
|
-
pullRequest, error: { code: 'PR_CREATED_BIND_FAILED', message: pullRequest.url, retryable: false }
|
|
837
|
-
});
|
|
838
|
-
const bound = bindIdentity(refreshed, pullRequest, options.agent);
|
|
839
|
-
if (bound.status === 'failed')
|
|
840
|
-
return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
841
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
842
|
-
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
|
|
843
|
-
operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: 'applied', reasonCode: null }, { name: 'task:bind-pr', status: 'failed', reasonCode: bound.error.code }],
|
|
844
|
-
error: { code: 'PR_CREATED_BIND_FAILED', message: `${pullRequest.url}: ${bound.error.message}`, retryable: false }
|
|
845
|
-
});
|
|
846
|
-
return result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
847
|
-
changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
|
|
848
|
-
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
|
|
849
|
-
operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: created ? 'applied' : 'no-op', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }],
|
|
850
|
-
result: created ? 'pr_created' : 'pr_reused', error: null
|
|
851
|
-
});
|
|
1074
|
+
}), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
852
1075
|
}
|
|
853
|
-
function createPlatformPullRequest(taskRef, options) {
|
|
854
|
-
const base = resolvedContext(taskRef, options);
|
|
1076
|
+
async function createPlatformPullRequest(taskRef, options) {
|
|
1077
|
+
const base = await resolvedContext(taskRef, options);
|
|
855
1078
|
if (!base.ok)
|
|
856
|
-
return base.output;
|
|
1079
|
+
return withCreation(base.output, PRECONDITION_NOT_CREATED);
|
|
857
1080
|
if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) {
|
|
858
|
-
return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1081
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
859
1082
|
error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
|
|
860
|
-
});
|
|
1083
|
+
}), PRECONDITION_NOT_CREATED);
|
|
861
1084
|
}
|
|
862
1085
|
try {
|
|
863
|
-
return withTaskExecutionLock(base.resolved.repoRoot, base.resolved.taskId, 'platform-pr.create', () =>
|
|
1086
|
+
return withTaskExecutionLock(base.resolved.repoRoot, base.resolved.taskId, 'platform-pr.create', () => createExternalPullRequest(taskRef, options, base));
|
|
864
1087
|
}
|
|
865
1088
|
catch (error) {
|
|
866
1089
|
if (error instanceof TaskExecutionLockError) {
|
|
867
|
-
return result('blocked', base.resolved.taskId, base.issueNumber, null, {
|
|
1090
|
+
return withCreation(result('blocked', base.resolved.taskId, base.issueNumber, null, {
|
|
868
1091
|
platform: base.context.platform,
|
|
869
1092
|
capabilities: base.context.capabilities,
|
|
870
1093
|
error: { code: error.code, message: error.message, retryable: true }
|
|
871
|
-
});
|
|
1094
|
+
}), PRECONDITION_NOT_CREATED);
|
|
872
1095
|
}
|
|
873
|
-
return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1096
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
874
1097
|
platform: base.context.platform,
|
|
875
1098
|
capabilities: base.context.capabilities,
|
|
876
1099
|
error: {
|
|
@@ -878,13 +1101,147 @@ function createPlatformPullRequest(taskRef, options) {
|
|
|
878
1101
|
message: error instanceof Error ? error.message : String(error),
|
|
879
1102
|
retryable: false
|
|
880
1103
|
}
|
|
1104
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
async function createExternalPullRequest(taskRef, options, base) {
|
|
1108
|
+
if (!base.fact)
|
|
1109
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1110
|
+
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
1111
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1112
|
+
const target = taskDeliveryTarget(base);
|
|
1113
|
+
if (!target.ok)
|
|
1114
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1115
|
+
error: target.error
|
|
1116
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1117
|
+
if (options.base !== target.value.baseRef)
|
|
1118
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1119
|
+
platform: base.context.platform,
|
|
1120
|
+
capabilities: base.context.capabilities,
|
|
1121
|
+
error: { code: 'PR_DELIVERY_TARGET_MISMATCH', message: `Pull request base '${options.base}' does not match delivery target '${target.value.baseRef}'`, retryable: false }
|
|
1122
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1123
|
+
let verifiedHeadSha;
|
|
1124
|
+
if (base.provider.changeRequests?.verifyHead) {
|
|
1125
|
+
const verified = await base.provider.changeRequests.verifyHead({
|
|
1126
|
+
context: providerOperationContext(base.loadedContext),
|
|
1127
|
+
head: options.head
|
|
881
1128
|
});
|
|
1129
|
+
if (!verified.ok)
|
|
1130
|
+
return withCreation(providerPullRequestError(base, verified.error, base.prNumber), PRECONDITION_NOT_CREATED);
|
|
1131
|
+
verifiedHeadSha = verified.value.sha;
|
|
1132
|
+
}
|
|
1133
|
+
if (base.prIdentity) {
|
|
1134
|
+
const inspected = await inspectExternalPullRequest(base, base.prIdentity);
|
|
1135
|
+
return inspected.status === 'failed' || inspected.status === 'blocked'
|
|
1136
|
+
? withCreation(inspected, PRECONDITION_NOT_CREATED)
|
|
1137
|
+
: withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
|
|
882
1138
|
}
|
|
1139
|
+
if (options.dryRun)
|
|
1140
|
+
return withCreation(result('planned', base.resolved.taskId, base.issueNumber, null, {
|
|
1141
|
+
platform: base.context.platform,
|
|
1142
|
+
capabilities: base.context.capabilities,
|
|
1143
|
+
operations: [{ name: 'pr:create', status: 'planned', reasonCode: null }],
|
|
1144
|
+
result: 'pr_created',
|
|
1145
|
+
error: null
|
|
1146
|
+
}), { kind: 'planned', action: 'create', createdByCurrentOperation: false });
|
|
1147
|
+
const started = writeCreateStarted(base, options.agent);
|
|
1148
|
+
if (started.status === 'failed')
|
|
1149
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1150
|
+
error: { code: started.error.code, message: started.error.message, retryable: false }
|
|
1151
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1152
|
+
const created = base.provider.changeRequests?.create
|
|
1153
|
+
? await base.provider.changeRequests.create({
|
|
1154
|
+
context: providerOperationContext(base.loadedContext),
|
|
1155
|
+
base: options.base,
|
|
1156
|
+
head: options.head,
|
|
1157
|
+
title: options.title,
|
|
1158
|
+
body: options.body,
|
|
1159
|
+
draft: Boolean(options.draft),
|
|
1160
|
+
mutation: { idempotencyKey: `pull-request:create:${base.resolved.taskId}` }
|
|
1161
|
+
})
|
|
1162
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.create');
|
|
1163
|
+
if (!created.ok)
|
|
1164
|
+
return withCreation(providerPullRequestError(base, created.error, null), { kind: 'unknown', errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' });
|
|
1165
|
+
const remoteId = String(created.value.remoteId || '');
|
|
1166
|
+
let createdIdentity;
|
|
1167
|
+
try {
|
|
1168
|
+
createdIdentity = providerResourceIdentity(base.provider, 'pull-request', remoteId);
|
|
1169
|
+
}
|
|
1170
|
+
catch (error) {
|
|
1171
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1172
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1173
|
+
error: { code: 'PR_CREATE_RESPONSE_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
1174
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1175
|
+
}
|
|
1176
|
+
const number = resourceIdentityNumber(createdIdentity);
|
|
1177
|
+
const fallbackNumber = number ?? 0;
|
|
1178
|
+
const fallback = normalizeProviderPullRequest({
|
|
1179
|
+
id: remoteId,
|
|
1180
|
+
identity: createdIdentity,
|
|
1181
|
+
number: number || undefined,
|
|
1182
|
+
state: 'open',
|
|
1183
|
+
title: options.title,
|
|
1184
|
+
body: options.body,
|
|
1185
|
+
headSha: '',
|
|
1186
|
+
baseSha: '',
|
|
1187
|
+
head: { repository: base.context.platform.repository, ref: options.head, sha: '' },
|
|
1188
|
+
base: { repository: base.context.platform.repository, ref: options.base, sha: '' },
|
|
1189
|
+
displayUrl: `${base.provider.type}:${remoteId}`
|
|
1190
|
+
}, base.context.platform.repository, fallbackNumber);
|
|
1191
|
+
const inspected = base.provider.changeRequests?.inspect
|
|
1192
|
+
? await base.provider.changeRequests.inspect({
|
|
1193
|
+
context: providerOperationContext(base.loadedContext),
|
|
1194
|
+
target: createdIdentity
|
|
1195
|
+
})
|
|
1196
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
|
|
1197
|
+
if (!inspected.ok)
|
|
1198
|
+
return withCreation(providerPullRequestError(base, inspected.error, number), { kind: 'created', createdByCurrentOperation: true });
|
|
1199
|
+
const pullRequest = normalizeProviderPullRequest(inspected.value, base.context.platform.repository, inspected.value.number ?? fallback.number, fallback);
|
|
1200
|
+
if (!pullRequest.nodeId || !isUsableResourceIdentity(pullRequestIdentity(pullRequest)))
|
|
1201
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1202
|
+
platform: base.context.platform,
|
|
1203
|
+
capabilities: base.context.capabilities,
|
|
1204
|
+
error: { code: 'PR_CREATE_RESPONSE_INVALID', message: 'Provider create response lacks validated identity', retryable: false }
|
|
1205
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1206
|
+
const identity = validateWriterIdentity(base, pullRequest, {
|
|
1207
|
+
expectedHead: options.head,
|
|
1208
|
+
expectedHeadSha: verifiedHeadSha,
|
|
1209
|
+
errorCode: 'PR_BIND_IDENTITY_MISMATCH'
|
|
1210
|
+
});
|
|
1211
|
+
if (!identity.ok)
|
|
1212
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1213
|
+
platform: base.context.platform,
|
|
1214
|
+
capabilities: base.context.capabilities,
|
|
1215
|
+
resource: { kind: 'pull-request', number: pullRequest.number },
|
|
1216
|
+
pullRequest,
|
|
1217
|
+
error: identity.error
|
|
1218
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1219
|
+
const bound = bindIdentity(base, pullRequest, options.agent, 'created', false, options.head);
|
|
1220
|
+
if (bound.status === 'failed')
|
|
1221
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1222
|
+
platform: base.context.platform,
|
|
1223
|
+
capabilities: base.context.capabilities,
|
|
1224
|
+
resource: { kind: 'pull-request', number: pullRequest.number },
|
|
1225
|
+
pullRequest,
|
|
1226
|
+
error: { code: 'PR_CREATED_BIND_FAILED', message: `${pullRequest.url}: ${bound.error.message}`, retryable: false }
|
|
1227
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1228
|
+
return withCreation(result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1229
|
+
changed: true,
|
|
1230
|
+
platform: base.context.platform,
|
|
1231
|
+
capabilities: base.context.capabilities,
|
|
1232
|
+
resource: { kind: 'pull-request', number: pullRequest.number },
|
|
1233
|
+
pullRequest,
|
|
1234
|
+
operations: [{ name: 'pr:create', status: 'applied', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }],
|
|
1235
|
+
result: 'pr_created',
|
|
1236
|
+
error: null
|
|
1237
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
883
1238
|
}
|
|
884
|
-
function syncPlatformPullRequest(taskRef, options) {
|
|
1239
|
+
async function syncPlatformPullRequest(taskRef, options) {
|
|
885
1240
|
const warningResult = warningResultForPrimary(options.primaryResult);
|
|
886
1241
|
const softenFailure = (output) => {
|
|
1242
|
+
const authorityError = output.error?.code.startsWith('IN_LABEL_SYNC') || output.error?.code.startsWith('PR_IDENTITY');
|
|
887
1243
|
const warning = output.error && output.task.prNumber !== null
|
|
1244
|
+
&& !authorityError
|
|
888
1245
|
&& !['PR_NOT_LINKED', 'PR_BIND_CONFLICT'].includes(output.error.code)
|
|
889
1246
|
? {
|
|
890
1247
|
code: output.error.code,
|
|
@@ -899,77 +1256,268 @@ function syncPlatformPullRequest(taskRef, options) {
|
|
|
899
1256
|
? { ...output, status: 'applied', changed: false, error: null, result: warningResult, warnings: mergeOperationWarnings([warning]) }
|
|
900
1257
|
: output;
|
|
901
1258
|
};
|
|
902
|
-
const base = resolvedContext(taskRef, options);
|
|
1259
|
+
const base = await resolvedContext(taskRef, options);
|
|
903
1260
|
if (!base.ok)
|
|
904
1261
|
return softenFailure(base.output);
|
|
905
|
-
if (!base.
|
|
1262
|
+
if (!base.fact)
|
|
906
1263
|
return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
907
|
-
error: { code: '
|
|
908
|
-
}));
|
|
909
|
-
const fetched = inspectGitHubPullRequest(base.client, base.context.platform.repository, base.prNumber, base.resolved.repoRoot);
|
|
910
|
-
if (!fetched.ok)
|
|
911
|
-
return softenFailure(result(fetched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { error: fetched.error }));
|
|
912
|
-
if (!options.metadata && !options.closingIssue)
|
|
913
|
-
return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
914
|
-
error: { code: 'PR_PAYLOAD_INVALID', message: 'sync requires a desired-state option', retryable: false }
|
|
915
|
-
}));
|
|
916
|
-
if (!base.issueNumber)
|
|
917
|
-
return softenFailure(result('degraded', base.resolved.taskId, null, base.prNumber, {
|
|
918
|
-
pullRequest: fetched.value, error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no linked Issue to copy metadata from', retryable: false }
|
|
919
|
-
}));
|
|
920
|
-
const issue = inspectPlatformIssue(taskRef, { cwd: base.resolved.repoRoot, client: base.client });
|
|
921
|
-
if (!issue.issue)
|
|
922
|
-
return softenFailure(result(issue.status, base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
923
|
-
platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: fetched.value, error: issue.error
|
|
1264
|
+
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
924
1265
|
}));
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
taskType: base.frontmatter.type || 'task',
|
|
929
|
-
issueNumber: base.issueNumber,
|
|
930
|
-
capabilities: base.context.capabilities
|
|
931
|
-
}).operations.filter((operation) => options.metadata || operation.name === 'closing-issue')
|
|
932
|
-
.filter((operation) => options.closingIssue || operation.name !== 'closing-issue');
|
|
933
|
-
const operations = planned.map(({ name, status, reasonCode }) => ({ name, status, reasonCode }));
|
|
934
|
-
if (options.dryRun)
|
|
935
|
-
return softenFailure(result(planned.some((operation) => operation.status === 'planned') ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
936
|
-
platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: fetched.value, operations, error: null
|
|
1266
|
+
if (!base.prIdentity)
|
|
1267
|
+
return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1268
|
+
error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
|
|
937
1269
|
}));
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
if (
|
|
941
|
-
|
|
942
|
-
if (
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1270
|
+
{
|
|
1271
|
+
const inspected = await inspectExternalPullRequest(base, base.prIdentity);
|
|
1272
|
+
if (inspected.status === 'failed' || inspected.status === 'blocked' || !inspected.pullRequest)
|
|
1273
|
+
return softenFailure(inspected);
|
|
1274
|
+
if (!options.metadata && !options.closingIssue)
|
|
1275
|
+
return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1276
|
+
error: { code: 'PR_PAYLOAD_INVALID', message: 'sync requires a desired-state option', retryable: false }
|
|
1277
|
+
}));
|
|
1278
|
+
const issue = await inspectPlatformIssue(taskRef, { cwd: base.resolved.repoRoot, client: base.client });
|
|
1279
|
+
if (!issue.issue)
|
|
1280
|
+
return softenFailure(result(issue.status, base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1281
|
+
platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: inspected.pullRequest, error: issue.error
|
|
1282
|
+
}));
|
|
1283
|
+
let inLabels = inspected.pullRequest.labels.filter((label) => label.startsWith('in:')).sort();
|
|
1284
|
+
let issueInLabelOperation = null;
|
|
1285
|
+
if (options.metadata && base.provider.issues?.listLabels) {
|
|
1286
|
+
const labels = await base.provider.issues.listLabels({ context: providerOperationContext(base.loadedContext) });
|
|
1287
|
+
if (!labels.ok)
|
|
1288
|
+
return softenFailure(providerPullRequestError(base, labels.error, base.prNumber));
|
|
1289
|
+
const target = taskInLabelTarget(base.resolved.repoRoot, base.frontmatter, inspected.pullRequest.labels, new Set(labels.value));
|
|
1290
|
+
if (!target.ok)
|
|
1291
|
+
return softenFailure(providerPullRequestError(base, target.error, base.prNumber));
|
|
1292
|
+
inLabels = target.value;
|
|
1293
|
+
const currentIssueLabels = issue.issue.labels.filter((label) => label.startsWith('in:')).sort();
|
|
1294
|
+
issueInLabelOperation = base.context.capabilities.triage
|
|
1295
|
+
? currentIssueLabels.join('\0') === inLabels.join('\0')
|
|
1296
|
+
? { name: 'labels:in:issue', status: 'no-op', reasonCode: null }
|
|
1297
|
+
: { name: 'labels:in:issue', status: 'planned', reasonCode: null }
|
|
1298
|
+
: { name: 'labels:in:issue', status: 'skipped', reasonCode: 'TRIAGE_REQUIRED' };
|
|
1299
|
+
}
|
|
1300
|
+
const planned = planPullRequestMetadata({
|
|
1301
|
+
pullRequest: inspected.pullRequest,
|
|
1302
|
+
issue: issue.issue,
|
|
1303
|
+
taskType: String(base.frontmatter.type || 'task'),
|
|
1304
|
+
issueNumber: base.issueNumber,
|
|
1305
|
+
capabilities: base.context.capabilities,
|
|
1306
|
+
inLabels
|
|
1307
|
+
}).operations.filter((operation) => options.metadata || operation.name === 'closing-issue')
|
|
1308
|
+
.filter((operation) => options.closingIssue || operation.name !== 'closing-issue');
|
|
1309
|
+
const operations = [
|
|
1310
|
+
...(issueInLabelOperation ? [issueInLabelOperation] : []),
|
|
1311
|
+
...planned.map(({ name, status, reasonCode }) => ({ name, status, reasonCode }))
|
|
1312
|
+
];
|
|
1313
|
+
const milestoneOperation = planned.find((operation) => operation.name === 'milestone' && operation.status === 'planned');
|
|
1314
|
+
if (milestoneOperation && base.provider.issues?.listMilestones) {
|
|
1315
|
+
const milestones = await base.provider.issues.listMilestones({ context: providerOperationContext(base.loadedContext) });
|
|
950
1316
|
if (!milestones.ok)
|
|
951
|
-
return
|
|
952
|
-
const flat = milestones.value.flatMap((entry) => Array.isArray(entry) ? entry : [entry]);
|
|
953
|
-
payload.milestone = flat.find((entry) => entry.title === operation.value)?.number ?? null;
|
|
1317
|
+
return providerPullRequestError(base, milestones.error, base.prNumber);
|
|
954
1318
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1319
|
+
if (options.dryRun)
|
|
1320
|
+
return softenFailure(result(planned.some((operation) => operation.status === 'planned') ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1321
|
+
platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: inspected.pullRequest, operations, error: null
|
|
1322
|
+
}));
|
|
1323
|
+
let issueChanged = false;
|
|
1324
|
+
if (issueInLabelOperation?.status === 'planned') {
|
|
1325
|
+
const desiredIssueLabels = [...new Set([
|
|
1326
|
+
...issue.issue.labels.filter((label) => !label.startsWith('in:')),
|
|
1327
|
+
...inLabels
|
|
1328
|
+
])].sort();
|
|
1329
|
+
const updatedIssue = base.provider.issues?.update
|
|
1330
|
+
? await base.provider.issues.update({
|
|
1331
|
+
context: providerOperationContext(base.loadedContext),
|
|
1332
|
+
target: base.issueIdentity,
|
|
1333
|
+
currentLabels: issue.issue.labels,
|
|
1334
|
+
patch: { labels: desiredIssueLabels },
|
|
1335
|
+
mutation: { idempotencyKey: `issue:update:${base.resolved.taskId}:in-labels` }
|
|
1336
|
+
})
|
|
1337
|
+
: unsupportedProviderOperation(base.provider, 'issues.update');
|
|
1338
|
+
if (!updatedIssue.ok)
|
|
1339
|
+
return softenFailure(providerPullRequestError(base, updatedIssue.error, base.prNumber));
|
|
1340
|
+
issueChanged = updatedIssue.value.changed;
|
|
1341
|
+
const rereadIssue = base.provider.issues?.inspect
|
|
1342
|
+
? await base.provider.issues.inspect({ context: providerOperationContext(base.loadedContext), target: base.issueIdentity })
|
|
1343
|
+
: unsupportedProviderOperation(base.provider, 'issues.inspect');
|
|
1344
|
+
if (!rereadIssue.ok)
|
|
1345
|
+
return softenFailure(providerPullRequestError(base, {
|
|
1346
|
+
...rereadIssue.error,
|
|
1347
|
+
code: 'IN_LABEL_SYNC_PARTIAL',
|
|
1348
|
+
message: `In-label synchronization is partial or unknown: ${rereadIssue.error.message}`
|
|
1349
|
+
}, base.prNumber));
|
|
1350
|
+
const actualIssueLabels = rereadIssue.value.labels.filter((label) => label.startsWith('in:')).sort();
|
|
1351
|
+
if (actualIssueLabels.join('\0') !== inLabels.join('\0'))
|
|
1352
|
+
return softenFailure(providerPullRequestError(base, {
|
|
1353
|
+
code: 'IN_LABEL_SYNC_PARTIAL',
|
|
1354
|
+
message: 'Issue in: labels did not converge after update',
|
|
1355
|
+
retryable: true
|
|
1356
|
+
}, base.prNumber));
|
|
1357
|
+
}
|
|
1358
|
+
const patch = {};
|
|
1359
|
+
for (const operation of planned) {
|
|
1360
|
+
if (operation.status !== 'planned')
|
|
1361
|
+
continue;
|
|
1362
|
+
if (operation.name === 'labels')
|
|
1363
|
+
patch.labels = operation.value;
|
|
1364
|
+
if (operation.name === 'assignees')
|
|
1365
|
+
patch.assignees = operation.value;
|
|
1366
|
+
if (operation.name === 'closing-issue')
|
|
1367
|
+
patch.body = operation.value;
|
|
1368
|
+
if (operation.name === 'milestone')
|
|
1369
|
+
patch.milestone = operation.value;
|
|
1370
|
+
}
|
|
1371
|
+
if (Object.keys(patch).length === 0)
|
|
1372
|
+
return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1373
|
+
platform: base.context.platform, capabilities: base.context.capabilities, resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: inspected.pullRequest, operations, error: null
|
|
1374
|
+
}));
|
|
1375
|
+
const updated = base.provider.changeRequests?.update
|
|
1376
|
+
? await base.provider.changeRequests.update({
|
|
1377
|
+
context: providerOperationContext(base.loadedContext),
|
|
1378
|
+
target: base.prIdentity,
|
|
1379
|
+
currentLabels: inspected.pullRequest.labels,
|
|
1380
|
+
patch: patch,
|
|
1381
|
+
mutation: { idempotencyKey: `pull-request:update:${base.resolved.taskId}` }
|
|
1382
|
+
})
|
|
1383
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.update');
|
|
1384
|
+
if (!updated.ok) {
|
|
1385
|
+
const error = issueChanged && !updated.error.code.startsWith('IN_LABEL_SYNC')
|
|
1386
|
+
? { ...updated.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${updated.error.message}` }
|
|
1387
|
+
: updated.error;
|
|
1388
|
+
return softenFailure(providerPullRequestError(base, error, base.prNumber));
|
|
1389
|
+
}
|
|
1390
|
+
let finalPullRequest = inspected.pullRequest;
|
|
1391
|
+
if (planned.some((operation) => operation.name === 'labels' && operation.status === 'planned')) {
|
|
1392
|
+
const reread = base.provider.changeRequests?.inspect
|
|
1393
|
+
? await base.provider.changeRequests.inspect({ context: providerOperationContext(base.loadedContext), target: base.prIdentity })
|
|
1394
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
|
|
1395
|
+
if (!reread.ok)
|
|
1396
|
+
return softenFailure(providerPullRequestError(base, {
|
|
1397
|
+
...reread.error,
|
|
1398
|
+
code: 'IN_LABEL_SYNC_PARTIAL',
|
|
1399
|
+
message: `In-label synchronization is partial or unknown: ${reread.error.message}`
|
|
1400
|
+
}, base.prNumber));
|
|
1401
|
+
finalPullRequest = normalizeProviderPullRequest(reread.value, base.context.platform.repository, base.prNumber || 0, inspected.pullRequest);
|
|
1402
|
+
const expected = ((planned.find((operation) => operation.name === 'labels')?.value || [])
|
|
1403
|
+
.filter((label) => label.startsWith('in:') || label.startsWith('type:'))).sort();
|
|
1404
|
+
const actual = finalPullRequest.labels.filter((label) => label.startsWith('in:') || label.startsWith('type:')).sort();
|
|
1405
|
+
if (actual.join('\0') !== expected.join('\0'))
|
|
1406
|
+
return softenFailure(providerPullRequestError(base, {
|
|
1407
|
+
code: 'IN_LABEL_SYNC_PARTIAL',
|
|
1408
|
+
message: 'Pull request labels did not converge after update',
|
|
1409
|
+
retryable: true
|
|
1410
|
+
}, base.prNumber));
|
|
1411
|
+
}
|
|
1412
|
+
return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1413
|
+
changed: issueChanged || updated.value.changed,
|
|
1414
|
+
platform: base.context.platform,
|
|
1415
|
+
capabilities: base.context.capabilities,
|
|
1416
|
+
resource: { kind: 'pull-request', number: base.prNumber },
|
|
1417
|
+
pullRequest: finalPullRequest,
|
|
1418
|
+
operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation),
|
|
1419
|
+
error: null
|
|
960
1420
|
}));
|
|
961
1421
|
}
|
|
962
|
-
|
|
963
|
-
|
|
1422
|
+
}
|
|
1423
|
+
function readProjectPrFlow(repoRoot) {
|
|
1424
|
+
try {
|
|
1425
|
+
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
|
|
1426
|
+
return config.prFlow === 'required' || config.prFlow === 'disabled' ? config.prFlow : undefined;
|
|
1427
|
+
}
|
|
1428
|
+
catch {
|
|
1429
|
+
return undefined;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
function skipPlatformPullRequestFactUnlocked(resolved, options) {
|
|
1433
|
+
let content;
|
|
1434
|
+
let frontmatter;
|
|
1435
|
+
try {
|
|
1436
|
+
content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
1437
|
+
frontmatter = parseTypedTaskFrontmatter(content);
|
|
1438
|
+
}
|
|
1439
|
+
catch (error) {
|
|
1440
|
+
return result('failed', resolved.taskId, null, null, {
|
|
1441
|
+
error: { code: 'TASK_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
let issueIdentity;
|
|
1445
|
+
try {
|
|
1446
|
+
issueIdentity = taskIssueIdentity(frontmatter, undefined, options.runtimeVersion);
|
|
1447
|
+
}
|
|
1448
|
+
catch (error) {
|
|
1449
|
+
return result('failed', resolved.taskId, null, null, { error: { ...taskIssueIdentityError(error), retryable: false } });
|
|
1450
|
+
}
|
|
1451
|
+
const issueNumber = resourceIdentityNumber(issueIdentity);
|
|
1452
|
+
const existing = readPrDeliveryFact(frontmatter, options.runtimeVersion);
|
|
1453
|
+
if (existing.status === 'invalid')
|
|
1454
|
+
return result('failed', resolved.taskId, issueNumber, null, {
|
|
1455
|
+
error: { code: existing.error.code, message: existing.error.message, retryable: false }
|
|
1456
|
+
});
|
|
1457
|
+
if (existing.status === 'missing')
|
|
1458
|
+
return result('failed', resolved.taskId, issueNumber, null, {
|
|
1459
|
+
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
1460
|
+
});
|
|
1461
|
+
if (existing.fact.state === 'skipped')
|
|
1462
|
+
return result('no-op', resolved.taskId, issueNumber, null, {
|
|
1463
|
+
operations: [{ name: 'task:skip-pr', status: 'no-op', reasonCode: null }],
|
|
1464
|
+
error: null
|
|
1465
|
+
});
|
|
1466
|
+
if (existing.fact.state === 'bound') {
|
|
1467
|
+
const boundNumber = resourceIdentityNumber(existing.fact.identity.resource);
|
|
1468
|
+
return result('failed', resolved.taskId, issueNumber, boundNumber, {
|
|
1469
|
+
error: { code: 'PR_DELIVERY_FACT_CONFLICT', message: `Task is already bound to PR ${boundNumber === null ? 'identity' : `#${boundNumber}`}`, retryable: false }
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
if (readProjectPrFlow(resolved.repoRoot) === 'required')
|
|
1473
|
+
return result('failed', resolved.taskId, issueNumber, null, {
|
|
1474
|
+
error: { code: 'PR_SKIP_FORBIDDEN', message: 'Cannot skip PR delivery when prFlow is required', retryable: false }
|
|
1475
|
+
});
|
|
1476
|
+
const metadata = captureTaskWriteMetadata();
|
|
1477
|
+
const fact = buildSkippedFact(factTimestamp(metadata.timestamp));
|
|
1478
|
+
const write = writeTask({
|
|
1479
|
+
taskRef: resolved.taskId,
|
|
1480
|
+
expectedState: 'active',
|
|
1481
|
+
dryRun: options.dryRun,
|
|
1482
|
+
mutations: [
|
|
1483
|
+
{ kind: 'frontmatter', set: { ...factFrontmatterMutation(fact).set, assigned_to: options.agent } },
|
|
1484
|
+
{ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(content, `- ${metadata.timestamp} — **Skip PR** by ${options.agent} — PR delivery explicitly skipped`) }
|
|
1485
|
+
]
|
|
1486
|
+
}, { repoRoot: resolved.repoRoot, metadataProvider: () => metadata });
|
|
1487
|
+
if (write.status === 'failed')
|
|
1488
|
+
return result('failed', resolved.taskId, issueNumber, null, {
|
|
1489
|
+
error: { code: write.error.code, message: write.error.message, retryable: false }
|
|
1490
|
+
});
|
|
1491
|
+
const status = options.dryRun ? 'planned' : write.status === 'no-op' ? 'no-op' : 'applied';
|
|
1492
|
+
return result(status, resolved.taskId, issueNumber, null, {
|
|
1493
|
+
changed: !options.dryRun && write.changed,
|
|
1494
|
+
operations: [{ name: 'task:skip-pr', status, reasonCode: null }],
|
|
1495
|
+
error: null
|
|
964
1496
|
});
|
|
965
|
-
if (!patched.ok)
|
|
966
|
-
return softenFailure(result(patched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: patched.error }));
|
|
967
|
-
return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
968
|
-
changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
|
|
969
|
-
resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: fetched.value,
|
|
970
|
-
operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
|
|
971
|
-
}));
|
|
972
1497
|
}
|
|
973
|
-
|
|
1498
|
+
async function skipPlatformPullRequestFact(taskRef, options) {
|
|
1499
|
+
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
1500
|
+
if (!resolved.ok)
|
|
1501
|
+
return result('failed', resolved.taskId, null, null, {
|
|
1502
|
+
error: { code: resolved.code, message: resolved.message, retryable: false }
|
|
1503
|
+
});
|
|
1504
|
+
if (resolved.state !== 'active')
|
|
1505
|
+
return result('failed', resolved.taskId, null, null, {
|
|
1506
|
+
error: { code: 'TASK_STATE_INVALID', message: 'platform-pr skip requires an active task', retryable: false }
|
|
1507
|
+
});
|
|
1508
|
+
try {
|
|
1509
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'platform-pr.skip', () => skipPlatformPullRequestFactUnlocked(resolved, options));
|
|
1510
|
+
}
|
|
1511
|
+
catch (error) {
|
|
1512
|
+
if (error instanceof TaskExecutionLockError)
|
|
1513
|
+
return result('blocked', resolved.taskId, null, null, {
|
|
1514
|
+
error: { code: error.code, message: error.message, retryable: true }
|
|
1515
|
+
});
|
|
1516
|
+
return result('failed', resolved.taskId, null, null, {
|
|
1517
|
+
error: { code: 'PR_SKIP_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
export { bindPlatformPullRequest, createPlatformPullRequest, inspectGitHubPullRequest, inspectGitHubIssueClosingChangeRequests, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, normalizePullRequest, resolveExternalPullRequest, skipPlatformPullRequestFact, selectExternalPullRequest, selectPullRequest, syncPlatformPullRequest, syncPlatformPullRequestInLabels };
|
|
974
1522
|
export { warningResultForPrimary };
|
|
975
1523
|
//# sourceMappingURL=pull-requests.js.map
|