[object Object]

← back to Cli Printing Press

fix(cli): close redfin retro verification gaps (#762)

172c6c299ef48f278faadfa0fbdcef4a39a7c2ec · 2026-05-08 17:36:10 -0700 · Trevin Chow

Files touched

Diff

commit 172c6c299ef48f278faadfa0fbdcef4a39a7c2ec
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Fri May 8 17:36:10 2026 -0700

    fix(cli): close redfin retro verification gaps (#762)
---
 docs/GLOSSARY.md                                   |  2 +-
 internal/cli/schema.go                             | 84 ++++++++++++++++++++--
 internal/cli/schema_test.go                        | 52 ++++++++++++++
 internal/cli/shipcheck.go                          | 36 ++++++++--
 internal/cli/shipcheck_test.go                     | 41 ++++++++---
 internal/pipeline/seeds.go                         |  5 +-
 skills/printing-press/SKILL.md                     | 21 +++---
 .../cases/schema-phase5-marker/artifacts.txt       |  1 +
 .../golden/cases/schema-phase5-marker/command.txt  |  1 +
 .../golden/cases/schema-phase5-skip/artifacts.txt  |  1 +
 .../golden/cases/schema-phase5-skip/command.txt    |  1 +
 .../golden/expected/schema-phase5-marker/exit.txt  |  1 +
 .../expected/schema-phase5-marker/stderr.txt       |  0
 .../expected/schema-phase5-marker/stdout.txt       | 35 +++++++++
 .../golden/expected/schema-phase5-skip/exit.txt    |  1 +
 .../golden/expected/schema-phase5-skip/stderr.txt  |  0
 .../golden/expected/schema-phase5-skip/stdout.txt  | 28 ++++++++
 17 files changed, 277 insertions(+), 33 deletions(-)

diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md
index c6f588da..8b481239 100644
--- a/docs/GLOSSARY.md
+++ b/docs/GLOSSARY.md
@@ -38,7 +38,7 @@ Default disambiguation conventions:
 | **canonicalargs** | Tiny generator subpackage at `internal/canonicalargs/` exporting `Lookup(name) (string, bool)` for cross-domain positional placeholder names (`since`, `until`, `tag`, `vertical`). Both verify mock-mode dispatch and the SKILL template consult this registry as one step in the lookup chain `spec.Param.Default → canonicalargs → legacy syntheticArgValue switch → "mock-value"`. **Domain-specific names belong in the spec author's `Param.Default`, not here** — anti-pattern: "Never change the machine for one CLI's edge case." |
 | **mcp-sync** | Subcommand on the printing-press binary (`printing-press mcp-sync <cli-dir>`) that migrates generated MCP surfaces from the old static novel-feature list to the runtime Cobra-tree mirror. It rewrites generated MCP files, adds the root command export when possible, regenerates `tools-manifest.json`, and refuses hand-edited `internal/mcp/tools.go` unless `--force` is passed. |
 | **regen-merge** | Subcommand on the printing-press binary (`printing-press regen-merge <cli-dir> --fresh <fresh-dir>`) that classifies each Go file in a published library CLI by AST decl-set comparison against a fresh-generated tree, applies safe templated overwrites, restores lost AddCommand registrations in `root.go` and resource-parents, and merges `go.mod` while preserving the published monorepo module path. Targets the per-CLI sweep workflow (~5-15 min/CLI vs ~30-90 min manual). Six classification verdicts; stage-and-swap-with-recovery transactional apply; macOS+Linux only. Lives in `internal/pipeline/regenmerge/`. |
-| **shipcheck** | The verification block that gates publishing: dogfood + verify + workflow-verify + verify-skill + scorecard, run together. Dogfood includes `mcp_surface_parity`, so stale static MCP surfaces block shipping. All legs must pass before a printed CLI ships. |
+| **shipcheck** | The verification block that gates publishing: dogfood + verify + workflow-verify + verify-skill + validate-narrative + scorecard, run together. Dogfood includes `mcp_surface_parity`, and validate-narrative checks README/SKILL narrative commands against the built CLI. All legs must pass before a printed CLI ships. |
 | **scorecard** / **scoring** | Two-tier quality assessment with a 50/50 weighted composite. Tier 1: infrastructure (16 string-matching dimensions, raw max 160, normalized to 0-50). Tier 2: domain correctness (7 semantic dimensions, raw max 60 when live verify ran, normalized to 0-50). Total /100 with letter grades. Source of truth: `internal/pipeline/scorecard.go` (`tier1Max` / `tier2Max`). Subcommand: `printing-press scorecard`. |
 | **machine-owned freshness** | Opt-in freshness contract for store-backed printed CLIs using `cache.enabled`. Covered command paths map to syncable resources; in `--data-source auto` they may run a bounded pre-read refresh before serving local data. `--data-source local` never refreshes, `--data-source live` must not mutate the local store, and env opt-out only disables the freshness hook. This is current-cache freshness, not a guarantee of full historical backfill or API-specific enrichment. |
 | **doctor** | Self-diagnostic command shipped inside every printed CLI for end-users to run. Checks environment, auth config, and connectivity at the user's runtime. Unlike dogfood (which validates at generation time), doctor runs post-install. |
diff --git a/internal/cli/schema.go b/internal/cli/schema.go
index 081e4b60..9ed0088e 100644
--- a/internal/cli/schema.go
+++ b/internal/cli/schema.go
@@ -192,18 +192,92 @@ const trafficAnalysisSchemaJSON = `{
   }
 }`
 
+const phase5MarkerSchemaJSON = `{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "https://github.com/mvanhorn/cli-printing-press/schemas/phase5-acceptance.schema.json",
+  "title": "CLI Printing Press phase5-acceptance.json",
+  "type": "object",
+  "additionalProperties": false,
+  "required": ["schema_version", "api_name", "run_id", "status", "level", "matrix_size", "tests_passed"],
+  "properties": {
+    "schema_version": {"type": "integer", "const": 1},
+    "api_name": {"type": "string", "minLength": 1},
+    "cli_name": {"type": "string"},
+    "run_id": {"type": "string", "minLength": 1},
+    "status": {"type": "string", "enum": ["pass", "fail"]},
+    "level": {"type": "string", "enum": ["quick", "full"]},
+    "matrix_size": {"type": "integer", "minimum": 1},
+    "tests_total": {"type": "integer", "minimum": 0},
+    "tests_passed": {"type": "integer", "minimum": 1},
+    "tests_skipped": {"type": "integer", "minimum": 0},
+    "tests_failed": {"type": "integer", "minimum": 0},
+    "completed_at": {"type": "string", "format": "date-time"},
+    "summary": {"type": "string"},
+    "auth_context": {"$ref": "#/$defs/auth_context"}
+  },
+  "$defs": {
+    "auth_context": {
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "type": {"type": "string"},
+        "api_key_available": {"type": "boolean"},
+        "browser_session_available": {"type": "boolean"}
+      }
+    }
+  }
+}`
+
+const phase5SkipSchemaJSON = `{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "https://github.com/mvanhorn/cli-printing-press/schemas/phase5-skip.schema.json",
+  "title": "CLI Printing Press phase5-skip.json",
+  "type": "object",
+  "additionalProperties": false,
+  "required": ["schema_version", "api_name", "run_id", "status", "skip_reason"],
+  "properties": {
+    "schema_version": {"type": "integer", "const": 1},
+    "api_name": {"type": "string", "minLength": 1},
+    "cli_name": {"type": "string"},
+    "run_id": {"type": "string", "minLength": 1},
+    "status": {"type": "string", "const": "skip"},
+    "skip_reason": {"type": "string", "minLength": 1},
+    "auth_context": {"$ref": "#/$defs/auth_context"}
+  },
+  "$defs": {
+    "auth_context": {
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "type": {"type": "string"},
+        "api_key_available": {"type": "boolean"},
+        "browser_session_available": {"type": "boolean"}
+      }
+    }
+  }
+}`
+
 func newSchemaCmd() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "schema",
 		Short: "Print machine-readable schemas for Printing Press artifacts",
 	}
-	cmd.AddCommand(&cobra.Command{
-		Use:   "traffic-analysis",
-		Short: "Print the traffic-analysis.json schema",
+	cmd.AddCommand(newSchemaPrintCmd("traffic-analysis", "Print the traffic-analysis.json schema", trafficAnalysisSchemaJSON))
+	cmd.AddCommand(newSchemaPrintCmd("phase5-marker", "Print the phase5-acceptance.json schema", phase5MarkerSchemaJSON))
+	cmd.AddCommand(newSchemaPrintCmd("phase5-skip", "Print the phase5-skip.json schema", phase5SkipSchemaJSON))
+	return cmd
+}
+
+func newSchemaPrintCmd(use, short, schema string) *cobra.Command {
+	var asJSON bool
+	cmd := &cobra.Command{
+		Use:   use,
+		Short: short,
 		RunE: func(cmd *cobra.Command, args []string) error {
-			fmt.Fprintln(cmd.OutOrStdout(), trafficAnalysisSchemaJSON)
+			fmt.Fprintln(cmd.OutOrStdout(), schema)
 			return nil
 		},
-	})
+	}
+	cmd.Flags().BoolVar(&asJSON, "json", false, "Emit JSON schema (default; accepted for command symmetry)")
 	return cmd
 }
diff --git a/internal/cli/schema_test.go b/internal/cli/schema_test.go
index d9028d36..117b5e0b 100644
--- a/internal/cli/schema_test.go
+++ b/internal/cli/schema_test.go
@@ -21,3 +21,55 @@ func TestSchemaTrafficAnalysisPrintsJSONSchema(t *testing.T) {
 	assert.Contains(t, output, `"confidence": {"type": "number"`)
 	assert.Contains(t, output, `"endpoint_clusters"`)
 }
+
+func TestSchemaPhase5MarkerPrintsJSONSchema(t *testing.T) {
+	cmd := newSchemaCmd()
+	cmd.SetArgs([]string{"phase5-marker", "--json"})
+
+	output, err := runWithCapturedStdout(t, cmd.Execute)
+	require.NoError(t, err)
+
+	var schema map[string]any
+	require.NoError(t, json.Unmarshal([]byte(output), &schema))
+	assert.Equal(t, "CLI Printing Press phase5-acceptance.json", schema["title"])
+	for _, field := range []string{
+		"schema_version",
+		"run_id",
+		"api_name",
+		"cli_name",
+		"level",
+		"status",
+		"matrix_size",
+		"tests_total",
+		"tests_passed",
+		"tests_failed",
+		"completed_at",
+		"summary",
+	} {
+		assert.Contains(t, output, `"`+field+`"`)
+	}
+}
+
+func TestSchemaPhase5SkipPrintsJSONSchema(t *testing.T) {
+	cmd := newSchemaCmd()
+	cmd.SetArgs([]string{"phase5-skip"})
+
+	output, err := runWithCapturedStdout(t, cmd.Execute)
+	require.NoError(t, err)
+
+	var schema map[string]any
+	require.NoError(t, json.Unmarshal([]byte(output), &schema))
+	assert.Equal(t, "CLI Printing Press phase5-skip.json", schema["title"])
+	for _, field := range []string{"schema_version", "run_id", "api_name", "cli_name", "status", "skip_reason", "auth_context"} {
+		assert.Contains(t, output, `"`+field+`"`)
+	}
+}
+
+func TestSchemaUnknownNameFails(t *testing.T) {
+	cmd := newSchemaCmd()
+	cmd.SetArgs([]string{"unknown-name"})
+
+	err := cmd.Execute()
+	require.Error(t, err)
+	assert.Contains(t, err.Error(), `unknown command "unknown-name"`)
+}
diff --git a/internal/cli/shipcheck.go b/internal/cli/shipcheck.go
index 21ed5052..db3b5bf0 100644
--- a/internal/cli/shipcheck.go
+++ b/internal/cli/shipcheck.go
@@ -15,7 +15,7 @@ import (
 )
 
 // shipcheck is the canonical Phase 4 verification umbrella. It runs each
-// of the five legs as a subprocess of the same printing-press binary,
+// of the six 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.
 //
@@ -64,11 +64,10 @@ type shipcheckLeg struct {
 	args func(*shipcheckOpts) []string
 }
 
-// shipcheckLegs enumerates the five legs in canonical execution order.
+// shipcheckLegs enumerates the six 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.
+// later consumes, verify builds the CLI binary validate-narrative uses,
+// and scorecard should see all earlier validation failures first.
 var shipcheckLegs = []shipcheckLeg{
 	{
 		name: "dogfood",
@@ -118,6 +117,18 @@ var shipcheckLegs = []shipcheckLeg{
 			return a
 		},
 	},
+	{
+		name: "validate-narrative",
+		args: func(o *shipcheckOpts) []string {
+			return []string{
+				"validate-narrative",
+				"--strict",
+				"--full-examples",
+				"--research", shipcheckResearchPath(o),
+				"--binary", shipcheckCLIPath(o),
+			}
+		},
+	},
 	{
 		name: "scorecard",
 		args: func(o *shipcheckOpts) []string {
@@ -136,6 +147,18 @@ var shipcheckLegs = []shipcheckLeg{
 	},
 }
 
+func shipcheckResearchPath(o *shipcheckOpts) string {
+	dir := o.researchDir
+	if dir == "" {
+		dir = o.dir
+	}
+	return filepath.Join(dir, "research.json")
+}
+
+func shipcheckCLIPath(o *shipcheckOpts) string {
+	return filepath.Join(o.dir, filepath.Base(o.dir))
+}
+
 // shipcheckLegResult is the per-leg outcome of one umbrella run.
 type shipcheckLegResult struct {
 	Name      string
@@ -341,7 +364,7 @@ func newShipcheckCmd() *cobra.Command {
 
 	cmd := &cobra.Command{
 		Use:   "shipcheck",
-		Short: "Run all five verification legs (dogfood, verify, workflow-verify, verify-skill, scorecard) as one canonical Phase 4 sweep",
+		Short: "Run all six verification legs (dogfood, verify, workflow-verify, verify-skill, validate-narrative, 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.
@@ -351,6 +374,7 @@ Legs (in canonical order):
   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
+  validate-narrative — README/SKILL narrative commands against the built CLI
   scorecard        — Steinberger quality bar (with --live-check sampled output probes)
 
 In default mode, every leg streams its full output to the terminal as it runs
diff --git a/internal/cli/shipcheck_test.go b/internal/cli/shipcheck_test.go
index dd0c8f72..b00fac44 100644
--- a/internal/cli/shipcheck_test.go
+++ b/internal/cli/shipcheck_test.go
@@ -104,7 +104,7 @@ func runShipcheckCmd(t *testing.T, args ...string) error {
 }
 
 // TestShipcheck_AllLegsPass: every leg exits 0, umbrella returns nil.
-// All five legs must be invoked in canonical order with correct argv.
+// All six legs must be invoked in canonical order with correct argv.
 func TestShipcheck_AllLegsPass(t *testing.T) {
 	h := newShipcheckHarness(t)
 
@@ -117,8 +117,8 @@ func TestShipcheck_AllLegsPass(t *testing.T) {
 		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"}
+	// Confirm canonical order: dogfood, verify, workflow-verify, verify-skill, validate-narrative, scorecard.
+	wantOrder := []string{"dogfood", "verify", "workflow-verify", "verify-skill", "validate-narrative", "scorecard"}
 	for i, want := range wantOrder {
 		// argv[0] is the stub binary path; argv[1] is the leg name.
 		if len(invocations[i]) < 2 {
@@ -131,7 +131,7 @@ func TestShipcheck_AllLegsPass(t *testing.T) {
 }
 
 // TestShipcheck_OneLegFails: verify-skill exits 1, umbrella returns
-// ExitError with code 1; all five legs still ran (no fail-fast).
+// ExitError with code 1; all six legs still ran (no fail-fast).
 func TestShipcheck_OneLegFails(t *testing.T) {
 	h := newShipcheckHarness(t)
 	t.Setenv("STUB_EXIT_VERIFY_SKILL", "1")
@@ -232,15 +232,34 @@ func TestShipcheck_PassesSpecAndResearchDir(t *testing.T) {
 		t.Errorf("scorecard argv missing --research-dir: %v", scorecardArgs)
 	}
 
-	// workflow-verify and verify-skill don't take --spec or --research-dir;
+	// workflow-verify, verify-skill, and validate-narrative 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)
+	for _, leg := range []string{"workflow-verify", "verify-skill", "validate-narrative"} {
+		args := findInvocation(invocations, leg)
+		if argvHas(args, "--spec") {
+			t.Errorf("%s should not receive --spec; got %v", leg, args)
+		}
+		if argvHas(args, "--research-dir") {
+			t.Errorf("%s should not receive --research-dir; got %v", leg, args)
+		}
 	}
-	vsArgs := findInvocation(invocations, "verify-skill")
-	if argvHas(vsArgs, "--spec") {
-		t.Errorf("verify-skill should not receive --spec; got %v", vsArgs)
+}
+
+func TestShipcheck_ValidateNarrativeUsesResearchAndBuiltBinary(t *testing.T) {
+	h := newShipcheckHarness(t)
+	researchDir := t.TempDir()
+
+	if err := runShipcheckCmd(t, "--dir", h.dir, "--research-dir", researchDir); err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	args := findInvocation(readStubLog(t, h.logFile), "validate-narrative")
+	wantResearch := filepath.Join(researchDir, "research.json")
+	wantBinary := filepath.Join(h.dir, filepath.Base(h.dir))
+	for _, want := range []string{"--strict", "--full-examples", "--research", wantResearch, "--binary", wantBinary} {
+		if !argvHas(args, want) {
+			t.Errorf("validate-narrative argv missing %q: %v", want, args)
+		}
 	}
 }
 
diff --git a/internal/pipeline/seeds.go b/internal/pipeline/seeds.go
index 869cb57e..905705b3 100644
--- a/internal/pipeline/seeds.go
+++ b/internal/pipeline/seeds.go
@@ -274,7 +274,7 @@ date: {{now}}
 
 # Phase Goal
 
-Evaluate the generated CLI with one shipcheck block: dogfood, runtime verification, and scorecard evidence.
+Evaluate the generated CLI with one shipcheck block: dogfood, runtime verification, workflow verification, skill verification, narrative validation, and scorecard evidence.
 
 ## Context
 
@@ -299,6 +299,9 @@ Evaluate the generated CLI with one shipcheck block: dogfood, runtime verificati
 
 - printing-press dogfood --dir {{.OutputDir}} --spec <spec>
 - printing-press verify --dir {{.OutputDir}} --spec <spec> --fix
+- printing-press workflow-verify --dir {{.OutputDir}}
+- printing-press verify-skill --dir {{.OutputDir}}
+- printing-press validate-narrative --strict --full-examples --research {{.PipelineDir}}/research.json --binary {{.OutputDir}}/<cli-binary>
 - printing-press scorecard --dir {{.OutputDir}} --spec <spec>
 - Generated CLI binary and help surfaces in {{.OutputDir}}
 `,
diff --git a/skills/printing-press/SKILL.md b/skills/printing-press/SKILL.md
index 1f0dc3d1..193f5edf 100644
--- a/skills/printing-press/SKILL.md
+++ b/skills/printing-press/SKILL.md
@@ -1942,7 +1942,7 @@ Phase 1 research brief for auth requirements and manually add env var support to
 `config.go` using the pattern: add `APIKey`/`APIKeySource` fields to the Config struct,
 and `os.Getenv("<API>_API_KEY")` in the Load function.
 
-**REQUIRED: Validate narrative `command` strings before saving/publishing examples.**
+**Validate narrative `command` strings before publishing examples.**
 The LLM (or human) authoring `research.json` can name commands that don't actually
 exist in the generated CLI — `<cli> stats` when the real shape is `<cli> reports stats`,
 or a command that was dropped because its endpoint had a complex body. It can also
@@ -1951,12 +1951,14 @@ broken commands ship to the README's Quick Start (`narrative.quickstart`) and th
 SKILL's recipes (`narrative.recipes`); users copy-paste them and hit failures on the
 very first invocation.
 
-Build the CLI binary, then run `printing-press validate-narrative` against it. The
-subcommand walks every `narrative.quickstart[].command` and `narrative.recipes[].command`,
-strips the binary name and trailing arguments, and runs `<binary> <words> --help` for
-each. With `--full-examples`, it also runs the complete example under
-`PRINTING_PRESS_VERIFY=1`, appending `--dry-run` when the command advertises it. This
-catches bad flags and argument shapes without making live API calls.
+`printing-press shipcheck` now runs `validate-narrative --strict --full-examples`
+automatically after `verify` builds the CLI binary. The standalone command is still
+useful immediately after editing `research.json`: it walks every
+`narrative.quickstart[].command` and `narrative.recipes[].command`, strips the binary
+name and trailing arguments, and runs `<binary> <words> --help` for each. With
+`--full-examples`, it also runs the complete example under `PRINTING_PRESS_VERIFY=1`,
+appending `--dry-run` when the command advertises it. This catches bad flags and
+argument shapes without making live API calls.
 
 ```bash
 QUICKSTART_BINARY="$CLI_WORK_DIR/<api>-pp-cli"
@@ -2299,7 +2301,7 @@ Do not rationalize skipping transcendence features because "the CLI already work
 
 ## Phase 4: Shipcheck
 
-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).
+Run one combined verification block via the `shipcheck` umbrella, which runs all six legs (dogfood, verify, workflow-verify, verify-skill, validate-narrative, 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
@@ -2313,7 +2315,7 @@ printing-press shipcheck \
   --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.
+The umbrella defaults to `verify --fix` (auto-repair common failures), `validate-narrative --strict --full-examples` (README/SKILL narrative command validation), 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.
 
@@ -2322,6 +2324,7 @@ Interpretation:
 - `verify` catches runtime breakage and runs the auto-fix loop for common failures
 - `workflow-verify` tests the primary workflow end-to-end using the verification manifest (workflow_verify.yaml). Three verdicts: workflow-pass, workflow-fail, unverified-needs-auth
 - `verify-skill` checks that every `--flag` and command path in SKILL.md actually exists in the shipped CLI source. Catches bogus examples invented by the absorb LLM (e.g., `search --max-time` when `--max-time` is a `tonight` flag). Exit 1 = findings to fix; exit 0 = SKILL is honest.
+- `validate-narrative` checks that every README/SKILL narrative command path, flag, and argument shape in research.json resolves against the built CLI under `PRINTING_PRESS_VERIFY=1`
 - `scorecard` is the structural quality snapshot, not the source of truth by itself
 
 Fix order (update heartbeat between each fix category to prevent stale lock during long fix loops):
diff --git a/testdata/golden/cases/schema-phase5-marker/artifacts.txt b/testdata/golden/cases/schema-phase5-marker/artifacts.txt
new file mode 100644
index 00000000..a96ba8e2
--- /dev/null
+++ b/testdata/golden/cases/schema-phase5-marker/artifacts.txt
@@ -0,0 +1 @@
+# no extra artifacts
diff --git a/testdata/golden/cases/schema-phase5-marker/command.txt b/testdata/golden/cases/schema-phase5-marker/command.txt
new file mode 100644
index 00000000..0733608c
--- /dev/null
+++ b/testdata/golden/cases/schema-phase5-marker/command.txt
@@ -0,0 +1 @@
+"$BINARY" schema phase5-marker
diff --git a/testdata/golden/cases/schema-phase5-skip/artifacts.txt b/testdata/golden/cases/schema-phase5-skip/artifacts.txt
new file mode 100644
index 00000000..a96ba8e2
--- /dev/null
+++ b/testdata/golden/cases/schema-phase5-skip/artifacts.txt
@@ -0,0 +1 @@
+# no extra artifacts
diff --git a/testdata/golden/cases/schema-phase5-skip/command.txt b/testdata/golden/cases/schema-phase5-skip/command.txt
new file mode 100644
index 00000000..e8ec0164
--- /dev/null
+++ b/testdata/golden/cases/schema-phase5-skip/command.txt
@@ -0,0 +1 @@
+"$BINARY" schema phase5-skip
diff --git a/testdata/golden/expected/schema-phase5-marker/exit.txt b/testdata/golden/expected/schema-phase5-marker/exit.txt
new file mode 100644
index 00000000..573541ac
--- /dev/null
+++ b/testdata/golden/expected/schema-phase5-marker/exit.txt
@@ -0,0 +1 @@
+0
diff --git a/testdata/golden/expected/schema-phase5-marker/stderr.txt b/testdata/golden/expected/schema-phase5-marker/stderr.txt
new file mode 100644
index 00000000..e69de29b
diff --git a/testdata/golden/expected/schema-phase5-marker/stdout.txt b/testdata/golden/expected/schema-phase5-marker/stdout.txt
new file mode 100644
index 00000000..c8a1c61b
--- /dev/null
+++ b/testdata/golden/expected/schema-phase5-marker/stdout.txt
@@ -0,0 +1,35 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "https://github.com/mvanhorn/cli-printing-press/schemas/phase5-acceptance.schema.json",
+  "title": "CLI Printing Press phase5-acceptance.json",
+  "type": "object",
+  "additionalProperties": false,
+  "required": ["schema_version", "api_name", "run_id", "status", "level", "matrix_size", "tests_passed"],
+  "properties": {
+    "schema_version": {"type": "integer", "const": 1},
+    "api_name": {"type": "string", "minLength": 1},
+    "cli_name": {"type": "string"},
+    "run_id": {"type": "string", "minLength": 1},
+    "status": {"type": "string", "enum": ["pass", "fail"]},
+    "level": {"type": "string", "enum": ["quick", "full"]},
+    "matrix_size": {"type": "integer", "minimum": 1},
+    "tests_total": {"type": "integer", "minimum": 0},
+    "tests_passed": {"type": "integer", "minimum": 1},
+    "tests_skipped": {"type": "integer", "minimum": 0},
+    "tests_failed": {"type": "integer", "minimum": 0},
+    "completed_at": {"type": "string", "format": "date-time"},
+    "summary": {"type": "string"},
+    "auth_context": {"$ref": "#/$defs/auth_context"}
+  },
+  "$defs": {
+    "auth_context": {
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "type": {"type": "string"},
+        "api_key_available": {"type": "boolean"},
+        "browser_session_available": {"type": "boolean"}
+      }
+    }
+  }
+}
diff --git a/testdata/golden/expected/schema-phase5-skip/exit.txt b/testdata/golden/expected/schema-phase5-skip/exit.txt
new file mode 100644
index 00000000..573541ac
--- /dev/null
+++ b/testdata/golden/expected/schema-phase5-skip/exit.txt
@@ -0,0 +1 @@
+0
diff --git a/testdata/golden/expected/schema-phase5-skip/stderr.txt b/testdata/golden/expected/schema-phase5-skip/stderr.txt
new file mode 100644
index 00000000..e69de29b
diff --git a/testdata/golden/expected/schema-phase5-skip/stdout.txt b/testdata/golden/expected/schema-phase5-skip/stdout.txt
new file mode 100644
index 00000000..2b62aec0
--- /dev/null
+++ b/testdata/golden/expected/schema-phase5-skip/stdout.txt
@@ -0,0 +1,28 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "https://github.com/mvanhorn/cli-printing-press/schemas/phase5-skip.schema.json",
+  "title": "CLI Printing Press phase5-skip.json",
+  "type": "object",
+  "additionalProperties": false,
+  "required": ["schema_version", "api_name", "run_id", "status", "skip_reason"],
+  "properties": {
+    "schema_version": {"type": "integer", "const": 1},
+    "api_name": {"type": "string", "minLength": 1},
+    "cli_name": {"type": "string"},
+    "run_id": {"type": "string", "minLength": 1},
+    "status": {"type": "string", "const": "skip"},
+    "skip_reason": {"type": "string", "minLength": 1},
+    "auth_context": {"$ref": "#/$defs/auth_context"}
+  },
+  "$defs": {
+    "auth_context": {
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "type": {"type": "string"},
+        "api_key_available": {"type": "boolean"},
+        "browser_session_available": {"type": "boolean"}
+      }
+    }
+  }
+}

← 745f2e75 fix(cli): sync sibling list endpoints (#756)  ·  back to Cli Printing Press  ·  feat(ci): add verify-skill drift check (#766) 1af0f275 →