Files
rutster/docs/DEVELOPMENT.md
opencode controller 3b7b0d6459 docs: propagate rutster-trunk rename + repo URL through plans/specs/DEVELOPMENT
Mechanical propagation of the crate rename ( rutster-signaling-sip →
rutster-trunk ) and the repo URL fix ( github.com/anomalyco →
git.adlee.work/alee ) through the documents that name them:

- docs/DEVELOPMENT.md: crate-layout sketch + stub-crate description.
- docs/superpowers/plans/2026-06-28-slice-1-webrtc-loopback.md: the
  workspace members list in the plan's binding-values section —
  repository URL updated.
- docs/superpowers/plans/2026-06-28-slice-2-agent-tap.md: workspace
  members list in binding-values + crate-layout sketch in the file-
  structure section (rutster-signaling-sip → rutster-trunk).
- docs/superpowers/specs/2026-06-28-slice-2-agent-tap-design.md: §2.1
  workspace-layout sketch's STUB crate-row.

No content changes beyond the substitutions; the slice-2 spec/plan
body (protocol, TapAudioPipe, TapClient, TapEngine, lifecycle, done-
  criteria) is untouched.
2026-06-29 20:27:01 -04:00

7.2 KiB

Development

The dev loop: how to iterate on the Rust workspace, run tests, and find your way around the codebase.

Status: Slice 1 is the active build target. The workspace is landing task-by-task on the slice-1-webrtc-loopback branch. Once it merges to main, everything below applies directly. Until then, git checkout slice-1-webrtc-loopback to follow along. Implementation plan: docs/superpowers/plans/2026-06-28-slice-1-webrtc-loopback.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-model
  • rutster-mediarutster-call-model
  • rutster-call-model is 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 opus crate links system libopus via FFI. If you see error: linking with cc failed: exit code, install libopus-dev (Debian/Ubuntu) / opus-devel (Fedora) / brew install opus (macOS). See QUICKSTART.md for platform-specific commands.
  • cargo deny first-time setup. Run cargo install cargo-deny --locked once. cargo deny check then validates licenses, advisories, and duplicate-version bans.
  • Editor IDE. rust-analyzer is recommended — it's what the plan was written against. Open the repo root in your editor; rust-analyzer picks up rust-toolchain.toml and 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 of cargo build for type-check-only.