| name | pr-writing |
| description | Draft or rewrite GitHub Pull Request titles and descriptions — bug fixes, security fixes, features, refactors, dependency upgrades, rollbacks, plus release review notes and reviewer-facing change summaries. Use when the user mentions "写 PR", "PR 描述", "PR body", "review notes". |
PR Writing
A PR description answers what changed and why; the diff already answers "how, line by line" — never restate it. Two tests apply to every sentence:
- Necessity: if deleted, does the reviewer lose judgment input? Keep. Only completeness? Cut.
- Readability: understandable without opening any source file? If not, raise the abstraction level.
Title
<type>(<scope>): <subject>, always English even when the body is Chinese — full rules in the commit-writing skill. Two PR-specific rules: the title alone must let a reviewer decide whether to open the PR (update auth fails, fix(auth): race condition in token refresh under concurrent login passes), and neither title nor branch name carries provenance (feat/billing-record-edits, not codex/... or [agent] ...).
Body language
Match the author's past PRs in this repo (gh pr list --author @me --state all --limit 10); else recently merged PRs; else English. Commands, paths, API names stay verbatim.
Body structure
## AS IS
- <current behavior / missing capability>
- <trigger or business impact, only when the diff can't show it>
## TO BE
- <behavior after the change>
- <blast radius / compatibility / migration, only when the reviewer judges risk by it>
Two sections; the only addition is "Introduced by" on fix PRs. No Background / Motivation / Implementation / Validation / Future Work unless the user or a template demands it.
- 1–3 bullets per section, one fact each. AS IS describes state only; solutions belong to TO BE.
- No validation section by default — only when requested, template-mandated, or the result gates the release.
- Cut filler words, never concrete facts. A specific long bullet beats an abstract short one.
- Verbs match the diff: replacing an old approach is "replace", not "add"; renames and boundary cleanups are noted as companions of the replacement.
- If the diff is unusually large, say why (e.g. the old naming had leaked into DB, workers, API, SDK).
- Motivation comes from facts the user stated, never from speculation.
- No production process (tools, iteration count, AI involvement) unless it is itself what the PR changes.
Abstraction level
The default reader opens no source file. Bullets state externally observable facts: in what scenario, who sees what.
- Verdict level ("improved token refresh logic") — too high: conclusion without behavior.
- Behavior level ("concurrent logins mint multiple valid tokens; replaced ones stay usable for 5 min") — right.
- Mechanism level ("added a
SETNX lock in refresh.ts") — too low: requires reading code.
Test: cover the diff, open no file — does the bullet still hold and read? Easily missed under this test: database object names, constraint names, migration IDs, internal error codes and exception names are implementation details just like file and function names. Write them only when the reviewer must act on them (run a migration by hand, audit live data), with one line on what they mean. Contracts are fine: user-visible endpoints, command names, config keys. Mechanism gets one sentence only when it changes the risk picture.
- "improved error handling" → "sync now retries 3 times then fails loudly when the remote resource is gone, instead of silently skipping"
- "improved query performance" → "search drops from ~2s to ~200ms at 100k records"
- "changed
uq_x_y to DEFERRABLE INITIALLY DEFERRED" → "term uniqueness is now checked at save time; transient duplicates while editing no longer count as conflicts"
Introduced by (required on fix PRs)
fix / revert / security PRs name the change that broke the behavior — it determines affected released versions, backport need, and residual bad data. Own section after TO BE, before issue refs (it is a provenance conclusion, not current state):
## Introduced by
- <short-sha> (#PR, YYYY-MM-DD) <what it did>; since then <which scenario breaks>.
Sha and PR number are required identifiers, exempt from the no-code-references rule; the bullet still stays at behavior level. Optionally add dormancy time and why it went unseen (narrow trigger path, no test coverage).
Locating it:
git log -S '<code fragment>' --all — where a piece of logic first appeared; survives reformatting and moves, beats git blame.
git log --diff-filter=A -- <path> — first addition of a file (migration, config). No --follow: rename tracking bleeds into unrelated history.
- Behavior reproducible but change unlocatable:
git bisect.
Two traps:
- Introduction is not manifestation. Defects are often a combination: one side harmless for months until the other changes a schema, default, or timing — name both commits and say it is the combination. A commit that only renames or moves files is not the source, even when the error message shows the name it introduced.
- If unlocatable, write "could not locate the introducing commit". A plausible stand-in misleads the backport scope.
Linked issues
At the end of the body, one list item per reference — GitHub expands #123 to its title only in a list item, not inline; same for cross-repo owner/repo#123 and external trackers (Linear, JIRA, Sentry).
- resolves #123
- refs JIRA-4567
resolves / fixes / closes auto-close the issue on merge — only for the PR that fully solves it; partial progress or follow-up pending uses refs. One reference per item, or the preview is lost.
Variants
Same skeleton, different content. Feature: missing capability and current workaround → shape, entry point, usage. Refactor: observable costs of the current structure (duplication, coupling, untestable boundaries) → how the new one removes them. Dependency upgrade: concrete problems of the old version (CVE, deprecated API, known bug) → changed interfaces, callers to adapt. Rollback: what broke, when, how it was found → version reverted to, follow-up fixes needed.
A diff readable in 5 seconds (typo, lockfile, one config value, pure style) gets one sentence, no skeleton.
Example
Title: fix(auth): race condition in session-token refresh under concurrent login
AS IS
- Concurrent logins each trigger a refresh and each mint their own token.
- Replaced tokens stay valid for 5 minutes — a session-hijack window.
TO BE
- Concurrent refreshes converge on one valid token; later requests reuse it.
- Old tokens are invalidated immediately; no residual window.
Introduced by
-
3f9c1ab (#712, 2025-03-05) moved refresh from a single point to per-request minting; since then concurrent logins produce multiple valid tokens. Dormant 4 months: only same-second logins trigger it, no test coverage.
-
resolves #842
-
refs SEC-431