From e8412513998ad6b7e4b9ce33744a71cf9162ad78 Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Fri, 10 Jul 2026 21:06:45 +0100 Subject: [PATCH] =?UTF-8?q?feat(doctor):=20#77=20hibernate/sleep=20section?= =?UTF-8?q?=20(VISION=20=C2=A7=20C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nomarchy-doctor gains a read-only hibernate/sleep section (the highest-ROI Day-2 gap, a natural #76 follow-on): resume device on the kernel cmdline (+ resume_offset for a swapfile), disk swap >= RAM, zram active, and any suspend/hibernate error in the previous boot's kernel log. Self-gates (skip on swap=0) and emits only ok/warn/skip, so it never fails the sheet. Adds `hibernate` to the checks.doctor probe list so the row is asserted. Fixes a set -euo pipefail hazard the first VM run caught (exit 1, no verdict): a no-match grep inside $(...) and a `cond && action` both abort under set -e — now guarded with || true / || echo 0 / if. Recorded in MEMORY as a reusable gotcha for writeShellScriptBin scripts. Verification: V2 PASS — checks.doctor VM test green (fresh build, exit 0; asserts the hibernate row renders, healthy run stays 0). V1 — ran on real LUKS+@swap hardware: all three ok rows render (resume set, swap 34G >= RAM, zram 16G) and the sheet reaches its verdict. V0 bash -n + flake check. Co-Authored-By: Claude Opus 4.8 --- agent/BACKLOG.md | 19 +++------ agent/JOURNAL.md | 21 ++++++++++ agent/MEMORY.md | 8 ++++ docs/ROADMAP.md | 8 ++++ flake.nix | 2 +- pkgs/nomarchy-doctor/nomarchy-doctor.sh | 56 +++++++++++++++++++++++++ 6 files changed, 99 insertions(+), 15 deletions(-) diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index 1541184..901fd34 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -99,16 +99,7 @@ high-ROI, etc.) live in the journal + ROADMAP — not here.* ### Product / day-2 -- **Doctor: hibernate/sleep section** (`VISION § C`) — the doctor covers - units/disk/flake/network/audio/GPU/fingerprint/fwupd/charge but has **no** - hibernate check, even though #76 just made hibernation a default. Add a - read-only section: `resume=`+`resume_offset` present on `/proc/cmdline` - (or acknowledge the `swap=0` opt-out), a disk swap ≥ RAM present, zram - active at its priority, and no failed suspend/resume in the recent journal. - Self-gating (skip when swap=0). Why: "trust > new power UI." Cost: ~1 - iteration, V2-guardable — the highest-ROI Day-2 gap found + a natural #76 - follow-on. (Verified 2026-07-10: no resume/swap/hibernate lines in - `nomarchy-doctor.sh`.) +_(Doctor hibernate/sleep section → promoted to **NEXT #77** 2026-07-10.)_ _(Battery charge-limit instant → shipped 2026-07-10; V3 PASS Latitude 5310: Custom charge type + live menu + unplug re-apply.)_ @@ -169,10 +160,10 @@ _(Identity taste retunes + hand `btop.theme` for white/vantablack/lumon/ ### v1.0 pointer -See **VISION**. Open PROPOSED: doctor hibernate section (`§C`, top Day-2 -pick); Omarchy migrant doc (`§F`); auto time-of-day theme pair (`§D`, big); -NVIDIA wrappers deferred past v1; IR portal (b)/(c) hardware. Browser -default = Chromium; power = PPD. +See **VISION**. Doctor hibernate section promoted → NEXT #77. Open +PROPOSED: Omarchy migrant doc (`§F`); auto time-of-day theme pair (`§D`, +big); NVIDIA wrappers deferred past v1; IR portal (b)/(c) hardware. +Browser default = Chromium; power = PPD. ## Decisions `[human]` diff --git a/agent/JOURNAL.md b/agent/JOURNAL.md index ebc95f3..4a2f7f0 100644 --- a/agent/JOURNAL.md +++ b/agent/JOURNAL.md @@ -17,6 +17,27 @@ Template: --- +## 2026-07-10 — #77 doctor hibernate/sleep section (V2 PASS) +- **Task:** BACKLOG #77 (promoted from PROPOSED by Bernardo) — read-only + hibernate section in nomarchy-doctor (`VISION § C`, #76 follow-on). +- **Did:** Added a hibernate/sleep row group to `nomarchy-doctor.sh`: resume + device on cmdline (+ `resume_offset` for a swapfile), disk swap ≥ RAM, + zram active, previous-boot suspend/hibernate error. Self-gates (`skip` on + swap=0); only ok/warn/skip → never fails the sheet. Added `hibernate` to + the `checks.doctor` probe list. +- **Verified:** **V2 PASS** — `checks.doctor` VM test green (fresh build, + exit 0; asserts the hibernate row renders + healthy run still 0). **V1** — + ran on this box's real LUKS+@swap layout: all three `ok` rows render + (resume set, swap 34G ≥ RAM, zram 16G) and the sheet reaches its verdict. + **V0** bash -n + flake check. +- **Gotcha (fixed + MEMORY):** the doctor runs `set -euo pipefail`; my first + cut aborted before the verdict because a no-match `grep` inside `$(…)` and + a `cond && action` both trip set -e. Fixed with `|| true` / `|| echo 0` and + an `if`. First VM run caught it (exit 1, no verdict) — the test earned its keep. +- **Pending:** none — fully agent-verifiable (read-only checks, no V3). +- **Next suggestion:** NEXT head is #76 `[blocked:hw]`; remaining PROPOSED + (Omarchy doc §F, auto time-of-day theme §D) await triage. + ## 2026-07-10 — QA sweep (clean) + VISION-gap pass → 3 PROPOSED - **Task:** Bernardo — QA/drift sweep, then a VISION-gap pass with proposals. - **Did (sweep):** Fanned out two subagents (Bernardo OK'd spawning for diff --git a/agent/MEMORY.md b/agent/MEMORY.md index a16de4a..8e3567a 100644 --- a/agent/MEMORY.md +++ b/agent/MEMORY.md @@ -25,6 +25,14 @@ iteration would otherwise rediscover. - In VM tests `pgrep -f PATTERN` can match the test backdoor's own `bash -c` wrapper (the pattern is in its cmdline) — use `pgrep -x` or a `[t]uigreet`-style bracket pattern. +- **`writeShellScriptBin` scripts run `set -euo pipefail`** (nomarchy-doctor, + the menu, lifecycle CLIs). So a **no-match `grep` inside `$(…)`** (grep exits + 1 → command-sub fails → abort) and a **standalone `cond && action`** (false + cond → abort) both kill the script mid-run — the tell is output that stops + before the final/verdict line with no error. Guard: `… | grep … || true` + inside `$()`, `cmd 2>/dev/null || echo 0` for captures, and `if` instead of + `&& action`. (#77 doctor hibernate section; caught by the checks.doctor VM + test on first run.) - A checks.* fixture CANNOT be a writeText/toFile state file read at eval time ("path … is not valid" — flake check's eval store won't realise it): extract the logic into a pure importable file and diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index e34f33d..3b7380e 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -332,6 +332,14 @@ Design/decision records and a running log of shipped work (items marked 100 doesn't swallow it). The LUKS-initrd unlock + btrfs-swapfile `resume_offset` triple is a V3 laptop check (QEMU can't feed a boot-time offset for a file that only exists after first boot). + ✓ **Doctor hibernate/sleep section (#77, `VISION § C`):** `nomarchy-doctor` + gained a read-only hibernate row group — resume device on the cmdline + (+ `resume_offset` for a swapfile), disk swap ≥ RAM, zram active, and any + suspend/hibernate error in the previous boot's kernel log. Self-gates + (`skip` on `swap=0`); only `ok`/`warn`/`skip`, never fails the sheet. The + `checks.doctor` VM test asserts the `hibernate` row renders. (Gotcha + recorded: the script runs `set -euo pipefail`, so a no-match `grep` inside + `$(…)` or a `cond && action` aborts it — use `|| true` and `if`.) - ✓ **Hibernate double-unlock:** on resume from hibernate the LUKS passphrase already gates the machine, but locking hyprlock before sleep meant a second password. Fixed by *not locking* before an encrypted diff --git a/flake.nix b/flake.nix index 32e56d0..ce53222 100644 --- a/flake.nix +++ b/flake.nix @@ -428,7 +428,7 @@ # sheet must still exit 0 — i.e. the section runs without # erroring under `set -euo pipefail`. healthy = machine.succeed("nomarchy-doctor 2>&1") - for probe in ["NetworkManager", "fingerprint", "fwupd", "VA-API"]: + for probe in ["NetworkManager", "fingerprint", "fwupd", "VA-API", "hibernate"]: assert probe in healthy, f"doctor hardware section missing a self-gated '{probe}' row:\n{healthy}" ''; }; diff --git a/pkgs/nomarchy-doctor/nomarchy-doctor.sh b/pkgs/nomarchy-doctor/nomarchy-doctor.sh index fd02e83..cbaf521 100644 --- a/pkgs/nomarchy-doctor/nomarchy-doctor.sh +++ b/pkgs/nomarchy-doctor/nomarchy-doctor.sh @@ -263,6 +263,62 @@ if [ "$bat_seen" -eq 1 ] && [ "$bat_limited" -eq 0 ]; then skip "battery charge limit (none set — charges to 100%)" fi +# ── hibernate / sleep (BACKLOG #76) ────────────────────────────────── +# Read-only: is there a working hibernate path? zram is RAM-only and can't +# hold a resume image, so hibernation needs a disk swap (partition or file) +# PLUS resume= wiring; a swapfile additionally needs resume_offset. A +# swap=0 machine opts out cleanly and this section skips. Never fails the +# sheet — an absent or under-sized swap is advisory, not a running fault. +# (|| echo 0 keeps set -euo pipefail happy if a proc file is unreadable.) +disk_swap_kb=$(awk 'NR>1 && $1 !~ /zram/ {s+=$3} END{print s+0}' /proc/swaps 2>/dev/null || echo 0) +zram_kb=$(awk 'NR>1 && $1 ~ /zram/ {s+=$3} END{print s+0}' /proc/swaps 2>/dev/null || echo 0) +ram_kb=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo 2>/dev/null || echo 0) + +if [ "${disk_swap_kb:-0}" -gt 0 ]; then + if grep -q 'resume=' /proc/cmdline 2>/dev/null; then + ok "hibernate: resume device set on the kernel cmdline" + # A swapfile also needs resume_offset to locate the image within it. + if awk 'NR>1 && $1 !~ /zram/ && $2=="file"{f=1} END{exit !f}' /proc/swaps 2>/dev/null \ + && ! grep -q 'resume_offset=' /proc/cmdline 2>/dev/null; then + warn "swapfile in use but no resume_offset= on the cmdline — resume will fail" \ + "add boot.kernelParams resume_offset (docs/MIGRATION.md → Enabling hibernation)" + fi + else + warn "disk swap present but no resume= on the kernel cmdline — hibernate won't resume" \ + "set boot.resumeDevice (docs/MIGRATION.md → Enabling hibernation)" + fi + if [ "${ram_kb:-0}" -gt 0 ] && [ "$disk_swap_kb" -lt "$ram_kb" ]; then + warn "disk swap ($(numfmt --to=iec $((disk_swap_kb * 1024)))) is smaller than RAM ($(numfmt --to=iec $((ram_kb * 1024)))) — a full hibernate image may not fit" \ + "size swap ≥ RAM (docs/MIGRATION.md → Enabling hibernation)" + else + ok "hibernate: disk swap ≥ RAM ($(numfmt --to=iec $((disk_swap_kb * 1024))))" + fi +else + skip "hibernate (no disk swap — swap=0 opt-out; zram alone can't resume)" +fi + +# zram compressed-RAM swap — the memory-pressure layer oom.nix ships on +# by default (#76). Its absence on a Nomarchy box means something is off. +if [ "${zram_kb:-0}" -gt 0 ]; then + ok "zram compressed-RAM swap active ($(numfmt --to=iec $((zram_kb * 1024))))" +else + warn "zram swap not active (the default memory-pressure layer)" \ + "expected on by default — check modules/nixos/oom.nix, then sys-rebuild" +fi + +# Best-effort: a suspend/hibernate failure recorded in the previous boot's +# kernel log. Needs journal read access; silently no-ops without it. The +# `|| true` keeps a no-match grep / missing -1 boot from tripping set -e. +if command -v journalctl >/dev/null 2>&1; then + pmerr=$(journalctl -b -1 -k --no-pager 2>/dev/null \ + | grep -iE 'PM: .*(hibernat|suspend).*(fail|error)|Failed to (hibernate|suspend)' \ + | tail -n1 || true) + if [ -n "$pmerr" ]; then + warn "a suspend/hibernate error is in the previous boot's log" \ + "review: journalctl -b -1 -k -g 'hibernat|suspend'" + fi +fi + # ── verdict ────────────────────────────────────────────────────────── echo if [ "$fails" -eq 0 ]; then