article.html: 34 added the lede under the h1, 35 added the folded
contents strip in the same place. Both belong, and site.css settles the
order — its lede adjacency names .menu-contents + p, so the strip sits
between the title and the lede.
Two things the merge exposed rather than caused:
- $toc and $sidebar were spelled inside article.html's own comments,
and string.Template substitutes there too, so every page carried a
second copy of both rails inside a comment (28 toc links for 7
headings). Doubled to $$toc / $$sidebar, as the template contract
already said to.
- test_site_pages assumed one rendering of each rail. Both are now
written twice by design, so the tests assert every rendering agrees
with the page instead of assuming there is one.
The card file keeps main's location: the board owns a card's stage, and
main had already moved 35 to review/.
Committing the woff2 files is redistribution, and OFL 1.1 asks that its
text and copyright notice travel with the font. IBMPlex.LICENSE.txt from
IBM/plex, ZillaSlab.LICENSE.txt from mozilla/zilla-slab; both ship in
dist/static/fonts/ with the faces they cover.
Fetching them per checkout made every deploy machine ask Google for the
site's own typography, and a clean checkout build silently fell back to
system fonts. 113 KB, immutable under their filenames — which is what
the year-long immutable cache in root/_headers already assumes.
37 sync resolves its remote like github.py does, and says so when it
cannot; 38 a card being merged wears the state and holds still; 39 the
tab says whether agents are working; 42 a long closing report keeps its
headline instead of its tail. 40 (the open-questions launch gate) is
archived, unmoved by the board and so never committed by one.
site/fetch-fonts.py downloads the seven woff2 files once per checkout,
as site/README.md documents, so they are build inputs rather than
sources. .wrangler/ is the cache a deploy leaves behind.
md() split a block into physical lines and made each one a unit. Task
files are hard-wrapped at ~74 columns, so the second line of an item
became its own bullet, `- [ ]` rendered as a literal bracket pair,
nested lists flattened, and prose kept the author's ragged edge as
<br>. "Enough for these task files" was exactly what it was not.
Lists are now grouped into logical items before rendering: a new item
begins only at a marker, and a line without one is continuation text
joined with a space. Indentation is honoured — a marker past its level
opens a nested list, a shallower one closes back to the level that
fits — and one entry point serves both bullets and ordered lists, so
an <ol> nests under a <ul> the same way. Task-list items render as a
glyph in a span, never an <input>: the file is the source of truth and
the drawer is not an editor. A ticked box reads as settled (--calm);
an open one stays neutral. Paragraphs and blockquotes join their
source lines with a space, so prose reflows to the drawer's width.
Fences, tables, headings and rules are untouched, including the fence
state machine that spans blocks.
The tests lift esc() and md() out of the page and run them under node,
because the renderer is a pure function and its output is what to
assert on; node is not a bench dependency, so those checks skip when
it is absent and source-level invariants cover the shape of the fix.
One check renders every card on the board plus AGENTS.md and asserts
one bullet per source marker — the acceptance criterion applied to the
whole corpus.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The design is drawn at a fixed 1180px with three-column articles and a
two-column hero, and the site's first traffic is a link pasted into a
chat and opened on a handset. This is what happens below that width —
three max-width steps, and nothing that takes effect at or above the
width the design defines.
The two columns an article loses come back as <details> strips: "On
this page" folds under the title at 1080px, where the gutter goes, and
the section nav becomes a menu under the masthead at 760px, where the
sidebar goes. Both are filled from the same $sidebar and $toc as the
columns they replace, so a folded copy cannot say something the column
does not — and one menu that opens and closes needs no script, which
keeps the site's promise of shipping none.
The rest is what the acceptance asks for: a table becomes its own
scroller with an edge shadow that appears only while there is more to
the right; a token that cannot break, breaks; fenced code keeps
scrolling inside its own block; the hero and title steps come down
without leaving Zilla Slab; everything a finger has to hit is --tap
(44px) tall; and the masthead, footer and hero actions wrap instead of
widening the page.
The one thing that would have changed desktop is guarded rather than
noticed later: the contents strip is a sibling between the h1 and the
body's first paragraph even when display:none, so the lede rule names
it too.
tests/test_site_responsive.py mechanises all four promises, including
the one that matters most — every media query is a max-width below the
design.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fills the middle of the site. Nine routes, every body a heading slice of
AGENTS.md or README.md, and the article layout given the furniture the
design calls for.
The manifest gains the two concepts nothing covered: /concepts/task-files/
(the header format, from AGENTS.md's own section) and /concepts/adapters/
(the adapter summary, which is the other half of the three-layer law).
/concepts/stages/ now runs through "Moving a task", because the five
directories and moving between them are one idea.
The layout:
- A lede under the title — the one sentence an article authors, taken
from the manifest's `description` or an explicit `lede` where the two
want different words. A slice starts mid-document; a reader arriving
from the nav is owed a line saying what they are looking at.
- Prev/next at the foot, walking the sidebar's own order so the arrows
and the rail cannot disagree. Pages with no section (the landing page,
the 404) are not on the flow.
- "Edit this page on GitHub" anchors to the section the page was cut
from, built from the same `from` heading the slice starts at.
Two bugs the new pages found:
- string.Template substitutes inside HTML comments, so a comment naming
the body placeholder emitted the whole body twice and closed itself
early on the first `-->` in it.
- Promotion could produce a second <h1>. A slice that deliberately runs
past its own section carries headings at the `from` level, and those
promoted to h1 on a page that already had one. Promotion now stops at
h2, where they read as peers — which is what putting them on one page
said in the first place.
tests/test_site_pages.py covers the furniture on the real built site:
the routes, the layout, the sidebar marking one page, the contents list
being exactly the body's own h2s in order, the prev/next chain end to
end, the edit link's anchor, the six landing-page doors, and a table, a
fenced block and a nested list surviving the renderer. The scratch-repo
helper now copies every file a slice links to, since the builder checks
those exist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The site's page bodies are heading slices of these two files, so a
sentence that only makes sense to an agent inside an installed project
becomes a sentence on bench.12vectors.com. Fixed at the source rather
than forked into the site:
- "The flow is linear" and its diagram sat at the end of "Pull requests",
which put the whole stage flow on the PR page and left "Stages" with no
opening. Moved under "## Stages", where it introduces what follows.
- "Agents working the board" ended in a stray sentence about worktrees
and one-agent-per-task; folded into the start-work paragraph it
belongs to.
- Start work no longer claims to launch `claude -p`: it launches through
the configured adapter, which is the whole point of adapters.
- "see below" and "the PR guard above" name their sections instead,
since above and below are other pages on the site.
- The tool-chips sentence had grown into the middle of the verdict it
interrupted ("The card wears it in the design system's colours", with
no nearby "it"). Split into two paragraphs.
- "Task file format" gains a table of every header field, what it may
contain and who writes it — including PR, which the board writes and
nothing documented until now.
- Paths are written from the repo root, the reference-directory example
no longer names another project's regulator, and README's three-layer
law links the two documents it points at rather than naming them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Turn design 1b (Dockside) into `/`: a terminal hero, the claim README.md
already makes, six doors, and a strip at the foot. The layout is task
31's; what this commit is really about is that nothing factual on the
page is typed by hand.
- **Two facts are read, not written.** `site/build.py` reads the install
one-liner out of README.md's "Install into a repo" block and the
version out of `manager/core/VERSION`, and offers them to the template
as `$install_block` and `$version`. A renamed section, a missing
VERSION or an install section that lost its command block stops the
build, exactly as a renamed heading already did. `pages.json` loses
its `version` key, and a build refuses one if it comes back.
- **A dead internal link stops the build.** Every href a rendered page
emits — a door as much as a link inside a slice — must resolve to a
route in the manifest or a file in `static/`/`root/`. The check runs
after rendering and before writing, so a bad link leaves the last good
build standing rather than shipping a 404 with a nice typeface.
- **Six real doors.** `pages.json` grows the routes they open: install
and first run, the five stages, agents on the board, PRs and review,
team mode, the three-layer law. They are heading slices, i.e. the
stub routes task 33 expected and task 34 will re-cut.
- **The terminal is a transcript.** README.md's own command, then lines
install.py and board.py really print, with the abridgement declared in
the terminal's title bar. `tests/test_site_landing.py` holds every one
of those lines against the source that prints it, so a reworded prompt
fails the suite instead of quietly making the page fiction.
- **No fake telemetry.** Turn 1's "most opened this week" strip becomes
the version, read from VERSION, and a link to the releases.
The landing page no longer carries a generated body, so the tests that
read one from it now read `/concepts/stages/`, and the two scratch repos
in the suite copy VERSION alongside the markdown.
python3 -m unittest: 407 tests, OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
site/wrangler.jsonc puts site/dist/ behind bench.12vectors.com as static
assets. No `main`: the site is files, and a Worker with no script is the
cheapest correct way to serve them.
html_handling force-trailing-slash, so /x redirects to /x/ — the
url the pages link and rel=canonical names. One
page, one address; no url ends in .html.
not_found_handling 404-page, so an unknown path gets dist/404.html
with a 404 status rather than the landing page
with a 200.
routes bench.12vectors.com as a custom domain. Cloudflare
takes the hostname at the zone level and makes the
DNS record; nothing else on 12vectors.com moves.
site/root/_headers carries the response policy. HTML revalidates on
every view, so a deploy is visible on the next reload without anyone
clearing a cache; /static/* is kept for a year and never re-checked,
which is safe because the stylesheet and icon urls carry a hash of their
contents. The general rule is written first and the specific one second,
so a host that merged the two instead of overriding would still land on
max-age=0 — the safe side. Alongside it the baseline a public page owes:
nosniff, a referrer policy, a year of HSTS without preload,
X-Frame-Options, and a default-src 'none' CSP that makes "no analytics,
no third-party anything" something the browser enforces rather than
something a test asserted once.
Deploys are run by hand, as releases already are — no Cloudflare token
in repository secrets, no first deploy pipeline. site/README.md names
the account, the Worker, the route and the four-command sequence, plus
the four things to check after a deploy that no test here can reach.
The tests cover everything before Cloudflare: that the config says what
the site needs, that the build writes the files it names, and that
wrangler.jsonc, pages.json and README.md cannot drift apart about which
domain this is. A live response is not among them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three things the build owed the host before the site could have an
address, all of them visible in site/dist/ rather than in a deploy
script.
A 404 page. Cloudflare's not-found handling serves a literal 404.html
from the root of the assets directory, so a route may now name an .html
file instead of ending in "/" — the one that does is /404.html, a normal
manifest entry with its own layout. It is the site's design and the
site's nav, with a way back to the landing page, a noindex and no
canonical. A null section keeps it off the nav it renders.
site/root/, copied verbatim to the TOP of the build the way static/ is
copied to a subdirectory of it. _headers is what it holds; the host
reads that file from the root and nowhere else. A file in root/ that a
route would also write stops the build rather than letting one silently
win.
Stamped urls for the stylesheet and the icon: the templates link them
through $stylesheet and $icon, which carry a ?v=<hash> of the file's own
contents. Nothing in static/ is renamed, so the tree and the /static/*
glob stay as they were — but the url changes when the file does, which
is what will make a year-long immutable cache safe.
Tests ask the builder where a route lands rather than keeping their own
copy of the rule.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The generated site's whole value is that drift is loud, so the tests
are mostly about drift: a heading renamed in AGENTS.md must fail the
build naming the route and the heading, a slice emptied to its heading
must fail rather than emit a page, and a heading inside a code fence
must never match — AGENTS.md fences a task template whose first line is
'# Task title', and matching that would slice the document in half.
Alongside: link rewriting (GitHub blob, link_routes overrides, dead and
escaping links refused), manifest validation, the output-directory
guard, and the acceptance criterion that no built page fetches anything
third-party — mechanised as 'every rel that opens a connection is
same-origin', with rel=canonical excluded because it is a statement,
not a fetch.
The release artifact test now asserts site/ is absent from bench.tar.gz.
The manifest already excludes it by saying nothing about it, but
correct-by-omission is exactly what a future manifest edit undoes
silently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bench has no public page. site/ is the machinery for one: a manifest,
a generator, two layouts from the turn-2 docs design, and the Daylight
half of the board's own token set.
Nothing here is transcribed. Every page body is a heading slice of a
file that already documents bench, and site/pages.json is the only
place that mapping is written down — the IA is editorial judgement, so
it lives in one readable file rather than in a directory convention.
The point is the failure: a from/to heading the source no longer
contains stops the build naming the route, the file and the heading,
and every page renders before the output directory is touched, so a
drifting rebuild leaves the last good build standing instead of
replacing it with an empty one. Repo-relative links come out as site
routes or GitHub links; a dead one stops the build too.
Rendering is markdown-it-py, pinned in site/requirements.txt. The
stdlib-only law binds manager/core/ — the tool people install — and
site/ is neither shipped nor installed, so a hand-rolled parser would
have been risk spent in the wrong place on a document that leans on
fenced code, nested lists and inline code throughout.
Templates are string.Template: $name substitutes and a literal dollar
doubles, because str.format's braces collide with every line of CSS.
Fonts are self-hosted and same-origin: no page bench serves may need a
request to a third party to be read. fetch-fonts.py downloads the seven
woff2 files once; until they are there the build says which are missing
and the pages fall back to the system stack.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>