Skip to content

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/orchestrator ships docs/: 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 as concepts/gates.md, and each ADR is at reference/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, and reference/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.md block. 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. Run npx --no jr2 check before you give a change to the user. A final state means that the work succeeded; when it did not, invoke fail(), 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 ended done or error. 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 because npm run typecheck loads no module. Thus it does not find a refusal of createMachine, defineEvent or workspace(). An Assistant that may not run jr2 up gets those refusals only from jr2 check (ADR-0009). The command has --no because npx with no TTY installs a missing package with no prompt. The package jr2 on the npm registry is not jr2. With --no, npx stops when the Instance has no local jr2 bin, for example before npm install. npm run check also does not fetch, but an Instance from an older scaffold has no check script. 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 .md URL, plus llms.txt, for tools that read over the network.
  • The docs ride @jr2/orchestrator, not a package of their own. The Kit version is the @jr2/orchestrator the Instance resolves, so the bundle matches the code the Assistant writes against by construction. There is nothing else to pin.

  • jr2 init writes the block, and jr2 up keeps it current. init writes AGENTS.md with the block between two markers, and a CLAUDE.md that holds @AGENTS.md, because Claude Code reads AGENTS.md only when no CLAUDE.md exists. init takes the block from the bundle of the CLI's own @jr2/orchestrator when that is the version it pins; without that bundle it leaves the markers empty. An AGENTS.md or a CLAUDE.md that exists stays as it is, and init says what to add to it. up rewrites 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. up is the verb because every upgrade ends in up. A folder without the markers has opted out: up never creates the file or the markers, and no config key exists for it. A broken marker pair is reported, and up does not change the file.

  • The Reference is generated from the code wherever the code holds the facts.

    PageSource
    Authoring surface: a contents page, and a page per nameTSDoc on the exports, exactly the root's list
    @jr2/machinesTSDoc on the exports
    jr2.config.tsTSDoc on the JR2Config fields
    CLIa structured command table, which also prints --help
    HTTP APIthe route table in http.ts, made data
    Mechanism eventsthe code that defines them
    Glossary, DecisionsGLOSSARY.md and docs/adr/, rendered as they are

    The 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 .ts source, so an Assistant that follows an import into node_modules reads the same text the site shows. The Authoring surface is exactly what @jr2/orchestrator exports from its root: what the kit's packages share is internal even where it can be imported, and @jr2/machines is not part of it, because it ships Machines, not names to author with.

  • docs:build assembles the bundle at pack time. Its inputs are the hand-written pages in docs/site/, the generated Reference, GLOSSARY.md, and docs/adr/. It runs in @jr2/orchestrator's prepack and 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 @dist tier asserts that the packed bundle exists and that the AGENTS.md from jr2 init indexes it.

  • The site describes the latest release and deploys on its tag, in a pages job in release.yml that 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:build accepts 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:provider from vitepress-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:build refuses tabs with another key, and a <<< of a file outside docs/site/examples/, which pnpm typecheck checks. A mermaid fence is a theme component that imports mermaid only on a page with a diagram. The site has local search, a sitemap.xml, and the .md URL 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.txt only. 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/docs package. 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: customize and Customize are 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 with type- 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.md written once by init. Rejected: it goes stale at the first upgrade. A jr2 docs command to refresh it is rejected too: a step that someone must remember.
  • Deploy when npm serves the version — an hourly job that compares npm view with 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 .mdx that GitHub shows as source text, and a page carries more than twice the generator lines.

Consequences ​

  • jr2 up writes into the Instance folder, and only between the markers.
  • @jr2/orchestrator gains its first pack-time step. Its source still ships as .ts; only docs/ is assembled.
  • The Orchestrator's Kit image packs @jr2/orchestrator with npm pack --ignore-scripts, and its build context drops docs/: the prepack needs 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 local docs:build does 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. --help and 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.ts and 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.

The docs of Kit version 0.5.1. Each page is also at its .md URL.