ADR-0072 Architecture decision record
The docs ship inside the Kit, and an Instance's AGENTS.md indexes them
Most Machines will be written by an Assistant, not by a human: the human asks for a Machine and reviews its graph. No model's training data holds jr2's API, so an Assistant needs the docs for the exact Kit version the Instance resolves, and it must find them without being told to look. The docs-site grill of 2026-10 settled where those docs live and how an Assistant reaches them. Its evidence is Vercel's evals on APIs that are not in training data: a compressed docs index inline in AGENTS.md passed 100% of cases; skills passed 79% at best, because the agent did not invoke the skill in 56% of cases; no docs passed 53% (Vercel). Next.js ships the same design: its docs as Markdown inside the next package, and a managed block in the project's AGENTS.md (Next.js).
Decision
One Markdown source, three outputs.
- The bundle.
@jr2/orchestratorshipsdocs/: every page of the sitemap, the generated Reference, the Glossary and the Decisions, as plain Markdown. No site-generator syntax reaches it. It is the main channel for an Assistant. Each page is at its slug, such asconcepts/gates.md, and each ADR is atreference/decisions/<file>. An Assistant reads a page with its file tool, which cuts off a long file. So the Authoring surface is two parts:reference/authoring-surface.md, a contents page that gives each name its kind and the first sentence of its TSDoc, andreference/authoring-surface/<anchor>.md, one page for each export and for each type that a signature names. One page of all the names is about 200 KB, and the contents page is less than 25 KB. No line of the contents page or of an entry page is longer than 1,000 characters, so the type plumbing is a list, not a paragraph. When the contents page passes 25 KB, first make the long first sentences shorter; if it still passes, give each group of the contents page a page of its own, and make the contents page a list of the groups. The first sentence ends at a stop, not at a colon, because the words before a colon often tell nothing alone ("An invoke."). A link to an entry goes to its page. Each of the other generated Reference pages (CLI, config, machines, mechanism events, HTTP API) stays one page. Each is less than 30 KB, the size that one read of a file tool shows, with one exception: the HTTP API page is about 50 KB. It is one page because its routes come first and are less than 30 KB, so one read shows every route; the types of the bodies come after the routes, and an Assistant reads one of them with an offset or by its heading. When the routes pass 30 KB, give the types a page of their own. The alternative, a split of the HTTP API page now, is rejected because a route and the types it names are then on two pages, and one read shows all routes already. The Glossary and the Decisions are not generated, and this rule does not hold them: an ADR can be about 50 KB, and an Assistant reads the one ADR that a page links to. - The
AGENTS.mdblock. A compressed index of the bundle, and an instruction in five parts. jr2 is newer than your training data, so read the bundled docs before you write a Machine or config. Start with the guide "Write a workflow" and the contents page of the Authoring surface. Read one page with each call of the file tool. Runnpx --no jr2 checkbefore you give a change to the user. A final state means that the work succeeded; when it did not, invokefail(), and do not throw to fail. The Failure rule is in the block because no refusal can teach it: the walk cannot see that a final state means "this did not succeed", or that a promise throws only to fail, and in the eval of ADR-0073 three of the four trials wrote a failure that endeddoneorerror. The two entry pages in one read are more than about 30 KB, and a file tool then shows only the start of the output. The check is in the block becausenpm run typecheckloads no module. Thus it does not find a refusal ofcreateMachine,defineEventorworkspace(). An Assistant that may not runjr2 upgets those refusals only fromjr2 check(ADR-0009). The command has--nobecause npx with no TTY installs a missing package with no prompt. The packagejr2on the npm registry is not jr2. With--no, npx stops when the Instance has no localjr2bin, for example beforenpm install.npm run checkalso does not fetch, but an Instance from an older scaffold has nocheckscript. The index names each file of a directory, but the Decisions folder and the Authoring surface folder each have one entry, which names the contents page of the folder. Thus the line stays short. - The site, on GitHub Pages. HTML for humans, and every page also at a
.mdURL, plusllms.txt, for tools that read over the network.
- The bundle.
The docs ride
@jr2/orchestrator, not a package of their own. The Kit version is the@jr2/orchestratorthe Instance resolves, so the bundle matches the code the Assistant writes against by construction. There is nothing else to pin.jr2 initwrites the block, andjr2 upkeeps it current.initwritesAGENTS.mdwith the block between two markers, and aCLAUDE.mdthat holds@AGENTS.md, because Claude Code readsAGENTS.mdonly when noCLAUDE.mdexists.inittakes the block from the bundle of the CLI's own@jr2/orchestratorwhen that is the version it pins; without that bundle it leaves the markers empty. AnAGENTS.mdor aCLAUDE.mdthat exists stays as it is, andinitsays what to add to it.uprewrites the text between the markers when it differs from its Kit version's block, changes nothing outside them, and prints one line:AGENTS.md: jr2 block updated for <version>. It does this before the cluster work, so a converge that fails still updates the block.upis the verb because every upgrade ends inup. A folder without the markers has opted out:upnever creates the file or the markers, and no config key exists for it. A broken marker pair is reported, andupdoes not change the file.The Reference is generated from the code wherever the code holds the facts.
Page Source Authoring surface: a contents page, and a page per name TSDoc on the exports, exactly the root's list @jr2/machinesTSDoc on the exports jr2.config.tsTSDoc on the JR2ConfigfieldsCLI a structured command table, which also prints --helpHTTP API the route table in http.ts, made dataMechanism events the code that defines them Glossary, Decisions GLOSSARY.mdanddocs/adr/, rendered as they areThe TSDoc on an Authoring surface export is written for the author: what it does, what it refuses, what to read next, and one example. Maintainer reasoning goes in
//comments in the body. The packages ship their.tssource, so an Assistant that follows an import intonode_modulesreads the same text the site shows. The Authoring surface is exactly what@jr2/orchestratorexports from its root: what the kit's packages share is internal even where it can be imported, and@jr2/machinesis not part of it, because it ships Machines, not names to author with.docs:buildassembles the bundle at pack time. Its inputs are the hand-written pages indocs/site/, the generated Reference,GLOSSARY.md, anddocs/adr/. It runs in@jr2/orchestrator'sprepackand before the site build. Its output is gitignored, and no generated file is committed. A link that leaves the docs set becomes a GitHub URL at the release tag; a link that resolves nowhere fails the build. The@disttier asserts that the packed bundle exists and that theAGENTS.mdfromjr2 initindexes it.The site describes the latest release and deploys on its tag, in a
pagesjob inrelease.ymlthat needs the release job, so a failed gate deploys nothing. It does not wait for the staged packages' approval (ADR-0055).VitePress builds the site, from the pages as they are. The source format is VitePress's own Markdown, so a page renders on GitHub and in the site, and an error names the file that a writer edits.
docs:buildaccepts four forms of it, and makes each one plain for the bundle: an aside, the provider tabs, a code group, and a<<<code import. It refuses every other form, so no generator syntax reaches the bundle. The provider tabs are::: tabs key:providerfromvitepress-plugin-tabs, not VitePress's::: code-group: a provider tab holds prose as well as code, and the plugin keeps the reader's choice on each page, which a code group does not.docs:buildrefuses tabs with another key, and a<<<of a file outsidedocs/site/examples/, whichpnpm typecheckchecks. Amermaidfence is a theme component that imports mermaid only on a page with a diagram. The site has local search, asitemap.xml, and the.mdURL of each page in a<link rel="alternate">.
Considered options
- Skills. Rejected as the channel for API knowledge: the Vercel evals above, and an agent decides whether to invoke a skill, where it always reads
AGENTS.md. Skills stay possible for a multi-step job, such as an upgrade. - A docs MCP server. Rejected: one more process to run for text that can sit on disk. MCP is for acting on a running system, not for reading docs.
- The site and
llms.txtonly. Rejected: the site describes the latest release, and an Instance can pin an older one. An Assistant also fetches over the network inconsistently, and some run without network. - A separate
@jr2/docspackage. Rejected: the Instance must install it and pin it to the Kit version, and a mismatch is silent. Inside@jr2/orchestrator, it cannot mismatch. - The Authoring surface as one page. Rejected: an Assistant's file tool cuts it off, so it reads the first entries and not the rest.
- An entry page named by its export, such as
Customize.md. Rejected:customizeandCustomizeare both exports, and on a file system that ignores case (macOS, Windows) the two are one file. A page is named by its anchor, which is lower case and starts withtype-for a type. - A hand-written Reference. Rejected: it drifts from the code, and the Reference is what an Assistant trusts most.
- Copies of the Glossary and the ADRs committed inside the package, kept equal by a drift check. Rejected: two copies of one text, to avoid one pack-time step.
AGENTS.mdwritten once byinit. Rejected: it goes stale at the first upgrade. Ajr2 docscommand to refresh it is rejected too: a step that someone must remember.- Deploy when npm serves the version — an hourly job that compares
npm viewwith the deployed version. Rejected: more machinery to close a window of minutes. - Astro Starlight. Built to the same bundle, and compared with VitePress on the same pages. It had better search and a lighter page, which VitePress gets with a small search fix and a mermaid component. Rejected: its tabs and code imports are MDX components, so 28 pages become
.mdxthat GitHub shows as source text, and a page carries more than twice the generator lines.
Consequences
jr2 upwrites into the Instance folder, and only between the markers.@jr2/orchestratorgains its first pack-time step. Its source still ships as.ts; onlydocs/is assembled.- The Orchestrator's Kit image packs
@jr2/orchestratorwithnpm pack --ignore-scripts, and its build context dropsdocs/: theprepackneeds pnpm and the whole repo, which the image stage has not, and the Orchestrator's process never reads the bundle. So the image holds no docs, and a localdocs:builddoes not move its tag (ADR-0069). - The tarball grows by the docs, most of it the ADRs (about 0.7 MB of text today).
- A TSDoc edit on the Authoring surface is a docs edit.
--helpand the CLI Reference come from one table, so they cannot disagree. - A docs fix goes live with the next release, not on merge.
- A rejected stage leaves the site describing a version that npm does not serve, until the next tag. Accepted: the window is short, and a rejected stage is rare.
- The bundle is plain Markdown, so a change of the site generator changes
build/vitepress.tsand the pages' forms, and nothing that an Assistant reads. - VitePress 1.x is on Vite 5, with dev-server advisories that the site cannot fix. The site is static, so they reach only
docs:dev. A move to VitePress 2 is a later change.