The agent interface
Standing rules for how every part of this system presents itself to an LLM. New commands, errors, schemas, and generated files get checked against this list. Where a rule is mechanical, the engine's own test suite enforces it.
The premise: an agent working in a repo fails in specific, predictable ways. Each rule below answers one of them.
The constraint on all of it
Nothing here may cost a person anything. Everything must stay documented to the point where someone can open the repo at any stage and change the output by hand. This system is AI-focused and not AI-only, and no rule below is a reason to weaken documentation or standards.
In practice that means every machine-facing artifact owes a human-readable counterpart:
| Machine-facing | What a person is owed |
|---|---|
| A generated or lock file | A header naming what regenerates it and what to edit instead |
| A derived value | A command that shows the derivation, and a way to override it |
| A schema | Prose `summary` and `help` written for a reader, not for a parser |
| A prompt slot | The same content available as documentation, from one source |
The test to apply when adding an agent affordance: what would a person reading this repo cold need in order to do the same thing by hand? If the answer is "read the source", the affordance is not finished.
cms media explain exists because of this rule. Width derivation was correct and completely opaque, which made the sidecar unusable by anyone who had not read the pipeline.
1. The CLI is the default path
Structural work goes through cms and the hooks enforce it: creating, moving, or deleting a page; inserting, reordering, or removing a block; nav; media import; redirects; translations; context records. These are the operations where a hand edit is silently wrong in another file.
Prose may go either way, and the CLI is not clumsy for it:
cms block set /displays/ 3 --field lead --stdin
Direct editing of a markdown page stays legal, because it produces the best diffs for a copy change. When it happens, the hook returns the CLI equivalent alongside the check result. Teaching the surface at the moment of use is how an agent learns it without reading a manual it will not read.
2. Errors state the fix
An error message is the highest-leverage prompt surface in the system, because it arrives exactly when the agent is wrong and it is read in full.
weak cards.items[3].title exceeds 48 characters (61)
strong cards.items[3].title is 61 characters; the limit is 48.
Card titles are scannable labels. Move the detail into `body`,
which allows 240, or shorten the title.
Current: "Import any GTFS feed into structured, fieldable content"
Every validation failure names the constraint, the reason it exists, and at least one way forward.
A parser's own error is rarely the author's mistake. YAML reports "bad indentation of a mapping entry" when a value begins with a backtick, because a backtick is a reserved indicator. The message now says that instead:
Block `prose` body is not valid YAML: bad indentation of a mapping entry
src/content/en/index.md:9
A plain YAML value may not begin with `` ` ``: it is a reserved indicator.
Wrap the whole value in double quotes.
Value: `code` at the start
An agent writing prose about code hits this constantly, and the underlying message gives it nothing to act on.
3. Unknown identifiers list the valid ones
An unknown field, component, route, locale, media name, or context id returns the closest match and the full set when the set is small.
No field `subtitle` on `cards`. Did you mean `lead`? Fields: heading, lead, tag, columns, items
4. Every command takes --json
Agents parse structured output reliably and scrape formatted output badly. The human rendering is a view over the same object, never a separate code path.
5. Every command is idempotent, and destructive ones have --dry-run
Agents retry. Running cms media add twice must not produce two entries. --dry-run prints the exact set of files that would change, which is how an agent checks its understanding before acting rather than after.
6. Printed addresses are copy-pasteable
Anything that names a thing prints it in the form the next command accepts. cms outline prints block numbers because cms block set takes block numbers.
7. One status command
cms status returns the whole state on one screen: what is failing, stale, unattested, untranslated, over budget, and awaiting a person. Agents reconstruct state poorly from several sources and reconstruct it well from one.
8. Schemas carry negative guidance
Every component schema has when_not beside summary.
summary: A grid of linked cards, for routing from an overview to its sections.
when_not: If the items do not link anywhere, this is not cards. A list of
properties is `ticks`. A sequence is `steps`.
Choosing between similar components is where an LLM actually fails, and a negative example does more work there than another sentence of description.
9. Everything that reaches the prompt is budgeted
Prompt slots, standing context records, and the generated component reference all have word caps in .cms/budgets.json, and the check fails over them.
Oversized context is skimmed context, and skimmed context is worse than none, because the resulting work is confident and built on whichever half was read.
10. The build is deterministic
Byte-identical output from the same commit means an agent can verify its own work: rebuild, diff, and see only what it meant to change. Without that, "I think that worked" is the best any session can offer.
11. The system says when a person is required
Some work cannot be finished by an agent: an accessibility attestation, a translation review, a font licence. Those surface in cms status flagged as needing a person, and the check blocks rather than passing quietly. An explicit stop beats a plausible completion.
12. Generated files say so
Every generated file opens with a header naming what regenerates it.
<!-- Generated by `cms docs`. Edits here are overwritten. Change
components/cards/schema.yml instead. -->
An agent that finds an unmarked generated file will edit it, and the edit will vanish on the next build with nothing to explain why.
Applying this to new work
When adding a command, ask: does it have --json, is it idempotent, do its errors name a fix, are its printed addresses accepted by another command. When adding a component, ask: does the schema say when not to use it, and is its class count inside budget. When adding anything that reaches the prompt, ask what it displaces, because context is the scarcest thing in the system.