Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
27a7645
spec: microphone-domain (devague /scope + /think)
OriNachum Sep 6, 2026
a287568
spec: microphone-domain challenge pass (devague /challenge)
OriNachum Sep 6, 2026
c8ded40
plan: microphone-domain (devague /spec-to-plan) + split artifact
OriNachum Sep 6, 2026
8aa63d2
t1: add typed access errors and audio/usb device access model
OriNachum Sep 6, 2026
330413f
t4: add activation log (record_activation, activation_scope)
OriNachum Sep 6, 2026
298ae92
merge agent/t1: typed errors and access model
OriNachum Sep 6, 2026
a588677
merge agent/t4: activation log
OriNachum Sep 6, 2026
23cab49
t9: add GStreamer audio engine (detect/require + pure argv builders)
OriNachum Sep 6, 2026
dd11f69
merge agent/t9: GStreamer audio engine
OriNachum Sep 6, 2026
814c517
t2: device identity core — enumerate, stable ids, resolve
OriNachum Sep 6, 2026
557dbd7
merge agent/t2: device identity and fixture trees
OriNachum Sep 6, 2026
16d8f32
t3: XVF3800 control layer on stdlib usbdevfs (no pyusb)
OriNachum Sep 6, 2026
89afeab
merge agent/t3: XVF3800 control layer over usbdevfs
OriNachum Sep 6, 2026
51463fa
t5: verbs 'list' and 'inspect' — devices, access, and firmware
OriNachum Sep 6, 2026
9ec09e0
merge agent/t5: list and inspect verbs
OriNachum Sep 6, 2026
6c5e861
t7: add the array noun (doa watch + aec get/set)
OriNachum Sep 6, 2026
ed641d5
merge agent/t7: array noun (doa, watch, aec)
OriNachum Sep 6, 2026
4e15952
t8: add param noun (list/get/set) over the XVF3800 table
OriNachum Sep 6, 2026
a5f4414
merge agent/t8: param noun with persistent tier
OriNachum Sep 6, 2026
9108b17
t6: add gain get/set noun group (ALSA mixer + XVF3800 firmware)
OriNachum Sep 6, 2026
cc75471
merge agent/t6: gain get/set
OriNachum Sep 6, 2026
50cd7db
t10: add 'stream audio' and 'record' verbs (dry-run by default)
OriNachum Sep 6, 2026
521dfa3
merge agent/t10: stream audio and record verbs
OriNachum Sep 6, 2026
90e1f84
t11: wire the microphone surface into the CLI and purge template prose
OriNachum Sep 6, 2026
fffb600
merge agent/t11: surface wiring, prog=microphone, parity tests
OriNachum Sep 6, 2026
29369c6
t12: rewrite docs for the landed microphone domain, bump 0.9.0
OriNachum Sep 6, 2026
00e791b
merge agent/t12: docs, changelog, version 0.9.0
OriNachum Sep 6, 2026
a13ea7c
fix: udev hint names the refused device's USB ids; list probes the ro…
OriNachum Sep 6, 2026
7e92495
feat: per-firmware parameter overlays; DoA from DOA_VALUE on Seeed fi…
OriNachum Sep 6, 2026
f4ed456
fix: three defects found on-device (ReSpeaker XVF3800, USB fw 2.1.0)
OriNachum Sep 6, 2026
e686961
acceptance: on-device run against ReSpeaker XVF3800 (t13) + scripts +…
OriNachum Sep 6, 2026
9093224
devague: frame/plan state through wave 4 (deviations d1, d2; lapses; …
OriNachum Sep 6, 2026
5576c60
review: fix startup tracebacks, short xvf3800 replies, unmasked encod…
OriNachum Sep 6, 2026
e918c9e
merge review/rA: startup boundary, codec range/short-reply checks, ga…
OriNachum Sep 6, 2026
438d53e
review: fix truncated audit writes and applied-but-unreported failures
OriNachum Sep 6, 2026
f146f9c
merge review/rC: activation log full writes and pre-flight writabilit…
OriNachum Sep 6, 2026
ac83d00
review: stop pretending about recordings and streams
OriNachum Sep 6, 2026
a89c686
merge review/rB: record bound enforcement, stream startup failure det…
OriNachum Sep 6, 2026
1fa61e6
record: a polled max-bytes stop may overshoot by one interval; only a…
OriNachum Sep 6, 2026
461a462
test: enforce a 1000-line ceiling on every tracked Python file
OriNachum Sep 6, 2026
e9a18a2
sonar: tests — clear S5778/S9073/S9083 findings in owned test files
OriNachum Sep 6, 2026
3f65974
merge sonar/sT: test-file findings (S5778, S9073, S9083)
OriNachum Sep 6, 2026
daed232
sonar: commands — clear 8 findings in record/stream/param/gain
OriNachum Sep 6, 2026
0e912cd
merge sonar/sS1: command-module findings (S107, S3516, S3358, S3776, …
OriNachum Sep 6, 2026
c2bca37
sonar: domain — fix regex backtracking, cognitive complexity, redunda…
OriNachum Sep 6, 2026
82fe678
merge sonar/sS2: domain-module findings (S8786, S3776, S3626, S5655)
OriNachum Sep 6, 2026
c54953e
changelog: review round (Qodo, SonarCloud, line ceiling)
OriNachum Sep 6, 2026
422d6df
sonar: drop backtracking anchors in mixer regexes; build the finished…
OriNachum Sep 6, 2026
3ca3c04
sonar: possessive quantifier in the amixer key/value scanner
OriNachum Sep 6, 2026
3ada9e2
sonar: parse amixer attribute pairs by splitting instead of a scannin…
OriNachum Sep 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .devague/current
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
microphone-domain
1 change: 1 addition & 0 deletions .devague/current_plan
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
microphone-domain
39 changes: 39 additions & 0 deletions .devague/deliveries/microphone-domain.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"plan_slug": "microphone-domain",
"schema_version": 2,
"created": "2026-09-06T19:58:21Z",
"updated": "2026-09-06T20:07:24Z",
"deviations": [
{
"id": "d1",
"what": "Wave-4 acceptance target is a Seeed ReSpeaker XVF3800 (XIAO variant) on the dev host, id 2886:001a serial 114993702263100642, reflashed from I2S to USB firmware v2.1.0 via dfu-util, instead of the Reachy Mini Lite (38fb:1001) the plan named; the Reachy Mini's array is on its own Pi at 192.168.1.162 and was never reachable from this host",
"task_ref": "t13",
"reason": "the connected robot's USB-C is a host port (Reachy Mini, not Lite); the user chose to bring up a separate ReSpeaker board and asked for the firmware update; issue #4 tracks CLI support for the DFU path",
"affects": [
"t13"
],
"origin": "llm",
"status": "approved",
"classification": "acceptable",
"seq": 1
},
{
"id": "d2",
"what": "Seeed's USB firmware v2.1.0 on the 2886:001a board does not implement DOA_VALUE_RADIANS (resid 20 cmd 19) and its DOA_VALUE (20/18) is two uint16 values (degrees 0-359, speech flag), not two uint32; 'array doa' fails with firmware status 66 while every other probed parameter (VERSION, BLD_*, AEC_*, AUDIO_MGR_MIC_GAIN, LED/GPO) reads correctly through the stdlib usbdevfs path",
"task_ref": "t13",
"reason": "the vendored PARAMETERS table is reachy_mini's 38fb:1001 map; the spec parked this exact unknown (v4) and hardware now answers it: the map is firmware-specific. Needs a uint16 type and a per-firmware DoA source before array doa can pass acceptance on this board",
"affects": [
"t13",
"t7",
"t3"
],
"origin": "llm",
"status": "approved",
"classification": "needs-follow-up",
"seq": 2
}
],
"evidence": [],
"deltas": [],
"supersessions": []
}
1,027 changes: 1,027 additions & 0 deletions .devague/frames/microphone-domain.json

Large diffs are not rendered by default.

682 changes: 682 additions & 0 deletions .devague/plans/microphone-domain.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,8 @@ __marimo__/

# Per-machine skills config (copy from skills.local.yaml.example)
skills.local.yaml

# devague working state (not committed by default)
.devague/questions/

.devague/reviews/
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
Format follows [Keep a Changelog](https://keepachangelog.com/). This project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.9.0] - 2026-09-06

### Added

- **The microphone domain** — `list`, `inspect`, `gain get|set`, `array doa` (single-shot or `--watch` JSON Lines), `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), and `record` (bounded WAV/Matroska), built on 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), `xvf3800` (vendored parameter table, typed codec, status-64 retry), `mixer` (`amixer`), `engine` (GStreamer argv), and `activation` (append-only log of every `--apply`). Spec: `docs/specs/2026-09-06-microphone-domain.md`; plan: `docs/plans/2026-09-06-microphone-domain.md`.
- **Per-firmware parameter overlays** (`xvf3800.FIRMWARE_OVERLAYS`, `parameters_for`, `Xvf3800(vendor=…)`, `param list --vendor`) and a `uint16` codec, because Seeed's USB firmware (`2886`) has no `DOA_VALUE_RADIANS` and its `DOA_VALUE` is two `uint16`; `array doa` now reports `azimuth_deg` alongside `azimuth_rad` and names its `source` command.
- **docs/acceptance-microphone-domain.md** and `scripts/acceptance/` — on-device acceptance against a ReSpeaker XVF3800 (Seeed USB firmware 2.1.0): DoA matched the vendor's reference reader exactly, all volatile writes round-tripped, record and stream produced real audio.
- **docs/xvf3800-parameters.md** — attribution and resid-group guide for the vendored XVF3800 parameter table (Pollen Robotics' reachy_mini, Apache-2.0), the persistent tier, and how the array/param/gain verbs map onto it.

### Fixed

- **Seven defects found only on hardware** (see the acceptance doc): the announced RTP L16 consumer hardcoded `clock-rate=48000` (now follows the negotiated rate and channel count); the udev permission hint named `38fb:1001` instead of the refused device's own ids; `list --root <fixture>` probed the host's real `/dev/snd` node; the `amixer contents` parser dropped a second same-named control's `,index=1` suffix so gain readback was stale; passthrough streaming handed `S16LE` to `rtpL16pay`, which only takes `S16BE`; fixed 48 kHz mono defaults could never open a 16 kHz stereo device — `stream audio`/`record` now default to the advertised format and report each field's source; and the parameter map was assumed identical across firmwares.

### Changed

- **Review round on PR #6** — ten Qodo findings fixed (startup errors are structured, byte/half-word parameter values are range-checked and short replies rejected, `gain set` validates its 0.0..1.0 domain, a recording child that ignores SIGTERM is killed before the bound is reported, an artifact that finishes on its own over `--max-bytes` is an error with the file kept, `stream audio --apply` detects a pipeline that dies at startup and quotes GStreamer's diagnostics, a live stream's audit line is open-ended, the activation log is opened before the action and completes short writes); 38 SonarCloud findings cleared by behaviour-preserving refactors (parsers split into helpers, backtracking-free regexes, handlers return `None` under the dispatch contract, a shared `JSON_FLAG_HELP`, `_payload` takes a `_Plan`); and `tests/test_repo_hygiene.py` now enforces a 1000-line ceiling on every tracked Python file.
- **README.md and CLAUDE.md replace the scaffold-state narrative left by `5f9b1bd` ("scaffold microphone-cli from culture-agent-template")** with the domain state: `list`, `inspect`, `gain get`/`gain set`, `array doa`, `array aec get`/`array aec set`, `param list`/`param get`/`param set`, `stream audio`, and `record` now sit alongside the six agent-first verbs the scaffold shipped (`whoami`, `learn`, `explain`, `overview`, `doctor`, `cli overview`) — 13 top-level verbs, 276 tests, 92% coverage.
- **README.md rewritten for the domain state** — Status (13 verbs landed, on-device acceptance pending issue #3), Scope (non-goals: video, remote Reachy Mini, STT/TTS, playback, DoA coordinate transforms), a full CLI verb table, What comes out (JSON/JSON Lines shapes), What touches the hardware (the three-level split, activation log, persistent tier), and Why device identity is the hard part (stable ids, udev access).
- **CLAUDE.md rewritten from scaffold-state to domain-state** — a module map for the seven `microphone_cli/` domain modules, the three-level hardware split, the testing seams (`root=`, `_open_array`, `_ioctl`, `_spawn`, `_sleep`, `run=`), the fixture trees under `tests/fixtures/`, the parity tests that keep the catalog/learn/overview surfaces in sync with the registered parser, and the hardware-acceptance status.
- **Console-script note resolved** — `pyproject.toml`'s `microphone` script and argparse's `prog` now agree; the CLAUDE.md note that used to flag the mismatch now records it as fixed.
- **docs/skill-sources.md** gained rows for the `recall`/`remember` skills, which were vendored (from `eidetic-cli`, not guildmaster) but never entered into the provenance ledger.

## [0.8.2] - 2026-09-06

### Fixed
Expand Down
130 changes: 112 additions & 18 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,105 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

**microphone-cli** — an agent-first CLI for USB microphones and microphone
arrays: enumerate devices, select and inspect channels, control gain and sample
format, and read direction-of-arrival from array firmware.

**Current state: scaffold only.** The repo was cloned from the AgentCulture
agent template (`5f9b1bd scaffold microphone-cli from culture-agent-template`).
The package is renamed and the CI/identity/skills baseline is live, but **no
microphone domain code exists yet** — the only verbs are the template's
agent-first introspection surface (`whoami`, `learn`, `explain`, `overview`,
`doctor`, `cli overview`). Several strings still describe the template
("a clonable template for AgentCulture mesh agents") rather than the microphone
domain: `microphone_cli/cli/_commands/learn.py`, `overview.py` (`_ARTIFACTS`),
`microphone_cli/explain/catalog.py`, and `README.md`. Rewrite those as the
domain lands.
format, and read direction-of-arrival and echo-canceller state from
XVF3800-class array firmware.

**Current state: the domain surface is built.** The repo started as a clone
of the AgentCulture agent template
(`5f9b1bd scaffold microphone-cli from culture-agent-template`), which shipped
only the agent-first introspection surface. The microphone domain has since
landed on top of it: 13 top-level verbs, 285 tests, 92% coverage,
`teken cli doctor . --strict` at 26/26. The converged spec and plan it was
built from live at `docs/specs/2026-09-06-microphone-domain.md` and
`docs/plans/2026-09-06-microphone-domain.md`.

### Module map

| Module | Owns |
|--------|------|
| `microphone_cli/devices.py` | Device identity: `/proc/asound` + sysfs parsing, stable-id synthesis (udev-style, from USB manufacturer/product/serial), `resolve()` by selector. Never opens a device or checks permissions. Cited from `webcam_cli/devices.py` with the video half dropped. |
| `microphone_cli/access.py` | Typed device-access state: `ok` / `absent` / `forbidden` / `busy`, for both `audio` (ALSA capture nodes) and `usb` (raw USB nodes) device kinds, each with its own remediation. Cited from `webcam-cli/webcam_cli/access.py`. |
| `microphone_cli/usbctl.py` | Stdlib `usbdevfs` control transfers (`USBDEVFS_CONTROL` ioctl via `fcntl.ioctl`) — no `pyusb`, no `libusb`. `find_devices()`/`open_device()` plus the `_ioctl` testing seam. |
| `microphone_cli/xvf3800.py` | The XVF3800 vendor control protocol: `PARAMETERS` (vendored verbatim from Pollen Robotics' `reachy_mini`, Apache-2.0), `PERSISTENT`, `Xvf3800` read/write. See `docs/xvf3800-parameters.md`. |
| `microphone_cli/mixer.py` | ALSA mixer control via `amixer` subprocess calls (no `libasound` bindings) — `list_controls`/`get_gain`/`set_gain`, all taking a `run` seam. |
| `microphone_cli/engine.py` | GStreamer boundary: capability detection and pipeline construction, shelling out to `gst-launch-1.0`/`gst-inspect-1.0`. No `gi`/PyGObject import, ever. Cited (audio subset) from `webcam-cli/webcam_cli/engine.py`. |
| `microphone_cli/activation.py` | Append-only activation log: one JSON line per `--apply` action. Path resolution order and shape cited from `webcam-cli/webcam_cli/activation.py`. |

### The three-level hardware split

`array`/`param` (reads open the USB node directly — there is no cheaper probe
stage) and `stream`/`record` (which do have a probe stage) share one rule,
readable from the invocation alone:

| Invocation | What it touches |
|------------|------------------|
| default (no flag) | Nothing. Resolves the device, validates the request, prints the plan. Not logged. |
| `--probe` (`stream`/`record` only) | Detects the GStreamer engine and checks the capture node's access state, still without opening it. Not logged. |
| `--apply` | Opens the device and acts (writes gain, flips AEC state, writes a firmware parameter, streams, or records). Logged — one JSON line appended to the activation log (`$MICROPHONE_ACTIVATION_LOG`, else `$XDG_STATE_HOME/microphone-cli/activation.jsonl`, else `~/.local/state/microphone-cli/activation.jsonl`). |

`param set` on a name in `xvf3800.PERSISTENT` (`SAVE_CONFIGURATION`,
`CLEAR_CONFIGURATION`, `REBOOT`, `TEST_CORE_BURN`,
`TEST_AEC_DISABLE_CONTROL`, `USB_BIT_DEPTH`, every `SPECIAL_CMD_*`) needs
`--allow-persistent` in addition to `--apply` — an ordinary `rw` write is
volatile and reverts on power-cycle, this tier is not.

### Testing seams

No test ever touches real hardware. Every module that would open a device or
spawn a process exposes a callable seam that tests monkeypatch:

- `root=` (`devices.py`, `access.py`, and every command module that resolves
a device) — points filesystem parsing at a synthetic tree instead of `/`.
- `_open_array` (`cli/_commands/array.py`, `cli/_commands/param.py`) —
resolves a device and opens an `Xvf3800`; tests replace it.
- `_ioctl` (`usbctl.py`) — module-level callable defaulting to
`fcntl.ioctl`; tests replace it so no test ever touches `/dev`.
- `_spawn` (`cli/_commands/record.py`, `cli/_commands/stream.py`) — spawns
the `gst-launch-1.0` subprocess.
- `_sleep` (`cli/_commands/record.py`, and `array.py`'s DoA `--watch` poll
loop) — the retry/poll delay.
- `run=` (`mixer.py`'s `RunFunc`) — defaults to `subprocess.run`; every
`amixer`-calling function takes it so tests inject a fake.

Fixture trees live under `tests/fixtures/`: `host-baseline` (one array, one
plain mic), `host-renumbered` (the same devices after a simulated replug —
proves selectors survive card-index churn), `respeaker` (an older ReSpeaker
XVF3800, `2886:001a`), and `two-arrays` (disambiguation when more than one
`38fb:1001`/`2886:001a` device is attached).

### Parity tests

`tests/test_cli.py` enforces that the hand-maintained surfaces stay in sync
with the registered parser: `test_every_catalog_path_resolves` and
`test_every_registered_path_has_a_catalog_entry` (catalog ↔ parser, both
directions), `test_every_registered_path_appears_in_overview_verbs`
(`overview._VERBS` ↔ parser), and
`test_learn_json_command_map_matches_the_registered_surface`
(`learn._TEXT`/`_as_json_payload()` ↔ parser). Adding a verb without updating
all four fails CI, not just the rubric gate.

### Hardware acceptance

Run on 2026-09-06 against a Seeed ReSpeaker XVF3800 (`2886:001a`, Seeed USB
firmware 2.1.0) attached to the dev host; see
`docs/acceptance-microphone-domain.md` for the evidence and the seven defects it
surfaced. Two facts from that run shape the code:

- **The parameter map is firmware-specific.** `xvf3800.PARAMETERS` is Pollen's
`38fb:1001` map; `xvf3800.FIRMWARE_OVERLAYS` patches it per USB vendor id
(`2886` = Seeed: `DOA_VALUE` is two `uint16` degrees/speech, no
`DOA_VALUE_RADIANS`). Always construct `Xvf3800(fd, vendor=...)` and resolve
parameter names *after* resolving the device.
- **Capture format is advertised, never assumed.** `stream audio` and
`record` default `--rate/--channels/--format` from `stream0`
(`stream.advertised_format`) because the exact caps filter never falls back.

Still open: the Reachy Mini Lite (`38fb:1001`) named in the plan
([issue #3](https://github.com/agentculture/microphone-cli/issues/3)), CLI
support for the DFU/firmware bring-up
([issue #4](https://github.com/agentculture/microphone-cli/issues/4)), and
voice-activity exposure
([issue #5](https://github.com/agentculture/microphone-cli/issues/5)).

## Commands

Expand Down Expand Up @@ -53,13 +140,14 @@ markdownlint-cli2 "**/*.md" "#node_modules" "#.local" "#.claude/skills"
Config lives in `.markdownlint-cli2.yaml` (MD013 and MD060 off, MD024
siblings-only for the changelog; `.claude/skills/**` ignored).

### Console-script name
### Console-script name — RESOLVED

`pyproject.toml` declares `microphone = "microphone_cli.cli:main"` — the binary
is **`microphone`**, not `microphone-cli`. The argparse `prog` is
`"microphone-cli"`, so `--help` and every doc string say `microphone-cli …`
while the actual command is `microphone …`. Either rename the script or the
`prog` before this ships; until then, prefer `microphone` in anything runnable.
`pyproject.toml` declares `microphone = "microphone_cli.cli:main"` and
argparse's `prog` is also `"microphone"` — the binary and the program name
both agree now. `--help` output and every doc string say `microphone …`;
nothing presents `microphone-cli <verb>` as something to type.
`microphone-cli` still correctly names the *project*, the PyPI *distribution*,
and the mesh *nick* — do not blanket-replace it.

## Architecture

Expand Down Expand Up @@ -116,6 +204,12 @@ guidance file. Changing `backend` means adding the matching prompt file or

### Adding a verb or noun

The domain modules (`devices.py`, `access.py`, `usbctl.py`, `xvf3800.py`,
`mixer.py`, `engine.py`, `activation.py`) are the domain logic; a new verb on
an *existing* noun almost never touches them. Scope the change to
`microphone_cli/cli/_commands/*.py` plus the three hand-maintained surfaces
below — that is the whole checklist:

1. New module in `microphone_cli/cli/_commands/` exposing `register(sub)`, with
`--json` and a `func` default.
2. Register it in `_build_parser()` (there is a marked spot).
Expand Down
Loading
Loading