draft-pr
Drafts a PR/MR title and markdown body from a branch's commits. Stops at the clipboard by default — for the user to review and paste into `gh pr create`, `glab mr create`, or the web UI; opens the PR/
Files2
View on GitHubSKILL.mdengineering/draft-pr/SKILL.md
GitHubPR Message
This skill stops at the clipboard by default, on purpose: the user wants to read the title and body before pasting them into a PR (GitHub) or merge request (GitLab) — via gh, glab, or the web UI. The one exception is an explicit instruction to open — the user (or an orchestrating workflow) saying "draft and open it"; then Step 6 runs the create command instead of stopping. Absent that instruction, stop at the clipboard — an inferred intent to open is not an instruction. ("PR" throughout means either; the job is identical.) Two siblings cover the adjacent jobs:
commit-me— makes every commit: formats, writes the message, stages, commits.aa-commit-clarity— advisory only. Use first when a branch contains genuinely separable concerns and you want to think about whether it should be one PR or several before drafting the body.
The body of a PR is a different artifact than a commit message. A commit explains one change; a PR explains a branch — usually multiple commits — to a human reviewer who has not been living inside it. The diff shows the what; the body supplies the why. That principle drives every choice below — the job is reviewer comprehension, not change logging.
Where the signal lives. In a well-maintained branch the commit messages already are the human-readable summary of what changed and why — the author wrote them while context was fresh. Treat them as the primary source. The diff is a fallback for cases where the commit messages are thin, the branch is one big WIP commit, or you need to verify a specific claim. Don't burn tokens re-deriving the story from raw hunks when the author already wrote it down.
Workflow
Step 0: Setup
Three quick checks before drafting:
Identify the base branch. Try in order; use the first that succeeds:
git symbolic-ref --quiet refs/remotes/origin/HEAD(strip therefs/remotes/origin/prefix) — the repo's configured default.git rev-parse --verify --quiet origin/main, thenorigin/master.- Ask Atila. Don't guess.
Confirm we have a branch to PR. If
git rev-parse --abbrev-ref HEADreturns the base, stop and tell Atila — there's nothing to PR.Check for a PR/MR template. This shapes the body's sections, so it has to happen before you draft anything. Look for any of:
- GitHub:
.github/pull_request_template.md(and case variantPULL_REQUEST_TEMPLATE.md), or.github/PULL_REQUEST_TEMPLATE/— a directory of multiple templates (pick the one that fits, or ask) - GitLab:
.gitlab/merge_request_templates/*.md— a directory; the filename is the template name (e.g.Default.md). Pick the fitting one, or ask. docs/pull_request_template.mdorpull_request_template.mdat repo root
One-liner:
find . -maxdepth 3 \( -iname '*request_template*' -o -ipath '*/merge_request_templates/*' -o -ipath '*/pull_request_template/*' \) -not -path './node_modules/*' 2>/dev/null. If a template exists, use its section headings in Step 4 instead of inventing your own. Fill in only the sections that genuinely have content — leave a section blank rather than fabricating filler.- GitHub:
Step 1: Read the branch — commit messages first
The commit messages are the primary source. Read them in full, not just subjects:
git log <base>..HEAD --format=fuller
(or git log <base>..HEAD --pretty=format:'%h %s%n%n%b%n---' for a tighter view of subject + body per commit). Authors often put the why in the commit body where --oneline would hide it.
Then get the file shape so you can describe scope honestly:
git diff <base>...HEAD --stat
Note the three dots — that's branch-vs-merge-base, which is what reviewers actually see.
When to read actual hunks. Default: don't. The commit messages plus --stat are enough for 99% of branches. Reach for git diff <base>...HEAD -- <path> only when:
- The branch is one big "wip" / "fix" commit and the message tells you nothing.
- A commit message claims something specific (e.g. "fixes race in deviceId mint") and you want to confirm the body summary you're about to write reflects what actually changed.
- The user explicitly asks you to double-check.
When you do read hunks, prioritise files where the diff shape suggests semantic change — new files, large +/- deltas, files matching obvious feature names. Skip lockfiles, snapshots, and formatter-only churn.
Also surface anything dirty or untracked that looks related to the branch's theme — easy to forget before pushing. Frame as a heads-up, not a blocker:
"Heads up —
docs/investigations/foo.mdis untracked. Want it in this PR before I copy the message?"
Step 2: Cohesion check (non-blocking)
Before drafting, ask: would a reviewer see this as one story, or several unrelated stories stapled together?
If it's one story, keep going. If it spans a few independent concerns, don't ask permission to proceed — draft the PR anyway and add a single-line P.S. when you hand off:
"FYI this branch looks like it spans a couple of independent concerns. If you want to think about splitting before opening,
aa-commit-clarityis the tool. Otherwise the message below is ready to paste."
There are intentional reasons to bundle (end-of-sprint cleanup, related-but-separate fixes, feature plus its prep work). Surfacing the signal once, without blocking, respects Atila's time.
Step 3: Draft the title
The title is the single most-read line in the PR. Rules:
- Short, imperative, no trailing period.
- Match the repo's recent merged PR/MR style — sample with
gh pr list --state merged --limit 8 --json title(on GitLab:glab mr list --merged) if the CLI is available and authed. Some repos use conventional-commit prefixes (feat(scope): …), some use plain imperative ("Add JWT refresh flow"). Match what's already there. - Conventional-commit scope at the PR level is fine but not required. PR titles are read by humans skimming a PR list, not by a changelog generator.
- If the branch is one commit, the PR title is usually that commit's subject. Don't reinvent it.
- If the branch is many commits serving one theme, name the theme. Don't list the commits.
Examples (the same change, different repo styles):
Make recommended ads client-side and cache-safefeat(recommender): move fetching to client and harden device-id reads
Pick the style the repo already uses.
Step 4: Draft the body
If a PR template exists (Step 0), use its sections. Otherwise this template is a maximum, not a minimum — size the body to the change. A one-line bugfix doesn't need a ## Why section; a 30-file refactor probably does.
## Summary
<One short paragraph naming the user-visible or system-visible outcome. Not "this PR does X" — say what now works differently.>
## Changes
- <Thematic bullet 1 — group related edits, not file lists>
- <Thematic bullet 2>
- <…>
## Why
<Optional paragraph. Skip this section unless the WHY adds something the Summary didn't. The body of a PR exists for the reviewer's benefit — if they'd ask "why?" while reading, answer it here. If they wouldn't, don't pad.>
Rules:
- Group by theme, not by file. "Adds
useDeviceIdhook" beats "modifiessrc/lib/device/use-device-id.ts". The diff already lists the files. - The diff shows the what; the body supplies the why. Give the reviewer enough context to evaluate whether the what is the right what.
- No attribution footers. No
Co-Authored-By, noGenerated with…, no🤖. The PR author is the human pushing it. - Don't enumerate every commit. Reviewers can click "Commits" if they want that. The body is the human summary.
- Issue reference (optional): if the branch's parent issue can be confidently determined — the user mentioned it, the branch name encodes it (
spec/252-…,123-fix-login,feature/GH-123), or the commits carryRefs #123— end the body withCloses #<n>on its own line (Closeshere, unlike commits'Refs: merging the PR is exactly when the issue should close). If it can't be confidently determined, skip the line silently — don't ask and don't guess.
Worked example
Branch: 3 commits adding a useDeviceId hook and migrating two call sites.
Commit log:
feat(device): add useDeviceId hook with client-side mint
refactor(recommender): use useDeviceId hook
refactor(search): use useDeviceId hook
Output:
Title: feat(device): add useDeviceId hook and migrate consumers
Body:
## Summary
Centralises device-id reads behind a `useDeviceId` hook so consumers no longer have to remember the mint-if-missing dance.
## Changes
- New `useDeviceId` hook that mints client-side on first read and reuses thereafter.
- Recommender and search now read through the hook instead of touching cookies directly.
## Why
Pulls device-id handling out of feature code so the Varnish-prep work (client-first mint) can land in one place rather than being duplicated at every call site.
Notice what's not there: no file paths, no per-commit bullets, no "this PR does X" phrasing. ## Why is included only because the surrounding context (Varnish prep) isn't obvious from the diff alone.
Step 5: Polish the body
If unslop is in your available skills, call the Skill tool with unslop on the drafted body; otherwise skip this step and hand off the draft as is. Keep template headings, code spans, identifiers, and the Closes #<n> line exactly as drafted; the title keeps the repo's format from Step 3. Done when unslop has run (or is unavailable) and every claim in the body still matches the commits.
Step 6: Hand off
Write the body to a temp file first — this avoids shell-quoting corruption when the body contains code blocks, backticks, or $. Direct printf "$BODY" | pbcopy works for plain text but silently mangles realistic PR bodies.
cat > /tmp/pr-body.md <<'EOF'
<the body verbatim>
EOF
pbcopy < /tmp/pr-body.md
(No clipboard utility available? Just leave the file in place and tell Atila where it is.)
Print the title and body to the chat too, so Atila can grab either independently. Suggest the file-based invocation as primary — it survives any body content:
# GitHub
gh pr create --title "<title>" --body-file /tmp/pr-body.md
# GitLab — no file flag, but quoted command-substitution is safe even with backticks/$ in the body
glab mr create --title "<title>" --description "$(cat /tmp/pr-body.md)"
The clipboard form (gh pr create --title "<title>" --body "$(pbpaste)") is fine as a fallback for short bodies without quotes or backticks, but don't recommend it for bodies that contain code blocks.
If the invoker explicitly asked to open the PR: run the file-based create command yourself instead of handing it off, and report the PR URL. Everything else above still applies — including printing the title and body in full, and the push check below (an unpushed branch still stops you; the user pushes).
Check that the branch is pushed. git status won't tell you this reliably — it only reports against the configured upstream. Use:
git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null
If that command fails (no upstream configured), mention it — "Branch isn't pushed yet — git push -u origin <branch> first." — but don't run git push yourself. Atila pushes on his own terms.
One boundary not covered above: this skill describes the branch, it doesn't reshape it — no amend, rebase, squash, or reword. History cleanup, the merge-strategy verdict, and the merge itself happen after review, outside this skill.