92a076c239
gates / gates (push) Successful in 8s
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.
308 lines
19 KiB
Markdown
308 lines
19 KiB
Markdown
# 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:**
|
|
|
|
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.
|