pr-shepherd 0.56.5 → 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 +40 -19
- 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.d.mts +6 -5
- package/bin/commands/iterate/native-stack-rebase.mjs +24 -7
- package/bin/commands/iterate/render.d.mts +8 -2
- package/bin/commands/iterate/render.mjs +28 -42
- package/bin/commands/iterate/stack-trunk-conflict.d.mts +4 -2
- package/bin/commands/iterate/stack-trunk-conflict.mjs +44 -19
- 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/gql/upper-layer-conflict-target.gql +0 -15
- 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 +16 -3
- package/bin/github/merge-target-rules.mjs +79 -44
- 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 +6 -3
- package/bin/github/poll-summary-unreported.mjs +15 -7
- package/bin/github/poll-summary.d.mts +4 -1
- package/bin/github/poll-summary.mjs +48 -34
- package/bin/github/queries.d.mts +9 -5
- package/bin/github/queries.mjs +10 -6
- 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 -3
- package/bin/github/stack-read.mjs +22 -11
- 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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { buildPrShepherdCommand } from "../cli/runner.mjs";
|
|
2
|
+
import { formatPrUrl } from "../pr-reference.mjs";
|
|
1
3
|
/**
|
|
2
4
|
* The one-PR hold reason for a `READY` draft that only the disabled setting keeps in draft. The
|
|
3
5
|
* probe re-reads the draft, so this phrase, not a bare `[WAIT]`, proves it is still clean.
|
|
@@ -30,9 +32,39 @@ export function splitStackWork(candidates, staleChildren) {
|
|
|
30
32
|
*/
|
|
31
33
|
export function appendMarkReadyInstructions(instructions, layers) {
|
|
32
34
|
for (const item of layers) {
|
|
35
|
+
if (!item.owned) {
|
|
36
|
+
instructions.push(`${instructions.length + 1}. PR #${item.pr} is not owned. Leave its draft state unchanged.`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (item.transport === "rest") {
|
|
40
|
+
const command = buildPrShepherdCommand([
|
|
41
|
+
"iterate",
|
|
42
|
+
formatPrUrl(item.repo, item.pr),
|
|
43
|
+
"--transport",
|
|
44
|
+
"rest",
|
|
45
|
+
]).text;
|
|
46
|
+
instructions.push(`${instructions.length + 1}. Run \`${item.pollCommand}\` first for PR #${item.pr}. If it returns \`[WAIT]\` saying the draft is held only because automatic mark-ready is disabled, run \`${command}\` to authorize the CCR ready transition; otherwise follow the probe's instructions.`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
33
49
|
instructions.push(`${instructions.length + 1}. Automatic mark-ready is disabled, so marking PR #${item.pr} ready for review is your step. Run \`${item.pollCommand}\` first. If it returns \`[WAIT]\` saying PR #${item.pr} stays in draft because ${AUTO_MARK_READY_DISABLED_HOLD}, run \`gh pr ready ${item.pr} -R ${item.repo}\`; otherwise complete its instructions and leave PR #${item.pr} in draft this round.`);
|
|
34
50
|
}
|
|
35
51
|
}
|
|
36
52
|
function isProbed(item) {
|
|
37
53
|
return item.pollProbe === true && item.pollCommand !== undefined;
|
|
38
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Work exists, but every routed layer belongs to another author: rerunning the stack selector
|
|
57
|
+
* cannot change anything, so the caller reports the overview and stops instead.
|
|
58
|
+
*/
|
|
59
|
+
function onlyUnownedWork(layers) {
|
|
60
|
+
return layers.length > 0 && layers.every((item) => !item.owned);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The closing step after routed sessions: rerun the selector, or stop when nothing is owned.
|
|
64
|
+
* Returns the plan action: a stop is a handoff, so no rerun, quota cadence, or wake-up follows it.
|
|
65
|
+
*/
|
|
66
|
+
export function appendStackRerunInstruction(instructions, layers, rerun) {
|
|
67
|
+
const stop = onlyUnownedWork(layers);
|
|
68
|
+
instructions.push(`${instructions.length + 1}. ${stop ? "No owned layer needs a session. Report this overview and stop." : rerun}`);
|
|
69
|
+
return stop ? "escalate" : "shepherd";
|
|
70
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RepoInfo } from "../github/client.mts";
|
|
2
|
+
import { type ReplyEvidence } from "../github/reply-recovery-read.mts";
|
|
3
|
+
/**
|
|
4
|
+
* Threads whose last comment is already this exact Shepherd reply from the authenticated viewer.
|
|
5
|
+
*
|
|
6
|
+
* A cloud session can lose its state directory between ticks, taking the uncertain-reply
|
|
7
|
+
* records with it. With no local record to consult, GitHub is the only evidence of whether a
|
|
8
|
+
* reply already landed, so the `apply review` commands Shepherd generates in durable-state
|
|
9
|
+
* sessions opt in (`--adopt-existing-replies`) to checking it before posting. Direct apply
|
|
10
|
+
* requests never run this scan. A failed read never blocks the reply: it only means this
|
|
11
|
+
* safeguard had nothing to say. A batch fully covered by `preloaded` evidence (the
|
|
12
|
+
* apply-review preflight, read with no wait since) skips its read.
|
|
13
|
+
*/
|
|
14
|
+
export declare function findExistingReplies(context: {
|
|
15
|
+
repo: RepoInfo;
|
|
16
|
+
pr: number;
|
|
17
|
+
}, ids: readonly string[], message: string, preloaded?: Map<string, ReplyEvidence>): Promise<string[]>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readReplyRecoveryEvidence } from "../github/reply-recovery-read.mjs";
|
|
2
|
+
import { addPrShepherdMarker } from "./marker.mjs";
|
|
3
|
+
const BATCH = 10;
|
|
4
|
+
/**
|
|
5
|
+
* Threads whose last comment is already this exact Shepherd reply from the authenticated viewer.
|
|
6
|
+
*
|
|
7
|
+
* A cloud session can lose its state directory between ticks, taking the uncertain-reply
|
|
8
|
+
* records with it. With no local record to consult, GitHub is the only evidence of whether a
|
|
9
|
+
* reply already landed, so the `apply review` commands Shepherd generates in durable-state
|
|
10
|
+
* sessions opt in (`--adopt-existing-replies`) to checking it before posting. Direct apply
|
|
11
|
+
* requests never run this scan. A failed read never blocks the reply: it only means this
|
|
12
|
+
* safeguard had nothing to say. A batch fully covered by `preloaded` evidence (the
|
|
13
|
+
* apply-review preflight, read with no wait since) skips its read.
|
|
14
|
+
*/
|
|
15
|
+
export async function findExistingReplies(context, ids, message, preloaded) {
|
|
16
|
+
if (ids.length === 0)
|
|
17
|
+
return [];
|
|
18
|
+
const marked = addPrShepherdMarker(message);
|
|
19
|
+
const found = [];
|
|
20
|
+
for (let offset = 0; offset < ids.length; offset += BATCH) {
|
|
21
|
+
const batch = ids.slice(offset, offset + BATCH);
|
|
22
|
+
try {
|
|
23
|
+
const evidence = batch.every((id) => preloaded?.has(id))
|
|
24
|
+
? preloaded
|
|
25
|
+
: await readReplyRecoveryEvidence(context.repo, context.pr, batch);
|
|
26
|
+
for (const id of batch) {
|
|
27
|
+
const item = evidence.get(id);
|
|
28
|
+
const last = item?.comments.at(-1);
|
|
29
|
+
if (item &&
|
|
30
|
+
last?.body === marked &&
|
|
31
|
+
last.author?.toLowerCase() === item.viewer.toLowerCase()) {
|
|
32
|
+
found.push(id);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* Evidence is advisory here; the normal reply path still runs. */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return found;
|
|
41
|
+
}
|
|
@@ -17,7 +17,7 @@ export function rateLimitFromGraphQlResult(messages, meta) {
|
|
|
17
17
|
if (message)
|
|
18
18
|
return buildRateLimitStop(message, meta);
|
|
19
19
|
if (meta.stopOnZeroRemaining === true && meta.rateLimit?.remaining === 0) {
|
|
20
|
-
return buildRateLimitStop(
|
|
20
|
+
return buildRateLimitStop(`GitHub ${meta.rateLimit?.resource === "core" ? "REST core" : "GraphQL"} rate limit remaining is 0`, meta);
|
|
21
21
|
}
|
|
22
22
|
return undefined;
|
|
23
23
|
}
|
|
@@ -47,5 +47,5 @@ function finiteNumber(value) {
|
|
|
47
47
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
48
48
|
}
|
|
49
49
|
export function isRateLimitMessage(message) {
|
|
50
|
-
return /rate limit|rate-limit|secondary limit|secondary rate/i.test(message);
|
|
50
|
+
return /rate limit|rate-limit|secondary limit|secondary rate|abuse detection/i.test(message);
|
|
51
51
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type RepoInfo } from "../github/client.mts";
|
|
2
2
|
import type { ResolveOptions } from "../types.mts";
|
|
3
3
|
import { type ResolveRateLimitStop } from "./rate-limit.mts";
|
|
4
|
+
import { type ReplyEvidence } from "../github/reply-recovery-read.mts";
|
|
4
5
|
export interface ResolveResult {
|
|
5
6
|
repliedThreads: string[];
|
|
6
7
|
resolvedThreads: string[];
|
|
@@ -26,12 +27,17 @@ export interface ResolveResult {
|
|
|
26
27
|
/** @deprecated Direct apply requests now rely on GitHub's mutation response. */
|
|
27
28
|
skippedUnauthorizedDismissals?: string[];
|
|
28
29
|
rateLimit?: ResolveRateLimitStop;
|
|
30
|
+
sessionRefusal?: string;
|
|
31
|
+
instructions?: string[];
|
|
29
32
|
unrepliedThreads?: string[];
|
|
30
33
|
unresolvedThreads?: string[];
|
|
31
34
|
unminimizedComments?: string[];
|
|
32
35
|
undismissedReviews?: string[];
|
|
33
36
|
}
|
|
34
|
-
export declare function applyResolveOptions(pr: number, repo: RepoInfo, opts: ResolveOptions
|
|
37
|
+
export declare function applyResolveOptions(pr: number, repo: RepoInfo, opts: ResolveOptions, preflight?: {
|
|
38
|
+
headSha?: string;
|
|
39
|
+
evidence: Map<string, ReplyEvidence>;
|
|
40
|
+
}): Promise<ResolveResult>;
|
|
35
41
|
/** @deprecated Compatibility alias; use `autoResolveThreads`. */
|
|
36
42
|
export declare function autoResolveOutdated(threadIds: string[]): Promise<{
|
|
37
43
|
resolved: string[];
|
package/bin/comments/resolve.mjs
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
import { graphqlWithRateLimit } from "../github/client.mjs";
|
|
3
3
|
import { addPrShepherdMarker } from "./marker.mjs";
|
|
4
|
+
import { findExistingReplies } from "./existing-reply-scan.mjs";
|
|
4
5
|
import { isRateLimitMessage, rateLimitFromError, rateLimitFromGraphQlResult, } from "./rate-limit.mjs";
|
|
5
6
|
import { setPendingOps } from "./pending-ops.mjs";
|
|
6
7
|
import { waitForSha } from "./sha-poll.mjs";
|
|
8
|
+
import { githubOperation, getGithubTransport } from "../github/transport.mjs";
|
|
9
|
+
import { resolveGraphqlThreadId } from "../github/rest-identities.mjs";
|
|
10
|
+
import { applyRestReviewChunk, isAmbiguousMutationError } from "./rest-review-mutations.mjs";
|
|
11
|
+
import { assertReplyOutcomeKnown, rememberUncertainReplies, trackAdoptedReplyThreads, } from "./uncertain-replies.mjs";
|
|
12
|
+
import { isRestSessionRefusal } from "../github/rest-session-refusal.mjs";
|
|
13
|
+
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
14
|
+
import { readReplyRecoveryEvidence } from "../github/reply-recovery-read.mjs";
|
|
15
|
+
import { GitHubRequestError, isRetryableGraphQlInternal, } from "../github/errors.mjs";
|
|
7
16
|
const COMMENTED_DISMISS_ERROR_PATTERNS = [
|
|
8
17
|
/can\s*not\s+dismiss[\s\S]*?commented pull request review/i,
|
|
9
18
|
];
|
|
@@ -30,7 +39,15 @@ function mutationErrorMessage(errors, alias) {
|
|
|
30
39
|
.map((error) => error.message);
|
|
31
40
|
return messages.length > 0 ? messages.join("; ") : undefined;
|
|
32
41
|
}
|
|
33
|
-
|
|
42
|
+
function uncertainReplyIds(ids, confirmed, errors) {
|
|
43
|
+
return ids.filter((id, index) => {
|
|
44
|
+
if (confirmed.includes(id))
|
|
45
|
+
return false;
|
|
46
|
+
const applicable = errors.filter((error) => error.path === undefined || (Array.isArray(error.path) && error.path[0] === `p${index}`));
|
|
47
|
+
return applicable.length === 0 || isRetryableGraphQlInternal(applicable);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export async function applyResolveOptions(pr, repo, opts, preflight) {
|
|
34
51
|
const resolveThreadIds = dedupeIds(opts.resolveThreadIds ?? []);
|
|
35
52
|
const replyThreadIds = dedupeIds(opts.replyThreadIds ?? []);
|
|
36
53
|
const minimizeCommentIds = opts.minimizeCommentIds ?? [];
|
|
@@ -45,12 +62,30 @@ export async function applyResolveOptions(pr, repo, opts) {
|
|
|
45
62
|
if ((dismissReviewIds.length > 0 || replyThreadIds.length > 0) && !opts.dismissMessage) {
|
|
46
63
|
throw new Error("--message is required when replying to threads or dismissing reviews");
|
|
47
64
|
}
|
|
48
|
-
|
|
65
|
+
// A preflight that already saw the required head stands in for the first poll; its
|
|
66
|
+
// recovery evidence is used only when no poll wait separated it from the mutation.
|
|
67
|
+
const headConfirmed = !opts.requireSha || preflight?.headSha === opts.requireSha;
|
|
68
|
+
const preloadedEvidence = headConfirmed ? preflight?.evidence : undefined;
|
|
69
|
+
if (opts.requireSha && !headConfirmed) {
|
|
49
70
|
// Verify GitHub received the commit before resolving — prevents auto-merge
|
|
50
71
|
// before reviewers see the fix.
|
|
51
72
|
await waitForSha(pr, repo, opts.requireSha);
|
|
52
73
|
}
|
|
53
|
-
await
|
|
74
|
+
const known = await assertReplyOutcomeKnown({ repo, pr }, replyThreadIds, opts.dismissMessage ?? "");
|
|
75
|
+
const adopted = [
|
|
76
|
+
...known,
|
|
77
|
+
...(opts.adoptExistingReplies
|
|
78
|
+
? await findExistingReplies({ repo, pr }, replyThreadIds.filter((id) => !known.includes(id)), opts.dismissMessage ?? "", preloadedEvidence)
|
|
79
|
+
: []),
|
|
80
|
+
];
|
|
81
|
+
result.repliedThreads.push(...adopted);
|
|
82
|
+
trackAdoptedReplyThreads(result, adopted);
|
|
83
|
+
await bulkApply(replyThreadIds.filter((id) => !adopted.includes(id)), resolveThreadIds, minimizeCommentIds, dismissReviewIds, opts.dismissMessage ?? "", result, { repo, pr }, preloadedEvidence);
|
|
84
|
+
if (result.sessionRefusal)
|
|
85
|
+
result.instructions = [
|
|
86
|
+
"Restore GitHub access for this session using the proxy instructions above.",
|
|
87
|
+
"Retry only the pending IDs listed above.",
|
|
88
|
+
];
|
|
54
89
|
return result;
|
|
55
90
|
}
|
|
56
91
|
/** @deprecated Compatibility alias; use `autoResolveThreads`. */
|
|
@@ -68,6 +103,8 @@ export async function autoResolveThreads(threadIds) {
|
|
|
68
103
|
errors: [],
|
|
69
104
|
};
|
|
70
105
|
await bulkApply([], threadIds, [], [], "", result);
|
|
106
|
+
if (result.sessionRefusal)
|
|
107
|
+
throw new ShepherdError(result.sessionRefusal, EXIT.NOPERM);
|
|
71
108
|
return { resolved: result.resolvedThreads, errors: result.errors };
|
|
72
109
|
}
|
|
73
110
|
export async function autoMinimizeComments(minimizeIds) {
|
|
@@ -81,6 +118,8 @@ export async function autoMinimizeComments(minimizeIds) {
|
|
|
81
118
|
errors: [],
|
|
82
119
|
};
|
|
83
120
|
await bulkApply([], [], minimizeIds, [], "", result);
|
|
121
|
+
if (result.sessionRefusal)
|
|
122
|
+
throw new ShepherdError(result.sessionRefusal, EXIT.NOPERM);
|
|
84
123
|
return { minimized: result.minimizedComments, errors: result.errors };
|
|
85
124
|
}
|
|
86
125
|
// Keep mutation batches small so rate-limit stops leave a precise pending list.
|
|
@@ -102,7 +141,7 @@ function buildBulkMutation(replyIds, resolveIds, minimizeIds, dismissIds, dismis
|
|
|
102
141
|
}
|
|
103
142
|
return `mutation BulkApply {\n${ops.join("\n")}\n}`;
|
|
104
143
|
}
|
|
105
|
-
async function bulkApply(replyIds, resolveIds, minimizeIds, dismissIds, dismissMessage, result) {
|
|
144
|
+
async function bulkApply(replyIds, resolveIds, minimizeIds, dismissIds, dismissMessage, result, context, firstChunkEvidence) {
|
|
106
145
|
const allOps = [
|
|
107
146
|
...replyIds.map((id) => ({ kind: "p", id })),
|
|
108
147
|
...resolveIds.map((id) => ({ kind: "r", id })),
|
|
@@ -112,34 +151,76 @@ async function bulkApply(replyIds, resolveIds, minimizeIds, dismissIds, dismissM
|
|
|
112
151
|
for (let i = 0; i < allOps.length; i += BULK_CHUNK_SIZE) {
|
|
113
152
|
const chunk = allOps.slice(i, i + BULK_CHUNK_SIZE);
|
|
114
153
|
// eslint-disable-next-line no-await-in-loop
|
|
115
|
-
const stopped = await bulkApplyChunk(chunk.filter((o) => o.kind === "r").map((o) => o.id), chunk.filter((o) => o.kind === "p").map((o) => o.id), chunk.filter((o) => o.kind === "m").map((o) => o.id), chunk.filter((o) => o.kind === "d").map((o) => o.id), dismissMessage, result, i + BULK_CHUNK_SIZE < allOps.length);
|
|
154
|
+
const stopped = await bulkApplyChunk(chunk.filter((o) => o.kind === "r").map((o) => o.id), chunk.filter((o) => o.kind === "p").map((o) => o.id), chunk.filter((o) => o.kind === "m").map((o) => o.id), chunk.filter((o) => o.kind === "d").map((o) => o.id), dismissMessage, result, i + BULK_CHUNK_SIZE < allOps.length, context, i === 0 ? firstChunkEvidence : undefined);
|
|
116
155
|
if (stopped) {
|
|
117
156
|
setPendingOps(result, allOps.slice(i));
|
|
118
157
|
return;
|
|
119
158
|
}
|
|
120
159
|
}
|
|
121
160
|
}
|
|
122
|
-
async function bulkApplyChunk(resolveIds, replyIds, minimizeIds, dismissIds, dismissMessage, result, hasPendingAfter) {
|
|
123
|
-
const doc = buildBulkMutation(replyIds, resolveIds, minimizeIds, dismissIds, dismissMessage);
|
|
161
|
+
async function bulkApplyChunk(resolveIds, replyIds, minimizeIds, dismissIds, dismissMessage, result, hasPendingAfter, context, preloadedEvidence) {
|
|
124
162
|
let data = {};
|
|
125
163
|
let graphQlErrors = [];
|
|
126
164
|
let rateLimitStop;
|
|
127
165
|
let suppressCurrentChunkErrors = false;
|
|
166
|
+
let restStopped = false;
|
|
167
|
+
let graphqlMutationAttempted = false;
|
|
168
|
+
let replyEvidence = new Map();
|
|
169
|
+
if (context && replyIds.length && getGithubTransport() === "graphql") {
|
|
170
|
+
// Evidence is all-or-nothing per batch, as the standalone read is.
|
|
171
|
+
if (replyIds.every((id) => preloadedEvidence?.has(id)))
|
|
172
|
+
replyEvidence = new Map(replyIds.map((id) => [id, preloadedEvidence.get(id)]));
|
|
173
|
+
else
|
|
174
|
+
try {
|
|
175
|
+
replyEvidence = await readReplyRecoveryEvidence(context.repo, context.pr, replyIds);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
/* Preserve explicit mutation eligibility when recovery evidence is unavailable. */
|
|
179
|
+
}
|
|
180
|
+
}
|
|
128
181
|
try {
|
|
129
|
-
const resp = await
|
|
130
|
-
|
|
131
|
-
|
|
182
|
+
const resp = await githubOperation("BulkApply", async () => {
|
|
183
|
+
const [graphqlReplies, graphqlResolves] = await Promise.all([
|
|
184
|
+
Promise.all(replyIds.map(resolveGraphqlThreadId)),
|
|
185
|
+
Promise.all(resolveIds.map(resolveGraphqlThreadId)),
|
|
186
|
+
]);
|
|
187
|
+
graphqlMutationAttempted = true;
|
|
188
|
+
return graphqlWithRateLimit(buildBulkMutation(graphqlReplies, graphqlResolves, minimizeIds, dismissIds, dismissMessage), {}, { allowPartialData: true });
|
|
189
|
+
}, () => applyRestReviewChunk({
|
|
190
|
+
...context,
|
|
191
|
+
replyIds,
|
|
192
|
+
resolveIds,
|
|
193
|
+
minimizeIds,
|
|
194
|
+
dismissIds,
|
|
195
|
+
message: dismissMessage,
|
|
196
|
+
}), { mutation: true });
|
|
197
|
+
if ("restStopped" in resp)
|
|
198
|
+
restStopped = resp.restStopped === true;
|
|
199
|
+
if ("sessionRefusal" in resp && typeof resp.sessionRefusal === "string")
|
|
200
|
+
result.sessionRefusal = resp.sessionRefusal;
|
|
132
201
|
data = resp.data;
|
|
133
202
|
graphQlErrors = (resp.errors ?? []);
|
|
134
203
|
const graphQlErrorMessages = graphQlErrors.map((e) => e.message);
|
|
135
|
-
suppressCurrentChunkErrors = graphQlErrorMessages.some(isRateLimitMessage);
|
|
204
|
+
suppressCurrentChunkErrors = restStopped || graphQlErrorMessages.some(isRateLimitMessage);
|
|
136
205
|
rateLimitStop = rateLimitFromGraphQlResult(graphQlErrorMessages, {
|
|
137
206
|
rateLimit: resp.rateLimit,
|
|
138
|
-
retryAfterSeconds: resp.retryAfterSeconds,
|
|
207
|
+
retryAfterSeconds: "retryAfterSeconds" in resp ? resp.retryAfterSeconds : undefined,
|
|
139
208
|
stopOnZeroRemaining: hasPendingAfter,
|
|
140
209
|
});
|
|
141
210
|
}
|
|
142
211
|
catch (err) {
|
|
212
|
+
if (isRestSessionRefusal(err))
|
|
213
|
+
throw err;
|
|
214
|
+
const uncertainIds = err instanceof GitHubRequestError && err.status === 200
|
|
215
|
+
? uncertainReplyIds(replyIds, [], err.graphqlErrors ?? [])
|
|
216
|
+
: replyIds;
|
|
217
|
+
const ambiguous = isAmbiguousMutationError(err) ||
|
|
218
|
+
(graphqlMutationAttempted &&
|
|
219
|
+
err instanceof GitHubRequestError &&
|
|
220
|
+
(isRetryableGraphQlInternal(err.graphqlErrors) ||
|
|
221
|
+
(err.status === 200 && (!err.graphqlErrors?.length || uncertainIds.length > 0))));
|
|
222
|
+
if (context && graphqlMutationAttempted && ambiguous)
|
|
223
|
+
await rememberUncertainReplies(context, uncertainIds, dismissMessage, replyEvidence);
|
|
143
224
|
const msg = err instanceof Error ? err.message : String(err);
|
|
144
225
|
const stop = rateLimitFromError(err, msg);
|
|
145
226
|
if (stop) {
|
|
@@ -155,7 +236,7 @@ async function bulkApplyChunk(resolveIds, replyIds, minimizeIds, dismissIds, dis
|
|
|
155
236
|
result.errors.push(`${id}: ${msg}`);
|
|
156
237
|
for (const id of dismissIds)
|
|
157
238
|
result.errors.push(`${id}: ${msg}`);
|
|
158
|
-
return
|
|
239
|
+
return ambiguous;
|
|
159
240
|
}
|
|
160
241
|
for (let i = 0; i < replyIds.length; i++) {
|
|
161
242
|
const id = replyIds[i];
|
|
@@ -164,21 +245,21 @@ async function bulkApplyChunk(resolveIds, replyIds, minimizeIds, dismissIds, dis
|
|
|
164
245
|
const p = data[`p${i}`];
|
|
165
246
|
if (p?.comment?.id)
|
|
166
247
|
result.repliedThreads.push(id);
|
|
167
|
-
else if (!suppressCurrentChunkErrors)
|
|
248
|
+
else if (!suppressCurrentChunkErrors || mutationErrorMessage(graphQlErrors, `p${i}`))
|
|
168
249
|
result.errors.push(`${id}: ${mutationErrorMessage(graphQlErrors, `p${i}`) ?? "reply returned null or comment not created"}`);
|
|
169
250
|
}
|
|
170
251
|
for (let i = 0; i < resolveIds.length; i++) {
|
|
171
252
|
const r = data[`r${i}`];
|
|
172
253
|
if (r?.thread?.isResolved === true)
|
|
173
254
|
result.resolvedThreads.push(resolveIds[i]);
|
|
174
|
-
else if (!suppressCurrentChunkErrors)
|
|
255
|
+
else if (!suppressCurrentChunkErrors || mutationErrorMessage(graphQlErrors, `r${i}`))
|
|
175
256
|
result.errors.push(`${resolveIds[i]}: ${mutationErrorMessage(graphQlErrors, `r${i}`) ?? "resolve returned null or thread not resolved"}`);
|
|
176
257
|
}
|
|
177
258
|
for (let i = 0; i < minimizeIds.length; i++) {
|
|
178
259
|
const m = data[`m${i}`];
|
|
179
260
|
if (m?.minimizedComment?.isMinimized === true)
|
|
180
261
|
result.minimizedComments.push(minimizeIds[i]);
|
|
181
|
-
else if (!suppressCurrentChunkErrors)
|
|
262
|
+
else if (!suppressCurrentChunkErrors || mutationErrorMessage(graphQlErrors, `m${i}`))
|
|
182
263
|
result.errors.push(`${minimizeIds[i]}: ${mutationErrorMessage(graphQlErrors, `m${i}`) ?? "minimize returned null or comment not minimized"}`);
|
|
183
264
|
}
|
|
184
265
|
const singleDismiss = dismissIds.length === 1;
|
|
@@ -198,17 +279,26 @@ async function bulkApplyChunk(resolveIds, replyIds, minimizeIds, dismissIds, dis
|
|
|
198
279
|
const d = data[`d${i}`];
|
|
199
280
|
if (d?.pullRequestReview != null)
|
|
200
281
|
result.dismissedReviews.push(dismissIds[i]);
|
|
201
|
-
else if (!suppressCurrentChunkErrors)
|
|
282
|
+
else if (!suppressCurrentChunkErrors || mutationErrorMessage(graphQlErrors, `d${i}`))
|
|
202
283
|
result.errors.push(commentedDismissErrorIndexes.has(i) || (singleDismiss && hasUnmappedCommentedDismissError)
|
|
203
284
|
? dismissReviewNonDismissibleMessage(dismissIds[i])
|
|
204
285
|
: `${dismissIds[i]}: ${mutationErrorMessage(graphQlErrors, `d${i}`) ?? "dismiss returned null"}`);
|
|
205
286
|
}
|
|
287
|
+
const internalFailure = graphqlMutationAttempted && isRetryableGraphQlInternal(graphQlErrors);
|
|
288
|
+
const uncertainIds = graphqlMutationAttempted
|
|
289
|
+
? uncertainReplyIds(replyIds, result.repliedThreads, graphQlErrors)
|
|
290
|
+
: [];
|
|
291
|
+
if (context && uncertainIds.length) {
|
|
292
|
+
await rememberUncertainReplies(context, uncertainIds, dismissMessage, replyEvidence);
|
|
293
|
+
}
|
|
206
294
|
if (rateLimitStop) {
|
|
207
295
|
result.errors.push(`rate limit: ${rateLimitStop.message}`);
|
|
208
296
|
result.rateLimit = rateLimitStop;
|
|
209
297
|
return true;
|
|
210
298
|
}
|
|
211
|
-
|
|
299
|
+
if (result.sessionRefusal)
|
|
300
|
+
return true;
|
|
301
|
+
return restStopped || internalFailure || uncertainIds.length > 0;
|
|
212
302
|
}
|
|
213
303
|
function dismissErrorAliasIndex(error) {
|
|
214
304
|
if (!Array.isArray(error.path))
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RateLimitInfo } from "../github/http-utils.mts";
|
|
2
|
+
import type { RepoInfo } from "../github/client.mts";
|
|
3
|
+
interface ReplyResult {
|
|
4
|
+
id: string;
|
|
5
|
+
rateLimit?: RateLimitInfo;
|
|
6
|
+
}
|
|
7
|
+
export declare function isAmbiguousMutationError(error: unknown): boolean;
|
|
8
|
+
export declare function replyToThread(repo: RepoInfo, pr: number, root: string, body: string): Promise<ReplyResult>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile, writeFile, mkdir, unlink } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { resolvePrStatePath } from "../state/base.mjs";
|
|
5
|
+
import { rest, restWithRateLimit } from "../github/http.mjs";
|
|
6
|
+
import { GitHubRequestError } from "../github/errors.mjs";
|
|
7
|
+
import { isTransportError } from "../github/http-utils.mjs";
|
|
8
|
+
import { readRestPages, restRepoPath } from "../github/rest-reader-core.mjs";
|
|
9
|
+
import { isRestSessionRefusal } from "../github/rest-session-refusal.mjs";
|
|
10
|
+
export function isAmbiguousMutationError(error) {
|
|
11
|
+
return isTransportError(error) || (error instanceof GitHubRequestError && error.status >= 500);
|
|
12
|
+
}
|
|
13
|
+
function replyPath(repo, pr, root, body) {
|
|
14
|
+
const hash = createHash("sha256").update(`${root}\n${body}`).digest("hex");
|
|
15
|
+
return resolvePrStatePath({ owner: repo.owner, repo: repo.name, pr }, "pending-replies", `${hash}.json`);
|
|
16
|
+
}
|
|
17
|
+
async function readComments(repo, pr) {
|
|
18
|
+
return readRestPages(`${restRepoPath(repo)}/pulls/${pr}/comments`);
|
|
19
|
+
}
|
|
20
|
+
async function findPendingReply(repo, pr, pending) {
|
|
21
|
+
if (!pending.viewer)
|
|
22
|
+
return undefined;
|
|
23
|
+
const existing = new Set(pending.beforeIds);
|
|
24
|
+
const response = await readComments(repo, pr);
|
|
25
|
+
const matches = response.nodes.filter((comment) => String(comment.in_reply_to_id) === pending.root &&
|
|
26
|
+
comment.body === pending.body &&
|
|
27
|
+
comment.user?.login === pending.viewer &&
|
|
28
|
+
!existing.has(comment.id));
|
|
29
|
+
return matches.length === 1
|
|
30
|
+
? { id: String(matches[0].id), rateLimit: response.rateLimit }
|
|
31
|
+
: undefined;
|
|
32
|
+
}
|
|
33
|
+
export async function replyToThread(repo, pr, root, body) {
|
|
34
|
+
const path = replyPath(repo, pr, root, body);
|
|
35
|
+
let pending;
|
|
36
|
+
try {
|
|
37
|
+
pending = JSON.parse(await readFile(path, "utf8"));
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error.code !== "ENOENT")
|
|
41
|
+
throw new Error("Cannot read pending REST reply; cannot safely retry", { cause: error });
|
|
42
|
+
}
|
|
43
|
+
if (pending) {
|
|
44
|
+
if (pending.root !== root || pending.body !== body || !Array.isArray(pending.beforeIds))
|
|
45
|
+
throw new Error("Invalid pending REST reply; cannot safely retry");
|
|
46
|
+
const id = await findPendingReply(repo, pr, pending);
|
|
47
|
+
if (!id)
|
|
48
|
+
throw new Error("Previous reply outcome is uncertain; refresh the thread before retrying, no duplicate reply was sent");
|
|
49
|
+
await unlink(path);
|
|
50
|
+
return id;
|
|
51
|
+
}
|
|
52
|
+
// These reads provide the exact author and pre-write IDs needed after a lost response.
|
|
53
|
+
let viewer;
|
|
54
|
+
try {
|
|
55
|
+
viewer = (await rest("GET", "/user")).login;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (isRestSessionRefusal(error))
|
|
59
|
+
throw error;
|
|
60
|
+
if (!(error instanceof GitHubRequestError) || ![403, 404].includes(error.status))
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
const comments = await readComments(repo, pr);
|
|
64
|
+
pending = {
|
|
65
|
+
root,
|
|
66
|
+
body,
|
|
67
|
+
...(viewer && { viewer }),
|
|
68
|
+
beforeIds: comments.nodes.map((comment) => comment.id),
|
|
69
|
+
};
|
|
70
|
+
await mkdir(dirname(path), { recursive: true });
|
|
71
|
+
await writeFile(path, JSON.stringify(pending), { flag: "wx" });
|
|
72
|
+
try {
|
|
73
|
+
const result = await restWithRateLimit("POST", `${restRepoPath(repo)}/pulls/${pr}/comments/${root}/replies`, { body });
|
|
74
|
+
if (!Number.isSafeInteger(result.data?.id) || result.data.id <= 0)
|
|
75
|
+
throw new Error("Reply returned no valid comment ID; outcome is uncertain");
|
|
76
|
+
await unlink(path);
|
|
77
|
+
return { id: String(result.data.id), rateLimit: result.rateLimit };
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (isAmbiguousMutationError(error)) {
|
|
81
|
+
try {
|
|
82
|
+
const id = await findPendingReply(repo, pr, pending);
|
|
83
|
+
if (id) {
|
|
84
|
+
await unlink(path);
|
|
85
|
+
return id;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
}
|
|
90
|
+
else if (error instanceof GitHubRequestError && error.status >= 400 && error.status < 500) {
|
|
91
|
+
await unlink(path);
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RepoInfo } from "../github/client.mts";
|
|
2
|
+
import type { RateLimitInfo } from "../github/http-utils.mts";
|
|
3
|
+
export declare function applyRestReviewChunk(input: {
|
|
4
|
+
repo?: RepoInfo;
|
|
5
|
+
pr?: number;
|
|
6
|
+
replyIds: string[];
|
|
7
|
+
resolveIds: string[];
|
|
8
|
+
minimizeIds: string[];
|
|
9
|
+
dismissIds: string[];
|
|
10
|
+
message: string;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
data: Record<string, unknown>;
|
|
13
|
+
errors: Array<{
|
|
14
|
+
message: string;
|
|
15
|
+
path: string[];
|
|
16
|
+
}>;
|
|
17
|
+
rateLimit?: RateLimitInfo;
|
|
18
|
+
retryAfterSeconds?: number;
|
|
19
|
+
restStopped?: true;
|
|
20
|
+
sessionRefusal?: string;
|
|
21
|
+
}>;
|
|
22
|
+
export { isAmbiguousMutationError } from "./rest-reply.mts";
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { replyToThread, isAmbiguousMutationError } from "./rest-reply.mjs";
|
|
2
|
+
import { restWithRateLimit } from "../github/http.mjs";
|
|
3
|
+
import { GitHubRequestError } from "../github/errors.mjs";
|
|
4
|
+
import { isCcrTransport } from "../github/transport.mjs";
|
|
5
|
+
import { UnsupportedRestOperationError } from "../github/unsupported-rest.mjs";
|
|
6
|
+
import { resolveRestIdentity, resolveRestThreadRoot } from "../github/rest-identities.mjs";
|
|
7
|
+
import { readRestPages, restRepoPath } from "../github/rest-reader-core.mjs";
|
|
8
|
+
import { addPrShepherdMarker } from "./marker.mjs";
|
|
9
|
+
import { rateLimitFromError } from "./rate-limit.mjs";
|
|
10
|
+
import { markMutationDenied } from "../state/seen-comments.mjs";
|
|
11
|
+
import { readRestFeedback } from "../github/rest-feedback-read.mjs";
|
|
12
|
+
import { threadTranscriptBody } from "../threads/transcript.mjs";
|
|
13
|
+
import { isRestSessionRefusal } from "../github/rest-session-refusal.mjs";
|
|
14
|
+
async function reviewNumericId(repo, pr, id) {
|
|
15
|
+
if (/^[1-9][0-9]*$/.test(id))
|
|
16
|
+
return id;
|
|
17
|
+
try {
|
|
18
|
+
const entry = await resolveRestIdentity(id, "review");
|
|
19
|
+
if (entry.pr === pr &&
|
|
20
|
+
entry.repo.owner.toLowerCase() === repo.owner.toLowerCase() &&
|
|
21
|
+
entry.repo.name.toLowerCase() === repo.name.toLowerCase())
|
|
22
|
+
return entry.numericId;
|
|
23
|
+
}
|
|
24
|
+
catch { }
|
|
25
|
+
const reviews = (await readRestPages(`${restRepoPath(repo)}/pulls/${pr}/reviews`)).nodes;
|
|
26
|
+
const target = reviews.find((review) => review.node_id === id);
|
|
27
|
+
if (!target)
|
|
28
|
+
throw new Error(`Cannot address review ${id} through REST in ${repo.owner}/${repo.name}#${pr}`);
|
|
29
|
+
return String(target.id);
|
|
30
|
+
}
|
|
31
|
+
async function rememberDenied(input, id, kind) {
|
|
32
|
+
let key;
|
|
33
|
+
try {
|
|
34
|
+
const identity = input.repo && input.pr !== undefined ? undefined : await resolveRestIdentity(id);
|
|
35
|
+
const repo = input.repo ?? identity.repo;
|
|
36
|
+
const pr = input.pr ?? identity.pr;
|
|
37
|
+
key = { owner: repo.owner, repo: repo.name, pr };
|
|
38
|
+
const feedback = await readRestFeedback(pr, repo);
|
|
39
|
+
const root = kind === "dismiss" ? undefined : await resolveRestThreadRoot(repo, pr, id);
|
|
40
|
+
const body = kind === "dismiss"
|
|
41
|
+
? feedback.reviews.find((review) => review.node_id === id || String(review.id) === id)?.body
|
|
42
|
+
: feedback.threads.find((thread) => thread.id === id || thread.id === `rest-thread-${root}`);
|
|
43
|
+
if (body !== undefined && body !== null)
|
|
44
|
+
await markMutationDenied(key, id, typeof body === "string" ? body : threadTranscriptBody(body));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Generated mutations already surfaced this revision; a failed refresh must not retry it.
|
|
48
|
+
if (key)
|
|
49
|
+
await markMutationDenied(key, id);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export async function applyRestReviewChunk(input) {
|
|
53
|
+
const result = { data: {}, errors: [] };
|
|
54
|
+
const ops = [
|
|
55
|
+
...input.replyIds.map((id, index) => ({ id, alias: `p${index}`, kind: "reply" })),
|
|
56
|
+
...input.resolveIds.map((id, index) => ({ id, alias: `r${index}`, kind: "resolve" })),
|
|
57
|
+
...input.minimizeIds.map((id, index) => ({ id, alias: `m${index}`, kind: "minimize" })),
|
|
58
|
+
...input.dismissIds.map((id, index) => ({ id, alias: `d${index}`, kind: "dismiss" })),
|
|
59
|
+
];
|
|
60
|
+
for (const op of ops) {
|
|
61
|
+
try {
|
|
62
|
+
if (op.kind === "minimize")
|
|
63
|
+
throw new UnsupportedRestOperationError("minimizeComment");
|
|
64
|
+
let repo = input.repo;
|
|
65
|
+
let pr = input.pr;
|
|
66
|
+
if (!repo || pr === undefined) {
|
|
67
|
+
const identity = await resolveRestIdentity(op.id, op.kind === "dismiss" ? "review" : "thread");
|
|
68
|
+
repo = identity.repo;
|
|
69
|
+
pr = identity.pr;
|
|
70
|
+
}
|
|
71
|
+
const prefix = `${restRepoPath(repo)}/pulls/${pr}`;
|
|
72
|
+
if (op.kind === "reply") {
|
|
73
|
+
const root = await resolveRestThreadRoot(repo, pr, op.id);
|
|
74
|
+
const response = await replyToThread(repo, pr, root, addPrShepherdMarker(input.message));
|
|
75
|
+
result.rateLimit = response.rateLimit;
|
|
76
|
+
result.data[op.alias] = { comment: { id: response.id } };
|
|
77
|
+
}
|
|
78
|
+
else if (op.kind === "resolve") {
|
|
79
|
+
if (!isCcrTransport())
|
|
80
|
+
throw new UnsupportedRestOperationError("resolveReviewThread", "thread resolution requires the cloud CCR proxy");
|
|
81
|
+
const root = await resolveRestThreadRoot(repo, pr, op.id);
|
|
82
|
+
const response = await restWithRateLimit("POST", `${prefix}/ccr/comments/${root}/resolve`);
|
|
83
|
+
result.rateLimit = response.rateLimit;
|
|
84
|
+
if (response.data?.resolved !== true)
|
|
85
|
+
throw new Error("CCR resolve did not confirm the thread was resolved");
|
|
86
|
+
result.data[op.alias] = { thread: { isResolved: true } };
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const review = await reviewNumericId(repo, pr, op.id);
|
|
90
|
+
const response = await restWithRateLimit("PUT", `${prefix}/reviews/${review}/dismissals`, { message: input.message });
|
|
91
|
+
result.rateLimit = response.rateLimit;
|
|
92
|
+
if (response.data?.state !== "DISMISSED")
|
|
93
|
+
throw new Error("Review dismissal did not confirm DISMISSED state");
|
|
94
|
+
result.data[op.alias] = { pullRequestReview: { state: "DISMISSED" } };
|
|
95
|
+
}
|
|
96
|
+
if (result.rateLimit?.remaining === 0) {
|
|
97
|
+
result.errors.push({ message: "GitHub REST core rate limit remaining is 0", path: [] });
|
|
98
|
+
result.restStopped = true;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (isRestSessionRefusal(error)) {
|
|
104
|
+
result.sessionRefusal = error.message;
|
|
105
|
+
result.restStopped = true;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
109
|
+
result.errors.push({ message, path: [op.alias] });
|
|
110
|
+
const stop = rateLimitFromError(error, message);
|
|
111
|
+
if (!stop && error instanceof GitHubRequestError && [403, 404].includes(error.status))
|
|
112
|
+
await rememberDenied(input, op.id, op.kind);
|
|
113
|
+
if (error instanceof GitHubRequestError) {
|
|
114
|
+
result.rateLimit = error.rateLimit;
|
|
115
|
+
result.retryAfterSeconds = error.retryAfterSeconds;
|
|
116
|
+
}
|
|
117
|
+
if (stop || isAmbiguousMutationError(error) || message.includes("outcome is uncertain")) {
|
|
118
|
+
result.restStopped = true;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
export { isAmbiguousMutationError } from "./rest-reply.mjs";
|
|
@@ -24,5 +24,5 @@ export declare function classifyReviewsForDisplay(reviews: Review[], seenMap: Ma
|
|
|
24
24
|
*
|
|
25
25
|
* Human-authored CR reviews continue to flow through the standard seen-gate.
|
|
26
26
|
*/
|
|
27
|
-
export declare function classifyChangesRequestedReviewsForDisplay(reviews: Review[], seenMap: Map<string, SeenMarker>, botUsernames: NormalizedBotUsernames, repeatBotReviews?: boolean): ReviewVisibility;
|
|
27
|
+
export declare function classifyChangesRequestedReviewsForDisplay(reviews: Review[], seenMap: Map<string, SeenMarker>, botUsernames: NormalizedBotUsernames, repeatBotReviews?: boolean, deniedDismissalIds?: ReadonlySet<string>): ReviewVisibility;
|
|
28
28
|
export {};
|