# REPORT — R-404 / R-417: block the push that can act, notify the one that cannot (2026-09-01) **No version bumped, no image built, no golden owed.** This changes no Go code. Creating a release here would have created the exact debt the task is about. ## 1. The git stdin format, measured Against **git 2.47.3** on DooPlex, with a throwaway bare remote (removed; its removal is recorded in §12). A pre-push hook receives, on **stdin**, one line per ref: ` `, four whitespace-separated fields. Observed directly, not read from documentation: | case | line | |---|---| | ordinary push | `refs/heads/master 0bb77614… refs/heads/master bb88be35…` | | **first push of a ref** | `refs/heads/master bb88be35… refs/heads/master 0000000000000000000000000000000000000000` | | two refs at once | two lines, one per ref | | **deletion** | `(delete) 0000000000000000000000000000000000000000 refs/heads/side 0bb77614…` | Both all-zero cases classify as **code**, fail-closed: a first push has no range to diff and a deletion has no content. **My instrument failed first and I nearly believed it.** The initial probe printed nothing at all. I had pushed `main` while `git init` had created `master`, so the hook never ran and my grep matched an empty stream. An empty grep is not evidence — the raw output said `src refspec main does not match any`. Re-run on the real branch, all four cases above appeared. ## 2. The classifier against real history — 18/6, exact agreement Last 24 commits ending at the task's baseline `a91c058`: **docs = 18 · code = 6.** The task's §2 measurement was 18/6. **No disagreement.** The six code pushes: `22e1c95` (the R-410 gate fix), `1aeaa30` (hub v0.110.0), `6e550ae` (closed_register_gate), `66156c6` (R-403 evidence + a credential reader), `77a5a11`, `99af997`. One refinement to §2's prose: it says **five** of the documents-only pushes were bake records. I count **six** — `4f87517`, `db0812b`, `1623a4d`, `83ff9e8`, `2263245`, `63eff21`. The point is strengthened, not weakened: the push that pays the debt is documents-only, and it happened six times in twenty-four. ## 3. Files created / modified **felhom.eu** — `1c00af6` (code), `1f74427` (docs), plus the register/docs commit below. | file | what | |---|---| | `scripts/push_scope.py` | NEW — the classifier | | `scripts/test_push_scope.py` | NEW — P1–P5 | | `scripts/test_repo_gates_scope.py` | NEW — R1–R6, Scenario C | | `scripts/repo_gates.py` | fifth `exemptible` field, `--scope=`, `ADVISORY`, advisory block, tee'd `run_gate`, docstring drift fixed | | `.githooks/pre-push` | reads stdin, passes `--scope=`, honest-limits header extended | | `.gitea/workflows/gates.yml` | same rule in CI from the push event payload | | `documentation/runbooks/target-selection.md` | the drill-night line | | `CONTEXT.md`, `STATUS.md`, `scripts/CHANGELOG.md`, register | the ruling | **felhom-controller** | file | what | |---|---| | `controller/scripts/golden_notice.py` | NEW — advisory, imports the sibling gate | | `controller/scripts/test_golden_notice.py` | NEW — N1–N4 | | `controller/scripts/controller_gates.py` | fifth `blocking` field; the notice registered non-blocking | | `CHANGELOG.md` | an entry with **no version heading** | | `REUSE.md` | how to register a reporting-only gate | ## 4. Test results, and the three red-proofs by name All pass. | test | cases | |---|---| | `test_push_scope.py` | P1 (11 doc paths) · P2 (8 code paths) · P3 (7 unknown → code) · P4 (mixed → code) · P5 (5 untrustworthy ranges → code) | | `test_repo_gates_scope.py` | R1 · R2 · **R3 (Scenario C)** · R4 · R5 · R6 | | `test_golden_notice.py` | N1 · N2 (+ the only-one-non-blocking control) · N3 · N4 | **Red-proofs, all three run, all reverted, all confirmed by the suite passing afterwards:** - **P3** — allow-list swapped for a deny-list (`return not p.startswith(("hub/","website/", "manifests/","scripts/"))`). P3 **failed**, naming all seven unknown paths as documents: `terraform/main.tf`, `cmd/newthing/main.go`, `Makefile`, `docs/readme.md`, `documentation-old/x.md`, `src/app.py`, `.github/workflows/ci.yml`. - **R3 — the one that matters.** The `site` row's fifth field flipped to `True`. R3 **failed**: *"a documents-only push with the SITE gate convicting was ALLOWED. The exemption has become general."* - **N1** — the notice's debt branch changed to `return 1`. N1 **failed** with `Got exit 1`. **Scenario C was written first and failed for the right reason** before any implementation existed: `--scope` was an unknown argument, and unpacking the GATES table raised `ValueError: too many values to unpack (expected 4)`. ## 5. Live validations 2, 3 and 4, verbatim Run against the **real** `.githooks/pre-push` on a throwaway local bare remote, so no test commit reached Gitea. The hook does not know or care what the remote is. **Validation 3 — code push, golden owed → REFUSED (exit 1):** ``` push_scope: CODE (6 file(s): 0 document, 6 code) CODE because these are not on the document allow-list: scripts/push_scope.py scripts/test_push_scope.py pre-push [felhom.eu]: running scripts/repo_gates.py --fast --scope=code ... GOLDEN CURRENCY GATE FAILED: controller v0.232.0 is released and NO golden carries it (newest bake is 0.230.0). golden-currency FAILED (exit 1) CONVICTED: golden-currency pre-push [felhom.eu]: PUSH REFUSED - gates exited 1. ``` **Validation 2 — documents-only push, same debt → ADVISORY, ACCEPTED (exit 0):** ``` push_scope: DOCS (1 file(s): 1 document, 0 code) pre-push [felhom.eu]: running scripts/repo_gates.py --fast --scope=docs ... repo_gates (felhom.eu) — 13 gate(s) [--fast] [scope=docs] golden-currency ADVISORY (exit 1) !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! ADVISORY — golden-currency convicted, and this push is NOT refused for it. !! newest released controller : 0.232.0 !! newest golden baked : 0.230.0 !! !! This push touches DOCUMENTS ONLY, so it can neither create this debt nor clear !! it — and the push that DOES clear it (a bake record under documentation/tests/) !! is itself documents-only. Blocking here blocked the cure. !! !! WHAT CLEARS IT: bake a golden per documentation/runbooks/RUNBOOK-manual-build.md !! section 4.1, then vouch it (a THREE-field change: golden_version + agent_version !! + min_agent). The debt stays visible in STATUS.md and in the controller repo's !! own golden-notice until then. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! all felhom.eu gates OK (with 1 advisory — see above) pre-push [felhom.eu]: gates OK - push proceeding. 1c00af6..1f74427 main -> main ``` **Validation 4 — SCENARIO C, the acceptance step. Documents-only, golden owed, a second gate convicting → REFUSED for that gate alone:** ``` push_scope: DOCS (1 file(s): 1 document, 0 code) pre-push [felhom.eu]: running scripts/repo_gates.py --fast --scope=docs ... golden-currency ADVISORY (exit 1) observations FAILED (exit 1) !! ADVISORY — golden-currency convicted, and this push is NOT refused for it. CONVICTED: observations pre-push [felhom.eu]: PUSH REFUSED - gates exited 1. ``` **Validation 4 took two attempts and the first one was wrong.** Recorded rather than tidied away: 1. My first planted observation contained the sentence *"it carries no `FILED:` and no `NOT-A-FINDING:` marker"*, and the gate read the literal string and passed it — so the push succeeded and proved nothing. **That is a real defect in `observations_gate.py`, now R-419.** 2. Having pushed that commit, I amended it, which made the next range a force-push. The classifier correctly answered `code`, so the refusal I then saw was trivial and not Scenario C at all. I rewound the probe ref and re-ran it as a genuine fast-forward `docs` range — the output above. ## 6. Evidence restored, tree unchanged ``` golden currency gate OK — the newest released controller has a golden golden gate exit=0 evidence files: 14 diff vs HEAD: 0 git status --porcelain → (empty) ``` **A near-miss worth naming:** `git reset --hard` had already restored the tracked evidence directory before I moved my aside copy back, so the `mv` nested a duplicate *inside* it. Caught by `git status` showing an untracked `golden-0.232.0-2026-09-01/golden-aside/`. I diffed the two (`diff -r --exclude=golden-aside . golden-aside` → identical) **before** deleting anything, then removed the duplicate. 14 files, byte-identical to HEAD. ## 7. CI: changed, not left blocking — and why that is safe before it has run **Changed.** Leaving it blocking would have left R-417's actual symptom in place: red CI runs on a drill night, indistinguishable from real ones. That is half the harm. CI checks out `--depth 1` of a single SHA, so it has **no range**. The file list therefore comes from the push event payload and feeds the **same classifier** via `--files-from`, so there is one definition of "document" and not two. **Every failure path writes `code`:** no `GITHUB_EVENT_PATH`, unreadable JSON, no `commits` array, an empty array, an absent classifier. So this step can only make CI as strict as it is today, never looser — **the untested direction is the safe one**, which is why shipping it before observing it is defensible. **MEASURED after the push, so this is no longer an assumption.** CI job **481** (`1e6c387a`, felhom.eu) ran the new step and its log reads: ``` 3 commit(s), 12 distinct path(s) in the payload --- paths the push event reported --- scripts/push_scope.py scripts/test_push_scope.py push_scope: CODE (12 file(s): 5 document, 7 code) scope: code ::group::Run python3 scripts/repo_gates.py --fast --scope="${PUSH_SCOPE:-code}" ``` So Gitea's act-runner **does** populate `GITHUB_EVENT_PATH` with a `commits` array carrying per-file lists; the classifier ran on it and returned `code` for a push that genuinely touched `scripts/`. Job 481 is green. **STILL NOT OBSERVED: the `docs` branch in CI, and an advisory in a CI log.** The code path is proven; a documents-only CI run has not happened yet, and an ADVISORY there additionally needs a golden debt to exist at that moment. Neither is arranged artificially — the next documents-only push shows the first, and the next release-without-a-bake shows the second. The compensating controls that make a green documents-only CI run honest are named in the workflow itself: the advisory block in the run's own log, `STATUS.md`, and the controller-side notice. ## 8. `controller_gates.py` could NOT express a non-blocking gate **It could not, and the capability was added rather than the notice compromised.** Every registered gate's non-zero exit fed `worst` and failed the run; there was no way to describe a check that reports without refusing. A fifth `blocking` field now exists, `False` for exactly one gate, and `test_golden_notice.py` asserts it stays exactly one. Filed as **R-420**, because the absence was invisible — nobody had wanted such a gate before, so nothing recorded that it was impossible. `felhom.eu/scripts/repo_gates.py` still has **no** `blocking` field. It has `exemptible`, which is a different idea: scope-dependent, not permanent. If a permanently-advisory gate is ever wanted there, it needs the same addition. ## 9. Explicitly still open - **R-242's vouch half.** Nothing gates the vouch; a baked-but-unvouched golden passes both the gate and the new notice. Unchanged by this task and **not** closed by association. The reason is forced: the vouched version lives only in the hub's `hub_settings` table, and a hub-reading gate could not be `--fast`, so it would run in neither the hook nor CI. - **R-95** · **R-402** · **R-409** · **R-401** · **R-412 leg 2** — all untouched by this task. - **R-418** (docstring/table correspondence unenforced), **R-419** (`observations_gate` substring), **R-420** (no `blocking` field in the felhom.eu runner) — filed today, open. ## 10. No version, no image, no golden **No version was bumped. No image was built. No golden is owed by this work.** `golden_currency_gate.py` exits 0 and all thirteen felhom.eu gates are green. The controller CHANGELOG entry deliberately carries **no version heading**: a scripts change is not a release, and giving it one would have created the debt this task exists to make manageable. ## 11. Register **Before:** OPEN 171 · CLOSED 158. **After:** OPEN 172 · CLOSED 160. - **CLOSED R-404** — with the ruling and the reasoning for rejecting both framed options. - **CLOSED R-417** — cause removed, not worked around. - **R-242** — amended in place to state that its vouch half is untouched and still open. - **FILED R-418, R-419, R-420.** Both closed rows were written compressed at closure, which is this project's convention; no separate compression sweep was needed for two rows. ## 12. Observations, and my own mistakes by name 1. **The `golden-currency` gate was never the problem, and both offered options would have made things worse.** Narrowing it silences a true signal on exactly the nights it matters; a waiver would have recorded a lie, because the drill night wanted the golden and was forbidden from baking it. **FILED: R-404** — the ruling and this reasoning are in the closed row. 2. **My mistake — an empty grep read as a measurement.** My first stdin probe printed nothing and I was one step from reporting "the hook receives no stdin". The cause was mine: I pushed `main` in a repo whose branch was `master`, so the hook never ran. **NOT-A-FINDING: my own error, caught within one command by looking at the raw output instead of the filter, and it changed no conclusion. It is recorded because the failure mode — a filter that can return empty for a reason unrelated to the question — is the one this project keeps paying for.** 3. **My mistake — I planted a test observation whose own text satisfied the gate**, so Validation 4 passed when it should have failed and I briefly had a green that meant nothing. Chasing it found a genuine substring weakness. **FILED: R-419.** 4. **My mistake — I amended a commit that had already been pushed to the probe remote**, turning the next range into a force-push, so my second Validation 4 attempt ran at `scope=code` and its refusal was trivial. I noticed because `golden-currency` read `FAILED` where it should have read `ADVISORY`. Rewound and re-ran properly. **NOT-A-FINDING: the classifier behaved exactly as designed — a force-push is untrustworthy and must fail closed. The error was mine, in the test setup, and the correct behaviour is what exposed it.** 5. **My mistake — `lstrip("./")` ate the leading dot of `.claude/`**, silently classifying the whole rule-file tree as code. `lstrip` takes a set of characters, not a prefix. Caught by P1 on its first run. **NOT-A-FINDING: a bug I wrote and my own test caught before it left the working tree; it is listed so the next reader sees why the code now loops on `"./"` instead.** 6. **`repo_gates.py`'s docstring listed eleven gates while thirteen ran** — for eight days, in the sibling repo whose rule file already warns about exactly this drift. **FILED: R-418.** 7. **`controller_gates.py` had no way to express a reporting-only gate**, and nothing recorded that. **FILED: R-420.**