---
version: "1.0.0"
evaluation: programmatic
agent: claude-code
model: claude-sonnet-4-6
model_provider: anthropic
snapshot: python312-uv
# Operation-aware headline deliverable: an `audit` run surfaces audit_report.md;
# a `polish` run surfaces the cleaned-up polished.html. Only files actually
# written by the chosen operation are surfaced, in declaration order.
primary_outputs:
- polished.html
- audit_report.md
origin:
url: "https://github.com/pbakaus/impeccable"
source_host: "github.com"
source_title: "impeccable — production-grade frontend design skill"
imported_at: "2026-06-07T00:00:00Z"
imported_by: "hand-authored@jetty (de-slop capability)"
license: "Apache-2.0"
attribution:
collection_or_org: "pbakaus"
skill_name: "impeccable"
confidence: "high"
# Provenance carried forward from the upstream NOTICE:
notice: >-
impeccable © 2025-2026 Paul Bakaus, Apache-2.0. Builds on Anthropic's
frontend-design skill (Apache-2.0, © 2025 Anthropic, PBC). Typography
reference incorporates additions from ehmo's typecraft-guide-skill.
secrets: {}
---
# Impeccable — De-slop a UI — Agent Runbook
## Objective
Detect and remove **AI-slop** from a frontend artifact — the visual and copy tells
that make an interface read as machine-generated: purple→cyan gradients, gradient
text, Inter/Roboto everywhere, side-tab accent borders, nested cards, dark-mode
glows, bounce easing, broken/placeholder images, and SaaS buzzword copy. This
runbook wraps the deterministic detector that ships with [Paul Bakaus's
`impeccable`](https://github.com/pbakaus/impeccable) skill (`impeccable detect`) and
uses it as both the **finder** and the **grader**: it scans an HTML/CSS/JSX file (or
directory, or live URL), reports every anti-pattern, and — in `polish` mode —
rewrites the artifact to remove them, then re-scans to **prove** the page now scans
clean. Because the same tool that finds the slop also verifies the fix, evaluation is
fully programmatic: the polish pass succeeds only when a re-scan exits `0` (zero
anti-patterns) with no new findings introduced. Worked examples — a sloppy SaaS hero
audited, then the same hero and a pricing section polished to zero — ship in
`examples/`.
## Requested Operation (READ FIRST)
**Operation:** {{operation}}
Perform **only** this operation:
- **`audit`** — scan the target and produce a findings report. **Change nothing.**
Deliverables: `audit_report.json` + `audit_report.md`.
- **`polish`** — scan, then edit the target to remove every finding, then re-scan to
confirm zero. Deliverables: the cleaned artifact (`polished.html` for a single
file) + `audit_before.json` + `audit_after.json` + `before_after.md`.
If no operation is given, default to **`audit`** (the safe, read-only path).
## REQUIRED OUTPUT FILES
**Always** write these two to `{{results_dir}}` (every operation):
| File | Description |
|------|-------------|
| `{{results_dir}}/summary.md` | Executive summary: operation, target, finding counts, before/after, any caveats |
| `{{results_dir}}/validation_report.json` | Structured validation with `stages`, `results`, and `overall_passed` |
**Then** write **only** the deliverable(s) for the requested operation:
| Operation | Deliverable(s) in `{{results_dir}}` |
|-----------|-------------------------------------|
| `audit` | `audit_report.json` (raw findings + counts) **and** `audit_report.md` (human report) |
| `polish` | `polished.html` (the cleaned artifact) **and** `audit_before.json` **and** `audit_after.json` **and** `before_after.md` |
The task is NOT complete until `summary.md`, `validation_report.json`, and the
operation's deliverable(s) all exist and are non-empty. **Do not** run the other
operation's work — an `audit` request must NOT modify the target or emit a
`polished.html`.
## Parameters
| Parameter | Template Variable | Default | Description |
|-----------|-------------------|---------|-------------|
| Results directory | `{{results_dir}}` | `/app/results` (Jetty) / `./results` (local) | Output directory for all results |
| Target | `{{target}}` | *(required)* | The artifact to de-slop: an uploaded HTML/CSS/JSX/TSX file (lands in `/app/assets/`), a directory, or an `http(s)://` URL |
| Operation | `{{operation}}` | `audit` | `audit` (report only) or `polish` (fix + re-scan to zero) |
| Provider tells | `{{provider_tells}}` | `none` | `none`, `gpt`, or `gemini` — also report that provider's signature tells (off by default; passed through to `impeccable detect --gpt/--gemini`) |
| Max polish rounds | `{{max_polish_rounds}}` | `3` | Max fix→re-scan iterations before stopping (polish only) |
| Output basename | `{{output_basename}}` | `polished` | Label for the run (the primary output is always `polished.html`) |
## Dependencies
| Dependency | Type | Required | Description |
|------------|------|----------|-------------|
| Node.js ≥ 24 | Runtime | Yes | `impeccable` requires Node ≥ 24 (`engines.node >=24`). Step 1 verifies and installs if absent |
| `impeccable` | npm package | Yes | The detector + skill. Resolved via `npx impeccable@latest`; clone fallback pinned to a commit SHA (Step 1) |
| `npx` / `npm` | CLI | Yes | Resolve and run `impeccable detect` |
| `git` | CLI | Conditional | Only for the clone fallback when the npm registry is unreachable |
| Chromium / Puppeteer | Runtime | Conditional | Only for **URL** targets (browser-render scan); file/dir targets use the static-HTML + regex engines and need no browser |
---
## Step 1: Environment Setup
Ensure Node ≥ 24, resolve the `impeccable` CLI, and resolve the target.
```bash
set -e
mkdir -p {{results_dir}}
# --- Node ≥ 24 ---
NODE_MAJOR=$(node -v 2>/dev/null | sed -E 's/^v([0-9]+).*/\1/' || echo 0)
if [ "${NODE_MAJOR:-0}" -lt 24 ]; then
echo "Node ${NODE_MAJOR} < 24 — installing Node 24"
curl -fsSL https://deb.nodesource.com/setup_24.x | bash - >/dev/null 2>&1 && apt-get install -y nodejs >/dev/null 2>&1 \
|| (curl -fsSL https://fnm.vercel.app/install | bash && export PATH="$HOME/.local/share/fnm:$PATH" && eval "$(fnm env)" && fnm install 24 && fnm use 24)
fi
echo "node: $(node -v)"
# --- Resolve the impeccable detector ---
# Primary: run straight from npm. Verify it answers before relying on it.
if npx --yes impeccable@latest detect --help >/dev/null 2>&1; then
IMPECCABLE="npx --yes impeccable@latest detect"
else
# Fallback: clone the repo pinned to a known-good SHA and run the bundled CLI.
git clone --depth 1 https://github.com/pbakaus/impeccable /tmp/impeccable
git -C /tmp/impeccable fetch --depth 1 origin 1aedbcf538e3fa6694ccbf00294cc18e59ba1f21 2>/dev/null || true
IMPECCABLE="node /tmp/impeccable/cli/bin/cli.js detect"
fi
echo "detector: $IMPECCABLE"
echo "$IMPECCABLE" > {{results_dir}}/.impeccable_cmd # reused by later steps
```
**Resolve the target** (`{{target}}`):
```bash
# Uploaded files land in /app/assets/. Prefer an explicit {{target}}; otherwise
# pick the first HTML-ish asset.
TARGET="{{target}}"
case "$TARGET" in
http://*|https://*) : ;; # URL — scanned in browser mode
"" ) # nothing passed — discover an upload
TARGET=$(ls /app/assets/*.html /app/assets/*.htm 2>/dev/null | head -1)
[ -z "$TARGET" ] && TARGET=$(ls /app/assets/*.{css,jsx,tsx,vue,svelte} 2>/dev/null | head -1) ;;
/*) : ;; # absolute path
*) [ -e "/app/assets/$TARGET" ] && TARGET="/app/assets/$TARGET" ;;
esac
[ -z "$TARGET" ] && { echo "ERROR: no target resolved (pass a file, dir, or URL)"; exit 1; }
echo "$TARGET" > {{results_dir}}/.target
echo "target: $TARGET"
```
If no target can be resolved, fail fast and write `validation_report.json` with
`stages[0].passed=false` naming the missing input.
---
## Step 2: Baseline Scan (every operation)
Run the detector once and capture both the findings JSON **and the exit code** —
`impeccable detect` exits **`2`** when anti-patterns are present and **`0`** when the
target is clean. That exit code is the ground truth this runbook is built on.
```bash
IMPECCABLE=$(cat {{results_dir}}/.impeccable_cmd)
TARGET=$(cat {{results_dir}}/.target)
PROV=""
case "{{provider_tells}}" in gpt) PROV="--gpt" ;; gemini) PROV="--gemini" ;; esac
set +e
$IMPECCABLE --json $PROV "$TARGET" > {{results_dir}}/audit_before.json
SCAN_EXIT=$?
set -e
echo "$SCAN_EXIT" > {{results_dir}}/.before_exit
COUNT=$(python3 -c "import json;print(len(json.load(open('{{results_dir}}/audit_before.json'))))")
echo "baseline: $COUNT anti-pattern(s), detector exit $SCAN_EXIT"
```
Each finding has the shape:
```json
{ "antipattern": "side-tab", "name": "Side-tab accent border",
"description": "Thick colored border on one side of a card …",
"severity": "warning", "file": "/app/assets/input.html",
"line": 36, "snippet": "border-left: 4px solid #7c3aed" }
```
---
## Step 3: Execute the Operation
### 3a — `audit` (report only; change nothing)
Group the findings by rule and category, then emit the two reports.
```python
import json, pathlib, collections
R = pathlib.Path("{{results_dir}}")
findings = json.loads((R / "audit_before.json").read_text())
exit_code = int((R / ".before_exit").read_text().strip())
SLOP = {"side-tab","border-accent-on-rounded","overused-font","single-font",
"flat-type-hierarchy","gradient-text","ai-color-palette","cream-palette",
"nested-cards","monotonous-spacing","bounce-easing","dark-glow",
"icon-tile-stack","italic-serif-display","hero-eyebrow-chip",
"repeated-section-kickers","numbered-section-markers","em-dash-overuse",
"marketing-buzzword","aphoristic-cadence","oversized-h1","extreme-negative-tracking"}
cat = lambda r: "slop" if r in SLOP else "quality"
by_rule = dict(sorted(collections.Counter(f["antipattern"] for f in findings).items()))
by_cat = dict(sorted(collections.Counter(cat(f["antipattern"]) for f in findings).items()))
(R / "audit_report.json").write_text(json.dumps({
"tool": "impeccable detect",
"source": "https://github.com/pbakaus/impeccable",
"target": str((R / ".target").read_text().strip()),
"exit_code": exit_code,
"finding_count": len(findings),
"by_category": by_cat,
"by_rule": by_rule,
"findings": findings,
}, indent=2, ensure_ascii=False) + "\n")
verdict = ("reads as AI-generated — see tells" if by_cat.get("slop") else
("quality issues only" if findings else "clean — no anti-patterns detected"))
lines = [f"# Impeccable audit — `{(R/'.target').read_text().strip()}`", "",
f"**Findings:** {len(findings)} "
f"({by_cat.get('slop',0)} AI-slop, {by_cat.get('quality',0)} quality) ",
f"**Verdict:** {verdict}", "",
"| # | Rule | Category | Line | Snippet |",
"|---|------|----------|------|---------|"]
for i, f in enumerate(findings, 1):
snip = str(f.get("snippet","")).replace("|","\\|")[:48]
lines.append(f"| {i} | `{f['antipattern']}` | {cat(f['antipattern'])} | {f.get('line') or '—'} | `{snip}` |")
for i, f in enumerate(findings, 1):
lines += ["", f"### {i}. {f['name']} (`{f['antipattern']}`)",
f"- **Location:** line {f.get('line') or '—'} — `{f.get('snippet','')}`",
f"- **Why it matters:** {f['description']}"]
if not findings:
lines.append("\n_No anti-patterns detected. The detector exited 0._")
(R / "audit_report.md").write_text("\n".join(lines) + "\n")
print(f"audit: {len(findings)} findings written")
```
Then skip to Step 5 (no fixes in `audit` mode).
### 3b — `polish` (fix → re-scan, max `{{max_polish_rounds}}` rounds)
Read the artifact and **fix every finding**, guided by the rule's `description` and
the **De-slop fix reference** below. Write the cleaned file to
`{{results_dir}}/polished.html`, then re-scan it. Repeat until the re-scan exits `0`
or `{{max_polish_rounds}}` rounds are spent.
> Fix the *root cause*, not the symptom. Removing the gradient text by deleting the
> headline is not a fix. Replace slop with a deliberate, production-grade choice that
> a designer would defend (see the upstream skill's design rules).
**De-slop fix reference** (rule → what the detector wants):
| Rule | The tell | The fix |
|------|----------|---------|
| `overused-font` / `single-font` | Inter / Roboto / Geist / Fraunces / Plus Jakarta Sans / Space Grotesk, or one family for everything | Pair a distinctive display face with a refined body face on a **contrast axis** (serif + humanist sans). Cap at 3 families |
| `gradient-text` | `background-clip:text` + gradient on a heading/metric | Solid color text. Reserve color for intent, not decoration |
| `ai-color-palette` / `cream-palette` | Purple/violet gradient, cyan-on-dark, or reflexive cream/beige surface | A deliberate palette built from one seed hue (OKLCH); bg/surface/ink/muted/accent that belong together |
| `dark-glow` | Dark bg + colored `box-shadow` glow | Subtle, purposeful elevation (neutral, low-spread) — or drop the dark theme |
| `side-tab` / `border-accent-on-rounded` | Thick colored border on one side of a card | Remove the accent stripe; carry emphasis with type, spacing, or a full subtle border |
| `nested-cards` | Card inside a card | Flatten — use spacing, dividers, and typography instead of nested containers |
| `icon-tile-stack` | Rounded-square icon tile stacked above every heading | Side-by-side icon + heading, or let the icon sit in flow without its own tile |
| `hero-eyebrow-chip` / `repeated-section-kickers` | Tiny uppercase tracked label above the hero / repeated as section scaffolding | Drop the eyebrow; fold the kicker into the headline or real structure |
| `bounce-easing` | `cubic-bezier` with negative control points / elastic | Exponential ease-out (`cubic-bezier(0.22,1,0.36,1)` etc.). Real objects decelerate smoothly |
| `marketing-buzzword` | streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge | A concrete verb + noun that says what the product literally does |
| `em-dash-overuse` | >2 em-dashes in body copy | Commas, colons, periods, parentheses |
| `oversized-h1` / `extreme-negative-tracking` | Long headline at display size; letter-spacing crushed past legibility | Shorter headline or smaller size; tracking floor ≥ −0.04em |
| `broken-image` | `
` with empty/missing/placeholder `src` | Real asset, generated/inline SVG, or remove the tag |
| `gray-on-color` | Gray text on a colored background | A darker shade of the background's own hue, or a transparency of the text color |
| `tight-leading` / `line-length` / `tiny-text` | line-height <1.3, lines >~80ch, body <12px | 1.5–1.7 leading, 65–75ch measure, ≥14px body |
```bash
IMPECCABLE=$(cat {{results_dir}}/.impeccable_cmd)
PROV=""; case "{{provider_tells}}" in gpt) PROV="--gpt";; gemini) PROV="--gemini";; esac
# After EACH editing round, re-scan the cleaned file:
set +e
$IMPECCABLE --json $PROV {{results_dir}}/polished.html > {{results_dir}}/audit_after.json
AFTER_EXIT=$?
set -e
echo "$AFTER_EXIT" > {{results_dir}}/.after_exit
AFTER_COUNT=$(python3 -c "import json;print(len(json.load(open('{{results_dir}}/audit_after.json'))))")
echo "round result: $AFTER_COUNT remaining, exit $AFTER_EXIT"
# Loop back to fixing if AFTER_EXIT != 0 and rounds remain; otherwise continue.
```
When the loop ends, write `before_after.md` (per-rule before/after table, regressions
introduced, and the final verdict). The polish **passes** only if `AFTER_EXIT == 0`
and no new rule ids appear in `audit_after.json` that were not in `audit_before.json`.
---
## Step 4: Iterate on Errors (max 3 rounds)
If a step raised an error, the detector failed to run, or `polish` did not reach a
clean re-scan:
1. Read the specific failure.
2. Apply the relevant fix from Common Fixes.
3. Re-run the affected step.
4. Repeat up to **3 rounds total**, then record the best result honestly in
`summary.md` with `overall_passed` reflecting reality.
### Common Fixes
| Issue | Fix |
|-------|-----|
| `node: command not found` or Node < 24 | Re-run the Node-24 install block in Step 1; `impeccable` needs `engines.node >=24` |
| `npx` can't reach the registry | Use the clone fallback: `git clone … && node /tmp/impeccable/cli/bin/cli.js detect` (pinned SHA in Step 1) |
| Detector prints nothing on a clean file | That's correct — clean files print `[]` and exit `0`. Don't treat empty output as an error |
| URL scan hangs / no Chromium | Browser mode needs Puppeteer/Chromium. For a file target, scan the file directly (static engine, no browser) |
| Polish keeps re-flagging `marketing-buzzword` | The detector counts buzzword phrases across the whole document; rewrite **all** instances, not just the hero |
| `single-font` persists after swapping `overused-font` | You replaced one overused face with one non-overused face — still a single family. Add a second family on a contrast axis |
| Re-scan introduces a NEW rule (e.g. you added a cream surface) | A fix created fresh slop. Treat any new rule id in `audit_after.json` as a regression and fix it before declaring success |
| Findings reference a linked CSS file | The static-HTML engine follows linked CSS; edit the linked file, not just the inline `