# REPORT — installer-channel record correction + R-29 filing (2026-07-29) Two commits, documentation only. No code, no version bump, no CHANGELOG entry, no build, no deploy, no box touched. Written as `REPORT-.md` per `CLAUDE.md:82-87` so root `REPORT.md` (the E-2 increment-1 report) is preserved. | # | Commit | Baseline | Scope | |---|--------|----------|-------| | 1 | `d4c07873ca0c3d3e547373a9fafc0e472a6535e8` | `36d635a4cdc1`, unmoved | Retract a false R-94/E-2d finding; open R-110 | | 2 | (this commit) | `d4c07873`, unmoved | File R-29 to the register; three record defects; this report | --- ## Commit 1 — `d4c07873` ### What was false `36d635a4` recorded that `felhom-bootstrap.sh` fetches the installer **from the hub**, that the hub therefore serves 1.19.0, and that a fresh ISO install runs the pre-E-2 installer. All three wrong. The claim had propagated into two `OPEN-ITEMS.md` rows, the ranking rationale, and `ROADMAP.md:149`. ### Confirmation table — all PASS | # | Claim | Read at | Result | |---|-------|---------|--------| | F1 | bootstrap fetches from the **website** | `scripts/iso/felhom-bootstrap.sh:96` | PASS — `INSTALL_URL="${FELHOM_INSTALL_URL:-https://felhom.eu/scripts/felhom-host-install.sh}"` | | F2 | hub-rendered command points at the same URL | `customer_unified.html:563`, `:1262` | PASS — and **three** emission sites, not two: `:563` static, `:1262` JS error branch, **`:1267` JS success branch** | | F3 | website serves `/scripts/` from a git-sync tree tracking `main` | `manifests/webpage.yaml` — nginx `:74-77`, sparse-checkout CM `:211-218`, git-sync `:272-281`, init `:299-307` | PASS — `--branch=main --period=30s --link=current`; sparse-checkout `/website/` + `/scripts/`; `location /scripts/ { root …/current; }`. No image build, no ArgoCD step | | F4 | `hostInstallVersion` selects nothing | `configs.go:28`, `:487`; `render_test.go:219`; `customer_unified.html:494` | PASS — repo-wide grep returns exactly those 4 code sites; all other hits prose. Rendered as a text label | | F5 | every generated flag is parsed by 1.22.0 | generator `customer_unified.html:1206-1239` vs parser `felhom-host-install.sh:1175-1212` | PASS — `--mode --cores --memory --vmid --node --acl-storages --operator-pubkey-file --preserve-state-from --skip-provision --dry-run --preflight-only --allow-new-leaf` (+ `--customer-id`); every one a parser case. **No functional gap** | | F6 | installer is 1.22.0 | `scripts/felhom-host-install.sh:187` | PASS | | F7 | the drift test is hollow | `render_test.go:219-221` | PASS — `strings.Contains(html, hostInstallVersion)` compares the constant to itself; passes at any value | ### Live command 1 — what the URL actually serves ``` $ curl -fsS https://felhom.eu/scripts/felhom-host-install.sh | grep -m1 '^SCRIPT_VERSION=' SCRIPT_VERSION="1.22.0" # the SINGLE version source (F-1): -h, the run banners, and the hub ``` ### Live command 2 — the drift gate's real state ``` $ python3 scripts/hostinstall_gates.py; echo "exit=$?" ok: SCRIPT_VERSION=1.22.0 ok: header has no version literal FAIL: hub Setup-tab hostInstallVersion=1.19.0 != SCRIPT_VERSION=1.22.0 (F-1: bump both together) ok: age is in the installed package set … (six further ok lines) … hostinstall gates: 1 FAILURE(S) exit=1 ``` ### Phase 0 source read — PAIRING reaches the same installer invocation Mode selection `felhom-bootstrap.sh:537-541`: a fresh VM with no baked customer-id calls `run_pairing`. On HTTP 200 from `/api/v1/appliance/poll` the loop writes the hub-delivered `FELHOM_CUSTOMER_ID` + `FELHOM_RETRIEVAL_PASSPHRASE` into the 0600 env, re-sources it, and calls `run_direct` **in the same invocation** (`:495-499`). `run_direct` is the single site that fetches `$INSTALL_URL` (`:322-330`), builds the args (`:334`) and invokes `bash "$SCRIPT_TMP" "${args[@]}"` (`:343`). The customer it yields is the one the operator bound — claimable. **So the ISO leg is the spine for E-2d**, not an obstacle to it. ### Rows changed | ID | Before | After | |---|---|---| | R-94 (line 14) | `READY — deferred until E-2d`, blocked on E-2d, false ISO/hub framing | `READY (XS)`, blocked on nothing, retracted + re-scoped to three legs | | R-94 (line 16) | duplicate row, `READY #2`, 1.19.0 vs 1.20.0 | **deleted** — merged | | R-110 | did not exist | opened, `WAITING-ON-OPERATOR (S)` | | E-2d | ISO implied as obstacle | Next-action appended; ISO is the spine | | ranked list | 1 R-95 · 2 R-94 (high-consequence) · 3 R-86 · 4 R-87 | 1 R-95 · 2 R-94 **de-ranked** · 3 R-86 · 4 R-87 · 5 R-110 | Files: `documentation/backlog/OPEN-ITEMS.md`, `documentation/backlog/ROADMAP.md`, `documentation/runbooks/day0-install.md`. --- ## Commit 2 — R-29 filing + record hygiene ### The §1.1 ruling — is R-29 the right home for a non-design-v2 gate? **Yes. Proceeded.** R-29's title says *"the design-v2 green gates"*, and `scripts/hostinstall_gates.py` is not one — it comes from drill F-1 (2026-07-12) and postdates the item. Four things decide it anyway: 1. **R-29's own audit list already spans well beyond design-v2 subject matter.** It names `docker_run_volume_path_gate` (docker mount safety), `offbox_rename_gate`, `app_row_dedup_gate` and `manifest_bearer_gate` (secrets — `runbooks/secrets.md:76`). The title is a misnomer relative to the item's own body. 2. **Part (b) — "the systemic half is the real item" in R-29's words — is stated with no design-v2 restriction.** It is about the *enforcement mechanism*: "the gates run only when a human remembers to run them… decide where they run (pre-push hook, `build.sh` step, or a CI job) and make a red gate block the train." That is gate-agnostic and repo-wide. 3. **`hub_confirm_gate.py` is already on R-29's list and lives in the same `scripts/` directory** as `hostinstall_gates.py`. Wiring one and not the other would be arbitrary. 4. **Identical failure shape, identical genre.** Both self-describe as mechanical grep-assertions (`hostinstall_gates.py:2`, `hub_confirm_gate.py:1-8`); both exist, assert something true, and are invoked by nothing. R-29 has already absorbed one independent re-raise without minting an ID (2026-07-18 rehearsal note) and says so explicitly. This is the third. No new ID minted. ### Orphan-search evidence — `hostinstall_gates.py` and `hub_confirm_gate.py` Re-established at `d4c07873`. Pattern `hostinstall_gates\|hub_confirm_gate`. | # | Scope | Command | Result | |---|-------|---------|--------| | S1 | `felhom.eu`, all file types | `grep -rn "$PAT" . --exclude-dir=.git` | **19 hits, zero invocations.** All are docstrings (`scripts/hostinstall_gates.py:5`, `scripts/hub_confirm_gate.py:7`), code comments (`hub/internal/web/configs.go:27`, `scripts/felhom-host-install.sh:189`) or prose (`REUSE.md:62`, `CONTEXT.md:540,564`, `hub/CHANGELOG.md:371,1292,1351,1385`, `scripts/CHANGELOG.md:483,524`, 3 files under `documentation/audits/`, `ROADMAP.md:149,158`, `OPEN-ITEMS.md:14`) | | S2 | sibling repos | `grep -rln "$PAT" /mnt/5_hdd/felhom.eu/git --exclude-dir=.git` minus this repo | 3 files, all in `.claude-memory/` (`MEMORY.md`, `drtier-by-default-2026-07-12.md`, `polish-batch-2026-07-13.md`). Notes, not invokers | | S3 | `~/.claude` | `grep -rln "$PAT" /home/kisfenyo/.claude`, and targeted on `settings*.json`, `skills/`, `hooks/` | **Zero hits in settings, skills or hooks** — where an invoker would live. Remaining hits are `file-history/` (Claude Code's own backups of files edited in past sessions) and `paste-cache/` (pasted task specs). Neither is an invocation site | | S4 | git hooks | `ls -1 .git/hooks/ \| grep -v '\.sample$'` | **Empty — every hook is a `.sample` stub** | | S5 | build files | `find . -type f \( -iname Makefile -o -iname '*.mk' -o -iname justfile -o -iname 'Taskfile*' \)` | Only `hub/Makefile`; `grep -n gate hub/Makefile` → **zero occurrences** | | S6 | CI | `find . -type d \( -name .github -o -name .gitea -o -name .woodpecker* -o -name .drone* -o -name .circleci \)` | **Empty — `felhom.eu` has no CI configuration at all** | Of the four gates in `scripts/`, only `site_gates.py` is mandated (`CLAUDE.md:153`); `manifest_bearer_gate.py` is named in `runbooks/secrets.md:76`. ### Rows changed | ID | Before | After | |---|---|---| | **R-29** | **absent from `OPEN-ITEMS.md`** (`grep -c` → 0) while present at `ROADMAP.md:158` since before the 2026-07-27 rebuild | **opened, `READY (S for (a) / M for (b))`**, owner CC | | R-94 | leg (b) stood alone | leg (b) carries `→ R-29` as its class. Row otherwise untouched | | E-2d | cited `:322-341` for an invocation at `:343` | cited `:322-343`, with the fetch / args / call each pinned separately | | R-95, R-86, R-87 | `READY #1`, `READY #3`, `READY #4` | `READY` — markers dropped (see below) | Not added to the ranked list under *"Why the READY rows rank this way"*: that list is a top-N rationale, not a complete ordering (R-99, R-102, R-103, R-104, R-108, R-109, R-89, R-92, R-93 and E-2d are all READY and absent from it). Ranking R-29 was not asked for and would be a priority claim this task has no basis to make. ### §2.2 — markers dropped, not renumbered The `#1/#3/#4` markers duplicated ranked-list positions 1/3/4 for exactly those three rows; two orderings of one set is what produced the hole when the `#2` row was merged in `d4c07873`. Removing the duplicate leaves the ranked list as the single maintained ordering. ### The other two record defects - `ROADMAP.md:147` — cited a non-existent **R-164**; it means controller **v0.164.0**'s deliberate-stop filter. Corrected. (It had already cost one max-ID scan a false positive.) - `CONTEXT.md:564` — asserted in the present tense that the single VERSION source is *"gated by `scripts/hostinstall_gates.py`"*. The gate exists, asserts exactly that, is red, and runs nowhere. Corrected to say the cross-check exists but is not enforced, tracked as R-94 leg (b) / R-29. `hub/internal/web/configs.go:27` carries the same false claim in a code comment and was left alone — R-94 leg (b) territory, needs a hub build. Files: `documentation/backlog/OPEN-ITEMS.md`, `documentation/backlog/ROADMAP.md`, `documentation/backlog/README.md`, `CONTEXT.md`, this file. --- ## Commit 3 — `de5a3e5` — the record-hygiene rider Baseline `7383400a`, unmoved. Four XS items from `7383400a` §9; the three deferred observations stay deferred. | File | Change | |---|---| | `CONTEXT.md:540` | *"`scripts/hub_confirm_gate.py` enforces"* → asserts, but is not enforced (R-29). Third instance of the class after `:564` and `configs.go:27` | | `REUSE.md:62` | Same claim, *"enforces zero"*. The **rule stays** — never native `confirm()`/`prompt()` is correct guidance and this is a reuse-reference row — only the enforcement claim changes | | `OPEN-ITEMS.md:4` | Root `REPORT.md` = overwritten per-session; `REPORT-.md` = non-clobbering sibling (`CLAUDE.md:82-87`), 14 of them. Prohibition unchanged | | `OPEN-ITEMS.md:55` | Heading scoped to *"the **TOP** READY rows"* + a half-sentence that it is deliberately not a full ordering. **No row added to the list** | `hub/internal/web/configs.go:27` — the fourth instance — left alone (R-94 leg (b), needs a hub build). ### Part 3 NOT done — its stated evidence is false The spec proposed recording a convention: *"`ROADMAP`'s P3 `Status` column only ever takes `idea` or a `SHIPPED`/`PROVEN-LIVE` variant, and never takes `READY`, `BLOCKED` or `WAITING-ON-OPERATOR`"*, and concluded from that vocabulary split that `ROADMAP` `Status` is **disposition** while `OPEN-ITEMS` `State` is **live actionability**. Refuted inside P3 itself (`ROADMAP.md`, lines 113–172): | Line | ID | P3 `Status` cell | |---|---|---| | 151 | R-90 | `BLOCKED on Hetzner CX33 availability (operator, 2026-07-27)` | | 152 | R-91 | `WATCHING — gated on demo-felhom's first post-migration PBS backup` | | 150 | R-110 | `idea — found 2026-07-29, **WAITING-ON-OPERATOR (a ruling, not a defect)**` | | 137 | R-86 | `**NEXT — operator ruling 2026-07-27**` | And P2's recovery-gaps sub-table (lines 99–107) uses a bare `READY` for nine rows, including `E-2`. Both halves fail: the column **does** take the `OPEN-ITEMS` vocabulary, and `BLOCKED`/`WATCHING`/ `NEXT` are live-actionability words, not dispositions. The real shape is a free-text status field that has absorbed both vocabularies over time — usually disposition, sometimes not. Writing the proposed convention down would install a **new false invariant** in the commit chain whose whole purpose was removing them, so per the spec's own instruction (*"If you think this ruling is wrong, say so and make no edit"*) `documentation/backlog/README.md` was not touched. The narrow question that prompted it still resolves benignly: R-29 and R-94 reading `idea` in `ROADMAP` and `READY` in `OPEN-ITEMS` is **not** a defect and needs no sync. What is unsupported is generalising that into a rule. --- ## Standing note **No gate was wired, fixed, run as a hook, or deleted by any of the three commits.** Filing R-29 is not doing R-29; the enforcement decision (pre-push hook / `build.sh` step / CI) and the audit of the remaining eight gates are R-29 part (b), M-sized, and its own task.