[object Object]

← back to Cli Printing Press

feat(cli): add printing-press shipcheck umbrella + Phase 4 enforcement + polish-worker hook (#353)

494abe48c4c3dd5cb1fb26a6e5321c4f50939776 · 2026-04-27 15:07:25 -0700 · Trevin Chow

* feat(cli): add shipcheck umbrella command for canonical Phase 4 sweep

Adds `printing-press shipcheck --dir <cli> [--spec <path>] [--research-dir <path>]`
that runs all five Phase 4 verification legs in canonical order: dogfood, verify
(with --fix), workflow-verify, verify-skill, scorecard (with --live-check).

Each leg runs as a subprocess of the same printing-press binary (resolved via
os.Executable, with symlinks evaluated). Output streams to the operator's
terminal in real time; a per-leg verdict table prints at the end. The umbrella
exits non-zero when any leg fails, with the exit code reflecting the largest
non-zero leg code (preserves the most serious failure).

Run-all-legs semantics (no fail-fast): operators want to see all gaps in one
pass rather than iterate one-leg-at-a-time. Existing standalone leg commands
remain unchanged.

The legs slice in shipcheck.go is the single source of truth for which legs
run; adding a future leg = append one entry with an argv builder closure.

Tests in shipcheck_test.go use a stub binary
(internal/cli/testdata/shipcheck-stub/main.go) that mimics the leg surface
and is configurable via STUB_EXIT_<LEG> env vars. Coverage:
  - all legs pass -> umbrella exits 0
  - one leg fails -> umbrella exits with that leg's code, all 5 still ran
  - multiple failures -> umbrella exits with max code
  - default argv: verify gets --fix, scorecard gets --live-check
  - --spec and --research-dir pass through to the legs that accept them
  - missing/invalid --dir -> ExitInputError before any leg runs

Implements WU-1 from retro mvanhorn/cli-printing-press#351 (the company-goat
retro). The Phase 4 prose update and polish-worker hook follow in subsequent
commits.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(cli): add shipcheck umbrella implementation plan

Implementation plan for WU-1 from retro mvanhorn/cli-printing-press#351.
Documents the four implementation units (shipcheck command, --json + flag
pass-through, Phase 4 skill prose update, polish-worker hook), the
backwards-compat constraints, and the subprocess execution model decision.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli): add shipcheck --json envelope and selective flag pass-through

Adds five flags to the umbrella so it covers the canonical Phase 4
recommended invocation plus common operator overrides:

  --json           Emit a structured envelope at end-of-run; suppresses
                   per-leg stdout so the envelope can be piped to jq
                   without separation. Operators wanting per-leg detail
                   in JSON mode run the leg directly with --json.
  --no-fix         Disable verify's --fix auto-repair (read-only verify)
  --no-live-check  Disable scorecard's --live-check live-API sampling
  --api-key STR    Pass --api-key to verify (read-only GETs only)
  --env-var STR    Pass --env-var to verify (e.g., GITHUB_TOKEN)
  --strict         Pass --strict to verify-skill

The JSON envelope shape is { passed, exit_code, started_at, elapsed_ms,
legs: [{ name, exit_code, passed, started_at, elapsed_ms, command }] }.
Each leg's `command` field shows the full argv as it would be invoked at
the shell, so an operator can copy-paste-rerun a specific failing leg.

In --json mode, leg stdout/stderr are discarded (io.Discard). This
trade-off keeps both consumer modes simple: human mode streams real-time
output then prints a summary table, JSON mode emits a single clean
envelope. R4 in the plan ("legs stream their full output") applies to
default mode; --json explicitly opts into structured output instead.

Test coverage extends from 9 cases (U1) to 14 (U2):
  - --no-fix omits --fix from verify argv
  - --no-live-check omits --live-check from scorecard argv
  - --api-key and --env-var pass through to verify only (other legs
    do not receive them)
  - --strict passes through to verify-skill only
  - --json envelope is parseable, has the expected shape, and reflects
    pass/fail correctly for both all-pass and one-failure cases

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(skills): collapse Phase 4 to single shipcheck invocation

Replaces the 5-command code block in /printing-press Phase 4 with the
new umbrella:

  printing-press shipcheck \\
    --dir "$CLI_WORK_DIR" \\
    --spec <same-spec> \\
    --research-dir "$API_RUN_DIR"

Per-leg interpretation prose, fix order, and ship-threshold criteria are
preserved — they remain useful when an operator needs to understand why
a specific leg's verdict failed. Adds a new `shipcheck exits 0` clause
at the top of the ship-threshold list as the canonical umbrella signal.

The skill now also documents the recommended Phase 4 flag surface
(--no-fix, --no-live-check, --json, --api-key, --env-var, --strict) and
calls out that focused iteration on a single leg is supported via the
standalone command, but the canonical Phase 4 sweep is the umbrella.

Closes the gap that bit company-goat PR #140: the previous prose listed
five commands as a code block, and an operator running three of them
(skipping verify-skill) shipped 14 verify-skill errors to the public
library CI. The umbrella makes that failure mode structurally
impossible.

Implements WU-3 from retro mvanhorn/cli-printing-press#351.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(skills): polish-worker runs verify-skill + workflow-verify; gates ship_recommendation

Adds verify-skill and workflow-verify to the polish-worker's Phase 1
diagnostic sweep, captures their --json output to /tmp files, and adds
two new Categories table rows so findings flow into Phase 2 fixes.

Phase 2 gains a Priority 4.5 fix-loop section documenting the three
verify-skill check classes (flag-names, flag-commands, positional-args)
and their canonical fixes:
  - flag-commands: inline cmd.Flags().StringVar(...) in the affected
    command (the verify-skill grep cannot follow shared-helper
    indirection)
  - positional-args: change Use: "cmd <arg>" to Use: "cmd [arg]" when
    the command also accepts the value via --flag
  - flag-names: fix the SKILL example or restore the deleted flag

Phase 3 re-diagnose is updated to include the two new legs.

Phase 4 ship_recommendation logic is hard-gated on verify-skill exit 0
AND workflow-verify not reporting workflow-fail. A CLI that ships with
a SKILL that lies about it gives agents broken instructions; a CLI
whose primary workflow fails verification has not actually shipped.
Both conditions are required for `ship` and `ship-with-gaps`; failing
either drops to `hold`.

Closes the polish-worker side of the gap that bit company-goat PR #140:
even if Phase 4 shipcheck (the parent fix in feat/cli) is run, polish
runs may still ship verify-skill mismatches if they don't run the same
check. Now polish runs both legs and refuses to recommend ship while
findings exist.

Implements WU-4 from retro mvanhorn/cli-printing-press#351.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 494abe48c4c3dd5cb1fb26a6e5321c4f50939776
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Mon Apr 27 15:07:25 2026 -0700

    feat(cli): add printing-press shipcheck umbrella + Phase 4 enforcement + polish-worker hook (#353)
    
    * feat(cli): add shipcheck umbrella command for canonical Phase 4 sweep
    
    Adds `printing-press shipcheck --dir <cli> [--spec <path>] [--research-dir <path>]`
    that runs all five Phase 4 verification legs in canonical order: dogfood, verify
    (with --fix), workflow-verify, verify-skill, scorecard (with --live-check).
    
    Each leg runs as a subprocess of the same printing-press binary (resolved via
    os.Executable, with symlinks evaluated). Output streams to the operator's
    terminal in real time; a per-leg verdict table prints at the end. The umbrella
    exits non-zero when any leg fails, with the exit code reflecting the largest
    non-zero leg code (preserves the most serious failure).
    
    Run-all-legs semantics (no fail-fast): operators want to see all gaps in one
    pass rather than iterate one-leg-at-a-time. Existing standalone leg commands
    remain unchanged.
    
    The legs slice in shipcheck.go is the single source of truth for which legs
    run; adding a future leg = append one entry with an argv builder closure.
    
    Tests in shipcheck_test.go use a stub binary
    (internal/cli/testdata/shipcheck-stub/main.go) that mimics the leg surface
    and is configurable via STUB_EXIT_<LEG> env vars. Coverage:
      - all legs pass -> umbrella exits 0
      - one leg fails -> umbrella exits with that leg's code, all 5 still ran
      - multiple failures -> umbrella exits with max code
      - default argv: verify gets --fix, scorecard gets --live-check
      - --spec and --research-dir pass through to the legs that accept them
      - missing/invalid --dir -> ExitInputError before any leg runs
    
    Implements WU-1 from retro mvanhorn/cli-printing-press#351 (the company-goat
    retro). The Phase 4 prose update and polish-worker hook follow in subsequent
    commits.
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * docs(cli): add shipcheck umbrella implementation plan
    
    Implementation plan for WU-1 from retro mvanhorn/cli-printing-press#351.
    Documents the four implementation units (shipcheck command, --json + flag
    pass-through, Phase 4 skill prose update, polish-worker hook), the
    backwards-compat constraints, and the subprocess execution model decision.
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * feat(cli): add shipcheck --json envelope and selective flag pass-through
    
    Adds five flags to the umbrella so it covers the canonical Phase 4
    recommended invocation plus common operator overrides:
    
      --json           Emit a structured envelope at end-of-run; suppresses
                       per-leg stdout so the envelope can be piped to jq
                       without separation. Operators wanting per-leg detail
                       in JSON mode run the leg directly with --json.
      --no-fix         Disable verify's --fix auto-repair (read-only verify)
      --no-live-check  Disable scorecard's --live-check live-API sampling
      --api-key STR    Pass --api-key to verify (read-only GETs only)
      --env-var STR    Pass --env-var to verify (e.g., GITHUB_TOKEN)
      --strict         Pass --strict to verify-skill
    
    The JSON envelope shape is { passed, exit_code, started_at, elapsed_ms,
    legs: [{ name, exit_code, passed, started_at, elapsed_ms, command }] }.
    Each leg's `command` field shows the full argv as it would be invoked at
    the shell, so an operator can copy-paste-rerun a specific failing leg.
    
    In --json mode, leg stdout/stderr are discarded (io.Discard). This
    trade-off keeps both consumer modes simple: human mode streams real-time
    output then prints a summary table, JSON mode emits a single clean
    envelope. R4 in the plan ("legs stream their full output") applies to
    default mode; --json explicitly opts into structured output instead.
    
    Test coverage extends from 9 cases (U1) to 14 (U2):
      - --no-fix omits --fix from verify argv
      - --no-live-check omits --live-check from scorecard argv
      - --api-key and --env-var pass through to verify only (other legs
        do not receive them)
      - --strict passes through to verify-skill only
      - --json envelope is parseable, has the expected shape, and reflects
        pass/fail correctly for both all-pass and one-failure cases
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * feat(skills): collapse Phase 4 to single shipcheck invocation
    
    Replaces the 5-command code block in /printing-press Phase 4 with the
    new umbrella:
    
      printing-press shipcheck \\
        --dir "$CLI_WORK_DIR" \\
        --spec <same-spec> \\
        --research-dir "$API_RUN_DIR"
    
    Per-leg interpretation prose, fix order, and ship-threshold criteria are
    preserved — they remain useful when an operator needs to understand why
    a specific leg's verdict failed. Adds a new `shipcheck exits 0` clause
    at the top of the ship-threshold list as the canonical umbrella signal.
    
    The skill now also documents the recommended Phase 4 flag surface
    (--no-fix, --no-live-check, --json, --api-key, --env-var, --strict) and
    calls out that focused iteration on a single leg is supported via the
    standalone command, but the canonical Phase 4 sweep is the umbrella.
    
    Closes the gap that bit company-goat PR #140: the previous prose listed
    five commands as a code block, and an operator running three of them
    (skipping verify-skill) shipped 14 verify-skill errors to the public
    library CI. The umbrella makes that failure mode structurally
    impossible.
    
    Implements WU-3 from retro mvanhorn/cli-printing-press#351.
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * feat(skills): polish-worker runs verify-skill + workflow-verify; gates ship_recommendation
    
    Adds verify-skill and workflow-verify to the polish-worker's Phase 1
    diagnostic sweep, captures their --json output to /tmp files, and adds
    two new Categories table rows so findings flow into Phase 2 fixes.
    
    Phase 2 gains a Priority 4.5 fix-loop section documenting the three
    verify-skill check classes (flag-names, flag-commands, positional-args)
    and their canonical fixes:
      - flag-commands: inline cmd.Flags().StringVar(...) in the affected
        command (the verify-skill grep cannot follow shared-helper
        indirection)
      - positional-args: change Use: "cmd <arg>" to Use: "cmd [arg]" when
        the command also accepts the value via --flag
      - flag-names: fix the SKILL example or restore the deleted flag
    
    Phase 3 re-diagnose is updated to include the two new legs.
    
    Phase 4 ship_recommendation logic is hard-gated on verify-skill exit 0
    AND workflow-verify not reporting workflow-fail. A CLI that ships with
    a SKILL that lies about it gives agents broken instructions; a CLI
    whose primary workflow fails verification has not actually shipped.
    Both conditions are required for `ship` and `ship-with-gaps`; failing
    either drops to `hold`.
    
    Closes the polish-worker side of the gap that bit company-goat PR #140:
    even if Phase 4 shipcheck (the parent fix in feat/cli) is run, polish
    runs may still ship verify-skill mismatches if they don't run the same
    check. Now polish runs both legs and refuses to recommend ship while
    findings exist.
    
    Implements WU-4 from retro mvanhorn/cli-printing-press#351.
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 agents/polish-worker.md                            |  32 +-
 ...-feat-printing-press-shipcheck-umbrella-plan.md | 375 +++++++++++++
 internal/cli/root.go                               |   1 +
 internal/cli/shipcheck.go                          | 444 +++++++++++++++
 internal/cli/shipcheck_test.go                     | 610 +++++++++++++++++++++
 internal/cli/testdata/shipcheck-stub/main.go       |  63 +++
 skills/printing-press/SKILL.md                     |  18 +-
 7 files changed, 1531 insertions(+), 12 deletions(-)

diff --git a/agents/polish-worker.md b/agents/polish-worker.md
index a141396f..f41aba1e 100644
--- a/agents/polish-worker.md
+++ b/agents/polish-worker.md
@@ -42,6 +42,8 @@ go build -o "$CLI_NAME" ./cmd/"$CLI_NAME" 2>&1
 # Diagnostics (use SPEC_FLAG="--spec $SPEC_PATH" when SPEC_PATH is non-empty)
 printing-press dogfood --dir "$CLI_DIR" $SPEC_FLAG 2>&1
 printing-press verify --dir "$CLI_DIR" $SPEC_FLAG --json 2>&1
+printing-press workflow-verify --dir "$CLI_DIR" --json > /tmp/polish-workflow-verify.json 2>&1 || true
+printing-press verify-skill --dir "$CLI_DIR" --json > /tmp/polish-verify-skill.json 2>&1 || true
 # --live-check samples novel-feature outputs and populates
 # live_check.features[].warnings (Wave B entity detection) — required for
 # the "Output entity warnings" row below to have data to read.
@@ -50,11 +52,15 @@ printing-press scorecard --dir "$CLI_DIR" $SPEC_FLAG 2>&1
 go vet ./... 2>&1
 ```
 
+verify-skill and workflow-verify run alongside dogfood/verify/scorecard so polish catches the same class of failures the public-library CI catches. The polish-worker hard-gates `ship_recommendation: ship` on `verify-skill` exit 0 (see Phase 4 below).
+
 Parse findings into categories:
 
 | Category | Source | What to look for |
 |----------|--------|------------------|
 | Verify failures | verify --json | Commands with score < 3 |
+| SKILL static-check failures | verify-skill --json | Any `findings[]` with `severity=error` (flag-names, flag-commands, positional-args). Hard ship-gate: ship_recommendation cannot be `ship` while these exist. |
+| Workflow gaps | workflow-verify --json | Verdict `workflow-fail`. Soft gate: surface in `remaining_issues` and downgrade to `hold` when the workflow is the CLI's primary value. |
 | Dead code | dogfood | Dead functions, dead flags |
 | Stale files | dogfood | Unregistered commands |
 | Description issues | dogfood | Boilerplate root Short |
@@ -197,6 +203,20 @@ during generation.
 - **Sources & Inspiration**: credits to community projects (generated by
   the machine, preserve if present)
 
+### Priority 4.5: SKILL static-check failures (verify-skill)
+
+Read `/tmp/polish-verify-skill.json` for the full finding list. Each finding has a `check` (`flag-names`, `flag-commands`, or `positional-args`), a `command` (the path the SKILL claimed), and a `detail` describing the mismatch. Common shapes and fixes:
+
+- **`flag-names`** — SKILL references `--foo` but no command in `internal/cli/*.go` declares it. Either the example is wrong (fix the SKILL or remove the recipe) or the flag was deleted (decide if it should come back).
+- **`flag-commands`** — `--foo is declared elsewhere but not on <cmd>`. The flag exists somewhere but not on the command the SKILL invoked it on. Two fixes:
+  1. If the flag is added via a shared helper like `addXxxFlags(cmd, ...)`, inline the `cmd.Flags().StringVar(...)` declaration directly in the affected command's source file. The verify-skill grep cannot follow function-call indirection.
+  2. If the SKILL example is genuinely wrong, fix the example to use a flag the command does declare.
+- **`positional-args`** — `got N positional args; Use: "<cmd> <arg>" expects M-M`. The SKILL recipe passed N positional args but the command's `Use:` declares M required. Two fixes:
+  1. If the command also accepts the value via a `--flag`, change `Use: "cmd <arg>"` to `Use: "cmd [arg]"` (square brackets = optional). Verify-skill correctly accepts `--flag`-only invocations against an optional positional.
+  2. If the SKILL example is missing a required positional, fix the example.
+
+After fixing, re-run `printing-press verify-skill --dir "$CLI_DIR"` and confirm exit 0 before moving on.
+
 ### Priority 5: Remaining dogfood issues
 
 - Path validity mismatches
@@ -213,16 +233,18 @@ gofmt -w .
 
 ## Phase 3: Re-diagnose
 
-Re-run all four diagnostic tools on the fixed CLI:
+Re-run the diagnostic sweep on the fixed CLI:
 
 ```bash
 printing-press dogfood --dir "$CLI_DIR" $SPEC_FLAG 2>&1
 printing-press verify --dir "$CLI_DIR" $SPEC_FLAG --json 2>&1
+printing-press workflow-verify --dir "$CLI_DIR" --json 2>&1
+printing-press verify-skill --dir "$CLI_DIR" --json 2>&1
 printing-press scorecard --dir "$CLI_DIR" $SPEC_FLAG 2>&1
 go vet ./... 2>&1
 ```
 
-Record the after scores.
+Record the after scores. If verify-skill still has any `severity=error` findings or workflow-verify still reports `workflow-fail`, ship_recommendation cannot be `ship` (see Phase 4).
 
 ## Phase 4: Return
 
@@ -257,6 +279,6 @@ The three lists serve different purposes:
 - **remaining_issues**: issues you tried to fix but couldn't resolve
 
 Ship recommendation logic:
-- `ship`: verify >= 80%, scorecard >= 75, no critical failures
-- `ship-with-gaps`: verify >= 65%, scorecard >= 65, non-critical gaps remain
-- `hold`: verify < 65% or scorecard < 65 or critical failures
+- `ship`: verify >= 80%, scorecard >= 75, no critical failures, **AND** verify-skill exits 0 (no SKILL/CLI mismatches), **AND** workflow-verify is not `workflow-fail`. The two SKILL/workflow gates are hard requirements: a CLI that ships with a SKILL that lies about it (verify-skill findings) gives agents broken instructions; a CLI whose primary workflow fails verification has not actually shipped.
+- `ship-with-gaps`: verify >= 65%, scorecard >= 65, non-critical gaps remain, **AND** the SKILL/workflow gates above hold. Reserved for the rare case where a refactor or external-dependency blocker prevents a clean fix; the gap must be documented in `remaining_issues` and surfaced to the orchestrator.
+- `hold`: verify < 65% or scorecard < 65 or critical failures, **OR** verify-skill has unresolved findings, **OR** workflow-verify reports `workflow-fail` and the workflow is the CLI's primary value.
diff --git a/docs/plans/2026-04-27-002-feat-printing-press-shipcheck-umbrella-plan.md b/docs/plans/2026-04-27-002-feat-printing-press-shipcheck-umbrella-plan.md
new file mode 100644
index 00000000..14cb0731
--- /dev/null
+++ b/docs/plans/2026-04-27-002-feat-printing-press-shipcheck-umbrella-plan.md
@@ -0,0 +1,375 @@
+---
+title: Add `printing-press shipcheck` umbrella command + Phase 4 enforcement + polish-worker hook
+type: feat
+status: active
+date: 2026-04-27
+origin: docs/retros/ (none — origin is retro issue mvanhorn/cli-printing-press#351, WU-1)
+---
+
+# Add `printing-press shipcheck` umbrella + Phase 4 enforcement + polish-worker hook
+
+## Overview
+
+Adds a single `printing-press shipcheck` subcommand that runs all five verification legs (`dogfood`, `verify`, `workflow-verify`, `verify-skill`, `scorecard`) in sequence and propagates their exit codes through one umbrella verdict. Updates the `/printing-press` skill's Phase 4 prose to recommend the umbrella as the canonical shipcheck invocation. Updates the polish-worker agent so `verify-skill` runs in its diagnostic loop and gates `ship_recommendation`.
+
+The five legs already exist and remain callable standalone — the umbrella is pure orchestration. Backwards compatibility is preserved for any operator or CI script that calls the legs directly today.
+
+---
+
+## Problem Frame
+
+The retro for `company-goat` (issue mvanhorn/cli-printing-press#351, finding F1) traced a CI failure on PR #140 to a Phase 4 enforcement gap:
+
+- The `/printing-press` skill's Phase 4 block lists five commands as a code block to run during shipcheck. An operator can copy three of them, run them, and conclude shipcheck passed — silently skipping `verify-skill` and `workflow-verify`. That is what happened on the run that produced #140; the public-library CI surfaced 14 `verify-skill` errors that local Phase 4 missed.
+- The polish-worker agent's diagnostic loop runs `dogfood`, `verify`, and `scorecard`, but does not run `verify-skill` or `workflow-verify`. So polish does not catch the same class of failures even when the operator delegates to it.
+
+The fix is to make skipping these legs structurally impossible during the local loop: a single umbrella command that is the canonical Phase 4 invocation, plus a polish-worker contract that runs `verify-skill` and gates ship recommendation on it.
+
+---
+
+## Requirements Trace
+
+- R1. A single `printing-press shipcheck --dir <cli-dir> [--spec <path>] [...]` command runs `dogfood`, `verify`, `workflow-verify`, `verify-skill`, `scorecard` in sequence.
+- R2. Umbrella exits 0 only when every leg exits 0. Exits non-zero when any leg fails, with the exit code reflecting the first failing leg.
+- R3. Umbrella prints a per-leg verdict summary at the end so the operator can see which legs passed and which failed in one place.
+- R4. Each leg's full output streams to the operator's terminal as it runs (no buffering); the summary appears below.
+- R5. `--json` flag emits a structured envelope with per-leg verdicts for programmatic consumers.
+- R6. Existing standalone leg commands (`printing-press dogfood`, `printing-press verify`, etc.) continue to work unchanged.
+- R7. `/printing-press` skill Phase 4 prose recommends `shipcheck` as the canonical invocation; per-leg interpretation prose is preserved (still useful context).
+- R8. `polish-worker` agent runs `verify-skill` in its diagnostic loop and hard-gates `ship_recommendation` on `verify-skill` exit 0.
+- R9. Subprocess execution model: each leg is spawned as `exec.Command("printing-press", "<leg>", ...)`. Confirmed via design question; preserves stable ExitError propagation, real-time output, and ease of testing with stubs.
+
+---
+
+## Scope Boundaries
+
+- This plan adds an orchestration layer only. **Out of scope:** rewriting any underlying check tool, changing what any leg does, or fixing scorer false positives that surface during the umbrella's run (those are retro WU-2 through WU-8).
+- **Out of scope:** introducing new legs (e.g., MCP-coverage, secret-scan). If a future leg is wanted, it lands in a separate WU. The umbrella is designed to make adding a leg cheap, but no new leg ships in this WU.
+- **Out of scope:** any change to how the legs invoke their own internals (e.g., `verify-skill`'s python script). The umbrella treats each leg as an opaque subprocess.
+- **Out of scope:** WU-3 (auto-emit MCP tools for `extra_commands`). If U2 surfaces a clean place to add an MCP-coverage leg, note as follow-on but do not implement.
+- **Out of scope:** parallelizing legs. Sequential execution keeps output readable, avoids races on the CLI's working directory between dogfood + verify, and matches operator expectations.
+
+### Deferred to Follow-Up Work
+
+- **MCP-coverage leg in shipcheck:** depends on WU-3 emitting MCP tools for `extra_commands`; today there is nothing meaningful to check. Add as a sixth leg once WU-3 lands.
+- **Secret-scan leg in shipcheck:** today the publish skill runs scrubs at upload time. A pre-publish secret scan would be valuable but is its own WU.
+
+---
+
+## Context & Research
+
+### Relevant Code and Patterns
+
+- `internal/cli/root.go` — binary's command registration. Existing legs registered at lines 43–62 via `rootCmd.AddCommand(newXxxCmd())`. Pattern to mirror for `newShipcheckCmd()`.
+- `internal/cli/dogfood.go`, `internal/cli/verify.go`, `internal/cli/workflow_verify.go`, `internal/cli/verify_skill.go`, `internal/cli/scorecard.go` — existing leg implementations. Confirmed flag surfaces:
+  - `dogfood`: `--dir` (required), `--spec`, `--research-dir`, `--json`
+  - `verify`: `--dir` (required), `--spec`, `--api-key`, `--env-var`, `--threshold`, `--fix`, `--max-iterations`, `--json`
+  - `workflow-verify`: `--dir` (required), `--json`
+  - `verify-skill`: `--dir` (required), `--only`, `--json`, `--strict`
+  - `scorecard`: `--dir`, `--research-dir`, `--spec`, `--json`, `--live-check`, `--live-check-timeout`
+- `internal/cli/exitcodes.go` — `ExitError` type with `Code int`, `Err error`, `Silent bool`. Use this for umbrella's own input-validation errors. Leg exit codes are propagated via subprocess exit codes, not `ExitError`.
+- `internal/cli/verify_skill.go` line 95–117 — pattern for spawning a subprocess (`exec.Command`), forwarding stdout/stderr, and propagating the child's exit code through an `ExitError`. The umbrella will reuse this pattern, generalized across multiple subprocesses.
+- `skills/printing-press/SKILL.md` lines 1724–1781 — the Phase 4 block to update. Includes the 5-command code block, per-leg interpretation prose, ship threshold list.
+- `agents/polish-worker.md` — polish-worker contract. Phase 1 diagnostic block is at the top of the file under `## Phase 1: Baseline`; Categories table follows; output contract (`---POLISH-RESULT---` block with `ship_recommendation`) is later in the file.
+
+### Institutional Learnings
+
+- `verify-skill` ships its actual checker as an embedded python script (`internal/cli/verify_skill_bundled.py`) executed via `exec.Command("python3", ...)`. The pattern is a small Go shim that wraps an external process — the umbrella generalizes this further: a Go shim that wraps a sequence of `printing-press` self-invocations.
+- The `exec.Command(os.Args[0], ...)` self-invocation pattern is standard for Cobra-based CLIs that want an umbrella subcommand to drive its siblings without coupling to their internals. Resolving the binary path via `os.Args[0]` plus `exec.LookPath` ensures the umbrella runs the same binary that hosts it (no `PATH` collision risk during tests).
+
+### External References
+
+- Not used. This is internal repo work; all patterns exist locally.
+
+---
+
+## Key Technical Decisions
+
+- **Subprocess per leg, not in-process function calls.** Confirmed via design question. Trades 5 × 5ms startup overhead for stable exit-code propagation, real-time per-leg output, and easy stub-based testing. Matches the `verify-skill` shim pattern.
+- **Self-invocation via `os.Args[0]` resolved once at startup.** Use `exec.LookPath` against the running binary's path so the umbrella never accidentally runs an older `printing-press` from `$PATH`. Tests can override via an internal hook.
+- **Sequential execution, run all legs even on failure.** Operators want to see all gaps in one pass. Fail-fast would force `dogfood-fix-rerun-verify-fix-rerun` cycles instead of a single `shipcheck-fix-rerun-shipcheck` cycle.
+- **Verdict aggregation = max of leg exit codes.** Preserves first-failure code shape and surfaces a meaningful number to scripts that branch on exit codes. Summary table shows per-leg codes for the human.
+- **Recommended defaults baked in: `verify --fix` and `scorecard --live-check`.** These are the recommended invocations from current Phase 4 prose. Provide opt-out flags (`--no-fix`, `--no-live-check`) for cases where the operator wants a quick read without the auto-fix loop or live-check sample. Default-on matches "the operator should run one command and get the canonical result."
+- **No `--only` flag on the umbrella.** If a user wants to run only one leg, they should run that leg directly. The umbrella's job is to be the all-or-nothing canonical sweep.
+- **Skill Phase 4 prose: replace the 5-command block, keep per-leg interpretation prose.** The interpretation prose explains what each leg is for; that context is still useful when a leg fails and the operator needs to understand the verdict. Only the invocation collapses; the explanatory content stays.
+- **Polish-worker: keep the existing Phase 1 diagnostic block; add `verify-skill` and `workflow-verify` to it; do not switch the polish-worker to call `shipcheck`.** Polish has its own structured Phase 1 / Phase 2 / Phase 4.85 flow that the polish-worker agent prompt parses; folding into `shipcheck` would lose that structure. Inserting two more leg invocations is the minimal correct change.
+
+---
+
+## Open Questions
+
+### Resolved During Planning
+
+- **Should the umbrella support `--only` for selective leg execution?** No. Operators wanting selective runs use the legs directly. Umbrella's contract is all-or-nothing.
+- **Should the umbrella spawn legs in parallel?** No. Sequential. Avoids races on the working directory between `dogfood` and `verify`, keeps output readable.
+- **Should `verify --fix` be on or off by default in shipcheck?** On by default, with `--no-fix` to disable. Matches the current Phase 4 recommendation.
+- **What's the JSON envelope shape?** `{passed: bool, exit_code: int, legs: [{name, exit_code, passed, started_at, elapsed}], started_at, elapsed}`. Each leg's own JSON output is NOT nested — operators wanting structured per-leg detail run the leg directly with `--json`.
+
+### Deferred to Implementation
+
+- Whether `shipcheck` should print a leading "running 5 legs against <dir>" header before the first leg starts, or just let `dogfood`'s output be the first thing the user sees. Decide during implementation based on whether the per-leg streaming output already provides enough context.
+- Whether the summary table is markdown-pipe formatted (`| LEG | RESULT |`) or column-formatted plain text. Both work; pick whichever matches the closest existing summary in the binary (e.g., what does `verify`'s table look like?).
+
+---
+
+## High-Level Technical Design
+
+> *This illustrates the intended approach and is directional guidance for review, not implementation specification. The implementing agent should treat it as context, not code to reproduce.*
+
+The umbrella's runtime shape:
+
+```
+shipcheck RunE
+├── resolve self-binary path (exec.LookPath against os.Args[0])
+├── validate --dir exists and contains a built CLI
+├── for each leg in [dogfood, verify, workflow-verify, verify-skill, scorecard]:
+│     ├── build args (common: --dir; per-leg: --spec, --research-dir, --fix, --live-check, ...)
+│     ├── stream child stdout/stderr to ours (os.Stdout/os.Stderr)
+│     ├── capture exit code
+│     └── append LegResult{name, exit_code, started_at, elapsed} to results[]
+├── render summary table (per-leg verdict + total)
+└── return ExitError with code = max(leg_exit_codes), Silent=true (summary already printed)
+```
+
+Each leg is a small struct in shipcheck.go:
+
+```go
+type leg struct {
+    name string                                  // "dogfood", "verify", ...
+    args func(opts) []string                     // closure over umbrella flags + per-leg defaults
+    skip func(opts) (skip bool, reason string)   // optional — e.g., scorecard skipped when --no-live-check is incompatible with the leg
+}
+```
+
+A package-level slice `var legs = []leg{...}` enumerates the five in canonical order. Adding a future leg = append to the slice + define `args` builder.
+
+---
+
+## Implementation Units
+
+- U1. **Add `shipcheck` umbrella command with leg orchestration**
+
+**Goal:** A working `printing-press shipcheck --dir <cli> [--spec <path>] [--research-dir <path>]` command that runs all five legs in sequence, propagates exit codes, prints a summary, and is registered on the binary's root command.
+
+**Requirements:** R1, R2, R3, R4, R6, R9.
+
+**Dependencies:** none.
+
+**Files:**
+- Create: `internal/cli/shipcheck.go`
+- Modify: `internal/cli/root.go` (add `rootCmd.AddCommand(newShipcheckCmd())` near the existing leg registrations)
+- Test: `internal/cli/shipcheck_test.go`
+
+**Approach:**
+- Add `newShipcheckCmd()` returning `*cobra.Command` following the existing pattern in `dogfood.go`, `verify.go`.
+- Resolve self-binary path once at the start of `RunE` via `exec.LookPath(os.Args[0])`. Surface a clear error if the binary can't find itself (corrupted `os.Args[0]`, unusual launch context).
+- Validate `--dir` is non-empty and points to a directory containing a buildable Go module (look for `go.mod` and either `cmd/<name>-pp-cli/main.go` or `internal/cli/`). Use `ExitInputError` for validation failures.
+- Define a package-level `legs` slice with one entry per leg (`dogfood`, `verify`, `workflow-verify`, `verify-skill`, `scorecard`). Each entry has a `name` and an `args func(*shipcheckOpts) []string` closure that builds its argv from the umbrella's flag values.
+- Iterate the slice. For each leg: build argv, run via `exec.Command(selfBinary, argv...)`, attach stdin/stdout/stderr to `os.Stdin/os.Stdout/os.Stderr`, capture exit code via `cmd.Run()` plus `ExitError.ExitCode()`. Track per-leg `LegResult`.
+- After all legs run, render a summary table to `os.Stdout` (operator's terminal) listing per-leg verdict, exit code, and elapsed.
+- Compute `umbrellaCode = max(leg_exit_codes)`. If non-zero, return `&ExitError{Code: umbrellaCode, Err: fmt.Errorf("shipcheck failed: %d/%d legs failed", failingCount, len(legs)), Silent: true}` to propagate the code without printing a duplicate error message.
+
+**Patterns to follow:**
+- `internal/cli/verify_skill.go` lines 90–125 for the subprocess + stream + exit-code-propagation pattern.
+- `internal/cli/dogfood.go` for the cobra command shape (Use, Short, Long, Example, RunE signature, flag declarations).
+- `internal/cli/exitcodes.go` for `ExitError`/`ExitInputError` semantics.
+
+**Test scenarios:**
+- Happy path: stub binary on PATH that exits 0 for all leg invocations → `shipcheck` returns nil, summary shows 5 passing legs, total exit code 0.
+- One leg fails: stub binary exits 1 only for `verify-skill` → umbrella returns `ExitError{Code: 1}`, summary shows 4 passing + verify-skill failing, all 5 legs were invoked (no fail-fast).
+- Multiple legs fail: stub exits non-zero for `dogfood` (code 2) and `scorecard` (code 1) → umbrella returns `ExitError{Code: 2}` (max of failing codes), summary shows both failures.
+- Edge: `--dir` empty or missing → `ExitInputError` before any leg runs, no subprocesses spawned.
+- Edge: `--dir` does not exist on disk → `ExitInputError`.
+- Edge: self-binary path can't be resolved (test by mutating `os.Args[0]` to something invalid) → meaningful error returned, no spawn attempts.
+- Integration: stub printing-press binary placed in a temp dir, prepended to `PATH`, invoked via the umbrella → verifies argv passed to each leg matches expected (e.g., `verify` gets `--dir X --fix` by default, `dogfood` gets `--dir X --spec Y --research-dir Z`).
+
+**Verification:**
+- `go test ./internal/cli/...` passes including new shipcheck tests.
+- `go build -o printing-press ./cmd/printing-press && ./printing-press shipcheck --help` shows the new command with documented flags.
+- `./printing-press shipcheck --dir <existing-cli>` runs all five legs visibly and prints a summary; exit code matches whether any leg failed.
+
+---
+
+- U2. **Add `--json` output and selective flag pass-through to shipcheck**
+
+**Goal:** Operators and scripts can run `printing-press shipcheck --json` to get a stable structured envelope. Common per-leg flags (`--api-key`, `--env-var`, `--no-fix`, `--no-live-check`, `--strict`) pass through to the appropriate leg.
+
+**Requirements:** R5, R6.
+
+**Dependencies:** U1.
+
+**Files:**
+- Modify: `internal/cli/shipcheck.go`
+- Modify: `internal/cli/shipcheck_test.go`
+
+**Approach:**
+- Add umbrella flags: `--json` (bool), `--no-fix` (bool, default false → `verify --fix` is on by default), `--no-live-check` (bool, default false → `scorecard --live-check` is on by default), `--api-key` (string), `--env-var` (string), `--strict` (bool, passes to verify-skill).
+- Update each leg's `args` closure to read the umbrella flags. Examples:
+  - `dogfood`: `--dir <D>` plus `--spec <S>` if set plus `--research-dir <R>` if set.
+  - `verify`: `--dir <D>`, `--spec <S>` if set, `--fix` unless `--no-fix`, `--api-key <K>` if set, `--env-var <E>` if set.
+  - `scorecard`: `--dir <D>`, `--research-dir <R>` if set, `--spec <S>` if set, `--live-check` unless `--no-live-check`.
+  - `verify-skill`: `--dir <D>`, `--strict` if set.
+- When `--json` is set: do NOT pass `--json` through to legs (their JSON would interleave with each other). Instead, capture each leg's exit code as before, suppress the human summary table, and emit a single JSON envelope at the end:
+
+  ```json
+  {
+    "passed": true,
+    "exit_code": 0,
+    "started_at": "2026-04-27T13:45:00Z",
+    "elapsed_ms": 12450,
+    "legs": [
+      {"name": "dogfood", "exit_code": 0, "passed": true, "elapsed_ms": 1200, "command": "printing-press dogfood --dir ..."},
+      {"name": "verify", "exit_code": 0, "passed": true, "elapsed_ms": 4800, "command": "printing-press verify --dir ... --fix"},
+      ...
+    ]
+  }
+  ```
+
+- Each leg's stdout/stderr still streams to ours during the run (the JSON envelope is end-of-run only). Operators piping `--json` to `jq` should `2>/dev/null` if they want clean JSON.
+
+**Patterns to follow:**
+- `internal/cli/verify_skill.go` for the `--json` pass-through decision (it does pass `--json` through, but the umbrella's case is different because per-leg JSON would collide).
+- Existing JSON envelope shapes in `internal/cli/*.go` for naming conventions (`exit_code` over `code`, `elapsed_ms` over `duration`).
+
+**Test scenarios:**
+- Happy path: `--json` produces parseable JSON conforming to the envelope above; `passed: true` when all legs pass; `legs[].command` reflects the full argv per leg.
+- `--no-fix` happy: argv built for `verify` does NOT contain `--fix`; `argv` for other legs unchanged.
+- `--no-live-check` happy: argv built for `scorecard` does NOT contain `--live-check`.
+- `--api-key X` happy: argv for `verify` contains `--api-key X`; other legs unchanged.
+- `--env-var GITHUB_TOKEN` happy: argv for `verify` contains `--env-var GITHUB_TOKEN`.
+- `--strict` happy: argv for `verify-skill` contains `--strict`.
+- Defaults: with no opt-out flags, `verify` argv contains `--fix`, `scorecard` argv contains `--live-check`.
+- JSON failure case: when one leg fails, envelope `passed=false`, `exit_code` reflects max failing code, the failing leg's `passed=false`.
+
+**Verification:**
+- `./printing-press shipcheck --dir <cli> --json | jq '.passed'` returns a boolean.
+- `./printing-press shipcheck --dir <cli> --no-fix --json | jq '.legs[] | select(.name=="verify").command'` does not contain `--fix`.
+
+---
+
+- U3. **Update `/printing-press` skill Phase 4 prose to recommend `shipcheck`**
+
+**Goal:** Phase 4 of `skills/printing-press/SKILL.md` recommends `printing-press shipcheck` as the canonical invocation. Per-leg interpretation prose (what each leg is for, the fix order, the ship threshold criteria) is preserved. Operators can still see what each leg is for and what its failure means; they just don't have to remember to invoke each one.
+
+**Requirements:** R7.
+
+**Dependencies:** U1 (the umbrella must exist before the skill recommends it).
+
+**Files:**
+- Modify: `skills/printing-press/SKILL.md` (lines ~1724–1790 — the `## Phase 4: Shipcheck` block)
+
+**Approach:**
+- Replace the 5-command code block (lines ~1735–1739) with:
+
+  ```bash
+  printing-press shipcheck \
+    --dir "$CLI_WORK_DIR" \
+    --spec <same-spec> \
+    --research-dir "$API_RUN_DIR"
+  ```
+
+  Note: any operator who wants the legacy invocation can still run each leg individually; mention this once in the prose so existing scripts don't surprise the operator.
+- Keep the "Interpretation:" block (the bullet list explaining what each leg catches). This is the per-leg interpretation prose that's still useful when a leg fails.
+- Keep the "Fix order" numbered list (it's still relevant when an operator is iterating on fixes).
+- Update the "Ship threshold" section to lead with: "`shipcheck` exits 0" and then enumerate the per-leg conditions as supporting detail. This preserves all existing per-leg threshold semantics while making the umbrella the canonical signal.
+- Skim the rest of the skill for any other 5-command-block invocations of the same legs and decide case-by-case whether to collapse. Do NOT collapse standalone leg invocations elsewhere (e.g., during fix loops) — those are intentional iteration points, not shipcheck.
+
+**Patterns to follow:**
+- The skill already has umbrella-style command invocations elsewhere (e.g., `printing-press generate ...` does many things via one command). Match the same prose density for the new shipcheck block.
+
+**Test scenarios:**
+- Test expectation: none — this is a prose change in a skill markdown file. Verification is manual review against the diff. (No automated test harness asserts SKILL.md content; adding one would over-specify the prose.)
+
+**Verification:**
+- Manual review of the diff against `skills/printing-press/SKILL.md`. Confirm:
+  - One `printing-press shipcheck ...` invocation in the Phase 4 block.
+  - Per-leg interpretation prose preserved.
+  - Fix order list preserved.
+  - Ship threshold leads with `shipcheck` exit 0 and references per-leg conditions as supporting detail.
+  - No other content changed.
+
+---
+
+- U4. **Polish-worker runs `verify-skill` and `workflow-verify`; hard-gates `ship_recommendation` on verify-skill exit 0**
+
+**Goal:** The polish-worker agent runs `verify-skill` and `workflow-verify` in its Phase 1 diagnostic loop alongside the existing `dogfood`, `verify`, `scorecard`, `go vet` invocations. Surfaces verify-skill findings as a category. Refuses to return `ship_recommendation: ship` when `verify-skill` has unresolved findings.
+
+**Requirements:** R8.
+
+**Dependencies:** none (polish-worker invokes `verify-skill` directly, not via `shipcheck`; this is intentional — see Key Technical Decisions).
+
+**Files:**
+- Modify: `agents/polish-worker.md`
+
+**Approach:**
+- In `## Phase 1: Baseline`, add two new diagnostic invocations:
+
+  ```bash
+  printing-press verify-skill --dir "$CLI_DIR" --json 2>&1 | tee /tmp/polish-verify-skill.json
+  printing-press workflow-verify --dir "$CLI_DIR" --json 2>&1 | tee /tmp/polish-workflow-verify.json
+  ```
+
+- Add two rows to the Categories table:
+  - `SKILL static-check failures` — source: `verify-skill --json` — what to look for: any non-empty `findings[]` with `severity=error`.
+  - `Workflow gaps` — source: `workflow-verify --json` — what to look for: verdict `workflow-fail`.
+- In Phase 2 (the fix loop), add a fix priority for `verify-skill` errors before "README gaps":
+  - For `flag-commands` errors: inline the flag declarations in the affected command's source file (this is the WU-3 / F3 workaround until the verify-skill machine fix lands).
+  - For `flag-names` errors: declare the flag in `internal/cli/*.go` or remove the SKILL reference.
+  - For `positional-args` errors: change `Use: "X <arg>"` to `Use: "X [arg]"` when the command also accepts the value via a flag (the F2 fix); otherwise update the SKILL example to include the required positional.
+- In the output contract (the `---POLISH-RESULT---` block specification), add: `ship_recommendation: ship` requires `verify-skill` exit 0 AND `workflow-verify` verdict not `workflow-fail`. If either gate fails after the fix pass, set `ship_recommendation: hold` and surface the unresolved findings under `remaining_issues`.
+
+**Patterns to follow:**
+- The existing Phase 1 / Phase 2 / Phase 4.85 / output-contract structure in `agents/polish-worker.md`. Insert the new content into the existing sections; don't restructure.
+
+**Test scenarios:**
+- Test expectation: none — this is a prose change in an agent markdown file. Verification is by exercising the agent in a real polish run during U5 verification (or in the next time the polish-worker is invoked) and confirming it now runs both legs and gates ship_recommendation correctly.
+
+**Verification:**
+- Manual review of the diff against `agents/polish-worker.md`. Confirm:
+  - Phase 1 diagnostic block adds the two new commands.
+  - Categories table has two new rows.
+  - Phase 2 fix loop documents the verify-skill remediation patterns (flag-commands, flag-names, positional-args).
+  - Output contract requires verify-skill and workflow-verify to pass before `ship_recommendation: ship`.
+- After landing, the next polish run on a CLI with a deliberate verify-skill mismatch should report it under `findings` and refuse to ship.
+
+---
+
+## System-Wide Impact
+
+- **Interaction graph:** `shipcheck` self-invokes the binary five times via `exec.Command`. The legs are independent and have no inter-leg dependencies — none of them write files the next leg reads beyond what the legs already write today (e.g., `dogfood` writing `research.json` updates that `scorecard` consumes when run later). Sequential ordering preserves the existing leg contract.
+- **Error propagation:** Each leg's exit code propagates through subprocess `Run()` to a per-leg `LegResult`. The umbrella aggregates via `max()` and returns one `ExitError`. No silent suppression; the human summary always renders.
+- **State lifecycle risks:** None. The legs are read-only with respect to each other except for `dogfood`'s research.json updates and `verify`'s `--fix` source-code edits. Both already happen in the current Phase 4 sequence; ordering is unchanged.
+- **API surface parity:** None. The legs' standalone CLIs are unchanged. `shipcheck` is purely additive.
+- **Integration coverage:** A unit test stubs the binary self-invocation by writing a temporary `printing-press` shell script on `PATH` that exits with controllable codes per argv. Cross-layer behavior (umbrella → real leg → real CLI) is exercised by an end-to-end smoke test against a known-good CLI in the test environment, but that test runs only when explicitly opted in (`PRESS_INTEGRATION=1`) since it depends on `~/printing-press/library/` state.
+- **Unchanged invariants:** `printing-press dogfood`, `printing-press verify`, `printing-press workflow-verify`, `printing-press verify-skill`, `printing-press scorecard` continue to work standalone with their current flag surfaces. CI scripts and operator workflows that invoke them directly are not affected.
+
+---
+
+## Risks & Dependencies
+
+| Risk | Mitigation |
+|------|------------|
+| Self-binary resolution fails in unusual launch contexts (e.g., binary renamed, `os.Args[0]` is empty) | Fail fast with a clear error message naming what was tried (`os.Args[0]` value, `exec.LookPath` result). Never fall back to `printing-press` from `$PATH` — that risks invoking a different version. |
+| A leg's flag surface changes and the umbrella's argv builder goes stale | Keep the per-leg argv builder a closure with named flag accesses (no string-templated argv). Add a small comment per leg pointing at the leg's source file so a future change to the leg sets up the maintainer to update both files. |
+| Stub-binary tests are flaky on Windows (no shell scripts) | Use a Go-built test helper binary placed on `PATH` for integration tests, not a shell script. Pattern matches `verify-skill_test.go` if applicable. |
+| `verify --fix` mutates source during `shipcheck` and confuses operators who expected a read-only check | Document this in `shipcheck --help`. Provide `--no-fix` for read-only mode. Default-on matches current Phase 4 prose. |
+| Polish-worker invokes the legs directly instead of via `shipcheck`, drifting from the umbrella | This is intentional (Key Technical Decisions). Polish has its own Phase 1/2/4.85 structure that doesn't fold into `shipcheck` cleanly. Both paths run the same legs; the umbrella is for operators, the agent's manual sequence is for the agent. Document this in `agents/polish-worker.md`. |
+| `shipcheck` becomes a dumping ground for new legs over time, growing into something hard to reason about | The `legs` slice pattern keeps additions cheap and reviewable: one append + one args closure. Document the "add a leg here" comment in `shipcheck.go`. New legs from future WUs (MCP-coverage, secret-scan) will follow the same shape. |
+
+---
+
+## Documentation / Operational Notes
+
+- Add `printing-press shipcheck --help` output to the binary's command reference if one exists.
+- After landing, the retro issue mvanhorn/cli-printing-press#351 should be updated with a comment that WU-1 has shipped and references the merge commit. (The retro author handles this; not part of this plan's implementation.)
+- No CHANGELOG entry needed unless this binary uses release-please-style changelogs (it does — see AGENTS.md). The conventional commit format `feat(cli): add shipcheck umbrella` will populate the next release notes automatically. Same for `feat(skills): ...` and the polish-worker update.
+
+---
+
+## Sources & References
+
+- **Origin:** retro issue [mvanhorn/cli-printing-press#351](https://github.com/mvanhorn/cli-printing-press/issues/351), WU-1
+- **Retro proof:** `manuscripts/company-goat/20260427-121015/proofs/20260427-134000-retro-company-goat-pp-cli.md`
+- **Public-library CI failure that motivated this:** `mvanhorn/printing-press-library#140` (first run, before fix)
+- Related code: `internal/cli/verify_skill.go` (subprocess-shim pattern), `internal/cli/exitcodes.go` (`ExitError`), `internal/cli/root.go` (binary command registration), `internal/cli/dogfood.go` (cobra command shape)
+- AGENTS.md commit conventions: `feat(cli)`, `feat(skills)` scopes apply.
diff --git a/internal/cli/root.go b/internal/cli/root.go
index 3244f8ad..4b11bd19 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -58,6 +58,7 @@ func Execute() error {
 	rootCmd.AddCommand(newPublishCmd())
 	rootCmd.AddCommand(newPolishCmd())
 	rootCmd.AddCommand(newWorkflowVerifyCmd())
+	rootCmd.AddCommand(newShipcheckCmd())
 	rootCmd.AddCommand(newLockCmd())
 	rootCmd.AddCommand(newMCPAuditCmd())
 	rootCmd.AddCommand(newProbeReachabilityCmd())
diff --git a/internal/cli/shipcheck.go b/internal/cli/shipcheck.go
new file mode 100644
index 00000000..35af8b8b
--- /dev/null
+++ b/internal/cli/shipcheck.go
@@ -0,0 +1,444 @@
+package cli
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"github.com/spf13/cobra"
+)
+
+// shipcheck is the canonical Phase 4 verification umbrella. It runs each
+// of the five legs as a subprocess of the same printing-press binary,
+// aggregates exit codes, and prints a per-leg summary. Legs remain
+// callable standalone — this command is purely additive orchestration.
+//
+// The subprocess model (rather than calling each leg's RunE in-process)
+// gives us:
+//   - real-time per-leg output streaming to the operator's terminal,
+//   - reliable exit-code propagation through standard *exec.ExitError,
+//   - testability via a stub binary that mimics the leg surface.
+//
+// The legs slice below is the single source of truth for which legs run
+// and what argv each gets. Adding a leg = append one entry; the rest of
+// the umbrella reads from the slice.
+
+// shipcheckOpts holds every flag the umbrella accepts. Each leg's argv
+// builder is a closure over an opts pointer, so adding a flag = adding
+// a field here and consulting it from the relevant builder.
+//
+// noFix and noLiveCheck are opt-OUT flags: --fix and --live-check are on
+// by default because the canonical Phase 4 invocation enables them. The
+// opt-outs exist so an operator can ask for a quick read-only sweep
+// without verify auto-repairing source or scorecard sampling live calls.
+type shipcheckOpts struct {
+	dir         string
+	spec        string
+	researchDir string
+
+	// JSON envelope output. When set, suppresses the human summary table
+	// and emits a structured envelope at end-of-run instead. Each leg's
+	// own stdout/stderr still streams to the operator's terminal during
+	// the run; the envelope is end-of-run only.
+	asJSON bool
+
+	// Per-leg pass-through flags.
+	noFix       bool   // when true, omit --fix from verify argv
+	noLiveCheck bool   // when true, omit --live-check from scorecard argv
+	apiKey      string // when set, pass --api-key to verify
+	envVar      string // when set, pass --env-var to verify
+	strict      bool   // when set, pass --strict to verify-skill
+}
+
+// shipcheckLeg names one verification leg and how to invoke it.
+// args builds the leg's argv (without the binary path) from the umbrella's
+// resolved options.
+type shipcheckLeg struct {
+	name string
+	args func(*shipcheckOpts) []string
+}
+
+// shipcheckLegs enumerates the five legs in canonical execution order.
+// Order matters: dogfood writes research.json updates that scorecard
+// later consumes, so dogfood must run before scorecard. workflow-verify
+// and verify-skill have no inter-leg dependencies; their position is
+// driven by the canonical Phase 4 sequence in the /printing-press skill.
+var shipcheckLegs = []shipcheckLeg{
+	{
+		name: "dogfood",
+		args: func(o *shipcheckOpts) []string {
+			a := []string{"dogfood", "--dir", o.dir}
+			if o.spec != "" {
+				a = append(a, "--spec", o.spec)
+			}
+			if o.researchDir != "" {
+				a = append(a, "--research-dir", o.researchDir)
+			}
+			return a
+		},
+	},
+	{
+		name: "verify",
+		args: func(o *shipcheckOpts) []string {
+			a := []string{"verify", "--dir", o.dir}
+			if o.spec != "" {
+				a = append(a, "--spec", o.spec)
+			}
+			if !o.noFix {
+				a = append(a, "--fix")
+			}
+			if o.apiKey != "" {
+				a = append(a, "--api-key", o.apiKey)
+			}
+			if o.envVar != "" {
+				a = append(a, "--env-var", o.envVar)
+			}
+			return a
+		},
+	},
+	{
+		name: "workflow-verify",
+		args: func(o *shipcheckOpts) []string {
+			return []string{"workflow-verify", "--dir", o.dir}
+		},
+	},
+	{
+		name: "verify-skill",
+		args: func(o *shipcheckOpts) []string {
+			a := []string{"verify-skill", "--dir", o.dir}
+			if o.strict {
+				a = append(a, "--strict")
+			}
+			return a
+		},
+	},
+	{
+		name: "scorecard",
+		args: func(o *shipcheckOpts) []string {
+			a := []string{"scorecard", "--dir", o.dir}
+			if o.researchDir != "" {
+				a = append(a, "--research-dir", o.researchDir)
+			}
+			if o.spec != "" {
+				a = append(a, "--spec", o.spec)
+			}
+			if !o.noLiveCheck {
+				a = append(a, "--live-check")
+			}
+			return a
+		},
+	},
+}
+
+// shipcheckLegResult is the per-leg outcome of one umbrella run.
+type shipcheckLegResult struct {
+	Name      string
+	Argv      []string
+	ExitCode  int
+	StartedAt time.Time
+	Elapsed   time.Duration
+}
+
+// Passed reports whether the leg exited 0.
+func (r shipcheckLegResult) Passed() bool { return r.ExitCode == 0 }
+
+// resolveSelfBinary returns the path to the currently-running
+// printing-press binary so the umbrella can spawn itself for each leg.
+//
+// Indirected through a package-level var so tests can substitute a stub
+// binary that mimics the leg surface. Production callers always go
+// through os.Executable, which gives the actual running executable path
+// and avoids any ambiguity from an outdated `printing-press` on $PATH.
+var resolveSelfBinary = func() (string, error) {
+	exe, err := os.Executable()
+	if err != nil {
+		return "", fmt.Errorf("resolving printing-press binary: %w", err)
+	}
+	// Resolve symlinks so a `printing-press` symlink to the real binary
+	// still produces the canonical path subprocesses see.
+	if resolved, err := filepath.EvalSymlinks(exe); err == nil {
+		exe = resolved
+	}
+	return exe, nil
+}
+
+// runShipcheckLeg spawns one leg as a subprocess and captures its exit
+// code.
+//
+// In default (human) mode, the leg's stdout/stderr stream to the
+// operator's terminal in real time so they see progress as it happens.
+// In --json mode, leg output is discarded so the umbrella's JSON
+// envelope is the only thing on stdout (clean for jq pipes); operators
+// who want per-leg detail in JSON mode should run the leg directly with
+// --json. This trade-off keeps both consumer modes simple.
+//
+// Returns ExitCode 0 on clean completion, the child's exit code on
+// non-zero exit, and an error only when the subprocess could not be
+// started (binary missing, permission denied, etc.). A non-zero exit
+// from the child is reported via the result, not as an error — the
+// umbrella always wants to record what happened and continue.
+func runShipcheckLeg(binPath string, leg shipcheckLeg, opts *shipcheckOpts) (shipcheckLegResult, error) {
+	argv := leg.args(opts)
+	cmd := exec.Command(binPath, argv...)
+	cmd.Stdin = os.Stdin
+	if opts.asJSON {
+		// Discard per-leg output so the envelope at end-of-run is the
+		// only thing on stdout. Legs whose own stdout/stderr matters
+		// for diagnosis can be re-run standalone.
+		cmd.Stdout = io.Discard
+		cmd.Stderr = io.Discard
+	} else {
+		cmd.Stdout = os.Stdout
+		cmd.Stderr = os.Stderr
+	}
+
+	start := time.Now()
+	runErr := cmd.Run()
+	elapsed := time.Since(start)
+
+	res := shipcheckLegResult{
+		Name:      leg.name,
+		Argv:      argv,
+		StartedAt: start,
+		Elapsed:   elapsed,
+	}
+	if runErr == nil {
+		res.ExitCode = 0
+		return res, nil
+	}
+	var exitErr *exec.ExitError
+	if errors.As(runErr, &exitErr) {
+		res.ExitCode = exitErr.ExitCode()
+		return res, nil
+	}
+	// Subprocess could not be started at all.
+	return res, fmt.Errorf("running %s: %w", leg.name, runErr)
+}
+
+// renderShipcheckSummary prints a per-leg verdict table to w.
+func renderShipcheckSummary(w *os.File, results []shipcheckLegResult) {
+	fmt.Fprintln(w, "")
+	fmt.Fprintln(w, "Shipcheck Summary")
+	fmt.Fprintln(w, "=================")
+	fmt.Fprintf(w, "  %-16s  %-6s  %-8s  %s\n", "LEG", "RESULT", "EXIT", "ELAPSED")
+	for _, r := range results {
+		verdict := "PASS"
+		if !r.Passed() {
+			verdict = "FAIL"
+		}
+		fmt.Fprintf(w, "  %-16s  %-6s  %-8d  %s\n",
+			r.Name,
+			verdict,
+			r.ExitCode,
+			r.Elapsed.Round(time.Millisecond),
+		)
+	}
+	failing := 0
+	for _, r := range results {
+		if !r.Passed() {
+			failing++
+		}
+	}
+	fmt.Fprintln(w, "")
+	if failing == 0 {
+		fmt.Fprintf(w, "Verdict: PASS (%d/%d legs passed)\n", len(results), len(results))
+	} else {
+		fmt.Fprintf(w, "Verdict: FAIL (%d/%d legs failed)\n", failing, len(results))
+	}
+}
+
+// shipcheckUmbrellaCode returns the umbrella's overall exit code:
+// 0 if every leg passed, otherwise the largest non-zero exit code
+// among failing legs (preserves the most serious failure).
+func shipcheckUmbrellaCode(results []shipcheckLegResult) int {
+	max := 0
+	for _, r := range results {
+		if r.ExitCode > max {
+			max = r.ExitCode
+		}
+	}
+	return max
+}
+
+// shipcheckJSONLeg is one entry in the JSON envelope's legs[] array.
+// Field names use snake_case to match the rest of the binary's JSON
+// output conventions (exit_code over code, elapsed_ms over duration).
+type shipcheckJSONLeg struct {
+	Name      string `json:"name"`
+	ExitCode  int    `json:"exit_code"`
+	Passed    bool   `json:"passed"`
+	StartedAt string `json:"started_at"`
+	ElapsedMS int64  `json:"elapsed_ms"`
+	Command   string `json:"command"`
+}
+
+// shipcheckJSONEnvelope is the structured output emitted with --json. The
+// envelope is end-of-run; per-leg stdout/stderr still streams during the
+// run. Operators piping --json output to jq should redirect stderr.
+type shipcheckJSONEnvelope struct {
+	Passed    bool               `json:"passed"`
+	ExitCode  int                `json:"exit_code"`
+	StartedAt string             `json:"started_at"`
+	ElapsedMS int64              `json:"elapsed_ms"`
+	Legs      []shipcheckJSONLeg `json:"legs"`
+}
+
+// renderShipcheckJSON marshals the envelope to w. Each leg's `command`
+// field shows the full argv as it would be invoked at the shell so an
+// operator can copy-paste-rerun a specific leg from the JSON output.
+func renderShipcheckJSON(w *os.File, binPath string, results []shipcheckLegResult, runStartedAt time.Time, runElapsed time.Duration) error {
+	env := shipcheckJSONEnvelope{
+		Passed:    shipcheckUmbrellaCode(results) == 0,
+		ExitCode:  shipcheckUmbrellaCode(results),
+		StartedAt: runStartedAt.UTC().Format(time.RFC3339),
+		ElapsedMS: runElapsed.Milliseconds(),
+		Legs:      make([]shipcheckJSONLeg, 0, len(results)),
+	}
+	for _, r := range results {
+		env.Legs = append(env.Legs, shipcheckJSONLeg{
+			Name:      r.Name,
+			ExitCode:  r.ExitCode,
+			Passed:    r.Passed(),
+			StartedAt: r.StartedAt.UTC().Format(time.RFC3339),
+			ElapsedMS: r.Elapsed.Milliseconds(),
+			Command:   strings.Join(append([]string{binPath}, r.Argv...), " "),
+		})
+	}
+	enc := json.NewEncoder(w)
+	enc.SetIndent("", "  ")
+	return enc.Encode(env)
+}
+
+// validateShipcheckDir confirms --dir points at something that looks
+// like a built printing-press CLI: a directory containing go.mod and
+// either an internal/cli/ tree or a cmd/<name>-pp-cli/ tree. We are
+// intentionally permissive — full structural checks are the legs' job.
+func validateShipcheckDir(dir string) error {
+	if strings.TrimSpace(dir) == "" {
+		return fmt.Errorf("--dir is required")
+	}
+	st, err := os.Stat(dir)
+	if err != nil {
+		return fmt.Errorf("--dir %q: %w", dir, err)
+	}
+	if !st.IsDir() {
+		return fmt.Errorf("--dir %q is not a directory", dir)
+	}
+	if _, err := os.Stat(filepath.Join(dir, "go.mod")); err != nil {
+		return fmt.Errorf("--dir %q does not contain go.mod (is this a generated CLI directory?)", dir)
+	}
+	return nil
+}
+
+func newShipcheckCmd() *cobra.Command {
+	opts := &shipcheckOpts{}
+
+	cmd := &cobra.Command{
+		Use:   "shipcheck",
+		Short: "Run all five verification legs (dogfood, verify, workflow-verify, verify-skill, scorecard) as one canonical Phase 4 sweep",
+		Long: `shipcheck runs every Phase 4 verification leg in sequence and aggregates their
+exit codes into a single verdict. It is the canonical local invocation that
+matches what the public-library CI runs.
+
+Legs (in canonical order):
+  dogfood          — structural validation against the source spec
+  verify           — runtime command testing (with --fix to auto-repair common breakage)
+  workflow-verify  — primary workflow end-to-end against the verification manifest
+  verify-skill     — SKILL.md flag/positional/command consistency with the shipped CLI
+  scorecard        — Steinberger quality bar (with --live-check sampling novel features)
+
+In default mode, every leg streams its full output to the terminal as it runs
+and a per-leg verdict table prints at the end. In --json mode, leg output is
+suppressed and the only stdout is a structured envelope at end-of-run. The
+command exits non-zero when any leg fails, with the exit code reflecting the
+most serious leg failure.
+
+Each leg remains callable standalone — this command is additive orchestration.`,
+		Example: `  # Canonical Phase 4 invocation
+  printing-press shipcheck \
+    --dir ~/printing-press/library/notion \
+    --spec ./openapi.yaml \
+    --research-dir ~/printing-press/.runstate/scope/runs/RUN_ID
+
+  # Without a research dir (skips the dogfood/scorecard novel-feature checks)
+  printing-press shipcheck --dir ~/printing-press/library/notion --spec ./openapi.yaml`,
+		SilenceUsage:  true,
+		SilenceErrors: true,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			if err := validateShipcheckDir(opts.dir); err != nil {
+				return &ExitError{Code: ExitInputError, Err: err}
+			}
+
+			binPath, err := resolveSelfBinary()
+			if err != nil {
+				return &ExitError{Code: ExitInputError, Err: err}
+			}
+
+			runStart := time.Now()
+			results := make([]shipcheckLegResult, 0, len(shipcheckLegs))
+			for _, leg := range shipcheckLegs {
+				// Don't print the per-leg banner in JSON mode — the
+				// envelope at end-of-run is the structured signal.
+				// Per-leg stdout/stderr still streams (some legs print
+				// their own JSON or progress) so operators piping --json
+				// to jq should redirect stderr.
+				if !opts.asJSON {
+					fmt.Fprintf(os.Stdout, "\n=== %s ===\n", leg.name)
+				}
+				res, runErr := runShipcheckLeg(binPath, leg, opts)
+				if runErr != nil {
+					// Subprocess failed to start. Record as a synthetic
+					// failure, surface the error to stderr, and continue
+					// — operators want a complete summary even if one
+					// leg's binary went missing mid-run.
+					fmt.Fprintf(os.Stderr, "shipcheck: %v\n", runErr)
+					res.ExitCode = ExitUnknownError
+				}
+				results = append(results, res)
+			}
+			runElapsed := time.Since(runStart)
+
+			if opts.asJSON {
+				if err := renderShipcheckJSON(os.Stdout, binPath, results, runStart, runElapsed); err != nil {
+					return fmt.Errorf("rendering JSON envelope: %w", err)
+				}
+			} else {
+				renderShipcheckSummary(os.Stdout, results)
+			}
+
+			code := shipcheckUmbrellaCode(results)
+			if code != 0 {
+				failing := 0
+				for _, r := range results {
+					if !r.Passed() {
+						failing++
+					}
+				}
+				return &ExitError{
+					Code:   code,
+					Err:    fmt.Errorf("shipcheck failed: %d/%d legs failed", failing, len(results)),
+					Silent: true,
+				}
+			}
+			return nil
+		},
+	}
+
+	cmd.Flags().StringVar(&opts.dir, "dir", "", "Path to the generated CLI directory (required)")
+	cmd.Flags().StringVar(&opts.spec, "spec", "", "Path to the OpenAPI spec file (passed to dogfood, verify, scorecard)")
+	cmd.Flags().StringVar(&opts.researchDir, "research-dir", "", "Pipeline directory containing research.json (passed to dogfood and scorecard)")
+	cmd.Flags().BoolVar(&opts.asJSON, "json", false, "Emit a structured JSON envelope at end-of-run (suppresses per-leg stdout for clean piping; run legs standalone with --json for per-leg detail)")
+	cmd.Flags().BoolVar(&opts.noFix, "no-fix", false, "Disable verify's --fix auto-repair loop (read-only verify)")
+	cmd.Flags().BoolVar(&opts.noLiveCheck, "no-live-check", false, "Disable scorecard's --live-check live-API sampling")
+	cmd.Flags().StringVar(&opts.apiKey, "api-key", "", "API key for verify's live testing (read-only GETs only)")
+	cmd.Flags().StringVar(&opts.envVar, "env-var", "", "Environment variable name verify should read for the API key (e.g., GITHUB_TOKEN)")
+	cmd.Flags().BoolVar(&opts.strict, "strict", false, "Pass --strict to verify-skill (treat likely-false-positive findings as failures)")
+
+	return cmd
+}
diff --git a/internal/cli/shipcheck_test.go b/internal/cli/shipcheck_test.go
new file mode 100644
index 00000000..59c5a815
--- /dev/null
+++ b/internal/cli/shipcheck_test.go
@@ -0,0 +1,610 @@
+package cli
+
+import (
+	"bufio"
+	"encoding/json"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"slices"
+	"strings"
+	"testing"
+)
+
+// buildShipcheckStub compiles the shipcheck stub once per test run and
+// returns its path. The stub mimics the printing-press leg surface
+// (dogfood/verify/workflow-verify/verify-skill/scorecard) and is
+// configurable via env vars: see internal/cli/testdata/shipcheck-stub/main.go.
+func buildShipcheckStub(t *testing.T) string {
+	t.Helper()
+	out := filepath.Join(t.TempDir(), "shipcheck-stub")
+	cmd := exec.Command("go", "build", "-o", out, "./testdata/shipcheck-stub")
+	if buildOut, err := cmd.CombinedOutput(); err != nil {
+		t.Fatalf("building shipcheck stub: %v\n%s", err, string(buildOut))
+	}
+	return out
+}
+
+// fakeCLIDir creates a minimal directory that satisfies validateShipcheckDir:
+// a directory containing go.mod. Returned path is absolute.
+func fakeCLIDir(t *testing.T) string {
+	t.Helper()
+	dir := t.TempDir()
+	if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module fake\n"), 0o644); err != nil {
+		t.Fatalf("writing fake go.mod: %v", err)
+	}
+	return dir
+}
+
+// withStubBinary swaps resolveSelfBinary for the duration of a test so
+// the umbrella spawns the stub instead of the real printing-press
+// binary. Returns a cleanup function callers must defer.
+func withStubBinary(t *testing.T, path string) func() {
+	t.Helper()
+	prev := resolveSelfBinary
+	resolveSelfBinary = func() (string, error) { return path, nil }
+	return func() { resolveSelfBinary = prev }
+}
+
+// readStubLog parses the stub's per-invocation argv log. Each line is
+// tab-separated argv as the stub recorded it.
+func readStubLog(t *testing.T, logPath string) [][]string {
+	t.Helper()
+	f, err := os.Open(logPath)
+	if err != nil {
+		t.Fatalf("opening stub log: %v", err)
+	}
+	defer f.Close()
+	scanner := bufio.NewScanner(f)
+	var out [][]string
+	for scanner.Scan() {
+		line := scanner.Text()
+		if line == "" {
+			continue
+		}
+		out = append(out, strings.Split(line, "\t"))
+	}
+	if err := scanner.Err(); err != nil {
+		t.Fatalf("reading stub log: %v", err)
+	}
+	return out
+}
+
+// runShipcheckCmd runs newShipcheckCmd().RunE with the given args (no
+// "shipcheck" prefix) and returns the resulting error. It does not
+// intercept stdout/stderr — they go to the test process's own
+// streams, which lets `go test -v` show what the stub printed.
+func runShipcheckCmd(t *testing.T, args ...string) error {
+	t.Helper()
+	cmd := newShipcheckCmd()
+	cmd.SetArgs(args)
+	return cmd.Execute()
+}
+
+// TestShipcheck_AllLegsPass: every leg exits 0, umbrella returns nil.
+// All five legs must be invoked in canonical order with correct argv.
+func TestShipcheck_AllLegsPass(t *testing.T) {
+	stub := buildShipcheckStub(t)
+	defer withStubBinary(t, stub)()
+
+	dir := fakeCLIDir(t)
+	logFile := filepath.Join(t.TempDir(), "stub.log")
+	t.Setenv("STUB_LOG_FILE", logFile)
+
+	if err := runShipcheckCmd(t, "--dir", dir); err != nil {
+		t.Fatalf("expected nil error when all legs pass; got %v", err)
+	}
+
+	invocations := readStubLog(t, logFile)
+	if len(invocations) != len(shipcheckLegs) {
+		t.Fatalf("expected %d leg invocations; got %d: %v", len(shipcheckLegs), len(invocations), invocations)
+	}
+
+	// Confirm canonical order: dogfood, verify, workflow-verify, verify-skill, scorecard.
+	wantOrder := []string{"dogfood", "verify", "workflow-verify", "verify-skill", "scorecard"}
+	for i, want := range wantOrder {
+		// argv[0] is the stub binary path; argv[1] is the leg name.
+		if len(invocations[i]) < 2 {
+			t.Fatalf("invocation %d has fewer than 2 args: %v", i, invocations[i])
+		}
+		if invocations[i][1] != want {
+			t.Errorf("invocation %d: want leg %q, got %q (full argv: %v)", i, want, invocations[i][1], invocations[i])
+		}
+	}
+}
+
+// TestShipcheck_OneLegFails: verify-skill exits 1, umbrella returns
+// ExitError with code 1; all five legs still ran (no fail-fast).
+func TestShipcheck_OneLegFails(t *testing.T) {
+	stub := buildShipcheckStub(t)
+	defer withStubBinary(t, stub)()
+
+	dir := fakeCLIDir(t)
+	logFile := filepath.Join(t.TempDir(), "stub.log")
+	t.Setenv("STUB_LOG_FILE", logFile)
+	t.Setenv("STUB_EXIT_VERIFY_SKILL", "1")
+
+	err := runShipcheckCmd(t, "--dir", dir)
+	if err == nil {
+		t.Fatal("expected non-nil error when verify-skill fails; got nil")
+	}
+	exitErr, ok := err.(*ExitError)
+	if !ok {
+		t.Fatalf("expected *ExitError; got %T: %v", err, err)
+	}
+	if exitErr.Code != 1 {
+		t.Errorf("expected umbrella exit code 1; got %d", exitErr.Code)
+	}
+	if !exitErr.Silent {
+		t.Error("expected Silent=true so cobra does not duplicate the error message; got Silent=false")
+	}
+
+	invocations := readStubLog(t, logFile)
+	if len(invocations) != len(shipcheckLegs) {
+		t.Errorf("expected %d invocations even when one fails (no fail-fast); got %d", len(shipcheckLegs), len(invocations))
+	}
+}
+
+// TestShipcheck_MultipleFailures: dogfood exits 2, scorecard exits 1.
+// Umbrella exits with the largest non-zero code (2).
+func TestShipcheck_MultipleFailures(t *testing.T) {
+	stub := buildShipcheckStub(t)
+	defer withStubBinary(t, stub)()
+
+	dir := fakeCLIDir(t)
+	logFile := filepath.Join(t.TempDir(), "stub.log")
+	t.Setenv("STUB_LOG_FILE", logFile)
+	t.Setenv("STUB_EXIT_DOGFOOD", "2")
+	t.Setenv("STUB_EXIT_SCORECARD", "1")
+
+	err := runShipcheckCmd(t, "--dir", dir)
+	if err == nil {
+		t.Fatal("expected non-nil error when multiple legs fail")
+	}
+	exitErr, ok := err.(*ExitError)
+	if !ok {
+		t.Fatalf("expected *ExitError; got %T", err)
+	}
+	if exitErr.Code != 2 {
+		t.Errorf("expected umbrella exit code 2 (max of failing leg codes); got %d", exitErr.Code)
+	}
+}
+
+// TestShipcheck_DefaultArgvIncludesFixAndLiveCheck verifies that without
+// any opt-out flags, verify gets --fix and scorecard gets --live-check.
+// These are the recommended Phase 4 invocations.
+func TestShipcheck_DefaultArgvIncludesFixAndLiveCheck(t *testing.T) {
+	stub := buildShipcheckStub(t)
+	defer withStubBinary(t, stub)()
+
+	dir := fakeCLIDir(t)
+	logFile := filepath.Join(t.TempDir(), "stub.log")
+	t.Setenv("STUB_LOG_FILE", logFile)
+
+	if err := runShipcheckCmd(t, "--dir", dir); err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	invocations := readStubLog(t, logFile)
+	verifyArgs := findInvocation(invocations, "verify")
+	if !argvHas(verifyArgs, "--fix") {
+		t.Errorf("expected verify argv to include --fix by default; got %v", verifyArgs)
+	}
+	scorecardArgs := findInvocation(invocations, "scorecard")
+	if !argvHas(scorecardArgs, "--live-check") {
+		t.Errorf("expected scorecard argv to include --live-check by default; got %v", scorecardArgs)
+	}
+}
+
+// TestShipcheck_PassesSpecAndResearchDir: when --spec and --research-dir
+// are set, dogfood and scorecard receive both; verify receives --spec.
+func TestShipcheck_PassesSpecAndResearchDir(t *testing.T) {
+	stub := buildShipcheckStub(t)
+	defer withStubBinary(t, stub)()
+
+	dir := fakeCLIDir(t)
+	logFile := filepath.Join(t.TempDir(), "stub.log")
+	t.Setenv("STUB_LOG_FILE", logFile)
+
+	specPath := "/some/spec.yaml"
+	researchDir := "/some/research"
+	if err := runShipcheckCmd(t, "--dir", dir, "--spec", specPath, "--research-dir", researchDir); err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	invocations := readStubLog(t, logFile)
+
+	dogfoodArgs := findInvocation(invocations, "dogfood")
+	if !argvHas(dogfoodArgs, "--spec") || !argvHas(dogfoodArgs, specPath) {
+		t.Errorf("dogfood argv missing --spec: %v", dogfoodArgs)
+	}
+	if !argvHas(dogfoodArgs, "--research-dir") || !argvHas(dogfoodArgs, researchDir) {
+		t.Errorf("dogfood argv missing --research-dir: %v", dogfoodArgs)
+	}
+
+	verifyArgs := findInvocation(invocations, "verify")
+	if !argvHas(verifyArgs, "--spec") || !argvHas(verifyArgs, specPath) {
+		t.Errorf("verify argv missing --spec: %v", verifyArgs)
+	}
+
+	scorecardArgs := findInvocation(invocations, "scorecard")
+	if !argvHas(scorecardArgs, "--spec") || !argvHas(scorecardArgs, specPath) {
+		t.Errorf("scorecard argv missing --spec: %v", scorecardArgs)
+	}
+	if !argvHas(scorecardArgs, "--research-dir") || !argvHas(scorecardArgs, researchDir) {
+		t.Errorf("scorecard argv missing --research-dir: %v", scorecardArgs)
+	}
+
+	// workflow-verify and verify-skill don't take --spec or --research-dir;
+	// confirm they don't get them.
+	wfArgs := findInvocation(invocations, "workflow-verify")
+	if argvHas(wfArgs, "--spec") {
+		t.Errorf("workflow-verify should not receive --spec; got %v", wfArgs)
+	}
+	vsArgs := findInvocation(invocations, "verify-skill")
+	if argvHas(vsArgs, "--spec") {
+		t.Errorf("verify-skill should not receive --spec; got %v", vsArgs)
+	}
+}
+
+// TestShipcheck_RequiresDir: missing --dir returns ExitInputError before
+// any leg runs.
+func TestShipcheck_RequiresDir(t *testing.T) {
+	stub := buildShipcheckStub(t)
+	defer withStubBinary(t, stub)()
+	logFile := filepath.Join(t.TempDir(), "stub.log")
+	t.Setenv("STUB_LOG_FILE", logFile)
+
+	err := runShipcheckCmd(t)
+	if err == nil {
+		t.Fatal("expected error for missing --dir")
+	}
+	exitErr, ok := err.(*ExitError)
+	if !ok {
+		t.Fatalf("expected *ExitError; got %T", err)
+	}
+	if exitErr.Code != ExitInputError {
+		t.Errorf("expected ExitInputError; got %d", exitErr.Code)
+	}
+
+	// Stub log should be empty — no legs spawned.
+	if _, err := os.Stat(logFile); !os.IsNotExist(err) {
+		invocations := readStubLog(t, logFile)
+		if len(invocations) != 0 {
+			t.Errorf("expected 0 invocations when --dir missing; got %d", len(invocations))
+		}
+	}
+}
+
+// TestShipcheck_RejectsNonexistentDir: --dir pointing at a missing path
+// returns ExitInputError.
+func TestShipcheck_RejectsNonexistentDir(t *testing.T) {
+	stub := buildShipcheckStub(t)
+	defer withStubBinary(t, stub)()
+
+	err := runShipcheckCmd(t, "--dir", "/this/path/does/not/exist/anywhere")
+	if err == nil {
+		t.Fatal("expected error for nonexistent --dir")
+	}
+	exitErr, ok := err.(*ExitError)
+	if !ok {
+		t.Fatalf("expected *ExitError; got %T", err)
+	}
+	if exitErr.Code != ExitInputError {
+		t.Errorf("expected ExitInputError; got %d", exitErr.Code)
+	}
+}
+
+// TestShipcheck_RejectsDirWithoutGoMod: --dir pointing at a directory
+// without go.mod returns ExitInputError. Guards against accidentally
+// running shipcheck against a manuscripts dir or unrelated path.
+func TestShipcheck_RejectsDirWithoutGoMod(t *testing.T) {
+	stub := buildShipcheckStub(t)
+	defer withStubBinary(t, stub)()
+
+	dir := t.TempDir() // empty — no go.mod
+	err := runShipcheckCmd(t, "--dir", dir)
+	if err == nil {
+		t.Fatal("expected error for --dir without go.mod")
+	}
+	exitErr, ok := err.(*ExitError)
+	if !ok {
+		t.Fatalf("expected *ExitError; got %T", err)
+	}
+	if exitErr.Code != ExitInputError {
+		t.Errorf("expected ExitInputError; got %d", exitErr.Code)
+	}
+}
+
+// TestShipcheck_NoFix_OmitsFixFromVerify confirms --no-fix removes
+// --fix from verify's argv. Used when an operator wants a read-only
+// shipcheck pass without verify mutating source files.
+func TestShipcheck_NoFix_OmitsFixFromVerify(t *testing.T) {
+	stub := buildShipcheckStub(t)
+	defer withStubBinary(t, stub)()
+
+	dir := fakeCLIDir(t)
+	logFile := filepath.Join(t.TempDir(), "stub.log")
+	t.Setenv("STUB_LOG_FILE", logFile)
+
+	if err := runShipcheckCmd(t, "--dir", dir, "--no-fix"); err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	verifyArgs := findInvocation(readStubLog(t, logFile), "verify")
+	if argvHas(verifyArgs, "--fix") {
+		t.Errorf("--no-fix should omit --fix from verify argv; got %v", verifyArgs)
+	}
+}
+
+// TestShipcheck_NoLiveCheck_OmitsLiveCheckFromScorecard confirms
+// --no-live-check removes --live-check from scorecard's argv. Used when
+// an operator wants a quick scorecard read without sampling live calls.
+func TestShipcheck_NoLiveCheck_OmitsLiveCheckFromScorecard(t *testing.T) {
+	stub := buildShipcheckStub(t)
+	defer withStubBinary(t, stub)()
+
+	dir := fakeCLIDir(t)
+	logFile := filepath.Join(t.TempDir(), "stub.log")
+	t.Setenv("STUB_LOG_FILE", logFile)
+
+	if err := runShipcheckCmd(t, "--dir", dir, "--no-live-check"); err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	scorecardArgs := findInvocation(readStubLog(t, logFile), "scorecard")
+	if argvHas(scorecardArgs, "--live-check") {
+		t.Errorf("--no-live-check should omit --live-check from scorecard argv; got %v", scorecardArgs)
+	}
+}
+
+// TestShipcheck_PassesAuthFlagsToVerify confirms --api-key and --env-var
+// flow through to verify (and only verify — other legs do not accept them).
+func TestShipcheck_PassesAuthFlagsToVerify(t *testing.T) {
+	stub := buildShipcheckStub(t)
+	defer withStubBinary(t, stub)()
+
+	dir := fakeCLIDir(t)
+	logFile := filepath.Join(t.TempDir(), "stub.log")
+	t.Setenv("STUB_LOG_FILE", logFile)
+
+	if err := runShipcheckCmd(t,
+		"--dir", dir,
+		"--api-key", "ghp_test123",
+		"--env-var", "GITHUB_TOKEN",
+	); err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	invocations := readStubLog(t, logFile)
+	verifyArgs := findInvocation(invocations, "verify")
+	if !argvHas(verifyArgs, "--api-key") || !argvHas(verifyArgs, "ghp_test123") {
+		t.Errorf("verify argv missing --api-key: %v", verifyArgs)
+	}
+	if !argvHas(verifyArgs, "--env-var") || !argvHas(verifyArgs, "GITHUB_TOKEN") {
+		t.Errorf("verify argv missing --env-var: %v", verifyArgs)
+	}
+
+	// Other legs must NOT receive these flags — they don't accept them.
+	for _, leg := range []string{"dogfood", "workflow-verify", "verify-skill", "scorecard"} {
+		args := findInvocation(invocations, leg)
+		if argvHas(args, "--api-key") {
+			t.Errorf("%s argv should not include --api-key; got %v", leg, args)
+		}
+		if argvHas(args, "--env-var") {
+			t.Errorf("%s argv should not include --env-var; got %v", leg, args)
+		}
+	}
+}
+
+// TestShipcheck_StrictPassesToVerifySkill confirms --strict propagates
+// to verify-skill (and only verify-skill — other legs don't accept it).
+func TestShipcheck_StrictPassesToVerifySkill(t *testing.T) {
+	stub := buildShipcheckStub(t)
+	defer withStubBinary(t, stub)()
+
+	dir := fakeCLIDir(t)
+	logFile := filepath.Join(t.TempDir(), "stub.log")
+	t.Setenv("STUB_LOG_FILE", logFile)
+
+	if err := runShipcheckCmd(t, "--dir", dir, "--strict"); err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	invocations := readStubLog(t, logFile)
+	vsArgs := findInvocation(invocations, "verify-skill")
+	if !argvHas(vsArgs, "--strict") {
+		t.Errorf("verify-skill argv missing --strict: %v", vsArgs)
+	}
+	for _, leg := range []string{"dogfood", "verify", "workflow-verify", "scorecard"} {
+		args := findInvocation(invocations, leg)
+		if argvHas(args, "--strict") {
+			t.Errorf("%s argv should not include --strict; got %v", leg, args)
+		}
+	}
+}
+
+// TestShipcheck_JSONEnvelope_AllPass: --json produces parseable JSON
+// with the expected shape when every leg passes.
+func TestShipcheck_JSONEnvelope_AllPass(t *testing.T) {
+	stub := buildShipcheckStub(t)
+	defer withStubBinary(t, stub)()
+
+	dir := fakeCLIDir(t)
+	logFile := filepath.Join(t.TempDir(), "stub.log")
+	t.Setenv("STUB_LOG_FILE", logFile)
+
+	out := captureStdout(t, func() {
+		if err := runShipcheckCmd(t, "--dir", dir, "--json"); err != nil {
+			t.Fatalf("unexpected error: %v", err)
+		}
+	})
+
+	// The output stream is mixed: stub's own stdout plus the JSON
+	// envelope at end-of-run. Find the JSON envelope by locating the
+	// final `}` and walking back to the matching `{`.
+	envelopeJSON := extractFinalJSONObject(t, out)
+
+	var env shipcheckJSONEnvelope
+	if err := json.Unmarshal([]byte(envelopeJSON), &env); err != nil {
+		t.Fatalf("envelope is not valid JSON: %v\n--- envelope ---\n%s", err, envelopeJSON)
+	}
+	if !env.Passed {
+		t.Errorf("expected passed=true; got %+v", env)
+	}
+	if env.ExitCode != 0 {
+		t.Errorf("expected exit_code=0; got %d", env.ExitCode)
+	}
+	if len(env.Legs) != len(shipcheckLegs) {
+		t.Errorf("expected %d legs in envelope; got %d", len(shipcheckLegs), len(env.Legs))
+	}
+	for _, leg := range env.Legs {
+		if !leg.Passed {
+			t.Errorf("leg %s should be passed=true; got %+v", leg.Name, leg)
+		}
+		if leg.ExitCode != 0 {
+			t.Errorf("leg %s should have exit_code=0; got %d", leg.Name, leg.ExitCode)
+		}
+		if leg.Command == "" {
+			t.Errorf("leg %s should have non-empty command; got %+v", leg.Name, leg)
+		}
+		if leg.StartedAt == "" {
+			t.Errorf("leg %s should have non-empty started_at; got %+v", leg.Name, leg)
+		}
+	}
+}
+
+// TestShipcheck_JSONEnvelope_OneFailure: --json envelope reflects a
+// failing leg with passed=false at the leg and envelope level.
+func TestShipcheck_JSONEnvelope_OneFailure(t *testing.T) {
+	stub := buildShipcheckStub(t)
+	defer withStubBinary(t, stub)()
+
+	dir := fakeCLIDir(t)
+	logFile := filepath.Join(t.TempDir(), "stub.log")
+	t.Setenv("STUB_LOG_FILE", logFile)
+	t.Setenv("STUB_EXIT_VERIFY_SKILL", "1")
+
+	out := captureStdout(t, func() {
+		err := runShipcheckCmd(t, "--dir", dir, "--json")
+		if err == nil {
+			t.Fatal("expected non-nil error when verify-skill fails")
+		}
+	})
+
+	var env shipcheckJSONEnvelope
+	if err := json.Unmarshal([]byte(extractFinalJSONObject(t, out)), &env); err != nil {
+		t.Fatalf("envelope is not valid JSON: %v", err)
+	}
+
+	if env.Passed {
+		t.Errorf("envelope.passed should be false when verify-skill failed")
+	}
+	if env.ExitCode != 1 {
+		t.Errorf("envelope.exit_code should be 1; got %d", env.ExitCode)
+	}
+
+	var failingLeg *shipcheckJSONLeg
+	for i, l := range env.Legs {
+		if l.Name == "verify-skill" {
+			failingLeg = &env.Legs[i]
+			break
+		}
+	}
+	if failingLeg == nil {
+		t.Fatal("envelope missing verify-skill leg")
+	}
+	if failingLeg.Passed {
+		t.Errorf("verify-skill leg should be passed=false")
+	}
+	if failingLeg.ExitCode != 1 {
+		t.Errorf("verify-skill leg should have exit_code=1; got %d", failingLeg.ExitCode)
+	}
+}
+
+// extractFinalJSONObject finds the last balanced `{...}` block in s.
+// The umbrella's --json mode mixes per-leg stub output with the final
+// envelope; this walks from the end back to the matching brace.
+func extractFinalJSONObject(t *testing.T, s string) string {
+	t.Helper()
+	end := strings.LastIndex(s, "}")
+	if end < 0 {
+		t.Fatalf("no JSON object found in output:\n%s", s)
+	}
+	depth := 0
+	for i := end; i >= 0; i-- {
+		switch s[i] {
+		case '}':
+			depth++
+		case '{':
+			depth--
+			if depth == 0 {
+				return s[i : end+1]
+			}
+		}
+	}
+	t.Fatalf("could not find matching `{` for trailing `}` in output:\n%s", s)
+	return ""
+}
+
+// TestShipcheckUmbrellaCode_Aggregation tests the pure exit-code aggregator.
+func TestShipcheckUmbrellaCode_Aggregation(t *testing.T) {
+	cases := []struct {
+		name    string
+		results []shipcheckLegResult
+		want    int
+	}{
+		{
+			name: "all pass",
+			results: []shipcheckLegResult{
+				{Name: "dogfood", ExitCode: 0},
+				{Name: "verify", ExitCode: 0},
+			},
+			want: 0,
+		},
+		{
+			name: "one fails with code 1",
+			results: []shipcheckLegResult{
+				{Name: "dogfood", ExitCode: 0},
+				{Name: "verify", ExitCode: 1},
+			},
+			want: 1,
+		},
+		{
+			name: "max wins across multiple failures",
+			results: []shipcheckLegResult{
+				{Name: "dogfood", ExitCode: 2},
+				{Name: "verify", ExitCode: 1},
+				{Name: "scorecard", ExitCode: 3},
+			},
+			want: 3,
+		},
+		{
+			name:    "empty results",
+			results: nil,
+			want:    0,
+		},
+	}
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			if got := shipcheckUmbrellaCode(c.results); got != c.want {
+				t.Errorf("shipcheckUmbrellaCode = %d; want %d", got, c.want)
+			}
+		})
+	}
+}
+
+// findInvocation returns the argv slice (excluding the stub binary path)
+// for the given leg name, or nil if not found.
+func findInvocation(invocations [][]string, leg string) []string {
+	for _, argv := range invocations {
+		if len(argv) >= 2 && argv[1] == leg {
+			return argv[1:]
+		}
+	}
+	return nil
+}
+
+func argvHas(argv []string, needle string) bool {
+	return slices.Contains(argv, needle)
+}
diff --git a/internal/cli/testdata/shipcheck-stub/main.go b/internal/cli/testdata/shipcheck-stub/main.go
new file mode 100644
index 00000000..d6ae8407
--- /dev/null
+++ b/internal/cli/testdata/shipcheck-stub/main.go
@@ -0,0 +1,63 @@
+// Package main is a test stub that mimics the printing-press leg surface
+// (dogfood, verify, workflow-verify, verify-skill, scorecard) for shipcheck
+// orchestration tests.
+//
+// Behavior:
+//   - The first non-flag arg is the leg name (e.g., "dogfood").
+//   - If STUB_LOG_FILE is set, the stub appends its full os.Args to that
+//     file (one line per invocation, tab-separated) so tests can verify
+//     argv pass-through.
+//   - If STUB_EXIT_<LEG_UPPER> is set (e.g., STUB_EXIT_DOGFOOD=2), the
+//     stub exits with that code. Otherwise it exits 0.
+//   - Some output is written to stdout/stderr so tests can verify that
+//     leg output streams to the umbrella's terminal.
+package main
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
+)
+
+func main() {
+	if len(os.Args) < 2 {
+		fmt.Fprintln(os.Stderr, "stub: missing leg name")
+		os.Exit(99)
+	}
+	leg := os.Args[1]
+
+	// Record the invocation so tests can verify argv pass-through.
+	if logFile := os.Getenv("STUB_LOG_FILE"); logFile != "" {
+		f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "stub: opening log %s: %v\n", logFile, err)
+			os.Exit(99)
+		}
+		defer f.Close()
+		// One invocation per line, tab-separated argv.
+		if _, err := fmt.Fprintln(f, strings.Join(os.Args, "\t")); err != nil {
+			fmt.Fprintf(os.Stderr, "stub: writing log: %v\n", err)
+			os.Exit(99)
+		}
+	}
+
+	// Print a recognizable banner so tests can verify the leg's output
+	// streamed through the umbrella to the terminal.
+	fmt.Fprintf(os.Stdout, "stub running leg=%s args=%v\n", leg, os.Args[2:])
+
+	// Look up the configured exit code: STUB_EXIT_DOGFOOD=2, etc.
+	envName := "STUB_EXIT_" + strings.ToUpper(strings.ReplaceAll(leg, "-", "_"))
+	if v := os.Getenv(envName); v != "" {
+		code, err := strconv.Atoi(v)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "stub: invalid %s=%q: %v\n", envName, v, err)
+			os.Exit(99)
+		}
+		if code != 0 {
+			fmt.Fprintf(os.Stderr, "stub %s: forced exit %d\n", leg, code)
+		}
+		os.Exit(code)
+	}
+	os.Exit(0)
+}
diff --git a/skills/printing-press/SKILL.md b/skills/printing-press/SKILL.md
index 065a20a1..e383d4ad 100644
--- a/skills/printing-press/SKILL.md
+++ b/skills/printing-press/SKILL.md
@@ -1723,7 +1723,7 @@ Do not rationalize skipping transcendence features because "the CLI already work
 
 ## Phase 4: Shipcheck
 
-Run one combined verification block.
+Run one combined verification block via the `shipcheck` umbrella, which runs all five legs (dogfood, verify, workflow-verify, verify-skill, scorecard) in canonical order, propagates exit codes, and prints a per-leg verdict summary. The umbrella is the canonical Phase 4 invocation; running the legs individually is supported but not recommended (operators have skipped legs that way and shipped broken CLIs).
 
 Before running shipcheck, update the lock heartbeat:
 ```bash
@@ -1731,13 +1731,16 @@ printing-press lock update --cli <api>-pp-cli --phase shipcheck
 ```
 
 ```bash
-printing-press dogfood         --dir "$CLI_WORK_DIR" --spec <same-spec> --research-dir "$API_RUN_DIR"
-printing-press verify          --dir "$CLI_WORK_DIR" --spec <same-spec> --fix
-printing-press workflow-verify --dir "$CLI_WORK_DIR"
-printing-press verify-skill    --dir "$CLI_WORK_DIR"
-printing-press scorecard       --dir "$CLI_WORK_DIR" --spec <same-spec>
+printing-press shipcheck \
+  --dir "$CLI_WORK_DIR" \
+  --spec <same-spec> \
+  --research-dir "$API_RUN_DIR"
 ```
 
+The umbrella defaults to `verify --fix` (auto-repair common failures) and `scorecard --live-check` (sample novel-feature output against real targets). Use `--no-fix` for a read-only pass, `--no-live-check` to skip live sampling, or `--json` for a structured envelope (suppresses per-leg output for clean piping). Pass `--api-key` / `--env-var` through to verify when live testing needs a credential, or `--strict` to make verify-skill treat likely-false-positive findings as failures.
+
+If a leg fails, re-run that one leg standalone (e.g., `printing-press verify-skill --dir <CLI_WORK_DIR>`) for focused iteration; once it passes, re-run the full `shipcheck` umbrella to confirm no regression in the others.
+
 Interpretation:
 - `dogfood` catches dead flags, dead helpers, invalid paths, example drift, broken data wiring, command tree/config field wiring bugs, and novel features that were planned but not built
 - `verify` catches runtime breakage and runs the auto-fix loop for common failures
@@ -1773,7 +1776,8 @@ for the Phase 4 fix delegation pattern.
 When `CODEX_MODE` is false, fix bugs directly.
 <!-- CODEX_PHASE4_END -->
 
-Ship threshold:
+Ship threshold (the umbrella's verdict is the canonical signal — all of these must hold for `shipcheck` to exit 0):
+- `shipcheck` exits 0. The umbrella's per-leg summary table shows every leg PASS. A non-zero exit is a fix-before-ship blocker, period — do not ship if the umbrella is red.
 - `verify` verdict is `PASS` or high `WARN` with 0 critical failures
 - `dogfood` no longer fails because of spec parsing, binary path, or skipped examples
 - `dogfood` wiring checks pass (no unregistered commands, no config field mismatches)

← f3c18d4c docs(cli): add MIT LICENSE matching README declaration (#349  ·  back to Cli Printing Press  ·  fix(publish): gate packaging on skill verification (#348) 47fe3393 →