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

194 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 Ghostty** — `btop.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.4**`on: 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 `stayfocused``stay_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 `loadfont`s 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).