REPORT + STATUS: gate enforcement session, hub v0.87.0 live

REPORT overwritten per the standing rule; every red-proof, the core.hooksPath probe's four
measured outcomes, Scenario C's refusal-and-bypass, the hub deployment and the live Setup-tab
read are recorded there, plus three observations and two deliberate deviations from the spec
(a comment-only edit to felhom-host-install.sh, and __pycache__ in .gitignore).

STATUS: the 'check that needs a person to remember it' line is rewritten — the checks now run
themselves before every push, with both honest limits stated in plain words; and one entry
records the thirteen-check cleanup and the deleted installer version number.
This commit is contained in:
2026-08-02 15:37:19 +02:00
parent 8d9b78c153
commit af2d103880
2 changed files with 271 additions and 155 deletions
+257 -151
View File
@@ -1,168 +1,274 @@
# REPORT — `STATUS.md` created, and the 2026-08-02 operator decisions recorded (2026-08-02)
# 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 (hub v0.85.0 Network card + v0.86.0 Copy
without reveal, 2026-07-31) have their durable record in `hub/CHANGELOG.md` and
`documentation/audits/host-addresses-visible-2026-07-31.md`; nothing was lost by this overwrite.
**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.
**Class: documentation only.** No code, no template, no box, no build. Repo `felhom.eu` only —
`app-catalog-felhom.eu` was read for context and **not** modified. **No `CHANGELOG.md` entry exists
for this change and none is missing:** this repo has no root changelog, only per-area `hub/`,
`scripts/`, `website/` (`CLAUDE.md`), and this session touched none of those areas.
Baselines: `felhom.eu` @ `260a8f6`, `app-catalog-felhom.eu` @ `fd7747d`. Part 1 was derived by reading
the register's rows and both ranking sections, not from memory.
**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. `STATUS.md` — the file
## 1. Confirmed baselines — all four matched §1 on arrival
Root of `felhom.eu`, so it is the first thing visible. Sections in the specified order: what works ·
what's broken · what we're working on · waiting on you · changed since last update.
| 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 |
**Word count: 652 total, 581 excluding the header block** (`wc -w`; the header carries the
view-not-source, not-`CONTEXT.md` and maintenance rules, which the spec requires). **That is over the
~500 target and it is a deliberate miss, stated rather than hidden.** Five passes took it from 819 to
652. Getting under 500 needed either dropping a mandated item or dropping the off-site-credential line
— the register's **top-ranked** open item and the largest customer-data exposure on it. Cutting the
biggest data risk to save forty words is the wrong trade on a page whose job is to show the operator
what is at stake. It is 67 lines and fits a screen. **If the operator disagrees, the line to cut is
the R-95/R-87 one** and the page drops to ~545.
All four trees were clean and `HEAD == origin/main` on arrival.
Content, in the operator's ranking: an app can stay off after a power cut, silently (R-157) · the
off-site copy can be erased by the box that wrote it, and has never been restored from (R-95, R-87) ·
three of fifty-three apps saved data where backups never looked (R-156) · 20 GB of backup space
against 50 GB of apps (R-163) · when that trips, one page says so and nothing alerts (R-158) · the
checker exists but a person has to remember it (R-161).
## 2. Files created / modified
**Constraints honoured:** no `R-n` is the subject of any sentence — every identifier is a bracketed
pointer at the end of a line; no file paths, function names or version numbers appear; every broken
item is stated as what a customer or the operator would notice. Shipped, watching and
blocked-on-a-predicate rows (R-159, R-160, R-162, R-164) are absent by design.
**`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.
**One deviation, flagged per standing rule 4.** "Waiting on you" is specified as *decisions only*, and
it carries one non-decision: **the hub password needs rotating** (R-132, owner Viktor). It is the only
thing on the register waiting on the operator with a live credential consequence, and omitting it from
the operator's own page to honour a section rule would be the letter over the point. It is labelled
*"a job, not a decision"* so the section's shape is not quietly eroded.
**`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`.
## 2. The decisions — where each one went
**`felhom-agent`** — `scripts/agent_gates.py` (NEW), `scripts/test_agent_gates.py` (NEW),
`.githooks/pre-push` (NEW), `CLAUDE.md`, `CHANGELOG.md`, `REPORT.md`.
All four are in **`CONTEXT.md` as standing ruling S-5**, labelled **D-a … D-d** as in the discussion
and deliberately kept distinct from S-3's `D1…D6`. Open work is carried as backlog rows, per the
existing convention — no new home was created for either.
**`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`.
| Decision | Recorded | Work |
|---|---|---|
| **D-a** — merge the backup partition away (not resize) | `CONTEXT.md` S-5 | **R-165** (new) |
| **D-b** — desired/observed app state, own store | `CONTEXT.md` S-5 | **R-166** (new, `BLOCKED`) |
| **D-c** — storage monitoring + backup alerts | `CONTEXT.md` S-5 | **R-167** (new) |
| **D-d** — only DooPlex and Peti's box are protected | `CONTEXT.md` S-5 | `runbooks/target-selection.md`, this session — no row; the decision *is* the change |
| **Maintenance rule** (Part 3) | `STATUS.md` header **and** `CLAUDE.md` § End-of-session checklist | — |
## 3. Commits pushed to `main`
Recorded verbatim inside D-b, because it is the decision's binding constraint: *losing the state store
must never cause an app to be deleted, restarted wrongly, or reported healthy when it is not — the
worst acceptable outcome is re-running a backup that already ran.* Its two "establish before speccing"
items are carried on R-166 as the reason that row is `BLOCKED` rather than `READY`.
**`felhom-controller`** — Part 1.1 is its own commit, with nothing else in it, as specified.
**Deliverable 5 asks for "the five decisions".** Part 2 defines four (D-a … D-d); the fifth deliverable
line is the Part-3 maintenance rule, and it is in the table above. Nothing else in the task reads as a
fifth decision — flagged rather than invented.
**D-a's two conditions are recorded as conditions, not commentary:** it changes the disk layout so it
must land **before any external install**, and it removes a wall that currently fails safely so
**R-167 ships in the same step, never after**. R-165 restates both; R-167 names R-165 as the thing it
gates.
**None of D-a, D-b or D-c is implemented.** No controller, agent, installer or hub file was opened for
editing.
## 3. R-163 re-framed, and R-156's papra referral resolved
**R-163 is re-framed, not closed** — as instructed. State went `WAITING-ON-OPERATOR — the ratio is a
tier-sizing ruling``RE-FRAMED 2026-08-02 — open, no longer waiting on a ratio`; "Blocked on" went
from `the operator's sizing decision` to a pointer at R-165; owner `operator``CC`. The cell now says
the sizing **question is withdrawn rather than answered**, that the row survives as the record of the
constraint until the merge lands, and that the original finding follows unchanged. The intake ranking
(item 4) was updated with it, and records that **R-165 inherits R-163's rank and is the highest-ranked
item that must land before any external install**.
**R-156's papra referral is resolved.** The referral existed because moving a mount relocates live data
out from under a running app; with papra deployed nowhere there is nothing to strand, so the cheaper
leg — the template mounts `/app/app-data` — is takeable without waiting on upstream.
**The provenance is recorded with the claim, because it decides the row.** The evidence is
`docker ps -a` on **demo-hp's guest 9201** returning empty, **supplied with the task**; this session
**did not re-measure** — it is documentation-only and every box was fenced. The recorded scope is
honest about its edge: it covers the one guest papra was convicted on in Campaign 10, and **no other
customer's guest was enumerated**, so the row instructs the task that edits the template to re-check
first. Next action on the row is the catalog edit plus `catalog_gates.py`, explicitly not done here.
## 4. `target-selection.md` per D-d
The rule at the top is now D-d: **two protected machines, everything else disposable**, with the
correction stated as a correction — the earlier caution was costing sessions and pushing drills onto
DooPlex. The tier table's Tier 2 row is DooPlex + Peti's cluster "and, by D-d, nothing else".
**Two consequences the decision did not name, both handled visibly rather than silently:**
- **`ep0` + the Hetzner Storage Boxes.** D-d's protected list has two machines and ep0 is not one, so
the page no longer calls it Tier 2. It is **not** thereby scratch: it holds the PBS-DR datastore and
the restic copy of a real customer's data — the only off-premises copy that exists. Read the narrow
way (not protected, but not wipeable), using the page's own *fences-name-acts* rule, and **flagged
in the page for the operator to confirm explicitly.**
- **The shared "do not re-point either backup target" fence** on the two demo boxes was **downgraded
from a prohibition to a stated cost**, because D-d makes both boxes freely reinstallable, which
spends that reference configuration just as thoroughly — keeping the fence would have left the page
self-contradicting. The reason survives: know you are spending the regression reference, and put the
box back.
Also corrected while in the file: the *fences-name-acts* example cited the fence this edit removed, and
demo-hp's access line asserted "no baked SSH key" — which **R-129** records as measured false on
2026-07-31. It now points at R-129 instead of sending the next session to the hub vault for a
credential it may not need.
## 5. The maintenance rule
In two places, as specified: the `STATUS.md` header block, and a new **`## End-of-session checklist`**
in `CLAUDE.md` — which also gathers the couplings that were previously scattered (CHANGELOG + REPORT,
REUSE, the capability map's own end-of-session line, S-1's architecture coupling) and closes with *a
finding goes in `OPEN-ITEMS.md` first, never only in a report, an audit or `STATUS.md`*.
`CONTEXT.md` gained a header block stating why it and `STATUS.md` are separate — same subjects,
different readers, and `STATUS.md` holds nothing of its own. `STATUS.md` says the same from its side.
## 6. What could not be translated into plain language
Asked for explicitly, because an untranslatable row usually means the row itself is unclear.
- **R-29** — *"gates are enforced nowhere"*. The class is stateable ("we have checks nobody runs"), but
its instances are four differently-broken scripts across two repos with no shared consequence, so
every plain sentence either says nothing or misstates one instance. **R-161 is its readable
fragment**, which is why R-161 is on the page and R-29 is not.
- **R-123 / R-125** — process findings about how the register and how tests are written. Real, and they
belong on the register; there is no customer-visible symptom to lead with, so they have no honest
first sentence for this page. They are not "broken" in the operator's sense.
- **R-133 (the plaintext break-glass credential)** — translatable, and left off only for space. It is
the strongest candidate for the next update if something else closes.
- **R-115 vs R-110** — separate rows, one plain-language paragraph. Merged into a single "Waiting on
you" bullet carrying both pointers, because two adjacent bullets about publishing read as one item
the operator has already half-decided.
**A register defect found while reading, filed here because the fix is not mine to guess: `R-133` is
used TWICE** — `OPEN-ITEMS.md:80` (duplicate `domain` values accepted by the hub) and `:86` (the
plaintext break-glass credential). Two different findings, one ID, both `READY`. One needs renumbering,
and which one is the operator's call since both are cited from elsewhere (`CONTEXT.md` S-4 cites the
credential one).
## 7. Files changed
| File | Change |
| Hash | What |
|---|---|
| `STATUS.md` | **new** — the operator page |
| `CONTEXT.md` | S-5 (D-a … D-d); header note on the `STATUS.md` separation |
| `documentation/backlog/OPEN-ITEMS.md` | R-165/166/167 filed; R-163 re-framed (state, blocked-on, owner, ranking); R-156's referral resolved with its provenance |
| `documentation/runbooks/target-selection.md` | D-d rule; tier table; ep0; the demo-box fence; two stale lines |
| `CLAUDE.md` | new `## End-of-session checklist`, carrying the `STATUS.md` maintenance rule |
| `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) |
Nothing was built, deployed, published or touched on any host. Every claim about the register above is
a claim about pushed source in this repo at the commit below.
**`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".
+14 -4
View File
@@ -37,8 +37,11 @@ lost: the last good copy is kept intact. *(R-163)*
**When that happens, only one page says so** — no email, no alert. The page that answers "is this app
backed up?" is the one that stays silent. *(R-158)*
**The check that catches this needs a person to remember it.** One command, run by hand; nothing
refuses a change that skipped it. *(R-161)*
**The checks that catch this still can't stop a change on their own — but they now try.** Every
repository has one command that runs all of its checks, and it runs by itself before every push, so a
push carrying a broken check is refused rather than reported. Two limits are real: it has to be
switched on once in each copy of the code, and it can be skipped on purpose. The version that can be
neither is a build server, which we do not have yet. *(R-29, R-161, R-168)*
## What we're working on
@@ -59,8 +62,15 @@ refuses a change that skipped it. *(R-161)*
## Changed since last update
- **2026-08-02** — Fixed: thirteen mechanical checks had built up across the four repositories and
nothing ran most of them. Two were failing quietly, one since 14 July; neither did harm, but nothing
would have said so. Both fixed; every repository now has one command that runs all its checks, and
it runs automatically before every push. One of the two was the setup page claiming the installer
was version 1.19.0 while machines got 1.22.0 — the number is now **gone** rather than corrected,
because the page cannot know it: the installer is fetched fresh each run, so any number there is a
guess. A check refuses to let one be added back.
- **2026-08-02** — Decided: the 20 GB backup partition goes away and shares space with app data. That
changes the disk layout, so it happens before any machine is installed outside the house.
- **2026-08-02** — Decided: only this machine and the tester's box are protected; every other box,
both demo boxes included, may be broken or reinstalled freely.
- **2026-08-02** — Two of the three apps that never saved their data are fixed; this page created.
demo boxes included, may be broken or reinstalled freely. Two of the three apps that never saved
their data are fixed; this page created.