@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
|
@@ -5,17 +5,19 @@ import { parseTypedTaskFrontmatter } from '../task/frontmatter.js';
|
|
|
5
5
|
import { resolveTaskRef } from '../task/resolve-ref.js';
|
|
6
6
|
import { extractSection } from '../task/sections.js';
|
|
7
7
|
import { captureTaskWriteMetadata, writeTask } from '../task/write.js';
|
|
8
|
-
import {
|
|
9
|
-
import { parseGitHubRemote, resolvePlatformContext } from './context.js';
|
|
10
|
-
import { createGitHubClient } from './github-client.js';
|
|
8
|
+
import { resolvePlatformProviderContext } from './context.js';
|
|
11
9
|
import { inspectPlatformIssue } from './issues.js';
|
|
12
10
|
import { planPullRequestMetadata } from './pull-request-metadata.js';
|
|
11
|
+
import { planInLabelUpdate, validateInLabelMapping } from './in-label-sync.js';
|
|
13
12
|
import { platformResult } from './types.js';
|
|
14
13
|
import { inspectCompletionArtifacts } from '../task/finalization-artifacts.js';
|
|
15
14
|
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
|
|
16
15
|
import { resolveDeliveryTarget } from '../task/delivery-target.js';
|
|
17
16
|
import { mergeOperationWarnings } from '../task/operation-outcome.js';
|
|
18
17
|
import { buildBoundFact, buildSkippedFact, factFrontmatterMutation, readPrDeliveryFact } from '../task/pr-delivery-fact.js';
|
|
18
|
+
import { providerError, providerOperationContext, providerStatus, providerResourceToken, providerResourceIdentity, resourceIdentity, unsupportedProviderOperation } from './provider-bridge.js';
|
|
19
|
+
import { isResourceIdentity, resourceIdentityEquals, resourceIdentityNumber, serializeResourceIdentity } from './resource-identity.js';
|
|
20
|
+
import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.js';
|
|
19
21
|
function warningResultForPrimary(primaryResult) {
|
|
20
22
|
if (primaryResult === 'pr_created')
|
|
21
23
|
return 'pr_created_with_warnings';
|
|
@@ -105,9 +107,15 @@ function normalizeClosingPullRequest(node, repository) {
|
|
|
105
107
|
function repositoryKey(repository) {
|
|
106
108
|
return repository.trim().toLowerCase();
|
|
107
109
|
}
|
|
110
|
+
function isUsableResourceIdentity(identity) {
|
|
111
|
+
return isResourceIdentity(identity);
|
|
112
|
+
}
|
|
113
|
+
function identityLabel(identity) {
|
|
114
|
+
return identity.kind === 'number' ? `PR #${identity.value}` : `PR ${identity.kind}:${identity.value}`;
|
|
115
|
+
}
|
|
108
116
|
function identitySignature(pullRequest) {
|
|
109
117
|
return [
|
|
110
|
-
pullRequest.repository, pullRequest
|
|
118
|
+
pullRequest.repository, JSON.stringify(pullRequestIdentity(pullRequest)), pullRequest.nodeId, pullRequest.url,
|
|
111
119
|
pullRequest.head.repository, pullRequest.head.ref, pullRequest.head.sha,
|
|
112
120
|
pullRequest.base.repository, pullRequest.base.ref, pullRequest.base.sha,
|
|
113
121
|
pullRequest.mergedAt || '', pullRequest.mergeCommitSha || ''
|
|
@@ -119,48 +127,52 @@ function hasCompleteExternalIdentity(pullRequest) {
|
|
|
119
127
|
pullRequest.head.repository, pullRequest.head.ref, pullRequest.head.sha,
|
|
120
128
|
pullRequest.base.repository, pullRequest.base.ref, pullRequest.base.sha
|
|
121
129
|
];
|
|
122
|
-
return
|
|
123
|
-
required.every((value) => Boolean(value?.trim())) &&
|
|
130
|
+
return isUsableResourceIdentity(pullRequestIdentity(pullRequest)) && required.every((value) => Boolean(value?.trim())) &&
|
|
124
131
|
(pullRequest.state === 'open' || Boolean(pullRequest.mergedAt) === Boolean(pullRequest.mergeCommitSha));
|
|
125
132
|
}
|
|
126
|
-
function selectExternalPullRequest(candidates, repository,
|
|
133
|
+
function selectExternalPullRequest(candidates, repository, existingPrIdentity, explicitPrIdentity) {
|
|
134
|
+
const existingIdentity = typeof existingPrIdentity === 'number'
|
|
135
|
+
? resourceIdentity(existingPrIdentity) : existingPrIdentity;
|
|
136
|
+
const explicitIdentity = typeof explicitPrIdentity === 'number'
|
|
137
|
+
? resourceIdentity(explicitPrIdentity) : explicitPrIdentity;
|
|
127
138
|
const invalid = candidates.find((candidate) => !hasCompleteExternalIdentity(candidate));
|
|
128
139
|
if (invalid)
|
|
129
140
|
return {
|
|
130
141
|
status: 'failed', code: 'PR_IDENTITY_INVALID',
|
|
131
142
|
message: `Closing PR #${invalid.number} lacks required identity`, candidates, eligible: []
|
|
132
143
|
};
|
|
133
|
-
const
|
|
144
|
+
const byIdentity = new Map();
|
|
134
145
|
for (const candidate of candidates) {
|
|
135
146
|
const signature = identitySignature(candidate);
|
|
136
|
-
const
|
|
147
|
+
const identityKey = JSON.stringify(pullRequestIdentity(candidate));
|
|
148
|
+
const prior = byIdentity.get(identityKey);
|
|
137
149
|
if (prior && prior !== signature)
|
|
138
150
|
return {
|
|
139
151
|
status: 'failed', code: 'PR_IDENTITY_INVALID',
|
|
140
152
|
message: `Closing PR #${candidate.number} has conflicting identities`, candidates, eligible: []
|
|
141
153
|
};
|
|
142
|
-
|
|
154
|
+
byIdentity.set(identityKey, signature);
|
|
143
155
|
}
|
|
144
|
-
const unique = candidates.filter((candidate, index) => candidates.findIndex((other) => other
|
|
156
|
+
const unique = candidates.filter((candidate, index) => candidates.findIndex((other) => resourceIdentityEquals(pullRequestIdentity(other), pullRequestIdentity(candidate))) === index);
|
|
145
157
|
const wantedRepository = repositoryKey(repository);
|
|
146
158
|
const eligible = unique.filter((candidate) => repositoryKey(candidate.repository) === wantedRepository &&
|
|
147
159
|
repositoryKey(candidate.base.repository) === wantedRepository &&
|
|
148
160
|
candidate.state === 'closed' &&
|
|
149
161
|
Boolean(candidate.mergedAt && candidate.mergeCommitSha));
|
|
150
|
-
if (
|
|
151
|
-
const selected = unique.find((candidate) => candidate
|
|
162
|
+
if (explicitIdentity !== null) {
|
|
163
|
+
const selected = unique.find((candidate) => resourceIdentityEquals(pullRequestIdentity(candidate), explicitIdentity));
|
|
152
164
|
if (!selected)
|
|
153
|
-
return { status: 'failed', code: 'PR_NOT_FOUND', message: `PR
|
|
154
|
-
if (!eligible.some((candidate) => candidate
|
|
155
|
-
return { status: 'failed', code: 'PR_IDENTITY_INVALID', message: `PR
|
|
165
|
+
return { status: 'failed', code: 'PR_NOT_FOUND', message: `PR ${identityLabel(explicitIdentity)} is not a closing PR`, candidates, eligible };
|
|
166
|
+
if (!eligible.some((candidate) => resourceIdentityEquals(pullRequestIdentity(candidate), explicitIdentity))) {
|
|
167
|
+
return { status: 'failed', code: 'PR_IDENTITY_INVALID', message: `PR ${identityLabel(explicitIdentity)} is not an eligible merged PR`, candidates, eligible };
|
|
156
168
|
}
|
|
157
|
-
if (
|
|
158
|
-
return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to
|
|
169
|
+
if (existingIdentity !== null && !resourceIdentityEquals(existingIdentity, explicitIdentity)) {
|
|
170
|
+
return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to ${identityLabel(existingIdentity)}`, candidates, eligible };
|
|
159
171
|
}
|
|
160
172
|
return { status: 'selected', source: 'explicit', selected, candidates, eligible };
|
|
161
173
|
}
|
|
162
|
-
if (
|
|
163
|
-
return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to
|
|
174
|
+
if (existingIdentity !== null && eligible.length === 1 && !resourceIdentityEquals(existingIdentity, pullRequestIdentity(eligible[0]))) {
|
|
175
|
+
return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to ${identityLabel(existingIdentity)}`, candidates, eligible };
|
|
164
176
|
}
|
|
165
177
|
if (eligible.length === 0)
|
|
166
178
|
return { status: 'normal', candidates, eligible: [] };
|
|
@@ -238,64 +250,6 @@ function selectPullRequest(remotes, repository, head, base, expectedSha) {
|
|
|
238
250
|
return { status: 'resolved', pullRequest: matches[0] };
|
|
239
251
|
return { status: matches.length === 0 ? 'missing' : 'ambiguous', pullRequest: null };
|
|
240
252
|
}
|
|
241
|
-
function repositoryHead(cwd) {
|
|
242
|
-
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
243
|
-
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
|
|
244
|
-
}).trim();
|
|
245
|
-
}
|
|
246
|
-
function remoteBranchHead(cwd, repository, head) {
|
|
247
|
-
const wanted = expectedHead(repository, head);
|
|
248
|
-
if (!wanted.ref || !/^[A-Za-z0-9._/-]+$/.test(wanted.ref))
|
|
249
|
-
return {
|
|
250
|
-
ok: false,
|
|
251
|
-
error: { code: 'PR_HEAD_INVALID', message: 'Pull request head ref is invalid', retryable: false }
|
|
252
|
-
};
|
|
253
|
-
const remotes = configuredGitRemotes(cwd);
|
|
254
|
-
const remote = remotes.find((item) => repositoryKey(item.repository) === repositoryKey(wanted.repository))
|
|
255
|
-
?? remotes.find((item) => item.name === 'origin');
|
|
256
|
-
if (!remote)
|
|
257
|
-
return {
|
|
258
|
-
ok: false,
|
|
259
|
-
error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `No configured remote can verify ${wanted.repository}:${wanted.ref}`, retryable: false }
|
|
260
|
-
};
|
|
261
|
-
try {
|
|
262
|
-
const output = execFileSync('git', ['ls-remote', '--refs', remote.name, `refs/heads/${wanted.ref}`], {
|
|
263
|
-
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
|
|
264
|
-
}).trim();
|
|
265
|
-
const match = output.split(/\r?\n/).map((line) => line.trim()).find((line) => line.endsWith(`refs/heads/${wanted.ref}`));
|
|
266
|
-
const sha = match?.split(/\s+/)[0] || null;
|
|
267
|
-
if (!sha || !/^[a-f0-9]{40}$/i.test(sha))
|
|
268
|
-
return {
|
|
269
|
-
ok: false,
|
|
270
|
-
error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `Remote branch ${wanted.repository}:${wanted.ref} does not exist`, retryable: false }
|
|
271
|
-
};
|
|
272
|
-
return { ok: true, value: sha };
|
|
273
|
-
}
|
|
274
|
-
catch (error) {
|
|
275
|
-
return {
|
|
276
|
-
ok: false,
|
|
277
|
-
error: { code: 'PR_REMOTE_BRANCH_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: true }
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
function verifyCreateHead(base, head) {
|
|
282
|
-
let localHead;
|
|
283
|
-
try {
|
|
284
|
-
localHead = repositoryHead(base.resolved.repoRoot);
|
|
285
|
-
}
|
|
286
|
-
catch (error) {
|
|
287
|
-
return { ok: false, error: { code: 'PR_LOCAL_HEAD_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: false } };
|
|
288
|
-
}
|
|
289
|
-
const remote = remoteBranchHead(base.resolved.repoRoot, base.context.platform.repository, head);
|
|
290
|
-
if (!remote.ok)
|
|
291
|
-
return remote;
|
|
292
|
-
if (remote.value !== localHead)
|
|
293
|
-
return {
|
|
294
|
-
ok: false,
|
|
295
|
-
error: { code: 'PR_REMOTE_HEAD_MISMATCH', message: `Remote head ${remote.value} does not match local HEAD ${localHead}`, retryable: false }
|
|
296
|
-
};
|
|
297
|
-
return { ok: true, value: localHead };
|
|
298
|
-
}
|
|
299
253
|
function validateBoundPullRequest(base, pullRequest, head, baseRef, expectedSha) {
|
|
300
254
|
if (!pullRequest)
|
|
301
255
|
return {
|
|
@@ -376,7 +330,7 @@ function validateExternalMergedEvidence(initial, rechecked) {
|
|
|
376
330
|
};
|
|
377
331
|
return { ok: true };
|
|
378
332
|
}
|
|
379
|
-
function resolvedContext(taskRef, options) {
|
|
333
|
+
async function resolvedContext(taskRef, options) {
|
|
380
334
|
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
381
335
|
if (!resolved.ok)
|
|
382
336
|
return { ok: false, output: result('failed', resolved.taskId, null, null, {
|
|
@@ -384,24 +338,87 @@ function resolvedContext(taskRef, options) {
|
|
|
384
338
|
}) };
|
|
385
339
|
const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
386
340
|
const frontmatter = parseTypedTaskFrontmatter(content);
|
|
387
|
-
const factRead = readPrDeliveryFact(frontmatter);
|
|
341
|
+
const factRead = readPrDeliveryFact(frontmatter, options.runtimeVersion);
|
|
388
342
|
if (factRead.status === 'invalid')
|
|
389
343
|
return { ok: false, output: result('failed', resolved.taskId, null, null, {
|
|
390
|
-
error: { code:
|
|
344
|
+
error: { code: factRead.error.code, message: factRead.error.message, retryable: false }
|
|
391
345
|
}) };
|
|
392
346
|
const fact = factRead.status === 'valid' ? factRead.fact : null;
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
347
|
+
let issueIdentity;
|
|
348
|
+
try {
|
|
349
|
+
issueIdentity = taskIssueIdentity(frontmatter, undefined, options.runtimeVersion);
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
return { ok: false, output: result('failed', resolved.taskId, null, null, { error: { ...taskIssueIdentityError(error), retryable: false } }) };
|
|
353
|
+
}
|
|
354
|
+
const issueNumber = resourceIdentityNumber(issueIdentity);
|
|
355
|
+
const prIdentity = fact?.state === 'bound' ? fact.identity.resource : null;
|
|
356
|
+
const prNumber = resourceIdentityNumber(prIdentity);
|
|
357
|
+
const client = options.client;
|
|
358
|
+
const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client });
|
|
359
|
+
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
399
360
|
const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
|
|
400
|
-
if (!usable)
|
|
361
|
+
if (!usable || !loaded.ok)
|
|
401
362
|
return { ok: false, output: result(context.status, resolved.taskId, issueNumber, prNumber, {
|
|
402
363
|
platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
|
|
403
364
|
}) };
|
|
404
|
-
return { ok: true, resolved, content, frontmatter, fact, issueNumber, prNumber, client, context };
|
|
365
|
+
return { ok: true, resolved, content, frontmatter, fact, issueIdentity, issueNumber, prIdentity, prNumber, client, context, provider: loaded.value.provider, providerType: loaded.value.providerType, loadedContext: loaded.value };
|
|
366
|
+
}
|
|
367
|
+
function normalizeProviderPullRequest(remote, repository, fallbackNumber, fallback) {
|
|
368
|
+
const head = remote.head || fallback?.head || { repository, ref: '', sha: remote.headSha || '' };
|
|
369
|
+
const base = remote.base || fallback?.base || { repository, ref: '', sha: remote.baseSha || '' };
|
|
370
|
+
const identity = remote.identity || fallback?.identity || (remote.number ? { kind: 'number', value: remote.number } : { kind: 'id', value: remote.id });
|
|
371
|
+
const snapshot = {
|
|
372
|
+
repository,
|
|
373
|
+
number: remote.number ?? resourceIdentityNumber(identity) ?? fallbackNumber,
|
|
374
|
+
nodeId: remote.id,
|
|
375
|
+
url: remote.displayUrl || fallback?.url || '',
|
|
376
|
+
state: remote.state === 'closed' ? 'closed' : 'open',
|
|
377
|
+
title: remote.title,
|
|
378
|
+
body: remote.body,
|
|
379
|
+
draft: remote.draft ?? fallback?.draft ?? false,
|
|
380
|
+
head: { ...head, sha: remote.headSha || head.sha },
|
|
381
|
+
base: { ...base, sha: remote.baseSha || base.sha },
|
|
382
|
+
mergedAt: remote.mergedAt ?? fallback?.mergedAt ?? null,
|
|
383
|
+
mergeCommitSha: remote.mergeCommitSha ?? fallback?.mergeCommitSha ?? null,
|
|
384
|
+
labels: [...(remote.labels || fallback?.labels || [])].sort(),
|
|
385
|
+
assignees: [...(remote.assignees || fallback?.assignees || [])].sort(),
|
|
386
|
+
milestone: remote.milestone ?? fallback?.milestone ?? null,
|
|
387
|
+
mergeability: remote.mergeability || fallback?.mergeability || { state: 'unknown', detail: null }
|
|
388
|
+
};
|
|
389
|
+
Object.defineProperty(snapshot, 'identity', { value: identity, enumerable: false, configurable: true });
|
|
390
|
+
return snapshot;
|
|
391
|
+
}
|
|
392
|
+
function pullRequestIdentity(pullRequest) {
|
|
393
|
+
return pullRequest.identity || { kind: 'number', value: pullRequest.number };
|
|
394
|
+
}
|
|
395
|
+
function providerPullRequestError(base, error, prNumber) {
|
|
396
|
+
return result(error.code.startsWith('IN_LABEL_SYNC') ? 'blocked' : providerStatus(error), base.resolved.taskId, base.issueNumber, prNumber, {
|
|
397
|
+
changed: error.code.startsWith('IN_LABEL_SYNC'),
|
|
398
|
+
platform: base.context.platform,
|
|
399
|
+
capabilities: base.context.capabilities,
|
|
400
|
+
resource: { kind: 'pull-request', number: prNumber },
|
|
401
|
+
error: providerError(error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
async function inspectExternalPullRequest(base, prIdentity) {
|
|
405
|
+
const prNumber = resourceIdentityNumber(prIdentity);
|
|
406
|
+
const fallback = base.fact?.state === 'bound' && resourceIdentityEquals(base.fact.identity.resource, prIdentity)
|
|
407
|
+
? base.fact.identity
|
|
408
|
+
: null;
|
|
409
|
+
const inspected = base.provider.changeRequests?.inspect
|
|
410
|
+
? await base.provider.changeRequests.inspect({ context: providerOperationContext(base.loadedContext), target: prIdentity })
|
|
411
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
|
|
412
|
+
if (!inspected.ok)
|
|
413
|
+
return providerPullRequestError(base, inspected.error, prNumber);
|
|
414
|
+
const pullRequest = normalizeProviderPullRequest(inspected.value, base.context.platform.repository, prNumber || 0, fallback);
|
|
415
|
+
return result('no-op', base.resolved.taskId, base.issueNumber, prNumber, {
|
|
416
|
+
platform: base.context.platform,
|
|
417
|
+
capabilities: base.context.capabilities,
|
|
418
|
+
resource: { kind: 'pull-request', number: prNumber, identity: prIdentity },
|
|
419
|
+
pullRequest,
|
|
420
|
+
error: null
|
|
421
|
+
});
|
|
405
422
|
}
|
|
406
423
|
function inspectGitHubPullRequest(client, repository, number, cwd) {
|
|
407
424
|
const fetched = client.json(['api', `repos/${repository}/pulls/${number}`], { cwd });
|
|
@@ -412,189 +429,287 @@ function inspectGitHubPullRequest(client, repository, number, cwd) {
|
|
|
412
429
|
? { ok: true, value: pullRequest }
|
|
413
430
|
: { ok: false, error: { code: 'PR_IDENTITY_INVALID', message: 'Remote resource is not a valid pull request', retryable: false } };
|
|
414
431
|
}
|
|
415
|
-
function
|
|
432
|
+
async function inspectPlatformPullRequest(taskRef, options = {}) {
|
|
433
|
+
const base = await resolvedContext(taskRef, options);
|
|
434
|
+
if (!base.ok)
|
|
435
|
+
return base.output;
|
|
436
|
+
if (!base.fact)
|
|
437
|
+
return result('no-op', base.resolved.taskId, base.issueNumber, null, {
|
|
438
|
+
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
439
|
+
});
|
|
440
|
+
if (!base.prIdentity)
|
|
441
|
+
return result('no-op', base.resolved.taskId, base.issueNumber, null, {
|
|
442
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
443
|
+
error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
|
|
444
|
+
});
|
|
445
|
+
return inspectExternalPullRequest(base, base.prIdentity);
|
|
446
|
+
}
|
|
447
|
+
async function inspectPlatformPullRequestByNumber(prNumber, options = {}) {
|
|
448
|
+
const loaded = await resolvePlatformProviderContext({ cwd: options.cwd || process.cwd(), client: options.client });
|
|
449
|
+
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
450
|
+
const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
|
|
451
|
+
if (!usable) {
|
|
452
|
+
return result(context.status, null, null, null, {
|
|
453
|
+
platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
let identity;
|
|
416
457
|
try {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}).trim();
|
|
424
|
-
const repository = parseGitHubRemote(url);
|
|
425
|
-
return repository ? [{ name, url, repository }] : [];
|
|
426
|
-
}
|
|
427
|
-
catch {
|
|
428
|
-
return [];
|
|
429
|
-
}
|
|
458
|
+
identity = loaded.ok ? providerResourceToken(loaded.value.provider, 'pull-request', String(prNumber)) : resourceIdentity(prNumber);
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
return result('failed', null, null, null, {
|
|
462
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
463
|
+
error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
430
464
|
});
|
|
431
465
|
}
|
|
432
|
-
|
|
433
|
-
|
|
466
|
+
if (loaded.ok) {
|
|
467
|
+
const inspected = loaded.value.provider.changeRequests?.inspect
|
|
468
|
+
? await loaded.value.provider.changeRequests.inspect({ context: providerOperationContext(loaded.value), target: identity })
|
|
469
|
+
: unsupportedProviderOperation(loaded.value.provider, 'changeRequests.inspect');
|
|
470
|
+
const number = resourceIdentityNumber(identity);
|
|
471
|
+
if (!inspected.ok)
|
|
472
|
+
return result(providerStatus(inspected.error), null, null, number, {
|
|
473
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
474
|
+
resource: { kind: 'pull-request', number, identity }, error: providerError(inspected.error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
|
|
475
|
+
});
|
|
476
|
+
return result('no-op', null, null, number, {
|
|
477
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
478
|
+
resource: { kind: 'pull-request', number, identity },
|
|
479
|
+
pullRequest: normalizeProviderPullRequest(inspected.value, context.platform.repository, number || 0), error: null
|
|
480
|
+
});
|
|
434
481
|
}
|
|
482
|
+
return result('failed', null, null, null, {
|
|
483
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
484
|
+
resource: { kind: 'pull-request', number: null },
|
|
485
|
+
error: { code: 'PLATFORM_PROVIDER_LOAD_FAILED', message: 'Selected provider failed to load', retryable: false }
|
|
486
|
+
});
|
|
435
487
|
}
|
|
436
|
-
function
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
pullRequest.number <= 0) {
|
|
488
|
+
function readInLabelMapping(repoRoot) {
|
|
489
|
+
try {
|
|
490
|
+
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
|
|
491
|
+
const mapping = validateInLabelMapping(config.labels?.in);
|
|
492
|
+
return mapping.ok ? { ok: true, value: mapping.value } : mapping;
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
444
495
|
return {
|
|
445
496
|
ok: false,
|
|
446
|
-
error: {
|
|
447
|
-
code: 'PR_MERGE_EVIDENCE_SOURCE_UNAVAILABLE',
|
|
448
|
-
message: 'Pull request repository identity is inconsistent',
|
|
449
|
-
retryable: false
|
|
450
|
-
}
|
|
497
|
+
error: { code: 'IN_LABEL_SYNC_CONFIG_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
451
498
|
};
|
|
452
499
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
execFileSync('git', ['check-ref-format', ref], {
|
|
458
|
-
cwd, encoding: 'utf8', stdio: ['ignore', 'ignore', 'ignore']
|
|
459
|
-
});
|
|
460
|
-
return true;
|
|
461
|
-
}
|
|
462
|
-
catch {
|
|
463
|
-
return false;
|
|
464
|
-
}
|
|
465
|
-
};
|
|
466
|
-
if (!refValid(reviewedHeadRef) || !refValid(targetHeadRef)) {
|
|
500
|
+
}
|
|
501
|
+
function taskInLabelTarget(repoRoot, frontmatter, currentLabels, repositoryLabels) {
|
|
502
|
+
const baseRef = typeof frontmatter.delivery_base_ref === 'string' ? frontmatter.delivery_base_ref.trim() : '';
|
|
503
|
+
if (!baseRef)
|
|
467
504
|
return {
|
|
468
505
|
ok: false,
|
|
469
|
-
error: {
|
|
470
|
-
code: 'PR_MERGE_EVIDENCE_SOURCE_UNAVAILABLE',
|
|
471
|
-
message: 'Pull request Git refs are invalid',
|
|
472
|
-
retryable: false
|
|
473
|
-
}
|
|
506
|
+
error: { code: 'IN_LABEL_SYNC_BASE_MISSING', message: 'Task has no delivery_base_ref for in-label evidence', retryable: false }
|
|
474
507
|
};
|
|
508
|
+
let changedFiles;
|
|
509
|
+
try {
|
|
510
|
+
changedFiles = execFileSync('git', ['diff', `${baseRef}...HEAD`, '--name-only'], {
|
|
511
|
+
cwd: repoRoot, encoding: 'utf8'
|
|
512
|
+
}).trim().split(/\r?\n/).filter(Boolean);
|
|
475
513
|
}
|
|
476
|
-
|
|
477
|
-
const exact = remotes
|
|
478
|
-
.filter((remote) => remote.repository === repository)
|
|
479
|
-
.sort((left, right) => Number(right.name === 'origin') - Number(left.name === 'origin') ||
|
|
480
|
-
left.name.localeCompare(right.name))[0];
|
|
481
|
-
const origin = remotes.find((remote) => remote.name === 'origin');
|
|
482
|
-
const remoteUrl = exact?.url || (origin ? rewriteGitHubRemote(origin.url, repository) : null);
|
|
483
|
-
if (!remoteUrl) {
|
|
514
|
+
catch (error) {
|
|
484
515
|
return {
|
|
485
516
|
ok: false,
|
|
486
|
-
error: {
|
|
487
|
-
code: 'PR_MERGE_EVIDENCE_SOURCE_UNAVAILABLE',
|
|
488
|
-
message: `No GitHub remote can provide evidence for ${repository}`,
|
|
489
|
-
retryable: false
|
|
490
|
-
}
|
|
517
|
+
error: { code: 'IN_LABEL_SYNC_EVIDENCE_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
491
518
|
};
|
|
492
519
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
};
|
|
520
|
+
const mapping = readInLabelMapping(repoRoot);
|
|
521
|
+
if (!mapping.ok)
|
|
522
|
+
return mapping;
|
|
523
|
+
const planned = planInLabelUpdate({ changedFiles, currentLabels, mapping: mapping.value, repositoryLabels });
|
|
524
|
+
if (planned.error)
|
|
525
|
+
return { ok: false, error: planned.error };
|
|
526
|
+
return { ok: true, value: planned.target };
|
|
497
527
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
},
|
|
502
|
-
inspectIssueClosingChangeRequests({ client, repository, issueNumber, cwd }) {
|
|
503
|
-
return inspectGitHubIssueClosingChangeRequests(client || createGitHubClient(), repository, issueNumber, cwd);
|
|
504
|
-
},
|
|
505
|
-
resolveChangeRequestGitEvidence(context) {
|
|
506
|
-
return resolveGitHubChangeRequestGitEvidence(context);
|
|
507
|
-
}
|
|
508
|
-
});
|
|
509
|
-
function locatePullRequest(base, head, target, expectedSha) {
|
|
510
|
-
const repository = base.context.platform.repository;
|
|
511
|
-
const listed = base.client.json([
|
|
512
|
-
'api', `repos/${repository}/pulls?state=open&base=${encodeURIComponent(target)}&per_page=100`
|
|
513
|
-
], { cwd: base.resolved.repoRoot });
|
|
514
|
-
if (!listed.ok)
|
|
515
|
-
return listed;
|
|
516
|
-
const selected = selectPullRequest(listed.value, repository, head, target, expectedSha);
|
|
517
|
-
return selected.status === 'resolved'
|
|
518
|
-
? { ok: true, value: selected.pullRequest }
|
|
519
|
-
: { ok: false, error: {
|
|
520
|
-
code: selected.status === 'ambiguous' ? 'PR_IDENTITY_AMBIGUOUS'
|
|
521
|
-
: selected.status === 'head-mismatch' ? 'PR_HEAD_SHA_MISMATCH' : 'PR_NOT_FOUND',
|
|
522
|
-
message: selected.status === 'ambiguous' ? 'Multiple pull requests match the exact head/base identity'
|
|
523
|
-
: selected.status === 'head-mismatch' ? 'A pull request matches head/base but not the expected head SHA'
|
|
524
|
-
: 'No pull request matches the exact head/base identity',
|
|
525
|
-
retryable: false
|
|
526
|
-
} };
|
|
528
|
+
function resourceIdentityLabel(identity) {
|
|
529
|
+
const number = resourceIdentityNumber(identity);
|
|
530
|
+
return number === null ? `${identity.kind} ${identity.value}` : `#${number}`;
|
|
527
531
|
}
|
|
528
|
-
function
|
|
529
|
-
|
|
530
|
-
if (!base.ok)
|
|
531
|
-
return base.output;
|
|
532
|
-
if (!base.fact)
|
|
533
|
-
return result('no-op', base.resolved.taskId, base.issueNumber, null, {
|
|
534
|
-
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
535
|
-
});
|
|
536
|
-
if (!base.prNumber)
|
|
537
|
-
return result('no-op', base.resolved.taskId, base.issueNumber, null, {
|
|
538
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
539
|
-
error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
|
|
540
|
-
});
|
|
541
|
-
const fetched = inspectPlatformChangeRequest(base.context.platform.type, {
|
|
542
|
-
cwd: base.resolved.repoRoot,
|
|
543
|
-
repository: base.context.platform.repository,
|
|
544
|
-
number: base.prNumber,
|
|
545
|
-
client: options.client || base.client
|
|
546
|
-
});
|
|
547
|
-
if (!fetched.ok || !fetched.value) {
|
|
548
|
-
const error = fetched.error || {
|
|
549
|
-
code: 'PR_INSPECTION_INVALID',
|
|
550
|
-
message: 'Platform adapter returned no change-request snapshot',
|
|
551
|
-
retryable: false
|
|
552
|
-
};
|
|
553
|
-
return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
554
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
555
|
-
resource: { kind: 'pull-request', number: base.prNumber }, error
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
return result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
559
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
560
|
-
resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: fetched.value, error: null
|
|
561
|
-
});
|
|
532
|
+
function inLabelResource(kind, identity, before, expected, after, effect) {
|
|
533
|
+
return { kind, number: resourceIdentityNumber(identity), identity, before: [...before].sort(), expected: [...expected].sort(), after: after ? [...after].sort() : null, effect };
|
|
562
534
|
}
|
|
563
|
-
function
|
|
564
|
-
const
|
|
565
|
-
const
|
|
535
|
+
async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
|
|
536
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
537
|
+
const loaded = await resolvePlatformProviderContext({ cwd, client: options.client });
|
|
538
|
+
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
566
539
|
const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
|
|
567
|
-
if (!usable)
|
|
568
|
-
return result(context.status, null, null,
|
|
540
|
+
if (!loaded.ok || !usable)
|
|
541
|
+
return result(context.status, null, null, prNumber, {
|
|
569
542
|
platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
|
|
570
543
|
});
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
return result('failed', null, null, null, {
|
|
544
|
+
if (!Number.isSafeInteger(prNumber) || prNumber <= 0)
|
|
545
|
+
return result('failed', null, null, prNumber, {
|
|
574
546
|
platform: context.platform, capabilities: context.capabilities,
|
|
575
547
|
error: { code: 'PR_NUMBER_INVALID', message: 'PR number must be positive', retryable: false }
|
|
576
548
|
});
|
|
549
|
+
const provider = loaded.value.provider;
|
|
550
|
+
const operationContext = providerOperationContext(loaded.value);
|
|
551
|
+
const repository = context.platform.repository;
|
|
552
|
+
const pullRequestIdentity = providerResourceToken(provider, 'pull-request', String(prNumber));
|
|
553
|
+
const providerFailure = (error) => result(error.code === 'PLATFORM_CAPABILITY_UNSUPPORTED' ? 'degraded' : providerStatus(error), null, null, prNumber, {
|
|
554
|
+
platform: context.platform,
|
|
555
|
+
capabilities: context.capabilities,
|
|
556
|
+
resource: { kind: 'pull-request', number: prNumber, identity: pullRequestIdentity },
|
|
557
|
+
error: providerError(error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
|
|
558
|
+
});
|
|
559
|
+
const inspected = provider.changeRequests?.inspect
|
|
560
|
+
? await provider.changeRequests.inspect({ context: operationContext, target: pullRequestIdentity })
|
|
561
|
+
: unsupportedProviderOperation(provider, 'changeRequests.inspect');
|
|
562
|
+
if (!inspected.ok)
|
|
563
|
+
return providerFailure(inspected.error);
|
|
564
|
+
const fetched = normalizeProviderPullRequest(inspected.value, repository, prNumber);
|
|
565
|
+
const files = provider.changeRequests?.listFiles
|
|
566
|
+
? await provider.changeRequests.listFiles({ context: operationContext, target: pullRequestIdentity })
|
|
567
|
+
: unsupportedProviderOperation(provider, 'changeRequests.listFiles');
|
|
568
|
+
if (!files.ok)
|
|
569
|
+
return providerFailure(files.error);
|
|
570
|
+
const mapping = readInLabelMapping(cwd);
|
|
571
|
+
if (!mapping.ok)
|
|
572
|
+
return result('failed', null, null, prNumber, {
|
|
573
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched,
|
|
574
|
+
resource: { kind: 'pull-request', number: prNumber, identity: pullRequestIdentity }, error: mapping.error
|
|
575
|
+
});
|
|
576
|
+
let repositoryLabels = new Set();
|
|
577
|
+
if (Object.keys(mapping.value).length > 0) {
|
|
578
|
+
const labels = provider.issues?.listLabels
|
|
579
|
+
? await provider.issues.listLabels({ context: operationContext })
|
|
580
|
+
: unsupportedProviderOperation(provider, 'issues.listLabels');
|
|
581
|
+
if (!labels.ok)
|
|
582
|
+
return providerFailure(labels.error);
|
|
583
|
+
repositoryLabels = new Set(labels.value);
|
|
577
584
|
}
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
number: prNumber,
|
|
582
|
-
client
|
|
585
|
+
const plannedTarget = planInLabelUpdate({
|
|
586
|
+
changedFiles: files.value, currentLabels: fetched.labels,
|
|
587
|
+
mapping: mapping.value, repositoryLabels
|
|
583
588
|
});
|
|
584
|
-
if (
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
retryable: false
|
|
589
|
-
};
|
|
590
|
-
return result(error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
|
|
591
|
-
platform: context.platform, capabilities: context.capabilities,
|
|
592
|
-
resource: { kind: 'pull-request', number: prNumber }, error
|
|
589
|
+
if (plannedTarget.error)
|
|
590
|
+
return result('failed', null, null, prNumber, {
|
|
591
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched,
|
|
592
|
+
resource: { kind: 'pull-request', number: prNumber, identity: pullRequestIdentity }, error: plannedTarget.error
|
|
593
593
|
});
|
|
594
|
+
const association = provider.changeRequests?.listClosingIssues
|
|
595
|
+
? await provider.changeRequests.listClosingIssues({ context: operationContext, target: pullRequestIdentity })
|
|
596
|
+
: unsupportedProviderOperation(provider, 'changeRequests.listClosingIssues');
|
|
597
|
+
if (!association.ok)
|
|
598
|
+
return providerFailure(association.error);
|
|
599
|
+
const issueIdentity = association.value.length === 1 ? association.value[0] : null;
|
|
600
|
+
const issueNumber = issueIdentity ? resourceIdentityNumber(issueIdentity) : null;
|
|
601
|
+
const closingIssueIdentities = association.value;
|
|
602
|
+
let issue = null;
|
|
603
|
+
if (issueIdentity) {
|
|
604
|
+
const fetchedIssue = provider.issues?.inspect
|
|
605
|
+
? await provider.issues.inspect({ context: operationContext, target: issueIdentity })
|
|
606
|
+
: unsupportedProviderOperation(provider, 'issues.inspect');
|
|
607
|
+
if (!fetchedIssue.ok)
|
|
608
|
+
return providerFailure(fetchedIssue.error);
|
|
609
|
+
issue = fetchedIssue.value;
|
|
594
610
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
611
|
+
const resources = issue
|
|
612
|
+
? [
|
|
613
|
+
{ kind: 'issue', identity: issueIdentity, number: issueNumber, snapshot: issue },
|
|
614
|
+
{ kind: 'pull-request', identity: pullRequestIdentity, number: prNumber, snapshot: fetched }
|
|
615
|
+
]
|
|
616
|
+
: [{ kind: 'pull-request', identity: pullRequestIdentity, number: prNumber, snapshot: fetched }];
|
|
617
|
+
const plans = resources.map((resource) => ({
|
|
618
|
+
...resource,
|
|
619
|
+
plan: planInLabelUpdate({
|
|
620
|
+
changedFiles: files.value, currentLabels: resource.snapshot.labels,
|
|
621
|
+
mapping: mapping.value, repositoryLabels
|
|
622
|
+
})
|
|
623
|
+
}));
|
|
624
|
+
const operations = [
|
|
625
|
+
{ name: 'closing-issues', status: 'no-op', reasonCode: association.value.length === 1 ? null : association.value.length === 0 ? 'ISSUE_ASSOCIATION_NONE' : 'ISSUE_ASSOCIATION_AMBIGUOUS' },
|
|
626
|
+
...plans.map((item) => ({
|
|
627
|
+
name: `labels:in:${item.kind}`,
|
|
628
|
+
status: !context.capabilities.triage ? 'skipped' : item.plan.changed ? 'planned' : 'no-op',
|
|
629
|
+
reasonCode: !context.capabilities.triage ? 'TRIAGE_REQUIRED' : null
|
|
630
|
+
}))
|
|
631
|
+
];
|
|
632
|
+
const willWrite = context.capabilities.triage && !options.dryRun;
|
|
633
|
+
const resourcesState = plans.map((item) => inLabelResource(item.kind, item.identity, item.snapshot.labels, item.plan.target, item.plan.changed && willWrite ? null : item.snapshot.labels, item.plan.changed && willWrite ? 'unknown' : 'no-op'));
|
|
634
|
+
if (options.dryRun || !context.capabilities.triage)
|
|
635
|
+
return result(!context.capabilities.triage || association.value.length !== 1 ? 'degraded' : plans.some((item) => item.plan.changed) ? 'planned' : 'no-op', null, issueNumber, prNumber, {
|
|
636
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched,
|
|
637
|
+
resource: { kind: 'pull-request', number: prNumber, identity: pullRequestIdentity }, operations, resources: resourcesState,
|
|
638
|
+
evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: closingIssueIdentities }, error: null
|
|
639
|
+
});
|
|
640
|
+
let currentPullRequest = fetched;
|
|
641
|
+
let successfulWrites = 0;
|
|
642
|
+
for (const item of plans) {
|
|
643
|
+
if (!item.plan.changed)
|
|
644
|
+
continue;
|
|
645
|
+
const targetIdentity = item.kind === 'issue' ? issueIdentity : pullRequestIdentity;
|
|
646
|
+
const synced = item.kind === 'issue'
|
|
647
|
+
? provider.issues?.update
|
|
648
|
+
? await provider.issues.update({
|
|
649
|
+
context: operationContext,
|
|
650
|
+
target: targetIdentity,
|
|
651
|
+
currentLabels: item.snapshot.labels,
|
|
652
|
+
patch: { labels: item.plan.labels },
|
|
653
|
+
mutation: { idempotencyKey: `issue:update:${serializeResourceIdentity(targetIdentity)}:in-labels`, target: targetIdentity }
|
|
654
|
+
})
|
|
655
|
+
: unsupportedProviderOperation(provider, 'issues.update')
|
|
656
|
+
: provider.changeRequests?.update
|
|
657
|
+
? await provider.changeRequests.update({
|
|
658
|
+
context: operationContext,
|
|
659
|
+
target: targetIdentity,
|
|
660
|
+
currentLabels: item.snapshot.labels,
|
|
661
|
+
patch: { labels: item.plan.labels },
|
|
662
|
+
mutation: { idempotencyKey: `pull-request:update:${serializeResourceIdentity(targetIdentity)}:in-labels`, target: targetIdentity }
|
|
663
|
+
})
|
|
664
|
+
: unsupportedProviderOperation(provider, 'changeRequests.update');
|
|
665
|
+
if (!synced.ok) {
|
|
666
|
+
const state = resourcesState.find((resource) => resource.kind === item.kind && resourceIdentityEquals(resource.identity, item.identity));
|
|
667
|
+
state.effect = synced.error.retryable ? 'unknown' : 'no-op';
|
|
668
|
+
state.after = state.effect === 'unknown' ? null : item.snapshot.labels;
|
|
669
|
+
const partial = successfulWrites > 0 || synced.error.retryable;
|
|
670
|
+
const error = partial
|
|
671
|
+
? { ...synced.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${synced.error.message}` }
|
|
672
|
+
: synced.error;
|
|
673
|
+
return result(partial ? 'blocked' : providerStatus(synced.error), null, issueNumber, prNumber, {
|
|
674
|
+
changed: successfulWrites > 0, platform: context.platform, capabilities: context.capabilities,
|
|
675
|
+
pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber, identity: pullRequestIdentity }, operations, resources: resourcesState,
|
|
676
|
+
evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: closingIssueIdentities }, error: providerError(error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
successfulWrites += synced.value.changed ? 1 : 0;
|
|
680
|
+
const reread = item.kind === 'issue'
|
|
681
|
+
? provider.issues?.inspect
|
|
682
|
+
? await provider.issues.inspect({ context: operationContext, target: targetIdentity })
|
|
683
|
+
: unsupportedProviderOperation(provider, 'issues.inspect')
|
|
684
|
+
: provider.changeRequests?.inspect
|
|
685
|
+
? await provider.changeRequests.inspect({ context: operationContext, target: targetIdentity })
|
|
686
|
+
: unsupportedProviderOperation(provider, 'changeRequests.inspect');
|
|
687
|
+
if (!reread.ok)
|
|
688
|
+
return result('blocked', null, issueNumber, prNumber, {
|
|
689
|
+
changed: true, platform: context.platform, capabilities: context.capabilities,
|
|
690
|
+
pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber, identity: pullRequestIdentity }, operations, resources: resourcesState,
|
|
691
|
+
evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: closingIssueIdentities },
|
|
692
|
+
error: { ...providerError(reread.error, 'PLATFORM_PROVIDER_OPERATION_FAILED'), code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${reread.error.message}` }
|
|
693
|
+
});
|
|
694
|
+
const after = reread.value.labels || [];
|
|
695
|
+
const state = resourcesState.find((resource) => resource.kind === item.kind && resourceIdentityEquals(resource.identity, item.identity));
|
|
696
|
+
state.after = after;
|
|
697
|
+
state.effect = after.filter((label) => label.startsWith('in:')).sort().join('\0') === item.plan.target.join('\0') ? 'applied' : 'unknown';
|
|
698
|
+
if (state.effect === 'unknown')
|
|
699
|
+
return result('blocked', null, issueNumber, prNumber, {
|
|
700
|
+
changed: true, platform: context.platform, capabilities: context.capabilities,
|
|
701
|
+
pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber, identity: pullRequestIdentity }, operations, resources: resourcesState,
|
|
702
|
+
evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: closingIssueIdentities },
|
|
703
|
+
error: { code: 'IN_LABEL_SYNC_PARTIAL', message: `${item.kind} ${resourceIdentityLabel(item.identity)} did not converge to the expected in: labels`, retryable: true }
|
|
704
|
+
});
|
|
705
|
+
if (item.kind === 'pull-request')
|
|
706
|
+
currentPullRequest = normalizeProviderPullRequest(reread.value, repository, prNumber, currentPullRequest);
|
|
707
|
+
}
|
|
708
|
+
return result(association.value.length !== 1 ? 'degraded' : successfulWrites > 0 ? 'applied' : 'no-op', null, issueNumber, prNumber, {
|
|
709
|
+
changed: successfulWrites > 0, platform: context.platform, capabilities: context.capabilities,
|
|
710
|
+
pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber, identity: pullRequestIdentity },
|
|
711
|
+
operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation),
|
|
712
|
+
resources: resourcesState, evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: closingIssueIdentities }, error: null
|
|
598
713
|
});
|
|
599
714
|
}
|
|
600
715
|
function appendActivity(content, line) {
|
|
@@ -603,9 +718,8 @@ function appendActivity(content, line) {
|
|
|
603
718
|
}
|
|
604
719
|
function deliveryIdentity(pullRequest) {
|
|
605
720
|
return {
|
|
721
|
+
resource: pullRequestIdentity(pullRequest),
|
|
606
722
|
repository: pullRequest.repository,
|
|
607
|
-
number: pullRequest.number,
|
|
608
|
-
nodeId: pullRequest.nodeId,
|
|
609
723
|
url: pullRequest.url,
|
|
610
724
|
head: { ...pullRequest.head },
|
|
611
725
|
base: { ...pullRequest.base }
|
|
@@ -614,11 +728,11 @@ function deliveryIdentity(pullRequest) {
|
|
|
614
728
|
function factTimestamp(timestamp) {
|
|
615
729
|
return new Date(timestamp.replace(' ', 'T')).toISOString();
|
|
616
730
|
}
|
|
617
|
-
function boundFactFor(pullRequest, source,
|
|
731
|
+
function boundFactFor(pullRequest, source, issue, verifiedAt) {
|
|
618
732
|
return buildBoundFact({
|
|
619
733
|
identity: deliveryIdentity(pullRequest),
|
|
620
734
|
source,
|
|
621
|
-
issueNumber,
|
|
735
|
+
...(typeof issue === 'object' ? { issueIdentity: issue } : { issueNumber: issue }),
|
|
622
736
|
verifiedAt: factTimestamp(verifiedAt),
|
|
623
737
|
remoteState: pullRequest.state,
|
|
624
738
|
mergedAt: pullRequest.state === 'closed' ? pullRequest.mergedAt : null,
|
|
@@ -645,11 +759,11 @@ function bindIdentity(base, pullRequest, agent, source, dryRun = false, expected
|
|
|
645
759
|
});
|
|
646
760
|
if (!identity.ok)
|
|
647
761
|
return { status: 'failed', error: identity.error };
|
|
648
|
-
if (base.
|
|
762
|
+
if (base.prIdentity && !resourceIdentityEquals(base.prIdentity, pullRequestIdentity(pullRequest)))
|
|
649
763
|
return { status: 'failed', error: {
|
|
650
|
-
code: 'PR_BIND_CONFLICT', message:
|
|
764
|
+
code: 'PR_BIND_CONFLICT', message: 'Task is already bound to a different pull request identity'
|
|
651
765
|
} };
|
|
652
|
-
if (base.
|
|
766
|
+
if (base.prIdentity && resourceIdentityEquals(base.prIdentity, pullRequestIdentity(pullRequest)) && base.fact?.state === 'bound') {
|
|
653
767
|
if (JSON.stringify(base.fact.identity) !== JSON.stringify(deliveryIdentity(pullRequest)))
|
|
654
768
|
return { status: 'failed', error: {
|
|
655
769
|
code: 'PR_BIND_IDENTITY_MISMATCH', message: `Task binding for PR #${pullRequest.number} does not match the fetched pull request identity`
|
|
@@ -658,7 +772,7 @@ function bindIdentity(base, pullRequest, agent, source, dryRun = false, expected
|
|
|
658
772
|
}
|
|
659
773
|
const current = fs.readFileSync(base.resolved.taskMdPath, 'utf8');
|
|
660
774
|
const metadata = captureTaskWriteMetadata();
|
|
661
|
-
const fact = boundFactFor(pullRequest, source, base.
|
|
775
|
+
const fact = boundFactFor(pullRequest, source, taskIssueIdentity(base.frontmatter), metadata.timestamp);
|
|
662
776
|
const factMutation = factFrontmatterMutation(fact);
|
|
663
777
|
return writeTask({
|
|
664
778
|
taskRef: base.resolved.taskId,
|
|
@@ -670,64 +784,60 @@ function bindIdentity(base, pullRequest, agent, source, dryRun = false, expected
|
|
|
670
784
|
]
|
|
671
785
|
}, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
|
|
672
786
|
}
|
|
673
|
-
function bindPlatformPullRequest(taskRef, options) {
|
|
674
|
-
const base = resolvedContext(taskRef, options);
|
|
787
|
+
async function bindPlatformPullRequest(taskRef, options) {
|
|
788
|
+
const base = await resolvedContext(taskRef, options);
|
|
675
789
|
if (!base.ok)
|
|
676
790
|
return base.output;
|
|
677
791
|
if (!base.fact)
|
|
678
792
|
return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
679
793
|
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
680
794
|
});
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
795
|
+
let prIdentity;
|
|
796
|
+
try {
|
|
797
|
+
prIdentity = providerResourceToken(base.provider, 'pull-request', String(options.pr));
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
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 } });
|
|
801
|
+
}
|
|
802
|
+
const requestedNumber = resourceIdentityNumber(prIdentity);
|
|
803
|
+
if (base.prIdentity && !resourceIdentityEquals(base.prIdentity, prIdentity))
|
|
686
804
|
return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
687
|
-
error: { code: 'PR_BIND_CONFLICT', message:
|
|
805
|
+
error: { code: 'PR_BIND_CONFLICT', message: 'Task is already bound to a different pull request identity', retryable: false }
|
|
688
806
|
});
|
|
689
|
-
const
|
|
690
|
-
if (!
|
|
691
|
-
return
|
|
692
|
-
const initialIdentity = validateWriterIdentity(base,
|
|
807
|
+
const inspected = await inspectExternalPullRequest(base, prIdentity);
|
|
808
|
+
if (inspected.status === 'failed' || inspected.status === 'blocked' || !inspected.pullRequest)
|
|
809
|
+
return inspected;
|
|
810
|
+
const initialIdentity = validateWriterIdentity(base, inspected.pullRequest, {
|
|
693
811
|
expectedHead: String(base.frontmatter.branch || ''),
|
|
694
812
|
errorCode: 'PR_BIND_IDENTITY_MISMATCH'
|
|
695
813
|
});
|
|
696
814
|
if (!initialIdentity.ok)
|
|
697
|
-
return result('failed', base.resolved.taskId, base.issueNumber,
|
|
698
|
-
pullRequest:
|
|
699
|
-
});
|
|
700
|
-
const rechecked = inspectGitHubPullRequest(base.client, base.context.platform.repository, options.pr, base.resolved.repoRoot);
|
|
701
|
-
if (!rechecked.ok || !rechecked.value)
|
|
702
|
-
return result(rechecked.ok ? 'failed' : rechecked.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
703
|
-
pullRequest: fetched.value,
|
|
704
|
-
error: rechecked.ok ? { code: 'PR_BIND_RECHECK_FAILED', message: 'Pull request recheck returned no identity', retryable: false } : rechecked.error
|
|
705
|
-
});
|
|
706
|
-
if (!sameDeliveryIdentity(fetched.value, rechecked.value))
|
|
707
|
-
return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
708
|
-
pullRequest: rechecked.value,
|
|
709
|
-
error: { code: 'PR_BIND_IDENTITY_MISMATCH', message: 'Pull request identity changed before task binding', retryable: false }
|
|
815
|
+
return result('failed', base.resolved.taskId, base.issueNumber, requestedNumber, {
|
|
816
|
+
pullRequest: inspected.pullRequest, error: initialIdentity.error
|
|
710
817
|
});
|
|
711
|
-
const written = bindIdentity(base,
|
|
818
|
+
const written = bindIdentity(base, inspected.pullRequest, options.agent, 'explicit-bind', options.dryRun);
|
|
712
819
|
if (written.status === 'failed')
|
|
713
|
-
return result('failed', base.resolved.taskId, base.issueNumber,
|
|
714
|
-
pullRequest:
|
|
820
|
+
return result('failed', base.resolved.taskId, base.issueNumber, requestedNumber, {
|
|
821
|
+
pullRequest: inspected.pullRequest, error: { code: written.error.code, message: written.error.message, retryable: false }
|
|
715
822
|
});
|
|
716
|
-
return result(options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber :
|
|
823
|
+
return result(options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : inspected.pullRequest.number, {
|
|
717
824
|
changed: !options.dryRun && written.status === 'applied', platform: base.context.platform, capabilities: base.context.capabilities,
|
|
718
|
-
resource: { kind: 'pull-request', number:
|
|
825
|
+
resource: { kind: 'pull-request', number: inspected.pullRequest.number, identity: pullRequestIdentity(inspected.pullRequest) }, pullRequest: inspected.pullRequest,
|
|
719
826
|
operations: [{ name: 'task:bind-pr', status: options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', reasonCode: null }], error: null
|
|
720
827
|
});
|
|
721
828
|
}
|
|
722
|
-
function externalEvidenceNote(
|
|
829
|
+
function externalEvidenceNote(issueIdentity, source, pullRequest) {
|
|
723
830
|
return [
|
|
724
831
|
`authorization=${source}`,
|
|
725
|
-
externalIdentityEvidenceNote(
|
|
832
|
+
externalIdentityEvidenceNote(issueIdentity, pullRequest)
|
|
726
833
|
].join('; ');
|
|
727
834
|
}
|
|
728
|
-
function externalIdentityEvidenceNote(
|
|
835
|
+
function externalIdentityEvidenceNote(issueIdentity, pullRequest) {
|
|
836
|
+
const issueLabel = issueIdentity
|
|
837
|
+
? issueIdentity.kind === 'number' ? `#${issueIdentity.value}` : `${issueIdentity.kind}:${issueIdentity.value}`
|
|
838
|
+
: 'unknown';
|
|
729
839
|
return [
|
|
730
|
-
`issue
|
|
840
|
+
`issue=${issueLabel}`,
|
|
731
841
|
`pr=#${pullRequest.number}`,
|
|
732
842
|
`url=${pullRequest.url}`,
|
|
733
843
|
`base=${pullRequest.base.repository}:${pullRequest.base.ref}@${pullRequest.base.sha}`,
|
|
@@ -736,8 +846,8 @@ function externalIdentityEvidenceNote(issueNumber, pullRequest) {
|
|
|
736
846
|
`mergeCommitSha=${pullRequest.mergeCommitSha}`
|
|
737
847
|
].join('; ');
|
|
738
848
|
}
|
|
739
|
-
function resolveExternalPullRequest(taskRef, options) {
|
|
740
|
-
const base = resolvedContext(taskRef, options);
|
|
849
|
+
async function resolveExternalPullRequest(taskRef, options) {
|
|
850
|
+
const base = await resolvedContext(taskRef, options);
|
|
741
851
|
if (!base.ok)
|
|
742
852
|
return externalResult(base.output);
|
|
743
853
|
if (!base.fact)
|
|
@@ -765,25 +875,40 @@ function resolveExternalPullRequest(taskRef, options) {
|
|
|
765
875
|
error: null
|
|
766
876
|
}), { mode: 'normal' });
|
|
767
877
|
}
|
|
768
|
-
if (!base.
|
|
878
|
+
if (!base.issueIdentity)
|
|
769
879
|
return externalResult(result('failed', base.resolved.taskId, null, base.prNumber, {
|
|
770
880
|
platform: base.context.platform,
|
|
771
881
|
capabilities: base.context.capabilities,
|
|
772
882
|
error: { code: 'EXTERNAL_DELIVERY_ISSUE_REQUIRED', message: 'External delivery requires a valid issue_number', retryable: false }
|
|
773
883
|
}));
|
|
774
|
-
const inspected =
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
884
|
+
const inspected = base.provider.changeRequests?.listClosing
|
|
885
|
+
? await base.provider.changeRequests.listClosing({
|
|
886
|
+
context: providerOperationContext(base.loadedContext),
|
|
887
|
+
issue: base.issueIdentity
|
|
888
|
+
})
|
|
889
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.listClosing');
|
|
780
890
|
if (!inspected.ok || !inspected.value) {
|
|
781
|
-
const error =
|
|
891
|
+
const error = 'error' in inspected && inspected.error
|
|
892
|
+
? inspected.error
|
|
893
|
+
: { code: 'PR_IDENTITY_INVALID', message: 'Closing PR inspection returned no candidates', retryable: false };
|
|
782
894
|
return externalResult(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
783
895
|
platform: base.context.platform, capabilities: base.context.capabilities, error
|
|
784
896
|
}));
|
|
785
897
|
}
|
|
786
|
-
const
|
|
898
|
+
const candidates = inspected.value.map((item) => normalizeProviderPullRequest(item, base.context.platform.repository, item.number || 0));
|
|
899
|
+
let explicitPrIdentity = null;
|
|
900
|
+
if (options.pr !== undefined) {
|
|
901
|
+
try {
|
|
902
|
+
explicitPrIdentity = providerResourceToken(base.provider, 'pull-request', String(options.pr));
|
|
903
|
+
}
|
|
904
|
+
catch (error) {
|
|
905
|
+
return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
906
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
907
|
+
error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
908
|
+
}));
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
const selected = selectExternalPullRequest(candidates, base.context.platform.repository, base.prIdentity, explicitPrIdentity);
|
|
787
912
|
if (selected.status === 'normal')
|
|
788
913
|
return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
789
914
|
platform: base.context.platform,
|
|
@@ -803,7 +928,14 @@ function resolveExternalPullRequest(taskRef, options) {
|
|
|
803
928
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
804
929
|
pullRequest: selected.selected, error: initialIdentity.error
|
|
805
930
|
}), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
|
|
806
|
-
const rechecked =
|
|
931
|
+
const rechecked = base.provider.changeRequests?.inspect
|
|
932
|
+
? await base.provider.changeRequests.inspect({
|
|
933
|
+
context: providerOperationContext(base.loadedContext),
|
|
934
|
+
target: pullRequestIdentity(selected.selected)
|
|
935
|
+
}).then((response) => response.ok
|
|
936
|
+
? { ok: true, value: normalizeProviderPullRequest(response.value, base.context.platform.repository, selected.selected.number, selected.selected) }
|
|
937
|
+
: response)
|
|
938
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
|
|
807
939
|
if (!rechecked.ok || !rechecked.value)
|
|
808
940
|
return externalResult(result(rechecked.ok ? 'failed' : rechecked.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
809
941
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
@@ -829,8 +961,8 @@ function resolveExternalPullRequest(taskRef, options) {
|
|
|
829
961
|
pullRequest: rechecked.value, error: finalEvidence.error
|
|
830
962
|
}), { candidates: selected.candidates, eligible: selected.eligible, selected: rechecked.value });
|
|
831
963
|
const selectedPullRequest = rechecked.value;
|
|
832
|
-
const note = externalEvidenceNote(base.
|
|
833
|
-
const identityNote = externalIdentityEvidenceNote(base.
|
|
964
|
+
const note = externalEvidenceNote(base.issueIdentity, selected.source, selectedPullRequest);
|
|
965
|
+
const identityNote = externalIdentityEvidenceNote(base.issueIdentity, selectedPullRequest);
|
|
834
966
|
const activityEntry = `**Bind External PR** by ${options.agent} — ${note}`;
|
|
835
967
|
const auditLines = base.content.split(/\r?\n/).filter((line) => line.includes('**Bind External PR**'));
|
|
836
968
|
const alreadyAudited = auditLines.some((line) => line.endsWith(identityNote));
|
|
@@ -841,14 +973,14 @@ function resolveExternalPullRequest(taskRef, options) {
|
|
|
841
973
|
pullRequest: selectedPullRequest,
|
|
842
974
|
error: { code: 'PR_BIND_CONFLICT', message: 'Existing external PR audit evidence conflicts with the selected identity', retryable: false }
|
|
843
975
|
}), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
844
|
-
if (alreadyAudited && base.
|
|
976
|
+
if (alreadyAudited && base.prIdentity && !resourceIdentityEquals(base.prIdentity, pullRequestIdentity(selectedPullRequest)))
|
|
845
977
|
return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
846
978
|
platform: base.context.platform,
|
|
847
979
|
capabilities: base.context.capabilities,
|
|
848
980
|
pullRequest: selectedPullRequest,
|
|
849
981
|
error: { code: 'PR_BIND_CONFLICT', message: 'External PR audit evidence does not match the task binding', retryable: false }
|
|
850
982
|
}), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
851
|
-
if (base.
|
|
983
|
+
if (base.prIdentity && resourceIdentityEquals(base.prIdentity, pullRequestIdentity(selectedPullRequest)) && alreadyAudited) {
|
|
852
984
|
return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
853
985
|
platform: base.context.platform,
|
|
854
986
|
capabilities: base.context.capabilities,
|
|
@@ -886,18 +1018,42 @@ function resolveExternalPullRequest(taskRef, options) {
|
|
|
886
1018
|
error: null
|
|
887
1019
|
}), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
888
1020
|
}
|
|
889
|
-
function
|
|
890
|
-
const base = resolvedContext(taskRef, options);
|
|
1021
|
+
async function createPlatformPullRequest(taskRef, options) {
|
|
1022
|
+
const base = await resolvedContext(taskRef, options);
|
|
891
1023
|
if (!base.ok)
|
|
892
1024
|
return withCreation(base.output, PRECONDITION_NOT_CREATED);
|
|
1025
|
+
if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) {
|
|
1026
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1027
|
+
error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
|
|
1028
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1029
|
+
}
|
|
1030
|
+
try {
|
|
1031
|
+
return withTaskExecutionLock(base.resolved.repoRoot, base.resolved.taskId, 'platform-pr.create', () => createExternalPullRequest(taskRef, options, base));
|
|
1032
|
+
}
|
|
1033
|
+
catch (error) {
|
|
1034
|
+
if (error instanceof TaskExecutionLockError) {
|
|
1035
|
+
return withCreation(result('blocked', base.resolved.taskId, base.issueNumber, null, {
|
|
1036
|
+
platform: base.context.platform,
|
|
1037
|
+
capabilities: base.context.capabilities,
|
|
1038
|
+
error: { code: error.code, message: error.message, retryable: true }
|
|
1039
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1040
|
+
}
|
|
1041
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1042
|
+
platform: base.context.platform,
|
|
1043
|
+
capabilities: base.context.capabilities,
|
|
1044
|
+
error: {
|
|
1045
|
+
code: 'PR_CREATE_FAILED',
|
|
1046
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1047
|
+
retryable: false
|
|
1048
|
+
}
|
|
1049
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
async function createExternalPullRequest(taskRef, options, base) {
|
|
893
1053
|
if (!base.fact)
|
|
894
1054
|
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
895
1055
|
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
896
1056
|
}), PRECONDITION_NOT_CREATED);
|
|
897
|
-
if (!options.base || !options.head || !options.title.trim() || !options.body.trim())
|
|
898
|
-
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
899
|
-
error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
|
|
900
|
-
}), PRECONDITION_NOT_CREATED);
|
|
901
1057
|
const target = taskDeliveryTarget(base);
|
|
902
1058
|
if (!target.ok)
|
|
903
1059
|
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
@@ -905,194 +1061,132 @@ function createPlatformPullRequestUnlocked(taskRef, options) {
|
|
|
905
1061
|
}), PRECONDITION_NOT_CREATED);
|
|
906
1062
|
if (options.base !== target.value.baseRef)
|
|
907
1063
|
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
908
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
909
|
-
error: { code: 'PR_DELIVERY_TARGET_MISMATCH', message: `Pull request base '${options.base}' does not match delivery target '${target.value.baseRef}'`, retryable: false }
|
|
910
|
-
}), PRECONDITION_NOT_CREATED);
|
|
911
|
-
if (base.prNumber) {
|
|
912
|
-
const headCheck = verifyCreateHead(base, options.head);
|
|
913
|
-
if (!headCheck.ok)
|
|
914
|
-
return withCreation(result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
915
|
-
platform: base.context.platform,
|
|
916
|
-
capabilities: base.context.capabilities,
|
|
917
|
-
error: headCheck.error
|
|
918
|
-
}), PRECONDITION_NOT_CREATED);
|
|
919
|
-
const inspected = inspectPlatformPullRequest(taskRef, options);
|
|
920
|
-
const identity = validateBoundPullRequest(base, inspected.pullRequest, options.head, options.base, headCheck.value);
|
|
921
|
-
if (!identity.ok)
|
|
922
|
-
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
923
|
-
platform: base.context.platform,
|
|
924
|
-
capabilities: base.context.capabilities,
|
|
925
|
-
pullRequest: inspected.pullRequest,
|
|
926
|
-
error: identity.error
|
|
927
|
-
}), PRECONDITION_NOT_CREATED);
|
|
928
|
-
return withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
|
|
929
|
-
}
|
|
930
|
-
const headCheck = verifyCreateHead(base, options.head);
|
|
931
|
-
if (!headCheck.ok)
|
|
932
|
-
return withCreation(result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
|
|
933
1064
|
platform: base.context.platform,
|
|
934
1065
|
capabilities: base.context.capabilities,
|
|
935
|
-
error:
|
|
936
|
-
}), PRECONDITION_NOT_CREATED);
|
|
937
|
-
const expectedHeadSha = headCheck.value;
|
|
938
|
-
const located = locatePullRequest(base, options.head, options.base, expectedHeadSha);
|
|
939
|
-
if (!located.ok && located.error.code === 'PR_IDENTITY_AMBIGUOUS')
|
|
940
|
-
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
941
|
-
platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
|
|
942
|
-
}), PRECONDITION_NOT_CREATED);
|
|
943
|
-
if (!located.ok && located.error.code === 'PR_HEAD_SHA_MISMATCH')
|
|
944
|
-
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
945
|
-
platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
|
|
946
|
-
}), PRECONDITION_NOT_CREATED);
|
|
947
|
-
if (!located.ok && located.error.code !== 'PR_NOT_FOUND')
|
|
948
|
-
return withCreation(result(located.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
|
|
949
|
-
platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
|
|
1066
|
+
error: { code: 'PR_DELIVERY_TARGET_MISMATCH', message: `Pull request base '${options.base}' does not match delivery target '${target.value.baseRef}'`, retryable: false }
|
|
950
1067
|
}), PRECONDITION_NOT_CREATED);
|
|
1068
|
+
let verifiedHeadSha;
|
|
1069
|
+
if (base.provider.changeRequests?.verifyHead) {
|
|
1070
|
+
const verified = await base.provider.changeRequests.verifyHead({
|
|
1071
|
+
context: providerOperationContext(base.loadedContext),
|
|
1072
|
+
head: options.head
|
|
1073
|
+
});
|
|
1074
|
+
if (!verified.ok)
|
|
1075
|
+
return withCreation(providerPullRequestError(base, verified.error, base.prNumber), PRECONDITION_NOT_CREATED);
|
|
1076
|
+
verifiedHeadSha = verified.value.sha;
|
|
1077
|
+
}
|
|
1078
|
+
if (base.prIdentity) {
|
|
1079
|
+
const inspected = await inspectExternalPullRequest(base, base.prIdentity);
|
|
1080
|
+
return inspected.status === 'failed' || inspected.status === 'blocked'
|
|
1081
|
+
? withCreation(inspected, PRECONDITION_NOT_CREATED)
|
|
1082
|
+
: withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
|
|
1083
|
+
}
|
|
951
1084
|
if (options.dryRun)
|
|
952
1085
|
return withCreation(result('planned', base.resolved.taskId, base.issueNumber, null, {
|
|
953
|
-
platform: base.context.platform,
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1086
|
+
platform: base.context.platform,
|
|
1087
|
+
capabilities: base.context.capabilities,
|
|
1088
|
+
operations: [{ name: 'pr:create', status: 'planned', reasonCode: null }],
|
|
1089
|
+
result: 'pr_created',
|
|
1090
|
+
error: null
|
|
1091
|
+
}), { kind: 'planned', action: 'create', createdByCurrentOperation: false });
|
|
957
1092
|
const started = writeCreateStarted(base, options.agent);
|
|
958
1093
|
if (started.status === 'failed')
|
|
959
1094
|
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
960
1095
|
error: { code: started.error.code, message: started.error.message, retryable: false }
|
|
961
1096
|
}), PRECONDITION_NOT_CREATED);
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
options.
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
options.phase.value = 'post-accepted';
|
|
980
|
-
pullRequest = normalizePullRequest(response.value, repository);
|
|
981
|
-
}
|
|
982
|
-
else if (response.error.retryable) {
|
|
983
|
-
return withCreation(result('blocked', base.resolved.taskId, base.issueNumber, null, {
|
|
984
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
985
|
-
operations: [{ name: 'pr:create', status: 'failed', reasonCode: 'PR_CREATE_OUTCOME_UNKNOWN' }],
|
|
986
|
-
error: { code: 'PR_CREATE_OUTCOME_UNKNOWN', message: response.error.message, retryable: true }
|
|
987
|
-
}), { kind: 'unknown', errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' });
|
|
988
|
-
}
|
|
989
|
-
else
|
|
990
|
-
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
991
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
992
|
-
operations: [{ name: 'pr:create', status: 'failed', reasonCode: response.error.code }], error: response.error
|
|
993
|
-
}), { kind: 'not-created', reason: 'post-rejected', createdByCurrentOperation: false });
|
|
994
|
-
if (!pullRequest)
|
|
995
|
-
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
996
|
-
error: { code: 'PR_CREATE_RESPONSE_INVALID', message: 'PR create response lacks validated identity', retryable: false }
|
|
997
|
-
}), { kind: 'created', createdByCurrentOperation: true });
|
|
998
|
-
if (pullRequest.head.sha !== expectedHeadSha)
|
|
999
|
-
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1000
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1001
|
-
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
|
|
1002
|
-
error: { code: 'PR_HEAD_SHA_MISMATCH', message: 'Created pull request head does not match the expected local HEAD', retryable: false }
|
|
1003
|
-
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1004
|
-
}
|
|
1005
|
-
const bindHeadCheck = verifyCreateHead(base, options.head);
|
|
1006
|
-
if (!bindHeadCheck.ok || bindHeadCheck.value !== expectedHeadSha) {
|
|
1007
|
-
const error = bindHeadCheck.ok
|
|
1008
|
-
? { code: 'PR_REMOTE_HEAD_MISMATCH', message: 'Remote head changed before task binding', retryable: false }
|
|
1009
|
-
: bindHeadCheck.error;
|
|
1010
|
-
return withCreation(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1011
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1012
|
-
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
|
|
1013
|
-
}), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
|
|
1097
|
+
const created = base.provider.changeRequests?.create
|
|
1098
|
+
? await base.provider.changeRequests.create({
|
|
1099
|
+
context: providerOperationContext(base.loadedContext),
|
|
1100
|
+
base: options.base,
|
|
1101
|
+
head: options.head,
|
|
1102
|
+
title: options.title,
|
|
1103
|
+
body: options.body,
|
|
1104
|
+
draft: Boolean(options.draft),
|
|
1105
|
+
mutation: { idempotencyKey: `pull-request:create:${base.resolved.taskId}` }
|
|
1106
|
+
})
|
|
1107
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.create');
|
|
1108
|
+
if (!created.ok)
|
|
1109
|
+
return withCreation(providerPullRequestError(base, created.error, null), { kind: 'unknown', errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' });
|
|
1110
|
+
const remoteId = String(created.value.remoteId || '');
|
|
1111
|
+
let createdIdentity;
|
|
1112
|
+
try {
|
|
1113
|
+
createdIdentity = providerResourceIdentity(base.provider, 'pull-request', remoteId);
|
|
1014
1114
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
const error = beforeBind.ok
|
|
1018
|
-
? { code: 'PR_BIND_RECHECK_FAILED', message: 'Pull request recheck returned no identity', retryable: false }
|
|
1019
|
-
: beforeBind.error;
|
|
1020
|
-
return withCreation(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1115
|
+
catch (error) {
|
|
1116
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1021
1117
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1022
|
-
|
|
1023
|
-
}),
|
|
1118
|
+
error: { code: 'PR_CREATE_RESPONSE_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
1119
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1024
1120
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1121
|
+
const number = resourceIdentityNumber(createdIdentity);
|
|
1122
|
+
const fallbackNumber = number ?? 0;
|
|
1123
|
+
const fallback = normalizeProviderPullRequest({
|
|
1124
|
+
id: remoteId,
|
|
1125
|
+
identity: createdIdentity,
|
|
1126
|
+
number: number || undefined,
|
|
1127
|
+
state: 'open',
|
|
1128
|
+
title: options.title,
|
|
1129
|
+
body: options.body,
|
|
1130
|
+
headSha: '',
|
|
1131
|
+
baseSha: '',
|
|
1132
|
+
head: { repository: base.context.platform.repository, ref: options.head, sha: '' },
|
|
1133
|
+
base: { repository: base.context.platform.repository, ref: options.base, sha: '' },
|
|
1134
|
+
displayUrl: `${base.provider.type}:${remoteId}`
|
|
1135
|
+
}, base.context.platform.repository, fallbackNumber);
|
|
1136
|
+
const inspected = base.provider.changeRequests?.inspect
|
|
1137
|
+
? await base.provider.changeRequests.inspect({
|
|
1138
|
+
context: providerOperationContext(base.loadedContext),
|
|
1139
|
+
target: createdIdentity
|
|
1140
|
+
})
|
|
1141
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
|
|
1142
|
+
if (!inspected.ok)
|
|
1143
|
+
return withCreation(providerPullRequestError(base, inspected.error, number), { kind: 'created', createdByCurrentOperation: true });
|
|
1144
|
+
const pullRequest = normalizeProviderPullRequest(inspected.value, base.context.platform.repository, inspected.value.number ?? fallback.number, fallback);
|
|
1145
|
+
if (!pullRequest.nodeId || !isUsableResourceIdentity(pullRequestIdentity(pullRequest)))
|
|
1037
1146
|
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1147
|
+
platform: base.context.platform,
|
|
1148
|
+
capabilities: base.context.capabilities,
|
|
1149
|
+
error: { code: 'PR_CREATE_RESPONSE_INVALID', message: 'Provider create response lacks validated identity', retryable: false }
|
|
1150
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1151
|
+
const identity = validateWriterIdentity(base, pullRequest, {
|
|
1152
|
+
expectedHead: options.head,
|
|
1153
|
+
expectedHeadSha: verifiedHeadSha,
|
|
1154
|
+
errorCode: 'PR_BIND_IDENTITY_MISMATCH'
|
|
1155
|
+
});
|
|
1156
|
+
if (!identity.ok)
|
|
1157
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1158
|
+
platform: base.context.platform,
|
|
1159
|
+
capabilities: base.context.capabilities,
|
|
1160
|
+
resource: { kind: 'pull-request', number: pullRequest.number },
|
|
1161
|
+
pullRequest,
|
|
1162
|
+
error: identity.error
|
|
1163
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1164
|
+
const bound = bindIdentity(base, pullRequest, options.agent, 'created', false, options.head);
|
|
1041
1165
|
if (bound.status === 'failed')
|
|
1042
|
-
return withCreation(result('failed', base.resolved.taskId, base.issueNumber,
|
|
1043
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1044
|
-
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
|
|
1045
|
-
operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: 'applied', reasonCode: null }, { name: 'task:bind-pr', status: 'failed', reasonCode: bound.error.code }],
|
|
1046
|
-
error: { code: 'PR_CREATED_BIND_FAILED', message: `${pullRequest.url}: ${bound.error.message}`, retryable: false }
|
|
1047
|
-
}), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'reused', createdByCurrentOperation: false });
|
|
1048
|
-
return withCreation(result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1049
|
-
changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1050
|
-
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
|
|
1051
|
-
operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: created ? 'applied' : 'no-op', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }],
|
|
1052
|
-
result: created ? 'pr_created' : 'pr_reused', error: null
|
|
1053
|
-
}), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'reused', createdByCurrentOperation: false });
|
|
1054
|
-
}
|
|
1055
|
-
function createPlatformPullRequest(taskRef, options) {
|
|
1056
|
-
const base = resolvedContext(taskRef, options);
|
|
1057
|
-
if (!base.ok)
|
|
1058
|
-
return withCreation(base.output, PRECONDITION_NOT_CREATED);
|
|
1059
|
-
if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) {
|
|
1060
|
-
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1061
|
-
error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
|
|
1062
|
-
}), PRECONDITION_NOT_CREATED);
|
|
1063
|
-
}
|
|
1064
|
-
const phase = { value: 'before-post' };
|
|
1065
|
-
try {
|
|
1066
|
-
return withTaskExecutionLock(base.resolved.repoRoot, base.resolved.taskId, 'platform-pr.create', () => createPlatformPullRequestUnlocked(taskRef, { ...options, phase }));
|
|
1067
|
-
}
|
|
1068
|
-
catch (error) {
|
|
1069
|
-
if (error instanceof TaskExecutionLockError) {
|
|
1070
|
-
return withCreation(result('blocked', base.resolved.taskId, base.issueNumber, null, {
|
|
1071
|
-
platform: base.context.platform,
|
|
1072
|
-
capabilities: base.context.capabilities,
|
|
1073
|
-
error: { code: error.code, message: error.message, retryable: true }
|
|
1074
|
-
}), PRECONDITION_NOT_CREATED);
|
|
1075
|
-
}
|
|
1076
|
-
const creation = phase.value === 'post-accepted'
|
|
1077
|
-
? { kind: 'created', createdByCurrentOperation: true }
|
|
1078
|
-
: phase.value === 'post-dispatched'
|
|
1079
|
-
? { kind: 'unknown', errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' }
|
|
1080
|
-
: PRECONDITION_NOT_CREATED;
|
|
1081
|
-
return withCreation(result(phase.value === 'post-dispatched' ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1166
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1082
1167
|
platform: base.context.platform,
|
|
1083
1168
|
capabilities: base.context.capabilities,
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1169
|
+
resource: { kind: 'pull-request', number: pullRequest.number },
|
|
1170
|
+
pullRequest,
|
|
1171
|
+
error: { code: 'PR_CREATED_BIND_FAILED', message: `${pullRequest.url}: ${bound.error.message}`, retryable: false }
|
|
1172
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1173
|
+
return withCreation(result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1174
|
+
changed: true,
|
|
1175
|
+
platform: base.context.platform,
|
|
1176
|
+
capabilities: base.context.capabilities,
|
|
1177
|
+
resource: { kind: 'pull-request', number: pullRequest.number },
|
|
1178
|
+
pullRequest,
|
|
1179
|
+
operations: [{ name: 'pr:create', status: 'applied', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }],
|
|
1180
|
+
result: 'pr_created',
|
|
1181
|
+
error: null
|
|
1182
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1091
1183
|
}
|
|
1092
|
-
function syncPlatformPullRequest(taskRef, options) {
|
|
1184
|
+
async function syncPlatformPullRequest(taskRef, options) {
|
|
1093
1185
|
const warningResult = warningResultForPrimary(options.primaryResult);
|
|
1094
1186
|
const softenFailure = (output) => {
|
|
1187
|
+
const authorityError = output.error?.code.startsWith('IN_LABEL_SYNC') || output.error?.code.startsWith('PR_IDENTITY');
|
|
1095
1188
|
const warning = output.error && output.task.prNumber !== null
|
|
1189
|
+
&& !authorityError
|
|
1096
1190
|
&& !['PR_NOT_LINKED', 'PR_BIND_CONFLICT'].includes(output.error.code)
|
|
1097
1191
|
? {
|
|
1098
1192
|
code: output.error.code,
|
|
@@ -1107,80 +1201,169 @@ function syncPlatformPullRequest(taskRef, options) {
|
|
|
1107
1201
|
? { ...output, status: 'applied', changed: false, error: null, result: warningResult, warnings: mergeOperationWarnings([warning]) }
|
|
1108
1202
|
: output;
|
|
1109
1203
|
};
|
|
1110
|
-
const base = resolvedContext(taskRef, options);
|
|
1204
|
+
const base = await resolvedContext(taskRef, options);
|
|
1111
1205
|
if (!base.ok)
|
|
1112
1206
|
return softenFailure(base.output);
|
|
1113
1207
|
if (!base.fact)
|
|
1114
1208
|
return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1115
1209
|
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
1116
1210
|
}));
|
|
1117
|
-
if (!base.
|
|
1211
|
+
if (!base.prIdentity)
|
|
1118
1212
|
return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1119
1213
|
error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
|
|
1120
1214
|
}));
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1215
|
+
{
|
|
1216
|
+
const inspected = await inspectExternalPullRequest(base, base.prIdentity);
|
|
1217
|
+
if (inspected.status === 'failed' || inspected.status === 'blocked' || !inspected.pullRequest)
|
|
1218
|
+
return softenFailure(inspected);
|
|
1219
|
+
if (!options.metadata && !options.closingIssue)
|
|
1220
|
+
return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1221
|
+
error: { code: 'PR_PAYLOAD_INVALID', message: 'sync requires a desired-state option', retryable: false }
|
|
1222
|
+
}));
|
|
1223
|
+
const issue = await inspectPlatformIssue(taskRef, { cwd: base.resolved.repoRoot, client: base.client });
|
|
1224
|
+
if (!issue.issue)
|
|
1225
|
+
return softenFailure(result(issue.status, base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1226
|
+
platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: inspected.pullRequest, error: issue.error
|
|
1227
|
+
}));
|
|
1228
|
+
let inLabels = inspected.pullRequest.labels.filter((label) => label.startsWith('in:')).sort();
|
|
1229
|
+
let issueInLabelOperation = null;
|
|
1230
|
+
if (options.metadata && base.provider.issues?.listLabels) {
|
|
1231
|
+
const labels = await base.provider.issues.listLabels({ context: providerOperationContext(base.loadedContext) });
|
|
1232
|
+
if (!labels.ok)
|
|
1233
|
+
return softenFailure(providerPullRequestError(base, labels.error, base.prNumber));
|
|
1234
|
+
const target = taskInLabelTarget(base.resolved.repoRoot, base.frontmatter, inspected.pullRequest.labels, new Set(labels.value));
|
|
1235
|
+
if (!target.ok)
|
|
1236
|
+
return softenFailure(providerPullRequestError(base, target.error, base.prNumber));
|
|
1237
|
+
inLabels = target.value;
|
|
1238
|
+
const currentIssueLabels = issue.issue.labels.filter((label) => label.startsWith('in:')).sort();
|
|
1239
|
+
issueInLabelOperation = base.context.capabilities.triage
|
|
1240
|
+
? currentIssueLabels.join('\0') === inLabels.join('\0')
|
|
1241
|
+
? { name: 'labels:in:issue', status: 'no-op', reasonCode: null }
|
|
1242
|
+
: { name: 'labels:in:issue', status: 'planned', reasonCode: null }
|
|
1243
|
+
: { name: 'labels:in:issue', status: 'skipped', reasonCode: 'TRIAGE_REQUIRED' };
|
|
1244
|
+
}
|
|
1245
|
+
const planned = planPullRequestMetadata({
|
|
1246
|
+
pullRequest: inspected.pullRequest,
|
|
1247
|
+
issue: issue.issue,
|
|
1248
|
+
taskType: String(base.frontmatter.type || 'task'),
|
|
1249
|
+
issueNumber: base.issueNumber,
|
|
1250
|
+
capabilities: base.context.capabilities,
|
|
1251
|
+
inLabels
|
|
1252
|
+
}).operations.filter((operation) => options.metadata || operation.name === 'closing-issue')
|
|
1253
|
+
.filter((operation) => options.closingIssue || operation.name !== 'closing-issue');
|
|
1254
|
+
const operations = [
|
|
1255
|
+
...(issueInLabelOperation ? [issueInLabelOperation] : []),
|
|
1256
|
+
...planned.map(({ name, status, reasonCode }) => ({ name, status, reasonCode }))
|
|
1257
|
+
];
|
|
1258
|
+
const milestoneOperation = planned.find((operation) => operation.name === 'milestone' && operation.status === 'planned');
|
|
1259
|
+
if (milestoneOperation && base.provider.issues?.listMilestones) {
|
|
1260
|
+
const milestones = await base.provider.issues.listMilestones({ context: providerOperationContext(base.loadedContext) });
|
|
1162
1261
|
if (!milestones.ok)
|
|
1163
|
-
return
|
|
1164
|
-
const flat = milestones.value.flatMap((entry) => Array.isArray(entry) ? entry : [entry]);
|
|
1165
|
-
payload.milestone = flat.find((entry) => entry.title === operation.value)?.number ?? null;
|
|
1262
|
+
return providerPullRequestError(base, milestones.error, base.prNumber);
|
|
1166
1263
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1264
|
+
if (options.dryRun)
|
|
1265
|
+
return softenFailure(result(planned.some((operation) => operation.status === 'planned') ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1266
|
+
platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: inspected.pullRequest, operations, error: null
|
|
1267
|
+
}));
|
|
1268
|
+
let issueChanged = false;
|
|
1269
|
+
if (issueInLabelOperation?.status === 'planned') {
|
|
1270
|
+
const desiredIssueLabels = [...new Set([
|
|
1271
|
+
...issue.issue.labels.filter((label) => !label.startsWith('in:')),
|
|
1272
|
+
...inLabels
|
|
1273
|
+
])].sort();
|
|
1274
|
+
const updatedIssue = base.provider.issues?.update
|
|
1275
|
+
? await base.provider.issues.update({
|
|
1276
|
+
context: providerOperationContext(base.loadedContext),
|
|
1277
|
+
target: base.issueIdentity,
|
|
1278
|
+
currentLabels: issue.issue.labels,
|
|
1279
|
+
patch: { labels: desiredIssueLabels },
|
|
1280
|
+
mutation: { idempotencyKey: `issue:update:${base.resolved.taskId}:in-labels` }
|
|
1281
|
+
})
|
|
1282
|
+
: unsupportedProviderOperation(base.provider, 'issues.update');
|
|
1283
|
+
if (!updatedIssue.ok)
|
|
1284
|
+
return softenFailure(providerPullRequestError(base, updatedIssue.error, base.prNumber));
|
|
1285
|
+
issueChanged = updatedIssue.value.changed;
|
|
1286
|
+
const rereadIssue = base.provider.issues?.inspect
|
|
1287
|
+
? await base.provider.issues.inspect({ context: providerOperationContext(base.loadedContext), target: base.issueIdentity })
|
|
1288
|
+
: unsupportedProviderOperation(base.provider, 'issues.inspect');
|
|
1289
|
+
if (!rereadIssue.ok)
|
|
1290
|
+
return softenFailure(providerPullRequestError(base, {
|
|
1291
|
+
...rereadIssue.error,
|
|
1292
|
+
code: 'IN_LABEL_SYNC_PARTIAL',
|
|
1293
|
+
message: `In-label synchronization is partial or unknown: ${rereadIssue.error.message}`
|
|
1294
|
+
}, base.prNumber));
|
|
1295
|
+
const actualIssueLabels = rereadIssue.value.labels.filter((label) => label.startsWith('in:')).sort();
|
|
1296
|
+
if (actualIssueLabels.join('\0') !== inLabels.join('\0'))
|
|
1297
|
+
return softenFailure(providerPullRequestError(base, {
|
|
1298
|
+
code: 'IN_LABEL_SYNC_PARTIAL',
|
|
1299
|
+
message: 'Issue in: labels did not converge after update',
|
|
1300
|
+
retryable: true
|
|
1301
|
+
}, base.prNumber));
|
|
1302
|
+
}
|
|
1303
|
+
const patch = {};
|
|
1304
|
+
for (const operation of planned) {
|
|
1305
|
+
if (operation.status !== 'planned')
|
|
1306
|
+
continue;
|
|
1307
|
+
if (operation.name === 'labels')
|
|
1308
|
+
patch.labels = operation.value;
|
|
1309
|
+
if (operation.name === 'assignees')
|
|
1310
|
+
patch.assignees = operation.value;
|
|
1311
|
+
if (operation.name === 'closing-issue')
|
|
1312
|
+
patch.body = operation.value;
|
|
1313
|
+
if (operation.name === 'milestone')
|
|
1314
|
+
patch.milestone = operation.value;
|
|
1315
|
+
}
|
|
1316
|
+
if (Object.keys(patch).length === 0)
|
|
1317
|
+
return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1318
|
+
platform: base.context.platform, capabilities: base.context.capabilities, resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: inspected.pullRequest, operations, error: null
|
|
1319
|
+
}));
|
|
1320
|
+
const updated = base.provider.changeRequests?.update
|
|
1321
|
+
? await base.provider.changeRequests.update({
|
|
1322
|
+
context: providerOperationContext(base.loadedContext),
|
|
1323
|
+
target: base.prIdentity,
|
|
1324
|
+
currentLabels: inspected.pullRequest.labels,
|
|
1325
|
+
patch: patch,
|
|
1326
|
+
mutation: { idempotencyKey: `pull-request:update:${base.resolved.taskId}` }
|
|
1327
|
+
})
|
|
1328
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.update');
|
|
1329
|
+
if (!updated.ok) {
|
|
1330
|
+
const error = issueChanged && !updated.error.code.startsWith('IN_LABEL_SYNC')
|
|
1331
|
+
? { ...updated.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${updated.error.message}` }
|
|
1332
|
+
: updated.error;
|
|
1333
|
+
return softenFailure(providerPullRequestError(base, error, base.prNumber));
|
|
1334
|
+
}
|
|
1335
|
+
let finalPullRequest = inspected.pullRequest;
|
|
1336
|
+
if (planned.some((operation) => operation.name === 'labels' && operation.status === 'planned')) {
|
|
1337
|
+
const reread = base.provider.changeRequests?.inspect
|
|
1338
|
+
? await base.provider.changeRequests.inspect({ context: providerOperationContext(base.loadedContext), target: base.prIdentity })
|
|
1339
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
|
|
1340
|
+
if (!reread.ok)
|
|
1341
|
+
return softenFailure(providerPullRequestError(base, {
|
|
1342
|
+
...reread.error,
|
|
1343
|
+
code: 'IN_LABEL_SYNC_PARTIAL',
|
|
1344
|
+
message: `In-label synchronization is partial or unknown: ${reread.error.message}`
|
|
1345
|
+
}, base.prNumber));
|
|
1346
|
+
finalPullRequest = normalizeProviderPullRequest(reread.value, base.context.platform.repository, base.prNumber || 0, inspected.pullRequest);
|
|
1347
|
+
const expected = ((planned.find((operation) => operation.name === 'labels')?.value || [])
|
|
1348
|
+
.filter((label) => label.startsWith('in:') || label.startsWith('type:'))).sort();
|
|
1349
|
+
const actual = finalPullRequest.labels.filter((label) => label.startsWith('in:') || label.startsWith('type:')).sort();
|
|
1350
|
+
if (actual.join('\0') !== expected.join('\0'))
|
|
1351
|
+
return softenFailure(providerPullRequestError(base, {
|
|
1352
|
+
code: 'IN_LABEL_SYNC_PARTIAL',
|
|
1353
|
+
message: 'Pull request labels did not converge after update',
|
|
1354
|
+
retryable: true
|
|
1355
|
+
}, base.prNumber));
|
|
1356
|
+
}
|
|
1357
|
+
return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1358
|
+
changed: issueChanged || updated.value.changed,
|
|
1359
|
+
platform: base.context.platform,
|
|
1360
|
+
capabilities: base.context.capabilities,
|
|
1361
|
+
resource: { kind: 'pull-request', number: base.prNumber },
|
|
1362
|
+
pullRequest: finalPullRequest,
|
|
1363
|
+
operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation),
|
|
1364
|
+
error: null
|
|
1172
1365
|
}));
|
|
1173
1366
|
}
|
|
1174
|
-
const patched = base.client.json(['api', `repos/${base.context.platform.repository}/issues/${base.prNumber}`, '-X', 'PATCH', '--input', '-'], {
|
|
1175
|
-
cwd: base.resolved.repoRoot, method: 'PATCH', input: JSON.stringify(payload)
|
|
1176
|
-
});
|
|
1177
|
-
if (!patched.ok)
|
|
1178
|
-
return softenFailure(result(patched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: patched.error }));
|
|
1179
|
-
return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1180
|
-
changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1181
|
-
resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: fetched.value,
|
|
1182
|
-
operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
|
|
1183
|
-
}));
|
|
1184
1367
|
}
|
|
1185
1368
|
function readProjectPrFlow(repoRoot) {
|
|
1186
1369
|
try {
|
|
@@ -1203,12 +1386,18 @@ function skipPlatformPullRequestFactUnlocked(resolved, options) {
|
|
|
1203
1386
|
error: { code: 'TASK_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
1204
1387
|
});
|
|
1205
1388
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1389
|
+
let issueIdentity;
|
|
1390
|
+
try {
|
|
1391
|
+
issueIdentity = taskIssueIdentity(frontmatter, undefined, options.runtimeVersion);
|
|
1392
|
+
}
|
|
1393
|
+
catch (error) {
|
|
1394
|
+
return result('failed', resolved.taskId, null, null, { error: { ...taskIssueIdentityError(error), retryable: false } });
|
|
1395
|
+
}
|
|
1396
|
+
const issueNumber = resourceIdentityNumber(issueIdentity);
|
|
1397
|
+
const existing = readPrDeliveryFact(frontmatter, options.runtimeVersion);
|
|
1209
1398
|
if (existing.status === 'invalid')
|
|
1210
1399
|
return result('failed', resolved.taskId, issueNumber, null, {
|
|
1211
|
-
error: { code:
|
|
1400
|
+
error: { code: existing.error.code, message: existing.error.message, retryable: false }
|
|
1212
1401
|
});
|
|
1213
1402
|
if (existing.status === 'missing')
|
|
1214
1403
|
return result('failed', resolved.taskId, issueNumber, null, {
|
|
@@ -1219,10 +1408,12 @@ function skipPlatformPullRequestFactUnlocked(resolved, options) {
|
|
|
1219
1408
|
operations: [{ name: 'task:skip-pr', status: 'no-op', reasonCode: null }],
|
|
1220
1409
|
error: null
|
|
1221
1410
|
});
|
|
1222
|
-
if (existing.fact.state === 'bound')
|
|
1223
|
-
|
|
1224
|
-
|
|
1411
|
+
if (existing.fact.state === 'bound') {
|
|
1412
|
+
const boundNumber = resourceIdentityNumber(existing.fact.identity.resource);
|
|
1413
|
+
return result('failed', resolved.taskId, issueNumber, boundNumber, {
|
|
1414
|
+
error: { code: 'PR_DELIVERY_FACT_CONFLICT', message: `Task is already bound to PR ${boundNumber === null ? 'identity' : `#${boundNumber}`}`, retryable: false }
|
|
1225
1415
|
});
|
|
1416
|
+
}
|
|
1226
1417
|
if (readProjectPrFlow(resolved.repoRoot) === 'required')
|
|
1227
1418
|
return result('failed', resolved.taskId, issueNumber, null, {
|
|
1228
1419
|
error: { code: 'PR_SKIP_FORBIDDEN', message: 'Cannot skip PR delivery when prFlow is required', retryable: false }
|
|
@@ -1249,7 +1440,7 @@ function skipPlatformPullRequestFactUnlocked(resolved, options) {
|
|
|
1249
1440
|
error: null
|
|
1250
1441
|
});
|
|
1251
1442
|
}
|
|
1252
|
-
function skipPlatformPullRequestFact(taskRef, options) {
|
|
1443
|
+
async function skipPlatformPullRequestFact(taskRef, options) {
|
|
1253
1444
|
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
1254
1445
|
if (!resolved.ok)
|
|
1255
1446
|
return result('failed', resolved.taskId, null, null, {
|
|
@@ -1272,6 +1463,6 @@ function skipPlatformPullRequestFact(taskRef, options) {
|
|
|
1272
1463
|
});
|
|
1273
1464
|
}
|
|
1274
1465
|
}
|
|
1275
|
-
export { bindPlatformPullRequest, createPlatformPullRequest, inspectGitHubPullRequest, inspectGitHubIssueClosingChangeRequests, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, normalizePullRequest,
|
|
1466
|
+
export { bindPlatformPullRequest, createPlatformPullRequest, inspectGitHubPullRequest, inspectGitHubIssueClosingChangeRequests, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, normalizePullRequest, resolveExternalPullRequest, skipPlatformPullRequestFact, selectExternalPullRequest, selectPullRequest, syncPlatformPullRequest, syncPlatformPullRequestInLabels };
|
|
1276
1467
|
export { warningResultForPrimary };
|
|
1277
1468
|
//# sourceMappingURL=pull-requests.js.map
|