Review a pull request — analyze the diff, draft inline review comments, and submit a GitHub review. You are the reviewer.
Review a pull request as a code reviewer. Analyze the diff and surrounding code context, identify issues and areas for improvement, draft inline review comments, and submit them as a single atomic GitHub review — all with user approval before anything is posted.
Determine whether the current working directory is a local checkout of the PR's repository. This step performs detection only — do not clone anything. This skill never needs a local clone.
owner/repo and number from the URL pattern github.com/{owner}/{repo}/pull/{number}.gh repo view --json nameWithOwner --jq .nameWithOwner to extract owner/repo.gh pr view --json number,url,title,headRefName,baseRefName
After identifying the PR, determine whether the current directory is a checkout of the target repo:
gh repo view --json nameWithOwner --jq .nameWithOwner 2>/dev/null
Compare the result (case-insensitive) against the owner/repo extracted from the PR. Record the result as one of:
local — CWD is a checkout of the target repo. Use local file reads for code context.remote — CWD is not the target repo. Use the GitHub API for all file reads.If the context is remote, also extract the PR's headRefName and baseRefName for use in API-based file reads.
If no PR is found, inform the user and stop.
Check the user's invocation for a depth flag:
--quick or "quick review" → quick mode (diff + immediate context only)--deep, "thorough review", or no flag → deep mode (default — full file reads, imports, architecture)Gather all the information needed to perform the review.
gh pr view {number} -R {owner}/{repo} --json number,url,title,body,author,headRefName,baseRefName,headRefOid,additions,deletions,changedFiles
Store the headRefOid (HEAD commit SHA) — this is needed for posting inline comments.
gh pr diff {number} -R {owner}/{repo}
gh pr diff {number} -R {owner}/{repo} --name-only
Fetch existing reviews to avoid duplicating feedback that has already been given:
gh api graphql -f query='
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
reviews(first: 100) {
nodes {
state
body
author { login }
}
}
reviewThreads(first: 100) {
nodes {
id
isResolved
path
line
comments(first: 100) {
nodes {
body
author { login }
}
}
}
}
}
}
}
' -f owner="$OWNER" -f repo="$REPO" -F number="$PR_NUMBER"
Paginate if needed (check pageInfo.hasNextPage).
Read and understand every changed file in the context of the broader codebase. The depth of analysis depends on the mode selected in Step 1.
If context is local:
Read files directly from the local checkout.
If context is remote:
Read files via the GitHub API. Do NOT clone the repo. This skill never requires a local clone.
Read a specific file at the PR's head ref:
gh api "repos/{owner}/{repo}/contents/{path}?ref={headRefName}" --jq '.content' | base64 -d
Read a file at the base ref (to understand what changed):
gh api "repos/{owner}/{repo}/contents/{path}?ref={baseRefName}" --jq '.content' | base64 -d
Search for patterns across the repo (for convention checking):
gh api search/code -X GET -f q='{pattern}+repo:{owner}/{repo}' --jq '.items[].path'
For each changed file:
For each changed file:
For both modes, also:
Analyze across these categories:
| Category | What to look for |
|---|---|
bug | Logic errors, off-by-ones, null/undefined access, race conditions, wrong return values |
security | Injection vectors, auth bypass, secrets exposure, unsafe deserialization |
performance | Unnecessary allocations, N+1 patterns, missing memoization, unbounded operations |
error-handling | Swallowed errors, missing catch blocks, unhelpful error messages, unhandled edge cases |
logic | Dead code, unreachable branches, contradictory conditions, missing cases |
readability | Confusing naming, overly complex expressions, missing or misleading comments |
style | Inconsistency with surrounding code conventions (indentation, naming patterns) |
Assign each finding a severity that maps to its review impact:
Before recording a finding, check the existing reviews fetched in Step 2:
Before drafting any text that will be posted to GitHub, run tone-clone generate to sample the user's real writing:
tone-clone generate --stdout --type review_comment --limit 5
Study the output for: sentence length, punctuation patterns, capitalization, level of formality, use of contractions, how links and code are referenced. All drafted comment bodies and the summary body in the next step must match these patterns.
If tone-clone is not available or returns no results, fall back to the rules and examples in the voice and tone guide.
Compose the review as a set of inline comments plus a summary body.
For each finding, draft an inline comment. Each comment includes:
blocking, suggestion, nit, or question. Used for ordering and triage in the local presentation. Never included in the posted comment text.Comment writing guidelines:
All posted comment text must follow the voice and tone guide. Key points:
[blocking] or [nit]. The severity comes through in how you write it, not a label.UserService on line 45 expects a non-nil user" not "this might break something".Draft a summary review body. Keep it short and natural. A couple sentences covering the overall impression and the most important concerns. The inline comments carry the detail, so the summary doesn't need to enumerate everything.
All posted text must follow the voice and tone guide.
Based on the findings, recommend a review state:
Show the complete draft review for user approval before posting.
## Draft Review: PR #<number> — <title>
**Recommended state:** <APPROVE / REQUEST_CHANGES / COMMENT>
### Summary
<draft summary body>
For each inline comment, present using the embedded ascii-art format. Show the severity in the local header only (for the user's triage), not in the comment body that will be posted:
### Comment #<N>: `<file>:<line>` [<severity>]
Then the ascii-art block showing the code context and the draft comment in a box below the target line(s). The comment body shown here is exactly what will be posted to GitHub. For example:
```
40 │ async function getUser(id: string) {
41 │ const result = await db.query(id);
42 │ return result.name;
│
│ ┌─ Draft comment ─────────────────────────────────
│ │ `result` can be undefined if the query returns
│ │ no rows, this'll throw at `.name`. needs a
│ │ null check
│ └─────────────────────────────────────────────────
│
43 │ }
```
Use the Question tool with multiple: true to let the user select which comments to include in the review:
{
"questions": [
{
"header": "Select review comments",
"question": "Which comments should be included in the review?",
"multiple": true,
"options": [
{
"label": "#1 foo.ts:42 (blocking)",
"description": "Null guard missing on query result"
},
{
"label": "#2 bar.ts:15 (suggestion)",
"description": "Error message should include the request ID"
},
{
"label": "#3 baz.ts:88 (nit)",
"description": "Inconsistent naming: `getData` vs `fetchData` elsewhere"
}
]
}
]
}
The user can:
If the user modifies any comments, update the drafts and re-present the affected comments for confirmation.
If any selected comments will be posted as thread replies (reinforcing existing review threads), present those drafts separately under a "Thread Replies" heading after the inline comments. Use the same format:
### Thread Reply #<N>: `<file>:<line>` (replying to @<author>)
Followed by the draft reply text. These go through the same approval gate — nothing is posted until the user confirms.
Then ask for the review state:
{
"questions": [
{
"header": "Review state",
"question": "Submit this review as:",
"options": [
{
"label": "Comment",
"description": "General feedback, no explicit approval or rejection"
},
{
"label": "Approve",
"description": "Approve the PR with these comments"
},
{
"label": "Request Changes",
"description": "Request changes before this can be merged"
}
]
}
]
}
If the user selects nothing (no comments and no state), treat it as "cancel" — do not submit a review.
Execution mode for this run:
draft_only.execute_enabled only after the Step 6 Question gate returns Submit.execute_enabled to the exact selected payload (review state + inline comments + thread replies). Any payload change resets the mode to draft_only.Before Step 7, present the exact mutation plan and require a final submit gate:
COMMENT, APPROVE, or REQUEST_CHANGES)Then use the Question tool:
{
"questions": [
{
"header": "Submit review",
"question": "Submit this exact review now?",
"options": [
{
"label": "Submit",
"description": "Post exactly this state/comments/replies"
},
{
"label": "Cancel",
"description": "Do not post anything"
}
]
}
]
}
Only a direct user Question response of Submit in this run authorizes Step 7.
draft_only and stop.Post the review as a single atomic GitHub review using the addPullRequestReview GraphQL mutation. This submits all inline comments together as one review — not as individual comments.
Before any mutation call, run a strict preflight:
Submit) exists from a direct user Question response in this run.execute_enabled for this exact payload.If any check fails, return draft-only output with DRAFT_ONLY_BLOCKED and stop.
First, get the PR's GraphQL node ID:
gh api graphql -f query='
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
id
}
}
}
' -f owner="$OWNER" -f repo="$REPO" -F number="$PR_NUMBER" --jq '.data.repository.pullRequest.id'
Then submit the review. Write the full GraphQL request body (query + variables) to a temp file and use --input so that the comments array is passed as structured JSON, not a string:
cat <<'EOF' > /tmp/pr-review-request.json
{
"query": "mutation($prId: ID!, $body: String!, $event: PullRequestReviewEvent!, $commitOID: GitObjectID!, $comments: [DraftPullRequestReviewComment!]) { addPullRequestReview(input: { pullRequestId: $prId, body: $body, event: $event, commitOID: $commitOID, comments: $comments }) { pullRequestReview { id url } } }",
"variables": {
"prId": "$PR_NODE_ID",
"body": "$SUMMARY_BODY",
"event": "$EVENT",
"commitOID": "$HEAD_COMMIT_SHA",
"comments": $COMMENTS_JSON
}
}
EOF
gh api graphql --input /tmp/pr-review-request.json
$COMMENTS_JSON is a raw JSON array (not quoted) so it's embedded as structured data in the variables object. The other variables are strings and should be quoted.
Where $EVENT is one of COMMENT, APPROVE, or REQUEST_CHANGES, and $COMMENTS_JSON is a JSON array of objects:
[
{
"path": "src/foo.ts",
"position": 42,
"body": "`result` can be undefined if the query returns no rows, this'll throw at `.name`. needs a null check"
}
]
The position field is a 1-indexed offset within the file's diff. It is NOT the line number in the file. To convert a target file line number to a diff position:
gh pr diff {number} -R {owner}/{repo} | python3 -c "
import sys, re
target_file = 'PATH' # e.g. 'src/foo.ts'
target_lines = {42, 88} # file line numbers to find
in_file = False
pos = 0
new_line = 0
for line in sys.stdin:
line = line.rstrip('\n')
if line.startswith('diff --git'):
in_file = target_file in line
pos = 0
new_line = 0
continue
if not in_file:
continue
if line.startswith('@@'):
m = re.search(r'\+(\d+)', line)
if m:
new_line = int(m.group(1)) - 1
pos += 1
continue
if line.startswith('-'):
pos += 1
continue
if line.startswith('+') or line.startswith(' '):
pos += 1
new_line += 1
if new_line in target_lines:
print(f'pos={pos} new_line={new_line} -> {line}')
"
Run this for each file that has comments. The pos value is what goes in the position field.
If the target line is not in the diff (not an added, removed, or context line), find the nearest line that IS in the diff and adjust the comment text to reference the correct location.
After posting, confirm success and show the review URL:
Review submitted: <url>
State: <COMMENT / APPROVE / REQUEST_CHANGES>
Comments: <N> inline comments, <M> thread replies
When the user selects a comment that overlaps with an existing unresolved review thread (same file, same line, same concern), post a thread reply instead of creating a duplicate inline comment. Use the addPullRequestReviewThreadReply mutation:
gh api graphql -f query='
mutation($threadId: ID!, $body: String!) {
addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $threadId, body: $body }) {
comment { id url }
}
}
' -f threadId="$THREAD_ID" -f body="$COMMENT_BODY"
The $THREAD_ID comes from the id field on reviewThreads nodes fetched in Step 2.
Thread replies are posted individually (not part of the atomic review submission). Post them after the main review is submitted.
draft_only and must remain non-mutating until the Step 6 submit gate is completed.Submit in that same run.addPullRequestReview or addPullRequestReviewThreadReply unless the matching Step 6 checkpoint approval exists for that exact payload.addPullRequestReview, not as individual comment posts. This gives the PR author a single notification with all feedback, not a stream of individual comments.blocking means the code is broken or insecure, not that you prefer a different style. Misclassifying nits as blocking erodes trust.