Agent Skills › WingedGuardian/GENesis-AGI › genesis-development

genesis-development

GitHub

用于开发、调试和重构 Genesis 内部代码,如添加 MCP 工具或修复运行时问题。区分内部开发与外部工具使用场景。

.claude/skills/genesis-development/SKILL.md WingedGuardian/GENesis-AGI

Trigger Scenarios

修改 src/ 目录下的文件 调试 Genesis 启动或桥接问题 为 Genesis 添加新能力或 MCP 工具

Install

npx skills add WingedGuardian/GENesis-AGI --skill genesis-development -g -y
More Options

Non-standard path

npx skills add https://github.com/WingedGuardian/GENesis-AGI/tree/main/.claude/skills/genesis-development -g -y

Use without installing

npx skills use WingedGuardian/GENesis-AGI@genesis-development

指定 Agent (Claude Code)

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

安装 repo 全部 skill

npx skills add WingedGuardian/GENesis-AGI --all -g -y

预览 repo 内 skill

npx skills add WingedGuardian/GENesis-AGI --list

SKILL.md

Frontmatter
{
    "name": "genesis-development",
    "phase": 10,
    "consumer": "cc_foreground",
    "keywords": [
        "genesis",
        "worktree"
    ],
    "skill_type": "workflow",
    "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.\n"
}

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".
  • A cloud coding agent (Devin) is a third kind of builder. It works from the public repo in its own VM, with no access to an install: no running Genesis, no ~/.genesis/, none of an install's hooks or local reviewers. Hand it only self-contained, spec-complete work whose tests run in CI, off the enforcement-hook surface, as a public issue. The devin-ready label hands it over and starts a paid session, so a session applies it only after the owner explicitly approves that issue in conversation. Once approved, the session applies the label itself; the owner does not apply it by hand. Its PRs join the same queue (closing-session, "Devin-built PRs").

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, existing implementations outside the repo (public repos, grep.app, and the Claude Code mods catalogue for anything that runs inside a CC session), 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; the one exception is a part whose design the plan deferred (below), which gets its own review once that design is written. 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.

A plan part that defers its own design is not finalized. "Read X first, then design it" is a placeholder, and a review of the plan reviews the placeholder. Before coding that part, write its design, and give the part its own architect review. For anything that persists state another program reads, the design itself carries the writer-state × reader table: every state the writer can leave (complete, skipped, partial, failed midway, killed, a leftover from an earlier run) against every reader, with what each reader does in each cell. Hand that table to the architect and ask it to hunt for the missing cells. (genesis-architect Step 0.7 covers the lifecycle of state inside a guard or gate only, so it does not do this for you.) (Origin: PR #2853 — the plan reviewed once, its backup part was a placeholder, and ten of 25 external findings over five rounds were cells of the backup/restore state table nobody had written down.)

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; on an ADAPT verdict, reverse-engineer the reference before writing code (evaluate, Decision Protocol step 4).
  • About to build anything that runs inside a Claude Code session — a hook or guard, status line, prompt or context injection, tool interception, a notification, an in-terminal pane → search Claude Code mods and plugins first (evaluate workflow step 2 names the sources per session context). From CC 2.1.287 mods can carry UI, tools, agents and model calls; below that version they need the early-access flag, so check the pin in docs/reference/cc-compatibility.md. Genesis has already shipped one (plugins/genesis-web-override/). A mod may AUGMENT an enforcement guard but never replace it: a mod that denied a tool call ran before the classic PreToolUse chain and suppressed it when measured (#2260).
  • 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:

---
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.
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, scripts/hooks/deploy_timeout_advisory.py, nudges toward this whenever a command can run update.sh, and stays quiet on reads of it. It deliberately does not check whether you already detached: that needs systemd-run's option grammar, an open set, so a detached launch still gets the note.)

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.

  • Verify-RED of a safety fence runs the test UNFENCED. Removing a guard, stub or sandbox to watch its test fail means whatever the test executes reaches the real system for that run. Read the test first and make its probe harmless even unguarded: systemctl --user is-active <a unit that does not exist>, told apart from the stub by the stub's own log, never the dangerous action itself. (Origin: the fence test in PR #2935, for issue #2863: its verify-RED ran a real systemctl --user stop genesis-server and took a live server down for about 3.5 minutes.)

  • Bound every mutant run with its own short timeout. A mutation often turns a test into a HANG rather than a failure (a mutant that never sets an event, never cancels a task), and a hung run holds the box-wide test lock for the whole per-test timeout while every other session waits. MEASURED: one sweep's hung mutant held it for 53 minutes. Pass -p timeout -o timeout=<N> to each mutant run, with N = max(60, 10 × the baseline run's slowest test). Spell it -p timeout, never -p pytest_timeout: the plugin registers under the name timeout, and the module name aborts pytest with "Plugin already registered" (MEASURED, pytest 9.0.2 with pytest-timeout 2.4.0). And read the test result, not the exit code: a missing plugin also exits 1, which a sweep would count as a killed mutant.

  • Never edit a shell script while a test that runs it is in progress. bash reads a script as it executes it, so an edit mid-run yields bogus syntax errors that look like real failures. Wait for the run to finish, or test a copy.

  • 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 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 six 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 exactly as .claude/docs/premise-check.md "The output" specifies (code reads it). 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. A round advances only on the newest reviewed head (owner ruling, 2026-10-12): every review MARKS the head it reviewed, clean ones too, and a finding review that lands on a head after one of that head's descendants was marked is a LATE review. It opens no round; review_budget's findings list keeps it with the round it belongs to, for the round reflection to owe: scripts/review_late.py owes it from REFLECTION_CUTOVER_ISO (scripts/review_reflection.py) on, and credits it only by a reflection judged under its own round. The cutover stays None (nothing more owed) until the commit gate enforces reflections; review_reflection.py status --cutover <iso> reports what it would owe. Each entry says whether it was late (late) and each round when it opened (opened_at, None when unknown); read those, never membership in rounds[].reviews, which also lists late reviews of a head that already is 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 by config (its one exemption is per PR, for an outside contribution; see Pre-Merge Gate). 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:

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 -u origin HEAD && gh pr create --title "..." --body-file <file>   # first push: ONE command (approve the dialog)
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 other keys are push_routine (routine push prompts, 2026-10-06), review_request (a Codex review request whose review history could not be read; the round-cap prompt still asks, 2026-10-07) and push_publish (owner ruling 2026-10-01), all the same no-decision-plus-note shape; hook_ask_policy.py holds the exact scope of each. push_publish 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), or that push followed by && and one ready gh pr create. Two spellings also qualify for the lone push: git -C <path> push … when <path> is the literal, symlink-free top of a worktree of the SAME repository as the session's cwd, and a trailing 2>&1 and/or | tail -N / | head -N; the chained form allows neither. On the public repo only the chained form can be silenced, because of the rule below: a lone plain first push of the current branch there is denied, not silenced (other spellings keep their ask). Any other compound still asks. 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.

  • A FIRST push of the CURRENT branch to the configured public repo must open its PR in the same command — otherwise it is DENIED, whatever push_publish says. The re-push no-open-PR block only catches a branch on its NEXT push, so a session that stopped after one push left the branch public with no PR. The one accepted shape is git push -u origin HEAD && gh pr create --title "..." --body-file <file>: one plain git push of the current branch (no cd, git -C, prefix, output filter or earlier step), &&, and one ready gh pr create using only title/body/body-file/fill/no-maintainer-edit options — never --draft/-d, --web, --dry-run, --head, --repo or --base, and no --label/--assignee/--reviewer/--milestone/--project (gh resolves those names before creating the PR and fails the whole create on one it cannot resolve, after the push; add them with gh pr edit once the PR exists). Its values must be LITERAL: no $, backtick, backslash, *, ?, [, { or ~ anywhere in the create, quoted or not. The create must be one gh accepts with no terminal (MEASURED, gh 2.101): a non-empty title AND a body (--body, or --body-file naming a regular file in the push's directory that the hook can actually open and read as UTF-8, never -; not both, and at most 65,536 characters of body either way), OR exactly one of --fill/-f, --fill-first, --fill-verbose. A short value flag is read as gh reads it: -F=body.md names body.md, and -t= is an empty title. ||, ;, &, a newline, or a # before the && all deny. Work not ready for a PR stays committed locally, unpushed. Scope is the re-push block's predicate (_no_pr_block_applies): another destination, or no declared public repo, keeps the old behaviour; a dry run publishes nothing and is not denied, and after a dry-run push a chained create keeps its own would-publish probe. This is deliberately the client half only — what a hook can read from the command text. It does not model other push spellings (another branch, a refspec, --all, an unresolvable cd): those keep their ask, as before. Nor does it model what gh resolves at RUN time — the base repository (gh repo set-default, GH_REPO/GH_HOST in the environment), the base branch (branch.<b>.gh-merge-base), whether the branch has commits ahead of it — or a create that fails for auth/network reasons; any of those can still leave a branch public with no PR until its next push, which the re-push block then refuses. Modelling git/gh semantics from shell text is the open-set trap described under "Never hand-roll gh/bash/CLI argv parsing" (and in scripts/hooks/pr_close_advisory.py's docstring). The guarantee that no branch reaches the public repo unchecked is the SERVER-SIDE leak scan that runs on every branch push, not this hook.

  • Only the main session publishes. An Agent-tool worker (the hook input carries agent_id) may run git and gh only from an ALLOWLIST, whatever push_publish says (owner policy 2026-10-05; an allowlist since 2026-10-10, because each review round found a write verb a denylist had missed): local git work plus git's network reads (fetch, pull, clone, ls-remote), and gh reads (gh pr view/list/diff/checks/status/checkout, gh issue view/list, gh run view/list, gh search, gh api GETs and GraphQL queries). Everything else is refused: git push, git lfs lock, an unknown or aliased git command, every gh write including gh auth login, an unknown gh subcommand, a non-GET gh api or GraphQL mutation, git options that run a program (--upload-pack, grep -O, rebase --exec), and a command naming git or gh in a form the guard cannot read (a line continuation, a too-long command, a program held in a variable). Not reached: launchers the parse does not open (eval, process substitution, env -S, a function body) and programs git starts from config or the environment. Re-pushes of an already-published PR branch, dry runs and in-thread finding replies are included. The list is _GIT_SUBAGENT_OK, _GIT_FAMILY_VERBS and _GH_SUBAGENT_READS in git_push_guard.py. Commit locally, then hand the worktree path, branch, head SHA, the PR title and body, and any comment or reply you would have posted back to the main session. A subagent dispatched to fix a PR therefore leaves the push and the replies to the session that dispatched it.

  • 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).

  • The primary checkout is the deployed install; a guard keeps hand edits out of it — two different ways. scripts/hooks/main_checkout_guard.py (foreground and dispatched alike) REFUSES a Write/Edit/MultiEdit/NotebookEdit of a TRACKED file in the primary checkout its own script lives in. Bash it never refuses: it snapshots the deploy root's tracked state before the command (PreToolUse, keyed by tool_use_id) and compares after it (PostToolUse and PostToolUseFailure), and if a tracked file newly differs, an already-dirty one changed again, or HEAD moved, the session gets an advisory naming the files — with a restore command only when the call was aimed at the deploy root (its cwd, or the root's path in the command) and no merge is in progress, and never an instruction to move HEAD. An earlier version predicted what a shell command would write from its own model of cp/mv/install, the shell and git; an audit measured that model wrong in both directions, so it was removed rather than patched — the "could a flag you've never heard of change the verdict?" test above, applied. The Bash check reports after the fact: it prevents nothing, a file written and run by the same command has already run, and anything else that changed the deploy root during the command (a concurrent deploy) is reported too. Make the change in a worktree from origin/main and open a PR; if the advisory fires, restore the deploy root with the command it names. Untracked files, linked worktrees, other repositories and the deploy scripts' EPHEMERAL_DIRTY_RE paths are never reported. Sessions carrying GENESIS_UPDATE_TIER=1 (exactly) — the dashboard update pipeline's — are exempt. Off-switches belong to the owner: GENESIS_MAIN_CHECKOUT_GUARD=0, or the main_checkout_guard settings domain, whose overlay is read only from ${GENESIS_HOME:-~/.genesis}/config/, never from the checkout.

  • ⚠ 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:

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

  • PR shape, after your last commit and before opening the PR (#2737; owner, 2026-10-05). It reads committed history, so staged changes are not counted: commit first. Count the branch in counted lines, which excludes tests, prose, changelog fragments, and blank and comment lines, and counts a moved line once:

    # from the repo root, after `git fetch origin main`; a stacked PR passes its parent branch as --base.
    python3 scripts/pr_shape.py --base origin/main
    

    It prints <counted> <band>. It reads the diff through the same hardened git runner the review gates use, and exits 2 on a git error, so a failed diff never reads as 0 ok. Use this command, not a hand-written git diff | count_diff: an attributes file, a textconv driver or a replace ref can each shrink a raw diff at exit 0.

    • Under 500 is the target.
    • From 500 to 1,000, add a Shape: line to the PR body saying why it cannot be smaller.
    • Over 1,000 needs the owner's approval: ask in the PR body; the owner decides before merge.

    It is a guideline you weigh, not a wall: a legitimate reason, stated, is enough. Prefer one concern per PR (one mechanism or behaviour change a reviewer can accept or reject alone); that too is weighed. Full rule: .claude/docs/premise-check.md, step 6.

  • 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.

  • Heavy jobs go through genesis.hostmetrics run. A build, a parallel test run, a media encode, anything that holds gigabytes or several cores for minutes: launch it with the Genesis venv's interpreter (~/genesis/.venv/bin/python on a standard install; a session shell usually has no bare python) as … -m genesis.hostmetrics run --name N --ram GB --cpu PCT -- CMD. It checks the budget first (GO/WAIT/NO/ASK), then runs the job in a systemd scope capped at its estimates that other sessions' status and preflight see; without a systemd user manager it runs uncapped and invisible, and says so. preflight checks without running. It is not a way around the full-suite rule: test targets must still be named, and do not count on the full-suite guard to see inside the wrapper. Guide: docs/reference/resource-budget.md.

  • Deleting user or project data goes through the trash, not rm. A file or directory you did not create in this session (memory and plans, config, a deliverable, notes) goes to … -m genesis.trash put PATH --reason R, which renames it into ~/.genesis/trash/ with a tombstone so … -m genesis.trash restore ENTRY can bring it back. rm stays fine for what you created yourself and for temp (~/tmp, cc-tmp, the scratchpad). Unlike rm -f, a missing path is a refusal (exit 1); a path starting with - goes after --. The trash makes a deletion recoverable; it does not replace asking the user first where deleting their data needs their OK. When put refuses (another volume, the database, a file a running process is using), ask the user; do not fall back to rm. Guide: docs/reference/trash.md.

  • 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 prints 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,…
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 enabled 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 rules, 2026-10-04 and 2026-10-06). The closing session, or the builder of a rework that replaces it, may retire it when all three hold. Devin never retires one: the closing session retires a Devin rework's old PR after the same check ("Building a rework" below).

  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. For a rework, that record is a ## Rework spec comment posted by a maintainer (author association OWNER, MEMBER or COLLABORATOR); one from a non-maintainer, including a non-maintainer author of the PR, is not the owner's authorization.
  2. The successor PR is open, or already merged, so the work has a live home. For a split rework, the LAST replacement has opened and every replacement is open or merged.
  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.

Building a rework — report against the spec (standing owner rule, 2026-10-05)

A PR sent back with needs-rework, or a needs-architecture-session PR whose owner decision has since been posted as a rework spec, carries that spec: a maintainer comment headed ## Rework spec, plus any design issue it names. An audit or proposal without that heading is not a spec, and a needs-architecture-session PR with no posted spec has an undecided design; do not build it. Its contract is in .claude/docs/premise-check.md, "Handing a verdict to a builder". The session that takes up the rework is the reviving session above, and it owns the following:

  1. Acknowledge the spec on the OLD PR before building. Post one comment there with:

    • the spec as you understood it, in your own words;
    • the split you will build;
    • every question you need answered before you start.

    Head it ## Rework acknowledgement, and open no PR, not even a draft, until it is posted. If it asks no questions, proceed once it is posted; otherwise wait for an answer on the old PR, from the closing session or the owner. On a Devin-built old PR, put (aside) on the first line and the heading under it, or the comment starts a paid Devin session. Post it with gh api repos/<o>/<r>/issues/<N>/comments -F body=@<file>: on a PR past its terminal round the push guard reads a gh pr comment whose body it cannot see (--body-file, or an inline body with a backtick or $) as a possible @codex review request, which asks in the foreground and is refused in a dispatched session. A question answered before building is cheap; the same question answered silently inside the build is how a rebuild drifts. No acknowledgement means nobody can tell whether the spec was read at all.

  2. New PRs, by default. The rework arrives as one or more NEW PRs, with fresh round counts. When the LAST one opens, whoever opened it closes the old PR with a comment that maps every part of it, file by file, to the file:line in the replacement that covers it, or says why that part is moot. If any part is neither, leave the old PR open with a comment naming that part. Leave its labels on, so the rebuild stays traceable to it. If the spec carries a Follow-up: <id> line, copy it into the body of the replacement that will merge into main LAST, so the follow-up closes only when the whole rework has landed. A merge into any other base completes the marker too, so never put it on a PR that merges into another PR. Reworking under the old number happens only when the owner grants it. Devin builds a rework as fresh PR(s) too, and never closes the old PR; the closing session does, after the same coverage check (closing-session, "Devin-built PRs").

  3. Follow the split. If the spec has a PR-shape: SPLIT plan, open those PRs. Each one carries one concern and names the PRs it depends on. Open them in dependency order against main; a PR whose dependency has not merged waits as a branch, or opens stacked with its base named. Being a rework is not itself a reason to stay unsplit, and a better split is a deviation (item 4), not a reason to skip splitting. If the spec has no split, size each PR by the rule in .claude/docs/premise-check.md step 6: under 500 counted lines is the target, and a Shape: line states why one cannot be smaller. Split a PR that carries more than one concern.

  4. Answer the delegated questions in the PR body, each with your reasoning. If you hit a question the spec should have answered and did not, answer it the same way and mark it as missing from the spec. Do not resolve it silently in the most defensive direction: a fail-closed answer is still a design decision, and it can force machinery the spec never asked for.

  5. Report every deviation. Unforeseen complications are expected; an unexplained deviation is not. The body of each replacement PR carries:

    ## Rework
    Replaces: #N (spec: <link to the `## Rework spec` comment>; acknowledged: <link to your item-0 comment>)
    Split: PR k of n (<the other PRs, by number or concern>)
    Kept / deleted / reshaped as the spec asked: <one line each, or "as specified">
    Deviations: <each: what differs from the spec, and the complication that forced it> | none
    Questions answered: <each delegated or missing question: the answer and why>
    

The closing session reads this section before it spends a review round. A deviation with a stated reason goes to the owner as a design question. A missing section, an unexplained deviation, or an unsplit PR the spec split goes back to you before review. Origin: a rebuild came back as one PR, +2,538 raw lines (881 counted, so inside the shape band; size was not the failure), with conditional rendering, an uninstall retention mode and an in-place settings writer, none of which its spec asked for, and with no note explaining any of them. Working out whether the spec or the build had failed cost the owner a session.

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.

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:

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 → rounds (advisory) → scheduled-Claude-review → review-body → inline findings → main-reverts (advisory) → rework), so the two use the same checks. The merge arm checks review-body and inline findings before scheduled review, runs rework last (waived only by the logged # rework-override, which needs the owner's yes), blocks on none of the three advisory rows, 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. An outside contribution needs no leaks marker (owner ruling 2026-10-05): a fork PR opened by a human who is not OWNER, MEMBER or COLLABORATOR, at the current head, whose every commit they authored (committer them or web-flow, no Co-authored-by: trailer), whose title only they renamed, whose body only they or a named review app (_BODY_EDIT_REVIEW_APPS) edited, and with CI's leak-detector green at that head (on a fork it runs without its private-pattern step, which needs a secret forks do not get). The review guards the owner's private data, which an outsider's text cannot hold. A commit, a committed suggestion or a text edit of ours puts the requirement back, a leaks review that ran and objected is never overruled, --check-pr shows leaks not required: outside contribution by <login>, and a fork PR the exemption could not clear says why in its block message (_outside_contribution). Text written into the squash commit at merge time (gh pr merge --body) is not covered, as before. 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: — 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 six PR-merge sigils are covered (# review-override, # ci-override, # stale-review-override, # scheduled-review-override, # substitute-review, # rework-override), 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.

  • Adding tests no longer touches ci.yml. The collected-test count is checked by the collected-floor CI job (#2743), which compares the test ids your change collects against the commit it lands on. It is a required check once .github/rulesets/checks.json is applied (scripts/apply_rulesets.py). It goes red (exit 1) when ids vanish from test files your change did not touch (a conftest.py edit does not count as touching the files below it), or when your tree has a collection error the base does not. It refuses (exit 2) when it cannot measure: a base it cannot resolve (not a 2-parent merge, or an unreadable pre-push commit), base dependencies that would not install, pytest exiting other than 0, 1 or 5, a collection that did not finish or collected nothing, malformed collection output, an unreadable changed-file list, a PR-body scanner that would not load, or an unexplained drop whose PR body could not be read. Read the step summary first; it names every vanished id. If a drop is deliberate, put a line Collected-drop: <reason> in the PR body and re-run the job (or push, if a re-run no longer clears it: #2624). A branch cut before #2743 that still edits the old --min-collected line resolves that conflict by taking main's side and deleting its own floor paragraph. The --ceiling (skip count) still lives on the dormancy-guard line: raise it only in the commit that adds the skip, counted on the CI runner from the junit report, never locally (installed tools differ, so local skip counts are the wrong denominator).

  • 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.

Version History

  • aa71988 Current 2026-10-18 23:13

    修正 devin-ready 标签的触发逻辑:明确由会话在获得所有者批准后自行应用该标签以启动付费 Devin 会话,而非仅由所有者手动操作。

  • 46ec841 2026-09-27 16:49

    新增 Step 0.7 持久化状态检查规则,要求在设计守卫或门控时明确状态生命周期,并基于证据优化标记设计。

  • 0b82cd0 2026-09-22 05:49

    新增会话类型划分(构建与关闭)及吞吐量管理规则;更新安全缺陷披露指南,明确 fail-open 非缺陷及披露判断标准。

  • 32e00aa 2026-09-09 00:12

    修正bypass-mode的检测逻辑与防护机制;记录通过Bash编辑文件的发现并刷新注入映射;文档化prune与watcher的耦合关系;修复测试中因命名冲突导致的误报。

  • 6f3310e 2026-09-03 01:53

    修正 review 门控规则以回应 Codex 审查发现,并将 review-gate 经验教训迁移至本技能文档。

  • 3ca56ae 2026-08-27 14:05

    新增 hooks 表面合并的安全机制,要求 Codex-at-head 并提供证据以覆盖审查,增强门禁安全性。

  • 9a68e71 2026-08-19 16:21

    新增 /deep-review 对抗性逻辑预推送命令及无手工解析规则,强化审查流程与代码鲁棒性。

  • 51253c6 2026-08-13 14:05

    强化 Codex-at-head 合并门禁逻辑:拆分覆盖信号量以隔离新鲜度与发现扫描检查;实现智能差异缩小功能以优化阻塞判断;移除已驳回评审的生效资格;改进 TOCTOU 绑定块的执行时机并补充集成测试。

  • 94be12b 2026-08-07 00:30

    通过 hooks 在 commit gate 层面机器强制执行 review 升级轮次上限(3轮),防止审查循环无限进行;修复因文档/配置跳过逻辑导致的绕过漏洞,确保限制在跳过检查前生效。

  • 7b2d218 2026-08-06 06:35

    强化Review循环限制为硬性阻断,增加调试纪律(根因分析、测试先行)和TDD规范,新增反合理化检查项。

  • 563f090 2026-08-03 13:04

    新增贡献指纹生成功能,将本地私有泄露检测模式移出受控源码,增强安全与合规性。

  • 5236453 2026-08-01 05:32

    新增审查循环纪律:限制单次仅一名审查者,并在3轮提出新缺陷后升级处理。

  • 5d12d48 2026-07-31 11:21

    新增记忆完整性检测核心(Phase 0),实现跨后端一致性检查器以发现静默数据不一致;增加配置驱动的健康探针、作业调度及仪表盘支持,提升可观测性。

  • 83a6c7f 2026-07-19 22:29

    新增实例修复与类修复的判定门控,明确数据修复不属于修复范畴;增加反合理化校验行,要求手工艺品需修复机制或显式延期。

  • f9015bb 2026-07-05 18:16

Same Skill Collection

.agents/skills/genesis-external-client/SKILL.md
.claude/skills/cc-update/SKILL.md
.claude/skills/code-intelligence/SKILL.md
.claude/skills/content-publish/SKILL.md
.claude/skills/genesis-voice/SKILL.md
.claude/skills/gitnexus/gitnexus-cli/SKILL.md
.claude/skills/gitnexus/gitnexus-debugging/SKILL.md
.claude/skills/gitnexus/gitnexus-exploring/SKILL.md
.claude/skills/gitnexus/gitnexus-guide/SKILL.md
.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md
.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md
.claude/skills/shelve/SKILL.md
.claude/skills/subsystem-map/SKILL.md
.claude/skills/taste/SKILL.md
.claude/skills/unshelve/SKILL.md
.claude/skills/web-research/SKILL.md
config/gstack-patches/codex-SKILL.md
src/genesis/skills/aws-fde-delivery/SKILL.md
src/genesis/skills/browser-automation/SKILL.md
src/genesis/skills/debugging/SKILL.md
src/genesis/skills/evaluate/SKILL.md
src/genesis/skills/forecasting/SKILL.md
src/genesis/skills/integrate-module/SKILL.md
src/genesis/skills/lead-generation/SKILL.md
src/genesis/skills/linkedin-comment-strategy/SKILL.md
src/genesis/skills/linkedin-content-calendar/SKILL.md
src/genesis/skills/linkedin-dm-outreach/SKILL.md
src/genesis/skills/linkedin-hook-writer/SKILL.md
src/genesis/skills/linkedin-post-writer/SKILL.md
src/genesis/skills/linkedin-profile-optimizer/SKILL.md
src/genesis/skills/obstacle-resolution/SKILL.md
src/genesis/skills/onboarding/SKILL.md
src/genesis/skills/osint/SKILL.md
src/genesis/skills/prospect-researcher/SKILL.md
src/genesis/skills/research/SKILL.md
src/genesis/skills/retrospective/SKILL.md
src/genesis/skills/stealth-browser/SKILL.md
src/genesis/skills/triage-calibration/SKILL.md
src/genesis/skills/user_evaluate/SKILL.md
src/genesis/skills/video-processing/SKILL.md
.claude/skills/closing-session/SKILL.md
.claude/skills/deliverable-builder/SKILL.md
.claude/skills/validating-merges/SKILL.md
.claude/skills/voice-master/SKILL.md
.claude/skills/youtube-fetch/SKILL.md
src/genesis/skills/voice-master/SKILL.md

Metadata

Files
0
Version
aa71988
Hash
793f42c7
Indexed
2026-07-05 18:16

홈 - 위키
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-10-20 19:55
浙ICP备14020137号-1