← back to Cli Printing Press
feat(cli): add tools-audit subcommand and merge polish into a single skill (#378)
e4a0089c9580e7aab2ac7c6cb6987625c0830d95 · 2026-04-29 00:17:16 -0700 · Trevin Chow
Adds `printing-press tools-audit <cli-dir>`, an AST-based audit that
walks a printed CLI's Cobra command literals and emits findings for
empty Short, suspiciously thin Short, and read-shaped command names
missing the mcp:read-only annotation. Output is a human-readable table
or JSON. The audit writes a per-CLI ledger at
<cli-dir>/.printing-press-tools-polish.json that persists agent-written
status/note fields across re-runs and computes resolved/added deltas
for resumability.
Restructures polish to drop the polish-worker agent indirection in
favor of a `context: fork` skill:
- Deletes `agents/polish-worker.md`.
- Moves the polish workflow (Phase 1 baseline, Phase 2 priorities 1-5
plus a new Priority 6 for tools-polish, Phase 3 re-diagnose, ship
logic) inline into `skills/printing-press-polish/SKILL.md`. Adds
`context: fork` to the polish skill's frontmatter so its diagnostic
loop runs in an isolated context — same isolation the agent gave us,
without the path-resolution headaches that come with cross-tree
references.
- Moves the tools-polish playbook to live next to its primary
consumer: `skills/printing-press-polish/references/tools-polish.md`.
- Updates main printing-press SKILL.md Phase 5.5 to invoke the polish
skill via the Skill tool instead of dispatching the agent.
Captures the broader "Deterministic Inventory + Agent-Marked Ledger"
design pattern in AGENTS.md so future tools needing the same
detect-mechanically + decide-per-item + persist-rationale shape can
reach for it.
The audit is diagnostic (exit 0 regardless of findings). It exempts
commands carrying pp:endpoint (covered by typed-tool registration) and
verbs already in cobratree's frameworkCommands skip set (search, sql,
doctor, version) to avoid noise. The ledger ages out at 24h.
Files touched
M AGENTS.mdD agents/polish-worker.mdM internal/cli/root.goA internal/cli/tools_audit.goM skills/printing-press-polish/SKILL.mdA skills/printing-press-polish/references/tools-polish.mdM skills/printing-press/SKILL.md
Diff
commit e4a0089c9580e7aab2ac7c6cb6987625c0830d95
Author: Trevin Chow <trevin@trevinchow.com>
Date: Wed Apr 29 00:17:16 2026 -0700
feat(cli): add tools-audit subcommand and merge polish into a single skill (#378)
Adds `printing-press tools-audit <cli-dir>`, an AST-based audit that
walks a printed CLI's Cobra command literals and emits findings for
empty Short, suspiciously thin Short, and read-shaped command names
missing the mcp:read-only annotation. Output is a human-readable table
or JSON. The audit writes a per-CLI ledger at
<cli-dir>/.printing-press-tools-polish.json that persists agent-written
status/note fields across re-runs and computes resolved/added deltas
for resumability.
Restructures polish to drop the polish-worker agent indirection in
favor of a `context: fork` skill:
- Deletes `agents/polish-worker.md`.
- Moves the polish workflow (Phase 1 baseline, Phase 2 priorities 1-5
plus a new Priority 6 for tools-polish, Phase 3 re-diagnose, ship
logic) inline into `skills/printing-press-polish/SKILL.md`. Adds
`context: fork` to the polish skill's frontmatter so its diagnostic
loop runs in an isolated context — same isolation the agent gave us,
without the path-resolution headaches that come with cross-tree
references.
- Moves the tools-polish playbook to live next to its primary
consumer: `skills/printing-press-polish/references/tools-polish.md`.
- Updates main printing-press SKILL.md Phase 5.5 to invoke the polish
skill via the Skill tool instead of dispatching the agent.
Captures the broader "Deterministic Inventory + Agent-Marked Ledger"
design pattern in AGENTS.md so future tools needing the same
detect-mechanically + decide-per-item + persist-rationale shape can
reach for it.
The audit is diagnostic (exit 0 regardless of findings). It exempts
commands carrying pp:endpoint (covered by typed-tool registration) and
verbs already in cobratree's frameworkCommands skip set (search, sql,
doctor, version) to avoid noise. The ledger ages out at 24h.
---
AGENTS.md | 18 +
agents/polish-worker.md | 284 --------------
internal/cli/root.go | 1 +
internal/cli/tools_audit.go | 413 +++++++++++++++++++++
skills/printing-press-polish/SKILL.md | 312 ++++++++++++++--
.../references/tools-polish.md | 191 ++++++++++
skills/printing-press/SKILL.md | 34 +-
7 files changed, 924 insertions(+), 329 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 2cbe3d9f..10701850 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -306,3 +306,21 @@ Skills use a `references/` directory for content that is only needed during spec
**What stays inline:** Cardinal rules, decision matrices, phase structure, user-facing prompts — anything the agent needs at all times or to decide whether to load more.
**What gets extracted:** Implementation details for conditional paths: capture tool CLI commands, delegation templates, scoring frameworks, report templates. These are loaded on-demand when the agent reaches the relevant phase gate.
+
+## Deterministic Inventory + Agent-Marked Ledger
+
+When a workflow has a checklist where detection is mechanical but each item needs per-item judgment, split the work between a binary-emitted inventory and an agent-maintained ledger. The binary owns "what's there"; the agent owns "what to do about each item." A persistent file holds both, so the work survives context flushes and the audit trail surfaces the agent's reasoning.
+
+The canonical example is `printing-press tools-audit` + `skills/printing-press/references/tools-polish.md`. The binary parses every Cobra command and emits findings (empty Short, thin Short, missing read-only annotation). The agent walks each finding, fixes most, and marks the rest `accepted` with a one-sentence rationale. The ledger persists at `<cli-dir>/.printing-press-tools-polish.json` for 24 hours.
+
+Reach for this pattern when the work has the **detect mechanically + decide per-item + persist rationale** shape. The trigger isn't a numeric item count — a 15-item list with three accept decisions across two sessions benefits, while a 200-item batch update where every item has the same fix does not. Skip it when one pass is enough, when every item has the same fix, when detection itself requires judgment, or when a `TodoWrite` task list with rationale in the description carries the whole workflow.
+
+**Structure:**
+
+1. **Binary writes the inventory.** A subcommand emits a structured snapshot file (`.<topic>-ledger.json` or similar) on every run. Each entry has stable identity fields (file, line, kind, key) and may carry agent-written `status` and `note` fields (`omitempty` so the bare audit output stays clean).
+2. **Agent annotates the ledger.** When the agent decides to keep an item as-is, it edits the ledger to set `status: "accepted"` and writes a `note`. Code fixes are *not* marked manually — the next run re-detects and the finding disappears automatically.
+3. **Re-runs preserve agent state.** The binary reads the previous ledger before writing the new one. Findings whose identity key matches inherit `status` and `note`. Findings present last run but absent now read as "resolved" in the delta line. New findings start fresh as pending.
+4. **Staleness, not history.** Ledgers age out (e.g., 24h) and are deleted. They're working state, not artifacts to preserve in version control. Add the ledger filename to the relevant repo's `.gitignore` if the cli-dir lives inside one.
+5. **Verification asks for zero pending, not zero findings.** "Done" means every finding is either fixed (auto-removed) or explicitly accepted with a note — not that the count is zero. Reviewers can see accepts in the ledger and judge whether each rationale holds.
+
+Compared to the alternatives: pure `TodoWrite` state is invisible to the binary and dies with the session; pure binary recompute can't track accept decisions and re-flags them every run; multi-file artifacts (cards/, ledger.md, rejections.md per the `simplify-and-refactor` skill) are heavier than warranted when each item is small and self-contained.
diff --git a/agents/polish-worker.md b/agents/polish-worker.md
deleted file mode 100644
index f41aba1e..00000000
--- a/agents/polish-worker.md
+++ /dev/null
@@ -1,284 +0,0 @@
----
-name: polish-worker
-description: >
- Internal worker agent for CLI quality fixes. Dispatched by the printing-press
- skill (Phase 5.5) and the printing-press-polish skill. Not for direct
- invocation — requires CLI_DIR, SPEC_PATH, and CLI_NAME passed by the caller.
-model: inherit
-color: yellow
----
-
-You are the polish worker. You receive a CLI directory path, spec path, and CLI
-name. You run diagnostics, fix all quality issues autonomously, and return a
-structured delta report.
-
-## Rules
-
-- Fix everything without asking. You are fully autonomous.
-- Do not add new features. Polish fixes quality issues only.
-- Do not modify the printing-press generator or any files outside CLI_DIR.
-- Do not offer to publish. The caller handles that.
-- Maximum 1 fix-and-rediagnose pass.
-- Prefer mechanical fixes over creative decisions. When a creative decision is
- needed (like the CLI description), use the research brief from manuscripts if
- available.
-
-## Input
-
-Your dispatch prompt contains:
-
-- `CLI_DIR`: absolute path to the CLI directory
-- `CLI_NAME`: e.g., "notion-pp-cli"
-- `SPEC_PATH`: absolute path to the API spec (may be empty or "none")
-
-## Phase 1: Baseline
-
-```bash
-cd "$CLI_DIR"
-
-# Build
-go build -o "$CLI_NAME" ./cmd/"$CLI_NAME" 2>&1
-
-# Diagnostics (use SPEC_FLAG="--spec $SPEC_PATH" when SPEC_PATH is non-empty)
-printing-press dogfood --dir "$CLI_DIR" $SPEC_FLAG 2>&1
-printing-press verify --dir "$CLI_DIR" $SPEC_FLAG --json 2>&1
-printing-press workflow-verify --dir "$CLI_DIR" --json > /tmp/polish-workflow-verify.json 2>&1 || true
-printing-press verify-skill --dir "$CLI_DIR" --json > /tmp/polish-verify-skill.json 2>&1 || true
-# --live-check samples novel-feature outputs and populates
-# live_check.features[].warnings (Wave B entity detection) — required for
-# the "Output entity warnings" row below to have data to read.
-printing-press scorecard --dir "$CLI_DIR" $SPEC_FLAG --live-check --json > /tmp/polish-scorecard.json 2>&1 || true
-printing-press scorecard --dir "$CLI_DIR" $SPEC_FLAG 2>&1
-go vet ./... 2>&1
-```
-
-verify-skill and workflow-verify run alongside dogfood/verify/scorecard so polish catches the same class of failures the public-library CI catches. The polish-worker hard-gates `ship_recommendation: ship` on `verify-skill` exit 0 (see Phase 4 below).
-
-Parse findings into categories:
-
-| Category | Source | What to look for |
-|----------|--------|------------------|
-| Verify failures | verify --json | Commands with score < 3 |
-| SKILL static-check failures | verify-skill --json | Any `findings[]` with `severity=error` (flag-names, flag-commands, positional-args). Hard ship-gate: ship_recommendation cannot be `ship` while these exist. |
-| Workflow gaps | workflow-verify --json | Verdict `workflow-fail`. Soft gate: surface in `remaining_issues` and downgrade to `hold` when the workflow is the CLI's primary value. |
-| Dead code | dogfood | Dead functions, dead flags |
-| Stale files | dogfood | Unregistered commands |
-| Description issues | dogfood | Boilerplate root Short |
-| README gaps | scorecard | README score < 8 |
-| Example gaps | dogfood | Commands missing examples |
-| Go vet issues | go vet | Any output |
-| Output entity warnings | scorecard JSON | `live_check.features[].warnings` — raw HTML entities in human output |
-| Output plausibility | Phase 4.85 | Findings from the agentic output review |
-
-### Phase 4.85 — Agentic output review (Wave B)
-
-After the mechanical diagnostics above complete, run Phase 4.85 exactly as defined in the main printing-press SKILL.md (under `## Phase 4.85: Agentic Output Review`). The polish pathway uses the same Dispatch / Gate / Known blind spots contract — it's the canonical backfill path for CLIs shipped before Phase 4.85 existed. Record findings alongside the mechanical gates above so Phase 2 fixes address both.
-
-Wave B gating applies: all Phase 4.85 findings are surfaced as warnings, not blockers. Fix if obvious and cheap; document with a short comment in the scorecard JSON if deferred. Non-interactive polish runs (CI, cron) follow the fail-open-with-log contract from Phase 4.85's Gate section.
-
-Record baseline scores: scorecard total, verify pass rate, dogfood verdict, go vet issue count, Phase 4.85 finding count.
-
-## Phase 2: Fix
-
-Fix in priority order. After each priority level, update the lock heartbeat:
-```bash
-printing-press lock update --cli "$CLI_NAME" --phase polish 2>/dev/null
-```
-
-### Priority 1: Verify failures
-
-For each command that fails verify dry-run or exec:
-
-1. Read the command file
-2. Find `Args: cobra.ExactArgs(N)` or similar constraint
-3. Remove the `Args:` field
-4. Add at the top of `RunE`:
- ```go
- if len(args) == 0 {
- return cmd.Help()
- }
- ```
-5. For commands needing 2+ args, use `if len(args) < 2`
-6. Check for dry-run nil-data crashes and add guards:
- ```go
- if flags.dryRun {
- return nil
- }
- ```
-
-### Priority 2: Dead code
-
-1. For each dead function flagged by dogfood, grep all `.go` files to verify
- it's truly unused (not just its definition matching itself)
-2. If truly unused: remove the function
-3. If used by another helper: leave it (false positive)
-4. After removal, remove unused imports
-5. Delete stale files (promoted commands not registered in root.go)
-
-### Priority 3: CLI description and metadata
-
-1. Read root command `Short` in `internal/cli/root.go`
-2. If it contains boilerplate ("Reverse-engineered...", raw API title), rewrite:
- Pattern: `"<Product> CLI with <capability-1>, <capability-2>, and <capability-3>"`
-3. Check commands for missing `Example` fields. Add realistic examples with
- domain-specific values.
-
-### Priority 4: README
-
-**Cardinal rule: run `<cli> <cmd> --help` for EVERY command you put in the
-README.** Never guess flag names, argument formats, or valid values. If you
-write `--start-time` but the flag is `--start`, the README is wrong and
-users will get errors on their first try.
-
-#### Inject novel features from research
-
-If the README lacks a `## Unique Features` section, check whether the
-manuscript archive has novel features to surface:
-
-```bash
-PRESS_HOME="$HOME/printing-press"
-API_SLUG="${CLI_NAME%-pp-cli}"
-RESEARCH_JSON=""
-for f in "$PRESS_HOME/manuscripts/$CLI_NAME"/*/research.json \
- "$PRESS_HOME/manuscripts/$API_SLUG"/*/research.json; do
- if [ -f "$f" ]; then RESEARCH_JSON="$f"; break; fi
-done
-```
-
-If `RESEARCH_JSON` exists, read it and check for a `novel_features` array.
-If that array is non-empty and the README has no `## Unique Features`
-heading, inject the section **after `## Quick Start`** (or before
-`## Usage` if Quick Start doesn't exist).
-
-Format each feature exactly as the generator template does:
-
-```markdown
-## Unique Features
-
-These capabilities aren't available in any other tool for this API.
-
-- **`<command>`** — <description>
-```
-
-Before injecting, verify each feature's `command` actually exists in the
-built CLI by running `./$CLI_NAME <command> --help 2>/dev/null`. Skip any
-feature whose command does not exist — it may have been renamed or dropped
-during generation.
-
-#### Required sections (must be present and correct)
-
-1. **Title**: "# <Product Name> CLI" — use the product's real name with
- correct casing/punctuation (e.g., "Cal.com" not "Cal Com")
-2. **Subtitle**: one sentence describing what the CLI does for the user,
- matching the root `Short` field. NOT a description of the API.
-3. **Install**: correct install command. Use the printing-press-library
- repo URL, not a per-CLI repo that doesn't exist.
-4. **Authentication**: how to set `<API>_API_KEY` env var, where to get
- a key (link to the provider's settings page), self-hosted URL override
- if supported. Read `config.go` to find all env vars.
-5. **Quick Start**: 3-5 commands someone will actually run first. Pick
- commands that are both **most useful** (what you'd run daily) and
- **show the CLI's value** (why install this over curl). Usually:
- `doctor` → `sync` → transcendence command (`today`, `health`) →
- `search`. Avoid raw list commands — they dump data without
- demonstrating why the CLI exists.
-6. **Commands**: categorized table. Group by domain function (Scheduling,
- Analytics, Account, Utilities), not by implementation structure.
-7. **Output Formats**: show `--json`, `--select`, `--csv`, `--compact`,
- `--dry-run`, `--agent`. Use a real command, not a placeholder.
-8. **Agent Usage**: agent-native properties and exit codes.
-9. **Cookbook**: 8-15 recipes using **verified flag names** from `--help`.
- Show the CLI's unique capabilities: transcendence commands, filters,
- SQL queries, pipes. Include at least one mutation example.
-10. **Health Check**: show actual `doctor` output, not a placeholder.
-11. **Configuration**: list ALL env vars from config.go with descriptions.
- Include config file path.
-12. **Troubleshooting**: common errors mapped to exit codes with fixes.
-
-#### Optional sections (add at your discretion)
-
-- **Rate Limits**: if the API has documented limits
-- **Self-Hosting**: if the CLI supports `--api-url` or `BASE_URL` override
-- **Pagination**: if the API has notable pagination behavior
-- **Sources & Inspiration**: credits to community projects (generated by
- the machine, preserve if present)
-
-### Priority 4.5: SKILL static-check failures (verify-skill)
-
-Read `/tmp/polish-verify-skill.json` for the full finding list. Each finding has a `check` (`flag-names`, `flag-commands`, or `positional-args`), a `command` (the path the SKILL claimed), and a `detail` describing the mismatch. Common shapes and fixes:
-
-- **`flag-names`** — SKILL references `--foo` but no command in `internal/cli/*.go` declares it. Either the example is wrong (fix the SKILL or remove the recipe) or the flag was deleted (decide if it should come back).
-- **`flag-commands`** — `--foo is declared elsewhere but not on <cmd>`. The flag exists somewhere but not on the command the SKILL invoked it on. Two fixes:
- 1. If the flag is added via a shared helper like `addXxxFlags(cmd, ...)`, inline the `cmd.Flags().StringVar(...)` declaration directly in the affected command's source file. The verify-skill grep cannot follow function-call indirection.
- 2. If the SKILL example is genuinely wrong, fix the example to use a flag the command does declare.
-- **`positional-args`** — `got N positional args; Use: "<cmd> <arg>" expects M-M`. The SKILL recipe passed N positional args but the command's `Use:` declares M required. Two fixes:
- 1. If the command also accepts the value via a `--flag`, change `Use: "cmd <arg>"` to `Use: "cmd [arg]"` (square brackets = optional). Verify-skill correctly accepts `--flag`-only invocations against an optional positional.
- 2. If the SKILL example is missing a required positional, fix the example.
-
-After fixing, re-run `printing-press verify-skill --dir "$CLI_DIR"` and confirm exit 0 before moving on.
-
-### Priority 5: Remaining dogfood issues
-
-- Path validity mismatches
-- Auth protocol mismatches
-- Example drift (examples referencing wrong commands)
-- Data pipeline integrity issues
-
-### After all fixes
-
-```bash
-go build -o "$CLI_NAME" ./cmd/"$CLI_NAME"
-gofmt -w .
-```
-
-## Phase 3: Re-diagnose
-
-Re-run the diagnostic sweep on the fixed CLI:
-
-```bash
-printing-press dogfood --dir "$CLI_DIR" $SPEC_FLAG 2>&1
-printing-press verify --dir "$CLI_DIR" $SPEC_FLAG --json 2>&1
-printing-press workflow-verify --dir "$CLI_DIR" --json 2>&1
-printing-press verify-skill --dir "$CLI_DIR" --json 2>&1
-printing-press scorecard --dir "$CLI_DIR" $SPEC_FLAG 2>&1
-go vet ./... 2>&1
-```
-
-Record the after scores. If verify-skill still has any `severity=error` findings or workflow-verify still reports `workflow-fail`, ship_recommendation cannot be `ship` (see Phase 4).
-
-## Phase 4: Return
-
-End your response with this EXACT format. The orchestrator parses it:
-
-```
----POLISH-RESULT---
-scorecard_before: <N>
-scorecard_after: <N>
-verify_before: <N>
-verify_after: <N>
-dogfood_before: <PASS|FAIL>
-dogfood_after: <PASS|FAIL>
-govet_before: <N>
-govet_after: <N>
-fixes_applied:
-- <one-line description of each fix>
-skipped_findings:
-- <finding>: <why you chose not to fix it>
-remaining_issues:
-- <one-line description of each issue you tried to fix but couldn't>
-ship_recommendation: <ship|ship-with-gaps|hold>
----END-POLISH-RESULT---
-```
-
-The three lists serve different purposes:
-- **fixes_applied**: what changed — the caller displays these
-- **skipped_findings**: issues you found but deliberately did not fix, with reasoning
- (e.g., "verify classifies `stale` as read — scorer bug, not a CLI problem",
- "README cookbook section is generic — needs domain context from research brief").
- The caller surfaces these so the user can decide whether to address them manually.
-- **remaining_issues**: issues you tried to fix but couldn't resolve
-
-Ship recommendation logic:
-- `ship`: verify >= 80%, scorecard >= 75, no critical failures, **AND** verify-skill exits 0 (no SKILL/CLI mismatches), **AND** workflow-verify is not `workflow-fail`. The two SKILL/workflow gates are hard requirements: a CLI that ships with a SKILL that lies about it (verify-skill findings) gives agents broken instructions; a CLI whose primary workflow fails verification has not actually shipped.
-- `ship-with-gaps`: verify >= 65%, scorecard >= 65, non-critical gaps remain, **AND** the SKILL/workflow gates above hold. Reserved for the rare case where a refactor or external-dependency blocker prevents a clean fix; the gap must be documented in `remaining_issues` and surfaced to the orchestrator.
-- `hold`: verify < 65% or scorecard < 65 or critical failures, **OR** verify-skill has unresolved findings, **OR** workflow-verify reports `workflow-fail` and the workflow is the CLI's primary value.
diff --git a/internal/cli/root.go b/internal/cli/root.go
index c7657a23..38cfbe8b 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -61,6 +61,7 @@ func Execute() error {
rootCmd.AddCommand(newShipcheckCmd())
rootCmd.AddCommand(newLockCmd())
rootCmd.AddCommand(newMCPAuditCmd())
+ rootCmd.AddCommand(newToolsAuditCmd())
rootCmd.AddCommand(newProbeReachabilityCmd())
rootCmd.AddCommand(newSchemaCmd())
rootCmd.AddCommand(newBundleCmd())
diff --git a/internal/cli/tools_audit.go b/internal/cli/tools_audit.go
new file mode 100644
index 00000000..e7493734
--- /dev/null
+++ b/internal/cli/tools_audit.go
@@ -0,0 +1,413 @@
+package cli
+
+import (
+ "encoding/json"
+ "fmt"
+ "go/ast"
+ "go/parser"
+ "go/token"
+ "io"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/spf13/cobra"
+)
+
+const (
+ ledgerFilename = ".printing-press-tools-polish.json"
+ ledgerStaleAfter = 24 * time.Hour
+ statusAccepted = "accepted"
+ suspiciousMaxLen = 30
+ suspiciousMinWord = 4
+)
+
+// newToolsAuditCmd inspects a single printed CLI's command tree for
+// MCP tool quality issues a deterministic check can catch: empty Short,
+// suspiciously thin Short, and read-shaped command names that lack the
+// mcp:read-only annotation. The output is a JSON list of findings the
+// agent then runs through the references/tools-polish.md playbook.
+//
+// Deterministic only — judgment-grade questions ("is this description
+// agent-grade?") belong in the polish skill, not here. Diagnostic
+// exit code 0 regardless of findings.
+func newToolsAuditCmd() *cobra.Command {
+ var asJSON bool
+
+ cmd := &cobra.Command{
+ Use: "tools-audit <cli-dir>",
+ Short: "Mechanically audit a printed CLI's MCP tool surface for missing annotations and thin descriptions",
+ Long: `Walks <cli-dir>/internal/cli/*.go and reports per-command
+findings that signal MCP tool quality issues. Detection is purely
+mechanical: empty Short fields, Short text under 30 characters with
+fewer than 4 words, and read-shaped command names that lack the
+mcp:read-only annotation. The agent layer (references/tools-polish.md)
+takes these findings and applies judgment for descriptions and
+borderline classifications.
+
+Exit 0 regardless of findings (diagnostic, not gating).`,
+ Example: ` printing-press tools-audit ~/printing-press/library/dub
+ printing-press tools-audit ~/printing-press/library/dub --json`,
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ cliDir := args[0]
+ findings, err := runToolsAudit(cliDir)
+ if err != nil {
+ return err
+ }
+
+ if asJSON {
+ enc := json.NewEncoder(cmd.OutOrStdout())
+ enc.SetIndent("", " ")
+ return enc.Encode(findings)
+ }
+
+ previous := readPreviousLedger(cliDir)
+ delta := reconcileWithLedger(previous, findings)
+
+ if err := writeLedger(cliDir, findings); err != nil {
+ fmt.Fprintf(cmd.ErrOrStderr(), "warning: writing ledger %s: %v\n", filepath.Join(cliDir, ledgerFilename), err)
+ }
+ renderToolsAuditTable(cmd.OutOrStdout(), findings, delta)
+ return nil
+ },
+ }
+
+ cmd.Flags().BoolVar(&asJSON, "json", false, "emit JSON instead of a human-readable table")
+ return cmd
+}
+
+// ToolsAuditFinding is one mechanical issue discovered in a command
+// definition. Status and Note are ledger-only; the audit phase emits
+// findings without them (omitempty keeps --json clean for downstream
+// parsing). The agent edits the persisted ledger to flip Status to
+// "accepted" with a Note explaining why a thin-short is fine as-is.
+// On re-run the binary preserves these fields for any finding whose
+// identity key matches.
+type ToolsAuditFinding struct {
+ Kind string `json:"kind"` // "empty-short", "thin-short", "missing-read-only"
+ Command string `json:"command"` // value of the Use field, e.g. "tiktok"
+ File string `json:"file"` // path relative to cli-dir
+ Line int `json:"line"` // 1-based source line of the cobra.Command literal
+ Evidence string `json:"evidence"` // the offending text (Short value, etc.)
+ Status string `json:"status,omitempty"` // "" (== pending) or "accepted"; agent writes
+ Note string `json:"note,omitempty"` // agent-written rationale for an accept decision
+}
+
+// readShapedNames is the heuristic for "this command name suggests a
+// read operation." We exclude verbs already in cobratree's
+// frameworkCommands skip set (search, sql, doctor, version) — the
+// runtime walker doesn't register those as MCP tools, so a missing
+// read-only annotation is meaningless noise for them.
+var readShapedNames = map[string]struct{}{
+ "list": {}, "get": {}, "show": {}, "view": {},
+ "find": {}, "describe": {}, "context": {}, "stats": {},
+ "trending": {}, "trust": {}, "health": {}, "stale": {}, "orphans": {},
+ "reconcile": {}, "analytics": {},
+}
+
+// runToolsAudit reads every non-test .go file in <cliDir>/internal/cli/
+// and accumulates findings across each Command literal. Returns
+// findings sorted by file then line so the human-readable table is
+// stable and the JSON output is diff-friendly.
+func runToolsAudit(cliDir string) ([]ToolsAuditFinding, error) {
+ pkgDir := filepath.Join(cliDir, "internal", "cli")
+ entries, err := os.ReadDir(pkgDir)
+ if err != nil {
+ return nil, fmt.Errorf("reading %s: %w", pkgDir, err)
+ }
+ var findings []ToolsAuditFinding
+ fset := token.NewFileSet()
+ for _, e := range entries {
+ name := e.Name()
+ if e.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
+ continue
+ }
+ full := filepath.Join(pkgDir, name)
+ // Skip unparseable files — the agent can run go build separately
+ // to surface syntax errors without failing the audit.
+ file, err := parser.ParseFile(fset, full, nil, 0)
+ if err != nil {
+ continue
+ }
+ ast.Inspect(file, func(n ast.Node) bool {
+ lit, ok := n.(*ast.CompositeLit)
+ if !ok || !isCobraCommandType(lit.Type) {
+ return true
+ }
+ fields := extractCommandFields(lit)
+ if fields.use == "" {
+ return true
+ }
+ line := fset.Position(lit.Pos()).Line
+ findings = append(findings, auditCommandFields(name, line, fields)...)
+ return true
+ })
+ }
+ sort.Slice(findings, func(i, j int) bool {
+ if findings[i].File != findings[j].File {
+ return findings[i].File < findings[j].File
+ }
+ return findings[i].Line < findings[j].Line
+ })
+ return findings, nil
+}
+
+type commandFields struct {
+ use string
+ short string
+ hasReadOnly bool
+ hasEndpoint bool
+}
+
+func isCobraCommandType(expr ast.Expr) bool {
+ sel, ok := expr.(*ast.SelectorExpr)
+ if !ok {
+ return false
+ }
+ pkg, ok := sel.X.(*ast.Ident)
+ if !ok {
+ return false
+ }
+ return pkg.Name == "cobra" && sel.Sel.Name == "Command"
+}
+
+// extractCommandFields pulls Use/Short/Annotations out of a composite
+// literal. Concatenated string literals and unresolvable expressions
+// surface as the empty string — acceptable since the audit's job is to
+// flag missing or thin content, not enforce that all values be string
+// literals.
+func extractCommandFields(lit *ast.CompositeLit) commandFields {
+ var f commandFields
+ for _, el := range lit.Elts {
+ kv, ok := el.(*ast.KeyValueExpr)
+ if !ok {
+ continue
+ }
+ key, ok := kv.Key.(*ast.Ident)
+ if !ok {
+ continue
+ }
+ switch key.Name {
+ case "Use":
+ f.use = stringLit(kv.Value)
+ case "Short":
+ f.short = stringLit(kv.Value)
+ case "Annotations":
+ f.hasReadOnly, f.hasEndpoint = inspectAnnotations(kv.Value)
+ }
+ }
+ return f
+}
+
+func stringLit(e ast.Expr) string {
+ bl, ok := e.(*ast.BasicLit)
+ if !ok || bl.Kind != token.STRING {
+ return ""
+ }
+ if len(bl.Value) >= 2 && (bl.Value[0] == '"' || bl.Value[0] == '`') {
+ return bl.Value[1 : len(bl.Value)-1]
+ }
+ return bl.Value
+}
+
+func inspectAnnotations(e ast.Expr) (hasReadOnly, hasEndpoint bool) {
+ lit, ok := e.(*ast.CompositeLit)
+ if !ok {
+ return false, false
+ }
+ for _, el := range lit.Elts {
+ kv, ok := el.(*ast.KeyValueExpr)
+ if !ok {
+ continue
+ }
+ switch stringLit(kv.Key) {
+ case "mcp:read-only":
+ hasReadOnly = stringLit(kv.Value) == "true"
+ case "pp:endpoint":
+ hasEndpoint = stringLit(kv.Value) != ""
+ }
+ }
+ return hasReadOnly, hasEndpoint
+}
+
+func auditCommandFields(file string, line int, f commandFields) []ToolsAuditFinding {
+ cmdName := strings.Fields(f.use)
+ if len(cmdName) == 0 {
+ return nil
+ }
+ name := cmdName[0]
+
+ var out []ToolsAuditFinding
+ switch {
+ case f.short == "":
+ out = append(out, ToolsAuditFinding{
+ Kind: "empty-short", Command: name, File: file, Line: line,
+ Evidence: "(empty)",
+ })
+ case suspiciousShort(f.short):
+ out = append(out, ToolsAuditFinding{
+ Kind: "thin-short", Command: name, File: file, Line: line,
+ Evidence: f.short,
+ })
+ }
+ // pp:endpoint commands are skipped by the runtime walker (they get
+ // typed-tool registration with method-derived classification), so
+ // the missing-read-only check doesn't apply to them.
+ if !f.hasEndpoint && !f.hasReadOnly && readShapedName(name) {
+ out = append(out, ToolsAuditFinding{
+ Kind: "missing-read-only", Command: name, File: file, Line: line,
+ Evidence: "name matches read heuristic; no mcp:read-only annotation",
+ })
+ }
+ return out
+}
+
+// suspiciousShort flags Short text that's both short (under 30 chars)
+// and uses fewer than 4 words. Either dimension alone is fine: a long
+// 3-word phrase is OK, and a short-but-precise instruction is OK.
+// Both together is the "Search Ads" / "Subreddit Posts" anti-pattern.
+func suspiciousShort(s string) bool {
+ return len(s) < suspiciousMaxLen && len(strings.Fields(s)) < suspiciousMinWord
+}
+
+// readShapedName matches the head before the first hyphen against the
+// readShapedNames set. Compound names like "get-foo" or "list-bar"
+// classify by their leading verb.
+func readShapedName(name string) bool {
+ head := name
+ if i := strings.IndexByte(name, '-'); i > 0 {
+ head = name[:i]
+ }
+ _, ok := readShapedNames[head]
+ return ok
+}
+
+func renderToolsAuditTable(w io.Writer, findings []ToolsAuditFinding, delta ledgerDelta) {
+ var pending, accepted int
+ for _, f := range findings {
+ if f.Status == statusAccepted {
+ accepted++
+ } else {
+ pending++
+ }
+ }
+ if pending == 0 {
+ if accepted > 0 {
+ fmt.Fprintf(w, "tools-audit: no pending findings (%d accepted)\n", accepted)
+ } else {
+ fmt.Fprintln(w, "tools-audit: no findings")
+ }
+ if delta.hasPrevious && len(delta.resolved) > 0 {
+ fmt.Fprintf(w, "since last run: %d resolved, 0 new\n", len(delta.resolved))
+ }
+ return
+ }
+ fmt.Fprintf(w, "tools-audit: %d pending finding(s)", pending)
+ if accepted > 0 {
+ fmt.Fprintf(w, " (%d accepted)", accepted)
+ }
+ fmt.Fprintln(w)
+ if delta.hasPrevious {
+ fmt.Fprintf(w, "since last run: %d resolved, %d new\n", len(delta.resolved), len(delta.added))
+ }
+ fmt.Fprintln(w)
+ fmt.Fprintf(w, "%-20s %-15s %-30s %s\n", "KIND", "COMMAND", "FILE:LINE", "EVIDENCE")
+ for _, f := range findings {
+ if f.Status == statusAccepted {
+ continue
+ }
+ loc := fmt.Sprintf("%s:%d", f.File, f.Line)
+ fmt.Fprintf(w, "%-20s %-15s %-30s %s\n", f.Kind, f.Command, loc, f.Evidence)
+ }
+}
+
+// ToolsAuditLedger is the on-disk snapshot of the last audit run.
+type ToolsAuditLedger struct {
+ Timestamp time.Time `json:"timestamp"`
+ CLIDir string `json:"cli_dir"`
+ Findings []ToolsAuditFinding `json:"findings"`
+}
+
+type ledgerDelta struct {
+ hasPrevious bool
+ resolved []ToolsAuditFinding // present in previous, absent in current
+ added []ToolsAuditFinding // present in current, absent in previous
+}
+
+// readPreviousLedger loads the ledger at <cli-dir>/<ledgerFilename>.
+// Returns nil for missing, corrupt, or stale ledgers — the audit treats
+// all three as "no resumable state." Stale and corrupt files are
+// deleted so the next write starts clean. Read errors other than "not
+// exists" silently fall back to no-ledger; the next write surfaces
+// the same error to stderr.
+func readPreviousLedger(cliDir string) *ToolsAuditLedger {
+ path := filepath.Join(cliDir, ledgerFilename)
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil
+ }
+ var l ToolsAuditLedger
+ if err := json.Unmarshal(data, &l); err != nil {
+ _ = os.Remove(path)
+ return nil
+ }
+ if time.Since(l.Timestamp) > ledgerStaleAfter {
+ _ = os.Remove(path)
+ return nil
+ }
+ return &l
+}
+
+func writeLedger(cliDir string, findings []ToolsAuditFinding) error {
+ ledger := ToolsAuditLedger{
+ Timestamp: time.Now().UTC(),
+ CLIDir: cliDir,
+ Findings: findings,
+ }
+ data, err := json.MarshalIndent(ledger, "", " ")
+ if err != nil {
+ return fmt.Errorf("encoding ledger: %w", err)
+ }
+ data = append(data, '\n')
+ return os.WriteFile(filepath.Join(cliDir, ledgerFilename), data, 0644)
+}
+
+// reconcileWithLedger carries Status/Note from the previous ledger
+// onto matching current findings (so accept decisions survive re-runs)
+// and computes the resolved/added delta in a single pass. Identity is
+// (file, line, kind, command, evidence); a finding whose Short was
+// rewritten reads as "old resolved, new added" rather than mutated.
+func reconcileWithLedger(previous *ToolsAuditLedger, current []ToolsAuditFinding) ledgerDelta {
+ if previous == nil {
+ return ledgerDelta{}
+ }
+ prev := make(map[string]ToolsAuditFinding, len(previous.Findings))
+ for _, f := range previous.Findings {
+ prev[findingKey(f)] = f
+ }
+ delta := ledgerDelta{hasPrevious: true}
+ seen := make(map[string]bool, len(current))
+ for i := range current {
+ k := findingKey(current[i])
+ seen[k] = true
+ if old, ok := prev[k]; ok {
+ current[i].Status = old.Status
+ current[i].Note = old.Note
+ } else {
+ delta.added = append(delta.added, current[i])
+ }
+ }
+ for k, f := range prev {
+ if !seen[k] {
+ delta.resolved = append(delta.resolved, f)
+ }
+ }
+ return delta
+}
+
+func findingKey(f ToolsAuditFinding) string {
+ return fmt.Sprintf("%s:%d:%s:%s:%s", f.File, f.Line, f.Kind, f.Command, f.Evidence)
+}
diff --git a/skills/printing-press-polish/SKILL.md b/skills/printing-press-polish/SKILL.md
index c0d35545..b4c2abee 100644
--- a/skills/printing-press-polish/SKILL.md
+++ b/skills/printing-press-polish/SKILL.md
@@ -3,11 +3,12 @@ name: printing-press-polish
description: >
Polish a generated CLI to pass verification and become publish-ready. Runs
diagnostics (dogfood, verify, scorecard, go vet), automatically fixes all
- issues (verify failures, dead code, descriptions, README), reports the
- before/after delta, and offers to publish. Use after any /printing-press run,
- or on any CLI in ~/printing-press/library/. Trigger phrases: "polish",
- "improve the CLI", "fix verify", "make it publish-ready", "clean up the CLI",
- "get this ready to ship".
+ issues (verify failures, dead code, descriptions, README, MCP tool quality),
+ reports the before/after delta, and offers to publish. Use after any
+ /printing-press run, or on any CLI in ~/printing-press/library/. Trigger
+ phrases: "polish", "improve the CLI", "fix verify", "make it publish-ready",
+ "clean up the CLI", "get this ready to ship".
+context: fork
allowed-tools:
- Bash
- Read
@@ -15,7 +16,6 @@ allowed-tools:
- Grep
- Write
- Edit
- - Agent
- AskUserQuestion
---
@@ -23,9 +23,7 @@ allowed-tools:
Polish a generated CLI so it passes verification and is ready to publish.
-The retro improves the Printing Press. Polish improves the generated CLI. The actual
-fix protocol lives in the `polish-worker` agent — this skill resolves the CLI,
-checks locks, dispatches the agent, and offers to publish.
+The retro improves the Printing Press. Polish improves the generated CLI. This skill runs in a forked context (`context: fork`) so its diagnostic and fix loop doesn't pollute the caller — the diagnostic spam, fix iterations, and re-diagnose noise stay scoped to the polish session, and the caller receives a clean summary.
```bash
/printing-press-polish redfin
@@ -107,22 +105,256 @@ for f in "$PRESS_HOME/manuscripts/$API_SLUG"/*/research/*.yaml "$PRESS_HOME/manu
break
fi
done
+
+# Build the spec flag once. Empty when no spec was found — diagnostic
+# commands accept a missing --spec and degrade gracefully.
+SPEC_FLAG=""
+if [ -n "$SPEC_PATH" ]; then
+ SPEC_FLAG="--spec $SPEC_PATH"
+fi
+```
+
+## Phase 1: Baseline diagnostics
+
+```bash
+cd "$CLI_DIR"
+
+# Build
+go build -o "$CLI_NAME" ./cmd/"$CLI_NAME" 2>&1
+
+# Diagnostics. SPEC_FLAG is set in the "Find spec" step above.
+printing-press dogfood --dir "$CLI_DIR" $SPEC_FLAG 2>&1
+printing-press verify --dir "$CLI_DIR" $SPEC_FLAG --json 2>&1
+printing-press workflow-verify --dir "$CLI_DIR" --json > /tmp/polish-workflow-verify.json 2>&1 || true
+printing-press verify-skill --dir "$CLI_DIR" --json > /tmp/polish-verify-skill.json 2>&1 || true
+# --live-check samples novel-feature outputs and populates
+# live_check.features[].warnings (Wave B entity detection) — required for
+# the "Output entity warnings" row below to have data to read.
+printing-press scorecard --dir "$CLI_DIR" $SPEC_FLAG --live-check --json > /tmp/polish-scorecard.json 2>&1 || true
+printing-press scorecard --dir "$CLI_DIR" $SPEC_FLAG 2>&1
+printing-press tools-audit "$CLI_DIR" --json > /tmp/polish-tools-audit-before.json 2>&1 || true
+go vet ./... 2>&1
+```
+
+verify-skill and workflow-verify run alongside dogfood/verify/scorecard so polish catches the same class of failures the public-library CI catches. Polish hard-gates `ship` on `verify-skill` exit 0 (see ship logic at the end).
+
+Parse findings into categories:
+
+| Category | Source | What to look for |
+|----------|--------|------------------|
+| Verify failures | verify --json | Commands with score < 3 |
+| SKILL static-check failures | verify-skill --json | Any `findings[]` with `severity=error` (flag-names, flag-commands, positional-args). Hard ship-gate: ship cannot fire while these exist. |
+| Workflow gaps | workflow-verify --json | Verdict `workflow-fail`. Soft gate: surface in `remaining_issues` and downgrade to `hold` when the workflow is the CLI's primary value. |
+| Dead code | dogfood | Dead functions, dead flags |
+| Stale files | dogfood | Unregistered commands |
+| Description issues | dogfood | Boilerplate root Short |
+| README gaps | scorecard | README score < 8 |
+| Example gaps | dogfood | Commands missing examples |
+| Go vet issues | go vet | Any output |
+| Output entity warnings | scorecard JSON | `live_check.features[].warnings` — raw HTML entities in human output |
+| Output plausibility | Phase 4.85 | Findings from the agentic output review |
+| MCP tool quality | tools-audit | Empty Short, thin Short, missing read-only annotations |
+
+### Phase 4.85 — Agentic output review (Wave B)
+
+After the mechanical diagnostics above complete, run Phase 4.85 exactly as defined in the main printing-press SKILL.md (under `## Phase 4.85: Agentic Output Review`). The polish pathway uses the same Dispatch / Gate / Known blind spots contract — it's the canonical backfill path for CLIs shipped before Phase 4.85 existed. Record findings alongside the mechanical gates above so Phase 2 fixes address both.
+
+Wave B gating applies: all Phase 4.85 findings are surfaced as warnings, not blockers. Fix if obvious and cheap; document with a short comment in the scorecard JSON if deferred. Non-interactive polish runs (CI, cron) follow the fail-open-with-log contract from Phase 4.85's Gate section.
+
+Record baseline scores: scorecard total, verify pass rate, dogfood verdict, go vet issue count, Phase 4.85 finding count.
+
+## Phase 2: Fix
+
+Fix in priority order. After each priority level, update the lock heartbeat:
+
+```bash
+printing-press lock update --cli "$CLI_NAME" --phase polish 2>/dev/null
+```
+
+### Priority 1: Verify failures
+
+For each command that fails verify dry-run or exec:
+
+1. Read the command file
+2. Find `Args: cobra.ExactArgs(N)` or similar constraint
+3. Remove the `Args:` field
+4. Add at the top of `RunE`:
+ ```go
+ if len(args) == 0 {
+ return cmd.Help()
+ }
+ ```
+5. For commands needing 2+ args, use `if len(args) < 2`
+6. Check for dry-run nil-data crashes and add guards:
+ ```go
+ if flags.dryRun {
+ return nil
+ }
+ ```
+
+### Priority 2: Dead code
+
+1. For each dead function flagged by dogfood, grep all `.go` files to verify
+ it's truly unused (not just its definition matching itself)
+2. If truly unused: remove the function
+3. If used by another helper: leave it (false positive)
+4. After removal, remove unused imports
+5. Delete stale files (promoted commands not registered in root.go)
+
+### Priority 3: CLI description and metadata
+
+1. Read root command `Short` in `internal/cli/root.go`
+2. If it contains boilerplate ("Reverse-engineered...", raw API title), rewrite:
+ Pattern: `"<Product> CLI with <capability-1>, <capability-2>, and <capability-3>"`
+3. Check commands for missing `Example` fields. Add realistic examples with
+ domain-specific values.
+
+### Priority 4: README
+
+**Cardinal rule: run `<cli> <cmd> --help` for EVERY command you put in the
+README.** Never guess flag names, argument formats, or valid values. If you
+write `--start-time` but the flag is `--start`, the README is wrong and
+users will get errors on their first try.
+
+#### Inject novel features from research
+
+If the README lacks a `## Unique Features` section, check whether the
+manuscript archive has novel features to surface:
+
+```bash
+PRESS_HOME="$HOME/printing-press"
+API_SLUG="${CLI_NAME%-pp-cli}"
+RESEARCH_JSON=""
+for f in "$PRESS_HOME/manuscripts/$CLI_NAME"/*/research.json \
+ "$PRESS_HOME/manuscripts/$API_SLUG"/*/research.json; do
+ if [ -f "$f" ]; then RESEARCH_JSON="$f"; break; fi
+done
```
-## Polish: Dispatch the Agent
+If `RESEARCH_JSON` exists, read it and check for a `novel_features` array.
+If that array is non-empty and the README has no `## Unique Features`
+heading, inject the section **after `## Quick Start`** (or before
+`## Usage` if Quick Start doesn't exist).
+
+Format each feature exactly as the generator template does:
+
+```markdown
+## Unique Features
+
+These capabilities aren't available in any other tool for this API.
+
+- **`<command>`** — <description>
+```
-Dispatch the `polish-worker` agent to run the full diagnostic-fix-rediagnose
-loop. The agent is autonomous and returns a structured result.
+Before injecting, verify each feature's `command` actually exists in the
+built CLI by running `./$CLI_NAME <command> --help 2>/dev/null`. Skip any
+feature whose command does not exist — it may have been renamed or dropped
+during generation.
+
+#### Required sections (must be present and correct)
+
+1. **Title**: "# <Product Name> CLI" — use the product's real name with
+ correct casing/punctuation (e.g., "Cal.com" not "Cal Com")
+2. **Subtitle**: one sentence describing what the CLI does for the user,
+ matching the root `Short` field. NOT a description of the API.
+3. **Install**: correct install command. Use the printing-press-library
+ repo URL, not a per-CLI repo that doesn't exist.
+4. **Authentication**: how to set `<API>_API_KEY` env var, where to get
+ a key (link to the provider's settings page), self-hosted URL override
+ if supported. Read `config.go` to find all env vars.
+5. **Quick Start**: 3-5 commands someone will actually run first. Pick
+ commands that are both **most useful** (what you'd run daily) and
+ **show the CLI's value** (why install this over curl). Usually:
+ `doctor` → `sync` → transcendence command (`today`, `health`) →
+ `search`. Avoid raw list commands — they dump data without
+ demonstrating why the CLI exists.
+6. **Commands**: categorized table. Group by domain function (Scheduling,
+ Analytics, Account, Utilities), not by implementation structure.
+7. **Output Formats**: show `--json`, `--select`, `--csv`, `--compact`,
+ `--dry-run`, `--agent`. Use a real command, not a placeholder.
+8. **Agent Usage**: agent-native properties and exit codes.
+9. **Cookbook**: 8-15 recipes using **verified flag names** from `--help`.
+ Show the CLI's unique capabilities: transcendence commands, filters,
+ SQL queries, pipes. Include at least one mutation example.
+10. **Health Check**: show actual `doctor` output, not a placeholder.
+11. **Configuration**: list ALL env vars from config.go with descriptions.
+ Include config file path.
+12. **Troubleshooting**: common errors mapped to exit codes with fixes.
+
+#### Optional sections (add at your discretion)
+
+- **Rate Limits**: if the API has documented limits
+- **Self-Hosting**: if the CLI supports `--api-url` or `BASE_URL` override
+- **Pagination**: if the API has notable pagination behavior
+- **Sources & Inspiration**: credits to community projects (generated by
+ the machine, preserve if present)
+
+### Priority 4.5: SKILL static-check failures (verify-skill)
+
+Read `/tmp/polish-verify-skill.json` for the full finding list. Each finding has a `check` (`flag-names`, `flag-commands`, or `positional-args`), a `command` (the path the SKILL claimed), and a `detail` describing the mismatch. Common shapes and fixes:
+
+- **`flag-names`** — SKILL references `--foo` but no command in `internal/cli/*.go` declares it. Either the example is wrong (fix the SKILL or remove the recipe) or the flag was deleted (decide if it should come back).
+- **`flag-commands`** — `--foo is declared elsewhere but not on <cmd>`. The flag exists somewhere but not on the command the SKILL invoked it on. Two fixes:
+ 1. If the flag is added via a shared helper like `addXxxFlags(cmd, ...)`, inline the `cmd.Flags().StringVar(...)` declaration directly in the affected command's source file. The verify-skill grep cannot follow function-call indirection.
+ 2. If the SKILL example is genuinely wrong, fix the example to use a flag the command does declare.
+- **`positional-args`** — `got N positional args; Use: "<cmd> <arg>" expects M-M`. The SKILL recipe passed N positional args but the command's `Use:` declares M required. Two fixes:
+ 1. If the command also accepts the value via a `--flag`, change `Use: "cmd <arg>"` to `Use: "cmd [arg]"` (square brackets = optional). Verify-skill correctly accepts `--flag`-only invocations against an optional positional.
+ 2. If the SKILL example is missing a required positional, fix the example.
+
+After fixing, re-run `printing-press verify-skill --dir "$CLI_DIR"` and confirm exit 0 before moving on.
+
+### Priority 5: Remaining dogfood issues
+
+- Path validity mismatches
+- Auth protocol mismatches
+- Example drift (examples referencing wrong commands)
+- Data pipeline integrity issues
+
+### Priority 6: MCP tool quality
+
+**Your goal now is to ensure every MCP tool exposed by this CLI carries agent-grade descriptions and correct read/write classifications.** Tool descriptions and classifications are how agents discover and decide whether to call a tool — thin descriptions and missing annotations directly degrade agent UX, and Phase 1's mechanical gates (verify, dogfood) do NOT catch this class of issue.
+
+Stop and:
+
+1. Run `printing-press tools-audit "$CLI_DIR" --json` to surface mechanical findings (empty Short, thin Short, missing `mcp:read-only` on read-shaped command names).
+2. You must read `references/tools-polish.md` and follow its instructions to address the findings AND run a judgment pass over every command — regardless of whether the audit flagged it. The audit catches mechanical issues; description quality and borderline classification (read-only vs. local-write) always require agent reasoning. You must not skip this.
+
+Proceed to "After all fixes" only when audit findings are resolved AND every command's description has been evaluated against the playbook's agent-grade criteria.
+
+### After all fixes
+```bash
+go build -o "$CLI_NAME" ./cmd/"$CLI_NAME"
+gofmt -w .
```
-Agent(
- subagent_type: "cli-printing-press:polish-worker",
- description: "Polish CLI quality",
- prompt: "Polish this CLI.\nCLI_DIR: $CLI_DIR\nCLI_NAME: $CLI_NAME\nSPEC_PATH: $SPEC_PATH"
-)
+
+## Phase 3: Re-diagnose
+
+Re-run the diagnostic sweep on the fixed CLI:
+
+```bash
+printing-press dogfood --dir "$CLI_DIR" $SPEC_FLAG 2>&1
+printing-press verify --dir "$CLI_DIR" $SPEC_FLAG --json 2>&1
+printing-press workflow-verify --dir "$CLI_DIR" --json 2>&1
+printing-press verify-skill --dir "$CLI_DIR" --json 2>&1
+printing-press scorecard --dir "$CLI_DIR" $SPEC_FLAG 2>&1
+printing-press tools-audit "$CLI_DIR" 2>&1
+go vet ./... 2>&1
```
-The agent returns a `---POLISH-RESULT---` block. Parse it and display the delta:
+Record the after scores. If verify-skill still has any `severity=error` findings or workflow-verify still reports `workflow-fail`, ship cannot fire (see ship logic below).
+
+## Ship logic
+
+Compute the ship recommendation:
+
+- **`ship`**: verify >= 80%, scorecard >= 75, no critical failures, **AND** verify-skill exits 0 (no SKILL/CLI mismatches), **AND** workflow-verify is not `workflow-fail`, **AND** tools-audit shows zero pending findings (every finding fixed or explicitly accepted with rationale). The SKILL/workflow gates are hard requirements: a CLI that ships with a SKILL that lies about it (verify-skill findings) gives agents broken instructions; a CLI whose primary workflow fails verification has not actually shipped.
+- **`ship-with-gaps`**: verify >= 65%, scorecard >= 65, non-critical gaps remain, **AND** the SKILL/workflow gates above hold. Reserved for the rare case where a refactor or external-dependency blocker prevents a clean fix; the gap must be documented in the remaining issues.
+- **`hold`**: verify < 65% or scorecard < 65 or critical failures, **OR** verify-skill has unresolved findings, **OR** workflow-verify reports `workflow-fail` and the workflow is the CLI's primary value.
+
+## Display delta and emit result block
+
+Display the delta to the user, then emit the structured `---POLISH-RESULT---` block. The block lets calling skills (e.g., main printing-press SKILL.md Phase 5.5) parse the recommendation and scores reliably; the human-readable table above is for the user.
```
Polish Results for <CLI_NAME>:
@@ -130,17 +362,46 @@ Polish Results for <CLI_NAME>:
Before After Delta
Scorecard: XX/100 XX/100 +N
Verify: XX% XX% +N%
+ Tools-audit: XX XX -N pending findings
Fixes applied:
- - [from fixes_applied in result]
+ - <one-line description of each fix>
+
+Skipped findings:
+ - <finding>: <why you chose not to fix it>
Remaining issues:
- - [from remaining_issues in result]
+ - <one-line description of each issue you tried to fix but couldn't>
+
+---POLISH-RESULT---
+scorecard_before: <N>
+scorecard_after: <N>
+verify_before: <N>
+verify_after: <N>
+dogfood_before: <PASS|FAIL>
+dogfood_after: <PASS|FAIL>
+govet_before: <N>
+govet_after: <N>
+tools_audit_before: <N pending>
+tools_audit_after: <N pending>
+fixes_applied:
+- <one-line description of each fix>
+skipped_findings:
+- <finding>: <why you chose not to fix it>
+remaining_issues:
+- <one-line description of each issue you tried to fix but couldn't>
+ship_recommendation: <ship|ship-with-gaps|hold>
+---END-POLISH-RESULT---
```
+The three lists serve different purposes:
+- **fixes_applied**: what changed — the caller displays these
+- **skipped_findings**: issues you found but deliberately did not fix, with reasoning (e.g., "verify classifies `stale` as read — scorer bug, not a CLI problem", "thin-short on `version` accepted as-is — accurate and brief"). The caller surfaces these so the user can decide whether to address them manually.
+- **remaining_issues**: issues you tried to fix but couldn't resolve.
+
## Publish Offer
-If `scorecard_after` >= 65 and `verify_after` >= 80:
+If `ship` or `ship-with-gaps`:
Present via `AskUserQuestion`:
@@ -163,8 +424,7 @@ Then invoke `/printing-press-publish <cli-name>`.
### If "Polish again"
-Re-dispatch the `polish-worker` agent with the same arguments. Maximum 2
-additional polish passes (3 total including the first).
+Re-run Phase 1 → Phase 2 → Phase 3 with the same CLI. Maximum 2 additional polish passes (3 total including the first).
### If "Done for now"
@@ -172,10 +432,12 @@ End normally.
## Rules
-- Fix everything. Do not ask for approval before fixing — the agent handles it.
+- Fix everything. Do not ask for approval before fixing — polish is autonomous.
- Report results honestly. Show what improved and what didn't.
- Do not add new features. Polish fixes quality issues, not feature gaps.
- Do not re-run research or generation. Polish works with the CLI as-is.
- Do not modify the printing-press generator. That's `/printing-press-retro`.
+- Do not modify any files outside `$CLI_DIR`.
- If polish adds or renames a Cobra command, the MCP surface updates automatically through the generated `internal/mcp/cobratree` runtime mirror. Update `novel_features` only when README/SKILL highlights or registry display should change; use `cmd.Annotations["mcp:hidden"] = "true"` for debug-only commands.
-- Maximum 3 total polish passes (initial + 2 retries).
+- Maximum 1 fix-and-rediagnose pass per polish invocation. The "Polish again" path runs additional invocations (max 3 total).
+- Prefer mechanical fixes over creative decisions. When a creative decision is needed (like the CLI description), use the research brief from manuscripts if available.
diff --git a/skills/printing-press-polish/references/tools-polish.md b/skills/printing-press-polish/references/tools-polish.md
new file mode 100644
index 00000000..87b08ba7
--- /dev/null
+++ b/skills/printing-press-polish/references/tools-polish.md
@@ -0,0 +1,191 @@
+# Tools Polish — MCP Tool Quality Playbook
+
+**Your goal:** ensure every MCP tool exposed by this CLI carries the metadata an agent needs to use it correctly. Tool descriptions and classifications are how agents discover and decide whether to call a tool — thin descriptions and missing annotations directly degrade agent UX.
+
+This playbook has two passes. Both run on every CLI; do not skip either.
+
+## Pass 1: Address mechanical findings
+
+Run the audit:
+
+```bash
+printing-press tools-audit <cli-dir> --json
+```
+
+The audit emits findings of three kinds. Address each:
+
+### `empty-short`
+
+A `cobra.Command{}` with no `Short:` field. The runtime walker falls back to `"Run \`<cmd>\` through the companion CLI binary."` — a meaningless description for agents.
+
+**Fix:** write a verb-led, action-specific Short. Read the command's `RunE` body and `Flags()` block to ground the description in actual behavior. See **Description criteria** below.
+
+### `thin-short`
+
+Short text under 30 characters AND fewer than 4 words. Examples from real CLIs: `"Advertiser Search"`, `"Search Ads"`, `"Subreddit Posts"`, `"Product Reviews"`.
+
+**This is a suspect, not a verdict.** Some short Shorts are accurate (`"Print version"`, `"Check CLI health"`). Decide per finding:
+
+- If the Short is a fragment that doesn't tell an agent what action runs, what parameters it takes, or what comes back → rewrite per **Description criteria**.
+- If the Short is brief but precise → leave it; document the decision implicitly by skipping the rewrite.
+
+### `missing-read-only`
+
+Command name matches a read-shaped pattern (`list`, `get`, `search`, `show`, `view`, `find`, `describe`, `context`, `sql`, `stats`, `trending`, `trust`, `health`, `stale`, `orphans`, `reconcile`, `doctor`, `version`, `analytics`) but no `mcp:read-only` annotation, AND no `pp:endpoint` (so the runtime walker registers it as a shell-out tool).
+
+**Fix:** add `Annotations: map[string]string{"mcp:read-only": "true"},` to the command literal. The walker then emits `readOnlyHint: true` so MCP hosts skip the per-call permission prompt.
+
+**Don't blindly accept.** If the command name matches the heuristic but the body actually mutates state (writes to the local store, opens a browser, sends a notification, modifies config), do NOT add the annotation. The heuristic is a starting point; the body is the truth.
+
+## Pass 2: Evaluate every command (audit-independent)
+
+Run a judgment pass over every user-facing command in `internal/cli/`, including ones the audit didn't flag. Mechanical detection misses two real classes:
+
+1. **Read-only commands whose names don't match the heuristic.** Platform-named shortcuts (`tiktok`, `instagram`, `google`), one-word reads (`search`, `analytics`, `export`), commands renamed away from their `list-*`/`get-*` form (`facebook_list-post-3` → `post-transcript`). All of these are real reads that need `mcp:read-only` but won't trip the audit.
+2. **Descriptions that pass the length check but are still poor for agents.** A 50-character Short that says `"Manage your saved cookbook (list, search, tag, match)"` is category-shaped, not action-shaped — fine for human help text, weak for an agent deciding whether to call it.
+
+For each command:
+
+- Read its `RunE` body. Does it call an HTTP method (`client.Get`, `resolveRead`, `c.Post`, …)? Does it write to the local store, the filesystem, or config?
+ - **HTTP GET only, no local writes** → mark `mcp:read-only` (even if the audit didn't flag).
+ - **Any local write or external mutation** → do NOT mark; the default destructive hint is correct.
+ - **Side effect** (browser open, notification, audio) → do NOT mark; agents should be prompted.
+- Read its `Short:` and `Long:` against the criteria below. If thin or category-shaped, rewrite.
+
+## Description criteria
+
+Agent-grade Shorts share four properties:
+
+1. **Verb-led.** Open with the action: `"Search ..."`, `"Fetch ..."`, `"List ..."`, `"Estimate ..."`, `"Save ..."`. Not `"Cookbook commands"` or `"Manage ..."`.
+2. **Action-specific, not category-shaped.** `"Search the LinkedIn Ad Library by company, keyword, country, and date range"` — not `"Search Ads"`. The reader should know the *one specific operation* this command performs.
+3. **Parameter-aware.** Name the meaningful filters/options inline so the agent knows what to pass: `"... by max cook time, recency, and dietary tags"`, not just `"Pick dinner"`.
+4. **Scope-explicit.** When the command operates on a subset, say so: `"Search recipe titles across curated sites without fetching the full recipes (returns metadata only)"` — tells the agent the cost/output trade-off vs. a richer search.
+
+### Anti-patterns to remove
+
+- **Dev-state leakage.** `"(wip)"`, `"(planned)"`, `"(ranking integration wip)"`, `"(coming soon)"` — useless to agents, leaks dev backlog. Strip during polish.
+- **Self-referential Long.** `Long: "Shortcut for 'feed get-on-this-day'. Events on this day"` — repeats the Short and tells the reader nothing new. Rewrite Long to add genuine context (parameters, output shape, edge cases) or delete it.
+- **Bare verb fragments.** `"Show cooking history"`, `"List saved recipes"`, `"Print version"` — fine for humans browsing `--help`, weak for agents deciding which of many similar tools to invoke. Add the qualifier (`"... most recent first, with rating and notes"`, `"... optionally filtered by tag, site, or author"`).
+- **Empty Long with non-empty Short.** Long should add genuine context or be omitted. A Long that just restates Short adds noise to the MCP tool catalog.
+
+## Worked examples
+
+Pre/post pairs from real polish passes. The "before" lines are what we found in the wild; the "after" lines were the rewrites that landed.
+
+### recipe-goat (from PR #154 polish commit)
+
+```
+Before: "Lightweight cross-site recipe search (metadata only, no fetch)"
+After: "Search recipe titles across curated sites without fetching the full recipes (returns metadata only)"
+```
+
+```
+Before: "Show cooking history"
+After: "List past cooking sessions with ratings and notes, most recent first"
+```
+
+```
+Before: "View and override site trust scores (ranking integration wip)"
+After: "View and override per-site trust scores used by the cross-site ranker"
+```
+
+```
+Before: "Persist a site trust override (ranking integration wip)"
+After: "Save a local per-site trust adjustment that the ranker applies on top of the built-in scores"
+```
+
+### scrape-creators (from PR #113 polish commit)
+
+```
+Before: "Advertiser Search"
+After: "Search the Google Ads Transparency Center for advertisers by name or domain"
+```
+
+```
+Before: "Search Ads"
+After: "Search the LinkedIn Ad Library by company, keyword, country, and date range"
+```
+
+```
+Before: "Subreddit Posts"
+After: "Fetch posts from a subreddit, with sort (hot/new/top) and timeframe filters"
+```
+
+```
+Before: "Product Reviews"
+After: "Fetch product reviews from a TikTok Shop product page (by URL or product ID)"
+```
+
+## Per-finding response templates
+
+When the audit returns a finding, follow this minimum response.
+
+### Empty Short
+
+```go
+// Add the Short field; choose verb-led action description.
+Short: "<verb> <object> <key qualifier>",
+Annotations: map[string]string{"mcp:read-only": "true"}, // only if read
+```
+
+### Thin Short
+
+```go
+// Replace the existing Short with a parameter-aware rewrite.
+// Read RunE body and Flags() block to ground the description in real behavior.
+Short: "<verb> <specific scope> by <main filter>, <secondary filter>",
+```
+
+### Missing read-only
+
+```go
+// Add or extend the Annotations map. Preserve any existing keys.
+Annotations: map[string]string{"mcp:read-only": "true"},
+```
+
+The audit already exempts commands carrying `pp:endpoint` (those get typed-tool registration with method-derived classification), so this finding never fires on endpoint mirrors.
+
+## Ledger and resumability
+
+`tools-audit` writes `<cli-dir>/.printing-press-tools-polish.json` after every run. It contains the timestamp, cli-dir, and one entry per finding. The ledger serves three purposes:
+
+1. **Delta computation.** On a second run within 24 hours, the audit prints `since last run: N resolved, M new` so you can see your progress without re-counting. Stale ledgers (>24h) are deleted automatically.
+2. **Resumability.** If your context window flushes mid-polish, re-run `tools-audit <cli-dir>`. Findings you've fixed have disappeared from the new scan; findings you accepted are still recorded with status. You pick up where you left off.
+3. **Audit trail of accept decisions.** When you decide a `thin-short` is fine as-is, you mark the entry `accepted` and write a one-sentence rationale. The next run filters it out of the pending table.
+
+### Marking a finding accepted
+
+When the table shows a `thin-short` whose Short is brief but precise (`"Print version"`, `"Check CLI health"`, `"Show authentication status"`), edit the ledger entry directly:
+
+```json
+{
+ "kind": "thin-short",
+ "command": "version",
+ "file": "version.go",
+ "line": 12,
+ "evidence": "Print version",
+ "status": "accepted",
+ "note": "Standard 'version' command; agents understand this without elaboration"
+}
+```
+
+After marking, re-run `tools-audit <cli-dir>`. The pending table will exclude this finding. The accepted count in the header (`(1 accepted)`) confirms the decision persisted.
+
+**Don't accept `empty-short` or `missing-read-only` findings.** They have no judgment call to make: empty Shorts always need text, and a read command always wants the annotation. Acceptance is reserved for `thin-short`.
+
+### Don't manually mark findings as fixed
+
+If you fixed a finding via code change, do nothing in the ledger. The next audit re-scans the source; finding gone from the AST → finding gone from the table → delta line shows `1 resolved`. Never set `status: "fixed"` by hand — the tool detects this automatically and your manual marking will read as "still flagged but accepted-without-rationale."
+
+## Verification checklist
+
+After applying fixes, before declaring the polish complete:
+
+- [ ] `go build ./...` clean (annotations don't break compilation)
+- [ ] `printing-press tools-audit <cli-dir>` shows zero pending findings — every finding is either fixed (auto-removed) or explicitly accepted with a `note`
+- [ ] `printing-press dogfood --dir <cli-dir>` reports `MCP Surface: PASS`
+- [ ] If commands were renamed or had their annotations restructured, smoke-test the binary by inspecting `--help` output for the affected commands
+
+The ledger file persists until it ages out (24h). Once the polish PR merges and the CLI is rebuilt, the file is no longer load-bearing — the next `tools-audit` run can start fresh.
+
+If you're polishing a CLI inside a clone of the public library repo (not the internal `~/printing-press/library/`), add `.printing-press-tools-polish.json` to that repo's root `.gitignore` before committing — the ledger is local working state, not part of the published CLI.
diff --git a/skills/printing-press/SKILL.md b/skills/printing-press/SKILL.md
index 72d2e379..e2c1f229 100644
--- a/skills/printing-press/SKILL.md
+++ b/skills/printing-press/SKILL.md
@@ -1997,7 +1997,7 @@ Use the Agent tool (general-purpose) with this prompt contract:
Wave B policy (current):
- All findings surface as `warning` — never `error`. Shipcheck proceeds regardless.
-- Findings are returned in the reviewer agent's response to its caller (main skill at shipcheck, polish-worker during polish runs). The caller logs them to the run's artifact directory (e.g., `manuscripts/<api>/<run>/proofs/phase-4.85-findings.md`) and surfaces them to the user for review. Wave B does not persist findings into `scorecard.json` — that path is reserved for Wave C if findings become blocking.
+- Findings are returned in the reviewer agent's response to its caller (main skill at shipcheck, the polish skill during polish runs). The caller logs them to the run's artifact directory (e.g., `manuscripts/<api>/<run>/proofs/phase-4.85-findings.md`) and surfaces them to the user for review. Wave B does not persist findings into `scorecard.json` — that path is reserved for Wave C if findings become blocking.
- The user decides case by case whether to fix before shipping.
**Non-interactive contract (CI, cron, batch regeneration):**
@@ -2010,7 +2010,7 @@ Wave C (separate future PR) will flip `error`-severity findings to blocking afte
### Polish skill invocation
-Phase 4.85 also runs during `/printing-press-polish` as the backfill path for CLIs shipped before this phase existed. Polish already dispatches verify + dogfood + scorecard via the `polish-worker` agent; Phase 4.85 runs as part of the same worker pipeline so every polish run re-reviews outputs of older CLIs without a separate campaign.
+Phase 4.85 also runs during `/printing-press-polish` as the backfill path for CLIs shipped before this phase existed. The polish skill runs verify + dogfood + scorecard inline; Phase 4.85 runs as part of the same diagnostic loop so every polish run re-reviews outputs of older CLIs without a separate campaign.
### Why agentic vs template-only
@@ -2148,38 +2148,32 @@ Write:
## Phase 5.5: Polish
-**Always runs.** Dispatch the `polish-worker` agent to run diagnostics, fix quality
-issues, and return a structured delta report. The agent is autonomous — no user
-input needed. The goal is to ship the best CLI possible, not the fastest.
+**Always runs.** Invoke the `printing-press-polish` skill to run diagnostics, fix quality issues, and return a delta. The polish skill carries `context: fork` in its frontmatter, so its diagnostic-fix-rediagnose loop runs in a forked context — diagnostic spam, fix iterations, and re-audits stay scoped to the polish session and don't pollute this generation flow. The skill is autonomous — no user input needed. The goal is to ship the best CLI possible, not the fastest.
-Dispatch via the Agent tool (**foreground** — must complete before promoting):
+Invoke via the Skill tool (**foreground** — must complete before promoting):
```
-Agent(
- subagent_type: "cli-printing-press:polish-worker",
- description: "Polish CLI quality",
- prompt: "Polish this CLI.\nCLI_DIR: $CLI_WORK_DIR\nCLI_NAME: <api>-pp-cli\nSPEC_PATH: <same-spec>"
+Skill(
+ skill: "cli-printing-press:printing-press-polish",
+ args: "$CLI_WORK_DIR"
)
```
-The agent runs the full diagnostic-fix-rediagnose loop and ends its response with
-a `---POLISH-RESULT---` block containing scorecard/verify before/after, fixes
-applied, and a ship recommendation.
+The polish skill runs the full diagnostic-fix-rediagnose loop including MCP tool quality polish (via `printing-press tools-audit` plus the playbook at `references/tools-polish.md`) and ends its response with a `---POLISH-RESULT---` block containing scorecard/verify/tools-audit before/after, fixes applied, and a ship recommendation.
-Parse the result. Display the delta to the user:
+Parse the result block. Display the delta to the user:
```
Polish pass:
- Verify: 86% → 93% (+7%)
- Scorecard: 92 → 94 (+2)
+ Verify: 86% → 93% (+7%)
+ Scorecard: 92 → 94 (+2)
+ Tools-audit: 76 → 0 pending findings
Fixed: [summary of fixes_applied from result]
```
-**Verdict override:** If the agent's `ship_recommendation` is `hold` and the
-Phase 4 verdict was `ship`, downgrade to `hold`. Release the lock without
-promoting.
+**Verdict override:** If the polish skill's `ship_recommendation` is `hold` and the Phase 4 verdict was `ship`, downgrade to `hold`. Release the lock without promoting.
-Write the agent's full response to:
+Write the polish skill's full response to:
`$PROOFS_DIR/<stamp>-fix-<api>-pp-cli-polish.md`
← a8163024 fix(cli): mcp-sync detects suggestFlag/Deliver across cli pa
·
back to Cli Printing Press
·
refactor(skills): extract Phase 4.85 into printing-press-out a9ca3f80 →