clankvox/.agents/skills/clankvox-dev/SKILL.md

name: clankvox-dev description: >- Build, test, lint, run, and debug the clankvox Rust media plane and validate it against the Clanky agent that drives it over IPC. Use when editing voice / Go Live transport, DAVE, codecs, RTP/RTCP, playback pacing, or the stdin/stdout IPC contract; when you need the logging knobs (RUST_LOG, AUDIO_DEBUG); when you need the agent-side smoke names (clankvox:setup, smoke:voice:*); or when a change spans the clankvox <-> clanky-agent seam and both sides must move together.

clankvox Development

clankvox is Clanky's standalone Rust media plane — Discord voice and Go Live transport (realtime sockets, RTP/RTCP, Opus, DAVE, codecs, playback pacing). It runs as a subprocess of the Node agent (../clanky-agent) and speaks a stdin/stdout IPC contract to it. It carries transport truth only; product policy lives in the agent.

Read docs/architecture.md for the process model and module map, and CONTRIBUTING.md for the human-contributor PR rules. This skill is the agent runbook: how to build, test, log, validate, and where each change lands across the two repos.

Build

From this repo, plain cargo works — .cargo/config.toml force-pins the build env (OPUS_STATIC=1, OPUS_NO_PKG=1, CMAKE_POLICY_VERSION_MINIMUM=3.5; the last lets cmake >= 4 build the bundled libopus in audiopus_sys):

cargo build --release

From the sibling agent repo, build clankvox through Clanky's wrapper — it is idempotent (a no-op once the release binary exists; --force rebuilds), installs the Rust toolchain if missing, and builds --locked:

cd ../clanky-agent
pnpm clankvox:setup            # build the release binary
pnpm clankvox:setup --force    # force a rebuild

The agent locates the crate via CLANKY_CLANKVOX_DIR (source dir; defaults to a sibling clone) and can skip the build with CLANKY_CLANKVOX_BIN (path to a prebuilt binary).

Test, format, lint

cargo test --locked --all-features
cargo fmt --all --check
cargo clippy --locked --all-targets --all-features -- -D warnings

clippy::pedantic is enabled (see Cargo.toml [lints.clippy]), and CI denies warnings after applying the repo's explicit allow-list.

Agent-side smokes

The Rust <-> Node boundary is exercised from ../clanky-agent, not from here. After a transport or IPC change, run the relevant smokes there:

cd ../clanky-agent
pnpm smoke:clankvox:ipc        # IPC framing (tag byte + length) round-trip
pnpm smoke:voice:config        # voice config / setup contract
pnpm smoke:voice:control       # control-plane commands (connect, publish, pause)
pnpm smoke:voice:fault         # fault / disconnect / recovery handling
pnpm smoke:voice:realtime      # realtime capture/playback path

Prefer these script names over the file paths behind them — the paths rot, the scripts are stable.

Logging

clankvox writes structured tracing logs to stderr; the IPC channel stays clean (line-delimited JSON on stdin, framed tag-byte + length messages on stdout). The default filter (main.rs:125) is:

info,davey=warn,davey::cryptor::frame_processors=off

Override with the standard env knobs:

  • RUST_LOG=debug — transport detail (WS opcodes, UDP, DAVE handoffs)
  • AUDIO_DEBUG=1 — extra audio-path IPC logging (main.rs:136)

Trust subprocess telemetry, not the agent's view, for floor state: Clanky can look logically done while clankvox still has buffered playback draining.

High-Value Edit Locations

Task-oriented map. Rust files are in this repo; TypeScript files are in ../clanky-agent.

Changing…Rust (this repo)Agent side (../clanky-agent)
voice/session connectsrc/connection_supervisor.rs, src/voice_conn.rsagent/lib/voice/supervisor.ts
inbound speaking/audio capturesrc/capture_supervisor.rs, src/voice_conn/udp_rx.rs
TTS/music playbacksrc/playback_supervisor.rs, src/audio_pipeline.rs, src/music.rsagent/lib/voice/control.ts
media subprocess pipelinessrc/process_unix.rs, src/music.rs, src/stream_publish.rs
Go Live watch transportsrc/voice_conn.rs, src/video.rs, src/capture_supervisor.rsagent/lib/voice/discordStreamDiscovery.ts
Go Live publish transportsrc/stream_publish.rs, src/voice_conn.rs, src/connection_supervisor.rsagent/lib/voice/discordStreamDiscovery.ts
IPC contractsrc/ipc.rs, src/ipc_protocol.rs, src/ipc_router.rsagent/lib/voice/clankvoxIpcClient.ts

The full module map is docs/architecture.md.

Coordination with the agent

clankvox almost never changes alone. A new transport event or command is a two-repo change — land both sides together:

  • Rust IPC message contracts: src/ipc.rs, src/ipc_protocol.rs
  • TS IPC client: ../clanky-agent/agent/lib/voice/clankvoxIpcClient.ts
  • Stream discovery / control plane: ../clanky-agent/agent/lib/voice/discordStreamDiscovery.ts
  • Voice orchestration entry point: ../clanky-agent/agent/channels/voice.ts (attachVoiceRuntime()), backed by agent/lib/voice/supervisor.ts and agent/lib/voice/control.ts

The agent repo is ../clanky-agent — its voice code lives under agent/lib/voice/ and agent/channels/, and its build/test entry points are the clankvox:setup and smoke:* scripts above. The repo was renamed and its voice code relocated over time, so if an external doc points you at an older sibling name, a src/voice/ path under an agents/ tree, a standalone Discord-voice orchestration module, or a voice:* build/test script, treat it as stale and resolve against the paths and scripts here instead.

Manual validation

Audio changes

  1. cargo test
  2. cd ../clanky-agent && pnpm smoke:voice:realtime (add smoke:voice:control if you touched connect/publish control).
  3. In a live voice session, confirm:
    • join still works
    • inbound capture still arrives
    • TTS still drains
    • music still starts, pauses, and resumes cleanly

Go Live changes

  1. cargo test
  2. cd ../clanky-agent && pnpm smoke:voice:control (exercises stream watch/publish lifecycle) and pnpm smoke:voice:fault for disconnect paths.
  3. Validate the role you changed (stream_watch vs stream_publish) — a fix for one role is not automatically correct for the other.
  4. Confirm the right transport-state events still emit over IPC.

Sharp edges

  • DAVE transition handling is protocol-sensitive; small changes break encryption handoffs. davey reports UnencryptedWhenPassthroughDisabled for Go Live video frames — that path is a known open limitation (see docs/go-live.md, "DAVE Video Decrypt").
  • Stream-server behavior is role-specific. voice, stream_watch, and stream_publish are separate legs with different lifecycle expectations; they share protocol code but not identical behavior.
  • Sender and receiver video paths share depacketization/packetization code but diverge on keyframe and DAVE handling.
  • Discord's raw UDP path (protocol: "udp") does not honour PLI/FIR keyframe requests; clankvox sends them best-effort only. Do not assume on-demand keyframes are available.