Files
felhom.eu/documentation/audits/LEDGER-instruction-trim-part2-2026-08-06.md
T
admin 92a076c239
gates / gates (push) Successful in 8s
docs: backup verified IN the repository — snapshot b587f775, 118/118 + 40/40 (R-229)
58,158 files / 405.865 GiB in 27:56; 994 MiB added, 77 MiB stored -- the one-time full re-read
cost I/O, not storage, as predicted.

Also records a false alarm I raised against my own instrument: `restic ls <snapshot> <path>`
reported 0 of 40 archived memories because restic 0.18.0's path filter does not recurse into
subdirectories. The unfiltered listing shows all 40. An instrument that can drop results silently
is not a measurement -- and this one was measuring a safety property.
2026-08-06 11:22:34 +02:00

19 KiB

LEDGER — instruction trim, part 2 (felhom.eu, the memory index, the versioned workspace)

Companion to LEDGER-instruction-trim-2026-08-06.md, which this closes legs (a) and (c) of. Same per-block format; destinations are rule-file, html-comment, already-in:<path>, deleted-derivable, deleted-stale, kept-in-core.

Baselines (verified against live Gitea, all clean, all HEAD == origin/main): felhom.eu c21bcf84f709, felhom-controller 7db42c5fec3b, felhom-agent 062a7027abff.

Concurrency: a second Claude Code session (f192bf03, started 10:05) was live in these repos throughout, doing qm/ssh demo-hp console work. Managed rather than waited out: clean-tree and origin/main re-checked immediately before every commit, explicit-path staging only, pull --rebase before every push, and the memory store snapshotted before edit and re-diffed after. Per this repo's own rule for parallel sessions, this run wrote REPORT-instruction-trim-part2-2026-08-06.md and did not touch the shared REPORT.md.


A. Mechanism verification — done before relying on any of it

Claim Verdict Evidence
.claude/rules/*.md + paths: loads only on a matching file read confirmed, both directions hook log, two fresh sessions: reading website/index.htmlwebsite.md path_glob_match, no hub.md; reading hub/internal/api/handler.gohub.md path_glob_match, no website.md
Creating .claude/rules/ mid-session arms it DISPROVEN after writing the four rule files, in-session reads of website/index.html produced no hook line at all. A directory whose instructions were already seeded is not re-scanned — same class as the settings-watcher caveat. This is why the proof above needed claude -p
On-demand rules are accounted under Messages in /context still inference, not measured two /context all readings: Memory-files stayed at exactly 11.5k across three rule loads while Messages grew 8 → 105.7k. That shows rules are not counted as memory files; it does not show where they are. Recorded as reasoned, per spec §3

The second row is the load-bearing one and it is a negative result about the instrument: a rule file can be correct, pass every gate, and reach the model never, purely because of when it was created. Nothing in the gate or the frontmatter would have shown that.


B. felhom.eu/CLAUDE.md — 227 → 115 effective lines

Heading / first words Class Destination Reason
## The Felhom system three-component model (7 ln) duplicated already-in:CLAUDE.md (workspace root) the root always loads and survives /compact; restating it here is a second place to edit
## Hub — architecture ingest paragraph (11 ln) path-bound rule-file:hub.md only matters when hub/ is being edited; the frozen-contract warning went with it
## Hub stack — the two constraints (7 ln) path-bound rule-file:hub.md same trigger
## Build & deploy — Hub (GitOps) (25 ln) path-bound rule-file:hub.md the clean-tree gate inside it was already-in:CLAUDE.md (workspace root) and dropped here
## Key patterns (8 ln) path-bound rule-file:hub.md status logic, parseSQLiteTime, auto-refresh — all hub internals
seam-wiring / template-gates rule (4 ln) path-bound rule-file:hub.md it is about html/template gates, which exist only in hub/
health-check block-I/O rule (5 ln) path-bound rule-file:hub.md deliberately duplicated into three repos' rule files — a pointer to a sibling repo's rule does not load
logging levels/phrasing (3 ln) path-bound rule-file:hub.md applies when writing Go
website deploy + BOM + emoji + cache-busting (10 ln) path-bound rule-file:website.md
ArgoCD / deliberate sync / secretKeyRef (12 ln) path-bound rule-file:manifests.md
doc routing, one-home-per-fact, N.5 coupling (part of the checklist) path-bound rule-file:docs.md
## Project overview per-directory list (14 ln) derivable compressed to 6 lines + retrieval map the tree says what the directories are; what it cannot say is which one is authoritative for the whole system — that was kept
artifact taxonomy, TASK.md/RUNBOOK-*.md (10 ln) duplicated already-in:CLAUDE.md (workspace root) the root defines the taxonomy for all four repos
browser-automation / claude-in-chrome block (6 ln) duplicated already-in:CLAUDE.md (workspace root) root's "Live validation — no browser here"
## Environment & access DooPlex + host pointers (22 ln) duplicated already-in:CLAUDE.md (workspace root) + nodes.md replaced by two rows in the retrieval map
Legacy-Windows HTML comment (5 ln) archival deleted-derivable the root carries the fuller version in its own comment
## Build & deploy — Website/Manifests R-110 installer block (17 ln) fence kept-in-core, compressed to 6 see §C
## Gates — ONE entry point (25 ln) entry point kept-in-core, compressed §6 names it as core; the R-29 narrative stayed an HTML comment (free)
end-of-session checklist (18 ln) register discipline kept-in-core see §C

Kept deliberately: the gate entry point, the end-of-session checklist, register discipline, the R-110 fence, the git add -A prohibition, the parallel-session REPORT.md rule, the go test -run false-green rule, and "ask first — don't guess".


C. Two placements that deviate from the spec's §6 sketch — and why

The spec fixed the rule-file names and globs and left line-by-line placement to the implementer. Two blocks were kept in the core that §6's sketch would have relocated. Both would otherwise have rebuilt the exact failure class they exist to prevent.

Block §6 would put it in Kept in core because
Register disciplineOPEN-ITEMS.md first, STATUS.md is a view, nothing exists only in a report docs.md (paths: documentation/**) it applies at the end of every session that ships, breaks or decides something. Behind that glob it is invisible in precisely the code-shipping sessions where findings get lost — which is how R-153/154/155 and R-156/157 were minted and lost. A rule that loads only when you are already writing documentation cannot remind you to write documentation
The R-110 installer fence — pushing felhom-host-install.sh publishes nothing; publishing is a tag move website.md (paths: website/**) its trigger is editing scripts/felhom-host-install.sh, which none of the four fixed globs matches. 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

docs.md states the first of these explicitly, so a reader who expects the rule there is told where it went and why.


D. The memory store — reconciled, nothing deleted

Backup first (ruling 2): /mnt/5_hdd/felhom.eu/backups/claude-memory-20260806-103418, 158 files, taken before any edit.

Measure Before After
.md files total 158 158 — equal, zero deletions
top-level topic files 157 117
archive/ 40
indexed 113 117 (all of them)
orphaned 44 0
dangling references 0 0
MEMORY.md 145 ln / 17,688 B 150 ln / 17,977 B

The discriminator was the store's own schema, not taste. All 4 reference-type orphans are durable pointers and were indexed; all 39 project-type orphans plus 1 untyped are dated episode records — "SHIPPED", "spike GO", "FIXED in vX.Y.Z" — whose conclusions already live in CHANGELOGs and documentation/audits/, and were archived. Each of the four also justifies indexing on content, not merely type; hetzner-storagebox-api.md carries an open security constraint (the project-scoped Hetzner token can delete ep0 — move customer boxes to a dedicated project before the hub holds a write token).

Detail moved, not dropped. Seven index lines were shortened; for each, the removed detail was grep-verified present in the topic file first. One check came back negative (controller-internals.md did not contain the module path) — the line was trimmed anyway, caught on review, and the detail written into controller-internals.md. Recorded because the verification step is what caught it.

Headroom: 50 lines and 7,622 bytes (29%) against the 200-line / 25 KB limits.


E. Staleness diagnosis (Part 2.4) — diagnosed, deliberately not fixed

A ruling is owed before anyone edits Claude's own notes at scale. Filed as R-230(a).

Class Count Sharpest instance
component version literals 21 lines ctrl 0.162.0, hub 0.64/0.65, installer 1.15.0 — the exact literal class the gate now bans in every hand-written instruction file
bare host addresses 5 lines 100.70.170.35, 167.233.158.164, 10.77.0.1nodes.md is supposed to be the single home
expired temporal statements 3 "demo boxes REMOTE till ~08-02"
undated open items 2 RENOVATE_GITHUB_COM_TOKEN ROTATION DUE

The finding that matters is structural, not the counts. felhom-agent/CLAUDE.md had its expired TEMPORARY — felhom-pve is at a remote site (until ~2026-08-02) block deleted in part 1, and the gate now fails any such block. The same claim is still live in MEMORY.md — which loads in every session, is larger than the file the rule was written for, and is the one file whose content the gate does not police. The contradiction was moved, not resolved: the hand-written half is clean and the auto-written half still asserts the retired fact.


F. The gate — check 6, and why its three outcomes differ

Condition Outcome Reason
index over 200 lines or 25 KB FAIL auto-memory drops content past the limit with no error — silent truncation, no observable
top-level topic file unreferenced WARN the store is outside git and changes between sessions; a failure blocks pushes for something no commit can fix
store absent PASS, printing the reason machine-local by design; a clone elsewhere legitimately has none

The third is a deliberate exception to check 1's "a missing input is a FAILURE, never a skip". The printed reason is the only thing that made it safe to grant, and the test asserts on that text, not on rc == 0 — a pass with no reason is indistinguishable from a gate that stopped running.

Two load-bearing negatives: a file reachable only via [[wikilink]] is indexed, not orphaned (flagging it would push someone to add a duplicate row), and archive/ contents are never orphans.

Suite 20 → 39 assertions, 0 failures. Red-proof against the real store, not a fixture: ceiling 200 → 100, gate went red naming the real file and count (.claude-memory/MEMORY.md: 150 lines, ceiling 100), restored, green again.


G. The versioned workspace

Canonical path decision (§4.3): documentation/runbooks/workspace-CLAUDE.md stays the source. workspace/ carries only the hook fragment. A workspace/CLAUDE.md would be a third copy of a file whose whole problem is that copies drift, and check 5 already enforces byte-identity.

Symlink recommendation (§4.3): recommended, not done. Making the live root file a symlink to the versioned copy removes the divergence class outright, and Claude Code follows symlinks (the four skills already are). Not done here because it changes what check 5 asserts — identity of two files becomes correctness of one link — which deserves its own reviewable change. R-230(b).

Proofs. Merge preserved all 7 top-level settings.json keys byte-identically (sha256 of the file minus .hooks unchanged across the write). Run 2 wrote nothing — idempotent means "changed nothing the second time", not "ran twice without erroring", and only the second is evidence.

install_workspace.py refuses an unparseable settings.json rather than overwriting it: a malformed settings file silently disables every setting in it, and overwriting would destroy whatever was mid-fix.


H. Backup coverage (§4.4) — and what it does not buy

Added to dooplex-backup.service's User Data component via CLAUDE_MEMORY_DIR. Positive observable, from the journal of the real unit:

Including Claude auto-memory store: /mnt/5_hdd/felhom.eu/git/.claude-memory
start backup on [/mnt/4_hdd/data /mnt/5_hdd/felhom.eu/git/.claude-memory]

Completed and verified in the repository, not just in the log. Snapshot b587f775 (2026-08-06 10:51:17), 58,158 files / 405.865 GiB in 27:56:

Files:       58158 new,     0 changed,     0 unmodified
Added to the repository: 994.132 MiB (77.440 MiB stored)
snapshot b587f775 saved
Check Result
snapshot lists the store path yes — alongside /mnt/4_hdd/data
.claude-memory entries in the snapshot 161
top-level .md (incl. the index) 118 / 118
archive/ .md 40 / 40
previous snapshot 511b7f9d (03:07) /mnt/4_hdd/data only — a clean before/after in one listing

77 MiB stored for a 405 GiB re-read confirms the dedup prediction: the one-time full read cost I/O, not storage.

A false alarm I raised against my own instrument, recorded because the class is the point. The first verification used restic ls <snapshot> <path> and reported 0 of 40 archive files — which read exactly like a real defect (40 memories silently unprotected). It was wrong: in restic 0.18.0 that path filter lists entries at the filtered path and does not recurse into its subdirectories. The unfiltered ls shows all 40. This is the project's own rule — an instrument that can drop results silently is not a measurement — caught on a measurement made to verify a safety property, which is the worst place for a silent drop. Same class as the | tail -5 census and the go test -run false green already in the register.

Three caveats, each of which makes this weaker than "backed up" sounds:

  1. The destination /mnt/5_hdd/backup is on the same physical disk (sda1) as the store. This protects against deletion, not against disk loss.
  2. The DooPlex backup set has no off-site leg. sync-hetzner-backups.sh is a jarrs.eu job that pulls from Hetzner to DooPlex — the opposite direction.
  3. /opt/backup/scripts/ is itself unversioned host state (R-231) — the change exists only on the box, transcribed into workspace/README.md so it is at least recorded.

One-time cost, so it is not later read as a fault: adding a path invalidates restic's parent-snapshot match, so the first run logs no parent snapshot found and re-reads all 405 GiB. Dedup keeps storage flat; later runs are incremental again.


I. Secrets scan of the memory store (§9.3)

Pattern set: token|password|passwd|secret|api[_-]?key|private[_-]?key|BEGIN .*PRIVATE|bearer|credential, case-insensitive, over all 158 files.

Measure Count
lines matching the keyword set 535 across 104 files
matching a key: value shape with a ≥16-char value 1 — and it is prose (scoped token: provision-into-pool, …)
private-key blocks 0

No credential values. Nothing from the store was committed regardless (ruling 2); the scan is the evidence the later decision needs, not a licence.


J. context7 (Part 5) — nothing written, by the rule

The operator's /mcp and /plugin output: plugin:context7:context7 is ✘ failed (ENOENT on npx -y @upstash/context7-mcp), and /mcp lists only the six Google auth stubs as available. Per §5, write nothing — instructions telling Claude to use a tool that does not exist are worse than none. Not written: the felhom-app-catalog SKILL.md line and the infrastructure rule-file line.

Why the Go code is not the use case either, recorded as §5 asks: one direct dependency in felhom-agent (golang.org/x/crypto), and the hub and controller are stdlib-only by standing rule. There is no third-party API surface here that a docs-retrieval tool would help with; the catalog's upstream images are the only plausible consumer, and there docker inspect still decides.


J2. A stale claim found by the checklist's own last item — corrected in all four repos

The end-of-session checklist says "confirm your own last push's CI run went green, by run ID." Doing that surfaced a contradiction in the file that had just been trimmed.

Four instruction files asserted that continuous integration was still owed (OPEN-ITEMS.md R-168). R-168 was CLOSED on 2026-08-02: .gitea/workflows/gates.yml re-runs each repo's gate entry point on every push and emails the operator on failure via Resend. This session's own commits produced runs 117 (success) and 118 against the real SHAs.

File Status
felhom.eu/CLAUDE.md carried forward into the trim by this session — corrected
felhom-controller/.claude/rules/gates.md from part 1 — corrected
felhom-agent/CLAUDE.md from part 1 — corrected
app-catalog-felhom.eu/CLAUDE.md older wording, same claim — corrected

felhom-agent/CLAUDE.md contradicted itself. Its release section already said "R-168 sends that failure by mail" while its gates section said CI was still owed — a contradiction inside a single instruction file, which is exactly the class the whole R-229 effort exists to find, sitting in a file part 1 had already gone through.

Two lessons, both about this task rather than about CI:

  1. A trim carries stale claims forward unless each is re-checked. Compressing 227 lines to 115 is a volume operation; it does not validate content. This sentence survived because it read as settled — the same reason the nine invariant comments in the project's own case table survived.
  2. The gate cannot catch this class. It checks length, version literals, expired TEMPORARY blocks, rule scoping and copy identity. "This register item is closed" is not mechanically checkable from the instruction file alone. The checklist item that did catch it is a human PULL check, and it only worked because it demands a run ID, not a memory.

No new register row: found, fixed, and pushed in the same session. REPORT.md was deliberately not overwritten in the three sibling repos — a one-line docs correction must not destroy the record of their last real implementation.


K. Observations — noticed, not acted on

  1. target-selection.md t740 error — still present, carried from part 1 §L.1.
  2. felhom-agent/CLAUDE.md at 173 effective lines — passing with the least headroom; R-229(b).
  3. The /context Messages-accounting inference — see §A row 3. Not built on.
  4. rules_report.py first run: 6 of 9 rule files had never fired. Not a defect list — the log only covers since the hook was armed, and felhom-controller/gates.md is proof of the distortion: it fired at 08:11 the same day, before installation, and reads as silent.
  5. The other session's read of OPEN-ITEMS.md fired docs.md — independent third-party confirmation the new rules are live, from a session that knew nothing about them.
  6. MEMORY.md's header still says "felhom-controller Project Memory" though it indexes all four repos plus the homelab. Cosmetic; left for the R-230(a) ruling rather than edited.