diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index d40ccea..5c2c0aa 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -58,16 +58,21 @@ and never re-enables a disabled internal. **Progress 2026-07-15:** - Recovery tooling: undock ends with `dpms on`; `nomarchy-display-wake` / `nomarchy-display-dump` (SUPER+SHIFT+D; TTY if it works). -- **Mitigation (shipped):** do **not** DPMS-off when docked — if any +- **Mitigation (shipped):** do **not** DPMS-off when **clamshell** — if any laptop internal (eDP/LVDS/DSI) exists in `monitors all` but is not enabled, the 10 min listener skips blanking. Lock at 5 min still runs; undocked laptops still DPMS-off as before. Bernardo: Ctrl+Alt+F3 did not yield a usable TTY on the original brick, so prevention > dump path. -- **Revisit later (LATER):** intentional DPMS-when-docked once wake is +- **Scope settled (#136, 2026-07-16):** the gate is *clamshell*, not + "docked" — and Bernardo confirms the incident was exactly that, **laptop + closed**. So the code matched the evidence and only the wording + overclaimed; that is fixed (idle.nix, ROADMAP, and the journal line, now + `dpms-off skipped: clamshell`). Docked with the lid **open** still blanks + and is untested — not exempted, not claimed safe. +- **Revisit later (LATER):** intentional DPMS-when-clamshell once wake is proven trustworthy (monitor power-save without a brick). Tracked as - **#135** (state key + the surface to delete this by) and **#136** (the - gate is clamshell, not "docked" — lid-open docked still blanks). Both - are downstream of the hardware test below. + **#135** (state key + the surface to delete this by), downstream of the + hardware test below. - **Caveat the test must settle:** the mitigation is a cure for an **unconfirmed cause**. "Blanking the only live output caused the brick" is inference from one incident, shipped at V1. If DPMS-off is not the @@ -75,10 +80,67 @@ and never re-enables a disabled internal. sleep) and the brick is still live — so confirming the cause outranks #135/#136. -**Pass (current mitigation):** docked on AC, idle past 10+ min → external -stays lit under hyprlock (journal: `dpms-off skipped: docked`); undocked -still blanks at 10 min and wakes on input. Full original pass (wake after -DPMS-while-docked) deferred to the LATER revisit. +**Pass (current mitigation):** clamshell-docked on AC, idle past 10+ min → +external stays lit under hyprlock (journal: `dpms-off skipped: clamshell`); +undocked still blanks at 10 min and wakes on input. Full original pass (wake +after DPMS-while-clamshell) deferred to the LATER revisit. + +--- + +### The two tests, exactly (2026-07-16) + +**There is no SSH lifeline on this box** (`sshd` inactive), which is why test B +self-recovers instead of relying on one. If you would rather have the net, turn +`services.openssh.enable = true` on for the day and keep a phone SSH client +open — then a brick is a nuisance, not a reboot. + +**Test A — does the mitigation hold? (safe, ~15 min, unattended)** +1. Docked, **lid closed**, on AC. Leave it completely alone for 12 minutes. +2. Expect: at 5 min the external shows hyprlock; at 10 min it **stays lit**. +3. Then: `journalctl --user -b -t nomarchy-idle --since '-15 min'` + → one `dpms-off skipped: clamshell (internal eDP-1 present but off)`. +4. Now open the lid / undock, idle 11 min → the panel **should** blank, and + wake on a keypress. That half must still work; the mitigation is not + supposed to have disabled blanking everywhere. + +**Test B — was DPMS-off actually the trigger? (the decisive one; bounded)** +This is the test that outranks everything else here, because the mitigation is +a cure for an *inferred* cause. Docked, **lid closed**, from a terminal on the +external: + +```sh +( sleep 45 + hyprctl dispatch dpms on + hyprctl reload # re-enables every output (the #142 hammer) +) >/tmp/dpms-test.log 2>&1 & +hyprctl dispatch dpms off +``` + +The screen goes black. Then, in order: +1. **Press keys / move the mouse** in the first ~40s. Does it come back? + (In the incident it did not — this is the exact step that failed.) +2. **Wait for the 45s auto-recovery.** Does the external return by itself? +3. If it is still dark, **open the lid** — the `reload` should have re-enabled + eDP even if the external stayed dead. +4. Afterwards: + `journalctl --user -b -t nomarchy-idle -t nomarchy-display-watch --since '-5 min'` + +**What each outcome means — all three are useful:** +- **Input wakes it** → DPMS-off is **not** the trigger. The mitigation is a + permanent behaviour cost buying nothing, the brick is still live and + un-diagnosed → revert the skip, **#135 never exists**, and hunt the real + cause (hyprlock? hypridle's own resume path? the dock's DP link?). +- **Dark until the 45s recovery, then fine** → the compositor was alive and + `dpms on` works, so blanking-while-clamshell *is* the trigger and the + mitigation is right → **#135 becomes the way to opt back in**. +- **Dark even after recovery** (lid needed) → the original brick, reproduced + **deterministically and on demand**. That is the best outcome: it turns a + once-off incident into something bisectable (try it without hyprlock, with + the lid open, on the internal only …). + +Note the deliberate omission: this runs `dpms off` **by hand** rather than +waiting for hypridle, precisely because the shipped mitigation would otherwise +skip it. Same command, same state, no rebuild. **Fallback if the cause resists (Bernardo 2026-07-16):** diagnosing stays the preference — DPMS when docked is the outcome we want. But the mitigation as @@ -111,25 +173,6 @@ the key in the same commit. trigger, the mitigation is reverted wholesale and this key never exists — building it first risks shipping an option for a policy we withdraw. -### 136. `[human]` #127's gate is clamshell, not "docked" - -The code skips DPMS-off when a laptop internal is present in `monitors all` -but absent from the enabled list — i.e. **clamshell**. Commit 060bf52, the -#127 entry and the journal all say "docked", which is broader: **docked with -the lid open** (internal + externals all enabled) falls straight through to -`hyprctl dispatch dpms off` and still blanks. So either - -- the narrow scope is deliberate (the brick needs the internal disabled, and - lid-open docked is genuinely safe) → fix the wording in 060bf52's - descendants so the docs stop claiming more than the code does; or -- it is an accidental gap → the mitigation misses half the docked cases and - the brick can still fire with the lid open. - -Bernardo's call, and the #127 hardware test is what settles it: if the brick -reproduces lid-open, this is a gap. Cheap either way — one `jq` predicate or -one docs sweep. Filed because a doc that overclaims its own fix is how the -next session mis-diagnoses the recurrence. - ### 129. Permanent guard for the GTK button-colour trap (#98 follow-up) The #98 regression (destructive-action at 1.03:1, invisible) was caught only @@ -148,10 +191,18 @@ Fallout noted while fixing #98: in light palettes the suggested/destructive labels are base00 (cream) on saturated accent/bad — summer-day measures **2.72:1** (suggested) and **2.74:1** (destructive), under AA 4.5 and even under the 3:1 large-text floor. Not a regression: upstream adw-gtk3 does the -same (`color: white` on the accent), so we currently match GNOME. Decide -whether Nomarchy holds itself higher — darker label, or a darker accent mix -for light palettes — and note `tools/check-theme-contrast.py` covers palette -pairings only, never GTK widget surfaces. `[human]` for the aesthetic call. +same (`color: white` on the accent), so we currently match GNOME. + +**Decided 2026-07-16 (Bernardo): fix the contrast — we hold ourselves higher +than GNOME here.** Matching upstream is not a defence when upstream is under +even the large-text floor; "everyone ships it" is how 2.7:1 text becomes +normal. Two shapes to pick between while building: darken the *label* (the +label is ours; the accent is the user's palette and the thing they chose the +theme for), or darken the *accent mix* under the label in light palettes only. +Prefer the first unless it looks wrong — it leaves the palette alone. +Note `tools/check-theme-contrast.py` covers palette pairings only, never GTK +widget surfaces, so whatever lands needs its own assertion or it rots exactly +like #98 did. ### 143. Rofi keeps the selected *row number* when the search changes, so the highlight lands on an unrelated entry @@ -199,17 +250,79 @@ sent: the scoping call is the maintainer's, since keeping the index across a *reload* looks deliberate (#1064 asked for exactly that, and got it in 1.7.4). No duplicate found before filing. -**Options.** (a) Wait for upstream — done, now their move. (b) Carry the patch -ourselves in an overlay — a few lines, but it means **building rofi from source -on every user's machine**: our derivations are in no binary cache (#120's -gotcha), so this trades a real install/rebuild cost for it. (c) Live with it -and say nothing — the status quo, and the reason Bernardo hit it cold. -Recommend (a) + (b) together if the patch stays small: file it upstream, carry -it until it lands, delete on the release that fixes it. `[human]` for whether -a source-built rofi is a price worth paying. +**Decided 2026-07-16 (Bernardo): wait for upstream. Do not carry a patch.** The +overlay option was real but priced badly — our derivations are in no binary +cache, so a patched rofi means **every machine compiles rofi from source**, on +every lock bump, to fix a highlight. That is a permanent, distro-wide install +cost for an annoyance with a free workaround (glance before you press Enter). +**This item is now a watch, not work:** the only thing to do is check +rofi#2317, and take the fix when a release carries it. If it is still open at +the next lock bump, that is fine — nothing here rots meanwhile. + + +## LATER + +- **DPMS when docked (revisit #127):** today we **skip** display blanking + while any laptop internal is present-but-disabled (dock/clamshell), so + idle only locks. That avoids blanking the sole external and the brick + where wake/TTY failed (2026-07-15). Revisit when we want monitor + power-save on dock again: re-enable DPMS-off for that mode only if + `nomarchy-display-wake` (or better) reliably restores the external and + undock-while-black restores eDP on the AMD dock setup — without + depending on SSH. Until then, leave the skip in `modules/home/idle.nix`. +- **Wallpapers artifact split** (ROADMAP § Faster switches — decided, + deferred): pinned `Nomarchy-wallpapers` input so a state write stops + re-copying 86 MB. Follow-on: pre-built theme variants if switches are + still slow after. +- **Installer round 2** (ROADMAP § Installer): multi-disk BTRFS RAID, + impermanence, BIOS/legacy boot. +- **Boot-from-snapshot**: a systemd-boot equivalent of grub-btrfs. +- **MIPI/IPU software-ISP camera** support (no-UVC machines). +- **NixOS release bump → v2** `[human]`: deliberate, hand-edited, never + automated; the previous attempt was discarded (2026-06-22) over a + Hyprland OOM blocker — see MEMORY.md before retrying. + +## FUTURE (decided deferred — not the agent queue head) + +Work we **intend** someday but explicitly **not** NEXT. Agents do not +pick these unless Bernardo promotes one into NEXT/NOW. + +### 20. KVM runner → VM suite in CI `[human]` +**Status (2026-07-10):** keep **eval-only** CI on the current Gitea +stack (act_runner in docker-compose on the 4c/4 GB IONOS VPS). Nested +KVM + RAM headroom on that host are a poor fit next to Gitea; full +`checks.*` VMs stay local / promotion-time until a **separate** +KVM-capable machine exists. + +**When ready:** register a second runner (host-mode nix + `/dev/kvm`, +label `nix-kvm` — not the existing docker eval runner), then uncomment +the `vm-checks` job in `.gitea/workflows/check.yml` (`runs-on: nix-kvm`, +`nix flake check` + toplevel/HM builds). Do not enable the job until +that label is online (Gitea queues forever otherwise). + +### Formatter — adopt later `[human]` +**Intent:** add a Nix formatter (likely `nixfmt-rfc-style`) in a dedicated +pass: reformat the tree once, document in CONVENTIONS, optional CI +check. **Not** the queue head — no drive-by reformats until that pass. + +## PROPOSED (agent suggestions — await human triage) + +*Agents: append here with a one-paragraph pitch (what/why/cost). Do not +implement. Bernardo moves accepted items into a tier.* + +*Open work only. Shipped exam/A–C items (#47–#63, #14, #52 theme +high-ROI, etc.) live in the journal + ROADMAP — not here.* + +### Product / day-2 ### 120. A netinstall ISO, next to the fat offline one +**Deferred to PROPOSED 2026-07-16 (Bernardo): not now.** Nothing below is +stale — the measurements stand and the decision it needs is unchanged. It sits +here because it is the only genuinely large item left, and because the gotcha +at the bottom (no binary cache for our own derivations) probably makes a +cachix the real first step, not the ISO. + Bernardo 2026-07-14, after seeing the measured size: **keep the current ISO exactly as it is** — the guaranteed offline install is the feature it buys — and ship a **much lighter netinstall variant alongside it**. Two products, one @@ -268,62 +381,6 @@ Pass = a second, documented ISO target that is *substantially* smaller (state the measured number, both ISOs built from one tree), installs successfully with a network in a QEMU run, says clearly at boot that it needs one, and leaves the offline ISO's behaviour untouched (`checks.*` for the offline path stay green). - -## LATER - -- **DPMS when docked (revisit #127):** today we **skip** display blanking - while any laptop internal is present-but-disabled (dock/clamshell), so - idle only locks. That avoids blanking the sole external and the brick - where wake/TTY failed (2026-07-15). Revisit when we want monitor - power-save on dock again: re-enable DPMS-off for that mode only if - `nomarchy-display-wake` (or better) reliably restores the external and - undock-while-black restores eDP on the AMD dock setup — without - depending on SSH. Until then, leave the skip in `modules/home/idle.nix`. -- **Wallpapers artifact split** (ROADMAP § Faster switches — decided, - deferred): pinned `Nomarchy-wallpapers` input so a state write stops - re-copying 86 MB. Follow-on: pre-built theme variants if switches are - still slow after. -- **Installer round 2** (ROADMAP § Installer): multi-disk BTRFS RAID, - impermanence, BIOS/legacy boot. -- **Boot-from-snapshot**: a systemd-boot equivalent of grub-btrfs. -- **MIPI/IPU software-ISP camera** support (no-UVC machines). -- **NixOS release bump → v2** `[human]`: deliberate, hand-edited, never - automated; the previous attempt was discarded (2026-06-22) over a - Hyprland OOM blocker — see MEMORY.md before retrying. - -## FUTURE (decided deferred — not the agent queue head) - -Work we **intend** someday but explicitly **not** NEXT. Agents do not -pick these unless Bernardo promotes one into NEXT/NOW. - -### 20. KVM runner → VM suite in CI `[human]` -**Status (2026-07-10):** keep **eval-only** CI on the current Gitea -stack (act_runner in docker-compose on the 4c/4 GB IONOS VPS). Nested -KVM + RAM headroom on that host are a poor fit next to Gitea; full -`checks.*` VMs stay local / promotion-time until a **separate** -KVM-capable machine exists. - -**When ready:** register a second runner (host-mode nix + `/dev/kvm`, -label `nix-kvm` — not the existing docker eval runner), then uncomment -the `vm-checks` job in `.gitea/workflows/check.yml` (`runs-on: nix-kvm`, -`nix flake check` + toplevel/HM builds). Do not enable the job until -that label is online (Gitea queues forever otherwise). - -### Formatter — adopt later `[human]` -**Intent:** add a Nix formatter (likely `nixfmt-rfc-style`) in a dedicated -pass: reformat the tree once, document in CONVENTIONS, optional CI -check. **Not** the queue head — no drive-by reformats until that pass. - -## PROPOSED (agent suggestions — await human triage) - -*Agents: append here with a one-paragraph pitch (what/why/cost). Do not -implement. Bernardo moves accepted items into a tier.* - -*Open work only. Shipped exam/A–C items (#47–#63, #14, #52 theme -high-ROI, etc.) live in the journal + ROADMAP — not here.* - -### Product / day-2 - ### 134. `unstable.` in the downstream — a newer app without a second flake Bernardo 2026-07-15, wanting a newer LM Studio (pinned 0.4.15-2 vs unstable diff --git a/agent/HARDWARE-QUEUE.md b/agent/HARDWARE-QUEUE.md index d0d9f83..de9394c 100644 --- a/agent/HARDWARE-QUEUE.md +++ b/agent/HARDWARE-QUEUE.md @@ -49,20 +49,36 @@ Everything else below stays open; order is convenience, not a gate. restores **only** the radios that were on before. If Wi-Fi was already off, it must stay off after disengage. Glyph must **not** appear when airplane is off. -- [ ] **#127 docked idle — mitigation check (AMD dev box)** — - **Context:** original brick = docked idle → black, no input wake, undock - no eDP, Caps Lock alive; Ctrl+Alt+F3 gave no usable TTY. **Shipped - mitigation:** skip DPMS-off while docked (internal present-but-disabled); - lock at 5m still. Full DPMS-when-docked deferred (BACKLOG LATER). +- [ ] **#127 clamshell idle — TEST A, the mitigation holds (AMD dev box)** — + **Context:** original brick = clamshell-docked idle → black, no input + wake, undock no eDP, Caps Lock alive; Ctrl+Alt+F3 gave no usable TTY. + **Shipped mitigation:** skip DPMS-off while clamshell (internal + present-but-disabled); lock at 5m still. **After `nomarchy-home` + relogin:** - 1. Dock (eDP disabled in `hyprctl monitors all -j` / not in `monitors`). - 2. Idle past 10+ min (or wait for hypridle). - 3. **Pass mitigation:** external still shows hyprlock (not black power-save); + 1. Dock, **lid closed** (eDP in `hyprctl monitors all -j`, absent from + `hyprctl monitors`). + 2. Leave it entirely alone past 10+ min. + 3. **Pass:** external still shows hyprlock (not black power-save); `journalctl --user -t nomarchy-idle --since '-20 min'` has - `dpms-off skipped: docked`. - 4. Undocked control: eDP only → at 10 min screen *does* blank; input wakes. + `dpms-off skipped: clamshell`. + 4. Undocked control: eDP only → at 10 min it *does* blank; input wakes. Optional: `nomarchy-display-dump` / SUPER+SHIFT+D for other display faults. - Related: docking round 8; LATER “DPMS when docked”. +- [ ] **#127 TEST B — was DPMS-off the trigger at all? (AMD dev box)** — + **This one outranks #135**: the mitigation is a cure for an *inferred* + cause, and if the inference is wrong we are paying for it forever while + the brick stays live. Bounded and self-recovering (there is no sshd on + that box). Full protocol + how to read each outcome: **BACKLOG #127 § + "The two tests, exactly"**. In short — docked, lid closed: + ```sh + ( sleep 45; hyprctl dispatch dpms on; hyprctl reload ) & + hyprctl dispatch dpms off + ``` + then (1) try to wake it with input in the first ~40s, (2) let the 45s + recovery run, (3) open the lid if still dark. **Input wakes it** → the + mitigation is pointless, revert it and #135 never exists. **Dark until + recovery** → mitigation confirmed, #135 is the opt-in. **Dark after + recovery** → the brick, now reproducible on demand, which is the best + outcome available. - [ ] **#96 battery-limit row on threshold-less firmware (Acer M5-481T)** — on the Acer (live or installed session with the commit carrying this entry): open System (`SUPER+CTRL+I`). **Pass:** a "Battery limit" row diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index debe1d6..e70ad9c 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -670,11 +670,18 @@ Design/decision records and a running log of shipped work (items marked `nomarchy-suspend` (hypridle + Power menu). LUKS: unlock session before encrypted hibernate phase so resume is LUKS-only (not hyprlock twice). Menu row self-gates on CanHibernate. V3: HARDWARE-QUEUE bag-carry test. -- **Docked idle DPMS (#127 mitigation):** after a docked-idle brick (alive +- **Clamshell idle DPMS (#127 mitigation):** after an idle brick (alive machine, black external, undock no eDP, no usable Ctrl+Alt+F3), hypridle - **skips DPMS-off** when any laptop internal is present but not enabled - (dock/clamshell). Lock at 5 min unchanged; undocked still blanks at 10 min. - Intentional DPMS-when-docked is LATER (BACKLOG) once wake is proven. + **skips DPMS-off** when any laptop internal is present but not enabled. + Lock at 5 min unchanged; undocked still blanks at 10 min. + **Say clamshell, not "docked" (#136, settled 2026-07-16):** the gate is + "an internal exists and is switched off" — the lid shut — and Bernardo + confirms the incident was exactly that, laptop closed. Docked with the lid + **open** has every output enabled, falls straight through, and still blanks; + whether *that* can brick is untested and not claimed either way. The earlier + wording ("skip when docked") promised twice the coverage the code has, which + is how a recurrence gets misread as a regression. + Intentional DPMS-when-clamshell is LATER (BACKLOG) once wake is proven. Tooling kept: `nomarchy-display-wake` / `nomarchy-display-dump`. - ✓ **Generation prune (#128):** weekly `nomarchy-gen-prune` (system + HM): delete only gens older than **14 days** that are also beyond the **3 most diff --git a/modules/home/idle.nix b/modules/home/idle.nix index 0038d59..2cf34ff 100644 --- a/modules/home/idle.nix +++ b/modules/home/idle.nix @@ -52,10 +52,13 @@ let displayTransition = miniTransition; }).displayWakeTool; - # #127 mitigation (2026-07-15): blanking the *only* live output while - # docked (eDP disabled) left a black seat that sometimes would not - # recover (and even VT switch looked dead). Skip DPMS-off when any - # laptop internal is present but not enabled. Lock still runs at 5 min. + # #127 mitigation (2026-07-15): blanking the *only* live output with the + # lid shut (eDP disabled) left a black seat that sometimes would not + # recover (and even VT switch looked dead). Skip DPMS-off when any laptop + # internal is present but not enabled — i.e. CLAMSHELL, which is narrower + # than "docked" and is deliberately what the incident was (#136, Bernardo: + # laptop closed). Docked with the lid OPEN keeps every output enabled and + # still blanks; that case is untested, not exempted. Lock still runs at 5 min. # Revisit: intentional DPMS-on-dock once wake is trustworthy — see # BACKLOG #127 and LATER "DPMS when docked". dpmsOff = pkgs.writeShellScript "nomarchy-dpms-off" '' @@ -69,7 +72,7 @@ let if ! hyprctl monitors -j 2>/dev/null \ | $jq -e --arg m "$m" 'any(.[]; .name == $m)' >/dev/null 2>&1; then ${pkgs.util-linux}/bin/logger -t nomarchy-idle -- \ - "dpms-off skipped: docked (internal $m not enabled)" + "dpms-off skipped: clamshell (internal $m present but off)" exit 0 fi done