| name | pr-review |
| description | Review a pull request against godtools-shared project conventions. Use when asked to review a PR, check code quality, or audit changes. |
| argument-hint | ["pr-number"] |
| allowed-tools | Bash, Read, Grep, Glob, Write, Edit |
Review pull request $ARGUMENTS against the godtools-shared project conventions.
Steps
-
Check for dismissed issues by reading .claude/skills/pr-review/dismissed-issues.md if it exists.
Load all dismissed entries — each has a Pattern and Reason. You will use these to suppress matching findings later.
-
Fetch the PR diff and metadata. If $ARGUMENTS is provided, use it as the PR number:
gh pr diff $ARGUMENTS
gh pr view $ARGUMENTS
If no PR number is given (or the above fails because no upstream PR exists), fall back to reviewing the current branch against main:
git diff main...HEAD
git log main...HEAD --oneline
Use the branch name and commit log as the "title" in the review header.
-
Identify all changed files and categorize them (module, source set, tests, build config, etc.).
-
Pre-flight checks — run ktlint and lint, recording results for the review:
./gradlew :build-logic:ktlintCheck ktlintCheck
./gradlew lint
Any failures are reported as ❌ Must Fix items in the review output. They do not stop the rest of the review.
-
Review each category using the checklist below. Before outputting, cross-reference every finding against dismissed patterns — a finding matches when it describes the same class of issue (not necessarily the exact file/line). Move matched findings to a separate suppressed list.
-
Output a structured review (format below).
-
Post inline comments to the PR for every ⚠️ and ❌ finding that references a specific file and line number. Skip this step entirely when reviewing a branch with no PR — there is nowhere to post. Otherwise, before posting, deduplicate against all existing comments (resolved or not) to avoid re-posting anything already raised:
HEAD_SHA=$(gh pr view $ARGUMENTS --json headRefOid -q .headRefOid)
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
gh api repos/$REPO/pulls/$ARGUMENTS/comments --jq '[.[] | select(.in_reply_to_id == null) | {path, line, body}]'
For each finding, check whether any comment from the output above already covers the same file + line (or contains substantially the same text). Skip any finding that is already covered. Then bundle the remaining new comments into a single review submission:
gh api repos/$REPO/pulls/$ARGUMENTS/reviews \
--method POST \
--field commit_id="$HEAD_SHA" \
--field event="COMMENT" \
--field "comments[][path]=<file path>" \
--field "comments[][line]=<line number>" \
--field "comments[][side]=RIGHT" \
--field "comments[][body]=<finding text>
🤖 Posted by [Claude Code](https://claude.ai/code)" \
Use the exact file path from the diff (e.g. module/parser/src/.../Foo.kt) and the line number in the current version of the file (RIGHT side). Each comment body should contain the full finding description. Always append the attribution footer \n\n🤖 Posted by [Claude Code](https://claude.ai/code) to each comment. If no new actionable findings exist (only ✅ items or all already commented), skip this step.
-
If the review has no ❌ or ⚠️ findings (only ✅ and/or ⏭️ items), ask the user whether to post the full review. Skip this step entirely when reviewing a branch with no PR — branch review mode is local-only. Otherwise, if they say yes:
- Check whether the PR author matches the current git user (
gh pr view $ARGUMENTS --json author -q .author.login vs gh api user -q .login)
- If it is a self-review, post with
--comment (GitHub does not allow self-approval)
- If it is someone else's PR, ask whether to approve or just comment, then post with
--approve or --comment accordingly
- Always append
\n\n🤖 Posted by [Claude Code](https://claude.ai/code) to the body
-
After the review output, print:
---
To dismiss a finding so it won't appear in future reviews, say:
dismiss: <short title> — <reason>
Review Checklist
Multiplatform Source Sets
Parser Module (module/parser)
Renderer Module (module/renderer)
Renderer State Module (module/renderer-state)
Module Build Files
For any new module, check build.gradle.kts:
Testing
Paparazzi snapshot tests (renderer)
Unit tests (all modules)
Code Style
Ktlint and .editorconfig enforce most style rules (line length, formatter rules, constant naming) — step 4's pre-flight already covers those. Manual checks:
General Quality
JS Export Surface
Deprecated API Usage
Scan changed files for deprecated API calls. Flag each one as a Minor Issue (⚠️) with a suggested replacement.
To surface deprecated usages introduced or touched in the diff:
./gradlew testAndroidHostTest 2>&1 | grep -i "deprecat"
PR Hygiene
Output Format
Structure the review as (use ## PR Review: <title> (#<number>) when reviewing a PR, or ## Review: <branch-name> when reviewing a branch locally):
## PR Review: <title> (#<number>)
### Summary
<1–2 sentence summary of what the PR does>
### Checklist Findings
#### ✅ Looks Good
- <item>
#### ⚠️ Minor Issues
- <file:line> — <issue> — <suggested fix>
#### ❌ Must Fix
- <file:line> — <issue> — <suggested fix>
#### ⏭️ Suppressed
- <short title> — dismissed: <reason>
(omit this section entirely if nothing was suppressed)
### Overall Verdict
APPROVE / REQUEST CHANGES / COMMENT
<brief rationale>
Be specific. Reference file paths and line numbers. Cite the relevant convention from CLAUDE.md when flagging an issue.
Handling Dismissals
When the user says dismiss: <title> — <reason> (in any form — "dismiss the X issue because Y", etc.):
- Read
.claude/skills/pr-review/dismissed-issues.md if it exists (create it if not).
- Run
git config user.name to get the current user's name.
- Append a new entry in this format:
## <title>
**Pattern**: <describe the class of issue broadly enough to match future occurrences>
**Reason**: <reason the user gave>
**Dismissed**: <today's date as YYYY-MM-DD>
**Dismissed by**: <git user.name>
- If the current session reviewed a PR, find any open (unresolved) comment thread on that PR matching the dismissed issue. Use the GraphQL API to locate threads and resolve the matching one, replying with the dismissal reason first:
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
OWNER=${REPO%%/*}
REPONAME=${REPO##*/}
gh api graphql -f query="
{
repository(owner: \"$OWNER\", name: \"$REPONAME\") {
pullRequest(number: $PR_NUMBER) {
reviewThreads(first: 100) {
nodes {
id
isResolved
comments(first: 1) {
nodes { id body path line }
}
}
}
}
}
}"
Match the thread by file path, line number, or substantial text overlap with the dismissed finding. Then reply to the thread and resolve it:
gh api repos/$REPO/pulls/$PR_NUMBER/comments \
--method POST \
--field in_reply_to=<comment_id> \
--field body="Dismissed: <reason given by user>
🤖 [Claude Code](https://claude.ai/code)"
gh api graphql -f query="
mutation {
resolveReviewThread(input: { threadId: \"<thread_node_id>\" }) {
thread { id isResolved }
}
}"
- Confirm to the user what was added and that it will be suppressed in future reviews.