Imported from okisdev/ai-reference (
skills/ship-pr/SKILL.md). Install upstream withnpx skills add okisdev/ai-reference --skill ship-pr. Copyright stays with the author.
Context
Repository: !gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || echo "unknown"
Default branch: !git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@refs/remotes/origin/@@' || echo main
Current branch: !git branch --show-current
Existing PR: !gh pr view --json number,url -q '"#" + (.number|tostring) + " " + .url' 2>/dev/null || echo "(none)"
My login: !gh api user --jq .login 2>/dev/null || echo "unknown"
Changesets: !test -d .changeset && echo "yes (.changeset/)" || echo "(none)"
Uncommitted changes
!git status --short
Commits ahead of base
!git log $(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@refs/remotes/origin/@@' || echo main)..HEAD --oneline 2>/dev/null || echo "(none or base not found)"
Instructions
Drive the change to a merged PR: implement if needed, branch, open the PR, then monitor CI and reviews until the merge gate passes, addressing feedback each cycle. Execute immediately without asking.
Delegate branch/commit/PR to their named skills (create-branch, commit-changes, create-pr); the should-this-merge judgment to verify-pr; review-thread triage to verify-pr-comments. Own only the monitor loop, the merge gate, and the merge. Invoke the others by name and act on their output; never inline or reimplement their logic.
Auxiliary skills, used only when relevant:
- verify-issue: optional upstream precursor when the change originates from an issue.
- organize-commits: optional pre-PR tidy, kept out of the unattended loop because it pauses for confirmation.
- summarize-review: when shepherding a contributor's PR rather than ship-pr's own change.
- approve-pr: on another author's PR, to post the maintainer approval near the merge gate (condition (3)); it owns the self-approval guard and the
gh pr reviewwrite.
Process
-
Detect state and resume at the right step (adaptive entry). Read the Context probes and resume at the first matching row. The leading positional of
$ARGUMENTSis a change description, a PR number/URL to resume, or empty (operate on the current branch). Flag semantics that are not obvious from their names:--advisory <check>excludes a named check from the wait set (repeatable) but cannot mark a human reviewer skippable, and is never inferred from the required-set membership of a check or from a bot login;--draftis a hold short of merge, not a skip; the merge method (--squashdefault,--merge,--rebase) is validated early againstallowed_merge_methods(step 3);--adminis ownership-scoped to ship-pr's own PRs (see the Merge gate); on another author's PR the approval path in condition (3) runs by default, with no flag.Priority Condition Action 1 $ARGUMENTSnames a PR number/URL, or Existing PR is a URLskip the build, enter the monitor loop (step 3), capturing the PR number 2 uncommitted changes present build the PR from branch and commit (step 2, references/build-pr.md)3 commits ahead of base but no open PR build the PR from PR creation (step 2) 4 only a task description on a clean tree build the PR from implement (step 2) 5 none of the above nothing shippable; report and stop Never force the full flow when a PR already exists. Detect GitButler once by comparing Current branch to
gitbutler/workspace(the create side uses thebutCLI only there); on that path the current-branch Existing PR probe cannot see the virtual branch's PR, so look it up by virtual branch name (gh pr list --head <virtual-branch> --json number,url). Derive OWNER and REPO from the Repository value; for an existing PR prefer its real base viagh pr view <n> --json baseRefName. -
Build the PR. To build the PR (implement, branch, commit, changeset, open), follow
references/build-pr.md; skip this when resuming an existing PR. When resuming an existing PR that is missing a required changeset, still add and push one per the changeset guidance inreferences/build-pr.md. -
Monitor by watching gh state, not a fixed clock. Before the first wait, validate the configured merge method against the repo's
allowed_merge_methods(gh repo view --json squashMergeAllowed,mergeCommitAllowed,rebaseMergeAllowed); if the requested method is not permitted, substitute the closest allowed (prefer squash, then rebase, then merge) and note the substitution rather than discovering the conflict at merge time. Then block on gh's own API-polling watch until the awaited state changes, run one idempotent monitor pass, then block again. The wait set is every rollup check and every in-flight review, including review bots;--watchreturns when currently registered items settle, so after it returns re-read the rollup and keep watching if anything new is pending.gh pr checks <n> --watch --interval <seconds>is the primary wait;gh run watch <run-id> --exit-statusis the single-run equivalent. When the rollup does not cover the wait, fall back to a short bounded gh poll perreferences/monitor.md. Right after pushing a fix, pause briefly before watching so the new run registers, or--watchmay return immediately on the previous head's already-settled checks. Every wait is heartbeat-bounded: reviews and CI are expected to settle in about 15 minutes, so keep waiting through that window, including an item that finishes around it; 45 minutes bounds the whole PR unless the user set another. Emit one visible status line per tick naming what is still pending and its age; on each tick re-verify the awaited item is alive (its run or reviewer shows activity since the last tick), not merely still pending. A transient API failure ends the watch command, not the wait: re-enter the watch, and after three consecutive command failures switch to the bounded poll. Drop a still-pending item only when it has gone quiet well past the 15-minute window (no new timestamps after a re-fetch); 15 minutes elapsed with the run still moving is not a skip. A red conclusion is the stalled-check blocker in step 5 even if the check is absent from the repo's required set. No dropped item is ever polled again. -
Run each monitor pass following
references/monitor.mdfor the full per-tick mechanics (wait-set and required-set discovery, check and reviewer classification, the reviewThreads read, merge-state handling, idempotency). Callverify-prfor the merge-worthiness recommendation on the first pass after creation, then again for every commit that lands afterwards whoever pushed it, passing--since <last verified head>so a re-read costs the delta instead of the whole patch. Carry that SHA forward from verify-pr's Verified head line: it is the loop's own state, recorded when a pass returns and replaced only when a later pass supersedes it. Skip ticks where the head has not moved. Fix every Blocking and fold-in finding a pass returns in a follow-up commit and push; a finding marked follow-up issue stays out of the PR, and a fold-in whose fix outgrows the touched files or fails the PR's checks is backed out and reclassified as one. A fix ship-pr pushed itself still counts, because authoring a commit is not verifying it and the gate's checks establish only that the tree builds. -
Merge, then clean up. When the Merge gate holds, merge per the gate's command, then take the normal exit. When the shipped branch lived in its own worktree, that worktree has served its purpose: remove it by default (
git worktree remove <path>, run from another worktree of the same repo), and keep it only when the user said further work is coming in that same worktree, naming it when you do. Nevergit checkout <default>inside a worktree, because the default branch is normally checked out in the primary one and git refuses with'<default>' is already used by worktree at ...; that same collision makesgh pr merge --delete-branchprint a failure after the merge itself succeeded, so confirm the outcome fromgh pr view <n> --json state,mergedAtrather than the command's exit code. On a single checkout, return to the default branch and sync it so the local default carries the squashed merge and the next ship-pr starts clean:git checkout <default> && git pull --ff-onlyon the plain-git path; on the GitButler path stay ongitbutler/workspaceandbut pullinstead of checking out. Then stop (the only normal exit). A--draftPR is a hold: keep watching but nevergh pr readyor merge it until the user marks it ready. If the gate cannot clear, stop watching and report the blocker, leaving the PR branch checked out and its worktree in place. Blockers:- a persistently red check, or a still-pending item that has gone quiet well past the 15-minute window
- an unmet required approval the condition (3) approval path could not clear
- a DIRTY or conflict-blocked BEHIND branch
- a fork head ship-pr cannot push to
- an unreachable gating reviewer
verify-prrecommending close or redirect
The step 3 budgets bound the loop; treat UNKNOWN merge or check states as still computing and re-poll within them rather than escalate.
Addressing threads
Every unresolved thread is dispositioned and then resolved (three dispositions, reconciled with the governing rule below).
- Valid feedback. Fix in a follow-up commit (
commit-changeson the plain-git path,but commiton the GitButler path, then push), then resolve the thread. The fix commit is already on the PR timeline, so do not add a reply that only points at it. - Invalid feedback. Reply with a short rationale (it carries information the diff does not), then resolve.
- Outdated thread (
isOutdatedtrue). Resolve it; the moved diff is self-evident, so do not reply.
Use judgment on bot nits: common-sense suggestions a competent agent already follows are usually a silent resolve, and scope creep from a long bot-feedback loop is a signal to cut and merge.
ship-pr owns the writes (verify-pr-comments is read-only): build the reply body safely with printf '%s' "$reply" | jq -Rs '{body: .}' and post it via gh api repos/$OWNER/$REPO/pulls/<n>/comments/<databaseId>/replies --method POST --input - (inline -f body='...' breaks on backticks and code; databaseId is the REST integer from ship-pr's reviewThreads nodes), and resolve via gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -f id=<threadId> (threadId is the opaque node id, so -f not -F).
Governing rule: do not add comments or changeset prose that only acknowledge review feedback; the test is, would you write it if no reviewer had flagged the code? If no, drop it.
Merge gate
Three conditions, all required, plus the mergeability preconditions.
- (1) Every check settled. The wait set is the full rollup minus names the user passed via
--advisory. Wait for each item to leave pending. Reviews and CI are expected to finish in about 15 minutes; keep waiting through that window, including an item that completes around it. Drop a still-pending item only when it has gone quiet well past the window, and note it; a red conclusion never drops. A Bot review's check color does not block once that review has posted on this head (condition (2) owns its threads). Every other settled check must be pass (SUCCESS, NEUTRAL, SKIPPED). A pass is also only as current as the base it ran against: when the newest completion timestamp in the rollup is more than an hour old and the base branch has commits after it, the green is stale; refresh the head perreferences/monitor.mdand wait for the fresh run instead of merging on it. Absence from the repo's required set is not a skip, and a pending review bot is not skippable because it is a bot. Discover the required set perreferences/monitor.mdonly to know which red items--adminstill cannot override. Never hardcode required checks or a bot name; classify review bots viauser.type == "Bot"ongh api repos/$OWNER/$REPO/pulls/<n>/reviews. - (2) Threads resolved. Every review thread is resolved (ship-pr's reviewThreads read returns no
isResolved == false). - (3) Reviewers satisfied. The count of distinct current APPROVED reviewers meets the repo's
required_approving_review_count, and no GATING reviewer is in CHANGES_REQUESTED (the latest-per-author reduction is empty for humans and required reviewers). Address the feedback and wait for re-approval; never dismiss the review. On another author's PR (the PR author does not match the My login probe), when the only shortfall is a required approval (no GATING reviewer in CHANGES_REQUESTED) with conditions (1) and (2) already holding, delegate to/approve-pr <n>(without--merge) to post the approval by default, then re-evaluate the gate; approve-pr gates its own write on a fresh verify-pr, so a PR that does not earn the approval comes back as a decline and this gate reports the blocker instead of forcing the merge. Self-approval is platform-blocked, so on ship-pr's own PR this path never runs; an unmet required approval there waits for a human reviewer or falls to the--adminrule in Merge. - Preconditions.
mergeable != CONFLICTING;mergeStateStatusis CLEAN, accepting UNSTABLE or BLOCKED only when every non-passing check is a still-pending item dropped after going quiet well past the 15-minute window, a completed Bot-review check whose threads are resolved, or (with--adminon ship-pr's own PR) an own-PR review requirement, and never DIRTY or DRAFT. WhenmergeStateStatusis UNSTABLE, enumerate every non-passing check instatusCheckRollupand confirm each is one of those accepted leftovers before treating UNSTABLE as acceptable; any other non-passing check blocks the merge. A--draftPR is a deliberate hold (see step 5). verify-pr supplies only the merge-worthiness recommendation (necessity and correctness, and a verdict of merge rather than close, redirect, or push back); the wait-set classification in conditions (1) to (3) is ship-pr's own per-tick poll. - Merge. When all hold, merge with the configured method, appending
--delete-branchif requested.--adminis ownership-scoped: append it only when the user opted in and the PR is ship-pr's own (the author matches the My login probe), to bypass a review requirement that self-approval cannot satisfy; never--adminanother author's PR, whose review requirement condition (3) satisfies by approving. Immediately before merging, independently re-verify all three conditions and all preconditions, and re-readheadRefOid; abort and re-run one monitor pass if the head SHA no longer matches the one the gate evaluated. Check the review the same way: when verify-pr's Verified head sits behind the head about to merge, that delta has been built but not judged, so runverify-pr <n> --since <verified head>and act on it first. Green checks are not cover for it, and neither is the commit being ship-pr's own. Even with--admin, this re-verification still runs:--adminmay force past a BLOCKED or UNSTABLE state whose sole cause is an own-PR review requirement or a still-pending item dropped after going quiet well past the 15-minute window; it never overrides a red non-review check, a still-moving pending item, a stale green (condition (1)), an unresolved thread, a CHANGES_REQUESTED review, or a CONFLICTING, DIRTY, or DRAFT PR.
Rules
- Commit and push all uncommitted work, including the changeset, before PR creation; never let create-pr open a PR over a dirty tree.
- One branch per change group; never touch another agent's branch or staged work.