Ship PR
GraphQL Budget Discipline (read first)
GitHub meters two separate hourly budgets: ~5,000 GraphQL points/hr and ~5,000 REST requests/hr. Tools that drive a GitHub Project board (e.g. Detent) already spend the GraphQL budget on ProjectV2 polling (Projects v2 is GraphQL-only). If this skill also leans on GraphQL for routine PR ops, the two collide and exhaust the shared pool — a CI-watch loop alone can burn hundreds of GraphQL points per PR. Keep this skill's work on the REST budget:
- CI status / watch:
gh api repos/<o>/<r>/commits/<sha>/check-runsorgh run watch <run-id> --exit-status. Avoid loopinggh pr checks --watch/gh pr viewto poll CI (GraphQL-routed). - PR / mergeability / state reads:
gh api repos/<o>/<r>/pulls/<N>(REST, hasmergeable/mergeable_state) instead ofgh pr view --jsonorgh api graphqlmergeState queries. - Merge:
gh api --method PUT repos/<o>/<r>/pulls/<N>/merge -f merge_method=<m> -f sha=<sha>instead ofgh pr merge(GraphQL). - Reserve GraphQL only for things with no REST equivalent (ProjectV2
fields). If a GraphQL call hits
rate limit exceeded, the REST budget is almost certainly fine — switch to REST, don't wait for the reset.
(The same discipline applies to the other go-workflow skills — e2e-verify,
address-review, complete-issue.)
0. State File Bootstrap
Before calling setup-loop, check if a state file already exists with a non-empty
phase (re-entry). If so, skip setup-loop to preserve custom fields (args,
pass, pr_number, base_branch, no_merge, llm, discovered_bots).
STATE_FILE=".local/state/ship.loop.local.json"
if [ -f "$STATE_FILE" ]; then
EXISTING_PHASE=$(jq -r '.phase // empty' "$STATE_FILE" 2>/dev/null || true)
if [ -n "$EXISTING_PHASE" ]; then
echo "Re-entry detected (phase: $EXISTING_PHASE) — skipping setup-loop to preserve state."
fi
fi
Only call setup-loop on fresh starts (no state file or empty phase):
if [ -f ".local/state/ship.loop.local.json" ] && [ -n "$(jq -r '.phase // empty' .local/state/ship.loop.local.json 2>/dev/null)" ]; then
echo "Re-entry detected — skipping setup-loop."
elif [ ! -x "${CLAUDE_PLUGIN_ROOT}/scripts/setup-loop.sh" ]; then
echo "ERROR: Plugin cache stale. Run /gopher-ai-refresh (or refresh-plugins.sh) and restart Claude Code."
exit 1
else
"${CLAUDE_PLUGIN_ROOT}/scripts/setup-loop.sh" "ship" "SHIPPED" 50 "" "$(jq -c . "${CLAUDE_PLUGIN_ROOT}/lib/ship/resume-messages.json")"
fi
1. Parse Arguments
Parse $ARGUMENTS to extract:
--llm <value>:codex(default),gemini,ollama,fable(Claude subagent — no external CLI; prefer when the diff was written by Codex so a different model family reviews it)--passes <n>: max LLM review passes (default: 3)--no-merge: stop after bot approval, don't auto-merge--skip-coverage: skip coverage analysis. E2E may be reused only when a prior$e2e-verifypass is recorded; it is not skipped automatically for UI-visible diffs.--coverage-threshold <n>: override the default 60% threshold--tier <value>: gemini service tier (flex/standard/priority; gemini only; default: unset)
Store as LLM_CHOICE, MAX_PASSES, NO_MERGE, SKIP_COVERAGE,
COVERAGE_THRESHOLD (default 60), GEMINI_TIER.
Persist arguments to .local/state/ship.loop.local.json via jq so the
stop-hook can recover all fields on re-entry. The full jq invocation lives in
${CLAUDE_PLUGIN_ROOT}/lib/ship/state-fields.md — fields written: args,
llm, pass, no_merge, pr_number, base_branch,
bot_review_baseline, discovered_bots, has_ci, skip_coverage,
coverage_threshold, coverage_result, coverage_tests_generated,
e2e_required, e2e_attempted, e2e_result, e2e_skip_reason,
e2e_pages_tested, review_clean, head_sha, gemini_tier.
2. Re-entry Check
source "${CLAUDE_PLUGIN_ROOT}/lib/loop-state.sh"
STATE_FILE=".local/state/ship.loop.local.json"
[ -f "$STATE_FILE" ] && read_loop_state "$STATE_FILE"
If PHASE is set (non-empty), this is a stop-hook re-entry. Restore all fields
listed in Step 1 from state file via jq -r '.<field> // empty'. If
review_clean == "true", set REVIEW_CLEAN=true to preserve the clean-review
fast path.
Then jump to the matching phase:
| Phase | Step |
|-------|------|
| reviewing | Step 5 (Phase 1) |
| fixing | Step 6 (Phase 1) |
| verifying | Step 7 (Phase 1) |
| coverage-check | Step 7.5 (Phase 1) |
| e2e-testing | Step 7.6 (Phase 1) |
| pushing | Step 9 (Phase 2) |
| ci-watch | Step 10 (Phase 3) |
| bot-watching | Step 11 (Phase 4) |
| addressing | Step 12 (Phase 5) |
| merging | Step 13 (Phase 6) |
If PHASE is empty/unset → fresh start. Continue to Step 3.
3. Detect Context
CURRENT_BRANCH=$(git branch --show-current)
PR_JSON=$(gh pr view --json number,baseRefName --jq '.' 2>/dev/null || echo "")
if [ -n "$PR_JSON" ]; then
PR_NUM=$(echo "$PR_JSON" | jq -r '.number')
BASE_BRANCH=$(echo "$PR_JSON" | jq -r '.baseRefName')
echo "PR #$PR_NUM targets: $BASE_BRANCH"
else
BASE_BRANCH=$(git remote show origin 2>/dev/null | grep 'HEAD branch' | sed 's/.*: //' | grep . || echo "main")
PR_NUM=""
echo "No PR found. Base: $BASE_BRANCH"
fi
CRITICAL: If CURRENT_BRANCH == BASE_BRANCH → STOP, do not ship from
the default branch. Inform the user and ask how to proceed.
If git status --porcelain shows uncommitted changes, ask the user: "Commit
them before shipping, or abort?"
Persist BASE_BRANCH and PR_NUM (if found) in the state file.
4. Prerequisite Check
Verify the selected LLM CLI is installed. CRITICAL: Never silently fall
back — always use AskUserQuestion. The detection bash, diagnostic block,
and four-option AskUserQuestion (Retry / Debug / Install instructions
/ Use agent-based review / Abort) live in
${CLAUDE_PLUGIN_ROOT}/lib/ship/prerequisites.md.
On re-entry (Step 2): Restore USE_AGENT_REVIEW from state. If "true",
set CODEX_EXEC_FALLBACK=true — do NOT re-ask. If llm_check_failed=="true"
AND use_agent_review!="true", re-present the AskUserQuestion.
Phase 1: Local LLM Review (Steps 5–8)
LLM review → fix → verify → coverage gate (final pass) → E2E smoke (when applicable) → commit → loop decision.
Coverage gate (Step 7.5, final pass only): Read
${CLAUDE_PLUGIN_ROOT}/lib/coverage/coverage-verification.md and follow
Steps A–F with BASE_BRANCH=origin/${BASE_BRANCH}, STATE_FILE,
SKIP_COVERAGE, COVERAGE_THRESHOLD from parsed args.
Loop decision (Step 8): clean review (REVIEW_CLEAN=true) OR
PASS >= MAX_PASSES → Phase 2. Otherwise → back to Step 5. Always stage only
fixed files (never git add -A).
→ Read ${CLAUDE_PLUGIN_ROOT}/lib/ship/local-review.md for: LLM execution
paths (codex exhaustive/quick, fable Claude-subagent, gemini, ollama,
agent-based fallback), structured-JSON vs free-text parsing,
confidence_score < 0.3 filter, codegen-drift check
(make generate|gen|codegen|sqlc|proto|templ), E2E skip conditions, and the
staged-commit + pass-counter increment.
Phase 2: Push and PR Creation (Step 9)
set_loop_phase ".local/state/ship.loop.local.json" "pushing"
Push to remote (use the configured tracking remote and PR headRefName), ensure
a PR exists (auto-detect template at .github/pull_request_template.md or
PULL_REQUEST_TEMPLATE.md, else default ## Summary + ## Test Plan), capture
HEAD_SHA and BOT_REVIEW_BASELINE immediately and persist both.
→ Read ${CLAUDE_PLUGIN_ROOT}/lib/ship/push-and-pr.md for the push command, PR
creation logic, template detection, and the post-push capture block.
Phase 3: CI Watch (Step 10)
set_loop_phase ".local/state/ship.loop.local.json" "ci-watch"
MANDATORY — NO EXCEPTIONS: You MUST verify that CI checks correspond to the
latest pushed HEAD_SHA before considering CI as passed. You MUST NOT:
- Assume passing checks from a prior commit apply to the current commit
- Rationalize that "only a minor fix was pushed so old checks are still valid"
- Skip SHA verification because
gh pr checks --watchreturned success - Treat "no checks yet" as "checks passed"
The ENTIRE purpose of CI is to validate the EXACT code being merged. Stale check results are meaningless.
If no .github/workflows/*.yml files exist → persist has_ci: false and skip
to Step 11.
→ Read ${CLAUDE_PLUGIN_ROOT}/lib/ship/ci-watch.md for: HEAD-SHA
capture-and-verify, the 120s wait for checks to register against the SHA,
gh pr checks --watch, post-watch SHA shift detection (concurrent push →
fetch+reset to new HEAD, reset pass counter, set phase to reviewing, restart
from Step 5), and CI failure recovery.
Phase 4: Bot Watch (Step 11)
set_loop_phase ".local/state/ship.loop.local.json" "bot-watching"
Discover review bots via the GraphQL query for reviews + reviewThreads + comments author logins; also check gh pr checks names for status-only bots
(e.g., Greptile). Match against
${CLAUDE_PLUGIN_ROOT}/skills/address-review/bot-registry.md. Persist
discovered_bots (comma-separated). If none found and BOT_REVIEW_BASELINE is
recent (<2 min), AskUserQuestion whether to wait or proceed.
For polling, Read ${CLAUDE_PLUGIN_ROOT}/skills/address-review/watch-loop.md
Steps 12a–12d:
- All bots approved → Step 13
- New comments /
CHANGES_REQUESTED→ Step 12 - Timeout (5 min) →
AskUserQuestion
→ Read ${CLAUDE_PLUGIN_ROOT}/lib/ship/bot-watch.md for the full GraphQL query
and the bot-not-detected-yet retry policy.
Phase 5: Address Bot Feedback (Step 12)
set_loop_phase ".local/state/ship.loop.local.json" "addressing"
Fetch and rebase against base (git fetch origin "$BASE_BRANCH" && git rebase "origin/$BASE_BRANCH" || git rebase --abort); if rebase aborts on conflicts,
proceed without rebasing — user resolves manually.
Read ${CLAUDE_PLUGIN_ROOT}/skills/address-review/SKILL.md and follow
Steps 2–11 only (skip Step 1 / loop init — we're already managed; skip Step
12 / bot-watch — we own that in Step 11).
CRITICAL: Capture BOT_REVIEW_BASELINE BEFORE pushing (catches fast bot
responses). Then push, capture HEAD_SHA after push. Persist both. Return to
Step 10 — re-watch CI for the new SHA.
→ Read ${CLAUDE_PLUGIN_ROOT}/lib/ship/address-bots.md for the rebase-or-abort
handling and the baseline-then-push ordering.
Phase 6: Merge (Step 13)
set_loop_phase ".local/state/ship.loop.local.json" "merging"
CRITICAL: NEVER use --admin. NEVER bypass branch protection. If merge
fails due to protection, STOP and inform the user — do NOT retry with elevated
privileges.
→ Read ${CLAUDE_PLUGIN_ROOT}/lib/ship/merge.md for: final-checks (CI green, no
unresolved threads, no human CHANGES_REQUESTED), --no-merge early exit,
merge-strategy auto-detection (--merge > --squash > --rebase), the full
mergeStateStatus decision tree (UNKNOWN/CONFLICTING/BLOCKED/CLEAN/
HAS_HOOKS/BEHIND/UNSTABLE/other), merge-queue handling, and the
summary-line rendering (uses coverage_skip_reason to avoid N/A%). Output
<done>SHIPPED</done> after the merge succeeds.
Phase Flow Summary
5–8 local-review → 9 pushing → 10 ci-watch → 11 bot-watch ⇄ 12 addressing
↓
13 merging → <done>SHIPPED</done>
[coverage-check] runs only on the final pass when --skip-coverage isn't set.
[e2e-testing] is mandatory for UI-visible diffs. Missing MCP/browser tooling
or an unavailable dev server records e2e_result=blocked and stops before push
or merge. Non-UI diffs may record e2e_result=skipped.
Verification Gate (HARD — applies before ANY completion signal)
Before outputting <done>SHIPPED</done>, every claim MUST have FRESH evidence
from THIS session — actual command output, not narrative:
- "Tests pass" →
go testoutput with "ok" lines, zero failures - "Build succeeds" →
go build ./...exit 0 - "CI passes" →
gh pr checkswith all checks green - "Bot approvals" →
gh pr view --json reviews --jq '.reviews[] | {author: .author.login, state: .state}'with APPROVED - "PR merged" → merge output or
gh pr viewshowing MERGED
Red-flag language check — if you are about to write "should work" / "should be fine" / "probably" / "likely" / "I believe" / "I think" / "Done!" / "Shipped!" without preceding command output proving it, STOP and run verification instead.
Completion Criteria
Output <done>SHIPPED</done> ONLY when ALL of these are true:
- LLM review passes completed (clean or max passes reached)
- Coverage verified for changed files (or skipped via
--skip-coverage) - E2E smoke tests passed for UI-visible diffs (or skipped only because the diff is non-UI / no web components)
- Changes pushed to remote
- PR exists
- CI passes (or no CI configured) — with output shown above
- Bot approvals received (or no bots configured) — with output shown above
- PR merged (or
--no-mergespecified) — with output shown above
Safety note: If you've iterated 15+ times without completion, document what's blocking and ask the user.
Cancel
/cancel-loop ship cleanly exits the loop.
Further Reading
All sibling files live under ${CLAUDE_PLUGIN_ROOT}/lib/ship/:
state-fields.md— full jq invocation for Step 1's persist; field name referenceprerequisites.md— Step 4 LLM diagnostic outputlocal-review.md— Phase 1 (Steps 5–8): review/fix/verify/coverage/e2e/commitpush-and-pr.md— Phase 2 (Step 9): push, PR creation, template detection, baseline captureci-watch.md— Phase 3 (Step 10): SHA-anchored CI watch, post-watch shift detection, failure recoverybot-watch.md— Phase 4 (Step 11): GraphQL bot discovery, retry-on-empty policyaddress-bots.md— Phase 5 (Step 12): rebase, address-review delegation, baseline-then-push orderingmerge.md— Phase 6 (Step 13): final checks, merge strategy detection, mergeStateStatus tree, summary rendering