Most document formats serve one audience. A gateline contract
under contracts/ serves two: an agent writes the artifact, another
agent parses it, and a human reads the result well enough to approve or refuse
a phase of work. The interesting engineering is not the parser. It is
deciding, shape by shape, what a machine must read, what a human must check,
and what happens when an instance is malformed.
Prose as a wire format
The rule that organizes everything here is bounce, never guess. An artifact missing a required section is malformed, and the consuming agent's first duty is to bounce it back to its producer, not to infer what the author meant.
That is a stronger commitment than it looks. A tolerant parser is normally
a virtue: accept what you can, recover, keep going. Here tolerance is the
failure mode. An agent that fills in a missing ## Coverage section
with its own summary of what it believes was reviewed has manufactured evidence
for a gate a human is about to approve. Refusing costs a re-dispatch; guessing
costs a record nobody can trust.
So a contract is written in two registers at once. Some of what it declares is a grammar: literal shapes with regular expressions behind them. Some is a readability rule only a reader can evaluate. Both are normative, both are bounceable, and telling them apart is most of the skill.
What a contract actually declares
There are ten contracts. Each is a short markdown or YAML template whose
normative part is its comments and its headings: a leading block governing the
artifact as a whole, per-section comments carrying caps and rules of their own,
and the ## headings themselves, which are the required-section
list. What fills those sections is an example instance. A template can declare
up to five kinds of rule, and — the part newcomers over-generalize — most
declare only some.
- Required sections
- The template's own
##headings. Every markdown contract has these, and they are what the validator checks, and only on the artifacts a contract covers. - BUDGET
- Concision as a contract property, not a style hope: reference upstream artifacts by number, paste failing output in full and passing output in summary. Seven of the ten declare one.
- GRAMMAR
- Literal id and heading shapes, marked "normative — tooling parses" and
closed by "A deviation is a malformed artifact." Only
contracts/spec.md,contracts/plan.md,contracts/verification-report.mdandcontracts/release-plan.mddeclare one. - READABILITY
- Rules on the sections a gate human reads as prose. Five contracts declare one, and they do not all declare the same one.
- AUDIENCE
- One line naming the sections that are audit-time evidence rather than
decide-time reading, as
<section>=auditpairs.extractAudienceinvalidate.tsparses it and Gatehouse folds those sections to their heading. The line is contract meaning rather than a UI setting.spec.mdandreview-report.mdcarry one.
Two contracts add a block of their own. review-report.md's
VERIFY ROUND fixes the shape a later round uses to disposition earlier
findings, and verification-report.md's VERDICT block fixes the
one-word verdict line.
The ID grammars, and who parses them
Four GRAMMAR blocks declare eight shapes between them. Requirement headings
in a spec are ### R<n> — <short name>. Acceptance
criteria are list items whose text begins AC<n>.<m> — .
Architecture decisions in a plan are
### ADR-<n>[ (<qualifier>)]: <decision>.
Evidence blocks in a verification report are
### E<k> — AC<n>.<m>, with the report's Results
table carrying the bare criterion id in its first column and one overall
**Verdict:** pass | fail | escalate line. A release plan's
preamble fields (**Change released:** and three more bold
labels) and its numbered Release steps are the newest two.
Those shapes exist so a run's artifacts can be read as a graph, not as text.
buildLexicon in
packages/core/src/view-model/lexicon.ts turns a run's own
spec.md and plan.md into definitions Gatehouse serves
to the browser, and buildEvidenceRollup in the sibling
evidence.ts answers one question: for each criterion the spec
defines, where does evidence cite it? Both are deliberately non-judgmental: a
lexicon entry is the exact artifact bytes, "never a paraphrase", and the rollup
computes presence, never verdicts.
That job (locate, never summarize) shows up in small
decisions. Nearly every scanner in core skips fenced code
blocks, so a heading quoted inside a fence is not mistaken for a real one.
scanIds in lexicon.ts deliberately does not, and
says why in a comment: evidence blocks cite criteria from inside fences. It
is not the only scanner that reads through a fence (the review-mention loop
in evidence.ts carries no fence state either, unlike the
verification-report loop earlier in the same function), but it is the only
one that does so on purpose, with its reason attached. The engine's own
verdict reader, parseReviewReport in
packages/orchestrator/src/review-report.ts, reads through fences
as well. The contract's fenced round-two example carries a
**Verdict:** line, so the two verdict readers disagree on that
line: verdictLines in core skips it and the engine
counts it.
Now the part the reference page cannot fit: the shapes are not equally covered, and the coverage does not line up with what the contracts declare.
Normative and parsed are independent properties
Three cells of that two-by-two are occupied, and the empty one is recent.
Most shapes are declared and parsed. Some rules are normative with no code
behind them: the READABILITY family is the clearest case, enforced by a human
reviewer citing the rule. And some shapes are parsed without ever being
declared, which is the dangerous quadrant, because nothing tells an author
they have broken one. Some artifacts sit outside the grid altogether:
retro.md, docs-delta.md and
integration-profile.md get no contract back from
contractFor, and a null contract validates trivially.
contracts/review-report.md is where this collides. It declares
BUDGET, an ESCALATE SCOPE clause, READABILITY, AUDIENCE and a VERIFY ROUND
block, and no block labelled GRAMMAR. The F<n> finding
shape its own template demonstrates was for a long time a convention read by
no code. It is parsed now: parseReview in
packages/core/src/view-model/review.ts reads the finding
headings and their severities, the **Round:** lines, and the
- **F<n> — resolved|stands** dispositions a later round
appends, and the evidence rollup uses it to attribute findings to criteria.
It is declared now as well: the VERIFY ROUND block, normative from round
two, spells out both the finding heading and the disposition line, and says
tooling reads exactly the two disposition words. What the engine reads is
the header line # Review Report: <task id> and every
**Verdict:** line, which no block declares normative; VERIFY
ROUND does not mention either.
The cleanest example of a declared shape with no parser now lives in
contracts/release-plan.md. Its GRAMMAR block makes the bold
preamble fields and the numbered Release steps normative, and nothing in the
repository reads either. Only its ## headings are validated.
Those two undeclared shapes carry more operational weight than any declared
one. parseReviewReport binds a report to its task by the header, not
the filename, because repositories number review files
inconsistently, and treats the last **Verdict:** line as the live
one, because rounds append and never overwrite. The engine routes on that value
(approve moves the task to review-approved,
escalate waits for a human), and the count of verdict lines is
what review_rounds is reconciled against, the number that decides
whether a fourth round becomes an escalation. A human who resolves that
escalation grants the loop one more round.
The asymmetry tells you where to route a fix. If an artifact breaks a declared shape, the artifact is wrong and the contract is doing its job. If code depends on a shape no contract declares, the contract is incomplete, and reaching for the parser first only entrenches the gap.
READABILITY is two rule sets, not one
The second thing newcomers over-generalize is READABILITY. It is easy to read one contract's block and assume a house style. It is not one: the rules follow what each section is for.
Four contracts declare the same five-rule set, governing the sections a gate human reads as argument: Context in a spec, Approach and each ADR's Rejected and Consequences lines in a plan, Beyond the happy path and Gaps in a verification report, and CI health, Rollback plan, Verification after release and Blast radius in a release plan.
- The first sentence states the takeaway in plain words — no code spans, paths, or parenthetical cites.
- One idea per paragraph: at most four sentences and 120 words each.
- Three or more parallel items become a bulleted list under a lead-in sentence, never a semicolon chain.
- One claim per sentence; never join clauses with a semicolon.
- Name before cite: give any id or file a noun phrase on first use, at
most one parenthetical
file:linecite per sentence, full path at first mention only.
contracts/review-report.md governs one section, Coverage, and
declares three different rules:
- Open with one plain-words sentence stating overall coverage — no code spans, paths, or parenthetical cites.
- Then the Coverage table (shape below): one row per requirement or area checked. The table is the shape — a bullet list or a paragraph in its place is in breach. Cites live in the Where column, one location per row; the Mechanism column says what was checked in a clause, never a chain of clauses.
- Name before cite: give any id or file a noun phrase on first use in the opening sentence.
What Coverage does not impose matters as much. It has no one-idea-per-paragraph rule, no four-sentence limit, no three-parallel-items rule, no one-claim-per-sentence rule, and no full-path clause. In their place it prescribes a shape (a four-column table, one row per requirement or area) and moves every cite into a column of its own, one location per row.
That is coherent once you ask what each section is for. A spec's Context is an argument a G0 approver reads to decide whether the problem is understood, so its rules shape argument: one idea, one claim, plain first sentence. A Coverage section is an inventory a G2 approver scans for what was not checked, and an inventory in flowing prose is unscannable, so its contract demands the table and forbids both the list and the paragraph. The two even name their sanctions differently: a spec's breach is "bounced like a grammar deviation," while a review report, having no GRAMMAR block to point at, says a breach is "bounced like a malformed finding."
One consequence: the summary in AGENTS.md (plain-words
opening sentence, one idea per paragraph, lists instead of semicolon chains,
name before cite) describes the five-rule set accurately and is not true of
Coverage. When a summary and a contract disagree, the contract governs.
Evolving a contract without breaking what is committed
A format with committed instances going back through every finished run has an obvious hazard: tighten it and you retroactively invalidate history that must not be edited. Three mechanisms carry the weight.
First, the contract is the authority at read time, not a compiled
copy. validateArtifact derives required sections from the
target repository's own contracts/*.md, fetched from the default
branch by ref. BUILTIN_SECTIONS in
packages/core/src/record/validate.ts is a labelled
fallback for repositories carrying runs/ but no
contracts/ tree, and its docblock hedges that it mirrors the
contracts "at the time of writing." Amend a contract and every consumer's
required-section list moves with it.
Second, matching is deliberately forgiving where forgiveness is
harmless. Section names are lowercased with every non-alphanumeric run
collapsed to a single space before comparison, so
Requirement → task mapping matches a heading typed with a plain
hyphen. Punctuation drift is not a defect worth bouncing; the section being
absent is.
Third, where a prose contract states a rule the machine must also
enforce, the mirror is explicit and fails closed.
docs/DESIGN.md §4.1 defines
patch | standard | full as fixed sets; schema.ts
mirrors that in PROFILE_GATES and PROFILE_PHASES, and
runStateSchema requires every gate the declared profile carries to
be present. Presence is what it enforces, not exclusivity: a surplus entry left
over from a heavier profile parses without complaint, and profile-aware
consumers filter it out. The defaults keep older records meaningful and newer
fields additive: a state.yaml with no profile: field
parses as full, an absent gate entry parses as undecided and "can
never masquerade as approved," and unknown keys pass through untouched.
Spot the malformed artifact
Two cases, in ascending order of how long they take to notice.
Case 1 — a missing section
A spec should carry four sections. Here is the shape, reduced:
# Specification: slug collision handling
## Context
The run scaffolder mints a branch name from the slug and assumes it is free.
Two operators staging runs in the same minute can pick the same slug.
## Requirements
### R1 — Slug collisions fail closed
**Acceptance criteria:**
- [ ] AC1.1 — `gateline new` on an existing slug exits non-zero and mints no branch
## Assumptions
- **ASSUMPTION:** the slug is operator-supplied → resolved as validate at stage
time, because the branch name is derived from it
## Out of scope
Renaming a run that is already staged.
Now delete the ## Context heading and its two sentences.
Nothing looks broken: the requirement is still there, its criterion still
parses, and a careless consumer could reconstruct the problem statement from
the requirements underneath. That inference is what the system
forbids.
What happens instead is mechanical. validateArtifact compares
the spec's ## headings against those of
contracts/spec.md and returns Context in its
missing list. The orchestrator's derivation sees an invalid
artifact and emits its D7 action: re-dispatch the analyst, with the missing
sections named in the dispatch reason and in the commit subject:
state(<slug>): bounced spec.md — re-dispatching analyst
(missing: Context). Later ticks read those subjects back to count
bounces per artifact. On the third malformed delivery the bounce cap of two
is reached
and the derivation switches to D8: escalate and pause, on the reasoning that an
artifact bounced twice is a contract dispute, not a retry that will
converge. Resolving that escalation resets the count, so the next dispute
over the same artifact starts with the same two bounces.
Note what the check does not do. It never looks inside
## Requirements, and with one exception it never reads a
section's body. The exception is the verification report, whose last
**Verdict:** line the validator checks against the three
permitted words and reports as missing otherwise. Everywhere else a Context
section whose first sentence reads "scaffold.ts mints
the branch name (line 115)" satisfies the validator completely and breaches the
first READABILITY rule outright, with a code span and a parenthetical cite both
barred from that sentence. Only a reader catches it, and the sanction is the
same bounce with the rule cited.
Change the artifact class and a missing section is caught somewhere else
entirely. The validator does run over review reports, but its result has
two readers in the engine, and both look only at the artifacts a gate's
producing role owns: spec, plan, work items, verification report, release
plan. The reviewer is not among them. A review report missing
## Coverage but carrying **Verdict:** approve
therefore advances its task to review-approved with no bounce at all.
What closes that gap is the frontend, not the engine. Review files are part
of the G2 gate packet, and a malformed one turns that gate's inbox card into
Packet malformed — bounced, not reviewable, with the missing
section named. So the same word names two mechanisms at two layers: for a
producer's artifact, engine-side re-dispatch, automatic and counted; for a
review report, a human gate declining to open. Know which half is
mechanised before you go looking for a bounce commit that will not exist.
Case 2 — the wrong dash
This one is a single character. A spec writes:
### R1 - Slug collisions fail closed
An ASCII hyphen where the contract's grammar specifies an em dash. The
required-section check passes, because ## Requirements is present
and the heading inside it is never inspected. Nothing bounces. The pull-request
description generator still renders the requirement, because
requirementNames in pr-description.ts accepts an em
dash or a hyphen and requires exactly three hash marks. But
R_HEADING in lexicon.ts, which accepts any heading
level and only an em dash, does not match, so R1 has no lexicon entry, and
Gatehouse cannot show its definition.
Do the same to an acceptance criterion (AC2.1 - …) and the
damage travels further, because the evidence rollup seeds its criteria from the
lexicon. An unparsed criterion is not "uncited"; it is unknown. If the
verification report's Results table cites AC2.1 by its bare id,
the rollup creates the row anyway and marks it defined nowhere. That flag is
the tell: a criterion cited by the record but present in no spec almost always
means a grammar deviation upstream.
Case 1 is a contract doing its job. Case 2 is the shape of every bug in this area you will chase: a rule with no validator behind it, breached by an author who had no way to find out.
Maintainer note
I should be straight about how much of the bounce path is proven. The machinery is designed, implemented and covered by fixtures, but across the three human-orchestrated runs that took the pipeline end to end, not one artifact arrived malformed. The mdtoc retro records "no D7 bounce was exercised," and dupefind's "three runs, zero D7 bounces — bounce handling remains fixture-only evidence." Supported is not exercised, and I would rather say which one this is.
Check your understanding
contracts/review-report.mddeclares no GRAMMAR block. Name the shapes code parses anyway, and say which contract now holds a shape declared normative that no code reads.- A spec writes
### R1 - Slug collisionswith an ASCII hyphen. Which parser sees the requirement, which does not, and what bounces? - The Coverage section of a review report: which of the five READABILITY rules does its contract not impose, and what does it demand instead?
- An artifact is bounced twice and arrives malformed a third time. What does the engine do, and why is that better than a longer retry?
Further reading
- Contract grammar reference: the exact shapes and the contract-by-contract table.
contracts/in the framework repository: ten files, all short. Readspec.mdandreview-report.mdback to back; the contrast is the whole lesson.docs/DESIGN.md§5 for the contract model, and the "Invariants — check before editing" section ofAGENTS.md.- The co-written state file: the commit-message grammar as evidence for a human, not input to a decision, and the two parsers in the codebase that read commit subjects.
- How a run works for where each artifact sits in the G0–G3 lifecycle.