32a9b8eb3f
Files changed: - CHANGES.md - VERSION - instructions/gates.md - kb/entities/INDEX.md - kb/entities/projects/Chemenu.md - kb/log.md - tools/CONTRACT.md
162 lines
9.3 KiB
Markdown
162 lines
9.3 KiB
Markdown
---
|
|
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. |
|
|
|
|
## 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*. Three 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), and the
|
|
Publish-Remote Gate (`publish`, on a push to a target this checkout has not declared) - 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 <token>` 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 <dir>` (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 two 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.
|
|
|
|
## 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.
|
|
|
|
`wikitool search` is exempt from this budget entirely: retrieval is reading, not iterating.
|
|
|
|
### 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).
|