Gating
Stability: stable.
An agent finishes a stage by stopping. Gating is what stands between stopping and leaving.
Three checks run, in this order, and each is optional:
| Order | Check | Judges |
|---|---|---|
| 1 | the checklist | what the agent did |
| 2 | the schema | the shape of the output |
| 3 | the gate | whether the work is good |
The order runs from cheapest to most expensive and from most general to most specific. The first failure ends the round; a gate is never asked about work whose checklist is unfinished, and a schema is never applied to a file that was never written.
Before any of them
Section titled “Before any of them”An agent that stopped without writing $OUTPUT has not finished. That is caught
first, and it is treated like any other failure below. This is the one gating
text no author writes: the message is the runtime’s fixed sentence, “Nothing
was written to $OUTPUT.”, and its capture under that attempt’s checks/ is
named output-missing.txt (the record). An
output check that passes records an empty output.txt capture in the same
place, so the attempt’s checks/ folder shows that the check ran.
What a failure does
Section titled “What a failure does”The agent is held, not restarted. Its session is intact, it has not left the stage, and the failing check’s output goes into that session as one more thing it has read. It carries on working and stops again when it believes it is done, and the checks run again from the top.
A gate that exits 75 with nonempty captured output is different: it is the
external-blocker verdict, not a failed round. It ends the stage immediately
with exit 1 and cause blocked, without a send-back or retry; its failure
hook still runs. No other check or machinery can produce that verdict.
What the agent is told
Section titled “What the agent is told”That it is blocked from finishing, and the text the check produced. A failing gate is the exception in shape, not in information: its output follows the fixed action frame in gate.md. Nothing identifies the gate or its place in the runtime.
The text arrives in the agent’s session as the answer to its attempt to stop — the agent went quiet, the runtime intercepted the stop, and this is what came back (invariant 30).
It is captured to the run before it is handed over. A gate’s capture is its verbatim output; the agent’s later session turn is the fixed frame followed by those same bytes (the record).
It is not told which check produced the text, that checks exist, that there are three of them, or that anything ran at all. There is a thing it has to fix and a description of what is wrong, which is everything it needs and the whole of what it gets (invariants).
This is why what a check prints matters so much. It is not a diagnostic filed somewhere; it is the description of what is wrong, and for a failed gate it follows the runtime’s request to act.
retries bounds how many send-backs a stage allows, whichever check caused
them. At the default of 2, the agent’s first stop is checked and it may be sent
back twice — three rounds at most. When the last round fails, the stage fails.
This is the reason gating is worth its cost. A check that failed a stage outright would throw away everything the agent learned and charge for it again. A check that holds the agent in place costs one more turn.
What gating is for
Section titled “What gating is for”An agent working alone produces what it believes is right. Gating is the part of
the format that can disagree with it, in the author’s words, written beside the
prompt rather than inside it. schema.json and a gate are decided by a program;
schema.md machine-validates its frontmatter only and leaves its body as an
unchecked template for the agent.
Each check covers what the others cannot see. A checklist catches work that leaves no trace in the output. A schema catches an output that cannot be read by the next stage. A gate catches an output that is well-formed and wrong.
Where they live
Section titled “Where they live”A checklist is a heading in the stage’s markdown. A schema and a gate are files in the stage folder (the stage). All three belong to one stage, and a stage is the only thing that has them: they hold an agent back, and a stage is where the agent is.
The checklist
Section titled “The checklist”Stability: stable.
A checklist is a list of things the agent has to account for before it can leave the stage.
It is the first of a stage’s three checks. When the agent stops, the runtime looks at the checklist, then at the schema, then at the gate (the stage). All three are optional; whichever exist run in that order.
Where it is written
Section titled “Where it is written”Under a heading whose text, whitespace-trimmed, is exactly the word
Checklist — case-sensitive, at any heading level — in the markdown body of
the stage, and nowhere else. A checklist belongs to one stage; there is no
flow-wide or assembly-wide list.
Every top-level list item under that heading, up to the next heading of any level, is one checklist item. Nested items are part of the item above them.
## Checklist
- Every public function has a docstring- The test suite passes- CHANGELOG.md names the changeThe heading is part of the prompt, so the agent reads its checklist along with its instructions. Nothing else declares it: no frontmatter key, no separate file.
The three states
Section titled “The three states”Every item starts as todo, its start state. The agent moves it, one item at a time, to one of two resting states:
| State | Meaning |
|---|---|
todo |
not accounted for |
done |
the agent did it |
skipped |
the agent decided it did not apply, and said why |
The agent marks items with the mark tool (control
tools), giving an item’s number — 1-based, in the
order the list is written, so two identically worded items stay distinct — its
new state, and evidence. Evidence is a required nonempty string naming the
proof; its quality is for the checklist author to demand, not the runtime to
judge. It cannot edit the list, add to it, or remove from it.
A skip also needs a reason. skipped with nothing behind it is not a mark: it
is returned to the agent as an error and the item stays todo
(invariant 33). A mark without nonempty evidence is likewise
an error and leaves the item todo. The record is the point of allowing skips
and marks: an empty reason or empty or missing evidence records nothing.
Marking the same item twice replaces the first mark, and every mark is recorded
with its evidence, so an item that went to done and then to skipped shows
both. A number that is not on the list is an error returned to the agent, not a
stage failure.
What it blocks
Section titled “What it blocks”An item left todo when the agent stops blocks the exit. The unfinished items
go into the agent’s session, followed by:
The items above are unmarked. If an item is done, you must mark it with the
marktool; prose does not count.
The agent carries on working in that same session, exactly as it does when a gate says no.
Being sent back for an unfinished checklist spends one of the stage’s retries,
the same as any other check.
skipped does not block. A checklist is there to make sure nothing is passed
over silently, not to force every item. An agent that judges an item irrelevant
says so and moves on; what it said is kept in the record, so the decision is
visible instead of invisible.
Because a checklist fails before the schema and the gate are reached, an agent that has not finished its list is never told what the gate thinks. There is no value in judging work the agent has already been told is incomplete.
Why it exists
Section titled “Why it exists”A schema checks the shape of the output and a gate checks its content. Neither can see the things that leave no trace in the output — the test that was never run, the file that was never read, the step that was skipped because the agent forgot it existed.
The checklist covers that gap in the cheapest way available: the agent is asked, before it can leave, whether it did the things it was told to do.
The schema
Section titled “The schema”Stability: stable.
A schema declares what a stage’s output has to be. It is a file in the stage folder, and its extension decides the output’s format.
| File | $OUTPUT is |
The runtime |
|---|---|---|
schema.json |
JSON | validates against the schema |
schema.md |
Markdown | hands it to the agent, validates its frontmatter |
A stage folder holds one schema. A stage with none writes text.
This is the second of the three checks (gating).
schema.json
Section titled “schema.json”JSON Schema, dialect 2020-12. A $schema key naming a different dialect is
a malformed assembly, so a runtime never has to guess which rules a schema was
written against and two runtimes never disagree about one. There is no
schema.yaml: one carrier syntax for data means one parser, one refusal
surface, and no second way to write the same thing.
The output is parsed and validated. A file that does not parse, or that parses and does not match, sends the agent back with the parser’s or the validator’s complaint as the reason.
The agent is shown the schema, because a schema is an instruction. It is the stage saying what the next stage needs.
schema.md
Section titled “schema.md”A template rather than a validator — except for its frontmatter, which is data and is validated. Each key the template’s frontmatter names must appear in the output’s frontmatter, and a key may carry a slim type saying what its value must be:
---verdict: strconfidence: floatreasons: listblocking: bool---
## What changed
## What is wrong with it
## What to doThe slim types are str, int, float, bool, list, and date — date
is a calendar date, YYYY-MM-DD, and nothing looser. A key with no value
accepts anything. Keys the template does not name are the output’s own
business: they pass through unexamined, the way the body does. This is the
whole of the language — a namespace with types, not a schema notation folded
into frontmatter; an output whose frontmatter needs more than this is data, and
data takes schema.json.
An output missing a named key, or carrying a value of the wrong type, is sent back like any schema failure. The frontmatter’s bytes must be valid UTF-8 — it is data, and a mis-encoded value that decodes to a replacement character passes the very check meant to catch it — and the body’s need not be, like a prompt body’s. The body is not validated: the runtime hands the template to the agent as the shape to write in, and whether the prose honors it is a question for a gate, which can read markdown the way a person would and say what a parser cannot.
Markdown is what most stages produce, because most stages produce something a person will read. A template lets those stages be specific about form without pretending that prose has a syntax.
Choosing between them
Section titled “Choosing between them”Use schema.json when a later stage or a script will read the output as data.
The validation is free and the failure message is precise.
Use schema.md when the output is prose with a known shape.
Use no schema when the output is prose and the shape does not matter.
After success
Section titled “After success”A success hook does not rewrite the output (hooks). The output is
hashed the moment its checks pass, and what passed the checks is what is
sealed, always — so the promise that a .json file parses and matches cannot
be quietly broken on the way out. Any rewrite is drift, and drift is the
assembly being wrong: the run exits 2, cause fault, and the record names
the file (the record).
What the next stage sees
Section titled “What the next stage sees”The extension the schema chose is the extension of the file that lands in the
next stage’s $INPUT (slots). A stage named analyze with a
schema.json puts analyze.json in front of whatever runs next, and that is
how the next stage knows it can pipe it through jq.
The gate
Section titled “The gate”Stability: stable.
A gate is a program that reads a stage’s output and decides whether it is good enough to leave the stage.
It is the last of the three checks (gating), and the only one that judges the work rather than its form.
#!/bin/shif ! grep -q '^## What to do' "$1"; then echo "The report has no 'What to do' section." exit 1fiHow it is run
Section titled “How it is run”A gate is an executable in the stage folder named gate, with any extension or
none — gate.sh, gate.py, gate.ts, gate. It needs its executable bit set
and a shebang, or is a compiled binary, so any language will do.
More than one
Section titled “More than one”A stage that checks several things puts a gate/ folder in place of the file:
02-analyze/ STAGE.md schema.json gate/ 01-lint.sh 02-tests.sh 03-house-style.pyEvery entry in the folder is a gate, and they run in the order their names sort.
Its basename stem must not be a reserved hook name: before, success, or
failure. Every entry has to be executable, and an empty gate/ folder is a
malformed assembly. Numbered prefixes are how an author says which order;
without them the order is alphabetical, which is still an order and still
stable.
Run them cheapest first. The first non-zero exit ends the round and its output is the reason the agent is given, so a linter that takes a second belongs ahead of a test suite that takes a minute.
The record names which gate failed, which is the reason a folder is worth having over one script that calls three things.
A stage has a gate file or a gate/ folder. Both is an error.
The runtime passes the path of the output file as the first argument. The
stage’s slots are in its environment as well, so $OUTPUT names the same file
and $INPUT names what the stage was given.
A gate reads a file rather than a stream because a gate is written to inspect: it re-reads, it seeks, it hands the path to a linter or a test runner or a parser. Anything that takes a filename works without a temporary file.
A gate reads that file and does not write to it. It may copy it, parse it, hash it, hand it to a linter; the bytes it was given to judge are the bytes it leaves behind. Repairing an output is work, and work is a stage’s job, not a gate’s. A write that is still there when the output is sealed is caught — what passed the checks is what is sealed, and bytes that differ end the run (the record) — but a write the gate undoes before it exits is caught by nothing, and a runtime is not required to do anything about it. The run succeeds, the record seals the bytes the stage wrote, and nothing in it says the verdict was reached on something else. This is a rule about what a gate is for and not a boundary the runtime holds: a gate that writes to its output has judged a file no reader of the record will ever see, and its verdict means less than it appears to.
The verdict
Section titled “The verdict”The exit code, except that 75 with evidence is an audited external blocker.
| Exit | Meaning |
|---|---|
0 |
the output passes; anything the gate printed is not shown to the agent, though the record keeps it |
75 with nonempty output |
an external condition blocks the run; the stage ends immediately with exit 1, cause blocked, and a bounded view of the captured output as its reason |
| other non-zero | the output fails; everything the gate printed is the reason |
On failure, stdout and stderr are captured together. A 75 is a blocker only
when those combined captured bytes have at least one byte: its bytes are kept
exactly, without trimming, parsing, or assigning them meaning. A silent 75
is an ordinary failing gate and uses the retry budget. The agent receives an
ordinary failure’s output after this runtime-written frame:
Your output did not pass review. Fix the cause, then rewrite your output. If the cause is outside what you were asked to do, say so plainly instead of retrying.
The review output follows:
The frame says what happened and what to do without telling the agent about the machinery that made the judgment. The agent carries on in the same session (gating).
Valid UTF-8 output at or below 10,000 bytes reaches the agent unchanged. Larger valid UTF-8 output becomes a deterministic head-and-tail view no larger than 10,000 UTF-8 bytes. The notice between the two parts gives the original and omitted byte counts and directs the reader to check.capture. It cuts only between code points. A terminal blocked or exhausted reason uses the same rule with a 2,048-byte ceiling. Invalid UTF-8 remains exact in the capture. The agent and terminal reason receive only a fixed message that says text rendering was unavailable and names check.capture; they receive no partial decode or replacement characters.
126 and 127 come from the shell, not from the gate — a gate that could not
be executed is a broken assembly, and the run fails rather than the agent being
sent back (the runtime). Timeout, signal, execution
failure, output overflow, and 126/127 retain that machinery precedence over
a 75 verdict.
A gate gets the stage’s timeout as its own budget, counted from when the gate
starts. It does not take what is left of the agent’s clock: a gate that runs a
ninety-second test suite should pass or fail on its own terms, not on how long
the agent happened to take before it.
A gate that runs out of time fails the run rather than the agent. A gate that hangs is wrong, and sending an agent back to rewrite good work because a linter deadlocked is the failure this format exists to avoid.
Writing the reason
Section titled “Writing the reason”What a failing gate prints is read by an agent that is about to try again, so it is worth writing as an instruction rather than as a diagnosis. “The report has no ‘What to do’ section” is a repair the agent can make. “validation failed” is a sentence it has to guess at.
A gate that passes says nothing, because there is nobody to say it to.
What runs first
Section titled “What runs first”The schema is applied before the gate, so a gate reading a schema.json stage’s
output knows the file parses and matches. A schema.md stage’s body — its
frontmatter is validated first (the schema) — and a stage with
no schema reach their gate unvalidated, which is the case a gate is most useful
in: it can read prose and hold it to a standard that no validator expresses.
In the record
Section titled “In the record”Every gate invocation is recorded — its exit code, everything it printed, and
the hash of the gate’s bytes as they were at that moment. A hash that disagrees
with the one taken when the assembly was read ends the run: exit 2, the record
naming the file (the record).
Stability: stable.
A hook is an executable in a stage folder, named for when it runs.
| Hook | Runs | May change |
|---|---|---|
before |
before the agent loop | $INPUT |
success |
after the output passes every check | nothing |
failure |
after the stage fails | nothing |
Each belongs directly in the stage folder, not in its gate/ folder. Each
needs its executable bit set and a shebang, or is a compiled binary, so any
language will do. An extension is for whoever reads the folder; the runtime
ignores it (the stage). This
extension-independent hook recognition does not apply inside gate/.
How a hook is run
Section titled “How a hook is run”A hook is an ordinary process, started with the same slots in its environment as
the agent gets — $INPUT, $OUTPUT, $TMP, $SKILLS, $PWD, and
$SUBFLOWS when the stage has subflows in scope — over the caller’s
environment, which passes through beneath them
(slots, invariant 43).
failure gets two things more, because a hook that reports a failure should be able to say what happened. $CAUSE holds the one word from the cause vocabulary. $REASON holds the path to retained captured text when one exists. The capture contains a failing check’s output, an agent’s refusal, or an agent-reported fault’s reason.
It changes things by writing files, not by piping bytes. before rewrites,
adds, or removes the files in $INPUT, and the agent sees what before left
there — the prompt names whatever is in the directory when the agent starts
(prompt construction). This is what lets a hook work when
$INPUT holds three files from three branches, which a pipe could never
express.
success is different: it runs after the output passed every check, and the
output is no longer anyone’s to change. The hook holds $OUTPUT as a path to
read — copy it, publish it, announce it — and the next stage sees what the
agent wrote and the checks judged. Bytes that differ at sealing are drift, and
drift ends the run (the record). A transformation of the output
is work, and work is a stage’s job, not a hook’s.
Whatever a hook prints on stdout and stderr is captured as diagnostics and kept with the run. It is not the hook’s output; the files are.
Exit codes
Section titled “Exit codes”A hook’s exit code counts.
beforeexiting non-zero fails the stage before the agent has run at all, and the record namesbeforeas what failed.successexiting non-zero fails a stage whose output had already passed every check.- Either way the cause is
rejected: the assembly’s own machinery said no, which is neither the agent refusing nor anything being broken (the record). failureruns after the fact. Its exit, timeout, inability to execute, or output overflow is diagnostic evidence only: its capture and hook event remain in the record, but it cannot replace the stage’s existing ending. A hash-recheck drift is recorded ashash_driftbefore the hook starts, so no hook event or capture exists; it likewise cannot replace the ending.
126 and 127 are the shell’s, not the hook’s — a gate or before or
success hook that could not be executed is a broken assembly, not a verdict
(the runtime). The same failure-hook breakage is
recorded as diagnostic evidence without changing the failed stage’s ending.
A hook gets the stage’s timeout as its own budget, counted from when the hook
starts. It does not compete with the agent for a share of one clock, because a
hook that is malformed should fail the same way every time rather than depending
on how long the agent happened to take.
When they run
Section titled “When they run”A hook runs once per stage. An agent held back by a failing check has not left
the stage, so being sent back does not re-run before.
failure runs whenever a stage fails, whatever failed it — a check, a refusal, a timeout, an agent-reported fault, before, or success. It does not run when a signal ended the stage. When before was what failed there is no output for it to look at, and $OUTPUT names a file that was never written. When success was what failed, both hooks ran, each once.
Stage level
Section titled “Stage level”A hook belongs to one stage, and a stage is the smallest thing that has an input
and an output to bracket. A flow that wants its request normalized puts a
before on its first stage; a flow that wants its result published puts a
success on its last.
What hooks are for
Section titled “What hooks are for”Injecting policy without touching what the stage says: preparing a worktree, scanning an output for secrets, normalizing an input into the shape the prompt assumes, publishing a result somewhere.
The division is that the author of a flow writes stages, and whoever runs the flow somewhere particular writes hooks.