[object Object]

← back to Cli Printing Press

fix(cli): machine context compensation — scorer, generator, skill improvements (#104)

55c5525be3d8312a4b71da447760925330d032ca · 2026-04-01 11:09:54 -0700 · Trevin Chow

* fix(cli): scorer behavioral detection — path validity, insight/workflow, dogfood false positives (#101)

- Gate usageErr behind HasMultiPositional flag (dead when no 2+ arg commands)
- Sync correctness: rescale when API has no parameterized list endpoints
  so flat APIs like Steam aren't penalized for missing hierarchical resources
- Profiler: collect searchable fields from GET params (not just POST body)
  to enable FTS5 for APIs where entities are queried, not posted

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

* fix(skills): add research-context compensation instructions

Skill now instructs Claude to:
- Preserve 5 standard README sections when rewriting (Quick Start,
  Agent Usage, Health Check, Troubleshooting, Cookbook)
- Wire auth from research brief when spec detection fails
- Enrich terse flag descriptions from research context during polish

Includes Steam Run 4 retro and machine context compensation plan.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 55c5525be3d8312a4b71da447760925330d032ca
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Wed Apr 1 11:09:54 2026 -0700

    fix(cli): machine context compensation — scorer, generator, skill improvements (#104)
    
    * fix(cli): scorer behavioral detection — path validity, insight/workflow, dogfood false positives (#101)
    
    - Gate usageErr behind HasMultiPositional flag (dead when no 2+ arg commands)
    - Sync correctness: rescale when API has no parameterized list endpoints
      so flat APIs like Steam aren't penalized for missing hierarchical resources
    - Profiler: collect searchable fields from GET params (not just POST body)
      to enable FTS5 for APIs where entities are queried, not posted
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    
    * fix(skills): add research-context compensation instructions
    
    Skill now instructs Claude to:
    - Preserve 5 standard README sections when rewriting (Quick Start,
      Agent Usage, Health Check, Troubleshooting, Cookbook)
    - Wire auth from research brief when spec detection fails
    - Enrich terse flag descriptions from research context during polish
    
    Includes Steam Run 4 retro and machine context compensation plan.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
 ...01-003-fix-machine-context-compensation-plan.md | 258 +++++++++++++++++++++
 docs/retros/2026-04-01-steam-run4-retro.md         | 152 ++++++++++++
 internal/generator/generator.go                    |  15 +-
 internal/generator/templates/helpers.go.tmpl       |   2 +
 internal/pipeline/scorecard.go                     |  23 +-
 internal/profiler/profiler.go                      |  10 +-
 skills/printing-press/SKILL.md                     |  15 ++
 7 files changed, 466 insertions(+), 9 deletions(-)

diff --git a/docs/plans/2026-04-01-003-fix-machine-context-compensation-plan.md b/docs/plans/2026-04-01-003-fix-machine-context-compensation-plan.md
new file mode 100644
index 00000000..2a49b638
--- /dev/null
+++ b/docs/plans/2026-04-01-003-fix-machine-context-compensation-plan.md
@@ -0,0 +1,258 @@
+---
+title: "fix: Machine context compensation — use research intelligence beyond the spec"
+type: fix
+status: active
+date: 2026-04-01
+origin: docs/retros/2026-04-01-steam-run4-retro.md
+---
+
+# Fix: Machine Context Compensation
+
+## Overview
+
+The machine leaves 9 recoverable scorecard points on the table because it only uses the spec for decisions that research context could inform better. When the spec says `steamid` is an integer with description "access key", the machine trusts that — but the research brief already says "API key required (free at steamcommunity.com/dev/apikey)" and competing tools all use `STEAM_API_KEY`. This plan makes the machine use its full context — research brief, ecosystem scan, absorb manifest — to compensate for spec gaps.
+
+## Problem Frame
+
+The scorecard measures CLI quality, not spec compliance. A CLI generated from an incomplete spec should still be good — the machine has research intelligence to fill the gaps. Currently, the generator uses only the spec. The skill (Claude's orchestration) has the research context but doesn't systematically use it to fix generator output. This plan bridges that gap.
+
+(see origin: docs/retros/2026-04-01-steam-run4-retro.md — Key Insight section)
+
+## Requirements Trace
+
+- R1. `usageErr` only emitted when commands actually call it (generator template gate)
+- R2. README always includes 5 scored sections; skill prevents Claude from dropping them; extra sections allowed
+- R3. Profiler identifies searchable fields from response schemas, not just request body params
+- R4. Skill instructs Claude to wire auth from research context when spec detection fails
+- R5. Skill instructs Claude to enrich terse flag descriptions from research brief
+- R6. Terminal UX scorer detects uninformative boilerplate instead of counting words
+- R7. Sync correctness scorer adapts bonus when API has no parameterized list endpoints
+
+## Scope Boundaries
+
+- Not changing the auth inference 30% threshold (it's correct for spec-based detection)
+- Not changing the scorecard's overall scoring architecture — only specific heuristics in terminal_ux and sync_correctness
+- Not building domain-specific Search methods in the store template (that's a separate generator change — the profiler fix here enables it downstream)
+
+## Key Technical Decisions
+
+- **Skill instructions over generator changes for research-context items (#4, #5):** Auth wiring and description enrichment depend on research context that only exists during the skill-orchestrated build phase. The generator doesn't have access to the research brief. The right layer is the skill instruction, not the generator template.
+
+- **README: mandatory sections + flexible extras:** The template emits all 5 scored sections. The skill tells Claude to preserve them but allows adding API-specific sections. The scorer checks for presence of required sections and doesn't penalize extras.
+
+- **Terminal UX: boilerplate detection over word counting:** Replace the 5-word-average threshold with pattern detection for uninformative descriptions. "Check CLI health" (3 words, informative) should pass. "GetPlayerSummaries operation of ISteamUser" (6 words, boilerplate) should fail.
+
+- **Sync correctness: conditional bonus:** The +3 path-params bonus should only apply when the spec has parameterized list endpoints. For flat APIs like Steam, the bonus is N/A and the dimension max becomes 7/7 (rescaled to 10).
+
+## Implementation Units
+
+- [ ] **Unit 1: Gate usageErr emission in helpers template**
+
+**Goal:** `usageErr` only emitted when the spec has endpoints that use it (gated on `HasPositionalArgs`)
+
+**Requirements:** R1
+
+**Dependencies:** None
+
+**Files:**
+- Modify: `internal/generator/templates/helpers.go.tmpl`
+- Modify: `internal/generator/generator.go` (add `HasUsageErr` to `HelperFlags` if needed — or reuse existing `HasPathParams`)
+
+**Approach:**
+- `usageErr` was previously called by commands with `Args: cobra.ExactArgs(N)`. PR #102 replaced those with `cmd.Help()` help-guards. Now `usageErr` is dead in every generated CLI. Either: (a) gate behind a flag that's never true (effectively removing it), or (b) just remove it from the template since no generated command calls it anymore.
+- Simplest: remove `usageErr` from the template entirely. If a future template change reintroduces calls to it, the build will fail and we'll add it back then.
+
+**Test scenarios:**
+- Happy path: Generate from any spec → helpers.go does NOT contain `usageErr`
+- Negative: Build still succeeds (no missing reference)
+
+**Verification:**
+- Dogfood reports 0 dead functions for `usageErr`
+
+---
+
+- [ ] **Unit 2: Harden README template with required sections**
+
+**Goal:** README template always emits 5 scored sections; skill instruction preserves them
+
+**Requirements:** R2
+
+**Dependencies:** None
+
+**Files:**
+- Modify: `internal/generator/templates/readme.md.tmpl`
+- Modify: `skills/printing-press/SKILL.md` (Phase 3 instruction)
+
+**Approach:**
+- Verify the README template already has all 5 sections: Quick Start, Agent Usage, Doctor/Health Check, Troubleshooting, Cookbook. Based on PR #102, it should — but confirm each is present with real content.
+- Add a skill instruction in Phase 3 (after generation, during build): "The generated README contains 5 standard sections (Quick Start, Agent Usage, Health Check, Troubleshooting, Cookbook). When rewriting the README for this API, preserve all 5 sections. You may add additional sections that help users of this specific API, but never remove the standard ones."
+- The scorer's README check should: (a) require the 5 sections, (b) not penalize extra sections. Check if the current scorer already works this way.
+
+**Test scenarios:**
+- Happy path: Generate from any spec → README has all 5 scored sections
+- Happy path: Claude rewrites README during build → all 5 sections preserved
+- Edge case: Claude adds "Rate Limits" section → no penalty from scorer
+
+**Verification:**
+- Scorecard README score ≥9/10 consistently across runs
+
+---
+
+- [ ] **Unit 3: Profiler analyzes response schemas for searchable fields**
+
+**Goal:** Profiler identifies searchable string fields from GET response schemas, not just POST request bodies
+
+**Requirements:** R3
+
+**Dependencies:** None
+
+**Files:**
+- Modify: `internal/profiler/profiler.go` (`collectStringFields` or the searchable field logic)
+- Test: `internal/profiler/profiler_test.go`
+
+**Approach:**
+- Currently `collectStringFields` only examines `endpoint.Body` params (request body). GET endpoints don't have bodies — their entities are in response schemas.
+- The spec's `Endpoint.Response` field (a `ResponseDef`) may contain schema information. Check what `ResponseDef` provides.
+- If response schemas have field names, add those to `SearchableFields` alongside body fields.
+- If `ResponseDef` doesn't carry field info, this may need the OpenAPI parser to extract response field names during parsing. Defer that complexity to implementation.
+
+**Test scenarios:**
+- Happy path: Discord spec with messages resource (GET returns `content` field) → `SearchableFields["messages"]` includes "content"
+- Happy path: Steam spec → profiler identifies string fields from response shapes
+- Edge case: Endpoint with no response schema → no crash, no searchable fields added
+
+**Verification:**
+- Profile Steam spec → `SearchableFields` is non-empty
+- Generated store has FTS5-enabled Search methods
+
+---
+
+- [ ] **Unit 4: Skill instruction — wire auth from research context**
+
+**Goal:** When spec-based auth detection fails, Claude wires auth from the research brief during Phase 3
+
+**Requirements:** R4
+
+**Dependencies:** None
+
+**Files:**
+- Modify: `skills/printing-press/SKILL.md` (Phase 3 — after generation, during build)
+
+**Approach:**
+- Add instruction after the "REQUIRED: Rewrite the CLI description" block in Phase 2 (post-generation):
+  - "Check if the generated config.go has auth env var support. If not, check the research brief for auth requirements. If the brief identifies an API key, token, or auth method, add the appropriate env var support to config.go (e.g., `STEAM_API_KEY` for Steam, `DISCORD_TOKEN` for Discord). Use the pattern from existing generated CLIs."
+- This makes Claude responsible for compensating when the spec-based parser misses auth. Claude has the research brief in context and knows the auth pattern from the absorb manifest.
+
+**Test scenarios:**
+- Integration: Generate from Steam spec (no securitySchemes, auth inference misses by 0.3%) → Claude adds STEAM_API_KEY to config.go from research brief
+- Negative: Generate from Stripe spec (auth correctly detected by parser) → Claude doesn't duplicate auth setup
+
+**Test expectation: Skill instruction change — tested via full generation run, not unit test.**
+
+**Verification:**
+- Run `/printing-press steamapi` → config.go has STEAM_API_KEY without manual intervention
+
+---
+
+- [ ] **Unit 5: Skill instruction — enrich terse flag descriptions**
+
+**Goal:** Claude enriches unhelpful flag descriptions from research context during Phase 3
+
+**Requirements:** R5
+
+**Dependencies:** None
+
+**Files:**
+- Modify: `skills/printing-press/SKILL.md` (Phase 3 — during build)
+
+**Approach:**
+- Add instruction in Phase 3 (Priority 3 — polish): "Review generated command flag descriptions. If any are under 5 words or are generic spec-derived text (e.g., 'access key', 'The player'), improve them using the research brief. For example, change 'access key' to 'Steam API key (get one at steamcommunity.com/dev/apikey)'. Focus on the flags users interact with most: auth keys, IDs, and filter parameters."
+- This is a lightweight polish step, not a full rewrite. Claude should touch only terse/unhelpful descriptions.
+
+**Test expectation: Skill instruction change — tested via full generation run.**
+
+**Verification:**
+- Generated commands have flag descriptions >5 words for key params
+
+---
+
+- [ ] **Unit 6: Scorer — terminal UX boilerplate detection**
+
+**Goal:** Terminal UX scorer detects uninformative boilerplate instead of penalizing short-but-clear descriptions
+
+**Requirements:** R6
+
+**Dependencies:** None
+
+**Files:**
+- Modify: `internal/pipeline/scorecard.go` (`scoreTerminalUX`, specifically the description quality check)
+- Test: `internal/pipeline/scorecard_test.go` or `scorecard_artifacts_test.go`
+
+**Approach:**
+- Replace the 5-word-average threshold with a boilerplate detection check. A description fails if it matches uninformative patterns:
+  - Contains "operation of" (e.g., "GetPlayerSummaries operation of ISteamUser")
+  - Starts with "Manage " followed by a raw interface/resource name with no verb describing what it manages
+  - Is a raw camelCase operationId without humanization
+- A description passes if it describes what the command DOES, regardless of length. "Check CLI health" (3 words) passes. "List friends" (2 words) passes.
+- Keep the >10 chars + contains-a-space minimum as a baseline sanity check.
+
+**Test scenarios:**
+- Happy path: "Check CLI health" → passes (short but informative)
+- Happy path: "List games owned by a Steam player, sorted by playtime" → passes
+- Fail: "GetPlayerSummaries operation of ISteamUser" → fails (boilerplate)
+- Fail: "Manage isteam cdn" → fails (raw interface name, no useful info)
+- Edge case: "" (empty) → fails
+
+**Verification:**
+- Scorecard terminal_ux = 10/10 for CLIs with clear descriptions regardless of length
+
+---
+
+- [ ] **Unit 7: Scorer — sync correctness conditional path-params bonus**
+
+**Goal:** Sync correctness +3 path-params bonus only applies when the API has parameterized list endpoints
+
+**Requirements:** R7
+
+**Dependencies:** None
+
+**Files:**
+- Modify: `internal/pipeline/scorecard.go` (`scoreSyncCorrectness`)
+
+**Approach:**
+- Before awarding the +3 bonus for `/{` patterns, check whether the spec has any parameterized list endpoints (paths containing `{` that also have pagination). If the spec has no such endpoints, the bonus is N/A — don't penalize.
+- When the bonus doesn't apply, rescale the dimension: max becomes 7 instead of 10, and the final score maps 7/7 → 10/10.
+- This ensures flat APIs like Steam aren't penalized for not having hierarchical resources.
+
+**Test scenarios:**
+- Happy path: Steam spec (no parameterized list endpoints) → sync_correctness bonus is N/A, score rescales to 10/10 from 7/7
+- Happy path: Discord spec (has `/guilds/{guild_id}/channels`) → bonus applies normally
+- Edge case: Spec with 0 syncable resources → sync_correctness is 0 (no bonus either way)
+
+**Verification:**
+- Scorecard sync_correctness = 10/10 for Steam CLI
+
+## System-Wide Impact
+
+- **Generator template (Unit 1):** Removes one function from generated CLIs. No runtime impact.
+- **Skill instructions (Units 2, 4, 5):** Change Claude's behavior during generation. No binary or template changes.
+- **Profiler (Unit 3):** Changes what fields the profiler identifies as searchable. Affects downstream store generation. No breaking changes — additive only.
+- **Scorecard (Units 6, 7):** Changes how terminal_ux and sync_correctness are scored. Existing CLIs may score differently — likely higher since the changes remove false penalties.
+
+## Risks & Dependencies
+
+| Risk | Mitigation |
+|------|------------|
+| Boilerplate detection false positives (Unit 6) | Test with both informative and uninformative descriptions across multiple specs |
+| Sync rescaling changes scores for existing CLIs | Only affects CLIs without parameterized list endpoints — same as current false penalty but in the correct direction |
+| Skill instruction compliance varies by run | README section preservation is verifiable post-generation; auth wiring is checked by scorecard |
+| Profiler response schema analysis adds complexity | Defer to implementation if ResponseDef doesn't carry field names |
+
+## Sources & References
+
+- **Origin:** [docs/retros/2026-04-01-steam-run4-retro.md](docs/retros/2026-04-01-steam-run4-retro.md)
+- Prior PRs: #100, #101, #102, #103
+- Scorecard: `internal/pipeline/scorecard.go`
+- Generator templates: `internal/generator/templates/`
+- Profiler: `internal/profiler/profiler.go`
+- Skill: `skills/printing-press/SKILL.md`
diff --git a/docs/retros/2026-04-01-steam-run4-retro.md b/docs/retros/2026-04-01-steam-run4-retro.md
new file mode 100644
index 00000000..e6457396
--- /dev/null
+++ b/docs/retros/2026-04-01-steam-run4-retro.md
@@ -0,0 +1,152 @@
+# Printing Press Retro: Steam Web API (Run 4)
+
+## Session Stats
+- API: Steam Web API
+- Spec source: Zuplo/Steam-OpenAPI (158 operations, OpenAPI 3.0)
+- Scorecard: **84/100 Grade A** (stable from Run 3's 85)
+- Verify pass rate: 77% (62/81, 0 critical)
+- Machine PRs applied: #100, #101, #102, #103
+- Journey: 68 → 70 → 84 → 85 → 84
+
+## Core Question: Scorer accuracy vs API limitations
+
+For each remaining deduction, the question is: **does the scoring reflect something the CLI could realistically do better given Steam's API, or is it penalizing the CLI for Steam's limitations?**
+
+## Findings
+
+### 1. Auth 8/10 — inference threshold off by 1 operation (Scorer design edge case)
+
+- **Scorer correct?** The scoring itself is correct — the CLI genuinely doesn't have auth wired in the generated config. But the auth inference (PR #103) didn't fire because Steam has `key` on 47/158 operations = 29.7%, just under the 30% threshold. One more operation would trigger it.
+- **Is this a real CLI gap?** No — the CLI works fine with `STEAM_API_KEY` env var (we added it manually). The generated config just doesn't auto-detect it.
+- **Is the threshold right?** 30% is reasonable for avoiding false positives. Steam being at 29.7% is bad luck, not a design flaw. Lowering to 25% would catch Steam but might false-positive on APIs where only a few optional endpoints accept keys.
+- **Recommendation:** Don't change the 30% threshold — it's correct for spec-based detection. But the machine has a second path: the research brief explicitly says "API key required (free at steamcommunity.com/dev/apikey)" and the absorb manifest shows every competing tool uses `STEAM_API_KEY`. The skill should tell Claude: "If research identified auth requirements that the spec didn't declare, wire them into config.go during Phase 3." This uses context the machine already has.
+- **Verdict: Fixable via skill instruction.** The spec-based inference is borderline, but the research-based context is unambiguous. +2 points recoverable.
+
+### 2. Data pipeline 7/10 — no domain-specific Search methods (Generator gap — scorer is correct)
+
+- **Scorer correct?** Yes. The store has generic `Search()` but no `SearchPlayers()`, `SearchGames()`, etc. The scorer gives +3 for domain-specific Search.
+- **Why isn't the generator emitting them?** The store template gates Search on `{{if .FTS5}}` — but the Steam spec's tables aren't getting the FTS5 flag from the profiler. The profiler's `collectStringFields()` looks for string-type body params, but Steam's entities (games, players) are returned in GET responses, not POST bodies. The profiler doesn't analyze response schemas for searchable fields.
+- **Is this a real CLI gap?** Yes — domain-specific Search would make the CLI genuinely better. The Steam CLI already has a generic `search` command, but `SearchPlayers("gabe")` would be more useful.
+- **Recommendation: Fix the profiler.** `collectStringFields` should also analyze response schemas (not just request body params) to find searchable fields. This is a real generator improvement.
+- **Verdict: Real gap, fixable. +3 points recoverable.**
+
+### 3. Sync correctness 7/10 — no path params in list endpoints (API limitation — scorer should adapt)
+
+- **Scorer correct?** Partially. The +3 bonus for `/{` path params rewards APIs like Discord (`/guilds/{guild_id}/channels`) where sync needs to iterate parent resources. Steam's list endpoints (`/ISteamApps/GetAppList/v2/`) genuinely don't have path params — there's no parent-child hierarchy to traverse.
+- **Is this a real CLI gap?** No. Steam's sync doesn't NEED path params because the list endpoints return all data without parameterization. The CLI isn't worse for lacking something the API doesn't require.
+- **Should the scorer penalize this?** This is a scorer design question. The +3 bonus assumes path-parameterized sync is universally better. For Steam, it's irrelevant. The scorer should either: (a) not award the bonus when the API has no parameterized list endpoints (don't penalize what doesn't apply), or (b) keep the bonus as-is and accept that non-hierarchical APIs score lower on this dimension.
+- **Recommendation: This is a scorer design issue worth discussing.** The scorer rewards a capability that not every API needs. It's not "wrong" — it's a design choice about what "complete sync" means. For now, accept the 3 points.
+- **Verdict: API limitation, not CLI gap. Scorer could adapt but it's a design tradeoff, not a bug.**
+
+### 4. README 7/10 — inconsistent section generation (Skill instruction gap)
+
+- **Scorer correct?** Yes — the README is missing content. Run 3 got 9/10 with a better README. The difference is Claude's README generation varies between runs.
+- **Should the machine harden this?** Yes, but carefully. The README template already has Cookbook, Agent Usage, Troubleshooting, Health Check sections. The issue is Claude's agent sometimes rewrites the README and drops sections, or the template's output doesn't fully satisfy the scorer's quality checks.
+- **Is mandating sections too inflexible?** No — the scorer checks for specific sections (Quick Start, Agent Usage, Doctor, Troubleshooting, Cookbook). These are genuinely useful for every CLI. The machine should ensure they're always present. But the CONTENT should vary by API — mandating sections is fine, mandating content is not.
+- **Recommendation:** Two improvements: (1) The README template should emit all 5 scored sections with real content. (2) The skill instruction for Phase 3 should say "preserve all README sections from the template — do not delete Agent Usage, Troubleshooting, or Cookbook when rewriting." This prevents Claude from accidentally dropping sections.
+- **Verdict: Skill instruction gap, fixable. +2 points recoverable by hardening the README template.**
+
+### 5. Terminal UX 9/10 — short descriptions on generated commands (Both — partially scorer, partially real)
+
+- **Scorer correct?** Partially. The scorer checks that sampled command descriptions are >10 chars AND contain a space (multi-word). Descriptions like "Manage isteam cdn" (17 chars) pass the length check. The issue is the scorer's quality heuristic: average word count >5 across sampled commands.
+- **Are short descriptions actually bad?** Not always. "Check CLI health" (16 chars, 3 words) is perfectly clear. "Manage isteam cdn" is fine for a generated subcommand. The scorer's 5-word average threshold penalizes APIs with many simple subcommands — which is what happens when the spec has 50+ resources with terse names.
+- **Should the scorer penalize this?** The 5-word average threshold is too blunt. A description should be penalized for being UNINFORMATIVE ("GetPlayerSummaries operation of ISteamUser") not for being SHORT ("Check VAC and game bans"). The quality signal should be "does the description tell the user what the command does?" not "is it long enough?"
+- **Recommendation:** This is a scorer design issue. The word-count heuristic is a proxy for quality but penalizes naturally concise descriptions. Better: detect boilerplate patterns ("operation of", "Manage <interface>") rather than counting words. For now, accept the 1 point — the descriptions are adequate.
+- **Verdict: Scorer design issue — word-count proxy penalizes concise commands. Not a CLI gap.**
+
+### 6. Dead code 4/5 — usageErr still emitted (Generator bug — scorer is correct)
+
+- **Scorer correct?** Yes. `usageErr` is defined but never called.
+- **Why is it still there?** PR #100's retro identified this and PR #102 made `replacePathParam` conditional. But `usageErr` was supposed to be gated on positional args too — PR #102's help-guard pattern replaced `usageErr` calls with `cmd.Help()`. The function is now dead because no command calls it, but the template still emits it unconditionally.
+- **Why can't the machine do this automatically?** It should. The generator template emits `usageErr` unconditionally — it should be gated behind a `HasPositionalArgs` flag (same pattern as `replacePathParam`). This is the same fix approach as PR #102's conditional emission, just for one more function.
+- **Why does polish need to catch it?** It shouldn't. This is a generator bug — the function should never be emitted if no command uses it. The dogfood tool catches it, but the generator should have prevented it.
+- **Recommendation: Fix the generator template.** Gate `usageErr` emission behind the same `HasPathParams` or `HasPositionalArgs` flag that gates `replacePathParam`. This is a 1-line template fix.
+- **Verdict: Generator bug, trivially fixable. +1 point recoverable.**
+
+### 7. Vision 9/10 and Insight 9/10 — within noise (Minor)
+
+- Both are 1 point from 10/10. The specific missing element varies by run — sometimes FTS5 detection, sometimes a wiring issue. Not worth investigating further for Steam. Would need to verify on a different API to know if it's systematic.
+- **Verdict: Accept. 2 points within run-to-run variation.**
+
+### 8. Type fidelity 3/5 — flag descriptions and required count (Spec-dependent)
+
+- **Scorer correct?** Yes. Short flag descriptions come from the spec's parameter descriptions. "access key" (2 words) is what the Zuplo spec provides.
+- **Is this a real CLI gap?** Yes — "access key" is unhelpful to a user. The research brief says "API key required (free at steamcommunity.com/dev/apikey)" — that's what the description SHOULD say. The machine has the context to write better descriptions; it's just not using it.
+- **Can the machine compensate?** Yes. During Phase 3, when Claude builds wrapper commands, it already writes rich descriptions from research context ("Check VAC and game bans for a Steam player"). The generated raw commands could be enriched the same way — either by Claude during build, or by a post-generation enrichment pass that maps terse spec descriptions to research-informed alternatives.
+- **Verdict: Fixable via skill instruction.** Tell Claude to enrich terse flag descriptions (under 5 words) from the research brief during Phase 3. +1-2 points recoverable.
+
+## Prioritized Improvements
+
+### Fix the Scorer
+| # | Scorer | Issue | Recommendation |
+|---|--------|-------|----------------|
+| 3 | Sync correctness | +3 bonus for path params penalizes APIs that don't need them | **Discuss design:** should the bonus scale down or not apply when spec has 0 parameterized list endpoints? Not clearly wrong — it's a design tradeoff. |
+| 5 | Terminal UX | 5-word avg description threshold penalizes concise commands | **Fix:** detect uninformative boilerplate ("operation of", "Manage <interface>") instead of counting words |
+
+### Do Now
+| # | Fix | Component | Impact | Complexity |
+|---|-----|-----------|--------|------------|
+| 6 | Gate `usageErr` emission behind HasPositionalArgs | `helpers.go.tmpl` | +1 point, every API | Trivial |
+| 4 | Harden README template to always emit scored sections | `readme.md.tmpl` + skill instruction | +2 points | Small |
+
+### Do Next
+| # | Fix | Component | Impact | Complexity |
+|---|-----|-----------|--------|------------|
+| 2 | Profiler: analyze response schemas for searchable fields | `profiler.go` | +3 points | Medium |
+
+### Do Next (cont.)
+| # | Fix | Component | Impact | Complexity |
+|---|-----|-----------|--------|------------|
+| 1 | Auth: wire from research context when spec detection fails | Skill instruction + Phase 3 | +2 points | Small |
+| 8 | Enrich terse flag descriptions from research brief | Skill instruction + Phase 3 | +1-2 points | Small |
+| 7 | Investigate vision/insight consistency across runs | Scorecard + generator | +1-2 points | Small |
+
+### Scorer Design Questions (not bugs — tradeoffs to discuss)
+| # | Dimension | Question |
+|---|-----------|----------|
+| 3 | Sync correctness | Should +3 path-params bonus apply when the API has no parameterized list endpoints? Currently penalizes flat APIs for not having hierarchical resources. |
+| 5 | Terminal UX | Should description quality check detect uninformative boilerplate instead of counting words? "Check CLI health" (3 words) is clear; "GetPlayerSummaries operation of ISteamUser" (5 words) is useless. |
+
+### Accept (truly API-limited)
+| # | Gap | Why accept |
+|---|-----|-----------|
+| 3 | Sync path params 3pts | Steam genuinely has no hierarchical list endpoints. The CLI can't add path params the API doesn't have. This is the one truly API-limited gap. |
+
+## Key Insight: The Scorecard Measures CLI Quality, Not Spec Compliance
+
+An earlier version of this retro framed 4 points as "spec quality limitations" and set a ceiling of ~90. **That framing was wrong.** The scorecard measures "how good is this CLI as a tool," not "how well does the CLI implement the spec." A CLI generated from a bad spec that perfectly implements that bad spec is still a bad CLI.
+
+The corrected breakdown:
+
+**Of the 16 lost points:**
+- **3 points truly API-limited** (sync path params — Steam doesn't have hierarchical resources, nothing to fix)
+- **9 points fixable by the machine** — the machine has context beyond the spec (research brief, ecosystem scan, absorb manifest) that it's not using:
+  - Auth +2: Research identified Steam needs API keys. The spec didn't declare it, but Claude knows. The skill should wire auth from research when spec detection fails.
+  - Data pipeline +3: Profiler should analyze response schemas, not just request params.
+  - README +2: Template should always emit scored sections; skill should prevent Claude from dropping them.
+  - Dead code +1: Generator should gate `usageErr` behind `HasPositionalArgs`.
+  - Type fidelity +1: Claude could enrich terse flag descriptions from the research brief during Phase 3.
+- **2 points from scorer design tradeoffs** (terminal UX word-count heuristic, sync bonus design)
+- **2 points from run-to-run variation** (vision, insight consistency)
+
+**The theoretical ceiling is ~97.** Only the 3 sync points are truly unrecoverable for this API. Everything else is the machine not using context it already has.
+
+The previous "ceiling of 90" was giving the machine too much credit and the spec too much blame. A bad spec is not an excuse — it's a signal that the machine needs to compensate using its other intelligence sources.
+
+## README Design Decision
+
+The README template should:
+1. **Always include** the 5 scored sections: Quick Start, Agent Usage, Doctor, Troubleshooting, Cookbook
+2. **Allow additional sections** when they're useful for the specific API (e.g., "Rate Limits" for APIs with documented limits, "Pagination" for APIs with complex paging)
+3. The skill should tell Claude: "You may add sections that help users of this specific API, but never remove the 5 standard sections."
+4. The scorer should: (a) verify required sections are present, (b) not penalize extra sections, (c) penalize filler/irrelevant sections if they add noise
+
+## Anti-patterns to Avoid
+
+- **Blaming the spec for machine failures.** When the spec is incomplete, the machine has research context, ecosystem data, and Claude's knowledge to compensate. "The spec didn't declare auth" is not an excuse when the research brief says "API key required (free at steamcommunity.com/dev/apikey)."
+- **Setting artificial ceilings.** A ceiling implies "we can't do better." But 9 of the 16 lost points ARE fixable. Calling them "spec limitations" creates a false sense of completion.
+- **Optimizing the auth threshold for one borderline API.** 30% is still the right threshold for spec-based detection. The fix is a different path — use research context as a fallback, not weaken the spec-based heuristic.
+
+## What the Machine Got Right
+
+**Stable Grade A across 2 runs (84-85).** The retro→plan→implement loop works. But the machine is leaving 9 recoverable points on the table by not using context it already has (research brief, ecosystem scan). The next improvement round should focus on making the machine use its full context — not just the spec — to compensate for spec gaps.
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index d8988387..4343aa15 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -124,8 +124,9 @@ func New(s *spec.APISpec, outputDir string) *Generator {
 
 // HelperFlags controls which helper functions are emitted in helpers.go.
 type HelperFlags struct {
-	HasDelete     bool // spec has DELETE endpoints → emit classifyDeleteError
-	HasPathParams bool // spec has path parameters → emit replacePathParam
+	HasDelete          bool // spec has DELETE endpoints → emit classifyDeleteError
+	HasPathParams      bool // spec has path parameters → emit replacePathParam
+	HasMultiPositional bool // spec has endpoints with 2+ positional params → emit usageErr
 }
 
 // computeHelperFlags scans the spec's resources to determine which helpers are needed.
@@ -136,22 +137,32 @@ func computeHelperFlags(s *spec.APISpec) HelperFlags {
 			if strings.EqualFold(e.Method, "DELETE") {
 				flags.HasDelete = true
 			}
+			positionalCount := 0
 			for _, p := range e.Params {
 				if p.Positional {
 					flags.HasPathParams = true
+					positionalCount++
 				}
 			}
+			if positionalCount >= 2 {
+				flags.HasMultiPositional = true
+			}
 		}
 		for _, sub := range r.SubResources {
 			for _, e := range sub.Endpoints {
 				if strings.EqualFold(e.Method, "DELETE") {
 					flags.HasDelete = true
 				}
+				positionalCount := 0
 				for _, p := range e.Params {
 					if p.Positional {
 						flags.HasPathParams = true
+						positionalCount++
 					}
 				}
+				if positionalCount >= 2 {
+					flags.HasMultiPositional = true
+				}
 			}
 		}
 	}
diff --git a/internal/generator/templates/helpers.go.tmpl b/internal/generator/templates/helpers.go.tmpl
index 6a774f2c..7cd009d3 100644
--- a/internal/generator/templates/helpers.go.tmpl
+++ b/internal/generator/templates/helpers.go.tmpl
@@ -91,7 +91,9 @@ type cliError struct {
 func (e *cliError) Error() string { return e.err.Error() }
 func (e *cliError) Unwrap() error { return e.err }
 
+{{- if .HasMultiPositional}}
 func usageErr(err error) error    { return &cliError{code: 2, err: err} }
+{{- end}}
 func notFoundErr(err error) error { return &cliError{code: 3, err: err} }
 func authErr(err error) error     { return &cliError{code: 4, err: err} }
 func apiErr(err error) error      { return &cliError{code: 5, err: err} }
diff --git a/internal/pipeline/scorecard.go b/internal/pipeline/scorecard.go
index be3141e7..b7cdebb4 100644
--- a/internal/pipeline/scorecard.go
+++ b/internal/pipeline/scorecard.go
@@ -1296,14 +1296,23 @@ func scoreSyncCorrectness(dir string) int {
 		score += 2
 	}
 	// URL path parameters only count when other sync signals are present,
-	// otherwise any CLI with parameterized routes gets free sync credit
-	if score > 0 && strings.Contains(content, "/{") {
+	// otherwise any CLI with parameterized routes gets free sync credit.
+	hasParamPaths := strings.Contains(content, "/{")
+	if score > 0 && hasParamPaths {
 		score += 3
 	}
-	if score > 10 {
-		score = 10
+	// When the API has no parameterized list endpoints, the path-params bonus
+	// is N/A. Rescale the max from 10 to 7 so flat APIs aren't penalized for
+	// not having hierarchical resources.
+	max := 10
+	if !hasParamPaths {
+		max = 7
 	}
-	return score
+	if score > max {
+		score = max
+	}
+	// Rescale to 0-10 range
+	return score * 10 / max
 }
 
 func scoreTypeFidelity(dir string) int {
@@ -1627,7 +1636,9 @@ func hasQualityDescription(content string) bool {
 		return false
 	}
 	desc := rest[q1+1 : q1+1+q2]
-	// Quality: must be > 10 chars and contain a space (multi-word)
+	// Minimum quality: multi-word and non-trivial length.
+	// Actual description quality (informative vs boilerplate) is handled by
+	// the skill instruction during Phase 3 polish, not by this scorer.
 	return len(desc) > 10 && strings.Contains(desc, " ")
 }
 
diff --git a/internal/profiler/profiler.go b/internal/profiler/profiler.go
index 346fbcef..bd46a300 100644
--- a/internal/profiler/profiler.go
+++ b/internal/profiler/profiler.go
@@ -184,7 +184,15 @@ func Profile(s *spec.APISpec) *APIProfile {
 				p.HasDependencies = true
 			}
 
-			for _, field := range collectStringFields(endpoint.Body) {
+			// Collect searchable string fields from both request body and query
+			// params. GET endpoints don't have bodies, but their query params
+			// often name the same fields that responses contain (e.g., "name",
+			// "query", "search"). This enables FTS5 indexing for those entities.
+			allFields := collectStringFields(endpoint.Body)
+			if endpoint.Method == "GET" || endpoint.Method == "" {
+				allFields = append(allFields, collectStringFields(endpoint.Params)...)
+			}
+			for _, field := range allFields {
 				if searchable[resourceName] == nil {
 					searchable[resourceName] = make(map[string]struct{})
 				}
diff --git a/skills/printing-press/SKILL.md b/skills/printing-press/SKILL.md
index cc77659b..c3da276d 100644
--- a/skills/printing-press/SKILL.md
+++ b/skills/printing-press/SKILL.md
@@ -1437,6 +1437,20 @@ via the Stripe API"). Open `$PRESS_LIBRARY/<api>-pp-cli/internal/cli/root.go`, f
 `Short:` field on the root cobra command, and rewrite it as a concise, user-facing description
 of the CLI's purpose. Use the product thesis from the Phase 1 brief to inform the rewrite.
 
+**REQUIRED: Preserve README sections.** The generated README contains 5 standard sections
+that the scorecard checks for: Quick Start, Agent Usage, Health Check, Troubleshooting, and
+Cookbook. When rewriting the README for this API during Phase 3, **preserve all 5 sections**.
+You may add additional sections that help users of this specific API (e.g., "Rate Limits",
+"Pagination", "Authentication Setup"), but never remove the standard ones.
+
+**REQUIRED: Compensate for missing auth.** Check if the generated `config.go` has auth
+env var support (look for `os.Getenv` calls for API key variables). If not, check the
+Phase 1 research brief for auth requirements. If the brief identifies an API key, token,
+or auth method that the spec didn't declare, add the appropriate env var support to
+`config.go`. Use the pattern: add `APIKey`/`APIKeySource` fields to the Config struct,
+and `os.Getenv("<API>_API_KEY")` in the Load function. The research brief is the
+authoritative source when the spec is silent on auth.
+
 Then:
 - note skipped complex body fields
 - fix only blocking generation failures here
@@ -1636,6 +1650,7 @@ Priority 3 (polish):
 - skipped complex request bodies that block important commands
 - naming cleanup for ugly operationId-derived commands
 - tests for non-trivial store/workflow logic
+- enrich terse flag descriptions: review generated command flags. If any description is under 5 words or is generic spec-derived text (e.g., "access key", "The player"), improve it using the research brief. For example, change "access key" to "Steam API key (get one at steamcommunity.com/dev/apikey)". Focus on auth keys, IDs, and filter parameters.
 
 ### Agent Build Checklist (per command)
 

← 6da6fd09 feat(cli): generator pipeline improvements — auth inference,  ·  back to Cli Printing Press  ·  feat(cli): add printing-press polish --remove-dead-code (#10 2c9d57d7 →