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