docs+gate: felhom.eu/CLAUDE.md becomes core + path-scoped rules; instructions gate registered (R-229)
gates / gates (push) Successful in 8s
gates / gates (push) Successful in 8s
227 -> 115 effective lines, split into .claude/rules/{hub,website,manifests,docs}.md, and
repo_gates.py gains gate 6. Trim first, register second: a registered-but-failing gate refuses
every push through the pre-push hook, which is why this repo -- the one that OWNS the gate --
was the only one not running it.
Register discipline and the R-110 installer fence deliberately stayed in the core; both have
triggers no fixed glob covers, and scoping them would have rebuilt the failure class they exist
to prevent.
Scoping proven from the InstructionsLoaded hook log in two fresh sessions, not from frontmatter.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
---
|
||||
paths: ["documentation/**"]
|
||||
---
|
||||
|
||||
# Documentation — routing and register discipline
|
||||
|
||||
`documentation/` is the **authoritative design home for the whole Felhom system**, not just this
|
||||
repo. Sibling repos point here; this is where the pointed-at thing must actually be.
|
||||
|
||||
## Where a fact goes — one home each
|
||||
|
||||
| Fact | Home |
|
||||
|---|---|
|
||||
| the locked design | `architecture/01..05-*.md` |
|
||||
| capability status + its evidence | `architecture/00-capability-map.md` |
|
||||
| host addresses, routes, node names, break-glass, what is provisioned | `operations/nodes.md` |
|
||||
| Tailscale topology, accept-dns/accept-routes | `operations/tailscale.md` |
|
||||
| which box may be broken, and what may be done to it | `runbooks/target-selection.md` |
|
||||
| an operational procedure | `runbooks/RUNBOOK-*.md` |
|
||||
| logging levels and phrasing | `runbooks/logging-conventions.md` |
|
||||
| a spike or campaign result | `audits/` |
|
||||
| every open finding | `backlog/OPEN-ITEMS.md` |
|
||||
| the operator's one-screen view | root `STATUS.md` |
|
||||
|
||||
**Do not restate a fact that has a home** — point at it. Re-check an address rather than trusting one
|
||||
written down.
|
||||
|
||||
## Register discipline
|
||||
|
||||
The end-of-session register rules (`OPEN-ITEMS.md` first, `STATUS.md` is a view, nothing exists only
|
||||
in a report) are in the repo core `CLAUDE.md` **deliberately** — they apply to every session that
|
||||
ships or decides something, not only to sessions that touch `documentation/`, so they must load
|
||||
unconditionally. This file adds only the routing above.
|
||||
|
||||
## The coupling rule
|
||||
|
||||
**A recipe or contract section is a TWO-REPO change.** If a document here defines something a sibling
|
||||
consumes — a DR recipe's producers, an allow-list, an event type — the consumer's code and this
|
||||
document move in the same session, or the hub silently drops what it does not recognise.
|
||||
|
||||
## Version literals
|
||||
|
||||
**No component version literal in any instruction file, and none in a doc that reads as current
|
||||
fleet state.** Versions change several times a day and the fleet is not uniform. A *historical*
|
||||
citation ("fixed in hub v<X>") is fine in an audit or an HTML comment beside the rule it justifies —
|
||||
it cannot go stale the way "this box currently runs agent v<Y>" can.
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
paths: ["hub/**"]
|
||||
---
|
||||
|
||||
# Hub — architecture, constraints, deploy
|
||||
|
||||
## What the hub is
|
||||
|
||||
Operator backend. Authors operator *intent*, mirrors box *reality*, holds **no data-plane role**, and
|
||||
**never connects inbound to a box**. If it dies, apps keep serving; only management degrades.
|
||||
|
||||
Three inbound contracts, and one is frozen: the agent's host-domain report
|
||||
(`POST /api/v1/host-report`, the heartbeat/dead-man's-switch), the legacy controller report
|
||||
(`POST /api/v1/report`, **frozen until the slice-10 cutover — do not modify**), and structured
|
||||
controller events (`POST /api/v1/event`, gated by `allowedEventTypes`).
|
||||
|
||||
Everything else — checkers, the two-tier notification dispatcher, the app-mail relay, customer-config
|
||||
and Day-0 artifact-manifest management, the operator UI — is mapped in **`REUSE.md`**. Full design:
|
||||
`documentation/architecture/05-hub-architecture.md`.
|
||||
|
||||
## The two stack constraints
|
||||
|
||||
The dependency list is `hub/go.mod`'s business and the deploy shape is `manifests/`. Only these two
|
||||
cannot be read off the code:
|
||||
|
||||
- **No web frameworks.** Go stdlib `net/http` + `html/template`, and it stays that way.
|
||||
- **Secrets via out-of-band `secretKeyRef` — never inline `stringData`** (`REUSE.md` §3).
|
||||
|
||||
## Deploy — GitOps, and the manifest is the truth
|
||||
|
||||
**Full runbook: the `felhom-build-deploy` skill.** The load-bearing rules:
|
||||
|
||||
- **A code change + CHANGELOG bump deploys NOTHING.** The running image changes only when
|
||||
`manifests/hub.yaml`'s `image:` tag changes in git and the app is synced.
|
||||
- **Pin explicit versions, never `:latest`. Never bare `kubectl set image` / `kubectl apply`** —
|
||||
reverted on the next sync.
|
||||
- **The live image can lag the CHANGELOG** when a bump was committed but the manifest/sync step never
|
||||
happened — reconcile via the manifest, not the changelog.
|
||||
- Green gate before any hub commit: `go build ./... && go vet ./... && go test ./...` in `hub/`.
|
||||
|
||||
## Rules that bite here
|
||||
|
||||
- **Seam-wiring — it covers TEMPLATE GATES:** a feature is not shipped until its entry point is
|
||||
reachable. Any conditional affordance (`{{if .Flag}}` around a button, form or script) ships with a
|
||||
render test **per branch of the gate**. Handler tests that POST directly prove nothing about
|
||||
reachability.
|
||||
- **A health check issues 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.
|
||||
- New event types must enter `allowedEventTypes` **and** `customerMessages` together, or `POST
|
||||
/event` 400s.
|
||||
- Status logic: OK (report < 30m), WARN (30m–1h or `health=warn`), DOWN (> 1h or `health=fail`).
|
||||
Host-liveness thresholds are **shared** between UI and checker — never invent a second definition.
|
||||
- SQLite timestamps vary in format — always `parseSQLiteTime()`.
|
||||
- **Logging**: DEBUG = flow detail, INFO = state change + duration; operator English; keys never
|
||||
values (`documentation/runbooks/logging-conventions.md`). The bundle secret-gate fails closed.
|
||||
|
||||
<!--
|
||||
Seam-wiring / template gates: the fourth inert seam, hub v0.70.1. The v0.70.0 ghost-delete was fully
|
||||
implemented server-side and fully dead UI because the button sat inside the wrong gate.
|
||||
|
||||
Health check / block I/O: measured, R-117 spike §6.3
|
||||
(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.
|
||||
|
||||
This rule is duplicated in felhom-agent/.claude/rules/health-checks.md and
|
||||
felhom-controller/.claude/rules/gates.md. That is deliberate: all three write health checks, and a
|
||||
pointer to a sibling repo's rule file does not load.
|
||||
-->
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
paths: ["manifests/**", "**/*.yaml"]
|
||||
---
|
||||
|
||||
# Manifests — ArgoCD, deliberate sync, secrets
|
||||
|
||||
## One app, and auto-sync is OFF
|
||||
|
||||
The whole cluster is GitOps via a **single ArgoCD app `felhom`** syncing this repo's `manifests/` to
|
||||
the `felhom-system` namespace. **Auto-sync is OFF — a deploy is a deliberate manual sync.**
|
||||
|
||||
ArgoCD's source of truth is the **manifest in git**:
|
||||
|
||||
- Committing a manifest change deploys nothing until the app is synced.
|
||||
- **Pin explicit image versions, never `:latest`.**
|
||||
- **Never bare `kubectl set image` / `kubectl apply`** — the next sync reverts it, so the fix looks
|
||||
like it worked and then silently disappears.
|
||||
- Sync method (hard-refresh + sync, local `sudo kubectl`): the **`felhom-build-deploy`** skill.
|
||||
|
||||
## Secrets
|
||||
|
||||
- **Out-of-band `secretKeyRef` only — never inline `stringData`** (`REUSE.md` §3).
|
||||
- `manifest_bearer_gate.py` fails on any bearer-shaped literal anywhere under `manifests/`. It runs
|
||||
from `python3 scripts/repo_gates.py`.
|
||||
|
||||
## The two git-syncs in `webpage.yaml`
|
||||
|
||||
`manifests/webpage.yaml` runs **two** git-syncs and they track different refs: the website from
|
||||
`main`, and `/scripts/` from the tag `installer-v<SCRIPT_VERSION>`. Changing either `--ref` changes
|
||||
what real machines download — the init container **and** the sidecar both carry it, and both must
|
||||
move together. `hostinstall_gates.py` gate 6 fails if the manifest stops naming an `installer-v…`
|
||||
tag or if the website stops tracking `main`. The full publish/rollback procedure is the R-110 fence
|
||||
in the repo core `CLAUDE.md`.
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
paths: ["website/**"]
|
||||
---
|
||||
|
||||
# Website — build, deploy, and the encoding fences
|
||||
|
||||
## Deploy
|
||||
|
||||
The website **auto-deploys via git-sync from `main`** — just push, live in 1–2 min. No ArgoCD sync,
|
||||
no image build. Emergency edits: https://files.felhom.eu.
|
||||
|
||||
**Do NOT pin the website to the installer tag.** The sparse-checkout used to cover `/website/` and
|
||||
`/scripts/` in one sync; pinning that would turn every copy edit into a release. The installer's
|
||||
tag-based publishing is a separate mechanism — see the R-110 fence in the repo core `CLAUDE.md`.
|
||||
|
||||
## Gates
|
||||
|
||||
Website changes go through `python3 scripts/repo_gates.py`, which runs `site_gates.py`. **A new page
|
||||
must be added to that gate's `PAGES` list** or it is unchecked.
|
||||
|
||||
`site_gates.py` asserts: BOM, emoji, nav/footer consistency, analytics, CDN, design tokens, and
|
||||
cache-busting.
|
||||
|
||||
## The encoding fences
|
||||
|
||||
- **All `website/` HTML is UTF-8 *with BOM*. Preserve it.** An editor that strips the BOM is a
|
||||
silent, whole-file change.
|
||||
- **Emoji count is zero.** The design system forbids them; the gate enforces it.
|
||||
- Cache-busting: bump the asset query string when CSS or JS changes, or browsers serve the old file
|
||||
and the change looks like it did not deploy.
|
||||
|
||||
## Copy
|
||||
|
||||
Customer-facing copy is **Hungarian**, and its tone and terminology rules live in the
|
||||
**`felhom-ui-design`** skill — use it rather than inventing phrasing. Operator-facing text is
|
||||
English.
|
||||
Reference in New Issue
Block a user