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.
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.html → website.md path_glob_match, no hub.md; reading hub/internal/api/handler.go → hub.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 discipline — OPEN-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.1 — nodes.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:
- The destination
/mnt/5_hdd/backupis on the same physical disk (sda1) as the store. This protects against deletion, not against disk loss. - The DooPlex backup set has no off-site leg.
sync-hetzner-backups.shis a jarrs.eu job that pulls from Hetzner to DooPlex — the opposite direction. /opt/backup/scripts/is itself unversioned host state (R-231) — the change exists only on the box, transcribed intoworkspace/README.mdso 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:
- 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.
- 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
target-selection.mdt740 error — still present, carried from part 1 §L.1.felhom-agent/CLAUDE.mdat 173 effective lines — passing with the least headroom; R-229(b).- The
/contextMessages-accounting inference — see §A row 3. Not built on. rules_report.pyfirst run: 6 of 9 rule files had never fired. Not a defect list — the log only covers since the hook was armed, andfelhom-controller/gates.mdis proof of the distortion: it fired at 08:11 the same day, before installation, and reads as silent.- The other session's read of
OPEN-ITEMS.mdfireddocs.md— independent third-party confirmation the new rules are live, from a session that knew nothing about them. 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.