Files
Nomarchy/agent/MEMORY.md
Bernardo Magri 748d4af414
Some checks failed
Check / eval (push) Has been cancelled
fix(install): bake dconf, seed main, name ISO, display modes (#123–#126)
Batch of existing-bug fixes from Acer/XPS install findings:

- #123: HM pre-activate creates /run/user/$UID and prefers
  dbus-run-session so dconf no longer aborts the desktop bake
- #124: main-built ISOs seed ?ref=main; patch-template rewrites
  inputs.nomarchy.url from NOMARCHY_FLAKE_URL (was set but unused)
- #125: image.baseName → nomarchy-live-….iso (not nixos-live-…)
- #126: Display mode list sorted by pixel area; toast shows hyprctl
  size; TESTING notes QEMU fixed-window scaling illusion

Verified: V0 flake check, installer-safety, baseName eval, patch-template
flakeUrl rewrite, mode-sort fixture.
2026-07-15 09:58:53 +01:00

13 KiB

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 (Kitty --class=…). SoftGL may still struggle with a GPU terminal; size can ignore percent windowrules if client geometry wins.
  • theme-shot softGL may not start Kittybtop.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).
  • 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 needs guest GL (virtio-vga-gl, gl=on) in interactive QEMU or the session won't start; black screen ≈ missing GL (docs/TESTING.md § gotchas). Kitty is the sole terminal (no Ghostty).
  • 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)

  • Installer HM pre-activate needs XDG_RUNTIME_DIR (+ session bus) (#123): runuser … activate inside nixos-enter has no user session. Without mkdir -p /run/user/$UID owned by the install user and XDG_RUNTIME_DIR (prefer dbus-run-session -- activate), dconf dies with Permission denied and the bake aborts mid-way — first boot looks unthemed / missing nm-applet even though the system installed.
  • ISO filename is image.baseName (#125): volumeID alone does not rename result/iso/…; force image.baseName to nomarchy-… or the artifact stays nixos-live-….iso.
  • Install flake ref must match the ISO branch (#124): while v1 lags main, seed ?ref=main (and compose-lock original) from main-built ISOs; NOMARCHY_FLAKE_URL must actually rewrite inputs.nomarchy.url (was set but unused until #124).
  • Gum filter returns unmatched typed text by default; catalog-only pickers require --strict plus an independent exact-membership validation boundary.
  • 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).