| name | post-pr-review |
| description | Post a single non-blocking GitHub PR review with inline line-anchored comments generated by the agent. Every comment is prefixed with `[cursor]` so reviewers can tell they are AI-generated. Only Critical and Warning findings are posted; Suggestions / Nice-to-haves / Good-to-haves are never sent. Use when the user asks to "post the review on GitHub", "post these comments on the PR", "post the findings as PR comments", or otherwise wants an in-chat code review persisted on a GitHub pull request. Typically runs as a follow-up to the `branch-review` skill but does not require it. |
Post PR Review
Posts an AI-generated code review to a GitHub pull request as a single COMMENT review with per-finding line-anchored comments. Designed to be a safe, clearly-marked way to surface Cursor's review findings to a PR without requesting changes or approving.
Non-negotiable rules
- Event type is always
COMMENT. Never REQUEST_CHANGES, never APPROVE. The human reviewer decides whether to block the PR.
- Every inline comment body starts with
[cursor] (literal, lowercase, single space after the closing bracket). The overall review body also begins with [cursor].
- Only Critical and Warning findings are posted. Everything else — Suggestion, Nice to have, Good to have, Optional, Style nit, Question — is dropped before the payload is built. If after filtering there are zero findings left, abort and tell the user instead of posting an empty review.
- Each inline comment must be line-anchored to a line in the PR diff. Use
side: "RIGHT" and the line number from the current head of the PR branch. Un-anchorable findings (e.g. "the package has no tests") must be anchored to a sensible representative line in a new/changed file, not posted as a top-level comment.
- Post via
gh api with a JSON --input file, not by stringifying the payload inline. JSON escaping in shell is how reviews get corrupted.
Prerequisites
command -v gh
gh auth status
If gh is missing or unauthenticated, stop and ask the user to install and run gh auth login — do not attempt to post via raw curl.
Workflow
Copy this checklist and track progress in your response:
- [ ] Step 1: Identify the PR (number, owner, repo)
- [ ] Step 2: Collect findings from the conversation
- [ ] Step 3: Filter to Critical + Warning only
- [ ] Step 4: Verify each finding has a file path and anchor line
- [ ] Step 5: Build the review JSON payload
- [ ] Step 6: Show the user a summary and confirm
- [ ] Step 7: POST the review
- [ ] Step 8: Verify response and surface the review URL
- [ ] Step 9: Clean up temp file
Step 1: Identify the PR
Resolve in this order:
- If the user provided a PR number or URL, use it.
- Else, look up the PR for the current branch:
BRANCH=$(git rev-parse --abbrev-ref HEAD)
REMOTE_URL=$(git config --get remote.origin.url)
gh pr list --head "$BRANCH" --state open --json number,url,title,headRefName --limit 5
- If zero or multiple matches, ask the user which PR.
Record owner, repo, and pull_number — you will need them in Step 7.
Step 2: Collect findings from the conversation
Findings almost always come from a preceding branch-review pass in the same chat. Extract them as structured items. Do not re-run the review; reuse what was already produced.
For each finding, capture:
- Severity:
Critical | Warning | Suggestion | anything else
- File path (repo-relative, forward slashes)
- Line number (must be a line in the PR diff's RIGHT side)
- Title (one-line summary — becomes the first line of the comment body)
- Body (the multi-line explanation)
Step 3: Filter to Critical + Warning only
Keep only items whose severity matches (case-insensitive) one of:
Critical
Warning
Must fix
Should fix
Blocker
Bug
Drop (silently — do not warn the user individually):
Suggestion
Nice to have
Good to have
Optional
Style, Nit, Style nit, Polish
Question
Praise / Positive
If zero findings remain after filtering, stop and tell the user:
"After filtering, 0 Critical/Warning findings remain. Nothing to post — the review isn't worth creating. (Dropped: N suggestions / nice-to-haves.)"
Do not post an empty review.
Step 4: Verify each finding has a file path and anchor line
For each remaining finding:
- Confirm the
path exists in the PR diff:
gh pr diff <pull_number> --name-only | grep -F -x "<path>"
- Confirm the
line number is reachable on side: "RIGHT" (i.e. it exists in the PR head version of that file, not only in base). Read the file at the branch HEAD to confirm the line is present:
git show HEAD:"<path>" | sed -n '<line>p'
- If a finding is about the absence of something (e.g. "no unit tests for this package"), anchor it to a representative line inside a new/changed file in the same package (e.g. the
func PrintBanner( line). Do not fabricate line numbers.
If any finding cannot be anchored, either (a) re-anchor it to a reasonable line or (b) drop it and note the drop in the final summary you give the user.
Step 5: Build the review JSON payload
Write to a temp file under /tmp. Never inline the JSON into the shell command.
Payload shape:
{
"event": "COMMENT",
"body": "[cursor] Automated review — N findings (C critical, W warning). See inline comments.",
"comments": [
{
"path": "path/to/file.go",
"line": 123,
"side": "RIGHT",
"body": "[cursor] **One-line title.**\n\nMulti-line explanation...\n\nSuggested fix: ..."
}
]
}
Rules for comment bodies:
- Prefix: begins with
[cursor] exactly once. Never [CURSOR], never **[cursor]**, never (cursor).
- Title: the one-line finding title should be bolded on the first line after the prefix, e.g.
[cursor] **Double-close race in stopSpinnerLocked.**
- Body: separate from the title by a blank line. May include code blocks, file paths in backticks, and quoted BUGBOT rules when they motivated the finding.
- Keep it self-contained: the reader on GitHub may not have the chat context. Include the "why" and a concrete suggested fix.
- No emojis unless the user explicitly asked the
branch-review to use them.
Event-level rules:
"event": "COMMENT" — hard requirement.
- The overall
body is a short, factual one-liner that counts the findings. It starts with [cursor] so the review card on GitHub is identifiable at a glance.
Write the file:
cat > /tmp/pr-review-<pull_number>.json <<'JSON'
{ ...payload... }
JSON
Or, preferred when building programmatically, write via the Write tool.
Step 6: Show the user a summary and confirm
Before posting, display a compact table so the user can sanity-check:
PR: yugabyte/yb-voyager#3497
Event: COMMENT (non-blocking)
Review body: [cursor] Automated review — 6 findings (0 critical, 6 warning)...
| # | Severity | File | Line | Title |
|---|----------|----------------------------------------------|-----:|------------------------------------------------------|
| 1 | Warning | yb-voyager/cmd/assessMigrationCommand.go | 330 | Verbose output undermines Preflight block |
| 2 | Warning | yb-voyager/src/ux/progress.go | 194 | Latent double-close race in stopSpinnerLocked |
...
Dropped 8 Suggestion/Nice-to-have findings.
Ask the user if they want any edits before posting. If they say "go" / "post" / "send", continue.
Step 7: POST the review
gh api --method POST \
/repos/<owner>/<repo>/pulls/<pull_number>/reviews \
--input /tmp/pr-review-<pull_number>.json \
> /tmp/pr-review-<pull_number>-response.json 2>&1
echo "exit=$?"
Do not use gh pr review — its flags don't support per-line comments cleanly. Use the raw API.
Step 8: Verify and surface the URL
Parse the response:
python3 -c "
import json, sys
d = json.load(open(sys.argv[1]))
print('review_id:', d.get('id'))
print('state:', d.get('state'))
print('html_url:', d.get('html_url'))
print('n_comments:', len(d.get('comments_url','')) and 'see review')
" /tmp/pr-review-<pull_number>-response.json
If state is not COMMENTED, show the raw response to the user and stop. Common failure modes:
422 Unprocessable Entity with "Pull request review thread line must be part of the diff" — a finding anchored to a line outside the diff. Drop or re-anchor that finding and retry.
401 Bad credentials — token expired. Tell the user to re-auth.
403 Resource not accessible — token lacks repo scope for the target repo.
On success, give the user a short confirmation with the html_url.
Step 9: Clean up
rm -f /tmp/pr-review-<pull_number>.json /tmp/pr-review-<pull_number>-response.json
Anti-patterns
- Never stringify JSON in the shell. Always
--input <file>. Shell-quoting bugs have silently dropped inline comments before.
- Never use
REQUEST_CHANGES or APPROVE. The agent does not have the standing to block or approve a human's PR.
- Never post a review containing only suggestions. Drop them in Step 3 and abort if nothing is left.
- Never omit the
[cursor] prefix, even when the comment quotes a BUGBOT rule or references another comment. The prefix is the contract with the human reviewers.
- Never re-run
branch-review to get findings. If the conversation already produced findings, reuse them. Re-running wastes tokens and produces noise.
- Never invent line numbers. If you can't anchor a finding, drop it or ask the user where to anchor it.
Example: minimal happy path
gh api --method POST \
/repos/yugabyte/yb-voyager/pulls/3497/reviews \
--input /tmp/pr-review-3497.json \
> /tmp/pr-review-3497-response.json
rm -f /tmp/pr-review-3497.json /tmp/pr-review-3497-response.json
Example comment body
[cursor] **Latent double-close race in `stopSpinnerLocked`.**
This method temporarily releases `t.mu` while waiting on `spinnerDone`, then
re-acquires it to zero out `spinnerStop`. If any other goroutine grabs `t.mu`
during that release window, it will see a still-non-nil `spinnerStop`, call
`close(t.spinnerStop)` on an already-closed channel, and panic.
Today the tracker is driven by a single external goroutine, so this doesn't
trigger in practice. But the mutex nominally protects exactly this invariant.
Suggested fix: zero `t.spinnerStop` (and `spinnerDone`) **before** the
`t.mu.Unlock()`, then wait on the (local-variable captured) `spinnerDone`
channel without the lock.
Scope limits
This skill is only for posting reviews, not producing them. If the user hasn't done a review yet, suggest they run the branch-review skill first, then invoke this one.