All checks were successful
Check / eval (push) Successful in 3m49s
Answering "how do I give you root": you don't. tools/plymouth-preview.sh renders the splash — LUKS dialog included — into X11 windows in ~10s with no reboot, no root and no DRM: - unshare -rm = user + mount namespace; every mount dies with it. - /run/plymouth is BOTH the socket dir and the compiled-in plugin path (`strings plymouthd` → /run/plymouth/plugins/), so we bind a writable dir over it AND recreate its symlinks — miss those and plymouth can't find x11.so, silently skips X11 and reaches for a tty. - never pass --tty (main.c:2133 takes the X11 path only while default_tty is unset); /dev/dri is masked so a preview physically cannot mode-set a real output — which matters on a docked session (#127). - PLY_CREATE_FAKE_MULTI_HEAD_SETUP fakes 800x600 + 640x480 heads. - debug wants plymouth.debug=stream: — `file:` buffers and yields nothing. What it proved immediately: with a STABLE two-head canvas the current theme centres correctly on both heads. So the geometry is fine and #137 really is the timing bug (positions frozen at parse time vs a canvas that resizes when a head arrives) — recorded in the item. And it caught my own fix: hoisting positions into layout() rendered the background on both heads and nothing else — no logo, no bar — with NO parse error logged. Reverted rather than shipped, because that script draws the passphrase box on an encrypted machine. #137 carries the failure, the prime suspect (plymouth script scope: assignment lands in `local` unless the name already exists) and the fast edit/render loop for the next attempt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
544 lines
30 KiB
Markdown
544 lines
30 KiB
Markdown
# Backlog — the prioritized task queue
|
||
|
||
**This is the only executable work list for agents.** Product themes and
|
||
v1.0 intent live in [`docs/VISION.md`](../docs/VISION.md); design history
|
||
in [`docs/ROADMAP.md`](../docs/ROADMAP.md); map in
|
||
[`docs/README.md`](../docs/README.md) and [`agent/README.md`](README.md).
|
||
|
||
**Rules:**
|
||
- Agents take the topmost actionable item (see LOOP.md). Finished items
|
||
are **deleted** here — the journal + git log are the record; durable
|
||
design notes get a ✓-entry in docs/ROADMAP.md (and/or a note in VISION)
|
||
if worth keeping.
|
||
- Item numbers are **stable IDs** — never renumbered or reused. A gap in
|
||
the sequence means shipped (or dropped) work; new items take the next
|
||
free number regardless of tier.
|
||
- Tags: `[blocked:hw]` needs real hardware (see HARDWARE-QUEUE.md) ·
|
||
`[human]` needs Bernardo · `[stuck]` two failed attempts, needs help ·
|
||
`[big]` must be split before starting.
|
||
- Agents may append to **PROPOSED** and **Decisions** freely (include
|
||
`VISION § …` or `ROADMAP § …` when relevant); only Bernardo moves items
|
||
*out* of PROPOSED into the tiers.
|
||
|
||
---
|
||
|
||
## NOW
|
||
|
||
### Live ISO / install hardware findings — Acer Aspire M5-481T + Dell XPS 9350
|
||
|
||
Bernardo, real installs 2026-07-13–14 (photos of the install end screens and
|
||
post-boot sessions). Preserve separation: the installer bake failure and the
|
||
flake pin are different root causes even when they show up on the same machine.
|
||
(Terminal / Ghostty-on-Acer → shipped as Kitty-only, #95.)
|
||
|
||
### 127. `[blocked:hw]` Docked idle: black screens, no wake, undock does not restore panel
|
||
|
||
Bernardo 2026-07-15 (AMD dev box, docked): left the laptop docked; after a
|
||
few minutes of idle it appeared to "sleep". Then:
|
||
|
||
1. External keyboard/mouse did **not** wake any display.
|
||
2. Unplugging the external monitor did **not** turn the laptop panel back
|
||
on (dock mode has eDP disabled — undock *should* re-enable it).
|
||
3. The machine was still alive: Caps Lock LED toggled on the keyboard.
|
||
|
||
This also killed a long agent session (looked like a "crash" from the
|
||
outside). Separate from awake undock recovery (HARDWARE-QUEUE round 8):
|
||
the failure is **idle while docked**, then no path back to a usable panel.
|
||
|
||
**Likely shape (code-side, unconfirmed on the incident):** on AC, hypridle
|
||
does **not** call `systemctl suspend` (`onAc || suspend` — only battery
|
||
suspends at 15 min). Idle path is lock @5 min + `dpms off` @10 min
|
||
(`modules/home/idle.nix`). Caps Lock working fits **DPMS/lock blackout**
|
||
better than deep S3. Dock mode has already disabled eDP; recovery then
|
||
needs either (a) input → hypridle `on-resume` / `dpms on` on the external,
|
||
or (b) undock → `nomarchy-display-transition undock` re-enabling the
|
||
panel. Both failed. `after_sleep_cmd` only does `hyprctl dispatch dpms on`
|
||
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
|
||
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
|
||
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.
|
||
- **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
|
||
trigger, we are paying a permanent behaviour cost (docked externals never
|
||
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.
|
||
|
||
**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
|
||
shipped holds a near-static lock screen on the externals indefinitely, so if
|
||
the cause cannot be pinned down, a **screensaver while docked** is the
|
||
consolation prize: it protects the panels (burn-in — the same cost #135
|
||
names) without re-entering the blanking path that bricked the session. Not a
|
||
new item: it is the "if diagnosis fails" branch of this one, and #135's state
|
||
key is where it would land.
|
||
|
||
## NEXT
|
||
|
||
### 135. Give the #127 DPMS mitigation a state key (and a way back)
|
||
|
||
The shipped mitigation is hardcoded policy: docked (clamshell) externals now
|
||
**never** blank, and hyprlock has no timeout of its own, so they stay lit
|
||
indefinitely. Two real costs land on the user — a couple of 27" panels at
|
||
~60–100W all night, and **OLED burn-in** from a near-static lock screen held
|
||
for hours. Today there is no way to opt back in, which also breaks the
|
||
in-flake-state rule that settings are menu-writable.
|
||
|
||
Add `settings.idle.dpmsWhenDocked` (default **false** = today's safe
|
||
behaviour), read by `modules/home/idle.nix`'s `dpmsOff` gate, with a
|
||
Preferences row. The real point is not the option: it gives the mitigation a
|
||
**named surface to delete** once wake is proven, instead of an unowned LATER
|
||
note that quietly becomes the product. Whoever removes the mitigation removes
|
||
the key in the same commit.
|
||
|
||
**Order:** after the #127 hardware test. If DPMS-off turns out not to be the
|
||
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
|
||
by rendering a dialog — every scripted check was green, because the bug lives
|
||
in adw-gtk3's `mix(@destructive_color, alpha(currentColor,…))` background, not
|
||
in our CSS. `tools/theme-shot.nix` is deliberately not a CI gate (full-desktop
|
||
VM), so add the cheap static half: assert that for every theme, any rule that
|
||
pins a button label colour also pins that button's `background-color` — i.e.
|
||
no pinned label may sit on a `currentColor`-derived background. Guards the
|
||
class, not the instance. The scratch render harness that found it is worth
|
||
promoting to `tools/dialog-shot.nix` as a maintainer tool alongside theme-shot.
|
||
|
||
### 130. GTK accent buttons sit at ~2.7:1 in light themes
|
||
|
||
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.
|
||
|
||
### 132. VM cannot judge rofi menu geometry
|
||
|
||
The guest renders the picker with icons at roughly a quarter of `ui.iconSize`
|
||
(44 → ~10px) and rows short enough that a 6-row root clips its last entry
|
||
behind a scrollbar, though `lines = 8` and `dynamic = true` should fit it.
|
||
Root is unchanged since before #105, so this is the guest's font/icon
|
||
environment, not a regression — but it means `theme-shot.nix`/`menu-shot`
|
||
screenshots are trustworthy for **menu content** and not for spacing, icon
|
||
size, or truncation. Either fix the guest (fontconfig + icon cache in the
|
||
test node) or document the gap in docs/TESTING.md §5 so the next agent does
|
||
not chase it — right now nothing warns them.
|
||
|
||
### 133. No check pins the #107 legacy-name compat shim
|
||
|
||
`lib.mkFlake`, doctor and lifecycle all still accept `theme-state.json`, and
|
||
`nomarchy-state-sync` migrates it on write — but nothing in `checks.*`
|
||
evaluates a checkout that has *only* the legacy name, so the shim can rot
|
||
silently while every check stays green. It is load-bearing until every
|
||
existing machine has taken one menu write. Add a cheap eval check: a fixture
|
||
dir with `theme-state.json` only → `mkFlake` resolves it; plus the write-side
|
||
migration (state.json created + git-staged, legacy `git rm`'d). Proven by
|
||
hand 2026-07-15 (drvPaths identical across the rename) — this just keeps it
|
||
proven. Delete together with the shim + the `nomarchy-theme-sync` alias.
|
||
|
||
### 137. Plymouth splash draws on head 0's geometry, so it is off-centre when docked
|
||
|
||
Bernardo 2026-07-16 (T14s, docked): the boot **and** shutdown logo is not
|
||
centred on the external monitor.
|
||
|
||
**The first diagnosis here was wrong — do not act on it.** It claimed
|
||
`Window.GetWidth()` returns head 0 and that the fix was per-head sprites. Both
|
||
are false, and per-head sprites would have been a *regression* (duplicate
|
||
logos). Read from plymouth's own source (`plymouth.src`, the build nixpkgs
|
||
ships) rather than from a summary:
|
||
|
||
- `Window.GetWidth()` **without an index returns `data->max_width`**
|
||
(`script-lib-sprite.c:227`) — the widest head, not head 0.
|
||
- The script plugin builds a virtual canvas of `max_width × max_height` and
|
||
**centres every display inside it**: `display->x = (max_width - width) / 2`
|
||
(`:536`). Sprites are drawn per display at `sprite->x - display->x` (`:452`).
|
||
- Therefore a canvas-centred sprite **is** centred on every head, and today's
|
||
`Window.GetWidth()/2 - w/2` is *correct at the instant it runs*. Note the DRM
|
||
renderer sets every head's `area.x = 0` (`renderers/drm/plugin.c:612`) — the
|
||
origins that matter are the plugin's computed ones, not the crtc's.
|
||
|
||
**The real bug is time, not geometry.** `nomarchy.script` computes every
|
||
position as a **top-level statement at parse time** (`:9`, `:15–16`, `:129`,
|
||
`:210`, `:219`), i.e. once, against whatever canvas existed at that moment.
|
||
When a display arrives or leaves *during* the splash — which is exactly
|
||
"booting up / shutting down **with** an external" — `max_width/max_height`
|
||
change, the plugin re-centres each display (`:532-537`), but the sprite's frozen
|
||
canvas coordinate does not move with it. Every element then sits off by
|
||
`(new_max - old_max) / 2` **on every head**. A single monitor never changes the
|
||
canvas, which is precisely why this has never been seen undocked.
|
||
|
||
**Fix shape:** hoist the position math into a `reposition()` and call it from
|
||
the existing `refresh_callback` (already running at 50 fps) whenever
|
||
`Window.GetWidth()/GetHeight()` differ from the last canvas — logo (scale
|
||
included), entry, lock, any live bullets, progress box/bar. Keep it a
|
||
canvas-change check, not an every-frame recompute.
|
||
|
||
**The rig works, needs no root, and is in `scratchpad/ply-rig.sh`** (recipe
|
||
below — rebuild it, it is ~20 lines). Rendered on the dev box 2026-07-16:
|
||
|
||
- `unshare -rm` gives a user + mount namespace: root *in there* only, every
|
||
mount invisible to the real system and gone when it exits.
|
||
- `/run/plymouth` is BOTH the daemon's socket dir **and** its compiled-in
|
||
plugin/theme path (`strings plymouthd` → `/run/plymouth/plugins/`). Bind a
|
||
writable dir over it **and recreate its three symlinks** (`plugins`,
|
||
`themes`, `plymouthd.defaults` → `/etc/plymouth/…`) or plymouth cannot find
|
||
`x11.so`, silently skips X11 and reaches for a tty.
|
||
- **Do not pass `--tty`**: `main.c:2133` only takes the X11 path while
|
||
`default_tty` is unset. Bind an empty dir over `/dev/dri` too — with X11
|
||
first in the renderer order (`ply-renderer.c:283`) DRM is never used, and the
|
||
mask makes that a guarantee rather than a hope.
|
||
- `PLY_CREATE_FAKE_MULTI_HEAD_SETUP=1` → 800×600 + 640×480 windows.
|
||
`plymouth show-splash`, then `plymouth ask-for-password` renders the LUKS
|
||
dialog. Debug: `--kernel-command-line="plymouth.debug=stream:/path splash"`
|
||
(**`stream:`**, not `file:` — `file:` buffers and you get nothing).
|
||
- Fast loop: copy the theme into the rig's `themes/` and sed its `.plymouth`
|
||
`ScriptFile=`/`ImageDir=` to the copy — then editing the script is a 10s
|
||
render instead of a 3min `nixos-rebuild`.
|
||
|
||
**What the rig already proved (2026-07-16):** with a *stable* two-head canvas
|
||
the **current** theme centres correctly on both heads — logo and progress bar
|
||
each in their own head's middle. So the geometry is not the bug, which is the
|
||
evidence for the timing diagnosis above. The fake path builds both heads up
|
||
front, so it cannot reproduce a canvas *change*: for that, plug/unplug while
|
||
the splash is up, or compare a boot with and without the external.
|
||
|
||
**Attempt 1 failed, and the rig caught it — that is the point.** Hoisting the
|
||
positions into a `layout()` called from `refresh_callback` rendered the
|
||
background on both heads and **nothing else**: no logo, no progress bar. The
|
||
script plugin logs `starting boot animation` and **no parse error**, so it
|
||
fails *silently* — reverted rather than shipped (that script draws the LUKS box
|
||
on an encrypted machine). Next session: bisect with the fast loop. Prime
|
||
suspect is scope — plymouth script assigns into `local` unless the name already
|
||
exists, so a `logo.image = …` inside a function may not be the `logo` the rest
|
||
of the script sees, even though the same pattern works in `update_progress_bar`.
|
||
Test that hypothesis *first*, in isolation, before rewriting anything.
|
||
|
||
Pass = with the rig, a two-head render centred on both **plus the password
|
||
dialog visible**; then a real docked boot + shutdown, undocked unchanged.
|
||
|
||
### 143. Rofi keeps the selected *row number* when the search changes, so the highlight lands on an unrelated entry
|
||
|
||
Bernardo 2026-07-16, and his repro is exact: open the app menu, type `ca`, move
|
||
the selection two items right (onto ChatGPT), press backspace once (query is
|
||
now `c`). The list re-expands — first entry is now Slack — but the highlight
|
||
stays on the **third row**, which is now GHex, an app that has nothing to do
|
||
with what he typed. Enter at that moment launches the wrong thing.
|
||
|
||
**Cause — upstream rofi (2.0.0, `source/widgets/listview.c`), not our config.**
|
||
A filter change ends in `listview_set_num_elements()`, which does not reset the
|
||
selection; it re-asserts the old one:
|
||
|
||
```c
|
||
lv->req_elements = rows;
|
||
listview_set_selected(lv, lv->selected); /* the *previous* index */
|
||
```
|
||
|
||
and `listview_set_selected()` only clamps it to the new bounds:
|
||
|
||
```c
|
||
lv->selected = MIN(selected, lv->req_elements - 1);
|
||
```
|
||
|
||
So what is preserved is the **row number**, not the entry — nothing checks that
|
||
row 2 still holds the thing that was selected, or that it matches the query at
|
||
all. Typing *more* hides this (the list shrinks, the clamp usually drags the
|
||
highlight along); **backspacing exposes it**, because the list grows back
|
||
underneath a stationary index. There is no option for it: `-dump-config` has
|
||
nothing about selection-on-refilter, and `auto-select` is a different feature
|
||
(it fires only when exactly one row survives).
|
||
|
||
**One correction to the report:** it is not the app menu. `listview` is the
|
||
widget behind **every** rofi surface we ship, so our text menus (Recovery,
|
||
System, Preferences…) do the same. It is merely far more visible in drun, where
|
||
the list is long and — because we set `sort = true` — reorders on every
|
||
keystroke, so a stationary index points somewhere new each time. In a short
|
||
text menu you would probably see it and correct without noticing why.
|
||
|
||
**Reported upstream 2026-07-16: https://github.com/davatorium/rofi/issues/2317**
|
||
(open, filed as `bemagri`, with the 2.0.0 line numbers and the observation that
|
||
`rofi_view_set_selected_line` already maps a stable id through `line_map` — so
|
||
the fix reuses an existing primitive rather than inventing one). No patch was
|
||
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.
|
||
|
||
### 120. A netinstall ISO, next to the fat offline one
|
||
|
||
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
|
||
distro: "works on a plane" and "8 GiB is absurd to download" are both true, and
|
||
a second target settles them without compromising either.
|
||
|
||
**Measured facts (2026-07-14), so this starts from numbers, not vibes.**
|
||
*(These stand as measured: #121 would have cut ~0.67 GiB of duplicate chromium
|
||
from them, but it was **reverted** — decided against, ROADMAP § one chromium,
|
||
not two. If a netinstall ships, revisit it: the duplicate is worth ~195 MiB of
|
||
**download**, which is this item's whole currency, even though it is worth
|
||
almost nothing on disk or on the ISO.)*
|
||
|
||
> **Read this before using the numbers below.** They are **closure arithmetic**,
|
||
> and #121 proved the hard way that closure size is neither disk size nor image
|
||
> size: removing a 687 MiB path shrank the ISO by **8 KiB**, because
|
||
> **mksquashfs dedupes duplicate files** and **`auto-optimise-store` hardlinks**
|
||
> them on disk. So a change that looks like it sheds gigabytes of closure can
|
||
> shed nothing off the actual image. **Measure the artifact — build the ISO and
|
||
> `stat` it.** The corollary cuts the other way and is the good news for this
|
||
> item: what dedupe cannot help is the **wire**, so a netinstall's download is
|
||
> the one figure closure/NAR size predicts honestly (`nix path-info --store
|
||
> https://cache.nixos.org --json` gives the real `downloadSize`).
|
||
- Current ISO **8.078 GiB** compressed; **18.03 GiB** of store uncompressed
|
||
(`zstd -19`, 2.23:1 — compression is already near-max, not the lever).
|
||
- The offline pin (`system.extraDependencies`, 60 roots: a representative
|
||
installed system + the template HM closure + all flake inputs) is **4.02 GiB
|
||
uncompressed of that — only ~22%**. Dropping it entirely still leaves a
|
||
**~13.3 GiB** desktop → roughly **6 GiB** compressed at the same ratio.
|
||
**So "no pin" alone is NOT the lighter ISO** — this is the trap to avoid.
|
||
- The desktop's own top weights: libreoffice 1457 MiB, initrd 1369,
|
||
linux-firmware 770, chromium 1391 (two builds — #121, left in), llvm-lib 540,
|
||
bibata-cursors 322, mesa 264, mbrola-voices 259, nerd-fonts ~420 combined.
|
||
Note what that list implies: no single lever gets a desktop ISO under ~4 GB —
|
||
which is the case for (b) below.
|
||
|
||
**So the real decision is what a netinstall ISO IS**, and it should be settled
|
||
first (`[human]`): (a) the full try-before-install desktop minus the pin
|
||
(~6.3 GiB — barely lighter, probably not worth a second target); (b) a **TUI
|
||
installer only, no desktop** (~1 GiB, the actual "netinstall" in the Debian
|
||
sense) which drops "try before install" from that medium — the fat ISO still
|
||
offers it; (c) a middle desktop (no libreoffice/chromium — but note #103 just
|
||
put those there deliberately, and a *demo* desktop that can't browse is the
|
||
bug #103 fixed).
|
||
|
||
**The gotcha that decides feasibility:** without the pin, a netinstall target
|
||
fetches from `cache.nixos.org` for stock nixpkgs paths — but **Nomarchy's own
|
||
derivations are in no binary cache**, so they would build *from source on the
|
||
user's machine* during install. That is the same failure `tools/vm/gap-analysis.py`
|
||
exists to diagnose (and #113 is a live instance of). So this item probably
|
||
depends on a public binary cache (cachix) for the flake's own outputs, or it
|
||
trades an 8 GiB download for a 40-minute install. Establish that before
|
||
building the target.
|
||
|
||
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.<pkg>` 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
|
||
0.4.19-2) and disliking the only path that works today. There is **no seam**:
|
||
`mkFlake` takes `src`/`username`/`hardwareProfile`/`system` only, and
|
||
`homeConfigurations` is built from mkFlake's own `pkgs` (`inherit pkgs`), so a
|
||
`nixpkgs.overlays` in system.nix cannot reach a home package. The user is left
|
||
hand-pinning a `fetchTarball` rev+sha256 in home.nix (verified working) — a
|
||
second pin with no lock, which is exactly the Nix expertise the distro exists
|
||
to spare them. Sketch: `home.packages = [ unstable.lmstudio ];`, and maybe
|
||
`stable.` as an explicit synonym for today's bare `pkgs`.
|
||
|
||
The shape that keeps the promises: Nomarchy carries the `nixpkgs-unstable`
|
||
input **itself** and exposes it through `overlays.default` as an `unstable`
|
||
attrset, so downstream stays **one input**, stays locked, and `nomarchy-pull`
|
||
still moves it. Cost/questions to settle before building:
|
||
- Lock churn: every lock bump now moves two channels, and the full-checklist
|
||
rule (VERIFICATION §5) applies to both.
|
||
- Duplication: measured — pinned vs unstable lmstudio build closures share
|
||
only 690 of ~3.7–4k paths (separate stdenv bootstrap). Opt-in per package,
|
||
but a user who reaches for it pays a second glibc/stack in the closure.
|
||
- "Tested together upstream" weakens by construction: nothing validates
|
||
pinned-Nomarchy + unstable-app. Needs an explicit, documented "you own this
|
||
combination" line, not silence.
|
||
- Does `unstable` belong in the eval by default, or only when referenced?
|
||
(An unused input still locks + fetches.)
|
||
- Scope: home packages only, or system.nix too?
|
||
|
||
Alternative if that is too much surface: give `mkFlake` an `overlays ? []`
|
||
param (one-line seam, user still owns their own input/pin — cheaper for us,
|
||
still expert-only for them). Related: #133.
|
||
|
||
### 114. Greeter ignores per-device keyboard layouts
|
||
|
||
Found by Bernardo 2026-07-14: logging out while docked lands on tuigreet,
|
||
where his external keyboard (remembered as `us` via
|
||
`settings.keyboard.devices`) types the session layout `gb` — so the password
|
||
prompt fights him. Not a regression and not docking-related: per-device
|
||
layouts are applied with `hyprctl keyword device[<name>]:kb_layout`, which
|
||
only exists inside a running Hyprland session, while tuigreet draws on a
|
||
kernel VT whose single keymap comes from `console.useXkbConfig` ←
|
||
`services.xserver.xkb.layout`. A VT structurally cannot do per-device
|
||
layouts, so this is a design gap, not a bug to patch. Options, cheapest
|
||
first: (a) document it and stop there; (b) a greeter layout-cycle key
|
||
(tuigreet has no such feature — would need the keymap swapped under it);
|
||
(c) host tuigreet inside a small Wayland compositor (cage/labwc), which
|
||
*does* get per-device XKB and would let the greeter honour the same
|
||
in-flake state the session uses — real work, and it changes the greeter's
|
||
whole rendering path (`modules/nixos/greeter.nix` themes tuigreet through
|
||
the 16 ANSI console slots, so (c) is not a drop-in). Worth deciding whether
|
||
the greeter is meant to be layout-aware at all before costing it.
|
||
|
||
- **NVIDIA first-class options** — **deferred past v1** (Bernardo
|
||
2026-07-10). Keep #59 commented install guidance; no
|
||
`nomarchy.hardware.nvidia.*` until a hybrid maintainer + queue.
|
||
|
||
- **Post-install hardware hints** (`VISION § B`) — After the general
|
||
“you're set” card (#81), optionally fire **one** additional
|
||
self-gated notify when the machine actually has the hardware:
|
||
(a) `fwupdmgr` on PATH → “System › Firmware to check LVFS updates”;
|
||
(b) `fprintd-list` on PATH → “System › Fingerprint to enroll”.
|
||
One-shot markers in `settings.*` (same in-checkout discipline as
|
||
`firstBootShown`); never a permanent MOTD nag. Cost: small — extend
|
||
`nomarchy-first-boot` or a sibling oneshot + `checks.first-boot`
|
||
fixture. Control-center / MOTD already mention these; the gap is the
|
||
silent first *graphical* session for people who never open those.
|
||
|
||
_(#80–#83 + #85–#88 shipped 2026-07-11. Theme A day-2 + neon-glass finish
|
||
shipped — VISION ✓. Dock/hibernate V3 → HARDWARE-QUEUE. Parallel
|
||
fingerprint-or-password shipped 2026-07-12 (Bernardo promoted it live;
|
||
`fingerprint.parallel`, pam-fprint-grosshack) — reader V3 →
|
||
HARDWARE-QUEUE.)_
|
||
|
||
### v1.0 pointer
|
||
|
||
See **VISION**. Open PROPOSED: post-install hardware hints; NVIDIA
|
||
deferred past v1; IR portal (b)/(c) need T14s (HARDWARE-QUEUE § T14s).
|
||
Standing calls: browser = Chromium; power = PPD.
|
||
|
||
## Decisions `[human]`
|
||
|
||
Open calls only Bernardo can make; agents add options/evidence but never
|
||
decide. **Resolved** entries stay for history; agents treat them as closed.
|
||
|
||
### Resolved (2026-07-10)
|
||
|
||
- **Docs site vs Markdown-in-repo** — **markdown in-repo for now**
|
||
(`docs/`, README). A rendered docs site is FUTURE if wanted.
|
||
- **Default browser** — **ship Chromium** in
|
||
`templates/downstream/home.nix`; mime → `chromium-browser.desktop`.
|
||
Opt out: delete the line / override mime.
|
||
- **Default power backend** — **keep PPD** (`nomarchy.system.power.backend`
|
||
default). TLP remains the one-line opt-in. Rationale: stability + live
|
||
profile API for menu/Waybar; Omarchy’s TLP experiment reverted.
|
||
|
||
### Resolved (2026-07-10, more)
|
||
|
||
- **Formatter adoption** — **yes, but not now.** Tracked as FUTURE
|
||
(below). Nix-source style only (`nixfmt-rfc-style` or similar); one
|
||
bulk reformat + CI/check when promoted. Until then: hand-aligned
|
||
style per CONVENTIONS.
|
||
|
||
- **Hibernation** — **want by default** (product intent). Needs a
|
||
disk-backed swap (file or partition) sized for resume; not zram alone.
|
||
**Shipped as #76**; V3 power-cycle PASSED on TuringMachine 2026-07-12
|
||
(ROADMAP § Hibernation + zram by default).
|
||
|
||
### Resolved (2026-07-10, #76 design)
|
||
|
||
- **Swap sizing** — **exactly RAM** (installer default, unchanged). Hibernate
|
||
image ≤ RAM; zram takes day-to-day paging. **`swapSize=0`** stays no-swap.
|
||
- **Migration** — **docs runbook** (`docs/MIGRATION.md`), not a tool.
|
||
- **No-swap Hibernate** — keep the menu row; **notify on failure**.
|