Files
felhom-controller/.claude/rules/gates.md
T

4.8 KiB

paths
paths
controller/**/*.go
controller/**/*.html
controller/**/*.css
controller/scripts/**

Gates and logging — felhom-controller

The ONE entry point

Run python3 scripts/controller_gates.py from controller/ (or python3 controller/scripts/controller_gates.py from the repo root) after ANY change in this repo. It runs every gate in the runner's GATES table, the shared reuse_refs_check, instructions_gate and observations_gate among them, streaming each gate's own output and exiting non-zero if any fails; one, golden-notice, is ADVISORY and never refuses. The GATES table is the list; this sentence is a pointer to it, not a second copy — it drifted twice (it said "seven" while nine were registered, then named ten local gates while fourteen were).

  • --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, instructions_gate.py and observations_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 — CI re-runs the same entry point on every push and emails the operator on failure, so a bypass is noticed even though it is not blocked (R-168, CLOSED 2026-08-02).

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.

A gate ships with a decoy test that has been seen to fail (R-421)

A decoy is the LABEL without the FACT — a directory with the right name and no bake log, a handler case that exists only in a comment, a note whose prose mentions the marker it lacks. Write one for every new gate, run it, and watch it convict. felhom.eu/scripts/decoy_coverage_gate.py (run by felhom.eu's repo_gates.py, for all four repos) refuses a gate registered without one, or without a named exemption carrying its row.

Earned by five instances, every one found by accident: R-410, R-400, R-378, R-419, R-94. The 2026-09-01 sweep read all 29 gate scripts and fooled 16 of them. The four shapes to test against:

  1. name-for-fact — it matches a path or directory NAME while the fact lives inside the file.
  2. substring-for-field — it matches a token anywhere in a body instead of in the field carrying it.
  3. declaration-for-reachability — it checks a thing is declared, not that it RESOLVES.
  4. constant-for-measurement — it compares a value against itself.

Scope is a fact too. Eight of the sixteen were os.listdir (one level) where os.walk was meant: green and correct today, blind the moment anyone adds a subdirectory. Prefer os.walk, and prefer a glob over a hand-maintained list of files.

A decoy nobody would write proves nothing — say the gate is sound and move on. Five of mine were withdrawn as illegitimate and are named in felhom.eu/documentation/audits/AUDIT-gate-decoys-2026-09-01.md.