# Agent Operating Practices — how the Magogi Foundation works

> **Audience:** AI agents and their operators, anywhere. This is written to be
> **lifted**. Nothing here depends on our tooling; every practice is stated as a
> principle, then as the concrete mechanism we use, then as the incident that
> taught it.
> **Companion documents:** [`docs/standards/AGENTS.md`](standards/AGENTS.md) (the
> normative AG-* rules), [`LESSON_LEARNED.md`](../LESSON_LEARNED.md) (the dated
> incident log this distils).

Every rule below was paid for. Where a rule has a cost attached, the cost is
quoted — because a practice without its incident is just an opinion, and opinions
get argued away at 2am by a tired agent in a hurry.

---

## 0. The one-paragraph version

A federation has **layers** — what you *declared*, what you *committed*, what you
*published*, what is *installed*, what is *live*. Almost every defect that keeps
recurring is an answer taken from one layer and believed about another. So: **every
answer names its layer**, **"done" is defined at the layer that bites**, and **a
check that isn't in a gate's exit code does not exist**. Everything else is detail.

---

## 1. The layer model

```
declared  →  committed  →  published  →  installed  →  live
registry     origin/main    the index    what you      CI, hosts,
SECRETS.toml                             actually run  the forge
pyproject
```

**Every recurring defect we have had is a layer confusion.** A sample, all real:

| What was checked | What was claimed | Cost |
|---|---|---|
| the credential store (committed) | the live surface serves it | a rotation revoked the old credential while CI still used it |
| name + event scope (live metadata) | "declared == deployed" | a stale secret value; one session to trace an opaque error |
| a declared CI knob | the rendered pipeline | three red runs; the knob never existed |
| the checkout | the published corpus | regenerated against the wrong corpus, twice, in both directions |
| `main` has the fix | the fix is shipped | the operator's installed tool still had the bug |
| a branch was pushed | the work was delivered | two PRs never opened; work invisible |

### 1.1 Rules

**R1 — Every answer carries its layer.** A tool must state which layer it observed.
`version 0.17.0` is a defective report; `committed 0.18.0 · published 0.17.0 ·
installed 0.17.0` is a useful one. Cheapest possible implementation: one type,
`Observed(layer, value)`, and a renderer that prints the layer.

**R2 — "Done" is defined at the layer that bites.**

| thing | done when |
|---|---|
| a credential is rotated | the **live** surface serves the new value |
| a fix is shipped | the **installed** artifact has it |
| a library provides X | the **published** wheel exports it |
| work is delivered | it is **merged** |

**R3 — Never conclude "it does not exist" from a filesystem search.** Ask the
system of record. We once reported *"no API token exists on this machine"* after
grepping a directory; the token was declared, encrypted, retrievable by a
documented path, and its own metadata named agent sessions as its consumer.
Grepping answered *where I looked*, not *what exists*.

Corollaries: **"the tool errored" is not "the capability is absent"** — fix the
tool, then ask it. And **a boundary found is worth reporting precisely**: "the
token is scoped below repo creation" is useful; "nothing exists" is not.

---

## 2. Verification discipline

**V1 — A verifier that isn't in a gate's exit code does not exist.**

We measured this: **five hand-written verifiers, referenced by zero pipelines.**
Each was written after a real incident, ran once during the review that produced
it, and never again. One of them documented its failure class perfectly — and that
class recurred *the same day we re-read it*, because nothing ran it.

The knowledge was captured. The enforcement was not. **Filing a lesson is not
fixing a problem.**

*Mechanism:* a new check becomes a subcommand with an exit code, or it does not get
written. Ours is `mgf-fed audit`.

**V2 — A skip is not a pass.** "I could not verify" must never share an exit code
with "I verified." That equivalence let a dead webhook look like a healthy repo for
three weeks. Our audit exits non-zero when a check *could not run*, and its output
always names what did not run.

**V3 — State the scope of what you checked, in the success line.** Every one of
these sentences was *true* and read as something broader:

- *"the registry covers every discoverable web console"* — it swept only web consoles
- *"all current"* — it looped only over web consoles
- *"declared == deployed"* — it compared only metadata

The bug is never in the fact. It is in the gap between the scope checked and the
scope implied. Widening the query fixes one instance; **making the success line
name what it actually covered fixes the class.**

**V4 — Run the control.** Before trusting a check, run the case that should FAIL.
We wrote a credential probe, then ran it with a deliberately wrong password: it
returned success. The endpoint accepted any password. We had just written the rule
and it still took the control to catch it.

**V5 — Never pipe a gate you are testing.** `cmd | tail` reports *tail's* exit
code. We read a gate as passing when it was failing; and later suppressed a `git
push` failure with `| tail -0` and printed our own "pushed" line. Twice in one
session. **A wrapper's status is never the wrapped command's status.**

**V6 — Verify the write's blast radius against your claim.** An unanchored `sed`
edited three records while the commit message said one. Caught only because
`--stat` disagreed with the sentence. Read the diffstat; if it does not match what
you said you did, you are wrong about what you did.

**V7 — Over-reporting is a correctness bug in a gate, not a safe default.** A gate
that cries wolf 26 times is how the two real findings get ignored. Tune the
exclusions until the signal is real.

**V8 — An exclusion list is not write-once; it decays.** Our release gate's tuned
exclusion list was perfect when written. A later fleet-wide file addition made it
report 32 of 33 projects as stale — 25 of them for one unshipped governance file
that postdated the list. **The decay reads as noise long before anyone calls it a
bug.** Re-derive exclusions when the repo shape changes.

**V9 — A green subset is not a green suite.** Running `pytest tests/unit` and
reporting "tests pass" hid an untested new module. The remedy is to run what CI
runs, and to write the tests — never to add an allowlist entry.

**V10 — Don't inherit conclusions.** Verify directly, including conclusions from
other agents and from your own earlier turns. Roughly a dozen times in one session,
a confident report was falsified by a single measurement.

---

## 3. Testing discipline

**T1 — A fixture that starts empty tests the branch that never runs in production.**

Our credential tool's `rotate` never rotated a deployed secret's value. On the
*update* branch — reached whenever the secret already exists, i.e. **every real
rotation** — it sent the metadata and omitted the value. Six stages reported OK,
the cutover announced "surface serves new", and the final stage revoked the old
credential. Net effect: CI authenticating with a value that had just been revoked.

It survived a full per-kind drill suite — the "every kind, end to end" proof —
because **every drill started from an empty fake surface**, so all of them took the
*create* branch, where the value *is* sent.

> The suite was thorough along the axis it enumerated (kinds) and blind along the
> one it never varied (**pre-existing state**).

*Rules:* ask of any fake — *what state does this start in, and is that the state
production is in?* Empty is the easiest fixture to write and usually the least
realistic. Any create-or-update branch must be drilled **twice**. And **delete the
default that makes the unrealistic case free**: if `Fake(items={})` defaults to
empty, the author never chooses.

**T2 — Promoting a report to a gate raises the precision bar.** A noisy row in a
report is tolerable; a noisy row in a gate is how real findings get ignored. When
we turned a census into a check, it immediately false-positived on a *cohort*
heading. The fix had to be narrow — the identifier must be the **start** of a range
— because a looser rule would have traded a false positive for a false negative,
which in a gate is the worse of the two.

**T3 — Write the test that encodes the incident, not the symptom.** Name it after
what went wrong. A test called
`test_rotating_an_existing_secret_actually_pushes_the_new_value` survives
refactoring in a way that `test_rotate_2` does not.

**T4 — When a test you wrote fails and the *code* is right, keep the test
inverted.** We asserted that a re-scope operation should record a deployment. It
failed — correctly, because a re-scope pushes no value, so recording one would
claim something nothing established. We kept the test with the assertion reversed,
so the lie cannot be "fixed" back in.

---

## 4. Declaration discipline

**D1 — Close every schema.** A configuration key nothing reads is a bug, always.
One of our repos declared a CI knob that had never existed, written by analogy with
a real one. It parsed, printed a note to stderr in the middle of a 46-repo sweep,
let the tool report success and **exit 0**, and rendered a pipeline missing what the
knob was supposed to add.

*Rule:* unknown key → **finding**, not note. `extra="forbid"` everywhere a
declaration is parsed. One schema per surface replaces N bespoke unknown-key checks
— strictly **less** code.

**D2 — Route a diagnostic to the plane that acts on it.** A warning that only ever
reaches stderr is a warning nobody receives. **Exit code is what CI and humans both
actually read.** If a condition should change someone's behaviour, it belongs in a
gate's findings and its exit code.

**D3 — Name the consequence, not the observation.** *"key not read by the
renderer"* reads like bookkeeping. *"the rendered pipeline is NOT shaped by them"*
makes someone act. List the valid options in the error so the fix needs no source
dive.

**D4 — Did-you-mean needs two tables, not one.** Edit distance catches typos. It
never catches the **wrong concept** — `qt` is not a misspelling of `apt_packages`.
Keep an explicit alias table, and add the entry the **first** time a knob is
invented, not the second. Keep a **tombstone** table too, so a deleted knob gets a
reason rather than a suggestion.

**D5 — Generated files are regenerated, never hand-edited.** Hand-editing makes a
generated file agree with *you* instead of with the code. Regenerating our API
manifest during a release surfaced that one repo's had been stale for two releases
— a hand-edit would have silently papered over it.

---

## 5. Change and propagation

**P1 — One change at the centre means N repos must absorb it, and nothing tracks
absorption unless you make it.** A single breaking change in our cornerstone
library left 43 repos silently drifted. It presented as 43 unrelated small problems
and was one unrippled change.

> **Small drift is more dangerous than large change**, because each instance is
> below the threshold where anyone builds a mechanism, so they accumulate.

**P2 — A corpus release owes two waves**, not one: the release itself, then the
propagation to consumers. A release recorded as complete that reached no consumer
is not complete.

**P3 — Verify which artifact is loaded before regenerating from it.** We
regenerated projections against a working tree *ahead* of the release, then against
environments *behind* it. The fix was to reproduce the CI invocation exactly rather
than approximate it locally.

**P4 — A version lives in more places than you think.** Ours lives in five:
project metadata, the runtime `__version__`, the generated API manifest, the
lockfile's self-entry, and the changelog. A bump that changed one was caught by
three *different* gates, minutes apart. Automate the bump and make it **assert its
own completeness**.

**P5 — Pace waves to the narrowest real limit, and know which limit you hit.**
Our forge has two: a *title-similarity* anti-spam rule (fix: **vary the titles**;
backing off does nothing) and a genuine *volume cap* (fix: **keep the title**, wait
out the window). Opposite remedies. Applying the wrong one turns a minutes-long
wave into an overnight one. Separately, pace to your **CI capacity**, not just the
API's — 40 pipelines on a 2-agent estate produces a backlog and contention failures.

---

## 6. Reporting and self-discipline

**S1 — Report outcomes faithfully.** If tests fail, say so with the output. If a
step was skipped, say that. When something is done and verified, say it plainly
without hedging.

**S2 — A push is not a PR.** A green branch nobody opened is invisible. We did this
twice in one session.

**S3 — Correct plainly and move on.** State the correction, continue the task. No
apologising, no ruminating, no tallying past errors. The correction is the work;
the performance around it is not.

**S4 — Distinguish "I could not" from "it is not."** This is R3 and V2 restated as
a habit of speech, and it is the single highest-leverage thing an agent can fix
about its own reporting.

**S5 — Push back.** An agent that only complies is a slower keyboard. Challenge the
premise, ask for what is missing, give a real opinion — and when the operator
reaffirms, execute fully.

---

## 7. Safety and credentials

**C1 — Never read a secret value into the transcript.** Use paths and shell
substitution; pass values through process substitution (`--token-file <(printf
'%s' "$VAR")`) so nothing lands on disk or in argv. Listing *names* is fine.

**C2 — Prefer a credential broker to files.** Ours is `mgf-secrets exec --with
<id>`, which injects into a child process. The registry entry names its own
consumers, so "how do I use this?" has a documented answer instead of a folklore
one.

**C3 — Anything destructive or irreversible is proposed, then fired together.**
History rewrites, force-pushes, data wipes. In one session `git reset --hard`
discarded a commit that a correct `--ff-only` refusal had just protected; recovered
from reflog. Later the same command was **blocked** and the job was done with `git
branch -f`, which touches no working tree.

**C4 — A held path is held.** When something is deliberately not armed, the gate
must **invert**: the credential being unavailable is a PASS, and *arming* it is the
failure. Otherwise the hold reports as N red findings forever, and a gate everyone
learns to ignore is not a gate.

**C5 — Least privilege is a design, not a bug.** If a component asks for a
credential outside its allowed scope, **fix the component** — never widen the
credential. Encode that so the tool can never recommend the loosening.

---

## 8. The mechanisms we run

Portable shapes, not product pitches.

**The cross-layer audit.** One command, N checks, each declaring the layer boundary
it crosses. Exit 1 on findings *or* on a check that could not run.

**The expensive/cheap split.** A fleet audit needs every repo plus credentials, so
it cannot run inside a single-repo CI clone. So: the **expensive half** runs where
the fleet is and writes a committed artifact; the **cheap half** rides ordinary CI
and asks four questions the artifact answers about itself — *stamped, fresh,
complete, clean*. An artifact from a partial run records what did not run and
therefore **cannot** satisfy the gate.

**Freshness gates on committed artifacts.** We once found a committed audit result
still asserting a 25-day-old conclusion, reading as current, because nothing was
ever going to look. **A checker nobody runs silently becomes a liar.**

**Generated CI.** Pipelines are generated from a template and byte-compared;
divergence is drift. This makes a fleet-wide CI change one edit instead of N.

**A durable memory that is maintained.** An index, a working agreement, a decision
log, and one file per session. Update it *when facts change*, not in a batch at the
end — a crash should never cost knowledge. And **memory that isn't maintained is
memory that lies**: when something turns out wrong, fix it and record the
correction.

**Fix first, then wire.** Never introduce a gate that is red on arrival. **A gate
introduced red is how a team learns to disable gates.** Clear the findings, then
turn it on.

---

## 9. Adoption checklist

Roughly in value order. Items 1–4 are cheap and pay immediately.

1. **Name your layers.** Write down what "declared / committed / published /
   installed / live" mean in your system. Most teams have four or five and have
   never named them.
2. **Audit your `scripts/` directory.** For each checker, ask: *what runs this?*
   Anything with the answer "a human, once" is not enforcement. Fold it into a
   command with an exit code, or delete it.
3. **Make skips fail.** Find every check that can silently not-run. Make "could not
   verify" exit non-zero.
4. **Fix your success lines.** Make every clean verdict state the scope it actually
   covered.
5. **Close your config schemas.** Unknown key → finding.
6. **Re-read your fakes.** For every create-or-update path, is the initial state
   realistic? Drill both branches.
7. **Automate version bumps** with a self-completeness assertion.
8. **Add a freshness gate** to any committed artifact that a human generates.
9. **Write the incident into the test name** and the rationale into the code
   comment. Future-you deletes an unexplained guard; nobody deletes one that says
   what it cost.

---

## 10. The shortest version

> The fixes are mostly routine. **The corrections are the work.**
>
> What catches a wrong answer is never care. It is running the control.
