| name | create-pr |
| description | Draft a PR description into scratch/, pause for your review, then create or update the PR via gh. Gathers git context, indexes the session for semantic search, and follows the repo's CLAUDE.md PR guidelines. |
| argument-hint | [base branch, defaults to 'main'] |
| user-invocable | true |
| disable-model-invocation | false |
| effort | max |
| allowed-tools | ["Bash(.claude/skills/create-pr/gather-pr-context.py:*)","Bash(.claude/skills/create-pr/publish-plan.py:*)","Bash(~/claude-workspace/scripts/gh-upload.py:*)"] |
Create PR: Context → Draft → Review → Submit
Gathered context
!.claude/skills/create-pr/gather-pr-context.py "$ARGUMENTS"
Instructions
[!IMPORTANT]
Run every phase — do not skip one because you think you can shortcut it. Two skips are
specifically forbidden:
- Phase 1 enrichment is mandatory even if you lived this session. Your memory rounds off the
user's exact corrections; the transcript doesn't. Search it.
- Never drop a step over a judgment call ("the plan has PII," "this arg looks fine"). Handle the
concern inside the step — Phase 1: search anyway; Plan: have the user scrub. Skipping is the
user's decision, not yours.
Phase 1: Context enrichment
Before drafting, get the why — and the why is the user's, not yours.
Mandate gate — read it from the spine, or halt. The gather extracted the session's full user-intent spine (every verbatim user directive, deterministic) to the path it printed — this, not semantic search, is the source of record for why the change exists. Hand the spine and this branch's diff/commits to an agent (the spine can be long — let the agent read all of it) and have it return the user directive(s) this change folds into. That match is the mandate:
- Found → codify it in the body (paraphrased — the spine is verbatim user text and this repo is public) so the PR carries its own provenance. Intent bolsters the PR; it doesn't just clear a gate.
- Absent → the spine holds every ask and none fits. Stop and ask before drafting ("I can't find where you asked for this — is it intended?"). A rationale reconstructed from the diff is not a mandate — "I can justify this" ≠ "the user wanted this," and they diverge exactly when it's most dangerous: a plausible, well-described, unasked-for change. Doubly so when codifying uncommitted work —
git log -- <files> first; a change that reverts a merged PR with no spine entry behind it is a discard candidate, not a PR.
Then enrich — beyond the bare ask, recover the decisions and research behind the change via mcp__document-search__search_documents (collection document-chunks, the indexed transcript) and related/stacked PRs. The git context (commits, files changed, existing-PR mode) is in the gathered output above. Do not draft until the mandate is established or escalated.
Phase 2: Scratch file
Drafts live in the repo-root scratch/ (gitignored). Filename pr-{descriptive-slug}.md — match the topic, not a ticket. If a pr-*.md for this change already exists (listed above), reuse it.
Phase 3: Draft the description
Follows the repo's PR guidelines (CLAUDE.md → PR Description Guidelines).
The description is the human layer. It carries what CI can't: the why, the
decisions, and the empirical verification no check runs. Never relitigate what CI already
proves (tests, lint, types, pins) — that's enforced on every change, and prose restating it
only goes stale. If a claim you're about to write could be a CI check, the ideal is to
push it there, not assert it once here.
Title — type(scope): effect, imperative, ≤50 chars. Infer the conventional-commit
prefix (feat/fix/refactor/docs/perf/ci/test; ! for breaking) and scope from
the diff; name the effect, not the mechanics ("add search to the dashboard," not "add
SearchController"). It becomes the squash-merge subject and the searchable archive entry;
if it won't fit one line, the PR may be doing too much — say so.
Body — a lean skeleton; drop any section that doesn't apply (a one-line fix doesn't
get four headings):
- Context / TLDR — one line: what this is and why, graspable at a glance. Build it by
abstracting to intent — never paraphrase the diff back at higher word count.
- Why — the problem, key decisions, alternatives rejected; link issues (
Fixes #N).
Drop if the TLDR already carries it.
- Changes — 3–6 behavior-level bullets; omit the obvious. The diff shows the how.
- Test plan — the empirical residue: what you verified that CI can't — "ran the
real system, observed X," repro steps, edge cases, and untested conditions + their
risk. Not "CI's green" — CI owns that.
- After merge (required) — actions that have to happen once this lands, or the change is left
incomplete or broken: a data migration to run, an index to backfill, a dependent system to update
before it breaks against the new shape. List each as a task-list checkbox (
- [ ] …) under a
heading named exactly ## After merge (required), so an automated sweep can find the unmet ones.
The bar is broken or incomplete without it; anything that merely could be done later belongs
elsewhere, not here. Omit the section when there are none.
Shape — write for an engineer skimming it for the first time:
-
TLDR-first, scannable, never imposing — the gist in a ~20-second skim, no scrolling.
-
<details> for depth — collapse mechanism write-ups, evidence, and long lists; keep at most one high-impact item at top level.
-
Reach for GitHub-Flavored Markdown when it beats prose (all verified to render in a PR body) — a table for options / tradeoffs, an alert (> [!WARNING] / > [!NOTE]) for a caveat or breaking change, a Mermaid fenced block for architecture or flow, fenced code for commands + output, a footnote ([^1]) for an aside. Use only when it out-scans prose — a one-line fix needs none. Mermaid is the only diagram engine GitHub renders; Graphviz / D2 / PlantUML / Vega-Lite and ::: containers / ==highlight== fall back to raw text, so don't reach for them.
-
Screenshots / visual evidence — for rendering, UI, or layout work, pixels out-evidence prose. PR bodies render images only from URLs (base64 data URIs are stripped; local paths don't upload), so upload them first:
~/claude-workspace/scripts/gh-upload.py <image>...
Prints a paste-ready markdown line per file ( for images) pointing at a permanent first-party user-attachments URL — the same kind drag-dropping into the web editor produces; failure modes self-report with their remedy. Pair before/after captures in a table or side-by-side inside <details>. Uploading is auto-allowed like publish-plan.py, which makes the check before it the only gate: the public-repo PII gate applies to pixels — screenshots carry window chrome, document titles, and content vocabulary, so re-capture with generic content rather than upload-then-scrub (uploads are permanent; there is no delete).
-
Terse but COMPLETE — don't drop substance, hide it (push the bulk into <details>).
-
What & why, not how. No journey residue — no "Following review…", no dev chronology, no AI fluff; readable by someone who never saw this session.
-
Re-verify numerical claims (test counts, perf metrics) against current code before finalizing.
GitHub auto-link safety — the body renders as GitHub-flavored markdown:
- Bare
#N links to issue/PR N and @name pings that user — and so do GH-123 and bare
40-char SHAs. Backtick any generated #N / @handle / SHA that isn't a deliberate
reference (`#26`); use names or item N for list positions.
#N already renders the issue's title on GitHub — don't also write the title yourself.
- Closing keywords (
Fixes #N) auto-close only against the default branch and fire on
loose wording — use exactly one per genuinely-closed issue, Refs #N for related ones.
Claude Code Plan (when the session has one) — the plan is Claude's launch point, not kept live after implementation, so treat it as auxiliary origin context parked at the bottom, never the headline. First scan the plan for anything sensitive — secrets (tokens, keys) and personal/identifying details (private hostnames, home-network layout, personal paths, PII) — and sanitize it yourself (genericize the specifics) — a gist is URL-reachable, so scrub it before it leaves the repo. Then publish the session's plan (a *plan*.md written this session, or a path the user names) — the same file, never a copy:
.claude/skills/create-pr/publish-plan.py <plan>
[!IMPORTANT]
Publishing is automatic — the model runs publish-plan.py itself (a
Bash(.claude/skills/create-pr/publish-plan.py:*) allow-rule in .claude/settings.json clears the
data-exfiltration gate). That makes the sanitize step above the only gate, so it is mandatory:
genericize secrets + personal/network details before publishing — nothing else stands between the plan
and the gist.
It creates the secret gist on first run and reuses it on re-runs (a local slug→gist-id store — no duplicate gists), then prints the gisthost viewer URL. Link it just above the session-provenance trailer, using that URL as the href:
<a href="{viewUrl}">
<img src="https://github.com/user-attachments/assets/fcc9f9e9-e066-462d-9894-1f0ac2eda6f2" alt="Claude" width="16"> Claude Code Plan
</a>
Skip the section entirely if there's no plan.
Session provenance (always) — end the body with: <sub>Claude Code session <code>SESSION_ID</code></sub> (the gather step prints the session ID; or claude-session info). ID only — the machine is recoverable from the ID across the mesh.
Public-repo PII gate (this repo is public). Before writing the body — and again before create/update — scan both the PR body and the diff for workplace-identifying content: employer / workspace names, internal project / system / teamspace names, space IDs, private hostnames. Keep it workplace-neutral (generic scale metrics are fine — "~14K pages," not the workspace name); see CLAUDE.md → Public Repository. A leak in a pushed commit survives in history (and on forks / caches) even after a later fix — catch it here, scrub, re-commit, then open the PR.
Write the file to scratch/.
Phase 4: Pause for your review
Tell the user: "Draft is ready at scratch/{filename}.md — review in your IDE."
Proceed signal: if the user rejects a Write to the scratch file and says "create the PR", they edited it in their IDE — use the file as-is, do not re-draft.
Phase 5: Create or update
Create (no existing PR):
gh pr create --base <base> --head <branch> --title "..." --body-file scratch/{filename}.md
Use --body-file — never inline --body "$(cat ...)" (shell-escaping).
Update (existing PR detected above):
gh api -X PATCH repos/chrisguillory/claude-workspace/pulls/{number} -F body="$(cat scratch/{filename}.md)"
Use REST — gh pr edit --body hits the deprecated Projects Classic API.
Phase 6: Post-creation
Report the PR URL. Merges here gate on CI (ruleset-protected, linear history) — offer gh pr merge --squash --auto if the user wants it to land on green.
After the PR is open, never amend or force-push. Land follow-ons — review responses,
fixes, adjustments — as new dedicated commits (fix: …, "address review", "add test")
so anyone watching can see what changed since they last looked; a force-push erases that
incremental view. The squash-merge collapses the trail into one clean commit on main, so
you keep reviewability during the PR and a tidy history after.
Key rules
- Never inline the PR body — always
--body-file (create) / gh api -F body="$(cat ...)" (update).
- REST API for updates —
gh api -X PATCH, never gh pr edit --body.
- Gather context first — read the session (decisions, feedback) before drafting.
- Public-repo PII gate — scan the body and the diff for workplace identifiers before create/update; this repo is public (CLAUDE.md → Public Repository).
- Scratch file is the collaboration point — the user refines in their IDE; the model uses it as-is.
- What shipped, not the journey — no iteration history in the body.
- Description ≠ CI layer — carry the why + the empirical verification CI can't; never relitigate what CI proves (tests, lint, types).
- After merge = required only — post-merge actions that leave the change broken if skipped go under
## After merge (required) as scrapable checkboxes; anything that merely could happen later belongs elsewhere, not the PR body.
- Title —
type(scope): effect, imperative, ≤50 chars; name the effect, not the mechanics.
- No force-push once open — land follow-ons as new commits so the change-since-last-look stays visible; squash collapses them at merge.
- Don't flip the user's checkout — a second PR in one session builds in a
git worktree (or commits, then restores the branch the user was on); their working copy and uncommitted WIP stay where they left them.
- TLDR-first, depth in
<details> — write for a ~20-second skim, never a wall.
- Visual evidence via
~/claude-workspace/scripts/gh-upload.py — screenshots/renders upload to permanent first-party user-attachments URLs (same as web drag-drop); the pixel-level PII check happens before uploading, by re-capturing clean.
- Session provenance — end every body with
<sub>Claude Code session <id></sub> (ID only).
- Claude Code Plan — when the session has one, scrub it for secrets and personal/PII details, then publish via
.claude/skills/create-pr/publish-plan.py <plan> (the same file; idempotent slug→id store, no duplicate gists) and link it at the bottom; auxiliary origin context.
Anti-patterns
- Drafting before reading the session context.
- Re-writing the scratch file after the user edited it — recognize the proceed signal.
gh pr edit — deprecated Projects Classic API; use gh api.
- Inlining the body in the command — use
--body-file.
- Journey residue — "Following review…", dev chronology, padding.
- Restating CI — "tests pass," "lint clean," "types check" is the CI layer's job; the body's test plan is for the empirical verification CI can't run.
- Orphaning a must-do — a required post-merge action (migration, backfill) buried in the test plan or omitted; it's lost when the session ends. Put it under
## After merge (required). And keep the section to genuine must-dos — optional next-steps aren't tracked here; they belong in another system, not the PR body.
- Amending / force-pushing an open PR — destroys the reviewable "what changed since I last looked" view; add new commits instead.
- Wall-of-text body — if the reader must scroll to get the gist, move depth into
<details>.
- Screenshots left on disk — arguing a visual change in prose while the captures sit in a temp dir; upload via
~/claude-workspace/scripts/gh-upload.py and embed them.
- Bare
#N / @name — #N links to issue N and @name pings; use descriptive names or item N.
- Leaking workplace PII — employer / workspace / project / teamspace names or space IDs in the body or the diff; this repo is public, so scrub before push (CLAUDE.md → Public Repository).