feat!: docs/ sorted into tutorials, how-to, reference, explanation plus unshipped docs/dev/; explanation pages and developer guide moved, human docs English (#187)
The seven explanation pages move to docs/explanation/ without a stub, DEVELOPMENT.md becomes docs/dev/development.md (translated to English), dist export leaves docs/dev/ out, docs verify rejects a file outside the five areas, and AGENTS.md states that the stack's human documentation is English. Required migration 9.0.0-docs-diataxis rewrites the old path in an instance's own kb/CONVENTIONS.md. Files changed: - .wikitool-kb.json - AGENTS.md - CHANGES.md - CLAUDE.md - DEVELOPMENT.md - INSTALL.md - README.md - VERSION - docs/dev/development.md - docs/explanation/README.md - docs/explanation/knowledge-and-commitment.md - docs/explanation/language-boundaries.md - docs/explanation/model-and-effort-selection.md - docs/explanation/ownership-and-templates.md - docs/explanation/pipeline-rationale.md - docs/explanation/version-model.md - docs/explanation/why-gates-are-code.md - docs/how-to/README.md - docs/knowledge-and-commitment.md - docs/language-boundaries.md - docs/model-and-effort-selection.md - docs/ownership-and-templates.md - docs/pipeline-rationale.md - docs/reference/README.md - docs/tutorials/README.md - docs/version-model.md - docs/why-gates-are-code.md - instructions/CONTRACT.md - instructions/dev/dev-setup.md - instructions/dev/doc-pull-through.md - instructions/dev/stack-close/SKILL.md - instructions/dev/stack-mode.md - instructions/gtd-weekly-review/SKILL.md - instructions/ingest-queue.md - instructions/migrations/6.0.0-type-guidance-split.md - instructions/migrations/9.0.0-docs-diataxis.md - instructions/upgrade-instance.md - kb/CONVENTIONS.md - kb/CONVENTIONS.md.template - tools/CONTRACT.md - tools/chemenu/commands/dist_cmd.py - tools/chemenu/commands/docs_verify.py - tools/chemenu/commands/new_page.py - tools/chemenu/commands/task_cmd.py - tools/chemenu/tasks/protocol.py - tools/chemenu/tests/test_dist_cmd.py - tools/chemenu/tests/test_docs_verify.py - tools/chemenu/tests/test_git_publish.py - tools/chemenu/tests/test_toc.py - types/type-guidance.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
1 parent
0e15972ee7
commit
d66a3f65f4
43 files changed
+756
-324
No files matched your search
@@ -0,0 +1,149 @@
|
||||
# Why gates are code
|
||||
|
||||
Chemenu has five hard limits - the Mass-Update Gate, the Publish-Remote Gate, the Upload Review
|
||||
Gate, the Guideline Push Gate, and the Iteration Budget Gate - and all five 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 mechanisms in particular.
|
||||
|
||||
<!-- wikitool:toc -->
|
||||
## Contents
|
||||
|
||||
- [A suggestion an agent can talk itself past](#a-suggestion-an-agent-can-talk-itself-past)
|
||||
- [Why different mechanisms, not one](#why-different-mechanisms-not-one)
|
||||
- [Exit 42 is a posture, and it outgrew the gates](#exit-42-is-a-posture-and-it-outgrew-the-gates)
|
||||
- [A gate in code still has to be reachable](#a-gate-in-code-still-has-to-be-reachable)
|
||||
- [Numbers that come from measurement, not intuition](#numbers-that-come-from-measurement-not-intuition)
|
||||
<!-- /wikitool:toc -->
|
||||
|
||||
## 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 different mechanisms, not one
|
||||
|
||||
The five gates ask four different kinds of question, 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 Guideline Push Gate asks the same question once more, at the boundary facing outwards: *is
|
||||
this content right for these repositories, now?* `export guidelines --push` writes a generated
|
||||
file straight onto another repository's branch, with no review on the receiving side, and a
|
||||
public target publishes it the moment it lands. That is a per-run judgment - the guidelines
|
||||
change, the set of opted-in repositories changes, a branch moves - so it takes the token shape,
|
||||
digesting exactly what would be written where. It deliberately does not take the Publish-Remote
|
||||
Gate's shape, although it too pushes to a remote: which repositories are targets is already a
|
||||
standing, committed declaration - the capture manifests under `raw/` - and asking for the same
|
||||
URLs in an allowlist as well would be a second declaration of one fact. What is left to ask is
|
||||
the per-run question, and that is the one a token answers.
|
||||
|
||||
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.
|
||||
|
||||
## Exit 42 is a posture, and it outgrew the gates
|
||||
|
||||
Those five are the named gates, and they are not the only thing that exits 42 any more. When the
|
||||
task-tracker provider layer arrived, it brought a case that looks like a gate from the outside and
|
||||
is not one: a provider whose API cannot create a project (Super Productivity's local REST API
|
||||
reads projects but does not write them) raises `HumanInterventionRequired`, and the command prints
|
||||
what a human has to do and exits 42.
|
||||
|
||||
Reusing the code was deliberate, and so was not calling it another gate. What the named gates share
|
||||
is a *refusal*: the operation was possible and the tool declined to perform it unreviewed. This is
|
||||
the opposite situation - the operation is not possible at all, and no token could make it
|
||||
possible. What the two have in common is only what the exit code actually communicates: **stop,
|
||||
show this to a human, do not improvise a way around it.** That sentence is the whole meaning of
|
||||
42 here, and it is worth more as a shared convention than as a number reserved for one mechanism.
|
||||
|
||||
The alternative was worse in a specific way. A provider that cannot do something could have been
|
||||
described in the instruction layer instead - "if you are on this tracker, create the project by
|
||||
hand first" - which is exactly the prose-shaped rule this page argues against, with the added cost
|
||||
that every instruction would then have to know which provider an instance runs. The capability
|
||||
gap belongs where the capability is, and reaches the session as an exit code rather than as a
|
||||
paragraph it has to remember to apply.
|
||||
|
||||
The preflight is the second such case, and the one closest to this page's own argument. A machine
|
||||
without Python or ripgrep is not something the tool can fix, and an install that met that gap
|
||||
with only a setup instruction to go on showed what follows: the agent worked around each missing
|
||||
piece - another environment, a hand-made configuration - and kept going. So `tools/preflight.sh` (and its PowerShell twin, `tools/preflight.ps1`)
|
||||
exits 42 with the command a human has to run, and the launcher in front of every `wikitool` call
|
||||
exits 42 until the preflight has passed. "Check first, stop, let the user act" lives in two places
|
||||
a session cannot read past, not in a step it can skip.
|
||||
|
||||
## 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 ones 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.
|
||||
Reference in new issue
Block a user