Release Gate
The producer's claim is never the release.
Three events are easy to collapse into one and must stay separate:
- The work is correct.
- The producer says it is done.
- The system releases the result.
An agent that writes its own status brief will, some nights, invent a ticket number, a
milestone, or a date that looks right. If the brief is written straight to disk, that
invention ships with the same authority as a real fact. This skill owns event 3 with a
verifier that cannot be talked into anything, because it is not a model.
When to use
- A model produces an artifact that cites specifics (ids, dates, URLs, commits, paths) and
something downstream will act on it: a daily brief, a PR description, a release note, a
customer-facing summary, a handoff packet, a scheduled report.
- The producer runs unattended (cron, pipeline, overnight batch) and nobody reads the
output before it lands.
- You have a second model "reviewing" the first and the reviews cost tokens, drift, and
cannot be reproduced.
Avoid
- Judging quality, tone, or completeness. This gate checks that specifics are grounded,
nothing else. Pair it with a critique pass for quality.
- Gating free prose that has no evidence set. With nothing to ground against, every date
is "invented"; define the evidence first or do not gate.
- Actions with side effects that already happened. The gate must run before the write,
the publish, or the send; verifying after the fact only tells you what to apologize for.
Prerequisites
- Python 3.10+. No third-party packages; tests use pytest.
- A claim: the artifact about to ship, as a text file.
- An evidence set: the inputs the producer was allowed to cite, as one or more files.
JSON dicts are serialized with policy-like keys dropped (see below); everything else is
read as text.
- A fallback: a deterministic artifact that ships when the claim is rejected. Usually
"Nothing found" plus the raw facts. It must never come from the producer.
Procedure
Split the producer from the release. The producer returns the claim alone. Its
reasoning, chain of thought, or self-assessment never enters the gate; a verifier that
reads "I checked this carefully" is a verifier that can be persuaded.
Freeze the evidence set. Serialize exactly what the producer was handed: collected
facts, the day's date, the diff, the issue body. Exclude policy documents, prompt seeds,
and the whole history; those would ground any date or id that ever existed. The script
drops the JSON keys system_prompt, policy, seed, history, constitution by
default (--exclude-key adds more).
Run the gate.
python release_gate.py --claim brief.md --evidence facts.json --fallback nothing.md --ledger gate.jsonl --label cmo
Exit 0 prints the claim (release). Exit 1 prints the fallback and lists every
ungrounded token on stderr (recover). Exit 2 is a usage error (unknown kind, bad
--require regex, unreadable file, a .json evidence file that does not parse) and
releases nothing, ledgers nothing. The gate fails closed: unparseable JSON is never kept
as raw text, because that would let an excluded key ground the claim.
Recover to the fallback, never retry. A retry re-runs the producer that just
invented something, with the same inputs and the same incentive. The fallback is the
same path an empty producer response already takes, so recovery costs no new code.
Ledger every verdict. Each run appends verdict, state, reject_reasons, and
on rejection the full rejected_claim. False rejections are read from the ledger, not
hidden as producer failures. After two weeks, read the rejected rows and decide whether
a token kind is too strict for your artifact before loosening anything.
Add a required pattern where silence is itself an invention. If pending items exist,
the brief must name one: --require "/view/\d+". A producer that says "nothing to
read" while the queue is full is rejected the same way as one that invents an id.
Token kinds
| Kind |
Matches |
Default |
date |
YYYY-MM-DD |
on |
url |
http:// or https:// up to whitespace or a closing bracket; trailing .,;:!? dropped |
on |
ref |
#123, ABC-123 (ticket style) |
on |
hash |
7 to 40 hex characters, either case (commits, content hashes) |
on |
path |
dir/file.ext |
off |
number |
bare integers, negatives, decimals, percents |
off |
path and number are opt-in (--kinds date,url,ref,hash,path,number) because prose
carries numbers and paths that were never meant as citations; turn them on for
artifacts like changelogs where every figure should trace to the diff.
Every match must appear verbatim in the evidence text. Substring matching is deliberate:
it is cheap, has no false negatives on the tokens it knows, and its false positives are
visible in the ledger.
Why deterministic, not a second model
A second model reviewing the first costs tokens on every run, can be argued into passing
a claim by the claim itself, and gives non-reproducible rejections you cannot audit. A
regex grounding check by different code is more independent than a second copy of the
same weights, costs nothing, and every rejection is replayable from the ledger row.
The underlying result (arXiv 2609.20474, "How Do Agent Harnesses Create Value? Planning
Information and Release Control in Stateful LLM Agents") is that a read-only verifier
seeing only the objective, required outputs, evidence, and artifact captured nearly all
of the false-pass reduction of a full harness for under a cent per episode. The
limitation the paper names, that terminal verification arrives after side effects, is
handled here by running the gate before the only write.
Wiring into an agent loop
from release_gate import verify, load_evidence
claim = producer(facts) # CLAIMED, not released
reasons = verify(claim, load_evidence([facts_path]), require=[r"/view/\d+"] if facts["pending"] else None)
if reasons:
ledger.append({"verdict": "rejected", "reject_reasons": reasons, "rejected_claim": claim})
claim = fallback(facts) # deterministic, never the producer again
else:
ledger.append({"verdict": "verified"})
write_brief(claim) # the only side effect, after the verdict
Tests
python -m pytest test_release_gate.py -q runs the risk register (17 cases): invented
ref, date, URL, and hash each rejected; grounded claim passes; excluded keys never
ground; unparseable or BOM-prefixed JSON evidence fails closed; URL trailing punctuation
does not cause a false rejection; number edge cases (before a period, negative, percent);
required pattern; empty claim; opt-in kinds stay off; CLI rejects to fallback and ledgers
the claim; CLI passes a verified claim through unchanged; bad regex, missing fallback, and
unknown kind exit 2 with nothing released and nothing ledgered.
Open risk, waived until the ledger has rows: correct-but-unusual output (a real date the
collector did not capture) reads as a false rejection. The ledger is where you find it.
Example
Claim from an overnight brief:
Read "Why the retry loop lies" at /view/41 today, then ship it by 2026-09-24.
Evidence: {"day": "2026-09-19", "pending": [{"id": 41, "title": "Why the retry loop lies"}]}.
Result: REJECTED, date not in evidence: 2026-09-24. The producer invented a deadline.
The fallback brief ships ("1 pending post: Why the retry loop lies, /view/41"), the rejected claim
sits in the ledger, and the next morning the operator sees a grounded brief instead of a
plausible one.
1---2name: release-gate3description: Put a read-only grounding check between 'the agent says done' and 'the result ships'. Every date, id, URL, hash (optionally path and number) in the output must appear in the evidence the producer was given, or the output is rejected and a deterministic fallback ships instead. Use when: releasing a subagent's brief, report, PR description, changelog, status update, or any artifact that cites specifics; wiring a cron or pipeline that publishes model output unread. Triggers: 'gate this before it ships', 'verify the claim', 'is this grounded', 'release gate', 'the agent invented a date'.4---56# Release Gate78The producer's claim is never the release.910Three events are easy to collapse into one and must stay separate:11121. The work is correct.132. The producer says it is done.143. The system releases the result.1516An agent that writes its own status brief will, some nights, invent a ticket number, a17milestone, or a date that looks right. If the brief is written straight to disk, that18invention ships with the same authority as a real fact. This skill owns event 3 with a19verifier that cannot be talked into anything, because it is not a model.2021## When to use2223- A model produces an artifact that cites specifics (ids, dates, URLs, commits, paths) and24 something downstream will act on it: a daily brief, a PR description, a release note, a25 customer-facing summary, a handoff packet, a scheduled report.26- The producer runs unattended (cron, pipeline, overnight batch) and nobody reads the27 output before it lands.28- You have a second model "reviewing" the first and the reviews cost tokens, drift, and29 cannot be reproduced.3031## Avoid3233- Judging quality, tone, or completeness. This gate checks that specifics are grounded,34 nothing else. Pair it with a critique pass for quality.35- Gating free prose that has no evidence set. With nothing to ground against, every date36 is "invented"; define the evidence first or do not gate.37- Actions with side effects that already happened. The gate must run before the write,38 the publish, or the send; verifying after the fact only tells you what to apologize for.3940## Prerequisites4142- Python 3.10+. No third-party packages; tests use pytest.43- A **claim**: the artifact about to ship, as a text file.44- An **evidence set**: the inputs the producer was allowed to cite, as one or more files.45 JSON dicts are serialized with policy-like keys dropped (see below); everything else is46 read as text.47- A **fallback**: a deterministic artifact that ships when the claim is rejected. Usually48 "Nothing found" plus the raw facts. It must never come from the producer.4950## Procedure51521. **Split the producer from the release.** The producer returns the claim alone. Its53 reasoning, chain of thought, or self-assessment never enters the gate; a verifier that54 reads "I checked this carefully" is a verifier that can be persuaded.552. **Freeze the evidence set.** Serialize exactly what the producer was handed: collected56 facts, the day's date, the diff, the issue body. Exclude policy documents, prompt seeds,57 and the whole history; those would ground any date or id that ever existed. The script58 drops the JSON keys `system_prompt`, `policy`, `seed`, `history`, `constitution` by59 default (`--exclude-key` adds more).603. **Run the gate.**6162 ```63 python release_gate.py --claim brief.md --evidence facts.json --fallback nothing.md --ledger gate.jsonl --label cmo64 ```6566 Exit 0 prints the claim (release). Exit 1 prints the fallback and lists every67 ungrounded token on stderr (recover). Exit 2 is a usage error (unknown kind, bad68 `--require` regex, unreadable file, a `.json` evidence file that does not parse) and69 releases nothing, ledgers nothing. The gate fails closed: unparseable JSON is never kept70 as raw text, because that would let an excluded key ground the claim.714. **Recover to the fallback, never retry.** A retry re-runs the producer that just72 invented something, with the same inputs and the same incentive. The fallback is the73 same path an empty producer response already takes, so recovery costs no new code.745. **Ledger every verdict.** Each run appends `verdict`, `state`, `reject_reasons`, and75 on rejection the full `rejected_claim`. False rejections are read from the ledger, not76 hidden as producer failures. After two weeks, read the rejected rows and decide whether77 a token kind is too strict for your artifact before loosening anything.786. **Add a required pattern where silence is itself an invention.** If pending items exist,79 the brief must name one: `--require "/view/\d+"`. A producer that says "nothing to80 read" while the queue is full is rejected the same way as one that invents an id.8182## Token kinds8384| Kind | Matches | Default |85|---|---|---|86| `date` | `YYYY-MM-DD` | on |87| `url` | `http://` or `https://` up to whitespace or a closing bracket; trailing `.,;:!?` dropped | on |88| `ref` | `#123`, `ABC-123` (ticket style) | on |89| `hash` | 7 to 40 hex characters, either case (commits, content hashes) | on |90| `path` | `dir/file.ext` | off |91| `number` | bare integers, negatives, decimals, percents | off |9293`path` and `number` are opt-in (`--kinds date,url,ref,hash,path,number`) because prose94carries numbers and paths that were never meant as citations; turn them on for95artifacts like changelogs where every figure should trace to the diff.9697Every match must appear verbatim in the evidence text. Substring matching is deliberate:98it is cheap, has no false negatives on the tokens it knows, and its false positives are99visible in the ledger.100101## Why deterministic, not a second model102103A second model reviewing the first costs tokens on every run, can be argued into passing104a claim by the claim itself, and gives non-reproducible rejections you cannot audit. A105regex grounding check by different code is more independent than a second copy of the106same weights, costs nothing, and every rejection is replayable from the ledger row.107108The underlying result (arXiv 2609.20474, "How Do Agent Harnesses Create Value? Planning109Information and Release Control in Stateful LLM Agents") is that a read-only verifier110seeing only the objective, required outputs, evidence, and artifact captured nearly all111of the false-pass reduction of a full harness for under a cent per episode. The112limitation the paper names, that terminal verification arrives after side effects, is113handled here by running the gate before the only write.114115## Wiring into an agent loop116117```python118from release_gate import verify, load_evidence119120claim = producer(facts) # CLAIMED, not released121reasons = verify(claim, load_evidence([facts_path]), require=[r"/view/\d+"] if facts["pending"] else None)122if reasons:123 ledger.append({"verdict": "rejected", "reject_reasons": reasons, "rejected_claim": claim})124 claim = fallback(facts) # deterministic, never the producer again125else:126 ledger.append({"verdict": "verified"})127write_brief(claim) # the only side effect, after the verdict128```129130## Tests131132`python -m pytest test_release_gate.py -q` runs the risk register (17 cases): invented133ref, date, URL, and hash each rejected; grounded claim passes; excluded keys never134ground; unparseable or BOM-prefixed JSON evidence fails closed; URL trailing punctuation135does not cause a false rejection; number edge cases (before a period, negative, percent);136required pattern; empty claim; opt-in kinds stay off; CLI rejects to fallback and ledgers137the claim; CLI passes a verified claim through unchanged; bad regex, missing fallback, and138unknown kind exit 2 with nothing released and nothing ledgered.139140Open risk, waived until the ledger has rows: correct-but-unusual output (a real date the141collector did not capture) reads as a false rejection. The ledger is where you find it.142143## Example144145Claim from an overnight brief:146147> Read "Why the retry loop lies" at /view/41 today, then ship it by 2026-09-24.148149Evidence: `{"day": "2026-09-19", "pending": [{"id": 41, "title": "Why the retry loop lies"}]}`.150151Result: `REJECTED`, `date not in evidence: 2026-09-24`. The producer invented a deadline.152The fallback brief ships ("1 pending post: Why the retry loop lies, /view/41"), the rejected claim153sits in the ledger, and the next morning the operator sees a grounded brief instead of a154plausible one.