From 984ea8c8bd9bde62855cd410f7e9304510b4fced Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Tue, 21 Jul 2026 12:39:13 +0200 Subject: [PATCH] =?UTF-8?q?docs(report):=20TASK-D=20Parts=201-2=20?= =?UTF-8?q?=E2=80=94=20R-51/R-52=20shipped,=20image=20built,=20STOP=20legs?= =?UTF-8?q?=20pending?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- REPORT.md | 353 ++++++++++++++++++++++-------------------------------- 1 file changed, 144 insertions(+), 209 deletions(-) diff --git a/REPORT.md b/REPORT.md index 861da77..7decf48 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,254 +1,189 @@ -# REPORT — R-48 restore wizard (v0.154.0) + the v0.155.0 follow-up fix; STOP-1 BANKED; Part 3 STOPPED +# REPORT — TASK-D Parts 1–2: dead-primary alerting (R-51) + boot reconciliation (R-52) -Session date: **2026-07-21**. Executed on DooPlex as `kisfenyo`. +**Controller v0.155.0 → v0.156.0.** Session 2026-07-21, on DooPlex as `kisfenyo`. +**Status: code SHIPPED and image published; the three live STOP legs are operator-present and have +NOT run.** Nothing below claims live behaviour that was not observed. -## 0. Outcome at a glance +--- -| Leg | Status | -|---|---| -| **Part 1 — R-48 restore wizard (v0.154.0)** | **SHIPPED** `3a9d744`, image published, deployed via the floor save. | -| **v0.155.0 — follow-up fix** | **SHIPPED + DEPLOYED** `9d1b498`. Fixes a real defect in v0.154.0 found by the operator on the first live click-through. | -| **Part 2 — capability-map cell** | **DONE** `ce8c539`. | -| **STOP-1 — floor save + single-fire** | **BANKED.** Exactly one swap over a 39-min window, no rollback, healthy. §6. Doc rows flipped `11ead4b`. | -| **Live click-through + Ellenőrzés** | **DONE by the operator**, and re-run endpoint-level after the v0.155.0 fix. §7. | -| **Part 3 — agent 0.90.1** | **STOPPED, ruled.** Not published, not deployed. §8. | +## 1. Baselines (re-confirmed at session start) -## 1. Baselines - -| Repo | start | end | -|---|---|---| -| felhom-controller | `b30e2e5` | `9d1b498` (v0.155.0) | -| felhom.eu | `fb0b8c1` | `c35da9d` | -| felhom-agent | `8c55ac7` | **unchanged — no commit, no publish, no deploy** | - -## 2. Phase-0 probes - -**P1 — self-update wiring: PASS.** `self_update.enabled: true`, image correct. `auto_update: false` is -irrelevant and this was verified by reading the code, not assumed: `MaybeAutoUpdate` never consults -`cfg.AutoUpdate` — that flag is the customer's opt-in to chase *latest*, while the FLOOR path is the -managed one. Stronger evidence: `update-state.json` already recorded a prior `initiated_by: auto-floor` -success (0.143.0 → 0.145.0, 2026-07-18), which proves the agent swapper is wired. Anti-flap could not -block, since the persisted target was `0.145.0`. `0.154.0` verified as the highest registry semver tag, -so the `floor <= latest` gate would pass. - -**P2 — agent version: PASS, and it changed the plan.** Built at `main`, self-reported -`felhom-agent 0.90.1`, sha256 `ba1d0296…`, 13 734 067 bytes. But see §8. - -**P3 — before-state: CAPTURED** (endpoint-level; no browser on DooPlex). The offsite section rendered -**6× `/backup/offbox/restore`, 1× `place`, 1× `reconstitute`** across three apps. For immich the two -decisive controls were adjacent siblings differing only by label: - -``` -Helyreállítás az élő adatok közé (csak a hiányzó fájlok) <- data CANNOT come back -Teljes visszaállítás (fájlok + adatbázis) <- data CAN come back -``` - -## 3. v0.154.0 — the wizard (`3a9d744`) - -One „Visszaállítás…" entry per app row → `GET /backups/restore/app?name=`; three intent cards -with consequence sentences; danger styling and the R-43 double-confirm carried over verbatim; pure -`deriveWizardStep`; **no new mutation endpoint**; works without JavaScript. `internal/backup`, -`internal/appbackup`, `internal/selfupdate`, the shares/local/.fab blocks all untouched. - -**Latent bug fixed on the way:** `offboxRedirectTo` hardcoded `"?"` when appending its flash, which -against the wizard's `?name=` target would have buried the flash inside the app name. - -**Group-B red-proof (run, reverted):** trivial always-INTENT impl → **all 7 table rows FAILED** plus -the execution render test. Restored, green, `git diff` clean before commit. - -Also caught one of my own assertions being hollow during authoring: a bare substring check for the -skew/empty warnings passed on a *clean* pair, because the `confirmFullRestore` JS repeats both -sentences as string literals. Tightened to assert rendered banner markup + `data-restore-*`. - -## 4. v0.155.0 — the defect v0.154.0 shipped (`9d1b498`) - -**Found by the operator on the first live click-through**, from the phase strip disagreeing with the -progress banner on the same screen. This is the most important finding of the session. - -`backup.Manager` carries **two** different "is something running" booleans: - -| flag | read by | set by | covers the verification restore? | +| Repo | `main` @ start | clean | now | |---|---|---|---| -| `running` | `IsRunning()` | `acquireRunning()`, **inside** the goroutine | **no — `RestoreOffboxScratch` never acquires it at all** | -| `opRunning` | `RestoreStatus()` | `BeginRestoreOp()`, in the handler, synchronously | yes, all four offsite actions | +| felhom-controller | `0f9b29a` | yes | `285dd10` (v0.156.0) | +| felhom-agent | `08b55a1` | yes | `98adb72` (v0.92.1) | +| felhom.eu | `50a7ffa` | yes | docs commit (see §9) | -v0.154.0 sourced `OpRunning` from `IsRunning()`. Verified by reading the call sites: -`PlaceOffsiteRestore` and `ReconstituteFromOffsite` do acquire; **`RestoreOffboxScratch` does not**. -So for „Ellenőrzés" and the full-restore preparation — the wizard's two most-used and longest actions, -both streaming from restic — the execution step was **unreachable**. The page offered all three -intents with live buttons while a restore ran; the handler would have refused any of them. That is -precisely the "offering a control guaranteed to fail" dishonesty R-48 exists to remove, shipped inside -the release that removes it. +Image: `gitea.dooplex.hu/admin/felhom-controller:0.156.0`, pushed, **deliberately not hand-deployed** +— the floor save at STOP-3 deploys it, which banks another single-fire self-update datapoint free +(the R-23 pattern). 9201 still runs 0.155.0. -**Fix:** `restoreOpInFlight(st)` behind a documented seam, fed by a **single** `RestoreStatus()` read -per render, so the strip, the suppression decision and the running-op name cannot diverge. The helper -sits on the handler's actual path — deliberately, so a future edit reintroducing `IsRunning()` fails -the test rather than passing it. +--- -**Why v0.154.0's tests missed it — the generalisable lesson.** The Scenario-E table proved -`deriveWizardStep` behaves correctly *given* `OpRunning=true`. Nothing proved the handler ever -*computes* `true`. **A table test over a pure function proves the function, not the caller.** The new -`TestRestoreOpInFlight_UsesDisplayFlagNotConcurrencyFlag` drives a real `Manager` through -`BeginRestoreOp` and asserts the render. +## 2. The defect R-51 actually fixes (the ROADMAP row was wrong) -**Red-proof (run, reverted):** restoring the v0.154.0 `IsRunning()` shape → +The row says aggregation classifies a dead-primary stack `unhealthy`, and that `IsDownState`'s +deliberate `unhealthy` exclusion is therefore what suppresses the alarm. **That is not what the code +did.** `aggregateState`'s final branch was: -``` ---- FAIL: TestRestoreOpInFlight_UsesDisplayFlagNotConcurrencyFlag - restore_wizard_test.go:367: a started restore op MUST read as in-flight for display - (this is the v0.154.0 bug) +```go +// Mix (some running, some stopped) — report as running (partial) +if running > 0 { return StateRunning } ``` -**„Eredmény" was a dead label.** It never lit in v0.154.0. The strip's highlight is now its own -derived `Phase`, separate from `Step`: a finished restore is back on the intent step (everything -available again) while the strip reads „Eredmény" and an outcome card shows the result. Bounded by -`restoreResultWindow` (10 min) so a stale result cannot look fresh, and **bound to the app** so a -finished bookstack restore cannot light immich's page with bookstack's message. The card survives a -reload; the flash does not. 9-row table test incl. both boundary directions and a future-clock-skew row. +So the immich stack read **running**, not unhealthy. `IsDownState` was never consulted about +`unhealthy` at all, and the constraint the row protects was never in tension with the fix. The +correction is recorded in the ROADMAP row itself. -## 5. Test + gate results +**The fix.** New `StateDegraded`. The mixed branch asks each DOWN member for its docker restart +policy: `always` / `unless-stopped` → docker was supposed to be keeping it up → the stack is +**degraded**; `no` / `on-failure` → a finished one-shot init/migrate container → benign, stack stays +running. `IsDownState` gains `degraded` **and nothing else** — `downstate_test.go` is untouched and +green, which is the fix-3 contract. -Full suite green at both commits: `go build ./... && go vet ./... && go test ./...` — 0 failures. -All six controller design gates rc=0. +**An unreadable policy counts as supervised — fail-CLOSED**, deliberately the opposite of +`IsDownState`'s documented fail-open. They are different questions: there the *state* is ambiguous, +here a member is known dead and only its excuse is missing. P2 backs it (§4). -| Group | Test | Result | +**Blast radius, decided per call site** (`StateDegraded` is a stack-level aggregate and never a +container state, so container-level code is untouched by construction): + +| Question the caller asks | Sites | Degraded counts as | |---|---|---| -| B | `TestDeriveWizardStep_Table` (9 rows) | PASS | -| C | `TestResolveWizardApp_Refusals`, `TestRestoreWizardHandler_UnconfiguredRedirects` | PASS (302, no 500) | -| A | `TestRestoreList_SingleEntryPerApp` | PASS | -| C | `TestRestoreWizard_ThreeIntentCards`, `..._NoScratchLocksDataIntents` | PASS | -| E | `TestRestoreWizard_OpRunningSuppressesAllMutations` | PASS | -| D | `TestRestoreWizard_NoNewMutationEndpoints`, `..._FieldContract` | PASS | -| **new** | `TestRestoreOpInFlight_UsesDisplayFlagNotConcurrencyFlag` | PASS (red-proofed) | -| **new** | `TestHasRecentRestoreResult` (9 rows), `TestRestoreWizard_ResultCard` | PASS | +| "are there live containers to stop?" | quiesce `RunningAppStacks`, delete stop-first guard ×2, export `IsStackRunning`, telemetry, health-probe eligibility | **up** | +| "is this app working?" | dashboard counter, stopped filter, dead-app banner + `app_start_failed` | **down** | +| "did this come back healthy?" | `waitHealthy` (migration), post-restore `RefreshAndIsRunning`, integrations readiness | **not healthy** (left strict) | -Two pre-existing tests coupled to the old IA were **inverted, not deleted**: `TestAppRow_RestoreLists` -and `TestBackupsSplit_SectionsOnExactlyOnePage` now assert the mutation forms are ABSENT from the list. +The health probe can no longer mask it: the probe result only ever overrides `StateRunning`, so a +failing probe on a degraded stack cannot downgrade it to `unhealthy` (which would silence the alarm +again). Asserted by the wiring test's second refresh. -## 6. STOP-1 — single-fire self-update: **BANKED** +--- -Operator saved global floor → v0.154.0. Timeline (host CEST / guest UTC, 2 h offset): +## 3. R-52 — the recovery half -``` -06:57:13Z UpdateState "pending" initiated_by=auto-floor -06:57:17Z agent WARN controller swap requested 0.153.0 -> 0.154.0 -06:57:19Z agent INFO image file written, restarting bootstrap -06:57:21Z container StartedAt · UpdateState completed_at -06:57:29Z agent INFO new controller healthy -``` +`internal/bootrecon`, one bounded sweep 5 s after startup (after the quiesce recovery, so the two +never race for the same stack): at most **2 attempts, 30 s apart**, then it stops and the alarm owns +the problem. Never a loop. `compose up -d` exits 0 on a crash-loop, so success is decided by a fresh +`RefreshStatus`, never by a nil error. -| Assertion | Result | -|---|---| -| `controller swap requested` | **1** | -| `image file written, restarting bootstrap` | **1** | -| `new controller healthy` | **1** | -| rollback / swap-failed / unhealthy | **0** | -| container `RestartCount` | **0** | -| bootstrap unit `Result` | `success` | +**The safety argument is the container gate.** The UI's Stop is `compose down`, which REMOVES the +containers; a boot interruption leaves them behind as `Exited`. So "deployed, has containers, and +they are down" is the boot-orphan signature, and a zero-container stack is never touched. -> **Correction — I over-claimed one line.** I originally cited the bootstrap unit's `NRestarts=0` as -> no-storm evidence. It isn't: the unit is `Restart=no`, so `NRestarts` is 0 by construction and would -> read 0 during a storm too. Dropped. The real no-storm evidence is the agent-side swap count and the -> container's own `RestartCount=0`. +Worst case 5 s + 30 s = 35 s < the 90 s `deadAppBootGrace`, so a successful recovery never alerts +and a failure alerts honestly. `TestBootReconcileFitsInsideTheBootGrace` asserts that arithmetic +instead of leaving it in a comment. -`UpdateState` persisted `status: success`, `0.153.0 → 0.154.0`, `initiated_by: auto-floor`. -`VerifyStartup` banked it on the next boot (`Post-update startup: update successful`), and the 06:57:52 -periodic check reported `Current version 0.154.0 is up to date` — the anti-flap correctly doing nothing -now that current ≥ floor. **Exactly one swap, no storm, no rollback.** This closes the leg R-23(a) was -missing: the earlier floor save proved nothing because it targeted an already-running version. +--- -**30-minute no-re-attempt check — PASSED, and my earlier caveat was broader than the facts.** Recounted -over 06:50 → 07:29 (39 min): `controller swap requested` = **1**, agent-driven bootstrap restarts = **1**. +## 4. Phase-0 probes -I hand-deployed v0.155.0 at `07:17:10Z`, inside the observation window, and flagged that as -contaminating the result. Having checked the scope: it contaminates **`StartedAt` only**. A hand-deploy -writes `/etc/felhom-controller-image` and restarts the unit directly via `pct exec` — it never goes -through `SwapController` — so the swap-count assertions, which are the actual evidence, hold -uncontaminated across the whole window. I was right to flag it and wrong about how much it cost; the -window is clean, not merely the 19 minutes I claimed. +**P2 (R-51 one-shot census) — DONE, and the answer is clean.** All **53** catalog templates, +**78** services: **every single one is `unless-stopped`**. Zero services with `no` / `on-failure` / +absent. (The other 30 compose files under `output/` are generated artifacts, not templates.) So the +restart-policy guard protects a case that does not exist in today's catalog — it is there for future +templates and for the fail-closed decision above, and it costs one cached `docker inspect` per down +member of a mixed stack. -**An unplanned bonus confirmation came with it.** After the hand-deploy the box ran **0.155.0 against a -0.154.0 floor** — i.e. above the floor — and the updater logged `Current version 0.155.0 is up to date` -and did nothing. That is Scenario B ("at/above floor does NOTHING — must not chase latest") confirmed -live, which no test in this session set out to produce. +**P1 (why `unless-stopped` did not resurrect immich/calibre-web) — NOT RUN, and deliberately not a +blocker.** It requires stopping a container on a live guest and rebooting it: STOP-1's territory, +operator-present. The reconciliation is correct either way — if Docker did record them as +user-stopped, `unless-stopped` will never bring them back and only R-52 can; if it did not, R-52 is +a harmless no-op because the containers are already up when the sweep looks. Recorded as an open +question, not a dependency. **It can be answered for free during STOP-1**, whose R-52 leg reboots +9201 with containers stopped — precisely P1's experiment. -## 7. Live validation +**P3/P4** were agent-side — see `felhom-agent/REPORT.md`. -Method: **endpoint-level** (no browser on DooPlex) plus the operator's own UI click-through. +--- -**Operator, in the browser (v0.154.0):** list page → one „Visszaállítás…" per app; wizard → three -cards, pair-honesty panel showing `adatbázis-mentés ideje: 2026-07-19 12:28`; ran one **Ellenőrzés**; -result `A(z) immich visszaállítva ellenőrző mappába: /mnt/felhom-drives/hdd_1/backups/offsite-restore/immich -(a meglévő adatok változatlanok).` Non-destructive as designed. +## 5. Tests and red-proofs -**Endpoint-level, after the v0.155.0 fix — the proof the defect is gone.** A real Ellenőrzés run -through the actual endpoint: +Green gate: `go build ./... && go vet ./... && go test ./...` — **all 25 packages ok** (full run, +`-count=1`; `internal/backup` alone takes 174 s). -| moment | phase strip | `
` count | note | +New: `internal/stacks/degraded_test.go` (8 cases incl. two production-path tests), +`internal/bootrecon/bootrecon_test.go` (8 cases), `cmd/controller/bootrecon_wiring_test.go` (4). + +| # | Red-proof | Mutation | Result | |---|---|---|---| -| before | Előkészítés | 3 | intents offered | -| **mid-flight** | **Végrehajtás** | **0** | `Jelenleg egy mentési vagy visszaállítási művelet fut (immich).` | -| after | **Eredmény** | 3 | result card names the scratch path; intents available again | -| bookstack, same moment | Előkészítés | — | **no** result card — app-binding holds | +| A | dead primary must alert | mix branch → `return StateRunning` | **FAIL ×3** — `aggregateState = "running", want "degraded"`; both production-path tests report `immich state = "running"`. Restored, green. | +| C | the boot sweep must be wired | `go runBootReconcile(...)` commented out in `main()` | **FAIL** — `func main() no longer starts the R-52 boot reconciliation`. Restored, green. | +| D | a user-stopped app must stay stopped | zero-container gate dropped from `isBootOrphan` | **FAIL ×2** — `StartStack("jellyfin") called 1 times, want 0 — a deliberate Stop must survive a reboot`. Restored, green. | -Under v0.154.0 the mid-flight row would have read `Előkészítés / 3 forms`. Before/after on the list -page: **6 restore + 1 place + 1 reconstitute forms → 0**, and 3 wizard entries. Refusal path: unknown -app → **302** + Hungarian flash, no 500. +**A finding from red-proof C, worth more than the fix it guards.** That wiring test was first +written as `strings.Contains(src, "go runBootReconcile(ctx, stackMgr, logger)")`. Its red-proof +**passed** — because commenting the call out leaves the string in the file. A test whose own +red-proof cannot fail it is decoration. Rewritten as an AST walk over `func main()`'s body for a +`GoStmt` calling `runBootReconcile`; the red-proof then failed as it must. Recorded in REUSE.md and +promoted into `documentation/PROMPT-TEMPLATE.md` §10 (Part-5 rider). -Deploy verified: `felhom-controller:0.155.0 | Up (healthy)`, `RestartCount=0`. +**Seam discipline (§9 rule 6).** Both features have a production-path test: +- R-51: `TestRefreshStatus_WiresDegradedThroughTheRealPath` drives the whole real chain — + `RefreshStatus` → `docker ps` → `aggregateState` → `docker inspect` — through a new `execFn` seam + at the process boundary, and asserts the inspect COUNT (exactly 1: only the down member of the + mixed stack, never the healthy stack; still 1 after a second refresh, proving the cache). +- R-52: the sweep is asserted from `package main`, plus the AST wiring test above. -## 8. Part 3 — STOPPED, then ruled +--- -Part 3 asked me to publish + deploy v0.90.1 as "R-39's cheap half — the PBS wrapper argv fix", while -Rule 12 forbade touching the wrapper. Both cannot hold, because **the fix IS the wrapper**: +## 6. Live validation -- **`9596d5a` changes zero non-test Go files** — `CHANGELOG.md`, `REPORT.md`, `configs/felhom-pbs-apply`, - `internal/pbsdr/manager_test.go`. Its own message: *"the Go binary is unchanged."* -- **felhom-pve already has it** — wrapper hotfixed 2026-07-18, `args=(--fingerprint "$fp")` at L107, - `.bak-20260718-preR39` retained. -- **New hosts get it too, unversioned** — `felhom-host-install.sh:1914` `fetch_raw` pulls the wrapper - from `raw/branch/main`, and `9596d5a` is an ancestor of `main`. +**None claimed for R-51/R-52.** The three legs are destructive and operator-present. The available +method here is endpoint-level (no browser on DooPlex); these legs additionally need a container kill +and a guest reboot on the production demo box, so they are STOP-1, not a CC action. -Publishing would have delivered no behaviour change, restarted a production agent at a remote site for -nothing, and — once the Day-0 manifest was saved to 0.90.1 — advertised a versioned fix the artifact -channel never carried. Registry pre-check confirmed and left as found (0.90.1 → **404**, 0.90.0 → 200). +What IS verified without the box: the full green gate, all four design gates +(`template_id_gate.py`, `emoji_gate.py`, `native_confirm_gate.py`, `offbox_rename_gate.py` — all +OK), and the image build+push. -**Operator ruling: leave 0.90.0 published; record the closure in the docs.** Done in `ROADMAP.md` -(`c35da9d`). The stop surfaced a genuine new defect, filed as **R-50b**: a root-owned privileged host -artifact is delivered unversioned from `main`, absent from the Day-0 manifest, so "which wrapper is on -this host?" is unanswerable from any manifest. +--- -## 9. Doc state +## 7. UI surfaces (Hungarian) -- Capability map row 61 → **PROVEN-LIVE** (`ce8c539`). -- ROADMAP: **R-48 → SHIPPED**; R-39 cheap half recorded as closed-in-field with the publish cancelled; - **R-50b** filed (`c35da9d`). -- **FLIPPED** (`11ead4b`): R-23(a) → **BANKED in full** (only the cosmetic (b) leg remains), and the - capability-map row "Config/state change round-trips in **seconds**" → **PROVEN-LIVE (2026-07-21)**. - That row's residual PARTIAL was explicitly "the R-23 operator-UI save→apply leg alone", which §6 - closes. Both rows carry the hand-deploy disclosure and the bonus at/above-floor confirmation. +New state label „**Részlegesen leállt**", `warn` colour token, `◑` icon (shared with unhealthy), +counted with the stopped apps, filtered into the stopped bucket, and `routeUnpublished` → true (when +the dead member is the routed one Traefik withholds the route and the URL 404s — the card must not +imply the app is reachable). **No new customer strings were needed for the alarm itself**: the +existing `app_start_failed` sentence („Telepített alkalmazás nem fut: …") is reused unchanged, which +is why R-51 ships without touching the notifier. -## 10. Remaining — human +One honest wart carried forward, not introduced: the dead-app banner appends the raw state in +English — it will now read `(degraded)` where it read `(stopped)`. Pre-existing behaviour; changing +it is a copy decision, not part of this fix. -1. **Floor → v0.155.0** at your convenience, so the fleet matches the box (9201 is hand-deployed and - above floor, so nothing is pending on it). -2. **TASK-B** ruling (R-39 primary half: the descriptor-hash re-apply gap). -3. **R-50b** ruling — recommendation is (a): record the wrapper's sha256 in the Day-0 manifest and have - the agent report the installed file's hash, so drift becomes visible. -4. **C6 customer-restore pass** for the alpha. +--- -## 11. Observations +## 8. A workspace trap found the hard way (worth more than this task) -- **The most valuable finding came from the operator, not the tests.** The phase strip he questioned - was the visible symptom of a real bug — the strip and the progress banner disagreeing on one screen. - A UI element that renders derived state is an assertion the customer can check; that is worth more - than its decorative cost. -- **`docker_run_volume_path_gate.py` fails at `HEAD`, unrelated** (`internal/appexport/estimate.go:179`). - Verified pre-existing by stashing and re-running. Left alone — but that gate is currently not a - usable signal. -- **`opstatus.go` says in its own header that it is the DISPLAY surface** and does not gate - concurrency. I read the concurrency flag anyway. The file told me; I did not check. -- **`felhom-pve` is up and healthy** (agent 0.90.0, `active`) — the "agent is DOWN" TEMPORARY block in - `felhom-controller/CLAUDE.md` is stale and should be trimmed on return. -- **P3's accented-grep trap fired as documented** — used a local Python pass over fetched bytes rather - than grepping accented patterns through `ssh → pct exec → bash -c`. +`controller/.gitignore` line 7 is `controller` — intended for the built binary. It also matches the +**directory** `controller/cmd/controller/`. Two consequences, both live today: + +1. **ripgrep (and therefore the Grep tool) silently skips `cmd/controller/main.go`** — the entire + production wiring file. Early in this session that produced a false "the whole fix-3 dead-app + path has no production caller" reading — exactly the inert-seam conclusion this task is about. + `grep -rn --no-ignore` / plain `grep -rn` (without `--include`) shows it. +2. **New files under `cmd/controller/` need `git add -f`.** `bootrecon_wiring_test.go` was refused + by a plain `git add` — as part of a multi-path add, i.e. quietly. A wiring test that never + reaches the repo is the same defect class it exists to prevent. + +Recorded in the commit message and REUSE.md. Worth a `.gitignore` fix (anchored `/controller`) as +its own XS change — deliberately not smuggled into this task. + +--- + +## 9. Deliverables + +- `285dd10` — v0.156.0 code + tests + CHANGELOG + REUSE + README. +- Image `felhom-controller:0.156.0` pushed; **not deployed**. +- Docs in `felhom.eu`: ROADMAP R-51/R-52 → SHIPPED (with the corrected R-51 diagnosis), new R-54 + row, a new capability-map row (IMPLEMENTED — not PROVEN-LIVE, the live legs have not run), and the + two Part-5 riders. + +## 10. Operator actions outstanding + +1. **STOP-1** (controller legs) — needs 0.156.0 live; see the ordering question in + `felhom.eu/REPORT.md` §5. +2. **STOP-3** floor → `0.156.0`, which is also what deploys it. +3. P1's answer falls out of STOP-1's reboot leg for free — worth recording either way.