aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--archive/task-archive.org85
-rw-r--r--todo.org459
2 files changed, 272 insertions, 272 deletions
diff --git a/archive/task-archive.org b/archive/task-archive.org
index ba43804..cb2081a 100644
--- a/archive/task-archive.org
+++ b/archive/task-archive.org
@@ -1701,3 +1701,88 @@ 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).
diff --git a/todo.org b/todo.org
index f07efeb..176d62b 100644
--- a/todo.org
+++ b/todo.org
@@ -412,65 +412,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.
-** 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.
** TODO [#B] velox's systemd --user spins at 96% and cannot resolve unit files :bug:velox:
:PROPERTIES:
:CREATED: [2026-08-17 Mon]
@@ -529,88 +470,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.
-** 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.
** TODO [#B] post-rebuild-check needs a reference-host mode :feature:velox:solo:
:PROPERTIES:
:CREATED: [2026-08-17 Mon]
@@ -1155,35 +1014,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.
-** 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.
** TODO [#C] Auto-dim status forgotten on layout change :bug:dotfiles:
:PROPERTIES:
:LAST_REVIEWED: 2026-07-25
@@ -1204,23 +1034,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.
-** 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.
** TODO [#C] Night-watch live telemetry :feature:maint:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-02
@@ -3140,91 +2953,6 @@ carry a date and Craig's return date isn't known yet — date it and raise it to
Workaround in the meantime: Bluetooth mouse, already in use.
-** 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).
@@ -4632,3 +4360,190 @@ 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.