fix(display): #148 — enforce dock intent without the auto-profiles toggle
All checks were successful
Check / eval (push) Successful in 3m37s
All checks were successful
Check / eval (push) Successful in 3m37s
Every nomarchy-home re-lit eDP-1 inside the shut lid: HM activation fires hyprctl reload, the catch-all monitor rule re-enables the panel, and the tick invariant built for exactly this (#142, enforce_dock_intent) never fired — it was gated on settings.displayProfileAuto, the *named-profile* auto-switch toggle, which is unrelated to dock intent and off unless opted into. Confirmed live on the incident machine: dockMode=true, displayProfileAuto unset, watcher journal empty — every tick returned at that guard. Fix: drop the auto_on gate. settings.display.dockMode alone is the user's recorded intent (written by the menu and the dock transition); the profile-active check remains as the real guard (a named profile outranks the dock heuristic). No fight with the #142 undock hammer: undock clears the intent before its rescue reload, so the enforcer stands down by ordering. tools/monitor-fallback.nix grows the reload-mid-dock scenario: fake lid closed, hyprctl reload, panel re-disabled within ~4s, action=re-dock logged — under displayProfileAuto's default false, i.e. the exact condition the bug shipped under. Verification: V2 — nix flake check --no-build green, checks.docking-ux green, monitor-fallback harness green three times (implementer twice, reviewer once, ~71s VM). V3 pending: relogin + nomarchy-home while docked-clamshell (HARDWARE-QUEUE; the watcher is exec-once, the fix arms at next login). Implementation by a Sonnet subagent; diagnosis, design and review on Fable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -197,36 +197,6 @@ high-ROI, etc.) live in the journal + ROADMAP — not here.*
|
|||||||
|
|
||||||
### Product / day-2
|
### Product / day-2
|
||||||
|
|
||||||
### 148. Every `nomarchy-home` re-lights the laptop panel inside a shut lid
|
|
||||||
|
|
||||||
Bernardo, 2026-07-16, and he hit it twice in ten minutes: after `nomarchy-home`
|
|
||||||
the laptop panel "turns on again" while docked **lid closed**, and he had to
|
|
||||||
re-set docking by hand from the Display menu. Then an agent's `hyprctl reload`
|
|
||||||
silently undid his fix and re-lit it a second time — confirmed live:
|
|
||||||
`/proc/acpi/button/lid/*/state` = `closed` with `eDP-1` enabled and
|
|
||||||
`dpms_on=true`, i.e. a panel burning inside a shut clamshell with nobody
|
|
||||||
looking at it.
|
|
||||||
|
|
||||||
**Cause is a known hammer with no follow-through.** `hyprctl reload` re-applies
|
|
||||||
every monitor rule and re-enables every output — the ROADMAP already calls this
|
|
||||||
"the #142 hammer", and the undock transition *relies* on it. But
|
|
||||||
`nomarchy-display-watch` only reacts to `monitoradded`/`monitorremoved`; a
|
|
||||||
reload emits neither, so nothing re-asserts the dock profile and the panel just
|
|
||||||
stays on. Any HM activation reloads Hyprland, so **every rebuild while
|
|
||||||
docked-clamshell lights the panel** and leaves it lit until the user notices.
|
|
||||||
Journal for the window is empty — display-watch never saw a thing.
|
|
||||||
|
|
||||||
Cost: wasted power and panel life, and the live display state silently diverging
|
|
||||||
from the profile the user chose — the exact class of "you have to fix it by
|
|
||||||
hand" that the display work exists to remove. Fix is probably to re-assert the
|
|
||||||
dock/clamshell profile *after* a reload rather than only on hotplug: either
|
|
||||||
watch the lid switch as a trigger in its own right (logind already knows), or
|
|
||||||
have the transition/activation path re-run the dock decision once the reload
|
|
||||||
settles. Note the ordering trap — the reload is also the undock rescue, so
|
|
||||||
whatever re-asserts must not fight the rescue that deliberately re-enables eDP
|
|
||||||
when the lid is open.
|
|
||||||
|
|
||||||
|
|
||||||
### 146. `[watch]` hypridle hangs instead of exiting when its compositor vanishes
|
### 146. `[watch]` hypridle hangs instead of exiting when its compositor vanishes
|
||||||
|
|
||||||
**Rewritten 2026-07-16, hours after filing: the premise was disproven and almost
|
**Rewritten 2026-07-16, hours after filing: the premise was disproven and almost
|
||||||
|
|||||||
@@ -28,6 +28,17 @@ Everything else below stays open; order is convenience, not a gate.
|
|||||||
|
|
||||||
## Any machine (dev box is fine)
|
## Any machine (dev box is fine)
|
||||||
|
|
||||||
|
- [ ] **#148 dock-intent enforcement without auto-profiles (dev box, docked)** —
|
||||||
|
after pulling the #148 commit: **log out and back in first** — the
|
||||||
|
watcher is `exec-once`, so `nomarchy-home` alone leaves the old one
|
||||||
|
running. Then, docked clamshell (lid closed, panel off, dockMode set
|
||||||
|
from the Display menu or a prior auto-dock), run `nomarchy-home` or a
|
||||||
|
bare `hyprctl reload`. **Pass:** eDP-1 re-lights for at most ~1–2 s and
|
||||||
|
goes back off by itself; `journalctl -t nomarchy-display-watch
|
||||||
|
--since -5min` shows `dock-intent=true … action=re-dock`; a "Docked —"
|
||||||
|
toast appears. Same relogin answers #149's question (does the Waybar
|
||||||
|
unit still race Hyprland IPC on a cold relogin without linger?).
|
||||||
|
**Fail:** the panel stays lit inside the shut lid (the #148 symptom).
|
||||||
- [ ] **#115 suspend-then-hibernate (laptop with hibernate/resume wired)** —
|
- [ ] **#115 suspend-then-hibernate (laptop with hibernate/resume wired)** —
|
||||||
after system rebuild: Preferences shows **Suspend then hibernate (on)**
|
after system rebuild: Preferences shows **Suspend then hibernate (on)**
|
||||||
(hidden if `CanHibernate=no`). Confirm:
|
(hidden if `CanHibernate=no`). Confirm:
|
||||||
|
|||||||
@@ -19,6 +19,22 @@ Template:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-07-17 — #148: dock intent now enforced without the auto-profiles toggle
|
||||||
|
- **Task:** BACKLOG #148 (promoted from PROPOSED by Bernardo in-session).
|
||||||
|
- **Did:** one-line fix — `enforce_dock_intent` dropped its `auto_on || return`
|
||||||
|
guard (`settings.displayProfileAuto` gates named-profile auto-switching, not
|
||||||
|
dock intent; with it, the #142 invariant never fired for anyone not opted
|
||||||
|
into auto profiles — confirmed live on this box: dockMode=true, auto unset,
|
||||||
|
watcher journal empty). Comment rewritten to record the why.
|
||||||
|
`tools/monitor-fallback.nix` grew a reload-mid-dock scenario. Implementation
|
||||||
|
by a Sonnet agent from my design; diagnosis + review here.
|
||||||
|
- **Verified:** V2 — flake check green, checks.docking-ux green, monitor-
|
||||||
|
fallback harness green ×3 (coder ×2 + my independent re-run): lid closed →
|
||||||
|
reload → panel re-disabled ~4s, `action=re-dock` logged.
|
||||||
|
- **Pending:** V3 queued (relogin + nomarchy-home docked-clamshell; watcher is
|
||||||
|
exec-once so the fix arms at next login). Rides with #149's relogin check.
|
||||||
|
- **Next suggestion:** #127 rewrite from the boot -4 forensics; then #134.
|
||||||
|
|
||||||
## 2026-07-17 — #147: doctor learns to see machine state the flake never set
|
## 2026-07-17 — #147: doctor learns to see machine state the flake never set
|
||||||
- **Task:** BACKLOG #147 (promoted from PROPOSED by Bernardo in-session).
|
- **Task:** BACKLOG #147 (promoted from PROPOSED by Bernardo in-session).
|
||||||
- **Did:** `nomarchy-doctor` gains a machine-state-drift check — linger as the
|
- **Did:** `nomarchy-doctor` gains a machine-state-drift check — linger as the
|
||||||
|
|||||||
@@ -493,6 +493,22 @@ Design/decision records and a running log of shipped work (items marked
|
|||||||
V2 by render, both heads + the password dialog, via the new
|
V2 by render, both heads + the password dialog, via the new
|
||||||
`tools/plymouth-preview.sh`. V3 (a real docked boot/shutdown) is queued: the
|
`tools/plymouth-preview.sh`. V3 (a real docked boot/shutdown) is queued: the
|
||||||
fake heads exist from the start, so the rig cannot resize a canvas.
|
fake heads exist from the start, so the rig cannot resize a canvas.
|
||||||
|
- ✓ **Reload re-lit the panel inside a shut lid (#148, 2026-07-17):** every
|
||||||
|
`nomarchy-home` (HM activation → `hyprctl reload`) re-applied the catch-all
|
||||||
|
monitor rule and left eDP-1 burning in the closed clamshell. The #142 tick
|
||||||
|
invariant `enforce_dock_intent` was built for exactly this and never fired:
|
||||||
|
it was gated on `settings.displayProfileAuto` — the *named-profile*
|
||||||
|
auto-switch toggle, unrelated to dock intent and off for most users.
|
||||||
|
Confirmed on the incident machine: `dockMode=true`, `displayProfileAuto`
|
||||||
|
unset, watcher journal empty — every tick returned at that guard. Fix:
|
||||||
|
drop the gate; `dockMode` alone is the recorded intent, the profile-active
|
||||||
|
check stays as the real guard, and undock's ordering (intent cleared
|
||||||
|
*before* its rescue reload) keeps the enforcer from fighting the #142
|
||||||
|
hammer. V2: `tools/monitor-fallback.nix` grew a reload-mid-dock scenario —
|
||||||
|
fake lid closed → `hyprctl reload` → panel re-disabled in ~4s,
|
||||||
|
`action=re-dock` logged. V3 queued: real relogin + `nomarchy-home` while
|
||||||
|
docked-clamshell (the running watcher is exec-once, so the fix arms at the
|
||||||
|
next login).
|
||||||
- ✓ **Machine state the flake can't see (#147, 2026-07-17):** the QA box ran
|
- ✓ **Machine state the flake can't see (#147, 2026-07-17):** the QA box ran
|
||||||
six months with `Linger=yes` from a pre-migration marker in
|
six months with `Linger=yes` from a pre-migration marker in
|
||||||
`/var/lib/systemd/linger` — set by nothing in the repo, visible to no flake
|
`/var/lib/systemd/linger` — set by nothing in the repo, visible to no flake
|
||||||
|
|||||||
@@ -600,12 +600,21 @@ ${lib.concatStringsSep "\n" (lib.mapAttrsToList
|
|||||||
#
|
#
|
||||||
# `auto_dock` is already idempotent — it returns early unless the internal
|
# `auto_dock` is already idempotent — it returns early unless the internal
|
||||||
# is enabled — so this costs one `hyprctl monitors` per tick and does
|
# is enabled — so this costs one `hyprctl monitors` per tick and does
|
||||||
# nothing in the overwhelmingly common case. Gated on the same display
|
# nothing in the overwhelmingly common case. `settings.display.dockMode`
|
||||||
# profile check as the event path: a profile is a deliberate, more specific
|
# is the user's recorded intent — written by the menu and by the dock
|
||||||
# layout and outranks the dock heuristic.
|
# transition itself (`set_dock_intent`, which `auto_dock` above reaches
|
||||||
|
# via `$TRANSITION dock`) — and that alone is what this is gated on. It
|
||||||
|
# used to also require `auto_on` (`settings.displayProfileAuto`), but that
|
||||||
|
# toggle only controls *named-profile* auto-switching; gating the dock
|
||||||
|
# invariant on it meant enforcement never fired for anyone who had not
|
||||||
|
# separately opted into auto profiles, which is most users. A
|
||||||
|
# `home-manager` activation's `hyprctl reload` re-applies the catch-all
|
||||||
|
# monitor rule regardless, so the panel inside a shut lid came back on
|
||||||
|
# every rebuild while docked (#148). The profile-active check right below
|
||||||
|
# is the real, narrower guard this needs: a profile is a deliberate, more
|
||||||
|
# specific layout and outranks the dock heuristic.
|
||||||
enforce_dock_intent() {
|
enforce_dock_intent() {
|
||||||
[ "$(${sync} get settings.display.dockMode 2>/dev/null)" = true ] || return
|
[ "$(${sync} get settings.display.dockMode 2>/dev/null)" = true ] || return
|
||||||
auto_on || return
|
|
||||||
[ "$(nomarchy-display-profile active 2>/dev/null || echo none)" = "none" ] || return
|
[ "$(nomarchy-display-profile active 2>/dev/null || echo none)" = "none" ] || return
|
||||||
internal=$(internal_output)
|
internal=$(internal_output)
|
||||||
[ -n "$internal" ] || return
|
[ -n "$internal" ] || return
|
||||||
|
|||||||
@@ -124,6 +124,28 @@ pkgs.testers.runNixOSTest {
|
|||||||
assert mons()[0].get("focused") is True, mons()
|
assert mons()[0].get("focused") is True, mons()
|
||||||
print("dock transition: both workspaces on DP-1, internal disabled")
|
print("dock transition: both workspaces on DP-1, internal disabled")
|
||||||
|
|
||||||
|
# #148: every home-manager activation fires `hyprctl reload`, which
|
||||||
|
# re-reads the generated config's catch-all `monitor = ,preferred,auto,1`
|
||||||
|
# and would re-light Virtual-1 even though the session is still docked
|
||||||
|
# with the (simulated) lid shut. `enforce_dock_intent` — the tick-driven
|
||||||
|
# invariant in the watcher — is what is supposed to put it back off.
|
||||||
|
# Before the #148 fix it also required `settings.displayProfileAuto`
|
||||||
|
# (the *named-profile* auto-switch toggle, unrelated to dock intent), and
|
||||||
|
# this harness never sets that — it stays at its schema default `false`
|
||||||
|
# — which is exactly the condition the bug shipped under: the invariant
|
||||||
|
# silently never fired for any session that had not separately opted
|
||||||
|
# into auto profiles. Re-assert the lid is shut (it already is, from the
|
||||||
|
# tmpfiles seed, but do so explicitly since this scenario depends on it),
|
||||||
|
# reload, and prove the watcher re-disables the panel and logs the
|
||||||
|
# re-dock action.
|
||||||
|
machine.succeed("printf 'state: closed\\n' > /run/nomarchy-test-lid-state")
|
||||||
|
machine.succeed(hy + "reload'")
|
||||||
|
machine.wait_until_succeeds(
|
||||||
|
hy + "-j monitors' | jq -e 'all(.[]; .name != \"Virtual-1\")'", timeout=15
|
||||||
|
)
|
||||||
|
machine.succeed("journalctl -t nomarchy-display-watch | grep -q 'action=re-dock'")
|
||||||
|
print("reload-mid-dock: panel re-disabled by enforce_dock_intent; re-dock logged (#148)")
|
||||||
|
|
||||||
# QEMU's synthetic headless backend aborts Hyprland if its last active
|
# QEMU's synthetic headless backend aborts Hyprland if its last active
|
||||||
# output is deleted while the DRM output is disabled. Exercise the exact
|
# output is deleted while the DRM output is disabled. Exercise the exact
|
||||||
# production recovery primitive first, then remove DP-1: the real
|
# production recovery primitive first, then remove DP-1: the real
|
||||||
|
|||||||
Reference in New Issue
Block a user