Co-authored-by: Aaron D. Lee <himself@adlee.work> Co-committed-by: Aaron D. Lee <himself@adlee.work>
7.2 KiB
Development
The dev loop: how to iterate on the Rust workspace, run tests, and find your way around the codebase.
Status: Slices 1–3 are merged to
main. Slice 4 (barge-in / VAD-driven playout kill) is the active build target, in flight on theslice-4-dev-a-reflex+slice-4-dev-b-tapbranches. Everything below applies to the merged code onmain; to follow slice-4 in flight, check out either dev branch. Implementation plan:docs/superpowers/plans/2026-07-01-slice-4-barge-in.md.
Workspace layout
Cargo workspace at the repo root, shaped to ADR-0002's fused per-call vertical. One binary crate + five library crates.
rutster/
├── Cargo.toml # [workspace] + [workspace.dependencies]
├── deny.toml # cargo-deny config (licenses/bans/sources)
├── rust-toolchain.toml # pinned stable
├── LEARNING.md # index of "to learn concept X, read file Y"
├── crates/
│ ├── rutster/ # binary: axum signaling server + media driver
│ │ ├── src/main.rs
│ │ ├── src/session_map.rs # DashMap<ChannelId, RtcSession>
│ │ ├── src/routes.rs # HTTP routes
│ │ └── static/index.html # browser test client
│ ├── rutster-media/ # str0m WebRTC + Opus⇄PCM codec boundary
│ │ ├── src/pcm.rs # PcmFrame, AudioSource/Sink traits, EchoAudioPipe
│ │ ├── src/opus_codec.rs # OpusDecoder / OpusEncoder wrappers
│ │ ├── src/rtc_session.rs # RtcSession (per-peer owner)
│ │ └── src/loop_driver.rs # str0m poll loop
│ ├── rutster-call-model/ # the Channel/Leg object embryo
│ ├── rutster-trunk/ # stub until spearhead step 5
│ ├── rutster-tap/ # stub until spearhead step 2
│ └── rutster-spend/ # stub until spearhead step 6
└── fuzz/ # placeholder cargo-fuzz dir (real harnesses: step 5)
The three stub crates (rutster-trunk, rutster-tap,
rutster-spend) exist to lock the ADR-0002 boundary shape without
anticipating code. Each is a lib.rs with a //! module doc comment
describing what will land there and when. Don't fill them in early —
see AGENTS.md "Slice-1 boundaries — what NOT to add."
Dependency direction
rutster(binary) →rutster-media,rutster-call-modelrutster-media→rutster-call-modelrutster-call-modelis a leaf (depends on nothing in the workspace)- The three stubs depend on nothing in slice 1
Build / test / lint commands
The four CI gates — all must pass before merge. Run them locally before pushing:
cargo fmt --check # formatting check
cargo clippy -- -D warnings # lints (warnings = failures)
cargo test --all # all unit + integration tests
cargo deny check # licenses, advisories, bans, sources
To auto-fix formatting + lint warnings:
cargo fmt # apply formatting
cargo clippy --fix # apply lint suggestions
cargo clippy --fix --all-features # apply across all features
Per-crate iteration
When working on one crate, skip the rest for faster cycles:
cargo test -p rutster-media # one crate's tests
cargo test -p rutster-media -- --nocapture # see println! output
cargo run -p rutster # run the binary
cargo build -p rutster-call-model # type-check one crate
cargo doc -p rutster-media --open # render one crate's docs
Render full API docs
cargo doc --no-deps --open
Every module has //! module docs explaining what it does + why it
exists in the architecture. cargo doc is genuinely useful here, not
just ceremony — the user is learning Rust from this codebase and slice 1
carries thorough educational comments per spec §7.
Running the binary
cargo run -p rutster
# listening on http://0.0.0.0:8080
Open http://localhost:8080/ in a browser → click "Start call" → grant
mic → hear yourself echo. See QUICKSTART.md for
the full walkthrough + troubleshooting.
Verbose tracing:
RUST_LOG=rutster=debug cargo run -p rutster
Filter to one module:
RUST_LOG=rutster_media::loop_driver=trace,rutster=info cargo run -p rutster
The 20 ms media loop & the "drop + observe" rule
The hot path is the str0m poll loop in
crates/rutster-media/src/loop_driver.rs. It runs every ~10 ms (tokio
interval, slice-1 deviation per spec §3.4 — step 4 lands a dedicated
timing thread).
Error policy on the hot path: never ?-propagate. Match-and-continue.
A dropped packet must not terminate the peer. Policy: "drop + observe
(log + counter), don't crash." The eventual fuzz harness (step 5) will
test against this exact posture.
Cold path (signaling, setup, request handlers) uses thiserror-derived
error enums + ? propagation, converted to HTTP status codes at the
axum boundary.
See AGENTS.md "Error handling" for the full policy.
Shipped vs. deferred
Slice 1 proves the media core only: WebRTC termination + the Opus⇄PCM codec boundary. The tap, the brain, barge-in, the trunk, and the spend cap are all explicitly deferred — each lands in a subsequent spearhead step.
The full "what's deferred + when it returns" table is in
docs/superpowers/specs/2026-06-28-slice-1-webrtc-loopback-design.md §1.2.
The short version — don't add these in slice 1:
- ❌ Dedicated timing thread (step 4)
- ❌ TLS on the HTTP signaling surface (step 5)
- ❌ Authn / authz / multi-tenancy (step 6)
- ❌ Trickle ICE (when NATs demand it)
- ❌ The tap itself (step 2 — slice 1 only pre-paves the seam)
- ❌ The brain / STT / LLM / TTS (step 3)
- ❌ Barge-in / VAD (step 4)
- ❌ PSTN trunk / SIP (step 5)
- ❌ Spend cap (step 6)
- ❌ Event bus / Valkey / CDR emission (step 5)
- ❌ Transfer / park / pickup / barge features (escalation rung 2)
- ❌ Browser automation e2e tests (post-slice-1)
- ❌ Docker / compose (later rung)
If you find yourself reaching for any of these, the right answer is "no, see slice-1 spec §1.2."
Dev loop gotchas
- libopus system dependency. The
opuscrate links system libopus via FFI. If you seeerror: linking with cc failed: exit code, installlibopus-dev(Debian/Ubuntu) /opus-devel(Fedora) /brew install opus(macOS). SeeQUICKSTART.mdfor platform-specific commands. cargo denyfirst-time setup. Runcargo install cargo-deny --lockedonce.cargo deny checkthen validates licenses, advisories, and duplicate-version bans.- Editor IDE.
rust-analyzeris recommended — it's what the plan was written against. Open the repo root in your editor;rust-analyzerpicks uprust-toolchain.tomland the workspace manifest automatically. - Slow first build. str0m + axum + tokio compile fresh on first
build (~2 minutes). Incremental builds are fast. Use
cargo check -p <crate>instead ofcargo buildfor type-check-only.