2026-09-17
- Operations:
scripts/rescore-all.ts --urls=and--status=now select rows and actually write them —--urls=was documented for rescoring, butselectSites()ignored it and the flag routed to calibration mode, which is dry-run always: on 2026-09-16 a 374-site "remainder" run crawled for four hours, logged plausible scores and wrote nothing.--urls=<source urls>and--status=blocked,unknown_failure,…now filter the candidate set on the writing path.--urlsis matched against all candidates before--statusnarrows, so a URL that exists but is excluded by status reports "zero sites" instead of a false "unmatched URL"; matching normalises origin plus path (trailing slash dropped, host lowercased, query string kept,www.deliberately not stripped, because over-matching would write a row nobody named). Calibration moved behind an explicit--calibrate(which requires--urls);--rubric-compareis unchanged and still read-only. Any mode that writes nothing now says so in its first log line and in its exit summary, and a run that resolves to zero sites aborts loudly rather than printing DONE. Argument handling is strict, because the failure class here is silent over-selection: unknown flags are rejected (a--url=typo used to select — and write — all ~1.4k sites), a present-but-empty value (--status=$UNSET_VAR) aborts instead of degrading to "no filter", repeated value flags are rejected, and--rubric-comparecombinations that cannot mean anything (--dry-run,--urlswith--status,--out=outside compare mode) are refused. Regression tests:tests/rescore-all-select.test.ts. - Audit accuracy: DataDome and PerimeterX challenges are named, from live captures (core tarball update) —
scripts/probe-audit-failures.ts --dumpcaptures status, headers, a 64 KB body prefix and the current classifier verdict for eachunknown_failurehost into an operator-named file (read-only: no DB writes, one fetch attempt per host,--statusselects the whole filtered set unless--limitis given), and the two newCHALLENGE_FINGERPRINTSentries were written from those captures rather than from memory: DataDome'sgeo.captcha-delivery.com(not the bare delivery domain, which is a documented CSP value customers ship on ordinary pages) and PerimeterX'sid="px-captcha"(not the bare substring, whose first occurrence is a CSS rule). The honest result: 6 of the 50unknown_failurehosts are named, not the ~40 the plan hoped for. Most of that population is not a challenge at all — roughly 16 are Akamai hard blocks — 12 of them the same static 360–384 byte "Access Denied" page, the other 4 larger custom block pages, none with a JS challenge or a captcha, ~10 are Cloudflare Error 1020 blocks carrying only JavaScript-Detections instrumentation (the standingwww.g2.comnegative control, correctly excluded), and the rest are bare 403s behind an ELB or no identifiable vendor. Naming those needs a mechanism other thandetectChallenge→blocked, because that path's message asserts the site "served an anti-bot challenge". No Akamai or Imperva fingerprint ships, deliberately: zero Akamai challenges exist in 26 Akamai-fronted captures, and Imperva's only evidence is an HTTP 200 interstitial, which no body fingerprint can reach at any size cap because the classifier is status-gated. PerimeterX ships dormant — both real captures sit beyond the 4 KB body window. Regression tests: coretests/challenge.test.ts(every new pattern has a negative control that must not match); platformtests/probe-audit-dump.test.ts. - Audit accuracy: the agent probe set is derived from the registry (core tarball update) —
AGENT_UASwas a hardcoded three, so adding a documented user agent tobot-tokens.tssilently left it innotProbedas though it were unprobeable. It is now derived fromBOT_TOKENS.filter(purpose === "user" && uaString), and the homepage is probed under all six: the three that back a scoredreach-*check first, pinned in their historical positions and keys (ChatGPT-User, Perplexity-User, Google-Agent), then the rest in registry order (Amzn-User, MistralAI-User, Meta-ExternalFetcher). The pinning is not cosmetic: the homepage loop is budget-gated per fetch, so a token's position decides whether its check is measured at all on a slow site, and plain registry order would have flippedreach-perplexityandreach-googlebetweenpassandunmeasuredon identical input. Extra key pages stay at the first three — the pass runs on amin(timeout*3, 15s)wall-clock budget that already produced 82unmeasuredresults, and six UAs across three pages would buybudget-skippedresults rather than coverage.edge-agent-accesscompleteness is therefore measured per page, against the tokens probed on that page, with an explicit gate that no probed token went unanswered everywhere (which the old union rule gave for free); a token probed on the homepage but not on an extra page now shows per page indetailsas<url>.notProbed: … (extra-pages-only)instead of being indistinguishable from "probed and failed", and the site-widenotProbedline is Claude-User alone, since Anthropic publishes no UA string.metadata.multiUaResponsesgains anAmzn-User,MistralAI-UserandMeta-ExternalFetcherkey — a derived entry is keyed by its own registry token, so a vendor-string typo cannot move a scored key — and the homepage results grow from three entries to six, so any reader indexing them by position past the third moves. No score changes:edge-agent-accessis weight 0 and tracked, excluded from category renormalization, and the three scoredreach-*checks keep their probe positions, their keys and their verdicts — the threeCheckResults were captured over seven fixture scenarios before and after the change and the dumps are byte-identical, and budget exhaustion between agent fetches (the case that discriminates the orders) is pinned by its own tests.RUBRIC_VERSIONstays 3.0 (see coredocs/RUBRIC-CHANGELOG.mdfor why widening a tracked check's inputs owes no bump). At the next rescore expect moreedge-agent-accessresults to readunmeasured— 12 agent fetches per site (6 on the homepage, 3 on each of up to 2 extra pages) instead of 9, against the same budget, plus the new unanswered gate — and longerdetailson every audited site. How much more is unmeasured, and the affected population is the ~1225oksites, not the failures: the planned owner re-run covers the failed statuses only, so it cannot size this — a site can flipconsistent→unmeasuredon a tracked check while itsaudit_statusstaysok. Sizing it needs a rescore (or dry-run comparison) over a sample of theokpopulation with a before/after count of the check's outcomes. Regression tests: coretests/edge-probe-registry.test.ts,tests/edge-probe.test.ts,tests/edge-agent-access.test.ts,tests/edge-evidence-summary.test.ts,tests/calibration-fixes.test.ts. - Docs: counts, statuses and shipped surface re-synced to the code — no behaviour change.
docs/PRODUCT.mddocuments the rescore flags as they now behave (--urls=/--status=write;--calibrate --urls=,--dry-runand--rubric-comparedo not; the abort rules),audit_history.edge_evidence, the fingerprinted challenge vendors (PerimeterX dormant behind the 4 KB body prefix) and the null public score for a failed latest attempt, and corrects the rubric notice date to 2026-09-15 (the 2026-09-14 entry below predates the cutover date being set).docs/API.mdcorrects the webhook trigger (anAuthorization: Bearertoken, not a body field, with its 401/503 responses) and states that/api/v0/serversreturns anullaudit_score/audit_gradefor a site whose latest audit failed.docs/BUGS.mdcloses the p0 webhook auth bypass, fixed indf2fe77. CoreREADME.md,README_CN.mdanddocs/GETTING-STARTED.mdreplace the pre-2.0 "60 checks / six categories / A = 90+" scoring description with rubric 3.0's 56 checks (50 scored, 6 tracked), seven category weights and 75/60/45/30 grade bands (the sample report and the Nuxt/Astro adapter check lists too), and coredocs/RUBRIC-CHANGELOG.mdlogs the DataDome/PerimeterX fingerprints as a no-bump 3.0 note. - Methodology spec v1.0 published at
/methodology— the spec is now canonical in the OSS repo (docs/spec/agent-readiness-spec-v1.0.md, text licensed CC BY 4.0) and the platform renders a byte-identical copy at/methodology(react-markdown+remark-gfm), stamped with the spec version, rubric version and publish date, listed in the sitemap and linked from the footer Learn section.scripts/sync-spec.tscopies the canonical file from the sibling OSS checkout (../w2agent, or a path argument /W2AGENT_REPO), and--checkexits non-zero when the copies differ;tests/methodology-spec.test.tsfails when the copy's engine version differs from core'sRUBRIC_VERSIONor when any registered core, site-type or platform check lacks its documented weight, detection rule, evidence boundary, maturity tier or weight rationale. Spec content audited against the 3.0 registry (56 core checks, 50 scored and 6 tracked): adds the trackedusage-preference-declaredandedge-agent-accessentries, audit-level failures and challenge fingerprints, crawl inputs and the multi-UA probe set, maturity tiers (M1–M4 / H), site-type and platform adapter checks (disclosing thatastro-sitemap-integrationalways fails andnuxt-llms-moduleignores the llms.txt probe), and states that weights are not yet calibrated (P4-2 pending). No score changes.
2026-09-15
-
Fix: agent subdomains serve their own well-known files again — since the E3
mcp.jsonchange,middleware.tspassed every/.well-known/*request through before the agent-subdomain rewrites, soagent.w2agent.devand<slug>.agent.w2agent.devreturned w2agent.dev's own agent card and/.well-known/ucp404'd. Agent hosts now route/.well-known/agent-card.jsonto/api/meta-agent/agent-card//api/agent/<slug>/agent-cardand/.well-known/ucpto/api/agent/<slug>/ucp.agent.w2agent.dev/.well-known/mcp.jsonstill serves the meta MCP server card. Other/.well-known/*paths on agent hosts, includingmcp.jsonon slug hosts, now fall through to the agent route instead of the apex files. Apex hosts are unchanged, and the Clerk and no-Clerk branches now share one routing function. Regression tests:tests/middleware-routing.test.ts. -
Fix: hosted agents speak the A2A v1.0 wire format their cards advertise — hosted agent cards claimed JSON-RPC
protocolVersion: "1.0", butSendMessagereturned a bare pre-0.3 task (state: "completed",{type: "text"}parts, artifactid) that v1.0 clients can't parse. The response is nowresult: {task}withTASK_STATE_COMPLETED/TASK_STATE_FAILED,contextId,status.timestamp,artifactIdand bare{text}parts. Input accepts v1.0{text}parts and still takes legacykind: "text"/type: "text"parts. Behaviour changes for callers: a message with no non-empty text part now returns JSON-RPC-32602instead of a failed task;message.taskIdreturns-32001(tasks are not stored); anA2A-Versionheader (or?A2A-Version=query parameter) with a major other than 1 returns-32009(no version is still served). Hosted cards now saycapabilities.streaming: false(the handler never streamed),provider.organizationinstead ofprovider.name, and drop the non-speccapabilities.a2a. Regression tests:tests/a2a.test.ts(spec §6.1 request fixture),lib/agent-host.test.ts. -
Fix: w2agent's own agent cards are honest A2A v1.0 cards that pass our audit — the meta agent (
agent.w2agent.dev) now answers A2ASendMessageby runningsearch_websiteson the message text (after the existing IP rate limit; MCP requests and invalid JSON reach the MCP transport as before). Its card and w2agent.dev's/.well-known/agent-card.jsonare now v1.0 cards (supportedInterfaces→https://agent.w2agent.dev/a2a,streaming: false,text/plainmodes,provider.organization) that list onlysearch_websites. The meta card drops the five MCP-only tools, and the apex card drops the audit / llms.txt generation / deploy skills, which no A2A endpoint serves. Hosted agent cards advertise A2A athttps://<slug>.agent.w2agent.dev/a2ainstead of the homepage, so self-audits score pass instead of stub 50; the root still answers A2A. Both agent endpoints now return the spec's errors for the other v1.0 methods instead of routing them to MCP:-32004for streaming, subscribe and the extended card,-32003for push-notification configs,-32001forGetTask/CancelTask;ListTasksreturns an empty page. Fixed a bridge bug: hosted-agentSendMessagealways returnedTASK_STATE_FAILED, because the MCP transport answered as SSE and the bridge parsed JSON. Adapter agents (data/adapters/*.yaml) have no A2A endpoint, so their agent card now 404s, and neitherget_website_infonor the site page links one. Regression tests:tests/agent-cards.test.ts(corewell-known-agent-cardcheck on the served cards),tests/a2a-routes.test.ts,tests/a2a.test.ts,tests/middleware-routing.test.ts. -
Fix:
/a2aspeaks only A2A, and A2A answers follow the spec more strictly — the advertisedhttps://agent.w2agent.dev/a2aandhttps://<slug>.agent.w2agent.dev/a2anow reach dedicated A2A-only routes (/api/meta-agent/a2a,/api/agent/<slug>/a2a) with the same rate limit, plan quota and usage/bot tracking as before. An A2A client sendingAccept: application/jsonused to get MCP's 406 or parse error there. It now gets JSON-RPC-32700for invalid JSON,-32600for non-object bodies, and-32601for MCP methods;GETis405 Allow: POST. The root paths still detect A2A. Hosted agent cards now list onlysearch-site(they also listed page, FAQ, form, OpenAPI and commerce tools, which A2A never reaches; those stay on MCP). Behaviour changes on every A2A path: a tool that throws is nowTASK_STATE_FAILEDwith a generic message (the SDK's error result used to come back as a completed task containing the error text);A2A-Versionmust be 1.0 (1.1now gets-32009); a bad JSON-RPC envelope (jsonrpc≠"2.0", boolean/object id) is-32600;ListTaskspageSizeoutside 1-100 is-32602;configuration.returnImmediately: trueis-32004. w2agent.dev's card gets the legacy top-levelurlthe other w2agent cards have. Regression tests:tests/a2a-routes.test.ts,tests/a2a.test.ts,lib/agent-host.test.ts,tests/middleware-routing.test.ts. -
Failure honesty: an unmeasurable site names its cause (core tarball update) — core used to throw one
Could not reach <host>for DNS failures, TLS errors, timeouts and rate limits alike, and a homepage 403 did not throw at all: the engine scored the anti-bot page. The homepage response is now classified once, after redirects and retries, intounreachable/blocked/auth_gated/rate_limited/timeout/unknown_failureand thrown as a typedAuditErrorcarrying the evidence (final status, challenge vendor, signal). An anti-bot challenge is detected from a vendor-cited fingerprint table (cf-mitigated: challenge, Cloudflare's challenge-platform orchestrate path,window._cf_chl_opt, AWS WAF'sx-amzn-waf-action, CAPTCHA scripts on a 403/429/503) and is never retried and never scored. Behaviour change at the next rescore: a homepage 403 with no challenge markers no longer produces a score, so sites currently ranked on an anti-bot or WAF error page drop out of the ranking and appear under "Couldn't measure" instead (scripts/probe-audit-failures.ts --allsizes this before deploy). Regression tests: coretests/challenge.test.ts,tests/audit-errors.test.ts; platformtests/probe-audit-failures.test.ts. -
Failure honesty: every entrypoint that audits a registered site records the failed attempt, and
/api/auditreturns 422 — a failing audit from deploy, directory submit, self-hosted register or scheduled refresh used to leave no trace at all (onlyrescore-allrecorded, as a flaterror).runSiteAuditnow writes anaudit_historyfailure row with the typed status, error message anduser_id, setssites.audit_status, then rethrows, so every caller's existing error handling is unchanged.POST /api/auditanswers an unmeasurable site with422 {error: "audit_failed", kind, message}instead of a 500 and no longer reports it to Sentry (it audits a URL rather than a registered site and never goes throughrunSiteAudit, so it still writes noaudit_historyrow — the typed reason reaches the caller only); its stream error event, and/api/directory/submit's, carry the samekindalongside the message string clients already parse. Requirespnpm db:migrate:prod(0030_audit_status_typed, which widens theaudit_history.statusandsites.audit_statusCHECK constraints) before this deploy — code first would make every typed failure write violate the old constraint. No row is relabelled retroactively; the next rescore writes typed statuses. Regression tests:tests/audit-error.test.ts,tests/audit-write.test.ts,tests/rescore-site.test.ts,tests/run-site-audit-failure.test.ts,tests/refresh-site-audit-failure.test.ts,tests/audit-route-failures.test.ts. -
Failure honesty: the leaderboard and the audit tool say why a site has no score —
/leaderboard's "Couldn't measure" list showed bare domains; each row now carries a status badge (Anti-bot block / Unreachable / Auth-gated / Rate-limited / Timeout / Couldn't tell / Error / Not attempted)./leaderboard/[domain]heads an unmeasured domain with "This site has no public score", the badge, a next action naming what the site must change (no surface promises a rescore it cannot deliver), and an "Audit now" link that runs the audit live; its page metadata says "unmeasured (<label>)" instead of "Site Not Found". The audit tool and the directory submit form label a failed run with the same badge plus the engine's message, and an unrecognisedkindfrom a newer core falls back instead of blanking the page. Archived leaderboard editions, which predate these statuses, are normalised on read toErrorrather than a guessed kind. Until the rescore runs, today'serrorrows simply render an "Error" badge. Regression tests:tests/leaderboard-unmeasured.test.tsx,tests/audit-client-failure.test.ts,tests/audit-failure-ui.test.tsx,tests/submit-stream-kind.test.ts. -
Audit accuracy: declared AI-usage preferences are reported (tracked, core tarball update) — new tracked check
usage-preference-declared(weight 0, not scored) reads the robots.txt the crawl already fetched forContent-Signal:, AIPREFContent-Usage:and RSLLicense:lines, the homepageContent-Usageresponse header, and one new well-known probe/.well-known/tdmrep.json(behind the catch-all guard). It passes when any declaration is found and reports which vocabulary declared it, whether a training opt-out applies to a generic agent at the site root, and whether the robots file carries Cloudflare's managed Content Signals block — so a deliberate training opt-out reads as a stated preference rather than a misconfiguration. Declarations scoped to another user-agent or path are reported as scoped instead of being counted as the site-wide verdict. Nothing moves in any score: the check is weight 0, so the rubric stays 3.0 and category sums are unchanged. The rubric is now 56 checks, 6 of them tracked (site-type adapter checks still run on top of it, as before), and the new row shows under "Tracked (not scored)"; leaderboard rows pick it up at the next rescore. Regression tests: coretests/preferences.test.ts,tests/catch-all-guard.test.ts. -
Audit accuracy: edge treatment of agent user-agents is reported (tracked, core tarball update) — robots.txt can say "allowed" while a CDN or WAF challenges the agent at the edge, which no check could see. The existing multi-user-agent pass now also records each response's status, a small header set, a 4 KB body prefix and any challenge fingerprint, for the homepage plus up to two extra pages. The probe picks those extra pages itself, inside
probeSiteMetadata— the crawl's own ranking re-run over the homepage's links and the sitemap, so it takes the URLs a profile crawl would take first — but it fetches them itself instead of reusing the crawl's fetches, so the probed set is not guaranteed to be the set the audit scored. Each page gets its own control under our declared crawler, as a separate single-shot fetch — never the crawl's retrying page fetch, which could turn one transient 503 into anagent-rejected— and the fetches this pass adds are paced by the crawl'srequestDelay. New tracked checkedge-agent-access(weight 0, not scored) compares them: a page counts as evidence only when its control came backok, 2xx and challenge-free, so an edge that challenges every user-agent alike no longer reads as an agent block. A challenge, 403 or 429 where the control got 2xx isagent-blocked, another non-2xx isagent-rejected, a 402 ispaywalled;consistentneeds completeness as well as agreement — some comparable page where every probed user-agent answered 2xx — and no usable control, no agent response, or no page where all of them answered isunmeasured;detailscarries the per-page, per-agent grid and the documented agents with no published UA string are listed as not probed. It is tracked rather than scored because the probe identifies as an agent from our own network and so is not a CDN-verified agent request, and thereach-*checks are unchanged so nothing is counted twice. No score moves; from the next rescore every okaudit_historyrow records the pass/fail status of both tracked checks incheck_results, which is what a future decision to score this check would measure prevalence from.check_resultsholds only{status, score}per check, so the per-agent grid and theoutcomevalue are not queryable across sites — they survive only inside each run's full report JSON (audits/<id>.jsonbehind/audit/[id],sites/<slug>/audit.jsonfor a hosted site) and in the rendered report. Regression tests: coretests/edge-probe.test.ts,tests/edge-agent-access.test.ts; platformtests/audit-summary-tracked-persistence.test.ts.
2026-09-14
- Score integrity: deploy no longer accepts client-asserted scores —
POST /api/agent/deployignoresauditScore/auditGradein the body; only the server-side post-deploy audit writesaudit_history. That audit now records the full result (category scores + llms.txt/agent-card/MCP flags) via the sharedsummarizeAuditReporthelper (lib/audit-summary.ts, also used byscripts/rescore-all.ts), and the score-onlyminimalwrite path is removed fromrecordAuditAttempt. - Score integrity: failed-audit sites no longer leak stale scores — directory search
min_scoreonly matches sites whose latest audit succeeded (also in the fuzzy-match fallback, which now honours all structured filters and only serves the first page) and orders by the public score (failed-audit sites and unscored sites last); the quality-score cron ignoresaudit_scorewhen the latest audit failed;has_mcpnow means themcp-discoverycheck passed (WebMCP / MCP metadata checks no longer set it — leaderboard MCP adoption will drop after the next rescore; expected);scripts/rescore-all.tscounts a failed DB write as a write error instead of recording the site asstatus='error'. - Score integrity: one crawl profile for every score — deploy, refresh, directory submit/register,
/api/audit,scripts/rescore-all.tsand calibration now audit on core'sAUDIT_CRAWL_PROFILE(10 pages, 10 s; previously rescore 5 pages/15 s, refresh/submit 20 pages, deploy the whole deploy crawl)./api/auditno longer honours a clientoptions.maxPages. The platform's reducedfetchSiteMetadatais removed — spider.cloud and incremental crawls take metadata from coreprobeSiteMetadata(MCP init, 8 well-knowns, multi-UA), so a spider-cloud redeploy no longer flipshas_mcp/category scores against rescore. Spider-cloud deploys' audits now run their own fresh profile crawl. Scores for sites last scored byrescore-allat 5 pages may move on their next audit. - Score integrity:
audit_history.crawl_profile— every audit attempt row (ok and failed) now records core'sAUDIT_CRAWL_PROFILE_ID, so a score move caused by a crawl-profile change is visible in history. Migration0028_audit_crawl_profileadds the nullable column; older rows stay NULL (unknown profile). Requirespnpm db:migrate:prodbefore deploy. - Security: call-time SSRF re-validation of stored agent endpoints — the health-check cron (
checkEndpoint),route_queryrouting and workflow steps now re-check the stored endpoint (DNS included) at call time, so an endpoint registered while public that later resolves to a private/metadata address is refused and never contacted. Routing and workflows refuse before connecting (DNS bounded to 5 s), and their MCP transports send every request (initialize, tools/list, tools/call, SSE GET, DELETE) through coreguardedFetch, so each request URL and each redirect hop is checked. Health checks also go throughguardedFetch: redirects to public hosts are followed and the final status is judged (as before), redirects to private hosts are refused; latency now includes the per-hop DNS checks. A refused, unresolvable or DNS-timed-out endpoint counts as a health-check failure. The same per-hop check now covers hosted-agent form-submit and OpenAPI tool calls and the register endpoint reachability probe. IPv4-mapped/compatible and NAT64 IPv6 addresses (any spelling, e.g.::ffff:a00:1) are decoded and judged by the IPv4 rules, and::is blocked. - Security: every audit/crawl/generate fetch and redirect hop is private-IP checked —
w2agent-coregained anisAllowedTargethook applied to each fetch and each redirect hop; the platform passesisAllowedCrawlTarget(lib/url-validation.ts:validateCrawlUrlbounded to the audit per-request timeout, 10 s) into all audits (runProfileAudit: deploy, refresh, submit, register, rescore;/api/audit), legacy/spider-cloud/incremental crawls, the register metadata probe,/api/generateand on-demand search page fetches. Incremental-refresh page refetches now go through coreguardedFetch, so a refused target keeps the stored page. Previously only the submitted URL was validated, so a public site's sitemap entry (subdomain) or a redirect could point the crawler at an internal address. Crawled page URLs whose path matches a cloud-metadata pattern (e.g./metadata/v1/…) are now skipped, and each crawl fetch costs up to two extra DNS lookups. - CI: typecheck + production build —
.github/workflows/ci.ymlnow runs Lint → Typecheck (tsc --noEmit) → Test → Build (next build). The 51 pre-existing type errors (all in test fixtures/mocks) are fixed; the build needs no secrets or env beyondNEXT_TELEMETRY_DISABLED. - Audit accuracy (rubric v3.0 groundwork, core tarball update) — catch-all guard: a well-known file only counts when it is served 200 with a JSON content-type and differs from the site's soft-404 body; robots.txt/llms.txt served as HTML count as absent; sites answering 200 for non-existent paths get a
catch-all-responsesreport warning. Tracked (not scored): checks marked tracked are excluded from category scores, top issues, warnings and site flags, and the audit check list shows them in a separate neutral "Tracked (not scored)" section, never as fixes (no check is tracked yet). Retired:well-known-agents-json,well-known-araandwell-known-ai-pluginare removed and those paths are no longer probed (5 well-knowns now);sim-chatgptexpects only an OpenAPI spec; directory register no longer falls back toagents.jsonas an agent card. llms.txt reweight:llms-txt-exists2→0.5,llms-txt-valid1→0.5,llms-txt-links-resolve1→0.5, and the missing-llms.txt message no longer claims LLMs can't discover the site. Rubric version stays "2.0" until the v3.0 bump; scores (agent-discovery especially, and sites that passed on catch-all 200s) will move at the next rescore. - Audit accuracy: MCP discovery rewrite (core tarball update) — the audit now finds MCP servers by posting
server/discover(MCP 2026-07-28) to same-site/mcp//sse/mcp.*links on the homepage, then/mcp,mcp.<host>/mcpandmcp.<host>/, falling back to a 2025-11-25initialize(and a legacy SSEendpointevent) per candidate./.well-known/mcpis no longer probed.mcp-discoverypasses 100 on a live handshake, 75 on an OAuth 401 (WWW-Authenticate: Bearerwithresource_metadata=), partial 50 for legacy HTTP+SSE only; only 307/308 redirects are followed. The MCP auth/metadata checks (C1/C2/B3/D4) are N/A only when no MCP endpoint was confirmed. Directory register (andscripts/auto-discover.ts) mark a site self-hosted when core confirmed an MCP endpoint and use that URL as the default agent endpoint (previously<origin>/.well-known/mcp). - Audit accuracy: api-catalog and operator signing (core tarball update) —
api-catalog-rfc9727now requires an RFC 9727 linkset served asapplication/linkset+json(at least onelinksetentry with ananchoranditem/service-desc/service-doclinks); the oldapis[]shape fails.web-bot-auth-directoryis retired and replaced by the tracked (not scored)agent-operator-signingcheck: a signing directory served asapplication/http-message-signatures-directory+jsonwith an Ed25519 key and asignature_agenton the site's own origin. Tracked checks that skip (the site has no such file) are hidden from the audit check list and the CLI verbose report. - Audit accuracy: AI agent access and user-agent reach (core tarball update) —
robots-txt-ai-botsis replaced byai-agent-access, which reads robots.txt per purpose tier from a vendor-cited token table (training, search, user-initiated fetchers). Only blocking search or user-fetch agents lowers the score; training opt-outs are neutral.agent-allowlistis retired. The fivesim-*checks are replaced byreach-openai-user-agent,reach-perplexity-user-agentandreach-google-user-agent(weight 0.5 each; the homepage must return 2xx with a body under ChatGPT-User, Perplexity-User and Google-Agent). Claude is omitted because Anthropic publishes no user-agent string, and DeepSeek is dropped. A new check statuserrormeans "unmeasured" (for example, the crawl budget ran out): it is left out of the score, raises an "unmeasured checks" audit warning, and renders in the audit check list as a blue?marked "unmeasured" (hidden for tracked checks). - Audit accuracy: robots.txt rate limits and Crawl-delay (core tarball update) — a robots.txt that keeps answering 429 through every retry now scores
ai-agent-accessaserror(unmeasured) instead of pass, since the file was never seen; genuine 403/404/410 still pass. The generated robots.txt fix keeps a moved agent'sCrawl-delay. - Audit accuracy: OAuth discovery, server card, A2A v1.0 agent cards (core tarball update) — six OAuth/MCP-auth checks (
oauth-support,oauth-protected-resource,oauth-metadata-rfc9728,mcp-auth-mechanism,mcp-oauth-metadata,mcp-pkce-s256) are replaced byoauth-discovery(follows a 401WWW-Authenticateor protected-resource metadata to an issuer-matched authorization server, including off-origin servers through the SSRF guard),mcp-authandcimd-support;pkce-s256now reads the discovered server.mcp-server-cardmoves to Agent Discovery as a tracked check (<mcp endpoint>/server-card, an ai-catalog entry, or/.well-known/mcp/server-card.json;/.well-known/mcp.jsonis no longer probed).well-known-agent-cardvalidates the A2A v1.0 shape: v0.3 cards and cards whose only interface is the homepage score partial. 54 checks total. Platform: the leaderboard/directory agent-card flag now counts a pass or partialwell-known-agent-card(a v0.3 card is still a card; a JSON file that is not an A2A card no longer counts); directory register reads the agent endpoint from v1.0supportedInterfaces(JSONRPC preferred); hosted agent cards add the v1.0 fields (supportedInterfaces,defaultInputModes/defaultOutputModes, skilltags);/learn/agent-carddocuments the v1.0 shape. - Audit accuracy: UCP profile validation and WebMCP tracked (core tarball update) —
well-known-ucpnow passes only for a profile with a top-levelucpobject carrying aYYYY-MM-DDversionand non-emptyservicesandpayment_handlersmaps; any other JSON fails and the message names the missing fields. A missing or invalid profile counts only on sites detected as ecommerce (not applicable elsewhere; a valid profile still passes on a store the detector missed). Both WebMCP checks (webmcp-declarative,webmcp-imperative) are now tracked (not scored): they show under "Tracked (not scored)" when detected and are hidden otherwise. Imperative detection recognisesdocument.modelContext(andnavigator.modelContext) in inline scripts, up to 5 same-origin script files when no inline code is found (www/apex variants included,<base href>honoured, redirects followed only within the site, fetched through the SSRF guard), and a WebMCP origin-trial token; declarative detection counts everytoolnameform plus unannotated task forms (unannotated search, login and newsletter forms excluded). Generated WebMCP scripts usedocument.modelContextwith anavigator.modelContextfallback. Agent-protocols category scores move at the next rescore. Hosted ecommerce agents' generated/.well-known/ucpmanifest has nopayment_handlers, so it failswell-known-ucpwhen audited. - Rubric 3.0 (core tarball 0.2.0) — the platform takes
RUBRIC_VERSIONfrom core (lib/rubric.tsre-exports it; no platform copy), so audits andaudit_historyrows are stamped3.0. The dismissible "Rubric updated" banner on/auditand/leaderboardis replaced by a permanent notice "Rubric 3.0 since 2026-09-14 — see what changed";/audit/[id]labels a stored report with the rubric it was scored under (a 2.0 report stays "Rubric v2.0"). Migration0029_audit_check_resultsadds nullableaudit_history.check_results({ status, score }per check, tracked checks included; NULL on failed attempts) and drops the'2.0'default onrubric_version.scripts/rescore-all.ts --rubric-compareruns the frozen 2.0 engine and the 3.0 engine side by side per site without writing to the database and writes a JSON report (median deltas overall, per category and per site type; per-check status transitions). Requirespnpm db:migrate:prod(0028 and 0029) before deploy. - Methodology spec v1.0-draft and rubric changelog — the methodology spec moves from its v0.1 filename to
docs/spec/agent-readiness-spec-v1.0-draft.mdand is updated to rubric 3.0: theerror(unmeasured) status and tracked checks in the scoring algorithm, the N/A cascade table as the engine runs it, the catch-all / soft-404 evidence rule, a catalog of the 54 current checks with a "Tracked (not scored)" list and a "Retired in 3.0" list, and a new normative emerging-standards policy (scored / tracked / retired states, changes only at a major version bump, newly promoted checks at weight 0.5 or less for one cycle, merges are not promotions, documented exceptions for UCP,cimd-supportand llms.txt). Core shipsdocs/RUBRIC-CHANGELOG.md(version bump rules and every 3.0 check change with old/new weight and rationale; the measured 2.0 → 3.0 delta is pending the owner's--rubric-comparerun). No score changes.