New shared scripts/instructions_gate.py, registered in controller_gates.py and agent_gates.py, never copied into a sibling repo (the reuse_refs_check.py precedent). 20 fixture tests, all asserting the effect: exit code AND that the message names the file and the reason. It is a consistency gate, not a budget gate, and the failure message says so. A /context reading measured the instruction files at 15k tokens against 869k free in a 1M window -- space is not the constraint, and a future reader must not re-derive the wrong reason. The 200-line ceiling is adherence guidance; a file nobody can hold in their head is where contradictions hide, and five were found here. Checks run against effective text (HTML comments stripped, because they are stripped before injection): the line ceiling; every .claude/rules/*.md declares paths: or an explicit unconditional: true; no component version literal; no TEMPORARY block carrying a past date; and the workspace-root CLAUDE.md is byte-identical to its versioned copy -- the live file sits outside any git repo, so that copy is its only version-controlled record. Two traps recorded so they are not reintroduced: a bare \d+\.\d+\.\d+ matches the first three octets of every IPv4 (the gate excludes dotted quads, or it fails on 192.168.0.180 in the agent's own file); and unconditional: true is NOT a Claude Code feature but this project's own marker. Workspace-root CLAUDE.md 208 -> 182 lines (142 effective), copy kept identical. The nine-instance invariant table moved into the felhom-testing skill, which triggers when writing or reviewing a test; all three directive bullets stayed in the core. felhom.eu/CLAUDE.md got surgical corrections only and is knowingly still over the ceiling at 227 effective lines -- closing it needs the restructure R-229 defers, said plainly rather than quietly absorbed. CONTEXT.md gains standing ruling S-35. OPEN-ITEMS.md gains R-229. Docs only -- no Go, no version bump, nothing built or deployed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJc8sAGRWmavP3rMtdpkr2
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
# LEDGER — instruction-file trim, 2026-08-06
|
||||
|
||||
Every block removed from a `CLAUDE.md` in this task, where it went, and why. Nothing was deleted
|
||||
without a row here. Destinations: `rule-file`, `html-comment`, `already-in:<path>`, `deleted-stale`,
|
||||
`deleted-derivable`, `deleted-amnesty`.
|
||||
|
||||
**Baselines (verified against live Gitea before starting):** felhom-controller `a62bb3874b25`,
|
||||
felhom-agent `a2e914f683bd`, felhom.eu `d30c2a51ed2a`, app-catalog-felhom.eu `ee2c8102016a` — all
|
||||
`HEAD == origin/main`, all clean.
|
||||
|
||||
**Correction to the spec's §1 table:** the workspace-root `CLAUDE.md` measured **16,642 bytes / 208
|
||||
lines**, not the stated 15,431 / 207 — it was edited at 08:32 on 2026-08-06, after the spec was
|
||||
written. The other five files matched the spec exactly.
|
||||
|
||||
---
|
||||
|
||||
## A. Mechanism verification (done before any edit — the task's design depends on these)
|
||||
|
||||
| Claim (spec §3) | Verdict | Evidence |
|
||||
|---|---|---|
|
||||
| `.claude/rules/*.md` + `paths:` loads only on a matching file read | **confirmed** | product-authored strings in the installed Claude Code 2.1.222 binary describe exactly this |
|
||||
| Block-level HTML comments are stripped before injection | **confirmed empirically** | control (two plain markers → both reported) vs treatment (one marker inside `<!-- -->` → not reported, run twice), via `claude -p` against a scratch `CLAUDE.md` |
|
||||
| `unconditional: true` frontmatter | **NOT a product feature** | absent from the build. It is this project's own marker; the gate documents it as such |
|
||||
| Root `CLAUDE.md` survives `/compact` | **not verified** | taken from the spec; drove keeping fences in the core, so recorded as an assumption |
|
||||
|
||||
The HTML-comment test needed two attempts. The first asked an open question ("list the markers you
|
||||
can see") and the red-proof did **not** go red — the model reported one marker in both arms, so the
|
||||
instrument was dropping a result silently. Rewritten as two forced yes/no questions with a
|
||||
both-plain control, it discriminated cleanly. Recorded because it is the project's own rule
|
||||
("an instrument that can drop results silently is not a measurement") catching a measurement made
|
||||
*for* this task.
|
||||
|
||||
---
|
||||
|
||||
## B. `felhom-controller/CLAUDE.md` — 215 → 110 lines (92 effective)
|
||||
|
||||
| Heading / first words | Class | Destination | Reason |
|
||||
|---|---|---|---|
|
||||
| `## Layout (verified against the tree)` (36 ln) | derivable | `deleted-derivable` | reconstructible by `ls controller/internal/`; the per-package seams and traps the annotations stood in for are `REUSE.md`'s job |
|
||||
| `!!! IMPORTANT !!!` header (3 ln) | duplicated | `already-in:CLAUDE.md` end-of-session checklist items 3 and 5 | a rule stated twice in one file gets edited in one of them |
|
||||
| `## Environment & access` host table (13 ln) | duplicated + stale | `already-in:documentation/operations/nodes.md` | see §D — three separate defects in one table |
|
||||
| `> **Legacy: Windows workstation.**` (4 ln) | duplicated | `already-in:CLAUDE.md` (workspace root, as an HTML comment) | archival; the root carries the fuller version |
|
||||
| `> **felhom-pve is back on the home LAN…**` (8 ln) | stale | `deleted-stale` | bookkeeping *about* a retired block; the record is `audits/AUDIT-vacation-remote-ops-2026-07-20.md` |
|
||||
| gates paragraph, `**Run … controller_gates.py**` (16 ln) | path-bound | `rule-file:.claude/rules/gates.md` | only matters when Go/HTML/CSS/scripts are being edited |
|
||||
| R-29 "why a runner and not seven invocations" (6 ln) | inert rationale | `html-comment` in `gates.md` | nobody acts on it; it exists so a future reader narrows the rule correctly |
|
||||
| logging paragraph (3 ln) | path-bound | `rule-file:.claude/rules/gates.md` | applies when writing Go |
|
||||
| Hungarian-UI + design-tokens line (2 ln) | path-bound | `rule-file:.claude/rules/ui-hungarian.md` | applies when editing templates/CSS |
|
||||
| ASCII-grep trap + `!`-in-credentials trap (8 ln) | path-bound gotcha | `rule-file:.claude/rules/ui-hungarian.md` | both are UI-validation traps; the core keeps a pointer |
|
||||
| coupled-features / `featureProbes` (4 ln) | path-bound | `rule-file:.claude/rules/agent-coupling.md` | applies only in `internal/agentapi/` |
|
||||
| `> **In every repository…**` CHANGELOG/REPORT/secrets (5 ln) | duplicated | `already-in:CLAUDE.md` (workspace root) | §8.4 names the root as the single home; the root always loads and survives `/compact` |
|
||||
| generic code-quality line (1 ln) | duplicated | `already-in:CLAUDE.md` (workspace root) | "double-check for bugs, add debug logging, ask rather than guess" |
|
||||
| build/deploy 4-step table (10 ln) | duplicated | `already-in:` the `felhom-build-deploy` skill | the file already said to use the skill, then restated it; the bootstrap-managed/no-compose-file gotcha was **kept** |
|
||||
|
||||
**Kept deliberately:** the seven session-critical invariants (highest-value block in the file), the
|
||||
F9 live-validation fence, the `CHANGELOG.md` read-discipline, the end-of-session checklist.
|
||||
|
||||
**New:** `.claude/rules/{gates,ui-hungarian,backup-paths,agent-coupling}.md` — 4 files, all
|
||||
`paths:`-scoped, all under 60 lines. `backup-paths.md` additionally carries the R-181 consequence-vs-
|
||||
mechanism lesson and "presence is not success", which apply exactly where backup code is written.
|
||||
|
||||
---
|
||||
|
||||
## C. Workspace-root `CLAUDE.md` (+ its versioned copy) — 208 → 182 lines (142 effective)
|
||||
|
||||
| Heading / first words | Class | Destination | Reason |
|
||||
|---|---|---|---|
|
||||
| `## Per-repo guidance` (7 ln) | derivable | `deleted-derivable` | those files load on their own; the section said so itself |
|
||||
| `## Skills` roster, 4 names + purposes (5 ln) | duplicated | `already-in:` the resident skill listing | kept the `install_skills.py` line, which is not derivable |
|
||||
| `(119 files)` in `## Memory` | stale + derivable | `deleted-stale` | the directory holds **157** memory files plus the index |
|
||||
| `## Access` host table, 5 rows (7 ln) | duplicated | `already-in:documentation/operations/nodes.md` | replaced by a pointer; the Tier-2 DooPlex fence and the "an absent fence is not permission" line were **kept** |
|
||||
| `## Legacy: Windows workstation` (14 ln) | archival | `html-comment` | operator wants it kept for revival; invisible to the model, still in the file for a human |
|
||||
| R-96 four incident narratives (18 ln) | inert rationale | `html-comment` | the four **rules** stay as directives; only the stories moved |
|
||||
| "Presence is not success" 2-row table (5 ln) | inert rationale | `html-comment` | rule + corollary stay in the core |
|
||||
| nine-row invariant table (11 ln) | actionable doctrine | `already-in:` `felhom.eu/skills/felhom-testing/SKILL.md` | it triggers on writing/reviewing a test, hardening a guard, fixing a bug — exactly when the table matters. **All three directive bullets stayed in the core** |
|
||||
|
||||
**Kept deliberately:** every production-infrastructure prohibition, the artifact taxonomy, the four
|
||||
R-96 rules, the clean-tree gate, the CHANGELOG/REPORT/secrets blockquote, trunk-based, the
|
||||
live-validation fence, the no-versions-in-docs rule, the target-selection fence.
|
||||
|
||||
**Copy discipline:** `felhom.eu/documentation/runbooks/workspace-CLAUDE.md` was re-synced and is
|
||||
byte-identical (`cmp` clean). The live file sits in a directory that is **not a git repo**, so the
|
||||
copy is the only version-controlled record of it — the new gate now enforces the equality.
|
||||
|
||||
---
|
||||
|
||||
## D. `felhom-agent/CLAUDE.md` — surgical only, 216 → 205 lines (173 effective)
|
||||
|
||||
| Heading / first words | Class | Destination | Reason |
|
||||
|---|---|---|---|
|
||||
| `> **TEMPORARY — felhom-pve is at a remote site (until ~2026-08-02).**` (16 ln) | stale | `deleted-stale` | **expired four days before this task** and instructed its own deletion; the location-independence fact worth keeping moved to an HTML comment |
|
||||
| `> **Legacy: Windows workstation.**` (4 ln) | duplicated | `html-comment` | root carries the full version |
|
||||
| drill-VM claim, "that ruling is **realized** — it hosts drill VM `300`" (5 ln) | duplicated | `already-in:documentation/operations/nodes.md:96` | **this file was right** (see §F) but the fact already had a single home |
|
||||
| host addresses / node names / break-glass (4 ln) | duplicated | `already-in:documentation/operations/nodes.md` | replaced by a pointer |
|
||||
| `felhom-agent --version → 0.115.0` | stale | `deleted-stale` | version literal; went with the TEMPORARY block |
|
||||
| `go.mod` directive `go 1.25.0` | derivable | `deleted-derivable` | `go.mod`'s business |
|
||||
| module path + binary name line (1 ln) | derivable | `deleted-derivable` | `go.mod` + the tree |
|
||||
| R-115 / R-188 / R-186 release narratives (26 ln) | inert rationale | `html-comment` + `already-in:` `felhom-build-deploy` skill | the **directives** stayed (never hand-roll; the order; reproducible build); the history and the verification recipe moved |
|
||||
| health-check block-I/O rule (4 ln) | path-bound | `rule-file:.claude/rules/health-checks.md` | it was duplicated from `felhom.eu/CLAUDE.md` *with a note explaining why* — that reasoning predates path-scoped rules |
|
||||
|
||||
**New:** `felhom-agent/.claude/rules/health-checks.md`, scoped to the five packages where health
|
||||
checks are written.
|
||||
|
||||
---
|
||||
|
||||
## E. `felhom.eu/CLAUDE.md` — surgical only, 241 → 235 lines (**227 effective — still over the 200 ceiling**)
|
||||
|
||||
| Heading / first words | Class | Destination | Reason |
|
||||
|---|---|---|---|
|
||||
| `## Tech stack (Hub)` list (5 ln) | derivable | `deleted-derivable` | `hub/go.mod` + `manifests/`; the two constraints that are *not* derivable ("no web frameworks", "never inline `stringData`") were kept |
|
||||
| three generic code-quality bullets (3 ln) | amnesty | `deleted-amnesty` | "double-check generated code", "handle edge cases", "add debug capabilities" — no recorded production violation, recoverable worst case. **"ask first — don't guess" was kept**: it is a real behavioural directive |
|
||||
| skills roster, 4 names (4 ln) | duplicated | `already-in:` the resident skill listing | kept the `install_skills.py` line |
|
||||
| host table, 3 rows (6 ln) | duplicated | `already-in:documentation/operations/nodes.md` | replaced by a pointer; the Tier-2/Tier-0 verdicts kept inline |
|
||||
| `> **Legacy: Windows workstation.**` (3 ln) | duplicated | `html-comment` | root carries the full version |
|
||||
| seam-wiring / `-run` / health-check citations (12 ln) | inert rationale | `html-comment` | the **rules** stayed; the incident detail and the three version literals moved into the comment beside them |
|
||||
| R-29 gate-census narrative (6 ln) | inert rationale | `html-comment` | as elsewhere |
|
||||
| `git add -A` sweep incident citation (1 ln) | inert rationale | `html-comment` | the prohibition stayed |
|
||||
|
||||
---
|
||||
|
||||
## F. The contradictions — resolved, not annotated
|
||||
|
||||
| # | Contradiction | Resolution | Where the fact lives now |
|
||||
|---|---|---|---|
|
||||
| 1 | agent: demo-hp "hosts drill VM `300` (`drill-r50`)" vs controller: "no drill VM is provisioned there yet" | **measured live** — `ssh demo-hp "qm list"` shows `300 drill-r50 stopped`. **felhom-agent was right; felhom-controller was wrong.** No new text needed: `documentation/operations/nodes.md:96` already stated it correctly, and `runbooks/target-selection.md:64` already fences it | `nodes.md` (unchanged) |
|
||||
| 2 | agent: `TEMPORARY — felhom-pve is at a remote site (until ~2026-08-02)` vs controller: "back on the home LAN (2026-07-25)" | both blocks deleted; neither belongs in a `CLAUDE.md`. The gate now fails any TEMPORARY block whose date has passed | `audits/AUDIT-vacation-remote-ops-2026-07-20.md` |
|
||||
| 3 | `agent 0.93.0` recorded in controller's host table, against the root file's own no-versions rule | deleted, with every other version literal in effective text | ask the hub `/hosts` + `/configs`, or the box |
|
||||
| 4 | controller gave `demo-felhom` as plain `root@192.168.0.162` (the LAN *fallback*) while the other two documented the Tailscale route | host tables removed from all three `CLAUDE.md` files | `nodes.md` |
|
||||
| 5 | root file: memory is `(119 files)`; the directory holds 157 + the index | parenthetical deleted rather than corrected — it is derivable and would go stale again | `ls .claude-memory/` |
|
||||
|
||||
**The sweep found no contradictions beyond the five.** After the trim: zero version literals in
|
||||
effective text across all four files, zero TEMPORARY blocks, and exactly one file (`nodes.md`)
|
||||
stating the drill-VM status.
|
||||
|
||||
---
|
||||
|
||||
## G. Amnesty list (ruling 1) — every prohibition deleted, and why it qualified
|
||||
|
||||
Deliberately short. Only one item met the bar.
|
||||
|
||||
| Rule deleted | Where | Why it qualified |
|
||||
|---|---|---|
|
||||
| "Always double-check generated code for bugs, logic issues, syntax errors" / "Handle edge cases without overcomplicating" / "Add debug capabilities (logging, verbose output)" | `felhom.eu/CLAUDE.md` | Generic best practice with **no recorded production violation** and a recoverable worst case. Also duplicated in the workspace-root file, which keeps the one clause that is a real directive: **ask rather than guess** |
|
||||
|
||||
**Nothing on the protected list was touched.** The eight-invariant table (in fact **nine** rows — the
|
||||
spec's §8.1 undercounts it), "presence is not success", the four R-96 standing rules, the F9
|
||||
live-validation fence, secrets-never-in-committed-files, trunk-based/no-branches, the DooPlex
|
||||
protection rules, the destructive-target fences with their permitted targets, and the clean-tree
|
||||
gate all survive — moved or compressed, never dropped, and every compressed fence kept its permitted
|
||||
target and its reason.
|
||||
|
||||
---
|
||||
|
||||
## H. Before / after
|
||||
|
||||
| File | before (ln/B) | after raw (ln/B) | after **effective** (ln/B) | ceiling |
|
||||
|---|---|---|---|---|
|
||||
| workspace-root `CLAUDE.md` | 208 / 16,642 | 182 / 11,280 | **142 / 8,105** | pass |
|
||||
| `felhom.eu/…/workspace-CLAUDE.md` | 208 / 16,642 | 182 / 11,280 | **142 / 8,105** | pass (byte-identical) |
|
||||
| `felhom-controller/CLAUDE.md` | 215 / 14,775 | 110 / 6,341 | **92 / 4,881** | pass |
|
||||
| `felhom-agent/CLAUDE.md` | 216 / 15,554 | 205 / 13,619 | **173 / 11,491** | pass |
|
||||
| `felhom.eu/CLAUDE.md` | 241 / 17,471 | 235 / 17,003 | **227 / 16,286** | **OVER — deferred, see below** |
|
||||
| `app-catalog-felhom.eu/CLAUDE.md` | 79 / 6,294 | untouched | 79 / 6,294 | pass — **the reference shape** |
|
||||
|
||||
*Effective* = with block-level HTML comments stripped, i.e. what the model actually receives.
|
||||
|
||||
**Controller-session total** (workspace root + repo file): 31,417 → **17,621 bytes raw**, and
|
||||
**12,986 bytes effective** — a 59% reduction in what loads. Stated as bytes, not tokens: the measured
|
||||
`/context` ratio (§I) shows a bytes/4 token estimate understates the real cost by 1.6–1.9×, so the
|
||||
byte figure is the honest one.
|
||||
|
||||
**`felhom.eu/CLAUDE.md` is knowingly left over the ceiling at 227 effective lines.** Spec §12
|
||||
forbids restructuring it into core+rules ("a diff Viktor cannot read is a diff that gets approved
|
||||
unread") and §6.2 registers the gate only in the controller and agent runners, so it is not gated
|
||||
today. Bringing it under 200 would require the restructure §12 prohibits. Filed as deferred work
|
||||
(§N.5 row below); when that lands, register `instructions_gate` in `felhom.eu/scripts/repo_gates.py`
|
||||
too.
|
||||
|
||||
---
|
||||
|
||||
## I. Measured context baseline (operator-supplied `/context`, workspace root, Opus 5 / 1M window)
|
||||
|
||||
| Category | Measured |
|
||||
|---|---|
|
||||
| Window | 1,000,000 tokens · 131.1k used (13%) · **868.9k free (86.9%)** |
|
||||
| Memory files | **2 files · 15.0k tokens (1.5%)** — root `CLAUDE.md` 6.6k + `MEMORY.md` 8.4k |
|
||||
| Skills | 19 · 2.5k (0.3%) |
|
||||
| MCP tools | deferred · 0 tokens |
|
||||
|
||||
**Which files load, settled:** the `/context` breakdown names exactly two memory files at the
|
||||
workspace root — the root `CLAUDE.md` and the auto-memory `MEMORY.md`. The per-repo `CLAUDE.md`
|
||||
files are **not** loaded at launch; they load on demand once a file in that directory is read. This
|
||||
confirms the spec's §1 assumption and is why the irreversible fences were kept in the root file.
|
||||
|
||||
**Token ratio, measured:** root `CLAUDE.md` 16,642 B → 6.6k tokens (2.52 B/token); `MEMORY.md`
|
||||
17,688 B → 8.4k tokens (2.11 B/token). A bytes/4 estimate understates by **1.59× and 1.90×**
|
||||
respectively — the spec's 1.7–2× range is right, and every disk-based figure in this ledger is
|
||||
labelled as bytes for that reason.
|
||||
|
||||
**Space was never the constraint.** 869k tokens were free. The justification for this task is the
|
||||
contradiction count (§F) and the adherence guidance, and the gate's failure message says so
|
||||
explicitly so that no future reader re-derives the wrong reason.
|
||||
|
||||
---
|
||||
|
||||
## J. `MEMORY.md` — measured only, unchanged (ruling 4 deferred it)
|
||||
|
||||
| Measure | Value | Limit | Verdict |
|
||||
|---|---|---|---|
|
||||
| Lines | 146 | 200 | within |
|
||||
| Size | 17,688 B (17.3 KB) | 25 KB | within |
|
||||
| Referenced past the limit | 0 | — | **nothing is truncated** |
|
||||
|
||||
The expected finding — a truncated index — is **not present**. A different one is: the index
|
||||
references **113** distinct memory files while **157** exist on disk (plus the index itself), and
|
||||
**zero** referenced files are missing. So 44 memory files exist that the index never points at.
|
||||
Recorded, not acted on.
|
||||
|
||||
---
|
||||
|
||||
## K. Reconciliation
|
||||
|
||||
| Repo | `git diff --stat` | Ledger rows |
|
||||
|---|---|---|
|
||||
| felhom-controller | `CLAUDE.md` 92 ins / 194 del; `controller_gates.py` +3 | 14 rows (§B) |
|
||||
| felhom-agent | `CLAUDE.md` 65 ins / 73 del; `agent_gates.py` +3 | 9 rows (§D) |
|
||||
| felhom.eu | `CLAUDE.md` +113 ctx; `workspace-CLAUDE.md` 242 changed; `SKILL.md` +25 | 8 rows (§C) + 8 rows (§E) |
|
||||
|
||||
Rows reconcile against the diffstat: every deleted block above appears in a deletion, and the
|
||||
insertions are the replacement pointers, the rule files, the HTML comments and the skill section.
|
||||
Files created: 5 rule files, `instructions_gate.py`, `test_instructions_gate.py`, this ledger.
|
||||
|
||||
---
|
||||
|
||||
## L. Observations — noticed, not acted on
|
||||
|
||||
1. **`target-selection.md` carries a known error** (the t740's off-site tier). Out of scope per §12;
|
||||
still present.
|
||||
2. **The workspace-root `CLAUDE.md` lives outside any git repo.** `/mnt/5_hdd/felhom.eu/git` is not a
|
||||
repository, so the live file is untracked and only the `felhom.eu` copy is version-controlled.
|
||||
Making the live file a **symlink** to the versioned copy would remove the divergence class
|
||||
entirely and looks safe here — both are on the same filesystem and Claude Code reads through
|
||||
symlinks (the four skills are already symlinks into this tree). **Not done in this task**, per
|
||||
§3.2. If adopted, the gate's copy-identity check becomes trivially true and should be replaced by
|
||||
a check that the symlink still points where it should.
|
||||
3. **A blanket version-literal ban has a false-positive class the spec did not anticipate:** a bare
|
||||
`\d+\.\d+\.\d+` matches the first three octets of **every IPv4 address**. The gate excludes dotted
|
||||
quads; without that it fails on `192.168.0.180` in the agent's own file.
|
||||
4. **Historical version citations are not fleet state.** "fixed in hub v0.97.0" cannot go stale the
|
||||
way "this box runs agent 0.93.0" can. Rather than carve an exception into the gate, each citation
|
||||
moved into the HTML comment beside its rule — the rule text stays clean and the gate stays
|
||||
absolute. Recorded because it is a deviation from §8.2 item 3's literal wording ("delete every
|
||||
one") in favour of its stated purpose.
|
||||
5. **The spec's §8.1 calls the invariant table "the eight-invariant table"; it has nine rows** and
|
||||
its own text says "Nine instances". Nothing was dropped — flagging the miscount so the protected
|
||||
list is not later applied to eight of nine.
|
||||
6. **`demo-hp` also hosts VM `321 c11-appliance`, running**, alongside the drill VM. Seen in the same
|
||||
`qm list`; `nodes.md` does not mention it. Not investigated.
|
||||
7. **Two unused Claude Code plugins and a broad user-scope allowlist** (`Bash(python3:*)`,
|
||||
`Bash(curl:*)`, `Bash(scp:*)` — standing arbitrary execution and network egress in every project)
|
||||
were found by the earlier setup audit. **These live in `~/.claude/settings.json`, are not in any
|
||||
repo, and are deliberately out of scope here.** Recorded only.
|
||||
8. **The `felhom-agent` core is at 173 effective lines** — passing, but with the least headroom. Its
|
||||
release section is the next candidate for the `felhom-build-deploy` skill when §12's
|
||||
corrections-only restriction is lifted.
|
||||
@@ -109,6 +109,12 @@ the fault was real. Full observables: `tests/campaign11-evidence-2026-08-05/jour
|
||||
| **R-228** | **After „I do not want the old data", the set-aside history becomes invisible — the box records where it is and shows it to nobody.** The move-aside itself is **correct and was verified byte-for-byte**: `/home/felhom-repo` → `/home/felhom-repo.orphaned-20260805` with its mtime, its `du -s` (**12 535 KB**) and snapshot **`f3d9cd67`** all unchanged, and a fresh empty repo initialised beside it. **Nothing was deleted.** But `settings.json` then carries `"orphaned_renamed_to": "/home/felhom-repo.orphaned-20260805"` and a census returns **zero** references to `OrphanedRenamedTo` in any template or web handler — the field is written and read by nobody. `/backups/remote` after the set-aside contains no occurrence of the path, „félretéve", „régi előzmény" or any equivalent (instrument controls: `felhom-repo` → 2, „letétbe helyezve" → 1). **12.5 MB of the customer's deliberately retained data sits at a path the box knows and never shows**; its only mention is a flash message on the redirect, gone on the next click. Meanwhile `GET /recovery` → 302 and `POST /recovery/unlock` → 302 with no message, so a customer who changes their mind gets **no explanation at all** (correctly, not a typing accusation — but not an explanation either). **The project's own "seam built but never wired" pattern**, landing on the one promise the set-aside screen makes | **CLOSED 2026-08-06 — controller v0.202.0.** `OrphanedRenamedTo` is surfaced as two facts and stops. **It does not promise the history can be reopened** — it cannot be, by anyone, today (R-199's inventory is unbuilt) — and the set-aside **confirmation copy was corrected** for the same reason: *"a helyreállítási kód nélkül többé nem lesznek megnyithatók"* implied that WITH the code they could be. The field's own comment said "recovery-code-recoverable", the same over-promise in the code. **PROVEN LIVE**: the notice renders on the venue |
|
||||
| **R-227** | **A controller restart mid-unlock returns a raw English `Bad Gateway`.** F8 restarted the container at T+0.7 s, inside the unseal window (control: `StartedAt` moved). The customer got **HTTP 502 / „Bad Gateway"** from traefik — a raw upstream error, in English, naming no reason and saying nothing about whether the key was installed. **The state half is clean**: the four `/data/offbox` files stayed byte-identical with mtimes frozen, and the controller returned healthy in 40 s. Breaches **I3** | **CLOSED 2026-08-06 — controller v0.202.0, partially and stated as such.** **The layer that answers is traefik**, whose config this repo generates — but traefik v3 serves no static files, so a branded proxy page needs a **new always-up container** for every 502 on the box: **scoped, not built**. Shipped: the unlock posts via `fetch` and answers a gateway failure in Hungarian in-page. **Progressive enhancement — with no JS the plain POST still shows the proxy's error** |
|
||||
|
||||
## Instruction files — deferred half, 2026-08-06
|
||||
|
||||
| ID | What | State |
|
||||
|---|---|---|
|
||||
| **R-229** | **The instruction-file rightsizing landed for `felhom-controller` and the workspace root; three pieces were deliberately deferred.** Done 2026-08-06: controller split into a 92-effective-line core plus four `paths:`-scoped `.claude/rules/*.md`; workspace root 208→142 effective lines with its versioned copy kept byte-identical; surgical corrections to `felhom-agent` and `felhom.eu` (expired TEMPORARY block, every version literal, the Legacy-Windows copies, the duplicated health-check rule); five contradictions resolved — including a drill-VM claim **measured live** (`qm list` on demo-hp shows VM 300 `drill-r50`; `felhom-agent` was right, `felhom-controller` was wrong); new shared `felhom.eu/scripts/instructions_gate.py` registered in `controller_gates.py` and `agent_gates.py`, 20 fixture tests + red-proof. **Deferred, and why:** (a) **`felhom.eu/CLAUDE.md` is at 227 effective lines, over the 200 ceiling** — reducing it needs the core+rules restructure that the task spec explicitly forbade for reviewability, so it is not gated today; when it lands, also register `instructions_gate` in `scripts/repo_gates.py`. (b) **`felhom-agent/CLAUDE.md` at 173 effective lines** passes with the least headroom; its release section is the next candidate for the `felhom-build-deploy` skill. (c) **The auto-memory decision** — `MEMORY.md` was measured only (146 lines / 17.3 KB, **within** both the 200-line and 25 KB limits, nothing truncated), but the index references 113 memory files while **157 exist on disk**, so 44 are unindexed. (d) **The spec-as-failing-test pilot**, approved in principle and not started. Full accounting: `audits/LEDGER-instruction-trim-2026-08-06.md` | **READY** — owner Viktor |
|
||||
|
||||
**Recorded against existing rows by Phase 2:**
|
||||
|
||||
- **R-216 — §4.1 is now MEASURED, not deduced.** The previous session could only offer two absences.
|
||||
|
||||
@@ -2,15 +2,21 @@
|
||||
|
||||
## What this workspace is
|
||||
|
||||
`/mnt/5_hdd/felhom.eu/git` is a parent folder holding the felhom sibling repos. Most are one logical
|
||||
product — **Felhom**, a managed home-server service for Hungarian households — spread across several
|
||||
repos. (Any non-felhom repo is unrelated; ignore unless asked.)
|
||||
A parent folder holding the felhom sibling repos. Most are one logical product — **Felhom**, a
|
||||
managed home-server service for Hungarian households. (Any non-felhom repo is unrelated; ignore
|
||||
unless asked.)
|
||||
|
||||
**Claude Code runs HERE, on DooPlex (192.168.0.180), as `kisfenyo`.** Builds are local commands; the
|
||||
Proxmox host is one SSH hop (`ssh felhom-pve`). The Windows workstation is no longer the
|
||||
orchestration point and its trees are stale — see "Legacy: Windows workstation" at the bottom.
|
||||
**Claude Code runs HERE, on DooPlex (192.168.0.180), as `kisfenyo`.** Builds are local; the Proxmox
|
||||
host is one SSH hop. Run CC inside tmux so sessions survive SSH drops: **`tmux new -A -s cc`**.
|
||||
|
||||
Run CC inside tmux so sessions survive SSH drops: **`tmux new -A -s cc`**.
|
||||
- **Hub** — operator backend on k3s (`hub.felhom.eu`), in `felhom.eu/hub/`.
|
||||
- **Host agent** — one per Proxmox host, operator-tier, owns all Proxmox interaction: `felhom-agent/`.
|
||||
- **In-guest controller** — one per customer LXC, Docker-only: `felhom-controller/`.
|
||||
- Also: `app-catalog-felhom.eu/` (app templates), `homelab-manifests/` (DooPlex k3s).
|
||||
|
||||
Each repo's own `CLAUDE.md` and `.claude/rules/` load when you touch files there. The four Felhom
|
||||
skills are installed from `felhom.eu/skills/` with `python3 felhom.eu/scripts/install_skills.py`
|
||||
(symlink — repo edits are live immediately).
|
||||
|
||||
## This host is production infrastructure
|
||||
|
||||
@@ -23,77 +29,58 @@ DooPlex runs Gitea, the container registry, k3s + Longhorn, PBS, and the hub. Tr
|
||||
- Do not run Claude Code with permission prompts disabled on this host.
|
||||
- Watch disk headroom before large builds: `df -h /mnt/5_hdd /` — abort if either is >90%.
|
||||
|
||||
## The Felhom system (three-component model, Proxmox-based)
|
||||
## Artifact taxonomy (it prevents the "what do I do?" stall)
|
||||
|
||||
- **Hub** — operator backend on k3s (`hub.felhom.eu`). Lives in `felhom.eu/hub/`.
|
||||
- **Host agent** — one per Proxmox host, operator-tier, owns all Proxmox interaction. Repo `felhom-agent/`.
|
||||
- **In-guest controller** — one per customer LXC, Docker-only. Repo `felhom-controller/`.
|
||||
The planning/architecture assistant (in claude.ai, "project Claude") produces files with distinct
|
||||
roles. **A file being open in the editor is NOT an instruction. If no task is stated, ask.**
|
||||
|
||||
Other felhom repos: `app-catalog-felhom.eu/` (app templates), `homelab-manifests/` (DooPlex k3s).
|
||||
- **`TASK.md` / `TASK-*.md`** — a spec for **you (Claude Code) to implement**. Implement it when it is
|
||||
placed as `TASK.md` at a repo root, or when explicitly told "implement <file>". Then push, update
|
||||
`CHANGELOG.md`, and write the repo's `REPORT.md`.
|
||||
- **`RUNBOOK-*.md`** — an operational procedure. CC executes every step it has access and capability
|
||||
for, live hosts included (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 lacks.
|
||||
**Do not decline a whole procedure because it touches a live host or a privileged token.** Confirm
|
||||
before irreversible ops on real customer data; demo scratch guests are fair game.
|
||||
- **Validation/review** — checking a push against a spec's criteria is **project Claude's** job, not
|
||||
yours, unless asked.
|
||||
|
||||
**Authoritative design docs (read these before designing anything):** `felhom.eu/documentation/architecture/01..05-*.md`, `felhom.eu/documentation/proxmox-platform.md`, `felhom.eu/documentation/tests/phase{0,1-2,3,4}-findings.md`.
|
||||
## Standing rules — each earned by a real failure (R-96)
|
||||
|
||||
## Per-repo guidance
|
||||
1. **Never combine a test run and a commit in one command.** A combined command has ONE exit code and
|
||||
the interesting one gets swallowed. Run the suite, read `rc`, *then* commit.
|
||||
2. **A "no access" claim must list what was tried.** "No access" is unfalsifiable unless it names its
|
||||
attempts.
|
||||
3. **An absent log line is not evidence of correct behaviour.** Verify with a POSITIVE observable —
|
||||
something that MUST appear when the system is healthy. An empty log is equally consistent with
|
||||
"working" and "stopped entirely".
|
||||
4. **A recommendation that is not followed gets one line saying why.** Silence reads as agreement and
|
||||
the disagreement is lost.
|
||||
|
||||
When you work in a repo, read its `CLAUDE.md` (it loads on-demand the moment you touch a file there):
|
||||
- `felhom-agent/CLAUDE.md` — the Go host agent.
|
||||
- `felhom.eu/CLAUDE.md` — hub + website + manifests + the architecture docs.
|
||||
- `felhom-controller/CLAUDE.md` — the in-guest controller.
|
||||
|
||||
## Skills
|
||||
|
||||
Four Felhom skills exist (personal scope, `~/.claude/skills/`): **`felhom-build-deploy`** (all
|
||||
build/deploy/publish runbooks), **`felhom-ui-design`** (design-system v2 tokens/rules/gates),
|
||||
**`felhom-testing`** (non-hollow tests + red-proofs + seams), **`felhom-app-catalog`** (catalog
|
||||
authoring workflow). Source of truth: `felhom.eu/skills/`; install/update with
|
||||
`python3 felhom.eu/scripts/install_skills.py` (symlink — repo edits are live immediately).
|
||||
|
||||
## Memory
|
||||
|
||||
The accumulated project memory (119 files) migrated from the Windows workstation lives at
|
||||
`/mnt/5_hdd/felhom.eu/git/.claude-memory/`, surfaced to Claude Code via
|
||||
`~/.claude/projects/-mnt-5-hdd-felhom-eu-git/memory` (symlink). `MEMORY.md` there is the index.
|
||||
Memories reflect what was true when written — verify a named file/flag still exists before acting
|
||||
on it.
|
||||
|
||||
## Artifact taxonomy (READ THIS — it prevents the "what do I do?" stall)
|
||||
|
||||
The planning/architecture assistant (in claude.ai, "project Claude") produces files with distinct roles. **A file being open in the editor is NOT an instruction. If no task is stated, ask.**
|
||||
|
||||
- **`TASK.md` / `TASK-*.md`** — a spec for **you (Claude Code) to implement**. Implement it when it is placed as `TASK.md` at a repo root, or when explicitly told "implement <file>". Then push, update `CHANGELOG.md`, and write the repo's `REPORT.md`.
|
||||
- **`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). A step is human-only only when it genuinely needs physical presence, a real-world decision, or credentials CC truly lacks — mark those steps HUMAN. Do not decline a whole procedure because it touches a live host or a privileged token. (Judgment still applies: confirm before irreversible ops on real customer data — but demo scratch guests are fair game.)
|
||||
- **Validation/review** — checking a push against a spec's criteria is **project Claude's** job, not yours, unless asked.
|
||||
<!--
|
||||
R-96 incident record (committed 2026-07-27) — rationale, not directives.
|
||||
1. Three recorded occurrences; the worst pushed a red suite because `packages ok: 28` was read while
|
||||
rc=1 was not.
|
||||
2. Two wrong verdicts on 2026-07-27 alone: ep0 (declared unreachable after trying exactly one route
|
||||
— felhom-pve -> 10.77.0.1; DooPlex -> 167.233.158.164 worked and the project memory said so), and
|
||||
the storage-box API (api.hetzner.cloud 404s for every storage-box endpoint; api.hetzner.com/v1 is
|
||||
the real one, and the hub's own hetznerapi.go:3 records it).
|
||||
3. Earned twice on 2026-07-27: the R-88 watcher (an empty quiesce log could not distinguish a healthy
|
||||
loop from a dead one — retired in favour of the per-tier /backup/due polls in pveproxy/access.log),
|
||||
and a hub DB copy whose write had silently failed, returning a confident "0 events in window" from
|
||||
a file a day stale until its mtime was checked.
|
||||
4. Twice in the R-88/R-97 arc a review point was absorbed rather than argued: R-84 was folded into
|
||||
R-82 without a word, and R-97a's operator-only guard was dropped while the claim it was meant to
|
||||
enforce got committed as a comment — which is how a false guarantee shipped and survived a release.
|
||||
-->
|
||||
|
||||
## Shared conventions
|
||||
|
||||
### Standing rules — each one earned by a real failure (R-96, committed 2026-07-27)
|
||||
|
||||
These were agreed in conversation and lived nowhere, so they bound nobody. They do now.
|
||||
|
||||
1. **Never combine a test run and a commit in one command.** A combined command has ONE exit code and
|
||||
the interesting one gets swallowed. Three recorded occurrences; the worst pushed a red suite
|
||||
because `packages ok: 28` was read while `rc=1` was not. Run the suite, read `rc`, *then* commit.
|
||||
|
||||
2. **A "no access" claim must list what was tried.** "No access" is unfalsifiable unless it names its
|
||||
attempts. Two wrong verdicts on 2026-07-27 alone: ep0 (declared unreachable after trying exactly
|
||||
one route — `felhom-pve → 10.77.0.1`; `DooPlex → 167.233.158.164` worked and the project memory
|
||||
said so), and the storage-box API (`api.hetzner.cloud` 404s for every storage-box endpoint;
|
||||
`api.hetzner.com/v1` is the real one, and the hub's own `hetznerapi.go:3` records it).
|
||||
|
||||
3. **An absent log line is not evidence of correct behaviour.** Verify with a POSITIVE observable —
|
||||
something that MUST appear when the system is healthy. An empty log is equally consistent with
|
||||
"working" and "stopped entirely". Earned twice on 2026-07-27: the R-88 watcher (an empty quiesce
|
||||
log could not distinguish a healthy loop from a dead one — retired in favour of the per-tier
|
||||
`/backup/due` polls in `pveproxy/access.log`), and a hub DB copy whose write had silently failed,
|
||||
returning a confident "0 events in window" from a file a day stale until its mtime was checked.
|
||||
|
||||
4. **A recommendation that is not followed gets one line saying why.** Silence reads as agreement and
|
||||
the disagreement is lost. Twice in the R-88/R-97 arc a review point was absorbed rather than
|
||||
argued: R-84 was folded into R-82 without a word, and R-97a's operator-only guard was dropped
|
||||
while the claim it was meant to enforce got committed as a comment — which is how a false
|
||||
guarantee shipped and survived a release. Disagreeing is fine; disagreeing silently is not.
|
||||
|
||||
- **Push to `main` directly** — no feature branches.
|
||||
- **Versioning** via build-time ldflags (`-X main.version`/`-X main.Version`); bump on meaningful
|
||||
changes + a CHANGELOG entry.
|
||||
- Code quality: double-check for bugs/edge cases; add debug logging; **ask rather than guess** when
|
||||
you'd otherwise need to invent input or output.
|
||||
|
||||
> **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
|
||||
@@ -103,12 +90,11 @@ These were agreed in conversation and lived nowhere, so they bound nobody. They
|
||||
|
||||
> **In every repository where you make a change, update both files in that repo:**
|
||||
> - **`CHANGELOG.md`** — a cumulative log of **all** changes; newest entry on top.
|
||||
> - **`REPORT.md`** — **overwrite** with a summary of the **most recent** implementation (or significant validation/operational run) only; not cumulative.
|
||||
> - **`REPORT.md`** — **overwrite** with a summary of the **most recent** implementation (or
|
||||
> significant validation/operational run) only; not cumulative.
|
||||
>
|
||||
> **Never write secrets** — tokens, passwords, private keys, API keys — into `CHANGELOG.md`, `REPORT.md`, or any committed file. Reference them as "stored out-of-band" instead.
|
||||
|
||||
- **Versioning** is via build-time ldflags (`-X main.version`/`-X main.Version`); bump on meaningful changes + add a CHANGELOG entry.
|
||||
- Code quality: double-check for bugs/edge cases; add debug logging; **ask rather than guess** when you'd otherwise need to invent input or output.
|
||||
> **Never write secrets** — tokens, passwords, private keys, API keys — into `CHANGELOG.md`,
|
||||
> `REPORT.md`, or any committed file. Reference them as "stored out-of-band" instead.
|
||||
|
||||
## Live validation — no browser here
|
||||
|
||||
@@ -122,87 +108,75 @@ Local (this host): repos `/mnt/5_hdd/felhom.eu/git/<repo>`, build dirs
|
||||
`/mnt/5_hdd/felhom.eu/build/felhom-{controller,hub,agent}`, `sudo kubectl`, Go toolchain, Docker
|
||||
build+push to `gitea.dooplex.hu/admin/`.
|
||||
|
||||
| Host | Access | Use | Blast radius |
|
||||
|---|---|---|---|
|
||||
| **DooPlex (this host)** | local — Debian 13, `kisfenyo`, `/mnt/5_hdd/felhom.eu/` | build/push images, `sudo kubectl`, build+run the agent for tests | **Tier 2 — precious.** It *is* the recovery chain (hub, Gitea, registry, PBS, k3s+Longhorn). **Never a drill target** |
|
||||
| Demo Proxmox host `demo-hp` (HP t740) | `ssh demo-hp` (tailnet `100.76.96.79`; **no baked key** — G1 break-glass password vaulted in the hub) | **the designated drill + build VM host** (operator ruling 2026-07-25) | **Tier 0 — disposable. Reach here first** |
|
||||
| Demo Proxmox host `demo-felhom` (N100) | `ssh felhom-pve` (root, no sudo; tailnet `100.70.170.35`) | pveum/pct + live Proxmox validation | **Tier 0 — disposable** |
|
||||
| Demo guest 9201 | `ssh felhom-pve "pct exec 9201 -- ..."` | the live demo controller | Tier 0 (rides its host) |
|
||||
| felhotest (legacy) | `ssh -p 33022 kisfenyo@router.abonet.hu` — **`Connection refused` 2026-07-30** | OLD /opt/docker compose mechanism | untiered — assume nothing |
|
||||
**Host addresses, routes, break-glass and per-node facts:**
|
||||
`felhom.eu/documentation/operations/nodes.md` — the single home. Do not restate them elsewhere.
|
||||
|
||||
**Which box do I break?** → **`felhom.eu/documentation/runbooks/target-selection.md`** — the tiers, and
|
||||
**Which box do I break?** → `felhom.eu/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. **A task that needs a victim names
|
||||
one; an absent fence is not permission.**
|
||||
picking a machine for a drill, a destructive test or a throwaway VM. **A task that needs a victim
|
||||
names one; an absent fence is not permission.** DooPlex is **Tier 2 — precious**: it *is* the recovery
|
||||
chain, and never a drill target.
|
||||
|
||||
**Component versions are not recorded in any inventory doc** — agent/controller/hub versions change
|
||||
several times a day and the fleet is not uniform. Ask the hub's `/hosts` + `/configs`, or
|
||||
`felhom-agent --version` / `pct exec <vmid> -- docker ps` on the box.
|
||||
|
||||
The demo Proxmox host key changes on reprovision (N100) → refresh with
|
||||
`ssh-keygen -R 192.168.0.162` then connect with `-o StrictHostKeyChecking=accept-new`
|
||||
(`ssh-keyscan` hangs — avoid it).
|
||||
## Memory
|
||||
|
||||
## Legacy: Windows workstation
|
||||
Project memory lives at `/mnt/5_hdd/felhom.eu/git/.claude-memory/`, surfaced via
|
||||
`~/.claude/projects/-mnt-5-hdd-felhom-eu-git/memory` (symlink); `MEMORY.md` is the index. Memories
|
||||
reflect what was true when written — **verify a named file/flag still exists before acting on it.**
|
||||
|
||||
Kept so the old environment can be revived; **not the current setup**.
|
||||
|
||||
- Repos were in `E:\git\` (`/e/git/` in Git Bash); this file lived at `E:\git\CLAUDE.md`.
|
||||
- **SSH binary had to be** `SSH=/c/Windows/System32/OpenSSH/ssh.exe` — Git Bash's `/usr/bin/ssh`
|
||||
lacks access to the Windows SSH Agent and fails silently. Every remote command was
|
||||
`$SSH kisfenyo@192.168.0.180 "..."`; details in `felhom-controller/docs/vscode-ssh-fix.md`.
|
||||
- `pct exec` over SSH needed `export MSYS_NO_PATHCONV=1` (MSYS mangled `/`-paths).
|
||||
- Agent deploy was a two-hop copy: build on 180 → `scp` to the Windows box (local path needed
|
||||
`cygpath -w`) → `scp` on to felhom-pve. Beware CRLF when scp-ing config files through Windows.
|
||||
- Skills were installed as Windows junctions (`mklink /J`) rather than POSIX symlinks.
|
||||
- `claude-in-chrome` browser automation WAS available there (attaching only to sessions started
|
||||
after the bridge connected).
|
||||
|
||||
### Presence is not success
|
||||
## 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.
|
||||
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".
|
||||
|
||||
| # | instance | what happened |
|
||||
|---|---|---|
|
||||
| 1 | **F-CRIT-2** | a phantom snapshot's ctime set tier freshness — an aborted 1-byte upload made the tier look backed up |
|
||||
| 2 | **R-100** | `LastRun` is written on failure, so a nightly-failing offsite tier kept the staleness clock fresh forever |
|
||||
**Corollary:** when a verdict changes which field it counts from, the alarm text has to change with
|
||||
it. Leaving the message reading `last run 8h ago` while alarming on a six-day-old success turns a true
|
||||
alarm into one the operator dismisses.
|
||||
|
||||
Both were found by asking of a timestamp: *what exactly must have happened for this to be set?* If the
|
||||
answer is "we tried", it cannot answer "did it work".
|
||||
<!--
|
||||
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. Both found by asking of a timestamp what must have happened
|
||||
for it to be set.
|
||||
-->
|
||||
|
||||
Corollary, from R-100's fix: when a verdict changes which field it counts from, **the alarm text has to
|
||||
change with it**. Leaving the message reading `last run 8h ago` while alarming on a six-day-old success
|
||||
turns a true alarm into one the operator dismisses.
|
||||
|
||||
### A comment asserting an invariant needs a test pinning it, or it is a wish
|
||||
## A comment asserting an invariant needs a test pinning it, or it is a wish
|
||||
|
||||
**Nine instances in this project have shipped guarantees the code did not provide** — each survived
|
||||
review because the comment read as settled:
|
||||
|
||||
| # | Comment | What it claimed | What the code did |
|
||||
|---|---|---|---|
|
||||
| 1 | `EffectiveProtected` | a stack was protected | it was not — the samba false alarm |
|
||||
| 2 | `newestArchiveOn` | *"errors degrade to unknown, never to no-backup"* | the `(time,bool)` signature made that impossible (R-88 Part 2) |
|
||||
| 3 | R-97a operator-only | the event *"cannot be routed to a customer"* | only configuration stopped it; fixed by a real `operatorOnlyEvents` register |
|
||||
| 4 | `classifyRunStates` I1 | *"StateStopped means deliberately stopped by the user"* | quiesce stops stacks the same way — a failed restart was silent (F-CRIT-1) |
|
||||
| 5 | `inflight.go` | *"a caller that cannot acquire DEFERS"* | the backup caller recorded a failure and paged the operator (F-A1) |
|
||||
| 6 | `quiesce.go` | the agent's 409 *prevents* "a spurious failure" | on the start path it produced one (F-A1) |
|
||||
| 7 | `recovery_unit.go` B2 refusal (R-181) | *"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 |
|
||||
| 8 | `ResolveManagedFloor` (R-216) | *"never push a controller past the agent it depends on"* | it 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. Measured live 2026-08-05: golden 0.192.0/MinAgent 0.113.0, floor 0.200.0, agent 0.120.0 → served, and the box landed on a controller needing 0.125.0. Its customer was then told their correct recovery code was wrong. **The first entry in this table where the false invariant was a GUARD, not a comment alone.** Fixed hub v0.97.0: a floor above the vouched golden is HELD, with its own reason |
|
||||
| 9 | `escrow/recover.go` header (R-224) | *"The errors below are DISTINCT on purpose"*, naming **three** situations | there were **four**. A failed FETCH was wrapped as an anonymous error and fell through the local-api handler's `default` into the wrong-code answer, so a hub that could not be reached was reported to the customer as a bad recovery code. 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. **AND A GREEN TEST NAMED IT AND DID NOT PREVENT IT:** `TestRecoverOffsiteRepoPassword_FetchErrorIsDistinct` has said since v0.125.0 that *"the operator must not be sent to re-read their recovery code because the hub was unreachable"* — it asserted this package's error **string**, one layer below where the merge happened, and a string is not something a caller can branch on. **Mechanism asserted, consequence unpinned.** Fixed agent v0.126.0 (`ErrBundleFetch` → HTTP 502) + controller v0.202.0 (classify by value; the typing message reachable from ONE class; unknown → neutral) |
|
||||
|
||||
Three of these (4, 5/6 and 7) were found **on live hardware**, not by review or unit tests — #4 had a
|
||||
green, red-proofed test suite over a production path that was broken two independent ways, and #7
|
||||
survived a full green suite plus three of its own red-proofs, because every one of them asserted the
|
||||
mechanism inside `captureAllRecoveryUnits` and none asserted the **consequence** across the whole
|
||||
backup run. The test that would have caught it is the one #7's fix ships: fingerprint the tree before
|
||||
and after, and compare. So:
|
||||
review because the comment read as settled, and three were caught only on live hardware. The case
|
||||
table is in the **`felhom-testing`** skill, which loads when you write or review a test, harden a
|
||||
guard, or fix a bug.
|
||||
|
||||
- If a comment states an invariant, **name the test that pins it**, or write one.
|
||||
- If an invariant has a stated dependency (*"if either invariant changes, revisit this"*), that is
|
||||
not a safeguard — nobody revisits. Pin it with a test that fails when the dependency moves.
|
||||
- If an invariant has a stated dependency (*"if either invariant changes, revisit this"*), that is not
|
||||
a safeguard — nobody revisits. Pin it with a test that fails when the dependency moves.
|
||||
- Prefer a test that asserts the **consequence** (does the alarm fire?) over one that asserts the
|
||||
**mechanism** (does suppression expire?). R-97b's Scenario F proved the mechanism and the
|
||||
consequence was still broken.
|
||||
|
||||
<!--
|
||||
LEGACY: WINDOWS WORKSTATION — kept so the old environment can be revived; not the current setup.
|
||||
- Repos were in E:\git\ (/e/git/ in Git Bash); this file lived at E:\git\CLAUDE.md.
|
||||
- SSH binary had to be SSH=/c/Windows/System32/OpenSSH/ssh.exe — Git Bash's /usr/bin/ssh lacks
|
||||
access to the Windows SSH Agent and fails silently. Every remote command was
|
||||
$SSH kisfenyo@192.168.0.180 "..."; details in felhom-controller/docs/vscode-ssh-fix.md.
|
||||
- pct exec over SSH needed export MSYS_NO_PATHCONV=1 (MSYS mangled /-paths).
|
||||
- Agent deploy was a two-hop copy: build on 180 -> scp to the Windows box (local path needed
|
||||
cygpath -w) -> scp on to felhom-pve. Beware CRLF when scp-ing config files through Windows.
|
||||
- Skills were installed as Windows junctions (mklink /J) rather than POSIX symlinks.
|
||||
- claude-in-chrome browser automation WAS available there (attaching only to sessions started after
|
||||
the bridge connected).
|
||||
|
||||
THIS FILE'S SHAPE (2026-08-06, instruction-trim task): core + path-scoped rules. Removed here and
|
||||
rehomed, not lost — the per-repo guidance list (those files load on their own), the skills roster
|
||||
(already resident in the skill listing), the host table (nodes.md is the single home), the "(119
|
||||
files)" memory count (derivable and wrong — 158), and the nine-row invariant table (felhom-testing
|
||||
skill). Full accounting: felhom.eu/documentation/audits/LEDGER-instruction-trim-2026-08-06.md
|
||||
An HTML comment is invisible to Claude and costs no context — verified 2026-08-06 with a control
|
||||
(both markers plain -> both seen) and a treatment (one marker commented -> not seen), twice.
|
||||
-->
|
||||
|
||||
Reference in New Issue
Block a user