pr-shepherd 0.56.6 → 0.57.0
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/.claude-plugin/plugin.json +1 -1
- package/README.md +44 -27
- package/bin/api.d.mts +25 -3
- package/bin/api.mjs +51 -13
- package/bin/checks/classify.d.mts +1 -0
- package/bin/checks/classify.mjs +1 -1
- package/bin/checks/conclusions.d.mts +5 -1
- package/bin/checks/conclusions.mjs +8 -2
- package/bin/checks/superseded.d.mts +8 -2
- package/bin/checks/superseded.mjs +24 -7
- package/bin/classify/loader.d.mts +2 -0
- package/bin/classify/loader.mjs +15 -0
- package/bin/classify/standard/author.d.mts +3 -0
- package/bin/classify/standard/author.mjs +10 -0
- package/bin/classify/standard/codecov-report.d.mts +4 -0
- package/bin/classify/standard/codecov-report.mjs +10 -0
- package/bin/classify/standard/coderabbit-notices.d.mts +8 -0
- package/bin/classify/standard/coderabbit-notices.mjs +38 -0
- package/bin/classify/standard/coderabbit-review-summary.d.mts +8 -0
- package/bin/classify/standard/coderabbit-review-summary.mjs +37 -0
- package/bin/classify/standard/codex-notices.d.mts +7 -0
- package/bin/classify/standard/codex-notices.mjs +36 -0
- package/bin/classify/standard/codex-review-summary.d.mts +7 -0
- package/bin/classify/standard/codex-review-summary.mjs +20 -0
- package/bin/classify/standard/dependabot-notices.d.mts +4 -0
- package/bin/classify/standard/dependabot-notices.mjs +15 -0
- package/bin/classify/standard/gemini-notices.d.mts +4 -0
- package/bin/classify/standard/gemini-notices.mjs +13 -0
- package/bin/classify/standard/index.d.mts +4 -0
- package/bin/classify/standard/index.mjs +22 -0
- package/bin/classify/standard/kilo-no-issues.d.mts +4 -0
- package/bin/classify/standard/kilo-no-issues.mjs +17 -0
- package/bin/classify/standard/sourcery-notices.d.mts +8 -0
- package/bin/classify/standard/sourcery-notices.mjs +20 -0
- package/bin/cli/api-usage-formatter.mjs +6 -1
- package/bin/cli/apply-merge-handler.d.mts +3 -0
- package/bin/cli/apply-merge-handler.mjs +73 -0
- package/bin/cli/arg-flags.d.mts +2 -0
- package/bin/cli/arg-flags.mjs +47 -0
- package/bin/cli/args.mjs +10 -41
- package/bin/cli/clean-handler.d.mts +1 -0
- package/bin/cli/clean-handler.mjs +71 -0
- package/bin/cli/default-poll.mjs +3 -0
- package/bin/cli/event-state-scope.d.mts +19 -0
- package/bin/cli/event-state-scope.mjs +60 -0
- package/bin/cli/fix-formatter.mjs +5 -11
- package/bin/cli/handlers.d.mts +0 -1
- package/bin/cli/handlers.mjs +8 -64
- package/bin/cli/help-command-pages.d.mts +62 -18
- package/bin/cli/help-command-pages.mjs +59 -15
- package/bin/cli/help-iterate-poll-pages.d.mts +2 -2
- package/bin/cli/help-iterate-poll-pages.mjs +6 -0
- package/bin/cli/help-log-file-page.d.mts +1 -1
- package/bin/cli/help-log-file-page.mjs +4 -1
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +5 -0
- package/bin/cli/help.d.mts +63 -19
- package/bin/cli/help.mjs +2 -0
- package/bin/cli/iterate-branch-segment.d.mts +2 -0
- package/bin/cli/iterate-branch-segment.mjs +12 -7
- package/bin/cli/iterate-flags.d.mts +7 -1
- package/bin/cli/iterate-flags.mjs +38 -0
- package/bin/cli/iterate-formatter.d.mts +6 -6
- package/bin/cli/iterate-formatter.mjs +56 -22
- package/bin/cli/iterate-instructions.mjs +45 -15
- package/bin/cli/iterate-lean.mjs +39 -14
- package/bin/cli/iterate-merge-formatter.d.mts +6 -0
- package/bin/cli/iterate-merge-formatter.mjs +24 -14
- package/bin/cli/iterate-verbose.mjs +3 -6
- package/bin/cli/list-formatters.mjs +3 -1
- package/bin/cli/mutate-formatter.mjs +10 -5
- package/bin/cli/next-check-format.d.mts +7 -0
- package/bin/cli/next-check-format.mjs +10 -0
- package/bin/cli/playbook-handler.d.mts +5 -0
- package/bin/cli/playbook-handler.mjs +40 -0
- package/bin/cli/poll-handler.mjs +7 -6
- package/bin/cli/poll-summary-formatter.mjs +11 -2
- package/bin/cli/poll-targets.mjs +2 -0
- package/bin/cli/resolve-validators.mjs +2 -2
- package/bin/cli/runner.mjs +6 -1
- package/bin/cli/stack-layer-view.d.mts +33 -0
- package/bin/cli/stack-layer-view.mjs +1 -0
- package/bin/cli/stack-overview.d.mts +10 -28
- package/bin/cli/stack-overview.mjs +39 -14
- package/bin/cli/transport-args.d.mts +5 -0
- package/bin/cli/transport-args.mjs +23 -0
- package/bin/cli/transport-formatter.d.mts +9 -0
- package/bin/cli/transport-formatter.mjs +13 -0
- package/bin/cli-parser.mjs +62 -7
- package/bin/commands/apply-merge-options.d.mts +4 -0
- package/bin/commands/apply-merge-options.mjs +34 -0
- package/bin/commands/apply-merge-output.d.mts +7 -0
- package/bin/commands/apply-merge-output.mjs +20 -0
- package/bin/commands/apply-merge-pull.d.mts +20 -0
- package/bin/commands/apply-merge-pull.mjs +15 -0
- package/bin/commands/apply-merge.d.mts +18 -0
- package/bin/commands/apply-merge.mjs +126 -0
- package/bin/commands/apply-queue-removal.mjs +7 -0
- package/bin/commands/check-annotations.mjs +28 -18
- package/bin/commands/check-execution-context.d.mts +5 -0
- package/bin/commands/check-execution-context.mjs +16 -1
- package/bin/commands/check-fingerprint.d.mts +26 -2
- package/bin/commands/check-fingerprint.mjs +64 -28
- package/bin/commands/check-terminal-report.mjs +11 -1
- package/bin/commands/check-unreported.d.mts +6 -2
- package/bin/commands/check-unreported.mjs +32 -12
- package/bin/commands/check-wait-detectors.d.mts +35 -0
- package/bin/commands/check-wait-detectors.mjs +107 -0
- package/bin/commands/check.d.mts +4 -2
- package/bin/commands/check.mjs +130 -40
- package/bin/commands/event-instructions.d.mts +11 -0
- package/bin/commands/event-instructions.mjs +27 -0
- package/bin/commands/event-mode.d.mts +14 -0
- package/bin/commands/event-mode.mjs +138 -0
- package/bin/commands/iterate/api-usage.mjs +4 -2
- package/bin/commands/iterate/base.mjs +4 -0
- package/bin/commands/iterate/check-blocker-gate.mjs +14 -2
- package/bin/commands/iterate/check-instructions.d.mts +9 -10
- package/bin/commands/iterate/check-instructions.mjs +23 -22
- package/bin/commands/iterate/classify.mjs +19 -5
- package/bin/commands/iterate/escalate.mjs +13 -3
- package/bin/commands/iterate/fix-code.mjs +26 -9
- package/bin/commands/iterate/fix-instruction-threads.d.mts +0 -8
- package/bin/commands/iterate/fix-instruction-threads.mjs +0 -16
- package/bin/commands/iterate/fix-loop-instruction.d.mts +9 -0
- package/bin/commands/iterate/fix-loop-instruction.mjs +35 -0
- package/bin/commands/iterate/helpers.mjs +15 -2
- package/bin/commands/iterate/index.mjs +26 -4
- package/bin/commands/iterate/mark-ready.mjs +50 -10
- package/bin/commands/iterate/merge-plan.d.mts +19 -0
- package/bin/commands/iterate/merge-plan.mjs +88 -0
- package/bin/commands/iterate/merge-state.mjs +3 -0
- package/bin/commands/iterate/merge.d.mts +4 -15
- package/bin/commands/iterate/merge.mjs +18 -51
- package/bin/commands/iterate/merged-base-pull-requests.d.mts +2 -6
- package/bin/commands/iterate/merged-base-pull-requests.mjs +15 -11
- package/bin/commands/iterate/native-stack-rebase.mjs +11 -0
- package/bin/commands/iterate/render.d.mts +8 -2
- package/bin/commands/iterate/render.mjs +28 -42
- package/bin/commands/iterate/stack-trunk-conflict.mjs +17 -1
- package/bin/commands/iterate/stale-ancestry.mjs +4 -1
- package/bin/commands/iterate/stall.d.mts +5 -1
- package/bin/commands/iterate/stall.mjs +19 -7
- package/bin/commands/iterate/thread-mutation-routing.d.mts +1 -0
- package/bin/commands/iterate/thread-mutation-routing.mjs +4 -2
- package/bin/commands/iterate/unreported-required.mjs +5 -2
- package/bin/commands/mark-files-as-viewed.mjs +7 -0
- package/bin/commands/next-check.d.mts +31 -0
- package/bin/commands/next-check.mjs +46 -0
- package/bin/commands/playbook.d.mts +13 -0
- package/bin/commands/playbook.mjs +48 -0
- package/bin/commands/poll-mode.d.mts +8 -0
- package/bin/commands/poll-mode.mjs +14 -0
- package/bin/commands/poll-progress.d.mts +1 -0
- package/bin/commands/poll-progress.mjs +6 -3
- package/bin/commands/poll-quota.d.mts +3 -2
- package/bin/commands/poll-quota.mjs +7 -3
- package/bin/commands/poll-summary-explicit-instructions.d.mts +2 -0
- package/bin/commands/poll-summary-explicit-instructions.mjs +10 -1
- package/bin/commands/poll-summary-instructions.mjs +16 -17
- package/bin/commands/poll-summary.mjs +42 -17
- package/bin/commands/poll.d.mts +0 -1
- package/bin/commands/poll.mjs +11 -19
- package/bin/commands/quota-selection.d.mts +2 -1
- package/bin/commands/quota-selection.mjs +12 -7
- package/bin/commands/resolve-mutate.mjs +33 -5
- package/bin/commands/rest-stack-merge-readiness.d.mts +4 -0
- package/bin/commands/rest-stack-merge-readiness.mjs +74 -0
- package/bin/commands/rule-auto-resolve.mjs +9 -3
- package/bin/commands/shepherd-journal.d.mts +5 -4
- package/bin/commands/shepherd-journal.mjs +9 -8
- package/bin/commands/stack-drain.d.mts +1 -9
- package/bin/commands/stack-drain.mjs +50 -37
- package/bin/commands/stack-handoffs.d.mts +10 -0
- package/bin/commands/stack-handoffs.mjs +25 -0
- package/bin/commands/stack-stall.d.mts +5 -0
- package/bin/commands/stack-stall.mjs +11 -0
- package/bin/commands/stack-work.d.mts +5 -0
- package/bin/commands/stack-work.mjs +32 -0
- package/bin/comments/existing-reply-scan.d.mts +17 -0
- package/bin/comments/existing-reply-scan.mjs +41 -0
- package/bin/comments/rate-limit.mjs +2 -2
- package/bin/comments/resolve.d.mts +7 -1
- package/bin/comments/resolve.mjs +108 -18
- package/bin/comments/rest-reply.d.mts +9 -0
- package/bin/comments/rest-reply.mjs +95 -0
- package/bin/comments/rest-review-mutations.d.mts +22 -0
- package/bin/comments/rest-review-mutations.mjs +125 -0
- package/bin/comments/review-visibility.d.mts +1 -1
- package/bin/comments/review-visibility.mjs +3 -2
- package/bin/comments/sha-poll.mjs +7 -3
- package/bin/comments/thread-visibility.mjs +10 -5
- package/bin/comments/uncertain-replies.d.mts +11 -0
- package/bin/comments/uncertain-replies.mjs +121 -0
- package/bin/comments/uncertain-reply-guards.d.mts +10 -0
- package/bin/comments/uncertain-reply-guards.mjs +94 -0
- package/bin/comments/visible-comments.d.mts +1 -1
- package/bin/comments/visible-comments.mjs +4 -2
- package/bin/config/load.d.mts +37 -1
- package/bin/config/load.mjs +73 -1
- package/bin/config.json +6 -2
- package/bin/exit-codes.d.mts +3 -0
- package/bin/exit-codes.mjs +8 -0
- package/bin/github/api-telemetry-aggregate.d.mts +15 -4
- package/bin/github/api-telemetry-aggregate.mjs +57 -18
- package/bin/github/api-telemetry.d.mts +4 -0
- package/bin/github/api-telemetry.mjs +22 -8
- package/bin/github/apply-review-preflight.d.mts +20 -0
- package/bin/github/apply-review-preflight.mjs +61 -0
- package/bin/github/base-behind-cache.d.mts +15 -0
- package/bin/github/base-behind-cache.mjs +18 -0
- package/bin/github/batch-parse-suites.d.mts +4 -2
- package/bin/github/batch-parse-suites.mjs +6 -1
- package/bin/github/batch-parser-helpers.mjs +40 -2
- package/bin/github/batch-parsers.mjs +1 -0
- package/bin/github/batch-raw-rules.d.mts +26 -0
- package/bin/github/batch-raw-types.d.mts +8 -6
- package/bin/github/batch-stack-evidence.d.mts +28 -0
- package/bin/github/batch-stack-evidence.mjs +62 -0
- package/bin/github/batch.d.mts +15 -2
- package/bin/github/batch.mjs +34 -17
- package/bin/github/check-annotations-batch.d.mts +0 -5
- package/bin/github/check-annotations-batch.mjs +25 -4
- package/bin/github/check-annotations.mjs +3 -1
- package/bin/github/client.d.mts +8 -19
- package/bin/github/client.mjs +45 -13
- package/bin/github/fingerprint-fields.d.mts +8 -0
- package/bin/github/fingerprint-fields.mjs +6 -1
- package/bin/github/fingerprint.d.mts +4 -2
- package/bin/github/fingerprint.mjs +6 -38
- package/bin/github/github-fetch.d.mts +2 -0
- package/bin/github/github-fetch.mjs +19 -0
- package/bin/github/gql/base-behind.gql +3 -0
- package/bin/github/gql/batch-pr-page.gql +16 -0
- package/bin/github/gql/batch-pr.gql +57 -0
- package/bin/github/gql/commit-check-contexts.gql +17 -0
- package/bin/github/gql/get-pr-body.gql +1 -0
- package/bin/github/gql/poll-stack-summary.gql +1 -0
- package/bin/github/gql/poll-summary-check-contexts.gql +5 -0
- package/bin/github/gql/poll-summary-fragment.gql +65 -15
- package/bin/github/gql/stack-handoff-check-runs.gql +42 -0
- package/bin/github/graphql-http.mjs +9 -4
- package/bin/github/graphql-internal-retry.d.mts +2 -0
- package/bin/github/graphql-internal-retry.mjs +1 -1
- package/bin/github/graphql-quota-probe.d.mts +6 -0
- package/bin/github/graphql-quota-probe.mjs +56 -0
- package/bin/github/graphql-session-refusal.d.mts +8 -0
- package/bin/github/graphql-session-refusal.mjs +14 -0
- package/bin/github/http-request.d.mts +1 -1
- package/bin/github/http-request.mjs +22 -6
- package/bin/github/http-utils.mjs +6 -0
- package/bin/github/merge-target-rules.d.mts +12 -3
- package/bin/github/merge-target-rules.mjs +71 -34
- package/bin/github/mutation-policy.d.mts +6 -0
- package/bin/github/mutation-policy.mjs +21 -0
- package/bin/github/poll-summary-annotation-probe.mjs +17 -10
- package/bin/github/poll-summary-check-blockers.d.mts +1 -1
- package/bin/github/poll-summary-check-blockers.mjs +2 -2
- package/bin/github/poll-summary-check-hydration.mjs +25 -10
- package/bin/github/poll-summary-checks.d.mts +2 -2
- package/bin/github/poll-summary-checks.mjs +9 -6
- package/bin/github/poll-summary-command.d.mts +2 -0
- package/bin/github/poll-summary-command.mjs +24 -0
- package/bin/github/poll-summary-fingerprint.d.mts +1 -1
- package/bin/github/poll-summary-fingerprint.mjs +4 -2
- package/bin/github/poll-summary-handoff-fields.d.mts +3 -0
- package/bin/github/poll-summary-handoff-fields.mjs +73 -0
- package/bin/github/poll-summary-projector.d.mts +1 -1
- package/bin/github/poll-summary-projector.mjs +17 -24
- package/bin/github/poll-summary-raw.d.mts +70 -10
- package/bin/github/poll-summary-readiness.mjs +4 -1
- package/bin/github/poll-summary-rest-queue.d.mts +3 -0
- package/bin/github/poll-summary-rest-queue.mjs +12 -0
- package/bin/github/poll-summary-review.d.mts +1 -1
- package/bin/github/poll-summary-review.mjs +23 -11
- package/bin/github/poll-summary-route.mjs +19 -3
- package/bin/github/poll-summary-stack-page.d.mts +11 -0
- package/bin/github/poll-summary-stack-page.mjs +27 -0
- package/bin/github/poll-summary-unreported.d.mts +3 -1
- package/bin/github/poll-summary-unreported.mjs +6 -3
- package/bin/github/poll-summary.d.mts +4 -1
- package/bin/github/poll-summary.mjs +47 -33
- package/bin/github/queries.d.mts +7 -3
- package/bin/github/queries.mjs +8 -4
- package/bin/github/rate-limit-sample.d.mts +7 -0
- package/bin/github/rate-limit-sample.mjs +12 -0
- package/bin/github/reply-recovery-read.d.mts +50 -0
- package/bin/github/reply-recovery-read.mjs +118 -0
- package/bin/github/reply-thread-transcripts.d.mts +0 -1
- package/bin/github/reply-thread-transcripts.mjs +24 -1
- package/bin/github/rest-annotation-read.d.mts +5 -0
- package/bin/github/rest-annotation-read.mjs +44 -0
- package/bin/github/rest-batch-read.d.mts +12 -0
- package/bin/github/rest-batch-read.mjs +57 -0
- package/bin/github/rest-batch-snapshot.d.mts +30 -0
- package/bin/github/rest-batch-snapshot.mjs +115 -0
- package/bin/github/rest-batch-unavailable.d.mts +5 -0
- package/bin/github/rest-batch-unavailable.mjs +16 -0
- package/bin/github/rest-check-read.d.mts +34 -0
- package/bin/github/rest-check-read.mjs +124 -0
- package/bin/github/rest-comment-authorship.d.mts +7 -0
- package/bin/github/rest-comment-authorship.mjs +11 -0
- package/bin/github/rest-conditional-scope.d.mts +20 -0
- package/bin/github/rest-conditional-scope.mjs +44 -0
- package/bin/github/rest-explicit-summary-read.d.mts +3 -0
- package/bin/github/rest-explicit-summary-read.mjs +8 -0
- package/bin/github/rest-feedback-read.d.mts +36 -0
- package/bin/github/rest-feedback-read.mjs +147 -0
- package/bin/github/rest-feedback-validation.d.mts +5 -0
- package/bin/github/rest-feedback-validation.mjs +28 -0
- package/bin/github/rest-http.d.mts +12 -0
- package/bin/github/rest-http.mjs +108 -23
- package/bin/github/rest-identities.d.mts +22 -0
- package/bin/github/rest-identities.mjs +134 -0
- package/bin/github/rest-merge-stack-guard.d.mts +14 -0
- package/bin/github/rest-merge-stack-guard.mjs +41 -0
- package/bin/github/rest-merge.d.mts +29 -0
- package/bin/github/rest-merge.mjs +56 -0
- package/bin/github/rest-merged-base-read.d.mts +9 -0
- package/bin/github/rest-merged-base-read.mjs +48 -0
- package/bin/github/rest-pr-core.d.mts +52 -0
- package/bin/github/rest-pr-core.mjs +60 -0
- package/bin/github/rest-reader-core.d.mts +16 -0
- package/bin/github/rest-reader-core.mjs +154 -0
- package/bin/github/rest-review-read.d.mts +11 -0
- package/bin/github/rest-review-read.mjs +47 -0
- package/bin/github/rest-rules-read.d.mts +9 -0
- package/bin/github/rest-rules-read.mjs +154 -0
- package/bin/github/rest-session-refusal.d.mts +5 -0
- package/bin/github/rest-session-refusal.mjs +21 -0
- package/bin/github/rest-snapshot-context.d.mts +10 -0
- package/bin/github/rest-snapshot-context.mjs +1 -0
- package/bin/github/rest-stack-read.d.mts +21 -0
- package/bin/github/rest-stack-read.mjs +47 -0
- package/bin/github/rest-stack-summary.d.mts +8 -0
- package/bin/github/rest-stack-summary.mjs +62 -0
- package/bin/github/rest-summary-read.d.mts +7 -0
- package/bin/github/rest-summary-read.mjs +112 -0
- package/bin/github/rest-terminal-batch.d.mts +14 -0
- package/bin/github/rest-terminal-batch.mjs +45 -0
- package/bin/github/rest-text.mjs +7 -3
- package/bin/github/rest-thread-read.d.mts +4 -0
- package/bin/github/rest-thread-read.mjs +35 -0
- package/bin/github/rest-viewer-read.d.mts +2 -0
- package/bin/github/rest-viewer-read.mjs +41 -0
- package/bin/github/stack-bottom.d.mts +3 -0
- package/bin/github/stack-bottom.mjs +14 -0
- package/bin/github/stack-handoff-context.d.mts +25 -0
- package/bin/github/stack-handoff-context.mjs +12 -0
- package/bin/github/stack-handoff-page.d.mts +17 -0
- package/bin/github/stack-handoff-page.mjs +306 -0
- package/bin/github/stack-handoff.d.mts +16 -0
- package/bin/github/stack-handoff.mjs +115 -0
- package/bin/github/stack-order.d.mts +6 -0
- package/bin/github/stack-order.mjs +13 -0
- package/bin/github/stack-read.d.mts +7 -5
- package/bin/github/stack-read.mjs +22 -24
- package/bin/github/stack-size-hint.d.mts +3 -0
- package/bin/github/stack-size-hint.mjs +14 -0
- package/bin/github/suggestion-thread.mjs +27 -1
- package/bin/github/thread-comments.mjs +9 -1
- package/bin/github/transport-evidence.d.mts +20 -0
- package/bin/github/transport-evidence.mjs +26 -0
- package/bin/github/transport-mode.d.mts +2 -0
- package/bin/github/transport-mode.mjs +5 -0
- package/bin/github/transport.d.mts +19 -0
- package/bin/github/transport.mjs +126 -0
- package/bin/github/unsupported-rest.d.mts +4 -0
- package/bin/github/unsupported-rest.mjs +7 -0
- package/bin/mcp/server.mjs +52 -3
- package/bin/merge-status/derive.d.mts +2 -0
- package/bin/merge-status/derive.mjs +17 -13
- package/bin/merge-status/required-check-requirements.d.mts +7 -0
- package/bin/merge-status/required-check-requirements.mjs +15 -0
- package/bin/merge-status/requirements-format.d.mts +5 -0
- package/bin/merge-status/requirements-format.mjs +41 -12
- package/bin/merge-status/requirements.mjs +18 -7
- package/bin/state/base.mjs +41 -0
- package/bin/state/durable-state.d.mts +6 -0
- package/bin/state/durable-state.mjs +41 -0
- package/bin/state/graphql-quota-policy.mjs +23 -16
- package/bin/state/merge-request.d.mts +25 -0
- package/bin/state/merge-request.mjs +145 -0
- package/bin/state/pr-fingerprint.d.mts +15 -0
- package/bin/state/pr-fingerprint.mjs +22 -7
- package/bin/state/rest-cache.d.mts +3 -0
- package/bin/state/rest-cache.mjs +1 -0
- package/bin/state/rest-snapshot-report.d.mts +21 -0
- package/bin/state/rest-snapshot-report.mjs +52 -0
- package/bin/state/seen-comments.d.mts +3 -0
- package/bin/state/seen-comments.mjs +18 -0
- package/bin/types/api-usage.d.mts +9 -1
- package/bin/types/escalate.d.mts +1 -1
- package/bin/types/github.d.mts +23 -9
- package/bin/types/iterate.d.mts +24 -2
- package/bin/types/merge-action.d.mts +1 -1
- package/bin/types/merge-requirements.d.mts +7 -7
- package/bin/types/next-check.d.mts +12 -0
- package/bin/types/next-check.mjs +1 -0
- package/bin/types/poll-summary.d.mts +21 -0
- package/bin/types/report.d.mts +13 -1
- package/bin/types/review-thread.d.mts +1 -1
- package/bin/types.d.mts +1 -0
- package/bin/types.mjs +1 -0
- package/package.json +3 -2
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- package/plugins/pr-shepherd/.mcp.json +1 -1
- package/plugins/pr-shepherd/skills/mark-files-as-viewed/SKILL.md +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +14 -51
- package/plugins/pr-shepherd/skills/pr-shepherd/references/ci-failure-triage.md +12 -16
- package/plugins/pr-shepherd/skills/pr-shepherd/references/cloud-event-loop.md +14 -0
- package/plugins/pr-shepherd/skills/pr-shepherd/references/create-pr.md +10 -0
- package/plugins/pr-shepherd/skills/pr-shepherd/references/fix-code-loop.md +10 -0
- package/plugins/pr-shepherd/skills/pr-shepherd/references/mcp-fallback.md +9 -0
- package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +2 -0
- package/bin/github/gql/pr-fingerprint.gql +0 -75
- package/plugins/pr-shepherd/skills/pr-shepherd/references/journal.md +0 -7
- package/plugins/pr-shepherd/skills/pr-shepherd/references/review-mutations.md +0 -9
- package/plugins/pr-shepherd/skills/pr-shepherd/references/stack-merge.md +0 -7
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { isCcrTransport } from "./transport.mjs";
|
|
2
|
+
import { GitHubRequestError } from "./errors.mjs";
|
|
3
|
+
import { EXIT } from "../exit-codes.mjs";
|
|
4
|
+
import { readRestPages, restRepoPath, restArray, restObject, restNumber, restBoolean, malformedRest, } from "./rest-reader-core.mjs";
|
|
5
|
+
import { recordRestIdentity, recordThreadIdentity, restThreadId } from "./rest-identities.mjs";
|
|
6
|
+
import { parseCreatedAt } from "./batch-parser-helpers.mjs";
|
|
7
|
+
import { restCommentAuthorFields as authorFields } from "./rest-comment-authorship.mjs";
|
|
8
|
+
import { validateRestReviews } from "./rest-review-read.mjs";
|
|
9
|
+
import { validateRestComments, feedbackIdsMatch } from "./rest-feedback-validation.mjs";
|
|
10
|
+
import { readRestViewerLogin } from "./rest-viewer-read.mjs";
|
|
11
|
+
export async function readRestFeedback(pr, repo, knownViewerLogin) {
|
|
12
|
+
const prefix = restRepoPath(repo);
|
|
13
|
+
let inline = (await readRestPages(`${prefix}/pulls/${pr}/comments`)).nodes;
|
|
14
|
+
const issue = (await readRestPages(`${prefix}/issues/${pr}/comments`)).nodes;
|
|
15
|
+
const reviews = (await readRestPages(`${prefix}/pulls/${pr}/reviews`)).nodes;
|
|
16
|
+
if (new Set(issue.map((comment) => comment.id)).size !== issue.length ||
|
|
17
|
+
new Set(reviews.map((review) => review.id)).size !== reviews.length)
|
|
18
|
+
malformedRest("duplicate feedback IDs");
|
|
19
|
+
validateRestReviews(reviews);
|
|
20
|
+
validateRestComments([...inline, ...issue, ...reviews]);
|
|
21
|
+
let statuses = await readRestThreadStatuses(`${prefix}/pulls/${pr}/ccr/review_threads`);
|
|
22
|
+
if (statuses && !feedbackIdsMatch(inline, statuses)) {
|
|
23
|
+
// A reviewer can add or delete comments between these independently paginated reads.
|
|
24
|
+
// Retry both resources, with thread membership first, before certifying the join.
|
|
25
|
+
statuses = await readRestThreadStatuses(`${prefix}/pulls/${pr}/ccr/review_threads`);
|
|
26
|
+
inline = (await readRestPages(`${prefix}/pulls/${pr}/comments`)).nodes;
|
|
27
|
+
validateRestComments(inline);
|
|
28
|
+
if (statuses && !feedbackIdsMatch(inline, statuses))
|
|
29
|
+
throw new GitHubRequestError("Pull request feedback changed while REST snapshot was being read; retry", { status: 409, exitCodeOverride: EXIT.TEMPFAIL });
|
|
30
|
+
}
|
|
31
|
+
for (const comment of [...inline, ...issue, ...reviews]) {
|
|
32
|
+
await recordRestIdentity(repo, pr, comment.node_id, String(comment.id), "state" in comment ? "review" : "comment");
|
|
33
|
+
}
|
|
34
|
+
let unavailable = [];
|
|
35
|
+
if (!statuses && inline.length > 0)
|
|
36
|
+
unavailable = [
|
|
37
|
+
{
|
|
38
|
+
field: "reviewThreads.status",
|
|
39
|
+
reason: "REST has no resolved/outdated thread state; CCR review_threads is unavailable",
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
const inlineMap = new Map(inline.map((comment) => [comment.id, comment]));
|
|
43
|
+
if (inlineMap.size !== inline.length)
|
|
44
|
+
malformedRest("duplicate inline comment IDs");
|
|
45
|
+
const groups = [];
|
|
46
|
+
if (statuses) {
|
|
47
|
+
for (const status of statuses) {
|
|
48
|
+
const comments = status.comment_ids.map((id) => inlineMap.get(id));
|
|
49
|
+
const root = comments[0];
|
|
50
|
+
if (root.in_reply_to_id != null)
|
|
51
|
+
malformedRest("CCR thread root is a reply");
|
|
52
|
+
for (const reply of comments.slice(1))
|
|
53
|
+
if (reply.in_reply_to_id !== root.id)
|
|
54
|
+
malformedRest("CCR reply does not belong to its root");
|
|
55
|
+
groups.push({ id: root.id, comments, status });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
for (const comment of inline.filter((comment) => comment.in_reply_to_id == null)) {
|
|
60
|
+
groups.push({
|
|
61
|
+
id: comment.id,
|
|
62
|
+
comments: [comment, ...inline.filter((reply) => reply.in_reply_to_id === comment.id)],
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (groups.reduce((sum, group) => sum + group.comments.length, 0) !== inline.length)
|
|
66
|
+
malformedRest("inline comment reply has no root");
|
|
67
|
+
}
|
|
68
|
+
const threads = [];
|
|
69
|
+
const viewerLogin = knownViewerLogin === undefined ? await readRestViewerLogin() : knownViewerLogin;
|
|
70
|
+
for (const group of groups) {
|
|
71
|
+
const root = group.comments[0];
|
|
72
|
+
const id = restThreadId(String(group.id));
|
|
73
|
+
await recordThreadIdentity(repo, pr, id, String(group.id));
|
|
74
|
+
const review = reviews.find((review) => review.id === root.pull_request_review_id);
|
|
75
|
+
threads.push({
|
|
76
|
+
id,
|
|
77
|
+
...(group.status
|
|
78
|
+
? { isResolved: group.status.resolved, isOutdated: group.status.outdated }
|
|
79
|
+
: {}),
|
|
80
|
+
path: group.status ? group.status.path : (root.path ?? null),
|
|
81
|
+
line: group.status ? group.status.line : (root.line ?? null),
|
|
82
|
+
startLine: root.start_line ?? null,
|
|
83
|
+
...(review ? { reviewId: review.node_id } : {}),
|
|
84
|
+
...authorFields(root, viewerLogin),
|
|
85
|
+
body: root.body ?? "",
|
|
86
|
+
url: root.html_url,
|
|
87
|
+
createdAtUnix: parseCreatedAt(root.created_at),
|
|
88
|
+
comments: group.comments.map((comment) => ({
|
|
89
|
+
id: comment.node_id,
|
|
90
|
+
...(review ? { reviewId: review.node_id } : {}),
|
|
91
|
+
...authorFields(comment, viewerLogin),
|
|
92
|
+
body: comment.body ?? "",
|
|
93
|
+
url: comment.html_url,
|
|
94
|
+
createdAtUnix: parseCreatedAt(comment.created_at),
|
|
95
|
+
})),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const comments = issue.map((comment) => ({
|
|
99
|
+
id: comment.node_id,
|
|
100
|
+
...authorFields(comment),
|
|
101
|
+
body: comment.body ?? "",
|
|
102
|
+
url: comment.html_url,
|
|
103
|
+
createdAtUnix: parseCreatedAt(comment.created_at),
|
|
104
|
+
}));
|
|
105
|
+
return { threads, comments, reviews, unavailable, viewerLogin };
|
|
106
|
+
}
|
|
107
|
+
async function readRestThreadStatuses(path) {
|
|
108
|
+
if (!isCcrTransport())
|
|
109
|
+
return undefined;
|
|
110
|
+
try {
|
|
111
|
+
const statuses = (await readRestPages(path)).nodes;
|
|
112
|
+
for (const status of statuses) {
|
|
113
|
+
restObject(status, "CCR thread");
|
|
114
|
+
restBoolean(status.resolved, "CCR thread resolved");
|
|
115
|
+
restBoolean(status.outdated, "CCR thread outdated");
|
|
116
|
+
restArray(status.comment_ids, "CCR comment_ids");
|
|
117
|
+
if (status.path !== null && typeof status.path !== "string")
|
|
118
|
+
malformedRest("CCR thread path");
|
|
119
|
+
if (status.line !== null)
|
|
120
|
+
restNumber(status.line, "CCR thread line");
|
|
121
|
+
if (status.comment_ids.length === 0)
|
|
122
|
+
malformedRest("CCR thread has no root comment");
|
|
123
|
+
for (const id of status.comment_ids)
|
|
124
|
+
restNumber(id, "CCR comment ID");
|
|
125
|
+
}
|
|
126
|
+
return statuses;
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (!(error instanceof GitHubRequestError) || error.status !== 404)
|
|
130
|
+
throw error;
|
|
131
|
+
// The proxy answers a missing PR with 404 too; that is not an unavailable route.
|
|
132
|
+
if (/could not resolve to a pullrequest/i.test(error.responseMessage ?? error.message)) {
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export function restReviewToReview(review) {
|
|
139
|
+
return {
|
|
140
|
+
id: review.node_id,
|
|
141
|
+
...authorFields(review),
|
|
142
|
+
body: review.body ?? "",
|
|
143
|
+
url: review.html_url,
|
|
144
|
+
...(review.commit_id && { commitOid: review.commit_id }),
|
|
145
|
+
...(review.submitted_at && { createdAtUnix: parseCreatedAt(review.submitted_at) }),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { restObject, restString, restNumber, malformedRest } from "./rest-reader-core.mjs";
|
|
2
|
+
export function validateRestComments(comments) {
|
|
3
|
+
for (const comment of comments) {
|
|
4
|
+
restObject(comment, "feedback comment");
|
|
5
|
+
restString(comment.html_url, "feedback html_url");
|
|
6
|
+
if (!("state" in comment)) {
|
|
7
|
+
restString(comment.created_at, "comment created_at");
|
|
8
|
+
restString(comment.updated_at, "comment updated_at");
|
|
9
|
+
if (!Number.isFinite(Date.parse(comment.created_at)) ||
|
|
10
|
+
!Number.isFinite(Date.parse(comment.updated_at)))
|
|
11
|
+
malformedRest("feedback timestamps");
|
|
12
|
+
}
|
|
13
|
+
restNumber(comment.id, "feedback id");
|
|
14
|
+
restString(comment.node_id, "feedback node_id");
|
|
15
|
+
if (typeof comment.body !== "string" && comment.body !== null)
|
|
16
|
+
malformedRest("feedback body");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function feedbackIdsMatch(inline, statuses) {
|
|
20
|
+
const inlineIds = new Set(inline.map((comment) => comment.id));
|
|
21
|
+
if (inlineIds.size !== inline.length)
|
|
22
|
+
malformedRest("duplicate inline comment IDs");
|
|
23
|
+
const threadIds = statuses.flatMap((status) => status.comment_ids);
|
|
24
|
+
const covered = new Set(threadIds);
|
|
25
|
+
if (covered.size !== threadIds.length)
|
|
26
|
+
malformedRest("repeated CCR thread comment IDs");
|
|
27
|
+
return covered.size === inlineIds.size && threadIds.every((id) => inlineIds.has(id));
|
|
28
|
+
}
|
|
@@ -3,6 +3,12 @@ import { type RateLimitInfo } from "./http-utils.mts";
|
|
|
3
3
|
export interface RestResult<T = unknown> {
|
|
4
4
|
data: T;
|
|
5
5
|
rateLimit?: RateLimitInfo;
|
|
6
|
+
/** HTTP status, including documented non-2xx statuses accepted by the caller. */
|
|
7
|
+
status?: number;
|
|
8
|
+
/** Validator of a conditional read: the stored ETag on a 304, the response ETag on a 200. */
|
|
9
|
+
etag?: string;
|
|
10
|
+
/** GitHub pagination header, retained for named REST readers. */
|
|
11
|
+
link?: string;
|
|
6
12
|
}
|
|
7
13
|
/**
|
|
8
14
|
* Enables `If-None-Match` conditional requests for a REST call, backed by a
|
|
@@ -11,11 +17,17 @@ export interface RestResult<T = unknown> {
|
|
|
11
17
|
* the primary REST rate-limit quota.
|
|
12
18
|
*/
|
|
13
19
|
export interface RestRequestOptions {
|
|
20
|
+
/** Endpoints such as merge-async return useful state on documented non-2xx responses. */
|
|
21
|
+
acceptStatuses?: readonly number[];
|
|
14
22
|
conditional?: {
|
|
15
23
|
key: StateKey;
|
|
16
24
|
/** Logical cache name; must be unique per distinct resource + page. */
|
|
17
25
|
name: string;
|
|
18
26
|
headSha?: string;
|
|
27
|
+
/** Return false to keep a 200 body out of the cache (e.g. a still-computing resource). */
|
|
28
|
+
shouldStore?: (body: unknown) => boolean;
|
|
29
|
+
/** Send no validator, forcing a fresh body that then replaces the cached entry. */
|
|
30
|
+
revalidate?: boolean;
|
|
19
31
|
};
|
|
20
32
|
}
|
|
21
33
|
export declare function rest<T = unknown>(method: string, path: string, body?: unknown): Promise<T>;
|
package/bin/github/rest-http.mjs
CHANGED
|
@@ -3,11 +3,13 @@ import { appendEntry, nextEntry } from "../log/log-file.mjs";
|
|
|
3
3
|
import { formatRequestEntry, formatResponseEntry } from "../log/session.mjs";
|
|
4
4
|
import { loadEtagEntry, storeEtagEntry } from "../state/rest-cache.mjs";
|
|
5
5
|
import { GitHubRequestError } from "./errors.mjs";
|
|
6
|
+
import { githubFetch } from "./github-fetch.mjs";
|
|
6
7
|
import { makeAuthHeaders } from "./http-auth.mjs";
|
|
7
8
|
import { requestWithTokenRetry } from "./http-request.mjs";
|
|
8
9
|
import { parseRateLimit, parseRetryAfter, redactToken, sanitizeBody, } from "./http-utils.mjs";
|
|
9
10
|
import { recordApiTelemetry } from "./api-telemetry.mjs";
|
|
10
11
|
import { recordIntermediateResponse } from "./http-intermediate.mjs";
|
|
12
|
+
import { isRestSessionRefusalResponse } from "./rest-session-refusal.mjs";
|
|
11
13
|
const BASE_URL = "https://api.github.com";
|
|
12
14
|
const SAFE_GITHUB_REST_PATH = /^\/[A-Za-z0-9._~!$&'()*+,;=:@%/?-]*$/;
|
|
13
15
|
function githubApiUrl(path) {
|
|
@@ -34,13 +36,15 @@ export async function restWithRateLimit(method, path, body, opts) {
|
|
|
34
36
|
appendEntry(formatRequestEntry({ n, kind: "REST", method, url, body }));
|
|
35
37
|
const t0 = performance.now();
|
|
36
38
|
let authSource = "unknown";
|
|
37
|
-
|
|
39
|
+
let credentialFingerprint;
|
|
40
|
+
const cached = opts?.conditional && !opts.conditional.revalidate
|
|
38
41
|
? await loadEtagEntry(opts.conditional.key, opts.conditional.name)
|
|
39
42
|
: null;
|
|
40
43
|
const { res, attempt, retryT0 } = await requestWithTokenRetry(async () => {
|
|
41
44
|
const auth = await makeAuthHeaders(cached ? { "If-None-Match": cached.etag } : undefined);
|
|
42
45
|
authSource = auth.source;
|
|
43
|
-
|
|
46
|
+
credentialFingerprint = auth.fingerprint;
|
|
47
|
+
return githubFetch(url, {
|
|
44
48
|
method,
|
|
45
49
|
headers: auth.headers,
|
|
46
50
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
@@ -53,7 +57,8 @@ export async function restWithRateLimit(method, path, body, opts) {
|
|
|
53
57
|
response,
|
|
54
58
|
durationMs,
|
|
55
59
|
authSource,
|
|
56
|
-
|
|
60
|
+
credentialFingerprint,
|
|
61
|
+
}), method === "GET" || method === "HEAD");
|
|
57
62
|
const durationMs = Math.round(performance.now() - retryT0);
|
|
58
63
|
const rateLimit = parseRateLimit(res.headers) ?? undefined;
|
|
59
64
|
const retryAfterSeconds = parseRetryAfter(res.headers);
|
|
@@ -73,11 +78,25 @@ export async function restWithRateLimit(method, path, body, opts) {
|
|
|
73
78
|
rateLimit,
|
|
74
79
|
retryAfterSeconds,
|
|
75
80
|
}));
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
// A 304 consumes no quota: telemetry counts it as notModified, not as a request.
|
|
82
|
+
recordApiTelemetry({
|
|
83
|
+
kind: "REST",
|
|
84
|
+
method,
|
|
85
|
+
authSource,
|
|
86
|
+
credentialFingerprint,
|
|
87
|
+
rateLimit,
|
|
88
|
+
notModified: true,
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
data: cached.body,
|
|
92
|
+
rateLimit,
|
|
93
|
+
status: res.status,
|
|
94
|
+
etag: cached.etag,
|
|
95
|
+
link: res.headers.get("link") ?? cached.link,
|
|
96
|
+
};
|
|
78
97
|
}
|
|
79
98
|
const ct = res.headers.get("content-type") ?? "";
|
|
80
|
-
if (!res.ok) {
|
|
99
|
+
if (!res.ok && !opts?.acceptStatuses?.includes(res.status)) {
|
|
81
100
|
const text = await res.text();
|
|
82
101
|
appendEntry(formatResponseEntry({
|
|
83
102
|
n,
|
|
@@ -92,17 +111,63 @@ export async function restWithRateLimit(method, path, body, opts) {
|
|
|
92
111
|
rateLimit,
|
|
93
112
|
retryAfterSeconds,
|
|
94
113
|
}));
|
|
95
|
-
recordApiTelemetry({
|
|
96
|
-
|
|
114
|
+
recordApiTelemetry({
|
|
115
|
+
kind: "REST",
|
|
116
|
+
method,
|
|
117
|
+
authSource,
|
|
118
|
+
credentialFingerprint,
|
|
119
|
+
rateLimit,
|
|
120
|
+
});
|
|
121
|
+
const responseMessage = isRestSessionRefusalResponse(res.status, text)
|
|
122
|
+
? redactToken(text)
|
|
123
|
+
: sanitizeBody(text);
|
|
124
|
+
throw new GitHubRequestError(`GitHub REST ${method} ${path} failed: ${res.status} ${responseMessage}`, {
|
|
97
125
|
status: res.status,
|
|
98
126
|
rateLimit,
|
|
99
127
|
retryAfterSeconds,
|
|
100
128
|
authSource,
|
|
101
|
-
responseMessage
|
|
129
|
+
responseMessage,
|
|
102
130
|
});
|
|
103
131
|
}
|
|
104
132
|
if (ct.includes("application/json")) {
|
|
105
|
-
|
|
133
|
+
let json;
|
|
134
|
+
try {
|
|
135
|
+
json = (await res.json());
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
const detail = err instanceof Error ? `: ${err.message}` : "";
|
|
139
|
+
appendEntry(formatResponseEntry({
|
|
140
|
+
n,
|
|
141
|
+
kind: "REST",
|
|
142
|
+
method,
|
|
143
|
+
url,
|
|
144
|
+
status: res.status,
|
|
145
|
+
durationMs,
|
|
146
|
+
contentType: ct,
|
|
147
|
+
textBody: `Invalid JSON response${detail}`,
|
|
148
|
+
attempt: attempt > 1 ? attempt : undefined,
|
|
149
|
+
authSource,
|
|
150
|
+
rateLimit,
|
|
151
|
+
retryAfterSeconds,
|
|
152
|
+
}));
|
|
153
|
+
recordApiTelemetry({
|
|
154
|
+
kind: "REST",
|
|
155
|
+
method,
|
|
156
|
+
authSource,
|
|
157
|
+
credentialFingerprint,
|
|
158
|
+
rateLimit,
|
|
159
|
+
});
|
|
160
|
+
if (!res.ok) {
|
|
161
|
+
throw new GitHubRequestError(`GitHub REST ${method} ${path} failed: ${res.status} Invalid JSON response${detail}`, {
|
|
162
|
+
status: res.status,
|
|
163
|
+
rateLimit,
|
|
164
|
+
retryAfterSeconds,
|
|
165
|
+
authSource,
|
|
166
|
+
responseMessage: `Invalid JSON response${detail}`,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
throw err;
|
|
170
|
+
}
|
|
106
171
|
appendEntry(formatResponseEntry({
|
|
107
172
|
n,
|
|
108
173
|
kind: "REST",
|
|
@@ -117,18 +182,32 @@ export async function restWithRateLimit(method, path, body, opts) {
|
|
|
117
182
|
rateLimit,
|
|
118
183
|
retryAfterSeconds,
|
|
119
184
|
}));
|
|
120
|
-
recordApiTelemetry({
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
185
|
+
recordApiTelemetry({
|
|
186
|
+
kind: "REST",
|
|
187
|
+
method,
|
|
188
|
+
authSource,
|
|
189
|
+
credentialFingerprint,
|
|
190
|
+
rateLimit,
|
|
191
|
+
});
|
|
192
|
+
const etag = opts?.conditional ? (res.headers.get("etag") ?? undefined) : undefined;
|
|
193
|
+
const link = res.headers.get("link") ?? undefined;
|
|
194
|
+
if (opts?.conditional && etag && (opts.conditional.shouldStore?.(json) ?? true)) {
|
|
195
|
+
await storeEtagEntry(opts.conditional.key, opts.conditional.name, {
|
|
196
|
+
etag,
|
|
197
|
+
body: json,
|
|
198
|
+
...(opts.conditional.headSha !== undefined && {
|
|
199
|
+
headSha: opts.conditional.headSha,
|
|
200
|
+
}),
|
|
201
|
+
...(link !== undefined && { link }),
|
|
202
|
+
});
|
|
130
203
|
}
|
|
131
|
-
return {
|
|
204
|
+
return {
|
|
205
|
+
data: json,
|
|
206
|
+
rateLimit,
|
|
207
|
+
status: res.status,
|
|
208
|
+
...(etag !== undefined && { etag }),
|
|
209
|
+
link,
|
|
210
|
+
};
|
|
132
211
|
}
|
|
133
212
|
appendEntry(formatResponseEntry({
|
|
134
213
|
n,
|
|
@@ -143,6 +222,12 @@ export async function restWithRateLimit(method, path, body, opts) {
|
|
|
143
222
|
rateLimit,
|
|
144
223
|
retryAfterSeconds,
|
|
145
224
|
}));
|
|
146
|
-
recordApiTelemetry({
|
|
147
|
-
|
|
225
|
+
recordApiTelemetry({
|
|
226
|
+
kind: "REST",
|
|
227
|
+
method,
|
|
228
|
+
authSource,
|
|
229
|
+
credentialFingerprint,
|
|
230
|
+
rateLimit,
|
|
231
|
+
});
|
|
232
|
+
return { data: undefined, rateLimit, status: res.status };
|
|
148
233
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SeenMarker } from "../state/seen-comments.mts";
|
|
2
|
+
import type { RepoInfo } from "./client.mts";
|
|
3
|
+
import type { FixAttemptsState } from "../state/fix-attempts.mts";
|
|
4
|
+
type IdentityKind = "check" | "review" | "comment" | "pull" | "thread";
|
|
5
|
+
interface RestIdentity {
|
|
6
|
+
repo: RepoInfo;
|
|
7
|
+
pr: number;
|
|
8
|
+
numericId: string;
|
|
9
|
+
kind: IdentityKind;
|
|
10
|
+
graphqlThreadId?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function restThreadId(rootCommentId: string | number): string;
|
|
13
|
+
export declare function recordRestIdentity(repo: RepoInfo, pr: number, nodeId: string, numericId: string | number, kind: IdentityKind): Promise<void>;
|
|
14
|
+
export declare function resolveRestIdentity(id: string, kind?: IdentityKind): Promise<RestIdentity>;
|
|
15
|
+
export declare function recordThreadIdentity(repo: RepoInfo, pr: number, graphqlThreadId: string, rootCommentId: string | number): Promise<void>;
|
|
16
|
+
export declare function resolveRestThreadRoot(repo: RepoInfo, pr: number, id: string): Promise<string>;
|
|
17
|
+
export declare function resolveGraphqlThreadId(id: string): Promise<string>;
|
|
18
|
+
/** Preserve previously displayed feedback when the adapter changes its thread handle. */
|
|
19
|
+
export declare function aliasThreadSeenMarkers(repo: RepoInfo, pr: number, ids: readonly string[], seen: Map<string, SeenMarker>): Promise<void>;
|
|
20
|
+
/** Keep fix-attempt counts tied to the same feedback when its transport handle changes. */
|
|
21
|
+
export declare function aliasThreadFixAttempts(repo: RepoInfo, pr: number, ids: readonly string[], state: FixAttemptsState | null): Promise<void>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { resolveStateBase } from "../state/base.mjs";
|
|
5
|
+
function identityPath(id) {
|
|
6
|
+
const hash = createHash("sha256").update(id).digest("hex");
|
|
7
|
+
return join(resolveStateBase(), "github-identities", `${hash}.json`);
|
|
8
|
+
}
|
|
9
|
+
function numeric(value) {
|
|
10
|
+
const result = String(value);
|
|
11
|
+
if (!/^[1-9][0-9]*$/.test(result))
|
|
12
|
+
throw new Error(`Invalid GitHub numeric ID: ${result}`);
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
export function restThreadId(rootCommentId) {
|
|
16
|
+
return `rest-thread-${numeric(rootCommentId)}`;
|
|
17
|
+
}
|
|
18
|
+
async function storeIdentity(id, entry) {
|
|
19
|
+
await mkdir(join(resolveStateBase(), "github-identities"), { recursive: true });
|
|
20
|
+
await writeFile(identityPath(id), JSON.stringify({ id, ...entry }));
|
|
21
|
+
}
|
|
22
|
+
export async function recordRestIdentity(repo, pr, nodeId, numericId, kind) {
|
|
23
|
+
if (!nodeId)
|
|
24
|
+
return;
|
|
25
|
+
await storeIdentity(nodeId, { repo, pr, numericId: numeric(numericId), kind });
|
|
26
|
+
}
|
|
27
|
+
export async function resolveRestIdentity(id, kind) {
|
|
28
|
+
let entry;
|
|
29
|
+
try {
|
|
30
|
+
entry = JSON.parse(await readFile(identityPath(id), "utf8"));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new Error(`No recorded REST identity for ${id}; refresh this PR with REST transport first`);
|
|
34
|
+
}
|
|
35
|
+
if (entry.id !== id ||
|
|
36
|
+
!entry.repo?.owner ||
|
|
37
|
+
!entry.repo.name ||
|
|
38
|
+
!Number.isSafeInteger(entry.pr) ||
|
|
39
|
+
entry.pr < (entry.kind === "check" ? 0 : 1) ||
|
|
40
|
+
!/^[1-9][0-9]*$/.test(entry.numericId) ||
|
|
41
|
+
(kind !== undefined && entry.kind !== kind))
|
|
42
|
+
throw new Error(`Invalid REST identity for ${id}`);
|
|
43
|
+
return entry;
|
|
44
|
+
}
|
|
45
|
+
export async function recordThreadIdentity(repo, pr, graphqlThreadId, rootCommentId) {
|
|
46
|
+
const numericId = numeric(rootCommentId);
|
|
47
|
+
const canonical = restThreadId(numericId);
|
|
48
|
+
let existing;
|
|
49
|
+
try {
|
|
50
|
+
existing = await resolveRestIdentity(canonical, "thread");
|
|
51
|
+
}
|
|
52
|
+
catch { }
|
|
53
|
+
const genuineId = graphqlThreadId.startsWith("rest-thread-")
|
|
54
|
+
? existing?.graphqlThreadId
|
|
55
|
+
: graphqlThreadId;
|
|
56
|
+
const entry = {
|
|
57
|
+
repo,
|
|
58
|
+
pr,
|
|
59
|
+
numericId,
|
|
60
|
+
kind: "thread",
|
|
61
|
+
...(genuineId && { graphqlThreadId: genuineId }),
|
|
62
|
+
};
|
|
63
|
+
if (genuineId)
|
|
64
|
+
await storeIdentity(genuineId, entry);
|
|
65
|
+
await storeIdentity(canonical, entry);
|
|
66
|
+
}
|
|
67
|
+
function samePr(entry, repo, pr) {
|
|
68
|
+
return (entry.pr === pr &&
|
|
69
|
+
entry.repo.owner.toLowerCase() === repo.owner.toLowerCase() &&
|
|
70
|
+
entry.repo.name.toLowerCase() === repo.name.toLowerCase());
|
|
71
|
+
}
|
|
72
|
+
export async function resolveRestThreadRoot(repo, pr, id) {
|
|
73
|
+
const match = /^rest-thread-([1-9][0-9]*)$/.exec(id);
|
|
74
|
+
if (match)
|
|
75
|
+
return match[1];
|
|
76
|
+
const entry = await resolveRestIdentity(id, "thread");
|
|
77
|
+
if (!samePr(entry, repo, pr))
|
|
78
|
+
throw new Error(`Thread ${id} does not belong to ${repo.owner}/${repo.name}#${pr}`);
|
|
79
|
+
return entry.numericId;
|
|
80
|
+
}
|
|
81
|
+
export async function resolveGraphqlThreadId(id) {
|
|
82
|
+
if (!id.startsWith("rest-thread-"))
|
|
83
|
+
return id;
|
|
84
|
+
const entry = await resolveRestIdentity(id, "thread");
|
|
85
|
+
if (!entry.graphqlThreadId)
|
|
86
|
+
throw new Error(`No GraphQL thread identity for ${id}; use --transport rest`);
|
|
87
|
+
return entry.graphqlThreadId;
|
|
88
|
+
}
|
|
89
|
+
/** Preserve previously displayed feedback when the adapter changes its thread handle. */
|
|
90
|
+
export async function aliasThreadSeenMarkers(repo, pr, ids, seen) {
|
|
91
|
+
await Promise.all(ids.map(async (id) => {
|
|
92
|
+
try {
|
|
93
|
+
const entry = await resolveRestIdentity(id, "thread");
|
|
94
|
+
if (!samePr(entry, repo, pr))
|
|
95
|
+
return;
|
|
96
|
+
const aliases = [id, restThreadId(entry.numericId), entry.graphqlThreadId].filter((alias) => alias !== undefined);
|
|
97
|
+
const marker = aliases
|
|
98
|
+
.map((alias) => seen.get(alias))
|
|
99
|
+
.find((candidate) => candidate !== undefined);
|
|
100
|
+
if (marker)
|
|
101
|
+
for (const alias of aliases)
|
|
102
|
+
if (!seen.has(alias))
|
|
103
|
+
seen.set(alias, marker);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Old snapshots may have no association. Surface again rather than hide an unseen item.
|
|
107
|
+
}
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
/** Keep fix-attempt counts tied to the same feedback when its transport handle changes. */
|
|
111
|
+
export async function aliasThreadFixAttempts(repo, pr, ids, state) {
|
|
112
|
+
if (!state?.threadBodyHashes)
|
|
113
|
+
return;
|
|
114
|
+
await Promise.all(ids.map(async (id) => {
|
|
115
|
+
try {
|
|
116
|
+
const entry = await resolveRestIdentity(id, "thread");
|
|
117
|
+
if (!samePr(entry, repo, pr))
|
|
118
|
+
return;
|
|
119
|
+
const aliases = [id, restThreadId(entry.numericId), entry.graphqlThreadId].filter((alias) => alias !== undefined);
|
|
120
|
+
const source = aliases.find((alias) => state.threadBodyHashes?.[alias] !== undefined);
|
|
121
|
+
if (!source)
|
|
122
|
+
return;
|
|
123
|
+
const bodyHash = state.threadBodyHashes[source];
|
|
124
|
+
const attempts = Math.max(...aliases
|
|
125
|
+
.filter((alias) => state.threadBodyHashes?.[alias] === bodyHash)
|
|
126
|
+
.map((alias) => state.threadAttempts[alias] ?? 0));
|
|
127
|
+
state.threadBodyHashes[id] = bodyHash;
|
|
128
|
+
state.threadAttempts[id] = attempts;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* Unmapped older handles retain their ordinary first-look behavior. */
|
|
132
|
+
}
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Native stack and exact lower prefix observed when the merge command was generated. */
|
|
2
|
+
export interface RestMergeStackGuard {
|
|
3
|
+
number: number;
|
|
4
|
+
baseRefName: string;
|
|
5
|
+
prefix: Array<{
|
|
6
|
+
pr: number;
|
|
7
|
+
headRefName: string;
|
|
8
|
+
headRefOid: string;
|
|
9
|
+
baseRefName: string;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
export declare function validateRestMergeStackGuard(value: unknown): asserts value is RestMergeStackGuard;
|
|
13
|
+
/** Stable equality ignores object key order and unknown caller fields. */
|
|
14
|
+
export declare function restMergeStackGuardKey(guard: RestMergeStackGuard | undefined): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function validateRestMergeStackGuard(value) {
|
|
2
|
+
const guard = value;
|
|
3
|
+
if (!guard ||
|
|
4
|
+
typeof guard !== "object" ||
|
|
5
|
+
Array.isArray(guard) ||
|
|
6
|
+
!Number.isSafeInteger(guard.number) ||
|
|
7
|
+
guard.number < 1 ||
|
|
8
|
+
typeof guard.baseRefName !== "string" ||
|
|
9
|
+
!guard.baseRefName ||
|
|
10
|
+
!Array.isArray(guard.prefix) ||
|
|
11
|
+
guard.prefix.length === 0 ||
|
|
12
|
+
guard.prefix.length > 100_000 ||
|
|
13
|
+
guard.prefix.some((member) => !member ||
|
|
14
|
+
typeof member !== "object" ||
|
|
15
|
+
Array.isArray(member) ||
|
|
16
|
+
!Number.isSafeInteger(member.pr) ||
|
|
17
|
+
member.pr < 1 ||
|
|
18
|
+
typeof member.headRefName !== "string" ||
|
|
19
|
+
!member.headRefName ||
|
|
20
|
+
typeof member.baseRefName !== "string" ||
|
|
21
|
+
!member.baseRefName ||
|
|
22
|
+
typeof member.headRefOid !== "string" ||
|
|
23
|
+
!/^[0-9a-f]{40}$/.test(member.headRefOid)) ||
|
|
24
|
+
new Set(guard.prefix.map((member) => member.pr)).size !== guard.prefix.length)
|
|
25
|
+
throw new Error("expectedStack must bind a positive stack number, trunk, and unique ordered PR prefix with full head SHAs and branch refs");
|
|
26
|
+
}
|
|
27
|
+
/** Stable equality ignores object key order and unknown caller fields. */
|
|
28
|
+
export function restMergeStackGuardKey(guard) {
|
|
29
|
+
return JSON.stringify(guard
|
|
30
|
+
? [
|
|
31
|
+
guard.number,
|
|
32
|
+
guard.baseRefName,
|
|
33
|
+
guard.prefix.map((member) => [
|
|
34
|
+
member.pr,
|
|
35
|
+
member.headRefName,
|
|
36
|
+
member.headRefOid,
|
|
37
|
+
member.baseRefName,
|
|
38
|
+
]),
|
|
39
|
+
]
|
|
40
|
+
: null);
|
|
41
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RepoInfo } from "./client.mts";
|
|
2
|
+
import type { MergeMethod } from "../config/merge-method.mts";
|
|
3
|
+
import type { RestMergeStackGuard } from "./rest-merge-stack-guard.mts";
|
|
4
|
+
export type { RestMergeStackGuard } from "./rest-merge-stack-guard.mts";
|
|
5
|
+
export type RestMergeAction = "direct_merge" | "merge_queue" | "default";
|
|
6
|
+
export interface RestMergeOptions {
|
|
7
|
+
requireSha: string;
|
|
8
|
+
mergeAction: RestMergeAction;
|
|
9
|
+
mergeMethod?: MergeMethod;
|
|
10
|
+
expectedStack?: RestMergeStackGuard;
|
|
11
|
+
}
|
|
12
|
+
export interface RestMergeResponse {
|
|
13
|
+
status: "pending" | "enqueued" | "merged" | "failed";
|
|
14
|
+
details: {
|
|
15
|
+
message?: string;
|
|
16
|
+
uuid?: string;
|
|
17
|
+
expected_head_sha?: string;
|
|
18
|
+
merge_action?: RestMergeAction;
|
|
19
|
+
merge_method?: MergeMethod;
|
|
20
|
+
bypass_rules?: boolean;
|
|
21
|
+
sha?: string;
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** This named mutation is never retried after a network failure or server error. */
|
|
26
|
+
export declare function requestRestMerge(repo: RepoInfo, pr: number, options: RestMergeOptions): Promise<RestMergeResponse>;
|
|
27
|
+
export declare function getRestMerge(repo: RepoInfo, pr: number, uuid: string): Promise<RestMergeResponse>;
|
|
28
|
+
/** Conflict requests must match the caller's guarded options before their UUID can be adopted. */
|
|
29
|
+
export declare function matchesRestMergeRequest(result: RestMergeResponse, options: RestMergeOptions): boolean;
|