Commit Graph

58 Commits

Author SHA1 Message Date
86d55c3e67 test(slice-4): primary-path barge-in e2e — local VAD kills playout without brain
This integration test proves wedge #1: loud synthetic caller audio
(samples = 1000, well above VAD_RMS_THRESHOLD = 500.0) sent through
LocalVadReflex<Reflex<TapAudioPipe>> trips the local VAD and kills
playout within one tick, without any brain advisory.

Cites README:98-100 and ARCHITECTURE.md:79-81 ("local reflexes that
don't need the brain"); see slice-4 spec §3.4, §6.1, and §7
done-criteria #8.

Signed-off-by: Aaron D. Lee <himself@adlee.work>
2026-07-03 21:45:46 -04:00
0eb20b729a docs(reviews/2026-07-03): P4 fix — sweep status staleness + re-aim fuzz/README (#11)
Some checks failed
CI / fmt (push) Successful in 1m32s
CI / clippy (push) Successful in 1m31s
CI / test (1.85) (push) Successful in 3m57s
CI / deny (push) Has been cancelled
CI / test (stable) (push) Has been cancelled
Co-authored-by: Aaron D. Lee <himself@adlee.work>
Co-committed-by: Aaron D. Lee <himself@adlee.work>
2026-07-04 01:44:27 +00:00
276dc5ac45 slice-4 (dev-b): TapAudioPipe::barge_in_flush + advisory_tx + MockRealtimeBrain schedule (#9)
Some checks failed
CI / clippy (push) Has been cancelled
CI / test (1.85) (push) Has been cancelled
CI / test (stable) (push) Has been cancelled
CI / deny (push) Has been cancelled
CI / fmt (push) Has started running
Co-authored-by: Aaron D. Lee <himself@adlee.work>
Co-committed-by: Aaron D. Lee <himself@adlee.work>
2026-07-04 01:43:41 +00:00
bc7e8f1acd slice-4 (dev-a): Reflex<P> + LocalVadReflex<P> (Task 2 + 2b) (#8)
All checks were successful
CI / fmt (push) Successful in 53s
CI / clippy (push) Successful in 1m45s
CI / test (1.85) (push) Successful in 4m29s
CI / test (stable) (push) Successful in 4m18s
CI / deny (push) Successful in 1m31s
Co-authored-by: Aaron D. Lee <himself@adlee.work>
Co-committed-by: Aaron D. Lee <himself@adlee.work>
2026-07-03 13:50:36 +00:00
8cde52b1d3 Merge pull request 'slice-4 (dev-a): Task 1 — AdvisoryEvent + ReflexMetrics + barge_in_flush trait' (#7) from slice-4-dev-a-reflex into main
All checks were successful
CI / fmt (push) Successful in 1m3s
CI / clippy (push) Successful in 1m29s
CI / test (1.85) (push) Successful in 3m47s
CI / test (stable) (push) Successful in 5m48s
CI / deny (push) Successful in 1m55s
Reviewed-on: #7
2026-07-03 03:37:34 +00:00
d06e79cc50 docs(media): add reflex module-map entry to lib.rs catalogue (slice-4 §3.1)
All checks were successful
CI / fmt (pull_request) Successful in 1m29s
CI / clippy (pull_request) Successful in 2m31s
CI / test (1.85) (pull_request) Successful in 4m16s
CI / test (stable) (pull_request) Successful in 4m19s
CI / deny (pull_request) Successful in 1m21s
Task 1 review flagged the new reflex module was missing from the
module-map //! catalogue. AGENTS.md learner-facing convention requires
the catalogue entry describing what the module does + why it exists.

Addresses the Minor finding from the Task 1 task review.

Signed-off-by: Aaron D. Lee <himself@adlee.work>
2026-07-02 23:08:53 -04:00
0b054e76f4 feat(media): AdvisoryEvent + ReflexMetrics + barge_in_flush trait (slice-4 §3.1, §3.3)
The critical-path foundation for the barge-in reflex. AdvisoryEvent is
the enum carried over a tokio mpsc from TapEngine to Reflex (brain →
FOB). ReflexMetrics is the observable surface. barge_in_flush is the
new AudioPipe trait method (default delegates to clear_playout_ring) —
the kill-now path that clears the ring AND drains rx_audio_out.

Task 1 of the slice-4 plan. Everything else depends on this landing.

Signed-off-by: Aaron D. Lee <himself@adlee.work>
2026-07-02 23:08:53 -04:00
3f494cb919 spec+plan(slice-4): barge-in / VAD-driven playout kill on dedicated media thread (#6)
All checks were successful
CI / fmt (push) Successful in 1m37s
CI / clippy (push) Successful in 2m34s
CI / test (1.85) (push) Successful in 5m19s
CI / test (stable) (push) Successful in 4m5s
CI / deny (push) Successful in 1m58s
2026-07-03 03:06:58 +00:00
c30a45232d Slice 3 — OpenAI Realtime brain: swap echo for the brain (#4)
All checks were successful
CI / fmt (push) Successful in 1m40s
CI / clippy (push) Successful in 2m24s
CI / test (1.85) (push) Successful in 5m8s
CI / test (stable) (push) Successful in 5m20s
CI / deny (push) Successful in 1m34s
2026-07-01 22:25:09 +00:00
opencode controller
2f3f92ec6b ci: bump cargo-deny-action v1 -> v2 (CVSS 4.0 advisory-db parse fix)
Some checks failed
CI / test (1.85) (pull_request) Failing after 13m59s
CI / clippy (pull_request) Failing after 14m8s
CI / fmt (pull_request) Failing after 14m14s
CI / test (stable) (pull_request) Successful in 2m48s
CI / deny (pull_request) Failing after 1m55s
CI / fmt (push) Successful in 1m3s
CI / test (1.85) (push) Successful in 2m35s
CI / test (stable) (push) Successful in 2m50s
CI / deny (push) Failing after 1m17s
CI / clippy (push) Failing after 14m28s
2026-07-01 01:05:51 -04:00
opencode controller
c86bb19c0e ci: drop sudo for libopus install (act job containers run as root)
Some checks failed
CI / fmt (pull_request) Successful in 1m10s
CI / clippy (pull_request) Successful in 6m41s
CI / test (1.85) (pull_request) Successful in 8m54s
CI / test (stable) (pull_request) Successful in 9m44s
CI / deny (pull_request) Failing after 11m23s
2026-07-01 00:22:31 -04:00
opencode controller
2bc9877105 fmt: apply rustfmt across slice-2 use-statement drift
Some checks failed
CI / fmt (pull_request) Successful in 56s
CI / clippy (pull_request) Failing after 28s
CI / test (1.85) (pull_request) Failing after 27s
CI / test (stable) (pull_request) Failing after 28s
CI / deny (pull_request) Failing after 27s
2026-07-01 00:18:04 -04:00
opencode controller
c89d65c621 ci: retrigger pivot PR (runner config fix for gitea DNS)
Some checks failed
CI / fmt (pull_request) Failing after 52s
CI / clippy (pull_request) Failing after 29s
CI / test (1.85) (pull_request) Failing after 30s
CI / test (stable) (pull_request) Failing after 28s
CI / deny (pull_request) Failing after 27s
2026-07-01 00:13:23 -04:00
opencode controller
42060b68e3 spec(ui): default operator console design (later-rung, not yet scheduled)
Some checks failed
CI / fmt (pull_request) Failing after 58s
CI / clippy (pull_request) Failing after 33s
CI / test (1.85) (pull_request) Failing after 32s
CI / test (stable) (pull_request) Failing after 47s
CI / deny (pull_request) Failing after 41s
Design record for the operator console — the downstream non-technical
operator's surface. **Not scheduled for current build;** lands after the
spearhead (steps 1-6) and the ACD/escalation rungs (capability ladder
rungs 1-2). The current build target is slice-2; the next build target
after slice-2 is slice-3 (the OpenAI Realtime adapter). An agent that
proposes building the console now is wrong — point it at spec §0.

Records the visual-brainstorming session's resolved decisions so they
don't get re-litigated at implementation time:

§1 Principles (load-bearing):
- GUI is a pure API client — **green-zone, never an insider** (ADR-0008);
  holds no privilege a third party couldn't get from the public API.
- Config-as-code is the single source of truth; the UI is a reconciled
  *view* (the k8s model — declarative desired-state in git, the engine
  reconciles; console + CLI + git are views over it).
- Operator persona, not builder. The builder authors flows in **code +
  an AI pair** (ARCHITECTURE 'DX spine'); this console is NOT the
  authoring surface for the technical builder. **No drag-drop authoring
  canvas** — a deliberately-closed decision (vision-revision §8).
- Ships in the batteries-included distro and the all-in-one image
  (the reference GUI is bundled — 'boom' includes a usable console).

§2 IA — Ops Console shell: left nav + center panel + persistent live rail.
Dense single-screen; for an operator who lives in it. Live rail always
visible regardless of the selected section.

§3 Dashboards — Live Ops (default landing) + Quality (sibling view). Maps
the k8s split: Live Ops = live operational state; Quality = the data-owned
eval loop (capability ladder rungs 3→4).

§4 Flows editor — master/detail + a generated, navigable preview map.
The preview map is a *view* rendered from the config, not a drag-drop
authoring canvas. Earns its place by showing branching (the escalation
split) the linear stage-list flattens.

§5 Declarative flow schema (illustrative YAML; format is open). The config
the console edits via the API; the builder edits in git; the engine
reconciles. The map is generated from this structure.

§6 Other sections (conventional): Agents · Numbers · Queues · People ·
Recordings · Settings.

§7 Architecture: separate app, public API only; never reaches into the FOB.
Config writes go through the reconcile path. Live data rides the event
stream off Valkey (ADR-0005); never the 20 ms media loop.

§8 v1 vs. later scoping. §9 open decisions (flow config format, map
rendering depth, quality metrics, live transport, framework).

Cross-refs ADR-0002 (fused vertical + composable platform), ADR-0006
(WebRTC ingress / escalation UX), ADR-0007 (rented trunk / Numbers
config), ADR-0008 (FOB / green-zone — the UI is green-zone),
ARCHITECTURE.md 'DX spine' + 'GUI & extension architecture', PORT_PLAN §5
(contact-center domain).
2026-06-29 20:27:25 -04:00
opencode controller
4b0f53eb2d tooling: gitignore *.kate-swp + .superpowers/ scratch dir
- *.kate-swp: Kate editor's swap files have a different extension pattern
  than vim's *.swp; without this, kate-swp files like
  docs/reviews/.2026-06-28-slice-1-claude-adversarial-assessment.md.kate-swp
  leak into the working tree.
- .superpowers/: the superpowers companion scratches its visual-brainstorming
  server state (HTML mockups, server.log, server.pid, server-info) under
  .superpowers/brainstorm/ in the project root. The companion explicitly
  advises gitignoring this directory; scratch artifacts are not source.
2026-06-29 20:27:11 -04:00
opencode controller
3b7b0d6459 docs: propagate rutster-trunk rename + repo URL through plans/specs/DEVELOPMENT
Mechanical propagation of the crate rename ( rutster-signaling-sip →
rutster-trunk ) and the repo URL fix ( github.com/anomalyco →
git.adlee.work/alee ) through the documents that name them:

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

No content changes beyond the substitutions; the slice-2 spec/plan
body (protocol, TapAudioPipe, TapClient, TapEngine, lifecycle, done-
  criteria) is untouched.
2026-06-29 20:27:01 -04:00
opencode controller
7616878bb1 docs: propagate ADR-0007/0008 to README, ARCHITECTURE, PORT_PLAN, AGENTS
ADR-0007 (rent the trunk) + ADR-0008 (FOB/green-zone doctrine) propagation
into the narrative docs that orient contributors + readers. No code changes.

README:
- Wedge bullet 2: 'one secure auditable boundary' now lists media + local
  reflexes + spend + tap + audit (was: trunk termination + media + spend).
  Adds the honest caveat that PSTN media inside the boundary is the on-prem
  *graduation* (ADR-0007), not a day-one claim.
- 'Is / isn't': 'isn't a TDM/PSTN-hardware PBX — *and not a SIP stack.*'
  No first-party SIP (ADR-0007). PSTN reach is rented transport.
- Memory-safety pillar: 'rutster parses no SIP at all' — entire first-party
  wire surface is WebRTC/RTP/SRTP + WebSocket tap/ingress, all memory-safe
  Rust. The carrier-SIP interop tail lives outside the trust boundary.
- Spearhead step 5: 'Add a real phone number via rented transport' (was:
  'Replace WebRTC ingress with a real PSTN trunk call'). Re-aims at the
  AI-telephony frontier; no first-party SIP stack.
- References: 0007 supersedes 0003 in the highlighted ADR list.

ARCHITECTURE:
- New 'FOB and the green zone' section after the fused-vertical framing —
  names the build-vs-reuse doctrine with the FOB member list + green-zone
  member list, and restates memory-safety precisely ('FOB is 100%
  memory-safe Rust; the green zone is trusted OSS kept outside the
  boundary — not an over-claim that every byte is Rust').
- 'Inside the boundary': 'Carrier trunk — rented transport, not first-party'
  replaces 'Trunk SIP termination — Rust-native.' PSTN audio arrives as
  a media-leg ingress from a rented CPaaS raw-media fork or an out-of-tree
  SBC for on-prem sovereignty.
- Biggest technical risk: 'No longer the SIP stack — because rutster no
  longer builds one.' Retires ADR-0003's named schedule risk; redirects
  to 'the reflex loop itself' (turn-taking, VAD-driven barge-in, jitter,
  pacing) — which is also the differentiator.

PORT_PLAN:
- Design rule 4: 'rent it, don't own it' replaces ADR-0003's 'Rust-native
  trunk SIP, no SBC shield.' ADR-0007 restores the rule's original instinct.
- SIP signaling (trunk) row: disposition flipped to 🔌 Rented / out-of-tree.
- Outbound registration row: disposition flipped to 🔌 Rented / out-of-tree
  (handled by the rented transport or out-of-tree SBC; rutster parses no SIP).
- Spend/abuse engine row: 'co-located with call origination + the tap inside
  the boundary' (was: 'co-located with trunk termination'). rutster mediates
  both the provider call-control API and the brain tap — the brain never
  holds the wire.
- Spearhead step 5: 'Add a real phone number via rented transport.'
- Open decisions: SIP line updated to 'Re-decided — ADR-0007.'

AGENTS.md:
- ADR list: adds 0007 and 0008 with annotations; marks 0003 as superseded
  by 0007.
- Key decisions to respect: prepends the FOB/green-zone doctrine as the
  *the* build-vs-reuse rule. 'When in doubt, default to green zone — the
  FOB earns its members, it doesn't collect them. This is why the trunk is
  rented (ADR-0007) and Valkey reused (ADR-0005), not rebuilt. Don't pull
  green-zone plumbing into the core.'
2026-06-29 20:26:51 -04:00
opencode controller
3c3197e57a refactor(workspace): rutster-signaling-sip → rutster-trunk; edition 2024; repo URL = git.adlee.work
ADR-0007 lands in code:
- Rename crates/rutster-signaling-sip → crates/rutster-trunk. The crate was
  a stub anyway (lib.rs with a doc-comment + crate_compiles() test); the
  rename locks the new boundary shape — the future rented-transport
  ingress (CPaaS media-leg fork / out-of-tree SBC glue, **no SIP stack**)
  lands here at spearhead step 5.
- rutster-trunk/src/lib.rs doc-comment now describes the ADR-0007 split:
  CPaaS media-leg adapter as primary, out-of-tree SBC for on-prem
  graduation. Cross-refs ADR-0007 + ADR-0008 (FOB vs. green zone: trunk
  is green zone; SIP lives outside the trust boundary).
- rutster-spend/src/lib.rs doc-comment updated to quote ADR-0007's
  'rutster mediates both the provider call-control API and the brain tap'
  framing — the spend gate sits in that boundary, structurally preventing
  a runaway brain from exceeding spend/pacing. Pulling spend out into a
  service re-introduces the 3-vendor structural hole.

Cargo bumps:
- edition = '2024' (slice-1's pinned Rust 1.85 + edition-2024 floor
  already requires this; rutster-media's let-else pattern in
  OpusDecoder::decode · Slice-2's let-else in AcceptOffer's Uuid
  parse_str · all rely on edition 2024's stabilized let-chains / let-else).
- repository = 'https://git.adlee.work/alee/rutster' (the self-hosted
  Gitea remote — matches 'git remote -v' origin). The github.com/anomalyco
  URL was stale from the pre-pivot copy.
- Cargo.lock regenerated by cargo for the rename.

No behavioral code changes — the trunk crate's body is still the stub
crate_compiles() test. FOB membership (per ADR-0008) is unchanged:
rutster-trunk will be FOB-internal at step 5 because it's the media-leg
ingress (hot path); spend-spend stays FOB because spend is
security-constitutive.
2026-06-29 20:26:37 -04:00
opencode controller
272aa07acd adr: +0007 rent trunk transport +0008 FOB/green-zone doctrine; supersede 0003
ADR-0007 — Trunk/PSTN strategy: rent the transport, no first-party SIP stack.
Supersedes ADR-0003 (Rust-native trunk SIP). The 2026-06 strategic-relevance
review stress-tested ADR-0003 against the 2026 market + solo-build realities
and it did not survive: a trunk SIP/media core is the highest-cost,
lowest-differentiation square on the board (the perceived-quality battle and
the buying criteria both live above the transport), and no production-hardened
pure-Rust SIP stack exists to stand on (ezk is bus-factor-1 / pre-1.0;
str0m self-describes as 'not for production' for SIP).

rutster owns no SIP stack. PSTN reach is rented transport in three layers:
  1. Primary (demo, most users) — CPaaS raw-media fork (Twilio Media Streams,
     Telnyx). Media-leg ingress, core-as-server (parallel to WebRTC ingress
     per ADR-0006). Use the raw-audio fork, *not* managed Voice-AI products
     that would consume the reflex loop.
  2. Graduation (on-prem) — out-of-tree SBC (Kamailio/FreeSWITCH/drachtio +
     rtpengine) B2BUAs carrier SIP into rutster as clean RTP/tap media,
     outside the trust boundary.
  3. Never — a first-party Rust trunk SIP stack. rsip/ezk stay off the
     critical path.

The only things rutster owns in Rust: the call model, the reflex loop, the
agent tap. Everything that touches a carrier is rented (layer 1) or
out-of-tree (layer 2).

ADR-0008 — The FOB and the green zone: the build-vs-reuse doctrine. Names
the boundary criterion implicit across ADRs 0002-0007 as one mechanical rule
every contributor (human or agent) applies the same way.

  - FOB (build in Rust) — admitted only if it passes one of: hot path,
    security-constitutive, differentiating. Current FOB: media termination
    (rutster-media on str0m) · real-time reflexes (VAD/barge-in/jitter/pacing)
    · call model (rutster-call-model) · agent tap (rutster-tap) · spend/abuse
    gate (rutster-spend) · control API + state/bus trait (rutster).
  - Green zone (reuse at arm's length) — its own process/container/trust
    domain, never in the FOB's address space. Admitted when it fails all
    three FOB tests AND a trusted, actively-maintained project already does
    it well. Current green zone: Valkey (ADR-0005) · carrier trunk (ADR-0007)
    · agent brain · reference GUI · object storage · KMS · OTel collector ·
    container supervisor.
  - 'Actively maintained' is the load-bearing gate: excludes a first-party
    Rust SIP stack and webrtc-rs; admits Valkey and str0m-for-WebRTC.
  - When in doubt, default to green zone. The FOB earns its members.

Restates the memory-safety pillar precisely: the FOB is 100% memory-safe
Rust; the green zone is trusted battle-tested OSS kept outside the boundary
— not an over-claim that every byte is Rust.

ADR-0003's status is flipped to Superseded with a forward-pointer to ADR-0007.
Kept as the historical record of the 'own trunk SIP + media termination in
Rust, no SBC shield' plan; reversed under the strategic-relevance review.
2026-06-29 20:26:24 -04:00
5a193871d8 Merge pull request 'Slice 2 — The agent tap: splicing the brain seam' (#2) from slice-2-agent-tap into main
Some checks failed
CI / fmt (push) Failing after 14m55s
CI / clippy (push) Failing after 1m16s
CI / test (1.85) (push) Failing after 27s
CI / test (stable) (push) Failing after 30s
CI / deny (push) Failing after 1m14s
Reviewed-on: #2
2026-06-28 22:35:27 +00:00
opencode controller
b2aaf149b0 fix(slice-2): widen outer close-bound + strengthen reconnect-test assertion
Some checks failed
CI / clippy (pull_request) Failing after 43s
CI / test (1.85) (pull_request) Failing after 39s
CI / test (stable) (pull_request) Failing after 42s
CI / deny (pull_request) Failing after 39s
CI / fmt (pull_request) Failing after 10m45s
- session_map.rs: AppState::close outer timeout bumped 500ms → 750ms
  so the inner close-arm bound (500ms in tap_client.rs) has room to
  finish `ws.close(None).await` cleanly before the abort fallback
  fires. Outer > inner per final-fixes re-review Important #1.
- tap_integration.rs: reconnect-path test now asserts the resumed
  `next_pcm_frame()` returns the FRESH marker (samples[0] == 9, not
  the stale samples[0] == 7), actively witnessing the §5.3 step 4
  "no stale bleed-through" contract. Pre-kill seeding strengthened
  to ≥2 frames so step 7's silence-after-flush assertion is non-
  vacuous. Per final-fixes re-review Important #2.

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §5.1 step 5, §5.3 step 4.
2026-06-28 18:21:52 -04:00
opencode controller
0d666b5ba7 fix(slice-2): §5.1 teardown seq + §5.3 playout flush + §8.4 reconnect test + §7.2 index.html
Code-review findings from the slice-2 final whole-branch review:

- tap_client.rs: close arm sends session_end (not bye) per §5.2, awaits
  brain bye with bounded 500ms timeout, then ws.close(). Promotes the
  Task-7-documented Minor to Important's required spec compliance.
- session_map.rs: AppState::close() now clears channel.tap = None before
  Closing->Closed, sends close signal, bounded-awaits engine task for the
  teardown handshake (500ms cap), aborts as fallback. Per §5.1 step 5.
- pcm.rs + tap_audio_pipe.rs: AudioPipe trait gains clear_playout_ring()
  (default no-op for EchoAudioPipe); TapAudioPipe clears the VecDeque.
  RtcSession::clear_playout_ring delegates. Per §5.3 step 4 playout-on-
  disconnect-flush contract.
- tap_engine.rs: spawn_tap_engine creates + shares a flush_tx mpsc
  channel; run_engine_loop signals flush after each failed pump loop
  (before backoff); TapConn.flush_rx is drained by drive_all_sessions
  to call RtcSession::clear_playout_ring.
- tests/tap_integration.rs: new reconnect-path test asserts Channel
  stays Connected, metrics.reconnect_attempts increments, playout
  returns None during outage, playout resumes after EchoServer
  restart with fresh frames (no stale bleed-through). Per §8.4.
- static/index.html: one static line acknowledging external-brain
  routing per §7.2 (defer real-time tap status to step-3 GUI).

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §5.1 step 5, §5.2, §5.3 step 4, §7.2, §8.4.
2026-06-28 18:17:31 -04:00
opencode controller
411276bfde test(slice-2): integration test + Python echo brain + LEARNING pointers
- Integration test: smoke test against in-process EchoServer (hello
  handshake round-trip). Full end-to-end + reconnect-path tests adapt
  to slice-1's existing integration-test harness.
- examples/echo_brain/: Python reference brain (~80 lines, websockets lib).
  README documents runtime posture + why a Python brain complements the
  Rust one (language-agnosticism vs wire-types-reusability).
- LEARNING.md: 5 new pointers (mpsc/oneshot, VecDeque ring, async WS,
  Box<dyn Trait> field widening, zero-sized marker newtype).

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §8.4, §8.5 #7.
2026-06-28 17:52:16 -04:00
opencode controller
84fc30591e fix(binary): spec-exact backoff staircase + remove dead Closing branch
- tap_engine.rs: Backoff::next_delay now matches spec §4.3 exactly:
  250ms -> 500ms -> 1s -> 2s -> 5s cap (no intermediate 4s step from
  naive doubling). Test asserts the spec enumeration including the
  "stays at 5s forever" cap-after-step-5 invariant.
- session_map.rs: removed the dead 'Closing && tap.is_some()' branch
  from drive_all_sessions. AppState::close already handles teardown
  inline (fires close_tx, aborts task, clears field, advances
  state, removes entry). The branch was never exercised and
  pre-paved a "future peer-initiated close" path -- both AGENTS.md
  anti-patterns ("don't pre-pave the wrong pattern").

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §4.3 step 4 + §5.1 step 3.
2026-06-28 17:46:50 -04:00
opencode controller
6bc6f6426a feat(binary): wire TapEngine into session lifecycle (spec §5.1, §7)
- tap_engine.rs: spawn_tap_engine(session_id, tap_url) — dials the brain,
  runs TapClient with bounded exponential backoff (250ms→5s cap, infinite
  retries). Cold-path network I/O on tokio's pool, NOT a timing thread
  (spec §4.3 deviation boundary).
- session_map.rs: +tap_registry (DashMap<ChannelId, TapConn>); +default_tap_url.
  drive_all_sessions observes Connected+tap.is_none() → spawn TapEngine, set
  channel.tap = Some(TapHandle). Observes Closing+tap.is_some() → fire close
  oneshot, abort task, clear tap BEFORE state advances to Closed.
- routes.rs: POST /v1/sessions accepts optional {"tap_url":...} body;
  resolve_tap_url validates ws:// 127.0.0.1/localhost, rejects wss:// + non-
  loopback ws:// + bad schemes with 400 Bad Request (fail-fast per spec §4.4).
- main.rs: reads RUTSTER_TAP_URL env (default ws://127.0.0.1:8081/echo).
- rtc_session.rs (rutster-media): pipe field changed from EchoAudioPipe to
  Box<dyn AudioPipe + Send> — the seam test (§8.5 #6):
  loop_driver's call sites (sink.on_pcm_frame / source.next_pcm_frame)
  are byte-identical; only the field type widens. RtcSession::new keeps
  EchoAudioPipe default (slice-1 tests unchanged); binary uses
  RtcSession::set_pipe(tap_audio_pipe) to swap on the Connected transition.
- 4 unit tests for tap_url validation (loopback accepted, non-loopback
  rejected, wss rejected fail-fast, default fallback).

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §4.3, §4.4, §5.1, §7.
2026-06-28 17:39:52 -04:00
opencode controller
e0616fe42d feat(call-model): +tap field on Channel + TapHandle marker (spec §5.2, §6)
- pub struct TapHandle(()) — zero-sized marker (Option<TapHandle> compiles
  to a bool, no allocation). The live tap connection lives in the binary's
  DashMap<ChannelId, TapConn>, keyed by the channel's existing ChannelId
  (== wire session_id per spec §5.3).
- Channel grows pub tap: Option<TapHandle>. None until Connected, set on
  Connected, cleared before state advances to Closed.
- State invariant: tap and ChannelState are tied (Connected+None=bug).
- Slice-1 backwards compatible: new field is Option<...>;

Multi-tap-per-channel is the future trigger for a TapId newtype (YAGNI).

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §5.2, §6.
2026-06-28 17:24:11 -04:00
opencode controller
237a1388a4 feat(tap-echo): Rust reference echo brain + test server (spec §2.3)
- start_echo_server(addr): in-process WS server for integration tests;
  returns EchoHandle { shutdown, join, addr }.
- echo_one_connection: the per-connection echo loop — hello handshake,
  audio_in → audio_out (same PCM), bye/session_end graceful close.
- Reuses rutster-tap's protocol types — the wire-types-reusable contract
  test (spec §2.3).
- Stateless across reconnects (spec §5.3) — every hello starts fresh.
- Standalone binary: binds ws://127.0.0.1:8081, runs forever.
- 1 unit test exercises full hello/ack/audio_in/audio_out/bye over a
  TCP loopback pair.

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §2.3, §5.3.
2026-06-28 17:20:00 -04:00
opencode controller
1bb5b7203c feat(tap): TapClient WSS pump loop (spec §4.2)
- run_tap_client: drives a connected WebSocketStream with a tokio::select!
  over rx_pcm_in (inbound PCM → audio_in WS frame) and ws.next() (brain
  frame → audio_out mpsc or control handling).
- Handshake: send hello, await brain hello (bounded 2s timeout).
- seq gap detection (log + count, never drop on gap — spec §3.1).
- Hot-path errors (encode/decode failures, send/recv failures) are logged
  + counted; TapClientError is returned only on graceful close, hello
  timeout, or WS errors. The TapEngine (Task 7) decides reconnect policy.
- Pure-helper unit test (elapsed_ms); full pump behavior is exercised
  against the in-process EchoServer in the Task 8 integration test.

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §4.2.
2026-06-28 14:25:15 -04:00
opencode controller
51d5e6b01e feat(tap): TapAudioPipe + TapMetrics (spec §4.1)
- TapMetrics: atomic counters (drop + observe per slice-1 §3.8).
- TapAudioPipe: AudioSource + AudioSink impl over mpsc + VecDeque ring.
  Drop-oldest on overflow (lowest-latency-correct); silence on underflow.
- TAP_PLAYOUT_FRAMES = 5 (100ms @ 20ms/frame; tunable constant per spec §4.1).
- 6 unit tests cover underflow, overflow drops-oldest, sink-full drops,
  disconnected engine, frame round-trip, empty ring.

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §4.1.
2026-06-28 14:18:57 -04:00
opencode controller
5b931d447b feat(tap): versioned JSON event wire protocol (spec §3)
- protocol.rs: Envelope + FrameKind + Payload types, serde-derived.
- Explicit little-endian PCM codec (to_le_bytes / from_le_bytes) —
  spec §3, §9. No host-endian silent hazard.
- encode_* helpers for hello, audio_in, audio_out, session_end, bye, error.
- decode_envelope validates protocol version + samples count; unknown
  type values decode to DecodedPayload::Unknown (log + count + drop
  per spec §3.4).
- 10 unit tests cover round-trips, LE byte order, sample-count
  mismatch, unknown-type drop, and version mismatch.

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §3.
2026-06-28 14:12:48 -04:00
opencode controller
0cccf77faf build(slice-2): workspace deps + rutster-tap-echo skeleton
- Add tokio-tungstenite 0.24, futures-util 0.3, url 2, base64 0.22 to
  [workspace.dependencies] (spec §8.1).
- Add crates/rutster-tap-echo as 7th workspace member (spec §2).
- Transition crates/rutster-tap/Cargo.toml from stub to real manifest
  with the deps Tasks 2-4 will need (rutster-media, tokio, tokio-tungstenite,
  serde, serde_json, base64, url, thiserror, tracing).
- Skeleton lib.rs (compile test) + main.rs (placeholder fn main) for
  rutster-tap-echo; Task 5 fills in the echo logic.
- Verify cargo deny check passes against the new transitive dep tree.

Spec ref: docs/superpowers/specs/2026-06-28-slice-2-agent-tap-design.md §2, §8.1.
2026-06-28 14:01:12 -04:00
opencode controller
22d3f03b8c plan(slice-2): agent tap implementation plan (spec §1.1)
8 TDD tasks, 53 checkbox steps, sequentially dependent:
1. Workspace deps + rutster-tap-echo skeleton (build foundation)
2. Tap wire protocol (spec §3) — JSON envelope, explicit LE PCM codec
3. TapMetrics + TapAudioPipe (spec §4.1) — the seam object
4. TapClient WSS pump loop (spec §4.2) — async WS pump + seq gap detection
5. Rust echo brain crate (spec §2.3) — stereotype binary + in-process EchoServer
6. Call-model TapHandle + Channel field (spec §5.2, §6)
7. Binary wiring — TapEngine + session_map + routes + main (spec §5.1, §7)
8. Integration test + Python echo brain + LEARNING.md (spec §8.4, §8.5 #7)

Plan self-reviewed: no placeholders; TapHandle::new visibility fixed
(pub(crate) → pub); spawn_tap_engine return shape fixed (returns
(TapAudioPipe, TapConn) tuple, not TapConn holding the pipe's mpsc).

Spec ref: 2026-06-28-slice-2-agent-tap-design.md (commit f83bca9).
2026-06-28 13:54:57 -04:00
opencode controller
f83bca98db spec(slice-2): adversarial review patches
Some checks failed
CI / fmt (push) Failing after 41s
CI / clippy (push) Failing after 45s
CI / test (1.85) (push) Failing after 36s
CI / test (stable) (push) Failing after 39s
CI / deny (push) Failing after 31s
Patches the agent-tap design with seven findings from PM-side review
before the implementation plan gets written. Resolves invariant-breakers
and deferred-decision debt that would compound across steps 3-6.

- P1 (high): §8.5 #6 'byte-identical loop_driver.rs' was the wrong bar
  for the seam test. Reworded: the trait-method *call sites* are
  unchanged; impl bodies are free to differ. That's what 'the seam
  held' actually means — the contract is the test.
- P2 (high): tap: Option<TapHandle> and ChannelState were parallel
  state machines without an invariant tying them. Added an explicit
  state-pair table: tap is Some iff state == Connected. Lookups tolerate
  transient inconsistency; the source-of-truth is the pair, not either
  field alone.
- P3 (medium): v1 wire format was host-endian (silent big-endian
  hazard on paper). Nailed LE explicitly via i16::to_le_bytes /
  i16::from_le_bytes. v2 can negotiate endianness if a big-endian
  brain ever materializes; v1 contract is LE-only. Added a §3.4
  byte-order invariant alongside the sample-count invariant.
- P4 (medium): wss:// URLs were accepted by schema but returned
  501 at connect time (UX: 2 s into a call). Now rejected at
  POST /v1/sessions with 400 + clear message — fail fast at
  session-create. Updated §4.4, §7.1, §7.3, §9, and the decisions
  table for consistency; removed all stale '501 at connect' claims.
- P5 (low): TapEngine spawn owner was unspecified. Added explicit
  ownership: session_map::drive_all_sessions (the binary's poll
  task) observes the Connected transition from run_poll_once and
  spawns the engine. Keeps loop_driver.rs behaviorally unchanged
  (the §8.5 #6 seam test still holds).
- P6 (low): 'two new deps' in §1.1 undercounted. Updated list:
  tokio-tungstenite, futures-util, url, serde_json (if not already
  pulled).
- P7 (low): ADR-0007 was 'post-implementation follow-up, not a
  slice-2 deliverable.' Tightened: strongly recommended to land
  alongside or immediately after slice-2 implementation — once
  step 3 ships against an unpinned protocol, the wire shape will
  silently drift. The spec lists the decisions ADR-0007 should
  capture (LE byte order added to the list).
2026-06-28 13:41:54 -04:00
opencode controller
11e72fa733 spec(slice-2): agent tap design
Some checks failed
CI / fmt (push) Failing after 31s
CI / clippy (push) Failing after 44s
CI / test (1.85) (push) Failing after 39s
CI / test (stable) (push) Failing after 35s
CI / deny (push) Has been cancelled
Slice 2 hardens the presumptive agent-tap shape from ARCHITECTURE.md §"Agent tap"
(PORT_PLAN §10 open decision) against a working implementation:

- Replace slice-1's in-process EchoAudioPipe with a real out-of-process brain
  reached over WSS; core-as-client, no inbound tap port (ADR-0006 posture).
- Versioned JSON event protocol (v1) over WS text frames with base64 PCM.
  Event-named + JSON-over-text-WS so the step-3 OpenAI-Realtime adapter is a
  translation shim, not a gRPC-bridge project.
- Decoupled TapEngine (cold-path tokio task) owns the WS connection; TapAudioPipe
  is a thin sync wrapper the AudioSource/AudioSink seam holds. RtcSession's media
  loop is byte-identical to slice-1 — the seam-test payoff.
- Core-authoritative playout buffer (5-frame/100ms ring; drop-oldest on overflow,
  silence on underflow) — the place where "brain proposes, core disposes" lives.
- Bounded-backoff reconnect (250ms→5s cap, infinite retries); Channel stays
  Connected; stateless-brain reconnect contract.
- Both a Python reference echo brain (examples/echo_brain/, not in CI) and a
  Rust echo brain crate (crates/rutster-tap-echo, the in-process test server
  + standalone dev binary).
- ws:// loopback-only enforced; wss:// URL accepted at schema, 501 at connect
  (cert/mTLS impl deferred to step 6).

Out-of-scope table (§1.2) locks the spearhead sequencing: real brain (3),
barge-in/VAD-driven playout kill (4), PSTN trunk (5), spend cap (6), wss:// TLS
posture (6), binary PCM mode (future-rung), byte-endian negotiation (tracked
open decision §9).
2026-06-28 13:37:57 -04:00
9fcdb60548 Merge pull request 'slice-1: WebRTC media loopback — the media-core proof' (#1) from slice-1-webrtc-loopback into main
Some checks failed
CI / fmt (push) Failing after 35s
CI / clippy (push) Failing after 39s
CI / test (1.85) (push) Failing after 41s
CI / test (stable) (push) Failing after 44s
CI / deny (push) Failing after 34s
Reviewed-on: #1
2026-06-28 17:20:23 +00:00
opencode controller
52462723d3 Merge remote-tracking branch 'origin/main' into slice-1-webrtc-loopback
Some checks failed
CI / fmt (pull_request) Failing after 35s
CI / clippy (pull_request) Failing after 41s
CI / test (1.85) (pull_request) Failing after 36s
CI / test (stable) (pull_request) Failing after 39s
CI / deny (pull_request) Failing after 44s
# Conflicts:
#	README.md
2026-06-28 13:19:16 -04:00
adlee-was-taken
3041036d94 fix: restore doc bar on RtcSession accessors; refresh stale README status
Some checks failed
CI / fmt (pull_request) Failing after 42s
CI / clippy (pull_request) Failing after 1m19s
CI / test (1.85) (pull_request) Failing after 49s
CI / test (stable) (pull_request) Failing after 29s
CI / deny (pull_request) Failing after 46s
Two Important findings from the final whole-branch review:

T4-M3: RtcSession::channel_id/channel_state/is_closed lacked /// docs
(AGENTS.md mandates /// on every public item; learner-facing code is a
primary product). One-line docs added per existing /// style in rtc_session.rs.

T7-M2: README "Status" section still read "Scoping. No code yet" — stale
post-slice-1. Updated to reflect slice-1 implementation status; minimal
edit, surrounding content preserved.
2026-06-28 13:13:27 -04:00
adlee-was-taken
85a58ab880 deny: workaround cargo-deny 0.18.x GPL-3.0-or-later parse bug
cargo-deny 0.18.3 (the latest that installs on rust 1.85) can't parse
the SPDX 'GPL-3.0-or-later' suffix — 'expected a <bare-gnu-license>'.
CI uses cargo-deny-action@v1 (bundles 0.19+) which parses it correctly.

Switching to bare 'GPL-3.0' lets local 'cargo deny check' work on the
pinned toolchain. ADR-0004's GPL-3.0-or-later stance is unchanged; this
is a parser workaround flagged for revert when the MSRV lifts past 1.88
(where cargo-deny 0.19.x installs natively).

Three of four 'cargo deny check' subcommands now pass locally:
  - licenses ok
  - bans ok
  - sources ok
  - advisories: fails locally only because the advisory-db contains CVSS 4.0
    entries (anchor-lang RUSTSEC-2026-0146) that cargo-deny 0.18.3's parser
    rejects. Unfixable on the rust 1.85 toolchain; CI's 0.19+ parses fine.

Surfaced by Task 7's done-checklist run; not a Task 7 code change.
2026-06-28 13:08:19 -04:00
adlee-was-taken
f765fc2b27 docs: LEARNING.md + fuzz/ placeholder + README dev-loop (spec §7, §6.3)
LEARNING.md indexes ten concept-to-file pointers (the spec floor was
five) — the newtype pattern, exhaustive enum match, sans-IO, trait
extension seams, thiserror + hot-path match-and-continue, Arc<Mutex>
vs Arc<RwLock>, DashMap, str0m's single-mutation invariant, graceful
shutdown, include_str!. fuzz/README.md pre-paves the layout (no
hostile-bytes surface in slice 1; harnesses land at step 5 per the
out-of-scope table). README's new dev-loop section documents the
libopus FFI prerequisite and the manual e2e steps.
2026-06-28 13:06:37 -04:00
adlee-was-taken
f92e6f467b ci: cargo-deny + GitHub Actions workflow (spec §6.1, §6.2)
deny.toml allows the permissive Rust-ecosystem licenses + our own
GPL-3.0-or-later; bans duplicate versions of tokio/serde/bytes/tracing
to catch dep-tree divergence early; restricts sources to crates.io. CI
runs fmt --check, clippy -D warnings, test --all (matrix: stable +
MSRV 1.85), and cargo deny check on push + PR to main. The CI job
installs libopus-dev — the opus crate's FFI dependency (PORT_PLAN §7
'Core (FFI)' disposition).
2026-06-28 12:59:29 -04:00
adlee-was-taken
1f0f64c1c6 binary: axum signaling + DashMap session store + browser test client
Four routes on axum 0.7 per spec §4.1: POST /v1/sessions (mint),
POST /v1/sessions/:id/offer (str0m-native SDP accept), DELETE
/v1/sessions/:id (close), GET / (static HTML client). Session store is
a DashMap<ChannelId, Arc<Mutex<RtcSession>>> (spec §4.5); one tokio
task drives all session poll loops — per-session tasks would pre-pave
the wrong pattern for step 4's dedicated thread. Graceful shutdown
drops the DashMap on Ctrl-C / SIGTERM. Integration test exercises the
REST surface; manual browser e2e per README §6.5.
2026-06-28 12:48:02 -04:00
adlee-was-taken
34c590b48a media: RtcSession + str0m poll loop (the media core)
RtcSession owns a str0m::Rtc + Opus decoder/encoder + EchoAudioPipe +
a bound UDP socket (spec §3.1, §4.5). accept_offer calls str0m 0.21's
sdp_api().accept_offer() natively — no hand-rolled SDP munger; str0m
fills DTLS fingerprint + ICE creds + Opus codec. The loop driver
drains poll_output per str0m's single-mutation invariant, routes
inbound MediaData through Opus decode + EchoAudioPipe sink, sends
Transmit packets on the UDP socket, and checks the 60 s idle timeout.

DEV-DEVIATION: loop runs on tokio (spec §3.4); step 4 replaces with
a dedicated timing thread per ARCHITECTURE.md.

str0m 0.21 API adjustments from the brief's sketch (verified against
src/str0m-0.21.0):
- SdpAnswer has no .mid() accessor; use answer.media_lines[0].mid()
  via the Deref<Target=Sdp> impl.
- Input::Receive carries the Instant as the first tuple element
  (Input::Receive(now, recv)); handle_input takes a single Input arg.
- Receive constructed via Receive::new(proto, src, dst, buf) (the
  DatagramRecv field is private).
- UDP socket binds 127.0.0.1:0 (Candidate::host rejects the
  unspecified address 0.0.0.0).
- BROWSER_SDP_OFFER fixture restored a=group:BUNDLE 0 (str0m's
  Sdp::assert_consistency rejects SDP without a session group).
2026-06-28 12:39:57 -04:00
adlee-was-taken
27fb64aad5 lock: pin transitive rcgen/time deps for rust 1.85
str0m 0.21's default crypto backend pulls rcgen 0.14.8 and time
0.3.51, both of which require rustc 1.88+. The workspace toolchain
is pinned to 1.85 (edition 2024 floor, the str0m MSRV). Pin rcgen
to 0.14.7 and time to 0.3.41 in Cargo.lock so the str0m tree
builds on the pinned toolchain. No source changes; this only
restricts transitive versions.
2026-06-28 12:39:44 -04:00
adlee-was-taken
4c0898cd49 docs: QUICKSTART + DEVELOPMENT + CONTRIBUTING, polish README index
Builds out the user-facing docs tree alongside the slice-1 build target.
Kept the implementer's planned Task 7 'Slice 1 dev loop' README section
untouched — these docs are the canonical destination for that pointer.

- docs/QUICKSTART.md: 5-min path to 'hear the echo' (libopus install,
  cargo run, browser steps, troubleshooting, what's happening under the
  hood).
- docs/DEVELOPMENT.md: dev loop — workspace layout, per-crate iteration,
  running tests, the 20 ms loop / 'drop + observe' rule, slice-1
  boundaries (what NOT to add yet).
- CONTRIBUTING.md (at repo root, conventional): trunk-based dev,
  CI gates, commit message style, atomic commits, code style +
  learner-facing documentation policy, terminology policy, PR workflow
  + review checklist, GPL-3.0-or-later license.
- README.md: add a Quickstart pointer at the top, a Documentation table
  linking to every doc, and the slice-1 build-target status block.
2026-06-28 12:32:12 -04:00
adlee-was-taken
e09af55e00 media: PcmFrame + AudioSource/Sink + Opus codec pair
PcmFrame is the canonical tap format (16-bit mono @ 24 kHz, 480 samples
per 20 ms frame — ARCHITECTURE.md). AudioSource/AudioSink are the seam
step 2 splices the tap client into (spec §3.3); EchoAudioPipe is the
slice-1 wiring of that seam. OpusDecoder/OpusEncoder wrap the opus
crate's libopus FFI with hot-path match-and-continue (no ? on the 20 ms
loop, spec §3.8); decode/encode return Option<PcmFrame>/Option<Vec<u8>>
so a dropped frame is logged + counted, never propagated to crash the
peer.
2026-06-28 12:22:29 -04:00
adlee-was-taken
8260c76824 toolchain: bump pinned Rust 1.80 → 1.85 (edition 2024 floor)
uuid 1.x → getrandom 0.4.x requires edition = "2024", stabilized in Rust 1.85.
The Task 1 pin of 1.80 was written when uuid 1.x resolved to a getrandom that
still compiled on edition 2021; the resolution has shifted. The brief said
'confirm the latest stable at impl time' — bumping the pin is faithful to that.

Plan doc's Task 6 CI matrix also updated: [stable, "1.85"].

Cross-task correction in response to Task 2 implementer's DONE_WITH_CONCERNS
escalation; not a Task 2 code change.
2026-06-28 11:29:46 -04:00
adlee-was-taken
a5d0f90fe2 call-model: Channel + ChannelId + ChannelState (signaling embryo)
rutster-call-model is real-but-minimal (spec §5): the unifying leg
object the future API exposes. ChannelId is a Uuid newtype for
type-safety (the slice-1 worked example of the newtype pattern).
Channel is signaling-state only — media lives in rutster-media as a
leaf concern of the Channel, surfaced only when a second consumer needs
to observe it (spec §5.3). ChannelState matches the New→Connecting→
Connected→Closing→Closed flow from §5.4.
2026-06-28 11:27:55 -04:00
adlee-was-taken
39245a6553 workspace: scaffold + three stub crates (sip/tap/spend)
Workspace root, pinned toolchain, and the three stub crates whose only
job in slice 1 is to lock the ADR-0002 boundary shape. Each ships a
lib.rs module doc (what it will hold, why deferred, which spearhead step
fills it) and a crate_compiles test. Spec §2.2.
2026-06-28 11:22:52 -04:00
adlee-was-taken
470a22a6b7 git: ignore local worktree directories
Per using-git-worktrees skill — local .worktrees/ is for isolated
development workspaces, not tracked content. .
2026-06-28 11:20:58 -04:00
adlee-was-taken
3c5cc75015 plan(slice-1): adversarial review patches
Verified str0m 0.21 + opus 0.3.1 API surfaces via docs.rs subagents;
patched the plan with the real signatures and several design-level fixes.

API-surface patches (str0m 0.21, verified):
- Global Constraints: full verified API surface documented inline
  (Rtc::new takes Instant; SdpOffer::from_sdp_string is the entry
  point, NOT from_str_unchecked; add_local_candidate returns
  Option<&Candidate>; Writer::write takes rtp_time not media_time;
  MediaTime has no add(Duration) -> use mt + MediaTime::from(d);
  payload_params returns impl Iterator; MediaData.data is Arc<[u8]>).
- Task 4 accept_offer: rewritten with from_sdp_string + correct error
  mapping; RtcSessionError::SdpOffer changed to String (collapses parse
  + accept failures uniformly).
- Task 4 loop_driver: MediaTime::add -> + MediaTime::from(Duration);
  media.data deref coercion documented.

Design-level patches:
- Task 4: RtcSession::new_for_test -> pub fn new() (single idiomatic
  constructor, no test/prod split).
- Task 4: added accept_offer_transitions_channel_to_connecting test
  (the transition was claimed but untested in the original plan).
- Task 5: dropped reqwest from workspace deps (unused — integration
  test uses tower::ServiceExt::oneshot); added tower as workspace dep
  + binary crate dev-dependency.
- Task 5: removed duplicate DELETE /v1/sessions route (was incorrectly
  chained via .delete() on the collection route AND on /v1/sessions/:id
  — only the latter is correct).
- Task 5: clarified pub mod requirement (must be pub because the
  integration test references modules via absolute paths).
- Global Constraints: added task/PR strategy (one commit per task,
  merged in numeric order, granular history is load-bearing for the
  learning-codebase goal).

Self-review section updated to reflect the patches.

Plan is now implementation-ready.
2026-06-28 11:19:41 -04:00