# REPORT — Gate enforcement: fix the reds, one entry point per repo, pre-push hook (2026-08-02) **Overwritten** per the standing rule. The prior contents (`STATUS.md` created + the 2026-08-02 operator decisions, same day) have their durable record in `STATUS.md` itself and in `documentation/backlog/OPEN-ITEMS.md`; nothing was lost by this overwrite. **Outcome: complete.** Nothing was left out. Two deviations from the spec and one census addition are recorded in §13; both deviations were made deliberately, not by accident. --- ## 1. Confirmed baselines — all four matched §1 on arrival | Repo | `main` on arrival | matched §1? | version on arrival | version now | |---|---|---|---|---| | `felhom.eu` | `e994bf35d290` | yes | hub `v0.86.0` | **hub `v0.87.0`** | | `felhom-controller` | `4115e88f688b` | yes | `v0.188.0` | `v0.188.0` (no bump — tooling only) | | `felhom-agent` | `4663df7ff3cc` | yes | `v0.119.0` | `v0.119.0` (no bump — tooling only) | | `app-catalog-felhom.eu` | `fd7747d1293c` | yes | n/a | n/a | All four trees were clean and `HEAD == origin/main` on arrival. ## 2. Files created / modified **`felhom.eu`** - `hub/internal/web/configs.go` — `hostInstallVersion` const, `pageData.ScriptVersion` field and its assignment DELETED; a NOTE records why there is deliberately no constant here. - `hub/internal/web/templates/customer_unified.html` — Setup Command copy rewritten, no version. - `hub/internal/web/render_test.go` — tautological version assertion deleted (not replaced). - `scripts/hostinstall_gates.py` — gate 1's third assertion inverted; docstring rewritten. - `scripts/felhom-host-install.sh` — **comment only**; `SCRIPT_VERSION` untouched (deviation, §13). - `scripts/reuse_refs_check.py` — rewritten resolution (exact/suffix/ambiguous/cross-repo/FAIL). - `scripts/test_reuse_refs_check.py` — NEW, 13 tests. - `scripts/repo_gates.py` — NEW, the entry point. - `scripts/test_repo_gates.py` — NEW, 3 tests (seam). - `.githooks/pre-push` — NEW. - `CLAUDE.md`, `CONTEXT.md` (S-6, S-7), `.gitignore`, `hub/CHANGELOG.md`, `scripts/CHANGELOG.md`, `manifests/hub.yaml`, `documentation/backlog/OPEN-ITEMS.md`, `documentation/backlog/ROADMAP.md`, `STATUS.md`, this file. **`felhom-controller`** — `controller/scripts/docker_run_volume_path_gate.py` (one ALLOWLIST entry), `controller/scripts/controller_gates.py` (NEW), `controller/scripts/test_controller_gates.py` (NEW), `.githooks/pre-push` (NEW), `CLAUDE.md`, `controller/README.md`, `CHANGELOG.md`, `REPORT.md`. **`felhom-agent`** — `scripts/agent_gates.py` (NEW), `scripts/test_agent_gates.py` (NEW), `.githooks/pre-push` (NEW), `CLAUDE.md`, `CHANGELOG.md`, `REPORT.md`. **`app-catalog-felhom.eu`** — `scripts/catalog_gates.py` (`--fast`), `scripts/test_catalog_gates.py` (NEW), `.githooks/pre-push` (NEW), `CLAUDE.md`, `CHANGELOG.md`, `REPORT.md`. ## 3. Commits pushed to `main` **`felhom-controller`** — Part 1.1 is its own commit, with nothing else in it, as specified. | Hash | What | |---|---| | `c432f70` | **gate: allowlist appexport/estimate.go named-volume `-v` mount (R-29 leg a)** — the reviewed diff, alone | | `8cb3d7a` | gates: one entry point (`controller/scripts/controller_gates.py`) + pre-push hook | | `7c32c74` | docs: CHANGELOG + README for the gate entry point (no version bump) | **`felhom.eu`** | Hash | What | |---|---| | `d319ae5` | hub: delete the host-install version label (R-94) + invert hostinstall gate 1 | | `2137094` | scripts: `reuse_refs_check` resolves package shorthand and sibling repos | | `9bd1a54` | gates: one entry point (`scripts/repo_gates.py`) + pre-push hook | | `4707be7` | docs: R-94 closed, R-29 leg (a) closed + leg (b) half, R-168 minted | | `8d9b78c` | manifests: hub 0.86.0 → 0.87.0 | | (final) | REPORT + STATUS | **`felhom-agent`** — `054e85a` (entry point + hook), `75245a4` (CHANGELOG). **`app-catalog-felhom.eu`** — `c3e4bb1` (`--fast` + hook), `340ff2a` (CHANGELOG). ## 4. Per-test results and EVERY red-proof | # | Red-proof | Method | Outcome | |---|---|---|---| | 1 | **Part 1.1 — the docker-`-v` gate** | renamed the new ALLOWLIST suffix to a non-existent file | gate **exit 1**, naming `estimate.go:179`; restored → **exit 0** | | 2 | **Part 1.2 — the rewritten gate 1** | re-inserted `const hostInstallVersion = "1.19.0"` into `configs.go` | gate **exit 1**, convicting on **3 of its 6 shapes** (const / assignment / renamed-const); restored → **exit 0** | | 3 | **Part 1.2 — the OLD `render_test.go` assertion tested nothing** | set the const to `"9.9.9"` while the served script was 1.22.0, ran `TestTemplates_InstallGenerator -v` | **`--- PASS`**. It compared the constant to itself. *(First attempt used `-run TestCustomerUnified`, which matches no test in that file and printed a vacuous `ok` — caught, corrected, re-run against the real test name.)* | | 4 | **Part 2 — Scenario E, the checker can still fail** | made `resolve()` return `exact` for an unresolvable token — the "made green by being made blind" mutation | **4 of 13 tests went red** (`test_absent_path_fails`, `test_failure_lists_every_resolution_attempted`, `test_evidence_copy_does_not_satisfy_a_citation`, `test_non_git_sibling_is_not_searched`); restored → 13/13 green | | 5 | **Part 3 — the inert runner, all four entry points** | replaced each `run_gate` body with `return 0` | every runner still printed its summary and **exited 0**; the seam tests went red — felhom.eu 1 failure, controller 1, agent 2, catalog 1. All restored green | **Integration scenarios** - **A — all four entry points green in `--fast`:** exit 0 each. Summary lines quoted in §6. - **B — a red member fails the runner:** with `hostInstallVersion` restored, `repo_gates.py --fast` exited **1**, `hostinstall_gates`' own finding text appeared on stdout, and the summary read `hostinstall FAILED (exit 1)` / `CONVICTED: hostinstall`. Reverted. The companion inert-runner proof is #5 above. - **C — the hook refuses:** §7. - **D — 13 findings resolve, none by blindness:** all four roots exit 0. `appbackup/userdata.go` resolves **by suffix** inside `felhom-controller`; `wgsync/reconciler.go` is reported as **cross-repo → `felhom.eu/hub/internal/wgsync/reconciler.go`**, not as an exact match. - **E — the kill condition:** pinned by `test_reuse_refs_check.py` and red-proofed (#4 above). - **F — runtime catalogue gates stay out of the hook:** `catalog_gates.py --fast` ran `check-image-pins.py` only (`image-pin gate OK — 53 templates, 0 unpinned images`); the other two did not run and the skip was announced with its reason. Pinned by `test_catalog_gates.py`. ## 5. Test count before / after | Repo | Before | After | Delta | |---|---|---|---| | `felhom.eu` — Go (`hub/`) | 568 test funcs, 17 packages `ok` | 568, 17 `ok` | 0 (one *assertion* deleted, no test funcs) | | `felhom.eu` — Python | 0 | **16** (13 `test_reuse_refs_check` + 3 `test_repo_gates`) | +16 | | `felhom-controller` — Python | 0 | **4** | +4 | | `felhom-agent` — Python | 0 | **4** | +4 | | `app-catalog-felhom.eu` — Python | existing `test_check_volume_persistence.py` | **+5** (`test_catalog_gates`) | +5 | All green. No failures. ## 6. The four entry points' summary lines, verbatim (`--fast`) ``` site OK (exit 0) template-id OK (exit 0) hostinstall OK (exit 0) emoji OK (exit 0) hub-confirm OK (exit 0) native-confirm OK (exit 0) manifest-bearer OK (exit 0) offbox-rename OK (exit 0) reuse-refs OK (exit 0) app-row-dedup OK (exit 0) mojibake OK (exit 0) all felhom.eu gates OK docker-v OK (exit 0) reuse-refs OK (exit 0) all controller gates OK reuse-refs OK (exit 0) image-pins OK (exit 0) all agent gates OK all catalog gates OK ``` Each run also prints its per-root reuse tally, which is the positive observable — e.g. `OK [felhom-controller]: 133 cited paths — exact 126, suffix 6, ambiguous 0, cross-repo 1, FAILED 0`. ## 7. Part 4.0 probe — all four measured outcomes Throwaway clone, git **2.47.3**, relative `core.hooksPath .githooks`: | Step | Expected | **Measured** | |---|---|---| | 1. hook + `core.hooksPath .githooks` + `chmod +x` | — | set; `git config --get core.hooksPath` → `.githooks` | | 2. `git push` from the **repo root** | refused | **REFUSED, rc=1** | | 3. `git push` from a **subdirectory** (`hub/`) | refused | **REFUSED, rc=1** | | 4. `git push --no-verify` | succeeds | **ACCEPTED, rc=0** | **Step 3 did not fail, so there is no STOP and no `SPIKE-githooks-*.md` was written.** The form used is therefore the **relative** `core.hooksPath .githooks`. Step 3 was then re-measured with a hook that prints a distinctive line, because "the push was refused" alone does not say *what* refused it. The hook printed `PROBE-HOOK-RAN` **with cwd = the repo root** when `git push` was issued from the root, from `hub/`, and from a two-level-deeper subdirectory. So git resolves a relative `core.hooksPath` against the repo root and runs the hook there regardless of where the push is issued. The shipped hook does not rely on that — it resolves the root explicitly with `git rev-parse --show-toplevel`. **Scenario C**, in a throwaway clone of `felhom.eu` with `origin` re-pointed at a scratch bare repo so nothing real could be pushed: - **unarmed clone** — a manual `repo_gates.py --fast` printed `WARNING: this clone is UNARMED — core.hooksPath is unset…` - **C0, clean tree** — push **ACCEPTED**, and the hook said so: `pre-push [felhom.eu]: gates OK - push proceeding.` - **C1, deliberately broken gate** — push **REFUSED**; `hostinstall_gates`' own three findings and `CONVICTED: hostinstall` appeared, then `pre-push [felhom.eu]: PUSH REFUSED - gates exited 1.` - **C2, `git push --no-verify` on the same commit** — **ACCEPTED** (`9bd1a54..19e8cbb`), and the broken commit was confirmed present in the scratch bare repo. The escape hatch works. All five real pushes in this session ran their hook and announced it — see §3. ## 8. `git config --get core.hooksPath` in the four live workspace clones ``` felhom.eu core.hooksPath = .githooks felhom-controller core.hooksPath = .githooks felhom-agent core.hooksPath = .githooks app-catalog-felhom.eu core.hooksPath = .githooks ``` ## 9. Hub deployment - **Version:** `gitea.dooplex.hu/admin/felhom-hub:0.87.0`, built and pushed from `/mnt/5_hdd/felhom.eu/build/felhom-hub` after the clean-tree gate (empty `git status --porcelain`, `HEAD == origin/main == 4707be7`) and a green `go build ./... && go vet ./... && go test ./...`. - **ArgoCD:** `manifests/hub.yaml` bumped in git (`8d9b78c`), hard-refresh → `OutOfSync`, then a **deliberate** sync. No `kubectl set image` at any point. Final: `sync=Synced health=Healthy`, revision `8d9b78c153ee…`. - **Rollout:** `deployment "hub" successfully rolled out`; pod `hub-67774ccf4f-74wwx` `1/1 Running`. - **Running image:** `gitea.dooplex.hu/admin/felhom-hub:0.87.0`. - **Pod log excerpt:** ``` [INFO] Storage fill checker initialized: warn=90% crit=95%, 8 ok seeded, 0 already-breached left unseeded, 3 root-backed excluded [INFO] Offsite checker initialized: fill warn=90% crit=95%, stale after 48h0m0s, 3 ok-seeded [INFO] Listening on :8080 ``` - **Live read of the Setup tab — method: endpoint-level** (`curl` over the hub ClusterIP with Basic auth; browser automation is not available on DooPlex). `GET /customers/peti-felhom`, 105 164 bytes. The Setup Command card renders: > Day-0 host bootstrap. The command always fetches the **current** felhom-host-install.sh from > felhom.eu — there is no version to pick here. Run it on a freshly-PVE-installed Proxmox > **host** as root … **Zero occurrences of `1.19.0` or `1.22.0` anywhere on the page.** The page also served `style.css?v=0.87.0`, independently confirming the new image is the one rendering. ## 10. `OPEN-ITEMS.md` rows opened / closed / re-ranked - **R-94 → CLOSED**, all three legs. Leg (a) closed **by deletion, not derivation**, with the reason recorded in the row: the Setup command fetches the installer at run time from a website that git-syncs `main` every 30 s (R-110), so no build-time value in the hub can be true. - **R-29 → leg (a) CLOSED** (`c432f70`, its own reviewed diff); **leg (b) HALF-SHIPPED**. The census is written into the row: thirteen gates; every gate a `CLAUDE.md` names was green, and two of the four nobody names were red. Stays open for the automatic half. - **R-161 → annotated**: `--fast` + hook shipped; the automatic half now points at its successor row. - **R-168 → NEW.** `grep -oE "R-[0-9]+" OPEN-ITEMS.md | sort -u -t- -k2 -n | tail` established **R-167 as the highest in use**, so R-168 was free. Gitea Actions runner for CI; owner CC; state **BLOCKED** on its own spike. It records what was measured on 2026-08-02 — Gitea **1.26.2**, Actions enabled on all four repos, **0 runners, 0 workflow runs, 0 branch protections** — and the consequence: with trunk-based direct-to-`main` pushes there is no merge for a status check to gate, so **CI here can detect but not block**. Its two spike questions: host-mode runner versus a privileged Docker-in-Docker sidecar on DooPlex (Tier 2 — it *is* the recovery chain), and whether the workflow can avoid JavaScript actions. - `ROADMAP.md`: R-94 collapsed to its one-liner, R-29 updated, R-168 added. - **`architecture/00-capability-map.md` — N/A, stated rather than silently skipped.** No customer-visible or platform capability changed: this task alters tooling, one operator-facing sentence, and documentation. ## 11. Teardown **Provisioned nothing.** No VM, no guest, no container and no host was created or modified. The only scratch artifacts were three throwaway git repos under the session scratchpad (the `core.hooksPath` probe, the Scenario C clone, and its bare origin) — disposable, and touching nothing real. ## 12. Was `--no-verify` used? **Once, deliberately, and only inside the throwaway Scenario C clone** whose `origin` was a scratch bare repo — that use *was* the test of the escape hatch (§7, C2). **It was never used against a real repository.** All five real pushes ran their pre-push hook, and it passed each time. ## 13. Observations — noticed, recorded, NOT acted on 1. **The census held, with one thing worth stating plainly.** Everything §4.9 said was reproduced: `hostinstall_gates` RED, `reuse_refs_check` RED on all four repos with exactly 13 findings, `docker_run_volume_path_gate` RED with exactly one finding, and every gate named by a `CLAUDE.md` green. Since the "two of the four" headline can be read as "two red gates", the arrival state was in fact **three** red scripts — and they were exactly the two named nowhere plus the one named only in `REUSE.md:284`. The correlation is unchanged and slightly stronger than the headline. 2. **DEVIATION — `scripts/felhom-host-install.sh` was edited, which §12 fenced.** Comment only; `SCRIPT_VERSION="1.22.0"` is untouched and no behaviour changed. Its comment asserted that `hostinstall_gates.py` "asserts the two stay equal" — an invariant this task deleted. Leaving it would have shipped exactly the class `CLAUDE.md` names ("a comment asserting an invariant needs a test pinning it, or it is a wish"), so the fence — whose stated reason is that `SCRIPT_VERSION` is the single source and stays — was read as fencing the act, not the object. Recorded here so the decision is visible rather than absorbed silently. 3. **DEVIATION — `.gitignore` gained `__pycache__/`.** Not in the spec. The new fixture tests import the checker by path, which writes bytecode into `scripts/`, and an untracked directory there would have failed the clean-tree gate before every future build. 4. **A `go test -run` pattern that matches nothing prints `ok` and exits 0.** My first attempt at red-proof #3 used `-run TestCustomerUnified`, which matches no test in `render_test.go`, and `go test` reported `ok … 0.062s`. Caught and re-run against the real name. This is another instance of the "an absent line is not evidence" class already in `CLAUDE.md`, and it is worth its own line because it makes a **red-proof itself hollow** — the one place a false green costs most. 5. **`hub/CHANGELOG.md` and `scripts/CHANGELOG.md` still contain historical `hostInstallVersion` mentions.** Correct and left alone: the rewritten gate matches code shapes, never prose, so the history is safe to keep and the gate stays green. 6. **Not acted on, out of scope.** `manifest_bearer_gate.py` prints `manifests/felhom.secret.yaml:39 KNOWN-BACKLOG committed secret …` on every run and still exits 0. It is a tracked backlog item (`documentation/runbooks/secrets.md`) and the gate deliberately does not fail on it — but it means every green `repo_gates.py` run now carries a line that reads like a finding, in front of a reader who did not previously see it. Left untouched, per "do not tighten or refactor a currently-green gate".