create-pr · git:20261015.8339eb7 · 2026-10-15 · sha256 66be7feff69cc3b3
create-pr git:20261015.8339eb7A
Immutable. This exact content never changes and is served at /api/v1/blob/66be7feff69cc3b3.
---
name: create-pr
description: Create or update a GitHub PR with automatic template detection and filling, enforcing a plain staff-engineer writing voice in the PR body
argument-hint: "[--ready] [--force] [--parent <branch>] [<title>]"
model: sonnet
metadata:
execution-tier: balanced
---
# Create PR
Create (or update) a GitHub pull request using `gh`, auto-detecting and filling any PR template.
## Writing voice
Apply the `plain-writing` skill in technical mode to the PR body, then apply these PR-specific rules on top. Write the way a staff engineer messages a colleague who reviews the code: one direct sentence per idea, no ceremony.
Never produce any of these. Each one immediately signals AI authorship:
- Em dashes (—) or en dashes (–). Use a comma, colon, semicolon, parenthesis, or split into two sentences. No exceptions.
- Bold inside prose sentences. Bold is for labels at the start of a line only (e.g., `**Test plan:**`). Never bold error messages, key terms, or warnings mid-paragraph.
- Bolded pseudo-labels like `**Root cause:**`, `**Caveat:**`, `**Note:**`. Make it a real heading or fold the content into a sentence without a label.
- Hedging meta-commentary such as "I'm an agent" or "I have not verified this". State what you did and what you found.
- Inflation words: critical, pivotal, robust, seamless, leverage, utilize, ensure, facilitate.
- Title Case headings, emoji, or a closing summary paragraph. Use Sentence case and stop when the point is made.
Two exceptions: a template-requested agent-context section speaks as the agent (see Step 5), and a footer or trailer the coding tool mandates stays as given.
How it sounds (cause and effect in one sentence, caveats as plain declarative sentences with no label):
> The workflow calls `getMembershipForUserInOrg()` with the default `GITHUB_TOKEN`, which only has repo scope and can't read org-private team memberships. The API returns `404` (not `403`) when it can't see the membership, producing the misleading error.
>
> If the app doesn't have Organization > Members > Read permission, the same error will recur.
## Arguments
- `--ready`: create the PR ready for review instead of as a draft (PRs are drafts by default)
- `--force`: skip preview and confirmation; create or update immediately
- `--parent <branch>`: target the PR at this base instead of auto-detecting; it becomes the PR's base on GitHub, so it must be a branch name (bare or `origin/`-prefixed), not an arbitrary ref
- `<title>`: optional title hint; if omitted, derive from commits
Example invocations:
- `/create-pr`
- `/create-pr --ready`
- `/create-pr --force`
- `/create-pr --parent origin/parent-branch`
- `/create-pr Add support for webhook retries`
- `/create-pr --ready Fix race condition in job queue`
- `/create-pr --force --ready Fix race condition in job queue`
## Steps
### 1. Parse Arguments
Extract from user input:
- `draft` = true unless `--ready` is present (PRs are drafts by default)
- `force` = true if `--force` is present
- `parent` = branch after `--parent`, or empty
- `title_hint` = remaining text after stripping `--ready`, `--force`, and `--parent <branch>`, or empty string
### 2. Gather Git Context
Determine the base branch and gather context. The base is normally the repo's default branch, but for stacked PRs it's the parent branch in the stack.
```bash
eval "$(bash "$HOME/.dotfiles/bin/lib/git-pr-base.sh")" # append --parent <branch> if the user passed one
base="$BASE" # bare branch name: gh pr create --base and git ls-remote need this form
stacked=$([ "$base" != "$DEFAULT" ] && echo true || echo false)
```
The helper resolves the stack-aware base, an existing open PR's base foremost (never silently retarget a PR someone already opened); the precedence and full key contract are documented in its header. This skill consumes `BASE` (bare name), `REF` (the diffable, origin-preferred form — use it for every log/diff below), `PR` (the open PR's number, if any), `DEFAULT`, and `NOTES` (non-empty when detection degraded). If the helper is not available or `base` is empty, tell the user and **stop**. If `NOTES` reports the PR's base has no local ref, it names the exact `git fetch` command — run it, then re-run the helper.
Then run in parallel:
```bash
git log "$REF"..HEAD --oneline # commits on this branch
git diff "$REF"...HEAD --stat # changed-file summary
if [ -n "$PR" ]; then gh pr view "$PR" --json number,title,isDraft,url; fi # existing PR's display fields
```
The helper already determined whether an open PR exists: a non-empty `PR` means you will **update** that PR rather than create a new one (see Step 9). `PR` can be empty-but-wrong in exactly two cases — you passed `--parent` (the override skips the PR lookup) or `NOTES` says GitHub was unreachable; in those cases check with `gh pr list --head "$(git rev-parse --abbrev-ref HEAD)" --state open --json number,title,isDraft,url --jq '.[0]'`.
When composing the PR body in Step 5, use the `--stat` output to decide how much diff to read:
- If the stat shows fewer than ~200 changed lines total, fetch the full diff with `git diff "$REF"...HEAD`.
- Otherwise, read per-file diffs only for files that are directly relevant to the PR description: `git diff "$REF"...HEAD -- <path>`. Skip generated files, lock files, and files whose names make their changes obvious (e.g. version bumps in `package.json`).
### 3. Find PR Template
Check these locations in order and stop at the first match:
1. `.github/pull_request_template.md`
2. `.github/PULL_REQUEST_TEMPLATE.md`
3. `.github/PULL_REQUEST_TEMPLATE/`: use `default.md` if it exists, otherwise the only file present; if multiple files with no clear default, ask the user which to use
4. `docs/pull_request_template.md`
5. `pull_request_template.md`
If the template embeds agent-directed instructions (typically in comments), obey the ones that shape the PR, each at the step it affects: invoke body-shaping repo skills it names (e.g. posthog/posthog's `/writing-pr-descriptions`) before composing the body in Step 5 (skip ones whose guidance is already in context from this session), and fold assignee and label requests into Step 9's flags rather than a follow-up call. These instructions win over this skill's defaults, and explicit user arguments win over both. Anything beyond shaping the PR (e.g. running scripts) needs the user's OK.
### 4. Detect Saved Test Plan
Check for a saved manual test plan (produced by `/test-plan`):
```bash
test_plan_path=".context/test-plan.md"
[ -f "$test_plan_path" ] && test_plan_content=$(cat "$test_plan_path")
```
If the file exists, hold its full contents for use in Step 5. If not, skip; the body composition step writes its own short test plan instead.
### 5. Compose Title and Body
**Title:**
- If `title_hint` is non-empty, use it as the title (trim and keep under 70 characters)
- Otherwise derive a short imperative title from the commits (e.g., "Add webhook retry support")
**Body:**
If a template was found, fill each section using the commits and diff:
- Describe the code in its final state: what it does now, not what it replaced. This rule does not cover the problem the PR solves. State that problem even when it means saying how the system behaved before the change.
- Fill a prose section, such as a summary or a problem statement, with paragraphs rather than bullets, unless the template's own instructions ask for bullets. The agent-context section follows its own rules below.
- Remove unfilled optional sections rather than leaving placeholder text
- Leave checkboxes intact; check the ones clearly satisfied by the diff
- **Never include customer-specific data.** Redact or omit any team IDs, team names, organization names, user IDs, or other identifying customer information found in commits or diffs; describe the fix generically instead (e.g., "fixes flag evaluation for teams with large cohorts" not "fixes team 12345 / Acme Corp")
- **Agent/AI context sections: follow the template.** If the template asks about agent involvement or AI context (e.g. posthog/posthog's `## 🤖 Agent context`), fill it per "Filling an agent-context section" below. If no template section asks, the global instructions at `~/.dotfiles/ai/AGENTS.md` hold: don't volunteer AI attribution
- **Never hard-wrap prose.** Write each paragraph as a single line and let GitHub's renderer handle wrapping; only insert newlines between paragraphs, list items, or headings
- **Never escape backticks, dollar signs, or other markdown.** Step 9 passes the body through a quoted heredoc (`<<'EOF'`), which is literal; write `` `foo` `` not `` \`foo\` ``, and `$var` not `\$var`
**Voice.** Apply the writing voice from the `## Writing voice` section at the top of this skill throughout the body. The three hard rules (no em dashes, no bold inside prose, no bolded pseudo-labels) apply to every sentence you write here.
If no template was found, write two short prose paragraphs and a test plan:
- A paragraph on the problem: what breaks or is missing, and for whom. The body opens with it.
- A paragraph on how the PR solves the problem. Name only what a reviewer needs to judge the approach: the key decision, a rejected alternative, a risk, or a behavior change someone will notice. The diff carries the details, so don't walk through files, functions, flags, or values the reviewer will read in the code.
- A short **Test plan** section describing how to verify the change. It can be a checklist.
A small PR may need only two or three sentences above the test plan. The example under Writing voice shows the target voice. Keep customer-specific IDs and names out.
**Merge order.** When this PR must not merge until another PR merges, put a `Depends on <ref>` line for each such PR at the end of the body, above any tool-mandated footer, such as `Depends on #123` or `Depends on PostHog/posthog#456 (deployed)`. Write the ref as `#N` for a PR in this repo and as `owner/repo#N` for a PR in another repo. When the other PR must also be deployed first, end the line with `(deployed)`. A stacked PR needs no line for the PR it targets, because its base already shows that dependency, unless that PR must also be deployed first. Take each dependency from the user, the plan, or the work done in this session, and never guess one. When updating a PR, keep its existing `Depends on` lines unless the user says the dependency is gone. `/sessions` reads these lines to show which sessions wait on which.
**Filling an agent-context section (if the template has one):**
- The section's own embedded instructions win over this skill's defaults. If it is commented out with activation instructions (e.g. "uncomment if AI-assisted"), follow them; if it is commented out with no instructions, the repo didn't ask: remove it rather than uncommenting.
- Autonomy: pick honestly from the options the template offers. User-directed work (the normal case for this skill) is agent-assisted (e.g. posthog's "Human-driven (agent-assisted)"); reserve a fully autonomous label for work no human drove.
- Content: answer what the section asks at the size it asks (a lone checkbox gets a check, not an essay). When it wants prose: a handful of reviewer-facing bullets covering what the agent did, what the user decided or redirected along the way, and skills invoked. No verbatim prompts, no session logs, no sensitive data, no duplication of earlier sections.
- Voice: this section speaks as the agent, first person allowed; every other section stays in the user's voice. Every other Writing voice rule, hard and soft, still applies. Bold field labels the template itself defines (e.g. `**Autonomy:**`) are kept.
**Embedding the saved test plan (if `test_plan_content` is set):**
If a saved test plan was detected in Step 4, embed it inside the PR's testing section. Locate the testing section by matching the first heading whose title contains any of: `Test plan`, `Testing`, `How did you test`, `QA`. If no such heading exists in the template, append a `## Test plan` section at the end.
Insert this block as the section's content (replacing any auto-generated test plan you would otherwise have written):
```markdown
<details>
<summary>Manual test plan</summary>
<contents of .context/test-plan.md verbatim, with any leading `## Test plan` heading stripped>
</details>
```
Notes:
- Strip a leading `## Test plan` (or equivalent) heading from the file contents before insertion (the surrounding section heading already provides that context)
- Preserve the one-line `- [ ]` checkbox format exactly; do not re-wrap or reformat
- Keep the blank lines around `<details>` and `</details>` so GitHub renders the collapsible block correctly
- If the testing section already contains template guidance text (e.g., placeholders), replace that text with the `<details>` block rather than stacking them
### 6. Edit the prose before preview
First cut the problem and solution prose from Step 5. Remove text whose only job is to walk through the diff, such as tours of individual files and the step-by-step mechanics of code the reviewer will read anyway. Keep the problem, the key decisions, and anything a reviewer would miss in the code. Keep a value or identifier when it is the cause or the behavior change. This cut does not touch the test plan (including an embedded saved test plan), checkboxes, the agent-context section, the `Depends on` lines, or the footer. When the template or a repo skill it names sets its own rules for what to cut, follow those instead. `plain-writing` keeps every fact it receives, so it will not make this cut for you.
Then apply the `plain-writing` skill in technical mode to the composed body. Keep the template structure, its checkboxes, the `Depends on` lines, and any tool-mandated footer exactly as given, and leave the agent-context section in the first-person agent voice Step 5 requires. Last, enforce the PR-specific rules in the Writing voice section, and ask of each sentence: would a staff engineer send this verbatim in a Slack message to the reviewer? If not, simplify it. The agent-context section and the footer are exempt from that test. Show only the finished body in the preview.
### 7. Show Preview and Confirm
If `force` is true, skip to Step 8 immediately, do not show a preview or ask for confirmation.
Otherwise, display the proposed PR to the user. When `stacked=true`, include the base in the header so the non-default target is obvious:
```text
Title: <title>
Base: <base> # only show this line when stacked=true; append " (stacked)"
Draft: yes/no
Assignee/labels: <values> # only when the template's agent instructions set them
<body>
```
Ask: "Create this PR? Reply yes to confirm, or describe any changes to make."
Wait for confirmation. If the user requests edits, apply them and show the updated preview before proceeding.
### 8. Ensure Branch Is Pushed
When `stacked=true`, the parent branch must already exist on `origin` (GitHub can't open a PR against a base it doesn't have). Check first:
```bash
if [ "$stacked" = "true" ] && [ -z "$(git ls-remote --heads origin "$base")" ]; then
echo "Parent branch '$base' is not on origin yet. Push it first ('git push origin $base') and re-run."
exit 1
fi
```
Don't push the parent automatically. Pushing it publishes another branch, so the user decides when.
Then push HEAD:
```bash
git push --set-upstream origin HEAD
```
If the push fails, report the error and stop.
### 9. Create or Update PR
**New PR:**
The leading `: __create-pr-skill__ ;` is a shell no-op that flags this invocation as coming from the skill, so the `enforce-create-pr-skill` PreToolUse hook lets it through. Keep it on bare `gh pr create` calls; without it, the hook denies the command.
```bash
: __create-pr-skill__ ; gh pr create \
--base <base> \
--title "<title>" \
--body "$(cat <<'EOF'
<body>
EOF
)" \
[--draft if draft=true] \
[--assignee <user> / --label <label> when the template's agent instructions request them]
```
**Existing PR** (found in Step 2; the initial check did not fetch the body to save tokens, so fetch it now if you need to inspect it before writing the update):
```bash
gh pr view <number> --json body --jq '.body'
```
Note the existing body may already contain a `<details><summary>Manual test plan</summary>…</details>` block; replace it with the new one rather than appending. Treat an existing agent-context section the same way: update its facts in place, preserving any human edits, rather than regenerating or dropping it.
If the user passed `--parent` and the existing PR targets a different base, confirm the retarget with the user, then add `--base "$base"` to the `gh pr edit` call; without an explicit `--parent`, never change an existing PR's base.
```bash
gh pr edit <number> \
--title "<title>" \
--body "$(cat <<'EOF'
<body>
EOF
)" \
[--add-assignee <user> / --add-label <label> when the template's agent instructions request them]
```
If `draft=true` and the existing PR is not already a draft:
```bash
gh pr ready --undo <number>
```
If `draft=false` and the existing PR is a draft:
```bash
gh pr ready <number>
```
### 10. Request Reviews on PostHog/posthog
Run this step only for a PR that Step 9 just created, and only when the URL `gh pr create` printed is under `github.com/PostHog/posthog/` (compare case-insensitively). The update path skips it: each `reviewhog` add buys a paid ReviewHog round, and re-requesting Copilot starts another review. Skip it as well when Step 9 applied `skip-agent-review`, the label the template reserves for PRs that need no Copilot review.
```bash
gh pr edit <pr-url> --add-label reviewhog --add-reviewer "@copilot"
```
`gh pr edit` accepts the printed URL in place of a number. `gh` runs as the user, so the label and the review request are attributed to their account. Requesting them in a separate call after creation keeps a failed label or reviewer request from failing the PR creation itself. If the command fails, keep the PR and report the error next to its URL in Step 11.
### 11. Report Result
On success, display the PR URL, and the outcome of Step 10 when it ran. On failure, show the full error output and stop. Do not retry silently.