GitHub Action · CLI · agent integrations

review-replay.

The CI for PR feedback. It checks whether every review comment was actually addressed (by a code change, a reply, or a resolved thread) and blocks the merge if anything is still pending.

Not a review generator (that's CodeRabbit, Copilot Review). Not a fix bot (that's Claude Code, Cursor). The verifier that closes the loop.

See the merge gate ↓ ★ Star on GitHub ☕ Buy me a coffee
$ go install github.com/alejandroSuch/review-replay/cmd/review-replay@latest

Three layers around a PR. Generation and implementation are crowded. The one that closes the loop wasn't:

layer 1 · generation

Write the feedback

An agent or human leaves the review comments.

CodeRabbit · Copilot Review · Greptile
layer 2 · implementation

Apply the fixes

Code gets pushed claiming "I addressed it".

Claude Code · Cursor agent · Ellipsis
layer 3 · verification

Did it actually land?

An independent check reads the thread + the code and decides, per comment.

review-replay

The merge gate

Drop the Action into a workflow, make it a required check, and a PR can't merge while a reviewer's concern is unanswered. Re-runs on every push, so the gate clears the moment the fix lands.

.github/workflows/review-replay.yml

# re-check whenever the PR gets new commits
on:
  pull_request:
    types: [synchronize, opened, reopened]

jobs:
  verify:
    runs-on: ubuntu-latest
    permissions: { pull-requests: read, contents: read }
    steps:
      - uses: alejandroSuch/review-replay/action@main
        with:
          mode: check            # exit 1 on any pending comment
          api-key: ${{ secrets.OPENROUTER_API_KEY }}
          # reviewer: copilot     # optional: gate only on Copilot's review
  Some checks were not successful
review-replay/ verify — Failing after 11sDetails
build— Successful in 1m
test— Successful in 2m
🔒
Merging is blocked — 1 review comment is still pending. Push a fix or reply in the thread; the check re-runs on the next commit.
Actions · job summary
acme/api#482 · head a7c1e3d
3 addressed · 1 partial · 1 pending · 1 needs-discussion
StatusCommentEvidence
addressedruleextract this into a helperthread resolved by reviewer
addressedllmrename tmp to something descriptivee4f5g6h
"Renamed to pendingWriteBuffer."
partialadd a test for the error pathi7j8k9l
happy-path test only
pendingrulethis should be debouncedno change since comment
needs-discussionshould we cache this at all?question, not a request

Every verdict ships with its evidence (the commit, the resolved thread, or the reply) so a human can sanity-check in one click. rule verdicts are deterministic and free; only the ambiguous ones cost an LLM call.

Run it in your terminal

Same engine, no CI needed. Run it before pinging the reviewer, or before a second-pass review, straight from the shell. --post drafts the replies and posts them for you (interactive by default).

review-replay acme/api#482
$ review-replay acme/api#482

acme/api#482 · head a7c1e3d
3 addressed · 1 partial · 1 pending · 1 needs-discussion

 # │ author │ status            │ conf │ evidence │ comment
───┼────────┼───────────────────┼──────┼──────────┼──────────────────────────
 1 │ alice  │ addressed         │ 1.00 │ -        │ extract this into a helper
 2 │ alice  │ addressed         │ 0.92 │ e4f5g6h  │ rename `tmp` to descriptive
 3 │ bob    │ partial           │ 0.74 │ i7j8k9l  │ add a test for the error path
 4 │ alice  │ pending           │ 0.95 │ -        │ this should be debounced
 5 │ carol  │ needs-discussion  │ 0.61 │ -        │ should we cache this at all?

# exit 1: 1 pending  (use --reviewer copilot to gate on one reviewer)

How it decides

GitHub marks a comment "outdated" when its line range shifts. That's not the same as resolved: a cosmetic refactor outdates a comment without fixing the bug; a correct fix in another file leaves it looking pending. review-replay reads intent, not line numbers.

1

Fetch the conversation

Review threads (with resolved state), review summaries, issue comments, and the commits that came after each one, via the GitHub API.

2

Build evidence packets

Per comment: did the region change at HEAD? Which later commits touched the file? Any replies? Chitchat and self-reviews filtered out.

3

Short-circuit the obvious

Thread resolved by its opener → addressed. No change, no replies, no commits → pending. No LLM call.

4

Classify the rest

The ambiguous ones go to the LLM of your choice (OpenRouter, OpenAI, Anthropic, Gemini) with a strict rubric and a confidence score.

5

Report with evidence

Status + confidence + the commit or thread that proves it + a draft reply. Table, --json, or a CI exit code.

6

Stateless, BYO key

No DB, no web app. Auth from gh / GITHUB_TOKEN. Only the diffs and comments needed for classification reach your LLM provider.

What counts as "addressed" (and what doesn't)

Every comment lands in one of four buckets. The call is about whether the reviewer's intent was satisfied, not whether a line moved.

StatusWhen it applies
addressedA later commit makes the change the reviewer asked for (the diff moves in that direction). Also counts if the reviewer who opened the thread resolved it, a question got an answer in the thread, or it was a non-blocking nit the reviewer pre-blessed skipping.
partialSome of the blocking asks landed, not all (e.g. "add tests" → only the happy path).
pendingNothing relevant changed since the comment, or the change doesn't go in the asked direction.
needs-discussionA question or a judgment call with no answer yet, not a concrete request.

GitHub gives you a few signals that look like "addressed" but mean different things. Here's what each one actually buys you:

Signal on GitHubWhat it really meansWhat review-replay does
Comment went outdatedThe line range shiftedIgnored. A cosmetic refactor outdates a comment without fixing it; a fix in another file leaves it looking pending. Reads intent, not line numbers.
A reply saying "done"A claim, not proofCounts as addressed only if the diff moved in the asked direction, or the reply names a commit SHA it can match.
Thread resolved by the reviewer who opened itThe person who raised it is satisfiedaddressed deterministically: a short-circuit rule, no LLM call.
Resolved by someone else, or never resolvedWeak or no signal: many teams never click ResolveTreated as a hint at most. The verdict comes from the code + the thread.

Install

A single Go binary for local and CLI use, the composite Action for CI, and ready-made rules for the popular AI assistants.

CLI

# prebuilt binary (linux/macOS/windows × amd64/arm64):
github.com/alejandroSuch/review-replay/releases/latest

# or with Go:
go install github.com/alejandroSuch/review-replay/cmd/review-replay@latest

# auth from your gh session:
export GITHUB_TOKEN="$(gh auth token)"
export OPENROUTER_API_KEY=<key>   # or OPENAI / ANTHROPIC / GEMINI

review-replay owner/repo#42

GitHub Action

- uses: alejandroSuch/review-replay/action@main
  with:
    mode: check
    api-key: ${{ secrets.OPENROUTER_API_KEY }}

Mark the job a required check to block merge on pending feedback.

Use it from your coding agent

Ready-to-copy rules and skills, one per tool. Same verification, invoked in chat: "did I address the reviews on this PR?"