Author or sync the current branch's PR description. No open PR — draft the body to the standard here, check it, and report a file path for the caller to create the PR from. Open PR — fetch the body and verify it against branch state. Both modes read the body end to end for self-consistency, then flag per-commit narratives, stale content claims, TBD markers, and files missing from the body, and preserve coordination steps. Dispatched from /ready-for-review step 5 and the /handoff pre-write checklist; also run standalone.
Resources
2Install
npx skillscat add jcdendrite/claude-config/pr-description Install via the SkillsCat registry.
PR description
The PR description is for the reviewer, not for posterity.
Resolve the PR with gh pr view --json number,body,title, then take one of
two modes. Both run the same checks; the difference is where the body comes
from and where it goes.
- No open PR → author mode. Draft a body per "What the body must carry" below, run every check against the draft, write it to a file, and report that path. Do not create the PR — the caller owns PR lifecycle. Run standalone, that reported path is the deliverable; never no-op because there is no PR to edit.
- Open PR → sync mode. Fetch the body, run every check against it, and
apply the result withgh pr edit <n> --body-file <path>.
Load project-specific layer
If a project-specific layer exists for this skill, load it now. Glob for.claude/skills/pr-description-*/SKILL.md from the repo root (resolved viagit rev-parse --show-toplevel); if exactly one matches, read it with the
Read tool and merge its check items into the Checks section below. If
multiple match, list them and stop — that's a config error in the project,
not something you can resolve. If none match, proceed without a layer.
What the body must carry
- What and why. What change is being made — summarized so the reviewer
gets its shape without reading the whole diff — and why: the context you
had as the author, and decisions that are not visible in the source. - A first line of prose that stands alone. The first line of prose, read after the attribution trailer below, is what a reader skimming a list of PRs sees; it has to carry the change by itself.
- No list of commit subjects. A bulleted
git logof the branch is
chronology, not a summary: it re-narrates how the work arrived instead of
what it does, and it is the per-commit narrative the checks below exist to
strip. Organize by the surface a reviewer maps to instead. - Current state, not branch history. Every section says what the change is
and does now, to a reader who never saw the branch develop. Review rounds,
superseded designs, and who found what stay out;git logand the review
record hold them. A finding, limitation, or accepted risk that is still true
at HEAD stays as a present-tense fact, with only its round and attribution
removed. A mechanism that exists only in the branch's own history is
not context, because the reviewer cannot see it. A rejected approach a
reviewer would plausibly propose belongs in## Alternatives considered
with its one-line reason, not in Context. The machine-managed blocks under
Checks below are exempt. - Section structure from the repo's template. If the repo has
.github/PULL_REQUEST_TEMPLATE.md, read it and use its headings — neithergh pr create --body-filenor--bodyapplies the template, so it is
honored only by reading it here. Absent one, read${CLAUDE_SKILL_DIR}/DEFAULT_TEMPLATE.mdand use its headings the same way. - A
## Test planof results, not a checklist. Verification has already
run by the time this fires, so state what ran and what it produced, in past
tense. If verification was skipped under a documented scope exception, say
that the exception applied and that no executable-code verification ran.
Never- [ ]items, never a placeholder prompt, never an empty section —
and never fabricated results. A future-tense checklist for work already
done is the heading-negates-its-own-body defect the coherence pass below
flags. In## Test plan, state what the final review pass returned, not how
many rounds ran or what earlier ones found. - The caller's context, folded in. Text passed as
$ARGUMENTSis the
caller's own account of the change. Work it into the What/Why prose under
the body's own headings — do not drop it, do not silently paraphrase it,
and do not leave it sitting as an unlabeled block between sections. A bare
block is exactly the span the coherence pass is told to flag. - A closing-keyword reference, when a
<TICKET-ID>was passed in. IncludeFixes <TICKET-ID>in the body alongside any existing citation of it (see REFERENCES.md for why). - The attribution trailer, at both the top and the bottom of the body — the first line (blank line, then the rest of the body) and the last line:
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Cost section
Machine-managed, delimited by <!-- pr-cost:start --> / <!-- pr-cost:end --> — regenerated fresh every sync, never reinserted verbatim (contrast ## Deferred review findings below).
When the script below exits 0, its block is pre-cleared for publication (the account's pr-cost-disclosure sentinel is the standing approval), so embed it without asking for per-PR approval — see docs/private-project-redaction.md § "Publishing a tooling measurement".
Resolve the section with a single script call:
~/.claude/scripts/pr-cost-section.sh- Exit 0: enabled and the branch resolved cleanly — stdout is the finished
## Cost (list-price estimate)block, with its delimiters, heading, tables,
and reproducibility trailer already in it. Embed that stdout verbatim and
add nothing around it: no heading, no trailer, and no delimiters of your own.
Never recompose, round, or re-narrate the figures. Embed it as raw markdown,
never inside a code fence. A fenced block renders its GFM alert and its### Cost by …tables as literal text instead of a callout and tables. - Exit 1: disabled, unreadable, or malformed
<config-dir>/pr-cost-disclosure
— delete the block if one exists, no stdout. - Exit 2: enabled but the branch is the literal
HEAD(detached) — omit
the block and say why, no stdout. - Exit 3: branch resolved but the downstream cost report itself failed —
omit the block and note in the body that the report failed to generate,
unlike exit 1's silent deletion.
The sentinel check (<config-dir>/pr-cost-disclosure, trimmed and lowercased, exactly dollars) is per Claude account, not per repo: cost is an organizational fact, and each account is its own billing entity.
Resolves that one config-dir path only — never unions it with $HOME/.claude, or one account's opt-in would activate disclosure under another; call the script once, it performs the sentinel check internally.
One deliberate narrowing: a sentinel consisting of a blank line followed by dollars reads as
two lines and is judged disabled, where a whitespace-collapsing read would have judged it enabled
— in the direction this gate already prefers (under-disclosing over guessing). Session/turn counts,
per-model-ID dollars, per-review-skill round counts, and per-agent-type dispatch counts are not
neutral — they signal engagement scale, model mix, and review cadence. That is the intended read
under an account that opted in; it is not a property of the output format, and an account enabling
this for one engagement should not assume the fields are harmless in another.
The Scope: caption also prints the local branch name. See docs/transcript-analysis.md § "cost" for the echo's hazards.
Prose tightening pass
Gate: run ~/.claude/scripts/config-get.sh pr_description_tighten_prose. On exit 0, dispatch tighten-prose by name against the drafted body file, leaving the cost block (<!-- pr-cost:start --> / <!-- pr-cost:end -->), the ## Deferred review findings block, and the attribution trailer untouched (its own carve-out rule already protects code spans, headings, identifiers, and file paths). Exit 1 (disabled), exit 2 (unknown key), and exit 3 (config dir unresolvable) all skip the pass, failing toward off like every other config-dir-only key's resolution-failure behavior (see docs/config-file.md). Run it after $ARGUMENTS is folded in and before ## Checks, so ## Checks validates the final tightened bytes, not pre-rewrite text.
Checks
Run every check below in both modes — against the draft in author mode,
against the fetched body in sync mode.
Machine-managed blocks come out first. A ## Deferred review findings
section delimited by <!-- code-review:deferred:start --> /<!-- code-review:deferred:end --> is located mechanically by later/code-review runs and must survive byte-identical, delimiters included.
Lift the delimited span out before the coherence pass and reinsert it
verbatim afterward — left in place it is precisely the "what is this?" span
that pass is told to flag. The cost block (<!-- pr-cost:start --> /<!-- pr-cost:end -->, "Cost section" above) gets the same lift-out
treatment but not the same reinsert rule, stated here rather than left to
proximity: delete the lifted span outright and put the current script run's
stdout in its place.
Reader-coherence pass. Before the pattern checks below, read the
body end to end as the reviewer will and answer: does this document
make sense on its own? Judge the body against itself — comparison
against branch state and file content comes after. Those checks are
pattern matches; none of them surfaces "this section contradicts
itself" or "this text should not be here at all."
Markers, illustrative rather than exhaustive:
- A heading whose own body negates it — a "why I skipped X" section
whose text says X was not skipped. - Two sections that contradict each other — a "no breaking changes"
claim up top against a breaking change in the deploy notes. - Leftover template instruction text: placeholder prompts the
template's own directions said to remove once a condition holds. - Narration only someone who watched the branch develop could follow; see
Current state, not branch history above. - Two sections saying the same thing — a Summary bullet restating a list in
Context. Keep it in the section where the reader looks for it. - Any span a reader arriving cold would stop on and ask "what is this?"
If nothing fires after a careful read, say so — naming the sections
you read end to end. A bare negative cannot distinguish a coherent
body from a skipped pass.
Then compare the body against branch state:
git log <base>..HEAD --onelinegit diff <base>..HEAD --name-only
Flag and fix:
- Per-commit narratives ("Commit X did Y, Commit Z did W").
Reorganize "What shipped" by surface the reviewer maps to (schema /
handler / tests / invariants / migration-deploy notes);git log
already has the chronology. - Branch-history narration. The per-commit check's sibling, for prose that
narrates by review round or by reviewer instead of by commit: "earlier
rounds implemented…", "a later review found…", or a reviewer or agent name
credited with a finding or a method. Rewrite it per Current state, not
branch history above. The tells are illustrative. - Reviewer-action items Claude can answer itself. Strip claims
you can verify ("all migrations match precedent" — confirm and
remove), test counts (those belong in the commit message), and
CI placeholders (CI status is/ready-for-review's job —gh pr checks). Keep items requiring reviewer judgment: deploy
coordination, security-invariant catalog approval, architectural
sign-off. - Content-claim verification. Read each file the body describes at
its final state (clean tree = HEAD) and confirm its claims about that
content (deployment order, feature names, step numbers) still match —
a removed guard or deleted structure must be gone from the body too. - External-state claims. Content-claim verification covers files in this repo; this covers state outside it — most often a follow-up ticket said to be pending, or promised as
will create. Re-check each claim at its own source, then rewrite it to its current truth value; carry an identifier across only where the body already names that tracker — never invent a fresh reference for a claim the body didn't already make. Whether CI is wired up is such a claim; whether CI is passing is not — that is stripped under Reviewer-action items Claude can answer itself. - Uncited numbers and causal claims. A cost figure, latency delta, or "fixes X because Y" claim surviving into the body must name the code, config, or query it came from — verified once in some other artifact isn't verified here.
TBD/pending/ "to be updated" markers still in the body.- Files in the diff absent from the body.
- Missing or duplicated attribution trailer. Older bodies may carry the trailer only at the bottom — add it as the first line too. Never end up with more than one copy at either position.
- A code-fenced cost block. The
<!-- pr-cost:start -->span holds
raw markdown; a fence around it renders the alert and tables as
literal text.
Delivering the body
The body reaches its consumer as a file, never as a shell argument.
Create it with mktemp "${TMPDIR:-/tmp}/pr-body.XXXXXX" — the random suffix
keeps concurrent runs in sibling worktrees from colliding, and the prefix
makes the file identifiable to whoever meets it later — then populate it with
the Write tool.
Author the body with the Write tool, not a shell heredoc. An unquoted<<EOF parses embedded backticks/$(...) as command substitution exactly
like --body "$(cat <<EOF)" does — writing to a file does not by itself
escape that. Use <<'EOF' if a heredoc is unavoidable.
Never write \` to "escape" a backtick — nothing consumes a
backslash before a backtick, so it survives into the body unconsumed.
This breaks GitHub's code-span rendering, whether authored via Write
tool or heredoc.
Then, by mode:
- Author mode: end your report with the path on a line of its own, in
the formBODY_FILE: <path>. The caller builds itsgh pr createargument
by reading that line out of an otherwise long report; a fixed single-line
form makes the path extractable rather than transcribed. - Sync mode: apply with
gh pr edit <n> --body-file <path>.
Write the path as literal text in both commands, not a $VAR — the
redaction gate resolves --body-file statically, so a variable is opaque to
it and the call fails closed.
When a body already exists, keep the project's template structure intact —
refresh content inside existing sections, don't restructure. Exception: a
section the template's own instructions say to delete once a condition holds
is meant to be removed, not emptied or annotated — give any action item
inside it a disposition first (see "Coordination-step preservation" below).
Coordination-step preservation. Before applying an edit to an existing body, enumerate every action item that body carries — coordination steps, pre-deploy commands, manual external-system setup, sync workflows. Give each action item a disposition: survive (name its new section if the old one is deleted), answer-and-strip (Claude resolved it — see the "Reviewer-action items Claude can answer itself" bullet above), or strip-as-stale (no longer applies — see the TBD / pending markers bullet above). Deliberate removal is fine; a survivor left with no home is not.