rculbertson

pr-review-loop

Automates the AI reviewer response loop for GitHub PRs. Polls the PR for comments from an AI reviewer (Gemini, Copilot, etc.), addresses or disputes each comment, pushes changes, requests re-review, and repeats until the PR is approved. Optionally auto-merges when all comments are resolved. Invoked explicitly via "/pr-review-loop".

rculbertson 0 Updated 1w ago

Resources

1
GitHub

Install

npx skillscat add rculbertson/pr-review-loop

Install via the SkillsCat registry.

SKILL.md

PR Review Loop

Automates responding to AI reviewer comments on a GitHub PR. Polls for new comments, addresses or disputes them, pushes changes, and requests re-review — looping until approved.

Argument Parsing

The skill is invoked as:

/pr-review-loop [PR_NUMBER_OR_URL] [OPTIONS]

Parse the invocation arguments:

Argument Default Description
First positional arg (none) PR number (e.g. 42) or full GitHub PR URL
--auto-merge false Merge the PR automatically once approved
--reviewer <username> gemini-code-assist GitHub username of the reviewer bot
--review-command <text> /gemini review Comment body that triggers a new review
--poll-interval <seconds> 60 Seconds to wait between polls when no new comments
--resume <path> (none) Internal. Set by the loop's self-re-arm. Marks a woken cycle that resumes from an existing state file instead of starting fresh. Not normally passed by hand.

If no PR number or URL is provided, infer the PR from the current branch by running:

gh pr view --json number,headRefName,baseRefName,url

If that command fails (no associated PR), stop and tell the user no PR was found for the current branch.

Initialization

This skill runs one poll cycle per invocation and re-arms itself with ScheduleWakeup (see Poll Loop). There are two entry paths:

  • Fresh start (no --resume): run all of Initialization below.
  • Resume (--resume <path> is set): this is a woken cycle. Set STATE_FILE=<path>, skip steps 2–4 (no PR-metadata re-resolve, no state-file creation, no config summary print), load the persisted REREVIEW_TIME line if present, and go straight to the Poll Loop. Still resolve $PR and the other options (step 1) from the invocation args.
  1. Resolve the PR number and repo. If a full URL was given, extract the number from the path. Store the PR number as $PR.

  2. Fetch PR metadata:

    gh pr view $PR --json number,headRefName,baseRefName,title,url,reviewDecision

    Print the PR title and URL so the user can confirm the right PR is targeted.

  3. Create a state file and pre-populate it with already-addressed comment IDs:

    STATE_FILE=$(mktemp "${TMPDIR:-/tmp}/pr-review-loop-${PR}-XXXXXX")

    Fetch all inline review comments on the PR and check which ones already have a reply from the authenticated user (i.e. us). Pre-populate $STATE_FILE with those IDs so they are skipped:

    # Get our own username
    OUR_LOGIN=$(gh api user --jq '.login')
    
    # Find inline review comment IDs that already have a reply from us
    gh api repos/{owner}/{repo}/pulls/$PR/comments --jq \
      ".[] | select(.user.login==\"$REVIEWER\") | .id" | while read id; do
      # Check if any reply to this comment is from us
      replied=$(gh api repos/{owner}/{repo}/pulls/comments/$id/replies \
        --jq ".[] | select(.user.login==\"$OUR_LOGIN\") | .id" 2>/dev/null | head -1)
      if [ -n "$replied" ]; then
        echo "$id" >> "$STATE_FILE"
      fi
    done

    This means that on a fresh session, comments already replied to in a previous session are treated as already processed and will not be re-addressed.

    State-file format: the file holds one seen comment/review id per line, plus two optional bookkeeping lines that must survive across woken cycles (so they live in the file, not just in memory):

    • REREVIEW_TIME=<epoch> — when re-review was last requested; the clock for the no-comment termination (see Termination).
    • POLL_COUNT=<n> — number of cycles run; used by the "reviewer not found" guard (see Error Handling).
  4. Print a summary of the configuration:

    PR: #$PR — $PR_TITLE
    Reviewer: $REVIEWER
    Re-review command: "$REVIEW_COMMAND"
    Auto-merge: $AUTO_MERGE
    Poll interval: ${POLL_INTERVAL}s

Poll Loop

Each invocation runs exactly one poll cycle (Steps 1–8 below), then either terminates or re-arms itself with ScheduleWakeup and ends the turn. The waiting between cycles is handled by ScheduleWakeup parking the session — do not use a blocking sleep and do not use the Monitor tool. Drive each gh call directly with the Bash tool.

Step 1: Bump the poll counter, then check termination conditions

First increment the POLL_COUNT=<n> line in $STATE_FILE (replace the old line; start at 1 if absent). See the "Reviewer not found" guard in Error Handling.

Then check approval status via the Bash tool:

gh pr view $PR --json reviewDecision --jq '.reviewDecision'
  • If the result is APPROVED, run the Approved termination path (auto-merge if set), then stop without re-arming.
  • Otherwise, if a REREVIEW_TIME is recorded in $STATE_FILE and $(date +%s) - REREVIEW_TIME >= 600 (10 minutes with no new comments since re-review), run the No new comments after re-review termination path, then stop without re-arming.

Only if neither termination condition is met, continue to Step 2.

Step 2: Fetch new inline comments

Print: [HH:MM:SS] Fetching inline review comments...

Run via the Bash tool:

gh api repos/{owner}/{repo}/pulls/$PR/comments \
  --jq '.[] | select(.user.login=="REVIEWER") | [.id, .path, (.line // .original_line // 0), .body] | @tsv'

Parse the TSV output line by line. For each row (fields: id, path, line, body):

  • Skip if id is already in $STATE_FILE
  • Otherwise: append id to $STATE_FILE, print New inline comment on $path:$line (id=$id), and add to the list of comments to process this cycle

If no new comments found, print: No new inline comments.

Step 3: Fetch new PR-level review comments

Print: [HH:MM:SS] Fetching PR-level reviews...

Run via the Bash tool:

gh pr view $PR --json reviews \
  --jq '.reviews[] | select(.author.login=="REVIEWER") | select(.state=="CHANGES_REQUESTED" or .state=="COMMENTED") | select(.body != "") | [.id, .state, .body] | @tsv'

Parse the TSV output. For each row (fields: id, state, body):

  • Skip if id is already in $STATE_FILE
  • Otherwise: append id to $STATE_FILE, print New review (state=$state, id=$id), and add to the list

If no new comments found, print: No new PR-level reviews.

Step 4: No new comments — skip ahead

If both Step 2 and Step 3 found no new comments this cycle, there is nothing to process — skip Steps 5–7 and go straight to Step 8 (re-arm).

Step 5: Process each new comment

For each new comment collected in Steps 2–3, in order:

  1. ID is already marked seen — it was written to $STATE_FILE when discovered. No action needed here.

  2. Read the context — the comment body references specific files and lines. Use the Read tool to read the full relevant file(s) before deciding how to respond. Do not act on a comment excerpt without reading the actual code.

  3. Announce the comment and your intent — before doing anything, print:

    Comment from $REVIEWER:
    "$COMMENT_BODY"
    
    Proposed action: [one sentence describing what you will change, or that you will dispute this]
  4. Decide: agree or disagree — based on the comment's suggestion and the actual code:

    If you AGREE with the suggestion:

    • Make the code change using Edit or Write tools
    • Reply inline to the comment thread confirming the fix (same gh api call as the disagree path above), e.g. "Fixed — [one sentence describing what was changed]."
    • Note the change for the commit message
    • Do NOT commit yet — collect all agreed changes before committing

    If you DISAGREE with the suggestion:

    • Reply inline to the specific comment thread:
      • For inline review comments (have a $COMMENT_ID):
        gh api repos/{owner}/{repo}/pulls/$PR/comments/$COMMENT_ID/replies \
          --method POST --field body="I disagree with this suggestion because [specific technical reason]. [Explanation of why the current code is correct or the suggestion doesn't apply in this context]."
      • For PR-level review comments (have a $REVIEW_ID):
        gh api repos/{owner}/{repo}/pulls/$PR/reviews/$REVIEW_ID/comments \
          --method POST --field body="..."
    • Be specific and technical. Reference the code, the invariants, or the design decision that makes the suggestion incorrect or inapplicable.

Step 6: Commit and push (if changes were made)

If any code changes were made in Step 5:

  1. Review all changes: git diff
  2. Stage all modified files: git add -A
  3. Commit with a descriptive message:
    git commit -m "Address $REVIEWER review comments
    
    $(list the specific issues that were addressed)"
  4. Push: git push

If no changes were made (all comments disputed), skip to Step 7.

Step 7: Request re-review

If changes were pushed or any comments were disputed this cycle, post the re-review trigger comment so the reviewer can see and respond to the disputes:

gh pr comment $PR --body "$REVIEW_COMMAND"

Then record the re-review timestamp in the state file (this resets the no-comment termination clock). Replace any existing REREVIEW_TIME= line:

sed -i '' '/^REREVIEW_TIME=/d' "$STATE_FILE"
echo "REREVIEW_TIME=$(date +%s)" >> "$STATE_FILE"

Then print a status update to the user:

# If changes were pushed:
Pushed changes and requested re-review. Waiting for $REVIEWER to respond...

# If only disputes were posted (no code changes):
Posted disputes and requested re-review. Waiting for $REVIEWER to respond to the disagreements...

Step 8: Re-arm the loop and end the turn

The termination checks already ran at the top of this cycle (Step 1), so if execution reached here the loop should continue. Schedule the next cycle and then stop (end the turn) — do not loop in-process and do not sleep.

Call the ScheduleWakeup tool with:

  • delaySeconds: $POLL_INTERVAL (the runtime clamps to [60, 3600])
  • prompt: the resume invocation, re-passing the original options plus --resume:
    /pr-review-loop $PR --reviewer "$REVIEWER" --review-command "$REVIEW_COMMAND" [--auto-merge] --poll-interval $POLL_INTERVAL --resume $STATE_FILE
  • reason: a short note, e.g. "polling PR #$PR for $REVIEWER response"

Print Next check in ${POLL_INTERVAL}s (state: $STATE_FILE). Parking until then. and end the turn. The scheduled wakeup re-enters this skill via the resume path (Initialization), which runs the next single cycle.

Termination Conditions

When a termination condition is met (detected in Step 1), run the relevant path below and do not call ScheduleWakeup — ending the turn without re-arming stops the loop.

Approved: reviewDecision is APPROVED. Print a success message, run auto-merge if set, and stop without re-arming.

No new comments after re-review: no new comments from $REVIEWER have appeared within 10 minutes of requesting re-review. The REREVIEW_TIME=<epoch> line is written to $STATE_FILE in Step 7 and checked at the top of each cycle ($(date +%s) - REREVIEW_TIME >= 600). Treat the review as complete, run auto-merge if set, and stop without re-arming.

Auto-merge: If --auto-merge is set and the PR is approved or marked as complete:

gh pr merge $PR --squash --delete-branch

Use --squash by default. If the merge fails with a squash error (e.g., branch protection requires merge commits), retry with --merge.

Manual stop: The user can interrupt during a park or Ctrl+C at any time. Whenever the loop stops (terminating or parking), the $STATE_FILE path is printed so the session can be resumed later via --resume.

Error Handling

  • Rate limit: If gh returns HTTP 429 or a rate-limit message, wait 60 seconds before retrying (not the normal poll interval).
  • Push failure: If git push fails, print the error and stop — do not request re-review or loop further. The user needs to resolve the conflict manually.
  • Reviewer not found: Increment the POLL_COUNT=<n> line in $STATE_FILE at the start of each cycle (replace the old line). If POLL_COUNT reaches 10 and no comments from $REVIEWER have ever been seen (state file has no comment IDs), warn the user — "No comments found from '$REVIEWER' after N polls. Verify the reviewer username is correct." — and stop without re-arming so the loop does not park forever on a misconfigured reviewer name.
  • gh not authenticated: If gh commands fail with an auth error, stop immediately and tell the user to run gh auth login.

Implementation Notes

  • Batch changes: When multiple comments touch the same file, process them all before committing. One commit per poll cycle, not one commit per comment.
  • Read before editing: Always use the Read tool to see the current file state before making edits. Comment line numbers may be stale if earlier commits have shifted lines.
  • Commit message quality: Reference the specific issue(s) addressed, not just "address review comments".
  • Reviewer username is case-sensitive and exact: Match .author.login exactly.
  • Do not re-process seen comments: The state file is the source of truth. Always check it before processing a comment.
  • Stay on the PR branch: Verify you're on the correct branch with git branch --show-current if in doubt.