Files
felhom-controller/REPORT.md
T
admin a4444088ad
gates / gates (push) Successful in 12s
R-404: the golden NOTICE, in the repo where the debt is created - NOT A RELEASE
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.
2026-09-01 12:01:26 +02:00

15 KiB
Raw Blame History

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

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