An agent that joins my tracker reads one URL: /llms-full.txt. It is 6,245 lines of Markdown, and I wrote about 1,800 of them. The rest is generated from the code, and the deploy refuses to ship a copy that has drifted from it.
The constraint: an agent should need one page, not a tour of the docs site, and that page can't be a second copy of the truth that rots the week after it's written.
I'm Panth, and I lead the software team at Oizom. This is from ticket-tracker, my open-source board, which my own agents work from. Section O of its agent plan is titled "One page an orchestrator can be pointed at". This post is how that page is built.
Four files, one source
The app serves these beside itself:
/llms.txt the short index: what this is, the URLs, how to authenticate
/llms-full.txt the one page: the whole operating context, Markdown
/integrate the same content as a readable web page, for people
/integrate.json a small manifest: api, mcp, sdk, openapi and llms URLs + version
/llms.txt is 85 lines and mostly says "read /llms-full.txt". The full page has nine chapters: what the software is, how an agent gets in, the three doors (SDK, REST, MCP), the loop as a complete runnable orchestrator, the reference, the rules of the house, recipes, artifacts, and the version and changelog.
All four come out of one script, scripts/gen-integration.mjs. It also builds the Claude Code plugin, whose skill ships llms-full.txt verbatim as its reference file, so the plugin can't hold a second copy either.
What is written by hand, and what isn't
The prose lives in scripts/integration-source/, one Markdown file per chapter. That is the only hand-written part. Where the prose needs a table that the code already knows, it holds a marker instead:
## 5. Reference
...
{{gen:contracts}}
...
{{gen:rest}}
...
{{gen:mcp}}
...
{{gen:sdk}}
The generator replaces each marker with a block built from the same sources the software runs on:
| Block | Comes from |
|---|---|
| REST: every endpoint, its parameters, body and responses |
/v1/openapi.json, generated from the zod schemas the routes parse with |
| MCP: every tool, its scopes and input schema | the tool registry in the shared package |
| Scopes, event types, error codes, field types, states | the shared contracts, from the built package |
| SDK: every method |
sdk/dist/sdk.d.ts, quoted verbatim with its doc comments |
The reference chapter alone is 3,218 lines: 72 REST endpoints and 36 MCP tools. Nobody types those. When a route is added, it shows up in the page on the next build because the route's schema is the page's source.
The expansion is strict. A marker the generator doesn't know stops it:
.replace(/\{\{gen:([a-z]+)\}\}/g, (_, name) => {
if (!(name in blocks)) fail(`unknown generated block {{gen:${name}}} in the prose`);
return blocks[name];
})
Some tables need a sentence of meaning next to each generated name, like what an agent with the commenter role may do. Those sentences are written in the script, keyed by name. If the shared package grows a new role and nobody wrote what it means, the build fails instead of printing a blank row.
The guard
node scripts/gen-integration.mjs --check generates everything fresh and compares it with the committed copy. The deploy script runs it before firebase deploy, so nothing goes out on a stale page. If I change a schema and forget pnpm integrate:gen, the deploy stops.
That only works if the same inputs give the same bytes. So every list is emitted in a fixed order and nothing reads the clock. The page's "updated" date is the date of the newest changelog entry, which an author bumps when the contract moves. A new Date() stamp would make the guard fail every midnight.
The guard is tested too. The end-to-end suite generates into a temp directory, appends a line to each file in turn and checks that exactly that file is reported stale. The plugin zip is compared as bytes, so the test flips one byte in the middle of it and expects the same.
Served as text, uncached
The page exists to answer "has anything changed?", so a cached copy defeats it, and a model handed HTML where it expected Markdown is worse off than with no page. Hosting serves /llms*.txt as text/plain; charset=utf-8 with no-store and access-control-allow-origin: *. A test reads firebase.json and asserts those headers, and a separate check proves them against the hosting emulator.
What an agent gets from it
A person creates an agent, adds it to a board and gives it a token. The agent gets that token and one URL. From the page it learns how to wait for work without polling, publish a task list, heartbeat every minute, ask a question when it is blocked and post its result with files, with every endpoint and tool it needs in the same file.
The changelog at the end goes back to version 1.0.0 on 2026-09-26, so an agent can tell whether what it learned last week still holds.
Where do your agents learn your API from, and what stops that from going stale?
Top comments (0)