Open a PR
Opening a PR well is a research problem before a git problem. A reviewer decides in the first screenful whether the author knows the repo — the title grammar, the body shape, the commit subjects all signal it — so the work is: learn what this repo's maintainers write, then write that.
Pick the depth first
Express is for "just open the PR", "just ship it", "get this in" — and for any change small enough that the research would cost more than the review it saves. Orient, write, ship, hand off the watch. Skip the style scout and its card, the voice calibration, and the prose gate. Do not ask whether express is wanted: the phrasing already said so, and confirming it is the delay the user was avoiding.
ccx vcs lane --json
ccx vcs diff
ccx vcs ship --no-watch --pr-title-file "$TITLE" --pr-body-file "$BODY"
The body still says what changed and why, in the repo's template when it has one, and the title still matches the grammar of the last few merged PRs — one gh pr list --state merged --limit 5 --json title read, not a scout. Express drops research, never the body: a bodyless PR is the one thing no lane ships.
Full is the rest of this file, and it earns its cost on a repo you do not own, on a change a maintainer has to be persuaded of, or the first time you open a PR anywhere. On a foreign repo the conformance and politeness passes are not optional even in express — a guest who skips them wastes the maintainer's round trip, not their own.
Orient
The path matters here: three commands whose named output everything downstream references.
ccx vcs lane --json # lane, branch, trunk, dirty, github.mine, downstack PRs and which lack bodies
gh repo view --json isPrivate,owner,viewerPermission,pullRequestTemplates,nameWithOwner,defaultBranchRef
ccx vcs guidelines # CONTRIBUTING, PR template, code of conduct, issue config — prints the card path
Then ccx vcs diff for the change itself.
ccx vcs lane reports the lane — it doesn't choose it, and neither do you. ccx vcs ship selects jj, git, or gt at ship time, and a repo the user doesn't control silently falls off the gt lane, so write nothing that assumes a lane before ship reports one. github.mine is the ownership verdict; the test behind it and everything downstream of mine: false live in reference/oss.md. Read the card ccx vcs guidelines wrote, then separate hard requirements from preferences — reference/guidelines.md has the tells, and the gh api fallbacks for a repo that publishes nothing.
Learn the house style
The house style lives in a card on disk, scouted once per repo and cached:
bash "${CLAUDE_PLUGIN_ROOT}/scripts/pr-cache.sh" status <owner/name> # fresh | stale:<reason> | missing
bash "${CLAUDE_PLUGIN_ROOT}/scripts/pr-cache.sh" path <owner/name> # prints the cache dir
fresh needs no scout — read the card and move on. stale or missing → spawn the scout, once per repo per run:
Agent(subagent_type: "open-pr:pr-style-scout",
prompt: "repo: <owner/name>\ncache: <dir>\nviewer: <gh login>")
The scout samples recent merged PRs, filters out bots, AI-written PRs, and — on a foreign repo — the viewer's own, and writes the card to the cache dir. It returns a path, a short summary, and a confidence — read the card it wrote, not its narration.
The card carries its sample size and a per-axis confidence, because the filters that keep the sample human also shrink it. Title grammar and body structure are conventions — they hold at any sample size. Review-response tone and prose register rest on fewer signals: at confidence: low they're a lean, not a rule, and the PR template and guidelines carry more weight. A convention read off three samples described those three. Cache layout, card schema, and staleness live in reference/style-cache.md.
Calibrate the voice
When the user has a local wlm profile, borrow the parts of their voice that survive the format change:
wlm profile list
wlm -p <profile> stylecard show # -p comes before the subcommand and is not optional
The wlm card was built for long-form blog prose: the Never list and word choice transfer whole, tone at half strength, length and openers not at all. The full transfer table and the no-profile fallback are in reference/voice.md. Where the repo's card and the voice card disagree, the repo wins — you're a guest in their format.
Repos you don't control
github.mine: false changes the register and adds two passes before writing (reference/oss.md):
- The politeness register — direct about the change, deferential about the decision: numbers carry the claims, the maintainer keeps the calls that are theirs.
- Standards conformance — walk
ccx vcs diffagainst the guidelines card and the checklist: scope, style, tests, docs, commit hygiene, diff size against the repo's merged-PR norm. Each miss is a fix, or a sentence in the body owning the deviation. - Issue-first — when the guidelines card marks an issue as a hard requirement, search before opening:
gh issue list --repo <slug> --search "<keywords>" --state all. No match → draft the issue, then put the draft in front of the user withAskUserQuestion— open as drafted / edit first / link an existing issue / skip the requirement deliberately — and act on their pick. Opening an issue publishes under the user's name on someone else's project; drafting doesn't.
Write
Title, body, and commit subjects follow the style card at its stated confidence; the PR template, where one exists, is the body's skeleton. A number wherever an adjective would otherwise grade the change: a reviewer verifies "40ms off cold boot" and discounts "much faster".
Write the body and the title to files, never shell arguments — a body with backticks, $, or a code fence doesn't survive a command line intact, and a title is refused there for less (see Ship):
BODY=$(mktemp)
TITLE=$(mktemp)
The body is prose, so it takes the prose gate before it ships: slop-cop check "$BODY" --lang=markdown, triaging the flags rather than accepting all of them.
Commit messages get the same treatment: the card records this repo's subject grammar, its length, and which trailers its own merged commits carry — match those trailers and no others. On a repo whose history has no Co-Authored-By: Claude or Generated with Claude Code, a trailer announcing the tooling is the single line that makes a careful PR read as automated, and maintainers filter on it; on a repo whose history does carry them, keeping them is the convention.
One stack per session
Related work from one session goes in one chained stack, never independent siblings off trunk — even when the diffs are provably disjoint. "The files don't overlap" is not a reason to split: siblings erase the review order a chain states, and forfeit the mid-stack machinery — one restack carries an amend through every branch above it, where siblings each rebase alone. Chain in dependency order, what consumes above what it consumes; a lane still in flight goes at the tip, so the finished branches below it never get re-rebased under its churn. Mechanics in reference/stack.md.
A PR is a feature-sized unit of work. Follow-ups, fixes, and review
responses are commits on the open PR, never a stacked child. Ship them with
ccx vcs ship, adding --amend to fold into its commit or --tip-only
when only a stack's tip changed. Stack only when a parent must land before
other work can proceed, or when review needs the split. Push at coherent
checkpoints, never per edit, because each push supersedes the running
build and reruns CI and the review bots. Still push within the hour,
since origin is the backup.
Ship
The flag combination is load-bearing:
ccx vcs ship --no-watch --pr-title-file "$TITLE" --pr-body-file "$BODY"
Write the title to a one-line file beside the body file and pass --pr-title-file, never an inline --pr-title. A worktree-isolated agent's Bash sandbox refuses any argument that pairs a quote with the word git or a shell name, such as GitHub's ... git, and a title in a file never reaches the command line. Ship takes the commit subject from that title and the commit body from the PR body, so -m is needed only for a subject that differs from the title.
--no-watch because ship's built-in watch blocks until CI concludes — right when you're waiting, wrong when the watch is about to be handed off. Ship owns PR create-and-update in every lane: a hand-rolled gh pr create alongside it produces a PR the gt lane cannot reconcile with its own stack.
On a PR that is already open, ship warns and keeps the title and body it has, so an amendment carries neither flag, and a PR whose text should change takes --replace-pr-text.
On the gt lane one submit can create several PRs, and every PR in the submit that has no body yet gets one. --pr-title-file and --pr-body-file repeat and scope by branch (--pr-body-file <branch>=<path>; a bare value applies to the tip); ccx vcs lane --json is what tells you which downstack PRs are bodyless.
A refusal is information, not a retry: nothing to commit means a prior ship already landed this; ship replays a gt needs_restack itself. When ship still refuses because a branch is off its parent or trunk moved, run ccx vcs stack submit (or hand the refusal verbatim to the stack's owner when another lane owns it), then re-run ship.
Ship reports the branch, the PR number and URL, and the head sha. The handoff needs all four.
Hand off the watch
Resolve ${CLAUDE_PLUGIN_ROOT} to its absolute path in the caller at spawn
time and fill in poll: with the ready-to-run command. The variable is
substituted in this skill's text; the subagent does not receive it. Pass the
sentence after poll: verbatim so the watcher can arm the script even if
the harness omits its agent definition.
Agent(subagent_type: "open-pr:pr-watcher", run_in_background: true,
name: "pr-watch-<number>",
prompt: "pr: <number>\nurl: <url>\nrepo: <owner/name>\nhead: <sha>\n"
"branch: <branch>\nlane: <gt|jj|git>\ncache: <dir>\nownership: <mine|foreign>\n"
"poll: bash \"<abs plugin root>/scripts/pr-poll.sh\" <owner/name> <number> <cache>/pr/<number>.json\n"
"Run PR_POLL_WINDOW=40 <poll: command> alone as foreground Bash with timeout: 60000, rerun it at once on DONE window-elapsed, and keep calling until a final verdict: never arm a Monitor or background the call, since neither wakes a subagent, never wrap it in a loop, and never substitute a hand-rolled gh pr checks/bk loop, which cannot see a queue landing. On DONE ready-to-merge, SendMessage main before anything else.")
After spawning, verify that the poll script wrote its state file:
bash "${CLAUDE_PLUGIN_ROOT}/scripts/pr-watch-armed.sh" "<cache>/pr/<number>.json"
On exit 1, SendMessage the watcher the same poll: line again and run
the check a second time. If it fails again, report to the user that the
watch is unarmed. An agent spawn alone does not prove the script is running.
One watcher per PR — a second on the same PR would push over the first. Every report except evicted ends the watch; SendMessage by name resumes it for the next round. An evicted: <reason> <detail> message arrives mid-run and the watcher keeps watching for the caller to re-enqueue by label or in Graphite's UI. Push any fix before re-enqueueing; the caller decides when to re-enqueue.
After a ready-to-merge report: open-pr.on-ready
A ready-to-merge report means the PR is green, mergeable, and approved:
every check passed, every GitHub Actions check suite on the head has
completed, mergeable is true, queue state was read cleanly (never
QUEUE unknown), and the PR is neither queued nor evicted on its current head.
No reviewer's latest review is CHANGES_REQUESTED, and REST mergeable_state
is not blocked (the state for a missing required approval). A
ready-to-merge downstack #<n> report is a stacked PR waiting on downstack PR
# (including when Graphite's mergeability check is the only unfinished check
on a legacy Graphite stack). The PR itself is ready, but it lands only with
its downstack: name # in the question and enqueue the stack, never this
PR alone.
Read the user's CLAUDE.md for one of these exact lines:
open-pr.on-ready: offer-open(the default when no line is set): in that same turn, ask the user throughAskUserQuestionwhether to open the PR page. Rungh pr view <number> --webonly on a yes.open-pr.on-ready: offer-merge: in that same turn, ask "merge # now?" throughAskUserQuestion. Add the repo's queue label only on a yes.
Never label or open on the report alone. Under the long-running skill,
forward the report to the landing desk; it owns the decision regardless of
the setting.
After adding the queue label to a PR whose watcher already reported ready-to-merge, resume that watcher by name so it watches the queue through green checks to a landing or a failure:
SendMessage(to: "pr-watch-<number>", message: "Queue label <label> added. Resume the queue watch on the same state file.")
Its report arrives as a notification mid-task — keep working until it does. When it reports a judgment call, it brings 2–4 options: route them to the user through AskUserQuestion, because the watcher runs with no user attached — that's exactly why it hands the question to you.
Offer the review in cc-review
Once the watcher is armed and you've reported the PR URL, check whether the user has cc-review:
command -v cc-review >/dev/null ||
jq -e '.enabledPlugins["cc-review@cc-review"] == true' "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json" >/dev/null
On exit 0, ask "Review # in cc-review?" through AskUserQuestion in that same turn. On a yes, invoke the cc-review:start skill with --pr <url> --open: its first step runs cc-review start --pr <url> --open, which loads the PR's stack and opens the review in the browser, and the rest of that skill drives the event loop. On a no, or when the check fails, skip it. Ask once per PR, never again on a re-ship. A lane under the long-running skill has no user to ask, so it skips the offer.
<success_criteria> The PR is open when ship has reported branch, PR number, URL, and head sha, and exactly one watcher holds the watch. It's done well when the title, body, and commit subjects match the style card at its stated confidence; every hard requirement in the guidelines card has a disposition — met, or skipped by the user's explicit pick; the body fills the repo's template and passed a triaged slop-cop run; and on a foreign repo, the diff cleared the conformance pass before the body was written. </success_criteria>
Reference
- reference/guidelines.md — the guidelines card: what it aggregates, hard requirement vs preference,
gh apifallbacks. - reference/style-cache.md — cache layout,
style.mdfrontmatter schema, staleness rules. - reference/voice.md — the wlm transfer table and the no-profile fallback.
- reference/oss.md — ownership matrix, politeness register, CLA/DCO, the conformance checklist.
- reference/stack.md — one stack per session: chain order, sibling costs, mid-stack amend mechanics.
- reference/merge-state.md — merged vs abandoned on a queue lane: the closer-actor recipe and the trunk-history confirm.
- reference/worktrees.md — worktree isolation for parallel PR lanes.
- reference/rebase.md — restack and rebase mechanics: conflicts and keeping the stack current.
- reference/checks.md — reading check state: buckets, required vs optional, rerun semantics.