ship-it
Conductor skill. Runs in the main loop (interactive steps stay interactive). The only expensive part, heavy review fan-out, runs as a dynamic Workflow.
/commit ──▶ gh pr create ──▶ REVIEW (lite|heavy) ──▶ fix ALL ──▶ re-REVIEW
▲ │
└── loop ────┘ until 0
│
CI green ──▶ gh pr merge --merge
Arguments
/ship-it [lite|heavy] [pr-number]
lite(default): single-pass PR review. Cheap, fast.heavy: ce-style multi-persona fan-out + Codex cross-model peer.pr-number: skip commit/PR-create, start at the review loop on that PR.
Model routing (hard rules)
| Role | Model |
|---|---|
| Conductor, commit/PR/merge mechanics | session model |
| Review agents (both tiers) | Opus, always (Fable only if Stevie says so in chat) |
| Codex peer (heavy only) | Codex, high reasoning effort (put the literal --effort high in the peer prompt, see Step 3) |
| Applying mechanical fixes | Sonnet (fast-worker) for well-specified edits; main loop for judgment calls |
Review is the safety net: never route review to Sonnet or Haiku. lite's
single pass runs inline only when the session model is Opus-class (Opus or
Fable); on a Sonnet/Haiku session, spawn a code-reviewer agent with
model: opus instead of reviewing inline.
Step 1: Commit
Invoke the /commit skill (Skill tool). It owns cleanup, atomic staging by
named paths, signed commits, push. Do not reimplement any of it here.
If the working tree is already clean and the branch is pushed, skip ahead.
Step 2: Pull request
BRANCH=$(git branch --show-current)
git fetch origin
# Sync against the PR's OWN base, never a hardcoded main (stacked PRs exist).
# Resolution order: caller-supplied base > this branch's OPEN PR > repo default.
# `gh pr view` with no number is deliberately NOT used: it resolves a merged or
# closed PR too, handing back a stale base.
BASE="${SHIP_IT_BASE:-}"
[ -n "$BASE" ] || BASE=$(gh pr list --head "$BRANCH" --state open \
--json baseRefName --jq '.[0].baseRefName' 2>/dev/null)
[ -n "$BASE" ] || BASE=$(gh repo view --json defaultBranchRef \
--jq .defaultBranchRef.name)
git merge "origin/$BASE" # MERGE, never rebase a pushed PR branch
gh pr list --head "$BRANCH" --state open --json number,url # reuse existing PR if open
gh pr create --fill --base "$BASE" # otherwise create, onto the SAME base
Sync first: reviewing a branch that is far behind base wastes the whole loop on conflicts at merge time. Resolve merge conflicts before the first review pass. Roll new commits into an existing open PR for this branch, never open a second one. PR body: summary, test evidence, no AI attribution.
Stacking a NEW PR on a parent branch: nothing can infer that intent, so the
caller must pass it (SHIP_IT_BASE=<parent-branch>). Without it a new PR
syncs and targets the repo default branch.
Step 3: Review (tier switch)
lite
Invoke the /review skill on the PR number. One pass. Collect findings as a
severity-tagged list (P0 blocker, P1 major, P2 minor, P3 nit).
heavy
Spin up a dynamic Workflow (Workflow tool). Template:
- Persona selection is diff-aware. Always run:
correctness,project-standards. Add only when the diff touches the area:security(auth/input/secrets),tests(test files or runtime behavior),performance(hot paths, queries),data-migration(schema/persisted formats),api-contract(public interfaces, wire types). - Each persona = one
agent()on Opus (model: 'opus'), schemaless (returns markdown findings text; schemas on advisory agents trip StructuredOutput failures and abort the run). - Codex peer: one
agent()usingagentType: 'codex:codex-rescue', independent, not shown the personas' output.agentTypealone does NOT set reasoning effort.codex-rescueis a thin forwarder tocodex-companion.mjs task, which takes--effort <none|minimal|low|medium |high|xhigh>; it leaves effort unset unless the prompt explicitly asks, and it strips--effort <value>out of the task text as a routing control. SocodexPromptmust contain the literal token--effort high(NOTcodex exec -c model_reasoning_effort=..., which this agent never runs and would pass through as prose). The peer must also be framed read-only.codex-rescuedefaults to adding--write(workspace-write sandbox) unless the prompt asks for review/diagnosis only, so a peer told to "review and fix" will edit the live worktree mid-review, behind the conductor's back and outside/commitstaging.codexPromptcarries no fix mandate: say "review only, report findings, do not edit any files". - Synthesis: conductor (not another agent) merges persona + Codex findings, dedupes by file:line, tags P0-P3, promotes confidence when two reviewers agree, discards unverifiable style noise.
Workflow skeleton (adapt persona list to the diff before launching):
export const meta = {
name: 'ship-it-heavy-review',
description: 'Diff-aware persona fan-out + Codex peer for a PR',
phases: [{ title: 'Review' }],
}
// args may arrive as a JSON string depending on how the host serialises the
// Workflow args input; guard or personas.map explodes on the first line.
const A = typeof args === 'string' ? JSON.parse(args) : args
const personas = A.personas // [{key, prompt}], chosen by conductor
const thunks = personas.map(p => () =>
agent(p.prompt, { label: `review:${p.key}`, phase: 'Review', model: 'opus' }))
thunks.push(() => agent(A.codexPrompt,
{ label: 'review:codex-peer', phase: 'Review', agentType: 'codex:codex-rescue' }))
const out = await parallel(thunks) // codex runs alongside personas, no barrier between them
return { personas: out.slice(0, personas.length).filter(Boolean),
codex: out[personas.length] ?? null } // codex may be null (skipped/dead); synthesis must tolerate that
Every persona prompt must include: repo path, PR diff scope (gh pr diff N),
"report findings only, file:line, one line each, severity P0-P3, no praise,
no scope creep".
Step 4: Fix loop
Fix every finding, all severities, P3 nits included. No deferral, no "follow-up issue" unless Stevie explicitly reclassifies a finding.
Per iteration:
- Apply fixes. Mechanical, well-specified edits go to
fast-worker(Sonnet); judgment calls stay in the main loop. Sonnet output gets a quick conductor sanity pass before commit (advisor rule). - Commit fixes via
/commit(atomic, signed), push. - Re-review. Round 1 only runs the full tier (fan-out for heavy).
Rounds 2+ run a single verify-pass instead: one Opus agent that (a)
checks each prior finding is actually fixed and (b) reviews only the fix
delta (
git diff <pre-fix-sha>..HEAD). Full fan-out again only if a fix touched files outside the original review scope. This keeps the loop from multiplying heavy-tier cost by iteration count. - Zero findings: exit loop. Findings remain: iterate.
Guards:
- Max 5 iterations, then stop and escalate to Stevie with the survivors.
- Same finding survives 2 fix attempts: stop, surface it, do not silently switch strategy.
- A finding that is wrong (reviewer misread) may be rebutted once with evidence; if it reappears next round, escalate instead of re-rebutting.
Step 5: Merge
Pre-merge, verify independently (do not collapse to one signal):
gh pr checks <N> # CI per-job
gh pr view <N> --json mergeable,mergeStateStatus,reviewDecision
Evaluate every state below. A state with no matching branch is NOT green: stop and escalate rather than guessing.
CI (gh pr checks):
- Any check still pending (exit 8): not decidable yet. Wait for every check to reach a terminal state before judging CI at all.
- No checks reported at all (exit 1): the repo may have no CI, or none triggered. Do not read this as green; confirm which it is, escalate if the PR was expected to run checks.
- Red from THIS PR's code: go back to Step 4.
- Red that is pre-existing drift: prove it (git history + clean local test run on base), then present merge options honestly, do not force a cleanup commit into this PR.
mergeable:
UNKNOWN: GitHub is still recomputing after the last push. NOT green. Poll a few seconds apart, ~5 tries. StillUNKNOWNafter that: escalate to Stevie, never merge on an unresolved state.CONFLICTING: re-run the Step 2 sync against the PR's own base branch, resolve, push, re-check. Never merge a conflicting PR.MERGEABLE: green.
mergeStateStatus (fetched, so consume more than DIRTY):
CLEANorUNSTABLE: green. (UNSTABLE= non-required check failing; the CI rules above still decide whether that failure blocks.)DIRTY: conflicts, same remedy asCONFLICTING.BEHIND: base requires an up-to-date branch. Re-run the Step 2 sync.DRAFT:gh pr ready <N>first, or escalate if draft was intentional.BLOCKED: unresolved review threads or an unmet protection rule. Escalate;gh pr mergewould fail anyway.
reviewDecision:
CHANGES_REQUESTED: treat the requested changes as findings, back to Step 4.REVIEW_REQUIRED: a human approval the conductor cannot self-grant. Stop and escalate to Stevie.APPROVEDor empty string (no review gate configured): green.
All four gates green and zero findings:
gh pr merge <N> --merge # merge commit, NEVER squash
Honor the repo's landing contract before/after merge: if the project tracks
work in beads (AGENTS.md mandates it here), run bd sync, close or update
the beads this PR lands, and note the PR URL on them. Skip only in repos
without an issue-tracking contract.
Report: PR URL, merge commit SHA, iterations used, findings fixed per round.
When NOT to use
- Review-only, no shipping intent: use
/reviewor/code-reviewdirectly. - Uncommitted WIP mid-feature: finish or use
/commitalone. - Repo without a GitHub remote: nothing to PR against, stop and say so.