An AI agent can open a pull request against a production repository. In a regulated business three questions follow immediately, and most teams cannot answer any of them.
- What was the agent permitted to do? Not what it was able to do. What it held authority to do.
- What did it actually do, and under whose authority? A chat log is not evidence.
- Is the output still as good as it was last month? A model change or a prompt change can lower quality silently.
This is the model I use to answer them. It runs across a twelve-repository estate.
Then I tested it by building this site through it, and five parts of it turned out to be broken. Those are in the second half, because a reference architecture that quietly fixes its own errors is the thing this document exists to argue against.
Authority is a predicate, not a policy document
Every artefact carries an autonomy_tier. The tier decides one thing: whether AI may merge
to the canonical corpus. It never restricts drafting.
| Tier | Meaning | May draft | May merge |
|---|---|---|---|
| 1 | Internal. Tooling, process, private surfaces | Yes | Yes |
| 2 | Operator-adjacent. Affects how the business runs | Yes | No |
| 3 | Regulated or customer-facing. Payout maths, RNG, KYC, AML | Yes | No |
A transition usually touches more than one artefact, so the effective tier is computed rather than read:
effectiveTier = max(source.tier, target.tier, domain.tier)
A missing tier is treated as Tier 3. That is deliberate rejection bias: a gap must stop the agent, never release it.
The gate itself is nine words.
export function mayAutoMerge(effectiveTier: number): boolean {
return effectiveTier === 1
}
Governance fails when it lives in prose that each new feature reinterprets. Here it lives in one predicate every transition calls, and a reviewer reads the whole rule in five seconds.
The boundary is the merge, not the commit
An agent may commit to a branch at any tier, including Tier 3. Corpus state changes only when authority permits. A draft is a proposal, not history.
That distinction is what makes the model usable. Agents get to do the work; humans keep the decisions that carry consequence.
Two records, and only one is history
Most agent systems keep a log. A log answers “what happened recently”. An auditor asks “what happened, in what order, on whose authority, and can you prove nothing was removed”. Those need different records.
| Audit ledger | Run state | |
|---|---|---|
| Role | Permanent system of record | Live, resumable progress |
| Shape | Append-only, one line per terminal outcome | Mutable, one file per run |
| Lifetime | Never pruned, never rewritten | Deleted once the run cannot resume |
| On conflict | Authoritative | Working copy |
They join on a run identifier, so a whole run is reconstructable from the permanent ledger alone and run state never has to be kept for history’s sake.
Evaluate the one step that can degrade unseen
A lifecycle transition runs as six hops: compose, draft, create, gate, merge, sync. Five are ordinary code. Exactly one calls a model.
That asymmetry is the design. Deterministic hops fail loudly. The drafting hop is the only place output can get quietly worse while every test still passes, so it is the only place that earns a regression gate.
The evaluator returns one of four verdicts, and two of them are neither pass nor fail:
| Verdict | Effect |
|---|---|
pass |
Both layers passed on a covered stage |
regression |
Withholds a Tier 1 merge |
uncovered |
No golden input exists. Reported honestly, never counted as a pass |
could-not-run |
The harness itself failed. Never disguised as a pass |
Most harnesses collapse “we checked and it was fine” with “we had nothing to check against”. Keeping them separate means an operator always knows which one they are looking at.
It escalates and never loosens. A regression can withhold a merge the tier permitted. No
verdict can release a merge the tier refused.
What building this site found
The claims above were true as designs. Here is what happened when I made them carry real work.
1. The gate that could never fail
The canonical state checkers ran in CI on every pull request. They never ran at all.
The step resolved a path from a repository variable that was never defined, so the command
became /runtime-adapters/... and exited MODULE_NOT_FOUND every single time. It carried
continue-on-error: true, so the job went green anyway.
Eight pull requests passed a gate that could not fail. I wrote the mask myself, so an unproven step would not block the first merge. That was the wrong trade.
Worse, I cited those checkers as evidence while they were inert. The results I quoted were real, because I ran them by hand locally. The gate was decoration.
On its first genuinely enforced run it caught a violation I had introduced in the same commit.
2. Attribution did not exist
The whole model rests on being able to say who did what. It could not.
PR#8 merged_by=anthony <- the operator merged this
PR#9 merged_by=anthony <- the agent merged this
Agent and operator shared one identity. And the fallback signal was already gone, because squash merging regenerates the co-authorship trailer from the commit author:
46d2002 (branch) Co-Authored-By: Claude Opus 5
fdc49c2 (squash) Co-Authored-By: Anthony Lewis
I chose squash. Every squash merge erased the agent from the main branch.
The fix was an account of its own for the agent, and disabling squash at the repository level rather than agreeing not to use it. Nine pull requests predate that split and are permanently ambiguous. That history cannot be reconstructed and is not presented as though it could be.
3. A status code is not evidence
The deployment smoke test asserted HTTP 200 from the site’s apex domain. The apex returned 200, serving a Plesk default page dated 2014 from a hosting account I had forgotten about, because DNS still pointed there.
A liveness check would have reported that as a successful production deploy.
The test now stamps the deploying commit into the built bundle and asserts every bound hostname serves that exact commit. Four lines of shell. It caught the placeholder on the run that mattered.
4. Genesis had no gate at all
A repository was created for a product that did not exist. No brief, no requirement, no spec. It reached production two days later and nothing objected.
Neither checker was at fault. Both validate a repository’s internal coherence, and nothing in canonical state linked a repository to the product it serves.
The operator caught it, not a gate. That distinction matters more than the defect: the other failures here were the system working, and this one was the system being bypassed.
5. A reversal, and a correction to this document
A decision was reversed before any of its code was built, which is the cheapest moment to
change your mind. The atom work-atom:deliver-container-image-and-tunnel-deployment was
refined in place: new title, new purpose, new acceptance checks. It delivered real work and
is now done. Its id still says “container” and “tunnel”. It delivers neither.
When this was first published I called that “the model cannot express a reversal” and blamed the status vocabulary. That was wrong, and the error stays visible here because it is exactly the kind this document exists to catch. There are two gaps, not one.
Identity. An atom still wanted, but refined until its id misleads. No status value fixes that. It needs the id to migrate across the atom file, the index, the work graph, every dependency edge and any verification report naming it, atomically.
Terminal state. An atom that will never be built. The status values are proposed,
ready, active, done and blocked, so the only moves are to leave it proposed forever,
which is a lie because nothing has declared it next, or mark it done, which is a worse one.
The case that occurred was the first. Adding a cancelled status would not have helped.
The identity gap closed on 12 September, three days after this was published. The persistence
primitive gained a migrate-id verb. Building it surfaced something else first, which is the
better story: an edge naming an atom that did not exist passed every checker in the
repository. Edge resolution had to ship before the migration could be proven atomic, because
the gate could not see the surface a partial migration was most likely to break.
The terminal-state gap is still open, and deliberately. No atom in this estate has yet been abandoned outright, and adding vocabulary for a case that has not happened is its own debt.
Where it still falls short
- The checkers test resolvability, not agreement. Since 12 September an edge naming an atom that does not exist is caught. But where a work graph carries its own copy of a dependency list, nothing compares that copy against the atom’s own, so the two can disagree in silence. I injected exactly that drift after the fix landed and it still passed.
- Nine pull requests are permanently unattributable. See above.
- The terminal-state gap has no fix. See above, and that is a deliberate choice rather than an oversight.
- The evidence is uneven. The lifecycle ledger covers product transitions. Ordinary code changes in a delivery repository are attributable through the forge record and nothing else. Those are different levels of rigour and should not be presented as one.
Provenance
Every mechanism here is running code. The tier predicate, the ledger, the span schema, the evaluation verdicts and the checker codes are quoted from a private twelve-repository estate that has run under them since June 2026. The estate spans a regulated gambling product under UK Gambling Commission technical standards, a delivery orchestrator, a governance engine and a cross-language engineering standards corpus.
This site is the first thing built through that model in public. Thirteen gated pull requests, nineteen design tests, and a published record of every change that made it.
The five failures above were found in eight days. That rate is not a boast. It is the argument: a model only earns trust by being run against real work and reporting what broke.