V2 vs V1

Two different things are called “v2” on this site, and they confuse everyone in the same way. The llms.txt spec went from v1 to v2 on 2026-08-10, and a file written against v1 is still valid. The hub’s pipeline went from V1 to V2 on 2026-08-30, and the V1 artifacts are retired. One is a proposal that loosened; the other is a toolchain that was replaced. They are laid out side by side below because a reader with a 2025 file usually needs both answers: “is my file still valid?” (yes) and “why did the folder layout change?” (because the old one produced output nobody consumed).

The spec: v1 → v2

The spec’s structure did not change: an optional BOM, an H1, a blockquote, free markdown without headings, then H2 sections holding - [name](url): notes lines. What changed is which parts are required, where the file may live, and how a consumer is expected to use it.

Rulev1v2 (2026-08-10)Effect on an existing file
Required elementsH1 + blockquote + sections impliedH1 only is required; blockquote, prose and sections are optionalnone is invalidated; the lint still scores a missing blockquote as Medium (I2) — a quality finding, not a validity one
Placement/llms.txt at the site rootroot or any subpath (/docs/llms.txt); a file covers the URLs under its path; where several apply, the most specific winsenables families and split roots (<section>/llms.txt)
DiscoverynoneLink: <…>; rel="describedby" on the files; rel="alternate" type="text/markdown" on HTML pagesadd two headers (see the serving reference)
Markdown twinspage.html.mdpage.html.md or page.md; directories append index.html.md or index.mdeither form passes the twin probe
## Optionalmechanical: “can be skipped if a shorter context is needed”, consumed by llms_txt2ctxa convention for secondary information; llms_txt2ctx and its context-expansion mechanics are no longer part of the proposalkeep it last; build nothing that depends on it
BOMan optional BOM is toleratedthe lint strips it as hygiene (P14)
Consumption expectationexpand the file into context“view or search the index, then follow the relevant links”; the index stays small; detail lives behind linksthe size ladder (small / full) becomes the producer’s job
/.well-known/explicitly rejected: well-known URIs exist only at the origin root, which defeats subpath scopingserve at the root or the subpath, not under .well-known

The one-sentence summary: v2 made the file smaller in obligation and larger in reach. Less is required, more places may hold one, and the reader is now told to search-and-follow rather than to inhale. The consumption sentence is the important one for producers. If an agent is expected to read the index and then fetch two pages, the index has to be small enough to read and descriptive enough to choose from — which is why the lint measures both.

The pipeline: V1 → V2

The hub’s V1 pipeline produced site dumps. It crawled with trafilatura, wrote one banner mirror per site, distilled that mirror with a zero-LLM bulk pass, and indexed the raw text in one vector layer. The distilled output was never consumed: the working notes describe it as “hundreds of pages of repeating internal links”. V2 keeps the banner mirror as the internal format and replaces everything around it.

StageV1 (to 2026-08-29)V2 (from 2026-08-30)
Acquiretrafilatura BFS crawl → banner mirrora ladder in llms_acquire.py: the site’s llms-full.txt → its llms.txt + .md twins → Accept: text/markdown → a docs API → a structured crawl; the banner mirror stays the internal format
Cleannone (raw HTML → text)docset_refine clean: boilerplate lines, MDX → markdown, page classes (reference / guide / changelog / marketing / index)
Extractdistill_offline.py bulk — zero-LLM, output never consumedextract (code snippets, table rows → parameter, definitions, changelog change units; anchors to real source headings) + units (local LLM under the evidence rule) + polish (Claude)
Exportnoneexport_llms: index (split above 10 KB) / full (Mintlify grammar) / small (≤ ~50k tokens) / facts / manifest.json; topical; vocabulary
Indexone raw vector layer (nomic-embed-text in hub.db for files; mxbai-embed-large for docsets)raw and facts vector layers, plus an FTS5 keyword layer beside each (docset_indexer keyword-index)
Serveweb-text-mirror --serve (HTML)llms_serve.py: /llms.txt, /d/<stem>/… (with sections), /m/<key>/…, /t/<slug>/…, markdown headers on every response
Gatenonellms_lint.py (the deterministic passes P0–P3, P5–P7, P9, P14) inside docset_rollout cleanup; /ldo for the model, live and family passes
Artifacts<stem>.pages/, _master.md, ._distill_index.json<stem>.reference/{pages.json, structured.jsonl, units.jsonl, all_units.jsonl} and <stem>.llms/

“V2” for the pipeline is a naming choice made on this site; the code carries no version constant. Dating it (2026-08-30) is more honest than numbering it, and the tables here do that.

The measured shape of one V2 export, from outputs/exports/code.claude.com.llms/manifest.json (191 pages, acquired from the publisher’s own llms-full.txt): the root index is 1,136 bytes (~280 tokens) and splits into three section indexes; llms-small.txt is 199,155 chars (~49,785 tokens, just under the budget); llms-full.txt is ~2.1M tokens; llms-facts.txt holds 14,031 units (~845k tokens). The V1 pipeline had no equivalent numbers to print, which is its own summary.

Migration

For a publisher with a v1 file:

  1. Run the migrate check (llmsx migrate <url|file>; today, llms_lint.py check <file> with --check-links). It is the lint with a V1→V2 lens: findings are mapped to the steps below.
  2. If the report says full file wearing the wrong name — page bodies inside llms.txt, a file over 100 KB (I6) — split it into llms.txt + llms-full.txt. docset_refine export does this from a mirror; by hand, the index keeps the link lines and the full file takes the bodies in the Mintlify grammar (# Title / Source: <url> / blank / body).
  3. Add .md twins for every linked page (either form) and the two Link headers (H3).
  4. Move skippable material — changelog, legal, old posts — to a trailing ## Optional (N4).
  5. If the index is over 10 KB: hub-and-spoke split. ## Sections in the root, one <slug>/llms.txt per section, counts on every section line.
  6. Re-lint. The bar is 0 High.

For a hub user with V1 folders: run extract → render → export on each mirror, then docset_rollout cleanup to retire the V1 artifacts. Cleanup only removes a site’s .pages/, _master.md and _distill_index.json once a fact layer exists for it, so nothing is lost before its replacement is in place.

For most 2025 files the report’s first line is “nothing required”. The recommendations that follow are the twins and the headers, because those are what v2 added for consumers to find the file at all.

Compatibility matrix

Rows are producer choices; columns are consumers. The cells are dated evidence (verified 2026-08-30) and need re-checking every 90 days, because consumer behaviour is the part of this table nobody controls.

producer choiceClaude Code (WebFetch / hub MCP)Cursorgeneric MCP clientllms_acquirelintLighthouse agentic audit
v1 file at rootworksworksworksworksworks (I2 Medium if no blockquote)works
v2 file at rootworksworksworksworksworksworks
v2 file at a subpath onlyworks if given the URLdegraded — no root discoveryworks if given the URLworks — the ladder probes the given pathworksdegraded — expects the root
split root (## Sections)works — follows section linksworks — one extra hopworksworks — recurses by path then part-Nworks — check DIR walks sectionsworks
family file (links only indexes)worksworksworksworksworks (F1 requires index targets)not evaluated
llms-full.txt, Mintlify grammarworks via hub_llms_full_read(page=…)degraded above ~50k tokens (the consumer ceiling)worksworks — split_llms_full round-tripsworksnot evaluated
llms-full.txt, YAML-block or Cloudflare frontmatter grammarworksdegraded above ~50k tokensworksworks — grammar detected from the header commentworksnot evaluated
llms-full.txt served as llms.txtdegraded — the index is unreadable at that sizebreaksdegradedworks — detected and splitHigh (I6)breaks
no .md twinsworks — fetches HTMLworksworksdegraded — falls to the Accept probe or the crawlHigh (N6, with --check-links)degraded
no Link headersworksworksworksworksLow (H3)degraded

Read down a column to see what a given consumer needs; read across a row to see what a given choice costs. The only cell that breaks everything is the full file served under the index’s name.

What breaks

Honest list of what does not survive the two transitions.

On the spec side, nothing a v1 file relied on is invalidated, but two things stop meaning what they meant:

On the pipeline side:

What does not break, and is worth saying plainly: every v1 llms.txt still parses, still lints, and still answers questions for an agent handed its URL. The migration guide above is a list of recommendations for one, not repairs.