Opening a pull request
Three steps that always travel together: push the branch → gh pr create → announce
it in the Review Requests chat. The team picks review work out of that chat, not off
GitHub — a PR nobody announced is a PR nobody reviews. Stopping after step 2 is the
most common way this goes wrong.
The one exception is a draft PR: it is not ready for review, so it is not
announced. The announcement happens later, once, when the draft is marked ready — and
marking it ready is itself a two-step job: gh pr ready <n> → announce. Doing the first
without the second is the draft-flavoured way of stopping after step 2.
Marking a draft ready
When the ask is to mark an existing draft ready rather than to open a PR, skip the
preconditions and steps 1–3: the branch was already pushed and the PR already exists.
gh pr list --head "$(git branch --show-current)" --json number,url,isDraft
gh pr ready <n>
Then open it in the browser if the machine asks for it (see step 3), and run step 4 — this
is the PR's one and only post. If the PR is not a draft, it was
announced when it was created; say so and do not post again.
Invoking this skill is the permission to push
The standing rule everywhere else is: never git push unless the user asked in that
message. Asking for a PR is that ask — push and create without stopping to
re-confirm. It does not authorize a force-push over rewritten history; that belongs
to /prepare-merge.
Preconditions
| Check | Command | If it fails |
|---|
On a feature branch, not dev/master/release/* | git branch --show-current | Stop. Ask what to branch. |
| Working tree clean | git status --short | Commit or stash first — never PR a dirty tree. |
| Branch has commits over the base | git log --oneline origin/dev..HEAD | Nothing to PR. Stop. |
| No PR already open for it | gh pr list --head "$(git branch --show-current)" --json url,state,isDraft | One exists → do not open a second, and do not re-announce it. The exception: a draft the user wants ready now → gh pr ready <n>, then step 4 (it was never announced). Use pr list, not pr view — pr view errors on a branch with no upstream. |
| Build/tests actually run | — | You may still PR, but the Testing section must say plainly what was not run. |
| Branch is linked to its issue | git config --get "branch.$(git branch --show-current).issue" | Empty → ask once, via AskUserQuestion: run /track-issue now, or open the PR without an issue. Never invent a number from the branch name. |
Branch names follow the commit prefixes: feat/…, fix/…, refactor/… — never
feature/….
1. Offer /prepare-merge first
If the branch carries a fix trail — review-comment commits, "wip", several commits on
one concern, added docs/superpowers/ plans — ask once, via AskUserQuestion, whether
to run /prepare-merge before the PR. It rebases onto origin/dev, regroups the
commits, and strips the working docs. Ask; don't run it unprompted, and don't insist if
they decline.
If they accept: /prepare-merge ends by pushing the branch itself (a -bak ref,
then a --force-with-lease). Skip step 2 entirely when you come back and go straight
to step 3 — re-running the plain push would fail or fight the force-push.
If they decline and the branch adds files under docs/superpowers/, say so in one
line: those are working documents and they are about to land on dev for good. Offer
to drop them in a final chore: drop the branch's working docs commit. Their call —
do not delete anything unasked.
2. Push
git push -u origin "$(git branch --show-current)"
Never --force here. If the push is rejected as non-fast-forward, stop and report it —
that is someone else's work on the same branch.
3. Create the PR
Write the body to a file under the project's tmp/, never to the repo root, and never
pass a multi-KB body inline:
gh pr create --base dev --title "type(scope): summary" --body-file tmp/pr-body.md --assignee @me
-
Draft only when the user asked for one — add --draft. It decides step 4, so
check the result with gh pr view <n> --json isDraft rather than assuming.
-
Always assign the PR to its author (--assignee @me). Verify afterwards with
gh pr view <n> --json assignees: gh pr edit --add-assignee can fail silently on a
GraphQL deprecation error, in which case fall back to the REST call
gh api -X POST repos/<owner>/<repo>/issues/<n>/assignees -f 'assignees[]=<login>'.
-
Base is the repo's default branch (dev on actual-chat) — confirm with
gh repo view --json defaultBranchRef rather than assuming.
-
Title describes the whole branch, as type(scope): summary. One commit (or a
branch /prepare-merge just collapsed to one) → use its subject verbatim. Several
commits → write the line the squashed commit would carry; do not just lift the
biggest one, and never --fill, which staples subjects together.
-
Body shape: ## Summary (what was broken and why — the mechanism, not a diff
restatement), ## Fix (what the change does, and the invariants a reviewer must not
break), ## Testing (what ran, green or not, and what is still owed — device passes,
manual verification). Then a line Closes #<N> with the issue from the preconditions
check — that is what moves the issue to Done on merge — and your harness's
attribution footer, if it gives you one. No linked issue → no Closes line.
-
Issue still in ToDo? The PR is proof the work is in progress. Check and move it:
gh api graphql -F n=<N> -f query='query($n:Int!){ repository(owner:"Actual-Chat",name:"actual-chat"){ issue(number:$n){
projectItems(first:5){ nodes{ id fieldValueByName(name:"Status"){ ... on ProjectV2ItemFieldSingleSelectValue{ name } } } } } } }'
gh project item-edit --project-id PVT_kwDOBSwBWs4AA3Ej --id <ITEM> \
--field-id PVTSSF_lADOBSwBWs4AA3EjzgAc2xg --single-select-option-id 47fc9ee4 # In Progress
Only ToDo (or Backlog) moves; In Progress and Done are left alone.
-
Say plainly what was not verified. "Android device-verified; iOS compiles, device
test still owed" is the useful sentence.
Open it in the browser — a per-machine preference
Run this right after the PR is created, and after gh pr ready <n> when marking a draft
ready. Skip it entirely when AC_OS is Linux in Docker — there is no browser to open.
open_pr="${AC_PR_OPEN_IN_BROWSER:-$(jq -r '.env.AC_PR_OPEN_IN_BROWSER // empty' ~/.claude/settings.json 2>/dev/null)}"
$open_pr | Do |
|---|
1 | gh pr view <n> --web |
0 | Nothing. |
| empty | The developer has never been asked on this machine. Ask once, via AskUserQuestion: open PRs in the browser after creating them — always, or never? Act on the answer for this PR, then save it so the question never comes back. |
Saving the answer ("1" or "0") — it goes into the user's own Claude settings, never
into the repo's:
f=~/.claude/settings.json; [ -f "$f" ] || echo '{}' > "$f"
jq '.env = ((.env // {}) + {AC_PR_OPEN_IN_BROWSER: "1"})' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
The harness reads env at session start, which is why the check above falls back to the
file: a second PR in the same session must not ask again. If the file cannot be written,
say so in one line and move on. A failure anywhere in this step is not a reason to stop:
carry on to step 4.
4. Announce in Review Requests — every non-draft PR
Draft PR → skip this step. Tell the user the draft is open and unannounced, and
that it gets announced when it is marked ready. When that happens — gh pr ready <n>,
in this session or a later one — run this step then, as the PR's one and only post.
Otherwise post once, via mcp__voxt-robokitty__post_message, to the Review Requests chat
(https://voxt.ai/chat/s-pmMsV1UVKG-gz3ymbh6n3). Pass s-pmMsV1UVKG-gz3ymbh6n3 straight
as chatId: it is the chat's own id, so there is no place to look it up in
(list_place_chats on it fails). If the RoboKitty MCP is not wired up in your
setup, stop and tell the user the PR is open but unannounced — do not treat the PR as
done.
The announcement is the title and the link, nothing more:
PR #4494 — fix(threads): notifications, Threads-tab unread badge, mention picker and back button
https://github.com/Actual-Chat/actual-chat/pull/4494
A reviewer decides from the title whether to open the PR; everything else lives in its
body. Add at most one short line under the URL, and only when a reviewer would
otherwise miss something that matters before merge — a changed contract, an open
question for the team, the one path the fix targets that nobody has run:
PR #4495 — fix(pooling): stop SharedResourcePool.Rent from retrying a failed factory forever
https://github.com/Actual-Chat/actual-chat/pull/4495
Callers now get the factory error instead of a silent retry.
No mechanism, no file list, no test counts — that is the PR's Summary/Fix/Testing, and
restating it in chat is exactly what the team asked to stop: a long post gets skimmed,
and its first sentence can read as a change nobody agreed to.
One post per PR, ever. No "added a second commit", no revised summaries, no
re-posting after a push. The chat is a review queue, not a changelog — a follow-up post
pushes other people's requests down and tells a reviewer nothing they wouldn't see on
the PR. New commits on an announced PR get mentioned in the session, to the user, not
to the chat.
Release announcements are a different thing entirely — Releases chat, and they need
their own confirmation. Never fold one into this step.
Common mistakes
| Mistake | Fix |
|---|
| Non-draft PR created, chat never posted | Step 4 is part of the deliverable, not a follow-up |
| Draft announced in the chat | Drafts wait; announce once, after gh pr ready |
| Draft marked ready, chat never posted | gh pr ready is half the job; step 4 is the other half |
"Mark it ready" handled as a bare gh pr ready | That request is this skill too — see Marking a draft ready |
| Re-announcing after new commits | One post per PR; tell the user instead |
| Announcement retells the PR body | Title + link; one extra line at most |
gh pr create --fill | Write a real Summary/Fix/Testing body |
| PR left unassigned | --assignee @me, then verify with gh pr view --json assignees |
| Body file in the repo root | Put it in tmp/ |
| Base branch assumed | gh repo view --json defaultBranchRef |
Closes #N guessed from the branch name | Only branch.<name>.issue counts; otherwise ask about /track-issue |
| Second PR opened for a branch that already has one | gh pr list --head first |
| "All tests pass" without running them | State what ran and what is owed |
Pushing again after /prepare-merge | It already pushed; skip step 2 |
gh pr view on a never-pushed branch | It errors; use gh pr list --head |
| Force-pushing to land the PR | That is /prepare-merge, and it asks first |