Skip to content
Back to skills

Pr Ship

ASecurity

Autonomously iterate on a PR — local CI, code review, remote CI, review comments, merge conflicts — until ready to merge

  • 8 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added September 20, 2026
developmenttypescriptpythonrustgojavakotlinbashtestinggitapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 5, 2026

npx -y skills add tstapler/dotfiles --skill pr-ship --agent claude-code

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.

Security grade badge for Pr Ship
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tstapler-pr-ship/badge)](https://www.skillsdirectory.com/skills/tstapler-pr-ship)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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`.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…