aboutsummaryrefslogtreecommitdiff
path: root/todo.org
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-20 15:30:14 -0500
committerCraig Jennings <c@cjennings.net>2026-07-20 15:30:14 -0500
commit417640ef5835021cea3e539c5a43e19034a46b36 (patch)
treef350030fcaa9a076ad76b97002907089e46e8fcf /todo.org
parentc4ae21253f71268ade4a0c753b547cc26cc270b1 (diff)
downloadarchsetup-417640ef5835021cea3e539c5a43e19034a46b36.tar.gz
archsetup-417640ef5835021cea3e539c5a43e19034a46b36.zip
chore: reorder open-work tasks in todo.org
Diffstat (limited to 'todo.org')
-rw-r--r--todo.org488
1 files changed, 244 insertions, 244 deletions
diff --git a/todo.org b/todo.org
index 8f9e318..4cd0055 100644
--- a/todo.org
+++ b/todo.org
@@ -45,55 +45,21 @@ below):
input-side-spec.org (DRAFT, four decisions open).
* Archsetup Open Work
-** TODO [#B] Installer GRUB_CMDLINE overwrite drops boot params :bug:solo:
-Grading: Critical severity (unbootable) x some-users-sometimes (machines whose base install set a cryptdevice=/resume=/zfs= cmdline param) = P2 = [#B].
-archsetup:3054 rewrites the whole GRUB_CMDLINE_LINUX_DEFAULT line with a fixed string; nothing re-adds a pre-existing cryptdevice/resume/zfs token, so grub-mkconfig (3059) can bake an unbootable config. Fix: read the current value and append only the missing tokens; assert any pre-existing boot-critical token survives before grub-mkconfig. See [[file:docs/design/2026-07-19-sentry-code-findings.org][sentry code findings]] (S3).
** DONE [#A] Tracked WireGuard private keys in repo — public leak, resolved :bug:security:network:
CLOSED: [2026-07-20 Mon]
Confirmed a live public leak, not just at-risk: git.cjennings.net runs cgit (scan-path=/var/git), so archsetup.git was anonymously cloneable over https. An unauthenticated clone pulled the configs with intact PrivateKeys. Exposed 2026-07-05 (c7b7d16) to 2026-07-20. Regraded to P1/[#A] (public credential exposure, severity-alone carve-out) from the initial [#B].
Scope was wider than first found: the current 3 configs (assets/wireguard-config/wg-*.conf) plus 7 older ones at the pre-reorg path assets/wireguard/ (switzerland x2, USCALA/USCASF/USDC/USGAAT/USNY) — 10 config files, all with real keys.
Resolution: Craig expired all the Proton WireGuard configs (keys dead). Purged all 10 from every commit with git filter-repo, force-pushed main + v0.5, and ran git gc --prune=now on the server bare repo. Verified via anonymous clone: zero real-key blobs reachable, all old exposed commits gone. Stopped tracking plaintext (gitignore + README, out-of-band configs only).
Follow-ups filed below: harden cgit exposure; installer no longer ships configs.
+** TODO [#B] Installer GRUB_CMDLINE overwrite drops boot params :bug:solo:
+Grading: Critical severity (unbootable) x some-users-sometimes (machines whose base install set a cryptdevice=/resume=/zfs= cmdline param) = P2 = [#B].
+archsetup:3054 rewrites the whole GRUB_CMDLINE_LINUX_DEFAULT line with a fixed string; nothing re-adds a pre-existing cryptdevice/resume/zfs token, so grub-mkconfig (3059) can bake an unbootable config. Fix: read the current value and append only the missing tokens; assert any pre-existing boot-critical token survives before grub-mkconfig. See [[file:docs/design/2026-07-19-sentry-code-findings.org][sentry code findings]] (S3).
** TODO [#B] Audit cgit-published repos for secrets and privacy :bug:security:
Grading: security carve-out — cgit at git.cjennings.net serves every repo under scan-path=/var/git over unauthenticated https (any repo is anonymously cloneable). archsetup being public is by design (curl-install), but this means ANY secret in ANY /var/git repo is world-readable, and any repo meant to be private is not. Severity depends on what else lives there = P2 = [#B], raise if a private repo with secrets is found.
Not :solo: — needs Craig's decisions. Steps: list repos under /var/git; for each, decide intended public vs private; scan each for secrets (keys, tokens, credentials) the way this WireGuard leak was found; for any meant-to-be-private repo, actually restrict access (cgit repo.hide only hides from the index — a known repo name is still cloneable; use http auth or move it off the public scan-path); for public repos, confirm no secrets and add a pre-receive/CI secret scan. Check dotfiles (git.cjennings.net/dotfiles) specifically — it is also under /var/git. archsetup's own move is decided and tracked separately below.
** TODO [#B] Move archsetup off cgit to cjennings@cjennings.net :chore:security:
Decided (Craig, 2026-07-20): move the archsetup repo off the public cgit host (git@cjennings.net, scan-path /var/git) to Craig's private account remote cjennings@cjennings.net, so it is no longer world-cloneable. This is the archsetup-specific fix for the cgit-exposure finding above.
Plan: create a bare repo under cjennings's control off the cgit scan-path (e.g. =~cjennings/git/archsetup.git=); push current main + tags there; migrate the post-receive hook that publishes the installer to =/var/www/cjennings/archsetup= so curl-install keeps working (the single published file stays public by design; only the repo goes private); update the origin remote on ratio and velox to =cjennings@cjennings.net:git/archsetup.git=; remove =/var/git/archsetup.git= so cgit no longer serves it. Verify: anonymous =git clone https://git.cjennings.net/archsetup.git= fails, the new private clone works from both machines, and the curl-install URL still returns the installer. Keep the two daily drivers' remotes in sync (daily-drivers rule).
-** TODO [#C] WireGuard import is now config-less — decide feature fate :feature:network:
-scripts/import-wireguard-configs.sh reads assets/wireguard-config/*.conf, but no configs ship in the repo anymore (removed as a public-leak fix; .gitignore blocks plaintext). Decide: remove the import feature entirely, or keep it and document dropping plaintext configs into the dir out-of-band at install time (they stay gitignored). If kept, confirm the script no-ops gracefully when the dir has no *.conf.
-** TODO [#C] Installer chpasswd unguarded — unloggable primary user :bug:solo:quick:
-Grading: Major severity (fresh system's primary user can't log in) x rare edge case (chpasswd seldom fails) = P3 = [#C].
-archsetup:1168 runs =echo "$user:$pass" | chpasswd= with no guard, then unsets the password next line; set -e is off (line 21), so a silent failure leaves no password and no log entry. Fix: guard with error_fatal (report + "set it by hand: passwd $user") before unsetting. See findings doc (S2).
-** TODO [#C] Installer nvme early module never built into initramfs :bug:solo:
-Grading: Minor severity (module autoload still boots the system) x most-machines (all Craig's ZFS-root boxes) = P3 = [#C].
-archsetup:2910 writes MODULES=(nvme) but the only mkinitcpio -P in boot_ux runs =if ! is_zfs_root=, so on ZFS-root non-Framework machines the early-load hardening is never compiled in. Also archsetup:2918 greps the whole file for "nvme" (not the MODULES line). Fix: rebuild initramfs after the MODULES edit regardless of ZFS; scope the presence grep to =^MODULES=(=. See findings doc (S3).
-** TODO [#C] Installer disk-space pre-flight check is fragile :bug:solo:quick:
-Grading: Major severity (aborts a valid install) x some (df wraps long device names on a live ISO / device-mapper root) = P3 = [#C].
-archsetup:487 parses =df / | awk 'NR==2'=, which reads the device-name line (empty $4 -> 0 GB) when df wraps; archsetup:488 also integer-truncates the GB compare against the 20 GB floor. Fix: =df -P /= (single-line) or =df --output=avail=; compare in KB to avoid the rounding bias. See findings doc (S1).
-** TODO [#C] Installer run_step state + exit-code handling :bug:solo:
-Grading: Major severity (resume re-runs steps and can abort on a survivable warning) x some (a step whose last action is a non-fatal failure) = P3 = [#C].
-archsetup:298 marks a step complete only when its function returns 0, but error_warn/run_task return 1, so a non-fatal-failing step never writes its marker and re-runs on resume. Also archsetup:1034 reports =$?= of the =false= test, not pacman's real exit code; and run_step locals (290/318) leak to global scope. Fix: step functions =return 0= explicitly (or gate run_step on a per-step error flag); capture the real exit code; add =local=. See findings doc (S1).
-** TODO [#C] cmail password decrypted world-readable before chmod :bug:security:solo:quick:cmail:
-Grading: security carve-out — brief local plaintext exposure of the mail password, requires a concurrent local shell during install; narrow window = low severity = P3 = [#C].
-scripts/cmail-setup-finish.sh:52 gpg-decrypts to ~/.config/.cmailpass at the process umask (often 0644), then chmod 600 on the next line. Fix: =(umask 077; gpg ... --output ...)= or decrypt to a mktemp 0600 file and mv into place (mirror the import-wireguard mktemp -d 0700 pattern). See findings doc (S4).
-** TODO [#C] Installer sudoers.pacnew blind copy risks lockout :bug:solo:quick:
-Grading: Major severity (a malformed sudoers locks out privilege escalation) x rare edge case = P3 = [#C].
-archsetup:1146 does =[ -f /etc/sudoers.pacnew ] && cp /etc/sudoers.pacnew /etc/sudoers= with no validation, right before the NOPASSWD rule at 1183. Fix: =visudo -cf /etc/sudoers.pacnew && cp ... || error_warn=. See findings doc (S2).
-** TODO [#C] WireGuard import leaves full-tunnel VPN live on failure :bug:solo:network:
-Grading: Major severity (all traffic silently routed through Proton until manual cleanup) x rare (nmcli modify failure) = P3 = [#C].
-scripts/import-wireguard-configs.sh:51-62 imports (which brings the 0.0.0.0/0 tunnel up), renames, then deactivates; under set -e a failed modify aborts before the down, leaving the tunnel live. Fix: bring the connection down right after parsing the UUID, before the rename. See findings doc (S4).
-** TODO [#D] Installer resume-idempotency cluster :bug:solo:
-Grading: Minor severity x rare edge case (re-run after a mid-step failure) = P4 = [#D]. Group of small non-idempotent / wrong-target spots.
-crontab log-cleanup line duplicates on resume (archsetup:1713 — guard on absence); zfs scrub timer picks an arbitrary pool via =head -1= and yields =@.timer= when empty (archsetup:1857); gamemode enabled via =systemctl --user= which the script itself documents fails at install time (archsetup:2419 — use the manual wants-symlink like syncthing). See findings doc (S2, S3).
-** TODO [#D] Installer unguarded chmod/cp after non-fatal ops :bug:solo:quick:
-Grading: Minor severity x rare edge case (only when a preceding non-fatal cp/clone failed) = P4 = [#D].
-With set -e off, unguarded chmod/cp hit missing/partial files silently: hypr-live-update-guard chmods (archsetup:2108/2144), zfs-replicate cp (archsetup:1820) leaving a service with a dead ExecStart, zfs-pre-snapshot cp (archsetup:1943) leaving a broken pacman hook. Fix: wrap each in =(...) >> log 2>&1 || error_warn=. See findings doc (S2, S3).
-** TODO [#D] normalize-notify-sounds temp/atomicity can corrupt tracked file :bug:solo:quick:
-Grading: Minor severity (corrupts a repo-tracked sound file, recoverable via git) x rare (ffmpeg failure/interrupt) = P4 = [#D].
-scripts/normalize-notify-sounds.sh:39-46 has no EXIT trap on the mktemp and does =cat "$tmp" > "$f"= (truncate-first) where $f is a stow symlink into the repo; a zero-byte/failed encode writes a corrupt file. Fix: EXIT trap; =[ -s "$tmp" ]= guard; write $f.tmp and overwrite on success. See findings doc (S4).
-** TODO [#D] Installer + scripts refactor opportunities :refactor:
-Grading: no behavior change; parking lot. 12 refactors from the sentry audit — duplicated GPU-modalias scan, triple hand-rolled retry loop, stow x4, display_server/window_manager dispatch dup, Maia ELO range x3, per-script log helpers, GRUB/snapper/fsck sed clusters, waybar-battery positional sed. Full list with line numbers in [[file:docs/design/2026-07-19-sentry-code-findings.org][sentry code findings]] (High/Medium/Low tagged). Pull individual ones out as their own tasks when tackled.
** TODO [#B] Velox boot-failure retrospective — upgrade guard gaps :bug:zfs:maint:
Post-mortem for the 2026-07-15 velox no-kernel boot failure, from the archsetup/maint code review:
- maint's UPDATE remedy runs a plain =yay -Syu --noconfirm= (remedies.py:297). The live-update guard (guard.py) only matches mesa/hyprland (the 2026-06-07 live-swap class) — it never checks /boot, kernel, initramfs, or mkinitcpio exit. No post-upgrade /boot assertion exists. An interrupted kernel transaction slips straight through.
@@ -101,35 +67,12 @@ Post-mortem for the 2026-07-15 velox no-kernel boot failure, from the archsetup/
- Sanoid-vs-actual dataset drift: configure_zfs_snapshots configures zroot/var/log + zroot/var/lib/pacman as separate datasets; velox's actual layout has neither separate (/var/log sits inside zroot/var). Reconcile.
- Confirm the pre-pacman snapshot hook is actually installed + firing on velox (it should be — it's what makes recovery possible).
-** TODO [#C] Add inetutils to install base :feature:solo:quick:network:
-TRAMP's /ftp: method (ange-ftp) shells out to a command-line ftp client; Arch ships none by default. GNU inetutils provides =/usr/bin/ftp=. Craig's dirvish config has an FTP quick-access entry (phone FTP server), so it's a config dependency. Installed manually on ratio 2026-07-14; velox needs it once it boots. Add to the install base so future machines get it for free; verify via VM test. From .emacs.d handoff 2026-07-14-1751.
-
-** TODO [#C] Waybar modules run together — need subtle separators :bug:dotfiles:waybar:
-Craig misreads where one module ends and the next begins — the wind (weather) value runs straight into the date with no visual stop, so he reads the wind figure as the start of the date. Add a light, subtle separator or spacing between adjacent Waybar modules.
-Grading: Minor severity (legibility, nothing broken) x frequent (every glance at the bar) = P3 = [#C].
-Not fully :solo: — needs Craig's eye on the result (separator style is a taste call, plus a live visual check). Prior work added a date-facing divider (dotfiles 103cccb); evidently not enough, so revisit the whole inter-module treatment rather than just the weather/date seam. From .emacs.d handoff 2026-07-20-1114 (roam capture; waybar is archsetup-owned per the dotfiles standing rule).
-** TODO [#C] Dupre theme waybar.css drifted from live style.css :bug:dotfiles:waybar:
-Grading: Minor severity (cosmetic, reverts only on a theme switch) × rare edge case (dupre is already the active theme) = P4 = [#D] on user impact, bumped to [#C] because the dotfiles =make test= stays RED until synced, poisoning the green baseline for every future commit.
-The weather-kit work added =#custom-weather= selectors to =hyprland/.config/waybar/style.css= but never mirrored them into =hyprland/.config/themes/dupre/waybar.css=. =tests/theme-css= asserts the two files are identical (set-theme copies the theme file over the live one), so switching to dupre would silently revert the weather chip styling. Fix: sync the theme file to live. Pre-existing; found 2026-07-19 during an unrelated commit's green-baseline run.
-
** TODO [#B] Assess a Hyprland left-drag window gesture :feature:hyprland:
Evaluate whether a global left-click drag can move ordinary windows without
breaking application selection, text interaction, or Wayland security
expectations. Document the safe modifier/gesture alternatives before changing
any binding.
-** TODO [#C] Add a time selector to the timer panel :feature:timer:
-Offer a period-appropriate selector for timer duration, likely drawing on the
-tape-counter idiom, while preserving the existing direct-entry path.
-
-** TODO [#C] Order network-panel connections by availability :feature:network:
-Present saved and currently available networks in this order: available saved
-profiles, available unsaved networks, then saved profiles that are unavailable.
-
-** TODO [#C] Indicate hotspot or metered WiFi in amber :feature:network:waybar:
-Detect hotspot/metered connectivity and render the WiFi icon plus SSID amber,
-while ordinary WiFi stays white.
-
** TODO [#B] Reconcile panel keybindings around Super+N :feature:hyprland:
Swap the notification and networking bindings so primary panels are one
Super-plus-letter chord away, audit the other exceptions, and bring the
@@ -144,10 +87,6 @@ maintenance console.
Expose channel-level input and output volume controls without losing the
existing device-level workflow.
-** TODO [#C] Add a whole-display dim mode :feature:hyprland:
-Extend auto-dim with an explicit “dim everything” setting for bright
-non-dark-mode contexts, with a security/usability review of its scope.
-
** DOING [#B] Widget gallery upgrades :feature:design:
:PROPERTIES:
:LAST_REVIEWED: 2026-07-13
@@ -378,27 +317,6 @@ Restyle the audio panel's GTK CSS onto =tokens-waybar.css= + the banked composit
After ~5 hand ports, weigh widget-level codegen with evidence (mechanical duplication vs judgment per port). Recorded as a dated decision in the spec; go spawns its own spec.
*** TODO Flip the spec to IMPLEMENTED
When the phases above close: status heading keyword → =IMPLEMENTED=, dated history line with the reason, Metadata =Status= mirror. Three lines, one file.
-** TODO [#C] Gallery probe: the fader-drag check is flaky :bug:test:design:
-=probe.mjs= check 3 ("fader drag tracks at 3x") intermittently reports =level 68 -> level 68=, i.e. the synthetic drag never registers. It has presumably been doing this all along unnoticed, since the suite is normally run once per batch.
-
-Grading: *Minor* severity (a false FAIL costs a re-run and a few minutes, and never ships a defect) x *most users, frequently* = P3 = =[#C]=.
-
-Frequency measured 2026-07-16, not estimated: 1 failure in 6 consecutive runs, having already fired twice in about fifteen that afternoon. The first grading guessed "some users, sometimes" (~1 in 10); at ~1 in 6, both people who run this suite hit it most sessions, so the row is "most users, frequently". The letter lands on =[#C]= either way, but the input was wrong and the matrix is only worth anything if its inputs are measured.
-
-Suspected cause: the check clicks the 3x size chip, calls =scrollIntoView=, waits a fixed 200ms, then reads =getBoundingClientRect= and dispatches the drag against those coordinates. If the zoom relayout or the smooth scroll hasn't settled, the rect is stale and the press lands off the fader — so the drag is a no-op and the readout never moves. The other timing-sensitive checks share the same fixed-sleep shape.
-
-*Do not fix this by raising the sleep.* That hides the race rather than removing it and leaves the check failing again on a slower run. Wait on the actual condition instead: poll until the rect stops changing between frames, or assert the press landed on the fader before dispatching the drag (the probes' own README already warns that a =find()= miss dispatches into nothing and reports as a widget bug).
-
-Why it matters beyond the annoyance: a gate that cries wolf gets its real failures ignored, and this suite is the only thing standing between the gallery and a silent regression.
-
-Recurrences: 2026-07-18 batch-6 gate (first run, passed 3 reruns); 2026-07-18 batch-9 gate (first cold run, =level 68 -> level 68=, passed 2 reruns). Both were the session's first probe run — consistent with the stale-rect theory (cold-start relayout settling slower than the fixed 200ms sleep).
-
-** TODO [#C] Maint live-refresh hairline replacement :feature:maint:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-07-14
-:END:
-From the roam inbox (routed 2026-07-13): the memory-killer section seemed to update too often, and "it's a bit unclear what the line is doing; consider something else." Diagnosis (2026-07-14): the data cadence is already the requested 3s (gui live tier, _LIVE_SECONDS); the perceived churn is the live-refresh hairline — the 2px bar under the live sections that drains full-to-empty over each 3s window, redrawn at 150ms (gui._hair_tick, viewmodel.refresh_fraction). It exists to tell a stale board from a frozen one (2026-07-09), but it reads as constant unexplained motion. Design call for Craig: replace the draining line with something whose meaning is legible — candidates: a dot that blinks once per refresh, a "3s" age caption that only appears when refresh is overdue, slowing the drain redraw, or dropping the indicator on live tiers and keeping it only when data goes stale. Keep the stale-vs-frozen distinguishability that motivated the hairline.
-
** TODO [#B] Net doctor expansion v1 — VM live verification :feature:dotfiles:network:
:PROPERTIES:
:SPEC_ID: ce29b103-ed9d-4f56-bf8c-9ed8fe680ff3
@@ -420,37 +338,6 @@ On dotfiles main (=12e3e76=, pushed). =gather_context= derives an auth cause on
*** 2026-07-12 Sun @ 09:14:00 -0500 Flipped the net spec to IMPLEMENTED and logged the vNext items
Spec status heading now IMPLEMENTED (dated history line + Status mirror); all four phase headings DONE. vNext items (flaky/drops cluster, DoT/DNSSEC verdict, profile hygiene) logged as the "Net doctor vNext" task. The privileged-fix live halves remain with the VM live-verification sub-task and the manual-testing checklist — findings there come back as bugs.
-** TODO [#C] Net panel speedtest history :feature:dotfiles:network:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-07-14
-:END:
-From the roam inbox (routed 2026-07-13): the networking panel should track speedtests over time with appropriate info. Shape: persist each SPEED TEST result (timestamp, down/up, latency, server) to a small local store and surface history in the net panel. Design questions for work time: retention window, which fields matter, and presentation within the panel's ~400px width (recent-results list vs trend readout). Point-in-time results exist today; the gap is comparison across days and venues.
-
-** TODO [#D] Net doctor vNext :feature:dotfiles:network:
-Deferred from the [[file:docs/specs/2026-07-11-net-doctor-expansion-spec.org][net doctor expansion spec]] (IMPLEMENTED, v1 shipped): event-log correlation for the flaky/drops cluster (powersave, roaming stalls, USB autosuspend, no-reconnect-after-resume, firmware crashloop drop signatures — needs history a one-shot probe can't see); a DoT/DNSSEC-specific verdict distinguishing "the venue resolver mangles DNSSEC" from the generic DNS-not-resolving; per-profile autoconnect/duplicate-profile hygiene.
-
-** TODO [#D] Bt doctor vNext :feature:dotfiles:bluetooth:
-Deferred from the [[file:docs/specs/2026-07-11-bt-doctor-expansion-spec.org][bt doctor expansion spec]] (IMPLEMENTED, v1 shipped): the stale-bond re-pair-offer signature (v1 keeps re-pair strictly user-initiated; the signature must be designed and validated before the doctor ever offers it); a connection-parameter/coexistence hint tail for a "keeps dropping" verdict; the bt-audio-profile expansion beyond the current a2dp repair (codec fallback, default-sink, absolute-volume) — wants coordination with the audio doctor so the two panels don't claim the same A2DP/HFP diagnosis with divergent verdicts.
-
-** TODO [#D] Widget catalogue vNext :feature:design:
-Deferred from the [[file:docs/specs/2026-07-12-component-generation-spec.org][component-generation spec]] (DRAFT): framework wrappers for the web library (React or otherwise — demand-gated, none until a real framework consumer exists); Level-2/3 widget codegen if the spec's Phase 5 decision point says go (a go spawns its own spec); ports beyond the demand matrix as new consumers appear.
-
-** TODO [#D] Maintenance console vNext :feature:
-Deferred from the [[file:docs/specs/2026-07-07-maintenance-console-spec.org][spec]] (v1 ships determinate remedies only): AI/workflow assistance on read-only metrics (the vLater tier — failed-unit diagnosis, journal-error fix suggestions, OOM investigation); SIGKILL escalation for TERM-survivors on the KILL lever; live streaming meters on evidence rows where a count could be a level.
-
-** TODO [#D] Retention-repair remedy unreachable from the panel GUI :bug:dotfiles:
-snapshot_retention_repair (WRITE SANE LIMITS — =snapper -c <config> set-config TIMELINE_CREATE=yes TIMELINE_LIMIT_*=<TOML values>=) sits in the maint remedy table but is wired into no GUI layer (no reference in panel.py, viewmodel.py, or gui.py) — it's reachable only via =maint doctor review= / =maint fix=. Worse, the REVIEW & FIX roster's wording for item-bearing remedies says "per-item — on its subpanel", which for this remedy points at a key that doesn't exist. Found 2026-07-08 while walking the SNAPSHOTS subpanel with Craig.
-
-Two fix shapes, decide at work time: (1) wire a digest key — e.g. on the timeline row when the config's installed TIMELINE_LIMIT_* drift from the TOML values (needs the probe to read the installed limits for comparison); or (2) keep it CLI-only deliberately and special-case the roster wording for remedies with no panel key ("CLI only — maint fix"). Option 2 is a few lines; option 1 makes limit drift visible on the board, which is the 2026-05-26 pile-up's root cause. Severity minor × rare edge = P4 per the bug matrix.
-
-** TODO [#C] zfs base VM image build failure: ZFS DKMS module missing :bug:zfs:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-07-09
-:END:
-=FS_PROFILE=zfs make test-vm-base= fails inside the VM at initramfs time: archangel reports "ZFS module not found! DKMS build may have failed" against the installed kernel (linux-lts 6.18.38 at the 2026-07-08 attempt). Consequences: the maint scenario harness's zfs lane (Phase 12) is filtered but unexercised, and a real zfs bare-metal install via archangel would plausibly hit the same wall. Priority per the bug matrix: Major severity (zfs install path broken) × some-users-sometimes = P3. When fixed, run =FS_PROFILE=zfs bash scripts/testing/run-maint-scenarios.sh --list= and add zfs scenario files (zpool scrub / autotrim / snapshot destroy) to the harness.
-*** 2026-07-14 Tue @ 01:40:48 -0500 Diagnosed: OpenZFS/kernel version skew, blocked on archzfs
-Reproduced in ~1 minute of install: =dkms install zfs/2.3.3 -k 6.18.38-2-lts= exits 1 during pacstrap. Root cause confirmed: OpenZFS 2.3.3's META declares Linux-Maximum 6.15, and the VM installs linux-lts 6.18.38. The first release supporting 6.18 is 2.4.0 (2.4.1 covers 6.19), and archzfs currently serves only zfs-dkms 2.3.3-1 — nothing on our side to fix. Unblock condition: archzfs publishes zfs-dkms ≥2.4.0; recheck with =curl -s https://archzfs.com/archzfs/x86_64/ | grep -o 'zfs-dkms-[0-9.]*'=, then rerun =FS_PROFILE=zfs make test-vm-base=.
-
** TODO [#B] Consistent keybinding family for the panel console :feature:hyprland:
:PROPERTIES:
:LAST_REVIEWED: 2026-07-09
@@ -488,20 +375,6 @@ Disabled 2026-06-12 (bind and cycle entry points removed; Super+Shift+S reassign
The support machinery was deliberately kept for this task: =layout-navigate= and =layout-resize= retain their scrolling branches, =waybar-layout= still renders the scrolling state, and the unbound legacy =cycle-layout= script still lists it. Re-enabling is two lines: add =scrolling= back to =LAYOUTS= in =layout-cycle= and restore a direct-jump bind (the old chord is taken now — pick a new one). The =tests/layout-cycle= suite pins the disabled state and will go red on re-enable, which is the reminder to update it.
-** TODO [#C] Dotfiles stow conflicts: first-launch risk + restow directory handling :bug:dotfiles:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-07-14
-:END:
-*** 2026-07-14 Tue @ 00:51:51 -0500 Ratio calibre check passed; waypaper canonical decided (dark-lion)
-Ratio's ~/.config/calibre is a directory symlink into the dotfiles repo (stow folded the whole dir), so the first-launch gap never existed there — check closed. Craig decided dark-lion.jpg is the canonical waypaper wallpaper; the repo config.ini updated from the that-one-up-there.jpg placeholder (the file is skip-worktree volatile, unskipped for the commit and re-flagged). Remaining: when velox is back online, run make conflicts / make reset there so its old conflict copy clears against the updated repo.
-*** 2026-07-02 Thu @ 17:30:00 -0400 Shipped the Makefile hardening + first-launch guard (dotfiles 42a82d2)
-The solo-able subset landed in the speedrun. =make conflicts <de>= is the loud first-launch guard: dry-runs all tiers, parses all four stow error shapes (plain file conflict, foreign symlink, dir-over-file, and restow's unstow_contents non-directory ERROR), lists each blocker with a directory/foreign-symlink marker, exits 1 when any exist. =make reset= now pre-clears the directory and foreign-symlink blockers =--adopt= aborts atomically on (removals printed; repo version wins per the target's contract), then adopts + git-checkouts as before. =make restow='s overwrite path switched rm -f → rm -rf so directory conflicts clear. 8 sandbox tests drive the real Makefile against a throwaway HOME (44 suites green). Also verified on velox: the whereami and mpd-playlists conflicts noted in this task were already hand-converted 2026-06-29 — =make conflicts hyprland= reports clean live. REMAINING (deferred per Craig's speedrun pre-flight): the waypaper canonical decision (live velox dark-lion.jpg vs repo that-one-up-there.jpg) and the ratio calibre-symlink check (ratio paused).
-From the velox calibre incident (2026-06-27, note in ~/.dotfiles/inbox/processed/): calibre was launched before =make stow= ran, wrote its own default config into =~/.config/calibre/=, and silently blocked its own stow — it ran on factory defaults while the rest of common/ stowed fine. General pattern: any GUI app that auto-creates config on first run, launched before stow, blocks its own stow the same way. Velox was repaired by hand (=ln -srf= symlinks byte-identical to =stow --no-folding= output).
-
-Remaining work (re-graded C 2026-07-02 — the first-launch risk and the Makefile handling shipped in the speedrun; what's left is a decision and a paused-machine check):
-- Waypaper canonical decision (Craig): live velox has =dark-lion.jpg=, repo has =that-one-up-there.jpg= (placeholder?). Decide the canonical, then =make reset= clears it.
-- Ratio check: whether =~/.config/calibre/*.json= on ratio are symlinks or real files — same first-launch gap likely if calibre ever launched there before stow. On the ratio trip list.
-
** TODO [#B] Audit dotfiles/common directory :chore:dotfiles:
:PROPERTIES:
:LAST_REVIEWED: 2026-07-14
@@ -629,18 +502,6 @@ Build handed off to the dotfiles project 2026-07-04 (=~/.dotfiles/inbox/2026-07-
*** TODO Phase 5 — VPN / WireGuard CLI fold (vNext) :network:
Rescoped 2026-07-04 (audit): the tunnels track already shipped most of the original Phase 5. Panel tunnel bring-up/down and detection landed (dotfiles 2d9d060 probes tailscale/NM-wireguard/Proton; 21db05a brings overlays up/down from the panel's Tunnels sub-view; 31ba056 diagnose/doctor understand tunnel routes; archsetup 2e40781 wireguard config import; the net-panel-other-interfaces spec is IMPLEMENTED). What remains for Phase 5 is only the =net vpn ...= CLI subcommand — cli.py still has no vpn/tunnel parser. Fold the panel's existing tunnel operations into a CLI surface; spec separately when picked up.
-** TODO [#C] Waybar collapse control: replace the triangle glyph :feature:waybar:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-07-14
-:END:
-From the 2026-07-04 roam capture. The waybar collapse mechanism (click the triangle, the bar sections redisplay shortened) works, but the triangle glyph doesn't match the instrument-console aesthetic the panels now use. Replace it with something in keeping with the console look. Aesthetic decision — bring Craig two or three concrete glyph/style options (a machined chevron, a console-key style expander, an engraved caret) before wiring. Dotfiles waybar config (handled per the archsetup-owns-dotfiles rule). Raised alongside the net-panel/audio speedrun; deferred from it because the glyph choice is a taste call.
-
-** TODO [#C] Net panel: driver-health diagnostic tier :feature:network:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-07-14
-:END:
-Follow-up from the 2026-07-04 net-panel hardening speedrun (Craig's cj question on the no-WiFi item). The shipped no-wifi-hardware verdict covers "no adapter at all." This tier covers "adapter present but the driver is wedged": read-only health signals — =ip link= (device present but no-carrier / down), =dmesg= / =journalctl -k= for firmware-load failures, =rfkill= for a hard block, =modinfo= / =lsmod= for the driver module — classified before a generic reset. Remedy actions: a privileged =modprobe -r <mod> && modprobe <mod>= reload of the wifi driver, and a firmware-package pointer when the failure is a missing/failed firmware load. Dotfiles net-package work (handled per the archsetup-owns-dotfiles rule). Design pass first to decide whether it's worth a repair tier vs a needs-user-action pointer.
-
** TODO [#B] Desktop-settings dropdown panel :feature:waybar:
:PROPERTIES:
:LAST_REVIEWED: 2026-07-09
@@ -708,14 +569,6 @@ Boot the configured endpoint and send a short prompt; surface success/failure +
Acceptance: fresh VM install of the ratio profile reaches an endpoint on =:8081= that answers a smoke prompt; velox profile gets Q4_K_M + 8B and answers a prompt within reasonable laptop latency; network-down install completes successfully with the pending-models warning surfaced.
-** TODO [#C] Voice dictation / speech-to-text input :feature:tooling:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-07-06
-:END:
-Push-to-talk dictation that types transcribed speech into the focused Wayland window — usable at any text field, including the Claude Code terminal prompt and Emacs buffers. Claude Code has no built-in voice input; dictation has to happen at the OS level and inject text. Raised 2026-07-03.
-
-Tool choice is the open decision (needs Craig): =nerd-dictation= (Vosk, lighter, lower accuracy) vs a =whisper.cpp=-based daemon (heavier, higher accuracy, optional GPU). Wayland typing backend is =wtype= or =ydotool=. Scope once chosen: install + model download, a push-to-talk keybind (Hyprland), and an autostart entry; fold into archsetup so it lands on both daily drivers. Consider an Emacs-native path (=whisper.el=) as a complement for in-buffer dictation.
-
** TODO [#B] Review post-archsetup laptop setup steps (velox 2026-04-10)
:PROPERTIES:
:LAST_REVIEWED: 2026-07-04
@@ -858,34 +711,6 @@ Need to investigate proper machine-ID regeneration that doesn't break networking
Would enable parallel test execution in CI/CD
Priority C because snapshot-based testing meets current needs
-** TODO [#C] Fix install errors surfaced by the 2026-05-11 VM test run
-:PROPERTIES:
-:LAST_REVIEWED: 2026-07-06
-:END:
-*** 2026-06-28 Sun @ 13:29:29 -0400 Audit reconcile: 2026-06-28 btrfs+zfs runs reproduce the same residual set
-Newer full runs landed since the 2026-06-11 reconcile below: the 2026-06-25 zfs run (Testinfra 96/0) and the 2026-06-28 btrfs+zfs runs (97/0, "zero attributed issues"). The residual four were NOT fixed and reproduce unchanged: =enabling firewall= (archsetup:1496-1498, carries a VM-kernel note), =enabling gamemode for user= (archsetup:2221, non-critical), and =tidaler (AUR)=. Zero archsetup-attributed Testinfra issues across both profiles confirms these are environment / non-critical, not archsetup bugs. Bare-metal confirmation of the firewall pair is still the open thread.
-
-*** 2026-06-15 Mon @ 23:53:21 -0500 Audit reconcile: latest VM run (2026-06-11) confirms the surviving error set
-The most recent VM run (=test-results/20260611-113904/=) carries four error-summary entries: =enabling firewall= + =verifying firewall is active= (the iptables/nf_tables "Could not fetch rule set generation id" pair, still unconfirmed on bare metal), =enabling gamemode for user= (non-critical), and =tidaler (AUR)=. The earlier fontconfig/dconf fixes held — none reappear. So the count is down from the 7→6 anchor below to four, all of them the known-residual items already itemized.
-Errors logged during the VM install. Status as of the 2026-05-11 18:36 run (=test-results/20260511-183643/archsetup-output.log=) after the =48c9439= fontconfig/dconf fix: 7 → 6.
-- refreshing font cache — RESOLVED in =48c9439= (now installs =fontconfig= before calling =fc-cache=).
-- configuring GTK file chooser — RESOLVED in =ecab29f= (switched to a system-wide dconf db at =/etc/dconf/db/site.d/=; needs no session bus during install).
-- configuring GNOME interface settings in dconf — RESOLVED in =ecab29f= (same fix as the GTK file chooser above).
-- enabling firewall — exit 1: =iptables v1.8.13 (nf_tables): Could not fetch rule set generation id: Invalid argument=. Still present in the 18:36 run; likely a VM-kernel/nf_tables artifact — confirm on bare metal before treating as an archsetup bug.
-- verifying firewall is active — exit 1 (follow-on from the firewall-enable error).
-- enabling gamemode for user — exit 1 → step "gaming" FAILED — non-critical.
-- tidaler (AUR) — logged in the error summary with exit code 0 (odd; logging quirk or transient AUR build noise?).
-Also seen in the 18:36 run's log-diff (post-install systemd noise, probably VM-environment): =pam_systemd … CreateSession failed= / =logind: Failed to start session scope … Permission denied=, and =Failed to start Proton VPN Daemon= (no VPN config in the test VM).
-
-*** 2026-05-19 Tue @ 13:18:56 -0500 Fixed AUR exit-0 logging bug at the root
-Root cause was in =retry_install=: =last_exit_code=$?= ran AFTER =if eval ...; then return 0; fi=. Bash defines an if-compound's exit status as zero when no condition tested true, so a failing eval's exit code got overwritten with 0 before reaching =error_warn=. Fix in =8221c54=: capture =$?= from =eval= directly into a local var, then compare against the captured value in the if. VM-verified in =test-results/20260519-115318/=: =mkinitcpio-firmware (AUR)= and =tidaler (AUR)= now report =error code: 1= (yay's actual exit) instead of the misleading =error code: 0=. The same packages still appear in the summary because yay returns non-zero when sub-deps fail to build (e.g. =aic94xx-firmware=), but the codes are accurate now. If the underlying sub-dep failures stay noisy, that's a separate concern — open a new task.
-
-*** 2026-05-16 Sat @ 09:00:41 -0500 AI Response: Surfaced the expanded AUR-exit-0 pattern
-2026-05-16 07:40 VM run passed (52/0/5) with the same warning profile as the 2026-05-11 18:36 run. Error count went 7 → 13: 5 fixed/unchanged, +5 new AUR-exit-0 entries (broadens the existing tidaler item into the dedicated =[#B]= subtask above), +1 genuinely new error in =setting up emacs configuration files= (=git pull= ran in =~/.emacs.d= which existed from stow but had no =.git=). Patched =archsetup:1932-1945= with a three-branch check: clone if missing/empty, pull if =.git= exists, =git init=/=fetch=/=checkout= in place if the dir came from stow.
-
-*** 2026-05-19 Tue @ 01:25:26 -0500 Verified the b9907c7 emacs-stow fix end-to-end
-=make test= 21:44 → 22:29 (42 min), =test-results/20260518-214516/=. 52/0/5, =ArchSetup Exit Code: 0=. The third-branch path fired correctly — install log =archsetup-2026-05-18-21-45-46.log:14358-14365= shows =From https://git.cjennings.net/dotemacs= → =[new branch] main -> origin/main= → =Reset branch 'main'= → =branch 'main' set up to track 'origin/main'=. No exit-128, no =fatal: not a git repository=. Error Summary down to 7 (was 13 on 2026-05-16); the emacs entry is gone. AUR exit-0 logging triggered for 2 packages this run (mkinitcpio-firmware, tidaler) vs 6 on 2026-05-16 — same bug class, fewer triggers, still tracked under =[#B] AUR exit-0 logged as error=. Issue Attribution: 1 ARCHSETUP entry (Proton VPN Daemon failed — known VM-no-VPN-config artifact). Cleanup ran clean via the normal path.
-
** TODO [#B] Review undeclared ratio packages for installer inclusion :chore:
:PROPERTIES:
:LAST_REVIEWED: 2026-07-09
@@ -993,72 +818,6 @@ Add kernel parameter: ~rtc_cmos.use_acpi_alarm=1~ (will become systemd default)
Consider: ~acpi_mask_gpe=0x1A~ for battery drain, suspend-then-hibernate config
See Framework community notes on logind.conf and sleep.conf settings
-** TODO [#C] Re-check python-lyricsgenius --skipinteg workaround :chore:solo:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-07-09
-:END:
-archsetup installs =python-lyricsgenius= with =--mflags --skipinteg=, skipping makepkg integrity + PGP checks — a workaround originally for an expired-signature issue upstream (surfaced by the 2026-06-23 --noconfirm audit). Periodically test whether the cause has cleared: if a plain =aur_install python-lyricsgenius= builds without complaint, drop the =--skipinteg= workaround. Removal needs a real AUR build to confirm, so it isn't a blind change.
-
-*** 2026-07-14 Tue @ 01:40:48 -0500 Rechecked: still needed, same cause
-Fresh AUR clone, =makepkg --verifysource= on 3.7.0-1 (PKGBUILD unchanged): tarball passes, =LICENSE.txt= still FAILS its b2sum — the github-master pin, structural as diagnosed 2026-06-24. =--skipinteg= stays.
-
-*** 2026-07-02 Thu @ 05:08:37 -0400 Rechecked: still needed, same cause
-=makepkg --verifysource= on 3.7.0-1: tarball passes, =LICENSE.txt= still FAILS
-its b2sum — the PKGBUILD still pins a hash for a file fetched from github
-master. Structural, as diagnosed 2026-06-24; =--skipinteg= stays.
-
-*** 2026-06-24 Wed @ 17:55:34 -0400 Rechecked: still needed, but the cause changed
-Ran =makepkg --verifysource= on the current AUR PKGBUILD (3.7.0-1). The package tarball =lyricsgenius-3.7.0.tar.gz= now passes its b2sum — the original expired-PGP-signature problem is gone (the PKGBUILD no longer carries any =validpgpkeys=). But integrity still FAILS, on a different file: =LICENSE.txt=, which the PKGBUILD fetches from the project's github master and pins a b2sum for. github master is a moving target, so that b2sum drifts and =--skipinteg= is still required. This is structural (not a transient upstream fix away), so it likely won't clear until the maintainer pins the LICENSE to a tagged release. Updated the archsetup comment to the real cause. Keep rechecking, but lower expectations of it clearing.
-
-** TODO [#C] Review theme config architecture for dunst/fuzzel
-:PROPERTIES:
-:LAST_REVIEWED: 2026-07-06
-:END:
-The active dunst config is stowed from dotfiles/common/ but theme templates
-live in dotfiles/hyprland/.config/themes/. set-theme copies the templates to
-the stowed locations at runtime, so edits to the common file get overwritten
-on theme switch. This split between stowed configs and theme templates is
-error-prone — changes must be made in both places. Consider:
-- Having set-theme be the single source of truth (remove common dunstrc from stow)
-- Or symlinking the stowed config to a theme-managed location
-- Same situation applies to fuzzel.ini
-The goal is a single place to edit each config, not two.
-
-** TODO [#C] Review current tool pain points annually
-:PROPERTIES:
-:LAST_REVIEWED: 2026-07-06
-:END:
-Once-yearly systematic inventory of known deficiencies and friction points in current toolset
-
-** TODO [#D] Consider Customizing Hyprland Animations
-Current: windows pop in, scratchpads slide from bottom.
-
-Customizable animations:
-- windows / windowsOut / windowsMove - window open/close/move
-- fade - opacity changes
-- border / borderangle - border color and gradient angle
-- workspaces - workspace switching
-- specialWorkspace - scratchpads (currently slidevert)
-- layers - waybar, notifications, etc.
-
-Styles: slide, slidevert, popin X%, fade
-Parameters: animation = NAME, ON/OFF, SPEED, BEZIER, STYLE
-Speed: lower = faster (1-10 typical)
-
-Example tweaks:
-#+begin_src conf
-animation = windows, 1, 2, myBezier, popin 80%
-animation = workspaces, 1, 4, default, slide
-animation = fade, 1, 2, default
-animation = layers, 1, 2, default, fade
-#+end_src
-
-** TODO [#D] Parse and improve AUR error reporting
-Parse yay errors and provide specific, actionable fixes instead of generic error messages
-
-** TODO [#D] Improve progress indicators throughout install
-Enhance existing indicators to show what's happening in real-time
-
** TODO [#B] Manual testing and validation :test:
:PROPERTIES:
:LAST_REVIEWED: 2026-07-09
@@ -1700,6 +1459,247 @@ Specced 2026-07-10 after discussion with Craig, and the design grew past the ori
Parent spec: [[file:docs/specs/2026-07-09-audio-doctor-spec.org][docs/specs/2026-07-09-audio-doctor-spec.org]] (IMPLEMENTED). This is a v1 gap found after the fact, not a phase of it.
+** TODO [#C] WireGuard import is now config-less — decide feature fate :feature:network:
+scripts/import-wireguard-configs.sh reads assets/wireguard-config/*.conf, but no configs ship in the repo anymore (removed as a public-leak fix; .gitignore blocks plaintext). Decide: remove the import feature entirely, or keep it and document dropping plaintext configs into the dir out-of-band at install time (they stay gitignored). If kept, confirm the script no-ops gracefully when the dir has no *.conf.
+** TODO [#C] Installer chpasswd unguarded — unloggable primary user :bug:solo:quick:
+Grading: Major severity (fresh system's primary user can't log in) x rare edge case (chpasswd seldom fails) = P3 = [#C].
+archsetup:1168 runs =echo "$user:$pass" | chpasswd= with no guard, then unsets the password next line; set -e is off (line 21), so a silent failure leaves no password and no log entry. Fix: guard with error_fatal (report + "set it by hand: passwd $user") before unsetting. See findings doc (S2).
+** TODO [#C] Installer nvme early module never built into initramfs :bug:solo:
+Grading: Minor severity (module autoload still boots the system) x most-machines (all Craig's ZFS-root boxes) = P3 = [#C].
+archsetup:2910 writes MODULES=(nvme) but the only mkinitcpio -P in boot_ux runs =if ! is_zfs_root=, so on ZFS-root non-Framework machines the early-load hardening is never compiled in. Also archsetup:2918 greps the whole file for "nvme" (not the MODULES line). Fix: rebuild initramfs after the MODULES edit regardless of ZFS; scope the presence grep to =^MODULES=(=. See findings doc (S3).
+** TODO [#C] Installer disk-space pre-flight check is fragile :bug:solo:quick:
+Grading: Major severity (aborts a valid install) x some (df wraps long device names on a live ISO / device-mapper root) = P3 = [#C].
+archsetup:487 parses =df / | awk 'NR==2'=, which reads the device-name line (empty $4 -> 0 GB) when df wraps; archsetup:488 also integer-truncates the GB compare against the 20 GB floor. Fix: =df -P /= (single-line) or =df --output=avail=; compare in KB to avoid the rounding bias. See findings doc (S1).
+** TODO [#C] Installer run_step state + exit-code handling :bug:solo:
+Grading: Major severity (resume re-runs steps and can abort on a survivable warning) x some (a step whose last action is a non-fatal failure) = P3 = [#C].
+archsetup:298 marks a step complete only when its function returns 0, but error_warn/run_task return 1, so a non-fatal-failing step never writes its marker and re-runs on resume. Also archsetup:1034 reports =$?= of the =false= test, not pacman's real exit code; and run_step locals (290/318) leak to global scope. Fix: step functions =return 0= explicitly (or gate run_step on a per-step error flag); capture the real exit code; add =local=. See findings doc (S1).
+** TODO [#C] cmail password decrypted world-readable before chmod :bug:security:solo:quick:cmail:
+Grading: security carve-out — brief local plaintext exposure of the mail password, requires a concurrent local shell during install; narrow window = low severity = P3 = [#C].
+scripts/cmail-setup-finish.sh:52 gpg-decrypts to ~/.config/.cmailpass at the process umask (often 0644), then chmod 600 on the next line. Fix: =(umask 077; gpg ... --output ...)= or decrypt to a mktemp 0600 file and mv into place (mirror the import-wireguard mktemp -d 0700 pattern). See findings doc (S4).
+** TODO [#C] Installer sudoers.pacnew blind copy risks lockout :bug:solo:quick:
+Grading: Major severity (a malformed sudoers locks out privilege escalation) x rare edge case = P3 = [#C].
+archsetup:1146 does =[ -f /etc/sudoers.pacnew ] && cp /etc/sudoers.pacnew /etc/sudoers= with no validation, right before the NOPASSWD rule at 1183. Fix: =visudo -cf /etc/sudoers.pacnew && cp ... || error_warn=. See findings doc (S2).
+** TODO [#C] WireGuard import leaves full-tunnel VPN live on failure :bug:solo:network:
+Grading: Major severity (all traffic silently routed through Proton until manual cleanup) x rare (nmcli modify failure) = P3 = [#C].
+scripts/import-wireguard-configs.sh:51-62 imports (which brings the 0.0.0.0/0 tunnel up), renames, then deactivates; under set -e a failed modify aborts before the down, leaving the tunnel live. Fix: bring the connection down right after parsing the UUID, before the rename. See findings doc (S4).
+** TODO [#C] Add inetutils to install base :feature:solo:quick:network:
+TRAMP's /ftp: method (ange-ftp) shells out to a command-line ftp client; Arch ships none by default. GNU inetutils provides =/usr/bin/ftp=. Craig's dirvish config has an FTP quick-access entry (phone FTP server), so it's a config dependency. Installed manually on ratio 2026-07-14; velox needs it once it boots. Add to the install base so future machines get it for free; verify via VM test. From .emacs.d handoff 2026-07-14-1751.
+
+** TODO [#C] Waybar modules run together — need subtle separators :bug:dotfiles:waybar:
+Craig misreads where one module ends and the next begins — the wind (weather) value runs straight into the date with no visual stop, so he reads the wind figure as the start of the date. Add a light, subtle separator or spacing between adjacent Waybar modules.
+Grading: Minor severity (legibility, nothing broken) x frequent (every glance at the bar) = P3 = [#C].
+Not fully :solo: — needs Craig's eye on the result (separator style is a taste call, plus a live visual check). Prior work added a date-facing divider (dotfiles 103cccb); evidently not enough, so revisit the whole inter-module treatment rather than just the weather/date seam. From .emacs.d handoff 2026-07-20-1114 (roam capture; waybar is archsetup-owned per the dotfiles standing rule).
+** TODO [#C] Dupre theme waybar.css drifted from live style.css :bug:dotfiles:waybar:
+Grading: Minor severity (cosmetic, reverts only on a theme switch) × rare edge case (dupre is already the active theme) = P4 = [#D] on user impact, bumped to [#C] because the dotfiles =make test= stays RED until synced, poisoning the green baseline for every future commit.
+The weather-kit work added =#custom-weather= selectors to =hyprland/.config/waybar/style.css= but never mirrored them into =hyprland/.config/themes/dupre/waybar.css=. =tests/theme-css= asserts the two files are identical (set-theme copies the theme file over the live one), so switching to dupre would silently revert the weather chip styling. Fix: sync the theme file to live. Pre-existing; found 2026-07-19 during an unrelated commit's green-baseline run.
+
+** TODO [#C] Add a time selector to the timer panel :feature:timer:
+Offer a period-appropriate selector for timer duration, likely drawing on the
+tape-counter idiom, while preserving the existing direct-entry path.
+
+** TODO [#C] Order network-panel connections by availability :feature:network:
+Present saved and currently available networks in this order: available saved
+profiles, available unsaved networks, then saved profiles that are unavailable.
+
+** TODO [#C] Indicate hotspot or metered WiFi in amber :feature:network:waybar:
+Detect hotspot/metered connectivity and render the WiFi icon plus SSID amber,
+while ordinary WiFi stays white.
+
+** TODO [#C] Add a whole-display dim mode :feature:hyprland:
+Extend auto-dim with an explicit “dim everything” setting for bright
+non-dark-mode contexts, with a security/usability review of its scope.
+
+** TODO [#C] Gallery probe: the fader-drag check is flaky :bug:test:design:
+=probe.mjs= check 3 ("fader drag tracks at 3x") intermittently reports =level 68 -> level 68=, i.e. the synthetic drag never registers. It has presumably been doing this all along unnoticed, since the suite is normally run once per batch.
+
+Grading: *Minor* severity (a false FAIL costs a re-run and a few minutes, and never ships a defect) x *most users, frequently* = P3 = =[#C]=.
+
+Frequency measured 2026-07-16, not estimated: 1 failure in 6 consecutive runs, having already fired twice in about fifteen that afternoon. The first grading guessed "some users, sometimes" (~1 in 10); at ~1 in 6, both people who run this suite hit it most sessions, so the row is "most users, frequently". The letter lands on =[#C]= either way, but the input was wrong and the matrix is only worth anything if its inputs are measured.
+
+Suspected cause: the check clicks the 3x size chip, calls =scrollIntoView=, waits a fixed 200ms, then reads =getBoundingClientRect= and dispatches the drag against those coordinates. If the zoom relayout or the smooth scroll hasn't settled, the rect is stale and the press lands off the fader — so the drag is a no-op and the readout never moves. The other timing-sensitive checks share the same fixed-sleep shape.
+
+*Do not fix this by raising the sleep.* That hides the race rather than removing it and leaves the check failing again on a slower run. Wait on the actual condition instead: poll until the rect stops changing between frames, or assert the press landed on the fader before dispatching the drag (the probes' own README already warns that a =find()= miss dispatches into nothing and reports as a widget bug).
+
+Why it matters beyond the annoyance: a gate that cries wolf gets its real failures ignored, and this suite is the only thing standing between the gallery and a silent regression.
+
+Recurrences: 2026-07-18 batch-6 gate (first run, passed 3 reruns); 2026-07-18 batch-9 gate (first cold run, =level 68 -> level 68=, passed 2 reruns). Both were the session's first probe run — consistent with the stale-rect theory (cold-start relayout settling slower than the fixed 200ms sleep).
+
+** TODO [#C] Maint live-refresh hairline replacement :feature:maint:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-14
+:END:
+From the roam inbox (routed 2026-07-13): the memory-killer section seemed to update too often, and "it's a bit unclear what the line is doing; consider something else." Diagnosis (2026-07-14): the data cadence is already the requested 3s (gui live tier, _LIVE_SECONDS); the perceived churn is the live-refresh hairline — the 2px bar under the live sections that drains full-to-empty over each 3s window, redrawn at 150ms (gui._hair_tick, viewmodel.refresh_fraction). It exists to tell a stale board from a frozen one (2026-07-09), but it reads as constant unexplained motion. Design call for Craig: replace the draining line with something whose meaning is legible — candidates: a dot that blinks once per refresh, a "3s" age caption that only appears when refresh is overdue, slowing the drain redraw, or dropping the indicator on live tiers and keeping it only when data goes stale. Keep the stale-vs-frozen distinguishability that motivated the hairline.
+
+** TODO [#C] Net panel speedtest history :feature:dotfiles:network:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-14
+:END:
+From the roam inbox (routed 2026-07-13): the networking panel should track speedtests over time with appropriate info. Shape: persist each SPEED TEST result (timestamp, down/up, latency, server) to a small local store and surface history in the net panel. Design questions for work time: retention window, which fields matter, and presentation within the panel's ~400px width (recent-results list vs trend readout). Point-in-time results exist today; the gap is comparison across days and venues.
+
+** TODO [#C] zfs base VM image build failure: ZFS DKMS module missing :bug:zfs:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-09
+:END:
+=FS_PROFILE=zfs make test-vm-base= fails inside the VM at initramfs time: archangel reports "ZFS module not found! DKMS build may have failed" against the installed kernel (linux-lts 6.18.38 at the 2026-07-08 attempt). Consequences: the maint scenario harness's zfs lane (Phase 12) is filtered but unexercised, and a real zfs bare-metal install via archangel would plausibly hit the same wall. Priority per the bug matrix: Major severity (zfs install path broken) × some-users-sometimes = P3. When fixed, run =FS_PROFILE=zfs bash scripts/testing/run-maint-scenarios.sh --list= and add zfs scenario files (zpool scrub / autotrim / snapshot destroy) to the harness.
+*** 2026-07-14 Tue @ 01:40:48 -0500 Diagnosed: OpenZFS/kernel version skew, blocked on archzfs
+Reproduced in ~1 minute of install: =dkms install zfs/2.3.3 -k 6.18.38-2-lts= exits 1 during pacstrap. Root cause confirmed: OpenZFS 2.3.3's META declares Linux-Maximum 6.15, and the VM installs linux-lts 6.18.38. The first release supporting 6.18 is 2.4.0 (2.4.1 covers 6.19), and archzfs currently serves only zfs-dkms 2.3.3-1 — nothing on our side to fix. Unblock condition: archzfs publishes zfs-dkms ≥2.4.0; recheck with =curl -s https://archzfs.com/archzfs/x86_64/ | grep -o 'zfs-dkms-[0-9.]*'=, then rerun =FS_PROFILE=zfs make test-vm-base=.
+
+** TODO [#C] Dotfiles stow conflicts: first-launch risk + restow directory handling :bug:dotfiles:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-14
+:END:
+*** 2026-07-14 Tue @ 00:51:51 -0500 Ratio calibre check passed; waypaper canonical decided (dark-lion)
+Ratio's ~/.config/calibre is a directory symlink into the dotfiles repo (stow folded the whole dir), so the first-launch gap never existed there — check closed. Craig decided dark-lion.jpg is the canonical waypaper wallpaper; the repo config.ini updated from the that-one-up-there.jpg placeholder (the file is skip-worktree volatile, unskipped for the commit and re-flagged). Remaining: when velox is back online, run make conflicts / make reset there so its old conflict copy clears against the updated repo.
+*** 2026-07-02 Thu @ 17:30:00 -0400 Shipped the Makefile hardening + first-launch guard (dotfiles 42a82d2)
+The solo-able subset landed in the speedrun. =make conflicts <de>= is the loud first-launch guard: dry-runs all tiers, parses all four stow error shapes (plain file conflict, foreign symlink, dir-over-file, and restow's unstow_contents non-directory ERROR), lists each blocker with a directory/foreign-symlink marker, exits 1 when any exist. =make reset= now pre-clears the directory and foreign-symlink blockers =--adopt= aborts atomically on (removals printed; repo version wins per the target's contract), then adopts + git-checkouts as before. =make restow='s overwrite path switched rm -f → rm -rf so directory conflicts clear. 8 sandbox tests drive the real Makefile against a throwaway HOME (44 suites green). Also verified on velox: the whereami and mpd-playlists conflicts noted in this task were already hand-converted 2026-06-29 — =make conflicts hyprland= reports clean live. REMAINING (deferred per Craig's speedrun pre-flight): the waypaper canonical decision (live velox dark-lion.jpg vs repo that-one-up-there.jpg) and the ratio calibre-symlink check (ratio paused).
+From the velox calibre incident (2026-06-27, note in ~/.dotfiles/inbox/processed/): calibre was launched before =make stow= ran, wrote its own default config into =~/.config/calibre/=, and silently blocked its own stow — it ran on factory defaults while the rest of common/ stowed fine. General pattern: any GUI app that auto-creates config on first run, launched before stow, blocks its own stow the same way. Velox was repaired by hand (=ln -srf= symlinks byte-identical to =stow --no-folding= output).
+
+Remaining work (re-graded C 2026-07-02 — the first-launch risk and the Makefile handling shipped in the speedrun; what's left is a decision and a paused-machine check):
+- Waypaper canonical decision (Craig): live velox has =dark-lion.jpg=, repo has =that-one-up-there.jpg= (placeholder?). Decide the canonical, then =make reset= clears it.
+- Ratio check: whether =~/.config/calibre/*.json= on ratio are symlinks or real files — same first-launch gap likely if calibre ever launched there before stow. On the ratio trip list.
+
+** TODO [#C] Waybar collapse control: replace the triangle glyph :feature:waybar:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-14
+:END:
+From the 2026-07-04 roam capture. The waybar collapse mechanism (click the triangle, the bar sections redisplay shortened) works, but the triangle glyph doesn't match the instrument-console aesthetic the panels now use. Replace it with something in keeping with the console look. Aesthetic decision — bring Craig two or three concrete glyph/style options (a machined chevron, a console-key style expander, an engraved caret) before wiring. Dotfiles waybar config (handled per the archsetup-owns-dotfiles rule). Raised alongside the net-panel/audio speedrun; deferred from it because the glyph choice is a taste call.
+
+** TODO [#C] Net panel: driver-health diagnostic tier :feature:network:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-14
+:END:
+Follow-up from the 2026-07-04 net-panel hardening speedrun (Craig's cj question on the no-WiFi item). The shipped no-wifi-hardware verdict covers "no adapter at all." This tier covers "adapter present but the driver is wedged": read-only health signals — =ip link= (device present but no-carrier / down), =dmesg= / =journalctl -k= for firmware-load failures, =rfkill= for a hard block, =modinfo= / =lsmod= for the driver module — classified before a generic reset. Remedy actions: a privileged =modprobe -r <mod> && modprobe <mod>= reload of the wifi driver, and a firmware-package pointer when the failure is a missing/failed firmware load. Dotfiles net-package work (handled per the archsetup-owns-dotfiles rule). Design pass first to decide whether it's worth a repair tier vs a needs-user-action pointer.
+
+** TODO [#C] Voice dictation / speech-to-text input :feature:tooling:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-06
+:END:
+Push-to-talk dictation that types transcribed speech into the focused Wayland window — usable at any text field, including the Claude Code terminal prompt and Emacs buffers. Claude Code has no built-in voice input; dictation has to happen at the OS level and inject text. Raised 2026-07-03.
+
+Tool choice is the open decision (needs Craig): =nerd-dictation= (Vosk, lighter, lower accuracy) vs a =whisper.cpp=-based daemon (heavier, higher accuracy, optional GPU). Wayland typing backend is =wtype= or =ydotool=. Scope once chosen: install + model download, a push-to-talk keybind (Hyprland), and an autostart entry; fold into archsetup so it lands on both daily drivers. Consider an Emacs-native path (=whisper.el=) as a complement for in-buffer dictation.
+
+** TODO [#C] Fix install errors surfaced by the 2026-05-11 VM test run
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-06
+:END:
+*** 2026-06-28 Sun @ 13:29:29 -0400 Audit reconcile: 2026-06-28 btrfs+zfs runs reproduce the same residual set
+Newer full runs landed since the 2026-06-11 reconcile below: the 2026-06-25 zfs run (Testinfra 96/0) and the 2026-06-28 btrfs+zfs runs (97/0, "zero attributed issues"). The residual four were NOT fixed and reproduce unchanged: =enabling firewall= (archsetup:1496-1498, carries a VM-kernel note), =enabling gamemode for user= (archsetup:2221, non-critical), and =tidaler (AUR)=. Zero archsetup-attributed Testinfra issues across both profiles confirms these are environment / non-critical, not archsetup bugs. Bare-metal confirmation of the firewall pair is still the open thread.
+
+*** 2026-06-15 Mon @ 23:53:21 -0500 Audit reconcile: latest VM run (2026-06-11) confirms the surviving error set
+The most recent VM run (=test-results/20260611-113904/=) carries four error-summary entries: =enabling firewall= + =verifying firewall is active= (the iptables/nf_tables "Could not fetch rule set generation id" pair, still unconfirmed on bare metal), =enabling gamemode for user= (non-critical), and =tidaler (AUR)=. The earlier fontconfig/dconf fixes held — none reappear. So the count is down from the 7→6 anchor below to four, all of them the known-residual items already itemized.
+Errors logged during the VM install. Status as of the 2026-05-11 18:36 run (=test-results/20260511-183643/archsetup-output.log=) after the =48c9439= fontconfig/dconf fix: 7 → 6.
+- refreshing font cache — RESOLVED in =48c9439= (now installs =fontconfig= before calling =fc-cache=).
+- configuring GTK file chooser — RESOLVED in =ecab29f= (switched to a system-wide dconf db at =/etc/dconf/db/site.d/=; needs no session bus during install).
+- configuring GNOME interface settings in dconf — RESOLVED in =ecab29f= (same fix as the GTK file chooser above).
+- enabling firewall — exit 1: =iptables v1.8.13 (nf_tables): Could not fetch rule set generation id: Invalid argument=. Still present in the 18:36 run; likely a VM-kernel/nf_tables artifact — confirm on bare metal before treating as an archsetup bug.
+- verifying firewall is active — exit 1 (follow-on from the firewall-enable error).
+- enabling gamemode for user — exit 1 → step "gaming" FAILED — non-critical.
+- tidaler (AUR) — logged in the error summary with exit code 0 (odd; logging quirk or transient AUR build noise?).
+Also seen in the 18:36 run's log-diff (post-install systemd noise, probably VM-environment): =pam_systemd … CreateSession failed= / =logind: Failed to start session scope … Permission denied=, and =Failed to start Proton VPN Daemon= (no VPN config in the test VM).
+
+*** 2026-05-19 Tue @ 13:18:56 -0500 Fixed AUR exit-0 logging bug at the root
+Root cause was in =retry_install=: =last_exit_code=$?= ran AFTER =if eval ...; then return 0; fi=. Bash defines an if-compound's exit status as zero when no condition tested true, so a failing eval's exit code got overwritten with 0 before reaching =error_warn=. Fix in =8221c54=: capture =$?= from =eval= directly into a local var, then compare against the captured value in the if. VM-verified in =test-results/20260519-115318/=: =mkinitcpio-firmware (AUR)= and =tidaler (AUR)= now report =error code: 1= (yay's actual exit) instead of the misleading =error code: 0=. The same packages still appear in the summary because yay returns non-zero when sub-deps fail to build (e.g. =aic94xx-firmware=), but the codes are accurate now. If the underlying sub-dep failures stay noisy, that's a separate concern — open a new task.
+
+*** 2026-05-16 Sat @ 09:00:41 -0500 AI Response: Surfaced the expanded AUR-exit-0 pattern
+2026-05-16 07:40 VM run passed (52/0/5) with the same warning profile as the 2026-05-11 18:36 run. Error count went 7 → 13: 5 fixed/unchanged, +5 new AUR-exit-0 entries (broadens the existing tidaler item into the dedicated =[#B]= subtask above), +1 genuinely new error in =setting up emacs configuration files= (=git pull= ran in =~/.emacs.d= which existed from stow but had no =.git=). Patched =archsetup:1932-1945= with a three-branch check: clone if missing/empty, pull if =.git= exists, =git init=/=fetch=/=checkout= in place if the dir came from stow.
+
+*** 2026-05-19 Tue @ 01:25:26 -0500 Verified the b9907c7 emacs-stow fix end-to-end
+=make test= 21:44 → 22:29 (42 min), =test-results/20260518-214516/=. 52/0/5, =ArchSetup Exit Code: 0=. The third-branch path fired correctly — install log =archsetup-2026-05-18-21-45-46.log:14358-14365= shows =From https://git.cjennings.net/dotemacs= → =[new branch] main -> origin/main= → =Reset branch 'main'= → =branch 'main' set up to track 'origin/main'=. No exit-128, no =fatal: not a git repository=. Error Summary down to 7 (was 13 on 2026-05-16); the emacs entry is gone. AUR exit-0 logging triggered for 2 packages this run (mkinitcpio-firmware, tidaler) vs 6 on 2026-05-16 — same bug class, fewer triggers, still tracked under =[#B] AUR exit-0 logged as error=. Issue Attribution: 1 ARCHSETUP entry (Proton VPN Daemon failed — known VM-no-VPN-config artifact). Cleanup ran clean via the normal path.
+
+** TODO [#C] Re-check python-lyricsgenius --skipinteg workaround :chore:solo:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-09
+:END:
+archsetup installs =python-lyricsgenius= with =--mflags --skipinteg=, skipping makepkg integrity + PGP checks — a workaround originally for an expired-signature issue upstream (surfaced by the 2026-06-23 --noconfirm audit). Periodically test whether the cause has cleared: if a plain =aur_install python-lyricsgenius= builds without complaint, drop the =--skipinteg= workaround. Removal needs a real AUR build to confirm, so it isn't a blind change.
+
+*** 2026-07-14 Tue @ 01:40:48 -0500 Rechecked: still needed, same cause
+Fresh AUR clone, =makepkg --verifysource= on 3.7.0-1 (PKGBUILD unchanged): tarball passes, =LICENSE.txt= still FAILS its b2sum — the github-master pin, structural as diagnosed 2026-06-24. =--skipinteg= stays.
+
+*** 2026-07-02 Thu @ 05:08:37 -0400 Rechecked: still needed, same cause
+=makepkg --verifysource= on 3.7.0-1: tarball passes, =LICENSE.txt= still FAILS
+its b2sum — the PKGBUILD still pins a hash for a file fetched from github
+master. Structural, as diagnosed 2026-06-24; =--skipinteg= stays.
+
+*** 2026-06-24 Wed @ 17:55:34 -0400 Rechecked: still needed, but the cause changed
+Ran =makepkg --verifysource= on the current AUR PKGBUILD (3.7.0-1). The package tarball =lyricsgenius-3.7.0.tar.gz= now passes its b2sum — the original expired-PGP-signature problem is gone (the PKGBUILD no longer carries any =validpgpkeys=). But integrity still FAILS, on a different file: =LICENSE.txt=, which the PKGBUILD fetches from the project's github master and pins a b2sum for. github master is a moving target, so that b2sum drifts and =--skipinteg= is still required. This is structural (not a transient upstream fix away), so it likely won't clear until the maintainer pins the LICENSE to a tagged release. Updated the archsetup comment to the real cause. Keep rechecking, but lower expectations of it clearing.
+
+** TODO [#C] Review theme config architecture for dunst/fuzzel
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-06
+:END:
+The active dunst config is stowed from dotfiles/common/ but theme templates
+live in dotfiles/hyprland/.config/themes/. set-theme copies the templates to
+the stowed locations at runtime, so edits to the common file get overwritten
+on theme switch. This split between stowed configs and theme templates is
+error-prone — changes must be made in both places. Consider:
+- Having set-theme be the single source of truth (remove common dunstrc from stow)
+- Or symlinking the stowed config to a theme-managed location
+- Same situation applies to fuzzel.ini
+The goal is a single place to edit each config, not two.
+
+** TODO [#C] Review current tool pain points annually
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-06
+:END:
+Once-yearly systematic inventory of known deficiencies and friction points in current toolset
+
+** TODO [#D] Installer resume-idempotency cluster :bug:solo:
+Grading: Minor severity x rare edge case (re-run after a mid-step failure) = P4 = [#D]. Group of small non-idempotent / wrong-target spots.
+crontab log-cleanup line duplicates on resume (archsetup:1713 — guard on absence); zfs scrub timer picks an arbitrary pool via =head -1= and yields =@.timer= when empty (archsetup:1857); gamemode enabled via =systemctl --user= which the script itself documents fails at install time (archsetup:2419 — use the manual wants-symlink like syncthing). See findings doc (S2, S3).
+** TODO [#D] Installer unguarded chmod/cp after non-fatal ops :bug:solo:quick:
+Grading: Minor severity x rare edge case (only when a preceding non-fatal cp/clone failed) = P4 = [#D].
+With set -e off, unguarded chmod/cp hit missing/partial files silently: hypr-live-update-guard chmods (archsetup:2108/2144), zfs-replicate cp (archsetup:1820) leaving a service with a dead ExecStart, zfs-pre-snapshot cp (archsetup:1943) leaving a broken pacman hook. Fix: wrap each in =(...) >> log 2>&1 || error_warn=. See findings doc (S2, S3).
+** TODO [#D] normalize-notify-sounds temp/atomicity can corrupt tracked file :bug:solo:quick:
+Grading: Minor severity (corrupts a repo-tracked sound file, recoverable via git) x rare (ffmpeg failure/interrupt) = P4 = [#D].
+scripts/normalize-notify-sounds.sh:39-46 has no EXIT trap on the mktemp and does =cat "$tmp" > "$f"= (truncate-first) where $f is a stow symlink into the repo; a zero-byte/failed encode writes a corrupt file. Fix: EXIT trap; =[ -s "$tmp" ]= guard; write $f.tmp and overwrite on success. See findings doc (S4).
+** TODO [#D] Installer + scripts refactor opportunities :refactor:
+Grading: no behavior change; parking lot. 12 refactors from the sentry audit — duplicated GPU-modalias scan, triple hand-rolled retry loop, stow x4, display_server/window_manager dispatch dup, Maia ELO range x3, per-script log helpers, GRUB/snapper/fsck sed clusters, waybar-battery positional sed. Full list with line numbers in [[file:docs/design/2026-07-19-sentry-code-findings.org][sentry code findings]] (High/Medium/Low tagged). Pull individual ones out as their own tasks when tackled.
+** TODO [#D] Net doctor vNext :feature:dotfiles:network:
+Deferred from the [[file:docs/specs/2026-07-11-net-doctor-expansion-spec.org][net doctor expansion spec]] (IMPLEMENTED, v1 shipped): event-log correlation for the flaky/drops cluster (powersave, roaming stalls, USB autosuspend, no-reconnect-after-resume, firmware crashloop drop signatures — needs history a one-shot probe can't see); a DoT/DNSSEC-specific verdict distinguishing "the venue resolver mangles DNSSEC" from the generic DNS-not-resolving; per-profile autoconnect/duplicate-profile hygiene.
+
+** TODO [#D] Bt doctor vNext :feature:dotfiles:bluetooth:
+Deferred from the [[file:docs/specs/2026-07-11-bt-doctor-expansion-spec.org][bt doctor expansion spec]] (IMPLEMENTED, v1 shipped): the stale-bond re-pair-offer signature (v1 keeps re-pair strictly user-initiated; the signature must be designed and validated before the doctor ever offers it); a connection-parameter/coexistence hint tail for a "keeps dropping" verdict; the bt-audio-profile expansion beyond the current a2dp repair (codec fallback, default-sink, absolute-volume) — wants coordination with the audio doctor so the two panels don't claim the same A2DP/HFP diagnosis with divergent verdicts.
+
+** TODO [#D] Widget catalogue vNext :feature:design:
+Deferred from the [[file:docs/specs/2026-07-12-component-generation-spec.org][component-generation spec]] (DRAFT): framework wrappers for the web library (React or otherwise — demand-gated, none until a real framework consumer exists); Level-2/3 widget codegen if the spec's Phase 5 decision point says go (a go spawns its own spec); ports beyond the demand matrix as new consumers appear.
+
+** TODO [#D] Maintenance console vNext :feature:
+Deferred from the [[file:docs/specs/2026-07-07-maintenance-console-spec.org][spec]] (v1 ships determinate remedies only): AI/workflow assistance on read-only metrics (the vLater tier — failed-unit diagnosis, journal-error fix suggestions, OOM investigation); SIGKILL escalation for TERM-survivors on the KILL lever; live streaming meters on evidence rows where a count could be a level.
+
+** TODO [#D] Retention-repair remedy unreachable from the panel GUI :bug:dotfiles:
+snapshot_retention_repair (WRITE SANE LIMITS — =snapper -c <config> set-config TIMELINE_CREATE=yes TIMELINE_LIMIT_*=<TOML values>=) sits in the maint remedy table but is wired into no GUI layer (no reference in panel.py, viewmodel.py, or gui.py) — it's reachable only via =maint doctor review= / =maint fix=. Worse, the REVIEW & FIX roster's wording for item-bearing remedies says "per-item — on its subpanel", which for this remedy points at a key that doesn't exist. Found 2026-07-08 while walking the SNAPSHOTS subpanel with Craig.
+
+Two fix shapes, decide at work time: (1) wire a digest key — e.g. on the timeline row when the config's installed TIMELINE_LIMIT_* drift from the TOML values (needs the probe to read the installed limits for comparison); or (2) keep it CLI-only deliberately and special-case the roster wording for remedies with no panel key ("CLI only — maint fix"). Option 2 is a few lines; option 1 makes limit drift visible on the board, which is the 2026-05-26 pile-up's root cause. Severity minor × rare edge = P4 per the bug matrix.
+
+** TODO [#D] Consider Customizing Hyprland Animations
+Current: windows pop in, scratchpads slide from bottom.
+
+Customizable animations:
+- windows / windowsOut / windowsMove - window open/close/move
+- fade - opacity changes
+- border / borderangle - border color and gradient angle
+- workspaces - workspace switching
+- specialWorkspace - scratchpads (currently slidevert)
+- layers - waybar, notifications, etc.
+
+Styles: slide, slidevert, popin X%, fade
+Parameters: animation = NAME, ON/OFF, SPEED, BEZIER, STYLE
+Speed: lower = faster (1-10 typical)
+
+Example tweaks:
+#+begin_src conf
+animation = windows, 1, 2, myBezier, popin 80%
+animation = workspaces, 1, 4, default, slide
+animation = fade, 1, 2, default
+animation = layers, 1, 2, default, fade
+#+end_src
+
+** TODO [#D] Parse and improve AUR error reporting
+Parse yay errors and provide specific, actionable fixes instead of generic error messages
+
+** TODO [#D] Improve progress indicators throughout install
+Enhance existing indicators to show what's happening in real-time
+
** TODO [#D] Telega coredump recurrence tell :bug:maint:
:PROPERTIES:
:LAST_REVIEWED: 2026-07-09