Skip to content

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.

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.

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.

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.

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.

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.

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.

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 change

The 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.

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.

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 mark tool; 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.

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.

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).

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.

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: str
confidence: float
reasons: list
blocking: bool
---
## What changed
## What is wrong with it
## What to do

The 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.

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.

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).

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.

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.

gate.sh
#!/bin/sh
if ! grep -q '^## What to do' "$1"; then
echo "The report has no 'What to do' section."
exit 1
fi

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.

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.py

Every 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 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.

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.

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.

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/.

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.

A hook’s exit code counts.

  • before exiting non-zero fails the stage before the agent has run at all, and the record names before as what failed.
  • success exiting 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).
  • failure runs 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 as hash_drift before 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.

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.

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.

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.