Skip to content

Docs site: the docs/ tree, compiled and hosted

The human-facing docs (docs/, organized by Diataxis) are published as a static site — searchable, dark/light, with mermaid diagrams rendered — without ever ceasing to be the markdown that renders on GitHub. There is one tree, one source of truth, two surfaces: a reader on github.com follows a relative .md link to the file, and a reader on the site follows the same link to a clean URL, because the compiler rewrites it.

The mechanical half of the reference pages is not written by hand at all: every command table comes from the command registry (command-registry.md) through a generator, and CI fails when the committed tables and the code disagree. That is what "docs change as the code changes" means here — not a reminder, a check.

The site is an assets-only Cloudflare Worker, deliberately not part of the bot's Worker: a docs typo must never require a container rollover (the deploy hazard in slack-channel.md items 7–8), so docs deploy from CI in seconds on every docs push, while the four runtime Workers deploy on the release — preflighted, in order, only the ones the release changed (release-and-deploy.md).

Hosts

HostWhatAuth
The host of project.json's docsThe compiled site (<name>-docs, assets only) — the project's website, deployed by the project's own CI; never a Worker an installation runsPublic: the project's site is world-readable on its product domain, and workers_dev is disabled so no twin exists outside that hostname
<bot host>/docs…A 302 to the host above, subpath preserved — the same on every installationPublic: it discloses only the docs hostname

Behavior

  1. One tree, two surfaces. The site is built in place from docs/ (docs/.vitepress/), never from a copy. A relative in-tree link (reference/cli.md) resolves on GitHub to the file and on the site to /reference/cli. A relative link that leaves the tree (root README.md, AGENTS.md, a source file) resolves on GitHub to the file and is rewritten at build time to that file's page in the repository, because the site has no page for it; the lint (src/docs/links.test.ts) requires the target to exist, since the site's dead-link check never sees it. Absolute URLs are linted too: a malformed separator (https:///host) is invisible to a URL parser — for http(s) the WHATWG parser collapses the extra slashes, so such a link still reaches the right page — which leaves a literal check as the only thing that can hold the line; code spans are quoted text and are not linted. Inline code is literal on the site (v-pre), so a {{placeholder}} in a code span is text, never a Vue interpolation.
  2. README.md is the directory index on both surfaces. GitHub renders a directory's README.md when you browse to the directory; the compiler's rewrites map every README.md to that directory's index.html, so a bare [Tutorials](tutorials/) link works in both places. Each of the four Diataxis directories carries one, and it says what that kind of page is for; the specs directory beneath reference carries one too, two levels deep, and the rewrite covers both depths (check:site requires its built index — the dev server resolves a directory's README on its own, so only the built tree shows a missing rule).
  3. docs/plans/** is not published. Dated implementation plans are working documents for the repo, excluded by srcExclude; they stay readable on GitHub.
  4. A dead internal link fails the build (ignoreDeadLinks: false), so a renamed or deleted page cannot merge while links still point at it.
  5. Mermaid renders on both surfaces. A ```mermaid fence is a diagram on GitHub and a rendered diagram on the site: the plugin's fence rule turns it into a component, the theme's own renderer draws it (item 19), and the library is bundled — the page fetches nothing from a CDN at runtime.
  6. The mechanical reference tables are generated, and the markers say so. A region between <!-- generated:<name> · … --> and <!-- /generated:<name> --> is written by npm run docs:gen from the command registry and by nothing else. Today: cli-commands in reference/cli.md (every command in CLI form, with the surfaces it reaches), chat-commands in reference/slack-commands.md (the chat-reachable subset, with who may run each), api-routes in reference/dashboard-routes.md (/api/<group>.<verb>, methods from the effect, scope), capability-commands in how-to/turn-features-on-and-off.md (one row per capability axis with the commands whose enabledWhen depends on it, read off the registry through dependsOncapabilities.md item 3; the prose matrix around it is hand-written).
  7. The generator writes only between the markers and is idempotent: regenerating identical content reports no change. A missing marker, an unclosed region, or a marker no renderer owns is a reported problem — never a silent no-op, which is the drift this mechanism exists to prevent.
  8. npm run docs:check is the drift gate. It re-renders every region and exits non-zero, naming the file, when the committed text differs — the same discipline skills:check applies to vendored skills. It needs no network, no config, and no deps beyond the registry, so it runs from a bare worktree.
  9. Table-safe rendering. describe texts are code, not prose written for a table: a | is escaped, newlines are flattened, and a bare <agent> is escaped to an entity — but never inside an inline code span, where an entity would render literally. (An unescaped <agent> is an invisible HTML tag on GitHub and an unknown component to the compiler's Vue pass.)
  10. /docs is the stable in-product path. GET /docs302 to the docs site's home; GET /docs/<path>302 to <path> on the docs site, so a deep link can be typed, bookmarked, or shared. The target is the project's published docs — PROJECT_DOCS_URL in src/core/docsLink.ts, held equal to project.json's docs by check:project-facts — on every installation, and never taken from the request or from the environment: there is no open redirect and no per-installation docs site to point at (the site is the project's, item 12). The path is not in the Access-gated set: the redirect is public, and so is the destination. The startup log states where /docs points.
  11. Every dashboard page links to the docs. The shared header (/runs, /runs/:id, /runs/scheduled, /residents, /residents/:owner/:name, /costs) carries a docs icon button that opens /docs in a new tab — the app knows the path, never the docs hostname. No capability gates it: the site is the project's and every installation has it (capabilities.md has no docs axis). It is not a fourth section in the site nav (it leaves the app), and below sm it folds into the hamburger menu as its own group above the sections.
  12. CI deploys the docs; nothing else does. CI runs on every PR and push and proves three separate things, each an npm run script a contributor runs locally: the generated tables match the registry (docs:check, part of verify:root), the site builds with no dead links (verify -w docs), and the deploy itself would be accepted — the built tree carries every page the site must have (index, 404, the four Diataxis indexes, the specs index nested under reference, a reference page; check:site) and none it must not (plans/), and wrangler deploy --dry-run (verify -w deploy/cloudflare-docs) validates wrangler.jsonc and resolves assets.directory against that tree. The dry-run needs no credentials, uploads nothing, and touches no account; without it, nothing would read the Worker's config until the push to main, after review is over. The docs-deploy job runs only on main, only when that push touched docs/ or deploy/cloudflare-docs/, and builds before uploading so a stale dist cannot ship. With no CLOUDFLARE_API_TOKEN repository secret it logs a warning and skips rather than failing the build; when the changed-file diff cannot be computed it deploys (fails open toward freshness). The site is the project's website, not a Worker of an installation: its script name is <name>-docs and its hostname the host of docs, both from project.json (siteView in src/deploy/wranglerTemplate.ts), so no deployment profile names a docs Worker (WORKER_KINDS has none, the profile schema has no workers.docs, deploy plan has no docs step) and an installation never deploys a copy of the site — its /docs sends people to the published one (item 10). The one value the profile contributes is the account: the job renders the site's wrangler.jsonc from the project's own production profile first (SWITCHBOARD_DEPLOY_PROFILE names it in the installation's private configuration repository, read with a read-only App token as CONFIG_REPO_TOKEN), so the upload can never carry the example account; CLOUDFLARE_ACCOUNT_ID is never set in CI — the account comes from the profile and the env var would override it. deploy init renders the site's config beside the Workers' from the same facts, so verify -w deploy/cloudflare-docs dry-runs the real name and host on every PR.
  13. The docs Worker cannot disturb a run. It has no main, no Durable Object, and no container, so it has no rollout, no drain, and no preflight; deploying it while a run is in flight is a no-op for the bot.
  14. AGENTS.md is an index under a budget, and its command table is generated. AGENTS.md is read by every agent on every task, so it is held under 15 KiB (agents:check, part of check:consistency) and its Commands table — every root package.json script with what it does and when to run it — is written by npm run agents:gen into a <!-- generated:commands --> region from package.json (which scripts exist) and project.json (the prose). A script with no { does, when } entry fails the check, so a script cannot ship undocumented; an entry for a script that no longer exists fails it too. The module-by-module map, operations runbook, sizing notes and known limits that used to sit in AGENTS.md are pages under docs/ that it links.
  15. One statement of project facts. project.json names the project twice — name, the identifier (package names, the CLI, the bot's mention, config keys, Worker script names, the repository), and displayName, the name a reader sees (the README's first heading, the site's title and hero, the subject of the GitHub description) — plus its one-sentence description, its organization, repository, docs URL, npmPackage (the package it publishes, @<scope>/<name>), topics, contact address and steward. npm run check:project-facts (part of check:consistency) reads every hand-written copy — the community files, the README (its first # heading must be displayName) and its badge row (github.com paths, the Scorecard URL, the shields.io release badge), docs/README.md, this spec, the /docs redirect constant, the docs Worker's route — and fails naming each copy that disagrees (a stale contact address, a docs URL on another host, the repository under another owner, package.json's five identity fields, a package mentioned under the project's npm scope that is not npmPackage, the README headed with another name). npmPackage must be a scoped npm name whose unscoped part is name, so npm install <npmPackage> and the identifier agree. The description and topics have no copy in the tree: gh repo edit reads them from the file (Configure the repository), so the check holds them to what GitHub accepts — a non-blank description of at most 350 characters, 1–20 topics of [a-z0-9-]{1,50}. The docs site has no copy either: docs/.vitepress/config.ts reads displayName for the site title, and the landing hero reads it back as site.title, so the name is proven on the built artifact (check:site, item 18) rather than on a source copy. Changing a fact is one edit plus the list the check prints. In prose, the product is displayName; the bot's mention, the CLI and the package are name, in a code span — identifiers are never renamed for the display name.
  16. Records are frozen by a check, not by discipline. A record — a decision record under docs/decisions/ or a dated plan under docs/plans/ — carries frontmatter status from the closed set proposed | accepted | implemented | superseded and a date; a superseded one names superseded_by, a path that resolves. npm run decisions:check (in check:consistency) fails the build when any of that is missing, and diffs every record that was already accepted, implemented or superseded on origin/main: its body and every frontmatter key but status / superseded_by must be byte-identical, the record must still exist (a deletion or rename fails the same way), and a status never moves backwards. The remedy the message names is to write a new record and supersede the old one. Without a reachable base (a shallow clone) the immutability half is skipped and said so; CI's checkout fetches the full history for it.
  17. The Design decisions index is generated. docs/explanation/design-decisions.md carries a decision-records region that npm run docs:gen renders from the records' own frontmatter — id, title, pattern, status (a superseded record links forward, whichever superseded_by form the gate accepted), date — so a new record or a status change reaches the site by regeneration, never by hand; docs:check guards it like the command tables.
  18. The root route is the landing page. docs/README.md stays the tree's hub on GitHub; on the site the same route renders as a home page — the compiler marks the source file layout: home in transformPageData, so no frontmatter reaches GitHub, and the tab reads the site's name rather than the hub's heading. The site's name is project.json's displayName, read by config.ts at build time — the title, the home page's tab, the nav entry that names the product — never a copy typed into the theme (item 15). The landing (theme/LandingPage.vue) comes first, and nothing on it is a paragraph — every line of text is under twenty words: the eyebrow naming the product (an element of class product reading site.title, so it follows displayName too), a two-line headline in the display face, one sentence, two doors in (get started, the repository — project.json's repository, imported, not typed) and the install line (npx <npmPackage> init, read from project.json, with a copy button and no clipboard library); then the run page in a full-measure window frame; then a statement beside each of three further pictures (item 20: the runs index, the residents, the spend page — each in the theme's SHOTS list, cropped from the top to the ratio its content fills, right–left–right and stacked on a phone), the statement two-tone — its first clause in ink, its second in the muted grey — with the page that proves it; then a thread drawn in CSS from the fixture's made-up workspace (a mention of name, a status card with three ticked steps and one in progress, the reply with the pull request; the in-progress ring stops under prefers-reduced-motion: reduce), never a capture of a real conversation; the four seams as one row, each term with its implementations from theme/seams.mjs (item 21) and its page; and the three commands (init, ask, deploy all) with one closing line. Every picture carries its 2880×1800 dimensions so the layout never moves. The run page is one eager image (fetchpriority="high", so the preload scanner starts the largest paint from the HTML) inside a <picture> whose source picks the dark file under prefers-color-scheme: dark; a reader whose appearance differs from the OS scheme gets one swap after mount, and a toggle swaps again — everyone else fetches exactly one hero file. The three section pictures are lazy light/dark pairs, so a hidden appearance's copy is never fetched; the README's own content follows as the docs map, and the footer names the license and Discussions. The landing's links and image sources are component attributes the dead-link build never sees, so src/docs/links.test.ts resolves each internal path to a page in this tree, each fragment to a heading on it, and each image to a file under docs/public/.
  19. One visual system, offline. theme/product.css is the whole look: the dashboard's grounds and green in both modes (so the docs and the dashboard read as one product), one warm accent, and three faces — Bricolage Grotesque for titles (a variable grotesk; the opsz build, set at its 96-point optical size, weight 600, letters tightened), Geist for text, Geist Mono for code — bundled from the docs workspace's own @fontsource-variable dependencies and the two above the fold preloaded (PRELOADED_FONTS in config.ts names the built woff2 files), so the site builds and serves with no request to a third party for a font or a script. The landing's hero wears one hairline texture — CSS lines at a few percent of ink, fading toward the picture — and no image or gradient is ever a background. Diagrams are part of the system: theme/MermaidDiagram.vue draws every fence with the --sb-diagram-* tokens read from the page at draw time and redraws on an appearance change, waits for the faces before measuring labels, and imports the library only on a page that has a diagram — the plugin's own renderer would have put it in every page's entry chunk, and knew only mermaid's built-in dark theme. The home page scores at least 95 in each of Lighthouse's four categories. One mark, three copies of one drawing: three isometric planes with one message entering the top plane and routed to two lanes on the middle one — docs/public/logo-light.svg / logo-dark.svg beside the site name (themeConfig.logo, one file per scheme because an <img> cannot inherit currentColor), docs/public/favicon.svg for the site's tab plus docs/public/favicon.ico — the same mark as three PNG frames (16, 32, 48) for every client that asks for /favicon.ico by convention without reading the page, both named in the site's head — the dashboard's BrandMark.vue (inline, ink from the text color, the top face from the page ground, the route in the dashboard's green) in every page header and its FAVICON_MARK_SVG for the tabs that claim no state, and the README's title lockup.
  20. The dashboard's pictures are rendered, not taken, and their inputs are pinned. The screenshots the README, the landing page (item 18) and the architecture page show — the runs index, a finished run's page, the residents, the spend page and the scheduled tab, each in light and dark — come from npm run screenshots:gen: it builds the dashboard, serves the fixture preview (scripts/web-preview.ts) and the browser at one fixed clock, viewport (1440×900 at 2×), locale and timezone, with motion off, and writes docs/public/screenshots/<surface>-<theme>.png plus manifest.json: the viewport and the clock the pictures were rendered at, and the sha256 of every file a picture is rendered from (the fixture, the shell, the renderer, the module that pins the surfaces, viewport and clock, every non-test source under web/src/). Two renders of one tree are byte-identical. The fixture carries only made-up names (acme/web, alice), so a picture can carry no real workspace. npm run screenshots:check (in check:consistency, and the same rule in the unit suite) needs no browser: it hashes the tree and fails naming each input that changed, appeared or vanished since the pictures were rendered, a manifest rendered at another viewport or clock than the module now pins, and each picture missing or unexpected — a dashboard change ships with its pictures or not at all. gen is not part of fix because it needs the pinned browser (npx playwright-core install chromium). The one hand-taken set is docs/public/slack/ — a real review run as the Slack thread pane shows it (mention.png, status-card.png, verdict.png, and run.gif at 2 fps), cropped to the pane with the requester's avatar and name masked and the reply composer cut off, so no workspace name, sidebar, member or channel name is in frame; the README shows the recording. Nothing hashes these — a Slack capture has no source to render from — and they are re-taken by hand when the card or the verdict format changes.
  21. The diagrams share one visual system, and the shared ones have one source. Every diagram outside the records is a ```mermaid fence that carries structure and words only: a flowchart runs LR for a path across the system or TB for a stack, never TD, RL or BT; every node and edge label is quoted; the shapes say what a thing is (["…"] a component, {"…"} the dispatcher, [["…"]] a Worker, [("…")] where state lives, (["…"]) a system or person outside the tree, {{"…"}} a human gate); a label carries no HTML but <br/> and no #; and no style, classDef, linkStyle or %% line sets a colour — the theme's tokens (item 19) draw the site and GitHub draws its defaults. Two diagrams are drawn in more than one place, so each is a generated region rather than copies: four-seams (the root README.md, explanation/architecture.md, explanation/how-a-request-flows.md) is rendered from the seams' one statement in theme/seams.mjs — the file the landing page's seam row reads too (item 18) — and deploy-order (explanation/worker-topology.md, how-to/ship-a-release.md) from DEPLOY_ORDER in src/deploy/plan.ts; docs:gen writes them and docs:check guards them like the tables (items 6–8). The unit suite holds the rules over every fence in the README and the docs tree and parses each with mermaid, since the site build compiles a page without drawing its diagrams. Records under docs/decisions/ and docs/plans/ are immutable (item 16) and exempt: a record's diagram stays as it was accepted.

Validation criteria

CriterionEvidence
/docs and /docs/ redirect to the docs site's home[unit] src/core/docsLink.test.ts::docsRedirectTarget::sends /docs to the docs site's home
A docs deep link keeps its subpath[unit] ::carries a deep link across unchanged, so a docs URL can be shared
No path outside /docs is claimed (/docsomething, /runs, /healthz)[unit] ::claims no path outside /docs — including one that merely starts with the word
The redirect cannot be steered off the configured origin[unit] ::cannot be steered off the configured origin
A generated region is replaced in place, every byte outside the markers untouched[unit] src/docs/regions.test.ts::replaceRegion::replaces only the region body and leaves every byte outside the markers alone
Regenerating identical content reports no change (idempotent)[unit] ::is idempotent — regenerating identical content reports no change
A region whose marker note is older is still found, and the note is rewritten[unit] ::still finds a region whose opening marker carries an older note, and rewrites the note
A missing region is refused, not appended; an unclosed region is refused[unit] ::refuses when the region is missing, rather than appending or silently doing nothing, ::refuses an unclosed region
Every region a page declares is listed, ignoring ordinary comments[unit] src/docs/regions.test.ts::declaredRegions::lists every generated region a page declares, in order, ::ignores ordinary HTML comments and closing markers
An enum positional prints its values (config set <me|channel>)[unit] src/docs/reference.test.ts::usageFor::prints an enum positional as its values, since a table has no help text under it
Pipes, newlines, and prose angle brackets are table-safe; code spans are left alone[unit] src/docs/reference.test.ts::cell::* (four cases)
The CLI table sections by group in registration order and names each command's surfaces[unit] src/docs/reference.test.ts::renderCliCommands::*
The chat table omits chat-opted-out commands and states who may run each[unit] src/docs/reference.test.ts::renderChatCommands::*
/api routes give a write POST only and a read either verb; non-HTTP commands are absent[unit] src/docs/reference.test.ts::renderApiRoutes::*
The capability matrix's command column has one row per axis in the contract's order, names a gated command in its axis's row in chat form, reads for an axis that turns nothing on, and counts the always-on commands (item 6)[unit] src/docs/reference.test.ts::renderCapabilityCommands::*
On the real catalogue that column is exactly what dependsOn derives per axis — a list nobody typed[unit] src/docs/reference.test.ts::the real catalogue::the capability column lists, per axis, exactly the commands dependsOn derives from the registry — a list nobody typed
Every registered command reaches the CLI table — including groups a hand table had gone stale on (mcp, deploy restart)[unit] src/docs/reference.test.ts::the real catalogue::renders every registered command into the CLI table — including the groups a hand-written table had gone stale on
No rendered table row can break its column count[unit] ::emits no unescaped pipe inside a table row (each row must have the column count its header declares)
The declared regions and the renderers cannot drift apart[unit] src/docs/reference.test.ts::GENERATED_REGIONS::matches the markers actually present in each docs page…
The header links to /docs in a new tab and adds no fourth nav section — on every installation, whatever else is off (live-view.md item 18)[unit] web/src/components/AppNav.test.ts::AppShell::links to the docs at /docs in a new tab — the project's published site, on every installation — without adding a fourth entry to the section nav, ::AppShell::the docs link and its menu group stay when a section is off; the menu's sections follow the nav, ::AppShell::the minimal installation's header is Runs, the docs link, the theme toggle and the menu — nothing that leads nowhere (the web package's suite)
Records: a status outside the set, a missing status or date, a decision without a title, a superseded record without a resolving superseded_by (or one that supersedes itself), and a superseded_by on a non-superseded record are each named (item 16)[unit] src/docs/records.test.ts::statusProblems::*, ::parseFrontmatter::*
Records: an accepted / implemented / superseded record's body and frozen frontmatter cannot change against the base, and the record cannot vanish; status-only changes (accepted → implemented → superseded with a pointer) pass; a proposed record may change; a status never moves backwards (item 16)[unit] src/docs/records.test.ts::immutabilityProblems::*
Every record in this tree passes the status half[unit] src/docs/records.test.ts::the records in this tree::*
The decisions index: one row per record in id order linking the file, pattern / status / date from the frontmatter, a superseded record linking forward, table-safe titles; the directory README, plans and frontmatter-less files ignored (item 17)[unit] src/docs/decisions.test.ts::renderDecisionIndex::*
The phone menu carries the docs as its own group above the sections[unit] web/src/components/AppNav.test.ts::AppShell::puts the docs in the phone menu too, as its own group above the sections
Every absolute URL in a docs page is scheme://hosthttps:///host is refused[unit] src/docs/links.test.ts::absolute URLs in docs/::every absolute URL is \scheme://host` — not `https:///host` (which browsers silently repair) and not a hostless one`
A relative link that leaves the tree names a file that exists in the repository (the site's dead-link check never sees it)[unit] src/docs/links.test.ts::absolute URLs in docs/::a relative link that leaves the tree names a file that exists…
Every internal path in a theme component or its data module (seams.mjs) names a page in this tree, and every fragment a heading on it (item 18)[unit] src/docs/links.test.ts::site links in the theme's components::every internal path names a page, and every fragment one of its headings
Every fence outside the records keeps the visual system — LR or TB, quoted node and edge labels, no style or %% line, no HTML but <br/>, no # — and each is named with its file, line and the rule it breaks (item 21)[unit] src/docs/diagrams.test.ts::diagramProblems::*, ::mermaidFences::*, ::the diagrams in this tree::every fence outside the records keeps the visual system
Every fence outside the records parses with mermaid (item 21)[unit] src/docs/diagrams.test.ts::the diagrams in this tree::every fence outside the records parses with mermaid
The four-seam diagram is rendered from the seams' one statement and the deploy order from DEPLOY_ORDER, both keeping the system they ask of every other fence, and every copy in the tree is the generator's output (item 21)[unit] src/docs/diagrams.test.ts::renderFourSeams::*, ::renderDeployOrder::*, ::the diagrams in this tree::the four seams and the deploy order are the generator's output wherever they are drawn, and each file declares exactly its regions
Every image a theme component draws — the landing's /screenshots/<name>-<theme>.png included — is a file under docs/public/ (items 18, 20)[unit] src/docs/links.test.ts::site links in the theme's components::every image a component draws is a file under docs/public
The screenshot inputs are the fixture, the shell, the renderer, the pinning module and every non-test source under web/src/; tests, helpers, generated declarations and builds are not (item 20)[unit] src/docs/screenshotManifest.test.ts::isScreenshotInput::*, ::listInputs::finds the dashboard's sources in this tree, repository-relative, tests left out
The manifest hashes each input by content, in path order, and records the viewport and the fixed clock and nothing that varies between two renders (item 20)[unit] src/docs/screenshotManifest.test.ts::hashInputs::*, ::renderManifest::*, ::expectedFiles::*
The check names a changed, new or removed input — the pinning module included — a manifest rendered at another viewport or clock, a missing or unexpected picture, and refuses a manifest of another shape (item 20)[unit] src/docs/screenshotManifest.test.ts::manifestProblems::* (six cases)
The committed manifest equals this tree and every picture is present (item 20)[unit] src/docs/screenshotManifest.test.ts::the repository's screenshots::the manifest's inputs equal the tree's, and every picture is present; [agent] npm run screenshots:checkscreenshots:check ok — 10 picture(s) current against N input(s), exit 0. Red step: change one byte of a component under web/src/ and re-run — it must name that file and exit 1.
The pictures are deterministic and show no real workspace (item 20)[agent] npm run screenshots:gen twice; shasum docs/public/screenshots/*.png is identical across the two runs. Open each picture: the repositories are acme/…, the people alice and sam, every timestamp relative to the manifest's now; light and dark carry the same content.
The landing, the README and the architecture page show the pictures without moving the layout (items 18, 20)[agent] after npm run docs:build, serve docs/.vitepress/dist and open / at 1440 wide in each appearance: the hero frame spans the measure and shows the run page in that appearance under its window bar; the runs index, the residents and the spend page each sit beside their statement, cropped from the top, right–left–right; with the appearance following the OS scheme the resource log shows one run-page-<theme>.png fetched, not both, and one file per section picture; no placeholder bars. On GitHub the README's <picture> shows the run page for the viewer's color scheme, and /explanation/architecture shows the light run page with its caption.
The root route is the landing page with the hub beneath it, titled with the site's name (item 18)[agent] after npm run docs:build, docs/.vitepress/dist/index.html contains class="VPHome", <title>OpenSwitchboard</title> and the hub's Pick your surface heading; docs/README.md carries no frontmatter.
The built home page reads displayName in its <title> and in the hero's product element; a missing page, another title, another hero name, or a hero without the element is named (items 15, 18)[unit] src/checkSite.test.ts::siteProblems::*; [agent] npm run docs:build && npm run check:sitecheck:site ok — 8 required page(s) present in docs/.vitepress/dist, home page titled "OpenSwitchboard", exit 0. Red step: change displayName in project.json without rebuilding and re-run — it must name the title and the hero and exit 1.
The README's first heading is displayName, and displayName itself is a non-blank string (item 15)[unit] src/projectFacts.test.ts::factsProblems::the display name::*
npmPackage is a scoped npm name whose unscoped part is name, and every package a checked file mentions under that scope is npmPackage (item 15)[unit] src/projectFacts.test.ts::factsProblems::the npm package::*
Reduced motion stops the thread's in-progress ring (item 18)[agent] on the home page with the browser's prefers-reduced-motion emulated as reduce: the thread card's ring under Writing the review does not turn; without the emulation it turns.
The home page scores ≥ 95 in every Lighthouse category (item 19)[agent] npm run docs:build && npx vitepress preview docs --port <port>, then npx lighthouse http://localhost:<port>/ --preset=desktop --only-categories=performance,accessibility,best-practices,seo --quiet --chrome-flags="--headless" → each of the four categories ≥ 95.
The site fetches nothing from a third party, and a page without a diagram never loads mermaid (item 19)[agent] after a build: grep -rl "fonts.googleapis|cdn\." docs/.vitepress/dist/*.html is empty; docs/.vitepress/dist/index.html preloads one bricolage-grotesque-latin-opsz-normal.*.woff2 and one geist-latin-wght-normal.*.woff2 from /assets/; the entry docs/.vitepress/dist/assets/app.*.js does not contain mermaid; the home page's network log shows only same-origin requests.
Diagrams wear the page's palette in both modes (item 19)[agent] open /explanation/worker-topology; in each appearance the rendered SVG's node fill equals the page's --sb-diagram-node token and toggling redraws it.
The committed reference tables match the registry and the shared diagrams their sources[agent] npm ci && npm run docs:checkdocs:check ok — 10 file(s) match the command registry, the decision records and the diagram sources, exit 0. Red step: edit a row inside a generated region, or a node label inside the README's four-seams region, and re-run — it must name that file and exit 1.
The site builds with no dead links, and docs/plans/** is not published[agent] npm --prefix docs ci && npm --prefix docs run buildbuild complete, exit 0; test ! -d docs/.vitepress/dist/plans. Red step: add [x](./nope.md) to any page and rebuild — the build must fail naming the dead link.
A PR proves the built site is complete: every required page present, plans/ absent[agent] after npm --prefix docs run build, the docs job's "the built site is complete" step exits 0. Red step: mv docs/.vitepress/dist/tutorials/index.html /tmp/ and re-run — it must print missing: tutorials/index.html and exit 1.
A PR proves the docs Worker's config would deploy, with no credentials[agent] npx wrangler deploy --dry-run in deploy/cloudflare-docs/Read <n> files from the assets directory …/docs/.vitepress/dist, exit 0. Red step: point assets.directory at a path that does not exist and re-run — it must fail with The directory specified by the "assets.directory" field … does not exist and exit 1.
Every Diataxis directory is browsable on both surfaces[agent] after a build, each of docs/.vitepress/dist/{tutorials,how-to,reference,explanation}/index.html exists; on GitHub, opening docs/tutorials/ renders its README.
A mermaid fence renders as a diagram on the deployed site[agent] open https://<docs host>/explanation/worker-topology and confirm an SVG diagram (not a code block) in both light and dark theme.
The deployed /docs redirect works and the docs host answers[agent] curl -sS -o /dev/null -w '%{http_code} %{redirect_url}\n' https://<bot host>/docs/reference/cli302 https://<docs host>/reference/cli; curl -sS -o /dev/null -w '%{http_code}\n' https://<docs host>/200 (the project's public site).
A push that touches docs/ deploys the site; one that doesn't, doesn't[agent] on the docs-deploy job of a docs-touching push to main: the build + deploy step ran and wrangler reported a new version; on a code-only push: the no docs changes in this push notice.
The docs host is never a destination of the bot's Access application (an application spanning two hostnames routes every login callback through one of them, so a retired docs host would break the dashboard's login); the site itself has no Access application — it is public (item 12)[agent] Zero Trust → Access → Applications: the bot's application lists only bot-host paths, and no application names the docs host.
A docs deploy cannot interrupt a live run[agent] start a long run, deploy the docs Worker (npm --prefix deploy/cloudflare-docs run deploy), confirm /healthz never reports draining and the run finishes normally.
The AGENTS.md command table renders every script in package.json order, escapes pipes, and a script without a description or a description without a script is a finding (item 14)[unit] src/agentsGen.test.ts::renderCommandsTable::renders one row per script in package.json order, escaping pipes, ::commandDocProblems::a script without a description, and a description without a script, are both problems, ::commandDocProblems::ignores $-prefixed keys and requires both fields
AGENTS.md stays under its budget, declares exactly the commands region, and its table is current (item 14)[unit] src/agentsGen.test.ts::budgetProblem::passes at the budget and names the overage past it, ::the repository's AGENTS.md::declares exactly the commands region, and its table is current, ::the repository's AGENTS.md::describes every root script and no phantom ones, ::the repository's AGENTS.md::stays under the budget
Every copy of a project fact agrees with project.json, and a stale contact, docs URL, route, or repository owner is named (item 15)[unit] src/projectFacts.test.ts::factsProblems::is silent when every copy agrees, ::factsProblems::names a stale contact address, docs URL, route pattern, and repository owner, ::factsProblems::leaves other repositories under the org alone, and checks package.json's five fields, ::the repository's own facts::every checked file exists and agrees with project.json
The README's badge row follows a repository move: a github.com path or a shields.io GitHub badge under another owner is named (item 15)[unit] src/projectFacts.test.ts::factsProblems::the README's badges::*
The description and topics are what GitHub accepts — a non-blank description of at most 350 characters, 1–20 topics of [a-z0-9-]{1,50} — and package.json's description is the same sentence (item 15)[unit] src/projectFacts.test.ts::factsProblems::the description and topics::*
Per-PR docs previews[gap] CI builds the site on every PR but publishes nothing; a reviewer reads the markdown diff. A preview deployment per PR (a --name switchboard-docs-pr<N> Worker, or Cloudflare's preview URLs) is not built.