---
name: forge-pr-description
description: PR descriptions that get reviewed and merged fast. Title under 70 chars, Summary/Why/Test plan structure, screenshots for UI changes, risk callouts for sensitive areas, length under 300 words. Contains ready-to-paste templates for standard / UI / migration / revert PRs. Use when opening or rewriting a pull request.
license: MIT
---

# forge-pr-description

You are writing a PR description that another engineer will read at 11pm before approving. Default agent-written PR descriptions are either "made changes to fix bug" or a 2000-line dump of every file touched. Both fail the same way: the reviewer cannot decide whether to approve in under two minutes. This skill exists to make PRs reviewable in two minutes.

The contract: **a PR description tells the reviewer what changed, why, and what to look for.** The diff says what; the description says why and where to focus.

## Quick reference (the things you must never ship)

1. Title over 70 characters.
2. Title in past tense or noun-only ("Pagination updates").
3. Description that is just the file list.
4. "Sorry it's so big" without explaining why bundled.
5. "Tested locally" without saying how.
6. No issue link on a non-trivial PR.
7. UI change with no before/after screenshot.
8. Migration PR with no rollback plan.
9. "Co-authored-by: Claude" or any AI attribution.
10. Whole description over 300 words.

## Hard rules

### Title

**1. Title is one line, under 70 characters, imperative mood.** "Add cursor-based pagination to /orders" - not "Added pagination" (past tense) and not "Pagination" (noun-only).

**2. Title names the change at the level of behavior, not the file.** "Fix double-charge on retry" beats "Update PaymentService.ts."

**3. Use prefixes if your team has a convention.** `feat:`, `fix:`, `chore:` are useful for changelog automation; cosmetic noise without.

### Structure

**4. Three sections in order: Summary, Why, Test plan.** Add Risk and Rollout if applicable. Anything else is decoration.

**5. Summary is 1-3 bullets of what changed.** Not paragraphs. Bullets read in 5 seconds.

**6. Why is one paragraph.** Why does this change exist? What was the problem? Reviewer needs to evaluate the choice, not the implementation alone.

**7. Test plan is a bulleted checklist of how to verify.** "Run the local server, navigate to /orders, scroll to the end of the list, confirm next page loads." Specific.

**8. Risk callout: what could break, who would notice.** "Touches the auth middleware - any regression affects every endpoint." Short, honest.

**9. Rollout: feature flags, migrations, ordering with other PRs.** Skip if not relevant.

### Length

**10. Whole description under 300 words.** More? Link out to a doc.

**11. No "How it works" wall-of-text describing the diff.** The diff already shows how it works. Describe intent and decision.

**12. No tables of every file changed.** GitHub already lists them.

### Visuals

**13. Screenshot or short GIF for UI changes.** Before/after side-by-side beats prose. Attach images directly via GitHub upload.

**14. For backend changes that produce output (new API response, new log line, new CLI), paste a sample.** Real output beats described output.

**15. For schema or interface changes, paste the diff of the schema explicitly.**

### Linked work

**16. Link the issue. Use the magic keyword `Closes #123`.**

**17. Link related PRs.** "Depends on #456" or "follow-up to #789."

**18. Link to design docs, ADRs, or Slack discussions if the decision needs more context than fits.**

### What to NOT include

**19. Do not narrate the PR's history.** "First I tried X, then Y didn't work, so I went back to Z." Reviewers want the final shape.

**20. Do not apologize for the size.** "Sorry it's so big" wastes a sentence. Either split it or explain why bundled in one line.

**21. Do not claim "tested locally" without saying how.**

**22. No AI attribution. No emoji checkboxes.**

### Voice

**23. First person plural ("we") or active impersonal.** "We deduplicate orders by idempotency key" reads as team work.

**24. Past tense for what the PR does; present tense for what the code does.** "Added cursor-based pagination. Each response includes `next_cursor` when more results exist."

### Sensitive PRs

**25. Security-relevant changes get a clear callout.** "This changes how session tokens are validated. Reviewer should focus on token-replay scenarios."

**26. Migration PRs:** explicit before/after, rollback plan, and timing. See [`forge-migrations`](../../data/forge-migrations/SKILL.md).

**27. Performance-claiming PRs need numbers.** "p95 latency down from 320ms to 180ms" beats "should be faster."

### Drafts

**28. Mark drafts as Draft.** GitHub PR button supports it. Use it instead of `[WIP]` in the title.

**29. Convert draft to ready only when the test plan is real.** "Tests are still flaky" is a draft, not "ready for review."

## Common AI-output patterns to reject

| Pattern | Why bad | Fix |
| --- | --- | --- |
| Title: "update" / "fix bug" | Tells nothing | Imperative behavior: "Fix double-charge on retry" |
| Title in past tense | Wrong convention | Present imperative |
| Description: file list | Useless (GitHub shows it) | Why + test plan |
| Description: 2000 words | Unreviewable | Under 300 words, link out for more |
| "Tested locally" no method | Unverifiable claim | "Test plan:" with steps |
| No issue link | Lost context | `Closes #123` |
| UI change no screenshot | Reviewer must run the branch | Attach before/after |
| Migration PR no rollback | Risky | Explicit rollback plan |
| "Co-authored-by: AI-assistant" | Inappropriate | Author is the human committing |
| Apology for size | Padding | Either split, or one line explanation |

## Templates

### Standard feature / fix PR

```markdown
## Summary
- [Bullet 1: what changed]
- [Bullet 2: what changed]

## Why
[1-paragraph context. Problem it solves, who reports it, constraint driving the design.]

## Test plan
- [ ] [Specific step a reviewer can run]
- [ ] [Edge case to verify]
- [ ] [Regression check]

## Risk
[Short honest note. "Low risk, isolated change." OR "Touches auth path; verify against existing session tests."]

Closes #123
```

### UI-change PR

```markdown
## Summary
- [Bullets]

## Visual
| Before | After |
| --- | --- |
| ![before](url) | ![after](url) |

## Test plan
- [ ] Mobile + desktop
- [ ] Light + dark theme
- [ ] Tab-key navigation works

Closes #123
```

### Migration / breaking-change PR

```markdown
## Summary
- [Schema change at a high level]

## Why
[Why this needs to happen now]

## Migration plan
1. Ship this PR (additive only)
2. Deploy
3. Backfill in batches via [script]
4. Ship follow-up #XYZ (NOT NULL constraint)

## Rollback
[How to undo, what data state results]

## Test plan
- [ ] Local: applied + rolled back successfully
- [ ] Staging: run against prod-shaped data
- [ ] Production: run during low-traffic window

Closes #123
```

### Revert PR

```markdown
## Summary
- Revert #456 (Add cursor pagination to /orders)

## Why
The new pagination broke the legacy /api/v1/orders client, which relies on offset-based queries.
We are reverting to unblock v1 clients while we coordinate the migration.

## Test plan
- [ ] Run legacy v1 client against staging; confirm /orders returns offset-paginated payload
- [ ] Existing v2 cursor tests still skip (the code is reverted; tests will be re-added with the redo)

## Risk
Low. This restores known-good behavior.

Refs: incident-2026-05-22-orders-api
```

## Worked example: good PR description

```markdown
## Summary
- Cursor-based pagination on GET /v1/orders (`?cursor=...&limit=...`).
- Response shape: `{ data: Order[], next_cursor: string | null, has_more: boolean }`.
- Old `?page=&per_page=` query params return a 400 with code `unsupported_pagination`.

## Why
Offset pagination on the orders table was timing out at OFFSET >5000 (~250ms p99).
The v2 contract is keyset-paginated; this PR adds it and explicitly rejects the v1 query params on the v2 endpoint to surface the breaking change rather than silently serving wrong data.

## Test plan
- [ ] Run `npm test` - new test cases cover empty, first page, mid-page, last page.
- [ ] Hit `/v1/orders?limit=2` against staging; confirm `next_cursor` round-trips.
- [ ] Hit `/v1/orders?page=2` against staging; confirm 400 `unsupported_pagination`.
- [ ] Run `EXPLAIN` on the production query plan; index `orders_created_at_id_idx` used.

## Risk
v1 clients passing `?page=...` will get 400 instead of (incorrect) data.
We've audited internal callers - none still use `?page=`. External API consumers were notified Tuesday.

Closes #1234
```

Word count: ~190. Title under 70 chars. Issue linked. Test plan specific (commands, expected output). Risk honest and bounded. No AI attribution.

## Workflow

When writing a PR description:

1. **Title first.** If you cannot title it in 70 chars, the PR is doing too much.
2. **Summary bullets.** 1-3 lines.
3. **Why paragraph.** Make sure a reviewer who has not seen the ticket can evaluate the choice.
4. **Test plan.** Specific, runnable.
5. **Add Risk callout if anything touches a sensitive area.**
6. **Add screenshots/output samples if visual.**
7. **Link the issue.**

## Verification

Manual checklist:

- [ ] Title under 70 chars, imperative mood.
- [ ] Whole description under 300 words.
- [ ] Has Summary, Why, Test plan.
- [ ] Has issue link.
- [ ] Has screenshot if UI changed.
- [ ] Risk noted if non-isolated.
- [ ] No AI attribution.

## When to skip this skill

- Trivial PRs (one-line typo fix): a one-line description is fine.
- Bot-generated PRs from Dependabot, Renovate.
- Internal hackweek prototypes that won't merge.

## Related skills

- [`forge-commit-messages`](../forge-commit-messages/SKILL.md) - the commits inside the PR.
- [`forge-code-review`](../forge-code-review/SKILL.md) - reviewing the PRs you wrote descriptions for.
- [`forge-migrations`](../../data/forge-migrations/SKILL.md) - the rollback discipline migration PRs require.
