Commit Graph

1015 Commits

Author SHA1 Message Date
admin 5ca5082e7c docs: close out the instruction arc — t740 corrected on evidence, registers, ledger, S-37
gates / gates (push) Successful in 8s
target-selection.md said demo-hp has no off-site tier. Measured first: pvesm list felhom-pbs on the
box returns two snapshots in demo-hp's OWN namespace (2026-07-28, 2026-08-04) against ep0's
felhom-offsite. The claim was TRUE WHEN WRITTEN and went stale when F10 resolved 2026-07-23. The
measurement is kept in an HTML comment beside the corrected sentence. This file decides which
machine may be destroyed, so the sentence was load-bearing, not cosmetic.

R-229(b) CLOSED (agent 175 -> 99 eff). R-230(b) CLOSED (symlink, proven from fresh sessions).
R-230(a) part-actioned -- three false statements fixed, WARN loop added, bulk ruling still owed.

S-37: a claim in an instruction file is checked, not trusted.
2026-08-06 11:49:22 +02:00
admin f49b1f390b workspace: the root CLAUDE.md becomes a symlink; check 5 learns two shapes (R-230(b))
gates / gates (push) Successful in 7s
Two files kept identical by hand and one check is a divergence class policed forever; one file
reachable by two paths cannot diverge at all. install_workspace.py now links by default, MIGRATES
an existing regular file (backing it up first and SAYING SO if it differed -- that difference is
the last chance to notice an unsynced edit), and keeps --copy for a clone that wants the old shape.

Check 5 asserts a different thing per shape: for a link, that it points at the versioned copy and
resolves to a real file; for two files, byte-identity as before. A dangling link is worse than a
diverged copy -- the instructions load NOTHING and there is no content left to notice is wrong --
so that case is red-proofed.

NOT yet proven to LOAD: that needs a fresh session and a hook line, which is Phase 7. If it does
not load, this reverts to the copy.
2026-08-06 11:41:36 +02:00
admin 15fa5273ba gate: check 7 (register citations) + content WARNings on the memory index
gates / gates (push) Successful in 8s
Check 7 catches "cites a register item and calls it open when it is not" -- the R-168 class, four
files, one self-contradicting. Trigger is an openness CLAIM, not any citation: policing every
mention would fire on ~30 legitimate provenance citations and the gate would be switched off.
Deliberate deviation from the task's literal wording, to keep it alive.

Two bugs found by the check's own red-proofs, both of which would have shipped:
- the state marker is not self-closing (**SHIPPED - text**), so the first parser read R-168 itself
  as OPEN -- a gate that cannot convict its founding case is decoration;
- the CLOSED exemption was line-wide, so "shipped" in a title pardoned "OPEN R-25b".

Check 6 gains WARN-only content classes on MEMORY.md. Link targets are stripped first: the earlier
scan reported three expired statements, all three false (dates in filenames), while missing the one
real expired claim, whose deadline was written ~08-02 with no ISO date.

39 -> 60 assertions. All four runners green.
2026-08-06 11:39:34 +02:00
admin 92a076c239 docs: backup verified IN the repository — snapshot b587f775, 118/118 + 40/40 (R-229)
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.
2026-08-06 11:22:34 +02:00
admin 6a835ad9ec Re-walk R-201: Phase A record, written BEFORE anything is destroyed
gates / gates (push) Successful in 8s
The fixture is built and the gate passed: all three sentinels are in snapshot
a7bc23bd BY NAME and at the right sizes, and the accented filename survived
into restic intact.

Recorded because they are results, not incidentals:
- what a FRESH install actually landed on — controller 0.201.0, agent 0.125.0,
  golden 0.201.0. Neither carries the fixes. Brought to 0.202.0/0.126.0 BY HAND,
  and the delivery gap is recorded as owed: this proves the journey, NOT that a
  customer would receive that build.
- baseline drift caught on arrival: one documentation-only R-229 commit per repo,
  and the highest register ID is R-229, not the runbook's R-228.
- the runbook's installer baseline (1.25.0) is stale; the published artifact is
  1.26.1, verified byte-identical to demo-hp's local copy.
- credentials DISCOVERED not assumed: HUB_PW mapped and verified live; nothing
  else was needed from the file.

A harness fault worth keeping: the accented sentinel's filename was destroyed at
creation by my own base64->bash->pct exec chain, producing U+FFFD replacement
characters. A Python decode('utf-8') check called it VALID UTF-8, because U+FFFD
is valid UTF-8 — only a hex dump of the filename bytes exposed it. Rewritten
from explicit bytes inside Python on the guest.

R claimed one-shot, streamed file->file to a 0600 store on DooPlex, never
rendered, shape verified without printing (10 words, 75 chars). The runbook's
§5.5-vs-§9.4 tension about where R may live is flagged in the journal rather
than resolved silently.
2026-08-06 11:19:19 +02:00
admin 415371862d REUSE + STATUS: map the new scripts, correct a second stale "not wired" claim (R-229)
gates / gates (push) Successful in 8s
REUSE.md line 62 claimed hub_confirm_gate.py is "not wired to run (R-29)" -- it is gate 3 of
repo_gates.py, which both the pre-push hook and CI run. Second instance of the same stale-claim
class found today, in a third file.

Added rows for repo_gates.py (the runner), instructions_gate.py, install_workspace.py and
rules_report.py, plus the extension point: a new .claude/rules/*.md is NOT live in the session
that created it -- verify from a fresh session, never from the frontmatter.

STATUS.md: one operator-facing entry -- the assistant's notes had no backup; they do now, with
the same-disk and no-off-site caveats stated rather than implied.
2026-08-06 11:05:33 +02:00
admin a0b50eb48f docs: R-168 is CLOSED — correct the stale "CI is still owed" sentence carried into the trim (R-229)
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.
2026-08-06 11:03:40 +02:00
admin 9c10c5d600 docs: ledger, registers and standing ruling S-36 for the part-2 instruction/memory work (R-229)
gates / gates (push) Successful in 8s
R-229 legs (a) and (c) CLOSED. R-230 opened for what this deliberately left -- the ruling owed on
auto-written staleness, the symlink decision, the spec-as-failing-test pilot. R-231 opened for
/opt/backup/scripts/ being unversioned host state, found while adding the memory store to the
backup set: the same class this task was closing, inside the fix for it.

S-36: the auto-memory store is backed up, never committed; no memory file is ever deleted;
a rule file created mid-session is NOT live in that session.

Second session in this repo, so REPORT-<topic>.md per CLAUDE.md; shared REPORT.md untouched.
2026-08-06 11:01:13 +02:00
admin f65ea89a24 workspace: version the root CLAUDE.md + InstructionsLoaded hook, and report which rules fire (R-229)
gates / gates (push) Successful in 8s
install_workspace.py lays down the two things that shaped every session while existing on one
host only. Unlike install_skills.py the targets are LIVE CONFIG, so: timestamped backup before
every write, settings.json MERGED (this script owns exactly one key), a diverged CLAUDE.md
reported rather than silently resolved, and an unparseable settings.json refused outright.

Proven: all 7 top-level settings keys survived byte-identically, and run 2 wrote nothing.

rules_report.py surfaces the column that matters -- rules that have NEVER fired, which are
mis-globbed or dead. 6 of 9 on first run. The hook now self-rotates at 5 MB.

The memory store is BACKED UP, NOT COMMITTED (auto-written, may name hosts/paths): added to
dooplex-backup.service's User Data component. /opt/backup/scripts/ is itself unversioned host
state -- filed, not fixed here.
2026-08-06 10:57:01 +02:00
admin f27aed87cd gate: instructions_gate check 6 — the auto-memory index (R-229)
gates / gates (push) Successful in 15s
MEMORY.md is the larger half of what loads before a word is typed (8.4k tokens vs the root
CLAUDE.md's 6.6k) and is the one instruction file nobody hand-edits, so nothing was watching it.

Three deliberately different outcomes, each pinned by a test: over-ceiling FAILS (auto-memory
drops content past the limit with no error), an orphan WARNS (the store is outside git), and an
absent store PASSES while PRINTING its reason -- asserted on the reason text, because a pass with
no reason is indistinguishable from a gate that stopped running.

39 assertions (was 20). Red-proof run against the real store, not a fixture.
2026-08-06 10:48:59 +02:00
admin 3a9dd81e18 docs+gate: felhom.eu/CLAUDE.md becomes core + path-scoped rules; instructions gate registered (R-229)
gates / gates (push) Successful in 8s
227 -> 115 effective lines, split into .claude/rules/{hub,website,manifests,docs}.md, and
repo_gates.py gains gate 6. Trim first, register second: a registered-but-failing gate refuses
every push through the pre-push hook, which is why this repo -- the one that OWNS the gate --
was the only one not running it.

Register discipline and the R-110 installer fence deliberately stayed in the core; both have
triggers no fixed glob covers, and scoping them would have rebuilt the failure class they exist
to prevent.

Scoping proven from the InstructionsLoaded hook log in two fresh sessions, not from frontmatter.
2026-08-06 10:41:42 +02:00
admin c21bcf84f7 docs+gate: instruction files cannot silently regrow (R-229)
gates / gates (push) Successful in 7s
New shared scripts/instructions_gate.py, registered in controller_gates.py and
agent_gates.py, never copied into a sibling repo (the reuse_refs_check.py
precedent). 20 fixture tests, all asserting the effect: exit code AND that the
message names the file and the reason.

It is a consistency gate, not a budget gate, and the failure message says so. A
/context reading measured the instruction files at 15k tokens against 869k free in
a 1M window -- space is not the constraint, and a future reader must not re-derive
the wrong reason. The 200-line ceiling is adherence guidance; a file nobody can
hold in their head is where contradictions hide, and five were found here.

Checks run against effective text (HTML comments stripped, because they are
stripped before injection): the line ceiling; every .claude/rules/*.md declares
paths: or an explicit unconditional: true; no component version literal; no
TEMPORARY block carrying a past date; and the workspace-root CLAUDE.md is
byte-identical to its versioned copy -- the live file sits outside any git repo,
so that copy is its only version-controlled record.

Two traps recorded so they are not reintroduced: a bare \d+\.\d+\.\d+ matches the
first three octets of every IPv4 (the gate excludes dotted quads, or it fails on
192.168.0.180 in the agent's own file); and unconditional: true is NOT a Claude
Code feature but this project's own marker.

Workspace-root CLAUDE.md 208 -> 182 lines (142 effective), copy kept identical.
The nine-instance invariant table moved into the felhom-testing skill, which
triggers when writing or reviewing a test; all three directive bullets stayed in
the core. felhom.eu/CLAUDE.md got surgical corrections only and is knowingly still
over the ceiling at 227 effective lines -- closing it needs the restructure R-229
defers, said plainly rather than quietly absorbed.

CONTEXT.md gains standing ruling S-35. OPEN-ITEMS.md gains R-229.

Docs only -- no Go, no version bump, nothing built or deployed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJc8sAGRWmavP3rMtdpkr2
2026-08-06 09:38:52 +02:00
admin d30c2a51ed R-224..R-228 CLOSED: registers, capability map, campaign annotation, STATUS
gates / gates (push) Successful in 7s
Five closed in controller v0.202.0 + agent v0.126.0, each with its live or
red-proof evidence in the row. Five explicitly still open and named as such
rather than left to inference: R-214, R-220, R-221, R-213, R-202 — and R-220 is
flagged as currently worked around BY HAND on the campaign venue, which is the
only reason an app could be deployed there.

The capability map's recovery row STAYS FAIL and says why: fixes are not a
re-walk, nothing walked a customer end to end, and the customer-facing messages
were NOT re-driven live because /recovery correctly retires itself once the old
data is set aside — restoring that state is the reconfiguration the task forbade.

The campaign document is ANNOTATED, not rewritten: it records what was true when
it ran, and that is its value.

workspace-CLAUDE.md gains comment-vs-code entry 9 — the escrow header said the
errors were 'DISTINCT on purpose' and named THREE situations while a fourth was
folded into one of them, and a green test named the defect and did not prevent
it because it asserted a STRING one layer below the merge.

ROADMAP needed no collapse — it carries no rows for these IDs.
2026-08-06 08:34:04 +02:00
admin 453e4503a9 capability map: the recovery row stays FAIL — what Phases 2+4 add, and what they do not
gates / gates (push) Successful in 9s
Says explicitly that these faults are NOT a re-walk, so the row cannot go green
on them. What they add: the BACKUP promise strengthened (the offsite tier ran
itself at 04:15 on a twice-rebuilt box, snapshot_count 1->2; all five daily jobs
fired once; nothing on the must-not list fired), R-217 and R-215 proven live
under exactly their faults, and the set-aside proved not to delete (12 535 KB
byte-exact at the far end).

What they do NOT add: any progress on the JOURNEY. R-224 is Phase 1's headline
defect relocated from the version channel to the transport — a hub outage and a
stopped agent are both reported as a bad recovery code, in 0.056s and 0.030s,
with no unseal attempted. Plus R-225, R-226, R-228.

Also records that §4.1 is now MEASURED rather than deduced, and §4.2's positive
half still is not.
2026-08-06 04:33:27 +02:00
admin 9c1d05d360 CAMPAIGN-11: hygiene, what-did-not-run, venue end state, and the session report
gates / gates (push) Successful in 8s
The recovery codes are shredded with the plant->find->shred->fail-to-find
control the brief asks for, and THE CONTROL PAID FOR ITSELF ON ITS FIRST RUN:
it found the Phase 0 code in ~/.config/credentials as R_CAMPAIGN_11 — a copy
this session did not create and would never have looked for. Without it, a
'codes shredded' claim would have been false. That key was removed from the
shared file with a verified diff (every other line identical, nine keys intact)
and HUB_PW re-tested at hub:200.

Consequence stated plainly rather than left to be discovered:
/home/felhom-repo.orphaned-20260805 (12 535 KB, the three Phase 0 sentinels) is
now permanently unopenable — which is what the set-aside screen promises, and
teardown removes it anyway.

Venue left WORKING and said so: ONLINE, 4 containers healthy, backup target not
degraded, off-site on 2 snapshots. Two things a future session needs: the raw
/mnt/{adatok,mentes} mounts are deliberately left unmounted (R-220's
workaround), and the appliance root credential was shredded — re-fetch it from
the hub.

REPORT-campaign11-phase24.md rather than REPORT.md, per the repo's
parallel-session rule.

No product code changed. No version bumped.
2026-08-06 04:32:56 +02:00
admin df6081e60b CAMPAIGN-11 Phase 4: the soak passed, with two absences answered honestly
gates / gates (push) Successful in 8s
All five daily jobs fired exactly once and completed on time. The 04:15 offsite
run produced snapshot_count 1 -> 2, last_status ok, 24.9s — unprompted, on a box
rebuilt twice and set aside four hours earlier. Nothing on the must-not list
fired, including offsiteheal: that is R-218's NEGATIVE control passing (a box
with a target must not declare), which does not substitute for its positive
half, still owed.

Investigated and DISPROVED: tier2-backup completing in 118ms looked like a
silent no-op; the copy is real and on the backup drive (818.5 KB verified).

Correction to my own pre-registration: backup_run_digest is a TEST FILENAME, not
an event type. The real one is backup_run_failures, a failures digest whose
silence on a clean night is correct — reporting it as a miss would have been a
finding I invented. What survives: the offsite run emits no hub event while both
lesser tiers announce success; failures and staleness are covered elsewhere, so
it is a consistency wrinkle, recorded not filed.

Two absences answered rather than assumed: the restore-test's silence was
PRE-REGISTERED as correct (6h eval, 24h settle); the agent's whole-guest tier is
left explicitly UNRESOLVED because routine local-api calls are not logged at INFO
— a 5h search returns 0 on a box that demonstrably served such calls, so the
instrument cannot answer.

No product code changed.
2026-08-06 04:21:13 +02:00
admin 0f8c3cc848 STATUS: what the overnight stress test found, in plain words
gates / gates (push) Successful in 7s
Leads with what held — nothing lost a byte, the set-aside really is set aside
(verified 12.5 MB untouched at the far end), a wrong code refused three times
with nothing written and no lockout, the alarm fired and cleared itself.

Then the four new findings in operator language: R-224 (our own systems being
down is reported to the customer as a bad recovery code — the same lie as
yesterday through a different door, and the machine had not even tried the
code: three hundredths of a second against a real attempt's one second),
R-226 (a mistyping customer can no longer be told to check their typing),
R-225 (0 snapshots / 0 GB shown above a paragraph saying the store holds
backups), R-228 (the set-aside backups become invisible).

States explicitly that nothing was fixed last night, on purpose.
2026-08-06 00:01:51 +02:00
admin 95e2fba8ca CAMPAIGN-11 doc: invariants, RTO, teardown-owed, hygiene
gates / gates (push) Successful in 7s
Invariants across the phase boundaries: I1/I4(product)/I5/I7 held; I3 breached
twice (R-227's raw Bad Gateway, and R-220's refusal naming an impossible
action, reproduced live a third time); I6 breached twice (R-224, R-225). I2 is
recorded as untested rather than passed, because F10 could not be injected.
I4 was breached by the HARNESS, not the product — sourcing ~/.config/credentials
echoed two demo-box recovery codes into the transcript.

RTO unchanged and explicitly not re-measured: Phase 2's faults are not a
re-walk. Phase 1's 61 attended minutes stand; the unaided number stays
undefined.

Teardown recorded as OWED with all three layers named plus the off-site side —
including that the campaign's sub-account now holds TWO repos, the fresh one
and the 12 535 KB set-aside history, and that demo-felhom/demo-hp namespaces on
ep0 must not be touched.
2026-08-06 00:00:53 +02:00
admin 07998dcff2 CAMPAIGN-11 Phase 4: pre-registered expectations, both directions, before the window
gates / gates (push) Successful in 9s
Written BEFORE the soak so the comparison cannot be fitted afterwards. Sources
are the box's own [scheduler] Registered lines and the agent's backup tier armed
lines, read at 23:56.

MUST happen: db-dump 02:30, tier2-backup + fill-watch 03:30, metrics-prune
04:00, offbox-backup 04:15 (snapshot_count 1 -> 2), a backup_run_digest event,
reports every ~15m.
MUST NOT happen: host_stale/host_recovered, offsite_selfheal_restaged (this is
also R-218's negative control — a box WITH a target must not declare),
offbox_repo_orphaned, offsite_repo_key_changed, escrow blob SERVED, any
controller self-update.
Genuinely uncertain and recorded as such: the restore-test has a 24h settle and
every archive here is hours old, so NOT running is correct, not a gap.
2026-08-05 23:59:19 +02:00
admin ce6fafa14f CAMPAIGN-11: R-228 (set-aside history invisible) + campaign doc F7/F9/F10/F11 sections
The move-aside is correct and verified byte-for-byte; what follows it is not.
settings.json records orphaned_renamed_to and OrphanedRenamedTo has zero
references in any template or handler — 12.5 MB of deliberately retained
customer data at a path the box knows and never shows. Fifth instance of the
project's seam-built-but-never-wired pattern.

Campaign document gains the F7/F9/F10/F11 write-ups: F7 split (set-aside PASS,
afterwards FAIL), F9 partial (R-215's gate proven live on a narrower shape),
F10 NOT INJECTED (harness — three self-healing attempts), F11 PASS.

No product code changed.
2026-08-05 23:59:09 +02:00
admin f88faad50b CAMPAIGN-11 Phase 2 complete: F7/F9/F10/F11, and R-228 (the set-aside history is invisible)
gates / gates (push) Successful in 8s
F11 PASS  ok->stale->ok, an operator mail each way, customer mail correctly
          pairing-gated. DOWN (>1h) not reached — stated, not implied.
F7  the set-aside PASSES decisively: move-aside to
          /home/felhom-repo.orphaned-20260805, mtime and du -s (12535 KB) and
          snapshot f3d9cd67 all unchanged; a fresh empty repo beside it.
          NOTHING DELETED.
    the afterwards FAILS -> R-228: settings.json records
          "orphaned_renamed_to" and OrphanedRenamedTo has ZERO references in
          any template or handler. 12.5 MB of retained customer data at a path
          the box knows and never shows. Seam built, never wired.
F9  R-215's fix PROVEN LIVE on a narrower shape: with recoveryOffer() false,
          GET /recovery now 302s instead of rendering its story. The literal
          never-had-offsite precondition was not staged (it needs a rebuild,
          which the brief forbids before Phase 4) — recorded as partial.
F10 NOT INJECTED, harness not product. Three attempts, each with a control:
          the app, then the controller's monitor, then the run itself recreate
          the mandatory directory within ~1 s. Kept as an observation: at
          capture the directory held only a recreated metadata.db and NOT the
          customer's file, and the run still said ok.

Also reproduced live for a third time: R-220 (candidates empty; unmounting only
the raw mounts flips both drives back) — still OPEN.

Venue left WORKING: 4 containers healthy, backup target not degraded, off-site
fresh repo last_status ok, escrow escrowed.
No product code changed.
2026-08-05 23:55:52 +02:00
admin 259f48aedc OPEN-ITEMS: CAMPAIGN-11 Phase 2 findings R-224..R-227, and what Phase 2 recorded against existing rows
gates / gates (push) Successful in 7s
R-224  every non-code failure on the unlock path is reported as a statement
       about the customer's code. F3 (hub REJECTed) and F4 (agent stopped) both
       returned M4 for a CORRECT current code, in 0.056 s and 0.030 s — neither
       compatible with a real unseal (~1.0 s of scrypt). The agent's err field
       distinguishes fetch-failure from unseal-failure and both become one HTTP
       400; the controller has no reach-failure branch. The vocabulary exists
       and is wired to agentClient() construction, not to the RPC. R-216's gate
       answers source=version and cannot see reachability.
R-225  the store reports 0 snapshots / 0 GB when it cannot read it, above a
       card saying it holds backups. Measured over SFTP: snapshot f3d9cd67 and
       12 535 KB are really there. snapshot_count/repo_size_bytes are ABSENT
       from settings.json after a rebuild — unknown renders as zero.
R-226  M1, the only message that tells a customer to check their typing, is
       unreachable on any box that has re-escrowed (M4's branch precedes it).
R-227  a restart mid-unlock returns a raw English "Bad Gateway" (I3).

Recorded against existing rows: R-216 §4.1 now MEASURED twice (and a correction
— SetFloor's DEBUG line can never reach the debug ring, so a restart alone
would not have produced it); R-218's live half still NOT measured, deliberately,
because the venue has a target; R-217 and R-215's fixes HELD under their faults.
R-199's back-pointer was already present in the capability map — the brief's
assumption that it was owed is stale.

No product code changed.
2026-08-05 23:28:54 +02:00
admin 04ac465da6 CAMPAIGN-11 Phases 2+4: the fault journal, the campaign document, and hub v0.97.1's missing heading
gates / gates (push) Successful in 8s
Phase 2 (eleven injected faults) and the §4 positives that were owed.

- §4.1 MEASURED, twice: the box's rendered GetFloor() is 0.200.0, and a
  cold-started controller logs "settle-gate: GO — at/above floor 0.200.0"
  against the same line reading "floor still unknown" while the hold was in
  force. Also corrects the brief's plan: SetFloor's line is u.dbg(), gated on
  logging.level=debug and written to the logger, so it can NEVER reach the
  debug ring — a restart alone would not have produced it.
- §4.2 still NOT measured, deliberately: the venue has an off-site target, so
  needsOffsiteCredential correctly returns false. Recorded, not inferred from
  the unit test.
- F1 PARTIAL, F2 PASS, F3 FAIL, F4 FAIL, F5 PASS, F6 PASS, F8 PARTIAL.
  F3+F4: a hub outage and a stopped agent are both rendered as "this code does
  not open your package", in 0.056 s and 0.030 s — no unseal attempted. The
  agent's own err field distinguishes them exactly and it is discarded at the
  HTTP boundary; the R-216 capability gate answers source=version and cannot
  see reachability.
- R-217's fix HOLDS under exactly its fault (F5), verified with the false-claim
  strings absent and accented positive controls present.

hub/CHANGELOG.md: v0.97.1 had no heading of its own — the change was written
into the v0.97.0 entry while the deployed tag is 0.97.1. Given its own entry,
marked as added retroactively. Second occurrence of the class (agent 0.90.1).

Evidence: documentation/tests/campaign11-evidence-2026-08-05/journal-phase24.md
No product code changed.
2026-08-05 23:13:47 +02:00
admin 3a539ea530 docs: R-223 CLOSED by a golden rebake — and it was never a dropdown
gates / gates (push) Successful in 11s
The first vouch of agent 0.125.0 was REFUSED by R-120's gate: the artifacts form saves as
a unit, and golden 0.192.0 was older than the newest controller the fleet reports. The
golden had been stale since before controller 0.193.0, so the Day-0 manifest had been
effectively unvouchable for days and nobody had cause to notice. Correcting STATUS.md and
OPEN-ITEMS, which said "one dropdown".

Golden 0.201.0 baked in the drill VM, round-trip verified from Gitea
(e730d7cab343eb35...f007654, 658165766 B), teardown clean, 0 token hits in the saved log.
Manifest now agent=0.125.0 golden=0.201.0 min_agent=0.125.0, read back after the save.

The held floor: the HOLD is established positively (hub log + the box's own "floor still
unknown"). Its RELEASE is not — no HELD line and no dashboard reason are both absences,
the served path logs nothing by design, and the box-side positive needs a restart the
venue cannot take before Phase 2. Recorded as deduction plus a pinned test, not a live
positive.
2026-08-05 22:29:30 +02:00
admin 1a0f7db92f docs: CAMPAIGN-11 — the journey FAILED, R-198's retention PROVEN, six findings fixed
gates / gates (push) Successful in 8s
Registers and evidence for the campaign and its fix pass.

OPEN-ITEMS: R-214..R-223. Six SHIPPED (R-215/216/217/218/219/222); three deliberately
still open and each blocks a real flow (R-214 console banner, R-220 drives unenrollable
after a rebuild, R-221 a rebuilt box cannot run the escrow ceremony); R-223 minted and
WAITING-ON-OPERATOR (vouch agent 0.125.0). R-213 and R-202 untouched.

Capability map: a new row for the customer's UNAIDED journey, recorded FAILED and staying
failed until a re-walk passes — fixes are not a journey. The existing rebuild row is
corrected where it said R-198's retention was unit-proven only: it was proven in production
on the first supersession since the fix, identity_blob retained at 572 B byte-length exact.

CLAUDE.md comment-vs-code table: eighth entry — ResolveManagedFloor, the first where the
false invariant was a GUARD rather than a comment alone.

STATUS: the headline is now "the backup promise is proved, the recovery journey is not",
and the one thing waiting on the operator.
2026-08-05 18:04:13 +02:00
admin 79e31ac24b manifests: hub 0.97.0 -> 0.97.1 (held-floor reason matches its cause)
gates / gates (push) Successful in 9s
2026-08-05 17:54:33 +02:00
admin a7f1d277b1 hub: the held-floor REASON must match the CAUSE (CAMPAIGN-11 follow-on)
gates / gates (push) Successful in 7s
v0.97.0 introduced a second hold reason and left both surfaces printing the first. The
freshly deployed hub logged, for the campaign box:

  managed floor HELD for c11: agent "0.125.0" < MinAgent 0.113.0

which is FALSE — 0.125.0 is ABOVE 0.113.0. That box is held because its floor sits above
the vouched golden, not because of its agent. CLAUDE.md's corollary exactly: when a verdict
changes which field it counts from, the alarm text has to change with it, or a true alarm
reads as one to dismiss.

Both the ACK log line and the Hosts-dashboard HeldReason now come from one
ManagedFloorDecision.HoldReason(), and TestResolveManagedFloor_HoldReasonMatchesTheCause
pins each reason to its cause.
2026-08-05 17:53:24 +02:00
admin cad0406e2b manifests: hub 0.96.0 -> 0.97.0 (R-216 floor guard, R-222 ACK fields)
gates / gates (push) Successful in 7s
2026-08-05 17:50:23 +02:00
admin 7e1d2898bd hub v0.97.0 — the floor stops being served past the agent it depends on (CAMPAIGN-11)
gates / gates (push) Successful in 8s
R-216, the hub half. ResolveManagedFloor's own comment says it exists to "never push a
controller past the agent it depends on", and it compared against ArtifactManifest.MinAgent
— which by ITS own comment describes the GOLDEN's controller. publish-train-rules.md rule 3
states the rule about the FLOOR's controller. Measured live: golden 0.192.0 / MinAgent
0.113.0, floor 0.200.0, agent 0.120.0 — served, and the box was pushed onto a controller
needing agent 0.125.0.

A floor ABOVE the vouched golden is now HELD with its own reason (HeldBeyondGolden), reusing
Part D's dashboard visibility. Nobody types a number twice: the vouched MinAgent keeps its
meaning, the guard stops applying it to versions it does not describe. An uncoupled release
is untouched; an unparseable golden degrades rather than gating.

R-222: the report ACK's escrow object gains superseded_present / superseded_at, counting only
rows that actually carry an identity blob. One boolean and one timestamp, for one message.
No read path — that link is still unbuilt.

Red-proof: removing the floor-above-golden branch reproduces the campaign's measurement.
2026-08-05 17:49:04 +02:00
admin f45b1f6761 docs: R-193 CLOSED (the recovery screen); R-213 minted for the put-back
gates / gates (push) Successful in 7s
- OPEN-ITEMS: R-193 CLOSED with both 2026-08-05 rulings (unlocking and restoring
  are separate; 'I do not want the old data' moves the store aside after a double
  confirmation), and the shape-(b) reasoning — WriteOffboxSecrets auto-generates a
  repository password on re-apply, so the literal 'fresh data area' trigger would
  have opened a window that closes by itself.
- R-213 MINTED (R-212 was and still is the highest, re-checked for the second
  writer): putting files back in place, with the live-versus-backup comparison
  named as its requirement. Not started, deliberately.
- capability map: the 'needs someone who knows to look' qualifier is GONE; what
  remains is stated narrowly — no correct-code run through the page, the put-back
  is out of scope, and the journey has not been re-walked end to end.
- 07-backup-architecture 7.0: a fifth row, and where the screen deliberately stops.
- CONTEXT: standing ruling S-34.
- STATUS: the headline change and the two things still owed as proof.

No hub change and no hub bump.
2026-08-05 12:56:55 +02:00
admin 63e0ac01f2 R-212 CLOSED: the three orphaned stores deleted after a corrected list (~1.45 GB)
gates / gates (push) Successful in 7s
The register said 'two set-aside stores, ~1.2 GB'. Measured before touching
anything: THREE set-aside stores totalling ~1.45 GB, and the thing that was
exactly 1.2 GB was demo-felhom's LIVE felhom-repo. Matching on the size would
have deleted a working repository. The operator was shown the corrected list
and confirmed 'delete all three'.

Deleted: demo-felhom orphaned-20260717 (1.4 G) + orphaned-20260718 (3.0 M);
demo-hp orphaned-20260804 (43 M). Both LIVE repos untouched, confirmed by full
listings before and after on each account.

Proof nothing live was caught: a real off-site run on demo-hp immediately
afterwards returned status ok, orphaned false, no error, 6 snapshots.

Method note recorded for the next session: the storage box has a RESTRICTED
shell. 'test -d X && rm -rf -- X' returns 'Command not found' and does nothing
(it failed CLOSED, verified by an unchanged listing); 'rm -r <path>' as one
simple command is the working form.
2026-08-05 11:15:24 +02:00
admin 4faebe2926 docs: R-204 ALL FOUR items closed; R-193 credential half; R-192 by replacement; R-212 filed
gates / gates (push) Successful in 8s
- OPEN-ITEMS: R-204 all four CLOSED with both 2026-08-05 rulings recorded (the
  declared-state trigger and its four-meanings-of-absence reasoning; the recovery
  preview's dashboard-password exposure accepted as metadata, not content).
  R-193's credential half CLOSED, screen + deletion still open. R-192 CLOSED by
  REPLACEMENT. R-202 untouched.
- R-212 MINTED (R-211 was the highest, grepped): the orphaned-ciphertext deletion
  HALTED at its STOP because the measured paths do not match the register — three
  set-aside stores totalling ~1.45 GB, and the thing that is exactly 1.2 GB is
  demo-felhom's LIVE repo. Nothing was deleted.
- capability map: all four interventions closed; the row KEEPS a qualifier for a
  new reason — no step needs an operator, but there is no customer-facing recovery
  screen, and the journey has not been re-walked end to end.
- 07-backup-architecture 7.0: the four-step table updated; the declaration-vs-
  inference reasoning and the credential-automatic/key-customer-present split.
- CONTEXT: standing ruling S-33.
- STATUS: the headline change and the deletion STOP.
- REPORT-r204-item4.md rather than REPORT.md: a parallel session is active in this
  shared clone.
2026-08-05 11:06:41 +02:00
admin 4114c5f891 manifests: hub 0.96.0 (R-204 item 4, hub half)
gates / gates (push) Successful in 7s
2026-08-05 10:51:44 +02:00
admin b2462b8f36 CHANGELOG: hub v0.96.0 (R-204 item 4, hub half)
gates / gates (push) Successful in 7s
2026-08-05 10:49:23 +02:00
admin fe1e81628d pre-push: refuse a push from a clone outside the felhom workspace (R-204 rider)
Identical to the assertion added in felhom-agent 0404f60,
app-catalog-felhom.eu ee2c810 and felhom-controller a91f055.
2026-08-05 10:48:24 +02:00
admin f62a115891 R-204 item 4 (hub half): the hub answers a rebuilt box's request (hub v0.96.0)
New internal/offsiteheal, the sibling of pbsdrheal: it acts ONLY on the state the
box declares, sustained across two distinct reports, re-staging the stored
credential before ever minting a new one. A healthy box is a pure no-op; it never
blind-timer-reissues and never re-runs a provisioning step.

RESTAGE IS POSSIBLE because the stored value survives a consume — established from
the schema and ConsumeOneTimeSecret (which stamps consumed_at and nothing else),
not inherited from the PBS analogy, and pinned by a test that asserts the SAME
value comes back.

reportHasOffsite is TIGHTENED to require enabled:true. Its comment asserted that
presence == applied-on-the-box, and the declaration deliberately breaks that
premise; left alone it would have read a request for help as proof the tier was
applied. Provably a no-op for every report shape that existed before, because an
attached object has always carried enabled:true.

R-192's guard half is CLOSED BY REPLACEMENT: the delivery checker's counting
inference read the OLDEST 500 reports after a consume — all predating a rebuild,
which is why demo-hp sat stranded for 108 reports under a confident regressed-shape
verdict. A declaration outranks both inferred shapes, and the checker stands down
with a record so the two mechanisms cannot double-issue.

No escrow ceremony is ever run or requested: credential automatic, key
customer-present.
2026-08-05 10:48:18 +02:00
admin c917251eeb docs: P6 EXECUTED — containerd store moved to SSD2; reboot deferred (R-209, R-209a)
gates / gates (push) Successful in 7s
Operator ruled "proceed" after reading the pre-analysis; the storageReserved
condition CC made the move conditional on was applied with it. The pre-analysis
is retained verbatim as §11-bis so the ruling can be audited against what it
was given.

MOVED WITH ZERO LOSS, verified BEFORE the original was touched on four
independent observables: 550,891 = 550,891 filesystem entries; 448 = 448
trusted.overlay xattrs (load-bearing — overlayfs stacking rides them, which is
why rsync -X is not optional); 37,243 = 37,243 hardlinks; byte-identical
meta.db sha256. Verified again after: identical image/tag/volume ID SETS,
cache 2.782 GB / 38 records, ceiling still 30 GiB, pg 4 DBs / 31 tables /
175,135,767 B, redis DBSIZE 2437. End-to-end proof was a real build on the
relocated store, rc=0 — not an inventory count.

k3s was never at risk, and that was established BEFORE stopping anything: it
runs a separate containerd, so Gitea, the registry, the hub, PBS, Longhorn and
~160 pods stayed up. Only the two jarr-* dev containers were affected.

TRAP FOUND WHILE PROVING THE GUARD, and it is the reusable part:
RequiresMountsFor on a path with NO mount unit is a SILENT NO-OP — containerd
started normally against an absent-but-unmounted path. A typo'd guard buys
nothing and says nothing: the built-but-never-wired shape again. The guard was
therefore verified positively at the unit level (Requires= AND
After=mnt-ssd_2.mount on both units), and refusal proven with a genuinely
absent DEVICE via a temporary synthetic .mount unit — /mnt/ssd_2 hosts 12 live
Longhorn replicas and must never be unmounted, and editing fstab on a
production host risks emergency mode at boot. Result:
"Job containerd.service/start failed with result 'dependency'", is-active
inactive. It refused rather than creating an empty store on the root disk.

storageReserved on SSD2 0 -> 80 GB; still Schedulable=True at 76.34%.

NOT PROVEN, AND NOT GLOSSED (R-209a): the operator ruled DO NOT REBOOT, so the
move has not survived a reboot — which is this task's own standard for
validated. The MECHANISM is proven; the CONSEQUENCE (that a real boot mounts
/mnt/ssd_2 before containerd starts, in this host's actual ordering) is not,
and mount-ordering reasoning is exactly the class this project has been burned
by. So: the pre-move tree stays MOVED ASIDE, not deleted (34.3 GB, which is why
/ sits at 54% and not lower), and felhom-store-postboot-check.service — oneshot,
enabled, dry-run PASS at install — validates it automatically at the next boot,
asserting positively that /var/lib/containerd does NOT exist. Read
/var/log/felhom-store-postboot-check.log after the next boot; on PASS the
rollback copy can go and ~34 GB returns.

Gates: repo_gates.py --fast — all OK, rc=0 (run separately from this commit).
2026-08-05 10:27:40 +02:00
admin ee9d9bf203 docs: SPIKE — DooPlex build-cache containment; R-205..R-211 (2026-08-05)
gates / gates (push) Successful in 7s
Spike output only; no production Go code. The one shipped change rides in
homelab-manifests 6808a4b (R-205, the monitoring rule).

VERDICT: mechanism confirmed, with one correction and one refutation.

- CONFIRMED: builder.gc IS honoured under the containerd worker and DOES evict.
  Proven by naming a 440 MB `go mod download` record present at build N and
  absent by N+2 — not by absence of an error.
- CORRECTED: honoured ONLY in the `policy` array form. The flat form is
  SILENTLY ignored — daemon starts, logs nothing, keeps its defaults.
  `dockerd --validate` returned "configuration OK" for a bogus key AND for a
  config that then crashed the daemon. The oracle is `docker buildx inspect`.
- REFUTED: Docker's `data-root` would NOT move the cache — it moves 0.62 GB.
  The 181.4 GB belongs to the system containerd (`root` in
  /etc/containerd/config.toml).

P3 (operator-approved) executed: prune claimed 156.9 GB, the filesystem
returned 150.35 GB (the 6.5 GB gap is layers shared with images), SYNCHRONOUSLY
— / went 86% -> 53% used, and Longhorn's default disk went
Schedulable=False (DiskPressure) -> Schedulable=True (18.85% -> 50.32%).

P7 root-caused the largest item and it is NOT the cap: all 208 `go mod download`
records had Usage count 1. Isolated by controlled builds — same VERSION build-arg
-> CACHED, new VERSION -> executed, byte-identical tree. `ARG VERSION`/`ARG
GIT_COMMIT` sit ABOVE the module-download step, and a RUN's cache key includes
the stage environment. Both Dockerfiles have it. One line each to fix -> R-208.

P6 NOT EXECUTED — stops at the operator, as specified. Pre-analysis: the move is
safe as measured (+38.8 pp above the 25% floor) but SSD2 is the only Longhorn
disk with storageReserved=0 and is overcommitted 6.9x; at full inflation the move
lands 12 pp BELOW the floor. The prune removed the move's urgency, so CC
recommends against it unless ~80 GB is reserved on SSD2 -> R-209.

Register: R-205 (CLOSED, shipped), R-206 (Ansible: cap + prune + narrowed Docker
ban), R-207 (DRY_RUN guard), R-208 (ARG ordering), R-209/R-210 (operator),
R-211 (Prometheus has no config-reloader — rules changes have never applied
until something restarted the pod; found while verifying R-205).

Gates: repo_gates.py --fast — all OK, rc=0 (run separately from this commit).
2026-08-05 10:02:46 +02:00
admin 0dbd954fec docs: R-196 closed, R-204 items 1-3 closed, item 4 open (R-193)
gates / gates (push) Successful in 7s
- OPEN-ITEMS: R-196 CLOSED; R-204 items 1-3 CLOSED with item 4 named and
  its dependency stated. Header restates that R-202, the 1.2 GB ciphertext
  deletion and R-198's still-unit-proven retention all REMAIN OPEN.
- capability map: the recovery row keeps its 'with a person present'
  qualifier, names which crutch remains, and cites the three now gone.
- 07-backup-architecture: new 7.0 - what a customer can and cannot do
  ALONE, the four steps in a table with status. This is the section a
  future reader will use to answer that question.
- CONTEXT: standing ruling S-32, superseding S-31 steps 2-5.
- STATUS: rewritten to one screen per its own header; removes a corrupted
  half-overwritten section left from the drill session.
- ROADMAP: R-196 and R-204 collapsed.
2026-08-05 07:37:35 +02:00
admin 975a690fbe manifests: hub 0.95.0 (R-196 / R-204 item 2)
gates / gates (push) Successful in 7s
2026-08-05 07:19:57 +02:00
admin 5c7d67102d CHANGELOG: hub v0.95.0 (R-196 / R-204 item 2)
gates / gates (push) Successful in 7s
2026-08-05 07:17:37 +02:00
admin d1a8edb332 R-196 / R-204 item 2: a re-issue no longer marks a healthy escrow stale (hub v0.95.0)
ReissueCredentials marked the escrow stale on every re-issue, on precautionary
grounds — the box's re-apply MIGHT mint a fresh repository password. It usually
does not. A stale flag withholds restic_pw_sha256 from the ACK, which stops the
controller's auto-confirm, which leaves EscrowState pending, which makes
OffboxRunnable false: every off-site backup refused on a box whose key was never
in doubt — and the customer told to re-run the one ceremony that would have
superseded the key just recovered.

The case it guessed at is measured elsewhere: the controller's Scenario-F
re-check compares the sealed hash against the live repo password on every ACK
(and the mark was BLINDING it by emptying that hash), and R-197's
offsite_repo_key_changed fires on a proven difference across a supersession.

offsite_reissued is unchanged. MarkEscrowStale is kept without a caller so a
future EVIDENTIAL writer has the mechanism, with a test pinning it live.
TestReissue_InvalidatesEscrow is replaced by its exact inverse.
2026-08-05 07:17:29 +02:00
admin 2a7ac03c47 R-201 PASSED: a customer's file survived a machine rebuild and came back byte-identical
gates / gates (push) Successful in 7s
2026-08-04 23:18:47 +02:00
admin b228fd102d R-201 night run: the off-site key IS recoverable after a real rebuild (proven); the verdict is blocked by R-204
gates / gates (push) Successful in 6s
2026-08-04 22:33:01 +02:00
admin 24acc8d816 REPORT: R-203 shipped and proven live; R-201 unblocked
gates / gates (push) Successful in 8s
2026-08-04 18:53:13 +02:00
admin 73fb595e38 R-203 shipped: the app and its backup agree, and 'ok' means it — R-201 unblocked
gates / gates (push) Successful in 7s
2026-08-04 18:52:14 +02:00
admin a0c4b607a6 drill R-201: prepared and HALTED before the wipe — a mandatory data directory was missing from the off-site snapshot while the run said ok (R-203)
gates / gates (push) Successful in 8s
2026-08-04 15:00:06 +02:00
admin 846253baa8 docs: the offsite key is proven recoverable (R-199 closed, R-200 half, R-201 scoped)
gates / gates (push) Successful in 8s
2026-08-04 13:55:25 +02:00
admin dd089265e8 manifests: hub 0.93.0 -> 0.94.0 (R-199 box-authenticated escrow retrieval)
gates / gates (push) Successful in 8s
2026-08-04 13:40:26 +02:00
admin 435f4a5229 hub v0.94.0: a box can fetch its own sealed recovery package (R-199 link 6)
gates / gates (push) Successful in 7s
Link 6 of the recovery chain had no client. The hub has served the identity blob since
slice 10D from handleReEnroll / handleGetRestoreDirective, gated on operator-armed recovery
mode and the global key -- and nothing in the agent, the hub UI, any script or any runbook
ever called either. The only documented retrieval was sqlite3 writefile() by hand on a
kubectl cp-ed database.

GET /api/v1/hosts/{host_id}/escrow is the box-authenticated mirror of the PUT that put the
blob there. Self-scoped (a per-host key reads only its own; global may read any). A host with
no bundle gets 200 {present:false} -- a 404 is indistinguishable from an unknown host and a
bare empty 200 from a zero-length blob.

THE TRADE IS RECORDED IN THE HANDLER, not inferred: obtaining the blob used to require the
operator to arm recovery mode; now whoever controls a rebuilt box can obtain it with that
box's own credential. They still cannot open it -- the hub has never held R and a wrong code
fails closed at age's scrypt KDF. The mitigation is that every retrieval raises
escrow_blob_served (warning, operator-only), recorded before the bytes leave.

escrowSelfServiceRetrieval is the single decision point: flip it to false and the endpoint
additionally requires recovery mode, changing nothing else.

The operator-driven DR path is untouched, pinned by a test. Red-proofs observed: removing the
ownership check serves host B's blob to host A; removing the record makes it silent.
2026-08-04 13:39:27 +02:00