Skip to main content

wait-for-pr-review

Wait for pull-request reviewers without spending tokens, using a deterministic Rust poller, then run the review loop on each event. Use after opening or updating a pull request when you must wait for Copilot or a maintainer review, instead of being told to wait. Triggers on "wait for review", "wait for da2ce7", "wait for Copilot review", "poll the PR", "block until reviewed", "review loop", or "wait-for-pr-review".

Source facts

Repository
torrust/torrust-tracker
Last source activity
November 6, 2026 at 16:31
Detected SKILL.md language
English
Stars
536
Forks
58

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
11 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
wait-for-pr-review
description
Wait for pull-request reviewers without spending tokens, using a deterministic Rust poller, then run the review loop on each event. Use after opening or updating a pull request when you must wait for Copilot or a maintainer review, instead of being told to wait. Triggers on "wait for review", "wait for da2ce7", "wait for Copilot review", "poll the PR", "block until reviewed", "review loop", or "wait-for-pr-review".
metadata
{"author":"torrust","version":"1.2","semantic-links":{"related-artifacts":[".github/skills/dev/pr-reviews/wait-for-pr-review/tool/Cargo.toml",".github/skills/dev/ci/watch-pull-request-checks/SKILL.md",".github/skills/dev/pr-reviews/process-pr-review/SKILL.md",".github/skills/dev/pr-reviews/fetch-review-threads/SKILL.md",".github/skills/dev/git-workflow/merge-pull-request/SKILL.md","docs/adrs/20260519000000_define_global_cli_output_contract.md","docs/issues/closed/2581-2003-pr-review-wait-poller/ISSUE.md"]}}
# Waiting for Pull-Request Reviews After you open or update a pull request, reviewers answer on their own schedule. Run the poller instead of asking the contributor to tell you when to look, and instead of polling through an MCP server, which spends inference tokens on every check. The poller is a Rust tool that calls no model, holds no token, and returns only when something happens on the pull request. This skill is the waiting half of the review loop. [`process-pr-review`](../process-pr-review/SKILL.md) is the processing half. ## Prerequisites - GitHub CLI (`gh`), authenticated. The poller refuses to run otherwise. - The pull request number and the commit you last pushed. ## Run the Poller Stdout carries the result and the poller refuses to run when stdout is a terminal. Redirect each channel to a file, as the [CLI output contract](../../../../../docs/adrs/20260519000000_define_global_cli_output_contract.md) recommends for agents: ```sh mkdir -p .tmp cargo run -q -p wait-for-pr-review -- \ --pr-number <PR> \ --since-head-sha "$(git rev-parse HEAD)" \ > .tmp/wait-for-pr-review.stdout 2> .tmp/wait-for-pr-review.stderr echo "exit=$?" cat .tmp/wait-for-pr-review.stdout ``` | Option | Default | Meaning | | --------------------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | `--pr-number <N>` | required | Pull request to watch. | | `--owner`, `--repository` | `torrust`, `torrust-tracker` | Repository. | | `--reviewer <login>` | `da2ce7`, `copilot-pull-request-reviewer[bot]` | Whose reviews and comments count. Repeat the flag to watch several; passing it replaces the defaults. | | `--timeout-seconds <N>` | 900 (15 minutes) | Longest wait before a `timeout` result. | | `--poll-interval-seconds N` | 120 (2 minutes) | Pause between two looks at the pull request. | | `--since-head-sha <sha>` | the head at the start of the wait | The commit you have checked out. A pull request head that differs from it is reported as `head_changed`. Unless `--since` is given, reviews and comments created after this commit was made count as new. | | `--since <utc time>` | the moment the poller starts | Count reviews and comments created strictly after this time. Pass the `cursor` of the previous result. Combine it with `--since-head-sha`, so a head that moved is not adopted silently. | Always pass `--since-head-sha "$(git rev-parse HEAD)"`, and add `--since <cursor>` when resuming after an event that needed no push (see [The Review Loop](#the-review-loop)). `--since-head-sha` tells the poller which commit your checkout has, so it reports `head_changed` when the pull request is ahead of you. Without it, the baseline is the head at the poller start, a push that happened before that is never reported, and a review that landed between your push and the poller start is missed. If your shell tool caps a call below the 15-minute default, set `--timeout-seconds` under that cap and run the poller again on a `timeout` result. ## Result Stdout is exactly one JSON object whose `kind` says what happened. Every outcome, including a timeout, exits `0`, so branch on `kind`. | `kind` | Meaning | Other fields | | -------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------- | | `review` | A watched reviewer submitted a review (any state). | `reviewer`, `review_id`, `state`, `review_commit_id`, `url`, `head_sha`, `cursor`, `head_moved` | | `comment` | A watched reviewer commented. | `comment_kind` (`issue` or `review`), `reviewer`, `comment_id`, `url`, `head_sha`, `cursor`, `head_moved` | | `head_changed` | The head commit changed: someone pushed. | `previous_head_sha`, `head_sha` | | `pr_closed` | The pull request was closed without merging. | `head_sha` | | `pr_merged` | The pull request was merged. | `head_sha` | | `timeout` | Nothing happened within the wait. | `waited_seconds`, `head_sha` | `head_moved` is present and `true` on a `review` or `comment` result when the pull request head is not the commit you passed with `--since-head-sha` (or, without it, the head at the start). The feedback may then be about commits your checkout does not have; [synchronize](#the-review-loop) before processing it. `cursor` is the GitHub time of the item the result reports. `--since <cursor>` counts only items created strictly after it, so the reported item and everything before it are skipped. An item created in the same second as the cursor is skipped too; the review loop reads the whole pull request after every event, so it is still read. When several things happened at once, the poller reports a closed or merged pull request first, then the earliest new review or comment by GitHub time (a review before a conversation comment before an inline comment when made in the same second), then a head change. Reporting the earliest keeps the `cursor` honest: resuming from it never skips an item that was still unreported. The poller reports one event; the review loop reads the whole pull request afterward, so nothing is lost. Copilot's reviews come from `copilot-pull-request-reviewer[bot]`, but its inline comments come from the login `Copilot`. The default reviewer set therefore sees its reviews and not its inline comments, which is enough because the review result is the signal. Add `--reviewer Copilot` to be woken by each inline comment as well. Stderr is NDJSON, one record per line, each with a `kind`: `wait_started` at the start, and on failure one diagnostic. | Exit code | Meaning | | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `0` | A result is on stdout (or help was printed). | | `1` | Failure; stdout is empty. Diagnostic kinds: `gh_unavailable`, `gh_not_authenticated`, `gh_api_error`, `malformed_response`, `commit_date_error`, `deadline_exceeded`, `interrupted`, `output_error`. | | `2` | Usage error or TTY refusal (`usage_error`, `tty_refusal`). | On exit `1`, read the `message` of the last stderr record. A signal (`SIGINT` or `SIGTERM`) stops the poller and any `gh` call in flight and reports `interrupted`. The `--timeout-seconds` budget is a hard bound. No request starts once it is spent, and a `gh` call still running at that moment is killed. The bound also covers reading the output of a `gh` call that has already exited: if a process `gh` started still holds its output pipes open when the budget ends or a signal arrives, the call fails and no partial output is used. A signal always ends the run with an `interrupted` failure (exit `1`). If the budget ends after the first look at the pull request, the result is `timeout` (exit `0`). If it ends before, nothing was observed, so it is a `deadline_exceeded` failure (exit `1`), not a `timeout` that would suggest a quiet pull request. The last look is taken one poll interval or less before the budget ends, so rerun with `--since-head-sha` and nothing in that gap is missed. ## The Review Loop 1. Run the poller with `--since-head-sha "$(git rev-parse HEAD)"`. After an event that needed no push, add `--since <cursor>` from that result. 2. Branch on `kind`: - `review` or `comment`: first make sure you are working on the commit the feedback is about. If `head_moved` is `true`, or `head_sha` differs from `git rev-parse HEAD`, run `git fetch` and bring the branch up to date with `git pull --ff-only` (or rebase your unpushed commits onto it) before processing; if that cannot be done safely, stop and tell the contributor. Then run the [`process-pr-review`](../process-pr-review/SKILL.md) workflow. Fetch every thread with [`fetch-review-threads`](../fetch-review-threads/SKILL.md) and read the new review body (a finding in a review body has no thread). - If you fixed something, commit, push, and reply. Then [watch the checks](#watching-the-checks) of the new head and fix any failure. Re-request the review only when the checks are green (see below), then go to step 1 with the commit you pushed. - If you pushed nothing, because the review was clean or the comment was informational, do not re-request the review. Go to step 1 with your `HEAD` and the `cursor` of this result, so this event is not reported again and nothing after it is missed. - `head_changed`: someone else pushed. Pull the branch, then go to step 1 with the new `HEAD`. - `timeout`: check the exit condition. If it is not met, run the poller again with the same flags as the run that timed out, since nothing was processed in between. After a few timeouts in a row, tell the contributor the reviewer has not answered. - `pr_closed` or `pr_merged`: stop watching and tell the contributor which one happened. 3. Count the round (see [Round Limit](#round-limit)) after each push, and stop and ask the contributor when the limit is exceeded. 4. Check the exit condition after each return. The loop is done when all of these hold: - Copilot's latest review is on the current head commit. Copilot never submits `APPROVED`, so its last review is `COMMENTED`; do not wait for an approval state. - Every check of the head commit finished and none failed, as [`watch-pull-request-checks`](../../ci/watch-pull-request-checks/SKILL.md) reports (skipped checks do not count as failures). - The pull request has no unresolved actionable review thread anywhere, including threads on earlier commits. Count them over the whole pull request with `fetch-review-threads`. - The other maintainer has commented `ACK <full head commit hash>` on the head commit, or the maintainer has recorded a waiver, as the Merge Readiness section of [`merge-pull-request`](../../git-workflow/merge-pull-request/SKILL.md) describes. 5. When the exit condition holds, print the maintainer merge command from `merge-pull-request`. Do not merge, approve, or post `ACK` yourself; an agent never waives a check. 6. Keep watching until the pull request is merged or closed; do not end the session when the exit condition first holds, and do not wait for the contributor to say it was merged. Run the poller again with `--since-head-sha "$(git rev-parse HEAD)"` and the `cursor` of the last result. - `pr_merged` or `pr_closed`: stop and report it. - `timeout`: run it again. Waiting costs no tokens, so there is no limit on repeats, but use the longest `--timeout-seconds` your shell tool allows to keep the number of calls small. - `review`, `comment` or `head_changed`: the pull request changed after it looked ready. Go back to step 2, because the exit condition no longer holds until it is checked again. The poller returns events and does not decide the exit condition. A review-body finding with no thread is not counted by the thread check, so read each new review body through the audit workflow. ## Watching the Checks CI is part of the loop, not a separate task. Checks finish in minutes, long before Copilot answers, so run them first: 1. After the first push, and after every later push, run `gh pr checks <PR> --watch --fail-fast` as [`watch-pull-request-checks`](../../ci/watch-pull-request-checks/SKILL.md) describes. It spends no tokens while it waits. 2. If no check starts and the runs say `action_required`, a maintainer has to approve them (see "When Checks Never Start" in that skill). Ask for it and wait; this is not a failure and does not count toward the check limit. 3. If a check failed, follow "When a Check Fails" in that skill: read the log, fix the change (or re-run the jobs when the cause is outside it), push, and watch again. 4. Only when every check is green, request the review (Copilot reviews the head as of the request, so a push after it is not covered) and run the poller. A review that arrives while checks are failing is still processed, but its fixes and the check fixes go in the same push when possible, so the checks run once. ## Round Limit A round is one push of fixes made in response to a review event. Stop the loop and ask the contributor before starting round 6, that is, when more than 5 rounds of fixes have been pushed. Rounds that ended with a clean review and no push do not count, and neither do timeouts. Failed check runs have their own limit of 5, because a check failure is not a review event. Count the distinct head commits of the pull request whose checks failed. Each one is a push that did not yet make the checks pass, so the count measures failed fixing attempts, not successful fixes. A re-run of the same commit does not add to it. Ask the contributor when more than 5 head commits have failed checks. Pass a large `--limit`, because `gh run list` returns only the 20 latest runs by default and a lower count would let the loop run past the limit: ```sh gh run list --repo torrust/torrust-tracker --branch <branch> --event pull_request --limit 1000 \ --json headSha,conclusion \ --jq '[.[] | select(.conclusion == "failure") | .headSha] | unique | length' ``` A loop that keeps finding new things after 5 rounds usually means something is wrong with the agent or its harness (for example a fix that regresses another finding, or a reviewer reacting to the agent's own replies). A person should look before more tokens are spent. Keep the count where any agent can recompute it, not in the session memory: it is the number of distinct reviews in the audit record (`docs/pr-reviews/pr-<PR_NUMBER>-review/PR-REVIEW.md`) that have at least one `FIXED` finding. A restarted session, or a different agent or harness taking over the pull request, therefore resumes with the right count instead of starting at zero. When asking, say how many rounds ran, which findings keep coming back, and what you suggest. The contributor can raise the limit for that pull request; record the new limit in the pull request conversation. ## Re-Requesting a Review After you push fixes for a reviewer, ask the same reviewer again. Do it only when new fixes need a new look; a clean review needs no re-request. The poller never does this. The same REST endpoint works for a maintainer and for Copilot; the Copilot reviewer is a bot, so its login ends in `[bot]`: ```sh gh api --method POST repos/torrust/torrust-tracker/pulls/<PR>/requested_reviewers \ -f 'reviewers[]=da2ce7' > .tmp/re-request.stdout 2> .tmp/re-request.stderr gh api --method POST repos/torrust/torrust-tracker/pulls/<PR>/requested_reviewers \ -f 'reviewers[]=copilot-pull-request-reviewer[bot]' > .tmp/re-request.stdout 2> .tmp/re-request.stderr ``` The Copilot call was verified on PR #2683 (2026-10-28): it exited `0`, GitHub recorded a `review_requested` timeline event for `Copilot` one second later, and Copilot submitted a review about 40 minutes after the request. Four things follow:
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub