aboutsummaryrefslogtreecommitdiff
path: root/docs/design/2026-07-02-bluetooth-panel-spec.org
diff options
context:
space:
mode:
Diffstat (limited to 'docs/design/2026-07-02-bluetooth-panel-spec.org')
-rw-r--r--docs/design/2026-07-02-bluetooth-panel-spec.org470
1 files changed, 470 insertions, 0 deletions
diff --git a/docs/design/2026-07-02-bluetooth-panel-spec.org b/docs/design/2026-07-02-bluetooth-panel-spec.org
new file mode 100644
index 0000000..121197a
--- /dev/null
+++ b/docs/design/2026-07-02-bluetooth-panel-spec.org
@@ -0,0 +1,470 @@
+#+TITLE: Bluetooth Panel — CLI-Driven, Net-Panel Kin
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-02
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED Status
+:PROPERTIES:
+:ID: 1271a845-4463-4831-9902-990eda6b2265
+:END:
+- [2026-07-02 Thu] IMPLEMENTED — all five phases shipped the same day
+ (dotfiles eb2230f / 76b2c05 / e372de3 / 2a026b1; archsetup d8d8c53):
+ engine, panel, bar module + blueman retirement, bt-priv + package swap,
+ install wiring proven by VM assertions. 43 dotfiles suites green, both
+ AT-SPI smokes green, panels verified live; the phase 4-5 VM assertions
+ run on the next VM pass.
+- [2026-07-02 Thu] DOING — spec-response decomposed the five phases into
+ build sub-tasks under the todo.org parent (:SPEC_ID: bound); build
+ started same day per Craig ("4 first, then 1" — bugs then bluetooth).
+- [2026-07-02 Thu] READY — spec-review passed the gate: all four
+ decisions resolved, phases decomposable, CLI verbs verified against
+ bluez 5.86. Two non-blocking findings recorded and dispositioned in
+ the same pass (donor-pattern answers).
+- [2026-07-02 Thu] DRAFT — initial spec from Craig's request: a bluetooth
+ module driving a CLI underneath, consistent with the net panel, minimal
+ interface, full functionality, diagnostics section, visual mockups.
+
+* Metadata
+
+| Field | Value |
+|--------+---------------------------------------------------------------------------------|
+| Status | implemented |
+|--------+---------------------------------------------------------------------------------|
+| Owner | Craig Jennings |
+|--------+---------------------------------------------------------------------------------|
+| Repo | dotfiles (bt module); archsetup (packages, sudoers, keybind defaults) |
+|--------+---------------------------------------------------------------------------------|
+| Kin | net panel (architecture donor), desktop-settings panel (same donor, shared css) |
+|--------+---------------------------------------------------------------------------------|
+
+* Problem
+
+Bluetooth on both daily drivers runs through blueman: a tray applet plus a
+GTK3 manager window (Super+Shift+B). It's the odd one out on the desktop —
+a foreign visual style next to the dupre-themed panels, a tray icon where
+every other indicator is a first-class waybar module, and no diagnostics
+story at all. When the BT mouse fails to reconnect at boot (a recurring
+gotcha — touchpad-auto exists because of it) or headphones pair but route
+no audio, the fix is a terminal séance: bluetoothctl, rfkill, systemctl,
+wpctl, in whatever order folklore suggests.
+
+The net panel proved the shape that fixes this: a minimal layer-shell
+popup over a GTK-free engine that drives a CLI, with a diagnostics tab
+that names the failure and offers the repair. Bluetooth is the same
+problem with a smaller surface: one adapter, a handful of devices, a
+short list of well-known failure modes.
+
+* Goals
+
+1. Visibility: adapter power state and every known device with live state
+ (connected, battery, signal) in one glance — panel and bar module agree.
+2. Control: power, scan, pair, connect, disconnect, forget — full
+ functionality from the panel, zero terminals (the net panel's V2
+ contract).
+3. Diagnostics: a doctor that walks the known failure chain (adapter →
+ rfkill → service → power → device → audio profile), names the broken
+ link in evidence rows, and offers tiered repairs.
+4. Consistency: same stack, same window shape, same interaction grammar,
+ same palette as the net panel. A user who knows one panel knows both.
+
+Audio-profile switching is in scope for v1 (Craig, 2026-07-02 — "bitten
+by this too many times to count"): the doctor's audio-profile step
+carries a one-click repair, not just a diagnosis, and connected audio
+devices surface their active profile (details in the doctor chain below).
+
+Non-goals (this iteration): OBEX file transfer, multi-adapter support
+(both machines have one controller), BLE sensor/GATT browsing.
+
+* Design sketch
+
+** Architecture — the net panel's stack, verbatim
+
+- GTK4 + gtk4-layer-shell, Blueprint .blp compiled to committed .ui
+ (=make ui=), PyGObject at runtime.
+- Humble-object split: GTK-free =PanelModel= presenter (unit-tested like
+ net's), thin composite-widget pages, =bg(work, done)= worker-thread
+ helper for every slow call.
+- Engine: a new =bt= package in dotfiles (=bluetooth/src/bt/=, sibling of
+ =net/=), CLI entry =bt= with =bt status= / =bt panel= / =bt doctor= —
+ the same cmd/cli layout as net.
+- Layer-shell OVERLAY popup anchored TOP+RIGHT, 380x520, Esc closes,
+ focus-out auto-hides, single-instance toggle via a =bt-panel= wrapper.
+ Dupre palette css shared with the net panel (the factored css asset the
+ desktop-settings spec calls for — three consumers now, so the factoring
+ happens in this project's phase 1 if settings hasn't landed it).
+- Testing: engine TDD with fake binaries on a temp PATH (fake-bluetoothctl,
+ fake-rfkill, fake-systemctl, fake-wpctl); PanelModel unit suite; one
+ gated AT-SPI smoke (=make test-panel= pattern).
+
+** CLI backing — bluetoothctl one-shot verbs
+
+bluez 5.86 (installed) supports everything non-interactive:
+
+- Adapter: =bluetoothctl show= (powered, discoverable, pairable),
+ =bluetoothctl power on|off=.
+- Device lists: =bluetoothctl devices Paired|Connected|Trusted= — the
+ Paired view is a merge of Paired + Connected states; =bluetoothctl info
+ <mac>= per row fills caption detail (battery percentage rides bluez's
+ built-in Battery1 profile and appears in info output; RSSI appears
+ during discovery).
+- Scan: =bluetoothctl --timeout N scan on= (bounded discovery burst),
+ then =devices= diffed against Paired for the Nearby list. The panel
+ scans in 8s bursts with a live "Scanning…" state rather than an
+ unbounded scan.
+- Connect/disconnect/forget: =bluetoothctl connect|disconnect|remove <mac>=.
+- Pairing: the one interactive corner. =bluetoothctl pair <mac>= can demand
+ a passkey confirmation. The engine drives bluetoothctl's line protocol
+ over a pty with a bounded state machine (expect "Confirm passkey",
+ reply yes/no); a passkey prompt surfaces as a panel dialog showing the
+ six digits, mirroring the net panel's password dialog. NoInputNoOutput
+ devices (mice, most headphones) sail through without the dialog.
+- rfkill: the user is in the =rfkill= group, so block/unblock is
+ unprivileged (=rfkill unblock bluetooth=).
+- Privileged path: exactly one verb needs root — =systemctl restart
+ bluetooth= — so =bt-priv= is a one-verb closed helper with its own
+ NOPASSWD sudoers rule placed by archsetup, cloning net-priv's
+ regex-validated pattern rather than widening net-priv's scope.
+
+** Panel anatomy
+
+Two tabs. Devices is the panel; Diagnostics is the escape hatch.
+
+Devices tab, Paired sub-view (the default — daily use is reconnecting
+known devices, not discovering new ones):
+
+#+begin_example
+╭──────────────────────────────────────────────╮
+│ [ Devices ] [ Diagnostics ] │ ← top switcher
+│ │
+│ Bluetooth ●──○ hci0 on │ ← adapter row: power switch
+│ ──────────────────────────────────────────── │
+│ [ Paired ] [ Nearby ] │ ← sub-view switcher
+│ ┌──────────────────────────────────────────┐ │
+│ │ 󰍽 MX Master 3 │ │
+│ │ Connected · battery 80% │ │
+│ │ 󰋋 WH-1000XM4 │ │
+│ │ Paired, not connected │ │
+│ │ 󰌌 K380 Keyboard │ │
+│ │ Paired, not connected │ │
+│ │ │ │
+│ └──────────────────────────────────────────┘ │
+│ [ Disconnect ] [ Forget ] │ ← acts on selected row
+╰──────────────────────────────────────────────╯
+#+end_example
+
+The primary button is one control with a state-following label:
+"Connect" when the selection is disconnected (suggested-action styling),
+"Disconnect" when connected. Row-activate (Enter / double-click)
+connects — never disconnects — matching the net panel's asymmetry.
+Captions carry the human state line; the MAC lives in the row tooltip,
+not the visible caption.
+
+Devices tab, Nearby sub-view:
+
+#+begin_example
+╭──────────────────────────────────────────────╮
+│ [ Devices ] [ Diagnostics ] │
+│ │
+│ Bluetooth ●──○ hci0 on │
+│ ──────────────────────────────────────────── │
+│ [ Paired ] [ Nearby ] │
+│ ┌──────────────────────────────────────────┐ │
+│ │ Scanning… (6s) │ │ ← overlay state label
+│ │ 󰋋 JBL Flip 6 −58 dBm │ │
+│ │ 󰄜 Pixel 9 −71 dBm │ │
+│ │ 󰂱 (unnamed) 74:A5:… −83 dBm │ │
+│ └──────────────────────────────────────────┘ │
+│ [ Pair ] [ Rescan ] [ Discoverable ⊙ ] │
+╰──────────────────────────────────────────────╯
+#+end_example
+
+Pair does the whole intended thing — pair, then trust, then connect —
+because pairing a device means "use it now and reconnect on its own
+later" (decision below). Discoverable is a toggle for the inbound case
+(pairing a phone TO the laptop), off by default, auto-off with bluez's
+discoverable-timeout. Rows sort by RSSI, strongest first; named devices
+above unnamed ones.
+
+Diagnostics tab (mirrors the net panel's shape: one big verb + streaming
+evidence rows + tiered repairs behind confirmation):
+
+#+begin_example
+╭──────────────────────────────────────────────╮
+│ [ Devices ] [ Diagnostics ] │
+│ │
+│ [ Get Bluetooth Working ] [ Advanced ▸]│
+│ ┌──────────────────────────────────────────┐ │
+│ │ ✓ Adapter present (hci0) │ │
+│ │ ✓ Not blocked (rfkill clear) │ │
+│ │ ✓ bluetooth.service active │ │
+│ │ ✓ Adapter powered │ │
+│ │ ✗ MX Master 3: paired but unreachable │ │
+│ │ … Re-pair suggested — see below │ │
+│ │ │ │
+│ │ Fix: [ Reconnect ] [ Re-pair device ] │ │
+│ └──────────────────────────────────────────┘ │
+│ power-cycle · restart service · unblock │ ← tiered repairs (confirm)
+╰──────────────────────────────────────────────╯
+#+end_example
+
+The doctor chain, in order, each an evidence row:
+
+1. Adapter present — =bluetoothctl list= / rfkill has an hci entry.
+ Absent → hardware/driver verdict, no repair offered.
+2. rfkill state — soft-blocked names the likely cause when the
+ airplane-mode state file says airplane is on ("Blocked by airplane
+ mode — turn airplane mode off"), otherwise offers Unblock (no root
+ needed, rfkill group).
+3. bluetooth.service — inactive/failed → offer restart (the one bt-priv
+ verb), evidence quotes the last journal line.
+4. Adapter powered — off → offer power on (and note if a boot-time
+ policy keeps turning it off).
+5. Per-device reachability — paired-but-connect-fails distinguishes
+ "device off/out of range" (RSSI absent in a scan burst) from "bond
+ corrupt" (connect error string), and only the latter suggests the
+ re-pair repair (remove + pair + trust + connect, confirmed first —
+ it's the destructive tier).
+6. Audio profile (audio devices only) — device connected but no wpctl
+ sink/source, or the card stuck in HSP/HFP when A2DP is expected:
+ evidence names the active profile and offers the repair inline —
+ "Switch to A2DP" drives =wpctl set-profile <card> <index>= (profile
+ inventory from =pw-dump= — ground truth 2026-07-02: wpctl can't
+ enumerate a card's profiles, and the card's =bluez5.profile= prop
+ reads "off" mid-stream; the card's Profile param and the sink node's
+ =api.bluez5.profile= are authoritative), verifies the sink came back
+ in the expected profile, and reports fixed or no-change. In v1 per
+ Craig (2026-07-02): this failure mode has bitten repeatedly, so it
+ gets the one-click fix, not just a diagnosis. Connected audio-device
+ row captions also show the profile when it's the degraded one
+ ("Connected · mic mode (HSP)") so the state is visible before the
+ doctor runs.
+
+Repairs confirm with the net panel's future-tense scope copy ("This will
+restart the Bluetooth service. Connected devices will drop and
+reconnect."), run on the worker thread, verify after (re-read state,
+report "fixed" or "no change"), and never chain silently.
+
+** Bar module
+
+=custom/bluetooth= replacing the blueman-applet tray icon: the panel's
+glanceable layer, one glyph, state-following like =custom/net=:
+
+#+begin_example
+ 󰂲 off / blocked (dim; red slash variant when rfkill-blocked)
+ 󰂯 on, nothing connected (dim)
+ 󰂱 connected (white; tooltip lists devices + battery)
+#+end_example
+
+Tooltip carries device names, battery percentages, and the keybind hints
+(the module-tooltip convention shipped 2026-07-02). Click opens the
+panel (=bt-panel= toggle wrapper); the existing Super+Shift+B bind moves
+from blueman-manager to =bt panel=. Low-battery on a connected device
+(<15%) adds a red percentage to the glyph text — the mouse dying
+mid-meeting is the one state worth surfacing unprompted.
+
+** UX conformance notes
+
+Named against the heuristics the panel family follows (Nielsen's ten,
+plus the rulesets patterns catalog):
+
+- Visibility of status: live captions, scan countdown, elapsed ticker on
+ long ops, verify-after-repair rows.
+- Match to the real world: device-kind glyphs + plain state lines; MACs
+ demoted to tooltips; "Forget" not "Remove bond".
+- User control: Esc closes, Rescan is idempotent, scan bursts are
+ bounded, repairs confirm, running ops show a Stop where stoppable.
+- Consistency: interaction grammar is the net panel's — same switcher
+ layout, same primary-button contract, same confirm copy shape.
+- Error prevention: Forget and Re-pair confirm; power-off while devices
+ are connected states the consequence in the confirm body.
+- Recognition over recall: every action is a visible button; no context
+ menus, no hidden gestures (transient-state-buttons pattern).
+- Minimalism: two tabs, one primary action per view, detail behind
+ tooltips and the Advanced reveal.
+- Help users recover: the doctor's evidence rows name the broken link
+ and carry the repair inline (default-most-common-friction-proportional:
+ the likely fix is one click, the destructive one is confirmed).
+
+Tension found with the net panel while writing this (filed as todo.org
+tasks per Craig's instruction, 2026-07-02): transient error toasts
+auto-dismiss in 4s, and the V2 spec's keyboard-navigation claims
+(tab-between-sections, arrow rows, type-to-filter) aren't verifiably
+implemented. Both filed against the net panel rather than cloned here;
+this panel adopts whatever resolution those tasks land on.
+
+* Decisions (Craig) [4/4]
+
+** DONE Pair implies trust + connect?
+CLOSED: [2026-07-02 Thu]
+Decided (Craig, 2026-07-02): yes — one Pair verb does pair → trust →
+connect. A device that shouldn't auto-reconnect gets untrusted later; a
+per-device auto-reconnect toggle can ride a later pass.
+
+** DONE Retire blueman entirely?
+CLOSED: [2026-07-02 Thu]
+Decided (Craig, 2026-07-02): drop it outright, no bake-in period — the
+package leaves archsetup and both machines once phase 2 lands,
+bluetoothctl stays as the terminal fallback. Craig's framing: any issue
+after retirement is a signal the doctor needs another check or the panel
+has a real bug, and it gets fixed there rather than papered over by
+keeping blueman around.
+
+** DONE Battery in the row caption or tooltip only?
+CLOSED: [2026-07-02 Thu]
+Approved (Craig, 2026-07-02): caption when the device reports it
+("Connected · battery 80%"), tooltip otherwise.
+
+** DONE Scan burst length and auto-rescan?
+CLOSED: [2026-07-02 Thu]
+Approved (Craig, 2026-07-02): 8s bursts, no auto-repeat — Rescan stays
+explicit, matching the net panel's Available view.
+
+* Review findings [2/2]
+
+** DONE Empty-state and no-adapter presentation copy undefined :nonblocking:
+CLOSED: [2026-07-02 Thu]
+The mockups show populated lists; the spec didn't say what an empty Paired
+list, an empty post-scan Nearby list, or a machine with no adapter shows
+in the panel and on the bar glyph. Dispositioned same pass: clone the
+donor — the net panel's in-box overlay message pattern (=show_loading= /
+placeholder label) carries the copy. Paired empty: "No paired devices —
+switch to Nearby to pair one." Nearby post-scan empty: "Nothing found —
+Rescan, or make the device discoverable." No adapter: adapter row reads
+"No Bluetooth adapter", Devices controls disable, Diagnostics stays
+usable (the doctor's step 1 names the hardware/driver verdict); bar
+glyph shows the off/blocked state. Non-blocking; recorded so the
+implementer doesn't invent copy mid-build.
+
+** DONE Logging/redaction carry-over unstated :nonblocking:
+CLOSED: [2026-07-02 Thu]
+The spec says "the net panel's stack, verbatim" but didn't name whether
+the engine adopts net's =eventlog= (structured op log) and =redact=
+(sensitive-field scrubbing) modules. Dispositioned same pass: yes, both
+carry over — every mutating verb (pair/connect/forget/repair) logs an
+eventlog entry, and MACs are the redaction surface (device names stay,
+MACs redact in copied reports, mirroring net's report redaction).
+Non-blocking; it's the donor default made explicit.
+
+* Implementation phases
+
+1. Engine =bt= package: adapter/device/scan probes over fake-bluetoothctl,
+ status + doctor chain (rfkill, service, powered, reachability, audio
+ profile probe + A2DP switch repair over fake-wpctl) — pure TDD, no
+ GTK. =bt status= and =bt doctor= work from a terminal. Shared dupre
+ css factored to the common asset if the settings panel hasn't already
+ done it.
+2. Panel: PanelModel presenter + Blueprint pages (Devices with
+ Paired/Nearby, Diagnostics), worker-thread wiring, pairing-dialog
+ state machine, bt-panel toggle wrapper, AT-SPI smoke. Super+Shift+B
+ rebind.
+3. Bar module =custom/bluetooth= (glyph states, tooltip, low-battery
+ surface, refresh signal), waybar config + suite coverage; blueman
+ retirement per the decision.
+4. bt-priv one-verb helper + sudoers rule in archsetup; package-list
+ swap (blueman out per decision, bluez-utils stays); VM test
+ assertions.
+5. archsetup keybind/config defaults so a fresh install lands the panel
+ wired (waybar module present, bind set, sudoers placed).
+
+* Review and iteration history
+
+** 2026-07-02 Thu @ 15:19:58 -0400 — Claude Code (archsetup) — phase 5 builder, spec closed
+- *What changed or was recommended:* Phase 5 shipped and the spec flipped
+ to IMPLEMENTED. No new install code was needed — the waybar module, the
+ =Super+Shift+B= bind, and the shared panel css all ride the dotfiles
+ hyprland tier that a fresh install already clones and stows, and sudoers
+ is covered by the blanket grant. The phase's substance is proof:
+ =test_desktop.py= gained hyprland-gated assertions for the four stowed
+ bt bins, the =custom/bluetooth= waybar entry, the =bt-panel= keybind,
+ and the stowed =panel.css=.
+- *Why:* Final phase of the DOING decomposition; with it the todo parent
+ closed and the lifecycle keyword flipped with a history line.
+- *Artifacts:* archsetup =scripts/testing/tests/test_desktop.py=; todo.org
+ parent DONE + dated phase 5 / test-surface entries; this spec's Status
+ heading.
+
+** 2026-07-02 Thu @ 15:16:51 -0400 — Claude Code (archsetup) — phase 4 builder
+- *What changed or was recommended:* Phase 4 shipped. Dotfiles =2a026b1=:
+ the stowed =bt-priv= shim (one verb, verified against the fake-systemctl)
+ and the sxhkd =Super+Shift+B= bind repointed from blueman-manager to
+ =st -e bluetoothctl= (terminal fallback per the retirement decision — the
+ panel is Wayland-only). archsetup: blueman dropped from the
+ =desktop_environment= package loop; VM assertions added (bluez/bluez-utils
+ present, blueman absent). blueman also removed live from velox.
+- *Why:* Build order per the DOING decomposition. The spec's "sudoers rule"
+ item resolved as net-priv's did: archsetup already grants the primary
+ user blanket =NOPASSWD: ALL= (archsetup:1089), so a narrow bt-priv rule
+ would be dead config — no new sudoers needed, and phase 5's "sudoers
+ placed" is satisfied by the existing grant.
+- *Artifacts:* dotfiles =hyprland/.local/bin/bt-priv=,
+ =common/.config/sxhkd/sxhkdrc=; archsetup =archsetup= (bluetooth loop),
+ =scripts/testing/tests/test_packages.py=; dated phase 4 entry under the
+ todo.org parent.
+
+** 2026-07-02 Thu @ 15:06:00 -0400 — Claude Code (archsetup) — phase 3 builder
+- *What changed or was recommended:* Phase 3 shipped (dotfiles =e372de3=):
+ the =custom/bluetooth= bar module (state-following glyph, low-battery red
+ percentage, device+battery tooltip with the keybind hint, signal 10 with
+ the panel poking it after each reload) and the blueman retirement from the
+ Hyprland session (exec-once + windowrules removed, applet killed live).
+ The phase 2 deferred items also closed this pass: both AT-SPI smokes green
+ (the bt smoke's primary-button assertion fixed for the state-following
+ label, =c1a8219=), both panels eyeballed correct in dupre, and the
+ net-panel keyboard claims verified live (archsetup =e80df2b= — false
+ claims struck from the net spec).
+- *Why:* Build order per the DOING decomposition; the Zoom meeting ended,
+ unblocking the visual work. Phases 4-5 (bt-priv/sudoers/packages, install
+ defaults — archsetup side) remain.
+- *Artifacts:* dotfiles =bluetooth/src/bt/indicator.py=, =waybar-bt=,
+ waybar config + three css files; dated phase 3 entry under the todo.org
+ parent.
+
+** 2026-07-02 Thu @ 14:15:27 -0400 — Claude Code (archsetup) — phase 2 builder
+- *What changed or was recommended:* Phase 2 shipped (dotfiles =76b2c05=):
+ the GTK panel — PanelModel/viewmodel presenter pair (69 tests), Blueprint
+ pages, pairing pty state machine with default-deny passkey confirms,
+ manage.py op envelopes shared by CLI and panel (power + discoverable verbs
+ added), =bt-panel= toggle, Super+Shift+B rebind. The shared dupre css
+ factoring landed as planned: net's inline =_CSS= became
+ =themes/dupre/panel.css= with =dupre-*= classes, both panels consume it.
+ 43 suites green. The AT-SPI smoke (=make test-panel-bt=) is written but
+ not yet run live — a Zoom meeting occupied the compositor; it runs when
+ the meeting ends, along with a visual check of both panels.
+- *Why:* Build order per the DOING decomposition; phases 3-5 (bar module,
+ bt-priv/sudoers, install defaults) remain.
+- *Artifacts:* dotfiles =bluetooth/src/bt/{panel,viewmodel,pairing,manage,
+ gui,pages}.py=, =ui/*.blp=, =tests/bt/test_btpanel.py=, the panel smoke;
+ dated phase 2 entry under the todo.org parent.
+
+** 2026-07-02 Thu @ 13:31:00 -0400 — Claude Code (archsetup) — phase 1 builder
+- *What changed or was recommended:* Phase 1 shipped (dotfiles =eb2230f=):
+ the =bt= engine package, 101 tests over fakes, live-verified read-only
+ on velox. Two spec corrections from ground truth: profile inventory
+ comes from =pw-dump= (wpctl can't enumerate profiles), and the active
+ profile reads from the card's Profile param / sink's
+ =api.bluez5.profile= (the card's =bluez5.profile= prop is unreliable).
+ The shared-css factoring moved into phase 2 — net's css is an inline
+ string in its =gui.py=, so extracting it belongs with the first second
+ consumer rather than as a standalone poke at the working net panel.
+- *Why:* Build order per the DOING decomposition; corrections keep the
+ spec honest for the phase 2 implementer.
+- *Artifacts:* dotfiles =bluetooth/src/bt/=, =tests/bt/=, the stowed
+ =bt= shim; dated phase 1 entry under the todo.org parent.
+
+** 2026-07-02 Thu @ 13:10:00 -0400 — Claude Code (archsetup) — reviewer + responder
+- *What changed or was recommended:* Ran the spec-review gate: passed.
+ All four decisions were already DONE (cookie added to the heading);
+ the five phases are each a clean single-session stop; CLI verbs are
+ verified against installed bluez 5.86. Two non-blocking findings
+ recorded and dispositioned in the same fused pass (empty-state /
+ no-adapter copy, eventlog + redaction carry-over) — both resolve to
+ "clone the net-panel donor," now stated explicitly. Flipped DRAFT →
+ READY → DOING and decomposed the phases into build sub-tasks under the
+ todo.org parent with :SPEC_ID: bound.
+- *Why:* Craig queued the build ("4 first, then 1", 2026-07-02) after
+ resolving all decisions the same morning; the gate held nothing back,
+ so review and response fused to keep the speedrun moving.
+- *Artifacts:* Findings in =* Review findings [2/2]= above; build parent
+ in todo.org ("Bluetooth panel + bar module"); net-panel toast fix the
+ UX-conformance note references landed as dotfiles =0f017d4=.