| name | web-audit |
| description | Audit a URL for modern web quality and (optionally) fix it. This is a FULLY AGENTIC audit. YOU (the model) gather multi-modal evidence about the page with the generic evidence primitives, decide for yourself which tools to run (Lighthouse, axe, your own ad-hoc static tests), reason over that evidence, and judge every principle, then emit a findings report. There are no hard-coded checks and no fast path. The principles are the spec, this skill is the method, and you supply all the intelligence. Use when asked to web-audit, UX-audit, uplift, modernise, or quality-audit a site. |
Web audit (fully agentic)
Audit $ARGUMENTS: a URL, plus optional flags:
--out <dir> - report directory (default reports/<host>/).
--source <dir> - path to the site's local source, so you can read the
authored HTML/CSS/JS, not just the rendered output.
--fix - after auditing, apply fixes to local source and re-audit (requires
--source). Without --fix, this is report mode (critique only).
--findings <path> - a pre-aggregated report.json to start fix mode from,
so you can skip the baseline audit and go straight to the hill-climb.
--max-iterations <n> - cap on fix-mode passes (default ~4); stop earlier if
no outstanding issues remain.
--expected <file> - a ground-truth file to self-score against (used for the
playground eval).
The two ways to run this (the DEFAULT is your own session)
- In your own agent session (DEFAULT, uses your subscription). You are
already a capable model in a session (Claude Code, Codex, Gemini, Antigravity,
Copilot, opencode). Just follow this file: run
/web-audit <url> for an audit,
or /web-audit <url> --source <dir> --fix for the hill-climb, doing the
reasoning yourself and calling node evidence/cli.mjs ... for evidence. No
headless runner, no claude -p, no extra API billing. This is the path an
individual should use.
- Headless / CI / batch (uses API tokens).
npm run audit/npm run fix
(or web-uplift audit/web-uplift fix) spawn an agent CLI in -p/exec
mode to drive this same skill unattended. That bills API tokens, so it is for
automation, not the default for a person. Same methodology either way.
The contract: you are the auditor, not a runner
There is no deterministic check runner and there are no fast paths in
this project. Nothing decides for you whether a principle passes. You decide,
by gathering evidence and reasoning over it. The repo gives you two declarative
inputs and one generic capability:
- Principles - knowledge/principles.json
in this repo, or
../knowledge/principles.json when this skill is installed
under .web-uplift/skill/:
the spec of what good looks like, as OUTCOMES. Seventeen principles: Una
Kravets' five modern-UX principles (respect-user-preferences,
implement-natural-interactions, provide-guided-navigation,
maximize-content-reduce-noise, adapt-to-the-form-factor);
support-core-task-success; two
widened/narrowed Lighthouse-dimension principles (be-inclusive - the former
be-accessible, widened beyond WCAG conformance; follow-best-practices -
narrowed so it is no longer the catch-all for security and forms); two
unchanged Lighthouse-dimension principles (be-fast-and-stable,
be-discoverable); six framework-derived principles
(be-private-and-secure, be-resilient, be-internationalised, be-trustworthy,
be-sustainable, be-agent-ready); and be-memory-efficient (derived from the
memory-tracer leak-audit methodology). Each check has a detectableVia HINT (may
MENTION candidate evidence/tools, MANDATES nothing), a guides list of
Modern Web Guidance ids and/or query strings, and may have references for
non-MWG standards, methods, or optional tools. These are declarative pointers
you consult to set the bar, not hard-coded tests. Each principle also has an
applicability block (expectation: default | contextual); see step 0 and
step 4. The full coverage map (all 137 mwg guides -> principles) and the
rationale for the set is in
docs/principles-analysis.md.
- Guidance - Modern Web Guidance via the
modern-web-guidance npm feed
(the how). See knowledge/guidance.md in
this repo, or ../knowledge/guidance.md when installed: search
to find the recommended approach, retrieve to get the fix detail. Pin the
catalog version in principles.json (guidanceCatalogVersion, currently
modern-web-guidance@0.0.172) unless web-uplift.json overrides it.
- Evidence primitives - evidence/cli.mjs: a
generic, judgement-free CLI you call to gather evidence. It launches the
system Chrome and drives it over raw CDP (chrome-remote-interface). It returns
data and artifacts; it never decides anything.
Non-negotiable atomic coverage contract
The checks in principles.json, not the 17 principle headings, are the audit
denominator. A broad principle judgement is never a substitute for executing
and recording each defined check. At the current catalog version that means 58
check outcomes per audited site; derive the number from the file rather than
hard-coding 58.
Before gathering evidence:
- Materialise a coverage manifest containing every expected
(principleId, checkId) pair from the exact principles.json used by the run.
Record its catalog version and checksum.
- Create one planned result slot for every pair. Never invent aliases,
aggregate several checks into a generic review, or replace a check with an
easier proxy.
- Decide the evidence method and target path/condition for each check. One
artifact may support several checks, but every check must point to the
evidence and reasoning that establishes its own outcome.
For every expected pair, emit exactly one checkOutcomes row with one of:
pass — direct evidence supports this check; absence of a finding is not
evidence and cannot produce a pass.
issues — direct evidence shows a failure, linked to finding IDs.
not-applicable — the check genuinely does not apply, with a check-specific
rationale. This does not mean untested, unavailable, or difficult.
opted-out — configuration explicitly excludes it, with the declared reason.
blocked — execution was attempted but an external condition prevented it,
with the attempted method and blocker.
not-run — execution did not happen, with a reason. This is incomplete work,
never N/A and never a pass.
A principle verdict is derived from its check outcomes:
- any
issues => principle issues;
- otherwise any
blocked or not-run => principle incomplete;
- otherwise all applicable checks
pass => principle pass;
- a principle is
not-applicable/opted-out only when all of its checks have
that status with valid reasons.
A run may be saved with blocked/not-run rows, but it is partial, not
completed. It must report exact executed/expected, blocked, not-run,
missing, unknown, and duplicate counts. Do not publish a score, pass
rate, “audited sites” count, or completed claim unless missing, unknown,
duplicate, blocked, and not-run are all zero. Run the report coverage validator
before generating summaries or scorecards. Validator failure is audit failure,
not a warning to suppress.
This contract applies equally to interactive, batch, CI, and delegated audits.
Prompts, runners, schemas, database importers, and UIs must preserve the exact
check IDs and outcomes. A scale-oriented runner that only gathers a subset is an
evidence/recon pass and must not call its output an audit.
The evidence primitives (your senses)
node evidence/cli.mjs <primitive> <url> [options]
Use whichever invocation resolves in your context (they run the SAME CLI):
- In this repo:
node evidence/cli.mjs <primitive> <url> [options].
- Per-project install (files vendored under
.web-uplift/):
node .web-uplift/evidence/cli.mjs <primitive> <url> [options].
- Global package install (e.g. this skill loaded from a pi package, so no
local copy exists):
web-uplift evidence <primitive> <url> [options], or
npx -y web-uplift evidence <primitive> <url> [options] if web-uplift is not
on PATH. This form works from any cwd and needs nothing vendored.
Pick the first that exists; the rest of this file writes node evidence/cli.mjs
for brevity, but the web-uplift evidence ... form is equivalent everywhere.
Primitives, all content- and tool-agnostic:
| Primitive | What it gives you | Key CDP |
|---|
screenshot | a PNG (full or --selector-clipped) under any emulated condition | Page.captureScreenshot |
video | an MP4 of an interaction window, frames assembled with ffmpeg; --interact "<js>" triggers the transition/animation | Page.startScreencast |
heap | a readable heap summary (types/constructors by size); --interact to exercise first, for leak hunting. Take a baseline and a post-interaction snapshot and compare retained growth (the memory-tracer methodology) to judge be-memory-efficient | HeapProfiler.takeHeapSnapshot |
layout | layout metrics, a CLS/layout-shift observer, long tasks, overflow at the current viewport | Page.getLayoutMetrics + observers |
dom | DOM, computed styles for --selector list, page HTML/CSS, and (--source <dir>) the local source files | DOM/CSS/Runtime |
evaluate | runs your own --expr "<js>" in the page: ad-hoc probes and static tests you write on the spot | Runtime.evaluate |
trace | a DevTools performance trace over the load (+ --interact): a devtools-loadable trace.json AND a compact *-summary.json (FCP/LCP, long tasks, total blocking time). Read the summary, never the raw trace | Tracing.start/end |
har | a valid HAR 1.2 of the network over the load (+ --interact/--duration; --bodies to include response bodies) AND a compact *-summary.json of network SIGNALS: totals + by-resource-type, first/third-party origins by bytes, render-blocking candidates (grounded in the CDP initiator + priority + renderBlockingStatus when exposed, each with a stated basis; confirm against the DOM), weight offenders (largest/slowest), and hygiene (uncompressed text, missing cache headers, redirects, HTTP errors). Read the HAR summary, never the raw HAR. Feeds be-fast-and-stable (request weight, render-blocking), be-sustainable (bytes over the wire), and be-private-and-secure (third parties) | Network domain |
discoverability | how much of the page a crawler that does NOT run JavaScript can see: fetches the RAW server HTML (a plain request, no JS) and diffs it against the rendered DOM, reporting coveragePct (share of rendered content words also present in the raw HTML), isJsShell, empty SPA mounts (#root/#app/#__next shipped empty), and whether the title/h1/meta-description survive without JS. Also captures a browser-view vs crawler-view screenshot pair (-rendered.png with JS on, -crawler.png with JS disabled) - for a shell site the crawler view is blank, the single most legible evidence. Reference both in the finding's artifacts so they render in the report and scorecard. This is the url-influence failure mode (JS-rendered SPAs reach AI crawlers and search as empty shells) made measurable per-site. Feeds be-discoverable and be-agent-ready | fetch + DOM |
secrets | scans page HTML, inline scripts, external JS resources, and meta tags for exposed API keys, tokens, and credentials (AWS keys, Google API keys, Stripe keys, GitHub/Slack tokens, JWTs, private keys, DB connection strings, generic API keys). Returns structured findings with redacted matches. This is a DESCRIPTIVE signal, not a verdict: you MUST reason about each finding. Legitimate public API keys (Google Maps keys, Stripe publishable keys, Firebase config keys) are EXPECTED to be client-side and are NOT security issues. Actual sensitive secrets (AWS keys, Stripe secret keys, JWTs, private keys, DB connection strings with credentials, GitHub/Slack tokens) exposed client-side ARE critical be-private-and-secure failures. Look at the page context to determine which: is the key near a <script src="https://maps.googleapis.com"> (legitimate Maps key) or in a config object with secret_access_key (critical leak)? Feeds be-private-and-secure | Runtime.evaluate + fetch |
headers | inspects the main document's security response headers: Content-Security-Policy (present? unsafe-inline/unsafe-eval?), Strict-Transport-Security (HSTS), X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy. Returns structured presence/absence + issues per header. Descriptive signal — judge against be-private-and-secure. Missing CSP/HSTS/nosniff are security gaps; unsafe-inline/unsafe-eval weakens XSS protection. Pair with cookies for the full transport-security picture | Network domain |
cookies | audits all cookies set by the page via CDP Network.getCookies: checks Secure flag, SameSite (Strict/Lax/None), HttpOnly, expiry duration, and whether each is third-party. Flags auth-like cookies (session/auth/token/id) that lack HttpOnly, cookies with SameSite=None, and long-lived (>365 day) third-party cookies. Descriptive signal — judge against be-private-and-secure. Long-lived third-party cookies also indicate tracking | Network domain |
trackers | enumerates all third-party request origins during page load and matches against a built-in list of known tracker/analytics domains (Google Analytics, DoubleClick, Facebook, Hotjar, Segment, Mixpanel, etc.). Reports total origins, third-party count, known trackers found, and top third-party by request count. Descriptive signal — judge against be-private-and-secure (tracking footprint) and be-sustainable (third-party bytes). A large third-party count indicates a heavy privacy footprint | Network domain |
images | inspects all <img> elements: checks for width/height attributes (CLS risk), loading="lazy" on below-fold images, oversized images (naturalWidth > 2x displayWidth), missing srcset/responsive sources, legacy vs modern formats (avif/webp/svg vs jpg/png/gif), and missing alt text. Descriptive signal — judge against be-fast-and-stable (missing dimensions cause CLS, oversized images waste bandwidth), be-sustainable (legacy formats, missing srcset), and be-inclusive (missing alt) | Runtime.evaluate |
Common options the harness simply applies (you choose them, it does not):
--emulate-media prefers-color-scheme=dark,prefers-reduced-motion=reduce,
--viewport 360x800, --wait <ms>, --selector <css>, --interact "<js>",
--out <path>, --source <dir>.
You may also run any other tool you judge useful at inspection time. None of
these is wired into the runtime; you invoke them yourself when they help:
- Lighthouse:
npx -y lighthouse <url> --output=json --quiet --chrome-flags="--headless=new --no-sandbox" for LCP/CLS/TBT and the a11y /
best-practices / SEO audits. Use it to corroborate the Lighthouse-dimension
principles, or skip it and gather the same signal first-party with layout
and evaluate.
- axe-core: inject it and run it via the
evaluate primitive, e.g. fetch
the script text and --expr a call to axe.run(), to enumerate accessibility
violations. Or write your own contrast/label/role probes with evaluate.
- Chrome DevTools MCP skills (optional): if your agent environment exposes
Chrome DevTools MCP, its
memory-leak-debugging skill provides a stronger
workflow for be-memory-efficient: baseline, target and final heap snapshots,
memlab analysis, and common leak-pattern diagnosis. Do not make it a
dependency; the package native heap primitive is still the default evidence
path.
- Your own static tests: when no tool fits, write a probe with
evaluate
(e.g. focus an element and read its computed outline; diff two heap summaries;
measure the same component in two containers). This is the point of leaning on
the model: you can invent the test the situation needs.
Method
0. Read the project config (if present) and set the bar
Before anything else:
- Read
web-uplift.json if one exists (at the site root, the --source
dir, or passed explicitly). It conforms to
schema/config.schema.json and lets the
developer declare siteType, scope, per-principle optOut (with a
reason), and intent. Honour it. A declared optOut means you report
that principle as opted-out with the developer's reason and do NOT count it
as an issue. intent sets context you judge against; it does not silence
findings. If no config is present, proceed with judgement (step 4). Record in
the report's config field whether one was loaded.
- Consult the mapped guidance UP FRONT. For each principle you will judge,
look at its checks'
guides lists and search/retrieve the relevant
Modern Web Guidance guides from the live feed (at the pinned
guidanceCatalogVersion) BEFORE you judge, so you set the bar from the
current recommended approach rather than from memory. Also read any
check-level references; these cover non-MWG standards, methods, and optional
tools such as the Chrome DevTools MCP memory-leak-debugging skill for memory
analysis. The guides and references entries are declarative pointers, not
tests; you still decide what evidence proves the outcome. Cache
list/retrieve for the run.
1. Recon and coverage (decide which pages to audit)
Use dom (with --source if you have it) and a screenshot to understand the
entry page: SPA or MPA, framework, the meaningful surfaces (routes, forms,
overlays, key flows). For a hash-routed SPA each route is reached as
<url>#<route> and a real reload is needed to re-run per-route styles; the
primitives navigate via about:blank already, so just pass the route URL. Note an
auth wall or bot block and stop with status: blocked if you cannot proceed.
Then decide COVERAGE, do not just audit the homepage. A site is its templates,
and most quality problems hide on the pages people actually use (articles,
product or detail pages, listings, forms, checkout, search, account). Work out
whether to navigate and where:
- Discover candidate routes: read the nav/menu and prominent internal links from
the DOM; fetch
/sitemap.xml and /robots.txt (an evaluate fetch(...), or
point a primitive at those URLs); check the web app manifest (the
<link rel="manifest"> target) for start_url and shortcuts; for an SPA,
enumerate routes from the router or the visible links.
- Select a REPRESENTATIVE set, not every URL: the homepage PLUS one example of
each distinct page archetype/template the site has, e.g. a content/article or
detail page, a section or listing/index page, a primary form or interactive
flow, search results, and any surface a specific principle targets. Aim for a
handful (roughly 4 to 8) covering the distinct templates and the
highest-traffic or highest-risk journeys. De-duplicate pages that share a
template (audit one, note it represents the rest).
- Justify it: in the plan and in the report's
paths, list each page you will
audit and WHY (which archetype or journey it represents), and state explicitly
what you are NOT covering and why. No silent homepage-only. If the site
genuinely is a single page, say so as an explicit judgement, do not assume it.
Principles are then judged across this set: some are global (security headers,
manifest, transport) and judged once; many are per-page (layout, color-scheme,
headings, content, forms) and must be checked on the representative pages, not
only the entry URL. Aggregate findings across pages and record which page each
came from.
User flows. When the important surfaces are reached by a JOURNEY (a checkout,
a signup, a search, an SPA route sequence) rather than a static URL, replay a
recorded flow instead of guessing selectors: node runner/flow.mjs replay <flow.json> --out <dir> (accepts web-uplift's own flow record output, a Chrome
DevTools Recorder export, or a hand-authored flow.json). It drives the steps over
CDP and captures a screenshot per step into <dir>; judge the per-step states as
additional paths and record the flow in paths.
2. Build the check manifest and plan evidence, per check
First generate the full (principleId, checkId) manifest required by the atomic
coverage contract. Verify its expected count against principles.json; do not
start from a hand-written list of 17 principle names. Read every check's
detectableVia HINT, guides, and any references (which you already consulted
up front in step 0). For each manifest row, record what evidence WOULD let you
judge it, under which condition and on which representative path. Do not begin
judgement until every row has a plan or an explicit anticipated blocker. Examples (not a
script; you adapt to the actual page):
- respects-color-scheme ->
screenshot/dom --selector under
--emulate-media prefers-color-scheme=dark; does the surface re-tint?
- respects-reduced-motion ->
video --interact or an evaluate of
getAnimations() under --emulate-media prefers-reduced-motion=reduce.
- responsive-no-horizontal-scroll ->
layout --viewport 360x800; is there
horizontal overflow? a screenshot shows the clipping.
- component-level-responsiveness ->
dom --selector of the same component in a
wide vs narrow container, or CSS inspection for @container.
- input-modality-aware (focus) -> an
evaluate probe that focuses the control
and reads the computed outline.
- be-fast-and-stable ->
layout (CLS + long tasks) AND a trace (capture a
performance trace; read its *-summary.json for navigationStart->FCP/LCP, long
tasks, total blocking time), and/or Lighthouse. Record the trace artifact path
in the finding so the perf claim is backed by concrete evidence.
- network-relevant principles (be-fast-and-stable's request weight,
be-private-and-secure's transport/third-party surface, be-sustainable's bytes
over the wire) -> a
har capture; read its *-summary.json (totals,
by-resource-type, first/third-party origins, render-blocking candidates, weight
offenders, hygiene signals), NEVER the raw multi-MB HAR, and record the .har
artifact path in the finding. renderBlockingCandidates are GROUNDED in the real
CDP signals the har capture records per request - the rich _initiator
(parser|script|preload, with the inserting document url/line or the script call
frame), the _priority (initial + final), and _renderBlockingStatus when the
Chrome build exposes it - and each candidate carries an explicit basis. The
HAR alone is partial (it does not by itself show <head> placement or
async/defer/type=module), so treat the list as the STARTING signal and CONFIRM
it against the live DOM: use dom/evaluate to read the real element's
placement and its async / defer / type=module attributes before asserting
a resource is render-blocking (e.g. a parser-inserted module script is deferred
by spec and is not render-blocking). The harness as a whole CAN determine this;
combine the HAR initiator/priority with the DOM.
- be-memory-efficient -> the memory-tracer-style before/after methodology with
the
heap primitive: take a BASELINE heap snapshot, then take a POST snapshot
after repeating a representative interaction ~10x via --interact (open/close a
modal, navigate a route and back, infinite-scroll a list), and COMPARE the two
summaries for retained growth (totals: nodeCount/totalSelfSizeBytes; and by
constructor: a growing Detached* population). Corroborate with
Performance.getMetrics (JSHeapUsedSize, Nodes) across the same window, and an
evaluate probe sampling listener/timer counts before vs after. If Chrome
DevTools MCP is available, consult its memory-leak-debugging skill: capture
baseline, target and final snapshots, then use memlab or the provided compare
workflow to identify leak traces. The package-native heap primitive remains
the default path. Read the heap SUMMARY, never the raw .heapsnapshot unless a
dedicated heap-analysis tool is doing the analysis. This pairs naturally with
the multi-page coverage (step 1): run the leak test on the INTERACTIVE
archetypes (SPA routes, feeds, editors, modal-heavy pages), not a static
content page. If a page has no representative interaction to repeat, do not
fabricate one - mark no-leak-under-repeated-interaction not-applicable with a
rationale (bounded-footprint and the detached-DOM/listener check still apply
from a single state). detached nodes can be intentional caches, so judge
confidence rather than asserting a bug.
- be-inclusive -> axe via
evaluate, and/or Lighthouse a11y, and/or your own
contrast/label probes; plus a screenshot to judge legibility/alignment.
- follow-best-practices / be-discoverable -> a
dom/evaluate probe for
doctype, charset, title, meta description, viewport, anchor hrefs; and/or
Lighthouse.
- be-discoverable / be-agent-ready -> the
discoverability primitive: it
reports how much of the rendered content is present in the RAW server HTML
(coveragePct), whether the page is a JS shell, and whether title/h1/meta
survive without JavaScript. A low coverage / isJsShell: true means AI
crawlers and search see little to nothing (the url-influence failure mode) -
a real be-discoverable/be-agent-ready finding. Confirm a surprising result
against the raw HTML and the dom primitive before asserting it.
- be-private-and-secure -> run the
secrets primitive to scan for exposed API
keys, tokens, and credentials in the page HTML, inline scripts, external JS,
and meta tags. REASON about each finding: is it a legitimate public key
(Google Maps, Stripe publishable, Firebase config — expected client-side, NOT
an issue) or an actual sensitive secret (AWS keys, Stripe secret keys,
JWTs, private keys, DB connection strings, GitHub/Slack tokens — a CRITICAL
leak)? Use the dom/evaluate primitive to read the surrounding context
(what script/config the key appears in) to distinguish. Also run har to
inspect third-party origins and request hygiene.
You own this mapping. If the HINT names a tool you do not want to use, use a
different one. If you want evidence the HINT does not mention, gather it.
3. Gather the evidence
Run the primitives and tools you planned. Keep artifacts (screenshots, videos,
heap summaries, layout JSON, Lighthouse JSON) under the report directory or
scratch/ (gitignored). Capture enough that a reader could verify each finding.
4. Judge every check, then derive every principle (quality without shaming)
Work through the coverage manifest one check at a time. Record exactly one
checkOutcomes row per expected pair, including status, confidence, method,
evidence, path/artifact references, and any reason/finding IDs required by that
status. Never initialise principles to pass and flip only those with findings;
that is absence-of-evidence scoring and invalidates the audit.
After every check is recorded, derive principleOutcomes mechanically from the
check statuses using the atomic coverage contract. For each principle, decide
applicability and verdict as follows:
- Opted out. If
web-uplift.json declared an optOut for this principle
(or check), report it as opted-out with the developer's reason and move on.
Do not raise findings against it.
- Applicability by expectation. Read the principle's
applicability.expectation:
default principles (respect-user-preferences,
implement-natural-interactions, provide-guided-navigation,
maximize-content-reduce-noise, adapt-to-the-form-factor, be-fast-and-stable,
be-inclusive, follow-best-practices, be-discoverable, be-private-and-secure,
be-trustworthy, be-memory-efficient) are expected of essentially every site;
absence is a finding. (be-memory-efficient is default, but its
no-leak-under-repeated-interaction check is contextual on the page having a
representative interaction to repeat - see below.)
contextual principles (be-resilient's offline/installable aspect,
be-internationalised, be-sustainable's absolute weight bar, be-agent-ready,
and public discoverability for a deliberately gated site) legitimately may
not apply. If there is no declared intent/optOut, make a judgement
call on applicability from the recon (siteType, surfaces). If you judge it
does not apply, mark it not-applicable WITH A RATIONALE rather than
penalising the site. If you judge it does apply, judge it normally.
- Verdict. For each applicable principle check, weigh the evidence and decide
pass or issue. Be honest about
confidence for subjective judgements.
The reported statuses are distinct: pass, issues, incomplete,
not-applicable (you judged it does not apply, with a reason), and opted-out
(the developer declared it, with their reason). Check outcomes additionally use
blocked and not-run, which derive an incomplete principle and a partial
run. Never silently drop a default principle, convert untested work to N/A, or
penalise a contextual one you reasonably judged out of scope.
For each issue, use the check's guides (already consulted in step 0) to cite
the most relevant Modern Web Guidance id; run a fresh search if you need to
refine. Search the feed ad hoc for anything you observe that no principle names.
5. Findings and task list
Classify each divergence into a finding tied to a principleId /
principleCheckId and/or a guidance id. Severity: critical (core
content/flow unusable under a common condition), high (principle clearly
violated on a primary path), medium (meaningful divergence or partial
support), low (polish / missed modern technique). Every finding needs a
concrete evidence string naming the modality you used (e.g. "screenshot under
prefers-color-scheme: dark shows .ndm-card still #fff" or "layout primitive
reported CLS 0.18 from a banner injected at ~600ms"). Then derive a prioritised,
deduplicated taskList (highest leverage first), each task citing its
findingIds and a guidanceId.
6. Report (and where it goes: retained, comparable runs)
Runs are RETAINED so they can be compared over time. Write each run into its own
timestamped directory and keep a latest pointer:
- Default report dir:
reports/<host>/<runId>/ where <runId> is a real
timestamp (an ISO timestamp with :/. replaced by -, e.g.
2026-06-13T17-30-00-000Z; use the actual current time). --out <dir>
overrides this.
- After writing, point
reports/<host>/latest at the run dir (a symlink, or a
latest.txt naming the run dir where symlinks are unavailable). The
runner/run-history.mjs helpers (runDir, updateLatest) compute these for
you if you drive them from a script; otherwise just follow the layout.
- Keep evidence artifacts under the run dir (e.g.
<runId>/evidence/...) so a
run is self-contained and its before/after artifacts move with it.
Write two files in the run dir:
report.json - MUST validate against
schema/findings.schema.json and pass
the atomic coverage validator. Set evidenceUsed (the modalities and tools
you actually ran), config (whether a web-uplift.json was loaded),
checkOutcomes (exactly one row for every manifest pair),
principleOutcomes (derived from those rows, so incomplete, opted-out, and
not-applicable remain distinct from pass/issue), coverage (the exact
denominator and completion counts), and the structured artifacts manifest
(one entry per evidence file you kept: {type, path, caption, condition, findingIds} with path relative to the run dir). Each finding SHOULD list the
artifact paths that evidence it in finding.artifacts, so every "action to fix"
ties back to concrete before-evidence. If scoring against --expected, include
your own precision/recall under an eval field.
report.md - human-readable: page profile, the evidence you gathered, an
artifacts-manifest table, findings grouped by principle (embed screenshots
inline with  and link video/trace/har/heap), the prioritised task
list, and anything skipped or low-confidence.
Before writing report.md, scoring, aggregation, or a completion claim, run the
validator and include its output in the run log:
web-uplift validate <run-dir>/report.json
If it reports missing, unknown, duplicate, blocked, or not-run rows, keep the
report as partial; do not emit a score or call the site audited. A human-readable report may explain partial
work, but it may not hide the denominator.
6b. Compare runs (before/after)
To measure progress across runs, diff two retained runs:
node aggregate/compare.mjs <host|url> [runA] [runB]
Defaults to the two most recent runs (older = before, newer = after) and writes
compare.md + compare.json into the newer run: principle status changes,
per-finding resolved/new/persisting, metric deltas (LCP/INP/CLS, Lighthouse
scores where present), network/HAR deltas (request count, transferred bytes),
and PAIRED before/after screenshots (matched by capture condition). Fix mode
(below) runs this automatically at the end, so a fix run shows the before->after.
6c. Scorecard (complete runs only)
Only after the atomic coverage validator reports complete: true (and after any
compare in fix mode), generate the interactive scorecard and use it as the close
of your reply. For a partial run, do not score it: close with the exact coverage
counts and blockers/not-run checks instead.
node aggregate/scorecard.mjs <host|url>
This rolls ALL retained runs for the host into a self-contained
reports/<host>/scorecard.html (Lighthouse-style outcome gauges, a top-3, a
per-finding deep-dive, a score-over-time history with per-run and per-outcome
deltas, and a before/after panel). It also prints a compact text scorecard
to stdout.
End your run by:
- Showing that text scorecard inline (overall score, the six outcome scores,
and the top-3 "do these first"). It leads with outcomes, not a raw findings
dump. Do NOT re-type a long list of every finding above it.
- Linking the interactive report:
reports/<host>/scorecard.html (and mention
the History tab shows the deltas over time once there is more than one run).
Fix mode emits the scorecard automatically only for a coverage-complete run; for
a complete report-only run, run the command above yourself. Partial runs must
not enter score aggregation.
7. Fix mode (--fix --source <dir>, the model-driven hill-climb)
Only with local source. This is a MODEL-DRIVEN hill-climb: YOU write every edit
based on Modern Web Guidance. There are no canned transforms anywhere. You can
run this loop entirely INSIDE your own session (the default, subscription path)
- you do not need the headless runner; you ARE the model.
Inputs: the findings + taskList you just produced (step 5), or a pre-aggregated
report passed with --findings <path> (in which case you may skip the baseline
audit and go straight to the climb).
The loop, highest-leverage task first:
retrieve the task's guidance guide and read its technique + browser-support
notes (assume Baseline Widely available is safe; follow the guide's fallback
advice otherwise, unless web-uplift.json states a custom policy).
- Write the fix into the local source under
<dir>. You are the coding agent.
Honour web-uplift.json: never "fix" a principle reported opted-out or
not-applicable - those are out of scope, not issues.
- Re-gather the relevant evidence (the SAME primitive and emulated condition you
used to find it) and confirm the issue is gone. Compare against a known-good
reference where one exists (e.g. a
?mode=fixed variant). If a fix introduces
a new issue, log it and continue.
- After a pass, recount the OUTSTANDING issue-findings: findings whose
principleId is NOT one you reported not-applicable/opted-out. This is the
number that must climb DOWN; print it so the descent is visible (e.g.
9 -> 5 -> 0).
- Repeat passes until there are zero outstanding
issues (a clean audit may
still carry not-applicable/opted-out principles - that is a pass), or a
pass makes no further progress, or --max-iterations is hit. Record
budget.auditPasses. Optionally open a PR (branch, commit only the source
changes, gh pr create).
- Goal-directed stop (
--goal-overall <n> / --goal-min <outcome>=<n> /
--goal-max-critical <n> / --goal-max-high <n>): when a goal is set,
also stop as soon as the scorecard meets it, so you can climb to a target
(e.g. overall>=80, no criticals) without chasing every last low-severity
issue. Print the score per pass.
- Emit the before/after comparison. Preserve the baseline as a
-before
run and the final state as an -after run (see step 6's run layout) and run
the compare (step 6b) so the fix produces a compare.md showing the measurable
before->after: outstanding 9 -> 0, which findings resolved, and the metric /
network / screenshot deltas. The headless web-uplift fix does this for you;
in-session, run node aggregate/compare.mjs <host> <before> <after> yourself.
Never edit source outside <dir>. Never run fix mode against a site whose
source you do not have locally.
The headless web-uplift fix / npm run fix command drives exactly this loop
unattended via an agent CLI (API tokens); the in-session path above is the same
methodology with no extra billing.
Why fully agentic (and why no fast path)
Tooling normally bakes judgement into a check runner and falls back to it. This
project deliberately does not automate judgement: web quality is open-ended,
the modern platform moves fast, and fixed pass/fail heuristics rot. It does,
however, enforce coverage deterministically. principles.json is the exact
registry and denominator; schemas and validators must reject missing, unknown,
or duplicate check outcomes. The model chooses how to gather evidence and judge
each registered check, but it may not choose to omit one. By leaning on the
model for evidence and judgement while using deterministic coverage validation,
the method stays current without allowing silent gaps.
Cross-agent note (this is NOT Claude-only)
This skill is plain markdown methodology any agent can follow. It runs as
/web-audit <url> in Claude Code, Codex, Gemini CLI, Antigravity, GitHub
Copilot, and opencode from this repo, and as a raw prompt in anything else
("Read the file .claude/skills/web-audit/SKILL.md and follow its instructions
exactly, with these arguments: "). Each CLI is wired to this one canonical
SKILL.md via a thin per-agent entry point; see the support matrix and the
"How to add an agent" section in README.md and
runner/README.md.
No MCP server is required. The evidence primitives are a plain Node CLI
(node evidence/cli.mjs <primitive> <url> ..., raw CDP via
chrome-remote-interface) that any agent able to run shell commands and read this
file can use directly. The repo's web-uplift MCP server only distributes
this SKILL.md to MCP-aware hosts as a convenience; it is optional and there is no
browser-automation MCP server anywhere in this project. The only hard
requirements on the host are: Node, a google-chrome-stable (override with
CHROME_BIN), ffmpeg (for the video primitive), and network access for the
Modern Web Guidance feed and optional Lighthouse (npx).
Finish your reply with a one-paragraph TLDR. For a complete run: finding count
by severity, evidence modalities/tools used, the single highest-leverage fix,
and (in fix mode) how many findings you verified fixed. For a partial run: exact
checks judged/expected, blocked, not-run, missing, unknown and duplicate counts,
plus what prevents completion; never substitute a score.