Commit Graph

278 Commits

Author SHA1 Message Date
be2d0d200b feat(menu): group keybinds cheatsheet Window/Workspace/Menu/Media (#108)
All checks were successful
Check / eval (push) Successful in 3m39s
Every bind in keybinds.nix carries a group; SUPER+? renders section
headers. checks.keybinds-cheatsheet asserts all groups are valid and
every description appears in the sheet (same source as Hyprland binds).

Verified: V0 flake check; built cheatsheet shows ── Window ── / etc.
2026-07-15 10:04:33 +01:00
e6be5a5770 fix: disk picker fd0 (#112), menu Back guard (#106), offline theme (#113)
Some checks failed
Check / eval (push) Has been cancelled
- #112: list_installable_disks drops fd/loop/sr/zram/<8GiB, largest-first
  so OVMF no longer offers /dev/fd0 as the default wipe target
- #106: tools/check-menu-back.py + checks.menu-back (proved to fail)
- #113: offline theme-switch contract documented (default/pinned only);
  theme-sync run_switch adds offline-oriented hint when no network

Verified: V0 flake check; installer-safety; menu-back; py_compile.
2026-07-15 10:02:19 +01:00
748d4af414 fix(install): bake dconf, seed main, name ISO, display modes (#123–#126)
Some checks failed
Check / eval (push) Has been cancelled
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
25e498a5ef docs(agent): file #125 ISO still named nixos, #126 display res invert
Some checks failed
Check / eval (push) Has been cancelled
Bernardo: live/install ISO artifact is nixos-live-….iso (image.baseName);
live VM Display picker appeared to invert higher/lower modes — investigate
sort/apply vs QEMU scaling before fixing.
2026-07-15 09:53:14 +01:00
545e40f4d9 feat(terminal): Kitty only — drop Ghostty (#95)
Some checks failed
Check / eval (push) Has been cancelled
Ghostty's OpenGL 4.3 floor broke the Acer (HD 4000 / 4.2). Dual-terminal
support (default Ghostty + install-time Kitty fallback) was more complexity
than it was worth.

- Remove ghostty.nix / nomarchy.ghostty.enable
- Kitty always installed and themed from theme-state
- Default nomarchy.terminal = kitty; SUPER+Return, doctor, calendar,
  what-changed use kitty --class=com.nomarchy.*
- Drop install-time glxinfo probe and mesa-demos on the installer
- Docs/REQUIREMENTS no longer list OpenGL 4.3 as a terminal floor

Verified: V0 flake check; option-docs; installer-safety; template HM has
kitty, no ghostty; menu/calendar emit classed kitty launches.
2026-07-15 09:51:27 +01:00
1e648b6c49 feat(terminal): themed Kitty when OpenGL < 4.3 (#95)
All checks were successful
Check / eval (push) Successful in 3m41s
Ghostty needs OpenGL 4.3; older GPUs (Intel HD 4000) top out at 4.2.
Installer probes glxinfo and writes settings.terminal=kitty into
theme-state when below the floor. modules/home/kitty.nix enables Kitty
with the same palette, mono font, and opacity as Ghostty so the fallback
still looks like Nomarchy.

Default machines stay Ghostty-only (no kitty in the HM closure). Doctor
and calendar still invoke Ghostty by class — V3 on Acer for SUPER+Return.

Verified: V0 flake check; installer-safety; HM with terminal=kitty builds
themed kitty.conf + TERMINAL=kitty; default template has no kitty bin.
2026-07-15 09:45:32 +01:00
a09399702d docs: REQUIREMENTS.md — OpenGL 4.3 floor, disk/RAM planning
All checks were successful
Check / eval (push) Successful in 3m44s
Acer M5-481T fails Ghostty because Ivy Bridge HD 4000 tops out at
OpenGL 4.2 in Mesa; Ghostty needs 4.3 — a generation ceiling, not a
missing driver. Document that plus UEFI/x86_64, RAM, and storage floors
(Nix store generations + BTRFS snapper + hibernate swap≈RAM). Linked
from README, docs map, HARDWARE.md; #95 points here.
2026-07-15 09:40:17 +01:00
2c9df329dc docs(agent): #95 both Ghostty fails are Acer pre/post HM, XPS is fine
Some checks failed
Check / eval (push) Has been cancelled
Bernardo: XPS starts Ghostty. Aspire only fails — first attempt before
home built (OpenGLOutdated), second after home built (quiet GDK_DISABLE
exit). Kitty was the manual working shell used to run both.
2026-07-15 09:36:54 +01:00
70f2d4d54d docs(agent): #95 is two Ghostty failures, not one OpenGL floor
Some checks failed
Check / eval (push) Has been cancelled
Photo A (Acer): OpenGL 4.2 → OpenGLOutdated / SurfaceError.
Photo B (XPS): silent exit after GDK_DISABLE=gles-api,vulkan with no
OpenGL line — different machine, different root cause. A fix for only
one is not done.
2026-07-15 09:34:17 +01:00
fbc93159a6 docs(agent): queue #123–#124 + diagnose #95 from Acer/XPS install photos
Some checks failed
Check / eval (push) Has been cancelled
Real installs on Acer Aspire M5-481T and Dell XPS 9350 (2026-07-14):

- #123: HM pre-activate dies at dconf without XDG_RUNTIME_DIR on both
  machines — partial first boot (weird GTK + missing nm-applet are
  symptoms of the incomplete bake).
- #124: install seeds ?ref=v1 while writing main-era nomarchy.hardware.*,
  and v1 lags hard enough that rebuilds die with "option does not exist".
- #95: Ghostty on the Acer is OpenGL 4.2 < required 4.3 (OpenGLOutdated);
  XPS starts Ghostty fine.

HARDWARE-QUEUE V3 rows; photos stay local (.gitignore WhatsApp Image*).
2026-07-15 09:33:37 +01:00
355d8cb1a4 fix(mime): text/plain falls through to Text Editor on live (#119)
All checks were successful
Check / eval (push) Successful in 4m5s
A singleton default naming code.desktop left the live ISO with no text
handler — GIO skips missing .desktop files and offers nothing. Prefer
vscode when present, then org.gnome.TextEditor.desktop. Live also gains
mpv (video defaults had the same hole; free via the template pin).

checks.live-baseline-apps now requires every Default Applications key
to resolve to a present .desktop on live (HM + system path) and on the
default template install. Proved to fail on a vscode-only text/plain.

Verified: V2 (live-baseline-apps green + prove-to-fail); V0 flake check.
2026-07-15 09:24:48 +01:00
6fbad46bca docs(agent): #122 — sextants DO work; chase the fallback, not one font's charset
All checks were successful
Check / eval (push) Successful in 3m27s
Bernardo asked whether #122 was quick, and then caught the reason my first
answer was wrong: the mono face is per THEME (ghostty.nix:22 reads
t.fonts.mono from the theme JSON), and Boreal — the default theme and the base
state — is GeistMono Nerd Font, not stylix's JetBrains Mono. I had probed the
wrong font, then drawn a confident conclusion from a single charset.

Both conclusions inverted on measurement, so the item now says the opposite of
what it said an hour ago:

  * GeistMono covers no sextants, no braille — and no block elements or box
    drawing either. So the CURRENT logo already renders through fontconfig
    fallback on the default theme, and always has. Fallback is the normal case
    here, not the hazard I painted it as.
  * Fallback supplies sextants fine: fc-list :charset=1fb00 finds 37 families
    on a real Nomarchy box, including the shipped nerd-fonts.caskaydia-cove and
    noto-fonts' Symbols 2; fc-match under GeistMono picks CaskaydiaCove. No
    tofu. Braille: 59 families.

So --symbols sextant is available and is roughly a one-flag edit for 6x the
detail — rendered both variants to confirm the shape genuinely sharpens. The
item is re-scoped accordingly: the edit is minutes, the WORK is verification.
Two things can still make it worse and only a real terminal can say — fallback
metrics (CaskaydiaCove's sextants drawn in GeistMono's cell may misalign or
differ in stroke weight, exactly the seam that looks fine in a VM and wrong on
a laptop), and the mark being diagonal-heavy, since diagonals staircase under
any cell symbol: more resolution buys a finer staircase, not a straight line.
If the mark itself wants straight strokes that is a logo.svg design change
hitting all three surfaces, not a chafa flag — settle that first.

V0: nix flake check --no-build passes. Backlog only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:47:44 +01:00
347f6544aa docs(agent): file #122 — a crisper fastfetch logo, with the trap named up front
All checks were successful
Check / eval (push) Successful in 3m27s
Bernardo 2026-07-14: the fastfetch logo should be crisper, with straight lines.

Filed with the cause rather than the wish. modules/home/fastfetch.nix converts
the vector mark with `chafa --symbols block --size 20x10`, and that symbol class
is the chunkiness: restricted to full/half blocks, a 20x10 grid carries ~20x20
pixels of detail, so every diagonal staircases. chafa 1.18.2 ships sextant (2x3
subcells), quad and wedge — verified present today, so the cheap lever exists.

Named the trap that decides the approach, because trying it first is what
prevents a wasted round: sextants are Unicode 13 Symbols for Legacy Computing,
and if the shipped terminal font lacks them the logo renders as tofu — strictly
worse than chunky. So font coverage in the real Ghostty gets checked before a
symbol class is chosen, and if it is missing, hand-authored box-drawing art is
the honest alternative (and the only way to get genuinely straight lines; any
image→symbol converter approximates).

Also recorded that the three logo surfaces are different renderers — fastfetch,
the ISO splash, the Waybar glyph font — so this item touches one of them, while
a change to the shared logo.svg would hit all three and should be split.

Visual change, so THEME-DESIGN and the honesty rule apply: pass needs
screenshots viewed under two themes, no tofu, accent tracking intact, and the
fastfetch row grid still aligned.

V0: nix flake check --no-build passes. Backlog only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:40:54 +01:00
19ba4e2b12 fix(nixos): smartd self-gates on the hardware — the doctor was right, smartd wasn't
Some checks failed
Check / eval (push) Has been cancelled
BACKLOG #118. Bernardo booted the live ISO and the Waybar health icon was red,
reporting smartd. Everything downstream turned out to be working correctly,
which is the part worth recording: smartd's config is DEVICESCAN, and where no
drive answers SMART it exits 17 ("Unable to monitor any SMART enabled
devices"), systemd marks the unit failed, nomarchy-doctor faithfully reports a
failed system unit, and Waybar paints @bad. The doctor was telling the truth.
smartd was the bug.

Scope was never live-only, which is why this sat in NOW rather than as a live
nit: services.smartd.enable mkDefaults true on every machine and QEMU virtio
exposes no SMART, so every VM install has been booting to a health warning
about a daemon with nothing to do — and every V2 run had been showing it as
noise.

Fixed with the distro's own self-gate convention: an ExecCondition running
`smartctl --scan`, which prints nothing exactly when smartd would find nothing.
A failed condition leaves the unit inactive rather than failed. Deliberately
NOT SuccessExitStatus = 17: that would also swallow exit 17 from a machine that
does have drives, which is the entire reason the daemon ships.

V2. checks.smartd-gate boots the REAL distro module rather than a restatement
of it (its nixpkgs.config needs mkForce to yield to the test's pkgs) and
asserts both halves, because they pull in opposite directions: a gate that
never skips leaves the red icon, and a gate that always skips silently disables
drive-health monitoring on real hardware — the failure nobody notices until a
disk dies quietly. So the no-SMART node must go ActiveState=inactive, unfailed,
and absent from `systemctl --failed` (what the doctor actually reads); and the
gate's logic is driven against a scan that DOES find a device, since QEMU
cannot answer SMART honestly and pretending otherwise would test nothing. The
check was proved to fail by unwiring the condition. flake check, doctor,
hardware-toggles, live-baseline-apps, option-docs and state-bridges pass.

V3 pending: that smartd still RUNS where drives have SMART (dev box, real
NVMe). Queued with an explicit fail-condition — if it skips there, revert the
gate rather than tune it.

Also swept: #120's size table said the duplicate chromium was gone; #121 was
reverted, so it is back and the table says so.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:38:58 +01:00
0727ba6d44 revert(pkgs): leave the duplicate chromium — honestly, and with the numbers
All checks were successful
Check / eval (push) Successful in 3m21s
Bernardo on #121 (shipped in 0b464af): "Let's leave it honestly. Sounds too
much work for a negligible gain." Agreed, and his own reasoning is the
argument: the fix worked, but it substitutes into a buildCommand nixpkgs owns,
which couples us to wrapper internals and has a SILENT no-op failure mode —
so it also needed a permanent check, because nothing about a returning
duplicate breaks a build. That standing maintenance cost buys ~195 MiB of
download and ~19 MiB of disk. Boring beats clever at that exchange rate.

Removed the overlay and checks.chromium-single-closure. The template HM closure
is back to 9.38 GiB with the duplicate present — verified by closure, not by
reading the diff. overlays.default now carries a comment saying the duplicate
is known and deliberate, where the next person will look for it.

The ROADMAP entry stays, downgraded from ✓ to ✗ decided-against: it holds the
measurement, the approach that worked (self-contained -wv copy; repoint the
wrapper's share/* symlinks; .override composes with overrideAttrs), and the
commit to recover it from. The bug is real and worth an upstream patch — the
wrapper should take its desktop entry from the variant it wraps, which helps
every NixOS user with enableWideVine, not only us. Revisit locally if #120's
netinstall ships, since download is the one axis this moves.

What survives is worth more than the fix, and is stamped on #120: closure size
is not disk size and is not image size — measure the artifact. Removing a
687 MiB path moved the ISO by 8 KiB, because mksquashfs dedupes duplicate files
and auto-optimise-store hardlinks them. Without that caveat, #120's "drop the
pin, save 4 GiB" would have evaporated at the squashfs exactly as this did.

V0: nix flake check --no-build passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:32:06 +01:00
0b464af3e5 fix(pkgs): one chromium, not two — and what that is actually worth
All checks were successful
Check / eval (push) Successful in 3m32s
BACKLOG #121. nixpkgs' chromium wrapper with enableWideVine = true (what the
template ships, for DRM) runs the `-wv` copy but links its share/* from the
PLAIN unwrapped build. A symlink is a store reference, so a second 687 MiB
chromium rode along for a directory of .desktop files nothing executes.
overlays.default now points those symlinks at the copy we already run; the
template needs no change, since lib.nix, the repo pkgs and nixosModules all
apply the overlay.

Checked first, because it decided whether the item was possible at all:
whether the `-wv` copy references the plain build. It is `cp -a` of it, so it
plausibly would have — and then no wrapper change could have dropped the
duplicate. It does not. Also verified rather than assumed: `.override
{ enableWideVine = true; }` composes with the overlay's overrideAttrs (the
order usually matters), so the template's existing line picks the fix up
untouched; the wrapper still execs the -wv build with WidevineCdm present; and
share/{applications,icons,man} match stock with the .desktop byte-identical.

I claimed this was "687 MiB off every install". It is not, and measuring the
artifact instead of the closure is what corrected it:

    nominal closure          -687 MiB  (9.38 -> 8.71 GiB)
    ISO image                -8 KiB    of 8.078 GiB
    installed disk           ~-19 MiB
    cache-install download   -195 MiB

Two dedupes that closure arithmetic cannot see. mksquashfs detects duplicate
files, so the ISO had already stored the near-identical blocks once. And
auto-optimise-store (on by default here) hardlinks identical files: the two
paths share inodes — verified by stat, and du counts 639 MiB for the pair
against ~620 for one. The only thing that cannot dedupe is the wire, where the
extra path is its own 195 MiB NAR.

So this is a DOWNLOAD fix — precisely what #120's netinstall cares about — and
close to a no-op for the offline ISO that is the default today. Whether that
justifies coupling an overlay to nixpkgs wrapper internals is Bernardo's call;
it is guarded and no-ops if upstream moves, but it is his to weigh. Worth an
upstream patch regardless: the wrapper should take its desktop entry from the
variant it wraps.

The guard is the load-bearing part, because the failure mode is a SILENT no-op
that no build complains about: checks.chromium-single-closure asserts exactly
one full unwrapped chromium in the template closure, and was proved to fail by
neutering the overlay — it reports "found 2", names both paths, and warns off
the tempting "fix" of dropping enableWideVine, which silently removes DRM.

The rule this establishes, now recorded in ROADMAP and on #120 (whose entire
size table is closure arithmetic): closure size is not disk size and is not
image size. Measure the artifact.

V2: flake check, chromium-single-closure, live-baseline-apps, template-sot,
option-docs, state-bridges all pass. Three ISOs built from one tree for the
numbers above.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:26:24 +01:00
00b1e10fc3 feat(live): the live ISO gets a desktop — browser, office, editor — for +41 MiB
All checks were successful
Check / eval (push) Successful in 3m22s
BACKLOG #103. Bernardo, live ISO 2026-07-14: still no browser, no office —
"that makes a live iso useful". He is right, and it is the thing a user judges
the distro by before installing, and what they boot to rescue a machine that
won't start. It could do neither.

The live HM user now names its own set: chromium, libreoffice-fresh,
gnome-text-editor, amberol, snapshot. Firefox deliberately excluded (Bernardo's
call): chromium already owns the HTTP mime default and ships in the installed
template, so it is the browser that matches what a user gets post-install.

I asked him to choose on a "~+2 GB" premise. The premise was wrong, and
measuring rather than assuming is what corrected it: system.extraDependencies
ALREADY pins the template's HM closure into the ISO for offline installs, so
chromium/libreoffice/amberol were already in the image's store — merely absent
from the live user's profile, so nothing put them on PATH or in the launcher.
The live and template chromium resolve to the SAME store path, verified, so
reusing the template's exact `chromium.override { enableWideVine = true; }`
costs zero; a plain `chromium` would have been a second 2.5 GiB closure. Only
gnome-text-editor and snapshot are new: 9 paths, 133 MiB uncompressed. His
Firefox call still held for the right reason — it was the one item genuinely
unpinned.

V2. Measured ISO delta, both built from the same tree — the item's pass
condition: 8.038 → 8.078 GiB = +41.2 MiB (+0.50%). New
checks.live-baseline-apps asserts each app is on PATH *and* has a .desktop the
launcher can see (a binary without one is invisible, which is the failure that
matters), that HTTPS resolves to a chromium entry actually PRESENT (#94's exact
trap — a mime default naming a package nothing ships), and that firefox has not
crept back, since that is a size decision not a drive-by. The guard was proved
to fail: dropping snapshot makes it name the missing entry. flake check,
live-install-entry, option-docs, template-sot green.

V3 pending: that the apps LAUNCH needs hardware — queued on the Acer M5-481T,
from the launcher only, since that is the claim a file-level check cannot make.

Also filed, from measuring the 8 GiB rather than speculating about it:
  * #120 (NEXT, Bernardo's call) — a netinstall ISO beside the offline one.
    Starts from numbers: the offline pin is only 4.02 of 18.03 GiB uncompressed
    (~22%), so dropping it still leaves a 14 GiB desktop (~6.3 GiB compressed).
    "No pin" is not the lighter ISO; and without a binary cache for Nomarchy's
    own outputs a netinstall trades a download for a from-source install.
  * #121 — the Widevine wrapper drags a SECOND 687 MiB unwrapped chromium in
    for its share/applications alone. Pre-existing, on every installed machine.
  * #119 — text/plain names vscode, which the live ISO never ships.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:01:20 +01:00
b1c4e516c9 docs(agent): #103 confirmed on hardware — record the why, the inventory, and the size trade
All checks were successful
Check / eval (push) Successful in 3m17s
Bernardo, live ISO 2026-07-14: still no browser, no office/utility apps —
"that makes a live iso useful". Not a new item; #103 already covers it and #94
already pointed here. Recorded against #103 instead of duplicating.

Captured the rationale, which outlives the app list: a live ISO is what
someone judges the distro by before installing, and what they boot to rescue a
machine that won't start — read a wiki page, save a document off a dying disk,
check webmail. The live session can do none of that today.

Verified the inventory rather than restating the report: eval of nomarchy-live's
HM packages + systemPackages gives chromium=no firefox=no libreoffice=no
amberol=no snapshot=no text-editor=no. It does carry terminal tooling (yazi,
btop, bat, fd, ripgrep, ghostty) plus zathura and imv, so the gap is precisely
the GUI apps a non-Nix user reaches for.

Flagged the part that makes this more than a package list: the ISO-size delta
is the real trade — both browsers plus libreoffice-fresh is plausibly +2 GB on
an image people download, and squashfs can't be trimmed afterwards. Listed the
options (both browsers / Chromium only / lighter office for the live medium)
and asked for measured deltas per option, noting that nomarchy-live and the
installed profile need not carry the same set — the live medium is the one
with a size ceiling.

V0: nix flake check --no-build passes. Backlog only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 16:30:38 +01:00
334354a103 docs(agent): file #118 — smartd fails with no SMART device, reddening the health icon
Some checks failed
Check / eval (push) Has been cancelled
Bernardo tested the live ISO and saw a smartd error on the Waybar doctor
module. Filed with the cause rather than the symptom: reproduced headlessly in
a scratch runNixOSTest whose node's only config is services.smartd.enable =
true — the same block default.nix:166 mkDefaults on for every machine.

  smartd: In the system's table of devices NO devices found to scan
  smartd: Unable to monitor any SMART enabled devices. Exiting.
  smartd.service: Main process exited, code=exited, status=17/n/a

Everything downstream is working correctly: systemctl --failed lists
smartd.service, so the doctor's generic failed-unit check reports it and
#custom-doctor goes @bad. The doctor is telling the truth — smartd is what
needs fixing.

Scope is wider than the live ISO, which is why it sits in NOW with the other
live findings rather than as a live-only nit: smartd mkDefaults true on
`nomarchy` too, and QEMU virtio disks expose no SMART, so a plain VM install
boots to a red health icon out of the box. Every V2 QEMU run has been showing
this.

The fix direction is the distro's own self-gate convention: `smartctl --scan`
prints nothing on such a machine, so an ExecCondition leaves the unit inactive
instead of failed and real SMART failures still surface. Recorded explicitly
that SuccessExitStatus = 17 is the wrong fix — it would also swallow smartd
dying on a machine that does have drives, which is the reason the daemon
exists.

V0: nix flake check --no-build passes. Backlog only — no fix attempted here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 16:27:59 +01:00
0bb75b05a9 docs(agent): #117 is an input to #110, not work — Control Center is being retired
All checks were successful
Check / eval (push) Successful in 3m12s
Bernardo's triage: the Control Center is going away (#110), so making its
toggles rebuild would be building on a condemned surface. #117 stays filed
because the observation outlives the tool — whatever menu inherits those rows
must not reprint "requires rebuild" — but it is marked not-standalone so no
agent picks it up as work. The previous entry's "next suggestion: #117" was
wrong and the correction is appended rather than rewritten.

#110's migration matrix gets what #116 turned up, which changes its shape:
its Bluetooth and Printing rows write settings.{bluetooth,printing}.enable,
and those keys only became live bridges in a9f3a64 — so those two toggles
never worked for any user. There is therefore no behavior to preserve, but
the keys and their bridges are real now and outlive the TUI: dropping the
rows must not drop the keys, or a machine whose theme-state.json already says
bluetooth.enable = false silently flips back on at its next rebuild.
checks.state-bridges covers exactly that and fails loudly if the bridges go
with the tool. Updates (settings.updates.enable) is read in
modules/home/options.nix — the HM side, where nomarchy.settings genuinely
exists — so it needs none of this; verified, not assumed.

V0: nix flake check --no-build passes. Backlog/journal only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 16:21:41 +01:00
a9f3a642ee fix(nixos): two toggles that reported success and did nothing — wire the state bridges
All checks were successful
Check / eval (push) Successful in 3m16s
BACKLOG #116. `nomarchy.settings` is declared in exactly one place —
modules/home/options.nix:412, the Home Manager side. On NixOS the attribute
does not exist, and `or <fallback>` swallows the missing-attribute error, so
four options that "defaulted from the state" had silently been their fallback
on every machine ever built.

The item said three options, and called them benign. Both halves were wrong,
and re-grepping rather than trusting the account is what found it:

  * There were four. The original enumeration read options.nix instead of
    modules/nixos/ and missed services.nix's printing.enable — the same
    mistake in miniature as the bug it was filing.
  * Two were live user-facing bugs. Control Center is shipped
    (default.nix:337) and reachable from the menu; its Bluetooth and Printing
    toggles wrote settings.{bluetooth,printing}.enable and printed "requires
    rebuild", and the rebuild changed nothing. They had never worked.

The fix is one shape, now uniform: the option declares a STATIC default, and
the implementing module reads the state via theme-state-read.nix (fails closed
on bad JSON, unlike greeter.nix's raw fromJSON — also moved onto the reader
here) and mkDefaults it behind `mkIf (state != null)`. An absent key leaves the
option default as the single source of the fallback; a hand-set system.nix
value still pins it. batteryChargeLimit gets no bridge and loses its dead read:
power.nix's oneshot already reads that key with jq at RUNTIME and prefers it
over the baked value, which is why that menu worked all along.

V2. The bug is proved real before/after on the same flipped state: BEFORE,
bluetooth stays true and printing stays false; AFTER, both flip, and a hand-set
value still outranks the state. Nothing in a build fails when a bridge dies, so
the guards are the point — checks.state-bridges asserts 11 eval cases, and
checks.printing-from-state boots a VM whose only input is the state file and
waits for a running cups.service. The guard was itself proved to fail:
re-breaking the bluetooth bridge makes it throw, naming both assertions. A
check that passes whether or not the property holds is worse than no check
(625b7e3). flake check, option-docs, template-sot, downstream-template-*,
installer-safety, hardware-toggles and battery-charge-limit all pass.

No V3: the mechanism is fully proved headlessly. Design record in ROADMAP §
NixOS-side state bridges (#116); new #117 (PROPOSED) for the control-center
toggles still leaving the rebuild to the user.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 16:03:12 +01:00
7353568115 docs(agent): #116 undercounted the phantom reads, and two of them are live bugs
All checks were successful
Check / eval (push) Successful in 3m2s
Re-grepped the tree instead of trusting eb38008's own account of it. Two
corrections, both in the direction of worse:

A fourth read: services.nix:80 `printing.enable`. The original enumeration
only looked at options.nix, which is the same mistake in miniature as the
bug it was filing — `or` hides a dead bridge, so the only honest check is a
grep of all of modules/nixos/, which the item's own pass condition already
said and its own list did not do.

"Benign today" was wrong about Bluetooth. The TUI control center is shipped
(default.nix:337) and reachable from the menu's Control Center row, and its
Bluetooth and Printing toggles write settings.bluetooth.enable /
settings.printing.enable and print "disabled (requires rebuild)". Nothing
reads either key, so the fallback wins and the rebuild changes nothing. Two
user-facing toggles that report success and do nothing — that is a live bug,
not a dead key. batteryChargeLimit stays benign for the stated reason: its
menu patches the baked option instead.

nomarchy.settings is declared in exactly one place, modules/home/options.nix:
412 — verified, not assumed. V0: nix flake check --no-build passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 15:45:00 +01:00
50bfdb99d0 docs(agent): reconstruct the five journal entries a crashed session owed
All checks were successful
Check / eval (push) Successful in 3m3s
A session crashed after shipping the fingerprint/idle/menu batch. The work
itself survived intact: 625b7e3, e2de906, fbbeac6, 9792976 and eb38008 are
committed and pushed, and each did its own sync sweep — BACKLOG #115/#116
filed, HARDWARE-QUEUE carrying the toggle test and the removed fprintd trap,
docs and template updated. Only the journal was owed, so the next session
would have read docking round 7 as the head and missed five iterations of
context, including two live bugs the batch exposed.

Reconstructed from the commit messages rather than from memory, and marked
as such in the file — the commits are the record, this is an index into
them. The entries keep what the messages proved and what they left pending,
notably the lesson from fbbeac6 (the lock field has never been visible; I
assumed it was and reasoned from there) and the one from 625b7e3 (a check
that passes whether or not the property holds is worse than no check).

V0: nix flake check --no-build passes. Docs-only.

Noted, not done: the journal is at 56 entries against its own ~25 rotation
threshold — it was already 51 before this, so the tail is overdue for
JOURNAL-ARCHIVE.md. That is its own commit, not this one's business.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 15:40:24 +01:00
eb38008ebb feat(menu): one fingerprint switch, an auto-login toggle, and the state bridge they needed
All checks were successful
Check / eval (push) Successful in 3m7s
Bernardo, post-reboot: "Use for login" was the wrong question. Whether the
finger works is one decision, not two, and whether login prompts at all is
a different decision that was never in the menu.

System › Fingerprint is now a single Fingerprint (on/off) switch, leading
the menu with enroll/list/verify/delete as the plumbing behind it. It
writes the one settings.fingerprint.pam key, and modules/home/idle.nix now
defaults idle.fingerprint from that same key — so the lock screen and
login/sudo move together instead of drifting apart the way they did until
e2de906. nomarchy-fingerprint does the two rebuilds this needs (sudo
system for PAM, home switch for hyprlock) and refuses to turn on with no
finger enrolled.

System › Auto-login is new (nomarchy-autologin), and it is what decides
whether anything is asked at boot: auto-login on means no prompt whatever
the fingerprint switch says; off means the greeter asks, for a password or
a finger. Installer-seeded ON for LUKS machines — the passphrase already
gates the disk — and off without it, where the greeter is the only thing
between power-on and the desktop.

Both had to become state-owned to be toggleable at all, which surfaced two
real bugs:

  * nomarchy.system.greeter.autoLogin defaulted from
    `config.nomarchy.settings…` — an attribute that exists ONLY on the Home
    Manager side. On NixOS it is absent and `or null` swallowed the error,
    so the default silently evaluated to null on every machine ever built.
    That is why the installer baked a Nix line: the state path never
    worked. Now read via theme-state-read.nix (the hardware.nix/timezone.nix
    pattern) and mkDefault'd, so the menu owns it and a hand-set line still
    pins it. Two more options read the same phantom bridge — BACKLOG #116.
  * `theme-sync get` printed Python's "None" for a JSON null, so every
    `case … null)` a caller writes would miss. Now prints "null", as the
    comment above it already promised for booleans.

The installer seeds the state instead of emitting the system.nix line,
because that line outranks the state and would strand the toggle.

V1 (V3 pending: HARDWARE-QUEUE). nix flake check --no-build, installer-
safety and option-docs all pass. Proved by eval/build, not assumed: a state
carrying autoLogin yields greetd initial_session {"user":"bernardo"}, the
template state (no autoLogin) yields none, and a hand-set null beats a state
that says otherwise; a state with only fingerprint.pam=true — nothing set by
hand — renders the hyprlock auth.fingerprint block; both new tools pass
bash -n and land in systemPackages (nomarchy-fingerprint only with a
reader); the patcher writes settings.greeter.autoLogin and no system.nix
line; and the get round trip prints null, so the menu reads "Auto-login
(off)" where it would have read "(on)".

The reader itself, the two rebuilds, and the reboot are hardware — queued.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 14:34:56 +01:00
625b7e38a3 docs(hardware): sudo path of parallel fingerprint passes; drop a check that cannot fail
All checks were successful
Check / eval (push) Successful in 3m4s
Bernardo, AMD dev box, gen 422, fingerprint.pam = true (parallel default):
one prompt, typed password works, sensor works, wrong-finger x3 falls back to
password. Three of the item's counts confirmed on the real reader.

The fourth — "password keeps working with fprintd stopped" — is removed as a
trap rather than checked off. fprintd is D-Bus activated, so `systemctl stop`
does not keep it down: PAM asks D-Bus, D-Bus restarts it. Bernardo ran it and
reported that *the fingerprint still worked*, which is precisely the evidence
that the daemon came back and the dead-reader path was never exercised. A
check that passes whether or not the property holds is worse than no check.

The property itself needs no hardware: password is `auth sufficient` at order
11700, independent of the fprintd rule at 11400, so no fprintd failure can
gate it — visible by eval on the rendered pam.d text, and asserted in the VM
with no reader by checks.hardware-toggles. Doing it for real would need
`systemctl mask`, and masking a working machine's auth daemon to re-prove an
evaluated invariant is not a trade worth making.

Still open on this item: hyprlock, the greeter (needs the reboot), and
`fingerprint.parallel = false` restoring sequential behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:48:16 +01:00
e4404e0162 test(docking): prove the keyboard-restore path; the reload clobber is real
All checks were successful
Check / eval (push) Successful in 3m10s
The last unproven half of bec826b, and it needed a keyboard that stays behind
when the dock leaves. The dev box's by-tech hangs off the BenQ's own hub, so
it always walked away with the cable — a Logitech K400 with its receiver in
the laptop is the real case, and Bernardo happened to plug one in.

Both halves measured on hardware (dev box, 2026-07-14):

  - PRIMARY: across a real unplug + the fixed transition, the K400 kept its
    remembered `us`. Panel back, layout intact.
  - CONTROL: a bare `hyprctl reload` with no restore took the same board from
    `us` to the session's `gb`.

The control matters more than the pass. That reload clobbers runtime
`device[<name>]:kb_layout` was an inference from the mechanism when
restore_keyboards was written — reasonable, but unmeasured, and this session
has already shown what unmeasured reasoning about this watcher is worth.
It holds: without that call the undock's escalation would have silently
broken the layout of every keyboard that outlives the cable, trading a black
panel for a keyboard that types the wrong characters at the password prompt.

Verified V2 again after the comment/doc edits: nix flake check --no-build,
checks.docking-ux, shellcheck clean.

Round 8 now asks only for what is still open: relogin (the watcher calls the
transition by baked store path, so exec-once means it runs the old build
until then) and repetition — 5+ unplugs, one with the lid shut.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:35:23 +01:00
bec826baf0 fix(docking): the undock keyword is inert, not raced; escalate to reload
All checks were successful
Check / eval (push) Successful in 3m10s
Round 7 FAILED: 5-6 consecutive unplugs on the dev box, the panel never came
back. Round 6's retry could not have worked, because round 6's diagnosis was
wrong — and so was the "phantom re-add" theory I brought to this session.

With ZERO enabled outputs — panel disabled by the dock, external gone —
Hyprland 0.55.4 accepts `hyprctl keyword monitor eDP-1,preferred,auto,1`,
prints `ok`, exits 0, and never flushes it: the rule waits for a DRM event
that is not coming. It is inert, not raced. Retrying it 25x/poll for 6 polls
bought 30s of black screen and nothing else.

What made this survive two rounds is worth more than the fix: every
`transition=undock result=ok` in the round-6 journal was Bernardo plugging
the cable back in because the screen was black. His hotplug flushed the
queued rule and the next poll took the credit. Success was indistinguishable
from the user working around the failure, so the logs confirmed whichever
story we brought to them. Ten minutes of probing on hardware killed both.

Probed live 2026-07-14 (dev box, cable out): keyword inert across 4s and 5s
in two runs; `dispatch forcerendererreload` inert too; only `hyprctl reload`
escapes — 99ms and 289ms. So: issue the keyword (enough whenever another
output is still enabled, e.g. the menu's Dock mode), and escalate to reload
only when `monitors` proves it inert. After a reload, re-assert the rule so a
config that parks the panel off cannot undo the undock, and restore per-device
keyboard layouts — a reload re-reads the config and drops the runtime
`device[<name>]:kb_layout` an external board depends on. The menu's `enable`
now proves itself against `monitors` too, instead of cheering for an exit code.

Verified V3 (partial): the fixed transition driven through a real unplug —
panel on, workspaces 1-3 home, 1.8s, `keyword=inert escalate=reload` ->
`enable=via-reload` -> `result=ok`. V2: nix flake check --no-build; docking-ux
+ dock-audio; monitor-fallback.nix; shellcheck clean on display-transition.

NOT proven, and queued as HARDWARE-QUEUE round 8: the watcher-driven path
(exec-once + baked store path = relogin required, the round-5 trap) and
repetition. The keyboard-restore path is untested — the dev box's keyboard
hangs off the dock's hub, so it leaves with the cable; round 8 adds a check
with a keyboard in the laptop. The VM harness still cannot reach any of this:
QEMU aborts Hyprland if the last active output is deleted while the DRM output
is disabled, which is the same zero-output degeneracy from the other side.

New BACKLOG #114 (PROPOSED): tuigreet ignores per-device keyboard layouts —
a VT has one keymap, so the greeter cannot honour `device[]:kb_layout`.
Bernardo hit it logging out while docked; not a regression, a design gap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:28:06 +01:00
28be6779a7 docs(hardware): round 6 settles 4 of 5 counts; headphones still untested
All checks were successful
Check / eval (push) Successful in 3m15s
Bernardo's round 6 report plus the keyboard result confirms on hardware:
wallpaper on the external, automatic dock mode, audio reaching the
monitor, and the external keyboard prompting exactly once (was four).

That leaves headphone jack-follow as the only count from rounds 4-6 never
tested at all, so it gets its own entry rather than staying buried in a
list of settled ones. Fold it into the round 7 session — the dock is
already in hand and the two checks don't interfere.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 11:37:52 +01:00
44ecc9094e fix(docking): retry the undock; a lost keyword left the panel black
Some checks failed
Check / eval (push) Has been cancelled
Round 6 passed on the plug side but unplugging the cable left the laptop
panel black until it was re-docked. The journal named it: across three
undocks, two logged result=enable-timeout and one result=ok — a race, not
a logic error.

Two halves, both needed:

  - `hyprctl keyword monitor eDP-1,preferred,auto,1` is accepted (exit 0)
    while Hyprland is still tearing down the departing external, then
    silently dropped. Re-issue it on every poll for 5s and keep `monitors`
    listing the panel — not hyprctl's exit code — as the only proof.

  - The watcher fired the undock once off the removal event and marked the
    departure handled regardless (`|| true` + awaiting_lid_open=1). Since
    known_outputs had already moved, the set-changed guard never reopened
    and nothing retried, so one lost keyword cost the panel until re-dock.
    The undock is now a queued invariant driven on the 1s tick, bounded at
    6 attempts so a genuinely unenableable panel can't pin the inhibitor.

The VM harness cannot reach this race: QEMU's headless backend aborts
Hyprland if the last active output is deleted while the DRM output is
disabled, so the test must undock before removing DP-1 and never collides
the two. Noted there, and V3 queued as round 7 — unplug 5+ times, since
the old failure only hit ~2 in 3.

Verified: V2 — nix flake check --no-build; monitor-fallback.nix exit 0 in
57s; shellcheck clean. The fix itself is V3 pending.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 11:35:12 +01:00
f625c0eaf4 feat(waybar): show the layout indicator only when the layout is ambiguous (#109)
All checks were successful
Check / eval (push) Successful in 3m27s
The indicator answers "which layout am I typing in?", so it is worth bar
space exactly when that question has more than one answer. It was gated on a
per-device override merely *existing*, which is the trap this box walked
into: the junk rows a keyboard's extra HID collections leave behind are all
remembered at the session layout, so they added no second answer and still
raised an indicator that could only ever show one value.

Gate on layouts rather than devices, and read the runtime-remembered
settings.keyboard.devices alongside the declared option — the new-keyboard
watcher writes those without a rebuild and they only graduate into
keyboard.devices later, so a bar reading the option alone was wrong for
exactly as long as the memory was fresh, which is when it matters most.

keyboard.layouts is now excluded on purpose: hyprland.nix keeps it as the
pool the picker offers first and deliberately never merges it into kb_layout,
so listing candidates cannot make the current layout ambiguous. A device
actually using one of them still does.

All four whole-swaps list the module unconditionally, so a single-layout
setup saw on a swapped bar what the generated bar hides — the parity rule was
already broken. Filter a swap through the same answer instead of asking four
hand-written files to re-derive it.

Logic is pure in modules/home/waybar-language.nix, matching monitor-rules /
dock-audio-rules; checks.waybar-language unit-tests both halves against
boreal's real swap, including a fixture-drift guard so the parity assertion
cannot pass vacuously.

Verified V2: new check passes, docking-ux and theme-wholeswap still pass,
`nix flake check` is clean, and the live config's generated bar now resolves
modules-right without hyprland/language on its single-layout default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 11:10:48 +01:00
ce480f3669 fix(docking): dock automatically; repair cards parked on pro-audio
All checks were successful
Check / eval (push) Successful in 3m9s
Bernardo's retest on the AMD dev box. Two of the four reports were never
running the new code — both watchers are Hyprland `exec-once`, so a
`nomarchy-home` switch leaves the old processes alive until relogin; only
`nomarchy-dock-audio`, a user service, had picked the fix up. The journal
showed the old watcher's `event=monitoradded` next to the new audio tool's
`result=no-available-sink`, which is what gave it away. The other two were
real.

Automatic dock mode did not exist. The watcher auto-*undocked* when the last
external left, but on plug it only ever matched a display profile — and with
no profile saved and displayProfileAuto unset it did nothing at all, while
`Dock mode` was reachable only from the menu. Half the pair being automatic
is what made the dock feel broken: the session came home by itself but never
left. Mirror it, letting a matched profile win as the more specific layout.

The BenQ's audio was unreachable by any means because WirePlumber had the
GPU's DisplayPort card pinned to `pro-audio`, whose raw `pro-output-N` sinks
carry no ports, no routing and no volume — so no sink matched, nothing was
selectable, and the profile is stored per card and so survived forever. The
monitor's own USB card is input-only (every profile has sinks: 0), leaving
DisplayPort as the only path to its speakers. Repair the card when a plug
finds nothing routable, which is both the symptom and the only moment worth
touching profiles: narrow to cards carrying an *available* HDMI/DP port —
the monitor asking for audio over the cable — so the internal analog card is
never touched and a considered pro-audio setup elsewhere survives.

Verified V2: checks.docking-ux and checks.dock-audio pass with guards for
both; the repair filter picks exactly the GPU card → HiFi against the dev
box's live graph, leaving the BenQ USB, internal analog and Bluetooth cards
alone. Hotplug itself still needs hardware — HARDWARE-QUEUE round 6, which
now leads with the relogin requirement that invalidated round 5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 10:56:52 +01:00
38ae6775e4 docs(backlog): settle #107 on state.json
All checks were successful
Check / eval (push) Successful in 3m14s
Bernardo's call — the rename target is decided, so drop the alternative
rather than leave whoever picks the item re-opening it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 10:42:42 +01:00
e0da1ea40f fix(docking): don't lose the hotplug event; one prompt per keyboard; follow the jack
All checks were successful
Check / eval (push) Successful in 3m31s
Four failures from Bernardo's round-4 dock pass on the AMD dev box, plus a
headphone regression, tracked to three causes.

The display watcher ran socat with `-T 1`, which closes Hyprland's IPC
socket after one second of *inactivity*. A dock is normally plugged into an
idle desktop, so the socket was almost always mid-reconnect at that moment
and `monitoradded` was lost outright — and the wallpaper repaint, the
docking profile, and the dock-audio reprobe all hung off that single event.
The reconnect path recovered only the lid inhibitor, so the dock stayed
half-configured until something else poked the watcher.

Make the connection long-lived and take the periodic tick from a 1s `read`
timeout instead, then let the *output set* — not the event — decide: a new
`reconcile` diffs it and is called on both ticks and monitor events, so an
event now only makes the reaction instant rather than necessary. A newly
present external output is as unambiguous a physical plug as the event was,
so it may still override a manual sink choice. Wallpaper paints are retried
over the first few seconds because awww registers an output on its own
schedule, not the compositor's.

Hyprland calls every key-capable evdev node a "keyboard": the lid and power
buttons, a monitor's control channel, and each extra HID collection of a
real keyboard. One dock with one keyboard asked for a layout four times.
udev already draws the distinction properly, so take ID_INPUT_KEYBOARD from
it, map the names into Hyprland's spelling, and ask once per physical
keyboard — applying the answer to every node it brought with it.

Finally, a sink WirePlumber has been told to prefer is stored as the
configured default and outranks every priority rule. On UCM cards the
headphones are a sink of their own rather than a route, so once anything
pinned a sink (this feature on a dock plug, or the user in the Audio menu),
plugging headphones in could no longer take the audio back. The `watch`
loop already subscribed to exactly the right events with an empty handler;
finish it. Unplug needs no undo — the pin goes stale and WirePlumber falls
back by priority on its own, to the dock when docked and the speakers
otherwise.

Verified V2: checks.docking-ux and checks.dock-audio pass with new guards
against each regression; the keyboard filter and both sink selectors run
against the dev box's real devices and sink names. The hotplug paths need
hardware — queued as docking round 5.

Also queues #107, renaming theme.json now that it holds the system state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 10:24:48 +01:00
9550d2be5e fix(live): git-init the seeded flake so doctor stops false-warning
All checks were successful
Check / eval (push) Successful in 3m26s
BACKLOG #99: the live-session seed (nomarchy-seed-flake) did a bare
`cp -r` with no `git init`, so `~/.nomarchy` was not a repo in the live
session and nomarchy-doctor's `git ls-files --error-unmatch
theme-state.json` false-alarmed "theme-state.json is NOT git-tracked"
(installed systems commit at nomarchy-install.sh:654 and were always
fine). The seed now init+add+commits, exactly like the installer.

Verified: V2 — focused serial-log VM run
(/tmp/nomarchy-v2-theme-99/): doctor prints "theme-state.json is
git-tracked" and "flake checkout is clean"; GIT_REPO_OK / TRACKED_OK
markers. A drv-equivalence test proves the seed change is
build-neutral: homeConfigurations.<user>.activationPackage is
byte-identical path-seeded vs git-seeded
(bvl30ggn...-home-manager-generation.drv). nix flake check green.

The same run exposed a separate, pre-existing failure — offline theme
switching in the live session rebuilds from source and dies offline —
filed as BACKLOG #113 (NOT caused by this change: same drv either way).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 20:14:04 +01:00
b645573149 docs(agent): park #94/#98/#99 with findings and in-flight state
All checks were successful
Check / eval (push) Successful in 3m13s
Session wrap-up: progress notes so the next iteration resumes without
re-deriving — #94 installed-chromium path proven at V1 (live posture is
the gap, #103 covers it), #98 ruled out palette-level causes, #99
root-caused (live seed lacks git init) with the fix drafted but held on
the open live offline theme-switch failure; a serial-log VM run is in
flight. V0 (notes only); two deliberate working-tree drafts documented
in the entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 18:37:33 +01:00
79528ff392 fix(menu): explain unsupported battery-limit and BlueZ-down states
Some checks failed
Check / eval (push) Has been cancelled
BACKLOG #96 + #97 (Acer live pass). Battery limit: the System row now
gates on battery presence (same rule as Power profile) instead of the
charge-threshold sysfs node, so firmware without the control gets the
leaf's explanation ("no charge_control_end_threshold — hardware
capability") rather than a silently missing row that reads as a broken
feature. Bluetooth: the leaf checks bluetooth.service before launching
blueman-manager and replaces the raw "Connection to BlueZ failed" with
a state-aware message (service stopped vs no adapter in
/sys/class/bluetooth, with rfkill/firmware hints).

Verified: V1 — nomarchy-menu builds, bash -n green, both paths in the
generated script; V0 flake check green. QEMU can fake neither a
threshold-less battery nor a BT adapter: #96 behavioral check queued in
HARDWARE-QUEUE (Acer + T14s); #97's no-adapter message will be
click-checked in the next VM run (V2 pending).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 18:36:19 +01:00
10853f662c feat(menu): direct SUPER+CTRL+I / SUPER+CTRL+T binds for System and Tools
Some checks failed
Check / eval (push) Has been cancelled
BACKLOG #107: both submenus get conflict-free direct bindings through
the canonical keybinds.nix (single source for Hyprland binds and the
SUPER+? cheatsheet) — I follows the Super+I settings muscle memory,
T is Tools. The root-menu rows now show the shortcuts via menuHint.

Verified: V2 — eval audit: 41 binds, no duplicate mods+key; both binds
present in the generated Hyprland config; built nomarchy-menu passes
bash -n with cheatsheet rows + hints rendered. In-session on the live
ISO KVM run: viewed 40-system-menu.png / 41-tools-menu.png in
/tmp/nomarchy-v2-swap-93/ — each bind opens its submenu directly.
V0 flake check green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 18:33:54 +01:00
c97ecade63 fix(install): label and explain the swap-size prompt
Some checks failed
Check / eval (push) Has been cancelled
BACKLOG #93 (Acer live pass): the swap field appeared as a bare
prefilled number — gum never renders the placeholder once --value is
set, and the block had no section header, so nothing said what the
value meant. Add a visible "Swap & hibernation" section, two info lines
(default = RAM keeps hibernation possible; 0 = no swap), a labeled
--header on the input, and make the info + Review lines state the
outcome in full ("N GiB swapfile (enables hibernation)" /
"none (hibernation disabled)"). Default behavior unchanged (swap = RAM).

Verified: V2 — interactive KVM UI drive of the built ISO; viewed
10-swap-prompt.png (section + explanations + labeled input) and the
Review box ("Swap: 6 GiB swapfile (enables hibernation)") in
/tmp/nomarchy-v2-swap-93/; the wipe confirmation was aborted, nothing
touched. bash -n + nix flake check --no-build green.

Side-finding recorded on #112: QEMU's /dev/fd0 was listed first by the
disk picker (live repro evidence in the same artifacts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 18:33:01 +01:00
f7246941e8 fix(power): exempt charge-limit oneshot from start rate limiting
All checks were successful
Check / eval (push) Successful in 3m9s
Round 1 of #101's V3 failed on TuringMachine (2026-07-13 18:16): a
SPACED dock/AC event storm — each run finishing before the next event —
never coalesces, so 5 successful sub-second starts inside systemd's
default 10s window tripped start-limit-hit and marked the unit failed
(doctor badge) although every run succeeded. The write is idempotent
and cheap; rate limiting it only manufactures false failures, so the
unit sets StartLimitIntervalSec=0.

checks.battery-charge-limit now asserts the exemption and runs 8
sequential starts expecting a clean, non-failed state; the original
mid-run coalescing phase is unchanged.

Verified: V2 — the check passes under KVM (exit 0). V0 flake check
green. V3 round 2 re-queued in HARDWARE-QUEUE.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 18:26:00 +01:00
720d9a38fb feat(template): document Nextcloud client opt-in via upstream HM service
All checks were successful
Check / eval (push) Successful in 3m15s
BACKLOG #102: a commented services.nextcloud-client example in the
downstream template (enable + startInBackground) instead of only the
bare package line — the upstream Home Manager module installs the app
AND ties it to graphical-session.target, so one block covers install,
session autostart, and clean removal. No Nomarchy wrapper added.

Verified: V1 — pinned HM 26.05 module options confirmed; eval test of
the uncommented block through lib.mkFlake shows the user unit
(ExecStart nextcloud --background, WantedBy/PartOf
graphical-session.target); nix flake check --no-build green
(template-sot, downstream-template-home included). Runtime lifecycle is
the upstream module's contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 17:57:14 +01:00
5d97cf23fe docs(agent): queue desktop and menu follow-ups
All checks were successful
Check / eval (push) Successful in 3m18s
Promote NEXT items #102-#112 for Nextcloud, live ISO apps, airplane mode, menu navigation and hierarchy, keybinding discoverability, keyboard indicator parity, Control Center retirement, recovery terminology, and installer disk-picker safety. Expand #96 for the live-ISO power policy and record #92's completed KVM UI verification.

Verified: V0 source audit and git diff --check; #92 additionally reached V2 on the exact current ISO under KVM.
2026-07-13 17:19:18 +01:00
d09c11d872 fix(install): constrain keyboard selection to XKB catalog
All checks were successful
Check / eval (push) Successful in 3m17s
Make keyboard typing filter-only with Gum strict mode, validate interactive and unattended choices against the pinned XKB catalog before any disk mutation, and show resolved human-readable choices in review.

Verified: V1 flake eval, focused installer builds, shell/Python/diff checks; current-ISO KVM boot proved the packaged new flow while the pre-destructive interaction pass continued.
2026-07-13 17:07:08 +01:00
9d0abe5422 fix(install): normalize empty keyboard variants
All checks were successful
Check / eval (push) Successful in 3m18s
Treat the picker-only (none) row as an empty XKB variant at the shared input boundary and again in the template patcher. Guard the real downstream output and console keymap, and carry the sentinel through the offline installer VM.

Verified: V0 full flake evaluation plus shell/Python/Nix/diff checks; V1 installer-keyboard, installer-safety, and template-SoT builds; V2 full KVM offline LUKS+swap install and themed first boot. No V3 required.
2026-07-13 15:01:51 +01:00
7bfe1af5b1 fix(power): coalesce charge-limit reapply events
All checks were successful
Check / eval (push) Successful in 3m16s
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
2a34c7398b fix(docking): make workspace and lid transitions atomic
All checks were successful
Check / eval (push) Successful in 3m19s
Restore the old flake's external-only workspace handoff, protect closed-lid cable removal with a verified low-level logind inhibitor, and use monitor-added as the settled audio reprobe boundary.

Verified: V2 — nix flake check --no-build; docking-ux, option-docs, and template-sot checks; KVM tools/monitor-fallback.nix lifecycle test.

V3 pending: closed-lid BenQ round 4 in agent/HARDWARE-QUEUE.md.
2026-07-13 13:56:54 +01:00
cffe432912 docs(agent): queue live ISO hardware findings
All checks were successful
Check / eval (push) Successful in 3m20s
Record the Acer Aspire install and session feedback as separate NOW items #91-#99, with the literal (none) XKB build blocker first and acceptance criteria for each issue.

Verified: V0 — source screenshots reviewed and git diff --check passed. Implementation verification remains item-specific.
2026-07-13 12:03:13 +01:00
7ebfab4bd4 fix(docking): make clamshell hotplug safe and discoverable
All checks were successful
Check / eval (push) Successful in 3m32s
Real dock QA exposed pointer-bound menus, an absent default rescue watcher, the unsafe zero-output panel-off path, missing keyboard/audio hotplug coverage, and unpainted new outputs.

Route Rofi to the focused output; replace panel disable with safe workspace migration; always run and reconnect the display watcher and repaint hotplugged outputs; expose a default per-device keyboard picker; handle audio card availability transitions.

Verified: V1 — nix flake check --no-build; checks.docking-ux real Home Manager artifact build; option-docs/template-sot; Nix parse, py_compile, and git diff --check. V2 unavailable: no readable KVM. V3 pending in agent/HARDWARE-QUEUE.md.
2026-07-13 11:53:01 +01:00
b898d59295 Merge remote-tracking branch 'origin/main' into main
All checks were successful
Check / eval (push) Successful in 3m2s
Lock bump / bump (push) Successful in 6m30s
2026-07-12 20:21:23 +01:00
ae6fe1d679 docs: update agent backlog, journal, and roadmap 2026-07-12 20:20:35 +01:00
67400b07dc fix(ci): memory-bounded eval — one output per nix process
Some checks failed
Check / eval (push) Failing after 7m59s
check.yml has been permanently red since the runner container was
capped at 2 GB (uncapped, it OOM'd the whole 4 GB VPS). Root cause,
measured with GNU time (eval-cache off): `nix flake check --no-build`
walks every output in ONE evaluator process and peaks at ~6.0 GB RSS
on this repo — the checks.* suite alone is ~15 runNixOSTests, each a
full NixOS eval, all accumulating in one heap.

New tools/ci-eval.sh: identical coverage (all checks.* drvPaths, both
nixosConfigurations toplevels, the HM activationPackage — enumerated
dynamically, no drift), one fresh nix process per output so the heap is
freed between outputs. Cold per-output peaks: worst check 0.99 GB
(hardware-toggles), nomarchy toplevel 0.78 GB, HM 0.60 GB; the one
outlier is nomarchy-live at 2.69 GB (live-ISO eval), which fits the
2 GB cap only via the container's swap allowance (docker's default
--memory-swap is 2x memory). check.yml and bump.yml both call the
script now; bump.yml also gains max-jobs=1/cores=2 (it had no limits,
so its V1 build gate could spawn parallel builders in the capped
container). ROADMAP's stale "no CI today" corrected: bump.yml landed
8fded63 on schedule 2026-07-06.

Verified: V1+ — ci-eval.sh green locally end-to-end (3m22s, tree peak
2.75 GB = nomarchy-live's process); per-output peaks measured
individually. The decisive proof is the next Actions run on the VPS
itself — watch the nomarchy-live step; if it OOMs there, the runner
needs swap allowed: --memory=2g --memory-swap=6g.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:39:46 +01:00