Skip to content

Docs format invariants โ€‹

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.md calls 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-documentation drifted 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 invariants block. select finds candidate lines, unless excuses some, and exactly one of require or forbid decides. Two structural checks that no single-line regex can express are named rather than expressed: issue-refs and prompt-spine.
  • Direction one is the skill's OWN fenced templates. Direction two is a document the skill produced, which for agenda-documentation is 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.js is the checker. npm run lint:docs-format runs it, and npm run lint:docs-format:update-baseline re-snapshots the produced-document ceiling.
  • It runs in validate under the lint scope as Docs Format Invariants.
  • tooling/scripts/__tests__/lint.docs-format.test.js covers 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.json as a burn-down list. The pattern is lint.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, ... where standard is a declared tier name, so it takes backticks and keeps its own casing.
  • The worked session block carried a bare #882 inside its Watch out line, 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. โ€‹

InvariantCountWhat it is
issue-ref-carries-title33Bare #N references, plus one bare linked number
main-quest-time-block11Main quests written as - [x] **0642**: ... rather than - [x] 0642-0730: ...
bold-paragraph-is-a-heading4A claim standing as a bold paragraph instead of taking a heading
session-heading-checkbox1### [๐Ÿ”„] 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-block again and every appended issue mention breaks issue-ref-carries-title again, because the agenda breaks those two SYSTEMATICALLY rather than occasionally.
  • So the gate will keep firing on other sessions' npm run validate until 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-checkbox at 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.js and it is honoured here.

No other skill declares invariants yet. โ€‹

  • The checker scans every .agents/skills/*/skill.md for a declaration, so a second skill opts in by adding a block. Nothing else was migrated.

Built with VitePress