Docs format invariants โ
- Status: In progress, opened 2026-08-24.
- Issue:
#954: feat(tooling): a gate that checks a document's stated rules against its own templates - Launch plan: None. This is tooling, not product, so it maps to no row and does not get a fake one.
What is this? โ
A document that states a rule in prose and also ships a template has that rule written twice, and nothing kept the two in agreement. โ
.agents/rules/project/writing-into-documents.mdcalls this the most expensive drift in the repo, and it is the one gate that did not exist.- When the two disagree the TEMPLATE wins, because an agent told to follow a format copies the shape rather than reading the argument for it.
agenda-documentationdrifted six times on 2026-08-24 alone. It carried three renderings of the same log entry, it said "every issue is a link carrying its real title" while none of its three templates held a single reference, and it said the prompt is four bold bullets while all four templates showed bare lines.
A generic prose-versus-template checker is not buildable, so the file declares its invariants and the checker reads them in two directions. โ
- The skill owns a
yaml invariantsblock.selectfinds candidate lines,unlessexcuses some, and exactly one ofrequireorforbiddecides. Two structural checks that no single-line regex can express are named rather than expressed:issue-refsandprompt-spine. - Direction one is the skill's OWN fenced templates. Direction two is a document the skill produced, which for
agenda-documentationis the latest dated agenda in the operator's context home. - A rule that is not in the block is not enforced. That is deliberate: the block is the machine-readable half of the document, not a summary of it.
What is the current state? โ
The gate is built, green, and wired into npm run validate. โ
tooling/scripts/lint.docs-format.jsis the checker.npm run lint:docs-formatruns it, andnpm run lint:docs-format:update-baselinere-snapshots the produced-document ceiling.- It runs in
validateunder thelintscope as Docs Format Invariants. tooling/scripts/__tests__/lint.docs-format.test.jscovers 16 cases, including both failure directions and the bold-lead-in-on-a-bullet case that must NOT fire.
Templates are enforced at ZERO. The produced agenda is ratcheted. โ
- A template lives in the repo and is the copy that ships, so a violation there propagates into every later build. There is no baseline for them.
- The agenda is a living file several sessions write during the day, so its violations sit in
tooling/scripts/docs-format-baseline.jsonas a burn-down list. The pattern islint.no-em-dash.js, which splits UI copy at zero from everything else on a ratchet.
Four template violations were found in agenda-documentation and fixed in the same change. โ
- The three agenda-state templates wrote the header fields as
- Status: ...while the prose says every field name is bold with the colon outside, and while the real agenda already bolds them. - The worked session block wrote
- **Model**: standard, ...wherestandardis a declared tier name, so it takes backticks and keeps its own casing. - The worked session block carried a bare
#882inside itsWatch outline, in the one file whose prose says a reference always carries its title. - The log-entry invariant had to learn that
**[LOG TITLE]**is a legal placeholder opener, which is a template fact rather than a drift.
The agenda's grandfathered ceiling, snapshotted 2026-08-24 at 0827. โ
| Invariant | Count | What it is |
|---|---|---|
issue-ref-carries-title | 33 | Bare #N references, plus one bare linked number |
main-quest-time-block | 11 | Main quests written as - [x] **0642**: ... rather than - [x] 0642-0730: ... |
bold-paragraph-is-a-heading | 4 | A claim standing as a bold paragraph instead of taking a heading |
session-heading-checkbox | 1 | ### [๐] where the checkbox belongs and the status emoji does not |
The ceiling moved three times in twenty minutes, and that is the finding rather than a flaw in the ratchet. โ
- The agenda is being written live. Every appended main quest breaks
main-quest-time-blockagain and every appended issue mention breaksissue-ref-carries-titleagain, because the agenda breaks those two SYSTEMATICALLY rather than occasionally. - So the gate will keep firing on other sessions'
npm run validateuntil those two are burned down to zero in the agenda. Once they are clean, an append cannot re-break them and the gate goes quiet. - This is an operator decision, not an agent one: burn the two down in one PM pass, or drop the produced-document direction to a warning until they are clean. It was deliberately left undecided here.
What did the gate catch on its first day? โ
The [โ] session-5 heading appeared in the live agenda while this session was running. โ
- The baseline read 1 for
session-heading-checkboxat 0815 and 2 at 0820, and the ratchet failed on the difference before it was re-snapshotted. - That is the mechanism working, and it is also the reason the ceiling is a burn-down list rather than a claim that today's agenda is clean.
What did NOT get done? โ
The 48 grandfathered agenda violations were not fixed. โ
- The agenda is the operator's live document, written by several sessions at once. Editing it mid-day is not this session's call.
- Burning them down drops the baseline. The checker warns when a count falls, so the win gets locked in rather than leaving headroom for a silent regression.
The produced-document direction does not run in CI. โ
- The agenda lives in
~/.agents/context/<repo>/agenda/, outside the repo, so CI has no file to read. The checker reports that as SKIPPED and names why. - Zero findings against a file that was never read is not the same fact as zero findings, and reporting the stronger one is how a gate sits green while the thing it guards drifts. That lesson is
lint.no-em-dash.jsand it is honoured here.
No other skill declares invariants yet. โ
- The checker scans every
.agents/skills/*/skill.mdfor a declaration, so a second skill opts in by adding a block. Nothing else was migrated.
What else is related? โ
#952: docs(agents): agent asset hierarchy, plus a drift gate for the workflows layer, the sibling gate one layer up, in docs/projects/agent-workflow-hierarchy/.- .agents/rules/project/writing-into-documents.md, the rule this enforces.
- tooling/scripts/lint.no-em-dash.js, the ratchet pattern this copies.