# AGENTS.md Always-on speech-to-text daemon plus a separate voice-assistant layer. Python 3.11+ (dev env is 3.13, but `requires-python = ">=3.11"` and ruff targets `py311` — don't use 3.12+ syntax), `uv`, `src/` layout. ## Commands ```bash uv sync # install into .venv uv run pytest -v # full suite (hardware- and network-free) uv run pytest tests/test_segmenter.py::test_name -v # single test uv run ruff check . # only static check (no mypy/pyright) uv build # sdist + wheel (hatchling) ``` There is no CI, pre-commit, Makefile, or task runner. Run ruff + pytest manually before finishing. ## Process architecture (don't break this) Three processes wired by UNIX sockets under `$XDG_RUNTIME_DIR` (fallback `/tmp`): - `stt-daemon` (`cli.py`) — capture → pre-roll/VAD → segment → transcribe → stdout + JSONL. **Publish-only** on `stt-daemon.sock`: it never accepts inbound messages; lifecycle is signals only. - `stt-assistant` (`assistant/runner.py`) — subscribes to that bus, owns all state (modes, command buffer, chat history, LLM, tools, audit), serves `stt-assistant.sock`. - `stt-tui` / `stt-ctl` — clients of the assistant control socket only. Stateful logic belongs in the assistant. Transcription runs on a single worker thread by design (faster-whisper isn't thread-safe) — do not parallelize it. `stt-daemon` with no subcommand runs the daemon (legacy `--check` / `--list-sources` still work). ## Config - Loaded from `config.toml` in the **current working directory** only (no env var, no XDG lookup); `--config` overrides. A missing file means dataclass defaults. - `config.example.toml` is the shipped template (values only); the README config section is the annotated reference. Keep all three in sync when adding a key. - Mapping is **manual** — no pydantic/dynaconf. Adding a key means editing the dataclass *and* `load_config()` in `config.py`. - Env vars: `STT_DAEMON_STATE` / `XDG_STATE_HOME` (provisioning state at `~/.local/state/stt-daemon/audio.json`), `XDG_RUNTIME_DIR` (sockets), `HF_HOME` (Whisper cache). - Never treat the live `config.toml` as a source of defaults; it is gitignored user state. ## Testing - `tests/` has 206 tests, all hardware- and network-free. Preserve that: inject fakes instead of touching real `pactl`, audio devices, or an LLM. - Key seams: `Daemon(...)` accepts `capture_factory`, `vad_factory`, `transcriber_factory`, `sink_factory`; `PulseAudio` takes an injectable `runner`; `AssistantService` takes `time_fn`, `executor`, `audit`. - No `conftest.py` — per-file fakes are the convention. Tests are the behavioral spec; read them before changing behavior. ## Conventions & gotchas - Never commit gitignored runtime output: `config.toml`, `transcripts/`, `audio/`, `audit/`, `.omo/`. - New assistant tools: register in `assistant/tools.py`. Model-supplied arguments are JSON-parsed and schema-validated before running; set `side_effect` and `requires_confirmation` for anything non-read-only. - Whisper models must be multilingual — do not use `.en` variants (defaults auto-detect es/en). - `.omo/plans/` holds local design plans (gitignored), including the assistant-layer spec.