← 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
M agents/polish-worker.mdM internal/cli/scorecard.goM internal/pipeline/live_check.goM internal/pipeline/live_check_test.goM skills/printing-press/SKILL.md
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
+// (') and hex ('). Named entities (&, ", <) are
+// intentionally excluded in Wave B — they false-positive on legitimate JSON
+// strings ("AT&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
+// "�...;" 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's Chocolate Chip Cookies"},
+ {"decimal typographic apostrophe", "Ben’s Pizza"},
+ {"decimal mid-line", "Row 1\nRow 2 with " in it\nRow 3"},
+ // Hex numeric entities are equally common — APIs that encode
+ // apostrophes as ' should trip the same check. Decimal-only
+ // regex was an oversight flagged by Wave B ce:review.
+ {"hex lowercase", "Ben's Pizza"},
+ {"hex uppercase x", "foo'bar"},
+ {"hex multi-char", "quote’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'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'y"}`, nil))
+ require.Empty(t, detectRawHTMLEntities(`[{"title":"x'y"}]`, nil))
+ // Leading whitespace before the JSON start still counts as JSON mode.
+ require.Empty(t, detectRawHTMLEntities(" \n{\"title\":\"x'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&T" and README prose. Wave C can
+ // revisit after calibrating on the library.
+ require.Empty(t, detectRawHTMLEntities("AT&T", nil))
+ require.Empty(t, detectRawHTMLEntities("foo "bar" baz", nil))
+ require.Empty(t, detectRawHTMLEntities("less than: < greater than: >", 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'\''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 " + "�" + " 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 →