[object Object]

← back to Cli Printing Press

feat(cli): machine-output-verification Wave B — live-check entity rule + Phase 4.85 agentic output review (#214)

270270ab1da2b80352b59a7bc7be2af35d3e7536 · 2026-04-13 17:43:43 -0700 · Trevin Chow

Files touched

Diff

commit 270270ab1da2b80352b59a7bc7be2af35d3e7536
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Mon Apr 13 17:43:43 2026 -0700

    feat(cli): machine-output-verification Wave B — live-check entity rule + Phase 4.85 agentic output review (#214)
---
 agents/polish-worker.md              |  14 +++-
 internal/cli/scorecard.go            |  16 ++++
 internal/pipeline/live_check.go      | 102 +++++++++++++++++++++--
 internal/pipeline/live_check_test.go | 151 +++++++++++++++++++++++++++++++++++
 skills/printing-press/SKILL.md       |  65 +++++++++++++++
 5 files changed, 342 insertions(+), 6 deletions(-)

diff --git a/agents/polish-worker.md b/agents/polish-worker.md
index 392563a6..a141396f 100644
--- a/agents/polish-worker.md
+++ b/agents/polish-worker.md
@@ -42,6 +42,10 @@ 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
+# --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
 ```
@@ -57,8 +61,16 @@ Parse findings into categories:
 | 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 |
 
-Record baseline scores: scorecard total, verify pass rate, dogfood verdict, go vet issue count.
+### 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
 
diff --git a/internal/cli/scorecard.go b/internal/cli/scorecard.go
index 7f087da0..3e76d18a 100644
--- a/internal/cli/scorecard.go
+++ b/internal/cli/scorecard.go
@@ -115,6 +115,22 @@ func newScorecardCmd() *cobra.Command {
 							}
 						}
 					}
+					// Wave B output-quality warnings are surfaced here so a
+					// developer running scorecard without --json sees them.
+					// Wave A review flagged the "human sees less than agent"
+					// gap as an agent-native parity concern.
+					warnCount := 0
+					for _, f := range live.Features {
+						warnCount += len(f.Warnings)
+					}
+					if warnCount > 0 {
+						fmt.Printf("  Warnings (%d): not blocking in Wave B — flip to failures in Wave C after calibration\n", warnCount)
+						for _, f := range live.Features {
+							for _, w := range f.Warnings {
+								fmt.Printf("    - %s: %s\n", f.Name, w)
+							}
+						}
+					}
 				}
 			}
 
diff --git a/internal/pipeline/live_check.go b/internal/pipeline/live_check.go
index 6319297d..4aa9fabe 100644
--- a/internal/pipeline/live_check.go
+++ b/internal/pipeline/live_check.go
@@ -10,6 +10,7 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
+	"regexp"
 	"strings"
 	"sync"
 	"time"
@@ -66,14 +67,36 @@ func (r *LiveCheckResult) Checked() int {
 }
 
 // LiveFeatureResult is one feature's outcome.
+//
+// OutputSample carries a bounded snapshot of the captured stdout so
+// downstream consumers (Phase 4.85's agentic output review in particular)
+// can inspect what the command actually produced without re-invoking the
+// CLI. Re-invocation is brittle for stochastic endpoints, expensive for
+// rate-limited ones, and auth-dependent for most — persisting a sample
+// avoids all three.
+//
+// Warnings carries advisory findings that don't flip the feature's Status
+// — the plan's Wave B ships output-quality checks (like raw HTML entities)
+// as warnings for a 2-week calibration window before Wave C escalates them
+// to failures. Consumers should surface warnings in reports but not factor
+// them into pass-rate math until Wave C lands.
 type LiveFeatureResult struct {
-	Name    string     `json:"name"`
-	Command string     `json:"command"`
-	Example string     `json:"example"`
-	Status  LiveStatus `json:"status"`
-	Reason  string     `json:"reason,omitempty"`
+	Name         string     `json:"name"`
+	Command      string     `json:"command"`
+	Example      string     `json:"example"`
+	Status       LiveStatus `json:"status"`
+	Reason       string     `json:"reason,omitempty"`
+	Warnings     []string   `json:"warnings,omitempty"`
+	OutputSample string     `json:"output_sample,omitempty"`
 }
 
+// outputSampleMaxBytes caps the captured-output snapshot stored on each
+// LiveFeatureResult. The raw capture buffer allows up to MaxOutputBytes
+// (1 MiB) but the serialized sample is bounded much tighter so scorecard
+// JSON files stay readable and agentic reviewers don't blow through their
+// context window on one feature's output.
+const outputSampleMaxBytes = 4096
+
 // LiveCheckOptions bundles the optional knobs for RunLiveCheck. CLIDir is
 // required; every other field has a sensible zero-value default.
 type LiveCheckOptions struct {
@@ -284,10 +307,79 @@ func runOneFeatureCheck(cliDir, binaryPath string, f NovelFeature, timeout time.
 		}
 	}
 
+	stdout := stdoutCap.String()
 	result.Status = StatusPass
+	result.OutputSample = sampleOutput(stdout)
+	if msg := detectRawHTMLEntities(stdout, args); msg != "" {
+		result.Warnings = append(result.Warnings, msg)
+	}
 	return result
 }
 
+// sampleOutput truncates captured stdout to outputSampleMaxBytes for
+// persistence on LiveFeatureResult.OutputSample. An ellipsis marker at the
+// boundary tells downstream readers the snapshot is truncated.
+func sampleOutput(s string) string {
+	if len(s) <= outputSampleMaxBytes {
+		return s
+	}
+	return s[:outputSampleMaxBytes] + "…[truncated]"
+}
+
+// rawHTMLEntityRe matches numeric HTML character references, both decimal
+// (&#39;) and hex (&#x27;). Named entities (&amp;, &quot;, &lt;) are
+// intentionally excluded in Wave B — they false-positive on legitimate JSON
+// strings ("AT&amp;T") and on documentation text, so we calibrate the
+// stricter numeric-only rule first. Wave C may broaden to named entities
+// after observing the library's actual output patterns.
+//
+// The digit count is bounded at 10 to prevent a malicious output like
+// "&#99999999999...;" from forcing the regex engine into a large matched
+// span that then propagates into the warning message. Valid Unicode code
+// points fit in 7 decimal digits (10FFFF = 1,114,111), so 10 is a generous
+// upper bound that stays well inside regex budget.
+var rawHTMLEntityRe = regexp.MustCompile(`&#[xX]?[0-9a-fA-F]{1,10};`)
+
+// detectRawHTMLEntities returns a short human-readable reason when output
+// contains raw numeric HTML entities, or "" when output is clean. Gated to
+// non-JSON output so JSON-mode features (whose output legitimately contains
+// escape sequences) don't trip the check.
+//
+// JSON-mode detection:
+//   - any arg that is `--json` or begins with `--json=` (cobra accepts both)
+//   - first non-whitespace character of output is `{` or `[`
+//
+// Both heuristics are conservative: a feature that renders JSON inside a
+// human table would still be checked, which is the right behavior.
+func detectRawHTMLEntities(output string, args []string) string {
+	trimmed := strings.TrimSpace(output)
+	if trimmed == "" {
+		return ""
+	}
+	// Skip JSON-mode: agent-facing output legitimately contains escape
+	// sequences and isn't rendered to a human terminal.
+	for _, a := range args {
+		if a == "--json" || strings.HasPrefix(a, "--json=") {
+			return ""
+		}
+	}
+	if first := trimmed[0]; first == '{' || first == '[' {
+		return ""
+	}
+	match := rawHTMLEntityRe.FindString(output)
+	if match == "" {
+		return ""
+	}
+	// Cap the match-echo so a pathological output (e.g., a huge
+	// numeric ref — guarded by the regex's 10-digit bound, but the
+	// warning message still benefits from a defensive cap) can't
+	// produce megabyte-sized warning strings in the scorecard JSON.
+	if len(match) > 64 {
+		match = match[:64] + "…"
+	}
+	return fmt.Sprintf("raw HTML entity %q in output (decode with cliutil.CleanText or equivalent)", match)
+}
+
 // limitedWriter caps the bytes forwarded to w at `remaining`; further writes
 // are discarded (but still report as successful, so the subprocess doesn't
 // SIGPIPE). Intentionally tolerant of truncation — the live check only needs
diff --git a/internal/pipeline/live_check_test.go b/internal/pipeline/live_check_test.go
index cbb0e9a8..f3983872 100644
--- a/internal/pipeline/live_check_test.go
+++ b/internal/pipeline/live_check_test.go
@@ -353,3 +353,154 @@ func TestChecked_DerivedFromCounters(t *testing.T) {
 	var nilRes *LiveCheckResult
 	require.Zero(t, nilRes.Checked())
 }
+
+// --- detectRawHTMLEntities (Wave B / R3) ---
+
+func TestDetectRawHTMLEntities_CleanOutput(t *testing.T) {
+	cases := []string{
+		"The Food Lab's Cookies",
+		"Recipe title with AT&T in it",
+		"Ordinary scraped text. No entities here.",
+		"",
+		"   \n\t  ",
+	}
+	for _, out := range cases {
+		require.Empty(t, detectRawHTMLEntities(out, nil),
+			"unexpected warning for clean output %q", out)
+	}
+}
+
+func TestDetectRawHTMLEntities_DetectsNumericEntity(t *testing.T) {
+	cases := []struct {
+		name   string
+		output string
+	}{
+		{"decimal apostrophe", "The Food Lab&#39;s Chocolate Chip Cookies"},
+		{"decimal typographic apostrophe", "Ben&#8217;s Pizza"},
+		{"decimal mid-line", "Row 1\nRow 2 with &#34; in it\nRow 3"},
+		// Hex numeric entities are equally common — APIs that encode
+		// apostrophes as &#x27; should trip the same check. Decimal-only
+		// regex was an oversight flagged by Wave B ce:review.
+		{"hex lowercase", "Ben&#x27;s Pizza"},
+		{"hex uppercase x", "foo&#X27;bar"},
+		{"hex multi-char", "quote&#x2019;end"},
+	}
+	for _, tc := range cases {
+		msg := detectRawHTMLEntities(tc.output, nil)
+		require.NotEmpty(t, msg, "expected warning for %q", tc.name)
+		require.Contains(t, msg, "raw HTML entity", tc.name)
+	}
+}
+
+func TestDetectRawHTMLEntities_SkipsWhenJSONFlagPresent(t *testing.T) {
+	// --json in args means agent-facing structured output. Entities in
+	// string values are legitimate JSON, not a display bug.
+	out := `{"title": "The Food Lab&#39;s Cookies"}`
+	require.Empty(t, detectRawHTMLEntities(out, []string{"--json"}))
+	require.Empty(t, detectRawHTMLEntities(out, []string{"goat", "brownies", "--json", "--limit", "5"}))
+	// Cobra accepts `--json=true` / `--json=false` as distinct tokens
+	// from bare `--json`. Adversarial review flagged the exact-match
+	// form as a bypass hole.
+	require.Empty(t, detectRawHTMLEntities(out, []string{"--json=true"}))
+	require.Empty(t, detectRawHTMLEntities(out, []string{"cmd", "--json=false", "--limit", "5"}))
+}
+
+func TestDetectRawHTMLEntities_SkipsWhenOutputStartsWithJSON(t *testing.T) {
+	// Defense in depth: a feature that always emits JSON regardless of
+	// flags still shouldn't trip the check when the output is structured.
+	require.Empty(t, detectRawHTMLEntities(`{"title":"x&#39;y"}`, nil))
+	require.Empty(t, detectRawHTMLEntities(`[{"title":"x&#39;y"}]`, nil))
+	// Leading whitespace before the JSON start still counts as JSON mode.
+	require.Empty(t, detectRawHTMLEntities("  \n{\"title\":\"x&#39;y\"}", nil))
+}
+
+func TestDetectRawHTMLEntities_IgnoresNamedEntities(t *testing.T) {
+	// Named entities are out of scope in Wave B — false-positive risk on
+	// legitimate strings like "AT&amp;T" and README prose. Wave C can
+	// revisit after calibrating on the library.
+	require.Empty(t, detectRawHTMLEntities("AT&amp;T", nil))
+	require.Empty(t, detectRawHTMLEntities("foo &quot;bar&quot; baz", nil))
+	require.Empty(t, detectRawHTMLEntities("less than: &lt; greater than: &gt;", nil))
+}
+
+func TestDetectRawHTMLEntities_IgnoresPartialSequences(t *testing.T) {
+	// Pattern requires digits AND a closing semicolon. "&#abc;" or "&#"
+	// alone are not valid entities and shouldn't warn.
+	require.Empty(t, detectRawHTMLEntities("price: $1&#USD", nil))
+	require.Empty(t, detectRawHTMLEntities("foo & bar #39", nil))
+}
+
+func TestRunOneFeatureCheck_WarnsOnEntityButStaysPass(t *testing.T) {
+	// Integration: a feature whose output is valid but contains a raw
+	// numeric entity should still Pass (pass-rate unaffected) but carry
+	// a warning in the result.
+	binary := buildFakeCLI(t, `#!/usr/bin/env bash
+printf 'The Food Lab&#39;\''s Chocolate Chip Cookies\n'
+`)
+	feature := NovelFeature{
+		Name:    "goat",
+		Command: "goat",
+		Example: "bin goat chocolate chip cookies",
+	}
+	result := runOneFeatureCheck(t.TempDir(), binary, feature, 5*time.Second)
+	require.Equal(t, StatusPass, result.Status, "reason: %s", result.Reason)
+	require.NotEmpty(t, result.Warnings, "expected entity warning")
+	require.Contains(t, result.Warnings[0], "raw HTML entity")
+}
+
+// buildFakeCLI writes a shell script to a temp file and returns its path.
+// Used by entity tests to exercise runOneFeatureCheck end-to-end without
+// depending on a real generated CLI binary.
+func buildFakeCLI(t *testing.T, script string) string {
+	t.Helper()
+	dir := t.TempDir()
+	path := filepath.Join(dir, "fake-cli")
+	require.NoError(t, os.WriteFile(path, []byte(script), 0o755))
+	return path
+}
+
+func TestRunOneFeatureCheck_PopulatesOutputSample(t *testing.T) {
+	// Phase 4.85's agentic reviewer needs the captured stdout to judge
+	// output plausibility without re-invoking the binary. OutputSample
+	// must be populated on pass results.
+	binary := buildFakeCLI(t, `#!/usr/bin/env bash
+printf 'Hello cookie world\n'
+`)
+	feature := NovelFeature{
+		Name:    "demo",
+		Command: "demo",
+		Example: "bin demo cookie",
+	}
+	result := runOneFeatureCheck(t.TempDir(), binary, feature, 5*time.Second)
+	require.Equal(t, StatusPass, result.Status, "reason: %s", result.Reason)
+	require.Contains(t, result.OutputSample, "Hello cookie world")
+}
+
+func TestSampleOutput_TruncatesLargeCapture(t *testing.T) {
+	// Guard the serialized-sample size so one feature can't bloat the
+	// scorecard JSON or overwhelm an agentic reviewer's context window.
+	big := strings.Repeat("x", outputSampleMaxBytes*2)
+	got := sampleOutput(big)
+	require.Contains(t, got, "…[truncated]", "truncation marker missing")
+	require.LessOrEqual(t, len(got), outputSampleMaxBytes+len("…[truncated]"))
+}
+
+func TestSampleOutput_ShortCapturePassesThrough(t *testing.T) {
+	// Small captures must not be truncated or rewritten — they need to
+	// survive a JSON round-trip byte-identical so downstream tests can
+	// assert exact content.
+	short := "hello world"
+	require.Equal(t, short, sampleOutput(short))
+}
+
+func TestDetectRawHTMLEntities_TruncatesLongMatchInMessage(t *testing.T) {
+	// Regex is bounded to 10 digits so this shouldn't trigger in
+	// practice, but defend against future regex broadening: warning
+	// message must never embed an unbounded match string.
+	//
+	// Construct an entity at the upper regex bound (10 digits).
+	out := "text before " + "&#9999999999;" + " text after"
+	msg := detectRawHTMLEntities(out, nil)
+	require.NotEmpty(t, msg)
+	require.LessOrEqual(t, len(msg), 200, "warning message should stay bounded regardless of match length")
+}
diff --git a/skills/printing-press/SKILL.md b/skills/printing-press/SKILL.md
index 71cffebe..8c7e5f06 100644
--- a/skills/printing-press/SKILL.md
+++ b/skills/printing-press/SKILL.md
@@ -1724,6 +1724,71 @@ A template-level check would require every possible semantic mismatch to be patt
 
 The agent can't verify runtime behavior without running commands; stick to help-text and source-based claims. For runtime-behavior claims (e.g., "returns 5 matching recipes"), Phase 5 dogfood is the right gate.
 
+## Phase 4.85: Agentic Output Review
+
+**Runs after Phase 4.8, before Phase 5.** Phase 4.8 reviews SKILL.md prose against the shipped CLI. Phase 4.85 reviews the CLI's **actual command output** for plausibility — the class of bug rule-based checks can't encode:
+
+- Substring-match results that coincidentally contain the query but don't match semantically (e.g., a query matches a substring of a larger unrelated term)
+- Aggregation commands silently dropping sources when only some of the requested N come back
+- Ranking or sort commands returning top-N results that aren't plausibly the best for the query (broken weights, extractor fallbacks)
+- URLs in output pointing at category index pages, feed endpoints, or random-selector routes rather than canonical content permalinks
+- Format bugs that live-check's rule-based layer doesn't catch (mojibake, inconsistent pluralization, truncated/wrapped cell content)
+
+These bugs are typically surfaced by 5 minutes of hands-on testing but slip past existing dogfood, verify, and the rule-based `scorecard --live-check` rules — only a human-in-the-loop pattern-matcher finds them. Phase 4.85 is that loop. A concrete case history that motivated this phase: `docs/retros/2026-04-13-recipe-goat-retro.md`.
+
+**Wave B rollout policy (first 2 weeks):** all findings from this phase are surfaced as **warnings**, not blockers. Shipcheck does not fail on Phase 4.85 findings. The goal of Wave B is to calibrate false-positive rates across domains (transactional APIs like Stripe, document stores like Notion, scraping CLIs like recipe-goat) before Wave C flips errors to blocking.
+
+### Dispatch
+
+Use the Agent tool (general-purpose) with this prompt contract:
+
+> Review the sampled outputs from the shipped CLI at `$CLI_WORK_DIR`. You have these ground-truth sources:
+>
+> - Sampled command output: run `printing-press scorecard --dir $CLI_WORK_DIR --live-check --json` and read the `live_check.features[]` array. Each entry has the command, example invocation, actual stdout, the pass/fail reason, and a `warnings` array (populated by rule-based checks like the raw-HTML-entity detector).
+> - `$CLI_WORK_DIR/research.json` `novel_features` (planned behavior per feature) and `novel_features_built` (verified built commands).
+> - The CLI binary at `$CLI_WORK_DIR/<cli-name>-pp-cli` — you may invoke additional commands to gather more output when a finding needs verification.
+>
+> For each of these checks, report findings under 50 words each. Only report issues a human user would notice in 5 minutes of hands-on testing — not every edge case a thorough QA pass might find:
+>
+> 1. **Output matches query intent.** For sampled novel features with a query argument, does the output contain results clearly related to the query? Watch for results that coincidentally contain the query as a substring of an unrelated term, or fallback behavior where the extractor failed and returned adjacent (not matching) content.
+> 2. **No obvious format bugs.** Does the output contain raw HTML entities, mojibake (question marks or replacement chars in titles), or malformed URLs (pointing at category index pages, feed endpoints, or random-selector routes rather than canonical content permalinks)? Rule-based live-check catches numeric entities; this layer catches the broader class.
+> 3. **Aggregation commands show all requested sources.** For commands with a `--source`/`--site`/`--region` CSV flag: if the user requested N sources, does output show N, or does stderr explain the missing ones? Silent drops of failed sources are a top failure mode for fan-out commands.
+> 4. **Result ordering/ranking makes sense.** For commands that claim to rank or sort, does the top result look plausibly best given the query? Watch for broken score weights, off-by-one sort bugs, and silent fallback to recency when relevance computation fails.
+>
+> Return a list of findings. For each: check name, severity (`warning` in Wave B; `error` reserved for Wave C), one-line description, one-sentence fix suggestion. If the CLI passes all four checks, return "PASS — no findings."
+
+### Gate
+
+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.
+- The user decides case by case whether to fix before shipping.
+
+**Non-interactive contract (CI, cron, batch regeneration):**
+
+- If stdout is not a TTY, findings default to fail-open-with-log: recorded in the scorecard, shipcheck proceeds without prompting.
+- Reviewer crashes (timeout, agent-budget exhaustion) map to `SKIP` status with detail in the scorecard — shipcheck treats as informational, not blocking.
+- No `--auto-approve-warnings` flag yet. The policy is already "warnings don't block" in Wave B, so the flag has no effect to gate.
+
+Wave C (separate future PR) will flip `error`-severity findings to blocking after calibration data across the library shows false-positive rate below 10%.
+
+### 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.
+
+### Why agentic vs template-only
+
+Output-plausibility questions are not pattern-matchable against source. Rule-based live-check rules cover what regexes can (numeric HTML entities, query-token absence). Everything else — "are these substitution results plausibly correct for the query?", "does the top search result look related?" — is an LLM-shaped question. The token cost is bounded (once per run, not per command) and the catch rate against the bug classes that motivated this phase (see `docs/retros/2026-04-13-recipe-goat-retro.md` for a concrete case) justifies the dispatch.
+
+### Known blind spots
+
+- Can't verify numeric accuracy (prices, ratings, rankings vs ground-truth). If the CLI says a recipe has 4.8 stars and it actually has 4.2, Phase 4.85 won't catch it.
+- Can't detect data-freshness issues (recipe published 2019 vs 2024). These need live comparison against authoritative sources.
+- Can't judge subjective preferences ("is this the *best* recipe for chocolate chip cookies?").
+- Sampled outputs only — covers the commands in `live_check.features[]`. Full command-tree coverage belongs in Phase 5 dogfood.
+- Non-English output: the reviewer's query-intent check assumes English-language query/output. For non-English CLIs, calibrate the prompt separately.
+
 ## Phase 5: Dogfood Testing
 
 **MANDATORY when an API key is available. Do NOT skip or shortcut this phase.**

← 65dacc25 feat(cli): machine-output-verification Wave A — cliutil pack  ·  back to Cli Printing Press  ·  fix(skills): Phase 4.85 prompt refinements from calibration 6f8b0c7d →