Skip to content
Back to skills

Genesis Development

CSecurity

This skill should be used when developing, debugging, refactoring, or building Genesis itself — tasks like "fix this in Genesis", "add a new MCP tool", "wire up the runtime", "Genesis won't start", "create a worktree", "debug the bridge", or "add a capability". Applies to any task modifying files under src/, .claude/, or tests/. Do NOT load for Genesis-as-tool work ("summarize this", "write a LinkedIn post", "research X") or general questions unrelated to Genesis internals.

  • 96 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 3, 2026
ai-agentspythonrustgoshellbashsqlreactnodeexpresstesting

Works with

  • claude code
  • cursor
  • terminal
  • cli
  • api
  • mcp

Security analysis

C60/100
  • highPerforms destructive filesystem operations
  • criticalModifies startup scripts or system services for persistence

Pro scans all 9 files and shows the line behind each finding

Scanned October 6, 2026

npx -y skills add WingedGuardian/GENesis-AGI --skill genesis-development --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Genesis Development?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Genesis Development
[![Security: C — Skills Directory](https://www.skillsdirectory.com/api/skills/wingedguardian-genesis-development/badge)](https://www.skillsdirectory.com/skills/wingedguardian-genesis-development)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: genesis-development
description: >
  This skill should be used when developing, debugging, refactoring, or
  building Genesis itself — tasks like "fix this in Genesis", "add a new
  MCP tool", "wire up the runtime", "Genesis won't start", "create a
  worktree", "debug the bridge", or "add a capability". Applies to any
  task modifying files under src/, .claude/, or tests/. Do NOT load for
  Genesis-as-tool work ("summarize this", "write a LinkedIn post",
  "research X") or general questions unrelated to Genesis internals.
keywords: [genesis, worktree]
consumer: cc_foreground
phase: 10
skill_type: workflow
---

## Load Gate

Before reading any reference, confirm the task is Genesis-*development*,
not Genesis-*as-tool*. If uncertain, ask the user: "Are we modifying
Genesis itself, or using Genesis for something else?"

## On-Load Mindset

Internalize these immediately when this skill fires — they shape how to
work from the start, not just what to check before commit.

### Where your session ENDS — build sessions and closing sessions

**A session that writes code and a session that drives a PR to merge are
different SESSION TYPES, not two phases of one session's life** (user decision,
2026-09-02). Loading this skill makes you a **build session**.

- **A build session owns an item from Ready through opening its PR.** It runs
  the review the change EARNS — `/deep-review` for a substantial one, because
  that is what clears the commit gate's depth check; code-reviewer inline for a
  small focused fix (the Adaptive Review Protocol below is authoritative, and
  dispatching a full adversarial pass on a one-line change is not the bar) —
  and then it is DONE with that item. Where its effect can be observed here, a
  wrong run can be undone, and it is not on the never-run list, it is seen
  working BEFORE the PR opens ("Verify BEFORE the PR" below). It does not wait
  for Codex, and it
  carries no review loop — the obligations under "When to DRIVE a Merge" below
  pass to the closing session along with the PR; they do not lapse.
- **A closing session owns the open-PR queue**, whichever session built each
  PR. Its unit of work is the queue, not the card. That is the
  **`closing-session`** skill; load it instead when the job is "get the open PRs
  merged".

The handoff between them is **the PR itself** — a durable artifact that survives
compaction and session death, so nothing has to be remembered across the
boundary. It also satisfies the `reviewer ≠ implementer, fresh context` contract
structurally rather than by discipline.

Why it matters here: fused, the first item's review loop consumes the whole
session — several compactions deep — while everything else the owner arrived
with goes untouched. **Finishing the PR is finishing the work.** Handing it to
the queue is not abandoning it.

Two consequences worth internalizing:

- **Throughput is a RATE, not a per-session virtue.** Closed/wk must exceed
  opened/wk or the queue grows without bound (Little's Law). Build sessions do
  NOT throttle themselves to protect it — that just relocates the queue upstream
  onto the human deciding what not to start. Closing capacity is the control
  variable. (`scripts/pr_flow_rate.py` measures the two rates; it lands with
  PR #1613, so check that it exists before reaching for it.)
- **Compaction policy follows the same seam.** Reset context at
  plan→implement. Implement→review is a session-TYPE boundary, not a compaction
  decision — you hand off, you do not compact and continue. And never reset
  mid-"fix the findings": that work needs the implementation context you would
  be throwing away.

### PR readiness and mode

**Finish the work and clear blockers to entering review BEFORE opening a PR.**
Complete the implementation, required local review, and verification required
before publication. If unfinished work or an unresolved prerequisite prevents
normal review from proceeding, keep the change local, resolve the blocker, and
then open its PR. A draft PR is not a place to park incomplete or blocked work.

**A session opens every PR in regular, non-draft mode.** Never create a PR with
`--draft`. The exception is the runtime's own automated openers, which open
drafts by design: the executor's build lane (`autonomy/executor/pr_open.py`) and
the contribution opener (`contribution/pr_opener.py`).

**Moving an open PR to draft** is for a blocker to review that appeared AFTER it
opened (a prerequisite found missing, a design question raised in review); it
needs no explicit user request. It is never a way to hold work that was already
unfinished when the PR opened.

**A PR in draft returns to regular once the blocker that put it there is
resolved.** That holds for drafts opened before this rule. A draft from one of
the automated openers is different: it has no blocker to clear, neither opener
ever marks a PR ready, and the closing session counts and lists drafts without
closing them, so marking it ready is the owner's decision, which the closing
session's draft list is there to surface. Whoever resolves a blocker marks the
PR ready (`gh pr ready <N>`) and requests review (`@codex review`), because
marking a draft ready is not a step to rely on for triggering review. A draft
is never abandoned in that state.

CI and external review that run after opening, and verification explicitly
required after merge, remain their normal gates; waiting for those is not a
reason to use draft mode. Distinguish a blocker to entering review from a check
that review or merge will subsequently require. Open review findings, and a
dependency on another open PR named in the body, are of the second kind. A PR
sent BACK for rework or for an architecture decision is of the first kind,
because review stops until the rework lands (closing-session, "Sending a PR
back").

### Wiring Discipline

Every new component needs at least one call site in the actual runtime
path. Apply this 4-level verification taxonomy:

1. **Exists** — file/function present. Proves nothing.
2. **Substantive** — tests pass, handles happy + error. No runtime proof.
3. **Wired** — live call site, import chain unbroken. Minimum for "done."
4. **Data-Flow Verified** — real data flows end-to-end. Required for
   critical paths.

Mark nothing "done" below Level 3.

### Measure, Do Not Choose

**If your reason for picking a command, a flag, a value or a procedure is a claim
about how something OUTSIDE this repo behaves — git, the shell, the harness, a
provider API — then "which of these is right?" is an experiment you have not run,
not a judgement you are entitled to make.** Run it first. Ship what the run says.

The tripwire is mechanical, so it cannot be reasoned around: you are about to write
an instruction someone will execute, and your justification is a sentence about
another program's semantics that you did not observe. Reading the manual feels like
evidence. It is not.

Three clauses, each bought by a defect:

1. **Enumerate the space; do not pick cases from it.** List the AXES that
   independently change the behaviour, sweep the cross product, score every
   candidate on every cell. The cases you would think of are the cases you already
   believe in, which is exactly why they pass.
2. **Pre-register the predicate, the decision rule, and what you will do if
   nothing passes.** That last clause is what stops the least-bad option being
   rationalised into the right one.
3. **Control the instrument.** An ORACLE arm that must score 100% and a NO-OP arm
   that must fail. If the oracle is not perfect, every number in the run is void —
   including the flattering ones. Treat a surprising result as a suspicion about
   the harness before it is a finding about the code.

MEASURED instance: a hook note telling a reader how to undo a destructive git
operation was wrong FOUR times, each version reasoned out and each refuted by a
state nobody had constructed. The fifth was swept rather than chosen — 320 states x
6 procedures with an oracle and a no-op. Of the 240 states where the reader's HEAD
had not moved, the winner scored 240/240; the best alternative managed 80, and the
procedure that had actually SHIPPED managed 56. The sweep also surfaced a boundary
no reasoning had: that same winner scores 0/80 once the reader has committed, where
a different command gets 72. Twenty minutes of compute against three review rounds
of guessing.

Full method, including how to split a space where the question stops being
well-posed: `references/high-stakes-verification.md` section 9.

**And where you cannot measure, READ THE AUTHORITATIVE SOURCE — never your own
recollection of it.** Measurement is the top of the ladder; this is the rung
below, and the one most often skipped because recall feels like knowledge. For
any claim about how something OUTSIDE this repo behaves — a flag's semantics, an
exit code's meaning, an API's contract, a shell builtin's edge case — the order
is MEASURE > the authoritative document (the vendor's own reference, the man
page, the spec) > everything else. Pretraining recall is not a source. It is a
hypothesis phrased confidently, and it is wrong most often exactly where a
flag's NAME supports the assumption everyone makes about it.

READ, from an external audit of a sibling toolkit (2026-09): its CI documented
`uv --frozen` as the check that fails when the lockfile has drifted from the
project file. It is not. Per the vendor's own reference — uv docs, "Locking and
syncing", consulted 2026-09-15 — `--frozen` uses the lockfile as the source of
truth *instead of* checking whether it is up to date, while `--locked` is the
flag that "requires that the lockfile is up-to-date" and errors when it is not.
(That citation is this passage obeying its own rule: an earlier draft asserted
the same thing from recall and cited nothing, which an adversarial review
caught.) The claim was plausible, the flag name invites the misreading, and
nothing in that pipeline could ever have contradicted it — the verification
everyone believed was running simply was not, and a green run said so every
time.

The habit that prevents it costs one lookup: when you write an instruction, a
CI step, or a code comment that names an external tool's flag, open that tool's
documentation in the same minute, and cite what you consulted so the next reader
re-checks rather than re-derives. "I am fairly sure that flag means X" is the
sentence to catch yourself in.

**A new SKILL has its own version of this, and it is easy to miss.** Dropping a
`SKILL.md` into `.claude/skills/` gets it INDEXED automatically (the catalog
generator scans the directory — no registry to update), which looks like done.
It is Level 1. The nudge that actually surfaces it scores **only** whole-word
skill-NAME tokens and explicit frontmatter `keywords:` — description prose is
deliberately not scored, and name words on the hook's `_NAME_TOKEN_STOPLIST`
("genesis", "user", "plan", …) never score either — so a skill whose name is a
concept nobody types is indexed and silent. MEASURED 2026-09-02: `closing-session` scored **0.0** on
every one of its own trigger phrases ("work the open PR queue", "review and fix
the open PRs") until `keywords:` was declared; with them, 4/4 trigger phrases
fire and 3/3 unrelated prompts stay silent. Note the extractor drops tokens
shorter than 3 characters and does no stemming, so `pr` can never match and
`merge` will not match "merging" — declare the surface forms. Verify a new
skill by scoring it against the phrasings a user would really type, in BOTH
directions.
### GROUNDWORK Code Is NOT Dead Code

Code tagged `# GROUNDWORK(feature-id): why` is intentional future
investment. Never delete or refactor it as dead code. Only remove when
the feature is fully active or the user explicitly cancels it.

### Due diligence before building (standing owner rule, 2026-10-04)

**Get it right the first time. On everything that could change the plan, the
bar is beyond reasonable doubt BEFORE building, not after review finds it.** A
PR that comes out half-built because half its premises were never checked is
the failure this rule exists to stop. The checklist, which every development
plan presented for approval answers (task-executor plans from `/task` keep their
own `TASK_INTAKE.md` section contract instead):

1. **Every free read is done.** A free read is anything this session can settle
   read-only, or by a probe that changes nothing beyond scratch state, using
   tools it already holds. It is owed whenever its answer could change a decision. That covers: the code and every caller the change touches,
   its `CURRENT.md` entry, recent commits and PRs in the area, overlapping open
   PRs and issues, peer sessions, code intelligence, an API or renderer probe,
   and a corpus measurement. "I did not verify X, but it should be fine" is not
   an answer for an X like that: verify it, or name it as a residual with the
   reason no read or probe settles it.
2. **Premises measured, then red-teamed.** Try to disprove the root cause and the
   change's consequences, and name the alternatives weighed, simplest first.
3. **The plan body states:** what, why and how; confidence per item with what
   would disprove it; what was verified and what was NOT; alternatives; the
   red-team of the root cause and consequences; regression markers; and the
   test plan (targeted tests midway and at the end, CI for the full suite, a
   functional check of the built thing, "Verify BEFORE the PR" below, and the
   `E2E:` line for post-merge verification).
4. **A revised plan opens with a "Changed in this revision" section** right
   after its title (YAML frontmatter stays on line 1, and the title stays the
   first heading): what changed, and why.
5. **Ask the owner about intent, priorities and trade-offs. Decide measured
   technical edge cases yourself** and state the call; the owner overrides at
   approval. Design principles and standing axioms, approval and sovereignty
   gates, irreversible or outward-facing actions, and anything user-visible
   still go to the owner.

A question is material when its answer could change the plan; stakes set how
deep to go on each one, never whether it is asked. On the material ones, this
bar is higher than CLAUDE.md's investigate-below-90% floor. Detail, the
evidence behind the rule, and how it fits the rules it ties together:
`references/due-diligence.md`.

### Architecture Review

Every FINALIZED plan gets exactly ONE `genesis-architect` review — premise
check, scope drift, architecture — before it is presented for approval.
The agent's Step 0.5/0.6 take the plan file as input; hand it the path.
Revisions made in answer to that review do not re-trigger it. A plan too
small to write down needs none. `/plan-ceo-review` and `/office-hours` are
optional extras, if installed. Its findings are claims, handled under
CLAUDE.md's "Verify agent output": re-derive them, then fold the confirmed ones
into the plan BEFORE presenting it.

### Skill invocation points

Required steps at named moments. A skill marked "if installed" comes from an
optional plugin (gstack, superpowers); where it is absent, skip it — there is
no substitute checklist.

- **A test fails unexpectedly** → this skill's "Debugging Discipline"
  section first; plus `superpowers:systematic-debugging` or `/investigate`,
  if installed.
- **Before any done / fixed / passing claim** →
  `superpowers:verification-before-completion`, if installed.
- **An external tool or repo surfaces as a candidate** → the `evaluate`
  skill; on an ADOPT verdict, `integrate-module`.
- **A diff touches dashboard or other UI** → `/qa-only` or `/qa`, plus
  `/design-review`, if installed.

### Plan documents carry a structured header

A plan doc that outlives one session (`~/.claude/plans/<name>.md`) opens with
YAML frontmatter naming what it commits to and what it was written against:

```yaml
---
plan: <slug>          # matches the filename
status: active        # active | stalled | superseded | done
updated: 2026-09-15   # last revision of the LIVE section
pinned:
  main: <12-hex>      # origin/main as of the `updated` date above
binds: "<one line — what adopting this plan commits us to>"
prevents: "<one line — what it forecloses, or nothing>"
---
```

Quote `binds` and `prevents` — they are prose, and unquoted, a `": "` makes the
whole header unparseable while a `" #"` **silently truncates** the value
(`binds: PR #2046 ships first` loads as `"PR"`).

Optional `decisions:` / `ledger:` / `issues:` lists name the trackers this plan
executes, in FULL ids, so a reader of either end can find the other. An absent
list means "none" — so only omit it once you have looked; write
`issues: unchecked` if you have not. In the body,
`## ═══ SUPERSEDED BELOW ═══` divides live content from archaeology; no divider
means the whole file is live. What the body must STATE is set by "Due diligence
before building" above, for every development plan presented for approval.

**`pinned.main` is the field that pays for itself**: it makes
`git fetch origin main --quiet && git log --oneline <pinned.main>..origin/main`
a one-command staleness read, on a document whose line numbers and PR heads
otherwise go quietly false. The fetch is load-bearing — against an unfetched
`origin/main` the command prints nothing and reads as "no drift". A moved main
means re-verify, never that the plan is wrong.

Nothing checks any of this yet — the header is written by hand, and a plan
missing it fails silently. Rationale field-by-field, what the divider does NOT
do for a grepper, and what was and was not carried across from the upstream
schema: `references/plan-docs.md`.

### Timeout Policy

The burden of proof is on you to justify why a timeout should exist.
Do not default to "add a timeout for safety." Instead:

1. **Identify the specific failure mode.** What hangs? Why? Is there
   evidence this actually happens, or is it speculative?
2. **Justify the specific value.** Why this number and not another?
   What legitimate work would be killed at a lower value?
3. **If you have no strong justification for a specific value, default
   to 2 hours (7200s).** This is the project floor — generous enough to
   never interfere with legitimate work while preventing permanent
   resource lockout from truly hung processes.
4. **Surface the request to the user** with the value, the failure mode,
   and the evidence. Never add a timeout as a "small improvement" or
   "defense in depth."

Timeouts on reflections, CC calls, cognitive paths, and long-thinking
work fight Genesis instead of helping it — they cap legitimate long
thinking and add speculative defense against rare hangs. The exception
is raw subprocess calls with no external watchdog (e.g., deterministic
executor steps), where a hung process blocks shared resources (executor
semaphore) with no other recovery mechanism.

**The Bash TOOL's own timeout is separate — default 120000ms, HARD CEILING
600000ms (10 min).** An inner `timeout N …` INSIDE the command does NOT extend it:
the tool wrapper SIGTERMs the whole call at its own `timeout` param (default
120000ms → exit 143). To allow longer, set that `timeout` PARAMETER explicitly —
**but you cannot exceed 600000ms.** A larger value does not buy more time; the
call still dies at 10 minutes (MEASURED 2026-08-27: `timeout: 1600000` was killed
at exactly `10m 0s`). Treat "raise the timeout" as a fix that tops out, not one
that scales.

For long or unbounded work generally, use **`run_in_background: true`** —
harness-tracked, notifies on completion, and it does NOT inherit the 120s default
(MEASURED 2026-09-16: a 400s background task completed clean, exit 0). Prefer it
over a foreground timeout or `nohup … &` (detached but untracked → no completion
signal, so you end up hand-polling anyway).

**DEPLOYS ARE THE EXCEPTION, and this is the one to get right.**
`scripts/update.sh` must be **DETACHED FROM THE SESSION ENTIRELY** — not
foregrounded, and not backgrounded. (`bootstrap.sh` and `host-setup.sh` are long
too but are NOT in this group; see the warning below before reaching for either.)

- Foreground is impossible: MEASURED 2026-09-16, a bare `update.sh` ran **1022s**,
  1.7× the 600000ms hard ceiling.
- `run_in_background` is NOT the fix either, despite having no 120s ceiling: it is
  tied to the SESSION's lifetime, and deploys launched that way were killed mid-run
  on 2026-07-22 (leaving genesis-server DOWN during bootstrap) and 2026-09-16
  (during the pre-update backup). The precise kill mechanism is UNRESOLVED — three
  candidates are eliminated in CC memory `deploy_detach_not_background`, and the
  successor hypothesis there is hedged, not established. Detachment removes the
  coupling to this session, which is what every remaining candidate runs through.
  It is not immunity in general: an OOM kill reaches a systemd unit just as readily.

```bash
XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" \
DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-unix:path=/run/user/$(id -u)/bus}" \
systemd-run --user --collect --unit genesis-deploy-manual \
  --working-directory=$HOME/genesis \
  --setenv=PATH="$PATH" --setenv=SSH_AUTH_SOCK --setenv=GENESIS_SYNC_PRIVATE_PATTERNS \
  /bin/bash -c 'mkdir -p ~/tmp; exec ./scripts/update.sh > ~/tmp/deploy-$(date +%Y%m%d-%H%M).log 2>&1'
```

substituting the checkout root you actually ran from for `$HOME/genesis` if it
differs — `scripts/install.sh` sets `REPO_DIR` from its own location, so
noncanonical checkouts are supported, and `update.sh` derives its root the same way, so the canonical path is an example, not a
constant. (Keep the command itself pasteable — a literal `<placeholder>` in the
value is a shell syntax error.)

**Two preconditions, checked before detaching, not after:**

- **Linger.** A `--user` unit dies with the user manager when your last login
  session ends unless linger is enabled. `loginctl show-user $(id -u) -p Linger`
  must print `Linger=yes`; if it does not, run `loginctl enable-linger $(id -u)`
  first (one-time, needs sudo). Without it the "detached" deploy survives the CC
  session only as long as you stay logged in — which is the same coupling, one
  level up.
- **Noninteractive sudo.** `update.sh` runs `bootstrap.sh` on every run that
  applies a change (its "Run bootstrap" step), and bootstrap calls `sudo`. The no-sudo claim covers only
  `update.sh`'s own text, not its transitive calls. If sudo prompts anywhere on
  this install, the detached deploy aborts mid-bootstrap. Require NOPASSWD-style
  sudo: sudo's per-terminal timestamps mean refreshing the credential in your
  terminal does not authorize the no-TTY calls inside the unit, and a warm
  cache expires mid-deploy anyway. Check it in the same no-TTY service
  context the deploy will get, with the bus variables set:
  `systemd-run --user --wait --pipe --quiet --collect -- sudo -n true` must
  exit 0 (MEASURED 2026-10-01: `--scope --wait` is refused outright, "--wait
  may not be combined with --scope"). If it fails, run the deploy in a
  terminal instead.

**`mkdir -p ~/tmp` is part of the command, not tidiness.** `~/tmp` is not
guaranteed — `install.sh` and `bootstrap.sh` create it only when `/tmp` is small —
and the shell opens the redirect *before* exec'ing `update.sh`, so a missing
directory kills the unit instantly, before the deploy's own `mkdir -p "$HOME/tmp"`
near the top of `update.sh` can run. With `--collect` that leaves nothing behind to diagnose,
which is the same invisible failure the verify step below exists for.

**The bus variables come first, and they are not decoration.** A CC session often
has no `XDG_RUNTIME_DIR`, and without it the `systemd-run --user` CLIENT cannot
reach the user manager: MEASURED 2026-09-16, a scrubbed environment gives
`Failed to connect to bus: No medium found` and nothing starts at all. `--setenv`
configures the prospective *unit* and cannot help the client connect, so it is no
substitute. `update.sh` exports exactly these two near its top for the same reason, and
`cc/invoker.py` records the same failure as measured on a live install. This one is
easy to get wrong by testing in a session that happens to have the variables —
which is how it reached review here.

**A transient service inherits the user manager's environment, not your shell's.**
Only what you name with `--setenv` crosses over, so a deploy depending on an
exported variable silently changes behaviour: `SSH_AUTH_SOCK` (`update.sh`'s
`git fetch` of a private SSH remote fails without the agent) and
`GENESIS_SYNC_PRIVATE_PATTERNS` (`bootstrap.sh` branches on it) are forwarded above. Naming an unset variable is
harmless — measured, the unit still starts.

**Never apply this to `bootstrap.sh` or `host-setup.sh`.** Both need a channel a
detached service does not have. `bootstrap.sh` runs under `set -euo pipefail` and
calls bare `sudo` (the journald and sshd drop-in steps' `sudo mkdir`), so wherever
sudo prompts it aborts partway through configuring the machine — and "sudo is
passwordless here" is a fact about one box, not about the recipe. `host-setup.sh` is
worse: interactive, run on the bare host VM, and its "Delete and recreate? [Y/n]"
prompt treats EOF as the default `Y`, so detaching it stops and renames the existing
container. `update.sh` is the only script in this family with **no sudo calls of
its own** — it does invoke `bootstrap.sh` (its "Run bootstrap" step), which does call
sudo, hence the noninteractive-sudo precondition above — and it remains the only
one covered. Run the other two in a real terminal;
the advisory no longer fires on them rather than offer a recipe that breaks them.

Wrap **the command you actually ran** — `update.sh` and `bootstrap.sh` are not
interchangeable and take their own arguments. If you build that wrapper
programmatically, quote the
whole inner script as one unit (`shlex.quote`) rather than interpolating an
already-quoted command into `bash -c '…'`: MEASURED, the naive form turns
`--msg 'a b'` into `--msg a` and executes a `$(…)` the user quoted as data.

`--setenv=PATH` is load-bearing: a `--user` unit otherwise gets systemd's default
PATH, which omits `~/.local/bin`, so bootstrap takes its `uv not found — installing…`
branch rather than the branch an interactive run takes. The shipped code sidesteps
this by passing the whole environment (`updates.py::_apply_direct` uses
`env=os.environ.copy()`).

**Two things that look like detachment and are not.** `systemd-run --user --scope`
keeps the CALLER'S session id (MEASURED) — cgroup-isolated, still session-held;
`_apply_direct` gets away with it only because its parent is genesis-server rather
than a CC session. And **`setsid` is not detachment**: it runs the program in a new
session but does not background it or stop the caller waiting on it — MEASURED,
`timeout 1 setsid bash -c 'sleep 4; echo > f'` exits 124 having written no file.
What detaches is the default transient SERVICE mode (no `--scope`): MEASURED
2026-10-01 on systemd 255, a service child's parent is the user manager and it
runs in a new process session, while a `--scope` child's parent is the calling
shell. `--unit` does not detach anything — an auto-named service lands in the
same place — it only gives the unit a fixed name for the verify step below.

Then VERIFY it took — with the same two bus variables set (a scrubbed session
otherwise gets a bus error, which says nothing about the launch),
`systemctl --user is-active genesis-deploy-manual` must print `active`. `systemd-run` does NOT inherit the tool's cwd, so a bare relative
path resolves under `$HOME`, bash exits instantly, `--collect` reaps the unit, and
a failed launch is indistinguishable from a successful one. (It also reports
`inactive` once the deploy has FINISHED, so check promptly and read the log for the
outcome.) `genesis-server` going `inactive` mid-run is EXPECTED. When the server is
up, `POST /api/genesis/updates/apply` with `{"supervised": false}` does the same job
and passes the environment for you.

**A mid-run kill is never a no-op, but what it does depends on the signal and on
the phase.** Three windows, in order. From where `trap '_on_signal INT' INT` arms
the rollback trap — just BEFORE `_write_state "merging"`, not after the merge — an
interrupt runs `_on_signal` → `_do_rollback`. Between `trap '_on_signal_prestop …'`
and there, `_on_signal_prestop` restarts the services it stopped and explicitly does
not roll back. Earlier still — during the pre-update backup (the `backup.sh` call) —
**no INT/TERM trap is installed yet**, so a SIGTERM there runs no handler at all. That last point matters
for diagnosis: "no trap ran" does NOT identify a kill as SIGKILL, because the
untrapped window produces the same evidence. Note too that `_do_rollback` disarms
INT/TERM as it starts, so a second signal lands mid-rollback with default
disposition, and the rollback has failure exits that report `ROLLBACK INCOMPLETE`.
Either way: verify state (server active, no mid-rebase, pin, both CC versions)
before re-running; it is idempotent. (A non-blocking PreToolUse advisory hook,
`.claude/hooks/cc-deploy-timeout-guard`, nudges toward this on any deploy — it does
not check whether you already detached, which is tracked in issue #2088.)

**Why this paragraph is worded so defensively:** PR #1221 shipped both this policy
and that hook on 2026-07-22 prescribing `run_in_background`, from a CC memory
written two days earlier. An incident that same day refuted it. The correction
landed only in a second CC memory on 07-23, both public surfaces kept the original,
and a third deploy was killed by this advice 56 days later. A remedy is only as
good as the case it was actually executed against.

An earlier draft of this paragraph also claimed #1221 shipped a second, already-stale
assertion about signal handling. That was **false, and worth recording as the more
useful lesson**: it was inferred from the CC memory #1221 was written from, rather
than read out of the commit, which shipped the correct claim. A note is evidence
about what someone believed when they wrote it — never about what the code says
now, and never about what some other artifact contains. Open the commit.

### Verify Outcomes, Not Just Tests

`ruff check . && pytest -v` is the minimum bar, not the finish line.
After tests pass, verify the actual end-to-end outcome the change
delivers. Diff behavior between main and your changes when relevant.
For wiring changes: verify the init/bootstrap order passes the right
values at runtime, not just that parameters exist. For notification
changes: verify the notification actually arrives. Ask: "If the system
restarts right now, will this actually work?" If you can't answer yes
with evidence, you're not done.

**A check keyed on EXISTENCE can only confirm — it can never disconfirm.**
Before trusting a verification, ask what it would do if the thing were absent,
stale, or the wrong object. If the answer is "pass", it proved nothing. Three
shapes of this, all measured in one session 2026-09-08:

- **Waiting for a NEW artifact by testing that one exists.** A poll for "a
  scheduled-review marker is present" passed instantly against a marker from six
  days earlier. Key the wait on the artifact's IDENTITY — the head SHA it names —
  not on its presence.
- **Matching the first row instead of the right one.** "Is main green?" matched
  the first `push` run for that SHA, which was the CodeQL workflow (success),
  while the CI workflow for the same SHA had failed. Filter by the thing you
  actually mean (`--workflow ci.yml`), and prefer reading the JOB you care about
  over an aggregate conclusion.
- **Counting from a listing sorted by the wrong key.** `gh pr list --state
  merged --limit 40` sorts by CREATION, so PRs created earlier and merged today
  fall outside the window and vanish; the count read low and looked plausible.
  Use a query whose filter IS the property you are counting (`gh search prs
  --merged-at`), per CLAUDE.md's truncated-listing rule.

The failure is silent and self-confirming in all three: an under-read is
indistinguishable from a clean result, so nothing prompts a second look. The
tell is that the check passed FASTER or more easily than the work should have
allowed — a poll that succeeds on the first attempt for something that takes
40 seconds to produce has not observed that thing.

**Verify in the REAL runtime context, not a shell proxy.** "Works when I
run it" is not "works where it runs." Same code + same uid ≠ same context:
a long-running systemd service (genesis-server, guardian) differs from your
interactive shell in mount namespace, seccomp, `NoNewPrivileges`, dropped
caps, and — the one that bites — **ptrace/`/proc` access**. Anything that
reads `/proc/<other-pid>/{environ,mem,stat}`, another process's env, sockets,
or namespaced/hardened resources MUST be verified by hitting the **live
endpoint** (`curl` the real server) or running inside the real service — not
`python -c` in your shell. (Origin, 2026-08: the CC-slot stale-code badge
read `/proc/<pid>/environ`, which succeeds in a shell but returns EACCES
under the server's `ProtectSystem=strict` sandbox — so `enumerate_cc_slots()`
returned 0 in-server and **three** features shipped green but inert since
July; the module's docstring claim "same-uid reads succeed" was a shell-tested
falsehood. The fix routed around the ptrace-gated read entirely.)

**And when the work is done, audit the CLAIMS as well as the outcome.**
Everything above distrusts a VERIFICATION; the companion failure is the
SENTENCES you wrote about the work — a number in a commit message, a docstring's
assertion, an in-thread reply saying a finding is fixed. A green suite says
nothing about any of them, and permanent record has no tone of voice, so the
next session reads a confident sentence as a measured one. Those surfaces are
ENUMERABLE, which makes the check mechanical rather than a mood.
`references/high-stakes-verification.md` §11 carries the canonical surface list
and the walk; do not keep a second copy of that list here.

### Verify BEFORE the PR, when this install can show it (standing user rule, 2026-09-24)

You build on a long-lived install, so **when the change's effect can
be observed here AND a wrong run can be undone, see it work before you open the
PR.** Review then looks at a change that works, not at its first attempt to run.
Put what you ran and what you saw under the PR's `## Testing` section. The `E2E:`
line keeps its meaning: the check to run AFTER the merge. For a hook that is still
needed, because a pre-PR run tests the worktree copy, not the merged copy the
harness fires.

**Which copy actually ran is the whole question. Check it; never assume it.**
- A hook guard run as `python3 <worktree>/scripts/hooks/<guard>.py`, with a real
  payload on stdin, runs the branch's copy of that file. The harness, and the
  worktree's own `.claude/hooks/genesis-hook`, run MAIN's copy by default (see
  "The Gate Machinery" below). `GENESIS_HOOK_DEV_LOCAL=1` on that ONE launcher
  call switches it, and stderr announces the switch. Never set it session-wide or
  in `settings.json`: your own commits and pushes would then run under the gate
  you are still editing.
- Which `genesis` package a Python file imports depends on the file. MEASURED
  2026-09-25: from the worktree root, `import genesis` resolves to the MAIN tree
  through the editable install. But more than twenty scripts and hooks put their
  own tree's `src` on `sys.path`; run from a worktree they load the BRANCH's
  package, whose database path then resolves inside the worktree, where no
  database exists, while Qdrant is still the live one. Read the file's imports
  before running it, and print `genesis.__file__` when in doubt.
- So runtime code (`src/genesis/**`) is verified by tests, or by the options
  `references/worktrees.md` lists for runtime checks, never by a live run,
  until there is a supported way to run a branch in the live runtime.
- Say in the PR which copy ran and what it touched.

**Only a run you can undo qualifies.** A script that deletes, prunes,
restores, deploys to the host, pushes, posts or sends a message, or writes to any
live store (the database, Qdrant, the graph engine) does NOT run before it
merges. A `--dry-run` flag is not an exception by itself: `restore.sh --dry-run`
still takes the live update and backup-restore locks, runs `git pull --rebase` on
the backup repository, and overwrites `~/.genesis/restore_status.json` on exit.
Run a dry-run only after reading that mode's code and finding that it writes
nothing. Do not improvise isolation: `GENESIS_HOME` moves neither
the database nor Qdrant, and many files hard-code `~/.genesis`. Tests are how such
a script is verified.

**Never run on the live install before it merges:** a branch's migration, privacy,
egress or credential handling, anything that loosens a gate other sessions rely
on, and anything whose verification needs days or a machine the owner does not
control. Test a migration in a pytest fixture, and patch EVERY store it touches on
the migration MODULE itself. The suite-wide fixture patches
`genesis.env.genesis_db_path`, but data migrations import that name directly
(`from genesis.env import genesis_db_path`), so do not rely on that fixture to
isolate a migration: patch the migration module's own names. A migration can
also reach live Qdrant and write under `~/.genesis`.
`d0008_reconcile_memory_cross_store`, for
example, deletes the Qdrant points it judges to be ghosts. Its test patches the
module's `genesis_db_path`, `get_client` and `_export_path`:
`tests/test_db/test_d0008_reconcile_memory_cross_store.py` is the pattern to
copy.

NEW behaviour on a risky surface (memory, graph, database) ships behind a
shadow flag, and the session never flips it: the owner does, after verifying.
Entity adjudication is the house example. `config/entity_adjudication.yaml` ships
it in `propose_only`, so a merge is recorded as a proposal. The owner reviews and
approves proposals and applies the approved ones (`entity_adjudication_approve`,
then `entity_adjudication_apply`) without changing the mode. Switching to `live`
is a separate decision: it applies only approved backlog rows, but from then on
it applies each new merge verdict with no approval step.

**Iterate with the owner.** When acceptance needs the owner's hands (a device, a
chat channel, another machine they use, or something only they can see), do not
open the PR on a guess. Tell them exactly what to try and what they should see,
ask through `AskUserQuestion`, fix what they report, and re-ask while it blocks.
Open the PR once they confirm it works. Waiting on the owner here is the design,
not a stall: no review round could have caught what they are about to see.
- **Do not dispatch work whose acceptance needs the owner's hands.** No
  dispatched path is known to hold a change for the owner's test today.
  - The autonomy executor pushes a completed code task's branch and opens a
    draft PR for a build-lane task that passes its scope gate. It then deletes the
    worktree and the local branch, so there is nothing left to test locally
    (#2421).
  - The executor's code steps have no MCP write except `observation_write`, so
    `follow_up_create` is not available to them (#2422).
  - A `direct_session_run` session's tools depend on its profile, and the
    profiles differ in what they allow. Read the chosen profile's definition
    (the built-ins in `src/genesis/cc/direct_session.py`, plus any install
    overlay in `genesis.cc.profile_overlay`) before assuming the session can commit
    code, keep a worktree, or record anything.
  If a dispatched session finds an owner test is owed anyway, it states what to
  try and what they should see in its final output. For an executor task that
  output is read through `task_detail`, and it reaches no notification. A
  follow-up row that a dispatched session creates (on a profile that allows it)
  lands on the hidden tabled lane, so its `reason` must say it awaits the owner's
  test in a foreground session.
- **The owner says they cannot test it this session, or the session ends
  before they confirm:** a `follow_up_create` row naming the owner test still
  owed and where the code is (branch, worktree path, head SHA). Never write the
  change up as verified.

### Acceptance Bar + Measured Rate — the primary methodology

**Use this as often as it applies. It is the default way to build anything here,
not a special-occasion technique.** Unit tests prove the code does what you wrote.
This proves the thing actually WORKS — with numbers and a denominator.

Two artifacts, both produced BEFORE shipping:

**1. The acceptance bar — replay the real defect.** Take the actual failure that
motivated the work and run it through the new thing. If it does not catch/fix
the case it exists for, it does not ship, however elegant it is and however
green the suite is. Reconstruct the real case (from git history, from the
transcript, from live data) rather than a stylised approximation — a synthetic
case can pass while the real shape does not.

**2. The measured rate — run it against real data and produce a number.**
A claim like "low false-positive rate" or "it should be fine in practice" is not
evidence. Run the thing over a real corpus — recent commits, live rows, real
traffic, historical transcripts — and report `k/N (x.x%)`. **A number without a
denominator is not a measurement.** Then look at the individual hits and say
honestly which are real signal and which are noise; a "false-positive rate" that
turns out to be mostly true positives is a fire rate, and saying so is part of
the result.

**For a Bash GUARD's predicate there is a corpus of this install's own recorded
`(command, cwd)` pairs, and `scripts/replay_guard_corpus.py` builds it — but
REPLAY IS CURRENTLY UNAVAILABLE.** Every guard is refused; `--guard <name>` and
`--all` exit 2. So the `blocked k/N` measurement this bullet wants has to be
taken another way for now — by hand, which is how it was always taken: MEASURED
2026-09-16, `git log origin/main --pretty='%s%n%b' | grep -icE "real commands"`
returns 23 against 1,656 merged-PR commits, of which 121 are `fix(hooks)` or
`feat(hooks)`. Roughly one hook PR in five already does this sweep by hand.

The permission was withdrawn rather than lost. It used to rest on a declared
claim that a guard performs no writes, spawns or network calls, re-derived by
walking its imports — and that claim cannot be established by reading: the
spelling set is open, and four review rounds each surfaced the next round's
miss, including three fail-opens inside the checks themselves. Issue #2036
restores replay the other way round, by running it where the effects are
impossible, so nothing has to be proved about the guard at all.

What the tool still does is check that each declaration's prose still points at
code that exists: `--list` resolves every `Cite(module, symbol, fragment)`
against the symbol's source and refuses on a mismatch. Comment stripping is
complete for PYTHON, which is tokenized; a non-Python file loses only full-line
comments, so a fragment cited from a shell file can still match from inside a
trailing one — and a citation with no symbol matches the whole file, unscoped.
`--list` publishes both limits. That is worth running
after editing a guard — six citations in this table went stale within five days,
one onto a comment about an unrelated timeout. It tells you nothing about what
the guard DOES; `--list` prints its own limits.

Two limits will travel with the number when replay returns: it is stamped
UNCLASSIFIED because the corpus contains dangerous commands too, so it is one
side of the tradeoff and needs the positive control the bullet above demands;
and there is **no way to print the blocked command lines**, because those are
real commands containing secrets passed in argv (issue #2007 tracks what a
correct version owes). The count and the verdict are what go public.

The measurement is a GATE, not a footnote. Decide the acceptable threshold
BEFORE measuring, and if the number misses it, tighten and re-measure rather
than shipping with a caveat. When you tighten, re-run the acceptance bar in the
same breath — a filter that improves the rate by breaking the thing you built it
for has made it worse, and only running both together catches that.

Worked example (2026-08-27/28, an orphaned-literal detector). The tool itself
was still on an unmerged branch when this was written, so treat the first two
bullets as a method illustration; the figures in the paragraph after them are
derived from this repository's own history and can be re-derived.
- Acceptance: replayed a real review defect; the detector named the exact
  sibling file. PASS.
- Measured, false-positive side: `1/151 real file-edits fired (0.7%)`. The one
  hit was an identifier rather than prose, so the filter was tightened to
  require interior whitespace, re-measured at `0/151 (0.0%)`, **and the
  acceptance replay was re-run to confirm the tightening had not blinded it.**

**That example then became a lesson against itself, which is why it is kept.**
Everything above measures FALSE POSITIVES, and `0/151` reads as though the
tightening were free. It was not. Measuring the other direction needs
INDEPENDENT ground truth rather than the tool's own criterion — otherwise you
grade the tool on its own definition of success. Here that meant mining history
for a literal removed from one file and then removed AGAIN from a second file in
a later commit: the repo itself recording that the first fix left a sibling.
Over 1,584 commits that yields **95 verified cases (6.0% of commits)** — and the
interior-whitespace filter that scored so well on precision **excludes 48 of
those 95 (51%) by construction.** The number that actually decided the design
was recall against a budget cap: **28/47 in-scope cases caught (60%)** with a
cap of 6 literals per edit, versus **47/47 (100%)** with the cap lifted — every
one of the 19 misses was that single cap, and lifting it recovered all of them.

The rule that generalises: **a rate measured on one side of a tradeoff is half a
measurement.** A precision number with no recall number cannot distinguish a
good filter from a blind one, and the side you did not measure is the side that
will be wrong. Decide which direction matters for the thing you are building,
and measure that one first.

This also catches a specific self-deception. A first prototype of that same
detector used a regex and reported "2 findings" while **silently skipping the
entire class it was built for** (the pattern excluded backslashes; every prompt
string ends in `\n`). The acceptance replay is what exposed it. A matcher that
finds nothing is indistinguishable from a matcher that looks at nothing —
only replaying a known-positive tells them apart.

**A null result is a LEAD, never a clearance — and it travels with its blind
spot attached or it does not travel.** The paragraph above is about an
instrument that looked at nothing. This is the harder case: the instrument
worked, the reading was clean, and the reading was still wrong. Three null
results from one session were wrong the same way, and every one was overturned
by CONSTRUCTION rather than by more sampling:

- *"An expansion in verb position always emits ≥2 words, so it self-corrupts any
  command it appears in."* Generalised from a handful of samples of ONE of the
  construct's spellings. Another spelling admits a degenerate case that emits
  exactly one word — so argv survives intact, and the parse resolves a verb bash
  never runs. Stated abstractly on purpose; see the note under the
  resource-defect bullet in Test-First Discipline.
- *"Zero over-block flips across 129,179 real commands."* The regression was
  CONSTRUCTIBLE, and a later worker constructed it: a `BLOCK → ALLOW` flip on
  an `rm -rf` of the production database's parent directory.
- *"Zero regressions, verified three ways."* All three ways were the same axis —
  see the resource-defect bullet under Test-First Discipline.

A corpus measures what has been TYPED. It says nothing about what is TYPEABLE.
For a SAFETY property the question is therefore never "did I observe a failure"
but "can one be CONSTRUCTED", and only the second question has an answer that
clears anything. **So when a null result is handed to anyone — a subagent, a
peer session, a PR body, the user — state the DENOMINATOR and the METHOD'S BLIND
SPOT in the same breath as the number, and say in words that it is not proof.**
Then name the recipient's job: the person best placed to construct the
counterexample is whoever is about to rely on the null result, and they will not
go looking unless you tell them the search is still open. A null passed on bare
is read as a clearance by everyone downstream, which is how one unproven
sentence becomes the premise of three later decisions.

### Select, don't amputate — truncation is the absence of a decision

**Scope first, because bounding is often correct.** What makes something an
amputation is LOSS — the value cut here was the only copy. Nothing else. A
bounded PREVIEW of something stored intact elsewhere is a selection, and stays
one even if its handle is useless. Bounding against a hard external budget is
likewise correct: a hook's stdout cap, a context window, a column whose limit is
actually ENFORCED. That last qualifier is load-bearing here — this repo's SQLite
`TEXT` columns enforce no length at all, so "the database column" does not
excuse a cut; a self-imposed storage assumption is a decision to justify, not a
budget to obey. So is refusing an oversized value outright.

**And a SAFETY cap may be lossy — that is the one place cutting the only copy is
right.** Streaming is a TRANSPORT property and only-copy is a DURABILITY one;
check them separately, because a chunked read is often backed by a retained
source you could go back to. The case that earns the lossy cap is the source
that genuinely has no retained copy — a live subprocess pipe — where reading to
the end to avoid "truncating" is how a runaway command exhausts memory; this
repo bounds exactly that at a few MiB (`autonomy/executor/deterministic.py`
`_read_limited`, whose own comment names the `yes`-command threat; verified
2026-09-04). Losing the tail of a log beats losing the process. The obligation
there is not to keep the bytes, it is to be LOUD about the cut — say the output
was bounded and roughly by how much, so nobody reads a clipped log as a
complete one. That cited cap has since been fixed AT THE READ and is no longer
the silent-cut example it was: `_read_limited` drains the whole stream while
retaining only `limit` bytes and returns `(retained, total_size)`, so the caller
appends `... (truncated, N bytes total)` with N the TRUE drained total (PR
#1796; verified in `autonomy/executor/deterministic.py` 2026-09-08).
**It is still not a worked example of a loud cut, and the reason generalises:
a declaration has to survive the CONSUMERS, not just be emitted.** That marker
is appended at character 50,000 of the result field, and all six onward paths
head-slice it at 200-2000 characters — including the one that feeds the next
step's prompt — so the declaration is unreachable in every direction it
travels, and the slice that removes it declares nothing itself (MEASURED
2026-09-08). When you fix a silent cut, check the READERS of the field you just
made honest; otherwise you have moved the silence one layer out. A silent lossy
cap is still the defect; a declared one is a resource guard doing its job. This
section is about the remaining case.

**A handle that does not resolve is a separate defect, and do not conflate the
two** — that conflation is the mistake this section made about itself, twice.
Check the pointer, because a preview advertising a retrieval path that does not
exist teaches a lie; but when the full value survives somewhere, the fix is to
mend or drop the handle — removing the cap TO PREVENT DATA LOSS is fixing a
loss that never happened, and that misdiagnosis is how this rule causes the
damage it exists to stop. Whether the cap should exist at all is the separate
question the "what breaks if it is unbounded" test answers: a cap with no
external, safety, or measured compatibility justification may be removed once
that is established — for being unjustified, never for being an amputation.

**There, do not truncate.** Not strings, not lists, not context, not output.
Reaching for a character cap is a signal that a question was skipped, not
answered. Omitting is legitimate — it is a judgement about relevance. Truncating
is not: it is what happens when that judgement was never made, so the value gets
cut at a point that has nothing to do with meaning. A truncated value is
frequently worse than either alternative, because it still LOOKS complete, so
nobody checks it — at which point you may as well not have passed it at all.

Before bounding anything, answer: what is this value FOR, who reads it, why does
it need budgeting at all, and what actually breaks if it is unbounded? Solve
THAT. Usually the answer is "select less, whole" rather than "cut", and often the
bound turns out not to be load-bearing.

Three rules when a bound really is needed:

- **Bound by MEANING, not by one blanket number.** A closed set is validated
  against that set — a value outside it is INVALID, not "too long". A timestamp
  is a shape; half a timestamp is not a shorter timestamp. A blanket cap turns
  100,000 characters of foreign data into 300 characters of foreign data and
  calls it bounded.
  **This governs HOW you bound, never WHETHER you may.** Rejecting a structured
  value or a collection by size is correct and stays correct: an over-long id, an
  over-large batch or an implausibly large file is simply not a value we accept,
  and refusing it is a resource guard, not an amputation. What is forbidden is
  silently CUTTING them to fit. Only free text gets a bound it is expected to sit
  under; every other INBOUND value gets one it must not cross. Scope that to
  inbound on purpose: a READ that pages a large collection — the first n WHOLE
  elements plus a total and a truncation flag — is a selection with a
  denominator, this section's own preferred shape, and the source collection is
  valid precisely because it exceeds the page. Reject the oversized value you
  are asked to ACCEPT; paginate the oversized collection you are asked to LIST.
- **Derive the number from the right thing, and record how.** A SAFETY bound —
  memory exhaustion, an abuse ceiling, untrusted input — does not come from the
  corpus at all: historical traffic says nothing about adversarial input,
  concurrency, or the memory you actually have, and a cap chosen from observed
  values will be exactly the wrong size when it matters. Derive those from the
  protocol, the capacity and the threat model FIRST, then use `k/N` only to price
  what the bound rejects. It is the COMPATIBILITY bounds — how long is this field
  in practice, what does this cap cost real readers — that a corpus answers, and
  for those: measure the real population and report `k/N` per the Acceptance Bar,
  then name the corpus, the query and the date. Naming them is necessary and not
  sufficient: the query is only re-derivable if the rows are still there when the
  next reader runs it, so a table with a retention window yields an EPHEMERAL
  observation. Say which one you have. And what the bound COSTS is a SECOND claim
  needing its own denominator — "the cap discards the part worth keeping" is
  exactly the sentence that sounds measured because it followed a measurement.
- **Omit explicitly, with a constant-bounded marker** (`<omitted: 104,823
  chars>`) in preference to a mid-value cut. An honest gap beats a
  plausible-looking fragment. The bound-plus-loud-flag half of this is already
  the house pattern; the character-count marker is a proposal, so do not go
  looking for a precedent that is not there.
  **Declaration is a requirement on top of the loss rules, never a substitute
  for them.** Stating the rule as "never cut" overshoots: this repo cuts
  mid-value in several places on purpose and is right to — a resource guard on
  an unbounded stream, a preview rendered next to the full record, a display
  string trimmed before escaping so the cut cannot land mid-entity. Each of
  those is a cut the loss rules PERMIT (a safety cap, a pointer-backed
  selection, third-party display text) — which does not certify how each is
  reported today: the safety cap above still cuts silently, and PERMITTED is
  not DECLARED. The order matters: first the loss rules decide whether a cut may
  happen at all — announcing a sliced KEY does not un-merge the two identities
  it collapsed — and only then does declaration decide whether the permitted
  cut is honest. A silent permitted cut is still a defect; a loud forbidden cut
  is still forbidden.
  **When the total is not already known, say that instead of computing it.**
  Bounding a stream is the case — but first ask which KIND of bounded reader
  you have, because the bound is on RETENTION, not necessarily on reading. A
  drain-and-discard reader consumes to EOF anyway (a subprocess pipe must be
  drained or the child blocks) and can count what it discards in constant
  space, so it KNOWS the exact total and should say it. Only a stop-at-limit
  reader, which truly stops consuming, cannot know. There, state the quantity
  you KNOW — the cap — and mark the tail unknown:
  `<kept first 40,000 chars; rest of stream omitted>`. Do not write
  `<omitted: ≥40,000 chars>`: under this rule's own grammar that number
  describes the OMITTED content, and one character over the cap makes it a
  fabricated tail length — an invented number wearing the honest marker's
  clothes, which is the exact thing the marker exists to prevent.

One trap deserves naming, because it is what produced this rule: **a cap can
manufacture a correctness bug in the very data it was added to protect.**
Truncating an identifier used as a KEY merges two distinct identities into one,
and downstream code then attributes one subject's state to another. That is not
hypothetical — it shipped here. Two roster peers whose names shared a prefix
collapsed onto a single key, so one peer's success cleared the other peer's
recorded failure. A short DISPLAY handle is a different thing and is fine; the
rule is about the stored key, not the rendered one.

**A real need to truncate is a CONVERSATION to have, not a magic number to pick
alone.** If you catch yourself choosing 300 or 200 or 1000, stop and raise it.

**When there is nobody to raise it with** — an unattended background session —
the rule is NOT "never pick a number". It is **never pick one silently.** Bound
if you must, and put the reasoning beside the number, where a reader of the value
will see it. That includes the question that comes BEFORE the number — whether
this value needed bounding at all — because "is this big enough to matter?" is
the same judgement drawing on the same missing information, and skipping it is
precisely how the number gets invented. State both. A number with its reasoning
next to it can be argued with and corrected, which is all anyone needed from you.
What made the original defect dangerous was never that 300 existed — it was that
300 arrived silent, looking deliberate, and was then defended.

That default is about SIZE, never about secrecy, and the two must not be
confused. If a value may carry a credential, token, or personal data, the
unbounded default does NOT apply — for UNAUTHORIZED egress and for storage you
cannot vouch for: there, fail closed, omit wholesale with a marker as the "omit
explicitly" rule says, and keep only non-sensitive metadata. Losing diagnostic
prose is recoverable; leaking a token is not. The scope qualifier is
load-bearing and an earlier revision dropped it while restructuring — read
unconditionally, "omit wholesale" would delete values existing features exist
to hold: the References store deliberately RETAINS credentials, and its own
dashboard tab masks the value behind an explicit reveal
(`dashboard/routes/references.py`; verified 2026-09-04). That is a statement
about ONE surface, not a security guarantee — other readers of the same store
return raw bodies (`reference_lookup`, the knowledge routes), which is worth
knowing precisely because a rule reader might otherwise lean on the reveal
gate as if it covered them. An authorized, gated store holding a secret is the
feature working, not a leak.

**Then ask WHO WROTE IT as well as where it is going — two independent axes,
and each governs a different decision.** Destination governs DISCLOSURE: what
may cross a trust boundary is decided by where it lands, whoever wrote it — a
user-authored secret bound for an external channel still gets scanned — and
quarantined when a SUPPORTED pattern matches. Say that precisely, because the
scanner is a high-confidence pattern gate by its own declaration, not a general
secret filter: content carrying a secret outside its pattern set passes as
safe, so nothing downstream may lean on that gate as proof of cleanliness
(`security/output_scanner.py`; verified 2026-09-05). Authorship is a RISK INPUT to sanitization, and the treatment
itself is chosen by the CONSUMING SINK: the same stranger-authored email
fields are HTML-escaped and truncated where the sink renders raw HTML
(`outreach/engagement.py` `_sanitize_ping_field`, whose docstring names both
the threat and the destination; verified 2026-09-04) and stored UNESCAPED
where the sink is a parameterised database column (`db/crud/email_threads.py`
`record_reply`; verified 2026-09-04). Unescaped is not unbounded, and an
earlier draft conflated them: what reaches that column is `body_preview`,
already cut to 500 characters upstream (`mail/reply_poller.py`,
`parsed.body[:500]`) — a silent mid-value cut of exactly the kind this section
exists to surface, sitting inside the example chosen to demonstrate the
opposite. The escaping point stands; the "canonical, unbounded" flourish did
not survive following its own pointer. Escaping at
ingestion would corrupt the stored copy; passing raw markup to a live
renderer hands a stranger the user's most trusted channel. Maximally
authorized destination, maximally defensive treatment AT THE RENDERER — any
rule that reads "it's going somewhere trusted, so pass it whole" deletes that
defence, and any rule that reads "a stranger wrote it, so escape it
everywhere" corrupts the stored original.

**And an authorized destination does not mean untreated.** The rule is narrow:
do not strip the USER'S OWN content on its way to the user. It is not a licence
to stop scrubbing on owner-facing surfaces, and this repo does not (each
verified 2026-09-04): it rewrites username-bearing paths out of what the owner's
own dashboard renders (`dashboard/routes/backup.py` `_scrub_reason`), gates
credential values behind an explicit reveal rather than showing them (the
References tab — that one surface, per the caveat above), applies the safe
mechanical rewrite to the draft the user reviews (`content/egress.py`
`should_gate`, `category == "content"` — non-fixable tells are FLAGGED, not
removed, so the copy they approve has been treated, not certified clean), and
in at least one subsystem deliberately keeps captured text out of its own
private store entirely (`attention/types.py`: "never stored";
`db/crud/attention.py`: "value-free … NO text"). "Into a private store" is
emphatically not a blanket exemption; some stores are built specifically to
never receive the value.

**One last thing, learned the hard way from this section itself.** Four review
rounds found defects in it, and every single one had the same shape: the cited
FACT was true, and the CONCLUSION drawn from it was false. A retrieval tool
really did lack an id lookup — and "therefore this is an amputation" was wrong,
because the value was stored whole elsewhere. A field really was written
unsliced — and "therefore it is a cheap place to record something" was wrong,
because reaching it aborted the whole run. Verifying that each cited fact is true
is the easy half, and it is not the half that fails. **State the inference as its
own claim, and check THAT.**

The sharpest instance is this section committing that error in the sentence next
to the one warning against it. A draft of the paragraph above cited a real file
that says, correctly, that a particular scrub applies to external audiences and
not to replies going to the user — and generalised it into "this repo leaves
owner-facing content untouched", which four other paths contradict. True fact,
false inference, two paragraphs from the rule forbidding exactly that. The
generalisation is the step to distrust, and it is seductive precisely when the
evidence under it is solid.

### A blocked compound command loses EVERYTHING in it

A PreToolUse block kills the **whole** Bash call, not the offending part — so a
guard firing on step 3 also silently discards steps 1 and 2, while the error
text talks only about step 3. This repo has many blocking guards
(`review_enforcement_commit`, `full_suite_guard`, `concurrent_test_guard`,
`git_push_guard`, the destructive/protected-path guards), so it is not rare.

**Never chain a state-changing step with a step that can be blocked.** Keep
`cd`, heredocs, file writes and restore-from-backup in their own invocation,
separate from test runs, commits, pushes, or anything a guard inspects.

**After any block, verify state before continuing.** Run `pwd`, and re-check the
file you believed you wrote. Do not assume the earlier half of the command ran.
Prefer `git -C <literal path>` over a persistent `cd`, so a lost `cd` cannot
silently redirect later commands. Do the same for scripts by spelling them
`$ROOT/scripts/…` (`ROOT="$(git rev-parse --show-toplevel)"`), which is what
`.claude/commands/deep-review.md` requires — a bare relative `scripts/…`
resolves against whatever the cwd drifted to.

**But the path is not what decides which worktree a script acts on. The PROCESS
CWD is.** Some scripts derive their target from the directory they run in —
`review_state.py`'s `evidence-path` and `mark` resolve the worktree via
`git rev-parse` with the inherited cwd, and never read `sys.argv[0]`. So run
those FROM the worktree they are about, in their own invocation, however you
spell the path. Get this wrong and review evidence is written under another
worktree's key, and the depth gate then blocks on a file you never wrote.

MEASURED 2026-08-28, with controls in both directions: from one cwd, the
relative, absolute and `$ROOT/`-prefixed spellings of `review_state.py` all
printed the SAME evidence path; the same absolute path run from three different
cwds printed three DIFFERENT ones. Path form: no effect. Cwd: decisive.

Measured cost in one session: four heredocs that never wrote, a
restore-from-backup that never ran (leaving a file deliberately regressed), and
a `cd` that never happened — so edits landed in the **wrong worktree** and had
to be reverted as cross-branch contamination. Guard false-positives make this
worse: `shell_parse` mis-parses backslash line-continuations and quoted heredoc
bodies, so legitimate commands get blocked too.

### Editing source in bypass/auto mode: reads via Bash, WRITES via Edit/Write

In bypass or auto permission mode the CC binary injects a meta message telling
the session to prefer Bash (`sed`, heredocs, scripts) over Read/Edit/Write —
and NOT only there: a third selector branch is a cohort assignment, so a
default-mode session can receive it too. It is an EFFICIENCY heuristic making no
correctness claim, which follows from how the message is phrased (a preference,
with its own escape hatch back to the dedicated tools) rather than from where it
is gated (provenance + full measurement:
`docs/reference/cc-compatibility.md` "Bypass/auto mode tells the agent to edit
via Bash"). For reads and search it is simply right — use `cat`/`sed -n`/`grep`/
`find` freely. For WRITES TO SOURCE it is not: a bare `sed -i` returns exit 0 on
an absent anchor (silent no-op), rewrites ALL matches of a non-unique one, and
silently hits the wrong line on a regex-metacharacter anchor where a literal was
meant — where Edit fails LOUDLY on each. A Bash write also fires none of the five
`Edit|Write` PostToolUse hooks (the repo's own post-edit verification plane) and
a heredoc sits in the blocked-compound blast radius above. MEASURED: 54.9% of
real edit anchors in this repo carry a character `sed` treats as special in BRE,
its default dialect (2,750 / 5,013; an ERE set scores 74.0% over the same
corpus, so quote the dialect or the number misleads) — the unsafe case is the
common one. Use Edit/Write for source; a script that does a literal replace AND
asserts its occurrence count is acceptable (it re-implements Edit's checks). This
is universal — the machinery is tracked, so every clone inherits it.

### Instance-Fix vs Class-Fix Gate

When a mechanism failed to write or propagate something (a memory, a
directive, a config row, a status flag), hand-writing the missing
artifact is a **data repair** — it mitigates ONE instance on ONE
install. It is never the fix. Before reporting anything as "fixed",
classify it:

- **Data repair** — you wrote the artifact the mechanism should have
  written. Label it "data repair" explicitly, and in the same session
  either fix the mechanism or get the user's explicit deferral (the mechanism
  fix recorded as an ISSUE; a local row only if the deferral itself needs
  tracking). Never report a data repair as "fixed".
- **Class fix** — you changed the mechanism so the artifact is written
  correctly on every install, going forward, with a test proving it.

The test: "If a fresh install hits the same situation tomorrow, does my
change help them?" If the answer is no, you have repaired data, not
fixed anything. (Origin: 2026-07-17 — a stale-decision recurrence was
"fixed" with a hand-written memory + directive; the propagation
mechanism that failed to write them stayed broken.)

### Debugging Discipline (phase-gated)

Adapted from superpowers `systematic-debugging`. "Find root causes" is a value;
these are the GATES that make it enforceable:

- **Iron Law: no fix proposals until root-cause investigation completes.**
  Investigation (read the full error, reproduce, check recent changes, gather
  evidence) is a phase that FINISHES before any fix is proposed — never propose
  fixes in the same breath as the symptom. "It's probably X, let me fix that"
  = investigation skipped.
- **Fix-attempt cap: 3 failed fixes → STOP and question the architecture.**
  The debugging twin of the review escalation cap, with the same mechanics
  (count attempts visibly; the cap consumes standing approval). Each failed fix
  revealing a new problem in a different place is not bad luck — it is the
  signature of a wrong architecture or a wrong problem statement. Do not
  attempt fix #4; bring the pattern to the user.
- **Date the code before classifying a red — a stale tree fabricates live
  blockers.** MEASURED 2026-09-02: the main worktree sat at ONE commit from
  09-01 13:40 to 09-02 19:06. A test run against it at ~18:05 failed
  reproducibly and was reported as a live repo-wide blocker; it had been fixed
  at 15:47 that day by a merged PR. The failure was real, reproducible, and
  describing state that no longer existed. Nothing warns you — `git log --all`
  and `git status` both work perfectly on a stale tree, and `git log --all`
  even SHOWS the fix, because the fetch is fine and only the checkout is old.
  Before calling any red live, FETCH AND COMPARE REFS —
  `git fetch origin main --quiet` then
  `git merge-base --is-ancestor origin/main HEAD`. Do NOT date the code
  (`git log -1 --format=%ad -- <file>`) or read the reflog: neither compares
  the checkout against current `origin/main`, and both mislead in BOTH
  directions — a current tree holding an unchanged old file looks stale, and a
  stale branch carrying one recent unrelated commit looks current. Dating it
  can therefore reproduce the exact false blocker this bullet exists to
  prevent.
  **The same trap applies to your TOOLS, which is easier to miss**: a worktree
  carries its own copy of `scripts/`, so a script run from an old branch is the
  OLD script. MEASURED the same day — `git_push_guard.py --check-pr 1611` from
  a days-old worktree reported `ci: pending` where the current copy reported
  `ci: green`, same PR, same minute. Run repo tooling from a tree at
  `origin/main`, not from whatever branch you happen to be on — and verify
  that with EQUALITY, not the ancestry test above:
  `[ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ]`. Ancestry is
  satisfied by any branch that merely CONTAINS main, including a PR branch
  that MODIFIES the tool — which is precisely the case where the verdict
  differs and the one you must not run.
  *"Verify against actual code" needs the companion "verify against actual
  CURRENT code."*
- **Boundary instrumentation for multi-component failures.** When the path
  crosses components (hook → server → engine; CI → build → deploy), don't
  reason about where it breaks — LOG entry/exit at each boundary, run ONCE,
  and let the evidence localize the failing component before investigating it.
  (The "starved vs broken" check — inputs before regression-hunting — is the
  special case of this.)
- **Read the reference implementation COMPLETELY before deriving from it.**
  When a change must mirror what another subsystem does (what routing consumes,
  what the runtime resolves, what a protocol expects), read that subsystem's
  path END-TO-END first and derive from its own code/loader — never
  incrementally guess-and-patch toward it. Incremental spec discovery is how a
  review loop runs 7 rounds. (Origin: PR #1281 — the onboarding floor's key
  list was wrong three times until it was derived from the router's own
  `load_config` + call-site chains.)
- **Spec required-sets: enforce the WHOLE set at once, and lock it with ONE
  test.** The spec-facing corollary of the rule above. When a change must make
  code satisfy a canonical spec's *required-set* — a prompt's required JSON
  fields, a validator's mandatory keys, an allow-list — read the spec's required
  list in full and enforce ALL of it in one move; then write a single test
  asserting the required block == the complete canonical set (not one assertion
  per field). Adding only the field a reviewer just flagged leaves the next one
  for the next round, and a per-field test goes green while the next missing
  field ships. (Origin: PR #1333 — `_SALVAGE_PROMPT`'s Required block was
  completed one field at a time across three Codex rounds — cognitive_state_update
  → confidence → observations — for what `REFLECTION_DEEP.md` declares as one
  closed set `{observations, confidence, cognitive_state_update}`.)
- **Condition-based waiting.** When a fix or test must wait for a state change,
  poll the CONDITION (with a bounded deadline), never sleep an arbitrary
  duration — arbitrary sleeps are flaky under load and slow everywhere else.
  This complements the Timeout Policy (which governs the values).

### Test-First Discipline

Adapted from superpowers `test-driven-development`, scoped to where it pays:

- **Bug fixes: failing reproduction test FIRST — always.** Before touching the
  code, write the minimal test that reproduces the bug and WATCH IT FAIL for
  the expected reason. Then fix; the same test proves the fix and pins the
  regression. A repro test written after the fix proves nothing (it never
  caught the bug).
- **Verify-RED, always and everywhere.** Any new test must be seen to FAIL
  (correctly) at least once — via the bug, a reverted fix, or a deliberately
  broken assertion — before its green is trusted. A test that has only ever
  passed may be testing nothing; a whole suite passing every review round
  while a reviewer keeps finding real spec bugs is the tell that the tests
  encode the same wrong spec as the code.
- **A RED that comes back GREEN has AT LEAST six causes, and "the test is
  vacuous" is the LAST one to reach for.** In rough order of how often they
  actually occur:
  1. **The run never EXECUTED** — a guard refused it, a lock held it, the tool
     timed out — so there is no result at all. This is the CONFIDENT FALSE
     NEGATIVE: a sweep reporting "all mutations survived" is far more often a
     sweep that never ran. Make the runner ABORT when the test command emits no
     result line, and treat a SKIPPED/deselected line FOR THE TEST UNDER
     VERIFICATION the same way — it is a result line, and it still means
     nothing ran. Scope that check to the target: a suite carrying legitimate
     `skipif` tests emits SKIPPED lines on every healthy run, so a runner that
     aborts on ANY of them refuses every run — and the agent then either sits
     blocked or starts stripping skip markers to unblock itself.
  2. **The MUTATION silently failed to apply** — the auto-formatter reflows
     lines and a `str.replace()` anchor written from memory then matches nothing.
  3. **The test ran against a DIFFERENT COPY of the code** — an installed
     package shadowing the source tree, a stale `.pyc`, the wrong virtualenv, or
     (measured, this session) a path relative to a process whose cwd had moved to
     another worktree. The mutation applied, the run happened, the test is sound,
     and none of the other causes fits.
     **A test module can manufacture this for everyone else**, which is the
     variant that hides longest: loading a script under a SHARED `sys.modules`
     name and not restoring it. pytest imports every test module at COLLECTION,
     so the last registration wins for the session — a module collected earlier
     keeps the object it bound while production code doing a call-time
     `from <name> import …` resolves the newer one, and
     `monkeypatch.setattr(<module>, …)` then patches nobody. It passes in a
     single-file run and fails only in the full suite, in collection order, so it
     reads as a bug in whichever test depended on the patch. MEASURED on main:
     four such modules, across `review_state` and `review_scope` — both imported
     at call time by the commit gate. Use `tests.conftest.private_module`, which
     registers, execs and restores; never hand-roll the sequence.
  4. **The mutation was BEHAVIOURALLY NULL** — it applied and parses, so every
     postcondition below passes, but it changed no behaviour: swapped operands
     that commute, an edit inside a dead branch, a type annotation Python does
     not enforce (also measured this session). The remedy is a different
     MUTATION, not a different test.
  5. **A SIBLING LAYER still enforces the invariant**, so the green is correct.
     When two layers produce the same behaviour, mutate the WHOLE mechanism, not
     one of its halves. (The duplicated-layer anecdote in the review-loop section
     is the worked example.)
  6. **A SIBLING TEST LEAKED STATE that masks the mutation** — an undone
     `monkeypatch`, a stray `os.environ` entry, a mutated singleton or a
     module-level cache — so the mutated path is never reached in THIS run.
     It matches none of the five above: the run executed, the mutation applied
     and is not behaviourally null, the code is the right copy, and no
     production layer is enforcing anything. MEASURED in this repo: a leaked
     disable lever made the very lock under test a no-op, and five real
     failures read as a story about the mechanism instead. The remedy is test
     ISOLATION (an autouse fixture that clears the lever) — NOT a different
     mutation, and NOT a different test.
  Only after all six: the test is vacuous. The list is ordered and still not
  closed — if none of them fits, the vacuous conclusion is UNPROVEN rather than
  established: look for the cause you have not modelled before rewriting a test
  that may be sound, because rewriting a sound test is the expensive mistake
  here.
  The PRINCIPLE, which is what to remember:
  **every injection must prove it applied, by its own postcondition, before any
  result is read as RED.** Two corollaries follow, and both have bitten:
  prove it against the value THAT injection was handed, never against the
  pristine original — with two or more edits, the first edit keeps a whole-file
  `mutated != original` true while a later one silently misses its anchor, so
  the partial mutation reads as complete (assert the anchor matched the expected
  number of times); and prove it with the mutated file's OWN parser —
  `compile(src, path, "exec")` for Python (NOT `ast.parse`: that only builds a
  tree, so it accepts context-invalid constructs like a `return` moved outside a
  function or a `break` outside a loop, and the `SyntaxError` then surfaces at
  COLLECTION, where a nonzero exit reads as a successful RED), `bash -n` for
  shell — since an invalid mutation breaks collection and reads as a successful
  RED, while the wrong language's parser
  rejects a valid mutation and hides a real survivor. Restore from a file copy
  taken beforehand, never `git checkout` — the work is uncommitted — and make
  the restore ATTEMPT unconditional ON THE RUN'S OUTCOME (`trap restore EXIT`, a
  `finally:`), never the tail of an `&&` chain. Unconditional means it always
  RUNS, not that it always OVERWRITES: what it writes is still gated on the hash
  check below. A `trap` that restores blindly is the very thing that destroys a
  concurrent edit. The expected outcome here is a NONZERO exit, and under
  `set -e` a trailing restore is exactly the statement that never runs (the
  `out=$(cmd)` entry in Common Traps is the same mechanism), so the shape that
  reads as careful leaves a deliberately-broken file in an uncommitted worktree
  on the ordinary path — as well as on interruption or a tool timeout. Take
  that copy ONCE for the whole sweep and refuse to start EACH cycle if the file
  already differs from it — a copy re-taken immediately before each mutation
  makes the check vacuous, since the file trivially matches a copy a moment
  old; the baseline exists to catch a PREVIOUS cycle that failed to restore, or
  a concurrent edit. Verify the restore by hash rather than assuming it. And
  restore ONLY what you broke: compare against
  the hash the MUTATION wrote before overwriting, because between the mutation
  and the handler another session, agent or formatter may have edited that file,
  and a blind snapshot restore silently destroys their uncommitted work — a
  final hash check does not catch this, it only confirms the overwrite
  succeeded. If the file no longer matches what the mutation wrote, PRESERVE it
  and report the conflict instead. The cleanest way to avoid the window entirely
  is to mutate inside an isolated worktree nobody else is editing.
- **A RED for the WRONG REASON is not a verify-RED — read WHICH assertion
  fired, never just that one did.** The six causes above all ask why an
  expected RED came back GREEN. This is the mirror, and the dangerous half of
  it passes review: the run failed, so the checklist is satisfied and the test
  is trusted, but it failed on something other than the defect. **The tell is
  any failure line you have not actually read.** Two shapes, and they cost
  differently:
  1. **The PRIMARY assertion fired, but on a condition the FIXTURE created**
     rather than the defect — a leftover competing candidate, an unaged file,
     stale state from setup. This one goes green on the fix, because the fix's
     side effects remove the condition, so it passes review and pins nothing.
     This is the expensive one.
  2. **A guard-the-guard / precondition assertion fired FIRST**, which proves
     the fixture never built the hazard at all. This one does NOT go green on
     the fix — the fixture is unchanged, so it fails again and says so. It
     costs a round, not a false green, and the guard is doing exactly its job
     (see the vacuous-shapes bullet below, which prescribes adding one).
     Repair the fixture and re-run before recording a RED.
  MEASURED twice in one session on the same PR: a fileless-session test whose
  file was not yet aged past the 60-second window, and a selection test with a
  competing candidate still in the tree — each reported a textbook RED while
  exercising nothing. Name the assertion that fired and confirm its message
  describes the defect. A RED you have not read is a GREEN you have not earned.
- **A fixture must create the shape its docstring claims — and the check is the
  guard-the-guard assert, not a re-read.** This is the same obligation as the
  vacuous-shapes bullet below; the addition here is WHERE the shape most often
  goes wrong, which is a contract between two components. Two ways to break it,
  both MEASURED in one session, and both PASS against the unfixed code — which
  is the only way they are caught: a **hand-written intermediate** (the test fed
  the consumer a value the real producer never emits, so the arity mismatch it
  claimed to pin was never present) and an **inlined copy of the fix** (the test
  ran the corrected logic as a snippet instead of invoking the real script, so
  it graded its own copy and the shipped file never executed). If the defect
  lives in the seam between a producer and a consumer, drive the REAL producer
  into the REAL consumer; anything typed by hand in the middle is the bug's
  hiding place, and anything inlined is a second copy of the code under test.
- **Two reviewers pushing OPPOSITE values for the same constant, one review
  pass apart, is a signal about the DEPENDENCY, not about the value.** (Review
  passes here, not the cross-model *rounds* the escalation cap counts — see
  that section for the reserved sense.) The instinct is to adjudicate: measure
  the box, pick the winner, move on. That answers the pass and leaves the
  mechanism, because both reviewers are usually right about the environment
  each has in mind — which means the code depends on something that is not
  invariant. MEASURED: one reviewer moved a parse to field `$4`, the next moved
  it to `$5`, and both were correct, for different releases of the tool being
  parsed. The value was never the defect; depending on column POSITION was.
  Remove the dependency — match the field by what it IS, derive the index, or
  read a named output format — which is usually cheaper than a third pass of
  picking a number. **Scope: this applies where the constant names a POSITION
  or a shape in something external** (a field index, a column offset, an offset
  into a format you do not control). It does NOT apply to a threshold or a
  timeout: there the disagreement is about which failure mode each reviewer has
  in mind, and the Timeout Policy above governs — name the failure mode, and
  the value follows from it.
- **Vacuous-test shapes to check for by name** (a list of the common ones, not a
  definition). The most frequent in practice is the one that never ran at all: a
  test SKIPPED by a marker, or deselected by a `-k` filter or a wrong path, which
  reports SKIPPED or "no tests ran" and never goes red — check the count, not just
  the absence of failures. Beyond that, a test is vacuous when: its
  assertion is ALSO true on the success path (`assert x.blocked is False` where
  a successful call also returns False — assert the fact that DISTINGUISHES
  them); its setup short-circuits the path it names (passing an explicit
  argument the code prefers over the env var under test); or its fixture never
  creates the shape it claims (a `bash -c 'sleep 30 # marker'` decoy
  exec-replaces itself and loses the marker from its argv — add a
  guard-the-guard assert that the fixture really has the property); or the code
  under test HEALS the condition before the guarded step runs (an earlier
  `git diff` refreshes the index as a side effect, so a test of an
  index-refresh guard passed with the refresh deleted). To test a guard against
  a transient condition, inject the condition AFTER the last step that could heal
  it, and prove it with a mutation. Ask of every
  new test: *would this still pass if the mechanism it names were deleted?*
  When the DISTINGUISHING fact is produced by a lane the assertion cannot see,
  asking every test to assert it is a convention, and conventions decay — the
  merge gate's own suite carried that request in a fixture docstring and 3 tests
  had already drifted from it. `tests/test_hooks/conftest.py`'s `offdiff_lock` is
  the worked chokepoint: it fails any IN-PROCESS hook test whose finding was
  silently discounted as outside the diff, so a forgotten allowlist entry can no
  longer turn a not-block assertion green for the wrong reason. It fails OPEN,
  so its own self-tests are the load-bearing part — a lock nobody checks is the
  convention it replaced.
- **Contested/subtle specs: write the expectations first.** When what-should-
  happen is itself under discussion (which keys count, which states clear an
  alarm), enumerate the expectation table as failing tests BEFORE implementing
  — it forces the spec question to surface at design time instead of review
  round 4.
- **Corpus replay cannot find a false-POSITIVE class — generate the matrix.**
  Replaying real recorded inputs proves only that shapes you have ALREADY run
  still behave; it is structurally blind to a shape you have never typed. A
  guard change measured "0 false positives across 18k real commands" and still
  hard-blocked an ordinary command, because the corpus happened to contain no
  instance of the one shape that mattered: a construct the parser mis-handles
  that ALSO leaves the guard with no parsed segment. For any classifier
  or guard, enumerate the CROSS PRODUCT of the axes that actually drive the
  decision — {operation} × {the constructs your parser can mis-segment} ×
  {evidence present, absent} × {interactive, unattended} — and assert the invariant per
  cell, so an untested cell fails
  loudly instead of silently. Keep the corpus replay as the realism check; the
  generated matrix is the coverage check FOR THE MODEL YOU DECLARED, which is
  the most it can be — a construct you never thought of has no cell to skip and
  so passes in silence, which is the same false confidence one level up. Naming
  it "the coverage check" without that qualifier is the trap. Discovering new
  axes is a different instrument: differential or property testing against a
  canonical parser, which fails on shapes nobody enumerated. Skip a cell only
  with an explicit reason recorded in the skip, since a silent skip and a hole
  look identical.

  **Generate the cells from the GRAMMAR of what the rule flags, not from
  history.** The axes above are the shape; this is where they come from. MEASURED
  2026-09-09, after three corpus-based measurements had each missed a real
  regression: enumerating {program} × {option form} × {construct} × {position} —
  the axes the rule itself keys on — and running EVERY cell through real `bash`
  via an argv-printing shim AND through the real guard subprocesses on BOTH trees
  produced 746 cells and, more to the point, a control that MOVED: `main` 744
  ALLOW / 2 BLOCK, PR head 339 / 407, after the fix 534 / 212. That surfaced 206
  over-blocks which a 129,179-command corpus had shown none of. A second sweep
  built 384 cells over the same four axes. Run the matrix against main as well as
  against your branch, and compare it PER CELL, keyed by input — never by the
  aggregate counts. Two cells can swap ALLOW and BLOCK while the totals sit
  identical, and a resource-only fix legitimately preserves every verdict (the
  next bullet is exactly that case), so a matching distribution proves nothing in
  either direction. Establish liveness separately: use a deliberately mutated
  control or another known outcome that the harness must distinguish. A correct
  resource-only fix can preserve every production verdict, so it need not make
  the two production trees differ.
  **Instrument the probe before you trust 746 of anything.** These guards emit
  ASK as JSON on stdout with EXIT CODE 0, so a probe reading only the return code
  cannot tell ASK from ALLOW — and ASK is precisely the state a newly added flag
  usually produces, so the sweep reports "no change" while the new flag fires on
  every cell. Combine exit status with the JSON payload when status is zero:
  BLOCK is the non-zero gate result, while ALLOW and ASK are distinguished by
  the payload. Prove the harness can distinguish each outcome on known inputs
  before believing any aggregate it prints.
- **A correctness test cannot see a RESOURCE defect — and on a path the harness
  can SIGKILL, that defect is a bypass.** Verifying a change three ways is ONE
  verification when all three ask the same question. MEASURED 2026-09-09 on an
  in-flight fix that added a new scan to the shell parser the hook stack shares:
  it was checked at the parser level, at the guard level, and against the
  specific exploit it existed for — three passes, all CORRECTNESS, all green —
  while the scan itself was quadratic in the length of a SINGLE token, the
  ordinary accidental-O(n²) shape of a nested scan with no early exit. On the
  linear form it held flat at 0.004s (4ms) at every length; on the pathological one
  it ran to SECONDS, then minutes, at input sizes a caller can simply type. A
  PreToolUse hook that overruns its wall-clock is SIGKILLed, and a killed hook
  fails OPEN — the guard code says as much in its own comments — so a scan that
  can be made slow enough disarms the gate rather than tripping it, and because
  the parser is shared it does not stop at the one guard being edited. A FOURTH
  correctness check would have passed too.
  So, for any code the harness can kill: ask the three questions the correctness
  suite never asks — what is its complexity, what input maximises it, and what
  does the system do when it does not finish. Where "does not finish" means
  PERMIT, a super-linear scan over attacker-influenced input IS the defect,
  however correct its output. Measure its worst-case runtime in a controlled
  performance probe, and keep a deterministic regression test for the bounded
  algorithm in CI; a wall-clock assertion in an install-agnostic test is not
  reliable. Record the measured margin against the smallest timeout on its path.
  (The Generalizability
  Gate's "retention machinery does not belong on a hook path" is the same
  mechanism met from the other end: work that scales with the input, on a path
  whose deadline is a security boundary.)
  **This bullet is deliberately imprecise, and that is the second rule in it: a
  lesson about a vulnerability does not need the vulnerability's parameters.**
  The shape teaches — super-linear on attacker-chosen input, measured at seconds
  where the linear form completes in milliseconds, on a path that fails open. The exact
  input sizes, the timeout tier they defeat, the count of guards behind the
  shared parser and the quoted fail-open line teach nothing further; they only
  compose into a working recipe, and one that keeps working for every install
  still on the pre-fix code. Nor does "the fix has landed" license the specifics
  — check whether it landed on `main`, not whether a PR for it exists.
- **Anti-patterns (binding):** never assert on a mock's behavior when the real
  code path can run; never add test-only methods/branches to production
  classes; fakes implement the real contract (real method names, real return
  types — import them). Test setup so complex it needs its own debugging =
  the design is too coupled; fix the design.

### Code Intelligence — pick the right lane

**Serena (Python LSP) is always live** — it parses current files per query, so
it's the default for symbol/reference/impact questions ("who calls X", "what
breaks if I change Z") and never goes stale. CBM gives the architecture/graph
overview. GitNexus does what neither can — multi-hop blast radius, execution
flows, route/tool maps, coupling/community analysis — but it is **snapshot-
based**: its answers are only correct when the index matches the working tree,
and it drifts after you pull merged PRs (its reindex fires on local commit, not
on pull). So reach for GitNexus deliberately for its unique views, and run
**from the main checkout, run `scripts/lib/code_intel_index.sh "$PWD" gitnexus fast` first** when freshness matters. Linked worktrees deliberately skip indexing; for live "who calls this"
during active editing, prefer Serena. There is no "always run impact before
every edit" mandate — that just gates work behind a tool that's stale-by-design.

- **Blast radius / impact:** Serena `find_referencing_symbols` (live) for the
  direct caller set; GitNexus `impact <symbol>` (reindex first) for multi-hop +
  affected processes/risk. Use the full UID if ambiguous
  (`Method:path/file.py:Class.method#N`).
- **Unfamiliar code:** `gitnexus context <symbol>` or browse
  `gitnexus://repo/GENesis-AGI/processes` (when fresh).
- **Custom questions:** `gitnexus cypher` — LadybugDB uses `CodeRelation` with a
  `type` property for edges, not Neo4j-style named edge labels.

Full syntax and Cypher examples: `.claude/docs/code-intelligence-guide.md`;
tool-selection decision matrix: `.claude/docs/code-intelligence.md`

### Advisory is the default; a BLOCK is what needs the argument

Before the fail-direction question below, there is a prior one, and it is the
one that gets skipped: **should this refuse at all?**

**Advisory is the default and the first step.** Escalating to a BLOCK needs a
specific, credible, MEASURED reason — the risk is that severe, or that frequent.
*"For safety"* and *"defense in depth"* are not reasons. Neither is having just
been bitten once: an n=1 incident earns a COUNTER, not a gate.

**Persisted state inside a guard needs its own justification, separate from
the decision to refuse.** Before a guard or gate grows a marker, cache or
counter that one invocation writes and a later one reads (a telemetry counter
no guard decision reads is observability, not guard state), show why the answer
cannot be derived at the moment it is needed from state that already exists,
and enumerate the lifecycle the state adds — write, read, retire, validate,
scope — because each stage is a defect site. Where a derivation exists, a second
should-fix-or-worse finding in machinery the change adds means delete it, not
harden it; where none exists, narrow the lifecycle. Approval records are out of
scope for deletion. MEASURED on the gate-menu feature at comparable size: the
persisted-marker layer drew roughly 15 should-fix-or-worse findings against
roughly 3 in the half it served (179 vs 155 non-comment lines, per #2027's PR
body); the marker designs were closed (#1863, #1999) and the counter-based one
merged (#2027). This prices the cost of STATE, not the case for gating; the
New-Store Gate below asks the neighbouring question (why a NEW store rather than
an existing one), and the `genesis-architect` agent's Step 0.7 asks this one at
review time.

**Fail-open is not an automatic defect — but say WHICH question you are
answering, because there are two and they get opposite defaults.** (1) The
VERDICT question: when a guard evaluates successfully, should its design be an
advisory or a block? Advisory by default, per the above. (2) The DEGRADE
question — what a guard does when it CANNOT evaluate — is decided per boundary
by the section below, and many of this repo's Bash hooks are REQUIRED to fail
closed there by the degrade-direction contract test
(`test_every_bash_hook_declares_its_degrade_direction`); nothing in this
section loosens that, and satisfying it with a false advisory claim in the
exemption list is the named failure, not compliance. The claim here is about
(1): a control that fights the operator daily against a harm that is usually
hypothetical is a cost, not a defense. Ask which direction THIS boundary should
fail in — open, closed, or open-and-loud — from consequence, never from which
one sounds safer.

⚠ **This governs GUARDS — advisory nudges and friction hooks — never APPROVAL
BOUNDARIES.** Anything that gates autonomy expansion, spending, destruction of
data, or publication to an external surface is a sovereignty boundary, not
friction to be tuned: never a downgrade candidate, and proposing to relax one
is itself the failure. The autonomous-CLI gate, ego proposals, per-transaction
financial approval and destructive-command confirmation are canonical members
of that class, not an exhaustive list — classify by what the boundary GATES,
never by whether it appears here.

Two consequences worth stating, because they are easy to get backwards:

- **Blocking stops foreground and background equally; advisory stops neither.
  Both are fine.** What is NOT fine is an ask that reaches an UNATTENDED
  session — an ask with no human present is a block nobody intended — or a
  block that impedes background work nobody meant to impede. An ask leg is
  legitimate exactly where the shipped ones sit: at an approval boundary in a
  foreground flow, degrading to a DEFINED verdict when dispatched (the push
  guard's ask converts to deny under `_is_dispatched()`) rather than hanging.
- **A guard is written for the agent, not the user.** Nine times in ten the user
  need not know it fired.

This is a design axiom, not a preference: changing it is a redesign
conversation, not a session's call.

### Guard failure semantics — the third option is fail OPEN, LOUDLY

Every guard has TWO INDEPENDENT axes when it cannot evaluate, and collapsing
them into a single "fail-open or fail-closed" choice is what produces the bad
design:

- **DIRECTION** — what happens to the DECISION: open (the action proceeds) or
  closed (it is refused). Decided per boundary, by consequence.
- **VISIBILITY** — what happens to the FAILURE: silent, or recorded where a
  later reader will actually find it. **Silent is never correct, in either
  direction.**

The third option is the one that gets forgotten: not open, not closed, but
**open and LOUD** — the action proceeds AND the guard's inability to evaluate
becomes a durable record plus an alert.

**THE TEST IS THE REPAIR PATH, and it is measurable — do not reach for a
judgement about severity.** Ask: with this failure present, can the session
still REPAIR the failure? Name the operations repair needs — read a file, edit
it, restore it, restart a unit — and check whether your refusal predicate
matches any of them. If the repair path survives, fail-closed is available. If
it does not, fail-closed is not "safer", it is a BRICK, and the answer is
open-loud however severe the thing you wanted to stop.

The test has a worked precedent, and it is worth reading for the SHAPE of the
argument rather than as a verdict. PR #2042's body justifies its fail-closed leg
by naming what survives — *"Read/Edit/Grep are ungated and `git checkout --
<file>` matches none of the matchers"* — which is exactly this question, asked
and answered with a measurement. (The quote is from the PR BODY; the function's
own docstring makes a related but weaker claim, so cite the PR, not the symbol.)

Two cautions travel with that precedent, both MEASURED, and they are why the
test is a question you re-ask rather than a conclusion you inherit. First, an
ungated repair verb is not automatically a SAFE one: in that same degraded state
`git_discard_guard` no longer takes its recovery snapshot, and its own
`also_lost` notice says so — *"a discard run now is not recoverable from it"* —
so `git checkout` being unmatched means it RUNS, not that it is safe to run
broadly. Second, the precedent covers only ONE of that helper's two failure
legs; the other is the worked example below of the test returning the opposite
answer.

**SYSTEMIC failure means no usable predicate survives**, so any refusal would be
TOTAL and the repair path closes with everything else — typically because the
machinery that runs guards is dead (the wrapper, the interpreter, the venv), or
because one missing dependency hits every guard at once. **There the default is
open-loud** (standing owner ruling, 2026-09-15). Genesis runs headless: the
interactive session IS the repair path, and the host Guardian is the
repair-of-the-repair. A guard layer that fails closed at that level does not
protect the machine, it bricks it, with nobody at a console to type the fix. A
messy action Genesis can clean up afterwards beats a system that can no longer
act at all.

Note what does NOT decide this: "how bad was the cause". A single missing
dependency produces EITHER regime, and the same helper contains one of each —
which is the clearest worked example in the repo, so apply the test to both:

- **`degraded_exit`'s predicate-gated leg** — a shared sibling (`shell_parse`)
  is unimportable, so the guard refuses only what its degraded MATCHER names.
  Read/Edit survive, `git checkout` is unmatched, the rates below are what it
  costs. Repair path open ⇒ **PARTIAL**, and fail-closed is licensed.
- **`degraded_exit`'s unconditional leg** — `hook_input` ITSELF is unimportable
  or version-skewed, so nothing the guard could import can recover it and each
  guard `os._exit(2)`s with no payload read and no predicate consulted.
  **The size of that refusal is no longer written down here, and that is the
  point.** It is DERIVED on every test run by
  `tests/test_hooks/test_import_time_degraded.py::test_every_bash_hook_declares_its_degrade_direction`,
  which parses `.claude/settings.json` (never greps it), runs every hook whose
  matcher can fire on Bash against a poisoned `hook_input`, and asserts the RULE
  the number was only ever evidence for:

  > A hook that can fire on Bash either carries a degraded handler — emits `GUARD
  > DEGRADED`, exits 2 — or is NAMED: in `_ADVISORY_BY_DESIGN` with words verified
  > verbatim against its own docstring, or in `_NOT_PYTHON_ON_BASH` because it
  > never imports the module. There is no third bucket.

  Polarity is ALLOWLIST, so a guard wired next year that forgets its handler fails
  by construction instead of quietly ceasing to guard. That matters because the
  omission is invisible in production: Claude Code treats a non-2 exit as a
  non-blocking error **when the hook emits no `permissionDecision`**, so a guard
  that dies on its import traceback exits 1, the command runs, and nothing
  distinguishes it from a guard that looked and approved. (The qualifier is not
  pedantry: an exit-0 hook carrying `permissionDecision: "ask"` does gate, and
  this repo has one — so "non-2 means non-blocking" is true of the degraded leg,
  where that channel is unreachable, and not of hooks in general.)

  **THE POPULATION FILTER IS WHERE THIS KIND OF GATE FAILS, not the assertion**,
  and the first version of this one proved it. It compared `matcher != "Bash"` —
  an exact-string test against a field Claude Code treats as a REGEX — and
  discarded any command its narrow pattern could not parse. An adversarial audit
  broke it four ways against mutated copies of the real settings: a hook wired
  `"Bash|Edit"` was invisible, `".*"` was invisible, and an EXISTING blocker
  respelled as a bare `python3 …/guard.py` silently left the population with the
  suite still green. `procedure_advisor.py` is wired on `".*"` in this repo, so
  that was a live gap, not a future one. An enumerator that silently drops what it
  cannot parse is a DENYLIST one level up. The fix is the shape
  `test_hook_output_contract.py::_resolve` already uses: evaluate the matcher as a
  regex, and make an unresolvable command a FAILING row rather than a skipped one.

  Two limits remain, stated rather than assumed away: the enumeration is scoped to
  the REPO settings, so a user-level `~/.claude/settings.json` can wire more on the
  same matcher invisibly (MEASURED on one install: it wires a shell hook on
  `Grep|Glob|Bash`); and shell hooks on the matcher never import `hook_input`, so
  they are exempted BY NAME rather than left unseen.

  **Why a count became a rule — four failures, and only three were mistakes.**
  `grep degraded_exit(` finds six callers, and an early draft therefore said "six
  guards"; but this leg is precisely the one where `degraded_exit` is UNREACHABLE,
  so every refusing guard refuses from its own import handler and grep sees only
  those that ALSO call it on the other leg. Count the condition, never the helper.
  Correcting it to nine then left the denominator silently narrowed to the subset
  actually executed — **a figure written as "N of M things wired on X" has to
  re-enumerate X's full population programmatically or it ships one unstated
  restriction copied verbatim into every surface that quotes it.** Naming what M
  excludes is the fix; adjusting M is not.

  And then the fourth: the corrected figure went stale within a day with nobody
  being wrong at all, because another PR wired one more hook on the same matcher.
  A denominator maintained by hand in four places is not a measurement, it is four
  chances to be out of date — which is why the rule is asserted and the number is
  computed. **Two limits stay stated rather than assumed away:** the enumeration is
  scoped to the REPO settings, so a user-level `~/.claude/settings.json` can wire
  more on the same matcher invisibly; and the matcher also carries shell hooks,
  which never import `hook_input` and so cannot be affected — one of which is
  itself a blocking guard.

  **Bash alone is not the question the test asks** — what matters is whether ANY
  route to repair survives, and Write/Edit is one.

**THIS TEST FOUND A REAL BRICK ON ITS FIRST APPLICATION, and the fix is the
worked example of what it is for.** `scripts/pretool_check.py` carried the same
unconditional block on `Write|Edit`, so the session could neither run a command
NOR edit a file: repair path CLOSED, SYSTEMIC, on a headless box. And that
refusal was a strict OVER-block rather than a conservative one — this guard
blocks CRITICAL-path writes in DISPATCHED sessions only, an interactive session
is allowed through by design, and its healthy session test
(`os.environ.get("GENESIS_CC_SESSION")`) needs nothing from `hook_input`. It was
refusing a category it was built never to refuse, and that category is the one
that performs repairs.

So the degraded block now asks the guard's own scope question with the one input
that cannot fail: a DISPATCHED session still refuses (nobody is present to
approve), an INTERACTIVE one is allowed with a loud notice. The security delta
is ZERO and is locked by a test rather than asserted — the healthy guard already
permits exactly that call, so restoring it surrenders no protection that existed.
Bash stays refused, which is the point: fail-closed keeps everything it was
protecting, and the session keeps the edit that repairs the tree.

**A DISPATCHED session remains SYSTEMIC by this same test** — Bash and Write/Edit
both refused, repair path closed — and that is the intended direction rather than
an oversight: an unattended session is not who should be self-repairing a broken
guard tree, and its repair route is a human interactive session, which is exactly
what this change makes usable again.

**The general rule, which is why this sits in the doctrine and not only in a
changelog:** when a fail-closed leg has no predicate, do not ask whether the
refusal is severe — ask which repair routes it closes, and leave one open on
purpose. Two other routes exist here and NEITHER is a design: an MCP editing
tool is matched by neither matcher, and the host Guardian can `REVERT_CODE` from
outside the container, but nothing currently WAKES it, because the container is
up and `genesis-server` never imports `hook_input`. A repair route that survives
by accident is not a repair route; name it and WIRE it.

**PARTIAL failure means a usable predicate survives**, and fail-CLOSED stays
correct there — `run_guard` is the other merged instance (an uncaught crash in
an irreversible-action guard exits 2 instead of the exit 1 CC treats as
non-blocking). What licenses them is MEASURED. Six guards are wired to
`degraded_exit`, and their predicate-gated matchers refuse between **1.90% and
21.47%** of 74,282 unique real commands individually; PR #2042 measures the
UNION of four of them at **28.80%**, which it headlines as "roughly 29% of
ordinary work is refused while the tree is broken".

Read that union as the shape of an operability budget, NOT as its value: the
other two wired guards are outside it, so the real six-guard figure is unmeasured
and strictly higher. A seventh guard does not get to add its rate to 28.80% and
call the result the cost — that arithmetic is the population error this file
keeps catching. Measure the union you actually have. What each protects earns its
share: irreversible for `worktree_cwd_guard`, merely heavy for
`full_suite_guard` — a distinction taken from PR #2042's own table rather than
assumed, and a reminder that "what it protects" is a mixed bag even inside one
wired set.

`background_pipe_guard` is the one that was deliberately NOT wired: its degraded
matcher would refuse **70.30%** of that same corpus, and a guard refusing the
majority of ordinary work does not make the broken state safe — it makes it
unrepairable, which is the brick again by another route.

**Ambiguity about WHICH REGIME you are in resolves toward OPEN-LOUD**, because
that direction's worst case is a recorded mess and the other's is a system
nobody can reach.

**This section decides the REGIME; it does not decide the direction within
one.** Once you have established PARTIAL, the per-boundary mandate under "Never
hand-roll `gh`/bash/CLI argv parsing" and its corollaries (a)/(b)/(c) govern
which way that guard fails — and corollary (c) settles the unattended-session
case in the direction of REFUSING, after three narrowing rounds on one
predicate. Nothing here loosens it. What this section adds is the prior
question those corollaries assume has already been answered: whether a usable
predicate exists at all.

Four rules follow, each cheap:

1. **"Could not evaluate" is its own state.** Never render it as allow, clean,
   healthy, or no-finding. A check reports exactly one of four things:
   CHECKED-CLEAN, FINDINGS, COULD-NOT-CHECK, OUT-OF-SCOPE. The merge gate
   already works this way where it prints an unrecognised reviewer's comment
   rather than scoring it silently; `InjectionHealth.errors` does it by emitting
   a DEGRADED finding instead of resolving healthy.
2. **Write the degraded marker BEFORE the early return, with ZERO dependency on
   the component that failed.** A refusal path that records its reason only
   after the check it could not run has recorded nothing. READ, a sibling
   toolkit's 2026-09 incident: its blocking hooks refused every command for 30
   minutes while its own liveness canary reported OK — the dependency-missing
   branch printed its refusal and exited BEFORE the line that writes the
   heartbeat. The canary asked "did it receive a payload", never "could it
   evaluate one", so a guard refusing everything left exactly the trace a guard
   nobody called leaves.
3. **Degrade the RENDERER, never the VERDICT.** When the thing that failed is
   how you SPEAK — a serializer, a formatter, a size budget — the decision
   itself must still land. `print_json_bounded` is the in-repo instance: an
   oversized advisory loses prose, never its `permissionDecision`. **It also
   tells you when it could not keep that promise** — it returns False and emits
   ANYWAY when the envelope alone exceeds the budget or no named text key was
   trimmable, and CC discards an exit-0 hook's stderr, so that return value is
   the only signal a caller gets. Check it: "something was trimmed" is not "the
   decision landed". Another install's toolkit reaches the identical rule from
   the other side, rendering verdicts through a fallback chain after a broken
   serializer turned every refusal into empty stdout — which the harness reads
   as ALLOW.
4. **The watcher must detect COULD-NOT-EVALUATE, not merely DID-NOT-RUN.** A
   liveness check keyed on "is the timer active" or "did a heartbeat arrive"
   cannot see a guard that ran, refused everything, and wrote nothing. Ask what
   your watcher would report if the guard were evaluating nothing at all; if the
   answer is "healthy", it is measuring the wrong thing.

### Common Traps

- **A `gh` listing is ALREADY capped before you pass a flag, and the caps are not
  uniform.** MEASURED by reading `--help` on gh 2.98.0 (2026-08-20): `pr list` 30 ·
  `issue list` 30 · `run list` **20** · `workflow list` **50** · `gist list` **10** ·
  `release list` 30 · `repo list` 30 · `cache list` 30 · every `search` subcommand 30.
  So `gh pr list --limit 30` is behaviourally identical to passing nothing, and a
  session once reported its own cap back as the repo's open-PR count (said 30, the
  real number was 78). The unflagged form is the dangerous one precisely because
  nothing in the command hints a cap is in force. `scripts/hooks/capped_read_advisory.py`
  now says so pre-flight, and a drift test re-reads `--help` so this table fails loudly
  when gh moves a number rather than quietly naming a cap that no longer exists — treat
  the numbers above as the reading at that version, not as durable facts. Raising
  `--limit` gets you MORE ROWS, and `--paginate` is a `gh api` flag every one of these
  subcommands rejects (MEASURED on gh 2.98.0: `unknown flag: --paginate`).

  **A SHORT read is NOT proof of completeness, and an earlier version of this very
  bullet said it was.** "Re-read until the result comes back short of your limit" is
  the intuitive rule and it is false, because GitHub shortens a response on its own
  for reasons the command cannot see. MEASURED, three independent ways: a FILTERED
  `gh run list` (`--branch`/`--created`/`--event`/`--status`/`--user`) is served by the
  workflow-runs endpoint, which returns at most 1,000 results for such a search;
  `gh pr list --search` and `gh issue list --search` route through GitHub search and
  stop at 1,000 (`gh pr list --help` advertises `-S, --search`); and any `gh search`
  whose query TIMED OUT returns fewer rows than asked for with
  `incomplete_results: true`. Each is short, and none is complete. `gh search` also
  refuses a limit above 1,000 outright (`` `--limit` must be between 1 and 1000 ``),
  so you cannot widen past it at all.

  So: a SATURATED read supports "at least N" and never "N" — that much still holds.
  For an exact count, use a source that reports a TOTAL rather than the length of a
  list you asked for, and check that total is not itself partial: the Search API
  returns `total_count` alongside `incomplete_results`, and the count is exact only
  when that flag is `false`. Date-slicing a query is still the way to get under a
  ceiling; just do not treat a short slice as self-certifying.

- **Fail-closed data access.** A data-access boundary must RAISE (or return a
  clearly-typed "unknown/unavailable") on missing scope or an unavailable
  dependency — it must NEVER silently return the wrong data, the singleton's
  data, or an empty result that reads as "all clear". A monitoring/consistency
  check whose dependency (Qdrant, FTS, a remote) is down reports `unknown`, never
  `healthy`/`degraded` — a dependency outage that masquerades as data
  corruption (or as cleanliness) is worse than a loud error. Prefer a helper
  that raises over one that swallows (the `batch_retrieve_point_ids`-raises vs
  `batch_retrieve_vectors`-swallows split exists for exactly this). Origin: the
  home-anchored-DB reads that silently returned no data from an empty worktree
  path, and the memory-integrity checker (2026-07).
- **Never replace a runtime the CURRENT session depends on — that is a HANDOFF,
  not a repair.** Upgrading, downgrading, reinstalling or removing the
  interpreter, Node, the CC binary, or the venv that the ACTIVE session (or its
  hooks, MCP servers, or test runner) is running on kills the thing performing
  the repair, mid-repair — and the second half of the procedure, the half that
  puts the replacement in place, never runs. The safe sequence is side-by-side:
  install the replacement ALONGSIDE, verify it works, switch the pointer, and
  only then remove the old one; and run the whole procedure from OUTSIDE the
  dependent session (a separate shell, a systemd unit, the host Guardian).
  READ, a sibling install's 2026-09 incident: a rollback script uninstalled the
  running Node before installing the intended version, the uninstall terminated
  the agent session executing it, and the machine was left with neither. Before
  touching any runtime, inventory what is running on it.
- **Ego sessions are ACTIVE.** `src/genesis/ego/` is live (v3.0a11).
  Two egos: user ego (CEO, Opus) and Genesis ego (COO, Sonnet). Both
  run on adaptive cadence via the awareness loop. Changes here are
  production changes.
- **DB path confusion.** `genesis.db` is at `~/genesis/data/genesis.db`,
  NOT `~/genesis/genesis.db`. Use `genesis.env.genesis_db_path()`.
- **Column names.** Use `db_schema` MCP before assuming column names.
  The DB has 60+ tables.
- **Signal collectors.** Phase 1 built stubs; Phase 6 replaced some with
  real implementations. Code that looks complete may not produce signals.
- **Capabilities manifest.** `~/.genesis/capabilities.json` is write-once
  at bootstrap, not dynamic. New capabilities need registration in
  `_CAPABILITY_DESCRIPTIONS` in `src/genesis/runtime/_capabilities.py`
  AND a bootstrap init step.
- **APScheduler IntervalTrigger resets on restart.** `IntervalTrigger`
  counts from server startup, not from last successful run. If the
  server restarts more frequently than the interval, the job never
  fires. Use `CronTrigger` for anything longer than a few hours.
  Bit us with `user_model_evolution` (48h interval, daily restarts).
- **Silent skips are banned (provision-or-surface).** A setup/resilience
  feature that gracefully skips on a missing prerequisite (a package, a
  host knob) must either PROVISION the prerequisite (bootstrap.sh /
  host-setup.sh / a guardian reconciler) or register an effective-fact in
  `infra_profile` that the awareness posture check
  (`awareness/loop.py::_check_infra_protection_posture`) reads — so an
  unprotected box raises a standing alert instead of staying silent. A
  graceful skip with neither = a box that runs unprotected with zero
  signal (a sibling install ran weeks without swap/systemd-oomd until a
  memory spike wedged it, 2026-07). Guardrail:
  `tests/test_awareness/test_infra_protection_posture.py`.
- **Modules are NEVER subsystems.** A capability *module*
  (`src/genesis/modules/**`, an external pluggable capability — "hands,
  not brain", see `modules/base.py`) is not an internal Genesis
  *subsystem* (memory, reflection, ego, triage, autonomy, sentinel).
  Module memory writes must **never** set a `source_subsystem` value —
  that tag means "internal decisional output, exclude from default
  recall", which is wrong for module output. This is enforced
  mechanically: any `.store()` under `modules/**` passing
  `source_subsystem` is a hard CI failure in
  `tests/test_memory/test_store_subsystem_coverage.py`, which also forces
  every new memory-writer to either tag itself or be explicitly
  classified as user-context. `_KNOWN_SUBSYSTEMS`
  (`memory/retrieval.py`) is the authoritative subsystem list; adding a
  module name to it is a category error.
- **Destructive data migrations must reconcile cross-store mirror fields.**
  When a cleanup/backfill deletes data in one store (e.g. Qdrant vectors) but
  another store mirrors that data's existence (e.g.
  `memory_metadata.embedding_status`), the delete MUST also fix the mirror
  field. A deleted vector left as `embedding_status='embedded'` is a field
  that *lies*, and that lie is not cosmetic if any code path *reads* it —
  `MemoryStore._mark_superseded` gates an `update_payload` on
  `embedding_status != 'fts5_only'` and would fire a doomed write on the
  now-deleted point. Before assuming a stale field is harmless, grep for its
  *reads*, not just its writes. (Bit us in the source_subsystem purge, #918;
  fixed by #921 Step 2c — reconcile tagged rows to `fts5_only`.)
- **`immutable=1` reads miss WAL-resident writes.** A read-only `sqlite3`
  connection opened with `file:...?immutable=1` reads only the main db file
  and ignores the `-wal`, so a change you JUST committed (still
  un-checkpointed) is *invisible* — you get a false-negative "the write
  didn't land." To verify a live write, use `?mode=ro` (WAL-aware) or query
  through the server/CRUD path; reserve `immutable=1` for historical
  read-only sampling where a little staleness is fine. (A reconcile UPDATE
  read clean under `mode=ro` but appeared unchanged under `immutable=1`.)
- **Never hand-roll `gh`/bash/CLI argv parsing inside a hook or security gate.**
  Re-implementing shell/`gh` command-line semantics by hand (regex, manual token
  walking) creates an effectively UNBOUNDED adversarial-divergence tail: a good
  reviewer (Codex especially) will keep surfacing real gaps from true semantics —
  `--repo`/`-R`/`-Rvalue`, `GH_REPO`, `cd`, `&&`, `||`, `--body-file -`, duplicate
  flags, URL-vs-branch targets, enterprise hosts, `--help`, nested `bash -c` — and
  each named fix ships the next round's bug. This is the mechanism behind the
  measured Codex review-loop (an internal analysis of recent review-looping PRs:
  first-round findings were real catchable bugs, but later rounds were dominated by
  fix-churn on the hand-rolled parser itself — no finite pre-push audit bounds that
  tail). The cure is architectural,
  not "review harder": bind atomically to the real tool (e.g. `gh pr merge
  --match-head-commit`) and use a canonical parser (`shlex`/`bashlex`) — never
  bespoke semantics. Choose the fail direction PER BOUNDARY by consequence: the
  shared parser must DEGRADE gracefully (fail-open, never crash — that is
  `shell_parse.py`'s stated contract), while each security-critical caller
  (merge/push authorization) treats an unparseable command as a block (fail-closed
  THERE). This bullet and its corollaries decide the DIRECTION for one guard's
  degraded path, and they assume a usable predicate still exists. "Guard failure
  semantics" above decides the prior question — whether one does — and when none
  does (wrapper, interpreter or venv dead, or one dependency taking every guard
  at once) its repair-path test governs instead of this bullet. A parser-wide absolute fail-closed is wrong — it would deny legitimate
  uncommon commands without closing evasion paths. Same family as the
  canonical-parser lesson (regex→yaml, #1393). Loci today:
  `scripts/hooks/shell_parse.py` + `scripts/hooks/git_push_guard.py`.

  **The boundary of the class — read this BEFORE you reason yourself out of it.**
  The tar pit is NOT "who tokenizes the string." Delegating tokenization to
  `shell_parse` and asking git for repo state does NOT exempt a guard: if the
  guard's CORRECTNESS depends on modeling what a git command WILL DO — which
  flags force, which operands are paths vs refs, which modes destroy, which
  repo is targeted — it is argv→EFFECT mapping, and that mapping is the same
  unbounded open-set surface as raw string parsing. This was reasoned around
  once already (2026-08-23, PR #1432): the guard used the canonical tokenizer
  and probed live git state, the author concluded "so it's not hand-rolling,"
  and Codex returned **13 real findings (10 P1) — every one of them living in
  the argv→effect layer**. The first architect finding of that shape (a
  separated global value-flag bypass) was the CLASS signal and got
  instance-patched; the next round found the rest of the class. n=1 IS the
  signal: any reviewer finding that exposes a semantic-modeling gap in a guard
  means STOP and re-architect — never patch the named instance.

  **Decision test (verbatim, apply before shipping any guard):** could a git
  flag you've never heard of change your guard's verdict? If yes, your claim
  is open-set — redesign to closed-set token claims (exact-form whitelists /
  literal token blocks) or to RECOVERABILITY (snapshot-then-allow, where a
  miss degrades to the status quo instead of a broken guarantee). Do not ship
  the open-set version and plan to harden it later; the review loop IS the
  hardening loop, one bug per round, and it does not converge.

  **An instrument's stated invariant is its blind spot — and so is the one it
  does not state.** Before trusting a harness's clean result, read what it
  promises to hold fixed, then ask what ELSE it holds fixed without saying so.
  READ, and EXTERNAL to this repo — a sibling toolkit's own release notes,
  2026-09, not a Genesis measurement: a fuzzer whose mutators were all fixed
  one-character strings had a whole mutant shape unreachable by construction,
  and that writeup attributes its own long-lived bypass to exactly that. The
  remedy generalises even if the number does not: vary what the instrument
  never varies, rather than running it more times.
  Method for the whole class: `references/high-stakes-verification.md`.

  **Three corollaries, each bought with a non-converging review loop.**

  **(a) NEVER normalize the command text before a blind-spot probe.** A probe
  whose whole job is "notice that this text is unparseable" must read the RAW
  command. Preprocessing it can only ever DELETE the evidence the probe exists
  to find. MEASURED: a normalization step added in good faith — to stop a class
  of ordinary command from prompting — turned that shape into a silent
  ALLOW on two independent guards. Ordinary, not adversarial: the shape was one
  a developer writes without thinking, and the command really executed (verified
  against a shimmed binary, so the proof was execution rather than parse). The
  normalizer removed the very evidence the guard keyed on, and its own model of
  the shell's comment syntax was narrower than the shell's, so it could delete
  executed code as well. It then turned out not to be load-bearing at all: every
  case cited to justify it was one where the parser already resolved the
  operation, so its branch never ran. The fix was a DELETION, and it closed both
  defects at once. When a guard loop will not converge, look for the component
  that MODELS shell semantics and remove it — refining it is the loop.

  Scope this to a probe. Normalizing before a *tokenizer whose tokens you are
  about to use* is a different act with a different fail direction, and a
  sibling guard does exactly that, deliberately. Before calling any such site an
  instance of this rule, the question to answer is whether the normalization
  CAN change the tokenizability verdict or hide a target — and that is a
  possibility question, so a count cannot answer it. Demand a STRUCTURAL
  argument. The model is already in the repo, at
  `scripts/hooks/destructive_command_guard.py:101-120`: its replacements delete a
  line continuation — which is what the shell does with it — or insert
  whitespace and separators; none introduces a quote or an escape, so none can
  corrupt the quote balance shlex decides on. (The one place the shell keeps a
  backslash-newline literally, inside single quotes, is an over-block the guard
  states rather than hides.) That is a `cannot` — but read the next paragraph
  before reusing it, because the version of this argument that stood here for
  weeks was WRONG in a way that shipped a live bypass.

  **What that earlier version got wrong, and why it is the sharpest example on
  this page.** It added: *"a backslash-newline is a continuation, not an escape
  the shell keeps."* That is true only when the backslash is itself unescaped —
  an ODD-length run. In an EVEN-length run every backslash is escaped by its
  neighbour, so the last one is a literal character and the newline after it is
  a REAL command separator. The guard folded it anyway, deleting the separator
  and gluing the next command's first word onto the previous token, so no `rm`
  token existed and a destructive command was ALLOWED — measured end-to-end
  through the live hook, exit 0 where the plain-newline control gave exit 2. The
  legacy regex net did not save it either: that fires only when tokenizing
  FAILED, and this tokenized fine, just wrongly. Fixed 2026-09-02 by folding
  only odd-length runs.

  Note precisely what was and was not at fault, because the first attempt at
  this correction got it wrong in an instructive way: it said the quote-balance
  reasoning "was CORRECT and is not what broke", and exonerated it. Quote
  corruption is indeed not the *mechanism* of the token-glue bypass — but the
  quote-balance `cannot` **was not sound either, and it failed in the very same
  cell**. Deleting one backslash from an even-length run leaves an ODD run whose
  survivor escapes the next character; when that character is a quote, the fold
  DOES introduce an escape and shlex's balance shifts. Measured: it turned
  parseable commands UNPARSEABLE (11 of 20,000 random parseable commands under
  the old fold, 0 under the fix), which dropped them into the legacy-regex net —
  a net that matches neither `-Rf` nor `-r -f` nor `--recursive --force` nor a
  quoted `'rm'`, so they failed OPEN. One quiet universal quantifier (*every*
  backslash-newline is a continuation) falsified BOTH claims at once, and the
  old code therefore had two bypass families rather than one. The fold now
  always leaves an even-length run, which is what finally makes the `cannot`
  true.

  A structural `cannot` is only as good as the case-split it rests on, so state
  the split explicitly and enumerate it — the same trap as the direction-claim
  two paragraphs down, which was true of the operand and false of the option
  token. That both the wrong premise AND the first correction of it were written
  in a document teaching this exact discipline is the point: the danger is not
  knowing the rule, it is believing you already applied it.

  A corpus run only ever yields `did not, here`, and by the rule above it is
  structurally blind to the shape nobody typed. Run the corpus as
  corroboration, never as the proof, and pair it with a control that DOES flip
  — an unflipped corpus and an inert measurement look identical.

  State the direction too, not just a total — and then check the direction on
  the token you did not think of. The same sibling's fold used to split a word
  the shell joins, and the paragraph that stood here said the verdict could
  therefore only move toward refusing. That was measured on path operands and
  was false on the option token: the split could hide the flags, and a spelling
  the shell runs as a recursive-force removal of a protected path was allowed.
  A reviewer found it from the diff; the earlier audit had split every position
  of a path and never the option, and its zero came from benign traffic with no
  true-positive control. The fold now deletes the sequence, which is what the
  shell does, and the guard is asserted to give the same answer for the
  continued and the joined spelling. The general lesson is the one above: the
  examples you enumerate are a sample, and a direction claim needs the cell that
  would falsify it.

  Deliberately stated without the triggering shapes. A guard's defeat
  conditions are not a teaching aid, and this file is public.

  **(b) When the parse is unreliable, ASK — do not BLOCK.** A hard block forces
  a surgically precise trigger, and precision is exactly what an unreliable
  parse cannot deliver. Measured over five review rounds: every narrowing
  conjunct became a new way to STARVE the trigger (an over-strip that ate the
  evidence; a decoy segment that stood the net down), while every widening
  hard-blocked benign shapes (`git status # don't commit yet` was refused).
  Emitting a PreToolUse `ask` inverts the cost of being wrong — a false
  positive is one confirmation, a miss is the pre-existing status quo — which
  is what lets the trigger stay broad instead of clever. Measured prompt rate
  after widening: 0.43% of ~19k real commands.

  **SUPERSEDED for the git-operation net by owner ruling 2026-09-08 (re-checked
  against the code 2026-09-27).** That net no longer asks in any session type:
  the commit gate and the push guard REFUSE an unreadable command that names a
  gated operation, with a message giving the cause and a rewrite the session can
  perform — both cite the ruling at their blind branch. The reasoning here still
  prices the TRIGGER (a broad predicate remains right); what changed is the cost
  of a false positive, now an agent rewrite rather than a human confirmation. So
  wherever this passage and the paragraphs below say `ask` for that net, read
  "refuse with an actionable message". A line continuation joined the same path
  when the parser began REPORTING it as a blind spot (`_BLIND_CONTINUATION`)
  instead of modelling the join: refused, never parsed around.

  This does NOT loosen the fail-closed mandate above, and the two are easy to
  read as contradicting each other. Rule (b) is scoped to the git-operation
  blind-spot net — a guard whose trigger is deliberately broad and whose false
  positive is one confirmation. It is NOT a template for every guard: where the
  false negative is an irreplaceable path or a broad recursive removal and the
  false positive is a rewrite, that asymmetry justifies refusing outright rather
  than asking. Neither the protected-paths nor the destructive-command guard has
  an `ask` branch at all — both return only 0 or 2 — so a person's presence
  genuinely cannot change their verdict.

  One clause of that used to read "hard-block an unreliable parse", and it is
  FALSE. MEASURED 2026-09-06 through both live hook entries, controls flipping:
  each guard refuses on the PARSED path and then falls back to a DIFFERENT
  matcher, neither a subset nor a superset of it — it misses spellings the
  parser catches, AND it drops the target tests the parser applies, so it also
  refuses commands the parser allows. **A guard's fail direction is a property
  of its DEGRADED path, and that path has to be read in BOTH directions.** The
  two differ in what their fallback is for: `destructive_command_guard`'s is
  fail-open by design and says so in its own docstring, so argue with the design
  rather than patching it; `protected_paths_guard`'s calls itself "conservative:
  over-blocks, never under", which is true relative to the old guard it
  reinstates (`_legacy_substring_block`'s docstring) and false of the parsed
  resolver it stands in for — one sentence, two readings, and that ambiguity is
  the defect. Verify a docstring's stated direction IN CONTEXT; never quote it as
  a fact. The shapes are deliberately not enumerated here, per the rule above.

  That guard's MODULE docstring has since been split so it no longer states one
  fail direction for both blind spots: a bounds-induced blind spot REFUSES and an
  untokenizable one falls back. The sentence quoted above still sits on the
  fallback helper, where both readings remain available, so the lesson is
  unchanged — and note the citation here is now to a SYMBOL rather than to a line
  range, because the range this paragraph originally named stopped containing the
  quote the moment that docstring was edited. A line number is a claim with a
  shelf life.

  Within the git-operation blind-spot net, the two rules are scoped by who is
  present: `ask` is the interactive form of the refusal, and where a session is
  unattended fail-closed governs and (c) applies. The operation proceeds
  unverified in neither case.

  This is the shipped shape now, not an aspiration, and one distinction inside
  it must stay visible. The shared parser still degrades to a naive split with
  NO failure signal, so a caller that only asks "did I get a matching segment?"
  allows. What closes the hole is a separate conjunction AT THE CALLER — no
  matching segment, AND the raw text is un-tokenizable, AND it names a gated
  operation — which yields `ask` interactively and a refusal when unattended.
  The parser's contract did not change and must not: it degrades, the caller
  chooses the fail direction, exactly as the mandate above requires.

  Earlier revisions of this passage described that net in the present tense
  while it was still unmerged. Both directions of that error are worth naming,
  because fixing one produces the other: an unbuilt mechanism written as
  shipped, and then — once it does ship — a hedge left standing that now
  understates the tree. A status sentence in a durable document is a claim with
  a date on it. Re-check it against the code whenever the surrounding work
  lands, not only when it is first written.

  Corollaries of the corollaries, each measured: a net that returns inline
  PRE-EMPTS every gate below it (a hard block with no other backstop was
  observed downgrading to a prompt) — set a reason and DEFER it to the tail
  where the other decisions are resolved. And an invariant pair of the form
  "never silently allowed" + "never hard blocked" is satisfied BY a block→ask
  downgrade, so pin the verdict EXACTLY wherever a hard block is the contract.
  Read "never hard blocked" here only as the shape of the trap — as an actual
  invariant it is false unscoped, and (c) below replaces it.

  **(c) The ask-cost argument does NOT survive the move to a refusal.** Rule (b)
  buys its broad trigger with "a false positive costs one confirmation" — and
  that is true only where someone can confirm. An unattended session has nobody
  to answer, so the obvious completion of (b) is a deny leg for that path, and
  that is where it goes wrong: the SAME broad predicate whose errors were cheap
  now produces unappealable refusals of ordinary work. MEASURED: sharing one
  predicate across both legs refused routine, entirely benign commands in
  unattended sessions, in the one failure direction the design had been chosen
  to avoid.

  Narrowing the refusal predicate is the obvious repair and it does not
  converge, for a reason worth stating precisely, because the imprecise version
  of it is false. The claim is NOT that no raw-text rule could ever work — the
  raw text does carry quote and comment syntax, and a complete canonical parser
  could read it, which is what the canonical-parser rule above tells you to
  reach for. The claim is bounded to a predicate built on the SAME degraded
  parse that failed: at that point the guard cannot say whether an occurrence of
  a gated verb is executed or merely quoted, commented, or documented, and that
  inability is the premise of the net existing. A predicate with no more
  information than the failure itself cannot both refuse the hidden operation
  and permit the inert mention. Escaping that needs a different information
  source — a fuller parser, or enforcement at the execution boundary, where
  mentions are never classified at all — not a cleverer rule over the same text.

  Three narrowing rounds on one predicate is the signature to STOP and make the
  policy decision explicitly. The decision taken on the guard this was learned
  from, and since shipped: the unattended path KEEPS refusing,
  and the invariant gets scoped rather than deleted. "A benign shape is never
  hard blocked" was simply false as written; what is true and testable is that
  it is never hard blocked where a human can approve, and where no one can, the
  refusal carries an ACTIONABLE stderr — the cause, plus a route that gives the
  gate MORE information rather than less.

  That last qualifier is load-bearing and the sloppy version of this sentence is
  a bypass instruction. Telling an operator to find "a rephrasing that avoids
  the predicate" invites mutating raw text until a degraded predicate stops
  matching, while the same unverified operation still runs — the fail-closed
  mandate defeated by its own error message. Only two routes are legitimate, and
  neither is an evasion. If the text is PROSE that merely mentions a gated verb,
  take it out of a shell command altogether — write the file with an editor tool
  — because it was never an operation to gate and never should have been parsed
  as one. If it IS the operation, express it so the parser can actually read it,
  which does not dodge the gate but submits to it. A rewrite that suppresses the
  trigger while still performing the operation is the one thing such a message
  must never suggest, and a message is not "actionable" if that is what it
  teaches. (Actionability here is a courtesy to the operator, NOT the
  RECOVERABILITY of the Decision test above, which is about a MISS degrading to
  the status quo. Same word, opposite failure direction; do not satisfy the
  Decision test by printing a nicer error.) State the narrower invariant; do not
  leave the false one standing, and do not delete the guarantee that still holds.

  Two things keep this from reading as licence. The open-set imprecision is
  tolerable here ONLY because the verdict is `ask` or `deny` and never `allow` —
  an imprecise predicate that cannot authorize anything does not violate the
  Decision test, while the same predicate wired to an allow would. And a broad
  regex over raw text is admissible ONLY as a mention scan whose outcome is an
  `ask` where a person is present and a refusal where none is — never an allow;
  the moment it maps argv to an effect and authorizes on the result, it is the
  hand-rolled-parser tar pit the mandate above forbids.

  A measurement informed this rather than settling it, and it was afterwards
  WITHDRAWN — which is the more useful half of the story. The claim was that
  across a sample of unattended sessions the leg had never fired in either
  direction. It does not survive: the transcripts it counted no longer existed
  when someone went to re-derive it, and the sample was far too small to carry
  a word as strong as "never" even while they did. Absence in a small sample is
  the expected observation for a rare event, not evidence the event cannot
  happen; the honest reading is that the leg's rate is LOW, which is a different
  claim and a weaker one. What was actually being chosen was which promise the
  suite should make, not which incident to prevent — and that conclusion never
  rested on the number, which is why it outlived it.

  A note on every number in these three rules, including the ones above. They
  come from one operator's local session transcripts at one date, and no corpus
  or harness is checked in, so a reader cannot reproduce or falsify them — the
  differing denominators are different harvests, not one corpus quoted three
  ways. That is not a hypothetical weakness: one of them was withdrawn the
  first time anyone tried, for exactly that reason. Treat them as the scale at
  which something was observed, never as a published result. A rule that only
  holds at someone else's numbers is not a rule; each of these should stand on
  its stated mechanism alone — and if one ever seems to DEPEND on a figure,
  that dependency is the defect to fix, not the figure to defend.

- **`out=$(cmd)` under `set -e` swallows the failure path — and NO linter catches
  it.** An assignment whose value is a command substitution INHERITS that
  substitution's exit status, so under `set -euo pipefail` a bare
  `out=$(cmd)` followed by `rc=$?` **never reaches the `rc=$?`** when `cmd`
  fails: errexit fires first. Any error handling keyed on `rc` is dead code on
  exactly the path it was written for. Two properties make this vicious: it is
  invisible (the function just stops, printing nothing), and it hides behind
  call sites — `f || echo …` / `if ! f` disable errexit INSIDE the function, so
  the bug stays latent until someone writes the first bare call.
  **shellcheck 0.9.0 does not flag it at any severity, including `-o all`**
  (SC2155 is the *different* `local x=$(cmd)` declare-and-assign case; measured
  2026-08-27 — no other linter was tested, and a future shellcheck could add it).
  Always write `rc=0; out=$(cmd) || rc=$?` — but **declare `local` on its own
  line first**: `local out=$(cmd) || rc=$?` NEVER fires, because `local` is a
  command and the compound takes `local`'s status (0), not the substitution's.
  MEASURED: the split form yields `rc=100`, the inline form yields `rc=0` and the
  caller proceeds as if the command succeeded — strictly WORSE than the bug this
  entry describes, since it converts a loud abort into a silent false success.
  The same applies inside the error handler: with `set -o pipefail`, `x=$(… | grep -v … | tail -1)` aborts when
  `grep` matches nothing, so a `${x:-fallback}` default written for that very
  case never runs — guard it with `|| x=""`.
  Origin: `scripts/lib/cc_version.sh` (caught only by adversarial review), then
  three instances found in one `scripts/bootstrap.sh` function whose whole
  diagnostic block was unreachable. Pinned by
  `tests/test_scripts/test_bootstrap_guards.py::test_install_pkg_*`.

### Iterative-Refinement Discipline

AI refinement cycles degrade code they were asked to "improve" — validation
gets stripped, types relaxed, function scope widened. Published measurements
show vague improvement prompts degrade security fastest across iterations.
Three binding rules:

1. **Iterate with scoped, explicit prompts** ("fix the race in X by
   serializing on Y"), never "improve/clean up/make robust".
2. **Be security-explicit when touching validation, auth, or boundaries** —
   state what must not be weakened.
3. **Diff each refinement for what it REMOVED** (constraints, guards, type
   enforcement), not just what it added.

Full failure-mode taxonomy + ordered audit passes: `references/ai-code-audit.md`.

### Anti-Rationalization

These are excuses sessions use to skip discipline. If you catch yourself
thinking any of these, STOP — you are rationalizing a shortcut.

| Rationalization | Why it's wrong |
|---|---|
| "This is just a simple fix, no tests needed" | Simple fixes break complex systems. The Qdrant regression was a "simple fix." Write the test. |
| "I already know what this function does" | You haven't read the implementation. Docstrings lie. Read the actual code. |
| "Tests pass, so we're done" | Tests verify what they cover, not the outcome. Verify actual end-to-end behavior. |
| "I'll clean this up in the next commit" | Next commit never comes in autonomous sessions. Do it now, or file it — Genesis-repo work is a GitHub issue, not a local row. |
| "This file is too large to read fully" | Read the relevant section. Partial reads lead to partial understanding and wrong fixes. |
| "The linter is happy, ship it" | Linters catch syntax, not logic. Clean lint with broken behavior is worse than a warning with correct behavior. |
| "This change is low-risk, no impact analysis needed" | Your confidence is based on what you know; checking callers reveals what you don't. Serena `find_referencing_symbols` is live — run it. For multi-hop blast radius from the main checkout, run `scripts/lib/code_intel_index.sh "$PWD" gitnexus fast`, then `impact`; in a linked worktree, use Serena for live branch truth. |
| "I can skip the worktree, I'll be quick" | Concurrent session safety exists because "quick" commits have destroyed work before. Always worktree. |
| "The error is transient, retry will fix it" | Diagnose first. Retrying a misdiagnosed error wastes tokens and masks root causes. |
| "I'll add the follow-up later" | Records not created in-session are lost. File it now while context is fresh — Genesis-repo work as a GitHub issue, user-owned work as a follow-up. |
| "I don't need a skill for this" | If a skill exists, use it. The using-superpowers Red Flags table exists for this exact rationalization. |
| "This review round is the same class, it doesn't really count" | For an EXTERNAL cross-model round, the counter decides, not you — a repeat-class external round still counts; update it every external cycle and STOP at the cap. (Internal same-model reviews are never rounds.) |
| "The user already said proceed, so I can keep looping" | The escalation/fix-attempt caps CONSUME standing approval. Round 4+ (or fix #4) on an old instruction is a violation, not obedience. |
| "I can read the summary instead of the source" | Summaries lose context. If you're about to change code, read the code, not the description of it. |
| "The missing data was the problem — I wrote it, so it's fixed" | The mechanism that failed to write it is the problem. Hand-written artifacts are data repair, not a fix (see Instance-Fix vs Class-Fix Gate). |
| "I'll just add the field the reviewer flagged" | A spec's required-set is closed — derive and enforce ALL of it at once, with one test locking the whole set, or the next round finds the next missing field (see Debugging Discipline: spec required-sets). |

### Code Discovery

Use the right tool for how you're exploring:

- **Architecture overview** — CBM `get_architecture(aspects=["overview"])`
- **Finding symbols** — CBM `search_graph(name_pattern="...")` or Serena `find_symbol`
- **Call tracing** — CBM `trace_path(function_name="...")` or Serena `find_referencing_symbols`
- **Impact / blast radius** — Serena `find_referencing_symbols` (live caller set); GitNexus `impact` (reindex first) for multi-hop + affected processes
- **Config/doc/non-code files** — Grep/Read directly

Full decision matrix: `.claude/docs/code-intelligence.md`

### Auditing Existing Capabilities — enumerate, don't spot-check

Before claiming Genesis "lacks X", "needs to add X", or is "weaker than
<external system> at X" — or before any competitive/architecture comparison —
verify by ENUMERATION, not a spot-check. **Auditing a symbol is not auditing the
stack**, and a negative from a positive search is not evidence of absence:

1. Enumerate the subsystem's full module inventory before concluding anything is absent.
2. Trace the call graph BOTH directions — mechanisms often live in the
   wrapper/caller layer, not the first symbol (CRAG lives in the MCP recall
   wrapper, not `retrieval.py`; the reranker is applied by the caller).
3. Grep by CONCEPT with several synonyms, not one symbol.
4. Verify built/enabled/disabled against RUNTIME state (env gates, server logs),
   not code presence.
5. Multi-path systems → coverage matrix (N entry points × M mechanisms); hot
   auto-fired paths often carry a thinner stack than the deep path — a gradient,
   not an absence.
6. Confidence is capped by enumeration completeness.

A 2026-06-30 competitive audit wrongly claimed Genesis lacked CRAG,
scope-before-rank, and a live reranker — all three had already shipped. Full
protocol: procedure `codebase_audit` / CC memory `audit-enumerate-not-spotcheck`.
For "does Genesis already have X", consult the subsystem map
(`docs/architecture/CURRENT.md`, via the `subsystem-map` skill) FIRST;
`references/codebase-map.md` stays the package-level structural companion.

## Adaptive Review Protocol

**Review rides the commit, because the commit is the only mechanical
trigger.** "After each meaningful chunk" is unenforceable — nobody can gate a
vibe — but this repo commits continuously, so the commit IS the chunk, and the
commit gate already refuses unreviewed code commits in every session type
(hooks are session-agnostic; a dispatched builder hits the same gate an
interactive one does). What this paragraph adds is the named DEFAULT below the
substantial line: for an ordinary code commit, run the built-in `/code-review`
at **low** effort and let its findings feed the evidence you mark. Cheap,
catches drift while the fix is one edit instead of a review round, and it is
NOT a clearance — the marker flow is unchanged. (The gate auto-allows a
docs/config-only commit; any code change goes through the marker flow, and the
gate itself sets the depth — an ordinary edit takes a plain marker, a
substantial or prompt-surface one takes the adversarial pass.)
Every defect that
survives to the end-of-build review costs an adversarial cycle there, and
every one that survives THAT costs an external Codex round against the
escalation cap.

**For a SUBSTANTIAL change, the end-of-build adversarial review is canonical,
not the builder's choice: run `/deep-review`.** Judge substantial at end of
build over the WHOLE branch — the `merge-base(HEAD, origin/main)..working-tree`
range `/deep-review` and the CI check examine — not the last staged commit: the
commit gate classifies each commit's own staged diff, so continuous commits
leave the index empty and a branch split into individually-small commits would
otherwise slip the bar it clears in aggregate. The threshold VALUES are the
commit gate's — ≥50 reviewable lines, more than one code file, or any auth /
API / migration / prompt / agent / skill surface (the prompt-surface trigger
excludes the user-sovereign top-level CAPS docs, CLAUDE.md / SOUL.md / USER.md)
— applied here to the branch range rather than one staged commit. `/deep-review`
dispatches the adversarial pass in the required shape (genesis-architect, plus
genesis-security-reviewer when the diff touches a security surface) and writes
the evidence marker the gate reads; it is the end-of-build terminus, riding on
TOP of the during-build `/code-review` inline passes — which is exactly the
table's "Code-reviewer inline + `/deep-review`" for a substantial row, the two
playing different positions rather than a choice between them. Below
substantial, the table governs: a small focused fix ends with code-reviewer
inline. `/audit-changes` stays what it is — a light self-check that writes no
marker — and is a terminus only where the gate needs no marker at all: a
docs/config-only commit it auto-allows.

Choose the review level proportional to the change:

| Change type | Review level | Examples |
|---|---|---|
| Docs / text / comments | **None** | Markdown prose, inline comments |
| Simple mechanical | **None** | Variable rename, typo fix, import reorder |
| Small focused fix | **Code-reviewer agent inline** | Single-function bug fix, config tweak |
| Substantial change | **Code-reviewer inline + `/deep-review`** | Multi-file refactor, new MCP tool, wiring |
| Prompt / LLM behavior | **Both + extra scrutiny** | System prompts, skill instructions, routing |

Decision criteria when ambiguous: "If the change could break a runtime
path not covered by its own unit test, it needs `/deep-review`. If it only
touches things with clear, isolated test coverage, code-reviewer inline
is sufficient."

**`/review` is not in this repo — it comes from the optional `superpowers`
plugin.** Where that plugin is installed, `/review` and
`superpowers:code-reviewer` are the preferred path. Where it is not, those names
do not resolve, and older text saying "Run /review" sends you after nothing —
which is why the enforcement hooks now name the plugin as optional rather than
assuming it. **Check, don't assume, in either direction**: the plugin is offered
by the official marketplace, so its presence is per-install, not per-repo.

Always available, plugin or no: **`/deep-review`** (dispatches the adversarial
pass AND writes the evidence marker), the built-in `/code-review`, and the
by-hand `scripts/review_state.py evidence-path` → `mark` flow.
`/audit-changes` is a light self-check, not a substitute for any of them.

The enforcement hooks (`review_enforcement_prompt.py`,
`review_enforcement_commit.py`) still fire on every change — they are
safety nets, not the decision-maker. This protocol provides the
judgment framework.

### Review depth is machine-checked (the front-stop)

The "substantial → adversarial /review-level audit" decision above is no longer
left to judgment alone (that judgment is exactly what failed on PR #1353 — an
under-depth inline pass was self-certified as sufficient). The commit gate now
COMPUTES substantiality from the staged diff
(`review_scope.classify_change_substantiality`) and BLOCKS a substantial change
whose review marker is not an ADVERSARIAL audit (`review_enforcement_commit.py`
Rule 2.5). Substantiality is a surface-area × risk model — ≥50 reviewable lines OR
>1 code file OR an auth/api/migrations file OR an **executable prompt/agent/skill
surface** — and that last set is WIDER than it used to say here: read
`review_enforcement_commit._PROMPT_SURFACE_PREFIXES` + `_is_prompt_surface` rather
than a copied list. It covers `.claude/agents|commands|skills/*`,
`src/genesis/skills/*`, **`src/genesis/identity/`**, and **any**
`src/genesis/**/prompts/` path — so editing `src/genesis/identity/CODE_AUDITOR.md` or an
executor prompt forces `substantial` even at one line, which the old list denied.
So the "Prompt / LLM behavior → both + extra scrutiny" row above is machine-enforced (a
trivial edit to one is depth-audited). User-sovereign top-level CAPS docs
(`SOUL.md`/`USER.md`/`CLAUDE.md`) are exempt. Clearance binds the marker to the
reviewed diff's FULL content, so re-staging different content after the audit
re-blocks. A precision-filtered "no findings" inline pass is FALSE CONFIDENCE for a
substantial change — not clearance. Depth is override-exempt: a findings
`# review-override` does NOT waive it; only a loud, announced-on-stderr `# depth-ack`
does (announced, not RECORDED — nothing persists it, unlike the five PR-merge sigils
below, whose rows survive the session; the word "logged" now means a durable row) (the
audited escape for a genuine format mismatch). "Adversarial" is verified
STRUCTURALLY, and the recognised vocabulary is NOT what an earlier version of this
line said. `review_state._LADDER_LABEL_RE` accepts
**BLOCKER · SHOULD-FIX · CRITICAL · HIGH · MEDIUM · LOW · P1/P2/P3** — **`NOTE` is
not in it, and `MEDIUM` is** (the old text had that exactly backwards). A second
pattern, `_LADDER_PHRASE_RE`, also accepts a `"severity":` key or the phrase
`scope check`, which is why a `genesis-architect` audit clears the LADDER test even
when every finding is NOTE-level: it emits a `Scope Check:` block. That is ONE of
THREE tests, all required — `_evidence_is_adversarial` is a conjunction, so the
gate also demands `file:line` engagement (or the CODE_AUDITOR JSON's `"file"` +
`"line"` pair) and at least `_MIN_EVIDENCE_CHARS` of substance. A scope-check
header alone does NOT pass. The trap is a HAND-WRITTEN audit that is all
NOTE-level and carries neither marker — it fails on the ladder test before the
other two are reached. So run the real recall-tuned audit (`genesis-architect` /
`CODE_AUDITOR.md`); do not hand-write a soft prompt.

**Honest enforcement model — VERIFY IT, do not assume it.** This section used to
claim the enforcing teeth were "the independent cloud reviewer + a required human
approval, gated by **branch protection**", and that the local hook was merely
advisory friction. **Measured on a live deploy (2026-08-27), that was false in
both halves**, and the correction matters because it inverts which layer you can
actually rely on:

- `GET /repos/{owner}/{repo}/branches/main/protection` → **404, not protected**.
  The protection the claim rested on did not exist. A *ruleset* did — a different
  API, invisible to the branch-protection endpoint.
- That ruleset required **one status check** (`test`), not the other CI jobs that
  blocked AT THE TIME. This whole bullet is a dated measurement, not current
  state; see the CI paragraph below. Lint, leak-detector and the rest were never
  server-side required.
- It carried `bypass_actors: [{actor_type: RepositoryRole, actor_id: 5 (admin),
  bypass_mode: always}]`. **A bypass entry voids the rule for that actor.** The
  sole author is the repo admin, and the merge command this skill mandates
  carries `--admin`.

⇒ For the actor who merges, the server-side backstop was **void**, and the local
`git_push_guard.py` merge gate was the *only* real enforcement — precisely the
layer the old text told you to discount.

**⇒ THAT MEASUREMENT IS NOW HISTORY, and this is what the rules below are for.**
Since PR #1907 (applied 2026-09-10) the rules are SPLIT into two rulesets, and
the split is exactly the property the 2026-08-27 measurement found missing:
`.github/rulesets/checks.json` carries `"bypass_actors": []` and requires `test`,
`leak-detector` and `lint`, so **those three now bind an `--admin` merge
server-side** — GitHub reports `current_user_can_bypass: "never"` for the owner
on that ruleset. `approvals.json` keeps the admin bypass deliberately, because a
sole maintainer cannot approve their own pull request; it carries `pull_request`,
`update` and `creation` only. Rationale and the accepted residual:
`.github/rulesets/README.md`.
Kept rather than rewritten, because the reasoning is the lesson and the state is
the perishable part. Do not read the paragraph above as current — read it as what
you find when you finally query, and note that this correction itself has a date
on it.

**Two rules follow.**

1. **Never state where the teeth are without querying it.** Both endpoints, every
   time: `branches/main/protection` AND `rulesets` (then the ruleset by id, since
   the list view omits `bypass_actors` and `rules`). A rule with a matching bypass
   actor is decoration for that actor. A "required" check list is meaningless
   until you have compared it against the jobs that actually block.
2. **Do not weaken a local gate on the theory that something external catches it.**
   That reasoning is only as good as the configuration you just verified, and
   configuration drifts silently — nothing announces a ruleset edit.

The local gate's job is still to interrupt autopilot, and it is still editable by
the same author. But "editable in principle" is not "backed up in practice."
Where a real external backstop exists, name it and cite the query that proved it.

**Close every review with a verdict.** End each review with an explicit
`Ready to merge: Yes | No | With fixes` + a one-line reason, alongside the
DONE / DONE_WITH_CONCERNS completion status. And the DON'Ts that keep a review
honest: never "looks good" without reading the code; never a finding on code you
did not read; never vague ("improve error handling") — always `file:line` + why it
matters. (Deliberately NOT a "praise-first / acknowledge strengths" balance: an
adversarial audit's job is to assume bugs and enumerate the class, not to reassure.)

Three protocol steps apply to every review at "Code-reviewer inline" level or
above (full definitions in `.claude/agents/genesis-architect.md`):

- **Scope-drift check first**: compare stated intent (plan file / PR
  description / commit messages) against `git diff --stat` vs the merge-base,
  and open the review with the `Scope Check: CLEAN / DRIFT DETECTED /
  REQUIREMENTS MISSING` + Intent/Delivered block. Informational, never
  blocking.
- **Premise check second** (Step 0.6, method in `.claude/docs/premise-check.md`):
  before reviewing the code, verdict each claim the change DEPENDS on
  independently — with evidence, a confidence, and a falsifier — ask what the
  caller does differently because of its output, and say whether a better shape
  exists (an existing chokepoint it re-implements, a simpler mechanism, a place
  the problem disappears). Emit the `Design-premise:` block. Also informational,
  and its BROKEN verdict has a HIGH bar: it routes to the EXISTING
  premise-wrong disposition (architecture conversation, or
  `needs-architecture-session` + draft + a `ready` row) rather than to another round, so everything short of "the change cannot do what it
  says" is SOUND-BUT-INFERIOR with the better shape named. Render a
  better-shape finding on the severity ladder too (normally SHOULD-FIX), or it
  is invisible to every surface that scores findings.
- **Completion status last**: every review (and every skill workflow that
  concludes work) ends with exactly one of DONE / DONE_WITH_CONCERNS /
  BLOCKED / NEEDS_CONTEXT — with concerns listed, or blocker + what was
  tried, or exactly what context is missing. Findings use the
  BLOCKER / SHOULD-FIX / NOTE severity ladder with per-finding confidence
  and the pre-emit quote gate (a finding must quote its motivating
  file:line or be confidence-capped).

### Review-loop discipline

- **A RELEASE review is scoped to what the release CHANGES; a whole-codebase
  audit gates nothing.** Point a reviewer at "the repository" and it returns
  findings that PRE-DATE the release — real, worth fixing, and not release
  blockers. Treating them as blockers is how a release never ships. Scope the
  gating pass to the diff between the previous RELEASE tag and HEAD (this repo
  also carries non-release tags, so `git describe --tags` alone can land on the
  wrong one); run the broader
  audit separately, on its own clock, where its findings become ordinary work
  rather than a stop. The distinction is not "which findings are real" but
  "which findings this release is answerable for" — a defect that already
  shipped in the last release is answerable by the next fix, not by this gate.
  Release scope decides what BLOCKS, never whether an external round COUNTS: a
  pre-dating finding is still a defect-bearing round on the escalation counter,
  filed as ordinary work rather than held as a release blocker. "It pre-dates
  the release" is otherwise just a new spelling of the rationalization that
  counter exists to kill.

- **A pre-existing defect the change RECRUITS FOR is IN-arc — FOR A RELEASE
  REVIEW.** The bullet above splits findings into "this release's" and
  "pre-dating it", and there is a third bucket between them that neither
  catches: a defect that already existed and which THIS change newly leads
  people INTO — because it adds the documentation that sends them down that
  path, flips the default that reaches it, or ships the feature whose obvious
  next step lands on it. It is not in the diff, and the RELEASE is still
  answerable for it, because the release is what made it reachable. Ask of every
  pre-existing finding: *did we just build the road to it?* If yes, it gates
  **the release**. (Adopted from a sibling toolkit's release-review protocol,
  which carries a dated case of exactly this shape: a latent defect in an
  install path that was harmless until a release's own docs began recommending
  that path.)

  **This does NOT override "Keep the PR the PR"** (standing user rule,
  2026-09-09, further down this file). On an individual PR the routing question
  is unchanged — *does the PR work without this fixed?* — and for a recruited
  defect the answer is USUALLY yes, so it is FILED as an issue while the PR
  merges. Run the question anyway rather than assuming the answer: a change that
  flips a DEFAULT onto the latent path fails it, and a PR that breaks on first
  use is not a merge. What this bullet changes is the RELEASE's answerability,
  not whether a PR waits. Holding a PR for a recruited defect is the exact
  friction the owner removed, so if you find yourself about to, you have
  mis-scoped this bullet.

- **A review's findings are a SAMPLE, not a to-do list.** This is the single
  highest-value habit in this section, and the one most often skipped. CLAUDE.md
  already says to treat the *user's* examples as a sample and enumerate the
  broader class — the same rule applies to REVIEWER output and is easy to miss
  there, because N findings look exactly like an N-item work queue. Before
  fixing any finding: name the CLASS it belongs to, enumerate the full
  population of that class (programmatically — an AST walk or a grep that lists
  every member, not a mental scan), and fix the population. Then the next round
  has nothing of that class left to find.
  Origin: 2026-08-27, three defect-bearing rounds on one PR where each round's
  fix introduced the next round's defect — one of three renderers, then one of
  two branches in the same function, then one direction of a two-directional
  boundary. Every round I fixed exactly what was named. The enumeration that
  finally closed it took one AST script and would have worked in round one.
- **Fixing the named instance is how a loop runs away.** If two consecutive
  rounds each surface NEW defects, stop patching: that is the signature of
  instance-fixing, and the commit gate hard-blocks TWICE: first at the SECOND
  round (`review_enforcement_commit.py` mode-switch, cleared by `# audit-ack`),
  then at the third (escalation cap, `# escalation-ack`). Both call `_deny`, so
  the first stop arrives one round earlier than "the cap" suggests — see the
  two-tier table below. Switch to enumeration BEFORE the gate has to say so.
- **Rising findings are a prompt to check PROVENANCE, not a verdict by count.**
  When round N+1 returns more findings than round N, stop patching long enough to
  determine whether the new findings land on additions made to address round N.
  That provenance is the divergence signal; an unrelated increase alone is not.
  If the added material is correct and separable, SPLIT it into one-concern PRs;
  if it is wrong or unnecessary, REVERT to the smallest correct diff. Patching
  again before that decision enlarges the surface the next round reviews.
  MEASURED 2026-09-10 on PR #1831: insertions 731 → 827 → 1193 → 1372, findings
  round 1 = 2 → round 2 = 4, and round 2's findings were entirely about round 1's
  own additions. Every one was a genuine live bug — the fixes were correct AND
  they were manufacturing the next round's findings, which is why the owner chose
  SPLITTING there: a plain revert would have discarded correct work.
  The discriminator is provenance, not growth or a raw count: the same session,
  PR #1793 grew 85 → 139 → 209 insertions with findings 3 → 2 and converged.
  The queue observation is descriptive only: the 196-PR sample included more
  3+-round PRs among changes over 1,000 lines than among changes under 200; it
  does not establish that size causes rounds. **The limit, honestly:** the
  provenance trigger rests on one clear instance and one clear counter-instance.
  Act on it as a signal; it is not a law.
- **Findings concentrating on safety code you built AROUND one external command:
  read that command's manual for a native mode before writing the next guard.**
  Adopt-before-build covers whole tools; this is the same question one level
  down. When successive rounds land on hand-built protection around a single
  command (snapshot, then refuse, then a destructive form of the command), the
  tool often already ships the guarantee. Instance, #2722 (open as of 2026-10-01): moving the
  rollback from hand-built snapshot-and-refuse around `reset --hard` to
  `git reset --keep` retired most of a round's findings at once, and a later
  premise check found that `git checkout --no-overwrite-ignore` refuses an ignored
  file that `--keep` silently overwrites (MEASURED, git 2.43). The modes differ
  in exactly these edges, so read every candidate's manual entry, not only the
  first that fits. The trigger is the concentration of findings on one seam, not
  their count.
- **Interrogate every MECHANISM you introduce along six axes BEFORE the first
  review — original code and fixes alike.** The two bullets above enumerate the
  class of a DEFECT, reactively, once a reviewer names one. This one is about the
  extent of a MECHANISM — a deadline, a lock, a cache, a guard, a token — which is
  correct on the path that motivated it and undefined everywhere else.
  MEASURED 2026-09-09 on 31 inline review findings from two PRs, each attributed
  by `git blame` at the SHA the reviewer was looking at, and classified as
  "original" or "post-review" by whether the blamed commit predates that PR's
  first review. Over all rounds, 21 of 31 (68%) blame to the original
  implementation. But round 1 CANNOT blame to a fix commit — none exists yet — so
  that figure is partly forced arithmetic. In the rounds that actually loop it is
  close to even: **of the 21 findings from rounds 2+, 11 were already in the
  original code and 10 were in code written to answer an earlier round.** Both
  halves are worth the same attention; neither dominates.
  Deduplicating re-posts and cross-reviewer duplicates and dropping one finding
  later refuted by measurement leaves 25 distinct findings, and **24 of those 25
  fall into six shapes.** These are the checklist:
  1. **SIGNAL** (8, the largest) — does the thing you read change exactly when
     the property you assert changes? A whole-database counter read as "did this
     table change"; an mtime read as "did content change"; empty read as
     "unbuilt"; a proxy's identity read as "same connection"; two autocommit
     reads used as one snapshot.
  2. **SCOPE and LIFETIME** (5) — a deadline per query when the operation is a
     traversal; a lock per process when the race is cross-process; a lease that
     expires mid-work; a hang moved from the operation to interpreter teardown.
  3. **INPUT DOMAIN** (5) — the values the type permits, not the ones you
     pictured: non-boolean, empty string, malformed timestamp, sub-second
     precision, unescaped path.
  4. **EQUIVALENCE** (3) — what does it claim to match, and has anything checked?
  5. **FAILURE PATH** (2) — what is left behind when the work aborts.
  6. **CALLER CONTRACT** (1) — does an explicit option the caller passed survive?
  (The 25th was a test whose identity comparison was unsound in both directions.)
  A finding is a POINT, so a fix that treats the finding as its spec inherits
  that point as its only test — which is why the fix half of that near-even split
  exists at all, and why the six axes are worth running over a fix and not only
  over a feature.
  METHOD NOTE, because getting this wrong inverted an earlier version of this
  measurement: to attribute a finding to a commit, blame the line at the SHA the
  reviewer was looking at. `original_commit_id` is the PR HEAD when the review was
  posted, not the introducing commit — one commit here touched only changelog
  files yet carries three findings on a Python file.
  FALSIFIABLE, with a denominator: #1850 pushed four commits after its first
  review round, and deduplicated they carried 5 findings between them — 1.25 per
  post-review commit. If the next three PRs whose mechanisms are interrogated on
  these six axes before pushing still average above one deduplicated finding per
  post-review commit, the axes are not the right ones. Re-derive rather than
  adding a seventh.

- **Run the pre-push adversarial pass with `/deep-review`** (`.claude/commands/deep-review.md`):
  one command that dispatches a fresh-context `genesis-architect` (+ `genesis-security-reviewer`
  on security surfaces) over the FULL branch diff with the right SHAPE — fail-open/state/TOCTOU/
  hand-rolled-parsing hunting, not a lint/secrets scan — and writes the evidence marker. This is
  the review that catches Round-1 bugs before Codex does; `/audit-changes` is only a light
  self-check.
- **PR review-findings status = `python3 scripts/hooks/git_push_guard.py --check-pr <N>`
  — ONLY.** This runs the SAME code path as the merge gate (strict fail-closed: a
  failed scan is never reported clean). NEVER hand-roll a `gh api pulls/N/comments`
  query to decide whether a PR is review-clean: a wrong filter's EMPTY result reads
  exactly like "clean". Origin (2026-08-23, #1431/#1432): Codex authors BOTH its inline
  findings AND its review-summary body as `chatgpt-codex-connector[bot]` — the REST
  `user.login`, WITH the `[bot]` suffix. A hand-rolled filter keyed on a DIFFERENT login
  (the GraphQL app login, which is not the REST login) matched nothing, and 13 real
  findings (10 P1) were reported to the user as "review-clean" until the merge gate
  blocked. An empty result from your own query is "my query found nothing", never "no
  findings exist". Freshness is a SEPARATE gate, and it is NOT a blanket
  reviewed-SHA-equals-HEAD rule: for a hook-surface or otherwise non-trivial delta a
  current Codex review must COVER head (reviews-API `commit_id == head`, or a clean
  Codex signal whose abbreviated id RESOLVES to the head, see the Pre-Merge Gate),
  but a trivial NON-hook delta may still merge on a stale
  review — `--check-pr` reports that as `codex-at-head : ok (STALE review of <sha>, delta
  since is trivial)`, a pass, not a block.

  **And the verdict is a function of the TREE YOU RAN IT FROM.** `--check-pr` is
  (remote PR state × LOCAL gate code) — it executes the `git_push_guard.py` sitting
  in your working copy, not the one on `main`. Run from a worktree carrying an
  unmerged change to the gate, it reports what WOULD be true once that change lands:
  a correct verification OF THE FIX, and a false statement about the PR. MEASURED
  2026-09-09: a session reported a peer's PR as one blocker from green on exactly
  that confusion. So say which tree produced any verdict you pass on, and for a
  claim about a PR's CURRENT state, run it from a tree at `origin/main`. To bound
  a verdict you already took, compare the CHECKER ITSELF and its effective policy
  inputs across the two trees, not main's history: `git diff origin/main --
  scripts/hooks/git_push_guard.py` (add whichever modules the checker imports),
  then identify any local configuration that can alter required reviews or CI.
  Hash the relevant blobs when the effective policy is identical. Reaching for
  `git log origin/main -- <checker>` instead is the trap — it inspects only what
  landed on main, so it comes back EMPTY in the exact case that bites you, a
  worktree carrying an unmerged change to the gate. Empty history is not identical
  logic. The same reasoning applies to any local checker whose answer is about a
  remote object.
- **Hook-surface PRs merge only with a current GitHub Codex review — mechanical.**
  A PR touching the enforcement-hook surface (the guard code itself) gets almost no
  stale-review leniency: the merge gate (1) never classifies a post-review delta that
  TOUCHES that surface as "review-trivial" — the test is on the DELTA's files, not the
  PR's, so a docs-only follow-up commit on such a PR can still be trivial (see the
  budgeting note further down, which measures exactly this) — and (2) refuses
  `# stale-review-override` — regardless of Codex
  head-freshness — unless recorded fallback-review evidence exists for the EXACT
  base+head (`~/.genesis/override_review_evidence/<repo>__<pr>__<base12>__<sha>.txt`).
  A current at-head Codex review does NOT substitute for that evidence: the same sigil
  also waives `_check_base_is_default`, and the evidence identity binds the BASE tip,
  which a head-only review cannot vouch for (a hook-surface PR retargeted to a
  non-default base must be re-reviewed in that base's context). The surface is defined
  authoritatively by `_HOOK_SURFACE_PREFIXES` + `_HOOK_SURFACE_FILES` in
  `scripts/hooks/git_push_guard.py` (hook dirs, the global bash safety hook, the
  review-scope/state modules, hook wiring in `.claude/settings.json`, and the tracked
  configs the hooks read) — read those constants, kept exhaustive by the
  `TestWiredHooksFenceGuardrail` test, rather than any hand-copied list. The override
  procedure requires the user's explicit authorization, then a fallback adversarial
  review (local `codex exec` when quota allows, else genesis-architect), evidence
  recorded naming the head, then the merge re-run — the gate's block message walks
  through it.
- **One reviewer at a time on a given diff — almost never two at once.** Run one
  reviewer (e.g. Codex), apply/verify its findings, then run the next reviewer
  (e.g. Claude) on the *fixed* code. The second reviewer should see the improved
  code, not the same unfixed diff both would otherwise review; parallel also
  doubles review spend per baseline. (Standing user directive.)

  **Sequential means WAIT. It does not mean kill.** When a review is already
  running and a different reviewer gets named — a tool switch, a stated
  preference, a peer session's run — let the in-flight one FINISH. Terminating
  it destroys the findings the next reviewer is supposed to build on, which is
  the opposite of what this rule is for. MEASURED 2026-09-08: a running review
  was killed four minutes in, mid-analysis on the load-bearing question, to
  honour this rule's former "NEVER" — and nothing was gated on stopping it.

  The "almost" is load-bearing. This is standard practice, not a prohibition:
  * Two reviewers on DIFFERENT diffs is fine — no shared baseline, no
    cross-bias. It is not licence to fan out across the queue: external
    reviewers share one quota, and exhausting it strands the cross-model gate
    every open PR depends on. Concurrency here is a tolerated exception, not a
    throughput strategy.
  * A peer session's reviewer is not yours to stop while that session is alive.
    An ORPHAN — a process whose session is gone, or one wedged holding a
    semaphore slot — is ordinary cleanup; establish it is orphaned first.
  * The user may ask for both, in their own words naming the running one. A
    preference for a different tool is NOT that ask; that misreading is the
    incident above.
  * ROUND 1 of a hook-surface PR runs two reviewers in parallel BY STANDING
    RULE — see the gate-fix lane below. The rationale above does not bind
    there: both reviewers see the same unfixed head, so neither could have
    seen "the fixed code" whatever the order. This carve-out is scoped to
    round 1 of that lane and to nothing else; rounds after it are sequential
    like everywhere else.

  **If you are about to terminate a running reviewer in order to satisfy this
  rule, that is the tell that you are misreading it.** Wait, or ask.

  This rule prohibits a STATE ("two running") rather than an ACTION, which is
  why it is spelled out: a state prohibition can be satisfied by DESTROYING the
  offending state, where an action prohibition — never push to main, never
  hardcode the slug — can only be satisfied by not acting. The other instance of
  the shape is `CLAUDE.md`'s genesis-bridge line ("must never run alongside the
  server"), where the wrong discharge is worse: stopping the server rather than
  declining to start the bridge. Prefer phrasing a new rule as an action; where
  it must name a state, say which side gives way, because an absolute with an
  unstated remedy is discharged in whatever direction the reader already leans.
- **A different model is the real correctness gate; Codex is the default.** A Claude
  reviewer shares this model's blind spots, so it clears the LOCAL depth gate but is not
  the cross-model gate. When the GitHub Codex reviewer is unavailable AND the install
  has an approved alternative external reviewer — a DIFFERENT model, with explicit
  per-use user approval every time — run it non-interactively over the diff with an
  adversarial mandate. Which reviewer that is (if any) is install-local and belongs in
  user-level config, not here.
  **Unavailable is established by ASKING**: comment `@codex review`, wait, and read the
  reply. An explicit usage-limits comment is the STRONGEST evidence available — but it
  is not proof, and treating it as proof is how an alternate-reviewer approval gets
  spent on a Codex that was never down. The two channels are INDEPENDENT: a
  usage-limits ISSUE comment can sit there while a later trigger delivers real INLINE
  findings (MEASURED on #1484 — see the inline-findings section below). So: re-trigger,
  let time pass, and check the INLINE endpoint before concluding unavailable.
  A second shape carries the same weight and the same caveat: an explicit
  REVIEW-FAILURE reply that REPRODUCES. The bot answering `Codex Review: Something went
  wrong` names its own cause, and a cause you can DISPROVE locally is the strongest
  reading available — MEASURED 2026-09-09 on two PRs, twice each 43 minutes apart,
  `Provided git ref <sha> does not exist` while `git ls-remote origin refs/heads/<branch>`
  returned that exact sha, and two other PRs on the same account reviewed normally in the
  same window. Reproduces + refuted locally + not account-wide = a defect on their side,
  not a quota and not something a further retry fixes. One such reply is an incident, not
  yet evidence: retry once, let time pass, and check the INLINE endpoint first, exactly
  as the usage-limits case demands.
  Everything weaker is not evidence at all — silence is not, and neither is
  `--check-pr` reporting no review, which says the same thing whether the reviewer is
  down or was simply never triggered at this head. Nor is a
  `codex exec` quota error: that is a separate surface on separate quota.
  Scope what you hand it exactly as `.claude/commands/deep-review.md` §1 specifies.
  **Verify it saw a diff at all**: a clean verdict that does not demonstrate WHAT it
  reviewed is void, and a false clean from the cross-model gate is worse than no review.
  Do not merge on a same-model-only review.
- **Escalation cap — a HARD BLOCK at 3 CROSS-MODEL rounds that each find NEW defects.**
  A *round* = one EXTERNAL cross-model review→fix→re-review iteration. INTERNAL
  same-model reviews (genesis-architect / genesis-security / any subagent) are NOT
  rounds — they never move the machine counter (see "THE COUNTER IS CROSS-MODEL ONLY"
  below) and must not be counted in the visible tally either, or a session re-creates the
  very false-stop this is meant to remove.

  ⚠ **CALL AN INTERNAL AUDIT A "PASS", NEVER A ROUND — including in your own
  evidence file and your own prose.** The word is reserved for the counted thing,
  and nothing enforces that, so it is on you. MEASURED 2026-09-25: a session ran
  three internal architect audits, headed them `ROUND 1` / `ROUND 2` / `ROUND 3` in
  its evidence file, and then told the owner its PR was "at round 4 with four
  reviewed heads". `scripts/review_budget.py --repo <r> --pr <n>` said `count=1,
  next_round=2`. It had been counting its own passes — the very thing this
  definition excludes — and built a process question on the wrong number. **Never
  state a round number you have not just read out of `review_budget.py`**; your own
  tally is not a source, and `review_state.py mark` prints "internal review —
  cross-model streak unchanged" precisely so you cannot mistake one for the other. A cloud-bot (Codex) re-review round counts; a
  locally-run non-Anthropic reviewer (the install's configured secondary) counts. The
  cap is enforced by three
  mechanics, not by vibes:
  1. **Visible streak counter.** From the first EXTERNAL round, the plan file (or task
     list) carries `Cross-model streak: N (cap 3)`, updated every external cycle. It
     counts consecutive defect-bearing external rounds for this cap — a STREAK
     POSITION, never a round NUMBER. Round numbers come only from
     `review_budget.py`'s round count, and the two differ (a locally run
     secondary advances the streak without adding a reviewed head). The streak is a tracked artifact — "it's the same class, it doesn't really count" is exactly
     the rationalization the counter exists to kill (for a repeat EXTERNAL round).
  2. **The block point is BEFORE dispatching the next review.** The check is
     "am I about to trigger a 4th consecutive defect-bearing external round
     (streak at the cap)?" — evaluated at the mechanical moment
     (the `@codex review` comment, the reviewer dispatch — NOT the push itself,
     which triggers nothing TODAY — an owner-tunable
     setting, so verify at the PR rather than trusting this clause), never after
     reading the next batch of findings.
  3. **The cap CONSUMES standing approval.** A prior "proceed", "merge when
     clean", or "keep going until Codex is green" is VOID once the cap fires.
     Continuing a round-4+ loop on an earlier instruction is a violation, not
     obedience — STOP, post the round ledger (round → what it found → what it
     cost), name the cap explicitly ("we've hit the 3-round escalation cap"),
     and get a FRESH decision: HAND IT BACK through the established disposition
     — architecture conversation, or `needs-architecture-session` + draft + a `ready` row
     (three rounds
     each finding something new, after a class-level audit, is the strongest
     evidence available that the PREMISE and not the code is what is wrong —
     and no further round can fix that), switch to a robust-by-construction
     redesign, narrow scope, or shelve. The hand-back option is first because
     it is the one nothing used to name, not because it is the likeliest —
     decide it on evidence via `.claude/docs/premise-check.md`, and see the
     two-path doctrine below.
  **Tabulate findings by CLASS before fixing — but never let that change what
  COUNTS.**
  Tabulate the findings with a CLASS column before fixing ANY round's findings,
  including round ONE. Deferring the tabulation to the second round is what
  spends a round discovering a shared cause that was visible in the first: if the
  opening review returns several instances of one generator, an instance-level
  first pass fixes the ones named and ships the rest as the next round's
  findings. The tabulation is cheap and the round it saves is not. Findings that look unrelated one at a time routinely share one
  generator — five across three rounds once reduced to a single defect (two
  layers that had to agree about every lever and could not), and patching
  instances twice changed nothing while deleting the second layer removed all
  five at once. If one class has ≥2 entries, look for the shared GENERATOR and fix
  that; two findings can land in one superficial class without sharing a cause, so
  the count is the prompt to look, not the verdict.
  **Class grouping decides HOW you fix — never whether an external round counts.**
  The counter is deliberately class-blind: `bump_review_round` increments on a
  distinct staged diff and records no CLASS identity (it records only `source`, to
  gate internal-vs-external), and the marking rule below is finding-based — ANY new
  BLOCKER/SHOULD-FIX/P1/P2 from an EXTERNAL reviewer makes the round defect-bearing,
  including the second instance of a class you have already named. On an external
  round, passing `--clean` to keep a repeat-class round off the counter is a
  falsification, and worse than miscounting: an external `--clean` RESETS the streak
  to zero, so it disarms the cap outright rather than merely under-counting it.
  (Internal reviews never count either way, so there is nothing to falsify there —
  the temptation and the rule both live only on external marks.)

  A corollary that costs a round if missed: when you delete a duplicated layer,
  verify the class is closed by enumerating the registry for the BEHAVIOUR, not
  for the deleted file's NAME. A name-scoped guard beneath a class-scoped claim
  passes happily while a sibling oracle sits on the same event and matcher.
  (And if the invariant turns out not to be statically checkable, say so and
  narrow the test to what it can prove — a guard that cries wolf gets deleted by
  whoever hits it next.)

  These disciplines are backstopped by two complementary machine layers.

  The LOCAL defect-bearing streak keeps its established interventions:

  | Local streak | Gate | Required action |
  |---|---|---|
  | 2 | **MODE-SWITCH block** | Decide premise-vs-polish, run the fresh-context class audit, then use `# audit-ack` only if that audit happened. |
  | 3 | **HARD STOP** | Hand back, redesign, narrow, or shelve. `# escalation-ack` records a fresh decision and resets only this local streak. |

  Standing authorization is a separate GitHub-backed budget. It counts ROUNDS, not
  local marks: a round is a distinct head that drew FINDINGS from any GitHub App
  reviewer (owner ruling, 2026-10-01). Every reviewer on one head collapses to one
  round; a dismissed review with findings still counts; a head a force-push removed
  still counts. Only an explicit informational class opens nothing — Codex P3, Devin
  🔍, CodeRabbit trivial/info and its nitpick section — while a comment a known parser
  cannot read, and any top-level comment from a reviewer with no known format, DOES
  count. Humans, workflow bots and CodeQL never open a round. A clean review, a Codex
  clean comment and the configured identity template CONFIRM a head; they never add a
  round. Evidence from before `ROUND_RULE_CUTOVER_ISO` (`scripts/review_budget.py`)
  keeps the old rule (every head Codex reviewed, clean included), so no PR's count
  moved at the cutover. The rule lives in `review_findings.is_finding` and
  `review_budget.evaluate_evidence`; read those, not this paragraph, for an edge case.
  Ordinary PRs carry standing authorization through four rounds. Starting with the
  fifth review request or the fix commit after four rounds, every action needs its
  own native user approval.

  **ROUND 4 IS TERMINAL (owner ruling, 2026-09-25).** There is no ordinary round 5.
  At four rounds the decision is not "another round" — it is MERGE, with the
  outstanding issues accepted and FILED, or SEND IT BACK for rework. A fifth round
  exists only where the owner explicitly authorizes one, and that authorization is
  re-asked EVERY subsequent round, each of which is equally terminal and faces the
  same decision. Earlier text here read "round 6 and later are strongly discouraged",
  which invited a session to treat round 5 as ordinary; the discouragement was never
  meant as a softer tier but as the mechanism that stops an authorized fifth round
  coasting into a sixth unasked.

  This approval is never a shell sigil and is never persisted. `# escalation-ack` still
  serves the local round-3 intervention; `# final-round-accept` is legacy syntax and
  authorizes no current gate. An earlier "okay" cannot authorize a later request or
  commit. Foreground sessions receive the native approval dialog at the action;
  autonomous/dispatched sessions are denied because nobody is present to answer it.
  Unreadable or ambiguous GitHub evidence takes the same ask/deny direction and never
  becomes zero rounds. Multiple gated actions must be split so one approval cannot cover
  several commits or review requests.

  The native approval enforcement in this section is implemented at Claude Code's
  `PreToolUse` hook boundary. The shared budget evaluator is reusable, but external
  coding agents do not yet receive an equivalent approval prompt from this change and
  must not be described as mechanically protected by it. Within Claude Code, keep each
  protected action in its own command: a HEAD-moving `git switch`/`git checkout` cannot
  share a command with `git commit`, and a command containing multiple possible review
  requests is rejected before any request is sent. Inline comment bodies containing
  shell expansion are opaque at the hook boundary and therefore take the ask/deny path.

  The round-2 block is not the round-3 cap arriving early — it is a different
  instruction. It says the *approach* is wrong (you are fixing instances, not the
  class), where the cap says *stop and re-decide*. Acking round 2 without actually
  doing the fresh-context audit is how a session arrives at round 3 having learned
  nothing. `# audit-ack` attests that the audit HAPPENED; it is not a "continue"
  button. Note the fresh-context subagent this tier mandates is INTERNAL — mark it
  plainly (`--source internal`, the default); it satisfies the depth gate and does
  NOT advance the counter, so it can never be the round that hard-blocks you. Only a
  repeat EXTERNAL (Codex, the configured secondary, …) non-convergence moves the streak
  toward the cap.
  (Origin, 2026-09-01: under the old model the audit this very tier demanded counted
  as round 3 and tripped the HARD cap — the gate penalized the remedy it mandated.)

  There is a separate depth tier that can fire at ANY round: a **substantial**
  change (≥50 reviewable lines OR >1 code file OR auth/api/migrations OR any
  prompt/agent/skill surface) whose marked review is not adversarial is blocked
  with `# depth-ack`. A `mark` that lands with no `file:line` engagement prints a
  WARNING and will be rejected by that gate — re-write the evidence with concrete
  anchors rather than acking past it.

  `review_state.py` still keeps the local `round` streak and legacy lifetime
  fields for stale-worktree compatibility. Current authorization uses
  `scripts/review_budget.py`; the commit gate reads the local streak from one snapshot
  only for the round-2/round-3 interventions. The old lifetime APIs remain
  import-compatible but current gates do not consult them. `FINAL_ROUND_CAP` is
  **DELETED** (2026-09-25) — it was consulted by no gate, in zero conditionals, while
  its value had been picked to avoid colliding with the four-head boundary, so a
  retired tier was still shaping the live one and reading as though a seventh round
  were real. The `final-round-accept` SIGIL is still parsed and loudly refused, so a
  stale worktree is still told it is dead.

  **THE COUNTER IS CROSS-MODEL ONLY.** The streak exists to catch *cross-model
  non-convergence* — an EXTERNAL reviewer finding NEW defects round after round. It does
  NOT exist to penalize the free, encouraged act of reviewing your own work. So `mark`
  takes a `--source {internal,external}` (default `internal`) that records WHO PRODUCED THE
  FINDINGS, and that is what decides whether the round counts.

  **EXTERNAL is judged by the reviewing MODEL, not the gateway/provider — this is the "big
  one".** External = a review by a non-ANTHROPIC model. Anthropic Claude via ANY route
  counts as INTERNAL — including a Claude model reached through OpenRouter (the repo's
  `openrouter-haiku/sonnet/opus` routes are Claude), so "it went through OpenRouter" NEVER
  makes a review external. And Genesis's OWN cognitive/routing systems are never reviewers:
  they are cognitive infrastructure, not a review service, so no internal Genesis model call
  is ever `--source external`. The approved methods are **Codex** plus whatever the
  install names as its SECONDARY reviewer — `merge_gate.secondary_reviewer` in local
  `genesis.yaml` — one value, the model or CLI to invoke; **absent means there is no
  standing secondary**,
  and round 1 of the gate-fix lane below runs Codex-only. It is named in config rather
  than here because which model an install can reach is install-local, and a version
  frozen into this file goes stale the day the model does. **OpenRouter is NOT an
  approved method** (a future option, not a current one).

  Two mechanics for that key, because it is unlike its neighbours: it is read by
  SESSIONS, not by the gate — no code consults it, so a typo degrades to "no
  secondary" rather than failing a check. And add it under the EXISTING `merge_gate:`
  block: a SECOND `merge_gate:` block trips the duplicate-key scan that
  `_required_ci_workflows` / `_required_scheduled_review_kinds` / `_doc_findings_mode`
  all share, and all three then discard your configured value and take their DEFAULT.
  Say "default", not "fail closed" — the directions differ, which is the whole reason
  it matters, and all three can now LOOSEN: `_doc_findings_mode` defaults to `skip`
  (`_DEFAULT_DOC_FINDINGS_MODE`; cite the SYMBOL — the line this used to name now holds a sibling constant), scoring fewer findings and saying nothing about it;
  `_required_ci_workflows` falls back to the shipped `("CI",)`, which is NARROWER than
  any larger required set an install declared — it does print a NOTE, so that one is
  loud rather than silent, but it is still a relaxation (Codex P2, #1903); and
  `_required_scheduled_review_kinds` now defaults to the MINIMAL set `("leaks",)`
  (⚠ it defaulted to the maximal `code-review` + `leaks` until the default was
  narrowed, so a discard there TIGHTENED — it now narrows instead, and likewise
  prints a NOTE when the key was visibly declared). The floor survives every
  discard: `leaks` is irreducible. The rule below
  keys on this:
  - **`--source internal` (the default)** — a same-model self / genesis-architect /
    genesis-security / any-subagent review. It is free and shares the author-model's
    blind spots (rubber-stamp risk), so it **NEVER moves the streak** — not an
    increment, not a reset — whatever it found. No outcome flag is needed (a bare
    `python3 scripts/review_state.py mark --agent-output <path>` is a valid internal
    review). This is the fix for the weeks of false blocks: your own audits, including
    the one the round-2 mode-switch gate itself *mandates*, can never trip the cap.
  - **`--source external`** — a non-Anthropic cross-model reviewer drove this round.
    This is the ONLY kind that counts, so it REQUIRES exactly one outcome flag:
    - a NEW **BLOCKER / SHOULD-FIX / P1 / P2** → `--defects`
      (`… mark --agent-output <path> --source external --defects`) — the round counts.
    - **no** new BLOCKER/SHOULD-FIX/P1/P2 → `--clean`
      (`… --source external --clean`) — RESETS the streak (circuit-breaker
      reset-on-success). Only an EXTERNAL clean round resets; an internal "looks
      fine" can never reset a standing cross-model streak (that would be a
      self-rubber-stamp reset).

  `--source` describes the REVIEW THAT PRODUCED THE FINDINGS, not who typed the
  evidence file: a mark recording "verified + fixed Codex's (or the secondary's) findings" is
  `external`; a mark of your own architect/security audit is `internal`. A round is
  CLEAN iff the external review found no BLOCKER/SHOULD-FIX/P1/P2 (and no security
  CRITICAL/WARNING). NOTEs, nitpicks, and dispositioned optional-hardening do NOT make
  a round defect-bearing. The ack (`# audit-ack` / `# escalation-ack`) is a conscious,
  logged act (like `# review-override`); adding it — or falsely passing an external
  `--clean` — WITHOUT the honest review result is the same violation as ignoring the
  prose above. (Supersedes feea3f71/#1446: its unconditional required-outcome only
  ever bit internal re-audits, which now can't inflate the streak at all; the outcome
  requirement is kept where it is still load-bearing — on external marks.)
  Caveats: **multiple findings in a single pass = one round** (not an
  escalation); the same defect reappearing (an incomplete prior fix) is a
  fix-it-properly issue, not an escalation trigger. This complements the
  enumerate-class-then-lock convergence discipline — the cap is the escalation
  trigger when the class won't lock within ≤3 rounds. (Origin: PR #1281 ran ~7
  reviewer rounds because a standing "proceed once clean" silently carried
  through rounds 4–6.)

  **The Claude Code PR-side hook uses the same shared evaluator.** A review request reads the
  rounds live from GitHub. Below four ordinary rounds it proceeds under
  standing authorization. At four or more it emits a native user approval for exactly
  one request; at five or more its message strongly recommends stopping. API, parse,
  prefix-resolution, changed-file, or PR-identity uncertainty asks in foreground and
  denies in autonomous sessions, and so do findings a review's body declares but whose
  comments were deleted. A clean review comment's abbreviated SHA is resolved against the
  PR's full commit list, and an ambiguous prefix makes the result unknown.
  `--check-pr` prints the count as an advisory `rounds` row, with findings per round and
  a RISING marker; it never moves the verdict.

- **GATE-FIX LANE — a hook-surface PR gets two standing discovery rounds
  (standing user rule, 2026-09-09).** The shared evaluator now enforces the distinction
  mechanically. After exactly two rounds, a current unreviewed head may receive
  ONE confirmation request without another approval only when the request carries
  `<!-- genesis-review-request head=<full-40-hex> kind=confirmation -->`. The marker is
  dispatch evidence, never approval or review evidence. Once present, a repeated request
  needs fresh approval. A review after that confirmation, or any further discovery
  request, also needs fresh approval. Because a CLEAN confirmation adds no round, the
  marker itself is the one-shot token: once a marker exists for any head other than the
  current one, the confirmation is SPENT and every later request and fix commit asks. A
  head that drew findings itself is a round, never a fix to confirm, so it gets no free
  request. (Residual, by owner ruling: an approved UNMARKED request that comes back
  clean leaves the marked one unspent.)

  **Scope: any PR whose diff touches the enforcement-hook surface** —
  `HOOK_SURFACE_PREFIXES` + `HOOK_SURFACE_FILES` in `scripts/review_budget.py`.
  Read those constants; do not copy them here — a hand-copied list is wrong the moment
  a hook is wired. `TestWiredHooksFenceGuardrail` keeps the WIRED-HOOK half
  self-maintaining FOR THE SPELLINGS IT RECOGNISES, by parsing
  `.claude/settings.json` (measured 2026-09-10: **54** wired command entries, **49**
  of them matched, **5** unmatched, yielding **42** unique paths — an earlier draft
  said "42 of 54", which divides a path count by an entry count and so implies twelve
  misses where there are five. The five are `.claude/hooks/*.sh` and inline blobs
  already covered by the prefixes, so no live gap — but a `python3 -u scripts/foo.py`
  or `node …` wiring would slip past it); it cannot do the same for the tracked CONFIGS the
  hooks read, because settings.json lists hooks and not the files they consume. Those
  entries are maintained by hand, so adding a config a hook's behaviour depends on means
  adding it to `_HOOK_SURFACE_FILES` deliberately (Codex P2, #1903). This lane is a STRICTER-REVIEW, FEWER-ROUNDS trade on that surface. It does
  not apply anywhere else, and applying it to an ordinary PR doubles review spend for
  nothing. The test is MECHANICAL and deliberately wider than the rationale — the
  surface includes hook wiring and the tracked configs the hooks read, so a PR that only
  incidentally edits one is still in the lane. That is the intended trade: a mechanical
  test nobody has to adjudicate beats a judgement call about whether an edit "really"
  changes the gate. Throughout this block "gate PR" means exactly "hook-surface PR".

  1. **Round 1 is DUAL and immediate.** CONFIRM Codex has fired (it auto-reviews when
     the PR opens). `--check-pr <N>` shows whether a review EXISTS at this head; it
     CANNOT distinguish "never requested" from "still running", so if you did not
     request one yourself, request one rather than reading a blank line as proof
     nobody did. Then
     run the secondary reviewer (`merge_gate.secondary_reviewer`) alongside it — two
     sets of blind spots in one calendar round, which is the scarce resource here. No
     secondary configured → Codex-only, and say so in the PR. Nothing validates that
     key, so a TYPO looks exactly like "absent" — read the value before concluding
     there is none, or the lane quietly runs at half strength while the PR truthfully
     reports Codex-only.
     **This lane carries STANDING approval for the secondary, and it is the ONE place
     that overrides the per-use rule above.** The general rule — an alternate reviewer
     needs explicit per-use approval, and only when Codex is UNAVAILABLE — still governs
     everywhere else; here the secondary runs alongside a healthy Codex, at PR open,
     without asking (owner decision, 2026-09-09). Do not generalise this to any other PR.
     **One head = one round on every counter, however many reviewers saw it**; write the
     local mark ONCE with combined evidence. That single mark is `--defects` if EITHER
     reviewer raised a new BLOCKER/SHOULD-FIX/P1/P2 — a clean secondary does not launder
     a defect-bearing Codex round.
     **Two separate marks do NOT halve the budget; they can DISARM it, which is worse.**
     An earlier draft said "halves", and the code says otherwise: `bump_review_round`
     increments only on a DISTINCT staged hash, so a second defect mark on the same diff
     is idempotent and costs nothing (`review_state.py:850-865`). But `--clean` resets
     the streak to 0 **regardless of whether the staged diff changed** (`:780-784`), and
     an idempotent same-hash `--defects` cannot restore the round it just erased. So on
     one staged diff, in EITHER order, a mixed clean/defect pair records as CLEAN — the
     defect-bearing round vanishes and the cap it was arming goes back to zero. That is
     the failure the one-mark rule prevents (Codex P2, #1903).
  2. **Triage before touching code — and here it OUTRANKS "fixing is usually free".**
     That rule (below, and right for ordinary PRs) says a cheap finding costs nothing to
     fix so just fix it. On this surface the cost is not the edit, it is the PUSH: it
     invalidates the review, and a push whose DELTA touches the hook surface is never
     classed review-trivial (`_classify_post_review_delta` tests the DELTA's files, not
     the PR's), so a one-line courtesy fix to guard code buys a whole extra review
     cycle. A push of ORDINARY PROSE can still ride the existing
     review — that is the one cheap fix this rule does not charge you for, and note
     how narrow it is: `_is_docs_or_config` DELIBERATELY excludes prompt surfaces
     even when they are `.md` (`review_enforcement_commit.py:114-127`), so a skill,
     command or agent file is SUBSTANTIAL and buys the full cycle
     (`review_scope.classify_compare_substantiality`). Since a gate PR's docs are
     usually exactly those files, assume you are paying (Codex P2, #1903). A cheap NON-floor finding gets a reply,
     not a commit. Merge both reviewers' findings into ONE table,
     then classify each: {live bug | latent trap | hardening | observation}. Only live
     bugs and cheaper-now-than-later traps change code. The rest get a maintainer reply
     carrying the evidence — IN-THREAD on the inline comment, from a maintainer
     account: that is the ONLY shape the gate scores as engagement (it reads
     `in_reply_to_id` on `pulls/N/comments`, so a top-level PR comment engages
     nothing). Two findings therefore have no thread to reply into and must be
     documented in the PR body instead: anything the SECONDARY reviewer raised, which
     runs locally, and anything living in a review BODY rather than an inline comment.
     Engagement costs ZERO rounds. Most findings never needed to become a diff.
  3. **One batched fix push per round, self-audited first.** Not two, not three: every
     push invalidates the review, so three pushes turn one round into three review
     requests. (A compliant run has at most two CODE pushes total — the free
     ordinary-prose push of rule 2 does not count against it — one answering round 1,
     and rule 4's terminal push if the floor demands one.)
     Before pushing, run the adversarial pass over your own FIX-CODE — reviewing your
     own work needs no approval and is the specific gap that keeps biting. READ, three
     instances (#1856, #1686, e7ae445a5): each a fail-open introduced BY fix-code
     written under review pressure, and each caught only by a later round. The danger is the fixing, not the
     reviewing.
  4. **Round 2 is the discovery stop.** The two branches are: accept
     and merge with every outstanding finding documented in the PR body, or abandon and
     re-cut from a design that does not need a third round. **The always-fix floor is
     never accepted** — a P1, a security defect, anything destructive or fail-open gets
     FIXED in the terminal push. "Accept and document" covers everything that is NOT
     floor-class, so a round 2 carrying floor findings has only two branches: fix them,
     or abandon.
     **A terminal push whose delta touches the hook surface needs a fresh review at that
     head** — that delta is never review-trivial, by design (a docs-only terminal push
     is the exception, and may ride the existing review — ORDINARY PROSE only, the
     same narrowness rule 2 spells out: a skill, command or agent file is a PROMPT
     SURFACE, classifies SUBSTANTIAL, and needs the fresh review even though it is
     `.md`. That is the identical claim rule 2 already corrects, and it survived here
     for a round because the fix was applied to the sentence that was flagged instead
     of to every sentence making the claim). That confirming review
     is a GATE REQUIREMENT, not a discovery round: if it is clean you merge. If it
     surfaces something new, the SEVERITY routes it, exactly as it does anywhere
     else in this rule — a non-floor finding is accepted and documented like any
     other, and only a FLOOR-CLASS one (P1, security, destructive, fail-open) goes
     to the owner under rule 5. An earlier draft sent everything to rule 5, which
     contradicted the accept-and-document branch two sentences above and left one
     confirmation result with two required actions (Codex P2, #1903). A round 2 with NO
     floor findings needs no push at all, so there is nothing to re-review.
  5. **A NEW P1 on the fix-code at round 2 is an owner decision, not round 3.** That is
     the measured tripwire: a PR whose fix introduced a P1 is the kind that introduces
     another. Stop and ask. The P1 itself is NOT optional (rule 4's floor) — what the
     owner is deciding is fix-and-merge versus abandon-and-re-cut, never accept-and-ship.
     **With no user to ask** (a dispatched session): do not merge and do not open round
     3 — comment on the PR naming the tripwire and the P1, apply the
     `needs-architecture-session` label, move it to draft (`gh pr ready <N> --undo`),
     and open a `ready` follow-up (a dispatched session's follow-up lands in the `tabled` lane; the intake gap is tracked in #2857), the same
     unattended route the architecture-session rule below uses.
  6. **After round 1 the diff SHRINKS in SCOPE, never grows.** Line count is not the
     measure — a floor fix under rule 4 may add lines and is mandatory. What may not
     grow is what the PR is FOR. A revert is fine; new capability is a new PR. This is what actually kills whack-a-mole — the loop is sustained by
     the diff expanding under review, not by the reviews themselves. (Composes with
     "Keep the PR the PR" below: adjacent findings become issues.) Against
     enumerate-the-CLASS above: fixing every instance of ONE defect inside the files
     this PR already changes is the same fix, not growth — that is the shape the class
     rule asks for. Reaching into files the PR does not touch, or adding capability to
     make a class fixable, is growth: that is an issue or a follow-on PR. Ask "Keep the
     PR the PR" FIRST — a PRE-EXISTING sibling instance the PR works fine without is an
     issue even when it sits in a file you already touch; this rule governs the reach of
     a fix you have already decided to make.

  **The machine implements this lane.** Gate-surface classification comes from the
  shared `review_budget` path predicate, including rename sources and destinations.
  The second-round fix is permitted, and the exact-head marked confirmation is the one
  exempt request. If that confirmation finds another defect and a further fix is needed,
  the commit requires fresh native approval. The independent local streak may still
  demand its round-2 class audit or round-3 stop; those are process interventions rather
  than substitutes for the per-action user decision.

  **Queue priority:** gate PRs get reviewed and driven before ordinary ones — read this
  as a THIRD standing justification alongside the two in "When to DRIVE a Merge" below,
  granted for the same reason those two are: a gate fix is what unblocks the PRs stacked
  behind it, so leaving one to bake is the queue declining to repair itself.

  **On the word "round" here:** one head that drew findings is one round however many
  reviewers saw it. The shared evaluator deduplicates reviewers by commit identity, so
  a dual round does not spend two rounds.

  **A note for anyone reading only CLAUDE.md:** the shared evaluator now gates a third
  discovery request mechanically. The local round-2 MODE-SWITCH still acts on commits
  and remains a separate class-audit intervention.

- **Some PRs are not a review problem — hand them to an architecture session.**
  When you ARE driving a PR toward green (per "When to DRIVE a Merge" below),
  asking a few clarifying questions is not a substitute for the design
  conversation. STOP and hand the PR to a foreground architecture conversation —
  a session with the USER present, never a `genesis-architect` subagent, which is
  review-session judgment by another name — when any of these holds: the conflict
  is STRUCTURAL, main having superseded the mechanism the PR implements, so no
  conflict resolution is correct on its own terms; it changes or contradicts the
  spec at a significant level; it raises real questions about how Genesis
  operates, its architecture, or its infrastructure; its consequences are
  far-reaching or irreversible; or it is delicate enough that getting it wrong
  damages a running Genesis. Treat the local round-2 mode switch, the round-3 hard
  stop, or a native approval after the standing round budget as a trigger
  to ask whether this is that kind of PR rather than merely another
  round — taking this exit does NOT discharge that tier's own stop, which is
  still owed; the handoff is the answer you bring to it, not a way around it.
  If you ARE the session with the user present, hold the conversation now. With
  no user to ask, the action is: comment on the PR naming WHICH trigger fired and
  the evidence for it, apply the `needs-architecture-session` label (create it if
  the repo lacks it — labels are per-repo and forks do not inherit them), move
  the PR to draft (`gh pr ready <N> --undo`; see "PR readiness and mode"), open a
  `ready` follow-up naming the PR and the decision it awaits — the label is a
  GitHub annotation nothing drains, so the row is the intake (a dispatched session's follow-up lands in the `tabled` lane; the intake gap is tracked in #2857) — and move on to the
  next PR. Expect it to be uncommon: the owner's estimate, explicitly unmeasured,
  is on the order of 1 in 10 or fewer, so a session reaching for it often is
  mis-triaging. Worked example, PR #1605: main's shared settings writer had
  replaced the two inline heredocs the PR reimplements, and the literal base-wins
  resolution measured 61 passed → 36 failed / 25 passed, because it deleted the
  PR's own fix on two of its three declared paths.
- **External review feedback is a set of claims to VERIFY, not orders.** For
  every bot/external finding: check it against the actual code (its stated
  mechanism may be wrong even when the underlying concern is real — quote the
  disproving file:line), check whether the "fix" breaks existing behavior or
  violates YAGNI, and push back with technical reasoning when it's wrong for
  this codebase. A finding that conflicts with the user's prior design
  decisions (e.g. live network calls in a hot autonomy gate, weakening an
  approval gate) is a STOP-and-discuss, never an auto-fix. Chasing a reviewer's
  green checkmark with a change you believe is wrong is a discipline failure.
  No performative agreement — state the verified fix, or the reasoned pushback.
- **Refuting a finding is itself a CLAIM, and it meets the same evidence bar you
  just held the finding to.** The bullet above is about pushing back when the
  reviewer is wrong; this is the cost of pushing back when YOU are. MEASURED
  2026-09-09: a reviewer session refuted a cross-model reviewer's severity
  assessment with a structural argument — "an expansion in verb position always
  corrupts argv, so the exploit cannot exist" — which covered a few sampled
  instances of one spelling rather than
  the construct's grammar; a builder session then refuted the refutation with a
  working construction, and the original severity was right all along. The tell
  was sitting in the refutation's own text: it said *"I have NOT proven no clean
  construction exists"* and stated the conclusion in the next sentence.
  **An acknowledged gap in a proof is the reason NOT to state the conclusion —
  never a hedge that licenses stating it.** So: a structural refutation must cover
  the CONSTRUCT'S GRAMMAR — every form the syntax admits — not a sample of its
  instances. Where it cannot, the finding STANDS and gets fixed or escalated on
  its own merits; "I could not construct one" is a null result and clears nothing
  (see the acceptance-bar section). Downgrading a severity is a refutation too,
  and costs exactly the same evidence as dismissing the finding outright.
- **Waiving the review GATE is not waiving the FINDINGS.** When the user says
  "skip the review" for a trivial change, that waives the blocking *ceremony*
  (the gate and its `*-override` sigils — note `# review-override` is the one
  that waives the findings scan) — it NEVER licenses ignoring a reviewer's
  *substantive* findings. Read Codex's inline findings
  (even non-blocking P2s) BEFORE merging even when the gate is waived, and
  engage each on merits: verify it, then fix or consciously accept with a
  stated reason. Merging past unread findings on a "skip review" is a trust
  breach, not obedience. (Origin: #1439 merged past 3 correct Codex P2s.)
- **"Not blocking" describes the GATE, not the finding — and fixing is usually
  FREE, so there is no tradeoff to weigh.** The subtler sibling of the rule
  above: there, a waiver was granted; here the gate legitimately passes the
  finding on its own rules, and the pass gets read as "handled".
  MEASURED 2026-09-03 — two correct non-blocking findings merged UNFIXED because
  nobody read them: #1620's P2 (an invalid `wing` raises `ValueError`, and
  `tool_api.py:200-202` maps every exception to HTTP 500, so a caller input error
  reads as a retryable server failure) and #1606's doc-path P1 above.
  **Neither cost anything to fix.** A docs-only follow-up commit is
  review-TRIVIAL: `classify_compare_substantiality` returns `inline` for doc
  paths at ANY size, and `_classify_post_review_delta` tests the DELTA's files
  rather than the PR's — so a CHANGELOG-only fix stays trivial even on a
  hook-surface PR (verified: CHANGELOG-only → `inline`; a 1-line guard change →
  `substantial`). So it costs zero CODEX rounds — but not zero blocks, and the
  difference matters when you are budgeting the follow-up. On the canonical
  public repo the SCHEDULED-review gate is head-pinned per kind, and `leaks` —
  the only kind required by default — has ancestor relief
  (`_MECHANICAL_RESCAN_BY_KIND`): the carried marker still satisfies the gate
  once `leak-detector` is green at the new head. Budget the follow-up as: one
  commit, no Codex round, no new scheduled review. An install that has ADDED
  `code-review` back via `merge_gate.required_scheduled_reviews` budgets one
  more: that kind has no ancestor relief, so the push invalidates its marker and
  the gate blocks until a fresh scheduled review lands at the new head.
  So: read every finding the report prints, fix the cheap ones, then merge.
  The read is what the standing merge-when-green policy is buying — a gate
  verdict of `ok` is not a report that there is nothing there.
  And the inverse failure is as costly: do NOT escalate a cheap finding into a
  mechanism change. The same session diagnosed its own inattention as a gate
  defect and proposed weighting that would have taxed every PR — *"we'll never
  get anything shipped if that's the standard"* (owner). Before proposing any
  gate change, ask whether the mechanism is broken or you simply did not do the
  work it assumed you would.

## The Gate Machinery — the sequence, and why it bites

Ten enforcement layers sit between a change and `main`. Learning them by hitting
them costs a session real time, every time. The canonical sequence:

```bash
python3 scripts/review_state.py evidence-path     # -> ~/.genesis/review_evidence/<key>.txt
# ... write the adversarial audit to exactly that path ...
git add <files>                                   # STAGE FIRST — mark hashes --cached
python3 scripts/review_state.py mark              # INTERNAL genesis-architect audit — plain mark, never counts
git commit -F <msg-file>                          # bare, not piped (see below)
# (a non-Anthropic cross-model round would instead be: mark --source external --defects|--clean)
git push                                          # approve the dialog on a branch's first push
gh pr create ...
gh pr comment <N> --body "@codex review"          # after EVERY subsequent push
python3 scripts/hooks/git_push_guard.py --check-pr <N>
gh pr merge <N> --squash --admin --match-head-commit <head>   # verbatim from --check-pr
```

**Ordering and lifetime rules that are not obvious:**

- **Stage → mark → commit.** `mark` hashes `git diff --cached`. Re-staging or
  amending after marking invalidates it, and the check fails CLOSED.
- **Evidence expires in 30 minutes.** The audit file must be recent when you
  `mark`, and the marker itself expires on the same clock. A long detour between
  audit and commit means re-marking.
- **`evidence-path` and `mark` key off the PROCESS cwd**, not a flag. Run them
  from the same worktree as the commit or the key diverges silently.
- **A successful commit WIPES the marker.** The next commit needs a fresh review;
  this is deliberate, not a bug.
- **`--match-head-commit` is mechanically required**, not a nicety — the merge
  arm blocks outright when a verified head exists and the flag is absent. Copy
  the whole command from `--check-pr` output rather than reconstructing it.

**Traps with a real cost, each one measured:**

- **ANSWERING A FINDING TAKES AN IN-THREAD REPLY. A top-level PR comment
  engages NOTHING.** The gate clears a finding from the score only when a
  MAINTAINER account replied IN ITS THREAD: it reads `in_reply_to_id` from
  `pulls/N/comments`, so a reply is matched to the inline comment it answers.
  `gh pr comment N --body ...` creates an ISSUE comment, which has no
  `in_reply_to_id` and answers nothing however thorough it is. Use:

      gh api repos/<owner>/<repo>/pulls/<N>/comments \
        -f body='verified and fixed at <sha>: …' -F in_reply_to=<comment_id>

  (comment ids from `gh api repos/<owner>/<repo>/pulls/<N>/comments`.)
  MEASURED 2026-09-09: switching to this form took two PRs from
  `inline-findings: BLOCK` to `ok` with NO code change — 4 findings on #1847 and
  5 on #1836 had been fixed for hours and still scored, because every reply was
  top-level. The symptom is a score that will not fall while you are certain
  you fixed everything; reach for this before you reach for an override.
  TWO FINDINGS HAVE NO THREAD AT ALL and cannot be engaged this way: one
  delivered in a review BODY rather than as an inline comment, and anything a
  locally-run secondary reviewer raised. Document those in the PR body.

- **A BLOCKED Bash call runs NOTHING** — including earlier `&&` segments and
  heredocs. If `mark && commit` is blocked, the `mark` did not happen either.
  Run gate-adjacent steps as separate calls.
- **A secrets.env access that did NOT prompt may have been silenced, or may
  have escaped the guard.** The guard matches only some access paths, so an
  unprompted access proves nothing by itself. An install may set
  `hooks.asks.secrets_env: off` in `~/.genesis/config/genesis.yaml`, which
  removes the credentials guard's prompt: the hook then emits no decision, only
  a context note naming the setting. That note is what proves a silenced
  access; with no note, assume the access went past the guard unseen. It
  approves nothing; other hooks and Claude Code's own permissions still decide
  the command, and a dispatched session is still denied. The only other key is
  `push_publish` (owner ruling 2026-10-01), same no-decision-plus-note shape.
  It silences only the first push of the CURRENT branch, and only when the
  whole command is exactly one plain `git push` (e.g. `git push -u origin
  HEAD`) — a chained command still asks, so run the first push as its own
  command. The remote git really pushes to must resolve (rewrites applied) to
  exactly `https://github.com/<public repo>` — ssh/scp forms always ask — with
  simple push config, no `http.*` config, no proxy/TLS/ssh/config env var in
  the hook's environment, and a live probe confirming the branch is absent
  there. Residue: shell-profile env/aliases/functions/PATH are invisible to the
  hook, and a concurrent process can still change config or publish the
  branch between check and push. `gh pr create` is not covered, since gh
  without a TTY aborts rather than pushing. Force pushes, other destinations,
  close-then-push, the no-open-PR block, round-cap asks and the dispatched deny
  are untouched, and any doubt about the destination keeps the prompt.
- **Ack sigils bind per-guard, and mostly to the LAST pipeline segment.**
  `git commit ... | tail  # audit-ack` puts the ack on `tail`. Run the commit
  bare. Some guards accept a sigil on any segment, others only on the offending
  one — do not generalise from one guard's behaviour.
- **Sigils must lead the trailing comment.** The override is read from the
  leading run of recognised tokens, so `# see audit-ack notes` overrides nothing.
- **`--no-verify` is blocked before any override is even considered.** There is
  no way to skip the native hooks; fix the cause.
- **Chained commits are heavily restricted** — across worktrees, blocked
  outright; within one, later commits must be pure `--amend` with no intervening
  git command. `cd "$VAR" && git commit` fails closed with a *branch-verification*
  message, which reads like a branch problem and is not: use a literal path.
- **Worktree removal is not yours to do.** `git worktree remove` is blocked;
  `scripts/worktree_lifecycle.py` owns it: it archives stale worktrees on a
  daily timer and never deletes them (archive retention is off, #2504). Leave a dead worktree alone.
- **Editing a tracked git hook blocks the commit** until its hash is re-recorded
  (`scripts/update_hook_versions.sh`).
- **⚠ `scripts/hooks/*` is NOT synced — and a WORKTREE edit is still not live.**
  `sync-hooks.sh` copies only the five GIT hooks (`commit-msg`, `post-commit`,
  `pre-commit`, `prepare-commit-msg`, `pre-push`) plus one helper into
  `.git/hooks/`. `.claude/hooks/genesis-hook` launches entry-point hook scripts
  such as `git_push_guard.py`; that guard imports the shared `shell_parse.py`
  module. `hook_output.py` is instead imported by other hook entry points. The
  launcher resolves `HOOK_ROOT` to the **MAIN
  worktree**, not the tree you are sitting in
  (`HOOK_ROOT="$MAIN_ROOT"` unless `GENESIS_HOOK_DEV_LOCAL=1`). That is
  deliberate — it stops per-branch hook drift, measured 2026-08 at 60 of 70
  worktrees running a stale `full_suite_guard`. Three consequences:
  (a) editing a guard in a worktree changes NOTHING until the branch reaches main;
  (b) to exercise a guard change in place, set `GENESIS_HOOK_DEV_LOCAL=1` — it
  runs the worktree copy and ANNOUNCES itself on stderr, so it is never a silent
  downgrade; (c) **a main checkout behind `origin/main` runs a STALE gate in every
  worktree** — MEASURED 2026-09-10 at 7,535 lines local vs 8,971 on `origin/main`,
  a 1,436-line divergence that had been silently deciding every gate call. After
  merging a PR, sync the main checkout, or the gate you are testing against is not
  the gate that ships.
  Two earlier versions of this bullet were wrong in opposite directions: one said
  such an edit "changes nothing until `sync-hooks.sh` copies it" (wrong mechanism),
  the replacement said it is "live on the very next Bash call" (wrong in every
  worktree, which is where this repo mandates you work).
- **`.github/**` and prompt surfaces are never "docs"** for the doc/config
  auto-allow (`_is_docs_or_config` returns False for both). They are NOT
  equivalent beyond that: only a PROMPT surface forces `substantial` and thereby
  the depth gate. A lone sub-50-line `.github/` workflow edit classifies as
  `inline` (its `_scope_tag` is `config`, which is not domain-sensitive), so
  Rule 2.5 demands nothing. An earlier version said both "always reach the depth
  gate", which over-states the workflow half.

**CI is not one check.** `ci.yml` defines **15** jobs. `review-depth-check` and
`cc-pin-receipts` are **Advisory BY DESIGN** (they always exit 0); a red result
from another job blocks the local gate. `dependency-audit` has a narrower
exception: the audit command reports vulnerability findings without making the
job red, while setup or installation failures can still fail the job. (An
earlier version said "ten of eleven … only `review-depth-check`", wrong on both
halves; count the `jobs:` keys and inspect the steps rather than trusting any
prose figure, including this one.) Only THREE checks — `test`, `leak-detector`,
and `lint` — are server-side REQUIRED, so a red local-gate job outside that trio
does not block GitHub. Several are
reproducible locally BEFORE pushing, which is far cheaper than a red PR:

```bash
ruff check src/ tests/ scripts/
python scripts/check_external_io.py
python scripts/check_subsystem_map.py
python scripts/check_shared_artifact_consumers.py
python scripts/check_frozen_clock.py
```

Do NOT run the full pytest suite locally (it is banned, and the concurrent-test
guard blocks a second run anyway) — CI's `test` job is the blocking one.

**Detecting a running pytest: match argv STRUCTURE, never a substring.** Any
check that greps command lines for `pytest` matches ITS OWN command line —
`pgrep -f "python -m pytest"`, `ps | grep`, and a bash `case *pytest*` all
self-match, as does another session's wait-loop. Test `argv[0]`'s basename, or a
python interpreter with an adjacent `-m pytest`.

## Pre-Commit Gate

Verify before any commit:

- `git diff --cached --stat` — every file in the diff belongs to your work
- `git status --short` — check untracked files (should be staged or ignored)
- Review level applied matches the adaptive protocol above
- Staged files do not include secrets (`secrets.env`, `.env`, credentials)
- **New-Store Gate (anti-proliferation).** A new persistent store — a DB table, a
  Qdrant collection, a file-plane under `~/.genesis/` — needs a written
  justification for why an existing store cannot hold it, plus a note on how it
  stays consistent with related stores (and its retention + backup path). The
  memory subsystem already sprawls across ~9 logical systems / 3 physical planes
  because this gate did not exist; a table that "felt cleaner" is how store #10
  is born. Reuse an existing store or an existing convention (e.g. the
  `~/.genesis/eval/golden/` install-local golden-set convention, #1143) unless
  the justification is real. Prefer NOT reusing a store whose SEMANTICS differ
  (don't shoehorn a store-health row into the model-eval `eval_runs` table just
  because it is "a table that exists").
- **Private-data scan before every push (public repo).** Grep the ENTIRE diff
  (`git diff origin/main...HEAD`) for private/identifying data — real names,
  company/product names, emails, IPs, private career/project specifics, verbatim
  user messages. Check ALL surfaces, not just prose: **source comments,
  docstrings, and test fixtures/data** are the easy misses. Use a synthetic
  stand-in in tests, never the real private artifact. (2026-07-01: a verbatim
  private DM leaked via a test docstring + a code comment after the commit
  message and PR body were already clean.)
- GROUNDWORK-tagged code not accidentally deleted
- New capabilities registered in `_capabilities.py` + bootstrap manifest
- **Conventional commit prefixes**: `feat:`, `fix:`, `refactor:`, `docs:`,
  `test:`, `chore:`. Scope optional: `feat(ego): add cadence manager`.
  Subject line under 72 characters. Dominant category wins if mixed.
- **NEVER push to main or merge into main without a PR and user approval.**
  Enforced by PreToolUse hook.
- **Targeted tests during development.** Run ONLY the relevant test file(s)
  for your changes. NEVER run the full test suite locally — CI handles that.
  Check CI via `gh pr checks`. Bare `pytest` without a file path is banned.
- **Commit continuously**: after every logical unit of work. Uncommitted = lost.
- **PR closes a ledger item → cite `Ledger: <item-id>` in the PR body** (the
  32-hex `session_ledger` row id, own line, e.g. `Ledger: 71337fab…`). The
  repo-pulse worker auto-absorbs the row with PR evidence at the next session
  boundary — deterministic, reversible via `session_ledger_update`. A bare id
  mention WITHOUT the `Ledger:` marker is context, not completion (the pulse
  only proposes it). Find ids via `session_charter` or the charter injection
  block.
- **Anything a hook prints to CC's context has a hard, measured ceiling.**
  The harness FILES a hook's stdout above **10,000 characters per hook entry**
  (CC 2.1.246; undocumented, version-volatile — see
  `docs/reference/cc-compatibility.md`) and shows the model a 2 KB preview;
  the rest silently never arrives. The SessionStart injection is therefore
  four `--part` entries with per-part budgets, CI ceilings on every tracked
  identity file (`tests/test_scripts/test_context_injection_budget.py`), and a
  watcher over the harness's own filings. Adding protocol text to an identity
  file is a budget decision: put DETAIL in a reference doc and a pointer in the
  injected file. If you add or grow a hook's stdout, measure it against the
  cap; after a CC bump, re-run the probe (`GENESIS_CTX_PROBE_BYTES`).
  **Emit through `scripts/hooks/hook_output.py`** — the single home of the
  measured constant, whose `BoundedStdout` enforces the budget at the WRITER so
  a block that forgets to check cannot overrun (2 of 12 blocks checked when the
  budget was per-caller), and whose `print_json_bounded` trims named free-text
  fields while never sacrificing the JSON envelope — an oversized advisory must
  lose prose, never its `permissionDecision`. Only SessionStart,
  UserPromptSubmit and UserPromptExpansion carry bare stdout to the model; a
  Stop hook that `print`s on exit 0 is INERT.

  **NEVER compute characters at the call site — say what the DEGRADE looks
  like.** `out.emit_or_degrade(text, block=…, pointer=…, notice=…, reserve=…)`
  settles every number (the divider, `print`'s newline, the closing-line
  reserve, what is already emitted) and returns which branch it took. There is
  no `fits()` for emitters to call any more, deliberately: five review findings
  in one cycle were a caller re-deriving what the writer already knew — the
  audit reserve counted twice, a `fits` that undercharged by one and approved
  blocks the writer then destroyed, a 120-char reserve for a 206-char line, a
  pointer reserved in a part that cannot emit one, a `keep` derived from the
  budget constant instead of the room. The arithmetic was never hard; having it
  in six places was. An AST test bans `.room`/`.fits` from the emitter so the
  next "just one `fits` call" cannot reintroduce the class.

  The general shape, which is the transferable part: when several call sites
  must each remember to do something — bound their output, report a failed
  read, escape untrusted text — that is a CONVENTION, and conventions are what
  reviewers keep finding one instance of at a time. Move the obligation into a
  chokepoint the callers cannot bypass, then LOCK the chokepoint with a test
  that fails when someone routes around it. A chokepoint nobody is forced
  through is a convention with better documentation.

## Never reinvent what GitHub already does natively

**Standing user rule, 2026-09-09.** Before writing repo-local code that decides
something about a pull request, branch or merge, check whether GitHub already
enforces it: rulesets and required status checks, CODEOWNERS, review-thread
resolution, stale-review dismissal on push, path-based labelling, issue and PR
templates. If it does, use that — and only depart from it for a specific,
concrete reason GitHub cannot meet, stated where the code lives.

**Why this is a rule and not a preference.** Enforcement written here is a
script that every install must have wired, correctly, forever; enforcement
written as GitHub config is one versioned thing that binds identically for
everyone and cannot drift between installs. MEASURED on this repo: hook WIRING
had silently diverged between two installs in a security hook, which is
invisible to CI because the scripts are tracked and the wiring is not.
`git_push_guard.py` is 8,971 lines (measured 2026-09-10; it was 8,222 on 09-09 — a figure in a durable doc is a claim with a date, re-measure rather than quote), and the fraction of it that only reads GitHub
state is the fraction that never needed to be local.

**How the layers actually split** — this is a division of labour, not a
migration, and the third layer is not a consolation prize:

- **GitHub config** takes what is a FACT ABOUT GITHUB STATE: is this check
  green, is this branch behind, was this thread resolved, who owns this path.
- **A required check run** (issue #1670) takes composite judgements that still
  read only GitHub state — CI rollup identity, review freshness, finding
  scores. Port these one at a time, and DELETE each ported check from the local
  guard in the same PR. Two enforcers of one rule is replica drift with extra
  steps, and the copy nobody is watching is the one that goes wrong.
- **Local hooks** keep everything reading THIS BOX or shaping THIS SESSION:
  review evidence and markers under `~/.genesis/`, override sigils, the
  bash-safety and destructive-command guards, the round caps. These are not
  merge conditions at all — a PreToolUse guard runs before a command executes,
  which is a place GitHub has no presence. Round caps in particular: a research
  pass across production OSS review tooling found no implementation of them, so
  that machinery is ahead of the field rather than behind it.

**The check before you build:** name the GitHub feature you considered and why
it does not fit. "I did not think to look" is the failure this rule exists to
catch, and it is invisible afterwards — a hand-rolled implementation of a
platform feature looks exactly like a hand-rolled implementation of something
novel.

## Generalizability Gate — build for ANY install, not this one

Genesis is a public, cloneable system. Every change must work on ANY user's
install, not just the machine it was written on. Standing user directive.

**Hardware/scale adaptivity.** Other installs have different RAM, disk, CPU
count, and workload scale. Never hardcode absolute resource numbers or scale
assumptions:

- Memory/disk caps: percentage-of-available or config-derived, never fixed
  GB (precedent: #1029 percentage-based memory caps). Concurrency: derive
  from `os.cpu_count()`/config, never a literal core count.
- Hard minimums are allowed but must be EXPLICIT (documented in install
  docs/config comments), not implicit assumptions that fail mysteriously.
- Workload scale varies (PR velocity, table sizes, transcript sizes):
  enumerate with pagination/bounds and LOUD truncation markers, never
  silent caps (precedent: repo-pulse `limit_hit`).
- Optional dependencies AND optional infrastructure (Ollama, GPU,
  individual API keys, a host VM/guardian, Tailscale, voice/edge hardware)
  must degrade gracefully behind detection/config — presence is never
  assumed (precedent: Ollama-optional, `API_KEY_VOYAGE`-gated reranker,
  guardian features no-op without `guardian_remote.yaml`).

**No install-specific values in code.** IPs, hostnames, usernames, absolute
`/home/<user>` paths, GitHub slugs, timezones: these belong in generated
local config (`~/.genesis/config/genesis.yaml`, written by
`setup-local-config.sh`) or config overlays — never in committed code,
defaults, or tests. Resolve repo paths via `genesis.env.repo_root()` /
`genesis_db_path()` (GENESIS_REPO_ROOT-aware); resolve GitHub slugs LIVE
(`gh repo view --json nameWithOwner`) — a configured slug can name a
real-but-wrong repo and return plausible stale data. Shipped config defaults
must work on a fresh install with ZERO overlay.

**Leak-detection patterns follow the same rule — never hardcode an install's
private literals into a tracked scanner.** A public repo's CI grep / gitleaks
rule / commit-msg hook / contribution sanitizer must ship only generic CLASS
patterns (all RFC1918, IPv6 ULA per RFC 4193, `/home/<user>` shapes — see
`scripts/check_portability.sh`). This install's SPECIFIC literals (its
hostnames, subnets, ULA prefixes, private repo name, timezone) live only in the
GENERATED `~/.genesis/release-fingerprints.txt` (built by
`genesis.contribution.fingerprints` at bootstrap; hand-edited section preserved,
backed up via `backup.sh`) and — opt-in — the public repo's
`GENESIS_PRIVATE_PATTERNS` Actions secret. A scanner that must exclude its own
definition files from scanning is self-allowlisting a leak. See procedure
`public_repo_leak_detection_design`.

**Tenant-neutral, not tenant-shaped.** Genesis is a single-user sovereign
system. Build clean single-user code; do NOT pre-genericise for multi-tenancy —
no `tenant_id` columns, ACL tables, or context objects that always resolve to
one identity — absent a committed multi-tenant requirement. Premature
genericisation taxes every change with abstraction for a customer that may never
exist, and the reliability work that WOULD precede multi-tenancy (a fail-closed
data boundary, consolidated stores, provenance) is worth doing on its own
single-user merits and makes the eventual retrofit easier as a side effect.
When that requirement lands, tenancy is a well-understood retrofit — not
insurance to carry now.

**Deploy-path answer required — "how does this reach other installs?"**
Every PR must have an answer for both an EXISTING install and a FRESH clone.
Merged-but-undeployable-elsewhere is a bug. The standard paths:

| Change type | Deploy path |
|---|---|
| Runtime code | `scripts/deploy_code_only.sh`, or `update.sh` for dependency, unit or host changes. Modes: `deploy` (the default: a locked pull + restart that queues behind update.sh and validation holds, refuses a dirty tree or a venv that does not match the pyproject, and stands the Guardian and watchdog down); `deploy` and `restart` also refuse while the server is running work a restart would cancel, naming each item: what the server itself reports at `GET /api/genesis/inflight` (internal bearer; every Claude invocation, plus a dispatched session's or CLI reflection's whole life, from before its Claude process starts until its result is delivered; NOT the short tail other subsystems run after their call returns, such as a chat turn delivering its reply: #2917), and any live Claude process below the server. They proceed only with `--allow-killing <item,…|all>` using the items the refusal prints (`<pid>@<start>` or a reported id); a server too old to answer (404) gets the process check alone, and any other failure to ask refuses unless `all` — a session the server launched must hand the restart off rather than override (launched detached, the script cannot tell it is one of them); `update.sh` does NOT refuse, and its restarts end those sessions too; `pull` (the same pull, no restart: it reports every change the running server has not loaded, and the next step); `restart`; `status`. Launch `deploy` and `restart` DETACHED as a transient unit (`systemd-run --user`, the command is in the script's header): a session's background job dies with the session (#2101). **Validators sample, they don't chase:** deploys need not be 1:1 with merges. Validate `main` at a cadence, and on a failure bisect the merged range; per-PR CI already covers per-merge granularity. Validating against the live server? Hold `flock -s -w 7200 "${GENESIS_HOME:-$HOME/.genesis}/locks/update.lock" <cmd>`, and deploy BEFORE taking the hold. The hold stops locked deploys, NOT every restarter (the watchdog, the dashboard's service routes, Guardian recovery) and not a bare `git pull`, so bracket the run and let the script judge it: keep the `bracket:` token `scripts/deploy_code_only.sh status` prints at the start (from a worktree it reports the main checkout), and run `scripts/deploy_code_only.sh status --verify <token>` at the end; exit 0 is a valid run. A token exists only when the server's boot commit is known, HEAD's runtime files (`src/`, `config/`, `pyproject.toml`, and the scripts it keeps imported, `_RUNTIME_RELOAD_SCRIPTS` in `scripts/lib/deploy_status.sh`) are the ones it booted from and were at every commit HEAD held since the boot (after a `pull` of code, even one a later commit undid: restart first — the server imports lazily, so the detour's modules stay loaded), and nothing there is edited outside git; otherwise it reads `unknown (<why>)`, which no token matches. It covers a restart (boot commit, MainPID, systemd invocation id) and an edit to an ignored override such as a `config/*.local.yaml` or to a user overlay in `~/.genesis/config`, and a change at HEAD to a script the server runs afresh (`_RUNTIME_FRESH_SCRIPTS`: voids the token, needs no restart); HEAD may move over docs, or hooks and scripts the server never runs. It is a tripwire, not a certificate: it cannot see, and reads valid through, a change to the venv's installed packages, the other files the server reads from `~/.genesis/config` (only the `*.local.yaml` overlays are fingerprinted), an edit undone without moving HEAD (a stash and its pop included), and a rewritten or backdated reflog. A daemon your command leaves behind keeps holding the lock, and overlapping holds can starve a waiting deploy (flock grants a late shared request ahead of a queued exclusive one) |
| DB schema | additive idempotent migration — applies at restart |
| One-off data fix / backfill | data-migration framework (post-boot, idempotent) — NEVER a hand-run script only this install executed |
| **Naming either migration** | **UTC timestamp id: `` `date -u +%Y%m%d%H%M%S` ``_description.py** (data migrations prefix a `d`). NEVER hand-pick the next number — the legacy 4-digit namespace is FROZEN and CI refuses a new one. An id you have to CHOOSE is an id two branches choose identically: measured 2026-09-03, one PR was renumbered twice in a day and four open PRs held live collisions, while a duplicate prefix aborts bootstrap on every install. Nobody allocates a timestamp. |
| **Changing an EXISTING migration** | **Don't — add a new one.** The legacy 4-digit set is frozen by ENUMERATED FILENAME, so renaming or deleting one fails CI. Installs that already applied `0050` will never run a renamed `0050_*`, while fresh installs will — two schemas diverging with nothing to notice. Discovery also REFUSES to run anything from a directory holding a file it cannot classify (a mistyped 13-digit id used to be skipped in silence, so the migration never ran at all). |
| Config default | repo config file (+ optional local overlay); works with no overlay |
| systemd unit / timer | registered in bootstrap.sh AND the update path — never hand-`systemctl enable`d only here |
| Hooks / MCP servers | land at next CC session start (note the mid-window in the PR) |
| Guardian / host VM | `update.sh` redeploy (Host-Deploy Gate below) |

**When a change CANNOT deploy through the standard paths** (one-time host
action: packages, sudoers, cgroup settings, firmware), it must ship one of:
(a) a gated self-heal that reconciles on a recurring tick (precedent: the
guardian's swap reconcile — checks every tick, repairs config + live state,
opt-out flag), or (b) an explicit, documented operator step in CHANGELOG +
install docs. Silent "works here because I hand-fixed it" divergence is the
failure mode this gate exists to kill — it bites hardest on guardian/host
changes.

**Empty-state correctness — a fresh install is state zero.** Every feature
must behave correctly with NO accumulated state: empty tables, no history,
no cursor files, first run ever. First runs bound their own work
(precedent: repo-pulse `lookback_days` — never "all history"); readers of
possibly-absent tables degrade explicitly (precedents: dashboard
`charters_available: false`; charter injection byte-identical when the
migration hasn't applied yet). Test the zero state, not just the populated
one — "works here" often means "works with two years of accumulated state."

**External-tool version drift.** Other installs run different versions of
`gh`, GitNexus, Node, and Claude Code — and upgrade on their own schedule.
Never key logic on one version's observed behavior without a fallback:
prefer first-class config over output-patching, and keep the patch as a
safety net when older versions ignore the config (precedent: `.gitnexusrc`
+ the strip job for rc-unaware versions); parse external-tool output
fail-closed against the LIVE stream, never assumed semantics; pin versions
only where the system owns the pin (`cc_version.sh` + cc-align).

**A settings lever for every autonomous behavior.** Anything that acts
without a user in the loop — detached workers, scheduled jobs, auto-writes
— ships its operator lever in the SAME PR: a settings domain
(`off | propose_only | live` or equivalent) plus an env kill switch, with
invalid values degrading toward LESS write authority (precedents:
`repo_pulse` domain + `GENESIS_REPO_PULSE_DISABLED`;
`session_ledger_shadow` live-coerced to shadow). Another operator must be
able to turn your feature off — or cap its authority — without editing
code. This is "the user decides tradeoffs" applied to every install.

**Retention for every unbounded store.** Any table, log, or directory that
grows without bound ships its prune path in the SAME PR, wired into
`disk_hygiene.sh` or an existing retention tick (precedents: repo-pulse
45d prune; ledger-shadow 45d prune; label-aware attention-snapshot GC;
hook audit stores 5 MB size trim). An unbounded store is a slow disk-leak on
someone else's smaller disk — retention is part of the feature, not a follow-up.

**But retention machinery does not belong on a hook path.** `disk_hygiene.sh` IS
the retention path; a guard computing a security verdict must not also be grooming
a store. Two measured reasons, both from the same feature: a helper whose work
scaled with the command exceeded a hook's registration timeout, and a hook killed
before its `exit 2` lets the refused command RUN; and an in-hook retention engine
whose failure mode is rewriting a file produced most of that feature's defects
across five review rounds. Prefer a store shape the daily timer can bound by
deleting whole old files — that is what makes the split possible at all.

**Install-agnostic tests.** Tests must pass on a fresh clone with no
Genesis services, no live DB, no network, no `gh` auth, no local config:
synthetic fixtures only (never real usernames/slugs/IPs — doubles as the
privacy gate), injectable runners for external commands, `tmp_path` over
real paths, no wall-clock dependence. CI on GitHub's runners IS the
reference "different install" — anything a test can't exercise there needs
an injectable seam, not a skip-on-my-machine guard.

## Host-Deploy Gate (merged ≠ deployed)

A merged PR that touches host-deployed paths is **NOT done at merge**. The
guardian and the host VM only pick up changes when `scripts/update.sh` runs —
merging and walking away leaves the host running stale code indefinitely
(observed live: a host guardian sat 3 PRs behind for a week because every
session assumed deploy "happens somehow").

**Trigger paths** (match = this gate applies): `src/genesis/guardian/`,
`scripts/guardian-gateway.sh`, `scripts/install_guardian.sh`,
`scripts/host-setup.sh`, `scripts/update.sh`, `scripts/lib/cc_version.sh`.

**After merging such a PR, in the same session:**

1. Run `scripts/update.sh` from `~/genesis`, DETACHED from the session with
   the `systemd-run --user` recipe in the Timeout Policy above, never in the
   foreground or via `run_in_background` (it redeploys the guardian when
   guardian-relevant paths changed and heals host/container CC + Node pin
   drift — including on a no-delta run).
2. Verify the deploy landed: gateway `version` op reports the expected
   `deployed_commit` / CC version; guardian tick healthy in its journal.
3. State the deploy + verification result explicitly in the wrap-up. If the
   deploy cannot happen this session, record it — never leave deploy as an
   implicit assumption. A purely LOCAL blocker (host unreachable tonight) is a
   `follow_up_create` row. A blocker exposing a REPO-level gap (a missing reconcile
   mechanism, no self-heal path) needs BOTH: an issue for the mechanism fix, AND a
   local row for the fact that THIS install is still undeployed — another
   contributor can close the issue without ever touching this host, which is
   exactly the stale-host failure above.

**The reverse direction is equally binding**: host VMs are deploy targets,
never edit-in-place dev environments. An emergency hand-edit on a host gets a
same-day PR that lands the same change at source — a host divergence that
outlives its incident is a bug.

## When to DRIVE a Merge (standing user rule, 2026-09-05)

A session does not shepherd its own PRs toward merge by default: open the
work and let the PR bake in the normal flow. DRIVING is the UNPROMPTED extra
cycle — polling a quiet PR's gates, soliciting re-review with nothing new
pushed, initiating a merge on a PR that still has un-green gates — and it is
justified ONLY when:

- tracked work is GATED on that PR's merge: the PR body carries a
  `Ledger:`/`Follow-up:` completion marker, a hot follow-up's close depends
  on it, a stacked branch needs its base, or a live hazard closes with it; or
- the user asks for that PR by name.

Three things are NEVER driving — they stay mandatory. They bind **whichever
session currently OWNS the PR**, which is not always the one that wrote it:
under "Where your session ENDS" above (owner ruling, 2026-09-14) a BUILD
session's ownership ends when the PR opens, and the closing/review session that
picks the queue up owns them from there. So "answered before you stop" means
answered by whoever holds the item when the finding arrives — it does not reach
back and re-attach a review loop to a build session that has already handed
off. A finding landing on a PR nobody holds is the drain's intake, not a
builder's debt.

- **Answering.** Findings received while you are present are answered before
  you stop (the zero-drop rule), and the mandatory post-push
  `@codex review` (Gate Machinery above) is part of answering. A fix round
  you have begun is FINISHED, not "chased": answered, pushed, re-review
  triggered — never abandoned half-answered.
- **Closing a green PR.** A PR already fully green at head is past baking:
  merging it under the standing pre-approval (or asking, where none exists)
  is closing, not driving — green DECAYS with base drift and head-freshness
  rules, so a cleared gate is a now-or-re-earn asset.
- **The stopping handoff.** A session that stops while its own PR has any
  gate un-green creates a `ready` follow-up naming the PR and the event it
  awaits, BEFORE stopping. That row is the drain's intake — an un-driven
  open PR without its row IS a stranded artifact under the zero-drop rule,
  never "the normal flow". Until a dedicated closing station exists, the
  queue drains through those rows under the project's ≈51/49 lean.

Scope against CLAUDE.md's ≈51/49 close-before-open lean: unchanged for
closing-shaped work (answering, merging green PRs, the handoff row). What
this rule removes is only the WATCHING — unprompted gate-polling and
review-soliciting between a push and the next external event. When you do
merge, the Pre-Merge Gate below governs unchanged.

### Which one first — prefer a `fix`, because it is usually SMALLER (standing user rule, 2026-09-10)

The section above says WHEN driving is justified. When several PRs qualify at
once, the owner's stated preference is the tiebreak: **a fix restores existing
functionality that is broken; new functionality can wait.** This is ONE axis in
a larger calculus — a gated PR, a live hazard, a user's named request all still
outrank it — not a hard ordering.

MEASURED across the 65 open `fix`/`feat` PRs on this repo, 2026-09-10:

| kind | n | mean added lines | ≥1000 lines | findings-blocked | mean findings score |
|---|---|---|---|---|---|
| `feat` | 30 | 2,024 | 67% | 77% | 3.00 |
| `fix` | 35 | 804 | 29% | 66% | 2.77 |

Chained to the round data: **86% of PRs over 1,000 lines reach 3+ Codex rounds,
against 6% under 200.** So the mechanism is `feat` → bigger → more rounds → more
of the maintainer's conscious decisions consumed, because past the escalation cap
every additional round costs a fresh human sign-off.

**The honest limit, which changes how to apply this.** The findings-score gap is
modest (3.00 vs 2.77; 77% vs 66% blocked), so *"feats attract more findings per
se"* is WEAKLY supported. *"Feats are bigger, and size drives rounds"* is
STRONGLY supported. The rule is therefore the SIZE axis wearing a `feat:` prefix
— which means **a small feat is not deprioritised, and a 2,000-line fix does not
get a pass.** Read the diffstat, not the prefix; a rule keyed on the prefix
rather than on the property would be wrong on exactly the cases where the
tiebreak matters.

### Never RETIRE a PR you are not the one reviving (standing user rule, 2026-09-08)

Terminology first, because the section above already uses "closing" to mean
merging: **RETIRING** is `gh pr close` — abandoning a PR unmerged. This rule is
about that, and only that. Merging a green PR is unaffected.

**A reviewer session retires nothing.** When a PR turns out to be wrong at the
premise — the mechanism cannot work, the design is contested, the fix belongs on
a different event — the disposition is `needs-architecture-session` **on the PR**,
carrying the evidence, and the PR stays OPEN. Retiring it belongs to the session
that actually takes up its revival, at the moment it takes it up.

Why the split, since "it's clearly dead, just close it" is the rationalization
this rule exists to stop:

- **A reviewer has read the diff, not the intent.** You can establish that a
  mechanism does not work. You cannot establish that nothing in the branch is
  worth reviving, that the author holds no context you lack, or that the right
  successor design does not reuse most of it.
- **An open PR carrying an evidence-bearing comment is a live handoff; a closed
  one is an archaeology task.** The next session finds an open PR by listing the
  queue. It finds a closed one only if someone remembers it existed.
- **Retiring costs the review record its addressability.** Threads on a closed
  PR stop being where the conversation happens, so the reasoning that just cost
  a review round stops being read.

So: post the finding, flag it, leave it open, and let the reviving session
decide — including deciding to retire it in favour of a successor, which is that
session's call to make and to justify.

**A superseded PR is the one exception (standing owner rule, 2026-10-04).** The
closing session may retire it when all three hold:

1. **The owner authorized the supersession**: the decision to replace this
   approach with another is the owner's, on record, not a reviewer's inference.
2. **The successor PR is open**, or already merged, so the work has a live home.
3. **It is superseded completely, in every part.** Walk the old PR's files one by
   one against the successor and current `main`, and decide each as covered (name
   the `file:line` that now does it) or moot (say why it no longer applies). One
   part neither covered nor moot keeps the PR open, with a comment naming that
   part.

The closing comment carries that per-file mapping and names the successor, so the
reasoning stays addressable after the close. Short of all three, name the
successor in a comment and leave the PR open.

If you believe any other PR should be retired and nobody is picking it up, that
is a question for the user, not a judgment call for the review station.

### Keep the PR the PR — adjacent findings become issues (standing user rule, 2026-09-09)

**A review session's job is to get THAT PR merged.** Reviews routinely surface
bugs that are real, pre-existing on `main`, and nothing to do with the diff in
front of you. Those do not belong in the PR and they must not hold it up.

Route every finding by ONE question — **does the PR work without this fixed?**

- **No, the PR is broken without it** → fix it in this PR. The builder session
  should have caught it, and shipping a PR that cannot function is not a merge.
- **Yes, the PR works** → **file a GitHub issue and merge the PR.** Do not grow
  the diff, do not open a discussion, do not park it in a reply and move on.

**No per-instance approval to file.** Do NOT ask — asking each time was the
friction this rule removes, and the owner relaxed the general rule on
2026-09-09 after reading what actually got filed. The gate on a public post is
now WHAT GOES IN IT, not permission: scrub anything personal or identifying —
names, hosts, IPs, paths embedding a username, anything about the user's
real-world life — so the issue carries technical detail only. Borderline, in
either direction? Ask. See CLAUDE.md, "Where deferred work goes".

**The exception that is NEVER waived: a security defect is not filed publicly
before it is fixed.** It goes to a private record under `~/.genesis/output/`
plus a `follow_up_create` row, and nothing about it reaches a public surface —
the relaxed filing rule above does not touch this one.

**The test is WHO GAINS, not the word "bypass"** (see CLAUDE.md, "Where
deferred work goes", for the full rule this mirrors). Security-class means
disclosure hands someone a capability they do not already have: a credential
exposure, an auth or privilege bypass, an injection path, anything reachable by
a party with LESS access than it grants. A fail-open in a LOCAL development
guard is usually not that — it runs only where an install wires it, whoever can
trigger it already has commit access to that checkout, and the worst case is
typically an under-reviewed change reaching a PR that still waits on maintainer
approval. Apply the test rather than the label; the catch-all phrasing this
replaces swept in most of what this repo builds. The exception the test catches
and the label does not: a guard whose job is stopping a SECRET or PRIVATE DATA
from reaching a public surface is security-class however local it is, because a
branch on a public repo is public the moment it is pushed, merged or not.

Write the issue while the context is in your head — the measurement, the
file:line, the falsifier, and why it was out of scope for the PR. An issue that
merely names a symptom costs the next session the whole investigation again, and
you already did it.

Why this is the rule: a review that widens into every adjacent defect stops
being a review and becomes an unbounded refactor, which is how a two-finding PR
turns into a six-round loop. The PR is the unit of work. The queue is the place
the rest of it goes.

### Before reviewing an OLD PR, check it is still work (standing user rule, 2026-09-15)

A review session drains a queue sorted by age, and the older the PR the more
likely it is no longer work — its content already merged under a different
number. Observed twice in one session on 2026-09-15 (unquantified: nobody
counted the old PRs that WERE still work, so treat this as a prompt to check,
not a base rate).

**Squash merges are why this is not obvious.** A squash rewrites the commits, so
the branch's own history never becomes an ancestor of `main`. MEASURED on a
two-commit branch whose work had landed as one squashed commit:

| test | result |
| --- | --- |
| `git merge-base --is-ancestor <branch> main` | rc 1 — reads as UNMERGED |
| `git cherry -v main <branch>` | `+` on BOTH commits — reads as unique |
| `git diff origin/main..HEAD -- <files>` | **empty — correctly detects it** |

`git cherry` compares patch-ids, so it DOES catch a squash of a single commit —
and is blind exactly when the squash combined several, which is the common case.
Do not rely on it.

```bash
git fetch origin main --quiet     # a stale ref decides this test otherwise
git diff origin/main..HEAD -- <the files it claims to change>   # empty ⇒ already there
```

TWO DOTS, not three. `origin/main...HEAD` is merge-base→HEAD: it ignores
everything that landed on `main` after the fork point, including the squash that
carries the work, so it is non-empty for a superseded PR every time. It is
exactly as blind as the ancestry tests above. (Caveat on the two-dot form: a
RENAME on `main` makes the old path read as a new file, so a renamed-and-merged
change still looks like work.)

Once you have a commit ON `main` that you suspect carries it, this NAMES the
successor — note it answers "which PRs contain this commit", so feeding it the
open PR's own head just returns that PR:

```bash
gh api repos/<owner>/<repo>/commits/<sha-on-main>/pulls --jq '.[] | "\(.number) \(.state)"'
```

**A second, separate question: is the tree you are standing in at the PR's
head?** A worktree created from a branch NAME sits wherever that ref pointed
when it was created. That check is already specified — with the fetch it needs,
the `main`-ancestry companion, and the reasons a ref comparison alone is not
enough — in the `closing-session` skill under *Establish freshness by comparing
REFS*, and in **Date the code before classifying a red** above. Use those; do
not re-derive a shorter version here. Note also that `rev-parse HEAD` is blind
to uncommitted edits, so check `status --porcelain` too.

MEASURED 2026-09-15, one worktree, the cost of skipping it: a tree four commits
behind its PR head produced a CRITICAL finding, a full adversarial audit, a
public issue, a commit and review evidence — all describing a defect the branch
had already fixed in a commit that tree could not see. The same audit declared
two functions absent from the codebase when both exist at the head, which turned
a reviewer's correct finding into a rejected one. Every measurement was true of
the tree it ran in and false about the PR.

**Treat a finding that is too good as a staleness signal first** — a defect that
would break the feature outright, a symbol a careful author somehow forgot.
Re-read the head before writing it down, and certainly before filing it
publicly. That check is cheap and unconditional; the judgement about whether a
finding is "too good" is not, which is why the action does not depend on it.

**A subagent inherits your staleness silently** — point an audit at a worktree
path and it will measure that tree with complete confidence. Put the head SHA in
the dispatch prompt and ask it to verify the match first.

**When a PR IS superseded: name the successor in a comment.** Leave it open
unless all three conditions of the superseded-PR exception in *Never RETIRE a PR
you are not the one reviving* above hold: owner-authorized supersession, the
successor open or merged, and a per-file check showing it superseded in every
part. Do not "rebase and revive" it either; that is how the same change lands
twice.

## Pre-Merge Gate

> **This is closing-session territory.** A build session's work ends when the PR
> is open (see "Where your session ENDS" above); driving it through this gate is
> the **`closing-session`** skill's job. The mechanics below stay here because
> they are the authority — that skill composes them rather than restating them.
> Read on when you are the one at the gate.

**Canonical pre-merge check:** run
`python3 scripts/hooks/git_push_guard.py --check-pr <N> [--repo OWNER/REPO]`
BEFORE proposing a merge. It runs the SAME check functions the enforcement gate uses, in this REPORT order
(mergeable → CI → base-invariant → pin-receipts → e2e-plan *(advisory)* →
Codex-freshness → scheduled-Claude-review → review-body → inline findings),
so the two use the same checks. The merge arm checks review-body and inline
findings before scheduled review, and additionally applies the
`--match-head-commit` binding, which has no report row —
this `--check-pr` read IS the mandatory pre-merge step: **always run it and read
the PR's automated-review comments (Codex, leak/CI, the scheduled Claude review)
before any merge** — never hand-roll a
gh/jq review check (a hand-rolled query once used the GraphQL bot login on the
REST endpoint, matched nothing, and reported "Codex clean" while P2s sat unread).
When all gates pass it prints the exact atomic merge command to copy
(`... --match-head-commit <verified-head>`); use that command verbatim.

`git_push_guard.py` enforces a **hard gate** at merge time. The gate reads only
the `gh pr merge` spelling, so the guard refuses a pull-request merge spelled
through the GitHub API instead: a non-GET to REST `pulls/N/merge` or
`merge-async`, a GraphQL `mergePullRequest` / `enablePullRequestAutoMerge` /
`enqueuePullRequest` mutation. It is a closed set: a `gh api` call that names a
merge passes only when it is a plain read (one literal endpoint, read-only flags),
and a GraphQL query or PUT endpoint it cannot read is refused too. The refusal
names the gated command. The recogniser is `scripts/hooks/gh_merge.py` (#2768);
it is a tripwire for ordinary spellings, not the boundary, which is server-side.
Only the bare `gh pr merge --help` skips the merge arm. Beyond the review
findings below, a gated `gh pr merge`:
- must carry `--admin` (explicit approval flag) and be bound to the reviewed head
  via `--match-head-commit` (GitHub rejects it server-side if the head moved —
  TOCTOU defense); the `--check-pr` command supplies this;
- requires Codex to have reviewed the **current head** — Codex does NOT auto-review
  a later fix-commit, so comment `@codex review` and wait after any push.
  **A clean Codex signal counts only when its id RESOLVES to the head.** Codex posts
  NO review object when it finds nothing; its word on a clean head is a comment — a
  clean re-review ("Codex Review: Didn't find any major issues. … **Reviewed
  commit:** `<sha>`") or a `✅ Completed` Code Review row in its PR summary comment
  (on PR open, often the only signal: #2418). Both name the commit by an ABBREVIATED
  id, and a prefix that merely MATCHES the head binds nothing — the head is whatever
  the branch's author pushed (#2487). So the gate resolves the id repo-wide via
  `GET repos/{owner}/{repo}/commits/{short}` and accepts it only when GitHub resolves
  it to exactly the PR head; a 422 (ambiguous or unknown) refuses it. Matching the
  head implies the commit is the PR's. The comment must be the Codex Bot's, no Codex
  review object (any state) may sit at the head, and no Codex issue comment on the PR
  may carry findings (Codex usually files findings as a review object, but MEASURED
  on 2 of 339 PRs it posted them as a `💡` issue comment with none). Any non-Codex edit
  or deleted edit revision on any Codex comment permanently refuses clean signals;
  the block names the editor when available. Edit-history completeness is required
  only for Codex Bot comments; unrelated comments' edit data is ignored. History
  veto covers head force-push, head-branch deletion or restoration, and base changes.
  A base change stays a veto because the clean signal names the head, not the base
  Codex reviewed against; retargeting changes the effective diff without moving the
  head. A base force-push is retired: merging requires the default base, whose
  ruleset forbids force-push and deletion, so a force-pushed non-default base can
  reach a merge only through a base change, which vetoes. All 541 commits dropped
  by 191 force-pushes still resolve repo-wide by 7-hex id, so dropped head commits
  are not the binding risk.
  A branch or tag exactly named after a short id can shadow GitHub's commit lookup
  (which uses git name-guessing rules); measured `pull/2720/head` and `main` resolve.
  Creating a shadow ref needs base-repo push rights, held only by the owner, and no
  hex-named ref exists; fork authors cannot create one. A deleted comment leaves no
  trace in the API. The block message says which clean signal it read and why it was
  refused, and when no later clean signal can count it says a finding-free re-review
  cannot help. `--check-pr` labels a pass from the gate's own record for its verified
  head; a subsequent head move is stated without relabeling the original pass, while
  merge-with stays bound to that verified head. When the signal does not resolve,
  the routes are an owner-approved `# substitute-review` on another reviewer's review
  at that exact head, or a conscious
  `# stale-review-override` (which on the hook surface also needs fallback evidence).
  **Smart-delta narrowing:** a STALE review passes anyway when the unreviewed
  delta (`reviewed...head` via the compare API, classified by `review_scope`
  substantiality) is provably review-trivial (docs-only / a small single-file
  touch-up) — the merge is then still bound to the exact head that was
  classified. A substantial or unclassifiable delta blocks; an ABSENT review
  always blocks;
- requires **every scheduled Claude review at the current head**. Each scheduled
  Claude review (a `/schedule` cloud routine) posts as the repo OWNER's account and
  must carry a marker `<!-- genesis-scheduled-review: head=<full-40-hex-sha> kind=<name> -->`
  naming the exact head it reviewed AND which routine it is (`kind`). The gate blocks
  unless an owner-authored marker for EVERY effective required kind
  (`_required_scheduled_review_kinds()` — DEFAULT `leaks` ALONE, which is also the
  irreducible leak/secret scanner, so the default set and the floor coincide;
  `code-review` is ADVISORY by default because no routine emits its marker — measured
  2026-09-16, zero `code-review` markers across all 51 open non-draft PRs and the 40
  most recently merged ones, against 47 and 31 `leaks` markers — and an install that
  DOES run one re-arms it by naming a LARGER set in
  `merge_gate.required_scheduled_reviews: [code-review, leaks]` in local `genesis.yaml`)
  names the PR's current head — so if any required routine never ran, ran on a stale
  commit, or was rate-limited, the merge blocks (naming the missing kinds). An ADVISORY
  routine still posts its review on the PR to be read/addressed, but its absence does not
  block. The block message is an **inventory**, not a diagnosis: under each missing
  kind it lists EVERY marker block the scan found that names that kind, with its
  status, and hides nothing. Run `python3 scripts/hooks/git_push_guard.py --check-pr <N>`
  — it renders those
  rows, not just the summary line (whose `present: none` clause reads like "nothing was
  posted" in every case below, and is the exact wording an operator was once measured
  acting wrongly on). Row statuses you will see:
  * *accepted at a DIFFERENT head* — a routine ran, then a push moved the head. Routines
  are generally not re-run on a push; re-review the current head and post the marker;
  * *REFUSED* (at this head or another) — the body reads as carrying a blocking finding
  and no clean-verdict line overrides it; see the clean-verdict rule below. The message
  deliberately does NOT print the verdict string, because a gate that prints the line
  that makes it pass is explaining how to get past itself;
  * *could not be counted: <reason>* — a head that is not full 40-lowercase-hex, an
  empty or refused field value (quoted back verbatim), an author who is not the repo
  owner, a dismissed review, a stale unpublished draft;
  * *unscoped* — blocks naming no REQUIRED kind, listed and credited to nothing:
  guessing which review a block "meant" would steer you into attesting for one that
  never ran. A value carrying a `/status` suffix (`kind=leaks/failed`) lands here and
  is flagged as a run that reported its own failure.
  The one conditional is a COUNT, keyed on a fact: a kind with no OWNER-authored block at
  any head gets "a routine may still be in flight — waiting is the right move", because
  that is the only state where patience can help. A stranger's comment is not evidence
  about the owner's routine and never silences that note; an owner's block in ANY state
  (accepted elsewhere, refused, dismissed, stale draft, malformed) is, and does.
  Why an inventory and not a diagnosis: the previous shape picked one cause per kind and
  hid the rest, and every one of nine review findings across six rounds was a hidden fact
  — a refused `[P1]` on an older commit hidden behind a typo at the current one, the only
  evidence a review had ever run hidden by a drive-by comment. Precedence is the right
  shape for a VERDICT; for a REPORT, hiding a true fact is never correct.
  Or append `# scheduled-review-override` to merge anyway (the conscious "merge without
  the scheduled reviews" case). Head match is EXACT for the LLM marker — no delta
  tolerance, unlike the Codex freshness gate, which grants relief on a provably trivial
  delta. That asymmetry is deliberate: the Codex classifier judges code-review
  substantiality by file type and size, and an inferential leak lands in exactly the small
  doc edit it would wave through. The ONE relief the leaks kind gets is MECHANICAL, not a
  delta tolerance: an ACCEPTED `leaks` marker on an ANCESTOR commit of head satisfies the
  gate when the `leak-detector` job of the `CI` workflow is green at head (identity pinned
  to that (name, workflow) pair; `_MECHANICAL_RESCAN_BY_KIND`), and NEVER when any
  refused `leaks` marker — or any blocking finding the scan could not credit to a head
  or kind (a tie, a malformed marker) — exists anywhere in the PR (the head axis is not
  a time axis — a later acceptance at an older head must not outrank a refusal). `--check-pr` renders a
  carried marker as `ok (leaks carried from <sha>, leak-detector green at head)`, never
  as `ok (at head)`. Measured motive: 6 of 10 sampled multi-push PRs were blocked purely
  because the routine does not re-stamp after a push, and the override had become
  routine. The marker means "ran **clean**", not merely "ran": a
  review whose body carries a blocking finding (`[P1]`/`HARD BLOCK`/`### ERROR`, unless a
  clean verdict overrides) is rejected, and DISMISSED/PENDING(draft) reviews don't count.
  **ALWAYS end a genuinely-clean scheduled review with an explicit verdict line**
  (`VERDICT: PASS`, or `PII/Secrets/Wording: CLEAN`). The blocking patterns are plain
  substrings with no negation awareness, so prose like "not a hard block" or "no hard
  blocks found" TRIPS them — measured on two 2026-08-28 PRs whose markers both contained
  that phrase in negated prose; the one that also carried a clean-verdict line was
  accepted and the one without it was silently refused. Without the verdict line a clean
  review can be rejected on wording alone;
  Fail-closed: an unreadable comments/reviews fetch BLOCKS (never a false all-clear);
- requires the PR base to equal the repo's default branch (retarget guard);
- blocks unless mergeability is a definite `MERGEABLE` (a failed/unknown read
  does not merge).
- **the CI gate** blocks red/pending checks; and — on the canonical public repo,
  where CI always runs — an `absent` CI state (a readable EMPTY check set = CI
  never ran, the tell of a conflicting branch or a dropped `pull_request` trigger)
  also blocks, so an un-CI'd PR can't merge. Likewise `incomplete` (a NON-empty
  rollup whose present checks are green but a REQUIRED workflow contributed no
  verdict — e.g. a lone green CodeQL after a workflow-specific trigger drop, or a
  fully-SKIPPED suite): the required identity is the rollup `workflowName`, config
  driven via `merge_gate.required_ci_workflows: [<names>]` in local `genesis.yaml`
  (default `CI`; fail-closed to the default on any malformed/empty config — there
  is no disable value). An UNREADABLE CI read (`unknown`) fails OPEN, and off the
  canonical repo `absent`/`incomplete` fail open too (another repo may
  legitimately have no CI, or a differently-named suite). Waive with
  `# ci-override` (never `--admin`).
- **Override sigils are split by boundary** — `# review-override` waives ONLY the
  finding scans (review-body + inline P1s); `# scheduled-review-override` waives
  ONLY the scheduled-Claude-review gate; CI is `# ci-override`. Append several in
  one trailing comment when several waivers are genuinely intended.
  **`# stale-review-override` is the EXCEPTION and is wider than its name.**
  Besides Codex-at-head freshness and the base invariant, it also drops the
  `--match-head-commit` TOCTOU binding — the guard's own module docstring says it
  "waives that binding for ALL gates" — and un-pins the scheduled-review check
  from head, making it point-in-time. Mechanically the merge arm only enforces
  the binding `if verified_head:`, and the force path leaves that `None`. So
  reaching for this sigil while believing the merge is still pinned to the
  reviewed head is exactly the unsafe read: a concurrent push CAN swap in an
  unreviewed head. It is a conscious "merge without current verification"
  choice, and the right fix for a stale review is `@codex review`, not the sigil.
  ⚠ An earlier version of this bullet claimed the split meant one waiver "can't
  silently disarm an unrelated gate", and listed this sigil as waiving "ONLY"
  the review-context gates. Both were false of this one sigil.
- **`# substitute-review` is the NARROW Codex stand-in** (owner standing order,
  2026-09-24). When Codex has not reviewed the head, a NON-dismissed,
  NON-pending review by another GitHub App reviewer (any, such as Devin or
  CodeRabbit, except the PR's own workflow bot and CodeQL) whose `commit_id` is EXACTLY the head
  satisfies the Codex freshness check. The owner approves each use IN
  CONVERSATION before you add the sigil — the sigil is the record of that yes, so
  never add it without asking (owner ruling 2026-09-24, which replaced a native
  permission prompt the first version raised). The gate prints a NOTE naming the
  stand-in and marks its override-log row `codex-freshness:used`; a dispatched
  session is refused, since nobody there can have approved it. Every other gate
  still applies. Unlike `# stale-review-override` it keeps the base-branch check and
  the `--match-head-commit` binding (labelled with the reviewer that verified the
  head), so no fallback-evidence file is needed on this path. Only a review
  record with a BODY counts: an empty-body record at head is the wrapper GitHub
  makes when the bot replies inside a thread, not a review of the head. With
  both sigils present, `# stale-review-override` wins and the stand-in is never
  used — it is already the broader waiver. `--check-pr` says when a stand-in exists:
  `codex-at-head : BLOCK — … — substitute available: <reviewer> reviewed this
  head`. Codex remains the official reviewer; this exists for its outages.

The review-findings gate specifically:

1. After CI passes, the merge hook automatically checks PR comments
   for automated review findings (ERROR, [P1], HARD BLOCK).
2. If review present with **blocking findings** → merge is **BLOCKED**
   by the hook (exit code 2). Fix the findings first.
3. Inline findings are SCORED, and **SEVERITY FLOORS while the LANE governs
   VOLUME.** Two independent rules, checked in that order:

   **(a) The always-fix floor, every lane, before the score is consulted.** Any
   unresolved Codex **P1**, any unresolved CodeRabbit **Critical/Major**, or any
   unresolved Devin **severe bug / critical security** finding (🔴/🟥) blocks the
   merge outright — whatever the change is. This is
   `floor_hits = len(p1) + len(cr_block) + len(devin_block)` in
   `_check_inline_review_findings`.
   It is a RULE because it used to be an ACCIDENT: before the lanes existed the
   single threshold was 1.0 and a P1 scores exactly 1.0, so the floor held by
   arithmetic, unnamed and untested — and raising any threshold would have
   deleted it in silence.

   **(b) The per-lane score threshold, for everything below the floor.** Weights
   — **Codex P1 = 1.0 · Codex P2 = 0.5 · CodeRabbit Critical OR Major = 1.0
   each · Devin severe/critical (🔴/🟥) = 1.0 · Devin non-severe bug or security
   warning (🟡/🟨) = 0.5** (`_CR_BLOCKING_SEVERITIES = {"critical", "major"}`,
   `_CR_BLOCKING_WEIGHT = 1.0`, `review_findings.DEVIN_MARKERS`) — but what a change can AFFORD
   depends on what it costs to be wrong (`_INLINE_SCORE_BLOCK_THRESHOLDS`).

   **Devin scores like Codex (owner ruling, 2026-09-24).** Its comments open
   with `<!-- devin-review-comment {json} -->` and a marker: red is Devin's severe
   tier (bug / critical security), yellow its non-severe tier (bug / security
   warning), and 🔍 is informational, surfaced and never scored. The marker agrees
   with the metadata `kind` on every comment measured. A
   marker outside that closed set is FORMAT DRIFT: surfaced, never scored. A
   finding Devin posted twice counts once (dedup on its metadata `id`). It is
   parsed only for the `devin-ai-integration[bot]` Bot account, and its branch
   runs before the Codex badge match, so a Devin body QUOTING `![P1 Badge]` is
   scored once, as Devin. CodeRabbit Minor and below stay SURFACE-ONLY by owner
   ruling — only Devin gained scoring.

   **Clearing a finding:** a MAINTAINER's in-thread reply, as always — or, for
   a DEVIN finding only, Devin itself replying in that finding's thread with a
   first line starting `✅ **Resolved**` (Devin may withdraw its own finding; no
   bot can clear another's, free prose such as "Fixed in …" clears nothing, and
   Codex and CodeRabbit findings still need a maintainer reply). Devin's reviewer
   and its builder post under ONE login, so a self-withdrawal is reply text, not
   an identity: it still clears, but the gate always LISTS it as
   `[Devin self-withdrawn]` so you can check the fix landed. A thread
   resolved in the GitHub UI does NOT clear a finding; reply instead. A finding
   Devin posted more than once counts once: each copy's diff and documentation
   scope is judged on its OWN anchor, the finding scores on its strongest copy
   that is eligible to score, and every copy carrying an unrecognised marker is
   reported as format drift, answered or not.

   | lane | blocks at | what lands there |
   |---|---|---|
   | `critical` | **1.0** | enforcement-hook surface · `.github/**` AND the implementations behind its required checks (`scripts/ci/**`, `check_*.py`/`.sh`) · schema and data migrations · HTTP surfaces (`dashboard/routes/**`, `hosting/**`, `api.py`/`api_*`, `_blueprint.py`) · secrets and credentials |
   | `standard` | **2.0** | ordinary runtime code |
   | `light` | **3.0** | PROSE (including prompt surfaces) / tests / fixtures only, or vendored-only |

   **`light` is PROSE, not `docs-config`.** A `.yaml`/`.toml`/`.ini`/`.cfg`
   reaches `_category() == "docs-config"` through the shared classifier, but
   config is not documentation — `config/desktop_takeover.yaml` arms desktop
   takeover and `pyproject.toml` pins dependencies, so both are `standard`. The
   light lane is `.md`/`.rst`/`.markdown`/`.adoc`, plus `.txt` and the
   extensionless form ONLY on a known documentation stem (`CHANGELOG.txt` and a
   bare `LICENSE` yes, `requirements.txt` no — the same split `_is_doc_path`
   makes), plus tests and fixtures (`review_scope._is_lane_light`). Those
   spellings are matched DIRECTLY rather than behind `_category`, because
   `_category` calls `.adoc` and an extensionless `README` **code** — so an
   earlier draft that listed them behind it advertised prose formats nothing
   could reach.

   **A PROMPT SURFACE is prose here.** `_category` calls `SKILL.md`,
   `.claude/commands/*.md` and `src/genesis/skills/**/*.md` *code*; the lane
   reads them as what they are. MEASURED: this is the single biggest effect of
   the lane's own vocabulary — 14 of 40 recent PRs classify `light` where the
   inherited tagger said `standard` — and it is mostly INERT, because 11 of those
   14 touch nothing whose findings score at all (every path is an `_is_doc_path`
   and `doc_findings` defaults to `skip`). Where it bites is a prose-plus-TESTS
   PR, whose test findings then clear at 3.0.

   So on a CRITICAL change two P2s still block exactly as before; on ordinary
   code it now takes four. MEASURED over the 40 most recently merged PRs:
   critical 35.0%, standard 20.0%, light 45.0% (a SLIDING window — see the
   classifier docstring; re-running will not reproduce it, and a difference is
   the merge queue moving, not a regression). The lane comes from
   `review_scope.classify_lane`, which FAILS CLOSED to `critical` on an
   unreadable file list — the lane relaxes a threshold, so the safe default is
   the one that relaxes nothing.

   ⚠ **A stable distribution is NOT a coverage proof, and neither is a passing
   example.** Building this lane produced the same miss twice, each time caught by
   a method the previous one could not reach:

   * A draft held critical at an unchanged 30.0% while having silently stopped
     classifying `*route*`/`*controller*`/`*endpoint*` paths as critical — same
     percentage, different membership, because none of the 40 sampled PRs touched
     such a file. A **constructed test case** found it; the measurement could not.
   * Four route-defining modules — `src/genesis/hosting/**` and
     `dashboard/_blueprint.py`, one serving `/genesis/login` — were still outside
     the lane after that fix. Every constructed case passed and the distribution
     reproduced to the decimal. Only an **enumeration over every tracked module**
     found them, which is why that class is now locked by a population check
     rather than by more examples.

   When you change what feeds a classifier: diff the per-item ASSIGNMENTS rather
   than the totals, and lock a category by enumerating its population rather than
   by naming the members you happened to think of.

   **`auth` is deliberately NOT a critical input**, though it sits in
   `_DOMAIN_SENSITIVE_TAGS` and drives the depth gate. Its glob is
   `*auth* *session* …`, and MEASURED over 3,999 tracked files, 58 of the 59
   `auth`-tagged files (98%) match on **"session"** — CC-session machinery, not
   authentication. Exactly one is real (`dashboard/auth.py`). Re-inheriting that
   set is the obvious "cleanup"; don't.

   One thing the lane does not change: **a single CodeRabbit
   Critical or Major blocks on its own, but ONLY from the INLINE endpoint.**
   `_check_inline_review_findings` reads two channels and they are NOT symmetric:
   `pulls/N/comments` (findings anchored inline) feeds the score, while the review
   BODY — CodeRabbit's "Outside diff range comments", the findings it could not
   anchor — is surfaced loudly and scores **0.0 at every severity**. The
   `outside_critical` / `outside_major` accumulators appear in no term of
   `score = len(p1) + 0.5*len(p2) + 1.0*len(cr_block)`, deliberately: an
   undelivered finding has no comment thread, so there is no maintainer-reply
   route to clear it. Read them anyway; nothing there will stop a merge. The
   gate's own block message prints the whole formula; read it rather than
   reciting this.
   A finding is excluded from the score when a MAINTAINER reply engages it, or
   when it is on a DOCUMENTATION path.
   **`_is_doc_path` is ANY prose extension at ANY depth — not an allowlist of
   directories.** OWNER DECISION 2026-09-04 (`815e0dbd2`, #1689) widened it:
   `.md`/`.markdown`/`.rst`/`.adoc` anywhere, plus a known doc STEM
   (CHANGELOG/README/LICENSE/…) with `.txt` or no extension. So `AGENTS.md`,
   every `SKILL.md`, `CLAUDE.md` and all of `.claude/**/*.md` ARE doc paths — and
   with the default `_DEFAULT_DOC_FINDINGS_MODE = "skip"` a finding on one NEVER
   scores, at any severity. Findings are still SURFACED in every mode; the lever
   decides only whether they COUNT.
   ⚠ This paragraph previously stated the exact opposite — that those files
   "return False from `_is_doc_path`, so a P1 anchored on one contributes its
   full 1.0" — describing the pre-#1689 world. It was itself a correction of an
   earlier error (see the callout below) and went stale the same way, which is
   the point: **verify a gate claim against the symbol before relying on it, and
   distrust this file most where it sounds most certain.**
   Pure WARNINGs/NOTEs (non-P1/P2) → merge allowed.
   **This paragraph used to say "any P1" blocks, which was FALSE, and the
   divergence cost a whole session.** A P1 anchored on `CHANGELOG.md` merged
   (#1606, 2026-09-03) with no override; a session read this text, saw the merge,
   concluded the gate had a hole, and built a reversal of the owner's directive
   before checking whether one existed. Docs describe intent, code describes
   reality — and when they disagree, suspect the doc. Verify against
   `git_push_guard.py` before concluding the gate misbehaved.

   **A doc-path P1 is still usually a CODE finding.** A changelog is where a PR
   states its own blast radius, so a P1 there typically means *"your stated blast
   radius is wrong"* — the anchor is prose, the defect is not. #1606's was
   anchored on the sentence "the quiet path is a hand-edited overlay", which was
   false: the settings writer persisted `default: glm-5.2` alone, so settings-UI
   users hit the quiet path too. Correctly non-blocking; still needed fixing.
4. **An ABSENT review BLOCKS — it does not merge on CI alone.**
   `_check_codex_reviewed_head` returns a block for `if not reviewed`, whatever
   the reason for the absence (quota, never triggered, still running). The only
   things that clear it are a review object at head, a clean Codex signal whose id
   resolves uniquely to the head (see above), a provably review-trivial delta since a
   stale review, a conscious
   `# stale-review-override` — except on a hook-surface PR, where that sigil also
   requires the exact base-and-head fallback-review evidence described above — or
   an owner-approved `# substitute-review` resting on another reviewer's review
   at that exact head.
   ⚠ This item previously read "no review comments at all (quota exhausted) →
   merge allowed on CI alone", which is FALSE and contradicted the Pre-Merge Gate
   section above ("an ABSENT review always blocks") four hundred lines earlier.
   A quota message is evidence about ONE channel at ONE moment, never a licence
   to merge unreviewed — re-trigger and check the INLINE endpoint.
5. **Override**: Append `# review-override` to the merge command to
   bypass the gate (e.g., `gh pr merge 123 --squash --admin  # review-override`).
   The override is logged — one metadata row per sigil, written as one file per
   merge under `~/.genesis/merge_overrides/` (sigil, PR, bound head, which gate it
   waived, and the verdict THAT GUARD returned: `allowed` / `asked` / `blocked` /
   `error`), so "was this escape reached for?" is a query rather than a survey:
   `cat ~/.genesis/merge_overrides/*.jsonl`. One file per merge rather than one
   shared log, so a multi-sigil merge is a single atomic record and the writer
   never maintains a file on the merge path. The store is bounded to 5 MB by the
   daily `disk_hygiene.sh` timer (`scripts/prune_hook_audit_logs.py`, oldest whole
   records dropped) and backed up by `backup.sh` §6d.
   `blocked` matters: a sigil can be appended to a command that some OTHER gate
   inside the same guard then stops, which is a real signal but not an override
   that took effect, and `asked` means the decision went to a human who may still
   have denied it. Read `allowed` as "the sigil was accepted HERE", never as "the
   merge happened": other PreToolUse matchers and the harness's own permission
   prompt sit downstream of this guard and can still stop the command.
   Never any command text — the row is metadata only, because a trailing comment
   rides a Bash command that can carry a credential.
   **Scope, stated so the log is not read as more complete than it is:** the five
   PR-merge sigils are covered (`# review-override`, `# ci-override`,
   `# stale-review-override`, `# scheduled-review-override`,
   `# substitute-review`), from the point the
   PR is resolved onward. A merge rejected BEFORE that point writes nothing —
   no `--admin`, an unresolvable repo/PR, a compound carrying two publish/merge
   operations, or a merge compounded with a local `git merge` into main are the
   ones measured, and the list is illustrative rather than exhaustive: anything
   that blocks before the PR resolves leaves no row. No OTHER sigil in
   `shell_parse._KNOWN_SIGILS` is recorded either — `# depth-ack`, `# audit-ack`,
   `# discard-override` and `# full-suite-ok` belong to gates outside this guard
   and announce themselves on stderr only. The command-scoped acks
   (`# merge-to-main-override`, `# escalation-ack`) are
   NOT logged, for two different reasons. `# merge-to-main-override` waives a
   gate that short-circuits BEFORE the branch is resolved, so a row could only
   say the ack was typed, never that it waived anything. The local
   `# escalation-ack` is outside this guard. `# final-round-accept` remains
   parser-compatible for stale worktrees but authorizes no current gate. Native round
   approvals are decisions rather than sigils and are deliberately not written as
   reusable receipts.
6. **Read the PR's warning comments before merging — not just the hard gate.**
   Beyond Codex, a structural-review bot posts under the repo-owner account
   (review state COMMENTED) and emits **SOFT WARNINGs** (PII /
   private-text / wording) that the hook does NOT block on and that a naive
   `.comments` scan misses. Check BOTH `gh pr view N --json reviews,comments`
   and `gh api repos/<owner>/<repo>/pulls/N/comments`, and address each soft
   warning or consciously accept it. Never merge past an unread warning.
7. **Codex findings are INLINE review comments — invisible to `gh pr view`.**
   Codex's review *body* is boilerplate ("Here are some automated review
   suggestions"); its actual `[P1]`/`[P2]` findings live only at
   `gh api repos/<slug>/pulls/N/comments`. Derive `<slug>` live —
   `gh repo view --json nameWithOwner --jq .nameWithOwner` — NEVER hardcode
   it (configs name several repos; the working repo is not the org default).
   A **404 from that endpoint means WRONG SLUG or PR number, never "no
   findings"** — a clean PR returns `[]`. The merge-gate hook blocks on a P1 or a
   CodeRabbit Critical/Major OUTRIGHT (the always-fix floor), and otherwise on the
   weighted inline SCORE against this change's LANE threshold — so two unresolved
   P2s block a CRITICAL change, while ordinary code takes four (full table in the
   Pre-Merge Gate section above). Unread P2s no longer slip through in pairs on the
   surface where that mattered (2026-07-10: 8 real P2s on the entity-layer PRs
   merged past the OLD P1-only gate, the exact gap this score closes). Note what it
   does NOT close, and do not read it as more than it is: a LONE P2 still passes
   unread, which is how #1620's HTTP-500 finding merged (2026-09-03), and on an
   ordinary change three now do. The score bounds what the gate stops; only reading
   the report stops the rest — which is exactly why the gate prints a NOTE naming
   the lane and threshold whenever a non-zero score passes under one. And the two
   channels are INDEPENDENT: Codex can post a quota/usage-limit message as an
   ISSUE comment while a later `@codex review` trigger delivers real inline
   findings anyway — a quota message is evidence about that channel at that
   moment, never proof Codex "can't review". After time passes, re-trigger and
   check the INLINE endpoint before concluding quota-limited (2026-08-26: #1484's
   real P2 arrived inline while the issue-comment channel still showed only the
   earlier quota message).
8. **A CONFLICTING PR silently suppresses the whole CI suite.** When a PR
   has a merge conflict with main, GitHub cannot build the merge ref, so
   `pull_request`-triggered workflows (the entire ci.yml suite) never run —
   while CodeQL still passes on the head SHA, making the check list LOOK
   green. A thin check list (only Analyze/CodeQL) means CHECK
   `gh pr view N --json mergeable` — `CONFLICTING` needs the base branch merged
   in before any CI verdict exists at all (2026-07-16: #1089 sat
   conflict-suppressed through three pushes; main had moved under it via
   concurrent sessions). Since #1484 the merge gate ENFORCES this class
   mechanically on the canonical repo: a fully-empty rollup reads `ci: absent`
   and blocks, and a thin/partial rollup missing the required CI workflow reads
   `ci: incomplete` and blocks (see the CI-gate bullet above) — the trap text
   stays because the DIAGNOSIS (check `mergeable` first) is still the fastest
   route to the cause.

   Three corollaries, each measured 2026-09-08 and each costing a wrong diagnosis:

   - **`UNKNOWN` suppresses it too, not just `CONFLICTING`.** GitHub recomputes
     mergeability lazily, and EVERY merge to main invalidates it repo-wide — so
     right after a merge batch the whole queue reads `UNKNOWN` and its CI looks
     missing. That is also why a merge gate refuses with "mergeable status is
     'UNKNOWN'": wait and re-read rather than concluding anything.
   - **A PR opened against a NON-DEFAULT base never ran CI at all**, because
     `ci.yml` filters `on: pull_request: branches: [main]` — the filter is on the
     BASE. Retargeting it to main afterwards fires nothing: the default
     `pull_request` types are opened/synchronize/reopened, and a base change is
     none of them. This one does NOT self-heal and nothing will ever fire for it
     again on its own; it needs a push. Stacked PRs land here by construction.
   - **`workflow_dispatch` is not a workaround.** `gh workflow run ci.yml --ref
     <branch>` runs to completion and never appears in the PR's
     `statusCheckRollup`, so it cannot satisfy a gate that reads the rollup.

9. **`gh run rerun` CANNOT clear a failure caused by a broken base.** It replays
   the ORIGINAL merge commit rather than recomputing one against current main,
   so re-running a PR whose base was broken faithfully reproduces the breakage.
   MEASURED: a re-run started 15 minutes AFTER the fix merged still failed, and
   `git merge-base --is-ancestor <fix-sha> refs/pull/<N>/merge` returned false —
   the pull request's merge ref did not contain the fix. Only a push or
   `gh pr update-branch <N>` reliably recomputes it, and both move the head.
   Closing and reopening the PR is NOT a substitute. MEASURED 2026-10-01: a
   reopen fired a new CI run that checked out a merge ref GitHub had built about
   three and a half hours earlier, on a main several merges behind. Whatever you
   try, read the job log's `HEAD is now at <merge sha>
   Merge <head> into <base>` line and confirm the base is the main you expect.
   "CI re-fired" is not evidence that it tested current main. Two consequences: do not "just
   re-run" someone's stale red, and do not tell them it will clear — it will
   not, and their next push clears it for free anyway.

10. **`gh run view --log-failed` truncates and shows only PASSED lines.** On a
    large suite it cuts around 44%, so a FAILING job reads as a clean log with
    no failure in it — the most misleading possible output, because it looks
    like an answer. It reported nothing useful on three separate attempts in one
    session. To find what actually failed, reproduce locally against a clean
    `origin/main` checkout (which is what identified it, twice), or fetch the
    raw job log rather than the CLI's rendering.

11. **Two individually-GREEN PRs can break main, and nothing warns you.** PR A
    adds a repo-wide invariant test (one that ENUMERATES modules or consumers and
    asserts a property); PR B adds a violation of it. Each is green against a
    main that lacks the other, neither diff touches the other's files, so there
    is no textual conflict, no reviewer signal, and both merge clean — then main
    goes red, and because PR CI builds the merge commit it fails EVERY open PR on
    a test unrelated to their changes. MEASURED 2026-09-08: a chokepoint test
    requiring every consumer of the shared shell parser to use the checked entry
    point, and a guard importing the unchecked one, merged an hour apart.
    This is NOT the structural collision described under the architecture-session
    rule — that one is main superseding a mechanism the PR uses, and it is
    visible from the PR's own files. Here neither PR can see the other.
    The detectable signature is a new ENUMERATING invariant test landing while
    other PRs are in flight: when you merge one, re-run it against the other open
    PRs' merge commits before merging them, not just against its own branch.
    Sequencing follows from the same fact — batch merges rather than interleaving
    them with active pushes, because each merge invalidates every other PR's
    mergeability and manufactures the false "CI never ran" signal above.

## Reference Router

Read references ONLY when relevant to the specific task. Do NOT load all
references on every trigger.

| When you need... | Read... |
|---|---|
| Subsystem purpose/maturity/do-not-touch (judgment layer) | `docs/architecture/CURRENT.md` |
| Codebase structure, package map, gotchas, debugging | `references/codebase-map.md` |
| Package/module/symbol navigation (progressive drill) | `codebase_navigate` MCP tool (L0→L1→L2) |
| venv, DB paths, Qdrant, Ollama, network, commands | `references/environment.md` |
| Worktree rules, concurrent sessions, branch naming | `references/worktrees.md` |
| tracked_task, exc_info, os.killpg, logging patterns | `references/observability.md` |
| V3 state, build order, GROUNDWORK, architecture docs | `references/architecture.md` |
| Phase 6 contribution pipeline, sanitizer | `references/contribution.md` |
| Pending work, active incidents, subsystem status | `references/build-state.md` |
| Auditing/deep-reviewing AI-generated code (failure taxonomy, audit passes) | `references/ai-code-audit.md` |
| Writing or revising a multi-session plan document | `references/plan-docs.md` |
| What a plan owes before building: free reads, red-team, body fields, ask vs decide | `references/due-diligence.md` |
| Pre-release review, bug hunt, guard/gate change — verification method | `references/high-stakes-verification.md` |
| Choosing a command/value/procedure by reasoning about an external tool | same, section 9 |
| Auditing the CLAIMS a session wrote (wrap-up, or before any permanent record) | same, section 11 |
| Which code tool to use (CBM vs Serena vs GitNexus vs Grep) | `.claude/docs/code-intelligence.md` |

**Freshness rule:** On first read of `codebase-map.md` in a session,
verify structural claims against current code. If a package status or
gotcha has changed, flag to user before acting on stale assumptions.
`docs/architecture/CURRENT.md` carries per-entry `verified:` stamps
enforced by `scripts/check_subsystem_map.py` (CI `subsystem-map-check`) —
after changing a subsystem's capabilities, update its entry and stamp.

## Public Repo & Release Workflow

The public repo (`GENesis-AGI`) is the primary development repo.
Standard open-source workflow: PRs go directly to the public repo.

- **Squash merges only** — merge commits are disabled on the public repo.
  Always `git pull --rebase origin main` **on main** after merging a PR before
  committing locally, or push will be rejected (non-fast-forward).
- **Once a branch is pushed: merge main IN, never rebase it** — force-push is
  banned, so a rebased pushed branch diverges from its remote with no way to
  publish the rewrite. A merge commit on a *branch* is fine — the squash erases
  it at PR merge; "squash only" above governs merging into main, not
  reconciling a branch.
- **In a reconciliation merge, the base wins on policy.** The merge commit may
  DELETE from your branch (a rule main superseded, a test that pinned your
  now-dead behavior); it may never re-litigate main. Scope, precisely: base
  wins where main *deliberately changed a thing your branch also changes*; on
  your branch's own new work the merge integrates both sides — keep your
  feature, adopt main's surrounding changes. "Base wins" is never
  `--theirs` on a whole hunk. If you disagree with what main did, that is a
  NEW PR with its own review (a sanctioned revert PR is exactly that — the ban
  is on *unreviewed* reversals) — never a conflict resolution, which is the
  one diff nobody re-reviews. And a clean auto-merge is not a clean
  reconciliation: the superseded-rule case usually merges with NO textual
  conflict, because your rule and main's live in different hunks — so after
  merging main in, re-check every rule and test your branch carries against
  what main changed in the interval. The lived failure (2026-09-04): main
  widened a gate rule while a narrower rule for the same paths sat in review;
  restoring the branch's rule "so its tests stay green" would have silently
  reverted a deliberate policy change inside a merge commit. Delete your
  superseded rule AND its tests — a test pinning behavior the base retired is
  not coverage, it is a revert waiting to be committed — and name every such
  deletion in the PR body; if you suspect main's change is a bug rather than
  policy, file that before merging. A deleted test with no filed disagreement
  is how coverage disappears.
- **The CI collected-test floor conflicts whenever main moved it while your
  branch added tests.** `.github/workflows/ci.yml` carries one hand-maintained
  `--min-collected` number, and both sides have raised it from a shared base.
  Picking either side under-counts. The merged floor is MAIN's floor plus YOUR
  branch's delta, and the delta is measured, never recalled. Run
  `--collect-only` on main's version of each test file your branch changes and on
  the merged version, after your LAST test-adding edit. A file new to the branch
  counts in full. Keep main's comment block and add one paragraph showing your
  per-file counts. The `--ceiling` on the same line merges the same way: main's
  ceiling plus your branch's skip delta, counted on the CI runner from the junit
  report, never locally (installed tools differ, so local skip counts are the
  wrong denominator). Issue #2743 tracks replacing the hand-maintained number. A test path held in a shell variable reads to
  `full_suite_guard` as a whole-suite run, so write the count as a script.
- **README is public-authoritative** — the public repo's `README.md` is
  hand-crafted and must NEVER be overwritten.
- **Never edit `CHANGELOG.md` in an ordinary PR — add a `changelog.d/`
  fragment.** (The one exception is the release-fold PR, where the
  `CHANGELOG.md` diff is produced by `scripts/assemble_changelog.py` and the
  section rename rather than written by hand — see the release procedure in
  `docs/reference/recovery-and-portability-workflow.md`.)
  One file per change, named `<YYYYMMDDHHMMSS>-<category>-<slug>.md`
  (timestamp from `date -u +%Y%m%d%H%M%S`; category is one of `added`,
  `changed`, `deprecated`, `removed`, `fixed`, `security`). The file holds
  the entry exactly as it should appear — a Markdown bullet, spliced in
  verbatim. `scripts/assemble_changelog.py` folds them into `[Unreleased]`
  at RELEASE time, not per PR.

  This is not bookkeeping preference: two branches editing one shared
  changelog insert at the same position, which git calls a conflict.
  Measured 2026-09-04 — of 49 open PRs, 21 could not merge and **18 of
  those conflicted on `CHANGELOG.md` and nothing else**. Two branches
  writing two different filenames have nothing to merge. A merge driver
  cannot substitute: GitHub ignores repository `.gitattributes`
  server-side, measured against its own merge engine.
- **CHANGELOG audience is users** — only include entries a user updating
  their install would care about. No internal refactors, README changes,
  CI tweaks, or process artifacts. Lead with the user-visible effect, not
  the implementation technique. Same bar for a fragment: it becomes a
  changelog entry verbatim.
- **No sensitive data in commits** — voice data, research profiles, IPs,
  and secrets must never enter the repo. User data lives in overlays
  outside the repo (e.g., `~/.claude/skills/*/`, `~/.genesis/`).
- **Individual campaigns are user data, not infrastructure** — a campaign's
  name/prompt/targets/cadence live only in the `campaigns` DB table and the
  private backups repo; never hardcode them into tracked source. Unlike modules
  (which ship defaults under `config/modules/*.yaml`), campaigns ship ZERO
  defaults (no `config/campaigns/`). Only campaign infrastructure ships. Express
  reusable session types as generic roles (e.g. the `community-responder`
  profile), not names coupled to a live campaign. See `src/genesis/campaigns/__init__.py`.
- **External egress is gated; owner-facing egress is not** — any autonomous send to the
  outside world (Discord, Medium, Twitter/X, Slack, `DistributionManager.distribute`) MUST
  route through the capability shadow-gate (`autonomy/shadow_gate`) before the enforce stage;
  the `scripts/check_external_io.py` CI guard backstops new endpoints. Delivery TO the owner
  (Telegram/voice/email-to-owner) is NEVER gated. Full contract in `autonomy/shadow_gate.py`.

Files in this skill

  • references/ai-code-audit.md3.6 KB
  • references/architecture.md2.5 KB
  • references/build-state.md1.9 KB
  • references/codebase-map.md11.2 KB
  • references/contribution.md2.5 KB
  • references/environment.md2.8 KB
  • references/observability.md2.5 KB
  • references/worktrees.md7.9 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…