aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--assets/2026-06-19-collapsible-waybar-sides-spec.org2
-rw-r--r--assets/2026-07-03-instrument-console-panels-build-summary.org3
-rw-r--r--assets/outbox/2025-11-08-keyring-fix-next-steps.org3
-rw-r--r--assets/outbox/2025-11-08-test-failure-analysis.org2
-rw-r--r--docs/PLAN-dotfiles-separation.org6
-rw-r--r--docs/PLAN-per-host-overrides.org2
-rw-r--r--docs/design/2026-06-25-testinfra-validation.org8
-rw-r--r--docs/design/2026-06-29-waybar-timer-module-spec.org2
-rw-r--r--docs/specs/2026-07-07-maintenance-console-spec.org2
-rw-r--r--docs/specs/2026-07-09-audio-doctor-spec.org284
-rw-r--r--docs/workflows/strix-soak-watch.org10
-rw-r--r--docs/workflows/system-health-check.org4
-rw-r--r--todo.org215
13 files changed, 443 insertions, 100 deletions
diff --git a/assets/2026-06-19-collapsible-waybar-sides-spec.org b/assets/2026-06-19-collapsible-waybar-sides-spec.org
index b9ddc0d..a80d755 100644
--- a/assets/2026-06-19-collapsible-waybar-sides-spec.org
+++ b/assets/2026-06-19-collapsible-waybar-sides-spec.org
@@ -1,5 +1,5 @@
#+TITLE: Collapsible waybar sides — implementation spec
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-06-19
* Goal
diff --git a/assets/2026-07-03-instrument-console-panels-build-summary.org b/assets/2026-07-03-instrument-console-panels-build-summary.org
index 0602570..6e975bf 100644
--- a/assets/2026-07-03-instrument-console-panels-build-summary.org
+++ b/assets/2026-07-03-instrument-console-panels-build-summary.org
@@ -1,7 +1,6 @@
#+TITLE: Instrument-Console Panel Rebuild — Build Summary
#+DATE: 2026-07-03
-#+AUTHOR: Craig Jennings & Claude
-
+#+AUTHOR: Craig Jennings
Findings summary for the no-approvals speedrun that rebuilt the net and
bluetooth waybar panels as single-screen instrument consoles. Spec:
[[file:../docs/specs/2026-07-03-instrument-console-panels-spec.org][2026-07-03-instrument-console-panels-spec.org]] (ID e73877f5, IMPLEMENTED).
diff --git a/assets/outbox/2025-11-08-keyring-fix-next-steps.org b/assets/outbox/2025-11-08-keyring-fix-next-steps.org
index e478ac3..c378335 100644
--- a/assets/outbox/2025-11-08-keyring-fix-next-steps.org
+++ b/assets/outbox/2025-11-08-keyring-fix-next-steps.org
@@ -1,7 +1,6 @@
#+TITLE: GNOME Keyring Authentication Fix - Next Steps
#+DATE: 2025-11-08
-#+AUTHOR: Craig Jennings & Claude
-
+#+AUTHOR: Craig Jennings
* URGENT: Complete Keyring Fix on Next Login
** What Was Done
diff --git a/assets/outbox/2025-11-08-test-failure-analysis.org b/assets/outbox/2025-11-08-test-failure-analysis.org
index 56453c3..93377f3 100644
--- a/assets/outbox/2025-11-08-test-failure-analysis.org
+++ b/assets/outbox/2025-11-08-test-failure-analysis.org
@@ -1,5 +1,5 @@
#+TITLE: Test Failure Analysis - VM Test Run 20251108-204202
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2025-11-08
* Test Overview
diff --git a/docs/PLAN-dotfiles-separation.org b/docs/PLAN-dotfiles-separation.org
index 8ef583f..5ce47d5 100644
--- a/docs/PLAN-dotfiles-separation.org
+++ b/docs/PLAN-dotfiles-separation.org
@@ -1,5 +1,5 @@
#+TITLE: Plan — Separate dotfiles from archsetup
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-05-13
* Overview
@@ -154,7 +154,7 @@ verified against existing repos at the same host on 2026-05-14).
In a temp working dir (does NOT touch the live archsetup repo):
#+begin_src bash
-git clone --no-local /home/cjennings/code/archsetup /tmp/extract-dotfiles
+git clone --no-local "$(git rev-parse --show-toplevel)" /tmp/extract-dotfiles
cd /tmp/extract-dotfiles
git filter-repo --subdirectory-filter dotfiles/
#+end_src
@@ -212,7 +212,7 @@ Document that the user's repo must contain =common/= plus =dwm/=,
*** Step 2.2 — Update archsetup script
-Edits to =/home/cjennings/code/archsetup/archsetup=:
+Edits to the =archsetup= script at the repo root:
1. *Read config* (around line 114-122): map =DOTFILES_REPO= / =DOTFILES_BRANCH=
/ =DOTFILES_DIR= env vars to lowercase script variables.
diff --git a/docs/PLAN-per-host-overrides.org b/docs/PLAN-per-host-overrides.org
index 6816a21..9eafb4d 100644
--- a/docs/PLAN-per-host-overrides.org
+++ b/docs/PLAN-per-host-overrides.org
@@ -1,5 +1,5 @@
#+TITLE: Per-Host Override Mechanism for the Dotfiles Repo
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-05-26
* Status
diff --git a/docs/design/2026-06-25-testinfra-validation.org b/docs/design/2026-06-25-testinfra-validation.org
index 5c82aa2..0f81231 100644
--- a/docs/design/2026-06-25-testinfra-validation.org
+++ b/docs/design/2026-06-25-testinfra-validation.org
@@ -106,16 +106,18 @@ scripts/testing/tests/
** Example tests (parity)
#+begin_src python
+TEST_USER = os.environ.get("ARCHSETUP_TEST_USER", "archie")
+
def test_ufw_enabled(host):
assert host.service("ufw").is_enabled
-def test_user_cjennings_exists(host):
- u = host.user("cjennings")
+def test_primary_user_exists(host):
+ u = host.user(TEST_USER)
assert u.exists
assert u.shell == "/usr/bin/zsh"
def test_zshrc_stowed_and_readable(host):
- f = host.file("/home/cjennings/.zshrc")
+ f = host.file(f"/home/{TEST_USER}/.zshrc")
assert f.is_symlink
assert ".dotfiles/" in f.linked_to
assert f.exists # not broken
diff --git a/docs/design/2026-06-29-waybar-timer-module-spec.org b/docs/design/2026-06-29-waybar-timer-module-spec.org
index 4b0ed0e..e8eecbe 100644
--- a/docs/design/2026-06-29-waybar-timer-module-spec.org
+++ b/docs/design/2026-06-29-waybar-timer-module-spec.org
@@ -1,5 +1,5 @@
#+TITLE: Waybar Timer Module (wtimer) — Design Spec
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-06-29
* Goal
diff --git a/docs/specs/2026-07-07-maintenance-console-spec.org b/docs/specs/2026-07-07-maintenance-console-spec.org
index bff187b..9810fe8 100644
--- a/docs/specs/2026-07-07-maintenance-console-spec.org
+++ b/docs/specs/2026-07-07-maintenance-console-spec.org
@@ -321,7 +321,7 @@ The four-layer strategy is Decision 11; per-phase gates are in the phase list. V
- Design doc (all dated decisions + the full metric tables with per-metric Automation/levers/notes): [[file:../design/maintenance-console-design-ideas.org][maintenance-console-design-ideas.org]]. The metric inventory is deliberately not duplicated here — the tables there are normative for *which metrics exist*. Where a table's Automation column disagrees with the doc's later dated decisions (failed-unit restart, pending updates, AUR staleness, and =-Qkk= reinstall all read Workflow in the tables but gained Confirm levers in the 2026-07-07 determinate-remedies and updates decisions), the dated decision prose — mirrored in Decisions 2, 3, and 7 here — wins. The Confirm set that defines REVIEW & FIX membership derives from the decisions, never from the table column.
- Converged prototype (interactive; SIM FAIL / SIM BAD DAY / SIM ZFS controls): [[file:../prototypes/2026-07-07-maint-console-E5-selector-subpanel.html][E5]]; exploration path A–E4 beside it.
- Sibling precedent: net =priv.py= (privilege pattern), audio =peak.py=/meters (visibility-gated live refresh), instrument-console-panels-spec (visual kit), PTT build (waybar runtime-config gotcha).
-- Home workflow to move: =~/projects/home/.ai/project-workflows/system-health-check.org= + =homelab-inventory/*.org=.
+- Workflow to move in from its previous owner: =system-health-check.org= + the host capability inventories.
* Review and iteration history
** 2026-07-07 Tue @ 18:30:04 -0500 — Claude Code (archsetup) — author
diff --git a/docs/specs/2026-07-09-audio-doctor-spec.org b/docs/specs/2026-07-09-audio-doctor-spec.org
new file mode 100644
index 0000000..028fe8c
--- /dev/null
+++ b/docs/specs/2026-07-09-audio-doctor-spec.org
@@ -0,0 +1,284 @@
+#+TITLE: Audio Doctor — diagnose and repair a broken sound stack
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-09
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* READY Audio Doctor — diagnose and repair a broken sound stack
+:PROPERTIES:
+:ID: 6ce3f2ef-052e-40cb-9067-42f8e665f813
+:END:
+- [2026-07-09 Thu] READY — all eight Decisions closed. Written from the roam-inbox ask "look into a doctor button for the audio panel: what happens if pulseaudio or its equivalent is borked? would we know? how could we test for it? what remedies would we have?" Probe set, classifier, and remedy tiers are grounded in a live survey of ratio's stack. Phase 0 shipped ahead of the rest.
+
+* READY Status
+:PROPERTIES:
+:ID: 1cc2b355-9450-4ace-bdb3-b935f7051918
+:END:
+- [2026-07-09 Thu] READY — Craig closed the six open Decisions, all to the recommended option: DOCTOR is a section-header key; CLI-first with the GUI as a face; hung and dead are two verdicts sharing one remedy; tiers stay Auto/Confirm/Arm as drafted; a stream-active guard refuses the audible remedies and is overridable by pressing again; xruns are out of v1.
+
+ Two consequences worth naming, because they were not obvious when the questions were asked. Taking the guard is *why* remedy 4 can stay at Confirm — the danger lives in the state of the machine, not the identity of the remedy. And the guard must read its stream state from =pw-dump= rather than =pactl=, because the remedies it protects are exactly the ones you reach for when the Pulse layer is dead; a =pactl=-fed guard would go blind precisely when it is needed.
+- [2026-07-09 Thu] DRAFT — Phase 0 shipped (dotfiles =4d42eb3=) and its Decision closed. The first draft claimed =build_status()= had no timeout and froze the panel; driving it disproved that — =pactl.run()= already times out. The real bug was narrower: two of the four reads sat outside the degrade guard, killing waybar's audio module. The claim is corrected in place rather than quietly dropped.
+- [2026-07-09 Thu] DRAFT — spine complete, Decisions open. Not ready to build: the remedy tiers touch a running meeting's audio, and the classifier's precedence needs Craig's call before code.
+
+* Metadata
+
+| Field | Value |
+|--------+-------------------------------------------------------------------------------------------------------------|
+| Status | ready |
+|--------+-------------------------------------------------------------------------------------------------------------|
+| Owner | Craig Jennings |
+|--------+-------------------------------------------------------------------------------------------------------------|
+| Repo | dotfiles (=audio/= package); archsetup owns this spec |
+|--------+-------------------------------------------------------------------------------------------------------------|
+| Kin | net panel doctor (=net/diag.py=, =classify.py=, =repair.py=, =doctor.py=) — the architecture to mirror. |
+| | Maintenance console (results wall, arm-to-fire) — the interaction idiom. |
+|--------+-------------------------------------------------------------------------------------------------------------|
+
+* Summary
+
+The audio panel shows the sound graph and lets you drive it. It has no answer for the graph being *broken*. When PipeWire, WirePlumber, or the Pulse compatibility layer fails, the panel shows an empty or stale board and the user is left to remember service names.
+
+This adds a DOCTOR key to the audio panel — read-only diagnose, then a lightest-repair-first fix — mirroring the net panel's doctor, which already solves this shape for networking.
+
+* Problem / Context
+
+Craig's questions, answered from a live survey of ratio (2026-07-09):
+
+** "What happens if pulseaudio or its equivalent is borked?"
+
+There is no PulseAudio. The stack is three user-scope services:
+
+| Unit | Role | Failure looks like |
+|--------------------------+---------------------------------------------+---------------------------------------------|
+| =pipewire.service= | the media graph itself | no devices at all; everything silent |
+|--------------------------+---------------------------------------------+---------------------------------------------|
+| =wireplumber.service= | session manager: routing, policy, device | devices exist but nothing routes; defaults |
+| | profiles, default picking | unset; profiles never applied |
+|--------------------------+---------------------------------------------+---------------------------------------------|
+| =pipewire-pulse.service= | Pulse protocol compat, via | =pactl= fails; the panel goes blind while |
+| | =pipewire-pulse.socket= | audio may still work for others |
+|--------------------------+---------------------------------------------+---------------------------------------------|
+
+The third row is the trap. The panel speaks =pactl=, so a dead =pipewire-pulse= blinds the panel even though PipeWire-native clients still play. The panel's own emptiness is therefore *not* evidence that audio is broken, and the doctor must not treat it as such.
+
+** "Would we know?"
+
+Today, no — and worse than no. Two findings from the live survey:
+
+1. *=pactl= hangs on a hung server.* Against a refused socket it fails in ~0ms ("Connection refused"). Against a socket that accepts and never answers, it blocks indefinitely — measured: still blocked at the 5s timeout. A hung server is precisely the case the doctor exists for, so *every probe carries a timeout*. The engine's own =pactl.run()= already does this and raises =PactlTimeout=; the doctor must do the same, and must never call a bare =subprocess.run=.
+
+2. *The panel cannot diagnose itself.* Its only data source is the layer most likely to be down. The doctor's probes therefore read =systemctl --user= (never blocks on the audio graph) and =pw-dump= / =wpctl= (PipeWire-native, alive when the Pulse layer is not) *before* they touch =pactl=.
+
+** "How could we test for it?"
+
+Four probe tiers, cheapest and most reliable first. Each is independently useful; the classifier below reads all four.
+
+*** Tier 1 — unit state (no audio IO, cannot hang)
+=systemctl --user show -p ActiveState -p Result <unit>= for =pipewire=, =wireplumber=, =pipewire-pulse=, and the two sockets. Distinguishes =active= / =failed= / =inactive= / =not-found=.
+
+*** Tier 2 — graph liveness (PipeWire-native, survives a dead Pulse layer)
+=pw-dump= with a timeout. Answers: does the graph exist, how many nodes, are there sinks and sources at all. =wpctl status= gives the same in human form. A graph with zero sinks is a different fault from a graph that will not answer.
+
+*** Tier 3 — Pulse compat reachability (the panel's own dependency)
+=pactl info= under a hard timeout. Three outcomes, and they are *different faults*: answered / refused / hung. The hung case is the one that currently freezes tooling, and it is invisible to a naive "does pactl work" check because that check hangs too.
+
+*** Tier 4 — semantic health (only meaningful once 1–3 pass)
+Default sink and source resolve to devices that exist; at least one sink is not =SUSPENDED= when a stream is playing. Ratio's sink states today are =RUNNING= and =SUSPENDED=; =SUSPENDED= alone is normal idle, not a fault.
+
+Xruns are deliberately *not* probed in v1 — see the Decision "Do xruns belong in v1?" below. They are a quality signal with no remedy in this feature's vocabulary, and a finding nobody can act on trains the user to ignore the wall.
+
+** "What remedies would we have?"
+
+All three units are =--user= scope. *No privilege escalation is needed anywhere in this feature*, so the maintenance console's =priv.py= verb table and its =sudo -n= machinery are dead weight here and must not be copied in. This is the single largest architectural difference from the maint console, and the reason the audio doctor mirrors the *net* panel instead.
+
+Remedies, lightest first:
+
+| # | Remedy | Blast radius | Tier |
+|---+-----------------------------------------------+----------------------------------------------+---------|
+| 1 | =systemctl --user restart wireplumber= | routing/profiles re-applied; streams survive | Auto |
+|---+-----------------------------------------------+----------------------------------------------+---------|
+| 2 | Set default sink/source to a present device | one setting | Auto |
+|---+-----------------------------------------------+----------------------------------------------+---------|
+| 3 | Unmute + restore volume on the default device | audible, but reversible | Confirm |
+|---+-----------------------------------------------+----------------------------------------------+---------|
+| 4 | =systemctl --user restart pipewire-pulse= | Pulse clients drop and reconnect | Confirm |
+|---+-----------------------------------------------+----------------------------------------------+---------|
+| 5 | =systemctl --user restart pipewire= | *every* audio client's stream drops | Arm |
+|---+-----------------------------------------------+----------------------------------------------+---------|
+
+Remedy 5 is the one that cuts audio out from under a live meeting. It gets the arm-to-fire treatment, and its arm line says so in words.
+
+* Goals and Non-Goals
+
+** Goals
+
+- Answer "is my sound stack broken, and if so where" without the user knowing a service name.
+- Work *when the panel's own data source is down* — a doctor that needs =pactl= to answer is useless in the case it exists for.
+- Never hang. Every probe is bounded; a probe that times out is a *finding*, not a stall.
+- Repair from the lightest remedy that could plausibly fix the classified fault, exactly as the net doctor does.
+- Read-only by default. Diagnose without touching anything; fixing is a separate, explicit press.
+
+** Non-Goals
+
+- Fixing a Bluetooth audio device that will not take an A2DP profile. That is the bluetooth panel's problem and it already has a doctor-shaped surface.
+- Per-application stream routing or a full patchbay. Helvum and qpwgraph exist.
+- Diagnosing hardware failure (a dead DAC, an unplugged cable). The doctor can report "sink present but never leaves SUSPENDED"; it cannot see a cable.
+- Sample-rate, quantum, or latency tuning. A separate concern with a separate audience.
+- Any use of =sudo=. If a remedy seems to need it, the remedy is wrong.
+
+* Design
+
+** For the user
+
+A DOCTOR key on the audio panel, beside the existing console keys. Pressing it streams findings into a results wall — the same wall idiom the maintenance console uses, so the interaction is already learned:
+
+#+begin_example
+DOCTOR diagnose · read-only
+ ok pipewire.service active
+ ok wireplumber.service active
+ FAIL pipewire-pulse.service failed (Result=exit-code)
+ ok graph 4 sinks · 7 sources (via pw-dump)
+ FAIL pulse compat not answering — pactl refused
+ -- defaults not checked (pulse compat down)
+
+ verdict: the Pulse compatibility layer is down.
+ PipeWire itself is healthy — native clients still play.
+ FIX restarts pipewire-pulse.
+#+end_example
+
+The verdict names one fault, in the user's terms, and names the remedy it would run. Then a FIX key applies the lightest remedy for that verdict, streaming each step and re-probing after, so the wall shows repaired truth rather than an optimistic claim.
+
+When everything passes, the wall says so and offers nothing to press.
+
+** For the implementer
+
+Mirror the net panel's module split, which already separates these concerns cleanly:
+
+| Module | Responsibility |
+|---------------------+------------------------------------------------------------------------------------------------|
+| =audio/diag.py= | The four probe tiers. Pure IO, each bounded by a timeout. Returns a context dict; never raises |
+| | on a broken stack, never blocks. |
+|---------------------+------------------------------------------------------------------------------------------------|
+| =audio/classify.py= | Pure. Context dict → one verdict, by precedence. Unit-tested against fixture contexts, no |
+| | services involved. |
+|---------------------+------------------------------------------------------------------------------------------------|
+| =audio/repair.py= | One function per remedy. Each returns a result envelope; each re-probes the thing it claimed |
+| | to fix. |
+|---------------------+------------------------------------------------------------------------------------------------|
+| =audio/doctor.py= | Orchestration: gather → classify → (optionally) attempt the lightest matching remedy → |
+| | re-probe → emit the event stream. |
+|---------------------+------------------------------------------------------------------------------------------------|
+
+Three constraints that fall out of the survey and are easy to get wrong:
+
+1. *Timeouts everywhere, and guard every read.* Bound every probe. Beyond that: a function that promises to degrade must guard *all* of its reads, not the first two. That was the Phase 0 bug. Test with a fault that answers early calls and hangs on later ones — a server that hangs on the first call masks everything behind it, and a refused socket does not reproduce the hang at all.
+
+2. *Probe order is load-bearing.* =systemctl= first (cannot hang), then =pw-dump= (alive when Pulse is dead), then =pactl= (may hang). Reversing this makes the doctor unable to report on the case it was built for.
+
+3. *The GTK thread never probes.* The panel already has a =bg()= worker; the doctor runs there and lands events on the main loop, as the maint console's =_stream= does.
+
+*** Classifier precedence (draft — see Decision 3)
+
+Terminal faults first, so a lower-layer failure is never reported as an upper-layer symptom:
+
+1. =pipewire.service= not active → *graph down*. Nothing else is meaningful.
+2. =wireplumber.service= not active → *no session manager*. Devices exist, routing does not.
+3. =pactl= hangs → *pulse compat hung*. Distinct from dead: a restart is the remedy, but the diagnosis differs and the user should see which.
+4. =pipewire-pulse= not active, or =pactl= refused → *pulse compat down*.
+5. Graph healthy, zero sinks → *no output devices*.
+6. Default sink/source names a device that is absent → *stale default*.
+7. Everything up, default device muted or at zero volume → *silenced, not broken* (a finding, not a fault; offer to unmute).
+8. Otherwise → *healthy*.
+
+*** Testing
+
+Follows the four-layer pattern the maint console established:
+
+- *Unit, fakes.* =classify.py= against fixture contexts covering every verdict, including the hung-vs-dead distinction. =diag.py= against a fake =systemctl= / =pactl= / =pw-dump= on a temp PATH, one case per outcome. Pairwise over (unit state × graph state × pulse reachability) — three parameters with three, three, and four values each is 36 exhaustive, and pairwise covers the interactions in far fewer.
+- *Timeout regression.* A test that stands up a Unix socket which accepts and never answers, points =PULSE_SERVER= at it, and asserts the probe returns a =hung= finding within its timeout rather than blocking. This is the test that would have caught the real behavior; write it first.
+- *Live read-only.* =audio doctor= (no fix) on ratio and velox, asserting a healthy verdict.
+- *Remedy, in a VM.* Restarting =pipewire= is not something to exercise on a live desktop. The archsetup VM harness runs remedies 1, 4, and 5 against a real stack.
+- *GUI smoke.* AT-SPI over the DOCTOR key and the wall, on fixtures.
+
+* Alternatives Considered
+
+** Reuse the maintenance console's doctor rather than building one in the audio panel
+
+Rejected. The maint doctor's remedies are system-scope and route through =priv.py= and =sudo -n=. Every audio remedy is =--user= scope. Bolting audio onto that table means either giving audio remedies a privilege path they do not need, or special-casing them out of the mechanism that defines the table. The net panel already demonstrates the right shape for a user-scope doctor.
+
+** A single RESTART AUDIO button
+
+Rejected. It is remedy 5 with no diagnosis: it drops every client's stream to fix a problem that is usually remedy 1 or 2. It also teaches nothing — the user learns to press it for every symptom, including symptoms it cannot fix, such as a stale default device.
+
+** Probe with =pactl= only, since the panel already speaks it
+
+Rejected on evidence. =pactl= hangs against a hung server, and it is dead when the Pulse compat layer is dead — the two most interesting faults. It is the *last* probe, not the first.
+
+** Auto-repair on detection, without asking
+
+Rejected. Remedies 3 through 5 are audible and, in a meeting, disruptive. Diagnosis is free and should be; repair is a press.
+
+* Decisions [8/8]
+
+** DONE Where does the DOCTOR key live, and does it replace anything?
+CLOSED: [2026-07-09 Thu]
+Resolved: a section-header key, the maint console's CLEAN UP / REVIEW & FIX shape. Diagnose and fix are two presses with different consequences, and the header is where the console already teaches that distinction. The console row (INPUT / OUTPUT / PUSH TALK) stays device controls; a doctor is not one. Nothing is replaced.
+
+** DONE Is there an =audio doctor= CLI, and is it the same code path?
+CLOSED: [2026-07-09 Thu]
+Resolved: CLI-first, GUI as a face, one code path — the maint console's architecture. =audio doctor= (read-only) and =audio doctor --fix= print the same findings the wall streams.
+
+The deciding reason: when the desktop's sound is dead, a TTY is where you will be standing. A GUI-only doctor cannot serve the case that motivates the whole feature. It also makes the classifier testable without a display.
+
+** DONE Confirm the classifier precedence above, particularly rows 3 and 4.
+CLOSED: [2026-07-09 Thu]
+Resolved: precedence as drafted, and rows 3 and 4 stay *two verdicts sharing one remedy*. They share a fix but not a diagnosis, and the hung case is the one that freezes other tooling — collapsing them would hide the more interesting fault behind the more common one.
+
+** DONE Does the panel's own status build get the same timeout treatment?
+CLOSED: [2026-07-09 Thu]
+Investigated and answered: it already had one. =pactl.run()= defaults to =timeout=8= and raises =PactlTimeout=, so the panel degrades rather than freezing. The draft of this spec claimed otherwise; the claim was wrong.
+
+What the investigation *did* find is a narrower, real bug, now fixed (dotfiles =4d42eb3=): =build_status()= guarded its two device-list reads but called =default_sink()= and =default_source()= outside the guard, so a server that answered the lists and then stopped answering raised =PactlTimeout= out of a function whose contract is to degrade. =waybar-audio= calls it with nothing catching it, so the bar's audio module died rather than dimming. A server that hangs on the *first* call never exposed this — =list_sinks= raised inside the guard and masked everything behind it, which is why the obvious test would have passed against broken code.
+
+** DONE Which remedies are Auto, which Confirm, which Arm?
+CLOSED: [2026-07-09 Thu]
+Resolved: the table as drafted. Remedies 1–2 (restart wireplumber, set a present default) are Auto; 3–4 (unmute + restore volume, restart pipewire-pulse) are Confirm; 5 (restart pipewire) is Arm.
+
+Remedy 4 stays at Confirm rather than Arm *because* the stream guard below was taken. The guard, not the tier, is what catches the mid-meeting case — which is the honest place for it, since the danger is the state of the machine, not the identity of the remedy. Had the guard been declined, remedy 4 would have moved to Arm.
+
+** DONE Should the doctor refuse to run a disruptive remedy while audio is playing?
+CLOSED: [2026-07-09 Thu]
+Resolved: yes. A stream-active guard refuses a disruptive remedy and is overridable by pressing again, mirroring the maintenance console's live-update guard. Same shape of problem: an operation that is fine when idle and destructive mid-use, where wording alone does not stop a press.
+
+Implementation note for the builder: the guard's input is "is any node running" — and it must come from *Tier 2* (=pw-dump=), not from =pactl=. The remedies the guard protects are precisely the ones you reach for when the Pulse layer is dead or hung, so a guard that asked =pactl= would go blind exactly when it is needed, and a PipeWire-native client can be mid-playback while =pactl= answers nothing at all.
+
+A cold or unreadable graph means the guard cannot see a stream. Treat that as *no stream* and let the remedy proceed: refusing on an unreadable graph would wedge the doctor shut in the worst case, which is the fault it exists to repair. Say so in the wall, so the user knows the guard abstained rather than passed.
+
+The guard applies to remedies 3, 4, and 5 — the audible ones. Its refusal names what is playing, not just the remedy.
+
+** DONE Do xruns belong in v1?
+CLOSED: [2026-07-09 Thu]
+Resolved: left out. They are a quality signal, not a broken/working one, and they have no remedy in this feature's vocabulary. A finding you cannot act on trains you to ignore the wall — the opposite of what a doctor is for. Revisit when there is something to do about them.
+
+* Implementation phases
+
+Sequenced so each phase is independently green and independently useful. Nothing here starts until the Decisions are closed.
+
+** DONE Phase 0 — the degrade-on-hang bug fix (prerequisite)
+CLOSED: [2026-07-09 Thu]
+Shipped ahead of the rest (dotfiles =4d42eb3=). =build_status()= now guards all four pactl reads, not just the two list reads, so a half-wedged server dims the bar's audio module instead of killing it. =fake-pactl= grew =FAKE_PACTL_SLEEP_DEFAULTS= to express "answers the lists, hangs on the defaults" — the blanket sleep knob cannot, because the first call would hang and mask the bug.
+
+** Phase 1 — =diag.py=, the four probe tiers
+Bounded probes, a context dict, fakes for every outcome including hung. No classifier, no GUI. =audio diag --json= prints the context.
+
+** Phase 2 — =classify.py=, the verdict
+Pure function, fixture-driven, pairwise over the parameter space. =audio doctor= (read-only) prints findings and a verdict in the CLI. Already useful from a TTY at this point.
+
+** Phase 3 — =repair.py= + =doctor.py=, the fix path
+One function per remedy, each re-probing what it claims to fix. =audio doctor --fix= applies the lightest matching remedy. VM-tested for remedies 1, 4, and 5.
+
+** Phase 4 — the DOCTOR key and the results wall
+GUI face over the CLI. Streams events to a wall, off the GTK thread. AT-SPI smoke over the key, the wall, and the fix path on fixtures.
+
+** Phase 5 — flip this spec to IMPLEMENTED
+With a history line naming what shipped and what was left out.
diff --git a/docs/workflows/strix-soak-watch.org b/docs/workflows/strix-soak-watch.org
index e2fbf68..b86921d 100644
--- a/docs/workflows/strix-soak-watch.org
+++ b/docs/workflows/strix-soak-watch.org
@@ -1,5 +1,5 @@
#+TITLE: Strix Halo SMU Deadlock — Kernel Update Watch
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-05-10
* Purpose
@@ -13,7 +13,7 @@ This is a one-time hardware/kernel issue. Delete this file when the strix kernel
- *Boot kernel:* =linux-lts-strix 6.18.25-1= (custom AUR-style local build), GRUB default pinned
- *SMC firmware:* =100.6.0= (from =/sys/kernel/debug/dri/*/amdgpu_firmware_info= SMC line)
- *Cmdline workaround:* =amdgpu.no_vpe_idle_pg=1= active
-- *Soak verdict:* HOLDING since 2026-04-30 — zero SMU/dcn35/VPE freeze signature hits (see =~/projects/home/assets/archive/2026-05-04-strix-soak-check-holding.org= (stays in the home project))
+- *Soak verdict:* HOLDING since 2026-04-30 — zero SMU/dcn35/VPE freeze signature hits (see the 2026-05-04 soak-check verdict, archived outside this repo)
* Background (one-paragraph summary so future sessions don't have to reconstruct it)
@@ -131,10 +131,8 @@ Drop the matching patches from the PKGBUILD on next strix rebuild; keep 0004 unt
* Reference Artifacts
-- Soak verdict (archived): =~/projects/home/assets/archive/2026-05-04-strix-soak-check-holding.org= (stays in the home project)
-- VPE-patch deployment session: =.ai/sessions/2026-04-29-23-35-linux-lts-strix-vpe-patch-deployed.org=
-- Soak fix + housekeeping session: =.ai/sessions/2026-05-03-06-48-soak-fix-and-velox-sync-housekeeping.org=
-- Verdict-holds session: =.ai/sessions/2026-05-05-01-48-strix-verdict-holds-mybitch-soak-deferred.org=
+- Soak verdict (archived): the 2026-05-04 soak-check verdict, archived outside this repo
+- Working records for the VPE-patch deployment (2026-04-29), the soak fix (2026-05-03), and the verdict-holds check (2026-05-05) are archived outside this repo.
- Forum thread: https://community.frame.work/t/smu-deadlock-system-freeze-on-fedora-43/81795
- Build dir: =~/build/linux-lts-patched/linux-lts/=
- Inventory: the =ratio - Desktop Workstation= node in =~/org/roam/hardware/= (resolve by =#+HOSTNAME: ratio=)
diff --git a/docs/workflows/system-health-check.org b/docs/workflows/system-health-check.org
index 98fa17f..b4f34a5 100644
--- a/docs/workflows/system-health-check.org
+++ b/docs/workflows/system-health-check.org
@@ -1,5 +1,5 @@
#+TITLE: System Health Check Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-02-27
* Overview
@@ -1030,7 +1030,7 @@ Each entry is scoped to one host (or =any=). When Phase 1 cross-references findi
** 2026-06-13: ratio — dockerized telega-server SIGSEGVs in musl build
:host: ratio
-- Symptom: =coredumpctl= shows repeated SIGSEGVs from =telega-server -O 31 -l /home/cjennings/.telega/telega-server.log -v 3= running inside the Telega Docker container. Example stack is entirely in the container's musl loader plus =/usr/bin/telega-server=, not host kernel, GPU, storage, or memory paths.
+- Symptom: =coredumpctl= shows repeated SIGSEGVs from =telega-server -O 31 -l ~/.telega/telega-server.log -v 3= running inside the Telega Docker container. Example stack is entirely in the container's musl loader plus =/usr/bin/telega-server=, not host kernel, GPU, storage, or memory paths.
- Evidence: =~/.telega/telega-server.log= ends with =Unexpected char 'm' in plist value= followed by =Assertion failed: false (telega-dat.c: tdat_plist_value: 500)=. Surrounding TDLib traffic includes sticker/custom-emoji metadata such as =documentAttributeCustomEmoji= and =PhotoSizeSourceThumbnail[Thumbnail, type = m]=.
- Prior local triage: =~/.emacs.d/todo.org= recorded the same issue on 2026-06-11 as spontaneous memory-corruption crashes in =zevlg/telega-server:latest='s musl build, with several coredumps occurring without action-verb traffic. Telega package installed at the 2026-06-13 health check was =20260513.509=; MELPA had =20260604.2321= available.
- Functional status at 2026-06-13 check: no coredumps yet that day; Telegram scans still worked from cached chat state. Treat as an app/server-container crash, not a machine-health fault.
diff --git a/todo.org b/todo.org
index 5d75413..1196e19 100644
--- a/todo.org
+++ b/todo.org
@@ -21,17 +21,6 @@ The vocabulary is open — topic tags are coined as needed — so these are conv
- *Effort / autonomy*: =:quick:= a spare-moment fix (minutes, not a sitting); =:solo:= Claude can carry it end to end — there's a build path, a test path, and no upfront decision needed (a leftover manual spot-check doesn't disqualify it).
- *Topic / area* (open): the subsystem a task touches — e.g. =:hyprland:= =:waybar:= =:mpd:= =:music:= =:network:= =:tooling:= =:llm:= =:eask:= =:pocketbook:= =:cmail:=. Coin a new one when it aids filtering.
* Archsetup Open Work
-** TODO [#B] Manual testing and validation: maintenance console :test:
-Promoted from the build parent when it closed 2026-07-08 — Craig's checklist, runs once in person. Collects everything not agent-verifiable; populate per verification.md as phases land. Known so far: panel look-and-feel vs the E5 prototype (Craig's eyeball); arm-press wording reads right at the moment of use; results-wall readability during a real doctor run; a real UPDATE through the armed guard (and the TTY path once); REBOOT offer after an update; velox in-person glyph check (battery %, charging glyph, low-charge red); SET 80% charge limit sticks across a charge cycle; KILL on a real memory hog.
-Phase 8 additions: subpanel scroll position survives an arm press on a long digest (MAINT_PANEL_FIXTURE=bad, PACKAGES band, scroll into the 51-orphan list, press REMOVE — the list should stay put and the key read REMOVE?); a KEEP on a real orphan dims it into the kept section and UNKEEP restores it; MERGE on a needs-merge pacnew opens pacdiff in a foot terminal.
-Phase 9a additions: MARK KNOWN on a real journal group (ratio's wpa_supplicant bgscan spam is the standing candidate) — arm shows the identifier + sample wording, fire writes curation.toml, the next re-scan drops the group into KNOWN NOISE with "marked <date>", and UNMARK restores it to SIGNAL; OPEN JOURNAL opens journalctl -p 3 -b in a foot terminal; a failed unit's RESTART on a real unit (systemctl restart via the armed key) lands the re-probe on the act line.
-Phase 9b additions: MARK EXPECTED on ratio's real unexpected listeners (postgres :5432, docker :8080 — the standing curation material) dims them into the expected list and UNMARK restores them; a CPU-mode press (e.g. BAL·PWR) writes the EPP hint and the row re-reads it; KILL renders disabled (insensitive, tooltip) on a session-critical name in top memory; on velox the battery row shows NO SET 80% key (cros_ec knob absent — correct withholding, not a bug).
-Phase 11 additions: glyph left-click opens the console and a second click closes it; right-click still toggles the btop scratchpad; the glyph color matches the board's worst lever-less diagnostic (open the console and check the watch items against the glyph tint); after fixing/curating a diagnostic, the glyph follows within one 30-min scan (or immediately after a manual =maint scan=); on velox in person — battery % + charging icon in the bar, collapsed bar keeps the battery glyph, low-charge red (drain below 15% or temporarily raise battery_low_pct in the TOML); after velox's reboot confirm systemctl --user list-timers shows both maint timers scheduled.
-Phase 10 additions: results-wall readability during a real doctor run (press CLEAN UP on the live board — every step should appear as a running line the moment it starts, resolve in place with the result, and the re-probed truth should follow; the wall should feel like watching the run, not reading a report afterward); COPY puts the same lines the CLI wall prints on the clipboard, paste somewhere to check; HIDE collapses the wall to its header and SHOW restores it with the scroll position at the bottom; REVIEW & FIX reads right at the moment of use (each row: what's wrong, the maint fix usage, and either a FIX key or "per-item — on its subpanel"); RECLAIM on a degraded disk row arms with the first step + "(+N more steps)" and the fire streams every step to the wall individually; a guard-tripped UPDATE fired from the REVIEW roster re-arms with the override wording and the second press actually runs (the token fix — verify the loop terminates).
-Phase 11b additions: the fidelity eyeball — open the live console beside the settled E5 prototype (headless-Chrome render or the browser) and judge whether the board finally reads as the same instrument: big-number typography, bar fills with the gold threshold tick on the cache card, the scrub/keyring/topgrade rings, status chips, the lit CPU-mode segment, selector count chips + severity left borders, right-aligned attention/ok/fixable/watch header; press an EPP segment on the card (e.g. BAL·PWR) and confirm it arms then fires exactly like the old digest control; on the bad fixture (MAINT_PANEL_FIXTURE=bad maint panel) check a long readout (the BACKUPS card) ellipsizes with a tooltip carrying the full value.
-*** 2026-07-08 Wed @ 06:31:00 -0500 Fixed TOPGRADE remedy failure found in testing (dotfiles 9b5384f)
-Craig's first testing find: TOPGRADE exited 2 on every launch — topgrade 17.6.1 names the git step git_repos and the remedy passed --disable git, rejected by clap before any step ran. Fixed red-first (test pins git_repos), full make test green, dry-run on the installed binary confirms the step name lands and the repo-rebasing step stays excluded. Velox pulled. RE-TEST: press TOPGRADE again (it's guard-armed on ratio's pending mesa set — TTY is still the safe path for the mesa run).
-
** TODO [#D] Maintenance console vNext :feature:
Deferred from the [[file:docs/specs/2026-07-07-maintenance-console-spec.org][spec]] (v1 ships determinate remedies only): AI/workflow assistance on read-only metrics (the vLater tier — failed-unit diagnosis, journal-error fix suggestions, OOM investigation); SIGKILL escalation for TERM-survivors on the KILL lever; live streaming meters on evidence rows where a count could be a level.
@@ -40,16 +29,33 @@ snapshot_retention_repair (WRITE SANE LIMITS — =snapper -c <config> set-config
Two fix shapes, decide at work time: (1) wire a digest key — e.g. on the timeline row when the config's installed TIMELINE_LIMIT_* drift from the TOML values (needs the probe to read the installed limits for comparison); or (2) keep it CLI-only deliberately and special-case the roster wording for remedies with no panel key ("CLI only — maint fix"). Option 2 is a few lines; option 1 makes limit drift visible on the board, which is the 2026-05-26 pile-up's root cause. Severity minor × rare edge = P4 per the bug matrix.
-** TODO [#C] minimal tier zsh login PATH gap :bug:dotfiles:quick:solo:
+** DONE [#C] minimal tier zsh login PATH gap :bug:dotfiles:quick:solo:
+CLOSED: [2026-07-09 Thu]
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-09
+:END:
+Fixed (dotfiles =c6a7878=). =minimal/.zprofile= added, mirroring =common/='s PATH-only prepend and its rationale comment.
+
+Tested the invariant rather than the file, because the bug *was* a per-file fix missing a tier: any stow tier shipping a =.zshrc= must ship a =.zprofile= that prepends =~/.local/bin=. Red first, and it named =minimal=. Verified behaviorally as the task asked — stowed the tier into a throwaway HOME and read a real =zsh -lc 'echo $PATH'=, which now leads with =~/.local/bin=. Full =make test= green.
+
The 2026-07-08 fix gave common/ a .zprofile (zsh logins never read .profile, so ~/.local/bin was missing from TTY/ssh PATH — dotfiles 39cef40). The minimal/ tier has the same gap: it carries .zshrc and .profile but no .zprofile, so headless installs (DESKTOP_ENV=none stows minimal/ instead of common/) still get zsh logins without ~/.local/bin. Add a matching minimal/.zprofile (PATH-only prepend, same rationale comment). Verify: fresh minimal stow, zsh -lc 'echo $PATH' shows ~/.local/bin first. The installer itself is clean — it writes no shell profiles; the stow layer owns this.
** TODO [#C] zfs base VM image build failure: ZFS DKMS module missing :bug:zfs:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-09
+:END:
=FS_PROFILE=zfs make test-vm-base= fails inside the VM at initramfs time: archangel reports "ZFS module not found! DKMS build may have failed" against the installed kernel (linux-lts 6.18.38 at the 2026-07-08 attempt). Likely a kernel/OpenZFS version skew — the archzfs zfs-dkms may not support 6.18 yet. Consequences: the maint scenario harness's zfs lane (Phase 12) is filtered but unexercised, and a real zfs bare-metal install via archangel would plausibly hit the same wall. Diagnosis needs the in-VM pacman/DKMS logs (rerun create-base-vm.sh with =--keep=-style access or pull /var/log from the ISO session before teardown). Priority per the bug matrix: Major severity (zfs install path broken) × some-users-sometimes = P3. When fixed, run =FS_PROFILE=zfs bash scripts/testing/run-maint-scenarios.sh --list= and add zfs scenario files (zpool scrub / autotrim / snapshot destroy) to the harness.
** TODO [#C] 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).
-** TODO [#C] Realtime lamp output for the net + bt doctors :feature:
+** TODO [#C] Realtime lamp output for the net + bt doctors :feature:solo:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-09
+:END:
Retrofit the net doctor (=~/.dotfiles/net/src/net/doctor.py=) and bluetooth doctor (=~/.dotfiles/bluetooth/src/bt/doctor.py=) to stream results as a live output wall — one lamp per escalation step, amber while running, green on success, red on failure — instead of a final summary. Matches the maintenance-console doctor design (see [[file:docs/design/maintenance-console-design-ideas.org][maintenance-console-design-ideas.org]], "Doctor = live output wall"). Goal: every doctor in the system reads the same way. Both doctors already step through an escalation chain re-probing after each, so the steps are natural lamp boundaries.
Scope note (Craig, 2026-07-07): realtime lamp *behavior* only. The maintenance console's wider results-wall layout (date+time stamp column, COPY, persistent history) does NOT backport — the net/bt panels are ~400px wide and lack the horizontal real estate. Their existing output wells keep their compact layout; this task just makes them stream live.
@@ -90,7 +96,7 @@ Refiled from the archsetup task audit (2026-06-28), landed via ~/.dotfiles/inbox
** TODO [#B] Waybar network module — custom/net :feature:waybar:network:
:PROPERTIES:
-:LAST_REVIEWED: 2026-07-04
+:LAST_REVIEWED: 2026-07-09
: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
@@ -188,7 +194,10 @@ The waybar network module ([#B] parent) is now COMPLETE through Phase 3. Phase 4
feature (indicator + recovery + panel + diagnostics + speed test) is done.
Verify (manual, live): see Manual testing and validation.
-*** TODO Phase 4 — docs + rollout :network:
+*** 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.
+
+*** TODO Phase 4 — docs + rollout :network:blocked:
Deliverable: in-app help (=net --help= + per-command, panel help affordance);
README/user-guide (commands, indicator states, panel, config keys, make targets,
troubleshooting from the failure table, rollback); archsetup Hyprland dep install
@@ -213,9 +222,9 @@ From the 2026-07-04 roam capture. The waybar collapse mechanism (click the trian
:END:
Follow-up from the 2026-07-04 net-panel hardening speedrun (Craig's cj question on the no-WiFi item). The shipped no-wifi-hardware verdict covers "no adapter at all." This tier covers "adapter present but the driver is wedged": read-only health signals — =ip link= (device present but no-carrier / down), =dmesg= / =journalctl -k= for firmware-load failures, =rfkill= for a hard block, =modinfo= / =lsmod= for the driver module — classified before a generic reset. Remedy actions: a privileged =modprobe -r <mod> && modprobe <mod>= reload of the wifi driver, and a firmware-package pointer when the failure is a missing/failed firmware load. Dotfiles net-package work (handled per the archsetup-owns-dotfiles rule). Design pass first to decide whether it's worth a repair tier vs a needs-user-action pointer.
-** TODO [#B] Desktop-settings dropdown panel :waybar:
+** TODO [#B] Desktop-settings dropdown panel :feature:waybar:
:PROPERTIES:
-:LAST_REVIEWED: 2026-06-24
+:LAST_REVIEWED: 2026-07-09
:END:
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).
@@ -452,9 +461,9 @@ Root cause was in =retry_install=: =last_exit_code=$?= ran AFTER =if eval ...; t
*** 2026-05-19 Tue @ 01:25:26 -0500 Verified the b9907c7 emacs-stow fix end-to-end
=make test= 21:44 → 22:29 (42 min), =test-results/20260518-214516/=. 52/0/5, =ArchSetup Exit Code: 0=. The third-branch path fired correctly — install log =archsetup-2026-05-18-21-45-46.log:14358-14365= shows =From https://git.cjennings.net/dotemacs= → =[new branch] main -> origin/main= → =Reset branch 'main'= → =branch 'main' set up to track 'origin/main'=. No exit-128, no =fatal: not a git repository=. Error Summary down to 7 (was 13 on 2026-05-16); the emacs entry is gone. AUR exit-0 logging triggered for 2 packages this run (mkinitcpio-firmware, tidaler) vs 6 on 2026-05-16 — same bug class, fewer triggers, still tracked under =[#B] AUR exit-0 logged as error=. Issue Attribution: 1 ARCHSETUP entry (Proton VPN Daemon failed — known VM-no-VPN-config artifact). Cleanup ran clean via the normal path.
-** TODO [#B] Review undeclared ratio packages for installer inclusion
+** TODO [#B] Review undeclared ratio packages for installer inclusion :chore:
:PROPERTIES:
-:LAST_REVIEWED: 2026-06-24
+:LAST_REVIEWED: 2026-07-09
: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.
@@ -545,9 +554,9 @@ Add kernel parameter: ~rtc_cmos.use_acpi_alarm=1~ (will become systemd default)
Consider: ~acpi_mask_gpe=0x1A~ for battery drain, suspend-then-hibernate config
See Framework community notes on logind.conf and sleep.conf settings
-** TODO [#C] Re-check python-lyricsgenius --skipinteg workaround :solo:
+** TODO [#C] Re-check python-lyricsgenius --skipinteg workaround :chore:solo:
:PROPERTIES:
-:LAST_REVIEWED: 2026-06-24
+:LAST_REVIEWED: 2026-07-09
:END:
archsetup installs =python-lyricsgenius= with =--mflags --skipinteg=, skipping makepkg integrity + PGP checks — a workaround originally for an expired-signature issue upstream (surfaced by the 2026-06-23 --noconfirm audit). Periodically test whether the cause has cleared: if a plain =aur_install python-lyricsgenius= builds without complaint, drop the =--skipinteg= workaround. Removal needs a real AUR build to confirm, so it isn't a blind change.
@@ -608,7 +617,42 @@ Parse yay errors and provide specific, actionable fixes instead of generic error
** TODO [#D] Improve progress indicators throughout install
Enhance existing indicators to show what's happening in real-time
-** TODO Manual testing and validation
+** TODO [#B] Manual testing and validation :test:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-09
+: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.
+
+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.
+
+*** Maintenance console — in-person checklist
+Re-homed here by the 2026-07-09 audit: this was a second top-level "Manual testing and validation" parent. One parent, per verification.md. Priority and the :test: tag now live on the parent.
+Promoted from the build parent when it closed 2026-07-08 — Craig's checklist, runs once in person. Collects everything not agent-verifiable; populate per verification.md as phases land. Known so far: panel look-and-feel vs the E5 prototype (Craig's eyeball); arm-press wording reads right at the moment of use; results-wall readability during a real doctor run; a real UPDATE through the armed guard (and the TTY path once); REBOOT offer after an update; velox in-person glyph check (battery %, charging glyph, low-charge red); SET 80% charge limit sticks across a charge cycle; KILL on a real memory hog.
+Phase 8 additions: subpanel scroll position survives an arm press on a long digest (MAINT_PANEL_FIXTURE=bad, PACKAGES band, scroll into the 51-orphan list, press REMOVE — the list should stay put and the key read REMOVE?); a KEEP on a real orphan dims it into the kept section and UNKEEP restores it; MERGE on a needs-merge pacnew opens pacdiff in a foot terminal.
+Phase 9a additions: MARK KNOWN on a real journal group (ratio's wpa_supplicant bgscan spam is the standing candidate) — arm shows the identifier + sample wording, fire writes curation.toml, the next re-scan drops the group into KNOWN NOISE with "marked <date>", and UNMARK restores it to SIGNAL; OPEN JOURNAL opens journalctl -p 3 -b in a foot terminal; a failed unit's RESTART on a real unit (systemctl restart via the armed key) lands the re-probe on the act line.
+Phase 9b additions: MARK EXPECTED on ratio's real unexpected listeners (postgres :5432, docker :8080 — the standing curation material) dims them into the expected list and UNMARK restores them; a CPU-mode press (e.g. BAL·PWR) writes the EPP hint and the row re-reads it; KILL renders disabled (insensitive, tooltip) on a session-critical name in top memory; on velox the battery row shows NO SET 80% key (cros_ec knob absent — correct withholding, not a bug).
+Phase 11 additions: glyph left-click opens the console and a second click closes it; right-click still toggles the btop scratchpad; the glyph color matches the board's worst lever-less diagnostic (open the console and check the watch items against the glyph tint); after fixing/curating a diagnostic, the glyph follows within one 30-min scan (or immediately after a manual =maint scan=); on velox in person — battery % + charging icon in the bar, collapsed bar keeps the battery glyph, low-charge red (drain below 15% or temporarily raise battery_low_pct in the TOML); after velox's reboot confirm systemctl --user list-timers shows both maint timers scheduled.
+Phase 10 additions: results-wall readability during a real doctor run (press CLEAN UP on the live board — every step should appear as a running line the moment it starts, resolve in place with the result, and the re-probed truth should follow; the wall should feel like watching the run, not reading a report afterward); COPY puts the same lines the CLI wall prints on the clipboard, paste somewhere to check; HIDE collapses the wall to its header and SHOW restores it with the scroll position at the bottom; REVIEW & FIX reads right at the moment of use (each row: what's wrong, the maint fix usage, and either a FIX key or "per-item — on its subpanel"); RECLAIM on a degraded disk row arms with the first step + "(+N more steps)" and the fire streams every step to the wall individually; a guard-tripped UPDATE fired from the REVIEW roster re-arms with the override wording and the second press actually runs (the token fix — verify the loop terminates).
+Phase 11b additions: the fidelity eyeball — open the live console beside the settled E5 prototype (headless-Chrome render or the browser) and judge whether the board finally reads as the same instrument: big-number typography, bar fills with the gold threshold tick on the cache card, the scrub/keyring/topgrade rings, status chips, the lit CPU-mode segment, selector count chips + severity left borders, right-aligned attention/ok/fixable/watch header; press an EPP segment on the card (e.g. BAL·PWR) and confirm it arms then fires exactly like the old digest control; on the bad fixture (MAINT_PANEL_FIXTURE=bad maint panel) check a long readout (the BACKUPS card) ellipsizes with a tooltip carrying the full value.
+**** 2026-07-08 Wed @ 06:31:00 -0500 Fixed TOPGRADE remedy failure found in testing (dotfiles 9b5384f)
+Craig's first testing find: TOPGRADE exited 2 on every launch — topgrade 17.6.1 names the git step git_repos and the remedy passed --disable git, rejected by clap before any step ran. Fixed red-first (test pins git_repos), full make test green, dry-run on the installed binary confirms the step name lands and the repo-rebasing step stays excluded. Velox pulled. RE-TEST: press TOPGRADE again (it's guard-armed on ratio's pending mesa set — TTY is still the safe path for the mesa run).
+
+*** CPU mode survives a reboot
+What we're verifying: =maint-epp-restore.service= replays the CPU mode the pill last set. The kernel resets EPP to the driver default on every boot, so this is the only check that can prove the fix; nothing an agent runs before a reboot distinguishes "it works" from "nothing was reset yet."
+- Open the maintenance console, MEM·PWR band, and press a CPU-mode segment other than the current one (say BAL·PWR).
+- Confirm the act line says the mode was remembered.
+#+begin_src sh :results output
+cat /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference
+grep -A1 '^\[power\]' ~/.config/maint/*.toml 2>/dev/null
+#+end_src
+- Reboot.
+- After logging back in, run the block below.
+#+begin_src sh :results output
+cat /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference
+systemctl --user status maint-epp-restore.service --no-pager | head -5
+#+end_src
+Expected: the EPP value after the reboot is the mode you selected, not the driver default, and the unit shows =Active: inactive (dead)= with =Result=success= (a oneshot that ran and exited).
+
*** Audio signal metering: apply the package (precondition)
What we're verifying: the signal-metering build is live. Only a =git pull= is needed — the =audio/= package .py files are stowed and no new launcher files were added, so no restow or reboot. The shared panel CSS gained a =.lamp.dim= class (live-idle); it's in the same stowed =panel.css=.
#+begin_src sh :results output
@@ -970,9 +1014,15 @@ NOTE (2026-07-04 audit): the "four-tab panel" framing predates the instrument-co
** DOING [#B] Prepare for GitHub open-source release
:PROPERTIES:
-:LAST_REVIEWED: 2026-07-04
+:LAST_REVIEWED: 2026-07-09
:END:
Remove personal info, credentials, and code quality issues before publishing.
+*** 2026-07-09 Thu @ 16:32:54 -0500 Audit reconcile: docs/ scrubbed; scripts and history still open
+The =docs/= half of "remove personal information" landed (=a8f7e9b=). Four absolute =/home/cjennings= paths became the repo root, a home-relative path, or a named variable — every snippet stays runnable. Three =~/projects/home= references and three =.ai/sessions/= links, all dead in any clone but Craig's, were replaced by what they mean. No =file:= links broke.
+
+The scrub also turned up something the epic didn't name: nine tracked files carried =#+AUTHOR: Craig Jennings & Claude=, a co-author who is not a person and which survives conversion into docx, a wiki page, or a PDF. The five under =docs/= are fixed. *Four remain, and they are Craig's call* (dated records under =assets/=, which the rule says stay as history, but which are tracked and would publish): =assets/2026-06-19-collapsible-waybar-sides-spec.org=, =assets/2026-07-03-instrument-console-panels-build-summary.org=, =assets/outbox/2025-11-08-keyring-fix-next-steps.org=, =assets/outbox/2025-11-08-test-failure-analysis.org=.
+
+The two open children below are untouched: the =scripts/= personal-information pass, and the git-history secret scrub.
*** 2026-06-16 Tue @ 00:55:39 -0500 Six dotfiles-scoped sub-tasks moved to the ~/.dotfiles project
Per the 2026-06-16 task audit, the six sub-tasks targeting files now owned by the standalone =~/.dotfiles= repo were handed off to that project (newly bootstrapped as its own AI project) and removed from this epic: "Remove credentials and secrets from dotfiles", "Remove/template personal info from dotfiles", "Remove binary font files from repo", "Move battery out of waybar sysmonitor group", "Resolution-adaptive scratchpad sizing", and "Dynamic waybar/foot config based on screen resolution". Handoff: =~/.dotfiles/inbox/2026-06-16-0053-from-archsetup-dotfiles-release-prep-handoff.org=. This epic now covers archsetup-proper release work only (scripts personal-info, device-specific config, history scrub, shellcheck, SPDX headers, README/LICENSE). The 2026-06-09 reconciliation note below is the prior state.
*** 2026-06-09 Tue @ 19:21:36 -0500 Reconciliation: six sub-tasks now target the ~/.dotfiles repo, not archsetup
@@ -1075,67 +1125,30 @@ 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.
-** DONE [#B] Signal and known-noise counts disagree with the journal :bug:maint:solo:
-CLOSED: [2026-07-09 Thu]
-Not a counting bug — a semantics bug (dotfiles eded0c0). journal_errors grouped by identifier and summed lines, but displayed one sample message, and logs.py overwrote that sample on every iteration so it named the *last* error rather than the counted one. Live on ratio: systemd had 10 error lines this boot — 6 "Failed to start Emacs text editor." and 4 org-roam auto-sync — which is exactly the 10-vs-6. Now grouped by identifier + normalized message (pids, hex addresses, uuids, long integer runs blanked), so a row's count is that message's count, and each row's hint carries a =--grep= reproducing it (verified: 6 and 4). MARK KNOWN binds the volatile-free literal instead of the pid-bearing sample, so marking suppresses exactly what the row counted. The KNOWN NOISE headline counts matched lines while the deck lists patterns; the caption now names both units.
-Logs > SIGNAL reports 10 journal errors and the signal item claims 10, but the group holds one item and the open journal shows it 6 times. KNOWN NOISE reports 19 against 12 visible entries. The counts are the point of the band, so a wrong number undermines the whole diagnostic. Find where the tally diverges from the rendered set — likely counting matched journal lines rather than distinct grouped entries, and counting curated entries against a different window than the one displayed.
-
-** DONE [#B] Persist the CPU pill setting across reboot :feature:maint:
-CLOSED: [2026-07-09 Thu]
-The kernel resets energy_performance_preference to the driver default every boot. A successful epp_set now records the preference in the maint user config; maint-epp-restore.service replays it at login. No-op when nothing is remembered, the host lacks EPP, or the mode already matches. A hand-edited bogus value is refused by priv's closed verb. Installer enables the unit via the existing wants-symlink idiom. Verified live on ratio: unit enabled, ran clean, EPP untouched. (dotfiles a7f34bd, archsetup 430ef1a)
-
-The power/perf/balance pill sets EPP live, but the value reverts to its original setting after a reboot. Add a way to make a selection permanent. Needs a call on where persistence lives — a systemd unit, a udev rule, or a config file the maint package owns.
-
-** DONE [#B] Preview the pacman + AUR update queue before upgrade :feature:maint:solo:
-CLOSED: [2026-07-09 Thu]
-A QUEUE key on the updates strip streams the pending pacman + AUR set onto the results wall, each package with its version move; `maint queue` prints the same in a terminal. The cap reports how many it dropped rather than truncating silently, and a cold cache says it has no pending set instead of rendering "nothing pending". Verified live against ratio's 105 pending packages. (dotfiles 8837a47)
-
-Show which pacman and AUR packages are queued before UPDATE or topgrade runs, so the armed live-update guard isn't the first place the package set becomes visible.
-
-** DONE [#B] Audio PTT state desync across panel, waybar icon, and actual state :bug:audio:solo:
-CLOSED: [2026-07-09 Thu]
-Two state stores and two copies of the logic. The panel flipped an in-memory ptt_armed flag with its own enter/exit rules; the CLI toggle and the bar tag read a runtime state file. The file is now the only state and ptt.toggle_plan the only decision. The panel adopts the persisted state on every refresh, routes its toggle through the shared plan, and persists before signalling waybar (writing after would race the bar into re-rendering the pre-toggle class). Tests drive both entry points against one state across armed/disarmed x muted/live. (dotfiles 9524a44)
-
-The audio panel's push-to-talk, the waybar PTT icon, and the real PTT state disagree. All three must agree at all times. Cover every state transition with tests, not just the happy path — this is the second PTT bug after the pre-talk mute restore landed 2026-07-05 (dotfiles 1443b9e).
-
-** DONE [#B] Publish the homelab inventory into the agent knowledge base :chore:docs:
-CLOSED: [2026-07-09 Thu]
-The four host inventories, the TrueNAS specs asset, and the ratio USB/xHCI record now live as roam nodes in =~/org/roam/hardware/=, linked from the "Homelab Hardware Inventory" index node. =docs/homelab-inventory/= is gone; roam is canonical.
-
-Carried rather than pointed, per home's argument, which beat mine. Pointing keeps one canonical copy but breaks the write path: a third project that discovers a durable hardware fact would have to write into archsetup's repo to record it, which the cross-project rules forbid, so the fact would sit in an inbox instead of on the device's page. Moving answers the canonical-copy worry without that cost.
-
-The =* Automated Capabilities= drawer moved with the pages. =system-health-check.org= now resolves a host's node by its =#+HOSTNAME:= keyword under =${ROAM_DIR:-$HOME/org/roam}/hardware/= — never by filename, since the timestamp prefix isn't stable. Verified: all four hosts resolve, an unknown host degrades to "NO INVENTORY FILE", and ratio's capability drawer still parses. A host without the roam clone (mybitch, truenas) takes the same no-inventory path it always did.
-
-The strix kernel watch stayed at =docs/workflows/strix-soak-watch.org= — it's a workflow, not an inventory page.
-
-** DONE [#C] Optional ticking sound for the pomodoro :feature:timer:
-CLOSED: [2026-07-09 Thu]
-WTIMER_TICK=1 plays a quiet tick each second through a pomodoro's work phase; off by default, silent through rest (a tick there would undo the break), while paused, and while awaiting a manual advance. Rides the bar's existing once-a-second `wtimer render` call. Playback is fire-and-forget so it can never stall or break the bar. Sound is an 18ms 1.4kHz sine at a tenth of full scale (notify/tick.ogg); WTIMER_TICK_SOUND overrides. Craig still has to judge whether the tick sounds right. (dotfiles 886dafd)
-
-Timers module. Off by default; needs a sound choice and a volume decision.
+** TODO [#B] Doctor button for the audio panel :feature:audio:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-09
+:END:
+What happens when pipewire or pulseaudio is broken — would we know? Spec: [[file:docs/specs/2026-07-09-audio-doctor-spec.org][docs/specs/2026-07-09-audio-doctor-spec.org]] — *READY*, all eight decisions closed 2026-07-09, Phase 0 shipped. Buildable: Phase 1 diag, Phase 2 classify, Phase 3 repair + doctor, Phase 4 the DOCTOR key and wall. Grounded in a live survey of ratio's stack: there is no PulseAudio (pipewire + wireplumber + pipewire-pulse, all user-scope, so no sudo anywhere), =pactl= hangs against a server that accepts and never answers, and the panel can't diagnose itself because =pactl= is the layer most likely to be down. Mirrors the net panel's diag/classify/repair/doctor split, not the maint console's priv-verb table.
-** DONE [#C] Refresh indicator for the top-like items :feature:maint:
-CLOSED: [2026-07-09 Thu]
-A hairline under the top-memory section drains toward the next live refresh, so a slow board reads differently from a frozen one. Withheld on a fixture board, which never live-ticks — a countdown that never counts is worse than none. The fraction is a pure viewmodel function; only the cairo stroke lives in the GUI. (dotfiles 5d384d9)
+Re-graded =[#C]= → =[#B]= by the 2026-07-09 audit: the spec is written, Phase 0 shipped, and six decisions are all that stand between this and a build. Active backlog, not parking lot.
-Show when the process/memory rows are about to refresh, so a stale-looking board is distinguishable from a frozen one. Shape is a design call — a countdown, a pulse, or a progress hairline.
+*** 2026-07-09 Thu @ 14:12:00 -0500 Fixed: waybar's audio module died against a half-wedged server
+My first read of this was wrong. =pactl.run()= already carries =timeout=8= and raises =PactlTimeout=, so the panel degrades and does not freeze. Correcting the record rather than leaving the claim standing.
-** DONE [#C] CPU pill should not reposition on selection :bug:maint:quick:solo:
-CLOSED: [2026-07-09 Thu]
-epp_rows dropped the current preference from the key list, so the remaining segments slid sideways on every selection. Every preference now renders in a fixed order with the current one lit and unpressable in place, at full contrast rather than GTK's disabled dimming. The separate lit label is gone — the active segment is the lit one. Three separate tests encoded the old contract (unit, AT-SPI smoke, phase11b card); all moved with the behavior. The smoke caught a real gap the unit tests missed: the EPP row renders through the card path, which ignored the disabled flag. (dotfiles 5d384d9)
+The real bug, found by driving it: =build_status()= guarded its two device-list reads but called =default_sink()= and =default_source()= outside the guard. A server that answered the lists and then stopped answering raised =PactlTimeout= straight out of a function whose whole contract is to degrade to =ok: False=. =waybar-audio= calls it with nothing catching it, so the bar's audio module died instead of dimming; the GTK panel survived only because its background worker catches everything.
-The perf/balance/power pill moves position depending on which option is selected. The segmented control should hold a fixed layout and mark the active segment in place.
+A server that hangs on the *first* call never exposes this — =list_sinks= raises inside the guard and masks the unguarded reads behind it — so the obvious regression test passes against broken code. =fake-pactl= grew =FAKE_PACTL_SLEEP_DEFAULTS= to express "answers the lists, hangs on the defaults". Verified end to end: =waybar-audio= now emits =class: degraded= / "Audio graph unavailable" and exits 0. dotfiles =4d42eb3=; full =make test= green.
-** DONE [#C] Session identifier needs more width :bug:waybar:quick:
-CLOSED: [2026-07-09 Thu]
-Not waybar — the tmux status line. status-left-length was never set, so tmux clipped the drawn status-left to its default of 10 columns: "[aiv-archsetup] " came out "[aiv-archs", losing the closing bracket and the separating space, which is why the window list ran into it. Set to 40. Applied to the live tmux server too. (dotfiles dc3c27f)
-
-The session section truncates; give it more characters, and keep the window sections that follow from running into it. Screenshot: [[file:/home/cjennings/pictures/screenshots/2026-07-07_145236.png][2026-07-07_145236.png]]
+** TODO [#D] Telega coredump recurrence tell :bug:maint:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-09
+:END:
-** TODO [#C] Doctor button for the audio panel :feature:audio:
-What happens when pipewire or pulseaudio is broken — would we know? Needs the probe set, the test for each fault, and the remedy tier before anything gets built. Design first.
+*** 2026-07-09 Thu @ 16:32:54 -0500 Audit reconcile: the fix is holding; re-graded C → D
+Verified rather than assumed: =~/.telega/telega-server.log= carries zero =tdat_plist_value= assertions, and the newest telega-server coredump is 2026-07-08 16:57 — before .emacs.d upgraded the elisp. Nothing has recurred.
-** TODO [#C] Telega coredump recurrence tell :bug:maint:
+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
@@ -1754,3 +1767,51 @@ Installer: install_maintenance_config in user_customizations installs the shippe
*** 2026-07-08 Wed @ 06:18:58 -0500 Flipped the spec to IMPLEMENTED
Spec keyword DOING → IMPLEMENTED with a dated history line naming the closing commits (phases 1-13, dotfiles 43a39ac..9a3f0c7, archsetup d6993d3/bef7053/18c081f) and the Status mirror updated. Residuals tracked as their own tasks: manual-testing checklist, zfs base-image DKMS bug [#C], vNext [#D].
+** DONE [#B] Signal and known-noise counts disagree with the journal :bug:maint:solo:
+CLOSED: [2026-07-09 Thu]
+Not a counting bug — a semantics bug (dotfiles eded0c0). journal_errors grouped by identifier and summed lines, but displayed one sample message, and logs.py overwrote that sample on every iteration so it named the *last* error rather than the counted one. Live on ratio: systemd had 10 error lines this boot — 6 "Failed to start Emacs text editor." and 4 org-roam auto-sync — which is exactly the 10-vs-6. Now grouped by identifier + normalized message (pids, hex addresses, uuids, long integer runs blanked), so a row's count is that message's count, and each row's hint carries a =--grep= reproducing it (verified: 6 and 4). MARK KNOWN binds the volatile-free literal instead of the pid-bearing sample, so marking suppresses exactly what the row counted. The KNOWN NOISE headline counts matched lines while the deck lists patterns; the caption now names both units.
+Logs > SIGNAL reports 10 journal errors and the signal item claims 10, but the group holds one item and the open journal shows it 6 times. KNOWN NOISE reports 19 against 12 visible entries. The counts are the point of the band, so a wrong number undermines the whole diagnostic. Find where the tally diverges from the rendered set — likely counting matched journal lines rather than distinct grouped entries, and counting curated entries against a different window than the one displayed.
+** DONE [#B] Persist the CPU pill setting across reboot :feature:maint:
+CLOSED: [2026-07-09 Thu]
+The kernel resets energy_performance_preference to the driver default every boot. A successful epp_set now records the preference in the maint user config; maint-epp-restore.service replays it at login. No-op when nothing is remembered, the host lacks EPP, or the mode already matches. A hand-edited bogus value is refused by priv's closed verb. Installer enables the unit via the existing wants-symlink idiom. Verified live on ratio: unit enabled, ran clean, EPP untouched. (dotfiles a7f34bd, archsetup 430ef1a)
+
+The power/perf/balance pill sets EPP live, but the value reverts to its original setting after a reboot. Add a way to make a selection permanent. Needs a call on where persistence lives — a systemd unit, a udev rule, or a config file the maint package owns.
+** DONE [#B] Preview the pacman + AUR update queue before upgrade :feature:maint:solo:
+CLOSED: [2026-07-09 Thu]
+A QUEUE key on the updates strip streams the pending pacman + AUR set onto the results wall, each package with its version move; `maint queue` prints the same in a terminal. The cap reports how many it dropped rather than truncating silently, and a cold cache says it has no pending set instead of rendering "nothing pending". Verified live against ratio's 105 pending packages. (dotfiles 8837a47)
+
+Show which pacman and AUR packages are queued before UPDATE or topgrade runs, so the armed live-update guard isn't the first place the package set becomes visible.
+** DONE [#B] Audio PTT state desync across panel, waybar icon, and actual state :bug:audio:solo:
+CLOSED: [2026-07-09 Thu]
+Two state stores and two copies of the logic. The panel flipped an in-memory ptt_armed flag with its own enter/exit rules; the CLI toggle and the bar tag read a runtime state file. The file is now the only state and ptt.toggle_plan the only decision. The panel adopts the persisted state on every refresh, routes its toggle through the shared plan, and persists before signalling waybar (writing after would race the bar into re-rendering the pre-toggle class). Tests drive both entry points against one state across armed/disarmed x muted/live. (dotfiles 9524a44)
+
+The audio panel's push-to-talk, the waybar PTT icon, and the real PTT state disagree. All three must agree at all times. Cover every state transition with tests, not just the happy path — this is the second PTT bug after the pre-talk mute restore landed 2026-07-05 (dotfiles 1443b9e).
+** DONE [#B] Publish the homelab inventory into the agent knowledge base :chore:docs:
+CLOSED: [2026-07-09 Thu]
+The four host inventories, the TrueNAS specs asset, and the ratio USB/xHCI record now live as roam nodes in =~/org/roam/hardware/=, linked from the "Homelab Hardware Inventory" index node. =docs/homelab-inventory/= is gone; roam is canonical.
+
+Carried rather than pointed, per home's argument, which beat mine. Pointing keeps one canonical copy but breaks the write path: a third project that discovers a durable hardware fact would have to write into archsetup's repo to record it, which the cross-project rules forbid, so the fact would sit in an inbox instead of on the device's page. Moving answers the canonical-copy worry without that cost.
+
+The =* Automated Capabilities= drawer moved with the pages. =system-health-check.org= now resolves a host's node by its =#+HOSTNAME:= keyword under =${ROAM_DIR:-$HOME/org/roam}/hardware/= — never by filename, since the timestamp prefix isn't stable. Verified: all four hosts resolve, an unknown host degrades to "NO INVENTORY FILE", and ratio's capability drawer still parses. A host without the roam clone (mybitch, truenas) takes the same no-inventory path it always did.
+
+The strix kernel watch stayed at =docs/workflows/strix-soak-watch.org= — it's a workflow, not an inventory page.
+** DONE [#C] Optional ticking sound for the pomodoro :feature:timer:
+CLOSED: [2026-07-09 Thu]
+WTIMER_TICK=1 plays a quiet tick each second through a pomodoro's work phase; off by default, silent through rest (a tick there would undo the break), while paused, and while awaiting a manual advance. Rides the bar's existing once-a-second `wtimer render` call. Playback is fire-and-forget so it can never stall or break the bar. Sound is an 18ms 1.4kHz sine at a tenth of full scale (notify/tick.ogg); WTIMER_TICK_SOUND overrides. Craig still has to judge whether the tick sounds right. (dotfiles 886dafd)
+
+Timers module. Off by default; needs a sound choice and a volume decision.
+** DONE [#C] Refresh indicator for the top-like items :feature:maint:
+CLOSED: [2026-07-09 Thu]
+A hairline under the top-memory section drains toward the next live refresh, so a slow board reads differently from a frozen one. Withheld on a fixture board, which never live-ticks — a countdown that never counts is worse than none. The fraction is a pure viewmodel function; only the cairo stroke lives in the GUI. (dotfiles 5d384d9)
+
+Show when the process/memory rows are about to refresh, so a stale-looking board is distinguishable from a frozen one. Shape is a design call — a countdown, a pulse, or a progress hairline.
+** DONE [#C] CPU pill should not reposition on selection :bug:maint:quick:solo:
+CLOSED: [2026-07-09 Thu]
+epp_rows dropped the current preference from the key list, so the remaining segments slid sideways on every selection. Every preference now renders in a fixed order with the current one lit and unpressable in place, at full contrast rather than GTK's disabled dimming. The separate lit label is gone — the active segment is the lit one. Three separate tests encoded the old contract (unit, AT-SPI smoke, phase11b card); all moved with the behavior. The smoke caught a real gap the unit tests missed: the EPP row renders through the card path, which ignored the disabled flag. (dotfiles 5d384d9)
+
+The perf/balance/power pill moves position depending on which option is selected. The segmented control should hold a fixed layout and mark the active segment in place.
+** DONE [#C] Session identifier needs more width :bug:waybar:quick:
+CLOSED: [2026-07-09 Thu]
+Not waybar — the tmux status line. status-left-length was never set, so tmux clipped the drawn status-left to its default of 10 columns: "[aiv-archsetup] " came out "[aiv-archs", losing the closing bracket and the separating space, which is why the window list ran into it. Set to 40. Applied to the live tmux server too. (dotfiles dc3c27f)
+
+The session section truncates; give it more characters, and keep the window sections that follow from running into it. Screenshot: [[file:/home/cjennings/pictures/screenshots/2026-07-07_145236.png][2026-07-07_145236.png]]