[object Object]

← back to Cli Printing Press

fix(cli): generator template polish (WU-1, F1+F2+F3+F6) (#576)

542835d393bf3efdc5870ef9c00feedbe0f0edf6 · 2026-05-04 12:13:46 -0700 · Trevin Chow

* fix(cli): generator template polish — usage line, framework Examples, framework JSON envelopes, sync trailing line

Four template defects in internal/generator/templates/ recur on every printed
CLI; movie-goat reprint surfaced them via dogfood and patched them post-hoc.
This change applies the same fixes back into the templates so future printed
CLIs ship clean.

F1 — Usage-line double-print
  command_promoted.go.tmpl, command_endpoint.go.tmpl: drop cmd.Root().Name()
  from the format args so usage errors print the binary name once, not twice.
  cmd.CommandPath() already begins with the root command's name.

F2 — Framework command Examples
  doctor.go.tmpl, profile.go.tmpl (save/use/list/show/delete), feedback.go.tmpl:
  add Example: blocks using the {{ .Name }}-pp-cli convention. Dogfood
  help-check failed for these on every CLI.

F3 — Framework command JSON envelopes
  auth.go.tmpl, auth_simple.go.tmpl, auth_client_credentials.go.tmpl: status,
    logout, set-token (where applicable) emit JSON envelopes when --json
    is set. auth status's not-authenticated path still returns authErr after
    writing JSON so exit code stays consistent.
  api_discovery.go.tmpl: data restructured to a typed []ifaceEntry consumed by
    both human and JSON paths; emits {interfaces, note} or {interface, methods}.
  import.go.tmpl: {succeeded, failed, skipped}.
  profile.go.tmpl delete: {deleted: name}.
  tail.go.tmpl no-arg path: {resources, note} (helper tailKnownResources()
    sources from .Resources, independent of sync's defaultSyncResources).
  which.go.tmpl: swap json.NewEncoder for printJSONFiltered; wrap as
    {matches: [...]} envelope (the wrap routes through compactObjectFields'
    blocklist instead of compactListFields' allowlist that would strip the
    Entry/Score keys); empty-match returns {matches: []} exit 0; non-JSON
    keeps the existing exit-2 on no-match human path.
  command_promoted.go.tmpl no-query path: write {error, usage} envelope first,
    then return usageErr so exit code stays 2 across modes.

F6 — Sync trailing human line
  sync.go.tmpl, graphql_sync.go.tmpl: wrap the trailing "Sync complete:"
  fmt.Fprintf(os.Stderr, ...) block in `if humanFriendly { ... }` so it's
  suppressed under --json. The sync_summary JSON event already carries the
  same data.

JSON envelope shapes are best-effort-stable contracts going forward (additive
changes only) — regression-guarded by U5's golden artifact byte-comparison.

Plan: docs/plans/2026-05-04-002-fix-generator-template-polish-plan.md
Closes mvanhorn/cli-printing-press#572 (parent retro mvanhorn/cli-printing-press#571 — Movie Goat)

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

* docs(cli): add plan for generator template polish (WU-1)

Plan authored from retro #571 sub-issue #572. Walked through ce-doc-review
with 13 findings applied (3 P1 critical: sync gate variable correction, auth
template flavors expansion, golden harness pivot to byte-comparison). Plan
status: completed by PR #576.

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

* refactor(cli): simplify generator templates per /simplify review

Four small cleanups applied after /simplify reviewer pass on the WU-1
template polish (PR #576):

api_discovery.go.tmpl
  Drop the parallel `interfaces []string` slice; iterate `ifaces`
  directly in the human-output path. The format string is built once
  per print, not stored. Removes one drift point and avoids building
  the human strings under --json.
  Also trim the WHAT-narrating sentence in the helper comment, keeping
  only the WHY about pre-formatting blocking the JSON path.

sync.go.tmpl, graphql_sync.go.tmpl
  Flatten the trailing summary block from `if !humanFriendly { JSON } if
  humanFriendly { if/else }` into `if humanFriendly { if/else } else {
  JSON }`. Single conditional, mutually exclusive branches, eliminates
  the future-edit footgun where both flags set would emit twice.

tail.go.tmpl
  Drop the dead `if resources == nil { resources = []string{} }` guard.
  `tailKnownResources()` returns a `[]string{...}` literal which is
  always non-nil even when empty.

Golden fixtures regenerated; `scripts/golden.sh verify` passes 11/11.

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 542835d393bf3efdc5870ef9c00feedbe0f0edf6
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Mon May 4 12:13:46 2026 -0700

    fix(cli): generator template polish (WU-1, F1+F2+F3+F6) (#576)
    
    * fix(cli): generator template polish — usage line, framework Examples, framework JSON envelopes, sync trailing line
    
    Four template defects in internal/generator/templates/ recur on every printed
    CLI; movie-goat reprint surfaced them via dogfood and patched them post-hoc.
    This change applies the same fixes back into the templates so future printed
    CLIs ship clean.
    
    F1 — Usage-line double-print
      command_promoted.go.tmpl, command_endpoint.go.tmpl: drop cmd.Root().Name()
      from the format args so usage errors print the binary name once, not twice.
      cmd.CommandPath() already begins with the root command's name.
    
    F2 — Framework command Examples
      doctor.go.tmpl, profile.go.tmpl (save/use/list/show/delete), feedback.go.tmpl:
      add Example: blocks using the {{ .Name }}-pp-cli convention. Dogfood
      help-check failed for these on every CLI.
    
    F3 — Framework command JSON envelopes
      auth.go.tmpl, auth_simple.go.tmpl, auth_client_credentials.go.tmpl: status,
        logout, set-token (where applicable) emit JSON envelopes when --json
        is set. auth status's not-authenticated path still returns authErr after
        writing JSON so exit code stays consistent.
      api_discovery.go.tmpl: data restructured to a typed []ifaceEntry consumed by
        both human and JSON paths; emits {interfaces, note} or {interface, methods}.
      import.go.tmpl: {succeeded, failed, skipped}.
      profile.go.tmpl delete: {deleted: name}.
      tail.go.tmpl no-arg path: {resources, note} (helper tailKnownResources()
        sources from .Resources, independent of sync's defaultSyncResources).
      which.go.tmpl: swap json.NewEncoder for printJSONFiltered; wrap as
        {matches: [...]} envelope (the wrap routes through compactObjectFields'
        blocklist instead of compactListFields' allowlist that would strip the
        Entry/Score keys); empty-match returns {matches: []} exit 0; non-JSON
        keeps the existing exit-2 on no-match human path.
      command_promoted.go.tmpl no-query path: write {error, usage} envelope first,
        then return usageErr so exit code stays 2 across modes.
    
    F6 — Sync trailing human line
      sync.go.tmpl, graphql_sync.go.tmpl: wrap the trailing "Sync complete:"
      fmt.Fprintf(os.Stderr, ...) block in `if humanFriendly { ... }` so it's
      suppressed under --json. The sync_summary JSON event already carries the
      same data.
    
    JSON envelope shapes are best-effort-stable contracts going forward (additive
    changes only) — regression-guarded by U5's golden artifact byte-comparison.
    
    Plan: docs/plans/2026-05-04-002-fix-generator-template-polish-plan.md
    Closes mvanhorn/cli-printing-press#572 (parent retro mvanhorn/cli-printing-press#571 — Movie Goat)
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * docs(cli): add plan for generator template polish (WU-1)
    
    Plan authored from retro #571 sub-issue #572. Walked through ce-doc-review
    with 13 findings applied (3 P1 critical: sync gate variable correction, auth
    template flavors expansion, golden harness pivot to byte-comparison). Plan
    status: completed by PR #576.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * refactor(cli): simplify generator templates per /simplify review
    
    Four small cleanups applied after /simplify reviewer pass on the WU-1
    template polish (PR #576):
    
    api_discovery.go.tmpl
      Drop the parallel `interfaces []string` slice; iterate `ifaces`
      directly in the human-output path. The format string is built once
      per print, not stored. Removes one drift point and avoids building
      the human strings under --json.
      Also trim the WHAT-narrating sentence in the helper comment, keeping
      only the WHY about pre-formatting blocking the JSON path.
    
    sync.go.tmpl, graphql_sync.go.tmpl
      Flatten the trailing summary block from `if !humanFriendly { JSON } if
      humanFriendly { if/else }` into `if humanFriendly { if/else } else {
      JSON }`. Single conditional, mutually exclusive branches, eliminates
      the future-edit footgun where both flags set would emit twice.
    
    tail.go.tmpl
      Drop the dead `if resources == nil { resources = []string{} }` guard.
      `tailKnownResources()` returns a `[]string{...}` literal which is
      always non-nil even when empty.
    
    Golden fixtures regenerated; `scripts/golden.sh verify` passes 11/11.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 ...05-04-002-fix-generator-template-polish-plan.md |  349 ++++++
 internal/generator/templates/api_discovery.go.tmpl |   46 +-
 internal/generator/templates/auth.go.tmpl          |   21 +-
 .../templates/auth_client_credentials.go.tmpl      |   50 +-
 internal/generator/templates/auth_simple.go.tmpl   |   50 +-
 .../generator/templates/command_endpoint.go.tmpl   |    2 +-
 .../generator/templates/command_promoted.go.tmpl   |   12 +-
 internal/generator/templates/doctor.go.tmpl        |    3 +
 internal/generator/templates/feedback.go.tmpl      |    3 +
 internal/generator/templates/graphql_sync.go.tmpl  |   17 +-
 internal/generator/templates/import.go.tmpl        |    8 +
 internal/generator/templates/profile.go.tmpl       |   22 +-
 internal/generator/templates/sync.go.tmpl          |   17 +-
 internal/generator/templates/tail.go.tmpl          |   33 +-
 internal/generator/templates/which.go.tmpl         |   22 +-
 .../golden/cases/generate-golden-api/artifacts.txt |    6 +
 .../printing-press-oauth2-cc/internal/cli/auth.go  |   48 +-
 .../internal/cli/api_discovery.go                  |  109 ++
 .../printing-press-golden/internal/cli/auth.go     |   48 +-
 .../printing-press-golden/internal/cli/doctor.go   |    3 +
 .../printing-press-golden/internal/cli/feedback.go |  225 ++++
 .../printing-press-golden/internal/cli/import.go   |  109 ++
 .../printing-press-golden/internal/cli/profile.go  |  344 ++++++
 .../internal/cli/projects_tasks_update-project.go  |    2 +-
 .../printing-press-golden/internal/cli/sync.go     | 1114 ++++++++++++++++++++
 .../printing-press-golden/internal/cli/which.go    |  219 ++++
 .../tier-routing-golden/internal/cli/doctor.go     |    3 +
 .../tier-routing-golden/internal/cli/sync.go       |   17 +-
 28 files changed, 2838 insertions(+), 64 deletions(-)

diff --git a/docs/plans/2026-05-04-002-fix-generator-template-polish-plan.md b/docs/plans/2026-05-04-002-fix-generator-template-polish-plan.md
new file mode 100644
index 00000000..213c5646
--- /dev/null
+++ b/docs/plans/2026-05-04-002-fix-generator-template-polish-plan.md
@@ -0,0 +1,349 @@
+---
+title: Generator template polish — Usage line, framework Examples, framework JSON envelopes, sync trailing line
+type: fix
+status: active
+date: 2026-05-04
+origin: https://github.com/mvanhorn/cli-printing-press/issues/572
+---
+
+# Generator template polish — Usage line, framework Examples, framework JSON envelopes, sync trailing line
+
+## Summary
+
+Patch four defects in `internal/generator/templates/` so future printed CLIs ship with correct usage strings, framework-command Examples, framework-command JSON envelopes, and clean `sync --json` output. Reference shapes are already proven in `~/printing-press/library/movie-goat/` from this run.
+
+---
+
+## Problem Frame
+
+The movie-goat run surfaced four template defects (filed as findings F1, F2, F3, F6 in retro #571, absorbed into WU-1 / sub-issue #572). All four have already been patched directly in the printed CLI, which proved the target shapes work; they recur on every future generation because the templates haven't been touched. The durable fix is mechanical: copy the patched shapes back into the templates so the next CLI lands without these defects.
+
+---
+
+## Requirements
+
+- R1. Generated CLIs emit a single binary name in the usage error for any required-positional command (no `<cli> <cli> <subcmd>` doubling).
+- R2. Generated framework commands (`doctor`, `profile save/use/list/show/delete`, `feedback list`) emit an `Examples:` block in `--help`.
+- R3. Generated framework commands that print human prose by default (`auth status/logout/set-token`, `api`, `import`, `profile delete`, `tail` no-arg, `which`, `multi` no-arg) emit valid JSON when `--json` is set.
+- R4. Generated `sync` command produces output parseable as line-delimited JSON when `--json` is set, with `{"event":"sync_summary",...}` as the last non-empty line. No trailing human prose.
+- R5. Existing non-JSON behavior outside the four fixes (F1, F2, F3, F6) is unchanged across all affected commands. The fixes themselves (usage-line binary name, Examples blocks, JSON envelopes, sync trailing-line gate) are intentional changes and do not count as regressions under R5.
+- R6. The existing `testdata/golden/cases/generate-golden-api/` fixture is extended with new rendered-Go-source artifacts to lock these four contracts as regression guards. No new fixture case is added; byte-comparison of generated `internal/cli/*.go` files is the enforcement mechanism (see U5).
+
+---
+
+## Scope Boundaries
+
+- Generator templates only. No changes to printed CLIs already in `~/printing-press/library/`; backports are explicitly excluded — those are printed-CLI fixes outside this WU's scope.
+- Live dogfood matrix accuracy fixes (F4, F5 from the parent retro) are out of scope; they live in WU-2 / sub-issue #573 with its own plan.
+- F7 (inline `#` shell-comment in Cobra Examples) is out of scope; skipped at retro triage as an authoring-guidance issue rather than a template change.
+- No refactor of the `flags.asJSON` pattern or `printJSONFiltered` helper. The existing helpers and patterns are extended in place; no new helpers are introduced.
+
+### Deferred to Follow-Up Work
+
+- Backporting the new JSON envelope shapes into already-published library CLIs — separate concern, follow-up PRs against `printing-press-library` per CLI when (or if) MCP hosts depend on the new shapes.
+
+---
+
+## Context & Research
+
+### Relevant Code and Patterns
+
+- `internal/generator/templates/` — all 11 template files in scope:
+  - F1: `command_promoted.go.tmpl:95`, `command_endpoint.go.tmpl:128`
+  - F2: `doctor.go.tmpl`, `profile.go.tmpl`, `feedback.go.tmpl`
+  - F3: `auth.go.tmpl`, `api_discovery.go.tmpl`, `import.go.tmpl`, `profile.go.tmpl` (delete subcommand), `tail.go.tmpl`, `which.go.tmpl`, `command_promoted.go.tmpl`
+  - F6: `sync.go.tmpl` (and `graphql_sync.go.tmpl` if it has the same trailing-line issue)
+- `printJSONFiltered` helper — already used in `analytics.go.tmpl`, `feedback.go.tmpl`, `profile.go.tmpl`, `which.go.tmpl`, `share_commands.go.tmpl`, etc. The canonical shape is `return printJSONFiltered(cmd.OutOrStdout(), value, flags)` inside an `if flags.asJSON { ... }` branch. Extends without modification.
+- Binary-name placeholder convention — templates already use `{{ .Name }}-pp-cli` for in-text references (e.g., `doctor.go.tmpl` has `"{{ .Name }}-pp-cli auth login --chrome"`). New `Example:` strings follow the same pattern.
+- Reference shapes proven in this run — every JSON envelope and Examples block from movie-goat's printed CLI:
+  - `internal/cli/auth.go` (status / logout / set-token JSON envelopes)
+  - `internal/cli/api_discovery.go` (`{interfaces, note}`)
+  - `internal/cli/import.go` (`{succeeded, failed, skipped}`)
+  - `internal/cli/profile.go` (delete envelope + Examples on save/use/list/show/delete)
+  - `internal/cli/tail.go` (no-arg JSON help envelope)
+  - `internal/cli/which.go` (matches envelope)
+  - `internal/cli/sync.go` (suppress trailing human line under --json)
+  - `internal/cli/promoted_multi.go` (no-query JSON envelope)
+  - `internal/cli/doctor.go`, `feedback.go` (Examples)
+
+### Institutional Learnings
+
+- `docs/solutions/best-practices/adaptive-rate-limiting-sniffed-apis.md` — adjacent (rate limiting, not template polish); not directly applicable but confirms the pattern of capturing learnings from per-CLI patches that became generator fixes.
+- AGENTS.md golden harness section — "Golden cases must be deterministic, offline, and auth-free." Extending the existing `golden-api` fixture is the right shape; new fixtures only when current ones can't exercise the contract.
+
+### External References
+
+- None required. The WU is mechanical template polish with all shapes already proven in a printed CLI.
+
+---
+
+## Key Technical Decisions
+
+- **Use `printJSONFiltered`, not `json.NewEncoder`, for new JSON envelope branches.** AGENTS.md Phase 3 rule #2 mandates `printJSONFiltered` for hand-written novel commands so `--select`/`--compact`/`--csv` work; the same rule applies to generator-emitted framework commands. `which.go.tmpl` currently uses bare `json.NewEncoder` — bring it into line with the rest. Rationale: consistent flag behavior across every command in every printed CLI.
+- **Treat new JSON envelope shapes as best-effort-stable contracts, regression-guarded by U5's golden snapshots.** Once shipped, MCP hosts and agents will depend on the keys: `{authenticated, source, config}`, `{cleared, note}`, `{saved, config_path}`, `{interfaces, note}`, `{succeeded, failed, skipped}`, `{deleted}`, `{resources, note}`, `{matches}`, `{error, usage}`, and `sync`'s `sync_summary` event. Future changes additive only (add fields, never rename or remove). Two enforcement layers: (1) document the canonical shapes inline in template comments so the contract is visible at the emission site, (2) U5's byte-comparison of rendered Go source (`expected/<case>/library/.../internal/cli/<file>.go`) will fail CI if any key is renamed or removed, since the literal map keys appear in the snapshot.
+- **Commit structure: one PR, two logical commits (default).** Commit 1: F1 + F6 + F2 (mechanical fixes — usage line, sync trailing-line gate, Examples additions). Commit 2: F3 (per-template JSON envelope shapes, including the auth-flavor expansion across `auth_simple.go.tmpl` / `auth_client_credentials.go.tmpl` / `auth.go.tmpl`). The split reduces per-commit diff size without fragmenting the delivery. Exception: if review feedback suggests splitting into two sequential PRs, that's acceptable and commit 2 can be hoisted to a follow-up. Default remains one PR.
+- **Extend `testdata/golden/cases/generate-golden-api/` rather than add a new case.** The existing fixture already exercises the full template render path. Adding probe lines to `command.txt` and updating `expected/stdout.txt` is cheaper than scaffolding a new case and gives the same regression coverage. New case only if the contracts genuinely don't fit the existing one (they do).
+- **Golden update cadence: run `scripts/golden.sh update` once after all four fixes land.** Inspect the diff manually, confirm only the expected output strings changed, document in the PR description what shifted. Avoids per-commit golden churn.
+- **graphql_sync.go.tmpl gets the same F6 fix.** If the trailing human line exists there too (likely — it parallels sync.go.tmpl), apply the same `if !flags.asJSON` gate. If it doesn't, no change.
+
+---
+
+## Open Questions
+
+### Resolved During Planning
+
+- **Should F3's JSON shapes match the printed CLI shapes I already shipped?** Yes — those shapes were vetted by the live dogfood matrix and shipped to the public library in PR #229. Re-using them ensures any agents that already adapted to movie-goat's outputs see the same shape from future CLIs. If a different shape is preferred for a specific command, that's a separate decision but defers to the shipped form by default.
+- **Should `which.go.tmpl`'s shift from `json.NewEncoder` to `printJSONFiltered` be in scope?** Yes. It's a one-line swap that aligns with the rest of the codebase and gets `--select`/`--compact`/`--csv` for free. The current `json.NewEncoder` was technically a pre-existing tech-debt issue but addressing it inside this WU is right-sized.
+- **Should the doctor template emit a JSON envelope for --json?** It already produces structured output via `report := map[string]any{}` + `json.Marshal`. F3 doesn't include doctor; only Examples (F2) is needed for doctor. Confirmed by re-reading the printed CLI's `doctor.go` — JSON works there already.
+
+### Deferred to Implementation
+
+- _(Resolved during ce-doc-review: `graphql_sync.go.tmpl` has the same trailing-human-line pattern at lines 184-194. U2 now scopes both files explicitly with the `humanFriendly` gate.)_
+- Exact wording of the inline template comments documenting the JSON envelope contracts. The comments serve as both maintainer cues and PR review anchor; finalize during U4.
+- _(Resolved during ce-doc-review: neither stdout assertions nor live CLI probes — the approach is byte-comparison of rendered Go source files added to `artifacts.txt`. See U5.)_
+
+---
+
+## Implementation Units
+
+- U1. **Fix usage-line double-print (F1)**
+
+**Goal:** Generated CLIs emit a single binary name in the usage error for any required-positional command.
+
+**Requirements:** R1, R5
+
+**Dependencies:** None (U1–U4 are independent; U5 depends on all four).
+
+**Files:**
+- Modify: `internal/generator/templates/command_promoted.go.tmpl`
+- Modify: `internal/generator/templates/command_endpoint.go.tmpl`
+- Test: `internal/generator/template_usage_error_test.go` (create)
+
+**Approach:**
+- Replace the format string and arg list at line 95 of `command_promoted.go.tmpl` and line 128 of `command_endpoint.go.tmpl`. Drop `cmd.Root().Name(),` and the leading `%s ` so the format reads `"%s is required\nUsage: %s <%s>"` with arg list `cmd.CommandPath(), "{{.Name}}"`.
+- `cmd.CommandPath()` already starts with the root command's name; concatenating with `cmd.Root().Name()` duplicates it.
+
+**Patterns to follow:**
+- Other templates that already use `cmd.CommandPath()` correctly without prefixing `cmd.Root().Name()` (search the templates dir if a reference is needed).
+
+**Test scenarios:**
+- Happy path: render `command_promoted.go.tmpl` with a representative endpoint having a required positional. Assert the rendered Go source contains `cmd.CommandPath()` exactly once and does not contain `cmd.Root().Name(), cmd.CommandPath()` or `%s %s` together. Covers R1.
+- Happy path: render `command_endpoint.go.tmpl` similarly. Same assertion. Covers R1.
+- Negative: render an endpoint without required positionals. Assert the rendered Go source contains no usage-error block at all (or the existing block is unchanged structurally — whichever the template's current shape produces).
+
+**Verification:**
+- `go test ./internal/generator/...` passes.
+- After landing, regenerate the golden-api fixture: `scripts/golden.sh update` produces a diff in usage-error strings only (binary name appears exactly once in each affected expected line).
+
+---
+
+- U2. **Suppress sync trailing human line under `--json` (F6)**
+
+**Goal:** Generated `sync` command produces parseable JSON when `--json` is set.
+
+**Requirements:** R4, R5
+
+**Dependencies:** None (U1–U4 are independent; U5 depends on all four).
+
+**Files:**
+- Modify: `internal/generator/templates/sync.go.tmpl` (lines 249-255 — the `Sync complete:` trailing block)
+- Modify: `internal/generator/templates/graphql_sync.go.tmpl` (lines 184-194 — confirmed identical pattern)
+- Test: `internal/generator/sync_json_output_test.go` (create)
+
+**Approach:**
+- Locate the trailing `fmt.Fprintf(os.Stderr, "Sync complete: ...")` block at the end of the sync RunE. Wrap it in `if !humanFriendly { ... }` matching the surrounding template idiom — `humanFriendly` is the package-level bool declared in `helpers.go.tmpl:42` and bound to the `--human-friendly` flag in `root.go.tmpl:153`. Every existing output gate in `sync.go.tmpl` (including the `sync_summary` event gate at lines 245-248) uses `humanFriendly`, not `flags.asJSON`.
+- Note: the trailing block writes to `os.Stderr`, not `out`; preserve that.
+- The `sync_summary` event already emitted into the JSON stream carries `total_records`, `resources`, `success`, `warned`, `errored`, `duration_ms` — same data as the human line. No information is lost.
+- Apply the same `if !humanFriendly { ... }` gate to `graphql_sync.go.tmpl:184-194`. The pattern is confirmed identical.
+
+**Patterns to follow:**
+- Existing `humanFriendly` gates throughout `sync.go.tmpl` (30+ sites; the `sync_summary` event gate at lines 245-248 is the closest analogue).
+- `analytics.go.tmpl` and `feedback.go.tmpl` use `flags.asJSON` directly because they're in different stream contexts; the sync template's idiom is `humanFriendly`.
+
+**Test scenarios:**
+- Happy path: render `sync.go.tmpl` with the canonical golden-api fixture. Assert the rendered Go source contains `if !humanFriendly {` wrapping the trailing `fmt.Fprintf(os.Stderr, ...)` for the human "Sync complete" line. Covers R4.
+- Happy path: same assertion for `graphql_sync.go.tmpl` (lines 184-194 equivalent). Covers R4.
+- Integration (via golden harness in U5 — see U5's source-grep approach): generated sync's rendered Go source contains the `humanFriendly` gate wrapping the trailing block. Covers R4 end-to-end.
+- Negative: rendered sync command run without `--json` (and with default `humanFriendly=true`) still prints "Sync complete: ..." to stderr as today. Covers R5.
+
+**Verification:**
+- `go test ./internal/generator/...` passes.
+- `scripts/golden.sh verify` shows expected diff only on sync-related output lines.
+
+---
+
+- U3. **Add `Example:` blocks to framework command templates (F2)**
+
+**Goal:** Generated framework commands (`doctor`, `profile save/use/list/show/delete`, `feedback list`) carry `Examples:` blocks in `--help`.
+
+**Requirements:** R2, R5
+
+**Dependencies:** None (U1–U4 are independent; U5 depends on all four).
+
+**Files:**
+- Modify: `internal/generator/templates/doctor.go.tmpl`
+- Modify: `internal/generator/templates/profile.go.tmpl`
+- Modify: `internal/generator/templates/feedback.go.tmpl`
+- Test: `internal/generator/framework_examples_test.go` (create)
+
+**Approach:**
+- Add an `Example:` field to each cobra command literal in the named templates, using the `{{ .Name }}-pp-cli` placeholder convention already established in those templates.
+- Reference shapes (proven in this run's printed CLI):
+  - doctor: `{{ .Name }}-pp-cli doctor` / `... doctor --json` / `... doctor --fail-on warn`
+  - profile save: `... profile save my-defaults --json --compact` / `... profile save tonight-defaults --region US`
+  - profile use: `... profile use my-defaults` / `... profile use tonight-defaults --json`
+  - profile list: `... profile list` / `... profile list --json`
+  - profile show: `... profile show my-defaults` / `... profile show tonight-defaults --json`
+  - profile delete: `... profile delete my-defaults --yes` / `... profile delete old-profile --yes --json`
+  - feedback list: `... feedback list` / `... feedback list --limit 5` / `... feedback list --json`
+- Keep examples generic enough that they apply to any printed CLI's profile/feedback semantics, not movie-goat-specific. `tonight-defaults` is generic enough as a profile name; if reviewers prefer `my-profile`/`other-profile`, swap during code review.
+
+**Patterns to follow:**
+- Existing `Example:` fields in `analytics.go.tmpl`, `auth.go.tmpl` (status), and `which.go.tmpl`.
+
+**Test scenarios:**
+- Happy path: render each affected template. Assert the rendered Go source contains `Example:` for each cobra command literal. Covers R2.
+- Integration (via golden harness in U5): `<generated-cli> doctor --help`, `<generated-cli> profile save --help`, `<generated-cli> feedback list --help` (and the other profile subcommands) all contain `Examples:` in stdout. Covers R2 end-to-end.
+- Negative: existing Examples on commands that already had them (e.g., `auth status`) are unchanged. Covers R5.
+
+**Verification:**
+- `go test ./internal/generator/...` passes.
+- Live dogfood help-check failures for these 7 commands resolve when running against a freshly-generated CLI.
+
+---
+
+- U4. **Add JSON envelopes to framework command templates (F3)**
+
+**Goal:** Generated framework commands respect `--json` by emitting a documented JSON envelope.
+
+**Requirements:** R3, R5
+
+**Dependencies:** None (U1–U4 are independent; U5 depends on all four).
+
+**Files:**
+- Modify: `internal/generator/templates/auth_simple.go.tmpl` (status at line 37, logout at line 100, set-token at line 69) — token-auth flavor; this is what movie-goat used
+- Modify: `internal/generator/templates/auth_client_credentials.go.tmpl` (status at line 166, logout at line 225, set-token at line 204) — client-credentials flavor
+- Modify: `internal/generator/templates/auth.go.tmpl` (status / logout only — this is the OAuth-flavored path; no set-token here)
+- Modify: `internal/generator/templates/api_discovery.go.tmpl`
+- Modify: `internal/generator/templates/import.go.tmpl`
+- Modify: `internal/generator/templates/profile.go.tmpl` (delete subcommand)
+- Modify: `internal/generator/templates/tail.go.tmpl` (no-arg path)
+- Modify: `internal/generator/templates/which.go.tmpl` (swap `json.NewEncoder` for `printJSONFiltered`, wrap matches in `{matches: ...}` envelope)
+- Modify: `internal/generator/templates/command_promoted.go.tmpl` (no-query path emits JSON envelope under `--json`)
+- Test: `internal/generator/framework_json_envelopes_test.go` (create)
+
+The auth flavor is selected by spec's `auth.type` (oauth vs api_key vs client_credentials → different template renders). The same JSON envelope shapes (`{authenticated, source, config}`, `{cleared, note}`, `{saved, config_path}`) land in every flavor so the agent-facing contract is consistent regardless of which is rendered. Total emission sites for auth: status × 3, logout × 3, set-token × 2 (the OAuth flavor doesn't have a set-token subcommand).
+
+**Approach:**
+- For each affected template, add an `if flags.asJSON { return printJSONFiltered(cmd.OutOrStdout(), envelope, flags) }` branch BEFORE the existing human prose path. Existing behavior is preserved when `--json` is absent.
+- Canonical envelope shapes (matching the proven shapes from this run's printed CLI; document each in an inline template comment at the emission site):
+  - `auth status`: `{authenticated: bool, source: cfg.AuthSource, config: cfg.Path}`. Preserve existing exit-non-zero-on-not-authenticated semantics under `--json` — write JSON, then return `authErr`.
+  - `auth logout`: `{cleared: true, note: "<env_var> env var is still set"}` when env still set; `{cleared: true}` otherwise. The env var name is template-substituted via existing auth env-vars list.
+  - `auth set-token`: `{saved: true, config_path: cfg.Path}`.
+  - `api` (root path): `{interfaces: [...], note: "<message>"}` (interfaces empty if none, with a note). **Implementation note:** the current template (lines 54-59) builds interfaces as pre-formatted strings via `fmt.Sprintf("  %-45s %s", ...)`. Restructure to assemble a typed `[]struct{Name, Short string}` once, then use it for both the human format loop AND the JSON marshal — not a one-line branch addition. Same restructuring for the methods sub-path.
+  - `api <interface>`: `{interface: name, methods: [...]}`.
+  - `import`: `{succeeded: N, failed: M, skipped: K}`.
+  - `profile delete`: `{deleted: name}`.
+  - `tail` (no resource arg + `--json`): `{resources: [...], note: "tail requires a resource name; pass one of the listed names"}`, return nil (exit 0). Existing happy path with a resource arg continues to stream NDJSON. **Implementation note:** verify whether the resource list is already a typed `[]string` or requires the same restructuring as `api_discovery` — if the human path currently builds resource names via formatted strings, assemble a typed slice once for both paths.
+  - `which`: wrap matches as `{matches: [...]}`. Empty match under `--json` returns `{matches: []}` exit 0; non-JSON path keeps existing exit-2 on no-match human behavior.
+  - `command_promoted` (no query + `--json`): write `{error: "<thing> is required", usage: "<usage string>"}` to stdout, then return `usageErr(...)` so exit code stays 2 — same as the non-JSON path. The envelope is informational about the error; the exit code carries the actual status. Matches the `auth status` not-authenticated pattern (write JSON, return authErr).
+- For `which.go.tmpl`, replace the bare `json.NewEncoder` block with `printJSONFiltered` so `--select`/`--compact`/`--csv` work consistently.
+- Inline template comment at each emission site documenting the canonical shape, e.g., `// JSON envelope: {authenticated, source, config} — see WU-1 / issue #572`.
+
+**Patterns to follow:**
+- `feedback.go.tmpl`'s existing JSON branch (`if flags.asJSON { return printJSONFiltered(... , map[string]any{...}, flags) }`) — exact shape to clone for the envelope-only commands.
+- `analytics.go.tmpl`'s `if flags.asJSON { ... } else { ... }` gate — exact shape for commands that have human prose to preserve under `else`.
+- For commands that need to return non-zero after writing JSON (auth status not-authenticated): write JSON first, then return the typed error — same as the existing patched shape in movie-goat's `auth.go`.
+
+**Test scenarios:**
+- Happy path: render each affected template. Assert each cobra command literal in scope contains an `if flags.asJSON {` branch and references `printJSONFiltered`. Covers R3.
+- Edge case: `auth status` not-authenticated under `--json`: assert the rendered source writes the JSON envelope before returning `authErr`. Covers R3 + the not-authenticated semantic.
+- Edge case: `which` with no match under `--json`: assert the rendered source emits `{matches: []}` and returns nil (exit 0); under non-JSON the existing exit-2 path is preserved. Covers R3 + R5.
+- Edge case: `which --json --compact` against the generated CLI: assert each match in the `matches` array preserves `entry`/`score` keys (i.e., the envelope wrap routes through `compactObjectFields`'s blocklist, not `compactListFields`'s allowlist). Covers the printJSONFiltered swap correctness.
+- Edge case: `which --csv` against the generated CLI: assert the output is CSV-shaped (the bare `json.NewEncoder` baseline ignored `--csv`; the new path honors it).
+- Integration (via U5 golden harness): rendered Go source for each affected file in `internal/cli/` contains the expected `if flags.asJSON {` block per the byte-comparison snapshot. Covers R3 end-to-end.
+- Negative: each named command without `--json` produces the same human prose as today. Covers R5.
+
+**Verification:**
+- `go test ./internal/generator/...` passes.
+- Live dogfood `json_fidelity` failures for the 9 framework commands listed in F3 resolve when running against a freshly-generated CLI.
+
+---
+
+- U5. **Extend `testdata/golden/cases/generate-golden-api/` to lock the four contracts (R6)**
+
+**Goal:** Golden harness regression-guards the four fixes so future template churn can't silently regress them.
+
+**Requirements:** R6
+
+**Dependencies:** U1, U2, U3, U4 (all four fixes must be in place before the golden update accurately captures the new expected output).
+
+**Files:**
+- Modify: `testdata/golden/cases/generate-golden-api/artifacts.txt` (add the new emission-bearing rendered files for byte-comparison)
+- Add: `testdata/golden/expected/generate-golden-api/library/golden-api-pp-cli/internal/cli/{auth_simple,api_discovery,import,profile,tail,which,promoted_multi,sync,doctor,feedback}.go` (snapshots of the rendered Go source, captured by `golden.sh update` once U1-U4 land)
+
+**Approach:**
+- The golden harness today runs `printing-press generate/dogfood/scorecard` and compares stdout/stderr/exit + listed artifacts. It does NOT chdir into the generated CLI, build it, or invoke its commands. Pivoting U5 to source-grep / artifact-byte assertions stays inside that envelope: the contracts being locked are template-emission contracts, and rendered Go source is the right artifact to snapshot.
+- Add the rendered Go source files listed above to `artifacts.txt`. The byte-comparison mechanism already in place will then catch:
+  - F1: missing `cmd.Root().Name(),` regression in `usageErr` format strings (single binary name)
+  - F2: presence of `Example:` blocks in framework commands (`doctor.go`, `profile.go`, `feedback.go`)
+  - F3: presence of `if flags.asJSON {` branches referencing `printJSONFiltered` in framework commands (`auth_simple.go`, `api_discovery.go`, `import.go`, `profile.go` delete, `tail.go`, `which.go`, `promoted_multi.go`)
+  - F6: `if !humanFriendly {` gate wrapping the trailing `Sync complete:` block in `sync.go` (and same for `graphql_sync.go` if a graphql-flavored fixture is added later)
+- Run `scripts/golden.sh update` once to capture the new expected snapshots. Inspect the diff manually before committing — confirm only the four contract changes appear and no unrelated incidental shifts (template churn elsewhere) bleed in.
+- Document the new artifacts in the PR description so reviewers can map each captured file back to a finding.
+
+**Patterns to follow:**
+- AGENTS.md "Golden Output Harness" section — keep cases deterministic, offline, auth-free. The source-grep / byte-comparison approach honors all three (no live CLI execution, no auth, deterministic generator output).
+- Existing case structure: `artifacts.txt` lists files for byte-comparison against `expected/<case>/<path>`; the harness diffs them at verify time.
+- AGENTS.md guidance: "Snapshot the specific files or output fields that demonstrate the stable behavior. Do not include broad reports, whole generated trees, or incidental diagnostics."
+
+**Test scenarios:**
+- Integration: `scripts/golden.sh verify` passes after the four template fixes land and the golden-api expected snapshots are regenerated. Covers R6.
+- Integration: with U1's fix in place, intentionally revert one of the two template lines and re-run `scripts/golden.sh verify`. The verifier should fail with a diff showing the doubled `cmd.Root().Name(), cmd.CommandPath()` reverting in the captured `promoted_multi.go`. Confirms the regression guard catches the F1 regression.
+- Integration: same revert test for F2 (delete an `Example:` block from a framework template), F3 (remove an `if flags.asJSON` branch), F6 (remove the `if !humanFriendly` gate). All should produce verifiable diffs in `expected/<file>.go`.
+
+**Verification:**
+- `scripts/golden.sh verify` passes.
+- Diff between old and new `expected/stdout.txt` contains only the new probe outputs and the four expected behavior changes; no unrelated rendered-file shifts.
+
+---
+
+## System-Wide Impact
+
+- **Interaction graph:** The four template fixes affect any newly-generated printed CLI's framework command surface — future generations plus any CLI explicitly regenerated. Already-published CLIs in the `printing-press-library` repo are unaffected until backported via separate follow-up PRs (explicitly deferred — see Scope Boundaries). No runtime effect on existing published CLIs.
+- **Error propagation:** F3's `auth status` change preserves the existing typed `authErr` return when not authenticated, ensuring callers who depend on exit codes still see the right exit even when JSON is requested. No new error paths introduced.
+- **State lifecycle risks:** None. All four fixes are pure-output changes in commands that don't mutate persisted state.
+- **API surface parity:** The new JSON envelope shapes for `auth status`, `auth logout`, `auth set-token`, `api`, `import`, `profile delete`, `tail` (no-arg), `which`, `command_promoted` (no-query), and `sync_summary` become Tier-1 stable contracts. Future changes additive only (add fields, never rename or remove existing keys). MCP hosts and agents may depend on these shapes.
+- **Integration coverage:** The golden harness extension in U5 covers cross-template integration — exercises the full generator render pipeline against a representative spec and verifies the output contracts. Unit tests in U1-U4 cover per-template emission, but the full integration is the golden harness.
+- **Unchanged invariants:** The existing `printJSONFiltered` helper, `flags.asJSON` field, `cmd.CommandPath()` semantics, all existing template `Example:` blocks on commands that already had them, and all existing non-JSON output paths are explicitly unchanged.
+
+---
+
+## Risks & Dependencies
+
+| Risk | Mitigation |
+|------|------------|
+| New JSON envelope shapes lock in contracts that future maintainers may want to change. | Two-layer mitigation: (1) inline template comments document each shape at the emission site; (2) U5's `expected/<file>.go` byte-snapshots include the literal map keys, so any rename/removal fails CI's golden verify. PR description states the contracts explicitly. Future changes follow additive-only rule (add fields, never rename or remove). |
+| Golden harness diff in U5 is noisier than expected (incidental output shifts from another change merging concurrently). | Run `scripts/golden.sh verify` once before starting U5 to confirm baseline is clean. If unexpected diffs appear, isolate to this WU's changes before committing the golden update. |
+| `graphql_sync.go.tmpl` may have a different trailing-line shape than `sync.go.tmpl`, requiring a non-mechanical fix. | U2's approach allows skip-if-not-applicable. If the GraphQL sync template's trailing path differs significantly, file a follow-up sub-issue under #571 rather than expanding this WU. |
+| `which.go.tmpl`'s shift from `json.NewEncoder` to `printJSONFiltered` is NOT a 1:1 output-shape swap. | `printJSONFiltered` routes through `printOutputWithFlags` (`helpers.go.tmpl:592`), which applies `--compact` allowlisting via `compactListFields` (line 662) — for a bare array of `whichMatch{Entry, Score}`, `--compact` would strip every entry. The plan's `{matches: [...]}` wrap routes through `compactObjectFields` (blocklist), which preserves the data. The wrap is what makes the swap safe, not output-format equivalence. Add a U4 test asserting `which --json` produces the wrapped envelope shape, plus separate tests for `--compact` and `--csv` (which the bare `json.NewEncoder` did not honor). |
+| `printJSONFiltered`'s `--select`/`--compact`/`--csv` behaviors apply to framework commands that previously didn't support them; users may see surprising filter behavior. | Acceptable — global flags should affect all commands consistently. Document in PR description as a behavior consistency improvement. The U4 test scenarios now exercise `--compact` and `--csv` for `which` so the new behavior is captured rather than discovered. |
+
+---
+
+## Documentation / Operational Notes
+
+- PR description lists each finding (F1, F2, F3, F6) and the exact JSON envelope shapes introduced. This is the canonical reference for downstream MCP host implementations.
+- AGENTS.md "Code & Comment Hygiene" rule allows inline template comments documenting WHY (a hidden constraint or stable contract); add comments at each new JSON envelope emission site naming the contract.
+- After landing, the next regeneration of any published library CLI (via `/printing-press-reprint`) will pick up these fixes automatically. No coordination needed.
+
+---
+
+## Sources & References
+
+- **Sub-issue:** [#572 — WU-1: Generator template polish](https://github.com/mvanhorn/cli-printing-press/issues/572)
+- **Parent retro:** [#571 — Movie Goat retro](https://github.com/mvanhorn/cli-printing-press/issues/571)
+- **Reference patches:** `~/printing-press/library/movie-goat/internal/cli/{auth,api_discovery,import,profile,tail,which,sync,promoted_multi,doctor,feedback}.go`
+- **Golden harness:** `scripts/golden.sh`, `testdata/golden/cases/generate-golden-api/`
+- **Existing JSON envelope pattern:** `internal/generator/templates/feedback.go.tmpl`, `internal/generator/templates/analytics.go.tmpl`
+- **AGENTS.md sections:** "Golden Output Harness", "Code & Comment Hygiene", "Phase 3 rule #2 (printJSONFiltered)"
diff --git a/internal/generator/templates/api_discovery.go.tmpl b/internal/generator/templates/api_discovery.go.tmpl
index a6a4b96f..2f27195e 100644
--- a/internal/generator/templates/api_discovery.go.tmpl
+++ b/internal/generator/templates/api_discovery.go.tmpl
@@ -37,6 +37,21 @@ Run 'api <interface>' to see that interface's methods.`,
 				for _, child := range root.Commands() {
 					if child.Hidden && strings.ToLower(child.Name()) == target {
 						methods := child.Commands()
+						// JSON envelope: {interface, short, methods: [{name, short}, ...]}.
+						if flags.asJSON {
+							methodList := make([]map[string]any, 0, len(methods))
+							for _, method := range methods {
+								methodList = append(methodList, map[string]any{
+									"name":  method.Name(),
+									"short": method.Short,
+								})
+							}
+							return printJSONFiltered(cmd.OutOrStdout(), map[string]any{
+								"interface": child.Name(),
+								"short":     child.Short,
+								"methods":   methodList,
+							}, flags)
+						}
 						if len(methods) == 0 {
 							return child.Help()
 						}
@@ -51,22 +66,39 @@ Run 'api <interface>' to see that interface's methods.`,
 				return fmt.Errorf("interface %q not found. Run '%s-pp-cli api' to list all interfaces", args[0], "{{.Name}}")
 			}
 
-			var interfaces []string
+			// Pre-formatting human strings ahead of time would block the JSON
+			// path from emitting clean field values; build the typed slice and
+			// derive human format on print.
+			type ifaceEntry struct {
+				Name  string `json:"name"`
+				Short string `json:"short"`
+			}
+			var ifaces []ifaceEntry
 			for _, child := range root.Commands() {
 				if child.Hidden {
-					interfaces = append(interfaces, fmt.Sprintf("  %-45s %s", child.Name(), child.Short))
+					ifaces = append(ifaces, ifaceEntry{Name: child.Name(), Short: child.Short})
+				}
+			}
+			sort.Slice(ifaces, func(i, j int) bool { return ifaces[i].Name < ifaces[j].Name })
+
+			// JSON envelope: {interfaces: [...], note?: "..."}.
+			if flags.asJSON {
+				out := map[string]any{"interfaces": ifaces}
+				if len(ifaces) == 0 {
+					out["interfaces"] = []ifaceEntry{}
+					out["note"] = "No hidden API interfaces found."
 				}
+				return printJSONFiltered(cmd.OutOrStdout(), out, flags)
 			}
-			sort.Strings(interfaces)
 
-			if len(interfaces) == 0 {
+			if len(ifaces) == 0 {
 				fmt.Fprintln(cmd.OutOrStdout(), "No hidden API interfaces found.")
 				return nil
 			}
 
-			fmt.Fprintf(cmd.OutOrStdout(), "Available API interfaces (%d):\n\n", len(interfaces))
-			for _, line := range interfaces {
-				fmt.Fprintln(cmd.OutOrStdout(), line)
+			fmt.Fprintf(cmd.OutOrStdout(), "Available API interfaces (%d):\n\n", len(ifaces))
+			for _, e := range ifaces {
+				fmt.Fprintf(cmd.OutOrStdout(), "  %-45s %s\n", e.Name, e.Short)
 			}
 			fmt.Fprintf(cmd.OutOrStdout(), "\nUse '%s-pp-cli api <interface>' to see methods.\n", "{{.Name}}")
 			return nil
diff --git a/internal/generator/templates/auth.go.tmpl b/internal/generator/templates/auth.go.tmpl
index fccd05a8..bf4ec25b 100644
--- a/internal/generator/templates/auth.go.tmpl
+++ b/internal/generator/templates/auth.go.tmpl
@@ -200,7 +200,18 @@ func newAuthStatusCmd(flags *rootFlags) *cobra.Command {
 			}
 
 			w := cmd.OutOrStdout()
-			if cfg.AccessToken == "" {
+			authed := cfg.AccessToken != ""
+			// JSON envelope: {authenticated, source, config}.
+			if flags.asJSON {
+				out := map[string]any{
+					"authenticated": authed,
+					"source":        cfg.AuthSource,
+					"config":        cfg.Path,
+				}
+				return printJSONFiltered(w, out, flags)
+			}
+
+			if !authed {
 				fmt.Fprintf(w, "  %s Not authenticated. Run 'auth login' to authenticate.\n", red("FAIL"))
 				return nil
 			}
@@ -237,6 +248,14 @@ func newAuthLogoutCmd(flags *rootFlags) *cobra.Command {
 			if err := cfg.ClearTokens(); err != nil {
 				return fmt.Errorf("clearing tokens: %w", err)
 			}
+			// JSON envelope: {cleared: true}. The OAuth flavor does not
+			// surface a "note" key because env-var creds aren't part of
+			// this auth flow's contract.
+			if flags.asJSON {
+				return printJSONFiltered(cmd.OutOrStdout(), map[string]any{
+					"cleared": true,
+				}, flags)
+			}
 			fmt.Fprintf(os.Stderr, "Logged out. Tokens removed.\n")
 			return nil
 		},
diff --git a/internal/generator/templates/auth_client_credentials.go.tmpl b/internal/generator/templates/auth_client_credentials.go.tmpl
index d3bdd719..d3522b93 100644
--- a/internal/generator/templates/auth_client_credentials.go.tmpl
+++ b/internal/generator/templates/auth_client_credentials.go.tmpl
@@ -176,7 +176,25 @@ func newAuthStatusCmd(flags *rootFlags) *cobra.Command {
 
 			w := cmd.OutOrStdout()
 			header := cfg.AuthHeader()
-			if header == "" {
+			authed := header != ""
+			// JSON envelope: {authenticated, source, config}. When not
+			// authenticated, write the envelope first then return authErr
+			// so exit code carries the auth-failure signal.
+			if flags.asJSON {
+				out := map[string]any{
+					"authenticated": authed,
+					"source":        cfg.AuthSource,
+					"config":        cfg.Path,
+				}
+				if printErr := printJSONFiltered(w, out, flags); printErr != nil {
+					return printErr
+				}
+				if !authed {
+					return authErr(fmt.Errorf("no credentials configured"))
+				}
+				return nil
+			}
+			if !authed {
 				fmt.Fprintln(w, red("Not authenticated"))
 				fmt.Fprintln(w, "")
 				fmt.Fprintln(w, "Mint a token:")
@@ -216,6 +234,13 @@ func newAuthSetTokenCmd(flags *rootFlags) *cobra.Command {
 			if err := cfg.SaveTokens("", "", args[0], "", cfg.TokenExpiry); err != nil {
 				return configErr(fmt.Errorf("saving token: %w", err))
 			}
+			// JSON envelope: {saved, config_path}.
+			if flags.asJSON {
+				return printJSONFiltered(cmd.OutOrStdout(), map[string]any{
+					"saved":       true,
+					"config_path": cfg.Path,
+				}, flags)
+			}
 			fmt.Fprintf(cmd.OutOrStdout(), "Token saved to %s\n", cfg.Path)
 			return nil
 		},
@@ -235,12 +260,29 @@ func newAuthLogoutCmd(flags *rootFlags) *cobra.Command {
 			if err := cfg.ClearTokens(); err != nil {
 				return configErr(fmt.Errorf("clearing tokens: %w", err))
 			}
+
+			// Identify which (if any) auth env var is still exported so the
+			// JSON envelope and the human prose can both surface it.
+			envStillSet := ""
 {{- range .Auth.EnvVars}}
-			if os.Getenv("{{.}}") != "" {
-				fmt.Fprintf(cmd.OutOrStdout(), "Config cleared. Note: {{.}} env var is still set.\n")
-				return nil
+			if envStillSet == "" && os.Getenv("{{.}}") != "" {
+				envStillSet = "{{.}}"
 			}
 {{- end}}
+
+			// JSON envelope: {cleared: true, note?: "<env_var> env var is still set"}.
+			if flags.asJSON {
+				out := map[string]any{"cleared": true}
+				if envStillSet != "" {
+					out["note"] = envStillSet + " env var is still set"
+				}
+				return printJSONFiltered(cmd.OutOrStdout(), out, flags)
+			}
+
+			if envStillSet != "" {
+				fmt.Fprintf(cmd.OutOrStdout(), "Config cleared. Note: %s env var is still set.\n", envStillSet)
+				return nil
+			}
 			fmt.Fprintln(cmd.OutOrStdout(), "Logged out. Credentials cleared.")
 			return nil
 		},
diff --git a/internal/generator/templates/auth_simple.go.tmpl b/internal/generator/templates/auth_simple.go.tmpl
index 8cb81351..0fa68588 100644
--- a/internal/generator/templates/auth_simple.go.tmpl
+++ b/internal/generator/templates/auth_simple.go.tmpl
@@ -47,7 +47,25 @@ func newAuthStatusCmd(flags *rootFlags) *cobra.Command {
 
 			w := cmd.OutOrStdout()
 			header := cfg.AuthHeader()
-			if header == "" {
+			authed := header != ""
+			// JSON envelope: {authenticated, source, config}. When not
+			// authenticated, write the envelope first then return authErr
+			// so exit code carries the auth-failure signal.
+			if flags.asJSON {
+				out := map[string]any{
+					"authenticated": authed,
+					"source":        cfg.AuthSource,
+					"config":        cfg.Path,
+				}
+				if printErr := printJSONFiltered(w, out, flags); printErr != nil {
+					return printErr
+				}
+				if !authed {
+					return authErr(fmt.Errorf("no credentials configured"))
+				}
+				return nil
+			}
+			if !authed {
 				fmt.Fprintln(w, red("Not authenticated"))
 				fmt.Fprintln(w, "")
 				fmt.Fprintln(w, "Set your token:")
@@ -91,6 +109,13 @@ func newAuthSetTokenCmd(flags *rootFlags) *cobra.Command {
 				return configErr(fmt.Errorf("saving token: %w", err))
 			}
 
+			// JSON envelope: {saved, config_path}.
+			if flags.asJSON {
+				return printJSONFiltered(cmd.OutOrStdout(), map[string]any{
+					"saved":       true,
+					"config_path": cfg.Path,
+				}, flags)
+			}
 			fmt.Fprintf(cmd.OutOrStdout(), "Token saved to %s\n", cfg.Path)
 			return nil
 		},
@@ -112,13 +137,28 @@ func newAuthLogoutCmd(flags *rootFlags) *cobra.Command {
 				return configErr(fmt.Errorf("clearing tokens: %w", err))
 			}
 
-			// Warn if env vars still set
+			// Identify which (if any) auth env var is still exported so the
+			// JSON envelope and the human prose can both surface it.
+			envStillSet := ""
 {{- range .Auth.EnvVars}}
-			if os.Getenv("{{.}}") != "" {
-				fmt.Fprintf(cmd.OutOrStdout(), "Config cleared. Note: {{.}} env var is still set.\n")
-				return nil
+			if envStillSet == "" && os.Getenv("{{.}}") != "" {
+				envStillSet = "{{.}}"
 			}
 {{- end}}
+
+			// JSON envelope: {cleared: true, note?: "<env_var> env var is still set"}.
+			if flags.asJSON {
+				out := map[string]any{"cleared": true}
+				if envStillSet != "" {
+					out["note"] = envStillSet + " env var is still set"
+				}
+				return printJSONFiltered(cmd.OutOrStdout(), out, flags)
+			}
+
+			if envStillSet != "" {
+				fmt.Fprintf(cmd.OutOrStdout(), "Config cleared. Note: %s env var is still set.\n", envStillSet)
+				return nil
+			}
 			fmt.Fprintln(cmd.OutOrStdout(), "Logged out. Credentials cleared.")
 			return nil
 		},
diff --git a/internal/generator/templates/command_endpoint.go.tmpl b/internal/generator/templates/command_endpoint.go.tmpl
index 880c3bb2..4c0e0d7d 100644
--- a/internal/generator/templates/command_endpoint.go.tmpl
+++ b/internal/generator/templates/command_endpoint.go.tmpl
@@ -125,7 +125,7 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
 {{- if .Positional}}
 {{- if gt $i 0}}
 			if len(args) < {{add $i 1}} {
-				return usageErr(fmt.Errorf("{{.Name}} is required\nUsage: %s %s <%s>", cmd.Root().Name(), cmd.CommandPath(), "{{.Name}}"))
+				return usageErr(fmt.Errorf("{{.Name}} is required\nUsage: %s <%s>", cmd.CommandPath(), "{{.Name}}"))
 			}
 {{- end}}
 {{- if pathContainsParam $.Endpoint.Path .Name}}
diff --git a/internal/generator/templates/command_promoted.go.tmpl b/internal/generator/templates/command_promoted.go.tmpl
index 28aa5bd1..6127d5ef 100644
--- a/internal/generator/templates/command_promoted.go.tmpl
+++ b/internal/generator/templates/command_promoted.go.tmpl
@@ -92,7 +92,17 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
 {{- range $i, $p := .Endpoint.Params}}
 {{- if .Positional}}
 			if len(args) < {{add $i 1}} {
-				return usageErr(fmt.Errorf("{{.Name}} is required\nUsage: %s %s <%s>", cmd.Root().Name(), cmd.CommandPath(), "{{.Name}}"))
+				// JSON envelope: {error, usage}. Written first; the
+				// usageErr return preserves exit code 2 across modes.
+				if flags.asJSON {
+					if printErr := printJSONFiltered(cmd.OutOrStdout(), map[string]any{
+						"error": "{{.Name}} is required",
+						"usage": fmt.Sprintf("%s <%s>", cmd.CommandPath(), "{{.Name}}"),
+					}, flags); printErr != nil {
+						return printErr
+					}
+				}
+				return usageErr(fmt.Errorf("{{.Name}} is required\nUsage: %s <%s>", cmd.CommandPath(), "{{.Name}}"))
 			}
 {{- if pathContainsParam $.Endpoint.Path .Name}}
 			path = replacePathParam(path, "{{.Name}}", args[{{$i}}])
diff --git a/internal/generator/templates/doctor.go.tmpl b/internal/generator/templates/doctor.go.tmpl
index 4c49e24c..1bb0107a 100644
--- a/internal/generator/templates/doctor.go.tmpl
+++ b/internal/generator/templates/doctor.go.tmpl
@@ -91,6 +91,9 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "doctor",
 		Short: "Check CLI health",
+		Example: `  {{.Name}}-pp-cli doctor
+  {{.Name}}-pp-cli doctor --json
+  {{.Name}}-pp-cli doctor --fail-on warn`,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			report := map[string]any{}
 
diff --git a/internal/generator/templates/feedback.go.tmpl b/internal/generator/templates/feedback.go.tmpl
index 1a45e19e..a9b84ed8 100644
--- a/internal/generator/templates/feedback.go.tmpl
+++ b/internal/generator/templates/feedback.go.tmpl
@@ -184,6 +184,9 @@ func newFeedbackListCmd(flags *rootFlags) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "list",
 		Short: "List recent feedback entries",
+		Example: `  {{.Name}}-pp-cli feedback list
+  {{.Name}}-pp-cli feedback list --limit 5
+  {{.Name}}-pp-cli feedback list --json`,
 		RunE: func(cmd *cobra.Command, _ []string) error {
 			p, err := feedbackFilePath()
 			if err != nil {
diff --git a/internal/generator/templates/graphql_sync.go.tmpl b/internal/generator/templates/graphql_sync.go.tmpl
index 7b5a2ae7..0525ec57 100644
--- a/internal/generator/templates/graphql_sync.go.tmpl
+++ b/internal/generator/templates/graphql_sync.go.tmpl
@@ -181,17 +181,18 @@ Exit codes & warnings:
 
 			elapsed := time.Since(started)
 			totalResources := successCount + warnCount + errCount
-			if !humanFriendly {
+			if humanFriendly {
+				if warnCount > 0 {
+					fmt.Fprintf(os.Stderr, "Sync complete: %d records across %d resources (%d warned, %.1fs)\n",
+						totalSynced, totalResources, warnCount, elapsed.Seconds())
+				} else {
+					fmt.Fprintf(os.Stderr, "Sync complete: %d records across %d resources (%.1fs)\n",
+						totalSynced, totalResources, elapsed.Seconds())
+				}
+			} else {
 				fmt.Fprintf(os.Stderr, `{"event":"sync_summary","total_records":%d,"resources":%d,"success":%d,"warned":%d,"errored":%d,"duration_ms":%d}`+"\n",
 					totalSynced, totalResources, successCount, warnCount, errCount, elapsed.Milliseconds())
 			}
-			if warnCount > 0 {
-				fmt.Fprintf(os.Stderr, "Sync complete: %d records across %d resources (%d warned, %.1fs)\n",
-					totalSynced, totalResources, warnCount, elapsed.Seconds())
-			} else {
-				fmt.Fprintf(os.Stderr, "Sync complete: %d records across %d resources (%.1fs)\n",
-					totalSynced, totalResources, elapsed.Seconds())
-			}
 
 			if errCount > 0 {
 				return fmt.Errorf("%d resource(s) failed to sync", errCount)
diff --git a/internal/generator/templates/import.go.tmpl b/internal/generator/templates/import.go.tmpl
index 6b5249b0..26c0d2f2 100644
--- a/internal/generator/templates/import.go.tmpl
+++ b/internal/generator/templates/import.go.tmpl
@@ -87,6 +87,14 @@ but do not stop the import.`,
 				return fmt.Errorf("reading input: %w", err)
 			}
 
+			// JSON envelope: {succeeded, failed, skipped}.
+			if flags.asJSON {
+				return printJSONFiltered(cmd.OutOrStdout(), map[string]any{
+					"succeeded": success,
+					"failed":    failed,
+					"skipped":   skipped,
+				}, flags)
+			}
 			fmt.Fprintf(os.Stderr, "Import complete: %d succeeded, %d failed, %d skipped\n", success, failed, skipped)
 			return nil
 		},
diff --git a/internal/generator/templates/profile.go.tmpl b/internal/generator/templates/profile.go.tmpl
index 66f736a8..1296e472 100644
--- a/internal/generator/templates/profile.go.tmpl
+++ b/internal/generator/templates/profile.go.tmpl
@@ -173,6 +173,8 @@ entry is replaced.
 
 To avoid creating empty profiles, at least one non-default flag must be
 present (other than --profile and --config).`,
+		Example: `  {{.Name}}-pp-cli profile save my-defaults --json --compact
+  {{.Name}}-pp-cli profile save tonight-defaults --region US`,
 		Args: cobra.ExactArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			name := args[0]
@@ -215,7 +217,9 @@ func newProfileUseCmd(flags *rootFlags) *cobra.Command {
 	return &cobra.Command{
 		Use:   "use <name>",
 		Short: "Print the flag values a profile will apply (does not execute anything)",
-		Args:  cobra.ExactArgs(1),
+		Example: `  {{.Name}}-pp-cli profile use my-defaults
+  {{.Name}}-pp-cli profile use tonight-defaults --json`,
+		Args: cobra.ExactArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			p, err := GetProfile(args[0])
 			if err != nil {
@@ -248,6 +252,8 @@ func newProfileListCmd(flags *rootFlags) *cobra.Command {
 	return &cobra.Command{
 		Use:   "list",
 		Short: "List saved profiles",
+		Example: `  {{.Name}}-pp-cli profile list
+  {{.Name}}-pp-cli profile list --json`,
 		RunE: func(cmd *cobra.Command, _ []string) error {
 			s, err := loadProfileStore()
 			if err != nil {
@@ -285,7 +291,9 @@ func newProfileShowCmd(flags *rootFlags) *cobra.Command {
 	return &cobra.Command{
 		Use:   "show <name>",
 		Short: "Show a profile's values as JSON",
-		Args:  cobra.ExactArgs(1),
+		Example: `  {{.Name}}-pp-cli profile show my-defaults
+  {{.Name}}-pp-cli profile show tonight-defaults --json`,
+		Args: cobra.ExactArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			p, err := GetProfile(args[0])
 			if err != nil {
@@ -303,7 +311,9 @@ func newProfileDeleteCmd(flags *rootFlags) *cobra.Command {
 	return &cobra.Command{
 		Use:   "delete <name>",
 		Short: "Remove a profile",
-		Args:  cobra.ExactArgs(1),
+		Example: `  {{.Name}}-pp-cli profile delete my-defaults --yes
+  {{.Name}}-pp-cli profile delete old-profile --yes --json`,
+		Args: cobra.ExactArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			name := args[0]
 			s, err := loadProfileStore()
@@ -321,6 +331,12 @@ func newProfileDeleteCmd(flags *rootFlags) *cobra.Command {
 			if err := saveProfileStore(s); err != nil {
 				return err
 			}
+			// JSON envelope: {deleted: name}.
+			if flags.asJSON {
+				return printJSONFiltered(cmd.OutOrStdout(), map[string]any{
+					"deleted": name,
+				}, flags)
+			}
 			fmt.Fprintf(cmd.OutOrStdout(), "deleted profile %q\n", name)
 			return nil
 		},
diff --git a/internal/generator/templates/sync.go.tmpl b/internal/generator/templates/sync.go.tmpl
index ef5b2a5a..73f837c1 100644
--- a/internal/generator/templates/sync.go.tmpl
+++ b/internal/generator/templates/sync.go.tmpl
@@ -242,17 +242,18 @@ Exit codes & warnings:
 
 			elapsed := time.Since(started)
 			totalResources := successCount + warnCount + errCount
-			if !humanFriendly {
+			if humanFriendly {
+				if warnCount > 0 {
+					fmt.Fprintf(os.Stderr, "Sync complete: %d records across %d resources (%d warned, %.1fs)\n",
+						totalSynced, totalResources, warnCount, elapsed.Seconds())
+				} else {
+					fmt.Fprintf(os.Stderr, "Sync complete: %d records across %d resources (%.1fs)\n",
+						totalSynced, totalResources, elapsed.Seconds())
+				}
+			} else {
 				fmt.Fprintf(os.Stderr, `{"event":"sync_summary","total_records":%d,"resources":%d,"success":%d,"warned":%d,"errored":%d,"duration_ms":%d}`+"\n",
 					totalSynced, totalResources, successCount, warnCount, errCount, elapsed.Milliseconds())
 			}
-			if warnCount > 0 {
-				fmt.Fprintf(os.Stderr, "Sync complete: %d records across %d resources (%d warned, %.1fs)\n",
-					totalSynced, totalResources, warnCount, elapsed.Seconds())
-			} else {
-				fmt.Fprintf(os.Stderr, "Sync complete: %d records across %d resources (%.1fs)\n",
-					totalSynced, totalResources, elapsed.Seconds())
-			}
 
 			// Exit-code policy:
 			//   1. --strict + any error  -> non-zero (legacy contract)
diff --git a/internal/generator/templates/tail.go.tmpl b/internal/generator/templates/tail.go.tmpl
index ca82bb9a..95764eb3 100644
--- a/internal/generator/templates/tail.go.tmpl
+++ b/internal/generator/templates/tail.go.tmpl
@@ -38,19 +38,29 @@ native streaming instead of polling.`,
   # Pipe to jq for filtering
   {{.Name}}-pp-cli tail events --interval 30s | jq 'select(.type == "error")'`,
 		RunE: func(cmd *cobra.Command, args []string) error {
-			c, err := flags.newClient()
-			if err != nil {
-				return err
-			}
-			c.NoCache = true
-
 			if len(args) > 0 {
 				resource = args[0]
 			}
+			// JSON help envelope: when called with no resource AND --json,
+			// surface the list of known resources so agents can discover
+			// what to pass without parsing a usage error message.
+			// Envelope: {resources: [...], note}.
+			if resource == "" && flags.asJSON {
+				return printJSONFiltered(cmd.OutOrStdout(), map[string]any{
+					"resources": tailKnownResources(),
+					"note":      "tail requires a resource name; pass one of the listed names",
+				}, flags)
+			}
 			if resource == "" {
 				return fmt.Errorf("resource name required (e.g., 'tail messages')")
 			}
 
+			c, err := flags.newClient()
+			if err != nil {
+				return err
+			}
+			c.NoCache = true
+
 			path := "/" + resource
 
 			sig := make(chan os.Signal, 1)
@@ -89,6 +99,17 @@ native streaming instead of polling.`,
 	return cmd
 }
 
+// tailKnownResources returns the resource names this CLI exposes, so the
+// no-arg JSON help envelope can list them without depending on sync's
+// defaultSyncResources (which only exists when sync is generated).
+func tailKnownResources() []string {
+	return []string{
+{{- range $name, $resource := .Resources}}
+		"{{$name}}",
+{{- end}}
+	}
+}
+
 func fetchAndEmit(c interface{ Get(string, map[string]string) (json.RawMessage, error) }, path string, enc *json.Encoder) error {
 	data, err := c.Get(path, nil)
 	if err != nil {
diff --git a/internal/generator/templates/which.go.tmpl b/internal/generator/templates/which.go.tmpl
index f3940862..02cc662e 100644
--- a/internal/generator/templates/which.go.tmpl
+++ b/internal/generator/templates/which.go.tmpl
@@ -4,7 +4,6 @@
 package cli
 
 import (
-	"encoding/json"
 	"fmt"
 	"sort"
 	"strings"
@@ -162,6 +161,15 @@ Exit codes:
 			}
 
 			if len(matches) == 0 {
+				// Under --json, return an empty matches envelope at exit 0
+				// so agents can branch on `matches.length == 0` instead of
+				// parsing a usage error message. Non-JSON keeps the typed
+				// exit-2 path so terminal users see the help hint.
+				if flags.asJSON {
+					return printJSONFiltered(cmd.OutOrStdout(), map[string]any{
+						"matches": []whichMatch{},
+					}, flags)
+				}
 				return usageErr(fmt.Errorf("no match for %q; try '%s --help' for the full command list", query, cmd.Root().Name()))
 			}
 			return renderWhich(cmd, flags, matches)
@@ -192,9 +200,15 @@ func renderWhich(cmd *cobra.Command, flags *rootFlags, matches []whichMatch) err
 		asJSON = true
 	}
 	if asJSON {
-		enc := json.NewEncoder(w)
-		enc.SetIndent("", "  ")
-		return enc.Encode(matches)
+		// JSON envelope: {matches: [...]}. The wrap is critical:
+		// printJSONFiltered's --compact path uses compactListFields
+		// (allowlist) for top-level arrays, which would strip
+		// entry/score keys; routing through compactObjectFields
+		// (blocklist) via an object envelope preserves them.
+		if matches == nil {
+			matches = []whichMatch{}
+		}
+		return printJSONFiltered(w, map[string]any{"matches": matches}, flags)
 	}
 	fmt.Fprintf(w, "%-24s  %-8s  %s\n", "COMMAND", "SCORE", "DESCRIPTION")
 	for _, m := range matches {
diff --git a/testdata/golden/cases/generate-golden-api/artifacts.txt b/testdata/golden/cases/generate-golden-api/artifacts.txt
index 7a965949..8f334d66 100644
--- a/testdata/golden/cases/generate-golden-api/artifacts.txt
+++ b/testdata/golden/cases/generate-golden-api/artifacts.txt
@@ -29,3 +29,9 @@ printing-press-golden/README.md
 printing-press-golden/SKILL.md
 printing-press-golden/internal/cli/doctor.go
 printing-press-golden/internal/cliutil/verifyenv.go
+printing-press-golden/internal/cli/profile.go
+printing-press-golden/internal/cli/feedback.go
+printing-press-golden/internal/cli/sync.go
+printing-press-golden/internal/cli/api_discovery.go
+printing-press-golden/internal/cli/import.go
+printing-press-golden/internal/cli/which.go
diff --git a/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/cli/auth.go b/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/cli/auth.go
index acaddf70..db801a1a 100644
--- a/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/cli/auth.go
+++ b/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/cli/auth.go
@@ -153,7 +153,25 @@ func newAuthStatusCmd(flags *rootFlags) *cobra.Command {
 
 			w := cmd.OutOrStdout()
 			header := cfg.AuthHeader()
-			if header == "" {
+			authed := header != ""
+			// JSON envelope: {authenticated, source, config}. When not
+			// authenticated, write the envelope first then return authErr
+			// so exit code carries the auth-failure signal.
+			if flags.asJSON {
+				out := map[string]any{
+					"authenticated": authed,
+					"source":        cfg.AuthSource,
+					"config":        cfg.Path,
+				}
+				if printErr := printJSONFiltered(w, out, flags); printErr != nil {
+					return printErr
+				}
+				if !authed {
+					return authErr(fmt.Errorf("no credentials configured"))
+				}
+				return nil
+			}
+			if !authed {
 				fmt.Fprintln(w, red("Not authenticated"))
 				fmt.Fprintln(w, "")
 				fmt.Fprintln(w, "Mint a token:")
@@ -188,6 +206,13 @@ func newAuthSetTokenCmd(flags *rootFlags) *cobra.Command {
 			if err := cfg.SaveTokens("", "", args[0], "", cfg.TokenExpiry); err != nil {
 				return configErr(fmt.Errorf("saving token: %w", err))
 			}
+			// JSON envelope: {saved, config_path}.
+			if flags.asJSON {
+				return printJSONFiltered(cmd.OutOrStdout(), map[string]any{
+					"saved":       true,
+					"config_path": cfg.Path,
+				}, flags)
+			}
 			fmt.Fprintf(cmd.OutOrStdout(), "Token saved to %s\n", cfg.Path)
 			return nil
 		},
@@ -207,8 +232,25 @@ func newAuthLogoutCmd(flags *rootFlags) *cobra.Command {
 			if err := cfg.ClearTokens(); err != nil {
 				return configErr(fmt.Errorf("clearing tokens: %w", err))
 			}
-			if os.Getenv("PRINTING_PRESS_OAUTH2_OAUTH2_CC") != "" {
-				fmt.Fprintf(cmd.OutOrStdout(), "Config cleared. Note: PRINTING_PRESS_OAUTH2_OAUTH2_CC env var is still set.\n")
+
+			// Identify which (if any) auth env var is still exported so the
+			// JSON envelope and the human prose can both surface it.
+			envStillSet := ""
+			if envStillSet == "" && os.Getenv("PRINTING_PRESS_OAUTH2_OAUTH2_CC") != "" {
+				envStillSet = "PRINTING_PRESS_OAUTH2_OAUTH2_CC"
+			}
+
+			// JSON envelope: {cleared: true, note?: "<env_var> env var is still set"}.
+			if flags.asJSON {
+				out := map[string]any{"cleared": true}
+				if envStillSet != "" {
+					out["note"] = envStillSet + " env var is still set"
+				}
+				return printJSONFiltered(cmd.OutOrStdout(), out, flags)
+			}
+
+			if envStillSet != "" {
+				fmt.Fprintf(cmd.OutOrStdout(), "Config cleared. Note: %s env var is still set.\n", envStillSet)
 				return nil
 			}
 			fmt.Fprintln(cmd.OutOrStdout(), "Logged out. Credentials cleared.")
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/api_discovery.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/api_discovery.go
new file mode 100644
index 00000000..3a148fb7
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/api_discovery.go
@@ -0,0 +1,109 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package cli
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+
+	"github.com/spf13/cobra"
+)
+
+func newAPICmd(flags *rootFlags) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:         "api [interface]",
+		Short:       "Browse all API endpoints by interface name",
+		Annotations: map[string]string{"mcp:read-only": "true"},
+		Long: `Browse and call any API endpoint using the raw interface names.
+
+The friendly top-level commands cover the most common operations.
+This command provides access to ALL endpoints for power users and
+agents that need full API coverage.
+
+Run 'api' with no arguments to list all interfaces.
+Run 'api <interface>' to see that interface's methods.`,
+		Example: `  # List all available interfaces
+  printing-press-golden-pp-cli api
+
+  # Show methods for a specific interface
+  printing-press-golden-pp-cli api <interface-name>`,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			root := cmd.Root()
+
+			if len(args) > 0 {
+				target := strings.ToLower(args[0])
+				for _, child := range root.Commands() {
+					if child.Hidden && strings.ToLower(child.Name()) == target {
+						methods := child.Commands()
+						// JSON envelope: {interface, short, methods: [{name, short}, ...]}.
+						if flags.asJSON {
+							methodList := make([]map[string]any, 0, len(methods))
+							for _, method := range methods {
+								methodList = append(methodList, map[string]any{
+									"name":  method.Name(),
+									"short": method.Short,
+								})
+							}
+							return printJSONFiltered(cmd.OutOrStdout(), map[string]any{
+								"interface": child.Name(),
+								"short":     child.Short,
+								"methods":   methodList,
+							}, flags)
+						}
+						if len(methods) == 0 {
+							return child.Help()
+						}
+						fmt.Fprintf(cmd.OutOrStdout(), "%s — %s\n\nMethods:\n", child.Name(), child.Short)
+						for _, method := range methods {
+							fmt.Fprintf(cmd.OutOrStdout(), "  %-50s %s\n", child.Name()+" "+method.Name(), method.Short)
+						}
+						fmt.Fprintf(cmd.OutOrStdout(), "\nUse '%s-pp-cli %s <method> --help' for details.\n", "printing-press-golden", child.Name())
+						return nil
+					}
+				}
+				return fmt.Errorf("interface %q not found. Run '%s-pp-cli api' to list all interfaces", args[0], "printing-press-golden")
+			}
+
+			// Pre-formatting human strings ahead of time would block the JSON
+			// path from emitting clean field values; build the typed slice and
+			// derive human format on print.
+			type ifaceEntry struct {
+				Name  string `json:"name"`
+				Short string `json:"short"`
+			}
+			var ifaces []ifaceEntry
+			for _, child := range root.Commands() {
+				if child.Hidden {
+					ifaces = append(ifaces, ifaceEntry{Name: child.Name(), Short: child.Short})
+				}
+			}
+			sort.Slice(ifaces, func(i, j int) bool { return ifaces[i].Name < ifaces[j].Name })
+
+			// JSON envelope: {interfaces: [...], note?: "..."}.
+			if flags.asJSON {
+				out := map[string]any{"interfaces": ifaces}
+				if len(ifaces) == 0 {
+					out["interfaces"] = []ifaceEntry{}
+					out["note"] = "No hidden API interfaces found."
+				}
+				return printJSONFiltered(cmd.OutOrStdout(), out, flags)
+			}
+
+			if len(ifaces) == 0 {
+				fmt.Fprintln(cmd.OutOrStdout(), "No hidden API interfaces found.")
+				return nil
+			}
+
+			fmt.Fprintf(cmd.OutOrStdout(), "Available API interfaces (%d):\n\n", len(ifaces))
+			for _, e := range ifaces {
+				fmt.Fprintf(cmd.OutOrStdout(), "  %-45s %s\n", e.Name, e.Short)
+			}
+			fmt.Fprintf(cmd.OutOrStdout(), "\nUse '%s-pp-cli api <interface>' to see methods.\n", "printing-press-golden")
+			return nil
+		},
+	}
+
+	return cmd
+}
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/auth.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/auth.go
index 62f0a258..48876560 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/auth.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/auth.go
@@ -37,7 +37,25 @@ func newAuthStatusCmd(flags *rootFlags) *cobra.Command {
 
 			w := cmd.OutOrStdout()
 			header := cfg.AuthHeader()
-			if header == "" {
+			authed := header != ""
+			// JSON envelope: {authenticated, source, config}. When not
+			// authenticated, write the envelope first then return authErr
+			// so exit code carries the auth-failure signal.
+			if flags.asJSON {
+				out := map[string]any{
+					"authenticated": authed,
+					"source":        cfg.AuthSource,
+					"config":        cfg.Path,
+				}
+				if printErr := printJSONFiltered(w, out, flags); printErr != nil {
+					return printErr
+				}
+				if !authed {
+					return authErr(fmt.Errorf("no credentials configured"))
+				}
+				return nil
+			}
+			if !authed {
 				fmt.Fprintln(w, red("Not authenticated"))
 				fmt.Fprintln(w, "")
 				fmt.Fprintln(w, "Set your token:")
@@ -79,6 +97,13 @@ func newAuthSetTokenCmd(flags *rootFlags) *cobra.Command {
 				return configErr(fmt.Errorf("saving token: %w", err))
 			}
 
+			// JSON envelope: {saved, config_path}.
+			if flags.asJSON {
+				return printJSONFiltered(cmd.OutOrStdout(), map[string]any{
+					"saved":       true,
+					"config_path": cfg.Path,
+				}, flags)
+			}
 			fmt.Fprintf(cmd.OutOrStdout(), "Token saved to %s\n", cfg.Path)
 			return nil
 		},
@@ -100,9 +125,24 @@ func newAuthLogoutCmd(flags *rootFlags) *cobra.Command {
 				return configErr(fmt.Errorf("clearing tokens: %w", err))
 			}
 
-			// Warn if env vars still set
-			if os.Getenv("PRINTING_PRESS_GOLDEN_API_KEY") != "" {
-				fmt.Fprintf(cmd.OutOrStdout(), "Config cleared. Note: PRINTING_PRESS_GOLDEN_API_KEY env var is still set.\n")
+			// Identify which (if any) auth env var is still exported so the
+			// JSON envelope and the human prose can both surface it.
+			envStillSet := ""
+			if envStillSet == "" && os.Getenv("PRINTING_PRESS_GOLDEN_API_KEY") != "" {
+				envStillSet = "PRINTING_PRESS_GOLDEN_API_KEY"
+			}
+
+			// JSON envelope: {cleared: true, note?: "<env_var> env var is still set"}.
+			if flags.asJSON {
+				out := map[string]any{"cleared": true}
+				if envStillSet != "" {
+					out["note"] = envStillSet + " env var is still set"
+				}
+				return printJSONFiltered(cmd.OutOrStdout(), out, flags)
+			}
+
+			if envStillSet != "" {
+				fmt.Fprintf(cmd.OutOrStdout(), "Config cleared. Note: %s env var is still set.\n", envStillSet)
 				return nil
 			}
 			fmt.Fprintln(cmd.OutOrStdout(), "Logged out. Credentials cleared.")
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/doctor.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/doctor.go
index 1f83b0be..8ca98b6c 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/doctor.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/doctor.go
@@ -68,6 +68,9 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "doctor",
 		Short: "Check CLI health",
+		Example: `  printing-press-golden-pp-cli doctor
+  printing-press-golden-pp-cli doctor --json
+  printing-press-golden-pp-cli doctor --fail-on warn`,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			report := map[string]any{}
 
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/feedback.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/feedback.go
new file mode 100644
index 00000000..c8be00b9
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/feedback.go
@@ -0,0 +1,225 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package cli
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"github.com/spf13/cobra"
+)
+
+// FeedbackEntry is one line in the local feedback ledger. Every run of
+// the feedback command appends one entry; upstream POST is a separate,
+// optional step.
+type FeedbackEntry struct {
+	Text      string    `json:"text"`
+	CLI       string    `json:"cli"`
+	Version   string    `json:"version"`
+	AgentID   string    `json:"agent_id,omitempty"`
+	Timestamp time.Time `json:"timestamp"`
+}
+
+const feedbackMaxTextLen = 4096
+
+func feedbackFilePath() (string, error) {
+	home, err := os.UserHomeDir()
+	if err != nil {
+		return "", fmt.Errorf("resolving home dir: %w", err)
+	}
+	dir := filepath.Join(home, ".printing-press-golden-pp-cli")
+	if err := os.MkdirAll(dir, 0o700); err != nil {
+		return "", fmt.Errorf("creating state dir: %w", err)
+	}
+	return filepath.Join(dir, "feedback.jsonl"), nil
+}
+
+// FeedbackEndpointConfigured reports whether an upstream feedback URL
+// is available. Surfaced via agent-context so introspecting agents know
+// whether their feedback will ship upstream.
+func FeedbackEndpointConfigured() bool {
+	return os.Getenv("PRINTING_PRESS_GOLDEN_FEEDBACK_ENDPOINT") != ""
+}
+
+func feedbackEndpoint() string {
+	return os.Getenv("PRINTING_PRESS_GOLDEN_FEEDBACK_ENDPOINT")
+}
+
+func feedbackAutoSend() bool {
+	v := strings.ToLower(strings.TrimSpace(os.Getenv("PRINTING_PRESS_GOLDEN_FEEDBACK_AUTO_SEND")))
+	return v == "1" || v == "true" || v == "yes"
+}
+
+func appendFeedback(entry FeedbackEntry) error {
+	p, err := feedbackFilePath()
+	if err != nil {
+		return err
+	}
+	f, err := os.OpenFile(p, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
+	if err != nil {
+		return fmt.Errorf("opening feedback ledger: %w", err)
+	}
+	defer f.Close()
+	return json.NewEncoder(f).Encode(entry)
+}
+
+func postFeedback(url string, entry FeedbackEntry) error {
+	body, err := json.Marshal(entry)
+	if err != nil {
+		return err
+	}
+	req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
+	if err != nil {
+		return fmt.Errorf("building feedback request: %w", err)
+	}
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("User-Agent", "printing-press-golden-pp-cli/feedback")
+	client := &http.Client{Timeout: 15 * time.Second}
+	resp, err := client.Do(req)
+	if err != nil {
+		return fmt.Errorf("posting feedback: %w", err)
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode >= 400 {
+		return fmt.Errorf("feedback endpoint returned %s", resp.Status)
+	}
+	return nil
+}
+
+func newFeedbackCmd(flags *rootFlags) *cobra.Command {
+	var useStdin bool
+	var send bool
+	cmd := &cobra.Command{
+		Use:   "feedback [text]",
+		Short: "Record feedback about this CLI (local by default; upstream opt-in)",
+		Long: `Feedback is captured locally first at ~/.printing-press-golden-pp-cli/feedback.jsonl.
+When ` + "`PRINTING_PRESS_GOLDEN_FEEDBACK_ENDPOINT`" + ` is set and either --send is
+passed or ` + "`PRINTING_PRESS_GOLDEN_FEEDBACK_AUTO_SEND=true`" + `, the entry is
+POSTed as JSON after the local write.
+
+Write what surprised you or tripped you up, not a bug report. The
+loop is: agent notices friction -> one invocation -> captured -> the
+maintainer sees it.`,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			var text string
+			if useStdin {
+				data, err := io.ReadAll(cmd.InOrStdin())
+				if err != nil {
+					return fmt.Errorf("reading stdin: %w", err)
+				}
+				text = strings.TrimSpace(string(data))
+			} else if len(args) > 0 {
+				text = strings.Join(args, " ")
+			}
+			text = strings.TrimSpace(text)
+			if text == "" {
+				return fmt.Errorf("feedback text is empty (pass arguments or --stdin)")
+			}
+			truncated := false
+			if len(text) > feedbackMaxTextLen {
+				text = text[:feedbackMaxTextLen]
+				truncated = true
+			}
+
+			entry := FeedbackEntry{
+				Text:      text,
+				CLI:       "printing-press-golden-pp-cli",
+				Version:   version,
+				AgentID:   os.Getenv("AGENT_ID"),
+				Timestamp: time.Now().UTC(),
+			}
+			if err := appendFeedback(entry); err != nil {
+				return err
+			}
+
+			upstreamResult := map[string]any{"sent": false}
+			if endpoint := feedbackEndpoint(); endpoint != "" && (send || feedbackAutoSend()) {
+				if err := postFeedback(endpoint, entry); err != nil {
+					fmt.Fprintf(cmd.ErrOrStderr(), "warning: feedback upstream POST failed: %v\n", err)
+					upstreamResult["sent"] = false
+					upstreamResult["error"] = err.Error()
+				} else {
+					upstreamResult["sent"] = true
+					upstreamResult["endpoint"] = endpoint
+				}
+			}
+
+			if flags.asJSON {
+				return printJSONFiltered(cmd.OutOrStdout(), map[string]any{
+					"recorded":  true,
+					"truncated": truncated,
+					"upstream":  upstreamResult,
+					"entry":     entry,
+				}, flags)
+			}
+			fmt.Fprintf(cmd.OutOrStdout(), "feedback recorded locally (%d chars%s)\n", len(text), func() string {
+				if truncated {
+					return ", truncated"
+				}
+				return ""
+			}())
+			if sent, _ := upstreamResult["sent"].(bool); sent {
+				fmt.Fprintf(cmd.OutOrStdout(), "upstream POST: %v\n", upstreamResult["endpoint"])
+			}
+			return nil
+		},
+	}
+	cmd.Flags().BoolVar(&useStdin, "stdin", false, "Read feedback body from stdin rather than arguments")
+	cmd.Flags().BoolVar(&send, "send", false, "POST to the configured feedback endpoint in addition to local write")
+
+	cmd.AddCommand(newFeedbackListCmd(flags))
+	return cmd
+}
+
+func newFeedbackListCmd(flags *rootFlags) *cobra.Command {
+	var limit int
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: "List recent feedback entries",
+		Example: `  printing-press-golden-pp-cli feedback list
+  printing-press-golden-pp-cli feedback list --limit 5
+  printing-press-golden-pp-cli feedback list --json`,
+		RunE: func(cmd *cobra.Command, _ []string) error {
+			p, err := feedbackFilePath()
+			if err != nil {
+				return err
+			}
+			data, err := os.ReadFile(p)
+			if err != nil {
+				if os.IsNotExist(err) {
+					if flags.asJSON {
+						return printJSONFiltered(cmd.OutOrStdout(), []FeedbackEntry{}, flags)
+					}
+					return nil
+				}
+				return err
+			}
+			var entries []FeedbackEntry
+			for _, line := range strings.Split(string(data), "\n") {
+				line = strings.TrimSpace(line)
+				if line == "" {
+					continue
+				}
+				var e FeedbackEntry
+				if err := json.Unmarshal([]byte(line), &e); err != nil {
+					continue
+				}
+				entries = append(entries, e)
+			}
+			if limit > 0 && limit < len(entries) {
+				entries = entries[len(entries)-limit:]
+			}
+			return printJSONFiltered(cmd.OutOrStdout(), entries, flags)
+		},
+	}
+	cmd.Flags().IntVar(&limit, "limit", 20, "Maximum number of recent entries to return")
+	return cmd
+}
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/import.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/import.go
new file mode 100644
index 00000000..96060e38
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/import.go
@@ -0,0 +1,109 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package cli
+
+import (
+	"bufio"
+	"encoding/json"
+	"fmt"
+	"io"
+	"os"
+	"strings"
+
+	"github.com/spf13/cobra"
+)
+
+func newImportCmd(flags *rootFlags) *cobra.Command {
+	var inputFile string
+	var dryRun bool
+	var batchSize int
+
+	cmd := &cobra.Command{
+		Use:   "import <resource>",
+		Short: "Import data from JSONL file via API create/upsert calls",
+		Long: `Import data from a JSONL file by issuing POST requests for each record.
+Each line must be a valid JSON object. Failed records are logged to stderr
+but do not stop the import.`,
+		Example: `  # Import from a JSONL file
+  printing-press-golden-pp-cli import <resource> --input data.jsonl
+
+  # Dry-run to preview without sending
+  printing-press-golden-pp-cli import <resource> --input data.jsonl --dry-run
+
+  # Import from stdin
+  cat data.jsonl | printing-press-golden-pp-cli import <resource> --input -`,
+		Args: cobra.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			c, err := flags.newClient()
+			if err != nil {
+				return err
+			}
+			c.DryRun = dryRun
+
+			resource := args[0]
+			path := "/" + resource
+
+			var reader io.Reader
+			if inputFile == "-" || inputFile == "" {
+				reader = os.Stdin
+			} else {
+				f, err := os.Open(inputFile)
+				if err != nil {
+					return fmt.Errorf("opening input file: %w", err)
+				}
+				defer f.Close()
+				reader = f
+			}
+
+			scanner := bufio.NewScanner(reader)
+			scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // 1MB line buffer
+
+			var success, failed, skipped int
+			for scanner.Scan() {
+				line := strings.TrimSpace(scanner.Text())
+				if line == "" || line[0] == '#' {
+					skipped++
+					continue
+				}
+
+				var body map[string]any
+				if err := json.Unmarshal([]byte(line), &body); err != nil {
+					fmt.Fprintf(os.Stderr, "warning: skipping invalid JSON line: %v\n", err)
+					failed++
+					continue
+				}
+
+				_, _, err := c.Post(path, body)
+				if err != nil {
+					fmt.Fprintf(os.Stderr, "warning: failed to import record: %v\n", err)
+					failed++
+					continue
+				}
+				success++
+			}
+
+			if err := scanner.Err(); err != nil {
+				return fmt.Errorf("reading input: %w", err)
+			}
+
+			// JSON envelope: {succeeded, failed, skipped}.
+			if flags.asJSON {
+				return printJSONFiltered(cmd.OutOrStdout(), map[string]any{
+					"succeeded": success,
+					"failed":    failed,
+					"skipped":   skipped,
+				}, flags)
+			}
+			fmt.Fprintf(os.Stderr, "Import complete: %d succeeded, %d failed, %d skipped\n", success, failed, skipped)
+			return nil
+		},
+	}
+
+	cmd.Flags().StringVarP(&inputFile, "input", "i", "", "Input JSONL file path (use - for stdin)")
+	_ = cmd.MarkFlagRequired("input")
+	cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Preview import without sending requests")
+	cmd.Flags().IntVar(&batchSize, "batch-size", 1, "Records per batch (future: batch API support)")
+
+	return cmd
+}
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/profile.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/profile.go
new file mode 100644
index 00000000..f3b60421
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/profile.go
@@ -0,0 +1,344 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package cli
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+
+	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
+)
+
+// Profile is a named set of flag values saved for reuse across invocations.
+// HeyGen's "Beacon" pattern: one named context that a scheduled agent reuses
+// day after day with the same voice/format but different input each run.
+type Profile struct {
+	Name        string            `json:"name"`
+	Description string            `json:"description,omitempty"`
+	Values      map[string]string `json:"values"`
+}
+
+type profileStore struct {
+	Profiles map[string]Profile `json:"profiles"`
+}
+
+func profileStorePath() (string, error) {
+	home, err := os.UserHomeDir()
+	if err != nil {
+		return "", fmt.Errorf("resolving home dir: %w", err)
+	}
+	dir := filepath.Join(home, ".printing-press-golden-pp-cli")
+	if err := os.MkdirAll(dir, 0o700); err != nil {
+		return "", fmt.Errorf("creating state dir: %w", err)
+	}
+	return filepath.Join(dir, "profiles.json"), nil
+}
+
+func loadProfileStore() (*profileStore, error) {
+	p, err := profileStorePath()
+	if err != nil {
+		return nil, err
+	}
+	data, err := os.ReadFile(p)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return &profileStore{Profiles: map[string]Profile{}}, nil
+		}
+		return nil, fmt.Errorf("reading profiles: %w", err)
+	}
+	var s profileStore
+	if err := json.Unmarshal(data, &s); err != nil {
+		return nil, fmt.Errorf("parsing profiles: %w", err)
+	}
+	if s.Profiles == nil {
+		s.Profiles = map[string]Profile{}
+	}
+	return &s, nil
+}
+
+func saveProfileStore(s *profileStore) error {
+	p, err := profileStorePath()
+	if err != nil {
+		return err
+	}
+	data, err := json.MarshalIndent(s, "", "  ")
+	if err != nil {
+		return fmt.Errorf("marshaling profiles: %w", err)
+	}
+	tmp := p + ".tmp"
+	if err := os.WriteFile(tmp, data, 0o600); err != nil {
+		return fmt.Errorf("writing profiles: %w", err)
+	}
+	return os.Rename(tmp, p)
+}
+
+// GetProfile returns a profile by name, or (nil, nil) if not found.
+func GetProfile(name string) (*Profile, error) {
+	s, err := loadProfileStore()
+	if err != nil {
+		return nil, err
+	}
+	if p, ok := s.Profiles[name]; ok {
+		return &p, nil
+	}
+	return nil, nil
+}
+
+// ApplyProfileToFlags overlays profile values onto flags that the user has
+// not set explicitly on the command line. Used from root.go's
+// PersistentPreRunE so profile values feed the whole command tree.
+func ApplyProfileToFlags(cmd *cobra.Command, profile *Profile) error {
+	if profile == nil || len(profile.Values) == 0 {
+		return nil
+	}
+	// Reserved flags that never come from a profile - they control profile
+	// resolution itself or are dangerous to overlay.
+	reserved := map[string]bool{
+		"profile": true, "config": true, "help": true,
+	}
+	for name, value := range profile.Values {
+		if reserved[name] {
+			continue
+		}
+		flag := cmd.Flags().Lookup(name)
+		if flag == nil {
+			flag = cmd.InheritedFlags().Lookup(name)
+		}
+		if flag == nil {
+			continue
+		}
+		if flag.Changed {
+			continue
+		}
+		if err := flag.Value.Set(value); err != nil {
+			return fmt.Errorf("applying profile value %s=%q: %w", name, value, err)
+		}
+	}
+	return nil
+}
+
+// ListProfileNames returns profile names sorted alphabetically. Used by the
+// agent-context subcommand to expose available_profiles at runtime.
+func ListProfileNames() []string {
+	s, err := loadProfileStore()
+	if err != nil {
+		return nil
+	}
+	names := make([]string, 0, len(s.Profiles))
+	for name := range s.Profiles {
+		names = append(names, name)
+	}
+	sort.Strings(names)
+	return names
+}
+
+func newProfileCmd(flags *rootFlags) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "profile",
+		Short: "Named sets of flags saved for reuse",
+		Long: `Profiles capture a set of flag values under a name so a scheduled
+agent can invoke the same command with the same configuration each run.
+
+  profile save <name>         captures the current invocation's set flags
+  profile use <name>          prints the values (for inspection)
+  profile list                lists all saved profiles
+  profile show <name>         shows the values of one profile
+  profile delete <name>       removes a profile
+
+Use --profile <name> on any command to apply that profile's values.
+Explicit flags override profile values.`,
+	}
+	cmd.AddCommand(newProfileSaveCmd(flags))
+	cmd.AddCommand(newProfileUseCmd(flags))
+	cmd.AddCommand(newProfileListCmd(flags))
+	cmd.AddCommand(newProfileShowCmd(flags))
+	cmd.AddCommand(newProfileDeleteCmd(flags))
+	return cmd
+}
+
+func newProfileSaveCmd(flags *rootFlags) *cobra.Command {
+	var description string
+	cmd := &cobra.Command{
+		Use:   "save <name> [--<flag> <value> ...]",
+		Short: "Save the current invocation's non-default flags as a named profile",
+		Long: `Captures every flag explicitly set on the invocation and stores
+them under <name>. To update an existing profile, run save again; the
+entry is replaced.
+
+To avoid creating empty profiles, at least one non-default flag must be
+present (other than --profile and --config).`,
+		Example: `  printing-press-golden-pp-cli profile save my-defaults --json --compact
+  printing-press-golden-pp-cli profile save tonight-defaults --region US`,
+		Args: cobra.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			name := args[0]
+			if strings.ContainsAny(name, `/\: `) {
+				return fmt.Errorf("profile name %q contains reserved characters", name)
+			}
+			values := map[string]string{}
+			// Walk inherited + local flags, capture only those the user set.
+			skip := map[string]bool{"profile": true, "config": true, "help": true, "description": true}
+			visit := func(fl *pflag.Flag) {
+				if fl.Changed && !skip[fl.Name] {
+					values[fl.Name] = fl.Value.String()
+				}
+			}
+			cmd.InheritedFlags().VisitAll(visit)
+			cmd.Flags().VisitAll(visit)
+			if len(values) == 0 {
+				return fmt.Errorf("no non-default flags set - pass at least one flag to save into %q", name)
+			}
+			s, err := loadProfileStore()
+			if err != nil {
+				return err
+			}
+			s.Profiles[name] = Profile{Name: name, Description: description, Values: values}
+			if err := saveProfileStore(s); err != nil {
+				return err
+			}
+			if flags.asJSON {
+				return printJSONFiltered(cmd.OutOrStdout(), s.Profiles[name], flags)
+			}
+			fmt.Fprintf(cmd.OutOrStdout(), "saved profile %q with %d values\n", name, len(values))
+			return nil
+		},
+	}
+	cmd.Flags().StringVar(&description, "description", "", "Short description shown in 'profile list'")
+	return cmd
+}
+
+func newProfileUseCmd(flags *rootFlags) *cobra.Command {
+	return &cobra.Command{
+		Use:   "use <name>",
+		Short: "Print the flag values a profile will apply (does not execute anything)",
+		Example: `  printing-press-golden-pp-cli profile use my-defaults
+  printing-press-golden-pp-cli profile use tonight-defaults --json`,
+		Args: cobra.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			p, err := GetProfile(args[0])
+			if err != nil {
+				return err
+			}
+			if p == nil {
+				return fmt.Errorf("profile %q not found", args[0])
+			}
+			if flags.asJSON {
+				return printJSONFiltered(cmd.OutOrStdout(), p, flags)
+			}
+			fmt.Fprintf(cmd.OutOrStdout(), "profile %q:\n", p.Name)
+			if p.Description != "" {
+				fmt.Fprintf(cmd.OutOrStdout(), "  description: %s\n", p.Description)
+			}
+			keys := make([]string, 0, len(p.Values))
+			for k := range p.Values {
+				keys = append(keys, k)
+			}
+			sort.Strings(keys)
+			for _, k := range keys {
+				fmt.Fprintf(cmd.OutOrStdout(), "  --%s %s\n", k, p.Values[k])
+			}
+			return nil
+		},
+	}
+}
+
+func newProfileListCmd(flags *rootFlags) *cobra.Command {
+	return &cobra.Command{
+		Use:   "list",
+		Short: "List saved profiles",
+		Example: `  printing-press-golden-pp-cli profile list
+  printing-press-golden-pp-cli profile list --json`,
+		RunE: func(cmd *cobra.Command, _ []string) error {
+			s, err := loadProfileStore()
+			if err != nil {
+				return err
+			}
+			names := make([]string, 0, len(s.Profiles))
+			for n := range s.Profiles {
+				names = append(names, n)
+			}
+			sort.Strings(names)
+			if flags.asJSON {
+				out := make([]map[string]any, 0, len(names))
+				for _, n := range names {
+					p := s.Profiles[n]
+					out = append(out, map[string]any{
+						"name":        p.Name,
+						"description": p.Description,
+						"field_count": len(p.Values),
+					})
+				}
+				return printJSONFiltered(cmd.OutOrStdout(), out, flags)
+			}
+			headers := []string{"NAME", "FIELDS", "DESCRIPTION"}
+			rows := make([][]string, 0, len(names))
+			for _, n := range names {
+				p := s.Profiles[n]
+				rows = append(rows, []string{p.Name, fmt.Sprintf("%d", len(p.Values)), p.Description})
+			}
+			return flags.printTable(cmd, headers, rows)
+		},
+	}
+}
+
+func newProfileShowCmd(flags *rootFlags) *cobra.Command {
+	return &cobra.Command{
+		Use:   "show <name>",
+		Short: "Show a profile's values as JSON",
+		Example: `  printing-press-golden-pp-cli profile show my-defaults
+  printing-press-golden-pp-cli profile show tonight-defaults --json`,
+		Args: cobra.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			p, err := GetProfile(args[0])
+			if err != nil {
+				return err
+			}
+			if p == nil {
+				return fmt.Errorf("profile %q not found", args[0])
+			}
+			return printJSONFiltered(cmd.OutOrStdout(), p, flags)
+		},
+	}
+}
+
+func newProfileDeleteCmd(flags *rootFlags) *cobra.Command {
+	return &cobra.Command{
+		Use:   "delete <name>",
+		Short: "Remove a profile",
+		Example: `  printing-press-golden-pp-cli profile delete my-defaults --yes
+  printing-press-golden-pp-cli profile delete old-profile --yes --json`,
+		Args: cobra.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			name := args[0]
+			s, err := loadProfileStore()
+			if err != nil {
+				return err
+			}
+			if _, ok := s.Profiles[name]; !ok {
+				return fmt.Errorf("profile %q not found", name)
+			}
+			if !flags.yes {
+				fmt.Fprintf(cmd.ErrOrStderr(), "refusing to delete %q without --yes\n", name)
+				return fmt.Errorf("confirmation required: pass --yes")
+			}
+			delete(s.Profiles, name)
+			if err := saveProfileStore(s); err != nil {
+				return err
+			}
+			// JSON envelope: {deleted: name}.
+			if flags.asJSON {
+				return printJSONFiltered(cmd.OutOrStdout(), map[string]any{
+					"deleted": name,
+				}, flags)
+			}
+			fmt.Fprintf(cmd.OutOrStdout(), "deleted profile %q\n", name)
+			return nil
+		},
+	}
+}
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go
index 142dcc2f..829f1909 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go
@@ -38,7 +38,7 @@ func newProjectsTasksUpdateProjectCmd(flags *rootFlags) *cobra.Command {
 			path := "/projects/{projectId}/tasks/{taskId}"
 			path = replacePathParam(path, "projectId", args[0])
 			if len(args) < 2 {
-				return usageErr(fmt.Errorf("taskId is required\nUsage: %s %s <%s>", cmd.Root().Name(), cmd.CommandPath(), "taskId"))
+				return usageErr(fmt.Errorf("taskId is required\nUsage: %s <%s>", cmd.CommandPath(), "taskId"))
 			}
 			path = replacePathParam(path, "taskId", args[1])
 			var body map[string]any
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.go
new file mode 100644
index 00000000..50694e61
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.go
@@ -0,0 +1,1114 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package cli
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"os"
+	"regexp"
+	"strconv"
+	"strings"
+	"sync"
+	"sync/atomic"
+	"time"
+	"printing-press-golden-pp-cli/internal/store"
+	"github.com/spf13/cobra"
+)
+
+// syncResult holds the outcome of syncing a single resource.
+type syncResult struct {
+	Resource string
+	Count    int
+	Err      error
+	Warn     error
+	Duration time.Duration
+}
+
+func newSyncCmd(flags *rootFlags) *cobra.Command {
+	var resources []string
+	var full bool
+	var since string
+	var concurrency int
+	var dbPath string
+	var maxPages int
+	var latestOnly bool
+	var strict bool
+
+	cmd := &cobra.Command{
+		Use:   "sync",
+		Short: "Sync API data to local SQLite for offline search and analysis",
+		Long: `Sync data from the API into a local SQLite database. Supports resumable
+incremental sync (only fetches new data since last sync) and full resync.
+Once synced, use the 'search' command for instant full-text search.
+
+Exit codes & warnings:
+  Resources the API denies access to (HTTP 403, or HTTP 400 with an
+  access-policy body) are reported as warnings rather than failing the
+  run. In --json mode each is emitted as a {"event":"sync_warning",...}
+  line carrying status, reason, and message fields, and a final
+  {"event":"sync_summary",...} aggregates the run.
+
+  Exit 0 when at least one resource synced and no resource flagged in
+  the spec as critical (x-critical: true) failed; non-critical failures
+  emit {"event":"sync_warning","reason":"exit_policy_default_changed",
+  ...} so callers can detect that a partial failure was tolerated. Pass
+  --strict to exit non-zero on any per-resource failure. Exit is always
+  non-zero when every selected resource failed, regardless of --strict.`,
+		Example: `  # Sync all resources
+  printing-press-golden-pp-cli sync
+
+  # Sync specific resources only
+  printing-press-golden-pp-cli sync --resources channels,messages
+
+  # Full resync (ignore previous checkpoint)
+  printing-press-golden-pp-cli sync --full
+
+  # Incremental sync: only records from the last 7 days
+  printing-press-golden-pp-cli sync --since 7d
+
+  # Parallel sync with 8 workers
+  printing-press-golden-pp-cli sync --concurrency 8
+
+  # Latest-only: refresh head of each resource, no historical backfill
+  printing-press-golden-pp-cli sync --latest-only`,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			c, err := flags.newClient()
+			if err != nil {
+				return err
+			}
+			c.NoCache = true
+
+			if dbPath == "" {
+				dbPath = defaultDBPath("printing-press-golden-pp-cli")
+			}
+
+			db, err := store.OpenWithContext(cmd.Context(), dbPath)
+			if err != nil {
+				return fmt.Errorf("opening local database: %w", err)
+			}
+			defer db.Close()
+
+			// If no specific resources, sync top-level resources
+			if len(resources) == 0 {
+				resources = defaultSyncResources()
+			}
+
+			// --full: clear all sync cursors before starting
+			if full {
+				for _, resource := range resources {
+					_ = db.SaveSyncState(resource, "", 0)
+				}
+			}
+
+			// --latest-only narrows to the first page of each resource
+			// ignoring the historical resume cursor. We cap maxPages at 1
+			// here rather than re-interpreting it downstream so the rest
+			// of the sync loop stays oblivious. Mutually-useful with
+			// --since: if the user set --since, that threshold still wins
+			// and we don't short-circuit historical context they asked for.
+			if latestOnly {
+				if since == "" {
+					maxPages = 1
+					// Clear the cursor so we start from the head each time;
+					// the goal of --latest-only is "refresh the top" not
+					// "resume from wherever I left off".
+					for _, resource := range resources {
+						existing, _, _, _ := db.GetSyncState(resource)
+						if existing != "" {
+							_ = db.SaveSyncState(resource, "", 0)
+						}
+					}
+				} else if humanFriendly {
+					fmt.Fprintln(os.Stderr, "warning: --latest-only ignored because --since is set; --since takes precedence")
+				}
+			}
+
+			// Resolve --since into an RFC3339 timestamp
+			sinceTS := ""
+			if since != "" {
+				ts, err := parseSinceDuration(since)
+				if err != nil {
+					return fmt.Errorf("invalid --since value %q: %w", since, err)
+				}
+				sinceTS = ts.Format(time.RFC3339)
+			}
+
+			// Worker pool: produce resources, N workers consume
+			if concurrency < 1 {
+				concurrency = 4
+			}
+
+			started := time.Now()
+			work := make(chan string, len(resources))
+			results := make(chan syncResult, len(resources))
+
+			var wg sync.WaitGroup
+			for i := 0; i < concurrency; i++ {
+				wg.Add(1)
+				go func() {
+					defer wg.Done()
+					for resource := range work {
+						res := syncResource(c, db, resource, sinceTS, full, maxPages)
+						results <- res
+					}
+				}()
+			}
+
+			// Enqueue all resources
+			for _, resource := range resources {
+				work <- resource
+			}
+			close(work)
+
+			// Collect results in a separate goroutine
+			go func() {
+				wg.Wait()
+				close(results)
+			}()
+
+			var totalSynced int
+			var errCount int
+			var criticalErrCount int
+			var warnCount int
+			var successCount int
+			for res := range results {
+				if res.Err != nil {
+					if humanFriendly {
+						fmt.Fprintf(os.Stderr, "  %s: error: %v\n", res.Resource, res.Err)
+					}
+					errCount++
+					if criticalResources[res.Resource] {
+						criticalErrCount++
+					}
+				} else if res.Warn != nil {
+					if humanFriendly {
+						fmt.Fprintf(os.Stderr, "  %s: warning: %v\n", res.Resource, res.Warn)
+					}
+					warnCount++
+				} else {
+					if humanFriendly {
+						fmt.Fprintf(os.Stderr, "  %s: %d synced (done)\n", res.Resource, res.Count)
+					}
+					totalSynced += res.Count
+					successCount++
+				}
+			}
+			// Sync dependent (parent-child) resources sequentially after flat resources.
+			depResults := syncDependentResources(c, db, sinceTS, full, maxPages)
+			for _, res := range depResults {
+				if res.Err != nil {
+					if humanFriendly {
+						fmt.Fprintf(os.Stderr, "  %s: error: %v\n", res.Resource, res.Err)
+					}
+					errCount++
+					if criticalResources[res.Resource] {
+						criticalErrCount++
+					}
+				} else if res.Warn != nil {
+					if humanFriendly {
+						fmt.Fprintf(os.Stderr, "  %s: warning: %v\n", res.Resource, res.Warn)
+					}
+					warnCount++
+				} else {
+					if humanFriendly {
+						fmt.Fprintf(os.Stderr, "  %s: %d synced (done)\n", res.Resource, res.Count)
+					}
+					totalSynced += res.Count
+					successCount++
+				}
+			}
+
+			elapsed := time.Since(started)
+			totalResources := successCount + warnCount + errCount
+			if humanFriendly {
+				if warnCount > 0 {
+					fmt.Fprintf(os.Stderr, "Sync complete: %d records across %d resources (%d warned, %.1fs)\n",
+						totalSynced, totalResources, warnCount, elapsed.Seconds())
+				} else {
+					fmt.Fprintf(os.Stderr, "Sync complete: %d records across %d resources (%.1fs)\n",
+						totalSynced, totalResources, elapsed.Seconds())
+				}
+			} else {
+				fmt.Fprintf(os.Stderr, `{"event":"sync_summary","total_records":%d,"resources":%d,"success":%d,"warned":%d,"errored":%d,"duration_ms":%d}`+"\n",
+					totalSynced, totalResources, successCount, warnCount, errCount, elapsed.Milliseconds())
+			}
+
+			// Exit-code policy:
+			//   1. --strict + any error  -> non-zero (legacy contract)
+			//   2. any critical failure  -> non-zero regardless of --strict
+			//   3. nothing synced        -> non-zero (preserves "all-warned" / "all-errored" exit)
+			//   4. otherwise             -> exit 0 (any data synced + no critical failed)
+			// When branch 4 suppresses what branch 1 would have rejected, emit a
+			// one-shot sync_warning with reason "exit_policy_default_changed" so
+			// CI scripts that depend on $? != 0 can discover the contract change
+			// without reading the CHANGELOG.
+			if strict && errCount > 0 {
+				return fmt.Errorf("%d resource(s) failed to sync", errCount)
+			}
+			if criticalErrCount > 0 {
+				return fmt.Errorf("%d critical resource(s) failed to sync", criticalErrCount)
+			}
+			if successCount == 0 {
+				if warnCount > 0 && errCount == 0 {
+					return fmt.Errorf("%d resource(s) skipped due to insufficient access", warnCount)
+				}
+				if errCount > 0 {
+					return fmt.Errorf("%d resource(s) failed to sync", errCount)
+				}
+			}
+			if errCount > 0 && !strict && criticalErrCount == 0 && successCount > 0 {
+				if !humanFriendly {
+					msg := fmt.Sprintf("%d resource(s) failed but exit code is 0 because the new default treats non-critical failures as warnings. Pass --strict to restore the old behavior, or annotate critical resources with x-critical: true. See CHANGELOG.", errCount)
+					fmt.Fprintf(os.Stderr, `{"event":"sync_warning","reason":"exit_policy_default_changed","errored":%d,"message":"%s"}`+"\n",
+						errCount, strings.ReplaceAll(msg, `"`, `\"`))
+				} else {
+					fmt.Fprintf(os.Stderr, "warning: %d resource(s) failed but exit code is 0 because the new default treats non-critical failures as warnings. Pass --strict to restore the old behavior, or annotate critical resources with x-critical: true.\n", errCount)
+				}
+			}
+			return nil
+		},
+	}
+
+	cmd.Flags().StringSliceVar(&resources, "resources", nil, "Comma-separated resource types to sync")
+	cmd.Flags().BoolVar(&full, "full", false, "Full resync (ignore previous checkpoint)")
+	cmd.Flags().StringVar(&since, "since", "", "Incremental sync duration (e.g. 7d, 24h, 1w, 30m)")
+	cmd.Flags().IntVar(&concurrency, "concurrency", 4, "Number of parallel sync workers")
+	cmd.Flags().StringVar(&dbPath, "db", "", "Database path (default: ~/.local/share/printing-press-golden-pp-cli/data.db)")
+	cmd.Flags().IntVar(&maxPages, "max-pages", 100, "Maximum pages to fetch per resource (0 = unlimited; cap-hit emits a sync_warning event)")
+	cmd.Flags().BoolVar(&latestOnly, "latest-only", false, "Refresh head of each resource only; clears resume cursor and caps pages at 1. Mutually exclusive with --since (--since wins).")
+	cmd.Flags().BoolVar(&strict, "strict", false, "Exit non-zero on any per-resource failure (default: only critical failures or all-resource failure exit non-zero).")
+
+	return cmd
+}
+
+// syncResource handles the full paginated sync of a single resource.
+// It resumes from the last cursor unless sinceTS or full mode overrides it.
+func syncResource(c interface {
+	Get(string, map[string]string) (json.RawMessage, error)
+	RateLimit() float64
+}, db *store.Store, resource, sinceTS string, full bool, maxPages int) syncResult {
+	started := time.Now()
+
+	if !humanFriendly {
+		fmt.Fprintf(os.Stderr, `{"event":"sync_start","resource":"%s"}`+"\n", resource)
+	}
+
+	path, err := syncResourcePath(resource)
+	if err != nil {
+		return syncResult{Resource: resource, Err: err, Duration: time.Since(started)}
+	}
+	var totalCount int
+
+	// Resume cursor from sync_state (unless --full cleared it)
+	existingCursor, lastSynced, _, _ := db.GetSyncState(resource)
+
+	// Determine the since param value:
+	// 1. Explicit --since flag takes priority
+	// 2. Otherwise use last_synced_at from sync_state for incremental sync
+	sinceParam := determineSinceParam()
+	effectiveSince := sinceTS
+	if effectiveSince == "" && !lastSynced.IsZero() && !full {
+		effectiveSince = lastSynced.Format(time.RFC3339)
+	}
+
+	cursor := existingCursor
+	pageSize := determinePaginationDefaults()
+
+	var progressCount int64
+	pagesFetched := 0
+	lastNextCursor := ""
+	// extractFailureTotal accumulates per-item primary-key extraction
+	// misses across pages within this resource sync. Resource-level
+	// concurrency is 1 (one goroutine per resource via the work channel)
+	// so this counter cannot race. We emit one primary_key_unresolved
+	// sync_anomaly per resource per run when there's at least one miss
+	// (rate-limited via the anomalyEmitted flag) and a roll-up
+	// all_items_failed_id_extraction event when 100% of a single page
+	// failed extraction.
+	var extractFailureTotal int
+	var consumedTotal int
+	anomalyEmitted := false
+
+	for {
+		params := map[string]string{}
+
+		// Set page size
+		params[pageSize.limitParam] = strconv.Itoa(pageSize.limit)
+
+		// Set cursor for resume
+		if cursor != "" {
+			params[pageSize.cursorParam] = cursor
+		}
+
+		// Set since filter
+		if effectiveSince != "" {
+			params[sinceParam] = effectiveSince
+		}
+
+		data, err := c.Get(path, params)
+		if err != nil {
+			if w, ok := isSyncAccessWarning(err); ok {
+				if !humanFriendly {
+					fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","status":%d,"reason":"%s","message":"%s"}`+"\n",
+						resource, w.Status, w.Reason, strings.ReplaceAll(w.Message, `"`, `\"`))
+				}
+				return syncResult{Resource: resource, Count: totalCount, Warn: fmt.Errorf("skipped %s: %s", resource, w.Reason), Duration: time.Since(started)}
+			}
+			if !humanFriendly {
+				fmt.Fprintf(os.Stderr, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
+			}
+			return syncResult{Resource: resource, Count: totalCount, Err: fmt.Errorf("fetching %s: %w", resource, err), Duration: time.Since(started)}
+		}
+
+		// Try to extract items from the response.
+		// Strategy: try array first, then common wrapper keys.
+		items, nextCursor, hasMore := extractPageItems(data, pageSize.cursorParam)
+
+		if len(items) == 0 {
+			// Single object response - try to store as-is
+			if err := upsertSingleObject(db, resource, data); err != nil {
+				if !humanFriendly {
+					fmt.Fprintf(os.Stderr, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
+				}
+				return syncResult{Resource: resource, Err: err, Duration: time.Since(started)}
+			}
+			totalCount++
+			break
+		}
+
+		// Batch upsert all items from this page. UpsertBatch returns
+		// (stored, extractFailures, err): stored counts rows actually
+		// landed; extractFailures counts items that survived JSON
+		// unmarshal but had no extractable primary key (templated
+		// IDField AND generic fallback both missed). Tracking these
+		// separately lets us emit precise sync_anomaly events: a
+		// roll-up "all_items_failed_id_extraction" when an entire
+		// page yields zero stored, a per-resource
+		// "primary_key_unresolved" the first time any single item
+		// fails, and the F4b "stored_count_zero_after_extraction"
+		// probe when extraction succeeded but rows still didn't land.
+		stored, extractFailures, err := upsertResourceBatch(db, resource, items)
+		if err != nil {
+			if !humanFriendly {
+				fmt.Fprintf(os.Stderr, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
+			}
+			return syncResult{Resource: resource, Count: totalCount, Err: fmt.Errorf("upserting batch for %s: %w", resource, err), Duration: time.Since(started)}
+		}
+
+		consumedTotal += len(items)
+		extractFailureTotal += extractFailures
+
+		// When a non-empty page yielded zero stored rows, the API
+		// returned items in a shape we couldn't extract IDs from —
+		// likely scalar IDs (Firebase /topstories.json, GitHub user-
+		// repo lists) where the spec author should declare a hydration
+		// pattern, or an unrecognized primary-key field name.
+		if len(items) > 0 && stored == 0 {
+			if humanFriendly {
+				fmt.Fprintf(os.Stderr, "warning: %s returned %d items but stored 0 — the local store will be empty for this resource. Likely cause: scalar item shape rather than objects with extractable IDs.\n", resource, len(items))
+			} else {
+				fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":0,"reason":"all_items_failed_id_extraction"}`+"\n", resource, len(items))
+			}
+			anomalyEmitted = true
+		} else if extractFailures > 0 && !anomalyEmitted {
+			// Per-item primary-key resolution failure but at least one
+			// item landed — emit one structured warning per resource per
+			// sync run so users see the first occurrence of silent drops
+			// instead of waiting for total failure.
+			if humanFriendly {
+				fmt.Fprintf(os.Stderr, "\nwarning: %s had %d item(s) on this page with no extractable primary key — those rows were dropped silently. Annotate the spec with x-resource-id to fix.\n", resource, extractFailures)
+			} else {
+				fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":%d,"count":%d,"reason":"primary_key_unresolved"}`+"\n", resource, len(items), stored, extractFailures)
+			}
+			anomalyEmitted = true
+		}
+
+		totalCount += stored
+		atomic.AddInt64(&progressCount, int64(stored))
+
+		// Progress reporting (include rate limit info when active)
+		currentRate := c.RateLimit()
+		if humanFriendly {
+			if currentRate > 0 {
+				fmt.Fprintf(os.Stderr, "\r  %s: %d synced [%.1f req/s]", resource, atomic.LoadInt64(&progressCount), currentRate)
+			} else {
+				fmt.Fprintf(os.Stderr, "\r  %s: %d synced", resource, atomic.LoadInt64(&progressCount))
+			}
+		} else {
+			if currentRate > 0 {
+				fmt.Fprintf(os.Stderr, `{"event":"sync_progress","resource":"%s","fetched":%d,"rate_rps":%.1f}`+"\n", resource, atomic.LoadInt64(&progressCount), currentRate)
+			} else {
+				fmt.Fprintf(os.Stderr, `{"event":"sync_progress","resource":"%s","fetched":%d}`+"\n", resource, atomic.LoadInt64(&progressCount))
+			}
+		}
+
+		// Save cursor after each page for resumability
+		if err := db.SaveSyncState(resource, nextCursor, totalCount); err != nil {
+			// Non-fatal: log and continue
+			fmt.Fprintf(os.Stderr, "\nwarning: failed to save sync state for %s: %v\n", resource, err)
+		}
+
+		pagesFetched++
+
+		// Enforce page ceiling to prevent runaway syncs on large-catalog APIs
+		if maxPages > 0 && pagesFetched >= maxPages {
+			if humanFriendly {
+				fmt.Fprintf(os.Stderr, "\n  %s: reached --max-pages limit (%d pages, %d items)\n", resource, maxPages, totalCount)
+			} else {
+				fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","reason":"max_pages_cap_hit","message":"reached --max-pages cap of %d; data may be truncated. Re-run with --max-pages 0 (unlimited) or higher to verify."}`+"\n", resource, maxPages)
+			}
+			break
+		}
+
+		// Sticky-cursor detector: if the API echoes the same next cursor across
+		// consecutive pages without advancing, abort to prevent burning the
+		// --max-pages budget on a non-terminating loop. Checked AFTER the cap
+		// guard so cap-hit takes precedence; checked BEFORE the natural-end
+		// check below because the natural-end check would not catch a sticky
+		// non-empty cursor on its own.
+		if nextCursor != "" && nextCursor == lastNextCursor {
+			if humanFriendly {
+				fmt.Fprintf(os.Stderr, "\n  %s: API returned the same next cursor across two pages; aborting to prevent budget waste.\n", resource)
+			} else {
+				fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","reason":"stuck_pagination","message":"API returned the same next cursor across two pages for resource %s; aborting to prevent budget waste."}`+"\n", resource, resource)
+			}
+			break
+		}
+		lastNextCursor = nextCursor
+
+		// Determine if there are more pages
+		if !hasMore || len(items) < pageSize.limit || nextCursor == "" {
+			break
+		}
+
+		cursor = nextCursor
+	}
+
+	// Final sync state: clear cursor (sync is complete), update count
+	_ = db.SaveSyncState(resource, "", totalCount)
+
+	// F4b symptom probe: if items were consumed and successfully
+	// extracted (extractFailures < consumed) but nothing landed in
+	// the store, something downstream of extraction silently dropped
+	// rows — FTS5 trigger error, transaction rollback, character
+	// encoding. Emit a sync_anomaly so the symptom is visible the
+	// next time it recurs; the underlying root cause is held out for
+	// controlled repro.
+	if consumedTotal > 0 && totalCount == 0 && extractFailureTotal < consumedTotal {
+		if humanFriendly {
+			fmt.Fprintf(os.Stderr, "\nwarning: %s consumed %d items, extracted %d primary keys, but stored 0 rows — extraction succeeded yet nothing landed. Investigate FTS triggers / transaction rollback / encoding.\n", resource, consumedTotal, consumedTotal-extractFailureTotal)
+		} else {
+			fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":0,"extract_failures":%d,"reason":"stored_count_zero_after_extraction"}`+"\n", resource, consumedTotal, extractFailureTotal)
+		}
+	}
+
+	if !humanFriendly {
+		fmt.Fprintf(os.Stderr, `{"event":"sync_complete","resource":"%s","total":%d,"duration_ms":%d}`+"\n", resource, totalCount, time.Since(started).Milliseconds())
+	}
+
+	return syncResult{Resource: resource, Count: totalCount, Duration: time.Since(started)}
+}
+
+// paginationDefaults holds the resolved pagination parameter names and page size.
+type paginationDefaults struct {
+	cursorParam string
+	limitParam  string
+	limit       int
+}
+
+// determinePaginationDefaults returns the pagination parameter names to use.
+// Values are detected from the API spec by the profiler at generation time.
+func determinePaginationDefaults() paginationDefaults {
+	return paginationDefaults{
+		cursorParam: "cursor",
+		limitParam:  "limit",
+		limit:       100,
+	}
+}
+
+// determineSinceParam returns the query parameter name for incremental sync filtering.
+func determineSinceParam() string {
+	return "since"
+}
+
+// extractPageItems attempts to extract an array of items and pagination cursor from a response.
+// It tries multiple strategies:
+// 1. Direct JSON array
+// 2. Common wrapper keys: "data", "results", "items", "records", "nodes", "entries"
+// It also extracts the next cursor from common response fields.
+func extractPageItems(data json.RawMessage, cursorParam string) ([]json.RawMessage, string, bool) {
+	// Strategy 1: direct array
+	var items []json.RawMessage
+	if err := json.Unmarshal(data, &items); err == nil {
+		return items, "", false
+	}
+
+	// Strategy 2: object with known wrapper keys
+	var envelope map[string]json.RawMessage
+	if err := json.Unmarshal(data, &envelope); err != nil {
+		return nil, "", false
+	}
+
+	// Try common item keys first (fast path)
+	itemKeys := []string{"data", "results", "items", "records", "nodes", "entries"}
+	for _, key := range itemKeys {
+		if raw, ok := envelope[key]; ok {
+			if err := json.Unmarshal(raw, &items); err == nil && len(items) > 0 {
+				nextCursor, hasMore := extractPaginationFromEnvelope(envelope, cursorParam)
+				return items, nextCursor, hasMore
+			}
+		}
+	}
+
+	// Fallback: try every key in the envelope. If exactly one maps to a JSON
+	// array with items, use it. This handles APIs that wrap responses with the
+	// resource name (e.g., {"markets": [...], "cursor": "..."}).
+	var arrayKey string
+	var arrayItems []json.RawMessage
+	arrayCount := 0
+	for key, raw := range envelope {
+		var candidate []json.RawMessage
+		if err := json.Unmarshal(raw, &candidate); err == nil && len(candidate) > 0 {
+			arrayKey = key
+			arrayItems = candidate
+			arrayCount++
+		}
+	}
+	if arrayCount == 1 {
+		nextCursor, hasMore := extractPaginationFromEnvelope(envelope, cursorParam)
+		_ = arrayKey // used for detection, items extracted above
+		return arrayItems, nextCursor, hasMore
+	}
+
+	return nil, "", false
+}
+
+// extractPaginationFromEnvelope extracts cursor and has_more from a response envelope.
+func extractPaginationFromEnvelope(envelope map[string]json.RawMessage, cursorParam string) (string, bool) {
+	var hasMore bool
+
+	nextCursor := nextCursorFromLinks(envelope, cursorParam)
+
+	// Try common cursor field names
+	cursorKeys := []string{
+		"next_cursor", "nextCursor", "cursor", "next_page_token",
+		"nextPageToken", "page_token", "after", "end_cursor", "endCursor",
+	}
+	if nextCursor == "" {
+		nextCursor = findCursorInMap(envelope, cursorKeys)
+	}
+
+	// If no top-level cursor was found, look one level deeper into well-known
+	// pagination wrapper objects. Slack returns {"messages":[...],
+	// "response_metadata":{"next_cursor":"..."}}; MongoDB Atlas uses
+	// "pagination"; many APIs use "meta" or "paging". Purely additive — only
+	// runs when the top-level scan returned empty — and uses the same
+	// cursorKeys set so wrapper contents go through the same name match.
+	if nextCursor == "" {
+		paginationWrapperKeys := []string{"response_metadata", "pagination", "meta", "paging"}
+		for _, wrapperKey := range paginationWrapperKeys {
+			rawWrapper, ok := envelope[wrapperKey]
+			if !ok {
+				continue
+			}
+			var inner map[string]json.RawMessage
+			if json.Unmarshal(rawWrapper, &inner) != nil {
+				continue
+			}
+			if c := findCursorInMap(inner, cursorKeys); c != "" {
+				nextCursor = c
+				break
+			}
+		}
+	}
+
+	// Try common has_more field names
+	hasMoreKeys := []string{"has_more", "hasMore", "has_next", "hasNext", "next_page"}
+	for _, key := range hasMoreKeys {
+		if raw, ok := envelope[key]; ok {
+			if err := json.Unmarshal(raw, &hasMore); err == nil {
+				break
+			}
+		}
+	}
+
+	// If we found a cursor, assume there are more pages even without explicit has_more
+	if nextCursor != "" && !hasMore {
+		hasMore = true
+	}
+
+	return nextCursor, hasMore
+}
+
+// nextCursorFromLinks extracts JSON:API-style pagination cursors from
+// {"links":{"next":"https://example.com/items?page[cursor]=..."}}.
+func nextCursorFromLinks(envelope map[string]json.RawMessage, cursorParam string) string {
+	rawLinks, ok := envelope["links"]
+	if !ok {
+		return ""
+	}
+	var links map[string]json.RawMessage
+	if json.Unmarshal(rawLinks, &links) != nil {
+		return ""
+	}
+	rawNext, ok := links["next"]
+	if !ok {
+		return ""
+	}
+	var nextURL string
+	if json.Unmarshal(rawNext, &nextURL) != nil || nextURL == "" {
+		return ""
+	}
+
+	cursorKeys := []string{cursorParam}
+	if cursorParam != "page[cursor]" {
+		cursorKeys = append(cursorKeys, "page[cursor]")
+	}
+	if cursorParam != "cursor" {
+		cursorKeys = append(cursorKeys, "cursor")
+	}
+	if cursorParam != "after" {
+		cursorKeys = append(cursorKeys, "after")
+	}
+
+	parsed, err := url.Parse(nextURL)
+	if err != nil {
+		return ""
+	}
+	values := parsed.Query()
+	for _, key := range cursorKeys {
+		if key == "" {
+			continue
+		}
+		if cursor := values.Get(key); cursor != "" {
+			return cursor
+		}
+	}
+	return ""
+}
+
+// findCursorInMap returns the first non-empty string-typed value in m
+// whose key matches one of cursorKeys. Used by extractPaginationFromEnvelope
+// to scan both the top-level envelope and well-known wrapper objects with
+// the same name-match rules — extracted so the two scans can't drift.
+func findCursorInMap(m map[string]json.RawMessage, cursorKeys []string) string {
+	for _, key := range cursorKeys {
+		raw, ok := m[key]
+		if !ok {
+			continue
+		}
+		var s string
+		if err := json.Unmarshal(raw, &s); err == nil && s != "" {
+			return s
+		}
+	}
+	return ""
+}
+
+type discriminatorDispatch struct {
+	Field  string
+	Values map[string]string
+}
+
+var discriminatorDispatchers = map[string]discriminatorDispatch{
+}
+
+func upsertResourceBatch(db *store.Store, resource string, items []json.RawMessage) (int, int, error) {
+	if _, ok := discriminatorDispatchers[resource]; !ok {
+		return db.UpsertBatch(resource, items)
+	}
+
+	grouped := map[string][]json.RawMessage{}
+	order := []string{}
+	for _, item := range items {
+		target := resource
+		var obj map[string]any
+		if err := json.Unmarshal(item, &obj); err == nil {
+			target = resolveDiscriminatedResource(resource, obj)
+		}
+		if _, ok := grouped[target]; !ok {
+			order = append(order, target)
+		}
+		grouped[target] = append(grouped[target], item)
+	}
+
+	var stored, extractFailures int
+	for _, target := range order {
+		targetStored, targetExtractFailures, err := db.UpsertBatch(target, grouped[target])
+		if err != nil {
+			return stored, extractFailures + targetExtractFailures, err
+		}
+		stored += targetStored
+		extractFailures += targetExtractFailures
+	}
+	return stored, extractFailures, nil
+}
+
+func resolveDiscriminatedResource(resource string, obj map[string]any) string {
+	dispatcher, ok := discriminatorDispatchers[resource]
+	if !ok || dispatcher.Field == "" {
+		return resource
+	}
+	value := store.LookupFieldValue(obj, dispatcher.Field)
+	if value == nil {
+		return resource
+	}
+	if target, ok := dispatcher.Values[fmt.Sprintf("%v", value)]; ok && target != "" {
+		return target
+	}
+	return resource
+}
+
+// upsertSingleObject stores a non-array API response as a single record.
+func upsertSingleObject(db *store.Store, resource string, data json.RawMessage) error {
+	var obj map[string]any
+	if err := json.Unmarshal(data, &obj); err != nil {
+		// Not a JSON object either - store raw under resource name
+		return db.Upsert(resource, resource, data)
+	}
+
+	resource = resolveDiscriminatedResource(resource, obj)
+
+	id := extractID(resource, obj)
+	if id == "" {
+		id = resource
+	}
+
+	switch resource {
+	case "projects":
+		return db.UpsertProjects(data)
+	case "tasks":
+		return db.UpsertTasks(data)
+	case "summary":
+		return db.UpsertSummary(data)
+	default:
+		return db.Upsert(resource, id, data)
+	}
+}
+
+// parseSinceDuration converts human-friendly duration strings into a time.Time.
+// Supported formats: "7d" (days), "24h" (hours), "30m" (minutes), "1w" (weeks).
+func parseSinceDuration(s string) (time.Time, error) {
+	re := regexp.MustCompile(`^(\d+)([dhwm])$`)
+	matches := re.FindStringSubmatch(strings.TrimSpace(s))
+	if matches == nil {
+		return time.Time{}, fmt.Errorf("expected format like 7d, 24h, 1w, or 30m")
+	}
+
+	n, err := strconv.Atoi(matches[1])
+	if err != nil {
+		return time.Time{}, err
+	}
+
+	now := time.Now()
+	switch matches[2] {
+	case "d":
+		return now.Add(-time.Duration(n) * 24 * time.Hour), nil
+	case "h":
+		return now.Add(-time.Duration(n) * time.Hour), nil
+	case "w":
+		return now.Add(-time.Duration(n) * 7 * 24 * time.Hour), nil
+	case "m":
+		return now.Add(-time.Duration(n) * time.Minute), nil
+	default:
+		return time.Time{}, fmt.Errorf("unknown unit %q", matches[2])
+	}
+}
+
+func defaultSyncResources() []string {
+	return []string{
+		"currencies",
+		"projects",
+	}
+}
+
+// syncResourcePath maps resource names to their actual API endpoint paths.
+// For REST APIs this is typically "/<resource>". For non-REST APIs (e.g., Steam)
+// this preserves the actual endpoint path like "/ISteamApps/GetAppList/v2".
+func syncResourcePath(resource string) (string, error) {
+	paths := map[string]string{
+		"currencies": "/currencies",
+		"projects": "/projects",
+	}
+	if p, ok := paths[resource]; ok {
+		return p, nil
+	}
+	return "", fmt.Errorf("unknown sync resource %q", resource)
+}
+
+// dependentResourceDef describes a child resource that requires iterating parent IDs to sync.
+type dependentResourceDef struct {
+	Name          string
+	ParentTable   string
+	ParentIDParam string
+	PathTemplate  string
+}
+
+func dependentResourceDefs() []dependentResourceDef {
+	return []dependentResourceDef{
+		{Name: "tasks", ParentTable: "projects", ParentIDParam: "projectId", PathTemplate: "/projects/{projectId}/tasks"},
+	}
+}
+
+// syncDependentResources iterates parent tables and syncs child resources per parent ID.
+func syncDependentResources(c interface {
+	Get(string, map[string]string) (json.RawMessage, error)
+	RateLimit() float64
+}, db *store.Store, sinceTS string, full bool, maxPages int) []syncResult {
+	var results []syncResult
+	for _, dep := range dependentResourceDefs() {
+		res := syncDependentResource(c, db, dep, sinceTS, full, maxPages)
+		results = append(results, res)
+	}
+	return results
+}
+
+// syncDependentResource syncs a single child resource by iterating all parent IDs.
+func syncDependentResource(c interface {
+	Get(string, map[string]string) (json.RawMessage, error)
+	RateLimit() float64
+}, db *store.Store, dep dependentResourceDef, sinceTS string, full bool, maxPages int) syncResult {
+	started := time.Now()
+
+	// Query parent table for all IDs
+	parentIDs, err := db.ListIDs(dep.ParentTable)
+	if err != nil || len(parentIDs) == 0 {
+		if len(parentIDs) == 0 {
+			if humanFriendly {
+				fmt.Fprintf(os.Stderr, "  %s: skipping (parent table %s is empty, sync it first)\n", dep.Name, dep.ParentTable)
+			}
+			return syncResult{Resource: dep.Name, Duration: time.Since(started)}
+		}
+		return syncResult{Resource: dep.Name, Err: fmt.Errorf("querying parent table %s: %w", dep.ParentTable, err), Duration: time.Since(started)}
+	}
+
+	if humanFriendly {
+		fmt.Fprintf(os.Stderr, "  %s: syncing for %d %s parents\n", dep.Name, len(parentIDs), dep.ParentTable)
+	}
+
+	var totalCount int
+	var deniedParents int
+	var firstDenial *accessWarning
+	pageSize := determinePaginationDefaults()
+	// Per-resource extract-failure tracking for the F4b symptom probe and
+	// per-item primary_key_unresolved warning. See syncResource for the
+	// concurrency rationale (one goroutine per resource → no race).
+	var depExtractFailureTotal int
+	var depConsumedTotal int
+	depAnomalyEmitted := false
+
+	for idx, parentID := range parentIDs {
+		// Build child endpoint path by replacing the param placeholder
+		path := strings.Replace(dep.PathTemplate, "{"+dep.ParentIDParam+"}", parentID, 1)
+
+		if humanFriendly {
+			fmt.Fprintf(os.Stderr, "\r  %s: syncing for %s (%d/%d parents)", dep.Name, dep.ParentTable, idx+1, len(parentIDs))
+		}
+
+		cursor := ""
+		pagesFetched := 0
+		lastNextCursor := ""
+
+		for {
+			params := map[string]string{}
+			params[pageSize.limitParam] = strconv.Itoa(pageSize.limit)
+			if cursor != "" {
+				params[pageSize.cursorParam] = cursor
+			}
+			if sinceTS != "" {
+				params[determineSinceParam()] = sinceTS
+			}
+
+			data, err := c.Get(path, params)
+			if err != nil {
+				// Non-fatal per parent: log and continue to next parent.
+				// Track access-denial separately so an all-denied dependent
+				// resource can surface as a Warn rather than silent success.
+				if w, ok := isSyncAccessWarning(err); ok {
+					deniedParents++
+					if firstDenial == nil {
+						firstDenial = w
+					}
+					if humanFriendly {
+						fmt.Fprintf(os.Stderr, "\n  %s: access denied for parent %s: %s\n", dep.Name, parentID, w.Reason)
+					} else {
+						fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","parent":"%s","status":%d,"reason":"%s","message":"%s"}`+"\n",
+							dep.Name, parentID, w.Status, w.Reason, strings.ReplaceAll(w.Message, `"`, `\"`))
+					}
+				} else if humanFriendly {
+					fmt.Fprintf(os.Stderr, "\n  %s: error for parent %s: %v\n", dep.Name, parentID, err)
+				}
+				break
+			}
+
+			items, nextCursor, hasMore := extractPageItems(data, pageSize.cursorParam)
+			if len(items) == 0 {
+				break
+			}
+
+			// Inject parent_id into each item before upserting
+			for i, item := range items {
+				var obj map[string]json.RawMessage
+				if err := json.Unmarshal(item, &obj); err == nil {
+					parentIDJSON, _ := json.Marshal(parentID)
+					obj["parent_id"] = parentIDJSON
+					if modified, err := json.Marshal(obj); err == nil {
+						items[i] = modified
+					}
+				}
+			}
+
+			stored, extractFailures, err := upsertResourceBatch(db, dep.Name, items)
+			if err != nil {
+				if humanFriendly {
+					fmt.Fprintf(os.Stderr, "\n  %s: upsert error for parent %s: %v\n", dep.Name, parentID, err)
+				}
+				break
+			}
+
+			depConsumedTotal += len(items)
+			depExtractFailureTotal += extractFailures
+			// Order matches the flat path (syncResource): all-fail first,
+			// then partial-fail. The all-fail case dominates — if stored==0
+			// with at least one item, the resource is broken regardless of
+			// how many extractions failed individually.
+			if len(items) > 0 && stored == 0 && !depAnomalyEmitted {
+				if humanFriendly {
+					fmt.Fprintf(os.Stderr, "\nwarning: %s returned %d items for parent %s but stored 0 — likely scalar item shape or unrecognized primary-key field name.\n", dep.Name, len(items), parentID)
+				} else {
+					fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","parent":"%s","consumed":%d,"stored":0,"reason":"all_items_failed_id_extraction"}`+"\n", dep.Name, parentID, len(items))
+				}
+				depAnomalyEmitted = true
+			} else if extractFailures > 0 && stored > 0 && !depAnomalyEmitted {
+				if humanFriendly {
+					fmt.Fprintf(os.Stderr, "\nwarning: %s had %d item(s) with no extractable primary key — those rows were dropped silently. Annotate the spec with x-resource-id to fix.\n", dep.Name, extractFailures)
+				} else {
+					fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","parent":"%s","consumed":%d,"stored":%d,"count":%d,"reason":"primary_key_unresolved"}`+"\n", dep.Name, parentID, len(items), stored, extractFailures)
+				}
+				depAnomalyEmitted = true
+			}
+
+			totalCount += stored
+			pagesFetched++
+
+			if maxPages > 0 && pagesFetched >= maxPages {
+				if humanFriendly {
+					fmt.Fprintf(os.Stderr, "\n  %s: reached --max-pages limit (%d pages, %d items) for parent %s\n", dep.Name, maxPages, totalCount, parentID)
+				} else {
+					fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","parent":"%s","reason":"max_pages_cap_hit","message":"reached --max-pages cap of %d; data may be truncated. Re-run with --max-pages 0 (unlimited) or higher to verify."}`+"\n", dep.Name, parentID, maxPages)
+				}
+				break
+			}
+			// Sticky-cursor detector: see syncResource for rationale. Same shape
+			// here so dependent-resource page loops cannot burn the budget on a
+			// non-advancing next cursor.
+			if nextCursor != "" && nextCursor == lastNextCursor {
+				if humanFriendly {
+					fmt.Fprintf(os.Stderr, "\n  %s: API returned the same next cursor across two pages for parent %s; aborting to prevent budget waste.\n", dep.Name, parentID)
+				} else {
+					fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","parent":"%s","reason":"stuck_pagination","message":"API returned the same next cursor across two pages for resource %s; aborting to prevent budget waste."}`+"\n", dep.Name, parentID, dep.Name)
+				}
+				break
+			}
+			lastNextCursor = nextCursor
+			if !hasMore || len(items) < pageSize.limit || nextCursor == "" {
+				break
+			}
+			cursor = nextCursor
+		}
+
+		// Brief rate-limit pause between parents to avoid hammering the API
+		time.Sleep(100 * time.Millisecond)
+	}
+
+	if humanFriendly {
+		fmt.Fprintf(os.Stderr, "\n")
+	}
+
+	_ = db.SaveSyncState(dep.Name, "", totalCount)
+
+	// F4b symptom probe: items consumed and extracted but nothing landed.
+	// See syncResource for rationale.
+	if depConsumedTotal > 0 && totalCount == 0 && depExtractFailureTotal < depConsumedTotal {
+		if humanFriendly {
+			fmt.Fprintf(os.Stderr, "\nwarning: %s consumed %d items, extracted %d primary keys, but stored 0 rows — extraction succeeded yet nothing landed. Investigate FTS triggers / transaction rollback / encoding.\n", dep.Name, depConsumedTotal, depConsumedTotal-depExtractFailureTotal)
+		} else {
+			fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":0,"extract_failures":%d,"reason":"stored_count_zero_after_extraction"}`+"\n", dep.Name, depConsumedTotal, depExtractFailureTotal)
+		}
+	}
+
+	// If every parent was access-denied and nothing was synced, surface as a
+	// warning so the run-level summary and exit code reflect insufficient access.
+	if deniedParents == len(parentIDs) && totalCount == 0 && firstDenial != nil {
+		return syncResult{
+			Resource: dep.Name,
+			Count:    0,
+			Warn:     fmt.Errorf("skipped %s: %s on all %d parents", dep.Name, firstDenial.Reason, len(parentIDs)),
+			Duration: time.Since(started),
+		}
+	}
+	return syncResult{Resource: dep.Name, Count: totalCount, Duration: time.Since(started)}
+}
+
+// resourceIDFieldOverrides projects per-resource IDField (set by the profiler
+// from x-resource-id or the response-schema fallback chain) into a runtime
+// lookup map. extractID consults this first so the templated path wins over
+// the generic fallback list; the generic list applies only when the override
+// is empty or the override field is absent on a given item.
+//
+// Includes both flat resources and dependent (parent-child) resources so
+// annotations on a child path-item are honored at runtime, not just on
+// flat paths.
+var resourceIDFieldOverrides = map[string]string{
+	"projects": "id",
+	"tasks": "id",
+}
+
+// genericIDFieldFallbacks is the runtime safety net for resources that did
+// NOT receive a templated IDField. API-specific names belong in spec
+// annotations (x-resource-id), not this list.
+var genericIDFieldFallbacks = []string{"id", "ID", "name", "uuid", "slug", "key", "code", "uid"}
+
+// criticalResources is the template-time projection of per-resource Critical
+// (set by the profiler from the spec's path-item x-critical extension). It
+// is consulted at error-aggregation time so a non-critical failure can be
+// downgraded to a sync_warning + exit 0 unless --strict was passed.
+//
+// Includes both flat resources and dependent (parent-child) resources so a
+// failed child sync flagged x-critical: true exits non-zero just like a
+// flat-resource critical failure.
+var criticalResources = map[string]bool{
+	"projects": true,
+}
+
+// extractID resolves an item's primary-key field. It consults the
+// per-resource templated override first; on miss, it falls through to the
+// generic fallback list. resource may be empty for callers that don't have
+// a resource context (only the generic list applies in that case).
+//
+// Field lookups go through store.LookupFieldValue so snake_case overrides
+// match camelCase JSON renderings. UpsertBatch resolves fields the same
+// way — divergence between the two paths produces silent drops on
+// heterogeneous payloads.
+func extractID(resource string, obj map[string]any) string {
+	if override, ok := resourceIDFieldOverrides[resource]; ok && override != "" {
+		if v := store.LookupFieldValue(obj, override); v != nil {
+			s := fmt.Sprintf("%v", v)
+			if s != "" && s != "<nil>" {
+				return s
+			}
+		}
+	}
+	for _, key := range genericIDFieldFallbacks {
+		if v := store.LookupFieldValue(obj, key); v != nil {
+			s := fmt.Sprintf("%v", v)
+			if s != "" && s != "<nil>" {
+				return s
+			}
+		}
+	}
+	return ""
+}
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/which.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/which.go
new file mode 100644
index 00000000..5692afb3
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/which.go
@@ -0,0 +1,219 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package cli
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+
+	"github.com/spf13/cobra"
+)
+
+// whichEntry is one row of the curated capability index. The index is
+// seeded at generation time from the same NovelFeature list that drives
+// the SKILL.md feature section, so the command a `which` query returns
+// is guaranteed to exist and to match what the skill advertises.
+type whichEntry struct {
+	Command      string `json:"command"`
+	Description  string `json:"description"`
+	Group        string `json:"group,omitempty"`
+	WhyItMatters string `json:"why_it_matters,omitempty"`
+}
+
+// whichIndex is the curated list of capabilities this CLI advertises as
+// its hero features. Endpoint-level commands are discoverable via
+// `--help`; `which` exists to resolve a natural-language capability
+// query to one of the commands the skill says matter most.
+var whichIndex = []whichEntry{
+	{Command: "currencies list", Description: "List supported currencies", Group: "currencies"},
+	{Command: "projects create", Description: "Create project", Group: "projects"},
+	{Command: "projects get", Description: "Get project", Group: "projects"},
+	{Command: "projects list", Description: "List projects", Group: "projects"},
+	{Command: "projects tasks list-project", Description: "List project tasks", Group: "projects"},
+	{Command: "projects tasks update-project", Description: "Update project task", Group: "projects"},
+	{Command: "public get-status", Description: "Get public service status", Group: "public"},
+	{Command: "reports summary get-report-year", Description: "Get a report summary for a year", Group: "reports"},
+}
+
+// whichMatch pairs an index entry with its ranking score for a query.
+// Higher score means stronger match. The ranker is naive (exact token
+// then substring then group tag) because 20-40 entries do not need
+// semantic retrieval - a ranker upgrade is a future change that would
+// not break this contract.
+type whichMatch struct {
+	Entry whichEntry `json:"entry"`
+	Score int        `json:"score"`
+}
+
+// rankWhich returns up to `limit` best matches for `query` against the
+// index, sorted by descending score. Score breakdown:
+//
+//	+3  exact token match on the command's leaf or full path
+//	+2  substring match on the command (any part)
+//	+2  substring match on the description
+//	+1  group tag contains the query as a word
+//
+// Ties break on declaration order in the index. An empty query returns
+// every entry at score 0 in declaration order - this is the "list all"
+// behavior the skill documents for broad agent discovery.
+func rankWhich(index []whichEntry, query string, limit int) []whichMatch {
+	if limit <= 0 {
+		limit = 3
+	}
+	q := strings.ToLower(strings.TrimSpace(query))
+	if q == "" {
+		out := make([]whichMatch, 0, len(index))
+		for _, e := range index {
+			out = append(out, whichMatch{Entry: e, Score: 0})
+		}
+		return out
+	}
+	qTokens := strings.Fields(q)
+
+	scored := make([]whichMatch, 0, len(index))
+	for i, e := range index {
+		score := whichScoreEntry(e, q, qTokens)
+		scored = append(scored, whichMatch{Entry: e, Score: score})
+		_ = i
+	}
+
+	sort.SliceStable(scored, func(i, j int) bool {
+		return scored[i].Score > scored[j].Score
+	})
+	// Drop zero-score matches when the query was non-empty; agents
+	// branching on exit code rely on "no match" meaning no confidence.
+	filtered := scored[:0]
+	for _, m := range scored {
+		if m.Score > 0 {
+			filtered = append(filtered, m)
+		}
+	}
+	if len(filtered) > limit {
+		filtered = filtered[:limit]
+	}
+	return filtered
+}
+
+func whichScoreEntry(e whichEntry, query string, qTokens []string) int {
+	score := 0
+	cmd := strings.ToLower(e.Command)
+	cmdTokens := strings.Fields(cmd)
+	desc := strings.ToLower(e.Description)
+	group := strings.ToLower(e.Group)
+
+	// Exact token match on the command path (any token).
+	for _, qt := range qTokens {
+		for _, ct := range cmdTokens {
+			if qt == ct {
+				score += 3
+				break
+			}
+		}
+	}
+	// Substring match on the full command (covers hyphenated leaves).
+	if strings.Contains(cmd, query) {
+		score += 2
+	}
+	// Substring match on the description.
+	if strings.Contains(desc, query) {
+		score += 2
+	}
+	// Group tag match.
+	if group != "" {
+		for _, qt := range qTokens {
+			if strings.Contains(group, qt) {
+				score += 1
+				break
+			}
+		}
+	}
+	return score
+}
+
+func newWhichCmd(flags *rootFlags) *cobra.Command {
+	var limit int
+	cmd := &cobra.Command{
+		Use:   "which [query]",
+		Short: "Find the command that implements a capability",
+		Annotations: map[string]string{
+			"pp:typed-exit-codes": "0,2",
+		},
+		Long: `which resolves a natural-language capability query (for example, "search messages" or "stale tickets") to the best matching command from this CLI's curated feature index.
+
+Exit codes:
+  0  at least one match found
+  2  no confident match - the query did not score against any indexed capability; fall back to '--help' or 'search' if this CLI has one`,
+		Example: `  printing-press-golden-pp-cli which "stale tickets"
+  printing-press-golden-pp-cli which "bottleneck"
+  printing-press-golden-pp-cli which --limit 1 "send message"
+  printing-press-golden-pp-cli which                                # list the full capability index`,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			if len(whichIndex) == 0 {
+				return usageErr(fmt.Errorf("this CLI has no curated capability index; run '--help' to see every command"))
+			}
+			query := strings.Join(args, " ")
+			matches := rankWhich(whichIndex, query, limit)
+
+			// Empty query returns the whole index at score 0 (listing mode).
+			if strings.TrimSpace(query) == "" {
+				return renderWhich(cmd, flags, rankWhichAll(whichIndex))
+			}
+
+			if len(matches) == 0 {
+				// Under --json, return an empty matches envelope at exit 0
+				// so agents can branch on `matches.length == 0` instead of
+				// parsing a usage error message. Non-JSON keeps the typed
+				// exit-2 path so terminal users see the help hint.
+				if flags.asJSON {
+					return printJSONFiltered(cmd.OutOrStdout(), map[string]any{
+						"matches": []whichMatch{},
+					}, flags)
+				}
+				return usageErr(fmt.Errorf("no match for %q; try '%s --help' for the full command list", query, cmd.Root().Name()))
+			}
+			return renderWhich(cmd, flags, matches)
+		},
+	}
+	cmd.Flags().IntVar(&limit, "limit", 3, "Maximum number of matches to return")
+	return cmd
+}
+
+// rankWhichAll is a narrow helper used by the "empty query lists the
+// index" path. It returns every entry in declaration order at score 0
+// so the render path treats them uniformly.
+func rankWhichAll(index []whichEntry) []whichMatch {
+	out := make([]whichMatch, 0, len(index))
+	for _, e := range index {
+		out = append(out, whichMatch{Entry: e, Score: 0})
+	}
+	return out
+}
+
+func renderWhich(cmd *cobra.Command, flags *rootFlags, matches []whichMatch) error {
+	w := cmd.OutOrStdout()
+	// Output shape follows the same rule as every other generated
+	// command: JSON when the caller asked for it OR when stdout is not
+	// a terminal; table when a human is looking.
+	asJSON := flags.asJSON
+	if !asJSON && !isTerminal(w) {
+		asJSON = true
+	}
+	if asJSON {
+		// JSON envelope: {matches: [...]}. The wrap is critical:
+		// printJSONFiltered's --compact path uses compactListFields
+		// (allowlist) for top-level arrays, which would strip
+		// entry/score keys; routing through compactObjectFields
+		// (blocklist) via an object envelope preserves them.
+		if matches == nil {
+			matches = []whichMatch{}
+		}
+		return printJSONFiltered(w, map[string]any{"matches": matches}, flags)
+	}
+	fmt.Fprintf(w, "%-24s  %-8s  %s\n", "COMMAND", "SCORE", "DESCRIPTION")
+	for _, m := range matches {
+		fmt.Fprintf(w, "%-24s  %-8d  %s\n", m.Entry.Command, m.Score, m.Entry.Description)
+	}
+	return nil
+}
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/doctor.go b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/doctor.go
index 0977580d..cf2052e6 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/doctor.go
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/doctor.go
@@ -68,6 +68,9 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "doctor",
 		Short: "Check CLI health",
+		Example: `  tier-routing-golden-pp-cli doctor
+  tier-routing-golden-pp-cli doctor --json
+  tier-routing-golden-pp-cli doctor --fail-on warn`,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			report := map[string]any{}
 
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/sync.go b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/sync.go
index 08975ba3..1c283e9a 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/sync.go
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/sync.go
@@ -200,17 +200,18 @@ Exit codes & warnings:
 
 			elapsed := time.Since(started)
 			totalResources := successCount + warnCount + errCount
-			if !humanFriendly {
+			if humanFriendly {
+				if warnCount > 0 {
+					fmt.Fprintf(os.Stderr, "Sync complete: %d records across %d resources (%d warned, %.1fs)\n",
+						totalSynced, totalResources, warnCount, elapsed.Seconds())
+				} else {
+					fmt.Fprintf(os.Stderr, "Sync complete: %d records across %d resources (%.1fs)\n",
+						totalSynced, totalResources, elapsed.Seconds())
+				}
+			} else {
 				fmt.Fprintf(os.Stderr, `{"event":"sync_summary","total_records":%d,"resources":%d,"success":%d,"warned":%d,"errored":%d,"duration_ms":%d}`+"\n",
 					totalSynced, totalResources, successCount, warnCount, errCount, elapsed.Milliseconds())
 			}
-			if warnCount > 0 {
-				fmt.Fprintf(os.Stderr, "Sync complete: %d records across %d resources (%d warned, %.1fs)\n",
-					totalSynced, totalResources, warnCount, elapsed.Seconds())
-			} else {
-				fmt.Fprintf(os.Stderr, "Sync complete: %d records across %d resources (%.1fs)\n",
-					totalSynced, totalResources, elapsed.Seconds())
-			}
 
 			// Exit-code policy:
 			//   1. --strict + any error  -> non-zero (legacy contract)

← 708a6159 fix(skills): speed up retro issue filing (#575)  ·  back to Cli Printing Press  ·  fix(cli): live-dogfood matrix accuracy (WU-2, F4+F5) (#577) 7572b1e0 →