← back to Cli Printing Press
docs(plans): autonomous CLI pipeline plan and status updates
9cfae3e4107c1ffd4f246e899f1e7efbebe25d4b · 2026-03-24 06:59:19 -0700 · Matt Van Horn
Files touched
A docs/plans/2026-03-24-feat-autonomous-cli-pipeline-plan.mdA docs/plans/2026-03-24-feat-goat-plan-gogcli-parity-and-press-upgrades-plan.mdA docs/plans/2026-03-24-fix-gmail-cli-dogfood-gaps-plan.md
Diff
commit 9cfae3e4107c1ffd4f246e899f1e7efbebe25d4b
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date: Tue Mar 24 06:59:19 2026 -0700
docs(plans): autonomous CLI pipeline plan and status updates
---
...2026-03-24-feat-autonomous-cli-pipeline-plan.md | 513 +++++++++++++++++++++
...t-plan-gogcli-parity-and-press-upgrades-plan.md | 222 +++++++++
.../2026-03-24-fix-gmail-cli-dogfood-gaps-plan.md | 86 ++++
3 files changed, 821 insertions(+)
diff --git a/docs/plans/2026-03-24-feat-autonomous-cli-pipeline-plan.md b/docs/plans/2026-03-24-feat-autonomous-cli-pipeline-plan.md
new file mode 100644
index 00000000..f194242d
--- /dev/null
+++ b/docs/plans/2026-03-24-feat-autonomous-cli-pipeline-plan.md
@@ -0,0 +1,513 @@
+---
+title: "Autonomous CLI Pipeline - From API Name to Finished CLI"
+type: feat
+status: active
+date: 2026-03-24
+---
+
+# Autonomous CLI Pipeline - From API Name to Finished CLI
+
+## Overview
+
+Transform the printing press from a single-pass generator into a multi-phase autonomous pipeline. The user says `printing-press print gmail`. The press discovers the spec, scaffolds, enriches with deep research, reviews, and ships - each phase writing checkpoint state and chaining to the next. Zero human intervention. Overnight quality.
+
+Inspired by OSC nightnight's proven patterns: checkpoint-based resumption, mechanical budget gates, CronCreate session chaining, JSONL event logging.
+
+## Problem Statement
+
+The press currently does one pass: spec in, CLI out, 70% quality. The missing 30% is information that exists in the spec and public docs but requires multiple analysis passes with different lenses to extract. A single context window can't hold spec parsing + deep doc research + dogfood analysis + enrichment application. The solution is phase chaining - the same pattern that lets OSC run for 8 hours overnight.
+
+## Architecture: The Print Pipeline
+
+```
+printing-press print gmail
+ |
+ [PREFLIGHT] - Go installed? Binary fresh? Disk space?
+ |
+ [DISCOVER] - Find OpenAPI spec (known-specs, apis-guru, WebSearch)
+ |
+ [SCAFFOLD] - Generate initial CLI (existing generate command)
+ |
+ [ENRICH] - Deep-read spec hints + research API docs -> spec overlay
+ |
+ [REGENERATE]- Re-run generator with enriched spec (SCAFFOLD + overlay)
+ |
+ [REVIEW] - Static analysis: help output, name quality, compilation, coverage
+ |
+ [SHIP] - git init, README, goreleaser validate, morning report
+ |
+ Done. CLI at ./gmail-cli/
+```
+
+**Key decision: ENRICH modifies the spec, not generated code.** The generator remains the single source of truth for code output. ENRICH produces a YAML overlay that gets merged into the original spec before re-generation. This collapses the original 6-phase plan into 7 tighter phases (PREFLIGHT + DISCOVER + SCAFFOLD + ENRICH + REGENERATE + REVIEW + SHIP) where ENRICH + REGENERATE replace the earlier ENRICH + POLISH split.
+
+## Scope Boundaries
+
+- No runtime library - templates only
+- No real API testing in REVIEW (no credential management) - static analysis only
+- No GitHub repo creation or release publishing in SHIP - local only
+- No Google Discovery format converter - use apis-guru pre-converted specs
+- No file upload/download generation (separate plan)
+- No batch mode (one API per pipeline invocation)
+- Single Claude Code skill with phase routing (not 7 separate skills)
+
+## Implementation Units
+
+### Unit 1: Checkpoint Schema and State Machine
+
+**Goal:** Define the pipeline's backbone - the checkpoint JSON that every phase reads and writes. Model after nightnight's `~/.osc/nightnight-session.json`.
+
+**Files:**
+- New: `internal/pipeline/checkpoint.go` - checkpoint types, read/write, phase enum
+- New: `internal/pipeline/events.go` - JSONL event logger
+
+**Approach:**
+
+Checkpoint schema:
+```go
+type Checkpoint struct {
+ APIName string `json:"api_name"`
+ StartedAt time.Time `json:"started_at"`
+ OutputDir string `json:"output_dir"`
+ SpecPath string `json:"spec_path"` // discovered spec file
+ OverlayPath string `json:"overlay_path"` // enrichment overlay
+ ConventionsPath string `json:"conventions_path"` // conventions cache
+ EventsLogPath string `json:"events_log_path"` // JSONL log
+ Phases map[string]Phase `json:"phases"`
+ Errors []string `json:"errors"`
+}
+
+type Phase struct {
+ Status string `json:"status"` // pending, in_progress, completed, failed
+ StartedAt time.Time `json:"started_at"`
+ Duration string `json:"duration"`
+ Retries int `json:"retries"`
+}
+```
+
+Checkpoint location: `~/.cache/printing-press/pipelines/<api-name>/checkpoint.json`
+Events log: `~/.cache/printing-press/pipelines/<api-name>/events.jsonl`
+Conventions cache: `~/.cache/printing-press/pipelines/<api-name>/conventions.json`
+
+Phase order: `preflight -> discover -> scaffold -> enrich -> regenerate -> review -> ship`
+
+Mechanical gate between phases: phase must be `completed` to advance. Failed after 2 retries = pipeline parks with morning report.
+
+**Test scenarios:**
+- Checkpoint round-trips through JSON marshal/unmarshal
+- Phase state machine transitions correctly (pending -> in_progress -> completed)
+- Events append to JSONL without corruption
+
+**Verification:** `go test ./internal/pipeline/...`
+
+---
+
+### Unit 2: PREFLIGHT Phase
+
+**Goal:** Validate environment before burning any tokens. Go installed, printing-press binary compiles, output dir clean.
+
+**Files:**
+- New: `internal/pipeline/preflight.go`
+
+**Approach:**
+1. Check `go version` succeeds
+2. Check printing-press binary builds: `go build -o /tmp/pp-check ./cmd/printing-press`
+3. Check output dir doesn't exist (or `--force` flag)
+4. Check disk space > 500MB free
+5. Write checkpoint with preflight=completed
+
+**Test scenarios:**
+- Preflight passes on healthy system
+- Preflight fails gracefully when Go missing (clear error message)
+
+**Verification:** Unit tests with mock exec
+
+---
+
+### Unit 3: DISCOVER Phase
+
+**Goal:** Given an API name, find a usable OpenAPI spec. Check known-specs registry, then apis-guru, then WebSearch.
+
+**Files:**
+- New: `internal/pipeline/discover.go`
+- Update: `skills/printing-press/references/known-specs.md` - add apis-guru URL patterns
+
+**Approach:**
+
+Discovery pipeline (ordered by reliability):
+1. **Known-specs registry** - exact match on API name. Already has 12 verified URLs.
+2. **apis-guru directory** - fetch `https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/<provider>/` and find the latest version. This covers 2,000+ APIs including all Google services.
+3. **WebSearch** - `"<api-name> openapi spec" site:github.com OR site:swagger.io` with 3 strategies
+4. **Fallback** - prompt the user (in interactive mode) or park (in autonomous mode)
+
+Spec quality gate: after fetching, validate:
+- Parses as valid OpenAPI (use existing `openapi.IsOpenAPI()` + `openapi.Parse()`)
+- Has at least 3 endpoints
+- Has at least 1 resource
+- Has a base URL
+
+If quality gate fails, try next discovery source.
+
+**Conventions cache:** After successful parse, write to conventions cache:
+- Auth type and schemes detected
+- Number of resources/endpoints
+- Pagination patterns found
+- Global params detected
+- Common prefix (if any)
+
+**Test scenarios:**
+- Discovery finds Petstore from known-specs
+- Discovery finds Gmail from apis-guru
+- Discovery falls back to WebSearch for unknown APIs
+- Quality gate rejects specs with 0 endpoints
+
+**Verification:** `go test ./internal/pipeline/...`, manual test with `gmail` and `stripe`
+
+---
+
+### Unit 4: SCAFFOLD Phase
+
+**Goal:** Run the existing `printing-press generate` command on the discovered spec. Validate with 7 quality gates.
+
+**Files:**
+- New: `internal/pipeline/scaffold.go`
+- Existing: `internal/generator/generator.go`, `internal/generator/validate.go`
+
+**Approach:**
+1. Read spec from checkpoint's `spec_path`
+2. Parse with existing openapi/spec parser
+3. Generate with existing generator
+4. Run 7 quality gates
+5. If gates fail: retry once with relaxed settings (skip lint gate), then park
+6. Update checkpoint with scaffold=completed
+
+This is a thin wrapper around the existing `generate` + `validate` pipeline.
+
+**Test scenarios:**
+- SCAFFOLD succeeds for Petstore, Gmail, Discord, Stytch specs
+- SCAFFOLD retries on quality gate failure
+- SCAFFOLD parks after 2 failures
+
+**Verification:** `go test ./...`, regenerate all 4 test specs
+
+---
+
+### Unit 5: ENRICH Phase - The Intelligence Layer
+
+**Goal:** Deep-read the original spec for hints the parser missed. Research the API's public docs. Produce a spec overlay that improves the generated CLI.
+
+**Files:**
+- New: `internal/pipeline/enrich.go`
+- New: `internal/pipeline/overlay.go` - spec overlay types and merge logic
+
+**Approach:**
+
+The overlay format mirrors `spec.APISpec` but every field is optional. Non-nil fields override the original spec when merged.
+
+```go
+type SpecOverlay struct {
+ Description *string `yaml:"description,omitempty"`
+ Resources map[string]ResourceOverlay `yaml:"resources,omitempty"`
+}
+
+type ResourceOverlay struct {
+ Description *string `yaml:"description,omitempty"`
+ Endpoints map[string]EndpointOverlay `yaml:"endpoints,omitempty"`
+}
+
+type EndpointOverlay struct {
+ Description *string `yaml:"description,omitempty"`
+ Params []ParamPatch `yaml:"params,omitempty"`
+}
+
+type ParamPatch struct {
+ Name string `yaml:"name"`
+ Default *string `yaml:"default,omitempty"` // e.g., "me" for userId
+}
+```
+
+Enrichment strategies (each runs independently, results merged):
+
+1. **Description hints** - scan param descriptions for default value hints. Gmail's userId says "The special value `me` can be used" - extract `me` as default.
+2. **mediaUpload detection** - scan for `x-]google-*` extensions or multipart content types. Flag endpoints that support file upload (future: generate upload commands).
+3. **Sync token patterns** - scan response schemas for `historyId`, `syncToken`, `nextSyncToken` fields. Flag resources that support incremental sync.
+4. **Batch endpoint detection** - scan for paths ending in `:batchGet`, `:batchCreate`, or `batch*` with array request bodies.
+5. **Better descriptions** - for endpoints with empty or generic descriptions, check if the operationId or path provides a clearer name.
+6. **Endpoint grouping hints** - for flat resources with many endpoints (like settings), detect naming patterns that suggest sub-groups (e.g., `cse-*`, `send-as-*`).
+
+Time budget: 15 minutes max. Each strategy gets 2 minutes. Partial results are written if time runs out.
+
+**Test scenarios:**
+- Gmail enrichment detects `me` default for userId
+- Gmail enrichment detects mediaUpload on messages/send
+- Overlay merges correctly with original spec (non-nil fields override)
+- Overlay merge preserves unmodified fields
+
+**Verification:** `go test ./internal/pipeline/...`, manual test with Gmail spec
+
+---
+
+### Unit 6: REGENERATE Phase
+
+**Goal:** Merge the spec overlay with the original spec and re-run the generator.
+
+**Files:**
+- New: `internal/pipeline/regenerate.go`
+- Update: `internal/spec/spec.go` - add `MergeOverlay()` method
+
+**Approach:**
+1. Load original spec from checkpoint
+2. Load overlay from checkpoint
+3. Merge: overlay fields replace original fields where non-nil
+4. Re-run generator with merged spec (overwrite existing output)
+5. Re-run 7 quality gates
+6. If gates fail: fall back to original spec (enrichment broke something), log the error
+
+**Test scenarios:**
+- Regenerate with overlay produces different output than without
+- Regenerate fallback works when overlay breaks compilation
+- Quality gates still pass after regeneration
+
+**Verification:** `go test ./...`
+
+---
+
+### Unit 7: REVIEW Phase - Static Quality Analysis
+
+**Goal:** Automated quality assessment of the generated CLI. No API calls - static analysis only.
+
+**Files:**
+- New: `internal/pipeline/review.go`
+
+**Approach:**
+
+Review checks (binary pass/fail):
+1. **Help completeness** - run `<cli> --help` and every `<cli> <resource> --help`. Verify exit code 0, non-empty output.
+2. **Name quality** - no command name > 40 chars, no raw operationId passthrough (contains dots or underscores), no duplicate command names.
+3. **Param coverage** - every GET endpoint has at least 1 non-positional param (if the original spec had query params). Flag endpoints where all params were filtered.
+4. **Description quality** - no empty descriptions on top-level resources. No descriptions that are just the endpoint name repeated.
+5. **Compilation clean** - `go vet` passes with zero warnings.
+6. **Binary size** - warn if > 50MB (something is wrong).
+7. **Doctor works** - `<cli> doctor` exits 0.
+
+Review produces a quality score (0-100) and a list of issues. Score > 70 = PASS. Score < 70 = log issues in morning report but still PASS (don't block shipping over cosmetics).
+
+**Test scenarios:**
+- Review passes for a well-generated CLI
+- Review catches empty descriptions
+- Review catches overly long command names
+
+**Verification:** `go test ./internal/pipeline/...`
+
+---
+
+### Unit 8: SHIP Phase
+
+**Goal:** Finalize the generated CLI for human consumption.
+
+**Files:**
+- New: `internal/pipeline/ship.go`
+
+**Approach:**
+1. Run `git init` in output directory
+2. Create initial commit: "Initial CLI generated by printing-press"
+3. Validate goreleaser config: `goreleaser check` (if installed) or skip
+4. Write morning report to `~/.cache/printing-press/pipelines/<api-name>/report.md`
+5. Update checkpoint with ship=completed, total duration
+6. Print summary to stderr
+
+Morning report includes:
+- API name and spec source
+- Resources and endpoint count
+- Quality score from REVIEW
+- Enrichments applied
+- Issues found
+- Time per phase
+- Next steps for the human (configure auth, test against real API, publish)
+
+**Test scenarios:**
+- SHIP creates git repo with initial commit
+- Morning report contains all required sections
+- Pipeline completion updates checkpoint correctly
+
+**Verification:** `go test ./internal/pipeline/...`
+
+---
+
+### Unit 9: Pipeline Orchestrator and CLI Command
+
+**Goal:** Wire all phases together. Add `printing-press print <api-name>` command. Handle chaining, resume, and error recovery.
+
+**Files:**
+- New: `internal/pipeline/pipeline.go` - orchestrator
+- Update: `internal/cli/root.go` - add `print` command
+
+**Approach:**
+
+The `print` command:
+```
+printing-press print gmail [--output ./gmail-cli] [--force] [--resume] [--phase enrich]
+```
+
+Flags:
+- `--output` - output directory (default: `./<api-name>-cli`)
+- `--force` - overwrite existing output
+- `--resume` - resume from checkpoint
+- `--phase` - run only a specific phase (for debugging)
+
+Orchestrator loop:
+```go
+func RunPipeline(apiName string, opts Options) error {
+ checkpoint := loadOrCreateCheckpoint(apiName, opts)
+
+ phases := []struct {
+ name string
+ fn func(*Checkpoint) error
+ }{
+ {"preflight", runPreflight},
+ {"discover", runDiscover},
+ {"scaffold", runScaffold},
+ {"enrich", runEnrich},
+ {"regenerate", runRegenerate},
+ {"review", runReview},
+ {"ship", runShip},
+ }
+
+ for _, phase := range phases {
+ if checkpoint.Phases[phase.name].Status == "completed" {
+ continue // already done (resume mode)
+ }
+
+ logEvent(checkpoint, "phase_start", phase.name)
+ checkpoint.Phases[phase.name] = Phase{Status: "in_progress", StartedAt: time.Now()}
+ saveCheckpoint(checkpoint)
+
+ var err error
+ for attempt := 0; attempt < 3; attempt++ {
+ err = phase.fn(checkpoint)
+ if err == nil {
+ break
+ }
+ checkpoint.Phases[phase.name] = Phase{Status: "failed", Retries: attempt + 1}
+ logEvent(checkpoint, "phase_retry", phase.name, err.Error())
+ }
+
+ if err != nil {
+ checkpoint.Errors = append(checkpoint.Errors, fmt.Sprintf("%s: %v", phase.name, err))
+ saveCheckpoint(checkpoint)
+ writeMorningReport(checkpoint) // partial report
+ return fmt.Errorf("pipeline failed at %s: %w", phase.name, err)
+ }
+
+ checkpoint.Phases[phase.name] = Phase{Status: "completed", Duration: time.Since(...).String()}
+ saveCheckpoint(checkpoint)
+ logEvent(checkpoint, "phase_completed", phase.name)
+ }
+
+ writeMorningReport(checkpoint)
+ return nil
+}
+```
+
+**Test scenarios:**
+- Full pipeline completes for Petstore (simplest spec)
+- Resume skips completed phases
+- Pipeline parks on repeated failure with morning report
+- `--phase` runs only the specified phase
+
+**Verification:** `go test ./...`, manual end-to-end: `printing-press print petstore`
+
+---
+
+### Unit 10: Claude Code Skill - The `print` Skill
+
+**Goal:** Update the printing-press Claude Code skill to support the autonomous pipeline. User says "print me a gmail CLI" or `/printing-press gmail` and it runs the full pipeline.
+
+**Files:**
+- Update: `skills/printing-press/SKILL.md`
+
+**Approach:**
+
+Add a new workflow to the existing skill:
+
+**Workflow 0 (updated): Autonomous Pipeline**
+1. Parse API name from user's message
+2. Run `printing-press print <api-name> --output ./<api-name>-cli`
+3. Monitor output for phase completion messages
+4. On completion: present morning report, show example commands
+5. On failure: show error, offer to resume or debug specific phase
+
+For overnight/autonomous mode (when invoked by nightnight-style chaining):
+- Read checkpoint, determine current phase
+- Execute next phase
+- If more phases remain: CronCreate to chain back in 30 seconds
+- If pipeline complete: write morning report, stop chaining
+
+**Test scenarios:**
+- User says "print me a stripe CLI" - full pipeline runs
+- User says "resume gmail" - picks up from checkpoint
+
+**Verification:** Manual test in Claude Code session
+
+## Dependencies
+
+```
+Unit 1 (Checkpoint) - foundation, everything depends on this
+Unit 2 (Preflight) - depends on Unit 1
+Unit 3 (Discover) - depends on Unit 1
+Unit 4 (Scaffold) - depends on Unit 1
+Unit 5 (Enrich) - depends on Unit 1
+Unit 6 (Regenerate) - depends on Unit 5
+Unit 7 (Review) - depends on Unit 1
+Unit 8 (Ship) - depends on Unit 1
+Unit 9 (Orchestrator) - depends on Units 1-8
+Unit 10 (Skill) - depends on Unit 9
+```
+
+Units 2-5, 7-8 can be built in parallel after Unit 1.
+Unit 6 needs Unit 5's overlay types.
+Unit 9 wires everything together.
+Unit 10 is the skill layer on top.
+
+## What "Done" Looks Like
+
+```bash
+# One command, one API name, finished CLI
+printing-press print gmail
+
+# Output:
+# [preflight] Go 1.23 OK, disk OK
+# [discover] Found Gmail spec via apis-guru (79 endpoints, OAuth2)
+# [scaffold] Generated gmail-cli (7/7 gates passed)
+# [enrich] Applied 12 enrichments (userId default=me, 3 upload hints, 8 better descriptions)
+# [regenerate] Re-generated with enrichments (7/7 gates passed)
+# [review] Quality score: 87/100 (2 minor issues)
+# [ship] Initialized git repo, wrote morning report
+#
+# gmail-cli ready at ./gmail-cli/
+# Resources: messages, labels, drafts, threads, history, settings, profile
+# Auth: OAuth2 (run `gmail-cli auth login` to authenticate)
+# Total time: 4m 23s
+
+cd gmail-cli && go build -o ./gmail ./cmd/gmail-cli
+./gmail auth login --client-id $GOOGLE_CLIENT_ID
+./gmail messages list me --limit 5
+./gmail doctor
+```
+
+That's not a scaffold. That's a finished CLI. From two words.
+
+## Sources & References
+
+### Internal
+- OSC nightnight chaining pattern: `~/.claude/skills/osc-nightnight/SKILL.md` - checkpoint JSON, CronCreate chaining, mechanical budget gates, JSONL events
+- OSC work execution: `~/.claude/skills/osc-work/SKILL.md` - phased CI execution, conventions cache, post-submission monitoring
+- OSC plan discovery: `~/.claude/skills/osc-plan/SKILL.md` - 7-layer discovery pipeline, per-issue scoring
+- Press generator: `internal/generator/generator.go` - 14 templates, 7 quality gates
+- Press parser: `internal/openapi/parser.go` - 1,900 lines, extracts auth/resources/params/pagination
+- Press spec types: `internal/spec/spec.go` - APISpec struct, validation
+- Known specs registry: `skills/printing-press/references/known-specs.md` - 12 verified URLs
+
+### External
+- apis-guru OpenAPI directory: `https://github.com/APIs-guru/openapi-directory` - 2,000+ pre-converted specs
+- Google Discovery -> OpenAPI conversions at `APIs-guru/openapi-directory/APIs/googleapis.com/`
diff --git a/docs/plans/2026-03-24-feat-goat-plan-gogcli-parity-and-press-upgrades-plan.md b/docs/plans/2026-03-24-feat-goat-plan-gogcli-parity-and-press-upgrades-plan.md
new file mode 100644
index 00000000..15f71345
--- /dev/null
+++ b/docs/plans/2026-03-24-feat-goat-plan-gogcli-parity-and-press-upgrades-plan.md
@@ -0,0 +1,222 @@
+---
+title: "The GOAT Plan - Print gogcli-Quality CLIs and Close Every Gap"
+type: feat
+status: completed
+date: 2026-03-24
+---
+
+# The GOAT Plan - Print gogcli-Quality CLIs and Close Every Gap
+
+## Context
+
+gogcli is Peter Steinberger's Google Workspace CLI: 80 commands across 8 services, OAuth2 with keyring, FTS5 search, multi-account. If the printing press can match gogcli quality, it can print anything.
+
+## What's Already Done
+
+- Deep sub-resource detection with common prefix collapse (Gmail now generates `messages list` not `gmail users-messages-list`)
+- Global param filtering (Google's 10 API-wide params stripped from every endpoint)
+- Pagination detection and `--all` flag generation (parser detects pageToken/nextPageToken patterns, templates generate pagination loop)
+- Retry with exponential backoff
+- Dry-run mode
+- Table output with column auto-detection
+- Structured exit codes
+- Usage examples generation
+
+## Scope Boundaries
+
+- No Discovery-to-OpenAPI converter (use pre-converted specs from APIs-guru)
+- No file upload/download (binary content handling is a separate plan)
+- No Apps Script execution
+- Templates only - no runtime library
+
+## Implementation Units
+
+### Unit 1: Color and TTY Detection
+
+**Goal:** Generated CLIs detect terminal vs pipe and apply color/bold formatting. Doctor output gets colored checkmarks. Tables get bold headers.
+
+**Files:**
+- `internal/generator/templates/helpers.go.tmpl` - add `isTerminal()`, color helpers
+- `internal/generator/templates/doctor.go.tmpl` - colored output
+- `internal/generator/templates/go.mod.tmpl` - add `mattn/go-isatty` dependency
+
+**Approach:**
+1. Add `mattn/go-isatty v0.0.20` to go.mod.tmpl
+2. In helpers.go.tmpl, add:
+ - `isTerminal()` that checks `os.Stdout` via isatty
+ - `colorEnabled()` that returns false if `NO_COLOR` env is set, `TERM=dumb`, or not a terminal
+ - `bold(s string)` and `green(s string)` and `red(s string)` ANSI wrappers that no-op when color disabled
+3. In doctor.go.tmpl, replace "PASS"/"FAIL" strings with `green("PASS")` and `red("FAIL")`
+4. In helpers.go.tmpl, update `renderTable` to use bold headers when terminal
+5. Add `--no-color` persistent flag to root.go.tmpl
+
+**Patterns to follow:** Existing `helpers.go.tmpl` render functions, `root.go.tmpl` persistent flags pattern
+
+**Execution note:** Straightforward, no test-first needed
+
+**Test scenarios:**
+- All 4 specs (Petstore, Stytch, Discord, Gmail) still compile after template changes
+- Generated CLI `--help` shows `--no-color` flag
+- Doctor output includes ANSI codes when run in terminal
+
+**Verification:** `go test ./...` passes, regenerate all 4 specs and `go build` each
+
+---
+
+### Unit 2: URL-Based Spec Input
+
+**Goal:** `printing-press generate --spec https://petstore3.swagger.io/api/v3/openapi.json` fetches the spec from URL, caches it, and generates.
+
+**Files:**
+- `internal/cli/generate.go` (or wherever the generate command lives) - URL detection and fetching
+- New file: `internal/cli/fetch.go` - HTTP fetch + cache logic
+
+**Approach:**
+1. In the generate command, before reading the spec file, check if `--spec` value starts with `http://` or `https://`
+2. If URL: fetch with `net/http`, cache to `~/.cache/printing-press/specs/<sha256-of-url>.json`
+3. If cached file exists and is <24h old, use cache. `--refresh` flag forces re-download
+4. Auto-detect format from Content-Type header or file extension in URL
+5. Pass fetched bytes to existing parse pipeline
+
+**Patterns to follow:** Existing `internal/cli/` command structure
+
+**Execution note:** Find the generate command first - `grep -rn "generate" internal/cli/`
+
+**Test scenarios:**
+- `printing-press generate --spec https://petstore3.swagger.io/api/v3/openapi.json --output /tmp/pet-url-test` works
+- Second run uses cache (faster)
+- `--refresh` forces re-download
+- Invalid URL gives clear error
+
+**Verification:** `go test ./...` passes, manual test with Petstore URL
+
+---
+
+### Unit 3: OAuth2 Auth Flow
+
+**Goal:** Generated CLIs support `auth login` (browser-based OAuth2), `auth status`, `auth logout`. Tokens stored in config file. Auto-refresh on expiry.
+
+**Files:**
+- New template: `internal/generator/templates/auth.go.tmpl` - auth login/logout/status commands
+- `internal/generator/templates/config.go.tmpl` - add token storage fields
+- `internal/generator/templates/client.go.tmpl` - auto-refresh before API calls
+- `internal/generator/templates/root.go.tmpl` - register auth command group
+- `internal/generator/generator.go` - conditionally render auth.go.tmpl when auth type is oauth2
+- `internal/generator/templates/go.mod.tmpl` - add `golang.org/x/oauth2` dependency
+
+**Approach:**
+1. Parser already detects OAuth2 (`auth.Type = "bearer_token"` when scheme type is oauth2). Need to also capture the OAuth2 flow URLs (authorization endpoint, token endpoint) from the security scheme.
+2. Update `spec.AuthConfig` to include `AuthorizationURL`, `TokenURL`, `Scopes` fields
+3. In `parser.go`, extract OAuth2 flow details when `scheme.Type == "oauth2"` and `scheme.Flows.AuthorizationCode` exists
+4. New `auth.go.tmpl`:
+ - `auth login`: starts local HTTP server on `localhost:8085/callback`, opens browser to authorization URL with redirect_uri, exchanges code for tokens, stores in config.toml
+ - `auth status`: reads stored token, shows expiry, account info
+ - `auth logout`: removes stored tokens
+5. In `config.go.tmpl`, add `AccessToken`, `RefreshToken`, `TokenExpiry`, `ClientID`, `ClientSecret` fields. Load/save to config.toml
+6. In `client.go.tmpl`, before each request check if token is expired, if so use refresh_token to get new one, update config
+7. In `generator.go`, only render `auth.go.tmpl` when `spec.Auth.Type == "bearer_token"` and `spec.Auth.AuthorizationURL != ""`
+8. In `root.go.tmpl`, conditionally register auth subcommand
+
+**Patterns to follow:** Existing `doctor.go.tmpl` command structure, `config.go.tmpl` for file storage
+
+**Execution note:** This is the largest unit. Build auth.go.tmpl first, then wire up config and client changes. Test with Gmail spec.
+
+**Test scenarios:**
+- Gmail CLI generates with `auth login`, `auth status`, `auth logout` commands
+- Petstore CLI (no OAuth2) does NOT generate auth commands
+- Discord CLI (apiKey auth) does NOT generate auth commands
+- `gmail-cli auth login --help` shows expected flags
+- All 4 specs compile after template changes
+
+**Verification:** `go test ./...` passes, regenerate all 4 specs and `go build` each. `gmail-cli auth --help` shows login/status/logout.
+
+---
+
+### Unit 4: Multi-Spec Composition
+
+**Goal:** `printing-press generate --spec gmail.yaml --spec calendar.yaml --output google-cli` merges multiple API specs into one CLI with each spec as a top-level resource group.
+
+**Files:**
+- `internal/cli/generate.go` - accept multiple `--spec` flags
+- `internal/spec/spec.go` - add `MergeSpecs()` function
+- `internal/generator/generator.go` - minor updates for merged spec
+
+**Approach:**
+1. Change `--spec` flag from `StringVar` to `StringSliceVar` (cobra supports this)
+2. Parse each spec independently
+3. New `MergeSpecs(specs []*APISpec) *APISpec` function:
+ - First spec's name becomes the CLI name, or use `--name` flag
+ - Each spec's resources get prefixed with the spec name if there are conflicts
+ - Auth configs merged (prefer OAuth2 if any spec uses it)
+ - Description concatenated
+4. Pass merged spec to generator
+
+**Patterns to follow:** Existing spec parsing pipeline
+
+**Execution note:** Depends on having multiple Google API specs in testdata. Start by duplicating/modifying Petstore to create a second test spec, or download Calendar spec.
+
+**Test scenarios:**
+- `printing-press generate --spec petstore.yaml --spec stytch.yaml --output multi-test` generates a combined CLI
+- Both specs' resources appear as commands
+- Single doctor command checks both
+- Single `--spec` still works (backwards compatible)
+
+**Verification:** `go test ./...` passes, manual test with two specs
+
+---
+
+### Unit 5: Dogfood Gmail CLI End-to-End
+
+**Goal:** Generate Gmail CLI, verify all commands look right, ensure help output is clean.
+
+**Files:** No press changes - this is pure verification.
+
+**Approach:**
+1. Regenerate Gmail CLI with latest press
+2. Build it
+3. Check every top-level resource has clean commands
+4. Spot-check 5+ endpoints for clean flags (no global params, good names)
+5. Check `doctor` output (should show OAuth2 auth if Unit 3 landed)
+6. Document any remaining gaps as follow-up issues
+
+**Verification:** Gmail CLI compiles, all resources have clean command names, no endpoint limit warnings, no global params on individual commands.
+
+## Dependencies
+
+```
+Unit 1 (Color) - independent
+Unit 2 (URL) - independent
+Unit 3 (OAuth2) - independent
+Unit 4 (Multi-Spec)- independent
+Unit 5 (Dogfood) - depends on Units 1-4
+```
+
+Units 1-4 can run in parallel (they touch different files). Unit 5 is final verification.
+
+## Execution Loop
+
+```
+for each unit:
+ 1. Edit templates/parser
+ 2. go test ./...
+ 3. Regenerate all 4 specs
+ 4. go build each generated CLI
+ 5. Check help output
+ 6. Fix what's wrong
+ 7. Commit when green
+```
+
+## What "Done" Looks Like
+
+```bash
+# Print a Gmail CLI from a URL
+printing-press generate --spec https://gmail.googleapis.com/$discovery/rest?version=v1 --output gmail-cli
+
+# Use it
+cd gmail-cli && go build -o ./gmail ./cmd/gmail-cli
+./gmail auth login # opens browser, OAuth2 flow
+./gmail messages list me --limit 5 # table output, paginated, colored
+./gmail messages list me --all # fetches everything with progress
+./gmail labels list me # formatted table with bold headers
+./gmail doctor # colored checkmarks
+```
diff --git a/docs/plans/2026-03-24-fix-gmail-cli-dogfood-gaps-plan.md b/docs/plans/2026-03-24-fix-gmail-cli-dogfood-gaps-plan.md
new file mode 100644
index 00000000..dfe9590c
--- /dev/null
+++ b/docs/plans/2026-03-24-fix-gmail-cli-dogfood-gaps-plan.md
@@ -0,0 +1,86 @@
+---
+title: "Fix Gmail CLI Dogfood Gaps - Deep Sub-Resources, Global Params, Endpoint Cap"
+type: fix
+status: completed
+date: 2026-03-24
+---
+
+# Fix Gmail CLI Dogfood Gaps
+
+## The 4 Problems
+
+The press prints a Gmail CLI that compiles and passes 7 gates, but it's not usable. Four specific problems found during dogfooding:
+
+### 1. Flat command names (no deep sub-resources)
+
+**Current:** `gmail-cli gmail users-messages-list <userId>`
+**Want:** `gmail-cli messages list <userId>`
+
+Gmail's paths are `/gmail/v1/users/{userId}/messages/{id}`. The parser detects `gmail` as the primary resource and `users` as the first sub-resource after the `v1` version segment. But `messages` is a sub-sub-resource (two levels deep) and gets flattened into the endpoint name instead of becoming its own command group.
+
+**Fix:** Make sub-resource detection recursive. Walk the path segments, skip params, and create nested sub-resources for each non-param segment after the primary. For Gmail:
+- `/gmail/v1/users/{userId}/messages` -> resource: `gmail`, sub: `users`, sub-sub: `messages`
+- Or better: detect that `users/{userId}` is just a required context param (every Gmail endpoint has it) and treat `messages`, `labels`, `drafts`, `threads` as the real top-level resources
+
+**Approach:** When all paths under a resource share the same path prefix pattern (e.g., all start with `/gmail/v1/users/{userId}/`), collapse that prefix into required positional args and use the next segment as the resource name.
+
+**Files:** `internal/openapi/parser.go` (resourceAndSubFromPath, mapResources)
+
+**Acceptance:**
+- [ ] `gmail-cli messages list <userId>` instead of `gmail-cli gmail users-messages-list <userId>`
+- [ ] `gmail-cli labels list <userId>` instead of `gmail-cli gmail users-labels-list <userId>`
+- [ ] `gmail-cli drafts create <userId>` instead of `gmail-cli gmail users-drafts-create <userId>`
+
+### 2. Google global params on every command
+
+**Current:** Every Gmail command shows `--access-token`, `--alt`, `--callback`, `--fields`, `--key`, `--oauth-token`, `--pretty-print`, `--quota-user`, `--upload-protocol`, `--upload-type` - 10 extra flags that clutter the help.
+
+These are Google API-wide parameters defined at the path level or as common parameters, not endpoint-specific.
+
+**Fix:** Detect "global" parameters that appear on every single endpoint in the spec. Move them to persistent flags on the root command (or filter them out entirely since they're rarely used by humans).
+
+**Approach:** After parsing all endpoints, count parameter frequency. Any param that appears on >80% of endpoints is "global" - remove it from individual endpoint params and optionally add it as a persistent root flag.
+
+**Files:** `internal/openapi/parser.go` (new post-processing step after mapResources)
+
+**Acceptance:**
+- [ ] `gmail-cli messages list --help` does NOT show `--alt`, `--callback`, `--fields`, etc.
+- [ ] Global params are either on root command or omitted entirely
+- [ ] Endpoint-specific params (like `--q`, `--maxResults`, `--labelIds`) still show
+
+### 3. Endpoint cap hit (20 per resource)
+
+**Current:** `watch` and `untrash` endpoints skipped because `gmail` resource hit 20-endpoint cap.
+
+**Fix:** With deep sub-resources (fix #1), endpoints spread across `messages`, `labels`, `drafts`, `threads`, `history`, `settings` sub-resources. Each gets its own 20-endpoint limit. This should resolve naturally once fix #1 lands.
+
+**Acceptance:**
+- [ ] Zero "endpoint limit reached" warnings for Gmail spec
+- [ ] All 49 Gmail paths generate commands
+
+### 4. No OAuth2 auth flow
+
+**Current:** `gmail-cli doctor` shows `auth=not configured`. Gmail requires OAuth2 - there's no API key option.
+
+**This is a bigger feature (from the GOAT plan A2) - not fixing in this plan.** This plan focuses on the structural issues (#1-3) that are press bugs. OAuth2 is a new feature that needs its own plan with browser callback server, token storage, refresh flow.
+
+**Workaround for testing:** User can manually set `--access-token` flag or configure `auth_header` in config.toml with a token obtained from Google's OAuth2 Playground.
+
+## Execution Loop
+
+```
+while (not good):
+ 1. Fix the press (parser.go only for #1-3)
+ 2. go test ./...
+ 3. Regenerate all 4 specs (Petstore, Stytch, Discord, Gmail)
+ 4. Check gmail-cli help output
+ 5. Find what's still wrong
+ 6. Go to 1
+```
+
+## Scope
+
+- Only modify `internal/openapi/parser.go`
+- Don't break Petstore, Stytch, or Discord
+- Don't add OAuth2 (separate plan)
+- Don't change templates
← bb12a605 feat(cli): multi-spec composition with --spec repetition
·
back to Cli Printing Press
·
feat(pipeline): add pipeline state manager with phase tracki e2560ea7 →