Skip to content

feat: microphone domain — list/inspect/gain/array/param/stream/record (0.9.0) - #6

Merged
OriNachum merged 50 commits into
mainfrom
spec/microphone-domain
Sep 6, 2026
Merged

feat: microphone domain — list/inspect/gain/array/param/stream/record (0.9.0)#6
OriNachum merged 50 commits into
mainfrom
spec/microphone-domain

Conversation

@OriNachum

Copy link
Copy Markdown
Contributor

What

The microphone domain, built from a converged devague spec and plan and accepted on real hardware.

  • 13 top-level verbs: list, inspect, gain get|set, array doa (single-shot or --watch JSON Lines) and array aec get|set, param list|get|set over the full XVF3800 table with a persistent tier behind --allow-persistent, stream audio (RTP/UDP passthrough or Opus), record (bounded WAV/Matroska), plus the agent-first baseline.
  • Seven zero-dependency modules: devices (stable ids from USB serial), access (ok/absent/forbidden/busy → exit 0/1/2/3), usbctl (stdlib usbdevfs control transfers, no pyusb), xvf3800 (vendored parameter table with per-firmware overlays, typed codec, status-64 retry), mixer (amixer), engine (GStreamer argv), activation (append-only log of every --apply).
  • Dry-run by default; --probe checks without opening; --apply touches hardware and is logged. prog is now microphone, matching the console script; template prose is gone and a parity test keeps catalog/learn/overview in sync with the parser.
  • Version 0.8.2 → 0.9.0. 286 tests, 92% coverage, teken cli doctor --strict 26/26, workflows untouched, dependencies = [].

How it was built

/scope/think/challenge/spec-to-plan/assign-to-workforce: 13 tasks in 5 waves, one agent per task in isolated worktrees, TDD-gated merges. Artifacts: docs/specs/2026-09-06-microphone-domain.md, docs/plans/2026-09-06-microphone-domain.md (+ split), .devague/.

Two approved plan deviations: d1 acceptance ran on a Seeed ReSpeaker XVF3800 (2886:001a) instead of the Reachy Mini Lite the plan named (the robot on hand was a Reachy Mini whose array lives on its own Pi); d2 the parameter map turned out to be firmware-specific, so xvf3800.FIRMWARE_OVERLAYS was added.

On-device acceptance

scripts/acceptance/run.sh --writes --media passed 10/10 on the array (Seeed USB firmware 2.1.0): DoA matched Seeed's own reference reader exactly, all volatile writes round-tripped and were restored, record produced a real 16 kHz stereo WAV, and a blind consumer attached to the stream from the JSON payload alone. Evidence and the seven hardware-only defects it surfaced (all fixed here) are in docs/acceptance-microphone-domain.md.

Follow-ups

🤖 Generated with Claude Code

https://claude.ai/code/session_01Jei6EarsNVUDHMNxb1U1Za

OriNachum and others added 30 commits September 6, 2026 21:19
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jei6EarsNVUDHMNxb1U1Za
Add EXIT_BUSY_ERROR=3 to cli/_errors.py and a new microphone_cli/access.py
(cited from webcam-cli webcam_cli/access.py) with AccessState
{ok,absent,forbidden,busy}, check_access/require_access, a best-effort
/proc/*/fd holder lookup, and kind-specific remediations: 'audio' group
membership for /dev/snd nodes, a udev SUBSYSTEM=="usb" rule for
/dev/bus/usb nodes. tests/test_access.py covers all four states against a
fake /proc tree, hardware-free (os.open is monkeypatched throughout; no
real /dev or /proc access).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jei6EarsNVUDHMNxb1U1Za
Cites webcam-cli/webcam_cli/activation.py:26-129, renamed to
MICROPHONE_ACTIVATION_LOG and adapted to microphone-cli's field set
(verb, device, params, started_at, ended_at). Appends one JSON line
per --apply action to XDG_STATE_HOME/microphone-cli/activation.jsonl
(or the env override), never overwriting prior lines.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jei6EarsNVUDHMNxb1U1Za
Ports the audio subset of webcam-cli's engine.py (Capability/AudioFormat,
detect(), require_engine()) for microphone-cli's own element set —
alsasrc/audioconvert/audioresample/queue core, opusenc/matroskamux/wavenc/
rtpopuspay/rtpL16pay/udpsink optional — plus require_elements() and two
pure argv builders: build_audio_stream_argv() (RTP/UDP passthrough or
opus) and build_audio_record_argv() (Matroska/Opus or WAV, with an
alsasrc num-buffers+latency-time bound for duration_s). No PyGObject/gi
import; detect() is the only function that shells out.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jei6EarsNVUDHMNxb1U1Za
Add microphone_cli/devices.py: enumerate USB audio capture cards from
/proc/asound/cards plus the sysfs USB parent walk, build stable ids from
the USB descriptors (udev's escaping rules; sysfs-path fallback when the
firmware ships no serial), flag XVF3800 arrays by idVendor:idProduct
(38fb:1001, 2886:001a), report the capture channel count from
/proc/asound/cardN/stream0, and resolve() a selector to exactly one
device — refusing raw hw:N / plughw:N / bare-integer card numbers and
refusing ambiguity, both as CliError(EXIT_USER_ERROR) naming the stable
ids to use instead.

Cited from webcam_cli/devices.py with the video half dropped; provenance
is in the module docstring.

Four synthetic fixture trees under tests/fixtures (text files and
relative symlinks only, no real captures): host-baseline,
host-renumbered, two-arrays, respeaker.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds the hardware-free protocol core for microphone-cli:

* microphone_cli/usbctl.py — ctypes mirror of struct usbdevfs_ctrltransfer,
  USBDEVFS_CONTROL derived as _IOWR('U', 0, sizeof(struct)) (0xC0185500 on
  64-bit), control_transfer() over fcntl.ioctl behind a module-level _ioctl
  seam, sysfs device enumeration to /dev/bus/usb/BBB/DDD node paths, and
  open_device() mapping EACCES/ENOENT onto CliError with a udev remediation.
* microphone_cli/xvf3800.py — the vendor protocol ported from reachy_mini's
  media/audio_control_utils.py (Apache-2.0, cited in the module docstring),
  with the PARAMETERS table vendored verbatim, typed pack/unpack, the
  status-64 retry loop, PERSISTENT/KNOWN_IDS, and param_info().

Tests inject a fake ioctl; no test opens /dev or touches real hardware.
Runtime dependencies stay empty — stdlib only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- list_devices.py: enumerate_devices() + per-device audio_access probe
  via access.check_access on the ALSA capture PCM node
- inspect.py: parses <root>/proc/asound/cardN/stream0 for formats/rates/
  channels; on an XVF3800 array reads firmware.{version,build,host,
  repo_hash} via usbctl.find_devices + Xvf3800.firmware_info(), reporting
  {"error": ...} rather than hard-failing on a lookup or permission problem
- Cites ../webcam-cli/webcam_cli/cli/_commands/list_devices.py for the
  "compose, never hard-fail" shape

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jei6EarsNVUDHMNxb1U1Za
`array` noun group: bare `array` and `array overview` describe the group;
`array doa <device>` reads DOA_VALUE_RADIANS as raw firmware radians (no
coordinate transform), single-shot or `--watch` JSON Lines with
`--interval`/`--count`, Ctrl-C exits 0 and a lost device exits 2; `array aec
get|set` reports converged/bypass/hpf/echo/num_mics/geometry and flips
--echo/--bypass/--hpf, dry-run unless --apply (which writes inside an
activation_scope).

Every nested add_subparsers passes parser_class=type(p) so parse errors keep
the structured error contract. Hardware is reached only through the
module-level _open_array / _sleep seams; tests serve every transfer from an
in-memory fake and never open a device node.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds microphone_cli/cli/_commands/param.py: `param overview`, `param list`,
`param get <device> <NAME>`, `param set <device> <NAME> <values...>
[--apply] [--allow-persistent]`. Name/access/count validation (via
xvf3800.param_info and a local per-type value parser) happens before any
device is opened, so an unknown name, a ro name, or a wrong value count
never issues a transfer, whether or not --apply is passed. The persistent
tier (xvf3800.PERSISTENT, already added by t3) is checked before opening the
device or writing when --apply is set: a persistent/destructive name
without --allow-persistent is refused with a hint naming the flag and
explaining the volatile-vs-persistent distinction. Applied writes run
inside activation_scope("param set", ...) and include a readback when the
parameter is also readable (rw).

Adds tests/test_param.py, which builds a standalone parser via
param.register() (the noun isn't wired into cli/__init__.py yet — that's a
separate task) and monkeypatches param.resolve / param._open_array so no
test opens /dev.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jei6EarsNVUDHMNxb1U1Za
Adds microphone_cli/mixer.py (amixer -c <card> contents/cset wrapper,
hardware-free via a run= seam) and microphone_cli/cli/_commands/gain.py
exposing `gain overview|get|set`. `gain get` reports the ALSA
capture-volume control and, on array devices, AUDIO_MGR_MIC_GAIN read
over the XVF3800 vendor protocol (firmware read failures degrade to
firmware: {error} rather than failing the whole command). `gain set`
is a dry run by default (plans the amixer argv/firmware write without
issuing them) and only applies with --apply, wrapped in exactly one
activation_scope("gain set", ...) log line.

Not yet wired into cli/__init__.py (a separate task owns that file);
tests/test_gain.py drives gain.register() through a standalone
_CliArgumentParser instead of microphone_cli.cli.main.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jei6EarsNVUDHMNxb1U1Za
Two capture verbs, both cited (audio paths only) from webcam-cli's
stream.py/record.py and both keeping the three-level hardware split:

* `stream audio <device>` — RTP/UDP attachment point via udpsink, with
  --port/--host/--rate/--channels/--format/--encode. Default is a dry run
  that prints the exact gst-launch-1.0 argv and touches nothing; --probe
  detects the engine and checks the capture node without spawning; --apply
  requires the engine and the encode's elements, enforces access (busy is
  exit 3), spawns through the module-level `_spawn` seam inside an
  activation_scope, and returns the pid plus per-codec receive pipelines.
* `record <device> <output>` — bounded capture to .mka/.wav (container from
  the extension), with --duration (default 30, 0 < d <= 3600) and
  --max-bytes (default 256 MiB, <= 4 GiB) enforced both by the argv's own
  num-buffers bound and by a poll loop over the growing artifact through
  the `_sleep`/`_monotonic` seams; stopped_reason reports which bound won.
  Refuses a missing parent dir, an unknown extension, and an existing file
  without --overwrite.

Payload key names follow the cited modules, minus the video-only ones.
Tests are hardware-free by construction: subprocess.Popen/run are
booby-trapped autouse, devices resolve under tests/fixtures/host-baseline,
the capture node is root-joined so it is absent under a fixture root, and
the activation log is redirected into tmp_path.

Not wired into cli/__init__.py, learn/overview/catalog — those files belong
to the wiring task; the tests build the same parser shape by hand.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jei6EarsNVUDHMNxb1U1Za
Register every wave-0/1 command module in `_build_parser` (list, inspect,
gain, array, param, stream, record) alongside the introspection verbs, and
set `prog="microphone"` to match `[project.scripts]` — the binary is
`microphone`, so nothing the CLI prints should tell an agent to type
`microphone-cli`.

Rewrite the three hand-maintained duplicates of the surface for the
microphone domain:

* `learn.py` — purpose, the three-level hardware-touch split first
  (dry-run / --probe / --apply) plus the read-only vendor control transfers
  that still open the USB node, the full command map, the stable-id selector
  rule, exit codes 0/1/2/3, the persistent tier, raw firmware radians,
  consent and the activation-log pointer, and an audience section naming
  media-cli as the composing consumer. `_as_json_payload` gains `command`,
  `import_package`, `audience`, `hardware_activation`, `device_selector`,
  `bounds`, `persistent_tier`, `raw_firmware_values`, `consent`.
* `overview.py` — `_VERBS` covering all 29 registered paths, plus
  `_HARDWARE`, `_CONTRACTS` and a consent section; `cli_sections` now reuses
  `_VERBS` verbatim instead of re-declaring it.
* `explain/catalog.py` — an entry per registered path, with
  `("microphone-cli",)` kept as a resolvable legacy alias.

Ported from webcam-cli's tests/test_cli.py:190-327: `_registered_paths()`
walks the live argparse tree, and the parity tests require the catalog,
`overview._VERBS` and learn's command map to agree with it. Two prose gates
join them — no user-facing string may present `microphone-cli` as a typable
command, and none may carry the scaffold's clonable/template wording.

`doctor`'s checks and JSON shape are untouched; only its text header and
docstring changed. Three wave-1 modules needed their overview *subject*
strings retitled (`microphone-cli array` -> `microphone array`, same for
`param` and `stream`) so the dead-command gate can pass; their flags and
payload keys are unchanged, and the affected assertions in
tests/test_array.py, test_param.py and test_stream.py moved with them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
README.md and CLAUDE.md replace the scaffold-state narrative from 5f9b1bd
with the domain state: 13 top-level verbs (list, inspect, gain get/set,
array doa, array aec get/set, param list/get/set, stream audio, record,
plus the six agent-first verbs), 276 tests, 92% coverage. README follows
webcam-cli's heading shape (Status, Scope, Quickstart, CLI table, What
comes out, What touches the hardware, Why device identity is the hard
part, What this repo carries, License). CLAUDE.md gets a module map, the
three-level hardware split, the testing seams (root=, _open_array, _ioctl,
_spawn, _sleep, run=), the fixture trees, the parity tests, and the
issue #3 hardware-acceptance status; the console-script note is marked
RESOLVED now that prog and the script both read `microphone`.

Adds docs/xvf3800-parameters.md: attribution for the vendored XVF3800
parameter table (Pollen Robotics' reachy_mini, Apache-2.0) and a guide to
its resid groups and persistent tier. docs/skill-sources.md gains rows for
the recall/remember skills (first-party to eidetic-cli, not guildmaster),
which were vendored but never entered into the provenance ledger.

Version bumped 0.8.2 -> 0.9.0 (MINOR) via the version-bump skill. No
Python source, tests, or .github/workflows touched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jei6EarsNVUDHMNxb1U1Za
…ot-joined capture node

Both found during on-device acceptance against a ReSpeaker XVF3800 (2886:001a):
the permission hint told the user to add a rule for 38fb:1001, and 'list --root
<fixture>' probed the host's real /dev/snd/pcmC1D0c because the fixture's card 1
collided with the live card 1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jei6EarsNVUDHMNxb1U1Za
…rmware

Found on hardware (ReSpeaker XVF3800, Seeed USB firmware v2.1.0): the vendored
reachy_mini table is the 38fb:1001 map. Seeed's 2886 firmware has no
DOA_VALUE_RADIANS, its DOA_VALUE is two uint16 (degrees, speech flag), and it
adds LED_RING_COLOR and the AIC3104 output levels. Adds the uint16 codec,
FIRMWARE_OVERLAYS keyed by USB vendor id, Xvf3800(vendor=...), param resolution
after device resolution, 'param list --vendor', and azimuth_deg alongside
azimuth_rad in 'array doa' (unit conversion only). Plan deviation d2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jei6EarsNVUDHMNxb1U1Za
- mixer: accept ',index=N' on the amixer numid line so a second same-named
  control no longer overwrites the first one's values (gain readback was stale)
- engine: passthrough stream converts to S16BE before rtpL16pay (the payloader
  refused S16LE: 'could not link queue0 to rtpl16pay0')
- stream/record: --rate/--channels/--format default to what the device
  advertises in /proc/asound stream0 instead of a fixed 48 kHz mono, which the
  exact caps filter could never negotiate on a 16 kHz stereo array; the payload
  records each field's source (explicit/advertised/default)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jei6EarsNVUDHMNxb1U1Za
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add complete microphone control, streaming, and recording domain

✨ Enhancement 🧪 Tests 📝 Documentation 🐞 Bug fix ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Adds stable device discovery, XVF3800 controls, gain, streaming, and bounded recording.
• Enforces dry-run safety, typed access failures, persistent-write consent, and activation auditing.
• Adds hardware-free tests, real-device acceptance evidence, and synchronized agent-facing
 documentation.
Diagram

graph TD
  ACCESS["Access checks"] <-- DEV["Device identity"] <-- CLI["CLI commands"] --> MIX["ALSA mixer"]
  CLI --> XVF["XVF3800 protocol"] --> USB["usbdevfs transport"]
  CLI --> ENG["GStreamer engine"]
  CLI --> ACT["Activation log"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use pyusb and native audio bindings
  • ➕ Less low-level ioctl and subprocess code
  • ➕ Higher-level APIs may simplify transport and mixer handling
  • ➖ Breaks the zero-runtime-dependency contract
  • ➖ Introduces native-library installation and compatibility risk
  • ➖ Makes hardware-free deployment less predictable
2. Delegate controls to the Reachy SDK
  • ➕ Reuses an existing XVF3800 parameter implementation
  • ➕ Reduces duplicated protocol knowledge
  • ➖ Pulls in the full robot SDK and pyusb stack
  • ➖ Couples generic microphones to Reachy-specific software
  • ➖ Weakens the standalone media-cli contract
3. Expose only generic ALSA capture
  • ➕ Substantially smaller implementation and review surface
  • ➕ Avoids firmware-version compatibility concerns
  • ➖ Cannot provide DoA, AEC, or raw XVF3800 controls
  • ➖ Does not satisfy the microphone-array domain requirements

Recommendation: Keep the PR's stdlib-and-subprocess boundary. It preserves the explicit zero-dependency requirement while isolating Linux USB, ALSA, and GStreamer concerns behind testable modules. The main review focus should be protocol correctness, firmware overlays, access semantics, and guarantees that dry runs never activate hardware.

Files changed (115) +14430 / -188

Enhancement (16) +5507 / -12
access.pyAdd typed device-access classification +387/-0

Add typed device-access classification

• Classifies audio and USB nodes as available, absent, forbidden, or busy, with holder discovery and actionable remediation.

microphone_cli/access.py

activation.pyAdd append-only activation auditing +153/-0

Add append-only activation auditing

• Writes one atomic JSON Lines record for every applied action, including failures and timestamps.

microphone_cli/activation.py

__init__.pyRegister the microphone command surface +42/-11

Register the microphone command surface

• Registers all domain commands, aligns argparse with the microphone executable, and preserves structured error routing.

microphone_cli/cli/init.py

array.pyAdd direction-of-arrival and AEC commands +563/-0

Add direction-of-arrival and AEC commands

• Implements single-shot and watched DoA reads plus dry-run-gated AEC inspection and updates across supported firmware variants.

microphone_cli/cli/_commands/array.py

gain.pyAdd ALSA and firmware gain controls +330/-0

Add ALSA and firmware gain controls

• Reads both gain stages and plans or applies targeted changes with readback and activation logging.

microphone_cli/cli/_commands/gain.py

inspect.pyAdd microphone capability inspection +214/-0

Add microphone capability inspection

• Reports advertised capture formats, rates, channels, and best-effort XVF3800 firmware identity.

microphone_cli/cli/_commands/inspect.py

list_devices.pyAdd stable microphone listing +144/-0

Add stable microphone listing

• Combines USB capture discovery with per-device access status while keeping unhealthy devices reportable.

microphone_cli/cli/_commands/list_devices.py

param.pyAdd raw firmware parameter commands +370/-0

Add raw firmware parameter commands

• Lists, reads, validates, and conditionally writes XVF3800 parameters with an extra persistent-action gate.

microphone_cli/cli/_commands/param.py

record.pyAdd bounded audio recording +648/-0

Add bounded audio recording

• Plans, probes, or records WAV and Matroska output under mandatory duration and byte limits.

microphone_cli/cli/_commands/record.py

stream.pyAdd RTP microphone streaming +667/-0

Add RTP microphone streaming

• Plans, probes, or starts passthrough and Opus RTP/UDP streams with self-contained consumer metadata.

microphone_cli/cli/_commands/stream.py

_errors.pyAdd retryable busy exit status +3/-1

Add retryable busy exit status

• Introduces exit code 3 so device contention remains distinct from environment failures.

microphone_cli/cli/_errors.py

devices.pyAdd stable USB microphone identity +530/-0

Add stable USB microphone identity

• Discovers USB capture cards from procfs and sysfs, synthesizes serial-based IDs, and rejects unstable or ambiguous selectors.

microphone_cli/devices.py

engine.pyAdd GStreamer audio engine adapter +427/-0

Add GStreamer audio engine adapter

• Detects required elements and builds pure argv for RTP streaming and bounded WAV or Matroska recording.

microphone_cli/engine.py

mixer.pyAdd zero-dependency ALSA gain adapter +242/-0

Add zero-dependency ALSA gain adapter

• Parses amixer controls, selects capture volume, builds dry-run argv, and performs writes with readback.

microphone_cli/mixer.py

usbctl.pyAdd stdlib USB control transport +259/-0

Add stdlib USB control transport

• Implements usbdevfs control ioctls, sysfs device lookup, and device-specific permission remediation without pyusb.

microphone_cli/usbctl.py

xvf3800.pyAdd typed XVF3800 firmware protocol +528/-0

Add typed XVF3800 firmware protocol

• Vendors the parameter table, implements typed codecs and status-64 retries, and applies per-vendor firmware overlays.

microphone_cli/xvf3800.py

Bug fix (3) +11 / -10
cli.pyAlign CLI introspection naming +5/-5

Align CLI introspection naming

• Changes user-facing command references from the distribution name to the executable name.

microphone_cli/cli/_commands/cli.py

explain.pyAlign explain command naming +2/-2

Align explain command naming

• Updates explain documentation and remediation to reference the installed microphone command.

microphone_cli/cli/_commands/explain.py

whoami.pyAlign identity output naming +4/-3

Align identity output naming

• Updates user-facing identity text to use the installed microphone command.

microphone_cli/cli/_commands/whoami.py

Tests (73) +5086 / -11
blind-consumer.shAdd payload-only stream consumer +77/-0

Add payload-only stream consumer

• Verifies that a second process can attach and depayload RTP using only the stream JSON response.

scripts/acceptance/blind-consumer.sh

run.shAdd on-device acceptance runner +117/-0

Add on-device acceptance runner

• Exercises read, write, record, stream, restoration, and activation-log behavior against selected hardware.

scripts/acceptance/run.sh

infoModel baseline playback PCM +11/-0

Model baseline playback PCM

• Adds synthetic ALSA playback metadata for the non-capture card.

tests/fixtures/host-baseline/proc/asound/card0/pcm3p/info

infoModel baseline capture PCM +11/-0

Model baseline capture PCM

• Adds synthetic ALSA capture metadata for the baseline USB microphone.

tests/fixtures/host-baseline/proc/asound/card1/pcm0c/info

infoModel baseline USB playback PCM +11/-0

Model baseline USB playback PCM

• Adds paired playback metadata to the baseline USB audio card.

tests/fixtures/host-baseline/proc/asound/card1/pcm0p/info

stream0Advertise baseline USB audio formats +19/-0

Advertise baseline USB audio formats

• Provides synthetic USB stream capabilities for format and channel inspection.

tests/fixtures/host-baseline/proc/asound/card1/stream0

cardsDefine baseline ALSA cards +4/-0

Define baseline ALSA cards

• Models one platform card and one USB microphone card.

tests/fixtures/host-baseline/proc/asound/cards

pipewire-0Model baseline PipeWire visibility +0/-0

Model baseline PipeWire visibility

• Adds a synthetic PipeWire runtime socket marker.

tests/fixtures/host-baseline/run/user/1000/pipewire-0

card0Link baseline platform sound card +1/-0

Link baseline platform sound card

• Adds the synthetic sysfs link for card zero.

tests/fixtures/host-baseline/sys/class/sound/card0

card1Link baseline USB sound card +1/-0

Link baseline USB sound card

• Adds the synthetic sysfs link from ALSA card one to its USB parent.

tests/fixtures/host-baseline/sys/class/sound/card1

idIdentify baseline platform card +1/-0

Identify baseline platform card

• Provides the platform card ID used to confirm non-USB devices are ignored.

tests/fixtures/host-baseline/sys/devices/platform/NVDA2014:00/sound/card0/id

idIdentify baseline USB card +1/-0

Identify baseline USB card

• Provides the stable ALSA card ID for the baseline array fixture.

tests/fixtures/host-baseline/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.1/5-1.1:1.0/sound/card1/id

idProductSet baseline USB product ID +1/-0

Set baseline USB product ID

• Marks the baseline device as a supported XVF3800 product.

tests/fixtures/host-baseline/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.1/idProduct

idVendorSet baseline USB vendor ID +1/-0

Set baseline USB vendor ID

• Marks the baseline device with its supported vendor identifier.

tests/fixtures/host-baseline/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.1/idVendor

manufacturerSet baseline USB manufacturer +1/-0

Set baseline USB manufacturer

• Provides manufacturer text for stable-ID synthesis.

tests/fixtures/host-baseline/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.1/manufacturer

productSet baseline USB product name +1/-0

Set baseline USB product name

• Provides the device label used for listing and selection.

tests/fixtures/host-baseline/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.1/product

serialSet baseline USB serial +1/-0

Set baseline USB serial

• Provides the persistent discriminator used in the stable device ID.

tests/fixtures/host-baseline/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.1/serial

infoModel renumbered platform playback +11/-0

Model renumbered platform playback

• Preserves the platform PCM in the replug simulation.

tests/fixtures/host-renumbered/proc/asound/card0/pcm3p/info

infoModel renumbered capture PCM +11/-0

Model renumbered capture PCM

• Moves the same USB capture endpoint to ALSA card two.

tests/fixtures/host-renumbered/proc/asound/card2/pcm0c/info

infoModel renumbered playback PCM +11/-0

Model renumbered playback PCM

• Moves the paired USB playback endpoint to ALSA card two.

tests/fixtures/host-renumbered/proc/asound/card2/pcm0p/info

stream0Preserve formats after renumbering +19/-0

Preserve formats after renumbering

• Advertises unchanged USB capabilities after simulated re-enumeration.

tests/fixtures/host-renumbered/proc/asound/card2/stream0

cardsDefine renumbered ALSA cards +4/-0

Define renumbered ALSA cards

• Models the same USB microphone changing from card one to card two.

tests/fixtures/host-renumbered/proc/asound/cards

pipewire-0Model renumbered PipeWire visibility +0/-0

Model renumbered PipeWire visibility

• Adds the PipeWire runtime marker to the renumbered host.

tests/fixtures/host-renumbered/run/user/1000/pipewire-0

card0Link renumbered platform card +1/-0

Link renumbered platform card

• Preserves the platform card sysfs link.

tests/fixtures/host-renumbered/sys/class/sound/card0

card2Link renumbered USB card +1/-0

Link renumbered USB card

• Links ALSA card two to the same physical USB microphone identity.

tests/fixtures/host-renumbered/sys/class/sound/card2

idIdentify renumbered platform card +1/-0

Identify renumbered platform card

• Provides the unchanged platform card identifier.

tests/fixtures/host-renumbered/sys/devices/platform/NVDA2014:00/sound/card0/id

idIdentify renumbered USB card +1/-0

Identify renumbered USB card

• Provides the unchanged ALSA card name at a new USB path and card index.

tests/fixtures/host-renumbered/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.3/5-1.3:1.0/sound/card2/id

idProductPreserve renumbered product ID +1/-0

Preserve renumbered product ID

• Keeps the physical device product identity stable across replugging.

tests/fixtures/host-renumbered/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.3/idProduct

idVendorPreserve renumbered vendor ID +1/-0

Preserve renumbered vendor ID

• Keeps the physical device vendor identity stable across replugging.

tests/fixtures/host-renumbered/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.3/idVendor

manufacturerPreserve renumbered manufacturer +1/-0

Preserve renumbered manufacturer

• Keeps manufacturer metadata stable across the simulated replug.

tests/fixtures/host-renumbered/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.3/manufacturer

productPreserve renumbered product name +1/-0

Preserve renumbered product name

• Keeps the human device label stable across the simulated replug.

tests/fixtures/host-renumbered/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.3/product

serialPreserve renumbered serial +1/-0

Preserve renumbered serial

• Proves stable IDs survive USB path and ALSA index changes.

tests/fixtures/host-renumbered/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.3/serial

infoModel ReSpeaker capture PCM +11/-0

Model ReSpeaker capture PCM

• Adds the standalone ReSpeaker capture endpoint.

tests/fixtures/respeaker/proc/asound/card0/pcm0c/info

cardsDefine ReSpeaker ALSA card +2/-0

Define ReSpeaker ALSA card

• Models a host containing the Seeed XVF3800 array.

tests/fixtures/respeaker/proc/asound/cards

card0Link ReSpeaker sound card +1/-0

Link ReSpeaker sound card

• Connects the synthetic ALSA card to its Seeed USB parent.

tests/fixtures/respeaker/sys/class/sound/card0

idIdentify ReSpeaker card +1/-0

Identify ReSpeaker card

• Provides the ALSA identifier for the Seeed fixture.

tests/fixtures/respeaker/sys/devices/platform/NVDA8000:01/usb3/3-1/3-1:1.0/sound/card0/id

idProductSet ReSpeaker product ID +1/-0

Set ReSpeaker product ID

• Marks the fixture as Seeed product 001a.

tests/fixtures/respeaker/sys/devices/platform/NVDA8000:01/usb3/3-1/idProduct

idVendorSet ReSpeaker vendor ID +1/-0

Set ReSpeaker vendor ID

• Marks the fixture as Seeed vendor 2886 for overlay selection.

tests/fixtures/respeaker/sys/devices/platform/NVDA8000:01/usb3/3-1/idVendor

infoModel first array capture PCM +11/-0

Model first array capture PCM

• Adds the capture endpoint for the first array.

tests/fixtures/two-arrays/proc/asound/card0/pcm0c/info

stream0Advertise first array formats +19/-0

Advertise first array formats

• Provides capture capabilities for the first array.

tests/fixtures/two-arrays/proc/asound/card0/stream0

infoModel second array capture PCM +11/-0

Model second array capture PCM

• Adds the capture endpoint for the second array.

tests/fixtures/two-arrays/proc/asound/card1/pcm0c/info

stream0Advertise second array formats +19/-0

Advertise second array formats

• Provides capture capabilities for the second array.

tests/fixtures/two-arrays/proc/asound/card1/stream0

cardsDefine two attached arrays +4/-0

Define two attached arrays

• Models concurrent arrays for ambiguity and serial-selection tests.

tests/fixtures/two-arrays/proc/asound/cards

pipewire-0Model multi-array PipeWire visibility +0/-0

Model multi-array PipeWire visibility

• Adds a PipeWire runtime marker for both capture devices.

tests/fixtures/two-arrays/run/user/1000/pipewire-0

card0Link first array sound card +1/-0

Link first array sound card

• Connects card zero to the first USB array.

tests/fixtures/two-arrays/sys/class/sound/card0

card1Link second array sound card +1/-0

Link second array sound card

• Connects card one to the second USB array.

tests/fixtures/two-arrays/sys/class/sound/card1

idIdentify first array card +1/-0

Identify first array card

• Provides the first array's ALSA card identifier.

tests/fixtures/two-arrays/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.1/5-1.1:1.0/sound/card0/id

idProductSet first array product ID +1/-0

Set first array product ID

• Provides supported product identity for the first array.

tests/fixtures/two-arrays/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.1/idProduct

idVendorSet first array vendor ID +1/-0

Set first array vendor ID

• Provides supported vendor identity for the first array.

tests/fixtures/two-arrays/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.1/idVendor

manufacturerSet first array manufacturer +1/-0

Set first array manufacturer

• Provides stable-ID manufacturer text for the first array.

tests/fixtures/two-arrays/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.1/manufacturer

productSet first array product name +1/-0

Set first array product name

• Provides the shared human label used to test ambiguity.

tests/fixtures/two-arrays/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.1/product

serialSet first array serial +1/-0

Set first array serial

• Provides the unique selector for the first array.

tests/fixtures/two-arrays/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.1/serial

idIdentify second array card +1/-0

Identify second array card

• Provides the second array's ALSA card identifier.

tests/fixtures/two-arrays/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.3/5-1.3:1.0/sound/card1/id

idProductSet second array product ID +1/-0

Set second array product ID

• Provides supported product identity for the second array.

tests/fixtures/two-arrays/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.3/idProduct

idVendorSet second array vendor ID +1/-0

Set second array vendor ID

• Provides supported vendor identity for the second array.

tests/fixtures/two-arrays/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.3/idVendor

manufacturerSet second array manufacturer +1/-0

Set second array manufacturer

• Provides stable-ID manufacturer text for the second array.

tests/fixtures/two-arrays/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.3/manufacturer

productSet second array product name +1/-0

Set second array product name

• Provides the shared label that makes short selectors ambiguous.

tests/fixtures/two-arrays/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.3/product

serialSet second array serial +1/-0

Set second array serial

• Provides the unique selector for the second array.

tests/fixtures/two-arrays/sys/devices/platform/NVDA8000:02/usb5/5-1/5-1.3/serial

test_access.pyTest access states and remediation +255/-0

Test access states and remediation

• Covers successful, absent, forbidden, vanished, and busy nodes plus holder discovery and exit-code mapping.

tests/test_access.py

test_activation.pyTest activation-log guarantees +235/-0

Test activation-log guarantees

• Covers path precedence, append behavior, atomic writes, timestamps, failures, and exception recording.

tests/test_activation.py

test_array.pyTest DoA and AEC commands +585/-0

Test DoA and AEC commands

• Covers firmware variants, watch lifecycle, transport errors, dry runs, applied writes, and activation logging.

tests/test_array.py

test_cli.pyEnforce parser and documentation parity +235/-6

Enforce parser and documentation parity

• Adds live parser-tree checks, command smoke tests, and regressions against dead executable names or scaffold prose.

tests/test_cli.py

test_cli_introspection.pyUpdate introspection expectations +5/-5

Update introspection expectations

• Aligns CLI introspection tests with the microphone executable name and expanded domain surface.

tests/test_cli_introspection.py

test_devices.pyTest stable device discovery +210/-0

Test stable device discovery

• Covers procfs and sysfs parsing, renumbering stability, multiple arrays, unsupported cards, and selector errors.

tests/test_devices.py

test_engine.pyTest GStreamer capability and argv logic +439/-0

Test GStreamer capability and argv logic

• Covers element detection, typed dependency failures, exact stream pipelines, recording containers, and bounds.

tests/test_engine.py

test_gain.pyTest ALSA and firmware gain behavior +462/-0

Test ALSA and firmware gain behavior

• Covers mixer parsing, gain mapping, targets, dry runs, writes, readback, arrays, and activation records.

tests/test_gain.py

test_inspect.pyTest capability and firmware inspection +246/-0

Test capability and firmware inspection

• Covers capture-block parsing, missing metadata, non-array output, firmware identity, and best-effort errors.

tests/test_inspect.py

test_list.pyTest microphone listing +117/-0

Test microphone listing

• Covers empty hosts, stable device payloads, rooted node probes, and non-fatal access states.

tests/test_list.py

test_param.pyTest firmware parameter commands +428/-0

Test firmware parameter commands

• Covers listing, typed parsing, access direction, dry runs, overlays, persistent gating, readback, and logging.

tests/test_param.py

test_record.pyTest bounded recording behavior +455/-0

Test bounded recording behavior

• Covers output validation, dry-run and probe modes, engine checks, access failures, process bounds, artifacts, and logging.

tests/test_record.py

test_stream.pyTest RTP streaming behavior +418/-0

Test RTP streaming behavior

• Covers advertised formats, consumer metadata, codecs, hardware modes, required elements, access errors, spawning, and logging.

tests/test_stream.py

test_usbctl.pyTest usbdevfs transport +215/-0

Test usbdevfs transport

• Verifies ioctl layout and numbers, transfer buffers, sysfs filtering, device opening, and permission guidance.

tests/test_usbctl.py

test_xvf3800.pyTest XVF3800 protocol and codecs +362/-0

Test XVF3800 protocol and codecs

• Covers request fields, status retries, typed encoding and decoding, parameter validation, firmware identity, and vendor overlays.

tests/test_xvf3800.py

Documentation (18) +3817 / -153
microphone-domain.jsonRecord microphone-domain delivery metadata +39/-0

Record microphone-domain delivery metadata

• Adds the structured devague delivery record for the completed domain work.

.devague/deliveries/microphone-domain.json

microphone-domain.jsonAdd converged microphone-domain frame +1027/-0

Add converged microphone-domain frame

• Stores the full scoped, challenged, and resolved domain specification data.

.devague/frames/microphone-domain.json

microphone-domain.jsonAdd structured microphone-domain plan +682/-0

Add structured microphone-domain plan

• Stores task dependencies, acceptance conditions, risks, and implementation assignments.

.devague/plans/microphone-domain.json

CHANGELOG.mdDocument the 0.9.0 microphone release +21/-0

Document the 0.9.0 microphone release

• Records the new domain, hardware fixes, acceptance evidence, documentation, and command-name correction.

CHANGELOG.md

CLAUDE.mdReplace scaffold guidance with domain architecture +112/-18

Replace scaffold guidance with domain architecture

• Documents the seven domain modules, hardware safety model, test seams, parity checks, and acceptance findings.

CLAUDE.md

README.mdDocument the complete microphone CLI +177/-36

Document the complete microphone CLI

• Replaces scaffold prose with scope, commands, output contracts, hardware behavior, identity rules, and acceptance status.

README.md

acceptance-microphone-domain.mdRecord real-device acceptance results +152/-0

Record real-device acceptance results

• Documents ReSpeaker hardware evidence, write restoration, media validation, plan deviations, and seven discovered defects.

docs/acceptance-microphone-domain.md

2026-09-06-microphone-domain-split.mdAdd split workforce plan +183/-0

Add split workforce plan

• Breaks the domain implementation into isolated tasks and execution waves.

docs/plans/2026-09-06-microphone-domain-split.md

2026-09-06-microphone-domain.mdAdd microphone-domain build plan +131/-0

Add microphone-domain build plan

• Defines thirteen tasks with dependencies, acceptance criteria, risks, and file ownership.

docs/plans/2026-09-06-microphone-domain.md

skill-sources.mdComplete skill provenance records +2/-0

Complete skill provenance records

• Adds missing source entries for the recall and remember skills.

docs/skill-sources.md

2026-09-06-microphone-domain.mdAdd converged microphone-domain specification +173/-0

Add converged microphone-domain specification

• Defines requirements, safety boundaries, honesty conditions, decisions, and success signals.

docs/specs/2026-09-06-microphone-domain.md

xvf3800-parameters.mdDocument XVF3800 parameter provenance +86/-0

Document XVF3800 parameter provenance

• Explains the vendored table, resource groups, persistent tier, firmware overlays, and command mappings.

docs/xvf3800-parameters.md

__init__.pyDescribe the microphone package domain +7/-1

Describe the microphone package domain

• Replaces the generic agent-template module description with the implemented microphone capabilities and naming conventions.

microphone_cli/init.py

doctor.pyClarify doctor as an identity check +6/-3

Clarify doctor as an identity check

• Corrects executable naming and distinguishes identity validation from microphone readiness checks.

microphone_cli/cli/_commands/doctor.py

learn.pyTeach agents the microphone domain +247/-28

Teach agents the microphone domain

• Expands text and JSON guidance with the complete command map, safety levels, selectors, consent, and exit codes.

microphone_cli/cli/_commands/learn.py

overview.pyDescribe domain contracts and consent +83/-17

Describe domain contracts and consent

• Replaces scaffold artifacts with the live verb surface, hardware rules, identity contracts, and audit posture.

microphone_cli/cli/_commands/overview.py

__init__.pyAlign explain API documentation +1/-1

Align explain API documentation

• Updates the public explain module description to use the executable command name.

microphone_cli/explain/init.py

catalog.pyDocument every microphone command path +688/-49

Document every microphone command path

• Adds self-contained domain documentation for all parser paths and retains the distribution name only as a legacy lookup alias.

microphone_cli/explain/catalog.py

Other (5) +9 / -2
currentSelect the microphone-domain frame +1/-0

Select the microphone-domain frame

• Points devague working state at the microphone-domain frame.

.devague/current

current_planSelect the microphone-domain plan +1/-0

Select the microphone-domain plan

• Points devague working state at the exported microphone-domain plan.

.devague/current_plan

.gitignoreIgnore transient devague state +5/-0

Ignore transient devague state

• Excludes local devague questions and reviews from version control.

.gitignore

pyproject.tomlRelease version 0.9.0 +1/-1

Release version 0.9.0

• Bumps the package version from 0.8.2 to 0.9.0 while retaining zero runtime dependencies.

pyproject.toml

uv.lockSynchronize release metadata +1/-1

Synchronize release metadata

• Updates the locked project version to 0.9.0 without adding dependencies.

uv.lock

@qodo-code-review

qodo-code-review Bot commented Sep 6, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Live streams look finished in the audit ✓ Resolved 🐞 Bug ◔ Observability
Description
cmd_stream_audio exits activation_scope immediately after spawning the intentionally unbounded
pipeline, which gives the record a completed ended_at timestamp. Every applied stream therefore
appears finished in the activation log while its returned PID is still expected to be capturing and
transmitting audio.
Code

microphone_cli/cli/_commands/stream.py[R457-460]

+    with activation.activation_scope("stream audio", device.stable_id, params) as act:
+        proc = _spawn(argv)
+        act.params["pid"] = proc.pid
+        act.params["pipeline"] = list(argv)
Evidence
Stream metadata says the pipeline is unbounded and runs until the child stops. Activation scope
writes a completed record on context exit, but the stream context surrounds only process creation
and therefore exits while that process remains live.

microphone_cli/cli/_commands/stream.py[328-353]
microphone_cli/cli/_commands/stream.py[450-475]
microphone_cli/activation.py[125-153]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The activation audit marks an unbounded stream complete immediately after it starts.

## Issue Context
The activation scope defines `ended_at` as action completion and says still-running actions must not appear as completed records, while stream lifetime is explicitly tied to the spawned process.

## Fix Focus Areas
- microphone_cli/cli/_commands/stream.py[328-353]
- microphone_cli/cli/_commands/stream.py[450-475]
- microphone_cli/activation.py[117-153]
- tests/test_stream.py[289-310]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Broken streams are reported as live ✓ Resolved 🐞 Bug ≡ Correctness
Description
cmd_stream_audio treats a successful Popen call as successful stream startup without polling the
child, and _spawn discards all child diagnostics. When exact capture caps or another runtime
pipeline condition fails during startup, the command returns exit zero with a PID and attachment
instructions for an already-dead process.
Code

microphone_cli/cli/_commands/stream.py[R163-166]

+    return subprocess.Popen(  # nosec B603 - fixed argv built by engine.py, shell=False
+        argv,
+        stdout=subprocess.DEVNULL,
+        stderr=subprocess.DEVNULL,
Evidence
The stream payload explicitly acknowledges startup failures from unsupported format combinations.
Nevertheless, the apply path only spawns the process, suppresses stderr, never polls it, and
unconditionally emits an applied payload and returns zero.

microphone_cli/cli/_commands/stream.py[156-167]
microphone_cli/cli/_commands/stream.py[318-337]
microphone_cli/cli/_commands/stream.py[450-475]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Stream apply returns success immediately after spawning, even when the media pipeline exits during startup.

## Issue Context
The payload itself documents that unsupported exact caps fail at pipeline startup, but the parent never observes that failure and suppresses stderr.

## Fix Focus Areas
- microphone_cli/cli/_commands/stream.py[156-167]
- microphone_cli/cli/_commands/stream.py[318-353]
- microphone_cli/cli/_commands/stream.py[450-475]
- tests/test_stream.py[277-383]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Recordings can outlive their bounds ✓ Resolved 🐞 Bug ☼ Reliability
Description
_stop emits a warning when the recording child ignores termination but neither kills nor continues
waiting for that child. When a duration or size limit invokes this branch, _run_bounded returns
normally and the command reports completion while the pipeline can keep capturing audio and growing
the output file.
Code

microphone_cli/cli/_commands/record.py[R358-361]

+    except subprocess.TimeoutExpired:
+        # A child that ignores SIGTERM is reported, never silently swallowed:
+        # the artifact on disk is still what it is, so the run continues and
+        # says so on stderr rather than failing the whole recording.
Evidence
The stop helper returns after a timeout without sending a stronger signal, while both bound branches
immediately return a successful stop reason. The command then records the current size and exits the
activation scope even though the child may still be running.

microphone_cli/cli/_commands/record.py[350-365]
microphone_cli/cli/_commands/record.py[370-392]
microphone_cli/cli/_commands/record.py[413-437]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A bounded recording can remain alive after its termination grace period, while the command reports that the bound stopped it.

## Issue Context
`_run_bounded` returns immediately after `_stop`, but `_stop` only warns when the child survives SIGTERM.

## Fix Focus Areas
- microphone_cli/cli/_commands/record.py[350-392]
- tests/test_record.py[1-455]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View high (3)
4. Firmware receives the wrong setting ✓ Resolved 🐞 Bug ≡ Correctness
Description
_encode() converts each uint8 parameter with int(v) & 0xFF instead of checking that it fits
the declared unsigned-byte range. Because param set validates only token count and integer syntax,
--apply sends inputs such as -1 and 256 to the device as 255 and 0 respectively while
reporting that the requested value was applied.
Code

microphone_cli/xvf3800.py[R517-518]

+        if info.type == "uint8":
+            return bytes(bytearray(int(v) & 0xFF for v in values))
Evidence
The CLI checks only the number of tokens and whether they parse as integers before the apply path
calls Xvf3800.write(). Encoding then checks the value count but masks every integer to eight bits
without a numeric range check, and write() transmits the resulting payload directly to the USB
device without any later comparison or validation.

microphone_cli/cli/_commands/param.py[150-165]
microphone_cli/cli/_commands/param.py[246-295]
microphone_cli/xvf3800.py[441-451]
microphone_cli/xvf3800.py[495-522]
microphone_cli/xvf3800.py[495-528]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Unsigned 8-bit firmware parameter values outside the representable range silently wrap, causing the hardware to receive a different value from the one supplied by the user while the command reports that the requested value was applied.

## Issue Context
The CLI validates token count and integer syntax but not the numeric range. `_encode()` also checks value count but masks each integer with `0xFF`, after which `Xvf3800.write()` transmits the encoded bytes directly without another range check or comparison. Other unsigned encodings use `struct.pack`, which rejects values outside their representable range; `uint8` should provide equivalent behavior.

## Fix Focus Areas
- microphone_cli/xvf3800.py[441-451]
- microphone_cli/xvf3800.py[495-528]
- microphone_cli/cli/_commands/param.py[131-165]
- tests/test_xvf3800.py[1-362]
- tests/test_xvf3800.py[184-218]
- tests/test_param.py[1-428]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Applied changes can report failure ✓ Resolved 🐞 Bug ☼ Reliability
Description
activation_scope performs its only log write after the protected hardware action and propagates
every logging failure through the command dispatcher. If the log directory is unwritable or storage
fills, the device has already changed but the command returns an error, encouraging callers to retry
a non-idempotent operation whose first attempt actually ran.
Code

microphone_cli/activation.py[R152-153]

+        finished = replace(activation, ended_at=_now_iso())
+        record_activation(finished, path=path)
Evidence
The context manager writes only after the body completes and does not catch failures from
record_activation. Applying commands execute their hardware writes inside that body, so any later
directory creation, append, or fsync error changes the overall result after mutation has occurred.

microphone_cli/activation.py[85-110]
microphone_cli/activation.py[117-153]
microphone_cli/cli/_commands/gain.py[234-265]
microphone_cli/cli/_commands/array.py[349-360]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An apply operation can change hardware and then report failure solely because its audit record cannot be written.

## Issue Context
Logging is intentionally mandatory, so the command must establish audit availability before mutation and preserve an unambiguous result if final logging fails.

## Fix Focus Areas
- microphone_cli/activation.py[85-110]
- microphone_cli/activation.py[117-153]
- microphone_cli/cli/_commands/gain.py[234-265]
- microphone_cli/cli/_commands/array.py[349-360]
- tests/test_activation.py[146-227]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Recordings can exceed their size cap ✓ Resolved 🐞 Bug ≡ Correctness
Description
_run_bounded() returns "eos" immediately when the recording process has exited successfully,
before it tests the output artifact against max_bytes. A pipeline that finishes between polling
iterations after exceeding the cap is reported as a successful bounded recording, and _apply()
does not reject that oversized artifact.
Code

microphone_cli/cli/_commands/record.py[R383-386]

+        code = proc.poll()
+        if code is not None:
+            return "eos" if code == 0 else "error"
+        if _artifact_size(output_path) >= max_bytes:
Evidence
The successful-exit branch returns before the only size comparison. The generated pipeline has a
duration bound but no byte bound, and the apply path only rejects pipeline errors and empty
artifacts.

microphone_cli/cli/_commands/record.py[370-392]
microphone_cli/engine.py[398-426]
microphone_cli/cli/_commands/record.py[417-437]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A recording that exits normally can exceed `--max-bytes` because the process-exit check precedes the artifact-size check.

## Issue Context
`--max-bytes` is documented as an enforced output-size cap, but the GStreamer argv only directly limits duration.

## Fix Focus Areas
- microphone_cli/cli/_commands/record.py[370-392]
- microphone_cli/cli/_commands/record.py[417-437]
- tests/test_record.py[1-455]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

7. Callers receive a nonstandard busy code ✗ Dismissed 📘 Rule violation ≡ Correctness
Description
EXIT_BUSY_ERROR assigns code 3 to device contention instead of classifying it as an environment
error with code 2. When another process holds a microphone, access_error() places that value in
CliError, and _dispatch() returns it to invoking scripts and automation.
Code

microphone_cli/cli/_errors.py[25]

+EXIT_BUSY_ERROR = 3
Evidence
Rule 3132774 permits only codes 0, 1, and 2 for active outcomes and allows higher codes only when
reserved and unused. The PR defines code 3 for busy devices and actively returns it from
access_error().

Rule 3132774: Use standardized CLI exit codes for success, user error, and environment error
microphone_cli/cli/_errors.py[17-25]
microphone_cli/access.py[344-387]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Device contention currently exits with code 3, outside the standardized success, user-error, and environment-error codes.

## Issue Context
Busy hardware is an environmental condition. Preserve the detailed message and remediation while returning the standard environment error code 2.

## Fix Focus Areas
- microphone_cli/cli/_errors.py[17-25]
- microphone_cli/access.py[344-387]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Audit entries can be truncated ✓ Resolved 🐞 Bug ☼ Reliability
Description
record_activation calls os.write once but ignores its returned byte count before declaring the
append durable with fsync. If the operating system accepts only part of the payload, the apply
operation succeeds with a malformed final JSON line that can break later audit readers.
Code

microphone_cli/activation.py[R107-108]

+        os.write(fd, payload)
+        os.fsync(fd)
Evidence
The implementation states that each complete record is carried by one syscall, but it never verifies
that the syscall wrote len(payload) bytes. It then fsyncs and returns successfully, so a short
write is indistinguishable from a complete append.

microphone_cli/activation.py[85-110]
tests/test_activation.py[115-152]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The append-only activation log does not detect or complete partial writes.

## Issue Context
Any multi-call completion strategy must also retain the log's concurrent-writer atomicity, such as by serializing writers with a lock.

## Fix Focus Areas
- microphone_cli/activation.py[85-110]
- tests/test_activation.py[115-152]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Gain settings accept invalid levels ✓ Resolved 🐞 Bug ≡ Correctness
Description
cmd_gain_set() accepts any float despite its documented 0.0..1.0 domain, and _map_to_alsa()
clamps only the mixer value while firmware planning and writing retain the original value. On an
array using the default combined target, an out-of-range or non-finite input therefore sets ALSA to
a clamped value but sends the invalid original float to AUDIO_MGR_MIC_GAIN, leaving the controls
inconsistent while the command reports success.
Code

microphone_cli/cli/_commands/gain.py[R182-187]

+def cmd_gain_set(args: argparse.Namespace) -> int:
+    json_mode = bool(getattr(args, "json", False))
+    root = getattr(args, "root", "/") or "/"
+    target = getattr(args, "target", "both") or "both"
+    value = float(args.value)
+    apply = bool(getattr(args, "apply", False))
Evidence
The command documentation and argparse help describe the accepted input range as 0.0..1.0, but
neither the parser nor cmd_gain_set() rejects values outside that range or non-finite floats.
_map_to_alsa() explicitly clamps its input, while firmware planning and writing preserve the
original float, and the firmware codec packs that float without enforcing the range.

microphone_cli/cli/_commands/gain.py[48-64]
microphone_cli/cli/_commands/gain.py[172-187]
microphone_cli/cli/_commands/gain.py[215-265]
microphone_cli/xvf3800.py[514-518]
microphone_cli/cli/_commands/gain.py[15-27]
microphone_cli/cli/_commands/gain.py[172-206]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description

`gain set` accepts out-of-range or non-finite values and can apply inconsistent ALSA and firmware gain values.

## Issue Context

The CLI advertises a `0.0..1.0` input contract, but ALSA mapping silently clamps values to that range while array firmware receives the unmodified float. Validate and reject non-finite or out-of-range values before planning or applying either target, including the default combined target, and add test coverage for invalid inputs.

## Fix Focus Areas

- microphone_cli/cli/_commands/gain.py[15-27]
- microphone_cli/cli/_commands/gain.py[172-231]
- microphone_cli/cli/_commands/gain.py[242-265]
- tests/test_gain.py[1-462]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (2)
10. Startup failures expose raw tracebacks ✓ Resolved 📘 Rule violation ☼ Reliability
Description
main() calls _build_parser() before _dispatch()'s exception boundary, and the PR adds new
command-module imports and registrations inside that unprotected phase. If an import or parser
registration fails during startup, the exception reaches sys.exit(main()) and Python writes its
traceback to stderr.
Code

microphone_cli/cli/init.py[76]

+    from microphone_cli.cli._commands import array as _array_group
Evidence
Rule 3132769 requires the top-level CLI entry point to wrap every non-system-exit failure. The added
command imports execute in _build_parser() before the only except Exception block in
_dispatch(), while the module guard directly invokes main().

Rule 3132769: Wrap all uncaught CLI exceptions in CliError to prevent raw tracebacks on stderr
microphone_cli/cli/init.py[75-89]
microphone_cli/cli/init.py[129-167]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The CLI exception boundary only covers command dispatch, leaving parser construction and argument processing able to emit raw tracebacks.

## Issue Context
Keep intentional `SystemExit` and `KeyboardInterrupt` behavior, but convert other startup exceptions into formatted `CliError` output.

## Fix Focus Areas
- microphone_cli/cli/__init__.py[75-167]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Short device replies look complete ✓ Resolved 🐞 Bug ☼ Reliability
Description
_decode returns whatever unsigned byte slice is present without checking it against the
parameter's declared count. When a successful transport reply is truncated, parameter reads and
firmware metadata consumers receive an incomplete list as valid state instead of an environment
error.
Code

microphone_cli/xvf3800.py[R482-483]

+    if info.type == "uint8":
+        return list(body[: info.count])
Evidence
A status-zero response is immediately decoded. The unsigned byte branch merely slices the available
body, while the following wide-type branch calculates the expected size and raises when the body is
short.

microphone_cli/xvf3800.py[386-419]
microphone_cli/xvf3800.py[478-492]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Truncated successful replies for unsigned byte parameters are accepted as complete values.

## Issue Context
Wide numeric types already validate their required body length, so unsigned bytes should enforce the equivalent count check.

## Fix Focus Areas
- microphone_cli/xvf3800.py[386-419]
- microphone_cli/xvf3800.py[478-492]
- tests/test_xvf3800.py[1-362]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 19 rules
Review mode: 🧠 Deep: This is a broad, hardware-facing feature with substantial new logic across many independent modules and CLI paths, including USB control, persistent writes, streaming, recording, and device access where multiple subtle defects are plausible.

Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread microphone_cli/cli/__init__.py
Comment thread microphone_cli/cli/_errors.py
Comment thread microphone_cli/cli/_commands/record.py Outdated
Comment thread microphone_cli/cli/_commands/stream.py Outdated
Comment thread microphone_cli/cli/_commands/stream.py Outdated
Comment thread microphone_cli/activation.py Outdated
Comment thread microphone_cli/activation.py Outdated
Comment thread microphone_cli/cli/_commands/record.py
Comment thread microphone_cli/xvf3800.py Outdated
Comment thread microphone_cli/cli/_commands/gain.py Outdated
OriNachum and others added 7 commits September 7, 2026 00:24
…e ranges, unvalidated gain

- cli/__init__.py: wrap parser construction/parse_args in main() so any non-SystemExit,
  non-KeyboardInterrupt exception raised before _dispatch's boundary (e.g. an import or
  registration failure) becomes a structured CliError instead of a raw traceback.
- xvf3800.py _decode: the uint8 branch now checks the reply length against
  info.count and raises the same short-reply CliError the wide/half types raise.
- xvf3800.py _encode: uint8 and uint16 values are range-checked (0..255 / 0..65535)
  before packing, so an out-of-range value raises CliError(1) instead of being
  silently masked/truncated (-1 -> 255, 256 -> 0) and sent to firmware.
- gain.py cmd_gain_set: validates the gain value is finite and in 0.0..1.0 before
  any planning or writing, for every --target, instead of relying on _map_to_alsa's
  clamp (which never reached the firmware write).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jei6EarsNVUDHMNxb1U1Za
- record_activation() looped os.write() once and ignored its return value,
  so a short write could truncate an activation line. Loop until the full
  payload is written (or raise OSError on a 0-byte/failed write), keeping
  the single-open O_APPEND semantics that make lines under PIPE_BUF atomic.
- activation_scope() only opened the log after running the protected
  action, so a permission/ENOSPC problem surfaced as a generic failure
  after hardware had already changed, inviting a retry of a non-idempotent
  action. It now resolves the path, creates parent directories, and opens
  the log for append (then closes it) before yielding, raising
  CliError(code=2) up front on failure. If the final write still fails
  after the body ran, it now raises CliError(code=2) stating the action
  was applied and only the audit write failed, instead of a bare OSError.

Tests: test_record_activation_survives_short_writes,
test_record_activation_raises_on_zero_byte_write,
test_activation_scope_raises_before_body_when_log_dir_unwritable,
test_activation_scope_reports_applied_but_not_logged_after_body_ran,
test_activation_scope_reports_applied_but_not_logged_on_late_write_failure_after_raise.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jei6EarsNVUDHMNxb1U1Za
Four review findings on the --apply paths of `record` and `stream audio`,
all fixed test-first.

1. Recordings can outlive their bounds (record.py `_stop`/`_run_bounded`).
   A child that ignored SIGTERM was only warned about, and the bound was
   reported as if it had stopped the recording. `_stop` now escalates to
   SIGKILL, waits for it, and raises CliError(code=2) if the process
   survives even that — it never returns while the child is alive.

2. Recordings can exceed their size cap (record.py `_run_bounded`/`_apply`).
   The process-exit check returned "eos" before the artifact was compared
   with --max-bytes, so a pipeline that blew the cap and exited inside one
   poll interval looked like a clean bounded recording. The finished
   artifact is now stat'ed whatever the stopped_reason, and an oversized
   file is a typed exit-2 error naming the bytes written and the cap. The
   file is deliberately kept at its path — deleting it is the caller's
   call — and the activation record still carries the outcome.

3. Broken streams are reported as live (stream.py `cmd_stream_audio`).
   A successful Popen was treated as a running stream and the child's
   stderr was discarded. The child's stderr now goes to a temp file, the
   command waits STARTUP_SETTLE_S (0.5s, via the new `_sleep` seam) and
   polls; a child that has already exited is
   CliError(code=2, "stream pipeline exited <rc> during startup") whose
   remediation quotes the pipeline's own first ERROR/WARNING/
   not-negotiated lines — never a traceback.

4. Live streams look finished in the audit (stream.py + activation).
   An unbounded stream got ended_at at launch. It is now logged honestly
   with ended_at=None and params carrying pid and lifetime="unbounded"
   (via record_activation with an explicit Activation, not
   activation_scope); a startup failure still logs a closed line with
   params.error. The payload's lifetime/consent text says the log line is
   open-ended.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jei6EarsNVUDHMNxb1U1Za
…ection, open-ended stream audit (PR #6 threads 3, 4, 5, 9)
…n unstopped over-cap artifact is an error

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jei6EarsNVUDHMNxb1U1Za
OriNachum and others added 8 commits September 7, 2026 00:35
Hoist setup calls out of pytest.raises blocks so each contains exactly
one call that can raise (S5778), split composite `and` assertions into
separate asserts (S9073), and drop empty parentheses from bare
@pytest.fixture decorators (S9083). Behaviour and assertions are
unchanged; test count stays at 304 passed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jei6EarsNVUDHMNxb1U1Za
Behaviour-preserving refactor of the four command modules Sonar flagged:

- record._payload took 14 parameters (S107): the container, format, bounds
  and output path now travel together in a frozen `_Plan` dataclass, built
  once in cmd_record and also passed to _apply. Payload keys and values are
  unchanged.
- record's `mode` selection was a nested conditional expression (S3358);
  it is now a plain if/elif/else.
- cmd_record, cmd_stream_audio and cmd_param_list always returned 0 (S3516).
  Per the dispatch contract (_dispatch maps None to exit 0) these handlers —
  and their unconditionally-zero neighbours cmd_param_get, cmd_param_set,
  cmd_param_overview, cmd_gain_get, cmd_gain_set, cmd_gain_overview and both
  _no_verb wrappers — now return None.
- stream.advertised_format had cognitive complexity 16 (S3776); the per-field
  fallback logic moved into _resolved_rate / _resolved_channels /
  _resolved_sample_format, each returning (value, source).
- "Emit structured JSON." was duplicated in param.py (5x) and gain.py (4x)
  (S1192). It is now JSON_FLAG_HELP in microphone_cli/cli/_commands/__init__,
  used by param, gain, stream and record.

304 tests pass unchanged; teken cli doctor --strict stays 26/26. Every
--json payload was diffed against the pre-refactor build and is identical
apart from its own resolved_at timestamp.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nt continue, type mismatch

- mixer.py: _NUMID_RE's lazy `.*?` name group (ambiguous with the closing
  quote char) replaced with `[^']*` (S8786); list_controls' line-kind
  branching split into _header_fields/_apply_attr_line/_apply_value_line/
  _control_from_fields helpers, dropping cognitive complexity from 19 and
  removing the now-unreachable trailing `continue` (S3776, S3626).
- inspect.py: _RATES_RE's `.+` (ambiguous with the preceding `\s*`, both
  matching whitespace) replaced with `\S.*` (S8786); _parse_capture_block
  split into _section_header/_append_format/_append_channels/_append_rates/
  _parse_capture_line helpers, dropping cognitive complexity from 27
  (S3776).
- usbctl.py: find_devices' per-device attribute read and the vendor/
  product/serial filter extracted into _read_device_attrs/_matches_filters,
  dropping cognitive complexity from 17 (S3776).
- activation.py: explicitly annotate activation_scope's `target` local as
  Path so its type is unambiguous at both _record_or_report_applied call
  sites (S5655).

All regex rewrites verified against the existing amixer/stream0 fixtures,
including the ",index=1" duplicate-control case. 304 tests pass unchanged;
black/isort/flake8/bandit clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jei6EarsNVUDHMNxb1U1Za
… Activation explicitly

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jei6EarsNVUDHMNxb1U1Za
@sonarqubecloud

sonarqubecloud Bot commented Sep 6, 2026

Copy link
Copy Markdown

@OriNachum
OriNachum merged commit c1c95c0 into main Sep 6, 2026
8 checks passed
@OriNachum
OriNachum deleted the spec/microphone-domain branch September 6, 2026 23:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant