Imported from Xantibody/dotfiles (
configs/claude/skills/pull-request/SKILL.md). Install upstream withnpx skills add Xantibody/dotfiles --skill pull-request. Copyright stays with the author.
Pull requests
GitHub already shows the reviewer the diff. The body's job is to convey what the diff cannot: why this change exists, what shape it has, and what was deliberately left out. Restating the diff only makes the body longer and wastes the reviewer's time. Brevity is courtesy, not laziness.
Pre-flight
-
Whose repo: the title and body format below are the user's own. In someone else's repo, that repo's conventions win.
gh repo view --json owner --jq .owner.login gh api user --jq .loginOn a fork, judge by
gh repo view --json parent— the PR lands upstream. If the logins differ, read.github/PULL_REQUEST_TEMPLATE.mdand the last several merged PRs (gh pr list --state merged --limit 10), then ask the user which to follow, recommending the repo's own style as the first option. Don't silently pick either. -
Base and branch:
BASE=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name) git fetch origin "$BASE" git branch --show-currentIf you are on the default branch, you need to branch off. With uncommitted work,
git switch -c <type>/<slug>carries the working tree over. If the work is already committed onto the default branch, creating a branch is not enough — the local default still points at those commits, so the diff comes back empty and the PR has no content:git switch -c <type>/<slug> git branch -f "$BASE" "origin/$BASE" -
Uncommitted changes: ask the user whether to include them, then commit via the
commitskill. A PR that omits half the change invites a review of the wrong thing. -
Confirm the range: read
git log --oneline "origin/$BASE..HEAD". Empty means step 1 went wrong. Unfamiliar commits are the user's unpushed work — don't silently include them, don't silently drop them; ask. -
Read the history as the reviewer will: the title and なぜやるか below are built from these commits, so a history that needs rebuilding yields a body that needs rewriting — and the rebuild is cheap only before the push. Load the
history-reviewskill. On rebuild, stop before drafting and hand the user/reconstruct; come back to this skill once the history is rebuilt. Skip this step when a PR already exists — its reviewer has read the history that is there.
Check for an existing PR with gh pr view; if one exists, use gh pr edit
instead of gh pr create.
Read the change first
Read before writing. Use three dots (merge-base comparison) — two dots would attribute other people's post-fork changes on the base to your PR:
git diff --stat "origin/$BASE...HEAD"
git log --format='%s%n%b' "origin/$BASE..HEAD" # commit bodies hold the why
git diff "origin/$BASE...HEAD"
A large diff overflows the Bash output cap and gets truncated silently.
Past a few hundred lines, write it to a file under $(mktemp -d) and Read
it in pieces — what you didn't see doesn't make it into the body.
Commit bodies are the primary source for the why, but verify their claims against the diff before repeating them. They are a source, not a draft: read them for the facts, close them, and write なぜやるか from scratch in the body's language (see Prose). On bot branches (Renovate etc.) with no bodies, go read the upstream release notes. Never invent a plausible-sounding rationale.
Title
Conventional Commits. Type and scope rules come from the commit skill and
the repo's CLAUDE.md. A single-commit branch reuses its subject verbatim. A
multi-commit branch gets one line for what the branch achieves — not a list of
operations. If it won't fit in one line, that is evidence the branch should be
split; say so to the user.
Body
Load the explain skill before drafting — it holds the bullet-structure rule
shared with the issue skill and with reports to the user. The body format:
## なぜやるか
<この変更が必要な理由。issue があれば closes #123 で引用>
## やったこと
<図を入れる場合はここに。1 行のキャプション + mermaid>
- <何が新しくできるようになったか / 何が変わったか、役割の言葉で>
## やらなかったこと
- <あえてスコープ外にしたこと、次に回したこと>
## 資料
- <参考リンク、issue、議論>
- なぜやるか is the core of the body: two or three sentences saying what was wrong and why it is worth fixing. The commit body holds those facts; it is not the draft.
- やったこと is a map of the change, not a table of contents for the diff. GitHub already shows file lists and line counts.
- やらなかったこと is the most valuable section when you can write it — stating "this is out of scope" saves the reviewer from wondering whether to flag it. It and 資料 are dropped when empty; never leave a blank section.
- 資料 holds links: the failing run, the issue, the design note. An argument that wants to go there belongs in なぜやるか or is cut.
The four subsections below decide how the body reads: at what level it speaks (Altitude), how items are arranged (Structure), how much of it there is (Length), and what its sentences sound like (Prose). Then Diagrams decides whether a picture replaces some of the text.
Altitude
Write やったこと one level above the diff. The reader has not opened the diff yet; they are deciding whether to, and where to look first. A bullet that only makes sense with the diff open is at the wrong altitude.
- Name things by their role, not their identifier. 「テンプレの読み書き
を core に足した」 reads without the repo open; 「
template/にread_template/create_from_templateを足した」 does not. Identifiers are pointers, not content — at most one per bullet, in parentheses, and only when the reviewer will want to jump there. (This budget is for やったこと; なぜやるか names whatever the problem is about, and a type mismatch is about the type.) A role name has to be one the reader already uses; when nothing has one, spend the identifier rather than coin a term — 「生成物パッチ」 for a recipe nobody calls that sends the reader to the diff to decode it, which is what the identifier would have done more honestly. - Say what became true, not what was done. 「同じテンプレの今日のノート があれば作らず開く」 is a behavior the reviewer can check; 「重複判定を 追加」 is an operation they have to reverse-engineer.
- Counts and measurements stay when they are the reason. 「21 箇所の 写経を 1 つのヘルパーに」 justifies a refactor; a list of the 21 call sites does not.
The test: a teammate on a different project should be able to read every bullet and understand what changed. If they would need the diff, raise the altitude; if they would still need the diff after that, that is what the diff is for.
Structure
The shape of the information decides the construct. GitHub renders a
dozen of them — nested and numbered lists, ### groups, tables, task
lists, <details>, alerts, blockquotes, code blocks, permalinks — and
each exists because some shape reads badly as anything else. A flat
bullet list is what you get when you haven't asked what shape the
information has: a reader scans it once and keeps three items, and the
body reads as long even when it is short.
Read references/markdown.md before writing the body — it maps each
shape to its construct and names the misuse that makes each one lie.
The rules that bite most often:
- Three siblings is the limit at any level. A fourth item at the same
indentation means a grouping is hiding. Find the axis and show it — a
parent bullet with members beneath,
1.when order means something,###when the groups are areas of the change. Exceed three only for true peers with no shared axis, and "I couldn't find the axis" is not the same as there being none. - Items that share attributes are a table, not bullets. Before/after values, option × trade-off, file × why: if every bullet would repeat the same two or three fields, the reader wants columns.
- Supporting material collapses; the core never does. Long output,
the full list of touched files, a log — inside
<details>with a summary that states the conclusion. The template sections stay open.
Preparatory commits — the refactor or two that made the feature possible — are one such group: one parent bullet 「先に構造を直した」 with the why, members beneath only if the reviewer needs them. They don't go in the diagram.
Length
A paragraph or bullet is one line — never hard-wrap, GitHub renders the newline as a visible line break in a PR body — so line counts say nothing about length; count characters. The whole body fits in one screen: なぜやるか in two or three sentences, one idea per bullet everywhere else. Outside the diagram, stay under ~600 characters; past ~900 you are narrating something the diff or the diagram already shows. The budget is a ceiling, not a target: a change of a few lines gets a body of a few sentences.
Measure, don't estimate. After the draft, count the body file with fences excluded, and cut while it is over:
sed '/^```/,/^```/d' <dir>/pr-body.md | tr -d '\n' | wc -m
Cut the same information appearing a second time, in this order: a reason that なぜやるか already gives, restated on a bullet; a fact a comment in the diff already states; a sentence explaining a mechanism the reviewer can see in the diff.
A 。 in the middle of a bullet is the tell. 「issue にするか聞く。テンプレ
を追加」 is two things that happened and wants two bullets; 「設定画面の
改善。並び順、検索、既定値」 is an item followed by its members and wants
a nested list. The only second sentence that stays is the reason for
the first, and only when なぜやるか has not already given it — a bullet
whose reason is the PR's reason states the fact alone. In やらなかったこと
the reason is mandatory, because it is the part the next person cannot
reconstruct. Budget is not a license to drop it; nest the members instead.
Drawing is a way to delete text. The bullets under a diagram get shorter because the structure moved into the picture; if the body is as long with the diagram as it would be without, the diagram is decoration.
The budget is per purpose, and a PR normally has one. When a second purpose rides along — a cleanup that was found on the way and wouldn't earn a review of its own — it gets its own paragraph in なぜやるか and its own bullet, and the body grows by that much. That is allowed only because the rider is low-risk: nothing deleted that someone depends on, no default changed, no behaviour the reviewer has to think hard about. A rider that needs thinking is a second PR, not a longer body. The length scales with what the reviewer must doubt, not with what was done.
Prose
The body is written from scratch, in the plain declarative sentences a coworker would say aloud. The sources — commit bodies, comments in the diff, the diff itself — are read for facts and then closed; none of them is a draft. An English commit body translated paragraph by paragraph keeps its length and carries its idioms over verbatim: "wins or loses a race" becomes 「レースの勝敗で決まる」, "giving the 72 seconds back" becomes 「72 秒を返さずに済む」, and the reader has to translate back to learn what happens. A comment written earlier in the same session is the same author repeating themself, in the same words.
- The subject is the thing; the verb is what happened to it. 「
rmが 終わる前に走ると落ちる」 names the step, the condition, and the outcome. 「レースの勝敗でビルドが決まる」 names none of them. - No metaphor, no personification, no idiom. Each stands in for a plainer sentence that is always available: 「配信ビルドを預けている」 for 「リリースビルドがそのパスを使っている」, 「手当てを入れた」 for 「同じ変更を 入れた」, 「畳んだ」 for 「統合した」. Use the plainer one.
The test: read each sentence back as if the reviewer had asked 「つまり何が 起きる?」. If the answer is a different sentence, write that one instead.
Diagrams
A diagram earns its place by showing relationships — which parts now talk to which, what step appeared or disappeared — and prose is bad at exactly that. "Only when the architecture changed" turned out to be a gate that never opens: a feature that adds a module, a pipeline stage, and a screen is architecture too, but it never feels like it from inside the diff. So the gate is a count, not a judgment. Draft やったこと once, count on that draft, decide; if you draw, the bullets get rewritten to sit under the diagram, and that rewrite does not reopen the decision.
- Count the actors whose exchange the change touched — modules, processes, services, screens, external systems, and the data that flows between them. An unchanged neighbour counts only when it is an endpoint of a new, removed, or rerouted exchange. Three or more means the reviewer is assembling a picture from text. Draw the exchange.
- Check the size. If やったこと needs more than three groups after applying Structure, the body runs past ~600 characters, or it carries more than ~10 backtick identifiers, the text is doing a diagram's job. Either draw, or raise the altitude until the count drops. Both are correct answers; leaving it as is, is not.
Two actors or fewer — a version bump, a config tweak, a typo fix, a single function change — gets no diagram. A forced diagram is decoration that looks like information, and it costs the body its credibility.
When you draw:
- Put it at the top of やったこと with a one-line caption saying what the reader should see in it. The bullets below then explain, not enumerate.
- Draw the change, not the system. Keep only enough unchanged context for the new parts to read as a delta. Past ~15 nodes you are drawing the system.
- Mark what appeared or disappeared with a stroke class, and say which is which in the caption. Mark nothing else — marks only mean something against unchanged neighbours.
flowchart LR
tpl["テンプレ (data/templates/*.md)"] --> core["core: ノート作成"]
core --> note["ノート (frontmatter: template)"]
sync["同期の走査"] -.-> tpl
classDef added stroke:#3fb950,stroke-width:3px
class tpl,core added
Read references/mermaid.md before writing the block. It has the diagram
type per kind of change, the syntax that breaks GitHub's renderer, and how to
render the body locally to catch layout that parses but lies.
Screenshots
A diagram shows relationships; a screenshot shows a result the reviewer would otherwise have to run the app to see — a screen, a chart, a TUI frame, a rendered document, a before/after pair. Same gate as the diagram: it earns its place only when the text would be describing pixels. Never for the mermaid block, which GitHub renders itself, and never for terminal text, which belongs in a code block where it can be searched and copied.
When the gate opens for something a browser renders, load the
browser-verify skill: it captures the base branch and the head at the
same URL and viewport, into the same temp directory as the body, and has
you read both images before writing the caption.
gh pr create --attach (gh 2.100.0 or later) uploads the file and
rewrites a matching  in the body to the uploaded asset.
Put the reference where the image belongs — at the top of やったこと,
with the same one-line caption a diagram gets — and pass the same path
string to --attach, alt text after #:
gh pr create --base "$BASE" --title "<title>" --body-file <dir>/pr-body.md \
--attach "$IMG/after.png#設定画面: 並び順の切り替えが増えた"
Keep the images beside the body file under the same temp directory, and
write the identical path in the body and on the flag — the rewrite is a
match on the path; a file passed without a matching reference is
appended to the end of the body. gh pr edit and gh pr comment take
the flag too, so a PR that already exists gets its screenshot the same
way. The limits as of gh 2.100.0: png, jpg, gif, webp, svg, mp4, mov,
webm; images up to 10 MiB; 50 files per command; write access to the
repo; GitHub.com and Enterprise Cloud only. The asset inherits the
repo's visibility. If some attachments fail, gh still creates the PR
and exits non-zero; read the body back for a reference left
unrewritten.
Verify, hand off the push, create
Write the body outside the worktree so a later git add -A can't swallow
it. Take a directory from mktemp -d once and spell its path out in every
later command — each Bash call is a fresh shell, and the lint hook on gh
reads the --body-file path from the command text, so "$BODY" there is
refused:
mktemp -d # then Write <dir>/pr-body.md
Lint the body before gh sees it — the explain skill has the command
and the house rules behind it — and fix every finding; the hook refuses a
body with findings left.
Before the push, run the project's checks via the check skill. A PR that
fails its own repo's fmt/check burns a review round on nothing.
git push is deny-listed on purpose — the user pushes. If the branch is not
on the remote yet, hand over the exact command with the real branch
substituted, in the form ! git push -u origin <branch>, and continue once
they have run it:
gh pr create --base "$BASE" --title "<title>" --body-file <dir>/pr-body.md
gh pr checks --watch
Use --body-file, not --body (which mangles newlines and mermaid fences).
Add --attach for each screenshot the body references (see Screenshots).
If gh pr create is denied too, that is the user's decision, not an obstacle
to route around — hand that command over the same way.
Report
Give the user the title and the CI outcome. State explicitly what you left
out — files not committed, checks that couldn't run, a diagram you could not
render. A PR that looks complete but isn't is the most expensive failure
here. The PR URL is the closing line, bare and alone on it, as the explain
skill describes — the user opens the PR from the terminal.
Then ask whether any of the やらなかったこと should become issues, and
recommend which — the ones that are follow-up work in this repo, not the
ones that are out of scope for good. On yes, file them with the issue
skill's deferred-work template, one per item, linking back to the PR. The
reason each was left out is already written; the issue is where it survives
the merge.