pr-review
Address feedback left on a GitHub pull request: fetch unresolved review threads, make agreed Elixir/Phoenix code fixes, reply, and resolve. Use for a PR URL/number or reviewer comments. NOT for pre-PR review, findings triage, or CI monitoring.
Install
npx skills add https://github.com/oliver-kriska/claude-elixir-phoenix/tree/main/plugins/elixir-phoenix/skills/pr-review
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install oliver-kriska-claude-elixir-phoenix@llmmart
git clone https://github.com/oliver-kriska/claude-elixir-phoenix.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole oliver-kriska/claude-elixir-phoenix collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
PR Review Response
Close the review loop: fetch unresolved threads → fix → reply → resolve.
GitHub's isResolved is the state — re-runs are idempotent, handled
threads drop out automatically.
Usage
/phx:pr-review 42 # Triage unresolved threads on PR #42
/phx:pr-review 42 --fix # Triage + apply approved code fixes
/phx:pr-review https://... # Full URL also works (repo parsed from URL)
/phx:pr-review 42 --bots-only # Triage only CI bot threads (Copilot, Codex...)
/phx:pr-review 42 --no-resolve # Reply but leave threads open
Step 1: Resolve PR + Fetch Threads
gh pr view "$PR" --json number,title,state,baseRefName,headRefName,url,author
(accepts number or URL; URL also yields owner/repo). Then fetch ALL review
threads with thread IDs + resolved status — REST alone cannot do this:
cat > /tmp/review_threads.graphql <<'GQL'
query($owner:String!, $repo:String!, $pr:Int!, $cursor:String) {
repository(owner:$owner, name:$repo) {
pullRequest(number:$pr) {
reviewThreads(first:50, after:$cursor) {
pageInfo { hasNextPage endCursor }
nodes {
id isResolved isOutdated path line originalLine
comments(first:20) { nodes {
databaseId body createdAt
author { login __typename } } }
}
}
}
}
}
GQL
gh api graphql --paginate -F owner="$OWNER" -F repo="$REPO" -F pr="$PR" \
-F query=@/tmp/review_threads.graphql \
--jq '.data.repository.pullRequest.reviewThreads.nodes[]
| select(.isResolved == false)
| {threadId: .id, isOutdated, path, line: (.line // .originalLine),
firstCommentId: .comments.nodes[0].databaseId,
author: .comments.nodes[0].author.login,
isBot: (.comments.nodes[0].author.__typename == "Bot"),
body: .comments.nodes[0].body}'
Also fetch review summaries (gh api "repos/$OWNER/$REPO/pulls/$PR/reviews")
— they are NOT threads and cannot be resolved; surface CHANGES_REQUESTED
bodies separately. Bot detection: __typename == "Bot" / user.type == "Bot"
(the [bot] login suffix is NOT reliable across endpoints).
Step 2: Triage Table
Group by file, one row per thread. With --bots-only, keep only isBot rows.
| # | file:line | author | category | proposed action |
|---|
Categories: code-change ("should be", "use X instead") · question
("why", "how does") · nitpick ("nit:", style) · praise (no action) ·
discussion (architecture) · bot-finding (CI bot inline comment —
verify before accepting, many are false positives) · outdated
(isOutdated: true — line moved; default: reply "addressed in " +
resolve). Present the table and let the user greenlight threads.
Step 3: Per-Thread Loop
For each greenlit thread:
Read code at
path:line; check the suggestion against Iron LawsApply fix with a user-visible diff (only with
--fixor explicit ok)Draft reply (templates:
${CLAUDE_SKILL_DIR}/references/response-patterns.md)STOP — show diff + reply, get confirmation
Post reply — REST, targeting the thread's root comment:
gh api --method POST \ "repos/$OWNER/$REPO/pulls/$PR/comments/$FIRST_COMMENT_ID/replies" \ -f body="$REPLY_TEXT"Resolve the thread (skip with
--no-resolve):gh api graphql -f query='mutation($threadId:ID!){ resolveReviewThread(input:{threadId:$threadId}){ thread { id isResolved } }}' -F threadId="$THREAD_ID"
Mistake recovery: unresolveReviewThread takes the same input shape.
Step 4: Verify
mix compile --warnings-as-errors && mix test scoped to changed files.
Do NOT commit or push — leave that to the user.
Step 5: Final Summary
Print rollup: # | thread | action | status (replied/resolved/skipped).
List changed files. Optionally post a top-level conversation comment
(gh api --method POST "repos/$OWNER/$REPO/issues/$PR/comments" -f body=...)
with the rollup — only on user approval.
Iron Laws
- NEVER auto-post responses — Always show drafts and get explicit approval
- NEVER dismiss a review — Only the reviewer should dismiss
- Iron Laws override reviewer suggestions — If a suggestion violates an Iron Law, explain why in the reply
- Keep responses constructive — Acknowledge the feedback, explain reasoning
- Separate fixes from responses — Apply code changes in a distinct step
- NEVER resolve a thread without first posting a reply — every resolve is preceded by a reply on that thread explaining what was done
- NEVER claim a fix without a shown diff — no "should be fixed" replies without a user-visible change
- Bot findings get the same scrutiny as humans — decline Iron-Law-violating bot suggestions with explanation; never bulk-resolve "bot noise" without replies
Integration
PR receives review → /phx:pr-review {number} ← YOU ARE HERE
↓ fetch unresolved threads (GraphQL, paginated)
↓ triage table → user greenlights
↓ per thread: fix (diff) → reply → resolve
↓ verify (mix compile + test) → summary
Push changes → user handles git push
Next Steps
/phx:plan— if findings reveal scope gaps/phx:verify— full verification before pushing- Re-run
/phx:pr-reviewafter the next review round (idempotent)
References
${CLAUDE_SKILL_DIR}/references/response-patterns.md— Response templates and tone${CLAUDE_SKILL_DIR}/references/gh-commands.md— Full gh command reference (3 comment surfaces, pagination, bot detection)${CLAUDE_SKILL_DIR}/references/bot-triage.md— Batch-triaging CI bot review passes
Files (claude-elixir-phoenix)
-
references
-
bot-triage.md 3.5 KB
# Bot Review Triage CI bots (Copilot, Codex, CodeRabbit, SonarCloud) post review passes as inline review threads + a review summary. They produce volume — triage in batch, but never bulk-resolve without replies (SKILL.md Iron Laws 6 + 8). ## Known bot logins `copilot-*`, `codex`, `coderabbitai`, `sonarcloud`, `github-actions`, `dependabot`, `*-ci`. Detect via `__typename == "Bot"` (GraphQL) or `user.type == "Bot"` (REST) — see `gh-commands.md` for why the `[bot]` login suffix is unreliable. ## Batch flow (`--bots-only`) 1. Fetch unresolved threads, filter `isBot == true` 2. Classify each finding: | Verdict | Signal | Action | |---------|--------|--------| | **Real bug** | Reproducible, matches code behavior | Fix → reply with diff summary → resolve | | **Real but deferred** | Valid, out of this PR's scope | Reply "tracked as follow-up: {ref}" → resolve | | **False positive** | Bot misread the code | Reply with one-line explanation of why it's safe → resolve | | **Iron Law conflict** | Bot suggests an Iron Law violation | Reply declining with the law + reasoning → resolve | 3. Present the verdict table to the user BEFORE posting anything 4. Post replies + resolve only after approval ## Codex thread anatomy (chatgpt-codex-connector) Codex inline comments have a fixed shape — parse it, don't guess: - Priority badge: `` (P1 orange / P2 yellow / P3), then a **bold one-line title**, a detailed body, and a `Useful? React with 👍 / 👎.` footer. - Mapping: P0/P1 → treat as code-change/blocker; P2 → verify-then-fix; P3 → nitpick. Codex P1s on Elixir code have proven accurate (Ecto schema-field crashes, tsquery guards) — verify, but don't dismiss. - Reviews are per-commit (`Reviewed commit: <sha>` in the summary body) — after a force-push or big rebase, outdated codex threads are expected; handle via the standard outdated-thread rule. - Reply + resolve works exactly like human threads. Optionally react 👍/👎 on the finding itself — it trains the reviewer. - A codex review summary with ZERO inline threads = clean pass (summary-only round); nothing to triage. - A clean pass can also be a plain bot COMMENT ("Codex Review: Didn't find any major issues" + `Reviewed commit: <sha>`) or a 👍 reaction on the trigger comment / PR body — all three mean the same thing. ## Common false-positive patterns (Elixir) - **`nil[:key]` flagged as crash risk** — Access protocol on nil returns nil; nil-safe by design. Reply: "Access lookup on nil is nil-safe in Elixir (`nil[:key]` → `nil`); no guard needed." - **"Unused variable" on pattern-match bindings** — bindings used for match assertion, not value. Prefix with `_` only if truly unused. - **"Missing error handling" on `!` functions** — `Repo.get!`/`File.read!` crash intentionally per let-it-crash; supervised recovery is the design. - **Atom-vs-string key confusion in test fixtures** — bots often suggest atomizing external/JSON data; that violates Iron Law #10 territory (`String.to_atom` on input). Decline. ## What NOT to do - Never auto-resolve a bot pass to "clean up the PR" — each thread gets a reply first, even one line. - Never accept a bot's code suggestion verbatim without reading the surrounding code — bots see the diff hunk, not the module. - Never let a bot summary (review body) block on "resolution" — summaries are not threads and cannot be resolved; address inline findings instead. -
gh-commands.md 4.4 KB
# gh Command Reference — PR Review Threads Verified against gh 2.94.0 and the live GitHub API (2026-06). Repo is inferred from cwd, or parsed from a PR URL, or passed via `-R owner/repo`. ## The three comment surfaces These are different endpoints with different reply mechanisms. Mixing them up means replies silently land in the wrong place. | Surface | Read | Resolvable? | Reply | |---------|------|-------------|-------| | Inline review comment (file/line) | GraphQL `reviewThreads` or REST `pulls/{n}/comments` | YES (thread) | REST `.../comments/{id}/replies` or GraphQL thread reply | | Review summary ("Changes requested" body) | REST `pulls/{n}/reviews` | NO | Top-level conversation comment, or address inline and re-request review | | Top-level conversation comment | REST `issues/{n}/comments` (a PR IS an issue) | NO | `POST repos/{o}/{r}/issues/{n}/comments -f body=...` | ## PR metadata ```bash gh pr view "$PR" --json number,title,state,baseRefName,headRefName,url,author \ --jq '{number,title,state,base:.baseRefName,head:.headRefName,url,author:.author.login}' ``` `gh pr view` accepts a number OR a URL and resolves the repo from the URL. ## Review threads with resolve state (GraphQL — the core query) REST review comments have NO `isResolved` field and NO thread node ID. The GraphQL query in SKILL.md Step 1 is the only way to get both. Key fields: - `id` — `PRRT_...` thread node ID → used by resolve/unresolve and GraphQL reply - `comments.nodes[].databaseId` — integer REST comment ID → used by REST reply - `line` may be `null` on outdated threads → fall back to `originalLine` - `--paginate` auto-follows `endCursor` because the query exposes `pageInfo { hasNextPage endCursor }` and accepts `$cursor` - With `--paginate`, `--jq` runs PER PAGE. Add `--slurp` to merge pages into one array when counting/sorting across pages ## Reply REST (preferred when `databaseId` is in hand — reply to the thread's ROOT comment): ```bash gh api --method POST \ "repos/$OWNER/$REPO/pulls/$PR/comments/$COMMENT_ID/replies" \ -f body="$REPLY_TEXT" --jq '{reply_id: .id, in_reply_to: .in_reply_to_id}' ``` GraphQL (when only the thread ID is in hand): ```bash gh api graphql \ -f query='mutation($threadId:ID!,$body:String!){ addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$threadId, body:$body}){ comment { id databaseId url } }}' \ -F threadId="$THREAD_ID" -F body="$REPLY_TEXT" ``` ## Resolve / unresolve ```bash gh api graphql -f query='mutation($threadId:ID!){ resolveReviewThread(input:{threadId:$threadId}){ thread { id isResolved } }}' \ -F threadId="$THREAD_ID" --jq '.data.resolveReviewThread.thread' gh api graphql -f query='mutation($threadId:ID!){ unresolveReviewThread(input:{threadId:$threadId}){ thread { id isResolved } }}' \ -F threadId="$THREAD_ID" ``` An invalid node ID returns a clean `NOT_FOUND` — safe to surface as an error. ## Review summaries + conversation comments ```bash # Review summaries (incl. bot passes — Copilot/Codex post these) gh api "repos/$OWNER/$REPO/pulls/$PR/reviews" \ --jq '.[] | {reviewer: .user.login, isBot: (.user.type=="Bot"), state, body}' # Top-level conversation: read + post (post = outward-facing, confirm first) gh api "repos/$OWNER/$REPO/issues/$PR/comments" --jq '.[] | {user: .user.login, body}' gh api --method POST "repos/$OWNER/$REPO/issues/$PR/comments" -f body="$SUMMARY" ``` ## Bot detection | Source | Field | Bot value | |--------|-------|-----------| | GraphQL | `author.__typename` | `"Bot"` | | REST | `user.type` | `"Bot"` | | REST | `user.login` suffix | `[bot]` — **NOT reliable**: `pulls/{n}/comments` reports `Copilot` (no suffix) while `pulls/{n}/reviews` reports `copilot-pull-request-reviewer[bot]` | Use the type fields, not the login suffix. ## Edge cases - **Outdated threads** (`isOutdated: true`): line moved or was deleted; `line` is often null → use `originalLine`. Default action: reply "addressed in {commit}, line has since moved" + resolve. - **Multi-round reviews**: re-running is idempotent — the unresolved-only filter drops handled threads; GitHub's `isResolved` IS the state. - **Summary-only feedback** (CHANGES_REQUESTED with no inline threads): can't be resolved; reply as conversation comment and/or change code — the review state flips on re-request. - **No threads / all resolved**: report "No unresolved review threads on PR #N", but still surface CHANGES_REQUESTED summaries. -
response-patterns.md 4.3 KB
# PR Review Response Patterns Templates and patterns for responding to PR review comments. ## Response Templates by Category ### Code Change Responses **Accepting a suggestion:** ``` Good catch! Updated to use {suggestion}. {Brief explanation of why it's better if not obvious.} ``` **Accepting with modification:** ``` Agreed on the concern about {issue}. I went with {alternative} instead because {reason}. Let me know if you'd prefer your original suggestion. ``` **Declining with explanation:** ``` Considered this, but {reason for current approach}. Specifically, {concrete detail — e.g., "this pattern avoids N+1 queries when the association is loaded"}. Happy to discuss further. ``` **Iron Law conflict:** ``` This would violate Iron Law #{n}: {description}. The current code uses {pattern} to avoid {consequence}. See {reference} for the rationale. ``` ### Question Responses **Explaining a decision:** ``` {Direct answer}. I chose this approach because {reason}. {Optional: alternative considered and why rejected.} ``` **Explaining Elixir/Phoenix patterns:** ``` This uses {pattern name} — {1-sentence explanation}. In Phoenix, {brief context for why this is idiomatic}. See {HexDocs link} for more detail. ``` ### Nitpick Responses **Quick acknowledgment:** ``` Fixed, thanks! ``` **Style preference:** ``` Updated. I'll follow this convention going forward. ``` **Disagreement on style (rare):** ``` I see the preference for {their style}. This codebase uses {current style} consistently in {examples}. Happy to align either way — what do you think? ``` ### Discussion Responses **Architecture trade-off:** ``` Good point about {concern}. Current approach optimizes for {priority}. The trade-off is {downside}. We could mitigate with {option} if it becomes an issue. Want me to open an issue to track? ``` **Alternative suggestion:** ``` Interesting approach! Comparing: - Current: {pros/cons} - Suggested: {pros/cons} I lean toward {choice} because {reason}, but open to your perspective. ``` ## Common Elixir Review Patterns ### Pattern: "Use with instead of nested case" Reviewers often suggest `with` chains. Check if it improves readability — `with` is better for 3+ clauses, worse for 1-2. ### Pattern: "Missing typespec" If the project uses typespecs consistently, add them. If not, acknowledge and note it as a follow-up. ### Pattern: "Could use pattern matching" Usually correct for Elixir. Replace `if map[:key]` with function head pattern matching when possible. ### Pattern: "N+1 query concern" Always take seriously. Check with `Repo.preload` or `from(..., preload: [...])`. Reference Ecto Iron Law #5. ### Pattern: "Missing error handling" Check if the caller expects `{:ok, _}/{:error, _}` or if the function should raise. Match the context's convention. ### Pattern: "Test coverage" If reviewer flags missing tests, check what's needed using the testing-reviewer criteria (public functions, handle_events, workers). ## Tone Guidelines - **Be grateful**: Reviewers spend time improving your code - **Be specific**: Reference exact lines, functions, patterns - **Be brief**: Don't over-explain accepted changes - **Be honest**: If you don't know, say so and research - **Avoid defensive language**: "Actually..." or "Well..." - **Use code blocks**: Show the fix, don't just describe it ## Handling Conflicts Between Reviewers When multiple reviewers give conflicting feedback: 1. Identify the core concern each reviewer has 2. Find a solution that addresses both concerns 3. If irreconcilable, present both options and ask the PR author to decide 4. Never silently pick one reviewer's suggestion over another ## Batch Response Strategy When a PR has many comments: 1. Group related comments (same file, same concern) 2. Address with a single response referencing all locations 3. Use "Addressed in {commit SHA}" for code changes 4. Leave "Will address in follow-up" for non-blocking items ## Anti-patterns to Avoid - **Rubber-stamping**: Don't accept every suggestion blindly - **Arguing style**: If it's not an Iron Law, defer to team convention - **Ignoring context**: Read the full diff, not just the commented line - **Over-promising**: Don't commit to changes you haven't verified - **Emoji-only responses**: Always include at least a brief text acknowledgment
-
-
SKILL.md 6 KB
--- name: pr-review description: "Address feedback left on a GitHub pull request: fetch unresolved review threads, make agreed Elixir/Phoenix code fixes, reply, and resolve. Use for a PR URL/number or reviewer comments. NOT for pre-PR review, findings triage, or CI monitoring." effort: high argument-hint: <PR number or URL> [--fix] [--bots-only] [--no-resolve] --- # PR Review Response Close the review loop: fetch unresolved threads → fix → reply → resolve. GitHub's `isResolved` is the state — re-runs are idempotent, handled threads drop out automatically. ## Usage ``` /phx:pr-review 42 # Triage unresolved threads on PR #42 /phx:pr-review 42 --fix # Triage + apply approved code fixes /phx:pr-review https://... # Full URL also works (repo parsed from URL) /phx:pr-review 42 --bots-only # Triage only CI bot threads (Copilot, Codex...) /phx:pr-review 42 --no-resolve # Reply but leave threads open ``` ## Step 1: Resolve PR + Fetch Threads `gh pr view "$PR" --json number,title,state,baseRefName,headRefName,url,author` (accepts number or URL; URL also yields owner/repo). Then fetch ALL review threads with thread IDs + resolved status — REST alone cannot do this: ```bash cat > /tmp/review_threads.graphql <<'GQL' query($owner:String!, $repo:String!, $pr:Int!, $cursor:String) { repository(owner:$owner, name:$repo) { pullRequest(number:$pr) { reviewThreads(first:50, after:$cursor) { pageInfo { hasNextPage endCursor } nodes { id isResolved isOutdated path line originalLine comments(first:20) { nodes { databaseId body createdAt author { login __typename } } } } } } } } GQL gh api graphql --paginate -F owner="$OWNER" -F repo="$REPO" -F pr="$PR" \ -F query=@/tmp/review_threads.graphql \ --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | {threadId: .id, isOutdated, path, line: (.line // .originalLine), firstCommentId: .comments.nodes[0].databaseId, author: .comments.nodes[0].author.login, isBot: (.comments.nodes[0].author.__typename == "Bot"), body: .comments.nodes[0].body}' ``` Also fetch review summaries (`gh api "repos/$OWNER/$REPO/pulls/$PR/reviews"`) — they are NOT threads and cannot be resolved; surface `CHANGES_REQUESTED` bodies separately. Bot detection: `__typename == "Bot"` / `user.type == "Bot"` (the `[bot]` login suffix is NOT reliable across endpoints). ## Step 2: Triage Table Group by file, one row per thread. With `--bots-only`, keep only `isBot` rows. | # | file:line | author | category | proposed action | |---|-----------|--------|----------|-----------------| Categories: **code-change** ("should be", "use X instead") · **question** ("why", "how does") · **nitpick** ("nit:", style) · **praise** (no action) · **discussion** (architecture) · **bot-finding** (CI bot inline comment — verify before accepting, many are false positives) · **outdated** (`isOutdated: true` — line moved; default: reply "addressed in {commit}" + resolve). Present the table and let the user greenlight threads. ## Step 3: Per-Thread Loop For each greenlit thread: 1. Read code at `path:line`; check the suggestion against Iron Laws 2. Apply fix with a user-visible diff (only with `--fix` or explicit ok) 3. Draft reply (templates: `${CLAUDE_SKILL_DIR}/references/response-patterns.md`) 4. **STOP — show diff + reply, get confirmation** 5. Post reply — REST, targeting the thread's root comment: ```bash gh api --method POST \ "repos/$OWNER/$REPO/pulls/$PR/comments/$FIRST_COMMENT_ID/replies" \ -f body="$REPLY_TEXT" ``` 6. Resolve the thread (skip with `--no-resolve`): ```bash gh api graphql -f query='mutation($threadId:ID!){ resolveReviewThread(input:{threadId:$threadId}){ thread { id isResolved } }}' -F threadId="$THREAD_ID" ``` Mistake recovery: `unresolveReviewThread` takes the same input shape. ## Step 4: Verify `mix compile --warnings-as-errors && mix test` scoped to changed files. Do NOT commit or push — leave that to the user. ## Step 5: Final Summary Print rollup: `# | thread | action | status (replied/resolved/skipped)`. List changed files. Optionally post a top-level conversation comment (`gh api --method POST "repos/$OWNER/$REPO/issues/$PR/comments" -f body=...`) with the rollup — **only on user approval**. ## Iron Laws 1. **NEVER auto-post responses** — Always show drafts and get explicit approval 2. **NEVER dismiss a review** — Only the reviewer should dismiss 3. **Iron Laws override reviewer suggestions** — If a suggestion violates an Iron Law, explain why in the reply 4. **Keep responses constructive** — Acknowledge the feedback, explain reasoning 5. **Separate fixes from responses** — Apply code changes in a distinct step 6. **NEVER resolve a thread without first posting a reply** — every resolve is preceded by a reply on that thread explaining what was done 7. **NEVER claim a fix without a shown diff** — no "should be fixed" replies without a user-visible change 8. **Bot findings get the same scrutiny as humans** — decline Iron-Law-violating bot suggestions with explanation; never bulk-resolve "bot noise" without replies ## Integration ```text PR receives review → /phx:pr-review {number} ← YOU ARE HERE ↓ fetch unresolved threads (GraphQL, paginated) ↓ triage table → user greenlights ↓ per thread: fix (diff) → reply → resolve ↓ verify (mix compile + test) → summary Push changes → user handles git push ``` ## Next Steps - `/phx:plan` — if findings reveal scope gaps - `/phx:verify` — full verification before pushing - Re-run `/phx:pr-review` after the next review round (idempotent) ## References - `${CLAUDE_SKILL_DIR}/references/response-patterns.md` — Response templates and tone - `${CLAUDE_SKILL_DIR}/references/gh-commands.md` — Full gh command reference (3 comment surfaces, pagination, bot detection) - `${CLAUDE_SKILL_DIR}/references/bot-triage.md` — Batch-triaging CI bot review passes
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.