Site context
Git records what changed. It does not record why, what was considered and rejected, or what is deliberately absent. A session opening a site repo for the first time has the files and git log, and git log is lossy in exactly the places that matter.
Three failures follow, and all three are expensive because they look like competent work:
- Re-deciding something already decided, differently.
- Reintroducing copy or a page that was removed on purpose.
- Missing the house vocabulary, and writing "users" on a site whose readers are riders and whose customers are agencies.
.cms/context/ is where a site keeps the facts that prevent them.
Records
One fact per file, small, with frontmatter that makes it addressable.
--- id: displays-not-screens kind: decision date: 2026-05-02 summary: The section is /displays/, not /screens/. Agencies say "display". scope: [route:/displays/, term:screen, term:display] status: active --- Transit agencies call the hardware a display; "screen" reads as a computer monitor to them. Checked against three agency style guides and the 511 docs. Rejected: /screens/, which tested better with the general public and worse with the people who sign the contracts. The general public does not navigate here.
Two kinds, and the difference is how they reach a session.
| Kind | Meaning | Delivery |
|---|---|---|
| `standing` | True until someone changes it. Vocabulary, audience, constraints, claims that are off limits. | Composed into the `site` prompt slot, loaded every session |
| `decision` | Decided on a date, with what was rejected. | Indexed, delivered when the work touches its scope |
Scope is the load-bearing field
scope declares what a record attaches to, using selectors the engine can resolve:
route:/displays/ component:cards term:screen media:pylon-single locale:es site
That is what lets context arrive at the moment it applies rather than at session start, where it competes with everything else and is forgotten by the time it matters. Three delivery paths:
- Standing records compose into the prompt on
cms sync. Always present. - Scoped records are injected by the
PostToolUsehook: editingsrc/content/en/displays/index.mdreturns the records scoped to that route alongside the check result, in the same feedback the agent already reads. - On demand,
cms context for /displays/orcms context for term:screen.
The second path is the one that changes behaviour. A rule delivered while the agent is editing the file it governs is a rule the agent follows.
Supersession, not deletion
A record is superseded, never removed:
status: superseded
superseded_by: displays-renamed-to-boards
"We decided X in May and reversed it in August" is more useful than X's absence, because the reversal is the thing someone is about to propose again. Both records stay; only the active one is delivered, and the superseded one is one link away.
This is the third use of one mechanism. An accessibility attestation, a translation, and a context record are all a fact that was true about a piece of content on a date, invalidated when that content moves. Same code, same lock file shape, same report.
A record whose scope no longer resolves, because the route was deleted or the component removed, is reported by cms check as needing to be rescoped or superseded. That is a prompt, not a failure.
The budget
Standing context is capped, in words, in .cms/budgets.json. Over budget fails the check.
Context that is too large gets skimmed, and a memory that gets skimmed is worse than no memory, because it produces confident work built on the half of the file that was read. The cap forces the curation that would otherwise never happen. cms context prune lists the standing records least often referenced by recent commits, which is the honest place to start cutting.
Decision records are not capped, because they are not loaded wholesale.
Writing records
cms context add --kind decision --scope route:/displays/,term:screen cms context supersede displays-not-screens --by displays-renamed-to-boards cms context for /displays/ [--json]
The workflow prompt slot tells the agent to record a decision when one gets made in conversation, and to record rejections in particular. A rejection is the highest-value record and the one most reliably lost: the repo shows what was built and never what the client turned down, so the next session proposes it again with enthusiasm.
Session history
No parallel changelog. Git is already the history, and a second one drifts from it within a month. What git needs is structure in the message:
Content: tighten the hero lead on /displays/ Context: displays-not-screens, rider-voice Check: pass
cms commit writes those trailers from what the session actually consulted and what the gate actually returned. cms log /displays/ then joins the two: commits that touched the route, and the decisions in force when each landed.
That is the answer to "what has happened to this page": one command, git as the store, and the records supplying the reasoning git never had.
Why not one file
A single long context file is skimmed by anything reading it, agent or person. Small records with a one-line summary each let a reader see the whole index cheaply and load only what applies. The index is generated, so it cannot drift from the records it lists.
Why not the harness's own memory
Claude Code's memory is per-user and per-machine. This is per-site, and it belongs in the repo where a second person, a second machine, and CI all see the same thing. A site's terminology is not one operator's preference.