agentsclimarketplace

Web audit

Skill PaulKinlan/web-uplift/.pi/skills/web-audit

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.From its SKILL.md

Install
npx -y skills add PaulKinlan/web-uplift --skill web-audit

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 6 stars6 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

39.9 KB, as published. Nobody here has run it

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)

  1. 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.
  2. 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:

  1. 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.
  2. 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 [email protected]) unless web-uplift.json overrides it.
  3. 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:

  1. 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.
  2. 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.
  3. 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:

PrimitiveWhat it gives youKey CDP
screenshota PNG (full or --selector-clipped) under any emulated conditionPage.captureScreenshot
videoan MP4 of an interaction window, frames assembled with ffmpeg; --interact "<js>" triggers the transition/animationPage.startScreencast
heapa 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-efficientHeapProfiler.takeHeapSnapshot
layoutlayout metrics, a CLS/layout-shift observer, long tasks, overflow at the current viewportPage.getLayoutMetrics + observers
domDOM, computed styles for --selector list, page HTML/CSS, and (--source <dir>) the local source filesDOM/CSS/Runtime
evaluateruns your own --expr "<js>" in the page: ad-hoc probes and static tests you write on the spotRuntime.evaluate
tracea 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 traceTracing.start/end
hara 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
discoverabilityhow 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-readyfetch + DOM
secretsscans 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-secureRuntime.evaluate + fetch
headersinspects 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 pictureNetwork domain
cookiesaudits 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 trackingNetwork domain
trackersenumerates 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 footprintNetwork domain
imagesinspects 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:

  1. 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.
  2. 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 ![](path) 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
# In this repo: node schema/validate-report.mjs knowledge/principles.json <run-dir>/report.json
# Per-project install: node .web-uplift/schema/validate-report.mjs .web-uplift/knowledge/principles.json <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]   # web-uplift compare ...

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>   # web-uplift scorecard <host>

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:

  1. 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.
  2. 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:

  1. 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).
  2. 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.
  3. 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.
  4. 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).
  5. 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.
  6. 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: <url>"). 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.

Keep looking

Skills are one crate of 628,971. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.