Worktree-side implementation orchestrator for an OpenSpec change. Idempotent: gates automated scenarios on filesystem reality, owns the red-test fix loop, runs the docker harness with always-teardown, then drives ship-change inline. Escape hatch writes SHIP_IT_BLOCKED.md. Runnable headless. Triggers: "ship it", "build and ship this change", "run ship-it", "implement + test + land in the worktree".
npx skills add https://github.com/BlackBeltTechnology/pi-agent-dashboard --skill ship-it
Orchestrates the implementation phase of an OpenSpec change **inside its git
worktree**. Twin of plan-proposal (which runs the planning phase on develop).
Composes existing skills — openspec-apply-change, the docker harness, and
ship-change — and adds the wiring they lack. Runnable headless.
flowchart LR
P["plan-proposal (develop)"] -->|"boundary: spawn worktree"| S
subgraph Worktree ["IMPLEMENTATION — worktree (this skill)"]
S["ship-it"] --> A["apply"] --> M["merge develop (2.5)"] --> T["docker-harness test"] --> E["enforcers (4.4)"] --> R["local review (4.5)"] --> C["ship-change"]
end
S -.->|"reverse: SHIP_IT_BLOCKED.md on design issue"| P
Pure decision logic lives in this skill's own scripts/ directory and is
unit-tested (.pi/skills/ship-it is a vitest project):
scripts/manifest.ts (parseManifest, deferDecision, filesystemRealityCheck),
scripts/no-weakening.ts (assertNoWeakening), and
scripts/review-gate.ts (reviewRoundDecision, resolveReviewer,
classifyFindings, REVIEW_TIMEOUT_MS).
.worktrees/os-<change>, branchos/<change>). Resolve the change name from the worktree dir basename.
docker available (the e2e harness); openspec CLI resolves from the parentrepo root when inside a worktree (per AGENTS.md).
<change> (worktree implementation phase)."*openspec status, but gate on filesystem reality (idempotent)Read openspec status --change <change> --json for orientation only. **Do not
trust the tasks.md checkbox** as proof an automated scenario is done — a
hand-checked or prior-partial - [x] can lie.
Parse test-plan.md (parseManifest). For each automated row, resolve its
folded test file and run filesystemRealityCheck: a scenario is satisfied only
when its test file exists AND passes in the harness (step 3). Any automated
row whose test file is absent → NOT done → author it (step 2) regardless of the
checkbox. This makes re-invocation genuinely idempotent: fresh run and re-run
reach the same all-green end state.
kb-first, even as an executor. You know which files the tasks name, so your
reflex is grep/cat/Read on them directly. The docs-first gate still applies:
before you grep/rg for a symbol, Read a file to learn its purpose, or chase
an import, run kb_search / kb agents <path> / kb_neighbors FIRST. Knowing
the file does not exempt you — kb the symbol/file, then edit.
If non-manual-only tasks remain (real code + the test files the fold tasked),
run openspec-apply-change for the change. apply writes code and marks tasks.
apply has no "copy from exemplar" capability of its own. For each folded test
task, resolve the harness-exemplar pointer (the nearest existing spec of that
category named in the task) and inject its path into the task context you hand
apply — or author the spec yourself in the fix loop (step 4). Bare
"author X.spec.ts" with no exemplar is forbidden.
develop — merge before the harness (primary integration point)The harness (step 3) is the strongest gate on the ship-it path, so integration
must sit upstream of it: the harness must validate the merged tree T1, not
the pre-merge tree T0. Merge origin/develop — the remote ref, never local
develop (checked out in the parent repo → worktree branch-collision pitfall):
git fetch origin develop
git merge --no-edit origin/develop
Idempotent: up-to-date → "Already up to date", no merge commit, safe to always
run. Merge, not rebase — step 9 squash-merges anyway, so rebase's linear
history is moot while its force-push is a documented ship-change footgun.
Conflict → abort + STOP. On an unresolved conflict, git merge --abort,
report, and do not enter the harness — never run the gate on a half-merged
tree. Resolve trivial conflicts with ship-change's existing recipes (AGENTS.md
union-keep; pnpm-lock.yaml → git checkout --theirs +
pnpm install --lockfile-only), then re-run the merge. A conflict you cannot
resolve mechanically → escape hatch (step 5).
Obtain the harness and its port from docker/test-up.sh (allocator on first run,
reuser on re-up — do NOT add port code). Read the derived port from
.pi-test-harness.json (dashboardPort) — never hardcode :18000.
Wrap the whole run so teardown ALWAYS happens (red test, abort, or a partial
test-up.sh start):
cleanup() { docker/test-down.sh || true; }
trap cleanup EXIT
docker/test-up.sh -d --build # allocates + records .pi-test-harness.json
port=$(jq -r '.dashboardPort' .pi-test-harness.json) # jq is present in the harness env
# run the relevant suite against $port:
PW_E2E_USE_RUNNING=1 npm run test:e2e # L3; or the L1/L2 suite for other levels
test-down.sh is safe against a partially-created compose project (`compose down
-v + best-effort rm`). Per-worktree compose-project isolation keeps one
worktree's leak from blocking another.
apply marks - [ ] → - [x] and never revisits a checked task, so re-invoking
apply on a red-but-checked test no-ops. **On a red test, ship-it drives the
fix itself** — edit the code or the test, re-run the harness. Never re-invoke
apply on an already-checked task.
Bound the loop by progress-making cycles, not a fixed count:
escalate (step 5). Do not spin.
No-weakening guardrail (mechanical, enforced every cycle): before accepting a
change to a test file, run assertNoWeakening on its diff
(git diff -- <test-file>). If it reports ok:false (added .only/skip,
deleted assertion, or a strong→permissive matcher swap), REJECT the change —
you may not reach green by degrading the test. Fix the code instead.
Run the enforcers from the worktree root. Each is already the sole owner of its
rule; this step only *invokes* them:
node scripts/check-conventions.mjs --base origin/develop
node scripts/dox-byte-gate.mjs
node scripts/i18n-lint.mjs --strict # --strict, else it exits 0 regardless
node scripts/i18n-parity.mjs
node scripts/knip-config.mjs # knip.json still roots every manifest entry
node scripts/knip-ratchet.mjs # per-class dead-code ratchet (~8s)
node scripts/knip-ratchet.mjs --check-baseline-diff origin/develop # ceiling not raised
The knip pair is the preventive half of the dead-code oracle: the nightly
job runs after merge and can only report. Order matters — knip-config.mjs
first, because an unrooted graph reports live files as dead, and a ratchet over
that number gates noise (measured: unrooted 723 findings / 90 unused files vs
rooted 437 / 10). Fix a ratchet failure by deleting the dead code; raising a
baseline is rejected by the third command — which must stay wired, or "never
raise the ceiling" is aspirational: the plain ratchet passes happily against a
raised number. It also rejects a DELETED class, the cheaper bypass.
They are placed here, after the harness and before the review, because they are
deterministic, offline and near-instant: a mechanically-failing tree must never
spend a model call. A non-zero exit routes to the step-4 fix loop; **step 4.5
does not run**.
--base is mandatory for gating: without it the touched set is undefined and
the Discipline-Skills and Mermaid rules report without gating. The touched set
unions the committed diff with the working tree, so fixes still uncommitted in
the fix loop are inspected.
These do NOT move into quality:changed. That script is the dev-loop oracle and
has no automated caller; the ship gate is here.
Runs on every invocation. There is no triviality escape: no diff-size, path,
or changed-file-count condition skips it.
resolveReviewer from scripts/review-gate.ts.@review is REQUIRED. Unconfigured → hard fail naming update_roles / the
dashboard Roles panel. There is deliberately **no fallback to the session
default model**: that model is the author, so falling back turns the gate into
self-review. Interactive runs may offer the bootstrap prompt; a headless run
fails.
Agent call with model: "@review"carrying review-code's rubric. Never an in-context self-review, never the
CodeRabbit CLI (that is ship-change's remote gate, later and different).
git diff origin/develop...HEAD (three-dot, so thestep-2.5 merge is not attributed to this change) plus uncommitted worktree
edits, plus proposal.md and the task text.
REVIEW_TIMEOUT_MS (300s). A timeout is neither a passnor a blocking finding — it is a checkpoint failure.
classifyFindings: only issue(blocking) re-entersthe fix loop. Everything else is reported and shipped.
reviewRoundDecision: review, fix, re-review —never a third round. This is a hard numeric cap, NOT step 4's no-progress
rule, because a reviewer can emit a fresh finding every round and each fix
changes the worktree, so a no-progress bound would never fire.
assertNoWeakening still governs every test edit a review fix makes. Afinding that can only be satisfied by weakening a test is unsatisfiable →
escape hatch (step 5), naming both the finding and the guardrail. The
guardrail is never relaxed to reach green.
Every escape decision carries a reason; write it into SHIP_IT_BLOCKED.md.
The worktree boundary is not one-way. Trigger the reverse path when EITHER:
apply reports a design issue (NL prose — implementation reveals the design iswrong), OR
Then, do NOT headlessly rewrite proposal.md/design.md:
openspec/changes/<change>/SHIP_IT_BLOCKED.md naming the failingscenario / design gap and what was tried.
plan-proposal /doubt-driven-review on develop.
Once every automated scenario is green (harness-verified), ship. Execute
ship-change's procedure inline (not as a black-box subagent) so you keep
step-level control for the teardown ordering below.
Defer rule (deferDecision, manifest-aware):
test-plan.md exists → a leftover - [ ] is deferrable only if it maps to amanual-only manifest row (inline (test-plan: manual-only) or a
(test-plan #<id>) reference resolved against the manifest). Any other leftover
= real work = STOP (return to step 2, or the escape hatch).
test-plan.md absent (legacy change) → ship-change's current keyword deferapplies unchanged.
Archive+sync gate before the destructive steps (load-bearing): do not let
ship-change merge the PR, delete the branch, or remove the worktree while the
proposal is not archived and specs are not synced. ship-change step 8.5 is that
hard gate — driving ship-change inline, hold at step 8.5 until the change is
archived (source dir moved to openspec/changes/archive/, move committed) and
specs synced. Failed/skipped archive → STOP (return to step 2 or the escape hatch),
never proceed to steps 9/10.
Teardown-before-removal ordering (load-bearing): the harness MUST be torn down
(test-down.sh, already wired via the step-3 trap) before ship-change step
10 removes the worktree. A leaked container makes the worktree "busy" and stalls
removal. Run the harness teardown, then let ship-change archive → commit → PR →
CI → CodeRabbit → (archive+sync gate) → squash-merge → remove worktree.
- [x] is never proof; thetest file must exist and pass in the harness.
apply on a checked task.assertNoWeakening rejects it.mechanically-failing tree.
@review isrequired; never fall back to the session default model (that is self-review).
no-progress bound: a model always makes "progress".
develop before the harness (step 2.5) — the strong gate validatesthe integrated tree T1; merge origin/develop (remote ref), never rebase.
Conflict → abort + STOP, never enter the harness on a half-merged tree.
ship-change step 8.5 (archive+sync gate) until it passes; failed archive → STOP.:18000 — read dashboardPort from .pi-test-harness.json.boundaries for teardown ordering).
openspec-apply-change · docker/test-up.sh + lib-ports.sh + test-down.sh ·
review-code (its rubric is what the step-4.5 reviewer applies) ·
ship-change (driven inline, manifest-aware defer). Handoff back to
plan-proposal via SHIP_IT_BLOCKED.md.
Take blackbelttechnology/ship-it from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npm.
Without those the skill loads but fails at the first command.