Installs into .claude/skills of the current project.
Are you the author of Pr Ship?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/tstapler-pr-ship)
---
description: Autonomously iterate on a PR — local CI, code review, remote CI, review comments, merge conflicts — until ready to merge
prompt: |
# PR Ship Loop — Make It Ready to Merge
Drive PR `${1:-$(gh pr list --head $(git branch --show-current) --json number --jq '.[0].number')}` to a mergeable state by iterating through five ordered gates until all are green.
## State File
All progress is tracked in `/tmp/pr-ship-${REPO_SLUG}-${BRANCH_SLUG}-${PR}.md`. Read it at the start of every iteration to understand what's already done. Update it after every action. This is your working memory across loop iterations and polling-agent wakeups.
**State file format** (initialize if missing):
```markdown
# PR Ship State — PR #N
Iteration: 0
Branch: <branch>
Base: <base-branch>
## Changed Files
(populated once from `gh pr diff "$PR" --name-only`)
- path/to/file.go
## Gate Status
- [ ] Gate 1a: Local compile
- [ ] Gate 1b: Local tests (changed packages only)
- [ ] Gate 2: Code review clean
- [ ] Gate 3: PR review comments addressed
- [ ] Gate 4: Remote CI green
- [ ] Gate 5: No merge conflicts
## Push History
(append after each push: `- COMMIT_SHA pushed at ITERATION N`)
## Code Review Issues
### Open
(populated after Gate 2 runs; format: `- [ ] FILE:LINE — DESCRIPTION [severity]`)
### Resolved
(moved here when fixed; format: `- [x] FILE:LINE — DESCRIPTION [fixed in COMMIT]`)
## Decision Log
(one line per iteration: what gate advanced, what was done, commits made)
```
## Entry Check
```bash
PR="${1:-$(gh pr list --head $(git branch --show-current) --json number --jq '.[0].number')}"
REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' | tr '/' '-')
BRANCH=$(gh pr view "$PR" --json headRefName --jq '.headRefName' | tr '/' '-' | tr '_' '-' | cut -c1-40)
STATE="/tmp/pr-ship-${REPO}-${BRANCH}-${PR}.md"
gh pr view "$PR" --json number,title,state,mergeable,mergeStateStatus,headRefName,baseRefName
```
If the PR is already merged or closed, report that and stop.
Read the state file. If it doesn't exist, initialize it. Populate **Changed Files** once using:
```bash
gh pr diff "$PR" --name-only
```
Never re-derive the changed files list from scratch — use what's in the state file.
---
## Linked-Issue Closing Keyword Check
Run once per entry, before Gate 1a — cheap and idempotent, so it's safe to re-run on every wakeup.
GitHub only auto-closes an issue when the PR body contains a **closing keyword** immediately before the reference — `Closes #N`, `Fixes #N`, `Resolves #N` (also accepts `close/closed`, `fix/fixed`, `resolve/resolved`). A bare `#N` mention elsewhere in the body, or a reference only in a commit message, does **not** trigger auto-close on merge.
```bash
BODY=$(gh pr view "$PR" --json body --jq '.body')
echo "$BODY" | grep -qiE '\b(clos(e|es|ed)|fix(e|es|ed)?|resolv(e|es|ed))[[:space:]]+#[0-9]+'
```
- **Keyword already present**: nothing to do, continue to Gate 1a.
- **No closing keyword found**: scan the body for bare `#[0-9]+` issue references (exclude anything already matched by the grep above). If exactly **one** distinct issue number is referenced anywhere in the body — e.g. the PR was opened to fix that issue but the literal `Closes #N` line was dropped, edited out, or never added — append a `Closes #<N>` line to the body:
```bash
gh pr edit "$PR" --body "$(printf '%s\n\nCloses #%s' "$BODY" "$N")"
```
Log the addition in the Decision Log with the issue number.
- **Zero or multiple** distinct issue numbers referenced with no keyword: do not guess which one this PR closes. Leave the body alone and note in the Decision Log that the PR isn't unambiguously linked to exactly one issue, so no auto-close keyword was added — surface this to the user rather than silently skipping if you're reporting a final status.
- Never invent an issue number that isn't already referenced somewhere in the PR body.
---
## Context Discipline — Orchestrator Only
**This skill is an orchestrator, not a worker.** Delegate all file editing, compiling, and committing to fresh subagents. Never accumulate file contents or diffs in this context — only gate status and state file updates. This is the `lean-agent-loop` skill pattern: the state file is the coordinator's memory, each fresh subagent is a Ralph Wiggum agent, and the five gates are the loop condition.
```
Orchestrator (this context)
└─ reads state file, checks gate, collects failure details
└─ spawns Agent(prompt="Fix these specific failures: ...") → waits for result
└─ fresh agent does all file reading, editing, testing, committing, pushing
```
After each agent returns: verify the fix locally (run the relevant check command), update the state file, append to Decision Log, then advance to the next gate.
---
## The Loop (max 10 iterations; stop if no gate advances)
At the top of each iteration, increment `Iteration:` in the state file. Read Gate Status. Skip gates already marked `[x]`. Process gates in order — do not jump ahead.
### Gate 1a — Local Compile
Only run if `[ ]`. Detect stack from repo contents, then compile:
| Stack | Compile check |
|-------|--------------|
| Java/Maven | `./mvnw compile -q` |
| Kotlin/KMP | `./gradlew compileTestKotlinJvm --no-daemon` |
| Go | `go build ./...` |
| TypeScript | `npx tsc --noEmit` |
| JS | `npm run build` |
If it fails: delegate to a fresh agent with the exact error and the repo path. After agent returns, re-run the same compile command to verify — do not mark `[x]` until you confirm it passes. Update state file.
### Gate 1b — Local Tests (changed packages only)
Only run if `[ ]` and Gate 1a is `[x]`. Scope tests to only the packages/modules containing changed files:
| Stack | Scoped test command |
|-------|-------------------|
| Go | `go test $(git diff --name-only origin/<base>...HEAD \| grep '\.go$' \| xargs -I{} dirname {} \| sort -u \| sed 's|^|./|' \| tr '\n' ' ')` |
| Java/Maven | `./mvnw test -pl $(changed modules) -q` |
| TypeScript | `npm test -- --testPathPattern="<changed files>"` |
| Kotlin/KMP | `./gradlew jvmTest --no-daemon` |
If a scoped command is too complex to derive, fall back to the full test suite. If it fails: delegate to a fresh agent with failing test output. After agent returns, re-run the failing tests to verify — not the full suite, just the specific failing ones. Mark `[x]` only after the re-run passes.
**Good Samaritan rule**: fix any failing or flaky test you encounter here, even if it's unrelated to this PR's changes and not this PR's fault. Leaving a known-broken or flaky test for someone else to hit isn't shipping — include it in the same delegated fix, and log it separately in the Decision Log as a pre-existing failure fixed in passing.
### Gate 2 — Code Review (changed files only)
Only run if `[ ]` and Gates 1a+1b are `[x]`.
Invoke the code review skill on the current diff (it naturally scopes to changed files):
```
/code:review --fix
```
Parse the findings. Write all **BLOCKER** and **CRITICAL** issues to `## Code Review Issues → Open` in the state file. Write **MAJOR** issues too. Suggestions/NITs are optional — log them but don't block.
Delegate a fresh agent to fix all BLOCKER/CRITICAL/MAJOR issues. Include in the agent prompt:
- The state file path
- Each open issue with file, line, severity, description
- Instruction to commit (but NOT push — Gate 3 handles the push decision)
After the agent returns:
1. Re-run `/code:review` to verify no new BLOCKER/CRITICAL issues
2. Move fixed issues to `Resolved` in the state file with the commit SHA
3. Repeat until no BLOCKER/CRITICAL/MAJOR issues remain
4. Mark `[x]` in state file
### Gate 3 — PR Review Comments
Only run if `[ ]` and Gates 1a+1b+2 are `[x]`. Address all reviewer feedback **before** pushing so CI runs on code reviewers have already seen and haven't flagged.
#### Copilot Review Check (run first)
Before processing any threads, check whether Copilot was requested as a reviewer:
```bash
REPO_NWO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner')
OWNER="${REPO_NWO%%/*}"; REPO_NAME="${REPO_NWO##*/}"
# Is Copilot a requested reviewer?
COPILOT_REQUESTED=$(gh pr view "$PR" --json reviewRequests \
--jq '[.reviewRequests[] | (.login // .name // "")] | map(select(test("copilot";"i"))) | length > 0')
```
If `COPILOT_REQUESTED == true`:
```bash
# Has Copilot already posted a review?
COPILOT_REVIEWED=$(gh pr view "$PR" --json reviews \
--jq '[.reviews[] | select(.author.login | test("copilot";"i"))] | length > 0')
# Has Copilot posted a rate-limit / skip comment?
COPILOT_RATE_LIMITED=$(gh api "repos/$OWNER/$REPO_NAME/issues/$PR/comments" \
--jq '[.[] | select(.user.login | test("copilot";"i")) | .body] | map(select(test("rate.limit|quota|unavailable|temporarily|skip|unable|error";"i"))) | length > 0')
```
Decision:
- **`COPILOT_REVIEWED == true`** → include Copilot's review comments in Gate 3 processing below (treat like any other reviewer).
- **`COPILOT_REVIEWED == false` + `COPILOT_RATE_LIMITED == true`** → log "Copilot rate-limited — skipping Copilot review" in Decision Log and proceed to thread processing.
- **`COPILOT_REVIEWED == false` + `COPILOT_RATE_LIMITED == false`** → Copilot review is still pending. Dispatch the Copilot-wait polling agent (see **Background Polling** below) and stop — do **not** advance Gate 3, do **not** `ScheduleWakeup`. Log "Waiting for Copilot review — polling agent dispatched" in Decision Log.
#### Thread Processing
Use the `github-address-pr-comments` skill (`~/.claude/skills/github-address-pr-comments/SKILL.md`) to:
1. Fetch all unresolved threads (single GraphQL call) — via the shared `~/.claude/scripts/pr-threads.py fetch` script, the same one Gate 4's staleness re-check and `/code:review` use, so all three stay in sync on one aggregation implementation instead of drifting.
2. For each thread: fix/decline/defer per decision rules below
3. Reply + resolve each thread
4. Commit any code changes locally (do NOT push yet — push happens in Gate 4)
Decision rules (encode in every delegated agent prompt):
- **Fix**: bugs, logic errors, security, clarity, naming, missing tests, valid perf issues
- **Also fix**: cosmetic/style if small and clearly correct
- **Defer**: valid-but-large refactors — reply "Deferring to follow-up — too broad for this PR"
- **Decline**: only if factually wrong or contradicts a documented design decision
- **CHANGES_REQUESTED**: treat every item as blocking
Mark `[x]` when all threads are resolved or explicitly declined with a reply. Record the check time in the state file as `Gate 3 last verified: <ISO8601 timestamp of this check>` — Gate 4 diffs against this, not a guess, to decide if anything new landed.
### Gate 4 — Remote CI
Only run if `[ ]` and Gates 1a+1b+2+3 are `[x]`. Push all local commits now:
```bash
git push origin HEAD
# append to Push History in state file: "- <sha> pushed at iteration N"
```
Then check:
```bash
gh pr checks "$PR" --watch=false
```
- **Pending/in_progress**: dispatch the CI-wait polling agent (see **Background Polling** below) and stop — do not `ScheduleWakeup`. Do not mark gate.
- **All success**: re-run the Gate 3 staleness check using the **same shared script** `github-address-pr-comments` uses for thread fetching — this is the fix for a real incident where a bot comment landed after Gate 3's last check and the loop never re-polled GitHub because it trusted a stale "all green" state file:
```bash
python3 ~/.claude/scripts/pr-threads.py summary \
--owner "$OWNER" --repo "$REPO_NAME" --pr "$PR" \
--since "<Gate 3 last verified timestamp from state file>" \
[--hostname <enterprise-host-if-applicable>]
```
Read `new_since_count` from the JSON output — do not eyeball `unresolved_count` alone, since a thread can be unresolved-but-already-known. If `new_since_count > 0`, reset Gate 3 to `[ ]`, log the new thread count in Decision Log, and loop. If `new_since_count == 0`, mark Gate 4 `[x]`.
- **Failing**: collect the logs:
```bash
gh run list --branch $(git branch --show-current) \
--json databaseId,name,status,conclusion \
--jq '.[] | select(.conclusion == "failure") | .databaseId'
gh run view <RUN_ID> --log-failed
```
Delegate to a fresh agent with the exact error lines. Apply the same Good Samaritan rule as Gate 1b: fix flaky or pre-existing CI failures too, not just failures caused by this PR's diff. Agent commits locally. Then re-run Gate 3 (address any new comments), then push again and dispatch a fresh CI-wait polling agent (see **Background Polling** below).
### Gate 5 — Merge Conflicts
Only run if `[ ]` and Gate 4 is `[x]`.
```bash
gh pr view "$PR" --json mergeable,mergeStateStatus
```
If `mergeable == "CONFLICTING"`: delegate to a fresh agent to fetch + merge base, resolve conflicts, commit, and push. Mark `[x]` when `mergeable == "MERGEABLE"`.
---
## Progress Check
After each gate transitions `[ ]` → `[x]`, append to Decision Log:
```
Iteration N — Gate X: <what was done, commits made, issues found/fixed count>
```
If an entire iteration completes with **zero gates newly marked `[x]`** (no progress), stop and report:
- Which gates are still open
- What was attempted
- What's blocking (with exact error or status)
---
## Exit Condition
When all five gates are `[x]`, report:
```markdown
## PR #N is Ready to Merge
- Gate 1a Local compile: green
- Gate 1b Local tests: green
- Gate 2 Code review: N issues fixed, N deferred, N declined
- Gate 3 PR review comments: N threads resolved
- Gate 4 Remote CI: all checks green
- Gate 5 Merge conflicts: none
Decision log:
<paste from state file>
Merge with: gh pr merge N --squash --delete-branch
```
Do NOT merge automatically — leave the final merge to the user.
---
## Background Polling — Cheap Model, Fresh Context
Two waits in this loop are pure status polling with zero reasoning required: Gate 3's Copilot-review wait and Gate 4's remote-CI wait. **Never use `ScheduleWakeup` for either** — it re-wakes *this* session, on *this* session's model, carrying the full accumulated gate-loop context, just to run `gh pr checks` or check for a comment. That's expensive and slow for no reason. Instead, dispatch a plain `Agent` call — no `subagent_type: "fork"` (a fork inherits this session's full context, which is exactly what polling doesn't need) — with `model: "haiku"`, the cheapest and fastest model available, since nothing here requires reasoning. End this turn; the agent's completion notification resumes the loop.
This is also strictly better than `ScheduleWakeup` on its own terms: `ScheduleWakeup` always re-enters by re-running `/github:pr-ship <PR_NUMBER>` from scratch (re-reading the state file, re-deriving the repo/branch slug), where a polling agent instead reports a verdict directly into this same conversation — one less round trip, and the only context cost is the agent's short report.
**Polling agent prompt template** (fill in the condition — keep it this terse, the agent doesn't need skill context, gate history, or the state file):
```
Repo: <owner>/<repo>. PR: <PR number>.
Poll <condition command> on a loop until it resolves or <timeout> elapses, sleeping between
attempts: < 2 min elapsed -> 90s; 2-10 min -> 270s; > 10 min -> 600s.
Report back in under 80 words: the final state (resolved-pass / resolved-fail / timed-out)
and, if failed, which check(s) and a one-line reason from the log. No narrative, no
suggested fixes, no restating these instructions — just the verdict.
```
- **Gate 3 Copilot wait** — condition: re-run the `COPILOT_REVIEWED`/`COPILOT_RATE_LIMITED` checks above until one flips true. Timeout: 15 minutes (Copilot review assignment is normally fast; past that, report timed-out so the orchestrator can log it as a MAJOR and proceed rather than blocking indefinitely).
- **Gate 4 CI wait** — condition: `gh pr checks "$PR" --watch=false` exits something other than `8` (`0` = all passed, `1` = a check failed). Timeout: 90 minutes — Bazel/Android CI can legitimately run that long; don't shorten this just to report back sooner.
When the polling agent's report arrives, resume the gate directly from its verdict — don't re-run the check yourself first. Only fall back to a direct check if the agent's report is ambiguous or incomplete.
---
## Never
- Push code before Gates 1a and 1b are `[x]`
- Push code before Gate 2 (code review) is `[x]`
- Push code before Gate 3 (PR comments) is `[x]` — reviewers' feedback must be addressed first
- Force-push over others' commits without asking
- Merge the PR automatically
- Skip `--no-verify` or bypass hooks
- Re-derive changed files — always use the state file's list
---
**Usage**: `/github:pr-ship` (current branch) or `/github:pr-ship 61`
Gates run in order: local compile → local tests (scoped) → code review → **PR comments** → remote CI → merge conflicts. Push only happens at Gate 4, after all local work and reviewer feedback is incorporated. After CI passes, re-check for new comments before marking done. State tracked in `/tmp/pr-ship-{repo}-{branch}-{PR}.md`.