Files
felhom-controller/REPORT.md
T
admin 3db62fc6b2
gates / gates (push) Successful in 12s
REPORT: the CI step is MEASURED, not assumed - job 481 shows the payload works
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.
2026-09-01 12:03:28 +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.

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.