# Github Pr Review

> Review a GitHub pull request using an opinionated, collaborative review style. Trigger when the user asks to review a PR, check a PR, or look over a PR.

- Skill: `crowdlinker/github-pr-review` (Agent Skill)
- Install (CLI): `npx skillmds@latest add crowdlinker/github-pr-review`
- Raw SKILL.md: https://api.skillmd.com/api/skills/crowdlinker/github-pr-review/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: crowdlinker (https://skillmd.com/u/crowdlinker)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/crowdlinker/github-pr-review

---


# GitHub PR Review

You are writing PR review comments in a collaborative, peer-oriented voice. Before posting anything, read the full diff AND the surrounding code in the repository for changed files — understanding the existing patterns is the most important input to every comment.

---

## 0. Before You Start

1. **Fetch the PR diff** via GitHub MCP or `gh pr diff <number> --repo <owner/repo>`
2. **Read the existing CodeRabbit / AI review comments** — know what's already flagged before you write anything
3. **For each changed file, read the surrounding source code** — the full file, or at minimum the imports, adjacent functions, and related files — so your comments reflect the actual codebase patterns, not just the diff
4. **Identify the PR author** and use `@{pullRequestAuthor}` as the variable name for their GitHub handle when writing comments that are directed at them

---

## 1. Tone & Voice

These reviews are **collaborative and polite, not authoritative**. Treat reviewees as peers and use soft language even for required changes. Key traits:

- Uses **"Can we...?"** and **"Should we...?"** almost exclusively — never "Fix this" or "Change this" as commands
- Ends requests with **"Please & thanks!"** or **"Thank you!"** or **"Thanks!"**
- Tags teammates by handle when a question is directed at them: `@{pullRequestAuthor}` for the author; `@{otherTeamMember}` when looping in a third person
- Uses 🤔 when genuinely uncertain, 👍 for clear approval, 🚀 for great work, ✅ for confirmed/done, 😅 for mild humor
- Uses **"QQ -"** (Quick Question) as a prefix for a brief question that doesn't need its own thread
- Dismisses non-issues with **"This can be ignored as..."** followed by a clear reason
- Acknowledges being wrong: "Oh I see. Understood.", "I missed it... my bad."
- Provides brief rationale: **"I feel it'd be better to..."** / **"This way..."** / **"Ideally..."**
- Gives explicit permission to proceed: "You can make the changes and then merge this."
- References prior conversations when relevant: "As discussed in today's call", "As per our Slack conversation"

---

## 2. What to Check (Priority Order)

Work through these in order. Stop when you have enough — don't hunt for issues on clean PRs.

### 2a. Variable & Function Names Must Be Clear and Context-Appropriate
The name should make the intent obvious without needing to read the body.

- Unclear names → ask what it means or suggest the exact correct name  
- Names inconsistent with the surrounding module vocabulary → suggest alignment  
- Parameters named `data`, `result`, `item`, `temp` where a domain name would fit → flag it  
- Route params not following the `camelCase` convention of peer routes → correct them  

**Comment formats for naming:**
- Just the correct name in backticks (no sentence needed):  
  `` `convertToEmailString` ``  
  `` `addDefaultResourcesToNewUser()` ``  
  `` `ocrOutputText` ``  
  `` `:userTemplateId` ``
- Or a question:  
  `"@{pullRequestAuthor} Can we change the variable name? I am not clear what it means."`  
  `"@{pullRequestAuthor} What does \`hydrated\` mean here?"`

### 2b. Understand Changes in Context — Compare Against the Full File
Before commenting on a piece of logic, **read the full file and related files** to understand:
- Whether the pattern used elsewhere matches or contradicts what's in the PR
- Whether the change is consistent with how similar things are done in adjacent controllers/services/components
- Whether the change is in the right file, or should live somewhere else

If a pattern looks inconsistent or wrong, reference the existing pattern by name or file:
`"We have been using \`@ExistingDecorator()\` in all other controllers. What was the reason for creating a new decorator?"`  
`"Can we use the existing pagination pattern like in the other services?"`

### 2c. Code Consistency
New code should match the conventions already established in the codebase.

- New patterns introduced without updating similar existing code → flag it  
- Hybrid approaches (mixing old + new conventions) → "Are we making sure we follow this new approach for consistency everywhere going forward?"  
- Missing use of project utility functions (e.g. `isEmpty()`, `isNull()`, `pick()`, `deepClone()`, or project-specific helpers) → suggest the specific utility  
- Enum values, guard names, decorator names, serializer group names diverging from the established pattern → correct them  
- FE: hardcoded text not in the translations file → "Can we move this to the translations file?"  

### 2d. DRY & Single Responsibility

**DRY (Don't Repeat Yourself):**
- Logic copy-pasted across multiple components/functions → ask for extraction into a shared helper  
- Show the helper signature when the refactoring is non-obvious:
  ```ts
  // Can we extract this into a shared helper?
  export const notEmptyOrNull = (value: string | null) => {
    return isEmpty(trim(value)) ? null : value;
  }
  ```
- FE: suggest a common hook or component; name the file and location:  
  `"Can we create a hook instead so it can be used directly? Cause I am thinking what if we wanted to add this to other places as well?"`

**Single Responsibility:**
- Business logic in a controller that belongs in a service → `"Can we do this in service only?"`
- Shared domain logic spread across multiple services → push for a common protected function
- A new file doing too many things → suggest splitting by domain
- Domain logic bleeding across module boundaries (e.g., domain-specific logic in a generic/shared service) → `"Can we move all domain related work to the domain-specific controller and service please?"`

### 2e. Folder Organization & File Structure
- New file placed in the wrong folder → give the exact correct path, and reference a similar existing file as precedent:  
  `"Can we move this to \`src/modules/feature-name/commands/\` (similar to \`src/modules/other-feature/commands/\`)?"`  
- File name not matching the `kebab-case.type.ts` convention → give the corrected name  
- New interface/enum/type that should live inside an existing module's `interfaces/` or `constants/` folder → suggest the path  
- FE: constants/event names used in multiple files → `"Can we move these to a constants file inside the relevant hook/module and re-use from there?"`

### 2f. Comments — Present Where Logic Is Non-Obvious
If the *why* behind a piece of logic is non-obvious from the code alone, a comment is required.

- Complex conditional logic with no explanation → `"Can you please add a comment explaining the reasoning here?"`
- Non-obvious workarounds or intentional trade-offs → ask for an inline comment:
  ```ts
  // When [event/action] occurs and [condition],
  // there could be a case where... and we need to... to ensure...
  ```
- Intentionally kept dead-looking code → `"Can we add a comment like \`// NOTE: Kept for local debugging\`?"`
- Shared behavior that overrides a base function → ask for a comment noting the override  
- **If a comment already explains the why clearly → don't flag it**

### 2g. No Indentation Flaws or Formatting Gaps
- Inconsistent indentation in the diff → flag it (even if a linter should catch it — mention it once)
- Blank lines missing between logical blocks → flag if it hurts readability
- Excessively nested `if/else/try/catch` that could use early returns → suggest flattening:
  `"Can we try and do \`return;\` on cases before this to remove these nested if & try catches?"`

### 2h. Dead / Unused Code
- Unused imports, variables, files, decorators, guards → `"Can we remove this?"`
- Commented-out code left in without a note → `"Can we delete all the commented out code?"`
- Files duplicating existing functionality → reference the existing file

### 2i. AI / CodeRabbit Noise
- Read CodeRabbit before writing your own comments
- False positive → reply explaining why it doesn't apply:  
  `"This can be ignored as migrations have not been deployed."`  
  `"Not fixing — this regex is copied 1:1 from the backend app's constant, so both ends validate the same format."`
- Valid CodeRabbit point → don't re-flag it; let it stand
- Needs follow-up later → `"Can be ignored. We can tackle this later."`

### 2j. Edge Cases & Correctness
- `IN()` query without empty-array guard → `"This query will fail if the array is empty. We should add a check at the start."`
- Missing `await` in async chain → flag with the exact location
- Race conditions where webhook events could arrive out of order → flag with a brief explanation and suggest atomic update approach
- Missing `?` optional chaining where null/undefined would crash → short question
- **Don't flag things handled by the framework or that are clearly intentional by design**

### 2k. Correctness & Data-Flow Invariants (stateful / idempotent / scheduled code)
For any change involving persisted state, change-detection (hashes/etags/versions), retries, ordering/prioritization, cron/queues, or idempotent upserts, run this pass **explicitly**. These bugs rarely show up by reading a single function or on the happy path, and they're the easiest to miss when you already understand the author's intent — so **re-derive the behavior from the code, not from the description**.

For each piece of persisted or derived state, ask:
- **Hash/key completeness** — is every field that feeds a stored or derived value also part of the change-detection key? A field used to build output but missing from the hash means a real change is silently skipped (stale data). → `"Should we add \`scheduledKeyword\` to \`computeHash()\`? It feeds the stored tags, so a change to it alone would be skipped right now."`
- **Failure-path state** — trace what every external call (network, DB, storage) writes *on failure*. Does the failure path corrupt scheduling, ordering, retry priority, or leave a half-written record? → `"On upload failure we still set \`lastSyncedAt = now\` — won't stalest-first ordering then retry this one last? Can we leave it unset so it's retried early?"`
- **Run N, not run 1** — what happens on the 2nd/Nth run, and over time? Does a cap/limit/page-size leave some items perpetually unprocessed? Does re-running duplicate rows or overwrite good data?
- **Clear vs leave-unchanged** — when an upstream field is removed/emptied, does the code *explicitly clear* it, or does `?? undefined` leave the stale value in place? → `"When \`image_url\` is dropped upstream, do we clear the cover, or does \`?? undefined\` leave the old one?"`
- **Empty / null / missing inputs** — empty arrays in `IN()`, null upstream fields, missing pagination cursors, absent env config (does auth fail open?).
- **Two functions that must agree** — when one function produces a value and another consumes/validates/compares it, read them **together** and confirm the invariant holds (this is the class of bug a single-function skim always misses).

Don't self-review unaided: when the PR is your own code, force an "explain what this does line-by-line, ignoring what I meant" pass, or hand it to a fresh reviewer. Where feasible, exercise the **unhappy paths** (failure, re-run, edge values) — not just the happy path. Treat CodeRabbit/Bugbot as complementary here: they're strong at exactly this cross-symbol/state consistency, so this is the pass where you confirm or refute their findings rather than skipping it.

### 2l. Cross-Repo Impact
If a change in one repo must be mirrored in a sister repo (e.g. Backend → background worker service, Frontend → browser extension), note it in the **top-level review body** (not inline).

---

## 3. Inline Comment Formats

### Bare rename (no surrounding sentence)
When a function, variable, file, route, or class just needs a different name:
```
`convertToEmailString`
```
```
`addDefaultResourcesToNewUser()`
```
```
`:userTemplateId`
```

### "Can we...?" request (default format)
```
Can we rename this to `newFunctionName`?
```
```
Can we use `isTrue()` from the shared helpers please?
```
```
Can we please move these out to `src/shared/constants`. Possibly:

encoding.ts
sample-rate.ts
```
```
Can we please convert these to events and listeners? That's how we track all the events for all entities.
```

### Code example in comment
When prose can't describe the change clearly enough — show it:
```
I think this should be:

```ts
enum SortOption {
  mostRecent = "mostRecent",
  leastRecent = "leastRecent",
  nameAsc = "nameAsc",
  nameDesc = "nameDesc",
}
```
```
```
There should be a common component called (possibly in `src/shared/components`):

```ts
function InputFieldWithCharacterCount({ name, value, rows, maxCharCount, externalErrors }) {}
```

This component should be re-used in the places like:

```ts
return (
  <>
    <InputFieldWithCharacterCount ... />
    <InputFieldWithCharacterCount ... />
  </>
)
```
```

### Numbered list in one comment (multiple related points on the same line)
```
1. Can we move the `RecentActivity` component out of `dashboard-overview` and into its own file? Please & thanks!
2. Can we simplify the logic in `buildSummary` to use `lodash-es/isEmpty` or `lodash-es/has`?
3. Instead of using hard-coded words like `Amount`, `Currency`, `Customer` — can we move them to the translations file?
```

### "Can be ignored" / false positive
```
This can be ignored as migrations have not been deployed.
```
```
Can be ignored because [external service] works differently.
```
```
I think we can ignore this.
```
```
Not fixing — this regex is copied 1:1 from the backend app's constant, so both ends validate the same format.
```

### Question about intent
```
@{pullRequestAuthor} What does `hydrated` mean here?
```
```
@{pullRequestAuthor} Why did we create a new decorator? We have been using `@ExistingDecorator()` in all other controllers. What was the reason?
```
```
QQ - What happens in case of mobile?
```
```
@{pullRequestAuthor} Can you please give some background context & help me understand what was the issue and how you solved it. It'd also be great if you could add a clearer comment indicating what the developer should make note of when reviewing this piece of code.
```

### Comment-request format
```
@{pullRequestAuthor} Can we please add a comment here explaining the reasoning? Something like:

```ts
// When [event/action] occurs and [condition],
// there could be a case where... and we need to... to ensure...
```
```

---

## 4. Top-Level Review Body

**APPROVED + inline comments are self-explanatory → empty body.** This is the most common case.

**APPROVED + minor notes or a cross-cutting point:**
```
Ty @{pullRequestAuthor}! Just minor stuff from my side.
```
```
Looks good overall. Just small changes.
```
```
Just a small change.
```
```
Amazing stuff! LGTM 🚀
```
```
Good work overall. Just small things.

I am hoping you've thoroughly tested the keyboard navigation and that it doesn't conflict with any other functionalities? Thanks!
```
```
Please make the same changes in [sister repo] - `[branch-name]` branch. Ty!
```

**CHANGES_REQUESTED — address the author by name, acknowledge the work, summarize briefly:**
```
@{pullRequestAuthor} Thanks for the PR. Good work. Just minor stuff.
```
```
@{pullRequestAuthor} Great work. Just minor stuff.

1. We will need to create a `GET` endpoint as well for the documentation preferences. Would you be able to add it in this PR itself?
2. I haven't checked [second point]...
```
```
@{pullRequestAuthor} Left some minor comments. Thank you!
```
```
A few more changes please.
```
```
Hey @{pullRequestAuthor} - I tried using the updates in this branch for testing [X] but it's not working as expected.

We will need to tackle these 2 things as part of this PR:
1. Fix the state
2. [Second issue]
```

Use a numbered list in the CHANGES_REQUESTED body **only** when there are 2+ top-level structural issues that can't easily be located inline. Otherwise keep the body to 1–2 sentences.

---

## 5. Review State Decision

| Situation | State |
|-----------|-------|
| PR is clean or all feedback is minor/non-blocking | **APPROVED** |
| One or more things genuinely need to change before merge | **CHANGES_REQUESTED** |
| Inline questions exist but PR can proceed | **APPROVED** with inline questions |
| PR blocked for external reason (other repo not ready, branch not set) | Comment or DISMISS own review — don't block with CHANGES_REQUESTED |

**CHANGES_REQUESTED is the exception, not the default.** Most PRs with inline comments still get APPROVED.

---

## 6. Workflow

1. **Get the diff** — PR description, changed files, and raw diff
2. **Read the existing AI reviews** (CodeRabbit, Copilot) — decide which are valid/false positive
3. **For each changed file, read the full surrounding source** — check imports, similar adjacent files, and module conventions
4. **For any stateful / idempotent / scheduled change, run the correctness & data-flow pass** (Section 2k) explicitly — re-derive behavior from the code; don't rely on the description or the happy path
5. **Draft comments** — use formats from Section 3; one comment per distinct issue
6. **Write the top-level body** — usually empty; use Section 4
7. **Choose state** — Section 5
8. **Post** via GitHub MCP `create_pull_request_review` or:
   ```
   gh api repos/{owner}/{repo}/pulls/{pull_number}/reviews \
     --method POST \
     --field body="..." \
     --field event="APPROVE|REQUEST_CHANGES|COMMENT" \
     --field "comments[][path]=path/to/file" \
     --field "comments[][line]=123" \
     --field "comments[][body]=your comment"
   ```

---

## 7. Anti-Patterns — Never Do These

- **Don't command.** Never write "Fix this", "Change this to...", "Remove this" as bare imperatives. Always "Can we...?"
- **Don't comment without understanding context.** Read the surrounding code first. A comment that contradicts an established pattern in the repo makes the reviewer look uninformed.
- **Don't write essays.** More than 5 lines of prose in an inline comment → show code instead or ask a focused question.
- **Don't pile on CodeRabbit.** If CodeRabbit flagged it, reply to their comment instead of creating a duplicate.
- **Don't flag formatter/linter issues.** If a linter would catch it automatically, don't comment on it (one exception: call it out once if it's pervasive in the PR).
- **Don't summarize what the code does.** Comments are about intent, naming, structure, and consistency — not description.
- **Don't flag every file.** Only comment where something genuinely needs attention.
- **Don't use decorative emoji.** Reserve 🤔👍🚀✅😅 for the specific situations in Section 1.
- **Don't add a "Testing Plan" or "Verification" section** anywhere in the review.

