| name | jj-pr |
| description | Create or update GitHub pull requests for jj branches, including stacked PRs, existing PR updates, signed pushes, bookmark handling, PR body/title formatting, BLUF descriptions, dual BLUF (stack goal + this-PR goal) for stacked PRs, stack sections, and body-file based gh commands. Use whenever creating a PR, updating a PR after review fixes, pushing PR branch changes, or editing PR descriptions. |
| argument-hint | [optional: PR title or description hint] |
JJ Pull Request
Create or update a GitHub pull request from the current jj branch.
User hint: $ARGUMENTS
Signing Policy (Mandatory)
- Commit signing for pushed changes is required in this environment.
- Never bypass signing via config overrides (for example
--config git.sign-on-push=false) or by changing config to disable signing.
- If pushing fails because signing fails, stop and ask the user to resolve signer/agent issues; do not push unsigned as a workaround.
Process
1. Gather Current State
Run these to understand the current situation:
Determine the mode:
UPDATE mode: Closest bookmark is NOT trunk() (there's an existing feature branch)
CREATE mode: Closest bookmark IS trunk() (new branch needed)
UPDATE Mode (existing feature branch)
U1. Check for Existing PR
Get the bookmark name from closest_bookmark(@) using jj log --no-pager -r "closest_bookmark(@)" -T 'bookmarks'.
Run gh pr list --repo "<target-repo>" --head "<origin-owner>:<bookmark-name>" --json url,state,title,number --limit 1 to see if a PR exists.
U2. Detect Stacked PR vs Update
Compare closest_bookmark(@) with @-. If the bookmark is already on @-, skip to U3.
If the bookmark is behind @- (new commits exist beyond the bookmark), ask the user:
- Update existing PR (Recommended) - move the bookmark forward to include new commits
- Create stacked PR - keep the bookmark where it is and create a new PR targeting it
If Update existing PR: run jj tug to move the existing bookmark forward to the nearest pushable commit, then continue to U3.
If Create stacked PR: jump to CREATE mode (C1), passing the existing bookmark name as the stacked base branch.
U3. Handle Changes
If @ has changes:
Ask the user to choose:
- Squash into last commit (Recommended) - amend the previous commit
- Create new commit - add as a separate commit
If squash: run jj squash
If new commit: invoke /jj-commit (which uses jj commit, not jj describe), then run jj tug to move the bookmark forward
If @ is empty:
The changes are already committed. Proceed to push.
U4. Push Updates
Run jj git push --tracked to push the bookmark.
U5. Optionally Update PR Title/Body
If the PR exists, ask the user if they want to update the PR title or description.
If yes, use --body-file when updating the body so markdown, backticks, and shell-special characters are preserved:
cat > /tmp/pr-body.md <<'EOF'
<new body content>
EOF
gh pr edit <pr-number> --repo "<target-repo>" --title "<new title>" --body-file /tmp/pr-body.md
U6. Report Result
If PR exists: Show the PR URL and confirm updates were pushed.
If no PR exists: Proceed to CREATE mode step C3 to create the PR.
AMEND Mode (rewriting an existing PR's commit)
Use when you need to change what's inside an existing PR's commit — review fixes that should fold into the existing commit instead of stacking on top, scope expansion within a draft PR, or conflict resolution after a rebase. Distinct from UPDATE mode (which adds a new commit on top, moving the bookmark forward) and CREATE mode (which makes a new bookmark from trunk).
A1. Position the working copy on the target revision
jj edit <change-id>
Use the change ID of the revision whose bookmark matches the PR you want to amend. The working copy is now ON that revision; descendants auto-rebase as you mutate.
A2. Make changes
Edit files normally. jj snapshots automatically — do NOT run jj commit or jj describe to land them. Your edits become part of the existing revision.
If descendants conflict during the rebase, resolve them in place; jj resolve --list shows what needs attention.
A3. Verify
Build and test as needed. The commit message and bookmark stay attached to this revision; only the SHA changes.
A4. Push
jj git push --bookmark <bookmark-name>
If multiple bookmarks in the stack moved (e.g., a rebase shifted every revision's SHA), push them all together:
jj git push --tracked
Each tracked bookmark whose target SHA changed gets pushed; bookmarks already in sync are skipped.
A5. Update PR description if scope changed
If the amendment changed what the PR does (not just how), update the description per U5 — the diff is now different from the body's claims.
CREATE Mode (new branch from trunk or stacked base)
C1. Handle Uncommitted Changes
If @ has changes, invoke /jj-commit first. This uses jj commit (not jj describe) so that @ becomes empty and the committed change lands at @-.
C2. Create Bookmark
Run jj log --no-pager -r "@-" -T 'description.first_line()' to get the commit message at @-.
If this is a stacked PR, leave the stacked base bookmark where it is. Do not run jj tug or otherwise move the base bookmark; create a new bookmark for the follow-up PR at @-.
Generate a branch name:
- Prefix with
rwjblue/
- If the commit message has a category prefix, include it as a kebab-case prefix in the branch name:
[Category Name] ... format: extract the bracketed text
type(scope): ... format: extract the scope
- Follow with 2-4 descriptive words from the rest of the message
- Use kebab-case (lowercase with hyphens)
- Examples:
[MCP Server] Enable JSON response -> rwjblue/mcp-server-enable-json-response
feat(skills): Add stacked PR support -> rwjblue/skills-add-stacked-pr-support
Add dark mode toggle -> rwjblue/add-dark-mode-toggle
Present the proposed branch name to the user for confirmation.
Once confirmed, run jj git push --named "<bookmark-name>=@-".
C3. Create Pull Request
First, check for a PR template:
cat .github/PULL_REQUEST_TEMPLATE.md 2>/dev/null || cat .github/pull_request_template.md 2>/dev/null || echo "No template found"
Gather information for the PR:
- If stacked: use
jj log --no-pager -r "<stacked-base-bookmark>..@-" -T 'description ++ "\n---\n"' and jj diff --stat -r "<stacked-base-bookmark>..@-"
- Otherwise: use
jj log --no-pager -r "trunk()..@-" -T 'description ++ "\n---\n"' and jj diff --stat -r "trunk()..@-"
If this is a stacked PR, gather the full stack for the PR body:
- Run
jj log --no-pager -r "bookmarks() & trunk()..@-" --reversed -T 'bookmarks ++ "\n"' to find all bookmarks in the stack (bottom-up order, base first)
- For each bookmark, run
gh pr list --repo "<target-repo>" --head "<origin-owner>:<bookmark>" --json url,title,number --limit 1 to get its PR info
- Include the current PR (being created now) as the last entry
Determine stacked PR creation mode:
- True stacked mode: only when
<stacked-base-bookmark> exists as a branch in <target-repo>
- Check with:
gh api repos/<target-repo>/branches/<stacked-base-bookmark>
- If this succeeds, create with
--base "<stacked-base-bookmark>"
- Upstream fallback mode: when stacked base branch is missing from
<target-repo> (common fork flow)
- Do not create the follow-up PR in your fork unless the user explicitly asks
- Create the PR in
<target-repo> against trunk() instead
- Include explicit stack context in the body explaining:
- Which prior PR this depends on (URL)
- That this PR temporarily includes commits from earlier PR(s)
- That after earlier PR(s) merge, the branch will be rebased and merged commits removed, leaving only incremental changes
Draft the PR:
-
Title: Use the primary commit message or user's hint, keep under 70 chars
-
Body: Do NOT hard-wrap prose in the PR body at any specific line length. PR descriptions are rendered as markdown on GitHub, which reflows text automatically. Write full paragraphs as single unwrapped lines. (This is different from commit messages, which should be wrapped at 72 characters.)
When citing prior PR-reviewed changes, prefer PR numbers (#NNNN) or full PR URLs in the PR body because GitHub auto-links these. Use raw commit SHAs only when the exact commit identity matters, such as cherry-picks, reverts, bisect notes, comparison anchors, or upstream commits without a PR.
If template exists, fill it out. Otherwise write a description that prioritizes reviewer context:
- BLUF up top. Open with the bottom-line-up-front headline, a blank line, then 1–3 supporting sentences for the "why" (motivation, gating, scope). Do NOT mush headline and context into one paragraph — the reviewer should grasp the change from the headline alone.
-
Non-stacked PR: a single line — **BLUF:** <one sentence, ≤ 15 words, "what does THIS PR do?">.
-
Stacked PR: two parallel BLUF lines whose labels announce scope — the stack-level bottom line first, then this PR's:
**BLUF (stack):** <one sentence on the whole stack's goal>
**BLUF (this PR):** <one sentence, ≤ 15 words, this PR's specific goal>
The labels exist to tell the reader which BLUF they're looking at (stack vs this PR); the what lives in the sentence, and the tracking id (e.g. PACT-292) goes in the ## Stack section / Context line, not the label. Write the **BLUF (stack):** line ONCE and reuse it verbatim in every PR of the stack — only **BLUF (this PR):** changes. The stack line is shared zoom-out context, not a dependency crutch: the **BLUF (this PR):** line plus the "why" must still read cold (see Standalone reviewability).
- Standalone reviewability. The PR description must read cold. Assume the reviewer has not opened the rest of the stack. Never refer to other PRs in the stack by position ("PR 4", "the next PR"). If a sibling PR's behavior is load-bearing for understanding this one, describe it inline; otherwise drop the reference. Words like "earlier" and "later" are fine as glue ("a later commit clears the matching metadata") but only when the description still reads without them.
### Details — optional, only when needed. When there's non-obvious what/why beyond the BLUF (a gotcha, a deliberate decision, behavior the diff and any code/proto doc-comments don't make obvious), put it under a ### Details heading so it is clearly separated from the BLUF line(s) above. Omit the section entirely when the **BLUF (this PR)** line plus the diff already say everything — e.g. a one-line proto field whose own doc-comment is self-explanatory needs no Details. Never restate file changes; the diff shows them.
- Linkify everything linkable. Any ticket, PR, doc, or runbook that has a URL should be a Markdown link, not bare text. Linear/PACT issues →
https://linear.app/gleanwork/issue/PACT-NNN; Jira escalations (EE-, EN-, etc.) → https://askscio.atlassian.net/browse/<KEY>; sibling PRs → #NNN (auto-links in-repo); go/ shortlinks stay as written. Bare EE-29350 / PACT-292 text is a miss. Ticket links live in the Context/Jira line (or the ## Stack section), keeping the BLUF lines clean.
- Compact metric table if the PR has a measurable outcome (perf, size, quality). One row per case, one aggregate row at the bottom.
- Stack section if stacked (see template below).
- Test/verification note only when there's something non-obvious to share — manual repro steps, known gaps, a deliberate decision not to test something. "Unit tests pass" is not worth saying.
- Include any useful maintainer/reviewer "color" (tradeoffs considered, follow-up work, rollout notes, edge cases, risks, or context from prior discussion).
Length discipline. Default to under 2 KB. GitHub's hard cap on the PR body is ~256 KB but you should be nowhere near it. Anti-patterns to avoid:
- File/test enumeration ("modified X.go, added 5 tests, BUILD.bazel updated") — the diff already shows this.
- Pre/post-condition lists summarizing what changed mechanically.
- "What to spot-check" filler — reviewers know how to read a diff.
- Pasting more than ~50 KB of raw evidence inline (logs, captures, eval outputs) — link to a gist or a local path instead.
The "Churchillian PR" — the one that defends itself against being read by sheer length — is the failure mode. Punchy and skimmable wins.
If stacked (true stacked mode or upstream fallback mode), include a Stack section immediately below the main author-written description, before generated template metadata, checklists, release notes, or tracking sections. In templates that separate reviewer prose from metadata with ---, place ## Stack before that separator. Use a single numbered list with the trunk branch (master/main/etc.) as item #1 in bold, then PRs in dependency order, each as #NNN — <short label>, with a 👉 prefix on the current PR:
## Stack
1. **master**
2. #AAAA — add client_id proto field
3. 👉 #BBBB — populate client_id from JWT claim
Reading top→bottom: PR at slot 2 builds on master, slot 3 builds on slot 2, etc. — direction is unambiguous. Including the trunk as a numbered base resolves "is this stack growing up or down?" confusion that comes up otherwise. Each entry is #NNN — <≤6-word label>: the #NNN auto-links within the repo, and the short label makes the stack skimmable at a glance (preferred over bare-URL cards, which stack into tall blocks you can't scan). The label is a terse description of that PR's change, not its full title. The 👉 prefix marks the current PR — more scannable than a "(this PR)" suffix and survives PR renames without going stale.
Present the draft to the user for approval.
Create the PR:
cat > /tmp/pr-body.md <<'EOF'
<body content>
EOF
gh pr create --repo "<target-repo>" --draft --head "<origin-owner>:<bookmark-name>" --base "<stacked-base-bookmark>" --title "<title>" --body-file /tmp/pr-body.md
gh pr create --repo "<target-repo>" --draft --head "<origin-owner>:<bookmark-name>" --base "<trunk-bookmark-or-branch>" --title "<title>" --body-file /tmp/pr-body.md
C4. Update Stack in Other PRs (stacked PRs only)
After creating the PR, update all other open PRs in the stack so they have the complete stack list.
Only do this in true stacked mode where the PRs live in the same <target-repo>. Skip this step in upstream fallback mode.
- For each other PR in the stack, read its current body via
gh pr view <pr-number> --repo "<target-repo>" --json body
- Replace or insert the
## Stack section immediately below the author-written prose, before generated template metadata, checklists, release notes, or tracking sections. If the template uses --- to separate reviewer prose from metadata, place the stack before that separator, not at the bottom of the body.
- Use the
#NNN — <short label> form for every entry (trunk as bold #1); mark whichever PR is being viewed with the 👉 prefix — not bold, not a "(this PR)" suffix. The **BLUF (stack):** line stays verbatim across all siblings; only **BLUF (this PR):** and the 👉 position differ per PR.
- Update via
gh pr edit <pr-number> --repo "<target-repo>" --body-file /tmp/pr-body.md
C5. Report Success
Show the PR URL and confirm the pull request was created.
C6. Stack composition changes after creation
If you later add, remove, or rename a PR in the stack, update every other PR's Stack section to match. Same flow as C4 — read each body, replace the Stack section, write back via gh pr edit --body-file. Stale stack listings on siblings confuse reviewers.