# Ship

> Push, open a PR, and watch CI; land mode merges and cleans branches. Triggers "ship it", "create PR", "babysit CI", "land it", "fix CI and merge".

- Skill: `darkroomengineering/ship` (Agent Skill)
- Install (CLI): `npx skillmds@latest add darkroomengineering/ship`
- Raw SKILL.md: https://api.skillmd.com/api/skills/darkroomengineering/ship/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: darkroomengineering (https://skillmd.com/u/darkroomengineering)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/darkroomengineering/ship

---


# Ship Pipeline

## Standalone Codex

Skip Claude's `!command` interpolation below. Run `git branch
--show-current`, `git status --porcelain`, and `git log --oneline -5`
explicitly. Create a new reviewer with `spawn_agent`, continue it while live
with `send_message`, trigger another turn once it is idle with `followup_task`,
wait with `wait_agent`, and stop its current turn with `interrupt_agent` only
when necessary. Never spawn `codex-verifier` and never run `codex-run.ts` from inside Codex.

Writers share the working tree unless the live host explicitly offers
isolation. Assign non-overlapping ownership and serialize any implementer and
test-writer fixes; only read-only reviewers may overlap. Codex implementers are
not promised Claude worktree isolation. Codex also skips TLDR and uses
`git diff --name-only`, direct import searches, and test-name searches to choose
affected tests.

You are in **Maestro orchestration mode**. Execute the shipping checklist in order.

## Claude current state
- Branch: !`git branch --show-current 2>/dev/null || echo "unknown"`
- Working tree: !`git status --porcelain 2>/dev/null | head -20`
- Recent commits: !`git log --oneline -5 2>/dev/null || echo "no commits"`

## Pipeline (All Steps Mandatory)

### Step 0: Preflight
```bash
ssh-add -l
```
If no identities loaded: **Unlock 1Password and retry — git signing will fail otherwise.** Agent drops mid-flow are a recurring cause of failed pushes; catch it now, not after the commit.

Read and validate `package.json` before choosing commands. Invalid JSON or invalid
script declarations block the pipeline. Do not commit until all applicable gates
in Steps 1–4 (typecheck/build/test/lint) are green; record an absent build script
as N/A with its reason. Skipping ahead is where amend churn comes from — `--amend`
to fix a lint error you would have caught in 30 seconds invalidates signatures and CI runs.

### Step 1: Type Check
```bash
bunx tsc --noEmit
```
If errors: fix them. Do not proceed until clean.

### Step 2: Build

Inspect the validated manifest: if `scripts.build` is a nonempty string, run:

```bash
bun run build
```
If the configured build fails, fix it; do not proceed until clean. If `build` is
absent, record **N/A — no build script** and continue with the applicable
typecheck, test, and lint gates. An empty, whitespace-only, or non-string build
declaration is invalid and blocks the pipeline. Do not add a dummy build script
or silently replace a failing build with typecheck. A proof runner that only
detects typecheck/test/lint does not replace this configured-build gate.

### Step 3: Test (if tests exist)

**3a. Affected tests first.** In Claude, use TLDR when available. Standalone
Codex uses the native searches in its host branch. Run only the tests touched
by your changes before the full suite.

```bash
tldr change-impact --project . 2>/dev/null
```

If TLDR returns a list, run those tests first (`bun test <file>` or `vitest run <file>`). If they fail, fix before the full run — don't waste cycles on the rest.

**3b. Full suite.**

Select one configured runner before executing it. Never treat a second runner as a fallback after a
real test failure:

```bash
if bun -e 'const p = await Bun.file("package.json").json(); process.exit(typeof p.scripts?.test === "string" ? 0 : 1)'; then
  bun run test
elif bun -e 'const p = await Bun.file("package.json").json(); process.exit(p.devDependencies?.vitest || p.dependencies?.vitest ? 0 : 1)'; then
  ./node_modules/.bin/vitest run
else
  bun test
fi
```

If no tests or runner are configured, record the evidence and skip this step. If the selected
runner fails, stop and fix that failure. Do not run a narrower suite to turn a real failure green.

### Step 4: Lint
```bash
biome check .
```
If errors: fix or justify.

### Step 5: Web Quality Gate
Quick sanity check before review:
- [ ] No `loading="lazy"` on above-fold/LCP images
- [ ] Images have explicit dimensions or `fill` prop
- [ ] No `console.log` left in production code
- [ ] Meta tags present on new pages (`title`, `description`, `canonical`)
- [ ] Structured data valid on new content pages

If issues found: fix them before proceeding to review.

### Step 6: Review Changes
Spawn `reviewer` agent:
```
Agent(reviewer, "Review all staged changes for quality, TypeScript strictness, a11y, and performance issues.")
```

**Standalone Codex branch:** follow the native lifecycle above and use a fresh
read-only `reviewer`. Skip the Claude bridge branch below.

When the Codex bridge is available, run a cross-model review **in parallel** — a different model family catches what Claude self-review misses, and this is right before a commit lands, the cheapest place to catch a Critical:

```
Agent(codex-verifier, "Cross-model review of the staged diff. Report findings by severity.")
```

If Claude and Codex disagree on a Critical/HIGH finding, surface it to the user as a gate **before** Step 7 — don't auto-commit through a cross-model disagreement. The bridge is gated and fails open: if Codex is unavailable, proceed with the Claude review alone.

If the `codex-verifier` spawn fails, or it reports that Bash was stripped (forked skill contexts), run `bun "$HOME/.claude/src/scripts/codex-run.ts" review` directly instead — never skip the cross-model pass.

### Step 7: Commit (Bisectable)

Analyze the diff to decide commit strategy:

```bash
git diff --cached --stat | tail -1
```

**Small diff** (<50 lines changed across <4 files): Single commit.
```bash
git add <relevant files>
git commit -m "<type>: <description>"
```

**Larger diff**: Split into ordered commits by dependency layer. Each commit must be independently valid — no broken imports, no forward references.

**Commit order (skip layers with no changes):**
1. **Infrastructure** — config files, env changes, package.json, build config
2. **Types/interfaces** — type definitions, schemas, shared interfaces
3. **Logic** — utilities, hooks, services, lib code (group with their tests when small)
4. **UI** — components, pages, styles (group with their tests when small)
5. **Tests** — remaining test files not already grouped with their subjects
6. **Meta** — docs, changelog, version bumps — **always last commit**

**Per-commit validation:** Each commit must independently pass:
```bash
npx tsc --noEmit && biome check .
```
If a commit would break either check in isolation, merge it with the next commit in the sequence.

**Commit messages:** Each gets a conventional prefix (`feat:`, `fix:`, `refactor:`, `test:`, `chore:`, `docs:`).

### Step 8: Push and PR
```bash
git push origin HEAD
```

If `gh` is not available, provide the push command and instruct the user to create the PR manually.

**Author the PR body — do NOT use `--fill`.** `--fill` dumps the commit messages
into the description, which reads as technical and over-engineered. Lead with a
plain-English "What this does" (see `rules/git.md` "Signal, not spam"):

```bash
gh pr create --title "<type>: <concise, plain-English title>" --body "$(cat <<'EOF'
## What this does
<2–3 plain sentences: what changed and why it matters — the real-world effect,
not the mechanism. A teammate who didn't write it should get it on one read.>

## Summary
- <technical bullet — the how>

## Test Plan
- [ ] <how it was verified>
EOF
)"
```

Write "What this does" from the *diff and its purpose*, not by pasting commit
subjects. If you can't explain it plainly, the change is unclear — say so, don't
reach for bigger words.

### Step 9: Watch CI Until Green (post-push)

After `gh pr create`, watch the PR-attached checks until all pass. Use `gh pr checks` as the source of truth — it covers all PR checks, not just GitHub Actions runs (which `gh run list` would miss).

```bash
gh pr checks --json name,bucket,state,workflow,link
gh pr checks --watch --fail-fast
```

If a check fails:

1. Identify the failing job and fetch logs (`gh run view RUN_ID --log-failed` for GHA; follow the `link` for external services).
2. **Reproduce the exact failing guard locally against the real built artifact** before pushing a fix — run the same test script / check command CI ran, against the binary or output CI would see. A fix verified only by reasoning is a push-and-pray loop.
3. Apply the smallest fix. Use the `fix` skill's "Variant: Failing PR CI" if the failure is non-trivial. For version/metadata guards, use the repo's canonical script (`bump-version.sh`-style) — never hand-edit versioned fields; hand-bumps desync the sibling fields the script maintains.
4. Push, then re-read `gh pr checks` — the check set can change between runs.
5. Repeat until green.

Guardrails:

- Scope each fix to a single failure cause.
- If failures are flaky, retry once and report flake evidence rather than chasing a phantom fix.
- If the failure is unrelated to the PR and already fixed on main, merge main into the branch instead of bloating the PR.
- Never take a watcher pipeline's exit code as the verdict — in `gh run watch ID; gh run view ID ...` the shell reports the *last* command's exit, not the run's outcome. Read the conclusion explicitly: `gh run view ID --json conclusion --jq .conclusion` (or `gh pr checks`).

### Land Mode (existing PR → merged → clean)

Invoked as `/ship land` or when the ask is "review/fix CI then merge then clean up" on a PR that already exists. Skip Steps 0–8; run:

1. Capture the head you're validating: `gh pr view --json headRefOid,reviewDecision,mergeable`. Everything below is checked against THIS `headRefOid`.
2. `gh pr checks` — if red, run the Step 9 CI-fix loop until green. A CI fix that pushes a new commit moves the head: re-capture `headRefOid` and re-check against it.
3. Confirm review state from step 1: if `reviewDecision` is CHANGES_REQUESTED or `mergeable` is CONFLICTING, stop and report — do not merge through either.
4. **Re-validate immediately before merging** — `gh pr view --json headRefOid,reviewDecision,mergeable`. All three can drift while the CI loop ran (a new push moves `headRefOid`; a late review flips `reviewDecision` to CHANGES_REQUESTED; a base-branch change flips `mergeable`) — the head moving is not the only race. If `headRefOid` differs from the SHA you validated, STOP and re-run checks against the new head; if `reviewDecision`/`mergeable` regressed, STOP and report. Don't merge stale-green. (A post-*merge* SHA check can't catch a moved head: a squash commit does not contain the PR head as an ancestor, so the guard has to run *before* merge. See the `gh-merge-race` learning.)
5. Merge with the repo's preferred strategy (`gh pr merge --squash --delete-branch` unless repo convention says otherwise). Branch protection / a merge queue, if enabled, is the atomic final gate.
6. Cleanup: `git checkout main && git pull`, delete the local branch, prune remotes (`git remote prune origin`). Leave only main unless other branches have open PRs.
7. Report: PR link, merge commit, branches deleted.

Per the Autonomy Contract (CLAUDE-FULL.md): steps 5–7 are pre-approved once CI is green, the head is confirmed unchanged, and review is clean — report, don't ask. External-org repos: land mode is forbidden entirely.

## Rules
- NEVER skip an applicable type check or configured build; an absent build script is N/A with a recorded reason
- NEVER create a PR with failing tests
- Conventional commit messages only (`feat:`, `fix:`, `refactor:`, etc.)
- No AI attribution in commits or PR descriptions — see `rules/git.md`.
- Each bisectable commit must pass `tsc --noEmit` AND `biome check` independently
- If total diff is small, single commit is fine -- don't over-split

## Output
Return:
- **Build status**: Pass/Fail/N/A (N/A requires the reason: no build script)
- **Test status**: Pass/Fail (with count)
- **Lint status**: Pass/Fail
- **Review status**: Approved/Needs Changes
- **Commits created**: Count and summary of each
- **PR link**: URL if created

