diff options
Diffstat (limited to 'todo.org')
| -rw-r--r-- | todo.org | 3244 |
1 files changed, 1544 insertions, 1700 deletions
@@ -45,6 +45,208 @@ below): input-side-spec.org (DRAFT, four decisions open). * Archsetup Open Work +** TODO [#B] Speedtest button cancels an in-flight run :feature:dotfiles:network: +:PROPERTIES: +:CREATED: [2026-09-01 Tue] +:LAST_REVIEWED: 2026-09-01 +:END: + +From the roam inbox (routed 2026-09-01), Craig's words: "pressing the +speedtest button on network admin panel when speedtest is already running +should cancel the speedtest. However, we should leave any numbers on the +display as if the speedtest completed successfully." + +Net panel work lives in ~/.dotfiles; archsetup owns it end-to-end per the +standing rule in notes.org. Distinct from the [#C] speedtest-history task +(that one persists results over time; this one is in-flight cancel +semantics). Behavior is fully specified: second press kills the running +test, display keeps whatever numbers are already shown as a completed +result. [#B]: real improvement to the active panel family, no hard date. + +** TODO [#B] gcalcli in the installer, token carried from the other daily driver :feature:velox:tooling:solo: +:PROPERTIES: +:CREATED: [2026-08-25 Tue] +:LAST_REVIEWED: 2026-08-25 +:END: +From home's 2026-08-25 handoff: velox's 08-13 reinstall left it without +gcalcli, and on 08-21 both calendar write paths on velox were down at once +(the google-calendar MCP with expired tokens, and no gcalcli), so a booked lab +appointment sat uncalendared for three days. The daily-drivers one-time-setup +drift, exactly. + +Part 1 is done (2026-08-25, this session): =pipx install gcalcli==4.5.1= on +velox to match ratio, then ratio's =~/.local/share/gcalcli/{oauth,cache}= +copied over tailscale (=oauth= is a 1 KB pickled google-auth credential, +=chmod 600=). =gcalcli list= on velox returned all six calendars with no +re-consent, so the token is portable between the daily drivers and the OAuth +click-through is not needed when the other machine is reachable. + +Part 2, this task: make the installer do it. +- =pip_install gcalcli= in the tool set beside =pip_install yt-dlp= (archsetup + ~line 3109; =pip_install= wraps =pipx install= as =$username=). Pin or not: + ratio and velox are both 4.5.1; unpinned matches how yt-dlp is installed. +- The credential can't be installed: add a named post-install manual step + "copy =~/.local/share/gcalcli/oauth= from the other daily driver + (=scp <other>:.local/share/gcalcli/oauth ~/.local/share/gcalcli/=, + =chmod 600=), or run =gcalcli init= per + =assets/2026-02-01-gcalcli-setup.org= when neither machine has it." +- A =post-rebuild-check= item: gcalcli on PATH and the oauth file present, so + the drift is caught by the checker rather than by a missed appointment. +- Tests: an installer-steps pytest asserting the tool set carries + =pip_install gcalcli=; a post-rebuild-check test for the new item, both + states. +- When it lands, confirm back to home (=inbox-send home=) so it can retire + its "gcalcli is not installed on velox" notes. + +Grading: feature, no hard date, real improvement to the install = [#B]. +:solo: — build path (installer + checker + tests) and verify path (pytest; +the live proof already exists on velox) with no open decision. + +** TODO [#A] Topgrade guarded-upgrade spec — decisions, review, decomposition :feature:maint:dotfiles: +SCHEDULED: <2026-08-25 Tue> +:PROPERTIES: +:CREATED: [2026-08-25 Tue] +:LAST_REVIEWED: 2026-08-25 +:SPEC_ID: 81cdfd72-db96-43d3-aa03-779878c99f3e +:END: +The waybar maint module's "topgrade freshness" warning never clears: the stamp +is written only when topgrade exits 0, and the =hypr-live-update-guard= +PreTransaction hook (mesa, wayland, hyprland, vulkan-*, nvidia-utils, +xorg-xwayland under a live Hyprland) plus any failing ecosystem step makes that +exit almost unreachable. Diagnosed 2026-08-24/25; the fix is specced, not +hacked, because it spans two repos and the design is contested. + +Spec: [[file:docs/specs/2026-08-25-topgrade-guarded-upgrade-spec.org][2026-08-25-topgrade-guarded-upgrade-spec.org]] (DRAFT). + +Four open decisions, all mine to make before the spec can move: +1. Freshness means *state* (a guarded upgrade still un-applied stays stale), + not recency (any run stamps). +2. Primary mechanism is alternative D: an armed boot-time oneshot ordered + before =getty@tty1= (no display manager to order against). +3. The boot run is arch-only (=topgrade --only system=), not the full sweep. +4. The arm flag lives on a persistent path and is one-shot. + +Then: flip the decisions DONE, run spec-review (DRAFT → READY), run +spec-response to decompose the four phases into build tasks here, file the +vNext =[#D]= kernel-reboot item, and commit the spec. + +** TODO [#A] Lock-screen clock stale after a real sleep :bug:hyprland:dotfiles:velox: +SCHEDULED: <2026-08-25 Tue> +:PROPERTIES: +:CREATED: [2026-08-25 Tue] +:LAST_REVIEWED: 2026-08-25 +:END: +After waking velox from a real sleep, the hyprlock clock shows a stale time +(Craig confirmed 2026-08-24: the wake-from-sleep case, not an idle-locked +screen). Three isolated tests on 2026-08-24 failed to reproduce it — hyprlock +0.9.6 repainted within a second of a display power-cycle, a three-minute +SIGSTOP, and both together with the screenshot background — so it needs a real +suspend on the real hardware. + +Grading: Minor severity (cosmetic-to-confusing, the screen still unlocks) × +most users frequently (every wake) = P3 = [#C] by the matrix; held at [#A] at +Craig's direction on 2026-08-25 so it gets run while velox is the daily driver +on the road. Revisit the letter once the manual check has an answer. + +Not :solo: — the distinguishing observation is Craig's. The check lives under +Manual testing and validation: "Lock screen after a real sleep: is the clock +frozen, or is all of hyprlock frozen?". Its three outcomes each name a +different fix: stale-then-corrects → repaint interval; frozen with live input +→ clock rendering; frozen with dead input → hyprlock hung, a crash/hang +recovery bug the =screen-lock= watchdog doesn't cover. + +** TODO [#C] post-rebuild-check: probe that Emacs frames come up Wayland-native :feature:emacs:velox:solo:quick: +:PROPERTIES: +:CREATED: [2026-08-25 Tue] +:LAST_REVIEWED: 2026-08-25 +:END: +On 2026-08-24, the first Emacs 31.1 start on velox opened its first frame on +XWayland (=:0=) with the pgtk "unsupported under X" dialog, while every later +frame went to =wayland-1=. The cause was in the Emacs config (a startup buffer +sweep killed =*Warnings*= while 31.1's warnings.el held it for a deferred +display, so the first =make-frame= failed and emacsclient fell back to +=$DISPLAY=); fixed in =.emacs.d= the same night. The trap generalizes: any +first-frame error on a PGTK daemon silently lands the session on X, and nothing +in the post-rebuild pass would notice. + +Design, under the script's fail-closed contract (a probe that cannot run +reports a finding, never a pass): +- Bound every =emacsclient= call with =timeout=, as the =systemctl= calls are. + A daemon stuck in a prompt is a finding, not a hang. +- No daemon (=emacsclient= cannot connect): a visible finding, "not checked: + no Emacs daemon, start Emacs and re-run". Emacs is started on demand here, + so this is the common state right after a rebuild, and the line is the point. +- Daemon up but no GUI frame yet: never skip, and never call + =pgtk-backend-display-class= with no frame (it errors with "Frames are not + in use"). Request one invisible frame through a waiting client in the + background, =timeout 20 emacsclient -c -F '((visibility . nil) (name . + "prc-probe"))'=, so the probe walks the same first-frame path that failed on + 2026-08-24. Then wait, bounded (poll for a frame named =prc-probe= for up to + the same 20 s), before inspecting: the =-e= must not run before the =-c= + has connected. Zero pgtk frames after the request is a finding in its own + right, because a frame request that produced nothing is the first-frame + failure this check hunts. Delete the probe frame after reading. +- Inspect every pgtk frame, not the selected display, and only pgtk frames: + a tty client frame (=emacsclient -t= in tmux) carries =$DISPLAY= as its + display parameter and its terminal is not a display, so it would both trip + the =:0= rule and make =pgtk-backend-display-class= error. + #+begin_src sh + emacsclient -e '(mapcar (lambda (f) (list (frame-parameter f (quote display)) (pgtk-backend-display-class (frame-terminal f)))) (seq-filter (lambda (f) (eq (framep f) (quote pgtk))) (frame-list)))' + #+end_src + Expected: at least one entry, every display equal to =$WAYLAND_DISPLAY=, + every class =GdkWaylandDisplay=. An empty list, a =:0= entry, or a + =GdkX11Display= is a finding. A build without =pgtk-backend-display-class= + is a finding too: the installer installs =emacs-wayland=. +- Limit, stated in the check's output: it sees live frames only. A first X + frame that was already closed is invisible, so this reports the machine's + current state, not its history. The invisible probe frame is created and + never mapped, so it exercises =make-frame= (where 2026-08-24 failed), not + the window-show path. + +Tests alongside the other checks, one per state: no daemon, no frame (probe +frame requested and waited for), probe frame never appears, Wayland-only, a +=:0= frame present, a tty client frame present alongside Wayland frames, hung +daemon, an =*ERROR*= reply from =emacsclient -e= (exit 1, a finding), non-pgtk +build. + +** TODO [#B] Timeline spine test picks the wrong "next" event off Denver :bug:dotfiles:test: +:PROPERTIES: +:CREATED: [2026-08-24 Mon] +:LAST_REVIEWED: 2026-08-24 +:END: +=make test= in dotfiles is red before any of this session's work. Two failures, +both in =settings/faces/timeline-face-spine.test.mjs=: "event bars never leave +the plot" and "exactly one event is marked as next, and it is the soonest ahead". + +NOT the bug =c96a216= fixed. Every =spineRows= call in that file is pinned to +=JUL=, and =scene()= and =EVENTS()= both default to it, so the fixture side is +already clean and the file's own guard test passes. + +TWO THINGS TO SETTLE, and they may be one bug or two: + +1. =timeline-face-spine.js:466= — =const next = timedOnly(events).find((e) => e.s + >= refMs)= takes the first array element starting at or after now, which is + the *soonest* only if =events= is sorted by start time. The test's failure + message is exactly that it is not: a bar ahead of the spine starts at x=1651.2 + while the one marked =event-next= sits at x=2132.8. Either sort before the + find, or use a min-by rather than a find. + +2. Why it is red *here* and presumably green on ratio. The most recent commit to + =timeline-face-spine.js= is =ffe43ab feat(settings): draw home where the + machine is, not where its zone is=. This machine is =America/Denver= (Craig + travelling); the tests pass =home("New Orleans")= explicitly. If a + machine-resolved home overrides the explicit argument, the geometry drifts + and the test is machine-dependent — which makes it useless as a gate, since it + would only ever fail on the machine nobody runs it on. Confirm by running the + faces suite with =TZ=America/Chicago= and again with =TZ=America/Denver=. + +If item 2 confirms, the design question is whether machine-resolved home belongs +in the pure geometry layer at all, or whether the host should resolve it and pass +it in — which is what the test already assumes. + +Not blocking the Lua port: =make test-faces= is disjoint from the hypr config and +the three suites that work touches. + ** TODO [#B] Qt apps render oversized on velox :bug:velox:solo: :PROPERTIES: :CREATED: [2026-08-19 Wed] @@ -101,26 +303,6 @@ The cursor entry in the same file records this identical failure a third time: compositor's scale." Three instances of one mistake in one file, two previously fixed in isolation without anyone naming the pattern. -** TODO [#B] Function keys issue media actions instead of F-keys :bug:velox: -:PROPERTIES: -:CREATED: [2026-08-19 Wed] -:LAST_REVIEWED: 2026-08-19 -:END: - -From the roam inbox, Craig's words: "function keys should issue F+number -functionality rather than their media functionality when the button is hit. -currently it's reversed and I have to hit function and the f button for F+number -functionality." - -Check first whether this belongs to archsetup at all. On a Framework the Fn-lock -is a firmware-level toggle held in the keyboard itself (Fn+Esc on most -revisions), not something the OS sets, in which case this is one keystroke -rather than a change here. If it is instead a hid/keyboard-module quirk, it is -ours. - -Grading: Minor severity (the keys work, they are on the wrong layer, and there -is a workaround) x every user every time (every F-key press) = P2 = [#B]. - ** TODO [#C] Waybar panels launch expanded instead of collapsed :bug:dotfiles:waybar: :PROPERTIES: :CREATED: [2026-08-19 Wed] @@ -178,97 +360,48 @@ Grading: Minor severity (nothing behaves wrong, it just waits) x some users sometimes (every failed lookup, which is occasional rather than constant) = P3 = [#C]. -** TODO [#A] Reseat velox input-cover ribbon — phantom power button :bug:velox:hardware: -DEADLINE: <2026-08-14 Fri> -:PROPERTIES: -:CREATED: [2026-08-13 Thu] -:LAST_REVIEWED: 2026-08-13 -:END: -Machine off, lift the input cover (Framework QR-guided procedure, 5 -fasteners), reseat its ribbon connector to the mainboard — disturbed in the -2026-08-13 board swap. Root cause of every "mystery reboot" that day: -chassis flex (flash-drive touch, ethernet bump, lid partially lowered) -fired phantom power-button presses — journalctl -b -1 showed "Power key -pressed short." → orderly logind poweroff, then the glitching button -powered it back on. While in there, reseat the USB expansion cards too — -the flaky slot (two hard resets, one no-enumeration) is likely the same -flex problem. -THIRD SYMPTOM (2026-08-13 evening): touchpad delivers ZERO input events — -15s synchronized libinput debug-events capture while swiping caught -nothing, though i2c enumeration and a driver rebind handshake are clean. -Signature of a dead interrupt line on the same ribbon. Keyboard + power -LED lines work; BT mouse is the interim pointer. -ESCALATED 2026-08-13 21:00: a fourth event killed the machine THROUGH the -shield. Previous boot's journal ends mid-line (tailscaled chatter) with no -shutdown sequence at all — a hard power cut, not logind acting. So the -glitch now reaches the EC/hardware power path, which no software setting -can intercept. The reseat is the only fix, and this is a -lose-work-without-warning failure mode, not an inconvenience. -Interim shield (already live): /etc/systemd/logind.conf.d/powerkey.conf -sets HandlePowerKey=ignore — phantom presses log but do nothing; EC-level -10s hold still force-cuts. Consider keeping it even after the repair. -Verify after reseat: flex the chassis edges + partially lower the lid, then -grep the journal for new "Power key pressed" lines — zero means fixed. -Must be done before the Sunday flight — a phantom press mid-travel with the -shield on is survivable, but the connector should not be trusted at 30,000 -feet on the loose setting. - -*** 2026-08-19 Wed @ 14:40:00 -0700 Retracted: the RTC reset is not this task's, and I should not have filed it here -I attributed the 2026-08-19 network outage to this ribbon earlier today. Craig -pushed back — he reseated it before the trip to get the touchpad working — and -he is right. The evidence does not support the attribution and some of it points -the other way. - -What actually holds. Boot -3 ended at 01:33:18 with no shutdown sequence: no -power-off target, no unmounting. The next boot's kernel line reads =rtc_cmos -00:01: setting system clock to 2025-01-01T00:00:16 UTC=, a firmware default, so -the RTC was reset rather than drifted. No firmware update was applied -(=fwupdmgr get-history= is empty) and the battery is fine. - -What refutes the ribbon. This boot logged *zero* =Power key pressed= events, and -so did the four boots before it. The phantom-press symptom had genuinely stopped -after 08-15, exactly as the 08-16 session recorded. The earlier events logged a -power-key press and an orderly poweroff; this logged neither, which makes it a -different signature, not a worse version of the same one. - -What I got wrong methodologically: I anchored on the most salient open hardware -task and read association as evidence. I even wrote "I can't prove it is the -same connector" and then filed it here anyway, which is the tell. - -Two things I checked and can rule out. There were no OOM kills — the 3,433 -matching lines are a systemd unit named "Periodically re-score Claude Code -processes for the OOM-killer" firing on a timer, not memory pressure, and there -is not a single "Killed process" line. Thermal is clean; the only mentions are -boot-time zone registration at 34C and 45C. - -One real thing the same window did surface, tracked separately: a python3 crash -loop, 251 core dumps in the final ten minutes, SIGABRT with =XFreeThreads= and -=PyEval_RestoreThread= in the trace. It does not explain the RTC, because -software cannot clear it, but it is its own problem. - -The open question that would settle the RTC is for Craig, not the journal: a -long power-button hold on a Framework triggers an EC-level reset that clears the -RTC, which fits a wedged machine being forced off. A 4-second hold would not. - -*** 2026-08-17 Mon @ 19:57:42 -0700 Not done, and the two symptoms now disagree -The reseat did not happen before the flight, and velox is travelling. The -deadline blew past on 08-14. - -The two symptoms have separated, which is worth recording because it changes -what the evidence proves. The phantom presses have stopped: fifteen "Power key -pressed" entries between 08-14 04:29 and 08-15 20:04, then nothing at all -across five boots including today's. The touchpad has not — there is still no -touchpad node under =/dev/input/by-path/=, which is the same dead interrupt -line the body describes. +** TODO [#B] Signal tray icon invisible under waybar (Electron 43 well-known-name SNI) :bug:waybar:velox: +:PROPERTIES: +:CREATED: [2026-08-25 Tue] +:LAST_REVIEWED: 2026-08-25 +:END: +Since signal-desktop 8.24.0 (Electron 43.4.0) Signal's tray item registers +under a well-known bus name (=org.freedesktop.StatusNotifierItem-<pid>-1=) +and answers Properties.Get/GetAll only when addressed by that name. waybar's +GDBus proxy addresses the owning unique name instead, reads back no Id or +Category, and logs "Invalid Status Notifier Item", so the icon never shows. +With =--start-in-tray= that leaves Signal running with no window and no icon; +launching it again from fuzzel raises the existing window. Slack (older +Electron, unique-name registration) is unaffected. Measured 2026-08-25 with a +bus monitor: the same connection returns the value for the well-known name +and "error occurred in Get" for its own unique name. + +Grading: Major severity (the app is unreachable from the desktop while +"running") × every user every time on velox = P1 by the matrix, held at +[#B] because the workaround (relaunch to raise the window) is cheap and the +fix is upstream. + +Upstream: [[https://github.com/Alexays/Waybar/issues/5240][Waybar #5240]] (open, proposes a raw-call fallback in item.cpp +proxyReady) and [[https://github.com/signalapp/Signal-Desktop/issues/7992][Signal-Desktop #7992]] (open, "Upstream Change Needed"). +Downgrading to 8.23.0 is closed off: 8.24.x migrated the SQLCipher schema to +1770 and 8.23.0 quits with DBVersionFromFutureError (tried and reverted +2026-08-25). + +Re-test after a waybar or signal-desktop upgrade, from the repo root: +#+begin_src sh :results output +grep -c 'Invalid Status Notifier Item' "$(\ls -t ~/.local/var/log/waybar-*.log | head -1)" +n=$(busctl --user list --no-legend | awk '$1 ~ /StatusNotifierItem-/ && $3=="signal-desktop"{print $1}') +busctl --user call "$n" /StatusNotifierItem org.freedesktop.DBus.Properties Get ss org.kde.StatusNotifierItem Id +busctl --user call "$(busctl --user call org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus GetNameOwner s "$n" | cut -d'"' -f2)" /StatusNotifierItem org.freedesktop.DBus.Properties Get ss org.kde.StatusNotifierItem Id +#+end_src +Expected when fixed: 0 "Invalid" lines in a fresh waybar log, or both Get +calls returning the Id (either side fixing it clears the icon). -So the quiet power button is not evidence the connector reseated itself. The -interrupt line is the symptom that cannot be masked in software, and it is -still dead, so the ribbon is still unseated. The most likely reason the -presses stopped is that the machine has been sitting on hotel surfaces instead -of being carried and flexed. +Ratio is on 8.21.0 and unaffected until its next upgrade brings 8.24.x. +Alternatives if it drags on: change Signal's tray setting so it keeps a +window (=~/.config/Signal/ephemeral.json= =system-tray-setting=), or run a +waybar carrying the #5240 fallback. -The interim shield is still live (=HandlePowerKey=ignore=), and the escalation -note stands: an EC-level glitch cuts power below systemd regardless of it. ** DOING [#A] Velox reinstall — DR test of archangel + archsetup :velox:chore: DEADLINE: <2026-08-15 Sat> :PROPERTIES: @@ -352,62 +485,6 @@ failures, so a regression here is uniquely undetectable. P2 = [#B]. :solo: — the surface is one script and its suite, the refactor is behaviour-preserving, and the existing 58 tests plus a mutation battery are the objective check that it stayed so. -** TODO [#A] powerprofilesctl crashes on a loop since ppd was masked :bug:velox:dotfiles: -:PROPERTIES: -:CREATED: [2026-08-17 Mon] -:LAST_REVIEWED: 2026-08-17 -:END: -Something polls power state every 10-30 seconds, and each poll runs -=powerprofilesctl get=, which SIGABRTs. 47 coredumps on velox on 2026-08-17 -alone, the earliest at 08:34, four in one minute while I was watching. - -Cause is the 2026-08-16 fix that masked =power-profiles-daemon= so TLP -survives on laptops. That fix is right and stays. What it did not account for -is the settings module's power backing -(=~/.dotfiles/settings/src/settings/power.py=), which shells out to -=powerprofilesctl=. Against a masked unit the D-Bus activation fails with -=NameHasNoOwner ... unit is masked=, and the caller aborts rather than -degrading. - -Run by hand the same command exits 0 and prints the error, so the abort is -context-dependent and the caller needs finding before the fix is written. -Ratio does not mask ppd, which is why this is velox-only and why it appeared -the day after the masking. - -Costs: journal spam, coredump disk churn, and repeated failed D-Bus -activations on a travelling laptop's battery. It is also the leading suspect -for the wedged user manager filed below. - -Fix shape: =power.py= should treat a masked or unavailable ppd as a -first-class "no profile control here" state rather than an error path, and -the poller should stop retrying a unit it has been told is masked. The -machine-level half is already correct. - -Grading: Major severity (a crash loop burning battery and filling the -journal, silently) x every user every time on any laptop with the TLP fix -applied = P1 = [#A]. - -*** 2026-08-17 Mon @ 19:57:42 -0700 The loop stopped at the reboot; the defect did not -velox rebooted at 16:04 and there have been zero coredumps since, against 47 -in the twelve hours before it. So the loop is not currently burning anything. - -That is not a fix, and the distinction matters for whoever picks this up. -=powerprofilesctl get= still fails exactly as recorded — =NameHasNoOwner ... -unit is masked= — so every precondition for the loop is intact and it returns -whenever the caller next polls. What the reboot cleared is the caller's state, -not the bug. - -Narrowed the search the body asks for: =power.py= is the *only* file in -dotfiles that shells out to =powerprofilesctl= (=SETTINGS_POWERPROFILESCTL=, -line 14), so the caller is inside the settings module rather than waybar or a -timer. Worth knowing that the coredumps are =powerprofilesctl= itself aborting -— it is a python script, which is why they log as =/usr/bin/python3.14= -SIGABRT rather than under its own name. - -Grade unchanged. The matrix inputs did not move: the severity is what happens -while the machine is in that state, and the frequency row is every laptop -carrying the TLP fix. A quiet interval since a reboot is not a frequency -change. ** TODO [#B] velox's systemd --user spins at 96% and cannot resolve unit files :bug:velox: :PROPERTIES: :CREATED: [2026-08-17 Mon] @@ -466,63 +543,6 @@ instruction rather than by a fresh judgment. Reproducing it deliberately is the open question, and it is not obviously worth doing — it costs a wedged session to learn something the crash-loop fix may make moot. -** TODO [#A] The installer clones my two working repos shallow and read-only :bug:velox: -:PROPERTIES: -:CREATED: [2026-08-17 Mon] -:LAST_REVIEWED: 2026-08-17 -:END: -=archsetup:1432= clones the user's archsetup repo and =archsetup:1445= clones -dotfiles, both with =--depth 1=. Those are not build directories. They are the -two repos I actively develop in, and on velox they came back from the -2026-08-13 rebuild with 7 commits of history each instead of 851. - -Found 2026-08-17, and found the worst way: I ran the credential-file history -check that the GitHub-release task asks for, and it reported all five files -absent from history with a clean exit. The real answer is that this clone -cannot see the history those files live in. A shallow clone does not error on -=git log -- <path>=, it answers "no commits" — so a security question came back -falsely clean, and nothing about the output said otherwise. - -Everything else it breaks is quieter: =git log=, =blame=, =bisect=, and any -archaeology past the boundary. The tree looks completely normal, which is why -this survived four days on the machine. - -The right shape is already in the codebase. =scripts/post-install.sh:42-51= -takes depth as a per-repo argument and defaults to a full clone, so wallpaper -gets =--depth 1= and org does not. The AUR build clones (=archsetup:855=, -=:1673=, =:1677=) are correctly shallow and stay that way. Only the two -user-repo sites change. - -*Second defect, same two lines, found 2026-08-17 while pushing:* the dotfiles -clone could not push at all. =archsetup:245= defaults =dotfiles_repo= to -=https://git.cjennings.net/dotfiles.git=, the public read-only endpoint, so -=git push= returned 403. Ratio uses =git@cjennings.net:dotfiles.git= and -archsetup's own clone uses the matching ssh form, so velox was the odd one out -purely because it was the machine rebuilt by the installer. Repointed velox's -remote and pushed. - -That half needs a decision rather than a fix, which is why this task is no -longer =:solo:=. The https default is *correct for a stranger* installing -archsetup, who has no ssh key on the server, and this repo is being prepared -for public release. It is wrong for my own machines, which need to push. The -override already exists (=DOTFILES_REPO=, documented in -=archsetup.conf.example=), so the question is only where my personal value -lives: a config the personal ISO bakes in, a post-install step, or a detection -that prefers ssh when a key is present. Craig's call. - -Repair on a machine already built: =git fetch --unshallow= in each repo, and -=git remote set-url origin git@cjennings.net:<repo>.git= for dotfiles. - -Grading: Major severity (two working repos silently missing their history on -the machine I develop on, and it returns confidently wrong answers to history -questions rather than failing) x every user every time (every fresh install, -both daily drivers) = P1 = [#A]. - -Not :solo:. The depth half is (two lines plus tests in the existing -=tests/installer-steps/= shape, verifiable by asserting the clone command -carries no =--depth= for these two repos). The remote-URL half needs the -decision above, so the task as a whole waits on it. Split it in two if the -depth fix is wanted sooner. ** TODO [#B] post-rebuild-check needs a reference-host mode :feature:velox:solo: :PROPERTIES: :CREATED: [2026-08-17 Mon] @@ -556,6 +576,27 @@ close. Most-users-frequently x Major = P2 = [#B]. :solo: — the checks exist, the ssh path is proven (the 2026-08-17 session ran exactly this comparison by hand), and correctness is verifiable locally by diffing the two reports. +*** 2026-08-21 Fri @ 07:10:00 -0700 The premise moved: velox now reports 1 finding, not 8 +Re-scope before building. The 1:7 ratio this task argues from is gone, and two +of the three things it cites as noise are fixed at the source rather than +filtered. + +=87ff0b7= gave check 2 a machine-local expected-disabled list, so the four unit +findings are declared intent rather than noise, and an entry whose unit turns +out to be enabled is itself reported so the list cannot rot. =3fbf3e0= dropped +=.claude= from check 4's expected set, since the gitignore sweep writes that +line into every project whether or not one exists. velox went 8 findings to 1. + +So the open question is no longer "how do we cut the noise" but whether a live +reference-host diff still earns its place against a static declaration of +intent. They are different tools: the list is offline, explicit, and states +what a machine means; the diff is automatic and catches drift nobody declared. +The reference-host comparison is still what *found* all of this, twice, by +hand. That is an argument for it and not against. + +Worth knowing this task already contained the whole 8-to-1 analysis when it was +filed 2026-08-17, and a session on 2026-08-20 re-derived it from scratch without +reading it. Not :solo: any more — the design call above is Craig's. ** TODO [#C] screen-lock test suite red on ratio :bug:test:dotfiles: :PROPERTIES: :CREATED: [2026-08-13 Thu] @@ -612,6 +653,33 @@ and turns a silent no-op into a visible line: 5. =signal-cli listAccounts= non-empty. velox lost its registration, and because agent-text relays to a hardcoded velox, that breaks the phone channel for the WHOLE FLEET, not just this machine. +6. =mbsync --list= parses. The Proton Bridge TLS cert + (=~/.config/protonbridge.pem=, referenced by =~/.mbsyncrc=) is generated + per *installation*, so it cannot be restored or copied between machines. + Its absence aborts the config parse, which kills *every* account — gmail + and dmail need no bridge and died anyway. The error names only the missing + pem, so "no mail at all" and "this one file is missing" look unrelated. + Re-derive it off the running bridge's own handshake, no GUI, no secrets: + =openssl s_client -connect 127.0.0.1:1143 -starttls imap -showcerts </dev/null | sed -n '/BEGIN CERTIFICATE/,/END CERTIFICATE/p' > ~/.config/protonbridge.pem= +7. The bridge password (=~/.config/.cmailpass=) is per-install too. It is a + real file rather than a stow symlink, so it survived the rebuild holding + the *previous* install's value — worse than absent, because it looks + right. Diagnostic trap: the bridge answers a wrong password with =no such + user=, which reads as "no account signed in" and sends you hunting a login + problem that doesn't exist. Never treat =no such user= as evidence about + account state. + +*The distinction that organizes all seven* (from the .emacs.d handoff +2026-08-14, inbox): every artifact that broke was generated on the machine by +an application rather than carried by git, stow, or dotfiles. But they split +two ways, and conflating them is what produces a file that exists, looks +right, and authenticates against nothing: +- *Restore* — the old value is still correct: gitignored tooling (1), + roam clone state (3), =*.local.el= configs (5). +- *Re-derive* — the old value is worthless because the application minted a + new one: signal-cli registration (4), bridge cert (6), bridge password (7). +So the checklist wants two columns, not one. + Graded [#A] because item 5 is live right now and silently disables paging, and because the flight is Sunday. ** TODO [#B] Restoring a git repo from backup can resurrect a dangerous diff :bug: @@ -758,6 +826,26 @@ archangel+archsetup ISO that's already ~80% built. Two ISO modes: generic don't start the migration until the credentials are rotated. Not started. Not :solo: — repo standup and history rewrite are Craig's calls; promote to a real spec (spec-create) when work resumes. + +*Also bake the push-capable repo URLs into the personal ISO* (decided +2026-08-19). =archsetup:240= and =:245= default =archsetup_repo= and +=dotfiles_repo= to =https://git.cjennings.net/...=, the anonymous read-only +endpoint. That default is right for a stranger installing archsetup — no key on +the server — and wrong for my machines, which have to push: velox came back +from its rebuild unable to push either repo, and I only found out at a 403 four +days later. I decided against detecting an ssh key in the installer, because +archsetup never restores =~/.ssh= (I do that by hand), so key-presence at clone +time depends on ordering the installer doesn't control, and a naive "any key +means ssh" would break a stranger who happens to have one. The override already +exists and is documented — =ARCHSETUP_REPO= / =DOTFILES_REPO= in +=archsetup.conf.example= — so the personal ISO just needs to carry the ssh +form of both, alongside the secrets bundle. The generic ISO keeps the https +default untouched. + +The gap that leaves is a curl|bash or stock-ISO install, which takes the https +default straight back. =post-rebuild-check= check 8 covers that path — it flags +a working repo whose origin is the read-only endpoint — so the ISO value is the +fix and the check is the net under it. ** TODO [#B] Settings toggles reset silently at session start :bug:dotfiles: :PROPERTIES: :CREATED: [2026-07-28 Tue] @@ -999,29 +1087,6 @@ Not =:quick:= despite being small: four pieces with tests is a sitting rather th From the roam inbox (Craig, claimed 2026-07-23): the wallpaper channel switches on sunrise/sunset today (the sun-pair mode, =settings/src/settings/wallpaper.py=, location read live via whereami with a state.json cache). Add a timed-schedule mode as an alternative: fixed clock times drive the transitions rather than the solar calc. Not :solo: — the capture itself flags the missing inputs ("we'll need to know the transition times, and how many of them there are"). The count and the times are a design decision Craig owes: is it a two-image day/night flip at fixed hours, an N-way ring across the day, per-image dwell vs shared interval? The =set= channel already does fixed-interval cycling through a set, so the new part is specifically clock-anchored transition points, not just "a timer". Ask for the schedule shape at pickup, then build against the existing wallpaper.apply presenter vocabulary. -** TODO [#D] Worldclock tooltip blanks on one bad timezone row :bug:dotfiles:waybar:quick:solo: -:PROPERTIES: -:LAST_REVIEWED: 2026-07-25 -:END: -Found by sentry (2026-07-25), verified by exercising. =hyprland/.local/bin/waybar-worldclock= builds each zone with =ZoneInfo(tz)= inside the loop (line ~99) with no guard, so a single malformed timezone row in =worldclock.conf= raises =ZoneInfoNotFoundError= and crashes the whole python pass. The tooltip then renders empty and *every* zone is lost, not just the bad row; the traceback only reaches stderr, where waybar never surfaces it. -Repro: a conf with =America/Chicago|Home=, =Not/AZone|Bad=, =Europe/London|London= renders =tooltip: ""= (Home and London gone too). -Grade: minor severity (one module's tooltip blanks, no data loss) x rare edge case (a malformed conf row) = P4 = [#D]. -Fix: wrap the per-row =ZoneInfo=/=datetime= in a try/except and =continue=, so a typo drops only that row and the valid zones still render. Solo + quick: the script already has an env-override test harness (=WAYBAR_TIME_EPOCH=, =WAYBAR_WORLDCLOCK_CONF=), so a red-first test is cheap. -** TODO [#C] obsbot-wb-guard polls forever on machines with no OBSBOT :bug:dotfiles:quick:solo: -:PROPERTIES: -:LAST_REVIEWED: 2026-08-16 -:END: -=obsbot-wb-guard.service= is =WantedBy=graphical-session.target= and lives in the shared =common/= stow tier, so it starts on every machine. Its main path is =while :; do check_once; sleep 2; done=, and =check_once= returns early when the camera node is absent. On a machine with no OBSBOT attached that is a process waking every two seconds forever to do nothing, which on a laptop is battery spend for zero benefit. No restart loop, though: the loop never exits, so =Restart=on-failure= never fires. - -Found 2026-08-16 on velox, after enabling it to match ratio and then having to disable it again by hand. A per-machine disable is the wrong shape, because it drifts velox from ratio permanently and a re-stow or a future audit will just put it back. - -Fix: give the unit =ConditionPathExists= on the camera node (=/dev/v4l/by-id/usb-Remo_Tech_Co.__Ltd._OBSBOT_PW106-video-index0=, the same default the script uses) so systemd skips it on any machine without the camera and starts it normally on ratio. Then re-enable it on velox, where it will simply be skipped. Note the limit: a camera plugged in later will not start it until the next login, which is the right trade against a permanent poll. - -Careful when disabling by hand in the meantime: =systemctl --user disable= on a *linked* unit deletes the unit symlink, and that symlink is stow-managed, so a bare disable silently removes a file from the dotfiles stow tree. Restore the link afterward or re-stow. - -Grade: minor severity (wasted wakeups and battery, no data loss, no failure) x every boot on any machine without the camera = P3 = [#C]. - -Solo: buildable here (archsetup owns dotfiles end-to-end), verifiable by the agent (assert the unit is skipped on velox and still active on ratio), and no design call left open. ** TODO [#C] Auto-dim status forgotten on layout change :bug:dotfiles: :PROPERTIES: :LAST_REVIEWED: 2026-07-25 @@ -1042,20 +1107,6 @@ From the roam inbox (Craig, claimed 2026-07-24): the network, bt, maint, and aud :LAST_REVIEWED: 2026-08-02 :END: From the roam inbox (Craig, claimed 2026-07-24): panel labels look cut off; a few more pixels of space fixes it. He named the audio and bt panels, but his "before" capture is the networking panel (=~/pictures/screenshots/2026-07-23_202419.png=; "after" resizing =~/pictures/screenshots/2026-07-23_202458.png=), so the whole panel family likely shares the tight spacing. Confirm which panels clip at pickup, then add the padding/width. Grade: cosmetic × every glance at the affected panels = P3 = [#C]. Solo — buildable (CSS/size tweak) and screenshot-verifiable, no design call once the clipping panels are identified. -** TODO [#C] Spine face tests decay against the wall clock :bug:test:dotfiles:solo: -:PROPERTIES: -:LAST_REVIEWED: 2026-08-02 -:END: -=settings/faces/timeline-face-spine.test.mjs= has thirteen =SP.spineRows(g, h)= calls that omit the third argument, so =ref= falls back to its =new Date()= default while the file's events fixture is pinned to =JUL= (2026-07-31 18:30 UTC). Any assertion that depends on how much room the day needs is then measured against today's clock, and rots as the fixture recedes. - -One of them, "spacing is uniform everywhere except the gap home opens", had already rotted: green on 07-31 because that was the fixture's own date, red by 08-02. Fixed in place on 2026-08-02 by pinning =JUL=; the remaining thirteen pass today by luck. The measurement, for whoever picks this up — with =ref=now= the even step is 85.21 and home's gaps are 129.10 / 65.40 (the lower one collapses below a plain gap); with =ref=JUL= the step is 78.54 and the gaps are 129.10 / 145.46. Only the lower gap moves, because =up= does not depend on events and =down= does. - -Six other calls in the same file already pass =JUL= explicitly, so the convention exists and this is a miss, not a gap in the design. Fix: pass =JUL= at every call whose assertion reads geometry. Leave the call around line 747 alone — it sweeps =new Date(t0)= deliberately. - -Grade: minor severity (dev-facing only; no product behavior is wrong, the face itself is fine) x some users, sometimes (each call rots independently, whenever the fixture drifts far enough) = P3 = [#C]. Not merely cosmetic though: a suite that goes red for no real reason is how a genuine regression gets waved through. - -Solo — mechanical, an existing convention to copy, and verifiable by running the suite plus re-running it under a faked clock to prove the determinism actually holds. - ** TODO [#C] Night-watch live telemetry :feature:maint: :PROPERTIES: :LAST_REVIEWED: 2026-08-02 @@ -1124,19 +1175,49 @@ snapshot. =origin= on this machine is still =git@cjennings.net:archsetup.git=, the cgit account, so nothing has moved. Everything in the plan stands unchanged. + +*** 2026-08-21 Fri @ 14:12:46 -0700 Recorded the publication mechanism: placement is the only control +The work project verified its own repo reads "not served" against a control +repo that reads PUBLIC, and reported the mechanism back: the host publishes via +=GIT_HTTP_EXPORT_ALL= over =GIT_PROJECT_ROOT=/var/git=, so *publication is +directory placement and nothing else* — there is no per-repo marker, no +=git-daemon-export-ok= file, no opt-in flag to check. A repo is public because +of where it sits. + +That is the durable hazard for this task's plan, and it cuts both ways. It +confirms the approach — a bare repo created outside the scan-path is private by +construction, which is exactly what the plan already specifies. It also means +nothing in a repo itself records whether it is exposed, so any future move +*into* =/var/git= publishes silently, with no local artifact to notice. Their +own repo is private for this reason alone: it lives under =/var/cjennings/git/=, +outside the served root. + +Caveat they raised and I agree with: any enumeration of the served set is a +snapshot, not a standing fact. The set moved twice while three projects were +measuring it. Verify placement at the time of the move rather than trusting a +recorded list. ** TODO [#B] Velox boot-failure retrospective — upgrade guard gaps :bug:zfs:maint: :PROPERTIES: -:LAST_REVIEWED: 2026-07-21 +:LAST_REVIEWED: 2026-08-26 :END: 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. - Add a post-upgrade /boot assertion: after a transaction touching linux/linux-*, confirm vmlinuz-* + initramfs-*.img present and mkinitcpio exit 0; refuse to end the run (or page Craig) otherwise. Would have caught this. - 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. - CONFIRMED (2026-07-21): the pre-pacman snapshot hook fired on velox — the 2026-07-15 no-kernel boot was recovered via the pre-pacman ZFS snapshot rollback, and velox is back on the tailnet running linux-lts 6.18.38 with initramfs present (2026-07-19 session). Root-cause hook-ordering fix shipped separately. Still open: the post-upgrade /boot assertion in guard.py and the sanoid-vs-actual dataset drift reconcile (the two bullets above). +*** 2026-08-26 Wed @ 22:35:01 -0600 The /boot assertion now lives in the topgrade spec; the dataset drift is what remains here +The post-upgrade /boot assertion is covered by the kernel-modules-check gate in +[[file:docs/specs/2026-08-25-topgrade-guarded-upgrade-spec.org][the topgrade guarded-upgrade spec]] +(dkms built for the new kernel, initramfs newer than vmlinuz, pre-pacman +snapshot on a ZFS root), which ships with that spec's Phase 1 rather than here. +What this task still owns is the sanoid-vs-actual dataset drift: whether to +split zroot/var/log and zroot/var/lib/pacman out as configure_zfs_snapshots +assumes, or change the config to match the layout velox actually has. That is +a call I have not made, so the task stays [#B] and not solo. -** TODO [#B] Assess a Hyprland left-drag window gesture :feature:hyprland: +** TODO [#C] Assess a Hyprland left-drag window gesture :feature:hyprland: :PROPERTIES: -:LAST_REVIEWED: 2026-07-21 +:LAST_REVIEWED: 2026-08-26 :END: Evaluate whether a global left-click drag can move ordinary windows without breaking application selection, text interaction, or Wayland security @@ -1145,35 +1226,94 @@ any binding. ** TODO [#B] Reconcile panel keybindings around Super+N :feature:hyprland: :PROPERTIES: -:LAST_REVIEWED: 2026-07-21 +:LAST_REVIEWED: 2026-08-21 :END: -Swap the notification and networking bindings so primary panels are one -Super-plus-letter chord away, audit the other exceptions, and bring the -proposed family to Craig for a final mapping decision. +Put every panel on one consistent chord family — net, bluetooth, audio, timer, +and the maintenance console — as a shared modifier set plus a mnemonic letter +per panel (N/B/A/T/M). Today they open by waybar click only, so a uniform +family is what makes them keyboard-reachable and predictable. The immediate +move is swapping the notification and networking bindings so the primary panels +sit one Super-plus-letter chord away. + +Maintenance (M) is the chord I want first — it is the panel I keep reaching for +without one. + +Constraints: +- Super+Shift+A is already the PTT toggle, and the hold-to-talk grave bind is + load-bearing. Audit every current hyprland bind for conflicts before + proposing a family, and treat these two as fixed. +- Both machines have to work the same way. Velox can't QMK-remap, so the chords + have to be typable on a plain laptop keyboard. + +Steps: settle the modifier family, audit the existing binds for collisions, +wire it through the dotfiles hyprland config, and document it in the keybind +reference. + +The family itself is the one call I haven't made — the audit and the wiring +follow from it, so that decision comes first rather than last. + +*** 2026-08-21 Fri @ 14:15:22 -0700 Merged the duplicate keybinding-family task into this one +Two tasks were carrying one job: this one and =[#B] Consistent keybinding family +for the panel console=, filed separately and both stalled. This one had the +tighter framing and the more recent review; that one had the better body — the +specific collisions, the velox plain-keyboard constraint, and maintenance-M as +the priority chord. Folded its detail in here and cancelled it, since two +half-specified tasks for one decision is plausibly why neither moved. + +Not =:solo:= and not =:quick:=: the modifier family is a preference call I have +to make, judging what's load-bearing among the existing binds needs me too, and +the audit plus wiring plus docs runs past thirty minutes on its own. ** TODO [#B] Add storage-capacity signals to the maintenance module :feature:maint: :PROPERTIES: -:LAST_REVIEWED: 2026-07-21 +:LAST_REVIEWED: 2026-08-26 :END: Investigate capacity and growth diagnostics for full disks, identify the appropriate remedies, and incorporate a clear storage signal into the maintenance console. -** TODO [#B] Add per-channel controls to the audio panel :feature:audio: +** TODO [#C] Add per-channel controls to the audio panel :feature:audio: :PROPERTIES: -:LAST_REVIEWED: 2026-07-21 +:LAST_REVIEWED: 2026-08-26 :END: Expose channel-level input and output volume controls without losing the existing device-level workflow. ** DOING [#B] Widget gallery upgrades :feature:design: :PROPERTIES: -:LAST_REVIEWED: 2026-07-13 +:LAST_REVIEWED: 2026-08-23 :END: -Usability + documentation pass over the [[file:docs/prototypes/panel-widget-gallery.html][panel widget gallery]], orthogonal to the component-generation spec work, so it runs on the =gallery-upgrades= branch (squash merge to main after Craig's UI confirmation + tweaks). Items 1-4 run as a no-approvals speedrun (Craig authorized 2026-07-12); item 5 is a joint brainstorm after the merge. +Usability + documentation pass over the [[file:docs/prototypes/panel-widget-gallery.html][panel widget gallery]], orthogonal to the component-generation spec work. Items 1-4 run as a no-approvals speedrun (Craig authorized 2026-07-12); item 5 is a joint brainstorm. + +The =gallery-upgrades= branch this originally described is gone — no local or remote ref, and every gallery commit since has landed straight on main. Whether it was squash-merged or abandoned, the branch stopped describing how this work runs, so the line came out at the 2026-08-23 review rather than being left to mislead. Work on main. *** TODO Extraction-readiness bar for every gallery component :refactor:design: Craig's standing directive (2026-07-18, set while finishing the split-flap): every =DUPRE.*= builder should meet the bar the split-flap now sets, since these become regular components. The bar: a contract comment documenting every opt and the full handle surface; no page globals touched (page owns cadence via handles/callbacks, e.g. =onSettle=); all component CSS in one named =DUPRE_CSS= block; refactored until no opportunity worth doing remains (small named helpers, no duplication); construction axes declared via =STYLES= where the component has them. Sweep the existing builders against that list, fix the gaps, and make the bar a stated convention in the widgets.js header or README so new builders inherit it. Overlaps the component-generation spec's extraction phase — reconcile there rather than doing the work twice. +*Audited 2026-08-23 — the sweep covered the bulk and stopped short.* Ten commits +on 2026-07-18 (=43725ff= … =1dd929d=) carried ~104 of the 112 builders over. Two +criteria are fully met: 105 builders carry a real contract comment naming opts +and the handle surface, and component CSS is wholly consolidated — the gallery's +own =<style>= block holds nothing but page chrome (masthead, grid, toc, card +frames, validation lamps, palette). Three gaps remain, and they are the whole of +what's left: + +1. *Seven builders were never swept*: =telegraphIndicator=, =radarSweep=, + =dotMatrix=, =flipDisc=, =dekatron=, =gearIndicator=, =blinkenlights=. Each + carries a one-line comment describing what the widget does, with no opts, no + handle surface, no CSS statement. They sit at the tail of =widgets.js= after + =responseGraph=, and the last batch was "well-through-response" — the sweep + stopped one builder short of the end and never came back. Not a clean cut: + =dayDateCal= is in that tail and does have a contract. +2. *The bar was never written down.* The README documents the API shape (Builder + contract) and =DUPRE_CSS= (Styling) — two of the five criteria. The checklist + itself appears nowhere, so a new builder inherits nothing and the sweep has to + be re-derived from this task every time. +3. *One page-global reach survived*: =patchBay= does + =window.addEventListener('resize', draw)= and never removes it. Fails the + no-page-globals criterion and leaks besides — a torn-down instance keeps + redrawing on every resize. The other two =document= reaches are benign + (=indexPlate= guards a shared SVG def; the other is the CSS injector). + *** 2026-07-18 Sat @ 04:32:20 -0500 Made the N20 split-flap an honest Solari mechanism =GW.splitFlap= rebuilt from the drop-fade fake: charset-as-drum stepping (=opts.chars= is the flap order, one flip at a time through intermediates, staggered arrival), re-aim-not-queue retargeting, and the real two-half-panel fold (WAAPI, backfaces hidden), with =animate:false= collapsing to instant jump for reduced motion. Handle grew =setText=/=chars=/=reading()=; default width 3 → 4 cells. Nine probe checks written red-first (arrival, intermediates, one-flap stepping, re-aim discriminator, instant path); technique studied from HotFX and re-derived — no license on their repo, nothing copied (reference filed in =working/retro-stereo-widgets/references/=). Review: sound; its two test-strength notes addressed in the same change. @@ -1330,11 +1470,25 @@ The cross's cell assignments were read off card names and spec sheets, not audit ** DOING [#B] Retro widget catalogue :feature:design: :PROPERTIES: :SPEC_ID: 3ac0d42c-db1a-4d21-bce4-e63785fef0ba -:LAST_REVIEWED: 2026-07-13 +:LAST_REVIEWED: 2026-08-23 :END: The panel widget gallery ([[file:docs/prototypes/panel-widget-gallery.html][docs/prototypes/panel-widget-gallery.html]]) grows into a retro-instrument component catalogue: reference photos of period hardware → gallery cards (the visual + behavioral spec) → reusable components for three targets (emacs svg.el, web/React, waybar). Tokens single-sourced in [[file:docs/prototypes/tokens.json][tokens.json]] (gen_tokens.py emits web/waybar/elisp); svg.el proof widget shipped (gallery-widget.el, needle gauge). Reference photos live in [[file:working/retro-stereo-widgets/][working/retro-stereo-widgets/]]. Collection converged at R56, then reopened at R57 as the taxonomy found empty cells (110 cards, all behaviorally verified; probes in [[file:tests/gallery-probes/][tests/gallery-probes/]]). Build runs per the [[file:docs/specs/2026-07-12-component-generation-spec.org][component-generation spec]] (DOING; reviewed + decomposed 2026-07-12): web extraction first (ungated, lossless), then demand-gated Emacs/waybar ports. Banked variant/composition ledger lives in the 2026-07-11/12 session archive. + +*The eight open subtasks are really one decision plus three builds* (noted at the +2026-08-23 review, because "8 open" reads as more contested than it is). Phase 1 +shipped. Phases 3, 4 and 5 and the spec flip are each gated, directly or +transitively, on *Phase 2 — the demand inventory*, which is my matrix to write +and nobody else's. That single artifact has been the whole chain's blocker since +2026-07-12. The three genuinely independent items are the magic-eye rebuild, the +wind-direction rose, and weather kit integration. + +Weather kit integration may already be unblocked: its note says live panel +verification "awaits a stowed desktop with a private weather location +configured", and both daily drivers are stowed now. Check whether +=$WEATHER_LAT=/=$WEATHER_LON= or =~/.config/weather/config.json= is set before +treating it as still waiting. *** TODO [#B] Rebuild the magic-eye tube component :feature:design: Reinstate the magic-eye tuning/level indicator (EM34/EM84/6E5 family), but replace the earlier weak UI rather than reviving it unchanged. The component @@ -1396,10 +1550,10 @@ 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 [#B] Net doctor expansion v1 — VM live verification :feature:dotfiles:network: +** TODO [#B] Net doctor expansion v1 — VM live verification :feature:dotfiles:network:solo: :PROPERTIES: :SPEC_ID: ce29b103-ed9d-4f56-bf8c-9ed8fe680ff3 -:LAST_REVIEWED: 2026-07-13 +:LAST_REVIEWED: 2026-08-25 :END: Build the [[file:docs/specs/2026-07-11-net-doctor-expansion-spec.org][net doctor expansion]] (IMPLEMENTED). Adds the control-plane cluster (rival-manager / nm-masked / keyfile-perms) and a sharper auth verdict to the shipped net doctor (=~/.dotfiles/net/=). Archsetup owns the dotfiles work end to end — edit, test, commit, and push in =~/.dotfiles=, then drop an inbox note. All build phases shipped and fake-verified; the one open piece is the VM live verification below. *** 2026-07-11 Sat @ 02:47:47 -0500 Built the read-only control-plane probe @@ -1419,23 +1573,15 @@ 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 [#B] Consistent keybinding family for the panel console :feature:hyprland: -:PROPERTIES: -:LAST_REVIEWED: 2026-07-09 -:END: -Consider putting every panel (net, bluetooth, audio, timer, and the coming maintenance console) on one consistent chord family — a shared modifier set (Super+Shift, Control+Alt, or similar) plus a mnemonic letter per panel (N/B/A/T/M). Today the panels open via waybar clicks only; a uniform chord family makes them keyboard-reachable and predictable. Watch for collisions with existing binds: Super+Shift+A is already PTT toggle, and the hold-to-talk grave bind is load-bearing. Decide the family, audit current hyprland binds for conflicts, wire via the dotfiles hyprland config, and document in the keybind reference. Both machines (velox can't QMK-remap, so chords must work on a plain laptop keyboard). -*** 2026-07-14 Tue @ 00:31:36 -0500 Folded Craig's ask for a maintenance-panel keybinding; bumped [#C] → [#B] -Craig asked (in session, 2026-07-14) for a maintenance keybinding specifically — the panel he's reaching for without one. Maintenance (M) is the priority chord when this task gets worked. The capture graduated the task from parking lot to active backlog. - ** DOING [#B] Run-time privilege model, standard across every panel doctor :feature:dotfiles: :PROPERTIES: -:LAST_REVIEWED: 2026-07-13 +:LAST_REVIEWED: 2026-08-25 :END: The audio input/output doctor is gaining a run-time privilege model (see [[file:docs/specs/2026-07-10-audio-doctor-input-side-spec.org][docs/specs/2026-07-10-audio-doctor-input-side-spec.org]], decision "The doctor may use sudo, resolved by context at run time"). Craig's call, 2026-07-10: make it a standard, "revise the other panels to be consistent with these changes." The model: a doctor resolves its privilege at run time from three signals — passwordless sudo available (=sudo -n true=, which never hangs), a tty to prompt at, and whether it is the GUI panel. Four remedy classes: Auto (user-scope, reversible), Privileged (needs sudo — runs where passwordless, prompts on a CLI tty, degrades to Guide in a GUI with neither), Reboot-tail (run the applicable part, then instruct the reboot), and Guide (physical/BIOS/wait-for-upstream, nothing to run). Safety floor: every Privileged and Reboot-tail remedy defaults to Confirm or Arm tier, never silent Auto, because passwordless sudo is not consequence-free. -The shared helper is built (see the dated entry below). What remains is per-panel adoption: wire each doctor's remedies through =panelkit.privmodel.resolve()= and audit them against the Confirm/Arm floor, and reconcile maint's =priv.py= build/fire table with the model rather than leaving its implicit always-passwordless assumption. That wiring lives in the per-panel fix phases (net Phase 1, bt Phase 2, audio input/output), each needing a real privileged host to verify =--fix= end to end, so none is agent-solo. +The shared helper is built (see the dated entries below), maint is reconciled onto it, and net is wired: =classify.py= carries the =remedy_class= on its privileged verdicts and =doctor.py= resolves each through =panelkit.privmodel.resolve()= (net Phase 1, shipped 07-11). Adoption is the gate only — every panel's repair actions already exist; what adoption changes is whether and how an existing privileged action is allowed to run (RUN where passwordless, PROMPT on a CLI tty, GUIDE in a GUI), under the Confirm/Arm floor. What remains, checked against the tree 2026-08-25: bluetooth is part-wired (=bt/doctor.py= makes one =resolve(PRIVILEGED, ...)= call, no per-remedy classes yet — audit its individual fixes against the floor), and audio has nothing on the doctor side (pending the input-side spec). Each needs a real privileged host to verify =--fix= end to end, so not agent-solo. Craig's decision, 2026-07-12: maint's harmless-reclaim privileged remedies (the silent CLEAN UP set — paccache keep3, journal vacuum, coredump clean) STAY silent-auto. The reconciliation gives that class a sanctioned, documented exception to the confirm floor rather than forcing Confirm/Arm; the value of the floor holds for everything else. Where sudo is not passwordless, maint should degrade per the model (prompt on a tty, guide in a GUI) instead of hard-failing. @@ -1460,7 +1606,7 @@ The support machinery was deliberately kept for this task: =layout-navigate= and ** TODO [#B] Audit dotfiles/common directory :chore:dotfiles: :PROPERTIES: -:LAST_REVIEWED: 2026-07-14 +:LAST_REVIEWED: 2026-08-25 :END: Refiled from the archsetup task audit (2026-06-28), landed via ~/.dotfiles/inbox; the dotfiles content split into its own repo 2026-06-16 but the task tracking stays here per Craig (2026-07-02). Three parts: - Review all 50+ scripts in =~/.local/bin= and remove unused ones. @@ -1472,134 +1618,32 @@ ACTION before the kill pass: redo the reference scan to grep all invocation sour *** 2026-07-14 Tue @ 01:40:48 -0500 Built the audit evidence report Shipped as =docs/2026-07-14-bin-audit-evidence.org= in the dotfiles repo (260752a). 150 scripts bucketed: 69 keep (referenced or cron-driven), 74 kill candidates (zero references in the tree), 7 flagged (all dwm-tier, expected on a hyprland host). Config sweep: audacious and wofi configs are orphan candidates, ranger needs an install-vs-delete call (declared in archsetup but not installed on ratio). Shell history was too shallow (~700 lines) to prove by-hand disuse either way — the kill pass stays Craig's call in the parent task. -** TODO [#B] Waybar network module — custom/net :feature:waybar:network: +** TODO [#C] net vpn CLI subcommand :feature:network:dotfiles:solo: :PROPERTIES: -:LAST_REVIEWED: 2026-07-09 +:LAST_REVIEWED: 2026-08-21 :END: -Unifies the old wifi-no-internet indicator (was =[#C]=) and the network-manager -dropdown (was =[#B]=) into one =custom/net= module: a tested Python =net= engine -(nmcli + diagnostics), a thin bar indicator, and a GTK4 layer-shell panel. Code -lives in the dotfiles repo (hyprland tier + a =net/= package like pocketbook); -archsetup only installs deps. Secrets stay in NetworkManager's own store (no -separate credential store). The =captive= script becomes the diagnostics engine. -Full design, acceptance criteria, and the failure-mode coverage table: -[[file:docs/design/2026-06-29-waybar-network-module-spec.org][2026-06-29-waybar-network-module-spec.org]]. - -Phases below, dependency order. Engine/unit work is agent-verifiable (=unittest= -+ fakes on PATH, coverage via venv); the live-network and visual states need real -conditions, filed under "Manual testing and validation". - -*** 2026-06-29 Mon @ 20:19:11 -0400 Phase 1 shipped — indicator + console recovery -Shipped to the dotfiles repo (10 commits, =5254bd8=..=c095a22=, pushed to main). -The =net= engine is a src-layout Python package in-tree, imported by a bin shim -that resolves the stow symlink back to the repo — so it runs from a bare TTY with -no install, which the recovery path depends on. +=cli.py= in the dotfiles =net/= package has no vpn/tunnel parser, so everything +the panel can already do with tunnels has no command-line equivalent. Fold the +panel's existing tunnel operations into a =net vpn ...= surface mirroring what +the Tunnels sub-view does — bring an overlay up, take it down, report status. -Landed: =net status= (fast path, one nmcli call + sysfs, degraded fallback in -budget) + =net probe= (native captive probe, single-flight flock, atomic cache, -fresh/stale/expired/unknown classes, iface/SSID/UUID invalidation); =waybar-net= -replacing =custom/netspeed=, throughput → tooltip, CSS states in both themes + -live; =net diagnose= (read-only steps) + =net repair= (rfkill/reset/bounce/ -dns-test, cleanup-verified) + =net doctor [--fix]= with the four terminal -classifications; =net portal= + the =captive --probe-json= refactor; redacted -JSONL event log; Makefile recovery targets (=make online= etc.); =~/.config/net/ -config=. Verified live: =make net-status= reads the real wlp170s0 / @Hyatt_WiFi. +The operations themselves already exist and are tested: dotfiles =2d9d060= +probes tailscale / NM-wireguard / Proton, =21db05a= brings overlays up and down +from the panel, =31ba056= taught diagnose and doctor to understand tunnel +routes, and archsetup =2e40781= imports wireguard configs. This is a CLI surface +over shipped behavior, not new capability. -Airplane (Craig's call, option 1): =custom/net= absorbs only the *display* — net -reads the airplane-mode state file and shows an airplane state/glyph. The -airplane-mode toggle stays (it's a low-power mode — radios + CPU + brightness + -services — not a radio switch), now on =custom/net='s right-click + signal 15. -Deleted: =waybar-airplane=, =waybar-netspeed=, =custom/airplane=, their tests + -css. =airplane-mode= kept. - -Tests: 160 in =tests/net/= (fake nmcli/curl/rfkill/resolvectl/ping/getent/ -systemctl on a temp PATH; doctor-classification fixtures; degraded-under-slow- -nmcli benchmark) + the =captive= probe-mode tests; full dotfiles suite green (32 -suites). Coverage-gap pass via throwaway venv: pure modules ≥90% branch -(classify 100%), IO-error branches excused in the test docstring. -Deferred to Phase 2/3: archsetup deps (gtk4-layer-shell/python-gobject Phase 2, -speedtest-go-bin Phase 3 — not added before the code that needs them). -Verify (manual, live): see Manual testing and validation. +Graded [#C] rather than [#B]: the panel already does the job, so this is +convenience rather than a gap. It earns a bump if I find myself wanting tunnel +control from a bare TTY — which is the same recovery-path argument that made the +rest of =net= worth having as a CLI. -*** 2026-06-29 Mon @ 22:19:25 -0400 Phase 2 shipped — panel shell + connection management -Shipped to dotfiles (commits =4e7740f=..=24bcac5=, pushed). Engine: =net list= (saved -MRU + in-range wifi scan, infrastructure types filtered), =net up/down= (UUID-keyed, -mutation safety — keep prior link until target activates, classify wrong-password vs -generic, report auto-reactivation), =net add/edit/remove/rescan= (open + WPA-PSK; -enterprise activate-only; secret to NM's store, never our JSON/log — tested). +=:solo:= — the subcommand shape is obvious (it mirrors the panel), the =net= +package's fake-based harness covers the build and verify path, and archsetup owns +the dotfiles work end to end. Not =:quick:=: every prior net phase landed with +twenty-odd new tests and a review pass, so this runs past a spare moment. -Panel: a GTK-free PanelModel (selection, four state machines, the UX-flow enable -rules, terminal states) + a GTK4 gtk4-layer-shell window (=net panel=) anchored -top-right under the bar — Connections section with MRU list, active marked, signal -glyph, row-click select, Connect/Add/Forget/Rescan, confirm-on-forget, worker-thread -engine calls via GLib.idle_add. GTK imported lazily so the CLI/tests stay GTK-free. - -Bar interactions (settled with Craig over live iteration): left = =net-panel= toggle, -middle = =net portal=, right = =net-fix= (notify the doctor result when one-way; open -a terminal only when the outcome is fixable — the sudo/interactive case). Airplane on -Super+Shift+A. archsetup adds =gtk4-layer-shell= + =python-gobject= (this commit); -already on velox. - -Tests: 204 in tests/net (merge ordering/dedup, up/down mutation safety, no-secret-leak -on add/edit, panel model + state machines, gui row-format helpers). Full dotfiles suite -green (32 suites). Live-verified on velox: panel opens/toggles, list shows real 24 -profiles, right-click notification delivers (Craig confirmed). Phase 3 (diagnose/repair/ -speedtest IN the panel) is next; the engine for it already exists from Phase 1. - -*** 2026-06-29 Mon @ 22:43:40 -0400 Phase 3 shipped — diagnostics + speed test in the panel -Shipped to dotfiles (=91277cf=..=691abcb=) + archsetup (=48052d6=, speedtest-go-bin), -pushed. Engine: =net speedtest= (parses speedtest-go --json → ping from latency ns, -down/up from per-server byte rates; missing-backend / offline / malformed → error -envelope per the failure table). Panel grew a section switcher with four pages: -- Connections (Phase 2). -- Diagnose: =net diagnose= on a worker thread, each step a row (✓/✗/… glyph + title + - redacted evidence), read-only; Open-portal button when captive. -- Repair: "Get me online" (=net doctor --fix=) + tiers (rfkill/reset/bounce/dns-test) - + force portal. Confirmations in-panel with the spec's exact wording; the privileged - tiers run via =net-popup= terminal (where the sudo prompt + step output, incl. - cleanup-verified, show) — a panel has no tty, and pkexec would mean a prompt per op. -- Speed test: in-process =net speedtest= (no privilege → inline result: ↓/↑ Mbps + ping - + server), Run/Cancel (Cancel pkills the child), error envelope shown. - -213 net tests; pure helpers (step_indicator, format_speedtest) unit-tested. Full -dotfiles suite green (32 suites). One unverified assumption: speedtest-go's dl/ul unit -(taken as bytes/s; =BYTES_PER_SEC= flips it) — needs one real run vs a reference. The -in-panel repair streaming (vs terminal) is a named future polish once the GUI-privilege -story settles. - -The waybar network module ([#B] parent) is now COMPLETE through Phase 3. Phase 4 -(in-app help + user guide) and Phase 5 (VPN/WireGuard) remain as future work; the core -feature (indicator + recovery + panel + diagnostics + speed test) is done. -Verify (manual, live): see Manual testing and validation. - -*** 2026-07-09 Thu @ 16:32:54 -0500 Audit reconcile: Phase 4 is filed on the dotfiles side, waiting on them -The dotfiles project accepted the Phase 4 handoff and filed it as a =[#C]= task in their own =todo.org= (their note, 2026-07-08 16:56): the help-text audit + panel help affordance, the user-guide/README, and the ratio rollout doc. Not started there. They ping when it lands, and this task's Phase 4 child closes then. Nothing to do here meanwhile. - -*** 2026-08-17 Mon @ 19:57:42 -0700 Landed on the dotfiles side; the block is cleared -dotfiles shipped it as =138da7b= and closed its own task, so this one closes -with it and the =:blocked:= tag comes off. Found by checking their =todo.org= -rather than waiting for the ping — their close-out note says "archsetup pinged -so its Phase 4 task can close", so the handoff worked and only this end was -left open. - -All three acceptance criteria are met on their side: the help audit found and -fixed a stale =net repair= action list (nine of nineteen actions were named; -both the CLI help and =repair.py='s docstring now generate from the ACTIONS -registry), =net/README.md= covers every command plus the recovery targets, and -the ratio rollout is documented with both daily drivers verified current. - -They split the panel help affordance out rather than inventing it — no sibling -panel has one, so its shape is a design call. It is tracked on their side, not -here. - -Original deliverable, for the record: in-app help (=net --help= + per-command, -panel help affordance); README/user-guide; archsetup Hyprland dep install -(=gtk4-layer-shell=, =python-gobject=, =speedtest-go-bin=); ratio manual dep + -stow step. Handed off 2026-07-04 with the archsetup deps already confirmed -installed. - -*** 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. +Carved out of the =custom/net= umbrella when that closed on 2026-08-21. ** TODO [#B] Local offline LLM runtime + per-host model cache :tooling:llm: :PROPERTIES: @@ -1641,7 +1685,7 @@ Acceptance: fresh VM install of the ratio profile reaches an endpoint on =:8081= ** TODO [#B] Test + CI infrastructure :test: :PROPERTIES: -:LAST_REVIEWED: 2026-07-13 +:LAST_REVIEWED: 2026-08-25 :END: Umbrella for the test-harness and CI-automation buildout. Consolidated from the 2026-06-28 task audit: these were scattered top-level tasks circling one effort, re-homed as children so the work reads as a unit. Each child ships independently and keeps the priority it carried before. No CI runner exists yet, so the CI/CD-pipeline child gates several of the others. @@ -1713,7 +1757,7 @@ Keep test runs performant as installs and post-install tests grow (target < 2 ho :LAST_REVIEWED: 2026-05-21 :END: Proactive monitoring integrated with testing -*** TODO [#B] Fix VM cloning machine-ID conflicts for parallel testing +*** TODO [#C] Fix VM cloning machine-ID conflicts for parallel testing :no-sync: :PROPERTIES: :LAST_REVIEWED: 2026-05-21 :END: @@ -1724,9 +1768,9 @@ 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 [#B] Review undeclared ratio packages for installer inclusion :chore: +** TODO [#C] Review undeclared ratio packages for installer inclusion :chore: :PROPERTIES: -:LAST_REVIEWED: 2026-07-09 +:LAST_REVIEWED: 2026-08-21 :END: Triggered by the 2026-06-14 =make package-diff= run on ratio: 62 packages are installed but not declared in archsetup. Stripped of the structural buckets — pacstrap base/boot/kernel (base, linux*, grub, efibootmgr, sudo, btrfs-progs, fwupd, logrotate, ex-vi-compat, linux-lts-strix, zram-generator), the =make deps= VM set (qemu-full, virt-manager, virt-viewer, libguestfs, bridge-utils, dnsmasq, archiso), and the yay bootstrap — these 40 remain. Check the ones to add to the installer, then rerun =make package-diff= to confirm they clear. @@ -1775,6 +1819,30 @@ Some entries are libraries likely pulled in as dependencies (blas-openblas, open - [ ] webkit2gtk - [ ] whisper.cpp +*** 2026-08-21 Fri @ 14:28:18 -0700 Dropped to [#C], and the sharper measurement is on velox now +Re-graded [#B] → [#C]. Not a change of mind about the value — nothing has been +ticked since I filed it on 2026-06-14, across two full cycles, and by my own +scheme [#B] means "this cycle" while [#C] is the parking lot. The grade should +say where it actually sits. + +The premise also moved. This list measures ratio, which carries years of +accumulated manual installs tangled up with whatever archsetup put there, so a +package being undeclared says little about whether it matters. Velox is the +better instrument now: rebuilt from archsetup on 2026-08-13 and working, so a +=make package-diff= there compares what the installer declares against what a +machine actually needs, with only days of drift on top. Re-run it on velox +before walking these forty by hand. + +Worth noting the empirical result already came in. The gaps that actually hurt +after that rebuild — rulesets never cloned, the .emacs.d systemd units never +linked, the missing =*.local.*= configs — surfaced on their own, and not one of +them is on this list. That is evidence about what this kind of list catches. + +Related but distinct: =[#B] Installed-package drift audit= below builds the tool +for the opposite direction (declared but missing, and provider substitutions). +If that lands first it plausibly subsumes the detection half of this one, leaving +only the include/ignore judgment. + ** TODO [#B] Installed-package drift audit :chore:packages:solo: :PROPERTIES: :LAST_REVIEWED: 2026-08-02 @@ -1797,7 +1865,7 @@ machine state. ** TODO [#B] Security hardening + audit :security: :PROPERTIES: -:LAST_REVIEWED: 2026-07-14 +:LAST_REVIEWED: 2026-08-25 :END: Umbrella for the security-hardening and audit effort. Consolidated from the 2026-06-28 task audit, re-homing the scattered security tasks as children so the work reads as a unit. Each child ships independently and keeps its prior priority. @@ -1813,12 +1881,12 @@ Umbrella for the security-hardening and audit effort. Consolidated from the 2026 **** TODO [#B] Implement port scanning check **** TODO [#B] Create security posture verification script **** TODO [#B] Set up intrusion detection monitoring -*** TODO [#B] Document threat model and mitigations within 6 months +*** TODO [#B] Document threat model and mitigations :PROPERTIES: :LAST_REVIEWED: 2026-05-21 :END: Identify attack vectors, what's mitigated, what remains -*** TODO [#B] Complete security education within 3 months +*** TODO [#B] Security education :PROPERTIES: :LAST_REVIEWED: 2026-06-24 :END: @@ -1881,14 +1949,352 @@ each on the live machine rather than reading the list back: The suspend work itself is untouched — no kernel parameter, no drain measurement. Only the riders moved. +*** 2026-08-25 Tue @ 11:57:48 -0600 Logged the two Aug 23 hibernate-leg failures +suspend-then-hibernate failed its hibernate leg twice on 2026-08-23 (21:06 and +23:29): "Failed to put system to sleep. System resumed again: Device or +resource busy". Noticed during the 08-24 Lua-port session and parked there; +filed here at Craig's direction so the sleep task carries it. Nothing +diagnosed yet — first step is =journalctl -b -1 -u systemd-suspend-then-hibernate= +around those timestamps to see which device reported busy. + +*** 2026-08-26 Wed @ 16:16:08 -0600 Diagnosed the hibernate battery drain: three separate faults, one task each +Craig hibernated twice in ten days and found the battery dead both times. Read +all 39 boots since the 08-13 reinstall, upower's charge history +(=/var/lib/upower/history-charge-Framewo-55-03F5.dat=, root-only, starts +08-19), sysfs, and the scripts inside =/efi/EFI/ZBM/zfsbootmenu.efi=. + +Hibernate is configured right and has worked: five hibernate+resume cycles +since reinstall (08-13, 08-17 14:31, and three suspend-then-hibernate cycles on +08-20/21). The two fatal events are the two overnight explicit +=systemctl hibernate= runs, 08-17 22:20 and 08-21 19:58. Both journals end at +"PM: hibernation: hibernation entry"; the next power-ons (08-18 10:13, 08-22 +17:22) were fresh boots whose resume hook found no image, no later swapon +reported a leftover suspend signature, and on 08-22 the battery read 2% at +power-on. The 08-23/24 night was on AC and not a battery death (three suspends +failed to enter, machine awake all night at the charge limit; the 09:57 end was +three power-key presses and a hard cut at 63%). The 08-19 death was the +caffeine/hypridle one already diagnosed. + +Three faults, tracked as the children below: +- Hibernate hard-freezes on entry (documented on Framework 13 AMD incl. Ryzen + AI 300: black screen, never powers off, intermittent, amdgpu-side). Fits + everything: the freeze precedes the swap signature, so the next boot is + fresh, and a frozen laptop at ~5 W empties 44.7 Wh in ~8 h. Unprovable from + logs by nature; the alternative (completed hibernate, unattended power-on to + the ZBM passphrase prompt) predicts a surviving image, which neither boot + had — see the VERIFY. +- ZFS ARC starves the hibernate image: "Image allocation is 8118265 pages + short" today 14:09, "390678 pages short" 08-20 09:14. ARC 58 GB of 93, + =zfs_arc_max=0= so =c_max= = RAM − 1 GiB; the kernel must free RAM − + =image_size= (37.4 GB) ≈ 56 GB. systemd falls back to s2idle and retries + every 90 min, so suspend-then-hibernate never actually hibernates. +- The SD card reader (090c:3350, =sda=, no media) can block suspend entirely: + "Freezing remaining freezable tasks failed after 20s (wq_busy=1)", pending + =disk_events_workfn= on =events_freezable_pwr_efficient=, three times on + 08-23/24. On battery that is a dead laptop by morning. + +Mistake worth remembering: =journalctl --since … -k= silently limits itself to +the current boot (=-k= implies =-b=); cross-boot kernel facts need +=_TRANSPORT=kernel= or an explicit =-b=. + +*** TODO Hibernate entry freeze — confirm under observation, then mitigate :bug:velox:hibernate: +Interim rule until this closes: do not hibernate unattended on battery. Shut +down, or suspend on AC. + +What is known: the two dead-battery hibernates match the Framework 13 AMD +"hard freeze on hibernate entry" reports (community threads 69516 and 53860, +Arch bbs 293242): screen black, power LED on, never powers off; intermittent +(one report: every 6–7 cycles); TTM/amdgpu warnings; improved by newer +=linux-firmware=; no confirmed fix. Board A9, BIOS 03.05, linux-lts 6.18.46, +=amdgpu.dcdebugmask=0x610= already on the cmdline. + +Confirm first: the "Hibernate entry freeze: five observed cycles on AC" test +under Manual testing and validation. A failed cycle there is the proof the +journal cannot give. + +Mitigations to try in order once confirmed, one at a time, re-running the +cycles after each: (1) =linux-firmware= at current, then =linux-firmware-git= +if the freeze persists; (2) =/sys/power/disk= = =shutdown= instead of +=platform= (a systemd =HibernateMode=shutdown= drop-in), which skips the ACPI +S4 path some Framework users found hanging; (3) a newer kernel (=linux= vs +=linux-lts=) for the amdgpu delta; (4) unload =mt7925e= in a pre-sleep hook +if the freeze survives the first three. Not =:solo:=: each cycle needs a +person watching the power LED. + +*** TODO ZFS ARC starves the hibernate image — cap it or shrink it pre-hibernate :bug:zfs:velox:solo: +The arithmetic: the kernel preallocates RAM − =image_size= pages before +snapshotting; with 93 GB RAM and the default =image_size= (2/5 of RAM, +37.4 GB) that is ~56 GB, and only free memory plus what shrinkers give back +counts. ARC was 58 GB today and the ZFS shrinker released little inside the +preallocation window, so it came up 31 GiB short. Nothing in +=/etc/modprobe.d/= sets =zfs_arc_max=. + +Two fixes, either or both: +- Cap the ARC: =options zfs zfs_arc_max=<bytes>= in =/etc/modprobe.d/zfs.conf= + (16 GiB leaves ~70 GB reclaimable) plus =echo <bytes> > + /sys/module/zfs/parameters/zfs_arc_max= for the running system. +- Or a =/usr/lib/systemd/system-sleep/= pre hook for the hibernate class that + lowers =zfs_arc_max=, waits for =size= in + =/proc/spl/kstat/zfs/arcstats= to fall, and restores it post-sleep. Keeps + the big ARC while awake. +- Raising =image_size= toward the kernel's ceiling (about half of RAM) also + shrinks the demand; combine with the cap. +Install it through archsetup so the next rebuild carries it (velox-only: ratio +has no swap partition). + +Verify: after the change =arcstats size= drops below the cap within seconds; +then one live suspend-then-hibernate cycle on AC with the delay temporarily +short shows "hibernation exit" and no "Image allocation … short" line in the +journal. That live cycle rides the entry-freeze test above; the ARC half is +checkable without it. + +*** TODO SD card reader media polling can block suspend :bug:velox:solo: +The reader (USB 090c:3350 Silicon Motion, =sda=, "Media removed, stopped +polling" at boot yet =events_poll_msecs= = −1 → default 2000 ms) left a +=disk_events_workfn= item pending on the freezable workqueue three times on +08-23/24, and the freezer gives up after 20 s: "Failed to put system to +sleep … Device or resource busy". Same symptom as the flaky expansion slot in +the ribbon task; a stalled poll never completes. + +Fix: a udev rule for that vendor/product setting +=ATTR{events_poll_msecs}="0"= (or =block.events_dfl_poll_msecs=0= on the +cmdline if every removable disk should stop polling), shipped by archsetup. +Verify with =rtcwake -m mem -s 20= on AC: journal shows "PM: suspend entry" +and "PM: suspend exit" with no "Freezing remaining freezable tasks failed", +and =/sys/block/sda/events_poll_msecs= reads 0 after a replug. Pulling the +card before sleeping is the manual workaround meanwhile. + +*** VERIFY After the 08-17 and 08-21 dead batteries, did the first power-on hang, or boot straight to a fresh login? +Decides between the two mechanisms. An entry freeze leaves no image, so the +next power-on boots straight through. A completed hibernate followed by an +unattended power-on (phantom power button, ZBM passphrase prompt until dead) +leaves the image in place, so the next power-on would try to resume — and the +only way that ends in the fresh boots the journal shows is a hung resume that +got force-cut. If both power-ons went straight to a fresh login, the freeze +is the answer. + +** TODO [#A] Port Hyprland config to Lua before 0.57 drops .conf support :hyprland:dotfiles: +SCHEDULED: <2026-08-25 Tue> +:PROPERTIES: +:LAST_REVIEWED: 2026-08-24 +:END: +Hyprland prints "You are using the .conf config format, support for which will be +removed in Hyprland 0.57" at every start. Installed and in =extra= is 0.56.2-1, so +the *next* release breaks the config. Craig's call 2026-08-24: port now, under no +time pressure, rather than pin the package or wait for the upgrade to force it. + +STATE (2026-08-24): built and verified in a nested compositor, *not deployed*. +I deployed it to the dotfiles tree this afternoon and rolled it back the same +hour on Craig's call — the switch had not been checked on real hardware and the +machine has to stay usable. The dotfiles repo is untouched at =8f692f5= and the +live config is the original =hyprland.conf=; =hyprctl reload= after the rollback +returned zero configerrors and the velox host override is applied +(=xwayland:force_zero_scaling= false), so the per-host chain is intact. + +Everything needed to redeploy is in =working/hyprland-lua-port/= with a README +carrying the step-by-step: the three =.lua= deliverables, plus the two reader +changes saved as patches (=reader-changes-for-lua.patch= for dotfiles' +=dotfiles-validate= and three test suites, =test-desktop-for-lua.patch= for +archsetup's post-install checks). Both patches were verified to apply clean +against their repos, and every assertion in them was mutation-tested — each one +confirmed to go red when the property it guards is removed. Replay them rather +than rewriting the assertions. + +Also settled along the way: =themes/dupre/hyprland.conf= is dead. Nothing sources +it, no apply script exists, and it has silently drifted from the live config +(=dab53dff= / =2c2f32ff= against =daa520ff= / =444444ff=). It needs no porting. + +HOW IT WAS BUILT. =hyprlang2lua= (github.com/EIonTusk/hyprlang2lua, AUR 0.7.1-1) +converts hyprlang to the 0.55+ Lua format and preserves comments. Built from +source into a scratchpad with the local Go rather than installing the AUR package +— one dependency, and no PKGBUILD executed. Run with =--no-merge=, which emits +each config section as its own =hl.config()= call at its original position; the +default merges them into one hoisted call, which both scrambles comment placement +and puts the =conf.d= source glob BEFORE the config it must override. + +THREE DEFECTS THE CONVERTER INTRODUCED, all fixed by hand: +1. Source glob emitted before the merged config block, silently reversing every + per-host override — including velox's =force_zero_scaling = false=, which is + the 2026-08-19 Qt scaling fix. =--no-merge= plus moving the glob to the last + line fixes it. +2. =bind = CTRL $mod, S= became ="CTRL" .. mod .. " + S"= → ="CTRLSUPER + S"=; + same for =CTRL ALT $mod, K=. Proven fatal, not merely odd: Hyprland answers + =hl.bind: failed to parse key string: Unknown keysym: "CTRLSUPER"=. Two dead + keybinds. +3. Super+RETURN is an =exec= of a shell pipeline; the converter pattern-matched + the =hyprctl dispatch layoutmsg= prefix and swallowed =&& sleep 0.05 && ...= + as the layoutmsg argument. That sleep is the one proven load-bearing 19/19. + +Also rebuilt the autostart section: the generator hoists every =exec-once= into +one block at the end and leaves the comments stranded where the commands were. +Each command now sits under its own comment again via =at_start= / =at_shutdown= +/ =at_reload= collectors that the =hl.on()= handlers at the bottom replay. + +VERIFIED by running both configs in a nested Hyprland on a headless output and +diffing runtime state, not by reading: 38 config keys identical (only type +*rendering* differs — =bool: true= where hyprlang prints =int: 1=); +=xwayland:force_zero_scaling= false on both sides, so the host override still +wins; 103 binds registered on both sides with 100 of 103 matching exactly; +autostart list byte-identical to the 16 =exec-once= lines in order; zero +=configerrors=; and no ERR/WARN line in the ported run that is absent from the +original run. + +KNOWN DELTA — the three =bindm= binds. hyprlang reports =mouse: true=, the Lua +path reports =mouse: false=, and the raw bind struct confirms the flag is unset +rather than merely unreported. Not a transcription error: the wiki documents +exactly the spelling used (=hl.bind("ALT + mouse:272", hl.dsp.window.drag(), +{ mouse = true })=), and all four candidate spellings were tested — none sets it. +Reads as a gap in 0.56.2's Lua config manager. Kept the documented spelling: it +is correct upstream, harmless now, and starts working when the gap closes. Per +the wiki that flag is what makes the action fire *while held*, so Super+drag to +move a window may fire once instead of tracking. Settle it in five seconds after +switching; worth an upstream report if it survives 0.57. + +WHAT REMAINS: +1. *Craig decides when to switch.* The port is ready to go in; it has not been + run on real hardware. The gate is the "Hyprland Lua config" test under Manual + testing and validation, which is written to be run right after the switch. +2. *Four review gates travel with the redeploy*, all written up in that README: + exclude any in-repo =retired/= dir from =dotfiles-validate= (its find globs + across slashes and would validate the dead config); add tests for the new + =dotfiles-validate= Lua branch (25 lines, currently zero coverage — proven + vacuous, since stubbing both regexes to =NEVERMATCHES= still passes 15 tests); + guard that =hl_source_glob= stays the last statement (the one invariant the + per-host layer rests on, and archsetup's VM cannot catch it); and sweep the + ~15 prose comments still naming =hyprland.conf=, of which + =waybar-reserve:12= is the load-bearing one. +3. *Redeploy per =working/hyprland-lua-port/README.org=*, which carries the eight + steps and the two traps that bit on 2026-08-24: =make restow hyprland= aborts + on the pre-existing =obsbot-wb-guard.service= conflict in =common= (restow + =hyprland= and the host package individually), and the running Hyprland + rewrites a stub =hyprland.conf= within a second of the symlink vanishing + (silence it with =hyprctl keyword misc:disable_autoreload 1=, stow, set back + to 0). +4. *Push dotfiles before committing archsetup.* One-directional and load-bearing: + archsetup's post-install suite asserts =~/.config/hypr/hyprland.lua= and the + installer clones the dotfiles *remote* (=archsetup:1481=), so a local commit is + not enough. Confirm with =git ls-tree -r origin/main --name-only | grep + hypr/hyprland.lua=. +5. *Do not leave the =.conf= beside the =.lua= as a rollback.* With both present + Hyprland 0.56.2 loads the =.lua= — proven in a nested instance with a fixture + whose =.conf= set =gaps_in=11= and =.lua= set =77=; the result was 77. A + =.conf= left in place buys nothing and only obscures which file is live. Move + it out of the stow package instead. +6. *=bindm='s missing =mouse= flag is worth an upstream report* if it survives + 0.57. Documented spelling, four variants tested, flag never set. + ** TODO [#B] Manual testing and validation :test: :PROPERTIES: -:LAST_REVIEWED: 2026-07-09 +:LAST_REVIEWED: 2026-08-23 :END: -Craig's standing checklist of everything that isn't agent-verifiable. Each child is one test in the =verification.md= shape (title, what we're verifying, steps, Expected). A child that fails gets its actual behavior written under it and is promoted to a top-level TODO. 44 checks pending as of the 2026-07-09 audit. +Craig's standing checklist of everything that isn't agent-verifiable. Each child is one test in the =verification.md= shape (title, what we're verifying, steps, Expected). A child that fails gets its actual behavior written under it and is promoted to a top-level TODO. 62 checks pending as of the 2026-08-23 review — up from 44 at the 2026-07-09 audit, so the queue has gained 18 in six weeks and nothing has drained it. A checklist that only grows is on its way to being where tests get filed rather than run; if the next review finds it higher again, the container needs a scheduled sweep rather than another re-stamp. Priority and type tag added by that audit: the task carried neither, which kept the project's largest live container out of the agenda entirely. +*** Hyprland Lua config: does the real desktop come up, and does Super+drag track? +What we're verifying: that the Lua port drives a real Hyprland session the way +the .conf did, and specifically whether the one known delta — the three =bindm= +binds losing their =mouse= flag — actually costs anything. A nested compositor +proved 38 config keys, 103 binds and the host-override chain identical, but it +cannot test real input devices or a real DRM display. + +PRECONDITION: run this *only after* redeploying the port per +=working/hyprland-lua-port/README.org=. As of 2026-08-24 the port is rolled back +and the live config is the original =hyprland.conf=, so running this now just +confirms the old config — which is not what it is for. Run it on velox; the stub +check in the last block is velox-specific. +- Restart Hyprland (log out and back in, or =hyprctl dispatch exit= from a TTY). +- Confirm the desktop comes up: waybar present and not off-screen, wallpaper + restored, dunst notifications working. +#+begin_src sh :results output +# Which config did it actually load, and did anything fail to parse? The log is +# per-instance under the runtime dir, not in ~/.local/share. Resolve the newest +# instance dir rather than reading $HYPRLAND_INSTANCE_SIGNATURE: Emacs runs as a +# daemon that survives the logout in step 1, so a block run from it can still be +# carrying the PREVIOUS session's signature. That path is gone after the restart, +# grep prints nothing, and an empty result under "Expected: names hyprland.lua" +# reads as "the port failed" when it in fact succeeded -- the worst possible +# wrong answer at exactly the wrong moment. +log="$(\ls -td "$XDG_RUNTIME_DIR"/hypr/*/ | head -1)hyprland.log" +echo "reading: $log" +grep -iE '\[cfg\].*(lua|legacy)' "$log" | tail -3 +hyprctl configerrors +#+end_src +Expected: the log names hyprland.lua, and configerrors is empty. +- Hold Super and drag a window with the left mouse button. +Expected: the window tracks the pointer continuously while Super is held. If it +jumps once and stops, the =bindm= =mouse= flag gap is real and costs the drag — +write that here, promote to a top-level TODO, and report upstream. +- Hold Super and drag with the right mouse button (resize), same check. +- Walk the keymap: the launcher, terminal, browser, screenshot chords, the panel + family (Super+Shift+B for bluetooth), workspace switching, layout cycling. +Expected: every chord does what it did before the port. +#+begin_src sh :results output +# The stub .conf should stay gone now that Hyprland started from the .lua. +# Refuse to touch a symlink: on a host that has not been through this port yet, +# ~/.config/hypr/hyprland.conf is still the stow link to the real config, and +# deleting it would report "stays gone" as a pass while having broken the desktop. +f=~/.config/hypr/hyprland.conf +if [ -L "$f" ]; then + echo "REFUSING: $f is a symlink (a live stowed config), not the stub." +elif [ -f "$f" ]; then + rm -f "$f"; sleep 2 + [ -e "$f" ] && echo "REGENERATED — still stubbing" || echo "stays gone" +else + echo "already absent — nothing to do" +fi +#+end_src +Expected: "stays gone". If it regenerates, Hyprland is still resolving its config +to the .conf path and the port is not actually live — stop and investigate. + +*** Lock screen after a real sleep: is the clock frozen, or is all of hyprlock frozen? +What we're verifying: which of two very different faults produces the stale +lock-screen clock Craig sees after waking the machine. Three isolated tests on +2026-08-24 all failed to reproduce it — hyprlock v0.9.6 repainted correctly +within one second of a display power-cycle, of a three-minute SIGSTOP, and of +both together with the =path = screenshot= background. So the cause needs a real +suspend on real AMD hardware, which only Craig can run. + +The distinguishing question is whether *anything* on the lock screen is alive. +A frozen clock with live input is a rendering bug in the clock. A frozen clock +with dead input means hyprlock is hung and the compositor is simply holding its +last pre-suspend frame — a different bug, in the crash/hang recovery layer. + +Run this the next time the machine has actually slept (30 min idle, or a lid +close), not on a screen you just locked by hand. + +- Wake the machine and do NOT type your password yet. +- Read the clock. Note the time it shows and the real time. +- Type one character into the password field. +Expected (record which): either the dots appear normally — input is alive, only +the clock is stale — or nothing appears at all, in which case hyprlock is hung. +- Keep watching for a full 90 seconds without touching anything else. +Expected (record which): the clock corrects itself to the current time within a +minute, or it never changes at all. +- Note whether the blurred background looks like your desktop or like a + previous lock screen (a lock screen photographed behind a lock screen means a + second hyprlock was started on top of the first). + +Expected: enough to pick one of three — stale-then-corrects (a repaint-interval +fix), frozen-with-live-input (a rendering fix), or frozen-with-dead-input +(hyprlock hung; the =screen-lock= watchdog only catches a hyprlock that +*exits*, and its own header says it is blind to one that hangs). + +*** Lock keybind has no crash or hang recovery +What we're verifying: that a hand-lock is as recoverable as an idle lock. +=hyprland.conf:495= is =bind = $mod, ESCAPE, exec, hyprlock=, which runs the +binary directly. hypridle's =lock_cmd= routes through =screen-lock=, which +relaunches a hyprlock that exits non-zero; the keybind bypasses that entirely. +=~/.local/var/log/screen-lock.log= does not exist on velox, so the watchdog has +never recorded a relaunch — consistent with it rarely being in the path at all. + +- Lock with Super+Escape. +- From another tty (ctrl+alt+F3), log in and run: =pkill -x hyprlock= +- Return to the graphical tty. +Expected: with the keybind as written, the session is left locked with no client +and Hyprland draws its "lockscreen app died" screen. If instead a fresh password +prompt appears, something is already relaunching it and the gap is closed. + *** Clock/DNS deadlock: does the next abrupt power loss strand velox again? What we're verifying: that the machine survives an RTC reset unattended. Not the coin cell, which is new with the 2026-08-13 mainboard and is ruled out. The RTC @@ -2507,6 +2913,32 @@ NOTE (2026-07-04 audit): the "four-tab panel" framing predates the instrument-co - Expected: ↓/↑ Mbps + ping + server shown inline. - Byte-rate→Mbps unit: VERIFIED 2026-06-30 (velox). Raw =speedtest-go --json= dl_speed read ~3.66M, unambiguously bytes/s (29 down / 80 up Mbps); =net speedtest= reported 33.62 / 77.99 through the wired path. =BYTES_PER_SEC = True= + =* 8 / 1e6= are correct, no flip needed. Remaining here is only that the panel renders the inline result. +*** Hibernate entry freeze: five observed hibernate cycles on AC +What we're verifying: whether velox hard-freezes on hibernate entry (black +screen, power LED on, never powers off), the documented Framework 13 AMD +failure that fits both dead-battery events. The journal cannot show it; a +person watching the LED can. +- Plug in AC, lid open, nothing important unsaved. +- Note the cycle number, then hibernate from a terminal: +#+begin_src sh :results output +date; systemctl hibernate +#+end_src +- Watch: the screen goes black; within about two minutes the power LED goes + off and the fans stop. +- Press power, enter the ZBM passphrase, and confirm the same session comes + back (windows still open). +- Check that the cycle was a real hibernate and not a fallback: +#+begin_src sh :results output +journalctl -b -o short-iso | grep -E "systemd-sleep|hibernation (entry|exit)|Image allocation|Failed to put" | tail -6 +#+end_src +- Repeat until five cycles are logged. +Expected: all five cycles power off within two minutes and resume into the +same session, with "hibernation exit" and no "Image allocation … short" line. +A cycle where the screen stays black with the power LED on for more than five +minutes is the entry freeze: hold power for 10 s, and write down the cycle +number and whether the keyboard backlight was lit. A cycle that instead comes +straight back with "Cannot allocate memory" is the ARC task, not a freeze. + ** DOING [#B] Prepare for GitHub open-source release :PROPERTIES: :LAST_REVIEWED: 2026-08-17 @@ -2637,9 +3069,9 @@ Rewrote the bare =if $var= boolean conditionals (=show_status_only=, =fresh_inst *** 2026-05-26 Tue @ 15:27:09 -0500 eval task moot — the line-434 eval is gone, the survivor is deliberate Verified: the only =eval= left in =archsetup= is line 578 in =retry_install=, and it's intentional and documented — it captures =$?= directly from =eval "$cmd"= to dodge the if-compound-swallows-exit-code trap. Replacing it with an array would reintroduce that bug. The line-434 eval this task pointed at no longer exists. Nothing to change. -** TODO [#B] The audio doctor never checks the microphone :bug:audio: +** TODO [#C] The audio doctor never checks the microphone :bug:audio: :PROPERTIES: -:LAST_REVIEWED: 2026-07-13 +:LAST_REVIEWED: 2026-08-25 :END: The classifier is output-only. =diag.probe_semantic= already collects =default_source= and =default_source_present=, and =classify.py= reads neither: the word "source" appears once in the whole module, in the graph row that counts them. So a muted mic, a default source naming an unplugged device, or a mic at zero volume all classify as =healthy=, and the verdict prints "the default output is present and audible" while the input side goes unexamined. Found 2026-07-10 while asking whether the doctor would have caught Chrome losing the mic. It would not have. @@ -2649,13 +3081,15 @@ Work: mirror the sink rules onto the source. =probe_semantic= gains =default_sou Two things not to get wrong. An absent microphone is legitimate on a desktop, so "no input devices" must never be a fault the way =no-output-devices= is. And a monitor source is a legitimate default source (recording desktop audio), which is why =probe_semantic= passes =include_monitors=True= — inheriting the panel's display filter here would call a working setup broken. -Specced 2026-07-10 after discussion with Craig, and the design grew past the original gap: [[file:docs/specs/2026-07-10-audio-doctor-input-side-spec.org][docs/specs/2026-07-10-audio-doctor-input-side-spec.org]] (DRAFT, four decisions open). A doctor key per direction, a kernel-level capture probe below PipeWire, PTT-aware muting, and a direction-aware guard. The precedence question the build would have faced is gone: a doctor per direction means the user's press says which side they came to fix. +Specced 2026-07-10 after discussion with Craig, and the design grew past the original gap: [[file:docs/specs/2026-07-10-audio-doctor-input-side-spec.org][docs/specs/2026-07-10-audio-doctor-input-side-spec.org]] (DRAFT, three decisions open as of 2026-08-25). A doctor key per direction, a kernel-level capture probe below PipeWire, PTT-aware muting, and a direction-aware guard. The precedence question the build would have faced is gone: a doctor per direction means the user's press says which side they came to fix. 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. +Grading (2026-08-25 review): Major severity — the doctor's verdict is silently wrong for a whole direction, workaround is checking the mic by hand — × "some users, sometimes" (mic faults are occasional) = P3 = [#C]. Was held at [#B] ungraded; regraded by the matrix. + ** TODO [#C] Weather chip color signals unclear + unenforced :bug:dotfiles:waybar:weather: :PROPERTIES: -:LAST_REVIEWED: 2026-07-21 +:LAST_REVIEWED: 2026-08-26 :END: From the roam inbox (2026-07-20): the shipped Waybar weather chip's comfort coloring reads as noise — it shows amber for no clear reason, and some items are bolded, which isn't a legible signal. Craig's intended scheme (every item except the arrow key colored by whether the weather is comfortable; NO bold or italic anywhere): - Normal — all text white: temp in 60-85; condition sunny/clear/etc. @@ -2704,7 +3138,7 @@ manual-testing entry; everything else verifies with fakes. ** TODO [#C] Net panel speedtest history :feature:dotfiles:network: :PROPERTIES: -:LAST_REVIEWED: 2026-07-14 +:LAST_REVIEWED: 2026-08-25 :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. @@ -2720,13 +3154,13 @@ Reproduced in ~1 minute of install: =dkms install zfs/2.3.3 -k 6.18.38-2-lts= ex ** TODO [#C] Waybar collapse control: replace the triangle glyph :feature:waybar: :PROPERTIES: -:LAST_REVIEWED: 2026-07-14 +:LAST_REVIEWED: 2026-08-26 :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 +:LAST_REVIEWED: 2026-08-26 :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. @@ -2860,1152 +3294,265 @@ Verified rather than assumed: =~/.telega/telega-server.log= carries zero =tdat_p Re-graded =[#C]= → =[#D]= per the bug matrix. There is no defect to fix here; it is a watch item with a named tell, and the severity × frequency read is cosmetic (host coredump noise on a metric we own) × rare edge case → P4 → =[#D]=. It stays on the list only so the tell isn't lost. The maintenance console's coredump metric flagged telega-server on ratio (8 coredumps) and velox (18). Root cause was a version skew: the Dockerized =zevlg/telega-server:latest= is frozen at the 2026-06-05 build while the installed elisp lagged at 20260513, so the newer server's plist parser choked on the older elisp's output. .emacs.d fixed it by upgrading telega to 20260706 on both machines (docker kept, =docker pull= is a no-op against the frozen image). Host-coredump pollution should stop. If zevlg later pushes a =:latest= that outruns the installed elisp, the skew and the coredumps recur — the tell is a fresh =tdat_plist_value:500= assertion in =~/.telega/telega-server.log=. The durable escape is a host-native pinned TDLib build, at the cost of an AUR source build. -* Archsetup Resolved - -** DONE [#A] Velox boot recovery — no kernel in BE :bug:velox:zfs: -CLOSED: [2026-07-19 Sun] -Recovered. Velox boots linux-lts 6.18.38 and is back on the tailnet (up 1d+, /boot holds initramfs-linux-lts.img). The pre-pacman ZFS snapshot rollback restored the kernel from the ZBM recovery shell. -Velox won't boot: ZBM prompts for the passphrase, unlocks, then reports no bootable environment with a kernel. Cause: an interrupted kernel =-Syu= removed the old kernel and never installed the new one — /mnt/be/boot (from zroot/ROOT/default) holds ONLY intel-ucode.img; vmlinuz-linux + both initramfs are gone. /boot lives inside zroot/ROOT/default (no separate boot dataset), so root-dataset snapshots capture it. - -Status 2026-07-15: a first rollback attempt did NOT fix it (square zero after reboot) — suspected typo in the snapshot name, so the rollback likely errored and did nothing. NOT verified. Next session: verify state in the ZBM recovery shell BEFORE any reboot. - -Recovery lever: the pre-pacman ZFS snapshot hook (live on velox since 2026-06-29) snapshots zroot/ROOT/default@pre-pacman_<ts> before every pacman transaction. The newest =pre-pacman_<ts>= predating the failed upgrade holds the intact old kernel — roll back to it. - -Morning steps (Craig at velox ZBM → recovery shell, Ctrl+R): -#+begin_src sh -# 1. pool writable + key loaded -zpool get readonly zroot -zfs get -H -o value keystatus zroot/ROOT/default -# if readonly=on: zpool export zroot && zpool import -f -N zroot -# if keystatus=unavailable: zfs load-key zroot - -# 2. list snapshots — COPY THE EXACT NAME (the typo bit here last time) -zfs list -t snapshot -o name,creation zroot/ROOT/default | grep pre-pacman - -# 3. see current /boot state (read-only mount) -umount /mnt/be 2>/dev/null; mkdir -p /mnt/be -mount -t zfs -o zfsutil,ro zroot/ROOT/default /mnt/be -ls -la /mnt/be/boot - -# 4. if /boot still shows only intel-ucode.img: redo rollback with the exact name -umount /mnt/be 2>/dev/null -zfs rollback -r zroot/ROOT/default@pre-pacman_<EXACT-TS> # -r, NOT -R - -# 5. VERIFY before reboot — remount RO, confirm the kernel is back -mount -t zfs -o zfsutil,ro zroot/ROOT/default /mnt/be -ls -la /mnt/be/boot # MUST show vmlinuz-linux + initramfs-linux.img -umount /mnt/be - -# 6. only once /boot shows a kernel: -zpool export zroot && reboot -#+end_src -Scope: only zroot/ROOT/default reverts; /home, /var, /media are separate datasets, untouched. After boot: =pacman -Syu= attended, confirm /boot holds vmlinuz-linux + initramfs before any shutdown. Full diagnosis: =inbox/PROCESSED-2026-07-15-0002-from-.emacs.d-velox-boot-failure-handoff.org=; ZBM photo: =inbox/PROCESSED-2026-07-15-0002-from-.emacs.d-PXL_20260715_043758976.jpg= (local on ratio; inbox is gitignored). -** DONE [#C] Restore date-format scrolling on the waybar date module :feature:waybar:dotfiles:quick: -CLOSED: [2026-07-19 Sun] -Shipped dotfiles 9dfe082: date-only ring (ordinal/full/longdate), on-scroll rewired, layout guard flipped. UTC/time stay on the time module. -Date and time are separate fixed-position controls. The time display cycles its -own formats, including UTC; the date/calendar control cycles date-only formats -and never displays a second time. Implement the dedicated format rings, -tooltip behavior, and tests together in the dotfiles Waybar configuration. -Reference material for the compact clock/chronograph treatment is filed in -[[file:working/clock-display-references/][working/clock-display-references/]]. - -*** 2026-07-19 Sun @ 04:36:26 -0500 Folded clock-panel interaction direction -The clock-panel handoff settled the prior open question: UTC belongs only to -the time ring, while the date ring is date-only. The existing task is therefore -a focused follow-up, not a two-line restoration of the old combined ring. -** DONE [#C] Notification sound loudness :chore:audio:quick:solo: -CLOSED: [2026-07-19 Sun] -Shipped dotfiles 808ca23: NOTIFY_VOLUME default 65536->39322 (0.6 gain) in both notify copies. -Reduce notification-sound playback loudness by 40% (0.6 gain, approximately --4.4 dB). Change the =NOTIFY_VOLUME= playback control rather than re-encoding -the normalized sound files; verify each notification type still plays clearly. -** DONE [#C] Show the active wired interface in the Waybar network module :feature:waybar:network: -CLOSED: [2026-07-19 Sun] -Shipped dotfiles 22867f9: select_device prefers connected wifi -> connected ethernet -> wifi fallback, so a live cable shows the wired glyph+iface instead of Offline. -When Ethernet is active, replace the offline-WiFi presentation with the wired -interface glyph and interface name. -** DONE [#C] Let the clock panel dismiss itself on right click :feature:clock:waybar: -CLOSED: [2026-07-19 Sun] -Shipped dotfiles fc9a2b7: secondary-button gesture -> ClockApplication._dismiss hides the open panel. Live-verified with Craig 2026-07-19. -Make a right click inside the open clock panel toggle it closed. Preserve left -click for its established interaction; the Waybar time module remains the -explicit way to reopen the panel. -** DONE [#C] Make the WiFi toggle connect the best available profile :feature:network: -CLOSED: [2026-07-19 Sun] -Shipped dotfiles 9105361: manage.wifi_radio -> _connect_best_saved activates the strongest in-range saved profile on enable; nothing in range falls back to NM autoconnect. -When enabling WiFi, automatically connect to the highest-priority available -saved network instead of requiring a panel selection first. -** 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. -** DONE [#C] Installer chpasswd unguarded — unloggable primary user :bug:solo:quick: -CLOSED: [2026-07-20 Mon] -Fixed (fa3135a): extracted set_user_password, which guards the chpasswd with error_fatal so a failure aborts loudly instead of silently leaving no password. Fake-chpasswd test pins the guard fires on failure and stays quiet on success. -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). -** DONE [#C] Installer nvme early module never built into initramfs :bug:solo: -CLOSED: [2026-07-20 Mon] -Fixed in e0d22bd: extracted ensure_nvme_early_module, which rebuilds the initramfs whenever it changed the conf (regardless of ZFS root) and scopes the presence check to the MODULES line. TDD via tests/installer-steps/test_ensure_nvme_early_module.py. -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). -** DONE [#C] Installer disk-space pre-flight check is fragile :bug:solo:quick: -CLOSED: [2026-07-20 Mon] -Fixed in aef074f: extracted check_disk_space using df -P (wrap-safe) and a KB comparison (no truncation bias); non-numeric df output falls back to zero so a malformed read aborts loudly. TDD via tests/installer-steps/test_check_disk_space.py. -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). -** DONE [#C] Installer run_step state + exit-code handling :bug:solo: -CLOSED: [2026-07-20 Mon] -Fixed in 6de55d2: run_step records the state marker whenever the step function returns (a return past error_fatal's exit means only a non-fatal warning is left), added local to run_step/show_status, and captured pacman's real exit in the refresh loop. TDD via tests/installer-steps/test_run_step.py. -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). -** DONE [#C] cmail password decrypted world-readable before chmod :bug:security:solo:quick:cmail: -CLOSED: [2026-07-20 Mon] -Already fixed in dffecf5 (before this session): decrypt_to_secure wraps the gpg decrypt in a 0077-umask subshell so the file is 0600 from creation, with tests/cmail/ verifying the umask at write time. The task was stale; verified green and closed. -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). -** DONE [#C] Installer sudoers.pacnew blind copy risks lockout :bug:solo:quick: -CLOSED: [2026-07-20 Mon] -Fixed in c80e855: extracted replace_sudoers_pacnew, which runs visudo -cf on the pacnew and only copies a validated file (warns and keeps the working sudoers otherwise). TDD via tests/installer-steps/test_replace_sudoers_pacnew.py. -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). -** DONE [#C] WireGuard import leaves full-tunnel VPN live on failure :bug:solo:network: -CLOSED: [2026-07-20 Mon] -Fixed in 36daf76: the down now runs before the rename modify (targets the stable UUID), so a failed modify under set -e can't leave a live full-tunnel VPN. Added a connection-down case to fake-nmcli and two ordering tests. -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). -** DONE [#C] net-scenarios diagnose failure exits green :bug:test:solo: -CLOSED: [2026-07-20 Mon] -Fixed in cf211cd: a diagnose miss sets a per-scenario rc carried to the subshell exit, so the run fails honestly while still running fix + assert. New harness at tests/net-scenarios/ drives the real script with stubbed ssh/rsync/jq. -Grading: Major severity (a net-doctor diagnosis regression is reported as a passing run — false green on a diagnostic tool) x rare edge case (only when a diagnosis regresses and this first-draft harness is relied on) = P3 = [#C]. -scripts/testing/run-net-scenarios.sh:103 — the scenario_diagnose_expect else-branch prints fail "...diagnose did NOT name it" but never forces a non-zero subshell exit, so ( ... ) || fails=... leaves fails unincremented and the script prints "all scenarios passed" + exit 0. Fix: exit 1 in that branch like the other two checks. See findings doc (S5). -** DONE [#C] pacman-hook-order test is a tautology :test:solo:quick: -CLOSED: [2026-07-20 Mon] -Fixed in 1b7236b: the test now extracts the hook filenames the installer writes and compares them against the stock 60-mkinitcpio-remove name (pacman's filename ordering is the real invariant, not source position). Mutation-verified: a 05->70 rename fails the new compare where the old literal compare stayed true. -Grading: Major severity (guards boot-critical hook ordering — a reorder that removes the current initramfs without a rebuild is unbootable, and this test would ship it green) x rare (hook order rarely changes) = P3 = [#C]. -tests/installer-steps/test_pacman_hook_order.py:20 — the two assertLess calls compare string literals ("05..." < "60..."), a constant ASCII fact always true regardless of file content; the ordering the test exists to protect is never measured. Only the assertIn presence checks do real work. Fix: assert on positions — text.index("05-zfs-snapshot.hook") < text.index("60-mkinitcpio-remove.hook") (and the guard hook). See findings doc (S6). -** DONE [#C] Add inetutils to install base :feature:solo:quick:network: -CLOSED: [2026-07-20 Mon] -Already done in 1115543 (earlier today): inetutils sits in install_required_software, with tests/installer-steps/test_required_software.py pinning it (test_installs_inetutils_for_ftp, green). The task was stale; verified and closed. The next full VM run covers the install-path verification. -Original context: TRAMP's /ftp: method needs =/usr/bin/ftp= (GNU inetutils); dirvish has an FTP quick-access entry. Installed manually on ratio 2026-07-14. From .emacs.d handoff 2026-07-14-1751. -** DONE [#D] Installer resume-idempotency cluster :bug:solo: -CLOSED: [2026-07-20 Mon] -Fixed in 8917f2f: extracted crontab_append_once (dedup guard), zfs_scrub_timer_units (one timer per pool, warn on none instead of @.timer), and enable_user_service (wants-symlink; gamemode now uses it and syncthing folds into the shared helper). TDD via tests/installer-steps/test_idempotency_cluster.py. -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). -** DONE [#D] Installer unguarded chmod/cp after non-fatal ops :bug:solo:quick: -CLOSED: [2026-07-20 Mon] -Fixed in dd41036: extracted install_executable (guarded cp + chmod +x) for the two zfs scripts; guarded the two hypr-live-update-guard chmods inline with error_warn. TDD via tests/installer-steps/test_install_executable.py. -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). -** DONE [#D] normalize-notify-sounds temp/atomicity can corrupt tracked file :bug:solo:quick: -CLOSED: [2026-07-20 Mon] -Fixed in a29769e: resolves the real target via readlink -f, stages the temp beside it, guards on a non-empty encode, and atomically mv's into place (preserving the stow symlink); an EXIT trap cleans a leaked temp. TDD via tests/normalize-notify/ with fake ffmpeg. -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). -** DONE [#D] VM test-framework robustness cluster :bug:test:solo: -CLOSED: [2026-07-20 Mon] -Fixed in 866d327: profile-suffixed PID/monitor/serial paths, kill_qemu reaps-or-polls to death before the snapshot restore, debug-vm uses DISK_PATH, and both runners report an honest ARCHSETUP_COMPLETED marker instead of a fake exit code. TDD via tests/vm-framework/test_vm_utils.py (suffix red->green; kill_qemu as a contract pin). -Grading: Minor severity x rare edge case (each fires only in a narrow test-harness path) = P4 = [#D]. Group of four small framework bugs from the S5 audit. -scripts/testing/debug-vm.sh:49 hardcodes the btrfs base disk, ignoring the profile-correct DISK_PATH from init_vm_paths (FS_PROFILE=zfs boots the wrong base or fatals); lib/vm-utils.sh:284 kill_qemu -9's and deletes the PID file without waiting, so a force-kill restore races the dying qemu's qcow2 lock and silently leaves the base image dirty (fix: wait for the PID); lib/vm-utils.sh:69 leaves PID_FILE/MONITOR_SOCK/SERIAL_LOG un-suffixed so parallel btrfs+zfs runs collide (fix: suffix by FS_PROFILE like DISK_PATH); run-test.sh:287 (and run-test-baremetal.sh:234) reports a completion-marker grep as ARCHSETUP_EXIT_CODE, not the installer's real exit — misleading since the installer runs set -e off and can error then still write the marker (fix: rename + capture the true status). Testinfra remains the real pass/fail backstop. See findings doc (S5). -** DONE [#D] Gallery-widget prototype elisp bugs :bug:design:solo:quick: -CLOSED: [2026-07-20 Mon] -Fixed in 552736e: shared clamp feeds needle + readout (150 renders 100%), explicit cl-lib require, and gallery-widget--source-dir with a default-directory fallback. TDD: 3 new ERT tests (clamp red->green; the other two land as pins since svg.el transitively loads cl-lib). -Grading: Minor severity x rare edge case (out-of-range input / cold byte-compile / interactive re-eval) = P4 = [#D]. Prototype code, all three Minor. -docs/prototypes/gallery-widget.el:139 renders the readout from the unclamped value while the needle clamps 0-100, so at value 150 the needle pins at +60 degrees but the text reads "150%" (fix: clamp once, format both from it); :69 calls cl-loop without (require 'cl-lib) — works only via the autoload cookie, bites on a cold byte-compile (fix: add the require); :29 computes its dir from (or load-file-name buffer-file-name), both nil on interactive re-eval outside a load/file buffer (fix: fall back to default-directory). See findings doc (S7). -** DONE [#D] Audit test-quality cluster (Python + elisp) :test:solo: -CLOSED: [2026-07-20 Mon] -Fixed in 179fbd5 (plus 552736e for the gauge-level clamp test): socket check via find -type s, gen_tokens degenerate case pinned exactly as characterization, tick count as direct occurrences, and write-svg covered. All five items dispositioned. -Grading: no runtime behavior change; test-suite quality. Group of five weak/missing tests from the S6/S7 audit. -scripts/testing/tests/test_desktop.py:96 passes a shell glob to `test -S`, which breaks on zero or multiple sockets (masked today because the test always skips); tests/gallery-tokens/test_gen_tokens.py:181 asserts properties too weak to notice the marker output is garbled (impossible input, so low); tests/gallery-widgets/test-gallery-widget.el:77 counts ticks via split-string + cl-count-if :start 1 (a coincidence of split semantics, not a match count); :47 tests the needle-angle helper's clamp but never the rendered readout at an out-of-range value (exactly why the S7 readout/needle bug ships green — add a gauge-level boundary case); :159 leaves gallery-widget-write-svg uncovered (add a Normal write-to-temp case). See findings doc (S6, S7). -** DONE [#B] Installer GRUB_CMDLINE overwrite drops boot params :bug:solo: -CLOSED: [2026-07-21 Tue] -Fixed in f9da097: update_grub_cmdline merges the current value with archsetup's tokens (existing tokens survive, same-key conflicts resolve to archsetup's value) behind a refuse-to-write safety check, via awk + mv with a backup_system_file first. TDD via tests/installer-steps/test_grub_cmdline.py (8 cases incl. cryptdevice/resume/zfs survival and idempotence). -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 [#C] Maint status wall copy buttons :feature:maint:dotfiles: -CLOSED: [2026-07-21 Tue] -Shipped in dotfiles 8bc79ba per Craig's calls (one global button, rendered text): COPY on the doctor row serializes every category band via the same card_spec the GUI renders, through panelkit clipboard. TDD tests/maint/test_status_copy.py, full dotfiles make test green, inbox note sent. Live check pending: open the maint panel, press COPY, paste. -Craig's roam capture 2026-07-20, routed via .emacs.d sentry inbox-zero as archsetup-owned UI work. Dotfiles maint panel work; archsetup drives it end-to-end per the standing rule. -** DONE [#B] Build: desktop-settings panel :feature:hyprland:dotfiles: -CLOSED: [2026-07-22 Wed] -:PROPERTIES: -:SPEC_ID: d6bb1e73-ec90-4327-85ee-bfa762da5bce -:END: -The GTK build of the desktop-settings panel per the spec (docs/specs/2026-07-02-desktop-settings-panel-spec.org, DOING; normative reference: prototype 37). Work happens in dotfiles settings/ — archsetup drives the lifecycle. Two non-blocking build-time picks live in the spec's Review findings (wallpaper setter tool; store location/format) — decide in phase 1 and record there. -*** 2026-07-22 Wed @ 13:14:01 -0500 Built the backings engine (phase 1) — dotfiles 7a15237 -Landed as dotfiles settings/src/settings (10 modules) + tests/settings (118 tests against fake binaries, auto-discovered by make test — 81 suites green). Covers brightness/kbd (5% floor, x10 drum), toggles (dim, pointer cycle via toggle-touchpad, caffeine), DND class-split (dunst pause level 60, close-all before unpause, alarms punch through live), powerprofilesctl, nightlight (resident gammastep), hypridle.conf renderer + symlink-safe write + caffeine-respecting reload + hyprlock grace, suntimes (pure NOAA math), and the wallpaper engine (awww/mpvpaper/projector adapters, galleries, random draw, atomic JSON store). All three build-time picks recorded as DONE findings in the spec (setter=awww, store=state.json, nightlight=gammastep). Handoff note in ~/.dotfiles/inbox/. -*** 2026-07-22 Wed @ 15:26:44 -0500 Built the presenters (phase 2) — dotfiles 5172289 -Three GTK-free models per prototype 37, all at 100% line coverage (tests/settings/test_presenters.py, 100 tests; full repo suite green before and after). programs.py: the matrix — eight complete programs (Craig's four factory scenes drafted here per the pre-flight pick, slots 1-4 first-class), pin rows + power radio row, activate returns the full sets, member writes return apply/updated with active-is-live surviving. bench.py: drum mapping (screen never reads 0, floor 5%; kbd floors at 0), idle rail order clamping between enabled neighbors, park/unpark with re-clamp, caffeine bypass, view-state builder tolerant of no-backlight None. channels.py: the eight-channel bank, per-mode sources visibility, alpha/recency sort (unlabeled last), the shared mint/edit/delete grammar for pairs/sets/colors (press arm-cycle, two-picture set minimum, dup rejection, selection clamping), sources guardrails, interval wheel, previews. Handoff note in ~/.dotfiles/inbox/. -*** 2026-07-22 Wed @ 16:03:52 -0500 Ported prototype 37's instruments to GTK (phase 3) — dotfiles 33d82eb -The panel renders P37 end to end. New instruments.py carries the three Cairo instruments as clock-free humble objects: ProgramMatrix (glyph/numbered heads over jewel pins + CPU POWER paper letter wheels), DrumRoller (paper drums, drag-to-set, dimmed n/a on no-backlight machines), TripDial (sqrt 300° scale, colored stage tabs, OFF-notch parking, exact-minutes drag counter, BYPASSED · CAFFEINE stamp, bottom legend). gui.py rebuilt to P37's layout with the wallpaper sub-view: channel bank with drawn faces, minted pair/color/set trays (alpha/time sort, edit/delete chip feet), the three presses (pair arm-cycle, color picker, set press + interval wheel), sources with a folder picker. New GTK-free glue all unit-tested (test_panel_glue.py, 33 tests): dial geometry in bench, matrix/idle/wallpaper wiring in panel, presenter-vocabulary channels (pair/solid/random-from-set) in wallpaper.apply. AT-SPI smoke (make test-panel-settings) drives the real wiring against faked backings + a sandboxed store, pinned to its own child pid so it can never fire a live panel's backings. Visually verified on a headless output against P37 captures (main + pair/single/solid/random). Adaptations recorded in the handoff: five-stage dial (WATCH gets its own green — the engine runs watch separately, P37 merged the label), DESKTOP_SETTINGS_START_VIEW test seam. Full suite 84 suites green; window rule widened for the 540px panel. Handoff note in ~/.dotfiles/inbox/. -*** 2026-07-22 Wed @ 16:47:54 -0500 Integrated phase 4 — dotfiles 680b50d -Bar consolidation had landed early (74f723e); this pass shipped the rest. settings-project hosts the watch/clock/world channels as HTML faces (settings/faces/) on a gtk-layer-shell background window over WebKit2 — all three visually verified on a headless output, world reading the waybar worldclock roster via query param. settings-watch is the hypridle watch-stage host: throwaway-profile chrome kiosk that reveals only after its window maps behind the lock and relocks before teardown — a failed face degrades to the plain lock, never a bare desktop (unlocked lifecycle verified live; the locked swap goes to the manual checklist). Sun-pair location reads whereami live per transition with last-good cache in state.json (verified live: 9.5s first beat, New Orleans coords, Gogh day side applied); desktop-settings-tick.timer (2 min, enabled on ratio, added to the installer) drives flips and random draws — 23ms no-op beats. dunstrc history_length 100 protects held alarms (full DND cycle verified against live dunst; wtimer alarms already CRITICAL via the notify wrapper, no promotion rule needed). Live hypridle rewrite verified — five-stage regime rendered through the stow symlink, caffeine respected (found engaged, daemon correctly left stopped). Refresh signals needed no rewiring (touchpad signals itself via toggle-touchpad). 45 new tests; suite 84 suites green; smoke 13/13. Handoff note in ~/.dotfiles/inbox/. -Velox one-time steps (sync doesn't carry): mpvpaper (AUR), optionally power-profiles-daemon (service off), and systemctl --user enable --now desktop-settings-tick.timer. -*** 2026-07-22 Wed @ 17:05:58 -0500 Landed the 17-point end-to-end pass — dotfiles 9038eee -Prototype 37's 17-point suite re-derived against the real panel (the original Playwright script wasn't preserved; the functional surface in the spec's Final prototype section is the source). tests/settings/panel_e2e.py + run-panel-e2e.sh + =make test-panel-e2e=: points 1-14 drive the running panel over AT-SPI (program recall with per-backing verification across FOCUS/BATTERY/slot1, pointer console keys, all eight wallpaper channels including projected watch/world stop/start ordering, close); points 15-17 cover the Cairo instruments (drums, tripper dial clamp/park/render/reload, matrix pins + letter wheels with active-is-live) at the backing layer, since AT-SPI can't reach a DrawingArea's hit-tests. Same safety posture as the smoke: sandboxed store, faked backings, pid-pinned a11y node. 17/17 green on ratio's live compositor; full suite 85 green; smoke 13/13; ruff clean. The drag gestures go to the manual checklist below. Handoff note in ~/.dotfiles/inbox/. -*** 2026-07-22 Wed @ 17:05:58 -0500 Flipped the spec to IMPLEMENTED -docs/specs/2026-07-02-desktop-settings-panel-spec.org DOING → IMPLEMENTED with a dated history line naming the shipping commits (dotfiles 7a15237 / 74f723e / 5172289 / 33d82eb / 680b50d / 9038eee) and the verification evidence (85 suites, smoke 13/13, e2e 17/17). The four panel drag-gesture checks and the locked-path night-watch swap live under "Manual testing and validation" — human-eye checks, not implementation blockers. -** CANCELLED [#B] Hyprland layoutmsg crash — bad_variant_access (upstream) :bug:hyprland: -CLOSED: [2026-07-21 Tue] -Dropped 2026-07-21 (Craig's call) — not tracking the upstream report. The crash evidence (both reports + tmpfs log excerpts) and the voice-passed issue draft stay preserved in [[file:working/hyprland-layoutmsg-crash/][working/hyprland-layoutmsg-crash/]] if it recurs and is worth reviving. -Grading: Critical severity (SIGSEGV kills the whole desktop session; every GUI app's unsaved state lost) x rare edge case (twice in ~4.5 months: 2026-03-07 on v0.54.1, 2026-07-20 on v0.55.4) = P2 = [#B]. Upstream Hyprland bug, not this repo's code — the task tracks reporting it and picking up the fix. -A layoutmsg mfact dispatch (layout-resize, mod+H/L) throws std::bad_variant_access inside Layout::CAlgorithm::layoutMsg, uncaught, SIGSEGV. Both crashes fired from the layout-resize mfact path (keycode 104 shrink today, 108 grow in March). Layout at crash was master and the identical mfact had worked seconds earlier; the pre-crash window held monocle<->master toggles, two window closes dropping focus to "[Window nullptr]", and togglefloating x2. Monocle is a registered v0.55 layout (log shows graceful "Unknown monocle layoutmsg" rejects), so the config is not at fault; related edges are guarded ("mfact -> no window") while this path misses its variant guard. Repo has no newer build (0.55.4-1 installed and repo). -Evidence preserved in [[file:working/hyprland-layoutmsg-crash/][working/hyprland-layoutmsg-crash/]] (both crash reports + excerpts from the tmpfs session log, extracted before reboot loses it). -Next: Craig posts the issue himself (2026-07-20 decision) — the voice-passed draft is [[file:working/hyprland-layoutmsg-crash/issue-draft.md][issue-draft.md]], with both crash reports and the log excerpts beside it for attaching. Watch the repo for a fixed release and close on confirmation. The layout-resize script guard was declined (a script can't observe the internal desync). -** DONE [#C] WireGuard import is now config-less — decide feature fate :feature:network: -CLOSED: [2026-07-21 Tue] -Decided 2026-07-21 (Craig): KEEP the import feature. The out-of-band flow is already in place — =assets/wireguard-config/= carries a README documenting "drop plaintext =*.conf= locally at install time (gitignored); ship encrypted =*.conf.gpg= to track", and its =.gitignore= enforces it (=*.conf= blocked, =!*.conf.gpg= allowed). The script also already no-ops gracefully on an empty dir (=shopt -s nullglob= + a =found= flag), so nothing ships and nothing errors when no configs are present. Nothing to build; the fate decision was the whole task. -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). -** DONE [#C] Dupre theme waybar.css drifted from live style.css :bug:dotfiles:waybar: -CLOSED: [2026-07-21 Tue] -Fixed in dotfiles 3e4e7ff (2026-07-20, "fix(theme): sync dupre waybar.css with the live weather rules") — dupre/waybar.css is byte-identical to live again, restoring the =#custom-weather= selectors/hover/gold divider, so =tests/theme-css= is green. -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. -** DONE [#A] Hyprlock lockout: AMD-iGPU DPMS invalidates the lock, session wedges :bug:hyprland:installer:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as archsetup =a9391c9= + dotfiles =3046c9c=, both pushed; applied live to ratio and velox. Reboot ratio to activate the root fix (=amdgpu.runpm=0=); the watchdog covers until then. - -WHAT HAPPENED. Ratio's screen idle-locked, then wedged: hyprlock gone, the compositor still holding the ext-session-lock, no password prompt, recoverable only from a console. Recovered live with =hyprctl dispatch exec hyprlock= (=allow_session_lock_restore=true= was already set, so a replacement client adopted the dead lock). - -ROOT CAUSE (evidence, not the first guess). My first read was "hyprlock crashed on its screenshot buffer" — WRONG. Coredumps are captured here (two telega SIGSEGVs the same afternoon) and there is NO hyprlock coredump, so it did not segfault; memory was fine, so not OOM. The hyprland log shows the real chain: =Modesetting DP-4= / =Restoring crtc 86= (a display modeset) → =color management protocol is enabled and outputs changed= → =SessionLock.cpp:50 SessionLockSurface object remains but surface is being destroyed=. A display power cycle tore down the lock surface. Online research confirms it's a documented AMD-integrated-Radeon issue (hyprlock#953, Hyprland#5822): the GPU resources the lock client holds become invalid when the display powers down and back up. Ratio is a Strix Halo Radeon 8060S — exactly that hardware, and its cmdline already carried =amdgpu.dcdebugmask=0x10= + =no_vpe_idle_pg=1= display workarounds, a history of the same fragility. - -THE FIX, four layers, research-validated: -1. Root cause: =amdgpu.runpm=0= on the kernel cmdline (AMD only, added in =update_grub_cmdline= behind =detect_gpu_vendors=). Keeps GPU runtime PM from invalidating the resources on a display cycle. Live in ratio's grub.cfg; effective next boot. -2. Separate crash cause: =configure_hyprlock_pam= writes a complete =/etc/pam.d/hyprlock= (auth/account/session). The package default is =auth include login= only, so pam_end() crashes on uninitialised handles. Applied live to both machines. -3. Recovery net: the =screen-lock= watchdog (dotfiles) relaunches hyprlock on a non-zero exit; hypridle's =lock_cmd= routes through it. Independently the same shape as the community's watchdog layer. -4. NOT done, deliberately: the =dpms off= listener stays in the committed hypridle — =runpm=0= makes it safe on AMD, and it's wanted on Intel/velox for idle display-off. Ratio's test rail already removed it as a local choice. - -REVERTED a wrong turn: I'd first built a screenshot-to-file change (grim the desktop, point hyprlock at the file) on the theory the live screencopy buffer crashed. The research showed the cause is GPU runtime PM, not the background source, so I dropped it and reverted hyprlock.conf to =path = screenshot=. - -PROCESS NOTE — I hit the pathspec-commit trap AGAIN (the one the =Two agent sessions sharing one repo= VERIFY documents). After surgically staging only the =lock_cmd= line via =git update-index=, I ran =git commit <path> -m ...=, which commits the WORKING TREE of that path, not the index — so it committed ratio's test rail (dpms-off removed, timeout 450) with a message claiming dpms-off stays. Caught it before push, =git reset --soft=, re-verified. The rule: after =update-index=, commit with =git commit= (no pathspec), never =git commit <path>=. - -Tests: archsetup 372 (test_grub_cmdline AMD-runpm cases + test_hyprlock_pam, both call sites in CALL_SITES); dotfiles 3687 incl. tests/screen-lock. Each guard proven by deletion. - -Grading: Critical severity (full session lockout, console-only recovery) x rare edge case (needs an idle lock plus a display modeset on the AMD iGPU) = P2 = [#B by the matrix]. Raised to [#A] here because it stranded a live machine and the root fix needs a reboot to arm — worth Craig seeing at the top until he reboots ratio. -** DONE [#B] Adversarial review of the sentry run — six fixes reworked :bug:test:tooling:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Craig asked for a skeptical review of every sentry change. Eight agents covered all 23 code commits, each told to disbelieve by default and to answer three questions per commit: does the problem exist and is it reachable, is the fix correct or is there a better one, would each test fail with the fix reverted. Every finding below was re-verified by hand before acting on it. - -SIX COMMITS NEEDED WORK, now fixed: archsetup =1207ca5= (wipedisk), =96e12b5= (firmware trim), =560e1dd= (autologin), =3c2155d= (initramfs tabs); dotfiles =ec7229b= (tunnel import), =a81aa0e= (thumbnail sweep), =56807e5= (three residual guards), =c90ee34= (event-log isolation). Both suites green: archsetup 341, dotfiles 3687 on both gates. - -THE ONE THAT MATTERED MOST. =wipedisk= ran =blkdiscard -f= BEFORE the busy check. =-f= disables the exclusive open util-linux has used since 2.36, so on the exact case the round-11 commit reasoned about — the user picked the wrong disk — it discarded a live filesystem and only then let sgdisk fail, printing "could not clear the partition table ... run this again". Data gone, user told nothing happened. The ordering predates the sentry commit, but round 11 wrote reasoning about the busy-disk case into the comment and error text while leaving the discard first, which made the misreport worse in the one direction that costs something. Dropping =-f= makes the kernel's own O_EXCL the gate. - -THREE PATTERNS WORTH MORE THAN THE INDIVIDUAL FIXES: - -1. CALL SITES WENT UNTESTED IN FIVE SUITES. Every helper had thorough tests; not one proved it was called. Deleting the call left everything green — including the guard on a =pacman -Rdd= of twelve firmware packages, whose removal would have run the trim on ratio. Closed with =CALL_SITES= in =test_orchestrators= (nine pairs, static) and a wiring assertion in the settings suite. Static on purpose: the behavioural harness runs un-stubbed bodies for real, which is fine for an orchestrator and not for a leaf that removes packages. - -2. A NEW OUTCOME VALUE NEEDS EVERY CONSUMER WALKED, EVERY TIME. Done for the portal enum in round 3, skipped for the tunnel-import one in round 4 — where =import_configs= folded a disarm failure into "none imported (N failed)", the opposite of what happened, in the multi-select flow the GUI actually uses. - -3. MY FIXTURES TWICE CLAIMED A FIDELITY THEY DID NOT HAVE. The wipedisk fixture used this machine's real disk names, so five of six tests passed with the seam removed. The mkplaylist fake does a full =cat > /dev/null= drain while its docstring says it "drains stdin exactly when the real one would" — which is what let the wrong failure mode survive. - -AND ONE FINDING WAS DISPROVED OUTRIGHT: round 1's =a57c443= claimed ffmpeg drains the read loop so only the first track is processed. Measured under strace and driven end to end with real ffmpeg (three runs of three, four 120s mp3s), the loop never truncates. The hazard is real and =-nostdin= is right; the symptom was reasoned from shellcheck SC2095 and never run. Corrected in =a30741a=, along with the OpenVPN autoconnect claim and the "four consumers" undercount. - -ALL THREE NOW CLOSED, in dotfiles =c7cb40d= (pushed). =_restore_dot='s =noop= split into =already-on= and =not-managed=, so the step stops claiming a restore that never happened. =_disable_dot= checks its restart as well as its move, since moving the drop-in aside does nothing until resolved reloads. - -The thumbnail one could not be built as described, and that is worth recording. The cache name is a SHA-1 of realpath plus mtime, so no filename says which source it came from; per-source sweeping would mean changing the key format and invalidating every cached thumbnail. Bounding the growth gets the same result for less: a deferred sweep now trims to a 500-file ceiling, oldest first, because eviction is safe exactly where sweeping is not (an evicted thumbnail is rebuilt on the next warm pass, costing one decode and never a file). WHEN A FIX CANNOT BE BUILT AS SPECIFIED, SAY SO AND SOLVE THE ACTUAL HAZARD — the hazard here was unbounded growth, not imprecise attribution. -** DONE [#D] Repair tiers call an unverifiable service restart a failed one :bug:network:bluetooth:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as dotfiles =041d6b9= (committed locally, deliberately NOT pushed — held for Craig's morning review of the sentry run). 7 new tests across =tests/bt/test_bt.py= and =tests/net/test_net.py=; dotfiles suite 3665 -> 3672, =make test= exit 0 on both gates. Each of the three guards proven a real gate by deleting it and watching the suite go red. - -Found in the 2026-07-24 sentry bug-hunt, round 14, on the cross-package =repair.py= diff that rounds 5-13 had left unspent. - -=cmd.service_active= is tri-state in both the net and bt packages, and its docstring says so outright: True, False, or None when systemctl itself can't answer (absent binary, or a timeout). Six callers. Three rule on it correctly — =bt/doctor._service_step= branches on None with "systemctl unavailable — can't check the service", and =net/diag= compares =is False= at both its call sites. Three tested it with plain truthiness: - -- =bt/repair.py= =repair_service_restart= -- =net/repair.py= =_service_restart= (the nm-restart and resolved-restart tiers) -- =net/repair.py= =repair_unmask_nm= - -So an unanswerable systemctl was reported as "bluetooth.service is still not active" / "NetworkManager still isn't running after a restart" — a statement about the service made on no evidence at all. Each then pointed the user at =journalctl -u <unit>=, which is the same systemd client stack that had just failed to answer. That last part is round 10's read again: an error message advertising a remedy it cannot honour. - -All three now report =warn= on None, with evidence naming the verification rather than the service, and a next action of checking systemd is reachable and re-running the doctor. Control flow is unchanged: =warn= was already a status both packages emit, both CLIs already exit non-zero on anything but =pass=, and =net/doctor= only inspects a repair step's status for the =dns-test= tier — every consumer was checked before the change, not after. (An adversarial re-review counted twelve, not four; all twelve handle =warn= correctly, so the conclusion held while the claim understated the work.) - -THE SEAM FOR THE TESTS, worth reusing: both suites already carry an exec-failure harness that plants a non-executable file on an emptied PATH, which is exactly what makes =cmd.run= return None. So the None case is reachable through the real code path with no mocking at all. Each test class asserts that premise first (=service_active= really is None in the sandbox) rather than assuming it. - -Grading: Minor severity (the claim is wrong but errs pessimistic — it says a repair failed when it may have worked, rather than falsely reassuring; nothing is damaged) x rare edge case = P4 = [#D]. Fixed rather than filed because the change is three branches and it completes a class — leaving two of three sites collapsed is the failure mode the round-6 =c2eb3e1= commit exists to remember. - -NOT PART OF THIS CLASS, checked and left alone: =settings/toggles.dim_state= is the only other genuine True/False/None helper in the tree, and both its callers pass the value through to the viewmodel rather than collapsing it. Every other "or None" in the packages is two-state (a value or nothing), where falsy handling is correct. -** DONE [#B] Firmware trim gated on a DMI field that never carries the vendor :bug:tooling:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as archsetup =2e228f7= (committed locally, deliberately NOT pushed — held for Craig's morning review of the sentry run). New =tests/installer-steps/test_framework_firmware_trim.py=, 12 tests carrying the real DMI strings off both daily drivers. Each of the three conditions proven load-bearing by deleting it and watching the suite go red, and the old gate proven wrong by restoring it (4 failures). - -Found in the 2026-07-24 sentry bug-hunt, round 13, reading archsetup's remaining state-mutating steps. =trim_firmware= gated on =grep -qi "framework" /sys/class/dmi/id/product_name= and no Framework machine has "framework" in =product_name= — it lives in =sys_vendor=. Read live: velox is =Framework= / ="Laptop (13th Gen Intel Core)"=, ratio is =Framework= / ="Desktop (AMD Ryzen AI Max 300 Series)"=. The gate returns false on both, so the step has been a silent no-op on the exact hardware it was written for. velox IS trimmed today (=linux-firmware-{atheros,intel,realtek,whence}= and nothing else) but not by this code path. - -THE REPAIR IS WHERE THE DANGER IS, which is why this is worth reading twice. Swapping =product_name= for =sys_vendor= is the obvious one-word fix and it is wrong: ratio is a Framework Desktop, and =trim_firmware= runs =pacman -Rdd linux-firmware-amdgpu=, which takes the firmware its Ryzen AI Max iGPU needs to bring up a display. Today only the =grep -qi intel /proc/cpuinfo= second gate stands between ratio and that. So =is_framework_intel_laptop= wants three DMI facts — vendor Framework, and a model naming both Laptop and Intel — and the cpuinfo read stays as an independent second gate rather than the only one. - -Verified live after the change: velox TRIM=yes, ratio TRIM=no, where the old gate said no to both. - -Grading: Minor severity (the trim never happens; nothing breaks, the machine just carries ~550MB it was meant to shed) x every user, every time (every Framework Intel install, which is the whole population the step targets) = P2 = [#B]. The AMD-firmware removal is not graded separately because it never shipped — it is the hazard the fix is shaped to avoid. -** DONE [#B] Fresh install leaves the dotfiles repo permanently dirty :bug:tooling:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as archsetup =c3b3617= (committed locally, deliberately NOT pushed — held for Craig's morning review of the sentry run). New =tests/installer-steps/test_mark_volatile_configs.py=, 8 tests against a fixture git repo with =sudo= stubbed on PATH. Every guard proven a real gate by deletion. A note went to =~/.dotfiles/inbox/= because =skip-volatile= now has an outside caller. - -Found in the 2026-07-24 sentry bug-hunt, round 13, diffing archsetup's =stow_dotfiles= against the dotfiles Makefile's =stow= target — two implementations of one operation, which is round 5's read applied across repos rather than across packages. - -The Makefile's =stow= target ends with =$(MAKE) skip-volatile=, setting git's skip-worktree bit on the four configs their apps rewrite in place (=btop=, =qalculate=, =calibre=, =waypaper=; the list is =volatile-configs=). archsetup stows inline with raw =stow= calls and never ran that step. So a machine archsetup installed goes dirty the first time one of those apps writes its config, and every later =git pull --ff-only= trips over paths the user never edited. Confirmed by grep: archsetup contains no =skip-volatile=, no =volatile=, and no =make stow= — yet both daily drivers carry the bits, so they came from a hand-run =make stow=, not the installer. ratio in fact carries seven, three more than =volatile-configs= lists, which is evidence the churn is real and ongoing. - -The fix calls the dotfiles target rather than copying its logic, so the volatile list stays single-source. Two details that are load-bearing: it runs *after* =git restore .= so the bit lands on a pristine tree, and it runs as the user, because root writing =.git/index= leaves it root-owned and the user's next git command then cannot update the index at all. A checkout with no Makefile is a quiet no-op — nothing to delegate to is not an error. - -DELIBERATELY NOT DONE: replacing the whole inline stow with =make -C "$dotfiles_dir" stow "$desktop_env"=. The Makefile stows =--target=$(HOME)=, which during an install is root's home, and it carries interactive conflict handling; archsetup stows =--target=/home/$username --adopt= as root on purpose. =skip-volatile= is the one target with no such coupling — it works on the repo through =git -C= and never reads HOME. - -Grading: Minor severity (a repo that reads dirty forever and pulls that need a stash; the workaround is one command) x every user, every time (every fresh install that stows dotfiles) = P2 = [#B]. -** DONE [#C] Unattended install blocks on an interactive prompt :bug:tooling:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as archsetup =cbcb53f= (committed locally, deliberately NOT pushed — held for Craig's morning review of the sentry run). New =tests/installer-steps/test_configure_autologin.py= (11) and =tests/installer-steps/test_select_locale.py= (11). Every guard proven a real gate by breaking it and watching the suite go red: dropping the autologin unattended branch fails 1 (on the leftover-stdin assertion, which is the real gate — the drop-in still gets written because the read swallows the sentinel and treats it as "yes"); dropping the locale unattended branch fails 1; breaking either precedence rule fails 2. - -Found in the 2026-07-24 sentry bug-hunt, round 12, continuing through archsetup's own installer. Two members of one class, which is the point: round 10 fixed the third member and left these. - -THE CLASS: an advisory prompt — one that carries its own default — still reading stdin under =--config-file=, the documented unattended mode. Round 10 ruled on it for =nvidia_preflight='s rc-10 prompt. Two sites never got the ruling. - -1. =configure_autologin=. When =enable_autologin= is unset (=AUTOLOGIN= is optional, and =archsetup.conf.example= line 31 ships it commented out) and the root is encrypted, it prompted =Enable automatic console login for $username? [Y/n]= on a bare =read=. It runs from =configure_encrypted_autologin=, inside =boot_ux=, the last entry in =STEPS= — so an unattended install of an encrypted machine works for 40-60 minutes and then sits at a prompt nobody is watching. Under =curl | bash= it is worse: stdin is the script itself, so the read eats a line of source. - -2. =select_locale= (extracted from =preflight_checks= by this commit). The =Choice [1]:= menu fired whenever =/etc/locale.conf= carried no =LANG== and =LOCALE= was unset — also commented out in the example config. archsetup does not require an archangel install, and =configure_build_environment='s own "no LANG=" branch is proof it expects that state. - -Both now take the prompt's own default under =--config-file= and print an =[OK] ... (unattended, --config-file)= line saying so. An explicit =AUTOLOGIN=yes/no= or =LOCALE== still wins; the default only answers a question nobody can. - -WHAT MADE THEM TESTABLE, which is round 10's read (d) applied again: =configure_autologin= hardcoded =/etc/systemd/system/getty@tty1.service.d= and =select_locale= hardcoded =/etc/locale.conf=, so neither could run against a fixture — while their siblings =replace_sudoers_pacnew= and =ensure_nvme_early_module= both take a defaulted path argument for exactly that reason. Both now do. Zero shellcheck delta against HEAD; =make test-unit= 276 -> 298, exit 0. - -Grading: Major severity (unattended installation, a documented feature, does not complete; recoverable by pressing a key, no data loss) x some users, sometimes (needs unattended mode plus an omitted key) = P3 = [#C]. - -THE PROMPTS DELIBERATELY LEFT ALONE, because the class is "prompts with a default", not "all prompts": username (line 636) and password (648/650) have no default to take — there is no sane fallback for either, and =archsetup.conf.example= documents both as "If not set, you will be prompted". They also fire in =preflight_checks=, in the first second of the run, where a blocked prompt is visible rather than silent. The "Enter locale" sub-prompt is reachable only from menu choice 9, which unattended never picks. -** DONE [#C] wipedisk says "Disk erased." when it erased nothing :bug:tooling:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as archsetup =HEAD= (committed locally, deliberately NOT pushed — held for Craig's morning review of the sentry run). New =tests/wipedisk/test_wipedisk.py=, 6 tests running the real script against a fixture device directory with fake blkdiscard/sgdisk on PATH. All four guards proven real by deleting each and watching the suite go red. - -Found in the 2026-07-24 sentry bug-hunt, round 11, reading =scripts/= — 30 lines, no tests, and the most destructive script in the repo. Not installed by the installer; it is run by hand from the checkout, which is why the frequency axis stays low. - -Three defects, all of which make the script's final word untrue: - -1. =sgdisk --zap-all= had its result discarded, and "Disk erased." printed unconditionally. sgdisk refuses a busy device — a mounted filesystem or a live md/LVM/ZFS holder — which is exactly what a user hits after picking the wrong disk. So the tool announced an erase it had not performed and exited 0. - -2. "Disk erased." overstates what the tool does even on success. =sgdisk --zap-all= destroys partition tables, not data, and =blkdiscard -f ... || true= deliberately tolerates a device that cannot discard. On a disk without discard support the script cleared the partition table and left every byte readable, while telling the user the disk was erased. That is the one path where the wrong belief has a privacy consequence — someone trusting the message before disposing of a drive. - -3. The prompt says "Select the disk id to use" and then listed every entry in =/dev/disk/by-id=. On this machine that is 18 entries of which 12 are =-partN= partitions (verified by listing it). The menu promised disks and offered partitions. - -Fix: whole disks only (globbed rather than =ls | grep=, so a name with whitespace cannot split into two menu entries); the zap's result is checked and a failure exits 1 naming the busy-device cause; the closing message reports what actually happened, and when discard was unsupported it says the data is still recoverable and points at =nvme format= / =hdparm= for a disposal-grade wipe. - -Grading: Major severity (the tool reports an outcome it did not achieve; in the disposal case that is a data-exposure consequence) × rare edge case (a hand-run helper the installer does not install, and defect 1 additionally needs sgdisk to fail) = P3 = [#C]. - -Worth recording about the tests rather than the code: two of the six passed against the unmodified script for the wrong reason. Without the =WIPEDISK_BY_ID= override the script read the real =/dev/disk/by-id=, so the harness was driving a menu of this machine's actual disks (harmless — the fake blkdiscard/sgdisk shadowed the real ones on PATH — but it was not testing the fixture). And =test_empty_by_id_directory= was not a gate at first: with the guard deleted the empty select menu still falls through to the confirm prompt, reads EOF and declines, so exit code and call log alone pass either way. It now asserts the message. -** DONE [#B] zfs-replicate reports success when every backup failed :bug:backup:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as archsetup =HEAD= (committed locally, deliberately NOT pushed — held for Craig's morning review of the sentry run). Diagnostics moved to stderr; the loop counts failures and exits 1 when any dataset failed. New =tests/zfs-replicate/test_zfs_replicate.py=, 9 tests driving the real script with a fake syncoid and a fake ping on PATH (the =tests/zfs-pre-snapshot/fake-zfs= pattern). Both fixes proven real gates by reverting them: dropping the counter fails 3, putting =error()= back on stdout fails 1. - -Found in the 2026-07-24 sentry bug-hunt, round 11, reading =scripts/= — 73 lines with no test file, installed by =configure_zfs_snapshots= as =/usr/local/bin/zfs-replicate= and run by =zfs-replicate.service=, a =Type=oneshot= on a nightly timer. Its exit code and its journal output are the only signals anyone ever sees. - -Two defects, both verified by running the script rather than argued: - -1. The full-replication loop caught each =syncoid= failure, warned, carried on, then printed "Replication complete." and exited 0 regardless. Driven with a fake syncoid failing all four datasets: four =[WARN] Failed= lines, then "Replication complete.", exit code 0. systemd records =Result=success=. A backup that has not run for months is indistinguishable from a working one — and the whole point of the tool is to have a copy when the primary is gone. - -2. =determine_host= runs inside a command substitution (=TRUENAS_HOST=$(determine_host)=) and its =error()= wrote to stdout. On an unreachable TrueNAS the message was captured into =TRUENAS_HOST= and discarded, and =set -e= then killed the script. Driven with both hosts unreachable: exit 1 and completely empty output. A nightly service failing with nothing in the journal to say why. - -Same class as three bugs already fixed this session — =_restore_dot= claiming "DNS-over-TLS restored" without checking, =portal_restore_watch= discarding its outcome, =import_config= returning ok on an unchecked modify. A mutating operation that reports a success it did not get. - -Grading: Critical severity (a backup system that reports success while backing nothing up; the failure surfaces only when the backup is needed — graded on the harm once in the failure state, not on how rarely it is entered) × rare edge case (needs a ZFS root, a reachable TrueNAS, and the user enabling the timer by hand — archsetup deliberately does not enable it, and =findmnt -n -o FSTYPE /= on this machine says btrfs, so it is latent here) = P2 = [#B]. - -Left alone: =BACKUP_PATH="backups" # TODO: Configure actual path= is still an unresolved TODO in the destination, and single-dataset mode relies on =set -e= to propagate a syncoid failure rather than reporting it. Neither is a defect in the sense above; the TODO is Craig's call. -** DONE [#D] Wireless regdom is silently unset for a three-letter-language locale :bug:installer:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as archsetup =249bb93=. =locale_country= matches the =_CC= group instead of counting characters, and =set_wireless_regdom= verifies the substitution landed rather than trusting sed's exit code. 16 tests. -=configure_networking= derives the wireless regulatory domain by fixed offset: =wireless_region="${current_lang:3:2}"=, with a comment reading "extract country code (positions 3-4)". That is correct only for a two-letter language code. - -=validate_config= accepts =^[a-z]{2,3}(_[A-Z]{2})?...=, so a three-letter language is a legal =LOCALE=, and glibc ships 75 of them (=agr_PE=, =ast_ES=, =ber_DZ=, =ayc_PE=, ...). Verified by running the expansion: =ber_DZ.UTF-8= yields =_D=, =ayc_PE.UTF-8= yields =_P=, =C= yields the empty string, =POSIX= yields =IX=. - -The sed that follows only uncomments an existing =#WIRELESS_REGDOM="XX"= line in =/etc/conf.d/wireless-regdom= (176 of them, owned by wireless-regdb). A garbage region matches nothing, sed exits 0, and the =|| error_warn= never fires — so the regdom is never set and nothing says so. The task line does print the garbage region ("configuring wireless regulatory domain (_D)"), so it is visible in the log rather than fully silent. - -Confirmed the mechanism itself works for the normal case: line 168 of this machine's =/etc/conf.d/wireless-regdom= reads =WIRELESS_REGDOM="US"= uncommented, which is archsetup's own edit. - -Grading: Minor severity (WiFi falls back to the conservative "00" regdomain — fewer channels and lower tx power, but WiFi works) × rare edge case (one of 75 three-letter-language locales, or a =LOCALE= with no country) = P4 = [#D]. - -Fix when it comes up: derive the country from the =_CC= group by pattern rather than by offset, and warn when it cannot be derived or when the sed changed nothing. Worth doing together with the sibling gap — nothing in the installer verifies that a =sed -i= uncomment actually matched, so a distro reshuffling one of these config files would fail the same silent way. All 22 =sed -i= sites share that stance, so it is a uniform design choice rather than an odd one out. -** DONE [#B] Initramfs hook swap can leave a LUKS machine unbootable :bug:installer:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as archsetup =HEAD= (committed locally, deliberately NOT pushed — held for Craig's morning review of the sentry run). The swap moved into =switch_udev_hook_to_systemd=, which declines when =hooks_need_busybox_init= sees a standalone =encrypt= token, and the caller now rebuilds the initramfs only when the conf actually changed. New =tests/installer-steps/test_switch_udev_hook.py=, 10 tests; both guards proven real by breaking them (removing the refusal: 4 failures; loosening the token match to a bare =encrypt= substring: 1 failure). - -Found in the 2026-07-24 sentry bug-hunt, round 10. =configure_initramfs_hook= ran =sed -i '/^HOOKS=/ s/\budev\b/systemd/'= on any non-ZFS root, then =mkinitcpio -P=. Its only guard was =is_zfs_root=. - -Why that breaks a LUKS machine, verified against the installed mkinitcpio rather than argued: -- =/usr/lib/initcpio/install/systemd= line 70 is =add_symlink /init usr/lib/systemd/systemd=, so the systemd hook replaces the busybox init outright. -- =/usr/lib/initcpio/hooks/encrypt= is an =#!/usr/bin/ash= script whose entire body is a =run_hook()= function — the busybox init's mechanism. Under systemd init nothing calls it. -- =mkinitcpio= carries no conflict check for the pairing (grepped; nothing), so the rebuild succeeds and archsetup reports success. -- This machine's own =/etc/mkinitcpio.conf= documents the two valid pairings as separate examples: =udev= + =encrypt= (line 45) and =systemd= + =sd-encrypt= (line 51). The sed converted half of the first pairing and produced neither. - -Effect: on a LUKS root using the standard busybox =encrypt= hook, archsetup rewrites HOOKS to =systemd= while leaving =encrypt= behind, rebuilds the initramfs, and exits cleanly. At the next boot the root is never unlocked. The machine needs live media and manual mkinitcpio surgery to recover. - -The sibling asymmetry: =is_encrypted_root()= already exists in this script and =configure_autologin= uses it to branch on exactly this condition. The initramfs step consulted neither it nor HOOKS. =merge_grub_cmdline='s own comment names =cryptdevice== as a boot-critical parameter to preserve — and =cryptdevice== is read only by the =encrypt= hook, so archsetup explicitly anticipates the configuration that another of its steps then breaks. - -Grading: Critical severity (the machine will not boot and recovery needs external media — graded on the harm once in the failure state, not on how rarely it is entered) × some users, sometimes (LUKS-encrypted non-ZFS root using the busybox =encrypt= hook; deterministic for those machines, absent everywhere else) = P2 = [#B]. - -Deliberately not attempted: migrating =encrypt= to =sd-encrypt=. That means rewriting the kernel cmdline from =cryptdevice== to =rd.luks.name== against the volume's UUID, which is a real migration and not a mechanical edit. Refusing the cosmetic swap keeps a working machine working, which is the right trade against quieter fsck output. -** DONE [#D] keymap and consolefont hooks are inert under the systemd initramfs :bug:installer:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as archsetup =249bb93=. The swap rewrites both to =sd-vconsole=, collapsing them into one entry and never duplicating an existing one. The open question is answered: this machine is KEYMAP=us with no encrypt hook, but the function runs on LUKS machines where a non-US layout at the passphrase prompt is exactly what sd-vconsole restores. 7 tests. -Same class as the =encrypt= bug above, but cosmetic rather than boot-critical, so it was filed rather than bundled into that fix. - -Enumerating the busybox-only hooks on this machine (every hook under =/usr/lib/initcpio/hooks/= defining =run_hook=/=run_earlyhook=/=run_latehook=) gives: btrfs, consolefont, encrypt, grub-btrfs-overlayfs, keymap, memdisk, resume, sleep, udev, usr. All go inert once =/init= is systemd. Of those, =encrypt= is the only boot-critical one — =resume= is handled natively by systemd's hibernate-resume generator, and =btrfs= by udev rules (this machine runs =btrfs= alongside =systemd= and boots fine). - -=keymap= and =consolefont= are the live leftovers. Run =grep '^HOOKS=' /etc/mkinitcpio.conf= on this machine: the line carries =systemd= plus =keymap consolefont= and no =udev=, so archsetup's swap has already run here and both hooks are installed into the image and never executed. The systemd equivalent is the single =sd-vconsole= hook, which is what the distro's own systemd example on line 51 of =/etc/mkinitcpio.conf= uses. - -Effect: the early-boot console keeps the default font and keymap until =systemd-vconsole-setup= runs in the real root. =add_nvme_early_module= sets =FONT=ter-132n= in =/etc/vconsole.conf= expecting it to apply at that stage, so the configured font is briefly not what archsetup asked for. - -Grading: Cosmetic severity (a few seconds of default console font on a machine that boots normally) × some users, sometimes = P4 = [#D]. - -Fix when it comes up: have =switch_udev_hook_to_systemd= also rewrite =keymap consolefont= to =sd-vconsole= when it performs the swap, and add the fixture cases to =tests/installer-steps/test_switch_udev_hook.py=. Worth confirming first whether a non-US keymap is ever needed at the initramfs prompt on a machine that reaches this path. -** DONE [#B] NVIDIA Wayland preflight blocks dwm and headless installs :bug:installer:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as archsetup =HEAD= (committed locally, deliberately NOT pushed — held for Craig's morning review of the sentry run). The NVIDIA block moved out of =preflight_checks= into a new =nvidia_preflight= function that returns early unless =desktop_env= is =hyprland= and archsetup is the one installing drivers. New =tests/nvidia-preflight/test_nvidia_preflight_gate.py=, 11 tests; each of the three guards was proven a real gate by deleting it and watching the suite go red (3, 1, and 1 failures respectively). - -Found in the 2026-07-24 sentry bug-hunt, round 10, reading archsetup's own installer. =preflight_checks= called =nvidia_preflight_report= unconditionally and exited 1 on rc 11 (repo driver below the 535 Wayland floor, or =pacman -Si nvidia-utils= unable to answer). The check is Wayland-specific — every line it prints names Wayland/Hyprland — but it ran before any =desktop_env= branch and consulted neither =desktop_env= nor =skip_gpu_drivers=. - -Effect, proven empirically rather than argued (three scenarios driven against the extracted block): =DESKTOP_ENV=dwm= plus =--no-gpu-drivers= on an NVIDIA machine with an old repo driver aborts the install; so does =DESKTOP_ENV=none=. Neither install ever runs a compositor, and =--no-gpu-drivers= means the user installs the driver themselves. Worse, the abort's own fix hint reads "install with DESKTOP_ENV=dwm (X11) instead" — the one remedy it prints is the one it refuses to honor, so the user has no working workaround short of editing the script. - -The sibling asymmetry that makes it an oversight rather than a decision: =install_gpu_drivers= returns early on =skip_gpu_drivers=, and =display_server= / =window_manager= both branch on =desktop_env= with a =none= arm that skips outright. The preflight gate applied neither ruling. - -Second defect at the same site, fixed in the same commit: the rc-10 path (card detected, driver fine) prompts with a bare =read=. =--config-file= is documented as "unattended installation", and =aur_install= already rules that a prompt not covered by =--noconfirm= "blocks forever waiting for input" on a headless install. The rc-10 prompt is advisory, so it now answers itself with its own =[Y/n]= default when a config file was supplied. rc 11 stays a hard stop either way. - -Grading: Critical severity (archsetup cannot be run at all on that machine, and the printed workaround does not work — graded on the harm once in the failure state, not on how rarely it is entered) × rare edge case (needs an NVIDIA card, a repo driver below the floor or an unsynced pacman db, and a non-hyprland =desktop_env=; hyprland is the default and Craig's own machines are AMD and Intel) = P2 = [#B]. - -Noted, not fixed: =display_server= and =window_manager= both point their unknown-value hint at a =--desktop-env= flag that the argument parser does not implement. Both arms are unreachable today (=validate_config= rejects a bad =DESKTOP_ENV=, and without a config file the value is always the default), so it is a stale string rather than a live defect. -** DONE [#B] mkplaylist retags only the first file :bug:music:quick:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as dotfiles =a57c443= (committed locally, deliberately NOT pushed — held for Craig's morning review of the sentry run). =ffmpeg -nostdin= on the conversion call. New =tests/mkplaylist= suite, 12 tests; removing the flag turns the suite red (verified by reverting: 5 failures, green on restore). NOTE: the fake ffmpeg does a full =cat > /dev/null= drain, which the real one does not do — so the suite gates the flag's presence, not the production failure mode. The docstring claiming the fake "drains stdin exactly when the real one would" is false and should be corrected. -Found in the 2026-07-24 sentry bug-hunt (shellcheck SC2095). =common/.local/bin/mkplaylist=: =generate_music_m3u= pipes the file list into =tag_music_file= (line 130), which consumes it with =while IFS= read -r file=. Inside that loop, =ffmpeg -i "$file" -vn -c:a flac "$outputfile"= (line 46) reads stdin by default for its interactive keyboard controls, so it consumes bytes the loop is relying on. - -CORRECTION (2026-07-24, from an adversarial re-review): the failure mode stated above — "the loop sees EOF and exits after the first file" — is WRONG, and this task originally asserted it. Measured under strace, ffmpeg polls fd 0 and reads roughly one byte per half-second of transcode wall time; flac encoding runs about 2000x realtime, so a ten-minute mp3 converts in ~0.28s and yields zero or one stolen byte, never a drain. Driven end to end with real ffmpeg against four 120s mp3s, three runs of three: all four were converted and retagged every time. The loop never truncated. - -What is real is the hazard, not the observed symptom: one stolen byte mangles a path, which makes mid3v2/metaflac fail and =set -e= abort the run loudly. =-nostdin= is still the right fix and the commit still stands. The original finding came from shellcheck SC2095 plus reasoning, and was never run — which is exactly what "verify before filing" exists to prevent. - -Effect: on a directory of non-flac audio, only the first file is converted and retagged. Files 2..N are silently skipped — no error, no output, and the playlist itself still generates (a separate =find=), so nothing signals that the retagging stopped. - -Grading: Major severity (the retagging feature is broken past the first file, and it fails silently) × most users frequently (the script exists to batch-process a directory, so more than one non-flac file is the normal case) = P2 = [#B]. - -Fix: =ffmpeg -nostdin= (or =< /dev/null= on the call). Verifiable with a fake =ffmpeg= on PATH asserting it is invoked once per input file. -** DONE [#C] timezone-change prints command-not-found instead of its help :bug:tooling:quick:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as dotfiles =15d2b63= (committed locally, deliberately NOT pushed — held for Craig's morning review), together with the Portugal-zone defect below. New =tests/timezone-change= suite, 12 tests. -Found in the 2026-07-24 sentry bug-hunt (shellcheck SC2288). =common/.local/bin/timezone-change=, default =*)= case (lines 63-67): =echo= sits alone on its own line, so the following quoted string runs as a *command* rather than as its argument. - -#+begin_src sh -*) - echo - "Invalid option chosen." - echo - "Some valid options are: eastern, central, pacific, rome, london, st_lucia, italy, france, spain ." - ;; -#+end_src - -The user gets two blank lines and two =command not found= errors; the list of valid options never prints. The timezone is correctly left unchanged, so this is an output defect only. - -Grading: Minor severity (wrong output on an error path, nothing corrupted) × some users sometimes (only on an unrecognized option) = P3 = [#C]. - -Fix: fold each string into its =echo=. Verifiable by running the script with a bogus argument and asserting the option list appears on stdout. -** DONE [#C] Thumbnail sweep wipes the whole cache when a wallpaper source is unreadable :bug:settings:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as dotfiles =0bd8c67= (committed locally, deliberately NOT pushed — held for Craig's morning review). 8 new tests. -Found in the 2026-07-24 sentry bug-hunt, reviewing the orphan sweep shipped the night before (dotfiles =e752a16=). =os.walk= stays silent about a directory it cannot enter, so =wallpaper.scan_sources= returns =[]= for a source that is missing, renamed, or permission-denied — the same answer it gives for a gallery the user emptied on purpose. =settings/cli.py= tick then hands that empty list to =thumbstore.sweep_orphans=, =live_names= comes back empty, and every cache-shaped file is classified an orphan. - -Proven empirically rather than reasoned: seeding three well-formed thumbnails plus a stray README, then sweeping against a nonexistent source directory, deleted all three (the README survived, so the cache-name regex guard works — it just doesn't help here). - -Effect once entered: the entire persistent thumbnail cache is deleted, so the next wallpaper-view open pays the cold-decode cost the cache was built to remove (measured at 3.7s for a viewport of Craig's largest 8, which is what tripped the compositor's kill prompt), and the tick needs roughly ten idle beats — about twenty minutes — to rewarm at =WARM_PER_BEAT= 8. - -Grading: Major severity (grading the being-in-it, per the don't-double-count-rarity rule: the cache is gone, the original freeze returns, and recovery is unattended and slow) × rare edge case (both configured sources — =~/videos/wallpaper= and =~/pictures/wallpaper= — are local directories, so this needs one deleted, renamed, or made unreadable while a beat fires; a removable or network source would hit it routinely) = P3 = [#C]. - -Fixed in this session: new =wallpaper.sources_available(sources)= tells "readable and empty" apart from "could not read", and =sweep_orphans= grew a =sources_ok= parameter that declines to sweep when it is False. Deferring a sweep costs only some stale files; sweeping wrongly costs the whole cache. -** DONE [#C] timezone-change sets a nonexistent zone for Portugal :bug:tooling:quick:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as dotfiles =15d2b63= (committed locally, deliberately NOT pushed — held for Craig's morning review). =Europe/Lisbon=. The suite also pins the general invariant: every zone the script can emit must exist in tzdata, so a future bad entry fails at test time rather than in Craig's hands. -Found in the 2026-07-24 sentry bug-hunt, validating every zone the script sets against =/usr/share/zoneinfo=. =common/.local/bin/timezone-change= line 39 maps =portugal= / =lisbon= to =Europe/Portugal=, which is not a tzdata identifier — the real one is =Europe/Lisbon= (a bare =Portugal= legacy alias also exists at the top level, but not under =Europe/=). =timedatectl set-timezone "Europe/Portugal"= fails, so the timezone is never changed. - -The other 17 zones the script sets all resolve correctly, so this is the single bad entry. - -Grading: Major severity (the option is wholly broken — the zone is not set and the command errors) × rare edge case (one option of eighteen, hit only when actually switching to Portugal) = P3 = [#C]. - -Fix: =Europe/Lisbon=. Verifiable by asserting the argument handed to a fake =timedatectl=, plus a suite-wide check that every zone the script names exists in the tzdata database. -** DONE [#C] settings-project stop() can SIGTERM an unrelated process :bug:settings:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Found in the 2026-07-24 sentry bug-hunt, round 2, reviewing =settings/src/settings/project.py=. =stop()= read a pid out of =$XDG_RUNTIME_DIR/settings-project.pid= and SIGTERMed it with no check that the pid still belonged to the projection. A projection that dies without running =stop()= (crash, OOM, a failed =execvpe= on the clock path — that last one was already noted as tolerated residue) leaves the file behind, so once the kernel wraps its pid counter that pid can name something else entirely, and the next =start= or =stop= kills it. - -This is a hazard the codebase had already ruled on elsewhere and simply hadn't applied here: =maint/src/maint/doctor.py= revalidates =/proc/<pid>/comm= against the expected name before its KILL remedy fires, explicitly to refuse recycled pids. - -Grading: Major severity (grading the being-in-it — an arbitrary user process takes a SIGTERM, and an editor with unsaved work is a plausible victim) × rare edge case (needs an unclean exit *and* pid reuse; =pid_max= here is 4194304, so wrap-around takes a very long time) = P3 = [#C]. - -Fixed as dotfiles =722994e= (committed locally, deliberately NOT pushed — held for Craig's morning review). The pidfile now records the process start time from =/proc/<pid>/stat= next to the pid, and =stop()= fires only when the recorded value still matches the live process. Start time is the right token rather than =comm=: it is mode-independent (the clock channel execs into =python3=, so comm changes while comm-matching would have needed per-mode knowledge) and it is exec-stable, verified directly — pid and start time were identical either side of an =execvpe=. A recycled pid cannot reproduce it. Legacy bare-pid pidfiles keep the old unconditional behavior so the upgrade never strands a live projection. -** DONE [#C] wtimer alarms fire an hour off on the eve of a DST change :bug:timer:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Found in the 2026-07-24 sentry bug-hunt, round 3, reading =timer/src/timer/engine.py=. =parse_alarm= resolves a bare wall-clock time ("07:00") to its next occurrence: it builds today's instant, and when that is already past it rolled forward with =epoch += 86400=. A DST day is 23 or 25 hours long, so a fixed 86400 lands on the wrong wall time whenever tomorrow crosses a transition. - -Reproduced against America/Chicago and the two 2026 US transitions. Asking for =07:00= at 08:00 on Sat 2026-03-07 (spring forward that Sunday) gave 08:00 Sunday — an hour late. Asking for =07:00= at 08:00 on Sat 2026-10-31 (fall back that Sunday) gave 06:00 Sunday — an hour early. - -The recurring path was never affected, which is what makes this an oversight rather than a design choice: =next_alarm= walks candidate days and rebuilds =datetime(y, m, d, hh, mm)= per day, so it is already DST-correct. Only the one-shot rollover took the shortcut. Both were pinned by the new tests. - -Grading: Major severity (grading the being-in-it — an alarm that fires an hour off has wholly failed at the one thing an alarm does, and the fall-back direction wakes you early while the spring-forward direction lets you oversleep) × rare edge case (two nights a year, and only when the requested wall time has already passed today) = P3 = [#C]. - -Fixed as dotfiles =9b6c2c9= (committed locally, deliberately NOT pushed — held for Craig's morning review). The rollover now rebuilds the local time on tomorrow's calendar date, the same construction =next_alarm= uses. Eight tests pin =TZ=America/Chicago= (saved and restored around each case), covering both transitions, the twelve-hour form, an ordinary-day control, a DST eve where the requested time is still ahead, and two characterization cases asserting the recurring path stays DST-safe. -** DONE [#B] net portal-restore claims encrypted DNS is back without checking :bug:net:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Found in the 2026-07-24 sentry bug-hunt, round 3, reading =net/src/net/repair.py=. A captive-portal login moves the DNS-over-TLS drop-in aside so plain DNS can reach the venue's login page, and =_restore_dot()= moves it back afterwards. It fired both privileged steps — the =mv= and the =systemctl restart systemd-resolved= — and returned ="restored"= without reading either result. =repair_portal_restore()= then rendered a pass step reading "DNS-over-TLS restored". - -So a declined or failed =sudo -n mv= left DNS-over-TLS off while the tool told the user it was back on. The same for a resolved restart that fails: the drop-in is on disk but the running resolver is still serving plain DNS. - -The asymmetry is what makes it an oversight rather than a decision. The sibling =_disable_dot()=, twenty lines up, checks its own move with =_ok()= and returns False rather than claiming a success it did not get. The restore half simply never got the same treatment, and it is the half where the failure is silent — the disable path's failure is visible immediately because the portal page won't load. - -Grading: graded on severity alone under the privacy carve-out. DNS queries continue in cleartext to the venue resolver on an untrusted network, and the affirmative "restored" message is what removes the user's reason to check. Bounded by =net diagnose='s =encrypted-dns= step, which exists precisely to catch a portal run that never restored, so the exposure ends at the next diagnose rather than persisting unseen forever. Major severity = P2 = [#B]. - -Fixed as dotfiles =018c0c5= (committed locally, deliberately NOT pushed — held for Craig's morning review). Both privileged steps are now checked, with two new outcomes: ="failed"= when the move back fails (encrypted DNS still off, rendered as a fail step) and ="unapplied"= when the drop-in is back but resolved would not restart (rendered as a warn step). Each names the command to run by hand. Four tests cover both failures at the =_restore_dot()= and step levels, mirroring the existing declined-move test on the disable side. -** DONE [#B] the portal restore watcher fails silently, so DNS stays in the clear :bug:net:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Found in the 2026-07-24 sentry bug-hunt, round 4, reading the rest of =net/src/net/repair.py= after the round-3 fix above. =portal_restore_watch()= polls until the link comes back online, calls =_restore_dot()=, and discards the outcome entirely. - -Three things compound into a silent failure. The watcher is spawned detached with =stdin=, =stdout=, and =stderr= all on =/dev/null=, so nothing it could print reaches anyone. It runs outside the =repair()= dispatch, so unlike every other mutating tier it never wrote an event-log line either. And =repair_portal_login= tells the user "encrypted DNS restores itself once you're online", which is precisely what removes their reason to check. A ="failed"=, ="unapplied"=, or ="ambiguous"= restore therefore left the machine on plain DNS on a venue network with no signal at any level. - -This is the round-3 finding one layer out, and the asymmetry is the tell: =018c0c5= taught =repair_portal_restore()= — the *manual fallback* — to stop claiming a success it did not get, while the *automatic* path, the one that actually runs in the normal flow, kept dropping the same result on the floor. Fixing the fallback and leaving the primary silent is a worse split than the original bug. - -Grading: graded on severity alone under the privacy carve-out, exactly as the round-3 sibling. Same exposure (cleartext DNS to an untrusted venue resolver), same bound (=net diagnose='s =encrypted-dns= step catches the stranded state), and the same affirmative promise removing the reason to look. Major severity = P2 = [#B]. - -Fixed as dotfiles =601c5b4= (committed locally, deliberately NOT pushed — held for Craig's morning review). The watcher now returns the outcome, appends a =portal-restore-watch= event with it, and fires a persistent =notify security= alert on each of the three failing outcomes, each naming the command to run by hand. A clean restore stays silent. Five tests: one per failing outcome, one pinning the silence on a clean restore, and one on the event-log line. The whole =TestPortalLogin= class now shadows =notify= with a logging fake, so no future watcher test can fire a real desktop notification mid-suite. -** DONE [#D] dns-override failure path says "reverted" without checking :bug:net:quick:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as dotfiles =2cf3fb3=. The revert is checked; a declined one now says 1.1.1.1 is still set and names =resolvectl revert <iface>=. -Found in the 2026-07-24 sentry bug-hunt, round 3, sweeping for siblings of the portal-restore finding above. =net/src/net/repair.py=, =repair_dns_override()= failure path: when the 1.1.1.1 override doesn't restore resolution, it calls =priv.run("dns-revert", iface)=, discards the result, and returns evidence reading "override didn't restore resolution — reverted". A failed revert leaves 1.1.1.1 set on the link while the step says it was removed. - -Same defect class as the portal-restore bug, three hundred lines up in the same file, and it survived the sweep only because the consequence is much smaller. Every other mutating repair in this file verifies by re-measuring afterwards rather than by reading an exit code, which is the stronger pattern and is why the sweep otherwise came back dry. - -Grading: Minor severity (a stale per-link override sends DNS to Cloudflare instead of the venue resolver, it dies on the next reconnect, and =net diagnose='s =dns-override-present= step exists specifically to catch it) × rare edge case (needs the override to fail *and* the revert to fail) = P4 = [#D]. - -Fix: the same idiom the portal-restore fix now uses. Wrap the revert in =_ok()= and drop the "— reverted" claim (or say the revert failed and name =resolvectl revert <iface>=) when it returns False. The existing =RepairHarness= makes the privileged call fail with =NET_SUDO="false"=, so the test is a near-copy of =test_restore_reports_failure_when_the_move_back_is_declined=. -** DONE [#B] a timezone-less Date header crashes the whole net diagnose run :bug:net:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Found in the 2026-07-24 sentry bug-hunt, round 4, reading =net/src/net/diag.py=. =_clock_skew_s()= fetches the probe server's =Date= header with =curl -sI=, parses it with =parsedate_to_datetime=, and subtracts it from a timezone-aware =datetime.now(timezone.utc)=. RFC 5322 allows a =Date= to carry =-0000=, which means UTC while explicitly claiming no local zone, and a =Date= with no zone at all parses leniently as well. Both come back *naive*, and subtracting a naive datetime from an aware one raises =TypeError=. - -The =try= wraps only the =parsedate_to_datetime= call, so the =TypeError= from the line below it is uncaught. It escapes =_clock_skew_s=, escapes =_steps_egress_edges=, and takes down the entire =diagnose()= run — no report, no steps, a Python traceback. =net doctor= runs diagnose first, so the panel's doctor button dies with it. - -Verified against Python 3.14.6 before writing the fix: =parsedate_to_datetime("Thu, 01 Jan 2020 00:00:00 -0000")= returns =tzinfo=None=, and the subtraction raises. The zoneless form behaves the same. Only the =GMT= form (which the well-behaved probe host sends) comes back aware, which is why this never showed up in normal use. - -What makes it more than a curiosity is *when* the code runs. =_steps_egress_edges= fires only after the http-probe has already failed, so the server answering that =HEAD= is frequently a captive portal's interception appliance rather than the real probe host — and a minimal embedded HTTP stack is exactly the kind that emits a non-GMT =Date=. The one path guaranteed to be talking to a non-standard server is the one that can't survive a non-standard header. - -Grading: Major severity (grading the being-in-it — the diagnostic tool produces no report at all, and =net doctor= goes with it, on precisely the broken network it exists to diagnose) × rare edge case (needs a failing probe *and* a portal appliance that omits a numeric offset) = P2 = [#B]. - -Fixed as dotfiles =8933500= (committed locally, deliberately NOT pushed — held for Craig's morning review). A naive parse is now read as UTC, which is what =-0000= means. Two tests, and the second is the one that matters: it drives a *current* =-0000= timestamp and asserts no clock row, so a lazy "catch =TypeError= and return None" fix would fail it while the correct reading passes. -** DONE [#C] a tunnel import that can't be disarmed still reports success :bug:net:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Found in the 2026-07-24 sentry bug-hunt, round 4, reading =net/src/net/manage.py=. =import_config()= imports a WireGuard or OpenVPN config as an NM profile, then fires =nmcli connection modify <uuid> connection.id <name> connection.autoconnect no= — and discarded the result, returning =ok=True= regardless. - -That modify is the whole safety of the feature, and the module's own docstring says so: =nmcli connection import= *auto-activates* the profile it creates, "which nobody asked for by picking a file", so "every import here ends with the profile deactivated and autoconnect off". A failed modify inverts that. For WireGuard — a device-type connection — autoconnect stays on, so the tunnel re-arms itself at the next boot and takes the default route with it, and the profile keeps the transient staged interface name (=wgpvpn=) while the envelope reports the config's real name, so the panel names a profile that isn't there. - -CORRECTION (2026-07-24, from an adversarial re-review): the blanket claim originally written here — that a failed disarm re-arms the tunnel at boot — is wrong for OpenVPN. =man 5 nm-settings-nmcli= states autoconnect is not implemented for VPN profiles, and an OpenVPN import is an NM VPN profile, so the modify is near-cosmetic on that half. The bug is real and security-relevant for WireGuard, which is the primary case; the severity as stated overreached to cover both. - -The asymmetry, again the tell: =_nmcli_import()=, twenty lines up in the same file, checks its own =returncode= and raises rather than return a UUID it did not get. The modify below it never got the same treatment. - -Grading: Major severity (grading the being-in-it — a full-tunnel VPN the user never asked to connect arms on every boot and carries all their egress, it persists across reboots rather than self-healing, and the affirmative "imported X" is what removes the reason to check) × rare edge case (needs the modify to fail after the import succeeded) = P3 = [#C]. - -Fixed as dotfiles =e0d4d8a= (committed locally, deliberately NOT pushed — held for Craig's morning review). New =_disarm()= returns whether the modify took. On failure the profile is still deactivated first — the import already brought it up, and the verdict shouldn't decide whether it keeps running — and then a =disarm-failed= envelope names the UUID and the exact command to finish the job. Three tests: the failing verdict, =import_configs= counting it as failed rather than imported, and a characterization test pinning that the deactivate still runs on the failure path. -** DONE [#C] a binary that can't be exec'd crashes the panels instead of degrading :bug:net:bluetooth:audio:maint:solo: -CLOSED: [2026-07-24 Fri] +** TODO [#B] Proton static WireGuard profiles pass no traffic :chore:network: :PROPERTIES: -:LAST_REVIEWED: 2026-07-24 +:LAST_REVIEWED: 2026-09-09 :END: -Found in the 2026-07-24 sentry bug-hunt, round 5, comparing the four panel packages' subprocess wrappers against each other. - -Every wrapper in the panels states the same contract: an unusable tool becomes a degraded result, never an exception. =cmd.run= returns None; =nmcli.run=, =btctl.run= and =pactl.run= raise their own domain error, which every caller already guards on; =speedtest.run_speedtest= returns an error envelope. All of them caught only =FileNotFoundError=, so they kept the contract for a tool that is *absent* and broke it for a tool that is *present but unusable*. - -Verified against Python 3.14.6 rather than argued. =subprocess.run= raises =PermissionError= for a file without its execute bit, =OSError= (ENOEXEC, "Exec format error") for an executable file that is neither a binary nor a script with a shebang, =NotADirectoryError= when a path component is a plain file, and =OSError= when a fork is refused under memory or PID pressure. None of the four is =FileNotFoundError=, so each escapes the guard: waybar's net/bt/audio modules die rather than dimming, and a maint probe takes the whole envelope with it — in exactly the machine state maint exists to report on. +wg-US-CA-144, wg-US-TX-714 and wg-NL-781 (all on wgpvpn) complete a WireGuard handshake and answer ICMP at 10.2.0.1, then forward nothing: no DNS on any transport, no HTTPS payload, no IPv6. The same account over the Proton CLI works, so the static configs are what Proton stopped honoring (the shape of an expired certificate on the profile). Diagnosed 2026-09-01. -The asymmetry, and this codebase had already ruled on it three separate times: =net/iw.py='s =signal_dbm= and =settings/spawn.py='s =detached= both catch =(OSError, subprocess.TimeoutExpired)=, and =audio/cmd.py='s doctor-tier =probe()= enumerates =FileNotFoundError=, =NotADirectoryError= and =PermissionError= as "absent" under a docstring promising it never raises. Its sibling =run()=, twenty lines up in the same file, kept the narrow catch — as did all five copies of =run()= and all three tool wrappers. =audio/status.py='s docstring records that this same class already bit once ("the bar's audio module died rather than dimming"); that fix widened the guard's *scope* and left its *exception set* alone. +The net doctor now names these as a dead tunnel and brings them down (dotfiles f56fd1a), which gets the machine back online but doesn't restore the tunnels. Two ways out: re-download the WireGuard configs from the Proton dashboard and re-import them (nmcli connection import type wireguard file ...), or drop the static profiles and use the Proton CLI only. Needs the Proton account, so not solo. -Grading: Major severity (grading the being-in-it — the status surface is dead while the condition holds, and for maint the tool that reports the fault is the one that dies of it; no data loss, and it clears when the tool or the pressure does) × rare edge case (needs a binary with wrong permissions, a lost shebang, or a fork refused under pressure) = P3 = [#C]. - -Fixed as dotfiles =44fdae1= (committed locally, deliberately NOT pushed — held for Craig's morning review). Widened to =OSError= across net, bt, audio, maint and panelkit — five =cmd.run= helpers, the three tool wrappers, =probe._curl= and =speedtest.run_speedtest=. The domain-error wrappers keep their "<tool> not found" message for a genuinely absent binary and add a second arm naming the errno for an unusable one, so the report can still tell the two apart. 28 tests, one class per package, driving all three exec failures against real files on a temp PATH; each was watched failing against unmodified production code first (27 red). Audio's class carries a characterization case pinning =cmd.probe='s existing behavior, so the sibling that got this right can't regress into the one that didn't. -** DONE [#C] a failed pty-backed spawn strands both ends of the pty :bug:net:bluetooth:solo: -CLOSED: [2026-07-24 Fri] +** TODO [#C] Declined dot-link-restore branch untested :test:network:dotfiles:solo:quick: :PROPERTIES: -:LAST_REVIEWED: 2026-07-24 +:LAST_REVIEWED: 2026-09-09 :END: -Found in the 2026-07-24 sentry bug-hunt, round 6, auditing the =subprocess.Popen= sites the round-5 fix didn't reach. - -Two spawns open a pty before launching and catch only =FileNotFoundError= around the =Popen=: =bt/pairing.py='s =pair_interactive= (bluetoothctl under a pty so the passkey agent is interactive) and =net/speedtest.py='s =run_speedtest_stream= (speedtest-go under a pty because it buffers everything to exit when piped). Both are the same exec-failure class as =44fdae1= — a binary present but not executable raises =PermissionError=, a lost shebang raises =OSError= — and neither is =FileNotFoundError=. - -What makes these worse than the =run= wrappers is where the cleanup lives. =os.close(master)= and =os.close(slave)= sit *inside* the =FileNotFoundError= arm, so an escaping =OSError= skips them: every failed attempt strands two descriptors. Both call sites are buttons in a long-lived panel process — the pairing flow and the console's SPEED key — and a user who gets no feedback presses again, so the leak accumulates under exactly the conditions that caused it. - -Grading: Major severity (grading the being-in-it — a descriptor leak in a process meant to run for days, on a path the user retries, plus the exception escaping a documented "(ok, detail)" / error-envelope contract) × rare edge case (needs an unusable bluetoothctl or speedtest-go) = P3 = [#C]. - -Fixed as dotfiles =c2eb3e1= (committed locally, deliberately NOT pushed — held for Craig's morning review). An =OSError= arm on each closes both ends and returns the module's own failure shape, naming the errno. Four tests: two pin the return contract, two count =/proc/self/fd= across three attempts — the fd count is what actually fails against unmodified code, and it was watched failing before the fix. - -The wider sweep this came from is recorded so it isn't repeated: every =except FileNotFoundError= in production was enumerated. The other exec sites were already correct (=maint/gui.py= x3, =net/kick.py=, =timer/engine.py= x2, =timer/gui.py=, =net/repair.py= x2, =audio/peak.py= all catch =OSError=), and the remaining hits are file-open catches, not exec. =clock/__main__.py='s =toggle()= has no guard at all but spawns =sys.executable=, which is by definition runnable; not filed. -** DONE [#C] one impatient client kills the clock panel's toggle listener for good :bug:clock:waybar:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Found in the 2026-07-24 sentry bug-hunt, round 6, sweeping every acquired resource (pty, socket, mkstemp, tempdir) for cleanup that isn't in a =finally=. - -=clock/src/clock/app.py='s =_listen()= guards =accept()= with =except OSError: return= and leaves the request body — =recv=, =runtime_log=, =sendall= — outside any guard. =send_toggle()= in =__main__.py= gives the panel 0.25s to acknowledge, then closes. An ack later than that hits a dead peer and raises =BrokenPipeError=, which escapes the =while= loop and ends the listener thread. - -Verified empirically, not argued: a client that connects, sends, and gives up after 250ms makes the server's =sendall= raise =BrokenPipeError= (errno 32) and the listener thread exits. - -What makes it Major rather than a nuisance is that it neither self-heals nor announces itself. The socket file stays bound, so every later =clock toggle= still *connects* — then stalls the full 250ms, gets no reply, and falls through to spawning =clock serve=. GTK's single-instance forwarding turns that into =do_activate= on the running service, and =do_activate= calls =show_clock()=, not =toggle()=. So from the first bad client onward, clicking the waybar time module opens the panel every time and never closes it; the only ways out are the right-click dismiss inside the panel or restarting the service. Nothing logs it. - -Grading: Major severity (grading the being-in-it — the toggle is one-way from then on, it persists for the life of the service, and there is no signal it happened) × rare edge case (needs a reply to miss the 250ms budget: a busy main loop mid-redraw, a slow runtime-log write, or an interrupted =clock toggle=) = P3 = [#C]. - -Fixed as dotfiles =7c02614= (committed locally, deliberately NOT pushed — held for Craig's morning review). An =OSError= arm around the request body scopes a dead peer to its own request, mirroring the guard =accept()= already had. =GLib.idle_add= runs before the ack, so the user's click still takes effect — only the acknowledgement is lost. New =tests/clock/test_socket.py=, 3 tests driving the real =_listen= against a stand-in owner (it touches only =self._socket= and =self.toggle=, so no Gtk.Application is needed). The gate is the second toggle after an impatient first: it times out on unmodified code because no listener is left. The other two pin what the fix must preserve — the toggle fires even when the ack can't be delivered, and an unknown command is still answered without toggling. - -Left alone deliberately: =do_activate= calling =show_clock()= rather than =toggle()=. Changing it would alter what a cold =clock toggle= does on first launch, which is a design call for Craig rather than part of this defect. Worth raising if he ever wants the spawn path to toggle too. -** DONE [#B] fuzzel breaks the pinentry protocol loop on every passphrase :bug:security:gpg:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Found in the 2026-07-24 sentry bug-hunt, round 7 — from the live journal rather than from reading. Grepping this boot for tracebacks turned up four instances of =pinentry-fuzzel: line 36: read: 0: read error: Resource temporarily unavailable=, and every one sits 4-7 seconds after a =GETPIN= (the time it takes to type a passphrase). The =BYE= handler's log line never appears once. - -=hyprland/.local/bin/pinentry-fuzzel= speaks the Assuan pinentry protocol on a pipe gpg-agent keeps open, reading one command per iteration of =while read cmd rest=. The =GETPIN= arm shells out to fuzzel, which *inherits that pipe as its stdin*. fuzzel runs an event loop over its own input, so it sets =O_NONBLOCK= on fd 0 — and =--dmenu= would read the pipe as menu items besides. The flag lands on the shared open file description and outlives fuzzel, so the shell's next =read= fails with =EAGAIN= and the loop ends mid-protocol. - -Grading: Minor severity (the passphrase is delivered *before* the break, so decrypts still succeed and nothing is corrupted — what's lost is everything after: =BYE= is never acknowledged, and gpg-agent's same-connection retry after a wrong passphrase, =SETERROR= then =GETPIN= again, can't be served; that retry is what the script's "reenter" label exists for, and it has never once been reachable) × every user, every time (four for four in the journal, and the test reproduces it deterministically) = P2 = [#B]. - -Fixed as dotfiles =e727dcd= (committed locally, deliberately NOT pushed — held for Craig's morning review). =< /dev/null= on the fuzzel call, so the non-blocking flag lands somewhere harmless; =--lines 0= was already there, so no menu input was ever wanted. =ENABLE_LOGGING= became env-overridable as a test seam — the script logs through an absolute =/usr/bin/logger= that PATH can't shadow, so without it every test run would write ten lines into the real journal. - -New =tests/pinentry-fuzzel/=, 8 tests driving the real script over a live pipe the way gpg-agent does. The fake fuzzel sets =O_NONBLOCK= on whatever fd 0 it is handed, exactly as the real one does, which is what makes them a gate rather than a restatement of the fix. Four fail against unmodified code — one reproducing the journal's message verbatim — and one records the fd fuzzel was given, pinning the cause rather than the symptom. - -THE CALIBRATION NOTE, and it is about my own earlier sweep. This is the same shape as round 1's =a57c443= (ffmpeg draining the pipe a =while read= loop was consuming). Round 1 swept both repos for siblings of that bug and came back empty — because it searched for the *mechanism* (a child that drains stdin) rather than the *shape* (a child that inherits stdin at all inside a read loop). Two different mechanisms, one shape, and the narrower search missed a live daily-use instance. Scope a class sweep by shape, not by the mechanism of the first instance found. -** DONE [#C] a truncated webcam record strands every camera off :bug:settings:privacy:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Found in the 2026-07-24 sentry bug-hunt, round 8, sweeping production for non-atomic file writes. - -=settings/src/settings/webcam.py='s =_record()= wrote =~/.local/state/settings/webcam.json= with a plain truncate-in-place =open(path, "w")=. That record is the only route back on, and the module docstring says so: deauthorizing a camera removes its video4linux nodes, so =usb_devices()= returns nothing afterward and =_recorded()= becomes the sole source of the paths to re-authorize. A write that truncated and then failed left an empty file; =_recorded()= caught the resulting =JSONDecodeError= and returned =[]=; =_known_devices()= then had nothing; and =set_power(True)= returned None without re-authorizing anything. Every camera stranded off, with no way back through the panel until a replug or a reboot. - -The asymmetry, seventh instance of this read: six other state writers in the tree already write through a temp file and a rename — =maint/cache=, =net/cache=, =audio/ptt=, =timer/engine=, =settings/store=, =maint/curation=. The one whose loss is most expensive was the one that didn't. - -Grading: Major severity (grading the being-in-it — the privacy switch becomes one-way, the panel offers no route back, and the user has to know to replug the camera or write sysfs by hand; bounded by the fact that a reboot re-enumerates USB and restores authorized=1) × rare edge case (needs a crash or ENOSPC inside a microsecond-wide write window) = P3 = [#C]. - -Fixed as dotfiles =8b40b79= (committed locally, deliberately NOT pushed — held for Craig's morning review). =_record= now mirrors =store.save=: =mkstemp= in the target directory, write, =os.replace=, unlink the temp on any failure. Four tests; the gate is a =_record= whose =json.dump= raises, after which the previous record must still be readable — it isn't on the old code. The other three pin what the fix must preserve: no temp-file residue, the =_recorded()= round trip, and the end-to-end power-off/power-on with the class symlinks removed, which is the scenario the record exists for. - -HOW IT WAS FOUND, and it confirms round 7's lesson twice over. Round 4 ran an atomic-write sweep and reported "nine sites, six unique-per-writer, three sharing a fixed =.tmp=" — it enumerated the writers that *were* atomic and compared their temp-file naming, and never asked which state writers aren't atomic at all. Same narrowing that made round 1's stdin sweep miss the pinentry bug: the sweep was scoped to a property of the instances already found rather than to the shape of the hazard. -** DONE [#D] a failed wallpaper apply reports "nothing to apply" :bug:settings:quick:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as dotfiles =2cf3fb3=. The decision moved to =gui.wallpaper_apply_toast=, a module-level pure helper, because the callback lives inside a GTK widget where no test can reach it. 5 tests. -Found in the 2026-07-24 sentry bug-hunt, round 7, sweeping the settings panel's worker callbacks. - -=settings/gui.py='s =_async= passes an exception through as the *result* rather than as a separate error argument, so every =done= callback has to test =isinstance(res, Exception)=. Five do — =_mx_pin=, =_mx_letter=, =_after_matrix=, =_set_pointer=, the drum/dial/gallery/refresh callbacks. =_wp_apply= is the one that doesn't: - -#+begin_src python -def _wp_apply(self, note="Wallpaper set"): - self._async(lambda: panel.wallpaper_apply(self.state), - lambda ok: self._toast( - note if ok is True else "nothing to apply", - good=ok is True)) -#+end_src - -=panel.wallpaper_apply= calls =store.save=, which can raise =OSError= (disk full, a permissions change on the config dir). The exception then arrives as =ok=, =ok is True= is False, and the toast reads "nothing to apply" — describing a no-op when the apply actually failed. The toast is at least marked =good=False= (red), so the user gets a negative signal; what's lost is the reason, which every sibling callback surfaces via =str(res)=. - -Grading: Minor severity (wrong text on an error path, correctly marked as a failure, nothing corrupted) × rare edge case (needs =store.save= or =wallpaper.apply= to raise rather than return False) = P4 = [#D]. - -Fix: give it the same =isinstance(res, Exception)= arm its five siblings have — toast =str(res)= on an exception, keep the current two-way message otherwise. One callback, three lines. -** DONE [#D] two manage.py nmcli reads sit outside their own error conversion :bug:net:quick:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as dotfiles =2cf3fb3=. =_key_mgmt= converts both nmcli exceptions to "", which both call sites already treat as neither wpa-eap nor sae. 2 tests, including one driving =_classify_up_failure= end to end. -Found in the 2026-07-24 sentry bug-hunt, round 4, reading =net/src/net/manage.py=. =nmcli.run()= raises =NmcliTimeout= on timeout and =NmcliError= on a missing binary, and every mutation in this module is written to convert both into a result envelope. Two calls escape that conversion because they run through =_key_mgmt()=, which wraps =nmcli.get_value= and catches nothing: - -- =edit()= line 243 calls =_key_mgmt(uuid)= for the enterprise-profile refusal *before* its own =try=, while the next four lines catch exactly those two exceptions around =nmcli.run=. -- =_classify_up_failure()= calls it on =up()='s failure path, so a slow =connection show= turns a classifiable activation failure into an exception. - -Consequence is a leaked exception where the caller expected an envelope. The panel absorbs it — =gui.bg()= catches =Exception= and renders =str(e)= — so there it degrades to a worse message rather than a crash. =net edit= from the CLI has no such catch and prints a traceback. - -Grading: Minor severity (the operation fails either way; what's lost is the classified message, and only the CLI path shows a traceback) × rare edge case (=connection show= has a 2s timeout and nmcli's presence is already established by the time either site runs) = P4 = [#D]. - -Fix: give =_key_mgmt= the same conversion its callers use — catch =(nmcli.NmcliError, nmcli.NmcliTimeout)= and return "", which both call sites already handle correctly (neither "wpa-eap" nor "sae"). One =try= in one helper covers both sites. -** DONE [#D] three atomic writers share one fixed .tmp name :bug:quick:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as dotfiles =2cf3fb3=. All three carry =.tmp.$(getpid)=, matching the six writers that already did. 6 tests across audio and maint. -Found in the 2026-07-24 sentry bug-hunt, round 4, sweeping both repos for the temp-file half of the atomic-write idiom. The tree writes state atomically in nine places, and six of them make the temp path unique per writer: =net/cache.py= and =timer/engine.py= both use =f"{path}.tmp.{os.getpid()}"=, and =settings/store.py=, =settings/idle.py=, =bt/repair.py=, =net/probe.py= all use =tempfile.mkstemp=/=NamedTemporaryFile=. Three use a bare =path + ".tmp"=: - -- =audio/src/audio/ptt.py= =write_state= (the lead carried over from round 3's Next Steps) -- =maint/src/maint/cache.py= =put= -- =maint/src/maint/curation.py= =_write_user= - -=os.replace= makes the *rename* atomic, but a shared temp name is not: two writers open the same path, the second truncates under the first, and the file that gets renamed into place is a blend of both. The loser's own =os.replace= then raises =FileNotFoundError=, because the winner already renamed the name out from under it. - -Real concurrent-writer pairs exist for two of the three. =maint/cache.py= =updates_repo= is written by =maint-net-scan.timer= hourly and again by =doctor._fresh_pending()= at UPDATE fire time. =audio/ptt.py= has three writers by design (the CLI toggle bound to a key, the waybar right-click, and the GTK panel) — its module docstring says so. =curation.py= is written by panel key presses and CLI verbs. - -Grading: Minor severity (every reader degrades rather than crashes — =cache.get= catches =ValueError= and reports no data, =read_state= reads a torn file as disarmed, and both recover on the next write; the sharpest edge is the loser's =FileNotFoundError= aborting the rest of =scan_net=, which the next hourly run repairs) × rare edge case (the write window is a millisecond or two, and the overlapping writers are an hourly timer against a human keypress) = P4 = [#D]. - -Fix: give all three the =f"{path}.tmp.{os.getpid()}"= form the two careful siblings already use. It is three one-line changes and needs no new abstraction. Note this closes the torn-file half only — the read-modify-write in =ptt.toggle_plan= and =curation.set_preference= can still lose an update between two writers, which wants a lock rather than a temp-name change and should stay a separate decision. -** DONE [#C] dmenuexitmenu word-splits its menu so no entry matches :bug:dwm:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Found in the 2026-07-24 sentry bug-hunt (shellcheck SC2128). =dwm/.local/bin/dmenuexitmenu= line 4 expands the menu unquoted: =choice=$(echo -e $menuitems | dmenu ...)=. Word-splitting collapses the runs of spaces the labels carry, so dmenu shows =Lock= where the =case= arm expects =Lock = (two spaces) and =Logout = where the arm expects a trailing space. No arm matches, so choosing an entry does nothing at all. - -CORRECTION (2026-07-24): THE BUG AS FILED DOES NOT EXIST. Ran it. =echo= rejoins the words split off the unquoted expansion with single spaces, and no label carries two spaces, so quoted and unquoted produce byte-identical output — verified against the exact literals from git rather than a retyped copy. Every =case= arm matches and every menu action works. - -What is real is latent. An unquoted expansion collapses a double space and glob-expands a =*=; the second was demonstrated turning a label into a directory listing. No current label triggers either. - -Hardened anyway in dotfiles =2cf3fb3= as robustness, not as a bug fix: the expansion is quoted and the bogus one-element array is now a plain string. Output confirmed unchanged byte-for-byte. New =tests/dmenuexitmenu/= (10 tests) pins the working behaviour, and shellcheck on the file drops from three findings to one. - -SECOND SENTRY FILING DISPROVED BY RUNNING IT, after =a57c443= (mkplaylist). Both came from a shellcheck hit plus reasoning, neither was executed. A static-analysis finding says a construct is unsafe, not that it currently misbehaves, and both filings treated the first as the second. -** DONE [#C] Timer module hero hierarchy :feature:waybar:timer:quick:solo: -CLOSED: [2026-07-24 Fri] -From the roam inbox (Craig, claimed 2026-07-22). Which display ("hero") wins the waybar timer module when several timer modes run simultaneously: pomodoro wins over everything (the user is actively working; it's likely their main focus). The rest rank in chronological order of when they would ring. Worked example: with a just-started 15-min timer, a 1-hr timer at 10 minutes left, a pomodoro, and an alarm ringing in 12 minutes — show the pomodoro; when it completes, the 1-hr timer (rings first), then the alarm, then the 15-min timer. Feeds the timer-panel spec (docs/specs/2026-07-02-timer-panel-spec.org). - -Shipped as dotfiles =9eedb39=. Pomodoro wins the hero, then soonest-to-ring, in both selectors (=engine.select_primary= for the bar, =panel.primary_id= for the GTK hero). Craig's worked example is a test. FLAGGED FOR CRAIG: the two selectors diverge on a *ringing* alarm (the bar excludes it, the panel gives it the hero) and I left that as-is rather than reverse a deliberate choice. Whether to unify them is your call. -** DONE [#C] Timer module: drop RING message, persistent notifications :bug:waybar:timer:quick:solo: -CLOSED: [2026-07-24 Fri] -From the roam inbox (Craig, claimed 2026-07-22). Remove the RING message from the timer module display; verify all timer and alarm notifications are persistent; the icon returns to normal once the notification has fired. Rationale: keeps timers and pomodoros from interfering with one another's displays (pairs with the hero-hierarchy task above). - -Shipped as dotfiles =9eedb39=. The tooltip no longer prints RING or a (ringing) suffix; a fired alarm shows its clock time and its persistent notification carries the alert. Verified the timer and alarm completion notes already set persist=True. -** DONE [#C] PTT icon outline removal :bug:waybar:quick:solo: -CLOSED: [2026-07-24 Fri] -From the roam inbox (Craig, claimed 2026-07-22): the waybar PTT icon should not have an outline. Cosmetic × every-glance = P3 = [#C]. - -Shipped as dotfiles =e63c0cf= (live style.css + dupre theme source). Removed the amber/green text-shadow glow from the armed/talk states, the only outline-like effect on the icon. FLAGGED FOR CRAIG: this is my read of "outline" (the glow). If you meant the glyph shape itself, it's a one-line revert. Confirm live by pressing PTT. -** DONE [#B] Dotfiles tests leak state across files :bug:test:dotfiles:solo: -CLOSED: [2026-07-23 Thu] -Resolved 2026-07-23 as dotfiles =c333598=. The polluter was =tests/weather/test_weather.py=, and it accounted for all 38 failures on its own. - -The mechanism was not the env leak the body below guessed at — tests/weather never writes =os.environ=. Its whereami fake did =weather.subprocess.run = ...= on a freshly-loaded module object. The fresh module isolated the weather code, but =weather.subprocess= is the one shared stdlib module object every module in the process holds, so the assignment replaced =subprocess.run= process-wide and never restored it. Every later test file got weather's fake result back from =subprocess.run=; the tell was wtimer asserting on =r.returncode= and getting "'R' object has no attribute 'returncode'", where =R= is weather's fake result class. - -Triage: TEST HYGIENE, not production global state. The weather script reads env at import and never writes, so no long-lived-process caching defect sits behind it. A scan for the same pattern (patching a stdlib module attribute reached through another module's namespace) finds exactly one instance in the suite — the three other =setattr= sites all snapshot and restore. So the planned shared env helper across 28 files was aimed at the wrong target and wasn't needed. - -Fix: rebind the loaded module's own =subprocess= name to a stub namespace, so nothing outside that module changes and there is nothing to restore. - -Gate: =make test= now runs two gates per the add-don't-replace decision — =test-forked= (one process per file, catches order dependence) and the new =test-shared= (every suite in one process, catches leakage). Built on stdlib unittest rather than pytest, since pytest was only the diagnostic tool and isn't a project dependency. Verified as a real gate, not just green today: with the defect deliberately reintroduced it goes red, and green once restored. A focused test in tests/weather pins the invariant on the culprit as well, because the shared gate alone blames the three victim files. - -Verification: 3500 tests, both gates, exit 0. - -Original finding follows. - -Found 2026-07-23 during the speedrun. =make test= is green, but it runs each test file in its own =python3 -m unittest= process, which hides cross-file state leakage. A single-process whole-tree run (=python3 -m pytest tests/ -p no:randomly=) fails 38: 22 in =tests/wtimer/test_wtimer.py=, 10 in =tests/zoom-web/test_zoom_web.py=, 6 in =tests/wlogout-menu/test_wlogout_menu.py=. - -Not a regression — a worktree at the pre-speedrun commit produces the identical 22/10/6 profile, so this predates tonight's work. Those three files also pass cleanly when run together (170 passed), so the polluter is a fourth file somewhere in the tree that mutates global state (env var, cwd, or a module-level patch) without restoring it. 28 test files write =os.environ= directly. - -Why it matters: the green gate can't see this class of bug, so a real isolation defect — or a genuine failure that only appears under a different order — passes CI silently. Bisect by running the tree with subsets until the polluter is identified (pytest's =-p no:randomly= keeps the order stable while bisecting), fix its cleanup, then decide whether =make test= should gain a single-process pass so the gate covers it. -** DONE [#B] Wallpaper view freezes the panel — thumbnail decode :bug:dotfiles:solo: -CLOSED: [2026-07-23 Thu] -Craig reported 2026-07-23: selecting the wallpaper button freezes the module and the compositor asks whether to kill it. Root cause proven: =_Thumb._draw= decoded each source image with =new_from_file_at_scale= on the GTK main thread. Measured on Craig's 78 wallpapers — a viewport of the 8 largest takes 3.7s, the whole set 13s. That block trips Hyprland's "not responding" watchdog. - -Grading: Critical severity (panel unusable, watchdog kill) × every user every time the wallpaper view opens = P1 = [#A] by the matrix. Held at [#B] because step 1 already shipped and removes the user-visible freeze; the remainder is a latency enhancement, not a showstopper. - -*** 2026-07-23 Thu @ 15:40 Step 1 — async decode (dotfiles f45f321) -Moved the decode to a worker thread via a new =settings/thumbcache.py= (pure, injected decode/scheduler/thread; 6 tests). The thumb shows its dark ground until the pixbuf lands, then redraws. Verified live on a headless output: worst main-loop stall opening the pair view dropped from multi-second to 68ms; the cache filled with 81 decoded pixbufs (the one miss is a .webm, correctly falling back to the ▶ glyph). Full suite 3512, both gates, smoke OK. This alone fixes the reported freeze. - -*** 2026-07-23 Thu @ 16:30 Step 2 — persistent on-disk cache (dotfiles 463cc4f) -Built the persistent layer: =settings/thumbstore.py= decodes each source once to a 512px PNG under =~/.cache/settings/thumbs=, keyed by path + mtime so an edited wallpaper self-invalidates. The hot-path decode reads that PNG and scales in-memory. Warming rides the existing =settings tick= CLI verb (the 2-min timer already runs it), building up to =WARM_PER_BEAT=8= missing thumbnails per beat — best-effort, journals a line on failure, never blocks the wallpaper flip. thumbstore is pure (stat/decode/load/save injected); 10 tests. - -Went with incremental warming (8/beat, ~10 beats to full) as the safe default rather than full-warm-on-change — the per-beat cap is a one-line flip if Craig wants it faster. Measured: hot-path decode of a viewport dropped from 3.7s cold to 47ms warm. No installer change (the tick service already runs =settings tick=); cache lives outside the repo. Full suite 3522, both gates, smoke OK, live panel verified (81 pixbufs render, 48ms worst stall warm). -** DONE [#C] Panel scrollbars too short :bug:dotfiles:quick:solo: -CLOSED: [2026-07-23 Thu] -Shipped 2026-07-23 as dotfiles =0d64837= (22px scrollbar, 16px trough, 14px slider thickness with a 48px floor along the travel axis). Left open by oversight during the speedrun; closing now. - -Follow-on, and my own regression: enlarging the bar to 22px is what made it start covering the thumbnails, because nothing grew the tray to match. Craig reported it the same day ("scrollbars that obscure the images") and it's fixed in =c0ddf57= — the tray now reserves a 22px lane for the bar as a margin on the scrolled box, so the bar sits below the images instead of across them. Measured before: tray 68px, content 68px, a visible 14px bar inside the same 68px. After: tray 90, content 68, bar clear. The lane is a constant under the scrollbar CSS with a note to keep the two in step, since the coupling between bar thickness and tray height is exactly what broke. - -From the roam inbox (Craig, claimed 2026-07-23): all scrollbars need to be much taller than before. The always-visible scrollbars shipped in 7e8eb4a set =min-height: 10px; min-width: 10px= on the slider (=settings/src/settings/gui.py=, the =.dupre-panel scrollbar slider= rule) — that's the floor for a short slider, and the trough itself is thin. Raise both the slider floor and the trough thickness so the bar is comfortably grabbable. Cosmetic × every glance at the wallpaper trays = P3 = [#C]. -** DONE [#C] Video wallpapers don't fit the desktop :bug:dotfiles:solo: -CLOSED: [2026-07-24 Fri] -From the roam inbox (Craig, claimed 2026-07-23): videos don't fit the desktop in desktop-settings. The video channel drives mpvpaper (=settings/src/settings/wallpaper.py=); mpvpaper passes options through to mpv, so the fit is a =--panscan=/=--video-unscaled=/keepaspect question rather than a layout one. Reproduce with a video whose aspect differs from the output, pick the mode that fills without distorting (cover, matching how the image channels behave), and cover it in the wallpaper tests. Minor severity × whenever the video channel is selected = P3 = [#C]. - -Shipped as dotfiles =04d1489=. =set_video= now passes =panscan=1.0=, so mpvpaper fills the output and crops the overflow instead of letterboxing; keepaspect stays on so nothing stretches. Tested against the mpvpaper arg log. -** DONE [#C] World-clock wallpaper arrangement :feature:dotfiles: -CLOSED: [2026-07-24 Fri] -Shipped 2026-07-24 as dotfiles =6afbe09=, iterated live with Craig. The grid of boxed mini-clocks became a centered vertical clock line: cities down a spine, west (Honolulu) top to east (Wellington) bottom, labels alternating both sides, no boxes. Each shows city / time (12h) / day+date / timezone region name ("US Central"). Day/night dimming + amber home carried over, title dropped, cursor restored over the desktop. Prototypes archived in archsetup 40216e7. The face is parameterized (=?layout=vertical|horizontal=, =?hour12=1|0=) so the panel pickers below can drive it. -** DONE [#C] Floating layout — should we? :feature:hyprland: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -From the roam inbox (Craig, claimed 2026-07-23): consider whether Hyprland should offer a floating layout — how it would work, the benefits, and the complexity. A brainstorm/spike, not a build: the deliverable is an assessment Craig reads and decides on, not a shipped layout. Not :solo:. When picked up, run it as a brainstorm — how a floating mode coexists with the current tiling binds (toggle keybind, per-workspace vs global, window-rule interactions), what it buys over the existing =togglefloating=, and the config/muscle-memory cost — then bring Craig the recommendation. - -CONCRETE PROPOSAL from a second roam item (Craig, 2026-07-24 via work) — "floating mode as the easiest mode": -- Can't select floating until at least one window is displayed. -- Entering floating freezes each window's position and floats it exactly where it is. -- During floating, drag windows with mod+mouse-drag. -- Exiting floating switches to tiling or monocle and lets that layout take over. -Craig's note: "simple, could be useful for different reasons." This is the design the brainstorm should evaluate first — assess feasibility against Hyprland's actual float/tile transitions (does freezing current geometry survive the tiling↔floating switch, does re-tiling on exit reflow cleanly) before recommending. - -ASSESSED, dotfiles =8cf4728=: =docs/2026-07-24-floating-layout-assessment.org=. Verdict: buildable and worth building on a capture-then-restore of window geometry (=hyprctl clients -j= gives at/size), which is a real gesture plain =togglefloating= can't express. Craig's four-rule proposal is folded in and each rule assessed. One taste call flagged (exit to previous layout vs always monocle). Ready to file a build task on Craig's go. -** DONE [#C] World clock wallpaper: bold the city names :feature:dotfiles:quick:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -From the roam inbox (Craig, 2026-07-24 via work): bold the city names on the world-clock wallpaper face (=settings/faces/world.html=, shipped =6afbe09=). Cosmetic × every glance at the world face = P3 = [#C]. Solo — a CSS weight change, screenshot-verifiable — but it's a visual call, so build it and show the render rather than close off a green suite. Pairs with the open world-face picker task. - -Shipped as dotfiles =e63c0cf=. =.lbl .city= is now =font-weight:700=. Rendered offscreen and confirmed the bold reads well over the time/zone lines; home city stays amber. Comparison render was on ws5 for Craig. -** DONE [#C] Floating clock toggles on control+mod+c :feature:dotfiles:hyprland:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -From the roam inbox (Craig, 2026-07-24 via work): a control+mod+c keychord should toggle the floating clock, the same as clicking the time waybar module. - -This answers the design question the round-6 clock-toggle fix deliberately left open (see the =clock toggle listener= DONE task above): =do_activate= calls =show_clock()= rather than =toggle()=, and the note there flagged "worth raising if he ever wants the spawn path to toggle too." He does. Build: a hyprland keybind bound to =clock toggle=, and confirm the toggle path (not show-only) fires whether the service is cold or warm. Solo — buildable and locally verifiable. - -Shipped as dotfiles =e73a70e=. =bind = $mod CONTROL, C, exec, clock-panel toggle= reuses the exact command the time module's click runs, so it toggles identically. Registered clean on reload. Live keypress is Craig's to confirm. -** DONE [#C] Calculator scratchpad won't toggle closed on mod+x :bug:hyprland:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -From the roam inbox (Craig, 2026-07-24 via work): =mod+x= opens the calculator scratchpad but doesn't close it — Craig has to kill the window by hand. A second =mod+x= should toggle it shut. Almost certainly a =togglespecialworkspace= vs plain =exec= binding in the hyprland config, or a scratchpad window-rule mismatch. Minor severity (a workaround exists: kill the window) × every time the calc scratchpad is used = P3 = [#C]. Solo — a keybind/window-rule fix, locally verifiable. - -Shipped as dotfiles =e73a70e=. New =calc-toggle= script (mirrors fuzzel-toggle: pgrep -x, pkill or launch), and =mod+X= now points at it, so a second press closes the calculator. 3 tests in tests/calc-toggle. -** DONE [#C] Saving and recalling window configurations :feature:hyprland: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -From the roam inbox (Craig, 2026-07-24 via work), a research idea: Craig wants to save a specific window+app arrangement and have it reappear on demand. What has to be known and built to make that happen — is there prior art (another WM or OS that does session/layout save-restore), what information do those need (app identity, geometry, workspace, launch command), and what are their rules. Explore how far Hyprland can get (hyprctl clients + dispatch, exec rules, window rules by class/title), document thoroughly, and review with Craig next time. Not :solo: — the deliverable is an assessment he reads and decides on, and it may spawn a build task once the shape is clear. Offer to file the build separately if part of it turns out urgent. - -RESEARCHED, dotfiles =8cf4728=: =docs/2026-07-24-window-config-save-recall-assessment.org=. Prior art surveyed (i3/sway =append_layout= swallow, KDE window rules, macOS Moom). Three tiers from cheapest: (1) reposition open windows — buildable + testable now; (2) relaunch + place by class rule; (3) full swallow-by-title, which hits the same-class ambiguity every tool hands back to the user. Recommends shipping tier 1; tiers 2-3 need Craig's call on how much manual disambiguation he'll accept. -** DONE [#C] Velox refresh sweep :chore:maint: -CLOSED: [2026-07-23 Thu] -From the roam inbox (Craig, claimed 2026-07-23): velox needs bringing up to date, the mouse/touchpad module is still there, investigate what else didn't move over. - -Resolved 2026-07-23 by a full sweep over tailscale. The touchpad module was already gone — velox's running waybar (started 01:05, after the reboot) and its tracked config both carry zero =custom/touchpad= entries; what Craig saw was the pre-restow waybar process from before the reboot, and the reboot cleared it. Sweep results: both machines at dotfiles f9b6404 (all three hyprland lock/exit fixes live on velox, config errors clean, =allow_session_lock_restore= reads true); stow restow clean, only the expected skip-worktree files; rulesets pulled to 50fc7ca and =make install= run (agent-text verified working by invoking it — an earlier "MISSING" reading was a PATH artifact of the non-interactive ssh shell, not a real gap); desktop-settings tick timer active; mpvpaper, power-profiles-daemon, gtk4-layer-shell, webkit2gtk all present. - -Genuine remaining differences, all per-machine installs rather than sync failures: =cmail-action=, =gcalcli=, and =playwright= aren't installed on velox, and =obsbot-wb-guard.service= isn't enabled there (the OBSBOT lives on ratio). None block anything; file separately if velox should send mail or drive browser tests. -** DONE [#C] Weather tooltip sunrise and sunset :feature:waybar:weather:quick:solo: -CLOSED: [2026-07-23 Thu] -Shipped 2026-07-23 as dotfiles =de62e9d=. The two rows sit directly below Humidity in the current-conditions block, rendered in the footer's 12-hour format (=%-I:%M %p=) so the tooltip reads one way throughout. - -Confirmed the no-extra-round-trip premise held: =sunrise,sunset= joined the existing =&daily== block. Split =forecast_url= and =reading_from= out of =fetch= so both the request and the reading are testable without network — that's what let the new cases cover a payload missing the fields. Six tests (Normal/Boundary/Error): row placement and format, a pre-change cache with no sun fields, an unparseable stamp, today's pair picked out of the six-day arrays, and the API omitting them. Reused the existing =_at= helper rather than adding a near-duplicate =_first=. - -Live-verified against the real API: sunrise 6:14 AM, sunset 7:59 PM for today in New Orleans, rendering in the actual tooltip. Full suite 3506 tests, both gates, exit 0. - -Open, not blocking: every other header row carries a glyph (thermometer, droplet, wind arrow) and the sun rows are plain text. The file's glyphs are marked font-confirmed codepoints, and I haven't verified a sunrise/sunset glyph renders rather than showing tofu, so I left them bare. Craig's call. - -From the roam inbox (Craig, claimed 2026-07-23): in the weather module's hover text, the section immediately after the location ends with the current humidity. Add the sunrise and sunset times for the current location directly below it. - -Cheap to source: the module already calls Open-Meteo with a =&daily== block (=common/.local/bin/weather=, the forecast URL around line 336), so =sunrise,sunset= joins that same request with no extra round trip — normalise_daily already parses the daily arrays. Times arrive as local ISO strings; render in Craig's canonical clock format rather than re-deriving one. The settings package's =suntimes.py= (pure NOAA math, no network) stays the offline fallback path if the API field is ever absent — don't duplicate its math here. -** DONE [#C] Maint doctor-row copy button :refactor:maint:quick:solo: -CLOSED: [2026-07-23 Thu] -Shipped 2026-07-23 as dotfiles =761fa5c=, "fix(maint): drop the COPY key from the doctor row" — the key and its orphaned handler removed from =maint/src/maint/gui.py=. =viewmodel.status_copy_text= stays: it's a tested pure serializer and the obvious source if a copy surface returns somewhere better placed. - -Correction to the body below: it describes a per-row button and a separate global one. There is only one COPY key, and it IS the global one Craig added in 8bc79ba two days earlier. He tried it and wanted it gone, so the row now reads DOCTOR · CLEAN UP · REVIEW & FIX. - -From the roam inbox (Craig, claimed 2026-07-23): remove the per-doctor-row copy button (next to REVIEW and FIX) from the maint status wall. The global COPY key (dotfiles 8bc79ba, "one global button copying rendered text") stays the one copy surface — the per-row button turned out to be clutter next to it. -** DONE [#C] WiFi tooltip signal strength :feature:waybar:network: -CLOSED: [2026-07-22 Wed] -From the roam inbox (Craig, claimed 2026-07-22): add signal strength to the WiFi tooltip. - -Resolved 2026-07-22: the tooltip's signal line existed but never fired on ratio — the mt7925 driver leaves /proc/net/wireless empty (legacy WEXT procfs unimplemented), so the dBm read returned None and the bar glyph fell to the weakest tier. Fix in dotfiles net/: an iw-dev-link nl80211 fallback (only spawns when procfs is empty), a signal_percent mapping, and an enriched line — Signal: ▂▄▆█ 100% · -32 dBm (excellent) — bars by band, percent, raw dBm, band word. The bar icon tier fixed itself as a side effect. -** DONE [#B] Desktop-settings dropdown panel :feature:waybar: -CLOSED: [2026-07-22 Wed] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-22 -:END: -Resolved 2026-07-22: shipped end to end via the "Build: desktop-settings panel" task (dotfiles 7a15237 → 9038eee; spec IMPLEMENTED, 85 suites + smoke 13/13 + e2e 17/17). Every open question below got settled in the spec: bar consolidation landed (74f723e), the wallpaper manager became the in-panel sub-view, and the format pickers split into their own sibling spec ([[file:docs/specs/2026-07-19-display-format-single-source-of-truth-spec.org]], DRAFT stub). Remaining human-eye checks live under "Manual testing and validation". - -Original body follows as the record. - -Initial spec written 2026-07-02: [[file:docs/specs/2026-07-02-desktop-settings-panel-spec.org]] (DRAFT — four decisions await Craig's review before build; architecture updated to the net panel's Blueprint/GTK4 stack). - -One waybar dropdown gathering the desktop toggles and sliders into a single settings panel, opened from a gear/settings glyph on the bar. Incorporate: -- *Auto-dim* toggle (the =custom/dim= feature just shipped — fold in here, or keep the standalone indicator and mirror it). -- *Brightness* slider (backlight, via brightnessctl). -- *Keyboard-backlight* brightness slider (brightnessctl on the kbd_backlight class). -- *Mouse* enable/disable toggle — shown only when a mouse is connected. -- *Trackpad* enable/disable toggle — shown only when a trackpad is connected (mirror =toggle-touchpad= / =touchpad-auto=). -- *Idle inhibitor* (the =custom/idle= module that replaced the built-in =idle_inhibitor= 2026-06-24 — toggles the hypridle daemon, state-synced icon). -- *Airplane mode* (the existing =airplane-mode= toggle; laptop-only). - -The conditional rows (mouse, trackpad, airplane) appear only when their hardware/context applies — reuse the laptop/device detection the airplane and touchpad indicators already do. - -Design / open questions (propose before building): -- Panel tech: sliders need a real toolkit (waybar can't host a slider), so a GTK4 + gtk4-layer-shell app like pocketbook is the likely shape. -- Which existing standalone bar modules (dim, touchpad, airplane, idle_inhibitor) collapse INTO this panel vs. stay on the bar as quick-access indicators. Craig's call. - -Implementation notes: a small GTK layer-shell app (mirror pocketbook's structure: src-layout Python package, pytest, Makefile) talking to brightnessctl / hyprctl / the touchpad + airplane helpers. Lives in the dotfiles repo or in-tree like pocketbook. TDD the backing toggle/slider logic. Sizable — worth a design doc first. - -Home handoff 2026-07-19 (inbox, resolving the open "few other things" decision — fold into the spec, close the open decision, extend the controls table, then run spec-review, may flip DRAFT→READY). Ownership: home drives the build (dotfiles settings/), archsetup keeps the canonical spec. Full reconciliation in home docs/design/2026-07-19-desktop-settings-module-brainstorm.org. -- ADD controls: night-light / color temperature; Do Not Disturb / notifications (dunst); lock / suspend quick actions; power profile (performance/balanced/saver); scenes/profiles — one control flipping several toggles at once (Focus, Presentation, Battery-saver, Night). Scenes are the payoff of consolidating everything. -- OUT (record reasons): volume / master-mute stays with the audio panel (no mirror here); theme light/dark goes to the theme-studio task. -- FORMAT PICKERS pulled to their own future sibling spec — time/date/weather format is out of THIS panel. Rationale: format settings live in many programs, so the design problem is a single source of truth for the canonical format. Track a future sibling-spec stub in docs/specs (time/date/weather format single-source-of-truth); Craig thinking it through separately, not started. -- STILL OPEN (spec already flags): wallpaper manager confirmed in scope, but row-that-opens-a-sub-view vs its own sub-spec undecided — resolve at spec-review. -** DONE [#C] Gallery probe: the fader-drag check is flaky :bug:test:design:quick:solo: -CLOSED: [2026-07-23 Thu] -Fixed 2026-07-23. Root cause confirmed rather than suspected: =panel-widget-gallery.html= line 74 sets =html{scroll-behavior:smooth}=, so =scrollIntoView= animates and the fixed 200ms sleep sometimes read =getBoundingClientRect= mid-scroll. The drag then dispatched at stale coordinates, the press missed the fader, and the check reported a dead widget. - -Fix: scroll with =behavior:'instant'=. The probe never needed the animation, so this removes the race instead of waiting it out. Also added a =settledRect= guard (rect stable across two reads AND on-screen) for zoom/column relayout, and a =hits()= assertion that the press actually lands on the fader before the drag goes out. +repair_tunnel_dot_off (dotfiles net/src/net/repair.py) puts a tunnel link back to its DoT mode when turning DoT off didn't bring names back, and the evidence says "put back to <mode>" only when that restore succeeded. The restore-declined branch has no direct test. Give fake-resolvectl a second failure switch (FAKE_RESOLVECTL_DOT_RESTORE_FAIL) so the wording can be asserted absent as well as present. Follow-up from the f56fd1a review. -Applied to the toggle-click check too — it shares the same fixed-sleep shape, and it failed for this exact reason during the diagnosis, so fixing only the fader would have left half the defect. - -Worth recording: my FIRST fix was wrong and made it worse. Polling until the rect stopped changing returned pre-scroll coordinates every time, because two identical samples are also what you get before the animation starts — an intermittent failure became a consistent one. The new hit-test assertion is what caught it, printing the press point at y=1326 against a 1200px window. That's the argument for asserting the press landed rather than only asserting the readout moved. - -Verified against the measured 1-in-6 failure rate: 8 consecutive runs, all three checks passing, with the press point identical every run (429,480) — deterministic, not lucky. Full probe 96 PASS, 0 FAIL, exit 0. - -=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); 2026-07-21 double-speedrun run (flashed one RED mid-run, passed on rerun). All were a session's first/early probe run — consistent with the stale-rect theory (cold-start relayout settling slower than the fixed 200ms sleep). -** DONE [#C] Dotfiles stow conflicts: first-launch risk + restow directory handling :bug:dotfiles:quick:solo: -CLOSED: [2026-07-23 Thu] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-14 -:END: -Closed 2026-07-23. The last open item was the velox check, and velox is reachable again. Pulled it from =02df01a= to =de62e9d= (clean tree, fast-forward), then ran =make conflicts hyprland=, which dry-runs every tier: "No stow conflicts", exit 0. Its old conflict copy had already cleared against the updated repo, so there was nothing for =make reset= to do. - -Verified the pull is live through the symlinks rather than just present in the repo: =~/.local/bin/weather= resolves into the dotfiles tree and returns today's sun times on velox. - -Note for the record: =make conflicts common= is not a valid invocation — =check-de= rejects it, because common and the host tier are auto-included in the DE-scoped run. =make conflicts hyprland= is the whole check. -*** 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 paused-machine check): -- Waypaper canonical decision (Craig): RESOLVED 2026-07-14 — dark-lion.jpg is canonical (dotfiles fea3e93), repo config.ini updated off the that-one-up-there.jpg placeholder. -- Ratio check: RESOLVED 2026-07-14 — ratio's =~/.config/calibre= is a directory symlink into the repo (stow folded the dir), so the first-launch gap never existed there. -- When velox is back online: run =make conflicts= / =make reset= there so its old conflict copy clears against the updated repo. (velox carries a separate boot-recovery task; check once it's reachable.) -** DONE [#B] Weather tooltip caching :feature:waybar:weather:solo: -CLOSED: [2026-07-25 Sat 10:53] -From the roam inbox (Craig, claimed 2026-07-22): retrieve the weather tooltip data once per hour and cache it. If the network is unavailable, display the cached tooltip with explanatory text saying so. Dotfiles-side work (archsetup owns the lifecycle); touches common/.local/bin/weather. -Verified complete in the 2026-07-25 batch: the weather CLI already had the hourly default TTL, fresh-cache no-fetch path, stale fallback, and explicit offline footer. Its 33-test suite and the full dotfiles suite pass. -** DONE [#B] Settings gear becomes four device toggles :feature:waybar:dotfiles:solo: -CLOSED: [2026-07-25 Sat 10:53] -From the roam inbox (Craig, claimed 2026-07-23): the waybar gear should become four icons — touchpad, mouse, webcam, and a notification bubble. Clicking each toggles that setting directly. The first three turn red when disabled; the bubble turns red when DND is enabled. - -Today =custom/settings= (=hyprland/.config/waybar/config=) is one gear glyph () whose only job is =on-click: settings-panel=. The toggles themselves already exist and are tested — the settings package owns touchpad, mouse, and webcam (=webcam.py= is the USB-authorized kill switch from 2026-07-22), so this is a bar-side surface over existing backends rather than new capability. - -Note the state-polarity split when wiring the colors: three read "red = off" and DND reads "red = on". That asymmetry is deliberate (red means "something is disabled that normally isn't, or suppressed that normally isn't"), so encode it per-icon rather than deriving one rule. - -Decided 2026-07-23 (Craig): the gear STAYS alongside the four toggles as the panel launcher. So the bar's right side grows from 12 modules to 16 — the four toggles are net-new, the gear keeps its =on-click: settings-panel=. Open sub-question for build time, not blocking: whether the four toggles are four separate waybar modules or one custom module rendering four glyphs (fewer layout entries, one exec). Pick at build; the four-module shape is simplest and matches how mic/net already sit as individual modules. -Shipped in the 2026-07-25 batch as four independent JSON modules over the existing verified settings backends. Touchpad, mouse, and webcam turn terracotta when disabled; DND uses the deliberate inverse polarity; unavailable hardware dims. The gear remains the panel launcher. The live and Dupre theme CSS copies stay byte-identical. -** DONE [#C] Wallpaper panel selection and scroll state :feature:dotfiles:solo: -CLOSED: [2026-07-25 Sat 10:53] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-25 -:END: -From the roam inbox (Craig, 2026-07-25). Screenshot: =~/pictures/screenshots/2026-07-25_013041.png=. Three related behaviors in the settings wallpaper panel (=settings/src/settings/wallpaper.py=): -1. Open at the wallpaper currently displayed, not the top of the list. -2. Highlight that wallpaper as selected in the scrollable pane while it shows in the preview. -3. Keep the scroll position when a picture is selected. Today selecting a picture snaps the scroll back to the top, which is the bug half of this. -Grade: minor scroll-reset defect x every panel selection = P3 = [#C]; the open-at-current and select-current behaviors are enhancements at the same level. One type tag, so filed =:feature:= with the scroll-reset called out as the bug. Solo: buildable in the settings GTK panel, agent-verifiable via headless capture plus the wallpaper.py tests, no design call — swww query gives the current wallpaper, and scroll-position preservation and row selection are standard GTK. -Shipped in the 2026-07-25 batch. The panel queries =awww query= off the UI thread, prefers the actually displayed image over stale stored state, highlights it, scrolls it into view on first open, and remembers the horizontal adjustment across selection-triggered rebuilds. -** DONE [#C] Net tooltip IPs and line order :feature:waybar:network:solo: -CLOSED: [2026-07-25 Sat 10:53] -From the roam inbox (Craig, claimed 2026-07-23): in the wifi hover, add the internal IP, external IP, and gateway IP just below the Interface line; move the Signal line to just above the keyboard-shortcuts line. Design constraint: the bar's hot path does no network I/O (status.py deliberately skips _address_facts on the 2s beat) — internal IP + gateway can ride cheap local reads, but the external IP must come from a cache the connectivity probe refreshes, never a live lookup in waybar-net. -Shipped in the 2026-07-25 batch. The slow connectivity probe caches local addressing and a validated external IP with the network identity; the Waybar hot path only reads that valid cache. Tooltip order is Interface, internal/external/gateway IPs, connectivity detail, throughput, Signal, shortcut. -** DONE [#B] Dupre Kit merge — casting additions :feature:tooling:solo: -CLOSED: [2026-07-25 Sat 10:53] -Fold docs/prototypes/dupre-kit-additions.js back into the kit proper: detentFader (NEW — multi-detent slide attenuator with speedbump drag physics: magnet + escape hysteresis, parked tick glow) and the drumRoller redefinition (UPGRADE — 1..N channels and min/max range; stock hardcodes two drums and throws on one, defaults reproduce stock exactly) and the guardedToggle redefinition (UPGRADE — lever throws with rotateX so it flips toward the viewer instead of the stock 180° planar spin that sweeps sideways mid-transition; contract unchanged). Merge means: builders into widgets.js, the additions CSS into DUPRE_CSS, additions-scoped gradients into the shared defs plate, gallery cards for both in panel-widget-gallery.html, and POLICY entries. Origin: the desktop-settings casting sitting 2026-07-21 — Craig's direction is that components get finished by being needed ("the ones needed most will have had the most attention"), so more additions may accrue here before the merge; batch them. -Shipped in the 2026-07-25 batch. =widgets.js= now owns all three builders, shared gradients/CSS, contracts, and policy records; additions no longer redefines them when older casting pages load it. The gallery has a three-detent fader card and a three-channel 0–12 drum demonstration (112 cards total). Static ownership tests, JS syntax checks, and the complete headless interaction probe pass. -** DONE [#C] Maint live-refresh hairline replacement :feature:maint:solo: -CLOSED: [2026-07-25 Sat 10:53] -: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. -*** 2026-07-21 Tue @ 08:35:00 -0500 Decided (Craig): silent-until-stale age caption -Replace the draining 2px hairline with an age caption that shows ONLY when refresh is overdue (e.g. "3s", "8s" once past the expected window) and shows nothing while the board is healthy. This keeps the stale-vs-frozen signal — a frozen board surfaces a growing age number, a live one stays clean — while removing the constant motion the hairline created. Implementation (dotfiles, archsetup-owned): drop =gui._hair_tick= / the hairline draw, add an overdue-age caption driven off =viewmodel.refresh_fraction= (or the last-refresh timestamp) rendered only past the live window. Now unblocked; needs a live visual check on the panel after. -Shipped in the 2026-07-25 batch. The animated draw area and 150ms timer are gone; the memory section header stays silent through the healthy three-second window, then shows a once-per-second growing age caption. Pure boundary tests and the full maintenance suite pass. -** DONE [#D] Test-framework + prototype refactor cluster :refactor:solo: -CLOSED: [2026-07-25 Sat 10:53] -Grading: no behavior change; parking lot. Refactors from the S5-S7 audit, distinct from the installer refactor rollup above. -scripts/testing/run-test.sh + run-test-baremetal.sh duplicate the run/poll/report skeleton and have drifted (VM uses setsid + copy helpers, baremetal uses nohup + hand-rolled sshpass scp) — extract the shared core so baremetal inherits the sturdier paths; run-maint-nspawn.sh:66 + run-maint-scenarios.sh:78 duplicate the transport-independent _scenario_var/_validate_scenario/run_scenario (a sourced lib/maint-scenario.sh); run-test.sh:251,265 uses two different mechanisms (pgrep vs ps|grep) for the same liveness check; docs/prototypes/gen_tokens.py:78 repeats the section-iteration skeleton across four emitters; gallery-widget.el:95,136 hardcodes SVG arc/hub path strings that duplicate the cx/cy/radius geometry (dial desyncs silently on a constant change); gallery-widget.el:72,84 leans on the private svg--append. See findings doc (S5, S6, S7). -Completed test-first in the 2026-07-25 batch. QEMU and bare-metal runners share liveness/report helpers; maintenance transports share scenario validation/execution; token emitters share ordered section traversal; and the Emacs SVG gauge shares semicircle geometry and uses the public DOM append API. Every fast Python/ERT suite passes. -** DONE [#B] Two agent sessions sharing one git repo :chore:tooling: -CLOSED: [2026-07-26 Sun] -Craig approved the shared-rules-layer solution on 2026-07-26. - -Use one repository-scoped publish lock for every session and worktree sharing a clone. Derive the lock name from the real Git common-directory path; hold it across reconcile, stage, staged review, and commit; track the owning session and reviewed staged-tree fingerprint; refresh it after conversational waits; and repeat the staged review if ownership or the fingerprint changed. Ordinary working-tree edits remain concurrent. - -An approval waiver never waives the staged review, because that review is the gate that reads the actual hunks entering the commit. Rulesets owns the implementation in =commits.md=, =agent-lock=, and its Bats coverage; archsetup sent the approved implementation package through the rulesets inbox. -** DONE [#A] Reboot ratio to activate amdgpu.runpm=0 :bug:hyprland:ratio: -CLOSED: [2026-07-28 Tue] DEADLINE: <2026-07-28 Tue> -:PROPERTIES: -:CREATED: [2026-07-28 Tue] -:LAST_REVIEWED: 2026-07-28 -:END: -Craig's plan: close everything down, run topgrade, then reboot. Alarm set for 08:00 (=at= job 56, persistent desktop notify). - -=amdgpu.runpm=0= sits in =/etc/default/grub= and in the generated =/boot/grub/grub.cfg= (5 occurrences, so the reboot will actually apply it) but is absent from =/proc/cmdline=. The box has been up since 2026-07-22 21:10 and the fix landed 2026-07-24, so the running kernel predates it. The GPU is AMD Strix Halo (Radeon 8060S, =1002:1586=), exactly what the parameter targets: runtime power management invalidates the GPU resources hyprlock holds across a display power-cycle, so hyprlock exits without unlocking. - -That is the root cause under the 2026-07-27 lockdead screen. The screen-lock flock fix (dotfiles =ec18fd7=) stops one dead client from becoming a lockdead screen, but it treats the symptom -- this reboot treats the cause. - -Not :solo: — Craig closes his own session and runs topgrade first. - -Rebooted 2026-07-28 08:59. =amdgpu.runpm=0= confirmed present in =/proc/cmdline= afterward, so the parameter is finally live. - -Correction, 2026-07-29: the claim above and in the body that this is "the root cause under the 2026-07-27 lockdead screen" is wrong, and superseded. hyprlock was never crashing. Every logged exit is =rc=143=, SIGTERM, from =settings-watch= killing it by design. See =[#B] Night watch and the lock watchdog fight each other=. The reboot was still worth doing (the parameter is a genuine mitigation for a real AMD defect) but it did not fix this, and the lockdead screens continued after it. -** DONE [#B] Caffeine state is unreadable on both surfaces :bug:dotfiles:design:solo: -CLOSED: [2026-07-28 Tue] -:PROPERTIES: -:CREATED: [2026-07-28 Tue] -:LAST_REVIEWED: 2026-07-28 -:END: -Neither surface that reports caffeine tells the truth reliably, so there is no way to know at a glance whether the screen will lock. Found while investigating the 2026-07-27 lockout, where Craig believed caffeine was on and the screen locked anyway. - -Defect 1 — the settings panel shows a frozen value. =gui.py= calls =_refresh_async()= once during window construction (line 454) and again only after the user's own actions (=_after_matrix=, line 663). The only two =GLib.timeout_add= calls are one-shots (the 2400ms toast hide and a 350ms fire), so nothing re-reads state on a timer. An open panel therefore displays the caffeine value from the moment it opened, forever. Any external flip -- the waybar click, Super+I, the =caffeine-toggle= script -- leaves it stale with no self-correction. The panel is the only surface in the repo carrying a caffeine control (=panel.py:25=); maint has none and does not embed these toggles, so this is the display Craig read. - -Defect 2 — there is no caffeine indicator on the bar at all. =custom/caffeine= appears in neither the stowed =hyprland/.config/waybar/config= nor the live generated =/run/user/1000/waybar/config=, and no =custom/caffeine= block is defined anywhere in the waybar config dir. The =waybar-caffeine= script exists, works, and has its own passing test suite, but nothing displays it. So the bar has never been a source of caffeine state, and the keybind and script have been signalling (=pkill -RTMIN+8 waybar=) a module that isn't there. - -(An earlier read of this task said the bar showed two near-identical glyphs. That was wrong: the module is absent, not merely unstyled. The script's class names are still backwards -- =active= when caffeine is OFF, =inhibited= when ON -- and neither class is styled, but both points are moot until the module is actually in the bar.) - -Grading: Major severity (the panel reports state wrongly while it is open, and the only other surface does not exist, so there is no reliable source for a setting Craig actively manages) x most-of-the-time (any external toggle while the panel is open; the bar never shows it) = P2 = [#B]. - -Fix all three. Wire =custom/caffeine= into the bar, rename its classes so they describe caffeine rather than idle, and style them from the existing palette. Give the panel's toggle row a re-read on a timer or on focus-in. Solo -- buildable and testable, and the direction is settled by the defects rather than a taste call, though the bar color is worth a glance from Craig once it renders. - -All three shipped as dotfiles =033076c=, pushed. =custom/caffeine= now sits in the bar between DND and settings on =interval: 2=; classes renamed =on=/=off= and both styled, caffeine-ON in the theme's gold =#dab53d=; the panel re-reads live state every 3s while visible. Verified live in the stowed config and the generated =/run/user/1000/waybar/config=. The full suite caught a theme-copy regression (=themes/dupre/waybar.css= out of sync with =waybar/style.css=) that the focused suites missed. -** DONE [#B] hyprlock still exits mid-lock; the watchdog relaunch is silent :bug:hyprland:dotfiles: -CLOSED: [2026-07-29 Wed] -:PROPERTIES: -:CREATED: [2026-07-28 Tue] -:LAST_REVIEWED: 2026-07-28 -:END: -Craig, 2026-07-28 ~15:00: saw the Hyprland lockdead/error text blurred *behind* a working lock screen; it vanished when he authenticated. - -That ordering is the diagnosis. hyprlock's blur samples what the compositor is currently rendering, so the compositor was already showing lockdead when the new hyprlock attached. Sequence: hyprlock exits non-zero (no coredump, so it exits rather than crashing), Hyprland renders lockdead because the client is gone while the session stays locked, =screen-lock='s watchdog relaunches within =LOCK_RELAUNCH_DELAY= (0.5s), and the new client draws over the lockdead frame and blurs it. - -*The recovery worked.* On 2026-07-27 this same hyprlock exit produced two contending clients and a session recoverable only from another console. It now self-heals in half a second, and the residue is cosmetic. Both the flock guard (dotfiles =ec18fd7=) and the watchdog did their jobs — verified in this session's compositor log, where all four lock events created exactly one =sessionLock= and one =sessionLockSurface= each, against two of each on 2026-07-27. - -Two things remain. - -*Why hyprlock exits.* The wrapper's header blames GPU-resource invalidation across a display power-cycle (hyprlock#953), which =amdgpu.runpm=0= targets — and that parameter is live as of the 2026-07-28 08:59 reboot, confirmed in =/proc/cmdline=. There is also no DPMS idle rule any more (=e900903=), so idling never power-cycles the display. Yet hyprlock still exited. Strongest untested candidate: a screen recording (=wf-recorder= into =~/sync/recordings/2026-07-28-12-53-57.mkv=, running 12:53 until Craig killed it) held screencopy sessions on DP-4 across the lock. The compositor log carries 2454 screenshare sessions and a =CScreencopyProtocol= bind in the window between the last two locks. A screencopy client churning dmabufs alongside hyprlock's own is a plausible way to invalidate them, and it was the one large new variable that day. - -*The relaunch is silent.* The watchdog loop re-runs hyprlock and logs nothing, so there is no record of how often this fires, when, or with what exit code — which is exactly why the frequency couldn't be established from the logs. Log the exit code and a timestamp on each relaunch. - -Grading: Major severity (the lock client dies mid-lock, and the pre-fix version of this wedged a session unrecoverably) x most users frequently (twice in three days, and this is a single-user machine, so every occurrence lands on the only user) = P2 = [#B]. Downgraded from the 2026-07-27 [#A] because the wedge is fixed and the failure now self-heals. - -An earlier draft of this grading said "some users sometimes", which the matrix maps to P3 = [#C], not the [#B] written beside it. The frequency row was the wrong input rather than the letter: on a one-user machine a fault hitting twice in three days is frequent, not occasional. Corrected the input per the rule that a disputed grade is fixed at its inputs. - -Solo for the instrumentation half only: adding the relaunch logging is buildable, testable against the existing =tests/screen-lock= suite, and needs no decision. Diagnosing the exit is not solo — it needs a reproduction, and the likely trigger is Craig recording his screen. - -Next step when picked up: land the relaunch logging first so the next occurrence produces evidence, then try to reproduce by locking with =wf-recorder= running. - -Superseded 2026-07-29 by =[#A] Night watch and the lock watchdog fight each other=. The logging landed (dotfiles =5bbe2c3=) and answered it within hours: three =rc=143= entries, SIGTERM, from =settings-watch= killing hyprlock by design. Nothing was crashing, so both the AMD-iGPU and the screen-recorder hypotheses in this task are wrong. Kept closed rather than deleted because the reasoning that led here is worth the record. -** DONE [#A] Idle commits silently drop the screen-lock wrapper :bug:hyprland:dotfiles:security: -CLOSED: [2026-08-04 Tue] DEADLINE: <2026-07-29 Wed> +* Archsetup Resolved +** DONE [#A] Reseat velox input-cover ribbon — phantom power button :bug:velox:hardware: +CLOSED: [2026-08-26 Wed] DEADLINE: <2026-08-26 Wed> :PROPERTIES: -:CREATED: [2026-07-29 Wed] -:LAST_REVIEWED: 2026-07-29 +:CREATED: [2026-08-13 Thu] +:LAST_REVIEWED: 2026-08-13 :END: -Caught live 2026-07-29 05:30, seconds after it happened, while verifying that Craig's watch-stage change had landed. - -=idle.py= renders the *whole* hypridle.conf, including a hardcoded =GENERAL= block. That block said =lock_cmd = pidof hyprlock || hyprlock=. The live config said =|| screen-lock=. So every idle-stage commit through the panel rewrote =lock_cmd= and dropped the wrapper out of the chain. +Machine off, lift the input cover (Framework QR-guided procedure, 5 +fasteners), reseat its ribbon connector to the mainboard — disturbed in the +2026-08-13 board swap. Root cause of every "mystery reboot" that day: +chassis flex (flash-drive touch, ethernet bump, lid partially lowered) +fired phantom power-button presses — journalctl -b -1 showed "Power key +pressed short." → orderly logind poweroff, then the glitching button +powered it back on. While in there, reseat the USB expansion cards too — +the flaky slot (two hard resets, one no-enumeration) is likely the same +flex problem. +THIRD SYMPTOM (2026-08-13 evening): touchpad delivers ZERO input events — +15s synchronized libinput debug-events capture while swiping caught +nothing, though i2c enumeration and a driver rebind handshake are clean. +Signature of a dead interrupt line on the same ribbon. Keyboard + power +LED lines work; BT mouse is the interim pointer. +ESCALATED 2026-08-13 21:00: a fourth event killed the machine THROUGH the +shield. Previous boot's journal ends mid-line (tailscaled chatter) with no +shutdown sequence at all — a hard power cut, not logind acting. So the +glitch now reaches the EC/hardware power path, which no software setting +can intercept. The reseat is the only fix, and this is a +lose-work-without-warning failure mode, not an inconvenience. +Interim shield (already live): /etc/systemd/logind.conf.d/powerkey.conf +sets HandlePowerKey=ignore — phantom presses log but do nothing; EC-level +10s hold still force-cuts. Consider keeping it even after the repair. +Verify after reseat: flex the chassis edges + partially lower the lid, then +grep the journal for new "Power key pressed" lines — zero means fixed. +Must be done before the Sunday flight — a phantom press mid-travel with the +shield on is survivable, but the connector should not be trusted at 30,000 +feet on the loose setting. -The wrapper is not incidental. It carries the flock duplicate guard (the fix for the 2026-07-27 unrecoverable wedge), the crash-relaunch watchdog, and the relaunch log. Parking one stage removed all three in a single write, and nothing said so. +*** 2026-08-19 Wed @ 14:40:00 -0700 Retracted: the RTC reset is not this task's, and I should not have filed it here +I attributed the 2026-08-19 network outage to this ribbon earlier today. Craig +pushed back — he reseated it before the trip to get the touchpad working — and +he is right. The evidence does not support the attribution and some of it points +the other way. -=tests/settings/test_settings.py:562= asserted the bare =|| hyprlock= form, so the suite *enforced* the regression. That is why 3845 tests stayed green through a day of work on exactly this subsystem. A test can pin the bug as readily as the fix. +What actually holds. Boot -3 ended at 01:33:18 with no shutdown sequence: no +power-off target, no unmounting. The next boot's kernel line reads =rtc_cmos +00:01: setting system clock to 2025-01-01T00:00:16 UTC=, a firmware default, so +the RTC was reset rather than drifted. No firmware update was applied +(=fwupdmgr get-history= is empty) and the battery is fine. -The false-negative this sets up is worth naming: with the wrapper gone the relaunch log stops receiving entries, and an empty log reads as "the problem is fixed" when it means "the instrument was removed". The =screen-lock= header already warns that an empty file is not proof; this is the mechanism that would have produced one. +What refutes the ribbon. This boot logged *zero* =Power key pressed= events, and +so did the four boots before it. The phantom-press symptom had genuinely stopped +after 08-15, exactly as the 08-16 session recorded. The earlier events logged a +power-key press and an orderly poweroff; this logged neither, which makes it a +different signature, not a worse version of the same one. -Fixed by dotfiles =ab059fb= (2026-07-29 05:59). Verified 2026-08-04 against the tree rather than the commit message: =idle.py:47= renders =lock_cmd = pidof hyprlock || screen-lock || hyprlock=, the live =hypridle.conf= matches, and =test_settings.py= now asserts the wrapper is in the chain plus a second test for the bare-hyprlock fallback. The test that used to pin the bug now pins the fix. +What I got wrong methodologically: I anchored on the most salient open hardware +task and read association as evidence. I even wrote "I can't prove it is the +same connector" and then filed it here anyway, which is the tell. -The evidence that matters is the one this task named: =~/.local/var/log/screen-lock.log= is *receiving entries*, so the instrument is present. An empty log was the false negative to fear, and it did not happen. +Two things I checked and can rule out. There were no OOM kills — the 3,433 +matching lines are a systemd unit named "Periodically re-score Claude Code +processes for the OOM-killer" firing on a timer, not memory pressure, and there +is not a single "Killed process" line. Thermal is clean; the only mentions are +boot-time zone registration at 34C and 45C. -FIXED here, TDD, in the working tree pending commit: -- =idle.py= =GENERAL= now names =screen-lock=, with a comment saying why the line is load-bearing. -- The test now pins the wrapper form. Red first against the old template. -- Live config rewritten through the panel's own path and hypridle restarted; =lock_cmd= confirmed back to =screen-lock=, one hypridle running. +One real thing the same window did surface, tracked separately: a python3 crash +loop, 251 core dumps in the final ten minutes, SIGABRT with =XFreeThreads= and +=PyEval_RestoreThread= in the trace. It does not explain the RTC, because +software cannot clear it, but it is its own problem. -Grading: Critical severity (=write_conf= truncates, so any hypridle key the renderer does not model is silently deleted rather than preserved — that is configuration data loss, and the =lock_cmd= case proved it happens in the field) x some users sometimes (only when an idle stage is committed, which is rare) = P2 = [#B]. +The open question that would settle the RTC is for Craig, not the journal: a +long power-button hold on a Framework triggers an EC-level reset that clears the +RTC, which fits a wedged machine being forced off. A 4-second hold would not. -An earlier draft graded this [#A] on a "security carve-out". That was wrong: disarming the guard is an availability problem, not a leak, and the carve-out is for privacy, security, compliance and safety. The severity band is what carries the weight here, and silent deletion of configuration is the =Critical= band's data-loss case. +*** 2026-08-17 Mon @ 19:57:42 -0700 Not done, and the two symptoms now disagree +The reseat did not happen before the flight, and velox is travelling. The +deadline blew past on 08-14. -Two further fixes came out of an independent review of the first one: +The two symptoms have separated, which is worth recording because it changes +what the evidence proves. The phantom presses have stopped: fifteen "Power key +pressed" entries between 08-14 04:29 and 08-15 20:04, then nothing at all +across five boots including today's. The touchpad has not — there is still no +touchpad node under =/dev/input/by-path/=, which is the same dead interrupt +line the body describes. -- *Fail-open restored.* =pidof hyprlock || screen-lock= made the wrapper the end of the chain, and =screen-lock= is a stow symlink in =~/.local/bin=, not a system binary. An unstowed tree, or a hypridle started without =~/.local/bin= on PATH, resolves it to 127 — so the screen would never lock *at all*. That is worse than the duplicate client the wrapper prevents. The chain now ends =|| hyprlock=, matching the wrapper's own fail-open discipline. -- *The file now says it is generated.* Three comment lines at the top of the rendered output name the renderer and warn that edits are overwritten. The absence of that header is how the divergence survived unnoticed. +So the quiet power button is not evidence the connector reseated itself. The +interrupt line is the symptom that cannot be masked in software, and it is +still dead, so the ribbon is still unseated. The most likely reason the +presses stopped is that the machine has been sitting on hotel surfaces instead +of being carried and flexed. -Still open, and why this stays a task rather than closing with the fixes: the header warns, but nothing *prevents* the next divergence, and the exposure is wider than =lock_cmd= alone. The review enumerated it: +The interim shield is still live (=HandlePowerKey=ignore=), and the escalation +note stands: an EC-level glitch cuts power below systemd regardless of it. +*** 2026-08-15 Sat @ 23:05:00 -0500 The reseat did happen, and the touchpad came back — this contradicts the 08-17 read +Recording this because a parallel session concluded on 08-17 that the reseat had +not happened and the touchpad was still dead. Both halves were done and verified +that night, so the two accounts disagree and the disagreement should be visible +rather than silently resolved by whichever session committed last. + +What was done: the input-cover ribbon was reseated first, which fixed the +phantom power button — the 22:09 boot logged zero =Power key pressed= lines +after Craig flexed the chassis, against nine on the boot before. The touchpad +did not change, because the input-cover ribbon is not its connector. The 4-pin +connector beside the printed =TOUCHPAD= label is silkscreened =PIN 1-2 GND / +PIN 3-4 VCC= — pure power, so it cannot carry i2c or an interrupt. Reseating the +ribbon that actually crosses to the mainboard fixed it. + +Measured, not assumed: the touchpad interrupt (=amd_gpio= pin 8) went from 0 +counts across all 24 CPUs to 1795, and =i2c_hid_acpi ... did not ack reset +within 1000 ms= disappeared from the boot log. Craig confirmed the pointer moved. + +*Why the 08-17 probe likely misread it:* it checked for a node under +=/dev/input/by-path/=. i2c-HID touchpads frequently get no =by-path= symlink +even when fully working, so its absence is not evidence of a dead interrupt +line. The falsifiable check is the interrupt count in =/proc/interrupts= while +the pad is being touched, or the reset message in =dmesg=. + +*Left open rather than closed* — velox was refusing ssh at merge time on 08-20, +so the current state could not be re-verified, and a later regression cannot be +ruled out. One second of Craig's time settles it: move the pointer. If it works, +close this; if it does not, the interrupt line went back down and that is new +information. + +*** 2026-08-26 Wed @ 22:30:46 -0600 Closed: the reseat was done on 08-15 and the task was never marked +I reseated the ribbon on 2026-08-15 and never closed this. The 08-15 entry +above already records the verification: zero =Power key pressed= lines on the +22:09 boot after flexing the chassis, the touchpad interrupt count back up +once the right connector was reseated. This boot shows zero presses as well. +The interim shield (=HandlePowerKey=ignore= in +=/etc/systemd/logind.conf.d/powerkey.conf=) is still live; I'm leaving it in +place, since a phantom press with it on costs nothing and without it costs +the session. +** DONE [#B] Velox touchpad interrupt line is dead — needs a part or a BIOS fix :bug:velox:hardware: +CLOSED: [2026-08-15 Sat] +:PROPERTIES: +:CREATED: [2026-08-15 Sat] +:LAST_REVIEWED: 2026-08-15 +:END: +*Fixed 2026-08-15 23:05 by reseating the correct connector* — a seating fault +all along, no part needed. Verified at the kernel level on the 23:05 boot: the +=did not ack reset within 1000 ms= message is gone (clean handshake), and the +interrupt count went 0 → 1795. Power-key events also zero, so both faults from +the mainboard swap are closed. + +What made this take three attempts is worth keeping: two of the connectors on +that board were decoys. The input-cover ribbon looked like the obvious suspect +and fixing it *did* resolve the power button, which made it look like the whole +answer. Then the 4-pin connector next to the printed =TOUCHPAD= label looked +like the touchpad's own — and its cable is silkscreened =PIN 1-2 - GND / +PIN 3-4 - VCC=, four contacts of pure power, incapable of carrying i2c or an +interrupt. Reading that silkscreen off the photo is what ruled it out and sent +the search to the ribbon that actually crosses to the mainboard. + +The ordered touchpad becomes a spare, which is what Craig wanted from it anyway. +The diagnostic path below is left intact — it is the reusable part: =dmesg= +for the i2c-HID reset message and the interrupt count in =/proc/interrupts= +together separate "device absent" from "device present but its interrupt line is +open", and a live USB separates hardware from software in two minutes. +Split from the ribbon-reseat task 2026-08-15 once the reseat fixed the power +button and left this untouched — they are two faults, not one. + +*Diagnosed to the interrupt line specifically, with software eliminated.* +- The i2c *data* path works. =i2c_hid_acpi= read the HID descriptor, returned + the right product ID (=093A:0274=), =hid-multitouch= bound, and input6/7/8/9 + were created. A descriptor read is a real bus transaction, so the device is + electrically present and answering. +- The *interrupt* path never fires. IRQ 81, =amd_gpio= hwirq 8, level-triggered, + =actions=PIXA3854:00= — the handler is correctly registered on the pin the + firmware names. Count is 0 across all 24 CPUs, including during active + swiping. +- =dmesg=: =i2c_hid_acpi i2c-PIXA3854:00: device did not ack reset within 1000 ms=. + The i2c-HID reset handshake is acknowledged *by the device asserting the + interrupt*, so the first operation needing that line already failed at boot, + before anything touched the pad. That is why the fault reproduces on any boot + in ten seconds. +- *Software ruled out by live USB.* Same "did not ack reset" message and no + pointer movement under Ubuntu's kernel (2026-08-15). Not a driver, not + libinput, not Hyprland, not this install. + +Three candidates remain, all needing a part or firmware: +1. Open conductor on the touchpad's own cable or a bad contact at either end. + Framework sells "Touchpad Cable" as a discrete spare, so it is separately + replaceable — and the input-cover ribbon reseat would not have touched it. +2. The touchpad module's interrupt output is dead while its i2c slave still + answers. Indistinguishable from 1 without swapping parts. +3. Firmware naming the wrong GPIO. The DSDT says =amd_gpio= pin 8; if this + board revision routes the interrupt elsewhere, the kernel watches a pin that + never toggles. Plausible because the mainboard is days old to this machine + and its firmware already needed the PSR workaround. BIOS is 03.05 + (2025-10-30); kernel 6.18.44-1-lts. + +*The connector that was reseated is NOT the touchpad's — confirmed from the +board photo.* Craig reseated the 4-pin connector near the printed word +=TOUCHPAD=. Its cable is silkscreened =PIN 1-2 - GND / PIN 3-4 - VCC= — four +contacts, all of them power. No clock, no data, no interrupt; almost certainly +the keyboard backlight feed. An i2c-HID touchpad cannot run through it, so that +reseat could never have fixed this, and *the free retry remains untried*. +Photo: [[file:working/velox-touchpad-interrupt/touchpad-module-underside-2026-08-15.jpg][working/velox-touchpad-interrupt/touchpad-module-underside-2026-08-15.jpg]]. + +Visible on that board: the controller IC marked =PCT3854= (matching the kernel's +=PIXA3854=), a larger =CON3= carrying a blue-backed ribbon with "26" marked +beside it, a white ZIF past the Framework QR label, and a further connector at +the board's end. The one that matters is whichever ribbon physically *leaves the +input cover and reaches the mainboard* — that is the touchpad cable, and its far +end is the press-fit connector at the board. Reseat both ends of that one before +fitting any new part. + +*BIOS 04.02 exists but does not look relevant.* Checked 2026-08-15 with velox +on AC at 90%: fwupd offers 0.0.3.5 → 0.0.4.2. Read the changelog — the only +touchpad line is haptic-touchpad support for the Laptop 13 *Pro* chassis, and +this machine has a conventional PixArt =PIXA3854=. The rest is BIOS Setup +layout, option naming, TPM behavior, iGPU defaults, PMF slider. Nothing about +GPIO routing or interrupt configuration. So candidate 3's cheap test is weaker +than it looked when it was filed sight-unseen; still worth doing (unlisted +fixes happen, and ACPI tables change), just no longer the front-runner. +Deliberately deferred past the flight — a cleared NVRAM is the failure that +started this whole rebuild. Boot-entry recovery reference captured at +[[file:working/velox-reinstall/velox-uefi-boot-entry-reference.org][working/velox-reinstall/velox-uefi-boot-entry-reference.org]]. + +Order of attack on return, cheapest first: reseat the touchpad's *own* press +connector at the mainboard (free, untried) → BIOS 04.02 → fit the replacement +touchpad. Craig's call 2026-08-15: order the parts now anyway, since they are +worth holding as spares regardless of which candidate wins. + +*What to order.* The replacement *Touchpad* ships with the Touchpad Cable +pre-installed, so that single part covers candidates 1 and 2 together — no need +to buy both to cover both. A bare Touchpad Cable is worth adding only as a cheap +spare. The *Input Cover* is a different and more expensive part, and nothing +points at it: the keyboard works, so the input-cover ribbon is carrying signal. +Framework's marketplace renders its catalogue in JavaScript, so prices could not +be read programmatically — search "Touchpad" under Laptop 13 parts. + +*Also worth a Framework support ticket* — the touchpad died coincident with +their mainboard swap, which may put it inside whatever recourse that carries. + +Grading: Major severity (a laptop's built-in pointer is entirely dead — the +counter-argument is that an external mouse is a complete workaround, which +would make it Minor; I took Major because losing the integrated pointer degrades +the machine's portability, which is the whole point of the laptop) x every user, +every time = P1 = [#A]. Filed [#B] rather than [#A] only because an [#A] must +carry a date and Craig's return date isn't known yet — date it and raise it to +[#A] when it is. + +Workaround in the meantime: Bluetooth mouse, already in use. -- =before_sleep_cmd= and =after_sleep_cmd= sit in the same hardcoded block, at identical risk. -- Every stage command is hardcoded in =_stage_commands= (brightness level, lock, watch, dpms, suspend), same one-way overwrite. -- =write_conf= *truncates* rather than merges, so any hypridle key the renderer does not know about (=ignore_dbus_inhibit=, =ignore_systemd_inhibit=, =inhibit_sleep=, =on-lock=, =on-unlock=) is deleted rather than preserved. That is the largest hole: a key nobody has added yet would vanish the first time a stage is parked. -Options: have the renderer preserve the existing general block and unknown keys instead of emitting its own, or accept the template as the single source and move every hypridle setting into the panel. A design call for Craig, and the truncation half is the part that will bite next. ** DONE [#C] hypridle.conf is generated per-machine but tracked :refactor:dotfiles: CLOSED: [2026-08-14 Fri] :PROPERTIES: @@ -4068,159 +3615,6 @@ ratio and absent on velox; full evidence and rationale in Scope: installer step + rule file + tests per existing shapes, and apply both live to velox over tailscale (daily-driver sync — neither exists there today). -** DONE [#A] Comet KVM setup for truenas :feature:infra:truenas: -CLOSED: [2026-08-08 Sat] -:PROPERTIES: -:CREATED: [2026-07-27 Mon] -:LAST_REVIEWED: 2026-07-27 -:END: -Resolved: Craig wired up and configured the Comet himself, confirmed working -2026-08-08. The ATX power-board follow-up (hard power-cycle for a truly wedged -box) remains unfiled — raise it if the next outage shows the KVM alone isn't -enough. -Wire up the GL.iNet Comet (GL-RM1) IP KVM against truenas. It was bought 2026-01-14 for exactly this job and its KB node still reads "Arrived, not yet set up." - -Why now: truenas went dark 2026-07-24 and stayed unreachable. Diagnosis from ratio on 2026-07-27 — no tailnet contact for 3 days, 100% packet loss on 192.168.86.5, ARP entry FAILED (nothing answers ARP for the address, so the NIC is down at layer 2), every service port closed, while the gateway and a dozen other LAN hosts stayed reachable. Wake-on-LAN to 70:85:c2:db:9d:94 drew no response. With no console and no out-of-band power control there was no remote remedy at all, so recovery needed hands on the box. The Comet closes exactly that gap: BIOS/UEFI console, Wake-on-LAN, and browser access over its native Tailscale integration. - -Not :solo: — the physical cabling is Craig's, and the Tailscale enrollment needs his account. - -Steps, from the KB node ([[id:67bc5994-a763-48e2-926f-4ac0d1bad3db][GL.iNet Comet (GL-RM1) - KVM]]): -1. HDMI from truenas video out to the Comet's HD IN. -2. USB-A-to-USB-C from the Comet to a truenas USB port (keyboard/mouse emulation). -3. Ethernet to the network. -4. Power via USB-C (5V/2A). -5. Reach the web interface and enroll it in Tailscale, so it's usable when the LAN side of truenas is the thing that's broken. - -Then verify while truenas is healthy, rather than discovering the gaps during the next outage: confirm the console shows POST and the BIOS, that keyboard input reaches the box, and that Wake-on-LAN from the Comet actually powers it on. Enable WOL in the truenas BIOS if that last check fails — this outage never established whether it was on. - -Worth considering as a follow-up: the ATX power board accessory gives hard power-cycle control for a truly wedged box, which the KVM alone can't do. -** DONE [#A] Review post-archsetup laptop setup steps (velox 2026-04-10) -CLOSED: [2026-08-08 Sat] -:PROPERTIES: -:LAST_REVIEWED: 2026-08-08 -:END: -Closed at the 2026-08-08 session: every open item got its automate-vs-document -call and the work landed the same night (tests green, committed). Residual: -velox itself still needs the new tlp.d radio line and a dotfiles pull — folded -into the [#A] sleep/suspend task, which works the same files on velox anyway. -Items discovered during velox setup that needed manual intervention after archsetup. -Decide which should be automated in archsetup vs documented as post-install steps. - -*** 2026-08-08 Sat @ 04:43:42 -0500 Automated radio enable via TLP (rfkill boot soft-block) -Root cause sharpened during triage: archsetup masks systemd-rfkill on laptops -(it fights TLP), so nothing restored radio state at boot — the "unblock once -should stick" premise was wrong under the mask. Fix in the TLP custom conf: -=DEVICES_TO_ENABLE_ON_STARTUP="bluetooth wifi"=, the TLP-native mechanism. -configure_tlp_power parametrized for tests; covered by -tests/installer-steps/test_configure_tlp_power.py. - -*** 2026-07-04 Sat @ 11:48:24 -0500 Automated /efi restrictive mount permissions in fstab generation -archsetup:2827-2836 now rewrites the /efi fstab line to =fmask=0177,dmask=0077= (idempotent), so fresh installs no longer land the world-accessible =fmask=0022,dmask=0022= default. Confirmed via the 2026-07-04 task audit. (Original velox note: default vfat mount had =fmask=0022,dmask=0022=, hand-fixed to restrictive; bootctl warned about a world-accessible random-seed file.) - -*** 2026-08-08 Sat @ 04:43:42 -0500 Automated tmp.mount mask for ZFS /tmp -New mask_tmp_mount_for_zfs, called from configure_snapshots' ZFS branch: -masks tmp.mount only when the pool actually carries a dataset mounted at -/tmp (exact match), silent no-op without zfs or without the dataset. Covered -by tests/installer-steps/test_mask_tmp_mount_for_zfs.py; the orchestrator -dispatch pin updated. - -*** 2026-08-08 Sat @ 04:43:42 -0500 Automated CPU microcode install by vendor -New install_cpu_microcode, first in boot_ux so grub-mkconfig and mkinitcpio's -microcode hook both see the installed /boot/<vendor>-ucode.img: vendor_id from -/proc/cpuinfo → intel-ucode / amd-ucode, error_warn on unknown vendor. -Covered by tests/installer-steps/test_install_cpu_microcode.py; boot_ux -sequence pin updated. - -*** 2026-07-04 Sat @ 11:48:24 -0500 Automated syncthing user-service enable in archsetup -archsetup:2263-2271 now installs syncthing and enables the user service (via symlink), so fresh installs no longer leave it installed-but-disabled. Confirmed via the 2026-07-04 task audit. (Original velox note: package installed but service not enabled; hand-fixed with =systemctl enable --now syncthing@cjennings=.) - -*** 2026-08-08 Sat @ 04:43:42 -0500 Closed the awww-daemon crash watch — no recurrence -The April boot crash never recurred across four months of daily use on both -machines (and the wallpaper stack has since been reworked). Reopen as its own -bug with fresh evidence if it ever comes back. - -*** 2026-08-08 Sat @ 04:43:42 -0500 Automated touchpad device detection in the pointer scripts -The scripts were already in stowed dotfiles with binds — the open half was the -hardcoded Framework device name. Both touchpad-auto and toggle-touchpad now -auto-detect the touchpad (first pointer named *touchpad*, pixa fallback) and -derive the internal-pointer exclusion set from the detected name, so they -agree on any machine. Test seams added (--detect / --has-external-mouse); -tests/touchpad-auto/ new, toggle-touchpad suite still green. Dotfiles commit; -velox picks it up on its next pull. - -*** 2026-08-08 Sat @ 04:43:42 -0500 Documented bluetooth pairing in the post-install checklist -Inherently interactive, so it can't ride the installer. Documented in the new -[[file:docs/post-install-checklist.org][docs/post-install-checklist.org]] along -with the Proton Bridge steps — the standing home for manual post-install work. -Consider: document as post-install step. No automation possible. - -*** 2026-05-26 Tue @ 13:32:31 -0500 pocketbook install concern moot — pulled from publication, folded in-tree -Resolved by removing pocketbook from archsetup's provisioning entirely. It's nowhere near ready, so the github mirror + cjennings.net repo were deleted and the project was folded into the archsetup tree at =pocketbook/=. Dropped the =gtk4-layer-shell= dep + =pip_install= from =archsetup= and the clone from =scripts/post-install.sh=. No fresh install pulls pocketbook now, so "not installed on velox" no longer applies. Re-wiring the install is tracked in the new pocketbook development backlog. - -*** TODO Review: Tailscale needs login after install -~tailscaled~ service was enabled but needed ~tailscale up~ for interactive auth. -Old machine entry needed cleanup in admin console. -Consider: document as post-install step. - -*** TODO Review: docs/ directories need manual sync from existing machine -docs/ dirs (gitignored) for ~/code and ~/projects repos needed scp/rsync from ratio. -Same for ~/.emacs.d/docs/. Not in git, so not available after clone. -Consider: document as post-install step or create a sync script. -** DONE [#C] Waybar modules run together — need subtle separators :bug:dotfiles:waybar: -CLOSED: [2026-08-08 Sat] -Closed at the 2026-08-08 task review: Craig confirms the separator work landed -a while back and the bar reads correctly now. -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). -** CANCELLED [#C] Add a whole-display dim mode :feature:hyprland: -CLOSED: [2026-08-08 Sat] -Killed at the 2026-08-08 task review: the July auto-dim work covers the actual -need; no separate dim-everything mode wanted. -Extend auto-dim with an explicit “dim everything” setting for bright -non-dark-mode contexts, with a security/usability review of its scope. -** DONE [#C] Fix install errors surfaced by the 2026-05-11 VM test run -CLOSED: [2026-08-08 Sat] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-06 -:END: -Closed at the 2026-08-08 task review: every archsetup-attributable error was -fixed and verified (fontconfig, dconf x2, emacs-stow, AUR exit-0 logging at -the root); the residual four reproduce unchanged and are diagnosed -environment/non-critical, with two 2026-06-28 full runs attributing zero -issues to archsetup. Residual thread: confirm the firewall nf_tables pair on -bare metal at the next real install — no container task needed to carry it. -*** 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. -** CANCELLED [#C] Review current tool pain points annually -CLOSED: [2026-08-08 Sat] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-06 -:END: -Killed at the 2026-08-08 task review: an undated annual intention that never -fired — pain points get surfaced organically as they bite. -Once-yearly systematic inventory of known deficiencies and friction points in current toolset ** CANCELLED [#B] agent-text relay reports success for a message that went nowhere :bug: CLOSED: [2026-08-19 Wed] :PROPERTIES: @@ -4422,3 +3816,453 @@ real hang in the script" and it was neither. Fixed by pinning =stdin=subprocess.DEVNULL= in =run_script=. Verified both ways: the previously-failing open-pipe case and the redirected case both pass in 0.08s, and a full =make test-unit= under a live pipe is clean across 50 suites. +** DONE [#A] powerprofilesctl crashes on a loop since ppd was masked :bug:velox:dotfiles: +CLOSED: [2026-08-21 Fri] +:PROPERTIES: +:CREATED: [2026-08-17 Mon] +:LAST_REVIEWED: 2026-08-17 +:END: +Something polls power state every 10-30 seconds, and each poll runs +=powerprofilesctl get=, which SIGABRTs. 47 coredumps on velox on 2026-08-17 +alone, the earliest at 08:34, four in one minute while I was watching. + +Cause is the 2026-08-16 fix that masked =power-profiles-daemon= so TLP +survives on laptops. That fix is right and stays. What it did not account for +is the settings module's power backing +(=~/.dotfiles/settings/src/settings/power.py=), which shells out to +=powerprofilesctl=. Against a masked unit the D-Bus activation fails with +=NameHasNoOwner ... unit is masked=, and the caller aborts rather than +degrading. + +Run by hand the same command exits 0 and prints the error, so the abort is +context-dependent and the caller needs finding before the fix is written. +Ratio does not mask ppd, which is why this is velox-only and why it appeared +the day after the masking. + +Costs: journal spam, coredump disk churn, and repeated failed D-Bus +activations on a travelling laptop's battery. It is also the leading suspect +for the wedged user manager filed below. + +Fix shape: =power.py= should treat a masked or unavailable ppd as a +first-class "no profile control here" state rather than an error path, and +the poller should stop retrying a unit it has been told is masked. The +machine-level half is already correct. + +Grading: Major severity (a crash loop burning battery and filling the +journal, silently) x every user every time on any laptop with the TLP fix +applied = P1 = [#A]. + +*** 2026-08-17 Mon @ 19:57:42 -0700 The loop stopped at the reboot; the defect did not +velox rebooted at 16:04 and there have been zero coredumps since, against 47 +in the twelve hours before it. So the loop is not currently burning anything. + +That is not a fix, and the distinction matters for whoever picks this up. +=powerprofilesctl get= still fails exactly as recorded — =NameHasNoOwner ... +unit is masked= — so every precondition for the loop is intact and it returns +whenever the caller next polls. What the reboot cleared is the caller's state, +not the bug. + +Narrowed the search the body asks for: =power.py= is the *only* file in +dotfiles that shells out to =powerprofilesctl= (=SETTINGS_POWERPROFILESCTL=, +line 14), so the caller is inside the settings module rather than waybar or a +timer. Worth knowing that the coredumps are =powerprofilesctl= itself aborting +— it is a python script, which is why they log as =/usr/bin/python3.14= +SIGABRT rather than under its own name. + +Grade unchanged. The matrix inputs did not move: the severity is what happens +while the machine is in that state, and the frequency row is every laptop +carrying the TLP fix. A quiet interval since a reboot is not a frequency +change. + +Fixed in dotfiles =e89d9db=. The caller was =waybar.py=, using =panel.read_state()= (the full snapshot of every control) to read one boolean, four bar modules deep on a 2-second interval. Two fixes, each needed alone: =panel.read_control()= reads a single control's backing, and =power.masked()= checks the mask symlink before shelling out. Verified with a logging stub: full snapshot unmasked calls powerprofilesctl once, masked calls it zero, and a waybar poll calls it zero even unmasked. +** DONE [#A] The installer clones my two working repos shallow and read-only :bug:velox: +CLOSED: [2026-08-21 Fri] +:PROPERTIES: +:CREATED: [2026-08-17 Mon] +:LAST_REVIEWED: 2026-08-17 +:END: +=archsetup:1432= clones the user's archsetup repo and =archsetup:1445= clones +dotfiles, both with =--depth 1=. Those are not build directories. They are the +two repos I actively develop in, and on velox they came back from the +2026-08-13 rebuild with 7 commits of history each instead of 851. + +Found 2026-08-17, and found the worst way: I ran the credential-file history +check that the GitHub-release task asks for, and it reported all five files +absent from history with a clean exit. The real answer is that this clone +cannot see the history those files live in. A shallow clone does not error on +=git log -- <path>=, it answers "no commits" — so a security question came back +falsely clean, and nothing about the output said otherwise. + +Everything else it breaks is quieter: =git log=, =blame=, =bisect=, and any +archaeology past the boundary. The tree looks completely normal, which is why +this survived four days on the machine. + +The right shape is already in the codebase. =scripts/post-install.sh:42-51= +takes depth as a per-repo argument and defaults to a full clone, so wallpaper +gets =--depth 1= and org does not. The AUR build clones (=archsetup:855=, +=:1673=, =:1677=) are correctly shallow and stay that way. Only the two +user-repo sites change. + +*Second defect, same two lines, found 2026-08-17 while pushing:* the dotfiles +clone could not push at all. =archsetup:245= defaults =dotfiles_repo= to +=https://git.cjennings.net/dotfiles.git=, the public read-only endpoint, so +=git push= returned 403. Ratio uses =git@cjennings.net:dotfiles.git= and +archsetup's own clone uses the matching ssh form, so velox was the odd one out +purely because it was the machine rebuilt by the installer. Repointed velox's +remote and pushed. + +That half needs a decision rather than a fix, which is why this task is no +longer =:solo:=. The https default is *correct for a stranger* installing +archsetup, who has no ssh key on the server, and this repo is being prepared +for public release. It is wrong for my own machines, which need to push. The +override already exists (=DOTFILES_REPO=, documented in +=archsetup.conf.example=), so the question is only where my personal value +lives: a config the personal ISO bakes in, a post-install step, or a detection +that prefers ssh when a key is present. Craig's call. + +*Decided 2026-08-19: the ISO bakes the value, and a check nets the rest.* +=archsetup:240= has the identical default for =archsetup_repo=, so this was +always two repos rather than one. I ruled out detection — archsetup never +restores =~/.ssh=, so key-presence at clone time depends on ordering it +doesn't control, and "any key means ssh" would break a stranger who has an +unrelated one. I ruled out a bare post-install step for the reason this whole +class of bug exists: manual steps don't get run, which is why this sat four +days. So the personal ISO carries =ARCHSETUP_REPO= / =DOTFILES_REPO= in the +ssh form (noted on the secrets/ISO task), and =post-rebuild-check= check 8 +flags any working repo still on the read-only endpoint — covering curl|bash +and stock-ISO installs, which the ISO value cannot reach. + +Repair on a machine already built: =git fetch --unshallow= in each repo, and +=git remote set-url origin git@cjennings.net:<repo>.git= for dotfiles. + +Grading: Major severity (two working repos silently missing their history on +the machine I develop on, and it returns confidently wrong answers to history +questions rather than failing) x every user every time (every fresh install, +both daily drivers) = P1 = [#A]. + +Not :solo:. The depth half is (two lines plus tests in the existing +=tests/installer-steps/= shape, verifiable by asserting the clone command +carries no =--depth= for these two repos). The remote-URL half needs the +decision above, so the task as a whole waits on it. Split it in two if the +depth fix is wanted sooner. +*** 2026-08-19 Wed @ 23:05:00 -0700 Dropped --depth from both user-repo clones +=archsetup:1462= and =:1475= now clone full history; +=tests/installer-steps/test_clone_user_repos.py= covers it with 8 cases, and +one of them asserts the AUR build clones still carry =--depth 1= so the fix +can't be over-applied by a careless repo-wide sed. Both my repos on velox were +already unshallowed by hand last session, so this is prevention rather than +repair. +*** 2026-08-19 Wed @ 23:05:00 -0700 Settled the remote-URL half and netted it +See the decision recorded above. The ISO half is a note on the secrets/ISO +task; the net is =post-rebuild-check= check 8, which ships now. + +Both halves resolved. Depth: =a028aa5= drops =--depth 1= from both user-repo clones, with 8 tests including one asserting the AUR build clones stay shallow. Remote URL: decided 2026-08-19 (see above) — the personal ISO carries the ssh form, and =post-rebuild-check= check 8 (=87ff0b7=) flags any working repo still on the read-only endpoint, covering the install paths the ISO cannot reach. +** DONE [#D] Worldclock tooltip blanks on one bad timezone row :bug:dotfiles:waybar:quick:solo: +CLOSED: [2026-08-21 Fri] +:PROPERTIES: +:LAST_REVIEWED: 2026-07-25 +:END: +Found by sentry (2026-07-25), verified by exercising. =hyprland/.local/bin/waybar-worldclock= builds each zone with =ZoneInfo(tz)= inside the loop (line ~99) with no guard, so a single malformed timezone row in =worldclock.conf= raises =ZoneInfoNotFoundError= and crashes the whole python pass. The tooltip then renders empty and *every* zone is lost, not just the bad row; the traceback only reaches stderr, where waybar never surfaces it. +Repro: a conf with =America/Chicago|Home=, =Not/AZone|Bad=, =Europe/London|London= renders =tooltip: ""= (Home and London gone too). +Grade: minor severity (one module's tooltip blanks, no data loss) x rare edge case (a malformed conf row) = P4 = [#D]. +Fix: wrap the per-row =ZoneInfo=/=datetime= in a try/except and =continue=, so a typo drops only that row and the valid zones still render. Solo + quick: the script already has an env-override test harness (=WAYBAR_TIME_EPOCH=, =WAYBAR_WORLDCLOCK_CONF=), so a red-first test is cheap. + +Fixed in dotfiles =8f692f5=. The per-row =ZoneInfo= is guarded, so a malformed row drops itself and the valid zones still render. Five cases, including a bad row first — the ordering that looks least like one typo and most like the module being broken. Caught the broad =except Exception= rather than =ZoneInfoNotFoundError=, because the row also parses floats and calls strftime and the contract wanted is "a bad row costs only itself". +** DONE [#C] obsbot-wb-guard polls forever on machines with no OBSBOT :bug:dotfiles:quick:solo: +CLOSED: [2026-08-21 Fri] +:PROPERTIES: +:LAST_REVIEWED: 2026-08-16 +:END: +=obsbot-wb-guard.service= is =WantedBy=graphical-session.target= and lives in the shared =common/= stow tier, so it starts on every machine. Its main path is =while :; do check_once; sleep 2; done=, and =check_once= returns early when the camera node is absent. On a machine with no OBSBOT attached that is a process waking every two seconds forever to do nothing, which on a laptop is battery spend for zero benefit. No restart loop, though: the loop never exits, so =Restart=on-failure= never fires. + +Found 2026-08-16 on velox, after enabling it to match ratio and then having to disable it again by hand. A per-machine disable is the wrong shape, because it drifts velox from ratio permanently and a re-stow or a future audit will just put it back. + +Fix: give the unit =ConditionPathExists= on the camera node (=/dev/v4l/by-id/usb-Remo_Tech_Co.__Ltd._OBSBOT_PW106-video-index0=, the same default the script uses) so systemd skips it on any machine without the camera and starts it normally on ratio. Then re-enable it on velox, where it will simply be skipped. Note the limit: a camera plugged in later will not start it until the next login, which is the right trade against a permanent poll. + +Careful when disabling by hand in the meantime: =systemctl --user disable= on a *linked* unit deletes the unit symlink, and that symlink is stow-managed, so a bare disable silently removes a file from the dotfiles stow tree. Restore the link afterward or re-stow. + +Grade: minor severity (wasted wakeups and battery, no data loss, no failure) x every boot on any machine without the camera = P3 = [#C]. + +Solo: buildable here (archsetup owns dotfiles end-to-end), verifiable by the agent (assert the unit is skipped on velox and still active on ratio), and no design call left open. + +Fixed in dotfiles =566dd14=. =ConditionPathExists= on the camera node, so systemd skips the unit where the camera is absent. velox is now =enabled= like ratio and reports =ConditionResult=no=; the stow symlink is untouched. Found while doing it: ratio has a Logitech BRIO and no OBSBOT on USB at all, so the 2-second poll was pointless on the desktop too, not merely costing laptop battery. A test asserts the unit's condition path and the script's =OBSBOT_WB_DEVICE= default stay equal, since drift there is invisible in both directions. +** DONE [#C] Spine face tests decay against the wall clock :bug:test:dotfiles:solo: +CLOSED: [2026-08-21 Fri] +:PROPERTIES: +:LAST_REVIEWED: 2026-08-02 +:END: +=settings/faces/timeline-face-spine.test.mjs= has thirteen =SP.spineRows(g, h)= calls that omit the third argument, so =ref= falls back to its =new Date()= default while the file's events fixture is pinned to =JUL= (2026-07-31 18:30 UTC). Any assertion that depends on how much room the day needs is then measured against today's clock, and rots as the fixture recedes. + +One of them, "spacing is uniform everywhere except the gap home opens", had already rotted: green on 07-31 because that was the fixture's own date, red by 08-02. Fixed in place on 2026-08-02 by pinning =JUL=; the remaining thirteen pass today by luck. The measurement, for whoever picks this up — with =ref=now= the even step is 85.21 and home's gaps are 129.10 / 65.40 (the lower one collapses below a plain gap); with =ref=JUL= the step is 78.54 and the gaps are 129.10 / 145.46. Only the lower gap moves, because =up= does not depend on events and =down= does. + +Six other calls in the same file already pass =JUL= explicitly, so the convention exists and this is a miss, not a gap in the design. Fix: pass =JUL= at every call whose assertion reads geometry. Leave the call around line 747 alone — it sweeps =new Date(t0)= deliberately. + +Grade: minor severity (dev-facing only; no product behavior is wrong, the face itself is fine) x some users, sometimes (each call rots independently, whenever the fixture drifts far enough) = P3 = [#C]. Not merely cosmetic though: a suite that goes red for no real reason is how a genuine regression gets waved through. + +Solo — mechanical, an existing convention to copy, and verifiable by running the suite plus re-running it under a faked clock to prove the determinism actually holds. + + +Fixed in dotfiles =c96a216=. All thirteen bare calls now pass =JUL=. The task's "line 747" was stale (the deliberate =t0= sweep is at 893 and already passed its own ref, so it was never at risk), and the continuation-form call closes its arguments on the next line, which is why a naive grep counts fourteen. Added a guard that reads the file and fails with the offending line numbers, and verified it bites by stripping =JUL= from one call and confirming it went red naming that line. +** DONE [#A] Velox still carries the install placeholder passwords :bug:security:velox: +CLOSED: [2026-08-23 Sun] SCHEDULED: <2026-08-20 Thu> +:PROPERTIES: +:CREATED: [2026-08-20 Thu] +:LAST_REVIEWED: 2026-08-20 +:END: +Closed 2026-08-23: I'd already rotated all three on the 08-14 bringup day, so +this task was never live. Verified on velox before closing — =chage -l= puts the +last password change for both =cjennings= and =root= at Aug 14 2026, and +=/etc/zfs/zroot.key= was rewritten 2026-08-14 05:29 and no longer holds the +placeholder (checked with a =grep -qx= that returns a yes/no without reading the +key into a transcript). + +The premise below was wrong, and it's worth naming how. Nothing ever tested the +credentials: the claim came from an unticked runbook item plus the archangel +session handing back the values the *installer* had set, which reads as "these +are current" only if you assume nobody changed them in between. An inference +about a security exposure got recorded in the same voice as a measurement. The +one command that settles it costs a second. + +Original body follows. + +The 2026-08-13 reinstall set placeholder credentials and the runbook's Phase 5 +item to replace them (=passwd=, =zfs change-key zroot=) was never ticked. +Believed still live 2026-08-20 via the archangel handoff, which had to hand +them back to Craig to get into the machine: =welcome1= for the pool, =welcome= +for the accounts. + +So velox's full-disk encryption is currently protected by a dictionary word +with a digit, on the machine that travels. Anyone who picks it up owns the pool +and every account on it — the encryption is doing no work at all. + +Two commands, both on velox: +- =passwd= for each account. +- =zfs change-key zroot= for the pool passphrase. Note this is the ZBM unlock + passphrase, so get it right before rebooting. + +Grading: *severity-alone carve-out* — this is a security exposure, so the +frequency row does not discount it (=todo-format.md=). Critical severity: total +compromise of an encrypted-at-rest laptop from a guessable string, with the +device leaving the house. = P1 = [#A]. + +Distinct from the =VERIFY [#A] Rotate the credentials exposed by the 2026-08-09 +dotfiles leak= under the cgit audit — that one covers credentials a crawler +already took from a public repo. This one is a local default never changed. Both +are rotation work; neither substitutes for the other. +** CANCELLED [#B] Consistent keybinding family for the panel console :feature:hyprland: +CLOSED: [2026-08-21 Fri] +:PROPERTIES: +:LAST_REVIEWED: 2026-07-09 +:END: +Merged into =[#B] Reconcile panel keybindings around Super+N=, which now carries +this body's detail: the collision list, the velox plain-keyboard constraint, and +maintenance-M as the priority chord. Cancelled rather than done — the work is +still open, just tracked in one place instead of two. + +Consider putting every panel (net, bluetooth, audio, timer, and the coming maintenance console) on one consistent chord family — a shared modifier set (Super+Shift, Control+Alt, or similar) plus a mnemonic letter per panel (N/B/A/T/M). Today the panels open via waybar clicks only; a uniform chord family makes them keyboard-reachable and predictable. Watch for collisions with existing binds: Super+Shift+A is already PTT toggle, and the hold-to-talk grave bind is load-bearing. Decide the family, audit current hyprland binds for conflicts, wire via the dotfiles hyprland config, and document in the keybind reference. Both machines (velox can't QMK-remap, so chords must work on a plain laptop keyboard). +*** 2026-07-14 Tue @ 00:31:36 -0500 Folded Craig's ask for a maintenance-panel keybinding; bumped [#C] → [#B] +Craig asked (in session, 2026-07-14) for a maintenance keybinding specifically — the panel he's reaching for without one. Maintenance (M) is the priority chord when this task gets worked. The capture graduated the task from parking lot to active backlog. +** DONE [#B] Waybar network module — custom/net :feature:waybar:network: +CLOSED: [2026-08-21 Fri] +:PROPERTIES: +:LAST_REVIEWED: 2026-07-09 +:END: +Closed 2026-08-21: the module shipped and is in daily use. Phases 1-4 all landed +in dotfiles, and the tunnels track absorbed most of what Phase 5 originally +covered. The one piece genuinely left — the =net vpn= CLI subcommand — is now its +own task below, so the residual is tracked at its real size instead of holding a +finished umbrella open. +Unifies the old wifi-no-internet indicator (was =[#C]=) and the network-manager +dropdown (was =[#B]=) into one =custom/net= module: a tested Python =net= engine +(nmcli + diagnostics), a thin bar indicator, and a GTK4 layer-shell panel. Code +lives in the dotfiles repo (hyprland tier + a =net/= package like pocketbook); +archsetup only installs deps. Secrets stay in NetworkManager's own store (no +separate credential store). The =captive= script becomes the diagnostics engine. +Full design, acceptance criteria, and the failure-mode coverage table: +[[file:docs/design/2026-06-29-waybar-network-module-spec.org][2026-06-29-waybar-network-module-spec.org]]. + +Phases below, dependency order. Engine/unit work is agent-verifiable (=unittest= ++ fakes on PATH, coverage via venv); the live-network and visual states need real +conditions, filed under "Manual testing and validation". + +*** 2026-06-29 Mon @ 20:19:11 -0400 Phase 1 shipped — indicator + console recovery +Shipped to the dotfiles repo (10 commits, =5254bd8=..=c095a22=, pushed to main). +The =net= engine is a src-layout Python package in-tree, imported by a bin shim +that resolves the stow symlink back to the repo — so it runs from a bare TTY with +no install, which the recovery path depends on. + +Landed: =net status= (fast path, one nmcli call + sysfs, degraded fallback in +budget) + =net probe= (native captive probe, single-flight flock, atomic cache, +fresh/stale/expired/unknown classes, iface/SSID/UUID invalidation); =waybar-net= +replacing =custom/netspeed=, throughput → tooltip, CSS states in both themes + +live; =net diagnose= (read-only steps) + =net repair= (rfkill/reset/bounce/ +dns-test, cleanup-verified) + =net doctor [--fix]= with the four terminal +classifications; =net portal= + the =captive --probe-json= refactor; redacted +JSONL event log; Makefile recovery targets (=make online= etc.); =~/.config/net/ +config=. Verified live: =make net-status= reads the real wlp170s0 / @Hyatt_WiFi. + +Airplane (Craig's call, option 1): =custom/net= absorbs only the *display* — net +reads the airplane-mode state file and shows an airplane state/glyph. The +airplane-mode toggle stays (it's a low-power mode — radios + CPU + brightness + +services — not a radio switch), now on =custom/net='s right-click + signal 15. +Deleted: =waybar-airplane=, =waybar-netspeed=, =custom/airplane=, their tests + +css. =airplane-mode= kept. + +Tests: 160 in =tests/net/= (fake nmcli/curl/rfkill/resolvectl/ping/getent/ +systemctl on a temp PATH; doctor-classification fixtures; degraded-under-slow- +nmcli benchmark) + the =captive= probe-mode tests; full dotfiles suite green (32 +suites). Coverage-gap pass via throwaway venv: pure modules ≥90% branch +(classify 100%), IO-error branches excused in the test docstring. +Deferred to Phase 2/3: archsetup deps (gtk4-layer-shell/python-gobject Phase 2, +speedtest-go-bin Phase 3 — not added before the code that needs them). +Verify (manual, live): see Manual testing and validation. + +*** 2026-06-29 Mon @ 22:19:25 -0400 Phase 2 shipped — panel shell + connection management +Shipped to dotfiles (commits =4e7740f=..=24bcac5=, pushed). Engine: =net list= (saved +MRU + in-range wifi scan, infrastructure types filtered), =net up/down= (UUID-keyed, +mutation safety — keep prior link until target activates, classify wrong-password vs +generic, report auto-reactivation), =net add/edit/remove/rescan= (open + WPA-PSK; +enterprise activate-only; secret to NM's store, never our JSON/log — tested). + +Panel: a GTK-free PanelModel (selection, four state machines, the UX-flow enable +rules, terminal states) + a GTK4 gtk4-layer-shell window (=net panel=) anchored +top-right under the bar — Connections section with MRU list, active marked, signal +glyph, row-click select, Connect/Add/Forget/Rescan, confirm-on-forget, worker-thread +engine calls via GLib.idle_add. GTK imported lazily so the CLI/tests stay GTK-free. + +Bar interactions (settled with Craig over live iteration): left = =net-panel= toggle, +middle = =net portal=, right = =net-fix= (notify the doctor result when one-way; open +a terminal only when the outcome is fixable — the sudo/interactive case). Airplane on +Super+Shift+A. archsetup adds =gtk4-layer-shell= + =python-gobject= (this commit); +already on velox. + +Tests: 204 in tests/net (merge ordering/dedup, up/down mutation safety, no-secret-leak +on add/edit, panel model + state machines, gui row-format helpers). Full dotfiles suite +green (32 suites). Live-verified on velox: panel opens/toggles, list shows real 24 +profiles, right-click notification delivers (Craig confirmed). Phase 3 (diagnose/repair/ +speedtest IN the panel) is next; the engine for it already exists from Phase 1. + +*** 2026-06-29 Mon @ 22:43:40 -0400 Phase 3 shipped — diagnostics + speed test in the panel +Shipped to dotfiles (=91277cf=..=691abcb=) + archsetup (=48052d6=, speedtest-go-bin), +pushed. Engine: =net speedtest= (parses speedtest-go --json → ping from latency ns, +down/up from per-server byte rates; missing-backend / offline / malformed → error +envelope per the failure table). Panel grew a section switcher with four pages: +- Connections (Phase 2). +- Diagnose: =net diagnose= on a worker thread, each step a row (✓/✗/… glyph + title + + redacted evidence), read-only; Open-portal button when captive. +- Repair: "Get me online" (=net doctor --fix=) + tiers (rfkill/reset/bounce/dns-test) + + force portal. Confirmations in-panel with the spec's exact wording; the privileged + tiers run via =net-popup= terminal (where the sudo prompt + step output, incl. + cleanup-verified, show) — a panel has no tty, and pkexec would mean a prompt per op. +- Speed test: in-process =net speedtest= (no privilege → inline result: ↓/↑ Mbps + ping + + server), Run/Cancel (Cancel pkills the child), error envelope shown. + +213 net tests; pure helpers (step_indicator, format_speedtest) unit-tested. Full +dotfiles suite green (32 suites). One unverified assumption: speedtest-go's dl/ul unit +(taken as bytes/s; =BYTES_PER_SEC= flips it) — needs one real run vs a reference. The +in-panel repair streaming (vs terminal) is a named future polish once the GUI-privilege +story settles. + +The waybar network module ([#B] parent) is now COMPLETE through Phase 3. Phase 4 +(in-app help + user guide) and Phase 5 (VPN/WireGuard) remain as future work; the core +feature (indicator + recovery + panel + diagnostics + speed test) is done. +Verify (manual, live): see Manual testing and validation. + +*** 2026-07-09 Thu @ 16:32:54 -0500 Audit reconcile: Phase 4 is filed on the dotfiles side, waiting on them +The dotfiles project accepted the Phase 4 handoff and filed it as a =[#C]= task in their own =todo.org= (their note, 2026-07-08 16:56): the help-text audit + panel help affordance, the user-guide/README, and the ratio rollout doc. Not started there. They ping when it lands, and this task's Phase 4 child closes then. Nothing to do here meanwhile. + +*** 2026-08-17 Mon @ 19:57:42 -0700 Landed on the dotfiles side; the block is cleared +dotfiles shipped it as =138da7b= and closed its own task, so this one closes +with it and the =:blocked:= tag comes off. Found by checking their =todo.org= +rather than waiting for the ping — their close-out note says "archsetup pinged +so its Phase 4 task can close", so the handoff worked and only this end was +left open. + +All three acceptance criteria are met on their side: the help audit found and +fixed a stale =net repair= action list (nine of nineteen actions were named; +both the CLI help and =repair.py='s docstring now generate from the ACTIONS +registry), =net/README.md= covers every command plus the recovery targets, and +the ratio rollout is documented with both daily drivers verified current. + +They split the panel help affordance out rather than inventing it — no sibling +panel has one, so its shape is a design call. It is tracked on their side, not +here. + +Original deliverable, for the record: in-app help (=net --help= + per-command, +panel help affordance); README/user-guide; archsetup Hyprland dep install +(=gtk4-layer-shell=, =python-gobject=, =speedtest-go-bin=); ratio manual dep + +stow step. Handed off 2026-07-04 with the archsetup deps already confirmed +installed. + +*** 2026-08-21 Fri @ 14:18:03 -0700 Promoted the Phase 5 residual out to its own task +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. +** DONE [#A] Ratio: pull .emacs.d before upgrading Emacs to 31.1 :chore:ratio:emacs: +CLOSED: [2026-08-25 Tue] +:PROPERTIES: +:CREATED: [2026-08-25 Tue] +:LAST_REVIEWED: 2026-08-25 +:END: +Emacs 31.1's warnings.el defers daemon-startup warnings into a closure holding +the =*Warnings*= buffer; the config's dashboard-only sweep killed that buffer, +so the first client frame of every fresh 31.1 daemon failed on Wayland and +emacsclient silently fell back to =$DISPLAY= (XWayland, pgtk warning dialog). +Fixed in =.emacs.d= commit =63831060= (2026-08-25, velox verified live: +=GdkWaylandDisplay=). Ratio is still on 30.2, which lacks the deferring code, +so it is fine until it upgrades — then it hits the same trap once per daemon +start unless the fix is pulled first. + +Order on ratio: =git -C ~/.emacs.d pull= (the push from velox is the telega +session's; confirm =63831060= is on origin first), then the =pacman -Syu= that +brings =emacs-wayland 31.1=, then restart the daemon. Check afterwards: +=emacsclient -e '(pgtk-backend-display-class)'= → =GdkWaylandDisplay=. + +*** 2026-08-25 18:10 — pull already landed; the upgrade half remains +Checked ratio over tailscale: =~/.emacs.d= is clean at =91fbac72= (= =origin/main=), +and =63831060= is an ancestor of HEAD — =modules/undead-buffers.el= carries the +=*Warnings*= entry. Ratio is on =emacs-wayland 30.2-3= with =31.1-1= pending among +720 updates (last full upgrade 2026-08-01; kernel 7.1.5 → 7.1.9 also pending, +btrfs root, uptime 3.5 weeks). The daemon is a plain =emacs --daemon= (not a +user unit) holding 2 live frames, so the restart step will drop those frames. +What remains: the =pacman -Syu= on ratio, the daemon restart, and the +=(pgtk-backend-display-class)= check. + +*** 2026-08-25 Tue @ 18:35:00 -0600 Upgraded ratio to Emacs 31.1 and verified the Wayland backend +Ran the upgrade over tailscale as a transient unit (=ratio-upgrade.service=, +log at =/var/log/ratio-upgrade.log=): 714 packages, =--ignore= on the six +packages the live-update guard would have blocked (aquamarine, hyprland, +hyprutils, mesa, vulkan-radeon, wayland — still pending, apply from a TTY +before the reboot). One orphan cleared first: =qemu-block-gluster= had been +dropped from the repo and pinned =qemu-common=; the new =qemu-full= no +longer needs it. Killed the plain =emacs --daemon= (no modified buffers, no +graphical frames), started =emacs.service= instead so the daemon carries the +systemd user environment, and probed from a throwaway frame: +=(pgtk-backend-display-class)= → =GdkWaylandDisplay=, =*Warnings*= alive. +Ratio still wants a reboot for =linux 7.1.9=. Pacnews to review there: +=/etc/ssh/sshd_config.pacnew= and two =/etc/tpm2-tss/fapi-profiles/*.json=. +** DONE [#B] Function keys issue media actions instead of F-keys :bug:velox: +CLOSED: [2026-09-01 Tue] +:PROPERTIES: +:CREATED: [2026-08-19 Wed] +:LAST_REVIEWED: 2026-08-19 +:END: + +From the roam inbox, Craig's words: "function keys should issue F+number +functionality rather than their media functionality when the button is hit. +currently it's reversed and I have to hit function and the f button for F+number +functionality." + +Check first whether this belongs to archsetup at all. On a Framework the Fn-lock +is a firmware-level toggle held in the keyboard itself (Fn+Esc on most +revisions), not something the OS sets, in which case this is one keystroke +rather than a change here. If it is instead a hid/keyboard-module quirk, it is +ours. + +Grading: Minor severity (the keys work, they are on the wrong layer, and there +is a workaround) x every user every time (every F-key press) = P2 = [#B]. + +Resolved 2026-09-01: not ours, as the body suspected. The Fn layer is decided +in the EC (the keyboard reaches Linux as a plain AT keyboard on i8042), so no +OS-side knob exists. One keystroke: Fn+Esc toggles Fn Lock; Craig confirmed +F1-F12 now send F-keys by default. The EC holds the state across reboots; it +reverts only if the EC loses power (battery disconnect or mainboard reset), +which is likely why it flipped around the August reinstall. |
