Gitea's act-runner does populate GITHUB_EVENT_PATH with a commits array carrying per-file lists. Job 481 read 3 commits / 12 distinct paths, classified CODE (5 document, 7 code), and ran the gates with --scope=code. Green. Still not observed: the docs branch in CI, and an advisory in a CI log - the second additionally needs a golden debt to exist at that moment. Neither is being arranged artificially.
15 KiB
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: <local ref> <local sha> <remote ref> <remote sha>, 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
siterow's fifth field flipped toTrue. 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 withGot 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:
- My first planted observation contained the sentence "it carries no
FILED:and noNOT-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 inobservations_gate.py, now R-419. - 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-forwarddocsrange — 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_settingstable, 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_gatesubstring), R-420 (noblockingfield 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
- The
golden-currencygate 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. - 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
mainin a repo whose branch wasmaster, 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. - 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.
- 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=codeand its refusal was trivial. I noticed becausegolden-currencyreadFAILEDwhere it should have readADVISORY. 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. - My mistake —
lstrip("./")ate the leading dot of.claude/, silently classifying the whole rule-file tree as code.lstriptakes 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. 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.controller_gates.pyhad no way to express a reporting-only gate, and nothing recorded that. FILED: R-420.