Files
Nomarchy/agent/MEMORY.md
Bernardo Magri 7bfe1af5b1
All checks were successful
Check / eval (push) Successful in 3m16s
fix(power): coalesce charge-limit reapply events
USB-C power event bursts could repeatedly restart and SIGTERM the settling oneshot until systemd reached its start limit. Let successful runs return inactive and use coalescing start jobs for immediate and delayed reapplication.

Verified: V0 nix flake check --no-build, module parse, and git diff --check; V2 focused battery-charge-limit KVM test with a 12-event active-run burst. V3 hardware item #101 remains queued.
2026-07-13 14:09:05 +01:00

12 KiB
Raw Blame History

Memory — durable lessons, learned the hard way

Curated, not append-only: one line per fact, newest at the top of its section; delete entries that stop being true. Details usually live in a docs/ROADMAP.md decision record — pointer given as (§ item). Add a fact here the moment a debugging session teaches you something a future iteration would otherwise rediscover.

Testing & VM recipes

  • Doctor float V2: THEME=<slug> nix build --impure -f tools/doctor-float.nix — softGL Hyprland, nomarchy-menu doctor, asserts class=com.nomarchy.doctor + floating + centered midpoints + screenshots. Ghostty does start here under softGL (classed one-shot); size often stays Ghosttys default 800×600 (client geometry wins over the percent size windowrule).
  • theme-shot softGL cannot start Ghosttybtop.png is best-effort (usually identical to desktop). Guest asserts on ~/.config/btop/themes/nomarchy.theme prove baking; the TUI look is hardware/GL tier (HARDWARE-QUEUE). Doctor float is the counter-example that a classed one-shot Ghostty can still map under softGL.
  • Waybar custom/doctor tripwire: status helper must invoke nomarchy-doctor by absolute store path (waybar's env can miss system PATH → command -v … || exit 0 self-hides forever); empty "text" also self-hides; strip ANSI before packing the tooltip; use signal 10 + format = "{}". theme-shot asserts class:bad + glyph and pokes RTMIN+10 before the desktop shot.
  • tuigreet dies silently under runNixOSTest (even bare, no theme flag: greetd sits as "(greetd)" with no child, nothing in the journal — its stderr goes to the VT) — nixpkgs' own greetd test uses agreety instead. Greeter rendering is interactive-ISO/hardware tier; don't burn another session on a checks.greeter VM test.
  • 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.
  • Don't default a "timer/session" feature to V3 — most of it is VM-testable. A scheduled/session behaviour usually decomposes into a generic step already covered elsewhere (e.g. home-manager switch, exercised by every theme apply) and a specific decision (which theme, when). Stub the generic step (NOMARCHY_REBUILD=<marker> for theme-sync) and simulate time by moving the VM clock (date -s, timedatectl set-ntp false, time.timeZone = "UTC"), then assert the decision + state change headlessly. checks.auto-theme does exactly this for #79's sunset/sunrise. Only the literal timer-fires-on-OnCalendar is truly on-hardware, and systemd-analyze calendar validates that schedule. (I first mis-framed #79 as V3 — it's V2.)
  • 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 unit-test THAT (monitor-rules.nix / checks.display-profiles is the pattern).
  • Hibernation reference (Latitude / Newton, BACKLOG #76): LUKS whole root BTRFS; @swap/swap; file /swap/swapfile; boot.resumeDevice = LUKS root UUID; resume_offset from btrfs inspect-internal map-swapfile -r. Swap is encrypted with root (not a cleartext partition). No zram on that box yet — zram is additive for live pressure only. Installer already creates this when swapSize>0.
  • CI (.gitea/workflows/check.yml) is eval-tier only (standing decision 2026-07-10): act_runner docker-compose on the Gitea VPS; no KVM there. Full VM suite is BACKLOG FUTURE #20, not NEXT — needs a separate nix+/dev/kvm runner. Container gotchas are in the workflow header (single-user Nix + nixbld users, sandbox=false for Stylix IFD, Nix pinned 2.31.5 vs lazy-trees, no JS actions past node20).
  • The Gitea instance is 1.25.4on: schedule workflows are supported; bump.yml assumes the Actions token can push to main (standard Gitea behaviour, but unconfirmed until the first run lands).
  • The git server is Gitea (gitea/act_runner via docker-compose), NOT Forgejo — workflows are read from .gitea/workflows/ (or .github/), never .forgejo/workflows/ (a whole push cycle was lost to that).
  • Reusable headless VM harness: checks.* via runNixOSTest — existing examples to crib from: distro-id (boots + switch-to-configuration dry-activate), hardware-toggles (kernel cmdline/PAM assertions), battery-charge-limit (fake Mains adapter via test_power, real udev event burst while the oneshot is active; clean inactive result plus an InvocationID change proves coalesced AC re-apply). AC udev hooks for a settling oneshot must use start, never restart: USB-C docks emit event bursts and restarts SIGTERM the in-flight pass into start-limit-hit.
  • Themed-desktop screenshots work headlessly: software-GL Hyprland (LIBGL_ALWAYS_SOFTWARE on virtio-gpu) + machine.screenshot() QMP dump — prototyped 2026-06-19, kept as the fallback for theme previews (§ Visual theme picker).
  • Hyprland/Ghostty need guest GL (virtio-vga-gl, gl=on) in interactive QEMU or the session won't start; black screen ≈ missing GL (docs/TESTING.md § gotchas).
  • No KVM = slow, not broken; don't read slowness as failure.

Known-broken / watchlist

  • btrfs-assistant "segfault" was unprivileged-only (re-diagnosed 2026-07-04): libbtrfsutil's unprivileged subvolume iteration crashes on btrfs-progs 6.17.1 (upstream-fixed after); as root it works, and the pkexec launcher runs it as root. The real distro bug was no polkit agent in the session (every pkexec failed silently) — hyprpolkitagent now ships (hyprland.nix exec-once). checks.snapshot-gui guards the root path. Lesson: before "app X is broken", check WHO it runs as — and whether polkit prompts can render at all (§ Snapshot browse/restore).
  • NixOS release bump is a trap: the discarded attempt (branch deleted 2026-06-22) hit a Hyprland OOM blocker; a redo is a deliberate v2, never part of routine lock bumps.
  • theme-state.json is git-tracked inside an 86 MB flake tree, so every state write re-copies the source before eval — the wallpapers-artifact split (BACKLOG LATER) is the decided fix (§ Faster switches).
  • Friendly theme-state load (modules/theme-state-read.nix, #66): builtins.tryEval does not catch readFile/fromJSON failures — gate with pathExists + empty/non-object checks before fromJSON. Subtle JSON syntax errors still surface from nlohmann (line/col); field schema stays in theme.nix. mkFlake must builtins.seq the check onto the whole return set or lazy attr access skips it.

Design invariants

  • Dock transitions are ordered safety operations (#100): dock in one hyprctl --batch (external on → every internal workspace moved → focus external → internal off); undock enables internal before moving anything. The Hyprland watcher, not a shell-pipeline subshell, owns a low-level handle-lid-switch inhibitor until the lid is physically open; startup cleans a validated stale process group. HDMI availability may appear only as change:sink, so fresh monitoradded is the intent boundary for the settled PipeWire/WirePlumber reprobe + sink pick; generic audio changes must not override a manual in-dock speaker choice.
  • Waybar status is never color-only (item 28 sweep, iteration #69): every status module must distinguish its states by SHAPE (glyph) or presence (self-hide), never color alone — good/warn/bad collapse under color-blindness. When adding a state, give it a distinct glyph or gate the module on it; a new class that only recolors an existing glyph is a regression. Suppressed notification states (DND and app-inhibited) all use the bell-off glyph + @muted.
  • Identity themes are not traffic lights (#69): white, vantablack, lumon, hackerman, matte-black, miasma — monochrome / mono-hue / earthy status by design. audit-theme-design.py tags their hue/CVD/ANSI-family findings [identity]; do not "fix" them into R/Y/G.
  • Import hierarchy ≠ ANSI (#70): import-palettes.py must not set surface==overlay when color0==color8; light color0 is often ANSI black (not a chip). Roles are first-class — never bulk-reimport shipped JSON without a hierarchy pass.

Gotchas (cost a debugging session once)

  • Waybar layer: top renders above even real-fullscreen windows — the bar draws over a fullscreen video. layer: bottom lets the fullscreen surface cover it while the exclusive zone still reserves the bar's space in normal tiling (trade-off: floating windows can now overlap the bar strip). Set in both waybar.nix and every whole-swap jsonc (item 30).
  • Hyprland binds match the exact modmask: a shifted keysym (question) needs SHIFT in mods or the bind never fires — the keypress falls through to the focused window (§ item 26; caught on hardware, invisible to eval-tier tests).
  • Never kill a Wayland session-lock client (hyprlock): its crash failsafe drops to a tty instead of unlocking (§ Hibernate double-unlock).
  • rofi element-icon size is one value = a square cell; WxH silently collapses and non-square icons letterbox — pre-crop images square at build (§ Visual theme picker).
  • WirePlumber 0.5 monitor rules can only early-match device.api; device.product.name etc. bind after the rule runs — surgical libcamera scoping is impossible (§ Webcam).
  • hyprctl switchxkblayout is a global layout flip; per-device isolation needs device[<name>]:kb_layout keywords (§ Keyboard layouts).
  • Waybar's clock captures the timezone at construction — a zone change needs SIGUSR2 (watcher in timezone.nix) (§ Automatic timezone).
  • Waybar persistent_workspaces (underscore) is dead syntax silently ignored; the hyphen form is honoured and renders phantom workspaces (§ Waybar shows non-existent workspaces).
  • GTK4/libadwaita/Qt6 read light/dark from the portal's org.freedesktop.appearance color-scheme (dconf), not Stylix polarity (§ GTK/Qt ignore the theme's mode).
  • Update order matters downstream: nomarchy-pull (lock) then nomarchy-rebuild then nomarchy-home, or desktop changes are silently skipped against the old lock (README § 3).
  • Hyprland 0.55 renames stayfocusedstay_focused (and similar underscore effects); stayfocused 1 is invalid field type at parse (§ polkit workspace rules, 2026-07-10 hardware).
  • Never gate a safety listener behind the optional feature it also serves: the display menu existed with no profiles while its blackout rescue did not. Rofi defaults to mouse-pointer output (monitor=-5), which is stale in clamshell mode; use focused output (-1) for keyboard-launched UI.
  • Hyprland 0.53 rewrote window rules: windowrulev2 is a hard error and the old rule-first float, class:^…$ no longer parses — both surface a red config-error banner on the default desktop. Hyprlang legacy form is now <effect> <value>, match:<prop> ^…$ (e.g. float 1, match:class ^…$); effects carry a value, matchers take match: (§ windowrule migration).
  • grub loadfonts every .pf2 in a theme dir — reuse a bundled DejaVu rather than shipping fonts (§ Distro branding).
  • Agent instructions live vendor-neutrally in agent/ (VERIFICATION, DELEGATION, THEME-DESIGN; entry AGENTS.md) — .claude/ is a thin adapter (permissions + subagent defs only; skills were removed 2026-07-11). Never git add -A blindly: check git status --short for genuine strangers first (settings.local.json, harness-dropped files) and commit with explicit pathspecs (§ loop hygiene).