Ship
/ship takes a spec reference and builds it, unattended, until a final adversary
says it can go — then pushes it.
It is the lean pipeline. Where goals gates and stops at every milestone,
/ship runs one builder and one reviewer per item, checks the gate before
spending either, and stops only for trouble. goals remains available for work
that earns the ceremony.
Start /ship with the spec reference. The handoff and push happen inside
/ship.
Worker Roles
The Ship Orchestrator may spawn named Worker Roles one level deep through the host's native mechanism:
researcher— settle ordinary research and fact lookups.builder— complete each code item or isolated verification command.reviewer— report item and milestone findings without editing them.adversary— report the read-only T3 verdict.small-task— handle a bounded mechanical lookup or lane.
Workers never spawn. A builder commits only its item branch. Workers return
evidence and never write the backlog, lock, handoff, or push; the Orchestrator
alone owns those actions. Role names are host-neutral; do not use a host API,
path, or provider name as a Role identity. Use council only for a genuinely
contested fork, never for ordinary research, verification, or review.
/ship is the code review. Every item passes its locked gate and one
non-maker reviewer applying Ship's review rubric; the milestone gate
and T3 widen the same lenses. Running a separate review skill afterwards
re-reads work that has already been reviewed.
Farm brief
/ship owns the worktree-isolated build farm. It keeps this brief and the
runner beside /ship. One item equals one branch and one worktree:
git worktree add -b ship/<slug>/<id> CONTEXT/worktrees/ship/<slug>/<id>
-> contributor builds to the ladder, commits
-> ship runs the locked Verification-command
-> green? one non-maker reviewer. red? straight back, no model spent
-> merge to MAIN_BRANCH or report failure
The farm runner pins one contributor model. A model never reviews its own code.
The ladder
Pass this to every contributor, verbatim. It is the whole build brief.
Work down these rungs in order. Stop at the first one that solves it.
- Necessity — does this need to exist at all? (YAGNI)
- Codebase reuse — is it already implemented in this project?
- Standard library — does the stdlib solve it?
- Native platform — can built-in features (CSS, HTML5 inputs, DB constraints) handle it?
- Existing dependencies — use what is installed before adding anything
- One-liner — can this be a single line?
- Minimal implementation — only now, write the smallest thing that works
Never simplify away: input validation at trust boundaries, error handling that prevents data loss, security measures, accessibility, or anything the acceptance criteria explicitly asked for. Lazy is not negligent.
Tests: non-trivial logic requires at least one minimal runnable check (
assert,demo(), or a single small test). Trivial code gets none. The locked Verification-command must pass. Write nothing beyond that.No speculative abstractions — no interface, factory, or config for a single call site. Fix root causes in shared code, not symptoms in callers. Prefer the compact diff and the fewest files.
Mark deliberate trade-offs with a
ship:comment naming the upgrade path. Report as:[what you built] → skipped: [X], add when [Y].
The ladder is adapted from ponytail (MIT, DietrichGebert). Interactive sessions can invoke the plugin directly; this copy exists because worktree subagents may not load plugins.
The review rubric
Pass this to every reviewer, verbatim, after the correctness brief. Correctness, security, and data loss are judged first and keep their existing weight. What follows is the quality pass, and it has three lenses.
over-build — reinvented standard library, a dependency for what the platform already ships, an abstraction with one implementation, a factory with one product, config nobody sets, a wrapper that only delegates, a flag with one caller.
slop — a comment restating the line below it, a defensive
try/catchon a path that cannot fail, a cast that silences the compiler instead of fixing the type, nesting where a guard clause would return early, code whose shape does not match the file it was added to.structure — a new special case bolted into a flow that does not own it, feature logic living in a shared path, a bespoke helper duplicating one this repo already has, a file this diff pushed past the size the rest of the tree keeps.
One line per finding:
<file>:<line>: <lens>: <what to cut>. <replacement>.A finding is admissible when it names the replacement — a standard-library function, an existing symbol in this repo, a native platform feature, or
delete, nothing replaces it. Name the replacement and the finding stands. Without one it is an opinion about taste, and taste does not block a merge.An admissible
over-buildorstructurefinding is P1 and blocks the merge.slopis P2. Cap the quality pass at 5 findings, ranked biggest cut first, and printnet: -<N> lines possible.on its own line, orLean already.for a clean item. It goes immediately beforeVERDICT:, never afterFOLLOWUPS:: the result contract parses the last three lines, so a trailing net line displaces the verdict and invalidates the whole review.One runnable check per piece of non-trivial logic (
assert,demo(), or one small test) is the required minimum and is never anover-buildfinding. Missing trust-boundary validation, data-loss handling, security, accessibility, or an unmet acceptance criterion stays P0/P1 under-build.
The rubric is adapted from ponytail
(MIT, DietrichGebert), Cursor's deslop and thermo-nuclear-code-quality-review,
and @elithrar's simplify. It is inlined here for
the same reason the ladder is: worktree subagents may not load plugins.
Batch input
MAX_BATCH defaults to 4 and is capped at 8. /ship marks all selected ids
in-progress atomically before invoking the farm. The CLI manifest is one line
per item:
<id>|<branch>|<model>|<task prompt>
The task carries the plan/item path, exact acceptance criteria, the locked
Verification-command, MAIN_BRANCH, the absolute worktree path, and the ladder
above. Set TEST_CMDS_JSON to a complete JSON map from id to its locked
command; TEST_CMD is only accepted for a one-item batch. Reject any manifest
model other than the run's contributor pin.
The runner resolves its agent binary as AGENT_BIN, then OPENCODE_BIN, then
the bundled harness's default install path, then a PATH lookup, so any agent
CLI taking the same run --dir --agent --model --format flags works. The
flags alone are not the whole contract: a substitute CLI must also honour the
read-only permissions injected through OPENCODE_CONFIG_CONTENT for reviewer
runs, because the runner's snapshot and ref-diff check proves only that nothing
local changed — it cannot prove a reviewer caused no remote effect.
CONTRIBUTOR_MODEL sets the maker pin and COUNCIL_<NAME>_MODEL each reviewer
pin, defaulting to the pins the bundled harness profile ships. A model the
runner does not recognise needs its family declared (CONTRIBUTOR_FAMILY,
COUNCIL_<NAME>_FAMILY) so maker-is-never-checker stays provable; only the
reviewers a run actually selects constrain it. A declaration cannot reclassify
a model the runner already recognises, and under HARDENED the extra
adversary is checked the same way.
Gate-first review and merge
- Gate first. Run the locked Verification-command before spending a model. A red gate is a failure on its own — report it and move on. Nothing to review.
- One reviewer per green item, a non-maker
reviewerWorker Role, never the contributor's model. No verdict means no merge. - Review items separately, never as a batch.
- Any evidenced P0/P1 blocks even if the reviewer mistakenly prints
PASS. - Every reviewer applies the review rubric above — the
over-build,slop, andstructurelenses, the named-replacement admissibility rule, and thenet: -<N>close. HARDENED=1adds one read-onlycouncil-adversarypass on top.- Partial success is default: merge green/PASS peers and report failed ids.
STRICT_BATCH=1integrates on a temporary branch and fast-forwardsMAIN_BRANCHonly when the whole batch succeeds, so a late failure cannot leave earlier peers merged./shipalone integrates. Workers commit only their item branch; reviewers do not edit; no child updates the backlog or issue labels.
Each reviewer emits:
VERDICT: PASS | FAIL
FINDINGS: [{"severity":"P0|P1|P2|P3","file":"path:line","trigger":"...","wrong_behavior":"..."}]
FOLLOWUPS: []
For overlap, begin a no-commit merge. Keep both unique non-overlapping hunks.
For the same hunk, prefer the incoming branch only inside its declared
files_touched; otherwise prefer HEAD. Remove markers and run the locked
Verification-command. If still red, git merge --abort, mark that item failed,
and continue the wave. After a resolver edits the integration, materialize an
immutable candidate commit and rerun the review in a separate detached worktree
before committing the same tree.
Results contract
Every batch atomically writes:
{"main_branch":"...","merged":[{"name":"...","branch":"...","sha":"...","verdict":"PASS"}],"blocked":[],"failed":[],"conflicts":[]}
Every item writes a compact one-line <id>.digest.json:
{"id":"...","verdict":"PASS|FAIL","files_touched":[],"tests":{"pass":0,"fail":0,"cmd":"..."},"findings":[],"followups":[]}
/ship ingests only these files, never logs. results.json, digests, branch
ancestry, and Git are producer truth for Phase R.
Input
/ship <reference> takes an issue number, URL, or file path; fetch the spec from
that reference. If the repo has a tracker-location document, use it only as an
optional hint. With no argument, look for the most recent spec carrying the
ready-for-agent label — on GitHub that is gh issue list --label ready-for-agent --limit 1. If no tracker is reachable, ask the user for the spec reference
rather than guessing.
Derive a stable <slug> from the spec's identifier (issue number + title slug,
or the filename). The slug is the resume key — pin it in goal.md and never
recompute it from a different source.
State
/ship is the sole writer of everything under CONTEXT/ship/<slug>/.
Reviewers report findings; they never edit state.
CONTEXT/ship/<slug>/
goal.md plan, locked gates, success criteria
backlog.jsonl runtime state
handoff.md resume cursor
lock.d/owner.json
reviews/M<n>.json milestone verdicts
reviews/T3.json final adversary verdict
CONTEXT/worktrees/ship/<slug>/
results.json
<id>.digest.json
*.log
Every backlog line has at most these 12 fields: id, type, title, status,
milestone, priority, source, source_id, depends_on, acceptance,
attempts, created_by.
Use delivery/ship/scripts/state.mjs for locks, validation, ready calculation, and
atomic replacement. It is the same contract goals uses, so a backlog written
by either is readable by both. Never append or edit the backlog in place.
Mirror backlog state to GitHub issue labels when a GitHub remote is available,
but always resume from the local backlog — labels are a view, never the
source of truth. Use the same goals:* vocabulary goals uses, one label per
status, and exactly one goals:* label per issue at any time. The
namespace is shared deliberately: the two pipelines share one backlog format
and one state.mjs, so an issue's state should not be named differently
depending on which pipeline drove it.
backlog status |
label |
|---|---|
planned |
goals:planned |
ready |
goals:ready |
in-progress |
goals:in-progress |
blocked |
goals:blocked |
failed |
goals:failed |
done |
goals:done |
cancelled |
goals:cancelled |
Rules:
- Remove the old label in the same operation that adds the new one. An
issue carrying two
goals:*labels is a corrupt view; if you find one, the backlog wins and the extra labels go. - Create labels on demand, idempotently —
gh label create goals:<status> --force. Never fail a run because a label is missing. - A label write never blocks the run. No GitHub remote, no
gh, no auth, or a rate limit means skip the mirror and keep building; record it inhandoff.mdrather than stopping. - Never read labels or milestones back as state. Phase R reconciles from
backlog.jsonl,results.json, digests, and branch ancestry only. - Leave the spec's own triage label (
ready-for-agent) alone; it belongs to whoever wrote the spec, not to this run.
Milestones
Publish the plan's thematic groups as native GitHub Milestones, the same view
goals publishes. Milestone size has no fixed issue cap.
- The title is the group's name; the backlog only keys it. A backlog line's
milestoneis an id —state.mjs readymatches it againstmilestone_cursor, andreviews/M<n>.jsonis named for it. Pin each id's thematic name ingoal.mdat plan time and title the Milestone with that name, so the published view reads as the plan rather than asM1,M2. - Create or match by title, across open and closed milestones alike.
ghhas no milestone command; usegh api repos/{owner}/{repo}/milestones. A resumed run whose gate already closed M1 has to match that closed milestone — searching only open ones makes it create a second M1, which GitHub rejects as a duplicate title and which would otherwise fail the run. - Attach an item's issue whenever the item first has one.
/shipopens no issue merely to fill a milestone; an item living only in the backlog stays there. That is the lean default. - Publishing degrades exactly as the label mirror above does, and is skipped on the same conditions.
Track goal.md, handoff.md, and reviews/*.json. Add the rest —
backlog.jsonl, lock.d/, CONTEXT/worktrees/ship/<slug>/, logs, results.json,
*.digest.json — to the project's ignore rules.
Preflight
Verify the spec resolves, the cwd is a Git worktree on a named branch,
CONTEXT/worktrees/ship is writable, and every configured Worker Role is dispatchable.
Record the pre-ship merge base. MAX_BATCH defaults to 4 and may not exceed 8.
Phase R: resume first
Run Phase R before every other phase in every fresh session.
- Atomically acquire
lock.d. If it exists, stop unless its owner is proven stale. Never run two orchestrators for one slug. - If
backlog.jsonlexists, this is a resume. Never reparse the spec. - Read
handoff.md, then the backlog. - Reconcile every stale
in-progressitem fromresults.json, its digest, andgit merge-base --is-ancestor <branch> <MAIN_BRANCH>. Never infer from chat, commit subjects, or elapsed time — and never from labels, which the previous session may have died halfway through writing. - Re-mirror the
goals:*labels from the reconciled backlog, dropping any duplicate or contradictory label the interrupted session left behind. In the same pass, match each milestone by title and attach any item whose issue an interrupted session created the milestone without attaching. - Recompute ready items and enter the execution loop.
Generate one stable session id when acquiring the lock and use that same id to release it. A same-host lock whose PID is provably dead may be recovered; malformed or remote-host locks require human inspection.
Phase 0: plan once
Only when no backlog exists.
- Parse the spec into 3-7 thematic milestones and dependency-ordered
code,research, andverifyitems. Each code item is a tracer bullet — a narrow but complete path through every layer, verifiable on its own, sized for one fresh context window. - Wide refactors are the exception to tracer bullets. A wide refactor is one mechanical change — rename a shared column, retype a shared symbol — whose blast radius fans across the codebase, so a single edit breaks thousands of call sites and no slice can land green. Sequence it as expand-contract instead. Expand: add the new form beside the old so nothing breaks. Migrate: move call sites over in batches sized by blast radius (per package, per directory), each batch its own item blocked by the expand, green batch to batch because the old form still exists. Contract: delete the old form once no caller remains, blocked by every migrate batch. Forcing this into tracer bullets makes every slice red and burns the attempt cap on a mechanical rename.
- Derive one runnable Verification-command per code item and lock it in
acceptance. The spec's Testing Decisions section is prose; read it together with the repo's test config (package.jsonscripts, test runner config, CI workflow) and write a real command that exits 0 exactly when the item is done — e.g.npm test -- auth.spec && tsc --noEmit. An item with no runnable gate is not ready. - Pin
MAIN_BRANCH,WORKTREE_ROOT, the pre-ship merge base, the Worker Role roster, and the success criteria in bothgoal.mdandhandoff.md. - Present the plan and its gates once, together, for approval. Show every item with the command that will judge it. This is the only approval in the run — after it, the gates are locked and never re-asked.
- Atomically write the backlog. Then publish the plan's milestones and mirror
each item's initial state to its
goals:*label, creating no duplicates of either. Always resume from the local backlog, never from the published view.
Execution loop
Repeat until every item is done or cancelled.
- Compute ready items whose dependencies are all
done, preserving dependency order and the active milestone. Pass the handoff'smilestone_cursortostate.mjs ready; readiness never crosses a milestone boundary. - Before spawning, flip the selected code ids to
in-progress, atomically rewrite the backlog, then mirror the new state to theirgoals:*labels. The backlog write comes first: a crash between the two leaves a stale label, which Phase R corrects, whereas the reverse loses the state. - Spawn up to
MAX_BATCHbuilderWorker Roles through the farm runner for code items. Partial success is the default: one failed item must not sink green peers. Each builder commits only its item branch. - Spawn a
researcherWorker Role for eachresearchitem and abuilderWorker Role for eachverifyitem. Do not run ordinary research or verification in the parent; tests and builds are deterministic worker work. - Ingest only
results.jsonand<id>.digest.json, each at most 30 lines. Never ingest worker or reviewer transcripts;.logfiles are for humans. - Rewrite the backlog and
handoff.mdatomically after every batch, then re-mirror every changed item'sgoals:*label. A failed source id retains its attempt count. Cap attempts at 2 persource_id, then escalate.
Milestone gate
After all items in a milestone finish, run the cheap checks first and reject mechanically before spawning a reviewer: no new test failures against baseline, build passes, no conflict or TODO or stub markers, every deliverable exists.
Then spawn one read-only reviewer Worker Role for integration scope only —
whether the merged items compose, cross-item coverage gaps, and drift from the
spec, under the structure lens of Ship's review rubric: a special
case bolted into a flow that does not own it, feature logic in a shared path, a
helper duplicating one the repo already has. Item-level over-build and
slop were settled at the item gate and are not re-litigated here. Do not
reuse the same reviewer consecutively when the host exposes that choice, and
never use the maker's implementation. Record PASS, PASS-WITH-FOLLOWUPS, or
FAIL in reviews/M<n>.json and last_reviewer in the handoff. On PASS or
PASS-WITH-FOLLOWUPS, close the milestone in that same step.
Continue straight into the next milestone. A passing gate is not a reason to stop and ask.
T3: final gate
Mandatory. Spawn one read-only adversary Worker Role on the full cumulative
MAIN_BRANCH diff from the pre-ship merge base, reviewing merged paths as a
composed system under all three lenses of Ship's review rubric, and
closing with net: -<N> lines possible. Require reviews/T3.json with SHIP,
SHIP-WITH-FOLLOWUPS, or BLOCK.
Drain P0/P1 before completion; P2 may become labelled follow-up issues. A BLOCK
escalates to the human and never pushes.
Delivery
On SHIP or SHIP-WITH-FOLLOWUPS, with the locked success criteria passing
after the final merge:
- Write the session document to
$TMPDIRand update thehandoff.mdresume cursor. /push-handoff— commit and push. Invoking/shipis the push authority its Step 0 requires. Report the fetched remote SHA as proof.- Tracker closeout review. If
docs/agents/issue-tracker.mdexists, read the issues, pull requests, and commits this run produced. Post one comment on the parent spec or open PR using that file's commands (GitHub:gh issue comment/gh pr comment): SHA, T3 verdict, remaining follow-ups. Do not create tickets. Do not rotategoals:*labels. A failed tracker write is a report, not a reason to undo the push.
Release the lock.
When to stop
/ship runs to T3 unattended. It stops for exactly three things:
- T3
BLOCK— report the P0 with itsfile:lineevidence. - Attempt cap — a
source_idfailed twice. Report both failures verbatim. - A hard blocker — auth failure, unreachable Worker Role, unresolvable conflict.
On a genuinely contested call — two defensible designs and no evidence
separating them — do not decide silently. Stop and say: "Contested: . Recommend /council on this before I continue." council is only for
that contested fork, never for ordinary research, verification, or review.
When a finding is disputed rather than a design, spawn a fresh read-only
reviewer or adversary Worker Role. The user runs it; /ship resumes from
Phase R with the answer.
After at least two batches, the session may checkpoint and stop cleanly with
Resume with Phase R. Autonomy is multi-session, not infinite context. Keep
handoff.md irreducible: main_branch, worktree_root, milestone_cursor,
last_reviewer, current blocker, updated_at. Recompute everything else.