1 Commits

Author SHA1 Message Date
ce4f65eddb docs: spearhead-4half-and-5 specs + plans + kickoff prompts + AGENTS.md auto-spawn update
All checks were successful
CI / fmt (pull_request) Successful in 59s
CI / clippy (pull_request) Successful in 2m6s
CI / test (1.85) (pull_request) Successful in 4m3s
CI / test (stable) (pull_request) Successful in 4m35s
CI / deny (pull_request) Successful in 1m32s
Phase A-B planning artifacts for the spearhead-4half-and-5 release:
- Strategic plan (.omo/plans/): ADR-0010 deviation, 3-dev dispatch shape,
  auto-spawn flow via task(subagent_type=general, run_in_background=true)
- Slice 4½ spec + plan: rutster-sim crate, SimAudioPipe, LatencyProbe,
  ConcurrencyRunner, TickLagGauge, sim-bench CI gate
- Slice 5 spec + plan: G711Codec, TwilioMediaStreamsServer, TrunkSession,
  trunk_driver::drive, MediaLeg enum, CallControlClient trait
- 4 kickoff prompts (PM + dev-a/b/c) updated for auto-spawn framing
- AGENTS.md PM launch checklist item 4: auto-spawn-dev-via-task() flow

Signed-off-by: Aaron D. Lee <himself@adlee.work>
2026-07-05 03:44:32 -04:00
10 changed files with 6073 additions and 6 deletions

View File

@@ -0,0 +1,420 @@
# Strategic plan — spearhead step 4½ (benchmark + sim harness) AND step 5 (rented transport)
- **Status:** Draft (awaiting Momus review)
- **Date:** 2026-07-05
- **Spearhead footprint:** steps 4½ + 5 ([ADR-0010](../../docs/adr/0010-spearhead-benchmark-sim-harness.md) inserts 4½ between 4 and 5; [ADR-0007](../../docs/adr/0007-trunk-rented-transport.md) defines step 5)
- **Depends on (already merged):**
- slice-1 WebRTC media core (PR #1)
- slice-2 agent tap (PR #2)
- slice-3 OpenAI Realtime brain (PR #4)
- slice-4 barge-in / VAD-driven playout kill (PRs #7#13)
- slice-"5" scalability seams (librutster, NOT spearhead 5) — config.rs, event_sink.rs, MediaThread tick-lag gauge, drain lifecycle, advertised media address (PR #14)
- ADR-0009 amendment shared spend accounting (PR #15)
- **Execution paradigm:** tmux-based multi-agent PM + N-dev, relay on port 7110, dev slots driven by Kimi K2.7 worker subagents (256k context window). See AGENTS.md "Multi-agent coordination — the relay".
- **Multi-agent-kickoff role count:** 1 PM + 3 devs (dev-a / dev-b / dev-c).
- **Branch prefix:** `slice-4-half/` and `slice-5/` (slice-level, not umbrella — see Git workflow).
---
## 1. Strategic scope
Two spearhead milestones bundled into one planning pass because (a) both ship on the same fused-vertical spine, (b) step 4½'s measurement harness is the proof artifact step 5's reflex claims depend on, and (c) bundling captures one PM setup + one relay poller lifecycle + one worktree layout.
### 1.1 What each slice IS
| Slice | Spearhead step | ADR | One-sentence goal | Output crate |
|---|---|---|---|---|
| 4½ — benchmark + sim | 4½ (inserted by ADR-0010) | ADR-0010 | A self-hostable simulation harness + CI-regressed latency thresholds that *measures* slice-4's ≤60 ms barge-in kill budget at 1/10/50 concurrency, exposing the single-thread HoL block debt with data not vibes. | `crates/rutster-sim/` (new) |
| 5 — rented transport | 5 of 6 | ADR-0007 | A real phone number via rented CPaaS raw-media fork (Twilio Media Streams MVP), no first-party SIP. PSTN audio enters the same reflex loop WebRTC legs use. | `crates/rutster-trunk/` (currently a stub; fills in this slice) |
### 1.2 The ADR-0010 sequencing fork (load-bearing — surface to user before execution starts)
[ADR-0010](../../docs/adr/0010-spearhead-benchmark-sim-harness.md) explicitly re-sequences the back half of the spearhead:
> **Sequencing after 4½:** rung-2 escalation (human takeover) is pulled forward ahead of steps 56. It is the white space no competitor ships… it works entirely on WebRTC ingress (no PSTN dependency), and it is the capability that answers "why not point the trunk at OpenAI directly?" before step 5 makes that question live. Steps 5 (rented transport) and 6 (spend cap) follow, unchanged in content.
Per ADR-0010, the canonical post-4½ order is:
```
4 (merged) → 4½ (this plan) → rung-2 escalation (deferred) → step 5 (this plan) → step 6 (spend cap)
```
This plan **bundles 4½ + step 5 per the user's explicit 2026-07-05 directive** ("write a plan to cover then implement 4.5 and 5"), which pushes step 5 AHEAD of rung-2 escalation. **This is a deliberate ADR-0010 deviation**, recorded here so it isn't a silent override. Reasons the user might choose this:
- The momentum value of a real phone number is the visceral demo the spearhead has been aiming at since slice-1.
- 4½'s simulation harness already proves the wedge; escalation's white-space argument ("why not point trunk at OpenAI") waits, but doesn't degrade.
- Rung-2 escalation is genuinely greenfield (no precedence in the codebase); step 5 is a more bounded integration against an existing slice-5-seams config. Risk profile favors 5 first.
**Default action:** proceed with this plan as written (4½ + step 5). If on review the user prefers to honor ADR-0010 literally, swap step 5 → rung-2 escalation in Phase 2; everything in Phase 1 + 3 still applies (Phase 1 = both specs, Phase 3 dispatch infra is ADR-agnostic).
### 1.3 Out-of-scope for THIS strategic plan
| Item | Returns in | Why out |
|---|---|---|
| rung-2 escalation (human takeover, whisper, warm handoff) | later, post-step-5 OR per ADR-0010 (preferred ordering) | ADR-0010 says first; this plan defers per §1.2 fork |
| Spend cap / abuse gate (spearhead step 6) | later (post-step-5) | sequenced last; needs trunk to spend against |
| Layer-2 out-of-tree SBC adapter | graduation rung | ADR-0007 explicitly defers — only the layer-1 CPaaS-fork MVP ships in step 5 |
| Twilio ConversationRelay / managed Voice-AI products | never in core | ADR-0007 forbids — they consume the reflex loop that is rutster's differentiation |
| LLM-driven synthetic callers (4½ extension) | post-4½ refinement | ADR-0010 says scripted scenarios are 4½; LLM callers are a later extension |
| VAD tuning framework | post-spearhead | slice-4 §1.2 defers; 4½ still ships the const threshold |
| Inbound endpoint registration (desk phones) | never | ADR-0007 green-zone, out-of-tree, never first-party |
| Per-session media threads / threadpool shard | later rung | slice-4 §1.2 defers; 4½'s concurrency sweep surfaces the debt with data |
| Barge-in across multi-party / conferencing | later rung | slice-4 §1.2 defers |
| Docker / `compose up` | later rung | not in spearhead |
| Browser-based Playwright e2e | post-spearhead | unchanged from slice-1's deferral |
### 1.4 FOB / green-zone classification (ADR-0008 application)
**Slice 4½** — entirely FOB. The sim harness:
- Hot-path-adjacent (it drives the same 20 ms media loop)
- Differentiating (the proof artifact; the wedge's measurement claim)
- Self-hosted, memory-safe Rust ⇒ FOB
The harness MAY link `str0m` and `tokio` (mature, actively-maintained) as trusted deps — same pattern as `rutster-media`. No green-zone members introduced.
**Step 5** — split:
- FOB: the WebSocket SERVER that ingests the provider's media fork, µ-law ↔ 24 kHz PCM transcoding, the `TrunkAudioPipe` (impl `AudioPipe`) that plugs into the existing media-leg seam, the reflex-loop wiring (slice-4's `Reflex`/`LocalVadReflex` apply transparently because they decorate `AudioPipe`, agnostic to leg type). All hot-path or differentiating ⇒ FOB.
- Green zone: Twilio REST API client (call control: answer/hangup/originate), credential storage, account-SID/account-token plumbing. This is a CPaaS dependency — ADR-0007 explicit. Lives in `crates/rutster-trunk/` at arm's length: behind a `CallControlClient` trait with a `TwilioCallControlClient` impl + a `MockCallControlClient` test double. The hot path never touches Twilio's REST API. The brain never holds provider credentials (ADR-0007 / ADR-0009).
### 1.5 The seam invariant (sacred)
`crates/rutster-media/src/loop_driver.rs` + `crates/rutster-media/src/rtc_session.rs` stay byte-identical through both slices. CI pinned-blob gate (slice-4 Task 10) keeps guarding. **Every dev directive broadcasts this.** The only exception: a future ADR explicitly says otherwise.
For slice 4½: no seam risk — the harness drives via existing media-leg path.
For step 5: the `TrunkAudioPipe` plugs into the existing `MediaThread` `HashMap<ChannelId, RtcSession>` shape; the existing RtcSession codepath (for WebRTC legs) is untouched. A new `MediaCmd::RegisterTrunk` variant routes through the existing command channel; the std thread accepts both WebRTC and trunk legs.
---
## 2. Phase 1 — Design specs (parallelizable)
Two design specs land before any code. Both can be written concurrently (different domains, no shared file between them).
### 2.1 Slice 4½ spec
**Path:** `docs/superpowers/specs/2026-07-XX-slice-4-half-benchmark-sim-design.md`
**Required sections (mirror slice-4 spec skeleton):**
- §1 Scope (1.1 in / 1.2 out — see §1.2 + §1.3 of this plan)
- §2 Architecture delta (the harness reusable across slices; measurement boundary; CI thresholds)
- §3 Component design (Scenario, SimAudioPipe, LatencyProbe, ConcurrencyRunner, TickLagGauge)
- §4 Data flow (caller-speech-onset → kill-fires path; the policy "drop + observe" for hot path)
- §5 Measurement plan (the budgets: ≤60 ms kill, p50/p99 mouth-to-ear, 1/10/50 concurrency targets)
- §6 Why these decisions (local vs LLM caller; scripted scenarios first; CI gate)
- §7 Done-criteria (CI threshold tables, regression fail-the-build contract)
- §8 Open decisions (threshold values; CI bench feature flag; machine-noise normalization)
- §9 Cross-references (ADR-0010, slice-4 §5.1 budget, slice-5/seams tick-lag gauge)
**Pre-decided design calls (to lock at spec time, not negotiate during impl):**
- The harness is a `cargo test --features=sim-bench` or `cargo bench`-style entrypoint; default `cargo test --all` does NOT trigger simulated benchmarks (they're opt-in to keep CI fast for code changes).
- A SECOND CI job runs `cargo test --features=sim-bench` per PR + nightly; failure fails the build (CI gate, ADR-0010).
- Scenario format is TOML or YAML (TBD at spec time). Each scenario = serialized caller PCM segments + timing + an expectation.
- Latency measurement uses `Instant::now()` (already monotonic) and aligned ring-buffer capture (`PcmFrame`-tagged timestamps at sink/source).
- Concurrency sweep asserts **p99 kill-time ≤ 80 ms at 1 / 10 / 50 calls** (80 ms = 60 ms budget + 20 ms observer slack; ADR-0010 explicitly allows the 60 ms kill + 1 tick).
- Tick-lag gauge (already in `media_thread.rs` from slice-5/seams) is the primary readout for "is the single-thread poll-loop under tension?" Expected at 1 call: ≤2 ms lag. At 50 calls: ≤10 ms lag. >20 ms ⇒ test fails (the HoL debt is real and needs a threadpool shard, slice-4 §1.2 explicitly deferred).
### 2.2 Step 5 spec
**Path:** `docs/superpowers/specs/2026-07-XX-slice-5-rented-transport-design.md`
**Required sections:**
- §1 Scope (1.1 in / 1.2 out — see §1.2 + §1.3 of this plan)
- §2 Architecture delta (the trunk as a media-leg ingress parallel to WebRTC; µ-law↔PCM transcoding; provider REST at arm's length; the `MediaThread` extension)
- §3 Component design (TwilioMediaStreamsServer, TrunkAudioPipe, G711Codec, CallControlClient trait + TwilioCallControlClient impl)
- §4 Data flow (provider WS connect → JSON "Start" → "Media" base64 µ-law frames → decode → PcmFrame → media-leg pipe → reflex loop → encode return)
- §5 Provider abstraction (the trait + the seam: config selects `twilio` | `telnyx` | `mock`; only Twilio ships in MVP, the trait locks the seam)
- §6 Why these decisions (no SIP, ever; raw-audio fork, not managed Voice-AI; FOB vs green-zone split)
- §7 Done-criteria (PSTN sim → reflex → barge-in e2e; media-leg abstraction works for both WebRTC + trunk; credential never leaks into the brain)
- §8 Open decisions (Twilio CallCtl vs raw-WS-only; Twilio chunk size / µ-law rate negotiation; account-SID storage — env vs KMS-backed Vault sidecar)
- §9 Cross-references (ADR-0007, ADR-0008, ADR-0009 spend-accounting amendment, slice-4 §3.1 reflex layering, slice-5/seams config surface)
**Pre-decided design calls:**
- MVP provider is **Twilio Media Streams** (raw-audio WSS fork at 8 kHz µ-law). Cleaner doc'd API than Telnyx; well-trodden.
- The provider REST call-control client (`answer`/`hangup`/`originate`) is **green-zone**: behind a trait, the Twilio impl uses `reqwest`. The FOB never holds Twilio credentials directly — they live in `crates/rutster-trunk` config behind a `TwilioCredentials` struct, scoped to the trunk crate only.
- µ-law (G.711) codec: build it in-core (~30 lines, table-driven, learner-facing comments — it's a fascinating piece of telephony history). The 8 kHz ↔ 24 kHz resampling uses linear interpolation (the slice-1 PcmFrame is 24 kHz; Twilio ships 8 kHz). No new dep.
- The trunk leg participates in `MediaThread`'s `MediaCmd` enum as `RegisterTrunk { provider, credentials, reply }` + `HangupTrunk { id, reply }`. The existing `Register`/`AcceptOffer`/`Delete`/`Shutdown` variants are unchanged. The std thread accepts the registration, opens a `TrunkAudioPipe`, wires `Reflex<TapAudioPipe>` (same composition as slice-4) and puts it in the same `HashMap<ChannelId, RtcSession-or-TrunkSession>`. Actually — to keep the seam clean, theEnum value carries the leg variant.
- The `AudioPipe` trait works as-is for trunk legs. The existing `TapAudioPipe` currently routes PCM over `tx_pcm_in` / `rx_audio_out` against the tap's WebSocket; for trunk legs, **the tap WS is the trunk WS** (provider raw-audio fork = the brain's audio_out / the caller's audio_in, swapped for the PSTN caller).
Wait — that's a clean design: the trunk leg IS a TapAudioPipe where the WS endpoint is Twilio's media fork instead of OpenAI's WS endpoint. The tap protocol is replaced by Twilio's JSON framing. Different protocol on the WS, same `AudioPipe` shape on the FOB side. ✓.
### 2.3 Spec writing mode
Specs are written by the PM in the main session (not delegated). Rationale: they encode load-bearing decisions cross-referencing ADRs, prior slices, and the reflex-loop seam. Dev terminals consume specs; they don't write them.
After both specs land, both get a Momus review (one per spec — `.omo/plans/<spec-name>` invocation pattern is non-standard, see OpenCode Momus docs; use the `task(subagent_type="momus", prompt="<spec-path>")` form per AGENTS.md).
---
## 3. Phase 2 — Implementation plans
Two implementation plans, one per slice. Each derived from its spec.
### 3.1 Slice 4½ implementation plan
**Path:** `docs/superpowers/plans/2026-07-XX-slice-4-half-benchmark-sim.md`
**Tasks (Kimi-256k-sized — each fits comfortably, ≤4 files, ≤200 LOC, ≤6 tool calls per step):**
| Task # | Title | Files | Depends on | Parallelizable-now? |
|---|---|---|---|---|
| **S1** | `crates/rutster-sim/` skeleton + `Scenario`/`ScenarioStep` types + `PcmSegment` | new crate, `crates/rutster-sim/src/{lib.rs,scenario.rs}`, root `Cargo.toml` member | none | **FOUNDATION** — lands first |
| **S2** | `SimAudioPipe: AudioPipe` — scripted PCM playback on `next_pcm_frame`, captures received frames on `on_pcm_frame` (timestamps each) | `crates/rutster-sim/src/sim_audio_pipe.rs` | S1 | after S1 |
| **S3** | `LatencyProbe` — mouth-to-ear + barge-in kill-time probes; ring-buffer capture timestamps from `SimAudioPipe` | `crates/rutster-sim/src/latency.rs` | S2 | after S2 |
| **S4** | `ScenarioRunner` — drives one `SimCall` end-to-end against a target binary URL (or in-process via `MediaThread`) | `crates/rutster-sim/src/runner.rs` | S2, S3 | after S3 |
| **S5** | `ConcurrencyRunner` — N concurrent `SimCall`s, computes p50/p99 + aggregates per-call latencies | `crates/rutster-sim/src/concurrency.rs` | S4 | after S4 |
| **S6** | `TickLagGauge` — reads slice-5/seams tick-lag metric from `media_thread.rs` (already exposed via `MediaCmd::Stats`); exposes as primary readout in sweep report | `crates/rutster-sim/src/tick_lag.rs` | S5 | after S5 |
| **S7** | CI bench feature + 1/10/50 sweep thresholds — `cargo test --features=sim-bench`; CI job asserts thresholds | `.github/workflows/ci.yml`, `crates/rutster-sim/Cargo.toml` | S5, S6 | after S5 + S6 |
| **S8** | One shipped scenario pack (`scenarios/loud-barge.toml`, `scenarios/quiet-advisory.toml`, `scenarios/sustained-call.toml`) + LEARNING.md pointer | `crates/rutster-sim/scenarios/*`, `LEARNING.md` | S4 | filler; any time after S4 |
**Critical path:** S1 → S2 → S3 → S4 → S5 → S6 → S7 (linear). S8 is filler.
**Why linear:** each task consumes the prior task's types. Fanning out across this 8-task chain stalls. Instead, this slice is best executed by ONE Kimi dev (dev-a) doing the linear chain, while dev-b and dev-c work on the step-5 spec/plan + the step-5 fundamental tasks (see §3.2).
### 3.2 Step 5 implementation plan
**Path:** `docs/superpowers/plans/2026-07-XX-slice-5-rented-transport.md`
**Tasks:**
| Task # | Title | Files | Depends on | Parallelizable-now? |
|---|---|---|---|---|
| **T1** | `G711Codec` — µ-law encode/decode (table-driven, learner-commented) + 8 kHz↔24 kHz resampling | `crates/rutster-trunk/src/g711.rs`, `crates/rutster-trunk/src/lib.rs` | new `crates/rutster-trunk/Cargo.toml` deps (none — pure std) | **FOUNDATION** — lands first |
| **T2** | `CallControlClient` trait + `MockCallControlClient` (in-process test double) + `TwilioCredentials` config struct + env parser | `crates/rutster-trunk/src/provider/{mod.rs, mock.rs, twilio.rs}` | T1 (independent — provider module imports `g711` types? probably not — different file) | after T1 OR parallel |
| **T3** | `TwilioMediaStreamsServer` — accept inbound WSS connection, parse JSON Start/Media/Stop frames, decode base64 µ-law → PCM frames, encode return | `crates/rutster-trunk/src/twilio_media_streams.rs` | T1 | after T1 |
| **T4** | `TrunkAudioPipe: AudioPipe` — wraps the Twilio media-streams session as an `AudioPipe`; produces PCM frames on `next_pcm_frame` (decoded from Twilio "Media" frames), consumes PCM on `on_pcm_frame` (encoded to µ-law + sent as JSON "Media" frames back) | `crates/rutster-trunk/src/trunk_audio_pipe.rs` | T1, T3 | after T3 |
| **T5** | `MediaThread::RegisterTrunk` — extends `MediaCmd` enum (`Register`, `AcceptOffer`, `Delete`, `Shutdown`, `Stats`, `Drain` from slice-5) with `RegisterTrunk` variant; accepts a `TrunkAudioPipe`, wires `Reflex<TapAudioPipe>` (same composition as slice-4) and wraps `LocalVadReflex` outer; places in session map | `crates/rutster/src/media_thread.rs`, `crates/rutster/src/routes.rs` (new `POST /v1/sessions/trunk` route) | T4, + slice-4 (merged) + slice-5 (merged) | after T4 |
| **T6** | `TwilioCallControlClient` impl — REST API client (answer/hangup/originate); uses `reqwest`; credentials from env (slice-5/seams `config.rs` already has the parser pattern) | `crates/rutster-trunk/src/provider/twilio.rs` (extend), `crates/rutster/src/config.rs` (env parser) | T2 | parallel with T3/T4/T5 |
| **T7** | `Reflex applies to PSTN leg` — verify slice-4's `Reflex<TapAudioPipe>` + `LocalVadReflex` decorate the trunk `TrunkAudioPipe` transparently (or wire `Reflex<TrunkAudioPipe>` if the leg has its own pipe type) | `crates/rutster-trunk/tests/reflex_on_trunk.rs` | T4, T5 | after T5 |
| **T8** | PSTN sim e2e — `MockCallControlClient` + an in-process Twilio-media-streams simulator; verify barge-in fires on PSTN leg; CDR / `EventSink` emission | `crates/rutster-trunk/tests/sim_integ.rs` | T5, T7 | after T7 |
| **T9** | QUICKSTART update — env-var table for Twilio credentials; "make a real phone call" section | `docs/QUICKSTART.md`, `README.md` | T5, T6 | after T5 + T6 |
| **T10** | CI seam gate re-pin + cargo deny for any new Twilio dep | `.github/workflows/ci.yml`, `deny.toml` | T6 (introduces `reqwest` dep) | after T6 |
**Critical path:** T1 → T3 → T4 → T5 → T7 → T8. T2/T6 can start parallel with T1 if dev-b picks up T2 right away (provider trait doesn't depend on g711). T9 is filler (after T5 + T6 land). T10 is final sweep.
**Note on T2 ordering:** T2 is the spec'd out `CallControlClient` trait + mock + credential struct. It DOES NOT depend on T1 (g711 codec) — they live in different modules. Dev-b can pick up T2 in parallel with dev-c on T1.
### 3.3 Cross-slice dependency graph
```
4½ spec → 4½ plan → S1 → S2 → S3 → S4 → S5 → S6 → S7
↘ S8 (filler; parallel after S4)
5 spec → 5 plan → T1 (foundation, parallel with T2) → T3 → T4 → T5 → T7 → T8
↘ T2 → T6 ↗
T9 (filler after T5 + T6) T10 (final sweep)
```
**4½'s harness doesn't depend on step 5 code** (the sim harness drives any media leg; we'll bus it against the WebRTC ingress for the MVP latency thresholds — 4½'s tests run against WebRTC, not trunk, since slice-4 proved barge-in on WebRTC first). But the harness CAN test trunk legs once trunk lands — false dependency avoided.
**Step 5's reflex reuse depends on slice-4 (merged) — not on 4½.** So 4½ and step 5 implementation can run in PARALLEL across different dev terminals, in different worktrees.
---
## 4. Phase 3 — Multi-agent execution (tmux + Kimi K2.7 workers, 256k context)
### 4.1 Dev count + role assignment
**3 dev terminals:** `dev-a`, `dev-b`, `dev-c`. Each dev's terminal runs its own persistent agent (Claude Code or GLM session) which dispatches Kimi K2.7 Code worker subagents (`task(subagent_type="kimi-worker", load_skills=[...], run_in_background=false, prompt="...")`) for the unit-of-work tasks.
**Why Kimi-worker:** 256k context window is plenty for the per-task payloads above (each task's plan section + required reading + code ≅ 3050k tokens). Kimi K2.7 Code is cheap + capable for slice-bound file-scoped Rust work. Each dev-session holds the cross-task context (worktree state, recent file edits, relay inbox); each Kimi-worker subagent does ONE task with TDD discipline.
**Dispatch shape:**
| Dev | Slice 4½ tasks | Step-5 tasks | Worktree path |
|---|---|---|---|
| `dev-a` | S1 → S2 → S3 → S4 → S5 → S6 → S7 → S8 (the linear chain) | — | `/home/alee/Sources/rutster.slice-4-half-sim` branch `slice-4-half/sim-harness-dev-a` |
| `dev-b` | — | T2 → T6 → T9 → T10 (the provider/REST + docs + final sweep chain) | `/home/alee/Sources/rutster.slice-5-trunk` branch `slice-5/rented-transport-dev-b` |
| `dev-c` | — | T1 → T3 → T4 → T5 → T7 → T8 (the media-streams + AudioPipe + MediaThread wiring chain) | `/home/alee/Sources/rutster.slice-5-trunk-b` branch `slice-5/rented-transport-dev-c` |
**Why split step-5 across two devs:** the file-set on the FOB side (`crates/rutster-trunk/src/{g711.rs, twilio_media_streams.rs, trunk_audio_pipe.rs}` + `crates/rutster/src/media_thread.rs`) is non-overlapping with the provider/REST + config side (`crates/rutster-trunk/src/provider/*` + `crates/rutster/src/config.rs` + `docs/QUICKSTART.md`). dev-c on FOB leg wiring, dev-b on provider/config/docs — same pattern as slice-4 (dev-a on reflex stack, dev-b on tap-client + mock brain).
### 4.2 Dispatch schedule (PM-side, derived after specs + plans land)
D-Day:
```
T+0 PM terminal opens (single tmux window — start.sh --tmux opens relay + pm only),
scans .omo/plans + specs 4½ + step-5.
T+10m PM applies multi-agent-kickoff skill → generates 4 kickoff prompts
(saved under `docs/superpowers/kickoffs/2026-07-XX-spearhead-4half-5-{pm,dev-a,dev-b,dev-c}-prompt.md`).
T+15m User pastes ONLY the PM prompt into the pm window. Relay + poller already running
(PM launch checklist steps 1+2 done by the user before pasting).
T+30m PM auto-spawns dev-a/b/c via 3 `task(subagent_type="general", run_in_background=true,
prompt="<dev-prompt-file-content>")` calls. Each returns a `bg_...` task ID; PM
retains them for `background_output(task_id="bg_...")` polling.
Each dev subagent:
- cd's into its worktree (creating it per the prompt's setup bash)
- Reads plan + spec + AGENTS.md
- Dispatches `task(subagent_type="kimi-worker", ...)` per plan task
- Posts STATUS reports to the relay (`from="dev-X"`, `to="pm"`, `kind="status"`)
- Returns REVIEW-READY + tea PR URL when its slice is complete
T+1h+ PM polls each dev's `bg_...` between its own turns + drains the relay inbox for
kind=status messages. Surfaces actionable items to the user before they have to ask.
Opens PR reviews (via tea) as each dev completes a task group.
T+5h ballpark EOL: all tasks merged, final CI sweep, Momus review of implementation,
optional surfacing of any ADR-0010 escalation-vs-step-5 deviation for the user to ratify.
```
### 4.3 Per-task payload shape (what each Kimi-worker subagent receives)
Each task becomes one Kimi-worker invocation. The dev-session (holding context across tasks) calls:
```text
task(
subagent_type="kimi-worker",
load_skills=["programming"], // shared/programming — strict-types, TDD
run_in_background=false,
prompt="<task-prompt>"
)
```
**Where `<task-prompt>` is the Kimi-sized work unit.** It carries six sections (matching AGENTS.md delegation protocol):
```
TASK: <S1 / T4 / etc. — short name>
EXPECTED OUTCOME: <files touched + tests added + commit message>
REQUIRED TOOLS: <Read/Grep/Edit/Bash + cargo test scoped to crate>
MUST DO:
- Read the plan file: docs/superpowers/plans/<plan-path> -- section Task <N>
- Read AGENTS.md "Code style (Rust)" section -- learner-facing comments REQUIRED
- DCO signoff on every commit: `git commit -s`
- Hot-path policy (if touching 20 ms tick code): NO ?-propagate; match-and-continue
- Worktree: cd <dev-worktree-path> before any edit
MUST NOT DO:
- Do NOT touch crates/rutster-media/src/{loop_driver.rs,rtc_session.rs} -- seam invariant
- Do NOT cross-touch files owned by another dev (see plan: parallel-safe set per task)
- Do NOT push --force, --reset-hard, branch -D, rm -rf
- Do NOT start the next task in this subagent -- report STATUS only
CONTEXT:
- Branch: <dev-branch>
- Plan: docs/superpowers/plans/<plan-path>
- Spec: docs/superpowers/specs/<spec-path>
- Predecessor task status (if applicable): <SHA + commit message>
- Successor task waiting on this: <task name + dev role>
```
**Task size validation (Kimi 256k constraint):** each `<task-prompt>` plus its required reading (plan section, spec section, AGENTS.md relevant section) plus the dev's code-write payload fits comfortably in **≤40k tokens**. The 256k ceiling gives 6× headroom for Kimi to read more broadly (other slice docs, str0m API exploration, etc.) if it benefits the work. Empirically kimi-worker is fast at file-scoped Rust work at this size.
### 4.4 Relay + worktree layout
| Item | Value |
|---|---|
| Relay path | `~/Sources/relay` (standalone, per AGENTS.md) |
| Relay port | `7110` (rutster's port per AGENTS.md table) |
| Poller command | `setsid env RELAY_PORT=7110 python3 ~/Sources/relay/poller.py >> /tmp/relay-poller/7110/poller.log 2>&1 < /dev/null & disown` |
| `watch.sh` window | `RELAY_PORT=7110 ~/Sources/relay/watch.sh` (live tail of inbox.log + poller.log) |
| Worktrees | `/home/alee/Sources/rutster.slice-4-half-sim` (dev-a) · `/home/alee/Sources/rutster.slice-5-trunk` (dev-b) · `/home/alee/Sources/rutster.slice-5-trunk-b` (dev-c) |
| Branch scheme | `slice-4-half/sim-harness-dev-a` · `slice-5/rented-transport-dev-b` · `slice-5/rented-transport-dev-c` |
| PM does NOT own a worktree | PM only reads main + reviews PRs (per multi-agent-kickoff role boundaries) |
| PR via `tea` | `tea pulls create --head <branch> --base main --title "..." --description "..."` (per AGENTS.md Git workflow) |
| Merge strategy | squash-merge for both slices (single-PR-per-task shape, linear history); carve-out for stacked branches (slice-4-half / slice-5 branch pair) → rebase-merge if SHAs carry to a dependent sibling branch |
### 4.5 Multi-agent kickoff — files this plan hands to the kickoff skill
After both specs + both impl plans land, the kickoff skill consumes:
- `<<SPEC_PATH>>` — there are TWO specs. The kickoff skill's `<<SPEC_PATH>>` placeholder is single-valued; we substitute with **both**, comma-separated, in the PM prompt's required-reading list (the kickoff skill's templates support explicit required-reading lists per role). Dev prompts reference ONLY their own slice's spec (dev-a → 4½ spec only; dev-b/dev-c → step-5 spec only).
- `<<PLAN_LIST_BULLETS>>` for the PM:
- **Plan A:** `docs/superpowers/plans/2026-07-XX-slice-4-half-benchmark-sim.md` — slice 4½ sim harness; dev-a; S1→S8 linear; readouts feed ADR-0010's "is the wedge measured?" debt
- **Plan B:** `docs/superpowers/plans/2026-07-XX-slice-5-rented-transport.md` — step-5 rented transport; dev-b (provider+docs) + dev-c (FOB media-streams + MediaThread wiring); 10 tasks T1→T10; Twilio Media Streams MVP per ADR-0007
- `<<DEV_COUNT>>` = 3
- `<<RELEASE_LABEL>>` = `spearhead-4half-and-5`
- `<<DEV_LETTERS_LIST>>` = `A, B, C`
- `<<ALL_ROLES_LIST>>` = `pm, dev-a, dev-b, dev-c`
- `<<RELAY_PORT>>` = `7110`
- `<<PROJECT_RULES_FILE>>` = `AGENTS.md` (not CLAUDE.md — rutster uses AGENTS.md)
### 4.6 Per-dev scope summaries (for the kickoff `<<DEV_SCOPE_SUMMARY>>` placeholders)
**dev-a (slice 4½):** Implements the entire `crates/rutster-sim/` crate (currently non-existent) — Scenario/ScenarioStep types, SimAudioPipe implementing rutster-media's AudioPipe trait, LatencyProbe with ring-buffer timestamp capture, ScenarioRunner for end-to-end call simulation, ConcurrencyRunner for N-concurrent-call p50/p99 latency measurement, TickLagGauge integration with slice-5/seams MediaThread::Stats metric, and a CI-regressed bench feature gating `cargo test --features=sim-bench`. Linear chain S1→S8; ships ADR-0010's "wedge is now measured by data, not arithmetic" closure.
**dev-b (step 5, provider + docs):** Implements the green-zone side of `crates/rutster-trunk/` — the `CallControlClient` trait + `MockCallControlClient` (in-process test double) + `TwilioCallControlClient` (REST client via `reqwest`), the `TwilioCredentials` config struct + env parser wired into `crates/rutster/src/config.rs`, the QUICKSTART + README updates for "make a real phone call", and the cargo-deny + CI seam-gate re-pinning after the new `reqwest` dep. Chain T2→T6→T9→T10; tightly bounded — never touches the FOB media-stream machinery.
**dev-c (step 5, FOB media + MediaThread wiring):** Implements the FOB side of `crates/rutster-trunk/` — the in-core µ-law (G.711) codec + 8kHz↔24kHz resampling (table-driven, learner-commented), the `TwilioMediaStreamsServer` (accept inbound WSS, parse JSON Start/Media/Stop frames, base64 decode → PCM frames), the `TrunkAudioPipe: AudioPipe` (wraps the WSS as an AudioPipe that produces PCM on next_pcm_frame / consumes + encodes on on_pcm_frame), the `MediaCmd::RegisterTrunk` extension to the existing MediaThread, the reflex-on-trunk-leg verification test, and the PSTN-sim e2e integration test. Chain T1→T3→T4→T5→T7→T8; the FOB hot-path / differentiating work.
---
## 5. Risks + open forks
### 5.1 ADR-0010 deviation (the load-bearing one)
§1.2 above. The user's directive ("4.5 and 5") pushes step 5 ahead of rung-2 escalation, contradicting ADR-0010's explicit pull-forward. Surfacing at user level with proposed default: **proceed with this plan as written; ratify the deviation by amending ADR-0010 (or leave ADR-0010 intact + note the deviation on the slice-5 PR description).** ADR-0010 doesn't strictly forbid the deviation — it states a default ordering. The user's call is legitimate; this plan records it loudly.
### 5.2 Twilio account credentials in CI
Slice 5's e2e needs Twilio credentials OR a Twilio simulator. The FOB-side TwilioMediaStreamsServer is testable in-process (simulator → real WSS→ real TrunkAudioPipe → real MediaThread → real Reflex); the green-zone-side TwilioCallControlClient needs either:
- (a) `cargo test --features=twilio-live` running against real Twilio (only on maintainer-triggered runs, not per-PR)
- (b) a `MockCallControlClient` for the per-PR sweep + a manual "live override" path
Default: **(b)**. CI runs the mock. The maintainer runs live e2e manually when validating a release. Documented in QUICKSTART.
### 5.3 Reflex re-composition for trunk legs
Slice-4's `Reflex<TapAudioPipe>` composes with `LocalVadReflex` outside. For trunk legs, the AudioPipe is `TrunkAudioPipe`. The composition is `LocalVadReflex<Reflex<TrunkAudioPipe>>` — same shape, different inner. The reflex trait + decorators are pipe-agnostic by design (slice-4 §3.2); the same wrappers instantiate against the new pipe.
**Risk:** if the trunk AudioPipe has different lifecycle semantics (e.g. needs explicit WebSocket close on barge), the slice-4 `barge_in_flush` interface might be too narrow. T7 (the reflex-on-trunk verification test) surfaces this. If it triggers, STOP + post `kind=question` to PM; do not silently widen the trait.
### 5.4 CI bench feature flag interaction with the existing test matrix
Slice-4's CI matrix runs `cargo test --all` on stable + 1.85. Adding `--features=sim-bench` to a separate job is fine; just ensure the bench feature doesn't ADD a default-on dependency (it must be opt-in only, or default `cargo test --all` stays fast for routine PRs).
### 5.5 Slice-5 / "scalability seams" naming collision
The merged infra-seams slice (PR #14) is internally labeled "slice-5" in its plan path (`docs/superpowers/plans/2026-07-04-slice-5-scalability-seams.md`) but the plan itself states: "this is *not* spearhead step 5 (rented-transport trunk)." That infra slice is the *pre-paver* for spearhead step 4½ + step 5. Newcomers may be confused. Mitigation: the new step-5 plan path uses `slice-5-rented-transport` (unambiguous); the existing one stays as-is (already merged). Document the distinction in the spec's §1.
### 5.6 Single-thread contention on trunk leg
Slice-4 §1.2 explicitly defers per-session media threads / threadpool shard. Slice 4½'s concurrency sweep (target: ≤10 ms tick-lag at 50 calls) reads the gauge but the trunk leg will be the first time a "second kind" of leg shares the std thread. If contention is observed during slice-5 dev-c e2e, the slice-4-half/seams debt pays off — the threadpool-shard graduation is the answer *with data*, not doctrine. Don't pull the graduation forward speculatively.
---
## 6. Phase order summary (PM cheat-sheet)
```
Phase A — Specs (PM, in main session, parallel):
1a. Write `docs/superpowers/specs/2026-07-XX-slice-4-half-benchmark-sim-design.md`
1b. Write `docs/superpowers/specs/2026-07-XX-slice-5-rented-transport-design.md`
→ Momus review both
Phase B — Impl plans (PM, in main session, parallel):
2a. Write `docs/superpowers/plans/2026-07-XX-slice-4-half-benchmark-sim.md`
2b. Write `docs/superpowers/plans/2026-07-XX-slice-5-rented-transport.md`
→ Momus review both
Phase C — Multi-agent kickoff (PM, in main session):
3. Run `multi-agent-kickoff` skill — generate pm + dev-a + dev-b + dev-c prompts
to `docs/superpowers/kickoffs/2026-07-XX-spearhead-4half-5-{pm,dev-a,dev-b,dev-c}-prompt.md`
4. Verify relay (7110) is running; verify poller (≥3 cycle-complete lines)
Phase D — Execute (PM + dev terminals, separate windows):
5. User opens 4 terminals, pastes prompts
6. Each dev dispatches Kimi-worker subagents per task
7. PM consolidates relay inbox + opens PRs as tasks finish
8. Momus sweeps the merged stack
```
---
## 7. Stopping condition
This strategic plan is **done** when:
1. Both specs (4½ + step-5) are merged to main.
2. Both impl plans are merged to main.
3. The multi-agent-kickoff prompts exist + are committed (or the user opts to paste them ad-hoc).
4. Kimi-worker dispatch has executed every task in §3.1 + §3.2; all PRs merged.
5. CI is green on main: `cargo fmt --check`, `cargo clippy -- -D warnings`, `cargo test --all` (stable + 1.85), `cargo deny check`, the new `cargo test --all --features=sim-bench` job.
6. The seam gate still passes — `loop_driver.rs` + `rtc_session.rs` byte-identical.
7. Momus review of the merged stack passes.
8. ADR-0010 status reconciled (amend OR note deviation on the step-5 PR description).
At this point the spearhead has steps 15 complete; only step 6 (spend cap) + rung-2 escalation remain (in ADR-0010-recommended order: escalation first, then spend cap; per §1.2 fork, the user may re-confirm).
---
## 8. First action (PM-side, after plan ratification)
If this plan is ratified post-Momus:
1. Confirm the ADR-0010 deviation choice with the user (proceed as-written, swap step 5 → escalation, or amend ADR-0010).
2. Read AGENTS.md "Multi-agent coordination" + "PM-mode discipline" sections in full.
3. Start the poller if not running (verify ≥3 cycle-complete lines).
4. Begin Phase A (specs).

View File

@@ -275,6 +275,8 @@ and "why isn't X here?" questions. Consult it before adding anything.
## Multi-agent coordination — the relay (cross-model)
> **Default for routine task splitting:** prefer subagent dispatch via `task(subagent_type=..., load_skills=[...], run_in_background=...)` from the main Sisyphus session — it natively isolates worker context and saves tokens without any message-bus setup. This section applies only to the long-haul multi-dev *persistent-session* paradigm where each dev needs its own visible streaming terminal.
When more than one agent session works this repo in parallel (the PM / senior-dev "lift"
paradigm), they coordinate through a small MCP message-bus — the **relay** at
[`~/Sources/relay`](file:///home/alee/Sources/relay). One server, many terminals: each
@@ -480,16 +482,28 @@ This live-tails both `inbox.log` (dev → pm messages) and `poller.log` (poll cy
markers + nudges), so you see dev activity in real time without depending on the PM
agent surfacing it. You see what the PM sees.
**4. Bring up the PM/dev sessions (manual / tmux / kitty):**
**4. Launch the PM session (single terminal the PM auto-spawns the devs):**
```bash
~/Sources/relay/start.sh --repo ~/Sources/rutster --port 7110 --kitty # or --tmux
~/Sources/relay/start.sh --repo ~/Sources/rutster --port 7110 --tmux
# Opens a tmux session "relay-lift" with two windows: relay (the server log)
# + pm (a fresh claude session). Attach: tmux attach -t relay-lift
```
`--kitty` / `--tmux` spawns 5 terminals (relay + pm + dev-a/b/c) with the MCP relay
pre-registered for each. The generated prompts bake in `RELAY_PORT=7110` so the shims
(`call.py`) self-configure even without the env. (For `--manual`, the relaying prompts
are printed for you to paste into separate terminals.)
The user pastes ONLY the PM kickoff prompt (from
`docs/superpowers/kickoffs/<release>-pm-prompt.md`) into the `pm` window. **The PM is
responsible for spawning the 3 dev subagents** — it dispatches each as a long-running
background `task(subagent_type="general", run_in_background=true, prompt="<dev-prompt-file-
contents>")`. The dev subagents inherit the kickoff prompt as their initial context; they
read the plan + spec + their worktree, dispatch `task(subagent_type="kimi-worker", ...)` per
plan task, and stream STATUS reports back to the PM via the relay.
(Previous flow opened 5 tmux windows + required the user to manually paste 4 prompts —
one per dev session. As of 2026-07-05 the harness supports subagent spawning for users,
which collapses the manual-paste step into a single PM auto-dispatch. If the older manual
flow is preferred, `start.sh --kitty` / `--tmux` still opens the 5-window layout and the
dev prompts at `docs/superpowers/kickoffs/<release>-dev-{a,b,c}-prompt.md` can be pasted
as before.)
### PM-mode discipline (load-bearing — this was a real failure mode)

View File

@@ -0,0 +1,337 @@
# Dev A Kickoff Prompt — spearhead-4half-and-5 — Plan A (slice 4½ sim harness)
You have been spawned by the PM as a long-running background `task(subagent_type="general",
run_in_background=true, prompt="<this-file>")`. This file IS your initial context — you
inherit it verbatim. Your job: execute the slice 4½ plan + dispatch Kimi-worker subagents per
task S1-S8.
---
You are a **senior developer** owning Plan A for the `spearhead-4half-and-5` "benchmark + sim
harness + rented transport" release. Your slice is **4½ (benchmark + simulation harness in
`crates/rutster-sim/`)** — the wedge's measurement artifact, ADR-0010's load-bearing deliverable.
A PM in another terminal coordinates you with dev-b and dev-c (the step-5 rented-transport
executors — different slice, different worktree, parallel work). With the relay server running on
localhost:7110, you communicate via `post_message` / `read_messages` directly — no user
copy-paste needed.
**Kimi-worker dispatch model (load-bearing — this is how you execute the plan):** you do NOT
write the code yourself. Each task (S1 through S8) in your plan becomes ONE
`task(subagent_type="kimi-worker", load_skills=["programming"], run_in_background=false,
prompt="<task-prompt>")` invocation. The task prompt is tightly scoped:
`cd <worktree> + read plan section Task S<n> + AGENTS.md "Code style" + execute the steps +
report back STATUS`. Your job as the dev-session is to (a) draft each per-task Kimi prompt,
(b) sanity-check the returned diff + test results, (c) commit it, (d) post STATUS to the PM
relay, (e) read the PM inbox, (f) move to the next task. Kimi K2.7 has a 256k context window
— your task prompt + the plan section + the spec section + the code-under-edit fits comfortably
(<40k tokens per task).
## Setup (do this first — single bash invocation)
```bash
cd /home/alee/Sources/rutster
git fetch && git checkout main && git pull
git worktree add /home/alee/Sources/rutster.slice-4-half-sim -b slice-4-half/sim-harness-dev-a
```
Then `cd /home/alee/Sources/rutster.slice-4-half-sim && pwd` — should print
`/home/alee/Sources/rutster.slice-4-half-sim`. **ALL subsequent work happens in that worktree.**
Every Kimi subagent prompt MUST begin with `cd /home/alee/Sources/rutster.slice-4-half-sim`.
Today: 2026-07-05. Project rules in `AGENTS.md` apply (read it in full).
## Relay server
A message-bus MCP server is running on `localhost:7110`. You have three native tools:
- `post_message(from, to, kind, body)` — your `from` is always `"dev-a"`. Recipients:
`pm, dev-a, dev-b, dev-c`.
- `read_messages(for)` — drain your inbox; call with `for="dev-a"` before each task + after
every Kimi subagent returns.
- `list_pending(for)` — check inbox count without consuming.
Recipients list: `pm, dev-a, dev-b, dev-c`. Use these instead of asking the user to copy-paste.
Before starting each task: `read_messages(for="dev-a")`. After emitting any status/question:
`post_message(from="dev-a", to="pm", kind="status"|"question", body="...")`.
**Fallback if MCP tools aren't registered** — use the Python shim:
```bash
export RELAY_PORT=7110
cd ~/Sources/relay
python3 call.py post_message '{"from":"dev-a","to":"pm","kind":"status","body":"..."}'
python3 call.py read_messages '{"for":"dev-a"}'
```
**Pitfall:** single-line `body`; no embedded newlines (use ` -- ` for breaks).
## Relay polling cadence — MANDATORY (do NOT go head-down)
The #1 failure mode: a dev goes head-down dispatching Kimi subagents in sequence and never
checks the inbox — so a PM `HOLD` or `RESCOPE` is never seen and you keep banging along on a
premise the PM already changed. Do not be that dev.
**Call `read_messages(for="dev-a")` (or `list_pending(for="dev-a")` for a cheap check) at ALL of:**
- Before dispatching EACH Kimi-worker subagent — and again the moment it returns.
- Before EACH commit, and at the start + end of every task/step.
- Any time you've been heads-down on a Kimi invocation for more than a few minutes.
**An inbound `Action: HOLD` or `RESCOPE` is an interrupt, not a suggestion:** stop immediately,
do NOT dispatch the next Kimi subagent, acknowledge with a STATUS UPDATE, and comply before
resuming. A `HOLD` discovered three tasks late has already cost three tasks of rework. If
`list_pending` shows anything queued, drain it with `read_messages` and act on it before continuing.
## Required reading (in order)
1. `AGENTS.md` — the whole file. Especially: "Code style (Rust)" (learner-facing comments are
REQUIRED — this project OVERRIDES the no-comments default), "Git workflow" (DCO signoff on
every commit via `git commit -s`; squash-merge is the default; PR via `tea` not `gh`),
"Architecture pre-reading" (ADR-0002 fused vertical; ADR-0008 FOB/green-zone doctrine;
ADR-0010 spearhead re-sequencing with step 4½ inserted).
2. `docs/superpowers/specs/2026-07-05-slice-4-half-benchmark-sim-design.md` — your slice's full
design (§1 scope, §1.2 out-of-scope, §2 architecture delta, §3 component design with code
skeletons, §5 measurement plan with thresholds, §6 why-these-decisions, §7 done-criteria,
§8 open decisions).
3. `docs/superpowers/plans/2026-07-05-slice-4-half-benchmark-sim.md` — your implementation
plan; 8 Kimi-sized tasks S1-S8. Execute task-by-task; do NOT skip ahead.
4. `.omo/plans/2026-07-05-spearhead-4half-and-step-5-strategic.md` — the strategic plan that
generated this slice's existence. §1.2 records the ADR-0010 deviation. §3.1 lists your tasks;
§4.3 is your per-task payload shape template; §4.1 is your scope summary.
## Execution mode — Kimi-worker dispatch shape (per task)
For each task S<n> in the plan, your dev-session issues:
```text
task(
subagent_type="kimi-worker",
load_skills=["programming"], // shared/programming — strict-types, TDD, modern stacks
run_in_background=false,
prompt="cd /home/alee/Sources/rutster.slice-4-half-sim
TASK: S<n> — <one-line title>
EXPECTED OUTCOME: <files touched + tests added + commit message>
REQUIRED TOOLS: Read, Edit, Write, Bash (cargo test scoped to crate, cargo fmt --check, cargo clippy -- -D warnings)
MUST DO:
- Read the plan section: docs/superpowers/plans/2026-07-05-slice-4-half-benchmark-sim.md -- Task S<n> (lines X-Y)
- Read the spec section: docs/superpowers/specs/2026-07-05-slice-4-half-benchmark-sim-design.md -- §<relevant section>
- Read AGENTS.md 'Code style (Rust)' section -- learner-facing comments are REQUIRED per the project override
- DCO signoff on the commit: git commit -s -m '<message from the plan>'
- Hot-path policy: never ?-propagate on the 20 ms tick; match-and-continue
- Tests: TDD — write the failing test first; verify it fails; implement; verify it passes
- Run: cargo fmt --all --check && cargo clippy --all --all-targets -- -D warnings && cargo test --all before commit
MUST NOT DO:
- Do NOT touch crates/rutster-media/src/{loop_driver.rs,rtc_session.rs} -- seam invariant (slice-4 Task 10)
- Do NOT modify crates/rutster/src/media_thread.rs for slice 4½ (per the plan's S4 reasoning:
RegisterSim is NOT needed; the SimCall wires itself standalone in tokio. The File Structure
table in the plan is STALE on this point; S4 supersedes. If you find yourself wanting
RegisterSim, STOP + emit STATUS UPDATE -- the wiring needs design review.)
- Do NOT push --force, --reset-hard, branch -D, rm -rf
- Do NOT start the next task in this subagent -- the dev-session (you, the human-readable layer
between relays) controls task sequencing; the subagent reports STATUS only.
CONTEXT:
- Branch: slice-4-half/sim-harness-dev-a
- Plan path: docs/superpowers/plans/2026-07-05-slice-4-half-benchmark-sim.md
- Spec path: docs/superpowers/specs/2026-07-05-slice-4-half-benchmark-sim-design.md
- Predecessor task status (if applicable): <SHA + commit message>
- Successor task waiting on this: <task name + sibling-dev-role if applicable>"
)
```
## Your scope and boundaries
**In scope:** slice 4½ — the `crates/rutster-sim/` crate seed (currently non-existent; you
create it from `Cargo.toml` + `lib.rs` skeleton in S1). Concrete tasks:
- **S1** — `crates/rutster-sim/` skeleton + `Scenario`/`ScenarioStep` TOML-deserializable
types. CRITICAL-PATH FOUNDATION — every later task consumes it.
- **S2** — `SimAudioPipe: AudioPipe` + `Capture` enum. Drives a Scenario; captures
`Instant::now()` timestamps at every meaningful event. The measurement boundary (spec §2.2).
- **S3** — `LatencyProbe`. Post-hoc computation of p50/p99 kill-time + mouth-to-ear from the
Capture stream.
- **S4** — `SimCall` + `ScenarioRunner` — drives one synthetic caller end-to-end against the
FOB reflex loop. **Acknowledged under-design:** the plan's pseudocode has placeholders; you
are expected to figure out the exact wiring of the SimAudioPipe + slice-4's wrapped Reflex
stack against an in-process MockRealtimeBrain. If you hit a real design fork (e.g. the
reflex stack doesn't actually compose as the spec asserts), STOP + emit `## QUESTION TO PM`.
- **S5** — `ConcurrencyRunner` — N concurrent SimCalls against the same MediaThread; aggregates
per-call latencies into a SweepReport.
- **S6** — `TickLagGauge` — polls the slice-5/seams-merged `MediaCmd::Stats`
`MediaStats.{tick_overruns, last_tick_micros}` during the sweep; surfaces both as primary
readouts in the SweepReport. The ADR-0010 doctrine-drift detector.
- **S7** — `cargo test --all --features=sim-bench` CI job + threshold consts + the assertion
tests. A latency regression fails the build.
- **S8** — Scenario pack (`crates/rutster-sim/scenarios/loud-barge.toml`,
`quiet-advisory.toml`, `sustained-call.toml`) + `LEARNING.md` pointer. Filler — any time
after S4 landed.
**Out of scope:** everything in step-5 (`crates/rutster-trunk/`) — dev-b and dev-c own it.
You do NOT need step-5 code to land for your work — the harness drives the slice-4 WebRTC
ingress only (spec §6.3 + §8.6: in-process measurement against MockRealtimeBrain, not
client-server against the binary's HTTP surface). LLM-driven callers are deferred (post-spearhead
per ADR-0010). Network-realism / client-server mode is deferred (post-spearhead refinement).
## Hard rules
- The two seam files (`crates/rutster-media/src/loop_driver.rs` +
`crates/rutster-media/src/rtc_session.rs`) are byte-identical through this slice per slice-4
Task 10's pinned-blob CI gate. You do NOT touch them. The only file you might be tempted to
modify is `crates/rutster/src/media_thread.rs` — DON'T, per S4's standalone-path conclusion.
- **DCO signoff** on every commit: `git commit -s`. No exceptions.
- **Learner-facing comments** per AGENTS.md — including in the unit tests + threshold constants.
- DCO signoff line: `Signed-off-by: <your name> <your email>` — agents committing on behalf of
a human MUST sign off with the human's name + email (a maintenance affirmer; not the agent's
own identity). Check `git config user.name` + `git config user.email` to confirm you've
inherited the maintainer's identity, not the agent-default.
- **Hot-path policy** (if you touch any 20 ms-tick code via the SimCall driving loop, which you
will in S4): never `?`-propagate on the tick; `try_recv`/`try_send`, drop + observe + continue.
The harness measures; it doesn't crash.
- A test you cannot make green after honest debugging is a `## QUESTION TO PM` block, NOT a
fudged assertion, NOT an `#[ignore]`, NOT a `#[cfg(not(ci))]`.
- No `git push --force`, no `git reset --hard`, no `git branch -D`, no `rm -rf`, no editing
files outside your worktree, no touching `main` directly. The maintainer owns merges.
- Every Kimi subagent prompt MUST start with `cd /home/alee/Sources/rutster.slice-4-half-sim`.
## Coordination protocol
You are one of 4 terminals (PM + dev-a + dev-b + dev-c). The user relays messages via the
relay server. The user's only window into your work is what flows through this terminal + the
relay live-tail (`RELAY_PORT=7110 ~/Sources/relay/watch.sh`). Silence reads as "stuck" even
when you're cooking — narrate.
**STATUS UPDATE format** — print it LOCALLY first, then `post_message(from="dev-a", to="pm",
kind="status", body="<single-line>")`. Use this template:
```
## STATUS UPDATE — DEV-A
Time: <iso8601 like 2026-07-05T14:30:00-04:00>
Branch: slice-4-half/sim-harness-dev-a
Task: S<n> | <short title>
Status: STARTED | IN-PROGRESS | DONE | BLOCKED | REVIEW-READY
Last commit: <short sha + first line of message>
Tests: <green | red (which) | N/A>
Notes: <3 sentences max; what + why + any surprise / trade-off>
```
Emit at: setup complete, each task start, each Kimi subagent dispatch + return, each commit,
anything unexpected, REVIEW-READY. Keep `Notes` to 3 sentences max per AGENTS.md's narration
discipline.
**QUESTION TO PM** when blocked on a PM-decision: `post_message(from="dev-a", to="pm",
kind="question", body="...")` with:
```
## QUESTION TO PM — DEV-A
Time: <iso8601>
Context: <what task, what decision point>
Options: <A: ... / B: ... / C: ...>
Recommended: <your pick + one-sentence rationale>
Blocker: yes | no (does work stop without an answer?)
```
You'll receive `## DIRECTIVE TO DEV-A` from the PM via the relay; print locally + comply.
## Ship-it autonomy + simplify discipline
If the project has a `.claude/settings.json` or `.opencode/` config with broad permissions
allow + narrow destructive deny (AGENTS.md "Permissions" section if present), you can write
files, run cargo, commit, push, and open PRs without confirmation prompts. Move at speed.
**Hard guardrails (rely on the deny list, but reinforce here):** no `rm`, no `rmdir`, no
`git push --force` / `--force-with-lease`, no `git reset --hard`, no `git branch -D`, no
`git worktree remove`, no `git clean -f*`, no `git checkout -- *`, no `sudo`. If you genuinely
need one of these, surface a `## QUESTION TO PM` block.
**Speed without spaghetti — required before every REVIEW-READY:**
- Invoke the `superpowers:remove-ai-slops` skill OR the project's equivalent code-review skill
on the changed code before opening the PR.
- Do NOT create parallel implementations of an existing helper. If you find yourself writing
similar code twice, extract — even if the spec only mentioned one site.
- Do NOT add error handling, fallbacks, or validation for scenarios that can't happen.
- Default to no comments UNLESS the WHY is non-obvious — this project OVERRIDES the
convention: learner-facing comments are REQUIRED per AGENTS.md "Code style (Rust)".
- Half-finished implementations are forbidden. Ship a complete sub-task OR surface a
`## QUESTION TO PM` block.
## Authority within the plan
You don't need PM permission to:
- Execute task-to-task per the plan.
- Make implementation decisions consistent with the plan and spec.
- Write tests, refactor your own code, fix bugs you introduce.
- Push commits to your feature branch.
You DO escalate to PM when:
- A scope question outside the plan.
- A test you can't make green after honest debugging (don't fudge — debug).
- A discovered bug not in your plan.
- Anything destructive (per project rules).
- Before opening the PR for review.
## Final steps before REVIEW-READY
Run the full validation (all from a clean state in the worktree):
```bash
cargo fmt --all --check
cargo clippy --all --all-targets -- -D warnings
cargo test --all
cargo test --all --features=sim-bench -- --test-threads=1 # the NEW sim-bench threshold CI gate
cargo deny check
cargo doc --no-deps
```
If the sim-bench thresholds pass locally but flake on the CI runner (the threshold slide guard),
that's a `## QUESTION TO PM` block: surface the failed number + the proposed adjustment for
user signoff; do NOT silently bump the threshold without disclosure.
Then push and open the PR:
```bash
git push -u origin slice-4-half/sim-harness-dev-a
tea pulls create \
--head slice-4-half/sim-harness-dev-a \
--base main \
--title "slice-4½: rutster-sim seed + CI-regressed thresholds (S1-S8)" \
--description "## What lands
- New crate crates/rutster-sim/ (currently non-existent): Scenario/ScenarioStep TOML-deserializable types + SimAudioPipe: AudioPipe + LatencyProbe + SimCall + ConcurrencyRunner + TickLagGauge + threshold consts + the assertion tests + three shipped scenarios.
- A NEW CI job runs cargo test --all --features=sim-bench -- --test-threads=1 per PR: a latency regression fails the build (ADR-0010).
- The seam gate from slice-4 Task 10 stays green (loop_driver.rs + rtc_session.rs byte-identical).
## Done-criteria (spec §7)
- [ ] cargo test --all passes (stable + 1.85) — routine gate, sim-bench feature is default off.
- [ ] cargo fmt --check + cargo clippy -D warnings clean.
- [ ] cargo test --all --features=sim-bench -- --test-threads=1 passes.
- [ ] cargo deny check passes.
- [ ] cargo doc --no-deps renders the new crates/rutster-sim/ cleanly.
- [ ] Seam gate: loop_driver.rs + rtc_session.rs byte-identical.
- [ ] loud-barge.toml scenarios at [1, 10, 50] concurrency pass thresholds.
- [ ] quiet-advisory.toml at 1 concurrency passes.
- [ ] sustained-call.toml multibarge no-drift within 1.5× across barges.
- [ ] Tick-lag gauge reads MediaStats during sweep + surfaces in SweepReport.
## Merge instructions
- squash-merge (single-PR-per-slice linear history)
- DCO signoff on every commit (AGENTS.md)
- after merge: maintainer tags main: slice-4-half-e2e-green"
```
Emit a `## STATUS UPDATE` with `Status: REVIEW-READY` and the tea PR URL. Do NOT merge the PR.
## First action
After the required reading: emit a `## STATUS UPDATE` confirming setup complete (worktree
created, on `slice-4-half/sim-harness-dev-a`, strategic plan + spec + impl plan absorbed),
then start Task S1 by dispatching a Kimi-worker subagent with the per-task prompt shape above
substituted for S1 (~30-50 LOC of pure-data types + TOML deserialization, very low risk).
**On your lifecycle as a background subagent:** the PM that spawned you holds your `bg_...`
task ID. The PM polls your output via `background_output(task_id="bg_...")` between its own
turns. The relay inbox at `for="dev-a"` is your authoritative directive channel — drain it
before each Kimi-worker dispatch + after each commit. When you reach REVIEW-READY (after S7 +
S8 land + the sim-bench CI job is green + you've opened the tea PR), emit a final STATUS +
return; the PM will surface your PR URL to the user for merge approval.

View File

@@ -0,0 +1,251 @@
# Dev B Kickoff Prompt — spearhead-4half-and-5 — Plan B (step-5 provider + docs + final sweep)
You have been spawned by the PM as a long-running background `task(subagent_type="general",
run_in_background=true, prompt="<this-file>")`. This file IS your initial context — you
inherit it verbatim. Your job: execute your tasks (T2, T6, T9, T10) of the step-5 plan +
dispatch Kimi-worker subagents per task.
---
You are a **senior developer** owning Plan B's green-zone side for the `spearhead-4half-and-5`
"benchmark + sim harness + rented transport" release. Your slice is **step-5 rented
transport, GREEN-ZONE half** (`crates/rutster-trunk/src/provider/*` + `crates/rutster/src/config.rs`
env parser + `docs/QUICKSTART.md` + `README.md` + final cargo-deny + CI re-pin). Per the
strategic plan §4.1 task partition: you own tasks **T2, T6, T9, T10**.
A PM coordinates you with dev-a (slice-4½ sim harness, parallel work — different worktree,
different slice, no overlap) and dev-c (step-5 FOB side — T1/T3/T4/T5/T7/T8; same slice as
you, different file-ownership). The relay server is on `localhost:7110`.
**Kimi-worker dispatch model:** same as dev-a's — you do NOT write the code yourself. Each of
T2/T6/T9/T10 becomes ONE `task(subagent_type="kimi-worker", load_skills=["programming"],
run_in_background=false, prompt="<task-prompt>")` invocation with a tightly-scoped prompt.
## Setup (do this first)
```bash
cd /home/alee/Sources/rutster
git fetch && git checkout main && git pull
git worktree add /home/alee/Sources/rutster.slice-5-trunk -b slice-5/rented-transport-dev-b
```
Then `cd /home/alee/Sources/rutster.slice-5-trunk && pwd` — should print
`/home/alee/Sources/rutster.slice-5-trunk`. **ALL work happens in that worktree.** Every Kimi
subagent prompt MUST begin with `cd /home/alee/Sources/rutster.slice-5-trunk`.
Today: 2026-07-05. Project rules in `AGENTS.md` apply (read in full).
## Relay server + polling cadence
Same shape as dev-a's: `post_message` / `read_messages` / `list_pending`, role = `"dev-b"`.
Fallback shim `RELAY_PORT=7110 python3 ~/Sources/relay/call.py ...` if MCP tools aren't
registered. Single-line `body` (no embedded newlines).
**Relay polling cadence — MANDATORY (do NOT go head-down):**
- Before dispatching each Kimi subagent + again the moment it returns.
- Before each commit, at the start/end of every task step.
- Whenever heads-down for more than a few minutes.
- An `Action: HOLD` or `RESCOPE` is an interrupt, not a suggestion — stop, ack, comply,
before resuming.
## Required reading (in order)
1. `AGENTS.md` — full. Especially: "Architecture pre-reading" (ADR-0007 rent the trunk; ADR-0008
FOB/green-zone doctrine; ADR-0009 spend-accounting amendment — provider credentials never
reach the brain), "Git workflow" (DCO signoff, `tea` not `gh`, squash-merge),
"Slice-1 boundaries — what NOT to add (yet)" (the deferred items list).
2. `docs/superpowers/specs/2026-07-05-slice-5-rented-transport-design.md` — the step-5 spec.
Your tasks intersect §3.4 (`CallControlClient` trait + mock + TwilioCallControlClient live
impl) + §3.6 (env parser) + §3.7 (`TwilioCredentials` config) + §5 (ADR-0009 honoring) +
§6.4 (why Twilio Media Streams as MVP) + §1.2 (out-of-scope rigor).
3. `docs/superpowers/plans/2026-07-05-slice-5-rented-transport.md` — the implementation plan.
Your tasks: T2 (provider trait + mock + credentials struct), T6 (live Twilio impl + env
parser), T9 (QUICKSTART + README), T10 (CI re-pin + cargo deny). Other tasks (T1, T3, T4,
T5, T7, T8) are dev-c's.
4. `.omo/plans/2026-07-05-spearhead-4half-and-step-5-strategic.md` — the strategic plan.
§3.2 is your task list with dependencies; §4.1 is your scope summary; §1.2 is the ADR-0010
deviation record (note it in the step-5 PR description per strategic plan §7 stopping
condition).
## Execution mode — Kimi-worker dispatch shape
Per task T<n>, your dev-session issues:
```text
task(
subagent_type="kimi-worker",
load_skills=["programming"],
run_in_background=false,
prompt="cd /home/alee/Sources/rutster.slice-5-trunk
TASK: T<n> — <one-line title>
EXPECTED OUTCOME: <files touched + tests added + commit message>
REQUIRED TOOLS: Read, Edit, Write, Bash (cargo test scoped to crate, cargo fmt --check, cargo clippy -- -D warnings)
MUST DO:
- Read the plan section: docs/superpowers/plans/2026-07-05-slice-5-rented-transport.md -- Task T<n>
- Read the spec section: docs/superpowers/specs/2026-07-05-slice-5-rented-transport-design.md -- §<relevant>
- Read AGENTS.md 'Code style (Rust)' + 'Architecture pre-reading' (ADR-0007/0008/0009 must be honored)
- DCO signoff: git commit -s -m '<message from plan>'
- Tests: TDD -- failing test first, verify failure, implement, verify pass
- Run: cargo fmt --all --check && cargo clippy --all --all-targets [--features=twilio-live for T6] -- -D warnings && cargo test --all [--features=twilio-live for T6 live tests, manual run only]
MUST NOT DO:
- Do NOT touch crates/rutster-media/src/{loop_driver.rs,rtc_session.rs} -- seam invariant
- Do NOT touch crates/rutster-trunk/src/{g711.rs, twilio_media_streams.rs, session.rs, loop_driver.rs, lib.rs} -- these are dev-c's files
- Do NOT touch crates/rutster/src/media_thread.rs -- dev-c owns the MediaLeg enum + RegisterTrunk variant
- Do NOT touch crates/rutster/src/routes.rs main handlers -- dev-c owns the new /v1/trunk routes
- Do NOT push --force, --reset-hard, branch -D, rm -rf
- Do NOT start the next task in this subagent -- report STATUS only
CONTEXT:
- Branch: slice-5/rented-transport-dev-b
- Plan path: docs/superpowers/plans/2026-07-05-slice-5-rented-transport.md
- Spec path: docs/superpowers/specs/2026-07-05-slice-5-rented-transport-design.md
- dev-c's sibling tasks (the FOB side) may be landing in parallel -- don't block on them
for T2 (provider trait doesn't depend on g711); T6 may need T2's struct shape merged in
IF the worktree is on the dev-b branch alone (sync from main when dev-c's T1, T3, T4 land if
they touch shared files -- they shouldn't, but rebase forward before pushing)."
)
```
## Your scope and boundaries
**In scope:** step-5 GREEN-ZONE-side (per spec §2.2):
- **T2** — `CallControlClient` trait (async; `originate` + `hangup`), `MockCallControlClient`
(in-process test double + CI testable), `TwilioCredentials` config struct (with redacted
`Debug` impl — NEVER log `auth_token`), `CallControlError` + `SpendToken` (the pre-paved
seam for step-6 spend gate). Files: `crates/rutster-trunk/src/provider/{mod.rs, mock.rs}`.
- **T6** — `TwilioCallControlClient` (live REST impl via `reqwest`, feature-gated behind
`twilio-live`; not compiled in CI default), config env parser
`crates/rutster/src/config.rs::twilio_credentials()` returning `Option<TwilioCredentials>` per
slice-5/seams pattern (env vars: `RUTSTER_TWILIO_ACCOUNT_SID`,
`RUTSTER_TWILIO_AUTH_TOKEN`, `RUTSTER_TWILIO_MEDIA_BIND`, `RUTSTER_TWILIO_WEBHOOK_BASE`).
- **T9** — `docs/QUICKSTART.md` env-var table + "make a real phone call" walkthrough;
`README.md` spearhead-status line update.
- **T10** — `.github/workflows/ci.yml` re-pin verification (the seam hashes from slice-4
Task 10 should be UNCHANGED; if a hash drifts, dev-c is the violator — STOP + emit
QUESTION to PM). `cargo deny check` recheck for the new `reqwest` + `base64` +
`async-trait` transitive deps (plan T10 Step 2). Add a `twilio-live` manual-trigger CI
job (workflow_dispatch only — never runs per PR; maintainer triggers before release).
**Out of scope (dev-c's territory — file ownership is non-overlapping):**
- `crates/rutster-trunk/src/g711.rs`, `mulaw_decode_table.rs`, `mulaw_encode_table.rs`
- `crates/rutster-trunk/src/twilio_media_streams.rs`
- `crates/rutster-trunk/src/session.rs` (TrunkSession struct)
- `crates/rutster-trunk/src/loop_driver.rs` (trunk_driver::drive)
- `crates/rutster-trunk/src/lib.rs` (`pub mod` declarations — dev-c manages after T1; if you
need to add `pub mod provider;` first for T2, coordinate: send the addition via PM OR
push your T2 stub to your branch and let dev-c know via relay that the lib.rs edit is
yours for `provider/`. Alternative: T2 lands the `provider/` files but defers the
`pub mod provider;` declaration to T1 + dev-c — stash the `pub use` lines locally until
dev-c's T1 lands + you rebase forward to inherit the lib.rs).
- `crates/rutster/src/media_thread.rs` (MediaLeg + MediaCmd::RegisterTrunk)
- `crates/rutster/src/main.rs` (axum router mount)
- `crates/rutster/src/routes.rs` (the two new /v1/trunk routes)
The fastest path through the file-overlap on `crates/rutster-trunk/src/lib.rs`: T2 Step 2
emits a `## QUESTION TO PM` asking whether to coordinate with dev-c "I'll add `pub mod
provider;` to lib.rs in my T2 commit; you can rebase my T2 SHA forward into your T3 branch
when ready" — this is the stacked-branches case per AGENTS.md Git workflow "Merge strategy"
carve-out. Get PM signoff on the stacking shape up-front.
**Cross-dev dependency:** T6 (TwilioCallControlClient live impl) depends on T2's
`TwilioCredentials` struct + the `CallControlClient` trait. Both are yours — linear chain.
T9 is filler; land after T5 + T6 (you'll want the env vars documented after they're wired).
T10 is the final sweep; depends on T6's `reqwest` dep landing.
## Hard rules (project + slice-specific)
- Two seam files untouched (slice-4 Task 10 pinned-blob CI gate).
- **Credential isolation (ADR-0009 amendment, load-bearing for slice 6):**
`TwilioCredentials` lives ONLY in `crates/rutster-trunk/`. NEVER re-export from the
workspace root. NEVER in `rutster-media`'s public API. NEVER in `rutster-tap`'s public
API. NEVER in the brain's WS protocol. A static assertion (a unit test that imports only
your crate's public API + asserts the struct is re-exported from `rutster-trunk` and NOT
from siblings) is in scope for T10's final sweep. Spec §7 done-criteria #10 demands this.
- DCO signoff every commit: `git commit -s`, signoff identity = the human maintainer's git
config (`git config user.name` + `git config user.email` — confirm before T2's first commit).
- Learner-facing comments per AGENTS.md.
- `TwilioCredentials::Debug` MUST be hand-impl with `auth_token` redacted (do NOT derive Debug
— it would print the token in tracing/debug output).
- The `spend_token: Option<SpendToken>` parameter on `CallControlClient::originate` is the
pre-paved seam for spearhead step 6; this slice passes `None` everywhere. NEVER remove from
the trait signature "to simplify" — step 6 needs it.
- No destructive git ops without PM signoff (per project rules).
- No `git push --force`, `--reset --hard`, `branch -D`, `rm -rf`. Ask PM via `## QUESTION`.
- Every Kimi subagent prompt starts with `cd /home/alee/Sources/rutster.slice-5-trunk`.
## Coordination protocol
4 terminals (PM + dev-a + dev-b + dev-c). You post to + receive from `dev-b`. STATUS UPDATE
+ QUESTION TO PM formats identical to dev-a's (substitute `DEV-B` for `DEV-A` and
`slice-5/rented-transport-dev-b` for the branch). Print locally + post to relay.
Narration discipline per AGENTS.md.
**Cross-dev signal worth relaying specifically to PM (dev-b → PM):**
- If you discover `TwilioCredentials` would benefit from being in `crates/rutster/src/config.rs`
instead of `crates/rutster-trunk/src/provider/mod.rs` — STOP. The spec §3.4 places it in
the trunk crate on purpose; the binary imports it via the trunk crate's public API. If you
find yourself wanting to move it, emit `## QUESTION TO PM` instead.
## Final steps before REVIEW-READY
Run the full validation from the worktree:
```bash
cargo fmt --all --check
cargo clippy --all --all-targets -- -D warnings
cargo clippy --all --all-targets --features=twilio-live -- -D warnings
cargo test --all
cargo test --all --features=twilio-live -- --include-ignored # if you have live creds; else skip
cargo deny check
cargo doc --no-deps
```
Then push + open the PR via `tea`:
```bash
git push -u origin slice-5/rented-transport-dev-b
tea pulls create \
--head slice-5/rented-transport-dev-b \
--base main \
--title "slice-5 (rented transport, green-zone half): provider trait + Twilio REST + docs + final sweep (T2/T6/T9/T10)" \
--description "## What lands
- CallControlClient trait + MockCallControlClient + TwilioCallControlClient (live, twilio-live feature-gated) + TwilioCredentials redacted-Debug struct.
- config.rs env parser for RUTSTER_TWILIO_* env vars (slice-5/seams pattern continued).
- QUICKSTART env-var table + 'make a real phone call' walkthrough. README spearhead-status updated.
- cargo deny recheck for new reqwest/base64/async-trait deps. CI seam-gate re-pin verification (slice-4 Task 10 hashes UNCHANGED).
- A twilio-live manual-trigger CI job (workflow_dispatch only — never per PR).
## ADR-0009 honored (load-bearing)
- TwilioCredentials lives ONLY in crates/rutster-trunk/, NEVER re-exported through the workspace; auth_token never logged (hand-impl Debug); brain never sees credentials. Static assertion in T10 confirms.
- Option<SpendToken> on originate pre-paves the spearhead step-6 spend-cap seam.
## ADR-0010 deviation (strategic plan §1.2)
- This slice precedes rung-2 escalation per the user's 2026-07-05 directive. ADR-0010 remains intact; the deviation is recorded in .omo/plans/2026-07-05-spearhead-4half-and-step-5-strategic.md §1.2.
## Merge instructions
- rebase-merge, NOT squash — this branch shares lib.rs edits with dev-c's slice-5/rented-transport-dev-c (stacked branches per AGENTS.md Git workflow carve-out). Preserve the SHAs so dev-c can rebases forward.
- DCO signoff every commit.
- after merge: maintainer tags main: slice-5-trunk-green-zone-merged"
```
Emit `## STATUS UPDATE` with `Status: REVIEW-READY` + tea PR URL. Do NOT merge the PR —
the maintainer (user) merges after the live Twilio e2e validation (manual `cargo test
--features=twilio-live` against real credentials, which the maintainer runs after your PR is
merged + dev-c's PR is merged + a clean main checkout).
## First action
After the required reading: emit a `## STATUS UPDATE` confirming setup complete (worktree
created, on `slice-5/rented-transport-dev-b`, spec + impl plan + ADRs absorbed), then start
Task T2 by dispatching a Kimi-worker subagent with the per-task prompt shape above substituted
for T2 (the trait + mock + redacted-Debug struct — pure interface code, low risk, lands
first per the plan's critical path).
**On your lifecycle as a background subagent:** same as dev-a's — the PM holds your `bg_...`
task ID, polls your output via `background_output(task_id="bg_...")` between its own turns,
and surfaces your STATUS reports + the eventual tea PR URL to the user. Drain your relay
inbox at `for="dev-b"` before each Kimi-worker dispatch + after each commit. On REVIEW-READY
emit a final STATUS + return; PM handles PR surface + user merge approval.

View File

@@ -0,0 +1,295 @@
# Dev C Kickoff Prompt — spearhead-4half-and-5 — Plan C (step-5 FOB media + MediaThread wiring)
You have been spawned by the PM as a long-running background `task(subagent_type="general",
run_in_background=true, prompt="<this-file>")`. This file IS your initial context — you
inherit it verbatim. Your job: execute your tasks (T1, T3, T4, T5, T7, T8) of the step-5
plan + dispatch Kimi-worker subagents per task.
---
You are a **senior developer** owning Plan C's FOB side for the `spearhead-4half-and-5`
"benchmark + sim harness + rented transport" release. Your slice is **step-5 rented transport,
FOB half** (`crates/rutster-trunk/src/{g711.rs, twilio_media_streams.rs, session.rs,
loop_driver.rs}` + `crates/rutster-trunk/src/lib.rs` + `crates/rutster/src/media_thread.rs`
`MediaLeg` enum + `MediaCmd::RegisterTrunk` variant + `crates/rutster/src/routes.rs` two new
HTTP routes + `crates/rutster/src/main.rs` axum router mount). Per strategic plan §4.1: you
own tasks **T1, T3, T4, T5, T7, T8**.
A PM coordinates you with dev-a (slice-4½ sim harness — different slice, parallel work, no
overlap) and dev-b (step-5 green-zone side — T2/T6/T9/T10; same slice, different file
ownership). The relay server lives at `localhost:7110`.
**Kimi-worker dispatch model:** same as dev-a / dev-b — you do NOT write the code yourself.
Each of your tasks becomes ONE `task(subagent_type="kimi-worker",
load_skills=["programming"], run_in_background=false, prompt="<task-prompt>")` invocation.
## Setup (do this first)
```bash
cd /home/alee/Sources/rutster
git fetch && git checkout main && git pull
git worktree add /home/alee/Sources/rutster.slice-5-trunk-b -b slice-5/rented-transport-dev-c
```
Then `cd /home/alee/Sources/rutster.slice-5-trunk-b && pwd` — should print
`/home/alee/Sources/rutster.slice-5-trunk-b`. **ALL work happens in that worktree.** Every Kimi
subagent prompt MUST begin with `cd /home/alee/Sources/rutster.slice-5-trunk-b`.
Today: 2026-07-05. Project rules in `AGENTS.md` apply (read in full).
## Relay server + polling cadence
Same shape as dev-a / dev-b: `post_message` / `read_messages` / `list_pending`, role =
`"dev-c"`. Fallback shim `RELAY_PORT=7110 python3 ~/Sources/relay/call.py ...` if MCP tools
aren't registered. Single-line `body` (no embedded newlines).
**Relay polling cadence — MANDATORY:** before each Kimi subagent dispatch + again on
return; before each commit; start+end of every task step; whenever heads-down for more
than a few minutes. A `HOLD` or `RESCOPE` is an interrupt, not a suggestion.
## Required reading (in order)
1. `AGENTS.md` — full. Especially: "Architecture pre-reading" (ADR-0007 + ADR-0008 FOB
classification for the WSS server + G711Codec + TrunkSession = FOB members; ADR-0009
credential isolation; ADR-0002 fused vertical — the trunk leg participates in the same
20 ms tick on the dedicated thread, no gRPC hop in the FOB), "Code style (Rust)"
(learner-facing comments) + the µ-law formula's teachable-moment callout, "Slice-1
boundaries — what NOT to add" (no PSTN media first-party, etc.).
2. `docs/superpowers/specs/2026-07-05-slice-5-rented-transport-design.md` — the step-5 spec.
Your tasks intersect §2 architecture delta + §3 components across all of §3.1 (G711Codec)
+ §3.2 (TwilioMediaStreamsServer) + §3.3 (TrunkSession + trunk_driver::drive) + §3.5
(MediaCmd::RegisterTrunk + MediaLeg enum) + §3.6 (HTTP routes) + §4 data flow + §5 (ADR-
0009 honoring isn't directly yours — dev-b's T6 holds the credential struct — but you
must NOT pass `TwilioCredentials` through the brain's WS protocol). Read §6 (why-these-
decisions; §6.1 REUSE of slice-4's `Reflex<TapAudioPipe>` is the architecture's load-bearing
claim) + §7 done-criteria + §8 open decisions carefully.
3. `docs/superpowers/plans/2026-07-05-slice-5-rented-transport.md` — implementation plan. Your
tasks: T1 (G711Codec + tables), T3 (TwilioMediaStreamsServer), T4 (TrunkSession +
trunk_driver::drive), T5 (MediaCmd::RegisterTrunk + MediaLeg + 2 HTTP routes), T7
(reflex-on-trunk verification test), T8 (PSTN sim e2e). Other tasks (T2, T6, T9, T10) are
dev-b's.
4. `.omo/plans/2026-07-05-spearhead-4half-and-step-5-strategic.md` — strategic plan. §3.2 is
your task list with dependencies; §4.1 is your scope summary.
5. `crates/rutster/src/media_thread.rs` — the merged slice-4 + slice-5/seams `MediaThread` you
extend. Read in full before T5. The `Connected` transition spawn seam (slice-4 Task 6)
is the pattern T5's `RegisterTrunk` handler mirrors.
6. `crates/rutster-media/src/loop_driver.rs` + `rtc_session.rs` — read for shape reference
only. DO NOT TOUCH. Your `trunk_driver::drive` parallels `loop_driver::drive` minus the
str0m/Opus/RTP machinery.
## Execution mode — Kimi-worker dispatch shape
Per task T<n>, dispatch:
```text
task(
subagent_type="kimi-worker",
load_skills=["programming"],
run_in_background=false,
prompt="cd /home/alee/Sources/rutster.slice-5-trunk-b
TASK: T<n> — <one-line title>
EXPECTED OUTCOME: <files touched + tests added + commit message>
REQUIRED TOOLS: Read, Edit, Write, Bash (cargo test, cargo fmt --check, cargo clippy -- -D warnings)
MUST DO:
- Read the plan section: docs/superpowers/plans/2026-07-05-slice-5-rented-transport.md -- Task T<n>
- Read the spec section: docs/superpowers/specs/2026-07-05-slice-5-rented-transport-design.md -- §<relevant>
- Read AGENTS.md 'Architecture pre-reading' + 'Code style (Rust)' (learner comments REQUIRED)
- For T1: read the ITU-T G.711 µ-law formula reference (Wikipedia is sufficient;
https://en.wikipedia.org/wiki/%CE%9C-law_algorithm). Cross-verify the table against the spec.
- For T4: read crates/rutster-media/src/loop_driver.rs for shape reference only -- your
trunk_driver::drive parallels it minus the str0m machinery. DO NOT modify that file.
- DCO signoff: git commit -s -m '<message from plan>'
- Tests: TDD -- failing test first, verify failure, implement, verify pass.
- Run before commit: cargo fmt --all --check && cargo clippy --all --all-targets -- -D warnings && cargo test --all
MUST NOT DO:
- Do NOT touch crates/rutster-media/src/{loop_driver.rs,rtc_session.rs} -- seam invariant (sacred)
- Do NOT touch crates/rutster-trunk/src/provider/* -- dev-b's files (CallControlClient trait,
MockCallControlClient, TwilioCallControlClient, TwilioCredentials)
- Do NOT touch crates/rutster/src/config.rs -- dev-b's territory for the env parser (T6)
- Do NOT push --force, --reset --hard, branch -D, rm -rf
- Do NOT start the next task in this subagent -- report STATUS only
CONTEXT:
- Branch: slice-5/rented-transport-dev-c
- Plan path: docs/superpowers/plans/2026-07-05-slice-5-rented-transport.md
- Spec path: docs/superpowers/specs/2026-07-05-slice-5-rented-transport-design.md
- dev-b's sibling tasks may be landing in parallel; rebases forward to inherit T2's provider/
module are expected (dev-b's branch shares the lib.rs edit per the stacked-branches carve-out
in AGENTS.md Git workflow). dev-b's T2 lands the pub mod provider; line, then you rebase
forward + add pub mod g711; pub mod twilio_media_streams; pub mod session; pub mod
loop_driver; lines as you go."
)
```
## Your scope and boundaries
**In scope:** step-5 FOB side (per spec §2.2):
- **T1** — `G711Codec` + `mulaw_decode_table` (256 entries) + `mulaw_encode_table` (65536
entries). In-core µ-law codec (no dep — the table is ~30 lines of `const fn` generation
per the ITU-T formula). 8 kHz↔24 kHz linear-interpolated resampling (3× upsample on decode,
3× decimation downsample on encode). Files: `crates/rutster-trunk/src/{g711.rs,
mulaw_decode_table.rs, mulaw_encode_table.rs, lib.rs}` — first commit on this crate
beyond the stub. CRITICAL-PATH FOUNDATION; T3 + T4 consume it.
- **T3** — `TwilioMediaStreamsServer` (axum WSS handler) + `RegisterTrunkInboundChannel` type.
Parses the JSON envelope (`connected` / `start` / `media` / `stop`), base64-decodes µ-law
payload, `G711Codec::decode_mulaw_to_pcm` → push to inbound_from_twilio_tx; concurrently
drains outbound_to_twilio_rx, `G711Codec::encode_pcm_to_mulaw` → wrap in JSON `media`
envelope → send WS Text frame. File: `crates/rutster-trunk/src/twilio_media_streams.rs`
(+ register it in lib.rs).
- **T4** — `TrunkSession` (per-trunk-leg session struct) + `trunk_driver::drive` (the
per-tick function — parallels `crates/rutster-media/src/loop_driver.rs` minus str0m). Files:
`crates/rutster-trunk/src/{session.rs, loop_driver.rs}` (+ register both in lib.rs).
REUSES slice-4's `Reflex<TapAudioPipe>` + `LocalVadReflex<Reflex<TapAudioPipe>>` verbatim
(spec §6.1 — the architectural load-bearing claim). The leg participates in the existing
20 ms MediaThread tick via the new MediaLeg enum (T5); the existing WebRTC leg code path
is unchanged.
- **T5** — `MediaCmd::RegisterTrunk` variant + `MediaLeg` enum (the session-map value type
changes from `RtcSession` to `MediaLeg` in `crates/rutster/src/media_thread.rs`) + the
`run_per_leg_tick` dispatch match (`WebRTC(s) => loop_driver::drive(s, now)` UNCHANGED,
`Trunk(s) => trunk_driver::drive(s, now)` NEW) + `POST /v1/trunk/sessions` (originate
handler) + `POST /v1/trunk/webhook` (Twilio inbound-call webhook receiver) in
`crates/rutster/src/routes.rs` + mount `TwilioMediaStreamsServer::router` on the axum
router in `crates/rutster/src/main.rs`.
- **T7** — `crates/rutster-trunk/tests/reflex_on_trunk.rs` — proves slice-4's
`Reflex<TapAudioPipe>` + `LocalVadReflex` decorate the trunk leg's TapAudioPipe identically;
barge-in fires on PSTN caller speech through the same state machine as WebRTC caller speech.
- **T8** — `crates/rutster-trunk/tests/sim_integ.rs` — PSTN-sim e2e integration test.
`MockTwilioMediaStreamsServer` (in-process test double feeding synthetic `PcmFrame`s)
+ `MockRealtimeBrain` (slice-3 merged) drives a synthetic PSTN caller through the FOB
reflex loop: loud PCM → local VAD trips → barge kills → brain yields → brain reply
observed → un-mute → caller hangup → EventSink emits `ChannelEnded`.
**Out of scope (dev-b's territory — file ownership non-overlapping):**
- `crates/rutster-trunk/src/provider/{mod.rs, mock.rs, twilio.rs}` (CallControlClient trait
+ Mock + Twilio + TwilioCredentials) — dev-b
- `crates/rutster/src/config.rs` (twilio_credentials env parser) — dev-b
- `crates/rutster-trunk/Cargo.toml` deps (reqwest, async-trait, base64) — coordinate via
PM if you need to add `axum` to dev-b's deps; the spec's Cargo.toml includes them all
- `docs/QUICKSTART.md`, `README.md` — dev-b
- `.github/workflows/ci.yml` re-pin verification + `deny.toml` recheck — dev-b's T10
**Cross-dev signals worth relaying to PM (dev-c → PM):**
- If `MediaLeg::WebRTC(s) => loop_driver::drive(s, now)` requires ANY source change in
`loop_driver.rs` or `rtc_session.rs` — that's a SEAM VIOLATION. STOP + emit `## QUESTION
TO PM`. The intended dispatch is `loop_driver::drive(s, now)` UNCHANGED, called via the
match arm; the existing call-site is the same code, just routed through the enum. No
signature changes. No new code in the seam files.
- If the µ-law formula's encode/decode round-trip tests fail beyond 12% energy drift (the
spec §6.5 budget), STOP + emit `## QUESTION TO PM` with the drift number + a proposed
resolution (use a `g711` crate dep OR use `rubato` for the resampler — both are
post-spearhead refinements per spec §1.2 + §8.1, but a real regression breaks the wedge
claim; the user must decide).
- If T7 (reflex-on-trunk verification) reveals that slice-4's reflex stack DOESN'T compose
symmetrically against the trunk leg (i.e. needs a "trunk-specific" barge code path), STOP
+ emit `## QUESTION TO PM`. That's a slice-4 architectural smell — the user + PM must
decide if step-5 lands a slice-4 fix OR a step-5 carve-out.
## Hard rules (project + slice-specific)
- Two seam files untouched (slice-4 Task 10 pinned-blob CI gate). Your `trunk_driver::drive`
is in `crates/rutster-trunk/src/loop_driver.rs` — a DIFFERENT file in a DIFFERENT crate.
Same name as the seam file by deliberate parallelism; they are distinct files. If your CI
pinned-blob check fails on `loop_driver.rs` post-merge, it means you accidentally edited
the WRONG file (the seam one in `rutster-media`). Run `git checkout main --
crates/rutster-media/src/{loop_driver.rs,rtc_session.rs}` to restore + re-commit.
- **The seam invariant is sacred.** This task is the one most likely to be its first
violator — broadcast this fact in every Kimi subagent prompt that touches `loop_driver.rs`.
- **`Reflex<P>` REUSE is the architecture's load-bearing claim** (spec §6.1). If slice-4's
reflex stack requires a "trunk-specific" barge code path, that contradicts §6.1 — surface
as `## QUESTION TO PM` immediately.
- DCO signoff every commit.
- Learner-facing comments per AGENTS.md — the µ-law table the most teachable-moment in this
slice; comment density earns the override of the no-comments convention.
- Hot-path policy (you touch the 20 ms tick via `trunk_driver::drive`): never `?`-propagate;
`try_recv`/`try_send`, drop + observe + continue. A malformed Twilio JSON envelope MUST
NOT crash the std thread.
- No destructive git ops without PM signoff.
- Every Kimi subagent prompt starts with `cd /home/alee/Sources/rutster.slice-5-trunk-b`.
## Coordination protocol
4 terminals (PM + dev-a + dev-b + dev-c). You post to + receive from `dev-c`. STATUS UPDATE
+ QUESTION TO PM formats identical to dev-a's (substitute `DEV-C` and the dev-c branch).
Print locally + post to relay. Narration discipline per AGENTS.md.
## Authority within the plan
You don't need PM permission to:
- Execute task-to-task per the plan.
- Make implementation decisions consistent with the plan + spec.
- Write tests, refactor your own code, fix bugs you introduce.
- Push commits to your feature branch.
You DO escalate to PM when:
- A scope question outside the plan.
- A test you can't make green after honest debugging (don't fudge — debug).
- A discovered bug not in your plan.
- A seam violation risk (the most likely escalation for this slice).
- Before opening the PR for review.
## Final steps before REVIEW-READY
Full validation from the worktree:
```bash
cargo fmt --all --check
cargo clippy --all --all-targets -- -D warnings
cargo test --all
cargo test --all --features=twilio-live # if dev-b's T6 is merged; manual run only
cargo deny check
cargo doc --no-deps
```
Then push + open the PR:
```bash
git push -u origin slice-5/rented-transport-dev-c
tea pulls create \
--head slice-5/rented-transport-dev-c \
--base main \
--title "slice-5 (rented transport, FOB half): G711Codec + TrunkSession + trunk_driver + MediaLeg enum + reflex-on-trunk verification (T1/T3/T4/T5/T7/T8)" \
--description "## What lands
- G711Codec (in-core µ-law + 8kHz↔24kHz linear-interpolated resampling; ~30-line ITU-T formula; no dep).
- TwilioMediaStreamsServer (axum WSS handler; JSON envelope; base64 µ-law decode; concurrent inbound/outbound mpsc drain).
- TrunkSession + trunk_driver::drive (parallels loop_driver::drive minus str0m/-- REUSES slice-4's Reflex<TapAudioPipe> + LocalVadReflex verbatim per spec §6.1).
- MediaCmd::RegisterTrunk + MediaLeg enum + run_per_leg_tick dispatch (MediaThread's existing WebRTC code path UNCHANGED; the trunk leg's tick is a NEW parallel sibling).
- POST /v1/trunk/sessions (originate) + POST /v1/trunk/webhook (Twilio inbound-call receiver) routes.
- Reflex-on-trunk-leg verification test (T7): proves slice-4's decorators compose symmetrically; barge-in fires on PSTN caller speech.
- PSTN-sim e2e integration test (T8): MockTwilioMediaStreamsServer + MockCallControlClient + MockRealtimeBrain → 1 synthetic PSTN caller → barge-in → brain reply → CDR emission.
## ADR-0007 honored
- Zero SIP bytes parsed. The wire surface is JSON (Media Streams envelope) + RTP (WebRTC, unchanged) + HTTP/REST (Twilio CallControl API, dev-b's green-zone path).
## Seam invariant
- crates/rutster-media/src/{loop_driver.rs,rtc_session.rs} byte-identical. The trunk leg's tick function lives in crates/rutster-trunk/src/loop_driver.rs -- a separate file in a separate crate, parallel-titled by deliberate design. The slice-4 Task 10 pinned-blob CI gate stays green.
## ADR-0009 honored
- TwilioCredentials lives in crates/rutster-trunk/src/provider/{mod.rs} (dev-b's territory); your trunk_driver.rs and trunk_session.rs NEVER reference TwilioCredentials directly -- only the CallControlClient trait's Option<SpendToken> parameter (the trait lives in dev-b's provider/; you reference the trait, not the impl).
## Merge instructions
- rebase-merge, NOT squash -- this branch shares lib.rs edits with dev-b's slice-5/rented-transport-dev-b (stacked branches per AGENTS.md Git workflow carve-out).
- DCO signoff every commit.
- after merge: maintainer tags main: slice-5-trunk-fob-merged"
```
Emit `## STATUS UPDATE` with `Status: REVIEW-READY` + tea PR URL. Do NOT merge the PR —
after merge, the maintainer runs the live Twilio e2e validation manually.
## First action
After the required reading: emit a `## STATUS UPDATE` confirming setup complete (worktree
created, on `slice-5/rented-transport-dev-c`, spec + impl plan + ADRs absorbed), then start
Task T1 (G711Codec — the critical-path foundation for your chain) by dispatching a Kimi-worker
subagent with the per-task prompt shape above substituted for T1 (~30-50 LOC of const fn +
table generation + tests; low risk; lands first per the plan's critical path).
**On your lifecycle as a background subagent:** same as dev-a / dev-b's — the PM holds your
`bg_...` task ID, polls your output via `background_output(task_id="bg_...")` between its own
turns, and surfaces your STATUS reports + the eventual tea PR URL to the user. Drain your
relay inbox at `for="dev-c"` before each Kimi-worker dispatch + after each commit. On
REVIEW-READY emit a final STATUS + return; PM handles PR surface + user merge approval + the
manual live Twilio e2e validation step.

View File

@@ -0,0 +1,295 @@
# PM Kickoff Prompt — spearhead-4half-and-5 (benchmark + sim harness + rented transport)
Paste everything below the `---` line into a fresh terminal (Claude Code, Codex, or OpenCode)
as the first user message.
---
You are the **project manager** for the `spearhead-4half-and-5` "benchmark + sim harness + rented
transport" release. 3 senior developers report to you across separate terminals:
- **dev-a** — slice 4½ (benchmark + simulation harness in `crates/rutster-sim/`).
Plan: `docs/superpowers/plans/2026-07-05-slice-4-half-benchmark-sim.md`.
- **dev-b** — step-5 green-zone side (`crates/rutster-trunk/src/provider/*` + `config.rs` env
parser + QUICKSTART/README docs + final cargo-deny + CI re-pin).
Plan: `docs/superpowers/plans/2026-07-05-slice-5-rented-transport.md` (tasks T2, T6, T9, T10).
- **dev-c** — step-5 FOB side (`crates/rutster-trunk/src/{g711.rs, twilio_media_streams.rs,
session.rs, loop_driver.rs}` + `crates/rutster/src/media_thread.rs` `MediaLeg` enum +
`MediaCmd::RegisterTrunk` variant + the two new HTTP routes).
Plan: `docs/superpowers/plans/2026-07-05-slice-5-rented-transport.md` (tasks T1, T3, T4, T5, T7, T8).
The user runs all 4 terminals (PM + 3 dev). Each dev dispatches Kimi K2.7 worker subagents
(`task(subagent_type="kimi-worker", load_skills=["programming"], run_in_background=false, prompt="<task>")`)
per task within its plan, ONE Kimi invocation per task. Each Kimi subagent gets a tight scoped
prompt with the worktree cd-sequence + task-section-pointer + acceptance criteria; it does NOT
read the whole plan itself.
## Setup
- Working directory: `/home/alee/Sources/rutster` (the main checkout). Do NOT clone or
branch here — the dev worktrees hold the work.
- Branch: stay on `main` (the integration branch). You do NOT own a worktree — you review
PRs via `tea`, not via local edits.
- Today: 2026-07-05. Project rules in `AGENTS.md` apply (read in full before kickoff).
## Relay server
A message-bus MCP server is running on `localhost:7110`. You have three native tools:
- `post_message(from, to, kind, body)` — push a message; `from` is always `"pm"` for you.
Recipients: `pm, dev-a, dev-b, dev-c`.
- `read_messages(for)` — drain your inbox; call with `for="pm"` before each action.
- `list_pending(for)` — check inbox count without consuming.
Use these instead of asking the user to copy-paste. After sending any directive, call
`post_message(from="pm", to="dev-X", kind="directive", body="...")`.
**Fallback if MCP tools aren't registered** (this happens if the relay server was not running
when your session opened) — use the Python shim:
```bash
export RELAY_PORT=7110
cd ~/Sources/relay
python3 call.py post_message '{"from":"pm","to":"dev-a","kind":"directive","body":"..."}'
python3 call.py read_messages '{"for":"pm"}'
```
The shim connects over HTTP and has the same semantics as the MCP tools. To verify the relay
+ poller are alive (per AGENTS.md "PM session launch checklist"):
```bash
pgrep -af "poller.py" # should list the poller running for port 7110
tail -n 20 /tmp/relay-poller/7110/poller.log # should show ≥3 "poll cycle N complete" lines
tail -n 30 /tmp/relay-poller/7110/inbox.log # any dev messages drained since last PM session
```
If the poller isn't running, relaunch per AGENTS.md:
`setsid env RELAY_PORT=7110 python3 ~/Sources/relay/poller.py >> /tmp/relay-poller/7110/poller.log 2>&1 < /dev/null & disown`
**Pitfalls (avoid):**
- Single-line `body` content; periods between sentences. Strict JSON parsers in some
inbox-monitors choke on embedded `\n` literals — use ` -- ` for stronger breaks.
- Don't `cd <dir> && <command>` in shell — use the `workdir` parameter / absolute paths.
## Required reading (in order — do not skip)
1. `AGENTS.md` — the whole file. Especially: "Architecture pre-reading" (spearhead + ADRs
0002/0007/0008/0009/0010), "Git workflow" (tea CLI not gh; DCO signoff on every commit;
squash-merge default; rebase-merge carve-out for stacked branches), "Multi-agent
coordination — the relay" (the entire section — port 7110, poller, PM-mode discipline,
session handoff, PM launch checklist), "Multi-dev parallelism" (5 rules with anti-pattern
from slice-3), "Slice-1 boundaries — what NOT to add (yet)" (deferred items).
2. `.omo/plans/2026-07-05-spearhead-4half-and-step-5-strategic.md` — the strategic plan that
generates this release. §1.2 records the ADR-0010 deviation (escalation deferred behind
step 5 per user directive); §4.1 is your dispatch table; §7 is the stopping condition.
3. `docs/superpowers/specs/2026-07-05-slice-4-half-benchmark-sim-design.md` — the 4½ spec.
4. `docs/superpowers/plans/2026-07-05-slice-4-half-benchmark-sim.md` — the 4½ implementation
plan; 8 Kimi-sized tasks S1-S8.
5. `docs/superpowers/specs/2026-07-05-slice-5-rented-transport-design.md` — the step-5 spec.
6. `docs/superpowers/plans/2026-07-05-slice-5-rented-transport.md` — the step-5 implementation
plan; 10 Kimi-sized tasks T1-T10 split across dev-b and dev-c.
## Momus review verdicts (read before kickoff)
Both implementation plans have been Momus-reviewed:
- **4½ impl plan: OKAY** — non-blocking concern flagged: the File Structure table + Global
Constraints paragraph say `MediaCmd::RegisterSim` lands in `media_thread.rs`, but Task S4's
reasoning concludes RegisterSim is NOT needed (the SimCall wires itself standalone in tokio
without touching MediaThread). S4's operative instruction supersedes. Dev-a IS EXPECTED to
follow S4's standalone path and NOT modify `media_thread.rs` for this slice. If the dev
surfaces confusion via QUESTION, point them at S4 line ~976-1015.
- **Step-5 impl plan: OKAY** — non-blocking concern: `reqwest` + `url` are not yet in the
workspace's `[workspace.dependencies]` map. Plan T1 Step 1 already contains the
check-and-add instruction for dev-c; just ensure dev-c runs it before the first `cargo build`.
## Your authority
- Approve or deny scope changes from devs.
- Review + merge PRs from each dev's feature branch via **`tea pulls create --head <branch>
--base main --title ... --description ...`** + `tea pulls list` + `tea pulls merge <index>`
(AGENTS.md Git workflow; the `tea` login is `alee` against `https://git.adlee.work`).
- Drive any release-prep not in a feature plan (e.g. the live Twilio validation, ADR-0010
deviation note in the step-5 PR description, CHANGELOG/version bumps) — this is your own work.
- Tag `spearhead-4half-and-5` once everything is integrated **— but only after explicit
user approval.** The maintainer (user) owns merges of the dev-a / dev-b / dev-c PRs after
your REVIEW-COMPLETE posting.
## Your boundaries
- Don't write feature code yourself. Edits to `AGENTS.md`, `README.md`, `LEARNING.md`,
`docs/QUICKSTART.md` (the docs-after-each-PR cadence), or the ADRs are fine.
- Don't deviate from the spec without user approval. If a dev surfaces a fork (e.g.
`Reflex<P>` not actually pipe-agnostic for trunk legs per step-5 §8 ADR), STOP the dev + post
`kind=question` to the user with a one-paragraph summary + proposed default.
- Don't merge a PR until the dev says REVIEW-READY and you've run `tea pulls diff` (or
`gh pr diff` if the local tea shim doesn't have a diff subcommand) to confirm.
- Don't tag without user approval.
- Project rule: ask the user before any git-destructive op.
## Judgment calls in the plans worth flagging
The strategic plan + specs flagged these for your awareness:
- **ADR-0010 deviation** (strategic §1.2): the user's 2026-07-05 directive bundles step 4½ + 5,
pushing step 5 AHEAD of rung-2 escalation. ADR-0010 prefers escalation-first per the
original re-sequencing. The deviation is recorded in `.omo/plans/2026-07-05-spearhead-
4half-and-step-5-strategic.md` §1.2; the step-5 PR description should note it explicitly.
If the user redirects mid-execution (e.g. "switch step 5 → escalation"), tee up a new spec
pass in the main session — do NOT fork the spec inside a dev branch.
- **Slice 4½ S4 under-design** — the SimCall wiring task is explicitly acknowledged as
"needs more design than I can resolve in this plan revision." Dev-a should emit a STATUS
UPDATE if they hit a the wiring design fork; PM's job is to surface that fork to the user
with a proposed default, NOT bike-shed it in relay messages.
- **Step-5 spec §1.2 out-of-scope rigor** — a number of "out of scope" items are explicitly
never (SIP/endpoint-registration/ConversationRelay). If a dev proposes any of them, the
answer is "no, see spec §1.2."
- **Credential isolation (ADR-0009) is load-bearing** — if dev-c or dev-b proposes putting
`TwilioCredentials` in `rutster-media`'s public API OR routing it through the brain's WS
protocol, that's a hard NO.
- **Seam invariant (`loop_driver.rs` + `rtc_session.rs` byte-identical) is sacred** — broadcast
this to dev-c specifically; dev-c will be tempted to extend `loop_driver.rs` for a Twilio path.
The right answer is `trunk_driver::drive` in `crates/rutster-trunk/src/loop_driver.rs` (a
parallel-titled file in a different crate). Don't let dev-c become the seam's first violator.
If any of these conflict with your judgment, raise it with the user before kickoff.
## Coordination protocol
You are 1 of 4 terminals. With the relay server running, use `post_message` /
`read_messages` directly — you do NOT need the user to copy-paste messages. Call
`read_messages(for="pm")` before every action. Per AGENTS.md "PM-mode discipline": at the start
of EVERY turn, (a) drain pm inbox (or read `/tmp/relay-poller/7110/inbox.log` if the poller
ran), (b) `list_pending` for each dev role (`dev-a`, `dev-b`, `dev-c`), (c) `git log --oneline
--all -10` for what landed since last seen, (d) surface anything actionable BEFORE the user
asks "did you see X?"
**Narrate** to the user in plain prose between tool calls. The user's only window into the
release is the PM terminal output + the relay live-tail window (`RELAY_PORT=7110
~/Sources/relay/watch.sh`). Don't emit DIRECTIVE blocks silently. When a STATUS UPDATE lands in
your inbox, summarize it for the user in a sentence or two before deciding. When you send a
directive, state the rationale briefly so the user sees the reasoning, not just the verdict.
**You receive:** `## STATUS UPDATE — DEV-A/B/C` or `## QUESTION TO PM — DEV-A/B/C` blocks from
the relay inbox.
**You emit:** a `## DIRECTIVE TO DEV-X` block — post it via `post_message` + print it in the PM
terminal. Format:
```
## DIRECTIVE TO DEV-X
Time: <iso8601>
Action: PROCEED | HOLD | RESCOPE | REVIEW-COMPLETE | MERGE-APPROVED
Notes: <one paragraph max; single-line in the relay body, multi-line OK in the printed-out block>
Next: <one concrete instruction or "continue plan">
```
**Confirm directives are actually seen.** A head-down dev on a long-running Kimi-worker
invocation can still miss a HOLD/RESCOPE. After posting one, watch that dev's next STATUS
UPDATE for an explicit ack. If the dev keeps posting forward progress on the old premise, do
NOT assume it landed — tell the user to nudge that terminal directly. An unacknowledged HOLD
is a blocker, not sent-and-forget.
When asked "status?" by the user at any time, give a current rollup:
```
## RELEASE STATUS — spearhead-4half-and-5
dev-a (4½ S1-S8): <one-line state>
dev-b (5 T2/T6/T9/T10): <one-line state>
dev-c (5 T1/T3/T4/T5/T7/T8): <one-line state>
PM: <what you're working on>
Blockers: <list, or "none">
Next milestone: <e.g. "dev-c T5 REVIEW-READY", "live Twilio e2e validation", "tag spearhead-4half-and-5">
```
## Reviewing PRs
When a dev posts `Action: REVIEW-READY` with a PR URL (or tea PR index):
1. `tea pulls view <index>` (or `gh pr view <url>`) to read description and CI status.
2. `tea pulls diff <index>` (or `gh pr diff <url>`) to read changes.
3. Check the diff against the spec + plan acceptance criteria (the `## Final acceptance
checklist` block at the bottom of each plan).
4. If green: post `Action: MERGE-APPROVED` to the relay + tell the user "merge ready for
dev-X, awaiting your merge approval" (per "Merge strategy" in AGENTS.md — the user merges
per the gender-neutral user-preference Option 2 of "Finishing a development branch."
Default merge strategy is squash; carve-out for stacked branches T-b → dev-c via rebase-merge.
State the merge strategy in the dispatch directive so the user knows which to apply).
5. If red: post `Action: HOLD` with specific concerns the dev needs to address.
Use the `superpowers:requesting-code-review` skill OR delegate to `task(subagent_type="oracle",
prompt="review this PR; <tea URL>")` if you want a deeper independent review from a fresh
subagent before approving — recommended for dev-a's slice-4-half CI gate (asserting CI
thresholds) AND for dev-c's slice-5 seam-evidence (proving trunk_driver doesn't touch
loop_driver).
## Pre-tag checklist
Before tagging `spearhead-4half-and-5`:
- [ ] Every dev branch merged to main (squash-merge default; rebase-merge for the dev-b → dev-c
stacked-branches case in slice-5 if SHAs carry).
- [ ] CI green on main: `cargo fmt --check`, `cargo clippy -- -D warnings` (default +
`--features=twilio-live`), `cargo test --all`, `cargo deny check`, AND `cargo test --all
--features=sim-bench -- --test-threads=1` (the new slice-4½ sim-bench CI gate).
- [ ] Seam gate proves the gate: `git rev-parse main:crates/rutster-media/src/loop_driver.rs`
returns the slice-4 Task-10-pinned blob hash; same for `rtc_session.rs`. If EITHER hash
differs: this slice is in violation; STOP + ask the user. Either the dev is the seam's
first violator OR the seam gate's CI pinned-hash should be updated in this same PR with
explicit reviewer signoff + spec-rationale documentation.
- [ ] User-run live Twilio e2e (`cargo test --all --features=twilio-live` against real Twilio
credentials) succeeds once on the maintainer's machine.
- [ ] Explicit user approval to tag.
## First action
The user pasted this prompt into the `pm` window of the `relay-lift` tmux session that
`start.sh --tmux` opened. The relay + poller (step 1+2 of AGENTS.md "PM session launch
checklist") are running and the watch.sh tail is live in its side window. **You are the
single point of contact for the user — the 3 dev subagents you spawn below stream their
output through `background_output` reads + the relay; the user does NOT paste dev prompts
into separate terminals.**
1. Call `read_messages(for="pm")` to drain any early inbox messages (likely empty at session
start).
2. Verify the poller is alive per AGENTS.md (≥3 "poll cycle N complete" lines in
`/tmp/relay-poller/7110/poller.log`).
3. Emit a `## RELEASE STATUS` block confirming you've absorbed the strategic plan + 4
specs/plans. Note the judgment calls above for the user's awareness + confirm the dev-a
4½-S4 standalone SimCall path is operative (NOT the RegisterSim-in-media_thread path
from the stale File Structure table).
4. **Auto-spawn the 3 dev subagents** as long-running background tasks. Each takes its
kickoff prompt (the `dev-{a,b,c}-prompt.md` file in `docs/superpowers/kickoffs/`) as its
initial context. Use the shape:
```text
task(
subagent_type="general",
run_in_background=true,
prompt="<contents of docs/superpowers/kickoffs/2026-07-05-spearhead-4half-5-dev-a-prompt.md, paste below the --- line>"
)
```
Repeat for dev-b + dev-c, returning each `bg_...` task ID for later polling. Each dev
subagent:
- Reads its kickoff prompt as initial context (inheriting the worktree path, the plan
+ spec paths, the seam/credential isolation invariants, the Kimi-worker dispatch shape)
- cd's into its worktree (creating it if needed per the prompt's setup bash)
- Reads the plan + spec + AGENTS.md
- Dispatches `task(subagent_type="kimi-worker", load_skills=["programming"], ...)` per
plan task (S1-S8 for dev-a; T2/T6/T9/T10 for dev-b; T1/T3/T4/T5/T7/T8 for dev-c)
- Posts STATUS reports to the relay (`from="dev-X"`, `to="pm"`, `kind="status"`); the
poller drains them into `/tmp/relay-poller/7110/inbox.log` between PM turns
- Returns REVIEW-READY + tea PR URL when its slice is complete
5. After the 3 `task()` calls return their `bg_...` IDs, post opening directives to each
via the relay (`post_message(from="pm", to="dev-X", kind="directive", body="PROCEED on
task <N> per plan; <slice>-specific constraints: <...>")`). Wait for acknowledgement
STATUS UPDATEs from all 3 devs (each dev must emit one with `Status: STARTED`) before
clearing any to actually proceed — track this via `background_output(task_id="bg_...")`
OR by polling the relay inbox.
6. Quit the spawning phase + enter coordination mode: at the start of every subsequent
turn, (a) drain pm inbox (or read `/tmp/relay-poller/7110/inbox.log` if the poller ran),
(b) `list_pending` for each dev role, (c) `git log --oneline --all -10` for commits that
landed since last seen, (d) surface anything actionable to the user before they ask.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,889 @@
# Rutster slice 4½ — Benchmark + simulation harness (the `rutster-sim` seed)
- **Status:** Draft (pending review)
- **Date:** 2026-07-05
- **Spearhead step:** 4½ of 6 (inserted by [ADR-0010](../../adr/0010-spearhead-benchmark-sim-harness.md) — "after barge-in (4), before rented transport (5)")
- **Origin:** [2026-07-03 adversarial review](../../reviews/2026-07-03-adversarial-review.md) (recommendations R1+R2) + [2026-07-03 market feature scan](../../reviews/2026-07-03-market-feature-scan.md) (F1 — simulation as the standout engine-fit feature)
- **Depends on (already merged):**
- [slice 1 — WebRTC media loopback](2026-06-28-slice-1-webrtc-loopback-design.md) — the media core + the `AudioPipe` trait slice 4½ simulates against
- [slice 2 — The agent tap](2026-06-28-slice-2-agent-tap-design.md) — the `TapAudioPipe` slice 4½ measures end-to-end
- 2026-06-30 slice-3 realtime brain (merged `c30a452`) — `MockRealtimeBrain` slice 4½ drives the harness against
- [slice 4 — Barge-in / VAD-driven playout kill](2026-07-01-slice-4-barge-in-design.md) — the ≤60 ms kill budget this slice MEASURES (slice-4 §5.1)
- 2026-07-04 slice-5/seams — `MediaCmd::Stats` exposes `MediaStats { tick_overruns, last_tick_micros }` (the "tick-lag gauge" readout this harness surfaces as the primary concurrency-sweep result)
- **Related:** [ADR-0002](../../adr/0002-north-star-and-fused-core.md) (the wedge: "local real-time reflexes that don't need the brain" — currently an arithmetic claim, this slice proves it), [ADR-0008](../../adr/0008-fob-and-green-zone.md) (the harness is FOB: hot-path-adjacent + differentiating), [ARCHITECTURE.md §"Biggest technical risk"](../../ARCHITECTURE.md) (the reflex loop *is* the long pole; the harness is its measurement surface)
---
## TL;DR
Stand up spearhead step 4½: a self-hostable **benchmark + simulation harness** in a new
`crates/rutster-sim/` crate. It drives synthetic callers through the SAME media-leg path real
callers use, measures **p50/p99 mouth-to-ear latency** and **barge-in kill-time** against slice-4's
≤60 ms kill budget, and runs the same measurements at **1 / 10 / 50 concurrent synthetic calls**.
A separate CI job (opt-in via `--features=sim-bench`) asserts the per-budget thresholds every
commit. **A latency regression fails the build the same way a broken test does** — ADR-0010's
central demand.
The harness is also the **doctrine-drift detector** for the timing-thread debt (slice-4 §1.2
deferred per-session threads; slice-5/seams landed a single shared std::thread + a tick-lag gauge
without data). 4½'s concurrency sweep turns `MediaStats.tick_overruns` from a counter into a
**decision artifact**: "does single-thread poll loop breach budget at realistic concurrency?" gets
answered with data, not vibes. If yes, the dedicated threadpool-shard graduation (slice-4 deferral
#2) gets scheduled on evidence.
Three things this slice deliberately is NOT:
- **Not an LLM-driven caller** (the scenario pack is scripted PCM segments; LLM-driven callers are
post-spearhead). The harness ships with three deterministic scenarios that exercise the wedge.
- **Not a fuzz target** (the harness exercises real latency under load; it is not designed to find
panics in the PCM path — that's the deferred `cargo-fuzz` harness).
- **Not a load test for production capacity** (p99 under 50 synthetic calls is the sperhead-scale
limit; load testing for horizontally-scaled fleet deployments lands with the rung-2 escalation /
step-6 spend-cap work).
---
## 1. Scope
### 1.1 In scope
- A new `crates/rutster-sim/` crate (currently non-existent; this slice creates it from `Cargo.toml`
+ `lib.rs`).
- A **`Scenario` data type** + a TOML scenario file format (`crates/rutster-sim/scenarios/*.toml`)
describing scripted caller PCM playback with timing directives ("speak N frames, pause M frames,
interrupt at T"). At least three shipped scenarios: `loud-barge.toml`, `quiet-advisory.toml`,
`sustained-call.toml`.
- A **`SimAudioPipe: AudioPipe`** implementation that drives a `Scenario` on `next_pcm_frame`
(plays back scripted PCM frames) and captures received frames on `on_pcm_frame` (timestamps
each — the latency-measurement boundary).
- A **`LatencyProbe`** that computes the two metrics slice-4's design sets targets for:
- **Mouth-to-ear round-trip** — caller-speech-onset → first frame returned at the caller's "ear"
(the `SimAudioPipe`'s source path).
- **Barge-in kill-time** — caller-speech-onset → reflex-barge-fire (the moment `next_pcm_frame`
first returns `None` post-barge).
- A **`ConcurrencyRunner`** that spawns N concurrent `SimCall`s against a target binary URL
(default: in-process via `MediaThread` for determinism) and computes per-call latencies + the
p50/p99 aggregates across the sample.
- **Tick-lag gauge integration**: the runner reads `MediaStats { tick_overruns, last_tick_micros }`
via `MediaCmd::Stats` (already exposed by slice-5/seams) and surfaces both as primary readouts in
the sweep report — both the per-call `last_tick_micros` distribution and the cumulative
`tick_overruns` count.
- **CI-regressed thresholds**: a separate CI job runs `cargo test --all --features=sim-bench` per
PR + nightly; thresholds asserted; a regression fails the build.
- New learner-facing comments explaining the measurement discipline (the test corpus *is* the
code-reading curriculum for "how do you measure a real-time system without lying to yourself?" —
the slice-1 §7 verbosity standard carries over).
### 1.2 Out of scope (with scheduled return)
| Deferred item | Returns in | Why deferred |
|---|---|---|
| LLM-driven synthetic callers | post-spearhead refinement | ADR-0010 says scripted scenarios are 4½; LLM callers are an extension. Scripted scenarios are deterministic (reproducibility is the point); LLM callers add variance that complicates threshold-assertion CI gates. |
| Per-environment calibration of threshold values | post-spearhead | The MVP ships thresholds tuned for the dev loopback (smoke) + CI runner (deterministic box). Real-world noise calibration (per-CPU, per-RAM) is a tuning-framework problem — paired with slice-4's VAD-threshold tuning deferral. |
| Multi-perspective scenario recording (the caller's audio + the operator's audio + the brain's audio saved per-run as WAV for review) | later rung (rung-2 escalation) | Recording per-call audio for supervisor review is a rung-2 capability (warm-handoff artifact). The harness measures; recording is a separate concern. |
| Distributed / multi-binary fleet latency sweep | rung 3 (post-escalation) | ADR-0010's sweep targets a single binary at low concurrency. Fleet-scale (N binaries, NAT, autoscaling) lands after the trunk is real (step 5) + escalation is real (rung 2). |
| Adaptive `VAD_RMS_THRESHOLD` tuning | post-spearhead | Slice-4 §1.2 defers; 4½ inherits the const threshold (500.0). Real-world noise-floor learning is a later rung. |
| Concurrency > 50 calls | later rung | The spearhead's scale envelope caps at "dozens of PSTN calls on one box" (slice-4 §6.3); 50 is the upper claim. Beyond 50 ⇒ fleet (above), not single-binary. |
| Latency under degraded brain conditions (mock brain stalls) | later refinement | The MVP measures against `MockRealtimeBrain` (deterministic). The real latency risk is "what if the brain takes 800 ms instead of 300 ms?" — addressed by a "brain slow" scenario ADDED LATER, not in the MVP. |
| Hooking `cargo bench` / criterion into the CI artifact publishing | later | False precision: the harness is `cargo test --features=sim-bench`, not `cargo bench`. We are asserting threshold-shape gates, not micro-bench-diff regressions (which are noisy in CI). `criterion` is a different epistemology. |
| Browser-based e2e (Playwright/Selenium) | post-spearhead | Unchanged from prior slices' deferral. The synthetic-peer harness is the test vehicle. |
---
## 2. Architecture delta
### 2.1 What changes vs slice-4 + slice-5/seams
Slice 4½ adds ONE crate + one CI matrix entry. The fused vertical's existing hot path
(`loop_driver.rs` + `rtc_session.rs` + `MediaThread` + `Reflex<P>` + `TapAudioPipe`) is **untouched**.
The harness drives the same media-leg ingress path a real caller uses — so what it measures is what
the customer experiences, by construction.
```
┌─ crates/rutster-sim/ (NEW, this slice) ──────────┐
│ │
│ Scenario(.toml) ──► SimAudioPipe: AudioPipe │
│ │ │
│ LatencyProbe ◄── timestamps on frames passing │
│ through the pipe │
│ │
│ ConcurrencyRunner (1/10/50 SimCalls) │
│ │ │
│ └─► MediaCmd::Stats (slice-5/seams already) │
│ reads tick_overruns / last_tick_micros│
└──────────────────────┬────────────────────────────┘
│ drives via the existing
│ media-leg ingress path
┌────────────────────────────────────────────── Rutster trust boundary (FOB) ────────────┐
│ WebRTC ingress / Trunk ingress (slice-5) → RtcSession → Reflex<TapAudioPipe> │
│ ↑ ↑ ↑ ↑ │
│ └── existing, untouched ───┘ │ │ │
│ │ │ │
│ loop_driver.rs + rtc_session.rs (byte-identical) ┘ │ │
│ │ │
│ MediaThread (slice-4) ─── MediaCmd::{Register, AcceptOffer, Delete, │
│ Shutdown, Stats, Drain, RegisterTrunk[slice-5]} │
│ │
│ Tick-lag gauge: MediaStats { tick_overruns, last_tick_micros } (slice-5) │
└───────────────────────────────────────────────────────────────────────────────────────────┘
```
### 2.2 The measurement boundary (load-bearing — gets it wrong, the harness lies)
A latency measurement is honest only if the timestamp is captured at the same point the customer
experiences the audio. The harness employs TWO clocks:
- **Caller-side onset timestamp** (`t_onset`): captured inside `SimAudioPipe::on_pcm_frame` when a
scenario-step directive says "speak now." This is the wall-clock the *caller* started speaking.
- **Caller-side receipt timestamp** (`t_receipt`): captured inside `SimAudioPipe::next_pcm_frame`
when the brain's response PCM frame is returned to the "caller's ear" (the `SimAudioPipe`'s source
path returns `Some(frame)`).
Latency = `t_receipt - t_onset`. Both timestamps come from `Instant::now()` (monotonic, unaffected by
NTP) within the same `SimAudioPipe` instance — so wall-clock skew between nodes is structurally
eliminated. The harness measures *the system's response to a synthetic caller*, not "what time it
is on the operator's box vs the caller's box."
This is the design choice that distinguishes the harness from "just add some `Instant::now()` calls
in tracing." The `SimAudioPipe` IS the caller — it owns both the onset timestamp (it decided when
to "speak") and the receipt timestamp (it observed when the system "replied"). The harness can't
lie about latency because the only clock it uses is the caller's clock.
### 2.3 Why `SimAudioPipe` instead of a tracing-only approach
The alternative: instrument `loop_driver.rs` / `rtc_session.rs` / the tap with `tracing::Span`
timestamps and aggregate them post-hoc. We reject this design because:
1. **It violates the seam.** `loop_driver.rs` + `rtc_session.rs` are byte-identical seams; timing
instrumentation would either change those files (slice-1 §8.5 #6 violated) or layer on top via
decorators (complex, flaky under load).
2. **It measures wall-clock at multiple points, not customer experience.** A `tracing::Span` that
says "encode took 4 ms" tells us nothing about whether the brain's reply actually reached the
caller's ear in ≤60 ms — there's queueing + jitter + WS transport between "encode" and "heard."
3. **It produces observability dashboards, not CI gates.** Operators need dashboards (separate
concern — the `EventSink` from slice-5/seams emits per-call CDR-anchored fields); the spearhead
needs *regression-failing thresholds* stated as test assertions, which is the harness's job.
The `SimAudioPipe` is the right architectural choice for **CI-suite measurements**. Tracing
dashboards land with ADR-0005 Valkey wiring (a later rung) — both necessary, both different.
### 2.4 Why 1 / 10 / 50 (and not other values)
- **1 call** isolates the *baseline* — the cold-path latency with zero concurrency pressure. If
this regresses, the bug is in the loop itself, not in contention. Slice-4's §5.1 ≤60 ms kill budget
is asserted here.
- **10 calls** is the *warm working set.* Approximately the peak spearhead-scale; the std thread's
10 ms meta-tick comfortably fits 10 sessions per tick (each session costs <100 µs in
`loop_driver::drive`), so this asserts budget at warm-but-uncontended conditions.
- **50 calls** is the *saturation point.* ADR-0010's "single-poll-task head-of-line-blocking debt"
(review P2) lives here: 50 sessions per 10 ms tick = 200 µs per session in the meta-tick before
contention squeezes; if `last_tick_micros` stays under 10 ms the single-thread design holds; if
`tick_overruns` grows past some threshold the dedicated-threadpool-shard graduation (slice-4 §1.2
deferral) gets its data-driven case.
We do NOT test 100/500/5000 — that's fleet-scale (rung 3). 50 is the upper edge of the spearhead's
"one binary, one city" claim.
### 2.5 The CI gate shape
A regression-failing threshold is a test assertion. The crate `crates/rutster-sim/` ships:
- `#[cfg(feature = "sim-bench")]` modules + tests — default OFF, so `cargo test --all` (the routine
CI gate on every PR) stays fast (assertions of *correctness*, not measurement).
- A `cargo test --all --features=sim-bench` invocation in a SEPARATE CI job. This job runs on every
PR + nightly. Failure ⇒ red X ⇒ PR does not merge.
The thresholds are encoded as Rust `assert!` statements in `crates/rutster-sim/src/thresholds.rs`:
```rust
// crates/rutster-sim/src/thresholds.rs
/// Slice-4 spec §5.1 + §7 done-criteria #8: kill-time budget is
/// ≤60 ms (3 debounce frames × 20 ms tick) + 1 tick to drain + apply.
/// Observer slack to make CI deterministic-but-not-flaky on a slow runner:
/// effective CI assertion ≤80 ms (60 ms budget + 20 ms slack).
pub const BARGE_IN_KILL_TIME_P99_MS: f64 = 80.0;
/// Slice-1 + slice-3 mouth-to-ear budget: 200 ms (slice-1 notification) +
/// 250 ms mock brain + 100 ms playout buffer. CI assertion ceiling:
/// 700 ms (allowance for CI runner variance against dev machine).
pub const MOUTH_TO_EAR_P99_MS: f64 = 700.0;
/// Slice-5/seams tick-lag gauge: the meta-tick must stay under 10 ms
/// (the loop's nominal period). At 1 call: ≤2 ms. At 50 calls: ≤10 ms.
/// Tick overruns (count of ticks exceeding 10 ms) at p50 across the sweep:
/// ≤1% of total ticks.
pub const TICK_LAG_MAX_MS: f64 = 10.0;
pub const TICK_OVERRUN_PCT_MAX: f64 = 1.0;
/// Concurrency-sweep sample sizes.
pub const SWEEP_CONCURRENCIES: &[usize] = &[1, 10, 50];
```
These are *constants* for the MVP. They become *env-var configurable* in a post-spearhead
tuning-framework (paired with slice-4's VAD threshold tuning deferral).
---
## 3. Component design
### 3.1 `Scenario` + `ScenarioStep`
```rust
// crates/rutster-sim/src/scenario.rs
/// A scripted caller scenario. Read from TOML (a scenario file under
/// `crates/rutster-sim/scenarios/*.toml`). Deterministic by construction —
/// the entire point is reproducible thresholds in CI.
///
/// # Why TOML (not YAML, not RON)
///
/// `serde` + `toml` is already a workspace member (cargo-deny licenses track).
/// TOML keeps the scenario file readable as a one-shot script (a sequence
/// of named steps + numbers); YAML would invite flow-mapping complexity
/// the scenario format doesn't need.
#[derive(Debug, Clone, serde::Deserialize)]
pub struct Scenario {
/// Human-readable identifier; shows up in the CI failure message.
pub name: String,
/// The sequence of caller-side actions; played front-to-back.
pub steps: Vec<ScenarioStep>,
}
/// One axis of caller behavior. A scenario is a time-ordered sequence of
/// these. The `SimAudioPipe` consumes them in order during `on_pcm_frame`.
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ScenarioStep {
/// Send N loud PCM frames (sample value 1000, well above VAD_RMS_THRESHOLD=500.0).
/// Triggers the local VAD via slice-4's `LocalVadReflex::on_pcm_frame`.
SpeakLoud { frames: u32 },
/// Send N zero frames (sample value 0, well below VAD_RMS_THRESHOLD).
/// Used by the quiet-caller advisory scenario: drives mock-brain advisory path.
SpeakQuiet { frames: u32 },
/// Insert N zero frames before the next step (silence pacing).
Pause { frames: u32 },
/// Wait until the harness receives M "ear" frames before advancing.
/// Used as a barrier: the brain's reply must arrive before the next caller action.
AwaitReply { frames: u32 },
/// End the scenario. The `SimAudioPipe` returns None from next_pcm_frame thereafter.
End,
}
```
Scenario file example (`crates/rutster-sim/scenarios/loud-barge.toml`):
```toml
# Drives the PRIMARY barge-in path (slice-4 §5.1). The caller says one loud
# burst of audio; the local VAD trips; playout dies; no brain advisory needed.
# Asserts the wedge-#1 path: "VAD killing TTS the instant the caller speaks,
# without the brain."
name = "loud-barge"
[[steps]]
kind = "speak_loud"
frames = 20 # 20 frames @ 20ms = 400 ms of speech; comfortably past the 60 ms debounce
[[steps]]
kind = "await_reply"
frames = 0 # barrier: the FOB should be muted at this point (Reflex::muted == true)
[[steps]]
kind = "end"
```
### 3.2 `SimAudioPipe: AudioPipe`
```rust
// crates/rutster-sim/src/sim_audio_pipe.rs
/// A test-double `AudioPipe` that simulates a caller. Drives a `Scenario`
/// on `on_pcm_frame` (the sink path: caller speaks); receives brain
/// response frames on `next_pcm_frame` (the source path: caller hears).
/// Captures `Instant::now()` at every meaningful event for the
/// `LatencyProbe` to consume.
///
/// # Why this is THE measurement boundary
///
/// Both clocks live INSIDE this pipe. The wall-clock the *caller* started
/// speaking is captured here (we decided when to "speak"); the wall-clock
/// the *caller* heard the reply is captured here (we observed the system's
/// reply). See §2.2 — the harness can't lie about latency because the only
/// clock it uses is the caller's.
pub struct SimAudioPipe {
/// The scenario under playback.
scenario: Scenario,
/// Cursor into `scenario.steps`.
step_idx: usize,
/// Frames remaining in the current step (decrements per `on_pcm_frame`
/// for `SpeakLoud`/`SpeakQuiet`/`Pause`; freezes for `AwaitReply`).
step_frames_remaining: u32,
/// Frames received from `next_pcm_frame` while in `AwaitReply`.
/// When this reaches the step's `frames` target, advance.
reply_frames_received: u32,
/// Captured timestamps (the `LatencyProbe` consumes this via
/// `take_captures()` after the run). Discarded on every `on_pcm_frame`
/// call once the capture buffer is at capacity (bounded; hot-path).
captures: Vec<Capture>,
/// A pre-allocated frame returned from `next_pcm_frame` when we have a
/// pending reply (the harness intercepts frames routed back through the
/// existing media loop and returns them here). See §3.4 for how the
/// `SimCall` wires this pipe to a real `MediaThread`.
reply_ring: std::collections::VecDeque<PcmFrame>,
}
/// A timestamped event captured by the `SimAudioPipe`. Read by the
/// `LatencyProbe` post-run to compute p50/p99 latencies.
#[derive(Debug, Clone, Copy)]
pub enum Capture {
/// The caller started speaking loudly (a `SpeakLoud` step began).
CallerLoudOnset { at: Instant },
/// The FOB killed playout (a `next_pcm_frame` call returned None
/// immediately after a barge event). The wall-clock this slice cares
/// about for kill-time.
BargeKillObserved { at: Instant },
/// The caller heard a brain reply (a `next_pcm_frame` returned Some
/// after the barge cleared). The wall-clock this slice cares about
/// for mouth-to-ear.
CallerHeardReply { at: Instant },
}
```
#### 3.2.1 The two key methods
```rust
// crates/rutster-sim/src/sim_audio_pipe.rs (continued)
impl AudioSource for SimAudioPipe {
fn next_pcm_frame(&mut self) -> Option<PcmFrame> {
// The source path: the caller's "ear." Brain replies (frames the
// system produced on the egress side and routed back to us via the
// SimCall's mpsc wiring — see §3.4) land here.
match self.reply_ring.pop_front() {
Some(frame) => {
if self.is_in_await_reply_step() {
self.reply_frames_received += 1;
if self.reply_frames_received >= self.current_step_target() {
self.advance_step();
}
}
// Capture: this is the "caller heard" wall-clock.
self.captures.push(Capture::CallerHeardReply { at: Instant::now() });
Some(frame)
}
None => {
// The reflex muted us (slice-4's Reflex<P>::muted == true).
// Capture: this is the "barge kill observed" wall-clock.
// (Only capture if we are mid-AwaitReply post-barge; see the
// LatencyProbe's classification §3.3 for the dedup logic.)
self.captures.push(Capture::BargeKillObserved { at: Instant::now() });
None
}
}
}
}
impl AudioSink for SimAudioPipe {
fn on_pcm_frame(&mut self, _frame: PcmFrame) {
// The sink path: the caller "speaks." The scenario drives here.
// Decode the current step + emit the appropriate PCM signal.
self.dispatch_step_action();
// (We discard the inbound frame — the caller doesn't hear itself;
// the SimCall's wiring pushes the caller-side frame into the
// `tx_pcm_in` channel for the tap to forward to the brain.)
}
}
```
### 3.3 `LatencyProbe`
```rust
// crates/rutster-sim/src/latency.rs
/// Computes the two metrics slice-4's design sets budgets for, from a
/// captured-stream of `Capture` events produced by a `SimAudioPipe`.
///
/// # Verification discipline
///
/// The probe is the single source of truth for "did latency regress?"
/// Assertions are made against `LatencyProbe::p99_kill_ms()` and
/// `LatencyProbe::p99_mouth_to_ear_ms()`. A failure here is the build-red
/// signal ADR-0010 demands.
pub struct LatencyProbe {
captures: Vec<Capture>,
}
impl LatencyProbe {
pub fn from_captures(captures: Vec<Capture>) -> Self { Self { captures } }
/// Barge-in kill-time: caller-speech-onset → first `BargeKillObserved`.
/// Returns Duration per call (we capture one onset+kill pair per barge).
pub fn kill_times(&self) -> Vec<Duration> { /* pair captures */ }
/// Mouth-to-ear: caller-speech-onset → next `CallerHeardReply`.
pub fn mouth_to_ear_times(&self) -> Vec<Duration> { /* pair captures */ }
pub fn p50_kill_ms(&self) -> f64 { percentile(&self.kill_times(), 50) }
pub fn p99_kill_ms(&self) -> f64 { percentile(&self.kill_times(), 99) }
pub fn p50_mouth_to_ear_ms(&self) -> f64 { percentile(&self.mouth_to_ear_times(), 50) }
pub fn p99_mouth_to_ear_ms(&self) -> f64 { percentile(&self.mouth_to_ear_times(), 99) }
}
fn percentile(durations: &[Duration], pct: u8) -> f64 {
if durations.is_empty() { return f64::NAN; }
let mut sorted: Vec<u128> = durations.iter().map(|d| d.as_millis()).collect();
sorted.sort_unstable();
let idx = ((sorted.len() as f64 - 1.0) * (pct as f64 / 100.0)).round() as usize;
sorted[idx] as f64
}
```
The `LatencyProbe` is **post-hoc**: a single `SimCall` runs to completion, the `SimAudioPipe`'s
captures are drained, the probe computes the metrics. No per-tick instrumentation cost during the
hot path itself — just the `Instant::now()` calls inside `SimAudioPipe::next_pcm_frame` (which
isn't the hot path anyway; it's the simulated-caller epilogue).
### 3.4 `SimCall` (the wiring) + `ConcurrencyRunner`
```rust
// crates/rutster-sim/src/runner.rs
/// One simulated call: a `SimAudioPipe` + the wiring to drive it against the
/// existing `MediaThread`. Single binary; no separate process.
pub struct SimCall {
/// The scenario-driven caller pipe.
pipe: SimAudioPipe,
/// The `MediaThread` cmd_tx — we register a session, drive the pipe
/// via `tx_pcm_in`, capture frame receipts in `next_pcm_frame`.
media_cmd_tx: mpsc::Sender<MediaCmd>,
/// Latency probe populated post-run.
probe: Option<LatencyProbe>,
}
impl SimCall {
pub async fn run(mut self) -> LatencyProbe {
// 1. Register a session with the MediaThread.
// 2. Wire self.pipe as the session's AudioPipe (MediaCmd::RegisterSim).
// 3. Drive the scenario: each scenario step emits `on_pcm_frame`
// calls against the SimAudioPipe; the MediaThread's loop_driver
// echoes frames back via next_pcm_frame.
// 4. On End: drain captures + return the probe.
todo!("see §4 data flow")
}
}
/// The concurrency sweep runner. Spawns N `SimCall`s in parallel (tokio),
/// awaits all, aggregates per-call latencies into the sweep report.
pub struct ConcurrencyRunner {
/// Target binary in-process MediaThread cmd_tx. Passed in by the test fixture.
media_cmd_tx: mpsc::Sender<MediaCmd>,
/// Concurrency levels to sweep (slice-4½ hardcoded [1, 10, 50]).
concurrencies: Vec<usize>,
}
impl ConcurrencyRunner {
/// Run the full sweep; return the per-concurrency-level report.
pub async fn run(&self, scenario: Scenario) -> SweepReport { /* ... */ }
}
/// The artifact feeding the CI assertions. The thresholds.rs asserts
/// `report.barge_kill_p99_ms <= BARGE_IN_KILL_TIME_P99_MS` etc.
#[derive(Debug)]
pub struct SweepReport {
pub per_concurrency: Vec<PerConcurrencyReport>,
}
#[derive(Debug)]
pub struct PerConcurrencyReport {
pub concurrency: usize,
pub p50_kill_ms: f64,
pub p99_kill_ms: f64,
pub p50_mouth_to_ear_ms: f64,
pub p99_mouth_to_ear_ms: f64,
/// From slice-5/seams MediaCmd::Stats. The "doctrine-drift detector"
/// for the timing-thread debt — ADR-0010's debt-pairing readout.
pub max_tick_lag_micros: u64,
pub tick_overruns: u64,
pub total_ticks: u64,
pub tick_overrun_pct: f64,
}
```
### 3.5 `MediaCmd::RegisterSim` (the seam — one new enum variant)
Slice-5/seams already exists with `MediaCmd::Register`, `AcceptOffer`, `Delete`, `Shutdown`,
`Stats`, `Drain`. Slice 4½ adds ONE variant: `RegisterSim`, which lets a `SimCall` register a
session whose `AudioPipe` is a `SimAudioPipe` instead of a WebRTC-backed `RtcSession`. This is the
minimum extension to drive the harness without needing to spin up a real WebRTC peer.
```rust
// crates/rutster/src/media_thread.rs (extended this slice)
pub enum MediaCmd {
// ... existing variants (unchanged from slice-5)
/// slice-4½: harness-side session registration. The
/// `SimAudioPipe` lives entirely on the binary side; no WebRTC
/// handshake needed. The `tx_pcm_in` channel is the existing
/// sink-input seam (the harness emits `on_pcm_frame(frame)` directly
/// rather than the loop_driver pulling RTP + decoding first).
RegisterSim {
pipe: Box<dyn AudioPipe>,
reply: oneshot::Sender<ChannelId>,
},
}
```
The std thread's `run_media_thread` handles `RegisterSim` by inserting a synthetic "session" entry
that drives the harness's `SimAudioPipe` through the same `loop_driver::drive` calls as real
WebRTC sessions — the seam holds (`loop_driver.rs` + `rtc_session.rs` byte-identical). The
harness measures what real callers experience because **it drives the same code path**.
### 3.6 Tick-lag gauge integration (the doctrine-drift detector)
Slice-5/seams added `MediaStats { tick_overruns, last_tick_micros }` to `MediaCmd::Stats`.
Slice 4½ consumes both. The `ConcurrencyRunner` polls `MediaCmd::Stats` once per second during
the sweep; the per-concurrency-level report carries:
- `max_tick_lag_micros` — the maximum observed `last_tick_micros` during the sweep at this
concurrency level. Indicates "the worst tick the loop experienced."
- `tick_overruns` / `total_ticks` — cumulative count of ticks where `last_tick_micros > 10_000`
(10 ms); converts to percent. Indicates "what fraction of ticks overflowed."
The thresholds (`TICK_LAG_MAX_MS = 10.0`, `TICK_OVERRUN_PCT_MAX = 1.0`) are the answer to ADR-0010's
"if the concurrency sweep shows the shared-tokio poll loop breaching budget at realistic call
counts, the dedicated-timing-thread work gets scheduled on data, not vibes." If slice 4½'s sweep
shows `tick_overrun_pct > 1.0` at 50 calls, **the FOB reflex loop's single-thread debt is real and
graduates from doctrine to data** — that's the slice's load-bearing finding regardless of whether
the latency thresholds pass.
---
## 4. Data flow
### 4.1 Single `SimCall` (the unit of measurement)
```
1. SimCall::run() starts.
2. Send MediaCmd::RegisterSim { pipe: Box<SimAudioPipe>, reply } to media_cmd_tx.
3. The MediaThread handles RegisterSim:
- Constructs a "synthetic session" entry in its HashMap.
- The session's `pipe` field is the SimAudioPipe.
- Subsequent loop_driver::drive(now) calls touch this session identically
to a WebRTC session (the seam holds).
4. The harness drives the scenario:
- For each SpeakLoud/SpeakQuiet step: the SimCall emits `pipe.on_pcm_frame(frame)` calls
at the 20 ms tick cadence. loop_driver::drive's encode path is bypassed for the SimPipe
(we wrote the frame directly into the pipe).
- loop_driver::drive immediately treats the next_pcm_frame call as the source path: it
pulls from the SimPipe's reply_ring (where brain replies populate when the SimCall
sees them via the tap/engine path).
5. Brain replies (from MockRealtimeBrain or the in-process tap):
- Routed back into the SimPipe's reply_ring via an mpsc the SimCall holds.
- loop_driver::drive picks them up on next_pcm_frame, encodes if real-WebRTC, but for sim
we just observe — captures CallerHeardReply timestamp.
6. Reflex barge-in (slice-4 already merged):
- If the SimPipe emitted SpeakLoud frames, the LocalVadReflex<P> (which wraps the pipe
in the session_map/MediaThread composition site, slice-4 Task 6) trips the SpeechStarted
advisory → Reflex<P>::muted = true → next_pcm_frame returns None → capture
BargeKillObserved timestamp.
7. On End step: harness stops driving, returns LatencyProbe.
```
### 4.2 Concurrency sweep (the doctrine detector)
```
1. ConcurrencyRunner::run(scenario) launches N Tokio tasks, each running SimCall::run
against the SAME media_cmd_tx (shared media thread).
2. The MediaThread drives ALL N sessions per 10 ms meta-tick (slice-4 §2.2 unchanged shape).
3. Per second during the sweep: ConcurrencyRunner fires MediaCmd::Stats; accumulates
tick_overruns + last_tick_micros samples.
4. On all SimCalls completing: aggregate per-call LatencyProbes → p50/p99 vector.
5. Build SweepReport with per-concurrency rows, asserting thresholds.rs constants.
```
### 4.3 Why in-process (not client-server)
The harness DOES NOT stand up the binary as a server + a separate sim-client process. Why:
1. **Determinism.** Loopback within the same process eliminates websocket / socket / TCP jitter as
a confounder. The thresholds are assertions about the FOB reflex loop itself — not assertions
about NTPD variance on the CI runner.
2. **CI simplicity.** `cargo test --features=sim-bench` runs in-process; no port binding, no
test-orchestration container, no race against `epoll` initialization.
3. **Direct seam access.** The harness can construct a `SimAudioPipe` and ship it via
`MediaCmd::RegisterSim` directly — same path the production binary would use if it had a
"synthetic caller" feature, no client-server glue needed.
A separate client-server mode (true loopback against the binary's HTTP/WebSocket surface) IS
deferred — it's needed when the harness gains LLM-driven callers (post-spearhead refinement) and
needs network realism.
---
## 5. Measurement plan + thresholds
### 5.1 The budgets (concrete numbers)
| Metric | Budget (slice-4 design) | CI assertion (slice 4½) | Rationale for slack |
|---|---|---|---|
| Barge-in kill-time, p99 | ≤60 ms (3 debounce × 20 ms + 1 drain tick) | ≤80 ms | CI runner has known variance against dev; the budget is 60 ms; the assertion is 80 to avoid flakiness. |
| Mouth-to-ear round-trip, p99 | ≤200 ms slice-1 + ≤300 ms mock brain + ≤100 ms playout = ~600 ms | ≤700 ms | Same logic; the mock brain is deterministic but the harness adds observer cost. |
| Tick-lag (max prev-poll duration) | unspecified | ≤10 ms | The slice-5/seams META_TICK const; the invariant the assertion makes explicit. |
| Tick overruns (fraction of ticks > 10 ms) | unspecified | ≤1% | At 50 calls × 1000 ticks each = ≥99% need to be ≤10 ms. Allows for one scheduling hiccup per ~99 well-behaved ticks. |
### 5.2 Per-concurrency swept assertions
```
For each N ∈ [1, 10, 50]:
run scenario loud-barge.toml against N concurrent SimCalls.
assert p99_kill_ms <= 80 ms;
assert p99_mouth_to_ear_ms <= 700 ms;
assert max_tick_lag_micros <= 10_000; // 10 ms
assert tick_overrun_pct <= 1.0;
```
### 5.3 The scenarios (3 shipped)
| Scenario | Path | What it asserts |
|---|---|---|
| `loud-barge.toml` | Caller speaks 20 loud frames → awaits reply → end. | The PRIMARY barge-in path (slice-4 §5.1): local VAD fires, kill within ≤80 ms at p99, NO brain advisory required. |
| `quiet-advisory.toml` | Caller speaks 20 quiet frames (sub-VAD-threshold) → awaits reply → end. | The SECONDARY barge-in path (slice-4 §5.2): brain advisory fires from `MockRealtimeBrain`, kill flows through slice-3 plumbing + slice-4 `advisory_tx``Reflex`. |
| `sustained-call.toml` | Caller speaks 10 loud → 10 quiet → 10 loud → 10 quiet → 10 loud → end (5 minutes of talk). The fatigue / sustained-load check. | Multi-barge: 3 `SpeechStarted` advisories should fire in sequence; the `Reflex::barge_epoch` increments 3×; latency is asserted across all three bars (the second + third bar shouldn't drift > 1.5× the first). |
### 5.4 CI integration
The CI workflow gains a new job:
```yaml
# .github/workflows/ci.yml (additive)
sim-bench:
name: sim-bench (stable)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install libopus
run: sudo apt-get update && sudo apt-get install -y libopus-dev
- name: Run sim-bench threshold sweep
run: cargo test --all --features=sim-bench -- --test-threads=1
```
Notes:
- `--test-threads=1` — concurrent sim-bench tests would pollute each other's `MediaStats` polling
(the tick-lag gauge measures the SHARED media thread; concurrent runs of the sweep would
contaminate each other).
- Run on `stable` only — the matrix already runs `cargo test --all` on stable + 1.85; the bench
feature lives on stable.
### 5.5 The thresholds-as-test contract
```rust
// crates/rutster-sim/src/thresholds.rs (continuation — the CI test entry)
#[cfg(test)]
mod threshold_assertions {
use super::*;
#[tokio::test]
#[cfg(feature = "sim-bench")]
async fn loud_barge_at_each_concurrency_passes_thresholds() {
let scenario = Scenario::load("scenarios/loud-barge.toml").unwrap();
for &n in SWEEP_CONCURRENCIES {
let report = ConcurrencyRunner::in_process(n).run(scenario.clone()).await;
let row = report.per_concurrency.iter()
.find(|r| r.concurrency == n).expect("concurrency row");
assert!(
row.p99_kill_ms <= BARGE_IN_KILL_TIME_P99_MS,
"p99 kill-time at N={}: {}ms > {}ms (budget overflow)",
n, row.p99_kill_ms, BARGE_IN_KILL_TIME_P99_MS,
);
assert!(
row.p99_mouth_to_ear_ms <= MOUTH_TO_EAR_P99_MS,
"p99 mouth-to-ear at N={}: {}ms > {}ms",
n, row.p99_mouth_to_ear_ms, MOUTH_TO_EAR_P99_MS,
);
assert!(
(row.max_tick_lag_micros as f64) / 1000.0 <= TICK_LAG_MAX_MS,
"max tick-lag at N={}: {}us > {}ms",
n, row.max_tick_lag_micros, TICK_LAG_MAX_MS,
);
assert!(
row.tick_overrun_pct <= TICK_OVERRUN_PCT_MAX,
"tick overrun % at N={}: {}% > {}%",
n, row.tick_overrun_pct, TICK_OVERRUN_PCT_MAX,
);
}
}
#[tokio::test]
#[cfg(feature = "sim-bench")]
async fn quiet_advisory_at_1_concurrency_passes_thresholds() { /* ... */ }
#[tokio::test]
#[cfg(feature = "sim-bench")]
async fn sustained_call_multibarge_does_not_drift() { /* ... */ }
}
```
---
## 6. Why these decisions
### 6.1 Why a new crate (`crates/rutster-sim/`) instead of in-tree tests
A test under `crates/rutster/tests/sim_*.rs` would absorb the harness into the binary crate —
encouraging the harness to depend on binary internals. The separate crate keeps the dependency
direction: `rutster-sim``rutster-media` + `rutster-call-model` + `rutster` (for `MediaCmd`).
The sim crate is a **FOB member** per ADR-0008: hot-path-adjacent (drives the loop), differentiating
(the proof artifact). It earns cratehood the same way `rutster-tap` did.
### 6.2 Why scripted scenarios, not LLM-driven callers (deferred)
- **Reproducibility** is the entire point. A CI gate that asserts "p99 ≤ 80 ms" can't be flaky on
LLM variance — the LLM might take 2 s to respond, the threshold fails, the build is red, the dev
attributes it to the LLM provider. The signal is corrupted.
- **Isolation** of what's being measured: scripted scenarios measure the FOB + the loop + the
reflex; LLM callers introduce brain-side variance that isn't a FOB property.
- LLM-driven callers land in a post-spearhead refinement tier (ADR-0010 explicit deferral). They
measure a different question ("how does it feel to talk to my brain?") than the MVP harness asks
("does the FOB reflex loop meet its budget under load?").
### 6.3 Why in-process, not client-server (the false-precision risk)
A client-server test harness gives Network-Realism™ but takes away the **measurement discipline**: a
TCP retransmit on the CI runner would inflate p99 unattributably, and a "drift over threshold"
failure becomes a triage time-sink ("is the FOB regressing, or did the CI runner's network blip?").
The in-process mode gives **single-process determinism** at the cost of network realism — which the
spearhead explicitly doesn't need (it's measuring the FOB loop, not the deployed system). A
future post-spearhead tier adds a client-server mode for integration realism + a different set of
thresholds.
### 6.4 Why tick-lag as a primary readout (the doctrine-drift contract)
The single-thread media loop (slice-4 §6.3) is a spearhead-scale decision explicitly deferred to a
"load demands it" trigger. Without the harness measuring it under load, the deferral has no
data-driven graduation criterion — it'd be doctrine ("faster per-session threads later") instead of
evidence ("measured `tick_overrun_pct = 12%` at 50 calls, the threadpool shard lands now"). ADR-0010
explicitly pairs the debt — `MediaStats.tick_overruns` exists because of slice-5/seams' seam
work; slice 4½ makes the gauge OBSERVED. Both pass and the single-thread design is validated; both
breach and the graduation is scheduled with data.
### 6.5 Why `--features=sim-bench` (default off)
`cargo test --all` runs in 12 s on the CI runner today (fmt+clippy+test sweep, no bench).
Turning the threshold sweep on by default would either (a) slow down every PR's turnaround to the
sim sweep duration (estimated 3060 s, dominated by the 50-concurrency sweep), or (b) make the
threshold sweep's failure mode just-another-failing-test that gets ignored. The opt-in feature
makes the threshold sweep a **separate concern with its own CI job**, surfacing its results
prominently in the PR status checks, and small enough to not block the routine `cargo test --all`
gate. This mirrors how performance-sensitive open-source projects gate `cargo bench` PRs.
### 6.6 Why p99 (not p50) as the load-bearing assertion
p50 = "the typical experience." p99 = "the worst acceptable case." The contact-center wedge
(README §"Why it exists") rests on tight-tail-latency: a p50 of 60 ms with a p99 of 1.5 s is
INDISTINGUISHABLE from a cloud CCaaS provider that averaged down to 60 ms but had bad tails. The
assertion HAS to fire on the tail to be meaningful.
p999 (or max) IS deferred — too noisy for CI gate assertions (one scheduler hiccup blowing past the
ceiling would block every PR for unrelated reasons). p99 is the empirical sweet spot: sensitive
enough to catch real regressions, lenient enough to survive CI runner variance.
---
## 7. Done-criteria
1. `cargo test --all` passes (stable + 1.85) — the routine gate, UNCHANGED. The sim-bench feature
is opt-in; default `cargo test --all` does NOT run the threshold sweep.
2. `cargo fmt --check` + `cargo clippy -- -D warnings` clean on the new crate.
3. `cargo test --all --features=sim-bench` passes — the new gate, on stable. CI runs this in a
separate `sim-bench` job per PR + nightly.
4. `cargo deny check` passes — no new dep conflicts (`toml` is already a workspace member;
`serde` already workspace member).
5. `cargo doc --no-deps` renders the new `crates/rutster-sim/` cleanly with learner-facing
comments per AGENTS.md code style.
6. Loop driver + rtc_session seam STILL holds: `loop_driver.rs` + `rtc_session.rs` byte-identical to
slice-3 (CI pinned-blob gate from slice-4 Task 10 unchanged). The new `MediaCmd::RegisterSim`
variant lives in `media_thread.rs`, NOT in the seam files.
7. The lance `loud-barge.toml` scenario passes the threshold sweep at all of [1, 10, 50]
concurrency.
8. The `quiet-advisory.toml` scenario passes at 1 concurrency (the secondary-path focus).
9. The `sustained-call.toml` scenario's 3-barge sequence shows performance drift ≤ 1.5× across
barges (anti-fatigue assertion).
10. Tick-lag gauge reads a `MediaStats.{tick_overruns, last_tick_micros}` value during the sweep
and surfaces it in the SweepReport.
11. SweepReport's per-concurrency rows are logged to stderr in a structured format (CI failure
messages are readable; "p99 kill-time at N=50: 84ms > 80ms" not "test_sim_thresholds
failed").
12. The single-thread-vs-threadpool question has a data-point answer documented ("slice 4½ found p99
tick-lag = Xms at 50 calls; the threadpool shard remains deferred / the threadpool shard
should land now"). Even if the answer is "data confirms the deferral is fine for now," the
decision is no longer vibe-based.
---
## 8. Open decisions
### 8.1 Should the harness also assert against the HTTP/WebSocket out-of-process surface?
**Decision (slice 4½):** no. In-process measurement only. The out-of-process mode is a future
post-spearhead refinement (paired with LLM-driven callers). Reasoning in §6.3.
### 8.2 Should the sim-bench CI job also run on the 1.85 toolchain?
**Decision (slice 4½):** no. The matrix already runs `cargo test --all` on both. The sim-bench
job is a stable-only opt-in feature; 1.85 doesn't get a second tier of benches. Rationale:
the sim-bench feature is `cfg(feature)` — feature-gated code paths need their own gates, not a
toolchain proliferation.
### 8.3 Threshold values: hardcodedconsts vs env-overridable for the CI runner operator?
**Decision (slice 4½):** hardcoded consts in `thresholds.rs`. Post-spearhead, with the
per-environment tuning framework (paired with slice-4 §1.2 VAD-threshold tuning deferral), they
become env-driven. Hardcoded now makes the budget-vs-assertion-slack reasoning (§5.1) explicit in
source — not subject to runner-env drift.
### 8.4 Should `MediaCmd::RegisterSim` carry a `pipe: Box<dyn AudioPipe>` or a more structured
"sim descriptor" the media thread materializes?
**Decision (slice 4½):** `Box<dyn AudioPipe>` — same shape as the existing `RtcSession`'s pipe
construction (which has `pipe: Box<dyn AudioPipe>`). A "sim descriptor" would add a layer of
indirection the harness doesn't benefit from; the harness already constructs the `SimAudioPipe`
and is fully prepared to ship it across the channel. The single variant is the minimal seam.
### 8.5 Concurrency-sweep sample size / iteration count per concurrency level
**Decision (slice 4½):** 1000 ticks per concurrency level (the meta-tick count for the sweep
durations). At 10 ms per tick = 10 seconds of sweep per concurrency × 3 levels = 30s sweep total.
Sufficient sample size for a stable p99 (n = ~1000 ticks); bounded enough to keep CI fast.
### 8.6 Should the harness record per-call audio (WAV capture of the `SimAudioPipe`'s frames) for
supervisor review?
**Decision (slice 4½):** no. That's a rung-2 escalation feature (warm-handoff artifact). The
harness measures; recording is a separate concern. The `SimAudioPipe` exposes a `take_captures()`
API for the `LatencyProbe` only; raw frame capture is out of scope.
---
## 9. Cross-references
- [ADR-0010](../../adr/0010-spearhead-benchmark-sim-harness.md) — centralized rationale: why 4½
exists, what it should produce, what it should defer (LLM callers), how it pairs the
timing-thread debt.
- [slice-4 spec §5.1](2026-07-01-slice-4-barge-in-design.md) — the ≤60 ms kill budget + the
latency-arithmetic this slice asserts against.
- [slice-1 spec §8.5 #6](2026-06-28-slice-1-webrtc-loopback-design.md) — the seam gate (`loop_driver.rs`
+ `rtc_session.rs` byte-identical) slice 4½ re-affirms (NO changes to those files).
- [slice-5/seams plan](../plans/2026-07-04-slice-5-scalability-seams.md) (the infrastructure
pre-paving this slice consumes) — `MediaCmd::Stats` exposes `MediaStats { tick_overruns,
last_tick_micros }`, the readout this slice's concurrency sweep surfaces.
- [ADR-0002](../../adr/0002-north-star-and-fused-core.md) — the fused vertical; the in-process
measurement IS the fused-vertical seam (no gRPC hop between the harness + the loop).
- [ADR-0008](../../adr/0008-fob-and-green-zone.md) — FOB/green-zone doctrine; the harness is a FOB
member (hot-path-adjacent + differentiating). No green-zone dep added.
- [PORT_PLAN.md §Phasing](../../PORT_PLAN.md) — step 4½ = sim harness (per ADR-0010 insertion).

File diff suppressed because it is too large Load Diff