dwarvesf

web-drift

Use for the live-website agent-readiness audit, "run the web-drift loop", "web drift sweep", "check the public sites", or a scheduled cadence run. NOT for docs/code in a checkout (doc-drift), NOT for CI/release state (ci-drift), NOT for the kit's feature registry (topology-drift), NOT for page-speed/Core Web Vitals, NOT for answer-engine citation, NOT for fixing a site (files rows only).

dwarvesf 12 2 Actualitzat fa 1 mes
GitHub

Instal·la

npx skillscat add dwarvesf/dwarves-kit/web-drift

Instal·la des del registre de SkillsCat.

SKILL.md

Web drift

Scope and triggers

Also fires on "audit our websites", "are our sites still readable by an agent". Enumerates every site the consumer declared in WEB_DRIFT_SITES, probes each over read-only HTTP with lib/webcheck (groundwork, page, and API tiers), verdicts each check with evidence, and files the fixes as board rows in the repo that owns the site's source, plus a report. Page-speed/Lighthouse/Core Web Vitals is a different tool and a different question. No answer-engine-citation probe ships here. This repo holds no website source: the loop files rows, it never edits a site.

Overview

Audit every declared public website against the agent-readiness contract and file the fixes.
This is the live-site instance of docs/patterns/audit-loop.md: enumerate, verdict with
evidence, apply, gate through the operator.

kit:ci-drift already reaches the network, so reaching outward is not the novelty. What is new
is the surface: no in-kit loop audits a public HTTP surface, and the fix lands in whichever repo
builds that site, never here.

The contract this measures: an agent that runs no JavaScript should be able to find the site,
fetch a page, read its content, tell a dead URL from a live one, and call its API if it has
one. A deploy can break any of those silently, and nothing in a repo notices.

The four slots (per the audit-loop pattern)

Slot This instance
Item set one (site, check) pair. The site axis is WEB_DRIFT_SITES, enumerated by python3 lib/webcheck/webcheck.py sites (comma or whitespace separated, never colon: every URL carries one). The check axis is fixed by the contract below. A site is not the item: a site with one hard fail and eight warnings has no single verdict, and a tier that does not apply has to drop out of the denominator, which only works per check. WEB_DRIFT_SITES unset means no sites are declared: report that and stop. The kit ships no hostname.
Contract each site stays discoverable, fetchable, and understandable without JavaScript: the groundwork tier (robots, sitemap, llms.txt, unknown-path status, markdown negotiation with Vary), the page tier (title, meta description, one h1, 500+ visible characters, canonical, OG, JSON-LD, internal links), and the API tier where the site exposes an API
Evidence class live HTTP responses, quoted from webcheck audit output. A hard fail and a warning are each quoted evidence. No response at all is not evidence.
Apply mechanics no code edit lands in this repo. Each FIX becomes one row appended to the _meta/BACKLOG.md of the CONSUMER repo that owns the site's source, in the shared kanban format (| ID | Item | Notes & source | Status |, status queued). The board CLI has no add verb, so the row is written directly; bin/board board --backlog-file <path> (in the kit install root) renders it back and bin/board set <ID> <state> moves it afterwards. Plus a report listing every verdict. UNSURE items go in the report for the operator, never into a row. A consumer with no _meta/BACKLOG.md has nowhere to file: the refusal guard (Step 1) stops the whole run against that consumer rather than falling back to a local ledger this skill would then own.
Closing evidence a row closes only when a later webcheck audit <url> shows that (site, check) pair green. The next run re-audits every site carrying an open row FIRST and reports each open row as still-failing or now-green. Without that the loop cannot fail, because a filed row proposes a fix and nothing re-tests it.

Verdict mapping

Tier 1 is the tool. Its hard fails and warnings map to the audit-loop grammar:

Tool output Verdict Note
hard fail FIX the tool's own message carries the "say how"; add the recipe from the reference table below
warning with one obvious fix (missing canonical, missing OG tag, og:image is an SVG, no Organization block, no llms.txt guidance section) FIX
warning whose fix depends on intent (meta description length, h1 count, internal-link count, unversioned API base) UNSURE someone decided that on purpose or did not; you cannot tell from outside
site unreachable, network error, groundwork fetch that did not answer UNTESTABLE the evidence exists but not from where you ran: a WAF block, a transient 503, a DNS hiccup. Re-run from another vantage or at the next cadence. Never REMOVE, never FIX, and NOT UNSURE, which means only the operator can answer and would escalate every flaky 503 to a human forever.
tier reports not applicable not a finding it drops out of the denominator, the way the rubric excludes a check rather than failing a site it cannot apply to
a declared site is permanently gone (NXDOMAIN or a settled 410) and a successor is named REMOVE this removes the entry from the consumer's WEB_DRIFT_SITES, never the site. Name the successor; "obviously dead" is not a verdict.
the site actively instructs something now wrong (llms.txt pointing agents at a dead surface, a developer page describing a flow that does not exist) DANGER quote the contradiction; an agent will follow it

DANGER never comes from Tier 1. The tool checks that llms.txt answers and carries an
orientation section; it does not follow the links inside it or read a developer page. A DANGER
verdict therefore requires the lead to fetch and read those bodies, and to quote the
contradiction. If nobody read them, there is no DANGER finding, only unexamined surface. Say so
in the report rather than implying the check ran.

Tier 2: the lead gathers, the scanner judges

agents/audit-scanner.md has Read, so it can judge saved evidence, and kit:ci-drift already
runs this split with a network Tier 1 (gh api) and a file-reading Tier 2. What the scanner
cannot do is FETCH: its roster carries no network verb. So the order is fixed here, not
optional.

Save Tier 1's output first (webcheck audit <url> > <file>), then dispatch kit:audit-scanner
with those files, this skill's path (for the verdict mapping and the fix-recipe table), and the
four-slots contract. It quotes both sides for any mismatch and returns findings; every row and
every report line is written HERE, never by the scanner.

Dispatch it when the run covers more than a couple of sites, or when a finding's FIX-versus-
UNSURE call is genuinely ambiguous. For a one or two site run the lead reads the output inline:
the judgment is a table lookup and a subagent would cost more than it saves. Either way the
evidence is the saved output, never a memory of it.

Process

  1. Enumerate. python3 lib/webcheck/webcheck.py sites. Write the list down before probing
    anything; it is the queue, and a resumed run picks up from it. No sites declared: say so,
    name WEB_DRIFT_SITES, and stop. That is a clean result, not a failure.

  2. Re-audit the open rows first. Any site carrying an open FIX row from a previous run goes
    to the front of the queue. Its rows close on green and stay open otherwise. This is the
    loop's closing evidence; skipping it is what makes an audit unfalsifiable.

  3. Tier 1, mechanical, zero model cost, every site.
    python3 lib/webcheck/webcheck.py audit <url> > <saved-output> per site. Read the exit code
    and the three blocks. For a site with a sitemap, pass the sitemap URL and a --limit so the
    page tier samples rather than crawls; the groundwork and API tiers run once regardless.

  4. Tier 2, judgment, only on what Tier 1 raised. Per the split above: inline for a one or
    two site run, kit:audit-scanner over the saved output for anything larger. Map each hard
    fail and warning through the verdict table. Attach the recipe from the reference table to
    every FIX. Do not re-derive a check the tool already ran, and do not open a browser to
    confirm what the response body already says.

  5. Verdict each (site, check) pair with the audit-loop grammar. A verdict with no
    checkable evidence downgrades to UNSURE. A site that did not answer is UNTESTABLE across
    every one of its checks, and the run continues to the next site.

  6. Check the evidence is not stale before filing anything. Compare when you scanned against
    when the site last deployed. A finding measured before the fix deployed is not a finding.
    Name both timestamps in the report.

  7. Apply. For each FIX, resolve the target repo, the one that owns that site's SOURCE, not
    the repo that owns the domain. A static site built from a separate engine repo takes its fix
    in the engine, and the content repo cannot carry it.

    Refusal guard: a boardless target repo REFUSES THE RUN, no local ledger. Before
    filing anything, check the target repo has _meta/BACKLOG.md:

    test -f _meta/BACKLOG.md || { echo "REFUSE: <repo> has no _meta/BACKLOG.md -- a FIX has nowhere to file and no closing loop. Fix: run 'bin/board init' in <repo> to give it a kit board, then re-run web-drift."; exit 1; }

    Stop the whole run there and report it; do not keep a local per-consumer ledger as a
    fallback (that ledger would be a second source of truth this skill would then have to own
    and reconcile). A consumer's own choice not to adopt a kit board is a decision for that
    consumer to make, by running bin/board init, not one this skill works around silently.

    Once the target repo passes the guard, file one row per FIX, say which repo and why in the
    row's Notes column. Never edit a site from here.

  8. Report. One table: site, check, verdict, evidence, target repo. List every UNSURE and
    every UNTESTABLE separately for the operator, plus the state of each row carried in from the
    last run. If every check came back clean, file no rows and report CLEAN with the enumeration
    list.

Fix recipes (reference)

Distilled from a real estate-wide run. Each row is what the fix actually is, not what the check
is named.

Finding The fix The rule behind it
unknown path answers 200 or 3xx serve a real 404 status with a body listing sitemap, llms.txt, and home needs request-time code. A static export alone cannot negotiate or answer a status it did not pre-render; the fix belongs in the worker or server in front of it
markdown negotiated without Vary: Accept add Vary: Accept at the same layer that negotiates without it a CDN hands one variant to the wrong client. If the site does not negotiate at all, build the negotiation first, then the header
under 500 visible characters without JavaScript server-render the body: heading plus prose in the raw HTML a client-side isClient gate that returns null on the server exports an empty root div. Bisect by building, not by reading: remove the gate and count <h1> and visible characters in the built output
no sitemap.xml, no llms.txt generate both at build time they are build artifacts, not hand-written files
llms.txt is a bare link index add a "When to use" orientation section a link list tells an agent what exists, not when to reach for it
no canonical, no JSON-LD, no Organization block emit canonical plus one Organization block per page from the site's own config read every value from config. Omit a field the content genuinely lacks rather than inventing one
API has no /openapi.json, or errors are not JSON publish an OpenAPI 3.1 spec and make every error route return the same JSON error schema one schema, referenced consistently, or a client cannot parse failures
no RateLimit headers emit IETF RateLimit headers from the real limiter count, do not guess. A header stating a budget the server does not enforce is worse than none
unversioned API base put a /v<n> segment in the canonical server URL, or state the deprecation policy in info.description a client needs to know when the surface may change

Three honesty rules that override any score:

  • Never serve JSX-shaped MDX as markdown twins. If the markdown twin reads worse than the HTML, it is metric gaming and it makes the site worse for the agent it claims to serve.
  • Never fake a sandbox, a key-issuance flow, or a developer surface that does nothing. A page that looks like an onboarding flow and is not one is a DANGER finding, not a fix.
  • Count, do not guess. Rate-limit budgets, page counts, and coverage numbers come from the system that enforces them.

And one evidence rule: a scan is only as fresh as the deploy behind it. Compare scanned_at
against the last deploy of the site's source before you believe a finding.

Cadence

Run after a deploy of any declared site, before an announcement that points agents at one, or
on a schedule per the audit-loop driver ladder. One pass per invocation, bounded by the
enumeration list.

Red flags

  • Reporting a finding for a site that did not answer: that is UNTESTABLE, always.
  • Filing rows and never re-auditing them: a loop with no closing evidence cannot fail.
  • Filing a row against the repo that owns the domain when a different repo owns the site's source.
  • Falling back to a local ledger when the target repo has no board. Refuse the run instead: a second, skill-owned source of truth is the same drift class the closing-evidence rule exists to prevent.
  • Asking kit:audit-scanner to FETCH anything: it has no network verb. Save the output first, then hand it the file.
  • Calling a DANGER on an llms.txt nobody opened. Tier 1 never produces that verdict.
  • Re-running the checks by hand in a browser when the tool already quoted the response.
  • Believing a finding measured before the fix deployed.
  • Hardcoding a site list anywhere in this repo. The list is the consumer's, and it has no default.

Cadence

This instance has a declared cadence in the ## Cadence table of docs/patterns/audit-loop.md.
Record a finished pass with bin/audit ran web-drift "<one line>", including a CLEAN one, so
bin/audit due stops naming it. The kit ships no scheduler: due is what a human or a job reads.