# Why gates are code Chemenu has four hard limits - the Mass-Update Gate, the Publish-Remote Gate, the Upload Review Gate, and the Iteration Budget Gate - and all four live inside `tools/wikitool`, not in a paragraph of instructions an agent reads and follows. The rules themselves, and what to do when one trips, are in [AGENTS.md ยง Gates](../AGENTS.md#gates) and [instructions/gates.md](../instructions/gates.md). This page is only about the design choice underneath them: why code, and why these four mechanisms in particular. ## A suggestion an agent can talk itself past An instruction like "don't publish too much at once" or "don't loop forever" lives in the same place as every other piece of guidance a session is holding - alongside the task, the user's last message, and whatever context made the moment feel urgent. Under pressure, or with a plausible-sounding reason ("this batch is different, it's mechanical"), that guidance can be reasoned around without anyone deciding to break a rule. Nothing enforces it; it just competes for attention with everything else in the context window, and sometimes loses. A check compiled into the tool doesn't have that problem, because it isn't part of the conversation at all. It runs before the command dispatches, regardless of how convincing the case for skipping it seemed a moment earlier. The difference isn't that code is smarter than a well-written instruction - it's that code doesn't get talked into anything. ## Why four different mechanisms, not one The four gates ask four different questions, and each one's shape follows from what kind of question it is. The Mass-Update Gate asks *is this change too large to publish unreviewed* - a judgment that varies changeset by changeset, so it clears with a `--confirm` token tied to the specific output the user just read. Approval is scoped to that one publish. The Publish-Remote Gate asks something underneath that: *is this even the right repository*. That's not a per-push judgment, it's a standing property of the checkout - true or false for every publish that checkout will ever attempt, not just this one. A confirm token would let an agent clear it once and then treat the answer as settled, which is exactly backwards for a question whose answer shouldn't move at all mid-session. The only way past it is the user editing `.wikitool-remotes.json` directly, outside the gate's own flow. The Upload Review Gate asks the Mass-Update Gate's own question - *is this change right?* - at the opposite end of its size range: one file from a stranger instead of a changeset from the session's own work. That similarity is exactly why it reuses the same shape (a `--confirm` token digesting the thing being approved) rather than inventing a fourth one: the two gates differ in *who* produced the change and *how much* of it there is, not in what kind of question either one is answering, so nothing about the mechanism needed to change - only the boundary it sits behind did, since the submission lives in a quarantine the ordinary pipeline never reads at all rather than in the working tree `publish` is about to commit. The Iteration Budget Gate asks a fourth kind of question - not "is this instance correct" but "has this session stopped making progress." That's read from the shape of the call history itself (call count, repeated identical calls), not from anything about the content of any one call. ## A gate in code still has to be reachable Code beats prose for the reason above, but on its own it buys less than it looks like: a check that runs on every call is only as good as the thing it counts under. The Iteration Budget Gate scopes its counter to a session, and "session" was approximated by the parent process id whenever nothing set an explicit one. On a harness that runs every tool call in a freshly initialised shell, that approximation hands out a new session per call - so a traced run of thirty-three calls arrived as twenty-one sessions of one to three calls each, the ceiling of sixty was never approached, and the loop-breaker's window never held three calls at once to compare. The gate ran on every one of those calls, exactly as written, and refused nothing. That failure has no symptom of its own. A gate that fires announces that it exists; a gate that *cannot* fire looks identical to a gate nobody happened to need - the same clean runs, the same silence - and what finally told the two apart was reading a trace for an unrelated reason. So there is a third property to keep alongside living in code and carrying measured numbers: each gate has to leave evidence that it can still fire. The three that clear by token or by a deliberate edit have it by construction, because clearing one is a visible event in somebody's terminal. The budget gate, whose ordinary outcome is silence, is the one that had to be given it - which is why its session id now carries where it came from, into both the trace and `budget status`, so a session's own record answers the question instead of an investigation having to. ## Numbers that come from measurement, not intuition The iteration ceiling didn't start where it sits now. It used to run 15-25, borrowed from a general rule of thumb, until four real ingest runs measured 24, 26, 29 and 30 calls apiece - every one of them an ordinary workflow doing nothing wrong, and every one of them at or past where the old ceiling would have refused it. A limit that the normal case keeps tripping stops functioning as a limit; it becomes background noise a session learns to route `--override-budget` around as a matter of course, and the whole point of a hard-coded check is that it isn't supposed to feel routine. That's the deeper reason these numbers live in a tool rather than in prose: prose is read once and remembered loosely, but a threshold enforced every call is tested by every call, and a threshold that fails its own test gets noticed and re-measured rather than quietly ignored. The suite's coverage floor is the same argument run forwards instead of backwards. The ceiling above was wrong first and measured afterwards; the floor was withheld on purpose until the number existed - measured, then watched across 38 runs while the code grew by a quarter, and only then written down as 85 against an observed 87.0%. The two points of daylight are the same consideration as the ceiling's headroom: a limit the ordinary case keeps tripping stops being a limit. A coverage floor set at the measured number goes red on the next thin command wrapper, which is not a regression, and a threshold that goes red for a non-reason gets lowered rather than earned - the failure mode above, reached from the other direction.