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.
|
||||||
@@ -1,252 +1,130 @@
|
|||||||
# CLAUDE.md — Project Instructions for Claude Code (`felhom.eu`)
|
# CLAUDE.md — `felhom.eu`
|
||||||
|
|
||||||
> Read automatically when Claude Code works in this repo. Stable orientation only — **current state
|
> Stable orientation only — **current state lives in `CONTEXT.md` and the tops of
|
||||||
> lives in `CONTEXT.md` and the tops of `hub/CHANGELOG.md` / `scripts/CHANGELOG.md` /
|
> `hub/CHANGELOG.md` / `scripts/CHANGELOG.md` / `website/CHANGELOG.md`**, never here. Cross-repo
|
||||||
> `website/CHANGELOG.md`**, never here. Cross-repo orientation (the felhom system, artifact
|
> conventions (the three-component model, artifact taxonomy, access, clean-tree gate, secrets):
|
||||||
> taxonomy, access): workspace-root `/mnt/5_hdd/felhom.eu/git/CLAUDE.md`; this file is
|
> workspace-root `/mnt/5_hdd/felhom.eu/git/CLAUDE.md`, whose versioned copy is
|
||||||
> `felhom.eu`-specific. A versioned copy of that workspace file lives at
|
> `documentation/runbooks/workspace-CLAUDE.md`. Path-scoped detail: `.claude/rules/`.
|
||||||
> `documentation/runbooks/workspace-CLAUDE.md`.
|
|
||||||
|
|
||||||
## Project overview
|
## What this repo is
|
||||||
|
|
||||||
This repo contains:
|
Four surfaces in one repo, plus the design home for the whole system:
|
||||||
- **Website** (`website/`) — static HTML at felhom.eu, served via k3s nginx + git-sync sidecar.
|
|
||||||
- **Hub** (`hub/`) — Go application (felhom-hub), the **operator backend**, on k3s at `hub.felhom.eu`.
|
- `hub/` — **felhom-hub**, the operator backend (Go, k3s, `hub.felhom.eu`).
|
||||||
- **K8s manifests** (`manifests/`) — k3s deployment manifests for felhom-system services.
|
- `website/` — static HTML at felhom.eu, served by k3s nginx + git-sync.
|
||||||
- **Architecture docs** (`documentation/`) — the **authoritative design home for the whole Felhom
|
- `manifests/` — k3s manifests for felhom-system, GitOps via one ArgoCD app.
|
||||||
system**: `architecture/01..05-*.md`, `proxmox-platform.md`, `tests/phase*-findings.md`,
|
- `scripts/` — the **public installer** (`felhom-host-install.sh`) and this repo's gates.
|
||||||
runbooks, audits. Read these before designing.
|
- `documentation/` — the **authoritative design home for all of Felhom**, not just this repo.
|
||||||
- **Skills** (`skills/`) — the versioned source of the Claude Code skills; install/update with
|
- `skills/` — versioned source of the Claude Code skills; install with
|
||||||
`python3 scripts/install_skills.py` (symlink — repo edits are live immediately).
|
`python3 scripts/install_skills.py` (symlink — repo edits are live immediately).
|
||||||
|
|
||||||
See `README.md` for full architecture/DNS/email/SEO docs. See `TASK.md` for the current task (if any).
|
## Doing X → read Y
|
||||||
See `REUSE.md` before writing new code.
|
|
||||||
|
|
||||||
## The Felhom system (so the hub's role is in context)
|
| Doing | Read |
|
||||||
|
|---|---|
|
||||||
|
| writing any new code | `REUSE.md` — helpers, seams, extension points, traps |
|
||||||
|
| needing current state / roadmap | `CONTEXT.md` |
|
||||||
|
| hub work (architecture, deploy, patterns) | loads itself: `.claude/rules/hub.md` |
|
||||||
|
| website or installer work | loads itself: `.claude/rules/website.md` |
|
||||||
|
| manifests / ArgoCD / secrets | loads itself: `.claude/rules/manifests.md` |
|
||||||
|
| writing or routing a document | loads itself: `.claude/rules/docs.md` |
|
||||||
|
| build, deploy, publish, verify a version | the **`felhom-build-deploy`** skill |
|
||||||
|
| writing or reviewing a test, fixing a bug | the **`felhom-testing`** skill |
|
||||||
|
| UI, tokens, badges, Hungarian copy | the **`felhom-ui-design`** skill |
|
||||||
|
| host addresses, break-glass, node facts | `documentation/operations/nodes.md` — never restate them |
|
||||||
|
| which box may I break | `documentation/runbooks/target-selection.md` |
|
||||||
|
| what version is live anywhere | ask the hub (`/hosts`, `/configs`) or the box — **never a doc** |
|
||||||
|
| the authoritative design | `documentation/architecture/01..05-*.md` |
|
||||||
|
|
||||||
Felhom is **Proxmox-based**, with a locked **three-component model**:
|
## Code quality
|
||||||
- **Hub** (this repo, `hub/`) — operator backend. Authors operator *intent*; mirrors box *reality*;
|
|
||||||
holds **no data-plane role** and never connects inbound to a box.
|
|
||||||
- **Host agent** (repo `felhom-agent/`) — one per Proxmox host; owns all Proxmox interaction.
|
|
||||||
- **In-guest controller** (repo `felhom-controller/`) — one per customer LXC; Docker-only.
|
|
||||||
|
|
||||||
## Hub — architecture (version-free; current version = `manifests/hub.yaml` image tag)
|
|
||||||
|
|
||||||
The hub ingests two report streams — the agent's host-domain report (`POST /api/v1/host-report`, the
|
|
||||||
heartbeat/dead-man's-switch) and the legacy controller report (`POST /api/v1/report`, **frozen until
|
|
||||||
the slice-10 cutover — do not modify**) — plus structured controller events (`POST /api/v1/event`,
|
|
||||||
gated by `allowedEventTypes`). Around them: staleness/disk/storage-fill/leaf/capability monitor
|
|
||||||
checkers, the two-tier notification dispatcher (operator English / customer Hungarian, Resend,
|
|
||||||
cooldowns), the app-mail relay, customer-config + Day-0 artifact-manifest management (the checksum
|
|
||||||
trust root the host bootstrap verifies against), assets serving, and the password-gated operator web
|
|
||||||
UI. Package map, helpers, seams, extension points: **`REUSE.md`** (e.g. new event types must enter
|
|
||||||
`allowedEventTypes` + `customerMessages` together).
|
|
||||||
|
|
||||||
## Code quality rules
|
|
||||||
|
|
||||||
- If you need more input or troubleshooting output, **ask first — don't guess**.
|
- If you need more input or troubleshooting output, **ask first — don't guess**.
|
||||||
- Testing doctrine (non-hollow tests, red-proofs, seams): use the `felhom-testing` skill.
|
- **A `go test -run` pattern that matches no test prints `ok` and exits 0.** A red-proof using `-run`
|
||||||
- **Seam-wiring rule — it covers TEMPLATE GATES:** a feature is not shipped until its entry point is
|
must first prove the filter matched something (`-v`, look for `=== RUN`). Generally: **an
|
||||||
reachable. For UI, any conditional affordance (`{{if .Flag}}` around a button/form/script) ships
|
|
||||||
with a render test **per branch of the gate** — handler tests that POST directly prove nothing
|
|
||||||
about reachability.
|
|
||||||
- **A `go test -run` pattern that matches no test prints `ok` and exits 0.** A red-proof that uses
|
|
||||||
`-run` must first prove the filter matched something (`-v`, look for `=== RUN`). Generally: **an
|
|
||||||
instrument that can drop results silently is not a measurement.**
|
instrument that can drop results silently is not a measurement.**
|
||||||
- **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 the kernel's own state, never by reading
|
|
||||||
or writing the filesystem.
|
|
||||||
- UI/design work (tokens, gates, copy rules): use the `felhom-ui-design` skill.
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
CODE-QUALITY RULE CITATIONS — history, not directives.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
go test -run: found 2026-08-02 while red-proofing — `-run TestCustomerUnified` matched nothing in the
|
go test -run: found 2026-08-02 while red-proofing — `-run TestCustomerUnified` matched nothing in the
|
||||||
target file and reported `ok ... 0.062s`, which was read as a passing red-proof. This is the "an
|
target file and reported `ok ... 0.062s`, which was read as a passing red-proof. This is the "an
|
||||||
absent line is not evidence" rule aimed at the one place a false green costs most: the proof itself.
|
absent line is not evidence" rule aimed at the one place a false green costs most: the proof itself.
|
||||||
The same class bit twice that day — a `| tail -5` inside a census query silently dropped rows and
|
The same class bit twice that day — a `| tail -5` inside a census query silently dropped rows and
|
||||||
looked exactly like a real finding.
|
looked exactly like a real finding.
|
||||||
|
|
||||||
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.
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
- **Logging**: levels/English/no-secrets rules per `documentation/runbooks/logging-conventions.md`
|
## The installer publishes by TAG, not by push (R-110)
|
||||||
(DEBUG = flow detail, INFO = state change + duration; logs are operator-tier English; keys never
|
|
||||||
values — the hub's bundle secret-gate blocks violating pulls fail-closed).
|
|
||||||
|
|
||||||
## Workflow & artifacts
|
This fence is in the core deliberately: its trigger is editing `scripts/felhom-host-install.sh`, and
|
||||||
|
no path-scoped rule covers that file. It governs the one artifact that runs **as root on a virgin
|
||||||
|
box**.
|
||||||
|
|
||||||
The planning/architecture assistant ("project Claude", in claude.ai) writes specs and validates
|
- **Pushing `scripts/felhom-host-install.sh` to `main` publishes NOTHING.** `manifests/webpage.yaml`
|
||||||
pushes; **you (Claude Code) implement**. A file being open in the editor is NOT an instruction.
|
runs two git-syncs: the website from `main`, and `/scripts/` from the tag
|
||||||
|
`installer-v<SCRIPT_VERSION>`.
|
||||||
|
- **To publish:** cut `installer-v<new SCRIPT_VERSION>`, bump the `--ref` in `webpage.yaml` (**both**
|
||||||
|
the sidecar and the init container), commit, sync.
|
||||||
|
- **To roll back:** move the tag back and wait ~30 s. No ArgoCD sync, no deploy — that is the
|
||||||
|
emergency lever; fix forward afterwards.
|
||||||
|
- **Do NOT pin the website to the tag**, and the URL never carries a ref — `felhom-bootstrap.sh` and
|
||||||
|
the hub's day-0 command follow the tag with no edit.
|
||||||
|
- `hostinstall_gates.py` gate 6 fails if the manifest stops naming an `installer-v…` tag or if the
|
||||||
|
website stops tracking `main`.
|
||||||
|
|
||||||
- **`TASK.md` / `TASK-*.md`** — a spec for you to implement. Then push and update `hub/CHANGELOG.md`
|
## Workflow — what is specific to this repo
|
||||||
and root `REPORT.md` per the convention below.
|
|
||||||
- **`RUNBOOK-*.md`** — an operational procedure. CC executes the steps it has access and capability
|
|
||||||
for, including live validation on the demo nodes and the demo Proxmox host (CC has root@felhom-pve
|
|
||||||
SSH + the felhom-agent token). Mark a step HUMAN only when it genuinely needs physical presence, a
|
|
||||||
real-world decision, or credentials CC truly lacks.
|
|
||||||
- Validation of a push against a spec's criteria is project Claude's job, not yours, unless asked.
|
|
||||||
- **Browser automation is NOT available** in the DooPlex environment (`claude-in-chrome` was a
|
|
||||||
Windows-workstation capability). Validate at the endpoint level — invoke the exact endpoint the UI
|
|
||||||
invokes — and via render tests; say which method was used. The hub UI is operator-password-gated
|
|
||||||
anyway, so render tests were already the method for UI changes. Strict end-to-end UI coverage is a
|
|
||||||
manual click-through by the operator.
|
|
||||||
|
|
||||||
> **In every repository where you make a change, update both files in that repo:**
|
- **Never `git add -A` here** — parallel sessions share the clone and it sweeps foreign WIP. Stage
|
||||||
> - **`CHANGELOG.md`** — cumulative log, newest on top (here: per-area `hub/`, `scripts/`, `website/`).
|
explicit paths only, `git pull --rebase` before every push.
|
||||||
> - **`REPORT.md`** — **overwrite** with the most recent implementation/validation summary only.
|
- **`REPORT.md` is overwritten, so two sessions in this repo clobber each other.** The second session
|
||||||
> **Parallel sessions:** `REPORT.md` is overwritten, so two sessions working in this repo at once
|
writes **`REPORT-<topic>.md`** and never touches the shared `REPORT.md`.
|
||||||
> will clobber each other. The second session writes **`REPORT-<topic>.md`** instead and never
|
- `CHANGELOG.md` here is **per-area**: `hub/`, `scripts/`, `website/`.
|
||||||
> touches the shared `REPORT.md`.
|
|
||||||
>
|
|
||||||
> **Never write secrets** into any committed file — reference them as "stored out-of-band".
|
|
||||||
|
|
||||||
- Update `REUSE.md` if you added/changed/deprecated a shared helper or pattern (same commit).
|
|
||||||
- **Never `git add -A` in this repo** — parallel sessions share the clone and it sweeps foreign WIP.
|
|
||||||
Stage explicit paths only, `git pull --rebase` before every push, and do not run two writing
|
|
||||||
sessions on one clone (use `git worktree` if truly needed).
|
|
||||||
|
|
||||||
<!-- The sweep incident: v0.47.0, commit 146d165 — a red-proof-mutated guard got swept to main. -->
|
<!-- The sweep incident: v0.47.0, commit 146d165 — a red-proof-mutated guard got swept to main. -->
|
||||||
|
|
||||||
|
|
||||||
## End-of-session checklist
|
|
||||||
|
|
||||||
- **`CHANGELOG.md` + `REPORT.md`** per the rule above, in every repo touched.
|
|
||||||
- **`REUSE.md`**, if a shared helper or pattern moved (same commit).
|
|
||||||
- **The capability map** (`documentation/architecture/00-capability-map.md`), if a capability's status
|
|
||||||
changed — with its new evidence citation.
|
|
||||||
- **The architecture doc** that owns any changed contract (S-1, `CONTEXT.md`).
|
|
||||||
- **Root `STATUS.md`** — *update it at the end of every session in which something shipped, broke, or
|
|
||||||
was decided. It is a view of `documentation/backlog/OPEN-ITEMS.md` — **nothing may exist only
|
|
||||||
there**. One screen; cut items rather than extending it.* It is written for the **operator** in
|
|
||||||
plain language, and is deliberately **not** `CONTEXT.md` — do not consolidate the two.
|
|
||||||
- **A finding goes in `OPEN-ITEMS.md` first**, never only in a report, an audit or `STATUS.md`. Four
|
|
||||||
items in this project were minted in a spike doc and lost (R-153/154/155, R-156/157).
|
|
||||||
- **Confirm your own last push's CI run went green, by run ID.** CI emails on failure, which is a
|
|
||||||
PUSH signal — this is the PULL check that catches a lost, filtered or unread mail. Quote the run
|
|
||||||
id and its conclusion in the session report, e.g.
|
|
||||||
`curl -s "https://gitea.dooplex.hu/api/v1/repos/admin/<repo>/actions/tasks?limit=3"` → match the
|
|
||||||
`head_sha` to your commit. An unchecked green is an assumption, not an observation.
|
|
||||||
|
|
||||||
## Hub stack — the two constraints
|
|
||||||
|
|
||||||
The dependency list is `hub/go.mod`'s business and the deploy shape is `manifests/`. Only the two
|
|
||||||
rules that the code cannot tell you belong here:
|
|
||||||
|
|
||||||
- **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).
|
|
||||||
|
|
||||||
## Environment & access
|
|
||||||
|
|
||||||
Claude Code runs **on DooPlex (192.168.0.180, Debian 13, user `kisfenyo`)** — the k3s node itself.
|
|
||||||
Repos in `/mnt/5_hdd/felhom.eu/git/`, build dirs in `/mnt/5_hdd/felhom.eu/build/`. `kubectl` and the
|
|
||||||
image build/push are local commands; felhom-pve is one SSH hop.
|
|
||||||
|
|
||||||
**Host addresses, routes, node names, break-glass and what is provisioned on each:**
|
|
||||||
**`documentation/operations/nodes.md`** — the single home. Do not restate them here; re-check an
|
|
||||||
address rather than trusting one written down. Tailscale topology, the accept-dns rule, the
|
|
||||||
accept-routes spike result and rollback: `documentation/operations/tailscale.md`.
|
|
||||||
|
|
||||||
**Which box do I break?** → **`documentation/runbooks/target-selection.md`** — the tiers, and per
|
|
||||||
machine what is freely permitted / needs care / forbidden, each with its reason. Read it before picking
|
|
||||||
a machine for a drill, a destructive test or a throwaway VM. **DooPlex is Tier 2 — precious**: it *is*
|
|
||||||
the recovery chain (hub, Gitea, registry, PBS, k3s+Longhorn), and never a drill target. Both demo
|
|
||||||
Proxmox hosts are Tier 0 — disposable; drill and build VMs belong on the t740.
|
|
||||||
|
|
||||||
<!--
|
|
||||||
LEGACY: WINDOWS WORKSTATION — until 2026-07-19 CC ran on Windows 11 with repos in E:\git\, and every
|
|
||||||
remote command needed SSH=/c/Windows/System32/OpenSSH/ssh.exe (Git Bash's ssh fails silently). The
|
|
||||||
workspace-root CLAUDE.md carries the full version. Retained in case that environment is revived.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Build & deploy — Hub (GitOps via ArgoCD)
|
|
||||||
|
|
||||||
**Full runbook: use the `felhom-build-deploy` skill.** The load-bearing rules:
|
|
||||||
|
|
||||||
The whole cluster is GitOps via a **single ArgoCD app `felhom`** syncing this repo's `manifests/`
|
|
||||||
to `felhom-system`. **Auto-sync is OFF** — deploys are a deliberate manual sync. ArgoCD's source of
|
|
||||||
truth is the **manifest**:
|
|
||||||
|
|
||||||
- **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 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/`.
|
|
||||||
|
|
||||||
> **Clean-tree gate before any build:** `git status --porcelain` must be empty and
|
|
||||||
> `git rev-parse HEAD` must equal `git rev-parse origin/main` in the repo being built. An unpushed
|
|
||||||
> change does not exist — never build a dirty or unpushed tree. The `git pull` in the build step
|
|
||||||
> stays (it is a no-op when you work in this tree, and load-bearing if anything was pushed from
|
|
||||||
> elsewhere).
|
|
||||||
|
|
||||||
Steps: commit+push code → `cd /mnt/5_hdd/felhom.eu/build/felhom-hub && ./build.sh <VER> --push`
|
|
||||||
(local) → bump `manifests/hub.yaml` tag + push → ArgoCD hard-refresh + sync (kubectl-patch method in
|
|
||||||
the skill, now local `sudo kubectl`) → verify Synced/Healthy + rollout + image + startup log.
|
|
||||||
|
|
||||||
## Gates — ONE entry point
|
## Gates — ONE entry point
|
||||||
|
|
||||||
**Run `python3 scripts/repo_gates.py` after ANY change in this repo.** It is the one entry point
|
**Run `python3 scripts/repo_gates.py` after ANY change in this repo.** It runs every gate —
|
||||||
and runs every gate — `site_gates.py`, `hostinstall_gates.py`, `hub_confirm_gate.py`,
|
`site_gates.py`, `hostinstall_gates.py`, `hub_confirm_gate.py`, `manifest_bearer_gate.py`,
|
||||||
`manifest_bearer_gate.py` and `reuse_refs_check.py` on this root — streaming each gate's own output
|
`reuse_refs_check.py` and `instructions_gate.py` — streaming each gate's own output and exiting
|
||||||
and exiting non-zero if any fails. `--fast` selects only the gates that touch no network and no
|
non-zero if any fails. `--fast` selects the gates that touch no network and no container runtime;
|
||||||
container runtime; today that is all of them. A missing gate script is a FAILURE, never a skip.
|
today that is all of them. **A missing gate script is a FAILURE, never a skip.**
|
||||||
|
|
||||||
`site_gates.py` is a *gate*, not a runner — do not model new work on it;
|
`site_gates.py` is a *gate*, not a runner — do not model new work on it;
|
||||||
`app-catalog-felhom.eu/scripts/catalog_gates.py` is the canonical runner (R-161).
|
`app-catalog-felhom.eu/scripts/catalog_gates.py` is the canonical runner (R-161).
|
||||||
|
|
||||||
|
**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 FIVE INVOCATIONS (2026-08-02, R-29): a census of all thirteen gates across the
|
WHY A RUNNER AND NOT FIVE INVOCATIONS (2026-08-02, R-29): a census of all thirteen gates across the
|
||||||
four repos found that every check a CLAUDE.md names was passing, and two of the four nobody is told
|
four repos found that every check a CLAUDE.md names was passing, and two of the four nobody is told
|
||||||
to run were failing — one since 14 July. The single-entry-point shape is the only one that
|
to run were failing — one since 14 July. The single-entry-point shape is the only one that
|
||||||
demonstrably gets run here.
|
demonstrably gets run here.
|
||||||
|
|
||||||
|
instructions_gate joined the list on 2026-08-06 (R-229 deferred leg). It is registered in the
|
||||||
|
controller and agent runners too, and it is the SHARED copy in felhom.eu/scripts/ — never copied
|
||||||
|
into a sibling repo, because a copy would recreate the drift it detects.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
## End-of-session checklist
|
||||||
|
|
||||||
**The pre-push hook.** `.githooks/pre-push` runs `repo_gates.py --fast` and refuses the push if it
|
Registers first — **a finding goes in `documentation/backlog/OPEN-ITEMS.md` first**, never only in a
|
||||||
fails. It is **per-clone** and switched on once with `git config core.hooksPath .githooks` — a
|
report, an audit or `STATUS.md`. Four items in this project were minted in a spike doc and lost
|
||||||
clone does not carry it, and any manual `repo_gates.py` run WARNS when this clone is unarmed.
|
(R-153/154/155, R-156/157). This applies to **every** session that ships, breaks or decides
|
||||||
`git push --no-verify` bypasses it deliberately; **say so in the session report when you use it**.
|
something, not only sessions that touch `documentation/` — which is why it is here and not in
|
||||||
Both facts are why continuous integration is still owed (`OPEN-ITEMS.md` R-168) — this hook is
|
`docs.md`.
|
||||||
local and skippable, and only CI is neither.
|
|
||||||
|
|
||||||
## Build & deploy — Website / Manifests
|
- **`CHANGELOG.md` + `REPORT.md`** in every repo touched (see the workspace root for the rule, and
|
||||||
|
the parallel-session caveat above).
|
||||||
- **Website** auto-deploys via git-sync; just push to `main` (live in 1–2 min). Website changes go
|
- **`REUSE.md`**, if a shared helper or pattern moved (same commit).
|
||||||
through `repo_gates.py` above (it runs `site_gates.py`); new pages go into that gate's `PAGES`
|
- **`OPEN-ITEMS.md`** — every finding, with a number.
|
||||||
list. Emergency edits: https://files.felhom.eu. All `website/` HTML is **UTF-8 with BOM** — preserve it.
|
- **Root `STATUS.md`** — at the end of every session in which something shipped, broke or was
|
||||||
- **THE INSTALLER DOES NOT (R-110, 2026-08-03).** `manifests/webpage.yaml` runs **two** git-syncs:
|
decided. It is a **view** of `OPEN-ITEMS.md`; nothing may exist only there. One screen, written for
|
||||||
the website from `main` as above, and `/scripts/` from the tag **`installer-v<SCRIPT_VERSION>`**.
|
the operator in plain language, and deliberately **not** `CONTEXT.md`.
|
||||||
Pushing `scripts/felhom-host-install.sh` therefore changes nothing that any machine downloads —
|
- **The capability map** (`documentation/architecture/00-capability-map.md`), if a capability's
|
||||||
which it used to, within thirty seconds, for the one artifact that runs as **root on a virgin box**.
|
status changed — with its new evidence citation.
|
||||||
- **To publish:** cut `installer-v<new SCRIPT_VERSION>`, bump the `--ref` in `webpage.yaml`
|
- **Confirm your own last push's CI run went green, by run ID.** CI emails on failure, which is a
|
||||||
(both the sidecar and the init container), commit, and sync. `hostinstall_gates.py` gate 6
|
PUSH signal; this is the PULL check that catches a lost, filtered or unread mail. Quote the run id
|
||||||
fails if the manifest stops naming an `installer-v…` tag or if the website stops tracking `main`.
|
and its conclusion, e.g.
|
||||||
- **To roll back:** move the tag back to the previous commit and wait ~30 s. **No ArgoCD sync and
|
`curl -s "https://gitea.dooplex.hu/api/v1/repos/admin/<repo>/actions/tasks?limit=3"` → match the
|
||||||
no deploy** — git-sync picks up a moved tag on its next period, measured live on 2026-08-03 in
|
`head_sha` to your commit. An unchecked green is an assumption, not an observation.
|
||||||
both directions. That is the emergency lever; fix forward with a new version afterwards.
|
|
||||||
- **Do NOT pin the website to the tag.** The sparse-checkout used to cover `/website/` and
|
|
||||||
`/scripts/` in one sync, and pinning that would turn every copy edit into a release.
|
|
||||||
- The **URL never carries a ref** (`https://felhom.eu/scripts/felhom-host-install.sh`), so
|
|
||||||
`felhom-bootstrap.sh` and the hub's day-0 command follow the tag with no edit — do not add one.
|
|
||||||
- The installer's own sixteen run-time fetches are pinned separately, to `raw/tag/v$ART_AGENT_VER`
|
|
||||||
in the **agent** repo (R-183) — they are the agent's configs, not this repo's.
|
|
||||||
- **Manifests** are GitOps via the `felhom` app — commit to `main`, then deliberate sync.
|
|
||||||
|
|
||||||
## Key patterns
|
|
||||||
|
|
||||||
- Hub status logic: OK (report < 30m), WARN (30m–1h or health=warn), DOWN (> 1h or health=fail);
|
|
||||||
host liveness thresholds shared between UI and checker (never invent a second definition).
|
|
||||||
- SQLite timestamps vary in format — always `parseSQLiteTime()`.
|
|
||||||
- Dashboard/detail auto-refresh every 60s via meta refresh. Geo-restricted to Hungary via nginx
|
|
||||||
ingress annotation.
|
|
||||||
- Helpers, seams, extension points, traps: **`REUSE.md`** — the map is maintained same-commit.
|
|
||||||
|
|||||||
@@ -1,3 +1,38 @@
|
|||||||
|
## repo_gates.py — `instructions` registered, and the repo that owns the gate now runs it (2026-08-06, R-229)
|
||||||
|
|
||||||
|
`instructions_gate.py` **lives in this repo's `scripts/`** and was registered in `controller_gates.py`
|
||||||
|
and `agent_gates.py` on the day it was written — but not in `repo_gates.py`. The reason was
|
||||||
|
mechanical: `felhom.eu/CLAUDE.md` was 227 effective lines against a 200 ceiling, and a
|
||||||
|
registered-but-failing gate refuses **every** push through `.githooks/pre-push`. So the one repo that
|
||||||
|
owns the check was the one place it did not run — the exact failure the R-29 gate census was created
|
||||||
|
to find.
|
||||||
|
|
||||||
|
Fixed in the required order: **trim first, register second.**
|
||||||
|
|
||||||
|
- `CLAUDE.md` **227 → 115 effective lines**, restructured into a core plus
|
||||||
|
`.claude/rules/{hub,website,manifests,docs}.md`, all `paths:`-scoped, all ≤60 effective lines.
|
||||||
|
- `repo_gates.py` gains gate 6, `--fast` safe. Six gates, all OK.
|
||||||
|
|
||||||
|
**Two placements deviate from the spec's sketch, both to avoid rebuilding a failure class:**
|
||||||
|
- **Register discipline (`OPEN-ITEMS.md` first, `STATUS.md` is a view) stayed in the core**, not in
|
||||||
|
`docs.md`. It applies to every session that ships or decides something; behind a
|
||||||
|
`documentation/**` glob it would be invisible in exactly the code-shipping sessions where findings
|
||||||
|
get lost — which is how R-153/154/155 and R-156/157 were minted and lost.
|
||||||
|
- **The R-110 installer fence stayed in the core.** Its trigger is editing
|
||||||
|
`scripts/felhom-host-install.sh`, which none of the four fixed globs covers; behind `website/**` it
|
||||||
|
would never load for the one artifact that runs as root on a virgin box. Compressed to its
|
||||||
|
actionable clauses rather than relocated.
|
||||||
|
|
||||||
|
**Scoping proven from the hook log, not the frontmatter** (`InstructionsLoaded`,
|
||||||
|
`~/.claude/instructions-loaded.jsonl`), in two fresh sessions so the negative control is clean:
|
||||||
|
reading `website/index.html` produced `website.md path_glob_match` and **no** `hub.md` line; reading
|
||||||
|
`hub/internal/api/handler.go` produced `hub.md path_glob_match` and **no** `website.md` line.
|
||||||
|
|
||||||
|
**Trap worth keeping:** creating `.claude/rules/` mid-session does **not** arm it. The in-session
|
||||||
|
reads that followed produced no hook line at all — a directory whose instructions were already seeded
|
||||||
|
is not re-scanned. Same class as the settings-watcher caveat. A new rule file is only live in a
|
||||||
|
session started after it exists, which is why the proof above needed `claude -p`.
|
||||||
|
|
||||||
## instructions_gate.py 1.0.0 — instruction files cannot silently regrow (2026-08-06, R-229)
|
## instructions_gate.py 1.0.0 — instruction files cannot silently regrow (2026-08-06, R-229)
|
||||||
|
|
||||||
New shared gate, registered in `controller_gates.py` and `agent_gates.py`, never copied into a
|
New shared gate, registered in `controller_gates.py` and `agent_gates.py`, never copied into a
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ Gates, in order (all must pass; **non-zero exit on any failure**):
|
|||||||
3. hub-confirm no native confirm()/prompt() in hub templates
|
3. hub-confirm no native confirm()/prompt() in hub templates
|
||||||
4. manifest-bearer no bearer-shaped literal anywhere in manifests/
|
4. manifest-bearer no bearer-shaped literal anywhere in manifests/
|
||||||
5. reuse-refs every path cited by this repo's REUSE.md still resolves
|
5. reuse-refs every path cited by this repo's REUSE.md still resolves
|
||||||
|
6. instructions CLAUDE.md length/versions/TEMPORARY, rule-file scoping, workspace-copy identity
|
||||||
|
|
||||||
|
WHY 6 IS HERE AND WAS NOT (2026-08-06, R-229 deferred leg). instructions_gate.py LIVES in this
|
||||||
|
repo's scripts/ and was registered in the controller and agent runners on the day it was written —
|
||||||
|
but not in this one, because this repo's own CLAUDE.md was still 27 lines over the ceiling and a
|
||||||
|
registered-but-failing gate refuses every push through .githooks/pre-push. The file was trimmed
|
||||||
|
(227 -> 115 effective lines, core + .claude/rules/) and the gate registered in the same session.
|
||||||
|
A check that does not run in the place it applies is the exact failure the R-29 gate census found.
|
||||||
|
|
||||||
WHY THIS FILE EXISTS (2026-08-02, closing R-29 leg (a) and half of leg (b)).
|
WHY THIS FILE EXISTS (2026-08-02, closing R-29 leg (a) and half of leg (b)).
|
||||||
|
|
||||||
@@ -49,6 +57,7 @@ GATES = [
|
|||||||
("hub-confirm", os.path.join(SCRIPTS, "hub_confirm_gate.py"), [], True),
|
("hub-confirm", os.path.join(SCRIPTS, "hub_confirm_gate.py"), [], True),
|
||||||
("manifest-bearer", os.path.join(SCRIPTS, "manifest_bearer_gate.py"), [], True),
|
("manifest-bearer", os.path.join(SCRIPTS, "manifest_bearer_gate.py"), [], True),
|
||||||
("reuse-refs", os.path.join(SCRIPTS, "reuse_refs_check.py"), [ROOT], True),
|
("reuse-refs", os.path.join(SCRIPTS, "reuse_refs_check.py"), [ROOT], True),
|
||||||
|
("instructions", os.path.join(SCRIPTS, "instructions_gate.py"), [ROOT], True),
|
||||||
]
|
]
|
||||||
|
|
||||||
VERDICT = {0: "OK", 1: "FAILED", 2: "INCONCLUSIVE"}
|
VERDICT = {0: "OK", 1: "FAILED", 2: "INCONCLUSIVE"}
|
||||||
|
|||||||
Reference in New Issue
Block a user