[object Object]

← back to Cli Printing Press

fix(cli): strip leading Markdown headings from CLI descriptions (#691)

b188dc2d607e41abb63c104be512607cf4e3436b · 2026-05-07 20:41:14 -0700 · Trevin Chow

* fix(cli): strip leading Markdown headings from CLI descriptions

Specs whose info.description leads with a Markdown heading (e.g. AeroAPI's
"# Introduction\nAeroAPI is a simple, query-based API…") were leaking into
SKILL.md frontmatter, .goreleaser.yaml brews, agent_context.go, and
mcp_context — every description surface except root.go, which has its own
generic fallback. Strip leading headings inside OneLineNormalize so all
oneline call sites inherit protection, and add CompactDescription for
top-level surfaces that need quote/backslash preservation.

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

* docs(skills): verify CLI description across all surfaces

Phase 2's "Verify the CLI description" step only directed the agent to
read root.go's Short — the one surface that's structurally immune to
the leak because it has a safe generic fallback. The four surfaces that
actually fall through to the spec's raw info.description (SKILL.md
frontmatter, goreleaser brews, agent_context.go, mcp/tools.go) shipped
unverified. Broaden the step to enumerate them and call out the failure
mode by example.

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

* docs(skills): point to handleContext map key, not a Description field

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 b188dc2d607e41abb63c104be512607cf4e3436b
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Thu May 7 20:41:14 2026 -0700

    fix(cli): strip leading Markdown headings from CLI descriptions (#691)
    
    * fix(cli): strip leading Markdown headings from CLI descriptions
    
    Specs whose info.description leads with a Markdown heading (e.g. AeroAPI's
    "# Introduction\nAeroAPI is a simple, query-based API…") were leaking into
    SKILL.md frontmatter, .goreleaser.yaml brews, agent_context.go, and
    mcp_context — every description surface except root.go, which has its own
    generic fallback. Strip leading headings inside OneLineNormalize so all
    oneline call sites inherit protection, and add CompactDescription for
    top-level surfaces that need quote/backslash preservation.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * docs(skills): verify CLI description across all surfaces
    
    Phase 2's "Verify the CLI description" step only directed the agent to
    read root.go's Short — the one surface that's structurally immune to
    the leak because it has a safe generic fallback. The four surfaces that
    actually fall through to the spec's raw info.description (SKILL.md
    frontmatter, goreleaser brews, agent_context.go, mcp/tools.go) shipped
    unverified. Broaden the step to enumerate them and call out the failure
    mode by example.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * docs(skills): point to handleContext map key, not a Description field
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 internal/generator/generator.go                    | 69 +++++++++++++----
 internal/generator/generator_test.go               |  6 ++
 internal/generator/skill_test.go                   | 50 ++++++++++++-
 internal/generator/templates/agent_context.go.tmpl |  2 +-
 internal/generator/templates/goreleaser.yaml.tmpl  |  2 +-
 internal/generator/templates/skill.md.tmpl         |  2 +-
 internal/naming/naming.go                          | 86 +++++++++++++++++++---
 internal/naming/naming_test.go                     | 10 +++
 skills/printing-press/SKILL.md                     | 29 +++++---
 9 files changed, 215 insertions(+), 41 deletions(-)

diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 3ddebb37..989472d3 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -658,18 +658,20 @@ type endpointTemplateData struct {
 // readmeTemplateData wraps APISpec with additional fields for README rendering.
 type readmeTemplateData struct {
 	*spec.APISpec
-	Sources           []ReadmeSource
-	DiscoveryPages    []string
-	NovelFeatures     []NovelFeature
-	Narrative         *ReadmeNarrative
-	ProseName         string
-	HasDataLayer      bool
-	HasAsyncJobs      bool
-	HasWriteCommands  bool
-	HasDelete         bool
-	HasAuth           bool
-	FreshnessCommands []string
-	TrafficAnalysis   *trafficAnalysisTemplateData
+	Sources            []ReadmeSource
+	DiscoveryPages     []string
+	NovelFeatures      []NovelFeature
+	Narrative          *ReadmeNarrative
+	ProseName          string
+	CompactDescription string
+	SkillDescription   string
+	HasDataLayer       bool
+	HasAsyncJobs       bool
+	HasWriteCommands   bool
+	HasDelete          bool
+	HasAuth            bool
+	FreshnessCommands  []string
+	TrafficAnalysis    *trafficAnalysisTemplateData
 	// PromotedResourceNames maps a resource name to true when the generator
 	// collapsed that single-endpoint resource into a leaf command. Templates
 	// (notably skill.md.tmpl's Command Reference) use this to emit `<cli>
@@ -686,7 +688,8 @@ type readmeTemplateData struct {
 
 type generatorTemplateData struct {
 	*spec.APISpec
-	TrafficAnalysis *trafficAnalysisTemplateData
+	CompactDescription string
+	TrafficAnalysis    *trafficAnalysisTemplateData
 }
 
 type trafficAnalysisTemplateData struct {
@@ -718,6 +721,8 @@ func (g *Generator) readmeData() *readmeTemplateData {
 		NovelFeatures:         g.NovelFeatures,
 		Narrative:             g.Narrative,
 		ProseName:             g.proseName(),
+		CompactDescription:    g.compactDescription(),
+		SkillDescription:      g.skillDescription(),
 		HasDataLayer:          g.VisionSet.Store,
 		HasAsyncJobs:          len(g.AsyncJobs) > 0,
 		HasWriteCommands:      hasWriteCommands(g.Spec.Resources),
@@ -730,6 +735,35 @@ func (g *Generator) readmeData() *readmeTemplateData {
 	}
 }
 
+func (g *Generator) compactDescription() string {
+	candidates := []string{}
+	if g.Narrative != nil {
+		candidates = append(candidates, g.Narrative.Headline)
+	}
+	if g.Spec != nil {
+		candidates = append(candidates, g.Spec.CLIDescription, g.Spec.Description)
+	}
+	for _, candidate := range candidates {
+		if desc := naming.CompactDescription(candidate); desc != "" {
+			return desc
+		}
+	}
+	return fmt.Sprintf("Printing Press CLI for %s.", g.proseName())
+}
+
+func (g *Generator) skillDescription() string {
+	switch {
+	case g.Narrative != nil && strings.TrimSpace(g.Narrative.Headline) != "":
+		return naming.CompactDescription(g.Narrative.Headline)
+	case g.Spec != nil && strings.TrimSpace(g.Spec.CLIDescription) != "":
+		return naming.CompactDescription(g.Spec.CLIDescription)
+	case g.Spec != nil && strings.TrimSpace(g.Spec.Description) != "":
+		return fmt.Sprintf("Printing Press CLI for %s. %s", g.proseName(), naming.CompactDescription(g.Spec.Description))
+	default:
+		return fmt.Sprintf("Printing Press CLI for %s.", g.proseName())
+	}
+}
+
 // freshnessCommandPaths returns the rendered slice of "covered command paths"
 // surfaced in user-facing docs (README.md and SKILL.md) for the freshness
 // section. The slice contains only paths whose subcommands actually exist in
@@ -1034,8 +1068,9 @@ func bodyIsAllFilterShape(body []spec.Param) bool {
 
 func (g *Generator) templateData() *generatorTemplateData {
 	return &generatorTemplateData{
-		APISpec:         g.Spec,
-		TrafficAnalysis: g.trafficAnalysisData(),
+		APISpec:            g.Spec,
+		CompactDescription: g.compactDescription(),
+		TrafficAnalysis:    g.trafficAnalysisData(),
 	}
 }
 
@@ -1127,7 +1162,7 @@ func safeDisplayURL(value string) string {
 func (g *Generator) buildDomainContext() DomainContext {
 	ctx := DomainContext{
 		APIName:     g.Spec.Name,
-		Description: naming.OneLine(g.Spec.Description),
+		Description: g.compactDescription(),
 		Archetype:   string(profiler.ArchetypeGeneric),
 	}
 
@@ -2382,6 +2417,7 @@ func (g *Generator) renderRootProjectFiles(promotedCommands []PromotedCommand, p
 		AsyncJobCount         int
 		HasAuthCommand        bool
 		HasDelete             bool
+		CompactDescription    string
 	}{
 		APISpec:               g.Spec,
 		VisionSet:             g.VisionSet,
@@ -2397,6 +2433,7 @@ func (g *Generator) renderRootProjectFiles(promotedCommands []PromotedCommand, p
 		AsyncJobCount:         len(g.AsyncJobs),
 		HasAuthCommand:        hasAuthCommand,
 		HasDelete:             helperFlags.HasDelete,
+		CompactDescription:    g.compactDescription(),
 	}
 	if err := g.renderTemplate("root.go.tmpl", filepath.Join("internal", "cli", "root.go"), rootData); err != nil {
 		return fmt.Errorf("rendering root: %w", err)
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 32b9176e..99a34792 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -437,6 +437,8 @@ func TestGenerateAgentContextCommand(t *testing.T) {
 
 	apiSpec, err := spec.Parse(filepath.Join("..", "..", "testdata", "stytch.yaml"))
 	require.NoError(t, err)
+	apiSpec.Description = "# Introduction\nAPI reference prose that should stay out of compact agent copy."
+	apiSpec.CLIDescription = "Manage Stytch users and sessions from the terminal."
 
 	outputDir := filepath.Join(t.TempDir(), naming.CLI(apiSpec.Name))
 	gen := New(apiSpec, outputDir)
@@ -462,6 +464,8 @@ func TestGenerateAgentContextCommand(t *testing.T) {
 	// (which shifts when the longest field name in the literal grows)
 	// doesn't break the assertion.
 	assert.Regexp(t, `Use:\s+"agent-context"`, src, `agent_context.go missing Use: "agent-context"`)
+	assert.Contains(t, src, `Description: "Manage Stytch users and sessions from the terminal."`)
+	assert.NotContains(t, src, "# Introduction")
 	// agent-context only reads CLI tree state and emits JSON to stdout.
 	// The runtime walker uses this annotation to set readOnlyHint on
 	// the resulting MCP tool so hosts skip the per-call permission prompt.
@@ -4894,6 +4898,7 @@ func TestGenerateMCPContextEscapesDomainStrings(t *testing.T) {
 	apiSpec, err := spec.Parse(filepath.Join("..", "..", "testdata", "stytch.yaml"))
 	require.NoError(t, err)
 	apiSpec.Description = `Stytch "quoted" API \ context`
+	apiSpec.CLIDescription = `Manage "quoted" Stytch sessions from C:\tmp.`
 	apiSpec.Auth.KeyURL = `https://example.test/keys?label="quoted"&path=\demo`
 
 	users := apiSpec.Resources["users"]
@@ -4921,6 +4926,7 @@ func TestGenerateMCPContextEscapesDomainStrings(t *testing.T) {
 	require.NoError(t, err, "MCP tools source must remain valid Go when context strings contain quotes and backslashes")
 
 	src := string(data)
+	assert.Contains(t, src, `Manage \"quoted\" Stytch sessions from C:\\tmp.`)
 	assert.Contains(t, src, `label=\"quoted\"&path=\\demo`)
 	assert.Contains(t, src, `Quote \"dashboard\"`)
 	assert.Contains(t, src, `filter=\"active\"`)
diff --git a/internal/generator/skill_test.go b/internal/generator/skill_test.go
index a7a170ae..ce775ab5 100644
--- a/internal/generator/skill_test.go
+++ b/internal/generator/skill_test.go
@@ -245,13 +245,13 @@ func TestSkillUsesExplicitDisplayNameForProse(t *testing.T) {
 }
 
 // TestSkillFrontmatterFallbackHandlesMultilineSpecDescription asserts that
-// OpenAPI specs with multi-line info.description values don't break the
-// YAML frontmatter in the narrative-absent fallback path.
+// OpenAPI specs with multi-line, heading-led info.description values don't
+// break the YAML frontmatter or leak a Markdown heading into compact copy.
 func TestSkillFrontmatterFallbackHandlesMultilineSpecDescription(t *testing.T) {
 	t.Parallel()
 
 	apiSpec := minimalSpec("multiline")
-	apiSpec.Description = "Line one of the description.\nLine two has more detail.\nLine three."
+	apiSpec.Description = "# Introduction\nLine one of the description.\nLine two has more detail.\nLine three."
 	outputDir := filepath.Join(t.TempDir(), "multiline-pp-cli")
 	gen := New(apiSpec, outputDir)
 	require.NoError(t, gen.Generate())
@@ -270,11 +270,55 @@ func TestSkillFrontmatterFallbackHandlesMultilineSpecDescription(t *testing.T) {
 	require.NoError(t, yaml.Unmarshal([]byte(body), &parsed),
 		"frontmatter must be valid YAML even with multi-line spec description")
 	assert.True(t, strings.HasPrefix(parsed.Description, "Printing Press CLI for Multiline"))
+	assert.False(t, strings.Contains(parsed.Description, "# Introduction"),
+		"description should not retain leading Markdown headings")
 	// Multi-line description should be flattened by oneline helper.
 	assert.False(t, strings.Contains(parsed.Description, "\n"),
 		"description should not contain raw newlines after oneline flattening")
 }
 
+func TestGoreleaserDescriptionUsesCompactMarkdownFreeDescription(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := minimalSpec("brewdesc")
+	apiSpec.Description = "# Introduction\nAeroAPI gives developers access to current and historical flight data.\n\n## Details\nMore verbose content."
+	outputDir := filepath.Join(t.TempDir(), "brewdesc-pp-cli")
+	gen := New(apiSpec, outputDir)
+	require.NoError(t, gen.Generate())
+
+	goreleaser, err := os.ReadFile(filepath.Join(outputDir, ".goreleaser.yaml"))
+	require.NoError(t, err)
+	content := string(goreleaser)
+
+	assert.Contains(t, content, `description: "AeroAPI gives developers access to current and historical flight data."`)
+	assert.NotContains(t, content, "# Introduction")
+	assert.NotContains(t, content, "## Details")
+}
+
+func TestCompactDescriptionPrefersCLIShapedCopy(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := minimalSpec("cliproduct")
+	apiSpec.Description = "# Introduction\nAPI-shaped docs that should not become product copy."
+	apiSpec.CLIDescription = "Search routes, compare prices, and track reliability from the terminal."
+	outputDir := filepath.Join(t.TempDir(), "cliproduct-pp-cli")
+	gen := New(apiSpec, outputDir)
+	require.NoError(t, gen.Generate())
+
+	skill, err := os.ReadFile(filepath.Join(outputDir, "SKILL.md"))
+	require.NoError(t, err)
+	end := strings.Index(string(skill[4:]), "\n---\n")
+	require.NotEqual(t, -1, end)
+	frontmatter := string(skill[:4+end+5])
+	goreleaser, err := os.ReadFile(filepath.Join(outputDir, ".goreleaser.yaml"))
+	require.NoError(t, err)
+
+	assert.Contains(t, frontmatter, `description: "Search routes, compare prices, and track reliability from the terminal."`)
+	assert.Contains(t, string(goreleaser), `description: "Search routes, compare prices, and track reliability from the terminal."`)
+	assert.NotContains(t, frontmatter, "# Introduction")
+	assert.NotContains(t, string(goreleaser), "# Introduction")
+}
+
 // TestSkillRendersAuthBranchPerType asserts the deterministic Auth Setup
 // block branches correctly on .Auth.Type when no narrative auth is provided.
 func TestSkillRendersAuthBranchPerType(t *testing.T) {
diff --git a/internal/generator/templates/agent_context.go.tmpl b/internal/generator/templates/agent_context.go.tmpl
index e0989eba..126e3b81 100644
--- a/internal/generator/templates/agent_context.go.tmpl
+++ b/internal/generator/templates/agent_context.go.tmpl
@@ -143,7 +143,7 @@ func buildAgentContext(rootCmd *cobra.Command) agentContext {
 		SchemaVersion: agentContextSchemaVersion,
 		CLI: agentContextCLI{
 			Name:        {{printf "%q" (printf "%s-pp-cli" .Name)}},
-			Description: {{printf "%q" (oneline .Description)}},
+			Description: {{printf "%q" .CompactDescription}},
 			Version:     rootCmd.Version,
 		},
 		Auth: agentContextAuth{
diff --git a/internal/generator/templates/goreleaser.yaml.tmpl b/internal/generator/templates/goreleaser.yaml.tmpl
index 6548ac1b..3ca36139 100644
--- a/internal/generator/templates/goreleaser.yaml.tmpl
+++ b/internal/generator/templates/goreleaser.yaml.tmpl
@@ -47,7 +47,7 @@ brews:
       owner: {{.Owner}}
       name: homebrew-tap
     homepage: "https://github.com/{{.Owner}}/{{.Name}}-pp-cli"
-    description: "{{.Description}}"
+    description: "{{yamlDoubleQuoted .CompactDescription}}"
     install: |
       bin.install "{{.Name}}-pp-cli"
 {{- if .VisionSet.MCP}}
diff --git a/internal/generator/templates/skill.md.tmpl b/internal/generator/templates/skill.md.tmpl
index 633771d7..6ec07e55 100644
--- a/internal/generator/templates/skill.md.tmpl
+++ b/internal/generator/templates/skill.md.tmpl
@@ -1,6 +1,6 @@
 ---
 name: pp-{{.Name}}
-description: "{{if and .Narrative .Narrative.Headline}}{{yamlDoubleQuoted .Narrative.Headline}}{{else}}Printing Press CLI for {{yamlDoubleQuoted .ProseName}}. {{yamlDoubleQuoted (oneline .Description)}}{{end}}{{if and .Narrative .Narrative.TriggerPhrases}} Trigger phrases: {{range $i, $p := .Narrative.TriggerPhrases}}{{if $i}}, {{end}}`{{yamlDoubleQuoted $p}}`{{end}}.{{end}}"
+description: "{{yamlDoubleQuoted .SkillDescription}}{{if and .Narrative .Narrative.TriggerPhrases}} Trigger phrases: {{range $i, $p := .Narrative.TriggerPhrases}}{{if $i}}, {{end}}`{{yamlDoubleQuoted $p}}`{{end}}.{{end}}"
 author: "{{yamlDoubleQuoted .OwnerName}}"
 license: "Apache-2.0"
 argument-hint: "<command> [args] | install cli|mcp"
diff --git a/internal/naming/naming.go b/internal/naming/naming.go
index c5d270c9..f2893064 100644
--- a/internal/naming/naming.go
+++ b/internal/naming/naming.go
@@ -10,6 +10,8 @@ import (
 	"golang.org/x/text/language"
 )
 
+var leadingMarkdownHeadingRE = regexp.MustCompile(`^#{1,6}\s+(.+)$`)
+
 // ASCIIFold transliterates Unicode to ASCII via Unidecode tables (the
 // same ones Django's slugify and Rails use). Apply at every chokepoint
 // that turns user-supplied spec strings (titles, resource names,
@@ -160,16 +162,7 @@ func EnvVarPlaceholder(envVar string) string {
 // including hand-authored mcp-descriptions.json overrides — use OneLineNormalize
 // instead, which does the same normalization without the length cap.
 func OneLine(s string) string {
-	s = OneLineNormalize(s)
-	if len(s) > 120 {
-		cut := s[:117]
-		if idx := strings.LastIndex(cut, " "); idx > 60 {
-			s = cut[:idx] + "..."
-		} else {
-			s = cut + "..."
-		}
-	}
-	return s
+	return truncateOneLine(OneLineNormalize(s))
 }
 
 // OneLineNormalize collapses whitespace, newlines, and quotes into a
@@ -177,6 +170,7 @@ func OneLine(s string) string {
 // that's already curated for length (MCP tool descriptions, agent-authored
 // overrides) where truncating would defeat the purpose.
 func OneLineNormalize(s string) string {
+	s = stripLeadingMarkdownHeading(s)
 	s = strings.ReplaceAll(s, "\r\n", " ")
 	s = strings.ReplaceAll(s, "\n", " ")
 	s = strings.ReplaceAll(s, "\r", " ")
@@ -188,6 +182,78 @@ func OneLineNormalize(s string) string {
 	return strings.TrimSpace(s)
 }
 
+// CompactDescription produces compact human-facing copy for catalog, skill,
+// Homebrew, and agent-context descriptions. Unlike OneLine, it preserves
+// quotes and backslashes because callers are responsible for escaping in their
+// target format.
+func CompactDescription(s string) string {
+	s = stripLeadingMarkdownHeading(s)
+	s = collapseWhitespace(s)
+	return truncateOneLine(s)
+}
+
+func collapseWhitespace(s string) string {
+	s = strings.ReplaceAll(s, "\r\n", " ")
+	s = strings.ReplaceAll(s, "\n", " ")
+	s = strings.ReplaceAll(s, "\r", " ")
+	for strings.Contains(s, "  ") {
+		s = strings.ReplaceAll(s, "  ", " ")
+	}
+	return strings.TrimSpace(s)
+}
+
+func truncateOneLine(s string) string {
+	if len(s) > 120 {
+		cut := s[:117]
+		if idx := strings.LastIndex(cut, " "); idx > 60 {
+			s = cut[:idx] + "..."
+		} else {
+			s = cut + "..."
+		}
+	}
+	return s
+}
+
+func stripLeadingMarkdownHeading(s string) string {
+	normalized := strings.ReplaceAll(s, "\r\n", "\n")
+	normalized = strings.ReplaceAll(normalized, "\r", "\n")
+	lines := strings.Split(normalized, "\n")
+	for i, line := range lines {
+		trimmed := strings.TrimSpace(line)
+		if trimmed == "" {
+			continue
+		}
+		m := leadingMarkdownHeadingRE.FindStringSubmatch(trimmed)
+		if m == nil {
+			return s
+		}
+		rest := firstParagraphAfter(lines[i+1:])
+		if rest != "" {
+			return rest
+		}
+		return strings.TrimSpace(m[1])
+	}
+	return s
+}
+
+func firstParagraphAfter(lines []string) string {
+	var paragraph []string
+	for _, line := range lines {
+		trimmed := strings.TrimSpace(line)
+		if trimmed == "" {
+			if len(paragraph) > 0 {
+				break
+			}
+			continue
+		}
+		if len(paragraph) > 0 && leadingMarkdownHeadingRE.MatchString(trimmed) {
+			break
+		}
+		paragraph = append(paragraph, line)
+	}
+	return strings.TrimSpace(strings.Join(paragraph, "\n"))
+}
+
 // MCPDescription builds an MCP tool description with optional minority-side
 // auth annotation. It annotates only when an API has a mix of public and
 // auth-required tools, and only the minority side gets annotated.
diff --git a/internal/naming/naming_test.go b/internal/naming/naming_test.go
index a09477cc..15d18251 100644
--- a/internal/naming/naming_test.go
+++ b/internal/naming/naming_test.go
@@ -160,6 +160,8 @@ func TestOneLine(t *testing.T) {
 		"too  many   spaces": "too many spaces",
 		`say "hello"`:        "say 'hello'",
 		"  spaces  ":         "spaces",
+		"# Introduction\nAeroAPI delivers flight data.": "AeroAPI delivers flight data.",
+		"## Overview": "Overview",
 	}
 
 	for input, want := range tests {
@@ -173,6 +175,14 @@ func TestOneLine(t *testing.T) {
 	}
 }
 
+func TestCompactDescriptionPreservesHumanText(t *testing.T) {
+	input := "# Introduction\nAn \"agent-native\" CLI with C:\\tmp paths."
+	want := `An "agent-native" CLI with C:\tmp paths.`
+	if got := CompactDescription(input); got != want {
+		t.Fatalf("CompactDescription(%q) = %q, want %q", input, got, want)
+	}
+}
+
 func TestMCPDescription(t *testing.T) {
 	tests := []struct {
 		name        string
diff --git a/skills/printing-press/SKILL.md b/skills/printing-press/SKILL.md
index cba74130..06ff89b6 100644
--- a/skills/printing-press/SKILL.md
+++ b/skills/printing-press/SKILL.md
@@ -1910,15 +1910,26 @@ GraphQL-only APIs:
 
 After generation:
 
-**Verify the CLI description.** The root cobra command's `Short:` text comes from
-the spec's `cli_description` field when set, then falls back to `narrative.headline`
-in `research.json`, then to a generic `"Manage <api> resources via the <api> API"`.
-The first two phrasings describe what the *CLI does* ("Manage payments, subscriptions,
-and invoices via the Stripe API"); the generic fallback often does too but reads as
-template filler. Open `$CLI_WORK_DIR/internal/cli/root.go`, find the `Short:` field,
-and confirm it reads as user-facing CLI purpose rather than API description. If it
-needs a rewrite, prefer adding a `cli_description:` line to the spec and regenerating
-over hand-editing the generated file.
+**Verify the CLI description across every surface.** A single curated one-liner is
+rendered into five files: `internal/cli/root.go` (`Short:`), `SKILL.md` frontmatter
+(`description:`), `.goreleaser.yaml` (`brews:` description), `internal/cli/agent_context.go`
+(`Description:`), and `internal/mcp/tools.go` (the `handleContext` response's `"description"` key). Each resolves
+from the authored sources (`narrative.headline` in `research.json`, or `cli_description:`
+in the spec) when set. `root.go`'s `Short:` has a safe generic fallback (`"Manage <api>
+resources via the <api> API"`); the other four fall through to the spec's raw
+`info.description` — which is often the upstream OpenAPI blob leading with a Markdown
+heading like `# Introduction` followed by API-shaped paragraphs. Eyeballing only `root.go`
+will miss the failure mode because `root.go` is the only surface that's structurally
+immune.
+
+Open at least the `SKILL.md` frontmatter `description:` and the `.goreleaser.yaml` `brews:`
+block in addition to `root.go`'s `Short:`. If any reads as API documentation rather than
+user-facing CLI purpose ("AeroAPI is a simple, query-based API…"), or contains a bare
+Markdown heading, the authored sources are missing. Fix at the source: set
+`narrative.headline` in `research.json` to a single-sentence differentiator (name what
+makes this CLI worth using, don't restate the API), or add a `cli_description:` line to
+the spec. Then regenerate. Do not hand-edit the printed files — they revert on the next
+regen.
 
 **REQUIRED: Preserve README sections.** The generated README contains 5 standard sections
 that the scorecard checks for: Quick Start, Agent Usage, Health Check, Troubleshooting, and

← fe625c6b fix(cli): dedupe regen-merge registrations by command use (#  ·  back to Cli Printing Press  ·  chore(main): release 4.0.4 (#692) af70886c →