# LEDGER — instruction arc close-out (agent split, memory truth, checks 6-content and 7, symlink, t740, context7) Third and final part. Companions: `LEDGER-instruction-trim-2026-08-06.md`, `LEDGER-instruction-trim-part2-2026-08-06.md`. Same per-block format. **Baselines, reconfirmed live before editing, all clean and `HEAD == origin/main`:** `felhom.eu` `92a076c239b9`, `felhom-controller` `66d80efb9f20`, `felhom-agent` `5b2666e3a2ae`, `app-catalog-felhom.eu` `459766cb1639`. --- ## A. `felhom-agent/CLAUDE.md` — 175 → 99 effective lines Measured 175, not the spec's 173: the CI correction pushed in part 2 added two lines. | Heading / first words | Class | Destination | Reason | |---|---|---|---| | `## Layout (verified against the tree)` (27 ln) | derivable | `rule-file:*` + `deleted-derivable` | the tree and `REUSE.md` are its home — but the per-package annotations that were doing real work moved into the rule file for the area they describe, rather than being dropped as the controller's were | | `## Build / run` release table + R-115/R-186/R-188 narrative (34 ln) | duplicated | `already-in:` the `felhom-build-deploy` skill | the file already said "never hand-roll it" and then restated the table, which drifts from the script it describes | | UPID/`WaitTask`, privsep intersection, TLS pinning (6 ln) | path-bound | `rule-file:proxmox.md` | only bite when `internal/proxmox/**` is open | | destructive-op trap rows (4 ln, from `REUSE.md` §3) | path-bound | `rule-file:proxmox.md` | | | local-API scoping, nonce store, bind traps | path-bound | `rule-file:localapi.md` | | | PBS three laws, verify default, prune grant | path-bound | `rule-file:backup.md` | | | format-safety, durable-ID schemes, escrow errors | path-bound | `rule-file:storage.md` | | | `## Demo host` addresses (8 ln) | duplicated | `already-in:operations/nodes.md` | replaced by two rows in the retrieval map | | CHANGELOG/REPORT/secrets blockquote, code-quality line | duplicated | `already-in:CLAUDE.md` (workspace root) | | | artifact taxonomy, `TASK.md`/`RUNBOOK-*.md` (12 ln) | duplicated | `already-in:CLAUDE.md` (workspace root) | | **Kept in the core** — it is the only part re-injected after `/compact`: the **root-CLI fence and its three named exceptions**, the destructive-op gate, prove-ownership (audit A1), the gate entry point, the F9 live-validation fence, trunk-based with its revert-and-report escape hatch, and the checklist. **New:** `.claude/rules/{proxmox,localapi,backup,storage}.md`, all ≤46 effective lines. **Glob overlap, stated rather than silently resolved.** `health-checks.md` already matched `internal/{capability,storage,localapi,hub,guesthook}/**`, which `localapi.md` and `storage.md` now also match. Both rules load in those directories and neither supersedes the other; each new file says so in its own text, so a reader who sees two rules fire is not left guessing which wins. --- ## B. The memory index — three false statements, corrected **Backup first:** `/mnt/5_hdd/felhom.eu/backups/claude-memory-20260806-112853` (158 files). **The premise needed correcting before the work could be done, and the error was mine.** Part 2's report said "3 expired temporal statements", and this task inherited that number. Re-run with the cause printed, **all three matched the ISO date inside a markdown link TARGET** — a filename, not a claim — while the one real expired claim in the same file was written `~08-02`, carried no ISO date, and was never matched. A scan that reports the wrong three and misses the right one is worse than no scan. The three genuinely false statements, each established by evidence rather than by the scan: | Line | Claim | How it was falsified | |---|---|---| | 17 | `R-193 decision open` | its register row reads `**CLOSED 2026-08-05 — controller v0.200.0**` | | 34 | `demo boxes REMOTE till ~08-02` | live: `ssh felhom-pve` answers and holds `192.168.0.162/24` on `vmbr0` — the home LAN, same /24 as DooPlex | | 68 | `OPEN R-25b` | ROADMAP: `**SHIPPED hub v0.69.0 (2026-07-21)**`; capability map: `R-25b CLOSED` | Only the temporal claim was removed from line 34; its durable half — that `ssh felhom-pve` still resolves to the **tailnet** address while `felhom-pve-lan` is the LAN alias — was kept, because it is still true and still surprising. **Nothing else in the file was edited. No memory file was deleted. 158 files before and after.** The remaining 32 version literals and 4 host addresses stay, for the warning loop to work on — their survival is deliberate, not an oversight. --- ## C. Check 6 gains content WARNings; check 7 is new ### Why WARN and not FAIL for the index Claude writes `MEMORY.md` between sessions, so a hard failure would refuse a human's push over a line no human typed. The warning is read by the same model that will next edit the file, which makes the loop self-correcting rather than a chore for a person. ### Check 7 — a citation that calls a register item OPEN must be right **Deviation from the task's literal wording, stated because it is a real one.** The task says every citation of a non-open item must itself contain `CLOSED`. Applied literally that fires on ~30 legitimate provenance citations — `(R-161)`, `R-117 spike §6.3` — which cite an item as the **source of a fact**, not as outstanding work. A gate that noisy is switched off, which is precisely the fate R-29 documented. **The trigger is therefore an OPENNESS CLAIM**, which still convicts both real defects and keeps the spec's required pass case (`R-168, CLOSED 2026-08-02`) green. **Two bugs found by this check's own red-proofs, both of which would have shipped:** 1. **The state marker is not self-closing.** Real rows read `**SHIPPED — and the alarm is DEMONSTRATED**`. The first parser required `**SHIPPED**` and therefore read **R-168 — the row the whole check exists for — as OPEN**. A gate that cannot convict its own founding case is decoration. 2. **The CLOSED exemption was line-wide.** `- [Customer RESET shipped](customer-reset-…-2026-07-17.md) — …; OPEN R-25b` contains "shipped" twice, in a title and a filename, and the whole line was pardoned. Found **only because the red-proof failed to go red**, and now scoped to the citation's clause with a regression test. **Suite 39 → 68 assertions, 0 failures.** Red-proofs against real files, both directions, quoted in the report. **What check 7 finds across the four repos right now: nothing** — 19 citations, 0 failures. That is because part 2 already corrected the four stale sentences; the red-proof, not the zero, is what shows the check works. --- ## D. The workspace symlink (R-230(b)) — done, and PROVEN `/mnt/5_hdd/felhom.eu/git/CLAUDE.md` is now a **relative symlink** to `felhom.eu/documentation/runbooks/workspace-CLAUDE.md`. Backup of the pre-link file: `/mnt/5_hdd/felhom.eu/backups/workspace-CLAUDE.md.bak-20260806-113952`. **Check 5 now has two legal shapes and asserts a different thing in each:** | Shape | Asserted | Rationale | |---|---|---| | LINK | it points at the versioned copy **and resolves to a real file** | divergence is impossible; what can still break is the link. **A dangling link is worse than a diverged copy** — the instructions load NOTHING and there is no content left to notice is wrong | | COPY | byte-identical, as before | a clone elsewhere may legitimately have two files; the link is not forced on anyone | **Proven, not assumed** — three fresh sessions each logged `session_start … /mnt/5_hdd/felhom.eu/git/CLAUDE.md` (Claude Code reports the LINK path, not the resolved one). And because a logged path proves discovery rather than delivery, a fourth fresh session **with no tools at all** was asked to quote standing rule 1 and returned it verbatim. The content reaches the model. --- ## E. The t740 off-site claim — measured, then corrected `target-selection.md` said the PBS-DR / off-site tier is carried by the N100 and **"(demo-hp has none)"**, and used that as the reason to steer backup-disturbing tests at demo-felhom. **Measured on the box, from the tier's own evidence:** ``` demo-hp:/etc/pve/storage.cfg pbs: felhom-pbs -> datastore felhom-offsite, server 10.77.0.1, namespace demo-hp, username felhom@pbs!demo-hp demo-hp# LC_ALL=C pvesm list felhom-pbs felhom-pbs:backup/ct/9201/2026-07-28T19:19:45Z pbs-ct 6264034048 9201 felhom-pbs:backup/ct/9201/2026-08-04T19:24:16Z pbs-ct 4637840512 9201 ep0:/mnt/pbs-datastore/ns/ c11 demo-felhom demo-hp rewalk ``` Two snapshots, newest two days old, in demo-hp's **own namespace** on the shared off-site datastore. **The claim is false.** It was **true when written** and went stale when F10 resolved on 2026-07-23 — the first snapshot is 2026-07-28. Corrected, with the measurement kept in an HTML comment beside it. **Why this outranked a typo:** the sentence was load-bearing for "which machine may be destroyed". "Its backup chain is not disturbable" is exactly the belief under which a drill lands somewhere it should not. --- ## F. context7 — diagnosed, nothing written **There is no Node.js on DooPlex at all.** `npx`, `node` and `npm` are all absent from `PATH`, no `nodejs`/`npm` package is installed, and no runtime directory exists. The plugin manifest (`~/.claude/plugins/.../context7/.mcp.json`) is: ```json { "context7": { "command": "npx", "args": ["-y", "@upstash/context7-mcp"] } } ``` So the `ENOENT` is literal: the `npx` binary does not exist. The fix is one package install away — **and that decision is not this task's to make.** DooPlex is Tier 2 and *is* the recovery chain (hub, Gitea, registry, PBS, k3s+Longhorn); adding a language runtime to it for a docs-lookup server is its own change with its own review, not a rider on a documentation task. **Whether it is worth pursuing — measured, not repeated from the spec:** | Module | Direct third-party dependencies | |---|---| | `felhom-agent` | **1** (`golang.org/x/crypto`) | | `felhom.eu/hub` | **3** | | `felhom-controller` | **6** | ~8 distinct packages, all small, stable and well known. The plausible consumer is the app catalog's upstream images — and there the skill's existing trap already answers it: **a more authoritative-sounding source of guesses is still a source of guesses, and `docker inspect` decides.** Per §6, **nothing was written** to `felhom-app-catalog/SKILL.md` or to any rule file. --- ## G. Before / after | File | before (eff/B) | after (eff/B) | |---|---|---| | workspace root `CLAUDE.md` | 142 / 11,280 | **142 / 11,280 — now a SYMLINK, one file not two** | | `felhom.eu/CLAUDE.md` | 117 / 8,006 | 117 / 8,006 (untouched) | | `felhom-controller/CLAUDE.md` | 92 / 6,341 | 92 / 6,341 (untouched) | | **`felhom-agent/CLAUDE.md`** | **175 / 14,093** | **99 / 6,267** | | `app-catalog-felhom.eu/CLAUDE.md` | 80 / 6,397 | 80 / 6,397 (untouched) | | rule files, workspace-wide | 9 | **13**, every one `paths:`-scoped, all ≤60 effective | | `MEMORY.md` | 150 ln / 17,977 B | 150 ln / 18,078 B — 3 statements corrected | | gate suite | 39 assertions | **68** | Every `CLAUDE.md` in the workspace is now under 120 effective lines except the workspace root at 142, which is deliberate: it is the only file re-injected after `/compact`. --- ## H. Observations — noticed, not acted on 1. **R-129 has fresh evidence.** `target-selection.md` records that demo-hp has no baked SSH key and needs G1 break-glass, "but a key authenticated on 2026-07-31 — R-129, unresolved". **A key authenticated again today, 2026-08-06**, non-interactively (`BatchMode=yes`) from DooPlex — that is how the t740 evidence in §E was gathered. The row is still right to be open, and now has a second dated observation. 2. **`~/.ssh/config` carries the same expired vacation claim** the memory index did: *"demo-hp … at the VACATION site until ~2026-08-02, LAN 192.168.0.87 there"*. It is host state, outside every repo and outside this task's scope, but it is the same statement in a third place. 3. **The hub's operator UI did not answer over its ClusterIP** during this session (request hung, 2 min timeout) — the off-site evidence was gathered from the boxes instead. Not investigated; the memory note describing that access path may need re-checking. 4. **`MEMORY.md`'s header still reads "felhom-controller Project Memory"** though it indexes all four repos plus the homelab. Cosmetic, left for the R-230(a) ruling. 5. **`register_state` is now a reusable register parser** living in the gate. If anything else ever needs "is R-nnn open", it should call this rather than re-deriving it — the two parsing traps in §C are not obvious and were both found the hard way.