L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Design et création · SKILL.md

visual-arbiter

visual-arbiter

Use when bob needs a mechanical verdict on whether a built UI product satisfies a frozen design-skeleton as part of the UI-INTEGRATED → UI-VERIFIED transition. Invoked as a subprocess (not a Claude Code Agent). Renders the built product with puppeteer at each required breakpoint, measures bbox / computed styles / interaction wiring against the skeleton, and…

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: visual-arbiter description: "Use when bob needs a mechanical verdict on whether a built UI product satisfies a frozen design-skeleton as part of the UI-INTEGRATED → UI-VERIFIED transition. Invoked as a subprocess (not a Claude Code Agent). Renders the built product with puppeteer at each required breakpoint, measures bbox / computed styles / interaction wiring against the skeleton, and emits ONE JSON visual-verdict.v1 object on stdout. Pure-Python decision path — NO LLM subprocess (CB4: bob is sole writer of .design-ledger/visual-verdicts/)." family: visual disambiguation: Mechanical built-UI-vs-frozen-skeleton verdict for the UI-VERIFIED transition. Evidence-bundle scoring is verification-arbiter; classifying a reject as drift is design-drift-arbiter.

visual-arbiter

Mechanical visual verifier invoked by bob at the UI-INTEGRATED → UI-VERIFIED gate (ecosystem-keystone §2.6, §2.9, §5.5, §5.6). Mirrors the CLI shape of verification-arbiter (S027) exactly, but the decision path is pure Python — puppeteer is used only for DOM measurement, never for judgment.

When to invoke

From bob at the UI-INTEGRATED → UI-VERIFIED transition (ecosystem-keystone HARD-RULE 6). Runs in sequence: visual-arbiter first; if it rejects with micro-drift only, bob may then invoke design-drift-arbiter for auto-approval. Either arm rejecting blocks UI-VERIFIED.

Prerequisites:

  • Frozen design-skeleton.v1 (signed index.yaml + per-screen yamls in .design-ledger/skeletons/)
  • Built product reachable via file:// or http(s):// URL
  • claims.open_visual_verification_request(...) must have opened a request for the produced tuple BEFORE invoking the arbiter

Invocation

Subprocess, not an Agent. Bob spawns it as:

python3 ~/.claude/skills/_meta/visual_arbiter_spawn.py \
  <skeleton_path> <skeleton_hash> <request_id> <attempt_id> \
  <prior_state_version> <built_product_url> <product_hash> \
  <inventory_hash> <runner_version> <rubric_version>

All 10 arguments are positional and required. Bob captures stdout; the arbiter writes nothing to disk.

Input tuple (8 fields echoed back verbatim — §2.9)

FieldFormatPurpose
request_id32-hexCausal linkage to the open visual-verification request
attempt_idnon-empty stringDistinguishes retries against the same component
prior_state_versionnon-empty stringPins verdict to ledger state at request-open time
skeleton_hash64-hex (sha256)Content hash of the frozen skeleton bundle
product_hash64-hex (sha256)Built product two-layer hash (source dir + rendered HTML)
inventory_hash64-hex (sha256)env-adoption inventory at verification time
runner_versionnon-empty stringtrusted_runner version that produced the bundle
rubric_versionnon-empty stringArbiter's evaluation rubric version (see below)

Tuple mismatch on any field → bob's claims.consume_visual_verdict returns rejected_tuple_mismatch and discards the verdict.

Output (stdout only)

One JSON object matching visual-verdict.v1 (design §2.9). Top-level verdict:

  • pass — every element at every required breakpoint verified; no fail-status element verdicts; no blocker concerns
  • warn — every element that WAS measured verified, but ≥1 warning-severity concern (e.g. fonts.ready >2s, or a partial measurement — see measurement.outcome)
  • reject — ≥1 element_verdict has status: fail (missing_from_dom, bbox_drift beyond tolerance, token_mismatch, or dead_handler)
  • AUDIT_UNAVAILABLE — reserved for the spawn script on environmental failure (chrome crash, schema violation). Bob MUST escalate to user; NEVER auto-approve.

measurement block (v1.1.0). Every verdict now carries measurement: {outcome, breakpoints_expected, breakpoints_measured, errors[]}, with outcome one of MEASURED / PARTIAL / INCONCLUSIVE. Read it before trusting coverage: coverage counts DECLARED elements, measurement states how much of the page was actually reachable. A pass over a PARTIAL measurement is the one claim this block exists to prevent, and the arbiter will not emit it.

Element verdict fields (§2.6):

  • missing_from_dom — selector resolves to nothing at the given breakpoint
  • bbox_drift_px: {x, y, w, h} — per-dim signed drift; compared against tolerance_for(element) = min(must_satisfy.tolerance_px, spacing.unit_px / 2)
  • token_mismatch: {field, computed, expected} — hardcoded hex/rgb found in inline style (or computed value fails var(--...) indirection)
  • dead_handler: [{event, binds_to, reason}] — interaction declared but no listener fires (and uri.resolve confirms target unreachable if binds_to is set)
  • interactions_ok: [{event, binds_to, status}] — per-interaction success

Overall:

  • pass iff every element verdict is pass AND no blocker concerns AND measurement.outcome == MEASURED
  • reject iff any element verdict is fail — a real failure outranks an incomplete measurement
  • warn iff all pass but warnings exist, including a partial_measurement concern

Exit codes

CodeMeaning
0Valid schema-compliant verdict emitted on stdout; all 8 tuple fields echoed correctly
3Environmental / usage error (wrong argv count, hash not 64-hex, unreadable skeleton)
4AUDIT_UNAVAILABLE — measurement subprocess crashed, timed out, chrome unreachable, or produced unparseable output

Design §2.6 / §5.6: arbiter never writes .design-ledger/visual-verdicts/. Bob is the sole ledger writer (CB4 preserved).

Decision rubric (v1.1.0 — pure-Python, no LLM)

Source of truth: this SKILL.md plus visual_arbiter_spawn.py, both hashed. Bump the rubric version (semver) when changes affect verdict semantics.

v1.1.0 (S074, #218) — partial measurement can no longer read as clean. visual_arbiter_measure.mjs now emits outcome / breakpoints_expected / breakpoints_measured; the arbiter carries them into verdict.measurement and, when the outcome is not MEASURED, adds a partial_measurement warning concern that downgrades pass to warn. Previously the degradation was recorded only as telemetry, so a verdict built from 1 of 4 breakpoints was byte-identical to one built on all 4. The measure script's exit code is deliberately unchanged — visual_arbiter_spawn.py maps any non-zero to AUDIT_UNAVAILABLE, which would flatten "chrome crashed" and "3 of 4 breakpoints measured" into one outcome. The payload is the right lever; the exit code is not.

v1.0.0 rules:

  1. Per-element at every required breakpoint. For each element in each screen's elements[], iterate must_satisfy.required_breakpoints. If the element declares null at a breakpoint, it is correctly hidden there — no verification needed. Otherwise:

  2. bbox check. Measured getBoundingClientRect() compared to declared bbox.<breakpoint>. Per-dim drift = measured - declared. Element passes bbox iff every abs(drift[dim]) <= tolerance_px where tolerance_px = min(must_satisfy.tolerance_px, spacing.unit_px // 2) — taken directly from the skeleton's own declarations; NEVER a hardcoded constant.

  3. Token back-resolution. For each tokens_used mapping, the element's computed styles must go through a var(--<token-name>) reference (e.g. var(--accent-sun) for $color.accent.sun) OR at minimum NOT contain a hardcoded hex/rgb in its inline style attribute. Hardcoded hex in inline style → token_mismatch fail with {field, expected, computed}.

  4. Interaction wiring. For each declared interactions[].event:

    • visual_only: true — no binding required; pass automatic
    • binds_to == null and NOT visual_only → schema violation → dead_handler
    • binds_to set → arbiter fires the event via dispatchEvent, then checks (a) inline on* attribute, (b) data-arbiter-wired="true" opt-in flag, OR (c) an el.__arbiter_handler_ran flag the handler itself set. None of those → dead_handler fail
    • If uri module available: binds_to URI resolved via uri.resolve(binds_to, project_root); UriError → dead_handler with reason binds_to unresolvable
  5. Font-load concern. fonts.ready + 300ms wait is mandatory. Wait >2000ms → warning concern (does not block pass but downgrades to warn). Also emits claude_observe('external_tool_slow', ...) fail-open.

  6. Chrome crash handling. Measurement subprocess non-zero exit or timeout → emit external_tool_fail / external_tool_slow observation (fail-open) and exit with code 4 + AUDIT_UNAVAILABLE.

  7. Tuple echo discipline. All 8 tuple fields echoed verbatim — bob's claims.consume_visual_verdict rejects on any mismatch.

Env vars

VariableDefaultPurpose
VISUAL_ARBITER_CHROME_PATH/bin/google-chromeChrome/Chromium binary (must match what puppeteer-core can drive)
VISUAL_ARBITER_NODE_BINnodeNode binary for the measurement subprocess
VISUAL_ARBITER_TIMEOUT_S180Measurement subprocess timeout (chrome launch + per-bp measurement)

Relationship to other verifiers

ArbiterScopeDecision engine
verification-arbiter (S027)Functional — test coverage against frozen planCold Claude + coverage scoring
visual-arbiter (this)Visual — bbox + tokens + interactions against frozen skeletonPure Python; puppeteer = measurement only
design-drift-arbiter (WP-10)Micro-drift auto-approval post-rejectionPure Python profile rules

Visual-arbiter runs before design-drift-arbiter. bob invokes drift-arbiter only if visual-arbiter rejected with micro-drift-only reasons (bbox within expanded profile tolerance OR token swap within same family). See ecosystem-keystone §5.6 / HARD-RULE 6.

Scope boundaries

In scopeOut of scope
Measuring bbox/tokens/interactions at each breakpointCreating/amending the skeleton (visual-architect owns)
Mechanical comparison against declared tolerancesJudgment calls about what a user "really wants" — v2 deferral (§1.3)
8-field tuple echoConsuming the verdict or writing visual-verdicts/ (bob owns via claims.consume_visual_verdict)
Emitting AUDIT_UNAVAILABLE via exit 4Model-emitted AUDIT_UNAVAILABLE (there is no model in the verdict path)
fail-open claude_observe for chrome slow/failBeing authoritative for observability — that's observability skill (S026)

References

  • Design doc: docs/plans/2026-04-23-ecosystem-keystone-design.md (§2.6 pure-Python arbiter, §2.9 visual-verdict.v1 schema, §5.5 claims.consume_visual_verdict, §5.6 HARD-RULE 6, §5.9 external-wrapper observations)
  • Contract map: progress/contract-map.yaml — component visual-arbiter (TS-VAR-01..05)
  • Sibling subprocess (S027): ~/.claude/skills/_meta/verification_arbiter_spawn.py
  • Binary: ~/.claude/skills/_meta/visual_arbiter_spawn.py
  • Measurement driver: ~/.claude/skills/_meta/visual_arbiter_measure.mjs
  • Tests: ~/.claude/skills/visual-arbiter/tests/test_visual_arbiter.py (TS-VAR-01..05)
Skills enregistrées