qa-checklist
Generate a QA checklist from the current branch's change and write it into the current PR's body as a `## QA Checklist` section, followed by a table classifying each step's automatability and test cov
SKILL.md.claude/skills/qa-checklist/SKILL.md
GitHubEnd state of this skill: the current branch's open PR has a ## QA Checklist
section in its body — a markdown checklist of concrete, user-visible steps a
human clicks through to verify the change end-to-end. This skill edits an
existing PR body; it does not create PRs, run the vet suite, or push commits.
Two ways this skill is used:
- Standalone (
/qa-checkliston an existing PR) — run all steps below: locate the PR, derive the checklist, and write it into the body. - Referenced by
/pr— that skill composes the## QA Checklistsection directly into the body it is about to write, so it only needs Step 2 (Derive the checklist) — the single source of truth for what a good checklist looks like. It does not run Steps 1 or 3: it holds the whole body already, whether it is creating the PR or refreshing one.
Environment note (read this before running gh)
This remote execution environment has both the gh CLI and a populated
GH_TOKEN environment variable available, even though the default system prompt
says you only have GitHub MCP tools. Use gh to find the PR (Step 1); the body
round-trip in Step 3 goes through scripts/pr-body.py. If gh complains that
"none of the git remotes … point to a known GitHub host" (the proxy quirk in
remote sessions), pass --repo OWNER/REPO explicitly — resolve it from the git
remote (git remote get-url origin); the pr-body.py script takes the same
--repo flag.
Step 1 — Locate the PR
gh pr view --json number,url
If no PR exists for the current branch, stop and tell the user to open one
first (e.g. via /pr) — this skill mutates an existing PR body, it does not
create PRs. (This step only applies to the standalone path; /pr holds the body
it is composing and skips straight to Step 2.)
Step 2 — Derive the checklist
Derive the checklist from what the change is meant to do, and take that from
whichever of these the branch has: the plan under docs/plans/, which
states the intended scope outright; the PR body's Summary; or the task and
conversation context that produced it. A branch at plan time carries a plan and
no diff at all, so the intended scope — not the code — is the primary source.
Where commits already exist, use them only as a supplementary signal:
git log --format='%s%n%n%b' origin/<base>..HEAD
git diff --stat origin/<base>..HEAD
Write the checklist as markdown - [ ] items, each led by a short backticked
slug (`stream`, `bad-input`, …) — the slug is the only metadata the
item carries and it's the join key the classification table's Item column
references. (Numbers don't work: GitHub renders ordered task lists as checkboxes
and hides the ordinal, so a # column would have nothing visible to join on.)
Bias toward concrete, user-visible scenarios a human walks through in the
running app — open this page, run this command, confirm this state changes —
not "run the tests" or "check CI". Each item should be something a reviewer
can actually do and observe. If the user passed focus guidance after
/qa-checklist, weave it in — it's a hint about what to emphasize, not a
free-form append.
The checklist isn't frozen at creation — /pr's refresh mode re-derives it
against the real diff at the end of implementation, and re-running
/qa-checklist directly refreshes it whenever the body of work changes in
between.
Classify automatability (a table after the checklist)
Keep the list itself clean — no inline tags or metadata on the items beyond the leading slug (legibility). Right after the checklist, emit a table with one row per checklist item, in the same order, keyed by that slug — a table, because the PR body is read rendered on GitHub:
| Item | Automatable | Covered? | Notes |
| ------------- | ----------- | -------- | -------------------------------------------------------------------------- |
| `stream` | e2e | ❌ | Drive the chat route, send a prompt, assert the reply streams in |
| `bad-input` | integration | ✅ | `handler.test` — unknown id → 400 + the "not available" copy |
| `token-count` | unit | ❌ | Counter returns the right count for multi-part input |
| `empty-state` | manual-only | — | Empty state must match the reference design pixel-for-pixel |
Columns:
- Item — the checklist item's slug (the
`slug`it leads with). - Automatable — the cheapest test layer that could cover it, from the
project's layered stack (see CLAUDE.md § "Testing", and any test directory's
CLAUDE.mdor.claude/rules/file covering tests):unit(pure logic or app-owned component behavior),integration(multiple units, or the HTTP boundary, or a handler's effects),e2e(a full flow against a built artifact), ormanual-only(genuinely needs human judgment and can't be meaningfully asserted: pixel/visual match against a reference, live third-party behavior, subjective UX feel). - Covered? — whether a backing test already exists:
✅yes /❌no /—formanual-only. Outside an implementation flow (see below) leave this—/unknown; it gets filled in there.- A
✅does not imply "protected at merge". Which bucket the backing test lives in decides when it actually runs. If the project splits its test buckets — some in the fast vet run, some only on a CI dispatch or a nightly — mark the deferred ones✅ (nightly)so the operator can see which rows the merge itself does not protect. Treat a cluster of them as a reason to dispatch the expensive bucket before landing (/test-on-gh, if the project has hydrated it).
- A
- Notes — a one-line test sketch for uncovered automatable rows, or why a
row is
manual-only.
The table is a durable, refreshable property of the change, so it lives in the PR body alongside the checklist.
Fill in coverage & flag gaps (implementation flows only)
This applies only when the QA run accompanies an implementation — the change
under QA includes code, not docs/config alone (e.g. a session that just
implemented a feature). A docs-only change or a pure body refresh has nothing to
automate → leave Covered? as —/unknown and skip the rest.
This skill does not write test code — it edits a PR body; it does not push commits (unchanged contract). Instead, for each row marked automatable, search for an existing backing test and set Covered? accordingly. Every automatable row left at Covered? = ❌ is a gap: surface it in the Step 4 report so the enclosing flow (or the user) implements it before merge. Follow the project's testing conventions for where each layer's tests live and its must-test rules — CLAUDE.md requires authorization/permission code to have tests, so an uncovered row touching it is a hard gap, not a nice-to-have.
Step 3 — Write it into the body (idempotent)
Don't reconstruct the body inline — that burns tokens and risks dropping the parts you're not touching. Pull the live body to a transient local file, edit only the QA section there, push it back:
python3 scripts/pr-body.py pull <n> # writes docs/pr/<n>/body.md (pass --repo OWNER/REPO if needed)
Open docs/pr/<n>/body.md and locate a verification-checklist section — a
## QA Checklist heading (also recognize legacy ## Manual QA and
## Test plan headings):
- A conforming checklist already exists (under one of those headings, a
proper
- [ ]checklist that already covers the change, with its slug-keyed classification table) → leave it. Nothing to push; remove the transient file (rm -f docs/pr/<n>/body.md— not the whole directory, which may also hold apr.mdexport) and report. - A section exists but is stale/empty/malformed → replace that section in
place with the freshly derived
## QA Checklistchecklist. - No such section exists → insert a new
## QA Checklistsection after the Summary section and before any trailingCloses #N/Fixes #Nline and thehttps://claude.ai/code/session_…attribution line.
Everything you don't edit in the file stays verbatim. Then push it back (this
PATCHes the PR body and deletes the transient body.md, leaving any co-resident
pr.md export in place):
python3 scripts/pr-body.py push <n>
pr-body.py goes through the REST API, which is also what keeps it clear of the
gh pr edit failure @.claude/skills/pr/SKILL.md Step 5 describes.
Step 4 — Report
Print the PR URL on its own line and a one-sentence summary of what changed (added / refreshed / left unchanged). In an implementation flow, also list the automatable-but-uncovered rows (Covered? = ❌) so the enclosing flow / user can close those test gaps before merge. Stop.
Do not mark the PR ready for review, run the vet suite, or push commits —
those belong to other skills (/finalize).