No version heading on purpose. No Go code, no image, no version bump; giving this one would create the exact golden debt the change is about. Until today this repo - where a release actually happens - had NO golden-currency check at all, while felhom.eu ran one on every push including documents-only ones that can neither create the debt nor clear it. The person who could act heard nothing; the person who could not act was blocked, thirteen --no-verify uses' worth. golden_notice.py is ADVISORY IN EVERY CASE, and that is the only correct behaviour rather than timidity: at the moment a release is committed the golden legitimately does not exist yet, so blocking there would refuse the commit that STARTS the process - and blocking later is the mistake being undone. NO SECOND IMPLEMENTATION: it IMPORTS felhom.eu/scripts/golden_currency_gate.py and calls that gate's own released_versions()/newest_baked(), so it is the same comparison read in the other direction. Cross-repo shape copied from instructions_gate.py; never a copy of the script, because a copy recreates the drift these gates exist to detect. An absent sibling clone is INCONCLUSIVE and silent about currency - it never guesses. controller_gates.py GAINED A FIFTH `blocking` FIELD. It could not express a reporting-only gate at all before: every registered gate's non-zero exit failed the run, so the only way to add a notice was to give it the power to refuse a push. The capability was added rather than the notice compromised (R-420). False for exactly one gate, and test_golden_notice.py asserts it stays one. Tests N1-N4 with a positive control that every other gate is still blocking. RED-PROOF RUN: making the debt branch return 1 fails N1 - in production that would refuse the commit that starts a release.
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.
NOT VALIDATED: CI's actual behaviour, until a real push lands. I have not confirmed that Gitea's
act-runner populates GITHUB_EVENT_PATH with a commits array carrying per-file lists. If it does
not, CI silently stays exactly as strict as it is now and the advisory never appears there. The
first push after this one is the observation, and the workflow prints the paths it found and the
scope it chose so the answer is readable in the log.
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.