| name | replying-to-pr-review-threads |
| description | Use when a `discussion_rNNN` URL anchor appears, when replying to a specific PR review comment from a bot or AI reviewer (CodeRabbit, cubic, Claude, Copilot) or a human reviewer, reading all reviews on a PR with their replies as a tree, or resolving review threads from the CLI. Also covers where AI reviewers put out-of-diff findings and the mandatory `@handle` tag when replying. Triggers on "reply to coderabbit", "reply to cubic", "reply to the bot", "answer the bot review", "address bot reviews", "answer AI review comments", "answer this review comment", "read all reviews on PR", "show review threads", "resolve this thread".
|
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
gh pr view N --comments
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.
databaseId is the REST id (the NNN in discussion_rNNN), needed to reply
or to find a thread by URL.
Filter examples (--jq):
... --jq '.data.repository.pullRequest.reviewThreads.nodes
| map(select(.isResolved|not))'
... --jq '.data.repository.pullRequest.reviewThreads.nodes
| map(select(.comments.nodes[0].author.login == "coderabbitai"))'
... --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
text gh returns; 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
databaseId and 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 |