"Ship a PR end-to-end: verify locally, push, create/update the PR, watch CI, handle review feedback, and merge without admin override. Use for 'ship', 'ship it', or 'push and merge'. SKIP if the user only wants a PR opened; use `create-pr`."
Resources
1Install
npx skillscat add gopherguides/gopher-ai/ship Install via the SkillsCat registry.
Ship PR
Plugin Resource Resolution
<PLUGIN_ROOT> is notation. Replace it with a concrete absolute plugin root before every resource read or command:
- Codex: Start from the directory containing the absolute selected
SKILL.mdpath, then ascend two directories (skills/<name>-> plugin root). - Claude Code: Bind it to the injected
${CLAUDE_PLUGIN_ROOT}value.
Before requesting decisions or delegating work, read<PLUGIN_ROOT>/lib/driver-interaction.md and follow its
cross-platform capability-binding rules.
Read <PLUGIN_ROOT>/lib/decision-gates.md before resolving any workflow
choice.
Bind the invocation arguments as SKILL_ARGS for $go-workflow:ship by
reading <PLUGIN_ROOT>/lib/skill-arguments.md with this Claude Code compatibility payload:
$ARGUMENTS
Load the shared GitHub REST helpers before any GitHub workflow operation:
source "<PLUGIN_ROOT>/lib/github-rest.sh"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 routine PR work on the REST budget through the
shared helper:
- CI status / watch: use
github_watch_pr_checksorgithub_check_snapshot, always pinned to the expected PR head SHA. - PR metadata: use
github_current_prandgithub_pr. - Formal reviews: use
github_pr_reviews. - Mergeability: read REST
.mergeableand.mergeable_statethroughgithub_pr. - Ordinary merge: use the REST pull merge endpoint with
merge_methodand
the expected head SHA.
The only GraphQL exceptions in these workflows are review-thread discovery and
resolution, closingIssuesReferences, and required merge-queue enqueueing.
The sibling e2e-verify, address-review, and complete-issue skills source
the same shared helper and follow this discipline.
0. State File Bootstrap
Ship has exactly one state owner. A caller embeds ship by supplying bothCALLER_LOOP_STATE_FILE and CALLER_WORKFLOW_STATE_PATH; ship creates a child
object in that physical file and never initializes another loop. Without both
values, ship is standalone and always resolves the released canonical state
file under the original repository root. A valid legacy standalone file is
migrated in place by read_loop_state, preserving its root field names and
phase routing.
Released ship versions could write that standalone file under a linked
worktree. When the canonical primary-root file is absent, ship enumerates the
linked worktree's loop-state files and relocates the candidate only when exactly
one exists and it is valid ship state. It then migrates the schema and fills
only missing root locator fields from the current registered worktree. Multiple
linked candidates, invalid JSON, or state owned by another loop stop without
mutation and name every ambiguous path. An existing canonical state always
wins; linked-worktree strays are ignored in that case.
source "<PLUGIN_ROOT>/lib/loop-state.sh"
SHIP_EMBEDDED=false
WORKFLOW_STATE_PATH='[]'
if [ -n "${CALLER_LOOP_STATE_FILE:-}" ] || [ -n "${CALLER_WORKFLOW_STATE_PATH:-}" ]; then
if [ -z "${CALLER_LOOP_STATE_FILE:-}" ] || [ -z "${CALLER_WORKFLOW_STATE_PATH:-}" ]; then
echo "ERROR: Embedded ship requires both caller state file and workflow path."
exit 1
fi
case "$CALLER_LOOP_STATE_FILE" in
/*) ;;
*) echo "ERROR: Embedded ship caller state file must be absolute."; exit 1 ;;
esac
if [ ! -f "$CALLER_LOOP_STATE_FILE" ] ||
! jq -e '.schema_version == 2' "$CALLER_LOOP_STATE_FILE" >/dev/null 2>&1; then
echo "ERROR: Embedded ship requires an existing v2 caller state file."
exit 1
fi
STATE_FILE="$CALLER_LOOP_STATE_FILE"
WORKFLOW_STATE_PATH=$(child_workflow_path "$CALLER_WORKFLOW_STATE_PATH" "ship")
initialize_workflow_state "$STATE_FILE" "$WORKFLOW_STATE_PATH"
SHIP_EMBEDDED=true
ORIGINAL_REPO_ROOT=$(get_loop_field "$STATE_FILE" "original_repo_root" '[]')
WORKTREE_PATH=$(get_loop_field "$STATE_FILE" "worktree_path" '[]')
REPO_SLUG=$(get_loop_field "$STATE_FILE" "repo_slug" '[]')
else
CURRENT_CHECKOUT_ROOT=$(git rev-parse --show-toplevel)
ORIGINAL_REPO_ROOT=$(git -C "$CURRENT_CHECKOUT_ROOT" worktree list --porcelain | awk '/^worktree / {sub(/^worktree /, ""); print; exit}')
WORKTREE_PATH="${WORKTREE_PATH:-$CURRENT_CHECKOUT_ROOT}"
if [ -z "$ORIGINAL_REPO_ROOT" ] || [ "${ORIGINAL_REPO_ROOT#/}" = "$ORIGINAL_REPO_ROOT" ] ||
[ -z "$WORKTREE_PATH" ] || [ "${WORKTREE_PATH#/}" = "$WORKTREE_PATH" ] || [ ! -d "$WORKTREE_PATH" ]; then
echo "ERROR: Could not resolve absolute repository paths."
exit 1
fi
CANONICAL_STATE_FILE="$ORIGINAL_REPO_ROOT/.local/state/ship.loop.local.json"
STATE_FILE="$CANONICAL_STATE_FILE"
CURRENT_REPO_SLUG=$(cd "$WORKTREE_PATH" && gh api "repos/{owner}/{repo}" --jq '.full_name')
MIGRATED_LINKED_STATE=false
if [ ! -f "$CANONICAL_STATE_FILE" ] &&
[ "$CURRENT_CHECKOUT_ROOT" != "$ORIGINAL_REPO_ROOT" ]; then
LINKED_STATE_CANDIDATES=()
for LINKED_STATE_CANDIDATE in "$CURRENT_CHECKOUT_ROOT/.local/state/"*.loop.local.json; do
[ -f "$LINKED_STATE_CANDIDATE" ] || continue
LINKED_STATE_CANDIDATES+=("$LINKED_STATE_CANDIDATE")
done
if [ "${#LINKED_STATE_CANDIDATES[@]}" -gt 1 ]; then
echo "ERROR: Ambiguous linked-worktree loop state candidates while canonical ship state '$CANONICAL_STATE_FILE' is absent; refusing migration:"
printf ' - %s\n' "${LINKED_STATE_CANDIDATES[@]}"
exit 1
elif [ "${#LINKED_STATE_CANDIDATES[@]}" -eq 1 ]; then
LEGACY_STATE_FILE="${LINKED_STATE_CANDIDATES[0]}"
if ! jq -e '
type == "object" and
(.schema_version == null) and
.loop_name == "ship" and
(.completion_promise == "SHIPPED" or .completion_promise == "INCOMPLETE")
' "$LEGACY_STATE_FILE" >/dev/null 2>&1; then
echo "ERROR: Invalid linked-worktree legacy ship state '$LEGACY_STATE_FILE': expected released unversioned ship JSON with a SHIPPED or INCOMPLETE promise; refusing migration to '$CANONICAL_STATE_FILE'."
exit 1
fi
mkdir -p "$(dirname "$CANONICAL_STATE_FILE")"
mv "$LEGACY_STATE_FILE" "$CANONICAL_STATE_FILE"
MIGRATED_LINKED_STATE=true
echo "Migrated linked-worktree ship state from '$LEGACY_STATE_FILE' to '$CANONICAL_STATE_FILE'."
fi
fi
EXISTING_PHASE=""
if [ -f "$STATE_FILE" ]; then
read_loop_state "$STATE_FILE" "$WORKFLOW_STATE_PATH"
if [ "$MIGRATED_LINKED_STATE" = "true" ]; then
if [ -z "$(get_loop_field "$STATE_FILE" "original_repo_root" '[]')" ]; then
set_loop_field "$STATE_FILE" "original_repo_root" "$ORIGINAL_REPO_ROOT" '[]'
fi
if [ -z "$(get_loop_field "$STATE_FILE" "worktree_path" '[]')" ]; then
set_loop_field "$STATE_FILE" "worktree_path" "$CURRENT_CHECKOUT_ROOT" '[]'
fi
if [ -z "$(get_loop_field "$STATE_FILE" "repo_slug" '[]')" ]; then
set_loop_field "$STATE_FILE" "repo_slug" "$CURRENT_REPO_SLUG" '[]'
fi
read_loop_state "$STATE_FILE" "$WORKFLOW_STATE_PATH"
fi
EXISTING_PHASE="$PHASE"
fi
if [ -n "$EXISTING_PHASE" ]; then
PERSISTED_ORIGINAL_REPO_ROOT=$(get_loop_field "$STATE_FILE" "original_repo_root" "$WORKFLOW_STATE_PATH")
PERSISTED_WORKTREE_PATH=$(get_loop_field "$STATE_FILE" "worktree_path" "$WORKFLOW_STATE_PATH")
PERSISTED_REPO_SLUG=$(get_loop_field "$STATE_FILE" "repo_slug" "$WORKFLOW_STATE_PATH")
REGISTERED_WORKTREES=$(git -C "$ORIGINAL_REPO_ROOT" worktree list --porcelain | awk '/^worktree / {sub(/^worktree /, ""); print}')
if [ "$PERSISTED_ORIGINAL_REPO_ROOT" != "$ORIGINAL_REPO_ROOT" ] ||
[ -z "$PERSISTED_WORKTREE_PATH" ] ||
[ "${PERSISTED_WORKTREE_PATH#/}" = "$PERSISTED_WORKTREE_PATH" ] ||
[ ! -d "$PERSISTED_WORKTREE_PATH" ] ||
! printf '%s\n' "$REGISTERED_WORKTREES" | awk -v path="$PERSISTED_WORKTREE_PATH" '$0 == path { found = 1 } END { exit !found }' ||
[ "$PERSISTED_REPO_SLUG" != "$CURRENT_REPO_SLUG" ]; then
set_loop_terminal_result "$STATE_FILE" "incomplete" "ship-worktree-path-invalid" "incomplete" "INCOMPLETE"
echo "WORKFLOW_RESULT=INCOMPLETE"
echo "WORKFLOW_REASON=ship-worktree-path-invalid"
echo "<done>INCOMPLETE</done>"
exit 1
fi
WORKTREE_PATH="$PERSISTED_WORKTREE_PATH"
REPO_SLUG="$PERSISTED_REPO_SLUG"
echo "Re-entry detected (phase: $EXISTING_PHASE) — skipping setup-loop to preserve state."
else
REPO_SLUG="${REPO_SLUG:-$CURRENT_REPO_SLUG}"
fi
if [ -z "$EXISTING_PHASE" ]; then
if [ ! -x "<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
fi
/bin/bash "<PLUGIN_ROOT>/scripts/setup-loop.sh" "ship" "SHIPPED" 50 "" \
"$(jq -c . "<PLUGIN_ROOT>/lib/ship/resume-messages.json")" \
"$STATE_FILE" "[\"SHIPPED\",\"INCOMPLETE\"]"
fi
initialize_workflow_state "$STATE_FILE" "$WORKFLOW_STATE_PATH"
fi1. Parse Arguments
Parse SKILL_ARGS 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: compatibility hint for source-free changes. Changed source
files always run the coverage gate. E2E may be reused only when a prior$go-workflow:e2e-verifypass is recorded.--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, and LLM_EXPLICIT.LLM_EXPLICIT=true only when SKILL_ARGS contains --llm; otherwise it isfalse.
Persist arguments to the resolved workflow object so the
stop-hook can recover all fields on re-entry. The path-aware initialization lives in<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, ci_skip_reason, skip_coverage,coverage_threshold, coverage_result, coverage_tests_generated,e2e_required, e2e_attempted, e2e_result, e2e_skip_reason,e2e_pages_tested, review_clean, review_result,review_skip_reason, head_sha, gemini_tier, llm_explicit.
For Ollama reviews, Step 5 also persists ollama_model after resolving it from
the installed model list.
Hard Invariant Failure
When this skill or a supporting file says to stop incomplete, set the supplied
reason code as WORKFLOW_REASON, then persist the machine-readable outcome:
WORKFLOW_REASON="${WORKFLOW_REASON:?workflow reason is required}"
if [ "$SHIP_EMBEDDED" = "true" ]; then
set_workflow_result "$STATE_FILE" "$WORKFLOW_STATE_PATH" "incomplete" "$WORKFLOW_REASON" "incomplete"
echo "WORKFLOW_RESULT=INCOMPLETE"
echo "WORKFLOW_REASON=$WORKFLOW_REASON"
else
set_loop_terminal_result "$STATE_FILE" "incomplete" "$WORKFLOW_REASON" "incomplete" "INCOMPLETE"
echo "WORKFLOW_RESULT=INCOMPLETE"
echo "WORKFLOW_REASON=$WORKFLOW_REASON"
echo "<done>INCOMPLETE</done>"
fiStop the ship workflow after this block. Embedded ship returns the structured
result to its caller without changing caller-owned terminal fields or emitting
a marker. Standalone ship emits its allowlisted INCOMPLETE marker. Never ask
for permission to bypass the invariant and never output SHIPPED on this path.
2. Re-entry Check
[ -f "$STATE_FILE" ] && read_loop_state "$STATE_FILE" "$WORKFLOW_STATE_PATH"
WORKTREE_PATH=$(get_loop_field "$STATE_FILE" "worktree_path" '[]')
REPO_SLUG=$(get_loop_field "$STATE_FILE" "repo_slug" '[]')If PHASE is set (non-empty), this is a stop-hook re-entry. Restore every Step
1 field through get_loop_field "$STATE_FILE" "<field>" "$WORKFLOW_STATE_PATH". If review_clean == "true", setREVIEW_CLEAN=true to preserve the clean-review fast path. Never read the
physical root directly because embedded ship owns only its child object.
An in-session review is never resumable. If PHASE == "reviewing" on
re-entry, the reviewer from the earlier session no longer exists. Do not wait
for it and do not dispatch a replacement. Follow Expired review recovery
below.
Then jump to the matching phase:
| Phase | Step |
|---|---|
reviewing |
Expired review recovery, then Step 9 (Phase 2) |
review-required |
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.
review-required is a durable request to start one review, used when CI
detects that the PR head changed. Step 5 immediately changes it toreviewing before dispatch. This keeps a review that has not started distinct
from an in-flight review that expired at a session boundary.
Expired review recovery
This path is for a successor session only. Treat the earlier review as void and
make the validated work durable before doing anything else:
- Persist
review_result="void"andreview_skip_reason="session-boundary". Never reuse a prior agent handle or
review output. - If the index contains validated staged changes, inspect the staged diff and
commit exactly those files with a conventional message that describes the
change. Do not label this commit as review findings. - Set the phase to
pushing, push every local commit, and ensure a non-draft
PR exists via Step 9. Do all three in the same session before yielding.
If there is no staged diff, continue with the existing local commits. Unstaged
or untracked files were not part of the validated index; leave them untouched
and report them after the PR is open.
3. Detect Context
CURRENT_BRANCH=$(git -C "$WORKTREE_PATH" branch --show-current)
LOCAL_HEAD_SHA=$(git -C "$WORKTREE_PATH" rev-parse HEAD)
if PR_JSON=$(cd "$WORKTREE_PATH" && github_current_pr "$CURRENT_BRANCH" "$LOCAL_HEAD_SHA"); then
PR_NUM=$(echo "$PR_JSON" | jq -r '.number')
BASE_BRANCH=$(echo "$PR_JSON" | jq -r '.base.ref')
echo "PR #$PR_NUM targets: $BASE_BRANCH"
else
PR_LOOKUP_STATUS=$?
if [ "$PR_LOOKUP_STATUS" -ne 4 ]; then
WORKFLOW_REASON="current-pr-api-error"
fi
BASE_BRANCH=$(gh api "repos/$REPO_SLUG" --jq '.default_branch')
PR_NUM=""
echo "No PR found. Base: $BASE_BRANCH"
fiAn empty exact-head lookup returns status 4 and means no PR exists yet. Any
other lookup failure sets WORKFLOW_REASON=current-pr-api-error; follow
Hard Invariant Failure and stop rather than treating an API failure as no
PR.
CRITICAL: If CURRENT_BRANCH == BASE_BRANCH, setWORKFLOW_REASON=default-branch, follow Hard Invariant Failure, and stop.
Do not ship from the default branch.
If git -C "$WORKTREE_PATH" status --porcelain shows uncommitted changes, resolve a
driver-resolvable gate. Inspect the diff, staged state, original request,
and workflow-owned file list:
- Include and commit changes only when they are unambiguously in scope and have
fresh validation evidence. - Preserve unrelated changes and ship only committed
HEADwhen later steps
cannot overwrite or stage them. - If ownership is ambiguous or safe isolation is impossible, stop incomplete
withWORKFLOW_REASON=unowned-worktree-changes.
State Decision, Evidence, and Rationale; do not request input for this
technical ownership decision.
Persist the detected context in the resolved ship workflow object:
set_loop_field "$STATE_FILE" "base_branch" "$BASE_BRANCH" "$WORKFLOW_STATE_PATH"
set_loop_field "$STATE_FILE" "pr_number" "$PR_NUM" "$WORKFLOW_STATE_PATH"4. Prerequisite Check
Verify the selected LLM CLI is installed. Read<PLUGIN_ROOT>/lib/ship/prerequisites.md for the evidence-based
fallback ordering. A driver may replace an unpinned default and must state the
rationale. Replacing an explicitly selected backend is a missing-intent gate.
On re-entry (Step 2): Restore USE_AGENT_REVIEW and LLM_EXPLICIT from
state. If use_agent_review=="true", set CODEX_EXEC_FALLBACK=true. Ifllm_check_failed=="true" and no fallback is persisted, repeat the
prerequisite evidence check and its deterministic recovery policy.
Phase 1: Local LLM Review (Steps 5–8)
LLM review → fix → verify → coverage gate (final pass) → E2E smoke (when
applicable) → commit → loop decision.
Delegated reviews are session-local: run them synchronously in the
foreground and wait for their final response. In a headless worker context,
skip an agent-backed review when it cannot finish within the current session;
persist the skip reason and proceed through commit, push, and non-draft PR
creation. Never end a session with staged or committed-but-unpushed work while
waiting on a background process.
Coverage gate (Step 7.5, final pass only): Read<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) ORPASS >= MAX_PASSES → Phase 2. Otherwise → back to Step 5. Always stage only
fixed files (never git add -A).
→ Read <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 targets generate, gen, codegen, sqlc, proto, or templ), E2E skip conditions, and the
staged-commit + pass-counter increment.
Phase 2: Push and PR Creation (Step 9)
set_loop_phase "$STATE_FILE" "pushing" "$WORKFLOW_STATE_PATH"Push to remote (use the configured tracking remote and PR headRefName), ensure
a PR exists (auto-detect template at .github/pull_request_template.md orPULL_REQUEST_TEMPLATE.md, else default ## Summary + ## Test Plan), captureHEAD_SHA and BOT_REVIEW_BASELINE immediately and persist both.
→ Read <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 "$STATE_FILE" "ci-watch" "$WORKFLOW_STATE_PATH"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 the post-watch PR head verification
- 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 withci_skip_reason: no-workflow-files and skip to Step 11. When workflow files
exist but no checks register, only skip CI after Step 10b establishes that no
active workflow applies to the current PR.
→ Read <PLUGIN_ROOT>/lib/ship/ci-watch.md for: HEAD-SHA
capture-and-verify, the 120s wait for checks to register against the SHA,
combined check-run and commit-status aggregation, 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 "$STATE_FILE" "bot-watching" "$WORKFLOW_STATE_PATH"Discover review bots from REST formal reviews, REST top-level issue comments,
and GraphQL review-thread comments; use an exact-head check snapshot for
status-only bots such as Greptile. Match against<PLUGIN_ROOT>/skills/address-review/bot-registry.md. Persistdiscovered_bots (comma-separated). If none are found andBOT_REVIEW_BASELINE is recent (<2 min), follow the bounded automatic wait inbot-watch.md.
For polling, Read <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) → apply the deterministic re-trigger or incomplete outcome
inwatch-loop.md
→ Read <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 "$STATE_FILE" "addressing" "$WORKFLOW_STATE_PATH"Fetch and rebase against base (git -C "$WORKTREE_PATH" fetch origin "$BASE_BRANCH" && git -C "$WORKTREE_PATH" rebase "origin/$BASE_BRANCH"). If conflicts cannot be resolved, abort the rebase,
set WORKFLOW_REASON=rebase-conflict, follow Hard Invariant Failure, and
stop before applying or pushing fixes.
Execute address-review through the caller/component contract inaddress-bots.md, follow Steps 2–11 only, and require its structuredresult=complete before continuing. Skip Step 1 and Step 12 because ship owns
checkout and bot watch.
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 <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 "$STATE_FILE" "merging" "$WORKFLOW_STATE_PATH"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 <PLUGIN_ROOT>/lib/ship/merge.md for: final-checks (CI green, no
unresolved threads, no human CHANGES_REQUESTED), --no-merge early exit,
merge-strategy selection (SHIP_MERGE_STRATEGY, then --squash > --rebase > --merge), the full
REST mergeable_state decision tree (unknown/dirty/blocked/clean/has_hooks/behind/unstable/other), merge-queue handling, and the
summary-line rendering (uses coverage_skip_reason to avoid N/A%). Output
the Step 13g result after the merge succeeds. Embedded ship returns structured
state without a marker; standalone ship outputs <done>SHIPPED</done>.
Phase Flow Summary
5–8 local-review → 9 pushing → 10 ci-watch → 11 bot-watch ⇄ 12 addressing
↓
13 merging → structured result[coverage-check] runs on every final pass that changes source files.--skip-coverage can avoid work only for source-free changes.[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 returning a shipped result, every claim MUST have FRESH evidence
from THIS session — actual command output, not narrative:
- "Tests pass" →
go -C "$WORKTREE_PATH" testoutput with "ok" lines, zero failures - "Build succeeds" →
go -C "$WORKTREE_PATH" build ./...exit 0 - "Generation is current" → configured generation target exits 0 with generated changes included
- "Lint passes" → configured lint command exits 0
- "CI passes" → an exact-head
github_check_snapshotwith all registered
items terminal and successful - "Bot approvals" →
github_pr_reviewsplus the review-thread evidence
used by the bot watch - "PR merged" → validated REST merge output or required merge-queue
enqueue output
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
Return result=shipped ONLY when ALL of these are true. Embedded ship persists
that result at its child path without a marker. Standalone ship additionally
outputs <done>SHIPPED</done>:
- Local LLM review passes completed (clean or max passes reached), or a
session-local review is durably recorded asvoid/skippedwith reasonsession-boundary/headless-workerand the exact current head passes CI.
An unrecorded timeout, error, or early exit never satisfies this criterion. - Coverage verified for changed source files (or not applicable because the
diff is source-free / all changed Go files arepackage main) - 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 the
blocking evidence and stop incomplete. Do not bypass a completion criterion.
Cancel
/cancel-loop ship cleanly exits the loop.
Further Reading
All sibling files live under <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): split REST/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, REST mergeability tree, summary rendering