docs: CLAUDE.md becomes a core plus path-scoped rules (R-229)
gates / gates (push) Successful in 12s

215 lines -> 110 (92 effective; block-level HTML comments are stripped before
injection and never reach the model, verified empirically on Claude Code 2.1.222
with a control and a treatment run).

Four new .claude/rules/*.md, each with a paths: glob list so it loads only when a
matching file is read: gates, ui-hungarian, backup-paths, agent-coupling.

The ## Layout tree was deleted as derivable; REUSE.md already owns the per-package
seams its annotations stood in for. The host/access table was deleted in favour of
a pointer to documentation/operations/nodes.md -- it carried three defects at once:
demo-felhom given as the LAN fallback address as if it were the route, a pinned
"agent 0.93.0" against the project's own no-versions-in-docs rule, and the claim
that no drill VM was provisioned on demo-hp. Measured live: qm list shows VM 300
drill-r50. felhom-agent/CLAUDE.md was right; this file was wrong.

Kept verbatim: the seven session-critical invariants, the F9 live-validation fence,
the end-of-session checklist.

controller_gates.py registers the shared instructions gate (felhom.eu/scripts/,
never copied here; an absent sibling clone FAILS).

Docs only -- no Go, no version bump, no image, no deploy.
Ledger: felhom.eu/documentation/audits/LEDGER-instruction-trim-2026-08-06.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJc8sAGRWmavP3rMtdpkr2
This commit is contained in:
2026-08-06 09:38:27 +02:00
parent a62bb3874b
commit 7db42c5fec
7 changed files with 280 additions and 193 deletions
+43
View File
@@ -0,0 +1,43 @@
---
paths: ["controller/internal/agentapi/**"]
---
# Coupling to the host agent — felhom-controller
`internal/agentapi` is **the disk seam**: the pinned-TLS client to the host agent's per-guest local
API. The controller holds no Proxmox credentials; everything disk/host/Proxmox goes through here.
## Declaring a coupled feature
Controller behaviour that depends on a specific agent version needs **all three**, or it ships broken
on an older box:
1. a `featureProbes` table row in `internal/agentapi/features.go`
2. a `Supports` gate call **at the feature's entry point** — not somewhere on the path to it
3. `MinAgent: X.Y.Z` in the CHANGELOG entry header
Rules: `felhom.eu/documentation/runbooks/publish-train-rules.md`.
## Never push a controller past the agent it depends on
The R-216 guard compared the box's agent against the **golden's** MinAgent while serving a **floor**
that could point elsewhere. Raise a floor above the vouched golden — which the day-0 runbook
recommends and a per-customer override makes trivial — and the guard checks a version it is not
serving. A box then landed on a controller needing a newer agent, and its customer was told a correct
recovery code was wrong.
**A floor above the vouched golden is HELD, with its own reason** (hub v0.97.0).
## Distinguish "could not reach" from "wrong answer"
A failed bundle FETCH must not be reported to a customer as a bad recovery code. Classify by **value**
(`ErrBundleFetch` → HTTP 502), never by error string — a string is not something a caller can branch
on. Unknown class → neutral message, never the typing message.
<!--
R-224, measured live 2026-08-05 (CAMPAIGN-11 F3/F4) with a correct current code: 0.0556 s with the
hub firewalled off and 0.0299 s with the agent stopped, against ~1.0 s for a genuine unseal — the
machine accused the customer of something it had not attempted. A green test named this exact
consequence since v0.125.0 and did not prevent it, because it asserted this package's error STRING
one layer below where the merge happened. Fixed agent v0.126.0 + controller v0.202.0.
-->
+41
View File
@@ -0,0 +1,41 @@
---
paths: ["controller/internal/backup/**", "controller/internal/appbackup/**", "controller/internal/recovery/**", "controller/internal/appexport/**", "controller/internal/quiesce/**"]
---
# Backup, recovery units and export — felhom-controller
## Assert the consequence across the whole run, not the mechanism inside one function
The R-181 recovery-unit refusal claimed *"the previous unit is untouched and NOTHING was deleted"*.
*Nothing deleted* held; **untouched was measured false** — the floor was checked ONLY in
`captureAllRecoveryUnits`, while the two dump legs wrote the bulk into the same tree first and
unguarded, so a 182,272 B tar became 2,147,666,432 B under a manifest that had not moved. A full
green suite plus three of its own red-proofs missed it, because every one asserted the mechanism
inside `captureAllRecoveryUnits`.
**The test that catches this class: fingerprint the tree before and after the whole backup run, and
compare.** Full doctrine and the other eight instances: the `felhom-testing` skill.
## Presence is not success
A timestamp recording an **attempt** must never be read as evidence of a **result**. Where a status
field travels alongside a timestamp, the verdict consults both — or the timestamp records only
successes. Ask of any timestamp: *what exactly must have happened for this to be set?* If the answer
is "we tried", it cannot answer "did it work".
**Corollary:** when a verdict changes which field it counts from, the alarm text has to change with
it. `last run 8h ago` while alarming on a six-day-old success turns a true alarm into one the
operator dismisses.
<!--
Two instances. F-CRIT-2: a phantom snapshot's ctime set tier freshness — an aborted 1-byte upload
made the tier look backed up. R-100: LastRun is written on failure, so a nightly-failing offsite
tier kept the staleness clock fresh forever.
-->
## Storage keys and paths
- Never guess a persisted key — it is `offbox`, not `offbox_target` (R-7b).
- `.fab` export/import uses strict segment validation; bundles from controller ≤0.124.0 are hollow.
- Recovery-unit restore and tier-2 copies share `appbackup`'s path primitives — change them there,
once, not per caller.
+53
View File
@@ -0,0 +1,53 @@
---
paths: ["controller/**/*.go", "controller/**/*.html", "controller/**/*.css", "controller/scripts/**"]
---
# Gates and logging — felhom-controller
## The ONE entry point
**Run `python3 controller/scripts/controller_gates.py` (from `controller/`) after ANY change in this
repo.** It runs all seven local gates — `template_id_gate`, `emoji_gate`, `native_confirm_gate`,
`offbox_rename_gate`, `app_row_dedup_gate`, `mojibake_gate`, `docker_run_volume_path_gate` — plus
`reuse_refs_check` and `instructions_gate` on the repo root, streaming each gate's own output and
exiting non-zero if any fails.
- `--fast` selects the gates that touch no network and no container runtime; today that is all of them.
- **A missing gate script is a FAILURE, never a skip.**
- **The shared `reuse_refs_check.py` and `instructions_gate.py` live in `felhom.eu/scripts/` and are
never copied here** — a copy would recreate the drift they detect; an absent sibling clone FAILS.
- **The pre-push hook** (`.githooks/pre-push`) runs it with `--fast` and refuses a failing push. It is
per-clone — switch it on once with `git config core.hooksPath .githooks`, and a manual run WARNS
when this clone is unarmed. `git push --no-verify` bypasses it deliberately; **say so in the session
report when you use it.** Both facts are why CI is still owed (`OPEN-ITEMS.md` R-168).
<!--
WHY A RUNNER AND NOT SEVEN INVOCATIONS (2026-08-02, R-29) — rationale, not a directive.
A census of all thirteen gates across the four repos found that every check a CLAUDE.md named was
passing, and two of the four nobody is told to run were failing. This repo's CLAUDE.md used to name
two of the seven; the other five were reachable only through a line in REUSE.md, and
docker_run_volume_path_gate.py was RED. The single-entry-point shape is the only one that
demonstrably gets run. app-catalog-felhom.eu/scripts/catalog_gates.py is the canonical version of
the runner (R-161); repo_gates.py copies it. site_gates.py is a *gate*, not a runner — do not model
new work on it.
-->
## Logging
New leveled lines use `internal/logx` — DEBUG always reaches the debug ring; stdout respects
`logging.level`. English, keys-never-values, durations on outcomes. Full rules:
`felhom.eu/documentation/runbooks/logging-conventions.md`.
## Health checks issue no block I/O
A probe that touches a wedged device enters uninterruptible sleep, survives `SIGKILL`, and cannot be
recovered until the device returns or the host reboots — so `systemctl restart` hangs too. A timeout
protects the caller's control flow and nothing else: the blocked thread remains. Liveness is decided
from `/proc` and kernel state, never by reading or writing the filesystem.
<!--
Measured, R-117 spike §6.3 (felhom.eu/documentation/audits/SPIKE-r117-bind-liveness-2026-07-30.md):
a probe stayed in D state 3m50s after kill -9; a buffered write with no fsync blocked too (O_CREAT
needs journal access); and statfs/getdents returned HEALTHY on a namespace that EIOs every byte —
fast, and wrong.
-->
+27
View File
@@ -0,0 +1,27 @@
---
paths: ["controller/internal/web/templates/**", "controller/internal/web/**/*.go", "**/*.css", "**/*.html"]
---
# UI and Hungarian copy — felhom-controller
- **All UI text is Hungarian**, Budapest timezone.
- Design tokens, badge/colour rules, the 2px/no-shadow/no-emoji/BOM hard rules and the mechanical
gate to run after each surface: **use the `felhom-ui-design` skill.**
- Template methods need **value receivers** — pointer receivers compile, pass `go vet`, pass the
suite, and then 500 at render time.
## Grep fetched pages with ASCII-only substrings
Accented Hungarian patterns get mangled through the `ssh → pct exec → bash -c` chain and return a
false `0` — which reads exactly like the banner or string being gone. Use `kezel`, `Utols`,
`Biztons`. **Never let an accented pattern gate a conclusion.**
<!--
From the 2026-07-20 remediation: an accented grep nearly produced a wrong "banner cleared" claim.
This is the "an absent line is not evidence" rule aimed at a UTF-8 transport, not at a log.
-->
## Credentials containing `!` or `'` break in heredoc-built helper scripts
History expansion eats `!!`. Use the proven inline `-d "password=$PW"` form for authed curl, and
delete any credential-bearing helper from `/tmp` (host AND guest) when done.