Ship it — definition of done
Take the current change from "code written" to "shipped", running this project's full finalize ritual. Work top to bottom. If a step surfaces a real problem, stop and report it rather than pushing broken work.
Two properties make this skill portable, and both matter more than any single step below.
It is stack-agnostic. It knows the shape of the ritual; it never knows the commands. Every command, path, glob, remote and convention comes from the project profile (step −1). Tool names in parentheses are illustrations drawn from several ecosystems, there to make a failure class concrete — never to be run because they appear here.
Every integration is conditional. This skill requires git, a POSIX shell
and the repo — nothing else. Wherever it names an MCP server, a sibling skill, a subagent or a CLI,
the rule is identical: use it only if this session actually has it, otherwise
take the stated inline fallback, and never interrupt a ship to tell the user to
install something. Decide by looking at the session's real tool and skill list,
not by making the call and catching the error. Appendix B is the full list of
what is used when present and what happens when it is not.
Step −1. Load the project profile — do this before anything else
The profile is authoritative for this repo: branch, remote and PR policy, the format command, the static-analysis and build gates, the test runner and its traps, which records must be updated, the commit style, and the failure modes this codebase actually produces. Where it contradicts anything below, the profile wins — the steps here are the shape of the ritual, the profile is the content. Appendix A is the schema; a profile that does not follow it will not answer the lookups these steps perform.
Look for it in this order and stop at the first hit:
- llmbrain, if this session has its MCP tools (a remote memory server
exposing session, search, doc and issue operations — names observed in the
wild are
start_session,search,save_doc,get_issue,update_issue; resolve the actual names from the session's tool list rather than assuming them). Open the session for this repo if the server offers it, then search its docs for this repo's ship-it profile. This is the preferred home: the profile follows the developer across machines and worktrees instead of depending on a file someone forgot to commit. .claude/ship-it.mdat the repo root —git rev-parse --show-toplevelresolves the root;${CLAUDE_PROJECT_DIR}is not a synonym for it in a monorepo subdirectory or a linked worktree, and needs a shell to expand.- Neither — build one now. Do not silently improvise a definition of done
from whatever framework or lockfile you recognise: a ship against guessed
conventions is how style debt and wrong PR bases get introduced. Instead:
- Read what the repo actually says: manifests and lockfiles, scripts/task
definitions, CI workflow files, contributor and agent instructions, the test
directory layout,
git remote -v, and the last twenty commit subjects (git log --oneline -20) for the commit convention. - Fill Appendix A's schema from that evidence.
- Show the filled profile and confirm the guesses before running anything that writes to tracked content or to the remote. Installing dependencies, generating artifacts and running read-only analysis are fine before confirmation; editing source, committing and pushing are not. Mark each field with where it came from; the fields you could not source are the ones to ask about. Where the session cannot ask (headless, scheduled, the user away), run the read-only half of the ritual and stop: steps 0 through 4 minus any fix — review, format check, analysis, tests — then report the generated profile, the findings and the assumptions, and leave the commit, the push and the records for a human. A ship against an unconfirmed profile is the failure mode this step exists to prevent.
- Persist it with the memory server's doc-save operation if present,
otherwise write
.claude/ship-it.md. Do both if both are available. - Stage the profile with this ship's commit where it is a file. It is project configuration, not scratch: teammates without the memory server read it, and an uncommitted profile does not exist for anyone else. If the user wants it personal, gitignore it and say so in the report.
- Read what the repo actually says: manifests and lockfiles, scripts/task
definitions, CI workflow files, contributor and agent instructions, the test
directory layout,
Both sources present? The memory server is the source of truth for reads. Compare the two on load, and where the file is stale, refresh it from the server and stage it with the commit — otherwise the offline copy rots into a trap for whoever relies on it. Where they conflict irreconcilably, say so and ask.
Minimum viable profile. Do not hold a first ship hostage to a complete
schema. The core is: base branch, remote name, source globs, format command,
test command, commit style, and how work reaches the base branch. Everything
else may start as Unknown and be filled the first time a step needs it. Two of
the unknowns are measurable rather than askable — the static-analysis error
baseline and the failing-test baseline — and measuring them means running the
analyser and the suite on the base branch, which is read-only. Measure them; do
not ask the user to recite them from memory. Treat Unknown at the moment a
step needs it as "establish it now, then record it", never as "assume zero".
The fields these steps read:
| Profile heading | Fields | Used in |
|---|---|---|
| Stack | bootstrap command, source globs, docs globs, generated artifacts + regen commands, components (polyglot) | 0, 1a, 2, 3, 4 |
| Branch and remote policy | remote name or "none", base branch, branch policy, protected branches, fast-moving | 0, 2, 7 |
| Format | fix command, check-only command, enforced-elsewhere | 2 |
| Static analysis, type checking, lint, build | commands, error baseline, build gate + its trigger | 3 |
| Tests | runner, filter syntax, full-suite policy, failing-test baseline, database-safety regime, fixture helpers, project-wide gate | 4 |
| Records | what to update | 5 |
| Commit style | convention, trailers, signing, staging policy | 6 |
| PR policy | PR or direct-push, forge + PR CLI, base branch, required body sections, merge policy | 7 |
| CI | exists / none / unknown, check names, expected duration | 7 |
| After the ship | post-ship steps, write-back switch | 8 |
| Review tooling | external review command + its budget, review agents this session may launch, agents that must be suggested rather than called | 1a, 1b |
| What breaks in this codebase | observed failure modes | 1b, 8 |
Polyglot repos. Where the profile describes more than one component (a TypeScript client and a Python service, say), each carries its own format, analysis, build and test commands. Run the gates of every component whose files the diff touches — plus the project-wide gate if the profile names one.
Where the profile points at CLAUDE.md, AGENTS.md, CONTRIBUTING.md or
similar for conventions, read that too.
0. Scope the change
Resolve three names first and use them everywhere below: <remote> (the
remote's name from the profile — origin is a default, not a guarantee),
<base> (the base branch from the profile — it is not main in every repo),
and <branch> (the branch being shipped, git rev-parse --abbrev-ref HEAD). Where the profile
says the repo has no remote, skip every fetch and every <remote>/<base>
reference in this step: scope the diff against the local base branch, or against
the working tree alone if there is no long-lived base.
-
Branch. Follow the profile's branch policy. Where it calls for a feature branch and you are on a protected branch, create one first (
git switch -c <type>/<short-name>). Where the repo does trunk-ish development, do not force a branch the user did not ask for. -
Sync with the remote first if the profile says the repo moves fast:
git fetch <remote>and checkgit log HEAD..<remote>/<base>. A conflicted PR cannot run CI at all:pull_request-style workflows execute against a merge ref the forge cannot build, so "no checks reported" usually means conflicted, not broken CI. -
Sanity-check the base, so the diff is not full of unrelated churn:
if MB=$(git merge-base HEAD "<remote>/<base>"); then git rev-list --count "$MB".."<remote>/<base>" else echo "no merge base — unrelated histories, stop and ask" fiThat is how far behind the branch point is. A handful of commits is normal; if it is large enough that the base has moved under the change — the profile's fast-moving flag, or churn in the same files the diff touches — rebase before reviewing, so you review the code that will actually merge. Two shapes to avoid: inlining
merge-baseas a bare substitution (when it fails, the empty result turns the range intoHEAD..<remote>/<base>and prints a plausible number for a different question), andA && B || C(a failure inrev-listwould print the unrelated-histories message and send you to the wrong stop). If local and remote histories have diverged in a way you cannot explain, stop and ask rather than improvising a reconciliation — a force-push here destroys commits. -
Determine the full feature diff, not just the last edit:
git diff <remote>/<base>...HEADplus staged, unstaged, and untracked changes (git status --porcelain,git ls-files --others --exclude-standard). New entry points, migrations, fixtures and tests are usually untracked and are easy to miss. Everything below reviews and ships the whole change, not one file. -
Bootstrap if the toolchain is absent — dependencies are frequently missing in a fresh worktree or container (
vendor/,node_modules/,.venv/, a cold module cache). Run the profile's install command before anything that needs the toolchain, and do not read a "command not found" as a code problem.
1. Adversarial review of the entire change — first, before any tooling
1a. External review pass — optional, opt-in per repo, confirmed per ship
Some teams run a third-party reviewer over the diff before the agent's own review, so its findings become prior art rather than a duplicate pass. This is off unless the profile turns it on. Three gates, in order; the first that closes ends this sub-step.
Gate 1 — is it configured? Only proceed if the profile's Review tooling section names an external review command. If it does not, skip silently: do not mention it, do not offer it, do not run one "just in case". These tools are typically rate limited per developer per hour across every repo they are used in, so spending a call on a repo that did not opt in is a real cost to the user. (CodeRabbit's CLI is the common case — a handful of reviews an hour on a paid plan, shared across all repos. Whatever the tool, treat the budget as scarce.)
Gate 2 — does this change warrant a call? Skip automatically, without asking, only when every changed path falls in the profile's docs globs — prose, changelogs, environment examples, ignore files, comment-only edits. Note in the report that you skipped and why.
Never auto-skip when the diff touches the profile's source globs, or — absent a list — anything carrying logic: application and library source, routing or handler definitions, schema and migrations, configuration that is code, infrastructure-as-code, CI workflow definitions, client-side entry points. A dependency or lockfile bump is not a docs change: it is a behaviour change you did not write, and exactly the diff worth a second reader.
Gate 3 — ask before spending the call. Ask the user, showing what the call covers and what it costs: the file count, the areas touched, and the budget. Offer run it and skip it. Continue the ship either way — declining is not a blocker and goes in the report as "skipped at your request", not as a failure. If the session cannot ask — headless, scheduled, the user away — skip and say so. Do not spend a rate-limited shared resource on a question nobody is there to answer.
When it does run, triage its output against this repo's conventions and verify each surviving finding is actually reachable before you believe it — external reviewers produce confident findings about code paths that cannot execute. Do not fix from its output directly: carry the confirmed findings into the review below as prior art, dedupe against them, and fix them there along with everything else. You are the only fixer in this run. If the pass is rate limited, misconfigured or the CLI is missing, note it and continue — it is never a reason to stop shipping.
1b. The review itself
Review the complete diff with an adversarial mindset — try to break it, not to praise it. Prefer parallel review subagents over the full diff, each with a distinct lens (correctness, silent failures, authorization, tests), and consolidate their findings. The profile's Review tooling section says which named review agents or skills this session may launch, and flags any that are user-triggered or billed — suggest those rather than calling them. Where the session has no subagents and no review skill, review inline yourself — but do not skip the review; it is the step that justifies the rest.
Form hypotheses and prove them with a failing test before fixing. A bug you cannot reproduce may not be one.
Hunt the failure modes named in the profile's "What breaks in this codebase" section first — those are observed, not theoretical. Then the standing list. Each item is a question about behaviour, not about a framework; translate it into this stack's vocabulary before answering it.
- Correctness and edge cases — null/empty/absent inputs, off-by-one, batch and pagination boundaries, operator precedence, unicode and encoding, date and timezone handling (establish whether the app stores and computes in UTC or local time — it decides windows, DST edges and off-by-one days), money and rounding, state transitions, retries that are not idempotent.
- Silent failures — swallowed exceptions, empty catch/except blocks, ignored error returns, a failed lookup treated as proof of absence, a default that lets the caller believe something worked, an async task whose rejection nobody awaits. A failure to ask is not an answer.
- Authorization — is the new route, handler, endpoint or job actually gated, and does it scope records to the caller rather than trusting an identifier from the request? Where a project has several authorization surfaces (web session, public API, admin panel, webhooks, signed URLs, background jobs, internal RPC), a change to one needs checking against the others: a secondary surface must never be more permissive than the app it fronts.
- Tenant / ownership isolation, where the project is multi-tenant — usually the bug class the product cannot survive. Whatever applies row-level scoping automatically (ORM global scopes, default managers, client middleware, an RLS policy) is bypassed by raw SQL, by query-builder validation helpers, by reporting and export paths, by admin tooling, and by any connection that authenticates as a privileged role. Check each of those, not just the happy path through the ORM.
- Transaction and consistency safety — anything that mutates money, inventory, balances or ledger/fiscal documents alongside a state change belongs in one atomic unit. Ask: if this call times out halfway, what does the record look like? Watch for work enqueued or an external call made inside a transaction that can be rolled back, and for a commit that depends on a remote system that has no compensating action.
- Database guarantees over application checks — a uniqueness, occupancy or balance rule enforced by read-then-write in application code is a race. Ask what happens when two requests run it concurrently, and prefer a constraint, a conditional write, or a lock over a check.
- Project-convention violations — the profile lists them; they are the point of having one. The shapes that recur across stacks: reading environment variables directly outside the configuration layer, missing or loose type annotations on new public functions, validation done ad hoc instead of at the declared boundary, hand-written queries where the project's data layer is the convention, N+1 access patterns and missing prefetch, and slow work done inline where the project has a queue or worker for it.
- Test gaps — happy path, failure path, and the weird path.
Fix blocking issues, or surface them clearly if they need a decision. Re-review anything you changed substantively. Keep the consolidated findings — they go in the PR body, the final report, and (step 8) back into the profile.
2. Format
Run the profile's format command in fix mode. Where the profile records both a fix command and a check-only command, the fix command is the one this step runs; a formatter that only reports leaves you to hand-apply what it found.
-
Where the profile says formatting is enforced by a pre-commit hook or by CI, run it here anyway unless the profile explicitly says the hook is sufficient. Running it twice costs seconds; discovering it in CI costs a round trip.
-
Never run a check-only pass across the whole repo. On a codebase with pre-existing style debt it reports scores of files that are not yours and buries the ones that are. Where — and only where — the profile asks for a check-only pass, scope it to the files this change touched, at every stage of the diff (committed, staged, unstaged and untracked — this step runs before the commit, so a committed-only file list is usually empty), and guard against an empty list, because given no paths most formatters default to scanning everything:
REMOTE=<the profile's remote name>; BASE=<the profile's base branch> LIST=$(mktemp); trap 'rm -f "$LIST"' EXIT G="git -c core.quotePath=false"; DF="diff --name-only -z --diff-filter=ACMR" { $G $DF "$REMOTE/$BASE"...HEAD -- <source globs> $G $DF -- <source globs> $G $DF --cached -- <source globs> $G ls-files -z --others --exclude-standard -- <source globs> } > "$LIST" if [ -s "$LIST" ]; then xargs -0 <the profile's check-only command> < "$LIST"; STATUS=$? else echo "no formattable files changed"; STATUS=0 fi # $STATUS is the formatter's verdict — report on it, do not discard itEvery part of that is load-bearing.
--diff-filter=ACMRkeeps deleted paths out (a formatter handed a nonexistent file errors);-zwithxargs -0is the only portable way to survive spaces and newlines in paths —xargs -dis GNU-only and dies on macOS, andsort -zis no better, which is why the list is not deduped: a formatter handed the same path twice does no harm.-c core.quotePath=falsestops git C-quoting non-ASCII names into"caf\303\251.txt", which the formatter then cannot open. Theif/elsematters too: do not chain the fallback with||, because a check-only formatter exits non-zero precisely when it finds violations and the||branch would turn that into a success message — and for the same reason keep the status in$STATUS, rather than letting a trailing cleanup command become the block's exit code. Drop the"$REMOTE/$BASE"...HEADline in a repo with no remote. Git pathspecs need:(glob)magic for**to match at the source root. -
Where the profile says there is no formatter, do not add one. Running a formatter across a legacy codebase buries the real diff and makes the change unreviewable. Match the conventions of the file you are editing — indentation, quoting, line width, import ordering; the siblings in the same file are the reference.
3. Static analysis, type checking and build gates
Run whatever the profile lists, and only what it lists. Where it says there are none, say so in the report and move on — do not introduce a checker mid-ship.
-
Regenerate generated artifacts first, before anything that consumes them. Typed route or client helpers, schema-derived types, ORM clients, API stubs and serialized schemas are usually gitignored, so imports will not resolve and the errors will point everywhere except the cause. The profile lists what this repo generates and how.
-
Where a static analyser or type checker is part of done, it must reach zero errors. Do not baseline it, do not add suppression comments, do not widen or cast a type to silence it.
-
Where the profile records a pre-existing error baseline, a raw count means nothing — diff the error sets:
set -e A=$(mktemp); B=$(mktemp) # never fixed /tmp names: runs collide trap 'rm -f "$A" "$B"' EXIT <analysis command> | sort -u > "$A" DIRTY=$(git status --porcelain) # sees untracked; stash create does NOT if [ -n "$DIRTY" ]; then git stash push -u -m shipit-baseline >/dev/null else echo "tree clean — this compares HEAD to base, not tree to base"; fi <regen command> # regenerate for the base run too <analysis command> | sort -u > "$B" if [ -n "$DIRTY" ]; then git stash pop >/dev/null || { echo "POP FAILED — your change is in the stash"; exit 1; } fi <regen command> comm -13 "$B" "$A" # both inputs must be sortedZero new errors is the bar. Strip line numbers from both sets first if the analyser emits them, or every shifted line reads as a new error. The dirty check is not defensive padding, and it deliberately avoids
git stash create: that ignores untracked files, so a change made entirely of new files — a new test, a new migration, a new module, which step 0 warns is the common shape — would read as "clean", compare the tree against itself, and report "no new errors" from a comparison that never happened.git status --porcelainsees them;git stash push -ustashes them.This procedure is how you establish any baseline, not just the analyser's: swap in the test command and you have the failing-test baseline step 4 relies on.
-
Run the profile's build gate when its recorded trigger condition is met — the profile names both, because what makes a build mandatory is stack-specific. The classic case is a client-side bundle: an entry point absent from the bundler manifest throws at runtime, and every test touching that route then fails with an error that looks nothing like the cause. A compiled binary or a packaged artifact has its own equivalent; the profile says which applies here.
4. Tests
- Run the affected tests first, by filter or path. Then run the full suite if the profile says the project's rules are cross-cutting — a green filter is not proof the refactor left the rest intact.
- Read the profile's database-safety note before writing a test that touches a database (where the profile says the project has none, skip this). Repos differ in whether tests run against a disposable database, a transactional fixture, or — through a misconfiguration nobody noticed — the development database itself, where a "refresh the schema" helper wipes real data. Know which regime this repo is in before you write the test, not after.
- Every change to executable code needs a test. If nothing covers it, write the test first, then run it. Use the project's own fixture and factory helpers rather than hand-rolling an object graph. A diff confined to the profile's docs globs is exempt — say so in the report rather than inventing a test for prose.
- All targeted tests must pass. If any fail, stop and report the output — do
not commit. The one exception is a failure the profile's failing-test
baseline already records as red on the base branch: confirm it fails there
too — the stash-diff procedure in step 3 is how — and name it in the report as
pre-existing. Where the profile says
Unknown, establish the baseline now and record it rather than guessing. An undeclared failure is never pre-existing — treat it as yours. - A run that prints nothing where tests were expected is a load failure, not a
pass. Distinguish it from the runner legitimately reporting that a package or
path has no tests (
no test files,collected 0 itemsfor a filter that matched nothing) — that is a coverage gap to fix under "every change needs a test", not a crash. A crash looks like silence plus a non-zero exit, or output that stops mid-collection. In any stack where the runner loads or compiles test code before executing it, something broke during that phase: an incompatible signature against a parent class or interface, a name collision, a syntax error in a shared helper or fixture file, a stale test double left behind when an interface gained a method, a bad import in an aggregating module. Read the exit code rather than the output, and bisect by running a single file. - Watch for tests that pass for the wrong reason. If an assertion about ownership or isolation would also pass with the feature removed, strengthen it. If a mock is asserted against itself, it proves nothing.
- Faked network responses must match the real payload shape. A fake built from an imagined response proves only that the code agrees with the imagination. Check the vendor's actual response — envelope, casing and field names included.
- Where the profile names a single project-wide gate (one aggregate command that runs format, analysis and tests together), that is what green means here. Per-file and changed-files-only forms are for the edit loop; they check only what you touched, and drift in a file you did not touch still leaves the gate red. This is the stated exception to step 2's "never check-only across the whole repo": where the profile names an aggregate gate, its repo-wide format check is part of the definition of green, and its noise is the project's problem to fix, not yours to scope around.
5. Update the records
Do whatever the profile's "Records" section names, and nothing else — do not invent a changelog for a project that deliberately has none. Typical shapes:
- a
CHANGELOG.md[Unreleased]entry (Keep a Changelog headings), - a spec, PRD or ADR update (record decisions and their reasons, not a changelog; if the change contradicts something the spec asserts, fix the assertion — a spec that disagrees with the code is worse than no spec),
- agent instructions (
CLAUDE.md,AGENTS.mdor equivalent), the README, and the environment example when a new env var, command, route, dependency or service landed, - API documentation or a published schema where the change is externally visible,
- nothing at all, where the PR body is the record.
Stale comments count too: if the diff makes a code comment or a docstring untrue, fix it in the same commit.
6. Commit
Follow the profile's commit style exactly — whether the project uses Conventional
Commits, whether it carries a Co-Authored-By: trailer, whether a generated-with
footer is wanted, and whether commits must be signed. These differ per repo and
getting them wrong is visible in git log forever. Where a commit-writing skill
is available in the session, invoke it and let it apply the profile's style;
otherwise hand-write the commit.
Standing rules:
- Stage what this ritual wrote. Tests, records, regenerated fixtures and
review fixes from steps 1b, 4 and 5 are frequently new files, and a commit that
omits them ships a feature with no test and a changelog that never landed.
Before committing, list untracked and unstaged paths (
git status --porcelain) and account for every one: staged as part of this change, or deliberately left out and named as such in the report. A profile generated or corrected this run (.claude/ship-it.md) belongs in the commit unless the user has gitignored it. - Beyond those, follow the profile's staging policy. The default is conservative: commit what the user staged, and do not sweep unrelated working-tree changes into the commit on their behalf.
- The body explains why, not what the diff already shows: what was wrong before, what the change makes true, and what it deliberately does not do. Where a defect was found, name the failure it would have caused in production rather than the line that was wrong.
- Do not commit unless the gates above are green.
7. Push, PR, CI
Follow the profile's PR policy — the base branch and the forge especially. Repos frequently document a promotion flow in the README that is not how PRs are actually based; trust the profile.
-
Where the repo has no remote, shipping means a clean commit on the current branch with the suite green. Do not invent a push or a PR step.
-
Where the repo pushes directly rather than through PRs, check what is about to go out before pushing:
git status -sb git fetch "<remote>" "<branch>" 2>/dev/null || true # local tracking ref may be stale if git ls-remote --exit-code --heads "<remote>" "<branch>" >/dev/null 2>&1; then git log --oneline "<remote>/<branch>..<branch>" || echo "could not compare — stop and check" else echo "branch not yet on <remote> — this push publishes its full history" fiBoth halves matter. On a first push the ref does not exist and a bare
git logis a fatal error, in exactly the case where the question is most worth asking — but testing the local tracking ref without fetching answers from a stale copy, under-reporting what the push carries when someone else has pushed since. Useif/elserather thanA && B || C, which would print "not yet on the remote" whenevergit logitself failed.If local is ahead by commits unrelated to this change, pushing publishes those too — say so and let the user decide. Pushing is outward-facing: do it when the user has asked to ship, and report exactly what went out.
-
Otherwise:
git push -u <remote> <branch>, then open the PR with the profile's forge CLI against the profile's base branch (gh pr create --base <base>on GitHub;glab,teaor the forge's web flow elsewhere). Return the PR URL. Check the CLI is actually installed and authenticated before relying on it (command -v gh, then its auth-status equivalent) — a profile naming a CLI is a statement about the project, not about this machine. Where it is missing, unauthenticated, or the profile records none for this forge, prepare the title and body, tell the user to open it in the web UI, and ask for the URL rather than guessing one. -
PR body: a short summary of the change, plus an "Adversarial review" section with the consolidated findings from step 1 — what was checked, what was fixed, and any residual risks or follow-ups you deliberately left out of scope. Then re-read the profile and add every extra PR-body section it requires — repos mandate things like a deploy-notes block, changelog bullets, a spec-status line, an impact statement, a screenshot for UI changes. Missing one is a defect, not a formatting nit. Keep the body clean: no promotional footer unless the profile asks for one.
-
Note any deploy-time step the change needs — a new env key or secret, a schema or data migration, a cache or config rebuild, an asset build, a queue or worker restart, a rewrite/redirect rule, a feature flag to flip — so it is not discovered in production. "No deploy steps" is a valid outcome, not a skipped one.
-
Where CI exists and a forge CLI is available, watch it to a verdict, paced by the profile's expected run time — use a monitoring/until-loop mechanism if the session has one, otherwise poll at a sane interval and say so. Resolve check names against the forge's live list (
gh pr checks, or the equivalent) rather than the profile's recorded names, which may have been renamed since; the profile's list tells you how many checks to expect, the live list tells you what they are called. Without a CLI there is no live list and no watch: say so, hand over the PR URL, name the checks the profile expects, and end the ship there rather than claiming a verdict you cannot see. Where the profile says there is no CI, say so in the report: the gates above were the only gates, and nothing will catch a mistake after the push. Where the CI field is unfilled, look for the pipeline definition yourself (.github/workflows/,.gitlab-ci.yml,.circleci/, a Jenkinsfile, the forge's own config) and say what you found — do not assume either way. -
Merge policy comes from the profile; its default is: do not merge unless the user asks. Where a repo genuinely auto-merges on green, the profile says so and that is what to follow. If a merge command is permission-gated and denied, stop and hand the decision back rather than routing around it.
8. After the ship
Do whatever the profile's "After the ship" section names — a public changelog entry, an in-app announcement, a tracker ticket moved, deploy notes filed. Where a skill that owns one of these decides for itself that the change is internal-only and writes nothing, that is a valid outcome, not a failure. Where the profile names a post-ship skill this session does not have, say so in the report instead of improvising its job.
Then feed the profile, unless its write-back field says no. A definition of done that never learns is worth less each month. What to add:
- Grow "What breaks in this codebase." If step 1 found a defect, or a gate failed in a way the profile did not warn about, append one line describing the observed failure — what it was, where it hides, how it presented. Observed failures only; do not fill the section with theory, and do not duplicate a line that is already there.
- Correct what the profile got wrong. A command that did not exist, a base branch that was stale, a test regime that turned out different: fix the field and say in the report that you did.
- Close the loop on tracked work. If the change resolves an issue the memory server is tracking, update it with the commit or PR reference. Persist with the memory server's doc-save operation where one is present; that write is out-of-band and can happen at any point.
Where the profile is a file, the timing matters, because step 6 has already
committed and step 7 has already pushed. Do not amend a pushed commit.
Either make the profile edits before step 6 — everything above is known by the
end of step 4, so this is usually possible and is the tidier outcome — or land
them as a small follow-up commit of their own (chore: ship-it profile learnings), push it alongside, and say in the report that you did. Never leave
the learnings uncommitted in the working tree: the next ship starts from the
profile, not from your memory of this one.
Output
A short report covering, in order: where the profile came from (memory server, file, or generated this run), the review verdict (what you tried to break, what actually broke, what you fixed, residual risks), format result, static analysis and build gate results relative to the baseline where one exists, test counts and whether the full suite ran, which records were updated, whether a schema or data migration was applied (or none), the commit hash and subject, what was pushed or the PR URL, CI status (or that no forge CLI was available to read one), the outcome of any post-ship step, and what you wrote back to the profile.
Then say plainly what is not done: anything deferred, any assumption the change rests on, any behaviour change that is not a no-op, any file you left uncommitted, and anything that needs a decision from the user. A feature reported as finished must actually be finished.
Appendix A — the project profile schema
This is what step −1 looks for, and what it generates when nothing exists. Store
it as an llmbrain doc, or as .claude/ship-it.md at the repo root, or both.
Filling rules: fields marked [core] are the ones a first ship cannot run
without; everything else may start as Unknown and be filled the first time a
step needs it. Write real commands, copy-pasteable, exactly as they should be
run. Never leave a field as a guess — "Unknown — check <where>" is a legitimate
value that tells the agent to look, while a wrong value tells it to act. Where
the repo genuinely has none of something, choose the explicit none option: a
"none" is a decision the skill reports, a blank is an unfilled field it has to
work around. Keep every heading, including ones whose answer is "none". Where the
repo is polyglot, repeat the Format / Static analysis / Tests fields per
component under ### <component name>.
# Ship-it profile — <project name>
## Stack
- Components: **single** | <name → path, one line each, for a polyglot repo>
- Language / runtime and version: <e.g. PHP 8.3 / Node 22 / Python 3.12 / Go 1.23>
- Framework(s): <...>
- Package manager(s): <...>
<!-- The three lines above are orientation for a reader, not inputs the skill
executes. Everything below this point is read by a specific step. -->
- Bootstrap from cold (fresh clone or worktree): <install command(s)>
- **Source globs** [core] — what counts as executable code here. Used to scope
formatting and to decide whether a diff is code or prose. Git pathspecs need
`:(glob)` magic for `**` to match at a source root:
<e.g. ':(glob)app/**/*.php' ':(glob)routes/**' ':(glob)resources/js/**'>
- **Docs globs** — the inverse: paths that are prose, not behaviour. A diff
confined to these skips the external review pass and the write-a-test rule.
Lockfiles do **not** belong here:
<e.g. ':(glob)docs/**' '*.md' '.env.example' '.gitignore'>
- Generated artifacts, and how to regenerate them (typed route/client helpers,
schema-derived types, ORM clients, API stubs — usually gitignored, and a stale
one makes type checking lie):
- <artifact> → <regen command>
- or **none**
## Branch and remote policy
- **Remote name** [core]: origin | <other name> | **none — local only**
- Base branch [core]: <name> (not necessarily `main`)
- Branch policy: <feature branches, naming convention> | <trunk-ish, commit directly>
- Protected branches: <...>
- Does the repo move fast enough to require a fetch + rebase check before
shipping? <yes / no>
## Format
- Fix command [core]: <command> — or **no formatter; match surrounding file style**
- Check-only command: **not wanted** | <command, run against changed files only>
- Enforced elsewhere: **no** | <pre-commit hook / CI job — and whether that is
sufficient, or the skill should still run it locally>
## Static analysis, type checking, lint, build
- Commands, in the order they should run:
1. <command>
2. <command>
- or **none — this repo has no static analysis gate**
- Pre-existing error baseline: **none, must be zero** | **Unknown — measure it**
| <N errors on the base branch — diff the sorted error sets, zero NEW errors is
the bar; strip line numbers first because <analyser> emits them>
- Build gate: **none** | <build command>, required when <trigger condition —
e.g. a new client-side entry point, any change under cmd/, a new package>
## Tests
- Runner and full-suite command [core]: <command>
- Filter / single-file syntax: <command --filter X> / <command path/to/file>
- Must the full suite run on every ship, or is a filtered run enough?
<full suite — the project's rules are cross-cutting> | <filtered is enough>
- **Database safety regime** — read before writing any test that touches a DB:
**no database in tests** |
<disposable test database, configured at <file>> |
<transactional fixtures> |
<WARNING: tests run against the development database; do NOT use the
schema-refresh helper — it wipes real data>
- Fixture / factory helpers to use instead of hand-rolling data: <...>
- **Failing-test baseline** — tests already red on the base branch, so a ship is
not blamed for them and a genuinely new failure is not waved through:
**none, the suite is green on <base>** | **Unknown — measure it** |
<test names / files, and why>
- Single project-wide gate (the one command that defines green here):
<command> — or **none; the individual gates above are the definition**
## Records
Give every line an explicit **yes** or **n/a** — keep the lines that do not
apply rather than deleting them, so the skill can tell "decided against" from
"not filled in":
- CHANGELOG.md [Unreleased] entry, Keep a Changelog headings: <yes / n/a>
- Spec / PRD / ADR at <path> — decisions and their reasons; fix any assertion
the change contradicts: <yes / n/a>
- CLAUDE.md / AGENTS.md, when a command, convention, route or package changed: <yes / n/a>
- README.md, when setup, commands or surface area changed: <yes / n/a>
- .env.example / <config sample>, when a new env var or secret landed: <yes / n/a>
- API docs / published schema at <path>, when externally visible: <yes / n/a>
- **Nothing — the PR body is the record**: <yes / n/a>
## Commit style
- Convention [core]: <Conventional Commits: type(scope): subject> | <other — describe>
- Trailers: Co-Authored-By: <yes / no> · generated-with footer <yes / no>
- Signed commits required? <yes / no>
- Staging policy beyond the files the ship itself wrote (those are always
staged): <only what the user already staged> | <stage all tracked changes>
## PR policy
- How work reaches the base branch [core]: <pull request> | <direct push> — this field
decides which branch of step 7 runs; keep it consistent with the forge line
- **Forge and PR CLI**: <GitHub — gh> | <GitLab — glab> | <Gitea — tea> |
<self-hosted <forge>, no CLI: prepare the body, ask the user to open it> |
**n/a — direct push, no PRs**
- PR base branch: <name> — note explicitly if the README's promotion flow
differs from how PRs are actually based
- Required PR-body sections beyond summary + Adversarial review:
- <e.g. Deploy notes> / <e.g. Changelog bullets> / <e.g. Screenshots>
- or **none**
- Merge policy: **never merge without asking** (default) | <auto-merge on green,
and under what conditions>
## CI
- <Exists: <pipeline file>, checks: <names> — names may drift, resolve them
live; this list is for knowing how many to expect> |
**No CI — the local gates are the only gates** |
<Unknown — inspect the pipeline definition and report what you find>
- Expected time to a verdict: <e.g. ~6 min — pace the watch loop accordingly>
## Review tooling
- External review command: **none** | <CLI command, e.g. a CodeRabbit CLI review
of the working diff> — plus its rate limit, so the skill can tell the user what
a call costs
- Review agents/skills this session may launch: <names> | **none — review inline**
- Any review skill that is user-triggered or billed and must be suggested rather
than called: <names> | **none**
## After the ship
- <e.g. a public-changelog skill — it may decide the change is internal-only and
write nothing; that is a valid outcome>
- <e.g. move the tracker ticket to In Review>
- Write the profile back with what this ship learned: <yes> | <no>
- or **nothing**
## What breaks in this codebase
<!--
The highest-value section, and the one step 8 grows. Observed failure modes,
not theory — the bugs this repo has actually shipped, the traps that have
actually cost an afternoon.
-->
- <e.g. Tenant scoping is applied by an ORM global scope; validation helpers and
the reporting exports bypass it and have leaked rows twice.>
- <e.g. Date columns are stored in local time while the app computes in UTC —
every window query is off by one day near a DST boundary.>
- <e.g. A test run that prints nothing means a load-time fatal, usually a
signature change against an interface an old test double still implements.>
Appendix B — optional integrations
Nothing here is required. For each, the rule is: use it if the session has it, otherwise take the fallback and note it in the report where it changed what was checked. The one deliberate exception is the external reviewer: a repo that never configured one is never told about it (step 1a, gate 1), because the option costs a rate-limited call the user did not ask to spend. Never pause a ship to ask the user to install something.
| Capability | When present | When absent |
|---|---|---|
| llmbrain (or any memory/doc MCP server) | Profile is read from and written back to it (steps −1, 8); tracked issues get closed with the commit/PR reference | .claude/ship-it.md is the profile; step 8's learnings go into that file, committed before step 6 or as a named follow-up commit — never as an amend of a pushed commit |
| External review CLI (CodeRabbit or similar) — not named in the profile | — | Step 1a is skipped silently: not mentioned, not offered. Step 1b is the whole review |
| External review CLI — named in the profile but rate-limited, unauthenticated or missing | Step 1a runs it, subject to its three gates; confirmed findings become prior art for step 1b | Note it in the report and continue — never a reason to stop shipping |
| Review subagents | Step 1b fans out parallel reviewers with distinct lenses and consolidates | Step 1b is done inline, in one pass, against the same checklist |
| A review skill that is user-triggered or billed | Suggest it to the user; do not launch it | — |
| A commit-writing skill | Step 6 delegates to it, passing the profile's style | Step 6 hand-writes the commit in the profile's style |
A forge CLI (gh, glab, tea) — verify with command -v plus an auth check, not by trusting the profile | Step 7 pushes and opens the PR, returns the URL, and can read CI | Step 7 pushes, prepares title and body, asks the user to open the PR and to hand back the URL; CI cannot be watched — say which checks the profile expects and end the ship there |
| A monitor / until-loop mechanism (and a forge CLI) | Step 7 watches CI to a verdict without polling | Step 7 polls at an interval paced by the profile's expected run time, and says so; with no forge CLI there is nothing to poll |
| A post-ship skill (public changelog, announcement) | Step 8 invokes it; "internal-only, nothing written" is a valid outcome | Step 8 reports that the profile expects one and this session cannot run it |
| The ability to ask the user | Gate 3 of step 1a asks before spending a rate-limited call; step −1 confirms a generated profile | No external review call is made; and with an unconfirmed generated profile the ship runs its read-only half only (steps 0–4, no fixes), reports, and leaves commit/push/records to a human |