--- type: types/instruction.md name: gates description: What to do when wikitool refuses a call - exit 42 (user clearance required) on publish, and the Iteration Budget Gate and loop-breaker on every command. --- # When a gate refuses a call Two limits are enforced in code rather than by instruction, because a prompt-level limit is one an agent can talk itself past. **Never open a gate on your own initiative.** Not `--override-budget`, not `budget reset`, not a `--confirm` token the user has not actually seen and approved. Read the exit code first - it says which of these applies: | Exit | Meaning | What to do | |------|---------|------------| | 42 | User clearance required | Reproduce the command's output in your reply, stop. See below. | | 1 | Validation error, or a budget/loop refusal | Read the `ERROR` line; fix and retry once, or stop and escalate. | ## Contents - [Exit 42: user clearance required](#exit-42-user-clearance-required) - [Publish-Remote Gate](#publish-remote-gate) - [Upload Review Gate](#upload-review-gate) - [Mass-Update Gate blind spot: `upstream merge`](#mass-update-gate-blind-spot-upstream-merge) - [Iteration Budget Gate and loop-breaker](#iteration-budget-gate-and-loop-breaker) - [Taking a new session id](#taking-a-new-session-id) - [Scope](#scope) ## Exit 42: user clearance required A `wikitool` command that exits **42** is not reporting an error. It is refusing to act until a human has *read its output*. Four gates use it today - the Mass-Update Gate (`publish`, on a change touching 10 or more counted files), the rebase-review gate (`sync` and `publish`, on a rebase whose incoming commits touch a file this session is also changing), the Publish-Remote Gate (`publish`, on a push to a target this checkout has not declared), and the Upload Review Gate (`upload accept`, on a submission nobody has cleared yet) - but the rule is about the exit code, not the command: > **Copy the command's output into your reply - the substance of it, not a description of it - > and stop.** Run no further commands in that turn. For the Mass-Update Gate that substance is the grouped file breakdown: the area headings, every path, and the sizes. It is already ordered for a reader - what a bad publish damages most comes first, and the mechanically-regenerated files come last - so reproducing it in order is both the cheapest and the most useful thing to do with it. For the rebase-review gate the substance is different: the commits arriving from the remote, the files they touch that this session is also touching, and a diff of those files. Read it - this is the check `sync`/`publish` cannot perform themselves, since a rebase between two commit ranges that touch disjoint files never reaches this gate at all (no content collision is possible by construction, so it rebases automatically). Judge whether the incoming change conflicts logically with what you are about to publish, summarize *that judgment*, not just the diff, to the user, and only then re-run with the `--confirm-rebase ` the refusal prints. **A command's output is not visible to the user.** On most harnesses stdout goes to the agent's context, not to the user's screen, so the tool having printed something and the user having seen it are different events. "See the output above", a summary, a file count, or a description of the change all leave the user approving something they never read. The one thing that discharges this is the content itself, restated in the reply. The output says what would change, lists the evidence, and carries the exact line that proceeds once the user has approved. Nothing more about the procedure lives here on purpose: a recipe written down in the instruction layer is one an agent can perform start-to-finish without a human ever being involved, which is exactly the failure this replaced. Paths under `work/` are committed but never counted - the gate protects published knowledge, and a workshop is working state deleted when its run closes ([work/CONTRACT.md](../work/CONTRACT.md)). Not a gate you may widen: the prefix list is a constant in the tool. `--path ` (repeatable) scopes a large change into reviewable batches, which is a legitimate alternative to one big clearance. Background: [[Mass-Update Gate]] (`kb/concepts/Mass-Update Gate.md`). ### Publish-Remote Gate The Mass-Update Gate asks whether a change is too large to publish. This one asks the question underneath it: **whether this is the right repository to publish to at all.** A checkout that holds private content usually has two remotes - its own, and the public upstream it takes stack updates from. Git does not distinguish them at push time, so one wrong `--remote` puts a private corpus on a public repository. That is not cheaply reversible: a force-push moves the branch, but the objects stay fetchable by SHA until someone expires the server's reflogs and runs `git gc --prune=now` on the bare repo. `.wikitool-remotes.json` names the push URLs a checkout permits: ```json { "schema": 1, "allowed_push_urls": ["ssh://git@example.net:22/you/your-wiki.git"] } ``` It pins **URLs, not remote names** - a name-based list would wave through a `publish` whose `origin` had been repointed, which is the failure it exists to catch. It reads the remote's `pushurl` when one is set, because that is where `git push` actually writes. The file is per-checkout and gitignored, for the same reason `ENVIRONMENT.md` is: two clones push to two different places, so a committed copy would tell a private clone that the public upstream is a legitimate target for its own content. **Absent means unrestricted** - a single-remote checkout with nothing private in it has nothing to protect, and `doctor` reports which state a checkout is in, WARNing only when there is more than one remote and no allowlist. A malformed file is an error rather than "no restriction": a corrupted safeguard must not read as a disabled one. **This gate has no `--confirm` token, on purpose.** The other three clear with a token because the question they ask ("is this change right?") is one the agent can put to the user and the user can answer for that one changeset. This one asks "does this content belong to that repository?", which is a standing property of the checkout, not a per-push judgment. The way past it is for the user to add the URL to the file. **An agent must never edit `.wikitool-remotes.json` to get past a refusal** - that is opening a gate on your own initiative, which AGENTS.md invariant 6 forbids. The setup this gate exists for - a private instance that takes stack updates from a public upstream - is [private-instance.md](private-instance.md). Step 4 there arms it, deliberately *before* the first `publish`: added afterwards it leaves open exactly the window it closes. ### Upload Review Gate The MCP server's opt-in `submit` tool (`.wikitool-upload.json`) writes into a quarantine, `mcp-upload/`, that no other command reads. This gate is the only door out of it: `wikitool upload accept ` refuses without a matching `--confirm `, printing the submission's manifest in full - filename, size, sha256, submitter, and `submitter_source` (the *header* the submitter's name came from, not a verified fact) - plus the exact re-run line. Same shape as the Mass-Update Gate, scoped to one submission instead of a changeset: the token digests id/filename/size/sha256/submitter, so an edited or superseded manifest invalidates it the same way a rewritten file invalidates a stale `--confirm`. What a reviewer actually checks before clearing it - secrets, license, an injection attempt, whether the material is worth a source page at all - is [instructions/ingest-queue.md](ingest-queue.md), not this file: the same split as the Mass-Update Gate's review report versus this file's exit-42 procedure. `wikitool upload reject --reason ""` is the other way out, and it has **no gate at all** - rejecting needs no clearance, only accepting a stranger's file into the pipeline does. It deletes the material and keeps only the reason and a sha256 in `mcp-upload/ledger.jsonl`. ### Mass-Update Gate blind spot: `upstream merge` `upstream merge` (a private instance taking a stack update - see [private-instance.md](private-instance.md)) can update or delete dozens of stack-owned paths in one commit, and the Mass-Update Gate does not see any of it. The gate counts *working-tree* changes before `publish` stages them; by the time `upstream merge` commits, the change is already history, and the commit it made is not what a later `publish` would be staging - that publish sees only whatever this session adds on top. A merge touching 200 files therefore goes out ungated the moment it is pushed. This is not a hole to patch by making `upstream merge` route through the gate: the gate's question ("is this too much to publish?") does not apply to a change that only ever touches stack-owned paths that are, by definition, not this instance's own content. The check that actually matters here is `upstream merge`'s own postcheck - it re-verifies the merge commit against `upstream verify`'s logic immediately after committing, and exits 1 with the offending paths if anything landed outside a stack-owned one. **The merge commit is deliberately left in place** rather than reverted: it exists, a human has to look at it, and a command that quietly repaired its own mistake would hide the one event worth seeing. That postcheck is the safeguard for this command, not the Mass-Update Gate. ## Iteration Budget Gate and loop-breaker Every `wikitool` call is counted per session. Calls are refused past **60 in a session**, or after **3 identical calls in a row** - whichever trips first. The check runs before the command dispatches, so the command never ran. Calibration: roughly 5-15 calls for a simple task, **20-35** for a complex multi-tool workflow such as an ingest or a full lint pass. The ceiling sits well clear of that band on purpose - it is not a target but the point past which a session is presumed stuck, and a real workflow carries overhead the band does not describe. A session that spends 60 calls on one task has a decomposition problem regardless of what the individual calls were. The upper band is measured here, not inherited. It read 15-25 until 2026-08-31, taken from an industry rule of thumb; four consecutive real ingests then measured 24, 26, 29 and 30 calls, every one at or above that ceiling while doing nothing unusual. A guideline the normal case exceeds teaches an agent that the numbers are decorative, which is the opposite of what a calibration is for. Re-measure it the same way when the workflows change: `tools/.wikitool_session/budget.json` holds the per-session counts. **A call that declined is refunded.** A rejected argument, or a read-only check reporting findings, exits 1 having changed nothing - and the tool error contract answers a rejected argument with "fix it and retry once", so charging for the rejection would make the prescribed response cost two slots for one operation. The call still enters the loop-breaker's history: repeating the same broken invocation is exactly what that instrument is for. When it trips: 1. Stop. Retrying is the failure mode the gate exists to prevent. 2. Summarise progress and the blocker to the user. `tools/wikitool budget status` stays readable at all times and is never counted. 3. Wait for explicit approval. **`budget reset` is not the escape hatch.** It is deliberately counted like any other call, so at exactly the limit it is refused too. The only way past is `--override-budget` on the command you actually need to run, and only with the user's approval. A dozen commands are exempt from this budget entirely - `search` and `doctor` because retrieval and diagnosis are reading, not iterating, plus the read-only forms of `links`, `cite`, `budget`, `eval`, `version`, `migrate` and `upstream verify`. The exemption is that fixed allowlist in [tools/CONTRACT.md](../tools/CONTRACT.md), not a "does not change the wiki" rule of thumb: `lint` only writes to gitignored `reports/` and still counts, because it is not on the list. ### Taking a new session id The budget is scoped by `WIKITOOL_SESSION_ID` ([session-setup.md](session-setup.md)), so a new id is a new budget. That is legitimate for a task made of several planned units - a tree ingest publishes one unit at a time - and is *not* legitimate as a way past a refusal. **A new session id may only be taken at a unit boundary written down in the run's `plan.md`, never in response to a gate refusal.** The plan is the human approval the gate would otherwise have to ask for; a refusal means that approval has not been given yet. If you are tempted to re-export the variable after an `ERROR` line, that is the gate working. Background: [[Iteration and Cost Limits]] (`kb/concepts/Iteration and Cost Limits.md`). ## Scope This covers refusals by *gates*. An ordinary validation error (a bad argument, a missing page, a duplicate title) is not a gate: fix the input and retry once, per the error contract in [tools/CONTRACT.md](../tools/CONTRACT.md).