a0b50eb48f
gates / gates (push) Successful in 8s
Found by the checklist's own last item (confirm your push's CI run by run ID). Corrected here and in the three sibling repos. Ledger J2 records the two lessons: a trim is a VOLUME operation and carries stale claims forward unless each is re-checked, and the gate cannot catch this class -- "this register item is closed" is not mechanically checkable from the instruction file.
279 lines
18 KiB
Markdown
279 lines
18 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]
|
|
```
|
|
|
|
**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.
|