← 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
M internal/generator/generator.goM internal/generator/generator_test.goM internal/generator/skill_test.goM internal/generator/templates/agent_context.go.tmplM internal/generator/templates/goreleaser.yaml.tmplM internal/generator/templates/skill.md.tmplM internal/naming/naming.goM internal/naming/naming_test.goM skills/printing-press/SKILL.md
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 →