3db62fc6b2
gates / gates (push) Successful in 12s
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.
284 lines
15 KiB
Markdown
284 lines
15 KiB
Markdown
# 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.**
|