PR review threads
A PR has three independent comment surfaces. Cover all three to "read all reviews".
| Stream | What it is | How to read |
|---|---|---|
| PR conversation | Top-level issue comments | gh pr view N --comments |
| Review summaries | Body of each "Approve / Request changes / Comment" | gh pr view N --comments |
| Inline review threads | Diff comments + replies, grouped by file/line | GraphQL reviewThreads |
gh pr comment writes only to stream 1. There is no native gh subcommand for
reading nested inline threads or replying to one (cli/cli#12273). Use gh api.
URL anchor: pull/N#discussion_rNNN -> NNN is the comment's REST id and
GraphQL databaseId.
Reading all reviews on a PR
# Streams 1 + 2
gh pr view N --comments
# Stream 3: inline threads as a tree
gh api graphql -f query='
query($owner:String!,$repo:String!,$num:Int!,$cursor:String){
repository(owner:$owner,name:$repo){
pullRequest(number:$num){
reviewThreads(first:100,after:$cursor){
pageInfo{ hasNextPage endCursor }
nodes{
id isResolved isOutdated path line
comments(first:100){
pageInfo{ hasNextPage endCursor }
nodes{ databaseId author{ login } createdAt body url }
}
}
}
}
}
}' -f owner=OWNER -f repo=REPO -F num=N
-F num=N (typed Int), not -f.
Repeat with -f cursor=END_CURSOR until reviewThreads.pageInfo.hasNextPage is
false. For any thread whose comments.pageInfo.hasNextPage is true, paginate
its comments separately:
gh api graphql -f query='
query($thread:ID!,$cursor:String){
node(id:$thread){
... on PullRequestReviewThread{
comments(first:100,after:$cursor){
pageInfo{ hasNextPage endCursor }
nodes{ databaseId author{ login } createdAt body url }
}
}
}
}' -f thread=PRRT_xxxx -f cursor=END_CURSOR
Repeat until comments.pageInfo.hasNextPage is false. Omit cursor on each
first request.
Shape:
- Each
reviewThreads.nodes[]is one diff-anchored thread. comments.nodes[0]is the original;[1..]are replies in order.id(PRRT_*) is the GraphQL thread ID, needed for resolve.databaseIdis the REST id (theNNNindiscussion_rNNN), needed to reply or to find a thread by URL.
Filter examples (--jq):
# Only unresolved threads
... --jq '.data.repository.pullRequest.reviewThreads.nodes
| map(select(.isResolved|not))'
# Only CodeRabbit threads
... --jq '.data.repository.pullRequest.reviewThreads.nodes
| map(select(.comments.nodes[0].author.login == "coderabbitai"))'
# Find the GraphQL thread.id given a discussion_rNNN
... --jq --argjson id NNN '.data.repository.pullRequest.reviewThreads.nodes[]
| select(.comments.nodes[].databaseId == $id) | .id'
Replying to a thread
gh api repos/OWNER/REPO/pulls/comments/NNN \
--jq '{id, path, body: (.body|.[0:200])}'
gh api repos/OWNER/REPO/pulls/N/comments/NNN/replies \
-X POST \
-f body="$(cat <<'EOF'
Reply body. Markdown OK.
EOF
)" --jq '.html_url'
Replying to an AI reviewer (CodeRabbit, cubic, Claude, etc.)
AI reviewers persist your reply as a learning for future reviews (CodeRabbit shows an "✏️ Learnings added" block confirming it). Write the reply so the stored learning is correct AND generalizable.
Decide whether to reply
Do not reply to every finding.
- Applied inline finding: do not reply or resolve it. The reviewer can inspect newer commits and resolve the thread automatically
- Out-of-diff finding: reply in a top-level comment and tag the reviewer
- Rejected finding: reply in its thread and tag the reviewer. Reject only when the claim is invalid or the fix is not worth its cost
Do not post acknowledgements, "fixed" replies, commit references, or summaries of applied findings.
Always tag the bot
Start every reply to an AI reviewer with its handle, on both surfaces (inline
thread reply and top-level comment). The tag is the bot's GitHub login, prefixed
@.
| Reviewer | Handle |
|---|---|
| CodeRabbit | @coderabbitai |
| cubic | @cubic-dev-ai |
| Claude | @claude |
| Copilot | @copilot |
Unknown bot: read its login from the thread (comments.nodes[0].author.login)
and tag that.
A tag in a thread reply is harmless; a missing tag on a top-level comment means the reply silently goes nowhere. Tag always - do not reason about which surface needs it.
Reply style
- Terse, low word count. Facts + rationale only. No pleasantries, no closing, no restating the finding.
- Lead with the rejection ("Skipping. YAGNI." / "Wrong - X is never null here.").
- One or two lines is the target. Prose paragraphs are wrong.
- Give the verifiable evidence: command run, files inspected, why the rule applies or doesn't.
- Phrase the rationale in terms that generalize beyond this file/line. "For sync
helpers in this codebase, ..." beats "for
with_modifiable, ...". The reviewer copies your wording into the learning - narrow phrasing produces a one-off rule. - If the AI reviewer should change behavior next time, say so directly. ("Grep callers before flagging defensive guards on sync helpers.")
- Do not include code blocks or diffs unless correcting the suggestion. The thread already has the suggested code.
Example:
@coderabbitai Skipping. All 4 callers of `parse_config` already validate the
path upstream (`rg 'parse_config\(' src/`). Defensive guards on internal sync
helpers in this codebase are dead code.
Out-of-diff findings live in the review body
CodeRabbit anchors what it can to a diff line (stream 3) and puts the rest in
the review summary body (stream 2, gh pr view N --comments):
- "Outside diff range" comments - findings on code the diff did not touch
- Nitpicks and duplicate findings, inside collapsed
<details>blocks - The actionable-comments count, which covers streams 2 AND 3
Reading only reviewThreads therefore misses real findings, and the ones it
misses skew severe: a finding that could not anchor to a changed line is often
one about code the change broke elsewhere.
- Read stream 2 to its last line. Collapsed
<details>content is in the body textghreturns; do not stop at the first visible section. - Reconcile the count: the body states N actionable comments. Fewer found across both streams means the read was incomplete, not that the rest do not exist.
- An out-of-diff finding has no thread, so it has no
databaseIdand cannot be replied to or resolved as a thread. Answer it in a top-level comment, tagged with the bot's handle.
Resolving a thread
REST has no resolve endpoint. Mutation needs the GraphQL thread id (PRRT_*),
not the REST comment id - look it up via the reviewThreads query above.
Do not manually resolve AI-review threads after applying a finding. Use this mutation only when the user explicitly requests manual resolution.
gh api graphql -f query='
mutation($id:ID!){
resolveReviewThread(input:{threadId:$id}){ thread{ isResolved } }
}
' -f id=PRRT_xxxx
Common mistakes
| Mistake | Symptom | Fix |
|---|---|---|
Used repos/.../pulls/{n}/comments/{id} to fetch one comment |
404 Not Found | Drop the /{n}: repos/.../pulls/comments/{id} |
Used POST /pulls/{n}/comments with in_reply_to |
422 "in_reply_to is not a permitted key" | Use /pulls/{n}/comments/{id}/replies |
Passed numeric ID with -f |
422 "not a number" | Use -F |
| Resolving with the REST comment id | GraphQL: invalid ID | Look up thread.id (PRRT_*) via reviewThreads |